# 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@v6
      - uses: dtolnay/rust-toolchain@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@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@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@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@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

  shell-tool-mcp:
    name: shell-tool-mcp
    needs: tag-check
    uses: ./.github/workflows/shell-tool-mcp.yml
    with:
      release-tag: ${{ github.ref_name }}
      publish: true
    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

  release:
    needs:
      - build
      - build-windows
      - shell-tool-mcp
      - argument-comment-lint-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@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@v8
        with:
          path: dist

      - name: List
        run: ls -R dist/

      # This is a temporary fix: we should modify shell-tool-mcp.yml so these
      # files do not end up in dist/ in the first place.
      - name: Delete entries from dist/ that should not go in the release
        run: |
          rm -rf dist/shell-tool-mcp*
          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@v4
        with:
          run_install: false

      - name: Setup Node.js for npm packaging
        uses: actions/setup-node@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@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@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@v2
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag: ${{ github.ref_name }}
          config: .github/dotslash-config.json

      - uses: facebook/dotslash-publish-release@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@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@19e706d4c9121098010096f9c495a70a7518b30f
        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
