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
100 changes: 100 additions & 0 deletions .github/workflows/release-ios-testflight.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
name: Release — iOS TestFlight (Lisa Pocket)

# Build Lisa Pocket (native SwiftUI, packaging/ios-companion) and upload it to
# TestFlight. The native-app analogue of Markup's EAS submit; modeled on the
# secret-gated, ephemeral-keychain pattern of release-mac-apps.yml.
#
# Runs automatically WHEN the App Store Connect API-key secrets are populated
# (gated on ASC_KEY_ID). When they're missing it's a no-op with a hint, so the
# repo stays green for contributors without Apple credentials.
#
# Required secrets (see packaging/ios-companion/RELEASE.md):
# ASC_KEY_ID App Store Connect API Key ID.
# ASC_ISSUER_ID API Key Issuer ID (UUID).
# ASC_API_KEY_BASE64 the AuthKey_<id>.p8, base64-encoded:
# base64 -i AuthKey_XXXX.p8 | pbcopy
# IOS_DIST_CERT_BASE64 an "Apple Distribution" cert .p12 (cert + key), base64.
# IOS_DIST_CERT_PASSWORD passphrase set when exporting that .p12.
# APPLE_TEAM_ID 10-char team id (reused from the mac workflow).

on:
push:
tags:
- "pocket-v*.*.*"
workflow_dispatch:
inputs:
build_number:
description: "Override build number (default: timestamp)"
required: false

permissions:
contents: read

jobs:
testflight:
runs-on: macos-latest
timeout-minutes: 45
env:
# GitHub Actions forbids secrets.* in step-level `if:`, so promote the
# gate to a job-level env var. "true" iff the ASC API key is wired.
HAS_IOS_SIGNING: ${{ secrets.ASC_KEY_ID != '' }}
steps:
- uses: actions/checkout@v6

- name: Skip notice (no Apple credentials)
if: ${{ env.HAS_IOS_SIGNING != 'true' }}
run: echo "::notice::ASC_KEY_ID not set — skipping TestFlight upload. Populate the App Store Connect secrets to enable."

- name: Install XcodeGen
if: ${{ env.HAS_IOS_SIGNING == 'true' }}
run: brew install xcodegen

- name: Import Apple Distribution certificate
if: ${{ env.HAS_IOS_SIGNING == 'true' }}
env:
IOS_DIST_CERT_BASE64: ${{ secrets.IOS_DIST_CERT_BASE64 }}
IOS_DIST_CERT_PASSWORD: ${{ secrets.IOS_DIST_CERT_PASSWORD }}
run: |
set -euo pipefail
KEYCHAIN_PATH="$RUNNER_TEMP/pocket-signing.keychain-db"
KEYCHAIN_PASSWORD="$(openssl rand -hex 24)"
echo "$IOS_DIST_CERT_BASE64" | base64 --decode > "$RUNNER_TEMP/dist.p12"
security create-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security set-keychain-settings -lut 21600 "$KEYCHAIN_PATH"
security unlock-keychain -p "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH"
security import "$RUNNER_TEMP/dist.p12" -P "$IOS_DIST_CERT_PASSWORD" \
-A -t cert -f pkcs12 -k "$KEYCHAIN_PATH"
security set-key-partition-list -S apple-tool:,apple: \
-k "$KEYCHAIN_PASSWORD" "$KEYCHAIN_PATH" >/dev/null
security list-keychains -d user -s "$KEYCHAIN_PATH" $(security list-keychains -d user | tr -d '"')
security default-keychain -s "$KEYCHAIN_PATH"
rm -f "$RUNNER_TEMP/dist.p12"
echo "POCKET_KEYCHAIN_PATH=$KEYCHAIN_PATH" >> "$GITHUB_ENV"
security find-identity -v -p codesigning "$KEYCHAIN_PATH"

- name: Write App Store Connect API key
if: ${{ env.HAS_IOS_SIGNING == 'true' }}
env:
ASC_API_KEY_BASE64: ${{ secrets.ASC_API_KEY_BASE64 }}
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
run: |
set -euo pipefail
mkdir -p "$RUNNER_TEMP/asc"
echo "$ASC_API_KEY_BASE64" | base64 --decode > "$RUNNER_TEMP/asc/AuthKey_${ASC_KEY_ID}.p8"
echo "ASC_KEY_PATH=$RUNNER_TEMP/asc/AuthKey_${ASC_KEY_ID}.p8" >> "$GITHUB_ENV"

- name: Archive + upload to TestFlight
if: ${{ env.HAS_IOS_SIGNING == 'true' }}
working-directory: packaging/ios-companion
env:
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
BUILD_NUMBER: ${{ github.event.inputs.build_number }}
run: ./testflight.sh

- name: Clean up signing keychain
if: ${{ always() && env.POCKET_KEYCHAIN_PATH != '' }}
run: |
security delete-keychain "$POCKET_KEYCHAIN_PATH" || true
rm -f "$POCKET_KEYCHAIN_PATH" || true
3 changes: 3 additions & 0 deletions packaging/ios-companion/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ Sources/LisaPocket.entitlements
Widgets/LisaPocketWidgets.entitlements
# Build output / Xcode cruft.
.build/
build/
DerivedData/
*.xcarchive
ExportOptions.plist
*.xcuserstate
.DS_Store
73 changes: 73 additions & 0 deletions packaging/ios-companion/RELEASE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
# Shipping Lisa Pocket to TestFlight

Lisa Pocket is a **native SwiftUI** app (XcodeGen), so it can't use Markup's
`eas build`/`eas submit` (that's Expo). The equivalent here is `xcodebuild
archive` + `-exportArchive (destination: upload)` driven by an **App Store
Connect API key**, which handles automatic provisioning *and* the TestFlight
upload. [`testflight.sh`](testflight.sh) runs it locally; the
[`release-ios-testflight.yml`](../../.github/workflows/release-ios-testflight.yml)
workflow runs the same thing in CI — modeled on the secret-gated pattern of
`release-mac-apps.yml` (itself lifted from Markup).

App identity: bundle id **`ai.meetlisa.pocket`** (+ the widget extension
`ai.meetlisa.pocket.widgets`), team **`9LH9NBX7P4`**.

## One-time setup (Apple account actions — only you can do these)

1. **Register the app in App Store Connect.** App Store Connect → Apps → ➕ →
New App → iOS, bundle id `ai.meetlisa.pocket`, pick an SKU. (If the bundle
id isn't in the list, register it first under Certificates, IDs & Profiles →
Identifiers, with **App Groups** + **Push Notifications** capabilities; also
create the app group `group.ai.meetlisa.pocket`.)
2. **Create an App Store Connect API key.** Users and Access → Integrations →
App Store Connect API → ➕, role **App Manager**. Note the **Key ID** and
**Issuer ID**, and download `AuthKey_<KEYID>.p8` (downloadable once).
3. **First push permission only:** APNs alerts / Live-Activity refresh stay
inert until you also set `LISA_APNS_*` on the Mac running `lisa serve`
(see ../../packaging/ios-companion/README.md). TestFlight itself doesn't
need that — only live push delivery does.

## Build + upload locally (one command)

```sh
cd packaging/ios-companion
brew install xcodegen # one-time
ASC_KEY_ID=ABC123XYZ9 \
ASC_ISSUER_ID=xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx \
ASC_KEY_PATH=~/Downloads/AuthKey_ABC123XYZ9.p8 \
./testflight.sh
```

The Mac already has the **Apple Distribution** cert in its keychain, so locally
you only need the API key. The build number defaults to a timestamp; override
with `BUILD_NUMBER=…` / `MARKETING_VERSION=…` if needed. On Xcode < 16 pass
`EXPORT_METHOD=app-store`.

Then: App Store Connect → your app → **TestFlight** → add yourself / a tester
group once Apple finishes processing (a few minutes).

## Build + upload from CI (tag-triggered)

Add these repo secrets (Settings → Secrets and variables → Actions):

| Secret | What |
| --- | --- |
| `ASC_KEY_ID` | API Key ID |
| `ASC_ISSUER_ID` | API Issuer ID |
| `ASC_API_KEY_BASE64` | `base64 -i AuthKey_<id>.p8 \| pbcopy` |
| `IOS_DIST_CERT_BASE64` | an *Apple Distribution* `.p12` (cert + key), base64 |
| `IOS_DIST_CERT_PASSWORD` | the `.p12` passphrase |
| `APPLE_TEAM_ID` | `9LH9NBX7P4` (reused from the mac workflow) |

Then push a tag `pocket-vX.Y.Z` (or run the workflow manually). With the secrets
absent the workflow is a no-op, so the repo stays green for others.

## Honest limits

- The actual upload authenticates to **your** Apple account, so it can't run
without the API key + the app record above — those are account actions.
- TestFlight builds use the **production** APNs environment; `testflight.sh`
flips the generated `aps-environment` entitlement to `production` for the
archive (project.yml stays `development` for normal dev builds).
- Live push behavior is still only verifiable on a real device with `LISA_APNS_*`
configured; everything up to the upload is scripted here.
99 changes: 99 additions & 0 deletions packaging/ios-companion/testflight.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,99 @@
#!/usr/bin/env bash
# ---------------------------------------------------------------------------
# testflight.sh — archive Lisa Pocket and upload it to TestFlight.
#
# The native-app analogue of Markup's EAS submit flow: Lisa Pocket is a native
# SwiftUI app (XcodeGen), not Expo, so instead of `eas build`/`eas submit` we
# drive `xcodebuild archive` + `-exportArchive` (destination: upload) with an
# App Store Connect API key, which handles automatic provisioning AND the
# upload in one go. Plain xcrun/xcodebuild, matching the secret-gated style of
# .github/workflows/release-mac-apps.yml (itself lifted from Markup).
#
# ── One-time setup (see RELEASE.md) ───────────────────────────────────────
# 1. Apple Developer account; an "Apple Distribution" cert in the keychain
# (already present on the dev Mac; CI imports one from a secret).
# 2. Create the app in App Store Connect with bundle id ai.meetlisa.pocket.
# 3. Create an App Store Connect API key (Users and Access → Integrations →
# App Store Connect API), role "App Manager". Note the Key ID + Issuer ID
# and download the AuthKey_<KEYID>.p8 once.
#
# ── Required env ──────────────────────────────────────────────────────────
# ASC_KEY_ID App Store Connect API Key ID (e.g. ABC123XYZ9)
# ASC_ISSUER_ID API Key Issuer ID (a UUID)
# ASC_KEY_PATH path to the downloaded AuthKey_<ASC_KEY_ID>.p8
# ── Optional env ──────────────────────────────────────────────────────────
# APPLE_TEAM_ID default 9LH9NBX7P4
# MARKETING_VERSION default: whatever project.yml has
# BUILD_NUMBER default: date +%Y%m%d%H%M (must increase per version)
# EXPORT_METHOD default: app-store-connect (use "app-store" on Xcode<16)
#
# Usage:
# ASC_KEY_ID=… ASC_ISSUER_ID=… ASC_KEY_PATH=~/AuthKey_….p8 ./testflight.sh
# ---------------------------------------------------------------------------
set -euo pipefail
cd "$(dirname "$0")"
export DEVELOPER_DIR="${DEVELOPER_DIR:-/Applications/Xcode.app/Contents/Developer}"

: "${ASC_KEY_ID:?set ASC_KEY_ID (App Store Connect API Key ID)}"
: "${ASC_ISSUER_ID:?set ASC_ISSUER_ID (API Key Issuer ID)}"
: "${ASC_KEY_PATH:?set ASC_KEY_PATH (path to AuthKey_<id>.p8)}"
[ -f "$ASC_KEY_PATH" ] || { echo "✗ ASC_KEY_PATH not found: $ASC_KEY_PATH" >&2; exit 1; }
command -v xcodegen >/dev/null || { echo "✗ need xcodegen — brew install xcodegen" >&2; exit 1; }

TEAM_ID="${APPLE_TEAM_ID:-9LH9NBX7P4}"
BUILD_NUMBER="${BUILD_NUMBER:-$(date +%Y%m%d%H%M)}"
EXPORT_METHOD="${EXPORT_METHOD:-app-store-connect}"
BUILD_DIR="build"
ARCHIVE="$BUILD_DIR/LisaPocket.xcarchive"

echo "==> xcodegen generate"
xcodegen generate

# TestFlight/App Store builds use the PRODUCTION APNs environment; project.yml
# stays "development" for normal dev builds, so flip the generated entitlement
# here (only for this archive).
if [ -f Sources/LisaPocket.entitlements ]; then
plutil -replace aps-environment -string production Sources/LisaPocket.entitlements
fi

AUTH=(-allowProvisioningUpdates
-authenticationKeyPath "$ASC_KEY_PATH"
-authenticationKeyID "$ASC_KEY_ID"
-authenticationKeyIssuerID "$ASC_ISSUER_ID")

MV_ARG=()
[ -n "${MARKETING_VERSION:-}" ] && MV_ARG=(MARKETING_VERSION="$MARKETING_VERSION")

echo "==> archive (build $BUILD_NUMBER · team $TEAM_ID)"
rm -rf "$ARCHIVE"
xcodebuild archive \
-project LisaPocket.xcodeproj -scheme LisaPocket \
-destination 'generic/platform=iOS' \
-archivePath "$ARCHIVE" \
"${AUTH[@]}" \
CODE_SIGN_STYLE=Automatic DEVELOPMENT_TEAM="$TEAM_ID" \
CODE_SIGNING_ALLOWED=YES CODE_SIGNING_REQUIRED=YES \
CURRENT_PROJECT_VERSION="$BUILD_NUMBER" "${MV_ARG[@]}"

echo "==> write ExportOptions.plist (method=$EXPORT_METHOD, destination=upload)"
cat > "$BUILD_DIR/ExportOptions.plist" <<PLIST
<?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>$EXPORT_METHOD</string>
<key>destination</key><string>upload</string>
<key>teamID</key><string>$TEAM_ID</string>
<key>signingStyle</key><string>automatic</string>
<key>uploadSymbols</key><true/>
</dict></plist>
PLIST

echo "==> export + upload to TestFlight"
xcodebuild -exportArchive \
-archivePath "$ARCHIVE" \
-exportOptionsPlist "$BUILD_DIR/ExportOptions.plist" \
-exportPath "$BUILD_DIR/export" \
"${AUTH[@]}"

echo "✓ Uploaded to App Store Connect (build $BUILD_NUMBER)."
echo " It appears in TestFlight after Apple finishes processing (usually a few minutes)."