PrompTugal for iOS

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 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. The builds are unsigned; installing one needs re-signing (see the release setup brief).

App version
0.4.94 (build 592)
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

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

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. changes on ubuntu-latest
    1. Use another branch's tree (source_ref)
    2. Rules self-test
    3. Plan

iOS release 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. package on macos-26
      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
    2. testflight on macos-26
      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

    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. submit on ubuntu-latest
      1. Check secrets
      2. Wait for the build to be processed
      3. Export compliance
      4. Submit

    iOS vault round trip 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. swift-writes on macos-15
      1. Write tests, exporting what Swift wrote
    2. sgit-reads-swift on ubuntu-latest
      1. sgit clones, switches branch, verifies signatures, compares files
    3. reference-reader on ubuntu-latest

      iOS 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
        1. gate on ubuntu-latest
          1. changes
            1. check-apps on ubuntu-latest
              1. check_apps.py
            2. mac on ‹needs.pick-runner.outputs.runner›
              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)
            3. testflight-processing on ubuntu-latest
              1. Wait for processing, answer export compliance, set What to Test
            4. result on ubuntu-latest

              Screens 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. shoot on macos-26
                  1. Select Xcode
                  2. Pick and boot an ‹matrix.device› simulator
                  3. Take the screenshots
                  4. Export PNGs
                2. collect on ubuntu-latest
                  1. store on macos-26
                    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
                  2. store-collect on ubuntu-latest
                    1. Lay out as <locale>/<display type>/<NN-id>.jpg

                  TestFlight ops 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. ops on ubuntu-latest
                    1. Unit tests (no network)
                    2. Run action

                  Wallet pass 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. pass on ubuntu-latest
                    1. Make the pass
                    2. Upload the pass
                    3. Commit the signed pass (no secret in it)

                  Release and deploy 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. release on ubuntu-latest
                    1. Bump the release version
                    2. Commit, tag and push
                  2. build on ubuntu-latest
                    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
                  3. deploy on ubuntu-latest
                    1. Deploy to GitHub Pages
                    2. Verify live