Skip to content

Refactor/deps checker - #170

Merged
mathieusouflis merged 14 commits into
mainfrom
refactor/deps-checker
Jul 12, 2026
Merged

Refactor/deps checker#170
mathieusouflis merged 14 commits into
mainfrom
refactor/deps-checker

Conversation

@mathieusouflis

@mathieusouflis mathieusouflis commented Jul 11, 2026

Copy link
Copy Markdown
Member

Summary

Closes #

Type of change

  • Bug fix
  • New feature
  • Refactor (no behavior change)
  • Performance improvement
  • Documentation
  • CI/CD / tooling
  • Breaking change

Changes

Testing

  • Added / updated tests
  • Manually tested locally
  • All existing tests pass

Checklist

  • Lint passes
  • Tests pass
  • Build succeeds
  • Documentation updated if applicable

Screenshots

@github-actions

Copy link
Copy Markdown
Contributor

Dependency Review

The following issues were found:
  • ✅ 0 vulnerable package(s)
  • ✅ 0 package(s) with incompatible licenses
  • ✅ 0 package(s) with invalid SPDX license definitions
  • ⚠️ 2 package(s) with unknown licenses.
See the Details below.

License Issues

.github/workflows/generator-versions.yml

PackageVersionLicenseIssue Type
actions/checkout7.*.*NullUnknown License
actions/setup-go6.*.*NullUnknown License

OpenSSF Scorecard

PackageVersionScoreDetails
actions/actions/checkout 7.*.* 🟢 7
Details
CheckScoreReason
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Code-Review🟢 10all changesets reviewed
Maintained🟢 1018 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 10
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Binary-Artifacts🟢 10no binaries found in the repo
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Packaging⚠️ -1packaging workflow not detected
Fuzzing⚠️ 0project is not fuzzed
License🟢 10license file detected
Pinned-Dependencies🟢 3dependency not pinned by hash detected -- score normalized to 3
Signed-Releases⚠️ -1no releases found
Security-Policy🟢 9security policy file detected
SAST🟢 10SAST tool is run on all commits
Branch-Protection🟢 6branch protection is not maximal on development and all release branches
actions/actions/setup-go 6.*.* 🟢 5.3
Details
CheckScoreReason
Maintained🟢 34 commit(s) and 0 issue activity found in the last 90 days -- score normalized to 3
Code-Review🟢 10all changesets reviewed
Binary-Artifacts🟢 10no binaries found in the repo
Dangerous-Workflow🟢 10no dangerous workflow patterns detected
Packaging⚠️ -1packaging workflow not detected
Token-Permissions⚠️ 0detected GitHub workflow tokens with excessive permissions
Pinned-Dependencies⚠️ 0dependency not pinned by hash detected -- score normalized to 0
CII-Best-Practices⚠️ 0no effort to earn an OpenSSF best practices badge detected
Fuzzing⚠️ 0project is not fuzzed
License🟢 10license file detected
Signed-Releases⚠️ -1no releases found
Security-Policy🟢 9security policy file detected
Branch-Protection⚠️ 0branch protection not enabled on development/release branches
SAST🟢 9SAST tool is not run on all commits -- score normalized to 9

Scanned Files

  • .github/workflows/generator-versions.yml

@github-actions

Copy link
Copy Markdown
Contributor

❌ Commit message validation failed.

Please ensure your commits follow the Conventional Commits format:

<type>(<scope>): <description>

Allowed types: feat, fix, docs, style, refactor, perf, test, chore, ci, revert

See CONTRIBUTING.md for details.

Allows() cannot distinguish "there is nothing newer" from "what's newer breaks
the constraint" — it rejects a version below the anchor for the same reason it
rejects one above the permitted range. Callers classifying dependency drift need
to tell those apart.
Pins every npm package the generators scaffold, in one place, with exactly one
pin per package. Generators name the packages they need; the catalog says which
version they get.

One version per package, repo-wide, is the point. When versions lived inline in
40 generator files the same package could drift to two versions unnoticed, and it
did — vitest sat at ^4.1.7 and ^4.1.8, bcryptjs at ^2.4.3 and ^3.0.3. With one
key per package that is unrepresentable rather than merely discouraged.

Ecosystem is declared by the pin, not inferred from which file a generator
happens to write. Only npm is live; Cargo, Maven and Go come back when a
generator actually needs them.

Refs: ADR-0002
Generators now name the packages they need; internal/deps supplies the version.
No generator source carries a version string any more.

Three generators built the package *name* in a Go variable — auth_clerk_frontend,
sentry_frontend and storybook_setup. That made @clerk/nextjs, @sentry/nextjs and
@storybook/nextjs invisible to any scan driven by an empty answer set, and made
the packages on the default branch impossible to patch by matching a source
literal. deps.NPM() takes a runtime string, so the conditionals survive intact
while every package stays visible in the catalog.

Five deliberate output changes ride along, because the catalog admits one pin per
package and these could not be carried forward as-is:

  bcryptjs            ^2.4.3 / ^3.0.3 -> ^3.0.3  (drift; identical default import)
  vitest              ^4.1.7 / ^4.1.8 -> ^4.1.8  (drift)
  @vitest/coverage-v8 ^4.1.7 / ^4.1.8 -> ^4.1.8  (drift)
  @version14/ui       0.8.0 -> ^0.8.0            (an exact pin admits no update)
  @types/bcryptjs     removed                    (deprecated stub; bcryptjs 3.x ships types)

react-router-dom is dropped and the templates now import from react-router,
completing the v7 -> v8 major that was previously merged as a version bump while
the templates still imported the v7 package.

Verified output-neutral for 56 of 61 generators; all 36 test-flow fixtures pass a
full run.
The catalog migration was output-neutral for 56 of 61 generators. These five
scaffold something genuinely different, so their manifest versions move:

  auth_jwt_mvc_route          minor  bcryptjs 2.x -> 3.x
  react_router_v7             minor  react-router-dom dropped, imports on v8
  auth_jwt_clean_arch_module  patch  @types/bcryptjs stub removed
  version14_ui                patch  exact pin -> caret
  vitest_testing_library      patch  vitest ^4.1.7 -> ^4.1.8
Thirteen doc pages disagreed with their manifest. The old dependency bot wrote
these rows and then discarded them every single run: it staged with
`git add generators/`, so nothing under docs/ was ever committed. No bot commit in
the repo's history touches docs/.

express_test_setup and monorepo_ts_workspaces had no doc page at all.
The old scanner ran Generate() on every generator with an empty Answers map and
read back whatever package.json fell out. ADR-0001 called this "strictly
accurate"; it was not. Generators branch on Answers, so an empty map executes one
arbitrary branch, and three generators pick their package name from the framework
answer — @clerk/nextjs, @sentry/nextjs and @storybook/nextjs were never checked
once in the tool's lifetime.

Worse, the scanner read *values* out of generated JSON while the patcher rewrote
*source literals* it located by regex. Nothing guaranteed such a literal existed.
Where it did not, the patch errored, the bash driver broke out of its loop and ran
`git checkout main` without resetting the working tree, and the next iteration's
`git add generators/` swept the already-patched files into an unrelated commit.

The scan is now a map read over the catalog: no generator runs, no answers, no
branch can hide a package. The patcher writes internal/deps/npm.go and nothing
else, so the value the scan read is the literal the patch writes.

Classification delegates to internal/versioning: an update is low-risk iff the new
version satisfies the pin's existing constraint. That gets zero-major right for
free — ^0.45.2 does not permit 0.46.0 — where the hand-rolled comparison it
replaces called drizzle-orm 0.45.2 -> 0.46.0 a safe "minor" and would have shipped
a breaking release inside a batch.

Cargo, Maven and Go extractors are removed; pom.xml was even routed to an
extractor that did not exist. No generator writes those files.

Refs: ADR-0002
Replaying a flow headlessly from a recorded answer map was private to test-flow.
Fingerprinting needs the same thing, and two copies of a flow runner would be two
things to keep in step.
What a generator contributes cannot be recovered after the fact. Generators merge
into shared files — above all package.json — and the finished state records only
that a generator touched a file, never what it put there. Bracketing each call is
the only way to attribute the edit.
A fingerprint hashes a generator's contribution — the files it adds and the edits
it makes to files other generators own — across every fixture that exercises it.
It answers the only question that matters when deciding whether a manifest version
must move: did what this generator scaffolds actually change?

A contribution is a diff, not a file hash, and that distinction is the whole point.
Hashing the resulting files would mean bumping the react pin moved the fingerprint
of every generator that touches package.json, when only two name react. That
over-triggering is not cosmetic: it is exactly the defect behind commit d53c302,
which bumped 15 generator manifests for a change to one generator.

Create and modify are deliberately not distinguished either. Whether a generator
creates package.json or merges into one another generator already created is an
accident of topological order, so encoding it would demand a version bump whenever
a new generator was inserted ahead of an existing one.
  dot gen-fingerprint   generator -> fingerprint, computed across the fixtures
  dot gen-check --docs  every doc version row equals its manifest version
  dot gen-check --bumped --base=<fingerprints.json>
                        every generator whose fingerprint moved has been bumped

Entirely in memory — no pnpm install — so the whole fixture suite runs in seconds.
A comment fix or a gofmt moves no fingerprint and demands nothing; a template edit
does.
Rule 1  a generator whose fingerprint moved has a bumped manifest version
Rule 2  every generator's doc version row equals its manifest version

Rule 1 deliberately does not fire on a diff that only touches internal/deps/.
A catalog pin change alters what many generators scaffold, but forcing those
manifest bumps into the dependency PR is what made merge order corrupt them: a
bump computed against main at PR-creation time is a relative operation applied at
merge time, when main has moved. Those bumps are derived at release instead.
Closing dependency drift is not one kind of work, and the old script treated it as
if it were.

  Rollup       every update that satisfies its pin's constraint, in one batched PR.
               Bot-owned and disposable: rebuilt from scratch each run. Excludes any
               package holding an open dep PR of its own.
  Migration    an update that breaks its pin's constraint. One PR per package,
               human-owned from the moment it is raised. The bot never touches that
               branch again, so it rebases cleanly for months.
  Deprecation  an issue, never a PR. A deprecation is not fixed by a version bump —
               the replacement is a different package, or none.

The bot patches internal/deps/npm.go and nothing else. The old script ran
`git add generators/`, which committed whatever happened to be dirty in the working
tree; that is how 997e489, titled "bump @clerk/clerk-react", came to contain 13
files of unrelated packages and no clerk changes.

It no longer bumps manifests — those are derived at release — and it no longer
claims to run test-flows, a gate ADR-0001 promised and never implemented. The PR's
own CI runs test-flows; duplicating it would double the most expensive job in CI.

Deprecation issues are keyed on a single package name. The old key was the issue
title, which contained a variable package list, which is why the same
@types/bcryptjs deprecation was filed twice under two different titles.
`make release-prep` bumps every generator whose fingerprint has moved since the
last tag, once, from the final state.

A version bump stored inside a PR is a relative operation — 0.8.0 -> 0.9.0 —
computed against main at PR-creation time and applied at merge time, when main has
moved. With several dependency PRs open, the version you end up with is a function
of merge order. It is fatal for long-lived migrations: one open for two months
collides with every weekly rollup touching the same manifest.

Deriving the bump here makes merge order provably irrelevant. Any sequence of
merges produces the identical manifest.
ADR-0002 replaces ADR-0001, whose central claim — that instantiating a generator
and reading back its output is "strictly accurate" and "can never drift" — is the
thing that broke.

CONTEXT.md is the glossary. It separates two concepts the codebase had been running
together under one word: pin drift (our pinned version has fallen behind the
registry) and generator drift (a project's generator version no longer satisfies
its recorded constraint). It also names rollup and migration, which are different
kinds of work rather than two sizes of the same one.

docs/contributor/dep-checker.md described a test-flows gate that never existed.
@mathieusouflis
mathieusouflis force-pushed the refactor/deps-checker branch from 4a0c54e to 0f05a06 Compare July 12, 2026 10:24
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
B Security Rating on New Code (required ≥ A)

See analysis details on SonarQube Cloud

💡 Need a hand with PR review? Try Gitar by Sonar!

@mathieusouflis
mathieusouflis merged commit 94e0a86 into main Jul 12, 2026
53 of 54 checks passed
@mathieusouflis
mathieusouflis deleted the refactor/deps-checker branch July 12, 2026 16:55
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