mirror of
https://github.com/openai/codex.git
synced 2026-09-10 20:26:47 +00:00
## What changed - Add a tag-release workflow gated by `CODEX_PROVISIONED_MACOS_CANDIDATE` for Apple Silicon and Intel macOS candidate artifacts. - Package the CLI in `CodexCLI.app` with an embedded provisioning profile and a relocatable `bin/codex` launcher. Validate independently supplied profile, certificate, and team expectations before signing. - Sign and notarize candidates, then verify signatures, entitlements, architecture, stapling, and Gatekeeper acceptance on macOS. Retain verified artifacts after package smoke tests pass. - Filter code-mode smoke-test requests to `/v1/responses` so analytics requests are excluded from response parsing. ## Testing Add tests for profile validation, launcher relocation and symlink handling, standard and provisioned signing flows, and rejection of signing, notarization, identity, and entitlement failures. The candidate workflow runs package smoke tests, including sandboxed code mode. GitOrigin-RevId: f214f6a23dd10df62cd72a2c63c20cb0864fbc9c
1934 lines
74 KiB
YAML
1934 lines
74 KiB
YAML
# Release workflow for codex-rs.
|
|
# To release, follow a workflow like:
|
|
# ```
|
|
# git tag -a rust-v0.1.0 -m "Release 0.1.0"
|
|
# git push origin rust-v0.1.0
|
|
# ```
|
|
#
|
|
# 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 }}
|
|
# 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:
|
|
# 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
|
|
|
|
- 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
|
|
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"
|
|
|
|
- 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
|
|
|
|
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"
|
|
"dist/${{ matrix.target }}/codex-package-${{ matrix.target }}.tar.gz"
|
|
--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-signed-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
|
|
|
|
# Separate opt-in candidates never enter the normal release download patterns.
|
|
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
|
|
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'
|
|
}}
|
|
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 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: Download supplemental release artifacts
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
path: dist
|
|
pattern: "{*-symbols,argument-comment-lint-*,python-runtime-wheel-*}"
|
|
|
|
- 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' \) \
|
|
-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: Download staged npm packages
|
|
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
|
|
with:
|
|
name: npm-packages
|
|
path: dist/npm
|
|
|
|
- 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
|
|
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]
|
|
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
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: write
|
|
|
|
steps:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- uses: facebook/dotslash-publish-release@9c9ec027515c34db9282a09a25a9cab5880b2c52 # v2
|
|
env:
|
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
with:
|
|
tag: ${{ github.ref_name }}
|
|
config: .github/dotslash-config.json
|
|
|
|
- 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
|
|
|
|
# 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
|