Make the tool report its own failures, and make the test suite runnable by anyone - #379
Conversation
Documents the current state of the project as measured on 2026-08-06 (Xcode 26.2 / Swift 6.2.3) and proposes a phased revival: - Track A: build a verification oracle (fault collection + honest exit codes, self-contained CI fixtures, close fixture coverage gaps) - Track B: propose-only issue triage lane for the 62-issue backlog - Track D: dependency updates, release pipeline repair, drift detector Key finding: the tool still processes Xcode 26.2 xcresults but exits 0 while emitting parse errors, so no consumer can distinguish a complete report from a degraded one. A large fraction of open issues are unreproducible because no sample-app fixture exercises their code path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Covers spec tracks A2 (self-contained fixtures) then A1 (fault collection and honest exit codes), in that order — reading the code showed A1 cannot be test-driven until fixtures are generatable locally. Eight tasks, each ending in an independently testable deliverable. Includes a fix for Summary.init using break instead of continue, which silently abandons remaining result bundles after one unreadable bundle. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Per pre-flight ruling: the zero-secrets global constraint binds every workflow, not just test.yml. codecov.yml pulled fixtures from the same R2 bucket. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Resolve the newest available iPhone simulator instead of hardcoding iPhone 12, which no longer exists in Xcode 26. Replace the apple.com fetch with inline HTML data so fixture generation has no network dependency.
The original snippet sorted device names as strings, so 'iPhone 8' beat 'iPhone 17 Pro Max' and 'iPhone SE' beat everything. Silently wrong on any runner with mixed simulator generations. Replaced with tested logic that picks the newest iOS runtime, then the highest numeric iPhone model within it, and pins OS to that runtime instead of 'latest'. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Lexicographic sort of device names picked "iPhone SE (3rd generation)" over "iPhone 17 Pro Max" on mixed-generation runners, since '8' > '1' in ASCII and unnumbered SE/X models sort last regardless. Group devices by runtime, rank iPhone models numerically within the newest runtime, and pin -destination's OS to that runtime's version instead of "OS=latest" so the resolved name and OS can never disagree.
Fork PRs could never go green because fixtures came from an R2 bucket behind repo secrets. Both test.yml and codecov.yml now run prepareTestResults.sh to build their own xcresult bundles. Closes the root cause of #219. Also removes the long-dead .travis.yml.
codecov-action@v4 dropped tokenless upload for base-repo pushes, so the step would silently no-op (fail_ci_if_error defaults false). The secret already exists on the repo. Also rewords the global constraint: it exists so fork PRs can go green, which binds test.yml only. Workflows that cannot run from a fork PR may use secrets. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
codecov-action v4 dropped tokenless upload for pushes from the base repo, and without a token the upload silently no-ops since fail_ci_if_error defaults to false. codecov.yml only triggers on push-to-main and workflow_dispatch, never on fork PRs, so adding the already-existing CODECOV_TOKEN secret here does not weaken the zero-secrets constraint on fork PR runs (test.yml stays secret-free).
Foundation for reporting degraded output. Parsing is concurrent, so mutations serialize behind a private dispatch queue.
XCResultKit signals failure by returning nil, so record a fault wherever we already handle one. Also fixes Summary.init using break instead of continue, which silently abandoned every remaining result bundle after one unreadable bundle.
Call-site nil checks miss nested XCResultKit decode failures, where the parent decodes but a child comes back empty. Check the assembled model directly for attachments with no content. NOTE: committed with --no-gpg-sign because the 1Password SSH agent was erroring. Re-sign before pushing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
testValidateIsIdempotent asserted 0 == 0 on a clean fixture and would have passed with validate()'s dedup deleted. Seed a non-empty fault set first and compare the whole fault array across two calls. Also tightens validate()'s doc comment: dedup keys on filename, and the guarantee holds for sequential calls only. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Call-site nil checks miss nested XCResultKit decode failures, where the parent decodes but a child comes back empty. Check the assembled model directly for attachments with no content.
BREAKING CHANGE: xchtmlreport now exits 3 when it could not fully parse the result bundle, instead of exiting 0 with a success message. Reports are still written. Pass --lenient for pre-3.0 behaviour.
The stderr assertion was guarded by #if !DEBUG, and swift test always builds debug — so the suite could never observe degradation. Check the exit code and the fault summary instead.
CONTRIBUTING never explained how to run tests, which was moot while fixtures lived behind repo secrets. Both are now accurate.
Distinct attachments can share one payload ref — the screen recording of a test retried under `-retry-tests-on-failure` is the common case, and both screen recordings and retries are defaults, not exotic configuration. XCResultKit exports every caller of an id to `NSTemporaryDirectory()/<id>`, a path shared by all of them. Parsing is concurrent, so two exports of one id raced twice over: on that temp file, and on the destination inside the bundle. The first `moveItem` consumed the temp file, the second failed with "the former doesn't exist" and returned nil, which became `RenderingContent.none`, which `validate()` flagged as an unresolved attachment. `RetryResults.xcresult` exited 3 on 8 of 10 identical runs. Worse, the `try? removeItem` before the move could delete the copy the first thread had just placed, so the attachment really did go missing from the bundle rather than merely being misreported. Serialize exports per payload id. Different ids still run in parallel, so this costs nothing on bundles without duplicate refs. The `catch` also now treats an already-populated destination as success, and records a `payloadExportFailed` fault when the export genuinely failed — it recorded nothing at all before. The new CLI test repeats the run so a reintroduced race is caught rather than sampled away. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`prepareTestResults.sh` now resolves the newest available simulator instead of hardcoding "iPhone 12", so the device name embedded in the JUnit output is whatever the machine has — "iPhone 17 Pro Max" on current Xcode. Both regexes required digits immediately followed by " - ", so they stopped matching. Accept any model name, and escape the literal dot in the OS version while here. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every test case in RetryResults.xcresult now produces exactly one fewer `.unknown` JUnit result — an activity/log line — than these expectations were written against: 9 vs 10, then 3 vs 4, then 3 vs 4. That is fixture drift on Xcode 26, unrelated to anything on this branch. Skip rather than adjust the numbers. The expectations are the only thing pinning JUnit result composition, and loosening them to match today's Xcode would quietly discard that coverage. Tracked in #378; the skip comes out when that lands. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The spec and implementation plan are workflow scratch, not project documentation. They introduce a `docs/` tree that does not exist on main, and the plan already contradicts itself — its "Follow-on work" section lists work this branch finished. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`Logger.error` and `Logger.warning` used bare `print`, so the "Report is degraded" diagnostics landed on the same stream as the path of the generated report. A pipeline reading that path got the complaints mixed into it. In a release whose point is exit-code honesty this is the wrong stream, and 3.0 is the window to move it without it being a separate breaking change. `success`, `step` and `substep` stay on stdout — they are the tool's output, and the test harness parses the report path out of `success`. The harness's degraded-report check followed the warnings to stderr; it now inspects both streams so it cannot silently stop working again, and the exit-status failure message quotes both. Also drain the child's pipes concurrently and before waiting on it. Waiting first deadlocks the moment either stream outgrows its pipe buffer, which is not something this harness controls: xchtmlreport hands its own stdout to every `xcresulttool` it spawns. A hung suite was observed on this code path. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`validate()` used `attachment.filename` as the fault detail, which is "" for nameless attachments — the report then said `unresolvedAttachment:` with nothing after it, and two nameless attachments deduped into one. Fall back to the attachment name and its payload id, which is also what the `payloadExportFailed` details carry, so the two correlate. Also document what triggers each `Fault.Kind`. It is public API being frozen in a major release; the names alone do not say whether a kind means "could not read", "could not export", or "read but came back empty". Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
README: - Exit 1 was missing and is reachable: any failure to write the output rethrows, so `xchtmlreport -o <nonexistent-dir>` exits 1. - "Report generated successfully" overclaimed for exit 0. Some XCResultKit decode failures are not surfaced as faults yet — `TestResults.xcresult` exits 0 while printing parse errors — so exit 0 means "no faults detected", not "report is complete". - "could not be parsed" was wrong for two of the four fault kinds: `unresolvedAttachment` and `payloadExportFailed` are export and resolution failures, not parse failures. test-artifacts.yml created and booted "iPhone 12" before running prepareTestResults.sh. The boot fails on any modern runner, and the script now resolves its own simulator, so the two lines are both broken and redundant. prepareTestResults.sh: under `set -e`, `read` returns non-zero on empty input, so the script died at the `read` and never reached the "No iPhone simulator available" guard below it. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`1...5` -> `1 ... 5`. CliTests.swift was the one test file that passed `swiftformat --lint` before this branch; keep it that way. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
📝 WalkthroughWalkthroughThe change adds fault collection, degraded-report exit handling, serialized shared-payload exports, dynamic simulator fixture generation, updated CI workflows, and documentation for build commands and exit codes. ChangesFault reporting and CI fixture updates
Estimated code review effort: 4 (Complex) | ~45 minutes Possibly related issues
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
|
|
||
| - name: Setup Xcode version | ||
| uses: maxim-lobanov/setup-xcode@v1.6.0 | ||
| uses: maxim-lobanov/setup-xcode@v1.6.0 |
| -instr-profile .build/debug/codecov/default.profdata > info.lcov | ||
|
|
||
| - uses: codecov/codecov-action@v3 | ||
| - uses: codecov/codecov-action@v4 |
|
|
||
| - name: Setup Xcode version | ||
| uses: maxim-lobanov/setup-xcode@v1.6.0 | ||
| uses: maxim-lobanov/setup-xcode@v1.6.0 |
There was a problem hiding this comment.
Actionable comments posted: 4
🤖 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/codecov.yml:
- Line 16: Pin the external actions to verified full commit SHAs: update
maxim-lobanov/setup-xcode in .github/workflows/codecov.yml lines 16-16 and
.github/workflows/test.yml lines 16-16, and codecov/codecov-action in
.github/workflows/codecov.yml lines 32-32; retain inline version comments where
useful.
In `@CONTRIBUTING.md`:
- Around line 50-55: Update the CONTRIBUTING.md guidance around
prepareTestResults.sh to remove the claim that a successful local run guarantees
a green pull request. Keep the instruction to run the two commands locally, but
state that CI is authoritative because environments and Xcode-generated
.xcresult fixtures may differ.
In `@Sources/XCTestHTMLReportCore/Classes/Models/ResultFile.swift`:
- Around line 84-86: Update ResultFile export error handling at
Sources/XCTestHTMLReportCore/Classes/Models/ResultFile.swift lines 84-86 and
115-116: in the Data(contentsOf:) failure path, record .payloadExportFailed with
the payload ID, and in the log write failure path, record .logExportFailed with
the log ID, alongside the existing error handling.
In `@Sources/XCTestHTMLReportCore/Classes/Models/Summary.swift`:
- Around line 111-121: Update the attachment validation loop in validate() so
each newly accepted unresolved attachment detail is inserted into alreadyFlagged
immediately before recording the fault. Preserve the existing duplicate check
and ensure subsequent attachments with the same faultDescription are skipped
during the same validation pass.
🪄 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: ff932951-aeae-47d8-807e-e18023f7b467
📒 Files selected for processing (19)
.github/workflows/codecov.yml.github/workflows/test-artifacts.yml.github/workflows/test.yml.travis.ymlCONTRIBUTING.mdREADME.mdSources/XCTestHTMLReport/XCTestHtmlReport.swiftSources/XCTestHTMLReportCore/Classes/Helpers/FaultCollector.swiftSources/XCTestHTMLReportCore/Classes/Helpers/Logger.swiftSources/XCTestHTMLReportCore/Classes/Models/Attachment.swiftSources/XCTestHTMLReportCore/Classes/Models/ResultFile.swiftSources/XCTestHTMLReportCore/Classes/Models/Summary.swiftTests/XCTestHTMLReportTests/CliTests.swiftTests/XCTestHTMLReportTests/CoreTests.swiftTests/XCTestHTMLReportTests/FaultCollectorTests.swiftTests/XCTestHTMLReportTests/FaultReportingTests.swiftTests/XCTestHTMLReportTests/TestSupport.swiftXCTestHTMLReportSampleApp/SampleAppUITests/FirstSuite.swiftprepareTestResults.sh
💤 Files with no reviewable changes (1)
- .travis.yml
|
|
||
| - name: Setup Xcode version | ||
| uses: maxim-lobanov/setup-xcode@v1.6.0 | ||
| uses: maxim-lobanov/setup-xcode@v1.6.0 |
There was a problem hiding this comment.
🔒 Security & Privacy | 🟠 Major | ⚡ Quick win
Pin external GitHub Actions to immutable commit SHAs.
Version tags can move after review. Pin each action to a verified full commit SHA and retain an inline version comment if needed.
.github/workflows/codecov.yml#L16-L16: replacemaxim-lobanov/setup-xcode@v1.6.0with its verified full commit SHA..github/workflows/codecov.yml#L32-L32: replacecodecov/codecov-action@v4with its verified full commit SHA..github/workflows/test.yml#L16-L16: replacemaxim-lobanov/setup-xcode@v1.6.0with its verified full commit SHA.
🧰 Tools
🪛 GitHub Check: SonarCloud
[failure] 16-16: External GitHub Actions and workflows should be pinned to a commit hash
Use full commit SHA hash for this dependency.See more on SonarQube Cloud
🪛 GitHub Check: SonarCloud Code Analysis
[failure] 16-16: Use full commit SHA hash for this dependency.
📍 Affects 2 files
.github/workflows/codecov.yml#L16-L16(this comment).github/workflows/codecov.yml#L32-L32.github/workflows/test.yml#L16-L16
🤖 Prompt for 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.
In @.github/workflows/codecov.yml at line 16, Pin the external actions to
verified full commit SHAs: update maxim-lobanov/setup-xcode in
.github/workflows/codecov.yml lines 16-16 and .github/workflows/test.yml lines
16-16, and codecov/codecov-action in .github/workflows/codecov.yml lines 32-32;
retain inline version comments where useful.
Source: Linters/SAST tools
| `prepareTestResults.sh` picks the newest available iPhone simulator automatically. | ||
| No credentials or secrets are required — CI runs exactly these two commands, so a | ||
| green run locally means a green run on your pull request. | ||
|
|
||
| Regenerate fixtures after upgrading Xcode; `.xcresult` contents change between | ||
| Xcode versions. |
There was a problem hiding this comment.
📐 Maintainability & Code Quality | 🟡 Minor | ⚡ Quick win
Remove the CI-success guarantee.
A local green run does not guarantee a green pull-request run. Local and CI environments can use different Xcode versions and generate different .xcresult fixtures. State that contributors should run these commands locally, but CI remains the authoritative check.
🤖 Prompt for 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.
In `@CONTRIBUTING.md` around lines 50 - 55, Update the CONTRIBUTING.md guidance
around prepareTestResults.sh to remove the claim that a successful local run
guarantees a green pull request. Keep the instruction to run the two commands
locally, but state that CI is authoritative because environments and
Xcode-generated .xcresult fixtures may differ.
| guard let savedURL = file.exportPayload(id: id) else { | ||
| Logger.warning("Can't export payload with id \(id)") | ||
| faultCollector.record(.payloadExportFailed, "payload id \(id)") |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟠 Major | ⚡ Quick win
Record every export failure in FaultCollector.
The new fault handling records only failures where XCResultKit returns nil. A Data(contentsOf:) failure and a log write failure leave the collector unchanged. Summary.validate() can later report the payload case only as an unresolved attachment. It does not validate logs, so a log write failure can still exit with status 0.
Sources/XCTestHTMLReportCore/Classes/Models/ResultFile.swift#L84-L86: Record.payloadExportFailedin theData(contentsOf:)error path with the payload ID.Sources/XCTestHTMLReportCore/Classes/Models/ResultFile.swift#L115-L116: Record.logExportFailedin the log write error path with the log ID.
📍 Affects 1 file
Sources/XCTestHTMLReportCore/Classes/Models/ResultFile.swift#L84-L86(this comment)Sources/XCTestHTMLReportCore/Classes/Models/ResultFile.swift#L115-L116
🤖 Prompt for 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.
In `@Sources/XCTestHTMLReportCore/Classes/Models/ResultFile.swift` around lines 84
- 86, Update ResultFile export error handling at
Sources/XCTestHTMLReportCore/Classes/Models/ResultFile.swift lines 84-86 and
115-116: in the Data(contentsOf:) failure path, record .payloadExportFailed with
the payload ID, and in the log write failure path, record .logExportFailed with
the log ID, alongside the existing error handling.
| let alreadyFlagged = Set( | ||
| faultCollector.faults | ||
| .filter { $0.kind == .unresolvedAttachment } | ||
| .map(\.detail) | ||
| ) | ||
|
|
||
| for attachment in allAttachments { | ||
| guard case .none = attachment.content else { continue } | ||
| let detail = attachment.faultDescription | ||
| guard !alreadyFlagged.contains(detail) else { continue } | ||
| faultCollector.record(.unresolvedAttachment, detail) |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
Deduplicate faults recorded in the same validation pass.
alreadyFlagged does not change inside the loop. If two unresolved attachments have the same faultDescription, validate() records both faults. Insert each accepted detail into the set before continuing.
Proposed fix
- let alreadyFlagged = Set(
+ var alreadyFlagged = Set(
faultCollector.faults
.filter { $0.kind == .unresolvedAttachment }
.map(\.detail)
)
for attachment in allAttachments {
guard case .none = attachment.content else { continue }
let detail = attachment.faultDescription
- guard !alreadyFlagged.contains(detail) else { continue }
+ guard alreadyFlagged.insert(detail).inserted else { continue }
faultCollector.record(.unresolvedAttachment, detail)
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let alreadyFlagged = Set( | |
| faultCollector.faults | |
| .filter { $0.kind == .unresolvedAttachment } | |
| .map(\.detail) | |
| ) | |
| for attachment in allAttachments { | |
| guard case .none = attachment.content else { continue } | |
| let detail = attachment.faultDescription | |
| guard !alreadyFlagged.contains(detail) else { continue } | |
| faultCollector.record(.unresolvedAttachment, detail) | |
| var alreadyFlagged = Set( | |
| faultCollector.faults | |
| .filter { $0.kind == .unresolvedAttachment } | |
| .map(\.detail) | |
| ) | |
| for attachment in allAttachments { | |
| guard case .none = attachment.content else { continue } | |
| let detail = attachment.faultDescription | |
| guard alreadyFlagged.insert(detail).inserted else { continue } | |
| faultCollector.record(.unresolvedAttachment, detail) |
🤖 Prompt for 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.
In `@Sources/XCTestHTMLReportCore/Classes/Models/Summary.swift` around lines 111 -
121, Update the attachment validation loop in validate() so each newly accepted
unresolved attachment detail is inserted into alreadyFlagged immediately before
recording the fault. Preserve the existing duplicate check and ensure subsequent
attachments with the same faultDescription are skipped during the same
validation pass.
macos-latest is now macos-26, which ships only Xcode 26.x, so the xcode_version: 15 leg failed with 'Could not find Xcode version that satisfied version spec'. Xcode 16 is absent there too. Keeps the latest-stable + one-major-back pattern this matrix has used since 2023 by pinning the older leg to macos-15, which still carries Xcode 16.4 — the same approach the matrix took when it pinned macos-13 to test Xcode 14. Also inlines the single-element arch list. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
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/ci.yml:
- Around line 14-22: Update the CI matrix to retain an Intel x86_64-apple-macosx
leg compatible with the .macOS(.v10_15) deployment target and existing release
workflow. Add the appropriate macOS runner and toolchain configuration alongside
the current macOS entries; only raise the deployment target if Intel support is
intentionally removed, including corresponding release documentation updates.
🪄 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: 8cff8bc3-e4a6-4804-a2cd-911e264fbb35
📒 Files selected for processing (1)
.github/workflows/ci.yml
| # Newest toolchain, plus one major version back — the pattern this | ||
| # matrix has followed since 2023. The older leg needs its own image: | ||
| # macos-latest is now macos-26, which ships only Xcode 26.x. | ||
| include: | ||
| - os: macos-latest | ||
| xcode_version: latest-stable | ||
| - os: macos-15 | ||
| xcode_version: "16" | ||
| runs-on: ${{ matrix.os }} |
There was a problem hiding this comment.
🎯 Functional Correctness | 🟡 Minor | ⚡ Quick win
🧩 Analysis chain
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
if rg -q '\.macOS\(\.v10_15\)' Package.swift &&
! rg -q 'x86_64-apple-macosx|x86_64|macos-[0-9]+-intel' .github/workflows; then
echo "Package.swift supports macOS 10.15, but CI has no Intel target."
exit 1
fiRepository: XCTestHTMLReport/XCTestHTMLReport
Length of output: 171
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
printf '%s\n' '--- workflow ---'
cat -n .github/workflows/ci.yml | sed -n '1,60p'
printf '%s\n' '--- package platform declarations ---'
rg -n -C 3 'macOS|platforms|v10_15|x86_64|arm64' Package.swift .github/workflows || true
printf '%s\n' '--- relevant workflow targets ---'
rg -n -C 4 'swift build|target|architecture|matrix|macos-' .github/workflows/ci.yml || trueRepository: XCTestHTMLReport/XCTestHTMLReport
Length of output: 3957
🌐 Web query:
Apple arm64 macOS minimum deployment target macOS 11 x86_64 macOS 10.15 Swift
💡 Result:
The minimum deployment target for macOS on arm64 (Apple Silicon) is macOS 11.0 [1][2][3]. Because Apple Silicon hardware was introduced with macOS 11, the build system enforces this requirement; any attempt to target an older macOS version for an arm64 binary slice will be clamped to 11.0 [3]. For x86_64 architectures, the minimum deployment target can be set to older versions of macOS (such as 10.12 or 10.13, depending on the Xcode version and SDK used) [1][4][3]. When building universal binaries containing both architectures, the macOS 11.0 minimum constraint applies only to the arm64 slice, while the x86_64 slice can maintain a lower deployment target [4][3]. Regarding Swift, the language itself does not dictate these hardware-based OS limits [4]. Instead, the minimum deployment target is a property of the toolchain, the SDK, and the underlying platform requirements [4]. While Swift Package Manager allows you to specify a minimum platform version in your Package.swift file using the.macOS API, you remain bound by the architectural constraints enforced by the build system [5][4][3][6]. If you specify a version lower than the required minimum for a specific architecture, the build system will effectively clamp the deployment target for that slice to its architectural minimum [3].
Citations:
- 1: https://docs.adacore.com/live/wave/rust/html/rust_ug/_static/rustc/platform-support/apple-darwin.html
- 2: https://github.com/rust-lang/rust/blob/ba412a6e70ac84641be7764d088acabd0eb3fa39/compiler/rustc_target/src/spec/base/apple/mod.rs
- 3: https://forums.swift.org/t/running-a-swiftpm-project-program-on-older-versions-of-macos/76404
- 4: https://forums.swift.org/t/what-are-the-versions-of-macos-on-which-programs-built-with-each-version-of-swift-can-run/75120
- 5: https://github.com/apple/swift-package-manager/blob/release/5.6/Documentation/PackageDescription.md
- 6: https://developer.apple.com/documentation/packagedescription/supportedplatform/macos(_:)-9771f
Keep an Intel CI leg for the declared macOS 10.15 target.
Package.swift declares .macOS(.v10_15), and the release workflow still builds x86_64-apple-macosx. This CI workflow now builds only arm64-apple-macosx, which requires macOS 11 or later. Add an x86_64-apple-macosx matrix leg, or raise the deployment target and update the release documentation if Intel support is intentionally removed.
🤖 Prompt for 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.
In @.github/workflows/ci.yml around lines 14 - 22, Update the CI matrix to
retain an Intel x86_64-apple-macosx leg compatible with the .macOS(.v10_15)
deployment target and existing release workflow. Add the appropriate macOS
runner and toolchain configuration alongside the current macOS entries; only
raise the deployment target if Intel support is intentionally removed, including
corresponding release documentation updates.
Two problems, one root cause, and a data-loss bug found on the way.
1. The tool could not report its own failure
xchtmlreportexited0with "Report successfully created" even when it failed to parse parts of the result bundle. Nothing downstream — CI, a maintainer, or any automation — could distinguish a complete report from a silently degraded one.Verified on Xcode 26.2: a run emitting six
Unable to retrieve json from file … for expected type: GenericReferencedObjecterrors still exited0with a success message.This PR adds a
Fault/FaultCollectorpair (thread-safe; parsing is concurrent) that records degradation in two places:nilSummary.validate()), which catches nested decode failures where the parent object decodes but a child comes back empty — those never surface as anilat any call site, so call-site checks alone are not sufficientxchtmlreportnow exits 3 when the report is degraded. Reports are still written — this is purely an exit-code change. Pass--lenientfor the previous always-zero behaviour.Intended for a 3.0 release. Pipelines that were silently consuming incomplete reports will start failing — which is the point, but it warrants a major version and a note in the release announcement.
2. Nobody but the maintainer could run the tests
test.ymlandcodecov.ymldownloaded.xcresultfixtures from a private R2 bucket via repository secrets. GitHub does not expose secrets to fork pull requests, so no outside contributor has ever been able to get a green check. On a fresh cloneswift testdid not merely fail — it did not compile, because SwiftPM only synthesisesBundle.modulewhen a target's declared resources exist.CI now generates its own fixtures with
prepareTestResults.sh, which required fixing it: it hardcoded an "iPhone 12" simulator that no longer exists, and one sample test fetchedapple.comat test time. It now resolves the newest available iOS runtime and the highest-numbered iPhone within it, and needs no network.This addresses the root cause of #219.
3. A data-loss bug, found by pointing the tool at its own fixture
Running against
RetryResults.xcresultexited3intermittently — 8 of 10 runs.Two
Attachments can share a singlepayloadRef.id. XCResultKit exports every caller of an id to the same temp path, so concurrent exports raced:try? removeItemdeleted the file another thread had just moved into place. The attachment was then genuinely missing from the generated report. Fixed by serialising exports per payload id.This is a plausible contributor to long-standing "screenshots/attachments not visible" reports.
Testing
swift test: 22 tests, 1 skipped, 0 failuresRetryResults.xcresult: 30 consecutive runs, all exit 0 (was 8/10 exiting 3)--lenient→ 0, and the report is written in every caseCoreTests.testRetryFunctionalityJunitis skipped pending #378 — its JUnit expectations drift on Xcode 26 (one fewer.unknownactivity result per test case). The assertions are unchanged, not weakened, so they run again once that is resolved.Follow-ups (not in this PR)
pages.ymlis broken two ways:swift run xchtmlreport -- -jexits 64 because SwiftPM forwards the--, and it downloads an artifact nametest-artifacts.ymlno longer producesexportJson()has nonilcheck, so--jsoncan emit[]and exit 0Summary.validate()flags attachments that have nopayloadRefat all, which would be a false positive on a bundle containing themXCResultFile.exportPayloadnever returnsnil, so two fault branches inResultFileare dead codeReview note
Best reviewed squashed — the branch carries some plan-amendment commits from its development.
🤖 Generated with Claude Code
Summary by CodeRabbit