Ascenda Wiki
Ascenda · operations

Frontend CI & TestFlight

A push to main starts an Xcode Cloud build. A successful Archive and TestFlight post-action make that build available to Opus Staff. The release version stays under our control; the Cloud build number increases automatically.

Configuration verified on . First successful automated delivery: 1.0.0 (4). These are a baseline and an example, not the current latest build.

1. The current delivery flow

The source is AscendaFinance/ascenda-app. The iOS product is Opus, with bundle identifier com.fintechopus.app. Xcode Cloud runs the workflow Opus - TestFlight main against the committed native workspace ios/Opus.xcworkspace and shared scheme Opus.

  1. A remote change to main, including a merge, meets the Branch Changes start condition.
  2. Xcode Cloud checks out the code and runs ios/ci_scripts/ci_post_clone.sh to prepare dependencies and app configuration.
  3. The Archive iOS action builds the Release scheme, signs the app, and prepares an internal TestFlight upload.
  4. The TestFlight Internal Testing - iOS post-action distributes the archive to Opus Staff.
  5. After Apple processes the upload, testers can install or update it in TestFlight.

One push can contain several commits: the trigger concerns the branch update, not a promise of one separately published binary for every commit in that push. Failed builds do not produce an available TestFlight update. A no-op push does not change the branch, and Apple supports skipping a build with [ci skip] in the latest commit message; do not use it for changes that need a beta delivery.

Internal distribution. This workflow does not publish the app on the App Store. Archives uploaded as Internal Only cannot later be submitted for external testing or App Store release. A store release needs a new archive using an appropriate TestFlight & App Store distribution configuration.

The configured action is Archive. The script installs dependencies and validates environment values; it does not run the frontend lint, TypeScript check, Jest suite, or its own shell test harness. A green delivery status confirms this workflow succeeded, not that those checks passed or that the app launches correctly on a phone.

2. Version and build rules

In 1.0.0 (4), 1.0.0 is the release version and 4 is the build number. A new TestFlight build does not require a new release version.

ValueWhere it comes fromWho changes it
Release versionMARKETING_VERSION in the Opus target, Debug and Release; CFBundleShortVersionString reads it.The team, when starting a new release cycle.
Cloud build numberXcode Cloud's build counter, used for the distributed CFBundleVersion.Xcode Cloud, for every build run.
Local build numberXcode target General → Build / CURRENT_PROJECT_VERSION; CFBundleVersion reads it.The person making a manual local upload.
Expo versionexpo.version in app.json.The team, together with the native release version.

The native iOS project is tracked and CI does not run Expo prebuild. Changing only app.json will therefore not change the native archive's release version. Update the Opus target's Version in Xcode, verify both Debug and Release MARKETING_VERSION entries in ios/Opus.xcodeproj/project.pbxproj, and keep app.json aligned. Keep package.json's version aligned as a repository convention; it does not control the iOS archive.

Opus release policy

  • Keep the release version during iterations for the same planned release: 1.0.0 (4), 1.0.0 (5), 1.0.0 (6).
  • Choose the next release version before beta testing that release. After shipping 1.0.0, begin the next cycle as, for example, 1.0.1 for fixes, 1.1.0 for features, or 2.0.0 for a major product change. These categories are our policy, not an Apple requirement.
  • Use three numeric components for the release version. Do not add a -beta suffix; TestFlight already identifies it as a beta distribution.
  • Use an increasing integer sequence for builds across our release versions and upload methods. Failed Cloud runs can consume numbers, so gaps are normal. Do not reset the Cloud counter for a version bump.
  • Apple requires a unique version/build combination for iOS. Our single increasing build sequence is a stricter team convention that makes mixed local and Cloud uploads easier to track.
  • Do not increment CURRENT_PROJECT_VERSION in Git for each automated delivery. Xcode Cloud manages its distributed build number independently.
  • Before an App Store release, create the matching version in App Store Connect and use a build eligible for App Review. Merely uploading to TestFlight does not submit or release it.

When mixing local and Cloud uploads

Prefer a manual Cloud start for a repeat build: it uses the same counter as automatic runs. For a local upload, reserve a number before creating the archive:

  1. Temporarily disable the workflow and wait for active or queued Cloud builds to finish. Coordinate with the team so nobody starts another delivery during the reservation.
  2. Inspect TestFlight's uploaded build numbers, Xcode Cloud's build history, and Xcode Cloud → Settings → Build Number → Next Build Number.
  3. Choose an integer greater than the uploaded and already assigned Cloud numbers, and at least the next Cloud number. Set it as the local Opus target's Build.
  4. After the manual upload, set Next Build Number above the number Apple actually accepted, then re-enable the workflow. Changing the Cloud counter requires an Admin or App Manager.

Example: if the highest assigned or uploaded number is 12 and Next Build Number is 13, reserve 13 locally and resume Cloud at 14. Recheck these values each time; never reuse the example number. Do not decrease the counter.

3. Configuration and ownership

The shell scripts live in the frontend repository. Start conditions, signing access, toolchain selection, environment values, and the TestFlight post-action live in Xcode Cloud, not in a GitHub Actions YAML file. Creating the scripts alone does not create the workflow.

Workflow settingVerified configuration
Name / statusOpus - TestFlight main / enabled
Repository / workspace / schemeAscendaFinance/ascenda-app / ios/Opus.xcworkspace / Opus
Start conditionBranch Changes / only main / any file
Auto-cancel BuildsDisabled, so a later push does not cancel an earlier run
EnvironmentXcode 27 (27A266a); macOS Latest Release. This is the verified baseline; revalidate toolchain upgrades.
ActionArchive iOS / Opus / TestFlight (Internal Testing Only)
Post-actionTestFlight Internal Testing - iOS / Archive iOS / Opus Staff

Manage these settings in App Store Connect → Opus → Xcode Cloud → Workflows, or in Xcode's Cloud workflow editor. Initial setup requires Xcode, an Apple Developer Program team, the existing Opus App Store Connect record, permission to manage the workflow, and Xcode Cloud access to the GitHub repository. Preserve the existing bundle identifier and app record.

App environment

VariableRuleConfigure it in
EXPO_PUBLIC_CLERK_PUBLISHABLE_KEYRequired. A real Clerk publishable key beginning with pk_test_ or pk_live_, for the intended instance.Workflow Environment
EXPO_PUBLIC_CORE_API_URLOptional while testing only the frontend. When provided, use a device-reachable HTTPS backend URL without embedded credentials.Workflow Environment
CI_PRIMARY_REPOSITORY_PATHProvided by Xcode Cloud. Do not set it to a developer's local checkout.Xcode Cloud itself

Environment edits affect future builds. Existing installed binaries do not acquire new values; start another build after changing the Clerk key or backend URL. GitHub Actions secrets and a developer's local .env do not populate this workflow automatically.

Keep workflow values redacted in logs. Both EXPO_PUBLIC_ variables are nevertheless embedded in the app and readable by clients: they must never contain a Clerk secret key or backend secret. Choose the Clerk instance deliberately; a successful prefix check does not prove the key is valid.

Frontend-only phase. An absent or empty backend URL is accepted by the CI script. This does not connect the backend, create a mock mode, or guarantee API-dependent screens work. Configure a real reachable backend and retest those flows when integration is ready. Loopback addresses such as localhost refer to the tester's device and are rejected by this script.

What the post-clone script does

  1. Validates the Clerk key prefix and the optional backend URL, failing early on invalid configuration.
  2. Installs or reuses Homebrew node@24 and sets the Node executable.
  3. Runs npm ci --include=dev --no-audit --no-fund, using the committed lockfile and .npmrc.
  4. Installs CocoaPods if needed and runs pod install in ios, honoring locked pod versions.
  5. Writes a build-local ios/.xcode.env.local with Node, PATH, public app values, NODE_ENV=production and EXPO_NO_DOTENV=1. Fresh Xcode build-phase shells source it.

The generated file must not be committed. Keep ios/ci_scripts/ci_post_clone.sh executable and retain the committed native project. Do not add expo prebuild --clean to this workflow: it would remove the CI scripts. Dependency updates should deliberately update and review the relevant lockfiles; delivery uses pod install, not pod update.

The shell harness lives beside the script as ios/ci_scripts/ci_post_clone.test.sh. It uses stubs to check configuration validation, install failures, reuse of tools, and safe environment persistence; it is not an iOS archive or a device test.

4. Publish automatically from main

  1. Review the change and run the relevant frontend checks before merging. Update the release version only if beginning a new release cycle.
  2. Merge or push to main in ascenda-app. Changes to another repository, including this wiki, do not trigger the Opus workflow.
  3. Open App Store Connect → Opus → Xcode Cloud → Builds. Match the run to the expected commit SHA and confirm Archive and the TestFlight post-action complete successfully.
  4. Wait for Apple processing, then verify the version/build appears in TestFlight and is assigned to Opus Staff.
  5. Install it on a real iPhone through TestFlight and run the smoke checks below.

No local Archive or manual group assignment is normally needed. Availability takes time for the Cloud run and Apple processing; there is no fixed delivery deadline.

Group assignment belongs to the post-action. Keep Opus Staff selected in the Cloud TestFlight post-action. The group's generic Enable automatic distribution checkbox alone is not a substitute for that configuration. If an uploaded build is missing from the group, use the manual assignment steps below and repair the post-action for future runs.

5. Publish manually using Xcode Cloud

This is the preferred way to rebuild committed code without making an empty commit. It uses the same preparation, signing, build counter, and Opus Staff post-action as automatic delivery. It does not include uncommitted changes on your Mac.

  1. In App Store Connect, open Opus → Xcode Cloud and find Opus - TestFlight main.
  2. If the desired manual source is not offered, edit Start Conditions and add Manual Start limited to main. Keep the existing Branch Changes condition so automatic delivery continues. This is an optional configuration step, not a claim that Manual Start was already configured.
  3. Choose Start Build for that workflow, select main, review the source selection, and start the run. In Xcode, the equivalent is Product → Xcode Cloud → Start Build.
  4. Verify the commit in the build report, then follow the same processing, group, and device checks as automatic delivery.

For a failed run, read its first failure before retrying. A configuration-only fix can be followed by a manual rebuild; a source fix must be committed. If that source fix is pushed to main, it already starts an automatic run, so avoid launching a duplicate unless necessary.

6. Publish manually from a local Xcode archive

Use this path when a local archive is necessary. It does not run the Cloud post-clone script or inherit Cloud environment variables. Record the source commit and any local differences so the uploaded binary can be reproduced.

Prepare the checkout and environment

Preserve existing local changes before switching branches or pulling. For a reproducible main delivery, use a clean checkout updated from origin/main. On a Mac with compatible Xcode, Node and CocoaPods installed, run from the frontend repository root:

npm ci --include=dev --no-audit --no-fund
cd ios
pod install
cd ..
open ios/Opus.xcworkspace

Open the workspace, not only Opus.xcodeproj, so the Pods are included. Run command -v node and use its absolute result in ios/.xcode.env.local. Edit or merge the following exports into that local file; replace both placeholders before archiving and preserve any necessary local settings:

export NODE_BINARY='/absolute/path/to/node'
export EXPO_PUBLIC_CLERK_PUBLISHABLE_KEY='YOUR_REAL_CLERK_PUBLISHABLE_KEY'
export EXPO_PUBLIC_CORE_API_URL=''
export NODE_ENV=production
export EXPO_NO_DOTENV=1

Use the intended HTTPS backend URL instead of the empty string when integrating the backend. Inspect old local environment overrides: a stale file can silently select a different Clerk instance or API. The explicit exports above match the Cloud bundling approach and do not depend on Expo loading a root .env. Do not commit this file.

Archive and upload

  1. Reserve a local build number using the mixed-upload procedure. Keep the existing release version unless starting a new release cycle.
  2. In the Opus target, confirm the existing bundle identifier, the correct Apple Developer team, and automatic signing.
  3. Select scheme Opus and the Any iOS Device destination. Check Archive uses Release, then choose Product → Archive.
  4. Open Window → Organizer → Archives and select the new Opus archive.
  5. Choose Distribute App → Custom → App Store Connect → Upload. Enable internal testing only and symbol upload; use automatic signing. Disable Manage version and build number to retain the reserved number.
  6. Verify version, build and app in the upload summary, then upload. Use the existing Opus record rather than creating another app.
  7. After processing, add the build to Opus Staff if it is not already assigned. Complete the Cloud counter/resume steps using the accepted build number.

The preset TestFlight Internal Only is also available, but verify its final uploaded build number if Xcode manages numbering. For an archive intended for external beta testing or store submission, choose the appropriate TestFlight & App Store distribution instead and review the release configuration separately.

Add a processed build to Opus Staff

  1. Open App Store Connect → Opus → TestFlight → Internal Testing → Opus Staff.
  2. In Builds, choose Add Builds or the + button.
  3. Select the intended version/build, continue, enter What to Test, and add it.
  4. Confirm the build is available for testing and the intended testers belong to the group.

Internal testers must be eligible App Store Connect users with app access. They accept the invitation and install through TestFlight; builds expire after 90 days. The current Cloud workflow only targets Opus Staff, not every group.

7. Verification and failure recovery

Find the correct report

In the Xcode application, open View → Navigators → Reports or press Command + 9. This is the Report Navigator. Select the Cloud run and inspect the failed action's logs. On the website, use Opus → Xcode Cloud → Builds → the run → the action → Logs. Always match the commit and build number before investigating.

SymptomWhat to check
No Cloud run after a pushRemote main actually changed; workflow enabled; Branch Changes points to main; repository access intact; latest commit does not contain [ci skip].
Missing or invalid Clerk configurationSet the real publishable key in the workflow Environment, then rebuild. Local .env values do not automatically reach Cloud.
Backend URL validation failsLeave it absent/empty during frontend-only testing, or supply a reachable HTTPS URL without localhost or credentials.
npm ci failsInspect dependency/lockfile consistency and .npmrc. Fix and commit the dependency files; do not replace CI's lockfile install with an unreviewed update.
CocoaPods deployment/checksum failureThe validated script uses pod install. Strict --deployment previously rejected generated podspec checksum/toolchain differences. Verify the run uses the corrected script; review actual dependency changes separately.
Signing or provisioning failureCorrect team, app identifier, signing permissions, and Archive distribution method. A Development/device-registration issue is a different signing path from the intended TestFlight archive.
Upload rejects a duplicate buildVersion/build already uploaded. Reserve a new integer; synchronize Next Build Number when mixing local and Cloud uploads.
Archive succeeds but group has no updateApple processing and the TestFlight post-action status; Opus Staff selection; build membership; any pending compliance request. An Archive success alone does not prove distribution.
Build requests encryption informationInspect the actual app's encryption and answer accurately. The tracked baseline has ITSAppUsesNonExemptEncryption=false in Info.plist and the matching Expo flag; reassess if encryption changes.
App crashes at launchInstall the exact build, obtain a symbolicated crash log in Organizer or the device log, and inspect the cause. Check bundled Clerk configuration, but do not assume every splash-screen crash has that cause.

Before calling a beta delivery usable

  • Verify the expected commit, release version, accepted build number, and Opus Staff assignment.
  • Install from TestFlight on a real iPhone and cold-launch beyond the splash screen.
  • Check Clerk authentication and the relevant navigation/screens.
  • When a backend is configured, test API-dependent flows against that environment; when it is not, record that limitation in What to Test.
  • Record reproducible issues with the version/build and retain symbols for crash investigation. A previous TestFlight build can remain available while a fix is prepared.

Run the relevant frontend validations before merging; they are not currently automated by this delivery workflow:

npm run lint
npx tsc --noEmit
npm test -- --runInBand
bash ios/ci_scripts/ci_post_clone.test.sh

For a documentation-only change to this wiki, use the wiki's own build/check commands instead. Changing this guide does not add quality gates to the frontend pipeline.

8. Sources and maintenance

Update this page when workflow settings, environment requirements, distribution groups, or version policy change. The source files below are pinned to the verified frontend baseline so the documented implementation can be inspected; compare with current main when maintaining it. Workflow settings were verified directly in Xcode and App Store Connect, culminating in the automated delivery of 1.0.0 (4).