Files
codex/.github/workflows/rust-release-provisioned-macos.yml
Eric Burke 45eec73b11 Add opt-in provisioned macOS CLI release candidates (#44307)
## What changed

- Add a tag-release workflow gated by `CODEX_PROVISIONED_MACOS_CANDIDATE` for Apple Silicon and Intel macOS candidate artifacts.
- Package the CLI in `CodexCLI.app` with an embedded provisioning profile and a relocatable `bin/codex` launcher. Validate independently supplied profile, certificate, and team expectations before signing.
- Sign and notarize candidates, then verify signatures, entitlements, architecture, stapling, and Gatekeeper acceptance on macOS. Retain verified artifacts after package smoke tests pass.
- Filter code-mode smoke-test requests to `/v1/responses` so analytics requests are excluded from response parsing.

## Testing

Add tests for profile validation, launcher relocation and symlink handling, standard and provisioned signing flows, and rejection of signing, notarization, identity, and entitlement failures. The candidate workflow runs package smoke tests, including sandboxed code mode.

GitOrigin-RevId: f214f6a23dd10df62cd72a2c63c20cb0864fbc9c
2026-09-09 21:28:49 +00:00

214 lines
10 KiB
YAML

# Optional side artifacts only; no release, npm, installer, or DMG publication.
# Required secrets in the codesigning environment:
# - CODEX_CLI_PROVISIONING_PROFILE_BASE64: profile approved for public distribution.
# - CODEX_CLI_PROVISIONING_PROFILE_SHA256: reviewed lowercase SHA-256 of that profile.
# - CODEX_CLI_PROVISIONING_TEAM_ID: expected ten-character Apple Team ID.
# - AKV_CODESIGN_CERTIFICATE_SHA256: required signing certificate pin.
# Set repository variable CODEX_PROVISIONED_MACOS_CANDIDATE to true to enable
# candidates on normal tag releases; unset it or set it to false to disable them.
name: Provisioned macOS CLI candidate
on:
workflow_call:
permissions:
contents: read
env:
CODEX_REPO_ROOT: ${{ github.workspace }}
jobs:
sign:
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/rust-v') && vars.CODEX_PROVISIONED_MACOS_CANDIDATE == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 45
environment:
name: codesigning
deployment: false
permissions:
contents: read
id-token: write
strategy:
fail-fast: false
matrix:
target: [aarch64-apple-darwin, x86_64-apple-darwin]
env:
TARGET: ${{ matrix.target }}
PROVISIONED_MACOS: "true"
PROFILE_SHA256: ${{ secrets.CODEX_CLI_PROVISIONING_PROFILE_SHA256 }}
CERTIFICATE_SHA256: ${{ secrets.AKV_CODESIGN_CERTIFICATE_SHA256 }}
TEAM_ID: ${{ secrets.CODEX_CLI_PROVISIONING_TEAM_ID }}
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: Validate the independently approved public-distribution profile
env:
PROFILE_BASE64: ${{ secrets.CODEX_CLI_PROVISIONING_PROFILE_BASE64 }}
run: |
python3 - <<'PY'
import base64
import os
from pathlib import Path
profile = Path(os.environ["RUNNER_TEMP"]) / "codex-cli.provisionprofile"
profile.write_bytes(base64.b64decode("".join(os.environ["PROFILE_BASE64"].split()), validate=True))
PY
python3 .github/scripts/macos-signing/provisioned_macos_cli_package.py validate-profile \
--profile "$RUNNER_TEMP/codex-cli.provisionprofile" --profile-sha256 "$PROFILE_SHA256" \
--certificate-sha256 "$CERTIFICATE_SHA256" --team-id "$TEAM_ID"
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ matrix.target }}
path: standard
- name: Extract the verified package from this tag-release run
run: |
python3 - <<'PY'
import json
import os
import tarfile
from pathlib import Path
with tarfile.open(f"standard/codex-package-{os.environ['TARGET']}.tar.gz") as archive:
archive.extractall("package", filter="data")
metadata = json.loads(Path("package/codex-package.json").read_text())
if metadata["target"] != os.environ["TARGET"]:
raise ValueError("Package target does not match this release job")
PY
- name: Set up the existing AKV signer
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 the provisioned bundle
env:
APPLE_NOTARIZATION_AKV_KEY_NAME: ${{ secrets.AKV_NOTARIZATION_KEY_NAME }}
APPLE_NOTARIZATION_AKV_KEY_VERSION: ${{ secrets.AKV_NOTARIZATION_KEY_VERSION }}
run: |
python3 .github/scripts/macos-signing/sign_macos_cli_package.py sign package \
--profile "$RUNNER_TEMP/codex-cli.provisionprofile" --profile-sha256 "$PROFILE_SHA256" \
--certificate-sha256 "$CERTIFICATE_SHA256" --team-id "$TEAM_ID"
mkdir signed
tar -czf "signed/codex-provisioned-package-$TARGET.tar.gz" -C package \
bin codex-resources codex-path codex-package.json CodexCLI.app
- uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: provisioned-macos-signed-candidate-${{ matrix.target }}
path: signed/*.tar.gz
if-no-files-found: error
- name: Retain signing reports
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: provisioned-macos-signing-reports-${{ matrix.target }}
path: signing-verification/
if-no-files-found: warn
verify:
needs: sign
runs-on: macos-15-xlarge
timeout-minutes: 30
environment:
name: codesigning
deployment: false
# Read independent profile/certificate expectations without a signing token.
permissions:
contents: read
id-token: none
strategy:
fail-fast: false
matrix:
target: [aarch64-apple-darwin, x86_64-apple-darwin]
env:
TARGET: ${{ matrix.target }}
PROVISIONED_MACOS: "true"
PROFILE_SHA256: ${{ secrets.CODEX_CLI_PROVISIONING_PROFILE_SHA256 }}
CERTIFICATE_SHA256: ${{ secrets.AKV_CODESIGN_CERTIFICATE_SHA256 }}
TEAM_ID: ${{ secrets.CODEX_CLI_PROVISIONING_TEAM_ID }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
- uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
with:
python-version: "3.12"
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: provisioned-macos-signed-candidate-${{ matrix.target }}
path: signed
- name: Extract candidate and load independent verification profile
env:
PROFILE_BASE64: ${{ secrets.CODEX_CLI_PROVISIONING_PROFILE_BASE64 }}
run: |
python3 - <<'PY'
import base64
import os
import tarfile
from pathlib import Path
profile = Path(os.environ["RUNNER_TEMP"]) / "codex-cli.provisionprofile"
profile.write_bytes(base64.b64decode("".join(os.environ["PROFILE_BASE64"].split()), validate=True))
with tarfile.open(f"signed/codex-provisioned-package-{os.environ['TARGET']}.tar.gz") as archive:
archive.extractall("package", filter="data")
PY
- name: Verify signatures, profile, architecture, stapling and Gatekeeper
run: |
profile_args=(--profile "$RUNNER_TEMP/codex-cli.provisionprofile"
--profile-sha256 "$PROFILE_SHA256" --certificate-sha256 "$CERTIFICATE_SHA256"
--team-id "$TEAM_ID")
python3 .github/scripts/macos-signing/sign_macos_cli_package.py verify package "${profile_args[@]}"
xcrun stapler staple package/CodexCLI.app
xcrun stapler validate package/CodexCLI.app
spctl --assess --type execute --verbose=4 package/CodexCLI.app
python3 .github/scripts/macos-signing/sign_macos_cli_package.py verify package "${profile_args[@]}"
mkdir verified
tar -czf "verified/codex-provisioned-package-$TARGET.tar.gz" -C package \
bin codex-resources codex-path codex-package.json CodexCLI.app
(cd verified && shasum -a 256 "codex-provisioned-package-$TARGET.tar.gz" > SHA256SUMS)
- uses: ./.github/actions/setup-ci
- uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
version: "0.11.3"
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ matrix.target }}-app-server
path: app-server
- uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: ${{ matrix.target }}-symbols
path: symbols
- name: Test the exact verified package, including sandboxed code mode
working-directory: scripts/codex_package/smoke_tests
env:
PYTHONPATH: ${{ github.workspace }}/sdk/python/src:${{ github.workspace }}/sdk/python/tests
run: |
# This repackaging does not change debug code. The normal release's
# primary symbols omit app-server symbols, so skip symbol-only tests.
uv run --frozen pytest -v --compression gzip --package-target "$TARGET" \
--cli-archive "$CODEX_REPO_ROOT/verified/codex-provisioned-package-$TARGET.tar.gz" \
--app-server-archive "$CODEX_REPO_ROOT/app-server/codex-app-server-package-$TARGET.tar.gz" \
--symbols-archive "$CODEX_REPO_ROOT/symbols/codex-symbols-$TARGET.tar.gz" \
test_codex_package.py -k 'not debug_symbols'
- name: Retain verified candidate only after package tests pass
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: provisioned-macos-verified-candidate-${{ matrix.target }}
path: verified/*
if-no-files-found: error
- name: Retain verification reports
if: always()
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
with:
name: provisioned-macos-verification-reports-${{ matrix.target }}
path: signing-verification/
if-no-files-found: warn