···59596060## Personal Release Strategy (Fork Release Page)
61616262-For personal usage, the easiest path is:
6262+The release helper now supports automatic notarization for personal fork releases.
63636464-1) Build unsigned Debug app locally (`make build-app`).
6565-2) Zip app bundle.
6666-3) Create a tag.
6767-4) Upload zip to your fork GitHub Release page.
6464+Default flow:
68656969-This avoids Apple signing/notarization setup and keeps the workflow simple.
6666+1) Build app locally (`make build-app`).
6767+2) Sign app with your `Developer ID Application` identity.
6868+3) Notarize via `notarytool` and staple ticket to app.
6969+4) Zip app bundle.
7070+5) Create tag and upload zip to your fork GitHub Release page.
7171+7272+If you want the old behavior (no notarization), set `ENABLE_NOTARIZATION=0`.
70737174## Helper Scripts
7275···7578 - Default target repo: auto-detected from `origin`
7679 - Override target repo: `GH_REPO=owner/repo`
7780 - Release create fallback: if `gh release create` fails (for example token scope mismatch), script falls back to `gh api` and then uploads assets
8181+ - Notarization: enabled by default (`ENABLE_NOTARIZATION=1`)
8282+ - Default keychain profile name: `supacode-notary` (override with `APPLE_NOTARY_KEYCHAIN_PROFILE`)
78837984### Example
8085···8893# Or specify tag explicitly
8994./doc-onevcat/scripts/release-to-fork.sh onevcat-v2026.02.26-01
9095```
9696+9797+## Notarization Credentials
9898+9999+The script first tries `xcrun notarytool submit --keychain-profile <profile>`.
100100+If profile is missing, it will create one using either:
101101+102102+- App Store Connect API key:
103103+ - `APPLE_NOTARIZATION_KEY_PATH`
104104+ - `APPLE_NOTARIZATION_KEY_ID`
105105+ - `APPLE_NOTARIZATION_ISSUER`
106106+- Or Apple ID credentials:
107107+ - `APPLE_ID`
108108+ - `APPLE_PASSWORD` (app-specific password)
109109+ - `APPLE_TEAM_ID` (optional if inferable from signing identity)
110110+111111+Signing identity:
112112+113113+- `APPLE_SIGNING_IDENTITY` (optional). If omitted, script auto-detects the first available `Developer ID Application` identity from keychain.
9111492115## Optional: Full Signed Release on Fork
93116
+170-11
doc-onevcat/scripts/release-to-fork.sh
···11#!/usr/bin/env bash
22set -euo pipefail
3344-if ! command -v gh >/dev/null 2>&1; then
55- echo "error: gh CLI is required"
66- exit 1
77-fi
88-99-if ! command -v jq >/dev/null 2>&1; then
1010- echo "error: jq is required"
1111- exit 1
1212-fi
1313-144origin_repo_from_remote() {
155 local remote_url
166 remote_url="$(git remote get-url origin 2>/dev/null || true)"
···3121 return 1
3222}
33232424+default_signing_identity() {
2525+ security find-identity -v -p codesigning 2>/dev/null \
2626+ | awk -F'"' '/Developer ID Application/ {print $2; exit}'
2727+}
2828+2929+team_id_from_identity() {
3030+ local identity="$1"
3131+ if [[ "$identity" =~ \(([A-Z0-9]{10})\)$ ]]; then
3232+ echo "${BASH_REMATCH[1]}"
3333+ fi
3434+}
3535+3636+submit_with_keychain_profile() {
3737+ local artifact_path="$1"
3838+ local output
3939+4040+ set +e
4141+ output="$(xcrun notarytool submit "$artifact_path" --keychain-profile "$KEYCHAIN_PROFILE" --wait 2>&1)"
4242+ local status=$?
4343+ set -e
4444+4545+ if [[ $status -eq 0 ]]; then
4646+ echo "$output"
4747+ return 0
4848+ fi
4949+5050+ echo "$output" >&2
5151+ if [[ "$output" == *"No Keychain password item found for profile"* ]] \
5252+ || [[ "$output" == *"profile"* && "$output" == *"not found"* ]]
5353+ then
5454+ return 2
5555+ fi
5656+5757+ return $status
5858+}
5959+6060+store_notary_credentials() {
6161+ local key_path="${APPLE_NOTARIZATION_KEY_PATH:-}"
6262+ local key_id="${APPLE_NOTARIZATION_KEY_ID:-}"
6363+ local issuer="${APPLE_NOTARIZATION_ISSUER:-}"
6464+6565+ if [[ -n "$key_path" || -n "$key_id" || -n "$issuer" ]]; then
6666+ if [[ -z "$key_path" || -z "$key_id" || -z "$issuer" ]]; then
6767+ echo "error: APPLE_NOTARIZATION_KEY_PATH/KEY_ID/ISSUER must all be set"
6868+ exit 1
6969+ fi
7070+ if [[ ! -f "$key_path" ]]; then
7171+ echo "error: APPLE_NOTARIZATION_KEY_PATH does not exist: $key_path"
7272+ exit 1
7373+ fi
7474+ xcrun notarytool store-credentials "$KEYCHAIN_PROFILE" \
7575+ --key "$key_path" \
7676+ --key-id "$key_id" \
7777+ --issuer "$issuer"
7878+ return
7979+ fi
8080+8181+ if [[ -z "$APPLE_ID_INPUT" ]]; then
8282+ if [[ -t 0 ]]; then
8383+ read -r -p "Apple ID email for notarization: " APPLE_ID_INPUT
8484+ else
8585+ echo "error: APPLE_ID is required when no key-based notarization credentials are provided"
8686+ exit 1
8787+ fi
8888+ fi
8989+9090+ if [[ -z "$APPLE_PASSWORD_INPUT" ]]; then
9191+ if [[ -t 0 ]]; then
9292+ read -r -s -p "App-specific password (input hidden): " APPLE_PASSWORD_INPUT
9393+ echo
9494+ else
9595+ echo "error: APPLE_PASSWORD is required when no key-based notarization credentials are provided"
9696+ exit 1
9797+ fi
9898+ fi
9999+100100+ if [[ -z "$TEAM_ID_INPUT" ]]; then
101101+ TEAM_ID_INPUT="$(team_id_from_identity "$SIGNING_IDENTITY" || true)"
102102+ fi
103103+ if [[ -z "$TEAM_ID_INPUT" ]]; then
104104+ if [[ -t 0 ]]; then
105105+ read -r -p "Apple Team ID: " TEAM_ID_INPUT
106106+ else
107107+ echo "error: APPLE_TEAM_ID is required when it cannot be inferred from signing identity"
108108+ exit 1
109109+ fi
110110+ fi
111111+112112+ xcrun notarytool store-credentials "$KEYCHAIN_PROFILE" \
113113+ --apple-id "$APPLE_ID_INPUT" \
114114+ --password "$APPLE_PASSWORD_INPUT" \
115115+ --team-id "$TEAM_ID_INPUT"
116116+}
117117+118118+sign_and_notarize_app() {
119119+ local app_path="$1"
120120+ local submission_zip="$2"
121121+122122+ echo "[release] codesigning app with identity: $SIGNING_IDENTITY"
123123+ codesign --force --deep --options runtime --timestamp --sign "$SIGNING_IDENTITY" "$app_path"
124124+ codesign --verify --deep --strict --verbose=2 "$app_path"
125125+126126+ echo "[release] create notarization artifact: $submission_zip"
127127+ ditto -c -k --sequesterRsrc --keepParent "$app_path" "$submission_zip"
128128+129129+ echo "[release] notarizing artifact..."
130130+ if submit_with_keychain_profile "$submission_zip"; then
131131+ echo "[release] used keychain profile: $KEYCHAIN_PROFILE"
132132+ else
133133+ local notary_status=$?
134134+ if [[ $notary_status -ne 2 ]]; then
135135+ exit "$notary_status"
136136+ fi
137137+138138+ echo "[release] keychain profile not found: $KEYCHAIN_PROFILE"
139139+ echo "[release] storing notarization credentials..."
140140+ store_notary_credentials
141141+ xcrun notarytool submit "$submission_zip" --keychain-profile "$KEYCHAIN_PROFILE" --wait
142142+ fi
143143+144144+ echo "[release] staple notarization ticket to app"
145145+ xcrun stapler staple "$app_path"
146146+ xcrun stapler validate "$app_path"
147147+}
148148+149149+if ! command -v gh >/dev/null 2>&1; then
150150+ echo "error: gh CLI is required"
151151+ exit 1
152152+fi
153153+154154+if ! command -v jq >/dev/null 2>&1; then
155155+ echo "error: jq is required"
156156+ exit 1
157157+fi
158158+159159+if [[ "$(uname -s)" != "Darwin" ]]; then
160160+ echo "error: this script only supports macOS"
161161+ exit 1
162162+fi
163163+34164REPO="${GH_REPO:-$(origin_repo_from_remote || true)}"
35165if [[ -z "${REPO}" ]]; then
36166 REPO="$(gh repo view --json nameWithOwner -q .nameWithOwner)"
···39169SHORT_SHA="$(git rev-parse --short HEAD)"
40170DEFAULT_TAG="onevcat-v$(date +%Y.%m.%d)-${SHORT_SHA}"
41171TAG="${1:-$DEFAULT_TAG}"
172172+ENABLE_NOTARIZATION="${ENABLE_NOTARIZATION:-1}"
173173+KEYCHAIN_PROFILE="${APPLE_NOTARY_KEYCHAIN_PROFILE:-supacode-notary}"
174174+SIGNING_IDENTITY="${APPLE_SIGNING_IDENTITY:-}"
175175+TEAM_ID_INPUT="${APPLE_TEAM_ID:-}"
176176+APPLE_ID_INPUT="${APPLE_ID:-}"
177177+APPLE_PASSWORD_INPUT="${APPLE_PASSWORD:-}"
4217843179echo "[release] repository: ${REPO}"
44180echo "[release] tag: ${TAG}"
181181+echo "[release] notarization: ${ENABLE_NOTARIZATION}"
4518246183if git rev-parse "${TAG}" >/dev/null 2>&1; then
47184 echo "error: local tag ${TAG} already exists"
···65202mkdir -p build
66203ZIP_PATH="build/${PRODUCT_NAME%.app}-${TAG}.app.zip"
67204NOTES_PATH="build/release-notes-${TAG}.md"
205205+SUBMISSION_ZIP="build/notary-submit-${TAG}.app.zip"
206206+BUILD_TYPE="Debug (unsigned)"
207207+208208+if [[ "${ENABLE_NOTARIZATION}" == "1" ]]; then
209209+ if ! command -v xcrun >/dev/null 2>&1; then
210210+ echo "error: xcrun is required for notarization"
211211+ exit 1
212212+ fi
213213+ if ! command -v codesign >/dev/null 2>&1; then
214214+ echo "error: codesign is required for notarization"
215215+ exit 1
216216+ fi
217217+ if [[ -z "$SIGNING_IDENTITY" ]]; then
218218+ SIGNING_IDENTITY="$(default_signing_identity || true)"
219219+ fi
220220+ if [[ -z "$SIGNING_IDENTITY" ]]; then
221221+ echo "error: APPLE_SIGNING_IDENTITY is not set and no Developer ID Application identity was found"
222222+ exit 1
223223+ fi
224224+ sign_and_notarize_app "${APP_PATH}" "${SUBMISSION_ZIP}"
225225+ BUILD_TYPE="Debug (Developer ID signed + notarized)"
226226+fi
6822769228echo "[release] package ${APP_PATH} -> ${ZIP_PATH}"
70229ditto -c -k --sequesterRsrc --keepParent "${APP_PATH}" "${ZIP_PATH}"
···7523476235- Commit: ${SHORT_SHA}
77236- Upstream main (local): ${UPSTREAM_MAIN_SHA}
7878-- Build type: Debug (unsigned)
237237+- Build type: ${BUILD_TYPE}
79238- Branch: $(git branch --show-current)
80239EOF
81240