diff --git a/.github/workflows/issue_automations.yml b/.github/workflows/issue_automations.yml index 887b9dbcd77..fd6fefbfcf7 100644 --- a/.github/workflows/issue_automations.yml +++ b/.github/workflows/issue_automations.yml @@ -31,4 +31,4 @@ jobs: github-token: ${{ secrets.ACCESS_TOKEN }} script: | const { main } = await import('${{ github.workspace }}/automations/js/src/project_automation/issues.mjs') - await main(github, context) + await main(github, core, context) diff --git a/automations/js/src/project_automation/issues.mjs b/automations/js/src/project_automation/issues.mjs index 95f37e91f4b..c38b8ea34e7 100644 --- a/automations/js/src/project_automation/issues.mjs +++ b/automations/js/src/project_automation/issues.mjs @@ -4,19 +4,25 @@ 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} + * @param core {import('@actions/core')} GitHub Actions toolkit, for logging + * @param issue {Issue} the issue for which to set the "Priority" custom field + * @param backlogBoard {Project} the project board for issues + * @param issueCard {Card} the card for the issue to sync */ -async function syncPriority(issue, board, card) { +async function syncPriority(core, issue, backlogBoard, issueCard) { + core.info(`Syncing priority for issue "${issue.number}".`) + const priority = issue.labels.find((label) => label.name.includes('priority') )?.name + core.debug(`Priority: ${priority}`) + if (priority) { - await board.setCustomChoiceField(card.id, 'Priority', priority) + await backlogBoard.setCustomChoiceField(issueCard.id, 'Priority', priority) } if (priority === '🟥 priority: critical') { - await board.moveCard(card.id, board.columns.ToDo) + core.info('Moving critical issue to "📅 To Do" column.') + await backlogBoard.moveCard(issueCard.id, backlogBoard.columns.ToDo) } } @@ -24,41 +30,51 @@ async function syncPriority(issue, board, card) { * This is the entrypoint of the script. * * @param octokit {import('@octokit/rest').Octokit} the Octokit instance to use + * @param core {import('@actions/core')} GitHub Actions toolkit, for logging * @param context {import('@actions/github').context} info about the current event */ -export const main = async (octokit, context) => { +export const main = async (octokit, core, context) => { + core.info('Starting script `issues.mjs`.') + const { EVENT_ACTION: eventAction } = process.env + core.debug(`Event action: ${eventAction}`) const issue = context.payload.issue + core.debug(`Issue node ID: ${issue.node_id}`) + const label = context.payload.label + core.info('Issue details:', issue) if (issue.labels.some((label) => label.name === '🧭 project: thread')) { - // Do not add project threads to the Backlog board. - process.exit(0) + core.warning('Issue is a project thread. Exiting.') + return } - const backlogBoard = await getBoard(octokit, 'Backlog') + const backlogBoard = await getBoard(octokit, core, 'Backlog') // Create new, or get the existing, card for the current issue. const card = await backlogBoard.addCard(issue.node_id) + core.debug(`Issue card ID: ${card.id}`) switch (eventAction) { case 'opened': case 'reopened': { if (issue.labels.some((label) => label.name === '⛔ status: blocked')) { + core.info('Issue was opened, labelled as blocked.') await backlogBoard.moveCard(card.id, backlogBoard.columns.Blocked) } else { await backlogBoard.moveCard(card.id, backlogBoard.columns.Backlog) } - - await syncPriority(issue, backlogBoard, card) + await syncPriority(core, issue, backlogBoard, card) break } case 'closed': { if (issue.state_reason === 'completed') { + core.info('Issue was closed as completed.') await backlogBoard.moveCard(card.id, backlogBoard.columns.Done) } else { + core.info('Issue was closed as discarded.') await backlogBoard.moveCard(card.id, backlogBoard.columns.Discarded) } break @@ -73,18 +89,19 @@ export const main = async (octokit, context) => { case 'labeled': { if (label.name === '⛔ status: blocked') { + core.info('Issue was labeled as blocked.') await backlogBoard.moveCard(card.id, backlogBoard.columns.Blocked) } - await syncPriority(issue, backlogBoard, card) + await syncPriority(core, issue, backlogBoard, card) break } case 'unlabeled': { if (label.name === '⛔ status: blocked') { - // TODO: Move back to the column it came from. + core.info('Issue was unlabeled as blocked.') await backlogBoard.moveCard(card.id, backlogBoard.columns.Backlog) } - await syncPriority(issue, backlogBoard, card) + await syncPriority(core, issue, backlogBoard, card) break } } diff --git a/automations/js/src/project_automation/prs.mjs b/automations/js/src/project_automation/prs.mjs index 114e851992c..f1b88984a8a 100644 --- a/automations/js/src/project_automation/prs.mjs +++ b/automations/js/src/project_automation/prs.mjs @@ -6,19 +6,27 @@ 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} + * @param core {import('@actions/core')} GitHub Actions toolkit, for logging + * @param pr {PullRequest} the PR to sync with the reviews and decision + * @param prBoard {Project} the project board for PRs + * @param prCard {Card} the card for the PR to sync */ -async function syncReviews(pr, prBoard, prCard) { +async function syncReviews(core, pr, prBoard, prCard) { + core.info(`Synchronizing reviews for PR ${pr.nodeId}.`) + const reviewDecision = pr.reviewDecision const reviewCounts = pr.reviewCounts + core.debug(`PR review counts: ${reviewCounts}`) + core.debug(`PR reviews decision: ${reviewDecision}`) if (reviewDecision === 'APPROVED') { + core.info('Moving PR on the basis of review decision.') await prBoard.moveCard(prCard.id, prBoard.columns.Approved) } else if (reviewDecision === 'CHANGES_REQUESTED') { + core.info('Moving PR on the basis of review decision.') await prBoard.moveCard(prCard.id, prBoard.columns.ChangesRequested) } else if (reviewCounts.APPROVED === 1) { + core.info('Moving PR on the basis of 1 approval.') await prBoard.moveCard(prCard.id, prBoard.columns.Needs1Review) } else { await prBoard.moveCard(prCard.id, prBoard.columns.Needs2Reviews) @@ -28,13 +36,21 @@ async function syncReviews(pr, prBoard, prCard) { /** * Move all linked issues to the specified column. * - * @param pr {PullRequest} - * @param backlogBoard {Project} - * @param destColumn {string} + * @param core {import('@actions/core')} GitHub Actions toolkit, for logging + * @param pr {PullRequest} the PR to sync with the reviews and decision + * @param backlogBoard {Project} the project board for issues + * @param destColumn {string} the destination column where to move the issue */ -async function syncIssues(pr, backlogBoard, destColumn) { +async function syncIssues(core, pr, backlogBoard, destColumn) { + core.info(`Synchronizing issues for PR ${pr.nodeId}.`) + for (let linkedIssue of pr.linkedIssues) { + core.info(`Syncing issue ${linkedIssue.id}.`) + + // Create new, or get the existing, card for the current issue. const issueCard = await backlogBoard.addCard(linkedIssue.id) + core.debug(`Issue card ID: ${issueCard.id}`) + await backlogBoard.moveCard(issueCard.id, backlogBoard.columns[destColumn]) } } @@ -46,36 +62,44 @@ async function syncIssues(pr, backlogBoard, destColumn) { * @param core {import('@actions/core')} GitHub Actions toolkit, for logging */ export const main = async (octokit, core) => { + core.info('Starting script `prs.mjs`.') + const { eventName, eventAction, prNodeId } = JSON.parse( readFileSync('/tmp/event.json', 'utf-8') ) + core.debug(`Event name: ${eventName}`) + core.debug(`Event action: ${eventAction}`) + core.debug(`PR node ID: ${prNodeId}`) const pr = new PullRequest(octokit, core, prNodeId) await pr.init() - const prBoard = await getBoard(octokit, 'PRs') - const backlogBoard = await getBoard(octokit, 'Backlog') + const prBoard = await getBoard(octokit, core, 'PRs') + const backlogBoard = await getBoard(octokit, core, 'Backlog') // Create new, or get the existing, card for the current pull request. const prCard = await prBoard.addCard(pr.nodeId) + core.debug(`PR card ID: ${prCard.id}`) if (eventName === 'pull_request_review') { - await syncReviews(pr, prBoard, prCard) + await syncReviews(core, pr, prBoard, prCard) } else { switch (eventAction) { case 'opened': case 'reopened': { if (pr.isDraft) { + core.info('PR is a draft.') await prBoard.moveCard(prCard.id, prBoard.columns.Draft) } else { - await syncReviews(pr, prBoard, prCard) + core.info('PR is ready for review.') + await syncReviews(core, pr, prBoard, prCard) } - await syncIssues(pr, backlogBoard, 'InProgress') + await syncIssues(core, pr, backlogBoard, 'InProgress') break } case 'edited': { - await syncIssues(pr, backlogBoard, 'InProgress') + await syncIssues(core, pr, backlogBoard, 'InProgress') break } @@ -85,13 +109,14 @@ export const main = async (octokit, core) => { } case 'ready_for_review': { - await syncReviews(pr, prBoard, prCard) + await syncReviews(core, pr, prBoard, prCard) break } case 'closed': { if (!pr.isMerged) { - await syncIssues(pr, backlogBoard, 'Backlog') + core.info('PR was closed without merge.') + await syncIssues(core, pr, backlogBoard, 'Backlog') } break } diff --git a/automations/js/src/utils/pr.mjs b/automations/js/src/utils/pr.mjs index 3c8900b8215..3fe24c7f176 100644 --- a/automations/js/src/utils/pr.mjs +++ b/automations/js/src/utils/pr.mjs @@ -89,6 +89,7 @@ export class PullRequest { id: this.nodeId, } ) + this.core.debug(`getPrDetails response: ${JSON.stringify(res, null, 2)}`) const pr = res.node return { isMerged: pr.merged, @@ -137,7 +138,7 @@ export class PullRequest { labelIds, } ) - this.core.debug('addLabels response:', JSON.stringify(res)) + this.core.debug(`addLabels response: ${JSON.stringify(res, null, 2)}`) return res.addLabelsToLabelable.labelable.labels.nodes } diff --git a/automations/js/src/utils/projects.mjs b/automations/js/src/utils/projects.mjs index a38d0be8947..af00e163140 100644 --- a/automations/js/src/utils/projects.mjs +++ b/automations/js/src/utils/projects.mjs @@ -27,12 +27,13 @@ class Project { * owner number * * @param octokit {import('@octokit/rest').Octokit} the Octokit instance to use + * @param core {import('@actions/core')} GitHub Actions toolkit, for logging * @param owner {string} the login of the owner (org) of the project * @param number {number} the number of the project */ - constructor(octokit, owner, number) { + constructor(octokit, core, owner, number) { this.octokit = octokit - + this.core = core this.owner = owner this.number = number } @@ -96,6 +97,7 @@ class Project { number: this.number, } ) + this.core.debug(`getProjectId response: ${JSON.stringify(res, null, 2)}`) const project = res.organization.projectV2 return { projectId: project.id, @@ -126,6 +128,7 @@ class Project { * @returns {Promise} the info of the added card */ async addCard(issueId) { + this.core.info(`Adding card for issue/PR "${issueId}".`) const res = await this.octokit.graphql( `mutation addCard($projectId: ID!, $contentId: ID!) { addProjectV2ItemById(input: { @@ -147,6 +150,7 @@ class Project { contentId: issueId, } ) + this.core.debug(`addCard response: ${JSON.stringify(res, null, 2)}`) const card = res.addProjectV2ItemById.item return { id: card.id, @@ -163,18 +167,23 @@ class Project { * @returns {Promise} the ID of the card that was updated */ async setCustomChoiceField(cardId, fieldName, optionName) { + this.core.info( + `Setting field "${fieldName}" to value "${optionName}" for card "${cardId}".` + ) // Preliminary validation if (!this.fields[fieldName]) { - throw new Error(`Unknown field name "${fieldName}".`) + const msg = `Unknown field name "${fieldName}".` + this.core.error(msg) + throw new Error(msg) } if (!this.fields[fieldName].options[optionName]) { - throw new Error( - `Unknown option name "${optionName}" for field "${fieldName}".` - ) + const msg = `Unknown option name "${optionName}" for field "${fieldName}".` + this.core.error(msg) + throw new Error(msg) } const res = await this.octokit.graphql( - `mutation setCustomField($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) { + `mutation setCustomChoiceField($projectId: ID!, $itemId: ID!, $fieldId: ID!, $optionId: String!) { updateProjectV2ItemFieldValue(input: { projectId: $projectId, itemId: $itemId, @@ -193,6 +202,7 @@ class Project { optionId: this.fields[fieldName].options[optionName], } ) + this.core.debug('setCustomChoiceField response:', JSON.stringify(res)) return res.updateProjectV2ItemFieldValue.projectV2Item.id } @@ -205,6 +215,7 @@ class Project { * @returns {Promise} the ID of the card that was moved */ async moveCard(cardId, destColumn) { + this.core.info(`Moving card "${cardId}" to column "${destColumn}".`) return await this.setCustomChoiceField(cardId, 'Status', destColumn) } } @@ -213,16 +224,17 @@ class Project { * Get the `Project` instance for the project board with the given name. * * @param octokit {import('@octokit/rest').Octokit} the Octokit instance to use + * @param core {import('@actions/core')} GitHub Actions toolkit, for logging * @param name {string} the name of the project (without the 'Openverse' prefix) - * @returns {Project} the `Project` instance to interact with the project board + * @returns {Promise} the `Project` instance to interact with the project board */ -export async function getBoard(octokit, name) { +export async function getBoard(octokit, core, name) { const projectNumber = PROJECT_NUMBERS[name] if (!projectNumber) { throw new Error(`Unknown project board "${name}".`) } - const project = new Project(octokit, 'WordPress', projectNumber) + const project = new Project(octokit, core, 'WordPress', projectNumber) await project.init() return project }