From e1fd27219bf3f531371044dd83fbc9cd485d5e53 Mon Sep 17 00:00:00 2001 From: Jeremy Herve Date: Wed, 13 Nov 2024 19:01:51 +0100 Subject: [PATCH] Repo Gardening: update Type column in project board based off label (#40110) * Repo Gardening: update Type column in project board based off label If we can extract the type of the issue from the labels, we will update our project board's "Type" column accordingly. * Get rid of extra label check when an issue is being labeled The added label is already returned by getLabels. * Simplify issue type management I made some changes to the One Board settings to make things simpler. * Fix project type extraction * Fix string check --- .../update-board-triage-issue-type-management | 4 + .../src/tasks/triage-issues/index.js | 13 +- .../src/tasks/triage-issues/update-board.js | 169 ++++++++++++------ .../src/utils/labels/get-issue-type.js | 33 ++++ .../repo-gardening/src/utils/labels/is-bug.js | 31 ---- 5 files changed, 159 insertions(+), 91 deletions(-) create mode 100644 projects/github-actions/repo-gardening/changelog/update-board-triage-issue-type-management create mode 100644 projects/github-actions/repo-gardening/src/utils/labels/get-issue-type.js delete mode 100644 projects/github-actions/repo-gardening/src/utils/labels/is-bug.js diff --git a/projects/github-actions/repo-gardening/changelog/update-board-triage-issue-type-management b/projects/github-actions/repo-gardening/changelog/update-board-triage-issue-type-management new file mode 100644 index 0000000000000..c086215eea4e5 --- /dev/null +++ b/projects/github-actions/repo-gardening/changelog/update-board-triage-issue-type-management @@ -0,0 +1,4 @@ +Significance: minor +Type: added + +Board Triage: automatically add the issue type to our project board when a Type label can be found in the issue. diff --git a/projects/github-actions/repo-gardening/src/tasks/triage-issues/index.js b/projects/github-actions/repo-gardening/src/tasks/triage-issues/index.js index 5b4cf709cac64..a402b8d3ba188 100644 --- a/projects/github-actions/repo-gardening/src/tasks/triage-issues/index.js +++ b/projects/github-actions/repo-gardening/src/tasks/triage-issues/index.js @@ -1,6 +1,6 @@ const { getInput } = require( '@actions/core' ); const debug = require( '../../utils/debug' ); -const isBug = require( '../../utils/labels/is-bug' ); +const getIssueType = require( '../../utils/labels/get-issue-type' ); const findPlatforms = require( '../../utils/parse-content/find-platforms' ); const findPlugins = require( '../../utils/parse-content/find-plugins' ); const formatSlackMessage = require( '../../utils/slack/format-slack-message' ); @@ -23,7 +23,7 @@ const updateBoard = require( './update-board' ); * @param {GitHub} octokit - Initialized Octokit REST client. */ async function triageIssues( payload, octokit ) { - const { action, issue, label = {}, repository } = payload; + const { action, issue, repository } = payload; const { number, body, state } = issue; const { owner, name, full_name } = repository; const ownerLogin = owner.login; @@ -35,7 +35,8 @@ async function triageIssues( payload, octokit ) { } const { labels: priorityLabels, inferred } = await getIssuePriority( payload, octokit ); - const isBugIssue = await isBug( octokit, ownerLogin, name, number, action, label ); + const issueType = await getIssueType( octokit, ownerLogin, name, number ); + const isBug = issueType === 'Bug'; const qualityChannel = getInput( 'slack_quality_channel' ); // If this is a new issue, add labels. @@ -71,7 +72,7 @@ async function triageIssues( payload, octokit ) { } // Add priority label to the issue, if none already existed on the issue. - if ( priorityLabels.length === 1 && isBugIssue && inferred ) { + if ( priorityLabels.length === 1 && isBug && inferred ) { const inferredPriority = priorityLabels[ 0 ]; debug( `triage-issues: Adding ${ inferredPriority } label to issue #${ number }` ); @@ -103,11 +104,11 @@ async function triageIssues( payload, octokit ) { } // Triage the issue to a Project board if necessary and possible. - await updateBoard( payload, octokit, isBugIssue, priorityLabels ); + await updateBoard( payload, octokit, issueType, priorityLabels ); // Send a Slack notification to Product ambassadors if the issue is important. if ( - isBugIssue && + isBug && qualityChannel && priorityLabels.length > 0 && ( priorityLabels.includes( '[Pri] BLOCKER' ) || priorityLabels.includes( '[Pri] High' ) ) diff --git a/projects/github-actions/repo-gardening/src/tasks/triage-issues/update-board.js b/projects/github-actions/repo-gardening/src/tasks/triage-issues/update-board.js index 11844ba6e7a59..84a1f006e8482 100644 --- a/projects/github-actions/repo-gardening/src/tasks/triage-issues/update-board.js +++ b/projects/github-actions/repo-gardening/src/tasks/triage-issues/update-board.js @@ -99,6 +99,14 @@ async function getProjectDetails( octokit, projectBoardLink ) { projectInfo.team = teamField; // Info about our Team column (id as well as possible values). } + // Extract the ID of the Type field. + const typeField = projectDetails[ projectInfo.ownerType ]?.projectV2.fields.nodes.find( + field => field.name === 'Type' + ); + if ( typeField ) { + projectInfo.type = typeField; // Info about our Type column (id as well as possible values). + } + return projectInfo; } @@ -266,6 +274,68 @@ async function setPriorityField( octokit, projectInfo, projectItemId, priorityTe return newProjectItemId; // New Project item ID (what we just edited). String. } +/** + * Set the Type field for a project item, to match the Type label if it exists. + * + * @param {GitHub} octokit - Initialized Octokit REST client. + * @param {object} projectInfo - Info about our project board. + * @param {string} projectItemId - The ID of the project item. + * @param {string} typeText - Type of our issue (must match an existing column in the project board). + * @return {Promise} - The new project item id. + */ +async function setTypeField( octokit, projectInfo, projectItemId, typeText ) { + const { + projectNodeId, // Project board node ID. + type: { + id: typeFieldId, // ID of the type field. + options, + }, + } = projectInfo; + + // Find the ID of the Type option that matches our issue type label. + const typeOptionId = options.find( option => option.name === typeText )?.id; + if ( ! typeOptionId ) { + debug( + `triage-issues > update-board: Type ${ typeText } does not exist as a column option in the project board.` + ); + return ''; + } + + const projectNewItemDetails = await octokit.graphql( + `mutation ( $input: UpdateProjectV2ItemFieldValueInput! ) { + set_type: updateProjectV2ItemFieldValue( input: $input ) { + projectV2Item { + id + } + } + }`, + { + input: { + projectId: projectNodeId, + itemId: projectItemId, + fieldId: typeFieldId, + value: { + singleSelectOptionId: typeOptionId, + }, + }, + } + ); + + const newProjectItemId = projectNewItemDetails.set_type.projectV2Item.id; + if ( ! newProjectItemId ) { + debug( + `triage-issues > update-board: Failed to set the "${ typeText }" type for this project item.` + ); + return ''; + } + + debug( + `triage-issues > update-board: Project item ${ newProjectItemId } was moved to "${ typeText }" type.` + ); + + return newProjectItemId; // New Project item ID (what we just edited). String. +} + /** * Update the "Status" field in our project board. * @@ -439,18 +509,11 @@ async function loadTeamAssignments( ownerLogin ) { * @param {object} payload - Issue event payload. * @param {object} projectInfo - Info about our project board. * @param {string} projectItemId - The ID of the project item. - * @param {boolean} isBugIssue - Is the issue a bug? + * @param {boolean} isBug - Is the issue a bug? * @param {Array} priorityLabels - Array of priority labels. * @return {Promise} - The new project item id. */ -async function assignTeam( - octokit, - payload, - projectInfo, - projectItemId, - isBugIssue, - priorityLabels -) { +async function assignTeam( octokit, payload, projectInfo, projectItemId, isBug, priorityLabels ) { const { action, issue: { number }, @@ -499,7 +562,7 @@ async function assignTeam( slack_id && priorityLabels.length > 0 && ( priorityLabels.includes( '[Pri] BLOCKER' ) || priorityLabels.includes( '[Pri] High' ) ) && - isBugIssue + isBug ) { debug( `triage-issues > update-board: Issue #${ number } has the following priority labels: ${ priorityLabels.join( @@ -553,19 +616,21 @@ async function assignTeam( * * @param {WebhookPayloadIssue} payload - Issue event payload. * @param {GitHub} octokit - Initialized Octokit REST client. - * @param {boolean} isBugIssue - Is the issue a bug? + * @param {string} issueType - Type of the issue, defined by a "[Type]" label on the issue. * @param {Array} priorityLabels - Array of Priority Labels matching this issue. */ -async function updateBoard( payload, octokit, isBugIssue, priorityLabels ) { +async function updateBoard( payload, octokit, issueType, priorityLabels ) { const { issue: { number }, repository: { owner, name }, } = payload; const ownerLogin = owner.login; + const isBug = issueType === 'Bug'; + const projectToken = getInput( 'triage_projects_token' ); if ( ! projectToken ) { - setFailed( + debug( `triage-issues > update-board: Input triage_projects_token is required but missing. Aborting.` ); return; @@ -573,7 +638,7 @@ async function updateBoard( payload, octokit, isBugIssue, priorityLabels ) { const projectBoardLink = getInput( 'project_board_url' ); if ( ! projectBoardLink ) { - setFailed( + debug( `triage-issues > update-board: No project board link provided. Cannot triage to a board. Aborting.` ); return; @@ -587,63 +652,61 @@ async function updateBoard( payload, octokit, isBugIssue, priorityLabels ) { // Get details about our project board, to use in our requests. const projectInfo = await getProjectDetails( projectOctokit, projectBoardLink ); if ( Object.keys( projectInfo ).length === 0 || ! projectInfo.projectNodeId ) { - setFailed( + debug( `triage-issues > update-board: we cannot fetch info about our project board. Aborting task.` ); return; } - // Check if the issue is already on the project board. If so, return its ID on the board. + // Check if the issue is already on the project board. If so, return its ID. let projectItemId = await getIssueProjectItemId( projectOctokit, projectInfo, name, number ); - if ( ! projectItemId ) { + + // If we have no ID, that means the issue isn't on the board yet. + // If it is a bug, add it to the board. + if ( ! projectItemId && isBug ) { debug( - `triage-issues > update-board: Issue #${ number } is not on our project board. Let's check if it is a bug. If it is, we will want to add it to our board.` + `triage-issues > update-board: Issue #${ number } is not on our project board. Let's add it.` ); + projectItemId = await addIssueToBoard( payload, projectOctokit, projectInfo ); - // If the issue is not a bug, stop. - if ( ! isBugIssue ) { + if ( projectItemId ) { + // Set the "Needs Triage" status for our issue on the board. debug( - `triage-issues > update-board: Issue #${ number } is not classified as a bug. Aborting.` + `triage-issues > update-board: Setting the "Needs Triage" status for this project item, issue #${ number }.` ); - return; + projectItemId = await setStatusField( + projectOctokit, + projectInfo, + projectItemId, + 'Needs Triage' + ); + } else { + debug( `triage-issues > update-board: Failed to add issue to project board.` ); } + } - // If the issue is a bug, add it to our project board. + // Check if the type needs to be updated for that issue. + // We do need info about the type column in the board to be able to do that. + if ( issueType && projectInfo.type ) { debug( - `triage-issues > update-board: Issue #${ number } is a bug. Adding it to our project board.` + `triage-issues > update-board: Issue #${ number } has a type label set, ${ issueType }. Let’s ensure the Type field of the project board matches that.` ); - projectItemId = await addIssueToBoard( payload, projectOctokit, projectInfo ); - if ( ! projectItemId ) { - debug( `triage-issues > update-board: Failed to add issue to project board. Aborting.` ); - return; - } - // Set the "Needs Triage" status for our issue on the board. - debug( - `triage-issues > update-board: Setting the "Needs Triage" status for this project item, issue #${ number }.` - ); - projectItemId = await setStatusField( - projectOctokit, - projectInfo, - projectItemId, - 'Needs Triage' - ); + // So far, our project board only supports the following types: 'Bug', 'Enhancement', and 'Task' + if ( [ 'Bug', 'Enhancement', 'Task' ].includes( issueType ) ) { + projectItemId = await setTypeField( projectOctokit, projectInfo, projectItemId, issueType ); + } } // Check if priority needs to be updated for that issue. - if ( priorityLabels.length > 0 ) { + // We do need info about the priority column in the board to be able to do that. + if ( priorityLabels.length > 0 && projectInfo.priority ) { debug( `triage-issues > update-board: Issue #${ number } has the following priority labels: ${ priorityLabels.join( ', ' ) }` ); - // If we have no info about the Priority column, stop. - if ( ! projectInfo.priority ) { - debug( `triage-issues > update-board: No priority column found in project board. Aborting.` ); - return; - } - // Remove the "[Pri]" prefix from our priority labels. We also only need one label. const priorityText = priorityLabels[ 0 ].replace( /^\[Pri\]\s*/, '' ); @@ -677,14 +740,12 @@ async function updateBoard( payload, octokit, isBugIssue, priorityLabels ) { projectItemId, 'Needs Core/3rd Party Fix' ); - return; + } else { + debug( + `triage-issues > update-board: Setting the "Triaged" status for this project item, issue #${ number }.` + ); + await setStatusField( projectOctokit, projectInfo, projectItemId, 'Triaged' ); } - - // Set the status field for this project item. - debug( - `triage-issues > update-board: Setting the "Triaged" status for this project item, issue #${ number }.` - ); - await setStatusField( projectOctokit, projectInfo, projectItemId, 'Triaged' ); } // Try to assign the issue to a specific team, if we have a mapping of teams <> labels and a matching label on the issue. @@ -694,7 +755,7 @@ async function updateBoard( payload, octokit, isBugIssue, priorityLabels ) { payload, projectInfo, projectItemId, - isBugIssue, + isBug, priorityLabels ); } diff --git a/projects/github-actions/repo-gardening/src/utils/labels/get-issue-type.js b/projects/github-actions/repo-gardening/src/utils/labels/get-issue-type.js new file mode 100644 index 0000000000000..24cd78b618dd8 --- /dev/null +++ b/projects/github-actions/repo-gardening/src/utils/labels/get-issue-type.js @@ -0,0 +1,33 @@ +const getLabels = require( './get-labels' ); + +/* global GitHub */ + +/** + * Extract the type of the issue, based of the the "[Type]" labels found in that issue. + * If multiple Type labels can be found in the issue, we cannot extract a specific type. + * We will consequently return an empty string. + * + * @param {GitHub} octokit - Initialized Octokit REST client. + * @param {string} owner - Repository owner. + * @param {string} repo - Repository name. + * @param {string} number - Issue number. + * @return {Promise} Promise resolving to a string, the type of the issue, extracted from the label. + */ +async function getIssueType( octokit, owner, repo, number ) { + const labels = await getLabels( octokit, owner, repo, number ); + + // Extract type labels, and return them all in a new array, but without the [Type] prefix. + const typeLabels = labels + .filter( label => label.startsWith( '[Type]' ) ) + .map( label => label.replace( '[Type] ', '' ) ); + + // If there are multiple types defined in the issue, we cannot extract a specific type. + // We will consequently return an empty string. + if ( typeLabels.length !== 1 ) { + return ''; + } + + return typeLabels[ 0 ]; +} + +module.exports = getIssueType; diff --git a/projects/github-actions/repo-gardening/src/utils/labels/is-bug.js b/projects/github-actions/repo-gardening/src/utils/labels/is-bug.js deleted file mode 100644 index 35ec1761e2d20..0000000000000 --- a/projects/github-actions/repo-gardening/src/utils/labels/is-bug.js +++ /dev/null @@ -1,31 +0,0 @@ -const getLabels = require( './get-labels' ); - -/* global GitHub */ - -/** - * Ensure the issue is a bug, by looking for a "[Type] Bug" label. - * It could be an existing label, - * or it could be that it's being added as part of the event that triggers this action. - * - * @param {GitHub} octokit - Initialized Octokit REST client. - * @param {string} owner - Repository owner. - * @param {string} repo - Repository name. - * @param {string} number - Issue number. - * @param {string} action - Action that triggered the event ('opened', 'reopened', 'labeled'). - * @param {object} eventLabel - Label that was added to the issue. - * @return {Promise} Promise resolving to boolean. - */ -async function isBug( octokit, owner, repo, number, action, eventLabel ) { - // If the issue has a "[Type] Bug" label, it's a bug. - const labels = await getLabels( octokit, owner, repo, number ); - if ( labels.includes( '[Type] Bug' ) ) { - return true; - } - - // Next, check if the current event was a [Type] Bug label being added. - if ( 'labeled' === action && eventLabel.name && '[Type] Bug' === eventLabel.name ) { - return true; - } -} - -module.exports = isBug;