<!-- https://ios.app.promptugal.news/admin/ci.html · PrompTugal for iOS 0.4.106 -->

Developer area

# Continuous integration

Nobody on the team can compile Swift on Linux, and most of the code is written by agents on Linux. So CI is the compiler: every branch is built and tested on a Mac in GitHub Actions, and a red branch is not merged.

## Two jobs for two things

- **The app.** The iOS workflow runs on every push to a `night/*` branch and to `dev`, and on pull requests. It tests the vault package with `swift test`, builds the app for the simulator without signing, and runs the unit tests on an iPhone simulator. A failure puts the compiler error in the job summary, so a session can read it through the GitHub tools.
- **The release and this site.** The release workflow runs on every push to `dev`. It bumps `VERSION`, tags the release, then tests, builds and validates this site and deploys it to GitHub Pages. The deploy is only done when the live [`version.json`](../version.json) serves the new release.

Below is what each workflow file on `dev` says, read on every build. A workflow that is still on a session branch appears here once it is merged.

Latest app build

## The newest successful iOS build on dev

Read when this site was built, from the release workflow’s `build-info` artifact (`scripts/release/latest-build.sh`). The same data is in [`ios-build.json`](../ios-build.json). The builds are unsigned; installing one needs re-signing (see the release setup brief).

**App version**
0.4.94 (build 592)

**Commit**
[`6e11482`](https://github.com/PrompTugal/PrompTugal__App__iOS/commit/6e11482)

**Built**
2026-11-11 13:49 UTC

**Devices**
iPhone and iPad

**Minimum iOS**
18.0

**Xcode and SDK**
26.6, iphoneos26.5

**Sizes**
app 6.0 MB, unsigned .ipa 4.9 MB

**Run**
[43023915403](https://github.com/PrompTugal/PrompTugal__App__iOS/actions/runs/43023915403)

Artifacts of that run (GitHub keeps them for a limited time):

- `appstore-ipas` (13.0 MB)
- `build-info-apps` (6 KB)

From .github/workflows

## 9 workflows

### iOS changes [`ios-changes.yml`](https://github.com/PrompTugal/PrompTugal__App__iOS/blob/dev/.github/workflows/ios-changes.yml)

Runs on: `workflow_call`. 1 jobs, 3 named steps.

```
Reusable. Decides which apps and Swift packages a run has to build, test and
upload, from apps.json and the diff (contracts section 4 and 5). Ubuntu only,
so it costs no macOS runner. Called by ios.yml and ios-release.yml.

Before the split lands there is no apps.json: scripts/release/apps.py then
describes today's single app (the root project). An app whose project does not
exist yet is skipped, with a line in the run summary.
```

1. Use another branch's tree (source_ref)
2. Rules self-test
3. Plan

### iOS release [`ios-release.yml`](https://github.com/PrompTugal/PrompTugal__App__iOS/blob/dev/.github/workflows/ios-release.yml)

Runs on: `push`, `workflow_dispatch`. 3 jobs, 23 named steps.

```
Packaging and distribution for the apps in apps.json (contracts section 5).

changes   ubuntu: which apps this run touches, from apps.json and the diff
(ios-changes.yml). An app whose project does not exist yet is skipped.
package   per affected app: pushes to dev (not docs/site-only), release tags,
and by hand. Unsigned: .xcarchive, .ipa (Payload/<App>.app zipped) and
a simulator .app, uploaded as artifacts. Needs no Apple account.
testflight  per affected app with "testflight": true whose unsigned package
succeeded: signs, exports and uploads to THAT app's App Store Connect
record. The built bundle id must equal apps.json, and the record must
exist for it, or nothing is uploaded.

Triggers
push to dev          NOT here since 10 Nov 2026: ios.yml's one macOS job builds, tests and, when
green, uploads the affected apps from the release commit website.yml tags
(briefs/2026-11-10__ci-on-one-mac). Pausing works the same way there.
tags                 team-v*, promptugal-v*, riskmandate-v*, fringe-v* release that app;
the old ios-v* means the app with bundle news.promptugal.reader
workflow_dispatch    input "app": all, or one app; "upload" or "dry_run" to sign

Pausing (repository variables, Settings > Secrets and variables > Actions > Variables)
TESTFLIGHT_ON_PUSH=false           pauses push uploads of every app
TESTFLIGHT_ON_PUSH_TEAM=false      pauses one app (…_PROMPTUGAL, …_RISKMANDATE, …_FRINGE)
Tags and manual runs still upload. The same switches pause ios.yml's uploads.

Version: MARKETING_VERSION of every app = VERSION at the built commit (v0.4.7 -> 0.4.7), passed to
xcodebuild. Summary and TestFlight notes say "0.4.7 (build) · tag v0.4.7 · commit <sha>".
Build number: CFBundleVersion = run number of THIS workflow + the app's offset (ios.yml uses its own
run number, which is higher; to upload a version ios.yml already uploaded, set build_number), passed as
CURRENT_PROJECT_VERSION. Offset: variable IOS_BUILD_OFFSET_<APP>, else IOS_BUILD_OFFSET,
else 100 (above the manual builds; PrompTugal's last manual build is 6). Both jobs of one
app use the same number. MARKETING_VERSION comes from the app's own project.

Secrets: ASC_* and the certificate are shared. Profiles are per app:
IOS_PROFILE_<APP>_{APP,WIDGETS,INTENTS,SHARE}_BASE64, for example IOS_PROFILE_TEAM_APP_BASE64.
The app with bundle news.promptugal.reader also accepts the old IOS_PROFILE_{APP,WIDGETS,INTENTS}_BASE64.
Setup: briefs/2026-11-08__overnight-push/release-setup.md
```

1. **changes**
1. Use another branch's project (source_ref)
2. Select Xcode
3. Version and build number
4. Archive and package (unsigned)
5. Bundle id matches apps.json
6. Size check
7. Release notes
8. Upload unsigned .ipa
9. Upload simulator app
10. Upload .xcarchive
11. Upload build-info.json
12. Upload logs

1. Check signing secrets
2. Use another branch's project (source_ref)
3. Select Xcode
4. Version and build number
5. Install certificate, profiles and API key
6. App Store Connect record exists?
7. Archive, sign, export and upload
8. Wait for processing, answer export compliance, set What to Test
9. Upload signed .ipa
10. Upload logs
11. Remove keychain, profiles and key

### iOS App Store submit [`ios-submit.yml`](https://github.com/PrompTugal/PrompTugal__App__iOS/blob/dev/.github/workflows/ios-submit.yml)

Runs on: `workflow_dispatch`. 1 jobs, 4 named steps.

```
Submits a build that is already in App Store Connect (uploaded by
"iOS release") for App Store review. By hand only. Metadata, screenshots,
pricing and the privacy questions stay manual in App Store Connect.
Without "confirm" it only prints the plan.
```

1. Check secrets
2. Wait for the build to be processed
3. Export compliance
4. Submit

### iOS vault round trip [`ios-vault.yml`](https://github.com/PrompTugal/PrompTugal__App__iOS/blob/dev/.github/workflows/ios-vault.yml)

Runs on: `push`, `workflow_dispatch`. 3 jobs, 2 named steps.

```
Swift writes a vault, sgit reads it back. Owned by the vault session.
ios.yml already runs `swift test` for Packages/SGitVault and builds the app, so this
workflow runs only when the write path changes (or by hand), to save macOS runners:
swift-writes     macOS: the write tests export the vaults Swift wrote (SGITVAULT_ROUNDTRIP_OUT)
sgit-reads-swift Linux: sgit-ai 0.27 clones each, switches to the Swift branch,
verifies every signature and compares every file
reference-reader Linux: the fixtures, read by the Python reference reader
```

1. Write tests, exporting what Swift wrote

1. sgit clones, switches branch, verifies signatures, compares files

3. **reference-reader** on `ubuntu-latest`

### iOS [`ios.yml`](https://github.com/PrompTugal/PrompTugal__App__iOS/blob/dev/.github/workflows/ios.yml)

Runs on: `push`, `pull_request`, `workflow_dispatch`. 7 jobs, 14 named steps.

```
CI is the compiler: nobody in the overnight push can build Swift on Linux.
Every job copies compiler and test errors into its job summary, so a session
can read a failure through the GitHub tools (actions_list, get_job_logs).

Only what a change touches is built (contracts section 5). The `changes` job
(ubuntu, ios-changes.yml) reads apps.json and the diff, then ONE macOS job does
all of it (briefs/2026-11-10__ci-on-one-mac):

mac       per affected app: build once, unit tests on an iPhone and an iPad simulator
(dev, pull requests and workflow_dispatch; night/** pushes build only), the
Release build for a device (unsigned); then each affected package's tests
quick     manual runs only: Debug build and iPhone tests, no Release build, no iPad
result    one stable check name that is green when everything above is

An app whose project does not exist yet is skipped, with a line in the summary.
Before apps.json exists, the built-in config builds today's single app.

Pushes that change only briefs/, website/, root Markdown or VERSION don't run
it: macOS runners are shared by every session, and those files can't break
the app. (A .md file inside PrompTugal/ can, so those still run.)
```

1. **pick-runner** on `ubuntu-latest`

2. **gate** on `ubuntu-latest`

3. **changes**
1. check_apps.py

1. Select Xcode
2. Build the apps and packages
3. Tests on the iPhone simulator
4. Tests on the iPad simulator
5. Find this push's release commit
6. Release build (no release commit, so no upload)
7. Sign, export and upload to TestFlight
8. Remove keychain, profiles and key
9. Upload build-info and release notes
10. Upload signed .ipa files
11. Delete the simulators (self-hosted only)
12. Upload logs and test results (failures only)

1. Wait for processing, answer export compliance, set What to Test

7. **result** on `ubuntu-latest`

### Screens [`screens.yml`](https://github.com/PrompTugal/PrompTugal__App__iOS/blob/dev/.github/workflows/screens.yml)

Runs on: `workflow_dispatch`. 5 jobs, 10 named steps.

```
Screenshots of every main screen on an iPhone and an iPad simulator, so the
team can see the app without a Mac. By hand only (workflow_dispatch): macOS runners
are shared, and branch CI (ios.yml) stays at a few jobs.
Per app: only the apps a change touches, and only apps whose project has a shared
"<Scheme>Screens" scheme (optional apps.json keys screensScheme, screensCount).
Artifact: screens-<sha>, files <iphone|ipad>-<destination id>.png, longest side
1600 px. The app with bundle news.promptugal.reader keeps that name (the editor
publishes it on the site from successful dev runs); the others get screens-<sha>-<app>.
```

1. **changes**
1. Select Xcode
2. Pick and boot an ‹matrix.device› simulator
3. Take the screenshots
4. Export PNGs

3. **collect** on `ubuntu-latest`
1. Select Xcode
2. Create and boot the ‹matrix.size.kind› simulator
3. Take the screenshots, once per locale
4. Normalise, check sizes, convert to JPEG
5. Delete the simulator

1. Lay out as <locale>/<display type>/<NN-id>.jpg

### TestFlight ops [`testflight-ops.yml`](https://github.com/PrompTugal/PrompTugal__App__iOS/blob/dev/.github/workflows/testflight-ops.yml)

Runs on: `workflow_dispatch`. 1 jobs, 2 named steps.

```
Manual TestFlight / Beta App Review operations through the App Store Connect API
(scripts/testflight/tf.py). Every changing action only prints its plan unless
"confirm" is ticked. The API key comes from the repository secrets; nothing is printed.

status               builds, processing, beta review state, groups, expiry
add-to-group         build + group
remove-from-group    build + group
set-test-info        build; sets What to Test (scripts/testflight/what-to-test.txt)
and the Beta App Review details (scripts/testflight/review-info.json
+ review-notes.txt; contact and demo keys from secrets)
submit-beta-review   build
expire               build
```

1. Unit tests (no network)
2. Run action

### Wallet pass [`wallet-pass.yml`](https://github.com/PrompTugal/PrompTugal__App__iOS/blob/dev/.github/workflows/wallet-pass.yml)

Runs on: `workflow_dispatch`. 1 jobs, 3 named steps.

```
The RiskMandate business card as an Apple Wallet pass (scripts/wallet/wallet_pass.py), at Dinis's word
(9 November 2026). Run by hand. Without "confirm" it only checks the Pass Type ID. With it, it creates the ID if
needed and a new pass certificate through the App Store Connect API, signs the pass, and uploads card.pkpass as an
artifact, and commits it to briefs/2026-11-09__wallet-pass/card.pkpass on dev ([skip ci]; a signed pass holds no
secret). The certificate's private key exists only on the runner and is deleted; nothing secret is printed.
```

1. Make the pass
2. Upload the pass
3. Commit the signed pass (no secret in it)

### Release and deploy [`website.yml`](https://github.com/PrompTugal/PrompTugal__App__iOS/blob/dev/.github/workflows/website.yml)

Runs on: `push`, `pull_request`, `workflow_dispatch`. 3 jobs, 11 named steps.

```
On every push to dev:
1. release  bump /VERSION (v0.1.n -> v0.1.n+1), commit it, tag it, push both to dev
2. build    test, build and validate the site from that release commit
3. deploy   publish to GitHub Pages (ios.app.promptugal.news) and confirm the
live site serves the release; the deploy is not done until it does
On pull requests, only the build job runs, with no release and no deploy.

The release commit is pushed with GITHUB_TOKEN, which does not start another
workflow run, so there is no loop.
```

1. Bump the release version
2. Commit, tag and push

1. Test the builder and the validator
2. Latest iOS build
3. Latest app screenshots
4. Build
5. Validate
6. Show the version stamp
7. Upload the Pages artifact

1. Deploy to GitHub Pages
2. Verify live
