actions/slash-commands/status/action.yml
Den Delimarsky 332da5ae1a
slash-commands: write commit status directly from /lgtm handler
/lgtm was adding the approved label and posting the bot review, but the
merge-gate commit status stayed pending forever. Root cause: the status
sub-action was designed to react to the labeled/unlabeled events emitted
when main.js mutates labels, but main.js writes via GITHUB_TOKEN and GitHub
suppresses workflow runs for GITHUB_TOKEN-triggered events. The event
cascade never happened.

Fix: main.js now calls createCommitStatus directly after each of the four
label mutations (/lgtm, /lgtm cancel, /hold, /unhold), computing the new
state from the pre-mutation label snapshot so cross-label cases (lgtm while
held, unhold while approved) resolve correctly.

The status sub-action is reduced to setting pending on open and on push.
It no longer reads the approved label at all — that branch is removed, and
the approved-label input with it. This closes a bypass: previously anyone
with triage permissions could manually add the approved label, and on the
resulting labeled event the sub-action would set success. Now the only
success writer is the auth-gated /lgtm path.

Caller workflows should trim pull_request_target types to [opened,
synchronize]. The labeled/unlabeled/reopened triggers are dead weight:
labeled/unlabeled never fired for bot writes, and reopened would overwrite
a valid success with pending (commit statuses persist on the SHA across
close/reopen).
2026-03-12 13:15:43 +00:00

84 lines
3.3 KiB
YAML

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}`);