Update to include publishing the apps

This commit is contained in:
Mo Tarbin
2026-07-14 12:17:13 -04:00
parent eacfca715c
commit 379e1a354b
5 changed files with 173 additions and 8 deletions

1
.gitignore vendored
View File

@@ -32,6 +32,7 @@ android/app/google-services.json
android/app/release/*.jks
android/keystore.properties
android/local.properties
android/play-service-account.json
ios/App/App/GoogleService-Info.plist
ios/*.p8

85
AUTOMATED_RELEASE.md Normal file
View File

@@ -0,0 +1,85 @@
# Automated Release Pipeline
Builds and (optionally) uploads signed Android and iOS release artifacts from the CLI — no Android Studio or Xcode GUI needed for routine releases.
## Quick start
```sh
./scripts/pull-secrets.sh # pulls all signing secrets from Vaultwarden
./scripts/release.sh # bump patch version, build + sign both platforms
./scripts/release.sh --upload # also upload: Android → Play internal track, iOS → TestFlight
```
Common flags on `release.sh`:
| Flag | Effect |
|---|---|
| `--android` / `--ios` | Build only one platform |
| `--bump minor\|major` | Bump type (default: `patch`) |
| `--skip-bump` | Build with the current version, don't bump |
| `--upload` | Upload after building |
| `--track TRACK` | Play Store track for `--upload` (default: `internal`) |
Outputs:
- Android: `android/app/build/outputs/bundle/release/app-release.aab`
- iOS: `build/ios/Donetick.ipa`
## One-time machine setup
1. `bundle install` — installs fastlane (Ruby gems, local to the repo via `vendor/bundle`)
2. `./scripts/pull-secrets.sh` — pulls all secrets below from Vaultwarden
3. `android/local.properties` needs `sdk.dir=<path to Android SDK>` — not pulled from vault, machine-specific. Android Studio creates this automatically; if building headless-only, create it manually.
4. Export `ASC_ISSUER_ID` (App Store Connect API key issuer ID) — either in your shell profile, or add it to the `Donetick Env Production` vault note so `pull-secrets.sh` delivers it into `.env.production` (which `release.sh` auto-sources).
5. A working Apple Distribution certificate + the `Donetick App Store(fastline)` provisioning profile must be installed in your keychain / `~/Library/MobileDevice/Provisioning Profiles/` (see "iOS signing model" below) — **not** currently pulled from vault, machine-specific one-time setup.
## Secrets pulled by `scripts/pull-secrets.sh`
All secrets live in Vaultwarden (`https://www.bitwarden.com`) as Secure Notes, named exactly as below:
| Vault item name | Written to | Encoding |
|---|---|---|
| Donetick Google Services Android | `android/app/google-services.json` | raw |
| Donetick Android Keystore | `android/app/release/donetick.jks` | base64 |
| Donetick Keystore Password | (used inline for `android/keystore.properties`) | raw |
| Donetick Google Play Service Account | `android/play-service-account.json` | raw |
| Donetick Google Services iOS | `ios/App/App/GoogleService-Info.plist` | raw |
| Donetick App Store Connect Key | `ios/AuthKey_84F695CDQ3.p8` | base64 |
| Donetick Env Production | `.env.production` | raw |
None of these files are committed to git — all covered by `.gitignore`.
## iOS signing model (important — don't relitigate this)
The App target uses **manual signing for Release**, not Automatic. This was a deliberate choice, not the default:
- The project has **two** Apple Distribution-type certificates on the Apple Developer account: an Xcode Cloudmanaged one (private key never leaves Apple, can't be used for local CLI signing) and a plain one created via Xcode → Settings → Accounts → Manage Certificates (private key is in the local keychain, usable). Automatic signing kept binding to the wrong (Managed) one.
- Fix: `ios/App/App.xcodeproj/project.pbxproj` → App target → Release config has `CODE_SIGN_STYLE = Manual`, `CODE_SIGN_IDENTITY = "Apple Distribution"`, `PROVISIONING_PROFILE_SPECIFIER = "Donetick App Store(fastline)"`. Debug config is untouched (still Automatic, for local dev builds in Xcode).
- `fastlane/Fastfile`'s `ios release` lane passes explicit `export_options: { signingStyle: "manual", provisioningProfiles: {...} }` to `build_app` — do **not** switch this back to `-allowProvisioningUpdates`/automatic without a good reason, it re-triggers the ambiguous cert selection.
**If you ever add/change an entitlement** (new Capability in Signing & Capabilities, e.g. a new permission), the existing provisioning profile becomes stale and exports will fail with an error like `"App.app" requires a provisioning profile with the X feature`. Fix:
1. developer.apple.com → Identifiers → `com.donetick.app` → confirm the new capability is checked
2. developer.apple.com → Profiles → find `Donetick App Store(fastline)` → regenerate it → download
3. Install it locally: get its UUID (`security cms -D -i <file>.mobileprovision | plutil -extract UUID xml1 -o - -`) and copy to `~/Library/MobileDevice/Provisioning Profiles/<uuid>.mobileprovision`
**If the Distribution certificate ever expires/is revoked**, redo the one-time Xcode step: Settings → Accounts → Manage Certificates → `+` → Apple Distribution, then regenerate the profile as above.
## Android signing model
Standard keystore signing via `android/keystore.properties`, read by `android/app/build.gradle`. One historical gotcha already fixed: `storeFile` must be `release/donetick.jks` (relative to the `android/app` module dir), **not** `app/release/donetick.jks` — that extra `app/` silently resolved to a double-nested wrong path and broke CLI builds (Android Studio may have masked this before).
## Play Store upload gotchas
- The service account (`fastline-471@donetick-5f910.iam.gserviceaccount.com`) needs the **Google Play Android Developer API enabled** in its GCP project (`donetick-5f910`), **and** explicit per-app release permission granted in Play Console → Users and permissions (not just account-level access) — **and you have to actually click Save**, the UI will silently not persist it otherwise.
- Google Play Console permission changes can take time to propagate on Google's backend even after saving (historically documented as up to ~24h, though in practice it was near-instant once actually saved).
- The Play Developer API requires the app to have had **at least one manual release** uploaded through the Play Console website before API uploads work at all — not an issue for Donetick (already satisfied) but relevant if this is ever set up for a brand-new app.
## Fastlane lanes reference
```sh
bundle exec fastlane android release # build signed .aab
bundle exec fastlane android apk # build signed .apk (sideloading/testing)
bundle exec fastlane android upload [track:X] # upload .aab to Play Store (default track: internal)
bundle exec fastlane ios release # build signed .ipa
bundle exec fastlane ios upload # upload .ipa to TestFlight
```

View File

@@ -5,6 +5,7 @@ default_platform(:ios)
IOS_KEY_PATH = File.expand_path("../ios/AuthKey_84F695CDQ3.p8")
IOS_OUTPUT_DIR = File.expand_path("../build/ios")
ANDROID_PROJECT_DIR = File.expand_path("../android")
PLAY_JSON_KEY_PATH = File.expand_path("../android/play-service-account.json")
platform :ios do
desc "Build a signed, App Store-ready .ipa"
@@ -31,6 +32,32 @@ platform :ios do
UI.success("Signed .ipa at #{IOS_OUTPUT_DIR}/Donetick.ipa")
end
desc "Upload the built .ipa to TestFlight"
lane :upload do
UI.user_error!("Missing #{IOS_KEY_PATH} — run scripts/pull-secrets.sh first") unless File.exist?(IOS_KEY_PATH)
UI.user_error!("ASC_ISSUER_ID is not set (App Store Connect API key issuer id)") if ENV["ASC_ISSUER_ID"].to_s.empty?
ipa_path = File.join(IOS_OUTPUT_DIR, "Donetick.ipa")
UI.user_error!("Missing #{ipa_path} — run `fastlane ios release` first") unless File.exist?(ipa_path)
api_key = app_store_connect_api_key(
key_id: ENV["ASC_KEY_ID"] || "84F695CDQ3",
issuer_id: ENV["ASC_ISSUER_ID"],
key_filepath: IOS_KEY_PATH,
duration: 1200,
in_house: false,
)
upload_to_testflight(
api_key: api_key,
ipa: ipa_path,
skip_waiting_for_build_processing: true,
skip_submission: true,
)
UI.success("Uploaded to TestFlight")
end
end
platform :android do
@@ -63,4 +90,24 @@ platform :android do
apk_path = lane_context[SharedValues::GRADLE_APK_OUTPUT_PATH]
UI.success("Signed .apk at #{apk_path}")
end
desc "Upload the built .aab to the Play Store (internal testing track by default)"
lane :upload do |options|
UI.user_error!("Missing #{PLAY_JSON_KEY_PATH} — run scripts/pull-secrets.sh first") unless File.exist?(PLAY_JSON_KEY_PATH)
aab_path = File.join(ANDROID_PROJECT_DIR, "app/build/outputs/bundle/release/app-release.aab")
UI.user_error!("Missing #{aab_path} — run `fastlane android release` first") unless File.exist?(aab_path)
upload_to_play_store(
json_key: PLAY_JSON_KEY_PATH,
package_name: "com.donetick.app",
aab: aab_path,
track: options[:track] || "internal",
skip_upload_metadata: true,
skip_upload_images: true,
skip_upload_screenshots: true,
)
UI.success("Uploaded to Play Store (#{options[:track] || 'internal'} track)")
end
end

View File

@@ -23,6 +23,14 @@ For _fastlane_ installation instructions, see [Installing _fastlane_](https://do
Build a signed, App Store-ready .ipa
### ios upload
```sh
[bundle exec] fastlane ios upload
```
Upload the built .ipa to TestFlight
----
@@ -44,6 +52,14 @@ Build a signed release .aab
Build a signed release .apk (for sideloading/testing)
### android upload
```sh
[bundle exec] fastlane android upload
```
Upload the built .aab to the Play Store (internal testing track by default)
----
This README.md is auto-generated and will be re-generated every time [_fastlane_](https://fastlane.tools) is run.

View File

@@ -7,22 +7,26 @@ cd "$REPO_ROOT"
PLATFORM="both"
BUMP="patch"
SKIP_BUMP=false
UPLOAD=false
ANDROID_TRACK="internal"
usage() {
cat <<EOF
Usage: $(basename "$0") [--android|--ios] [--bump patch|minor|major] [--skip-bump]
Usage: $(basename "$0") [--android|--ios] [--bump patch|minor|major] [--skip-bump] [--upload] [--track TRACK]
Builds signed release artifacts for Android (.aab) and/or iOS (.ipa).
--android Build Android only
--ios Build iOS only
--bump TYPE Version bump type before building (default: patch)
--skip-bump Don't bump the version, build with the current one
-h, --help Show this help
--android Build Android only
--ios Build iOS only
--bump TYPE Version bump type before building (default: patch)
--skip-bump Don't bump the version, build with the current one
--upload Also upload: Android to Play Store, iOS to TestFlight
--track TRACK Play Store track for --upload (default: internal)
-h, --help Show this help
Requires:
- scripts/pull-secrets.sh to have been run (keystore, google-services, App Store Connect key)
- ASC_ISSUER_ID env var set (App Store Connect API key issuer id) for iOS builds
- scripts/pull-secrets.sh to have been run (keystore, google-services, App Store Connect key, Play service account)
- ASC_ISSUER_ID env var set (App Store Connect API key issuer id) for iOS builds/uploads
- bundle install (fastlane) run at least once
EOF
}
@@ -33,6 +37,8 @@ while [[ $# -gt 0 ]]; do
--ios) PLATFORM="ios"; shift ;;
--bump) BUMP="$2"; shift 2 ;;
--skip-bump) SKIP_BUMP=true; shift ;;
--upload) UPLOAD=true; shift ;;
--track) ANDROID_TRACK="$2"; shift 2 ;;
-h|--help) usage; exit 0 ;;
*) echo "Unknown option: $1" >&2; usage; exit 1 ;;
esac
@@ -65,11 +71,21 @@ fi
if [ "$PLATFORM" = "android" ] || [ "$PLATFORM" = "both" ]; then
echo "→ Building signed Android release (.aab)"
$RUN android release
if [ "$UPLOAD" = true ]; then
echo "→ Uploading Android release to Play Store ($ANDROID_TRACK track)"
$RUN android upload track:"$ANDROID_TRACK"
fi
fi
if [ "$PLATFORM" = "ios" ] || [ "$PLATFORM" = "both" ]; then
echo "→ Building signed iOS release (.ipa)"
$RUN ios release
if [ "$UPLOAD" = true ]; then
echo "→ Uploading iOS release to TestFlight"
$RUN ios upload
fi
fi
echo "✓ Release build complete"