diff --git a/.github/workflows/gardening.yml b/.github/workflows/gardening.yml index d87dd2fdb9e71..a32fee68ea632 100644 --- a/.github/workflows/gardening.yml +++ b/.github/workflows/gardening.yml @@ -37,7 +37,7 @@ jobs: composer build-development working-directory: ./projects/github-actions/repo-gardening - - name: Checkout the PR + - name: Check out the PR if: github.event_name == 'pull_request_target' && github.event.pull_request.state != 'closed' uses: actions/checkout@v4 with: diff --git a/docs/git-workflow.md b/docs/git-workflow.md index c2ec0b5c6aa1c..ccb35850f08b8 100644 --- a/docs/git-workflow.md +++ b/docs/git-workflow.md @@ -80,7 +80,7 @@ If you're working directly with Jetpack trunk and need to update an external con This method assumes you are using the `gh` shorthand from the [Github CLI](https://cli.github.com/): ```sh -# Use the Github CLI to checkout the PR with the PR number - for example gh pr checkout 12345. +# Use the Github CLI to check out the PR with the PR number - for example gh pr checkout 12345. gh pr checkout xxxxx # Run merge-base to check where that branch differed from trunk - example git merge-base update/broken-jetpack-feature trunk. diff --git a/projects/github-actions/repo-gardening/README.md b/projects/github-actions/repo-gardening/README.md index 174af439a2056..df9209c409cd8 100644 --- a/projects/github-actions/repo-gardening/README.md +++ b/projects/github-actions/repo-gardening/README.md @@ -104,7 +104,7 @@ To get the channel ID of the channel where you'd like to post, copy one of the m Certain tasks require filesystem access to the PR, which `pull_request_target` does not provide. To accommodate this, you'll need to include a step to check the PR out in a subdirectory, like ```yaml - - name: Checkout the PR + - name: Check out the PR if: github.event_name == 'pull_request_target' uses: actions/checkout@v4 with: diff --git a/projects/github-actions/repo-gardening/changelog/fix-gardening-catch_max_labels b/projects/github-actions/repo-gardening/changelog/fix-gardening-catch_max_labels new file mode 100644 index 0000000000000..1723c0443417c --- /dev/null +++ b/projects/github-actions/repo-gardening/changelog/fix-gardening-catch_max_labels @@ -0,0 +1,5 @@ +Significance: patch +Type: fixed +Comment: Gardening: Prevent error if more than 100 labels are added to a PR. + + diff --git a/projects/github-actions/repo-gardening/changelog/fix-gardening-catch_max_labels2 b/projects/github-actions/repo-gardening/changelog/fix-gardening-catch_max_labels2 new file mode 100644 index 0000000000000..c0090e71358eb --- /dev/null +++ b/projects/github-actions/repo-gardening/changelog/fix-gardening-catch_max_labels2 @@ -0,0 +1,5 @@ +Significance: patch +Type: fixed +Comment: Gardening: Add an "all the things" label if more than 90 labels are on a PR. + + diff --git a/projects/github-actions/repo-gardening/src/tasks/add-labels/index.js b/projects/github-actions/repo-gardening/src/tasks/add-labels/index.js index 30a95cbbb4357..ea6164ab6b999 100644 --- a/projects/github-actions/repo-gardening/src/tasks/add-labels/index.js +++ b/projects/github-actions/repo-gardening/src/tasks/add-labels/index.js @@ -1,6 +1,7 @@ const { getInput } = require( '@actions/core' ); const debug = require( '../../utils/debug' ); const getFiles = require( '../../utils/get-files' ); +const getLabels = require( '../../utils/labels/get-labels' ); /* global GitHub, WebhookPayloadPullRequest */ @@ -49,7 +50,7 @@ function cleanName( name ) { } /** - * Build a list of labels to add to the issue, based off our file list. + * Build a list of labels to add to the pull request, based off our file list. * * @param {GitHub} octokit - Initialized Octokit REST client. * @param {string} owner - Repository owner. @@ -59,7 +60,7 @@ function cleanName( name ) { * @param {boolean} isRevert - Whether the pull request is a revert. * @return {Promise} Promise resolving to an array of keywords we'll search for. */ -async function getLabelsToAdd( octokit, owner, repo, number, isDraft, isRevert ) { +async function getFileDerivedLabels( octokit, owner, repo, number, isDraft, isRevert ) { const keywords = new Set(); // Get next valid milestone. @@ -320,7 +321,7 @@ async function getLabelsToAdd( octokit, owner, repo, number, isDraft, isRevert ) } /** - * Assigns any issues that are being worked to the author of the matching PR. + * Adds appropriate labels to the specified PR. * * @param {WebhookPayloadPullRequest} payload - Pull request event payload. * @param {GitHub} octokit - Initialized Octokit REST client. @@ -330,26 +331,83 @@ async function addLabels( payload, octokit ) { const { owner, name } = repository; const { draft, title } = pull_request; + // GitHub allows 100 labels on a PR. + // Limit to less than that to allow a buffer for future manual labels. + const maxLabels = 90; + const bigProjectLabel = '[Project] All the things!'; + // Get labels to add to the PR. const isDraft = !! ( pull_request && draft ); // If the PR title includes the word "revert", mark it as such. const isRevert = title.toLowerCase().includes( 'revert' ); - const labels = await getLabelsToAdd( octokit, owner.login, name, number, isDraft, isRevert ); + const fileDerivedLabels = await getFileDerivedLabels( + octokit, + owner.login, + name, + number, + isDraft, + isRevert + ); + + // Grab current labels on the PR. + // We can't rely on payload, as it could be outdated by the time this runs. + const currentLabels = await getLabels( octokit, owner.login, name, number ); + + // This is an array of labels that GitHub doesn't already have. + let labelsToAdd = fileDerivedLabels.filter( label => ! currentLabels.includes( label ) ); - if ( ! labels.length ) { - debug( 'add-labels: Could not find labels to add to that PR. Aborting' ); + // Nothing new was added, so abort. + if ( labelsToAdd.length === 0 ) { + debug( 'add-labels: No new labels to add to that PR. Aborting.' ); return; } - debug( `add-labels: Adding labels ${ labels } to PR #${ number }` ); + // Determine how many labels can safely be added. + let maxLabelsToAdd = Math.max( 0, maxLabels - currentLabels.length ); + + // Overkill, but let's prevent this label from counting toward the max. + const hasBigProjectLabel = currentLabels.includes( bigProjectLabel ); + if ( hasBigProjectLabel ) { + maxLabelsToAdd++; + } + + // If there are too many labels, we need to reduce the label count to keep GitHub happy. + if ( labelsToAdd.length > maxLabelsToAdd ) { + debug( `add-labels: Too many labels! Grouping project labels into '${ bigProjectLabel }'.` ); + + // Filter out project-type labels in deference to bigProjectLabel. + // In theory we could also remove any existing project-type labels here, but for now + // let's not as that would prevent manually adding specific project labels. + const projectLabelRegex = /^(\[Action\]|\[Package\]|\[Plugin\]|\[JS Package\])/; + labelsToAdd = labelsToAdd.filter( label => ! projectLabelRegex.test( label ) ); + + if ( ! hasBigProjectLabel ) { + // Add to the beginning of the labels array in case the array gets truncated later on. + labelsToAdd.unshift( bigProjectLabel ); + } + } else if ( hasBigProjectLabel ) { + await octokit.rest.issues.removeLabel( { + owner: owner.login, + repo: name, + issue_number: number, + name: bigProjectLabel, + } ); + } + // In the rare chance there would still be too many labels... + if ( labelsToAdd.length > maxLabelsToAdd ) { + debug( `add-labels: Limiting to the first ${ maxLabels }.` ); + labelsToAdd.splice( maxLabelsToAdd ); + } + + debug( `add-labels: Adding labels ${ labelsToAdd } to PR #${ number }` ); await octokit.rest.issues.addLabels( { owner: owner.login, repo: name, issue_number: number, - labels, + labels: labelsToAdd, } ); }