Skip to content

Commit

Permalink
Implementation of project board automations (#3375)
Browse files Browse the repository at this point in the history
Co-authored-by: sarayourfriend <24264157+sarayourfriend@users.noreply.github.com>
  • Loading branch information
dhruvkb and sarayourfriend authored Nov 23, 2023
1 parent f37a0cd commit 8d8d4ea
Show file tree
Hide file tree
Showing 14 changed files with 828 additions and 88 deletions.
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
- 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

0 comments on commit 8d8d4ea

Please sign in to comment.