openai/codex

19 workflows · maturity 67% · 12 patterns · GitHub ↗

Security 18.59/100

Security dimensions

permissions
5.3
security scan
0
supply chain
13.3
secret handling
0
harden runner
0

Workflows (19)

bazel matrix .github/workflows/bazel.yml
Triggers
pull_request, push, workflow_dispatch
Runs on
${{ matrix.os }}, ${{ matrix.os }}
Jobs
test, clippy
Matrix
include, include.os, include.target→ aarch64-apple-darwin, macos-15-xlarge, ubuntu-24.04, windows-latest, x86_64-apple-darwin, x86_64-pc-windows-gnullvm, x86_64-unknown-linux-gnu, x86_64-unknown-linux-musl
Commands
  • ./scripts/check-module-bazel-lock.sh
  • bazel_targets=( //... # Keep standalone V8 library targets out of the ordinary Bazel CI # path. V8 consumers under `//codex-rs/...` still participate # transitively through `//...`. -//third_party/v8:all ) ./.github/scripts/run-bazel-ci.sh \ --print-failed-test-logs \ --use-node-test-env \ -- \ test \ --test_tag_filters=-argument-comment-lint \ --test_verbose_timeout_warnings \ --build_metadata=COMMIT_SHA=${GITHUB_SHA} \ -- \ "${bazel_targets[@]}"
  • # 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
View raw YAML
name: Bazel

# Note this workflow was originally derived from:
# https://github.com/cerisier/toolchains_llvm_bootstrapped/blob/main/.github/workflows/ci.yaml

on:
  pull_request: {}
  push:
    branches:
      - main
  workflow_dispatch:

concurrency:
  # Cancel previous actions from the same PR or branch except 'main' branch.
  # See https://docs.github.com/en/actions/using-jobs/using-concurrency and https://docs.github.com/en/actions/learn-github-actions/contexts for more info.
  group: concurrency-group::${{ github.workflow }}::${{ github.event.pull_request.number > 0 && format('pr-{0}', github.event.pull_request.number) || github.ref_name }}${{ github.ref_name == 'main' && format('::{0}', github.run_id) || ''}}
  cancel-in-progress: ${{ github.ref_name != 'main' }}
jobs:
  test:
    timeout-minutes: 30
    strategy:
      fail-fast: false
      matrix:
        include:
          # macOS
          - os: macos-15-xlarge
            target: aarch64-apple-darwin
          - os: macos-15-xlarge
            target: x86_64-apple-darwin

          # Linux
          - os: ubuntu-24.04
            target: x86_64-unknown-linux-gnu
          - os: ubuntu-24.04
            target: x86_64-unknown-linux-musl
          # 2026-02-27 Bazel tests have been flaky on arm in CI.
          # Disable until we can investigate and stabilize them.
          # - os: ubuntu-24.04-arm
          #   target: aarch64-unknown-linux-musl
          # - os: ubuntu-24.04-arm
          #   target: aarch64-unknown-linux-gnu

          # Windows
          - os: windows-latest
            target: x86_64-pc-windows-gnullvm
    runs-on: ${{ matrix.os }}

    # Configure a human readable name for each job
    name: Local Bazel build on ${{ matrix.os }} for ${{ matrix.target }}

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

      - name: Set up Bazel CI
        id: setup_bazel
        uses: ./.github/actions/setup-bazel-ci
        with:
          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

      - name: bazel test //...
        env:
          BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
        shell: bash
        run: |
          bazel_targets=(
            //...
            # Keep standalone V8 library targets out of the ordinary Bazel CI
            # path. V8 consumers under `//codex-rs/...` still participate
            # transitively through `//...`.
            -//third_party/v8:all
          )

          ./.github/scripts/run-bazel-ci.sh \
            --print-failed-test-logs \
            --use-node-test-env \
            -- \
            test \
            --test_tag_filters=-argument-comment-lint \
            --test_verbose_timeout_warnings \
            --build_metadata=COMMIT_SHA=${GITHUB_SHA} \
            -- \
            "${bazel_targets[@]}"

      # 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.setup_bazel.outputs.cache-hit != 'true'
        continue-on-error: true
        uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
        with:
          path: |
            ~/.cache/bazel-repo-cache
          key: bazel-cache-${{ matrix.target }}-${{ hashFiles('MODULE.bazel', 'codex-rs/Cargo.lock', 'codex-rs/Cargo.toml') }}

  clippy:
    timeout-minutes: 30
    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. Add Windows gnullvm as well
          # so PRs get Bazel-native lint signal on the same Windows toolchain
          # that the Bazel test job uses.
          - os: ubuntu-24.04
            target: x86_64-unknown-linux-gnu
          - os: macos-15-xlarge
            target: aarch64-apple-darwin
          - os: windows-latest
            target: x86_64-pc-windows-gnullvm
    runs-on: ${{ matrix.os }}
    name: Bazel clippy on ${{ matrix.os }} for ${{ matrix.target }}

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

      - name: Set up Bazel CI
        id: setup_bazel
        uses: ./.github/actions/setup-bazel-ci
        with:
          target: ${{ matrix.target }}

      - name: bazel build --config=clippy //codex-rs/...
        env:
          BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
        shell: bash
        run: |
          # 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

      # 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.setup_bazel.outputs.cache-hit != 'true'
        continue-on-error: true
        uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
        with:
          path: |
            ~/.cache/bazel-repo-cache
          key: bazel-cache-${{ matrix.target }}-${{ hashFiles('MODULE.bazel', 'codex-rs/Cargo.lock', 'codex-rs/Cargo.toml') }}
blob-size-policy .github/workflows/blob-size-policy.yml
Triggers
pull_request
Runs on
ubuntu-24.04
Jobs
check
Commands
  • set -euo pipefail echo "base=$(git rev-parse HEAD^1)" >> "$GITHUB_OUTPUT" echo "head=$(git rev-parse HEAD^2)" >> "$GITHUB_OUTPUT"
  • python3 scripts/check_blob_size.py \ --base "$BASE_SHA" \ --head "$HEAD_SHA" \ --max-bytes 512000 \ --allowlist .github/blob-size-allowlist.txt
View raw YAML
name: blob-size-policy

on:
  pull_request: {}

jobs:
  check:
    name: Blob size policy
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
        with:
          fetch-depth: 0

      - name: Determine PR comparison range
        id: range
        shell: bash
        run: |
          set -euo pipefail
          echo "base=$(git rev-parse HEAD^1)" >> "$GITHUB_OUTPUT"
          echo "head=$(git rev-parse HEAD^2)" >> "$GITHUB_OUTPUT"

      - name: Check changed blob sizes
        env:
          BASE_SHA: ${{ steps.range.outputs.base }}
          HEAD_SHA: ${{ steps.range.outputs.head }}
        run: |
          python3 scripts/check_blob_size.py \
            --base "$BASE_SHA" \
            --head "$HEAD_SHA" \
            --max-bytes 512000 \
            --allowlist .github/blob-size-allowlist.txt
cargo-deny .github/workflows/cargo-deny.yml
Triggers
pull_request, push
Runs on
ubuntu-latest
Jobs
cargo-deny
Actions
dtolnay/rust-toolchain, EmbarkStudios/cargo-deny-action
View raw YAML
name: cargo-deny

on:
  pull_request:
  push:
    branches:
      - main

jobs:
  cargo-deny:
    runs-on: ubuntu-latest
    defaults:
      run:
        working-directory: ./codex-rs
    steps:
      - name: Checkout
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

      - name: Install Rust toolchain
        uses: dtolnay/rust-toolchain@631a55b12751854ce901bb631d5902ceb48146f7 # stable

      - name: Run cargo-deny
        uses: EmbarkStudios/cargo-deny-action@82eb9f621fbc699dd0918f3ea06864c14cc84246 # v2
        with:
          rust-version: stable
          manifest-path: ./codex-rs/Cargo.toml
ci .github/workflows/ci.yml
Triggers
pull_request, push
Runs on
ubuntu-latest
Jobs
build-test
Actions
pnpm/action-setup, facebook/install-dotslash
Commands
  • pnpm install --frozen-lockfile
  • set -euo pipefail # Use a rust-release version that includes all native binaries. CODEX_VERSION=0.115.0 OUTPUT_DIR="${RUNNER_TEMP}" python3 ./scripts/stage_npm_packages.py \ --release-version "$CODEX_VERSION" \ --package codex \ --output-dir "$OUTPUT_DIR" PACK_OUTPUT="${OUTPUT_DIR}/codex-npm-${CODEX_VERSION}.tgz" echo "pack_output=$PACK_OUTPUT" >> "$GITHUB_OUTPUT"
  • ./scripts/asciicheck.py README.md
  • python3 scripts/readme_toc.py README.md
  • ./scripts/asciicheck.py codex-cli/README.md
  • python3 scripts/readme_toc.py codex-cli/README.md
  • pnpm run format
View raw YAML
name: ci

on:
  pull_request: {}
  push: { branches: [main] }

jobs:
  build-test:
    runs-on: ubuntu-latest
    timeout-minutes: 10
    env:
      NODE_OPTIONS: --max-old-space-size=4096
    steps:
      - name: Checkout repository
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

      - name: Setup pnpm
        uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5
        with:
          run_install: false

      - name: Setup Node.js
        uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
        with:
          node-version: 22

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      # stage_npm_packages.py requires DotSlash when staging releases.
      - uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2

      - name: Stage npm package
        id: stage_npm_package
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          set -euo pipefail
          # Use a rust-release version that includes all native binaries.
          CODEX_VERSION=0.115.0
          OUTPUT_DIR="${RUNNER_TEMP}"
          python3 ./scripts/stage_npm_packages.py \
            --release-version "$CODEX_VERSION" \
            --package codex \
            --output-dir "$OUTPUT_DIR"
          PACK_OUTPUT="${OUTPUT_DIR}/codex-npm-${CODEX_VERSION}.tgz"
          echo "pack_output=$PACK_OUTPUT" >> "$GITHUB_OUTPUT"

      - name: Upload staged npm package artifact
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
        with:
          name: codex-npm-staging
          path: ${{ steps.stage_npm_package.outputs.pack_output }}

      - name: Ensure root README.md contains only ASCII and certain Unicode code points
        run: ./scripts/asciicheck.py README.md
      - name: Check root README ToC
        run: python3 scripts/readme_toc.py README.md

      - name: Ensure codex-cli/README.md contains only ASCII and certain Unicode code points
        run: ./scripts/asciicheck.py codex-cli/README.md
      - name: Check codex-cli/README ToC
        run: python3 scripts/readme_toc.py codex-cli/README.md

      - name: Prettier (run `pnpm run format:fix` to fix)
        run: pnpm run format
cla perms .github/workflows/cla.yml
Triggers
issue_comment, pull_request_target
Runs on
ubuntu-latest
Jobs
cla
Actions
contributor-assistant/github-action
View raw YAML
name: CLA Assistant
on:
  issue_comment:
    types: [created]
  pull_request_target:
    types: [opened, closed, synchronize]

permissions:
  actions: write
  contents: write
  pull-requests: write
  statuses: write

jobs:
  cla:
    # Only run the CLA assistant for the canonical openai repo so forks are not blocked
    # and contributors who signed previously do not receive duplicate CLA notifications.
    if: ${{ github.repository_owner == 'openai' }}
    runs-on: ubuntu-latest
    steps:
      - uses: contributor-assistant/github-action@ca4a40a7d1004f18d9960b404b97e5f30a505a08 # v2.6.1
        # Run on close only if the PR was merged. This will lock the PR to preserve
        # the CLA agreement. We don't want to lock PRs that have been closed without
        # merging because the contributor may want to respond with additional comments.
        # This action has a "lock-pullrequest-aftermerge" option that can be set to false,
        # but that would unconditionally skip locking even in cases where the PR was merged.
        if: |
          (
            github.event_name == 'pull_request_target' &&
            (
              github.event.action == 'opened' ||
              github.event.action == 'synchronize' ||
              (github.event.action == 'closed' && github.event.pull_request.merged == true)
            )
          ) ||
          (
            github.event_name == 'issue_comment' &&
            (
              github.event.comment.body == 'recheck' ||
              github.event.comment.body == 'I have read the CLA Document and I hereby sign the CLA'
            )
          )
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          path-to-document: https://github.com/openai/codex/blob/main/docs/CLA.md
          path-to-signatures: signatures/cla.json
          branch: cla-signatures
          allowlist: codex,dependabot,dependabot[bot],github-actions[bot]
close-stale-contributor-prs perms .github/workflows/close-stale-contributor-prs.yml
Triggers
workflow_dispatch, schedule
Runs on
ubuntu-latest
Jobs
close-stale-contributor-prs
View raw YAML
name: Close stale contributor PRs

on:
  workflow_dispatch:
  schedule:
    - cron: "0 6 * * *"

permissions:
  contents: read
  issues: write
  pull-requests: write

jobs:
  close-stale-contributor-prs:
    # Prevent scheduled runs on forks
    if: github.repository == 'openai/codex'
    runs-on: ubuntu-latest
    steps:
      - name: Close inactive PRs from contributors
        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
        with:
          github-token: ${{ secrets.GITHUB_TOKEN }}
          script: |
            const DAYS_INACTIVE = 14;
            const cutoff = new Date(Date.now() - DAYS_INACTIVE * 24 * 60 * 60 * 1000);
            const { owner, repo } = context.repo;
            const dryRun = false;
            const stalePrs = [];

            core.info(`Dry run mode: ${dryRun}`);

            const prs = await github.paginate(github.rest.pulls.list, {
              owner,
              repo,
              state: "open",
              per_page: 100,
              sort: "updated",
              direction: "asc",
            });

            for (const pr of prs) {
              const lastUpdated = new Date(pr.updated_at);
              if (lastUpdated > cutoff) {
                core.info(`PR ${pr.number} is fresh`);
                continue;
              }

              if (!pr.user || pr.user.type !== "User") {
                core.info(`PR ${pr.number} wasn't created by a user`);
                continue;
              }

              let permission;
              try {
                const permissionResponse = await github.rest.repos.getCollaboratorPermissionLevel({
                  owner,
                  repo,
                  username: pr.user.login,
                });
                permission = permissionResponse.data.permission;
              } catch (error) {
                if (error.status === 404) {
                  core.info(`Author ${pr.user.login} is not a collaborator; skipping #${pr.number}`);
                  continue;
                }
                throw error;
              }

              const hasContributorAccess = ["admin", "maintain", "write"].includes(permission);
              if (!hasContributorAccess) {
                core.info(`Author ${pr.user.login} has ${permission} access; skipping #${pr.number}`);
                continue;
              }

              stalePrs.push(pr);
            }

            if (!stalePrs.length) {
              core.info("No stale contributor pull requests found.");
              return;
            }

            for (const pr of stalePrs) {
              const issue_number = pr.number;
              const closeComment = `Closing this pull request because it has had no updates for more than ${DAYS_INACTIVE} days. If you plan to continue working on it, feel free to reopen or open a new PR.`;

              if (dryRun) {
                core.info(`[dry-run] Would close contributor PR #${issue_number} from ${pr.user.login}`);
                continue;
              }

              await github.rest.issues.createComment({
                owner,
                repo,
                issue_number,
                body: closeComment,
              });

              await github.rest.pulls.update({
                owner,
                repo,
                pull_number: issue_number,
                state: "closed",
              });

              core.info(`Closed contributor PR #${issue_number} from ${pr.user.login}`);
            }
codespell perms .github/workflows/codespell.yml
Triggers
push, pull_request
Runs on
ubuntu-latest
Jobs
codespell
Actions
codespell-project/codespell-problem-matcher, codespell-project/actions-codespell
View raw YAML
# Codespell configuration is within .codespellrc
---
name: Codespell

on:
  push:
    branches: [main]
  pull_request:
    branches: [main]

permissions:
  contents: read

jobs:
  codespell:
    name: Check for spelling errors
    runs-on: ubuntu-latest

    steps:
      - name: Checkout
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
      - name: Annotate locations with typos
        uses: codespell-project/codespell-problem-matcher@b80729f885d32f78a716c2f107b4db1025001c42 # v1
      - name: Codespell
        uses: codespell-project/actions-codespell@8f01853be192eb0f849a5c7d721450e7a467c579 # v2.2
        with:
          ignore_words_file: .codespellignore
issue-deduplicator .github/workflows/issue-deduplicator.yml
Triggers
issues
Runs on
ubuntu-latest, ubuntu-latest, ubuntu-latest, ubuntu-latest
Jobs
gather-duplicates-all, gather-duplicates-open, select-final, comment-on-issue
Actions
openai/codex-action, openai/codex-action
Commands
  • set -eo pipefail CURRENT_ISSUE_FILE=codex-current-issue.json EXISTING_ALL_FILE=codex-existing-issues-all.json gh issue list --repo "$REPO" \ --json number,title,body,createdAt,updatedAt,state,labels \ --limit 1000 \ --state all \ --search "sort:created-desc" \ | jq '[.[] | { number, title, body: ((.body // "")[0:4000]), createdAt, updatedAt, state, labels: ((.labels // []) | map(.name)) }]' \ > "$EXISTING_ALL_FILE" gh issue view "$ISSUE_NUMBER" \ --repo "$REPO" \ --json number,title,body \ | jq '{number, title, body: ((.body // "")[0:4000])}' \ > "$CURRENT_ISSUE_FILE" echo "Prepared duplicate detection input files." echo "all_issue_count=$(jq 'length' "$EXISTING_ALL_FILE")"
  • set -eo pipefail raw=${CODEX_OUTPUT//$'\r'/} parsed=false issues='[]' reason='' if [ -n "$raw" ] && printf '%s' "$raw" | jq -e 'type == "object" and (.issues | type == "array")' >/dev/null 2>&1; then parsed=true issues=$(printf '%s' "$raw" | jq -c '[.issues[] | tostring]') reason=$(printf '%s' "$raw" | jq -r '.reason // ""') else reason='Pass 1 output was empty or invalid JSON.' fi filtered=$(jq -cn --argjson issues "$issues" --arg current "$CURRENT_ISSUE_NUMBER" '[ $issues[] | tostring | select(. != $current) ] | reduce .[] as $issue ([]; if index($issue) then . else . + [$issue] end) | .[:5]') has_matches=false if [ "$(jq 'length' <<< "$filtered")" -gt 0 ]; then has_matches=true fi echo "Pass 1 parsed: $parsed" echo "Pass 1 matches after filtering: $(jq 'length' <<< "$filtered")" echo "Pass 1 reason: $reason" { echo "issues_json=$filtered" echo "reason<<EOF" echo "$reason" echo "EOF" echo "has_matches=$has_matches" } >> "$GITHUB_OUTPUT"
  • set -eo pipefail CURRENT_ISSUE_FILE=codex-current-issue.json EXISTING_OPEN_FILE=codex-existing-issues-open.json gh issue list --repo "$REPO" \ --json number,title,body,createdAt,updatedAt,state,labels \ --limit 1000 \ --state open \ --search "sort:created-desc" \ | jq '[.[] | { number, title, body: ((.body // "")[0:4000]), createdAt, updatedAt, state, labels: ((.labels // []) | map(.name)) }]' \ > "$EXISTING_OPEN_FILE" gh issue view "$ISSUE_NUMBER" \ --repo "$REPO" \ --json number,title,body \ | jq '{number, title, body: ((.body // "")[0:4000])}' \ > "$CURRENT_ISSUE_FILE" echo "Prepared fallback duplicate detection input files." echo "open_issue_count=$(jq 'length' "$EXISTING_OPEN_FILE")"
  • set -eo pipefail raw=${CODEX_OUTPUT//$'\r'/} parsed=false issues='[]' reason='' if [ -n "$raw" ] && printf '%s' "$raw" | jq -e 'type == "object" and (.issues | type == "array")' >/dev/null 2>&1; then parsed=true issues=$(printf '%s' "$raw" | jq -c '[.issues[] | tostring]') reason=$(printf '%s' "$raw" | jq -r '.reason // ""') else reason='Pass 2 output was empty or invalid JSON.' fi filtered=$(jq -cn --argjson issues "$issues" --arg current "$CURRENT_ISSUE_NUMBER" '[ $issues[] | tostring | select(. != $current) ] | reduce .[] as $issue ([]; if index($issue) then . else . + [$issue] end) | .[:5]') has_matches=false if [ "$(jq 'length' <<< "$filtered")" -gt 0 ]; then has_matches=true fi echo "Pass 2 parsed: $parsed" echo "Pass 2 matches after filtering: $(jq 'length' <<< "$filtered")" echo "Pass 2 reason: $reason" { echo "issues_json=$filtered" echo "reason<<EOF" echo "$reason" echo "EOF" echo "has_matches=$has_matches" } >> "$GITHUB_OUTPUT"
  • set -eo pipefail selected_issues='[]' selected_reason='No plausible duplicates found.' selected_pass='none' if [ "$PASS1_HAS_MATCHES" = "true" ]; then selected_issues=${PASS1_ISSUES:-'[]'} selected_reason=${PASS1_REASON:-'Pass 1 found duplicates.'} selected_pass='all' fi if [ "$PASS2_HAS_MATCHES" = "true" ]; then selected_issues=${PASS2_ISSUES:-'[]'} selected_reason=${PASS2_REASON:-'Pass 2 found duplicates.'} selected_pass='open-fallback' fi final_json=$(jq -cn \ --argjson issues "$selected_issues" \ --arg reason "$selected_reason" \ --arg pass "$selected_pass" \ '{issues: $issues, reason: $reason, pass: $pass}') echo "Final pass used: $selected_pass" echo "Final duplicate count: $(jq '.issues | length' <<< "$final_json")" echo "Final reason: $(jq -r '.reason' <<< "$final_json")" { echo "codex_output<<EOF" echo "$final_json" echo "EOF" } >> "$GITHUB_OUTPUT"
  • gh issue edit "$ISSUE_NUMBER" --remove-label codex-deduplicate || true echo "Attempted to remove label: codex-deduplicate"
View raw YAML
name: Issue Deduplicator

on:
  issues:
    types:
      - opened
      - labeled

jobs:
  gather-duplicates-all:
    name: Identify potential duplicates (all issues)
    # Prevent runs on forks (requires OpenAI API key, wastes Actions minutes)
    if: github.repository == 'openai/codex' && (github.event.action == 'opened' || (github.event.action == 'labeled' && github.event.label.name == 'codex-deduplicate'))
    runs-on: ubuntu-latest
    permissions:
      contents: read
    outputs:
      issues_json: ${{ steps.normalize-all.outputs.issues_json }}
      reason: ${{ steps.normalize-all.outputs.reason }}
      has_matches: ${{ steps.normalize-all.outputs.has_matches }}
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

      - name: Prepare Codex inputs
        env:
          GH_TOKEN: ${{ github.token }}
          REPO: ${{ github.repository }}
          ISSUE_NUMBER: ${{ github.event.issue.number }}
        run: |
          set -eo pipefail

          CURRENT_ISSUE_FILE=codex-current-issue.json
          EXISTING_ALL_FILE=codex-existing-issues-all.json

          gh issue list --repo "$REPO" \
            --json number,title,body,createdAt,updatedAt,state,labels \
            --limit 1000 \
            --state all \
            --search "sort:created-desc" \
            | jq '[.[] | {
                number,
                title,
                body: ((.body // "")[0:4000]),
                createdAt,
                updatedAt,
                state,
                labels: ((.labels // []) | map(.name))
              }]' \
            > "$EXISTING_ALL_FILE"

          gh issue view "$ISSUE_NUMBER" \
            --repo "$REPO" \
            --json number,title,body \
            | jq '{number, title, body: ((.body // "")[0:4000])}' \
            > "$CURRENT_ISSUE_FILE"

          echo "Prepared duplicate detection input files."
          echo "all_issue_count=$(jq 'length' "$EXISTING_ALL_FILE")"

      # Prompt instructions are intentionally inline in this workflow. The old
      # .github/prompts/issue-deduplicator.txt file is obsolete and removed.
      - id: codex-all
        name: Find duplicates (pass 1, all issues)
        uses: openai/codex-action@0b91f4a2703c23df3102c3f0967d3c6db34eedef # v1
        with:
          openai-api-key: ${{ secrets.CODEX_OPENAI_API_KEY }}
          allow-users: "*"
          prompt: |
            You are an assistant that triages new GitHub issues by identifying potential duplicates.

            You will receive the following JSON files located in the current working directory:
            - `codex-current-issue.json`: JSON object describing the newly created issue (fields: number, title, body).
            - `codex-existing-issues-all.json`: JSON array of recent issues with states, timestamps, and labels.

            Instructions:
            - Compare the current issue against the existing issues to find up to five that appear to describe the same underlying problem or request.
            - Prioritize concrete overlap in symptoms, reproduction details, error signatures, and user intent.
            - Prefer active unresolved issues when confidence is similar.
            - Closed issues can still be valid duplicates if they clearly match.
            - Return fewer matches rather than speculative ones.
            - If confidence is low, return an empty list.
            - Include at most five issue numbers.
            - After analysis, provide a short reason for your decision.

          output-schema: |
            {
              "type": "object",
              "properties": {
                "issues": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "reason": { "type": "string" }
              },
              "required": ["issues", "reason"],
              "additionalProperties": false
            }

      - id: normalize-all
        name: Normalize pass 1 output
        env:
          CODEX_OUTPUT: ${{ steps.codex-all.outputs.final-message }}
          CURRENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
        run: |
          set -eo pipefail

          raw=${CODEX_OUTPUT//$'\r'/}
          parsed=false
          issues='[]'
          reason=''

          if [ -n "$raw" ] && printf '%s' "$raw" | jq -e 'type == "object" and (.issues | type == "array")' >/dev/null 2>&1; then
            parsed=true
            issues=$(printf '%s' "$raw" | jq -c '[.issues[] | tostring]')
            reason=$(printf '%s' "$raw" | jq -r '.reason // ""')
          else
            reason='Pass 1 output was empty or invalid JSON.'
          fi

          filtered=$(jq -cn --argjson issues "$issues" --arg current "$CURRENT_ISSUE_NUMBER" '[
            $issues[]
            | tostring
            | select(. != $current)
          ] | reduce .[] as $issue ([]; if index($issue) then . else . + [$issue] end) | .[:5]')

          has_matches=false
          if [ "$(jq 'length' <<< "$filtered")" -gt 0 ]; then
            has_matches=true
          fi

          echo "Pass 1 parsed: $parsed"
          echo "Pass 1 matches after filtering: $(jq 'length' <<< "$filtered")"
          echo "Pass 1 reason: $reason"

          {
            echo "issues_json=$filtered"
            echo "reason<<EOF"
            echo "$reason"
            echo "EOF"
            echo "has_matches=$has_matches"
          } >> "$GITHUB_OUTPUT"

  gather-duplicates-open:
    name: Identify potential duplicates (open issues fallback)
    # Pass 1 may drop sudo on the runner, so run the fallback in a fresh job.
    needs: gather-duplicates-all
    if: ${{ needs.gather-duplicates-all.result == 'success' && needs.gather-duplicates-all.outputs.has_matches != 'true' }}
    runs-on: ubuntu-latest
    permissions:
      contents: read
    outputs:
      issues_json: ${{ steps.normalize-open.outputs.issues_json }}
      reason: ${{ steps.normalize-open.outputs.reason }}
      has_matches: ${{ steps.normalize-open.outputs.has_matches }}
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

      - name: Prepare Codex inputs
        env:
          GH_TOKEN: ${{ github.token }}
          REPO: ${{ github.repository }}
          ISSUE_NUMBER: ${{ github.event.issue.number }}
        run: |
          set -eo pipefail

          CURRENT_ISSUE_FILE=codex-current-issue.json
          EXISTING_OPEN_FILE=codex-existing-issues-open.json

          gh issue list --repo "$REPO" \
            --json number,title,body,createdAt,updatedAt,state,labels \
            --limit 1000 \
            --state open \
            --search "sort:created-desc" \
            | jq '[.[] | {
                number,
                title,
                body: ((.body // "")[0:4000]),
                createdAt,
                updatedAt,
                state,
                labels: ((.labels // []) | map(.name))
              }]' \
            > "$EXISTING_OPEN_FILE"

          gh issue view "$ISSUE_NUMBER" \
            --repo "$REPO" \
            --json number,title,body \
            | jq '{number, title, body: ((.body // "")[0:4000])}' \
            > "$CURRENT_ISSUE_FILE"

          echo "Prepared fallback duplicate detection input files."
          echo "open_issue_count=$(jq 'length' "$EXISTING_OPEN_FILE")"

      - id: codex-open
        name: Find duplicates (pass 2, open issues)
        uses: openai/codex-action@0b91f4a2703c23df3102c3f0967d3c6db34eedef # v1
        with:
          openai-api-key: ${{ secrets.CODEX_OPENAI_API_KEY }}
          allow-users: "*"
          prompt: |
            You are an assistant that triages new GitHub issues by identifying potential duplicates.

            This is a fallback pass because a broad search did not find convincing matches.

            You will receive the following JSON files located in the current working directory:
            - `codex-current-issue.json`: JSON object describing the newly created issue (fields: number, title, body).
            - `codex-existing-issues-open.json`: JSON array of open issues only.

            Instructions:
            - Search only these active unresolved issues for duplicates of the current issue.
            - Prioritize concrete overlap in symptoms, reproduction details, error signatures, and user intent.
            - Prefer fewer, higher-confidence matches.
            - If confidence is low, return an empty list.
            - Include at most five issue numbers.
            - After analysis, provide a short reason for your decision.

          output-schema: |
            {
              "type": "object",
              "properties": {
                "issues": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                },
                "reason": { "type": "string" }
              },
              "required": ["issues", "reason"],
              "additionalProperties": false
            }

      - id: normalize-open
        name: Normalize pass 2 output
        env:
          CODEX_OUTPUT: ${{ steps.codex-open.outputs.final-message }}
          CURRENT_ISSUE_NUMBER: ${{ github.event.issue.number }}
        run: |
          set -eo pipefail

          raw=${CODEX_OUTPUT//$'\r'/}
          parsed=false
          issues='[]'
          reason=''

          if [ -n "$raw" ] && printf '%s' "$raw" | jq -e 'type == "object" and (.issues | type == "array")' >/dev/null 2>&1; then
            parsed=true
            issues=$(printf '%s' "$raw" | jq -c '[.issues[] | tostring]')
            reason=$(printf '%s' "$raw" | jq -r '.reason // ""')
          else
            reason='Pass 2 output was empty or invalid JSON.'
          fi

          filtered=$(jq -cn --argjson issues "$issues" --arg current "$CURRENT_ISSUE_NUMBER" '[
            $issues[]
            | tostring
            | select(. != $current)
          ] | reduce .[] as $issue ([]; if index($issue) then . else . + [$issue] end) | .[:5]')

          has_matches=false
          if [ "$(jq 'length' <<< "$filtered")" -gt 0 ]; then
            has_matches=true
          fi

          echo "Pass 2 parsed: $parsed"
          echo "Pass 2 matches after filtering: $(jq 'length' <<< "$filtered")"
          echo "Pass 2 reason: $reason"

          {
            echo "issues_json=$filtered"
            echo "reason<<EOF"
            echo "$reason"
            echo "EOF"
            echo "has_matches=$has_matches"
          } >> "$GITHUB_OUTPUT"

  select-final:
    name: Select final duplicate set
    needs:
      - gather-duplicates-all
      - gather-duplicates-open
    if: ${{ always() && needs.gather-duplicates-all.result == 'success' && (needs.gather-duplicates-open.result == 'success' || needs.gather-duplicates-open.result == 'skipped') }}
    runs-on: ubuntu-latest
    permissions:
      contents: read
    outputs:
      codex_output: ${{ steps.select-final.outputs.codex_output }}
    steps:
      - id: select-final
        name: Select final duplicate set
        env:
          PASS1_ISSUES: ${{ needs.gather-duplicates-all.outputs.issues_json }}
          PASS1_REASON: ${{ needs.gather-duplicates-all.outputs.reason }}
          PASS2_ISSUES: ${{ needs.gather-duplicates-open.outputs.issues_json }}
          PASS2_REASON: ${{ needs.gather-duplicates-open.outputs.reason }}
          PASS1_HAS_MATCHES: ${{ needs.gather-duplicates-all.outputs.has_matches }}
          PASS2_HAS_MATCHES: ${{ needs.gather-duplicates-open.outputs.has_matches }}
        run: |
          set -eo pipefail

          selected_issues='[]'
          selected_reason='No plausible duplicates found.'
          selected_pass='none'

          if [ "$PASS1_HAS_MATCHES" = "true" ]; then
            selected_issues=${PASS1_ISSUES:-'[]'}
            selected_reason=${PASS1_REASON:-'Pass 1 found duplicates.'}
            selected_pass='all'
          fi

          if [ "$PASS2_HAS_MATCHES" = "true" ]; then
            selected_issues=${PASS2_ISSUES:-'[]'}
            selected_reason=${PASS2_REASON:-'Pass 2 found duplicates.'}
            selected_pass='open-fallback'
          fi

          final_json=$(jq -cn \
            --argjson issues "$selected_issues" \
            --arg reason "$selected_reason" \
            --arg pass "$selected_pass" \
            '{issues: $issues, reason: $reason, pass: $pass}')

          echo "Final pass used: $selected_pass"
          echo "Final duplicate count: $(jq '.issues | length' <<< "$final_json")"
          echo "Final reason: $(jq -r '.reason' <<< "$final_json")"

          {
            echo "codex_output<<EOF"
            echo "$final_json"
            echo "EOF"
          } >> "$GITHUB_OUTPUT"

  comment-on-issue:
    name: Comment with potential duplicates
    needs: select-final
    if: ${{ always() && needs.select-final.result == 'success' }}
    runs-on: ubuntu-latest
    permissions:
      contents: read
      issues: write
    steps:
      - name: Comment on issue
        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8
        env:
          CODEX_OUTPUT: ${{ needs.select-final.outputs.codex_output }}
        with:
          github-token: ${{ github.token }}
          script: |
            const raw = process.env.CODEX_OUTPUT ?? '';
            let parsed;
            try {
              parsed = JSON.parse(raw);
            } catch (error) {
              core.info(`Codex output was not valid JSON. Raw output: ${raw}`);
              core.info(`Parse error: ${error.message}`);
              return;
            }

            const issues = Array.isArray(parsed?.issues) ? parsed.issues : [];
            const currentIssueNumber = String(context.payload.issue.number);
            const passUsed = typeof parsed?.pass === 'string' ? parsed.pass : 'unknown';
            const reason = typeof parsed?.reason === 'string' ? parsed.reason : '';

            console.log(`Current issue number: ${currentIssueNumber}`);
            console.log(`Pass used: ${passUsed}`);
            if (reason) {
              console.log(`Reason: ${reason}`);
            }
            console.log(issues);

            const filteredIssues = [...new Set(issues.map((value) => String(value)))].filter((value) => value !== currentIssueNumber).slice(0, 5);

            if (filteredIssues.length === 0) {
              core.info('Codex reported no potential duplicates.');
              return;
            }

            const lines = [
              'Potential duplicates detected. Please review them and close your issue if it is a duplicate.',
              '',
              ...filteredIssues.map((value) => `- #${String(value)}`),
              '',
              '*Powered by [Codex Action](https://github.com/openai/codex-action)*'];

            await github.rest.issues.createComment({
              owner: context.repo.owner,
              repo: context.repo.repo,
              issue_number: context.payload.issue.number,
              body: lines.join("\n"),
            });

      - name: Remove codex-deduplicate label
        if: ${{ always() && github.event.action == 'labeled' && github.event.label.name == 'codex-deduplicate' }}
        env:
          GH_TOKEN: ${{ github.token }}
          GH_REPO: ${{ github.repository }}
          ISSUE_NUMBER: ${{ github.event.issue.number }}
        run: |
          gh issue edit "$ISSUE_NUMBER" --remove-label codex-deduplicate || true
          echo "Attempted to remove label: codex-deduplicate"
issue-labeler .github/workflows/issue-labeler.yml
Triggers
issues
Runs on
ubuntu-latest, ubuntu-latest
Jobs
gather-labels, apply-labels
Actions
openai/codex-action
Commands
  • json=${CODEX_OUTPUT//$'\r'/} if [ -z "$json" ]; then echo "Codex produced no output. Skipping label application." exit 0 fi if ! printf '%s' "$json" | jq -e 'type == "object" and (.labels | type == "array")' >/dev/null 2>&1; then echo "Codex output did not include a labels array. Raw output: $json" exit 0 fi labels=$(printf '%s' "$json" | jq -r '.labels[] | tostring') if [ -z "$labels" ]; then echo "Codex returned an empty array. Nothing to do." exit 0 fi cmd=(gh issue edit "$ISSUE_NUMBER") while IFS= read -r label; do cmd+=(--add-label "$label") done <<< "$labels" "${cmd[@]}" || true
  • gh issue edit "$ISSUE_NUMBER" --remove-label codex-label || true echo "Attempted to remove label: codex-label"
View raw YAML
name: Issue Labeler

on:
  issues:
    types:
      - opened
      - labeled

jobs:
  gather-labels:
    name: Generate label suggestions
    # Prevent runs on forks (requires OpenAI API key, wastes Actions minutes)
    if: github.repository == 'openai/codex' && (github.event.action == 'opened' || (github.event.action == 'labeled' && github.event.label.name == 'codex-label'))
    runs-on: ubuntu-latest
    permissions:
      contents: read
    outputs:
      codex_output: ${{ steps.codex.outputs.final-message }}
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

      - id: codex
        uses: openai/codex-action@0b91f4a2703c23df3102c3f0967d3c6db34eedef # v1
        with:
          openai-api-key: ${{ secrets.CODEX_OPENAI_API_KEY }}
          allow-users: "*"
          prompt: |
            You are an assistant that reviews GitHub issues for the repository.

            Your job is to choose the most appropriate labels for the issue described later in this prompt.
            Follow these rules:

            - Add one (and only one) of the following three labels to distinguish the type of issue. Default to "bug" if unsure.
            1. bug — Reproducible defects in Codex products (CLI, VS Code extension, web, auth).
            2. enhancement — Feature requests or usability improvements that ask for new capabilities, better ergonomics, or quality-of-life tweaks.
            3. documentation — Updates or corrections needed in docs/README/config references (broken links, missing examples, outdated keys, clarification requests).

            - If applicable, add one of the following labels to specify which sub-product or product surface the issue relates to.
            1. CLI — the Codex command line interface.
            2. extension — VS Code (or other IDE) extension-specific issues.
            3. app - Issues related to the Codex desktop application.
            4. codex-web — Issues targeting the Codex web UI/Cloud experience.
            5. github-action — Issues with the Codex GitHub action.
            6. iOS — Issues with the Codex iOS app.

            - Additionally add zero or more of the following labels that are relevant to the issue content. Prefer a small set of precise labels over many broad ones.
            1. windows-os — Bugs or friction specific to Windows environments (always when PowerShell is mentioned, path handling, copy/paste, OS-specific auth or tooling failures).
            2. mcp — Topics involving Model Context Protocol servers/clients.
            3. mcp-server — Problems related to the codex mcp-server command, where codex runs as an MCP server.
            4. azure — Problems or requests tied to Azure OpenAI deployments.
            5. model-behavior — Undesirable LLM behavior: forgetting goals, refusing work, hallucinating environment details, quota misreports, or other reasoning/performance anomalies.
            6. code-review — Issues related to the code review feature or functionality.
            7. safety-check - Issues related to cyber risk detection or trusted access verification.
            8. auth - Problems related to authentication, login, or access tokens.
            9. codex-exec - Problems related to the "codex exec" command or functionality.
            10. context-management - Problems related to compaction, context windows, or available context reporting.
            11. custom-model - Problems that involve using custom model providers, local models, or OSS models.
            12. rate-limits - Problems related to token limits, rate limits, or token usage reporting.
            13. sandbox - Issues related to local sandbox environments or tool call approvals to override sandbox restrictions.
            14. tool-calls - Problems related to specific tool call invocations including unexpected errors, failures, or hangs.
            15. TUI - Problems with the terminal user interface (TUI) including keyboard shortcuts, copy & pasting, menus, or screen update issues.

            Issue number: ${{ github.event.issue.number }}

            Issue title:
            ${{ github.event.issue.title }}

            Issue body:
            ${{ github.event.issue.body }}

            Repository full name:
            ${{ github.repository }}

          output-schema: |
            {
              "type": "object",
              "properties": {
                "labels": {
                  "type": "array",
                  "items": {
                    "type": "string"
                  }
                }
              },
              "required": ["labels"],
              "additionalProperties": false
            }

  apply-labels:
    name: Apply labels from Codex output
    needs: gather-labels
    if: ${{ needs.gather-labels.result != 'skipped' }}
    runs-on: ubuntu-latest
    permissions:
      contents: read
      issues: write
    env:
      GH_TOKEN: ${{ github.token }}
      GH_REPO: ${{ github.repository }}
      ISSUE_NUMBER: ${{ github.event.issue.number }}
      CODEX_OUTPUT: ${{ needs.gather-labels.outputs.codex_output }}
    steps:
      - name: Apply labels
        run: |
          json=${CODEX_OUTPUT//$'\r'/}
          if [ -z "$json" ]; then
            echo "Codex produced no output. Skipping label application."
            exit 0
          fi

          if ! printf '%s' "$json" | jq -e 'type == "object" and (.labels | type == "array")' >/dev/null 2>&1; then
            echo "Codex output did not include a labels array. Raw output: $json"
            exit 0
          fi

          labels=$(printf '%s' "$json" | jq -r '.labels[] | tostring')
          if [ -z "$labels" ]; then
            echo "Codex returned an empty array. Nothing to do."
            exit 0
          fi

          cmd=(gh issue edit "$ISSUE_NUMBER")
          while IFS= read -r label; do
            cmd+=(--add-label "$label")
          done <<< "$labels"

          "${cmd[@]}" || true

      - name: Remove codex-label trigger
        if: ${{ always() && github.event.action == 'labeled' && github.event.label.name == 'codex-label' }}
        run: |
          gh issue edit "$ISSUE_NUMBER" --remove-label codex-label || true
          echo "Attempted to remove label: codex-label"
rust-ci matrix .github/workflows/rust-ci.yml
Triggers
pull_request, workflow_dispatch
Runs on
ubuntu-24.04, ubuntu-24.04, ubuntu-24.04, ubuntu-24.04, ${{ matrix.runs_on || matrix.runner }}, ubuntu-24.04
Jobs
changed, general, cargo_shear, argument_comment_lint_package, argument_comment_lint_prebuilt, results
Matrix
include, include.name, include.runner, include.runs_on, include.timeout_minutes, runs_on.group, runs_on.labels→ 30, Linux, Windows, codex-runners, codex-windows-x64, macOS, macos-15-xlarge, ubuntu-24.04, windows-x64
Actions
dtolnay/rust-toolchain, dtolnay/rust-toolchain, taiki-e/install-action, dtolnay/rust-toolchain
Commands
  • set -euo pipefail if [[ "${{ github.event_name }}" == "pull_request" ]]; then BASE_SHA='${{ github.event.pull_request.base.sha }}' HEAD_SHA='${{ github.event.pull_request.head.sha }}' echo "Base SHA: $BASE_SHA" echo "Head SHA: $HEAD_SHA" mapfile -t files < <(git diff --name-only --no-renames "$BASE_SHA" "$HEAD_SHA") else # On manual runs, default to the full fast-PR bundle. files=("codex-rs/force" "tools/argument-comment-lint/force" ".github/force") fi codex=false argument_comment_lint=false argument_comment_lint_package=false workflows=false for f in "${files[@]}"; do [[ $f == codex-rs/* ]] && codex=true [[ $f == codex-rs/* || $f == tools/argument-comment-lint/* || $f == justfile ]] && argument_comment_lint=true [[ $f == tools/argument-comment-lint/* || $f == .github/workflows/rust-ci.yml || $f == .github/workflows/rust-ci-full.yml ]] && argument_comment_lint_package=true [[ $f == .github/* ]] && workflows=true done echo "argument_comment_lint=$argument_comment_lint" >> "$GITHUB_OUTPUT" echo "argument_comment_lint_package=$argument_comment_lint_package" >> "$GITHUB_OUTPUT" echo "codex=$codex" >> "$GITHUB_OUTPUT" echo "workflows=$workflows" >> "$GITHUB_OUTPUT"
  • cargo fmt -- --config imports_granularity=Item --check
  • cargo shear
  • rustup toolchain install nightly-2025-09-18 \ --profile minimal \ --component llvm-tools-preview \ --component rustc-dev \ --component rust-src \ --no-self-update rustup default nightly-2025-09-18
  • cargo install --locked cargo-dylint dylint-link
  • python3 -m py_compile tools/argument-comment-lint/wrapper_common.py tools/argument-comment-lint/run.py tools/argument-comment-lint/run-prebuilt-linter.py tools/argument-comment-lint/test_wrapper_common.py
  • python3 -m unittest discover -s tools/argument-comment-lint -p 'test_*.py'
  • cargo test
View raw YAML
name: rust-ci
on:
  pull_request: {}
  workflow_dispatch:

jobs:
  # --- Detect what changed so the fast PR workflow only runs relevant jobs ----
  changed:
    name: Detect changed areas
    runs-on: ubuntu-24.04
    outputs:
      argument_comment_lint: ${{ steps.detect.outputs.argument_comment_lint }}
      argument_comment_lint_package: ${{ steps.detect.outputs.argument_comment_lint_package }}
      codex: ${{ steps.detect.outputs.codex }}
      workflows: ${{ steps.detect.outputs.workflows }}
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
        with:
          fetch-depth: 0
      - name: Detect changed paths (no external action)
        id: detect
        shell: bash
        run: |
          set -euo pipefail

          if [[ "${{ github.event_name }}" == "pull_request" ]]; then
            BASE_SHA='${{ github.event.pull_request.base.sha }}'
            HEAD_SHA='${{ github.event.pull_request.head.sha }}'
            echo "Base SHA: $BASE_SHA"
            echo "Head SHA: $HEAD_SHA"
            mapfile -t files < <(git diff --name-only --no-renames "$BASE_SHA" "$HEAD_SHA")
          else
            # On manual runs, default to the full fast-PR bundle.
            files=("codex-rs/force" "tools/argument-comment-lint/force" ".github/force")
          fi

          codex=false
          argument_comment_lint=false
          argument_comment_lint_package=false
          workflows=false
          for f in "${files[@]}"; do
            [[ $f == codex-rs/* ]] && codex=true
            [[ $f == codex-rs/* || $f == tools/argument-comment-lint/* || $f == justfile ]] && argument_comment_lint=true
            [[ $f == tools/argument-comment-lint/* || $f == .github/workflows/rust-ci.yml || $f == .github/workflows/rust-ci-full.yml ]] && argument_comment_lint_package=true
            [[ $f == .github/* ]] && workflows=true
          done

          echo "argument_comment_lint=$argument_comment_lint" >> "$GITHUB_OUTPUT"
          echo "argument_comment_lint_package=$argument_comment_lint_package" >> "$GITHUB_OUTPUT"
          echo "codex=$codex" >> "$GITHUB_OUTPUT"
          echo "workflows=$workflows" >> "$GITHUB_OUTPUT"

  # --- Fast Cargo-native PR checks -------------------------------------------
  general:
    name: Format / etc
    runs-on: ubuntu-24.04
    needs: changed
    if: ${{ needs.changed.outputs.codex == 'true' || needs.changed.outputs.workflows == 'true' }}
    defaults:
      run:
        working-directory: codex-rs
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
      - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
        with:
          components: rustfmt
      - name: cargo fmt
        run: cargo fmt -- --config imports_granularity=Item --check

  cargo_shear:
    name: cargo shear
    runs-on: ubuntu-24.04
    needs: changed
    if: ${{ needs.changed.outputs.codex == 'true' || needs.changed.outputs.workflows == 'true' }}
    defaults:
      run:
        working-directory: codex-rs
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
      - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
      - uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
        with:
          tool: cargo-shear
          version: 1.5.1
      - name: cargo shear
        run: cargo shear

  argument_comment_lint_package:
    name: Argument comment lint package
    runs-on: ubuntu-24.04
    needs: changed
    if: ${{ needs.changed.outputs.argument_comment_lint_package == 'true' }}
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
      - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
      - name: Install nightly argument-comment-lint toolchain
        shell: bash
        run: |
          rustup toolchain install nightly-2025-09-18 \
            --profile minimal \
            --component llvm-tools-preview \
            --component rustc-dev \
            --component rust-src \
            --no-self-update
          rustup default nightly-2025-09-18
      - name: Cache cargo-dylint tooling
        id: cargo_dylint_cache
        uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
        with:
          path: |
            ~/.cargo/bin/cargo-dylint
            ~/.cargo/bin/dylint-link
            ~/.cargo/registry/index
            ~/.cargo/registry/cache
            ~/.cargo/git/db
          key: argument-comment-lint-${{ runner.os }}-${{ hashFiles('tools/argument-comment-lint/Cargo.lock', 'tools/argument-comment-lint/rust-toolchain', '.github/workflows/rust-ci.yml', '.github/workflows/rust-ci-full.yml') }}
      - name: Install cargo-dylint tooling
        if: ${{ steps.cargo_dylint_cache.outputs.cache-hit != 'true' }}
        run: cargo install --locked cargo-dylint dylint-link
      - name: Check Python wrapper syntax
        run: python3 -m py_compile tools/argument-comment-lint/wrapper_common.py tools/argument-comment-lint/run.py tools/argument-comment-lint/run-prebuilt-linter.py tools/argument-comment-lint/test_wrapper_common.py
      - name: Test Python wrapper helpers
        run: python3 -m unittest discover -s tools/argument-comment-lint -p 'test_*.py'
      - name: Test argument comment lint package
        working-directory: tools/argument-comment-lint
        run: cargo test

  argument_comment_lint_prebuilt:
    name: Argument comment lint - ${{ matrix.name }}
    runs-on: ${{ matrix.runs_on || matrix.runner }}
    timeout-minutes: ${{ matrix.timeout_minutes }}
    needs: changed
    if: ${{ needs.changed.outputs.argument_comment_lint == 'true' || needs.changed.outputs.workflows == 'true' }}
    strategy:
      fail-fast: false
      matrix:
        include:
          - name: Linux
            runner: ubuntu-24.04
            timeout_minutes: 30
          - name: macOS
            runner: macos-15-xlarge
            timeout_minutes: 30
          - name: Windows
            runner: windows-x64
            timeout_minutes: 30
            runs_on:
              group: codex-runners
              labels: codex-windows-x64
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
      - uses: ./.github/actions/setup-bazel-ci
        with:
          target: ${{ runner.os }}
          install-test-prereqs: true
      - name: Install Linux sandbox build dependencies
        if: ${{ runner.os == 'Linux' }}
        shell: bash
        run: |
          sudo DEBIAN_FRONTEND=noninteractive apt-get update
          sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev
      - name: Install nightly argument-comment-lint toolchain
        if: ${{ runner.os == 'Windows' }}
        shell: bash
        run: |
          rustup toolchain install nightly-2025-09-18 \
            --profile minimal \
            --component llvm-tools-preview \
            --component rustc-dev \
            --component rust-src \
            --no-self-update
          rustup default nightly-2025-09-18
      - name: Run argument comment lint on codex-rs via Bazel
        if: ${{ runner.os != 'Windows' }}
        env:
          BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
        shell: bash
        run: |
          ./.github/scripts/run-bazel-ci.sh \
            -- \
            build \
            --config=argument-comment-lint \
            --keep_going \
            --build_metadata=COMMIT_SHA=${GITHUB_SHA} \
            -- \
            //codex-rs/...
      - name: Run argument comment lint on codex-rs via packaged wrapper
        if: ${{ runner.os == 'Windows' }}
        shell: bash
        run: python3 ./tools/argument-comment-lint/run-prebuilt-linter.py

  # --- Gatherer job that you mark as the ONLY required status -----------------
  results:
    name: CI results (required)
    needs:
      [
        changed,
        general,
        cargo_shear,
        argument_comment_lint_package,
        argument_comment_lint_prebuilt,
      ]
    if: always()
    runs-on: ubuntu-24.04
    steps:
      - name: Summarize
        shell: bash
        run: |
          echo "argpkg : ${{ needs.argument_comment_lint_package.result }}"
          echo "arglint: ${{ needs.argument_comment_lint_prebuilt.result }}"
          echo "general: ${{ needs.general.result }}"
          echo "shear  : ${{ needs.cargo_shear.result }}"

          # If nothing relevant changed (PR touching only root README, etc.),
          # declare success regardless of other jobs.
          if [[ '${{ needs.changed.outputs.argument_comment_lint }}' != 'true' && '${{ needs.changed.outputs.codex }}' != 'true' && '${{ needs.changed.outputs.workflows }}' != 'true' ]]; then
            echo 'No relevant changes -> CI not required.'
            exit 0
          fi

          if [[ '${{ needs.changed.outputs.argument_comment_lint_package }}' == 'true' ]]; then
            [[ '${{ needs.argument_comment_lint_package.result }}' == 'success' ]] || { echo 'argument_comment_lint_package failed'; exit 1; }
          fi

          if [[ '${{ needs.changed.outputs.argument_comment_lint }}' == 'true' || '${{ needs.changed.outputs.workflows }}' == 'true' ]]; then
            [[ '${{ needs.argument_comment_lint_prebuilt.result }}' == 'success' ]] || { echo 'argument_comment_lint_prebuilt failed'; exit 1; }
          fi

          if [[ '${{ needs.changed.outputs.codex }}' == 'true' || '${{ needs.changed.outputs.workflows }}' == 'true' ]]; then
            [[ '${{ needs.general.result }}' == 'success' ]] || { echo 'general failed'; exit 1; }
            [[ '${{ needs.cargo_shear.result }}' == 'success' ]] || { echo 'cargo_shear failed'; exit 1; }
          fi
rust-ci-full matrix .github/workflows/rust-ci-full.yml
Triggers
push, workflow_dispatch
Runs on
ubuntu-24.04, ubuntu-24.04, ubuntu-24.04, ${{ matrix.runs_on || matrix.runner }}, ${{ matrix.runs_on || matrix.runner }}, ${{ matrix.runs_on || matrix.runner }}, ubuntu-24.04
Jobs
general, cargo_shear, argument_comment_lint_package, argument_comment_lint_prebuilt, lint_build, tests, results
Matrix
include, include.name, include.profile, include.remote_env, include.runner, include.runs_on, include.target, runs_on.group, runs_on.labels→ Linux, Windows, aarch64-apple-darwin, aarch64-pc-windows-msvc, aarch64-unknown-linux-gnu, aarch64-unknown-linux-musl, codex-linux-arm64, codex-linux-x64, codex-runners, codex-windows-arm64, codex-windows-x64, dev, macOS, macos-15-xlarge, release, true, ubuntu-24.04, ubuntu-24.04-arm, windows-arm64, windows-x64, x86_64-apple-darwin, x86_64-pc-windows-msvc, x86_64-unknown-linux-gnu, x86_64-unknown-linux-musl
Actions
dtolnay/rust-toolchain, dtolnay/rust-toolchain, taiki-e/install-action, dtolnay/rust-toolchain, dtolnay/rust-toolchain, taiki-e/install-action, mlugg/setup-zig, taiki-e/install-action, facebook/install-dotslash, dtolnay/rust-toolchain, taiki-e/install-action, taiki-e/install-action
Commands
  • cargo fmt -- --config imports_granularity=Item --check
  • cargo shear
  • cargo install --locked cargo-dylint dylint-link
  • python3 -m py_compile tools/argument-comment-lint/wrapper_common.py tools/argument-comment-lint/run.py tools/argument-comment-lint/run-prebuilt-linter.py tools/argument-comment-lint/test_wrapper_common.py
  • python3 -m unittest discover -s tools/argument-comment-lint -p 'test_*.py'
  • cargo test
  • sudo DEBIAN_FRONTEND=noninteractive apt-get update sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev
  • rustup toolchain install nightly-2025-09-18 \ --profile minimal \ --component llvm-tools-preview \ --component rustc-dev \ --component rust-src \ --no-self-update rustup default nightly-2025-09-18
View raw YAML
name: rust-ci-full
on:
  push:
    branches:
      - main
  workflow_dispatch:

# CI builds in debug (dev) for faster signal.

jobs:
  # --- CI that doesn't need specific targets ---------------------------------
  general:
    name: Format / etc
    runs-on: ubuntu-24.04
    defaults:
      run:
        working-directory: codex-rs
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
      - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
        with:
          components: rustfmt
      - name: cargo fmt
        run: cargo fmt -- --config imports_granularity=Item --check

  cargo_shear:
    name: cargo shear
    runs-on: ubuntu-24.04
    defaults:
      run:
        working-directory: codex-rs
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
      - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
      - uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
        with:
          tool: cargo-shear
          version: 1.5.1
      - name: cargo shear
        run: cargo shear

  argument_comment_lint_package:
    name: Argument comment lint package
    runs-on: ubuntu-24.04
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
      - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
        with:
          toolchain: nightly-2025-09-18
          components: llvm-tools-preview, rustc-dev, rust-src
      - name: Cache cargo-dylint tooling
        id: cargo_dylint_cache
        uses: actions/cache@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
        with:
          path: |
            ~/.cargo/bin/cargo-dylint
            ~/.cargo/bin/dylint-link
            ~/.cargo/registry/index
            ~/.cargo/registry/cache
            ~/.cargo/git/db
          key: argument-comment-lint-${{ runner.os }}-${{ hashFiles('tools/argument-comment-lint/Cargo.lock', 'tools/argument-comment-lint/rust-toolchain', '.github/workflows/rust-ci.yml', '.github/workflows/rust-ci-full.yml') }}
      - name: Install cargo-dylint tooling
        if: ${{ steps.cargo_dylint_cache.outputs.cache-hit != 'true' }}
        run: cargo install --locked cargo-dylint dylint-link
      - name: Check Python wrapper syntax
        run: python3 -m py_compile tools/argument-comment-lint/wrapper_common.py tools/argument-comment-lint/run.py tools/argument-comment-lint/run-prebuilt-linter.py tools/argument-comment-lint/test_wrapper_common.py
      - name: Test Python wrapper helpers
        run: python3 -m unittest discover -s tools/argument-comment-lint -p 'test_*.py'
      - name: Test argument comment lint package
        working-directory: tools/argument-comment-lint
        run: cargo test

  argument_comment_lint_prebuilt:
    name: Argument comment lint - ${{ matrix.name }}
    runs-on: ${{ matrix.runs_on || matrix.runner }}
    timeout-minutes: 30
    strategy:
      fail-fast: false
      matrix:
        include:
          - name: Linux
            runner: ubuntu-24.04
          - name: macOS
            runner: macos-15-xlarge
          - name: Windows
            runner: windows-x64
            runs_on:
              group: codex-runners
              labels: codex-windows-x64
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
      - uses: ./.github/actions/setup-bazel-ci
        with:
          target: ${{ runner.os }}
          install-test-prereqs: true
      - name: Install Linux sandbox build dependencies
        if: ${{ runner.os == 'Linux' }}
        shell: bash
        run: |
          sudo DEBIAN_FRONTEND=noninteractive apt-get update
          sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev
      - name: Install nightly argument-comment-lint toolchain
        if: ${{ runner.os == 'Windows' }}
        shell: bash
        run: |
          rustup toolchain install nightly-2025-09-18 \
            --profile minimal \
            --component llvm-tools-preview \
            --component rustc-dev \
            --component rust-src \
            --no-self-update
          rustup default nightly-2025-09-18
      - name: Run argument comment lint on codex-rs via Bazel
        if: ${{ runner.os != 'Windows' }}
        env:
          BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
        shell: bash
        run: |
          ./.github/scripts/run-bazel-ci.sh \
            -- \
            build \
            --config=argument-comment-lint \
            --keep_going \
            --build_metadata=COMMIT_SHA=${GITHUB_SHA} \
            -- \
            //codex-rs/...
      - name: Run argument comment lint on codex-rs via packaged wrapper
        if: ${{ runner.os == 'Windows' }}
        shell: bash
        run: python3 ./tools/argument-comment-lint/run-prebuilt-linter.py

  # --- CI to validate on different os/targets --------------------------------
  lint_build:
    name: Lint/Build — ${{ matrix.runner }} - ${{ matrix.target }}${{ matrix.profile == 'release' && ' (release)' || '' }}
    runs-on: ${{ matrix.runs_on || matrix.runner }}
    timeout-minutes: 30
    defaults:
      run:
        working-directory: codex-rs
    env:
      # Speed up repeated builds across CI runs by caching compiled objects, except on
      # arm64 macOS runners cross-targeting x86_64 where ring/cc-rs can produce
      # mixed-architecture archives under sccache.
      USE_SCCACHE: ${{ (startsWith(matrix.runner, 'windows') || (matrix.runner == 'macos-15-xlarge' && matrix.target == 'x86_64-apple-darwin')) && 'false' || 'true' }}
      CARGO_INCREMENTAL: "0"
      SCCACHE_CACHE_SIZE: 10G
      # In rust-ci, representative release-profile checks use thin LTO for faster feedback.
      CARGO_PROFILE_RELEASE_LTO: ${{ matrix.profile == 'release' && 'thin' || 'fat' }}

    strategy:
      fail-fast: false
      matrix:
        include:
          - runner: macos-15-xlarge
            target: aarch64-apple-darwin
            profile: dev
          - runner: macos-15-xlarge
            target: x86_64-apple-darwin
            profile: dev
          - runner: ubuntu-24.04
            target: x86_64-unknown-linux-musl
            profile: dev
            runs_on:
              group: codex-runners
              labels: codex-linux-x64
          - runner: ubuntu-24.04
            target: x86_64-unknown-linux-gnu
            profile: dev
            runs_on:
              group: codex-runners
              labels: codex-linux-x64
          - runner: ubuntu-24.04-arm
            target: aarch64-unknown-linux-musl
            profile: dev
            runs_on:
              group: codex-runners
              labels: codex-linux-arm64
          - runner: ubuntu-24.04-arm
            target: aarch64-unknown-linux-gnu
            profile: dev
            runs_on:
              group: codex-runners
              labels: codex-linux-arm64
          - runner: windows-x64
            target: x86_64-pc-windows-msvc
            profile: dev
            runs_on:
              group: codex-runners
              labels: codex-windows-x64
          - runner: windows-arm64
            target: aarch64-pc-windows-msvc
            profile: dev
            runs_on:
              group: codex-runners
              labels: codex-windows-arm64

          # Also run representative release builds on Mac and Linux because
          # there could be release-only build errors we want to catch.
          # Hopefully this also pre-populates the build cache to speed up
          # releases.
          - runner: macos-15-xlarge
            target: aarch64-apple-darwin
            profile: release
          - runner: ubuntu-24.04
            target: x86_64-unknown-linux-musl
            profile: release
            runs_on:
              group: codex-runners
              labels: codex-linux-x64
          - runner: ubuntu-24.04-arm
            target: aarch64-unknown-linux-musl
            profile: release
            runs_on:
              group: codex-runners
              labels: codex-linux-arm64
          - runner: windows-x64
            target: x86_64-pc-windows-msvc
            profile: release
            runs_on:
              group: codex-runners
              labels: codex-windows-x64
          - runner: windows-arm64
            target: aarch64-pc-windows-msvc
            profile: release
            runs_on:
              group: codex-runners
              labels: codex-windows-arm64

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
      - name: Install Linux build dependencies
        if: ${{ runner.os == 'Linux' }}
        shell: bash
        run: |
          set -euo pipefail
          if command -v apt-get >/dev/null 2>&1; then
            sudo apt-get update -y
            packages=(pkg-config libcap-dev)
            if [[ "${{ matrix.target }}" == 'x86_64-unknown-linux-musl' || "${{ matrix.target }}" == 'aarch64-unknown-linux-musl' ]]; then
              packages+=(libubsan1)
            fi
            sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends "${packages[@]}"
          fi
      - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
        with:
          targets: ${{ matrix.target }}
          components: clippy

      - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
        name: Use hermetic Cargo home (musl)
        shell: bash
        run: |
          set -euo pipefail
          cargo_home="${GITHUB_WORKSPACE}/.cargo-home"
          mkdir -p "${cargo_home}/bin"
          echo "CARGO_HOME=${cargo_home}" >> "$GITHUB_ENV"
          echo "${cargo_home}/bin" >> "$GITHUB_PATH"
          : > "${cargo_home}/config.toml"

      - name: Compute lockfile hash
        id: lockhash
        working-directory: codex-rs
        shell: bash
        run: |
          set -euo pipefail
          echo "hash=$(sha256sum Cargo.lock | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
          echo "toolchain_hash=$(sha256sum rust-toolchain.toml | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"

      # Explicit cache restore: split cargo home vs target, so we can
      # avoid caching the large target dir on the gnu-dev job.
      - name: Restore cargo home cache
        id: cache_cargo_home_restore
        uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
        with:
          path: |
            ~/.cargo/bin/
            ~/.cargo/registry/index/
            ~/.cargo/registry/cache/
            ~/.cargo/git/db/
            ${{ github.workspace }}/.cargo-home/bin/
            ${{ github.workspace }}/.cargo-home/registry/index/
            ${{ github.workspace }}/.cargo-home/registry/cache/
            ${{ github.workspace }}/.cargo-home/git/db/
          key: cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ steps.lockhash.outputs.toolchain_hash }}
          restore-keys: |
            cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-

      # Install and restore sccache cache
      - name: Install sccache
        if: ${{ env.USE_SCCACHE == 'true' }}
        uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
        with:
          tool: sccache
          version: 0.7.5

      - name: Configure sccache backend
        if: ${{ env.USE_SCCACHE == 'true' }}
        shell: bash
        run: |
          set -euo pipefail
          if [[ -n "${ACTIONS_CACHE_URL:-}" && -n "${ACTIONS_RUNTIME_TOKEN:-}" ]]; then
            echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV"
            echo "Using sccache GitHub backend"
          else
            echo "SCCACHE_GHA_ENABLED=false" >> "$GITHUB_ENV"
            echo "SCCACHE_DIR=${{ github.workspace }}/.sccache" >> "$GITHUB_ENV"
            echo "Using sccache local disk + actions/cache fallback"
          fi

      - name: Enable sccache wrapper
        if: ${{ env.USE_SCCACHE == 'true' }}
        shell: bash
        run: echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV"

      - name: Restore sccache cache (fallback)
        if: ${{ env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true' }}
        id: cache_sccache_restore
        uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
        with:
          path: ${{ github.workspace }}/.sccache/
          key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
          restore-keys: |
            sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-
            sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-

      - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
        name: Disable sccache wrapper (musl)
        shell: bash
        run: |
          set -euo pipefail
          echo "RUSTC_WRAPPER=" >> "$GITHUB_ENV"
          echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV"

      - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
        name: Prepare APT cache directories (musl)
        shell: bash
        run: |
          set -euo pipefail
          sudo mkdir -p /var/cache/apt/archives /var/lib/apt/lists
          sudo chown -R "$USER:$USER" /var/cache/apt /var/lib/apt/lists

      - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
        name: Restore APT cache (musl)
        id: cache_apt_restore
        uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
        with:
          path: |
            /var/cache/apt
          key: apt-${{ matrix.runner }}-${{ matrix.target }}-v1

      - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
        name: Install Zig
        uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2
        with:
          version: 0.14.0

      - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
        name: Install musl build tools
        env:
          DEBIAN_FRONTEND: noninteractive
          TARGET: ${{ matrix.target }}
          APT_UPDATE_ARGS: -o Acquire::Retries=3
          APT_INSTALL_ARGS: --no-install-recommends
        shell: bash
        run: bash "${GITHUB_WORKSPACE}/.github/scripts/install-musl-build-tools.sh"

      - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
        name: Configure rustc UBSan wrapper (musl host)
        shell: bash
        run: |
          set -euo pipefail
          ubsan=""
          if command -v ldconfig >/dev/null 2>&1; then
            ubsan="$(ldconfig -p | grep -m1 'libubsan\.so\.1' | sed -E 's/.*=> (.*)$/\1/')"
          fi
          wrapper_root="${RUNNER_TEMP:-/tmp}"
          wrapper="${wrapper_root}/rustc-ubsan-wrapper"
          cat > "${wrapper}" <<EOF
          #!/usr/bin/env bash
          set -euo pipefail
          if [[ -n "${ubsan}" ]]; then
            export LD_PRELOAD="${ubsan}\${LD_PRELOAD:+:\${LD_PRELOAD}}"
          fi
          exec "\$1" "\${@:2}"
          EOF
          chmod +x "${wrapper}"
          echo "RUSTC_WRAPPER=${wrapper}" >> "$GITHUB_ENV"
          echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV"

      - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
        name: Clear sanitizer flags (musl)
        shell: bash
        run: |
          set -euo pipefail
          # Clear global Rust flags so host/proc-macro builds don't pull in UBSan.
          echo "RUSTFLAGS=" >> "$GITHUB_ENV"
          echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV"
          echo "RUSTDOCFLAGS=" >> "$GITHUB_ENV"
          # Override any runner-level Cargo config rustflags as well.
          echo "CARGO_BUILD_RUSTFLAGS=" >> "$GITHUB_ENV"
          echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV"
          echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV"
          echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV"
          echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV"

          sanitize_flags() {
            local input="$1"
            input="${input//-fsanitize=undefined/}"
            input="${input//-fno-sanitize-recover=undefined/}"
            input="${input//-fno-sanitize-trap=undefined/}"
            echo "$input"
          }

          cflags="$(sanitize_flags "${CFLAGS-}")"
          cxxflags="$(sanitize_flags "${CXXFLAGS-}")"
          echo "CFLAGS=${cflags}" >> "$GITHUB_ENV"
          echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV"

      - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl' }}
        name: Configure musl rusty_v8 artifact overrides
        env:
          TARGET: ${{ matrix.target }}
        shell: bash
        run: |
          set -euo pipefail
          version="$(python3 "${GITHUB_WORKSPACE}/.github/scripts/rusty_v8_bazel.py" resolved-v8-crate-version)"
          release_tag="rusty-v8-v${version}"
          base_url="https://github.com/openai/codex/releases/download/${release_tag}"
          archive="https://github.com/openai/codex/releases/download/rusty-v8-v${version}/librusty_v8_release_${TARGET}.a.gz"
          binding_dir="${RUNNER_TEMP}/rusty_v8"
          binding_path="${binding_dir}/src_binding_release_${TARGET}.rs"
          mkdir -p "${binding_dir}"
          curl -fsSL "${base_url}/src_binding_release_${TARGET}.rs" -o "${binding_path}"
          echo "RUSTY_V8_ARCHIVE=${archive}" >> "$GITHUB_ENV"
          echo "RUSTY_V8_SRC_BINDING_PATH=${binding_path}" >> "$GITHUB_ENV"

      - name: Install cargo-chef
        if: ${{ matrix.profile == 'release' }}
        uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
        with:
          tool: cargo-chef
          version: 0.1.71

      - name: Pre-warm dependency cache (cargo-chef)
        if: ${{ matrix.profile == 'release' }}
        shell: bash
        run: |
          set -euo pipefail
          RECIPE="${RUNNER_TEMP}/chef-recipe.json"
          cargo chef prepare --recipe-path "$RECIPE"
          cargo chef cook --recipe-path "$RECIPE" --target ${{ matrix.target }} --release --all-features

      - name: cargo clippy
        run: cargo clippy --target ${{ matrix.target }} --all-features --tests --profile ${{ matrix.profile }} --timings -- -D warnings

      - name: Upload Cargo timings (clippy)
        if: always()
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
        with:
          name: cargo-timings-rust-ci-clippy-${{ matrix.target }}-${{ matrix.profile }}
          path: codex-rs/target/**/cargo-timings/cargo-timing.html
          if-no-files-found: warn

      # Save caches explicitly; make non-fatal so cache packaging
      # never fails the overall job. Only save when key wasn't hit.
      - name: Save cargo home cache
        if: always() && !cancelled() && steps.cache_cargo_home_restore.outputs.cache-hit != 'true'
        continue-on-error: true
        uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
        with:
          path: |
            ~/.cargo/bin/
            ~/.cargo/registry/index/
            ~/.cargo/registry/cache/
            ~/.cargo/git/db/
            ${{ github.workspace }}/.cargo-home/bin/
            ${{ github.workspace }}/.cargo-home/registry/index/
            ${{ github.workspace }}/.cargo-home/registry/cache/
            ${{ github.workspace }}/.cargo-home/git/db/
          key: cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ steps.lockhash.outputs.toolchain_hash }}

      - name: Save sccache cache (fallback)
        if: always() && !cancelled() && env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true'
        continue-on-error: true
        uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
        with:
          path: ${{ github.workspace }}/.sccache/
          key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}

      - name: sccache stats
        if: always() && env.USE_SCCACHE == 'true'
        continue-on-error: true
        run: sccache --show-stats || true

      - name: sccache summary
        if: always() && env.USE_SCCACHE == 'true'
        shell: bash
        run: |
          {
            echo "### sccache stats — ${{ matrix.target }} (${{ matrix.profile }})";
            echo;
            echo '```';
            sccache --show-stats || true;
            echo '```';
          } >> "$GITHUB_STEP_SUMMARY"

      - name: Save APT cache (musl)
        if: always() && !cancelled() && (matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl') && steps.cache_apt_restore.outputs.cache-hit != 'true'
        continue-on-error: true
        uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
        with:
          path: |
            /var/cache/apt
          key: apt-${{ matrix.runner }}-${{ matrix.target }}-v1

  tests:
    name: Tests — ${{ matrix.runner }} - ${{ matrix.target }}${{ matrix.remote_env == 'true' && ' (remote)' || '' }}
    runs-on: ${{ matrix.runs_on || matrix.runner }}
    # Perhaps we can bring this back down to 30m once we finish the cutover
    # from tui_app_server/ to tui/. Incidentally, windows-arm64 was the main
    # offender for exceeding the timeout.
    timeout-minutes: 45
    defaults:
      run:
        working-directory: codex-rs
    env:
      # Speed up repeated builds across CI runs by caching compiled objects, except on
      # arm64 macOS runners cross-targeting x86_64 where ring/cc-rs can produce
      # mixed-architecture archives under sccache.
      USE_SCCACHE: ${{ (startsWith(matrix.runner, 'windows') || (matrix.runner == 'macos-15-xlarge' && matrix.target == 'x86_64-apple-darwin')) && 'false' || 'true' }}
      CARGO_INCREMENTAL: "0"
      SCCACHE_CACHE_SIZE: 10G

    strategy:
      fail-fast: false
      matrix:
        include:
          - runner: macos-15-xlarge
            target: aarch64-apple-darwin
            profile: dev
          - runner: ubuntu-24.04
            target: x86_64-unknown-linux-gnu
            profile: dev
            remote_env: "true"
            runs_on:
              group: codex-runners
              labels: codex-linux-x64
          - runner: ubuntu-24.04-arm
            target: aarch64-unknown-linux-gnu
            profile: dev
            runs_on:
              group: codex-runners
              labels: codex-linux-arm64
          - runner: windows-x64
            target: x86_64-pc-windows-msvc
            profile: dev
            runs_on:
              group: codex-runners
              labels: codex-windows-x64
          - runner: windows-arm64
            target: aarch64-pc-windows-msvc
            profile: dev
            runs_on:
              group: codex-runners
              labels: codex-windows-arm64

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
      - name: Set up Node.js for js_repl tests
        uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
        with:
          node-version-file: codex-rs/node-version.txt
      - name: Install Linux build dependencies
        if: ${{ runner.os == 'Linux' }}
        shell: bash
        run: |
          set -euo pipefail
          if command -v apt-get >/dev/null 2>&1; then
            sudo apt-get update -y
            sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev
          fi

      # Some integration tests rely on DotSlash being installed.
      # See https://github.com/openai/codex/pull/7617.
      - name: Install DotSlash
        uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2

      - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
        with:
          targets: ${{ matrix.target }}

      - name: Compute lockfile hash
        id: lockhash
        working-directory: codex-rs
        shell: bash
        run: |
          set -euo pipefail
          echo "hash=$(sha256sum Cargo.lock | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"
          echo "toolchain_hash=$(sha256sum rust-toolchain.toml | cut -d' ' -f1)" >> "$GITHUB_OUTPUT"

      - name: Restore cargo home cache
        id: cache_cargo_home_restore
        uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
        with:
          path: |
            ~/.cargo/bin/
            ~/.cargo/registry/index/
            ~/.cargo/registry/cache/
            ~/.cargo/git/db/
          key: cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ steps.lockhash.outputs.toolchain_hash }}
          restore-keys: |
            cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-

      - name: Install sccache
        if: ${{ env.USE_SCCACHE == 'true' }}
        uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
        with:
          tool: sccache
          version: 0.7.5

      - name: Configure sccache backend
        if: ${{ env.USE_SCCACHE == 'true' }}
        shell: bash
        run: |
          set -euo pipefail
          if [[ -n "${ACTIONS_CACHE_URL:-}" && -n "${ACTIONS_RUNTIME_TOKEN:-}" ]]; then
            echo "SCCACHE_GHA_ENABLED=true" >> "$GITHUB_ENV"
            echo "Using sccache GitHub backend"
          else
            echo "SCCACHE_GHA_ENABLED=false" >> "$GITHUB_ENV"
            echo "SCCACHE_DIR=${{ github.workspace }}/.sccache" >> "$GITHUB_ENV"
            echo "Using sccache local disk + actions/cache fallback"
          fi

      - name: Enable sccache wrapper
        if: ${{ env.USE_SCCACHE == 'true' }}
        shell: bash
        run: echo "RUSTC_WRAPPER=sccache" >> "$GITHUB_ENV"

      - name: Restore sccache cache (fallback)
        if: ${{ env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true' }}
        id: cache_sccache_restore
        uses: actions/cache/restore@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
        with:
          path: ${{ github.workspace }}/.sccache/
          key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}
          restore-keys: |
            sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-
            sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-

      - uses: taiki-e/install-action@44c6d64aa62cd779e873306675c7a58e86d6d532 # v2
        with:
          tool: nextest
          version: 0.9.103

      - name: Enable unprivileged user namespaces (Linux)
        if: runner.os == 'Linux'
        run: |
          # Required for bubblewrap to work on Linux CI runners.
          sudo sysctl -w kernel.unprivileged_userns_clone=1
          # Ubuntu 24.04+ can additionally gate unprivileged user namespaces
          # behind AppArmor.
          if sudo sysctl -a 2>/dev/null | grep -q '^kernel.apparmor_restrict_unprivileged_userns'; then
            sudo sysctl -w kernel.apparmor_restrict_unprivileged_userns=0
          fi

      - name: Set up remote test env (Docker)
        if: ${{ runner.os == 'Linux' && matrix.remote_env == 'true' }}
        shell: bash
        run: |
          set -euo pipefail
          export CODEX_TEST_REMOTE_ENV_CONTAINER_NAME=codex-remote-test-env
          source "${GITHUB_WORKSPACE}/scripts/test-remote-env.sh"
          echo "CODEX_TEST_REMOTE_ENV=${CODEX_TEST_REMOTE_ENV}" >> "$GITHUB_ENV"

      - name: tests
        id: test
        run: cargo nextest run --all-features --no-fail-fast --target ${{ matrix.target }} --cargo-profile ci-test --timings
        env:
          RUST_BACKTRACE: 1
          NEXTEST_STATUS_LEVEL: leak

      - name: Upload Cargo timings (nextest)
        if: always()
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
        with:
          name: cargo-timings-rust-ci-nextest-${{ matrix.target }}-${{ matrix.profile }}
          path: codex-rs/target/**/cargo-timings/cargo-timing.html
          if-no-files-found: warn

      - name: Save cargo home cache
        if: always() && !cancelled() && steps.cache_cargo_home_restore.outputs.cache-hit != 'true'
        continue-on-error: true
        uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
        with:
          path: |
            ~/.cargo/bin/
            ~/.cargo/registry/index/
            ~/.cargo/registry/cache/
            ~/.cargo/git/db/
          key: cargo-home-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ steps.lockhash.outputs.toolchain_hash }}

      - name: Save sccache cache (fallback)
        if: always() && !cancelled() && env.USE_SCCACHE == 'true' && env.SCCACHE_GHA_ENABLED != 'true'
        continue-on-error: true
        uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # v5
        with:
          path: ${{ github.workspace }}/.sccache/
          key: sccache-${{ matrix.runner }}-${{ matrix.target }}-${{ matrix.profile }}-${{ steps.lockhash.outputs.hash }}-${{ github.run_id }}

      - name: sccache stats
        if: always() && env.USE_SCCACHE == 'true'
        continue-on-error: true
        run: sccache --show-stats || true

      - name: sccache summary
        if: always() && env.USE_SCCACHE == 'true'
        shell: bash
        run: |
          {
            echo "### sccache stats — ${{ matrix.target }} (tests)";
            echo;
            echo '```';
            sccache --show-stats || true;
            echo '```';
          } >> "$GITHUB_STEP_SUMMARY"

      - name: Tear down remote test env
        if: ${{ always() && runner.os == 'Linux' && matrix.remote_env == 'true' }}
        shell: bash
        run: |
          set +e
          if [[ "${{ steps.test.outcome }}" != "success" ]]; then
            docker logs codex-remote-test-env || true
          fi
          docker rm -f codex-remote-test-env >/dev/null 2>&1 || true

      - name: verify tests passed
        if: steps.test.outcome == 'failure'
        run: |
          echo "Tests failed. See logs for details."
          exit 1

  # --- Gatherer job for the full post-merge workflow --------------------------
  results:
    name: Full CI results
    needs:
      [
        general,
        cargo_shear,
        argument_comment_lint_package,
        argument_comment_lint_prebuilt,
        lint_build,
        tests,
      ]
    if: always()
    runs-on: ubuntu-24.04
    steps:
      - name: Summarize
        shell: bash
        run: |
          echo "argpkg : ${{ needs.argument_comment_lint_package.result }}"
          echo "arglint: ${{ needs.argument_comment_lint_prebuilt.result }}"
          echo "general: ${{ needs.general.result }}"
          echo "shear  : ${{ needs.cargo_shear.result }}"
          echo "lint   : ${{ needs.lint_build.result }}"
          echo "tests  : ${{ needs.tests.result }}"
          [[ '${{ needs.argument_comment_lint_package.result }}' == 'success' ]] || { echo 'argument_comment_lint_package failed'; exit 1; }
          [[ '${{ needs.argument_comment_lint_prebuilt.result }}' == 'success' ]] || { echo 'argument_comment_lint_prebuilt failed'; exit 1; }
          [[ '${{ needs.general.result }}' == 'success' ]] || { echo 'general failed'; exit 1; }
          [[ '${{ needs.cargo_shear.result }}' == 'success' ]] || { echo 'cargo_shear failed'; exit 1; }
          [[ '${{ needs.lint_build.result }}' == 'success' ]] || { echo 'lint_build failed'; exit 1; }
          [[ '${{ needs.tests.result }}' == 'success' ]] || { echo 'tests failed'; exit 1; }

      - name: sccache summary note
        if: always()
        run: |
          echo "Per-job sccache stats are attached to each matrix job's Step Summary."
rust-release matrix .github/workflows/rust-release.yml
Triggers
push
Runs on
ubuntu-latest, ${{ matrix.runs_on || matrix.runner }}, ubuntu-latest, ubuntu-latest, ubuntu-latest, ubuntu-latest
Jobs
tag-check, build, build-windows, argument-comment-lint-release-assets, zsh-release-assets, release, publish-npm, winget, update-branch
Matrix
include, include.runner, include.target→ aarch64-apple-darwin, aarch64-unknown-linux-gnu, aarch64-unknown-linux-musl, macos-15-xlarge, ubuntu-24.04, ubuntu-24.04-arm, x86_64-apple-darwin, x86_64-unknown-linux-gnu, x86_64-unknown-linux-musl
Actions
dtolnay/rust-toolchain, dtolnay/rust-toolchain, mlugg/setup-zig, pnpm/action-setup, facebook/install-dotslash, softprops/action-gh-release, facebook/dotslash-publish-release, facebook/dotslash-publish-release, facebook/dotslash-publish-release, vedantmgoyal9/winget-releaser
Commands
  • set -euo pipefail echo "::group::Tag validation" # 1. Must be a tag and match the regex [[ "${GITHUB_REF_TYPE}" == "tag" ]] \ || { echo "❌ Not a tag push"; exit 1; } [[ "${GITHUB_REF_NAME}" =~ ^rust-v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)(\.[0-9]+)?)?$ ]] \ || { echo "❌ Tag '${GITHUB_REF_NAME}' doesn't match expected format"; exit 1; } # 2. Extract versions tag_ver="${GITHUB_REF_NAME#rust-v}" cargo_ver="$(grep -m1 '^version' codex-rs/Cargo.toml \ | sed -E 's/version *= *"([^"]+)".*/\1/')" # 3. Compare [[ "${tag_ver}" == "${cargo_ver}" ]] \ || { echo "❌ Tag ${tag_ver} ≠ Cargo.toml ${cargo_ver}"; exit 1; } echo "✅ Tag and Cargo.toml agree (${tag_ver})" echo "::endgroup::"
  • set -euo pipefail cpu_model="$(lscpu | awk -F: '/Model name/ {gsub(/^[ \t]+/, "", $2); print $2; exit}')" total_ram="$(awk '/MemTotal/ {printf "%.1f GiB\n", $2 / 1024 / 1024}' /proc/meminfo)" echo "Runner: ${RUNNER_NAME:-unknown}" echo "OS: $(uname -a)" echo "CPU model: ${cpu_model}" echo "Logical CPUs: $(nproc)" echo "Total RAM: ${total_ram}" echo "Disk usage:" df -h .
  • set -euo pipefail total_ram="$(sysctl -n hw.memsize | awk '{printf "%.1f GiB\n", $1 / 1024 / 1024 / 1024}')" echo "Runner: ${RUNNER_NAME:-unknown}" echo "OS: $(sw_vers -productName) $(sw_vers -productVersion)" echo "Hardware model: $(sysctl -n hw.model)" echo "CPU architecture: $(uname -m)" echo "Logical CPUs: $(sysctl -n hw.logicalcpu)" echo "Physical CPUs: $(sysctl -n hw.physicalcpu)" echo "Total RAM: ${total_ram}" echo "Disk usage:" df -h .
  • set -euo pipefail sudo apt-get update -y sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev
  • set -euo pipefail if command -v apt-get >/dev/null 2>&1; then sudo apt-get update -y sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libubsan1 fi
  • set -euo pipefail cargo_home="${GITHUB_WORKSPACE}/.cargo-home" mkdir -p "${cargo_home}/bin" echo "CARGO_HOME=${cargo_home}" >> "$GITHUB_ENV" echo "${cargo_home}/bin" >> "$GITHUB_PATH" : > "${cargo_home}/config.toml"
  • bash "${GITHUB_WORKSPACE}/.github/scripts/install-musl-build-tools.sh"
  • set -euo pipefail ubsan="" if command -v ldconfig >/dev/null 2>&1; then ubsan="$(ldconfig -p | grep -m1 'libubsan\.so\.1' | sed -E 's/.*=> (.*)$/\1/')" fi wrapper_root="${RUNNER_TEMP:-/tmp}" wrapper="${wrapper_root}/rustc-ubsan-wrapper" cat > "${wrapper}" <<EOF #!/usr/bin/env bash set -euo pipefail if [[ -n "${ubsan}" ]]; then export LD_PRELOAD="${ubsan}\${LD_PRELOAD:+:\${LD_PRELOAD}}" fi exec "\$1" "\${@:2}" EOF chmod +x "${wrapper}" echo "RUSTC_WRAPPER=${wrapper}" >> "$GITHUB_ENV" echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV"
View raw YAML
# Release workflow for codex-rs.
# To release, follow a workflow like:
# ```
# git tag -a rust-v0.1.0 -m "Release 0.1.0"
# git push origin rust-v0.1.0
# ```

name: rust-release
on:
  push:
    tags:
      - "rust-v*.*.*"

concurrency:
  group: ${{ github.workflow }}
  cancel-in-progress: true

jobs:
  tag-check:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
      - uses: dtolnay/rust-toolchain@c2b55edffaf41a251c410bb32bed22afefa800f1 # 1.92
      - name: Validate tag matches Cargo.toml version
        shell: bash
        run: |
          set -euo pipefail
          echo "::group::Tag validation"

          # 1. Must be a tag and match the regex
          [[ "${GITHUB_REF_TYPE}" == "tag" ]] \
            || { echo "❌  Not a tag push"; exit 1; }
          [[ "${GITHUB_REF_NAME}" =~ ^rust-v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)(\.[0-9]+)?)?$ ]] \
            || { echo "❌  Tag '${GITHUB_REF_NAME}' doesn't match expected format"; exit 1; }

          # 2. Extract versions
          tag_ver="${GITHUB_REF_NAME#rust-v}"
          cargo_ver="$(grep -m1 '^version' codex-rs/Cargo.toml \
                        | sed -E 's/version *= *"([^"]+)".*/\1/')"

          # 3. Compare
          [[ "${tag_ver}" == "${cargo_ver}" ]] \
            || { echo "❌  Tag ${tag_ver} ≠ Cargo.toml ${cargo_ver}"; exit 1; }

          echo "✅  Tag and Cargo.toml agree (${tag_ver})"
          echo "::endgroup::"

  build:
    needs: tag-check
    name: Build - ${{ matrix.runner }} - ${{ matrix.target }}
    runs-on: ${{ matrix.runs_on || matrix.runner }}
    timeout-minutes: 60
    permissions:
      contents: read
      id-token: write
    defaults:
      run:
        working-directory: codex-rs
    env:
      # 2026-03-04: temporarily change releases to use thin LTO because
      # Ubuntu ARM is timing out at 60 minutes.
      CARGO_PROFILE_RELEASE_LTO: ${{ contains(github.ref_name, '-alpha') && 'thin' || 'thin' }}

    strategy:
      fail-fast: false
      matrix:
        include:
          - runner: macos-15-xlarge
            target: aarch64-apple-darwin
          - runner: macos-15-xlarge
            target: x86_64-apple-darwin
          - runner: ubuntu-24.04
            target: x86_64-unknown-linux-musl
          - runner: ubuntu-24.04
            target: x86_64-unknown-linux-gnu
          - runner: ubuntu-24.04-arm
            target: aarch64-unknown-linux-musl
          - runner: ubuntu-24.04-arm
            target: aarch64-unknown-linux-gnu

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
      - name: Print runner specs (Linux)
        if: ${{ runner.os == 'Linux' }}
        shell: bash
        run: |
          set -euo pipefail
          cpu_model="$(lscpu | awk -F: '/Model name/ {gsub(/^[ \t]+/, "", $2); print $2; exit}')"
          total_ram="$(awk '/MemTotal/ {printf "%.1f GiB\n", $2 / 1024 / 1024}' /proc/meminfo)"
          echo "Runner: ${RUNNER_NAME:-unknown}"
          echo "OS: $(uname -a)"
          echo "CPU model: ${cpu_model}"
          echo "Logical CPUs: $(nproc)"
          echo "Total RAM: ${total_ram}"
          echo "Disk usage:"
          df -h .
      - name: Print runner specs (macOS)
        if: ${{ runner.os == 'macOS' }}
        shell: bash
        run: |
          set -euo pipefail
          total_ram="$(sysctl -n hw.memsize | awk '{printf "%.1f GiB\n", $1 / 1024 / 1024 / 1024}')"
          echo "Runner: ${RUNNER_NAME:-unknown}"
          echo "OS: $(sw_vers -productName) $(sw_vers -productVersion)"
          echo "Hardware model: $(sysctl -n hw.model)"
          echo "CPU architecture: $(uname -m)"
          echo "Logical CPUs: $(sysctl -n hw.logicalcpu)"
          echo "Physical CPUs: $(sysctl -n hw.physicalcpu)"
          echo "Total RAM: ${total_ram}"
          echo "Disk usage:"
          df -h .
      - name: Install Linux bwrap build dependencies
        if: ${{ runner.os == 'Linux' }}
        shell: bash
        run: |
          set -euo pipefail
          sudo apt-get update -y
          sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev
      - name: Install UBSan runtime (musl)
        if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl' }}
        shell: bash
        run: |
          set -euo pipefail
          if command -v apt-get >/dev/null 2>&1; then
            sudo apt-get update -y
            sudo DEBIAN_FRONTEND=noninteractive apt-get install -y libubsan1
          fi
      - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
        with:
          targets: ${{ matrix.target }}

      - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
        name: Use hermetic Cargo home (musl)
        shell: bash
        run: |
          set -euo pipefail
          cargo_home="${GITHUB_WORKSPACE}/.cargo-home"
          mkdir -p "${cargo_home}/bin"
          echo "CARGO_HOME=${cargo_home}" >> "$GITHUB_ENV"
          echo "${cargo_home}/bin" >> "$GITHUB_PATH"
          : > "${cargo_home}/config.toml"

      - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
        name: Install Zig
        uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2
        with:
          version: 0.14.0

      - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
        name: Install musl build tools
        env:
          TARGET: ${{ matrix.target }}
        run: bash "${GITHUB_WORKSPACE}/.github/scripts/install-musl-build-tools.sh"

      - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
        name: Configure rustc UBSan wrapper (musl host)
        shell: bash
        run: |
          set -euo pipefail
          ubsan=""
          if command -v ldconfig >/dev/null 2>&1; then
            ubsan="$(ldconfig -p | grep -m1 'libubsan\.so\.1' | sed -E 's/.*=> (.*)$/\1/')"
          fi
          wrapper_root="${RUNNER_TEMP:-/tmp}"
          wrapper="${wrapper_root}/rustc-ubsan-wrapper"
          cat > "${wrapper}" <<EOF
          #!/usr/bin/env bash
          set -euo pipefail
          if [[ -n "${ubsan}" ]]; then
            export LD_PRELOAD="${ubsan}\${LD_PRELOAD:+:\${LD_PRELOAD}}"
          fi
          exec "\$1" "\${@:2}"
          EOF
          chmod +x "${wrapper}"
          echo "RUSTC_WRAPPER=${wrapper}" >> "$GITHUB_ENV"
          echo "RUSTC_WORKSPACE_WRAPPER=" >> "$GITHUB_ENV"

      - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
        name: Clear sanitizer flags (musl)
        shell: bash
        run: |
          set -euo pipefail
          # Avoid problematic aws-lc jitter entropy code path on musl builders.
          echo "AWS_LC_SYS_NO_JITTER_ENTROPY=1" >> "$GITHUB_ENV"
          target_no_jitter="AWS_LC_SYS_NO_JITTER_ENTROPY_${{ matrix.target }}"
          target_no_jitter="${target_no_jitter//-/_}"
          echo "${target_no_jitter}=1" >> "$GITHUB_ENV"

          # Clear global Rust flags so host/proc-macro builds don't pull in UBSan.
          echo "RUSTFLAGS=" >> "$GITHUB_ENV"
          echo "CARGO_ENCODED_RUSTFLAGS=" >> "$GITHUB_ENV"
          echo "RUSTDOCFLAGS=" >> "$GITHUB_ENV"
          # Override any runner-level Cargo config rustflags as well.
          echo "CARGO_BUILD_RUSTFLAGS=" >> "$GITHUB_ENV"
          echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV"
          echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_RUSTFLAGS=" >> "$GITHUB_ENV"
          echo "CARGO_TARGET_X86_64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV"
          echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=" >> "$GITHUB_ENV"

          sanitize_flags() {
            local input="$1"
            input="${input//-fsanitize=undefined/}"
            input="${input//-fno-sanitize-recover=undefined/}"
            input="${input//-fno-sanitize-trap=undefined/}"
            echo "$input"
          }

          cflags="$(sanitize_flags "${CFLAGS-}")"
          cxxflags="$(sanitize_flags "${CXXFLAGS-}")"
          echo "CFLAGS=${cflags}" >> "$GITHUB_ENV"
          echo "CXXFLAGS=${cxxflags}" >> "$GITHUB_ENV"

      - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl' }}
        name: Configure musl rusty_v8 artifact overrides
        env:
          TARGET: ${{ matrix.target }}
        shell: bash
        run: |
          set -euo pipefail
          version="$(python3 "${GITHUB_WORKSPACE}/.github/scripts/rusty_v8_bazel.py" resolved-v8-crate-version)"
          release_tag="rusty-v8-v${version}"
          base_url="https://github.com/openai/codex/releases/download/${release_tag}"
          archive="https://github.com/openai/codex/releases/download/rusty-v8-v${version}/librusty_v8_release_${TARGET}.a.gz"
          binding_dir="${RUNNER_TEMP}/rusty_v8"
          binding_path="${binding_dir}/src_binding_release_${TARGET}.rs"
          mkdir -p "${binding_dir}"
          curl -fsSL "${base_url}/src_binding_release_${TARGET}.rs" -o "${binding_path}"
          echo "RUSTY_V8_ARCHIVE=${archive}" >> "$GITHUB_ENV"
          echo "RUSTY_V8_SRC_BINDING_PATH=${binding_path}" >> "$GITHUB_ENV"

      - name: Cargo build
        shell: bash
        run: |
          echo "CARGO_PROFILE_RELEASE_LTO: ${CARGO_PROFILE_RELEASE_LTO}"
          cargo build --target ${{ matrix.target }} --release --timings --bin codex --bin codex-responses-api-proxy

      - name: Upload Cargo timings
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
        with:
          name: cargo-timings-rust-release-${{ matrix.target }}
          path: codex-rs/target/**/cargo-timings/cargo-timing.html
          if-no-files-found: warn

      - if: ${{ contains(matrix.target, 'linux') }}
        name: Cosign Linux artifacts
        uses: ./.github/actions/linux-code-sign
        with:
          target: ${{ matrix.target }}
          artifacts-dir: ${{ github.workspace }}/codex-rs/target/${{ matrix.target }}/release

      - if: ${{ runner.os == 'macOS' }}
        name: MacOS code signing (binaries)
        uses: ./.github/actions/macos-code-sign
        with:
          target: ${{ matrix.target }}
          sign-binaries: "true"
          sign-dmg: "false"
          apple-certificate: ${{ secrets.APPLE_CERTIFICATE_P12 }}
          apple-certificate-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
          apple-notarization-key-p8: ${{ secrets.APPLE_NOTARIZATION_KEY_P8 }}
          apple-notarization-key-id: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
          apple-notarization-issuer-id: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}

      - if: ${{ runner.os == 'macOS' }}
        name: Build macOS dmg
        shell: bash
        run: |
          set -euo pipefail

          target="${{ matrix.target }}"
          release_dir="target/${target}/release"
          dmg_root="${RUNNER_TEMP}/codex-dmg-root"
          volname="Codex (${target})"
          dmg_path="${release_dir}/codex-${target}.dmg"

          # The previous "MacOS code signing (binaries)" step signs + notarizes the
          # built artifacts in `${release_dir}`. This step packages *those same*
          # signed binaries into a dmg.
          codex_binary_path="${release_dir}/codex"
          proxy_binary_path="${release_dir}/codex-responses-api-proxy"

          rm -rf "$dmg_root"
          mkdir -p "$dmg_root"

          if [[ ! -f "$codex_binary_path" ]]; then
            echo "Binary $codex_binary_path not found"
            exit 1
          fi
          if [[ ! -f "$proxy_binary_path" ]]; then
            echo "Binary $proxy_binary_path not found"
            exit 1
          fi

          ditto "$codex_binary_path" "${dmg_root}/codex"
          ditto "$proxy_binary_path" "${dmg_root}/codex-responses-api-proxy"

          rm -f "$dmg_path"
          hdiutil create \
            -volname "$volname" \
            -srcfolder "$dmg_root" \
            -format UDZO \
            -ov \
            "$dmg_path"

          if [[ ! -f "$dmg_path" ]]; then
            echo "dmg $dmg_path not found after build"
            exit 1
          fi

      - if: ${{ runner.os == 'macOS' }}
        name: MacOS code signing (dmg)
        uses: ./.github/actions/macos-code-sign
        with:
          target: ${{ matrix.target }}
          sign-binaries: "false"
          sign-dmg: "true"
          apple-certificate: ${{ secrets.APPLE_CERTIFICATE_P12 }}
          apple-certificate-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
          apple-notarization-key-p8: ${{ secrets.APPLE_NOTARIZATION_KEY_P8 }}
          apple-notarization-key-id: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
          apple-notarization-issuer-id: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}

      - name: Stage artifacts
        shell: bash
        run: |
          dest="dist/${{ matrix.target }}"
          mkdir -p "$dest"

          cp target/${{ matrix.target }}/release/codex "$dest/codex-${{ matrix.target }}"
          cp target/${{ matrix.target }}/release/codex-responses-api-proxy "$dest/codex-responses-api-proxy-${{ matrix.target }}"

          if [[ "${{ matrix.target }}" == *linux* ]]; then
            cp target/${{ matrix.target }}/release/codex.sigstore "$dest/codex-${{ matrix.target }}.sigstore"
            cp target/${{ matrix.target }}/release/codex-responses-api-proxy.sigstore "$dest/codex-responses-api-proxy-${{ matrix.target }}.sigstore"
          fi

          if [[ "${{ matrix.target }}" == *apple-darwin ]]; then
            cp target/${{ matrix.target }}/release/codex-${{ matrix.target }}.dmg "$dest/codex-${{ matrix.target }}.dmg"
          fi

      - name: Compress artifacts
        shell: bash
        run: |
          # Path that contains the uncompressed binaries for the current
          # ${{ matrix.target }}
          dest="dist/${{ matrix.target }}"

          # For compatibility with environments that lack the `zstd` tool we
          # additionally create a `.tar.gz` alongside every binary we publish.
          # The end result is:
          #   codex-<target>.zst          (existing)
          #   codex-<target>.tar.gz       (new)

          # 1. Produce a .tar.gz for every file in the directory *before* we
          #    run `zstd --rm`, because that flag deletes the original files.
          for f in "$dest"/*; do
            base="$(basename "$f")"
            # Skip files that are already archives (shouldn't happen, but be
            # safe).
            if [[ "$base" == *.tar.gz || "$base" == *.zip || "$base" == *.dmg ]]; then
              continue
            fi

            # Don't try to compress signature bundles.
            if [[ "$base" == *.sigstore ]]; then
              continue
            fi

            # Create per-binary tar.gz
            tar -C "$dest" -czf "$dest/${base}.tar.gz" "$base"

            # Also create .zst and remove the uncompressed binaries to keep
            # non-Windows artifact directories small.
            zstd -T0 -19 --rm "$dest/$base"
          done

      - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
        with:
          name: ${{ matrix.target }}
          # Upload the per-binary .zst files as well as the new .tar.gz
          # equivalents we generated in the previous step.
          path: |
            codex-rs/dist/${{ matrix.target }}/*

  build-windows:
    needs: tag-check
    uses: ./.github/workflows/rust-release-windows.yml
    with:
      release-lto: ${{ contains(github.ref_name, '-alpha') && 'thin' || 'fat' }}
    secrets: inherit

  argument-comment-lint-release-assets:
    name: argument-comment-lint release assets
    needs: tag-check
    uses: ./.github/workflows/rust-release-argument-comment-lint.yml
    with:
      publish: true

  zsh-release-assets:
    name: zsh release assets
    needs: tag-check
    uses: ./.github/workflows/rust-release-zsh.yml

  release:
    needs:
      - build
      - build-windows
      - argument-comment-lint-release-assets
      - zsh-release-assets
    name: release
    runs-on: ubuntu-latest
    permissions:
      contents: write
      actions: read
    outputs:
      version: ${{ steps.release_name.outputs.name }}
      tag: ${{ github.ref_name }}
      should_publish_npm: ${{ steps.npm_publish_settings.outputs.should_publish }}
      npm_tag: ${{ steps.npm_publish_settings.outputs.npm_tag }}

    steps:
      - name: Checkout repository
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

      - name: Generate release notes from tag commit message
        id: release_notes
        shell: bash
        run: |
          set -euo pipefail

          # On tag pushes, GITHUB_SHA may be a tag object for annotated tags;
          # peel it to the underlying commit.
          commit="$(git rev-parse "${GITHUB_SHA}^{commit}")"
          notes_path="${RUNNER_TEMP}/release-notes.md"

          # Use the commit message for the commit the tag points at (not the
          # annotated tag message).
          git log -1 --format=%B "${commit}" > "${notes_path}"
          # Ensure trailing newline so GitHub's markdown renderer doesn't
          # occasionally run the last line into subsequent content.
          echo >> "${notes_path}"

          echo "path=${notes_path}" >> "${GITHUB_OUTPUT}"

      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
        with:
          path: dist

      - name: List
        run: ls -R dist/

      - name: Delete entries from dist/ that should not go in the release
        run: |
          rm -rf dist/windows-binaries*
          # cargo-timing.html appears under multiple target-specific directories.
          # If included in files: dist/**, release upload races on duplicate
          # asset names and can fail with 404s.
          find dist -type f -name 'cargo-timing.html' -delete
          find dist -type d -empty -delete

          ls -R dist/

      - name: Add config schema release asset
        run: |
          cp codex-rs/core/config.schema.json dist/config-schema.json

      - name: Define release name
        id: release_name
        run: |
          # Extract the version from the tag name, which is in the format
          # "rust-v0.1.0".
          version="${GITHUB_REF_NAME#rust-v}"
          echo "name=${version}" >> $GITHUB_OUTPUT

      - name: Determine npm publish settings
        id: npm_publish_settings
        env:
          VERSION: ${{ steps.release_name.outputs.name }}
        run: |
          set -euo pipefail
          version="${VERSION}"

          if [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
            echo "should_publish=true" >> "$GITHUB_OUTPUT"
            echo "npm_tag=" >> "$GITHUB_OUTPUT"
          elif [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then
            echo "should_publish=true" >> "$GITHUB_OUTPUT"
            echo "npm_tag=alpha" >> "$GITHUB_OUTPUT"
          else
            echo "should_publish=false" >> "$GITHUB_OUTPUT"
            echo "npm_tag=" >> "$GITHUB_OUTPUT"
          fi

      - name: Setup pnpm
        uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5
        with:
          run_install: false

      - name: Setup Node.js for npm packaging
        uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
        with:
          node-version: 22

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      # stage_npm_packages.py requires DotSlash when staging releases.
      - uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2
      - name: Stage npm packages
        env:
          GH_TOKEN: ${{ github.token }}
          RELEASE_VERSION: ${{ steps.release_name.outputs.name }}
        run: |
          ./scripts/stage_npm_packages.py \
            --release-version "$RELEASE_VERSION" \
            --package codex \
            --package codex-responses-api-proxy \
            --package codex-sdk

      - name: Stage installer scripts
        run: |
          cp scripts/install/install.sh dist/install.sh
          cp scripts/install/install.ps1 dist/install.ps1

      - name: Create GitHub Release
        uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2
        with:
          name: ${{ steps.release_name.outputs.name }}
          tag_name: ${{ github.ref_name }}
          body_path: ${{ steps.release_notes.outputs.path }}
          files: dist/**
          # Mark as prerelease only when the version has a suffix after x.y.z
          # (e.g. -alpha, -beta). Otherwise publish a normal release.
          prerelease: ${{ contains(steps.release_name.outputs.name, '-') }}

      - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag: ${{ github.ref_name }}
          config: .github/dotslash-config.json

      - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag: ${{ github.ref_name }}
          config: .github/dotslash-zsh-config.json

      - uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag: ${{ github.ref_name }}
          config: .github/dotslash-argument-comment-lint-config.json

      - name: Trigger developers.openai.com deploy
        # Only trigger the deploy if the release is not a pre-release.
        # The deploy is used to update the developers.openai.com website with the new config schema json file.
        if: ${{ !contains(steps.release_name.outputs.name, '-') }}
        continue-on-error: true
        env:
          DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL: ${{ secrets.DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL }}
        run: |
          if ! curl -sS -f -o /dev/null -X POST "$DEV_WEBSITE_VERCEL_DEPLOY_HOOK_URL"; then
            echo "::warning title=developers.openai.com deploy hook failed::Vercel deploy hook POST failed for ${GITHUB_REF_NAME}"
            exit 1
          fi

  # Publish to npm using OIDC authentication.
  # July 31, 2025: https://github.blog/changelog/2025-07-31-npm-trusted-publishing-with-oidc-is-generally-available/
  # npm docs: https://docs.npmjs.com/trusted-publishers
  publish-npm:
    # Publish to npm for stable releases and alpha pre-releases with numeric suffixes.
    if: ${{ needs.release.outputs.should_publish_npm == 'true' }}
    name: publish-npm
    needs: release
    runs-on: ubuntu-latest
    permissions:
      id-token: write # Required for OIDC
      contents: read

    steps:
      - name: Setup Node.js
        uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
        with:
          node-version: 22
          registry-url: "https://registry.npmjs.org"
          scope: "@openai"

      # Trusted publishing requires npm CLI version 11.5.1 or later.
      - name: Update npm
        run: npm install -g npm@latest

      - name: Download npm tarballs from release
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          RELEASE_TAG: ${{ needs.release.outputs.tag }}
          RELEASE_VERSION: ${{ needs.release.outputs.version }}
        run: |
          set -euo pipefail
          version="$RELEASE_VERSION"
          tag="$RELEASE_TAG"
          mkdir -p dist/npm
          patterns=(
            "codex-npm-${version}.tgz"
            "codex-npm-linux-*-${version}.tgz"
            "codex-npm-darwin-*-${version}.tgz"
            "codex-npm-win32-*-${version}.tgz"
            "codex-responses-api-proxy-npm-${version}.tgz"
            "codex-sdk-npm-${version}.tgz"
          )
          for pattern in "${patterns[@]}"; do
            gh release download "$tag" \
              --repo "${GITHUB_REPOSITORY}" \
              --pattern "$pattern" \
              --dir dist/npm
          done

      # No NODE_AUTH_TOKEN needed because we use OIDC.
      - name: Publish to npm
        env:
          VERSION: ${{ needs.release.outputs.version }}
          NPM_TAG: ${{ needs.release.outputs.npm_tag }}
        run: |
          set -euo pipefail
          prefix=""
          if [[ -n "${NPM_TAG}" ]]; then
            prefix="${NPM_TAG}-"
          fi

          shopt -s nullglob
          tarballs=(dist/npm/*-"${VERSION}".tgz)
          if [[ ${#tarballs[@]} -eq 0 ]]; then
            echo "No npm tarballs found in dist/npm for version ${VERSION}"
            exit 1
          fi

          for tarball in "${tarballs[@]}"; do
            filename="$(basename "${tarball}")"
            tag=""

            case "${filename}" in
              codex-npm-linux-*-"${VERSION}".tgz|codex-npm-darwin-*-"${VERSION}".tgz|codex-npm-win32-*-"${VERSION}".tgz)
                platform="${filename#codex-npm-}"
                platform="${platform%-${VERSION}.tgz}"
                tag="${prefix}${platform}"
                ;;
              codex-npm-"${VERSION}".tgz|codex-responses-api-proxy-npm-"${VERSION}".tgz|codex-sdk-npm-"${VERSION}".tgz)
                tag="${NPM_TAG}"
                ;;
              *)
                echo "Unexpected npm tarball: ${filename}"
                exit 1
                ;;
            esac

            publish_cmd=(npm publish "${GITHUB_WORKSPACE}/${tarball}")
            if [[ -n "${tag}" ]]; then
              publish_cmd+=(--tag "${tag}")
            fi

            echo "+ ${publish_cmd[*]}"
            set +e
            publish_output="$("${publish_cmd[@]}" 2>&1)"
            publish_status=$?
            set -e

            echo "${publish_output}"
            if [[ ${publish_status} -eq 0 ]]; then
              continue
            fi

            if grep -qiE "previously published|cannot publish over|version already exists" <<< "${publish_output}"; then
              echo "Skipping already-published package version for ${filename}"
              continue
            fi

            exit "${publish_status}"
          done

  winget:
    name: winget
    needs: release
    # Only publish stable/mainline releases to WinGet; pre-releases include a
    # '-' in the semver string (e.g., 1.2.3-alpha.1).
    if: ${{ !contains(needs.release.outputs.version, '-') }}
    # This job only invokes a GitHub Action to open/update the winget-pkgs PR;
    # it does not execute Windows-only tooling, so Linux is sufficient.
    runs-on: ubuntu-latest
    permissions:
      contents: read

    steps:
      - name: Publish to WinGet
        uses: vedantmgoyal9/winget-releaser@7bd472be23763def6e16bd06cc8b1cdfab0e2fd5
        with:
          identifier: OpenAI.Codex
          version: ${{ needs.release.outputs.version }}
          release-tag: ${{ needs.release.outputs.tag }}
          fork-user: openai-oss-forks
          installers-regex: '^codex-(?:x86_64|aarch64)-pc-windows-msvc\.exe\.zip$'
          token: ${{ secrets.WINGET_PUBLISH_PAT }}

  update-branch:
    name: Update latest-alpha-cli branch
    permissions:
      contents: write
    needs: release
    runs-on: ubuntu-latest

    steps:
      - name: Update latest-alpha-cli branch
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          set -euo pipefail
          gh api \
            repos/${GITHUB_REPOSITORY}/git/refs/heads/latest-alpha-cli \
            -X PATCH \
            -f sha="${GITHUB_SHA}" \
            -F force=true
rust-release-argument-comment-lint matrix .github/workflows/rust-release-argument-comment-lint.yml
Triggers
workflow_call
Runs on
ubuntu-latest, ${{ matrix.runs_on || matrix.runner }}
Jobs
skip, build
Matrix
include, include.archive_name, include.cargo_dylint_binary, include.lib_name, include.runner, include.runner_binary, include.runs_on, include.target, runs_on.group, runs_on.labels→ aarch64-apple-darwin, aarch64-unknown-linux-gnu, argument-comment-lint, argument-comment-lint-aarch64-apple-darwin.tar.gz, argument-comment-lint-aarch64-unknown-linux-gnu.tar.gz, argument-comment-lint-x86_64-pc-windows-msvc.zip, argument-comment-lint-x86_64-unknown-linux-gnu.tar.gz, argument-comment-lint.exe, argument_comment_lint@nightly-2025-09-18-x86_64-pc-windows-msvc.dll, cargo-dylint, cargo-dylint.exe, codex-runners, codex-windows-x64, libargument_comment_lint@nightly-2025-09-18-aarch64-apple-darwin.dylib, libargument_comment_lint@nightly-2025-09-18-aarch64-unknown-linux-gnu.so, libargument_comment_lint@nightly-2025-09-18-x86_64-unknown-linux-gnu.so, macos-15-xlarge, ubuntu-24.04, ubuntu-24.04-arm, windows-x64, x86_64-pc-windows-msvc, x86_64-unknown-linux-gnu
Actions
dtolnay/rust-toolchain
Commands
  • echo "Skipping argument-comment-lint release assets for prerelease tag"
  • install_root="${RUNNER_TEMP}/argument-comment-lint-tools" cargo install --locked cargo-dylint --root "$install_root" cargo install --locked dylint-link echo "INSTALL_ROOT=$install_root" >> "$GITHUB_ENV"
  • cargo build --release --target ${{ matrix.target }}
  • dest="dist/argument-comment-lint/${{ matrix.target }}" mkdir -p "$dest" package_root="${RUNNER_TEMP}/argument-comment-lint" rm -rf "$package_root" mkdir -p "$package_root/bin" "$package_root/lib" cp "tools/argument-comment-lint/target/${{ matrix.target }}/release/${{ matrix.runner_binary }}" \ "$package_root/bin/${{ matrix.runner_binary }}" cp "${INSTALL_ROOT}/bin/${{ matrix.cargo_dylint_binary }}" \ "$package_root/bin/${{ matrix.cargo_dylint_binary }}" cp "tools/argument-comment-lint/target/${{ matrix.target }}/release/${{ matrix.lib_name }}" \ "$package_root/lib/${{ matrix.lib_name }}" archive_path="$dest/${{ matrix.archive_name }}" if [[ "${{ runner.os }}" == "Windows" ]]; then (cd "${RUNNER_TEMP}" && 7z a "$GITHUB_WORKSPACE/$archive_path" argument-comment-lint >/dev/null) else (cd "${RUNNER_TEMP}" && tar -czf "$GITHUB_WORKSPACE/$archive_path" argument-comment-lint) fi
View raw YAML
name: rust-release-argument-comment-lint

on:
  workflow_call:
    inputs:
      publish:
        required: true
        type: boolean

jobs:
  skip:
    if: ${{ !inputs.publish }}
    runs-on: ubuntu-latest
    steps:
      - run: echo "Skipping argument-comment-lint release assets for prerelease tag"

  build:
    if: ${{ inputs.publish }}
    name: Build - ${{ matrix.runner }} - ${{ matrix.target }}
    runs-on: ${{ matrix.runs_on || matrix.runner }}
    timeout-minutes: 60

    strategy:
      fail-fast: false
      matrix:
        include:
          - runner: macos-15-xlarge
            target: aarch64-apple-darwin
            archive_name: argument-comment-lint-aarch64-apple-darwin.tar.gz
            lib_name: libargument_comment_lint@nightly-2025-09-18-aarch64-apple-darwin.dylib
            runner_binary: argument-comment-lint
            cargo_dylint_binary: cargo-dylint
          - runner: ubuntu-24.04
            target: x86_64-unknown-linux-gnu
            archive_name: argument-comment-lint-x86_64-unknown-linux-gnu.tar.gz
            lib_name: libargument_comment_lint@nightly-2025-09-18-x86_64-unknown-linux-gnu.so
            runner_binary: argument-comment-lint
            cargo_dylint_binary: cargo-dylint
          - runner: ubuntu-24.04-arm
            target: aarch64-unknown-linux-gnu
            archive_name: argument-comment-lint-aarch64-unknown-linux-gnu.tar.gz
            lib_name: libargument_comment_lint@nightly-2025-09-18-aarch64-unknown-linux-gnu.so
            runner_binary: argument-comment-lint
            cargo_dylint_binary: cargo-dylint
          - runner: windows-x64
            target: x86_64-pc-windows-msvc
            archive_name: argument-comment-lint-x86_64-pc-windows-msvc.zip
            lib_name: argument_comment_lint@nightly-2025-09-18-x86_64-pc-windows-msvc.dll
            runner_binary: argument-comment-lint.exe
            cargo_dylint_binary: cargo-dylint.exe
            runs_on:
              group: codex-runners
              labels: codex-windows-x64

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

      - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
        with:
          toolchain: nightly-2025-09-18
          targets: ${{ matrix.target }}
          components: llvm-tools-preview, rustc-dev, rust-src

      - name: Install tooling
        shell: bash
        run: |
          install_root="${RUNNER_TEMP}/argument-comment-lint-tools"
          cargo install --locked cargo-dylint --root "$install_root"
          cargo install --locked dylint-link
          echo "INSTALL_ROOT=$install_root" >> "$GITHUB_ENV"

      - name: Cargo build
        working-directory: tools/argument-comment-lint
        shell: bash
        run: cargo build --release --target ${{ matrix.target }}

      - name: Stage artifact
        shell: bash
        run: |
          dest="dist/argument-comment-lint/${{ matrix.target }}"
          mkdir -p "$dest"
          package_root="${RUNNER_TEMP}/argument-comment-lint"
          rm -rf "$package_root"
          mkdir -p "$package_root/bin" "$package_root/lib"

          cp "tools/argument-comment-lint/target/${{ matrix.target }}/release/${{ matrix.runner_binary }}" \
            "$package_root/bin/${{ matrix.runner_binary }}"
          cp "${INSTALL_ROOT}/bin/${{ matrix.cargo_dylint_binary }}" \
            "$package_root/bin/${{ matrix.cargo_dylint_binary }}"
          cp "tools/argument-comment-lint/target/${{ matrix.target }}/release/${{ matrix.lib_name }}" \
            "$package_root/lib/${{ matrix.lib_name }}"

          archive_path="$dest/${{ matrix.archive_name }}"
          if [[ "${{ runner.os }}" == "Windows" ]]; then
            (cd "${RUNNER_TEMP}" && 7z a "$GITHUB_WORKSPACE/$archive_path" argument-comment-lint >/dev/null)
          else
            (cd "${RUNNER_TEMP}" && tar -czf "$GITHUB_WORKSPACE/$archive_path" argument-comment-lint)
          fi

      - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
        with:
          name: argument-comment-lint-${{ matrix.target }}
          path: dist/argument-comment-lint/${{ matrix.target }}/*
rust-release-prepare perms .github/workflows/rust-release-prepare.yml
Triggers
workflow_dispatch, schedule
Runs on
ubuntu-latest
Jobs
prepare
Actions
peter-evans/create-pull-request
Commands
  • set -euo pipefail client_version="99.99.99" terminal_info="github-actions" user_agent="codex_cli_rs/99.99.99 (Linux $(uname -r); $(uname -m)) ${terminal_info}" base_url="${OPENAI_BASE_URL:-https://chatgpt.com/backend-api/codex}" headers=( -H "Authorization: Bearer ${OPENAI_API_KEY}" -H "User-Agent: ${user_agent}" ) url="${base_url%/}/models?client_version=${client_version}" curl --http1.1 --fail --show-error --location "${headers[@]}" "${url}" | jq '.' > codex-rs/core/models.json
View raw YAML
name: rust-release-prepare
on:
  workflow_dispatch:
  schedule:
    - cron: "0 */4 * * *"

concurrency:
  group: ${{ github.workflow }}
  cancel-in-progress: false

permissions:
  contents: write
  pull-requests: write

jobs:
  prepare:
    # Prevent scheduled runs on forks (no secrets, wastes Actions minutes)
    if: github.repository == 'openai/codex'
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
        with:
          ref: main
          fetch-depth: 0

      - name: Update models.json
        env:
          OPENAI_API_KEY: ${{ secrets.CODEX_OPENAI_API_KEY }}
        run: |
          set -euo pipefail

          client_version="99.99.99"
          terminal_info="github-actions"
          user_agent="codex_cli_rs/99.99.99 (Linux $(uname -r); $(uname -m)) ${terminal_info}"
          base_url="${OPENAI_BASE_URL:-https://chatgpt.com/backend-api/codex}"

          headers=(
            -H "Authorization: Bearer ${OPENAI_API_KEY}"
            -H "User-Agent: ${user_agent}"
          )

          url="${base_url%/}/models?client_version=${client_version}"
          curl --http1.1 --fail --show-error --location "${headers[@]}" "${url}" | jq '.' > codex-rs/core/models.json

      - name: Open pull request (if changed)
        uses: peter-evans/create-pull-request@c0f553fe549906ede9cf27b5156039d195d2ece0 # v8
        with:
          commit-message: "Update models.json"
          title: "Update models.json"
          body: "Automated update of models.json."
          branch: "bot/update-models-json"
          reviewers: "pakrym-oai,aibrahim-oai"
          delete-branch: true
rust-release-windows matrix .github/workflows/rust-release-windows.yml
Triggers
workflow_call
Runs on
${{ matrix.runs_on }}, ${{ matrix.runs_on }}
Jobs
build-windows-binaries, build-windows
Matrix
include, include.build_args, include.bundle, include.runner, include.runs_on, include.target, runs_on.group, runs_on.labels→ --bin codex --bin codex-responses-api-proxy, --bin codex-windows-sandbox-setup --bin codex-command-runner, aarch64-pc-windows-msvc, codex-runners, codex-windows-arm64, codex-windows-x64, helpers, primary, windows-arm64, windows-x64, x86_64-pc-windows-msvc
Actions
dtolnay/rust-toolchain, facebook/install-dotslash
Commands
  • $computer = Get-CimInstance Win32_ComputerSystem $cpu = Get-CimInstance Win32_Processor | Select-Object -First 1 $ramGiB = [math]::Round($computer.TotalPhysicalMemory / 1GB, 1) Write-Host "Runner: $env:RUNNER_NAME" Write-Host "OS: $([System.Environment]::OSVersion.VersionString)" Write-Host "CPU: $($cpu.Name)" Write-Host "Logical CPUs: $($computer.NumberOfLogicalProcessors)" Write-Host "Physical CPUs: $($computer.NumberOfProcessors)" Write-Host "Total RAM: $ramGiB GiB" Write-Host "Disk usage:" Get-PSDrive -PSProvider FileSystem | Format-Table -AutoSize Name, @{Name='Size(GB)';Expression={[math]::Round(($_.Used + $_.Free) / 1GB, 1)}}, @{Name='Free(GB)';Expression={[math]::Round($_.Free / 1GB, 1)}}
  • cargo build --target ${{ matrix.target }} --release --timings ${{ matrix.build_args }}
  • output_dir="target/${{ matrix.target }}/release/staged-${{ matrix.bundle }}" mkdir -p "$output_dir" if [[ "${{ matrix.bundle }}" == "primary" ]]; then cp target/${{ matrix.target }}/release/codex.exe "$output_dir/codex.exe" cp target/${{ matrix.target }}/release/codex-responses-api-proxy.exe "$output_dir/codex-responses-api-proxy.exe" else cp target/${{ matrix.target }}/release/codex-windows-sandbox-setup.exe "$output_dir/codex-windows-sandbox-setup.exe" cp target/${{ matrix.target }}/release/codex-command-runner.exe "$output_dir/codex-command-runner.exe" fi
  • set -euo pipefail ls -lh target/${{ matrix.target }}/release/codex.exe ls -lh target/${{ matrix.target }}/release/codex-responses-api-proxy.exe ls -lh target/${{ matrix.target }}/release/codex-windows-sandbox-setup.exe ls -lh target/${{ matrix.target }}/release/codex-command-runner.exe
  • dest="dist/${{ matrix.target }}" mkdir -p "$dest" cp target/${{ matrix.target }}/release/codex.exe "$dest/codex-${{ matrix.target }}.exe" cp target/${{ matrix.target }}/release/codex-responses-api-proxy.exe "$dest/codex-responses-api-proxy-${{ matrix.target }}.exe" cp target/${{ matrix.target }}/release/codex-windows-sandbox-setup.exe "$dest/codex-windows-sandbox-setup-${{ matrix.target }}.exe" cp target/${{ matrix.target }}/release/codex-command-runner.exe "$dest/codex-command-runner-${{ matrix.target }}.exe"
  • # Path that contains the uncompressed binaries for the current # ${{ matrix.target }} dest="dist/${{ matrix.target }}" repo_root=$PWD # For compatibility with environments that lack the `zstd` tool we # additionally create a `.tar.gz` and `.zip` for every Windows binary. # The end result is: # codex-<target>.zst # codex-<target>.tar.gz # codex-<target>.zip for f in "$dest"/*; do base="$(basename "$f")" # Skip files that are already archives (shouldn't happen, but be # safe). if [[ "$base" == *.tar.gz || "$base" == *.zip || "$base" == *.dmg ]]; then continue fi # Don't try to compress signature bundles. if [[ "$base" == *.sigstore ]]; then continue fi # Create per-binary tar.gz tar -C "$dest" -czf "$dest/${base}.tar.gz" "$base" # Create zip archive for Windows binaries. # Must run from inside the dest dir so 7z won't embed the # directory path inside the zip. if [[ "$base" == "codex-${{ matrix.target }}.exe" ]]; then # Bundle the sandbox helper binaries into the main codex zip so # WinGet installs include the required helpers next to codex.exe. # Fall back to the single-binary zip if the helpers are missing # to avoid breaking releases. bundle_dir="$(mktemp -d)" runner_src="$dest/codex-command-runner-${{ matrix.target }}.exe" setup_src="$dest/codex-windows-sandbox-setup-${{ matrix.target }}.exe" if [[ -f "$runner_src" && -f "$setup_src" ]]; then cp "$dest/$base" "$bundle_dir/$base" cp "$runner_src" "$bundle_dir/codex-command-runner.exe" cp "$setup_src" "$bundle_dir/codex-windows-sandbox-setup.exe" # Use an absolute path so bundle zips land in the real dist # dir even when 7z runs from a temp directory. (cd "$bundle_dir" && 7z a "$repo_root/$dest/${base}.zip" .) else echo "warning: missing sandbox binaries; falling back to single-binary zip" echo "warning: expected $runner_src and $setup_src" (cd "$dest" && 7z a "${base}.zip" "$base") fi rm -rf "$bundle_dir" else (cd "$dest" && 7z a "${base}.zip" "$base") fi # Keep raw executables and produce .zst alongside them. "${GITHUB_WORKSPACE}/.github/workflows/zstd" -T0 -19 "$dest/$base" done
View raw YAML
name: rust-release-windows

on:
  workflow_call:
    inputs:
      release-lto:
        required: true
        type: string
    secrets:
      AZURE_TRUSTED_SIGNING_CLIENT_ID:
        required: true
      AZURE_TRUSTED_SIGNING_TENANT_ID:
        required: true
      AZURE_TRUSTED_SIGNING_SUBSCRIPTION_ID:
        required: true
      AZURE_TRUSTED_SIGNING_ENDPOINT:
        required: true
      AZURE_TRUSTED_SIGNING_ACCOUNT_NAME:
        required: true
      AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME:
        required: true

jobs:
  build-windows-binaries:
    name: Build Windows binaries - ${{ matrix.runner }} - ${{ matrix.target }} - ${{ matrix.bundle }}
    runs-on: ${{ matrix.runs_on }}
    timeout-minutes: 60
    permissions:
      contents: read
    defaults:
      run:
        working-directory: codex-rs
    env:
      CARGO_PROFILE_RELEASE_LTO: ${{ inputs.release-lto }}

    strategy:
      fail-fast: false
      matrix:
        include:
          - runner: windows-x64
            target: x86_64-pc-windows-msvc
            bundle: primary
            build_args: --bin codex --bin codex-responses-api-proxy
            runs_on:
              group: codex-runners
              labels: codex-windows-x64
          - runner: windows-arm64
            target: aarch64-pc-windows-msvc
            bundle: primary
            build_args: --bin codex --bin codex-responses-api-proxy
            runs_on:
              group: codex-runners
              labels: codex-windows-arm64
          - runner: windows-x64
            target: x86_64-pc-windows-msvc
            bundle: helpers
            build_args: --bin codex-windows-sandbox-setup --bin codex-command-runner
            runs_on:
              group: codex-runners
              labels: codex-windows-x64
          - runner: windows-arm64
            target: aarch64-pc-windows-msvc
            bundle: helpers
            build_args: --bin codex-windows-sandbox-setup --bin codex-command-runner
            runs_on:
              group: codex-runners
              labels: codex-windows-arm64

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
      - name: Print runner specs (Windows)
        shell: powershell
        run: |
          $computer = Get-CimInstance Win32_ComputerSystem
          $cpu = Get-CimInstance Win32_Processor | Select-Object -First 1
          $ramGiB = [math]::Round($computer.TotalPhysicalMemory / 1GB, 1)
          Write-Host "Runner: $env:RUNNER_NAME"
          Write-Host "OS: $([System.Environment]::OSVersion.VersionString)"
          Write-Host "CPU: $($cpu.Name)"
          Write-Host "Logical CPUs: $($computer.NumberOfLogicalProcessors)"
          Write-Host "Physical CPUs: $($computer.NumberOfProcessors)"
          Write-Host "Total RAM: $ramGiB GiB"
          Write-Host "Disk usage:"
          Get-PSDrive -PSProvider FileSystem | Format-Table -AutoSize Name, @{Name='Size(GB)';Expression={[math]::Round(($_.Used + $_.Free) / 1GB, 1)}}, @{Name='Free(GB)';Expression={[math]::Round($_.Free / 1GB, 1)}}
      - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0
        with:
          targets: ${{ matrix.target }}

      - name: Cargo build (Windows binaries)
        shell: bash
        run: |
          cargo build --target ${{ matrix.target }} --release --timings ${{ matrix.build_args }}

      - name: Upload Cargo timings
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
        with:
          name: cargo-timings-rust-release-windows-${{ matrix.target }}-${{ matrix.bundle }}
          path: codex-rs/target/**/cargo-timings/cargo-timing.html
          if-no-files-found: warn

      - name: Stage Windows binaries
        shell: bash
        run: |
          output_dir="target/${{ matrix.target }}/release/staged-${{ matrix.bundle }}"
          mkdir -p "$output_dir"
          if [[ "${{ matrix.bundle }}" == "primary" ]]; then
            cp target/${{ matrix.target }}/release/codex.exe "$output_dir/codex.exe"
            cp target/${{ matrix.target }}/release/codex-responses-api-proxy.exe "$output_dir/codex-responses-api-proxy.exe"
          else
            cp target/${{ matrix.target }}/release/codex-windows-sandbox-setup.exe "$output_dir/codex-windows-sandbox-setup.exe"
            cp target/${{ matrix.target }}/release/codex-command-runner.exe "$output_dir/codex-command-runner.exe"
          fi

      - name: Upload Windows binaries
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
        with:
          name: windows-binaries-${{ matrix.target }}-${{ matrix.bundle }}
          path: |
            codex-rs/target/${{ matrix.target }}/release/staged-${{ matrix.bundle }}/*

  build-windows:
    needs:
      - build-windows-binaries
    name: Build - ${{ matrix.runner }} - ${{ matrix.target }}
    runs-on: ${{ matrix.runs_on }}
    timeout-minutes: 60
    permissions:
      contents: read
      id-token: write
    defaults:
      run:
        working-directory: codex-rs

    strategy:
      fail-fast: false
      matrix:
        include:
          - runner: windows-x64
            target: x86_64-pc-windows-msvc
            runs_on:
              group: codex-runners
              labels: codex-windows-x64
          - runner: windows-arm64
            target: aarch64-pc-windows-msvc
            runs_on:
              group: codex-runners
              labels: codex-windows-arm64

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

      - name: Download prebuilt Windows primary binaries
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
        with:
          name: windows-binaries-${{ matrix.target }}-primary
          path: codex-rs/target/${{ matrix.target }}/release

      - name: Download prebuilt Windows helper binaries
        uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
        with:
          name: windows-binaries-${{ matrix.target }}-helpers
          path: codex-rs/target/${{ matrix.target }}/release

      - name: Verify binaries
        shell: bash
        run: |
          set -euo pipefail
          ls -lh target/${{ matrix.target }}/release/codex.exe
          ls -lh target/${{ matrix.target }}/release/codex-responses-api-proxy.exe
          ls -lh target/${{ matrix.target }}/release/codex-windows-sandbox-setup.exe
          ls -lh target/${{ matrix.target }}/release/codex-command-runner.exe

      - name: Sign Windows binaries with Azure Trusted Signing
        uses: ./.github/actions/windows-code-sign
        with:
          target: ${{ matrix.target }}
          client-id: ${{ secrets.AZURE_TRUSTED_SIGNING_CLIENT_ID }}
          tenant-id: ${{ secrets.AZURE_TRUSTED_SIGNING_TENANT_ID }}
          subscription-id: ${{ secrets.AZURE_TRUSTED_SIGNING_SUBSCRIPTION_ID }}
          endpoint: ${{ secrets.AZURE_TRUSTED_SIGNING_ENDPOINT }}
          account-name: ${{ secrets.AZURE_TRUSTED_SIGNING_ACCOUNT_NAME }}
          certificate-profile-name: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE_NAME }}

      - name: Stage artifacts
        shell: bash
        run: |
          dest="dist/${{ matrix.target }}"
          mkdir -p "$dest"

          cp target/${{ matrix.target }}/release/codex.exe "$dest/codex-${{ matrix.target }}.exe"
          cp target/${{ matrix.target }}/release/codex-responses-api-proxy.exe "$dest/codex-responses-api-proxy-${{ matrix.target }}.exe"
          cp target/${{ matrix.target }}/release/codex-windows-sandbox-setup.exe "$dest/codex-windows-sandbox-setup-${{ matrix.target }}.exe"
          cp target/${{ matrix.target }}/release/codex-command-runner.exe "$dest/codex-command-runner-${{ matrix.target }}.exe"

      - name: Install DotSlash
        uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2

      - name: Compress artifacts
        shell: bash
        run: |
          # Path that contains the uncompressed binaries for the current
          # ${{ matrix.target }}
          dest="dist/${{ matrix.target }}"
          repo_root=$PWD

          # For compatibility with environments that lack the `zstd` tool we
          # additionally create a `.tar.gz` and `.zip` for every Windows binary.
          # The end result is:
          #   codex-<target>.zst
          #   codex-<target>.tar.gz
          #   codex-<target>.zip
          for f in "$dest"/*; do
            base="$(basename "$f")"
            # Skip files that are already archives (shouldn't happen, but be
            # safe).
            if [[ "$base" == *.tar.gz || "$base" == *.zip || "$base" == *.dmg ]]; then
              continue
            fi

            # Don't try to compress signature bundles.
            if [[ "$base" == *.sigstore ]]; then
              continue
            fi

            # Create per-binary tar.gz
            tar -C "$dest" -czf "$dest/${base}.tar.gz" "$base"

            # Create zip archive for Windows binaries.
            # Must run from inside the dest dir so 7z won't embed the
            # directory path inside the zip.
            if [[ "$base" == "codex-${{ matrix.target }}.exe" ]]; then
              # Bundle the sandbox helper binaries into the main codex zip so
              # WinGet installs include the required helpers next to codex.exe.
              # Fall back to the single-binary zip if the helpers are missing
              # to avoid breaking releases.
              bundle_dir="$(mktemp -d)"
              runner_src="$dest/codex-command-runner-${{ matrix.target }}.exe"
              setup_src="$dest/codex-windows-sandbox-setup-${{ matrix.target }}.exe"
              if [[ -f "$runner_src" && -f "$setup_src" ]]; then
                cp "$dest/$base" "$bundle_dir/$base"
                cp "$runner_src" "$bundle_dir/codex-command-runner.exe"
                cp "$setup_src" "$bundle_dir/codex-windows-sandbox-setup.exe"
                # Use an absolute path so bundle zips land in the real dist
                # dir even when 7z runs from a temp directory.
                (cd "$bundle_dir" && 7z a "$repo_root/$dest/${base}.zip" .)
              else
                echo "warning: missing sandbox binaries; falling back to single-binary zip"
                echo "warning: expected $runner_src and $setup_src"
                (cd "$dest" && 7z a "${base}.zip" "$base")
              fi
              rm -rf "$bundle_dir"
            else
              (cd "$dest" && 7z a "${base}.zip" "$base")
            fi

            # Keep raw executables and produce .zst alongside them.
            "${GITHUB_WORKSPACE}/.github/workflows/zstd" -T0 -19 "$dest/$base"
          done

      - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
        with:
          name: ${{ matrix.target }}
          path: |
            codex-rs/dist/${{ matrix.target }}/*
rust-release-zsh matrix .github/workflows/rust-release-zsh.yml
Triggers
workflow_call
Runs on
${{ matrix.runner }}, ${{ matrix.runner }}
Jobs
linux, darwin
Matrix
include, include.archive_name, include.image, include.runner, include.target, include.variant→ aarch64-apple-darwin, aarch64-unknown-linux-musl, arm64v8/ubuntu:24.04, codex-zsh-aarch64-apple-darwin.tar.gz, codex-zsh-aarch64-unknown-linux-musl.tar.gz, codex-zsh-x86_64-unknown-linux-musl.tar.gz, macos-15, macos-15-xlarge, ubuntu-24.04, ubuntu-24.04-arm, ubuntu:24.04, x86_64-unknown-linux-musl
Commands
  • set -euo pipefail apt-get update DEBIAN_FRONTEND=noninteractive apt-get install -y \ autoconf \ bison \ build-essential \ ca-certificates \ gettext \ git \ libncursesw5-dev
  • "${GITHUB_WORKSPACE}/.github/scripts/build-zsh-release-artifact.sh" \ "dist/zsh/${{ matrix.target }}/${{ matrix.archive_name }}"
  • set -euo pipefail if ! command -v autoconf >/dev/null 2>&1; then brew install autoconf fi
  • "${GITHUB_WORKSPACE}/.github/scripts/build-zsh-release-artifact.sh" \ "dist/zsh/${{ matrix.target }}/${{ matrix.archive_name }}"
View raw YAML
name: rust-release-zsh

on:
  workflow_call:

env:
  ZSH_COMMIT: 77045ef899e53b9598bebc5a41db93a548a40ca6
  ZSH_PATCH: codex-rs/shell-escalation/patches/zsh-exec-wrapper.patch

jobs:
  linux:
    name: Build zsh (Linux) - ${{ matrix.variant }} - ${{ matrix.target }}
    runs-on: ${{ matrix.runner }}
    timeout-minutes: 30
    container:
      image: ${{ matrix.image }}

    strategy:
      fail-fast: false
      matrix:
        include:
          - runner: ubuntu-24.04
            target: x86_64-unknown-linux-musl
            variant: ubuntu-24.04
            image: ubuntu:24.04
            archive_name: codex-zsh-x86_64-unknown-linux-musl.tar.gz
          - runner: ubuntu-24.04-arm
            target: aarch64-unknown-linux-musl
            variant: ubuntu-24.04
            image: arm64v8/ubuntu:24.04
            archive_name: codex-zsh-aarch64-unknown-linux-musl.tar.gz

    steps:
      - name: Install build prerequisites
        shell: bash
        run: |
          set -euo pipefail
          apt-get update
          DEBIAN_FRONTEND=noninteractive apt-get install -y \
            autoconf \
            bison \
            build-essential \
            ca-certificates \
            gettext \
            git \
            libncursesw5-dev

      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

      - name: Build, smoke-test, and stage zsh artifact
        shell: bash
        run: |
          "${GITHUB_WORKSPACE}/.github/scripts/build-zsh-release-artifact.sh" \
            "dist/zsh/${{ matrix.target }}/${{ matrix.archive_name }}"

      - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
        with:
          name: codex-zsh-${{ matrix.target }}
          path: dist/zsh/${{ matrix.target }}/*

  darwin:
    name: Build zsh (macOS) - ${{ matrix.variant }} - ${{ matrix.target }}
    runs-on: ${{ matrix.runner }}
    timeout-minutes: 30

    strategy:
      fail-fast: false
      matrix:
        include:
          - runner: macos-15-xlarge
            target: aarch64-apple-darwin
            variant: macos-15
            archive_name: codex-zsh-aarch64-apple-darwin.tar.gz

    steps:
      - name: Install build prerequisites
        shell: bash
        run: |
          set -euo pipefail
          if ! command -v autoconf >/dev/null 2>&1; then
            brew install autoconf
          fi

      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

      - name: Build, smoke-test, and stage zsh artifact
        shell: bash
        run: |
          "${GITHUB_WORKSPACE}/.github/scripts/build-zsh-release-artifact.sh" \
            "dist/zsh/${{ matrix.target }}/${{ matrix.archive_name }}"

      - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
        with:
          name: codex-zsh-${{ matrix.target }}
          path: dist/zsh/${{ matrix.target }}/*
rusty-v8-release matrix .github/workflows/rusty-v8-release.yml
Triggers
workflow_dispatch
Runs on
ubuntu-latest, ${{ matrix.runner }}, ubuntu-latest
Jobs
metadata, build, publish-release
Matrix
include, include.platform, include.runner, include.target→ aarch64-unknown-linux-musl, linux_amd64_musl, linux_arm64_musl, ubuntu-24.04, ubuntu-24.04-arm, x86_64-unknown-linux-musl
Actions
bazelbuild/setup-bazelisk, softprops/action-gh-release
Commands
  • set -euo pipefail version="$(python3 .github/scripts/rusty_v8_bazel.py resolved-v8-crate-version)" echo "version=${version}" >> "$GITHUB_OUTPUT"
  • set -euo pipefail release_tag="${RELEASE_TAG_INPUT}" if [[ -z "${release_tag}" ]]; then release_tag="rusty-v8-v${V8_VERSION}" fi echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT"
  • set -euo pipefail target_suffix="${TARGET//-/_}" pair_target="//third_party/v8:rusty_v8_release_pair_${target_suffix}" extra_targets=() if [[ "${TARGET}" == *-unknown-linux-musl ]]; then extra_targets=( "@llvm//runtimes/libcxx:libcxx.static" "@llvm//runtimes/libcxx:libcxxabi.static" ) fi bazel_args=( build -c opt "--platforms=@llvm//platforms:${PLATFORM}" "${pair_target}" "${extra_targets[@]}" --build_metadata=COMMIT_SHA=$(git rev-parse HEAD) ) bazel \ --noexperimental_remote_repo_contents_cache \ "${bazel_args[@]}" \ --config=ci-v8 \ "--remote_header=x-buildbuddy-api-key=${BUILDBUDDY_API_KEY}"
  • set -euo pipefail python3 .github/scripts/rusty_v8_bazel.py stage-release-pair \ --platform "${PLATFORM}" \ --target "${TARGET}" \ --compilation-mode opt \ --output-dir "dist/${TARGET}"
  • set -euo pipefail echo "Publishing is only allowed from ${DEFAULT_BRANCH}; current ref is ${GITHUB_REF_NAME}." >&2 exit 1
  • set -euo pipefail if gh release view "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" > /dev/null 2>&1; then echo "Release tag ${RELEASE_TAG} already exists; musl artifact tags are immutable." >&2 exit 1 fi
View raw YAML
name: rusty-v8-release

on:
  workflow_dispatch:
    inputs:
      release_tag:
        description: Optional release tag. Defaults to rusty-v8-v<resolved_v8_version>.
        required: false
        type: string
      publish:
        description: Publish the staged musl artifacts to a GitHub release.
        required: false
        default: true
        type: boolean

concurrency:
  group: ${{ github.workflow }}::${{ inputs.release_tag || github.run_id }}
  cancel-in-progress: false

jobs:
  metadata:
    runs-on: ubuntu-latest
    outputs:
      release_tag: ${{ steps.release_tag.outputs.release_tag }}
      v8_version: ${{ steps.v8_version.outputs.version }}

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

      - name: Set up Python
        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
        with:
          python-version: "3.12"

      - name: Resolve exact v8 crate version
        id: v8_version
        shell: bash
        run: |
          set -euo pipefail
          version="$(python3 .github/scripts/rusty_v8_bazel.py resolved-v8-crate-version)"
          echo "version=${version}" >> "$GITHUB_OUTPUT"

      - name: Resolve release tag
        id: release_tag
        env:
          RELEASE_TAG_INPUT: ${{ inputs.release_tag }}
          V8_VERSION: ${{ steps.v8_version.outputs.version }}
        shell: bash
        run: |
          set -euo pipefail

          release_tag="${RELEASE_TAG_INPUT}"
          if [[ -z "${release_tag}" ]]; then
            release_tag="rusty-v8-v${V8_VERSION}"
          fi

          echo "release_tag=${release_tag}" >> "$GITHUB_OUTPUT"

  build:
    name: Build ${{ matrix.target }}
    needs: metadata
    runs-on: ${{ matrix.runner }}
    permissions:
      contents: read
      actions: read
    strategy:
      fail-fast: false
      matrix:
        include:
          - runner: ubuntu-24.04
            platform: linux_amd64_musl
            target: x86_64-unknown-linux-musl
          - runner: ubuntu-24.04-arm
            platform: linux_arm64_musl
            target: aarch64-unknown-linux-musl

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

      - name: Set up Bazel
        uses: bazelbuild/setup-bazelisk@6ecf4fd8b7d1f9721785f1dd656a689acf9add47 # v3

      - name: Set up Python
        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
        with:
          python-version: "3.12"

      - name: Build Bazel V8 release pair
        env:
          BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
          PLATFORM: ${{ matrix.platform }}
          TARGET: ${{ matrix.target }}
        shell: bash
        run: |
          set -euo pipefail

          target_suffix="${TARGET//-/_}"
          pair_target="//third_party/v8:rusty_v8_release_pair_${target_suffix}"
          extra_targets=()
          if [[ "${TARGET}" == *-unknown-linux-musl ]]; then
            extra_targets=(
              "@llvm//runtimes/libcxx:libcxx.static"
              "@llvm//runtimes/libcxx:libcxxabi.static"
            )
          fi

          bazel_args=(
            build
            -c
            opt
            "--platforms=@llvm//platforms:${PLATFORM}"
            "${pair_target}"
            "${extra_targets[@]}"
            --build_metadata=COMMIT_SHA=$(git rev-parse HEAD)
          )

          bazel \
            --noexperimental_remote_repo_contents_cache \
            "${bazel_args[@]}" \
            --config=ci-v8 \
            "--remote_header=x-buildbuddy-api-key=${BUILDBUDDY_API_KEY}"

      - name: Stage release pair
        env:
          PLATFORM: ${{ matrix.platform }}
          TARGET: ${{ matrix.target }}
        shell: bash
        run: |
          set -euo pipefail

          python3 .github/scripts/rusty_v8_bazel.py stage-release-pair \
            --platform "${PLATFORM}" \
            --target "${TARGET}" \
            --compilation-mode opt \
            --output-dir "dist/${TARGET}"

      - name: Upload staged musl artifacts
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
        with:
          name: rusty-v8-${{ needs.metadata.outputs.v8_version }}-${{ matrix.target }}
          path: dist/${{ matrix.target }}/*

  publish-release:
    if: ${{ inputs.publish }}
    needs:
      - metadata
      - build
    runs-on: ubuntu-latest
    permissions:
      contents: write
      actions: read

    steps:
      - name: Ensure publishing from default branch
        if: ${{ github.ref_name != github.event.repository.default_branch }}
        env:
          DEFAULT_BRANCH: ${{ github.event.repository.default_branch }}
        shell: bash
        run: |
          set -euo pipefail
          echo "Publishing is only allowed from ${DEFAULT_BRANCH}; current ref is ${GITHUB_REF_NAME}." >&2
          exit 1

      - name: Ensure release tag is new
        env:
          GH_TOKEN: ${{ github.token }}
          RELEASE_TAG: ${{ needs.metadata.outputs.release_tag }}
        shell: bash
        run: |
          set -euo pipefail

          if gh release view "${RELEASE_TAG}" --repo "${GITHUB_REPOSITORY}" > /dev/null 2>&1; then
            echo "Release tag ${RELEASE_TAG} already exists; musl artifact tags are immutable." >&2
            exit 1
          fi

      - uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8
        with:
          path: dist

      - name: Create GitHub Release
        uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe # v2
        with:
          tag_name: ${{ needs.metadata.outputs.release_tag }}
          name: ${{ needs.metadata.outputs.release_tag }}
          files: dist/**
          # Keep V8 artifact releases out of Codex's normal "latest release" channel.
          prerelease: true
sdk .github/workflows/sdk.yml
Triggers
push, pull_request
Runs on
Jobs
sdks
Actions
pnpm/action-setup
Commands
  • set -euo pipefail sudo apt-get update -y sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev
  • set -euo pipefail # Use the shared CI wrapper so fork PRs fall back cleanly when # BuildBuddy credentials are unavailable. This workflow needs the # built `codex` binary on disk afterwards, so ask the wrapper to # override CI's default remote_download_minimal behavior. ./.github/scripts/run-bazel-ci.sh \ --remote-download-toplevel \ -- \ build \ --build_metadata=COMMIT_SHA=${GITHUB_SHA} \ --build_metadata=TAG_job=sdk \ -- \ //codex-rs/cli:codex # Resolve the exact output file using the same wrapper/config path as # the build instead of guessing which Bazel convenience symlink is # available on the runner. cquery_output="$( ./.github/scripts/run-bazel-ci.sh \ -- \ cquery \ --output=files \ -- \ //codex-rs/cli:codex \ | grep -E '^(/|bazel-out/)' \ | tail -n 1 )" if [[ "${cquery_output}" = /* ]]; then codex_bazel_output_path="${cquery_output}" else codex_bazel_output_path="${GITHUB_WORKSPACE}/${cquery_output}" fi if [[ -z "${codex_bazel_output_path}" ]]; then echo "Bazel did not report an output path for //codex-rs/cli:codex." >&2 exit 1 fi if [[ ! -e "${codex_bazel_output_path}" ]]; then echo "Unable to locate the Bazel-built codex binary at ${codex_bazel_output_path}." >&2 exit 1 fi # Stage the binary into the workspace and point the SDK tests at that # stable path. The tests spawn `codex` directly many times, so using a # normal executable path is more reliable than invoking Bazel for each # test process. install_dir="${GITHUB_WORKSPACE}/.tmp/sdk-ci" mkdir -p "${install_dir}" install -m 755 "${codex_bazel_output_path}" "${install_dir}/codex" echo "CODEX_EXEC_PATH=${install_dir}/codex" >> "$GITHUB_ENV"
  • set -euo pipefail "${CODEX_EXEC_PATH}" --version
  • pnpm install --frozen-lockfile
  • pnpm -r --filter ./sdk/typescript run build
  • pnpm -r --filter ./sdk/typescript run lint
  • pnpm -r --filter ./sdk/typescript run test
View raw YAML
name: sdk

on:
  push:
    branches: [main]
  pull_request: {}

jobs:
  sdks:
    runs-on:
      group: codex-runners
      labels: codex-linux-x64
    timeout-minutes: 10
    steps:
      - name: Checkout repository
        uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

      - name: Install Linux bwrap build dependencies
        shell: bash
        run: |
          set -euo pipefail
          sudo apt-get update -y
          sudo DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends pkg-config libcap-dev

      - name: Setup pnpm
        uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5
        with:
          run_install: false

      - name: Setup Node.js
        uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6
        with:
          node-version: 22
          cache: pnpm

      - name: Set up Bazel CI
        id: setup_bazel
        uses: ./.github/actions/setup-bazel-ci
        with:
          target: x86_64-unknown-linux-gnu

      - name: Build codex with Bazel
        env:
          BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
        shell: bash
        run: |
          set -euo pipefail
          # Use the shared CI wrapper so fork PRs fall back cleanly when
          # BuildBuddy credentials are unavailable. This workflow needs the
          # built `codex` binary on disk afterwards, so ask the wrapper to
          # override CI's default remote_download_minimal behavior.
          ./.github/scripts/run-bazel-ci.sh \
            --remote-download-toplevel \
            -- \
            build \
            --build_metadata=COMMIT_SHA=${GITHUB_SHA} \
            --build_metadata=TAG_job=sdk \
            -- \
            //codex-rs/cli:codex

          # Resolve the exact output file using the same wrapper/config path as
          # the build instead of guessing which Bazel convenience symlink is
          # available on the runner.
          cquery_output="$(
            ./.github/scripts/run-bazel-ci.sh \
              -- \
              cquery \
              --output=files \
              -- \
              //codex-rs/cli:codex \
              | grep -E '^(/|bazel-out/)' \
              | tail -n 1
          )"
          if [[ "${cquery_output}" = /* ]]; then
            codex_bazel_output_path="${cquery_output}"
          else
            codex_bazel_output_path="${GITHUB_WORKSPACE}/${cquery_output}"
          fi
          if [[ -z "${codex_bazel_output_path}" ]]; then
            echo "Bazel did not report an output path for //codex-rs/cli:codex." >&2
            exit 1
          fi
          if [[ ! -e "${codex_bazel_output_path}" ]]; then
            echo "Unable to locate the Bazel-built codex binary at ${codex_bazel_output_path}." >&2
            exit 1
          fi

          # Stage the binary into the workspace and point the SDK tests at that
          # stable path. The tests spawn `codex` directly many times, so using a
          # normal executable path is more reliable than invoking Bazel for each
          # test process.
          install_dir="${GITHUB_WORKSPACE}/.tmp/sdk-ci"
          mkdir -p "${install_dir}"
          install -m 755 "${codex_bazel_output_path}" "${install_dir}/codex"
          echo "CODEX_EXEC_PATH=${install_dir}/codex" >> "$GITHUB_ENV"

      - name: Warm up Bazel-built codex
        shell: bash
        run: |
          set -euo pipefail
          "${CODEX_EXEC_PATH}" --version

      - name: Install dependencies
        run: pnpm install --frozen-lockfile

      - name: Build SDK packages
        run: pnpm -r --filter ./sdk/typescript run build

      - name: Lint SDK packages
        run: pnpm -r --filter ./sdk/typescript run lint

      - name: Test SDK packages
        run: pnpm -r --filter ./sdk/typescript run test

      - name: Save bazel repository cache
        if: always() && !cancelled() && steps.setup_bazel.outputs.cache-hit != 'true'
        continue-on-error: true
        uses: actions/cache/save@668228422ae6a00e4ad889ee87cd7109ec5666a7 # 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') }}
v8-canary matrix .github/workflows/v8-canary.yml
Triggers
pull_request, push, workflow_dispatch
Runs on
ubuntu-latest, ${{ matrix.runner }}
Jobs
metadata, build
Matrix
include, include.platform, include.runner, include.target→ aarch64-unknown-linux-musl, linux_amd64_musl, linux_arm64_musl, ubuntu-24.04, ubuntu-24.04-arm, x86_64-unknown-linux-musl
Actions
bazelbuild/setup-bazelisk
Commands
  • set -euo pipefail version="$(python3 .github/scripts/rusty_v8_bazel.py resolved-v8-crate-version)" echo "version=${version}" >> "$GITHUB_OUTPUT"
  • set -euo pipefail target_suffix="${TARGET//-/_}" pair_target="//third_party/v8:rusty_v8_release_pair_${target_suffix}" extra_targets=( "@llvm//runtimes/libcxx:libcxx.static" "@llvm//runtimes/libcxx:libcxxabi.static" ) bazel_args=( build "--platforms=@llvm//platforms:${PLATFORM}" "${pair_target}" "${extra_targets[@]}" --build_metadata=COMMIT_SHA=$(git rev-parse HEAD) ) bazel \ --noexperimental_remote_repo_contents_cache \ "${bazel_args[@]}" \ --config=ci-v8 \ "--remote_header=x-buildbuddy-api-key=${BUILDBUDDY_API_KEY}"
  • set -euo pipefail python3 .github/scripts/rusty_v8_bazel.py stage-release-pair \ --platform "${PLATFORM}" \ --target "${TARGET}" \ --output-dir "dist/${TARGET}"
View raw YAML
name: v8-canary

on:
  pull_request:
    paths:
      - ".github/scripts/rusty_v8_bazel.py"
      - ".github/workflows/rusty-v8-release.yml"
      - ".github/workflows/v8-canary.yml"
      - "MODULE.bazel"
      - "MODULE.bazel.lock"
      - "codex-rs/Cargo.toml"
      - "patches/BUILD.bazel"
      - "patches/v8_*.patch"
      - "third_party/v8/**"
  push:
    branches:
      - main
    paths:
      - ".github/scripts/rusty_v8_bazel.py"
      - ".github/workflows/rusty-v8-release.yml"
      - ".github/workflows/v8-canary.yml"
      - "MODULE.bazel"
      - "MODULE.bazel.lock"
      - "codex-rs/Cargo.toml"
      - "patches/BUILD.bazel"
      - "patches/v8_*.patch"
      - "third_party/v8/**"
  workflow_dispatch:

concurrency:
  group: ${{ github.workflow }}::${{ github.event.pull_request.number > 0 && format('pr-{0}', github.event.pull_request.number) || github.ref_name }}
  cancel-in-progress: ${{ github.ref_name != 'main' }}

jobs:
  metadata:
    runs-on: ubuntu-latest
    outputs:
      v8_version: ${{ steps.v8_version.outputs.version }}

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

      - name: Set up Python
        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
        with:
          python-version: "3.12"

      - name: Resolve exact v8 crate version
        id: v8_version
        shell: bash
        run: |
          set -euo pipefail
          version="$(python3 .github/scripts/rusty_v8_bazel.py resolved-v8-crate-version)"
          echo "version=${version}" >> "$GITHUB_OUTPUT"

  build:
    name: Build ${{ matrix.target }}
    needs: metadata
    runs-on: ${{ matrix.runner }}
    permissions:
      contents: read
      actions: read
    strategy:
      fail-fast: false
      matrix:
        include:
          - runner: ubuntu-24.04
            platform: linux_amd64_musl
            target: x86_64-unknown-linux-musl
          - runner: ubuntu-24.04-arm
            platform: linux_arm64_musl
            target: aarch64-unknown-linux-musl

    steps:
      - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

      - name: Set up Bazel
        uses: bazelbuild/setup-bazelisk@6ecf4fd8b7d1f9721785f1dd656a689acf9add47 # v3

      - name: Set up Python
        uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6
        with:
          python-version: "3.12"

      - name: Build Bazel V8 release pair
        env:
          BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
          PLATFORM: ${{ matrix.platform }}
          TARGET: ${{ matrix.target }}
        shell: bash
        run: |
          set -euo pipefail

          target_suffix="${TARGET//-/_}"
          pair_target="//third_party/v8:rusty_v8_release_pair_${target_suffix}"
          extra_targets=(
            "@llvm//runtimes/libcxx:libcxx.static"
            "@llvm//runtimes/libcxx:libcxxabi.static"
          )

          bazel_args=(
            build
            "--platforms=@llvm//platforms:${PLATFORM}"
            "${pair_target}"
            "${extra_targets[@]}"
            --build_metadata=COMMIT_SHA=$(git rev-parse HEAD)
          )

          bazel \
            --noexperimental_remote_repo_contents_cache \
            "${bazel_args[@]}" \
            --config=ci-v8 \
            "--remote_header=x-buildbuddy-api-key=${BUILDBUDDY_API_KEY}"

      - name: Stage release pair
        env:
          PLATFORM: ${{ matrix.platform }}
          TARGET: ${{ matrix.target }}
        shell: bash
        run: |
          set -euo pipefail

          python3 .github/scripts/rusty_v8_bazel.py stage-release-pair \
            --platform "${PLATFORM}" \
            --target "${TARGET}" \
            --output-dir "dist/${TARGET}"

      - name: Upload staged musl artifacts
        uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7
        with:
          name: v8-canary-${{ needs.metadata.outputs.v8_version }}-${{ matrix.target }}
          path: dist/${{ matrix.target }}/*