ci: added macOS release signing and Homebrew automation to CI

- Added macOS CI signing and notarization steps when APPLE_* secrets are configured.
- Added strict darwin verification checks to reject ad-hoc signatures and run smoke tests.
- Added Homebrew formula publishing from release assets with SHA-256 checksums.
- Added helper scripts for signing secret upload, entitlements, and release workflows.
This commit is contained in:
can1357
2026-06-08 11:49:28 +02:00
parent fd40148dcb
commit af33d4bfe4
8 changed files with 615 additions and 1 deletions
+72 -1
View File
@@ -373,6 +373,8 @@ jobs:
permissions:
contents: read
id-token: write
env:
MACOS_SIGNING: ${{ secrets.APPLE_CERTIFICATE_P12 != '' && secrets.APPLE_API_KEY != '' }}
steps:
- uses: actions/checkout@v4
- uses: oven-sh/setup-bun@v2
@@ -402,6 +404,19 @@ jobs:
env:
RELEASE_TARGETS: ${{ matrix.target_id }}
run: bun run ci:release:build-binaries
- name: Sign and notarize macOS binary (Developer ID)
# Replaces the ad-hoc signature with a Developer ID + hardened-runtime
# one (+JIT/library-validation entitlements; omp dlopens its
# runtime-extracted native addon, which has a different Team ID) and
# notarizes. Auto-skips until the APPLE_* secrets are configured.
if: matrix.platform == 'darwin' && env.MACOS_SIGNING == 'true'
env:
APPLE_CERTIFICATE_P12: ${{ secrets.APPLE_CERTIFICATE_P12 }}
APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }}
APPLE_API_KEY_ID: ${{ secrets.APPLE_API_KEY_ID }}
APPLE_API_ISSUER_ID: ${{ secrets.APPLE_API_ISSUER_ID }}
APPLE_API_KEY: ${{ secrets.APPLE_API_KEY }}
run: bash scripts/ci-macos-sign.sh "${{ matrix.binary_path }}"
# Windows binary is cross-built on Linux, so we have no Windows runner
# to smoke it on. Cross-build correctness is verified via the napi
# entry-point exports (see build-native action) and the bun
@@ -463,6 +478,8 @@ jobs:
runs-on: macos-14
permissions:
contents: read
env:
MACOS_SIGNING: ${{ secrets.APPLE_CERTIFICATE_P12 != '' && secrets.APPLE_API_KEY != '' }}
steps:
- name: Download published macOS arm64 binary
run: |
@@ -470,9 +487,22 @@ jobs:
chmod +x omp-darwin-arm64
- name: Verify published macOS arm64 binary
run: |
codesign -dv ./omp-darwin-arm64
codesign -dvvv ./omp-darwin-arm64
codesign --verify --strict --verbose=4 ./omp-darwin-arm64
runtime_dir="$(mktemp -d)"
HOME="$runtime_dir/home" XDG_DATA_HOME="$runtime_dir/xdg" ./omp-darwin-arm64 --version
HOME="$runtime_dir/home" XDG_DATA_HOME="$runtime_dir/xdg" ./omp-darwin-arm64 --smoke-test
- name: Assert signed release is not ad-hoc
if: env.MACOS_SIGNING == 'true'
run: |
if codesign -dvvv ./omp-darwin-arm64 2>&1 | grep -qE "flags=.*adhoc|Signature=adhoc"; then
echo "published binary is still ad-hoc signed (Developer ID signing did not run)" >&2
exit 1
fi
# Gatekeeper assessment: a notarized Developer ID binary is accepted.
# Informational — a bare (unstapled) Mach-O relies on the online ticket
# lookup, so surface the result without gating the release on it.
spctl -a -t exec -vv ./omp-darwin-arm64 || echo "spctl non-zero (expected for unstapled bare binary; ticket served online)"
release-npm:
if: ${{ needs.gate.outputs.is-release == 'true' && !cancelled() &&
@@ -513,3 +543,44 @@ jobs:
# publisher for the package (or on a first publish).
NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
run: bun run ci:release:publish
# Regenerate the Homebrew tap formula (can1357/homebrew-tap) from the freshly
# published release assets and push it. Depends only on release-github (the
# release and its binaries must exist). No-ops when HOMEBREW_TAP_DEPLOY_KEY is
# unset, so a release never blocks on tap access.
release_brew:
if: ${{ needs.gate.outputs.is-release == 'true' && !cancelled() &&
needs['release-github'].result == 'success' }}
needs: [gate, release-github]
runs-on: ubuntu-22.04
env:
HAS_TAP_KEY: ${{ secrets.HOMEBREW_TAP_DEPLOY_KEY != '' }}
steps:
- uses: actions/checkout@v4
if: env.HAS_TAP_KEY == 'true'
- uses: oven-sh/setup-bun@v2
if: env.HAS_TAP_KEY == 'true'
with:
bun-version: "1.3"
- name: Check out the Homebrew tap
if: env.HAS_TAP_KEY == 'true'
uses: actions/checkout@v4
with:
repository: can1357/homebrew-tap
ssh-key: ${{ secrets.HOMEBREW_TAP_DEPLOY_KEY }}
path: homebrew-tap
- name: Regenerate and push the formula
if: env.HAS_TAP_KEY == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
bun scripts/ci-update-brew-formula.ts "${{ needs.gate.outputs.release-tag }}" --out homebrew-tap/Formula/omp.rb
cd homebrew-tap
if git diff --quiet -- Formula/omp.rb; then
echo "formula already up to date for ${{ needs.gate.outputs.release-tag }}"
exit 0
fi
git -c user.name="github-actions[bot]" \
-c user.email="41898282+github-actions[bot]@users.noreply.github.com" \
commit -m "omp ${{ needs.gate.outputs.release-tag }}" -- Formula/omp.rb
git push origin HEAD:main
+6
View File
@@ -34,6 +34,12 @@ The most capable agent surface that ships. Continuously tuned by real-world use
curl -fsSL https://omp.sh/install | sh
```
**Homebrew**
```sh
brew install can1357/tap/omp
```
**Bun (recommended)**
```sh
+125
View File
@@ -0,0 +1,125 @@
# macOS signing & notarization
The compiled macOS `omp` binaries shipped on GitHub Releases are signed with a
**Developer ID Application** certificate and **notarized** by Apple. This makes
them Gatekeeper-acceptable and is the prerequisite for an official Homebrew
submission (see [#776](https://github.com/can1357/oh-my-pi/issues/776)).
Signing happens in CI, in the `release_binary` job's darwin matrix legs
(`.github/workflows/ci.yml`), via `scripts/ci-macos-sign.sh`. It **auto-skips**
until the `APPLE_*` repository secrets below are configured, so releases keep
working (ad-hoc signed, as before) in the meantime.
## How it works
1. `ci:release:build-binaries` builds and **ad-hoc** signs the binary (so it can
run on the build runner).
2. `scripts/ci-macos-sign.sh` then:
- imports the Developer ID cert into a throwaway keychain;
- re-signs with `--options runtime --timestamp` (hardened runtime + secure
timestamp) and `--entitlements scripts/macos-entitlements.plist`;
- runs `--version` and `--smoke-test` under the new signature to fail fast;
- notarizes the binary via `notarytool submit --wait`.
3. `release_github_verify` re-downloads the published arm64 asset and asserts it
is **not** ad-hoc, passes `codesign --verify --strict`, and boots cleanly.
### Why the entitlements are mandatory
The binary is a Bun single-file executable, so the hardened runtime needs:
| Entitlement | Reason |
| --- | --- |
| `com.apple.security.cs.allow-jit` | JavaScriptCore JITs at runtime. |
| `com.apple.security.cs.allow-unsigned-executable-memory` | JSC executable memory pages. |
| `com.apple.security.cs.disable-library-validation` | omp extracts its native addon (`pi_natives.<triple>.node`) and other optional dylibs to a runtime cache and `dlopen()`s them. They do not share the main binary's Team ID, so without this the hardened runtime aborts with *"mapping process and mapped file have different Team IDs"* — breaking effectively every command. |
Without `disable-library-validation`, a signed+notarized binary signs and
notarizes fine but **fails at first real use**. `scripts/ci-macos-sign.sh` runs
`--smoke-test` after signing specifically to catch this before notarizing.
### Stapling limitation (important)
A bare Mach-O executable **cannot be stapled** (`stapler` only supports
`.app`/`.pkg`/`.dmg`). The binary is genuinely notarized — `notarytool` returns
`Accepted` and the ticket exists on Apple's servers keyed to its cdhash — but
because there is no *stapled* ticket, a direct `spctl -a -t exec` assessment
reports `rejected / source=Unnotarized Developer ID`. This is expected and is
**not** a signing or credential failure.
What this means in practice:
- `curl https://omp.sh/install | sh` — `curl` sets no quarantine bit, so
Gatekeeper is never consulted; the binary just runs. ✅
- Homebrew **formula** installs — Homebrew does not quarantine formula files, so
Gatekeeper is never consulted. ✅
- Anything that **quarantines** the binary (a browser download, or a Homebrew
**cask**) and is assessed offline will be blocked, because there is no stapled
ticket. For that route, wrap the binary in a stapleable, notarized **`.pkg` or
`.dmg`** (`xcrun stapler staple` works on those). That is a follow-up and is
**not** required for the `curl`/formula paths.
## Required GitHub secrets
Add these under **Settings → Secrets and variables → Actions** (repo secrets).
Both the cert (`APPLE_CERTIFICATE_P12`) **and** the API key (`APPLE_API_KEY`)
must be present for signing to engage.
| Secret | What it is |
| --- | --- |
| `APPLE_CERTIFICATE_P12` | base64 of the exported Developer ID Application `.p12` (cert + private key). |
| `APPLE_CERTIFICATE_PASSWORD` | password you set when exporting the `.p12`. |
| `APPLE_API_KEY_ID` | App Store Connect API **Key ID**. |
| `APPLE_API_ISSUER_ID` | App Store Connect API **Issuer ID** (UUID). |
| `APPLE_API_KEY` | base64 of the App Store Connect `.p8` private key. |
### Producing the credential files
Drop these into a working directory (default `~/omp-signing`):
| File | How |
| --- | --- |
| `*.p12` | **Keychain Access** → right-click your *Developer ID Application: …* identity (the entry that expands to a cert **with** a private key) → **Export…** → save as `.p12` and set a password. |
| `p12-password.txt` | the password you just set on the `.p12`. |
| `AuthKey_<KEYID>.p8` | App Store Connect → **Users and Access → Integrations → App Store Connect API** → create a key (**Account Holder** role also allows API cert creation; **Developer** is enough for notarization) → **download once** (non-recoverable). |
| `issuer-id.txt` | the **Issuer ID** (UUID) shown above the keys table. |
| `key-id.txt` | *optional* — the Key ID; otherwise read from the `.p8` filename. |
The App Store Connect API key is the one credential that **cannot** be minted
from a CLI — it is the bootstrap credential for the API itself, and the `.p8`
downloads exactly once. Everything else is local.
### Uploading (no value leaves disk)
`scripts/ci-macos-upload-secrets.sh` validates the files (opens the `.p12` with
your password, sanity-checks the `.p8`) and pipes each value to `gh secret set`
over stdin — no secret is ever printed to the terminal, argv, or shell history:
```sh
scripts/ci-macos-upload-secrets.sh ~/omp-signing --dry-run # validate first
scripts/ci-macos-upload-secrets.sh ~/omp-signing # upload all five
gh secret list --repo can1357/oh-my-pi # confirm
```
Re-run it whenever the certificate is renewed.
### Finding your signing identity / Team ID (sanity check)
```sh
security find-identity -v -p codesigning
# e.g. "Developer ID Application: Your Name (TEAMID1234)"
```
The script selects the first `Developer ID Application` identity automatically;
you do not need to store the identity string or Team ID as a secret.
## Local dry run
You can exercise the full sign+notarize path locally (real cert + API key) by
exporting the five env vars and running:
```sh
RELEASE_TARGETS=darwin-arm64 bun run ci:release:build-binaries
APPLE_CERTIFICATE_P12=… APPLE_CERTIFICATE_PASSWORD=… \
APPLE_API_KEY_ID=… APPLE_API_ISSUER_ID=… APPLE_API_KEY=… \
bash scripts/ci-macos-sign.sh packages/coding-agent/binaries/omp-darwin-arm64
```
+9
View File
@@ -2,6 +2,15 @@
## [Unreleased]
### Added
- macOS release binaries are now signed with a Developer ID Application identity (hardened runtime + secure timestamp + JIT/library-validation entitlements) and notarized in CI when the `APPLE_*` signing secrets are configured; releases auto-fall back to ad-hoc signing until then. This makes the shipped binaries Gatekeeper-acceptable, unblocking an official Homebrew submission ([#776](https://github.com/can1357/oh-my-pi/issues/776)). See `docs/macos-signing-notarization.md`.
- Added a Homebrew install path: `brew install can1357/tap/omp`. The [can1357/homebrew-tap](https://github.com/can1357/homebrew-tap) formula installs the prebuilt release binary, and a `release_brew` CI job regenerates it (version + per-asset sha256) from each published release via `scripts/ci-update-brew-formula.ts` ([#776](https://github.com/can1357/oh-my-pi/issues/776)).
### Changed
- Rewrote the session auto-title prompt (`prompts/system/title-system.md`) and the `set_title` tool description to ask for a concise, sentence-case title (3-7 words) that captures the session's topic/goal, with good/bad examples and explicit guidance to treat the first message as data (no following embedded links/instructions, no refusals, describe URL/reference asks). The local on-device title prompt (`tiny-title-system.md`) was aligned to the same 3-7 word, sentence-case convention. The deterministic greeting/low-signal filter and the `none` deferral sentinel are unchanged.
## [15.10.3] - 2026-06-08
### Added
+145
View File
@@ -0,0 +1,145 @@
#!/usr/bin/env bash
#
# Sign and notarize a compiled macOS `omp` binary with a Developer ID identity.
#
# The release build (`ci:release:build-binaries`) ad-hoc signs the binary so it
# runs locally. This script *replaces* that signature with a real Developer ID
# Application signature plus the hardened runtime, a secure timestamp, and the
# JIT / library-validation entitlements the Bun + JavaScriptCore runtime and the
# runtime-extracted native addon require (see scripts/macos-entitlements.plist),
# then notarizes the result with App Store Connect API credentials.
#
# A bare Mach-O executable cannot be stapled (stapler only supports .app/.pkg/
# .dmg), so the notarization ticket is served online: Gatekeeper fetches it by
# cdhash on first assessment. `curl` downloads and Homebrew *formula* installs do
# not set the quarantine bit, so they never invoke Gatekeeper; for an offline,
# quarantined cask we would need a stapleable .pkg/.dmg wrapper (follow-up).
#
# Required environment (wired from GitHub Actions secrets):
# APPLE_CERTIFICATE_P12 base64 of the Developer ID Application .p12 bundle
# APPLE_CERTIFICATE_PASSWORD password protecting that .p12
# APPLE_API_KEY_ID App Store Connect API key id (the "Key ID")
# APPLE_API_ISSUER_ID App Store Connect API issuer id (UUID)
# APPLE_API_KEY base64 of the App Store Connect .p8 private key
#
# Usage: scripts/ci-macos-sign.sh <path-to-binary>
set -euo pipefail
if [[ "${OSTYPE:-}" != darwin* ]]; then
echo "ci-macos-sign: must run on macOS" >&2
exit 1
fi
BINARY="${1:-}"
if [[ -z "$BINARY" ]]; then
echo "usage: ci-macos-sign.sh <path-to-binary>" >&2
exit 1
fi
if [[ ! -f "$BINARY" ]]; then
echo "ci-macos-sign: binary not found: $BINARY" >&2
exit 1
fi
missing=()
for var in APPLE_CERTIFICATE_P12 APPLE_CERTIFICATE_PASSWORD APPLE_API_KEY_ID APPLE_API_ISSUER_ID APPLE_API_KEY; do
[[ -n "${!var:-}" ]] || missing+=("$var")
done
if ((${#missing[@]})); then
echo "ci-macos-sign: missing required env: ${missing[*]}" >&2
exit 1
fi
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
ENTITLEMENTS="$SCRIPT_DIR/macos-entitlements.plist"
if [[ ! -f "$ENTITLEMENTS" ]]; then
echo "ci-macos-sign: entitlements not found: $ENTITLEMENTS" >&2
exit 1
fi
WORKDIR="$(mktemp -d)"
KEYCHAIN="$WORKDIR/omp-signing.keychain-db"
KEYCHAIN_PASSWORD="$(openssl rand -hex 24)"
CERT_PATH="$WORKDIR/cert.p12"
API_KEY_PATH="$WORKDIR/api-key.p8"
ZIP_PATH="$WORKDIR/$(basename "$BINARY").zip"
cleanup() {
security delete-keychain "$KEYCHAIN" >/dev/null 2>&1 || true
rm -rf "$WORKDIR"
}
trap cleanup EXIT
echo "ci-macos-sign: decoding credentials"
printf '%s' "$APPLE_CERTIFICATE_P12" | base64 --decode >"$CERT_PATH"
printf '%s' "$APPLE_API_KEY" | base64 --decode >"$API_KEY_PATH"
echo "ci-macos-sign: provisioning a temporary signing keychain"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
# Auto-relock after 6h as a safety net; the EXIT trap deletes it well before.
security set-keychain-settings -lut 21600 "$KEYCHAIN"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN"
# Prepend our keychain to the user search list so codesign can resolve the
# identity, keeping the runner's existing keychains intact.
existing_keychains="$(security list-keychains -d user | sed -e 's/"//g' -e 's/^[[:space:]]*//')"
# shellcheck disable=SC2086 # intentional word-splitting of the keychain list
security list-keychains -d user -s "$KEYCHAIN" $existing_keychains
security import "$CERT_PATH" -P "$APPLE_CERTIFICATE_PASSWORD" -k "$KEYCHAIN" \
-T /usr/bin/codesign -T /usr/bin/security
# Grant codesign non-interactive access to the imported private key.
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PASSWORD" "$KEYCHAIN" >/dev/null
IDENTITY="$(security find-identity -v -p codesigning "$KEYCHAIN" \
| awk -F'"' '/Developer ID Application/ {print $2; exit}')"
if [[ -z "$IDENTITY" ]]; then
echo "ci-macos-sign: no 'Developer ID Application' identity in the imported keychain" >&2
security find-identity -v -p codesigning "$KEYCHAIN" >&2 || true
exit 1
fi
echo "ci-macos-sign: signing as: $IDENTITY"
codesign --force --timestamp --options runtime \
--entitlements "$ENTITLEMENTS" \
--sign "$IDENTITY" \
"$BINARY"
echo "ci-macos-sign: verifying signature"
codesign --verify --strict --verbose=4 "$BINARY"
codesign -dvvv "$BINARY" 2>&1 | grep -E "Authority|TeamIdentifier|flags=|Timestamp" || true
# Fail fast before the slower notarization round-trip: a hardened-runtime binary
# missing an entitlement still signs cleanly but aborts at launch (e.g. the
# native-addon Team ID check). Exercise the runtime in an isolated HOME.
echo "ci-macos-sign: launch check under the hardened-runtime signature"
run_home="$WORKDIR/home"
HOME="$run_home" XDG_DATA_HOME="$run_home/xdg" "$BINARY" --version
HOME="$run_home" XDG_DATA_HOME="$run_home/xdg" "$BINARY" --smoke-test
echo "ci-macos-sign: submitting for notarization"
/usr/bin/ditto -c -k --keepParent "$BINARY" "$ZIP_PATH"
submit_json="$(xcrun notarytool submit "$ZIP_PATH" \
--key "$API_KEY_PATH" \
--key-id "$APPLE_API_KEY_ID" \
--issuer "$APPLE_API_ISSUER_ID" \
--wait \
--timeout 30m \
--output-format json)"
echo "$submit_json"
read -r status submission_id <<<"$(printf '%s' "$submit_json" \
| python3 -c 'import json,sys; d=json.load(sys.stdin); print(d.get("status",""), d.get("id",""))')"
if [[ "$status" != "Accepted" ]]; then
echo "ci-macos-sign: notarization status=$status (expected Accepted)" >&2
if [[ -n "$submission_id" ]]; then
xcrun notarytool log "$submission_id" \
--key "$API_KEY_PATH" \
--key-id "$APPLE_API_KEY_ID" \
--issuer "$APPLE_API_ISSUER_ID" >&2 || true
fi
exit 1
fi
echo "ci-macos-sign: notarized ($(basename "$BINARY"), submission $submission_id)"
echo "ci-macos-sign: note — a bare Mach-O cannot be stapled; the ticket is verified online."
+115
View File
@@ -0,0 +1,115 @@
#!/usr/bin/env bash
#
# Upload the macOS signing/notarization secrets to GitHub Actions WITHOUT ever
# printing a secret value. Every value is read from a file on disk and piped to
# `gh secret set` over stdin, so nothing lands in argv, the shell history, or a
# terminal transcript.
#
# Prepare a directory (default ~/omp-signing) containing:
# *.p12 Developer ID Application identity exported from Keychain
# Access (right-click identity -> Export -> .p12).
# p12-password.txt the password you set on that .p12 export.
# AuthKey_<KEYID>.p8 App Store Connect API key (download-once from the web).
# issuer-id.txt App Store Connect API issuer id (UUID).
# key-id.txt optional; otherwise the <KEYID> is read from the .p8
# filename.
#
# Usage:
# scripts/ci-macos-upload-secrets.sh [dir] [--dry-run]
# OMP_REPO=owner/repo scripts/ci-macos-upload-secrets.sh ~/omp-signing
set -euo pipefail
DIR=""
DRY_RUN=0
for arg in "$@"; do
case "$arg" in
--dry-run) DRY_RUN=1 ;;
*) DIR="$arg" ;;
esac
done
DIR="${DIR:-${OMP_SIGNING_DIR:-$HOME/omp-signing}}"
REPO="${OMP_REPO:-can1357/oh-my-pi}"
die() {
echo "ci-macos-upload-secrets: $1" >&2
exit 1
}
[[ -d "$DIR" ]] || die "directory not found: $DIR"
find_one() {
# Echo the single file in $DIR matching the glob, or fail.
local pattern="$1" matches=()
while IFS= read -r f; do matches+=("$f"); done < <(find "$DIR" -maxdepth 1 -type f -name "$pattern" | sort)
((${#matches[@]} == 1)) || die "expected exactly one '$pattern' in $DIR, found ${#matches[@]}"
printf '%s' "${matches[0]}"
}
read_file_value() {
# Trim a single trailing newline; reject empty.
local path="$1" name="$2" value
[[ -f "$path" ]] || die "missing $name file: $path"
value="$(cat "$path")"
[[ -n "$value" ]] || die "$name file is empty: $path"
printf '%s' "$value"
}
P12="$(find_one '*.p12')"
P8="$(find_one '*.p8')"
PW="$(read_file_value "$DIR/p12-password.txt" "p12-password.txt")"
ISSUER="$(read_file_value "$DIR/issuer-id.txt" "issuer-id.txt")"
if [[ -f "$DIR/key-id.txt" ]]; then
KEYID="$(read_file_value "$DIR/key-id.txt" "key-id.txt")"
else
# AuthKey_ABCDE12345.p8 -> ABCDE12345
KEYID="$(basename "$P8" .p8)"
KEYID="${KEYID#AuthKey_}"
[[ -n "$KEYID" && "$KEYID" != "$(basename "$P8" .p8)" ]] \
|| die "could not derive key id from '$(basename "$P8")'; add key-id.txt"
fi
# Validate the .p12 + password the same way CI consumes it — `security import`
# into a throwaway keychain — and confirm a Developer ID identity is inside, so a
# typo or wrong cert fails here instead of in CI. (We avoid `openssl pkcs12`:
# OpenSSL 3.x can't read the legacy RC2-40-CBC algorithm Keychain Access still
# uses, which `security import` handles fine.)
validate_p12=$(
kc="$(mktemp -d)/validate.keychain-db"
kp="$(openssl rand -hex 16)"
security create-keychain -p "$kp" "$kc" >/dev/null 2>&1
security unlock-keychain -p "$kp" "$kc" >/dev/null 2>&1
if security import "$P12" -P "$PW" -k "$kc" -T /usr/bin/codesign >/dev/null 2>&1 \
&& security find-identity -v -p codesigning "$kc" 2>/dev/null | grep -q "Developer ID Application"; then
echo ok
fi
security delete-keychain "$kc" >/dev/null 2>&1 || true
)
[[ "$validate_p12" == ok ]] \
|| die "the .p12 did not import with the password in p12-password.txt, or holds no Developer ID Application identity"
grep -q "BEGIN PRIVATE KEY" "$P8" \
|| die "the .p8 does not look like a PEM private key"
echo "ci-macos-upload-secrets: repo=$REPO"
echo " cert : $(basename "$P12")"
echo " key : $(basename "$P8") (key id $KEYID)"
echo " -> APPLE_CERTIFICATE_P12, APPLE_CERTIFICATE_PASSWORD, APPLE_API_KEY_ID, APPLE_API_ISSUER_ID, APPLE_API_KEY"
if ((DRY_RUN)); then
echo "ci-macos-upload-secrets: --dry-run, not uploading"
exit 0
fi
set_secret_stdin() {
# $1 = secret name; value piped on stdin. Never echoes the value.
gh secret set "$1" --repo "$REPO"
}
base64 <"$P12" | tr -d '\n' | set_secret_stdin APPLE_CERTIFICATE_P12
printf '%s' "$PW" | set_secret_stdin APPLE_CERTIFICATE_PASSWORD
printf '%s' "$KEYID" | set_secret_stdin APPLE_API_KEY_ID
printf '%s' "$ISSUER" | set_secret_stdin APPLE_API_ISSUER_ID
base64 <"$P8" | tr -d '\n' | set_secret_stdin APPLE_API_KEY
echo "ci-macos-upload-secrets: done. Verify with: gh secret list --repo $REPO"
+117
View File
@@ -0,0 +1,117 @@
#!/usr/bin/env bun
//
// Render the Homebrew formula for `omp` from a published GitHub release and write
// it to a tap checkout. The release publishes per-platform bare binaries
// (omp-<platform>-<arch>); this reads their sha256 digests straight from the
// release metadata so the formula never drifts from the shipped assets.
//
// Usage:
// bun scripts/ci-update-brew-formula.ts <tag> --out <path/to/Formula/omp.rb>
// bun scripts/ci-update-brew-formula.ts v15.10.3 # prints to stdout
import { $ } from "bun";
const REPO = process.env.OMP_REPO ?? "can1357/oh-my-pi";
const HOMEPAGE = "https://omp.sh";
const DESC = "Coding agent with the IDE wired in";
interface ReleaseAsset {
name: string;
digest?: string;
}
function parseArgs(argv: readonly string[]): { tag: string; out: string | null } {
const rest = [...argv];
let out: string | null = null;
const outIdx = rest.findIndex(a => a === "--out");
if (outIdx >= 0) {
out = rest[outIdx + 1] ?? null;
if (!out) throw new Error("--out requires a path");
rest.splice(outIdx, 2);
}
const tag = rest.find(a => !a.startsWith("--"));
if (!tag) throw new Error("usage: ci-update-brew-formula.ts <tag> [--out <file>]");
return { tag, out };
}
async function fetchAssets(tag: string): Promise<ReleaseAsset[]> {
const res = await $`gh release view ${tag} --repo ${REPO} --json assets`.quiet().nothrow();
if (res.exitCode !== 0) {
throw new Error(`gh release view ${tag} failed: ${res.stderr.toString().trim()}`);
}
const parsed = JSON.parse(res.stdout.toString()) as { assets: ReleaseAsset[] };
return parsed.assets;
}
function sha256For(assets: readonly ReleaseAsset[], name: string): string {
const asset = assets.find(a => a.name === name);
if (!asset) throw new Error(`release is missing asset ${name}`);
if (!asset.digest?.startsWith("sha256:")) {
throw new Error(`asset ${name} has no sha256 digest (got ${asset.digest ?? "none"})`);
}
return asset.digest.slice("sha256:".length);
}
// `${...}` is JS interpolation; the literal `#{version}` / `#{bin}` below are
// Ruby interpolations Homebrew resolves when it evaluates the formula.
function renderFormula(version: string, sums: Record<string, string>): string {
return `class Omp < Formula
desc "${DESC}"
homepage "${HOMEPAGE}"
version "${version}"
license "MIT"
on_macos do
on_arm do
url "https://github.com/${REPO}/releases/download/v#{version}/omp-darwin-arm64"
sha256 "${sums["omp-darwin-arm64"]}"
end
on_intel do
url "https://github.com/${REPO}/releases/download/v#{version}/omp-darwin-x64"
sha256 "${sums["omp-darwin-x64"]}"
end
end
on_linux do
on_arm do
url "https://github.com/${REPO}/releases/download/v#{version}/omp-linux-arm64"
sha256 "${sums["omp-linux-arm64"]}"
end
on_intel do
url "https://github.com/${REPO}/releases/download/v#{version}/omp-linux-x64"
sha256 "${sums["omp-linux-x64"]}"
end
end
def install
bin.install Dir["omp-*"].first => "omp"
(bin/"omp").chmod 0555
generate_completions_from_executable(bin/"omp", "completions", shells: [:bash, :zsh, :fish])
end
test do
assert_match version.to_s, shell_output("#{bin}/omp --version")
end
end
`;
}
async function main(): Promise<void> {
const { tag, out } = parseArgs(process.argv.slice(2));
const version = tag.replace(/^v/, "");
const assets = await fetchAssets(tag);
const targets = ["omp-darwin-arm64", "omp-darwin-x64", "omp-linux-arm64", "omp-linux-x64"];
const sums: Record<string, string> = {};
for (const name of targets) sums[name] = sha256For(assets, name);
const formula = renderFormula(version, sums);
if (out) {
await Bun.write(out, formula);
console.log(`wrote ${out} for ${tag}`);
} else {
process.stdout.write(formula);
}
}
await main();
+26
View File
@@ -0,0 +1,26 @@
<?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>
<!--
Entitlements for the hardened-runtime Developer ID signature applied to the
compiled `omp` macOS binary (see scripts/ci-macos-sign.sh).
These are NOT optional. The binary is a Bun single-file executable, and:
* allow-jit / allow-unsigned-executable-memory — JavaScriptCore JITs at
runtime; the hardened runtime kills JIT (MAP_JIT) pages without these.
* disable-library-validation — omp extracts its native addon
(pi_natives.<triple>.node) and other optional dylibs to a runtime cache
and dlopen()s them. Those dylibs do not share the main binary's Team ID,
so without this entitlement the hardened runtime refuses to map them
("mapping process and mapped file have different Team IDs") and every
command that touches natives (i.e. effectively all of them) aborts.
-->
<key>com.apple.security.cs.allow-jit</key>
<true/>
<key>com.apple.security.cs.allow-unsigned-executable-memory</key>
<true/>
<key>com.apple.security.cs.disable-library-validation</key>
<true/>
</dict>
</plist>