Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of project board automations #3375

Merged
merged 51 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
51 commits
Select commit Hold shift + click to select a range
f57cfe1
Replace `@octokit/rest` with `octokit`
dhruvkb Nov 18, 2023
cfd9dd0
Create a util to get authenticated Octokit
dhruvkb Nov 18, 2023
6a69665
Create a wrapper over GitHub's GraphQL APIs for Project v2
dhruvkb Nov 18, 2023
6323f04
Install `yargs` to parse CLI arguments
dhruvkb Nov 20, 2023
545eec7
Add utility to parse `event.json` files for project automations
dhruvkb Nov 20, 2023
4da2af9
Add automations for issues, replacing old `new_issues.yml`
dhruvkb Nov 20, 2023
e1a4e2b
Merge branch 'main' of https://github.com/WordPress/openverse into pr…
dhruvkb Nov 20, 2023
890bfa3
Clarify separation of event name and action
dhruvkb Nov 20, 2023
edb8724
Extract CLI parsing into a util to share between issues and PRs
dhruvkb Nov 20, 2023
70aa34b
Fix documentation typo
dhruvkb Nov 20, 2023
f88417e
Add automations for PRs
dhruvkb Nov 20, 2023
a810bcd
Strip leading numbers from column names
dhruvkb Nov 20, 2023
5d0ca31
Fix sync for PR automations
dhruvkb Nov 20, 2023
208dc98
Document the project automation files
dhruvkb Nov 20, 2023
2208c7e
Use `reviewDecision` to determine column placement
dhruvkb Nov 20, 2023
5e3bc44
Use `init` function to populate extra PR info
dhruvkb Nov 20, 2023
27693f2
Move critical issues directly to the "To Do" column
dhruvkb Nov 20, 2023
a3397f8
Move linked issues for PR open, reopen, edited and closed events
dhruvkb Nov 20, 2023
aa27065
Update documentation
dhruvkb Nov 20, 2023
f50cb20
Fix broken links
dhruvkb Nov 20, 2023
3eb386f
Remove stray space
dhruvkb Nov 21, 2023
e82e840
Add steps to check out repo and setup CI environment
dhruvkb Nov 21, 2023
25dfb8b
Ignore event data payload
dhruvkb Nov 21, 2023
780713e
Allow PR automations to run on fork PRs as well
dhruvkb Nov 21, 2023
1a5ce80
Expand documentation for the workflows
dhruvkb Nov 21, 2023
a56daa8
Fix typo in variable name
dhruvkb Nov 21, 2023
5502d77
Fix incorrect file name
dhruvkb Nov 22, 2023
53142df
Add braces around `if`-`else`
dhruvkb Nov 22, 2023
f4ba8ef
Add braces around each `case` of `switch`
dhruvkb Nov 22, 2023
aaf807f
Use a dict to get project number
dhruvkb Nov 22, 2023
d941bfa
Inject Octokit client into `PullRequest`
dhruvkb Nov 22, 2023
1317936
Get project ID and fields in one request
dhruvkb Nov 22, 2023
4fb2c87
Document constructors
dhruvkb Nov 22, 2023
5fdc687
Inject Octokit client into `getProject`
dhruvkb Nov 22, 2023
adf5ba8
Clean up typehints
dhruvkb Nov 22, 2023
85b8ec9
Nest everything under an exported `main` function
dhruvkb Nov 22, 2023
85a9be5
Document the other responsibility of `syncPriority`
dhruvkb Nov 22, 2023
760b2f6
Clarify the board associated with the `columns` variable
dhruvkb Nov 22, 2023
9916a56
Use `action/github-script`
dhruvkb Nov 22, 2023
51f3e78
Undo dependency changes
dhruvkb Nov 22, 2023
1428f82
Undo collateral changes
dhruvkb Nov 22, 2023
cc28ea0
Remove unused code
dhruvkb Nov 22, 2023
ba06d19
Remove unnecessary Git-ignore file
dhruvkb Nov 22, 2023
4e5a052
Fix type annotations for Octokit
dhruvkb Nov 22, 2023
c151464
Simplify `PullRequest` construction
dhruvkb Nov 22, 2023
70c7d8c
Collect all relevant info in one file
dhruvkb Nov 22, 2023
b1a6be4
Make `columns` a field instead of computed property
dhruvkb Nov 22, 2023
19e9d71
Extract methods outside `main`
dhruvkb Nov 22, 2023
a834ac3
Use the correct token
dhruvkb Nov 22, 2023
ae98281
Update comments to reflect code changes
dhruvkb Nov 22, 2023
807c388
Use `pull_request` instead of `pull_request_target` because it doesn'…
dhruvkb Nov 22, 2023
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
10 changes: 6 additions & 4 deletions .github/sync.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,12 @@ group:
- source: automations/data/
dest: automations/data/
# Synced workflows
- source: .github/workflows/new_issues.yml
dest: .github/workflows/new_issues.yml
- source: .github/workflows/new_prs.yml
dest: .github/workflows/new_prs.yml
- source: .github/workflows/issue_automations.yml
dest: .github/workflows/issue_automations.yml
- source: .github/workflows/pr_automations.yml
dest: .github/workflows/pr_automations.yml
- source: .github/workflows/pr_automations_init.yml
dest: .github/workflows/pr_automations_init.yml
- source: .github/workflows/label_new_pr.yml
dest: .github/workflows/label_new_pr.yml
- source: .github/workflows/pr_label_check.yml
Expand Down
40 changes: 40 additions & 0 deletions .github/workflows/issue_automations.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Runs all automations related to issue events.
#
# See `pr_automations_init.yml` and `pr_automations.yml` for the corresponding
# implementation for PRs.
name: Issue automations

on:
issues:
types:
- opened
- reopened
- closed
- assigned
- labeled
- unlabeled

jobs:
run:
name: Perform issue automations
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup CI env
uses: ./.github/actions/setup-env
with:
setup_python: false
install_recipe: node-install

- name: Perform issue automations
uses: actions/github-script@v7
env:
EVENT_NAME: ${{ github.event_name }}
EVENT_ACTION: ${{ github.event.action }}
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { main } = await import('./automations/js/project_automation/issues.mjs')
await main(github, context)
30 changes: 0 additions & 30 deletions .github/workflows/new_issues.yml

This file was deleted.

80 changes: 80 additions & 0 deletions .github/workflows/pr_automations.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Runs all automations related to PR events.
#
# See `issue_automations.yml` for the corresponding implementation for issues.
#
# The automations for PR events are a little more complex than those for issues
# because PRs are a less secure environment. To avoid leaking secrets, we need
# to run automations with code as it appears on `main`.
#
# `pull_request_target` serves this purpose but there is no corresponding
# `_target` version for `pull_request_review`. So we take this roundabout
# approach:
#
# This workflow waits for the `pr_automations_init.yml` workflow to complete and
# then uses its exports to run automations from main, with access to secrets.
#
# ...continued from `pr_automations_init.yml`
#
# 4. This workflow runs after `pr_automations_init.yml` workflow completes.
# 5. It downloads the artifacts from that workflow run.
# 6. It extracts the JSON file from the ZIP to `/tmp`.
# 7. It runs the automations as a script, which can access secrets.

name: PR automations

on:
workflow_run:
workflows:
- PR automations init
types:
- completed

jobs:
run:
name: Perform PR automations
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Setup CI env
uses: ./.github/actions/setup-env
with:
setup_python: false
install_recipe: node-install

# This step was copied from the GitHub docs.
# Ref: https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#using-data-from-the-triggering-workflow
- name: Download artifact
uses: actions/github-script@v7
with:
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 == "event_info"
})[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}/event_info.zip`, Buffer.from(download.data));

- name: Unzip artifact
run: |
unzip event_info.zip
mv event.json /tmp/event.json
Comment on lines +69 to +72
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is one thing that's bothering me a lot, it's two lines of Bash breaking up two action/github-scripts. Surely it could be done inside JS? Or the previous step in Bash?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could use https://www.npmjs.com/package/unzipper to do the unzipping in JavaScript? Converting the previous github script to bash looks more complicated to me (lots of gh cli response parsing 🫠).


- name: Perform PR automations
uses: actions/github-script@v7
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const { main } = await import('./automations/js/project_automation/prs.mjs')
await main(github)
50 changes: 50 additions & 0 deletions .github/workflows/pr_automations_init.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Initialises all automations related to PR events.
#
# See `issue_automations.yml` for the corresponding implementation for issues.
#
# The automations for PR events are a little more complex than those for issues
# because PRs are a less secure environment. To avoid leaking secrets, we need
# to run automations with code as it appears on `main`.
#
# `pull_request_target` serves this purpose but there is no corresponding
# `_target` version for `pull_request_review`. So we take this roundabout
# approach:
#
# 1. This workflow runs for the events and their subtypes we are interested in.
# 2. It saves the event name, action and PR node ID to a JSON file.
# 3. It uploads the JSON file as an artifact.
# 4. Its completion triggers the `pr_automations.yml` workflow.
#
# continued in `pr_automations.yml`...

name: PR automations init

on:
pull_request:
types:
- opened
- reopened
- edited
- converted_to_draft
- ready_for_review
- closed
pull_request_review:

jobs:
run:
name: Save event info
runs-on: ubuntu-latest
steps:
- name: Save event info
run: |
echo '{"eventName": "'"$EVENT_NAME"'", "eventAction": "'"$EVENT_ACTION"'", "prNodeId": "'"$PR_NODE_ID"'"}' > /tmp/event.json
env:
EVENT_NAME: ${{ github.event_name }}
EVENT_ACTION: ${{ github.event.action }}
PR_NODE_ID: ${{ github.event.pull_request.node_id }}

- name: Upload event info as artifact
uses: actions/upload-artifact@v3
with:
name: event_info
path: /tmp/event.json
91 changes: 91 additions & 0 deletions automations/js/src/project_automation/issues.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
import { getBoard } from '../utils/projects.mjs'

/**
* Set the "Priority" custom field based on the issue's labels. Also move
* the card for critical issues directly to the "πŸ“… To Do" column.
*
* @param issue {import('@octokit/rest')}
* @param board {import('../utils/projects.mjs').Project}
* @param card {import('../utils/projects.mjs').Card}
*/
async function syncPriority(issue, board, card) {
const priority = issue.labels.find((label) =>
label.name.includes('priority')
)?.name
if (priority) {
await board.setCustomChoiceField(card.id, 'Priority', priority)
}
if (priority === 'πŸŸ₯ priority: critical') {
await board.moveCard(card.id, board.columns.ToDo)
}
}

/**
* This is the entrypoint of the script.
*
* @param octokit {import('@octokit/rest').Octokit} the Octokit instance to use
* @param context {import('@actions/github').context} info about the current event
*/
export const main = async (octokit, context) => {
const { EVENT_ACTION: eventAction } = process.env

const issue = context.payload.issue
const label = context.payload.label

if (issue.labels.some((label) => label.name === '🧭 project: thread')) {
// Do not add project threads to the Backlog board.
process.exit(0)
}

const backlogBoard = await getBoard(octokit, 'Backlog')

// Create new, or get the existing, card for the current issue.
const card = await backlogBoard.addCard(issue.node_id)

switch (eventAction) {
case 'opened':
case 'reopened': {
if (issue.labels.some((label) => label.name === 'β›” status: blocked')) {
await backlogBoard.moveCard(card.id, backlogBoard.columns.Blocked)
} else {
await backlogBoard.moveCard(card.id, backlogBoard.columns.Backlog)
}

await syncPriority(issue, backlogBoard, card)
break
}

case 'closed': {
if (issue.state_reason === 'completed') {
await backlogBoard.moveCard(card.id, backlogBoard.columns.Done)
} else {
await backlogBoard.moveCard(card.id, backlogBoard.columns.Discarded)
}
break
}

case 'assigned': {
if (card.status === backlogBoard.columns.Backlog) {
await backlogBoard.moveCard(card.id, backlogBoard.columns.ToDo)
}
break
}

case 'labeled': {
if (label.name === 'β›” status: blocked') {
await backlogBoard.moveCard(card.id, backlogBoard.columns.Blocked)
}
await syncPriority(issue, backlogBoard, card)
break
}

case 'unlabeled': {
if (label.name === 'β›” status: blocked') {
// TODO: Move back to the column it came from.
await backlogBoard.moveCard(card.id, backlogBoard.columns.Backlog)
}
await syncPriority(issue, backlogBoard, card)
break
}
}
}
Loading