diff --git a/.github/actions/setup-bazel-ci/action.yml b/.github/actions/setup-bazel-ci/action.yml new file mode 100644 index 0000000000..a737ec8244 --- /dev/null +++ b/.github/actions/setup-bazel-ci/action.yml @@ -0,0 +1,61 @@ +name: setup-bazel-ci +description: Prepare a Bazel CI runner with shared caches and optional test prerequisites. +inputs: + target: + description: Target triple used for cache namespacing. + required: true + install-test-prereqs: + description: Install Node.js and DotSlash for Bazel-backed test jobs. + required: false + default: "false" +outputs: + cache-hit: + description: Whether the Bazel repository cache key was restored exactly. + value: ${{ steps.cache_bazel_repository_restore.outputs.cache-hit }} + +runs: + using: composite + steps: + - name: Set up Node.js for js_repl tests + if: inputs.install-test-prereqs == 'true' + uses: actions/setup-node@v6 + with: + node-version-file: codex-rs/node-version.txt + + # Some integration tests rely on DotSlash being installed. + # See https://github.com/openai/codex/pull/7617. + - name: Install DotSlash + if: inputs.install-test-prereqs == 'true' + uses: facebook/install-dotslash@v2 + + - name: Make DotSlash available in PATH (Unix) + if: inputs.install-test-prereqs == 'true' && runner.os != 'Windows' + shell: bash + run: cp "$(which dotslash)" /usr/local/bin + + - name: Make DotSlash available in PATH (Windows) + if: inputs.install-test-prereqs == 'true' && runner.os == 'Windows' + shell: pwsh + run: Copy-Item (Get-Command dotslash).Source -Destination "$env:LOCALAPPDATA\Microsoft\WindowsApps\dotslash.exe" + + - name: Set up Bazel + uses: bazelbuild/setup-bazelisk@v3 + + # Restore bazel repository cache so we don't have to redownload all the external dependencies + # on every CI run. + - name: Restore bazel repository cache + id: cache_bazel_repository_restore + uses: actions/cache/restore@v5 + with: + path: | + ~/.cache/bazel-repo-cache + key: bazel-cache-${{ inputs.target }}-${{ hashFiles('MODULE.bazel', 'codex-rs/Cargo.lock', 'codex-rs/Cargo.toml') }} + restore-keys: | + bazel-cache-${{ inputs.target }} + + - name: Configure Bazel startup args (Windows) + if: runner.os == 'Windows' + shell: pwsh + run: | + # Use a very short path to reduce argv/path length issues. + "BAZEL_STARTUP_ARGS=--output_user_root=C:\" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append diff --git a/.github/scripts/run-bazel-ci.sh b/.github/scripts/run-bazel-ci.sh new file mode 100755 index 0000000000..06bc97cded --- /dev/null +++ b/.github/scripts/run-bazel-ci.sh @@ -0,0 +1,179 @@ +#!/usr/bin/env bash + +set -euo pipefail +set -o pipefail + +print_failed_bazel_test_logs=0 +use_node_test_env=0 + +while [[ $# -gt 0 ]]; do + case "$1" in + --print-failed-test-logs) + print_failed_bazel_test_logs=1 + shift + ;; + --use-node-test-env) + use_node_test_env=1 + shift + ;; + --) + shift + break + ;; + *) + echo "Unknown option: $1" >&2 + exit 1 + ;; + esac +done + +if [[ $# -eq 0 ]]; then + echo "Usage: $0 [--print-failed-test-logs] [--use-node-test-env] -- -- " >&2 + exit 1 +fi + +bazel_startup_args=() +if [[ -n "${BAZEL_STARTUP_ARGS:-}" ]]; then + read -r -a bazel_startup_args <<< "${BAZEL_STARTUP_ARGS}" +fi + +ci_config=ci-linux +case "${RUNNER_OS:-}" in + macOS) + ci_config=ci-macos + ;; + Windows) + ci_config=ci-windows + ;; +esac + +print_bazel_test_log_tails() { + local console_log="$1" + local testlogs_dir + local -a bazel_info_cmd=(bazel) + + if (( ${#bazel_startup_args[@]} > 0 )); then + bazel_info_cmd+=("${bazel_startup_args[@]}") + fi + + testlogs_dir="$("${bazel_info_cmd[@]}" info bazel-testlogs 2>/dev/null || echo bazel-testlogs)" + + local failed_targets=() + while IFS= read -r target; do + failed_targets+=("$target") + done < <( + grep -E '^FAIL: //' "$console_log" \ + | sed -E 's#^FAIL: (//[^ ]+).*#\1#' \ + | sort -u + ) + + if [[ ${#failed_targets[@]} -eq 0 ]]; then + echo "No failed Bazel test targets were found in console output." + return + fi + + for target in "${failed_targets[@]}"; do + local rel_path="${target#//}" + rel_path="${rel_path/:/\/}" + local test_log="${testlogs_dir}/${rel_path}/test.log" + + echo "::group::Bazel test log tail for ${target}" + if [[ -f "$test_log" ]]; then + tail -n 200 "$test_log" + else + echo "Missing test log: $test_log" + fi + echo "::endgroup::" + done +} + +bazel_args=() +bazel_targets=() +found_target_separator=0 +for arg in "$@"; do + if [[ "$arg" == "--" && $found_target_separator -eq 0 ]]; then + found_target_separator=1 + continue + fi + + if [[ $found_target_separator -eq 0 ]]; then + bazel_args+=("$arg") + else + bazel_targets+=("$arg") + fi +done + +if [[ ${#bazel_args[@]} -eq 0 || ${#bazel_targets[@]} -eq 0 ]]; then + echo "Expected Bazel args and targets separated by --" >&2 + exit 1 +fi + +if [[ $use_node_test_env -eq 1 && "${RUNNER_OS:-}" != "Windows" ]]; then + # Bazel test sandboxes on macOS may resolve an older Homebrew `node` + # before the `actions/setup-node` runtime on PATH. + node_bin="$(which node)" + bazel_args+=("--test_env=CODEX_JS_REPL_NODE_PATH=${node_bin}") +fi + +bazel_console_log="$(mktemp)" +trap 'rm -f "$bazel_console_log"' EXIT + +bazel_cmd=(bazel) +if (( ${#bazel_startup_args[@]} > 0 )); then + bazel_cmd+=("${bazel_startup_args[@]}") +fi + +if [[ -n "${BUILDBUDDY_API_KEY:-}" ]]; then + echo "BuildBuddy API key is available; using remote Bazel configuration." + # Work around Bazel 9 remote repo contents cache / overlay materialization failures + # seen in CI (for example "is not a symlink" or permission errors while + # materializing external repos such as rules_perl). We still use BuildBuddy for + # remote execution/cache; this only disables the startup-level repo contents cache. + set +e + "${bazel_cmd[@]}" \ + --noexperimental_remote_repo_contents_cache \ + "${bazel_args[@]}" \ + "--config=${ci_config}" \ + "--remote_header=x-buildbuddy-api-key=${BUILDBUDDY_API_KEY}" \ + -- \ + "${bazel_targets[@]}" \ + 2>&1 | tee "$bazel_console_log" + bazel_status=${PIPESTATUS[0]} + set -e +else + echo "BuildBuddy API key is not available; using local Bazel configuration." + # Keep fork/community PRs on Bazel but disable remote services that are + # configured in .bazelrc and require auth. + # + # Flag docs: + # - Command-line reference: https://bazel.build/reference/command-line-reference + # - Remote caching overview: https://bazel.build/remote/caching + # - Remote execution overview: https://bazel.build/remote/rbe + # - Build Event Protocol overview: https://bazel.build/remote/bep + # + # --noexperimental_remote_repo_contents_cache: + # disable remote repo contents cache enabled in .bazelrc startup options. + # https://bazel.build/reference/command-line-reference#startup_options-flag--experimental_remote_repo_contents_cache + # --remote_cache= and --remote_executor=: + # clear remote cache/execution endpoints configured in .bazelrc. + # https://bazel.build/reference/command-line-reference#common_options-flag--remote_cache + # https://bazel.build/reference/command-line-reference#common_options-flag--remote_executor + set +e + "${bazel_cmd[@]}" \ + --noexperimental_remote_repo_contents_cache \ + "${bazel_args[@]}" \ + --remote_cache= \ + --remote_executor= \ + -- \ + "${bazel_targets[@]}" \ + 2>&1 | tee "$bazel_console_log" + bazel_status=${PIPESTATUS[0]} + set -e +fi + +if [[ ${bazel_status:-0} -ne 0 ]]; then + if [[ $print_failed_bazel_test_logs -eq 1 ]]; then + print_bazel_test_log_tails "$bazel_console_log" + fi + exit "$bazel_status" +fi diff --git a/.github/workflows/bazel.yml b/.github/workflows/bazel.yml index 9a647903cc..3555a52664 100644 --- a/.github/workflows/bazel.yml +++ b/.github/workflows/bazel.yml @@ -50,181 +50,40 @@ jobs: steps: - uses: actions/checkout@v6 - - name: Set up Node.js for js_repl tests - uses: actions/setup-node@v6 + - name: Set up Bazel CI + id: setup_bazel + uses: ./.github/actions/setup-bazel-ci with: - node-version-file: codex-rs/node-version.txt - - # Some integration tests rely on DotSlash being installed. - # See https://github.com/openai/codex/pull/7617. - - name: Install DotSlash - uses: facebook/install-dotslash@v2 - - - name: Make DotSlash available in PATH (Unix) - if: runner.os != 'Windows' - run: cp "$(which dotslash)" /usr/local/bin - - - name: Make DotSlash available in PATH (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: Copy-Item (Get-Command dotslash).Source -Destination "$env:LOCALAPPDATA\Microsoft\WindowsApps\dotslash.exe" - - # Install Bazel via Bazelisk - - name: Set up Bazel - uses: bazelbuild/setup-bazelisk@v3 + target: ${{ matrix.target }} + install-test-prereqs: "true" - name: Check MODULE.bazel.lock is up to date if: matrix.os == 'ubuntu-24.04' && matrix.target == 'x86_64-unknown-linux-gnu' shell: bash run: ./scripts/check-module-bazel-lock.sh - # Restore bazel repository cache so we don't have to redownload all the external dependencies - # on every CI run. - - name: Restore bazel repository cache - id: cache_bazel_repository_restore - uses: actions/cache/restore@v5 - with: - path: | - ~/.cache/bazel-repo-cache - key: bazel-cache-${{ matrix.target }}-${{ hashFiles('MODULE.bazel', 'codex-rs/Cargo.lock', 'codex-rs/Cargo.toml') }} - restore-keys: | - bazel-cache-${{ matrix.target }} - - - name: Configure Bazel startup args (Windows) - if: runner.os == 'Windows' - shell: pwsh - run: | - # Use a very short path to reduce argv/path length issues. - "BAZEL_STARTUP_ARGS=--output_user_root=C:\" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append - - name: bazel test //... env: BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }} shell: bash run: | - set -o pipefail - - bazel_console_log="$(mktemp)" - - print_failed_bazel_test_logs() { - local console_log="$1" - local testlogs_dir - - testlogs_dir="$(bazel $BAZEL_STARTUP_ARGS info bazel-testlogs 2>/dev/null || echo bazel-testlogs)" - - local failed_targets=() - while IFS= read -r target; do - failed_targets+=("$target") - done < <( - grep -E '^FAIL: //' "$console_log" \ - | sed -E 's#^FAIL: (//[^ ]+).*#\1#' \ - | sort -u - ) - - if [[ ${#failed_targets[@]} -eq 0 ]]; then - echo "No failed Bazel test targets were found in console output." - return - fi - - for target in "${failed_targets[@]}"; do - local rel_path="${target#//}" - rel_path="${rel_path/:/\/}" - local test_log="${testlogs_dir}/${rel_path}/test.log" - - echo "::group::Bazel test log tail for ${target}" - if [[ -f "$test_log" ]]; then - tail -n 200 "$test_log" - else - echo "Missing test log: $test_log" - fi - echo "::endgroup::" - done - } - - bazel_args=( - test - --test_verbose_timeout_warnings - --build_metadata=COMMIT_SHA=$(git rev-parse HEAD) - ) - - bazel_targets=( - //... - # Keep V8 out of the ordinary Bazel CI path. Only the dedicated - # canary and release workflows should build `third_party/v8`. + # Keep V8 out of the ordinary Bazel CI path. Only the dedicated + # canary and release workflows should build `third_party/v8`. + ./.github/scripts/run-bazel-ci.sh \ + --print-failed-test-logs \ + --use-node-test-env \ + -- \ + test \ + --test_verbose_timeout_warnings \ + --build_metadata=COMMIT_SHA=${GITHUB_SHA} \ + -- \ + //... \ -//third_party/v8:all - ) - - if [[ "${RUNNER_OS:-}" != "Windows" ]]; then - # Bazel test sandboxes on macOS may resolve an older Homebrew `node` - # before the `actions/setup-node` runtime on PATH. - node_bin="$(which node)" - bazel_args+=("--test_env=CODEX_JS_REPL_NODE_PATH=${node_bin}") - fi - - ci_config=ci-linux - if [[ "${RUNNER_OS:-}" == "macOS" ]]; then - ci_config=ci-macos - elif [[ "${RUNNER_OS:-}" == "Windows" ]]; then - ci_config=ci-windows - fi - - if [[ -n "${BUILDBUDDY_API_KEY:-}" ]]; then - echo "BuildBuddy API key is available; using remote Bazel configuration." - # Work around Bazel 9 remote repo contents cache / overlay materialization failures - # seen in CI (for example "is not a symlink" or permission errors while - # materializing external repos such as rules_perl). We still use BuildBuddy for - # remote execution/cache; this only disables the startup-level repo contents cache. - set +e - bazel $BAZEL_STARTUP_ARGS \ - --noexperimental_remote_repo_contents_cache \ - "${bazel_args[@]}" \ - "--config=${ci_config}" \ - "--remote_header=x-buildbuddy-api-key=$BUILDBUDDY_API_KEY" \ - -- \ - "${bazel_targets[@]}" \ - 2>&1 | tee "$bazel_console_log" - bazel_status=${PIPESTATUS[0]} - set -e - else - echo "BuildBuddy API key is not available; using local Bazel configuration." - # Keep fork/community PRs on Bazel but disable remote services that are - # configured in .bazelrc and require auth. - # - # Flag docs: - # - Command-line reference: https://bazel.build/reference/command-line-reference - # - Remote caching overview: https://bazel.build/remote/caching - # - Remote execution overview: https://bazel.build/remote/rbe - # - Build Event Protocol overview: https://bazel.build/remote/bep - # - # --noexperimental_remote_repo_contents_cache: - # disable remote repo contents cache enabled in .bazelrc startup options. - # https://bazel.build/reference/command-line-reference#startup_options-flag--experimental_remote_repo_contents_cache - # --remote_cache= and --remote_executor=: - # clear remote cache/execution endpoints configured in .bazelrc. - # https://bazel.build/reference/command-line-reference#common_options-flag--remote_cache - # https://bazel.build/reference/command-line-reference#common_options-flag--remote_executor - set +e - bazel $BAZEL_STARTUP_ARGS \ - --noexperimental_remote_repo_contents_cache \ - "${bazel_args[@]}" \ - --remote_cache= \ - --remote_executor= \ - -- \ - "${bazel_targets[@]}" \ - 2>&1 | tee "$bazel_console_log" - bazel_status=${PIPESTATUS[0]} - set -e - fi - - if [[ ${bazel_status:-0} -ne 0 ]]; then - print_failed_bazel_test_logs "$bazel_console_log" - exit "$bazel_status" - fi # Save bazel repository cache explicitly; make non-fatal so cache uploading # never fails the overall job. Only save when key wasn't hit. - name: Save bazel repository cache - if: always() && !cancelled() && steps.cache_bazel_repository_restore.outputs.cache-hit != 'true' + if: always() && !cancelled() && steps.setup_bazel.outputs.cache-hit != 'true' continue-on-error: true uses: actions/cache/save@v5 with: @@ -233,75 +92,52 @@ jobs: key: bazel-cache-${{ matrix.target }}-${{ hashFiles('MODULE.bazel', 'codex-rs/Cargo.lock', 'codex-rs/Cargo.toml') }} clippy: - runs-on: ubuntu-24.04 - name: Bazel clippy for codex-rs + strategy: + fail-fast: false + matrix: + include: + # Keep Linux lint coverage on x64 and add the arm64 macOS path that + # the Bazel test job already exercises. + - os: ubuntu-24.04 + target: x86_64-unknown-linux-gnu + - os: macos-15-xlarge + target: aarch64-apple-darwin + runs-on: ${{ matrix.os }} + name: Bazel clippy on ${{ matrix.os }} for ${{ matrix.target }} steps: - uses: actions/checkout@v6 - - name: Set up Bazel - uses: bazelbuild/setup-bazelisk@v3 - - # Restore bazel repository cache so we don't have to redownload all the external dependencies - # on every CI run. - - name: Restore bazel repository cache - id: cache_bazel_repository_restore - uses: actions/cache/restore@v5 + - name: Set up Bazel CI + id: setup_bazel + uses: ./.github/actions/setup-bazel-ci with: - path: | - ~/.cache/bazel-repo-cache - key: bazel-cache-x86_64-unknown-linux-gnu-${{ hashFiles('MODULE.bazel', 'codex-rs/Cargo.lock', 'codex-rs/Cargo.toml') }} - restore-keys: | - bazel-cache-x86_64-unknown-linux-gnu + target: ${{ matrix.target }} - name: bazel build --config=clippy //codex-rs/... env: BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }} shell: bash run: | - set -o pipefail - - bazel_args=( - build - --config=ci-linux - --config=clippy - --build_metadata=COMMIT_SHA=$(git rev-parse HEAD) - --build_metadata=TAG_job=clippy - ) - - bazel_targets=( - //codex-rs/... - # Keep the initial Bazel clippy scope on codex-rs and out of the - # V8 proof-of-concept target for now. + # Keep the initial Bazel clippy scope on codex-rs and out of the + # V8 proof-of-concept target for now. + ./.github/scripts/run-bazel-ci.sh \ + -- \ + build \ + --config=clippy \ + --build_metadata=COMMIT_SHA=${GITHUB_SHA} \ + --build_metadata=TAG_job=clippy \ + -- \ + //codex-rs/... \ -//codex-rs/v8-poc:all - ) - - if [[ -n "${BUILDBUDDY_API_KEY:-}" ]]; then - echo "BuildBuddy API key is available; using remote Bazel configuration." - bazel $BAZEL_STARTUP_ARGS \ - --noexperimental_remote_repo_contents_cache \ - "${bazel_args[@]}" \ - --remote_header="x-buildbuddy-api-key=$BUILDBUDDY_API_KEY" \ - -- \ - "${bazel_targets[@]}" - else - echo "BuildBuddy API key is not available; using local Bazel configuration." - bazel $BAZEL_STARTUP_ARGS \ - --noexperimental_remote_repo_contents_cache \ - "${bazel_args[@]}" \ - --remote_cache= \ - --remote_executor= \ - -- \ - "${bazel_targets[@]}" - fi # Save bazel repository cache explicitly; make non-fatal so cache uploading # never fails the overall job. Only save when key wasn't hit. - name: Save bazel repository cache - if: always() && !cancelled() && steps.cache_bazel_repository_restore.outputs.cache-hit != 'true' + if: always() && !cancelled() && steps.setup_bazel.outputs.cache-hit != 'true' continue-on-error: true uses: actions/cache/save@v5 with: path: | ~/.cache/bazel-repo-cache - key: bazel-cache-x86_64-unknown-linux-gnu-${{ hashFiles('MODULE.bazel', 'codex-rs/Cargo.lock', 'codex-rs/Cargo.toml') }} + key: bazel-cache-${{ matrix.target }}-${{ hashFiles('MODULE.bazel', 'codex-rs/Cargo.lock', 'codex-rs/Cargo.toml') }}