# 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-.zst (existing) # codex-.tar.gz (new) # codex-.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" \ --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