Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
56 changes: 20 additions & 36 deletions .github/workflows/pr-require-checks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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).
Expand Down Expand Up @@ -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
Expand Down
15 changes: 6 additions & 9 deletions docs/ci-runner-routing.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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.
Expand Down
5 changes: 5 additions & 0 deletions docs/conventions/pr-pipeline/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -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
Expand Down
4 changes: 3 additions & 1 deletion docs/conventions/pr-pipeline/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
27 changes: 14 additions & 13 deletions scripts/check-docs-only-gate.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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-<x>`, or `lint-shell` on
# `run_shell`, may skip as a job on its own
# `run_<x>` row (the ordered-skip form, the draft gate
Expand Down Expand Up @@ -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

Expand Down
10 changes: 7 additions & 3 deletions scripts/check-docs-only-gate.test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
80 changes: 0 additions & 80 deletions scripts/ci-fail-a-draft.test.sh

This file was deleted.

1 change: 0 additions & 1 deletion scripts/suite-seconds.txt
Original file line number Diff line number Diff line change
Expand Up @@ -694,7 +694,6 @@ scripts/check-standards-contract-bump.test.sh 2
scripts/check-summary-reader-parity.test.sh 3
scripts/check-test-tmp-cleanup.test.sh 1
scripts/check-vendor-version-bump.test.sh 2
scripts/ci-fail-a-draft.test.sh 1
scripts/dependabot-plugin-bump.test.sh 1
scripts/emit-windows-path.test.sh 1
scripts/gen-hook-event-registry.test.sh 1
Expand Down
Loading