Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 16 additions & 5 deletions .claude/skills/archive/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ Use these values based on the selected platform:
| Setting | iOS | macOS |
|---------|-----|-------|
| Destination | `generic/platform=iOS` | `generic/platform=macOS` |
| Test destination | `platform=iOS Simulator,name=iPhone 17 Pro` | `platform=macOS` |
| Test destination | `platform=iOS Simulator,name=iPhone 17 Pro` | *(none — step 2b is skipped)* |
| Tag prefix | `ios` | `macos` |
| `asc.py --platform` | `ios` | `macos` |
| Release notes file | `release-notes/ios-{version}.txt` | `release-notes/macos-{version}.txt` |
Expand All @@ -58,18 +58,29 @@ Verify that the Release/production build will NOT use localhost. Check `app/flyf
- The localhost URL (`localhost.ro-z.me:8443`) must only appear inside `#if targetEnvironment(simulator)` or `#if DEBUG`
- If localhost is in the production path, **stop and warn the user**

### 2b — App tests
### 2b — App tests (iOS only)

Run the Xcode test suite using the platform-appropriate destination:
**macOS: skip this step** and say so in the checklist. Tests are only kept green
on iOS: the XCUI journeys are iOS-only, and the unit target crashes its Mac host
app in the SwiftData test fixtures (pre-existing, not investigated).

**iOS:** run the whole scheme — the unit target *and* the XCUI journeys
(`flyfun-formsUITests`):
```bash
rm -rf /tmp/archive-tests.xcresult
xcodebuild test \
-project app/flyfun-forms/flyfun-forms.xcodeproj \
-scheme flyfun-forms \
-destination "{test_destination}" \
-resultBundlePath /tmp/archive-tests.xcresult \
-quiet \
2>&1 | tail -30
xcrun xcresulttool get test-results summary --path /tmp/archive-tests.xcresult
```
If tests fail, stop and show the failures. Use timeout of 300000ms.

CI only runs the journeys nightly, so this is the one place a release is guaranteed to have passed them. Allow ~10 minutes: use a timeout of 900000ms, in the background if needed.

Read `totalTestCount` / `failedTests` from the summary rather than trusting `** TEST SUCCEEDED **`: a filter that matches nothing prints that having run zero tests. If `failedTests` > 0 or `totalTestCount` is 0, stop and show the `testFailures`.

### 2c — Backend tests

Expand Down Expand Up @@ -135,7 +146,7 @@ For `MARKETING_VERSION`, apply the bump type:

Update ALL occurrences in `project.pbxproj` using the Edit tool with `replace_all`. There are typically 2 occurrences of `MARKETING_VERSION` and 2 of `CURRENT_PROJECT_VERSION` for the main target (Debug + Release).

**Important**: Only update the entries for the main target (flyfun-forms), not the test target. The test target entries typically have different surrounding context. Check line numbers to distinguish them.
**Important**: Only update the entries for the main target (flyfun-forms), not the two test targets (`flyfun-formsTests`, `flyfun-formsUITests`). The test target entries typically have different surrounding context. Check line numbers to distinguish them.

Show the user: "Bumped to X.Y (build N)"

Expand Down
127 changes: 127 additions & 0 deletions .github/workflows/ios-ui-nightly.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
name: iOS UI (nightly)

# The XCUI journeys in `flyfun-formsUITests`, run nightly rather than on PRs
# (see ios.yml for why). Breakage surfaces within a day, and the result bundle
# carries the screenshots to explain it. Not a gate: a failure here is a
# message, not a block.
#
# The journeys run the app in its UI-test mode (FLYFUN_UITEST + FLYFUN_MOCK):
# in-memory fixtures, every request answered by a stub. Nothing here reaches a
# server or an iCloud account.

on:
schedule:
# Off-round minute: scheduled jobs queue at the top of the hour.
- cron: '23 3 * * *'
workflow_dispatch:

permissions:
contents: read
# The `changed` guard reads this workflow's own run history.
actions: read

concurrency:
group: ios-ui-nightly
cancel-in-progress: false

jobs:
# Skip the simulator when main has not moved since the last green run.
# Compared against the last *successful* run, so a broken main re-tests
# nightly until it is fixed rather than going quiet after one red run.
changed:
name: New commits since last pass?
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
should_run: ${{ steps.check.outputs.should_run }}
steps:
- id: check
env:
GH_TOKEN: ${{ github.token }}
run: |
last=$(gh run list --repo "$GITHUB_REPOSITORY" \
--workflow ios-ui-nightly.yml --status success \
--limit 1 --json headSha -q '.[0].headSha // ""')
echo "HEAD: $GITHUB_SHA"
echo "last green on: ${last:-<none>}"
if [ "$last" = "$GITHUB_SHA" ]; then
echo "should_run=false" >> "$GITHUB_OUTPUT"
echo "Unchanged since the last green run — skipping the simulator." >> "$GITHUB_STEP_SUMMARY"
else
echo "should_run=true" >> "$GITHUB_OUTPUT"
fi

ui:
name: xcodebuild test (UI)
needs: changed
if: github.event_name == 'workflow_dispatch' || needs.changed.outputs.should_run == 'true'
runs-on: macos-26
timeout-minutes: 60

env:
# Pinned to match ios.yml: the two workflows differ in what they run, nothing else.
DEVELOPER_DIR: /Applications/Xcode_26.6.app/Contents/Developer
DESTINATION: 'platform=iOS Simulator,name=iPhone 17,OS=latest'

steps:
- uses: actions/checkout@v4

- name: Cache SPM checkouts
uses: actions/cache@v4
with:
path: .spm
key: spm-${{ runner.os }}-${{ hashFiles('app/flyfun-forms/flyfun-forms.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}
restore-keys: spm-${{ runner.os }}-

- name: Resolve packages
run: |
xcodebuild -resolvePackageDependencies \
-project app/flyfun-forms/flyfun-forms.xcodeproj \
-scheme flyfun-forms \
-clonedSourcePackagesDirPath .spm

# -retry-tests-on-failure re-runs only the tests that failed, so a
# simulator launch hiccup self-heals and what is reported reproduces.
- name: UI tests
run: |
set -o pipefail
xcodebuild test \
-project app/flyfun-forms/flyfun-forms.xcodeproj \
-scheme flyfun-forms \
-destination "$DESTINATION" \
-only-testing:flyfun-formsUITests \
-clonedSourcePackagesDirPath .spm \
-resultBundlePath ui.xcresult \
-retry-tests-on-failure \
-test-iterations 2 \
-skipMacroValidation \
-quiet

# Zero tests run is a silent pass (see ios.yml). `-quiet` also swallows
# the assertion text, so failures are printed into the run summary rather
# than needing the result bundle downloaded to read four lines.
- name: Guard against a vacuous pass
if: always()
run: |
summary=$(xcrun xcresulttool get test-results summary --path ui.xcresult)
total=$(echo "$summary" | python3 -c 'import json,sys; print(json.load(sys.stdin)["totalTestCount"])')
passed=$(echo "$summary" | python3 -c 'import json,sys; print(json.load(sys.stdin)["passedTests"])')
failed=$(echo "$summary" | python3 -c 'import json,sys; print(json.load(sys.stdin)["failedTests"])')
echo "UI journeys: $passed passed, $failed failed (of $total)" >> "$GITHUB_STEP_SUMMARY"
if [ "$total" -eq 0 ]; then
echo "::error::xcodebuild ran 0 UI tests — the -only-testing filter matched nothing."
exit 1
fi
if [ "$failed" -gt 0 ]; then
echo "$summary" | python3 -c "import json,sys; [print('- **%s** - %s' % (t.get('testName'), t.get('failureText'))) for t in json.load(sys.stdin).get('testFailures', [])]" | tee -a "$GITHUB_STEP_SUMMARY"
fi

# Always: the journeys attach screenshots, and a passing run's are how a
# layout regression no assertion covers gets noticed.
- name: Upload result bundle
if: always()
uses: actions/upload-artifact@v4
with:
name: ios-ui-xcresult
path: ui.xcresult
retention-days: 14
91 changes: 91 additions & 0 deletions .github/workflows/ios.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
name: iOS

# Gates PRs on the iOS unit target (`flyfun-formsTests`), which until now ran
# only when someone remembered to. The XCUI journeys are deliberately not run
# here: they drive a simulator, they are the flaky half of the suite, and a
# launch hiccup failing a PR teaches everyone to ignore red. They run nightly in
# ios-ui-nightly.yml instead. Same split as flyfun-weather's ios.yml.
#
# The repo is public, so standard GitHub-hosted macOS runners are free.

on:
push:
branches: [main]
paths: &paths
- 'app/flyfun-forms/**'
- '.github/workflows/ios.yml'
pull_request:
paths: *paths

permissions:
contents: read

concurrency:
group: ios-${{ github.ref }}
cancel-in-progress: true

jobs:
unit:
name: xcodebuild test
runs-on: macos-26
timeout-minutes: 30

env:
# Pinned so a runner-image bump cannot change the compiler under us.
DEVELOPER_DIR: /Applications/Xcode_26.6.app/Contents/Developer
# OS=latest rather than a pin: the installed runtimes move with the image,
# and an unavailable pin is a hard failure.
DESTINATION: 'platform=iOS Simulator,name=iPhone 17,OS=latest'

steps:
- uses: actions/checkout@v4

# Every SPM dependency is a public repo, so resolution needs no token.
- name: Cache SPM checkouts
uses: actions/cache@v4
with:
path: .spm
key: spm-${{ runner.os }}-${{ hashFiles('app/flyfun-forms/flyfun-forms.xcodeproj/project.xcworkspace/xcshareddata/swiftpm/Package.resolved') }}
restore-keys: spm-${{ runner.os }}-

- name: Resolve packages
run: |
xcodebuild -resolvePackageDependencies \
-project app/flyfun-forms/flyfun-forms.xcodeproj \
-scheme flyfun-forms \
-clonedSourcePackagesDirPath .spm

# -only-testing filters execution, not the build, so the UI target still
# compiles here: a journey that stops building fails the PR.
- name: Unit tests
run: |
set -o pipefail
xcodebuild test \
-project app/flyfun-forms/flyfun-forms.xcodeproj \
-scheme flyfun-forms \
-destination "$DESTINATION" \
-only-testing:flyfun-formsTests \
-clonedSourcePackagesDirPath .spm \
-resultBundlePath unit.xcresult \
-skipMacroValidation \
-quiet

# A filter that matches nothing is a silent pass: "** TEST SUCCEEDED **",
# exit 0, zero tests run. Read the count back from the result bundle.
- name: Guard against a vacuous pass
run: |
total=$(xcrun xcresulttool get test-results summary --path unit.xcresult \
| python3 -c 'import json,sys; print(json.load(sys.stdin)["totalTestCount"])')
echo "Ran $total unit tests" >> "$GITHUB_STEP_SUMMARY"
if [ "$total" -eq 0 ]; then
echo "::error::xcodebuild reported success but ran 0 tests — the -only-testing filter matched nothing."
exit 1
fi

- name: Upload result bundle
if: failure()
uses: actions/upload-artifact@v4
with:
name: ios-unit-xcresult
path: unit.xcresult
retention-days: 7
Loading
Loading