diff --git a/.github/workflows/pr-require-checks.yml b/.github/workflows/pr-require-checks.yml index a47d826da7..1d61261a00 100644 --- a/.github/workflows/pr-require-checks.yml +++ b/.github/workflows/pr-require-checks.yml @@ -5,8 +5,9 @@ on: workflow_dispatch: # NO PUSH TRIGGER. Every commit reaches main through the merge queue (the # repository's merge-queue ruleset, with the org `pull_request` rule and no - # bypass actor), and the queue merges a commit only after this workflow - # passed on that same SHA in its merge-group run. A push run re-tested the + # bypass actor; github-iac GovernedRepositories.cs), and the queue merges a + # commit only after this workflow passed on that same SHA in its + # merge-group run. A push run re-tested the # SHA the queue had just passed: all 55 main pushes from 2026-10-03T22:40Z # to 10-04T17:41Z did, for 591 job minutes. The check runs that merge-group # run left stay on the commit. @@ -506,11 +507,12 @@ jobs: # # The job skips only on a contract-only event or a draft; otherwise only # steps are gated. A skipped required lane's result is not `success`, which - # the ci-status aggregate rejects, a draft's included, and a workflow + # the ci-status aggregate rejects outside a draft, and a workflow # skipped by a path filter leaves its required check Pending forever. A - # draft lints nothing and its ci-status is red: the `ready_for_review` run - # lints the same SHA before the pull request can merge, and a draft run that - # lints is a runner the 60-job cap pays for and nothing gates on. + # draft lints nothing and its ci-status passes: the `ready_for_review` run + # lints the same SHA, the merge queue re-runs every lane before a merge, and + # a draft run that lints is a runner the 60-job cap pays for and nothing + # gates on. # # PR-ONLY STEPS BRANCH IN THE SHELL, NOT IN `if:`. A step cannot carry both a # resolver gate and an event gate: the docs-only gate reads a step condition @@ -2837,36 +2839,17 @@ jobs: # linkage check and its pull requests stay mergeable. An exact-login # match, never a `*[bot]` pattern. exempt-authors: 'dependabot[bot]' - # A DRAFT IS RED. Every lane skips on a draft, so its ci-status has linted - # and tested nothing. Between the flip to ready and the `ready_for_review` - # run's own ci-status check run (minutes later), the draft's ci-status is - # the newest one on the SHA, so a green draft would let a merge through on - # nothing. The aggregate below fails on the skipped lanes and records - # `ci-lanes=failure`; this step says why. - # - # A re-run keeps the payload of the event that started the run, so a - # contract-only run from the draft era that a full run re-runs after - # ready_for_review still carries draft: true; the live state decides. - # A contract-only run tests nothing either way; the `ci-lanes` status - # decides it. An unreadable state fails closed. - # Pinned by scripts/ci-fail-a-draft.test.sh, which runs this body. - - name: Fail a draft + # A DRAFT PASSES. Every lane skips on a draft, so its ci-status has linted + # and tested nothing, and the aggregate below passes those skips. A green + # draft cannot admit a merge: a draft cannot merge, and the merge-queue + # ruleset has no bypass actor, so every merge waits for this workflow's + # merge-group run, which runs every lane. This step says why it is green. + - name: Note a draft if: github.event.pull_request.draft == true - env: - # Only a contract-only run reads its verdict from ci-lanes, so only it - # may pass on the live state. A re-run of a draft-era full run still - # skipped its lanes and is refused. - CONTRACT_ONLY: ${{ github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('["labeled","unlabeled"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base)) }} - GH_TOKEN: ${{ github.token }} - PR: ${{ github.event.pull_request.number }} run: | - if [ "$CONTRACT_ONLY" = true ] && - [ "$(gh api "repos/$GITHUB_REPOSITORY/pulls/$PR" --jq .draft)" = false ]; then - echo "Marked ready since this run's event; the ci-lanes status decides." - exit 0 - fi - echo "::error::draft: lanes not run. Mark the pull request ready to lint and test this SHA." - exit 1 + msg="draft: lanes not run; the ready_for_review run lints and tests this SHA" + echo "::notice::$msg" + echo "$msg" >>"$GITHUB_STEP_SUMMARY" # A QUEUED PULL REQUEST IS HELD BY ITS LABEL TOO. A merge-group payload # carries no pull request, so `check-contract` above reports `skipped`, # and a `do-not-merge` label applied after enqueue would merge (#5952). @@ -2945,8 +2928,9 @@ jobs: # for the lane list — through the ordered-skip check. Adding a lane # to needs automatically extends this check. results: ${{ steps.lanes.outputs.results }} - # A `skipped` lane is red, a draft's included (see `Fail a draft`). - treat-skipped-as: fail + # A `skipped` lane is red, except on a draft, where every lane skips + # (see `Note a draft`). + treat-skipped-as: ${{ github.event.pull_request.draft && 'pass' || 'fail' }} # `carry-forward-wait-seconds: '0'` means no carry-forward wait. Under # `yield-to-full-run` the composite may re-read for up to 30 s, only # when the in-flight full run has already written `success` and its diff --git a/docs/ci-runner-routing.md b/docs/ci-runner-routing.md index c06bc45d7b..7ebb1d0006 100644 --- a/docs/ci-runner-routing.md +++ b/docs/ci-runner-routing.md @@ -49,11 +49,11 @@ as `success` only when `select-tests` succeeded and its row for that lane (`run_bash`, `run_python`, `run_node`) is `false`; any other skip stays red. On a draft pull request every lane but `select-tests` carries a draft gate, so a draft run lints and tests nothing, and its -`ci-status` fails (`draft: lanes not run`) and records `ci-lanes=failure` -(`scripts/check-docs-only-gate.sh` pins both). A green draft would be the -newest `ci-status` on the SHA from the flip to ready until the -`ready_for_review` run's lanes finish, so a merge could land on nothing tested. -The `ready_for_review` run lints and tests the same SHA. `test-windows` is deliberately outside that +`ci-status` passes those skips with the notice `draft: lanes not run; the +ready_for_review run lints and tests this SHA` (`scripts/check-docs-only-gate.sh` +pins both forms). The `ready_for_review` run lints and tests the same SHA, and a +green draft cannot admit a merge: the merge-queue ruleset has no bypass actor, so +every merge waits for the merge-group run, which runs every lane. `test-windows` is deliberately outside that aggregate, as an informational platform lane; `pr-test-windows.yml` says so at the top of the file and warns against wiring it into any required check. It runs in its own workflow because nothing gates on it and, inside `pr-require-checks.yml`, it was the @@ -177,10 +177,7 @@ No run waits on another run: (`rerun-contract-only-siblings`, `actions: write`); a draft full run skips its lanes and re-runs nothing. The re-run keeps its event, so it is contract-only again, reads `success` and replaces the red check run within - seconds. That includes a run drawn while the pull request was a draft: its - payload still says draft, so `Fail a draft` reads the live draft state on a - contract-only run and passes once the pull request is ready - (`scripts/ci-fail-a-draft.test.sh`). + seconds. A red contract-only run also stays red when its contract fails (an invalid title or a `do-not-merge` label). That is the intended answer. diff --git a/docs/conventions/pr-pipeline/CHANGELOG.md b/docs/conventions/pr-pipeline/CHANGELOG.md index a63589e1b6..4a08cb232b 100644 --- a/docs/conventions/pr-pipeline/CHANGELOG.md +++ b/docs/conventions/pr-pipeline/CHANGELOG.md @@ -1,5 +1,10 @@ # Changelog for the PR pipeline convention +## 1.10.5 - 2026-10-12 + +README: `ci-status` on a draft passes its skipped lanes; the `ready_for_review` run tests the head +and the merge queue re-runs every lane before a merge. `version` stays 1. + ## 1.10.4 - 2026-10-12 README gains "Planning lane work": a one-page plan per sub-topic, design and stress tests only for diff --git a/docs/conventions/pr-pipeline/README.md b/docs/conventions/pr-pipeline/README.md index a2b408ec27..9cd1354d91 100644 --- a/docs/conventions/pr-pipeline/README.md +++ b/docs/conventions/pr-pipeline/README.md @@ -62,7 +62,9 @@ no renumbering. `maintenance` and `upstream` are scheduled producers, not steps in a PR's path. They file work into `intake`. -Pipeline lanes skip draft PRs; `ci-status` still reports on them. Converting a PR back to draft +Pipeline lanes skip draft PRs; `ci-status` still reports on them, and passes the skipped lanes. +The `ready_for_review` run tests the head, and the merge queue re-runs every lane before a merge. +Converting a PR back to draft cancels every running pipeline lane for it. ## Lanes diff --git a/scripts/check-docs-only-gate.sh b/scripts/check-docs-only-gate.sh index 86fbc9a791..bfa6126e86 100755 --- a/scripts/check-docs-only-gate.sh +++ b/scripts/check-docs-only-gate.sh @@ -90,10 +90,10 @@ # The other reads are the test lanes' matrix sizes, # each pinned whole in MATRIX_READS with its one-leg # default. -# 11. A SKIP NEVER PASSES — the aggregate's `treat-skipped-as` is `fail`, a -# draft's included: a draft runs no lane, and a green -# draft ci-status is the newest one on the SHA from -# the flip to ready until the lanes finish. +# 11. A SKIP NEVER PASSES — the aggregate's `treat-skipped-as` is `fail`, or +# exactly the draft form that passes skips only on a +# draft, where every lane skips; the merge queue +# re-runs every lane before a merge. # 12. A SKIP IS CHECKED — a test lane `test-`, or `lint-shell` on # `run_shell`, may skip as a job on its own # `run_` row (the ordered-skip form, the draft gate @@ -1057,23 +1057,24 @@ fi # --- 11. A SKIPPED LANE NEVER PASSES ----------------------------------------- # -# The draft gate skips every lane on a draft, and the aggregate fails it. A -# draft's ci-status is the newest one on its SHA from the flip to ready until -# the `ready_for_review` run's lanes finish, so a draft that passed would let a -# merge through with nothing linted or tested. `treat-skipped-as` is therefore -# the literal `fail`: anything else turns a lane skipped for some reason into a -# green required check with nothing run. +# The draft gate skips every lane on a draft, and the aggregate passes those +# skips only there: a draft cannot merge, and the merge queue runs every lane +# on the merge-group commit before a merge. `treat-skipped-as` is therefore +# the literal `fail` or exactly $SKIP_AS_DRAFT; anything else turns a lane +# skipped for some reason on a ready pull request into a green required check +# with nothing run. +SKIP_AS_DRAFT="\${{ github.event.pull_request.draft && 'pass' || 'fail' }}" skipas_seen=0 while IFS="$TAB" read -r sjob svalue; do [[ -n "$sjob" ]] || continue [[ "$sjob" == "$AGGREGATE_JOB" ]] || continue skipas_seen=1 - [[ "$svalue" == "fail" ]] && continue - report "A SKIPPED LANE NEVER PASSES: job '$AGGREGATE_JOB' sets treat-skipped-as: $svalue. Use 'fail', a draft's included: a draft runs no lane." + [[ "$svalue" == "fail" || "$svalue" == "$SKIP_AS_DRAFT" ]] && continue + report "A SKIPPED LANE NEVER PASSES: job '$AGGREGATE_JOB' sets treat-skipped-as: $svalue. Use 'fail' or exactly $SKIP_AS_DRAFT." done <<<"$REC_SKIPAS" if [[ "$skipas_seen" -eq 0 ]] && has_line "$jobs_all" "$AGGREGATE_JOB"; then if [[ -n "$(printf '%s' "$REC_JOBIF" | awk -F'\t' -v j="$JOB_GATE_DRAFT" '$2 == j')" ]]; then - report "A SKIPPED LANE NEVER PASSES: a lane carries the draft gate but job '$AGGREGATE_JOB' sets no treat-skipped-as, whose default passes a skip. Set it to 'fail'." + report "A SKIPPED LANE NEVER PASSES: a lane carries the draft gate but job '$AGGREGATE_JOB' sets no treat-skipped-as, whose default passes a skip. Set it to 'fail' or exactly $SKIP_AS_DRAFT." fi fi diff --git a/scripts/check-docs-only-gate.test.sh b/scripts/check-docs-only-gate.test.sh index 4d041bdb37..f563e8909a 100755 --- a/scripts/check-docs-only-gate.test.sh +++ b/scripts/check-docs-only-gate.test.sh @@ -476,11 +476,15 @@ expect "the draft gate with its polarity flipped is rejected" 1 "NO JOB-LEVEL CO # --- 11. A SKIPPED LANE NEVER PASSES ------------------------------------------ expect "the aggregate failing every skip is allowed" 0 "scope resolved once" --check "$skip_as_fail" -# A draft that passed would be the newest ci-status on its SHA from the flip to -# ready until the lanes finish, green over nothing linted or tested. +# A draft cannot merge and the merge queue runs every lane, so the exact draft +# form may pass a draft's skips; any other expression is rejected. f="$scratch/skip-as-draft.yml" xform_insert_after "$base" " - name: Aggregate lane results" " with:\n treat-skipped-as: \${{ github.event.pull_request.draft && 'pass' || 'fail' }}" "$f" -expect "the aggregate passing skips on a draft is rejected" 1 "A SKIPPED LANE NEVER PASSES" --check "$f" +expect "the aggregate passing skips only on a draft is allowed" 0 "scope resolved once" --check "$f" + +f="$scratch/skip-as-draft-flipped.yml" +xform_insert_after "$base" " - name: Aggregate lane results" " with:\n treat-skipped-as: \${{ github.event.pull_request.draft && 'fail' || 'pass' }}" "$f" +expect "the draft form with its values flipped is rejected" 1 "A SKIPPED LANE NEVER PASSES" --check "$f" f="$scratch/skip-as-pass.yml" xform_insert_after "$base" " - name: Aggregate lane results" " with:\n treat-skipped-as: pass" "$f" diff --git a/scripts/ci-fail-a-draft.test.sh b/scripts/ci-fail-a-draft.test.sh deleted file mode 100755 index c47c78ff0b..0000000000 --- a/scripts/ci-fail-a-draft.test.sh +++ /dev/null @@ -1,80 +0,0 @@ -#!/usr/bin/env bash -# Runs the `Fail a draft` step body of .github/workflows/pr-require-checks.yml's ci-status -# under the shell Actions gives a step with no `shell:` (bash -e), against a -# stub `gh`. -# -# The case that matters is the re-run: a contract-only run drawn while the pull -# request was a draft keeps that payload when the full run re-runs it after the -# flip to ready, and must then pass. Every other combination stays red. -# test-scope: .github/workflows/pr-require-checks.yml -set -uo pipefail - -ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" -WORKFLOW="$ROOT/.github/workflows/pr-require-checks.yml" - -# shellcheck source=lib/test-harness.sh -. "$ROOT/scripts/lib/test-harness.sh" - -TMP_ROOT="$(mktemp -d)" -trap 'rm -rf "$TMP_ROOT"' EXIT - -# The step, from its `- name:` line to the next step. -step="$(awk '/^ - name: Fail a draft$/ { on = 1; print; next } - on && /^ - / { exit } - on { print }' "$WORKFLOW")" -# Its `run: |` body, de-indented. -awk '/^ run: \|$/ { on = 1; next } - on && /^ / { print substr($0, 11); next } - on && /^[[:blank:]]*$/ { print ""; next } - on { exit }' <<<"$step" >"$TMP_ROOT/body.sh" - -if [[ ! -s "$TMP_ROOT/body.sh" ]]; then - fail "no 'Fail a draft' step with a run: | body in $WORKFLOW" - test_harness::report - exit 1 -fi -for line in "if: github.event.pull_request.draft == true" \ - "CONTRACT_ONLY: \${{ github.event.pull_request.head.repo.full_name == github.repository && (contains(fromJSON('[\"labeled\",\"unlabeled\"]'), github.event.action) || (github.event.action == 'edited' && !github.event.changes.base)) }}" \ - "PR: \${{ github.event.pull_request.number }}"; do - if [[ "$step" == *"$line"* ]]; then ok "step carries '$line'"; else fail "step lost '$line'"; fi -done - -# The stub logs its arguments and answers with STUB_DRAFT, or fails when -# STUB_RC is non-zero. -mkdir -p "$TMP_ROOT/bin" -cat >"$TMP_ROOT/bin/gh" <<'EOF' -#!/usr/bin/env bash -printf '%s\n' "$*" >>"$STUB_LOG" -[[ "${STUB_RC:-0}" -eq 0 ]] || exit "$STUB_RC" -printf '%s\n' "$STUB_DRAFT" -EOF -chmod +x "$TMP_ROOT/bin/gh" - -# expect