actions/slash-commands/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

111 lines
4.3 KiB
YAML

name: PR Slash Commands
description: >-
Handles /lgtm, /hold, and /stageblog slash commands on PRs. Authorized
users (core maintainers or CODEOWNERS of the PR's changed files) can approve
via /lgtm (adds a label + bot APPROVE review), block via /hold, or trigger
a blog preview staging workflow via /stageblog. Approval is invalidated when
new commits are pushed.
inputs:
github-token:
description: >-
Token used for org team-membership checks only. MUST have read:org
scope — the default GITHUB_TOKEN cannot do this. Use a fine-grained PAT
with Organization Members:read, or a GitHub App installation token with
the same.
required: true
bot-token:
description: >-
Token used for all repo API calls: comments, reactions, labels, reviews,
CODEOWNERS fetch, listFiles, and workflow dispatch. Defaults to the
automatic GITHUB_TOKEN so user-visible side effects show as authored by
github-actions[bot]. The caller workflow must grant this token the
necessary permissions (pull-requests: write, issues: write,
contents: read; plus actions: write for /stageblog).
required: false
default: ${{ github.token }}
approved-label:
description: Label added by /lgtm and used by the status job as the merge gate
required: false
default: approved
hold-label:
description: Label added by /hold to block merging
required: false
default: do-not-merge/hold
always-allow-teams:
description: >-
Comma-separated team slugs (within the repo's org) whose members may
/lgtm or /hold any PR regardless of CODEOWNERS
required: false
default: core-maintainers
codeowners-path:
description: Path to the CODEOWNERS file (fetched from the PR base branch)
required: false
default: .github/CODEOWNERS
invalidate-on-push:
description: >-
Remove the approved label and dismiss the bot review when new commits
are pushed to the PR. Set to 'false' to keep approval across pushes.
required: false
default: 'true'
submit-review:
description: >-
Whether to submit a bot APPROVE review alongside the label on /lgtm.
Set to 'false' to rely on the label alone.
required: false
default: 'true'
status-context:
description: >-
Commit-status context name written after each label mutation. Must match
the status sub-action's status-context so both jobs write to the same
check. Mark this context as a required status check in branch protection.
required: false
default: slash-commands/approval
stageblog-workflow:
description: >-
Workflow file name (e.g. stage-blog.yml) to dispatch when /stageblog is
invoked. Leave empty to disable the command. The workflow must accept
inputs 'pr_number' and 'head_sha'.
required: false
default: ''
stageblog-paths:
description: >-
Comma-separated glob patterns (CODEOWNERS-style: **, *, ?, leading /,
trailing /) matched against the PR's changed files. /stageblog is refused
if no file matches.
required: false
default: 'blog/**'
outputs:
result:
description: >-
One of: lgtm-added, lgtm-removed, hold-added, hold-removed, invalidated,
unauthorized, self-lgtm-blocked, stageblog-dispatched, stageblog-not-blog,
stageblog-disabled, noop
value: ${{ steps.handle.outputs.result }}
actor:
description: Login of the user who triggered the action (commenter)
value: ${{ steps.handle.outputs.actor }}
runs:
using: composite
steps:
- name: Handle slash command
id: handle
uses: actions/github-script@v8
env:
APPROVED_LABEL: ${{ inputs.approved-label }}
HOLD_LABEL: ${{ inputs.hold-label }}
ALWAYS_ALLOW_TEAMS: ${{ inputs.always-allow-teams }}
CODEOWNERS_PATH: ${{ inputs.codeowners-path }}
INVALIDATE_ON_PUSH: ${{ inputs.invalidate-on-push }}
SUBMIT_REVIEW: ${{ inputs.submit-review }}
STATUS_CONTEXT: ${{ inputs.status-context }}
STAGEBLOG_WORKFLOW: ${{ inputs.stageblog-workflow }}
STAGEBLOG_PATHS: ${{ inputs.stageblog-paths }}
ORG_TOKEN: ${{ inputs.github-token }}
with:
github-token: ${{ inputs.bot-token }}
script: |
const main = require(process.env.GITHUB_ACTION_PATH + '/main.js');
await main({ github, context, core });