From 9e5452e06697323a9bfc1c9e864d49324ba35829 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Wed, 7 Jan 2026 11:41:40 +0000 Subject: [PATCH 1/9] add track reviews action --- .github/workflows/track-review-project.yaml | 226 ++++++++++++++++++++ track-review-project/README.md | 34 +++ 2 files changed, 260 insertions(+) create mode 100644 .github/workflows/track-review-project.yaml create mode 100644 track-review-project/README.md diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml new file mode 100644 index 0000000..47af826 --- /dev/null +++ b/.github/workflows/track-review-project.yaml @@ -0,0 +1,226 @@ +# This workflow runs whenever a pull request in the repository is marked as "ready for review". +name: Track Review Project +on: + workflow_call: + inputs: + runner: + description: 'The runner to use for the job' + required: false + type: string + default: 'ubuntu-24.04' + project_secret: + description: 'The name of the secret in the repository with access to projects' + required: false + type: string + default: 'PROJECT_ACTION_PAT' + +jobs: + track_project: + # Read reviewers from PR Summary + runs-on: ${{ inputs.runner }} + timeout-minutes: 2 + + steps: + # Add the PR Author as the Assignee if no assignees + - name: Assign Author + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + GH_TOKEN: ${{ github.token }} + AUTHOR: ${{ github.event.pull_request.user.login }} + run: | + assignees=$(gh pr view -R "$REPO" "$PR_NUMBER" --json "assignees" | jq '.assignees | length') + if [[ "$assignees" -eq 0 ]]; then + gh pr edit -R "$REPO" "$PR_NUMBER" --add-assignee "$AUTHOR" + fi + + - name: Read Reviewers + env: + PR_BODY: ${{ github.event.pull_request.body }} + PR_NUMBER: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + GH_TOKEN: ${{ github.token }} + run: | + echo "Running on PR #$PR_NUMBER in Repository $REPO" + + SCITECH_REVIEWER=$(echo "$PR_BODY" | grep -oP "Sci/Tech Reviewer:\s?@\K([a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38})") || true + CODE_REVIEWER=$(echo "$PR_BODY" | grep -oP "Code Reviewer:\s?@\K([a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38})") || true + + echo "Expected SciTech Reviewer: $SCITECH_REVIEWER" + echo "Expected Code Reviewer: $CODE_REVIEWER" + + echo "SCITECH_REVIEWER=$SCITECH_REVIEWER" >> $GITHUB_ENV + echo "CODE_REVIEWER=$CODE_REVIEWER" >> $GITHUB_ENV + + # Get Reviews by the SR and CR + - name: Get reviews and determine state + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + GH_TOKEN: ${{ github.token }} + run: | + reviews=$(gh pr view -R "$REPO" "$PR_NUMBER" --json "reviews,reviewRequests") + sr_requested=$(echo "$reviews" | jq -r --arg sr "$SCITECH_REVIEWER" '[ .reviewRequests[] | select(.login == $sr) ] | length') + cr_requested=$(echo "$reviews" | jq -r --arg cr "$CODE_REVIEWER" '[ .reviewRequests[] | select(.login == $cr) ] | length') + + # If there is no SR set and the CR review has been requested, assume this doesn't need an SR + if [[ -z "$SCITECH_REVIEWER" ]] && [[ "$cr_requested" -gt 0 ]]; then + echo "No SR set and CR requested so assuming this is trivial" + else + sr_reviews=$(echo "$reviews" | jq -r --arg sr "$SCITECH_REVIEWER" '[ .reviews[] | select(.author.login == $sr) ]') + sr_approved=$(echo "$sr_reviews" | jq -r '[ .[] | select(.state == "APPROVED") ] | length') + if [[ "$sr_approved" -eq 0 ]]; then + sr_rejected=$(echo "$sr_reviews" | jq -r '[ .[] | select((.state == "CHANGES_REQUESTED") or .state == "COMMENTED") ] | length') + if [[ "$sr_rejected" -gt 0 ]] && [[ "$sr_requested" -eq 0 ]]; then + required_state="Changes Requested" + else + required_state="SciTech Review" + fi + # Correct state set + echo "REQUIRED_STATE=$required_state" >> $GITHUB_ENV + exit 0 + fi + fi + + cr_reviews=$(echo "$reviews" | jq -r --arg cr "$CODE_REVIEWER" '[ .reviews[] | select(.author.login == $cr) ]') + cr_approved=$(echo "$cr_reviews" | jq -r '[ .[] | select(.state == "APPROVED") ] | length') + cr_rejected=$(echo "$cr_reviews" | jq -r '[ .[] | select((.state == "CHANGES_REQUESTED") or .state == "COMMENTED") ] | length') + if [[ "$cr_approved" -gt 0 ]]; then + required_state="Approved" + else + if [[ "$cr_rejected" -gt 0 ]] && [[ "$cr_requested" -eq 0 ]]; then + required_state="Changes Requested" + else + required_state="Code Review" + fi + fi + + echo "REQUIRED_STATE=$required_state" >> $GITHUB_ENV + if [[ "$cr_requested" -eq 0 ]] && [[ "$cr_approved" -eq 0 ]] && [[ "$cr_rejected" -eq 0 ]]; then + echo "CR_REQUEST_REQUIRED=true" >> $GITHUB_ENV + fi + + # If CR hasn't been requested and state is CR then add them as a reviewer + - name: Request CR review + env: + PR_NUMBER: ${{ github.event.pull_request.number }} + REPO: ${{ github.repository }} + GH_TOKEN: ${{ github.token }} + run: | + if [[ -z "$CR_REQUEST_REQUIRED" ]]; then + CR_REQUEST_REQUIRED=false + fi + if [[ "$CR_REQUEST_REQUIRED" = true ]] && [[ "$REQUIRED_STATE" == "Code Review" ]]; then + gh pr edit -R "$REPO" "$PR_NUMBER" --add-reviewer "$CODE_REVIEWER" + fi + + # Get Project IDs for project and relevant fields + # Project Number hard coded to Simulation Systems Review Tracker + - name: Get project id's + env: + GH_TOKEN: secrets.${{ inputs.project_secret }} + ORGANIZATION: MetOffice + PROJECT_NUMBER: 376 + run: | + gh api graphql -f query=' + query($org: String!, $number: Int!) { + organization(login: $org){ + projectV2(number: $number) { + id + fields(first:20) { + nodes { + ... on ProjectV2Field { + id + name + } + ... on ProjectV2SingleSelectField { + id + name + options { + id + name + } + } + } + } + } + } + }' -f org=$ORGANIZATION -F number=$PROJECT_NUMBER > project_data.json + + echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV + echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV + echo 'STATUS_OPTION_ID='$(jq -r --arg status "$REQUIRED_STATE" '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name==$status) |.id' project_data.json) >> $GITHUB_ENV + echo 'SCITECH_REVIEW_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "SciTech Review") | .id' project_data.json) >> $GITHUB_ENV + echo 'CODE_REVIEW_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Code Review") | .id' project_data.json) >> $GITHUB_ENV + + # Add the PR to the Project - get item id for this PR + - name: Add PR to project + env: + GH_TOKEN: secrets.${{ inputs.project_secret }} + PR_ID: ${{ github.event.pull_request.node_id }} + run: | + item_id="$( gh api graphql -f query=' + mutation($project:ID!, $pr:ID!) { + addProjectV2ItemById(input: {projectId: $project, contentId: $pr}) { + item { + id + } + } + }' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectV2ItemById.item.id')" + + # Stores the ID of the created item as an environment variable. + echo 'ITEM_ID='$item_id >> $GITHUB_ENV + + # Sets Status, CR and SR Fields based on Output from previous steps + - name: Set fields + env: + GH_TOKEN: secrets.${{ inputs.project_secret }} + run: | + gh api graphql -f query=' + mutation ( + $project: ID! + $item: ID! + $status_field: ID! + $status_value: String! + $cr_field: ID! + $cr_value: String! + $sr_field: ID! + $sr_value: String! + ) { + set_status: updateProjectV2ItemFieldValue(input: { + projectId: $project + itemId: $item + fieldId: $status_field + value: { + singleSelectOptionId: $status_value + } + }) { + projectV2Item { + id + } + } + set_code_review: updateProjectV2ItemFieldValue(input: { + projectId: $project + itemId: $item + fieldId: $cr_field + value: { + text: $cr_value + } + }) { + projectV2Item { + id + } + } + set_scitech_review: updateProjectV2ItemFieldValue(input: { + projectId: $project + itemId: $item + fieldId: $sr_field + value: { + text: $sr_value + } + }) { + projectV2Item { + id + } + } + }' -f project=$PROJECT_ID -f item=$ITEM_ID -f status_field=$STATUS_FIELD_ID -f status_value=$STATUS_OPTION_ID -f cr_field=$CODE_REVIEW_ID -f cr_value="$CODE_REVIEWER" -f sr_field=$SCITECH_REVIEW_ID -f sr_value="$SCITECH_REVIEWER" --silent diff --git a/track-review-project/README.md b/track-review-project/README.md new file mode 100644 index 0000000..ac058ac --- /dev/null +++ b/track-review-project/README.md @@ -0,0 +1,34 @@ +# Track Review Project Action + +This action modifies the Simulation Systems Review Tracker project entries in +pull requests. It will automatically fill the reviewer fields based on the +entries in the pull request template. It will change the state based on reviews +requested and performed. If a CR hasn't been requested, it will do so when +moving to the CR state. It will also add the PR author as an assignee if none +exist. + +It requires a PAT with permissions to write to projects to be added as a secret +to the repository - the default name of this secret is `PROJECT_ACTION_PAT` but +this can be overridden with the `project_pat` input. Project edits made using +this token will be shown as performed by the owner of the token. + +## Usage + +```yaml +name: Track Review Project + +on: + pull_request: + types: ["opened", "synchronize", "reopened", "edited", "review_requested", "review_request_removed"] + pull_request_review: + pull_request_review_comment: + workflow_dispatch: + +jobs: + track_review_project: + uses: MetOffice/growss/.github/workflows/track-review-project.yaml@main + # Optional inputs (with default values) + with: + runner: "ubuntu-22.04" + project_secret: "PROJECT_ACTION_PAT" +``` From d21e22e0df060f7e1c215ec58417a70baadbcb9c Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Wed, 7 Jan 2026 13:32:35 +0000 Subject: [PATCH 2/9] add copyright --- .github/workflows/track-review-project.yaml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index 47af826..bdf8c9b 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -1,3 +1,9 @@ +# ------------------------------------------------------------------------------ +# (c) Crown copyright Met Office. All rights reserved. +# The file LICENCE, distributed with this code, contains details of the terms +# under which the code may be used. +# ------------------------------------------------------------------------------ + # This workflow runs whenever a pull request in the repository is marked as "ready for review". name: Track Review Project on: From cef67ebb666cbfd26aa60b00eb1d2909a8c73103 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 8 Jan 2026 11:23:44 +0000 Subject: [PATCH 3/9] updates to track project action --- .github/workflows/track-review-project.yaml | 242 +++++++------------- track-review-project/README.md | 10 +- 2 files changed, 94 insertions(+), 158 deletions(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index bdf8c9b..a046649 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -4,7 +4,8 @@ # under which the code may be used. # ------------------------------------------------------------------------------ -# This workflow runs whenever a pull request in the repository is marked as "ready for review". +# This workflow runs whenever a pull request in the repository is marked as +# "Ready for review" (i.e., a non-Draft PR). name: Track Review Project on: workflow_call: @@ -14,219 +15,152 @@ on: required: false type: string default: 'ubuntu-24.04' - project_secret: - description: 'The name of the secret in the repository with access to projects' + project_org: + description: 'The organization that owns the project' required: false type: string - default: 'PROJECT_ACTION_PAT' + default: 'MetOffice' + project_number: + description: 'The project number' + required: false + type: number + default: 376 # Simulation Systems Review Tracker + +permissions: + contents: read + pull-requests: write jobs: track_project: - # Read reviewers from PR Summary runs-on: ${{ inputs.runner }} - timeout-minutes: 2 + timeout-minutes: 5 + env: + REPO: ${{ github.repository }} + PR_NUM: ${{ github.event.pull_request.number }} + PR_ID: ${{ github.event.pull_request.node_id }} steps: - # Add the PR Author as the Assignee if no assignees - - name: Assign Author + - name: Assign Author if no assignees env: - PR_NUMBER: ${{ github.event.pull_request.number }} - REPO: ${{ github.repository }} GH_TOKEN: ${{ github.token }} - AUTHOR: ${{ github.event.pull_request.user.login }} run: | - assignees=$(gh pr view -R "$REPO" "$PR_NUMBER" --json "assignees" | jq '.assignees | length') - if [[ "$assignees" -eq 0 ]]; then - gh pr edit -R "$REPO" "$PR_NUMBER" --add-assignee "$AUTHOR" + pr_data=$(gh pr view -R "$REPO" "$PR_NUM" --json assignees,author) + if [[ $(echo "$pr_data" | jq '.assignees | length') -eq 0 ]]; then + author=$(echo "$pr_data" | jq -r '.author.login') + gh pr edit -R "$REPO" "$PR_NUM" --add-assignee "$author" fi - - name: Read Reviewers + - name: Extract reviewers and determine state env: - PR_BODY: ${{ github.event.pull_request.body }} - PR_NUMBER: ${{ github.event.pull_request.number }} - REPO: ${{ github.repository }} GH_TOKEN: ${{ github.token }} run: | - echo "Running on PR #$PR_NUMBER in Repository $REPO" + pr_data=$(gh pr view -R "$REPO" "$PR_NUM" --json body,reviews,reviewRequests) - SCITECH_REVIEWER=$(echo "$PR_BODY" | grep -oP "Sci/Tech Reviewer:\s?@\K([a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38})") || true - CODE_REVIEWER=$(echo "$PR_BODY" | grep -oP "Code Reviewer:\s?@\K([a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38})") || true + # Extract reviewers from PR body + pr_body=$(echo "$pr_data" | jq -r '.body') + scitech_reviewer=$(echo "$pr_body" | grep -oP "Sci/Tech Reviewer:\s?@\K([a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38})" || true) + code_reviewer=$(echo "$pr_body" | grep -oP "Code Reviewer:\s?@\K([a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38})" || true) - echo "Expected SciTech Reviewer: $SCITECH_REVIEWER" - echo "Expected Code Reviewer: $CODE_REVIEWER" + echo "Expected SciTech Reviewer: ${scitech_reviewer:-None}" + echo "Expected Code Reviewer: ${code_reviewer:-None}" + echo "SCITECH_REVIEWER=$scitech_reviewer" >> $GITHUB_ENV + echo "CODE_REVIEWER=$code_reviewer" >> $GITHUB_ENV - echo "SCITECH_REVIEWER=$SCITECH_REVIEWER" >> $GITHUB_ENV - echo "CODE_REVIEWER=$CODE_REVIEWER" >> $GITHUB_ENV + # Determine review state + reviews=$(echo "$pr_data" | jq -c '{reviews, reviewRequests}') + sr_requested=$(echo "$reviews" | jq --arg sr "$scitech_reviewer" '[.reviewRequests[] | select(.login == $sr)] | length') + cr_requested=$(echo "$reviews" | jq --arg cr "$code_reviewer" '[.reviewRequests[] | select(.login == $cr)] | length') - # Get Reviews by the SR and CR - - name: Get reviews and determine state - env: - PR_NUMBER: ${{ github.event.pull_request.number }} - REPO: ${{ github.repository }} - GH_TOKEN: ${{ github.token }} - run: | - reviews=$(gh pr view -R "$REPO" "$PR_NUMBER" --json "reviews,reviewRequests") - sr_requested=$(echo "$reviews" | jq -r --arg sr "$SCITECH_REVIEWER" '[ .reviewRequests[] | select(.login == $sr) ] | length') - cr_requested=$(echo "$reviews" | jq -r --arg cr "$CODE_REVIEWER" '[ .reviewRequests[] | select(.login == $cr) ] | length') + # Determine required state + sr_approved=$(echo "$reviews" | jq --arg sr "$scitech_reviewer" '[.reviews[] | select(.author.login == $sr and .state == "APPROVED")] | length') + sr_changes=$(echo "$reviews" | jq --arg sr "$scitech_reviewer" '[.reviews[] | select(.author.login == $sr and (.state == "CHANGES_REQUESTED" or .state == "COMMENTED"))] | length') - # If there is no SR set and the CR review has been requested, assume this doesn't need an SR - if [[ -z "$SCITECH_REVIEWER" ]] && [[ "$cr_requested" -gt 0 ]]; then + [[ ! -z "$scitech_reviewer" ]] && [[ "$sr_requested" -eq 0 ]] && [[ "$sr_approved" -eq 0 ]] && [[ "$sr_changes" -eq 0 ]] && echo "SR_REQUEST_REQUIRED=true" >> $GITHUB_ENV + + if [[ -z "$scitech_reviewer" ]] && [[ "$cr_requested" -gt 0 ]]; then echo "No SR set and CR requested so assuming this is trivial" else - sr_reviews=$(echo "$reviews" | jq -r --arg sr "$SCITECH_REVIEWER" '[ .reviews[] | select(.author.login == $sr) ]') - sr_approved=$(echo "$sr_reviews" | jq -r '[ .[] | select(.state == "APPROVED") ] | length') if [[ "$sr_approved" -eq 0 ]]; then - sr_rejected=$(echo "$sr_reviews" | jq -r '[ .[] | select((.state == "CHANGES_REQUESTED") or .state == "COMMENTED") ] | length') - if [[ "$sr_rejected" -gt 0 ]] && [[ "$sr_requested" -eq 0 ]]; then + if [[ -z "$scitech_reviewer" ]] && [[ "$sr_changes" -eq 0 ]]; then + required_state="In Progress" + elif [[ "$sr_changes" -gt 0 ]] && [[ "$sr_requested" -eq 0 ]]; then required_state="Changes Requested" else required_state="SciTech Review" fi - # Correct state set echo "REQUIRED_STATE=$required_state" >> $GITHUB_ENV exit 0 fi fi - cr_reviews=$(echo "$reviews" | jq -r --arg cr "$CODE_REVIEWER" '[ .reviews[] | select(.author.login == $cr) ]') - cr_approved=$(echo "$cr_reviews" | jq -r '[ .[] | select(.state == "APPROVED") ] | length') - cr_rejected=$(echo "$cr_reviews" | jq -r '[ .[] | select((.state == "CHANGES_REQUESTED") or .state == "COMMENTED") ] | length') + cr_approved=$(echo "$reviews" | jq --arg cr "$code_reviewer" '[.reviews[] | select(.author.login == $cr and .state == "APPROVED")] | length') + cr_changes=$(echo "$reviews" | jq --arg cr "$code_reviewer" '[.reviews[] | select(.author.login == $cr and (.state == "CHANGES_REQUESTED" or .state == "COMMENTED"))] | length') + if [[ "$cr_approved" -gt 0 ]]; then required_state="Approved" + elif [[ "cr_changes" -eq 0 ]] && [[ "$cr_requested" -eq 0 ]] && [[ "$sr_approved" -eq 0 ]]; then + required_state="In Progress" + elif [[ "$cr_changes" -gt 0 ]] && [[ "$cr_requested" -eq 0 ]]; then + required_state="Changes Requested" else - if [[ "$cr_rejected" -gt 0 ]] && [[ "$cr_requested" -eq 0 ]]; then - required_state="Changes Requested" - else - required_state="Code Review" - fi + required_state="Code Review" + [[ "$cr_requested" -eq 0 ]] && [[ "$cr_approved" -eq 0 ]] && [[ "$cr_changes" -eq 0 ]] && echo "CR_REQUEST_REQUIRED=true" >> $GITHUB_ENV fi echo "REQUIRED_STATE=$required_state" >> $GITHUB_ENV - if [[ "$cr_requested" -eq 0 ]] && [[ "$cr_approved" -eq 0 ]] && [[ "$cr_rejected" -eq 0 ]]; then - echo "CR_REQUEST_REQUIRED=true" >> $GITHUB_ENV - fi - # If CR hasn't been requested and state is CR then add them as a reviewer - - name: Request CR review + - name: Request SciTech review, if needed + if: env.SR_REQUEST_REQUIRED == 'true' && env.REQUIRED_STATE == 'SciTech Review' && env.SCITECH_REVIEWER != '' env: - PR_NUMBER: ${{ github.event.pull_request.number }} - REPO: ${{ github.repository }} GH_TOKEN: ${{ github.token }} run: | - if [[ -z "$CR_REQUEST_REQUIRED" ]]; then - CR_REQUEST_REQUIRED=false - fi - if [[ "$CR_REQUEST_REQUIRED" = true ]] && [[ "$REQUIRED_STATE" == "Code Review" ]]; then - gh pr edit -R "$REPO" "$PR_NUMBER" --add-reviewer "$CODE_REVIEWER" - fi + gh pr edit -R "$REPO" "$PR_NUM" --add-reviewer "$SCITECH_REVIEWER" - # Get Project IDs for project and relevant fields - # Project Number hard coded to Simulation Systems Review Tracker - - name: Get project id's + - name: Request code review, if needed + if: env.CR_REQUEST_REQUIRED == 'true' && env.REQUIRED_STATE == 'Code Review' && env.CODE_REVIEWER != '' env: - GH_TOKEN: secrets.${{ inputs.project_secret }} - ORGANIZATION: MetOffice - PROJECT_NUMBER: 376 + GH_TOKEN: ${{ github.token }} run: | - gh api graphql -f query=' + gh pr edit -R "$REPO" "$PR_NUM" --add-reviewer "$CODE_REVIEWER" + + - name: Update project tracking + env: + GH_TOKEN: ${{ secrets.PROJECT_ACTION_PAT }} + run: | + # Get project data + project_data=$(gh api graphql -f query=' query($org: String!, $number: Int!) { organization(login: $org){ projectV2(number: $number) { id fields(first:20) { nodes { - ... on ProjectV2Field { - id - name - } - ... on ProjectV2SingleSelectField { - id - name - options { - id - name - } - } + ... on ProjectV2Field { id name } + ... on ProjectV2SingleSelectField { id name options { id name } } } } } } - }' -f org=$ORGANIZATION -F number=$PROJECT_NUMBER > project_data.json + }' -f org=${{ inputs.project_org }} -F number=${{ inputs.project_number }}) - echo 'PROJECT_ID='$(jq '.data.organization.projectV2.id' project_data.json) >> $GITHUB_ENV - echo 'STATUS_FIELD_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id' project_data.json) >> $GITHUB_ENV - echo 'STATUS_OPTION_ID='$(jq -r --arg status "$REQUIRED_STATE" '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name==$status) |.id' project_data.json) >> $GITHUB_ENV - echo 'SCITECH_REVIEW_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "SciTech Review") | .id' project_data.json) >> $GITHUB_ENV - echo 'CODE_REVIEW_ID='$(jq '.data.organization.projectV2.fields.nodes[] | select(.name== "Code Review") | .id' project_data.json) >> $GITHUB_ENV + project_id=$(echo "$project_data" | jq -r '.data.organization.projectV2.id') + status_field_id=$(echo "$project_data" | jq -r '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .id') + status_option_id=$(echo "$project_data" | jq -r --arg status "$REQUIRED_STATE" '.data.organization.projectV2.fields.nodes[] | select(.name== "Status") | .options[] | select(.name==$status) | .id') + scitech_field_id=$(echo "$project_data" | jq -r '.data.organization.projectV2.fields.nodes[] | select(.name== "SciTech Review") | .id') + code_field_id=$(echo "$project_data" | jq -r '.data.organization.projectV2.fields.nodes[] | select(.name== "Code Review") | .id') - # Add the PR to the Project - get item id for this PR - - name: Add PR to project - env: - GH_TOKEN: secrets.${{ inputs.project_secret }} - PR_ID: ${{ github.event.pull_request.node_id }} - run: | - item_id="$( gh api graphql -f query=' + # Add PR to project and update fields + item_id=$(gh api graphql -f query=' mutation($project:ID!, $pr:ID!) { addProjectV2ItemById(input: {projectId: $project, contentId: $pr}) { - item { - id - } + item { id } } - }' -f project=$PROJECT_ID -f pr=$PR_ID --jq '.data.addProjectV2ItemById.item.id')" - - # Stores the ID of the created item as an environment variable. - echo 'ITEM_ID='$item_id >> $GITHUB_ENV + }' -f project="$project_id" -f pr="$PR_ID" --jq '.data.addProjectV2ItemById.item.id') - # Sets Status, CR and SR Fields based on Output from previous steps - - name: Set fields - env: - GH_TOKEN: secrets.${{ inputs.project_secret }} - run: | + # Update all fields in single mutation gh api graphql -f query=' - mutation ( - $project: ID! - $item: ID! - $status_field: ID! - $status_value: String! - $cr_field: ID! - $cr_value: String! - $sr_field: ID! - $sr_value: String! - ) { - set_status: updateProjectV2ItemFieldValue(input: { - projectId: $project - itemId: $item - fieldId: $status_field - value: { - singleSelectOptionId: $status_value - } - }) { - projectV2Item { - id - } - } - set_code_review: updateProjectV2ItemFieldValue(input: { - projectId: $project - itemId: $item - fieldId: $cr_field - value: { - text: $cr_value - } - }) { - projectV2Item { - id - } - } - set_scitech_review: updateProjectV2ItemFieldValue(input: { - projectId: $project - itemId: $item - fieldId: $sr_field - value: { - text: $sr_value - } - }) { - projectV2Item { - id - } - } - }' -f project=$PROJECT_ID -f item=$ITEM_ID -f status_field=$STATUS_FIELD_ID -f status_value=$STATUS_OPTION_ID -f cr_field=$CODE_REVIEW_ID -f cr_value="$CODE_REVIEWER" -f sr_field=$SCITECH_REVIEW_ID -f sr_value="$SCITECH_REVIEWER" --silent + mutation($project: ID!, $item: ID!, $status_field: ID!, $status_value: String!, $cr_field: ID!, $cr_value: String!, $sr_field: ID!, $sr_value: String!) { + set_status: updateProjectV2ItemFieldValue(input: {projectId: $project, itemId: $item, fieldId: $status_field, value: {singleSelectOptionId: $status_value}}) { projectV2Item { id } } + set_code_review: updateProjectV2ItemFieldValue(input: {projectId: $project, itemId: $item, fieldId: $cr_field, value: {text: $cr_value}}) { projectV2Item { id } } + set_scitech_review: updateProjectV2ItemFieldValue(input: {projectId: $project, itemId: $item, fieldId: $sr_field, value: {text: $sr_value}}) { projectV2Item { id } } + }' -f project="$project_id" -f item="$item_id" -f status_field="$status_field_id" -f status_value="$status_option_id" -f cr_field="$code_field_id" -f cr_value="$CODE_REVIEWER" -f sr_field="$scitech_field_id" -f sr_value="$SCITECH_REVIEWER" --silent diff --git a/track-review-project/README.md b/track-review-project/README.md index ac058ac..7aafa72 100644 --- a/track-review-project/README.md +++ b/track-review-project/README.md @@ -8,8 +8,7 @@ moving to the CR state. It will also add the PR author as an assignee if none exist. It requires a PAT with permissions to write to projects to be added as a secret -to the repository - the default name of this secret is `PROJECT_ACTION_PAT` but -this can be overridden with the `project_pat` input. Project edits made using +to the repository with the name `PROJECT_ACTION_PAT`. Project edits made using this token will be shown as performed by the owner of the token. ## Usage @@ -18,7 +17,7 @@ this token will be shown as performed by the owner of the token. name: Track Review Project on: - pull_request: + pull_request_target: types: ["opened", "synchronize", "reopened", "edited", "review_requested", "review_request_removed"] pull_request_review: pull_request_review_comment: @@ -27,8 +26,11 @@ on: jobs: track_review_project: uses: MetOffice/growss/.github/workflows/track-review-project.yaml@main + secrets: inherit # Optional inputs (with default values) with: runner: "ubuntu-22.04" - project_secret: "PROJECT_ACTION_PAT" + project_org: "MetOffice" + project_number: 376 + ``` From d689b26bf4281dd22670ab9a982aa3b6e47f223b Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 8 Jan 2026 12:02:37 +0000 Subject: [PATCH 4/9] make case insensitive --- .github/workflows/track-review-project.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index a046649..9977868 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -58,8 +58,8 @@ jobs: # Extract reviewers from PR body pr_body=$(echo "$pr_data" | jq -r '.body') - scitech_reviewer=$(echo "$pr_body" | grep -oP "Sci/Tech Reviewer:\s?@\K([a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38})" || true) - code_reviewer=$(echo "$pr_body" | grep -oP "Code Reviewer:\s?@\K([a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38})" || true) + scitech_reviewer=$(echo "$pr_body" | grep -oiP "Sci/Tech Reviewer:\s?@\K([a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38})" || true) + code_reviewer=$(echo "$pr_body" | grep -oiP "Code Reviewer:\s?@\K([a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38})" || true) echo "Expected SciTech Reviewer: ${scitech_reviewer:-None}" echo "Expected Code Reviewer: ${code_reviewer:-None}" From 24240fb2f09770b2d35ef24c2e210122f337d375 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 8 Jan 2026 17:12:13 +0000 Subject: [PATCH 5/9] updates for forks --- .github/workflows/track-review-project.yaml | 42 +++++++++++++++++-- .../workflows/trigger-project-workflow.yaml | 34 +++++++++++++++ 2 files changed, 72 insertions(+), 4 deletions(-) create mode 100644 .github/workflows/trigger-project-workflow.yaml diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index 9977868..56216bd 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -27,8 +27,7 @@ on: default: 376 # Simulation Systems Review Tracker permissions: - contents: read - pull-requests: write + write-all jobs: track_project: @@ -36,10 +35,45 @@ jobs: timeout-minutes: 5 env: REPO: ${{ github.repository }} - PR_NUM: ${{ github.event.pull_request.number }} - PR_ID: ${{ github.event.pull_request.node_id }} steps: + # Required as running from the workflow_run trigger + - name: Checkout repo + uses: actions/checkout@v6 + + - name: Download PR data artifact + uses: actions/github-script@v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.payload.workflow_run.id, + }); + + let matchArtifact = allArtifacts.data.artifacts.filter((artifact) => { + return artifact.name == "context.json" + })[0]; + + let download = await github.rest.actions.downloadArtifact({ + owner: context.repo.owner, + repo: context.repo.repo, + artifact_id: matchArtifact.id, + archive_format: 'zip', + }); + + let fs = require('fs'); + fs.writeFileSync(`${process.env.GITHUB_WORKSPACE}/context.zip`, Buffer.from(download.data)); + + - name: Extract PR Data + run: | + unzip context.zip + pr_num=$(jq -r '.pr_num' context.json) + pr_id=$(jq -r '.pr_id' context.json) + echo "PR_NUM=$pr_num" >> $GITHUB_ENV + echo "PR_ID=$pr_id" >> $GITHUB_ENV + - name: Assign Author if no assignees env: GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/trigger-project-workflow.yaml b/.github/workflows/trigger-project-workflow.yaml new file mode 100644 index 0000000..6fa7e72 --- /dev/null +++ b/.github/workflows/trigger-project-workflow.yaml @@ -0,0 +1,34 @@ +# ------------------------------------------------------------------------------ +# (c) Crown copyright Met Office. All rights reserved. +# The file LICENCE, distributed with this code, contains details of the terms +# under which the code may be used. +# ------------------------------------------------------------------------------ + +# This workflow acts as a triggering workflow (via on: workflow_run) for the +# track-review-project workflow. It uploads an artifact containing PR data for +# the latter +name: Trigger Review Project + +on: + workflow_call: + +permissions: + contents: read + pull-requests: write + +jobs: + trigger_project: + runs-on: ubuntu-latest + timeout-minutes: 5 + env: + PR_NUM: ${{ github.event.pull_request.number }} + PR_ID: ${{ github.event.pull_request.node_id }} + steps: + - name: Triggering Workflow + run: | + echo "{\"pr_num\": $PR_NUM, \"pr_id\": \"$PR_ID\"}" >> context.json + - uses: actions/upload-artifact@v6 + with: + name: context.json + path: context.json + retention-days: 1 From 2425cea242012a043a7d39fbe1a906c262fc26cd Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Thu, 8 Jan 2026 17:15:55 +0000 Subject: [PATCH 6/9] update readme --- track-review-project/README.md | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/track-review-project/README.md b/track-review-project/README.md index 7aafa72..6d89c24 100644 --- a/track-review-project/README.md +++ b/track-review-project/README.md @@ -7,6 +7,11 @@ requested and performed. If a CR hasn't been requested, it will do so when moving to the CR state. It will also add the PR author as an assignee if none exist. +In order for this to work with forks when reviews are completed, a triggering +workflow is also required. This can be shown by the "Trigger Project Workflow" +yaml file below. The "Track Action Project" workflow is triggered by this +workflow via github "workflow_run" syntax. + It requires a PAT with permissions to write to projects to be added as a secret to the repository with the name `PROJECT_ACTION_PAT`. Project edits made using this token will be shown as performed by the owner of the token. @@ -17,10 +22,10 @@ this token will be shown as performed by the owner of the token. name: Track Review Project on: - pull_request_target: - types: ["opened", "synchronize", "reopened", "edited", "review_requested", "review_request_removed"] - pull_request_review: - pull_request_review_comment: + workflow_run: + workflows: [Trigger Review Project] + types: + - completed workflow_dispatch: jobs: @@ -34,3 +39,18 @@ jobs: project_number: 376 ``` + +```yaml +name: Trigger Review Project + +on: + pull_request_target: + types: ["opened", "synchronize", "reopened", "edited", "review_requested", "review_request_removed"] + pull_request_review: + pull_request_review_comment: + +jobs: + trigger_project_workflow: + uses: MetOffice/growss/.github/workflows/trigger-project-workflow.yaml@develop + secrets: inherit +``` From 1df7f771a5d079eba73f17074de04b4f8282036e Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Fri, 9 Jan 2026 08:10:31 +0000 Subject: [PATCH 7/9] update permissions --- .github/workflows/track-review-project.yaml | 4 +++- track-review-project/README.md | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index 56216bd..24c143e 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -27,7 +27,9 @@ on: default: 376 # Simulation Systems Review Tracker permissions: - write-all + actions: read + contents: read + pull-requests: write jobs: track_project: diff --git a/track-review-project/README.md b/track-review-project/README.md index 6d89c24..fd1d938 100644 --- a/track-review-project/README.md +++ b/track-review-project/README.md @@ -51,6 +51,6 @@ on: jobs: trigger_project_workflow: - uses: MetOffice/growss/.github/workflows/trigger-project-workflow.yaml@develop + uses: MetOffice/growss/.github/workflows/trigger-project-workflow.yaml@main secrets: inherit ``` From b046f1dafae370096fd6edf114467e956b91efe5 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Fri, 9 Jan 2026 13:58:16 +0000 Subject: [PATCH 8/9] update to use Sams regex --- .github/workflows/track-review-project.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index 24c143e..ed59884 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -94,8 +94,8 @@ jobs: # Extract reviewers from PR body pr_body=$(echo "$pr_data" | jq -r '.body') - scitech_reviewer=$(echo "$pr_body" | grep -oiP "Sci/Tech Reviewer:\s?@\K([a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38})" || true) - code_reviewer=$(echo "$pr_body" | grep -oiP "Code Reviewer:\s?@\K([a-z\d](?:[a-z\d]|-(?=[a-z\d])){0,38})" || true) + scitech_reviewer=$(echo "$pr_body" | grep -oiP "Sci/Tech Reviewer:\s?@\K([\w\-]{1,39})\b" || true) + code_reviewer=$(echo "$pr_body" | grep -oiP "Code Reviewer:\s?@\K([\w\-]{1,39})\b" || true) echo "Expected SciTech Reviewer: ${scitech_reviewer:-None}" echo "Expected Code Reviewer: ${code_reviewer:-None}" From ce92e77b28efb0683532b31b5af3a6afa8331713 Mon Sep 17 00:00:00 2001 From: James Bruten <109733895+james-bruten-mo@users.noreply.github.com> Date: Fri, 9 Jan 2026 13:58:54 +0000 Subject: [PATCH 9/9] remove -i --- .github/workflows/track-review-project.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/track-review-project.yaml b/.github/workflows/track-review-project.yaml index ed59884..ff91537 100644 --- a/.github/workflows/track-review-project.yaml +++ b/.github/workflows/track-review-project.yaml @@ -94,8 +94,8 @@ jobs: # Extract reviewers from PR body pr_body=$(echo "$pr_data" | jq -r '.body') - scitech_reviewer=$(echo "$pr_body" | grep -oiP "Sci/Tech Reviewer:\s?@\K([\w\-]{1,39})\b" || true) - code_reviewer=$(echo "$pr_body" | grep -oiP "Code Reviewer:\s?@\K([\w\-]{1,39})\b" || true) + scitech_reviewer=$(echo "$pr_body" | grep -oP "Sci/Tech Reviewer:\s?@\K([\w\-]{1,39})\b" || true) + code_reviewer=$(echo "$pr_body" | grep -oP "Code Reviewer:\s?@\K([\w\-]{1,39})\b" || true) echo "Expected SciTech Reviewer: ${scitech_reviewer:-None}" echo "Expected Code Reviewer: ${code_reviewer:-None}"