slash-commands: restrict /lgtm to core maintainers, add auto-merge
/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).
This commit is contained in:
parent
c6938c24e6
commit
96c6283aa5
4 changed files with 142 additions and 53 deletions
|
|
@ -9,5 +9,5 @@ GitHub Actions relevant to the management of MCP repositories.
|
||||||
| [`cloudflare-pages-preview/deploy`](cloudflare-pages-preview/README.md) | Deploy a static HTML directory to Cloudflare Pages under a per-PR branch alias, inject noindex headers, and post a sticky PR comment with preview URLs |
|
| [`cloudflare-pages-preview/deploy`](cloudflare-pages-preview/README.md) | Deploy a static HTML directory to Cloudflare Pages under a per-PR branch alias, inject noindex headers, and post a sticky PR comment with preview URLs |
|
||||||
| [`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`, `/hold`, and `/stageblog` PR slash commands gated by team membership + CODEOWNERS; 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) | Set a pending/success/failure commit status based on approval/hold labels — yellow pending check until `/lgtm`, not red |
|
| [`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 |
|
||||||
|
|
|
||||||
|
|
@ -8,17 +8,19 @@ CODEOWNERS.
|
||||||
|
|
||||||
Listens for PR comments and push events, then:
|
Listens for PR comments and push events, then:
|
||||||
|
|
||||||
- **`/lgtm`** — if the commenter is a core maintainer _or_ a CODEOWNER of the
|
- **`/lgtm`** — **core maintainers only** (`always-allow-teams`). Adds an
|
||||||
PR's changed files, adds an `approved` label and submits a bot **APPROVE**
|
`accepted` label, submits a bot **APPROVE** review on behalf of the team,
|
||||||
review with body "Approved on behalf of @user via `/lgtm`."
|
and **enables auto-merge**. The PR merges automatically once all required
|
||||||
- **`/lgtm cancel`** — removes the `approved` label and dismisses the bot
|
checks pass. CODEOWNERS does _not_ grant `/lgtm` authorization.
|
||||||
review.
|
- **`/lgtm cancel`** — removes the `accepted` label, dismisses the bot review,
|
||||||
|
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 bot review body notes
|
||||||
the approval came via `/lgtm force`.
|
the approval came via `/lgtm force`.
|
||||||
- **`/hold`** — adds a `do-not-merge/hold` label (same authorization rule).
|
- **`/hold`** — adds a `do-not-merge/hold` label. Core maintainers _or_
|
||||||
|
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.
|
||||||
- **`/stageblog`** — if the PR touches any blog paths (default `blog/**`),
|
- **`/stageblog`** — if the PR touches any blog paths (default `blog/**`),
|
||||||
dispatches a caller-defined `workflow_dispatch` workflow passing the PR
|
dispatches a caller-defined `workflow_dispatch` workflow passing the PR
|
||||||
|
|
@ -26,27 +28,36 @@ 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 `approved` label, dismisses the bot
|
- **New commits pushed** — removes the `accepted` label, dismisses the bot
|
||||||
review, and posts a brief comment asking for re-approval.
|
review, disables auto-merge, and posts a brief comment asking for
|
||||||
|
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,
|
||||||
must match `^/cmd\b`). Unauthorized attempts, or a PR author trying to `/lgtm`
|
must match `^/cmd\b`). Unauthorized attempts, or a PR author trying to `/lgtm`
|
||||||
their own PR, receive a 👎 reaction with no further noise. Successful commands
|
their own PR, receive a 👎 reaction with no further noise. Successful commands
|
||||||
receive a 👍 reaction.
|
receive a 👍 reaction.
|
||||||
|
|
||||||
The action writes a commit status (`slash-commands/approval` by default) after
|
### Merge gate
|
||||||
each label mutation — this is the merge gate. Mark it as a required status
|
|
||||||
check in branch protection. The commit status can **only** be set to `success`
|
The primary merge gate is **GitHub's native required-reviews rule** plus
|
||||||
by an authorized `/lgtm`; manually adding the `approved` label through the
|
**auto-merge**: configure branch protection to require at least one approving
|
||||||
GitHub UI has no effect on the gate. The caller workflow pairs this with the
|
review, and enable "Allow auto-merge" in repo Settings. `/lgtm` then satisfies
|
||||||
`slash-commands/status` sub-action, which sets the status to `pending` on open
|
the review requirement and turns on auto-merge; the PR merges as soon as CI
|
||||||
and on every push (fresh SHA = fresh approval needed).
|
passes. Manually adding the `accepted` label in the GitHub UI has no effect —
|
||||||
|
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
|
||||||
|
|
||||||
- The `approved` and `do-not-merge/hold` labels **must already exist** in the
|
- **Allow auto-merge** must be enabled in the caller repo
|
||||||
|
(Settings → General → Pull Requests → Allow auto-merge).
|
||||||
|
- 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:
|
||||||
`approved` → `#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`). Without one, only `always-allow-teams` members can act.
|
||||||
|
|
||||||
|
|
@ -127,19 +138,15 @@ jobs:
|
||||||
- uses: modelcontextprotocol/actions/slash-commands@main
|
- uses: modelcontextprotocol/actions/slash-commands@main
|
||||||
with:
|
with:
|
||||||
github-token: ${{ secrets.SLASH_COMMANDS_TOKEN }}
|
github-token: ${{ secrets.SLASH_COMMANDS_TOKEN }}
|
||||||
always-allow-teams: core-maintainers,lead-maintainers
|
# 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
|
||||||
|
|
||||||
# Merge-gate commit status. Mark `slash-commands/approval` as a required
|
|
||||||
# status check in branch protection. This stays pending (yellow) until
|
|
||||||
# /lgtm adds the approved label — no red ❌ on fresh PRs.
|
|
||||||
status:
|
|
||||||
if: github.event_name == 'pull_request_target'
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: modelcontextprotocol/actions/slash-commands/status@main
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Branch protection settings** for this flow: enable _Require a pull request
|
||||||
|
before merging_ → _Require approvals_ (1), and _Allow auto-merge_ in repo
|
||||||
|
Settings. The bot's APPROVE review satisfies the requirement and auto-merge
|
||||||
|
handles the rest.
|
||||||
|
|
||||||
### Enabling `/stageblog` — companion workflow
|
### Enabling `/stageblog` — companion workflow
|
||||||
|
|
||||||
When `stageblog-workflow` is set, you also need a separate workflow in the
|
When `stageblog-workflow` is set, you also need a separate workflow in the
|
||||||
|
|
@ -191,14 +198,16 @@ jobs:
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| `github-token` | ✅ | — | PAT or App token with `read:org` + repo `contents: read` (+ `actions: write` for `/stageblog`). Used for team membership checks, CODEOWNERS fetch, and workflow dispatch. **Default `GITHUB_TOKEN` will not work** for team checks. |
|
| `github-token` | ✅ | — | PAT or App token with `read:org` + repo `contents: read` (+ `actions: write` for `/stageblog`). Used for team membership checks, CODEOWNERS fetch, and workflow dispatch. **Default `GITHUB_TOKEN` will not work** for team checks. |
|
||||||
| `bot-token` | | `${{ github.token }}` | Token for user-visible side effects (comments, reactions, labels, reviews) — defaults to `GITHUB_TOKEN` so these show as `github-actions[bot]`. Rarely needs overriding. |
|
| `bot-token` | | `${{ github.token }}` | Token for user-visible side effects (comments, reactions, labels, reviews) — defaults to `GITHUB_TOKEN` so these show as `github-actions[bot]`. Rarely needs overriding. |
|
||||||
| `approved-label` | | `approved` | Label added by `/lgtm` |
|
| `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` or `/hold` any PR regardless of CODEOWNERS |
|
| `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) |
|
| `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 `approved` label + dismiss bot review + comment when new commits are pushed. Set `'false'` to keep approval across pushes. |
|
| `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. |
|
||||||
| `submit-review` | | `'true'` | Submit a bot APPROVE review alongside the label on `/lgtm`. Set `'false'` to use label only. |
|
| `submit-review` | | `'true'` | Submit a bot APPROVE review alongside the label on `/lgtm`. Set `'false'` to use label only. |
|
||||||
| `status-context` | | `slash-commands/approval` | Commit-status context written after each label mutation. Must match the `status` sub-action's `status-context`. Mark this as a required status check in branch protection. |
|
| `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` |
|
||||||
|
| `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. |
|
||||||
|
|
||||||
|
|
@ -211,6 +220,11 @@ jobs:
|
||||||
|
|
||||||
## `slash-commands/status` sub-action
|
## `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
|
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
|
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
|
`status` job on `pull_request_target` events. Mark the `status-context` value
|
||||||
|
|
@ -275,10 +289,9 @@ 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 commit status (the actual merge gate) is only
|
- **Manual label bypass** — the merge gate is the bot's APPROVE review (or,
|
||||||
ever set to `success` by the `/lgtm` handler after the auth check passes.
|
in the status-check flow, the commit status), both of which are only written
|
||||||
The `status` sub-action sets `pending`/`failure` but never `success`, and the
|
by the `/lgtm` handler after the auth check passes. Adding the `accepted`
|
||||||
caller workflow does not trigger on `labeled` events. Adding the `approved`
|
|
||||||
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
|
||||||
|
|
|
||||||
|
|
@ -1,10 +1,10 @@
|
||||||
name: PR Slash Commands
|
name: PR Slash Commands
|
||||||
description: >-
|
description: >-
|
||||||
Handles /lgtm, /hold, and /stageblog slash commands on PRs. Authorized
|
Handles /lgtm, /hold, and /stageblog slash commands on PRs. Core maintainers
|
||||||
users (core maintainers or CODEOWNERS of the PR's changed files) can approve
|
can approve via /lgtm (adds an accepted label, submits a bot APPROVE review,
|
||||||
via /lgtm (adds a label + bot APPROVE review), block via /hold, or trigger
|
and enables auto-merge). Core maintainers or CODEOWNERS can block via /hold
|
||||||
a blog preview staging workflow via /stageblog. Approval is invalidated when
|
or trigger a blog preview staging workflow via /stageblog. Approval is
|
||||||
new commits are pushed.
|
invalidated when new commits are pushed.
|
||||||
|
|
||||||
inputs:
|
inputs:
|
||||||
github-token:
|
github-token:
|
||||||
|
|
@ -25,9 +25,9 @@ inputs:
|
||||||
required: false
|
required: false
|
||||||
default: ${{ github.token }}
|
default: ${{ github.token }}
|
||||||
approved-label:
|
approved-label:
|
||||||
description: Label added by /lgtm and used by the status job as the merge gate
|
description: Label added by /lgtm to mark the PR as accepted
|
||||||
required: false
|
required: false
|
||||||
default: approved
|
default: accepted
|
||||||
hold-label:
|
hold-label:
|
||||||
description: Label added by /hold to block merging
|
description: Label added by /hold to block merging
|
||||||
required: false
|
required: false
|
||||||
|
|
@ -35,7 +35,8 @@ inputs:
|
||||||
always-allow-teams:
|
always-allow-teams:
|
||||||
description: >-
|
description: >-
|
||||||
Comma-separated team slugs (within the repo's org) whose members may
|
Comma-separated team slugs (within the repo's org) whose members may
|
||||||
/lgtm or /hold any PR regardless of CODEOWNERS
|
/lgtm any PR. Only these teams can /lgtm — CODEOWNERS does not grant
|
||||||
|
/lgtm authorization. Members may also /hold and /stageblog any PR.
|
||||||
required: false
|
required: false
|
||||||
default: core-maintainers
|
default: core-maintainers
|
||||||
force-allow-teams:
|
force-allow-teams:
|
||||||
|
|
@ -61,11 +62,24 @@ inputs:
|
||||||
Set to 'false' to rely on the label alone.
|
Set to 'false' to rely on the label alone.
|
||||||
required: false
|
required: false
|
||||||
default: 'true'
|
default: 'true'
|
||||||
|
enable-auto-merge:
|
||||||
|
description: >-
|
||||||
|
Enable auto-merge on the PR after a successful /lgtm. The PR will merge
|
||||||
|
automatically once all required checks pass. Requires the repo to have
|
||||||
|
"Allow auto-merge" enabled in Settings. Disabled on /lgtm cancel and on
|
||||||
|
push invalidation.
|
||||||
|
required: false
|
||||||
|
default: 'true'
|
||||||
|
auto-merge-method:
|
||||||
|
description: Merge method for auto-merge — one of squash, merge, rebase
|
||||||
|
required: false
|
||||||
|
default: squash
|
||||||
status-context:
|
status-context:
|
||||||
description: >-
|
description: >-
|
||||||
Commit-status context name written after each label mutation. Must match
|
Commit-status context name written after each label mutation. Set to
|
||||||
the status sub-action's status-context so both jobs write to the same
|
empty string to disable status writes (recommended when using
|
||||||
check. Mark this context as a required status check in branch protection.
|
enable-auto-merge + required reviews as the merge gate instead). If set,
|
||||||
|
must match the status sub-action's status-context.
|
||||||
required: false
|
required: false
|
||||||
default: slash-commands/approval
|
default: slash-commands/approval
|
||||||
stageblog-workflow:
|
stageblog-workflow:
|
||||||
|
|
@ -108,6 +122,8 @@ runs:
|
||||||
CODEOWNERS_PATH: ${{ inputs.codeowners-path }}
|
CODEOWNERS_PATH: ${{ inputs.codeowners-path }}
|
||||||
INVALIDATE_ON_PUSH: ${{ inputs.invalidate-on-push }}
|
INVALIDATE_ON_PUSH: ${{ inputs.invalidate-on-push }}
|
||||||
SUBMIT_REVIEW: ${{ inputs.submit-review }}
|
SUBMIT_REVIEW: ${{ inputs.submit-review }}
|
||||||
|
ENABLE_AUTO_MERGE: ${{ inputs.enable-auto-merge }}
|
||||||
|
AUTO_MERGE_METHOD: ${{ inputs.auto-merge-method }}
|
||||||
STATUS_CONTEXT: ${{ inputs.status-context }}
|
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 }}
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,8 @@ module.exports = async ({ github, context, core }) => {
|
||||||
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 statusContext = process.env.STATUS_CONTEXT;
|
||||||
|
const autoMerge = process.env.ENABLE_AUTO_MERGE === 'true';
|
||||||
|
const autoMergeMethod = process.env.AUTO_MERGE_METHOD.toUpperCase();
|
||||||
const reviewMarker = '<!-- slash-commands-lgtm -->';
|
const reviewMarker = '<!-- slash-commands-lgtm -->';
|
||||||
// `github` is authed with bot-token (defaults to GITHUB_TOKEN) so comments,
|
// `github` is authed with bot-token (defaults to GITHUB_TOKEN) so comments,
|
||||||
// reactions, labels, and reviews show as github-actions[bot]. The PAT is
|
// reactions, labels, and reviews show as github-actions[bot]. The PAT is
|
||||||
|
|
@ -116,14 +118,19 @@ module.exports = async ({ github, context, core }) => {
|
||||||
return ok;
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function isAuthorized(user, pr) {
|
async function isInAlwaysAllowTeam(user) {
|
||||||
// 1. always-allow teams
|
|
||||||
for (const team of alwaysAllowTeams) {
|
for (const team of alwaysAllowTeams) {
|
||||||
if (await isTeamMember(user, team)) {
|
if (await isTeamMember(user, team)) {
|
||||||
core.info(`@${user} authorized via always-allow team @${owner}/${team}`);
|
core.info(`@${user} authorized via always-allow team @${owner}/${team}`);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function isAuthorized(user, pr) {
|
||||||
|
// 1. always-allow teams
|
||||||
|
if (await isInAlwaysAllowTeam(user)) return true;
|
||||||
// 2. CODEOWNERS of the PR's changed files (from BASE ref)
|
// 2. CODEOWNERS of the PR's changed files (from BASE ref)
|
||||||
let content;
|
let content;
|
||||||
try {
|
try {
|
||||||
|
|
@ -217,6 +224,7 @@ module.exports = async ({ github, context, core }) => {
|
||||||
// state=success — the status sub-action deliberately never does, so that a
|
// state=success — the status sub-action deliberately never does, so that a
|
||||||
// manually-added approved label (triage+ perms suffice) cannot bypass auth.
|
// manually-added approved label (triage+ perms suffice) cannot bypass auth.
|
||||||
async function setCommitStatus(sha, approved, held) {
|
async function setCommitStatus(sha, approved, held) {
|
||||||
|
if (!statusContext) return;
|
||||||
let state, description;
|
let state, description;
|
||||||
if (held) {
|
if (held) {
|
||||||
state = 'failure';
|
state = 'failure';
|
||||||
|
|
@ -233,6 +241,49 @@ module.exports = async ({ github, context, core }) => {
|
||||||
});
|
});
|
||||||
core.info(`Set commit status ${state} on ${sha.slice(0,7)}: ${description}`);
|
core.info(`Set commit status ${state} on ${sha.slice(0,7)}: ${description}`);
|
||||||
}
|
}
|
||||||
|
async function enableAutoMerge(prNodeId, prNumber) {
|
||||||
|
if (!autoMerge) return;
|
||||||
|
try {
|
||||||
|
await github.graphql(
|
||||||
|
`mutation($id: ID!, $m: PullRequestMergeMethod!) {
|
||||||
|
enablePullRequestAutoMerge(input: {pullRequestId: $id, mergeMethod: $m}) {
|
||||||
|
pullRequest { autoMergeRequest { enabledAt } }
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
{ id: prNodeId, m: autoMergeMethod },
|
||||||
|
);
|
||||||
|
core.info(`Auto-merge enabled (${autoMergeMethod})`);
|
||||||
|
} catch (e) {
|
||||||
|
const msg = e.message || String(e);
|
||||||
|
// enablePullRequestAutoMerge rejects PRs that are already mergeable
|
||||||
|
// ("clean status") because there is nothing to wait for. Merge directly.
|
||||||
|
if (/clean status/i.test(msg)) {
|
||||||
|
core.info('PR already mergeable — merging directly');
|
||||||
|
await github.rest.pulls.merge({
|
||||||
|
owner, repo, pull_number: prNumber,
|
||||||
|
merge_method: autoMergeMethod.toLowerCase(),
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
core.warning(`Could not enable auto-merge: ${msg}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
async function disableAutoMerge(prNodeId) {
|
||||||
|
if (!autoMerge) return;
|
||||||
|
try {
|
||||||
|
await github.graphql(
|
||||||
|
`mutation($id: ID!) {
|
||||||
|
disablePullRequestAutoMerge(input: {pullRequestId: $id}) {
|
||||||
|
pullRequest { id }
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
{ id: prNodeId },
|
||||||
|
);
|
||||||
|
core.info('Auto-merge disabled');
|
||||||
|
} catch (e) {
|
||||||
|
core.info(`Auto-merge disable skipped: ${e.message}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// --- Dispatch ---------------------------------------------------
|
// --- Dispatch ---------------------------------------------------
|
||||||
const ev = context.eventName;
|
const ev = context.eventName;
|
||||||
|
|
@ -245,6 +296,7 @@ module.exports = async ({ github, context, core }) => {
|
||||||
if (!hasApproved) return setResult('noop');
|
if (!hasApproved) return setResult('noop');
|
||||||
await removeLabel(pr.number, approvedLabel);
|
await removeLabel(pr.number, approvedLabel);
|
||||||
await dismissBotApprovals(pr.number, 'New commits pushed — approval invalidated.');
|
await dismissBotApprovals(pr.number, 'New commits pushed — approval invalidated.');
|
||||||
|
await disableAutoMerge(pr.node_id);
|
||||||
await github.rest.issues.createComment({
|
await github.rest.issues.createComment({
|
||||||
owner, repo, issue_number: pr.number,
|
owner, repo, issue_number: pr.number,
|
||||||
body: `New commits were pushed — removed the \`${approvedLabel}\` label. Re-approve with \`/lgtm\`.`,
|
body: `New commits were pushed — removed the \`${approvedLabel}\` label. Re-approve with \`/lgtm\`.`,
|
||||||
|
|
@ -294,6 +346,7 @@ module.exports = async ({ github, context, core }) => {
|
||||||
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 setCommitStatus(pr.head.sha, true, hadHold);
|
||||||
|
await enableAutoMerge(pr.node_id, prNumber);
|
||||||
return setResult('lgtm-forced', actor);
|
return setResult('lgtm-forced', actor);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -303,8 +356,13 @@ module.exports = async ({ github, context, core }) => {
|
||||||
return setResult('self-lgtm-blocked', actor);
|
return setResult('self-lgtm-blocked', actor);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Auth check (same rule for all commands)
|
// Auth check. /lgtm is restricted to always-allow-teams (core maintainers)
|
||||||
if (!(await isAuthorized(actor, pr))) {
|
// only — CODEOWNERS does not grant it. /hold and /stageblog still accept
|
||||||
|
// CODEOWNERS of the PR's changed files.
|
||||||
|
const authorized = cmd === 'lgtm'
|
||||||
|
? await isInAlwaysAllowTeam(actor)
|
||||||
|
: await isAuthorized(actor, pr);
|
||||||
|
if (!authorized) {
|
||||||
await react(commentId, '-1');
|
await react(commentId, '-1');
|
||||||
return setResult('unauthorized', actor);
|
return setResult('unauthorized', actor);
|
||||||
}
|
}
|
||||||
|
|
@ -359,11 +417,13 @@ module.exports = async ({ github, context, core }) => {
|
||||||
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 setCommitStatus(pr.head.sha, true, hadHold);
|
||||||
|
await enableAutoMerge(pr.node_id, prNumber);
|
||||||
return setResult('lgtm-added', actor);
|
return setResult('lgtm-added', actor);
|
||||||
}
|
}
|
||||||
if (cmd === 'lgtm' && cancel) {
|
if (cmd === 'lgtm' && cancel) {
|
||||||
await removeLabel(prNumber, approvedLabel);
|
await removeLabel(prNumber, approvedLabel);
|
||||||
await dismissBotApprovals(prNumber, `@${actor} cancelled approval via /lgtm cancel.`);
|
await dismissBotApprovals(prNumber, `@${actor} cancelled approval via /lgtm cancel.`);
|
||||||
|
await disableAutoMerge(pr.node_id);
|
||||||
await react(commentId, '+1');
|
await react(commentId, '+1');
|
||||||
await setCommitStatus(pr.head.sha, false, hadHold);
|
await setCommitStatus(pr.head.sha, false, hadHold);
|
||||||
return setResult('lgtm-removed', actor);
|
return setResult('lgtm-removed', actor);
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue