Skip to content

Implementation of project board automations #3375

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

Merged
merged 51 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from 26 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
38 changes: 38 additions & 0 deletions .github/workflows/issue_automations.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
# 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
run: |
echo "$EVENT_PAYLOAD" > event.json
pnpm issues --event_name ${{ github.event_name }} --event_action ${{ github.event.action }}
rm event.json
env:
EVENT_PAYLOAD: ${{ toJson(github.event) }}M
working-directory: automations/js
30 changes: 0 additions & 30 deletions .github/workflows/new_issues.yml

This file was deleted.

78 changes: 78 additions & 0 deletions .github/workflows/pr_automations.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
# Runs all automations related to PR events.
#
# See `issue_automations_init.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 files, loads the env vars and runs the automation script.

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@v6
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: Perform PR automations
run: |
unzip event_info.zip
source event_info.env
pnpm prs --event_name "$EVENT_NAME" --event_action "$EVENT_ACTION"
rm event_info.env
rm event.json
env:
EVENT_PAYLOAD: ${{ toJson(github.event) }}
working-directory: automations/js
54 changes: 54 additions & 0 deletions .github/workflows/pr_automations_init.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Initialises all automations related to PR events.
#
# See `issue_automations_init.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 payload to a JSON file and the event name and action to
# an environment file.
# 3. It uploads the JSON file and the environment files as artifacts.
#
# continued in `pr_automations.yml`...

name: PR automations init

on:
pull_request_target:
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: |
mkdir -p ./pr
echo "$EVENT_PAYLOAD" > ./pr/event.json
echo "export EVENT_NAME=$EVENT_NAME" > ./pr/event_info.env
echo "export EVENT_ACTION=$EVENT_ACTION" >> ./pr/event_info.env
env:
EVENT_PAYLOAD: ${{ toJson(github.event) }}
EVENT_NAME: ${{ github.event_name }}
EVENT_ACTION: ${{ github.event.action }}

- name: Upload event info as artifact
uses: actions/upload-artifact@v3
with:
name: event_info
path: pr/
2 changes: 2 additions & 0 deletions automations/js/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
# Event data payload used by project automations
event.json
8 changes: 6 additions & 2 deletions automations/js/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,18 @@
"private": true,
"version": "0.0.0",
"dependencies": {
"@octokit/rest": "19.0.7",
"axios": "^1.0.0",
"js-yaml": "^4.1.0",
"k6": "0.0.0",
"nunjucks": "^3.2.4"
"nunjucks": "^3.2.4",
"octokit": "^3.1.2",
"yargs": "^17.7.2"
},
"devDependencies": {
"@actions/core": "^1.10.0",
"@actions/github": "^5.1.1"
},
"scripts": {
"issues": "node src/project_automation/issues.mjs"
}
}
2 changes: 1 addition & 1 deletion automations/js/src/count_user_reviewable_prs.js
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Return their Slack username and PR count, if found.
*
* @param {Object} options
* @param {import('@octokit/rest').Octokit} options.github
* @param {import('octokit').Octokit} options.github
* @param {import('@actions/github')['context']} options.context
* @param {import('@actions/core')} options.core
*/
Expand Down
11 changes: 3 additions & 8 deletions automations/js/src/last_week_tonight.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -8,29 +8,24 @@ import { resolve } from 'path'

import yaml from 'js-yaml'
import axios from 'axios'
import { Octokit } from '@octokit/rest'

import { escapeHtml } from './html.mjs'
import { getOctokit } from './utils/octokit.mjs'

/* Environment variables */

/** the personal access token for the GitHub API */
const pat = process.env.ACCESS_TOKEN
/** the username for the Make site account making the post */
const username = process.env.MAKE_USERNAME
/** the application password, not login password, for the Make site */
const password = process.env.MAKE_PASSWORD

if (!pat) {
console.log('GitHub personal access token "ACCESS_TOKEN" is required.')
}
if (!username) {
console.log('Make site username "MAKE_USERNAME" is required.')
}
if (!password) {
console.log('Make site application password "MAKE_PASSWORD" is required.')
}
if (!(pat && username && password)) process.exit(1)
if (!(username && password)) process.exit(1)

/* Read GitHub information from the data files */

Expand All @@ -51,7 +46,7 @@ const [startDate] = new Date(new Date().getTime() - msInWeeks(1))

/* GitHub API */

const octokit = new Octokit({ auth: pat })
const octokit = getOctokit()
const mergedPrsQ = (repo) =>
`repo:${org}/${repo} is:pr is:merged merged:>=${startDate}`
const closedIssuesQ = (repo) =>
Expand Down
2 changes: 1 addition & 1 deletion automations/js/src/migrate_issues.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
const fs = require('fs')

const { Octokit } = require('@octokit/rest')
const { Octokit } = require('octokit')

// TODO: add auth token
const octokit = new Octokit({
Expand Down
77 changes: 77 additions & 0 deletions automations/js/src/project_automation/issues.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* This module handles all events related to issues.
*
* Invoke it from the CLI with
* - the event name as the `--event_name` argument
* - the event action as the `--event_action` argument
* - the event payload in the `event.json` file in the project root
*/

import { getBoard } from '../utils/projects.mjs'
import { getEvent } from '../utils/event.mjs'

const { eventAction, eventPayload } = getEvent()

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

const backlogBoard = await getBoard('Backlog')
const columns = backlogBoard.columns // computed property

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

/**
* Set the "Priority" custom field based on the issue's labels.
*/
const syncPriority = async () => {
const priority = eventPayload.issue.labels.find((label) =>
label.name.includes('priority')
)?.name
if (priority)
await backlogBoard.setCustomChoiceField(card.id, 'Priority', priority)
if (priority === 'πŸŸ₯ priority: critical')
await backlogBoard.moveCard(card.id, columns.ToDo)
}

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

await syncPriority()
break

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

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

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

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