From 3588c1f4da87edb909666d3531f5c5eca22ca26c Mon Sep 17 00:00:00 2001
From: Adam Perry <anp@openai.com>
Date: Fri, 26 Jun 2026 00:33:23 +0000
Subject: [PATCH 1/9] add single required CI gate

---
 .github/workflows/bazel.yml            |  2 +-
 .github/workflows/blob-size-policy.yml |  2 +-
 .github/workflows/cargo-deny.yml       |  2 +-
 .github/workflows/ci.yml               |  2 +-
 .github/workflows/codespell.yml        |  3 +-
 .github/workflows/pr-ci.yml            | 76 ++++++++++++++++++++++++++
 .github/workflows/rust-ci.yml          |  2 +-
 .github/workflows/sdk.yml              |  2 +-
 8 files changed, 83 insertions(+), 8 deletions(-)
 create mode 100644 .github/workflows/pr-ci.yml

diff --git a/.github/workflows/bazel.yml b/.github/workflows/bazel.yml
index 7a6db869e012..f3e0841dbb5e 100644
--- a/.github/workflows/bazel.yml
+++ b/.github/workflows/bazel.yml
@@ -4,7 +4,7 @@ name: Bazel
 # https://github.com/cerisier/toolchains_llvm_bootstrapped/blob/main/.github/workflows/ci.yaml
 
 on:
-  pull_request: {}
+  workflow_call:
   push:
     branches:
       - main
diff --git a/.github/workflows/blob-size-policy.yml b/.github/workflows/blob-size-policy.yml
index ea50e8ee2e55..6fe516f121d3 100644
--- a/.github/workflows/blob-size-policy.yml
+++ b/.github/workflows/blob-size-policy.yml
@@ -1,7 +1,7 @@
 name: blob-size-policy
 
 on:
-  pull_request: {}
+  workflow_call:
 
 jobs:
   check:
diff --git a/.github/workflows/cargo-deny.yml b/.github/workflows/cargo-deny.yml
index aa37d7cec29b..5aba258319bd 100644
--- a/.github/workflows/cargo-deny.yml
+++ b/.github/workflows/cargo-deny.yml
@@ -1,7 +1,7 @@
 name: cargo-deny
 
 on:
-  pull_request:
+  workflow_call:
   push:
     branches:
       - main
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index a78ae034edf3..f83254e1cfdc 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -1,7 +1,7 @@
 name: ci
 
 on:
-  pull_request: {}
+  workflow_call:
   push: { branches: [main] }
 
 jobs:
diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml
index b642a6dbe189..825dd7f7f08e 100644
--- a/.github/workflows/codespell.yml
+++ b/.github/workflows/codespell.yml
@@ -3,10 +3,9 @@
 name: Codespell
 
 on:
+  workflow_call:
   push:
     branches: [main]
-  pull_request:
-    branches: [main]
 
 permissions:
   contents: read
diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/pr-ci.yml
new file mode 100644
index 000000000000..a5662cf89429
--- /dev/null
+++ b/.github/workflows/pr-ci.yml
@@ -0,0 +1,76 @@
+name: pr-ci
+
+on:
+  pull_request: {}
+
+jobs:
+  bazel:
+    name: Bazel
+    uses: ./.github/workflows/bazel.yml
+    secrets: inherit
+
+  blob-size-policy:
+    name: Blob size policy
+    uses: ./.github/workflows/blob-size-policy.yml
+    secrets: inherit
+
+  cargo-deny:
+    name: cargo-deny
+    uses: ./.github/workflows/cargo-deny.yml
+    secrets: inherit
+
+  ci:
+    name: ci
+    uses: ./.github/workflows/ci.yml
+    secrets: inherit
+
+  codespell:
+    name: Codespell
+    uses: ./.github/workflows/codespell.yml
+    secrets: inherit
+
+  rust-ci:
+    name: rust-ci
+    uses: ./.github/workflows/rust-ci.yml
+    secrets: inherit
+
+  sdk:
+    name: sdk
+    uses: ./.github/workflows/sdk.yml
+    secrets: inherit
+
+  required:
+    name: CI required
+    if: ${{ always() }}
+    needs:
+      - bazel
+      - blob-size-policy
+      - cargo-deny
+      - ci
+      - codespell
+      - rust-ci
+      - sdk
+    runs-on: ubuntu-24.04
+    steps:
+      - name: Require successful dependencies
+        env:
+          NEEDS: ${{ toJSON(needs) }}
+        shell: bash
+        run: |
+          set -euo pipefail
+
+          failures="$(
+            jq --raw-output '
+              to_entries[]
+              | select(.value.result != "success")
+              | "\(.key): \(.value.result)"
+            ' <<<"$NEEDS"
+          )"
+
+          if [[ -n "$failures" ]]; then
+            echo 'Required CI dependencies did not succeed:' >&2
+            echo "$failures" >&2
+            exit 1
+          fi
+
+          echo 'All required CI dependencies succeeded.'
diff --git a/.github/workflows/rust-ci.yml b/.github/workflows/rust-ci.yml
index f693994d1a77..cf1afdfe2de1 100644
--- a/.github/workflows/rust-ci.yml
+++ b/.github/workflows/rust-ci.yml
@@ -1,6 +1,6 @@
 name: rust-ci
 on:
-  pull_request: {}
+  workflow_call:
   workflow_dispatch:
 
 # Cargo's libgit2 transport has been flaky when fetching git dependencies with
diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml
index dcb94067989f..2850a60070c0 100644
--- a/.github/workflows/sdk.yml
+++ b/.github/workflows/sdk.yml
@@ -1,9 +1,9 @@
 name: sdk
 
 on:
+  workflow_call:
   push:
     branches: [main]
-  pull_request: {}
 
 jobs:
   python-sdk:

From 82f5dc4764adfd48d60302f29bcfe44efa83758c Mon Sep 17 00:00:00 2001
From: Adam Perry <anp@openai.com>
Date: Fri, 26 Jun 2026 03:17:32 +0000
Subject: [PATCH 2/9] rename PR workflow to blocking CI

---
 .github/workflows/{pr-ci.yml => blocking-ci.yml} | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)
 rename .github/workflows/{pr-ci.yml => blocking-ci.yml} (98%)

diff --git a/.github/workflows/pr-ci.yml b/.github/workflows/blocking-ci.yml
similarity index 98%
rename from .github/workflows/pr-ci.yml
rename to .github/workflows/blocking-ci.yml
index a5662cf89429..9871120e89ad 100644
--- a/.github/workflows/pr-ci.yml
+++ b/.github/workflows/blocking-ci.yml
@@ -1,4 +1,4 @@
-name: pr-ci
+name: blocking-ci
 
 on:
   pull_request: {}

From bfd5f922a968632b56f8fec68b7753db7da71aa6 Mon Sep 17 00:00:00 2001
From: Adam Perry <anp@openai.com>
Date: Fri, 26 Jun 2026 03:36:46 +0000
Subject: [PATCH 3/9] rename CI workflow to repo checks

---
 .github/workflows/blocking-ci.yml             | 8 ++++----
 .github/workflows/{ci.yml => repo-checks.yml} | 2 +-
 2 files changed, 5 insertions(+), 5 deletions(-)
 rename .github/workflows/{ci.yml => repo-checks.yml} (99%)

diff --git a/.github/workflows/blocking-ci.yml b/.github/workflows/blocking-ci.yml
index 9871120e89ad..d26f29ef2550 100644
--- a/.github/workflows/blocking-ci.yml
+++ b/.github/workflows/blocking-ci.yml
@@ -19,9 +19,9 @@ jobs:
     uses: ./.github/workflows/cargo-deny.yml
     secrets: inherit
 
-  ci:
-    name: ci
-    uses: ./.github/workflows/ci.yml
+  repo-checks:
+    name: repo-checks
+    uses: ./.github/workflows/repo-checks.yml
     secrets: inherit
 
   codespell:
@@ -46,7 +46,7 @@ jobs:
       - bazel
       - blob-size-policy
       - cargo-deny
-      - ci
+      - repo-checks
       - codespell
       - rust-ci
       - sdk
diff --git a/.github/workflows/ci.yml b/.github/workflows/repo-checks.yml
similarity index 99%
rename from .github/workflows/ci.yml
rename to .github/workflows/repo-checks.yml
index f83254e1cfdc..12a512652e84 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/repo-checks.yml
@@ -1,4 +1,4 @@
-name: ci
+name: repo-checks
 
 on:
   workflow_call:

From b726f79b7b48e83867136870cfc49db17ba7a656 Mon Sep 17 00:00:00 2001
From: Adam Perry <anp@openai.com>
Date: Fri, 26 Jun 2026 03:42:03 +0000
Subject: [PATCH 4/9] group main checks under blocking CI

---
 .github/workflows/bazel.yml            |  3 ---
 .github/workflows/blob-size-policy.yml | 15 ++++++++++++---
 .github/workflows/blocking-ci.yml      | 14 ++++++++------
 .github/workflows/cargo-deny.yml       |  3 ---
 .github/workflows/codespell.yml        |  2 --
 .github/workflows/repo-checks.yml      |  1 -
 .github/workflows/sdk.yml              |  2 --
 7 files changed, 20 insertions(+), 20 deletions(-)

diff --git a/.github/workflows/bazel.yml b/.github/workflows/bazel.yml
index f3e0841dbb5e..10dd8670de0a 100644
--- a/.github/workflows/bazel.yml
+++ b/.github/workflows/bazel.yml
@@ -5,9 +5,6 @@ name: Bazel
 
 on:
   workflow_call:
-  push:
-    branches:
-      - main
   workflow_dispatch:
 
 concurrency:
diff --git a/.github/workflows/blob-size-policy.yml b/.github/workflows/blob-size-policy.yml
index 6fe516f121d3..a5f937420e40 100644
--- a/.github/workflows/blob-size-policy.yml
+++ b/.github/workflows/blob-size-policy.yml
@@ -14,13 +14,22 @@ jobs:
           fetch-depth: 0
           persist-credentials: false
 
-      - name: Determine PR comparison range
+      - name: Determine comparison range
         id: range
         shell: bash
         run: |
           set -euo pipefail
-          echo "base=${{ github.event.pull_request.base.sha }}" >> "$GITHUB_OUTPUT"
-          echo "head=${{ github.event.pull_request.head.sha }}" >> "$GITHUB_OUTPUT"
+
+          if [[ "${{ github.event_name }}" == "pull_request" ]]; then
+            base='${{ github.event.pull_request.base.sha }}'
+            head='${{ github.event.pull_request.head.sha }}'
+          else
+            base='${{ github.event.before }}'
+            head='${{ github.sha }}'
+          fi
+
+          echo "base=$base" >> "$GITHUB_OUTPUT"
+          echo "head=$head" >> "$GITHUB_OUTPUT"
 
       - name: Check changed blob sizes
         env:
diff --git a/.github/workflows/blocking-ci.yml b/.github/workflows/blocking-ci.yml
index d26f29ef2550..e0ce7d8a79ad 100644
--- a/.github/workflows/blocking-ci.yml
+++ b/.github/workflows/blocking-ci.yml
@@ -2,6 +2,8 @@ name: blocking-ci
 
 on:
   pull_request: {}
+  push:
+    branches: [main]
 
 jobs:
   bazel:
@@ -19,16 +21,16 @@ jobs:
     uses: ./.github/workflows/cargo-deny.yml
     secrets: inherit
 
-  repo-checks:
-    name: repo-checks
-    uses: ./.github/workflows/repo-checks.yml
-    secrets: inherit
-
   codespell:
     name: Codespell
     uses: ./.github/workflows/codespell.yml
     secrets: inherit
 
+  repo-checks:
+    name: repo-checks
+    uses: ./.github/workflows/repo-checks.yml
+    secrets: inherit
+
   rust-ci:
     name: rust-ci
     uses: ./.github/workflows/rust-ci.yml
@@ -46,8 +48,8 @@ jobs:
       - bazel
       - blob-size-policy
       - cargo-deny
-      - repo-checks
       - codespell
+      - repo-checks
       - rust-ci
       - sdk
     runs-on: ubuntu-24.04
diff --git a/.github/workflows/cargo-deny.yml b/.github/workflows/cargo-deny.yml
index 5aba258319bd..be00e1147d12 100644
--- a/.github/workflows/cargo-deny.yml
+++ b/.github/workflows/cargo-deny.yml
@@ -2,9 +2,6 @@ name: cargo-deny
 
 on:
   workflow_call:
-  push:
-    branches:
-      - main
 
 # Cargo's libgit2 transport has been flaky when fetching git dependencies with
 # nested submodules. Prefer the system git CLI across every Cargo invocation.
diff --git a/.github/workflows/codespell.yml b/.github/workflows/codespell.yml
index 825dd7f7f08e..a1751c8d2941 100644
--- a/.github/workflows/codespell.yml
+++ b/.github/workflows/codespell.yml
@@ -4,8 +4,6 @@ name: Codespell
 
 on:
   workflow_call:
-  push:
-    branches: [main]
 
 permissions:
   contents: read
diff --git a/.github/workflows/repo-checks.yml b/.github/workflows/repo-checks.yml
index 12a512652e84..3a76a202eae8 100644
--- a/.github/workflows/repo-checks.yml
+++ b/.github/workflows/repo-checks.yml
@@ -2,7 +2,6 @@ name: repo-checks
 
 on:
   workflow_call:
-  push: { branches: [main] }
 
 jobs:
   build-test:
diff --git a/.github/workflows/sdk.yml b/.github/workflows/sdk.yml
index 2850a60070c0..41f887471220 100644
--- a/.github/workflows/sdk.yml
+++ b/.github/workflows/sdk.yml
@@ -2,8 +2,6 @@ name: sdk
 
 on:
   workflow_call:
-  push:
-    branches: [main]
 
 jobs:
   python-sdk:

From 2efdd80cf0868b49fd581cf93e01a9f5edc7c8c5 Mon Sep 17 00:00:00 2001
From: Adam Perry <anp@openai.com>
Date: Fri, 26 Jun 2026 03:50:05 +0000
Subject: [PATCH 5/9] group postmerge checks under parent workflow

---
 .github/workflows/postmerge-ci.yml | 97 ++++++++++++++++++++++++++++++
 .github/workflows/rust-ci-full.yml |  2 +-
 .github/workflows/v8-canary.yml    | 22 +------
 3 files changed, 100 insertions(+), 21 deletions(-)
 create mode 100644 .github/workflows/postmerge-ci.yml

diff --git a/.github/workflows/postmerge-ci.yml b/.github/workflows/postmerge-ci.yml
new file mode 100644
index 000000000000..f75ba0ee852c
--- /dev/null
+++ b/.github/workflows/postmerge-ci.yml
@@ -0,0 +1,97 @@
+name: postmerge-ci
+
+on:
+  push:
+    branches: [main]
+
+jobs:
+  changes:
+    name: Detect postmerge changes
+    runs-on: ubuntu-24.04
+    outputs:
+      v8_canary: ${{ steps.detect.outputs.v8_canary }}
+    steps:
+      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+        with:
+          ref: ${{ github.sha }}
+          fetch-depth: 0
+          persist-credentials: false
+
+      - name: Detect V8 canary changes
+        id: detect
+        env:
+          BASE_SHA: ${{ github.event.before }}
+          HEAD_SHA: ${{ github.sha }}
+        shell: bash
+        run: |
+          set -euo pipefail
+
+          v8_canary=false
+          while IFS= read -r file; do
+            case "$file" in
+              .bazelrc \
+                | .github/actions/setup-bazel-ci/* \
+                | .github/scripts/run_bazel_with_buildbuddy.py \
+                | .github/scripts/rusty_v8_bazel.py \
+                | .github/scripts/rusty_v8_module_bazel.py \
+                | .github/scripts/v8_canary_changes.py \
+                | .github/workflows/postmerge-ci.yml \
+                | .github/workflows/rusty-v8-release.yml \
+                | .github/workflows/v8-canary.yml \
+                | MODULE.bazel \
+                | MODULE.bazel.lock \
+                | codex-rs/Cargo.toml \
+                | patches/BUILD.bazel \
+                | patches/llvm_*.patch \
+                | patches/rules_cc_*.patch \
+                | patches/v8_*.patch \
+                | third_party/v8/*)
+                v8_canary=true
+                ;;
+            esac
+          done < <(git diff --name-only --no-renames "$BASE_SHA" "$HEAD_SHA")
+
+          echo "v8_canary=$v8_canary" >> "$GITHUB_OUTPUT"
+
+  rust-ci-full:
+    name: rust-ci-full
+    uses: ./.github/workflows/rust-ci-full.yml
+    secrets: inherit
+
+  v8-canary:
+    name: v8-canary
+    needs: changes
+    if: needs.changes.outputs.v8_canary == 'true'
+    uses: ./.github/workflows/v8-canary.yml
+    secrets: inherit
+
+  results:
+    name: Postmerge CI results
+    needs:
+      - changes
+      - rust-ci-full
+      - v8-canary
+    if: ${{ always() }}
+    runs-on: ubuntu-24.04
+    steps:
+      - name: Require successful dependencies
+        env:
+          V8_CANARY_REQUIRED: ${{ needs.changes.outputs.v8_canary }}
+        shell: bash
+        run: |
+          set -euo pipefail
+
+          echo "changes     : ${{ needs.changes.result }}"
+          echo "rust-ci-full: ${{ needs.rust-ci-full.result }}"
+          echo "v8-canary  : ${{ needs.v8-canary.result }}"
+
+          [[ '${{ needs.changes.result }}' == 'success' ]] || { echo 'change detection failed'; exit 1; }
+          [[ '${{ needs.rust-ci-full.result }}' == 'success' ]] || { echo 'rust-ci-full failed'; exit 1; }
+
+          if [[ "$V8_CANARY_REQUIRED" == 'true' ]]; then
+            [[ '${{ needs.v8-canary.result }}' == 'success' ]] || { echo 'v8-canary failed'; exit 1; }
+          else
+            [[ '${{ needs.v8-canary.result }}' == 'skipped' ]] || { echo 'v8-canary ran unexpectedly'; exit 1; }
+          fi
+
+          echo 'All required postmerge CI dependencies succeeded.'
diff --git a/.github/workflows/rust-ci-full.yml b/.github/workflows/rust-ci-full.yml
index 7ad1d4b3adcf..b60c0b7ca24e 100644
--- a/.github/workflows/rust-ci-full.yml
+++ b/.github/workflows/rust-ci-full.yml
@@ -1,8 +1,8 @@
 name: rust-ci-full
 on:
+  workflow_call:
   push:
     branches:
-      - main
       - "**full-ci**"
   workflow_dispatch:
 
diff --git a/.github/workflows/v8-canary.yml b/.github/workflows/v8-canary.yml
index efa06aaad743..6f10aba64c76 100644
--- a/.github/workflows/v8-canary.yml
+++ b/.github/workflows/v8-canary.yml
@@ -1,6 +1,7 @@
 name: v8-canary
 
 on:
+  workflow_call:
   pull_request:
     paths:
       - ".bazelrc"
@@ -9,26 +10,7 @@ on:
       - ".github/scripts/rusty_v8_bazel.py"
       - ".github/scripts/rusty_v8_module_bazel.py"
       - ".github/scripts/v8_canary_changes.py"
-      - ".github/workflows/rusty-v8-release.yml"
-      - ".github/workflows/v8-canary.yml"
-      - "MODULE.bazel"
-      - "MODULE.bazel.lock"
-      - "codex-rs/Cargo.toml"
-      - "patches/BUILD.bazel"
-      - "patches/llvm_*.patch"
-      - "patches/rules_cc_*.patch"
-      - "patches/v8_*.patch"
-      - "third_party/v8/**"
-  push:
-    branches:
-      - main
-    paths:
-      - ".bazelrc"
-      - ".github/actions/setup-bazel-ci/**"
-      - ".github/scripts/run_bazel_with_buildbuddy.py"
-      - ".github/scripts/rusty_v8_bazel.py"
-      - ".github/scripts/rusty_v8_module_bazel.py"
-      - ".github/scripts/v8_canary_changes.py"
+      - ".github/workflows/postmerge-ci.yml"
       - ".github/workflows/rusty-v8-release.yml"
       - ".github/workflows/v8-canary.yml"
       - "MODULE.bazel"

From cef1d748f784595b6bb86c9b4e45645149bf1e88 Mon Sep 17 00:00:00 2001
From: Adam Perry <anp@openai.com>
Date: Fri, 26 Jun 2026 04:01:43 +0000
Subject: [PATCH 6/9] move CI orchestration logic into scripts

---
 .github/scripts/check_ci_results.py  | 25 +++++++++
 .github/scripts/v8_canary_changes.py | 77 ++++++++++++++++++++++++----
 .github/workflows/blocking-ci.yml    | 25 +++------
 .github/workflows/postmerge-ci.yml   | 77 +++-------------------------
 .github/workflows/v8-canary.yml      | 24 ++-------
 5 files changed, 110 insertions(+), 118 deletions(-)
 create mode 100644 .github/scripts/check_ci_results.py

diff --git a/.github/scripts/check_ci_results.py b/.github/scripts/check_ci_results.py
new file mode 100644
index 000000000000..483649227194
--- /dev/null
+++ b/.github/scripts/check_ci_results.py
@@ -0,0 +1,25 @@
+#!/usr/bin/env python3
+
+import json
+import os
+
+
+def main() -> None:
+    needs = json.loads(os.environ["NEEDS"])
+    failures = sorted(
+        (name, dependency["result"])
+        for name, dependency in needs.items()
+        if dependency["result"] != "success"
+    )
+
+    if failures:
+        print("CI dependencies did not succeed:")
+        for name, result in failures:
+            print(f"{name}: {result}")
+        raise SystemExit(1)
+
+    print("All CI dependencies succeeded.")
+
+
+if __name__ == "__main__":
+    main()
diff --git a/.github/scripts/v8_canary_changes.py b/.github/scripts/v8_canary_changes.py
index 6d9693c60a9f..c8c539163238 100644
--- a/.github/scripts/v8_canary_changes.py
+++ b/.github/scripts/v8_canary_changes.py
@@ -3,10 +3,30 @@
 import argparse
 import subprocess
 import tomllib
+from fnmatch import fnmatchcase
 from pathlib import Path
 
 
 ROOT = Path(__file__).resolve().parents[2]
+CANARY_PATH_PATTERNS = {
+    ".bazelrc",
+    ".github/actions/setup-bazel-ci/**",
+    ".github/scripts/run_bazel_with_buildbuddy.py",
+    ".github/scripts/rusty_v8_bazel.py",
+    ".github/scripts/rusty_v8_module_bazel.py",
+    ".github/scripts/v8_canary_changes.py",
+    ".github/workflows/postmerge-ci.yml",
+    ".github/workflows/rusty-v8-release.yml",
+    ".github/workflows/v8-canary.yml",
+    "MODULE.bazel",
+    "MODULE.bazel.lock",
+    "codex-rs/Cargo.toml",
+    "patches/BUILD.bazel",
+    "patches/llvm_*.patch",
+    "patches/rules_cc_*.patch",
+    "patches/v8_*.patch",
+    "third_party/v8/**",
+}
 WINDOWS_SOURCE_BUILD_PATHS = {
     ".github/scripts/rusty_v8_bazel.py",
     ".github/scripts/rusty_v8_module_bazel.py",
@@ -16,6 +36,28 @@
 }
 
 
+def matching_canary_paths(changed_files: set[str]) -> set[str]:
+    return {
+        path
+        for path in changed_files
+        if any(fnmatchcase(path, pattern) for pattern in CANARY_PATH_PATTERNS)
+    }
+
+
+def canary_required(
+    changed_files: set[str],
+    base_v8_version: str,
+    head_v8_version: str,
+    *,
+    force: bool = False,
+) -> bool:
+    return (
+        force
+        or base_v8_version != head_v8_version
+        or bool(matching_canary_paths(changed_files))
+    )
+
+
 def resolved_v8_version(cargo_lock: bytes) -> str:
     versions = sorted(
         {
@@ -79,25 +121,42 @@ def parse_args() -> argparse.Namespace:
 def main() -> None:
     args = parse_args()
     if args.force:
-        required = True
-        reason = "manual workflow dispatch"
+        canary = True
+        canary_reason = "manual workflow dispatch"
+        windows_source = True
+        windows_source_reason = "manual workflow dispatch"
     elif not args.base or not args.head:
         raise SystemExit("--base and --head are required unless --force is set")
     else:
         files = changed_files(args.base, args.head)
         base_version = v8_version_at_revision(merge_base(args.base, args.head))
         head_version = v8_version_at_revision(args.head)
-        required = windows_source_required(files, base_version, head_version)
+
+        matched_canary_paths = sorted(matching_canary_paths(files))
+        canary = canary_required(files, base_version, head_version)
+        windows_source = windows_source_required(files, base_version, head_version)
         if base_version != head_version:
-            reason = f"v8 version changed from {base_version} to {head_version}"
+            canary_reason = (
+                f"v8 version changed from {base_version} to {head_version}"
+            )
+            windows_source_reason = canary_reason
         else:
-            matched_paths = sorted(files & WINDOWS_SOURCE_BUILD_PATHS)
-            reason = (
-                ", ".join(matched_paths) if matched_paths else "no relevant changes"
+            canary_reason = (
+                ", ".join(matched_canary_paths)
+                if matched_canary_paths
+                else "no relevant changes"
+            )
+            matched_windows_paths = sorted(files & WINDOWS_SOURCE_BUILD_PATHS)
+            windows_source_reason = (
+                ", ".join(matched_windows_paths)
+                if matched_windows_paths
+                else "no relevant changes"
             )
 
-    print(f"windows_source_required={str(required).lower()}")
-    print(f"windows_source_reason={reason}")
+    print(f"canary_required={str(canary).lower()}")
+    print(f"canary_reason={canary_reason}")
+    print(f"windows_source_required={str(windows_source).lower()}")
+    print(f"windows_source_reason={windows_source_reason}")
 
 
 if __name__ == "__main__":
diff --git a/.github/workflows/blocking-ci.yml b/.github/workflows/blocking-ci.yml
index e0ce7d8a79ad..52731efeb678 100644
--- a/.github/workflows/blocking-ci.yml
+++ b/.github/workflows/blocking-ci.yml
@@ -54,25 +54,12 @@ jobs:
       - sdk
     runs-on: ubuntu-24.04
     steps:
+      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+        with:
+          ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
+          persist-credentials: false
+
       - name: Require successful dependencies
         env:
           NEEDS: ${{ toJSON(needs) }}
-        shell: bash
-        run: |
-          set -euo pipefail
-
-          failures="$(
-            jq --raw-output '
-              to_entries[]
-              | select(.value.result != "success")
-              | "\(.key): \(.value.result)"
-            ' <<<"$NEEDS"
-          )"
-
-          if [[ -n "$failures" ]]; then
-            echo 'Required CI dependencies did not succeed:' >&2
-            echo "$failures" >&2
-            exit 1
-          fi
-
-          echo 'All required CI dependencies succeeded.'
+        run: python3 .github/scripts/check_ci_results.py
diff --git a/.github/workflows/postmerge-ci.yml b/.github/workflows/postmerge-ci.yml
index f75ba0ee852c..f5f6bd153b9c 100644
--- a/.github/workflows/postmerge-ci.yml
+++ b/.github/workflows/postmerge-ci.yml
@@ -5,54 +5,6 @@ on:
     branches: [main]
 
 jobs:
-  changes:
-    name: Detect postmerge changes
-    runs-on: ubuntu-24.04
-    outputs:
-      v8_canary: ${{ steps.detect.outputs.v8_canary }}
-    steps:
-      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
-        with:
-          ref: ${{ github.sha }}
-          fetch-depth: 0
-          persist-credentials: false
-
-      - name: Detect V8 canary changes
-        id: detect
-        env:
-          BASE_SHA: ${{ github.event.before }}
-          HEAD_SHA: ${{ github.sha }}
-        shell: bash
-        run: |
-          set -euo pipefail
-
-          v8_canary=false
-          while IFS= read -r file; do
-            case "$file" in
-              .bazelrc \
-                | .github/actions/setup-bazel-ci/* \
-                | .github/scripts/run_bazel_with_buildbuddy.py \
-                | .github/scripts/rusty_v8_bazel.py \
-                | .github/scripts/rusty_v8_module_bazel.py \
-                | .github/scripts/v8_canary_changes.py \
-                | .github/workflows/postmerge-ci.yml \
-                | .github/workflows/rusty-v8-release.yml \
-                | .github/workflows/v8-canary.yml \
-                | MODULE.bazel \
-                | MODULE.bazel.lock \
-                | codex-rs/Cargo.toml \
-                | patches/BUILD.bazel \
-                | patches/llvm_*.patch \
-                | patches/rules_cc_*.patch \
-                | patches/v8_*.patch \
-                | third_party/v8/*)
-                v8_canary=true
-                ;;
-            esac
-          done < <(git diff --name-only --no-renames "$BASE_SHA" "$HEAD_SHA")
-
-          echo "v8_canary=$v8_canary" >> "$GITHUB_OUTPUT"
-
   rust-ci-full:
     name: rust-ci-full
     uses: ./.github/workflows/rust-ci-full.yml
@@ -60,38 +12,23 @@ jobs:
 
   v8-canary:
     name: v8-canary
-    needs: changes
-    if: needs.changes.outputs.v8_canary == 'true'
     uses: ./.github/workflows/v8-canary.yml
     secrets: inherit
 
   results:
     name: Postmerge CI results
     needs:
-      - changes
       - rust-ci-full
       - v8-canary
     if: ${{ always() }}
     runs-on: ubuntu-24.04
     steps:
+      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
+        with:
+          ref: ${{ github.sha }}
+          persist-credentials: false
+
       - name: Require successful dependencies
         env:
-          V8_CANARY_REQUIRED: ${{ needs.changes.outputs.v8_canary }}
-        shell: bash
-        run: |
-          set -euo pipefail
-
-          echo "changes     : ${{ needs.changes.result }}"
-          echo "rust-ci-full: ${{ needs.rust-ci-full.result }}"
-          echo "v8-canary  : ${{ needs.v8-canary.result }}"
-
-          [[ '${{ needs.changes.result }}' == 'success' ]] || { echo 'change detection failed'; exit 1; }
-          [[ '${{ needs.rust-ci-full.result }}' == 'success' ]] || { echo 'rust-ci-full failed'; exit 1; }
-
-          if [[ "$V8_CANARY_REQUIRED" == 'true' ]]; then
-            [[ '${{ needs.v8-canary.result }}' == 'success' ]] || { echo 'v8-canary failed'; exit 1; }
-          else
-            [[ '${{ needs.v8-canary.result }}' == 'skipped' ]] || { echo 'v8-canary ran unexpectedly'; exit 1; }
-          fi
-
-          echo 'All required postmerge CI dependencies succeeded.'
+          NEEDS: ${{ toJSON(needs) }}
+        run: python3 .github/scripts/check_ci_results.py
diff --git a/.github/workflows/v8-canary.yml b/.github/workflows/v8-canary.yml
index 6f10aba64c76..14941e1cef5f 100644
--- a/.github/workflows/v8-canary.yml
+++ b/.github/workflows/v8-canary.yml
@@ -2,25 +2,7 @@ name: v8-canary
 
 on:
   workflow_call:
-  pull_request:
-    paths:
-      - ".bazelrc"
-      - ".github/actions/setup-bazel-ci/**"
-      - ".github/scripts/run_bazel_with_buildbuddy.py"
-      - ".github/scripts/rusty_v8_bazel.py"
-      - ".github/scripts/rusty_v8_module_bazel.py"
-      - ".github/scripts/v8_canary_changes.py"
-      - ".github/workflows/postmerge-ci.yml"
-      - ".github/workflows/rusty-v8-release.yml"
-      - ".github/workflows/v8-canary.yml"
-      - "MODULE.bazel"
-      - "MODULE.bazel.lock"
-      - "codex-rs/Cargo.toml"
-      - "patches/BUILD.bazel"
-      - "patches/llvm_*.patch"
-      - "patches/rules_cc_*.patch"
-      - "patches/v8_*.patch"
-      - "third_party/v8/**"
+  pull_request: {}
   workflow_dispatch:
 
 # Cargo's libgit2 transport has been flaky when fetching git dependencies with
@@ -36,6 +18,7 @@ jobs:
   metadata:
     runs-on: ubuntu-latest
     outputs:
+      canary_required: ${{ steps.changes.outputs.canary_required }}
       v8_version: ${{ steps.v8_version.outputs.version }}
       windows_source_required: ${{ steps.changes.outputs.windows_source_required }}
 
@@ -59,7 +42,7 @@ jobs:
           version="$(python3 .github/scripts/rusty_v8_bazel.py resolved-v8-crate-version)"
           echo "version=${version}" >> "$GITHUB_OUTPUT"
 
-      - name: Detect whether Windows source artifacts need rebuilding
+      - name: Detect V8 canary changes
         id: changes
         env:
           BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
@@ -86,6 +69,7 @@ jobs:
   build:
     name: Build ${{ matrix.variant }} ${{ matrix.target }}
     needs: metadata
+    if: ${{ needs.metadata.outputs.canary_required == 'true' }}
     runs-on: ${{ matrix.runner }}
     permissions:
       contents: read

From ee61530d747b9f8fd06966aa731c3d2456ef3225 Mon Sep 17 00:00:00 2001
From: Adam Perry <anp@openai.com>
Date: Fri, 26 Jun 2026 04:20:45 +0000
Subject: [PATCH 7/9] document CI orchestration invariants

---
 .github/scripts/check_ci_results.py    |  9 +++++++++
 .github/scripts/v8_canary_changes.py   | 19 +++++++++++++++++++
 .github/workflows/blob-size-policy.yml |  2 ++
 .github/workflows/blocking-ci.yml      |  9 +++++++++
 .github/workflows/postmerge-ci.yml     |  6 ++++++
 .github/workflows/rust-ci-full.yml     |  2 ++
 .github/workflows/v8-canary.yml        | 12 +++++++++++-
 7 files changed, 58 insertions(+), 1 deletion(-)

diff --git a/.github/scripts/check_ci_results.py b/.github/scripts/check_ci_results.py
index 483649227194..36d1eed3b4ee 100644
--- a/.github/scripts/check_ci_results.py
+++ b/.github/scripts/check_ci_results.py
@@ -1,10 +1,19 @@
 #!/usr/bin/env python3
 
+"""Fail a terminal CI job unless every serialized dependency succeeded.
+
+Parent workflows pass GitHub's `toJSON(needs)` object through the NEEDS
+environment variable. Treat skipped and cancelled dependencies as failures too:
+for a required fan-in job, only an explicit success is safe to accept.
+"""
+
 import json
 import os
 
 
 def main() -> None:
+    # Keep result policy in one script so blocking-ci and postmerge-ci cannot
+    # drift in how they interpret dependency conclusions.
     needs = json.loads(os.environ["NEEDS"])
     failures = sorted(
         (name, dependency["result"])
diff --git a/.github/scripts/v8_canary_changes.py b/.github/scripts/v8_canary_changes.py
index c8c539163238..0acc4ec3f6bc 100644
--- a/.github/scripts/v8_canary_changes.py
+++ b/.github/scripts/v8_canary_changes.py
@@ -1,5 +1,13 @@
 #!/usr/bin/env python3
 
+"""Decide which V8 canary work is needed for a commit range.
+
+The workflow deliberately has no trigger-level path filters because it is both
+directly triggered for pull requests and called by postmerge-ci. Keeping the
+patterns here gives those entrypoints one source of truth; unrelated events
+still run metadata but skip the expensive build matrices.
+"""
+
 import argparse
 import subprocess
 import tomllib
@@ -8,6 +16,8 @@
 
 
 ROOT = Path(__file__).resolve().parents[2]
+# These patterns replace the old pull_request/push path filters. Include parent
+# workflow changes because they can alter whether the canary is invoked.
 CANARY_PATH_PATTERNS = {
     ".bazelrc",
     ".github/actions/setup-bazel-ci/**",
@@ -27,6 +37,8 @@
     "patches/v8_*.patch",
     "third_party/v8/**",
 }
+# Windows source builds are a narrower, more expensive subset of the canary.
+# A V8 version change also requires them even when no path below changed.
 WINDOWS_SOURCE_BUILD_PATHS = {
     ".github/scripts/rusty_v8_bazel.py",
     ".github/scripts/rusty_v8_module_bazel.py",
@@ -37,6 +49,7 @@
 
 
 def matching_canary_paths(changed_files: set[str]) -> set[str]:
+    """Return changed paths that require the general V8 build matrix."""
     return {
         path
         for path in changed_files
@@ -51,6 +64,7 @@ def canary_required(
     *,
     force: bool = False,
 ) -> bool:
+    """Return whether the general V8 build matrix should run."""
     return (
         force
         or base_v8_version != head_v8_version
@@ -78,6 +92,7 @@ def windows_source_required(
     *,
     force: bool = False,
 ) -> bool:
+    """Return whether Windows must rebuild rusty_v8 from source."""
     return (
         force
         or base_v8_version != head_v8_version
@@ -100,6 +115,8 @@ def merge_base(base: str, head: str, *, root: Path = ROOT) -> str:
 
 
 def changed_files(base: str, head: str, *, root: Path = ROOT) -> set[str]:
+    # Three-dot diff gives PRs merge-base semantics while remaining equivalent
+    # to before/after for ordinary linear pushes to main.
     output = git_output(
         "diff",
         "--name-only",
@@ -121,6 +138,8 @@ def parse_args() -> argparse.Namespace:
 def main() -> None:
     args = parse_args()
     if args.force:
+        # workflow_dispatch has no comparison range, and callers use it as a
+        # manual retry path, so it intentionally runs every variant.
         canary = True
         canary_reason = "manual workflow dispatch"
         windows_source = True
diff --git a/.github/workflows/blob-size-policy.yml b/.github/workflows/blob-size-policy.yml
index a5f937420e40..fddc51afe7c2 100644
--- a/.github/workflows/blob-size-policy.yml
+++ b/.github/workflows/blob-size-policy.yml
@@ -20,6 +20,8 @@ jobs:
         run: |
           set -euo pipefail
 
+          # PRs inspect the proposed diff; main pushes inspect only the commit
+          # range that just landed. Both paths feed the same blob-size checker.
           if [[ "${{ github.event_name }}" == "pull_request" ]]; then
             base='${{ github.event.pull_request.base.sha }}'
             head='${{ github.event.pull_request.head.sha }}'
diff --git a/.github/workflows/blocking-ci.yml b/.github/workflows/blocking-ci.yml
index 52731efeb678..9cfde767f92c 100644
--- a/.github/workflows/blocking-ci.yml
+++ b/.github/workflows/blocking-ci.yml
@@ -1,11 +1,15 @@
 name: blocking-ci
 
+# This is the single entrypoint for checks that block a PR merge. It also runs
+# after pushes to main so the same check family stays grouped in the Actions UI.
 on:
   pull_request: {}
   push:
     branches: [main]
 
 jobs:
+  # Keep reusable workflow calls alphabetized. The `required` job below is the
+  # version-controlled list that the main-branch ruleset should require.
   bazel:
     name: Bazel
     uses: ./.github/workflows/bazel.yml
@@ -43,6 +47,8 @@ jobs:
 
   required:
     name: CI required
+    # Without `always()`, GitHub skips this job after a failed dependency and a
+    # required check can appear successful instead of reporting the failure.
     if: ${{ always() }}
     needs:
       - bazel
@@ -54,6 +60,9 @@ jobs:
       - sdk
     runs-on: ubuntu-24.04
     steps:
+      # Match the child workflows by checking out the PR head. During the
+      # initial rollout, PR branches created before check_ci_results.py exists
+      # must rebase onto main before this gate can run.
       - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
         with:
           ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
diff --git a/.github/workflows/postmerge-ci.yml b/.github/workflows/postmerge-ci.yml
index f5f6bd153b9c..ac2b46d1db4c 100644
--- a/.github/workflows/postmerge-ci.yml
+++ b/.github/workflows/postmerge-ci.yml
@@ -1,10 +1,14 @@
 name: postmerge-ci
 
+# This is the single entrypoint for main-only CI that is intentionally outside
+# the merge-blocking suite. It keeps the broader postmerge signal in one run.
 on:
   push:
     branches: [main]
 
 jobs:
+  # Keep reusable workflow calls alphabetized. Each child retains its own
+  # workflow_dispatch trigger so maintainers can rerun flaky suites directly.
   rust-ci-full:
     name: rust-ci-full
     uses: ./.github/workflows/rust-ci-full.yml
@@ -23,6 +27,8 @@ jobs:
     if: ${{ always() }}
     runs-on: ubuntu-24.04
     steps:
+      # Postmerge runs use the pushed main commit, so this helper always comes
+      # from the same revision that defined the parent workflow.
       - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
         with:
           ref: ${{ github.sha }}
diff --git a/.github/workflows/rust-ci-full.yml b/.github/workflows/rust-ci-full.yml
index b60c0b7ca24e..cb99c8911e11 100644
--- a/.github/workflows/rust-ci-full.yml
+++ b/.github/workflows/rust-ci-full.yml
@@ -3,6 +3,8 @@ on:
   workflow_call:
   push:
     branches:
+      # Main pushes enter through postmerge-ci. Keep this opt-in branch trigger
+      # for developers who want the full suite before merging.
       - "**full-ci**"
   workflow_dispatch:
 
diff --git a/.github/workflows/v8-canary.yml b/.github/workflows/v8-canary.yml
index 14941e1cef5f..53d9585108e8 100644
--- a/.github/workflows/v8-canary.yml
+++ b/.github/workflows/v8-canary.yml
@@ -1,5 +1,9 @@
 name: v8-canary
 
+# Do not use trigger-level path filters here. This workflow is also called by
+# postmerge-ci, and GitHub cannot share a path filter between pull_request and
+# workflow_call. v8_canary_changes.py is the single source of truth instead;
+# unrelated events run only the cheap metadata job below.
 on:
   workflow_call:
   pull_request: {}
@@ -18,7 +22,10 @@ jobs:
   metadata:
     runs-on: ubuntu-latest
     outputs:
-      canary_required: ${{ steps.changes.outputs.canary_required }}
+      # A stale PR head can contain the old detector, which does not emit this
+      # output. Missing must mean "run" so older branches cannot silently skip
+      # the expensive V8 coverage while reporting success.
+      canary_required: ${{ steps.changes.outputs.canary_required || 'true' }}
       v8_version: ${{ steps.v8_version.outputs.version }}
       windows_source_required: ${{ steps.changes.outputs.windows_source_required }}
 
@@ -52,6 +59,8 @@ jobs:
         run: |
           set -euo pipefail
 
+          # Manual runs have no meaningful before/after range. Force every V8
+          # variant so workflow_dispatch remains a reliable retry path.
           if [[ "${EVENT_NAME}" == "workflow_dispatch" ]]; then
             output="$(python3 .github/scripts/v8_canary_changes.py --force)"
           else
@@ -69,6 +78,7 @@ jobs:
   build:
     name: Build ${{ matrix.variant }} ${{ matrix.target }}
     needs: metadata
+    # Metadata always runs; only relevant changes pay for the large matrix.
     if: ${{ needs.metadata.outputs.canary_required == 'true' }}
     runs-on: ${{ matrix.runner }}
     permissions:

From 9997831b9c1bdfed715f5746edf7323036a2fb74 Mon Sep 17 00:00:00 2001
From: Adam Perry <anp@openai.com>
Date: Fri, 26 Jun 2026 19:17:51 +0000
Subject: [PATCH 8/9] trust CI gate helper on PRs

---
 .github/workflows/blocking-ci.yml | 14 ++++++++++----
 1 file changed, 10 insertions(+), 4 deletions(-)

diff --git a/.github/workflows/blocking-ci.yml b/.github/workflows/blocking-ci.yml
index 9cfde767f92c..e73263ab8d7f 100644
--- a/.github/workflows/blocking-ci.yml
+++ b/.github/workflows/blocking-ci.yml
@@ -60,12 +60,18 @@ jobs:
       - sdk
     runs-on: ubuntu-24.04
     steps:
-      # Match the child workflows by checking out the PR head. During the
-      # initial rollout, PR branches created before check_ci_results.py exists
-      # must rebase onto main before this gate can run.
+      # The terminal gate is policy, not code under test. For pull requests,
+      # load its helper from the trusted base commit instead of the PR head or
+      # synthetic merge commit. Otherwise a PR could change the helper to
+      # report success after one of the required child workflows failed.
+      #
+      # Using the base commit also lets branches created before this helper was
+      # added pick it up after main advances, without requiring a rebase. Main
+      # pushes use the pushed commit because it is already on the protected
+      # branch.
       - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
         with:
-          ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
+          ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.sha }}
           persist-credentials: false
 
       - name: Require successful dependencies

From eb158bfb5a1551a71bd574ad00eb63289cd6e188 Mon Sep 17 00:00:00 2001
From: Adam Perry <anp@openai.com>
Date: Fri, 26 Jun 2026 19:35:17 +0000
Subject: [PATCH 9/9] restore CI gate head checkout

---
 .github/workflows/blocking-ci.yml | 17 ++++++++---------
 1 file changed, 8 insertions(+), 9 deletions(-)

diff --git a/.github/workflows/blocking-ci.yml b/.github/workflows/blocking-ci.yml
index e73263ab8d7f..fe66fdd50952 100644
--- a/.github/workflows/blocking-ci.yml
+++ b/.github/workflows/blocking-ci.yml
@@ -60,18 +60,17 @@ jobs:
       - sdk
     runs-on: ubuntu-24.04
     steps:
-      # The terminal gate is policy, not code under test. For pull requests,
-      # load its helper from the trusted base commit instead of the PR head or
-      # synthetic merge commit. Otherwise a PR could change the helper to
-      # report success after one of the required child workflows failed.
+      # Keep the helper on the same revision as the caller and child workflows.
+      # CI workflow uploads are restricted, so this repository does not need a
+      # separate trusted-base checkout for the terminal policy step. Using the
+      # PR head also lets the introducing PR exercise a newly added helper.
       #
-      # Using the base commit also lets branches created before this helper was
-      # added pick it up after main advances, without requiring a rebase. Main
-      # pushes use the pushed commit because it is already on the protected
-      # branch.
+      # During the initial rollout, PR branches created before
+      # check_ci_results.py exists must rebase onto main before this gate can
+      # run.
       - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
         with:
-          ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.sha }}
+          ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
           persist-credentials: false
 
       - name: Require successful dependencies
