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.
/lgtm now only accepts members of always-allow-teams (core-maintainers
by default) — CODEOWNERS no longer grants /lgtm authorization. /hold
and /stageblog keep the team-or-CODEOWNERS rule.
On a successful /lgtm the action now:
- adds the accepted label (renamed from approved)
- submits the bot APPROVE review
- enables auto-merge (new)
Auto-merge is disabled on /lgtm cancel and on push invalidation. If the
PR is already mergeable when auto-merge is enabled, the action merges
directly instead.
This shifts the merge gate from the slash-commands/approval commit
status to GitHub's native required-reviews + auto-merge. The commit
status is now optional (set status-context to empty to skip). The
status sub-action is documented as optional for callers who want to
keep status-check-based gating.
New inputs: enable-auto-merge (default true), auto-merge-method
(default squash).
Adds a /lgtm force variant gated to force-allow-teams (default:
core-maintainers,lead-maintainers). Bypasses the self-approval guard
and the CODEOWNERS path — only team membership grants it. The bot
review body records that force was used, and the result output is
lgtm-forced (or force-unauthorized on denial).
/hold force and /stageblog force are rejected as noop rather than
silently treated as the base command.
/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).
The previous attempt (constructing orgClient via require('@actions/github')
inside the inline script) fails with MODULE_NOT_FOUND — the package is
bundled into github-script's dist/index.js and not resolvable even from
the eval'd inline script.
Switch to a raw fetch() call for the team membership endpoint. Node 20
(github-script@v8) has global fetch, so this has zero dependencies and
is immune to Octokit's auth-hook override behavior.
@actions/github is only resolvable from inside the github-script bundle,
not from main.js. Build the PAT-authed Octokit client in action.yml and
pass it into main as orgClient. Switch the team-membership check to use
orgClient.rest.teams.getMembershipForUserInOrg instead of a raw request
with header override.
PR #5 broke the action in production — the require() fails with
MODULE_NOT_FOUND because actions/github-script bundles @actions/github
into dist/index.js; it's not in node_modules and not resolvable from
a sibling require()'d file.
Fix: flip the architecture. github-script's 'github' client now uses
bot-token (GITHUB_TOKEN), so the single 'github' object serves all
user-visible operations. The PAT is passed via env (ORG_TOKEN) and
used ONLY for the team-membership call via github.request() with an
explicit authorization header override.
Side effect: PAT requirements shrink further. It now ONLY needs
Organization Members:read — no repo perms, no Actions:write. All
repo-level operations (including workflow dispatch) use GITHUB_TOKEN
with the workflow's permissions: block.
Previously the action used the PAT (SLASH_COMMANDS_TOKEN) for everything,
so comments and reviews showed as authored by whoever owns the PAT. This
is confusing — the PAT exists only because team-membership checks need
read:org, which GITHUB_TOKEN can never have.
New bot-token input (default ${{ github.token }}) is used for all
user-visible side effects. The PAT is now scoped to what actually needs
it: team membership, CODEOWNERS fetch, listFiles, pulls.get, and
workflow dispatch.
Side benefit: the PAT's required scopes shrink. It no longer needs
Pull requests:write or Issues:write — just Contents:read + Members:read
(+ Actions:write for /stageblog). README updated to reflect this.
listFiles pagination + workflow dispatch can take a few seconds. Without
an early ack, the commenter gets no feedback until the outcome reaction
lands. 👀 confirms the command was recognized and authorized while the
heavier work runs.
Extends slash-commands with a new /stageblog command that lets authorized
maintainers trigger a workflow_dispatch for staging blog previews from fork
PRs, where the normal pull_request-triggered preview cannot access secrets.
- Captures pr.head.sha at comment time and passes it to the dispatched
workflow so fork authors cannot push new commits between review and build
- Disabled unless stageblog-workflow input is set
- Refuses with stageblog-not-blog result if no changed file matches
stageblog-paths (default blog/**)
- Documents companion workflow pattern, additional PAT scope (Actions:
write), and SHA-pinning safety requirement
Move the handler logic out of the YAML script block into a standalone
main.js loaded via require(GITHUB_ACTION_PATH). This improves
readability and makes the JS easier to lint and unit-test directly.