# Signing & distribution

How Nucleic is code-signed and shipped:

- **macOS** is distributed **directly** (not via the App Store): Developer-ID-signed,
  notarized, stapled **DMGs**, with in-app **Sparkle** auto-update per channel
  (beta / rc / stable).
- **iOS** (`NucleicRemote`) goes through the **App Store**: archived, uploaded to App
  Store Connect, distributed as **TestFlight** betas/RCs and promoted to store releases.

Everything here uses the **paid team `L7UDTQ6F5W`**. (A leftover free-tier
`Apple Development: andrew@blakeslee.me` cert under team `X7ZT893MPV` may also be in the
keychain — it's unrelated to distribution and isn't used by any of this.)

---

## One-time setup

### 1. Certificates (in Xcode → Settings → Accounts → team `L7UDTQ6F5W` → Manage Certificates → +)

| Certificate                  | Used for                          |
| ---------------------------- | --------------------------------- |
| **Developer ID Application** | signing the macOS app + DMG       |
| **Apple Distribution**       | signing the iOS App Store build   |

(Optionally **Developer ID Installer** if you ever ship a `.pkg` instead of a DMG.)

> The macOS app's only entitlement, `com.apple.security.virtualization` (booting Linux VMs
> in-process), is **not** a restricted/managed entitlement — it needs no special Apple
> approval and **no provisioning profile**. Developer ID + notarization is sufficient. (The
> restricted entitlement we deliberately avoid is `com.apple.vm.networking`.) Developer ID
> distribution needs no App ID registration or provisioning profiles at all — that's only the
> iOS/App Store side.

### 2. App Store Connect API key (used by notarization **and** iOS upload)

App Store Connect → **Users and Access → Integrations → Keys** → generate a key with
**App Manager** access. Save three things:

- the `AuthKey_XXXX.p8` file (download once — it can't be re-downloaded),
- the **Key ID**,
- the **Issuer ID**.

Keep the `.p8` out of the repo (e.g. `~/.appstoreconnect/private_keys/`).

### 3. Notary credential (local convenience — wraps the key above)

```sh
xcrun notarytool store-credentials nucleic-notary \
  --key   ~/.appstoreconnect/private_keys/AuthKey_XXXX.p8 \
  --key-id   <KeyID> \
  --issuer   <IssuerID>
```

`scripts/notarize.sh` defaults to the profile `nucleic-notary`.

### 4. Sparkle update-signing key (macOS auto-update)

Generate the EdDSA key pair once with Sparkle's `generate_keys` (the private key lands in
your login keychain; the public key is printed). Commit the **public** half so packaging can
embed it:

```sh
generate_keys                                   # prints the public key; private key → keychain
echo '<the-printed-public-key>' > signing/sparkle_public_ed_key
```

`scripts/package-app.sh` reads `signing/sparkle_public_ed_key` and bakes it into beta/rc/stable
builds as `SUPublicEDKey`. (Override per-build with `NUCLEIC_SPARKLE_PUB`.) The private key never
leaves the keychain; `generate_appcast` uses it to sign each update.

### 5. iOS App Store record

Register the App ID **`xyz.blakeslee.nucleic.remote`** (Developer portal → Identifiers) and create the
matching **app record** in App Store Connect. Fill in the TestFlight test info before adding
external testers (external groups need a one-time Beta App Review).

### 6. Update-hosting location (macOS appcast + DMGs)

Updates are hosted on **Cloudflare R2** behind a custom domain; the embedded feed URL defaults to

```
https://updates.nucleic.blakeslee.xyz
```

Provision once: create an R2 bucket `nucleic-updates` and attach the custom domain
`updates.nucleic.blakeslee.xyz` (R2 → bucket → Settings → Custom Domains). `release-macos.sh`
publishes each release there via `scripts/upload-r2.sh` (needs `wrangler login` or
`CLOUDFLARE_API_TOKEN` with R2 write). The GitHub release tagged `appcast` remains a documented
fallback origin.

Object layout — DMGs are foldered by channel for tidy download links; the appcast feeds stay at
the root (where each build's baked-in `SUFeedURL` points them):

```
appcast-beta.xml                  the Sparkle feed (root)
beta/Nucleic-Beta-<ver>.dmg       the download → updates.nucleic.blakeslee.xyz/beta/Nucleic-Beta-<ver>.dmg
beta/latest                       302 → newest beta DMG, via the cloud/nucleic-updates Worker
```

For a stable "always newest" link (website/docs), use `…/<channel>/latest` — deploy
`cloud/nucleic-updates` once (see `docs/CLOUD_INFRA.md` §1.1).

Override the base anytime with `NUCLEIC_FEED_BASE` — it must match between `package-app.sh`
(which bakes `SUFeedURL` in) and `generate-appcast.sh` (which writes the enclosure URLs). Because
`SUFeedURL` is baked into every shipped build permanently, keep this domain stable.

---

## Releasing

### macOS (per channel)

```sh
make release-beta        # or release-rc / release-stable
```

Each runs `scripts/release-macos.sh <channel>`, which: builds → Developer-ID-signs the `.app`
→ notarizes + staples it → builds + signs the DMG → notarizes + staples the DMG → regenerates
the channel appcast (`dist/appcast-<channel>.xml`). Then **upload** the new DMG and the appcast
XML to the `appcast` GitHub release. Existing installs pick up the update via Sparkle; new users
download the DMG.

### iOS (TestFlight → App Store)

```sh
make ios-release          # archive + upload to App Store Connect
```

The build appears in TestFlight after processing. Assign it to a beta/RC tester group, or submit
it for review to ship a store release. `scripts/ios-release.sh --archive-only` builds the archive
without uploading (then use Xcode Organizer).

---

## Credentials: local vs CI

The scripts read credentials from the environment, so the same commands work locally and in CI.

| Need                       | Local                                   | CI (env / secrets)                                   |
| -------------------------- | --------------------------------------- | ---------------------------------------------------- |
| Code-sign identity         | keychain (auto-detected)                | import the cert `.p12` into a keychain; `NUCLEIC_SIGN_ID` |
| Notarization               | stored profile `nucleic-notary`         | `NUCLEIC_ASC_KEY_PATH` / `NUCLEIC_ASC_KEY_ID` / `NUCLEIC_ASC_ISSUER_ID` |
| iOS upload                 | same API key                            | same three `NUCLEIC_ASC_*` vars                      |
| Sparkle signing            | private key in keychain                 | import it, or sign appcast on a trusted machine      |

Useful overrides: `NUCLEIC_SIGN_ID`, `NUCLEIC_VERSION`, `NUCLEIC_FEED_BASE`,
`NUCLEIC_NOTARY_PROFILE`, `NUCLEIC_NOTARIZE_APP`, `NUCLEIC_APPCAST`, `NUCLEIC_IOS_BUILD`.

CI runs on a **macOS runner** (signing + notarization need macOS tooling). The existing
`.github/workflows/*.yml` build Linux container images and don't sign anything — a release
workflow would be a separate macOS job calling these same scripts.
