diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
index 41e3935..7d4838a 100644
--- a/.github/ISSUE_TEMPLATE/config.yml
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -2,8 +2,8 @@
blank_issues_enabled: false
contact_links:
- name: Question or discussion
- url: https://github.com/capsule-app/capsule/discussions
+ url: https://github.com/capsule-native/capsule/discussions
about: For usage questions and open-ended discussion, please use Discussions.
- name: Security vulnerability
- url: https://github.com/capsule-app/capsule/security/advisories/new
+ url: https://github.com/capsule-native/capsule/security/advisories/new
about: Report security issues privately via a GitHub Security Advisory — do not open a public issue.
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index f472180..158d7e9 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -22,6 +22,17 @@ jobs:
steps:
- uses: actions/checkout@v4
+ - name: Verify tag matches app version
+ if: startsWith(github.ref, 'refs/tags/')
+ run: |
+ tag="${GITHUB_REF_NAME#v}"
+ appver="$(/usr/libexec/PlistBuddy -c 'Print :CFBundleShortVersionString' App/Info.plist)"
+ if [ "$tag" != "$appver" ]; then
+ echo "::error::Tag v$tag does not match App/Info.plist CFBundleShortVersionString ($appver). Bump Info.plist and re-tag so the packaged artifacts carry the right version."
+ exit 1
+ fi
+ echo "Tag v$tag matches app version $appver."
+
- name: Select newest available Xcode
run: |
latest="$(ls -d /Applications/Xcode_*.app 2>/dev/null | sort -V | tail -1)"
@@ -72,6 +83,10 @@ jobs:
- name: Build, sign, notarize, staple, package, appcast
if: steps.signing.outputs.enabled == 'true'
+ env:
+ # Point the appcast's enclosure URLs at this tag's Release assets. Empty on non-tag
+ # (manual) runs, which makes generate_appcast fall back to relative filenames.
+ RELEASE_DOWNLOAD_URL_PREFIX: ${{ startsWith(github.ref, 'refs/tags/') && format('https://github.com/{0}/releases/download/{1}/', github.repository, github.ref_name) || '' }}
run: make release
# ── Unsigned path (forks / missing secrets) ──────────────────────────────
diff --git a/App/Info.plist b/App/Info.plist
index e400888..e407b51 100644
--- a/App/Info.plist
+++ b/App/Info.plist
@@ -40,9 +40,10 @@
Capsule uses the local network to communicate with container runtimes and services on this Mac.
-
+
SUFeedURL
- https://capsule-app.github.io/capsule/appcast.xml
+ https://capsule-native.github.io/appcast.xml
SUEnableAutomaticChecks
@@ -50,11 +51,11 @@
SUScheduledCheckInterval
86400
-
+
SUPublicEDKey
- REPLACE_WITH_SPARKLE_EDDSA_PUBLIC_KEY
+ SaJ9avcN8A1w5xrK7XmVidxCXX6swF0N/wsRfMIadcc=
diff --git a/Scripts/release/README.md b/Scripts/release/README.md
index e38e9cf..8318411 100644
--- a/Scripts/release/README.md
+++ b/Scripts/release/README.md
@@ -4,34 +4,37 @@ Capsule ships as a **Developer ID–signed, notarized, stapled** `.app`, distrib
for humans and a zip for Sparkle auto-updates. The Mac App Store is out of scope (the app is
unsandboxed to drive the `container` CLI).
-## One-time setup
-
-1. **Developer ID** — a “Developer ID Application” certificate in your login keychain, and your
- Team ID.
-2. **Notary credentials** — store them once as a keychain profile:
- ```sh
- xcrun notarytool store-credentials capsule-notary \
- --apple-id you@example.com --team-id ABCDE12345 \
- --password
- ```
-3. **Sparkle signing keys** — generate the EdDSA key pair once:
- ```sh
- # generate_keys ships with Sparkle (find it under .build/artifacts or set $SPARKLE_BIN)
- ./generate_keys
- ```
- Paste the printed **public** key into `App/Info.plist` → `SUPublicEDKey`. The **private** key
- stays in your keychain and never enters the repo.
-
-## Environment
+## How updates reach users
-| Var | Required | Meaning |
-| --- | --- | --- |
-| `TEAM_ID` | yes | Apple Developer Team ID |
-| `NOTARY_PROFILE` | yes | notarytool keychain profile name (e.g. `capsule-notary`) |
-| `DEVELOPER_ID_APP` | optional | explicit signing identity string |
-| `SPARKLE_BIN` | optional | dir containing `generate_appcast` / `generate_keys` |
-| `SPARKLE_ED_KEY_FILE` | optional | private-key file, if not using the keychain |
-| `DIST_DIR` | optional | output dir (default `dist/`) |
+1. Tagging `vX.Y.Z` runs [`.github/workflows/release.yml`](../../.github/workflows/release.yml)
+ on `capsule-native/capsule`: build → Developer-ID sign → notarize → staple → package →
+ **sign the appcast**.
+2. The workflow publishes a **GitHub Release** with `Capsule-.dmg`, `Capsule-.zip`, and
+ `appcast.xml`. The appcast's `` URLs point at that release's own asset download
+ URLs (via `--download-url-prefix`, set from the tag).
+3. A **Sync appcast** workflow in the Pages repo
+ [`capsule-native/capsule-native.github.io`](https://github.com/capsule-native/capsule-native.github.io)
+ pulls the latest release's `appcast.xml` and commits it, republishing the feed at
+ **** — the app's `SUFeedURL`. It runs on a
+ schedule (~15 min) and on demand (Actions ▸ Sync appcast ▸ Run workflow) for an instant
+ publish right after a release.
+
+> The code repo stays **public** so Sparkle can download the update zip from the release assets
+> without authentication.
+
+## Signing
+
+Signed, notarized releases require Developer-ID + notarization credentials held by the
+maintainer, plus a Sparkle EdDSA key whose public half is baked into `App/Info.plist`
+(`SUPublicEDKey`). These are configured as repository secrets and their setup is intentionally
+**not** documented in this public repo. When the signing secrets are absent (a fork, or a PR from
+one), the workflow still **builds and validates** the whole pipeline but skips
+notarization/publishing — it never fails for want of credentials, so contributors can run it.
+
+> Pipeline behaviour worth knowing: `generate_appcast` writes `sparkle:edSignature` **only when
+> the app's embedded `SUPublicEDKey` matches the signing key's public key** — otherwise it emits
+> an *unsigned* enclosure with no error, and every client silently rejects the update. Keep the
+> Info.plist public key and the signing key in lockstep.
## Run it
@@ -48,20 +51,32 @@ Individual steps (each accepts `--dry-run`):
| 2 | `export.sh` | `make export` | `xcodebuild -exportArchive` (Developer ID) → signed `dist/Capsule.app` (inside-out, incl. Sparkle; Hardened Runtime) |
| 3 | `notarize.sh` | `make notarize` | zip + `notarytool submit --wait` + `stapler staple` |
| 4 | `package.sh` | `make package` | `dist/Capsule-.zip` (Sparkle) + `dist/Capsule-.dmg` |
-| 5 | `appcast.sh` | `make appcast` | Sparkle `generate_appcast` signs the zip + writes `appcast.xml` |
+| 5 | `appcast.sh` | `make appcast` | Sparkle `generate_appcast` signs the zip + writes `appcast.xml` (with `RELEASE_DOWNLOAD_URL_PREFIX`) |
+
+## Environment (local runs)
+
+| Var | Required | Meaning |
+| --- | --- | --- |
+| `TEAM_ID` | signed runs | Apple Developer Team ID |
+| `NOTARY_PROFILE` | signed runs | `notarytool` keychain profile name |
+| `RELEASE_DOWNLOAD_URL_PREFIX` | CI-set | prepended to the appcast's enclosure filenames — the tag's release-download URL. Empty → filenames stay relative to the appcast. |
+| `SPARKLE_BIN` | optional | dir containing `generate_appcast` / `generate_keys` |
+| `SPARKLE_ED_KEY_FILE` | optional | private-key file; else the login keychain |
+| `DIST_DIR` | optional | output dir (default `dist/`) |
-## CI
+## Cutting a release
-`.github/workflows/release.yml` runs the same pipeline on a `v*` tag, importing the Developer
-ID cert + notary creds from repository secrets, then publishes the artifacts and `appcast.xml`.
-The signing/notarization steps are skipped automatically when the secrets are absent (e.g. on a
-fork), so the workflow never fails for want of credentials.
+1. Bump `CFBundleShortVersionString` **and** `CFBundleVersion` in `App/Info.plist`; commit + push.
+2. `git tag vX.Y.Z && git push origin vX.Y.Z`. The tag `vX.Y.Z` **must** match the Info.plist
+ version — the workflow's *Verify tag matches app version* step fails the build otherwise (no
+ more `v1.2.3` tags that package `Capsule-0.1.0.zip`).
+3. The workflow builds, signs, notarizes, and publishes the Release. The Pages repo picks up the
+ new appcast automatically within ~15 min, or immediately if you run **Sync appcast** there.
## Notes
- **Version comes from the app, not the tag.** Artifact names and the Sparkle appcast use
- `CFBundleShortVersionString` from `App/Info.plist`. Bump it (and `CFBundleVersion`) and commit
- **before** tagging `vX.Y.Z`, or a `v1.2.3` tag will still package `Capsule-0.1.0.zip`.
+ `CFBundleShortVersionString` from `App/Info.plist`; the tag guard just enforces they agree.
- **Notary password on argv.** `notarize.sh` / `release.yml` pass the app-specific password to
`notarytool store-credentials --password` (the API has no stdin form). It's masked and never
echoed; on the ephemeral, single-tenant `macos-26` runner nothing else can read the process
@@ -70,7 +85,7 @@ fork), so the workflow never fails for want of credentials.
## Why archive→export instead of `codesign --deep`
-`xcodebuild -exportArchive` signs embedded code inside-out — including Sparkle’s nested
+`xcodebuild -exportArchive` signs embedded code inside-out — including Sparkle's nested
`Autoupdate`, `Updater.app`, and XPC services — with the Hardened Runtime and a secure
timestamp, which is what the notary service demands. `codesign --deep` is discouraged by Apple
and easy to get subtly wrong for a framework with nested helpers.
diff --git a/Scripts/release/appcast.sh b/Scripts/release/appcast.sh
index ad3d4d0..bb7b408 100755
--- a/Scripts/release/appcast.sh
+++ b/Scripts/release/appcast.sh
@@ -37,15 +37,25 @@ if [ -z "$GEN" ]; then
fi
fi
+# Optional: prefix the bare enclosure filenames with the public download URL (e.g. the GitHub
+# Release asset base for this tag). Without it, generate_appcast writes filenames relative to
+# the appcast's own location — wrong here, since the appcast is hosted on the Pages repo but the
+# zips live on the code repo's Release. The release workflow sets this to the tag's download URL.
+PREFIX_ARGS=()
+if [ -n "${RELEASE_DOWNLOAD_URL_PREFIX:-}" ]; then
+ PREFIX_ARGS=(--download-url-prefix "$RELEASE_DOWNLOAD_URL_PREFIX")
+ log "Enclosure URL prefix: $RELEASE_DOWNLOAD_URL_PREFIX"
+fi
+
log "Signing artifacts + generating appcast with: $GEN"
if [ -n "${SPARKLE_ED_KEY_FILE:-}" ]; then
- run "$GEN" --ed-key-file "$SPARKLE_ED_KEY_FILE" "$DIST_DIR"
+ run "$GEN" --ed-key-file "$SPARKLE_ED_KEY_FILE" ${PREFIX_ARGS[@]+"${PREFIX_ARGS[@]}"} "$DIST_DIR"
else
# No key file → generate_appcast reads the private key from the login keychain.
- run "$GEN" "$DIST_DIR"
+ run "$GEN" ${PREFIX_ARGS[@]+"${PREFIX_ARGS[@]}"} "$DIST_DIR"
fi
log "appcast written → $DIST_DIR/appcast.xml"
if [ "$DRY_RUN" != "1" ] && [ -f "$DIST_DIR/appcast.xml" ]; then
- log "Host this file at the URL in App/Info.plist's SUFeedURL."
+ log "Attached to the GitHub Release; the Pages repo's sync workflow publishes it at SUFeedURL."
fi