build(release): use BuildBuddy RBE for release artifact builds (#4293)

Speed up release deliverable and wheel builds and stream invocation
results to BuildBuddy BES.

Configure the local setup-bazel action to enable `buildbuddy_presubmit`
on pull requests and `buildbuddy_postsubmit` on postsubmit runs when a
BuildBuddy API key is provided, writing the key directly to
`user.bazelrc` via `github-script` to keep it out of environment
variables and process tables. Pass the CAS-only key on pull requests and
the read+write key on postsubmit runs.

For now, only use RBE for linux builds. While BuildBuddy supports
Mac/Windows, something with our configuration is causing them to fail.
diff --git a/.github/actions/setup-bazel/action.yaml b/.github/actions/setup-bazel/action.yaml
index 60aa1d1..2938ca3 100644
--- a/.github/actions/setup-bazel/action.yaml
+++ b/.github/actions/setup-bazel/action.yaml
@@ -1,6 +1,16 @@
 name: "Setup Bazel"
 description: "Configure Bazel with caching for GitHub Actions workflows"
 
+inputs:
+  buildbuddy_api_key:
+    description: "BuildBuddy API key to use"
+    required: false
+    default: ""
+  buildbuddy_config:
+    description: "BuildBuddy config to enable when buildbuddy_api_key is set"
+    required: false
+    default: ${{ github.event_name == 'pull_request' && 'buildbuddy_presubmit' || 'buildbuddy_postsubmit' }}
+
 runs:
   using: "composite"
   steps:
@@ -26,6 +36,8 @@
           # pyrefly toolchain or type-check targets before running them.
           build --output_groups=-pyrefly
           build --//python/config_settings:py_pbs_distribution=install_only_stripped
+          # BuildBuddy RBE workers are Linux x86_64; only enable on Linux.
+          ${{ runner.os == 'Linux' && inputs.buildbuddy_api_key && format('common --config={0}', inputs.buildbuddy_config) || '' }}
 
     # setup-bazel only configures Bazelisk and restores caches; the Bazel
     # binary itself is downloaded by Bazelisk on first invocation. Because
@@ -34,3 +46,17 @@
     - name: Download Bazel
       shell: bash
       run: "${GITHUB_ACTION_PATH}/download_bazel.sh"
+
+    # Use github-script to write the key directly to user.bazelrc without
+    # passing it via environment variables or CLI args, keeping it out of
+    # process tables and child environments.
+    - name: Write secret to user.bazelrc
+      if: ${{ runner.os == 'Linux' && inputs.buildbuddy_api_key != '' }}
+      uses: actions/github-script@v7
+      with:
+        script: |
+          const fs = require('fs');
+          fs.appendFileSync(
+            'user.bazelrc',
+            `common:${{ inputs.buildbuddy_config }} --remote_header=x-buildbuddy-api-key=${{ inputs.buildbuddy_api_key }}\n`,
+          );
diff --git a/.github/workflows/release_build_artifacts.yaml b/.github/workflows/release_build_artifacts.yaml
index 0c254ef..ca2cdcd 100644
--- a/.github/workflows/release_build_artifacts.yaml
+++ b/.github/workflows/release_build_artifacts.yaml
@@ -82,6 +82,12 @@
 
       - name: Setup Bazel
         uses: ./.github/actions/setup-bazel
+        with:
+          buildbuddy_api_key: >-
+            ${{ github.event_name == 'pull_request'
+            && secrets.BUILDBUDDY_CAS_ONLY_KEY
+            || (github.event_name != 'pull_request' && secrets.BUILDBUDDY_READ_WRITE_KEY)
+            || '' }}
 
       # Release commands run main's workflows, even for patch releases of older
       # release branches. Skip refs without dev/platforms, since their scripts
@@ -123,6 +129,12 @@
 
       - name: Setup Bazel
         uses: ./.github/actions/setup-bazel
+        with:
+          buildbuddy_api_key: >-
+            ${{ github.event_name == 'pull_request'
+            && secrets.BUILDBUDDY_CAS_ONLY_KEY
+            || (github.event_name != 'pull_request' && secrets.BUILDBUDDY_READ_WRITE_KEY)
+            || '' }}
 
       - name: Build runfiles wheel
         run: >-
diff --git a/.github/workflows/release_publish.yaml b/.github/workflows/release_publish.yaml
index 2209e67..0e17bbb 100644
--- a/.github/workflows/release_publish.yaml
+++ b/.github/workflows/release_publish.yaml
@@ -57,6 +57,7 @@
     uses: ./.github/workflows/release_build_artifacts.yaml
     with:
       ref: ${{ inputs.tag_name || github.ref_name }}
+    secrets: inherit
 
   release:
     name: Release
diff --git a/tests/workflows/release_build_artifacts_test.py b/tests/workflows/release_build_artifacts_test.py
index 7f89a30..c3306b2 100644
--- a/tests/workflows/release_build_artifacts_test.py
+++ b/tests/workflows/release_build_artifacts_test.py
@@ -73,6 +73,21 @@
             )
 
 
+def test_build_jobs_use_buildbuddy_rbe():
+    """Verifies that build jobs pass BuildBuddy keys to setup-bazel."""
+    workflow = _load_workflow()
+
+    for job_name in ("build", "build_wheel"):
+        job = workflow["jobs"][job_name]
+        steps = job["steps"]
+        setup_steps = [s for s in steps if s.get("name") == "Setup Bazel"]
+        assert len(setup_steps) == 1, f"Missing Setup Bazel in {job_name}"
+        assert setup_steps[0]["uses"] == "./.github/actions/setup-bazel"
+        api_key = setup_steps[0]["with"]["buildbuddy_api_key"]
+        assert "secrets.BUILDBUDDY_CAS_ONLY_KEY" in api_key
+        assert "secrets.BUILDBUDDY_READ_WRITE_KEY" in api_key
+
+
 def test_create_archive_and_notes_fetches_and_expands_tag(tmp_path: Path):
     """Verifies create_archive_and_notes.sh fetches missing tags and expands export-subst."""
     script = (
diff --git a/tests/workflows/setup_bazel_test.py b/tests/workflows/setup_bazel_test.py
index 951b9a8..335c53e 100644
--- a/tests/workflows/setup_bazel_test.py
+++ b/tests/workflows/setup_bazel_test.py
@@ -40,6 +40,18 @@
     assert any("download_bazel.sh" in step.get("run", "") for step in steps)
 
 
+def test_action_yaml_enables_buildbuddy_only_on_linux():
+    """Verifies setup-bazel action.yaml only enables BuildBuddy on Linux."""
+    action = yaml.safe_load((_action_dir() / "action.yaml").read_text(encoding="utf-8"))
+    steps = action["runs"]["steps"]
+    setup_step = next(s for s in steps if s.get("name") == "Setup Bazel")
+    assert "runner.os == 'Linux'" in setup_step["with"]["bazelrc"]
+    secret_step = next(
+        s for s in steps if s.get("name") == "Write secret to user.bazelrc"
+    )
+    assert "runner.os == 'Linux'" in secret_step["if"]
+
+
 def test_download_bazel_succeeds_first_try(tmp_path: Path):
     """Verifies download_bazel.sh succeeds without warnings when bazel --version succeeds."""
     script = _action_dir() / "download_bazel.sh"