From ad2012d645b7146d31bb03f98e2bd9371635d11a Mon Sep 17 00:00:00 2001 From: Eric Burke <163169620+eburke-openai@users.noreply.github.com> Date: Wed, 3 Jun 2026 23:34:51 -0400 Subject: [PATCH] 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 --- .github/CODEOWNERS | 5 + .github/actions/macos-code-sign/action.yml | 259 ------- .../actions/macos-code-sign/notary_helpers.sh | 46 -- .../setup-akv-pkcs11-codesigning/action.yaml | 349 +++++++++ .../macos-signing}/codex.entitlements.plist | 0 .../notarize_macos_binary_with_rcodesign.sh | 131 ++++ .../notarize_macos_dmg_with_rcodesign.sh | 124 ++++ .../scripts/macos-signing/sign_macos_code.sh | 245 +++++++ .github/workflows/rust-release.yml | 693 +++++++++++++++--- 9 files changed, 1464 insertions(+), 388 deletions(-) delete mode 100644 .github/actions/macos-code-sign/action.yml delete mode 100644 .github/actions/macos-code-sign/notary_helpers.sh create mode 100644 .github/actions/setup-akv-pkcs11-codesigning/action.yaml rename .github/{actions/macos-code-sign => scripts/macos-signing}/codex.entitlements.plist (100%) create mode 100755 .github/scripts/macos-signing/notarize_macos_binary_with_rcodesign.sh create mode 100755 .github/scripts/macos-signing/notarize_macos_dmg_with_rcodesign.sh create mode 100755 .github/scripts/macos-signing/sign_macos_code.sh diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index 668897c48..d29c06e6f 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -3,5 +3,10 @@ /codex-rs/ext/extension-api/ @openai/codex-core-agent-team /codex-rs/prompts/ @openai/codex-core-agent-team +# Keep macOS AKV signing changes reviewed by Codex maintainers. +/.github/actions/setup-akv-pkcs11-codesigning/ @openai/codex-core-agent-team +/.github/scripts/macos-signing/ @openai/codex-core-agent-team +/.github/workflows/rust-release.yml @openai/codex-core-agent-team + # Keep ownership changes reviewed by the same team. /.github/CODEOWNERS @openai/codex-core-agent-team diff --git a/.github/actions/macos-code-sign/action.yml b/.github/actions/macos-code-sign/action.yml deleted file mode 100644 index 0e19fa11d..000000000 --- a/.github/actions/macos-code-sign/action.yml +++ /dev/null @@ -1,259 +0,0 @@ -name: macos-code-sign -description: Configure, sign, notarize, and clean up macOS code signing artifacts. -inputs: - target: - description: Rust compilation target triple (e.g. aarch64-apple-darwin). - required: true - binaries: - description: Space-delimited binary basenames to sign and notarize. - default: "codex codex-responses-api-proxy" - sign-binaries: - description: Whether to sign and notarize the macOS binaries. - required: false - default: "true" - sign-dmg: - description: Whether to sign and notarize the macOS dmg. - required: false - default: "true" - apple-certificate: - description: Base64-encoded Apple signing certificate (P12). - required: true - apple-certificate-password: - description: Password for the signing certificate. - required: true - apple-notarization-key-p8: - description: Base64-encoded Apple notarization key (P8). - required: true - apple-notarization-key-id: - description: Apple notarization key ID. - required: true - apple-notarization-issuer-id: - description: Apple notarization issuer ID. - required: true -runs: - using: composite - steps: - - name: Configure Apple code signing - shell: bash - env: - KEYCHAIN_PASSWORD: actions - APPLE_CERTIFICATE: ${{ inputs.apple-certificate }} - APPLE_CERTIFICATE_PASSWORD: ${{ inputs.apple-certificate-password }} - run: | - set -euo pipefail - - if [[ -z "${APPLE_CERTIFICATE:-}" ]]; then - echo "APPLE_CERTIFICATE is required for macOS signing" - exit 1 - fi - - if [[ -z "${APPLE_CERTIFICATE_PASSWORD:-}" ]]; then - echo "APPLE_CERTIFICATE_PASSWORD is required for macOS signing" - exit 1 - fi - - cert_path="${RUNNER_TEMP}/apple_signing_certificate.p12" - echo "$APPLE_CERTIFICATE" | base64 -d > "$cert_path" - - keychain_path="${RUNNER_TEMP}/codex-signing.keychain-db" - security create-keychain -p "$KEYCHAIN_PASSWORD" "$keychain_path" - security set-keychain-settings -lut 21600 "$keychain_path" - security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$keychain_path" - - keychain_args=() - cleanup_keychain() { - if ((${#keychain_args[@]} > 0)); then - security list-keychains -s "${keychain_args[@]}" || true - security default-keychain -s "${keychain_args[0]}" || true - else - security list-keychains -s || true - fi - if [[ -f "$keychain_path" ]]; then - security delete-keychain "$keychain_path" || true - fi - } - - while IFS= read -r keychain; do - [[ -n "$keychain" ]] && keychain_args+=("$keychain") - done < <(security list-keychains | sed 's/^[[:space:]]*//;s/[[:space:]]*$//;s/"//g') - - if ((${#keychain_args[@]} > 0)); then - security list-keychains -s "$keychain_path" "${keychain_args[@]}" - else - security list-keychains -s "$keychain_path" - fi - - security default-keychain -s "$keychain_path" - security import "$cert_path" -k "$keychain_path" -P "$APPLE_CERTIFICATE_PASSWORD" -T /usr/bin/codesign -T /usr/bin/security - security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PASSWORD" "$keychain_path" > /dev/null - - codesign_hashes=() - while IFS= read -r hash; do - [[ -n "$hash" ]] && codesign_hashes+=("$hash") - done < <(security find-identity -v -p codesigning "$keychain_path" \ - | sed -n 's/.*\([0-9A-F]\{40\}\).*/\1/p' \ - | sort -u) - - if ((${#codesign_hashes[@]} == 0)); then - echo "No signing identities found in $keychain_path" - cleanup_keychain - rm -f "$cert_path" - exit 1 - fi - - if ((${#codesign_hashes[@]} > 1)); then - echo "Multiple signing identities found in $keychain_path:" - printf ' %s\n' "${codesign_hashes[@]}" - cleanup_keychain - rm -f "$cert_path" - exit 1 - fi - - APPLE_CODESIGN_IDENTITY="${codesign_hashes[0]}" - - rm -f "$cert_path" - - echo "APPLE_CODESIGN_IDENTITY=$APPLE_CODESIGN_IDENTITY" >> "$GITHUB_ENV" - echo "APPLE_CODESIGN_KEYCHAIN=$keychain_path" >> "$GITHUB_ENV" - echo "::add-mask::$APPLE_CODESIGN_IDENTITY" - - - name: Sign macOS binaries - if: ${{ inputs.sign-binaries == 'true' }} - shell: bash - env: - TARGET: ${{ inputs.target }} - BINARIES: ${{ inputs.binaries }} - run: | - set -euo pipefail - - if [[ -z "${APPLE_CODESIGN_IDENTITY:-}" ]]; then - echo "APPLE_CODESIGN_IDENTITY is required for macOS signing" - exit 1 - fi - - keychain_args=() - if [[ -n "${APPLE_CODESIGN_KEYCHAIN:-}" && -f "${APPLE_CODESIGN_KEYCHAIN}" ]]; then - keychain_args+=(--keychain "${APPLE_CODESIGN_KEYCHAIN}") - fi - - entitlements_path="$GITHUB_ACTION_PATH/codex.entitlements.plist" - - for binary in ${BINARIES}; do - path="codex-rs/target/${TARGET}/release/${binary}" - codesign --force --options runtime --timestamp --entitlements "$entitlements_path" --sign "$APPLE_CODESIGN_IDENTITY" "${keychain_args[@]}" "$path" - done - - - name: Notarize macOS binaries - if: ${{ inputs.sign-binaries == 'true' }} - shell: bash - env: - TARGET: ${{ inputs.target }} - BINARIES: ${{ inputs.binaries }} - APPLE_NOTARIZATION_KEY_P8: ${{ inputs.apple-notarization-key-p8 }} - APPLE_NOTARIZATION_KEY_ID: ${{ inputs.apple-notarization-key-id }} - APPLE_NOTARIZATION_ISSUER_ID: ${{ inputs.apple-notarization-issuer-id }} - run: | - set -euo pipefail - - for var in APPLE_NOTARIZATION_KEY_P8 APPLE_NOTARIZATION_KEY_ID APPLE_NOTARIZATION_ISSUER_ID; do - if [[ -z "${!var:-}" ]]; then - echo "$var is required for notarization" - exit 1 - fi - done - - notary_key_path="${RUNNER_TEMP}/notarytool.key.p8" - echo "$APPLE_NOTARIZATION_KEY_P8" | base64 -d > "$notary_key_path" - cleanup_notary() { - rm -f "$notary_key_path" - } - trap cleanup_notary EXIT - - source "$GITHUB_ACTION_PATH/notary_helpers.sh" - - notarize_binary() { - local binary="$1" - local source_path="codex-rs/target/${TARGET}/release/${binary}" - local archive_path="${RUNNER_TEMP}/${binary}.zip" - - if [[ ! -f "$source_path" ]]; then - echo "Binary $source_path not found" - exit 1 - fi - - rm -f "$archive_path" - ditto -c -k --keepParent "$source_path" "$archive_path" - - notarize_submission "$binary" "$archive_path" "$notary_key_path" - } - - for binary in ${BINARIES}; do - notarize_binary "${binary}" - done - - - name: Sign and notarize macOS dmg - if: ${{ inputs.sign-dmg == 'true' }} - shell: bash - env: - TARGET: ${{ inputs.target }} - APPLE_NOTARIZATION_KEY_P8: ${{ inputs.apple-notarization-key-p8 }} - APPLE_NOTARIZATION_KEY_ID: ${{ inputs.apple-notarization-key-id }} - APPLE_NOTARIZATION_ISSUER_ID: ${{ inputs.apple-notarization-issuer-id }} - run: | - set -euo pipefail - - for var in APPLE_CODESIGN_IDENTITY APPLE_NOTARIZATION_KEY_P8 APPLE_NOTARIZATION_KEY_ID APPLE_NOTARIZATION_ISSUER_ID; do - if [[ -z "${!var:-}" ]]; then - echo "$var is required" - exit 1 - fi - done - - notary_key_path="${RUNNER_TEMP}/notarytool.key.p8" - echo "$APPLE_NOTARIZATION_KEY_P8" | base64 -d > "$notary_key_path" - cleanup_notary() { - rm -f "$notary_key_path" - } - trap cleanup_notary EXIT - - source "$GITHUB_ACTION_PATH/notary_helpers.sh" - - dmg_name="codex-${TARGET}.dmg" - dmg_path="codex-rs/target/${TARGET}/release/${dmg_name}" - - if [[ ! -f "$dmg_path" ]]; then - echo "dmg $dmg_path not found" - exit 1 - fi - - keychain_args=() - if [[ -n "${APPLE_CODESIGN_KEYCHAIN:-}" && -f "${APPLE_CODESIGN_KEYCHAIN}" ]]; then - keychain_args+=(--keychain "${APPLE_CODESIGN_KEYCHAIN}") - fi - - codesign --force --timestamp --sign "$APPLE_CODESIGN_IDENTITY" "${keychain_args[@]}" "$dmg_path" - notarize_submission "$dmg_name" "$dmg_path" "$notary_key_path" - xcrun stapler staple "$dmg_path" - - - name: Remove signing keychain - if: ${{ always() }} - shell: bash - env: - APPLE_CODESIGN_KEYCHAIN: ${{ env.APPLE_CODESIGN_KEYCHAIN }} - run: | - set -euo pipefail - if [[ -n "${APPLE_CODESIGN_KEYCHAIN:-}" ]]; then - keychain_args=() - while IFS= read -r keychain; do - [[ "$keychain" == "$APPLE_CODESIGN_KEYCHAIN" ]] && continue - [[ -n "$keychain" ]] && keychain_args+=("$keychain") - done < <(security list-keychains | sed 's/^[[:space:]]*//;s/[[:space:]]*$//;s/"//g') - if ((${#keychain_args[@]} > 0)); then - security list-keychains -s "${keychain_args[@]}" - security default-keychain -s "${keychain_args[0]}" - fi - - if [[ -f "$APPLE_CODESIGN_KEYCHAIN" ]]; then - security delete-keychain "$APPLE_CODESIGN_KEYCHAIN" - fi - fi diff --git a/.github/actions/macos-code-sign/notary_helpers.sh b/.github/actions/macos-code-sign/notary_helpers.sh deleted file mode 100644 index ad9757fe3..000000000 --- a/.github/actions/macos-code-sign/notary_helpers.sh +++ /dev/null @@ -1,46 +0,0 @@ -#!/usr/bin/env bash - -notarize_submission() { - local label="$1" - local path="$2" - local notary_key_path="$3" - - if [[ -z "${APPLE_NOTARIZATION_KEY_ID:-}" || -z "${APPLE_NOTARIZATION_ISSUER_ID:-}" ]]; then - echo "APPLE_NOTARIZATION_KEY_ID and APPLE_NOTARIZATION_ISSUER_ID are required for notarization" - exit 1 - fi - - if [[ -z "$notary_key_path" || ! -f "$notary_key_path" ]]; then - echo "Notary key file $notary_key_path not found" - exit 1 - fi - - if [[ ! -f "$path" ]]; then - echo "Notarization payload $path not found" - exit 1 - fi - - local submission_json - submission_json=$(xcrun notarytool submit "$path" \ - --key "$notary_key_path" \ - --key-id "$APPLE_NOTARIZATION_KEY_ID" \ - --issuer "$APPLE_NOTARIZATION_ISSUER_ID" \ - --output-format json \ - --wait) - - local status submission_id - status=$(printf '%s\n' "$submission_json" | jq -r '.status // "Unknown"') - submission_id=$(printf '%s\n' "$submission_json" | jq -r '.id // ""') - - if [[ -z "$submission_id" ]]; then - echo "Failed to retrieve submission ID for $label" - exit 1 - fi - - echo "::notice title=Notarization::$label submission ${submission_id} completed with status ${status}" - - if [[ "$status" != "Accepted" ]]; then - echo "Notarization failed for ${label} (submission ${submission_id}, status ${status})" - exit 1 - fi -} diff --git a/.github/actions/setup-akv-pkcs11-codesigning/action.yaml b/.github/actions/setup-akv-pkcs11-codesigning/action.yaml new file mode 100644 index 000000000..4d79cab42 --- /dev/null +++ b/.github/actions/setup-akv-pkcs11-codesigning/action.yaml @@ -0,0 +1,349 @@ +name: Set up AKV PKCS11 code signing +description: Download prebuilt rcodesign and Azure Key Vault PKCS11 provider artifacts, then export macOS signing environment. + +inputs: + setup-mode: + description: signing configures Azure and exports signing env vars; tools-only only downloads signing tools. + required: false + default: signing + rcodesign-blob-uri: + description: Azure Blob URI for the prebuilt Linux/amd64 rcodesign binary. + required: true + rcodesign-sha256: + description: Expected SHA-256 digest for the prebuilt rcodesign binary. + required: true + akv-pkcs11-library-blob-uri: + description: Azure Blob URI for the prebuilt Linux/amd64 AKV PKCS11 provider library. + required: true + akv-pkcs11-library-sha256: + description: Expected SHA-256 digest for the prebuilt AKV PKCS11 provider library. + required: true + azure-client-id: + description: GitHub OIDC client ID for the Azure signer application. + required: true + azure-tenant-id: + description: Azure tenant ID for the signer application. + required: true + azure-subscription-id: + description: Azure subscription ID that owns the signing vault. + required: true + key-vault-name: + description: Azure Key Vault name containing the certificate-backed signing key. + required: true + key-name: + description: Key Vault certificate/key name used as the PKCS11 key label. + required: true + key-version: + description: Optional Key Vault key version to pin while signing. + required: false + default: "" + certificate-sha256: + description: Optional expected SHA-256 fingerprint for the downloaded public certificate. + required: false + default: "" + +outputs: + pkcs11-library: + description: Path to the downloaded AKV PKCS11 provider library. + value: ${{ steps.paths.outputs.pkcs11_library }} + signing-certificate-pem: + description: Path to the downloaded public signing certificate. + value: ${{ steps.paths.outputs.signing_certificate_pem }} + rcodesign: + description: Path to the downloaded rcodesign binary. + value: ${{ steps.paths.outputs.rcodesign }} + +runs: + using: composite + steps: + - name: Validate pinned signing artifacts + shell: bash + env: + SETUP_MODE: ${{ inputs.setup-mode }} + RCODESIGN_BLOB_URI: ${{ inputs.rcodesign-blob-uri }} + RCODESIGN_SHA256: ${{ inputs.rcodesign-sha256 }} + AKV_PKCS11_LIBRARY_BLOB_URI: ${{ inputs.akv-pkcs11-library-blob-uri }} + AKV_PKCS11_LIBRARY_SHA256: ${{ inputs.akv-pkcs11-library-sha256 }} + KEY_VAULT_NAME: ${{ inputs.key-vault-name }} + KEY_NAME: ${{ inputs.key-name }} + run: | + set -euo pipefail + + case "$SETUP_MODE" in + signing|tools-only) + ;; + *) + echo "setup-mode must be 'signing' or 'tools-only', got '$SETUP_MODE'." >&2 + exit 1 + ;; + esac + + for variable_name in RCODESIGN_SHA256 AKV_PKCS11_LIBRARY_SHA256; do + value="${!variable_name}" + if [[ ! "$value" =~ ^[0-9a-f]{64}$ ]]; then + echo "$variable_name must be a lowercase SHA-256 digest." >&2 + exit 1 + fi + done + + for variable_name in RCODESIGN_BLOB_URI AKV_PKCS11_LIBRARY_BLOB_URI; do + value="${!variable_name}" + if [[ ! "$value" =~ ^az://[^/]+/[^/]+/.+ ]]; then + echo "$variable_name must use az:////." >&2 + exit 1 + fi + done + + if [[ "$SETUP_MODE" == "signing" ]]; then + for variable_name in \ + KEY_VAULT_NAME \ + KEY_NAME; do + if [[ -z "${!variable_name}" ]]; then + echo "$variable_name is required for AKV PKCS11 signing." >&2 + exit 1 + fi + done + fi + + - name: Resolve signing tool paths + id: paths + shell: bash + run: | + set -euo pipefail + + if [[ "${RUNNER_OS}" != "Linux" ]]; then + echo "Prebuilt AKV PKCS11 signing tools are only vendored for Linux runners, got ${RUNNER_OS}." >&2 + exit 1 + fi + + if [[ "${RUNNER_ARCH}" != "X64" && "${RUNNER_ARCH}" != "AMD64" ]]; then + echo "Prebuilt AKV PKCS11 signing tools are only vendored for amd64 runners, got ${RUNNER_ARCH}." >&2 + exit 1 + fi + + provider_root="${RUNNER_TEMP}/akv-pkcs11-provider" + rcodesign_root="${RUNNER_TEMP}/rcodesign-root" + signing_certificate_pem="${RUNNER_TEMP}/akv-signing-cert.pem" + library_name="libakv_pkcs_11.so" + + mkdir -p "$provider_root" "$rcodesign_root/bin" + + { + echo "pkcs11_library=$provider_root/$library_name" + echo "pkcs11_manifest=$provider_root/akv-pkcs11-provider.manifest" + echo "rcodesign_root=$rcodesign_root" + echo "rcodesign=$rcodesign_root/bin/rcodesign" + echo "signing_certificate_pem=$signing_certificate_pem" + } >> "$GITHUB_OUTPUT" + + - name: Validate Azure credentials for private signing artifacts + shell: bash + env: + AZURE_CLIENT_ID: ${{ inputs.azure-client-id }} + AZURE_TENANT_ID: ${{ inputs.azure-tenant-id }} + AZURE_SUBSCRIPTION_ID: ${{ inputs.azure-subscription-id }} + run: | + set -euo pipefail + + for variable_name in AZURE_CLIENT_ID AZURE_TENANT_ID AZURE_SUBSCRIPTION_ID; do + if [[ -z "${!variable_name}" ]]; then + echo "$variable_name is required for private AKV PKCS11 signing artifacts." >&2 + exit 1 + fi + done + + - name: Log in to Azure with GitHub OIDC + uses: azure/login@532459ea530d8321f2fb9bb10d1e0bcf23869a43 # v3.0.0 + with: + client-id: ${{ inputs.azure-client-id }} + tenant-id: ${{ inputs.azure-tenant-id }} + subscription-id: ${{ inputs.azure-subscription-id }} + + - name: Install prebuilt signing tools + shell: bash + env: + RCODESIGN_BLOB_URI: ${{ inputs.rcodesign-blob-uri }} + RCODESIGN_SHA256: ${{ inputs.rcodesign-sha256 }} + RCODESIGN: ${{ steps.paths.outputs.rcodesign }} + AKV_PKCS11_LIBRARY_BLOB_URI: ${{ inputs.akv-pkcs11-library-blob-uri }} + AKV_PKCS11_LIBRARY_SHA256: ${{ inputs.akv-pkcs11-library-sha256 }} + PKCS11_LIBRARY: ${{ steps.paths.outputs.pkcs11_library }} + PKCS11_MANIFEST: ${{ steps.paths.outputs.pkcs11_manifest }} + run: | + set -euo pipefail + + download_az_blob_uri() { + local uri="$1" + local destination="$2" + local rest account container blob + + rest="${uri#az://}" + account="${rest%%/*}" + rest="${rest#*/}" + container="${rest%%/*}" + blob="${rest#*/}" + + if [[ -z "$account" || -z "$container" || -z "$blob" || "$blob" == "$rest" ]]; then + echo "Invalid Azure Blob URI. Expected az:////." >&2 + exit 1 + fi + + mkdir -p "$(dirname "$destination")" + rm -f "$destination" + if ! az storage blob download \ + --account-name "$account" \ + --container-name "$container" \ + --name "$blob" \ + --file "$destination" \ + --auth-mode login \ + --only-show-errors \ + >/dev/null 2>&1; then + echo "Failed to download a private signing artifact from Azure Blob Storage." >&2 + exit 1 + fi + } + + verify_sha256() { + local path="$1" + local expected="$2" + local actual + + actual="$(shasum -a 256 "$path" | awk '{ print $1 }')" + if [[ "$actual" != "$expected" ]]; then + echo "SHA-256 verification failed for '$path'." >&2 + exit 1 + fi + } + + echo "Downloading prebuilt rcodesign." + download_az_blob_uri "$RCODESIGN_BLOB_URI" "$RCODESIGN" + verify_sha256 "$RCODESIGN" "$RCODESIGN_SHA256" + chmod 0755 "$RCODESIGN" + + echo "Downloading prebuilt AKV PKCS11 provider." + download_az_blob_uri "$AKV_PKCS11_LIBRARY_BLOB_URI" "$PKCS11_LIBRARY" + verify_sha256 "$PKCS11_LIBRARY" "$AKV_PKCS11_LIBRARY_SHA256" + chmod 0644 "$PKCS11_LIBRARY" + + { + echo "runner_os=$RUNNER_OS" + echo "runner_arch=$RUNNER_ARCH" + echo "library_name=$(basename "$PKCS11_LIBRARY")" + } > "$PKCS11_MANIFEST" + + - name: Verify downloaded signing tools + shell: bash + env: + RCODESIGN: ${{ steps.paths.outputs.rcodesign }} + RCODESIGN_SHA256: ${{ inputs.rcodesign-sha256 }} + PKCS11_LIBRARY: ${{ steps.paths.outputs.pkcs11_library }} + AKV_PKCS11_LIBRARY_SHA256: ${{ inputs.akv-pkcs11-library-sha256 }} + PKCS11_MANIFEST: ${{ steps.paths.outputs.pkcs11_manifest }} + run: | + set -euo pipefail + + verify_sha256() { + local path="$1" + local expected="$2" + local actual + + actual="$(shasum -a 256 "$path" | awk '{ print $1 }')" + if [[ "$actual" != "$expected" ]]; then + echo "SHA-256 verification failed for '$path'." >&2 + exit 1 + fi + } + + if [[ ! -x "$RCODESIGN" ]]; then + echo "rcodesign is missing or not executable at '$RCODESIGN'." >&2 + exit 1 + fi + + if [[ ! -f "$PKCS11_LIBRARY" ]]; then + echo "AKV PKCS11 provider library is missing at '$PKCS11_LIBRARY'." >&2 + exit 1 + fi + + verify_sha256 "$RCODESIGN" "$RCODESIGN_SHA256" + verify_sha256 "$PKCS11_LIBRARY" "$AKV_PKCS11_LIBRARY_SHA256" + + "$RCODESIGN" --version + "$RCODESIGN" notarize --help > /dev/null + + if [[ -f "$PKCS11_MANIFEST" ]]; then + echo "AKV PKCS11 provider artifact manifest is present." + else + echo "AKV PKCS11 provider artifact manifest is absent." >&2 + exit 1 + fi + + - name: Download signing certificate from Key Vault + if: ${{ inputs.setup-mode == 'signing' }} + shell: bash + env: + KEY_VAULT_NAME: ${{ inputs.key-vault-name }} + KEY_NAME: ${{ inputs.key-name }} + KEY_VERSION: ${{ inputs.key-version }} + CERTIFICATE_SHA256: ${{ inputs.certificate-sha256 }} + SIGNING_CERTIFICATE_PEM: ${{ steps.paths.outputs.signing_certificate_pem }} + run: | + set -euo pipefail + + certificate_version_args=() + if [[ -n "$KEY_VERSION" ]]; then + certificate_version_args+=(--version "$KEY_VERSION") + fi + + if ! az keyvault certificate download \ + --vault-name "$KEY_VAULT_NAME" \ + --name "$KEY_NAME" \ + "${certificate_version_args[@]}" \ + --file "$SIGNING_CERTIFICATE_PEM" \ + --encoding PEM \ + --only-show-errors \ + >/dev/null 2>&1; then + echo "Failed to download the public signing certificate from Azure Key Vault." >&2 + exit 1 + fi + + if [[ -n "$CERTIFICATE_SHA256" ]]; then + actual_sha256="$( + openssl x509 -in "$SIGNING_CERTIFICATE_PEM" -noout -fingerprint -sha256 | + awk -F= '{ print toupper($2) }' | + tr -d ':\r\n' + )" + expected_sha256="$(printf '%s' "$CERTIFICATE_SHA256" | tr '[:lower:]' '[:upper:]' | tr -d ':\r\n ')" + if [[ "$actual_sha256" != "$expected_sha256" ]]; then + echo "Downloaded signing certificate SHA-256 did not match the expected fingerprint." >&2 + exit 1 + fi + fi + + - name: Export AKV PKCS11 signing environment + if: ${{ inputs.setup-mode == 'signing' }} + shell: bash + env: + RCODESIGN_ROOT: ${{ steps.paths.outputs.rcodesign_root }} + PKCS11_LIBRARY: ${{ steps.paths.outputs.pkcs11_library }} + SIGNING_CERTIFICATE_PEM: ${{ steps.paths.outputs.signing_certificate_pem }} + KEY_VAULT_NAME: ${{ inputs.key-vault-name }} + KEY_NAME: ${{ inputs.key-name }} + KEY_VERSION: ${{ inputs.key-version }} + run: | + set -euo pipefail + + { + echo "$RCODESIGN_ROOT/bin" + } >> "$GITHUB_PATH" + + { + echo "OAI_CODESIGN_BACKEND=akv-pkcs11" + echo "OAI_AKV_PKCS11_LIBRARY=$PKCS11_LIBRARY" + echo "OAI_AKV_SIGNING_CERTIFICATE_PEM=$SIGNING_CERTIFICATE_PEM" + echo "OAI_AKV_KEY_LABEL=$KEY_NAME" + echo "AZURE_CREDENTIAL_KIND=azurecli" + echo "AZURE_KEYVAULT_NAME=$KEY_VAULT_NAME" + if [[ -n "$KEY_VERSION" ]]; then + echo "AZURE_KEYVAULT_KEY_VERSION=$KEY_VERSION" + fi + } >> "$GITHUB_ENV" diff --git a/.github/actions/macos-code-sign/codex.entitlements.plist b/.github/scripts/macos-signing/codex.entitlements.plist similarity index 100% rename from .github/actions/macos-code-sign/codex.entitlements.plist rename to .github/scripts/macos-signing/codex.entitlements.plist diff --git a/.github/scripts/macos-signing/notarize_macos_binary_with_rcodesign.sh b/.github/scripts/macos-signing/notarize_macos_binary_with_rcodesign.sh new file mode 100755 index 000000000..8ebe490d4 --- /dev/null +++ b/.github/scripts/macos-signing/notarize_macos_binary_with_rcodesign.sh @@ -0,0 +1,131 @@ +#!/usr/bin/env bash + +# Submits a signed standalone macOS binary to Apple notarization through +# rcodesign. Standalone binaries cannot carry a stapled ticket, so the binary +# is submitted in a ZIP and the successful notarization log is retained. + +set -euo pipefail + +usage() { + cat >&2 <<'EOF' +Usage: notarize_macos_binary_with_rcodesign.sh --binary PATH [--report-dir PATH] [--max-wait-seconds SECONDS] + +Options: + --binary PATH Signed standalone macOS binary to notarize. + --report-dir PATH Directory for notarization logs. + --max-wait-seconds SECONDS Maximum rcodesign notarization wait time. +EOF +} + +binary_path="" +report_dir="${RUNNER_TEMP:-/tmp}/macos-binary-notarization-verification" +max_wait_seconds="600" + +while [[ $# -gt 0 ]]; do + case "$1" in + --binary) + binary_path="${2:-}" + shift 2 + ;; + --report-dir) + report_dir="${2:-}" + shift 2 + ;; + --max-wait-seconds) + max_wait_seconds="${2:-}" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown notarization argument: $1" >&2 + usage + exit 2 + ;; + esac +done + +if [[ -z "$binary_path" ]]; then + echo "--binary is required." >&2 + usage + exit 2 +fi + +if [[ ! -f "$binary_path" ]]; then + echo "Binary does not exist: $binary_path" >&2 + exit 1 +fi + +if [[ ! "$max_wait_seconds" =~ ^[0-9]+$ ]]; then + echo "--max-wait-seconds must be a non-negative integer." >&2 + exit 2 +fi + +for command_name in rcodesign zip; do + if ! command -v "$command_name" >/dev/null 2>&1; then + echo "$command_name was not found on PATH." >&2 + exit 1 + fi +done + +missing_environment=0 +for variable_name in \ + APPLE_NOTARIZATION_ISSUER_ID \ + APPLE_NOTARIZATION_KEY_ID \ + APPLE_NOTARIZATION_KEY_P8 +do + if [[ -z "${!variable_name:-}" ]]; then + echo "$variable_name must be set from CI secrets before notarizing a binary." >&2 + missing_environment=1 + fi +done + +if [[ "$missing_environment" -ne 0 ]]; then + exit 2 +fi + +mkdir -p "$report_dir" + +notarization_temp_dir="$(mktemp -d)" +trap 'rm -rf "$notarization_temp_dir" >/dev/null' EXIT + +private_key_path="$notarization_temp_dir/AuthKey_${APPLE_NOTARIZATION_KEY_ID}.p8" +if ! printf '%s' "$APPLE_NOTARIZATION_KEY_P8" | base64 --decode >"$private_key_path" 2>/dev/null; then + if ! printf '%s' "$APPLE_NOTARIZATION_KEY_P8" | base64 -D >"$private_key_path" 2>/dev/null; then + echo "APPLE_NOTARIZATION_KEY_P8 must be a base64-encoded .p8 private key." >&2 + exit 2 + fi +fi +chmod 600 "$private_key_path" + +api_key_path="$notarization_temp_dir/app-store-connect-api-key.json" +rcodesign encode-app-store-connect-api-key \ + --output-path "$api_key_path" \ + "$APPLE_NOTARIZATION_ISSUER_ID" \ + "$APPLE_NOTARIZATION_KEY_ID" \ + "$private_key_path" \ + >"$report_dir/encode-app-store-connect-api-key.log" 2>&1 + +binary_name="$(basename "$binary_path")" +archive_path="$notarization_temp_dir/${binary_name}.zip" +( + cd "$(dirname "$binary_path")" + zip -q "$archive_path" "$binary_name" +) + +notarization_log="$report_dir/${binary_name}-notarization.log" +rcodesign notarize \ + --api-key-file "$api_key_path" \ + --max-wait-seconds "$max_wait_seconds" \ + --wait \ + "$archive_path" \ + 2>&1 | tee "$notarization_log" + +{ + echo "binary_name=$binary_name" + echo "max_wait_seconds=$max_wait_seconds" + echo "binary_sha256=$(shasum -a 256 "$binary_path" | awk '{ print $1 }')" + echo "rcodesign_notarize=completed" +} >"$report_dir/${binary_name}-notarization-summary.txt" diff --git a/.github/scripts/macos-signing/notarize_macos_dmg_with_rcodesign.sh b/.github/scripts/macos-signing/notarize_macos_dmg_with_rcodesign.sh new file mode 100755 index 000000000..a1125d436 --- /dev/null +++ b/.github/scripts/macos-signing/notarize_macos_dmg_with_rcodesign.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env bash + +# Notarizes and staples a signed macOS DMG through rcodesign. +# +# This is the Linux-compatible notarization path for the AKV/PKCS#11 signing +# flow. It records notarization inputs and logs so workflow artifacts can be +# audited without exposing the App Store Connect private key. + +set -euo pipefail + +usage() { + cat >&2 <<'EOF' +Usage: notarize_macos_dmg_with_rcodesign.sh --dmg PATH [--report-dir PATH] [--max-wait-seconds SECONDS] + +Options: + --dmg PATH Signed DMG to submit to Apple notarization. + --report-dir PATH Directory for notarization logs. + --max-wait-seconds SECONDS Maximum rcodesign notarization wait time. +EOF +} + +dmg_path="" +report_dir="${RUNNER_TEMP:-/tmp}/macos-notarization-verification" +max_wait_seconds="600" + +while [[ $# -gt 0 ]]; do + case "$1" in + --dmg) + dmg_path="${2:-}" + shift 2 + ;; + --report-dir) + report_dir="${2:-}" + shift 2 + ;; + --max-wait-seconds) + max_wait_seconds="${2:-}" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown notarization argument: $1" >&2 + usage + exit 2 + ;; + esac +done + +if [[ -z "$dmg_path" ]]; then + echo "--dmg is required." >&2 + usage + exit 2 +fi + +if [[ ! -f "$dmg_path" ]]; then + echo "DMG does not exist: $dmg_path" >&2 + exit 1 +fi + +if [[ ! "$max_wait_seconds" =~ ^[0-9]+$ ]]; then + echo "--max-wait-seconds must be a non-negative integer." >&2 + exit 2 +fi + +if ! command -v rcodesign > /dev/null 2>&1; then + echo "rcodesign was not found on PATH." >&2 + exit 1 +fi + +missing_environment=0 +for variable_name in \ + APPLE_NOTARIZATION_ISSUER_ID \ + APPLE_NOTARIZATION_KEY_ID \ + APPLE_NOTARIZATION_KEY_P8 +do + if [[ -z "${!variable_name:-}" ]]; then + echo "$variable_name must be set from CI secrets before notarizing a DMG." >&2 + missing_environment=1 + fi +done + +if [[ "$missing_environment" -ne 0 ]]; then + exit 2 +fi + +mkdir -p "$report_dir" + +notarization_temp_dir="$(mktemp -d)" +trap 'rm -rf "$notarization_temp_dir" > /dev/null' EXIT + +private_key_path="$notarization_temp_dir/AuthKey_${APPLE_NOTARIZATION_KEY_ID}.p8" +if ! printf '%s' "$APPLE_NOTARIZATION_KEY_P8" | base64 --decode > "$private_key_path" 2> /dev/null; then + if ! printf '%s' "$APPLE_NOTARIZATION_KEY_P8" | base64 -D > "$private_key_path" 2> /dev/null; then + echo "APPLE_NOTARIZATION_KEY_P8 must be a base64-encoded .p8 private key." >&2 + exit 2 + fi +fi +chmod 600 "$private_key_path" + +api_key_path="$notarization_temp_dir/app-store-connect-api-key.json" +rcodesign encode-app-store-connect-api-key \ + --output-path "$api_key_path" \ + "$APPLE_NOTARIZATION_ISSUER_ID" \ + "$APPLE_NOTARIZATION_KEY_ID" \ + "$private_key_path" \ + > "$report_dir/encode-app-store-connect-api-key.log" 2>&1 + +notarization_log="$report_dir/dmg-notarization.log" +rcodesign notarize \ + --api-key-file "$api_key_path" \ + --max-wait-seconds "$max_wait_seconds" \ + --staple \ + "$dmg_path" \ + 2>&1 | tee "$notarization_log" + +{ + echo "dmg_path=$dmg_path" + echo "max_wait_seconds=$max_wait_seconds" + echo "dmg_sha256=$(shasum -a 256 "$dmg_path" | awk '{ print $1 }')" + echo "rcodesign_notarize_staple=completed" +} > "$report_dir/dmg-notarization-summary.txt" diff --git a/.github/scripts/macos-signing/sign_macos_code.sh b/.github/scripts/macos-signing/sign_macos_code.sh new file mode 100755 index 000000000..9f8674141 --- /dev/null +++ b/.github/scripts/macos-signing/sign_macos_code.sh @@ -0,0 +1,245 @@ +#!/usr/bin/env bash + +# Small compatibility wrapper around native codesign and rcodesign. +# +# Existing packaging scripts call this instead of choosing a signing backend +# directly. OAI_CODESIGN_BACKEND=akv-pkcs11 routes signing through rcodesign +# while preserving the option, entitlement, identifier, timestamp, and deep +# signing surface used by the native codesign path. + +set -euo pipefail + +usage() { + cat >&2 <<'EOF' +Usage: sign_macos_code.sh --target PATH --identity IDENTITY [options] + +Options: + --deep true|false + --entitlements PATH + --identifier IDENTIFIER + --identity IDENTITY + --options FLAGS + --target PATH + --timestamp true|false|none +EOF +} + +target="" +identity="" +options="" +entitlements_file="" +identifier="" +deep="false" +timestamp="true" + +while [[ $# -gt 0 ]]; do + case "$1" in + --deep) + deep="${2:-}" + shift 2 + ;; + --entitlements) + entitlements_file="${2:-}" + shift 2 + ;; + --identifier) + identifier="${2:-}" + shift 2 + ;; + --identity) + identity="${2:-}" + shift 2 + ;; + --options) + options="${2:-}" + shift 2 + ;; + --target) + target="${2:-}" + shift 2 + ;; + --timestamp) + timestamp="${2:-}" + shift 2 + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "Unknown signing argument: $1" >&2 + usage + exit 2 + ;; + esac +done + +if [[ -z "$target" ]]; then + echo "--target is required." >&2 + usage + exit 2 +fi + +if [[ ! -e "$target" ]]; then + echo "Signing target does not exist: $target" >&2 + exit 1 +fi + +case "$deep" in + true|false) ;; + *) + echo "--deep must be true or false, got '$deep'." >&2 + exit 2 + ;; +esac + +case "$timestamp" in + true|false|none) ;; + *) + echo "--timestamp must be true, false, or none, got '$timestamp'." >&2 + exit 2 + ;; +esac + +sign_with_codesign() { + if [[ -z "$identity" ]]; then + echo "Native codesign requires --identity." >&2 + exit 2 + fi + + local -a args + args=(--force) + + if [[ "$deep" == "true" ]]; then + args+=(--deep) + fi + + if [[ -n "$options" ]]; then + args+=(--options "$options") + fi + + case "$timestamp" in + true) + args+=(--timestamp) + ;; + false|none) + args+=(--timestamp=none) + ;; + esac + + if [[ -n "$entitlements_file" ]]; then + args+=(--entitlements "$entitlements_file") + fi + + if [[ -n "$identifier" ]]; then + args+=(--identifier "$identifier") + fi + + args+=(--sign "$identity" "$target") + codesign "${args[@]}" +} + +append_rcodesign_flags() { + local raw_options="$1" + local option="" + + if [[ -z "$raw_options" ]]; then + return 0 + fi + + IFS=',' read -ra split_options <<< "$raw_options" + for option in "${split_options[@]}"; do + option="${option//[[:space:]]/}" + [[ -z "$option" ]] && continue + + case "$option" in + host|hard|kill|expires|restrict|library|runtime|linker-signed) + rcodesign_args+=(--code-signature-flags "$option") + ;; + *) + echo "Unsupported rcodesign code signature option: $option" >&2 + exit 2 + ;; + esac + done +} + +rcodesign_options_require_notarization() { + local raw_options="$1" + local option="" + + if [[ -z "$raw_options" || "$timestamp" != "true" ]]; then + return 1 + fi + + IFS=',' read -ra split_options <<< "$raw_options" + for option in "${split_options[@]}"; do + option="${option//[[:space:]]/}" + if [[ "$option" == "runtime" ]]; then + return 0 + fi + done + + return 1 +} + +sign_with_rcodesign() { + : "${OAI_AKV_PKCS11_LIBRARY:?OAI_AKV_PKCS11_LIBRARY is required for AKV PKCS11 signing.}" + : "${OAI_AKV_SIGNING_CERTIFICATE_PEM:?OAI_AKV_SIGNING_CERTIFICATE_PEM is required for AKV PKCS11 signing.}" + : "${OAI_AKV_KEY_LABEL:?OAI_AKV_KEY_LABEL is required for AKV PKCS11 signing.}" + + if ! command -v rcodesign >/dev/null 2>&1; then + echo "rcodesign was not found on PATH." >&2 + exit 1 + fi + + local -a rcodesign_args + rcodesign_args=( + sign + --config-file /dev/null + --pkcs11-library "$OAI_AKV_PKCS11_LIBRARY" + --pkcs11-certificate-file "$OAI_AKV_SIGNING_CERTIFICATE_PEM" + --pkcs11-key-label "$OAI_AKV_KEY_LABEL" + ) + + if [[ "$deep" == "false" ]]; then + rcodesign_args+=(--shallow) + fi + + case "$timestamp" in + true) + ;; + false|none) + rcodesign_args+=(--timestamp-url none) + ;; + esac + + append_rcodesign_flags "$options" + if rcodesign_options_require_notarization "$options"; then + rcodesign_args+=(--for-notarization) + fi + + if [[ -n "$entitlements_file" ]]; then + rcodesign_args+=(--entitlements-xml-file "$entitlements_file") + fi + + if [[ -n "$identifier" ]]; then + rcodesign_args+=(--binary-identifier "$identifier") + fi + + rcodesign_args+=("$target") + rcodesign "${rcodesign_args[@]}" +} + +case "${OAI_CODESIGN_BACKEND:-codesign}" in + codesign|"") + sign_with_codesign + ;; + akv-pkcs11) + sign_with_rcodesign + ;; + *) + echo "Unsupported OAI_CODESIGN_BACKEND: ${OAI_CODESIGN_BACKEND}" >&2 + exit 2 + ;; +esac diff --git a/.github/workflows/rust-release.yml b/.github/workflows/rust-release.yml index 55f95fa27..5e3b263ed 100644 --- a/.github/workflows/rust-release.yml +++ b/.github/workflows/rust-release.yml @@ -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.