Skip to content

Commit

Permalink
Repo Gardening: update Type column in project board based off label (#…
Browse files Browse the repository at this point in the history
…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
  • Loading branch information
jeherve authored Nov 13, 2024
1 parent e2ab2c1 commit e1fd272
Show file tree
Hide file tree
Showing 5 changed files with 159 additions and 91 deletions.
Original file line number Diff line number Diff line change
@@ -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.
Original file line number Diff line number Diff line change
@@ -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' );
Expand All @@ -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;
Expand All @@ -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.
Expand Down Expand Up @@ -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 }` );

Expand Down Expand Up @@ -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' ) )
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down Expand Up @@ -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<string>} - 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.
*
Expand Down Expand Up @@ -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<string>} - 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 },
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -553,27 +616,29 @@ 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;
}

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;
Expand All @@ -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*/, '' );

Expand Down Expand Up @@ -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.
Expand All @@ -694,7 +755,7 @@ async function updateBoard( payload, octokit, isBugIssue, priorityLabels ) {
payload,
projectInfo,
projectItemId,
isBugIssue,
isBug,
priorityLabels
);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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<string>} 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;
31 changes: 0 additions & 31 deletions projects/github-actions/repo-gardening/src/utils/labels/is-bug.js

This file was deleted.

0 comments on commit e1fd272

Please sign in to comment.