Compare commits

...

1 Commits

Author SHA1 Message Date
Pat Sukprasert 9e249d0c9e chore(ci): remove the force-merge label and its CI bypass
The `force-merge` label let a maintainer-effective PR bypass the entire
`Merge Ready` CI gate. In practice this was the "move fast" escape hatch
that produced the May 2026 force-merge backlog (~30 quarantined tests in
known_failures.yaml): it was self-serve (author-as-maintainer needed no
second pair of eyes) and coarse (greened the whole gate regardless of
which check was red), so broken changes rode in alongside flaky ones.

The two legitimate needs are already covered by better-scoped tools:
  - flaky CI  -> quarantine the specific test (tests/known_failures.yaml)
  - emergency -> a repo admin uses GitHub's native "merge without waiting
                 for requirements" affordance (branch protection has
                 enforce_admins=false)

Changes:
  - merge-ready.yml: drop the force-merge trigger, label read, the Load
    maintainers + bypass-eligibility steps, and all FORCE_MERGE/effective
    plumbing; the Evaluate step no longer gates on a bypass.
  - delete force-merge-eligibility.sh.
  - compute-gate.sh: collapse the truth table to CI green/red.
  - reword comments that referenced force-merge as the canonical
    maintainer-effective-waiver example (load-maintainers, should-scan,
    e2e-ui-required/check, authorize-merge-comment) and the design doc.

load-maintainers.sh stays: it is still consumed by the security-scan,
e2e-ui-required, fork-e2e-mirror, and oss-regen workflows.

Co-authored-by: Isaac
2026-06-17 15:43:46 +08:00
8 changed files with 45 additions and 145 deletions
+5 -2
View File
@@ -29,7 +29,9 @@
# uncertainty. A wrong/injected "pass" cannot merge anything on its own: the
# separate required `Maintainer Approval` check still gates merge.
#
# Case 3 mirrors merge-ready/force-merge-eligibility.sh exactly.
# Case 3 applies the maintainer-effective waiver: the `skip-e2e-ui-test` label
# is honoured only when the author is a maintainer, or a maintainer's latest
# decisive review is APPROVED (see below) -- a fork author cannot self-waive.
#
# Reads change/label/review state from the API only -- never checks out or runs
# PR-head code. Called from a base-branch (pull_request_target) job, so a PR
@@ -170,7 +172,8 @@ for m in $MAINTAINERS_LC; do
done
# Latest decisive (non-COMMENTED) review per user; effective if a maintainer's
# latest such review is APPROVED. Same semantics as force-merge-eligibility.sh.
# latest such review is APPROVED. Matches GitHub's UI: a later COMMENTED review
# doesn't supersede an approval, but CHANGES_REQUESTED or DISMISSED does.
APPROVERS=$(gh api "repos/$REPO/pulls/$PR/reviews" --paginate \
--jq '[.[] | select(.state != "COMMENTED")] | group_by(.user.login) | map(max_by(.submitted_at)) | .[] | select(.state == "APPROVED") | .user.login')
for u in $APPROVERS; do
@@ -4,8 +4,8 @@
# `/merge` only enables auto-merge / direct-merges an already-mergeable
# PR -- branch protection still blocks red or unreviewed PRs -- so the
# bar is repo write access, not the stricter MAINTAINER set that gates
# `force-merge`. This keeps `/merge` usable by the whole team while
# blocking outside contributors and drive-by accounts.
# the maintainer-only waivers. This keeps `/merge` usable by the whole
# team while blocking outside contributors and drive-by accounts.
#
# The job-level `if` already pre-filters on author_association as a
# cheap first pass; this is the authoritative check, because an org
+12 -23
View File
@@ -2,40 +2,29 @@
# Single source of truth for the Merge Ready outcome. Downstream steps
# just consume `state`, `short_desc`, and `long_desc`.
#
# Truth table (rows are mutually exclusive; first match wins):
# The gate is green iff every required check is green on its own merits.
# There is no CI bypass: to land despite red required checks, quarantine the
# flaky test (tests/known_failures.yaml) or have a repo admin use GitHub's
# native "merge without waiting for requirements" affordance.
#
# force-merge | effective | CI eval | state | meaning
# ------------+-----------+----------+----------+---------------------------
# true | true | (skipped)| success | maintainer bypass
# * | * | success | success | CI green on its own merits
# true | false | failure | failure | bypass attempted but rejected
# false | false | failure | failure | CI red, no bypass attempted
# CI eval | state | meaning
# ---------+----------+---------------------------
# success | success | CI green on its own merits
# failure | failure | CI red
#
# Row 2 (CI green with ineffective force-merge) is deliberately a
# success: applying the label without maintainer involvement should be
# a no-op, not a penalty.
#
# Env in: FORCE_MERGE, EFFECTIVE, REASON, EVAL, FAILED
# Env in: EVAL, FAILED
# Out: state, short_desc, long_desc on $GITHUB_OUTPUT
set -euo pipefail
if [[ "$FORCE_MERGE" == "true" && "$EFFECTIVE" == "true" ]]; then
STATE=success
SHORT="Bypassed via force-merge ($REASON)"
LONG=":fast_forward: gate is green via \`force-merge\` ($REASON), merging now."
elif [[ "$EVAL" == "success" ]]; then
if [[ "$EVAL" == "success" ]]; then
STATE=success
SHORT="All required checks green"
LONG=":white_check_mark: gate is green, merging now."
elif [[ "$FORCE_MERGE" == "true" ]]; then
STATE=failure
SHORT="force-merge label is not effective: $REASON"
LONG=":no_entry: \`force-merge\` is not effective: $REASON. The merge will not fire until a maintainer approves or one of them retriggers \`/merge\`."
else
STATE=failure
SHORT="Required checks not all green; force-merge requires maintainer approval"
LONG=$':hourglass: gate not green yet. Required checks not satisfied:\n\n'"$FAILED"$'\nThe merge will fire once these turn green, or apply `force-merge` with maintainer approval to bypass.'
SHORT="Required checks not all green"
LONG=$':hourglass: gate not green yet. Required checks not satisfied:\n\n'"$FAILED"$'\nThe merge will fire once these turn green.'
fi
# GitHub commit-status descriptions max out at 140 chars.
@@ -1,63 +0,0 @@
#!/usr/bin/env bash
# Decides whether the `force-merge` label can bypass the CI gate.
#
# Effective only if a maintainer is on the hook for the change: the PR
# author is a maintainer, OR a maintainer's most recent decisive review
# (non-COMMENTED) on the PR is APPROVED.
#
# When the label is applied without either, we surface the reason in a
# red Merge Ready status rather than silently letting the bypass land.
#
# Env in: GH_TOKEN, REPO, PR, FORCE_MERGE, MAINTAINERS
# Out: effective=true|false; reason=<human-readable>
set -euo pipefail
if [[ "$FORCE_MERGE" != "true" ]]; then
echo "effective=false" >> "$GITHUB_OUTPUT"
echo "reason=" >> "$GITHUB_OUTPUT"
exit 0
fi
if [[ -z "${MAINTAINERS// /}" ]]; then
echo "effective=false" >> "$GITHUB_OUTPUT"
echo "reason=no maintainers configured in .github/MAINTAINER on main" >> "$GITHUB_OUTPUT"
exit 0
fi
# GitHub usernames are case-insensitive (login is unique modulo case),
# so compare against a lowercase normalized list. Exact bash string
# compare on the lowercased pair -- not `grep -w`, which treats `-` as
# a word boundary and would let `alice` match `alice-admin`.
MAINTAINERS_LC=$(echo "$MAINTAINERS" | tr '[:upper:]' '[:lower:]')
AUTHOR=$(gh pr view "$PR" --repo "$REPO" --json author --jq '.author.login')
AUTHOR_LC=$(echo "$AUTHOR" | tr '[:upper:]' '[:lower:]')
for m in $MAINTAINERS_LC; do
if [[ "$m" == "$AUTHOR_LC" ]]; then
echo "effective=true" >> "$GITHUB_OUTPUT"
echo "reason=author @$AUTHOR is a maintainer" >> "$GITHUB_OUTPUT"
exit 0
fi
done
# Latest decisive (non-COMMENTED) review per user; keep those whose
# latest state is APPROVED. Matches GitHub's UI: a later COMMENTED
# review doesn't supersede an approval, but CHANGES_REQUESTED or
# DISMISSED does.
APPROVERS=$(gh api "repos/$REPO/pulls/$PR/reviews" --paginate \
--jq '[.[] | select(.state != "COMMENTED")] | group_by(.user.login) | map(max_by(.submitted_at)) | .[] | select(.state == "APPROVED") | .user.login')
for u in $APPROVERS; do
u_lc=$(echo "$u" | tr '[:upper:]' '[:lower:]')
for m in $MAINTAINERS_LC; do
if [[ "$m" == "$u_lc" ]]; then
echo "effective=true" >> "$GITHUB_OUTPUT"
echo "reason=approved by maintainer @$u" >> "$GITHUB_OUTPUT"
exit 0
fi
done
done
echo "effective=false" >> "$GITHUB_OUTPUT"
echo "reason=author @$AUTHOR is not a maintainer and no maintainer has approved this PR yet" >> "$GITHUB_OUTPUT"
@@ -2,7 +2,8 @@
# Loads the maintainer set from .github/MAINTAINER at main's tip.
#
# Always main, never the PR head SHA: otherwise a PR could edit
# MAINTAINER to grant itself force-merge bypass without being merged.
# MAINTAINER to grant itself a maintainer-gated waiver (e.g.
# skip-security-scan, skip-e2e-ui-test) without being merged.
# Defense-in-depth: a PR could still edit *this* workflow to drop
# `?ref=main`, so the remaining defense is `required_pull_request_reviews`
# in branch protection.
@@ -23,7 +24,7 @@ set -e
if [[ $RC -ne 0 || -z "$CONTENT_B64" ]]; then
echo "list=" >> "$GITHUB_OUTPUT"
echo "::warning::.github/MAINTAINER not found on main; force-merge label cannot be effective until the file is merged."
echo "::warning::.github/MAINTAINER not found on main; maintainer-gated waivers cannot be effective until the file is merged."
exit 0
fi
@@ -36,7 +37,7 @@ USERS="${USERS% }"
if [[ -z "${USERS// /}" ]]; then
echo "list=" >> "$GITHUB_OUTPUT"
echo "::warning::.github/MAINTAINER on main has no entries; force-merge label cannot be effective."
echo "::warning::.github/MAINTAINER on main has no entries; maintainer-gated waivers cannot be effective."
exit 0
fi
+1 -1
View File
@@ -24,7 +24,7 @@
# `skip-security-scan` label, but ONLY when the waiver is maintainer-effective
# -- the label is present AND the author is a maintainer, or a maintainer's
# latest decisive review is APPROVED. Same semantics as e2e-ui-required's
# `skip-e2e-ui-test` (and force-merge): the label alone is not enough, so a fork
# `skip-e2e-ui-test`: the label alone is not enough, so a fork
# author cannot self-waive (applying labels needs triage access anyway, and the
# extra maintainer check is defence in depth). All state is read from the API
# (trusted), and this script always runs from `main`, so a PR cannot edit the
+20 -50
View File
@@ -3,7 +3,7 @@ name: Merge Ready
# Posts the "Merge Ready" commit status on the PR head SHA -- the single
# required branch-protection check, backed by the REQUIRED list inside
# this workflow. Triggers: `/merge` comment (write-access commenter only),
# `pull_request` labeled (acts only with `automerge`/`force-merge`),
# `pull_request` labeled (acts only with `automerge`),
# `workflow_run` on same-repo CI completion, `check_suite` completion on a
# `fork-e2e/**` branch (the mirrored fork PR e2e -- a delivery that actually
# fires, unlike the brittle fork-PR `workflow_run` hop it replaces), and
@@ -14,11 +14,12 @@ name: Merge Ready
# Labels:
# automerge enable GitHub auto-merge (one-shot on label add) + opt
# into continuous gate updates (green AND red).
# force-merge bypass posting green regardless of CI, but only when the
# PR author is a maintainer or a maintainer approved; the
# list is read from .github/MAINTAINER at main's tip (never
# the PR head SHA). Bypass without maintainer + red CI posts
# a red status explaining the rejection.
#
# There is no CI bypass label. To land a PR despite red required checks,
# either quarantine the offending flaky test (tests/known_failures.yaml) or,
# for a genuine emergency, a repo admin uses GitHub's native "merge without
# waiting for requirements" affordance (branch protection has
# enforce_admins=false).
on:
# `labeled` only; `workflow_run` re-evaluates on CI completion (same-repo PRs
@@ -64,17 +65,14 @@ jobs:
checks: read
actions: read # evaluate-checks.sh reads GET /actions/runs to classify missing checks
statuses: write
# Fire on automerge/force-merge label adds, PR CI workflow_run completions
# Fire on automerge label adds, PR CI workflow_run completions
# (same-repo and the fork-e2e/** mirror push), `/merge` comments, or a
# workflow_dispatch re-eval; check_suite is a fork-PR fallback. Runs with no
# open PR (push to main, etc.) are dropped by the ctx step.
if: >-
(
github.event_name == 'pull_request' &&
(
github.event.label.name == 'automerge' ||
github.event.label.name == 'force-merge'
)
github.event.label.name == 'automerge'
) ||
(
github.event_name == 'workflow_run' &&
@@ -189,58 +187,34 @@ jobs:
PR: ${{ steps.ctx.outputs.pr }}
run: |
NAMES=$(gh pr view "$PR" --repo "$REPO" --json labels --jq '.labels[].name')
for label in force-merge automerge; do
if echo "$NAMES" | grep -qx "$label"; then
echo "${label//-/_}=true" >> "$GITHUB_OUTPUT"
else
echo "${label//-/_}=false" >> "$GITHUB_OUTPUT"
fi
done
if echo "$NAMES" | grep -qx "automerge"; then
echo "automerge=true" >> "$GITHUB_OUTPUT"
else
echo "automerge=false" >> "$GITHUB_OUTPUT"
fi
- name: Load maintainers
id: maintainers
if: steps.ctx.outputs.skip != 'true'
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
run: bash .github/scripts/merge-ready/load-maintainers.sh
- name: Determine force-merge bypass eligibility
id: bypass
if: steps.ctx.outputs.skip != 'true'
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
PR: ${{ steps.ctx.outputs.pr }}
FORCE_MERGE: ${{ steps.labels.outputs.force_merge }}
MAINTAINERS: ${{ steps.maintainers.outputs.list }}
run: bash .github/scripts/merge-ready/force-merge-eligibility.sh
# post_red gates posting a red status: /merge needs it, automerge /
# force-merge opt in; otherwise post green only so partial CI doesn't
# paint red.
# post_red gates posting a red status: /merge needs it, automerge opts
# in; otherwise post green only so partial CI doesn't paint red.
- name: Determine eligibility
id: eligible
if: steps.ctx.outputs.skip != 'true'
env:
EVENT: ${{ github.event_name }}
AUTOMERGE: ${{ steps.labels.outputs.automerge }}
FORCE_MERGE: ${{ steps.labels.outputs.force_merge }}
run: |
echo "run=true" >> "$GITHUB_OUTPUT"
if [[ "$EVENT" == "issue_comment" ]] || [[ "$AUTOMERGE" == "true" ]] || [[ "$FORCE_MERGE" == "true" ]]; then
if [[ "$EVENT" == "issue_comment" ]] || [[ "$AUTOMERGE" == "true" ]]; then
echo "post_red=true" >> "$GITHUB_OUTPUT"
else
echo "post_red=false" >> "$GITHUB_OUTPUT"
echo "::notice::No 'automerge' or 'force-merge' label; will post Merge Ready only if the gate is green."
echo "::notice::No 'automerge' label; will post Merge Ready only if the gate is green."
fi
- name: Evaluate required checks
id: eval
if: >-
steps.ctx.outputs.skip != 'true' &&
steps.eligible.outputs.run == 'true' &&
steps.bypass.outputs.effective == 'false'
steps.eligible.outputs.run == 'true'
continue-on-error: true
env:
GH_TOKEN: ${{ github.token }}
@@ -254,9 +228,6 @@ jobs:
steps.ctx.outputs.skip != 'true' &&
steps.eligible.outputs.run == 'true'
env:
FORCE_MERGE: ${{ steps.labels.outputs.force_merge }}
EFFECTIVE: ${{ steps.bypass.outputs.effective }}
REASON: ${{ steps.bypass.outputs.reason }}
EVAL: ${{ steps.eval.outcome }}
FAILED: ${{ steps.eval.outputs.failed }}
run: bash .github/scripts/merge-ready/compute-gate.sh
@@ -317,8 +288,7 @@ jobs:
steps.ctx.outputs.skip != 'true' &&
github.event_name == 'pull_request' &&
github.event.action == 'labeled' &&
github.event.label.name == 'automerge' &&
steps.bypass.outputs.effective != 'true'
github.event.label.name == 'automerge'
env:
GH_TOKEN: ${{ github.token }}
REPO: ${{ github.repository }}
+1 -1
View File
@@ -16,7 +16,7 @@ Define how contributor (fork) PRs get **reviewed, approved, and merged** — kee
## Current state (audited)
What already exists in `.github/workflows/`:
- **`maintainer-approval.yml`** — the `Maintainer Approval` status is a required check that stays red until a maintainer approves. Runs on `pull_request_target` from `main`, reads `.github/MAINTAINER` at main's tip (a PR can't self-grant), checks out no PR code. This is the merge gate we're building the human process around.
- **`merge-ready.yml`** — posts the single required `Merge Ready` status backing branch protection; supports `/merge` (write-access commenter), `automerge`/`force-merge` labels, and re-evaluates on CI completion.
- **`merge-ready.yml`** — posts the single required `Merge Ready` status backing branch protection; supports `/merge` (write-access commenter) and the `automerge` label, and re-evaluates on CI completion. (There is no CI bypass label: land despite red required checks by quarantining the flaky test or via a repo admin's native bypass.)
- **`security-gate.yml` / `security-scan.yml`** — the no-secrets deterministic diff scan that gates CI (see the CI proposal).
- **`code-coverage.yml` / `ui-code-coverage.yml`** — currently **report-only**: posts a `Coverage` status whose % rides in the description, never required, can't block merge.
- **`pr-size.yml`** — applies `size/{XS..XL}` labels for reviewer triage only (informational; *not* a gate — see below).