Skip to content

Scheduled PR automation #579

Scheduled PR automation

Scheduled PR automation #579

# This workflow runs on a schedule
# It finds all the PR owned by neurobagel
# that have the _bot labels applied.
# It then checks if the PR has already been added to
# the main project and adds the PR if it hasn't been added yet.
# Finally the workflow moves the PR to the "Automation" column
# on the project board.
name: Scheduled PR automation
on:
schedule:
# * is a special character in YAML so you have to quote this string
- cron: '30 5,17 * * *'
workflow_dispatch:
jobs:
get_prs:
name: Get all PRs with the _bot labels
runs-on: ubuntu-latest
outputs:
urls: ${{ steps.get_pr.outputs.pr_urls }}
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ vars.NB_BOT_ID }}
private-key: ${{ secrets.NB_BOT_KEY }}
- name: Set GH_TOKEN
run: echo "GH_TOKEN=${{ steps.generate-token.outputs.token }}" >> $GITHUB_ENV
- name: Get current repositories
id: get_pr
env:
MAX_REPO: 50
run: |
pr_urls=$(gh search prs --owner neurobagel label:_bot --state open --json 'url' | jq -c '[.[].url]')
echo "pr_urls=${pr_urls}" >> $GITHUB_OUTPUT
process_prs:
name: Process PR
runs-on: ubuntu-latest
permissions:
pull-requests: write
needs: get_prs
strategy:
fail-fast: false
matrix:
url: ${{fromJSON(needs.get_prs.outputs.urls)}}
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ vars.NB_BOT_ID }}
private-key: ${{ secrets.NB_BOT_KEY }}
# To avoid setting the GH_TOKEN in every step, we set it as an environment variable
- name: Set GH_TOKEN
run: echo "GH_TOKEN=${{ steps.generate-token.outputs.token }}" >> $GITHUB_ENV
- name: Check if PR is in project
id: in_project
continue-on-error: true
run: |
gh pr view ${{ matrix.url }} --json 'projectItems' | jq -r '.projectItems[0].title' | grep Neurobagel
- name: Add PR to project
if: steps.in_project.outcome == 'failure'
run: |
gh pr edit ${{ matrix.url }} --add-project Neurobagel
# Note that in contrast to the graphical github UI,
# once a PR (or other item) is added to a github project,
# project related changes are no longer applied directly to the PR
# but instead to the project card that contains the PR.
# Once the PR is added to the project (and thus has a project card)
# we therefore have to search for the node id of the containing project card
# and then set the Status and Community option on the project card.
#
# This step expects to find the id of the (parent) project card
# and will fail (crashing the entire workflow) otherwise
- name: Find project node id
id: project_node
run: |
# Get the node ID of the PR
node_id=$(gh pr view ${{ matrix.url }} --json 'id' | jq -r '.id')
parent_id=$(
newCursor=""
while true; do
response=$(gh api graphql -f query='{
organization(login: "neurobagel") {
projectV2(number: 1) {
items(first: 100, orderBy: {field: POSITION, direction: DESC}, after: "'"${newCursor}"'") {
edges {
cursor
node {
content {
... on PullRequest {
childID: id
}
}
parentID: id
}
}
}
}
}
}')
# Because we may not be able to find the parent ID in the first 100 items
# we have to keep advancing the cursor to move through the list
# Note: we use 100 items to balance speed and API limits, might have to be changed
while read -r pID cID cursor; do
if [ "$cID" == "${node_id}" ];
then
echo $pID;
exit 0;
fi
newCursor="$cursor"
# Note: we need to use the here string
# to avoid running the while loop in a subshell that would not let us access newCursor
# after the while loop has finished
# see: https://tldp.org/LDP/abs/html/subshells.html
done <<< $(echo "$response" | jq -r '.data.organization.projectV2.items.edges[] | "\(.node.parentID) \(.node.content.childID) \(.cursor)"')
if [ ! -n "$newCursor" ]; then
# We have passed through the entire list of items
# and didn't find the project card for our PR.
# Something is wrong and we will now crash the workflow.
exit 1;
fi
done
)
echo "parent_id=${parent_id}" >> $GITHUB_OUTPUT
# This step expects a custom field called "Status"
# with an option called "Automation" to exist in the project.
# We need their IDs as input for our later call to the API
# to move our PR project card to the "Automation" column.
#
# We make them available to other steps in this job
# by writing them to the GITHUB_OUTPUT environment variable.
# see: https://docs.github.com/en/actions/using-jobs/defining-outputs-for-jobs
- name: Get IDs for Status field and Automation option
id: get_id
env:
FIELD: "Status"
OPTION: "Automation"
run: |
response=$(gh api graphql -f query='{
organization(login: "neurobagel") {
projectV2(number: 1) {
field(name: "'"${FIELD}"'") {
... on ProjectV2SingleSelectField {
fieldID: id
options(names: "'"${OPTION}"'") {
optionID: id
}
}
}
}
}
}' | jq '.data.organization.projectV2.field | "\(.fieldID) \(.options[0].optionID)"')
read fieldID optionID <<< "${response//\"}"
echo "fieldID=${fieldID}" >> $GITHUB_OUTPUT
echo "optionID=${optionID}" >> $GITHUB_OUTPUT
- name: Set "Status" of PR to "Automation"
run: |
gh api graphql -f query='mutation {
updateProjectV2ItemFieldValue(
input: {projectId: "PVT_kwDOBaeejM4AAQiP", itemId: "${{ steps.project_node.outputs.parent_id }}", fieldId: "${{ steps.get_id.outputs.fieldID }}", value: {singleSelectOptionId: "${{ steps.get_id.outputs.optionID }}"}}
) {
clientMutationId
}
}'