mirror of
https://github.com/openai/codex.git
synced 2026-10-19 16:17:49 +00:00
## What changed Pass `BUILDBUDDY_API_KEY` to Linux, macOS, and Windows voice release builds, and give the dedicated macOS and Windows voice jobs access to the `bazel` environment without creating deployments. Use the shared Bazel preparation action and BuildBuddy wrapper for Linux and macOS voice builds so they inherit the existing repository cache and remote cache configuration. GitOrigin-RevId: 52cdf05bc4567f3e8c460d2509e57bb30e667868
2196 lines
86 KiB
YAML
2196 lines
86 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 }}
|
|
|
|
# The launcher uses this per-tag group as the migration boundary. Keep shared
|
|
# publication jobs on the original queued group below when changing this.
|
|
concurrency:
|
|
group: ${{ github.workflow }}-${{ github.ref }}
|
|
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; }
|
|
|
|
tag_ver="${GITHUB_REF_NAME#rust-v}"
|
|
[[ "${GITHUB_REF_NAME}" == rust-v* ]] \
|
|
&& python3 .github/scripts/releases.py "${tag_ver}" \
|
|
|| { echo "❌ Tag '${GITHUB_REF_NAME}' doesn't match expected format"; exit 1; }
|
|
|
|
# Release tags must match the version in Cargo.toml.
|
|
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 }} - ${{ matrix.build-system }}
|
|
# Parallel Bazel artifacts are advisory while Cargo remains the release path.
|
|
continue-on-error: ${{ matrix.build-system == 'bazel' }}
|
|
runs-on: ${{ matrix.runner }}
|
|
environment:
|
|
name: bazel
|
|
deployment: false
|
|
# 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:
|
|
# Preserve Cargo names while teams opt into the additional Bazel artifacts.
|
|
ARTIFACT_SUFFIX: ${{ matrix.build-system == 'bazel' && '-bazel' || '' }}
|
|
# Archive packed macOS dSYMs before stripping the Cargo binaries.
|
|
CARGO_PROFILE_RELEASE_SPLIT_DEBUGINFO: ${{ contains(matrix.target, 'apple-darwin') && 'packed' || 'off' }}
|
|
# The git CLI reliably fetches nested native-library submodules on macOS.
|
|
CARGO_NET_GIT_FETCH_WITH_CLI: "true"
|
|
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
build-system: [cargo, bazel]
|
|
target:
|
|
- aarch64-apple-darwin
|
|
- x86_64-apple-darwin
|
|
- x86_64-unknown-linux-musl
|
|
- aarch64-unknown-linux-musl
|
|
bundle: [primary, app-server]
|
|
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
|
|
- name: Set up Rust toolchain
|
|
uses: dtolnay/rust-toolchain@e081816240890017053eacbb1bdf337761dc5582 # 1.95.0
|
|
with:
|
|
targets: ${{ matrix.build-system == 'cargo' && matrix.target || '' }}
|
|
|
|
- if: ${{ matrix.build-system == 'cargo' && contains(matrix.target, 'linux') }}
|
|
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.build-system == 'cargo' && contains(matrix.target, 'linux') }}
|
|
name: Install Zig
|
|
uses: mlugg/setup-zig@d1434d08867e3ee9daa34448df10607b98908d29 # v2.2.1
|
|
with:
|
|
version: 0.14.0
|
|
use-cache: false
|
|
|
|
- if: ${{ matrix.build-system == 'cargo' && contains(matrix.target, 'linux') }}
|
|
name: Install musl build tools
|
|
env:
|
|
TARGET: ${{ matrix.target }}
|
|
run: bash "${GITHUB_WORKSPACE}/.github/scripts/install-musl-build-tools.sh"
|
|
|
|
- if: ${{ matrix.build-system == 'cargo' && contains(matrix.target, 'linux') }}
|
|
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
|
|
if: ${{ matrix.build-system == 'cargo' }}
|
|
uses: ./.github/actions/setup-rusty-v8
|
|
with:
|
|
target: ${{ matrix.target }}
|
|
|
|
- if: ${{ matrix.build-system == 'cargo' && 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
|
|
if: ${{ matrix.build-system == 'cargo' }}
|
|
shell: bash
|
|
run: |
|
|
target="${{ matrix.target }}"
|
|
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: Prepare Bazel for release binaries or Linux voice
|
|
if: matrix.build-system == 'bazel' || (matrix.bundle == 'primary' && contains(matrix.target, 'linux'))
|
|
uses: ./.github/actions/prepare-bazel-ci
|
|
with:
|
|
target: ${{ matrix.target }}
|
|
cache-scope: bazel-release-${{ matrix.bundle }}
|
|
|
|
- name: Build Bazel release binaries and symbols
|
|
if: ${{ matrix.build-system == 'bazel' }}
|
|
uses: ./.github/actions/build-bazel-release
|
|
env:
|
|
BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
|
|
with:
|
|
target: ${{ matrix.target }}
|
|
bundle: ${{ matrix.bundle }}
|
|
release-dir: ${{ github.workspace }}/codex-rs/target/${{ matrix.target }}/release
|
|
|
|
- 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: Suffix Bazel artifact filenames
|
|
if: ${{ matrix.build-system == 'bazel' }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
for artifact in "symbols-dist/${{ matrix.artifact_name }}"/*; do
|
|
name="${artifact##*/}"
|
|
stem="${name%%.*}"
|
|
mv "$artifact" "${artifact%/*}/${stem}-bazel${name#"$stem"}"
|
|
done
|
|
|
|
- name: Upload symbols archive
|
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
|
with:
|
|
name: ${{ matrix.artifact_name }}-symbols${{ env.ARTIFACT_SUFFIX }}
|
|
path: codex-rs/symbols-dist/${{ matrix.artifact_name }}/*
|
|
if-no-files-found: error
|
|
|
|
- name: Build Linux voice runtime
|
|
if: ${{ matrix.bundle == 'primary' && contains(matrix.target, 'linux') }}
|
|
shell: bash
|
|
env:
|
|
APP_TARGET: ${{ matrix.target }}
|
|
BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
|
|
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
|
|
python3 .github/scripts/run_bazel_with_buildbuddy.py build --config=release \
|
|
//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${{ env.ARTIFACT_SUFFIX }}
|
|
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: 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}" \
|
|
"${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.build-system == 'cargo' && 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"
|
|
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.build-system == 'cargo' && 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
|
|
|
|
- name: Suffix Bazel artifact filenames
|
|
if: ${{ matrix.build-system == 'bazel' && runner.os != 'macOS' }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
for artifact in "dist/${{ matrix.target }}"/*; do
|
|
name="${artifact##*/}"
|
|
stem="${name%%.*}"
|
|
mv "$artifact" "${artifact%/*}/${stem}-bazel${name#"$stem"}"
|
|
done
|
|
|
|
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
|
if: ${{ runner.os != 'macOS' }}
|
|
with:
|
|
name: ${{ matrix.artifact_name }}${{ env.ARTIFACT_SUFFIX }}
|
|
# 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 }}
|
|
environment:
|
|
name: bazel
|
|
deployment: false
|
|
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"
|
|
- name: Prepare Bazel CI
|
|
uses: ./.github/actions/prepare-bazel-ci
|
|
with:
|
|
target: ${{ matrix.target }}
|
|
cache-scope: bazel-release-primary
|
|
- name: Build matching helper and native runtime
|
|
shell: bash
|
|
env:
|
|
TARGET: ${{ matrix.target }}
|
|
PREFIX: ${{ matrix.prefix }}
|
|
BUILDBUDDY_API_KEY: ${{ secrets.BUILDBUDDY_API_KEY }}
|
|
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)
|
|
python3 .github/scripts/run_bazel_with_buildbuddy.py build --config=release \
|
|
--macos_minimum_os=14.0 //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 }} - ${{ matrix.build-system }}
|
|
continue-on-error: ${{ matrix.build-system == 'bazel' }}
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 45
|
|
environment:
|
|
name: codesigning
|
|
deployment: false
|
|
permissions:
|
|
contents: read
|
|
id-token: write
|
|
|
|
env:
|
|
ARTIFACT_SUFFIX: ${{ matrix.build-system == 'bazel' && '-bazel' || '' }}
|
|
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
build-system: [cargo, bazel]
|
|
target:
|
|
- aarch64-apple-darwin
|
|
- x86_64-apple-darwin
|
|
bundle: [primary, app-server]
|
|
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${{ env.ARTIFACT_SUFFIX }}
|
|
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}"
|
|
mkdir -p "$signed_root"
|
|
|
|
PYTHONPATH="${GITHUB_WORKSPACE}/scripts" python3 - "$TARGET" "$signed_root" <<'PY'
|
|
import shutil
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
from codex_package.ripgrep import fetch_rg
|
|
from codex_package.targets import TARGET_SPECS
|
|
|
|
spec = TARGET_SPECS[sys.argv[1]]
|
|
signed_root = Path(sys.argv[2])
|
|
shutil.copy2(fetch_rg(spec), signed_root / "rg")
|
|
PY
|
|
|
|
for resource in rg; 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${{ env.ARTIFACT_SUFFIX }}
|
|
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${{ env.ARTIFACT_SUFFIX }}
|
|
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${{ env.ARTIFACT_SUFFIX }}
|
|
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${{ env.ARTIFACT_SUFFIX }}
|
|
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 }} - ${{ matrix.build-system }}
|
|
continue-on-error: ${{ matrix.build-system == 'bazel' }}
|
|
runs-on: macos-15-xlarge
|
|
timeout-minutes: 45
|
|
permissions:
|
|
contents: read
|
|
defaults:
|
|
run:
|
|
working-directory: codex-rs
|
|
|
|
env:
|
|
ARTIFACT_SUFFIX: ${{ matrix.build-system == 'bazel' && '-bazel' || '' }}
|
|
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
build-system: [cargo, bazel]
|
|
target:
|
|
- aarch64-apple-darwin
|
|
- x86_64-apple-darwin
|
|
bundle: [primary, app-server]
|
|
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${{ env.ARTIFACT_SUFFIX }}
|
|
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${{ env.ARTIFACT_SUFFIX }}
|
|
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${{ env.ARTIFACT_SUFFIX }}
|
|
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; 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" \
|
|
${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${{ env.ARTIFACT_SUFFIX }}
|
|
path: codex-rs/target/${{ matrix.target }}/release/codex-${{ matrix.target }}.dmg
|
|
if-no-files-found: error
|
|
|
|
- name: Build Python runtime wheel
|
|
if: ${{ matrix.build-system == 'cargo' && 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"
|
|
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.build-system == 'cargo' && 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${{ env.ARTIFACT_SUFFIX }}
|
|
path: codex-rs/dist/${{ matrix.target }}/*
|
|
if-no-files-found: error
|
|
|
|
sign-macos-dmg:
|
|
needs: package-macos
|
|
name: Sign macOS DMG - ${{ matrix.target }} - ${{ matrix.build-system }}
|
|
continue-on-error: ${{ matrix.build-system == 'bazel' }}
|
|
runs-on: ubuntu-latest
|
|
timeout-minutes: 45
|
|
environment:
|
|
name: codesigning
|
|
deployment: false
|
|
permissions:
|
|
contents: read
|
|
id-token: write
|
|
|
|
env:
|
|
ARTIFACT_SUFFIX: ${{ matrix.build-system == 'bazel' && '-bazel' || '' }}
|
|
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
build-system: [cargo, bazel]
|
|
target:
|
|
- aarch64-apple-darwin
|
|
- x86_64-apple-darwin
|
|
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${{ env.ARTIFACT_SUFFIX }}
|
|
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${{ env.ARTIFACT_SUFFIX }}
|
|
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${{ env.ARTIFACT_SUFFIX }}
|
|
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 }} - ${{ matrix.build-system }}
|
|
continue-on-error: ${{ matrix.build-system == 'bazel' }}
|
|
runs-on: macos-15-xlarge
|
|
timeout-minutes: 30
|
|
permissions:
|
|
contents: read
|
|
defaults:
|
|
run:
|
|
working-directory: codex-rs
|
|
|
|
env:
|
|
ARTIFACT_SUFFIX: ${{ matrix.build-system == 'bazel' && '-bazel' || '' }}
|
|
|
|
strategy:
|
|
fail-fast: false
|
|
matrix:
|
|
build-system: [cargo, bazel]
|
|
target:
|
|
- aarch64-apple-darwin
|
|
- x86_64-apple-darwin
|
|
bundle: [primary, app-server]
|
|
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${{ env.ARTIFACT_SUFFIX }}
|
|
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${{ env.ARTIFACT_SUFFIX }}
|
|
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${{ env.ARTIFACT_SUFFIX }}
|
|
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"
|
|
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: Suffix Bazel artifact filenames
|
|
if: ${{ matrix.build-system == 'bazel' }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
for artifact in "dist/${{ matrix.target }}"/*; do
|
|
name="${artifact##*/}"
|
|
stem="${name%%.*}"
|
|
mv "$artifact" "${artifact%/*}/${stem}-bazel${name#"$stem"}"
|
|
done
|
|
|
|
- name: Upload verified macOS artifacts
|
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
|
with:
|
|
name: ${{ matrix.artifact_name }}${{ env.ARTIFACT_SUFFIX }}
|
|
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:
|
|
# Each job checks its channel under this lock; out-of-order builds cannot
|
|
# roll back GitHub, npm, R2, or the canary branch.
|
|
concurrency: &release-publication
|
|
group: rust-release
|
|
queue: max
|
|
cancel-in-progress: false
|
|
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).
|
|
# Preserve the previous publisher's 124,999-character cap, reserving
|
|
# the last character for a trailing newline for GitHub's renderer.
|
|
git log -1 --format=%B "${commit}" |
|
|
python3 -c 'import sys; print(sys.stdin.read()[:124998])' > "${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}{,-bazel},*-symbols{,-bazel},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
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
run: |
|
|
set -euo pipefail
|
|
# 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
|
|
make_latest="$(gh api "repos/${GITHUB_REPOSITORY}/releases/latest" \
|
|
| python3 .github/scripts/releases.py "$version" --check-github latest)"
|
|
echo "make_latest=${make_latest}" >> $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
|
|
mv dist/x86_64-pc-windows-msvc/install.ps1 dist/install.ps1
|
|
|
|
- name: Create GitHub Release
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
RELEASE_NAME: ${{ steps.release_name.outputs.name }}
|
|
RELEASE_NOTES: ${{ steps.release_notes.outputs.path }}
|
|
MAKE_LATEST: ${{ steps.release_name.outputs.make_latest }}
|
|
PRERELEASE: ${{ steps.release_name.outputs.prerelease }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
# Look up this tag, including drafts, without listing release history.
|
|
# API errors must fail the step rather than look like an absent release.
|
|
# shellcheck disable=SC2016
|
|
exists="$(gh api graphql \
|
|
-f query='query($owner:String!,$repo:String!,$tag:String!){repository(owner:$owner,name:$repo){release(tagName:$tag){databaseId}}}' \
|
|
-f owner="$GITHUB_REPOSITORY_OWNER" \
|
|
-f repo="${GITHUB_REPOSITORY#*/}" \
|
|
-f tag="$GITHUB_REF_NAME" \
|
|
--jq '.data.repository.release != null')"
|
|
release_args=(
|
|
--repo "$GITHUB_REPOSITORY"
|
|
--title "$RELEASE_NAME"
|
|
--notes-file "$RELEASE_NOTES"
|
|
--prerelease="$PRERELEASE"
|
|
)
|
|
if [[ "$exists" == false ]]; then
|
|
# Prereleases must be created directly to keep prereleased events.
|
|
draft=true
|
|
if [[ "$PRERELEASE" == true ]]; then
|
|
draft=false
|
|
fi
|
|
gh release create "$GITHUB_REF_NAME" "${release_args[@]}" \
|
|
--draft="$draft" --verify-tag --latest=false
|
|
fi
|
|
|
|
mapfile -d '' assets < <(find dist -type f -print0 | sort -z)
|
|
gh release upload "$GITHUB_REF_NAME" "${assets[@]}" \
|
|
--repo "$GITHUB_REPOSITORY" --clobber
|
|
gh release edit "$GITHUB_REF_NAME" "${release_args[@]}" \
|
|
--draft=false --latest="$MAKE_LATEST"
|
|
|
|
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:
|
|
concurrency: *release-publication
|
|
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:
|
|
concurrency: *release-publication
|
|
# 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: Checkout repository
|
|
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
persist-credentials: false
|
|
|
|
- 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 }}
|
|
NPM_CONFIG_LOGLEVEL: verbose
|
|
NPM_CONFIG_LOGS_DIR: ${{ runner.temp }}/npm-publish-logs
|
|
NPM_CONFIG_LOGS_MAX: "100"
|
|
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
|
|
|
|
# The publication workflow holds the lock from this lookup through
|
|
# publish, including concurrent releases from other version lines.
|
|
tag="$(python3 .github/scripts/npm_alpha_tag.py "${tarball}" "${VERSION}" "${tag:-latest}")"
|
|
|
|
publish_cmd=(npm publish "${GITHUB_WORKSPACE}/${tarball}")
|
|
if [[ -n "${tag}" ]]; then
|
|
publish_cmd+=(--tag "${tag}")
|
|
fi
|
|
|
|
# A fresh npm process obtains new OIDC credentials on each attempt.
|
|
for attempt in 1 2 3; do
|
|
echo "+ ${publish_cmd[*]} (attempt ${attempt}/3)"
|
|
set +e
|
|
publish_output="$("${publish_cmd[@]}" 2>&1)"
|
|
publish_status=$?
|
|
set -e
|
|
|
|
echo "${publish_output}"
|
|
if [[ ${publish_status} -eq 0 ]]; then
|
|
break
|
|
fi
|
|
|
|
if grep -qiE "previously published|cannot publish over|version already exists" <<< "${publish_output}"; then
|
|
echo "Skipping already-published package version for ${filename}"
|
|
break
|
|
fi
|
|
|
|
if [[ ${attempt} -eq 3 ]]; then
|
|
exit "${publish_status}"
|
|
fi
|
|
delay=$((attempt * 10))
|
|
echo "Publishing ${filename} failed; retrying in ${delay} seconds."
|
|
sleep "${delay}"
|
|
done
|
|
done
|
|
|
|
- name: Retain npm debug logs
|
|
if: always()
|
|
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
|
with:
|
|
name: npm-publish-logs-${{ github.run_attempt }}
|
|
path: ${{ runner.temp }}/npm-publish-logs/
|
|
if-no-files-found: ignore
|
|
retention-days: 7
|
|
|
|
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:
|
|
concurrency: *release-publication
|
|
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, '-')
|
|
}}
|
|
# komac opens the winget-pkgs PR using the published Windows archives;
|
|
# it runs on Linux and does not execute the Windows binaries.
|
|
runs-on: ubuntu-latest
|
|
permissions:
|
|
contents: read
|
|
environment:
|
|
name: mainline-release-winget
|
|
deployment: false
|
|
|
|
steps:
|
|
# Adapted from the WinGet Releaser version previously used by this job:
|
|
# https://github.com/vedantmgoyal9/winget-releaser/blob/7bd472be23763def6e16bd06cc8b1cdfab0e2fd5/action.yml
|
|
# Keep dependencies explicit because its cargo-binstall@main reference
|
|
# violates the policy requiring full commit SHA action pins.
|
|
- name: Install cargo-binstall
|
|
uses: cargo-bins/cargo-binstall@b874e25ea559687bec77e281e9b271aa1367b624 # v1.23.0
|
|
with:
|
|
version: "1.23.0"
|
|
env:
|
|
GITHUB_TOKEN: ${{ github.token }}
|
|
|
|
- name: Install komac
|
|
env:
|
|
GITHUB_TOKEN: ${{ github.token }}
|
|
run: cargo-binstall komac --version 2.16.0 --no-confirm
|
|
|
|
- name: Publish to WinGet
|
|
shell: bash
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
GITHUB_TOKEN: ${{ secrets.WINGET_PUBLISH_PAT }}
|
|
RELEASE_VERSION: ${{ needs.release.outputs.version }}
|
|
RELEASE_TAG: ${{ needs.release.outputs.tag }}
|
|
KOMAC_FORK_OWNER: openai-oss-forks
|
|
KOMAC_CREATED_WITH: Codex release workflow
|
|
KOMAC_CREATED_WITH_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
|
|
run: |
|
|
set -euo pipefail
|
|
installer_urls="$(gh release view "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" \
|
|
--json assets \
|
|
--jq '.assets[] | select(.name | test("^codex-(x86_64|aarch64)-pc-windows-msvc\\.exe\\.zip$")) | .url')"
|
|
mapfile -t urls <<< "$installer_urls"
|
|
if [[ ${#urls[@]} -ne 2 ]]; then
|
|
echo "::error::Expected x86_64 and aarch64 Windows archives for $RELEASE_TAG"
|
|
exit 1
|
|
fi
|
|
komac sync-fork
|
|
komac update OpenAI.Codex --version "$RELEASE_VERSION" --submit --urls "${urls[@]}"
|
|
komac cleanup --only-merged
|
|
|
|
update-branch:
|
|
concurrency: *release-publication
|
|
name: Update latest-alpha-cli branch
|
|
if: >-
|
|
${{
|
|
!cancelled() &&
|
|
needs.release.result == 'success' &&
|
|
needs.release.outputs.npm_tag == 'alpha' &&
|
|
needs.publish-r2.result == 'success' &&
|
|
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:
|
|
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
|
|
with:
|
|
persist-credentials: false
|
|
- name: Determine whether to update the GitHub canary branch
|
|
id: github_canary
|
|
shell: bash
|
|
env:
|
|
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
RELEASE_VERSION: ${{ needs.release.outputs.version }}
|
|
PYTHONPATH: .github/scripts
|
|
run: |
|
|
set -euo pipefail
|
|
# Despite its name, raw+json returns raw TOML. The default response
|
|
# is JSON metadata with the file contents encoded as base64.
|
|
update="$(gh api "repos/$GITHUB_REPOSITORY/contents/codex-rs/Cargo.toml?ref=latest-alpha-cli" \
|
|
-H 'Accept: application/vnd.github.raw+json' \
|
|
| python3 .github/scripts/releases.py "$RELEASE_VERSION" --check-github canary)"
|
|
echo "update=$update" >> "$GITHUB_OUTPUT"
|
|
- name: Update latest-alpha-cli branch
|
|
if: success() && steps.github_canary.outputs.update == 'true'
|
|
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
|