ci: sign macOS release artifacts with Azure Key Vault (#26252)

## Why

The public Codex release workflow needs to sign and notarize macOS
binaries and DMGs without placing the Developer ID private key in
GitHub. This moves the private-key operation behind the protected
`codesigning` environment and uses GitHub OIDC with Azure Key Vault
PKCS#11, while preserving the existing external `build_unsigned` /
`promote_signed` fallback.

## What changed

- Add a reusable AKV PKCS11 setup action that authenticates to Azure
with OIDC, downloads pinned signing tools, verifies their SHA-256
digests, and loads the public signing certificate from Key Vault.
- Replace the legacy macOS signing action with scripts that support
AKV-backed `rcodesign`, notarize signed binaries and DMGs, and staple
DMG notarization tickets.
- Restructure `rust-release.yml` so macOS builds produce unsigned
artifacts first, protected jobs perform signing and notarization, macOS
runners package and verify the results, and release publishing waits for
verified artifacts.
- Preserve the manual external-signing handoff flow and make manual-mode
conditions explicit.
- Move the Codex entitlements file alongside the signing scripts and
update CODEOWNERS for the new signing surfaces.

## Verification

- [Live protected signing workflow
run](https://github.com/openai/codex/actions/runs/26903610631) completed
successfully for both macOS architectures, including binary
signing/notarization, DMG signing/notarization, and final artifact
verification.
- Downloaded both signed DMGs and independently verified their checksums
and strict signatures.
- Confirmed `xcrun stapler validate` succeeds and Gatekeeper accepts
both DMGs as `Notarized Developer ID`.
- Mounted both DMGs and confirmed the contained `codex` and
`codex-responses-api-proxy` binaries have valid Developer ID signatures
for the expected architectures.

---------

Co-authored-by: shijie-openai <shijie.rao@openai.com>
This commit is contained in:
Eric Burke
2026-06-03 23:34:51 -04:00
committed by GitHub
Unverified
parent c143a86de8
commit ad2012d645
9 changed files with 1464 additions and 388 deletions
+610 -83
View File
@@ -5,6 +5,9 @@
# 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.
#
# To use external macOS signing, manually dispatch `release_mode=build_unsigned`,
# sign the unsigned macOS artifacts in a secure enclave, upload the signed handoff
# archive as a GitHub Release asset, then manually dispatch
@@ -113,18 +116,18 @@ jobs:
echo "::warning title=Deprecated sign_macos input ignored::Use release_mode=build_unsigned or release_mode=promote_signed instead."
fi
# 1. Must be a tag and match the regex
# All release modes must run from a tag.
[[ "${GITHUB_REF_TYPE}" == "tag" ]] \
|| { echo "❌ Not a tag push"; exit 1; }
|| { echo "❌ Not a tag ref"; exit 1; }
# Release tags must match the version in Cargo.toml.
[[ "${GITHUB_REF_NAME}" =~ ^rust-v[0-9]+\.[0-9]+\.[0-9]+(-(alpha|beta)(\.[0-9]+)?)?$ ]] \
|| { echo "❌ Tag '${GITHUB_REF_NAME}' doesn't match expected format"; exit 1; }
# 2. Extract versions
tag_ver="${GITHUB_REF_NAME#rust-v}"
cargo_ver="$(grep -m1 '^version' codex-rs/Cargo.toml \
| sed -E 's/version *= *"([^"]+)".*/\1/')"
# 3. Compare
[[ "${tag_ver}" == "${cargo_ver}" ]] \
|| { echo "❌ Tag ${tag_ver} ≠ Cargo.toml ${cargo_ver}"; exit 1; }
@@ -154,7 +157,6 @@ jobs:
# submodules through SecureTransport/libgit2, especially libwebrtc's
# libyuv submodule from chromium.googlesource.com.
CARGO_NET_GIT_FETCH_WITH_CLI: "true"
SIGN_MACOS: ${{ github.event_name != 'workflow_dispatch' }}
strategy:
fail-fast: false
@@ -333,7 +335,7 @@ jobs:
path: codex-rs/target/**/cargo-timings/cargo-timing.html
if-no-files-found: warn
- if: ${{ runner.os == 'macOS' && env.SIGN_MACOS != 'true' }}
- if: ${{ runner.os == 'macOS' }}
name: Stage unsigned macOS artifacts
shell: bash
run: |
@@ -358,7 +360,7 @@ jobs:
zstd -T0 -19 --rm "${unsigned_path}"
done
- if: ${{ runner.os == 'macOS' && env.SIGN_MACOS != 'true' }}
- if: ${{ runner.os == 'macOS' }}
name: Upload unsigned macOS artifacts
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
@@ -374,75 +376,8 @@ jobs:
artifacts-dir: ${{ github.workspace }}/codex-rs/target/${{ matrix.target }}/release
binaries: ${{ matrix.binaries }}
- if: ${{ runner.os == 'macOS' && env.SIGN_MACOS == 'true' }}
name: MacOS code signing (binaries)
uses: ./.github/actions/macos-code-sign
with:
target: ${{ matrix.target }}
binaries: ${{ matrix.binaries }}
sign-binaries: "true"
sign-dmg: "false"
apple-certificate: ${{ secrets.APPLE_CERTIFICATE_P12 }}
apple-certificate-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
apple-notarization-key-p8: ${{ secrets.APPLE_NOTARIZATION_KEY_P8 }}
apple-notarization-key-id: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
apple-notarization-issuer-id: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
- if: ${{ runner.os == 'macOS' && matrix.build_dmg == 'true' && env.SIGN_MACOS == 'true' }}
name: Build macOS dmg
shell: bash
run: |
set -euo pipefail
target="${{ matrix.target }}"
release_dir="target/${target}/release"
dmg_root="${RUNNER_TEMP}/codex-dmg-root"
volname="Codex (${target})"
dmg_path="${release_dir}/codex-${target}.dmg"
# The previous "MacOS code signing (binaries)" step signs + notarizes the
# built artifacts in `${release_dir}`. This step packages *those same*
# signed binaries into a dmg.
rm -rf "$dmg_root"
mkdir -p "$dmg_root"
for binary in ${{ matrix.binaries }}; do
binary_path="${release_dir}/${binary}"
if [[ ! -f "${binary_path}" ]]; then
echo "Binary ${binary_path} not found"
exit 1
fi
ditto "${binary_path}" "${dmg_root}/${binary}"
done
rm -f "$dmg_path"
hdiutil create \
-volname "$volname" \
-srcfolder "$dmg_root" \
-format UDZO \
-ov \
"$dmg_path"
if [[ ! -f "$dmg_path" ]]; then
echo "dmg $dmg_path not found after build"
exit 1
fi
- if: ${{ runner.os == 'macOS' && matrix.build_dmg == 'true' && env.SIGN_MACOS == 'true' }}
name: MacOS code signing (dmg)
uses: ./.github/actions/macos-code-sign
with:
target: ${{ matrix.target }}
sign-binaries: "false"
sign-dmg: "true"
apple-certificate: ${{ secrets.APPLE_CERTIFICATE_P12 }}
apple-certificate-password: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
apple-notarization-key-p8: ${{ secrets.APPLE_NOTARIZATION_KEY_P8 }}
apple-notarization-key-id: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
apple-notarization-issuer-id: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
- name: Stage artifacts
if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }}
if: ${{ runner.os != 'macOS' }}
shell: bash
run: |
dest="dist/${{ matrix.target }}"
@@ -472,7 +407,7 @@ jobs:
fi
- name: Build Codex package archive
if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }}
if: ${{ runner.os != 'macOS' }}
shell: bash
env:
TARGET: ${{ matrix.target }}
@@ -486,7 +421,7 @@ jobs:
--archive-dir "dist/${TARGET}"
- name: Build Python runtime wheel
if: ${{ matrix.bundle == 'primary' && (runner.os != 'macOS' || env.SIGN_MACOS == 'true') }}
if: ${{ matrix.bundle == 'primary' && runner.os != 'macOS' }}
shell: bash
run: |
set -euo pipefail
@@ -529,7 +464,7 @@ jobs:
"${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m build --wheel --outdir "$wheel_dir" "$stage_dir"
- name: Upload Python runtime wheel
if: ${{ matrix.bundle == 'primary' && (runner.os != 'macOS' || env.SIGN_MACOS == 'true') }}
if: ${{ matrix.bundle == 'primary' && runner.os != 'macOS' }}
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: python-runtime-wheel-${{ matrix.target }}
@@ -537,7 +472,7 @@ jobs:
if-no-files-found: error
- name: Compress artifacts
if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }}
if: ${{ runner.os != 'macOS' }}
shell: bash
run: |
# Path that contains the uncompressed binaries for the current
@@ -574,7 +509,7 @@ jobs:
done
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
if: ${{ runner.os != 'macOS' || env.SIGN_MACOS == 'true' }}
if: ${{ runner.os != 'macOS' }}
with:
name: ${{ matrix.artifact_name }}
# Upload the per-binary .zst files, .tar.gz equivalents, and any
@@ -582,6 +517,576 @@ jobs:
path: |
codex-rs/dist/${{ matrix.target }}/*
sign-macos-binaries:
if: ${{ github.event_name != 'workflow_dispatch' }}
needs: build
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-responses-api-proxy"
- target: aarch64-apple-darwin
bundle: app-server
artifact_name: aarch64-apple-darwin-app-server
binaries: "codex-app-server"
- target: x86_64-apple-darwin
bundle: primary
artifact_name: x86_64-apple-darwin
binaries: "codex codex-responses-api-proxy"
- target: x86_64-apple-darwin
bundle: app-server
artifact_name: x86_64-apple-darwin-app-server
binaries: "codex-app-server"
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: 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_KEY_P8: ${{ secrets.APPLE_NOTARIZATION_KEY_P8 }}
APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
run: |
set -euo pipefail
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"
.github/scripts/macos-signing/sign_macos_code.sh \
--target "$signed_path" \
--identity unused \
--deep false \
--identifier "$binary" \
--options runtime \
--timestamp true \
--entitlements .github/scripts/macos-signing/codex.entitlements.plist
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_rcodesign.sh \
--binary "$signed_path" \
--report-dir "${report_dir}/${binary}"
done
- 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 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:
if: ${{ github.event_name != 'workflow_dispatch' }}
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-responses-api-proxy"
build_dmg: "true"
- target: aarch64-apple-darwin
bundle: app-server
artifact_name: aarch64-apple-darwin-app-server
binaries: "codex-app-server"
build_dmg: "false"
- target: x86_64-apple-darwin
bundle: primary
artifact_name: x86_64-apple-darwin
binaries: "codex codex-responses-api-proxy"
build_dmg: "true"
- target: x86_64-apple-darwin
bundle: app-server
artifact_name: x86_64-apple-darwin-app-server
binaries: "codex-app-server"
build_dmg: "false"
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- name: Download signed macOS binaries
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ matrix.artifact_name }}-signed-binaries
path: codex-rs/target/${{ matrix.target }}/release
- 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
- 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
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: Stage macOS artifacts
shell: bash
run: |
set -euo pipefail
dest="dist/${{ matrix.target }}"
mkdir -p "$dest"
for binary in ${{ matrix.binaries }}; do
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
bash "${GITHUB_WORKSPACE}/.github/scripts/build-codex-package-archive.sh" \
--target "$TARGET" \
--bundle "$BUNDLE" \
--entrypoint-dir "target/${TARGET}/release" \
--archive-dir "dist/${TARGET}"
- name: Build Python runtime wheel
if: ${{ matrix.bundle == 'primary' }}
shell: bash
run: |
set -euo pipefail
case "${{ matrix.target }}" in
aarch64-apple-darwin)
platform_tag="macosx_11_0_arm64"
;;
x86_64-apple-darwin)
platform_tag="macosx_10_9_x86_64"
;;
*)
echo "No Python runtime wheel platform tag for ${{ matrix.target }}"
exit 1
;;
esac
python3 -m venv "${RUNNER_TEMP}/python-runtime-build-venv"
"${RUNNER_TEMP}/python-runtime-build-venv/bin/python" -m pip install build
stage_dir="${RUNNER_TEMP}/openai-codex-cli-bin-${{ matrix.target }}"
wheel_dir="${GITHUB_WORKSPACE}/python-runtime-dist/${{ matrix.target }}"
python3 \
"${GITHUB_WORKSPACE}/sdk/python/scripts/update_sdk_artifacts.py" \
stage-runtime \
"$stage_dir" \
"dist/${{ matrix.target }}/codex-package-${{ matrix.target }}.tar.gz" \
--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:
if: ${{ github.event_name != 'workflow_dispatch' }}
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_KEY_P8: ${{ secrets.APPLE_NOTARIZATION_KEY_P8 }}
APPLE_NOTARIZATION_KEY_ID: ${{ secrets.APPLE_NOTARIZATION_KEY_ID }}
APPLE_NOTARIZATION_ISSUER_ID: ${{ secrets.APPLE_NOTARIZATION_ISSUER_ID }}
run: |
set -euo pipefail
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_rcodesign.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:
if: ${{ github.event_name != 'workflow_dispatch' }}
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-responses-api-proxy"
verify_dmg: "true"
- target: aarch64-apple-darwin
bundle: app-server
artifact_name: aarch64-apple-darwin-app-server
binaries: "codex-app-server"
verify_dmg: "false"
- target: x86_64-apple-darwin
bundle: primary
artifact_name: x86_64-apple-darwin
binaries: "codex 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"
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}"
expected_entitlements="${GITHUB_WORKSPACE}/.github/scripts/macos-signing/codex.entitlements.plist"
verify_signed_binary() {
local path="$1"
local actual_entitlements normalized_actual normalized_expected
chmod 0755 "$path"
codesign --verify --strict --verbose=2 "$path"
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"
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}"
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"
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}"
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}"
done
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
stage-signed-macos:
if: ${{ github.event_name == 'workflow_dispatch' && inputs.release_mode == 'promote_signed' }}
needs: tag-check
@@ -822,7 +1327,7 @@ jobs:
codex-rs/dist/${{ matrix.target }}/*
build-windows:
if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }}
if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode == 'build_unsigned' }}
needs: tag-check
uses: ./.github/workflows/rust-release-windows.yml
with:
@@ -830,7 +1335,7 @@ jobs:
secrets: inherit
argument-comment-lint-release-assets:
if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }}
if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode == 'build_unsigned' }}
name: argument-comment-lint release assets
needs: tag-check
uses: ./.github/workflows/rust-release-argument-comment-lint.yml
@@ -838,7 +1343,7 @@ jobs:
publish: true
zsh-release-assets:
if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed' }}
if: ${{ github.event_name != 'workflow_dispatch' || inputs.release_mode == 'build_unsigned' }}
name: zsh release assets
needs: tag-check
uses: ./.github/workflows/rust-release-zsh.yml
@@ -847,6 +1352,7 @@ jobs:
needs:
- tag-check
- build
- finalize-macos
- stage-signed-macos
- build-windows
- argument-comment-lint-release-assets
@@ -861,6 +1367,7 @@ jobs:
inputs.release_mode == 'promote_signed' &&
needs.stage-signed-macos.result == 'success' &&
needs.build.result == 'skipped' &&
needs.finalize-macos.result == 'skipped' &&
needs.build-windows.result == 'skipped' &&
needs.argument-comment-lint-release-assets.result == 'skipped' &&
needs.zsh-release-assets.result == 'skipped'
@@ -868,6 +1375,17 @@ jobs:
(
(github.event_name != 'workflow_dispatch' || inputs.release_mode != 'promote_signed') &&
needs.build.result == 'success' &&
(
(
github.event_name == 'workflow_dispatch' &&
inputs.release_mode == 'build_unsigned' &&
needs.finalize-macos.result == 'skipped'
) ||
(
github.event_name != 'workflow_dispatch' &&
needs.finalize-macos.result == 'success'
)
) &&
needs.stage-signed-macos.result == 'skipped' &&
needs.build-windows.result == 'success' &&
needs.argument-comment-lint-release-assets.result == 'success' &&
@@ -1046,6 +1564,15 @@ jobs:
- name: Delete entries from dist/ that should not go in the release
run: |
rm -rf dist/windows-binaries*
rm -rf dist/*-apple-darwin*-signed-binaries
rm -rf dist/*-apple-darwin*-packaged
rm -rf dist/*-apple-darwin*-unsigned-dmg
rm -rf dist/*-apple-darwin*-signed-dmg
rm -rf dist/*-apple-darwin*-binary-signing-verification
rm -rf dist/*-apple-darwin*-dmg-signing-verification
if [[ "${SIGN_MACOS}" == "true" ]]; then
rm -rf dist/*-apple-darwin*-unsigned
fi
# cargo-timing.html appears under multiple target-specific directories.
# If included in files: dist/**, release upload races on duplicate
# asset names and can fail with 404s.