From 9515c7ab931f4ccf3f7d680ab4ec261717f272dc Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 23 Oct 2026 00:41:56 +0000 Subject: [PATCH] docs(policy): keep the ci-gate contract in this repository The README pointed at TanksterAI/.github for the ci-gate job to copy, so a personal-account policy depended on an organisation's docs. The contract now lives next to the ruleset that enforces it: the job, and why each part is there, per GitHub's documented behaviour for required checks (a skipped job reports Success; a job whose dependency failed is skipped; a workflow skipped by a path or branch filter never reports). The snippet passes actionlint and the reconciler's own publish test. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_016kyusa9AMWHzJmRs2buJX3 --- README.md | 50 ++++++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 46 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index eefd04d..3c8699a 100644 --- a/README.md +++ b/README.md @@ -110,10 +110,9 @@ The rulesets, one body each, written into every repository: every pull request into the default branch: a `pull_request` or `pull_request_target` trigger whose branch filters cover it, with no `paths` filter and no `types` list that drops opened, synchronize or - reopened. The job to copy is the one in TanksterAI/.github's README, "The - `ci-gate` contract". This repository's own is the `ci-gate` job in - `self-check.yml`, which needs actionlint, Renovate presets and Scenario - tests. + reopened. The job to copy is under "The `ci-gate` contract" below. This + repository's own is the `ci-gate` job in `self-check.yml`, which needs + actionlint, Renovate presets and Scenario tests. - **`long-lived-branches`**: `main`, `master`, `develop` and everything under `release/` can't be deleted. `delete_branch_on_merge` deletes a merged pull request's head branch, so without this a pull request from `develop` into @@ -128,6 +127,49 @@ for a person to remove. Nothing is skipped today: the profile repository workflow arms auto-merge only behind `review-gate`, so it is gated like the rest. +### The `ci-gate` contract + +A repository opts into the `ci-gate` ruleset by adding one job to a workflow +that runs on its pull requests. It needs no entry in `settings.yml`: the next +reconcile run sees the job and turns the ruleset on. + +```yaml + ci-gate: + if: always() + needs: [lint, test] # every job that must pass before a merge + runs-on: ubuntu-latest + timeout-minutes: 5 + permissions: {} + steps: + - name: Every job passed + env: + RESULTS: ${{ join(needs.*.result, ' ') }} + run: | + set -euo pipefail + for r in ${RESULTS}; do + [ "${r}" = success ] || { echo "::error::a gated job ended '${r}' (${RESULTS})"; exit 1; } + done +``` + +Each line is there for a reason: + +- **The check is named `ci-gate`**: the job's `name:`, or its id when it has + none. It must be a top-level job, not one inside a reusable workflow, + whose checks are named ` / `. +- **`if: always()`**: when a job it needs fails, a job without it is skipped, + and GitHub counts a skipped job as a passing required check. With it, the + gate runs and fails. +- **Anything but `success` fails**, `skipped` and `cancelled` included. So a + job that may legitimately skip itself (a docs-only shortcut, say) does not + belong in `needs`. +- **The workflow runs on every pull request into the default branch.** A + workflow skipped by a `paths` or branch filter never reports, and the + pull request waits on the check forever. The reconciler leaves `ci-gate` + disabled on a repository whose only `ci-gate` job is filtered like that + (see "Publish" above). +- **Only the jobs in `needs` are gated.** A job left out can fail without + blocking a merge. + **Auto-merge arms only behind a gate.** For each of the owner's pull requests, `auto-merge-sweep.yml` reads the rules that actually apply to its base branch (every ruleset, whatever its name) and arms `gh pr merge --auto --squash` only