mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-10-03 00:01:55 +00:00
ci(mobile): release iOS and Android together from one tag (#1048)
A `mobile-v<x.y.z>` tag now ships both platforms at one version: the Android APK to a draft release as before, and an iOS build to TestFlight. iOS no longer depends on one machine signed in to Xcode. - The version is tauri.conf.json's; a tag that disagrees with it fails the run, so the repo always says what shipped. Set to 0.1.1, the Android build already out. - scripts/testflight.sh signs and uploads with an App Store Connect API key when ASC_KEY_ID and ASC_ISSUER_ID are set, with a certificate Apple keeps in the cloud. That needs an Admin key. - A manual run builds both and uploads nothing.
This commit is contained in:
@@ -1,14 +1,16 @@
|
||||
name: Mobile
|
||||
|
||||
# The phone app's Android build: a signed APK people install by hand. A
|
||||
# `mobile-v<x.y.z>` tag builds it at that version and attaches it to a draft
|
||||
# release of the same name; a manual run builds it at the version in
|
||||
# tauri.conf.json and keeps it as a workflow artifact only.
|
||||
# The phone app, both platforms at one version. A `mobile-v<x.y.z>` tag, cut
|
||||
# from a commit whose tauri.conf.json says x.y.z:
|
||||
# - Android: a signed APK, attached to a draft release of the same name.
|
||||
# - iOS: a build uploaded to TestFlight, as <x.y.z>.<build number>.
|
||||
# A manual run builds both at tauri.conf.json's version and uploads nothing:
|
||||
# the APK is kept as a workflow artifact, the iOS build is only signed.
|
||||
#
|
||||
# The mobile app is versioned apart from the desktop's `v*` tags. Android
|
||||
# installs one build over another only when its versionCode is higher, and
|
||||
# Tauri derives it from the version (major * 1000000 + minor * 1000 + patch),
|
||||
# so each tag must be higher than the last.
|
||||
# so each version must be higher than the last.
|
||||
|
||||
on:
|
||||
push:
|
||||
@@ -25,24 +27,35 @@ env:
|
||||
NDK_VERSION: 28.2.13676358
|
||||
|
||||
jobs:
|
||||
android:
|
||||
# The version is the one in the tagged commit, so the repo always says what
|
||||
# was shipped; a tag that disagrees with it is a mistake, not an override.
|
||||
version:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Version
|
||||
- id: version
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [[ "$GITHUB_REF" == refs/tags/mobile-v* ]]; then
|
||||
VERSION="${GITHUB_REF_NAME#mobile-v}"
|
||||
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "::error::tag must be mobile-v<major>.<minor>.<patch>, got $GITHUB_REF_NAME"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
VERSION=$(jq -r .version mobile/src-tauri/tauri.conf.json)
|
||||
VERSION=$(jq -r .version mobile/src-tauri/tauri.conf.json)
|
||||
if ! [[ "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
echo "::error::tauri.conf.json's version must be <major>.<minor>.<patch>, got $VERSION"
|
||||
exit 1
|
||||
fi
|
||||
echo "VERSION=$VERSION" >> "$GITHUB_ENV"
|
||||
if [[ "$GITHUB_REF" == refs/tags/mobile-v* && "$GITHUB_REF_NAME" != "mobile-v$VERSION" ]]; then
|
||||
echo "::error::$GITHUB_REF_NAME is on a commit whose tauri.conf.json says $VERSION"
|
||||
exit 1
|
||||
fi
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
|
||||
android:
|
||||
needs: version
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
VERSION: ${{ needs.version.outputs.version }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-java@v4
|
||||
with:
|
||||
@@ -101,7 +114,7 @@ jobs:
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npm ci
|
||||
npx tauri android build --apk --target aarch64 --config "{\"version\":\"$VERSION\"}"
|
||||
npx tauri android build --apk --target aarch64
|
||||
|
||||
- name: Check and name the APK
|
||||
run: |
|
||||
@@ -146,3 +159,54 @@ jobs:
|
||||
--notes-file .github/mobile-install.md
|
||||
fi
|
||||
gh release upload "$GITHUB_REF_NAME" dist/* --clobber --repo "$GITHUB_REPOSITORY"
|
||||
|
||||
ios:
|
||||
needs: version
|
||||
# Xcode 26: App Store Connect takes only builds made with the iOS 26 SDK.
|
||||
runs-on: macos-26
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: 22
|
||||
cache: npm
|
||||
cache-dependency-path: mobile/package-lock.json
|
||||
|
||||
- uses: dtolnay/rust-toolchain@stable
|
||||
with:
|
||||
targets: aarch64-apple-ios
|
||||
|
||||
- uses: Swatinem/rust-cache@v2
|
||||
with:
|
||||
workspaces: mobile/src-tauri
|
||||
|
||||
# An App Store Connect API key signs in for Xcode: it signs the build
|
||||
# with a certificate Apple keeps, and uploads it.
|
||||
- name: App Store Connect key
|
||||
env:
|
||||
ASC_KEY_P8: ${{ secrets.ASC_KEY_P8 }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
if [ -z "$ASC_KEY_P8" ]; then
|
||||
echo "::error::the ASC_* secrets are not set"
|
||||
exit 1
|
||||
fi
|
||||
printf '%s\n' "$ASC_KEY_P8" > "$RUNNER_TEMP/AuthKey.p8"
|
||||
echo "ASC_KEY_PATH=$RUNNER_TEMP/AuthKey.p8" >> "$GITHUB_ENV"
|
||||
|
||||
# The build number defaults to the time (scripts/testflight.sh), so it
|
||||
# rises from any machine without a counter.
|
||||
- name: Build and upload
|
||||
working-directory: mobile
|
||||
env:
|
||||
ASC_KEY_ID: ${{ secrets.ASC_KEY_ID }}
|
||||
ASC_ISSUER_ID: ${{ secrets.ASC_ISSUER_ID }}
|
||||
run: |
|
||||
set -euo pipefail
|
||||
npm ci
|
||||
if [[ "$GITHUB_REF" == refs/tags/mobile-v* ]]; then
|
||||
scripts/testflight.sh
|
||||
else
|
||||
scripts/testflight.sh --no-upload
|
||||
fi
|
||||
|
||||
+34
-7
@@ -71,7 +71,9 @@ npm run tauri ios init # once: generates src-tauri/gen/apple
|
||||
npm run tauri ios dev # simulator, or pick a connected device
|
||||
```
|
||||
|
||||
To send a build to TestFlight (Xcode signed in to an account on the team):
|
||||
Releases go to TestFlight from CI (see [Releasing](#releasing)). To send one from this
|
||||
machine instead, signed in to Xcode with an account on the team, or with an App Store
|
||||
Connect API key in `ASC_KEY_ID`, `ASC_ISSUER_ID` and `ASC_KEY_PATH`:
|
||||
|
||||
```sh
|
||||
scripts/testflight.sh # archive, sign for the App Store, upload
|
||||
@@ -97,12 +99,6 @@ npm run tauri android build -- --debug --apk --target aarch64 # an installable
|
||||
keyboard's height to the page, which the WebView does not report edge to edge. Don't re-run
|
||||
`android init` over it.
|
||||
|
||||
To release an APK, push a `mobile-v<x.y.z>` tag, higher than the last. `.github/workflows/mobile.yml`
|
||||
builds it at that version, signs it with the release key and attaches it to a draft
|
||||
release, which is never marked latest, so the desktop updater doesn't see it. People
|
||||
download the APK on the phone and open it. A later one installs over it only if it's
|
||||
signed with the same key and its version is higher.
|
||||
|
||||
The release key is in the `ANDROID_KEYSTORE_BASE64`, `ANDROID_KEYSTORE_PASSWORD` and
|
||||
`ANDROID_KEY_ALIAS` secrets, with a copy kept outside GitHub. A local release build signs
|
||||
with it when `src-tauri/gen/android/keystore.properties` (ignored by git) names it:
|
||||
@@ -114,6 +110,37 @@ keyAlias=tty7
|
||||
keyPassword=…
|
||||
```
|
||||
|
||||
### Releasing
|
||||
|
||||
Both platforms ship at one version, apart from the desktop's:
|
||||
|
||||
1. Raise `version` in `src-tauri/tauri.conf.json` and merge it. Each version must be higher
|
||||
than the last: Android installs over a build only when its versionCode, which Tauri
|
||||
derives from the version, is higher.
|
||||
2. Tag that commit `mobile-v<version>` and push the tag.
|
||||
|
||||
`.github/workflows/mobile.yml` then:
|
||||
|
||||
- **Android:** builds a signed arm64 APK and attaches it to a draft release, which you
|
||||
publish. The release is never marked latest, because the desktop updater reads
|
||||
`/releases/latest`.
|
||||
- **iOS:** uploads a build to TestFlight. It reaches testers once App Store Connect has
|
||||
processed it, and, for the external group, once Beta App Review passes.
|
||||
- **Checks:** a tag that doesn't match `tauri.conf.json` fails the run.
|
||||
|
||||
Running the workflow by hand builds both platforms but uploads nothing.
|
||||
|
||||
Secrets:
|
||||
|
||||
| Secret | What |
|
||||
|---|---|
|
||||
| `ANDROID_KEYSTORE_BASE64`, `ANDROID_KEYSTORE_PASSWORD`, `ANDROID_KEY_ALIAS` | the Android release key |
|
||||
| `ASC_KEY_ID`, `ASC_ISSUER_ID`, `ASC_KEY_P8` | an App Store Connect API key with the Admin role, which signs and uploads iOS builds (App Manager keys may not sign in the cloud) |
|
||||
|
||||
Keep both keys outside GitHub as well; secrets can't be read back. A phone feature that
|
||||
needs a newer desktop says so when the desktop is older, so the release notes should name
|
||||
the desktop version a release needs.
|
||||
|
||||
### Without a phone
|
||||
|
||||
`crates/tty7-mobile-client/examples/probe.rs` is a phone in a shell. It pairs, prints the
|
||||
|
||||
@@ -5,7 +5,12 @@
|
||||
# --build-number N the build is <version>.N (default: the time, yyjjjHHMM)
|
||||
# --no-upload export a signed .ipa into build/testflight/ instead
|
||||
#
|
||||
# Needs Xcode signed in (Settings → Accounts) to an account on team 78SZC3DQ7B.
|
||||
# Signs in one of two ways:
|
||||
# - Xcode signed in (Settings → Accounts) to an account on team 78SZC3DQ7B.
|
||||
# - An App Store Connect API key, as CI does: ASC_KEY_ID and ASC_ISSUER_ID set,
|
||||
# and the key at ASC_KEY_PATH (default
|
||||
# ~/.appstoreconnect/private_keys/AuthKey_<ASC_KEY_ID>.p8). Xcode then signs
|
||||
# with a certificate Apple keeps in the cloud, so no keychain is needed.
|
||||
#
|
||||
# Why not just `tauri ios build --export-method app-store-connect`:
|
||||
# - The archive is signed with a development profile first, and the team has
|
||||
@@ -36,7 +41,15 @@ ARCHIVE=src-tauri/gen/apple/build/tty7-mobile_iOS.xcarchive
|
||||
OUT=build/testflight
|
||||
mkdir -p "$OUT"
|
||||
|
||||
[ -f "$PROJECT" ] || npm run tauri ios init
|
||||
[ -f "$PROJECT" ] || npm run tauri ios init -- --ci
|
||||
|
||||
AUTH=()
|
||||
if [ -n "${ASC_KEY_ID:-}" ]; then
|
||||
KEY="${ASC_KEY_PATH:-$HOME/.appstoreconnect/private_keys/AuthKey_$ASC_KEY_ID.p8}"
|
||||
[ -f "$KEY" ] || { echo "no App Store Connect key at $KEY" >&2; exit 1; }
|
||||
AUTH=(-authenticationKeyPath "$KEY" -authenticationKeyID "$ASC_KEY_ID"
|
||||
-authenticationKeyIssuerID "${ASC_ISSUER_ID:?ASC_ISSUER_ID is needed with ASC_KEY_ID}")
|
||||
fi
|
||||
|
||||
# `tauri ios init` fills the asset catalog with Tauri's placeholder icon;
|
||||
# put ours back so a regenerated gen/ never ships it.
|
||||
@@ -80,7 +93,8 @@ xcodebuild -exportArchive \
|
||||
-archivePath "$ARCHIVE" \
|
||||
-exportOptionsPlist "$OUT/ExportOptions.plist" \
|
||||
-exportPath "$OUT" \
|
||||
-allowProvisioningUpdates
|
||||
-allowProvisioningUpdates \
|
||||
${AUTH[@]+"${AUTH[@]}"}
|
||||
|
||||
if [ "$UPLOAD" = 1 ]; then
|
||||
echo "==> Uploaded $VERSION ($BUILD); it shows in TestFlight once App Store Connect has processed it"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"$schema": "https://schema.tauri.app/config/2",
|
||||
"productName": "tty7",
|
||||
"version": "0.1.0",
|
||||
"version": "0.1.1",
|
||||
"identifier": "dev.tty7.mobile",
|
||||
"build": {
|
||||
"beforeDevCommand": "npm run dev",
|
||||
|
||||
Reference in New Issue
Block a user