# 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@v5

      - 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: ${{ matrix.runner }} - ${{ matrix.target }}
    runs-on: ${{ matrix.runner }}
    timeout-minutes: 30
    defaults:
      run:
        working-directory: codex-rs

    strategy:
      fail-fast: false
      matrix:
        include:
          - runner: macos-14
            target: aarch64-apple-darwin
          - runner: macos-14
            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
          - runner: windows-latest
            target: x86_64-pc-windows-msvc
          - runner: windows-11-arm
            target: aarch64-pc-windows-msvc

    steps:
      - uses: actions/checkout@v5
      - uses: dtolnay/rust-toolchain@1.89
        with:
          targets: ${{ matrix.target }}

      - uses: actions/cache@v4
        with:
          path: |
            ~/.cargo/bin/
            ~/.cargo/registry/index/
            ~/.cargo/registry/cache/
            ~/.cargo/git/db/
            ${{ github.workspace }}/codex-rs/target/
          key: cargo-${{ matrix.runner }}-${{ matrix.target }}-release-${{ hashFiles('**/Cargo.lock') }}

      - if: ${{ matrix.target == 'x86_64-unknown-linux-musl' || matrix.target == 'aarch64-unknown-linux-musl'}}
        name: Install musl build tools
        run: |
          sudo apt install -y musl-tools pkg-config

      - name: Cargo build
        run: cargo build --target ${{ matrix.target }} --release --bin codex

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

          if [[ "${{ matrix.runner }}" == windows* ]]; then
            cp target/${{ matrix.target }}/release/codex.exe "$dest/codex-${{ matrix.target }}.exe"
          else
            cp target/${{ matrix.target }}/release/codex "$dest/codex-${{ matrix.target }}"
          fi

      - if: ${{ matrix.runner == 'windows-11-arm' }}
        name: Install zstd
        shell: powershell
        run: choco install -y zstandard

      - 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` for all platforms and `.zip` for
          # Windows alongside every single binary that we publish. The end result is:
          #   codex-<target>.zst          (existing)
          #   codex-<target>.tar.gz       (new)
          #   codex-<target>.zip          (only for Windows)

          # 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 ]]; 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 [[ "${{ matrix.runner }}" == windows* ]]; then
              (cd "$dest" && 7z a "${base}.zip" "$base")
            fi

            # Also create .zst (existing behaviour) *and* remove the original
            # uncompressed binary to keep the directory small.
            zstd -T0 -19 --rm "$dest/$base"
          done

      - uses: actions/upload-artifact@v4
        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 }}/*

  release:
    needs: build
    name: release
    runs-on: ubuntu-latest
    permissions:
      contents: write
      actions: read
    outputs:
      version: ${{ steps.release_name.outputs.name }}
      tag: ${{ github.ref_name }}

    steps:
      - name: Checkout repository
        uses: actions/checkout@v5

      - uses: actions/download-artifact@v4
        with:
          path: dist

      - name: List
        run: ls -R dist/

      - 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: Stage npm package
        env:
          GH_TOKEN: ${{ github.token }}
        run: |
          set -euo pipefail
          TMP_DIR="${RUNNER_TEMP}/npm-stage"
          python3 codex-cli/scripts/stage_rust_release.py \
            --release-version "${{ steps.release_name.outputs.name }}" \
            --tmp "${TMP_DIR}"
          mkdir -p dist/npm
          # Produce an npm-ready tarball using `npm pack` and store it in dist/npm.
          # We then rename it to a stable name used by our publishing script.
          (cd "$TMP_DIR" && npm pack --pack-destination "${GITHUB_WORKSPACE}/dist/npm")
          mv "${GITHUB_WORKSPACE}"/dist/npm/*.tgz \
             "${GITHUB_WORKSPACE}/dist/npm/codex-npm-${{ steps.release_name.outputs.name }}.tgz"

      - name: Create GitHub Release
        uses: softprops/action-gh-release@v2
        with:
          name: ${{ steps.release_name.outputs.name }}
          tag_name: ${{ github.ref_name }}
          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

  # 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:
    # Skip this step for pre-releases (alpha/beta).
    if: ${{ !contains(needs.release.outputs.version, '-') }}
    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@v5
        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 tarball from release
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        run: |
          set -euo pipefail
          version="${{ needs.release.outputs.version }}"
          tag="${{ needs.release.outputs.tag }}"
          mkdir -p dist/npm
          gh release download "$tag" \
            --repo "${GITHUB_REPOSITORY}" \
            --pattern "codex-npm-${version}.tgz" \
            --dir dist/npm

      # No NODE_AUTH_TOKEN needed because we use OIDC.
      - name: Publish to npm
        run: npm publish "${GITHUB_WORKSPACE}/dist/npm/codex-npm-${{ needs.release.outputs.version }}.tgz"

  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
