[codex] group blocking and postmerge CI workflows (#30146)

## Why

It's hard to change the set of required jobs when they're managed in the
GitHub UI, and when each workflow is responsible for choosing it's own
scheduling it's easy to end up with skew between what we enforce on PRs
vs. on main.

## What

- add a `blocking-ci` caller workflow, triggered by pull requests and
pushes to `main`, for Bazel, blob size, cargo-deny, Codespell,
`repo-checks`, rust CI, and SDK CI
- add an `always()` terminal job named `CI required` that fails unless
every called workflow succeeds
- add a `postmerge-ci` caller workflow for `rust-ci-full` and
`v8-canary`, with a terminal `Postmerge CI results` job
- centralize V8 relevance detection in `v8_canary_changes.py`; unrelated
PR and postmerge runs execute metadata only and skip the expensive build
matrices
- leave `v8-canary` outside the blocking gate and leave the external
`cla` check independent

## Rollout

A repository admin must replace the existing required GitHub Actions
contexts with `CI required` in the main-branch ruleset. Retain `cla` as
a separate required check. Until that change is coordinated, this PR
cannot satisfy the old standalone check names. In-flight PRs will need
to be rebased after this lands.
This commit is contained in:
Adam Perry @ OpenAI
2026-06-26 15:07:05 -07:00
committed by GitHub
Unverified
parent 6509f3148a
commit 1168254bd9
13 changed files with 280 additions and 72 deletions
+34
View File
@@ -0,0 +1,34 @@
#!/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"])
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()
+87 -9
View File
@@ -1,12 +1,44 @@
#!/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
from fnmatch import fnmatchcase
from pathlib import Path
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/**",
".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 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",
@@ -16,6 +48,30 @@ WINDOWS_SOURCE_BUILD_PATHS = {
}
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
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 whether the general V8 build matrix should run."""
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(
{
@@ -36,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
@@ -58,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",
@@ -79,25 +138,44 @@ def parse_args() -> argparse.Namespace:
def main() -> None:
args = parse_args()
if args.force:
required = True
reason = "manual workflow dispatch"
# 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
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__":
+1 -4
View File
@@ -4,10 +4,7 @@ name: Bazel
# https://github.com/cerisier/toolchains_llvm_bootstrapped/blob/main/.github/workflows/ci.yaml
on:
pull_request: {}
push:
branches:
- main
workflow_call:
workflow_dispatch:
concurrency:
+15 -4
View File
@@ -1,7 +1,7 @@
name: blob-size-policy
on:
pull_request: {}
workflow_call:
jobs:
check:
@@ -14,13 +14,24 @@ 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"
# 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 }}'
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:
+79
View File
@@ -0,0 +1,79 @@
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
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
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
secrets: inherit
sdk:
name: sdk
uses: ./.github/workflows/sdk.yml
secrets: inherit
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
- blob-size-policy
- cargo-deny
- codespell
- repo-checks
- rust-ci
- sdk
runs-on: ubuntu-24.04
steps:
# 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.
#
# 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 }}
persist-credentials: false
- name: Require successful dependencies
env:
NEEDS: ${{ toJSON(needs) }}
run: python3 .github/scripts/check_ci_results.py
+1 -4
View File
@@ -1,10 +1,7 @@
name: cargo-deny
on:
pull_request:
push:
branches:
- main
workflow_call:
# Cargo's libgit2 transport has been flaky when fetching git dependencies with
# nested submodules. Prefer the system git CLI across every Cargo invocation.
+1 -4
View File
@@ -3,10 +3,7 @@
name: Codespell
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_call:
permissions:
contents: read
+40
View File
@@ -0,0 +1,40 @@
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
secrets: inherit
v8-canary:
name: v8-canary
uses: ./.github/workflows/v8-canary.yml
secrets: inherit
results:
name: Postmerge CI results
needs:
- rust-ci-full
- v8-canary
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 }}
persist-credentials: false
- name: Require successful dependencies
env:
NEEDS: ${{ toJSON(needs) }}
run: python3 .github/scripts/check_ci_results.py
@@ -1,8 +1,7 @@
name: ci
name: repo-checks
on:
pull_request: {}
push: { branches: [main] }
workflow_call:
jobs:
build-test:
+3 -1
View File
@@ -1,8 +1,10 @@
name: rust-ci-full
on:
workflow_call:
push:
branches:
- main
# 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:
+1 -1
View File
@@ -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
+1 -3
View File
@@ -1,9 +1,7 @@
name: sdk
on:
push:
branches: [main]
pull_request: {}
workflow_call:
jobs:
python-sdk:
+15 -39
View File
@@ -1,44 +1,12 @@
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:
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/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/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/**"
workflow_call:
pull_request: {}
workflow_dispatch:
# Cargo's libgit2 transport has been flaky when fetching git dependencies with
@@ -54,6 +22,10 @@ jobs:
metadata:
runs-on: ubuntu-latest
outputs:
# 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 }}
@@ -77,7 +49,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 }}
@@ -87,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
@@ -104,6 +78,8 @@ 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:
contents: read