Skip to content

fix(report)!: address CI files from the repository root#1808

Open
Jerc92 wants to merge 1 commit into
fallow-rs:mainfrom
Jerc92:fix/monorepo-report-paths
Open

fix(report)!: address CI files from the repository root#1808
Jerc92 wants to merge 1 commit into
fallow-rs:mainfrom
Jerc92:fix/monorepo-report-paths

Conversation

@Jerc92

@Jerc92 Jerc92 commented Jul 10, 2026

Copy link
Copy Markdown
Contributor

What

When --root points at a package inside a larger git repo, fallow's CI output was unusable in two ways that share one cause: findings were keyed and emitted relative to the analysis root, while CI platforms and git diff address files from the repository root.

Report paths. codeclimate, review-github, and review-gitlab emitted --root-relative paths. GitLab's Code Quality widget matches location.path against the MR diff, and the review-discussion API rejects a position.new_path naming a file absent from the diff — so the widget silently showed nothing and every inline discussion POST was rejected. github-annotations already rebased onto the git toplevel via PathRebase; the other CI formats now do too. review-* and the sticky summary derive their paths from CodeClimate issues, so one seam covers them.

Diff filtering. --diff-file compared diff keys against finding paths already stripped to --root. Below the toplevel nothing matched: every source-anchored finding was dropped and the run reported a clean diff, exit 0, nothing on stderr. That is the dangerous one — it doesn't produce wrong output, it produces empty output that looks like "clean diff, no findings."

A unified diff does not declare its own base, and both conventions occur in practice: git diff writes toplevel-relative paths, git diff --relative writes root-relative ones. So the base is resolved from the diff's paths themselves — whichever candidate directory they actually name existing files under wins. Both conventions now work.

Namespaces. The underlying fix is that a path lives in one of three namespaces and the code mixed them:

namespace example who needs it
analysis-root-relative src/a.js findings, CODEOWNERS
diff key packages/pkg/src/a.js or src/a.js DiffIndex lookups
presentation packages/pkg/src/a.js CI platforms

Paths now stay analysis-root-relative through diff filtering and ownership resolution. The presentation prefix is applied at exactly two boundaries: emit_codeclimate (wire format) and ReviewEnvelopeRenderInput::path_prefix (review render). DiffIndex owns the translation (key_for_root_relative, root_relative_from_key, old_path_for_root_relative).

Also fixed, all found while reviewing the above:

  • Renamed files lost old_path. The rename map is keyed in the diff's namespace but the lookup used the rendered path, so old_path silently fell back to new_path — telling GitLab a moved file had not moved.
  • --diff-file did not scope inline review comments. That filter gated on $FALLOW_DIFF_FILE, so the flag rendered every comment while appearing to have applied the diff.
  • --report-path-prefix decided which comments survived the diff filter. Keys came from the rendered path; they now come from the root-relative path plus the diff's base. The prefix is presentation only.
  • A latent CLI misparse: neither --annotations-path-prefix nor its replacement was registered in global_value_options(), so fallow --annotations-path-prefix p check read the prefix as the subcommand.

Fail-open, never silently clean. A diff whose paths name no file under any candidate base is foreign; one that names real files under two candidates is ambiguous. Neither can be placed. check/filtering.rs sets the convention — an unfilterable path is RETAINED, never silently dropped — so both cases now discard the diff, warn, and report at full scope. Filtering to zero on a guess is the exact failure this PR removes.

Why

Downstream, this is what makes fallow usable on a monorepo package at all: the Code Quality widget, the sticky summary, and inline review discussions all address files by repo-root-relative path. Consumers have been working around it with post-processing scripts that rebase location.path by hand.

--annotations-path-prefix is renamed to --report-path-prefix (old name kept as a visible_alias) and now governs every CI-facing format. Pass an empty value to opt out.

Breaking change

codeclimate, review-github, and review-gitlab now emit repository-root-relative paths when --root is a subdirectory. Consumers that prepend the offset themselves should drop that step, or pass --report-path-prefix '' to restore the previous output. Single-package repositories, where --root is the toplevel, are unaffected — the two candidate bases coincide and output is byte-identical, which the untouched snapshot suite pins.

Deliberately out of scope

  • SARIF is not rebased and is excluded from the new flag's allowlist rather than half-applied: four separate builders, its own URI percent-encoding (a [slug] directory becomes %5Bslug%5D), and security-trace relatedLocations.
  • The programmatic / napi / MCP analysis.diffFile (crates/api/src/analysis_context.rs) still builds an unbased DiffIndex and keys against --root. Same defect class; left out to keep this PR's blast radius on the CLI/CI surface.

Happy to split either into a follow-up if you'd prefer them in this PR.

Test plan

Both defects were first reproduced against a freshly built main (3.3.0) in a throwaway monorepo, then pinned:

  • 17 new integration tests (crates/cli/tests/monorepo_report_paths_tests.rs) over a real git repo whose packages/pkg is the analysis root. They cover: repo-root-relative CodeClimate paths; --report-path-prefix override, empty opt-out, and deprecated alias; codeclimate and github-annotations agreeing on path shape with no double prefix; toplevel-relative and --relative diffs both keeping source-anchored findings; renames resolving old_path across all four namespace combinations; review paths prefixed exactly once on the combined and subcommand routes; --diff-file scoping review comments without the env var; ambiguous base failing open; foreign base warning; and --root == toplevel output unchanged.
  • New unit tests in crates/output/src/diff.rs for the namespace round-trip (key_for_root_relative / root_relative_from_key / old_path_for_root_relative), empty offsets, and sibling directories sharing a name prefix (packages/pkg-extra must not match packages/pkg).
  • cargo test --workspace --lib --bins --tests — 59 test binaries, 0 failures. snapshot_tests is untouched, which is the load-bearing check: if a snapshot had moved, the fix would have changed root == toplevel behavior and be wrong.
  • cargo clippy --workspace --all-targets -- -D warnings and cargo fmt --all -- --check clean.

The change was additionally reviewed by an independent adversarial pass, which caught three defects that are fixed here: the rename old_path fallback, a double prefix on the subcommand review route, and the ambiguous base failing closed.

Note: the commit is unsigned — no GPG key on the machine it was authored on. Happy to re-sign if that blocks merge.

When --root pointed at a package inside a larger repo, fallow's CI output was
unusable in two ways that shared one cause: findings were keyed and emitted
relative to the analysis root, while CI platforms and git address files from
the repository root.

Report paths. codeclimate, review-github, and review-gitlab emitted
--root-relative paths, so GitLab's Code Quality widget matched nothing and
every inline review discussion was rejected for naming a file absent from the
MR diff. github-annotations already rebased onto the git toplevel; the others
now do too, via the same PathRebase. The review and sticky-summary formats
derive their paths from CodeClimate issues, so one seam covers them all.
--annotations-path-prefix becomes --report-path-prefix (old name kept as an
alias) and governs every CI format. Both names are now registered in
global_value_options, fixing a latent misparse of the following subcommand.
The generated capability manifest and CLI reference are regenerated for the
rename.

The prefix is presentation only. The review and sticky-summary filters used to
key issues by their rendered path, so a custom or empty prefix silently stopped
matching the diff and dropped every inline comment. They now key by the
analysis-root-relative path plus the diff's own base.

Diff filtering. --diff-file compared diff keys against finding paths already
stripped to --root, so below the toplevel nothing matched: every
source-anchored finding was dropped and the run reported a clean diff, exit 0,
silently. A unified diff does not declare its own base, and both conventions
occur in practice -- `git diff` writes toplevel-relative paths, `git diff
--relative` writes root-relative ones. The base is therefore resolved from the
diff's paths themselves: whichever candidate directory they actually name
files under wins. The chosen base rides on DiffIndex, the type that defines
the namespace, so every filter call site picks it up and callers that build
their own index keep today's behavior.

Existence cannot always place a diff. A path naming a real file under both
candidates is ambiguous, and a path naming one under neither is foreign. Both
are reported. Silence here would rebuild the very failure this change removes:
a filter that matches nothing looks exactly like a clean diff.

When --root is the repository toplevel the two candidates coincide and output
is byte-identical, which the untouched snapshot suite pins.

SARIF is deliberately excluded: four builders and its own URI encoding make it
a separate change, so it is left out of the flag's allowlist rather than
half-rebased.

BREAKING CHANGE: codeclimate, review-github, and review-gitlab now emit
repository-root-relative paths when --root is a subdirectory. Consumers that
prepend the offset themselves should drop that step or pass
--report-path-prefix '' to restore the previous output.
@Jerc92 Jerc92 force-pushed the fix/monorepo-report-paths branch from 5402659 to 78f10aa Compare July 10, 2026 12:37
@Jerc92

Jerc92 commented Jul 10, 2026

Copy link
Copy Markdown
Contributor Author

Skills vendor drift needs a coordinated fallow-skills update

Heads up that this PR cannot go fully green on its own, and I'd rather say so than paper over it.

Renaming --annotations-path-prefix to --report-path-prefix restages two generated files:

  • npm/fallow/capabilities.json
  • npm/fallow/skills/fallow/references/cli-reference.md

generate:contracts:check requires both to be regenerated (it reports only the first stale file, which is why the earlier run named just capabilities.json). I regenerated them, and that gate now passes.

But npm/fallow/skills/** must mirror fallow-rs/fallow-skills verbatim, and that repo still documents the old flag name and its narrower help text. So Skills vendor drift now fails on exactly one line of cli-reference.md.

The two gates are mutually exclusive for this change:

gate wants cli-reference.md to say source of truth
generate:contracts:check --report-path-prefix the live CLI
Skills vendor drift --annotations-path-prefix fallow-rs/fallow-skills

Verified locally: restoring the old row makes generate:contracts:check fail with generated contract surfaces are stale; keeping the regenerated row makes the drift gate fail. No state of this branch satisfies both.

This is inherent to changing a CLI flag's name or help text, since cli-reference.md is generated from the CLI but owned by a separate repo. I don't have push access to fallow-rs/fallow-skills, so I can't land the other half.

Options, whichever you prefer:

  1. A maintainer regenerates cli-reference.md in fallow-rs/fallow-skills and merges it; this PR's drift gate then goes green with no further change here.
  2. I open a PR against fallow-rs/fallow-skills from a fork with the regenerated file, to be merged before this one.
  3. If the rename isn't wanted, I can drop it and keep --annotations-path-prefix as the only name — though the flag now governs codeclimate, review-github, and review-gitlab too, so the name and its "Valid only with the GitHub-native formats" help text would both be wrong.

Everything else on this PR is green: the full suite (including the untouched snapshot suite, which is the load-bearing check that --root == toplevel output is byte-identical), clippy -D warnings, fmt --check, generate:contracts:check, and a --features schema-emit build.

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.

1 participant