Add authenticated GCS caching for Bazel CI and releases (#61187)

## What changed

- Authenticate eligible protected `main` runs and Rust release tags with GitHub OIDC. Allow cache uploads only on `main` pushes; use read-only access for manual runs and release tags, and leave pull requests without GCS credentials.
- Rename the shared wrapper to `.github/scripts/run_bazel.py` and route Bazel callers through it. Disable the disk cache in GitHub Actions and default macOS GCS connections to 20 while respecting explicit limits.
- Gate additional Bazel release artifacts on `CODEX_BAZEL_RELEASES` and cache configuration, sharing the selected build systems across building, signing, and packaging. Keep Cargo as the default and prepare native Windows dependencies for Bazel releases.
- Add a `Bazel result` check for the full workflow and extend Bazel job timeouts.

## Testing

Add wrapper tests for cache access by event, argument preservation, and macOS connection limits. The wrapper test suite passes.

GitOrigin-RevId: d85edbd083acfaa8bafaa8fd5241daed3e9ef871
This commit is contained in:
Dmitriy Kovalenko
2026-11-12 21:41:05 +00:00
committed by copyberry
parent d0221bc947
commit 4490060db6
25 changed files with 464 additions and 184 deletions
+157
View File
@@ -0,0 +1,157 @@
#!/usr/bin/env python3
import os
import re
import subprocess
import sys
from collections.abc import Mapping
from collections.abc import Sequence
from pathlib import Path
# These configs select remote execution, which is unavailable on CI runners.
REMOTE_EXECUTION_CONFIGS = {
"--config=ci-linux",
"--config=ci-macos",
"--config=ci-v8",
"--config=ci-windows-cross",
}
# Honor either explicit setting so the wrapper never overrides the caller's
# choice when it supplies the CI default below.
REMOTE_REPO_CONTENTS_CACHE_STARTUP_OPTIONS = {
"--experimental_remote_repo_contents_cache",
"--noexperimental_remote_repo_contents_cache",
}
def startup_args(args: Sequence[str], env: Mapping[str, str]) -> list[str]:
"""Return shared startup options that are missing from a Bazel invocation.
Bazel startup options must precede the command, and changing them restarts
the server and discards its analysis cache. GitHub Actions invokes Bazel
through several helpers, so normalize their startup options here while
preserving any explicit choice made by the caller.
"""
command_idx = next(
(idx for idx, arg in enumerate(args) if not arg.startswith("-")),
len(args),
)
configured_startup_args = args[:command_idx]
injected_args = []
output_user_root = env.get("BAZEL_OUTPUT_USER_ROOT")
if output_user_root and not any(
arg.startswith("--output_user_root=") for arg in configured_startup_args
):
injected_args.append(f"--output_user_root={output_user_root}")
if env.get("GITHUB_ACTIONS") == "true" and not any(
arg in REMOTE_REPO_CONTENTS_CACHE_STARTUP_OPTIONS
for arg in configured_startup_args
):
# Work around Bazel 9 overlay materialization failures seen in CI. This
# disables only the startup-level repo contents cache.
injected_args.append("--noexperimental_remote_repo_contents_cache")
return injected_args
def bazel_args_without_remote_execution(args: Sequence[str]) -> list[str]:
try:
separator_idx = args.index("--")
except ValueError:
separator_idx = len(args)
return [
*(arg for arg in args[:separator_idx] if arg not in REMOTE_EXECUTION_CONFIGS),
*args[separator_idx:],
]
def bazel_args_with_remote_config(
args: Sequence[str], env: Mapping[str, str]
) -> list[str]:
command_idx = next(
(idx for idx, arg in enumerate(args) if not arg.startswith("-")),
None,
)
if command_idx is None:
raise ValueError("expected a Bazel command")
configured_args = bazel_args_without_remote_execution(args)
try:
separator_idx = configured_args.index("--")
except ValueError:
separator_idx = len(configured_args)
cache_args = [
f"{option_prefix}{env[env_name]}"
for env_name, option_prefix in (
("BAZEL_REPO_CONTENTS_CACHE", "--repo_contents_cache="),
("BAZEL_REPOSITORY_CACHE", "--repository_cache="),
)
if env.get(env_name)
and not any(
arg.startswith(option_prefix) for arg in configured_args[:separator_idx]
)
]
# Ephemeral runners already retain active outputs in Bazel's output tree.
# Avoid storing a second copy while using the GCS action cache.
if env.get("GITHUB_ACTIONS") == "true":
cache_args.append("--disk_cache=")
bucket = env.get("CODEX_BAZEL_GCS_BUCKET")
credentials = env.get("CODEX_BAZEL_GCS_CREDENTIALS")
if (
bucket
and credentials
and args[command_idx] in {"build", "test", "run", "coverage"}
):
if not re.fullmatch(r"[a-z0-9][a-z0-9_-]{1,61}[a-z0-9]", bucket):
raise ValueError("Expected a GCS bucket name")
if not Path(credentials).is_absolute() or not Path(credentials).is_file():
raise ValueError(
"Missing GitHub workload identity credentials for the GCS cache"
)
cache_args.extend(
(
f"--remote_cache=https://storage.googleapis.com/{bucket}",
f"--google_credentials={credentials}",
)
)
# Bound HTTP socket pressure on macOS runners while reusing the GCS cache.
if env.get("RUNNER_OS") == "macOS" and not any(
arg == "--remote_max_connections"
or arg.startswith("--remote_max_connections=")
for arg in configured_args[:separator_idx]
):
cache_args.append("--remote_max_connections=20")
if (
env.get("GITHUB_EVENT_NAME") != "push"
or env.get("GITHUB_REF") != "refs/heads/main"
):
cache_args.append("--remote_upload_local_results=false")
return [
*configured_args[:separator_idx],
*cache_args,
*configured_args[separator_idx:],
]
def bazel_command(*args: str, env: Mapping[str, str] | None = None) -> list[str]:
env = os.environ if env is None else env
bazel = env.get("CODEX_BAZEL_BIN", "bazel")
return [bazel, *startup_args(args, env), *bazel_args_with_remote_config(args, env)]
def main() -> None:
command = bazel_command(*sys.argv[1:])
if os.name == "nt":
# Windows CRT exec can split arguments containing spaces and lose the
# eventual child exit status. Wait for Bazel and propagate its status.
result = subprocess.run(command, check=False)
raise SystemExit(result.returncode)
os.execvp(command[0], command)
if __name__ == "__main__":
main()