Skip to content

Probe for the actual Danger.swiftmodule instead of trusting the compiler that built danger-swift - #663

Merged
f-meloni merged 4 commits into
danger:masterfrom
DylanBettermannDD:db/mobex-5198-modulefolder-probe
Aug 24, 2026
Merged

Probe for the actual Danger.swiftmodule instead of trusting the compiler that built danger-swift#663
f-meloni merged 4 commits into
danger:masterfrom
DylanBettermannDD:db/mobex-5198-modulefolder-probe

Conversation

@DylanBettermannDD

Copy link
Copy Markdown
Contributor

Problem

SPMDanger.moduleFolder (used to build the -I flag when compiling a Dangerfile against a DangerDeps SwiftPM product) picks between .build/debug and .build/debug/Modules using a compile-time check on the Swift version that built the danger-swift binary itself:

public var moduleFolder: String {
    #if compiler(<6.0)
        buildFolder
    #else
        buildFolder + "/Modules"
    #endif
}

That's a proxy for a runtime property — the toolchain and build system used to build the target package — and it's wrong whenever those two differ, which is the normal case for a distributed danger-swift binary (Homebrew, Docker, or the prebuilt universal binary from #660): the binary's own compile-time Swift version has nothing to do with which build system/toolchain later builds the Dangerfile's dependencies.

Concretely, this breaks on a toolchain that defaults swift build to the newer swiftbuild build system (observed on Xcode 27 Beta 4 / Swift 6.4; not claiming a specific version threshold here, just that this is a real, shipping build system that behaves differently from native). Under swiftbuild, SwiftPM produces flat *.swiftmodule files with no Modules/ subdirectory at all — the inverse of what native+Swift 6 produces. A danger-swift binary built with Swift ≥6.0 (true of every distributed binary today) then looks in Modules/, finds nothing, and fails:

error: no such module 'Danger'

Measured layouts (building only --product DangerDeps<X>, exactly what buildDependencies runs):

native (Xcode 26.6, Swift 6.3.3) swiftbuild (Xcode 27 Beta 4, Swift 6.4)
.build/debug symlink → arm64-apple-macosx/debug symlink → out/Products/Debug
flat *.swiftmodule in bin path 0 6
<bin>/Modules/ exists, 6 modules absent

The two layouts are disjoint. .build/debug itself stays a valid symlink under both — SwiftPM repoints it on every build — so only the module-search path (-I) is wrong, never -L.

Sources/DangerDependenciesResolver/Script.swift's artifactsPath has the identical #if compiler(<6.0) pattern for the Marathon-based inline-dependency path (import ... package: Dangerfiles with no DangerDeps library).

Fix

Replace the compile-time check with a runtime probe for the actual Danger.swiftmodule artifact at both candidate locations, falling back to today's compiled-in default whenever the probe is ambiguous (both or neither candidate present) so no currently-working configuration changes behavior:

public var moduleFolder: String {
    let flatModule = buildFolder + "/Danger.swiftmodule"
    let nestedModule = buildFolder + "/Modules/Danger.swiftmodule"

    switch (fileManager.fileExists(atPath: flatModule), fileManager.fileExists(atPath: nestedModule)) {
    case (true, false):
        return buildFolder
    case (false, true):
        return buildFolder + "/Modules"
    default:
        #if compiler(<6.0)
            return buildFolder
        #else
            return buildFolder + "/Modules"
        #endif
    }
}

Probing for the exact artifact (not mere Modules/ directory existence) avoids a false positive from an empty/partial Modules/ left over from a prior build under a different toolchain.

Backwards compatibility is testable, not just asserted: both unambiguous branches are byte-identical to today's two #if compiler branches, and the only behavior change is in states that are broken today (native <6.0 binary vs. ≥6.0 package or vice versa, and swiftbuild). The ambiguous fallback keeps every currently-working binary — including the official Docker image, which defaults to Swift 5.9 — on its existing compiled-in behavior.

The same fix is applied to Script.artifactsPath, resolved against the script's own folder (not the process's working directory, since the build for that path runs in a separate scratch folder).

Testing

  • swift test --filter "SPMDangerTests|ScriptTests" — 20/20 passing, including new coverage for all four probe states (flat-only, nested-only, both-present, neither-present) on both call sites, plus a regression test pinning that Script.artifactsPath probes under its own folder, not the process cwd.
  • Verified end-to-end against real Dangerfiles from a large iOS monorepo, compiling with the runner's actual swiftc flags:
    • Under the newer swiftbuild build system (Xcode 27 Beta 4, Swift 6.4): the unpatched behavior (-I <bin>/Modules) fails with no such module 'Danger'; the patched behavior (-I <bin>) compiles and runs.
    • Under native (Xcode 26.6, Swift 6.3.3): unchanged — the probe unambiguously selects the nested Modules/ path, byte-identical to today, and the flat path correctly fails (proving the two layouts are genuinely disjoint, not just "either works").
  • swift build -c release --product danger-swift succeeds under both toolchains.

Related

@DylanBettermannDD

Copy link
Copy Markdown
Contributor Author

The macOS CI failures here are unrelated to this diff. Every macOS job fails identically before Danger ever gets to run the compiled Swift:

Failed to fetch GitHub pull request files: TypeError: terminated
  ... ERR_INVALID_ARG_TYPE: The "stream" argument must be an instance of Stream. Received an instance of ReadableStream

This happens inside the danger-js binary's own fetch() call to GitHub, after our build already succeeded (Build complete!, danger-swift links and launches fine) — the compiled runner never gets a chance to run.

Root cause looks environmental: the macOS jobs install danger-js unpinned (brew install danger/tap/danger-js), while the Linux jobs use a pinned Node 20.x (actions/setup-node@v4) via yarn global add danger — and Linux passes on both this PR and #662. A recent danger-js release appears to bundle an undici version whose fetch() is incompatible with the current macOS runners.

This is reproducible on #662 as well (identical stack trace, identical point of failure), so it predates this PR and isn't something this diff can fix — flagging in case it's useful context, but not blocking review of the actual change.

@DylanBettermannDD

DylanBettermannDD commented Aug 1, 2026

Copy link
Copy Markdown
Contributor Author

Correction to my earlier note here about the macOS CI failure: I'd said "a recent danger-js release appears to bundle an undici version whose fetch() is incompatible with the current macOS runners" — that's not quite right. The runners' own Node (22/24) is fine. The actual cause is that macOS CI installs danger-js via the Homebrew tap, which ships a pkg-built standalone binary with its own embedded Node 18 older than 18.17 — and undici 6 (which danger-js 13.0.10 pins) requires >=18.17. Below that, every fetch() call throws ERR_INVALID_ARG_TYPE.

Opened a fix at #664: install danger-js on macOS via setup-node + yarn, matching how the (already-passing) Linux jobs install it, instead of the brew binary. CI on that PR is green, including a full run of the Test dependencies resolver and Test on macOS matrices (Danger: ✓ passed review). I haven't re-run this branch's own CI against that fix yet, but the mechanism is now confirmed rather than just theorized.

f-meloni pushed a commit that referenced this pull request Aug 21, 2026
…g Linux legs

The brew-installed danger-js on macOS is a pkg-built standalone binary with
its own embedded Node 18 (pkg --targets node18-macos-*; pkg 5.8.1 ->
pkg-fetch 3.4.2, whose newest Node 18 base is v18.15.0). danger-js 13.0.10
pins undici 6.21.1, which requires node >=18.17 per its engines field.
Below that floor, undici's fetchFinale hands stream.finished() a web
ReadableStream it doesn't support, so every fetch() in danger-swift ci
throws ERR_INVALID_ARG_TYPE and the step fails before Danger can post
anything -- unrelated to the runner's own Node (22/24), which is fine.

danger-js switched from node-fetch to undici in 13.0.10 (2026-06-25),
which is why every macOS PR run since has failed identically, including
on PRs unrelated to this change (#662, #663).

The Linux jobs already avoid this by installing danger from npm via
actions/setup-node + yarn instead of the brew binary. Do the same on
macOS.
@f-meloni
f-meloni requested a lite review from Copilot August 21, 2026 09:59

Copilot AI 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.

Pull request overview

This PR fixes Dangerfile compilation failures caused by SwiftPM’s differing module output layouts by replacing compile-time Swift-version heuristics with a runtime probe for the actual Danger.swiftmodule location, ensuring the -I import path matches the build artifacts produced by the target toolchain/build system.

Changes:

  • Updated SPMDanger.moduleFolder to probe for Danger.swiftmodule in both the flat and Modules/ layouts and fall back to the prior compiled default when ambiguous.
  • Updated Script.artifactsPath to use the same probing behavior, correctly resolved relative to the script’s folder.
  • Added/updated tests to cover flat-only, nested-only, both-present, neither-present probe states, plus a regression test ensuring probing occurs under the script folder (not process CWD); updated the changelog entry.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated no comments.

Show a summary per file
File Description
Sources/RunnerLib/SPMDanger.swift Replaces compile-time module path selection with a runtime probe for Danger.swiftmodule to choose the correct SwiftPM module layout.
Sources/DangerDependenciesResolver/Script.swift Applies the same runtime probe to Marathon/inline-dependency artifact discovery, scoped to the script’s folder.
Tests/RunnerLibTests/SPMDangerTests.swift Expands test coverage for the new moduleFolder probing behavior and updates the file manager stub.
Tests/DangerDependenciesResolverTests/ScriptTests.swift Adds comprehensive probe-state tests for artifactsPath, including a regression test for probing under folder.
CHANGELOG.md Documents the fix and adds the contributor reference.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

@f-meloni f-meloni left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Looks good! Thank you!

@DylanBettermannDD
DylanBettermannDD force-pushed the db/mobex-5198-modulefolder-probe branch from f142449 to ce800f6 Compare August 21, 2026 16:55
@DylanBettermannDD

Copy link
Copy Markdown
Contributor Author

CI here is failing on every job at the "Install danger-js" step:

error content-type@3.0.0: The engine "node" is incompatible with this module. Expected version ">=22". Got "20.20.2"

This isn't caused by this PR — yarn global add danger always installs the latest danger-js, and its content-type dependency now requires Node >=22, while CI still installs Node 20 via setup-node. This would break master too.

Opened #665 to bump setup-node to Node 22 across all CI jobs — all 15 checks pass there. Once that merges, rebasing this branch should clear up CI here.

@renfrenkel

renfrenkel commented Aug 24, 2026

Copy link
Copy Markdown

@DylanBettermannDD are you able to make this ready for review now that the CI fix was merged?

…ler that built danger-swift

moduleFolder (and the equivalent Script.artifactsPath) picked between
.build/debug and .build/debug/Modules using a compile-time #if compiler(<6.0)
check on whichever Swift built the danger-swift binary itself. That's a proxy
for a runtime property of the toolchain/build system building the target
package, and it's wrong whenever those two differ — which is the normal case
for a distributed binary (Homebrew, Docker, the new prebuilt universal
binary), and is why the swiftbuild build system (Xcode 16.3+'s new default)
breaks it.

Replace the compile-time check with a runtime probe for the exact
Danger.swiftmodule artifact at both candidate locations, falling back to
today's compiled-in default whenever the probe is ambiguous (both or neither
present) so no currently-working configuration changes behavior.
The Marathon inline-dependency path's Script.artifactsPath had the same
compile-time #if compiler(<6.0) defect as SPMDanger.moduleFolder, but an
initial fix probed FileManager.default against paths relative to the
process's current directory. Runner.swift's only caller resolves the
returned paths relative to the script's own folder (a separate directory
where the build actually ran), not the process cwd, so the probe could pick
the wrong candidate based on unrelated filesystem state.

Resolve the probe against the script's own folder instead, with an
injectable FileManager so this is actually testable, and add coverage
mirroring the SPMDangerTests cases plus a regression test pinning that the
probe ignores the process cwd.
@DylanBettermannDD
DylanBettermannDD force-pushed the db/mobex-5198-modulefolder-probe branch from ce800f6 to 8dad78b Compare August 24, 2026 14:40
@DylanBettermannDD
DylanBettermannDD marked this pull request as ready for review August 24, 2026 14:54
@DylanBettermannDD

Copy link
Copy Markdown
Contributor Author

@f-meloni @renfrenkel rebased and ready for review. It was previously approved, but I don't have permissions to merge PRs.

@f-meloni
f-meloni merged commit 5388741 into danger:master Aug 24, 2026
15 checks passed
@DylanBettermannDD
DylanBettermannDD deleted the db/mobex-5198-modulefolder-probe branch August 25, 2026 17:37
clarmso added a commit to mozilla-mobile/firefox-ios that referenced this pull request Sep 3, 2026
Xcode 27's SwiftPM emits modules flat in .build/debug instead of
.build/debug/Modules, so danger-swift 3.22.x compiled the Dangerfile with
a stale -I and failed the "Run Danger 2" step with "no such module
'Danger'". Pin to danger/swift#663, which probes for the module rather
than trusting a compile-time layout check.

The pinned revision has isDevelop = true, so danger/swift exports its own
DangerDeps product; rename ours to DangerDepsFirefox to avoid the
duplicate-product error. danger-swift discovers it by the
DangerDeps[A-Za-z]* pattern, so no other reference needs updating.

Verified end-to-end on Xcode 27.0 (Swift 6.4) and building clean under
Xcode 26.6 (Swift 6.3.3).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
github-actions Bot pushed a commit to mozilla-mobile/firefox-ios that referenced this pull request Sep 4, 2026
Xcode 27's SwiftPM emits modules flat in .build/debug instead of
.build/debug/Modules, so danger-swift 3.22.x compiled the Dangerfile with
a stale -I and failed the "Run Danger 2" step with "no such module
'Danger'". Pin to danger/swift#663, which probes for the module rather
than trusting a compile-time layout check.

The pinned revision has isDevelop = true, so danger/swift exports its own
DangerDeps product; rename ours to DangerDepsFirefox to avoid the
duplicate-product error. danger-swift discovers it by the
DangerDeps[A-Za-z]* pattern, so no other reference needs updating.

Verified end-to-end on Xcode 27.0 (Swift 6.4) and building clean under
Xcode 26.6 (Swift 6.3.3).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
github-actions Bot pushed a commit to mozilla-mobile/firefox-ios that referenced this pull request Sep 5, 2026
Xcode 27's SwiftPM emits modules flat in .build/debug instead of
.build/debug/Modules, so danger-swift 3.22.x compiled the Dangerfile with
a stale -I and failed the "Run Danger 2" step with "no such module
'Danger'". Pin to danger/swift#663, which probes for the module rather
than trusting a compile-time layout check.

The pinned revision has isDevelop = true, so danger/swift exports its own
DangerDeps product; rename ours to DangerDepsFirefox to avoid the
duplicate-product error. danger-swift discovers it by the
DangerDeps[A-Za-z]* pattern, so no other reference needs updating.

Verified end-to-end on Xcode 27.0 (Swift 6.4) and building clean under
Xcode 26.6 (Swift 6.3.3).

Co-Authored-By: Claude Opus 5 (1M context) <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.

4 participants