Skip to content

Commit

Permalink
Repo Gardening: allow escalating issues to multiple groups (#39973)
Browse files Browse the repository at this point in the history
* Repo Gardening: allow escalating issues to multiple groups

Up until now, once we had sent one Slack notification about an issue, we couldn't send another to a different team. This was problematic given that we attempt to warn about escalated issues to 2 different teams:

- Product Ambassador HEs in the Triage Issues task
- Dev teams in the Update Board task

By splitting things into 2 different labels ("[Status] Priority Review Triggered" for dev teams, "[Status] Escalated to Product Ambassadors" for Product ambassadors), we can now send notifications to two different teams.

It is not the perfect solution since ideally, we'd want to allow notifications per Slack channel instead of per team, but:

- There is currently no better way to warn about an issue already escalated than to add a label.
- We cannot really use the Slack channel ID in the label name, that would be odd.

* Simplify check before we send slack notification

This was added in #30100, but it ends up blocking legitimate messages from being sent.

* Move checking for labels out of the Slack notification function

This should be handled by each task calling the function, for more flexibility, and a simpler notification function.

* Remove Update Board task

We'll merge it into the existing triage issues task. It is not very intuitive to have this being two different tasks, when both perform actions that can be described as issue triage. It also makes the codebase more approachable, and will allow us to extract / centralize some of the logic that was used in both tasks.

* Extract more label detection functions

* Extract AI labeling into its own file

It should make it a bit easier for folks to contribute.

* Add new Project board management "sub-task" for triageIssues

* Document the new actions performed by the Triage issues task

* Extract method used to find the priority of an issue

* Bring it all together in the update triageIssues task

* Fix argument order

* Add missing argument

* Remove unnecessary state check (we check earlier) & add logging

* Remove files that are no longer used

* Add label when issue is automatically triaged to project board

See pfVjQF-55-p2#comment-119

* Update label name

See pfVjQF-55-p2#comment-127

* Switch to major version bump

See #39973 (comment)

* Update to new label value format

See #39973 (comment)

* Simplify check for existing labels on issues

See #39973 (comment)

This is similar to what was already done in 148b28c

* Better clarify where issue priority comes from, and use it

Let's not attempt to add a label to an issue when it is already on the issue.

See #39973 (comment)
  • Loading branch information
jeherve authored Nov 12, 2024
1 parent a512d13 commit f05867c
Show file tree
Hide file tree
Showing 14 changed files with 492 additions and 487 deletions.
7 changes: 3 additions & 4 deletions projects/github-actions/repo-gardening/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ Here is the current list of tasks handled by this action:
- Triage Issues (`triageIssues`): Adds labels to issues based on issue content, and send Slack notifications depending on Priority.
- Gather support references (`gatherSupportReferences`): Adds a new comment with a list of all support references on the issue, and escalates that issue via a Slack message if needed.
- Reply to customers Reminder ( `replyToCustomersReminder` ): sends a Slack message about closed issues to remind Automatticians to update customers.
- Update Board (`updateBoard`): this task updates specific columns in a GitHub Project board, based on labels applied to an issue.

Some of the tasks are may not satisfy your needs. If that's the case, you can use the `tasks` option to limit the action to the list of tasks you need in your repo. See the example below to find out more.

Expand Down Expand Up @@ -84,9 +83,9 @@ The action relies on the following parameters.
- (Optional) `slack_he_triage_channel` is the Slack public channel ID where messages for the HE Triage team will be posted. The value should be stored in a secret.
- (Optional) `slack_quality_channel` is the Slack public channel ID where issues needing extra triage / escalation will be sent. The value should be stored in a secret.
- (Optional) `reply_to_customers_threshold`. It is optional, and defaults to 10. It is the minimum number of support references needed to trigger an alert that we need to reply to customers.
- (Optional) `triage_projects_token` is a [personal access token](https://github.com/settings/tokens/new) with `repo` and `project` scopes. The token should be stored in a secret. This is required if you want to use the `updateBoard` task.
- (Optional) `project_board_url` is the URL of a GitHub Project Board. We'll automate some of the work on that board in the `updateBoard` task.
- (Optional) `labels_team_assignments` is a list of features you can provide, with matching team names, as specified in the "Team" field of your GitHub Project Board used for the `updateBoard` task, and lists of labels in use in your repository.
- (Optional) `triage_projects_token` is a [personal access token](https://github.com/settings/tokens/new) with `repo` and `project` scopes. The token should be stored in a secret. This is required if you want to use the `triageIssues` task.
- (Optional) `project_board_url` is the URL of a GitHub Project Board. We'll automate some of the work on that board in the `triageIssues` task.
- (Optional) `labels_team_assignments` is a list of features you can provide, with matching team names, as specified in the "Team" field of your GitHub Project Board used for the `triageIssues` task, and lists of labels in use in your repository.
- (Optional) `openai_api_key` is the API key for OpenAI. This is required if you want to use the `triageIssues` task to automatically add labels to your issues. **Note**: this option is only available for Automattic-hosted repositories.

#### How to create a Slack bot and get your SLACK_TOKEN
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: major
Type: changed

Board Triage: remove updateBoard task. It will now be part of the existing triageIssues task.
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
Significance: patch
Type: changed

Issue escalation: allow escalating the issue to multiple teams.
6 changes: 0 additions & 6 deletions projects/github-actions/repo-gardening/src/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,18 +11,12 @@ const notifyDesign = require( './tasks/notify-design' );
const notifyEditorial = require( './tasks/notify-editorial' );
const replyToCustomersReminder = require( './tasks/reply-to-customers-reminder' );
const triageIssues = require( './tasks/triage-issues' );
const updateBoard = require( './tasks/update-board' );
const wpcomCommitReminder = require( './tasks/wpcom-commit-reminder' );
const debug = require( './utils/debug' );
const ifNotClosed = require( './utils/if-not-closed' );
const ifNotFork = require( './utils/if-not-fork' );

const automations = [
{
event: 'issues',
action: [ 'labeled', 'opened' ],
task: updateBoard,
},
{
event: 'pull_request_target',
action: [ 'opened', 'synchronize', 'edited' ],
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,177 @@
const { getInput } = require( '@actions/core' );
const debug = require( '../../utils/debug' );
const getAvailableLabels = require( '../../utils/labels/get-available-labels' );
const getLabels = require( '../../utils/labels/get-labels' );
const sendOpenAiRequest = require( '../../utils/openai/send-request' );

/* global GitHub, WebhookPayloadIssue */

/**
* Request a list of matching labels from Open AI that can be applied to the issue,
* based on the issue contents.
*
* @param {GitHub} octokit - Initialized Octokit REST client.
* @param {string} owner - Repository owner.
* @param {string} repo - Repository name.
* @param {string} title - Issue title.
* @param {string} body - Issue body.
*
* @return {Promise<Object>} Promise resolving to an object of labels to apply to the issue, and their explanations.
*/
async function fetchOpenAiLabelsSuggestions( octokit, owner, repo, title, body ) {
const suggestions = { labels: [], explanations: {} };

// Get all the Feature and Feature Group labels in the repo.
const pattern = /^(\[Feature\]|\[Feature Group\])/;
const repoLabels = await getAvailableLabels( octokit, owner, repo, pattern );

// If no labels are found, bail.
if ( repoLabels.length === 0 ) {
debug(
'triage-issues > auto-label: No labels found in the repository. Aborting OpenAI request.'
);
return suggestions;
}

const prompt = `You must analyse the content below, composed of 2 data points pulled from a GitHub issue:
- a title
- the issue body
Here is the issue title. It is the most important part of the text you must analyse:
- ${ title }
Here is the issue body:
**********************
${ body }
**********************
You must analyze this content, and suggest labels related to the content.
The labels you will suggest must all come from the list below.
Each item on the list of labels below follows the following format: - <label name>: <label description if it exists>
${ repoLabels
.map( label => `- ${ label.name }${ label?.description ? `: ${ label.description }` : '' }` )
.join( '\n' ) }
Analyze the issue and suggest relevant labels. Rules:
- Use only existing labels provided.
- Include 1 '[Feature Group]' label.
- Include 1 to 3 '[Feature]' labels.
- Briefly explain each label choice in 1 sentence.
- Format your response as a JSON object, with each suggested label as a key, and your explanation of the label choice as the value.
Example response format:
{
"[Feature Group] User Interaction & Engagement": "The issue involves how users interact with the platform.",
"[Feature] Comments": "Specifically, it's about the commenting functionality."
}`;

const response = await sendOpenAiRequest( prompt, 'json_object' );
debug( `triage-issues > auto-label: OpenAI response: ${ response }` );

let parsedResponse;
try {
parsedResponse = JSON.parse( response );
} catch ( error ) {
debug(
`triage-issues > auto-label: OpenAI did not send back the expected JSON-formatted response. Error: ${ error }`
);
return suggestions;
}

const labels = Object.keys( parsedResponse );

if ( ! Array.isArray( labels ) ) {
return suggestions;
}

return { labels, explanations: parsedResponse };
}

/**
* Automatically add labels to issues.
*
* When an issue is first opened, parse its contents, send them to OpenAI,
* and add labels if any matching labels can be found.
* During testing, we'll run it for any issues, not just opened,
* but only on issues with the "[Experiment] Automated labeling" label.
* In that situation, we'll add a label to note that the issue was processed.
*
* @param {WebhookPayloadIssue} payload - Issue event payload.
* @param {GitHub} octokit - Initialized Octokit REST client.
*/
async function aiLabeling( payload, octokit ) {
const { issue, repository } = payload;
const { number, body, title } = issue;
const { owner, name } = repository;
const ownerLogin = owner.login;

const issueLabels = await getLabels( octokit, ownerLogin, name, number );
const apiKey = getInput( 'openai_api_key' );

if ( ! apiKey ) {
debug( `triage-issues > auto-label: No OpenAI key is provided. Bail.` );
return;
}

if (
issueLabels.includes( '[Experiment] Automated labeling' ) &&
! issueLabels.includes( '[Experiment] AI labels added' )
) {
debug(
`triage-issues > auto-label: Fetching labels suggested by OpenAI for issue #${ number }`
);
const { labels, explanations } = await fetchOpenAiLabelsSuggestions(
octokit,
ownerLogin,
name,
title,
body
);

if ( labels.length === 0 ) {
debug( `triage-issues > auto-label: No labels suggested by OpenAI for issue #${ number }` );
} else {
// Add the suggested labels to the issue.
debug(
`triage-issues > auto-label: Adding the following labels to issue #${ number }, as suggested by OpenAI: ${ labels.join(
', '
) }`
);
await octokit.rest.issues.addLabels( {
owner: ownerLogin,
repo: name,
issue_number: number,
labels,
} );

// During testing, post a comment on the issue with the explanations.
const explanationComment = `**OpenAI suggested the following labels for this issue:**
${ Object.entries( explanations )
.map( ( [ labelName, explanation ] ) => `- ${ labelName }: ${ explanation }` )
.join( '\n' ) }`;

await octokit.rest.issues.createComment( {
owner: ownerLogin,
repo: name,
issue_number: number,
body: explanationComment,
} );

// Add a label to note that the issue was processed.
await octokit.rest.issues.addLabels( {
owner: ownerLogin,
repo: name,
issue_number: number,
labels: [ '[Experiment] AI labels added' ],
} );
}
}
}
module.exports = aiLabeling;
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
const debug = require( '../../utils/debug' );
const getLabels = require( '../../utils/labels/get-labels' );
const findPriority = require( '../../utils/parse-content/find-priority' );

/* global GitHub, WebhookPayloadIssue */

/**
* Try to figure out the priority of the issue based off its contents and existing labels.
*
* @param {WebhookPayloadIssue} payload - Issue event payload.
* @param {GitHub} octokit - Initialized Octokit REST client.
*
* @return {Promise<object>} Promise resolving to an object, with 2 keys:
* - labels is an array of priority Labels matching this issue,
* - inferred is a boolean, returns true if the priority was inferred from the issue contents.
*/
async function getIssuePriority( payload, octokit ) {
const {
issue: { number, body },
repository: {
owner: { login: ownerLogin },
name,
},
} = payload;

const labels = await getLabels( octokit, ownerLogin, name, number );
const priorityLabels = labels.filter(
label => label.match( /^\[Pri\].*$/ ) && label !== '[Pri] TBD'
);
if ( priorityLabels.length > 0 ) {
debug(
`triage-issues > issue priority: Issue #${ number } has the following priority labels: ${ priorityLabels.join(
', '
) }`
);
return {
labels: priorityLabels,
inferred: false,
};
}

// If the issue does not have Priority labels yet, let's try to infer one from the issue contents.
debug(
`triage-issues > issue priority: Finding priority for issue #${ number } based off the issue contents.`
);
const priority = findPriority( body );
debug(
`triage-issues > issue priority: Priority inferred from the issue contents for issue #${ number } is ${ priority }`
);

return {
labels: [ `[Pri] ${ priority }` ],
inferred: true,
};
}

module.exports = getIssuePriority;
Loading

0 comments on commit f05867c

Please sign in to comment.