mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
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:
committed by
GitHub
Unverified
parent
c143a86de8
commit
ad2012d645
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
}
|
||||
@@ -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://<account>/<container>/<blob>." >&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://<account>/<container>/<blob>." >&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"
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
+245
@@ -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
|
||||
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user