Skip to content

feat(ci): send builds to Aspect Cloud - #1541

Closed
joshcbarnes wants to merge 1 commit into
mainfrom
feat/aspect-cloud
Closed

joshcbarnes wants to merge 1 commit into
mainfrom
feat/aspect-cloud

Conversation

@joshcbarnes

Copy link
Copy Markdown
Contributor

Connects this repo's builds to Aspect Cloud's remote cache and BES, so invocations appear in app.aspect.build.

The gap this closes

CI runs on stock GitHub Actions runners, not Aspect Workflows runners. That matters more than it looks: the Aspect CLI wires BES from the environment (ASPECT_WORKFLOWS_BES_BACKEND and friends, set by a Workflows runner). None of those variables exist here, and nothing passed --remote or --bes_backend, so the CLI tasks were uploading nowhere.

Easy to miss, because the Aspect GitHub App's check runs travel a different path — the API, not BES. Status checks looked healthy on every PR while zero invocations reached the UI.

.aspect/bazelrc (new)

Follows the shape aspect setup bazelrc generates (aspect-build/aspect-cli#1442). Flags are grouped by the endpoint that justifies them, so a deployment with a cache but no BES gets the cache tuning and none of the BES tuning:

Group Presumes Holds
aspect-common nothing --heap_dump_on_oom, --experimental_repository_cache_hardlinks
aspect-cache a remote cache --remote_upload_local_results, --remote_accept_cached, --remote_timeout, --remote_retries, --grpc_keepalive_timeout
aspect-bes a BES backend profile generation + enrichment, --legacy_important_outputs, --remote_build_event_upload=all
aspect-exec-log a writable .aspect/generated --execution_log_compact_file

--config=aspect-cloud chains the first three and adds the endpoints plus the aspect credential helper, scoped per host. Every line is common:<config>-scoped, so the file is inert until named — a fresh clone builds exactly as before.

aspect-exec-log is deliberately not in aspect-cloud. Its path is %workspace%-relative and Bazel will not create the parent directory, so folding it back in breaks the ~20 sibling workspaces under e2e/ and examples/. Only the root workspace names it, via .aspect/config.axl.

.bazelrc gains try-import %workspace%/.aspect/bazelrc at the top, so the repo's own lines come after and win.

One mechanism, every job

.aspect/config.axl can only ever cover the root workspace: it is read by the Aspect CLI alone and is per-workspace, so it reaches neither the bazel run @uv steps in other trees nor the aspect test runs that cd into e2e/* and examples/*. The repo's .bazelrc has the same workspace limit, and the e2e test.sh scripts run vanilla bazel besides. The one location indifferent to both axes is the user bazelrc, $HOME/.bazelrc, which every bazel process on the runner reads regardless of directory — and which the Aspect CLI also re-injects into the Bazel it spawns.

So a single composite action, .github/actions/aspect-cloud/, writes that file and every job uses it. config.axl keeps only --config=aspect-exec-log, which is genuinely workspace-bound.

The action emits the --build_metadata= keys get_build_metadata_flags derives, plus BUILD_USER, BUILD_HOST and COMMIT_TAG, and runs setup-aspect so aspect is on PATH for the credential helper.

On a pull request it resolves metadata against github.event.pull_request.head.sha rather than HEAD. actions/checkout leaves HEAD on GitHub's synthetic merge commit, so without this every PR build in the UI is titled Merge <sha> into <sha> and attributed to the merge's author. The head commit is not in the shallow clone, so it is fetched at depth 1 when absent, falling back to HEAD if that fails.

The token secret moves

secrets.ASPECT_API_TOKENsecrets.ASPECT_OSS_API_TOKEN, matching aspect-build/rules_lint#1002 and aspect-build/aspect-cli#1461. The org-level ASPECT_API_TOKEN authenticates as a tenant the OSS team cannot see, so uploads succeed while the UI stays empty. No fallback to the old secret — a fallback would silently re-route builds back to the invisible tenant whenever the new secret is missing.

ASPECT_HAS_API_TOKEN carries whether the secret exists, never its value. Three things it guards, each a real failure:

Condition Guards
the secret is non-empty a fork PR gets no secrets, and the credential helper exits non-zero for a host it cannot authenticate — every job on every fork PR would go red
CI dev machines stay untouched
read only in config.axl the token itself stays out of the job env on purpose: setup-aspect keeps it out of GITHUB_ENV so only the short-lived JWT is visible downstream, and an ASPECT_API_TOKEN in the environment outranks that stored credential on every invocation

Before merging

ASPECT_OSS_API_TOKEN has to exist under that name in repository or organization settings. If it does not, secrets.ASPECT_OSS_API_TOKEN resolves to empty, ASPECT_HAS_API_TOKEN becomes "false", and every authenticated job quietly takes the fork-PR path instead of failing loudly.

Test plan

Verified locally:

  • both YAML files parse, and the composite action's shell body passes bash -n
  • bazel info --config=aspect-cloud --announce_rc expands through aspect-commonaspect-cacheaspect-bes with every flag accepted
  • bazel build --config=aspect-exec-log writes .aspect/generated/exec.log.zstd, and git check-ignore confirms the committed .gitignore covers it
  • aspect buildifier --help still loads the edited config.axl; buildifier --mode=check reports no formatting change

The rest is observable only on CI: a run where invocations land in app.aspect.build under the OSS tenant, with the PR's real head SHA rather than the merge commit, is the confirmation.

🤖 Generated with Claude Code

CI runs on stock GitHub Actions runners, not Aspect Workflows runners. The
Aspect CLI wires BES from the environment (ASPECT_WORKFLOWS_BES_BACKEND and
friends, set by a Workflows runner); none of those variables exist here and
nothing passed --remote, so the CLI tasks were uploading nowhere.

.aspect/bazelrc follows the shape `aspect setup bazelrc` generates
(aspect-build/aspect-cli#1442): each flag sits in the --config group named for
the endpoint it presumes, so a deployment with a cache but no BES gets the
cache tuning and none of the BES tuning. --config=aspect-cloud chains
aspect-common, aspect-cache and aspect-bes and adds the endpoints plus the
`aspect` credential helper, scoped per host. Nothing is enabled, so a fresh
clone builds exactly as before. .bazelrc gains the try-import at the top, so
the repo's own lines come after and win.

aspect-exec-log is deliberately not chained from aspect-cloud. Its path is
%workspace%-relative and Bazel will not create the parent directory, so naming
it from the e2e/* and examples/* workspaces — which have no .aspect/generated —
would fail the build. Only the root workspace names it, via config.axl.

One mechanism, every job. This repo has ~20 sibling workspaces under e2e/ and
examples/, and the e2e test.sh scripts run vanilla `bazel`. Neither
.aspect/config.axl nor the root .bazelrc reaches them: both are per-workspace.
The user bazelrc is indifferent to that axis — every `bazel` process on the
runner reads it regardless of directory, and the Aspect CLI re-injects it into
the Bazel it spawns. So a single composite action writes it and every job
family uses it. On a pull request it resolves build metadata against
github.event.pull_request.head.sha, since actions/checkout leaves HEAD on the
synthetic merge commit and every build would otherwise be titled
"Merge <sha> into <sha>".

The token moves to secrets.ASPECT_OSS_API_TOKEN, matching
aspect-build/rules_lint#1002 and aspect-build/aspect-cli#1461: the org-level
ASPECT_API_TOKEN authenticates as a tenant the OSS team cannot see, so uploads
succeed while the UI stays empty. No fallback to the old secret — it would
silently re-route builds back to the invisible tenant. ASPECT_HAS_API_TOKEN
carries whether the secret exists, never its value: a fork PR gets no secrets,
and the credential helper exits non-zero for a host it cannot authenticate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Sep 16, 2026

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Advanced

Run ID: eb807aa0-6162-40bb-9a82-77b6d4ba5c5f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

py_binary startup benchmark

Version Time (ms) vs BCR vs main Build (s)
BCR 1.11.7 141.3/139.5 ±9.8 1.31
main 48.3/48.2 ±1.4 -65.5% 1.64
PR 49.9/49.2 ±3.6 -64.7% +2.1% 1.93

Time = mean/median ±stddev.
Measured with hyperfine --warmup 5 --runs 50 --shell=none on Linux
Gate: PR vs HEAD main median (threshold: 10%, and must exceed the 2×SE noise floor, here 2.3%). BCR is shown only as a historical baseline.
Build time: cold bazel build //:bench with isolated output base, no disk cache; external repos prefetched so network is excluded.

sys.path quality

Version entries sp roots dupes
BCR 1.11.7 6 1 0
main 7 2 0
PR 7 2 0

sys.path quality measured by bench_syspath inside the assembled venv: sys.path entries, distinct site-packages roots, duplicate realpaths. Duplicates indicate symlink redundancy; many roots suggest an inefficient venv layout.

Bazel analysis benchmark

Version Time (ms) vs BCR vs main Targets Actions Configured (ext) Cfg/target Actions/cfg
BCR 2.0.0-alpha.6 1495/1421 ±283 404 20613 7380 (6675) 18.3 2.79
main 1475/1413 ±264 -0.6% 404 18363 7379 (6674) 18.3 2.49
PR 1454/1371 ±314 -3.5% -3.0% 404 18363 7379 (6674) 18.3 2.49

Measured with hyperfine --warmup 1 --runs 10 on Linux
Time = mean/median ±stddev. Cfg = configured targets; (ext) = the count in external repos (the @pypi hub machinery).
Gate: PR vs HEAD main median (threshold: 10%, and must exceed the 2×SE noise floor, here 18.3%). BCR is shown only as a historical baseline.
Command: warm-server bazel build --nobuild //workspace/..., analysis cache discarded each run via a fresh --action_env value; no disk cache.
Workload: 100 py_test targets — 2 test files/package in file generation mode (one venv per test file); a single dep_group.

py_image_layer benchmark

Scenario Version Time (s) Actions vs BCR vs main
analysis BCR 2.0.0-alpha.6 0.84/0.78 ±0.15 747
analysis main 0.81/0.78 ±0.14 668 +0.3%
analysis PR 0.83/0.77 ±0.15 668 -1.3% -1.6%
1p source BCR 2.0.0-alpha.6 0.84/0.81 ±0.07 4
1p source main 0.85/0.82 ±0.08 5 +1.0%
1p source PR 0.82/0.79 ±0.08 5 -2.9% -3.8%
3p wheel BCR 2.0.0-alpha.6 0.96/0.94 ±0.06 5
3p wheel main 1.02/1.00 ±0.06 6 +6.3%
3p wheel PR 1.00/0.98 ±0.06 6 +3.9% -2.2%

Time = mean/median ±stddev.
Measured with hyperfine on Linux, building //workspace:image_layers (10 binaries, ~30-wheel dep pool, grouped first-party/pip/interpreter tier) with isolated output base, no disk cache.
Scenarios: analysis = warm-server bazel build --nobuild, re-analyzed each run via a fresh --action_env value; incrementals run against a built state with warm analysis: source = append to the last package's lib.py, wheel = rewrite click post_install_patches content.
Actions: for Analysis, the total action count behind the image target from aquery deps(...); for incrementals, actions re-executed for the mutation, from a single instrumented run's BEP build metrics (deterministic; per-mnemonic breakdown in the *-actions.json artifacts). Informational only, not gated.
Gate: PR vs HEAD main median per scenario (threshold: 10%, and must exceed the 2×SE noise floor). BCR is shown only as a historical baseline.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant