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.
This commit is contained in:
parent
b76984fd53
commit
63db292169
5 changed files with 69 additions and 239 deletions
|
|
@ -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 |
|
| [`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) |
|
| [`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`](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 |
|
|
||||||
|
|
|
||||||
|
|
@ -9,16 +9,16 @@ CODEOWNERS.
|
||||||
Listens for PR comments and push events, then:
|
Listens for PR comments and push events, then:
|
||||||
|
|
||||||
- **`/lgtm`** — **core maintainers only** (`always-allow-teams`). Adds an
|
- **`/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
|
and **enables auto-merge**. The PR merges automatically once all required
|
||||||
checks pass. CODEOWNERS does _not_ grant `/lgtm` authorization.
|
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.
|
and disables auto-merge.
|
||||||
- **`/lgtm force`** — same as `/lgtm`, but **only** members of
|
- **`/lgtm force`** — same as `/lgtm`, but **only** members of
|
||||||
`force-allow-teams` (default: core + lead maintainers) may use it, and it
|
`force-allow-teams` (default: core + lead maintainers) may use it, and it
|
||||||
**bypasses the self-approval guard**. Intended as an escape hatch for
|
**bypasses the self-approval guard**. Intended as an escape hatch for
|
||||||
maintainers who need to approve their own PRs. The bot review body notes
|
maintainers who need to approve their own PRs. The review body notes the
|
||||||
the approval came via `/lgtm force`.
|
approval came via `/lgtm force`.
|
||||||
- **`/hold`** — adds a `do-not-merge/hold` label. Core maintainers _or_
|
- **`/hold`** — adds a `do-not-merge/hold` label. Core maintainers _or_
|
||||||
CODEOWNERS of the PR's changed files may use this.
|
CODEOWNERS of the PR's changed files may use this.
|
||||||
- **`/hold cancel`** or **`/unhold`** — removes the hold label.
|
- **`/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
|
letting maintainers manually stage blog previews for fork PRs (where the
|
||||||
normal `pull_request`-triggered preview workflow can't access secrets).
|
normal `pull_request`-triggered preview workflow can't access secrets).
|
||||||
Disabled unless `stageblog-workflow` is set.
|
Disabled unless `stageblog-workflow` is set.
|
||||||
- **New commits pushed** — removes the `accepted` label, dismisses the bot
|
- **New commits pushed** — removes the `accepted` label, dismisses the
|
||||||
review, disables auto-merge, and posts a brief comment asking for
|
approval, disables auto-merge, and posts a brief comment asking for
|
||||||
re-approval.
|
re-approval.
|
||||||
|
|
||||||
Commands are parsed from the **first line** of the comment (case-insensitive,
|
Commands are parsed from the **first line** of the comment (case-insensitive,
|
||||||
|
|
@ -39,75 +39,71 @@ receive a 👍 reaction.
|
||||||
|
|
||||||
### Merge gate
|
### Merge gate
|
||||||
|
|
||||||
The primary merge gate is **GitHub's native required-reviews rule** plus
|
The merge gate is **GitHub's native required-reviews rule** plus
|
||||||
**auto-merge**: configure branch protection to require at least one approving
|
**auto-merge**. Configure branch protection to require at least one approving
|
||||||
review, and enable "Allow auto-merge" in repo Settings. `/lgtm` then satisfies
|
review; `/lgtm` satisfies that via the App's approval and turns on auto-merge.
|
||||||
the review requirement and turns on auto-merge; the PR merges as soon as CI
|
The PR merges as soon as CI passes. Manually adding the `accepted` label in
|
||||||
passes. Manually adding the `accepted` label in the GitHub UI has no effect —
|
the GitHub UI has no effect — the gate is the review, not the label.
|
||||||
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.
|
|
||||||
|
|
||||||
## Prerequisites
|
## 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).
|
(Settings → General → Pull Requests → Allow auto-merge).
|
||||||
- The `accepted` and `do-not-merge/hold` labels **must already exist** in the
|
- The `accepted` and `do-not-merge/hold` labels **must already exist** in the
|
||||||
caller repo (the action does not auto-create them). Suggested colors:
|
caller repo (the action does not auto-create them). Suggested colors:
|
||||||
`accepted` → `#0e8a16`, `do-not-merge/hold` → `#d93f0b`.
|
`accepted` → `#0e8a16`, `do-not-merge/hold` → `#d93f0b`.
|
||||||
- A CODEOWNERS file at `.github/CODEOWNERS` (or pass a different path via
|
- 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
|
## Required secrets
|
||||||
|
|
||||||
Create this secret in the caller repo: GitHub → repo → **Settings → Secrets
|
The action runs as a **GitHub App** so its approvals satisfy branch
|
||||||
and variables → Actions → New repository secret**.
|
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 |
|
| Repo secret name | 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**. |
|
| `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` —
|
### Creating the GitHub App
|
||||||
> 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]`.
|
|
||||||
|
|
||||||
### `SLASH_COMMANDS_TOKEN`
|
Org **Settings → Developer settings → GitHub Apps → New GitHub App**.
|
||||||
|
Configure:
|
||||||
**Option A — fine-grained PAT (recommended for single-repo setups):**
|
|
||||||
|
|
||||||
GitHub → **Settings → Developer settings → Personal access tokens → Fine-grained
|
|
||||||
tokens → Generate new token**. Configure:
|
|
||||||
|
|
||||||
| Section | Setting |
|
| Section | Setting |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Resource owner | `modelcontextprotocol` (or your org) |
|
| Webhook | **Disable** (uncheck "Active") — the App is just an identity |
|
||||||
| Repository access | **Only select repositories** → pick the caller repo |
|
| Repository permissions → Pull requests | **Read & write** |
|
||||||
| Repository permissions → Contents | Read-only |
|
| Repository permissions → Contents | **Read-only** |
|
||||||
| Organization permissions → Members | Read-only |
|
| Repository permissions → Actions | **Read & write** _(only if `/stageblog` is enabled)_ |
|
||||||
| Repository permissions → Actions | Read and write _(only if `stageblog-workflow` is set)_ |
|
| 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,
|
After creating:
|
||||||
labels, reviews) use the workflow's `GITHUB_TOKEN`. **Contents** must remain
|
|
||||||
read-only — the action never writes code.
|
|
||||||
|
|
||||||
**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
|
> **Why an App and not a PAT?** App installation tokens are short-lived
|
||||||
same scopes and store it as `SLASH_COMMANDS_TOKEN`. The App must be installed
|
> (auto-minted per workflow run via `actions/create-github-app-token`), are
|
||||||
on the caller repo _and_ have org-level **Members: read**.
|
> 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
|
## Caller workflow requirements
|
||||||
|
|
||||||
| | |
|
| | |
|
||||||
|---|---|
|
|---|---|
|
||||||
| **Triggers** | `issue_comment` (types: `created`) + `pull_request_target` (types: `opened`, `synchronize`) |
|
| **Triggers** | `issue_comment` (types: `created`) + `pull_request_target` (types: `synchronize`) |
|
||||||
| **Permissions** | `pull-requests: write`, `issues: write`, `contents: read`, `statuses: write`. If `/stageblog` is enabled, also `actions: write`. |
|
| **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. |
|
| **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. |
|
| **No checkout** | The action calls GitHub API only. Do not `actions/checkout` PR code. |
|
||||||
|
|
||||||
|
|
@ -120,31 +116,35 @@ on:
|
||||||
issue_comment:
|
issue_comment:
|
||||||
types: [created]
|
types: [created]
|
||||||
pull_request_target:
|
pull_request_target:
|
||||||
types: [opened, synchronize]
|
types: [synchronize]
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
pull-requests: write
|
|
||||||
issues: write
|
|
||||||
contents: read
|
contents: read
|
||||||
statuses: write
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
handle:
|
handle:
|
||||||
if: >-
|
if: >-
|
||||||
(github.event_name == 'issue_comment' && github.event.issue.pull_request) ||
|
(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
|
runs-on: ubuntu-latest
|
||||||
steps:
|
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
|
- uses: modelcontextprotocol/actions/slash-commands@main
|
||||||
with:
|
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
|
# auto-merge-method: squash # default; also: merge, rebase
|
||||||
# stageblog-workflow: stage-blog.yml # uncomment to enable /stageblog
|
# stageblog-workflow: stage-blog.yml # uncomment to enable /stageblog
|
||||||
```
|
```
|
||||||
|
|
||||||
**Branch protection settings** for this flow: enable _Require a pull request
|
**Branch protection settings** for this flow: enable _Require a pull request
|
||||||
before merging_ → _Require approvals_ (1), and _Allow auto-merge_ in repo
|
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.
|
handles the rest.
|
||||||
|
|
||||||
### Enabling `/stageblog` — companion workflow
|
### Enabling `/stageblog` — companion workflow
|
||||||
|
|
@ -196,18 +196,17 @@ jobs:
|
||||||
|
|
||||||
| Input | Required | Default | Description |
|
| 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`. |
|
| `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. Defaults to `GITHUB_TOKEN` so user-visible actions show as `github-actions[bot]`. The caller workflow must grant it the needed permissions. |
|
| `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 |
|
| `approved-label` | | `accepted` | Label added by `/lgtm` to mark the PR as accepted |
|
||||||
| `hold-label` | | `do-not-merge/hold` | Label added by `/hold` |
|
| `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. |
|
| `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. |
|
| `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`. |
|
| `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. |
|
| `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 a bot APPROVE review alongside the label on `/lgtm`. Set `'false'` to use label only. |
|
| `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". |
|
| `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` |
|
| `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-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. |
|
| `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` |
|
| `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) |
|
| `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
|
## CODEOWNERS pattern support
|
||||||
|
|
||||||
The inline CODEOWNERS parser handles the patterns currently used in MCP repos:
|
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
|
`/lgtm force` escape hatch bypasses this, but is gated strictly to
|
||||||
`force-allow-teams` membership (CODEOWNERS does not grant it) and the
|
`force-allow-teams` membership (CODEOWNERS does not grant it) and the
|
||||||
review body records that `force` was used.
|
review body records that `force` was used.
|
||||||
- **Manual label bypass** — the merge gate is the bot's APPROVE review (or,
|
- **Manual label bypass** — the merge gate is the App's APPROVE review, which
|
||||||
in the status-check flow, the commit status), both of which are only written
|
is only submitted by the `/lgtm` handler after the team-membership check
|
||||||
by the `/lgtm` handler after the auth check passes. Adding the `accepted`
|
passes. Adding the `accepted` label via the GitHub UI is cosmetic.
|
||||||
label via the GitHub UI is cosmetic.
|
|
||||||
- **`/stageblog` SHA pinning** — the dispatch passes `pr.head.sha` captured at
|
- **`/stageblog` SHA pinning** — the dispatch passes `pr.head.sha` captured at
|
||||||
the moment the maintainer comments. The companion workflow **must** check
|
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
|
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.
|
author cannot push new commits between the maintainer's review and the build.
|
||||||
- **Review provenance** — the bot review body always names the real approver
|
- **Review provenance** — the review body always names the real approver
|
||||||
("on behalf of @login"), even though GitHub shows the review as bot-authored.
|
("on behalf of @login"), even though GitHub shows the review as App-authored.
|
||||||
|
|
|
||||||
|
|
@ -74,14 +74,6 @@ inputs:
|
||||||
description: Merge method for auto-merge — one of squash, merge, rebase
|
description: Merge method for auto-merge — one of squash, merge, rebase
|
||||||
required: false
|
required: false
|
||||||
default: squash
|
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:
|
stageblog-workflow:
|
||||||
description: >-
|
description: >-
|
||||||
Workflow file name (e.g. stage-blog.yml) to dispatch when /stageblog is
|
Workflow file name (e.g. stage-blog.yml) to dispatch when /stageblog is
|
||||||
|
|
@ -124,7 +116,6 @@ runs:
|
||||||
SUBMIT_REVIEW: ${{ inputs.submit-review }}
|
SUBMIT_REVIEW: ${{ inputs.submit-review }}
|
||||||
ENABLE_AUTO_MERGE: ${{ inputs.enable-auto-merge }}
|
ENABLE_AUTO_MERGE: ${{ inputs.enable-auto-merge }}
|
||||||
AUTO_MERGE_METHOD: ${{ inputs.auto-merge-method }}
|
AUTO_MERGE_METHOD: ${{ inputs.auto-merge-method }}
|
||||||
STATUS_CONTEXT: ${{ inputs.status-context }}
|
|
||||||
STAGEBLOG_WORKFLOW: ${{ inputs.stageblog-workflow }}
|
STAGEBLOG_WORKFLOW: ${{ inputs.stageblog-workflow }}
|
||||||
STAGEBLOG_PATHS: ${{ inputs.stageblog-paths }}
|
STAGEBLOG_PATHS: ${{ inputs.stageblog-paths }}
|
||||||
ORG_TOKEN: ${{ inputs.github-token }}
|
ORG_TOKEN: ${{ inputs.github-token }}
|
||||||
|
|
|
||||||
|
|
@ -13,7 +13,6 @@ module.exports = async ({ github, context, core }) => {
|
||||||
const codeownersPath = process.env.CODEOWNERS_PATH;
|
const codeownersPath = process.env.CODEOWNERS_PATH;
|
||||||
const invalidateOnPush = process.env.INVALIDATE_ON_PUSH === 'true';
|
const invalidateOnPush = process.env.INVALIDATE_ON_PUSH === 'true';
|
||||||
const submitReview = process.env.SUBMIT_REVIEW === 'true';
|
const submitReview = process.env.SUBMIT_REVIEW === 'true';
|
||||||
const statusContext = process.env.STATUS_CONTEXT;
|
|
||||||
const autoMerge = process.env.ENABLE_AUTO_MERGE === 'true';
|
const autoMerge = process.env.ENABLE_AUTO_MERGE === 'true';
|
||||||
const autoMergeMethod = process.env.AUTO_MERGE_METHOD.toUpperCase();
|
const autoMergeMethod = process.env.AUTO_MERGE_METHOD.toUpperCase();
|
||||||
const reviewMarker = '<!-- slash-commands-lgtm -->';
|
const reviewMarker = '<!-- slash-commands-lgtm -->';
|
||||||
|
|
@ -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) {
|
async function enableAutoMerge(prNodeId, prNumber) {
|
||||||
if (!autoMerge) return;
|
if (!autoMerge) return;
|
||||||
try {
|
try {
|
||||||
|
|
@ -327,10 +303,6 @@ module.exports = async ({ github, context, core }) => {
|
||||||
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
|
const { data: pr } = await github.rest.pulls.get({ owner, repo, pull_number: prNumber });
|
||||||
if (pr.state !== 'open') return setResult('noop', actor);
|
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
|
// /lgtm force — restricted to force-allow-teams. Bypasses the self-approval
|
||||||
// guard and the CODEOWNERS path entirely; only team membership grants this.
|
// guard and the CODEOWNERS path entirely; only team membership grants this.
|
||||||
if (cmd === 'lgtm' && force) {
|
if (cmd === 'lgtm' && force) {
|
||||||
|
|
@ -345,7 +317,6 @@ module.exports = async ({ github, context, core }) => {
|
||||||
await addLabel(prNumber, approvedLabel);
|
await addLabel(prNumber, approvedLabel);
|
||||||
if (submitReview) await submitApproval(prNumber, actor, true);
|
if (submitReview) await submitApproval(prNumber, actor, true);
|
||||||
await react(commentId, '+1');
|
await react(commentId, '+1');
|
||||||
await setCommitStatus(pr.head.sha, true, hadHold);
|
|
||||||
await enableAutoMerge(pr.node_id, prNumber);
|
await enableAutoMerge(pr.node_id, prNumber);
|
||||||
return setResult('lgtm-forced', actor);
|
return setResult('lgtm-forced', actor);
|
||||||
}
|
}
|
||||||
|
|
@ -416,7 +387,6 @@ module.exports = async ({ github, context, core }) => {
|
||||||
await addLabel(prNumber, approvedLabel);
|
await addLabel(prNumber, approvedLabel);
|
||||||
if (submitReview) await submitApproval(prNumber, actor);
|
if (submitReview) await submitApproval(prNumber, actor);
|
||||||
await react(commentId, '+1');
|
await react(commentId, '+1');
|
||||||
await setCommitStatus(pr.head.sha, true, hadHold);
|
|
||||||
await enableAutoMerge(pr.node_id, prNumber);
|
await enableAutoMerge(pr.node_id, prNumber);
|
||||||
return setResult('lgtm-added', actor);
|
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 dismissBotApprovals(prNumber, `@${actor} cancelled approval via /lgtm cancel.`);
|
||||||
await disableAutoMerge(pr.node_id);
|
await disableAutoMerge(pr.node_id);
|
||||||
await react(commentId, '+1');
|
await react(commentId, '+1');
|
||||||
await setCommitStatus(pr.head.sha, false, hadHold);
|
|
||||||
return setResult('lgtm-removed', actor);
|
return setResult('lgtm-removed', actor);
|
||||||
}
|
}
|
||||||
if ((cmd === 'hold' && !cancel)) {
|
if ((cmd === 'hold' && !cancel)) {
|
||||||
await addLabel(prNumber, holdLabel);
|
await addLabel(prNumber, holdLabel);
|
||||||
await react(commentId, '+1');
|
await react(commentId, '+1');
|
||||||
await setCommitStatus(pr.head.sha, hadApproved, true);
|
|
||||||
return setResult('hold-added', actor);
|
return setResult('hold-added', actor);
|
||||||
}
|
}
|
||||||
// /hold cancel or /unhold
|
// /hold cancel or /unhold
|
||||||
await removeLabel(prNumber, holdLabel);
|
await removeLabel(prNumber, holdLabel);
|
||||||
await react(commentId, '+1');
|
await react(commentId, '+1');
|
||||||
await setCommitStatus(pr.head.sha, hadApproved, false);
|
|
||||||
return setResult('hold-removed', actor);
|
return setResult('hold-removed', actor);
|
||||||
};
|
};
|
||||||
|
|
|
||||||
|
|
@ -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}`);
|
|
||||||
Loading…
Reference in a new issue