Skip to content

feat(manifest): unify Gradle config-level resolution transparency (REA-519)#1398

Merged
Jeppe Fredsgaard Blaabjerg (jfblaa) merged 4 commits into
v1.xfrom
jfblaa/rea-519-gradle-manifest-unify-config-level-transparency-agp
Jul 2, 2026
Merged

feat(manifest): unify Gradle config-level resolution transparency (REA-519)#1398
Jeppe Fredsgaard Blaabjerg (jfblaa) merged 4 commits into
v1.xfrom
jfblaa/rea-519-gradle-manifest-unify-config-level-transparency-agp

Conversation

@jfblaa

@jfblaa Jeppe Fredsgaard Blaabjerg (jfblaa) commented Jul 2, 2026

Copy link
Copy Markdown
Contributor

What & why

Follow-up to REA-516. Two Gradle resolution paths sat in tension with the "reveal what we couldn't resolve" policy:

  1. A whole configuration whose resolution throws (as opposed to a single unresolved dependency within it) was caught and logged to stdout, but never surfaced — it still counted as scanned, so the user was never told we couldn't scan it.
  2. The --include-configs help still advertised a default AGP-instrumented-test exclusion that the native-facts rewrite had already removed.

Changes

REA-519 — config-level transparency

  • The per-config try/catch now records the config as a new unscannable fact (build-tool's deepest-cause message) instead of swallowing it. The build script still exits cleanly; the TS layer owns reporting.
  • unscannable configs are classified by the same cause rules as per-dependency failures: variant ambiguity stays lenient (a one-line notice), every other cause is fail-closed (blocking, overridable by --ignore-unresolved).
  • Un-scannable configs are excluded from "Resolution succeeded in"; full messages show under --verbose.
  • A config-level failure no longer trips the crashed-build path (which stays reserved for a build that produced nothing at all).
  • Dropped the stale "default excludes AGP instrumented-test classpaths" help text (+ snapshots).

REA-516 — classifier clarification (first commit)

  • Recognize Gradle's alternate zero-compatible-variant phrasings (unable to find a matching variant, no compatible variant) so they render under the accurate "No compatible variant" header instead of falling through to "Other". Severity unchanged (both blocking).

Empirical validation

Validated on real throwaway Gradle projects (AGP 8.13.2 / Gradle 9.2.1 / JDK 21):

  • AGP *AndroidTest* configs resolve cleanly from the init script (41 nodes/config, no ambiguity, no throws) — the hypothesized "init-script can't supply consumer attributes → ambiguity" does not occur, because AGP sets the attributes on the configuration itself. Removing the name-based exclusion was safe.
  • Gradle's lenientConfiguration swallows all dependency-graph failures (not-found, capability conflict, version conflict, variant ambiguity) into per-dep records. The unscannable path therefore fires only on non-graph exceptions (resolution-rule throw, artifact-transform failure, broken repo config, …) — a rare but real safety net.
  • The full unscannable pipeline was exercised end-to-end (one config throwing, others resolving): default → exit 1 + no facts written; --ignore-unresolved → exit 0 + partial facts written.

Testing

  • pnpm check:tsc clean; targeted unit tests green (render/assemble/gradle/kotlin help snapshots).
  • Verified end-to-end via socket manifest gradle --facts against the test projects.

Note

Medium Risk
Changes exit-code and facts-write behavior when Gradle configurations throw during resolution (partial SBOM vs hard fail), which affects manifest/scan workflows on Android and complex Gradle builds; logic is covered by new render tests and mirrors existing per-dep failure policy.

Overview
Gradle manifest facts generation now records whole-configuration resolution failures instead of swallowing them in the init script. Thrown configs emit unscannable records through the line protocol; the TypeScript layer classifies each message with the same rules as per-dependency failures (variant ambiguity → non-blocking notice; other causes → blocking, honor --ignore-unresolved). Reports distinguish “could not scan N configuration(s)” from unresolved deps, omit unscannable configs from “Resolution succeeded in”, and include them in --verbose output. Crashed-build detection no longer treats unscannable-only outcomes as a total build failure.

--include-configs help for manifest gradle / manifest kotlin no longer claims a default AGP instrumented-test exclusion (default is every resolvable configuration). Gradle’s “unable to find a matching variant” / “no compatible variant” wording maps to the existing no-matching-variant blocking category.

Reviewed by Cursor Bugbot for commit 0c65404. Configure here.

…ings

Gradle phrases a zero-compatible-variant failure several ways depending on
version and whether consumer attributes were supplied. The classifier only
matched "no matching variant"/"no variants of", so "unable to find a
matching variant" and "no compatible variant" fell through to the generic
"other" category. Severity was unaffected (both blocking), but the report
rendered under the wrong header; recognize the alternate phrasings so these
surface under the accurate "No compatible variant" heading.
A whole Gradle configuration whose resolution throws (as opposed to a single
unresolved dependency within it) was previously caught and logged to stdout but
never surfaced: it still counted as "scanned", so the user was never told we
could not scan it.

Record such configs as a new `unscannable` fact and classify them by the same
cause rules as per-dependency failures: variant ambiguity stays lenient (a
one-line notice), every other cause is fail-closed (blocking, overridable by
--ignore-unresolved). The build script still exits cleanly and the TS layer
owns the reporting; a config-level failure no longer trips the crashed-build
path, which stays reserved for a build that produced nothing at all.

Also drop the stale "--include-configs default excludes AGP instrumented-test
classpaths" help text: the native-facts rewrite removed that exclusion, and
AGP AndroidTest configs resolve like any other.

@mtorp Martin Torp (mtorp) left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

LGTM — traced the unscannable pipeline end-to-end (init script → records → assemble → render → exit policy), and typecheck + the render/help-snapshot tests pass locally. Two cosmetic nits inline, neither blocking.

Comment thread src/commands/manifest/scripts/resolution-report-render.mts Outdated
Comment thread src/commands/manifest/scripts/socket-facts.init.gradle Outdated
- Don't lead the failure summary with a blank line when there are only blocking
  unscannable configs and no per-dependency failures (avoids a dangling marker
  under logger.fail); added a regression assertion.
- Correct the config-throw comments: the path fires on config-LEVEL failures
  (resolution-rule throws, artifact-transform failures, broken repos), not AGP
  *AndroidTest* configs, which resolve cleanly per the validation.
@jfblaa Jeppe Fredsgaard Blaabjerg (jfblaa) merged commit 6619eae into v1.x Jul 2, 2026
13 checks passed
@jfblaa Jeppe Fredsgaard Blaabjerg (jfblaa) deleted the jfblaa/rea-519-gradle-manifest-unify-config-level-transparency-agp branch July 2, 2026 10:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

2 participants