@@ -426,10 +426,15 @@ jobs:
426426 # One test execution (the `swift test --enable-code-coverage` above)
427427 # feeds both Sonar and Codecov — deliberately not a second `swift
428428 # test` invocation just to collect coverage. SwiftPM writes raw
429- # profile data to `.build/debug/codecov/default.profdata`, keyed to
430- # the test binary at `.build/debug/<Package>PackageTests.xctest/
431- # Contents/MacOS/<Package>PackageTests`; `llvm-cov export` turns that
432- # pair into a single portable lcov.info that both tools consume.
429+ # profile data to `<bin path>/codecov/default.profdata`, keyed to the
430+ # test binary at `<bin path>/<Package>PackageTests.xctest/Contents/
431+ # MacOS/<Package>PackageTests`; `llvm-cov export` turns that pair into
432+ # a single portable lcov.info that both tools consume. The bin path
433+ # itself is resolved via `swift build --show-bin-path` rather than
434+ # hardcoded as `.build/debug` — it actually varies by toolchain/SDK
435+ # (verified: a local Xcode 26.6/Swift 6.3.3 run and this job's own
436+ # macos-15/Xcode 16.4 runner resolved to different paths, and a
437+ # hardcoded guess silently no-op'd here on the very first live run).
433438 # `always()`: coverage is informational, so still attempt it (and let
434439 # the step no-op if the earlier build never got far enough to produce
435440 # profile data) even when the Test step above reported a failure.
@@ -438,10 +443,16 @@ jobs:
438443 shell : bash
439444 run : |
440445 set -o pipefail
441- profdata=".build/debug/codecov/default.profdata"
442- test_bin=".build/debug/MutantKitPackageTests.xctest/Contents/MacOS/MutantKitPackageTests"
443- if [ ! -f "$profdata" ] || [ ! -f "$test_bin" ]; then
444- echo "No coverage profile data found at $profdata / $test_bin — skipping lcov export."
446+ bin_path="$(swift build --show-bin-path)"
447+ profdata="$bin_path/codecov/default.profdata"
448+ test_bundle="$(find "$bin_path" -maxdepth 1 -name '*.xctest' -type d | head -n1)"
449+ if [ -z "$test_bundle" ] || [ ! -f "$profdata" ]; then
450+ echo "No coverage profile data found under $bin_path (profdata=$profdata, test bundle=${test_bundle:-<none>}) — skipping lcov export."
451+ exit 0
452+ fi
453+ test_bin="$test_bundle/Contents/MacOS/$(basename "$test_bundle" .xctest)"
454+ if [ ! -f "$test_bin" ]; then
455+ echo "Found test bundle $test_bundle but no binary at $test_bin — skipping lcov export."
445456 exit 0
446457 fi
447458 xcrun llvm-cov export "$test_bin" -instr-profile "$profdata" -format=lcov > lcov.info
@@ -461,10 +472,18 @@ jobs:
461472 # continue-on-error'd failure every time. Do not switch this workflow
462473 # to `pull_request_target` to give forks a token instead — that would
463474 # hand fork-controlled code execution access to a real secret.
475+ #
476+ # `always()` is required here, not implied: unless a step's own `if`
477+ # uses one of always()/failure()/cancelled(), GitHub Actions silently
478+ # ANDs it with success() — so without this, the earlier Test step
479+ # failing (which coverage/Sonar/Codecov are meant to survive) would
480+ # skip this step even on a same-repo push/PR. Verified live: the
481+ # first real run on this branch had exactly that happen.
464482 - name : SonarCloud Scan
465483 if : >
466- github.event_name != 'pull_request' ||
467- github.event.pull_request.head.repo.full_name == github.repository
484+ always() &&
485+ (github.event_name != 'pull_request' ||
486+ github.event.pull_request.head.repo.full_name == github.repository)
468487 continue-on-error : true
469488 uses : SonarSource/sonarqube-scan-action@22918119ff8e1ca75a623e15c8296b6ea4fbe28f # v8.2.1
470489 env :
0 commit comments