Files
codex/.github/workflows/rust-release.yml
2025-12-06 13:11:58 -08:00

608 lines
22 KiB
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*.*.*"
# DO NOT SUBMIT
pull_request: {}
concurrency:
group: ${{ github.workflow }}
cancel-in-progress: true
jobs:
tag-check:
if: github.event_name != 'pull_request'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- 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
if: ${{ github.event_name == 'pull_request' || needs.tag-check.result == 'success' || needs.tag-check.result == 'skipped' }}
name: Build - ${{ matrix.runner }} - ${{ matrix.target }}
runs-on: ${{ matrix.runner }}
timeout-minutes: 30
permissions:
contents: read
id-token: write
defaults:
run:
working-directory: codex-rs
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
- runner: windows-latest
target: x86_64-pc-windows-msvc
- runner: windows-11-arm
target: aarch64-pc-windows-msvc
steps:
- uses: actions/checkout@v6
- uses: dtolnay/rust-toolchain@1.90
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-get update
sudo apt-get install -y musl-tools pkg-config
- name: Cargo build
run: cargo build --target ${{ matrix.target }} --release --bin codex --bin codex-responses-api-proxy
- if: ${{ contains(matrix.target, 'linux') && (github.event_name != 'pull_request' || github.event.pull_request.head.repo.fork == false) }}
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: ${{ contains(matrix.target, 'windows') }}
# 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: ${{ secrets.AZURE_TRUSTED_SIGNING_CERTIFICATE_PROFILE }}
# - if: ${{ matrix.runner == 'macos-15-xlarge' }}
# name: Configure Apple code signing
# shell: bash
# env:
# KEYCHAIN_PASSWORD: actions
# APPLE_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE_P12 }}
# APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
# run: |
# set -euo pipefail
# if [[ -z "${APPLE_CERTIFICATE:-}" ]]; then
# echo "APPLE_CERTIFICATE is required for macOS signing"
# exit 1
# fi
# if [[ -z "${APPLE_CERTIFICATE_PASSWORD:-}" ]]; then
# echo "APPLE_CERTIFICATE_PASSWORD is required for macOS signing"
# exit 1
# fi
# cert_path="${RUNNER_TEMP}/apple_signing_certificate.p12"
# echo "$APPLE_CERTIFICATE" | base64 -d > "$cert_path"
# keychain_path="${RUNNER_TEMP}/codex-signing.keychain-db"
# security create-keychain -p "$KEYCHAIN_PASSWORD" "$keychain_path"
# security set-keychain-settings -lut 21600 "$keychain_path"
# security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$keychain_path"
# keychain_args=()
# cleanup_keychain() {
# if ((${#keychain_args[@]} > 0)); then
# security list-keychains -s "${keychain_args[@]}" || true
# security default-keychain -s "${keychain_args[0]}" || true
# else
# security list-keychains -s || true
# fi
# if [[ -f "$keychain_path" ]]; then
# security delete-keychain "$keychain_path" || true
# fi
# }
# while IFS= read -r keychain; do
# [[ -n "$keychain" ]] && keychain_args+=("$keychain")
# done < <(security list-keychains | sed 's/^[[:space:]]*//;s/[[:space:]]*$//;s/"//g')
# if ((${#keychain_args[@]} > 0)); then
# security list-keychains -s "$keychain_path" "${keychain_args[@]}"
# else
# security list-keychains -s "$keychain_path"
# fi
# security default-keychain -s "$keychain_path"
# security import "$cert_path" -k "$keychain_path" -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security
# security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$keychain_path" > /dev/null
# codesign_hashes=()
# while IFS= read -r hash; do
# [[ -n "$hash" ]] && codesign_hashes+=("$hash")
# done < <(security find-identity -v -p codesigning "$keychain_path" \
# | sed -n 's/.*\([0-9A-F]\{40\}\).*/\1/p' \
# | sort -u)
# if ((${#codesign_hashes[@]} == 0)); then
# echo "No signing identities found in $keychain_path"
# cleanup_keychain
# rm -f "$cert_path"
# exit 1
# fi
# if ((${#codesign_hashes[@]} > 1)); then
# echo "Multiple signing identities found in $keychain_path:"
# printf ' %s\n' "${codesign_hashes[@]}"
# cleanup_keychain
# rm -f "$cert_path"
# exit 1
# fi
# APPLE_CODESIGN_IDENTITY="${codesign_hashes[0]}"
# rm -f "$cert_path"
# echo "APPLE_CODESIGN_IDENTITY=$APPLE_CODESIGN_IDENTITY" >> "$GITHUB_ENV"
# echo "APPLE_CODESIGN_KEYCHAIN=$keychain_path" >> "$GITHUB_ENV"
# echo "::add-mask::$APPLE_CODESIGN_IDENTITY"
# - if: ${{ matrix.runner == 'macos-15-xlarge' }}
# name: Sign macOS binaries
# shell: bash
# run: |
# set -euo pipefail
# if [[ -z "${APPLE_CODESIGN_IDENTITY:-}" ]]; then
# echo "APPLE_CODESIGN_IDENTITY is required for macOS signing"
# exit 1
# fi
# keychain_args=()
# if [[ -n "${APPLE_CODESIGN_KEYCHAIN:-}" && -f "${APPLE_CODESIGN_KEYCHAIN}" ]]; then
# keychain_args+=(--keychain "${APPLE_CODESIGN_KEYCHAIN}")
# fi
# for binary in codex codex-responses-api-proxy; do
# path="target/${{ matrix.target }}/release/${binary}"
# codesign --force --options runtime --timestamp --sign "$APPLE_CODESIGN_IDENTITY" "${keychain_args[@]}" "$path"
# done
# - if: ${{ matrix.runner == 'macos-15-xlarge' }}
# name: Notarize macOS binaries
# shell: bash
# env:
# 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 }}
# run: |
# set -euo pipefail
# for var in APPLE_NOTARIZATION_KEY_P8 APPLE_NOTARIZATION_KEY_ID APPLE_NOTARIZATION_ISSUER_ID; do
# if [[ -z "${!var:-}" ]]; then
# echo "$var is required for notarization"
# exit 1
# fi
# done
# notary_key_path="${RUNNER_TEMP}/notarytool.key.p8"
# echo "$APPLE_NOTARIZATION_KEY_P8" | base64 -d > "$notary_key_path"
# cleanup_notary() {
# rm -f "$notary_key_path"
# }
# trap cleanup_notary EXIT
# notarize_binary() {
# local binary="$1"
# local source_path="target/${{ matrix.target }}/release/${binary}"
# local archive_path="${RUNNER_TEMP}/${binary}.zip"
# if [[ ! -f "$source_path" ]]; then
# echo "Binary $source_path not found"
# exit 1
# fi
# rm -f "$archive_path"
# ditto -c -k --keepParent "$source_path" "$archive_path"
# submission_json=$(xcrun notarytool submit "$archive_path" \
# --key "$notary_key_path" \
# --key-id "$APPLE_NOTARIZATION_KEY_ID" \
# --issuer "$APPLE_NOTARIZATION_ISSUER_ID" \
# --output-format json \
# --wait)
# status=$(printf '%s\n' "$submission_json" | jq -r '.status // "Unknown"')
# submission_id=$(printf '%s\n' "$submission_json" | jq -r '.id // ""')
# if [[ -z "$submission_id" ]]; then
# echo "Failed to retrieve submission ID for $binary"
# exit 1
# fi
# echo "::notice title=Notarization::$binary submission ${submission_id} completed with status ${status}"
# if [[ "$status" != "Accepted" ]]; then
# echo "Notarization failed for ${binary} (submission ${submission_id}, status ${status})"
# exit 1
# fi
# }
# notarize_binary "codex"
# notarize_binary "codex-responses-api-proxy"
- 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"
cp target/${{ matrix.target }}/release/codex-responses-api-proxy.exe "$dest/codex-responses-api-proxy-${{ matrix.target }}.exe"
else
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 }}"
fi
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.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 }}"
# We want to ship the raw Windows executables in the GitHub Release
# in addition to the compressed archives. Keep the originals for
# Windows targets; remove them elsewhere to limit the number of
# artifacts that end up in the GitHub Release.
keep_originals=false
if [[ "${{ matrix.runner }}" == windows* ]]; then
keep_originals=true
fi
# 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
# 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 [[ "${{ 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_args=(-T0 -19)
if [[ "${keep_originals}" == false ]]; then
zstd_args+=(--rm)
fi
zstd "${zstd_args[@]}" "$dest/$base"
done
# - name: Remove signing keychain
# if: ${{ always() && matrix.runner == 'macos-15-xlarge' }}
# shell: bash
# env:
# APPLE_CODESIGN_KEYCHAIN: ${{ env.APPLE_CODESIGN_KEYCHAIN }}
# run: |
# set -euo pipefail
# if [[ -n "${APPLE_CODESIGN_KEYCHAIN:-}" ]]; then
# keychain_args=()
# while IFS= read -r keychain; do
# [[ "$keychain" == "$APPLE_CODESIGN_KEYCHAIN" ]] && continue
# [[ -n "$keychain" ]] && keychain_args+=("$keychain")
# done < <(security list-keychains | sed 's/^[[:space:]]*//;s/[[:space:]]*$//;s/"//g')
# if ((${#keychain_args[@]} > 0)); then
# security list-keychains -s "${keychain_args[@]}"
# security default-keychain -s "${keychain_args[0]}"
# fi
# if [[ -f "$APPLE_CODESIGN_KEYCHAIN" ]]; then
# security delete-keychain "$APPLE_CODESIGN_KEYCHAIN"
# fi
# fi
- uses: actions/upload-artifact@v5
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 }}/*
# 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
# release:
# needs:
# - build
# - shell-tool-mcp
# 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
# - uses: actions/download-artifact@v4
# 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*
# 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: 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@v5
# 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 }}
# run: |
# ./scripts/stage_npm_packages.py \
# --release-version "${{ steps.release_name.outputs.name }}" \
# --package codex \
# --package codex-responses-api-proxy \
# --package codex-sdk
# - 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:
# # 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@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 tarballs 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
# gh release download "$tag" \
# --repo "${GITHUB_REPOSITORY}" \
# --pattern "codex-responses-api-proxy-npm-${version}.tgz" \
# --dir dist/npm
# gh release download "$tag" \
# --repo "${GITHUB_REPOSITORY}" \
# --pattern "codex-sdk-npm-${version}.tgz" \
# --dir dist/npm
# # 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
# tag_args=()
# if [[ -n "${NPM_TAG}" ]]; then
# tag_args+=(--tag "${NPM_TAG}")
# fi
# tarballs=(
# "codex-npm-${VERSION}.tgz"
# "codex-responses-api-proxy-npm-${VERSION}.tgz"
# "codex-sdk-npm-${VERSION}.tgz"
# )
# for tarball in "${tarballs[@]}"; do
# npm publish "${GITHUB_WORKSPACE}/dist/npm/${tarball}" "${tag_args[@]}"
# done
# 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