Download .github/workflows/rust-release.yml from SaylorTwift/codex: direct link, hf CLI and curl.
- Browser
- Download file 80.2 kB
-
https://huggingface.co/SaylorTwift/codex/resolve/main/.github/workflows/rust-release.yml
- Command line
-
hf download hf://SaylorTwift/codex/.github/workflows/rust-release.yml
-
curl -L -o rust-release.yml https://huggingface.co/SaylorTwift/codex/resolve/main/.github/workflows/rust-release.yml
80.2 kB
| # 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 | |
| # ``` | |
| # | |
| # Tag releases sign macOS binaries and DMGs through the protected `codesigning` | |
| # GitHub environment and Azure Key Vault before final verification on macOS. | |
| name: rust-release | |
| on: | |
| push: | |
| tags: | |
| - "rust-v*.*.*" | |
| env: | |
| CODEX_REPO_ROOT: ${{ github.workspace }} | |
| CODEX_ZSH_RELEASE_TAG: codex-zsh-v0.1.0 | |
| CODEX_ZSH_MANIFEST_SHA256: c534eab89dcea7e3d8a5e5b3f49c025c7c64cd4e4d9814ee24871de58a9359a1 | |
| concurrency: | |
| group: ${{ github.workflow }} | |
| cancel-in-progress: false | |
| jobs: | |
| tag-check: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0 | |
| - name: Validate tag matches Cargo.toml version | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| echo "::group::Tag validation" | |
| # Release runs must come from a tag. | |
| [[ "${GITHUB_REF_TYPE}" == "tag" ]] \ | |
| || { echo "❌ Not a tag ref"; exit 1; } | |
| # Release tags must match the version in Cargo.toml. | |
| # Keep this pattern in sync with VERSION_RE in | |
| # .github/scripts/publish_r2_release.py. | |
| [[ "${GITHUB_REF_NAME}" =~ ^rust-v[0-9]+\.[0-9]+\.[0-9]+(-(alpha(\.[0-9]+){0,2}|beta(\.[0-9]+)?))?$ ]] \ | |
| || { echo "❌ Tag '${GITHUB_REF_NAME}' doesn't match expected format"; exit 1; } | |
| tag_ver="${GITHUB_REF_NAME#rust-v}" | |
| cargo_ver="$(grep -m1 '^version' codex-rs/Cargo.toml \ | |
| | sed -E 's/version *= *"([^"]+)".*/\1/')" | |
| [[ "${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 }} - ${{ matrix.bundle }} | |
| runs-on: ${{ matrix.runs_on || matrix.runner }} | |
| # Linux releases also build the native voice runtime in this job. | |
| timeout-minutes: 120 | |
| permissions: | |
| contents: read | |
| id-token: write | |
| defaults: | |
| run: | |
| working-directory: codex-rs | |
| env: | |
| # macOS release packages archive packed dSYM bundles before stripping. | |
| CARGO_PROFILE_RELEASE_SPLIT_DEBUGINFO: ${{ contains(matrix.target, 'apple-darwin') && 'packed' || 'off' }} | |
| # Use the git CLI instead of Cargo's libgit2 path for git dependencies. | |
| # macOS release runners have intermittently failed to fetch nested | |
| # submodules through SecureTransport/libgit2, especially libwebrtc's | |
| # libyuv submodule from chromium.googlesource.com. | |
| CARGO_NET_GIT_FETCH_WITH_CLI: "true" | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - runner: macos-15-xlarge | |
| target: aarch64-apple-darwin | |
| bundle: primary | |
| artifact_name: aarch64-apple-darwin | |
| binaries: "codex codex-code-mode-host 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 codex-code-mode-host" | |
| build_dmg: "false" | |
| - runner: macos-15-xlarge | |
| target: x86_64-apple-darwin | |
| bundle: primary | |
| artifact_name: x86_64-apple-darwin | |
| binaries: "codex codex-code-mode-host 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 codex-code-mode-host" | |
| build_dmg: "false" | |
| # Release artifacts intentionally ship MUSL-linked Linux binaries. | |
| - runner: ${{ github.event.repository.name }}-linux-x64-xl | |
| target: x86_64-unknown-linux-musl | |
| bundle: primary | |
| artifact_name: x86_64-unknown-linux-musl | |
| binaries: "codex codex-code-mode-host codex-responses-api-proxy bwrap" | |
| build_dmg: "false" | |
| - runner: ${{ github.event.repository.name }}-linux-x64-xl | |
| target: x86_64-unknown-linux-musl | |
| bundle: app-server | |
| artifact_name: x86_64-unknown-linux-musl-app-server | |
| binaries: "codex-app-server codex-code-mode-host" | |
| build_dmg: "false" | |
| - runner: ${{ github.event.repository.name }}-linux-arm64 | |
| target: aarch64-unknown-linux-musl | |
| bundle: primary | |
| artifact_name: aarch64-unknown-linux-musl | |
| binaries: "codex codex-code-mode-host codex-responses-api-proxy bwrap" | |
| build_dmg: "false" | |
| - runner: ${{ github.event.repository.name }}-linux-arm64 | |
| target: aarch64-unknown-linux-musl | |
| bundle: app-server | |
| artifact_name: aarch64-unknown-linux-musl-app-server | |
| binaries: "codex-app-server codex-code-mode-host" | |
| 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 binutils pkg-config libcap-dev | |
| - uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.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: Disable aws-lc jitter entropy (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" | |
| - name: Configure rusty_v8 artifact overrides and verify checksums | |
| uses: ./.github/actions/setup-rusty-v8 | |
| with: | |
| target: ${{ matrix.target }} | |
| - if: ${{ contains(matrix.target, 'linux') }} | |
| 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 | |
| # Codex embeds this digest at build time and verifies the bundled | |
| # bwrap resource before use. Strip bwrap before hashing so the digest | |
| # covers the exact bytes that the release packages. | |
| strip --strip-debug --strip-unneeded "$bwrap_path" | |
| 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: | | |
| target="${{ matrix.target }}" | |
| if [[ "$target" == "x86_64-pc-windows-msvc" ]]; then | |
| export LIBSQLITE3_FLAGS=SQLITE_DISABLE_INTRINSIC | |
| fi | |
| build_args=() | |
| for binary in ${{ matrix.binaries }}; do | |
| # bwrap was built, finalized, and hashed before this build so | |
| # Codex can embed the digest of the bytes that will be packaged. | |
| if [[ "$binary" == "bwrap" ]]; then | |
| continue | |
| fi | |
| build_args+=(--bin "$binary") | |
| done | |
| STABLE_GIT_COMMIT="$(git rev-parse HEAD)" | |
| export STABLE_GIT_COMMIT | |
| cargo build --target "$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 | |
| - name: Build symbols archive and strip binaries | |
| shell: bash | |
| run: | | |
| binaries=() | |
| for binary in ${{ matrix.binaries }}; do | |
| # bwrap is already stripped before hashing. Its symbols are not | |
| # useful enough to justify a separate pre-Codex symbols pass. | |
| if [[ "$binary" == "bwrap" ]]; then | |
| continue | |
| fi | |
| binaries+=("$binary") | |
| done | |
| bash "${GITHUB_WORKSPACE}/.github/scripts/archive-release-symbols-and-strip-binaries.sh" \ | |
| --target "${{ matrix.target }}" \ | |
| --artifact-name "${{ matrix.artifact_name }}" \ | |
| --release-dir "target/${{ matrix.target }}/release" \ | |
| --archive-dir "symbols-dist/${{ matrix.artifact_name }}" \ | |
| --binaries "${binaries[*]}" | |
| - name: Upload symbols archive | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: ${{ matrix.artifact_name }}-symbols | |
| path: codex-rs/symbols-dist/${{ matrix.artifact_name }}/* | |
| if-no-files-found: error | |
| - name: Set up Bazel for Linux voice | |
| if: ${{ matrix.bundle == 'primary' && contains(matrix.target, 'linux') }} | |
| uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0 | |
| with: | |
| bazelisk-version: 1.28.1 | |
| - name: Build Linux voice runtime | |
| if: ${{ matrix.bundle == 'primary' && contains(matrix.target, 'linux') }} | |
| shell: bash | |
| env: | |
| APP_TARGET: ${{ matrix.target }} | |
| run: | | |
| set -euo pipefail | |
| cd "$GITHUB_WORKSPACE" | |
| voice_target="${APP_TARGET%-musl}-gnu" | |
| case "$voice_target" in | |
| aarch64-unknown-linux-gnu) prefix=linux_aarch64 ;; | |
| x86_64-unknown-linux-gnu) prefix=linux_x86_64 ;; | |
| *) exit 1 ;; | |
| esac | |
| bazel build -c opt //codex-rs/voice-host:codex-voice-host //third_party/voice:native_runtime | |
| source="bazel-bin/third_party/voice/native_runtime_${prefix}" | |
| output="${RUNNER_TEMP}/signed-voice/${APP_TARGET}" | |
| mkdir -p "$output" | |
| python3 third_party/voice/release_runtime.py stage \ | |
| --target "$voice_target" --source "$source" --output "$output/runtime" | |
| cp bazel-bin/codex-rs/voice-host/codex-voice-host "$output/codex-voice-host" | |
| chmod 0755 "$output/codex-voice-host" | |
| python3 third_party/voice/release_runtime.py seal \ | |
| --target "$voice_target" --output "$output/runtime" | |
| - if: ${{ runner.os == 'macOS' }} | |
| 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' }} | |
| 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 }} | |
| - name: Stage artifacts | |
| if: ${{ runner.os != 'macOS' }} | |
| shell: bash | |
| run: | | |
| dest="dist/${{ matrix.target }}" | |
| mkdir -p "$dest" | |
| for binary in ${{ matrix.binaries }}; do | |
| # Both variants package the host, but only the primary bundle publishes | |
| # standalone binary archives to avoid duplicate release asset names. | |
| if [[ "${{ matrix.bundle }}" == "app-server" && "$binary" == "codex-code-mode-host" ]]; then | |
| continue | |
| fi | |
| 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.build_dmg }}" == "true" ]]; then | |
| cp target/${{ matrix.target }}/release/codex-${{ matrix.target }}.dmg "$dest/codex-${{ matrix.target }}.dmg" | |
| fi | |
| - name: Download packaged zsh manifest | |
| if: ${{ runner.os != 'macOS' }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| curl -fsSL \ | |
| "https://github.com/${GITHUB_REPOSITORY}/releases/download/${CODEX_ZSH_RELEASE_TAG}/codex-zsh" \ | |
| -o "${RUNNER_TEMP}/codex-zsh" | |
| bash "${GITHUB_WORKSPACE}/.github/scripts/verify-zsh-manifest.sh" \ | |
| "${RUNNER_TEMP}/codex-zsh" "$CODEX_ZSH_MANIFEST_SHA256" | |
| - name: Build Codex package archive | |
| if: ${{ runner.os != 'macOS' }} | |
| shell: bash | |
| env: | |
| TARGET: ${{ matrix.target }} | |
| BUNDLE: ${{ matrix.bundle }} | |
| run: | | |
| set -euo pipefail | |
| voice_args=() | |
| if [[ "$BUNDLE" == "primary" && "$TARGET" == *-unknown-linux-musl ]]; then | |
| voice_args+=(--voice-release-dir "${RUNNER_TEMP}/signed-voice/${TARGET}") | |
| voice_args+=(--release-version "${GITHUB_REF_NAME#rust-v}") | |
| fi | |
| bash "${GITHUB_WORKSPACE}/.github/scripts/build-codex-package-archive.sh" \ | |
| --target "$TARGET" \ | |
| --bundle "$BUNDLE" \ | |
| --entrypoint-dir "target/${TARGET}/release" \ | |
| --archive-dir "dist/${TARGET}" \ | |
| --zsh-manifest "${RUNNER_TEMP}/codex-zsh" \ | |
| "${voice_args[@]}" | |
| - name: Cosign Linux voice package archives | |
| if: ${{ matrix.bundle == 'primary' && contains(matrix.target, 'linux') }} | |
| uses: ./.github/actions/linux-code-sign | |
| with: | |
| target: ${{ matrix.target }} | |
| artifacts-dir: ${{ github.workspace }}/codex-rs/dist/${{ matrix.target }} | |
| binaries: codex-package-${{ matrix.target }}.tar.gz codex-package-${{ matrix.target }}.tar.zst | |
| - name: Build Python runtime wheel | |
| if: ${{ matrix.bundle == 'primary' && runner.os != 'macOS' }} | |
| 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="manylinux_2_17_aarch64" | |
| ;; | |
| x86_64-unknown-linux-musl) | |
| platform_tag="manylinux_2_17_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 | |
| # Keep the existing manylinux_2_17 wheel compatible with older glibc. | |
| # GNU voice libraries belong in the signed release package archives, | |
| # but their minimum glibc version is not covered by this wheel tag. | |
| wheel_archives="${RUNNER_TEMP}/voice-free-wheel/${{ matrix.target }}" | |
| bash "${GITHUB_WORKSPACE}/.github/scripts/build-codex-package-archive.sh" \ | |
| --target "${{ matrix.target }}" \ | |
| --bundle primary \ | |
| --entrypoint-dir "target/${{ matrix.target }}/release" \ | |
| --archive-dir "$wheel_archives" \ | |
| --zsh-manifest "${RUNNER_TEMP}/codex-zsh" | |
| wheel_archive="${wheel_archives}/codex-package-${{ matrix.target }}.tar.gz" | |
| python3 - "$wheel_archive" <<'PY' | |
| import sys | |
| import tarfile | |
| with tarfile.open(sys.argv[1]) as archive: | |
| assert not any("codex-resources/voice/" in item.name for item in archive) | |
| PY | |
| 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" | |
| "$wheel_archive" | |
| --codex-version "${GITHUB_REF_NAME}" | |
| --platform-tag "$platform_tag" | |
| ) | |
| 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' }} | |
| 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' }} | |
| 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" == *.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' }} | |
| 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 }}/* | |
| build-macos-voice: | |
| needs: tag-check | |
| name: Build voice runtime - ${{ matrix.target }} | |
| runs-on: ${{ matrix.runner }} | |
| timeout-minutes: 120 | |
| permissions: | |
| contents: read | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - target: aarch64-apple-darwin | |
| runner: macos-15 | |
| prefix: macos_aarch64 | |
| - target: x86_64-apple-darwin | |
| runner: macos-15-intel | |
| prefix: macos_x86_64 | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 | |
| with: | |
| python-version: "3.12" | |
| - uses: bazel-contrib/setup-bazel@c5acdfb288317d0b5c0bbd7a396a3dc868bb0f86 # 0.19.0 | |
| with: | |
| bazelisk-version: 1.28.1 | |
| - name: Build matching helper and native runtime | |
| shell: bash | |
| env: | |
| TARGET: ${{ matrix.target }} | |
| PREFIX: ${{ matrix.prefix }} | |
| run: | | |
| set -euo pipefail | |
| # Release tags bump Cargo.toml without rewriting the workspace lockfile. | |
| # Refresh workspace versions before Bazel reads the Cargo dependency graph. | |
| (cd codex-rs && cargo update --workspace) | |
| bazel build -c opt //codex-rs/voice-host:codex-voice-host //third_party/voice:native_runtime | |
| runtime="bazel-bin/third_party/voice/native_runtime_${PREFIX}" | |
| PYTHONPATH=third_party/voice python3 - "$runtime" "$TARGET" <<'PY' | |
| from pathlib import Path | |
| import sys | |
| from package_runtime import runtime_files | |
| runtime_files(Path(sys.argv[1]).resolve(strict=True), sys.argv[2]) | |
| PY | |
| mkdir -p "voice-unsigned/${TARGET}" | |
| cp -R "$runtime" "voice-unsigned/${TARGET}/runtime" | |
| # GNU tar extraction and signing need writable copies of Bazel outputs. | |
| chmod -R u+w "voice-unsigned/${TARGET}/runtime" | |
| cp bazel-bin/codex-rs/voice-host/codex-voice-host "voice-unsigned/${TARGET}/codex-voice-host" | |
| tar -C "voice-unsigned/${TARGET}" -czf "voice-unsigned-${TARGET}.tar.gz" runtime codex-voice-host | |
| - uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: voice-${{ matrix.target }}-unsigned | |
| path: voice-unsigned-${{ matrix.target }}.tar.gz | |
| if-no-files-found: error | |
| sign-macos-binaries: | |
| needs: [build, build-macos-voice] | |
| if: ${{ always() && needs.build.result == 'success' && needs.build-macos-voice.result == 'success' }} | |
| name: Sign macOS binaries - ${{ matrix.target }} - ${{ matrix.bundle }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| environment: | |
| name: codesigning | |
| deployment: false | |
| permissions: | |
| contents: read | |
| id-token: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - target: aarch64-apple-darwin | |
| bundle: primary | |
| artifact_name: aarch64-apple-darwin | |
| binaries: "codex codex-code-mode-host codex-responses-api-proxy" | |
| - target: aarch64-apple-darwin | |
| bundle: app-server | |
| artifact_name: aarch64-apple-darwin-app-server | |
| binaries: "codex-app-server codex-code-mode-host" | |
| - target: x86_64-apple-darwin | |
| bundle: primary | |
| artifact_name: x86_64-apple-darwin | |
| binaries: "codex codex-code-mode-host codex-responses-api-proxy" | |
| - target: x86_64-apple-darwin | |
| bundle: app-server | |
| artifact_name: x86_64-apple-darwin-app-server | |
| binaries: "codex-app-server codex-code-mode-host" | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Download unsigned macOS binaries | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: ${{ matrix.artifact_name }}-unsigned | |
| path: ${{ runner.temp }}/unsigned-macos | |
| - name: Download unsigned voice runtime | |
| if: ${{ matrix.bundle == 'primary' }} | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: voice-${{ matrix.target }}-unsigned | |
| path: ${{ runner.temp }}/unsigned-voice | |
| - name: Set up AKV PKCS11 macOS signing | |
| uses: ./.github/actions/setup-akv-pkcs11-codesigning | |
| with: | |
| rcodesign-blob-uri: ${{ secrets.AKV_CODESIGN_RCODESIGN_BLOB_URI }} | |
| rcodesign-sha256: ${{ secrets.AKV_CODESIGN_RCODESIGN_SHA256 }} | |
| akv-pkcs11-library-blob-uri: ${{ secrets.AKV_CODESIGN_PKCS11_LIBRARY_BLOB_URI }} | |
| akv-pkcs11-library-sha256: ${{ secrets.AKV_CODESIGN_PKCS11_LIBRARY_SHA256 }} | |
| azure-client-id: ${{ secrets.AKV_CODESIGN_AZURE_CLIENT_ID }} | |
| azure-tenant-id: ${{ secrets.AKV_CODESIGN_TENANT }} | |
| azure-subscription-id: ${{ secrets.AKV_CODESIGN_SUBSCRIPTION }} | |
| key-vault-name: ${{ secrets.AKV_CODESIGN_KEY_VAULT_NAME }} | |
| key-name: ${{ secrets.AKV_CODESIGN_KEY_NAME }} | |
| key-version: ${{ secrets.AKV_CODESIGN_KEY_VERSION || '' }} | |
| certificate-sha256: ${{ secrets.AKV_CODESIGN_CERTIFICATE_SHA256 || '' }} | |
| - name: Sign and notarize macOS binaries | |
| shell: bash | |
| env: | |
| TARGET: ${{ matrix.target }} | |
| BINARIES: ${{ matrix.binaries }} | |
| APPLE_NOTARIZATION_AKV_KEY_NAME: ${{ secrets.AKV_NOTARIZATION_KEY_NAME }} | |
| APPLE_NOTARIZATION_AKV_KEY_VERSION: ${{ secrets.AKV_NOTARIZATION_KEY_VERSION }} | |
| run: | | |
| set -euo pipefail | |
| input_dir="${RUNNER_TEMP}/unsigned-macos" | |
| output_dir="${GITHUB_WORKSPACE}/signed-macos/${TARGET}" | |
| report_dir="${GITHUB_WORKSPACE}/macos-binary-signing-verification/${TARGET}" | |
| mkdir -p "$output_dir" "$report_dir" | |
| for binary in ${BINARIES}; do | |
| unsigned_path="${input_dir}/${binary}-${TARGET}-unsigned.zst" | |
| signed_path="${output_dir}/${binary}" | |
| if [[ ! -f "$unsigned_path" ]]; then | |
| echo "Unsigned binary $unsigned_path not found" | |
| exit 1 | |
| fi | |
| zstd -d --stdout "$unsigned_path" >"$signed_path" | |
| chmod 0755 "$signed_path" | |
| entitlements="${GITHUB_WORKSPACE}/.github/scripts/macos-signing/${binary}.entitlements.plist" | |
| if [[ ! -f "$entitlements" ]]; then | |
| echo "Entitlements file $entitlements not found" | |
| exit 1 | |
| fi | |
| .github/scripts/macos-signing/sign_macos_code.sh \ | |
| --target "$signed_path" \ | |
| --identity unused \ | |
| --deep false \ | |
| --identifier "$binary" \ | |
| --options runtime \ | |
| --timestamp true \ | |
| --entitlements "$entitlements" | |
| mkdir -p "${report_dir}/${binary}" | |
| rcodesign print-signature-info "$signed_path" \ | |
| >"${report_dir}/${binary}/signature-info.yaml" | |
| .github/scripts/macos-signing/notarize_macos_binary_with_akv.sh \ | |
| --binary "$signed_path" \ | |
| --report-dir "${report_dir}/${binary}" | |
| done | |
| - name: Fetch, sign, and notarize pinned macOS helpers | |
| if: ${{ matrix.bundle == 'primary' }} | |
| shell: bash | |
| env: | |
| TARGET: ${{ matrix.target }} | |
| APPLE_NOTARIZATION_AKV_KEY_NAME: ${{ secrets.AKV_NOTARIZATION_KEY_NAME }} | |
| APPLE_NOTARIZATION_AKV_KEY_VERSION: ${{ secrets.AKV_NOTARIZATION_KEY_VERSION }} | |
| run: | | |
| set -euo pipefail | |
| signed_root="${GITHUB_WORKSPACE}/signed-resources/${TARGET}" | |
| zsh_manifest="${RUNNER_TEMP}/codex-zsh-${TARGET}" | |
| mkdir -p "$signed_root" | |
| curl -fsSL \ | |
| "https://github.com/${GITHUB_REPOSITORY}/releases/download/${CODEX_ZSH_RELEASE_TAG}/codex-zsh" \ | |
| -o "$zsh_manifest" | |
| bash "${GITHUB_WORKSPACE}/.github/scripts/verify-zsh-manifest.sh" \ | |
| "$zsh_manifest" "$CODEX_ZSH_MANIFEST_SHA256" | |
| PYTHONPATH="${GITHUB_WORKSPACE}/scripts" python3 - "$TARGET" "$signed_root" "$zsh_manifest" <<'PY' | |
| import shutil | |
| import sys | |
| from pathlib import Path | |
| from codex_package.ripgrep import fetch_rg | |
| from codex_package.targets import TARGET_SPECS | |
| from codex_package.zsh import resolve_zsh_bin | |
| spec = TARGET_SPECS[sys.argv[1]] | |
| signed_root = Path(sys.argv[2]) | |
| zsh_bin = resolve_zsh_bin(spec, Path(sys.argv[3])) | |
| if zsh_bin is None: | |
| raise RuntimeError(f"Pinned zsh release is missing {spec.target}") | |
| shutil.copy2(fetch_rg(spec), signed_root / "rg") | |
| shutil.copy2(zsh_bin, signed_root / "zsh") | |
| PY | |
| for resource in rg zsh; do | |
| binary="${signed_root}/${resource}" | |
| report_dir="${GITHUB_WORKSPACE}/macos-binary-signing-verification/${TARGET}/${resource}" | |
| mkdir -p "$report_dir" | |
| chmod 0755 "$binary" | |
| .github/scripts/macos-signing/sign_macos_code.sh \ | |
| --target "$binary" \ | |
| --identity unused \ | |
| --deep false \ | |
| --identifier "com.openai.codex.${resource}" \ | |
| --options runtime \ | |
| --timestamp true | |
| rcodesign print-signature-info "$binary" \ | |
| >"${report_dir}/signature-info.yaml" | |
| .github/scripts/macos-signing/notarize_macos_binary_with_akv.sh \ | |
| --binary "$binary" \ | |
| --report-dir "$report_dir" | |
| done | |
| - name: Sign and notarize voice runtime | |
| if: ${{ matrix.bundle == 'primary' }} | |
| shell: bash | |
| env: | |
| TARGET: ${{ matrix.target }} | |
| APPLE_NOTARIZATION_AKV_KEY_NAME: ${{ secrets.AKV_NOTARIZATION_KEY_NAME }} | |
| APPLE_NOTARIZATION_AKV_KEY_VERSION: ${{ secrets.AKV_NOTARIZATION_KEY_VERSION }} | |
| run: | | |
| set -euo pipefail | |
| unsigned="${RUNNER_TEMP}/unsigned-voice/extracted" | |
| signed="${GITHUB_WORKSPACE}/signed-voice/${TARGET}" | |
| mkdir -p "$unsigned" "$signed" | |
| tar -xzf "${RUNNER_TEMP}/unsigned-voice/voice-unsigned-${TARGET}.tar.gz" -C "$unsigned" | |
| python3 third_party/voice/release_runtime.py stage \ | |
| --target "$TARGET" --source "$unsigned/runtime" --output "$signed/runtime" | |
| cp "$unsigned/codex-voice-host" "$signed/codex-voice-host" | |
| chmod 0755 "$signed/codex-voice-host" | |
| while IFS= read -r -d '' library; do | |
| name="$(basename "$library")" | |
| .github/scripts/macos-signing/sign_macos_code.sh \ | |
| --target "$library" --identity unused --deep false \ | |
| --identifier "com.openai.codex.voice.${name}" \ | |
| --options runtime --timestamp true | |
| done < <(find "$signed/runtime" -name '*.dylib' -type f -print0) | |
| .github/scripts/macos-signing/sign_macos_code.sh \ | |
| --target "$signed/codex-voice-host" --identity unused --deep false \ | |
| --identifier com.openai.codex.voice-host --options runtime --timestamp true \ | |
| --entitlements .github/scripts/macos-signing/codex-voice-host.entitlements.plist | |
| # Notarize the actual signed closure together, then hash those bytes. | |
| (cd "$signed" && zip -qr "${RUNNER_TEMP}/voice-${TARGET}.zip" runtime codex-voice-host) | |
| report_dir="${GITHUB_WORKSPACE}/macos-binary-signing-verification/${TARGET}/voice" | |
| mkdir -p "$report_dir" | |
| python3 .github/scripts/macos-signing/notarize_with_akv.py \ | |
| --file "${RUNNER_TEMP}/voice-${TARGET}.zip" \ | |
| --report-log "${report_dir}/notarization.json" \ | |
| --max-wait-seconds 600 | |
| python3 third_party/voice/release_runtime.py seal \ | |
| --target "$TARGET" --output "$signed/runtime" | |
| tar -C "$signed" -czf "${GITHUB_WORKSPACE}/signed-voice-${TARGET}.tar.gz" \ | |
| runtime codex-voice-host | |
| - name: Upload signed voice runtime | |
| if: ${{ matrix.bundle == 'primary' }} | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: voice-${{ matrix.target }}-signed | |
| path: signed-voice-${{ matrix.target }}.tar.gz | |
| if-no-files-found: error | |
| - name: Upload signed macOS binaries | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: ${{ matrix.artifact_name }}-signed-binaries | |
| path: signed-macos/${{ matrix.target }}/* | |
| if-no-files-found: error | |
| - name: Upload signed macOS helpers | |
| if: ${{ matrix.bundle == 'primary' }} | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: ${{ matrix.target }}-signed-resources | |
| path: signed-resources/${{ matrix.target }}/* | |
| if-no-files-found: error | |
| - name: Upload binary signing verification | |
| if: ${{ always() }} | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: ${{ matrix.artifact_name }}-binary-signing-verification | |
| path: macos-binary-signing-verification/${{ matrix.target }}/ | |
| if-no-files-found: warn | |
| package-macos: | |
| needs: sign-macos-binaries | |
| name: Package macOS artifacts - ${{ matrix.target }} - ${{ matrix.bundle }} | |
| runs-on: macos-15-xlarge | |
| timeout-minutes: 45 | |
| 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-code-mode-host 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 codex-code-mode-host" | |
| build_dmg: "false" | |
| - target: x86_64-apple-darwin | |
| bundle: primary | |
| artifact_name: x86_64-apple-darwin | |
| binaries: "codex codex-code-mode-host 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 codex-code-mode-host" | |
| build_dmg: "false" | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Download signed macOS binaries | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: ${{ matrix.artifact_name }}-signed-binaries | |
| path: codex-rs/target/${{ matrix.target }}/release | |
| - name: Download signed macOS helpers | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: ${{ matrix.target }}-signed-resources | |
| path: codex-rs/signed-resources/${{ matrix.target }} | |
| - name: Download signed voice runtime | |
| if: ${{ matrix.bundle == 'primary' }} | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: voice-${{ matrix.target }}-signed | |
| path: ${{ runner.temp }}/signed-voice | |
| - name: Verify signed voice runtime | |
| if: ${{ matrix.bundle == 'primary' }} | |
| shell: bash | |
| env: | |
| TARGET: ${{ matrix.target }} | |
| run: | | |
| set -euo pipefail | |
| root="${RUNNER_TEMP}/signed-voice/${TARGET}" | |
| mkdir -p "$root" | |
| tar -xzf "${RUNNER_TEMP}/signed-voice/signed-voice-${TARGET}.tar.gz" -C "$root" | |
| case "$TARGET" in | |
| aarch64-apple-darwin) arch=arm64 ;; | |
| x86_64-apple-darwin) arch=x86_64 ;; | |
| *) exit 1 ;; | |
| esac | |
| PYTHONPATH="${GITHUB_WORKSPACE}/third_party/voice" python3 - "$root/runtime" "$TARGET" <<'PY' | |
| from pathlib import Path | |
| import sys | |
| from package_runtime import runtime_files | |
| runtime_files(Path(sys.argv[1]).resolve(strict=True), sys.argv[2], public_release=True) | |
| PY | |
| while IFS= read -r -d '' binary; do | |
| lipo "$binary" -verify_arch "$arch" | |
| codesign --verify --strict --verbose=2 "$binary" | |
| done < <(find "$root/runtime" -name '*.dylib' -type f -print0) | |
| lipo "$root/codex-voice-host" -verify_arch "$arch" | |
| codesign --verify --strict --verbose=2 "$root/codex-voice-host" | |
| - name: Verify signed macOS binaries | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| for binary in ${{ matrix.binaries }}; do | |
| binary_path="target/${{ matrix.target }}/release/${binary}" | |
| chmod 0755 "$binary_path" | |
| codesign --verify --strict --verbose=2 "$binary_path" | |
| done | |
| for resource in rg zsh; do | |
| resource_path="signed-resources/${{ matrix.target }}/${resource}" | |
| chmod 0755 "$resource_path" | |
| codesign --verify --strict --verbose=2 "$resource_path" | |
| done | |
| - name: Stage macOS artifacts | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| dest="dist/${{ matrix.target }}" | |
| mkdir -p "$dest" | |
| for binary in ${{ matrix.binaries }}; do | |
| # Both variants package the host, but only the primary bundle publishes | |
| # standalone binary archives to avoid duplicate release asset names. | |
| if [[ "${{ matrix.bundle }}" == "app-server" && "$binary" == "codex-code-mode-host" ]]; then | |
| continue | |
| fi | |
| cp "target/${{ matrix.target }}/release/${binary}" "$dest/${binary}-${{ matrix.target }}" | |
| done | |
| - name: Build Codex package archive | |
| shell: bash | |
| env: | |
| TARGET: ${{ matrix.target }} | |
| BUNDLE: ${{ matrix.bundle }} | |
| run: | | |
| set -euo pipefail | |
| voice_args=() | |
| if [[ "$BUNDLE" == "primary" ]]; then | |
| voice_args+=(--voice-release-dir "${RUNNER_TEMP}/signed-voice/${TARGET}") | |
| voice_args+=(--release-version "${GITHUB_REF_NAME#rust-v}") | |
| fi | |
| bash "${GITHUB_WORKSPACE}/.github/scripts/build-codex-package-archive.sh" \ | |
| --target "$TARGET" \ | |
| --bundle "$BUNDLE" \ | |
| --entrypoint-dir "target/${TARGET}/release" \ | |
| --archive-dir "dist/${TARGET}" \ | |
| --rg-bin "signed-resources/${TARGET}/rg" \ | |
| --zsh-bin "signed-resources/${TARGET}/zsh" \ | |
| ${voice_args[@]+"${voice_args[@]}"} | |
| - name: Build unsigned macOS DMG | |
| if: ${{ matrix.build_dmg == 'true' }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| target="${{ matrix.target }}" | |
| release_dir="target/${target}/release" | |
| dmg_root="${RUNNER_TEMP}/codex-dmg-root-${target}" | |
| volname="Codex (${target})" | |
| dmg_path="${release_dir}/codex-${target}.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 | |
| if [[ "${{ matrix.bundle }}" == "primary" ]]; then | |
| # Keep the root-level invocation, but resolve it into the canonical | |
| # package layout so InstallContext finds its adjacent voice runtime. | |
| package="${RUNNER_TEMP}/codex-package-voice-${target}" | |
| ditto "$package" "$dmg_root" | |
| rm "${dmg_root}/codex" | |
| ln -s bin/codex "${dmg_root}/codex" | |
| fi | |
| 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 | |
| - name: Upload unsigned macOS DMG | |
| if: ${{ matrix.build_dmg == 'true' }} | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: ${{ matrix.artifact_name }}-unsigned-dmg | |
| path: codex-rs/target/${{ matrix.target }}/release/codex-${{ matrix.target }}.dmg | |
| if-no-files-found: error | |
| - 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 | |
| # The wheel keeps its macOS 10.9/11 tags. The native voice build | |
| # currently targets macOS 14, so keep it in the release archives and | |
| # DMG but leave this older-OS-compatible wheel unchanged. | |
| wheel_archives="${RUNNER_TEMP}/voice-free-wheel/${{ matrix.target }}" | |
| bash "${GITHUB_WORKSPACE}/.github/scripts/build-codex-package-archive.sh" \ | |
| --target "${{ matrix.target }}" \ | |
| --bundle primary \ | |
| --entrypoint-dir "target/${{ matrix.target }}/release" \ | |
| --archive-dir "$wheel_archives" \ | |
| --rg-bin "signed-resources/${{ matrix.target }}/rg" \ | |
| --zsh-bin "signed-resources/${{ matrix.target }}/zsh" | |
| wheel_archive="${wheel_archives}/codex-package-${{ matrix.target }}.tar.gz" | |
| python3 - "$wheel_archive" <<'PY' | |
| import sys | |
| import tarfile | |
| with tarfile.open(sys.argv[1]) as archive: | |
| assert not any("codex-resources/voice/" in item.name for item in archive) | |
| PY | |
| 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" \ | |
| "$wheel_archive" \ | |
| --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 | |
| - name: Upload packaged macOS artifacts | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: ${{ matrix.artifact_name }}-packaged | |
| path: codex-rs/dist/${{ matrix.target }}/* | |
| if-no-files-found: error | |
| sign-macos-dmg: | |
| needs: package-macos | |
| name: Sign macOS DMG - ${{ matrix.target }} | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 45 | |
| environment: | |
| name: codesigning | |
| deployment: false | |
| permissions: | |
| contents: read | |
| id-token: write | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - target: aarch64-apple-darwin | |
| artifact_name: aarch64-apple-darwin | |
| - target: x86_64-apple-darwin | |
| artifact_name: x86_64-apple-darwin | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Download unsigned macOS DMG | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: ${{ matrix.artifact_name }}-unsigned-dmg | |
| path: ${{ runner.temp }}/unsigned-dmg | |
| - name: Set up AKV PKCS11 macOS signing | |
| uses: ./.github/actions/setup-akv-pkcs11-codesigning | |
| with: | |
| rcodesign-blob-uri: ${{ secrets.AKV_CODESIGN_RCODESIGN_BLOB_URI }} | |
| rcodesign-sha256: ${{ secrets.AKV_CODESIGN_RCODESIGN_SHA256 }} | |
| akv-pkcs11-library-blob-uri: ${{ secrets.AKV_CODESIGN_PKCS11_LIBRARY_BLOB_URI }} | |
| akv-pkcs11-library-sha256: ${{ secrets.AKV_CODESIGN_PKCS11_LIBRARY_SHA256 }} | |
| azure-client-id: ${{ secrets.AKV_CODESIGN_AZURE_CLIENT_ID }} | |
| azure-tenant-id: ${{ secrets.AKV_CODESIGN_TENANT }} | |
| azure-subscription-id: ${{ secrets.AKV_CODESIGN_SUBSCRIPTION }} | |
| key-vault-name: ${{ secrets.AKV_CODESIGN_KEY_VAULT_NAME }} | |
| key-name: ${{ secrets.AKV_CODESIGN_KEY_NAME }} | |
| key-version: ${{ secrets.AKV_CODESIGN_KEY_VERSION || '' }} | |
| certificate-sha256: ${{ secrets.AKV_CODESIGN_CERTIFICATE_SHA256 || '' }} | |
| - name: Sign, notarize, and staple macOS DMG | |
| shell: bash | |
| env: | |
| TARGET: ${{ matrix.target }} | |
| APPLE_NOTARIZATION_AKV_KEY_NAME: ${{ secrets.AKV_NOTARIZATION_KEY_NAME }} | |
| APPLE_NOTARIZATION_AKV_KEY_VERSION: ${{ secrets.AKV_NOTARIZATION_KEY_VERSION }} | |
| run: | | |
| set -euo pipefail | |
| dmg_path="${RUNNER_TEMP}/unsigned-dmg/codex-${TARGET}.dmg" | |
| report_dir="${GITHUB_WORKSPACE}/macos-dmg-signing-verification/${TARGET}" | |
| if [[ ! -f "$dmg_path" ]]; then | |
| echo "Unsigned DMG $dmg_path not found" | |
| exit 1 | |
| fi | |
| .github/scripts/macos-signing/sign_macos_code.sh \ | |
| --target "$dmg_path" \ | |
| --identity unused \ | |
| --deep false \ | |
| --timestamp true | |
| mkdir -p "$report_dir" | |
| rcodesign print-signature-info "$dmg_path" \ | |
| >"${report_dir}/signature-info-before-notarization.yaml" | |
| .github/scripts/macos-signing/notarize_macos_dmg_with_akv.sh \ | |
| --dmg "$dmg_path" \ | |
| --report-dir "$report_dir" | |
| rcodesign print-signature-info "$dmg_path" \ | |
| >"${report_dir}/signature-info.yaml" | |
| - name: Upload signed macOS DMG | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: ${{ matrix.artifact_name }}-signed-dmg | |
| path: ${{ runner.temp }}/unsigned-dmg/codex-${{ matrix.target }}.dmg | |
| if-no-files-found: error | |
| - name: Upload DMG signing verification | |
| if: ${{ always() }} | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: ${{ matrix.artifact_name }}-dmg-signing-verification | |
| path: macos-dmg-signing-verification/${{ matrix.target }}/ | |
| if-no-files-found: warn | |
| finalize-macos: | |
| needs: | |
| - package-macos | |
| - sign-macos-dmg | |
| name: Verify macOS artifacts - ${{ 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-code-mode-host codex-responses-api-proxy" | |
| verify_dmg: "true" | |
| - target: aarch64-apple-darwin | |
| bundle: app-server | |
| artifact_name: aarch64-apple-darwin-app-server | |
| binaries: "codex-app-server codex-code-mode-host" | |
| verify_dmg: "false" | |
| - target: x86_64-apple-darwin | |
| bundle: primary | |
| artifact_name: x86_64-apple-darwin | |
| binaries: "codex codex-code-mode-host codex-responses-api-proxy" | |
| verify_dmg: "true" | |
| - target: x86_64-apple-darwin | |
| bundle: app-server | |
| artifact_name: x86_64-apple-darwin-app-server | |
| binaries: "codex-app-server codex-code-mode-host" | |
| verify_dmg: "false" | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Download packaged macOS artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: ${{ matrix.artifact_name }}-packaged | |
| path: codex-rs/dist/${{ matrix.target }} | |
| - name: Download signed macOS binaries | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: ${{ matrix.artifact_name }}-signed-binaries | |
| path: ${{ runner.temp }}/signed-binaries | |
| - name: Download signed macOS DMG | |
| if: ${{ matrix.verify_dmg == 'true' }} | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| name: ${{ matrix.artifact_name }}-signed-dmg | |
| path: ${{ runner.temp }}/signed-dmg | |
| - name: Verify signed macOS artifacts | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| target="${{ matrix.target }}" | |
| packaged_dir="dist/${target}" | |
| case "$target" in | |
| aarch64-apple-darwin) expected_arch="arm64" ;; | |
| x86_64-apple-darwin) expected_arch="x86_64" ;; | |
| *) | |
| echo "Unexpected macOS target: $target" | |
| exit 1 | |
| ;; | |
| esac | |
| verify_signed_binary() { | |
| local path="$1" | |
| local binary="$2" | |
| local actual_entitlements expected_entitlements normalized_actual normalized_expected | |
| chmod 0755 "$path" | |
| lipo "$path" -verify_arch "$expected_arch" | |
| codesign --verify --strict --verbose=2 "$path" | |
| expected_entitlements="${GITHUB_WORKSPACE}/.github/scripts/macos-signing/${binary}.entitlements.plist" | |
| if [[ ! -f "$expected_entitlements" ]]; then | |
| echo "Expected entitlements file $expected_entitlements not found" | |
| exit 1 | |
| fi | |
| actual_entitlements="$(mktemp)" | |
| normalized_actual="$(mktemp)" | |
| normalized_expected="$(mktemp)" | |
| codesign -d --entitlements :- "$path" >"$actual_entitlements" | |
| plutil -convert xml1 -o "$normalized_actual" "$actual_entitlements" | |
| plutil -convert xml1 -o "$normalized_expected" "$expected_entitlements" | |
| diff -u "$normalized_expected" "$normalized_actual" | |
| rm -f "$actual_entitlements" "$normalized_actual" "$normalized_expected" | |
| } | |
| for binary in ${{ matrix.binaries }}; do | |
| binary_path="${RUNNER_TEMP}/signed-binaries/${binary}" | |
| verify_signed_binary "$binary_path" "$binary" | |
| # The app-server package contains the host, but its standalone archives | |
| # are omitted above to avoid duplicate release asset names. | |
| if [[ "${{ matrix.bundle }}" == "app-server" && "$binary" == "codex-code-mode-host" ]]; then | |
| continue | |
| fi | |
| direct_archive_dir="${RUNNER_TEMP}/direct-archive-${binary}-${target}" | |
| rm -rf "$direct_archive_dir" | |
| mkdir -p "$direct_archive_dir" | |
| tar -xzf "${packaged_dir}/${binary}-${target}.tar.gz" -C "$direct_archive_dir" | |
| verify_signed_binary "${direct_archive_dir}/${binary}-${target}" "$binary" | |
| direct_zstd_path="${RUNNER_TEMP}/${binary}-${target}-from-zstd" | |
| zstd -d --stdout "${packaged_dir}/${binary}-${target}.zst" >"$direct_zstd_path" | |
| verify_signed_binary "$direct_zstd_path" "$binary" | |
| done | |
| case "${{ matrix.bundle }}" in | |
| primary) | |
| package_stem="codex-package" | |
| package_entrypoint="codex" | |
| ;; | |
| app-server) | |
| package_stem="codex-app-server-package" | |
| package_entrypoint="codex-app-server" | |
| ;; | |
| *) | |
| echo "Unexpected macOS bundle: ${{ matrix.bundle }}" | |
| exit 1 | |
| ;; | |
| esac | |
| package_dir="${RUNNER_TEMP}/${package_stem}-${target}" | |
| rm -rf "$package_dir" | |
| mkdir -p "$package_dir" | |
| tar -xzf "${packaged_dir}/${package_stem}-${target}.tar.gz" -C "$package_dir" | |
| verify_signed_binary "${package_dir}/bin/${package_entrypoint}" "$package_entrypoint" | |
| verify_signed_binary "${package_dir}/bin/codex-code-mode-host" "codex-code-mode-host" | |
| for resource in \ | |
| "${package_dir}/codex-path/rg" \ | |
| "${package_dir}/codex-resources/zsh/bin/zsh" | |
| do | |
| chmod 0755 "$resource" | |
| lipo "$resource" -verify_arch "$expected_arch" | |
| codesign --verify --strict --verbose=2 "$resource" | |
| entitlements="$(mktemp)" | |
| codesign -d --entitlements :- "$resource" >"$entitlements" | |
| if [[ -s "$entitlements" ]]; then | |
| echo "Bundled helper $resource must not have code-signing entitlements." >&2 | |
| plutil -p "$entitlements" >&2 | |
| exit 1 | |
| fi | |
| rm -f "$entitlements" | |
| done | |
| if [[ "${{ matrix.bundle }}" == "primary" ]]; then | |
| voice="${package_dir}/codex-resources/voice" | |
| [[ -f "${voice}/lib/libgstreamer-1.0.0.dylib" ]] | |
| export VOICE_BUILD_COMMIT="$(git rev-parse HEAD)" | |
| PYTHONPATH="${GITHUB_WORKSPACE}/third_party/voice" python3 - "$voice" "$target" "$package_dir" <<'PY' | |
| import json | |
| import sys | |
| from pathlib import Path | |
| from package_runtime import runtime_files | |
| from runtime import digest | |
| voice, target, package = map(Path, sys.argv[1:]) | |
| runtime_files(voice.resolve(strict=True), str(target), public_release=True) | |
| manifest = json.loads((voice / "manifest.json").read_text()) | |
| assert manifest["buildCommit"] == __import__("os").environ["VOICE_BUILD_COMMIT"] | |
| for relative, expected in manifest["sha256"].items(): | |
| assert digest(package / relative) == expected, relative | |
| PY | |
| helper="${voice}/bin/codex-voice-host" | |
| [[ "$("$helper" --build-commit)" == "$VOICE_BUILD_COMMIT" ]] | |
| while IFS= read -r -d '' library; do | |
| lipo "$library" -verify_arch "$expected_arch" | |
| codesign --verify --strict --verbose=2 "$library" | |
| done < <(find "$voice" -name '*.dylib' -type f -print0) | |
| verify_signed_binary "$helper" "codex-voice-host" | |
| fi | |
| if [[ "${{ matrix.verify_dmg }}" != "true" ]]; then | |
| exit 0 | |
| fi | |
| dmg_path="${RUNNER_TEMP}/signed-dmg/codex-${target}.dmg" | |
| mount_dir="${RUNNER_TEMP}/codex-dmg-mount-${target}" | |
| if [[ ! -f "$dmg_path" ]]; then | |
| echo "Signed DMG $dmg_path not found" | |
| exit 1 | |
| fi | |
| hdiutil verify "$dmg_path" | |
| codesign --verify --strict --verbose=2 "$dmg_path" | |
| xcrun stapler validate "$dmg_path" | |
| rm -rf "$mount_dir" | |
| mkdir -p "$mount_dir" | |
| hdiutil attach "$dmg_path" -nobrowse -readonly -mountpoint "$mount_dir" | |
| cleanup_mount() { | |
| hdiutil detach "$mount_dir" >/dev/null | |
| } | |
| trap cleanup_mount EXIT | |
| for binary in ${{ matrix.binaries }}; do | |
| verify_signed_binary "${mount_dir}/${binary}" "$binary" | |
| done | |
| if [[ "${{ matrix.bundle }}" == "primary" ]]; then | |
| [[ "$(readlink "${mount_dir}/codex")" == "bin/codex" ]] | |
| cmp "${mount_dir}/codex-package.json" "${package_dir}/codex-package.json" | |
| cmp "${mount_dir}/codex-resources/voice/manifest.json" \ | |
| "${package_dir}/codex-resources/voice/manifest.json" | |
| cmp "${mount_dir}/codex-resources/voice/bin/codex-voice-host" \ | |
| "${package_dir}/codex-resources/voice/bin/codex-voice-host" | |
| fi | |
| cleanup_mount | |
| trap - EXIT | |
| cp "$dmg_path" "dist/${target}/codex-${target}.dmg" | |
| - name: Upload verified macOS artifacts | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: ${{ matrix.artifact_name }} | |
| path: codex-rs/dist/${{ matrix.target }}/* | |
| if-no-files-found: error | |
| # Keep provisioned packages separate from the legacy CLI/npm/installer layout. | |
| # Desktop consumes the versioned codex-provisioned manifest from this release. | |
| provisioned-macos-candidate: | |
| needs: finalize-macos | |
| if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/rust-v') && vars.CODEX_PROVISIONED_MACOS_CANDIDATE == 'true' | |
| permissions: | |
| contents: read | |
| id-token: write | |
| uses: ./.github/workflows/rust-release-provisioned-macos.yml | |
| secrets: inherit | |
| build-windows: | |
| needs: tag-check | |
| uses: ./.github/workflows/rust-release-windows.yml | |
| 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 | |
| stage-npm-packages: | |
| name: stage npm packages | |
| needs: | |
| - build | |
| - finalize-macos | |
| - build-windows | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - name: Download target artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| path: dist | |
| pattern: "{aarch64,x86_64}-{apple-darwin{,-app-server},unknown-linux-musl{,-app-server},pc-windows-msvc}" | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@a8198c4bff370c8506180b035930dea56dbd5288 # v5 | |
| with: | |
| run_install: false | |
| - name: Setup Node.js for npm packaging | |
| uses: actions/setup-node@53b83947a5a98c8d113130e565377fae1a50d02f # v6.3.0 | |
| with: | |
| node-version: 22 | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Stage npm packages | |
| run: | | |
| release_version="${GITHUB_REF_NAME#rust-v}" | |
| ./scripts/stage_npm_packages.py \ | |
| --release-version "$release_version" \ | |
| --artifacts-dir "${GITHUB_WORKSPACE}/dist" \ | |
| --package codex \ | |
| --package codex-responses-api-proxy \ | |
| --package codex-sdk | |
| - name: Upload staged npm packages | |
| uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 | |
| with: | |
| name: npm-packages | |
| path: dist/npm/*.tgz | |
| if-no-files-found: error | |
| release: | |
| needs: | |
| - tag-check | |
| - build | |
| - finalize-macos | |
| - build-windows | |
| - argument-comment-lint-release-assets | |
| - stage-npm-packages | |
| - provisioned-macos-candidate | |
| if: >- | |
| ${{ | |
| always() && | |
| needs.tag-check.result == 'success' && | |
| needs.build.result == 'success' && | |
| needs.finalize-macos.result == 'success' && | |
| needs.build-windows.result == 'success' && | |
| needs.argument-comment-lint-release-assets.result == 'success' && | |
| needs.stage-npm-packages.result == 'success' && | |
| (needs.provisioned-macos-candidate.result == 'success' || | |
| (vars.CODEX_PROVISIONED_MACOS_CANDIDATE != 'true' && | |
| needs.provisioned-macos-candidate.result == 'skipped')) | |
| }} | |
| name: release | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| outputs: | |
| version: ${{ steps.release_name.outputs.name }} | |
| tag: ${{ github.ref_name }} | |
| make_latest: ${{ steps.release_name.outputs.make_latest }} | |
| prerelease: ${{ steps.release_name.outputs.prerelease }} | |
| 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@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - 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}" | |
| - name: Download release artifacts | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| path: dist | |
| pattern: "{{aarch64,x86_64}-{apple-darwin{,-app-server},unknown-linux-musl{,-app-server},pc-windows-msvc},*-symbols,argument-comment-lint-*,python-runtime-wheel-*,npm-packages}" | |
| - name: Download verified provisioned macOS packages | |
| if: vars.CODEX_PROVISIONED_MACOS_CANDIDATE == 'true' | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| with: | |
| path: dist | |
| pattern: "provisioned-macos-verified-candidate-*" | |
| - name: Remove per-architecture checksum filenames before release upload | |
| if: vars.CODEX_PROVISIONED_MACOS_CANDIDATE == 'true' | |
| run: rm dist/provisioned-macos-verified-candidate-*/SHA256SUMS | |
| - name: List | |
| run: ls -R dist/ | |
| - name: Add Codex package checksum manifest | |
| run: | | |
| set -euo pipefail | |
| manifest="dist/codex-package_SHA256SUMS" | |
| tmp_manifest="$(mktemp)" | |
| find dist -type f \ | |
| \( -name 'codex-package-*.tar.gz' -o -name 'codex-app-server-package-*.tar.gz' -o -name 'codex-provisioned-package-*.tar.gz' \) \ | |
| -print | | |
| sort | | |
| while IFS= read -r archive; do | |
| sha256sum "$archive" | | |
| awk -v name="$(basename "$archive")" '{ print $1 " " name }' | |
| done > "$tmp_manifest" | |
| if [[ ! -s "$tmp_manifest" ]]; then | |
| echo "No Codex package archives found for checksum manifest" | |
| exit 1 | |
| fi | |
| mv "$tmp_manifest" "$manifest" | |
| cat "$manifest" | |
| - 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 | |
| if [[ "${version}" == *-* ]]; then | |
| echo "make_latest=false" >> $GITHUB_OUTPUT | |
| echo "prerelease=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "make_latest=true" >> $GITHUB_OUTPUT | |
| echo "prerelease=false" >> $GITHUB_OUTPUT | |
| fi | |
| - 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]+(\.[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: 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@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: ${{ steps.release_name.outputs.make_latest }} | |
| # Mark as prerelease only when the version has a suffix after x.y.z | |
| # (e.g. -alpha, -beta). Otherwise publish a normal release. | |
| prerelease: ${{ steps.release_name.outputs.prerelease }} | |
| publish-r2-assets: | |
| name: publish-r2-assets | |
| needs: release | |
| if: ${{ !cancelled() && needs.release.result == 'success' }} | |
| permissions: | |
| contents: read | |
| uses: ./.github/workflows/r2-release.yml | |
| secrets: inherit | |
| with: | |
| tag: ${{ needs.release.outputs.tag }} | |
| make_latest: ${{ fromJSON(needs.release.outputs.make_latest) }} | |
| prerelease: ${{ fromJSON(needs.release.outputs.prerelease) }} | |
| stage: assets | |
| publish-r2: | |
| name: publish-r2 | |
| needs: [release, publish-dotslash, publish-r2-assets] | |
| if: >- | |
| ${{ | |
| !cancelled() && | |
| needs.release.result == 'success' && | |
| needs.publish-dotslash.result == 'success' && | |
| needs.publish-r2-assets.result == 'success' | |
| }} | |
| permissions: | |
| contents: read | |
| uses: ./.github/workflows/r2-release.yml | |
| secrets: inherit | |
| with: | |
| tag: ${{ needs.release.outputs.tag }} | |
| make_latest: ${{ fromJSON(needs.release.outputs.make_latest) }} | |
| prerelease: ${{ fromJSON(needs.release.outputs.prerelease) }} | |
| stage: finalize | |
| publish-dotslash: | |
| name: publish-dotslash | |
| needs: release | |
| if: ${{ !cancelled() && needs.release.result == 'success' }} | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| persist-credentials: false | |
| - uses: ./.github/actions/publish-dotslash | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| with: | |
| tag: ${{ github.ref_name }} | |
| configs: | | |
| .github/dotslash-config.json | |
| .github/dotslash-argument-comment-lint-config.json | |
| - name: Publish the provisioned macOS package manifest | |
| if: vars.CODEX_PROVISIONED_MACOS_CANDIDATE == 'true' | |
| uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2 | |
| env: | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| with: | |
| tag: ${{ github.ref_name }} | |
| config: .github/dotslash-provisioned-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, numbered alphas, and alpha hotfixes. | |
| 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 | |
| # npm returns HTTP 409 when concurrent publishes update the same | |
| # packument. Every platform tarball is a version of @openai/codex, | |
| # so publish all tarballs serially. | |
| tarballs=( | |
| "${platform_tarballs[@]}" | |
| "${other_tarballs[@]}" | |
| "${root_tarball}" | |
| ) | |
| # The SDK depends on this exact root package version. | |
| 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 | |
| deploy-dev-website: | |
| name: Trigger developers.openai.com deploy | |
| needs: release | |
| # Only trigger the deploy for a stable release. | |
| # The deploy updates developers.openai.com with the new config schema json file. | |
| if: >- | |
| ${{ | |
| !cancelled() && | |
| needs.release.result == 'success' && | |
| !contains(needs.release.outputs.version, '-') | |
| }} | |
| runs-on: ubuntu-latest | |
| continue-on-error: true | |
| permissions: {} | |
| environment: | |
| name: dev-website-vercel-deploy | |
| deployment: false | |
| steps: | |
| - name: Trigger developers.openai.com deploy | |
| 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 | |
| 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' && | |
| !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 | |
| environment: | |
| name: mainline-release-winget | |
| deployment: false | |
| 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.publish-r2.result == 'success' && | |
| ( | |
| needs.release.outputs.should_publish_npm != 'true' || | |
| needs.publish-npm.result == 'success' | |
| ) | |
| }} | |
| permissions: | |
| contents: write | |
| # R2 waits for the GitHub release and DotSlash release-asset uploads. | |
| needs: | |
| - release | |
| - publish-r2 | |
| - publish-npm | |
| 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 | |