From 225d66d1f276fb53aa8efabd412379ecb669ba66 Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Sat, 28 Mar 2026 01:46:32 +0000 Subject: [PATCH] slash-commands: gate project status update on a required PR label MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add project-gate-label input. When set, the project status update on /lgtm accept only runs if the PR carries that label — lets callers restrict project-board tracking to a subset of PRs (e.g. only SEPs go to the SEP Review Pipeline, not every accepted PR). onAccept now takes the full PR object instead of (number, nodeId) so it can inspect labels without a second fetch. Label removal via remove-labels-on-accept is unchanged — it runs on every accept regardless of the gate. :house: Remote-Dev: homespace --- slash-commands/README.md | 1 + slash-commands/action.yml | 7 +++++++ slash-commands/main.js | 15 ++++++++++----- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/slash-commands/README.md b/slash-commands/README.md index 6dadcc4..31aebf6 100644 --- a/slash-commands/README.md +++ b/slash-commands/README.md @@ -221,6 +221,7 @@ jobs: | `project-number` | | _(empty)_ | Org-level Project (V2) number to update on accept. Empty = disabled. Adds the PR to the project if not already present. Requires the App to have **Organization Projects: write**. | | `project-status-field` | | `Status` | Name of the single-select field to set on accept. | | `project-accepted-option` | | `Accepted` | Name of the option to set in the status field on accept. | +| `project-gate-label` | | _(empty)_ | If set, only update the project status when the PR has this label (e.g. `SEP`). Empty = update for every accepted PR. Label removal via `remove-labels-on-accept` is not gated — it always runs. | | `stageblog-workflow` | | _(empty)_ | Workflow file name (e.g. `stage-blog.yml`) to dispatch when `/stageblog` is invoked. Empty = command disabled. The workflow must accept `pr_number` and `head_sha` string inputs. | | `stageblog-paths` | | `blog/**` | Comma-separated CODEOWNERS-style glob patterns. `/stageblog` is refused if no changed file matches. | diff --git a/slash-commands/action.yml b/slash-commands/action.yml index 60794fd..d560e56 100644 --- a/slash-commands/action.yml +++ b/slash-commands/action.yml @@ -87,6 +87,12 @@ inputs: description: Name of the option to set in the status field on accept required: false default: Accepted + project-gate-label: + description: >- + If set, only update the project status when the PR has this label. + Leave empty to update the project for every accepted PR. + required: false + default: '' stageblog-workflow: description: >- Workflow file name (e.g. stage-blog.yml) to dispatch when /stageblog is @@ -133,6 +139,7 @@ runs: PROJECT_NUMBER: ${{ inputs.project-number }} PROJECT_STATUS_FIELD: ${{ inputs.project-status-field }} PROJECT_ACCEPTED_OPTION: ${{ inputs.project-accepted-option }} + PROJECT_GATE_LABEL: ${{ inputs.project-gate-label }} STAGEBLOG_WORKFLOW: ${{ inputs.stageblog-workflow }} STAGEBLOG_PATHS: ${{ inputs.stageblog-paths }} with: diff --git a/slash-commands/main.js b/slash-commands/main.js index 3c314e2..f2da594 100644 --- a/slash-commands/main.js +++ b/slash-commands/main.js @@ -20,6 +20,7 @@ module.exports = async ({ github, context, core }) => { const projectNumber = parseInt(process.env.PROJECT_NUMBER, 10) || 0; const projectStatusField = process.env.PROJECT_STATUS_FIELD; const projectAcceptedOption = process.env.PROJECT_ACCEPTED_OPTION; + const projectGateLabel = process.env.PROJECT_GATE_LABEL; const reviewMarker = ''; function setResult(result, actor) { @@ -265,11 +266,15 @@ module.exports = async ({ github, context, core }) => { core.info(`Auto-merge disable skipped: ${e.message}`); } } - async function onAccept(prNumber, prNodeId) { + async function onAccept(pr) { for (const label of removeLabelsOnAccept) { - await removeLabel(prNumber, label); + await removeLabel(pr.number, label); } if (!projectNumber) return; + if (projectGateLabel && !pr.labels.some(l => l.name === projectGateLabel)) { + core.info(`Skipping project update — PR lacks '${projectGateLabel}' label`); + return; + } try { const lookup = await github.graphql( `query($org: String!, $num: Int!, $field: String!) { @@ -304,7 +309,7 @@ module.exports = async ({ github, context, core }) => { item { id } } }`, - { proj: project.id, pr: prNodeId }, + { proj: project.id, pr: pr.node_id }, ); const itemId = add.addProjectV2ItemById.item.id; await github.graphql( @@ -376,7 +381,7 @@ module.exports = async ({ github, context, core }) => { return setResult('force-unauthorized', actor); } await addLabel(prNumber, approvedLabel); - await onAccept(prNumber, pr.node_id); + await onAccept(pr); if (submitReview) await submitApproval(prNumber, actor, true); await react(commentId, '+1'); await enableAutoMerge(pr.node_id, prNumber); @@ -447,7 +452,7 @@ module.exports = async ({ github, context, core }) => { // Execute if (cmd === 'lgtm' && !cancel) { await addLabel(prNumber, approvedLabel); - await onAccept(prNumber, pr.node_id); + await onAccept(pr); if (submitReview) await submitApproval(prNumber, actor); await react(commentId, '+1'); await enableAutoMerge(pr.node_id, prNumber);