Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9e249d0c9e |
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 }}
|
||||
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user