Skip to content

ci: scheduled Xcode/Swift toolchain drift detector (#392) - #442

Merged
tylervick merged 3 commits into
mainfrom
tylervick/toolchain-drift-392
Aug 12, 2026
Merged

tylervick merged 3 commits into
mainfrom
tylervick/toolchain-drift-392

Conversation

@tylervick

@tylervick tylervick commented Aug 12, 2026

Copy link
Copy Markdown
Member

Fixes #392.

What this adds

A new scheduled workflow, .github/workflows/toolchain-drift.yml (Mon/Thu 07:23 UTC + workflow_dispatch), that exercises the newest stable Xcode on the runners end to end and fails loudly. It collects five independent signals per run, each continue-on-error so one failure cannot mask another:

  1. Legacy xcresulttool markerxcrun xcresulttool version currently prints (legacy commands format version: 3.56); that parenthetical is Apple's marker for the legacy command surface the pre-migration backend depends on. When it vanishes on a new Xcode, this step fails — the early-warning alarm that Apple pulled --legacy while the Migrate off xcresulttool --legacy before Apple removes it #391 migration is in flight.
  2. swift build on the new toolchain.
  3. Fixture generation./prepareTestResults.sh from scratch, plus the same Info.plist completeness check test.yml uses. Runs even if the package build failed (it only needs xcodebuild + a simulator).
  4. swift test against the freshly generated fixtures.
  5. xchtmlreport clean exit against a bundle produced by that exact toolchain — since 3.0 the tool exits non-zero on report degradation, so the exit code is the whole assertion (per the issue).

Loud failure

A final report step turns step outcomes into faults and files a GitHub issue labelled drift (creating the label if needed) containing the toolchain version, the fault list, and a per-step outcome table. De-duplication per the issue spec: while one drift issue is open, later failing runs append comments instead of filing duplicates, and a later green run also comments so a resolved break is distinguishable from a persistent one. The job itself also exits non-zero, so the run is red in Actions regardless. Manual cancellations file nothing (!cancelled(), not always()).

Cache interplay (#436)

Fixtures are deliberately regenerated, never cache-restored — the job's purpose is to prove the current toolchain can still produce them. But since a successful run has just paid for fresh fixtures on the newest toolchain, it saves them under the same key test.yml computes: after a toolchain bump on the runners, PR jobs start warm instead of each paying for regeneration.

#436 fast-follow: cache key ↔ script alignment

test.yml derived the cache-key runtime from simctl list runtimes (newest installed), while prepareTestResults.sh selects the newest runtime that offers an iPhone — the two could disagree whenever the newest runtime ships without iPhone devices. The selection logic now lives in scripts/select_simulator.py (verbatim extraction of the script's python, extended to emit the runtime build version), and both the script and the cache-key steps consume it, so the key can never disagree with the runtime actually booted. The new file is included in the cache key's hashFiles(...).

Notes

  • Scheduled workflows only run from the default branch — the schedule takes effect once this merges to main; until then (and for testing) workflow_dispatch works from main only. Noted in the workflow header.
  • Schedule is modest (twice weekly, off the top of the hour) since each run boots a simulator and regenerates all fixtures on a macOS runner. The issue asked for weekly; twice weekly is chosen because the Migrate off xcresulttool --legacy before Apple removes it #391 migration window makes the legacy-removal signal time-sensitive. Easy to dial down after the migration lands.
  • Per-step timeout-minutes (rather than a job timeout) so a hung simulator still produces a failed step and a filed issue instead of a silently cancelled run.
  • No product code touched. shellcheck and actionlint pass on everything changed; the report step's four outcome paths (create/comment × fault/green) were smoke-tested with a stubbed gh.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Testing

    • Improved iOS simulator selection for fixture generation and test result preparation.
    • Test runs now report the selected simulator’s runtime build.
    • Fixture caching remains aligned with the simulator used for generation.
  • Chores

    • Added scheduled and manual checks against the latest stable Xcode.
    • Toolchain compatibility issues are summarized and tracked automatically.
    • Successful test fixtures are saved for reuse.

@coderabbitai

coderabbitai Bot commented Aug 12, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: c41f3b75-d400-490b-9ae6-13a356572c82

📥 Commits

Reviewing files that changed from the base of the PR and between 406f103 and b5a8b02.

📒 Files selected for processing (3)
  • .github/actions/fixture-cache-key/action.yml
  • .github/workflows/test.yml
  • .github/workflows/toolchain-drift.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/toolchain-drift.yml

📝 Walkthrough

Walkthrough

The PR centralizes iOS simulator selection, aligns fixture cache keys with fixture generation, and adds a scheduled workflow that validates the latest stable Xcode and reports toolchain drift through GitHub issues.

Changes

Simulator Selection and Toolchain Drift

Layer / File(s) Summary
Centralized simulator selection
scripts/select_simulator.py
The script queries available simulators, selects the newest suitable iPhone runtime, resolves its runtime build, and emits tab-separated metadata.
Fixture generation and cache alignment
prepareTestResults.sh, .github/actions/fixture-cache-key/action.yml, .github/workflows/test.yml
Fixture generation and cache-key calculation use shared simulator metadata. The action hashes Xcode data, simulator data, and fixture-shaping sources.
Scheduled toolchain drift workflow
.github/workflows/toolchain-drift.yml
The workflow checks the latest stable Xcode, validates build and fixture generation, runs tests and xchtmlreport, saves successful fixtures, and reports failures through GitHub issues.

Estimated code review effort: 4 (Complex) | ~60 minutes

Mergeability Score: 🔵 Low · up to b5a8b

The workflow adds scheduled toolchain drift detection, but a failure in the version probe can produce an incomplete or misleading report about the legacy command surface, making breakage harder to diagnose. This is a bounded CI observability risk that is mergeable with explicit owner follow-up.

Sequence Diagram(s)

sequenceDiagram
  participant GitHubActions
  participant prepareTestResults
  participant select_simulator
  participant simctl
  participant xchtmlreport
  participant GitHubIssues
  GitHubActions->>prepareTestResults: generate fresh fixtures
  prepareTestResults->>select_simulator: request simulator metadata
  select_simulator->>simctl: list devices and runtimes
  simctl-->>select_simulator: return simulator JSON
  select_simulator-->>prepareTestResults: return device, OS, UDID, and runtime build
  GitHubActions->>xchtmlreport: validate generated result bundle
  GitHubActions->>GitHubIssues: report drift outcomes
Loading

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly identifies the scheduled Xcode and Swift toolchain drift detector, which is the primary change.
Linked Issues check ✅ Passed The workflow meets issue #392 requirements for scheduling, latest stable Xcode validation, fresh fixtures, failure reporting, and drift-issue deduplication.
Out of Scope Changes check ✅ Passed The cache, simulator-selection, and workflow changes directly support the toolchain drift detector and its fixture-generation requirements.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch tylervick/toolchain-drift-392

Comment @coderabbitai help to get the list of available commands.

A scheduled workflow (Mon/Thu + workflow_dispatch) that exercises the
newest Xcode on the runners end to end: checks that xcresulttool still
advertises legacy command support (the early-warning signal for the #391
migration), builds the package, regenerates fixtures from scratch via
prepareTestResults.sh, runs the test suite, and asserts xchtmlreport
exits clean against a bundle produced by that exact toolchain. Any fault
files or updates a single open `drift`-labelled issue, so a break cannot
rot silently in the Actions tab.

Fixtures are deliberately regenerated (never cache-restored) because the
job's purpose is to exercise the current toolchain — but a successful
run saves them under the #436 cache key, pre-warming PR jobs after a
toolchain bump.

Also folds in the #436 fast-follow: the fixture cache key derived its
runtime from `simctl list runtimes` (newest installed) while
prepareTestResults.sh selects the newest runtime that offers an iPhone.
The selection now lives in scripts/select_simulator.py and both consume
it, so the key can never disagree with the runtime the script boots.

Closes #392

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@tylervick
tylervick force-pushed the tylervick/toolchain-drift-392 branch from 5515660 to f5337f1 Compare August 12, 2026 22:34
id: build
continue-on-error: true
timeout-minutes: 30
run: swift build -v

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In @.github/workflows/toolchain-drift.yml:
- Around line 45-53: Remove the xcrun xcresulttool version probe from the
required Record toolchain step so Xcode outputs are always written. Move the
probe into the legacy step and export its successful version there, ensuring
Check legacy xcresulttool support and Report drift correctly reflect probe
failures.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 713ebd63-3203-4865-86f9-c65819d5fa9b

📥 Commits

Reviewing files that changed from the base of the PR and between e2a3e9c and f5337f1.

📒 Files selected for processing (4)
  • .github/workflows/test.yml
  • .github/workflows/toolchain-drift.yml
  • prepareTestResults.sh
  • scripts/select_simulator.py

Comment thread .github/workflows/toolchain-drift.yml Outdated
tylervick and others added 2 commits August 12, 2026 16:09
Review follow-up (CodeRabbit): the toolchain recording step invoked
`xcrun xcresulttool version` bare, so the tool disappearing entirely
would fail recording, skip the legacy probe, and file a generic setup
failure instead of the one alarm the workflow exists to raise. Recording
now tolerates the failure and the probe swallows the command error so
the missing marker fires the explicit ::error either way.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Review of #442 found the drift job's pre-warm dead on arrival: its save
key rendered hashFiles('XCTestHTMLReportSampleApp/**') at save time,
after prepareTestResults.sh had left .derivedData inside the hashed
tree (actions/glob matches dot-dirs), so the saved key could never
equal test.yml's clean-tree restore key — ~95 MB of unreachable cache
churned per run.

Both workflows now derive the key from one composite action
(.github/actions/fixture-cache-key) that runs before generation. The
hash inputs and key format are unchanged, so existing cache entries
still hit; sharing the action also removes the duplicated key logic
the two workflows could silently de-sync on.

Also from the review:
- run the legacy probe under !cancelled() so a hard-failed record step
  (xcodebuild/swift dying — the alarm's most important case) cannot
  skip it, and have the report call out a probe that somehow still
  did not run
- make the record step's xcresulttool tolerance survive pipefail
  (|| true inside the pipeline) instead of relying on the default
  shell leaving pipefail off

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add a scheduled Xcode/Swift drift detector

2 participants