Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
202 changes: 202 additions & 0 deletions .github/workflows/track-review-project.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,202 @@
# ------------------------------------------------------------------------------
# (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" (i.e., a non-Draft PR).
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_org:
description: 'The organization that owns the project'
required: false
type: string
default: 'MetOffice'
project_number:
description: 'The project number'
required: false
type: number
default: 376 # Simulation Systems Review Tracker

permissions:
actions: read
contents: read
pull-requests: write

jobs:
track_project:
runs-on: ${{ inputs.runner }}
timeout-minutes: 5
env:
REPO: ${{ github.repository }}

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 }}
run: |
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: Extract reviewers and determine state
env:
GH_TOKEN: ${{ github.token }}
run: |
pr_data=$(gh pr view -R "$REPO" "$PR_NUM" --json body,reviews,reviewRequests)

# 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([\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}"
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')

# 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')

[[ ! -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
if [[ "$sr_approved" -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
echo "REQUIRED_STATE=$required_state" >> $GITHUB_ENV
exit 0
fi
fi

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
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

- name: Request SciTech review, if needed
if: env.SR_REQUEST_REQUIRED == 'true' && env.REQUIRED_STATE == 'SciTech Review' && env.SCITECH_REVIEWER != ''
env:
GH_TOKEN: ${{ github.token }}
run: |
gh pr edit -R "$REPO" "$PR_NUM" --add-reviewer "$SCITECH_REVIEWER"

- name: Request code review, if needed
if: env.CR_REQUEST_REQUIRED == 'true' && env.REQUIRED_STATE == 'Code Review' && env.CODE_REVIEWER != ''
env:
GH_TOKEN: ${{ github.token }}
run: |
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 } }
}
}
}
}
}' -f org=${{ inputs.project_org }} -F number=${{ inputs.project_number }})

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 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 }
}
}' -f project="$project_id" -f pr="$PR_ID" --jq '.data.addProjectV2ItemById.item.id')

# 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_field_id" -f cr_value="$CODE_REVIEWER" -f sr_field="$scitech_field_id" -f sr_value="$SCITECH_REVIEWER" --silent
34 changes: 34 additions & 0 deletions .github/workflows/trigger-project-workflow.yaml
Original file line number Diff line number Diff line change
@@ -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
56 changes: 56 additions & 0 deletions track-review-project/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# 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.

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.

## Usage

```yaml
name: Track Review Project

on:
workflow_run:
workflows: [Trigger Review Project]
types:
- completed
workflow_dispatch:

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_org: "MetOffice"
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@main
secrets: inherit
```
Loading