Skip to content

Add App Store deployment pipeline for iOS and macOS#126

Merged
krazyjakee merged 11 commits into
masterfrom
ci/app-store-deploy
Jun 13, 2026
Merged

Add App Store deployment pipeline for iOS and macOS#126
krazyjakee merged 11 commits into
masterfrom
ci/app-store-deploy

Conversation

@krazyjakee

Copy link
Copy Markdown
Contributor

Summary

Adds a complete App Store deployment pipeline to the release workflow, enabling automated builds and uploads to TestFlight (iOS) and App Store Connect (macOS). Introduces Fastlane configuration for signing, archiving, and notarizing builds, along with support for manual workflow dispatch to test deployments without cutting a real release.

Key Changes

  • Release workflow enhancements (.github/workflows/release.yml):

    • Added workflow_dispatch trigger with deploy_ios and deploy_mac toggles for testing App Store uploads without a tag push
    • Gated verify job to tag pushes only (skipped on workflow_dispatch, leaving only App Store jobs)
    • Added Developer ID certificate import and Ruby/Fastlane setup for macOS DMG signing and notarization
    • Split macOS packaging into two paths: signed/notarized DMG (when cert available) and unsigned fallback
    • Added ios-appstore and mac-appstore jobs that build store variants and upload to App Store Connect
  • Fastlane configuration (fastlane/Fastfile, fastlane/Appfile):

    • ios beta lane: archives, signs, and uploads iOS build to TestFlight
    • mac appstore lane: archives, signs, and uploads sandboxed macOS .pkg to App Store Connect
    • mac dmg lane: re-exports Flutter-built app with Developer ID signing, wraps in universal DMG, and notarizes via App Store Connect API
    • All lanes use App Store Connect API key authentication (no Apple ID/2FA in CI)
    • Manual signing forced on Runner target only to avoid conflicts with Pod/SwiftPM targets
  • App Store entitlements (macos/Runner/AppStore.entitlements):

    • New sandboxed entitlements file for Mac App Store build (swapped in by workflow)
    • Enables App Sandbox, network client/server, audio/camera, and file access as required by App Store guidelines
  • Bundle ID migration (com.daccord-projects.daccordcom.cattrall.daccord):

    • Updated across iOS and macOS Xcode projects and xcconfig
    • Aligns with official App Store Connect app record
  • App Store build flag (lib/shared/app_info.dart):

    • Added kAppStoreBuild constant (set via --dart-define=APP_STORE=true)
    • Disables in-app GitHub self-updater for store builds (App Store Review Guidelines forbid external executable downloads)
    • Store builds update through the store instead
  • Update controller (lib/features/updates/controllers/update_controller.dart):

    • Gated startup and periodic update checks behind kAppStoreBuild flag
  • Documentation (docs/app-store-deploy.md):

    • Complete guide to App Store deployment, required secrets, one-time setup steps, and triggering
  • Gemfile:

    • Added Fastlane dependency for CI environment

Notable Implementation Details

  • Conditional signing: Developer ID certificate is optional; if DEVELOPER_ID_CERT_P12 secret is absent, the DMG is built unsigned (previous behavior) with a warning, rather than failing the release.
  • Store vs. direct-download builds: Store builds (ios-appstore, mac-appstore) compile with APP_STORE=true and use sandboxed entitlements (macOS); the DMG build keeps the self-updater and non-sandboxed entitlements.
  • Entitlements swap: The workflow copies AppStore.entitlements over Release.entitlements in the store job's checkout only; the DMG job's checkout remains untouched, preserving non-sandboxed behavior.
  • Fastlane parameterization: All signing assets, team IDs, bundle IDs, and API credentials are passed via environment variables from GitHub secrets, keeping the Fastfile generic and reusable.
  • Manual signing scope: force_manual_signing() targets only the Runner app target to avoid conflicts with Pod/SwiftPM library targets that reject provisioning profiles.

https://claude.ai/code/session_01BbSaERwi3UWjqm79JMuzZ7

krazyjakee and others added 6 commits June 13, 2026 10:50
Match the iOS and macOS PRODUCT_BUNDLE_IDENTIFIER (and RunnerTests
suffixes) to the App ID registered in the Apple Developer portal
(com.cattrall.daccord, Team 8MVM4FMRRC).

Android applicationId (com.daccord_projects.daccord) is unchanged, as
it is a separate Google Play identifier.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adds Fastlane lanes and release-workflow jobs that build the signed Apple
"store" variants and upload them to App Store Connect, plus notarize the
existing macOS DMG for Gatekeeper-clean direct downloads.

- fastlane/Fastfile: `ios beta` (TestFlight), `mac appstore` (sandboxed .pkg
  → App Store Connect), `mac dmg` (Developer ID + hardened runtime + notarize).
  Auth + upload via an App Store Connect API key; manual signing forced via
  gym xcargs so the committed project stays on Automatic for local dev.
- release.yml: `ios-appstore` + `mac-appstore` jobs (run on tag push and
  workflow_dispatch with deploy_ios/deploy_mac toggles); the macOS matrix entry
  now produces a notarized DMG; `verify` gated to tag pushes.
- macos/Runner/AppStore.entitlements: sandboxed entitlements for the Mac App
  Store build (network, mic, camera, user-selected files); the DMG build keeps
  the non-sandboxed Release.entitlements.
- kAppStoreBuild (--dart-define=APP_STORE=true) disables the in-app GitHub
  self-updater in store builds (guideline 2.4.5 / sandbox); DMG keeps it.
- docs/app-store-deploy.md: secrets + one-time setup runbook.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The Developer ID Application cert can only be created by the account holder, not
the App Store Connect API key, so it may not be configured. Gate the signing +
notarization on DEVELOPER_ID_CERT_P12 being set and fall back to the previous
unsigned hdiutil DMG otherwise, so tagged releases never fail for a missing
notarization cert.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
build_app passes xcargs to xcodebuild as a raw shell string, so the
unescaped space in `CODE_SIGN_IDENTITY=Apple Distribution` split into
two tokens — xcodebuild took "Apple" as the identity and choked on the
stray remainder, failing the iOS App Store build with
`Unknown build action 'iOS'` (exit 65).

Shell-escape every xcarg value that can contain a space: the
"Apple Distribution" / "Developer ID Application" identities, the
provisioning-profile specifier, and the entitlements path. Same latent
bug fixed in the `mac dmg` lane's Developer ID xcargs.
Setting PROVISIONING_PROFILE_SPECIFIER/CODE_SIGN_IDENTITY as global xcargs made
xcodebuild apply them to every Pod/SwiftPM target, which fail with 'does not
support provisioning profiles'. Use update_code_signing_settings on the Runner
target (Release config) instead; gym re-signs the whole bundle on export via the
export_options profile map. Mac App Store entitlements are swapped in by the
workflow (cp AppStore.entitlements over Release.entitlements in that job only).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
… controller tests

- release.yml: add --no-codesign to mac-appstore flutter build step so
  Flutter does not attempt to sign during the build phase; gym/Fastlane
  handles all signing on the archive step (iOS already did this correctly)
- Fastfile: move require "tmpdir" to top of file alongside require
  "shellwords" rather than inside the dmg lane body
- Gemfile.lock: commit resolved gem versions so bundler-cache: true works
  correctly across CI runs and fastlane version is pinned
- test/features/updates/update_controller_test.dart: add unit tests
  covering canInstallInPlace, dismissCurrent, skipCurrent, and
  UpdateState.updateAvailable; documents compile-time kAppStoreBuild
  limitation

https://claude.ai/code/session_0183v6JCceGijR4eR1XscVoT

Copy link
Copy Markdown
Contributor Author

Code Review — App Store deployment pipeline

Reviewed commit 5de09ef. Fixes pushed in a662932.


Findings ranked by severity

🔴 High — mac-appstore build missing --no-codesign

File: .github/workflows/release.yml · "Build macOS (Flutter, unsigned)" step

The iOS App Store job correctly uses flutter build ios --release --no-codesign and lets Fastlane/gym handle all code-signing on the archive step. The macOS App Store job was missing --no-codesign, meaning Flutter would attempt to sign the .app during the build phase with whatever identity is on the runner keychain. This produces an artifact signed with the wrong identity before gym re-exports it, and can fail outright when no default signing identity is found. Fixed: added --no-codesign to the macOS flutter build command.


🟡 Medium — No Gemfile.lock committed

File: Gemfile (new)

ruby/setup-ruby@v1 with bundler-cache: true uses Gemfile.lock as its cache key. Without a lockfile, gems are resolved fresh on every run (no cache reuse), and fastlane could silently upgrade to a breaking version. Fixed: generated and committed Gemfile.lock (fastlane 2.236.1).


🟡 Medium — require "tmpdir" inside lane body

File: fastlane/Fastfile · mac dmg lane, line 147

require "tmpdir" was inlined inside the dmg lane body rather than placed at the top of the file alongside require "shellwords". It works because Ruby only evaluates require once per process, but it obscures the file's stdlib dependencies. Fixed: moved to the top of the file.


🟢 Low — No unit tests for new UpdateController behaviour

File: lib/features/updates/controllers/update_controller.dart

The new kAppStoreBuild guard in maybeCheckOnStartup() and canInstallInPlace had no direct test coverage. Added: test/features/updates/update_controller_test.dart covering canInstallInPlace (returns false with no loaded release; returns false with a release that has no matching platform asset), dismissCurrent, skipCurrent, and UpdateState.updateAvailable (including the pre-release suppression logic).

Limitation noted inline: kAppStoreBuild is a const bool from bool.fromEnvironment('APP_STORE'), always false in standard test builds. The kAppStoreBuild == true early-return path in maybeCheckOnStartup() is compile-time gated and cannot be exercised without --dart-define=APP_STORE=true. This is a known Dart fromEnvironment constraint and is documented in the new test file.


No action needed

  • needs: ci for the App Store jobs is correct — the ci: job defined at line 56 calls .github/workflows/ci.yml as a reusable workflow; this is the expected gate.
  • DEVID_AVAILABLE conditional (secrets.X != '' in env block) is a valid GitHub Actions pattern.
  • CODE_SIGN_IDENTITY shellescape in Fastfile is safe: Fastlane passes xcargs through a shell, so Developer\ ID\ Application is correctly interpreted as a single token by xcodebuild.
  • Bundle ID migration (com.daccord-projects.daccordcom.cattrall.daccord) is consistent across all Xcode project files and xcconfig.
  • kAppStoreBuild import is already present via package:bonfire/shared/app_info.dart in update_controller.dart.

Generated by Claude Code

krazyjakee and others added 5 commits June 13, 2026 23:00
- Remove unused accord_settings import in update_controller_test.dart (the
  unused_import WARNING failed 'flutter analyze --no-fatal-infos').
- iOS/Mac App Store jobs now pass --build-name stripped of any prerelease/build
  suffix, so a prerelease tag like v0.2.4-rc.1 still uploads a valid numeric
  CFBundleShortVersionString (0.2.4).
- Bump version to 0.2.4-rc.1 for the first end-to-end prerelease run.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
App Store rejected the build for a missing NSPhotoLibraryUsageDescription (the
file/image pickers reference the photo library APIs). Add it plus the
NSPhotoLibraryAddUsageDescription (saving downloads) and the
NSLocationWhenInUseUsageDescription Apple also warned about (referenced by a
bundled component; the app does not collect location).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
- flutter build macos has no --no-codesign (iOS-only); remove it so the Mac App
  Store job stops failing with exit 64.
- The Developer ID cert is now set, so the notarized-DMG path runs and its
  failure was sinking the whole release. Wrap it: on failure, fall back to an
  unsigned DMG (with diagnostics) so the release always completes while the
  gym developer-id export is iterated on.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
gym exported the .pkg/.app successfully ("Successfully exported and signed the
pkg file") but the lane's Dir["build/..."] glob returned nil (gym's cwd differs
from the lane's), so it aborted with "No .pkg/.app produced". Capture build_app's
return value (absolute path) instead, with the glob only as a fallback.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Add an android-play job to release.yml (tag push / workflow_dispatch
deploy_android toggle) that builds a signed AAB and uploads it to the
Play internal testing track via a new fastlane `android play` lane
(release_status completed, so testers get it immediately).

Add an upload-key release signingConfig to android/app/build.gradle —
read from env vars in CI or android/key.properties locally, falling back
to the debug key when no keystore is present so secret-less builds (the
direct-download APK, flutter run --release) still work.

Document the required Play secrets and one-time keystore + service-account
setup in docs/app-store-deploy.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@krazyjakee krazyjakee merged commit 0d7529c into master Jun 13, 2026
2 checks passed
@krazyjakee krazyjakee deleted the ci/app-store-deploy branch June 13, 2026 23:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants