[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
+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__":