mirror of
https://github.com/hanzoai/dev.git
synced 2026-10-17 15:34:54 +00:00
github: upstream's signing, zsh release and workspace checks go
Nothing in release.yml, hanzo.yml, the Makefile or scripts/ names any of them: the zsh release script and its dotslash config (upstream dropped the zsh fork), verify_cargo_workspace_manifests.py and verify_tui_core_boundary.py (checks of a codex-rs this repository does not hold), the Linux, macOS and Windows code-signing actions and the macOS notarizing scripts (upstream's identities), and upstream's issue templates, which asked about the Codex app and CLI.
This commit is contained in:
@@ -1,54 +0,0 @@
|
|||||||
name: 🖥️ Codex App Bug
|
|
||||||
description: Report an issue with the Codex App
|
|
||||||
labels:
|
|
||||||
- app
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
Before submitting a new issue, please search for existing issues to see if your issue has already been reported.
|
|
||||||
If it has, please add a 👍 reaction (no need to leave a comment) to the existing issue instead of creating a new one.
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: version
|
|
||||||
attributes:
|
|
||||||
label: What version of the Codex App are you using (From “About Codex” dialog)?
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: input
|
|
||||||
id: plan
|
|
||||||
attributes:
|
|
||||||
label: What subscription do you have?
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: input
|
|
||||||
id: platform
|
|
||||||
attributes:
|
|
||||||
label: What platform is your computer?
|
|
||||||
description: |
|
|
||||||
For macOS and Linux: copy the output of `uname -mprs`
|
|
||||||
For Windows: copy the output of `"$([Environment]::OSVersion | ForEach-Object VersionString) $(if ([Environment]::Is64BitOperatingSystem) { "x64" } else { "x86" })"` in the PowerShell console
|
|
||||||
- type: textarea
|
|
||||||
id: actual
|
|
||||||
attributes:
|
|
||||||
label: What issue are you seeing?
|
|
||||||
description: Please include the full error messages and prompts with PII redacted. If possible, please provide text instead of a screenshot.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: steps
|
|
||||||
attributes:
|
|
||||||
label: What steps can reproduce the bug?
|
|
||||||
description: Explain the bug and provide a code snippet that can reproduce it. Please include session id, token limit usage, context window usage if applicable.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: expected
|
|
||||||
attributes:
|
|
||||||
label: What is the expected behavior?
|
|
||||||
description: If possible, please provide text instead of a screenshot.
|
|
||||||
- type: textarea
|
|
||||||
id: notes
|
|
||||||
attributes:
|
|
||||||
label: Additional information
|
|
||||||
description: Is there anything else you think we should know?
|
|
||||||
@@ -1,60 +0,0 @@
|
|||||||
name: 💻 CLI Bug
|
|
||||||
description: Report an issue in the Codex CLI
|
|
||||||
labels:
|
|
||||||
- bug
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
Thank you for submitting a bug report! It helps make Code better for everyone.
|
|
||||||
|
|
||||||
If you need help or support using Code, and are not reporting a bug, please post on [code/discussions](https://github.com/hanzoai/dev/discussions), where you can ask questions or engage with others on ideas for how to improve Code.
|
|
||||||
|
|
||||||
Make sure you are running the [latest](https://npmjs.com/package/@hanzo/dev) version of the Code CLI. The bug you are experiencing may already have been fixed.
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: version
|
|
||||||
attributes:
|
|
||||||
label: What version of Code is running?
|
|
||||||
description: Copy the output of `code --version` (or `coder --version`)
|
|
||||||
- type: input
|
|
||||||
id: model
|
|
||||||
attributes:
|
|
||||||
label: Which model were you using?
|
|
||||||
description: Like `gpt-5`, `o3`, etc.
|
|
||||||
- type: input
|
|
||||||
id: platform
|
|
||||||
attributes:
|
|
||||||
label: What platform is your computer?
|
|
||||||
description: |
|
|
||||||
For macOS and Linux: copy the output of `uname -mprs`
|
|
||||||
For Windows: copy the output of `"$([Environment]::OSVersion | ForEach-Object VersionString) $(if ([Environment]::Is64BitOperatingSystem) { "x64" } else { "x86" })"` in the PowerShell console
|
|
||||||
- type: input
|
|
||||||
id: terminal
|
|
||||||
attributes:
|
|
||||||
label: What terminal emulator and version are you using (if applicable)?
|
|
||||||
description: |
|
|
||||||
Also note any multiplexer in use (screen / tmux / zellij).
|
|
||||||
E.g., VS Code, Terminal.app, iTerm2, Ghostty, Windows Terminal (WSL / PowerShell)
|
|
||||||
- type: textarea
|
|
||||||
id: steps
|
|
||||||
attributes:
|
|
||||||
label: What steps can reproduce the bug?
|
|
||||||
description: Explain the bug and provide a code snippet that can reproduce it.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: expected
|
|
||||||
attributes:
|
|
||||||
label: What is the expected behavior?
|
|
||||||
description: If possible, please provide text instead of a screenshot.
|
|
||||||
- type: textarea
|
|
||||||
id: actual
|
|
||||||
attributes:
|
|
||||||
label: What do you see instead?
|
|
||||||
description: If possible, please provide text instead of a screenshot.
|
|
||||||
- type: textarea
|
|
||||||
id: notes
|
|
||||||
attributes:
|
|
||||||
label: Additional information
|
|
||||||
description: Is there anything else you think we should know?
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
name: 🪲 Other Bug
|
|
||||||
description: Report an issue in Codex Web, integrations, or other Codex components
|
|
||||||
labels:
|
|
||||||
- bug
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
Before submitting a new issue, please search for existing issues to see if your issue has already been reported.
|
|
||||||
If it has, please add a 👍 reaction (no need to leave a comment) to the existing issue instead of creating a new one.
|
|
||||||
|
|
||||||
If you need help or support using Codex and are not reporting a bug, please post on [codex/discussions](https://github.com/openai/codex/discussions), where you can ask questions or engage with others on ideas for how to improve codex.
|
|
||||||
|
|
||||||
- type: textarea
|
|
||||||
id: actual
|
|
||||||
attributes:
|
|
||||||
label: What issue are you seeing?
|
|
||||||
description: Please include the full error messages and prompts with PII redacted. If possible, please provide text instead of a screenshot.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: steps
|
|
||||||
attributes:
|
|
||||||
label: What steps can reproduce the bug?
|
|
||||||
description: Explain the bug and provide a code snippet that can reproduce it.
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: expected
|
|
||||||
attributes:
|
|
||||||
label: What is the expected behavior?
|
|
||||||
description: If possible, please provide text instead of a screenshot.
|
|
||||||
- type: textarea
|
|
||||||
id: notes
|
|
||||||
attributes:
|
|
||||||
label: Additional information
|
|
||||||
description: Is there anything else you think we should know?
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
name: 🎁 Feature Request
|
|
||||||
description: Propose a new feature for Code
|
|
||||||
labels:
|
|
||||||
- enhancement
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
Is Code missing a feature that you'd like to see? Feel free to propose it here.
|
|
||||||
|
|
||||||
Before you submit a feature:
|
|
||||||
1. Search existing issues for similar features. If you find one, 👍 it rather than opening a new one.
|
|
||||||
2. Please be aware that this feature may be automatically implemented through our Issue Triage - if this happens you will receive a reply to this issue with information on how to run it. We'd really appreciate any feedback on the implementation as it helps us improve the feature for everyone!
|
|
||||||
|
|
||||||
- type: input
|
|
||||||
id: variant
|
|
||||||
attributes:
|
|
||||||
label: What variant of Codex are you using?
|
|
||||||
description: (e.g., App, IDE Extension, CLI, Web)
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
id: feature
|
|
||||||
attributes:
|
|
||||||
label: What feature would you like to see?
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
name: 📗 Documentation Issue
|
|
||||||
description: Tell us if there is missing or incorrect documentation
|
|
||||||
labels: [documentation]
|
|
||||||
body:
|
|
||||||
- type: markdown
|
|
||||||
attributes:
|
|
||||||
value: |
|
|
||||||
Thank you for submitting a documentation request. It helps make Codex better.
|
|
||||||
- type: dropdown
|
|
||||||
attributes:
|
|
||||||
label: What is the type of issue?
|
|
||||||
multiple: true
|
|
||||||
options:
|
|
||||||
- Documentation is missing
|
|
||||||
- Documentation is incorrect
|
|
||||||
- Documentation is confusing
|
|
||||||
- Example code is not working
|
|
||||||
- Something else
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: What is the issue?
|
|
||||||
validations:
|
|
||||||
required: true
|
|
||||||
- type: textarea
|
|
||||||
attributes:
|
|
||||||
label: Where did you find it?
|
|
||||||
description: If possible, please provide the URL(s) where you found this issue.
|
|
||||||
@@ -1,49 +0,0 @@
|
|||||||
name: linux-code-sign
|
|
||||||
description: Sign Linux artifacts with cosign.
|
|
||||||
inputs:
|
|
||||||
target:
|
|
||||||
description: Target triple for the artifacts to sign.
|
|
||||||
required: true
|
|
||||||
artifacts-dir:
|
|
||||||
description: Absolute path to the directory containing built binaries to sign.
|
|
||||||
required: true
|
|
||||||
binaries:
|
|
||||||
description: Space-delimited binary basenames to sign.
|
|
||||||
default: "codex codex-responses-api-proxy"
|
|
||||||
|
|
||||||
runs:
|
|
||||||
using: composite
|
|
||||||
steps:
|
|
||||||
- name: Install cosign
|
|
||||||
uses: sigstore/cosign-installer@dc72c7d5c4d10cd6bcb8cf6e3fd625a9e5e537da # v3.7.0
|
|
||||||
|
|
||||||
- name: Cosign Linux artifacts
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
ARTIFACTS_DIR: ${{ inputs.artifacts-dir }}
|
|
||||||
BINARIES: ${{ inputs.binaries }}
|
|
||||||
COSIGN_EXPERIMENTAL: "1"
|
|
||||||
COSIGN_YES: "true"
|
|
||||||
COSIGN_OIDC_CLIENT_ID: "sigstore"
|
|
||||||
COSIGN_OIDC_ISSUER: "https://oauth2.sigstore.dev/auth"
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
dest="$ARTIFACTS_DIR"
|
|
||||||
if [[ ! -d "$dest" ]]; then
|
|
||||||
echo "Destination $dest does not exist"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
for binary in ${BINARIES}; do
|
|
||||||
artifact="${dest}/${binary}"
|
|
||||||
if [[ ! -f "$artifact" ]]; then
|
|
||||||
echo "Binary $artifact not found"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
cosign sign-blob \
|
|
||||||
--yes \
|
|
||||||
--bundle "${artifact}.sigstore" \
|
|
||||||
"$artifact"
|
|
||||||
done
|
|
||||||
@@ -1,349 +0,0 @@
|
|||||||
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"
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
name: windows-code-sign
|
|
||||||
description: Sign Windows binaries with Azure Trusted Signing.
|
|
||||||
|
|
||||||
inputs:
|
|
||||||
target:
|
|
||||||
description: Target triple for the artifacts to sign.
|
|
||||||
required: true
|
|
||||||
binaries:
|
|
||||||
description: Space-delimited binary basenames to sign.
|
|
||||||
default: "codex codex-responses-api-proxy codex-windows-sandbox-setup codex-command-runner"
|
|
||||||
client-id:
|
|
||||||
description: Azure Trusted Signing client ID.
|
|
||||||
required: true
|
|
||||||
tenant-id:
|
|
||||||
description: Azure tenant ID for Trusted Signing.
|
|
||||||
required: true
|
|
||||||
subscription-id:
|
|
||||||
description: Azure subscription ID for Trusted Signing.
|
|
||||||
required: true
|
|
||||||
endpoint:
|
|
||||||
description: Azure Trusted Signing endpoint.
|
|
||||||
required: true
|
|
||||||
account-name:
|
|
||||||
description: Azure Trusted Signing account name.
|
|
||||||
required: true
|
|
||||||
certificate-profile-name:
|
|
||||||
description: Certificate profile name for signing.
|
|
||||||
required: true
|
|
||||||
|
|
||||||
runs:
|
|
||||||
using: composite
|
|
||||||
steps:
|
|
||||||
- name: Azure login for Trusted Signing (OIDC)
|
|
||||||
uses: azure/login@a457da9ea143d694b1b9c7c869ebb04ebe844ef5 # v2.3.0
|
|
||||||
with:
|
|
||||||
client-id: ${{ inputs.client-id }}
|
|
||||||
tenant-id: ${{ inputs.tenant-id }}
|
|
||||||
subscription-id: ${{ inputs.subscription-id }}
|
|
||||||
|
|
||||||
- name: Prepare file list
|
|
||||||
id: prepare
|
|
||||||
shell: bash
|
|
||||||
env:
|
|
||||||
TARGET: ${{ inputs.target }}
|
|
||||||
BINARIES: ${{ inputs.binaries }}
|
|
||||||
run: |
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
{
|
|
||||||
echo "files<<EOF"
|
|
||||||
for binary in ${BINARIES}; do
|
|
||||||
echo "${GITHUB_WORKSPACE}/vendor/codex/target/${TARGET}/release/${binary}.exe"
|
|
||||||
done
|
|
||||||
echo "EOF"
|
|
||||||
} >> "$GITHUB_OUTPUT"
|
|
||||||
|
|
||||||
- name: Sign Windows binaries with Azure Trusted Signing
|
|
||||||
uses: azure/trusted-signing-action@1d365fec12862c4aa68fcac418143d73f0cea293 # v0.5.11
|
|
||||||
with:
|
|
||||||
endpoint: ${{ inputs.endpoint }}
|
|
||||||
trusted-signing-account-name: ${{ inputs.account-name }}
|
|
||||||
certificate-profile-name: ${{ inputs.certificate-profile-name }}
|
|
||||||
exclude-environment-credential: true
|
|
||||||
exclude-workload-identity-credential: true
|
|
||||||
exclude-managed-identity-credential: true
|
|
||||||
exclude-shared-token-cache-credential: true
|
|
||||||
exclude-visual-studio-credential: true
|
|
||||||
exclude-visual-studio-code-credential: true
|
|
||||||
exclude-azure-cli-credential: false
|
|
||||||
exclude-azure-powershell-credential: true
|
|
||||||
exclude-azure-developer-cli-credential: true
|
|
||||||
exclude-interactive-browser-credential: true
|
|
||||||
cache-dependencies: false
|
|
||||||
files: ${{ steps.prepare.outputs.files }}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
{
|
|
||||||
"outputs": {
|
|
||||||
"codex-zsh": {
|
|
||||||
"platforms": {
|
|
||||||
"macos-aarch64": {
|
|
||||||
"name": "codex-zsh-aarch64-apple-darwin.tar.gz",
|
|
||||||
"format": "tar.gz",
|
|
||||||
"path": "codex-zsh/bin/zsh"
|
|
||||||
},
|
|
||||||
"linux-x86_64": {
|
|
||||||
"name": "codex-zsh-x86_64-unknown-linux-musl.tar.gz",
|
|
||||||
"format": "tar.gz",
|
|
||||||
"path": "codex-zsh/bin/zsh"
|
|
||||||
},
|
|
||||||
"linux-aarch64": {
|
|
||||||
"name": "codex-zsh-aarch64-unknown-linux-musl.tar.gz",
|
|
||||||
"format": "tar.gz",
|
|
||||||
"path": "codex-zsh/bin/zsh"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
if [[ "$#" -ne 1 ]]; then
|
|
||||||
echo "usage: $0 <archive-path>" >&2
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
archive_path="$1"
|
|
||||||
workspace="${GITHUB_WORKSPACE:?missing GITHUB_WORKSPACE}"
|
|
||||||
zsh_commit="${ZSH_COMMIT:?missing ZSH_COMMIT}"
|
|
||||||
zsh_patch="${ZSH_PATCH:?missing ZSH_PATCH}"
|
|
||||||
temp_root="${RUNNER_TEMP:-/tmp}"
|
|
||||||
work_root="$(mktemp -d "${temp_root%/}/codex-zsh-release.XXXXXX")"
|
|
||||||
trap 'rm -rf "$work_root"' EXIT
|
|
||||||
|
|
||||||
source_root="${work_root}/zsh"
|
|
||||||
package_root="${work_root}/codex-zsh"
|
|
||||||
wrapper_path="${work_root}/exec-wrapper"
|
|
||||||
stdout_path="${work_root}/stdout.txt"
|
|
||||||
wrapper_log_path="${work_root}/wrapper.log"
|
|
||||||
|
|
||||||
git clone https://git.code.sf.net/p/zsh/code "$source_root"
|
|
||||||
cd "$source_root"
|
|
||||||
git checkout "$zsh_commit"
|
|
||||||
git apply "${workspace}/${zsh_patch}"
|
|
||||||
./Util/preconfig
|
|
||||||
./configure
|
|
||||||
|
|
||||||
cores="$(command -v nproc >/dev/null 2>&1 && nproc || getconf _NPROCESSORS_ONLN)"
|
|
||||||
make -j"${cores}"
|
|
||||||
|
|
||||||
cat > "$wrapper_path" <<'EOF'
|
|
||||||
#!/usr/bin/env bash
|
|
||||||
set -euo pipefail
|
|
||||||
: "${DEV_WRAPPER_LOG:?missing DEV_WRAPPER_LOG}"
|
|
||||||
printf '%s\n' "$@" > "$DEV_WRAPPER_LOG"
|
|
||||||
file="$1"
|
|
||||||
shift
|
|
||||||
if [[ "$#" -eq 0 ]]; then
|
|
||||||
exec "$file"
|
|
||||||
fi
|
|
||||||
arg0="$1"
|
|
||||||
shift
|
|
||||||
exec -a "$arg0" "$file" "$@"
|
|
||||||
EOF
|
|
||||||
chmod +x "$wrapper_path"
|
|
||||||
|
|
||||||
DEV_WRAPPER_LOG="$wrapper_log_path" \
|
|
||||||
EXEC_WRAPPER="$wrapper_path" \
|
|
||||||
"${source_root}/Src/zsh" -fc '/bin/echo smoke-zsh' > "$stdout_path"
|
|
||||||
|
|
||||||
grep -Fx "smoke-zsh" "$stdout_path"
|
|
||||||
grep -Fx "/bin/echo" "$wrapper_log_path"
|
|
||||||
|
|
||||||
mkdir -p "$package_root/bin" "$(dirname "${workspace}/${archive_path}")"
|
|
||||||
cp "${source_root}/Src/zsh" "$package_root/bin/zsh"
|
|
||||||
chmod +x "$package_root/bin/zsh"
|
|
||||||
|
|
||||||
(cd "$work_root" && tar -czf "${workspace}/${archive_path}" codex-zsh)
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="UTF-8"?>
|
|
||||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
||||||
<plist version="1.0">
|
|
||||||
<dict>
|
|
||||||
<key>com.apple.security.cs.allow-jit</key>
|
|
||||||
<true/>
|
|
||||||
</dict>
|
|
||||||
</plist>
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# Notarize a standalone binary and retain its diagnostic report.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat >&2 <<'EOF'
|
|
||||||
Usage: notarize_macos_binary_with_akv.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 Apple 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 python3 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_AKV_KEY_NAME \
|
|
||||||
AZURE_KEYVAULT_NAME
|
|
||||||
do
|
|
||||||
if [[ -z "${!variable_name:-}" ]]; then
|
|
||||||
echo "$variable_name must be configured 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
|
|
||||||
|
|
||||||
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"
|
|
||||||
python3 "$(dirname "$0")/notarize_with_akv.py" \
|
|
||||||
--file "$archive_path" \
|
|
||||||
--report-log "$report_dir/${binary_name}-notarization-developer-log.json" \
|
|
||||||
--max-wait-seconds "$max_wait_seconds" \
|
|
||||||
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 "notarization=completed"
|
|
||||||
} >"$report_dir/${binary_name}-notarization-summary.txt"
|
|
||||||
@@ -1,101 +0,0 @@
|
|||||||
#!/usr/bin/env bash
|
|
||||||
|
|
||||||
# Notarize a signed disk image and staple its ticket.
|
|
||||||
|
|
||||||
set -euo pipefail
|
|
||||||
|
|
||||||
usage() {
|
|
||||||
cat >&2 <<'EOF'
|
|
||||||
Usage: notarize_macos_dmg_with_akv.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 Apple 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_AKV_KEY_NAME \
|
|
||||||
AZURE_KEYVAULT_NAME
|
|
||||||
do
|
|
||||||
if [[ -z "${!variable_name:-}" ]]; then
|
|
||||||
echo "$variable_name must be configured before notarizing a DMG." >&2
|
|
||||||
missing_environment=1
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ "$missing_environment" -ne 0 ]]; then
|
|
||||||
exit 2
|
|
||||||
fi
|
|
||||||
|
|
||||||
mkdir -p "$report_dir"
|
|
||||||
|
|
||||||
notarization_log="$report_dir/dmg-notarization.log"
|
|
||||||
python3 "$(dirname "$0")/notarize_with_akv.py" \
|
|
||||||
--file "$dmg_path" \
|
|
||||||
--report-log "$report_dir/dmg-notarization-developer-log.json" \
|
|
||||||
--max-wait-seconds "$max_wait_seconds" \
|
|
||||||
2>&1 | tee "$notarization_log"
|
|
||||||
|
|
||||||
rcodesign staple "$dmg_path" 2>&1 | tee -a "$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 "notarization_staple=completed"
|
|
||||||
} > "$report_dir/dmg-notarization-summary.txt"
|
|
||||||
@@ -1,460 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Submit release artifacts for notarization and wait for the result."""
|
|
||||||
|
|
||||||
import argparse
|
|
||||||
import base64
|
|
||||||
import datetime
|
|
||||||
import hashlib
|
|
||||||
import hmac
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import re
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import time
|
|
||||||
import urllib.error
|
|
||||||
import urllib.parse
|
|
||||||
import urllib.request
|
|
||||||
from dataclasses import dataclass
|
|
||||||
from pathlib import Path
|
|
||||||
from typing import Any
|
|
||||||
|
|
||||||
APPLE_NOTARY_URL = "https://appstoreconnect.apple.com/notary/v2/submissions"
|
|
||||||
AZURE_KEYVAULT_API_VERSION = "7.4"
|
|
||||||
AWS_REGION = "us-west-2"
|
|
||||||
JWT_LIFETIME_SECONDS = 15 * 60
|
|
||||||
POLL_INTERVAL_SECONDS = 10
|
|
||||||
MAX_SINGLE_UPLOAD_BYTES = 5 * 1024 * 1024 * 1024
|
|
||||||
|
|
||||||
|
|
||||||
class NotarizationError(RuntimeError):
|
|
||||||
"""Describe a notarization failure."""
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True)
|
|
||||||
class NotarizationConfiguration:
|
|
||||||
"""Hold the account and signing-key configuration."""
|
|
||||||
|
|
||||||
issuer_id: str
|
|
||||||
apple_key_id: str
|
|
||||||
vault_name: str
|
|
||||||
vault_key_name: str
|
|
||||||
vault_key_version: str
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def from_environment(cls) -> "NotarizationConfiguration":
|
|
||||||
"""Load and validate the notarization configuration."""
|
|
||||||
|
|
||||||
required = {
|
|
||||||
"issuer_id": "APPLE_NOTARIZATION_ISSUER_ID",
|
|
||||||
"vault_name": "AZURE_KEYVAULT_NAME",
|
|
||||||
"vault_key_name": "APPLE_NOTARIZATION_AKV_KEY_NAME",
|
|
||||||
}
|
|
||||||
values = {field: os.environ.get(name, "").strip() for field, name in required.items()}
|
|
||||||
missing = [name for field, name in required.items() if not values[field]]
|
|
||||||
if missing:
|
|
||||||
raise NotarizationError("Missing notarization configuration: " + ", ".join(missing))
|
|
||||||
if not re.fullmatch(r"[A-Za-z][A-Za-z0-9-]{2,23}", values["vault_name"]):
|
|
||||||
raise NotarizationError("Invalid signing vault name")
|
|
||||||
if not re.fullmatch(r"[A-Za-z0-9-]+", values["vault_key_name"]):
|
|
||||||
raise NotarizationError("Invalid notarization key name")
|
|
||||||
key_version = os.environ.get("APPLE_NOTARIZATION_AKV_KEY_VERSION", "").strip()
|
|
||||||
if key_version and not re.fullmatch(r"[0-9a-fA-F]{32}", key_version):
|
|
||||||
raise NotarizationError(
|
|
||||||
"Notarization key must resolve to a 32-character pinned version"
|
|
||||||
)
|
|
||||||
|
|
||||||
command = [
|
|
||||||
"az",
|
|
||||||
"keyvault",
|
|
||||||
"key",
|
|
||||||
"show",
|
|
||||||
"--vault-name",
|
|
||||||
values["vault_name"],
|
|
||||||
"--name",
|
|
||||||
values["vault_key_name"],
|
|
||||||
]
|
|
||||||
if key_version:
|
|
||||||
command.extend(["--version", key_version])
|
|
||||||
command.extend(
|
|
||||||
[
|
|
||||||
"--query",
|
|
||||||
'{id:key.kid,apple_key_id:tags."apple-key-id"}',
|
|
||||||
"--output",
|
|
||||||
"json",
|
|
||||||
"--only-show-errors",
|
|
||||||
]
|
|
||||||
)
|
|
||||||
|
|
||||||
# Resolve the key identifier and version from the same lookup.
|
|
||||||
try:
|
|
||||||
result = subprocess.run(command, check=True, capture_output=True, text=True)
|
|
||||||
except (OSError, subprocess.CalledProcessError) as error:
|
|
||||||
raise NotarizationError("Notarization signing key could not be read") from error
|
|
||||||
try:
|
|
||||||
metadata = json.loads(result.stdout)
|
|
||||||
returned_key_id = metadata["id"]
|
|
||||||
apple_key_id = metadata["apple_key_id"]
|
|
||||||
except (json.JSONDecodeError, KeyError, TypeError) as error:
|
|
||||||
raise NotarizationError("Notarization key metadata is invalid") from error
|
|
||||||
|
|
||||||
key_id_prefix = (
|
|
||||||
f"https://{values['vault_name']}.vault.azure.net/keys/{values['vault_key_name']}/"
|
|
||||||
)
|
|
||||||
if not isinstance(returned_key_id, str) or not returned_key_id.startswith(key_id_prefix):
|
|
||||||
raise NotarizationError("Unexpected notarization signing key")
|
|
||||||
returned_key_version = returned_key_id[len(key_id_prefix) :]
|
|
||||||
if not re.fullmatch(r"[0-9a-fA-F]{32}", returned_key_version):
|
|
||||||
raise NotarizationError(
|
|
||||||
"Notarization key must resolve to a 32-character pinned version"
|
|
||||||
)
|
|
||||||
if key_version and returned_key_version.lower() != key_version.lower():
|
|
||||||
raise NotarizationError("Unexpected notarization key version")
|
|
||||||
if not isinstance(apple_key_id, str) or not apple_key_id.strip():
|
|
||||||
raise NotarizationError("Notarization key must have an apple-key-id tag")
|
|
||||||
|
|
||||||
values["apple_key_id"] = apple_key_id.strip()
|
|
||||||
values["vault_key_version"] = returned_key_version
|
|
||||||
return cls(**values)
|
|
||||||
|
|
||||||
@property
|
|
||||||
def versioned_key_id(self) -> str:
|
|
||||||
"""Return the versioned signing-key identifier."""
|
|
||||||
|
|
||||||
return (
|
|
||||||
f"https://{self.vault_name}.vault.azure.net/keys/"
|
|
||||||
f"{self.vault_key_name}/{self.vault_key_version}"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def base64url_encode(value: bytes) -> str:
|
|
||||||
"""Encode bytes using the unpadded base64url format required by JWTs."""
|
|
||||||
|
|
||||||
return base64.urlsafe_b64encode(value).decode("ascii").rstrip("=")
|
|
||||||
|
|
||||||
|
|
||||||
def base64url_decode(value: str) -> bytes:
|
|
||||||
"""Decode a base64url value and reject malformed input."""
|
|
||||||
|
|
||||||
try:
|
|
||||||
return base64.b64decode(value + "=" * (-len(value) % 4), altchars=b"-_", validate=True)
|
|
||||||
except (ValueError, UnicodeEncodeError) as error:
|
|
||||||
raise NotarizationError("Signing service returned an invalid signature") from error
|
|
||||||
|
|
||||||
|
|
||||||
def create_apple_jwt(configuration: NotarizationConfiguration, *, issued_at: int) -> str:
|
|
||||||
"""Create an authentication token for notarization requests."""
|
|
||||||
|
|
||||||
header = {"alg": "ES256", "kid": configuration.apple_key_id, "typ": "JWT"}
|
|
||||||
# Restrict the token to notarization requests.
|
|
||||||
claims = {
|
|
||||||
"iss": configuration.issuer_id,
|
|
||||||
"iat": issued_at,
|
|
||||||
"exp": issued_at + JWT_LIFETIME_SECONDS,
|
|
||||||
"aud": "appstoreconnect-v1",
|
|
||||||
"scope": ["/notary/v2"],
|
|
||||||
}
|
|
||||||
encoded_header = base64url_encode(json.dumps(header, separators=(",", ":")).encode("utf-8"))
|
|
||||||
encoded_claims = base64url_encode(json.dumps(claims, separators=(",", ":")).encode("utf-8"))
|
|
||||||
signing_input = f"{encoded_header}.{encoded_claims}"
|
|
||||||
digest = hashlib.sha256(signing_input.encode("ascii")).digest()
|
|
||||||
key_url = configuration.versioned_key_id
|
|
||||||
request_body = json.dumps({"alg": "ES256", "value": base64url_encode(digest)})
|
|
||||||
|
|
||||||
try:
|
|
||||||
result = subprocess.run(
|
|
||||||
[
|
|
||||||
"az",
|
|
||||||
"rest",
|
|
||||||
"--method",
|
|
||||||
"post",
|
|
||||||
"--url",
|
|
||||||
f"{key_url}/sign?api-version={AZURE_KEYVAULT_API_VERSION}",
|
|
||||||
"--resource",
|
|
||||||
"https://vault.azure.net",
|
|
||||||
"--headers",
|
|
||||||
"Content-Type=application/json",
|
|
||||||
"--body",
|
|
||||||
request_body,
|
|
||||||
"--output",
|
|
||||||
"json",
|
|
||||||
"--only-show-errors",
|
|
||||||
],
|
|
||||||
check=True,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
except (OSError, subprocess.CalledProcessError) as error:
|
|
||||||
detail = getattr(error, "stderr", "").strip()
|
|
||||||
message = "Signing service could not sign the authentication token"
|
|
||||||
if detail:
|
|
||||||
message = f"{message}: {detail}"
|
|
||||||
raise NotarizationError(message) from error
|
|
||||||
|
|
||||||
try:
|
|
||||||
response = json.loads(result.stdout)
|
|
||||||
returned_key_id = response["kid"]
|
|
||||||
encoded_signature = response["value"]
|
|
||||||
except (json.JSONDecodeError, KeyError, TypeError) as error:
|
|
||||||
raise NotarizationError("Signing service returned an invalid response") from error
|
|
||||||
|
|
||||||
# Reject signatures generated with a different key version.
|
|
||||||
if returned_key_id != key_url:
|
|
||||||
raise NotarizationError("Signing service returned an unexpected key version")
|
|
||||||
if not isinstance(encoded_signature, str):
|
|
||||||
raise NotarizationError("Signing service returned an invalid signature")
|
|
||||||
signature = base64url_decode(encoded_signature)
|
|
||||||
# ES256 JWT signatures are 64 raw R || S bytes, not ASN.1/DER-encoded ECDSA signatures.
|
|
||||||
if len(signature) != 64:
|
|
||||||
raise NotarizationError("ES256 signature must contain 64 JOSE R || S bytes")
|
|
||||||
return f"{signing_input}.{base64url_encode(signature)}"
|
|
||||||
|
|
||||||
|
|
||||||
def json_request(url: str, token: str, *, body: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
||||||
"""Send an authenticated JSON request to Apple's notarization service."""
|
|
||||||
|
|
||||||
data = None if body is None else json.dumps(body).encode("utf-8")
|
|
||||||
request = urllib.request.Request(
|
|
||||||
url,
|
|
||||||
data=data,
|
|
||||||
method="GET" if body is None else "POST",
|
|
||||||
headers={
|
|
||||||
"Accept": "application/json",
|
|
||||||
"Authorization": f"Bearer {token}",
|
|
||||||
"Content-Type": "application/json",
|
|
||||||
},
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
with urllib.request.urlopen(request, timeout=60) as response:
|
|
||||||
result = json.load(response)
|
|
||||||
except urllib.error.HTTPError as error:
|
|
||||||
detail = error.read(1024).decode("utf-8", errors="replace")
|
|
||||||
raise NotarizationError(f"Apple Notary API returned HTTP {error.code}: {detail}") from error
|
|
||||||
except (OSError, json.JSONDecodeError) as error:
|
|
||||||
raise NotarizationError("Apple Notary API request failed") from error
|
|
||||||
|
|
||||||
if not isinstance(result, dict):
|
|
||||||
raise NotarizationError("Apple Notary API returned an invalid response")
|
|
||||||
return result
|
|
||||||
|
|
||||||
|
|
||||||
def sha256_file(path: Path) -> str:
|
|
||||||
"""Hash an artifact incrementally so large release files stay off the heap."""
|
|
||||||
|
|
||||||
digest = hashlib.sha256()
|
|
||||||
with path.open("rb") as source:
|
|
||||||
for chunk in iter(lambda: source.read(1024 * 1024), b""):
|
|
||||||
digest.update(chunk)
|
|
||||||
return digest.hexdigest()
|
|
||||||
|
|
||||||
|
|
||||||
def hmac_sha256(key: bytes, value: str) -> bytes:
|
|
||||||
"""Derive one AWS Signature Version 4 signing-key component."""
|
|
||||||
|
|
||||||
return hmac.new(key, value.encode("utf-8"), hashlib.sha256).digest()
|
|
||||||
|
|
||||||
|
|
||||||
def upload_to_apple_s3(
|
|
||||||
path: Path,
|
|
||||||
file_digest: str,
|
|
||||||
credentials: dict[str, Any],
|
|
||||||
*,
|
|
||||||
timestamp: datetime.datetime | None = None,
|
|
||||||
) -> None:
|
|
||||||
"""Upload an artifact using the provided temporary credentials."""
|
|
||||||
|
|
||||||
required = (
|
|
||||||
"awsAccessKeyId",
|
|
||||||
"awsSecretAccessKey",
|
|
||||||
"awsSessionToken",
|
|
||||||
"bucket",
|
|
||||||
"object",
|
|
||||||
)
|
|
||||||
if any(
|
|
||||||
not isinstance(credentials.get(name), str) or not credentials[name] for name in required
|
|
||||||
):
|
|
||||||
raise NotarizationError("Apple returned incomplete temporary upload credentials")
|
|
||||||
|
|
||||||
size = path.stat().st_size
|
|
||||||
if not 0 < size <= MAX_SINGLE_UPLOAD_BYTES:
|
|
||||||
raise NotarizationError("Notarization payload must be between 1 byte and 5 GiB")
|
|
||||||
|
|
||||||
now = timestamp or datetime.datetime.now(datetime.timezone.utc)
|
|
||||||
amz_date = now.strftime("%Y%m%dT%H%M%SZ")
|
|
||||||
date_stamp = now.strftime("%Y%m%d")
|
|
||||||
host = f"{credentials['bucket']}.s3.{AWS_REGION}.amazonaws.com"
|
|
||||||
object_path = "/" + urllib.parse.quote(credentials["object"].lstrip("/"), safe="/-_.~")
|
|
||||||
headers = {
|
|
||||||
"content-type": "application/octet-stream",
|
|
||||||
"host": host,
|
|
||||||
"x-amz-content-sha256": file_digest,
|
|
||||||
"x-amz-date": amz_date,
|
|
||||||
"x-amz-security-token": credentials["awsSessionToken"],
|
|
||||||
}
|
|
||||||
# Build the signature required by the upload endpoint.
|
|
||||||
signed_headers = ";".join(sorted(headers))
|
|
||||||
canonical_headers = "".join(f"{name}:{headers[name]}\n" for name in sorted(headers))
|
|
||||||
canonical_request = "\n".join(
|
|
||||||
["PUT", object_path, "", canonical_headers, signed_headers, file_digest]
|
|
||||||
)
|
|
||||||
credential_scope = f"{date_stamp}/{AWS_REGION}/s3/aws4_request"
|
|
||||||
string_to_sign = "\n".join(
|
|
||||||
[
|
|
||||||
"AWS4-HMAC-SHA256",
|
|
||||||
amz_date,
|
|
||||||
credential_scope,
|
|
||||||
hashlib.sha256(canonical_request.encode("utf-8")).hexdigest(),
|
|
||||||
]
|
|
||||||
)
|
|
||||||
signing_key = ("AWS4" + credentials["awsSecretAccessKey"]).encode("utf-8")
|
|
||||||
for component in (date_stamp, AWS_REGION, "s3", "aws4_request"):
|
|
||||||
signing_key = hmac_sha256(signing_key, component)
|
|
||||||
signature = hmac.new(signing_key, string_to_sign.encode("utf-8"), hashlib.sha256).hexdigest()
|
|
||||||
authorization = (
|
|
||||||
"AWS4-HMAC-SHA256 "
|
|
||||||
f"Credential={credentials['awsAccessKeyId']}/{credential_scope}, "
|
|
||||||
f"SignedHeaders={signed_headers}, Signature={signature}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# Stream the artifact to avoid buffering its full contents.
|
|
||||||
with path.open("rb") as source:
|
|
||||||
request = urllib.request.Request(
|
|
||||||
f"https://{host}{object_path}",
|
|
||||||
data=source,
|
|
||||||
method="PUT",
|
|
||||||
headers={
|
|
||||||
**headers,
|
|
||||||
"Authorization": authorization,
|
|
||||||
"Content-Length": str(size),
|
|
||||||
},
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
with urllib.request.urlopen(request, timeout=300):
|
|
||||||
pass
|
|
||||||
except urllib.error.HTTPError as error:
|
|
||||||
raise NotarizationError(
|
|
||||||
f"Apple notarization payload upload failed with HTTP {error.code}"
|
|
||||||
) from error
|
|
||||||
except OSError as error:
|
|
||||||
raise NotarizationError("Apple notarization payload upload failed") from error
|
|
||||||
|
|
||||||
|
|
||||||
def write_developer_log(submission_id: str, token: str, destination: Path | None) -> None:
|
|
||||||
"""Write the submission's optional diagnostic report."""
|
|
||||||
|
|
||||||
if destination is None:
|
|
||||||
return
|
|
||||||
response = json_request(f"{APPLE_NOTARY_URL}/{submission_id}/logs", token)
|
|
||||||
try:
|
|
||||||
log_url = response["data"]["attributes"]["developerLogUrl"]
|
|
||||||
except (KeyError, TypeError) as error:
|
|
||||||
raise NotarizationError("Apple did not provide a notarization developer log") from error
|
|
||||||
if not isinstance(log_url, str) or not log_url.startswith("https://"):
|
|
||||||
raise NotarizationError("Apple returned an invalid notarization developer log URL")
|
|
||||||
try:
|
|
||||||
# Do not forward unrelated authorization headers to the download URL.
|
|
||||||
with urllib.request.urlopen(log_url, timeout=60) as response:
|
|
||||||
destination.write_bytes(response.read())
|
|
||||||
except urllib.error.HTTPError as error:
|
|
||||||
raise NotarizationError(
|
|
||||||
f"Apple notarization developer log download failed with HTTP {error.code}"
|
|
||||||
) from error
|
|
||||||
except OSError as error:
|
|
||||||
raise NotarizationError("Apple notarization developer log download failed") from error
|
|
||||||
|
|
||||||
|
|
||||||
def notarize(
|
|
||||||
path: Path,
|
|
||||||
configuration: NotarizationConfiguration,
|
|
||||||
*,
|
|
||||||
max_wait_seconds: int,
|
|
||||||
report_log: Path | None,
|
|
||||||
) -> str:
|
|
||||||
"""Submit an artifact, upload it to Apple, and wait for a terminal result."""
|
|
||||||
|
|
||||||
issued_at = int(time.time())
|
|
||||||
token = create_apple_jwt(configuration, issued_at=issued_at)
|
|
||||||
file_digest = sha256_file(path)
|
|
||||||
# Create the submission before uploading the artifact.
|
|
||||||
response = json_request(
|
|
||||||
APPLE_NOTARY_URL,
|
|
||||||
token,
|
|
||||||
body={"sha256": file_digest, "submissionName": path.name},
|
|
||||||
)
|
|
||||||
try:
|
|
||||||
submission_id = response["data"]["id"]
|
|
||||||
upload_credentials = response["data"]["attributes"]
|
|
||||||
except (KeyError, TypeError) as error:
|
|
||||||
raise NotarizationError("Apple returned an invalid notarization submission") from error
|
|
||||||
if not isinstance(submission_id, str) or not submission_id:
|
|
||||||
raise NotarizationError("Apple did not return a notarization submission ID")
|
|
||||||
if not isinstance(upload_credentials, dict):
|
|
||||||
raise NotarizationError("Apple returned invalid notarization upload credentials")
|
|
||||||
|
|
||||||
print(f"Uploading notarization submission {submission_id} for {path.name}")
|
|
||||||
upload_to_apple_s3(path, file_digest, upload_credentials)
|
|
||||||
# A monotonic deadline keeps the wait limit stable if the system clock changes.
|
|
||||||
deadline = time.monotonic() + max_wait_seconds
|
|
||||||
|
|
||||||
# Wait for completion, equivalent to `notarytool submit --wait`.
|
|
||||||
while True:
|
|
||||||
# Refresh the token before it expires.
|
|
||||||
if int(time.time()) >= issued_at + JWT_LIFETIME_SECONDS - 60:
|
|
||||||
issued_at = int(time.time())
|
|
||||||
token = create_apple_jwt(configuration, issued_at=issued_at)
|
|
||||||
result = json_request(f"{APPLE_NOTARY_URL}/{submission_id}", token)
|
|
||||||
try:
|
|
||||||
status = result["data"]["attributes"]["status"]
|
|
||||||
except (KeyError, TypeError) as error:
|
|
||||||
raise NotarizationError("Apple returned an invalid notarization status") from error
|
|
||||||
|
|
||||||
if status in {"Accepted", "Invalid", "Rejected"}:
|
|
||||||
# Save diagnostics for both accepted and rejected submissions.
|
|
||||||
write_developer_log(submission_id, token, report_log)
|
|
||||||
message = f"Notarization submission {submission_id} completed with status {status}"
|
|
||||||
if status != "Accepted":
|
|
||||||
raise NotarizationError(message)
|
|
||||||
print(message)
|
|
||||||
return submission_id
|
|
||||||
if status != "In Progress":
|
|
||||||
raise NotarizationError(f"Unexpected notarization status: {status!r}")
|
|
||||||
|
|
||||||
remaining = deadline - time.monotonic()
|
|
||||||
if remaining <= 0:
|
|
||||||
raise NotarizationError(
|
|
||||||
f"Notarization submission {submission_id} exceeded {max_wait_seconds} seconds"
|
|
||||||
)
|
|
||||||
print(f"Notarization submission {submission_id} is still in progress")
|
|
||||||
time.sleep(min(POLL_INTERVAL_SECONDS, remaining))
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
"""Run notarization using command-line arguments."""
|
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
|
||||||
parser.add_argument("--file", type=Path, required=True)
|
|
||||||
parser.add_argument("--report-log", type=Path)
|
|
||||||
parser.add_argument("--max-wait-seconds", type=int, default=600)
|
|
||||||
arguments = parser.parse_args()
|
|
||||||
|
|
||||||
try:
|
|
||||||
if not arguments.file.is_file():
|
|
||||||
raise NotarizationError(f"Notarization payload does not exist: {arguments.file}")
|
|
||||||
if arguments.max_wait_seconds < 0:
|
|
||||||
raise NotarizationError("--max-wait-seconds must be non-negative")
|
|
||||||
if arguments.report_log is not None:
|
|
||||||
arguments.report_log.parent.mkdir(parents=True, exist_ok=True)
|
|
||||||
notarize(
|
|
||||||
arguments.file,
|
|
||||||
NotarizationConfiguration.from_environment(),
|
|
||||||
max_wait_seconds=arguments.max_wait_seconds,
|
|
||||||
report_log=arguments.report_log,
|
|
||||||
)
|
|
||||||
except NotarizationError as error:
|
|
||||||
print(f"Notarization failed: {error}", file=sys.stderr)
|
|
||||||
return 1
|
|
||||||
return 0
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
raise SystemExit(main())
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
#!/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
|
|
||||||
@@ -1,202 +0,0 @@
|
|||||||
import contextlib
|
|
||||||
import hashlib
|
|
||||||
import io
|
|
||||||
import json
|
|
||||||
import os
|
|
||||||
import subprocess
|
|
||||||
import sys
|
|
||||||
import tempfile
|
|
||||||
import unittest
|
|
||||||
from pathlib import Path
|
|
||||||
from unittest.mock import patch
|
|
||||||
|
|
||||||
SIGNING_DIRECTORY = Path(__file__).resolve().parent
|
|
||||||
sys.path.insert(0, str(SIGNING_DIRECTORY))
|
|
||||||
import notarize_with_akv as notary # noqa: E402
|
|
||||||
|
|
||||||
CONFIGURATION = notary.NotarizationConfiguration(
|
|
||||||
"issuer-id",
|
|
||||||
"APPLEKEY01",
|
|
||||||
"notary-vault",
|
|
||||||
"example-signing-key",
|
|
||||||
"0123456789abcdef" * 2,
|
|
||||||
)
|
|
||||||
ENVIRONMENT = {
|
|
||||||
"APPLE_NOTARIZATION_ISSUER_ID": CONFIGURATION.issuer_id,
|
|
||||||
"AZURE_KEYVAULT_NAME": CONFIGURATION.vault_name,
|
|
||||||
"APPLE_NOTARIZATION_AKV_KEY_NAME": CONFIGURATION.vault_key_name,
|
|
||||||
}
|
|
||||||
CREDENTIALS = {
|
|
||||||
"awsAccessKeyId": "temporary-access-key",
|
|
||||||
"awsSecretAccessKey": "temporary-secret-key",
|
|
||||||
"awsSessionToken": "temporary-session-token",
|
|
||||||
"bucket": "apple-notary-bucket",
|
|
||||||
"object": "uploads/release artifact.dmg",
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def response(payload):
|
|
||||||
return io.BytesIO(json.dumps(payload).encode() if isinstance(payload, dict) else payload)
|
|
||||||
|
|
||||||
|
|
||||||
def azure_response(payload):
|
|
||||||
return subprocess.CompletedProcess([], 0, stdout=json.dumps(payload), stderr="")
|
|
||||||
|
|
||||||
|
|
||||||
class NotarizationTest(unittest.TestCase):
|
|
||||||
def test_validates_the_apple_key_tag_and_pins_its_version(self) -> None:
|
|
||||||
metadata = {"id": CONFIGURATION.versioned_key_id, "apple_key_id": "APPLEKEY01"}
|
|
||||||
with (
|
|
||||||
patch.dict(os.environ, ENVIRONMENT, clear=True),
|
|
||||||
patch.object(
|
|
||||||
notary.subprocess,
|
|
||||||
"run",
|
|
||||||
return_value=azure_response(metadata),
|
|
||||||
) as show,
|
|
||||||
):
|
|
||||||
self.assertEqual(
|
|
||||||
notary.NotarizationConfiguration.from_environment(),
|
|
||||||
CONFIGURATION,
|
|
||||||
)
|
|
||||||
show.return_value = azure_response({**metadata, "apple_key_id": None})
|
|
||||||
with self.assertRaisesRegex(notary.NotarizationError, "apple-key-id tag"):
|
|
||||||
notary.NotarizationConfiguration.from_environment()
|
|
||||||
self.assertIn('{id:key.kid,apple_key_id:tags."apple-key-id"}', show.call_args.args[0])
|
|
||||||
|
|
||||||
invalid_environment = {**ENVIRONMENT, "APPLE_NOTARIZATION_AKV_KEY_NAME": "../bad"}
|
|
||||||
with (
|
|
||||||
patch.dict(os.environ, invalid_environment, clear=True),
|
|
||||||
self.assertRaisesRegex(notary.NotarizationError, "key name"),
|
|
||||||
):
|
|
||||||
notary.NotarizationConfiguration.from_environment()
|
|
||||||
|
|
||||||
def test_signs_the_notary_scoped_digest_with_the_pinned_key(self) -> None:
|
|
||||||
signature = bytes(range(64))
|
|
||||||
payload = {
|
|
||||||
"kid": CONFIGURATION.versioned_key_id,
|
|
||||||
"value": notary.base64url_encode(signature),
|
|
||||||
}
|
|
||||||
with patch.object(notary.subprocess, "run", return_value=azure_response(payload)) as sign:
|
|
||||||
token = notary.create_apple_jwt(CONFIGURATION, issued_at=1_780_000_000)
|
|
||||||
header, claims, encoded_signature = token.split(".")
|
|
||||||
self.assertEqual(json.loads(notary.base64url_decode(header))["kid"], "APPLEKEY01")
|
|
||||||
self.assertEqual(
|
|
||||||
json.loads(notary.base64url_decode(claims))["scope"],
|
|
||||||
["/notary/v2"],
|
|
||||||
)
|
|
||||||
self.assertEqual(notary.base64url_decode(encoded_signature), signature)
|
|
||||||
command = sign.call_args.args[0]
|
|
||||||
body = json.loads(command[command.index("--body") + 1])
|
|
||||||
self.assertEqual(body["alg"], "ES256")
|
|
||||||
self.assertEqual(
|
|
||||||
notary.base64url_decode(body["value"]),
|
|
||||||
hashlib.sha256(f"{header}.{claims}".encode()).digest(),
|
|
||||||
)
|
|
||||||
|
|
||||||
def test_rejects_wrong_key_versions_and_invalid_signatures(self) -> None:
|
|
||||||
cases = (
|
|
||||||
(CONFIGURATION.versioned_key_id + "wrong", bytes(64), "unexpected key version"),
|
|
||||||
(CONFIGURATION.versioned_key_id, b"short", "64 JOSE"),
|
|
||||||
)
|
|
||||||
for key_id, signature, message in cases:
|
|
||||||
payload = {"kid": key_id, "value": notary.base64url_encode(signature)}
|
|
||||||
with (
|
|
||||||
self.subTest(message=message),
|
|
||||||
patch.object(
|
|
||||||
notary.subprocess,
|
|
||||||
"run",
|
|
||||||
return_value=azure_response(payload),
|
|
||||||
),
|
|
||||||
self.assertRaisesRegex(notary.NotarizationError, message),
|
|
||||||
):
|
|
||||||
notary.create_apple_jwt(CONFIGURATION, issued_at=1)
|
|
||||||
|
|
||||||
def test_uploads_polls_and_retains_sanitized_apple_diagnostics(self) -> None:
|
|
||||||
signature = azure_response(
|
|
||||||
{
|
|
||||||
"kid": CONFIGURATION.versioned_key_id,
|
|
||||||
"value": notary.base64url_encode(bytes(64)),
|
|
||||||
}
|
|
||||||
)
|
|
||||||
responses = [
|
|
||||||
response({"data": {"id": "submission-1", "attributes": CREDENTIALS}}),
|
|
||||||
response(b""),
|
|
||||||
response({"data": {"attributes": {"status": "In Progress"}}}),
|
|
||||||
response({"data": {"attributes": {"status": "Accepted"}}}),
|
|
||||||
response({"data": {"attributes": {"developerLogUrl": "https://logs.example.com/log"}}}),
|
|
||||||
response(b'{"status":"Accepted"}'),
|
|
||||||
]
|
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
|
||||||
artifact, log = (
|
|
||||||
Path(directory) / "codex.zip",
|
|
||||||
Path(directory) / "notary-log.json",
|
|
||||||
)
|
|
||||||
artifact.write_bytes(b"signed release binary")
|
|
||||||
output = io.StringIO()
|
|
||||||
with (
|
|
||||||
patch.object(notary.subprocess, "run", return_value=signature),
|
|
||||||
patch.object(notary.urllib.request, "urlopen", side_effect=responses) as requests,
|
|
||||||
patch.object(notary.time, "sleep") as sleep,
|
|
||||||
contextlib.redirect_stdout(output),
|
|
||||||
):
|
|
||||||
submission = notary.notarize(
|
|
||||||
artifact, CONFIGURATION, max_wait_seconds=600, report_log=log
|
|
||||||
)
|
|
||||||
self.assertEqual(submission, "submission-1")
|
|
||||||
self.assertEqual(json.loads(log.read_text()), {"status": "Accepted"})
|
|
||||||
sleep.assert_called_once_with(10)
|
|
||||||
upload = requests.call_args_list[1].args[0]
|
|
||||||
self.assertEqual(upload.get_method(), "PUT")
|
|
||||||
self.assertNotIn("temporary-secret-key", upload.get_header("Authorization"))
|
|
||||||
self.assertNotIn("temporary-secret-key", output.getvalue())
|
|
||||||
|
|
||||||
|
|
||||||
class NotarizationWrapperTest(unittest.TestCase):
|
|
||||||
def test_binary_and_dmg_wrappers_preserve_notarization_contracts(
|
|
||||||
self,
|
|
||||||
) -> None:
|
|
||||||
with tempfile.TemporaryDirectory() as directory:
|
|
||||||
root, tools = Path(directory), Path(directory) / "tools"
|
|
||||||
tools.mkdir()
|
|
||||||
call_log = root / "calls.txt"
|
|
||||||
environment = {
|
|
||||||
**os.environ,
|
|
||||||
**ENVIRONMENT,
|
|
||||||
"PATH": f"{tools}:{os.environ['PATH']}",
|
|
||||||
"RUNNER_TEMP": str(root),
|
|
||||||
"CALL_LOG": str(call_log),
|
|
||||||
}
|
|
||||||
for name in ("az", "python3", "rcodesign"):
|
|
||||||
body = "exit 0" if name == "az" else f'printf "{name} %s\\n" "$*" >> "$CALL_LOG"'
|
|
||||||
executable = tools / name
|
|
||||||
executable.write_text(f"#!/usr/bin/env bash\nset -euo pipefail\n{body}\n")
|
|
||||||
executable.chmod(0o755)
|
|
||||||
for kind in ("binary", "dmg"):
|
|
||||||
with self.subTest(kind=kind):
|
|
||||||
artifact = root / ("codex.dmg" if kind == "dmg" else "codex")
|
|
||||||
artifact.write_bytes(b"signed release artifact")
|
|
||||||
subprocess.run(
|
|
||||||
[
|
|
||||||
str(SIGNING_DIRECTORY / f"notarize_macos_{kind}_with_akv.sh"),
|
|
||||||
f"--{kind}",
|
|
||||||
str(artifact),
|
|
||||||
"--report-dir",
|
|
||||||
str(root / "report"),
|
|
||||||
],
|
|
||||||
env=environment,
|
|
||||||
check=True,
|
|
||||||
capture_output=True,
|
|
||||||
text=True,
|
|
||||||
)
|
|
||||||
calls = call_log.read_text().splitlines()
|
|
||||||
self.assertIn("notarize_with_akv.py --file", calls[0])
|
|
||||||
if kind == "dmg":
|
|
||||||
self.assertEqual(calls[1], f"rcodesign staple {artifact}")
|
|
||||||
else:
|
|
||||||
self.assertEqual(len(calls), 1)
|
|
||||||
self.assertEqual(list(root.rglob("*.p8")), [])
|
|
||||||
call_log.unlink()
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
||||||
@@ -1,391 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
"""Verify that codex-rs Cargo manifests follow workspace manifest policy.
|
|
||||||
|
|
||||||
Checks:
|
|
||||||
- Crates inherit `[workspace.package]` metadata.
|
|
||||||
- Crates opt into `[lints] workspace = true`.
|
|
||||||
- Crate names follow the codex-rs directory naming conventions.
|
|
||||||
- Workspace manifests do not introduce workspace crate feature toggles.
|
|
||||||
"""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import sys
|
|
||||||
import tomllib
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
|
||||||
CARGO_RS_ROOT = ROOT / "codex-rs"
|
|
||||||
WORKSPACE_PACKAGE_FIELDS = ("version", "edition", "license")
|
|
||||||
TOP_LEVEL_NAME_EXCEPTIONS = {
|
|
||||||
"windows-sandbox-rs": "codex-windows-sandbox",
|
|
||||||
}
|
|
||||||
UTILITY_NAME_EXCEPTIONS = {
|
|
||||||
"path-utils": "codex-utils-path",
|
|
||||||
}
|
|
||||||
MANIFEST_FEATURE_EXCEPTIONS = {
|
|
||||||
"codex-rs/code-mode/Cargo.toml": {"sandbox": ("v8/v8_enable_sandbox",)},
|
|
||||||
"codex-rs/v8-poc/Cargo.toml": {"sandbox": ("v8/v8_enable_sandbox",)},
|
|
||||||
}
|
|
||||||
OPTIONAL_DEPENDENCY_EXCEPTIONS = set()
|
|
||||||
INTERNAL_DEPENDENCY_FEATURE_EXCEPTIONS = {}
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
internal_package_names = workspace_package_names()
|
|
||||||
used_manifest_feature_exceptions: set[str] = set()
|
|
||||||
used_optional_dependency_exceptions: set[tuple[str, str, str]] = set()
|
|
||||||
used_internal_dependency_feature_exceptions: set[tuple[str, str, str]] = set()
|
|
||||||
failures_by_path: dict[str, list[str]] = {}
|
|
||||||
|
|
||||||
for path in manifests_to_verify():
|
|
||||||
if errors := manifest_errors(
|
|
||||||
path,
|
|
||||||
internal_package_names,
|
|
||||||
used_manifest_feature_exceptions,
|
|
||||||
used_optional_dependency_exceptions,
|
|
||||||
used_internal_dependency_feature_exceptions,
|
|
||||||
):
|
|
||||||
failures_by_path[manifest_key(path)] = errors
|
|
||||||
|
|
||||||
add_unused_exception_errors(
|
|
||||||
failures_by_path,
|
|
||||||
used_manifest_feature_exceptions,
|
|
||||||
used_optional_dependency_exceptions,
|
|
||||||
used_internal_dependency_feature_exceptions,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not failures_by_path:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
print(
|
|
||||||
"Cargo manifests under codex-rs must inherit workspace package metadata, "
|
|
||||||
"opt into workspace lints, and avoid introducing new workspace crate "
|
|
||||||
"features."
|
|
||||||
)
|
|
||||||
print(
|
|
||||||
"Workspace crate features are disallowed because our Bazel build setup "
|
|
||||||
"does not honor them today, which can let issues hidden behind feature "
|
|
||||||
"gates go unnoticed, and because they add extra crate build "
|
|
||||||
"permutations we want to avoid."
|
|
||||||
)
|
|
||||||
print(
|
|
||||||
"Cargo only applies `codex-rs/Cargo.toml` `[workspace.lints.clippy]` "
|
|
||||||
"entries to a crate when that crate declares:"
|
|
||||||
)
|
|
||||||
print()
|
|
||||||
print("[lints]")
|
|
||||||
print("workspace = true")
|
|
||||||
print()
|
|
||||||
print(
|
|
||||||
"Without that opt-in, `cargo clippy` can miss violations that Bazel clippy "
|
|
||||||
"catches."
|
|
||||||
)
|
|
||||||
print()
|
|
||||||
print(
|
|
||||||
"Package-name checks apply to `codex-rs/<crate>/Cargo.toml` and "
|
|
||||||
"`codex-rs/utils/<crate>/Cargo.toml`."
|
|
||||||
)
|
|
||||||
print(
|
|
||||||
"Workspace crate features are forbidden; add a targeted exception here "
|
|
||||||
"only if there is a deliberate temporary migration in flight."
|
|
||||||
)
|
|
||||||
print()
|
|
||||||
for path in sorted(failures_by_path):
|
|
||||||
errors = failures_by_path[path]
|
|
||||||
print(f"{path}:")
|
|
||||||
for error in errors:
|
|
||||||
print(f" - {error}")
|
|
||||||
|
|
||||||
return 1
|
|
||||||
|
|
||||||
|
|
||||||
def manifest_errors(
|
|
||||||
path: Path,
|
|
||||||
internal_package_names: set[str],
|
|
||||||
used_manifest_feature_exceptions: set[str],
|
|
||||||
used_optional_dependency_exceptions: set[tuple[str, str, str]],
|
|
||||||
used_internal_dependency_feature_exceptions: set[tuple[str, str, str]],
|
|
||||||
) -> list[str]:
|
|
||||||
manifest = load_manifest(path)
|
|
||||||
package = manifest.get("package")
|
|
||||||
if not isinstance(package, dict) and path != CARGO_RS_ROOT / "Cargo.toml":
|
|
||||||
return []
|
|
||||||
|
|
||||||
errors = []
|
|
||||||
if isinstance(package, dict):
|
|
||||||
for field in WORKSPACE_PACKAGE_FIELDS:
|
|
||||||
if not is_workspace_reference(package.get(field)):
|
|
||||||
errors.append(f"set `{field}.workspace = true` in `[package]`")
|
|
||||||
|
|
||||||
lints = manifest.get("lints")
|
|
||||||
if not (isinstance(lints, dict) and lints.get("workspace") is True):
|
|
||||||
errors.append("add `[lints]` with `workspace = true`")
|
|
||||||
|
|
||||||
expected_name = expected_package_name(path)
|
|
||||||
if expected_name is not None:
|
|
||||||
actual_name = package.get("name")
|
|
||||||
if actual_name != expected_name:
|
|
||||||
errors.append(
|
|
||||||
f"set `[package].name` to `{expected_name}` (found `{actual_name}`)"
|
|
||||||
)
|
|
||||||
|
|
||||||
path_key = manifest_key(path)
|
|
||||||
features = manifest.get("features")
|
|
||||||
if features is not None:
|
|
||||||
normalized_features = normalize_feature_mapping(features)
|
|
||||||
expected_features = MANIFEST_FEATURE_EXCEPTIONS.get(path_key)
|
|
||||||
if expected_features is None:
|
|
||||||
errors.append(
|
|
||||||
"remove `[features]`; new workspace crate features are not allowed"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
used_manifest_feature_exceptions.add(path_key)
|
|
||||||
if normalized_features != expected_features:
|
|
||||||
errors.append(
|
|
||||||
"limit `[features]` to the existing exception list while "
|
|
||||||
"workspace crate features are being removed "
|
|
||||||
f"(expected {render_feature_mapping(expected_features)})"
|
|
||||||
)
|
|
||||||
|
|
||||||
for section_name, dependencies in dependency_sections(manifest):
|
|
||||||
for dependency_name, dependency in dependencies.items():
|
|
||||||
if not isinstance(dependency, dict):
|
|
||||||
continue
|
|
||||||
|
|
||||||
if dependency.get("optional") is True:
|
|
||||||
exception_key = (path_key, section_name, dependency_name)
|
|
||||||
if exception_key in OPTIONAL_DEPENDENCY_EXCEPTIONS:
|
|
||||||
used_optional_dependency_exceptions.add(exception_key)
|
|
||||||
else:
|
|
||||||
errors.append(
|
|
||||||
"remove `optional = true` from "
|
|
||||||
f"`{dependency_entry_label(section_name, dependency_name)}`; "
|
|
||||||
"new optional dependencies are not allowed because they "
|
|
||||||
"create crate features"
|
|
||||||
)
|
|
||||||
|
|
||||||
if not is_internal_dependency(path, dependency_name, dependency, internal_package_names):
|
|
||||||
continue
|
|
||||||
|
|
||||||
dependency_features = dependency.get("features")
|
|
||||||
if dependency_features is not None:
|
|
||||||
normalized_dependency_features = normalize_string_list(
|
|
||||||
dependency_features
|
|
||||||
)
|
|
||||||
exception_key = (path_key, section_name, dependency_name)
|
|
||||||
expected_dependency_features = (
|
|
||||||
INTERNAL_DEPENDENCY_FEATURE_EXCEPTIONS.get(exception_key)
|
|
||||||
)
|
|
||||||
if expected_dependency_features is None:
|
|
||||||
errors.append(
|
|
||||||
"remove `features = [...]` from workspace dependency "
|
|
||||||
f"`{dependency_entry_label(section_name, dependency_name)}`; "
|
|
||||||
"new workspace crate feature activations are not allowed"
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
used_internal_dependency_feature_exceptions.add(exception_key)
|
|
||||||
if normalized_dependency_features != expected_dependency_features:
|
|
||||||
errors.append(
|
|
||||||
"limit workspace dependency features on "
|
|
||||||
f"`{dependency_entry_label(section_name, dependency_name)}` "
|
|
||||||
"to the existing exception list while workspace crate "
|
|
||||||
"features are being removed "
|
|
||||||
f"(expected {render_string_list(expected_dependency_features)})"
|
|
||||||
)
|
|
||||||
|
|
||||||
if dependency.get("default-features") is False:
|
|
||||||
errors.append(
|
|
||||||
"remove `default-features = false` from workspace dependency "
|
|
||||||
f"`{dependency_entry_label(section_name, dependency_name)}`; "
|
|
||||||
"new workspace crate feature toggles are not allowed"
|
|
||||||
)
|
|
||||||
|
|
||||||
return errors
|
|
||||||
|
|
||||||
|
|
||||||
def expected_package_name(path: Path) -> str | None:
|
|
||||||
parts = path.relative_to(CARGO_RS_ROOT).parts
|
|
||||||
if len(parts) == 2 and parts[1] == "Cargo.toml":
|
|
||||||
directory = parts[0]
|
|
||||||
return TOP_LEVEL_NAME_EXCEPTIONS.get(
|
|
||||||
directory,
|
|
||||||
directory if directory.startswith("codex-") else f"codex-{directory}",
|
|
||||||
)
|
|
||||||
if len(parts) == 3 and parts[0] == "utils" and parts[2] == "Cargo.toml":
|
|
||||||
directory = parts[1]
|
|
||||||
return UTILITY_NAME_EXCEPTIONS.get(directory, f"codex-utils-{directory}")
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def is_workspace_reference(value: object) -> bool:
|
|
||||||
return isinstance(value, dict) and value.get("workspace") is True
|
|
||||||
|
|
||||||
|
|
||||||
def manifest_key(path: Path) -> str:
|
|
||||||
return str(path.relative_to(ROOT))
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_feature_mapping(value: object) -> dict[str, tuple[str, ...]] | None:
|
|
||||||
if not isinstance(value, dict):
|
|
||||||
return None
|
|
||||||
|
|
||||||
normalized = {}
|
|
||||||
for key, features in value.items():
|
|
||||||
if not isinstance(key, str):
|
|
||||||
return None
|
|
||||||
normalized_features = normalize_string_list(features)
|
|
||||||
if normalized_features is None:
|
|
||||||
return None
|
|
||||||
normalized[key] = normalized_features
|
|
||||||
return normalized
|
|
||||||
|
|
||||||
|
|
||||||
def normalize_string_list(value: object) -> tuple[str, ...] | None:
|
|
||||||
if not isinstance(value, list) or not all(isinstance(item, str) for item in value):
|
|
||||||
return None
|
|
||||||
return tuple(value)
|
|
||||||
|
|
||||||
|
|
||||||
def render_feature_mapping(features: dict[str, tuple[str, ...]]) -> str:
|
|
||||||
entries = [
|
|
||||||
f"{name} = {render_string_list(items)}" for name, items in features.items()
|
|
||||||
]
|
|
||||||
return ", ".join(entries)
|
|
||||||
|
|
||||||
|
|
||||||
def render_string_list(items: tuple[str, ...]) -> str:
|
|
||||||
return "[" + ", ".join(f'"{item}"' for item in items) + "]"
|
|
||||||
|
|
||||||
|
|
||||||
def dependency_sections(manifest: dict) -> list[tuple[str, dict]]:
|
|
||||||
sections = []
|
|
||||||
for section_name in ("dependencies", "dev-dependencies", "build-dependencies"):
|
|
||||||
dependencies = manifest.get(section_name)
|
|
||||||
if isinstance(dependencies, dict):
|
|
||||||
sections.append((section_name, dependencies))
|
|
||||||
|
|
||||||
workspace = manifest.get("workspace")
|
|
||||||
if isinstance(workspace, dict):
|
|
||||||
workspace_dependencies = workspace.get("dependencies")
|
|
||||||
if isinstance(workspace_dependencies, dict):
|
|
||||||
sections.append(("workspace.dependencies", workspace_dependencies))
|
|
||||||
|
|
||||||
target = manifest.get("target")
|
|
||||||
if not isinstance(target, dict):
|
|
||||||
return sections
|
|
||||||
|
|
||||||
for target_name, tables in target.items():
|
|
||||||
if not isinstance(tables, dict):
|
|
||||||
continue
|
|
||||||
for section_name in ("dependencies", "dev-dependencies", "build-dependencies"):
|
|
||||||
dependencies = tables.get(section_name)
|
|
||||||
if isinstance(dependencies, dict):
|
|
||||||
sections.append((f"target.{target_name}.{section_name}", dependencies))
|
|
||||||
|
|
||||||
return sections
|
|
||||||
|
|
||||||
|
|
||||||
def dependency_entry_label(section_name: str, dependency_name: str) -> str:
|
|
||||||
return f"[{section_name}].{dependency_name}"
|
|
||||||
|
|
||||||
|
|
||||||
def is_internal_dependency(
|
|
||||||
manifest_path: Path,
|
|
||||||
dependency_name: str,
|
|
||||||
dependency: dict,
|
|
||||||
internal_package_names: set[str],
|
|
||||||
) -> bool:
|
|
||||||
package_name = dependency.get("package", dependency_name)
|
|
||||||
if isinstance(package_name, str) and package_name in internal_package_names:
|
|
||||||
return True
|
|
||||||
|
|
||||||
dependency_path = dependency.get("path")
|
|
||||||
if not isinstance(dependency_path, str):
|
|
||||||
return False
|
|
||||||
|
|
||||||
resolved_dependency_path = (manifest_path.parent / dependency_path).resolve()
|
|
||||||
try:
|
|
||||||
resolved_dependency_path.relative_to(CARGO_RS_ROOT)
|
|
||||||
except ValueError:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
|
|
||||||
|
|
||||||
def add_unused_exception_errors(
|
|
||||||
failures_by_path: dict[str, list[str]],
|
|
||||||
used_manifest_feature_exceptions: set[str],
|
|
||||||
used_optional_dependency_exceptions: set[tuple[str, str, str]],
|
|
||||||
used_internal_dependency_feature_exceptions: set[tuple[str, str, str]],
|
|
||||||
) -> None:
|
|
||||||
for path_key in sorted(
|
|
||||||
set(MANIFEST_FEATURE_EXCEPTIONS) - used_manifest_feature_exceptions
|
|
||||||
):
|
|
||||||
add_failure(
|
|
||||||
failures_by_path,
|
|
||||||
path_key,
|
|
||||||
"remove the stale `[features]` exception from "
|
|
||||||
"`MANIFEST_FEATURE_EXCEPTIONS`",
|
|
||||||
)
|
|
||||||
|
|
||||||
for path_key, section_name, dependency_name in sorted(
|
|
||||||
OPTIONAL_DEPENDENCY_EXCEPTIONS - used_optional_dependency_exceptions
|
|
||||||
):
|
|
||||||
add_failure(
|
|
||||||
failures_by_path,
|
|
||||||
path_key,
|
|
||||||
"remove the stale optional-dependency exception for "
|
|
||||||
f"`{dependency_entry_label(section_name, dependency_name)}` from "
|
|
||||||
"`OPTIONAL_DEPENDENCY_EXCEPTIONS`",
|
|
||||||
)
|
|
||||||
|
|
||||||
for path_key, section_name, dependency_name in sorted(
|
|
||||||
set(INTERNAL_DEPENDENCY_FEATURE_EXCEPTIONS)
|
|
||||||
- used_internal_dependency_feature_exceptions
|
|
||||||
):
|
|
||||||
add_failure(
|
|
||||||
failures_by_path,
|
|
||||||
path_key,
|
|
||||||
"remove the stale internal dependency feature exception for "
|
|
||||||
f"`{dependency_entry_label(section_name, dependency_name)}` from "
|
|
||||||
"`INTERNAL_DEPENDENCY_FEATURE_EXCEPTIONS`",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def add_failure(failures_by_path: dict[str, list[str]], path_key: str, error: str) -> None:
|
|
||||||
failures_by_path.setdefault(path_key, []).append(error)
|
|
||||||
|
|
||||||
|
|
||||||
def workspace_package_names() -> set[str]:
|
|
||||||
package_names = set()
|
|
||||||
for path in cargo_manifests():
|
|
||||||
manifest = load_manifest(path)
|
|
||||||
package = manifest.get("package")
|
|
||||||
if not isinstance(package, dict):
|
|
||||||
continue
|
|
||||||
package_name = package.get("name")
|
|
||||||
if isinstance(package_name, str):
|
|
||||||
package_names.add(package_name)
|
|
||||||
return package_names
|
|
||||||
|
|
||||||
|
|
||||||
def load_manifest(path: Path) -> dict:
|
|
||||||
return tomllib.loads(path.read_text())
|
|
||||||
|
|
||||||
|
|
||||||
def cargo_manifests() -> list[Path]:
|
|
||||||
return sorted(
|
|
||||||
path
|
|
||||||
for path in CARGO_RS_ROOT.rglob("Cargo.toml")
|
|
||||||
if path != CARGO_RS_ROOT / "Cargo.toml"
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def manifests_to_verify() -> list[Path]:
|
|
||||||
return [CARGO_RS_ROOT / "Cargo.toml", *cargo_manifests()]
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
|
|
||||||
"""Verify codex-tui does not depend on or import codex-core directly."""
|
|
||||||
|
|
||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import re
|
|
||||||
import sys
|
|
||||||
import tomllib
|
|
||||||
from pathlib import Path
|
|
||||||
|
|
||||||
|
|
||||||
ROOT = Path(__file__).resolve().parents[2]
|
|
||||||
TUI_ROOT = ROOT / "codex-rs" / "tui"
|
|
||||||
TUI_MANIFEST = TUI_ROOT / "Cargo.toml"
|
|
||||||
FORBIDDEN_PACKAGE = "codex-core"
|
|
||||||
FORBIDDEN_SOURCE_PATTERNS = (
|
|
||||||
re.compile(r"\bcodex_core::"),
|
|
||||||
re.compile(r"\buse\s+codex_core\b"),
|
|
||||||
re.compile(r"\bextern\s+crate\s+codex_core\b"),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def main() -> int:
|
|
||||||
failures = []
|
|
||||||
failures.extend(manifest_failures())
|
|
||||||
failures.extend(source_failures())
|
|
||||||
|
|
||||||
if not failures:
|
|
||||||
return 0
|
|
||||||
|
|
||||||
print("codex-tui must not depend on or import codex-core directly.")
|
|
||||||
print(
|
|
||||||
"Use the app-server protocol/client boundary instead; temporary embedded "
|
|
||||||
"startup gaps belong behind codex_app_server_client::legacy_core."
|
|
||||||
)
|
|
||||||
print()
|
|
||||||
for failure in failures:
|
|
||||||
print(f"- {failure}")
|
|
||||||
|
|
||||||
return 1
|
|
||||||
|
|
||||||
|
|
||||||
def manifest_failures() -> list[str]:
|
|
||||||
manifest = tomllib.loads(TUI_MANIFEST.read_text())
|
|
||||||
failures = []
|
|
||||||
for section_name, dependencies in dependency_sections(manifest):
|
|
||||||
if FORBIDDEN_PACKAGE in dependencies:
|
|
||||||
failures.append(
|
|
||||||
f"{relative_path(TUI_MANIFEST)} declares `{FORBIDDEN_PACKAGE}` "
|
|
||||||
f"in `[{section_name}]`"
|
|
||||||
)
|
|
||||||
return failures
|
|
||||||
|
|
||||||
|
|
||||||
def dependency_sections(manifest: dict) -> list[tuple[str, dict]]:
|
|
||||||
sections: list[tuple[str, dict]] = []
|
|
||||||
for section_name in ("dependencies", "dev-dependencies", "build-dependencies"):
|
|
||||||
dependencies = manifest.get(section_name)
|
|
||||||
if isinstance(dependencies, dict):
|
|
||||||
sections.append((section_name, dependencies))
|
|
||||||
|
|
||||||
for target_name, target in manifest.get("target", {}).items():
|
|
||||||
if not isinstance(target, dict):
|
|
||||||
continue
|
|
||||||
for section_name in ("dependencies", "dev-dependencies", "build-dependencies"):
|
|
||||||
dependencies = target.get(section_name)
|
|
||||||
if isinstance(dependencies, dict):
|
|
||||||
sections.append((f'target.{target_name}.{section_name}', dependencies))
|
|
||||||
|
|
||||||
return sections
|
|
||||||
|
|
||||||
|
|
||||||
def source_failures() -> list[str]:
|
|
||||||
failures = []
|
|
||||||
for path in sorted(TUI_ROOT.glob("**/*.rs")):
|
|
||||||
text = path.read_text()
|
|
||||||
for line_number, line in enumerate(text.splitlines(), start=1):
|
|
||||||
if any(pattern.search(line) for pattern in FORBIDDEN_SOURCE_PATTERNS):
|
|
||||||
failures.append(f"{relative_path(path)}:{line_number} imports `codex_core`")
|
|
||||||
return failures
|
|
||||||
|
|
||||||
|
|
||||||
def relative_path(path: Path) -> str:
|
|
||||||
return str(path.relative_to(ROOT))
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
Reference in New Issue
Block a user