Compare commits

...

6 Commits

Author SHA1 Message Date
Alan Wizemann 12610faba0 chore: Bump version to 1.6.2 2026-04-17 17:18:33 -07:00
Alan Wizemann 73b44202ba fix: release script preflight allows pre-written RELEASE_NOTES.md
CLAUDE.md's release-notes convention says "write them to
releases/v<version>/RELEASE_NOTES.md BEFORE running the script" — but
the script's git-clean preflight rejected any working-tree state
including that exact file as untracked. Chicken-and-egg: you couldn't
follow the documented flow.

Preflight now whitelists releases/v<VERSION>/RELEASE_NOTES.md as the one
allowed untracked path. Everything else still fails the check.

Caught while running v1.6.2.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 17:18:23 -07:00
Alan Wizemann eed55cbb0f chore: Ignore release-artifact binaries
Stops the release script's git-clean preflight from tripping on the
zips + appcast-entry.xml that every release run produces under
releases/v<VERSION>/. GitHub Releases hosts the actual downloads; there's
no reason to commit ~30 MB of binaries per release into git history.

RELEASE_NOTES.md stays tracked — it's committed as part of the version
bump by the release script.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 17:16:10 -07:00
Alan Wizemann 14c97bee62 docs: CLAUDE.md — document the release flow + canonical prompts
Adds a Releases section so future Claude sessions (and teammates) don't
have to rediscover the release workflow. Documents:

- The single entry point: `./scripts/release.sh <ver> [--draft]`
- What the script does end-to-end
- The release notes convention (write them before running)
- A handful of canonical prompts the user can type
- Pointers to deeper prerequisite docs (README, script header)

Deliberately brief — detail lives in README and the personal auto-memory
at reference_release_process.md. CLAUDE.md's job here is just to make
the entry point discoverable on session start.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 17:13:15 -07:00
Alan Wizemann 8d3fe70e2c fix: Chat tab false-positive "no credentials" warning before session pick
The orange "No AI provider credentials detected" banner was firing on the
Chat tab whenever no session was selected, even for users whose
credentials were configured and working. The banner only disappeared
when a session started — not because credentials were actually found,
but because the banner's `!hasActiveProcess` gate flipped to false once
ACP launched.

Root cause: `HermesFileService.hasAnyAICredential()` inspected only the
shell environment and `~/.hermes/.env`, while Hermes itself resolves
credentials from two additional places Scarf had never learned about:

  - `~/.hermes/auth.json` — the Credential Pools file written by the
    Configure → Credential Pools UI (the blessed v1.6 flow)
  - `~/.hermes/config.yaml` — embedded `api_key:` under auxiliary.<task>
    and delegation

The preflight now checks all four locations. For auth.json we parse the
JSON and look for any `credential_pool.<provider>[*].access_token` that
is non-empty. For config.yaml we line-scan for `api_key:` leaves with a
non-empty value, matching the defensive style of the existing .env
scanner (no YAML parser needed in a nonisolated function).

Also updated the banner subtitle to point users at Credential Pools
before .env, since the former is the blessed in-app flow.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-17 17:10:51 -07:00
Alan Wizemann da88c98c7a feat: release script builds Universal + ARM64 variants
Each release now produces two distribution zips:
- Scarf-vX.X.X-Universal.zip  (arm64 + x86_64, recommended)
- Scarf-vX.X.X-ARM64.zip      (arm64 only, ~14% smaller)

Both are independently archived, exported with Developer ID, notarized,
and stapled via a new build_variant helper. The appcast still points at
the Universal zip since it works on all supported macs; ARM64 is an
alternative manual download for Apple Silicon users who want the smaller
file.

README updated to list both variants.

Prompted by the v1.6.1 release shipping only Universal; the ARM64 zip
for v1.6.1 was produced ad-hoc and uploaded to the existing release.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-16 20:10:17 -07:00
8 changed files with 170 additions and 72 deletions
+5
View File
@@ -47,3 +47,8 @@ scarf/standards/backups/
# Scarf project dashboards (user-specific) # Scarf project dashboards (user-specific)
.scarf/ .scarf/
# Release artifacts — GitHub Releases hosts the binaries; no need to bloat git
# history. RELEASE_NOTES.md stays tracked (committed with the version bump).
releases/v*/*.zip
releases/v*/appcast-entry.xml
+20
View File
@@ -39,6 +39,26 @@ scarf/scarf/ Xcode project root (PBXFileSystemSynchronizedRootGroup
xcodebuild -project scarf/scarf.xcodeproj -scheme scarf -configuration Debug build xcodebuild -project scarf/scarf.xcodeproj -scheme scarf -configuration Debug build
``` ```
## Releases
Shipped via a single local script. **Never run manual `xcodebuild archive` / `notarytool` / `gh release create` steps — use the script so nothing is skipped or misordered.**
```bash
./scripts/release.sh <version> # full release: notarize → appcast → gh-pages → tag
./scripts/release.sh <version> --draft # draft: everything builds + notarizes, but appcast/tag are skipped
```
The script bumps version, archives Universal (arm64 + x86_64) + ARM64-only variants, signs with Developer ID, notarizes via `xcrun notarytool` (keychain profile `scarf-notary`), staples, EdDSA-signs the appcast entry with Sparkle's key, pushes the appcast to `gh-pages`, and creates a GitHub release with both zips attached. Draft mode stops after the release is uploaded so the current version stays "latest" until explicitly promoted.
**Release notes convention:** write them to `releases/v<version>/RELEASE_NOTES.md` BEFORE running the script — it's auto-included in the version-bump commit and used as the GitHub release body. If absent, a placeholder is used.
**Canonical prompts (any of these trigger the flow):**
- "Release v1.6.2" — full release
- "Release v1.6.2 as draft" — draft mode
- "Prepare v1.6.2 release notes from recent commits, then release" — generate notes first, then run
**Prerequisites (one-time, already set up on Alan's machine):** Developer ID Application cert in login Keychain (team `3Q6X2L86C4`), notarytool keychain profile `scarf-notary`, Sparkle EdDSA private key in Keychain item `https://sparkle-project.org`, `gh-pages` branch + GitHub Pages enabled. See the header of [scripts/release.sh](scripts/release.sh) and the Releases section in [README.md](README.md) for details.
## Hermes Version ## Hermes Version
Targets Hermes v0.9.0 (v2026.4.13). Log lines may carry an optional `[session_id]` tag between the level and logger name — `HermesLogService.parseLine` treats the session tag as an optional capture group, so older untagged lines still parse. Targets Hermes v0.9.0 (v2026.4.13). Log lines may carry an optional `[session_id]` tag between the level and logger name — `HermesLogService.parseLine` treats the session tag as an optional capture group, so older untagged lines still parse.
+1
View File
@@ -101,6 +101,7 @@ If a Hermes update changes the database schema or CLI output format, Scarf may n
Download the latest build from [Releases](https://github.com/awizemann/scarf/releases): Download the latest build from [Releases](https://github.com/awizemann/scarf/releases):
- `Scarf-vX.X.X-Universal.zip` — Apple Silicon + Intel (recommended) - `Scarf-vX.X.X-Universal.zip` — Apple Silicon + Intel (recommended)
- `Scarf-vX.X.X-ARM64.zip` — Apple Silicon only (smaller download)
1. Unzip and drag **Scarf.app** to Applications 1. Unzip and drag **Scarf.app** to Applications
2. Launch normally — builds are Developer ID signed and notarized, so Gatekeeper accepts them on first launch 2. Launch normally — builds are Developer ID signed and notarized, so Gatekeeper accepts them on first launch
+13
View File
@@ -0,0 +1,13 @@
## What's New in 1.6.2
### Fixes
- **No more bogus "missing credentials" banner on Chat.** The orange "No AI provider credentials detected" warning was firing on the Chat tab whenever no session was selected, even for users whose credentials were configured and working. Root cause: the preflight check only inspected `~/.hermes/.env` and shell environment variables, missing the Credential Pools file at `~/.hermes/auth.json` (the in-app flow introduced in 1.6.0) and `api_key:` fields in `config.yaml`. The check now covers all four locations Hermes itself reads at runtime, so if you've added credentials via **Configure → Credential Pools**, the warning stays hidden.
### Polish
- Banner subtitle updated to point users at the in-app Credential Pools flow first, rather than prescribing `.env` edits.
---
**Upgrading from 1.6.1:** Sparkle will offer the update automatically. You can also trigger a check via **Scarf → Check for Updates…** or the menu bar icon.
+12 -12
View File
@@ -424,7 +424,7 @@
CODE_SIGN_ENTITLEMENTS = scarf/scarf.entitlements; CODE_SIGN_ENTITLEMENTS = scarf/scarf.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 17; CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_TEAM = 3Q6X2L86C4; DEVELOPMENT_TEAM = 3Q6X2L86C4;
ENABLE_APP_SANDBOX = NO; ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
@@ -436,7 +436,7 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 14.6; MACOSX_DEPLOYMENT_TARGET = 14.6;
MARKETING_VERSION = 1.6.1; MARKETING_VERSION = 1.6.2;
PRODUCT_BUNDLE_IDENTIFIER = com.scarf.app; PRODUCT_BUNDLE_IDENTIFIER = com.scarf.app;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES; REGISTER_APP_GROUPS = YES;
@@ -458,7 +458,7 @@
CODE_SIGN_ENTITLEMENTS = scarf/scarf.entitlements; CODE_SIGN_ENTITLEMENTS = scarf/scarf.entitlements;
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
COMBINE_HIDPI_IMAGES = YES; COMBINE_HIDPI_IMAGES = YES;
CURRENT_PROJECT_VERSION = 17; CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_TEAM = 3Q6X2L86C4; DEVELOPMENT_TEAM = 3Q6X2L86C4;
ENABLE_APP_SANDBOX = NO; ENABLE_APP_SANDBOX = NO;
ENABLE_HARDENED_RUNTIME = YES; ENABLE_HARDENED_RUNTIME = YES;
@@ -470,7 +470,7 @@
"@executable_path/../Frameworks", "@executable_path/../Frameworks",
); );
MACOSX_DEPLOYMENT_TARGET = 14.6; MACOSX_DEPLOYMENT_TARGET = 14.6;
MARKETING_VERSION = 1.6.1; MARKETING_VERSION = 1.6.2;
PRODUCT_BUNDLE_IDENTIFIER = com.scarf.app; PRODUCT_BUNDLE_IDENTIFIER = com.scarf.app;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
REGISTER_APP_GROUPS = YES; REGISTER_APP_GROUPS = YES;
@@ -488,11 +488,11 @@
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 17; CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_TEAM = 3Q6X2L86C4; DEVELOPMENT_TEAM = 3Q6X2L86C4;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 26.2; MACOSX_DEPLOYMENT_TARGET = 26.2;
MARKETING_VERSION = 1.6.1; MARKETING_VERSION = 1.6.2;
PRODUCT_BUNDLE_IDENTIFIER = com.scarfTests; PRODUCT_BUNDLE_IDENTIFIER = com.scarfTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO; STRING_CATALOG_GENERATE_SYMBOLS = NO;
@@ -509,11 +509,11 @@
buildSettings = { buildSettings = {
BUNDLE_LOADER = "$(TEST_HOST)"; BUNDLE_LOADER = "$(TEST_HOST)";
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 17; CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_TEAM = 3Q6X2L86C4; DEVELOPMENT_TEAM = 3Q6X2L86C4;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MACOSX_DEPLOYMENT_TARGET = 26.2; MACOSX_DEPLOYMENT_TARGET = 26.2;
MARKETING_VERSION = 1.6.1; MARKETING_VERSION = 1.6.2;
PRODUCT_BUNDLE_IDENTIFIER = com.scarfTests; PRODUCT_BUNDLE_IDENTIFIER = com.scarfTests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO; STRING_CATALOG_GENERATE_SYMBOLS = NO;
@@ -529,10 +529,10 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 17; CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_TEAM = 3Q6X2L86C4; DEVELOPMENT_TEAM = 3Q6X2L86C4;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.6.1; MARKETING_VERSION = 1.6.2;
PRODUCT_BUNDLE_IDENTIFIER = com.scarfUITests; PRODUCT_BUNDLE_IDENTIFIER = com.scarfUITests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO; STRING_CATALOG_GENERATE_SYMBOLS = NO;
@@ -548,10 +548,10 @@
isa = XCBuildConfiguration; isa = XCBuildConfiguration;
buildSettings = { buildSettings = {
CODE_SIGN_STYLE = Automatic; CODE_SIGN_STYLE = Automatic;
CURRENT_PROJECT_VERSION = 17; CURRENT_PROJECT_VERSION = 18;
DEVELOPMENT_TEAM = 3Q6X2L86C4; DEVELOPMENT_TEAM = 3Q6X2L86C4;
GENERATE_INFOPLIST_FILE = YES; GENERATE_INFOPLIST_FILE = YES;
MARKETING_VERSION = 1.6.1; MARKETING_VERSION = 1.6.2;
PRODUCT_BUNDLE_IDENTIFIER = com.scarfUITests; PRODUCT_BUNDLE_IDENTIFIER = com.scarfUITests;
PRODUCT_NAME = "$(TARGET_NAME)"; PRODUCT_NAME = "$(TARGET_NAME)";
STRING_CATALOG_GENERATE_SYMBOLS = NO; STRING_CATALOG_GENERATE_SYMBOLS = NO;
@@ -1356,10 +1356,16 @@ struct HermesFileService: Sendable {
return env return env
} }
/// True if any known AI-provider credential is reachable either already /// True if any known AI-provider credential is reachable. Hermes itself
/// in the current process env, present in the login-shell env we queried, /// resolves credentials from four locations at runtime, so the preflight
/// or present in `~/.hermes/.env`. Used by Chat to warn the user before /// mirrors that set to avoid false "no credentials" warnings:
/// `hermes acp` fails on send with "No Anthropic credentials found". /// 1. Current process env + login-shell env (queried once at startup)
/// 2. `~/.hermes/.env`
/// 3. `~/.hermes/auth.json` Credential Pools (v1.6+ blessed flow)
/// 4. `~/.hermes/config.yaml` embedded `api_key:` for auxiliary /
/// delegation tasks
/// Used by Chat to warn the user before `hermes acp` fails on send with
/// "No Anthropic credentials found".
nonisolated static func hasAnyAICredential() -> Bool { nonisolated static func hasAnyAICredential() -> Bool {
let credentialKeys = shellEnvKeys.filter { $0 != "PATH" && $0 != "ANTHROPIC_BASE_URL" && $0 != "OPENAI_BASE_URL" } let credentialKeys = shellEnvKeys.filter { $0 != "PATH" && $0 != "ANTHROPIC_BASE_URL" && $0 != "OPENAI_BASE_URL" }
let env = enrichedEnvironment() let env = enrichedEnvironment()
@@ -1386,6 +1392,36 @@ struct HermesFileService: Sendable {
} }
} }
} }
// Scan ~/.hermes/auth.json the Credential Pools file written by the
// Configure Credential Pools UI. Schema is
// { "credential_pool": { "<provider>": [ { "access_token": "...", ... }, ... ] } }
// Defensive parse: any malformed input falls through to the next check.
let authPath = HermesPaths.home + "/auth.json"
if let data = try? Data(contentsOf: URL(fileURLWithPath: authPath)),
let root = try? JSONSerialization.jsonObject(with: data) as? [String: Any],
let pool = root["credential_pool"] as? [String: Any] {
for (_, entries) in pool {
guard let list = entries as? [[String: Any]] else { continue }
for cred in list {
if let token = cred["access_token"] as? String, !token.isEmpty {
return true
}
}
}
}
// Scan ~/.hermes/config.yaml for `api_key:` lines with a non-empty
// value. Covers both `auxiliary.<task>.api_key` and `delegation.api_key`
// without needing to parse the YAML structure any leaf `api_key: ...`
// with a value means Hermes has a credential to fall back on.
if let text = try? String(contentsOfFile: HermesPaths.configYAML, encoding: .utf8) {
for line in text.split(separator: "\n") {
let trimmed = line.trimmingCharacters(in: .whitespaces)
guard trimmed.hasPrefix("api_key:") else { continue }
let value = trimmed.dropFirst("api_key:".count)
.trimmingCharacters(in: CharacterSet(charactersIn: "\"' "))
if !value.isEmpty { return true }
}
}
return false return false
} }
@@ -96,7 +96,7 @@ struct ChatView: View {
VStack(alignment: .leading, spacing: 2) { VStack(alignment: .leading, spacing: 2) {
Text("No AI provider credentials detected") Text("No AI provider credentials detected")
.font(.callout) .font(.callout)
Text("Add `ANTHROPIC_API_KEY` (or similar) to `~/.hermes/.env` or your shell profile, then restart Scarf.") Text("Add credentials in **Configure → Credential Pools**, set `ANTHROPIC_API_KEY` (or similar) in `~/.hermes/.env`, or export it in your shell profile, then restart Scarf.")
.font(.caption) .font(.caption)
.foregroundStyle(.secondary) .foregroundStyle(.secondary)
} }
+67 -44
View File
@@ -54,8 +54,6 @@ APPCAST_URL="https://awizemann.github.io/scarf/appcast.xml"
DOWNLOAD_URL_BASE="https://github.com/awizemann/scarf/releases/download" DOWNLOAD_URL_BASE="https://github.com/awizemann/scarf/releases/download"
REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" REPO_ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BUILD_DIR="$REPO_ROOT/build" BUILD_DIR="$REPO_ROOT/build"
ARCHIVE_PATH="$BUILD_DIR/scarf.xcarchive"
EXPORT_DIR="$BUILD_DIR/export"
EXPORT_OPTIONS="$REPO_ROOT/scripts/ExportOptions.plist" EXPORT_OPTIONS="$REPO_ROOT/scripts/ExportOptions.plist"
RELEASE_DIR="$REPO_ROOT/releases/v${VERSION}" RELEASE_DIR="$REPO_ROOT/releases/v${VERSION}"
GH_PAGES_WORKTREE="${GH_PAGES_WORKTREE:-$REPO_ROOT/.gh-pages-worktree}" GH_PAGES_WORKTREE="${GH_PAGES_WORKTREE:-$REPO_ROOT/.gh-pages-worktree}"
@@ -76,8 +74,14 @@ require_cmd gh
cd "$REPO_ROOT" cd "$REPO_ROOT"
# git must be clean and on main # git must be clean and on main. The one exception: the release dir
if [[ -n "$(git status --porcelain)" ]]; then # (releases/v<VERSION>/) may already exist and be untracked — the user may
# have written RELEASE_NOTES.md there ahead of time, and the rest of the dir
# is auto-populated + gitignored anyway. Git status abbreviates to the dir
# path when all contents are untracked, so the whitelist matches both forms.
ALLOW="^\?\? releases/v${VERSION}/"
DIRTY="$(git status --porcelain | grep -Ev "$ALLOW" || true)"
if [[ -n "$DIRTY" ]]; then
die "working tree not clean — commit or stash first" die "working tree not clean — commit or stash first"
fi fi
CUR_BRANCH="$(git rev-parse --abbrev-ref HEAD)" CUR_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
@@ -111,68 +115,86 @@ if [[ -f "$NOTES_FILE" ]]; then
fi fi
git commit -m "chore: Bump version to ${VERSION}" git commit -m "chore: Bump version to ${VERSION}"
# ---------- build ---------- # ---------- build variants ----------
# Each release produces two zips: a Universal binary (recommended — works on
# both Apple Silicon and Intel) and an ARM64-only variant (smaller download for
# users who know they're on M-series silicon). Each variant is independently
# notarized and stapled. The appcast only references the Universal zip since
# it works everywhere; ARM64 is an alternative manual download.
log "Clean build directory" log "Clean build directory"
rm -rf "$BUILD_DIR" rm -rf "$BUILD_DIR"
mkdir -p "$BUILD_DIR" mkdir -p "$BUILD_DIR" "$RELEASE_DIR"
log "Archive (universal arm64+x86_64)" # build_variant <label> <archs> <output_zip>
xcodebuild \ # label e.g. "Universal" or "ARM64" (used as subdir name + log prefix)
# archs e.g. "arm64 x86_64" or "arm64" (space-separated ARCHS value)
# output_zip absolute path where the stapled, distribution-ready zip is written
build_variant() {
local label="$1"
local archs="$2"
local out_zip="$3"
local variant_dir="$BUILD_DIR/$label"
local archive_path="$variant_dir/scarf.xcarchive"
local export_dir="$variant_dir/export"
local app_path="$export_dir/Scarf.app"
local notarize_zip="$variant_dir/Scarf-notarize.zip"
mkdir -p "$variant_dir"
log "[$label] Archive (archs: $archs)"
xcodebuild \
-project "$PROJECT" \ -project "$PROJECT" \
-scheme "$SCHEME" \ -scheme "$SCHEME" \
-configuration Release \ -configuration Release \
-archivePath "$ARCHIVE_PATH" \ -archivePath "$archive_path" \
-destination "generic/platform=macOS" \ -destination "generic/platform=macOS" \
ONLY_ACTIVE_ARCH=NO \ ONLY_ACTIVE_ARCH=NO \
ARCHS="arm64 x86_64" \ ARCHS="$archs" \
archive archive
log "Export signed .app" log "[$label] Export signed .app"
xcodebuild \ xcodebuild \
-exportArchive \ -exportArchive \
-archivePath "$ARCHIVE_PATH" \ -archivePath "$archive_path" \
-exportPath "$EXPORT_DIR" \ -exportPath "$export_dir" \
-exportOptionsPlist "$EXPORT_OPTIONS" -exportOptionsPlist "$EXPORT_OPTIONS"
# Xcode exports as scarf.app (PRODUCT_NAME = $TARGET_NAME = "scarf"). Rename the # Xcode exports as scarf.app (PRODUCT_NAME = $TARGET_NAME = "scarf"). Rename so
# wrapper to Scarf.app so users see properly-cased app in /Applications. Renaming # users see properly-cased Scarf.app in /Applications. Renaming the bundle
# the bundle directory does NOT invalidate the signature (codesign signs contents, # wrapper does NOT invalidate the signature codesign signs contents, not the
# not the wrapper folder name). # wrapper folder name.
if [[ -d "$EXPORT_DIR/scarf.app" && ! -d "$EXPORT_DIR/Scarf.app" ]]; then if [[ -d "$export_dir/scarf.app" && ! -d "$app_path" ]]; then
mv "$EXPORT_DIR/scarf.app" "$EXPORT_DIR/Scarf.app" mv "$export_dir/scarf.app" "$app_path"
fi fi
APP_PATH="$EXPORT_DIR/Scarf.app" [[ -d "$app_path" ]] || die "[$label] exported app not found at $app_path"
[[ -d "$APP_PATH" ]] || die "exported app not found at $APP_PATH"
# ---------- verify signature ---------- log "[$label] Verify signature"
log "Verify signature" codesign --verify --deep --strict --verbose=2 "$app_path"
codesign --verify --deep --strict --verbose=2 "$APP_PATH"
# spctl will fail here (not yet notarized) — that's fine, we check after stapling
spctl --assess --type execute --verbose "$APP_PATH" || true
# ---------- notarize ---------- log "[$label] Zip for notarization"
log "Zip for notarization" ditto -c -k --keepParent "$app_path" "$notarize_zip"
NOTARIZE_ZIP="$BUILD_DIR/Scarf-notarize.zip"
ditto -c -k --keepParent "$APP_PATH" "$NOTARIZE_ZIP"
log "Submit to notarytool (blocking)" log "[$label] Submit to notarytool (blocking)"
xcrun notarytool submit "$NOTARIZE_ZIP" \ xcrun notarytool submit "$notarize_zip" \
--keychain-profile "$NOTARY_PROFILE" \ --keychain-profile "$NOTARY_PROFILE" \
--wait \ --wait \
--timeout 30m --timeout 30m
log "Staple notarization ticket" log "[$label] Staple + validate"
xcrun stapler staple "$APP_PATH" xcrun stapler staple "$app_path"
xcrun stapler validate "$APP_PATH" xcrun stapler validate "$app_path"
spctl --assess --type execute --verbose "$app_path"
log "Final gatekeeper assessment" log "[$label] Package $(basename "$out_zip")"
spctl --assess --type execute --verbose "$APP_PATH" ditto -c -k --keepParent "$app_path" "$out_zip"
}
# ---------- package distribution artifacts ----------
log "Package distribution zips"
mkdir -p "$RELEASE_DIR"
UNIVERSAL_ZIP="$RELEASE_DIR/Scarf-v${VERSION}-Universal.zip" UNIVERSAL_ZIP="$RELEASE_DIR/Scarf-v${VERSION}-Universal.zip"
ditto -c -k --keepParent "$APP_PATH" "$UNIVERSAL_ZIP" ARM64_ZIP="$RELEASE_DIR/Scarf-v${VERSION}-ARM64.zip"
build_variant "Universal" "arm64 x86_64" "$UNIVERSAL_ZIP"
build_variant "ARM64" "arm64" "$ARM64_ZIP"
# ---------- sign appcast entry ---------- # ---------- sign appcast entry ----------
log "Sign appcast entry with EdDSA" log "Sign appcast entry with EdDSA"
@@ -241,7 +263,8 @@ fi
gh release create "v${VERSION}" \ gh release create "v${VERSION}" \
--title "Scarf v${VERSION}" \ --title "Scarf v${VERSION}" \
"${GH_FLAGS[@]}" \ "${GH_FLAGS[@]}" \
"$UNIVERSAL_ZIP" "$UNIVERSAL_ZIP" \
"$ARM64_ZIP"
# ---------- tag main (skipped for drafts) ---------- # ---------- tag main (skipped for drafts) ----------
if [[ $DRAFT -eq 0 ]]; then if [[ $DRAFT -eq 0 ]]; then