mirror of
https://github.com/awizemann/scarf.git
synced 2026-05-10 18:44:45 +00:00
Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 12610faba0 | |||
| 73b44202ba | |||
| eed55cbb0f | |||
| 14c97bee62 | |||
| 8d3fe70e2c | |||
| da88c98c7a |
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
+78
-55
@@ -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)
|
||||||
-project "$PROJECT" \
|
# archs e.g. "arm64 x86_64" or "arm64" (space-separated ARCHS value)
|
||||||
-scheme "$SCHEME" \
|
# output_zip absolute path where the stapled, distribution-ready zip is written
|
||||||
-configuration Release \
|
build_variant() {
|
||||||
-archivePath "$ARCHIVE_PATH" \
|
local label="$1"
|
||||||
-destination "generic/platform=macOS" \
|
local archs="$2"
|
||||||
ONLY_ACTIVE_ARCH=NO \
|
local out_zip="$3"
|
||||||
ARCHS="arm64 x86_64" \
|
local variant_dir="$BUILD_DIR/$label"
|
||||||
archive
|
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"
|
||||||
|
|
||||||
log "Export signed .app"
|
mkdir -p "$variant_dir"
|
||||||
xcodebuild \
|
|
||||||
-exportArchive \
|
|
||||||
-archivePath "$ARCHIVE_PATH" \
|
|
||||||
-exportPath "$EXPORT_DIR" \
|
|
||||||
-exportOptionsPlist "$EXPORT_OPTIONS"
|
|
||||||
|
|
||||||
# Xcode exports as scarf.app (PRODUCT_NAME = $TARGET_NAME = "scarf"). Rename the
|
log "[$label] Archive (archs: $archs)"
|
||||||
# wrapper to Scarf.app so users see properly-cased app in /Applications. Renaming
|
xcodebuild \
|
||||||
# the bundle directory does NOT invalidate the signature (codesign signs contents,
|
-project "$PROJECT" \
|
||||||
# not the wrapper folder name).
|
-scheme "$SCHEME" \
|
||||||
if [[ -d "$EXPORT_DIR/scarf.app" && ! -d "$EXPORT_DIR/Scarf.app" ]]; then
|
-configuration Release \
|
||||||
mv "$EXPORT_DIR/scarf.app" "$EXPORT_DIR/Scarf.app"
|
-archivePath "$archive_path" \
|
||||||
fi
|
-destination "generic/platform=macOS" \
|
||||||
APP_PATH="$EXPORT_DIR/Scarf.app"
|
ONLY_ACTIVE_ARCH=NO \
|
||||||
[[ -d "$APP_PATH" ]] || die "exported app not found at $APP_PATH"
|
ARCHS="$archs" \
|
||||||
|
archive
|
||||||
|
|
||||||
# ---------- verify signature ----------
|
log "[$label] Export signed .app"
|
||||||
log "Verify signature"
|
xcodebuild \
|
||||||
codesign --verify --deep --strict --verbose=2 "$APP_PATH"
|
-exportArchive \
|
||||||
# spctl will fail here (not yet notarized) — that's fine, we check after stapling
|
-archivePath "$archive_path" \
|
||||||
spctl --assess --type execute --verbose "$APP_PATH" || true
|
-exportPath "$export_dir" \
|
||||||
|
-exportOptionsPlist "$EXPORT_OPTIONS"
|
||||||
|
|
||||||
# ---------- notarize ----------
|
# Xcode exports as scarf.app (PRODUCT_NAME = $TARGET_NAME = "scarf"). Rename so
|
||||||
log "Zip for notarization"
|
# users see properly-cased Scarf.app in /Applications. Renaming the bundle
|
||||||
NOTARIZE_ZIP="$BUILD_DIR/Scarf-notarize.zip"
|
# wrapper does NOT invalidate the signature — codesign signs contents, not the
|
||||||
ditto -c -k --keepParent "$APP_PATH" "$NOTARIZE_ZIP"
|
# wrapper folder name.
|
||||||
|
if [[ -d "$export_dir/scarf.app" && ! -d "$app_path" ]]; then
|
||||||
|
mv "$export_dir/scarf.app" "$app_path"
|
||||||
|
fi
|
||||||
|
[[ -d "$app_path" ]] || die "[$label] exported app not found at $app_path"
|
||||||
|
|
||||||
log "Submit to notarytool (blocking)"
|
log "[$label] Verify signature"
|
||||||
xcrun notarytool submit "$NOTARIZE_ZIP" \
|
codesign --verify --deep --strict --verbose=2 "$app_path"
|
||||||
--keychain-profile "$NOTARY_PROFILE" \
|
|
||||||
--wait \
|
|
||||||
--timeout 30m
|
|
||||||
|
|
||||||
log "Staple notarization ticket"
|
log "[$label] Zip for notarization"
|
||||||
xcrun stapler staple "$APP_PATH"
|
ditto -c -k --keepParent "$app_path" "$notarize_zip"
|
||||||
xcrun stapler validate "$APP_PATH"
|
|
||||||
|
|
||||||
log "Final gatekeeper assessment"
|
log "[$label] Submit to notarytool (blocking)"
|
||||||
spctl --assess --type execute --verbose "$APP_PATH"
|
xcrun notarytool submit "$notarize_zip" \
|
||||||
|
--keychain-profile "$NOTARY_PROFILE" \
|
||||||
|
--wait \
|
||||||
|
--timeout 30m
|
||||||
|
|
||||||
|
log "[$label] Staple + validate"
|
||||||
|
xcrun stapler staple "$app_path"
|
||||||
|
xcrun stapler validate "$app_path"
|
||||||
|
spctl --assess --type execute --verbose "$app_path"
|
||||||
|
|
||||||
|
log "[$label] Package $(basename "$out_zip")"
|
||||||
|
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
|
||||||
|
|||||||
Reference in New Issue
Block a user