# 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 # ``` # # To use external macOS signing, manually dispatch `release_mode=build_unsigned`, # sign the unsigned macOS artifacts in a secure enclave, upload the signed handoff # archive as a GitHub Release asset, then manually dispatch # `release_mode=promote_signed` with `unsigned_run_id` and `signed_macos_asset`. # The signed handoff archive should contain target or artifact directories such # as `aarch64-apple-darwin/` with signed binaries and signed DMGs. name: rust-release on: push: tags: - "rust-v*.*.*" workflow_dispatch: inputs: release_mode: description: "build_unsigned creates unsigned macOS handoff artifacts; promote_signed finishes a release from signed macOS handoff artifacts." required: false type: choice default: build_unsigned options: - build_unsigned - promote_signed sign_macos: description: "Deprecated compatibility input; use release_mode instead." required: false type: boolean default: false unsigned_run_id: description: "For promote_signed: workflow run id from the build_unsigned run." required: false type: string signed_macos_asset: description: "For promote_signed: exact GitHub Release asset name containing signed macOS handoff artifacts." required: false type: string signed_macos_sha256: description: "For promote_signed: optional SHA-256 of signed_macos_asset." required: false type: string concurrency: group: ${{ github.workflow }} cancel-in-progress: true jobs: tag-check: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - uses: dtolnay/rust-toolchain@a0b273b48ed29de4470960879e8381ff45632f26 # 1.93.0 - name: Validate tag matches Cargo.toml version shell: bash env: RELEASE_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode || 'signed' }} REQUESTED_SIGN_MACOS: ${{ inputs.sign_macos }} SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} UNSIGNED_RUN_ID: ${{ inputs.unsigned_run_id }} run: | set -euo pipefail echo "::group::Tag validation" case "${RELEASE_MODE}" in signed) if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then echo "❌ Manual rust-release runs must use release_mode=build_unsigned or release_mode=promote_signed" exit 1 fi ;; build_unsigned) if [[ "${GITHUB_EVENT_NAME}" != "workflow_dispatch" ]]; then echo "❌ release_mode=build_unsigned is only valid for manual runs" exit 1 fi ;; promote_signed) if [[ "${GITHUB_EVENT_NAME}" != "workflow_dispatch" ]]; then echo "❌ release_mode=promote_signed is only valid for manual runs" exit 1 fi if [[ ! "${UNSIGNED_RUN_ID}" =~ ^[0-9]+$ ]]; then echo "❌ release_mode=promote_signed requires unsigned_run_id to be a workflow run id" exit 1 fi if [[ -z "${SIGNED_MACOS_ASSET}" ]]; then echo "❌ release_mode=promote_signed requires signed_macos_asset" exit 1 fi if [[ "${SIGNED_MACOS_ASSET}" == */* || "${SIGNED_MACOS_ASSET}" == *"*"* || "${SIGNED_MACOS_ASSET}" == *"?"* || "${SIGNED_MACOS_ASSET}" == *"["* ]]; then echo "❌ signed_macos_asset must be an exact release asset name, not a path or glob" exit 1 fi if [[ "${UNSIGNED_RUN_ID}" == "${GITHUB_RUN_ID}" ]]; then echo "❌ unsigned_run_id must refer to the earlier build_unsigned run, not this run" exit 1 fi ;; *) echo "❌ Unknown release_mode '${RELEASE_MODE}'" exit 1 ;; esac if [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" && "${REQUESTED_SIGN_MACOS}" == "true" ]]; then echo "::warning title=Deprecated sign_macos input ignored::Use release_mode=build_unsigned or release_mode=promote_signed instead." fi # 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: if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} needs: tag-check name: Build - ${{ matrix.runner }} - ${{ matrix.target }} - ${{ matrix.bundle }} runs-on: ${{ matrix.runs_on || matrix.runner }} # Release builds can take a long time, so leave some headroom to avoid # having to restart the full workflow due to a timeout. timeout-minutes: 90 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' }} SIGN_MACOS: ${{ github.event_name != 'workflow_dispatch' }} strategy: fail-fast: false matrix: include: - runner: macos-15-xlarge target: aarch64-apple-darwin bundle: primary artifact_name: aarch64-apple-darwin binaries: "codex codex-responses-api-proxy" build_dmg: "true" - runner: macos-15-xlarge target: aarch64-apple-darwin bundle: app-server artifact_name: aarch64-apple-darwin-app-server binaries: "codex-app-server" build_dmg: "false" - runner: macos-15-xlarge target: x86_64-apple-darwin bundle: primary artifact_name: x86_64-apple-darwin binaries: "codex codex-responses-api-proxy" build_dmg: "true" - runner: macos-15-xlarge target: x86_64-apple-darwin bundle: app-server artifact_name: x86_64-apple-darwin-app-server binaries: "codex-app-server" build_dmg: "false" # Release artifacts intentionally ship MUSL-linked Linux binaries. - runner: ubuntu-24.04 target: x86_64-unknown-linux-musl bundle: primary artifact_name: x86_64-unknown-linux-musl binaries: "codex codex-responses-api-proxy bwrap" build_dmg: "false" - runner: ubuntu-24.04 target: x86_64-unknown-linux-musl bundle: app-server artifact_name: x86_64-unknown-linux-musl-app-server binaries: "codex-app-server" build_dmg: "false" - runner: ubuntu-24.04-arm target: aarch64-unknown-linux-musl bundle: primary artifact_name: aarch64-unknown-linux-musl binaries: "codex codex-responses-api-proxy bwrap" build_dmg: "false" - runner: ubuntu-24.04-arm target: aarch64-unknown-linux-musl bundle: app-server artifact_name: aarch64-unknown-linux-musl-app-server binaries: "codex-app-server" build_dmg: "false" steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - 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.2.1 with: version: 0.14.0 use-cache: false - 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}" <> "$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 and verify checksums uses: ./.github/actions/setup-rusty-v8-musl with: target: ${{ matrix.target }} - if: ${{ contains(matrix.target, 'linux') && matrix.bundle == 'primary' }} name: Build bwrap and export digest shell: bash run: | set -euo pipefail target="${{ matrix.target }}" cargo build --target "$target" --release --timings --bin bwrap bwrap_path="target/${target}/release/bwrap" if [[ ! -f "$bwrap_path" ]]; then echo "bwrap binary ${bwrap_path} not found" exit 1 fi digest="$(sha256sum "$bwrap_path" | awk '{print $1}')" echo "CODEX_BWRAP_SHA256=${digest}" >> "$GITHUB_ENV" echo "Built bwrap ${bwrap_path} with sha256:${digest}" - name: Cargo build shell: bash run: | build_args=() for binary in ${{ matrix.binaries }}; do build_args+=(--bin "$binary") done echo "CARGO_PROFILE_RELEASE_LTO: ${CARGO_PROFILE_RELEASE_LTO}" cargo build --target ${{ matrix.target }} --release --timings "${build_args[@]}" - name: Upload Cargo timings uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: cargo-timings-rust-release-${{ matrix.target }}-${{ matrix.bundle }} path: codex-rs/target/**/cargo-timings/cargo-timing.html if-no-files-found: warn - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS != 'true' }} name: Stage unsigned macOS artifacts shell: bash run: | set -euo pipefail target="${{ matrix.target }}" release_dir="target/${target}/release" dest="unsigned-dist/${target}" mkdir -p "$dest" for binary in ${{ matrix.binaries }}; do binary_path="${release_dir}/${binary}" unsigned_name="${binary}-${target}-unsigned" unsigned_path="${dest}/${unsigned_name}" if [[ ! -f "${binary_path}" ]]; then echo "Binary ${binary_path} not found" exit 1 fi cp "${binary_path}" "${unsigned_path}" tar -C "$dest" -czf "${unsigned_path}.tar.gz" "${unsigned_name}" zstd -T0 -19 --rm "${unsigned_path}" done - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS != 'true' }} name: Upload unsigned macOS artifacts uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: ${{ matrix.artifact_name }}-unsigned path: codex-rs/unsigned-dist/${{ matrix.target }}/* if-no-files-found: error - 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 binaries: ${{ matrix.binaries }} - if: ${{ runner.os == 'macOS' && env.SIGN_MACOS == 'true' }} name: MacOS code signing (binaries) uses: ./.github/actions/macos-code-sign with: target: ${{ matrix.target }} binaries: ${{ matrix.binaries }} 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' && matrix.build_dmg == 'true' && env.SIGN_MACOS == 'true' }} 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. rm -rf "$dmg_root" mkdir -p "$dmg_root" for binary in ${{ matrix.binaries }}; do binary_path="${release_dir}/${binary}" if [[ ! -f "${binary_path}" ]]; then echo "Binary ${binary_path} not found" exit 1 fi ditto "${binary_path}" "${dmg_root}/${binary}" done 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' && matrix.build_dmg == 'true' && env.SIGN_MACOS == 'true' }} 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 if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} shell: bash run: | dest="dist/${{ matrix.target }}" mkdir -p "$dest" for binary in ${{ matrix.binaries }}; do cp "target/${{ matrix.target }}/release/${binary}" "$dest/${binary}-${{ matrix.target }}" if [[ "${{ matrix.target }}" == *linux* ]]; then cp "target/${{ matrix.target }}/release/${binary}.sigstore" \ "$dest/${binary}-${{ matrix.target }}.sigstore" fi done if [[ "${{ matrix.target }}" == *linux* && "${{ matrix.bundle }}" == "primary" ]]; then bundle_root="${RUNNER_TEMP}/codex-${{ matrix.target }}-bundle" rm -rf "$bundle_root" mkdir -p "$bundle_root/codex-resources" cp "$dest/codex-${{ matrix.target }}" "$bundle_root/codex" cp "$dest/bwrap-${{ matrix.target }}" "$bundle_root/codex-resources/bwrap" chmod 0755 "$bundle_root/codex" "$bundle_root/codex-resources/bwrap" tar -C "$bundle_root" -cf - codex codex-resources/bwrap | zstd -T0 -19 -o "$dest/codex-${{ matrix.target }}-bundle.tar.zst" fi if [[ "${{ matrix.build_dmg }}" == "true" ]]; then cp target/${{ matrix.target }}/release/codex-${{ matrix.target }}.dmg "$dest/codex-${{ matrix.target }}.dmg" fi - name: Build Python runtime wheel if: ${{ matrix.bundle == 'primary' && (runner.os != 'macOS' || env.SIGN_MACOS == 'true') }} shell: bash run: | set -euo pipefail case "${{ matrix.target }}" in aarch64-apple-darwin) platform_tag="macosx_11_0_arm64" ;; x86_64-apple-darwin) platform_tag="macosx_10_9_x86_64" ;; aarch64-unknown-linux-musl) platform_tag="musllinux_1_1_aarch64" ;; x86_64-unknown-linux-musl) platform_tag="musllinux_1_1_x86_64" ;; *) echo "No Python runtime wheel platform tag for ${{ matrix.target }}" exit 1 ;; esac python3 -m venv "${RUNNER_TEMP}/python-runtime-build-venv" # Do not install into the runner's system Python; macOS runners mark # the Homebrew Python as externally managed under PEP 668. "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m pip install build stage_dir="${RUNNER_TEMP}/openai-codex-cli-bin-${{ matrix.target }}" wheel_dir="${GITHUB_WORKSPACE}/python-runtime-dist/${{ matrix.target }}" stage_runtime_args=( "${GITHUB_WORKSPACE}/sdk/python/scripts/update_sdk_artifacts.py" stage-runtime "$stage_dir" "${GITHUB_WORKSPACE}/codex-rs/target/${{ matrix.target }}/release/codex" --codex-version "${GITHUB_REF_NAME}" --platform-tag "$platform_tag" ) if [[ "${{ matrix.target }}" == *linux* ]]; then # Keep bwrap in the runtime wheel so Linux sandbox fallback behavior # matches the standalone release bundle on hosts without system bwrap. stage_runtime_args+=( --resource-binary "${GITHUB_WORKSPACE}/codex-rs/target/${{ matrix.target }}/release/bwrap" ) fi python3 "${stage_runtime_args[@]}" "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m build --wheel --outdir "$wheel_dir" "$stage_dir" - name: Upload Python runtime wheel if: ${{ matrix.bundle == 'primary' && (runner.os != 'macOS' || env.SIGN_MACOS == 'true') }} uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: python-runtime-wheel-${{ matrix.target }} path: python-runtime-dist/${{ matrix.target }}/*.whl if-no-files-found: error - name: Compress artifacts if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} 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-.zst (existing) # codex-.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" == *.tar.zst || "$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.0.0 if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }} with: name: ${{ matrix.artifact_name }} # Upload the per-binary .zst files, .tar.gz equivalents, and any # prebuilt archives staged above. path: | codex-rs/dist/${{ matrix.target }}/* stage-signed-macos: if: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode == 'promote_signed' }} needs: tag-check name: Stage signed macOS handoff - ${{ matrix.target }} - ${{ matrix.bundle }} runs-on: macos-15-xlarge timeout-minutes: 30 permissions: contents: read defaults: run: working-directory: codex-rs strategy: fail-fast: false matrix: include: - target: aarch64-apple-darwin bundle: primary artifact_name: aarch64-apple-darwin binaries: "codex codex-responses-api-proxy" build_dmg: "true" - target: aarch64-apple-darwin bundle: app-server artifact_name: aarch64-apple-darwin-app-server binaries: "codex-app-server" build_dmg: "false" - target: x86_64-apple-darwin bundle: primary artifact_name: x86_64-apple-darwin binaries: "codex codex-responses-api-proxy" build_dmg: "true" - target: x86_64-apple-darwin bundle: app-server artifact_name: x86_64-apple-darwin-app-server binaries: "codex-app-server" build_dmg: "false" steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Download signed macOS handoff shell: bash env: GH_TOKEN: ${{ github.token }} SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} SIGNED_MACOS_SHA256: ${{ inputs.signed_macos_sha256 }} run: | set -euo pipefail download_dir="${RUNNER_TEMP}/signed-macos-download" handoff_dir="${RUNNER_TEMP}/signed-macos-handoff" rm -rf "$download_dir" "$handoff_dir" mkdir -p "$download_dir" "$handoff_dir" gh release download "$GITHUB_REF_NAME" \ --repo "$GITHUB_REPOSITORY" \ --pattern "$SIGNED_MACOS_ASSET" \ --dir "$download_dir" asset_count="$(find "$download_dir" -maxdepth 1 -type f | wc -l | tr -d '[:space:]')" if [[ "$asset_count" != "1" ]]; then echo "Expected exactly one signed macOS handoff asset named ${SIGNED_MACOS_ASSET}; found ${asset_count}" find "$download_dir" -maxdepth 1 -type f -print exit 1 fi asset_path="$(find "$download_dir" -maxdepth 1 -type f -print -quit)" if [[ -n "${SIGNED_MACOS_SHA256}" ]]; then expected_sha="$(printf '%s' "$SIGNED_MACOS_SHA256" | tr '[:upper:]' '[:lower:]')" actual_sha="$(shasum -a 256 "$asset_path" | awk '{print $1}')" if [[ "$actual_sha" != "$expected_sha" ]]; then echo "signed_macos_sha256 mismatch for ${SIGNED_MACOS_ASSET}" echo "expected: ${expected_sha}" echo "actual: ${actual_sha}" exit 1 fi fi asset_name="$(basename "$asset_path")" case "$asset_name" in *.tar.zst) zstd -dc "$asset_path" | tar -C "$handoff_dir" -xf - ;; *.tar.gz|*.tgz) tar -C "$handoff_dir" -xzf "$asset_path" ;; *.zip) ditto -x -k "$asset_path" "$handoff_dir" ;; *) echo "Unsupported signed macOS handoff archive format: ${asset_name}" exit 1 ;; esac echo "SIGNED_MACOS_HANDOFF_DIR=$handoff_dir" >> "$GITHUB_ENV" - name: Stage signed macOS artifacts shell: bash run: | set -euo pipefail target="${{ matrix.target }}" artifact_name="${{ matrix.artifact_name }}" source_dir="${SIGNED_MACOS_HANDOFF_DIR}/${artifact_name}" if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" ]]; then source_dir="${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" fi if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/${target}" ]]; then source_dir="${SIGNED_MACOS_HANDOFF_DIR}/${target}" fi if [[ ! -d "$source_dir" && -d "${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" ]]; then source_dir="${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" fi if [[ ! -d "$source_dir" ]]; then echo "Signed macOS handoff is missing ${artifact_name}/" echo "Expected either:" echo " ${SIGNED_MACOS_HANDOFF_DIR}/${artifact_name}" echo " ${SIGNED_MACOS_HANDOFF_DIR}/dist/${artifact_name}" echo " ${SIGNED_MACOS_HANDOFF_DIR}/${target}" echo " ${SIGNED_MACOS_HANDOFF_DIR}/dist/${target}" find "$SIGNED_MACOS_HANDOFF_DIR" -maxdepth 3 -type f -print exit 1 fi dest="dist/${target}" mkdir -p "$dest" for binary in ${{ matrix.binaries }}; do source_path="${source_dir}/${binary}" if [[ ! -f "$source_path" ]]; then source_path="${source_dir}/${binary}-${target}" fi if [[ ! -f "$source_path" ]]; then echo "Signed macOS handoff is missing ${binary} for ${artifact_name}" exit 1 fi release_path="${dest}/${binary}-${target}" ditto "$source_path" "$release_path" chmod 0755 "$release_path" codesign --verify --strict --verbose=2 "$release_path" done if [[ "${{ matrix.build_dmg }}" == "true" ]]; then dmg_name="codex-${target}.dmg" dmg_source="${source_dir}/${dmg_name}" if [[ ! -f "$dmg_source" ]]; then echo "Signed macOS handoff is missing ${dmg_name} for ${artifact_name}" exit 1 fi codesign --verify --strict --verbose=2 "$dmg_source" xcrun stapler validate "$dmg_source" cp "$dmg_source" "$dest/$dmg_name" fi - name: Build Python runtime wheel if: ${{ matrix.bundle == 'primary' }} shell: bash run: | set -euo pipefail case "${{ matrix.target }}" in aarch64-apple-darwin) platform_tag="macosx_11_0_arm64" ;; x86_64-apple-darwin) platform_tag="macosx_10_9_x86_64" ;; *) echo "No Python runtime wheel platform tag for ${{ matrix.target }}" exit 1 ;; esac python3 -m venv "${RUNNER_TEMP}/python-runtime-build-venv" "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m pip install build stage_dir="${RUNNER_TEMP}/openai-codex-cli-bin-${{ matrix.target }}" wheel_dir="${GITHUB_WORKSPACE}/python-runtime-dist/${{ matrix.target }}" python3 \ "${GITHUB_WORKSPACE}/sdk/python/scripts/update_sdk_artifacts.py" \ stage-runtime \ "$stage_dir" \ "${GITHUB_WORKSPACE}/codex-rs/dist/${{ matrix.target }}/codex-${{ matrix.target }}" \ --codex-version "${GITHUB_REF_NAME}" \ --platform-tag "$platform_tag" "${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m build --wheel --outdir "$wheel_dir" "$stage_dir" - name: Upload Python runtime wheel if: ${{ matrix.bundle == 'primary' }} uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: python-runtime-wheel-${{ matrix.target }} path: python-runtime-dist/${{ matrix.target }}/*.whl if-no-files-found: error - name: Compress artifacts shell: bash run: | set -euo pipefail dest="dist/${{ matrix.target }}" for f in "$dest"/*; do base="$(basename "$f")" if [[ "$base" == *.tar.gz || "$base" == *.tar.zst || "$base" == *.zip || "$base" == *.dmg ]]; then continue fi tar -C "$dest" -czf "$dest/${base}.tar.gz" "$base" zstd -T0 -19 --rm "$dest/$base" done - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: ${{ matrix.artifact_name }} path: | codex-rs/dist/${{ matrix.target }}/* build-windows: if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} 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: if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} name: argument-comment-lint release assets needs: tag-check uses: ./.github/workflows/rust-release-argument-comment-lint.yml with: publish: true zsh-release-assets: if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }} name: zsh release assets needs: tag-check uses: ./.github/workflows/rust-release-zsh.yml release: needs: - tag-check - build - stage-signed-macos - build-windows - argument-comment-lint-release-assets - zsh-release-assets if: >- ${{ always() && needs.tag-check.result == 'success' && ( ( github.event_name == 'workflow_dispatch' && inputs.release_mode == 'promote_signed' && needs.stage-signed-macos.result == 'success' && needs.build.result == 'skipped' && needs.build-windows.result == 'skipped' && needs.argument-comment-lint-release-assets.result == 'skipped' && needs.zsh-release-assets.result == 'skipped' ) || ( (github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed') && needs.build.result == 'success' && needs.stage-signed-macos.result == 'skipped' && needs.build-windows.result == 'success' && needs.argument-comment-lint-release-assets.result == 'success' && needs.zsh-release-assets.result == 'success' ) ) }} name: release runs-on: ubuntu-latest permissions: contents: write actions: read env: RELEASE_MODE: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode || 'signed' }} SIGN_MACOS: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode == 'promote_signed' }} SIGNED_MACOS_ASSET: ${{ inputs.signed_macos_asset }} UNSIGNED_RUN_ID: ${{ inputs.unsigned_run_id }} outputs: version: ${{ steps.release_name.outputs.name }} tag: ${{ github.ref_name }} sign_macos: ${{ steps.release_mode.outputs.sign_macos }} should_publish_npm: ${{ steps.npm_publish_settings.outputs.should_publish }} npm_tag: ${{ steps.npm_publish_settings.outputs.npm_tag }} should_publish_python_runtime: ${{ steps.python_runtime_publish_settings.outputs.should_publish }} steps: - name: Checkout repository uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: persist-credentials: false - name: Define release mode id: release_mode run: | echo "release_mode=${RELEASE_MODE}" >> "$GITHUB_OUTPUT" echo "sign_macos=${SIGN_MACOS}" >> "$GITHUB_OUTPUT" - 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.0.1 with: path: dist - name: Validate unsigned build run if: ${{ env.RELEASE_MODE == 'promote_signed' }} env: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail run_summary="$(gh run view "$UNSIGNED_RUN_ID" \ --repo "$GITHUB_REPOSITORY" \ --json conclusion,event,headBranch,headSha,status,workflowName,url \ --jq '[.workflowName, .event, .headBranch, .headSha, .status, .conclusion, .url] | @tsv')" IFS=$'\t' read -r workflow_name event head_branch head_sha status conclusion run_url <<< "$run_summary" expected_head_sha="$(git rev-parse "${GITHUB_SHA}^{commit}")" if [[ "$workflow_name" != "$GITHUB_WORKFLOW" ]]; then echo "unsigned_run_id ${UNSIGNED_RUN_ID} is for workflow '${workflow_name}', expected '${GITHUB_WORKFLOW}'" echo "Run URL: ${run_url}" exit 1 fi if [[ "$event" != "workflow_dispatch" ]]; then echo "unsigned_run_id ${UNSIGNED_RUN_ID} was triggered by '${event}', expected 'workflow_dispatch'" echo "Run URL: ${run_url}" exit 1 fi if [[ "$head_branch" != "$GITHUB_REF_NAME" ]]; then echo "unsigned_run_id ${UNSIGNED_RUN_ID} used ref '${head_branch}', expected '${GITHUB_REF_NAME}'" echo "Run URL: ${run_url}" exit 1 fi if [[ "$head_sha" != "$expected_head_sha" ]]; then echo "unsigned_run_id ${UNSIGNED_RUN_ID} used head SHA '${head_sha}', expected '${expected_head_sha}'" echo "Run URL: ${run_url}" exit 1 fi if [[ "$status" != "completed" || "$conclusion" != "success" ]]; then echo "unsigned_run_id ${UNSIGNED_RUN_ID} is ${status}/${conclusion}, expected completed/success" echo "Run URL: ${run_url}" exit 1 fi - name: Download artifacts from unsigned build run if: ${{ env.RELEASE_MODE == 'promote_signed' }} env: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail gh run download "$UNSIGNED_RUN_ID" \ --repo "$GITHUB_REPOSITORY" \ --dir dist - name: Remove unsigned macOS staging artifacts if: ${{ env.RELEASE_MODE == 'promote_signed' }} run: | set -euo pipefail find dist -mindepth 1 -maxdepth 1 -type d \ -name '*-apple-darwin*-unsigned' \ -exec rm -rf {} + - name: Re-upload promoted Linux x64 artifacts if: ${{ env.RELEASE_MODE == 'promote_signed' }} uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: x86_64-unknown-linux-musl path: dist/x86_64-unknown-linux-musl/* if-no-files-found: error - name: Re-upload promoted Linux arm64 artifacts if: ${{ env.RELEASE_MODE == 'promote_signed' }} uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: aarch64-unknown-linux-musl path: dist/aarch64-unknown-linux-musl/* if-no-files-found: error - name: Re-upload promoted Windows x64 artifacts if: ${{ env.RELEASE_MODE == 'promote_signed' }} uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: x86_64-pc-windows-msvc path: dist/x86_64-pc-windows-msvc/* if-no-files-found: error - name: Re-upload promoted Windows arm64 artifacts if: ${{ env.RELEASE_MODE == 'promote_signed' }} uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: aarch64-pc-windows-msvc path: dist/aarch64-pc-windows-msvc/* if-no-files-found: error - name: List run: ls -R dist/ - name: Prune artifacts excluded from unsigned macOS release if: ${{ env.SIGN_MACOS == 'false' }} run: | find dist -mindepth 1 -maxdepth 1 -type d \ ! -name '*-apple-darwin*-unsigned' \ ! -name 'aarch64-unknown-linux-musl' \ ! -name 'aarch64-unknown-linux-musl-app-server' \ ! -name 'x86_64-unknown-linux-musl' \ ! -name 'x86_64-unknown-linux-musl-app-server' \ ! -name 'aarch64-pc-windows-msvc' \ ! -name 'x86_64-pc-windows-msvc' \ -exec rm -rf {} + if ! find dist -type f -name '*-apple-darwin*-unsigned*' | grep -q .; then echo "No unsigned macOS artifacts found in downloaded workflow artifacts." exit 1 fi - 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 [[ "${SIGN_MACOS}" != "true" ]]; then echo "should_publish=false" >> "$GITHUB_OUTPUT" echo "npm_tag=" >> "$GITHUB_OUTPUT" exit 0 fi 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: Determine Python runtime publish settings id: python_runtime_publish_settings env: VERSION: ${{ steps.release_name.outputs.name }} run: | set -euo pipefail version="${VERSION}" if [[ "${SIGN_MACOS}" != "true" ]]; then echo "should_publish=false" >> "$GITHUB_OUTPUT" exit 0 fi if [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "should_publish=true" >> "$GITHUB_OUTPUT" elif [[ "${version}" =~ ^[0-9]+\.[0-9]+\.[0-9]+-alpha\.[0-9]+$ ]]; then echo "should_publish=true" >> "$GITHUB_OUTPUT" else echo "should_publish=false" >> "$GITHUB_OUTPUT" fi - name: Setup pnpm if: ${{ env.SIGN_MACOS == 'true' }} uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5 with: run_install: false - name: Setup Node.js for npm packaging if: ${{ env.SIGN_MACOS == 'true' }} uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 with: node-version: 22 - name: Install dependencies if: ${{ env.SIGN_MACOS == 'true' }} run: pnpm install --frozen-lockfile # stage_npm_packages.py requires DotSlash when staging releases. - uses: facebook/install-dotslash@1e4e7b3e07eaca387acb98f1d4720e0bee8dbb6a # v2 - name: Stage npm packages if: ${{ env.SIGN_MACOS == 'true' }} env: GH_TOKEN: ${{ github.token }} RELEASE_VERSION: ${{ steps.release_name.outputs.name }} run: | workflow_url="${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}/actions/runs/${GITHUB_RUN_ID}" ./scripts/stage_npm_packages.py \ --release-version "$RELEASE_VERSION" \ --workflow-url "$workflow_url" \ --package codex \ --package codex-responses-api-proxy \ --package codex-sdk - name: Stage installer scripts if: ${{ env.SIGN_MACOS == 'true' }} 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.6.1 with: name: ${{ steps.release_name.outputs.name }} tag_name: ${{ github.ref_name }} body_path: ${{ steps.release_notes.outputs.path }} files: dist/** overwrite_files: true make_latest: ${{ env.SIGN_MACOS == 'true' && !contains(steps.release_name.outputs.name, '-') }} # 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, '-') }} - name: Clean up signed promotion handoff assets if: ${{ env.RELEASE_MODE == 'promote_signed' }} env: GH_TOKEN: ${{ github.token }} run: | set -euo pipefail release_id="$(gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${GITHUB_REF_NAME}" --jq '.id')" gh api --paginate "repos/${GITHUB_REPOSITORY}/releases/${release_id}/assets" \ --jq '.[] | [.id, .name] | @tsv' | while IFS=$'\t' read -r asset_id asset_name; do if [[ -z "$asset_id" || -z "$asset_name" ]]; then continue fi delete_asset=false if [[ "$asset_name" == *unsigned* || "$asset_name" == "$SIGNED_MACOS_ASSET" ]]; then delete_asset=true fi if [[ "$delete_asset" == "true" ]]; then echo "Deleting release asset ${asset_name}" gh api -X DELETE "repos/${GITHUB_REPOSITORY}/releases/assets/${asset_id}" fi done - if: ${{ env.SIGN_MACOS == 'true' }} uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag: ${{ github.ref_name }} config: .github/dotslash-config.json - if: ${{ env.SIGN_MACOS == 'true' }} uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag: ${{ github.ref_name }} config: .github/dotslash-zsh-config.json - if: ${{ env.SIGN_MACOS == 'true' }} 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: ${{ env.SIGN_MACOS == 'true' && !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. # promote_signed intentionally skips build jobs that are ancestors of release; # include the !cancelled() status function so Actions does not apply its implicit # success() check to the whole dependency chain before evaluating release outputs. if: >- ${{ !cancelled() && needs.release.result == 'success' && 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.3.0 with: # Node 24 bundles npm >= 11.5.1, which trusted publishing requires. node-version: 24 registry-url: "https://registry.npmjs.org" scope: "@openai" - 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 root_tarball="dist/npm/codex-npm-${VERSION}.tgz" sdk_tarball="dist/npm/codex-sdk-npm-${VERSION}.tgz" # Keep this list in sync with CODEX_PLATFORM_PACKAGES in # codex-cli/scripts/build_npm_package.py. The root wrapper advances # @openai/codex@latest as soon as it publishes, so every platform # package it aliases must already exist in the registry first. platform_tarballs=( "dist/npm/codex-npm-linux-x64-${VERSION}.tgz" "dist/npm/codex-npm-linux-arm64-${VERSION}.tgz" "dist/npm/codex-npm-darwin-x64-${VERSION}.tgz" "dist/npm/codex-npm-darwin-arm64-${VERSION}.tgz" "dist/npm/codex-npm-win32-x64-${VERSION}.tgz" "dist/npm/codex-npm-win32-arm64-${VERSION}.tgz" ) for required_tarball in "${platform_tarballs[@]}" "${root_tarball}"; do if [[ ! -f "${required_tarball}" ]]; then echo "Missing npm tarball: ${required_tarball}" exit 1 fi done shopt -s nullglob other_tarballs=() for tarball in dist/npm/*-"${VERSION}".tgz; do if [[ "${tarball}" == "${root_tarball}" || "${tarball}" == "${sdk_tarball}" ]]; then continue fi is_platform_tarball=false for platform_tarball in "${platform_tarballs[@]}"; do if [[ "${tarball}" == "${platform_tarball}" ]]; then is_platform_tarball=true break fi done if [[ "${is_platform_tarball}" == true ]]; then continue fi other_tarballs+=("${tarball}") done # Publish the platform packages before the root CLI wrapper. The root # wrapper advances @openai/codex@latest, so it should only publish # after the optional dependency versions it references exist. tarballs=( "${platform_tarballs[@]}" "${other_tarballs[@]}" "${root_tarball}" ) if [[ -f "${sdk_tarball}" ]]; then tarballs+=("${sdk_tarball}") 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 # Publish the platform-specific Python runtime wheels using PyPI trusted publishing. # PyPI project configuration must trust this workflow and job. Keep this # non-blocking while the Python runtime publishing path is new; failures still # need release follow-up, but should not invalidate the Rust release itself. publish-python-runtime: # Publish to PyPI for stable releases and alpha pre-releases with numeric suffixes. if: >- ${{ !cancelled() && needs.release.result == 'success' && needs.release.outputs.should_publish_python_runtime == 'true' }} name: publish-python-runtime needs: release runs-on: ubuntu-latest continue-on-error: true environment: pypi permissions: id-token: write # Required for PyPI trusted publishing. contents: read steps: - name: Download Python runtime wheels from release env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} RELEASE_TAG: ${{ needs.release.outputs.tag }} RELEASE_VERSION: ${{ needs.release.outputs.version }} run: | set -euo pipefail python_version="$RELEASE_VERSION" python_version="${python_version/-alpha./a}" python_version="${python_version/-beta./b}" python_version="${python_version/-rc./rc}" mkdir -p dist/python-runtime gh release download "$RELEASE_TAG" \ --repo "${GITHUB_REPOSITORY}" \ --pattern "openai_codex_cli_bin-${python_version}-*.whl" \ --dir dist/python-runtime ls -lh dist/python-runtime - name: Publish Python runtime wheels to PyPI uses: pypa/gh-action-pypi-publish@ed0c53931b1dc9bd32cbe73a98c7f6766f8a527e # v1.13.0 with: packages-dir: dist/python-runtime skip-existing: true 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: >- ${{ !cancelled() && needs.release.result == 'success' && needs.release.outputs.sign_macos == 'true' && !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 if: >- ${{ !cancelled() && needs.release.result == 'success' && needs.release.outputs.sign_macos == 'true' }} 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