diff --git a/.github/sync.yml b/.github/sync.yml index d91a99b03d0..e07e6cd1947 100644 --- a/.github/sync.yml +++ b/.github/sync.yml @@ -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 diff --git a/.github/workflows/issue_automations.yml b/.github/workflows/issue_automations.yml new file mode 100644 index 00000000000..2f3c6098b7d --- /dev/null +++ b/.github/workflows/issue_automations.yml @@ -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) diff --git a/.github/workflows/new_issues.yml b/.github/workflows/new_issues.yml deleted file mode 100644 index 2bae21d7e2a..00000000000 --- a/.github/workflows/new_issues.yml +++ /dev/null @@ -1,30 +0,0 @@ -name: New issue automation -# ℹī¸ https://github.com/WordPress/openverse/blob/main/.github/GITHUB.md#new-issue-automation - -on: - issues: - types: - - opened -env: - GH_TOKEN: ${{ secrets.ACCESS_TOKEN }} # Projects need a personal access token to work. - ISSUE_ID: ${{ github.event.issue.node_id }} # The global issue ID that works in both REST and GraphQL APIs. - -jobs: - add_issue_to_project: - name: Add new issue to project - runs-on: ubuntu-latest - steps: - - uses: bulatt3/add-to-project-and-label@v0.0.2 - with: - project-url: https://github.com/orgs/WordPress/projects/75 - github-token: ${{ secrets.ACCESS_TOKEN }} - # Exclude the issues with the following labels - labeled: "🧭 project: thread" - label-operator: NOT - label-map: | - { "Priority": [ - { "label": "đŸŸĨ priority: critical", "fieldValue": "đŸŸĨ priority: critical" }, - { "label": "🟧 priority: high", "fieldValue": "🟧 priority: high" }, - { "label": "🟨 priority: medium", "fieldValue": "🟨 priority: medium" }, - { "label": "🟩 priority: low", "fieldValue": "🟩 priority: low" } - ]} diff --git a/.github/workflows/pr_automations.yml b/.github/workflows/pr_automations.yml new file mode 100644 index 00000000000..39cc63186b2 --- /dev/null +++ b/.github/workflows/pr_automations.yml @@ -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) diff --git a/.github/workflows/pr_automations_init.yml b/.github/workflows/pr_automations_init.yml new file mode 100644 index 00000000000..265e189dab6 --- /dev/null +++ b/.github/workflows/pr_automations_init.yml @@ -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 diff --git a/automations/js/src/project_automation/issues.mjs b/automations/js/src/project_automation/issues.mjs new file mode 100644 index 00000000000..95f37e91f4b --- /dev/null +++ b/automations/js/src/project_automation/issues.mjs @@ -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 + } + } +} diff --git a/automations/js/src/project_automation/prs.mjs b/automations/js/src/project_automation/prs.mjs new file mode 100644 index 00000000000..4264e8280e6 --- /dev/null +++ b/automations/js/src/project_automation/prs.mjs @@ -0,0 +1,99 @@ +import { readFileSync } from 'fs' + +import { getBoard } from '../utils/projects.mjs' +import { PullRequest } from '../utils/pr.mjs' + +/** + * Move the PR to the right column based on the number of reviews. + * + * @param pr {PullRequest} + * @param prBoard {Project} + * @param prCard {Card} + */ +async function syncReviews(pr, prBoard, prCard) { + const reviewDecision = pr.reviewDecision + const reviewCounts = pr.reviewCounts + + if (reviewDecision === 'APPROVED') { + await prBoard.moveCard(prCard.id, prBoard.columns.Approved) + } else if (reviewDecision === 'CHANGES_REQUESTED') { + await prBoard.moveCard(prCard.id, prBoard.columns.ChangesRequested) + } else if (reviewCounts.APPROVED === 1) { + await prBoard.moveCard(prCard.id, prBoard.columns.Needs1Review) + } else { + await prBoard.moveCard(prCard.id, prBoard.columns.Needs2Reviews) + } +} + +/** + * Move all linked issues to the specified column. + * + * @param pr {PullRequest} + * @param backlogBoard {Project} + * @param destColumn {string} + */ +async function syncIssues(pr, backlogBoard, destColumn) { + for (let linkedIssue of pr.linkedIssues) { + const issueCard = await backlogBoard.addCard(linkedIssue) + await backlogBoard.moveCard(issueCard.id, backlogBoard.columns[destColumn]) + } +} + +/** + * This is the entrypoint of the script. + * + * @param octokit {import('@octokit/rest').Octokit} the Octokit instance to use + */ +export const main = async (octokit) => { + const { eventName, eventAction, prNodeId } = JSON.parse( + readFileSync('/tmp/event.json', 'utf-8') + ) + + const pr = new PullRequest(octokit, prNodeId) + await pr.init() + + const prBoard = await getBoard(octokit, 'PRs') + const backlogBoard = await getBoard(octokit, 'Backlog') + + // Create new, or get the existing, card for the current pull request. + const prCard = await prBoard.addCard(pr.nodeId) + + if (eventName === 'pull_request_review') { + await syncReviews(pr, prBoard, prCard) + } else { + switch (eventAction) { + case 'opened': + case 'reopened': { + if (pr.isDraft) { + await prBoard.moveCard(prCard.id, prBoard.columns.Draft) + } else { + await syncReviews(pr, prBoard, prCard) + } + await syncIssues(pr, backlogBoard, 'InProgress') + break + } + + case 'edited': { + await syncIssues(pr, backlogBoard, 'InProgress') + break + } + + case 'converted_to_draft': { + await prBoard.moveCard(prCard.id, prBoard.columns.Draft) + break + } + + case 'ready_for_review': { + await syncReviews(pr, prBoard, prCard) + break + } + + case 'closed': { + if (!pr.isMerged) { + await syncIssues(pr, backlogBoard, 'Backlog') + } + break + } + } + } +} diff --git a/automations/js/src/utils/pr.mjs b/automations/js/src/utils/pr.mjs new file mode 100644 index 00000000000..f31f3a6c8f5 --- /dev/null +++ b/automations/js/src/utils/pr.mjs @@ -0,0 +1,94 @@ +/** + * the final decision on the PR from combining all reviews + * @typedef {'APPROVED' | 'CHANGES_REQUESTED' | 'REVIEW_REQUIRED'} ReviewDecision + * + * the state of a particular review left on a PR + * @typedef {'APPROVED' | 'CHANGES_REQUESTED' | 'COMMENTED' | 'DISMISSED' | 'PENDING'} ReviewState + * + * the additional information about the PR obtained from a GraphQL query + * @typedef {{reviewDecision: ReviewDecision, linkedIssues: string[], reviewStates: ReviewState[]}} PrDetails + */ + +export class PullRequest { + /** + * Create a new `PullRequest` instance. This takes the `node_id` of the PR + * as opposed to the conventional `id` or `number` fields. + * + * @param octokit {import('@octokit/rest').Octokit} the Octokit instance to use + * @param nodeId {boolean} the `node_id` of the PR for GraphQL requests + */ + constructor(octokit, nodeId) { + this.octokit = octokit + + this.nodeId = nodeId + } + + async init() { + const prDetails = await this.getPrDetails() + this.linkedIssues = prDetails.linkedIssues + this.reviewDecision = prDetails.reviewDecision + this.reviewStates = prDetails.reviewStates + this.isDraft = prDetails.isDraft + this.isMerged = prDetails.isMerged + } + + /** + * Get additional information about the PR such as the linked issues and the + * review decision, as well the states of all submitted reviews. + * + * @returns {Promise} + */ + async getPrDetails() { + const res = await this.octokit.graphql( + `query getPrDetails($id: ID!) { + node(id: $id) { + ... on PullRequest { + isDraft + merged + reviewDecision + closingIssuesReferences(first: 10) { + nodes { + id + } + } + reviews(first: 100) { + nodes { + state + } + } + } + } + }`, + { + id: this.nodeId, + } + ) + const pr = res.node + return { + isMerged: pr.isMerged, + isDraft: pr.merged, + reviewDecision: pr.reviewDecision, + linkedIssues: pr.closingIssuesReferences.nodes.map((node) => node.id), + reviewStates: pr.reviews.nodes.map((node) => node.state), + } + } + + /** + * Get the count of each type of PR reviews. + * + * @returns {{[p: ReviewState]: number}} the PR review counts + */ + get reviewCounts() { + const reviewCounts = { + APPROVED: 0, + COMMENTED: 0, + CHANGES_REQUESTED: 0, + DISMISSED: 0, + PENDING: 0, + } + for (let reviewState of this.reviewStates) { + reviewCounts[reviewState] += 1 + } + return reviewCounts + } +} diff --git a/automations/js/src/utils/projects.mjs b/automations/js/src/utils/projects.mjs new file mode 100644 index 00000000000..3a307d78ac2 --- /dev/null +++ b/automations/js/src/utils/projects.mjs @@ -0,0 +1,229 @@ +/** + * a representation of an issue or PR on a project board + * @typedef {{id: string, status: string}} Card + * + * a custom variable on a project board + * @typedef {{id: string, options: {[p: string]: string}}} Field + * + * the additional information about the project obtained from a GraphQL query + * @typedef {{projectId: string, fields: {[p: string]: Field}}} ProjectDetails + */ + +const PROJECT_NUMBERS = { + Backlog: 75, + PRs: 98, + Todos: 59, + Discussions: 79, + 'Project Tracker': 70, +} + +class Project { + /** + * Create a new `Project` instance using owner name and project number, both + * of which can be found in the project URL. For example, + * + * https://github.com/orgs/WordPress/projects/75/views/1 + * ^^^^^^^^^ ^^ + * owner number + * + * @param octokit {import('@octokit/rest').Octokit} the Octokit instance to use + * @param owner {string} the login of the owner (org) of the project + * @param number {number} the number of the project + */ + constructor(octokit, owner, number) { + this.octokit = octokit + + this.owner = owner + this.number = number + } + + /** + * Initialise the project and populate fields that require API call to GitHub. + */ + async init() { + const projectDetails = await this.getProjectDetails() + this.projectId = projectDetails.projectId + this.fields = projectDetails.fields + this.columns = this.getColumns() + } + + /** + * Get a mapping of all options for the "Status" field against their slugified + * names to easily access status choices without typing emojis. + * + * @returns {{[p: string]: string}} mapping of "Status" slugs to choices + */ + getColumns() { + return Object.fromEntries( + Object.keys(this.fields['Status'].options).map((key) => [ + key.replace(/\W/g, '').replace(/^\d*/, '').trim(), + key, + ]) + ) + } + + /** + * Get additional information about the project such as node ID and custom + * fields. + * + * This function currently only supports `ProjectV2SingleSelectField` because + * that's all we currently have. + * + * @returns {Promise} the ID of the project + */ + async getProjectDetails() { + const res = await this.octokit.graphql( + `query getProjectId($login: String!, $number: Int!) { + organization(login: $login) { + projectV2(number: $number) { + id + fields(first: 20) { + nodes { + ... on ProjectV2SingleSelectField { + id + name + options { + id + name + } + } + } + } + } + } + }`, + { + login: this.owner, + number: this.number, + } + ) + const project = res.organization.projectV2 + return { + projectId: project.id, + fields: Object.fromEntries( + project.fields.nodes + .filter((field) => field.options) + .map((field) => [ + field.name, + { + id: field.id, + options: Object.fromEntries( + field.options.map((option) => [option.name, option.id]) + ), + }, + ]) + ), + } + } + + /** + * Add the issue or PR to the project. This takes the `node_id` of the issue + * or PR as opposed to the conventional `id` or `number` fields. + * + * This function is also idempotent, so it can be used to get the card ID for + * an existing card with no side effects. + * + * @param issueId {string} the ID of the issue/PR to add + * @returns {Promise} the info of the added card + */ + async addCard(issueId) { + const res = await this.octokit.graphql( + `mutation addCard($projectId: ID!, $contentId: ID!) { + addProjectV2ItemById(input: { + projectId: $projectId, + contentId: $contentId + }) { + item { + id + fieldValueByName(name: "Status") { + ...on ProjectV2ItemFieldSingleSelectValue { + name + } + } + } + } + }`, + { + projectId: this.projectId, + contentId: issueId, + } + ) + const card = res.addProjectV2ItemById.item + return { + id: card.id, + status: card.fieldValueByName?.name, // `null` if new card + } + } + + /** + * Set the value of the custom choice field to the given option. + * + * @param cardId {string} the ID of the card whose field is to be updated + * @param fieldName {string} the name of the field to update + * @param optionName {string} the updated value of the field + * @returns {Promise} the ID of the card that was updated + */ + async setCustomChoiceField(cardId, fieldName, optionName) { + // Preliminary validation + if (!this.fields[fieldName]) { + throw new Error(`Unknown field name "${fieldName}".`) + } + if (!this.fields[fieldName].options[optionName]) { + throw new Error( + `Unknown option name "${optionName}" for field "${fieldName}".` + ) + } + + const res = await this.octokit.graphql( + `mutation setCustomField($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) { + updateProjectV2ItemFieldValue(input: { + projectId: $projectId, + itemId: $itemId, + fieldId: $fieldId, + value: {singleSelectOptionId: $optionId} + }) { + projectV2Item { + id + } + } + }`, + { + projectId: this.projectId, + itemId: cardId, + fieldId: this.fields[fieldName].id, + optionId: this.fields[fieldName].options[optionName], + } + ) + return res.updateProjectV2ItemFieldValue.projectV2Item.id + } + + /** + * Moving a card to a different column is akin to updating the custom field + * "Status" that is present on all GitHub project boards. + * + * @param cardId {string} the ID of the card to move + * @param destColumn {string} the name of the column where to move it + * @returns {Promise} the ID of the card that was moved + */ + async moveCard(cardId, destColumn) { + return await this.setCustomChoiceField(cardId, 'Status', destColumn) + } +} + +/** + * Get the `Project` instance for the project board with the given name. + * + * @param octokit {import('@octokit/rest').Octokit} the Octokit instance to use + * @param name {string} the name of the project (without the 'Openverse' prefix) + * @returns {Project} the `Project` instance to interact with the project board + */ +export async function getBoard(octokit, name) { + const projectNumber = PROJECT_NUMBERS[name] + if (!projectNumber) { + throw new Error(`Unknown project board "${name}".`) + } + + const project = new Project(octokit, 'WordPress', projectNumber) + await project.init() + return project +} diff --git a/documentation/automations/reference/index.md b/documentation/automations/reference/index.md index ca6f9b62d88..5edf29987f1 100644 --- a/documentation/automations/reference/index.md +++ b/documentation/automations/reference/index.md @@ -4,4 +4,5 @@ :titlesonly: python_scripts +project_automations ``` diff --git a/documentation/automations/reference/project_automations.md b/documentation/automations/reference/project_automations.md new file mode 100644 index 00000000000..7f7ce698986 --- /dev/null +++ b/documentation/automations/reference/project_automations.md @@ -0,0 +1,18 @@ +# Project Automations + +This project contains automations for moving cards related to issues and PRs +across columns of the GitHub Project boards. + +The automations are contained in `automations/js` Node.js project and defined in +the following files. + +- `src/project_automation/issues.mjs` + + This file defines the rules for moving cards related to issues in the columns + of the [Openverse Backlog](https://github.com/orgs/WordPress/projects/75/) + project. + +- `src/project_automation/prs.mjs` + + This file defines the rules for moving cards related to PRs in the columns of + the [Openverse PRs](https://github.com/orgs/WordPress/projects/98/) project. diff --git a/documentation/meta/project_boards/issues.md b/documentation/meta/project_boards/issues.md index 532897c761e..229c981d9bd 100644 --- a/documentation/meta/project_boards/issues.md +++ b/documentation/meta/project_boards/issues.md @@ -7,41 +7,64 @@ this board are tied to events occurring for issues. ## Event automations -### Issue is created +```{note} +For all below events, "custom workflow" refers to +[`issue_automations.yml`](https://github.com/WordPress/openverse/blob/main/.github/workflows/issue_automations.yml) +which is also +[synced to the `WordPress/openverse-infrastructure` repo](https://github.com/WordPress/openverse-infrastructure/blob/main/.github/workflows/issue_automations.yml). +``` + +### Issue is opened/reopened If a new issue is created in the -[`WordPress/openverse`](https://github.com/WordPress/openverse/) repository, it -is automatically added to the project board provided it does not contain any -label with the text "project". +[`WordPress/openverse`](https://github.com/WordPress/openverse/) and +[`WordPress/openverse-infrastructure`](https://github.com/WordPress/openverse-infrastructure/) +repositories, it is automatically added to the project board provided it does +not contain the label "🧭 project: thread". + +- If an issue has the "đŸŸĨ priority: critical" label, it is automatically added + to the "📅 To Do" column. +- Else if an issue has the "⛔ status: blocked" label, it is automatically added + to the "⛔ Blocked" column. +- Else it is automatically added to the "📋 Backlog" column. ```{note} This workflow also sets the Priority custom field in the issue so that we can create a kanban-board view based on priority. ``` -- [Custom workflow](https://github.com/WordPress/openverse/blob/main/.github/workflows/new_issues.yml) +This is handled by a custom workflow. The following built-in workflows for this +task have been deactivated: + +- Auto-add to project (won't trigger our workflow, also does not set the + "Priority" custom field) +- Item added to project (does not differentiate blocked vs unblocked issues) +- Item reopened (does not differentiate blocked vs unblocked issues) ### Issue is closed -If an issue is closed, it moves into the "✅ Done" column. This is not affected -by whether it was closed as resolved or as discarded. +If an issue is closed, it moves into the "✅ Done" column or the "🗑ī¸ Discarded" +column based on whether it was completed or rejected. + +This is handled by a custom workflow. The following built-in workflows for this +task have been deactivated: -- [Built-in workflow](https://github.com/orgs/WordPress/projects/75/workflows/6899392) +- Item closed (does not differentiate completed vs rejected issues) -### Issue is reopened +### Issue is assigned -If a previously closed issue is reopened, it goes back to the "📋 Backlog" -column. That is because it will need to be re-prioritized alongside other -ongoing work and moved to "📅 To do" when it can be worked on again. +When an issue is assigned to someone, it is automatically moved into the "🏗ī¸ In +Progress" column. -- [Built-in workflow](https://github.com/orgs/WordPress/projects/75/workflows/8193212) +This is handled by a custom workflow. -### Issue is added to the project +### Issue is labeled/unlabeled -The status of this issue will be set to "📋 Backlog" and thus, it will be -included under the "📋 Backlog" column. +If an issue is added the "⛔ status: blocked" label, it is automatically moved +into the "⛔ Blocked" column. If an issue is removed from the "⛔ status: +blocked" label, it is automatically moved into the "📋 Backlog" column. -- [Built-in workflow](https://github.com/orgs/WordPress/projects/75/workflows/6899490) +This is handled by a custom workflow. ### Issue is closed and inactive @@ -49,4 +72,5 @@ If an issue is closed, and has not been updated in 8 days, it will automatically be archived from the project board. This ensures that the board is cleared in time for the weekly development chat. -- [Built-in workflow](https://github.com/orgs/WordPress/projects/75/workflows/8222891) +This is handled by a +[built-in workflow](https://github.com/orgs/WordPress/projects/75/workflows/8222891). diff --git a/documentation/meta/project_boards/prs.md b/documentation/meta/project_boards/prs.md index 46a68cbdaf8..aaedb43927a 100644 --- a/documentation/meta/project_boards/prs.md +++ b/documentation/meta/project_boards/prs.md @@ -16,55 +16,96 @@ The PRs board is currently private. It may be made public in the future. ## Event automations -### PR is created +```{note} +For all below events, "custom workflow" refers to +[`pr_automations.yml`](https://github.com/WordPress/openverse/blob/main/.github/workflows/pr_automations.yml) +which is also +[synced to the `WordPress/openverse-infrastructure` repo](https://github.com/WordPress/openverse-infrastructure/blob/main/.github/workflows/pr_automations.yml). +``` -If a new PR is created, it is automatically added to the project board. PRs from -the infrastructure repository are also added to the board. +### PR is opened/reopened -- [Built-in workflow (monorepo)](https://github.com/orgs/WordPress/projects/98/workflows/8656692) -- [Built-in workflow (infra)](https://github.com/orgs/WordPress/projects/98/workflows/8674459) +If a new PR is created in the +[`WordPress/openverse`](https://github.com/WordPress/openverse/) and +[`WordPress/openverse-infrastructure`](https://github.com/WordPress/openverse-infrastructure/) +repositories, it is automatically added to the project board. -### PR is closed or merged +- If the PR is a draft, it is automatically added to the "🚧 Draft" column. +- Else if the PR requires changes, it is automatically added to the "🔁 Changes + Requested" column. +- Else if the PR has the requisite approvals, it is automatically added to the + "✅ Approved" column. +- If the PR has one approval, it is automatically added to the "1ī¸âƒŖ Needs 1 + Review" column. +- Else it is automatically added to the "2ī¸âƒŖ Needs 2 Reviews" column. -If a PR is closed or merged, it moves into the "Merged" column. Understandably, -this is slightly misleading for PRs that were closed unmerged. +If the PR is linked to an issue, the linked issue moves to the "🏗ī¸ In Progress" +column. -- [Built-in workflow (closed)](https://github.com/orgs/WordPress/projects/98/workflows/8656664) -- [Built-in workflow (merged)](https://github.com/orgs/WordPress/projects/98/workflows/8656665) +This is handled by a custom workflow. The following built-in workflows for this +task have been deactivated: -### PR is reopened +- Auto-add to project (won't trigger our workflow) +- Item added to project (cannot classify PRs by state and reviews) +- Item reopened (cannot classify PRs by state and reviews) -If a previously closed, but unmerged, PR is reopened, it goes back to the "Needs -Review" column, even if had been reviewed before being closed. +### PR is edited -- [Built-in workflow](https://github.com/orgs/WordPress/projects/98/workflows/8674442) +If the PR is linked to an issue, the linked issue moves to the "🏗ī¸ In Progress" +column. -### PR is added to the project +This is handled by a custom workflow. -The status of this PR will be set to "Needs review" and thus, it will be -included under the "Needs review" column. This is not affected by whether the PR -is actually ready or in a draft state. +### PR is converted to draft -- [Built-in workflow](https://github.com/orgs/WordPress/projects/98/workflows/8674448) +When the PR is converted to draft, it is automatically moved to the "🚧 Draft" +column. -### PR is closed and inactive +This is handled by a custom workflow. -If a PR is closed (includes merged) and has not been updated in two weeks, it -will automatically be archived from the project board. This is different from -the archival threshold of the issues board (8 days). +### PR is ready for review -- [Built-in workflow](https://github.com/orgs/WordPress/projects/98/workflows/8674454) +When the PR is ready for review, it is automatically moved to the appropriate +column out of "1ī¸âƒŖ Needs 1 Review", "2ī¸âƒŖ Needs 2 Reviews", "✅ Approved", or "🔁 +Changes Requested". + +This is handled by a custom workflow. + +### PR has requested changes + +If the PR has requested changes, it is automatically added to the "🔁 Changes +Requested" column. + +This is handled by a custom workflow. ### PR has required approvals -If a PR has the required number of approvals, it moves into the "Approved" -column. PRs with fewer approvals than the merge requirement are not affected. +If the PR has required approvals, it is automatically added to the "✅ Approved" +column. -- [Built-in workflow](https://github.com/orgs/WordPress/projects/98/workflows/8674451) +This is handled by a custom workflow. -### PR has requested changes +### PR is merged + +If a PR is merged, it moves into the "🤝 Merged" column. + +This is handled by a +[built-in workflow](https://github.com/orgs/WordPress/projects/98/workflows/8656665). + +### PR is closed + +If a PR is closed without being merged, it moves into the "đŸšĢ Closed" column. If +the PR was linked to an issue, the linked issue moves back to the "📋 Backlog" +column. + +This is handled by a combination of a +[built-in workflow](https://github.com/orgs/WordPress/projects/98/workflows/8656664) +and a custom workflow. -If a PR was reviewed with change requests, it moves into the "Changes requested" -column. Even one change request qualifies the PR for this automation. +### PR is closed and inactive + +If a PR is closed (includes merged) and has not been updated in two weeks, it +will automatically be archived from the project board. This is different from +the archival threshold of the issues board (8 days). -- [Built-in workflow](https://github.com/orgs/WordPress/projects/98/workflows/8674445) +- [Built-in workflow](https://github.com/orgs/WordPress/projects/98/workflows/8674454) diff --git a/documentation/projects/proposals/project_improvement/20230914-implementation_plan_project_automations.md b/documentation/projects/proposals/project_improvement/20230914-implementation_plan_project_automations.md index 50f877169c9..d1e0196bd10 100644 --- a/documentation/projects/proposals/project_improvement/20230914-implementation_plan_project_automations.md +++ b/documentation/projects/proposals/project_improvement/20230914-implementation_plan_project_automations.md @@ -74,9 +74,9 @@ categories. ### Improvements to existing automations -1. [Issues/Issue is created](/meta/project_boards/issues.md#issue-is-created) +1. [Issues/Issue is created](/meta/project_boards/issues.md#issue-is-openedreopened) and - [Issues/Issue is added to the project](/meta/project_boards/issues.md#issue-is-added-to-the-project) + [Issues/Issue is added to the project](/meta/project_boards/issues.md#issue-is-openedreopened) The issue should be added to the project board under the appropriate column, "📋 Backlog" or "⛔ Blocked" depending on the issue labels. @@ -102,14 +102,15 @@ categories. all items in the "✅ Done" and "🗑ī¸ Discarded" columns weekly (preferably immediately after the chat). -4. [PRs/PR is created](/meta/project_boards/prs.md#pr-is-created) and - [PRs/PR is added to project](/meta/project_boards/prs.md#pr-is-added-to-the-project) +4. [PRs/PR is created](/meta/project_boards/prs.md#pr-is-openedreopened) and + [PRs/PR is added to project](/meta/project_boards/prs.md#pr-is-openedreopened) The PR should be added to the project board under the appropriate column, "In Progress" or "Needs <x> Review(s)" depending on the PR's draft or ready-for-review state and the number of existing reviews. -5. [PRs/PR is closed or merged](/meta/project_boards/prs.md#pr-is-closed-or-merged) +5. PRs/PR is [closed](/meta/project_boards/prs.md#pr-is-closed) or + [merged](/meta/project_boards/prs.md#pr-is-merged) The PR should be moved to the "✅ Done" column only if it was merged. If it was closed without merge, it should be moved to a new "🗑ī¸ Discarded" column.