ci(v8): gate Windows source builds on relevant changes (#27715)

Avoid rebuilding sandboxed Windows MSVC V8 artifacts for unrelated
changes to `codex-rs/Cargo.toml`.

The V8 canary now compares the resolved V8 version between the base and
head commits and only runs the Windows source-build matrix when:

- the resolved V8 crate version changes;
- Windows artifact-production scripts or workflows change; or
- the workflow is manually dispatched.

The existing Bazel V8 matrix is unchanged.

## Why

The Windows MSVC source builds take roughly two to three hours and
currently run whenever any entry in the broad `v8-canary` path filter
changes.
This commit is contained in:
Channing Conger
2026-06-11 18:44:42 -07:00
committed by GitHub
Unverified
parent 69b0f52b2a
commit 16c7c79540
3 changed files with 227 additions and 0 deletions
+98
View File
@@ -0,0 +1,98 @@
import subprocess
import tempfile
import unittest
from pathlib import Path
from v8_canary_changes import changed_files
from v8_canary_changes import merge_base
from v8_canary_changes import resolved_v8_version
from v8_canary_changes import windows_source_required
class V8CanaryChangesTest(unittest.TestCase):
def test_resolved_v8_version(self) -> None:
cargo_lock = b"""\
[[package]]
name = "other"
version = "1.0.0"
[[package]]
name = "v8"
version = "149.2.0"
"""
self.assertEqual(resolved_v8_version(cargo_lock), "149.2.0")
def test_unrelated_cargo_manifest_change_does_not_require_source_build(
self,
) -> None:
self.assertFalse(
windows_source_required(
{"codex-rs/Cargo.toml"},
"149.2.0",
"149.2.0",
)
)
def test_v8_version_change_requires_source_build(self) -> None:
self.assertTrue(windows_source_required(set(), "149.2.0", "150.0.0"))
def test_module_helper_change_requires_source_build(self) -> None:
self.assertTrue(
windows_source_required(
{".github/scripts/rusty_v8_module_bazel.py"},
"149.2.0",
"149.2.0",
)
)
def test_manual_dispatch_requires_source_build(self) -> None:
self.assertTrue(
windows_source_required(
set(),
"149.2.0",
"149.2.0",
force=True,
)
)
def test_changed_files_excludes_changes_made_only_on_base_branch(self) -> None:
with tempfile.TemporaryDirectory() as temp_dir:
root = Path(temp_dir)
self.run_git(root, "init", "--initial-branch=main")
self.run_git(root, "config", "user.name", "Test User")
self.run_git(root, "config", "user.email", "test@example.com")
self.write_and_commit(root, "initial", "initial.txt")
common = self.run_git(root, "rev-parse", "HEAD")
self.run_git(root, "switch", "-c", "feature")
self.run_git(root, "switch", "main")
self.write_and_commit(root, "base-only", "base-only.txt")
base = self.run_git(root, "rev-parse", "HEAD")
self.run_git(root, "switch", "feature")
self.write_and_commit(root, "feature-only", "feature-only.txt")
head = self.run_git(root, "rev-parse", "HEAD")
self.assertEqual(
changed_files(base, head, root=root),
{"feature-only.txt"},
)
self.assertEqual(merge_base(base, head, root=root), common)
def write_and_commit(self, root: Path, contents: str, path: str) -> None:
(root / path).write_text(contents)
self.run_git(root, "add", path)
self.run_git(root, "commit", "-m", contents)
def run_git(self, root: Path, *args: str) -> str:
return subprocess.check_output(
["git", *args],
cwd=root,
stderr=subprocess.PIPE,
text=True,
).strip()
if __name__ == "__main__":
unittest.main()
+104
View File
@@ -0,0 +1,104 @@
#!/usr/bin/env python3
import argparse
import subprocess
import tomllib
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
WINDOWS_SOURCE_BUILD_PATHS = {
".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",
}
def resolved_v8_version(cargo_lock: bytes) -> str:
versions = sorted(
{
package["version"]
for package in tomllib.loads(cargo_lock.decode())["package"]
if package["name"] == "v8"
}
)
if len(versions) != 1:
raise ValueError(f"expected exactly one resolved v8 version, found: {versions}")
return versions[0]
def windows_source_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 not changed_files.isdisjoint(WINDOWS_SOURCE_BUILD_PATHS)
)
def git_output(*args: str, root: Path = ROOT) -> bytes:
return subprocess.check_output(["git", *args], cwd=root)
def v8_version_at_revision(revision: str, *, root: Path = ROOT) -> str:
return resolved_v8_version(
git_output("show", f"{revision}:codex-rs/Cargo.lock", root=root)
)
def merge_base(base: str, head: str, *, root: Path = ROOT) -> str:
return git_output("merge-base", base, head, root=root).decode().strip()
def changed_files(base: str, head: str, *, root: Path = ROOT) -> set[str]:
output = git_output(
"diff",
"--name-only",
"--no-renames",
f"{base}...{head}",
root=root,
)
return set(output.decode().splitlines())
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser()
parser.add_argument("--base")
parser.add_argument("--head")
parser.add_argument("--force", action="store_true")
return parser.parse_args()
def main() -> None:
args = parse_args()
if args.force:
required = True
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)
if base_version != head_version:
reason = f"v8 version changed from {base_version} to {head_version}"
else:
matched_paths = sorted(files & WINDOWS_SOURCE_BUILD_PATHS)
reason = (
", ".join(matched_paths) if matched_paths else "no relevant changes"
)
print(f"windows_source_required={str(required).lower()}")
print(f"windows_source_reason={reason}")
if __name__ == "__main__":
main()
+25
View File
@@ -8,6 +8,7 @@ on:
- ".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"
@@ -27,6 +28,7 @@ on:
- ".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"
@@ -53,11 +55,13 @@ jobs:
runs-on: ubuntu-latest
outputs:
v8_version: ${{ steps.v8_version.outputs.version }}
windows_source_required: ${{ steps.changes.outputs.windows_source_required }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
fetch-depth: 0
persist-credentials: false
- name: Set up Python
@@ -73,6 +77,26 @@ 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
id: changes
env:
BASE_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || github.event.before }}
EVENT_NAME: ${{ github.event_name }}
HEAD_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }}
shell: bash
run: |
set -euo pipefail
if [[ "${EVENT_NAME}" == "workflow_dispatch" ]]; then
output="$(python3 .github/scripts/v8_canary_changes.py --force)"
else
output="$(python3 .github/scripts/v8_canary_changes.py \
--base "${BASE_SHA}" \
--head "${HEAD_SHA}")"
fi
echo "${output}"
echo "${output}" >> "${GITHUB_OUTPUT}"
build:
name: Build ${{ matrix.variant }} ${{ matrix.target }}
needs: metadata
@@ -301,6 +325,7 @@ jobs:
build-windows-source:
name: Build ptrcomp-sandbox ${{ matrix.target }} from source
needs: metadata
if: ${{ needs.metadata.outputs.windows_source_required == 'true' }}
runs-on: ${{ matrix.runner }}
permissions:
contents: read