From bc6966467689d3312e6d86027f2c9bb30fb21e3b Mon Sep 17 00:00:00 2001 From: Jordan Koch Date: Tue, 18 Aug 2026 14:39:32 -0700 Subject: [PATCH] chore: add notarized-release toolchain and fix CI MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirror the notarized-release pipeline merged into MBox-Explorer: - scripts/release.sh: archive → export (Developer ID) → DMG → notarize → staple → verify, driven by SCHEME/APP_NAME/TEAM_ID and the RSYNCGUI_NOTARY notary keychain profile. - scripts/ExportOptions.plist: developer-id export with team QRRCB8HB3W. - RELEASE.md: one-time setup + release instructions. - README Installation: DMG steps plus the Gatekeeper workaround (macOS 14 Control-click→Open vs 15/26 System Settings→Privacy & Security→Open Anyway, plus xattr quarantine removal) and a From Source section. - .github/workflows/build.yml: run `xcodebuild test` for the RsyncGUI scheme with code signing disabled. RsyncGUI is pure Swift — its AI insights talk to Ollama/MLX over HTTP and it compiles no mlx-swift dependency — so the Metal Toolchain step is omitted from both the README and CI. Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01QrPsErCe9kBuRRPyDSA5cF --- .github/workflows/build.yml | 37 ++++------------- README.md | 28 +++++++++++-- RELEASE.md | 55 +++++++++++++++++++++++++ scripts/ExportOptions.plist | 12 ++++++ scripts/release.sh | 82 +++++++++++++++++++++++++++++++++++++ 5 files changed, 182 insertions(+), 32 deletions(-) create mode 100644 RELEASE.md create mode 100644 scripts/ExportOptions.plist create mode 100755 scripts/release.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 6b82894..bd374fb 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -7,45 +7,24 @@ on: branches: [ main, master ] jobs: - build: + build-and-test: runs-on: macos-15 steps: - uses: actions/checkout@v7 - - name: Build + - name: Build & Test run: | XCODEPROJ=$(find . -name "*.xcodeproj" -maxdepth 2 | head -1) - if [ -z "$XCODEPROJ" ]; then - echo "No Xcode project found" - exit 1 - fi - - SCHEME=$(xcodebuild -list -project "$XCODEPROJ" 2>/dev/null | awk '/Schemes:/{found=1; next} found && NF{print; exit}' | xargs) - if [ -z "$SCHEME" ]; then - SCHEME=$(basename "$XCODEPROJ" .xcodeproj) - fi - - echo "Building scheme: $SCHEME" - xcodebuild build \ - -project "$XCODEPROJ" \ - -scheme "$SCHEME" \ - -destination 'platform=macOS' \ - CODE_SIGNING_ALLOWED=NO \ - | tail -20 - - - name: Test - run: | - XCODEPROJ=$(find . -name "*.xcodeproj" -maxdepth 2 | head -1) - SCHEME=$(xcodebuild -list -project "$XCODEPROJ" 2>/dev/null | awk '/Schemes:/{found=1; next} found && NF{print; exit}' | xargs) - if [ -z "$SCHEME" ]; then - SCHEME=$(basename "$XCODEPROJ" .xcodeproj) - fi - + if [ -z "$XCODEPROJ" ]; then echo "No Xcode project found"; exit 1; fi + SCHEME="RsyncGUI" echo "Testing scheme: $SCHEME" + set -o pipefail xcodebuild test \ -project "$XCODEPROJ" \ -scheme "$SCHEME" \ -destination 'platform=macOS' \ CODE_SIGNING_ALLOWED=NO \ - | tail -50 + CODE_SIGNING_REQUIRED=NO \ + SWIFT_TREAT_WARNINGS_AS_ERRORS=NO \ + | tail -60 diff --git a/README.md b/README.md index 1e91b12..1c8ca73 100644 --- a/README.md +++ b/README.md @@ -169,9 +169,31 @@ A related destination-picker fix already shipped in **1.7.3**, so updating from ## Installation -1. Download the latest DMG from [Releases](https://github.com/kochj23/RsyncGUI/releases/latest) -2. Open the DMG and drag RsyncGUI.app to `/Applications` -3. No sandbox -- full file system access for unrestricted rsync operation +### From DMG (recommended for most users) + +1. Download the latest `.dmg` from [Releases](https://github.com/kochj23/RsyncGUI/releases/latest). +2. Open it and drag **RsyncGUI** into your **Applications** folder. +3. Launch it from Applications. No sandbox — full file system access for unrestricted rsync operation. + +> **See "RsyncGUI can't be opened because the developer cannot be verified"?** +> That means you have a build that isn't yet Developer-ID-signed **and** notarized. To open it anyway: +> - **macOS 14 and earlier:** Control-click (right-click) the app → **Open** → **Open**. +> - **macOS 15 (Sequoia) / 26 and later:** double-click it, dismiss the dialog, then open **System Settings → Privacy & Security**, scroll down, and click **Open Anyway**. +> - Or from Terminal: `xattr -dr com.apple.quarantine "/Applications/RsyncGUI.app"` +> +> **Notarized releases open with no prompt at all** — maintainers, see [RELEASE.md](RELEASE.md). + +### From Source + +Requires **Xcode 16 or later**. RsyncGUI is pure Swift with no third-party package dependencies, so there +is nothing extra to install: + +```bash +git clone git@github.com:kochj23/RsyncGUI.git +cd RsyncGUI +open RsyncGUI.xcodeproj +# Build & run: Cmd+R +``` ## Requirements diff --git a/RELEASE.md b/RELEASE.md new file mode 100644 index 0000000..be8630c --- /dev/null +++ b/RELEASE.md @@ -0,0 +1,55 @@ +# Releasing RsyncGUI + +The goal: ship a `.dmg` that end users can **open with a simple drag-and-drop, no Gatekeeper +warnings**. On macOS that requires the app to be **signed with a "Developer ID Application" +certificate** *and* **notarized by Apple**. A build signed only with an "Apple Development" +certificate (the default for local debug builds) is **rejected by Gatekeeper** and gives users the +dreaded *"the developer cannot be verified"* dialog. + +`scripts/release.sh` automates the whole pipeline: **archive → export (Developer ID) → DMG → +notarize → staple → verify**. + +## One-time setup + +1. **Developer ID Application certificate** (needs a paid Apple Developer account, team `QRRCB8HB3W`). + In Xcode: *Settings → Accounts → Manage Certificates → + → Developer ID Application*. Confirm it's + installed: + ```bash + security find-identity -v -p codesigning | grep "Developer ID Application" + ``` + +2. **Notary credentials as a keychain profile** (so no secrets live in the repo). Create an + [app-specific password](https://support.apple.com/en-us/102654) for your Apple ID, then: + ```bash + xcrun notarytool store-credentials RSYNCGUI_NOTARY \ + --apple-id "you@digitalnoise.net" \ + --team-id QRRCB8HB3W \ + --password "abcd-efgh-ijkl-mnop" # the app-specific password + ``` + +## Cut a release + +```bash +./scripts/release.sh 2.3.0 # or omit the version to use today's date +``` + +The script prints the finished path, e.g. `build/RsyncGUI-2.3.0.dmg`. Upload that `.dmg` to a +[GitHub Release](https://github.com/kochj23/RsyncGUI/releases). Users drag it to Applications and +it launches clean — the stapled notarization ticket means it works even offline. + +## Overrides (env vars) + +| Var | Default | Purpose | +|---|---|---| +| `SCHEME` | `RsyncGUI` | Xcode scheme to archive | +| `CONFIG` | `Release` | Build configuration | +| `TEAM_ID` | `QRRCB8HB3W` | Apple Developer team | +| `NOTARY_PROFILE` | `RSYNCGUI_NOTARY` | `notarytool` keychain profile name | + +## Troubleshooting + +- **`error: No signing certificate "Developer ID Application" found`** → step 1 above isn't done. +- **`Error: No Keychain password item found for profile: RSYNCGUI_NOTARY`** → step 2 above isn't done. +- **Notarization status `Invalid`** → run `xcrun notarytool log --keychain-profile RSYNCGUI_NOTARY` + to see which binary failed (usually an un-signed nested helper — the export step signs everything, + so this is rare). diff --git a/scripts/ExportOptions.plist b/scripts/ExportOptions.plist new file mode 100644 index 0000000..17496da --- /dev/null +++ b/scripts/ExportOptions.plist @@ -0,0 +1,12 @@ + + + + + method + developer-id + teamID + QRRCB8HB3W + signingStyle + automatic + + diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..badf354 --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,82 @@ +#!/usr/bin/env bash +# +# release.sh — build, sign (Developer ID), notarize, staple, and package RsyncGUI +# as a Gatekeeper-clean .dmg that end users can open with a simple drag-and-drop. +# +# One-time setup is documented in RELEASE.md: +# 1. A "Developer ID Application" certificate in your login keychain. +# 2. Notary credentials stored as a keychain profile named "$NOTARY_PROFILE": +# xcrun notarytool store-credentials RSYNCGUI_NOTARY \ +# --apple-id "you@example.com" --team-id QRRCB8HB3W --password +# +# Usage: ./scripts/release.sh [version] (version defaults to today's date) +# +set -euo pipefail + +cd "$(dirname "$0")/.." + +PROJECT="RsyncGUI.xcodeproj" +SCHEME="${SCHEME:-RsyncGUI}" +CONFIG="${CONFIG:-Release}" +TEAM_ID="${TEAM_ID:-QRRCB8HB3W}" +NOTARY_PROFILE="${NOTARY_PROFILE:-RSYNCGUI_NOTARY}" +APP_NAME="RsyncGUI" +VERSION="${1:-$(date +%Y.%m.%d)}" + +BUILD_DIR="$(pwd)/build" +ARCHIVE="$BUILD_DIR/${APP_NAME}.xcarchive" +EXPORT_DIR="$BUILD_DIR/export" +DMG="$BUILD_DIR/${APP_NAME// /-}-${VERSION}.dmg" + +# ---- Preflight --------------------------------------------------------------- +echo "==> Preflight checks" +if ! security find-identity -v -p codesigning | grep -q "Developer ID Application"; then + echo "ERROR: no 'Developer ID Application' certificate found (see RELEASE.md step 1)." >&2 + exit 1 +fi +if ! xcrun notarytool history --keychain-profile "$NOTARY_PROFILE" >/dev/null 2>&1; then + echo "ERROR: notary profile '$NOTARY_PROFILE' not set up (see RELEASE.md step 2)." >&2 + exit 1 +fi + +rm -rf "$BUILD_DIR"; mkdir -p "$BUILD_DIR" + +# ---- Archive ----------------------------------------------------------------- +echo "==> Archiving ($CONFIG)…" +xcodebuild -project "$PROJECT" -scheme "$SCHEME" -configuration "$CONFIG" \ + -destination "generic/platform=macOS" \ + -archivePath "$ARCHIVE" archive + +# ---- Export (Developer ID signed) ------------------------------------------- +echo "==> Exporting with Developer ID…" +xcodebuild -exportArchive -archivePath "$ARCHIVE" \ + -exportPath "$EXPORT_DIR" \ + -exportOptionsPlist "scripts/ExportOptions.plist" + +APP="$EXPORT_DIR/${APP_NAME}.app" +[ -d "$APP" ] || { echo "ERROR: export failed — $APP not found." >&2; exit 1; } + +# ---- Package DMG ------------------------------------------------------------- +echo "==> Building DMG…" +STAGE="$(mktemp -d)" +cp -R "$APP" "$STAGE/" +ln -s /Applications "$STAGE/Applications" +hdiutil create -volname "$APP_NAME" -srcfolder "$STAGE" -ov -format UDZO "$DMG" >/dev/null +rm -rf "$STAGE" + +# ---- Notarize + staple ------------------------------------------------------- +echo "==> Notarizing (uploads to Apple and waits — usually a few minutes)…" +xcrun notarytool submit "$DMG" --keychain-profile "$NOTARY_PROFILE" --wait + +echo "==> Stapling the notarization ticket…" +xcrun stapler staple "$DMG" + +# ---- Verify ------------------------------------------------------------------ +echo "==> Verifying…" +codesign --verify --deep --strict --verbose=2 "$APP" +spctl -a -t open --context context:primary-signature -vv "$DMG" || true + +echo +echo "✅ Done: $DMG" +echo " Upload this .dmg to a GitHub Release. It opens with no Gatekeeper prompt," +echo " even offline, because the notarization ticket is stapled."