From 66b42eabc8eb36de01048de3703c69c0c50daada Mon Sep 17 00:00:00 2001 From: Beau Collins Date: Thu, 28 May 2026 10:51:18 -0700 Subject: [PATCH 1/8] Enable Bazel GCS remote cache for CI Add GCS remote cache via Workload Identity Federation to all Bazel CI workflows. Uses a reusable composite action with per-repo config via GitHub repository variables. - Remote cache shared across all jobs and runs - Disk cache for fast local hits on linux-ci jobs - Circuit breaker for graceful fallback on token expiry - Larger runner (Valdi-Larger-Runner) for linux-ci - PRs get read-only cache (no upload) to prevent cache poisoning Co-Authored-By: Claude Opus 4.6 (1M context) --- .bazelrc | 4 +- .github/actions/bazel-cache/action.yml | 79 ++++++++++++++++++++++++++ .github/workflows/bzl-changes.yml | 63 ++++++++++++-------- .github/workflows/publish-npm.yml | 11 ++++ .github/workflows/release-test.yml | 11 ++++ 5 files changed, 141 insertions(+), 27 deletions(-) create mode 100644 .github/actions/bazel-cache/action.yml diff --git a/.bazelrc b/.bazelrc index 9a05b656..6496413e 100644 --- a/.bazelrc +++ b/.bazelrc @@ -98,5 +98,5 @@ build --define=open_source_build=true common --@aspect_rules_ts//ts:skipLibCheck=always -# CI-specific overrides (must be last to win over earlier flags like --disk_cache="") -try-import %workspace%/.bazelrc.ci +# Remote cache (activated in CI via .bazelrc.local) +build:ci --experimental_circuit_breaker_strategy=failure diff --git a/.github/actions/bazel-cache/action.yml b/.github/actions/bazel-cache/action.yml new file mode 100644 index 00000000..73b5a785 --- /dev/null +++ b/.github/actions/bazel-cache/action.yml @@ -0,0 +1,79 @@ +name: 'Configure Bazel Cache' +description: 'Set up GCS remote cache and optional disk cache for Bazel builds' + +inputs: + gcp_project_id: + description: 'GCP project ID' + required: true + workload_identity_provider: + description: 'Full workload identity provider resource name' + required: true + service_account: + description: 'GCP service account email' + required: true + cache_bucket: + description: 'GCS bucket name for remote cache' + required: true + cache_key: + description: 'Cache key prefix for disk cache via actions/cache. Omit to skip disk cache.' + required: false + default: '' + +runs: + using: 'composite' + steps: + - name: Authenticate to Google Cloud + id: auth + uses: google-github-actions/auth@v2 + with: + project_id: ${{ inputs.gcp_project_id }} + workload_identity_provider: ${{ inputs.workload_identity_provider }} + service_account: ${{ inputs.service_account }} + token_format: 'access_token' + + - name: Configure Bazel remote cache + shell: bash + run: | + # Checkout directory config + echo "build --config=ci" >> .bazelrc.local + echo "build --remote_cache=https://storage.googleapis.com/${{ inputs.cache_bucket }}" >> .bazelrc.local + echo "build \"--remote_header=Authorization=Bearer ${{ steps.auth.outputs.access_token }}\"" >> .bazelrc.local + # Only upload cache results on push (trusted) events, not pull requests + if [ "${{ github.event_name }}" = "push" ] || [ "${{ github.event_name }}" = "workflow_dispatch" ] || [ "${{ github.event_name }}" = "release" ]; then + echo "build --remote_upload_local_results=true" >> .bazelrc.local + else + echo "build --remote_upload_local_results=false" >> .bazelrc.local + fi + # User-level config for bootstrapped projects (e.g. /tmp/valdi_app) + # Use > for first write to prevent unbounded growth on persistent runners + echo "build --remote_cache=https://storage.googleapis.com/${{ inputs.cache_bucket }}" > ~/.bazelrc + echo "build \"--remote_header=Authorization=Bearer ${{ steps.auth.outputs.access_token }}\"" >> ~/.bazelrc + echo "build --experimental_circuit_breaker_strategy=failure" >> ~/.bazelrc + if [ "${{ github.event_name }}" = "push" ] || [ "${{ github.event_name }}" = "workflow_dispatch" ] || [ "${{ github.event_name }}" = "release" ]; then + echo "build --remote_upload_local_results=true" >> ~/.bazelrc + else + echo "build --remote_upload_local_results=false" >> ~/.bazelrc + fi + + - name: Configure disk cache + if: inputs.cache_key != '' + shell: bash + run: | + echo "build --disk_cache=$HOME/.cache/bazel/disk" >> .bazelrc.local + echo "build --repository_cache=$HOME/.cache/bazel/repo" >> .bazelrc.local + echo "build --disk_cache=$HOME/.cache/bazel/disk" >> ~/.bazelrc + echo "build --repository_cache=$HOME/.cache/bazel/repo" >> ~/.bazelrc + + - name: Mount Bazel cache + if: inputs.cache_key != '' + uses: actions/cache@v4 + continue-on-error: true + with: + path: | + ~/.cache/bazel/disk + ~/.cache/bazel/repo + ~/.cache/bazelisk + key: bazel-${{ runner.os }}-${{ inputs.cache_key }}-${{ hashFiles('MODULE.bazel', '**/*.bzl') }}-${{ github.run_id }} + restore-keys: | + bazel-${{ runner.os }}-${{ inputs.cache_key }}-${{ hashFiles('MODULE.bazel', '**/*.bzl') }}- + bazel-${{ runner.os }}-${{ inputs.cache_key }}- diff --git a/.github/workflows/bzl-changes.yml b/.github/workflows/bzl-changes.yml index 1e544596..e6f51758 100644 --- a/.github/workflows/bzl-changes.yml +++ b/.github/workflows/bzl-changes.yml @@ -48,6 +48,9 @@ jobs: smoke-test: name: Valdi Smoke Tests runs-on: macos-latest + permissions: + contents: read + id-token: write steps: - name: Checkout code @@ -55,6 +58,15 @@ jobs: with: lfs: true + - name: Configure Bazel cache + uses: ./.github/actions/bazel-cache + with: + gcp_project_id: ${{ vars.GCP_PROJECT_ID }} + workload_identity_provider: ${{ vars.WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ vars.GCP_SERVICE_ACCOUNT }} + cache_bucket: ${{ vars.BAZEL_CACHE_BUCKET }} + cache_key: smoke-test + - name: Setup Node.js uses: actions/setup-node@v4 with: @@ -101,7 +113,10 @@ jobs: linux-ci: name: "Linux: ${{ matrix.name }}" - runs-on: ubuntu-latest + runs-on: Valdi-Larger-Runner + permissions: + contents: read + id-token: write strategy: fail-fast: false matrix: @@ -123,32 +138,18 @@ jobs: distribution: 'zulu' java-version: '17' + - name: Configure Bazel cache + uses: ./.github/actions/bazel-cache + with: + gcp_project_id: ${{ vars.GCP_PROJECT_ID }} + workload_identity_provider: ${{ vars.WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ vars.GCP_SERVICE_ACCOUNT }} + cache_bucket: ${{ vars.BAZEL_CACHE_BUCKET }} + cache_key: ${{ matrix.task }} + - name: Setup Linux environment run: source ./tools/ci/setup_linux_env.sh - - name: Mount Bazel cache - uses: actions/cache@v4 - timeout-minutes: 5 - continue-on-error: true - with: - path: | - ~/.cache/bazel/disk - ~/.cache/bazel/repo - ~/.cache/bazelisk - key: bazel-${{ runner.os }}-${{ matrix.task }}-${{ hashFiles('MODULE.bazel', '**/*.bzl') }}-${{ github.run_id }} - restore-keys: | - bazel-${{ runner.os }}-${{ matrix.task }}-${{ hashFiles('MODULE.bazel', '**/*.bzl') }}- - bazel-${{ runner.os }}-${{ matrix.task }}- - - - name: Configure Bazel CI cache - run: | - # Write to .bazelrc.ci which is imported LAST in .bazelrc, so these - # flags override the default --disk_cache="" for developer machines. - # Previously written to .bazelrc.local which is imported at the TOP - # of .bazelrc — the later --disk_cache="" silently overrode it. - echo "build --disk_cache=$HOME/.cache/bazel/disk" >> .bazelrc.ci - echo "build --repository_cache=$HOME/.cache/bazel/repo" >> .bazelrc.ci - - name: Setup environment and install Valdi CLI if: matrix.task == 'build-export' run: | @@ -178,6 +179,9 @@ jobs: snapshot-tests: name: Snapshot Tests runs-on: macos-latest + permissions: + contents: read + id-token: write steps: - name: Checkout code @@ -185,6 +189,15 @@ jobs: with: lfs: true + - name: Configure Bazel cache + uses: ./.github/actions/bazel-cache + with: + gcp_project_id: ${{ vars.GCP_PROJECT_ID }} + workload_identity_provider: ${{ vars.WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ vars.GCP_SERVICE_ACCOUNT }} + cache_bucket: ${{ vars.BAZEL_CACHE_BUCKET }} + cache_key: snapshot-tests + - name: Setup Node.js uses: actions/setup-node@v4 with: @@ -222,4 +235,4 @@ jobs: with: workflow_name: "Bazel & CI Test Results" success_message: "**All Bazel configuration and CI tests passed!** ✨\n\nThe build system and core tooling are working correctly." - additional_info: "🚀 _Bazel disk cache is enabled - builds with warm cache will be faster!_" + additional_info: "🚀 _Bazel remote cache is now enabled - future builds will be faster!_" diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml index 3c65d3aa..c6c77faf 100644 --- a/.github/workflows/publish-npm.yml +++ b/.github/workflows/publish-npm.yml @@ -74,12 +74,23 @@ jobs: needs: detect-changes if: needs.detect-changes.outputs.cli == 'true' runs-on: macos-latest + permissions: + contents: read + id-token: write steps: - name: Checkout code uses: actions/checkout@v4 with: lfs: true + - name: Configure Bazel cache + uses: ./.github/actions/bazel-cache + with: + gcp_project_id: ${{ vars.GCP_PROJECT_ID }} + workload_identity_provider: ${{ vars.WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ vars.GCP_SERVICE_ACCOUNT }} + cache_bucket: ${{ vars.BAZEL_CACHE_BUCKET }} + - name: Setup Node.js uses: actions/setup-node@v4 with: diff --git a/.github/workflows/release-test.yml b/.github/workflows/release-test.yml index 40f9821e..7d88ec31 100644 --- a/.github/workflows/release-test.yml +++ b/.github/workflows/release-test.yml @@ -22,6 +22,9 @@ jobs: release-test: name: Bootstrap from main (bleeding edge), build & test runs-on: macos-latest + permissions: + contents: read + id-token: write steps: - name: Checkout code @@ -29,6 +32,14 @@ jobs: with: lfs: true + - name: Configure Bazel cache + uses: ./.github/actions/bazel-cache + with: + gcp_project_id: ${{ vars.GCP_PROJECT_ID }} + workload_identity_provider: ${{ vars.WORKLOAD_IDENTITY_PROVIDER }} + service_account: ${{ vars.GCP_SERVICE_ACCOUNT }} + cache_bucket: ${{ vars.BAZEL_CACHE_BUCKET }} + - name: Setup Node.js uses: actions/setup-node@v4 with: From 790c8d603aa08f120aa53da733fab69983e61e23 Mon Sep 17 00:00:00 2001 From: Beau Collins Date: Thu, 28 May 2026 11:02:07 -0700 Subject: [PATCH 2/8] Fix remotedebug-ios-webkit-adapter: remove missing deps Remove @types/istanbul-lib-coverage and gulp-typescript from deps (no longer available), add types: [] to tsconfig to prevent implicit type resolution failures. Cherry-picked from Snapchat/mobile#99614 Co-Authored-By: Claude Opus 4.6 (1M context) --- compiler/companion/remotedebug-ios-webkit-adapter/BUILD.bazel | 2 -- .../companion/remotedebug-ios-webkit-adapter/tsconfig.json | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/compiler/companion/remotedebug-ios-webkit-adapter/BUILD.bazel b/compiler/companion/remotedebug-ios-webkit-adapter/BUILD.bazel index a7aaf3c0..d774340b 100644 --- a/compiler/companion/remotedebug-ios-webkit-adapter/BUILD.bazel +++ b/compiler/companion/remotedebug-ios-webkit-adapter/BUILD.bazel @@ -38,12 +38,10 @@ ts_project( deps = [ ":node_modules/@types/debug", ":node_modules/@types/express", - ":node_modules/@types/istanbul-lib-coverage", ":node_modules/@types/optimist", ":node_modules/@types/request", ":node_modules/@types/which", ":node_modules/@types/ws", - ":node_modules/gulp-typescript", ":node_modules/optimist", ], ) diff --git a/compiler/companion/remotedebug-ios-webkit-adapter/tsconfig.json b/compiler/companion/remotedebug-ios-webkit-adapter/tsconfig.json index 7f1f733e..1fc9104a 100644 --- a/compiler/companion/remotedebug-ios-webkit-adapter/tsconfig.json +++ b/compiler/companion/remotedebug-ios-webkit-adapter/tsconfig.json @@ -6,7 +6,8 @@ "composite": true, "declaration": true, "outDir": "src", - "rootDir": "src" + "rootDir": "src", + "types": [] }, "include": ["src/**/*.ts", "package.json"], "exclude": ["src/**/*.json", "node_modules"] From 7722e572896b90b185e3db86125b22282327da76 Mon Sep 17 00:00:00 2001 From: Beau Collins Date: Thu, 28 May 2026 12:12:05 -0700 Subject: [PATCH 3/8] Temporarily enable cache uploads on PRs for initial cache seeding TODO: Restore PR read-only after cache is warm Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/actions/bazel-cache/action.yml | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/.github/actions/bazel-cache/action.yml b/.github/actions/bazel-cache/action.yml index 73b5a785..2b367613 100644 --- a/.github/actions/bazel-cache/action.yml +++ b/.github/actions/bazel-cache/action.yml @@ -38,22 +38,15 @@ runs: echo "build --config=ci" >> .bazelrc.local echo "build --remote_cache=https://storage.googleapis.com/${{ inputs.cache_bucket }}" >> .bazelrc.local echo "build \"--remote_header=Authorization=Bearer ${{ steps.auth.outputs.access_token }}\"" >> .bazelrc.local + # TODO: Restore PR read-only after initial cache seeding # Only upload cache results on push (trusted) events, not pull requests - if [ "${{ github.event_name }}" = "push" ] || [ "${{ github.event_name }}" = "workflow_dispatch" ] || [ "${{ github.event_name }}" = "release" ]; then - echo "build --remote_upload_local_results=true" >> .bazelrc.local - else - echo "build --remote_upload_local_results=false" >> .bazelrc.local - fi + echo "build --remote_upload_local_results=true" >> .bazelrc.local # User-level config for bootstrapped projects (e.g. /tmp/valdi_app) # Use > for first write to prevent unbounded growth on persistent runners echo "build --remote_cache=https://storage.googleapis.com/${{ inputs.cache_bucket }}" > ~/.bazelrc echo "build \"--remote_header=Authorization=Bearer ${{ steps.auth.outputs.access_token }}\"" >> ~/.bazelrc echo "build --experimental_circuit_breaker_strategy=failure" >> ~/.bazelrc - if [ "${{ github.event_name }}" = "push" ] || [ "${{ github.event_name }}" = "workflow_dispatch" ] || [ "${{ github.event_name }}" = "release" ]; then - echo "build --remote_upload_local_results=true" >> ~/.bazelrc - else - echo "build --remote_upload_local_results=false" >> ~/.bazelrc - fi + echo "build --remote_upload_local_results=true" >> ~/.bazelrc - name: Configure disk cache if: inputs.cache_key != '' From c81d9c74002b9ea488cbb2a45db9ae029ffb6c34 Mon Sep 17 00:00:00 2001 From: Beau Collins Date: Thu, 28 May 2026 15:23:31 -0700 Subject: [PATCH 4/8] Sync cache config: pruning, race condition fix, actions/cache skip, fork hardening Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/actions/bazel-cache/action.yml | 56 +++++++++++++++++++------- .github/workflows/bzl-changes.yml | 2 +- 2 files changed, 43 insertions(+), 15 deletions(-) diff --git a/.github/actions/bazel-cache/action.yml b/.github/actions/bazel-cache/action.yml index 2b367613..1b94cfab 100644 --- a/.github/actions/bazel-cache/action.yml +++ b/.github/actions/bazel-cache/action.yml @@ -24,6 +24,7 @@ runs: steps: - name: Authenticate to Google Cloud id: auth + continue-on-error: true uses: google-github-actions/auth@v2 with: project_id: ${{ inputs.gcp_project_id }} @@ -34,31 +35,58 @@ runs: - name: Configure Bazel remote cache shell: bash run: | - # Checkout directory config + # Always write config=ci for non-remote flags (circuit breaker, etc.) echo "build --config=ci" >> .bazelrc.local + + # Write user-level bazelrc to a job-specific file to avoid race conditions + # on persistent runners where multiple matrix jobs share ~/.bazelrc + JOB_BAZELRC="${GITHUB_WORKSPACE}/.bazelrc.user-cache" + > "$JOB_BAZELRC" + + if [ "${{ steps.auth.outcome }}" != "success" ]; then + echo "Auth failed (expected for fork PRs). Skipping remote cache." + # Still set up the user-level bazelrc symlink (empty) for bootstrapped projects + ln -sf "$JOB_BAZELRC" ~/.bazelrc + exit 0 + fi + + # Checkout directory config echo "build --remote_cache=https://storage.googleapis.com/${{ inputs.cache_bucket }}" >> .bazelrc.local echo "build \"--remote_header=Authorization=Bearer ${{ steps.auth.outputs.access_token }}\"" >> .bazelrc.local - # TODO: Restore PR read-only after initial cache seeding # Only upload cache results on push (trusted) events, not pull requests - echo "build --remote_upload_local_results=true" >> .bazelrc.local + if [ "${{ github.event_name }}" = "push" ] || [ "${{ github.event_name }}" = "workflow_dispatch" ] || [ "${{ github.event_name }}" = "release" ]; then + echo "build --remote_upload_local_results=true" >> .bazelrc.local + else + echo "build --remote_upload_local_results=false" >> .bazelrc.local + fi + # User-level config for bootstrapped projects (e.g. /tmp/valdi_app) - # Use > for first write to prevent unbounded growth on persistent runners - echo "build --remote_cache=https://storage.googleapis.com/${{ inputs.cache_bucket }}" > ~/.bazelrc - echo "build \"--remote_header=Authorization=Bearer ${{ steps.auth.outputs.access_token }}\"" >> ~/.bazelrc - echo "build --experimental_circuit_breaker_strategy=failure" >> ~/.bazelrc - echo "build --remote_upload_local_results=true" >> ~/.bazelrc + echo "build --remote_cache=https://storage.googleapis.com/${{ inputs.cache_bucket }}" >> "$JOB_BAZELRC" + echo "build \"--remote_header=Authorization=Bearer ${{ steps.auth.outputs.access_token }}\"" >> "$JOB_BAZELRC" + echo "build --experimental_circuit_breaker_strategy=failure" >> "$JOB_BAZELRC" + if [ "${{ github.event_name }}" = "push" ] || [ "${{ github.event_name }}" = "workflow_dispatch" ] || [ "${{ github.event_name }}" = "release" ]; then + echo "build --remote_upload_local_results=true" >> "$JOB_BAZELRC" + else + echo "build --remote_upload_local_results=false" >> "$JOB_BAZELRC" + fi + # Symlink so Bazel picks it up as ~/.bazelrc + ln -sf "$JOB_BAZELRC" ~/.bazelrc - - name: Configure disk cache + - name: Prune and configure disk cache if: inputs.cache_key != '' shell: bash run: | - echo "build --disk_cache=$HOME/.cache/bazel/disk" >> .bazelrc.local - echo "build --repository_cache=$HOME/.cache/bazel/repo" >> .bazelrc.local - echo "build --disk_cache=$HOME/.cache/bazel/disk" >> ~/.bazelrc - echo "build --repository_cache=$HOME/.cache/bazel/repo" >> ~/.bazelrc + # Prune old cache files to prevent unbounded growth on persistent runners + find "$HOME/.cache/bazel/disk" -type f -atime +7 -delete 2>/dev/null || true + find "$HOME/.cache/bazel/repo" -type f -atime +7 -delete 2>/dev/null || true + + echo "build:ci --disk_cache=$HOME/.cache/bazel/disk" >> .bazelrc.local + echo "build:ci --repository_cache=$HOME/.cache/bazel/repo" >> .bazelrc.local + echo "build --disk_cache=$HOME/.cache/bazel/disk" >> "${GITHUB_WORKSPACE}/.bazelrc.user-cache" + echo "build --repository_cache=$HOME/.cache/bazel/repo" >> "${GITHUB_WORKSPACE}/.bazelrc.user-cache" - name: Mount Bazel cache - if: inputs.cache_key != '' + if: inputs.cache_key != '' && runner.environment == 'github-hosted' uses: actions/cache@v4 continue-on-error: true with: diff --git a/.github/workflows/bzl-changes.yml b/.github/workflows/bzl-changes.yml index e6f51758..96dae446 100644 --- a/.github/workflows/bzl-changes.yml +++ b/.github/workflows/bzl-changes.yml @@ -113,7 +113,7 @@ jobs: linux-ci: name: "Linux: ${{ matrix.name }}" - runs-on: Valdi-Larger-Runner + runs-on: ${{ github.event_name == 'push' && 'Valdi-Larger-Runner' || 'ubuntu-latest' }} permissions: contents: read id-token: write From 8f6fb8756be0dea53f9d48e794a00c4a604d5c67 Mon Sep 17 00:00:00 2001 From: Beau Collins Date: Thu, 28 May 2026 15:48:05 -0700 Subject: [PATCH 5/8] Fix fork PR compatibility: workflow-level permissions, remove symlinks Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/actions/bazel-cache/action.yml | 27 +++++++++++--------------- .github/workflows/bzl-changes.yml | 17 ++++++++-------- .github/workflows/publish-npm.yml | 7 ++++--- .github/workflows/release-test.yml | 7 ++++--- 4 files changed, 27 insertions(+), 31 deletions(-) diff --git a/.github/actions/bazel-cache/action.yml b/.github/actions/bazel-cache/action.yml index 1b94cfab..9be6d415 100644 --- a/.github/actions/bazel-cache/action.yml +++ b/.github/actions/bazel-cache/action.yml @@ -38,15 +38,12 @@ runs: # Always write config=ci for non-remote flags (circuit breaker, etc.) echo "build --config=ci" >> .bazelrc.local - # Write user-level bazelrc to a job-specific file to avoid race conditions - # on persistent runners where multiple matrix jobs share ~/.bazelrc - JOB_BAZELRC="${GITHUB_WORKSPACE}/.bazelrc.user-cache" - > "$JOB_BAZELRC" + # Write user-level bazelrc directly (not symlinked) for bootstrapped projects + # Content is identical across same-repo matrix jobs so concurrent writes are safe + > ~/.bazelrc if [ "${{ steps.auth.outcome }}" != "success" ]; then - echo "Auth failed (expected for fork PRs). Skipping remote cache." - # Still set up the user-level bazelrc symlink (empty) for bootstrapped projects - ln -sf "$JOB_BAZELRC" ~/.bazelrc + echo "Auth skipped (expected for fork PRs). Building without remote cache." exit 0 fi @@ -61,16 +58,14 @@ runs: fi # User-level config for bootstrapped projects (e.g. /tmp/valdi_app) - echo "build --remote_cache=https://storage.googleapis.com/${{ inputs.cache_bucket }}" >> "$JOB_BAZELRC" - echo "build \"--remote_header=Authorization=Bearer ${{ steps.auth.outputs.access_token }}\"" >> "$JOB_BAZELRC" - echo "build --experimental_circuit_breaker_strategy=failure" >> "$JOB_BAZELRC" + echo "build --remote_cache=https://storage.googleapis.com/${{ inputs.cache_bucket }}" > ~/.bazelrc + echo "build \"--remote_header=Authorization=Bearer ${{ steps.auth.outputs.access_token }}\"" >> ~/.bazelrc + echo "build --experimental_circuit_breaker_strategy=failure" >> ~/.bazelrc if [ "${{ github.event_name }}" = "push" ] || [ "${{ github.event_name }}" = "workflow_dispatch" ] || [ "${{ github.event_name }}" = "release" ]; then - echo "build --remote_upload_local_results=true" >> "$JOB_BAZELRC" + echo "build --remote_upload_local_results=true" >> ~/.bazelrc else - echo "build --remote_upload_local_results=false" >> "$JOB_BAZELRC" + echo "build --remote_upload_local_results=false" >> ~/.bazelrc fi - # Symlink so Bazel picks it up as ~/.bazelrc - ln -sf "$JOB_BAZELRC" ~/.bazelrc - name: Prune and configure disk cache if: inputs.cache_key != '' @@ -82,8 +77,8 @@ runs: echo "build:ci --disk_cache=$HOME/.cache/bazel/disk" >> .bazelrc.local echo "build:ci --repository_cache=$HOME/.cache/bazel/repo" >> .bazelrc.local - echo "build --disk_cache=$HOME/.cache/bazel/disk" >> "${GITHUB_WORKSPACE}/.bazelrc.user-cache" - echo "build --repository_cache=$HOME/.cache/bazel/repo" >> "${GITHUB_WORKSPACE}/.bazelrc.user-cache" + echo "build --disk_cache=$HOME/.cache/bazel/disk" >> ~/.bazelrc + echo "build --repository_cache=$HOME/.cache/bazel/repo" >> ~/.bazelrc - name: Mount Bazel cache if: inputs.cache_key != '' && runner.environment == 'github-hosted' diff --git a/.github/workflows/bzl-changes.yml b/.github/workflows/bzl-changes.yml index 96dae446..e0b29e5f 100644 --- a/.github/workflows/bzl-changes.yml +++ b/.github/workflows/bzl-changes.yml @@ -44,13 +44,17 @@ on: - 'apps/snapshot_tests/**' - 'snap_drawing/**' +# Workflow-level permissions: id-token: write needed for GCP WIF auth on push events. +# For fork PRs, GitHub automatically downgrades to read-only — the auth step +# has continue-on-error so builds proceed without remote cache. +permissions: + contents: read + id-token: write + jobs: smoke-test: name: Valdi Smoke Tests runs-on: macos-latest - permissions: - contents: read - id-token: write steps: - name: Checkout code @@ -114,9 +118,6 @@ jobs: linux-ci: name: "Linux: ${{ matrix.name }}" runs-on: ${{ github.event_name == 'push' && 'Valdi-Larger-Runner' || 'ubuntu-latest' }} - permissions: - contents: read - id-token: write strategy: fail-fast: false matrix: @@ -179,9 +180,6 @@ jobs: snapshot-tests: name: Snapshot Tests runs-on: macos-latest - permissions: - contents: read - id-token: write steps: - name: Checkout code @@ -231,6 +229,7 @@ jobs: if: always() uses: ./.github/workflows/comment-test-results.yml permissions: + contents: read pull-requests: write with: workflow_name: "Bazel & CI Test Results" diff --git a/.github/workflows/publish-npm.yml b/.github/workflows/publish-npm.yml index c6c77faf..e82b99e3 100644 --- a/.github/workflows/publish-npm.yml +++ b/.github/workflows/publish-npm.yml @@ -9,6 +9,10 @@ on: - 'npm_modules/*/package.json' workflow_dispatch: +permissions: + contents: read + id-token: write + jobs: detect-changes: runs-on: ubuntu-latest @@ -74,9 +78,6 @@ jobs: needs: detect-changes if: needs.detect-changes.outputs.cli == 'true' runs-on: macos-latest - permissions: - contents: read - id-token: write steps: - name: Checkout code uses: actions/checkout@v4 diff --git a/.github/workflows/release-test.yml b/.github/workflows/release-test.yml index 7d88ec31..1a847cdb 100644 --- a/.github/workflows/release-test.yml +++ b/.github/workflows/release-test.yml @@ -18,13 +18,14 @@ on: - 'tools/ci/release_test.sh' - '.github/workflows/release-test.yml' +permissions: + contents: read + id-token: write + jobs: release-test: name: Bootstrap from main (bleeding edge), build & test runs-on: macos-latest - permissions: - contents: read - id-token: write steps: - name: Checkout code From a2e080d28a1ec09f90c110740d27b957060ebbb9 Mon Sep 17 00:00:00 2001 From: Beau Collins Date: Thu, 28 May 2026 21:51:49 -0700 Subject: [PATCH 6/8] Fix ~/.bazelrc race: use job-specific file with try-import Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/actions/bazel-cache/action.yml | 31 +++++++++++++++++--------- 1 file changed, 20 insertions(+), 11 deletions(-) diff --git a/.github/actions/bazel-cache/action.yml b/.github/actions/bazel-cache/action.yml index 9be6d415..c4e04b52 100644 --- a/.github/actions/bazel-cache/action.yml +++ b/.github/actions/bazel-cache/action.yml @@ -38,9 +38,12 @@ runs: # Always write config=ci for non-remote flags (circuit breaker, etc.) echo "build --config=ci" >> .bazelrc.local - # Write user-level bazelrc directly (not symlinked) for bootstrapped projects - # Content is identical across same-repo matrix jobs so concurrent writes are safe - > ~/.bazelrc + # Write cache config to a job-specific file. Bootstrapped projects + # (e.g. /tmp/valdi_app) import this via BAZEL_CACHE_RC env var, + # avoiding races on ~/.bazelrc when concurrent jobs share a runner. + CACHE_RC="/tmp/bazelrc-cache-${GITHUB_RUN_ID}-${GITHUB_JOB}" + > "$CACHE_RC" + echo "BAZEL_CACHE_RC=$CACHE_RC" >> "$GITHUB_ENV" if [ "${{ steps.auth.outcome }}" != "success" ]; then echo "Auth skipped (expected for fork PRs). Building without remote cache." @@ -57,14 +60,18 @@ runs: echo "build --remote_upload_local_results=false" >> .bazelrc.local fi - # User-level config for bootstrapped projects (e.g. /tmp/valdi_app) - echo "build --remote_cache=https://storage.googleapis.com/${{ inputs.cache_bucket }}" > ~/.bazelrc - echo "build \"--remote_header=Authorization=Bearer ${{ steps.auth.outputs.access_token }}\"" >> ~/.bazelrc - echo "build --experimental_circuit_breaker_strategy=failure" >> ~/.bazelrc + # Point ~/.bazelrc to the job-specific file via try-import. + # Single atomic write — no truncation window for concurrent jobs. + echo "try-import $CACHE_RC" > ~/.bazelrc + + # Cache config for bootstrapped projects (discovered via try-import above) + echo "build --remote_cache=https://storage.googleapis.com/${{ inputs.cache_bucket }}" >> "$CACHE_RC" + echo "build \"--remote_header=Authorization=Bearer ${{ steps.auth.outputs.access_token }}\"" >> "$CACHE_RC" + echo "build --experimental_circuit_breaker_strategy=failure" >> "$CACHE_RC" if [ "${{ github.event_name }}" = "push" ] || [ "${{ github.event_name }}" = "workflow_dispatch" ] || [ "${{ github.event_name }}" = "release" ]; then - echo "build --remote_upload_local_results=true" >> ~/.bazelrc + echo "build --remote_upload_local_results=true" >> "$CACHE_RC" else - echo "build --remote_upload_local_results=false" >> ~/.bazelrc + echo "build --remote_upload_local_results=false" >> "$CACHE_RC" fi - name: Prune and configure disk cache @@ -77,8 +84,10 @@ runs: echo "build:ci --disk_cache=$HOME/.cache/bazel/disk" >> .bazelrc.local echo "build:ci --repository_cache=$HOME/.cache/bazel/repo" >> .bazelrc.local - echo "build --disk_cache=$HOME/.cache/bazel/disk" >> ~/.bazelrc - echo "build --repository_cache=$HOME/.cache/bazel/repo" >> ~/.bazelrc + if [ -n "$BAZEL_CACHE_RC" ]; then + echo "build --disk_cache=$HOME/.cache/bazel/disk" >> "$BAZEL_CACHE_RC" + echo "build --repository_cache=$HOME/.cache/bazel/repo" >> "$BAZEL_CACHE_RC" + fi - name: Mount Bazel cache if: inputs.cache_key != '' && runner.environment == 'github-hosted' From 28e4752d30cd782f949e5b2a408f852f33fb5d12 Mon Sep 17 00:00:00 2001 From: Beau Collins Date: Fri, 29 May 2026 08:04:31 -0700 Subject: [PATCH 7/8] =?UTF-8?q?Revert=20to=20ubuntu-latest=20=E2=80=94=20s?= =?UTF-8?q?elf-hosted=20runners=20unavailable?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/workflows/bzl-changes.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/bzl-changes.yml b/.github/workflows/bzl-changes.yml index e0b29e5f..aabc4513 100644 --- a/.github/workflows/bzl-changes.yml +++ b/.github/workflows/bzl-changes.yml @@ -117,7 +117,7 @@ jobs: linux-ci: name: "Linux: ${{ matrix.name }}" - runs-on: ${{ github.event_name == 'push' && 'Valdi-Larger-Runner' || 'ubuntu-latest' }} + runs-on: ubuntu-latest strategy: fail-fast: false matrix: From 5049f9b47ce6668c282217158fcd2a681174c7f3 Mon Sep 17 00:00:00 2001 From: Beau Collins Date: Fri, 29 May 2026 08:07:52 -0700 Subject: [PATCH 8/8] Fix shell injection: use env vars instead of direct interpolation Co-Authored-By: Claude Opus 4.6 (1M context) --- .github/actions/bazel-cache/action.yml | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/.github/actions/bazel-cache/action.yml b/.github/actions/bazel-cache/action.yml index c4e04b52..fe2f4db1 100644 --- a/.github/actions/bazel-cache/action.yml +++ b/.github/actions/bazel-cache/action.yml @@ -34,27 +34,32 @@ runs: - name: Configure Bazel remote cache shell: bash + env: + AUTH_OUTCOME: ${{ steps.auth.outcome }} + CACHE_BUCKET: ${{ inputs.cache_bucket }} + ACCESS_TOKEN: ${{ steps.auth.outputs.access_token }} + EVENT_NAME: ${{ github.event_name }} run: | # Always write config=ci for non-remote flags (circuit breaker, etc.) echo "build --config=ci" >> .bazelrc.local # Write cache config to a job-specific file. Bootstrapped projects - # (e.g. /tmp/valdi_app) import this via BAZEL_CACHE_RC env var, - # avoiding races on ~/.bazelrc when concurrent jobs share a runner. + # (e.g. /tmp/valdi_app) import this via try-import in ~/.bazelrc, + # avoiding races when concurrent jobs share a runner. CACHE_RC="/tmp/bazelrc-cache-${GITHUB_RUN_ID}-${GITHUB_JOB}" > "$CACHE_RC" echo "BAZEL_CACHE_RC=$CACHE_RC" >> "$GITHUB_ENV" - if [ "${{ steps.auth.outcome }}" != "success" ]; then + if [ "$AUTH_OUTCOME" != "success" ]; then echo "Auth skipped (expected for fork PRs). Building without remote cache." exit 0 fi # Checkout directory config - echo "build --remote_cache=https://storage.googleapis.com/${{ inputs.cache_bucket }}" >> .bazelrc.local - echo "build \"--remote_header=Authorization=Bearer ${{ steps.auth.outputs.access_token }}\"" >> .bazelrc.local + echo "build --remote_cache=https://storage.googleapis.com/$CACHE_BUCKET" >> .bazelrc.local + echo "build \"--remote_header=Authorization=Bearer $ACCESS_TOKEN\"" >> .bazelrc.local # Only upload cache results on push (trusted) events, not pull requests - if [ "${{ github.event_name }}" = "push" ] || [ "${{ github.event_name }}" = "workflow_dispatch" ] || [ "${{ github.event_name }}" = "release" ]; then + if [ "$EVENT_NAME" = "push" ] || [ "$EVENT_NAME" = "workflow_dispatch" ] || [ "$EVENT_NAME" = "release" ]; then echo "build --remote_upload_local_results=true" >> .bazelrc.local else echo "build --remote_upload_local_results=false" >> .bazelrc.local @@ -65,10 +70,10 @@ runs: echo "try-import $CACHE_RC" > ~/.bazelrc # Cache config for bootstrapped projects (discovered via try-import above) - echo "build --remote_cache=https://storage.googleapis.com/${{ inputs.cache_bucket }}" >> "$CACHE_RC" - echo "build \"--remote_header=Authorization=Bearer ${{ steps.auth.outputs.access_token }}\"" >> "$CACHE_RC" + echo "build --remote_cache=https://storage.googleapis.com/$CACHE_BUCKET" >> "$CACHE_RC" + echo "build \"--remote_header=Authorization=Bearer $ACCESS_TOKEN\"" >> "$CACHE_RC" echo "build --experimental_circuit_breaker_strategy=failure" >> "$CACHE_RC" - if [ "${{ github.event_name }}" = "push" ] || [ "${{ github.event_name }}" = "workflow_dispatch" ] || [ "${{ github.event_name }}" = "release" ]; then + if [ "$EVENT_NAME" = "push" ] || [ "$EVENT_NAME" = "workflow_dispatch" ] || [ "$EVENT_NAME" = "release" ]; then echo "build --remote_upload_local_results=true" >> "$CACHE_RC" else echo "build --remote_upload_local_results=false" >> "$CACHE_RC"