/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).
84 lines
3.3 KiB
YAML
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}`);
|