diff --git a/.agents/skills/ci-cd-publishing/SKILL.md b/.agents/skills/ci-cd-publishing/SKILL.md index 5eae3041..93f0e4cf 100644 --- a/.agents/skills/ci-cd-publishing/SKILL.md +++ b/.agents/skills/ci-cd-publishing/SKILL.md @@ -162,9 +162,9 @@ The `publish-google-play` CI job uploads Android AABs to [Google Play](https://p Ditto's iOS pipeline is split across two jobs: - **`build-ipa`** (stage `build`, `tags: [macos]`) runs on the self-hosted Mac runner. Decodes the App Store Connect API key, fetches the encrypted distribution cert + provisioning profile via fastlane match, builds the web assets, runs `cap sync ios`, stamps the marketing version into `project.pbxproj`, then `fastlane build_ipa` produces a signed App Store IPA at `artifacts/Ditto.ipa`. The IPA is uploaded to the GitLab Generic Packages registry as `Ditto-${CI_COMMIT_TAG}.ipa` (mirrors how `build-apk` publishes the APK and AAB) and exposed as a CI artifact for downstream jobs. -- **`publish-app-store`** (stage `publish`, `image: ruby:3.3` on a shared Linux runner) consumes the IPA artifact via `needs: [build-ipa]` and the release-notes artifact via `needs: [release-notes]`. Installs fastlane via `gem install`, decodes the API key, copies the release-notes summary into `ios/fastlane/metadata/en-US/release_notes.txt`, and runs `fastlane submit_release` which calls `deliver` to upload metadata + select the prebuilt build + auto-submit for App Store review. No Xcode required, no signing in this job — it's just an Apple API call. +- **`publish-app-store`** (stage `publish`, `tags: [macos]`) also runs on the self-hosted Mac runner. Consumes the IPA artifact via `needs: [build-ipa]` and the release-notes artifact via `needs: [release-notes]`. Decodes the API key, copies the release-notes summary into `ios/fastlane/metadata/en-US/release_notes.txt`, and runs `fastlane submit_release` which calls `deliver` to upload metadata + push the prebuilt IPA + auto-submit for App Store review. **macOS is required** even though the IPA is already signed: `fastlane deliver` shells out to Apple's iTMSTransporter / altool to upload the binary, and those tools only ship inside Xcode. A Linux container ran into `No such file or directory @ dir_chdir0` from `JavaTransporterExecutor#execute` because `Helper.itms_path` resolved to a missing Xcode path. -The Mac runner is therefore only used for `build-ipa`. For runner administration (operating the Mac, restarting the agent, viewing logs, rotating signing certs), load the **`mac-runner`** skill. +The Mac runner is therefore used for both iOS jobs. For runner administration (operating the Mac, restarting the agent, viewing logs, rotating signing certs), load the **`mac-runner`** skill. **Configuration files:** @@ -176,7 +176,7 @@ The Mac runner is therefore only used for `build-ipa`. For runner administration - `ios/fastlane/Appfile` — bundle identifier and team ID - `ios/fastlane/Matchfile` — points at the shared `soapbox-pub/certificates` repo - `ios/fastlane/metadata/en-US/release_notes.txt` — placeholder; CI overwrites it with the release summary paragraph from `CHANGELOG.md` per release -- `.gitlab-ci.yml` — `build-ipa` (Mac runner, `tags: [macos]`) + `publish-app-store` (Linux runner) +- `.gitlab-ci.yml` — `build-ipa` and `publish-app-store` both run on the Mac runner (`tags: [macos]`) **Code signing storage**: a private GitLab repo `soapbox-pub/certificates` holds encrypted distribution certs and provisioning profiles, managed by [fastlane match](https://docs.fastlane.tools/actions/match/). Match handles cert/profile lifecycle: one passphrase decrypts everything; the same repo can hold signing material for multiple Soapbox iOS apps under team `GZLTTH5DLM`. @@ -315,12 +315,12 @@ App Store Connect API keys can be revoked anytime. To rotate: ### Key points -- `build-ipa` (Mac) produces a signed **IPA** (App Store distribution format) and uploads it to GitLab's Generic Packages registry. `publish-app-store` (Linux) submits it to Apple via `deliver`. +- `build-ipa` (Mac) produces a signed **IPA** (App Store distribution format) and uploads it to GitLab's Generic Packages registry. `publish-app-store` (also Mac) submits it to Apple via `deliver`. - Builds go to **App Store Connect**, automatically submit for review, but do **not** auto-release after approval. The final "Release" click is manual in the web UI. - Marketing version comes from the git tag (`v2.1.0` → `MARKETING_VERSION = 2.1.0`); build number comes from `CI_PIPELINE_IID`. - Release notes ("What's New in This Version") come from the release-notes summary paragraph (see "Release notes pipeline" below). - `setup_ci` (in `build-ipa`) creates an ephemeral keychain per build, so the runner never touches the login keychain — works whether or not a GUI session is logged in. -- `publish-app-store` doesn't sign anything, so it doesn't need macOS or a keychain — pure Apple API call. +- `publish-app-store` does no code signing, but it still needs macOS: `fastlane deliver` shells out to Apple's iTMSTransporter / altool to upload the binary, and those tools only ship inside Xcode. ## Release notes pipeline diff --git a/.agents/skills/mac-runner/SKILL.md b/.agents/skills/mac-runner/SKILL.md index edcaaaa4..eaaba2a5 100644 --- a/.agents/skills/mac-runner/SKILL.md +++ b/.agents/skills/mac-runner/SKILL.md @@ -5,7 +5,7 @@ description: Operate the self-hosted GitLab Runner on the Mac that builds Ditto' # Mac Runner Operations -Ditto's iOS pipeline splits into two CI jobs: `build-ipa` runs on a self-hosted GitLab Runner on a MacBook in the rack (Xcode + signing only run on macOS), and `publish-app-store` runs on a shared Linux runner (no signing — just an Apple API call). This skill covers operating the Mac. +Ditto's iOS pipeline runs two CI jobs on a self-hosted GitLab Runner on a MacBook in the rack: `build-ipa` (signs and builds the IPA via Xcode + fastlane match) and `publish-app-store` (uploads the IPA via `fastlane deliver`, which shells out to Apple's iTMSTransporter — that tool only ships inside Xcode, so this job can't run on Linux). This skill covers operating the Mac. This skill covers operating the runner: SSH access, restarting after crashes or Xcode updates, watching logs, debugging fastlane locally, and rotating the match certificates. For initial provisioning, App Store Connect API key creation, and GitLab CI variable setup, load the **`ci-cd-publishing`** skill. @@ -27,7 +27,7 @@ This skill covers operating the runner: SSH access, restarting after crashes or - **User**: `alex` (the runner runs in user-mode so it can access keychain and Xcode UI tooling) - **Tooling**: Homebrew (`/opt/homebrew`), `gitlab-runner`, `node@22`, `ruby@3.3`, fastlane installed as a user gem under `~/.gem/ruby/3.3.0/` - **Service**: launchd LaunchAgent at `~/Library/LaunchAgents/gitlab-runner.plist`. `KeepAlive=true` (auto-restart on crash) and `RunAtLoad=true` (starts on login). The agent loads when `alex` logs in via auto-login at boot. -- **Tags**: `macos`, `ios`, `xcode` — only the `build-ipa` job in `.gitlab-ci.yml` targets this runner. The `publish-app-store` job runs on a shared Linux runner (no signing or Xcode needed there — it's a pure Apple API call). +- **Tags**: `macos`, `ios`, `xcode` — both `build-ipa` and `publish-app-store` in `.gitlab-ci.yml` target this runner. `publish-app-store` doesn't sign anything, but it still needs Xcode's bundled iTMSTransporter to push the IPA to App Store Connect. - **Shell setup**: `~/.bash_profile` sources brew shellenv and prepends `~/.gem/ruby/3.3.0/bin` and `/opt/homebrew/opt/ruby@3.3/bin` to `PATH` so `bash --login` (the runner's executor) finds fastlane + ruby 3.3. ### Why Ruby 3.3, not the brewed 4.0 diff --git a/.agents/skills/release/SKILL.md b/.agents/skills/release/SKILL.md index 97ce18c2..ab0a3778 100644 --- a/.agents/skills/release/SKILL.md +++ b/.agents/skills/release/SKILL.md @@ -319,7 +319,7 @@ The CI pipeline (`.gitlab-ci.yml`) is triggered by tags matching the pattern `/^ 4. **release**: Creates a GitLab Release with the full changelog section and APK / AAB / IPA download links 5. **publish-zapstore**: Publishes the APK to Zapstore 6. **publish-google-play**: Uploads the AAB to Google Play production track and writes the release summary to `metadata/android/en-US/changelogs/.txt` -7. **publish-app-store**: Submits the prebuilt IPA to App Store Connect for review with the release summary as the "What's New" text (runs on a shared Linux runner; no Xcode needed since the IPA is already built). The build appears in App Store Connect within ~30 minutes; Apple's human review then takes 24-48 hours typically. Once approved, you must release manually in App Store Connect (`automatic_release: false`) — this is the final human gate. For runner operations, match cert rotation, and debugging, load the **`mac-runner`** skill. +7. **publish-app-store**: Submits the prebuilt IPA to App Store Connect for review with the release summary as the "What's New" text. Runs on the self-hosted Mac runner (`tags: [macos]`) because `fastlane deliver` shells out to Apple's iTMSTransporter to upload the IPA, and that tool only ships inside Xcode — the previous Linux runner crashed at the upload step with `No such file or directory @ dir_chdir0` because `Helper.itms_path` resolved to a missing Xcode path. The build appears in App Store Connect within ~30 minutes; Apple's human review then takes 24-48 hours typically. Once approved, you must release manually in App Store Connect (`automatic_release: false`) — this is the final human gate. For runner operations, match cert rotation, and debugging, load the **`mac-runner`** skill. ## Troubleshooting diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 8be353a7..471a26b2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -389,7 +389,15 @@ publish-google-play: publish-app-store: stage: publish - image: ruby:3.3 + # Runs on the self-hosted Mac runner, same as build-ipa. fastlane's `deliver` + # action shells out to Apple's iTMSTransporter / altool to upload the IPA + # binary, and those tools ship inside Xcode. On a generic Linux container + # the upload step crashes with `No such file or directory @ dir_chdir0` + # because `Helper.itms_path` resolves to a path inside Xcode that doesn't + # exist. The IPA is already signed in `build-ipa`; we just need an Apple + # tool to push it, which means macOS. + tags: + - macos needs: - job: build-ipa artifacts: true @@ -403,7 +411,8 @@ publish-app-store: FASTLANE_HIDE_CHANGELOG: "1" FASTLANE_SKIP_UPDATE_CHECK: "1" before_script: - - gem install fastlane --no-document + # PATH is set up via ~/.bash_profile on the runner host (brew + Ruby 3.3 + user gems) + - ruby --version - fastlane --version | head -3 # Decode the App Store Connect API key (.p8) into a private location. @@ -413,6 +422,9 @@ publish-app-store: - export ASC_KEY_PATH="$HOME/.private_keys/AuthKey_${APP_STORE_CONNECT_API_KEY_ID}.p8" - echo "$APP_STORE_CONNECT_API_KEY_P8_BASE64" | base64 -d > "$ASC_KEY_PATH" - chmod 600 "$ASC_KEY_PATH" + # Avoid env-var collision: match's APP_STORE_CONNECT_API_KEY_PATH expects + # a JSON descriptor; we pass the API key inline via the Fastfile. + - unset APP_STORE_CONNECT_API_KEY_PATH || true script: - test -f artifacts/Ditto.ipa - test -f artifacts/release-notes-summary.txt diff --git a/AGENTS.md b/AGENTS.md index ec1d961e..612bc5d2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -353,7 +353,7 @@ Ditto uses GitLab CI (`.gitlab-ci.yml`) with five stages: 2. **deploy** — `deploy-nsite` builds and uploads `dist/` to nsite via nsyte (default branch only). 3. **build** — `build-apk` produces a signed APK and AAB (Linux); `build-ipa` produces a signed IPA on the self-hosted Mac runner; `release-notes` extracts the changelog section + summary paragraph from `CHANGELOG.md`. All three run on tags only. 4. **release** — creates a GitLab Release with the changelog body and APK / AAB / IPA artifacts (tags only). -5. **publish** — `publish-zapstore` (APK → Zapstore), `publish-google-play` (AAB → Google Play with the release summary as "What's new"), and `publish-app-store` (IPA → App Store Connect with the release summary as "What's New", runs on a shared Linux runner since the IPA is already signed in `build-ipa`), tags only. +5. **publish** — `publish-zapstore` (APK → Zapstore), `publish-google-play` (AAB → Google Play with the release summary as "What's new"), and `publish-app-store` (IPA → App Store Connect with the release summary as "What's New", runs on the self-hosted Mac runner because `fastlane deliver` shells out to Apple's iTMSTransporter to push the binary and that tool only exists inside Xcode), tags only. To cut a release, load the **`release`** skill — it walks through version bumping (`X.Y.Z`), changelog generation, native build-file updates, and tagging/pushing (`vX.Y.Z`) to trigger the CI pipeline.