Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 8 additions & 29 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
28 changes: 25 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
55 changes: 55 additions & 0 deletions RELEASE.md
Original file line number Diff line number Diff line change
@@ -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 <submission-id> --keychain-profile RSYNCGUI_NOTARY`
to see which binary failed (usually an un-signed nested helper — the export step signs everything,
so this is rare).
12 changes: 12 additions & 0 deletions scripts/ExportOptions.plist
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
<?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>
<key>method</key>
<string>developer-id</string>
<key>teamID</key>
<string>QRRCB8HB3W</string>
<key>signingStyle</key>
<string>automatic</string>
</dict>
</plist>
82 changes: 82 additions & 0 deletions scripts/release.sh
Original file line number Diff line number Diff line change
@@ -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 <app-specific-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."