From 63db292169c75ccc2c1a51fd37cf02a522d3bfe1 Mon Sep 17 00:00:00 2001 From: Den Delimarsky Date: Thu, 26 Mar 2026 23:21:16 +0000 Subject: [PATCH] slash-commands: remove commit-status gate, document GitHub App flow The merge gate is now solely the App's APPROVE review satisfying branch protection's required-reviews rule, plus auto-merge. The commit-status mechanism is removed entirely: - delete slash-commands/status sub-action - drop status-context input and setCommitStatus() from main.js - drop the now-unused hadApproved/hadHold label snapshot README rewritten to document the GitHub App setup (MCP_COMMANDER_APP_ID + MCP_COMMANDER_APP_KEY secrets, actions/create-github-app-token to mint the installation token) as the primary flow. The App token is passed as both github-token (team checks) and bot-token (approval/auto-merge) so approvals come from the App identity and always count toward required reviews, independent of the "Allow GitHub Actions to approve" repo setting. --- README.md | 1 - slash-commands/README.md | 175 ++++++++++++------------------- slash-commands/action.yml | 9 -- slash-commands/main.js | 33 ------ slash-commands/status/action.yml | 84 --------------- 5 files changed, 66 insertions(+), 236 deletions(-) delete mode 100644 slash-commands/status/action.yml diff --git a/README.md b/README.md index ae0d7de..09042be 100644 --- a/README.md +++ b/README.md @@ -10,4 +10,3 @@ GitHub Actions relevant to the management of MCP repositories. | [`cloudflare-pages-preview/cleanup`](cloudflare-pages-preview/README.md) | Delete Cloudflare Pages deployments for a closed PR's branch alias and update the sticky comment | | [`hugo-build`](hugo-build/README.md) | Install Go + Hugo extended and build a Hugo site (preview or production mode) | | [`slash-commands`](slash-commands/README.md) | Handle `/lgtm` (core-maintainers only: label + approve + auto-merge), `/hold`, and `/stageblog` PR slash commands; invalidate approval on push | -| [`slash-commands/status`](slash-commands/README.md#slash-commandsstatus-sub-action) | _Optional:_ set a pending/failure commit status for callers using status-check-based gating instead of required-reviews + auto-merge | diff --git a/slash-commands/README.md b/slash-commands/README.md index 0b9c850..649bb5f 100644 --- a/slash-commands/README.md +++ b/slash-commands/README.md @@ -9,16 +9,16 @@ CODEOWNERS. Listens for PR comments and push events, then: - **`/lgtm`** — **core maintainers only** (`always-allow-teams`). Adds an - `accepted` label, submits a bot **APPROVE** review on behalf of the team, + `accepted` label, submits an **APPROVE** review on behalf of the commenter, and **enables auto-merge**. The PR merges automatically once all required checks pass. CODEOWNERS does _not_ grant `/lgtm` authorization. -- **`/lgtm cancel`** — removes the `accepted` label, dismisses the bot review, +- **`/lgtm cancel`** — removes the `accepted` label, dismisses the approval, and disables auto-merge. - **`/lgtm force`** — same as `/lgtm`, but **only** members of `force-allow-teams` (default: core + lead maintainers) may use it, and it **bypasses the self-approval guard**. Intended as an escape hatch for - maintainers who need to approve their own PRs. The bot review body notes - the approval came via `/lgtm force`. + maintainers who need to approve their own PRs. The review body notes the + approval came via `/lgtm force`. - **`/hold`** — adds a `do-not-merge/hold` label. Core maintainers _or_ CODEOWNERS of the PR's changed files may use this. - **`/hold cancel`** or **`/unhold`** — removes the hold label. @@ -28,8 +28,8 @@ Listens for PR comments and push events, then: letting maintainers manually stage blog previews for fork PRs (where the normal `pull_request`-triggered preview workflow can't access secrets). Disabled unless `stageblog-workflow` is set. -- **New commits pushed** — removes the `accepted` label, dismisses the bot - review, disables auto-merge, and posts a brief comment asking for +- **New commits pushed** — removes the `accepted` label, dismisses the + approval, disables auto-merge, and posts a brief comment asking for re-approval. Commands are parsed from the **first line** of the comment (case-insensitive, @@ -39,75 +39,71 @@ receive a 👍 reaction. ### Merge gate -The primary merge gate is **GitHub's native required-reviews rule** plus -**auto-merge**: configure branch protection to require at least one approving -review, and enable "Allow auto-merge" in repo Settings. `/lgtm` then satisfies -the review requirement and turns on auto-merge; the PR merges as soon as CI -passes. Manually adding the `accepted` label in the GitHub UI has no effect — -the gate is the review, not the label. - -An optional commit status (`slash-commands/approval`) is also written for -callers who prefer status-check-based gating — set `status-context: ''` to -disable it. See the [`slash-commands/status` sub-action](#slash-commandsstatus-sub-action) -if you want to use the status-check flow instead of auto-merge. +The merge gate is **GitHub's native required-reviews rule** plus +**auto-merge**. Configure branch protection to require at least one approving +review; `/lgtm` satisfies that via the App's approval and turns on auto-merge. +The PR merges as soon as CI passes. Manually adding the `accepted` label in +the GitHub UI has no effect — the gate is the review, not the label. ## Prerequisites -- **Allow auto-merge** must be enabled in the caller repo +- **A GitHub App** installed on the caller repo with the permissions listed + below. The App's identity is what approves PRs and enables auto-merge. +- **Allow auto-merge** enabled in the caller repo (Settings → General → Pull Requests → Allow auto-merge). - The `accepted` and `do-not-merge/hold` labels **must already exist** in the caller repo (the action does not auto-create them). Suggested colors: `accepted` → `#0e8a16`, `do-not-merge/hold` → `#d93f0b`. - A CODEOWNERS file at `.github/CODEOWNERS` (or pass a different path via - `codeowners-path`). Without one, only `always-allow-teams` members can act. + `codeowners-path`) if you want CODEOWNERS to grant `/hold`/`/stageblog`. + Not required for `/lgtm`. ## Required secrets -Create this secret in the caller repo: GitHub → repo → **Settings → Secrets -and variables → Actions → New repository secret**. +The action runs as a **GitHub App** so its approvals satisfy branch +protection's required-reviews rule without depending on the +"Allow GitHub Actions to create and approve pull requests" repo setting. -| Repo secret name | Action input | Value | -|---|---|---| -| `SLASH_COMMANDS_TOKEN` | `github-token` | Fine-grained PAT (or GitHub App installation token) with **Organization → Members: read** plus **Repository → Contents: read**. If `/stageblog` is enabled, additionally **Repository → Actions: write**. | +| Repo secret name | Value | +|---|---| +| `MCP_COMMANDER_APP_ID` | The App's numeric ID (shown on the App's General settings page) | +| `MCP_COMMANDER_APP_KEY` | The App's private key — full contents of the downloaded `.pem` file, including the `-----BEGIN/END-----` lines | -> **The default `${{ github.token }}` will NOT work** for `github-token` — -> team-membership checks (`GET /orgs/{org}/teams/{team}/memberships/{user}`) -> require `read:org` scope, which the automatic `GITHUB_TOKEN` never has. You -> _must_ provide a PAT or App token. Comments, reactions, labels, and reviews -> use a separate `bot-token` (defaults to `GITHUB_TOKEN`) so they show as -> authored by `github-actions[bot]`. +### Creating the GitHub App -### `SLASH_COMMANDS_TOKEN` - -**Option A — fine-grained PAT (recommended for single-repo setups):** - -GitHub → **Settings → Developer settings → Personal access tokens → Fine-grained -tokens → Generate new token**. Configure: +Org **Settings → Developer settings → GitHub Apps → New GitHub App**. +Configure: | Section | Setting | |---|---| -| Resource owner | `modelcontextprotocol` (or your org) | -| Repository access | **Only select repositories** → pick the caller repo | -| Repository permissions → Contents | Read-only | -| Organization permissions → Members | Read-only | -| Repository permissions → Actions | Read and write _(only if `stageblog-workflow` is set)_ | +| Webhook | **Disable** (uncheck "Active") — the App is just an identity | +| Repository permissions → Pull requests | **Read & write** | +| Repository permissions → Contents | **Read-only** | +| Repository permissions → Actions | **Read & write** _(only if `/stageblog` is enabled)_ | +| Organization permissions → Members | **Read-only** | +| Where can this GitHub App be installed? | **Only on this account** | -The PAT does not need write permissions — all writes (comments, reactions, -labels, reviews) use the workflow's `GITHUB_TOKEN`. **Contents** must remain -read-only — the action never writes code. +After creating: -**Option B — GitHub App installation token:** +1. **Install** — left sidebar → Install App → pick the caller repo(s) +2. **App ID** — copy from the top of General settings → save as + `MCP_COMMANDER_APP_ID` secret +3. **Private key** — scroll to bottom → Generate a private key → + copy `.pem` contents → save as `MCP_COMMANDER_APP_KEY` secret -If your org already uses a bot App, generate an installation token with the -same scopes and store it as `SLASH_COMMANDS_TOKEN`. The App must be installed -on the caller repo _and_ have org-level **Members: read**. +> **Why an App and not a PAT?** App installation tokens are short-lived +> (auto-minted per workflow run via `actions/create-github-app-token`), are +> not tied to any individual's account, and their approvals always count +> toward required reviews. The automatic `GITHUB_TOKEN` lacks `read:org` and +> its approvals are gated by a separate repo setting that may be disabled +> org-wide. ## Caller workflow requirements | | | |---|---| -| **Triggers** | `issue_comment` (types: `created`) + `pull_request_target` (types: `opened`, `synchronize`) | -| **Permissions** | `pull-requests: write`, `issues: write`, `contents: read`, `statuses: write`. If `/stageblog` is enabled, also `actions: write`. | +| **Triggers** | `issue_comment` (types: `created`) + `pull_request_target` (types: `synchronize`) | +| **Permissions** | `contents: read` is sufficient — the App token handles all writes. If falling back to `GITHUB_TOKEN` for `bot-token`, add `pull-requests: write`, `issues: write`. | | **Fork-PR safety** | No special guard needed — `issue_comment` and `pull_request_target` both run in the **base** repo's context with the base workflow definition, so fork authors cannot modify the logic. CODEOWNERS is also fetched from the PR's **base** ref, never the head. | | **No checkout** | The action calls GitHub API only. Do not `actions/checkout` PR code. | @@ -120,31 +116,35 @@ on: issue_comment: types: [created] pull_request_target: - types: [opened, synchronize] + types: [synchronize] permissions: - pull-requests: write - issues: write contents: read - statuses: write jobs: handle: if: >- (github.event_name == 'issue_comment' && github.event.issue.pull_request) || - (github.event_name == 'pull_request_target' && github.event.action == 'synchronize') + github.event_name == 'pull_request_target' runs-on: ubuntu-latest steps: + - uses: actions/create-github-app-token@v2 + id: app-token + with: + app-id: ${{ secrets.MCP_COMMANDER_APP_ID }} + private-key: ${{ secrets.MCP_COMMANDER_APP_KEY }} + - uses: modelcontextprotocol/actions/slash-commands@main with: - github-token: ${{ secrets.SLASH_COMMANDS_TOKEN }} + github-token: ${{ steps.app-token.outputs.token }} + bot-token: ${{ steps.app-token.outputs.token }} # auto-merge-method: squash # default; also: merge, rebase # stageblog-workflow: stage-blog.yml # uncomment to enable /stageblog ``` **Branch protection settings** for this flow: enable _Require a pull request before merging_ → _Require approvals_ (1), and _Allow auto-merge_ in repo -Settings. The bot's APPROVE review satisfies the requirement and auto-merge +Settings. The App's APPROVE review satisfies the requirement and auto-merge handles the rest. ### Enabling `/stageblog` — companion workflow @@ -196,18 +196,17 @@ jobs: | Input | Required | Default | Description | |---|---|---|---| -| `github-token` | ✅ | — | PAT or App token with `read:org` (Organization Members: read). Used **only** for the team-membership check. **Default `GITHUB_TOKEN` will not work** — it lacks `read:org`. | -| `bot-token` | | `${{ github.token }}` | Token for all repo API calls: comments, reactions, labels, reviews, CODEOWNERS fetch, file listing, workflow dispatch, auto-merge. Defaults to `GITHUB_TOKEN` so user-visible actions show as `github-actions[bot]`. The caller workflow must grant it the needed permissions. | +| `github-token` | ✅ | — | Token with `read:org` (Organization Members: read) for the team-membership check. Pass the App installation token. **Default `GITHUB_TOKEN` will not work** — it lacks `read:org`. | +| `bot-token` | | `${{ github.token }}` | Token for all repo API calls: comments, reactions, labels, reviews, CODEOWNERS fetch, file listing, workflow dispatch, auto-merge. Pass the App installation token so approvals come from the App identity. | | `approved-label` | | `accepted` | Label added by `/lgtm` to mark the PR as accepted | | `hold-label` | | `do-not-merge/hold` | Label added by `/hold` | | `always-allow-teams` | | `core-maintainers` | Comma-separated team slugs (in the repo's org) whose members can `/lgtm` any PR. **Only these teams can `/lgtm`** — CODEOWNERS does not grant it. Members may also `/hold`/`/stageblog` any PR. | | `force-allow-teams` | | `core-maintainers,lead-maintainers` | Comma-separated team slugs whose members may use `/lgtm force` (bypasses self-approval guard). CODEOWNERS does **not** grant this — only team membership. | | `codeowners-path` | | `.github/CODEOWNERS` | Path to CODEOWNERS, fetched from the PR's **base** ref (never head — tamper-proof). Grants `/hold` and `/stageblog` only, not `/lgtm`. | -| `invalidate-on-push` | | `'true'` | Remove `accepted` label + dismiss bot review + disable auto-merge + comment when new commits are pushed. Set `'false'` to keep approval across pushes. | -| `submit-review` | | `'true'` | Submit a bot APPROVE review alongside the label on `/lgtm`. Set `'false'` to use label only. | +| `invalidate-on-push` | | `'true'` | Remove `accepted` label + dismiss approval + disable auto-merge + comment when new commits are pushed. Set `'false'` to keep approval across pushes. | +| `submit-review` | | `'true'` | Submit an APPROVE review alongside the label on `/lgtm`. Set `'false'` to use label only. | | `enable-auto-merge` | | `'true'` | Enable auto-merge after `/lgtm` (disabled on `/lgtm cancel` and push invalidation). Requires repo setting "Allow auto-merge". | | `auto-merge-method` | | `squash` | Merge method for auto-merge: `squash`, `merge`, or `rebase` | -| `status-context` | | `slash-commands/approval` | Optional commit-status context. Set to `''` to disable status writes (recommended when using auto-merge + required reviews as the gate). | | `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. | @@ -218,47 +217,6 @@ jobs: | `result` | One of: `lgtm-added`, `lgtm-forced`, `lgtm-removed`, `hold-added`, `hold-removed`, `invalidated`, `unauthorized`, `force-unauthorized`, `self-lgtm-blocked`, `stageblog-dispatched`, `stageblog-not-blog`, `stageblog-disabled`, `noop` | | `actor` | Login of the commenter (empty for non-comment triggers) | -## `slash-commands/status` sub-action - -> **Optional** — only needed if you're using the commit-status merge gate -> instead of (or alongside) the required-reviews + auto-merge flow. If your -> branch protection uses required reviews, you can skip this sub-action and -> set `status-context: ''` on the main action. - -Sets the **initial** `pending` commit status when a PR opens, and forces it -back to `pending` on every push (new SHA = new approval required). Run in a -`status` job on `pull_request_target` events. Mark the `status-context` value -as a required status check in branch protection. - -This sub-action **never sets `success`** — that would let anyone with `triage` -permissions bypass the auth gate by adding the `approved` label manually. -Only the main action's `/lgtm` handler (which runs the team/CODEOWNERS check) -writes `success`. The sub-action also cannot react to the main action's label -writes: `GITHUB_TOKEN`-triggered events do not create workflow runs. - -**States:** - -| Condition | Commit status | Description | -|---|---|---| -| `synchronize` event + `invalidate-on-push: true` | `pending` (🟡) | New commits — re-approve | -| `do-not-merge/hold` present | `failure` (❌) | Blocked by hold | -| otherwise | `pending` (🟡) | Awaiting /lgtm | - -**Inputs:** - -| Input | Required | Default | Description | -|---|---|---|---| -| `github-token` | | `${{ github.token }}` | Token for the commit-status API. Workflow needs `statuses: write`. | -| `hold-label` | | `do-not-merge/hold` | Must match the main action's `hold-label` | -| `status-context` | | `slash-commands/approval` | Name shown in the merge box. **Must match** the main action's `status-context`. Mark this as a required status check. | -| `invalidate-on-push` | | `'true'` | On `synchronize`, force pending regardless of labels. **Must match** the main action's `invalidate-on-push`. | - -**Outputs:** - -| Output | Description | -|---|---| -| `state` | The commit status state set: `pending` or `failure` | - ## CODEOWNERS pattern support The inline CODEOWNERS parser handles the patterns currently used in MCP repos: @@ -289,13 +247,12 @@ Last-match-wins per file, consistent with GitHub's native CODEOWNERS semantics. `/lgtm force` escape hatch bypasses this, but is gated strictly to `force-allow-teams` membership (CODEOWNERS does not grant it) and the review body records that `force` was used. -- **Manual label bypass** — the merge gate is the bot's APPROVE review (or, - in the status-check flow, the commit status), both of which are only written - by the `/lgtm` handler after the auth check passes. Adding the `accepted` - label via the GitHub UI is cosmetic. +- **Manual label bypass** — the merge gate is the App's APPROVE review, which + is only submitted by the `/lgtm` handler after the team-membership check + passes. Adding the `accepted` label via the GitHub UI is cosmetic. - **`/stageblog` SHA pinning** — the dispatch passes `pr.head.sha` captured at the moment the maintainer comments. The companion workflow **must** check out `inputs.head_sha` (not `refs/pull/N/head` or `pr.head.ref`) so a fork author cannot push new commits between the maintainer's review and the build. -- **Review provenance** — the bot review body always names the real approver - ("on behalf of @login"), even though GitHub shows the review as bot-authored. +- **Review provenance** — the review body always names the real approver + ("on behalf of @login"), even though GitHub shows the review as App-authored. diff --git a/slash-commands/action.yml b/slash-commands/action.yml index 2be0347..4fa714e 100644 --- a/slash-commands/action.yml +++ b/slash-commands/action.yml @@ -74,14 +74,6 @@ inputs: description: Merge method for auto-merge — one of squash, merge, rebase required: false default: squash - status-context: - description: >- - Commit-status context name written after each label mutation. Set to - empty string to disable status writes (recommended when using - enable-auto-merge + required reviews as the merge gate instead). If set, - must match the status sub-action's status-context. - required: false - default: slash-commands/approval stageblog-workflow: description: >- Workflow file name (e.g. stage-blog.yml) to dispatch when /stageblog is @@ -124,7 +116,6 @@ runs: SUBMIT_REVIEW: ${{ inputs.submit-review }} ENABLE_AUTO_MERGE: ${{ inputs.enable-auto-merge }} AUTO_MERGE_METHOD: ${{ inputs.auto-merge-method }} - STATUS_CONTEXT: ${{ inputs.status-context }} STAGEBLOG_WORKFLOW: ${{ inputs.stageblog-workflow }} STAGEBLOG_PATHS: ${{ inputs.stageblog-paths }} ORG_TOKEN: ${{ inputs.github-token }} diff --git a/slash-commands/main.js b/slash-commands/main.js index 8af1c5d..62913d2 100644 --- a/slash-commands/main.js +++ b/slash-commands/main.js @@ -13,7 +13,6 @@ module.exports = async ({ github, context, core }) => { const codeownersPath = process.env.CODEOWNERS_PATH; const invalidateOnPush = process.env.INVALIDATE_ON_PUSH === 'true'; const submitReview = process.env.SUBMIT_REVIEW === 'true'; - const statusContext = process.env.STATUS_CONTEXT; const autoMerge = process.env.ENABLE_AUTO_MERGE === 'true'; const autoMergeMethod = process.env.AUTO_MERGE_METHOD.toUpperCase(); const reviewMarker = ''; @@ -218,29 +217,6 @@ module.exports = async ({ github, context, core }) => { } } } - // GITHUB_TOKEN-triggered label events do NOT create workflow runs, so the - // status sub-action cannot react to our addLabel/removeLabel calls. Set the - // commit status directly here. This is also the ONLY path that may set - // state=success — the status sub-action deliberately never does, so that a - // manually-added approved label (triage+ perms suffice) cannot bypass auth. - async function setCommitStatus(sha, approved, held) { - if (!statusContext) return; - let state, description; - if (held) { - state = 'failure'; - description = `Blocked by ${holdLabel} — comment /unhold to clear`; - } else if (approved) { - state = 'success'; - description = `Approved via /lgtm`; - } else { - state = 'pending'; - description = `Awaiting /lgtm from a maintainer or CODEOWNER`; - } - await github.rest.repos.createCommitStatus({ - owner, repo, sha, state, description, context: statusContext, - }); - core.info(`Set commit status ${state} on ${sha.slice(0,7)}: ${description}`); - } async function enableAutoMerge(prNodeId, prNumber) { if (!autoMerge) return; try { @@ -327,10 +303,6 @@ module.exports = async ({ github, context, core }) => { const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber }); if (pr.state !== 'open') return setResult('noop', actor); - const preLabels = new Set(pr.labels.map(l => l.name)); - const hadApproved = preLabels.has(approvedLabel); - const hadHold = preLabels.has(holdLabel); - // /lgtm force — restricted to force-allow-teams. Bypasses the self-approval // guard and the CODEOWNERS path entirely; only team membership grants this. if (cmd === 'lgtm' && force) { @@ -345,7 +317,6 @@ module.exports = async ({ github, context, core }) => { await addLabel(prNumber, approvedLabel); if (submitReview) await submitApproval(prNumber, actor, true); await react(commentId, '+1'); - await setCommitStatus(pr.head.sha, true, hadHold); await enableAutoMerge(pr.node_id, prNumber); return setResult('lgtm-forced', actor); } @@ -416,7 +387,6 @@ module.exports = async ({ github, context, core }) => { await addLabel(prNumber, approvedLabel); if (submitReview) await submitApproval(prNumber, actor); await react(commentId, '+1'); - await setCommitStatus(pr.head.sha, true, hadHold); await enableAutoMerge(pr.node_id, prNumber); return setResult('lgtm-added', actor); } @@ -425,18 +395,15 @@ module.exports = async ({ github, context, core }) => { await dismissBotApprovals(prNumber, `@${actor} cancelled approval via /lgtm cancel.`); await disableAutoMerge(pr.node_id); await react(commentId, '+1'); - await setCommitStatus(pr.head.sha, false, hadHold); return setResult('lgtm-removed', actor); } if ((cmd === 'hold' && !cancel)) { await addLabel(prNumber, holdLabel); await react(commentId, '+1'); - await setCommitStatus(pr.head.sha, hadApproved, true); return setResult('hold-added', actor); } // /hold cancel or /unhold await removeLabel(prNumber, holdLabel); await react(commentId, '+1'); - await setCommitStatus(pr.head.sha, hadApproved, false); return setResult('hold-removed', actor); }; diff --git a/slash-commands/status/action.yml b/slash-commands/status/action.yml deleted file mode 100644 index bf31891..0000000 --- a/slash-commands/status/action.yml +++ /dev/null @@ -1,84 +0,0 @@ -name: PR Slash Commands — Merge-Gate Status -description: >- - Sets the initial pending commit status on the PR head SHA at open time and - forces it back to pending on push (new SHA, approval invalidated). Never - sets success — only the main slash-commands action's auth-gated /lgtm - handler may do that, so a manually-added approved label cannot bypass the - merge gate. Sets failure if the hold label is present. - -inputs: - github-token: - description: >- - Token for the commit-status API call. The caller workflow must grant - this token `statuses: write`. - required: false - default: ${{ github.token }} - hold-label: - description: Label that blocks merging (set by /hold) - required: false - default: do-not-merge/hold - status-context: - description: >- - The commit-status context name. This is what appears in the PR merge box - and what you mark as a required status check in branch protection. - required: false - default: slash-commands/approval - invalidate-on-push: - description: >- - When 'true', a push (synchronize event) forces the status to pending - regardless of labels. Must match the main slash-commands action's - invalidate-on-push setting. - required: false - default: 'true' - -outputs: - state: - description: The commit status state that was set (pending or failure) - value: ${{ steps.set.outputs.state }} - -runs: - using: composite - steps: - - name: Set commit status - id: set - uses: actions/github-script@v8 - env: - HOLD_LABEL: ${{ inputs.hold-label }} - STATUS_CONTEXT: ${{ inputs.status-context }} - INVALIDATE_ON_PUSH: ${{ inputs.invalidate-on-push }} - with: - github-token: ${{ inputs.github-token }} - script: | - const { owner, repo } = context.repo; - const pr = context.payload.pull_request; - if (!pr) { - core.setFailed('slash-commands/status must run on a pull_request(_target) event'); - return; - } - const sha = pr.head.sha; - const holdLabel = process.env.HOLD_LABEL; - const statusContext = process.env.STATUS_CONTEXT; - const invalidateOnPush = process.env.INVALIDATE_ON_PUSH === 'true'; - - // Never set success here. The only success writer is the main - // action's /lgtm handler, which is auth-gated. Reading the approved - // label would let anyone with triage+ perms bypass auth by labeling - // manually. This sub-action's job is: pending on open, pending on - // push (new SHA needs fresh approval), failure on hold. - let state, description; - if (invalidateOnPush && context.payload.action === 'synchronize') { - state = 'pending'; - description = `New commits pushed — awaiting re-approval via /lgtm`; - } else if (pr.labels.some(l => l.name === holdLabel)) { - state = 'failure'; - description = `Blocked by ${holdLabel} — comment /unhold to clear`; - } else { - state = 'pending'; - description = `Awaiting /lgtm from a maintainer or CODEOWNER`; - } - - await github.rest.repos.createCommitStatus({ - owner, repo, sha, state, description, context: statusContext, - }); - core.setOutput('state', state); - core.info(`Set status ${state} on ${sha.slice(0,7)}: ${description}`);