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
34 changes: 34 additions & 0 deletions .github/scripts/check_ci_results.py
Original file line number Diff line number Diff line change
@@ -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()
96 changes: 87 additions & 9 deletions .github/scripts/v8_canary_changes.py
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -16,6 +48,30 @@
}


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(
{
Expand All @@ -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
Expand All @@ -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",
Expand All @@ -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__":
Expand Down
5 changes: 1 addition & 4 deletions .github/workflows/bazel.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
19 changes: 15 additions & 4 deletions .github/workflows/blob-size-policy.yml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name: blob-size-policy

on:
pull_request: {}
workflow_call:

jobs:
check:
Expand All @@ -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:
Expand Down
79 changes: 79 additions & 0 deletions .github/workflows/blocking-ci.yml
Original file line number Diff line number Diff line change
@@ -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
Comment thread
anp-oai marked this conversation as resolved.
5 changes: 1 addition & 4 deletions .github/workflows/cargo-deny.yml
Original file line number Diff line number Diff line change
@@ -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.
Expand Down
5 changes: 1 addition & 4 deletions .github/workflows/codespell.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@
name: Codespell

on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_call:

permissions:
contents: read
Expand Down
40 changes: 40 additions & 0 deletions .github/workflows/postmerge-ci.yml
Original file line number Diff line number Diff line change
@@ -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
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
name: ci
name: repo-checks

on:
pull_request: {}
push: { branches: [main] }
workflow_call:

jobs:
build-test:
Expand Down
4 changes: 3 additions & 1 deletion .github/workflows/rust-ci-full.yml
Original file line number Diff line number Diff line change
@@ -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:

Expand Down
Loading
Loading