feat(generator): version-aware migrations between docs-kit releases in --sync
Summary
Closes #41. docs_kit:install --sync was deliberately version-agnostic (the right v1 call): it reconciled a site against the current gem template with no notion of which docs-kit version the site was last synced from. Now that the gem is post-1.0, this adds the mechanism to apply the ordered migration steps between a site's last-synced version and the current one.
What changed
| Piece | What it does |
|---|---|
| Version stamp | Every install and --sync writes an inert # docs-kit synced: vX.Y.Z comment on the first line of the config initializer — the one file every site has. Injected into an existing (never-clobbered) initializer, updated in place, idempotent. |
Migration | An ordered, versioned transform: to (the release it belongs to), a description, and a block run with (root, generator) that returns the warn-only messages it couldn't safely automate (nil → []). |
MigrationRegistry | Selects the migrations in the half-open range (last_synced, gem_version], runs them ascending, collects warnings. Ships empty at 1.0.x — the mechanism is the deliverable. |
--sync wiring | Reads the stamp before restamping, runs the applicable migrations, prints a migration steps to apply by hand: checklist for anything warn-only, then restamps. Full (non-sync) installs don't migrate — a fresh scaffold isn't an upgrade. |
The first concrete 1.x → 1.y migration is now a one-line Migration.new(to: "1.x.0", description: "...") { ... } addition to MigrationRegistry::MIGRATIONS.
Key invariant (surfaced by an end-to-end drive, not the mocked tests)
applicable caps at the installed gem version (upto:, default DocsKit::VERSION). A migration targeting a version above the gem can't legitimately apply — a site can't have "arrived" at a release it doesn't have — and, without the cap, would re-fire on every sync after the site restamps to the gem version. The upto bound is what makes --sync idempotent. This bug only appeared when I drove the real generator on disk; it's now covered by both a unit spec and an end-to-end assertion.
Backwards compatibility
- A pre-feature site (un-stamped initializer) is treated as the earliest version (
0.0.0) → every migration applies. Its config body is preserved byte-for-byte; only the inert stamp comment is prepended. - New sites via
docs-kit neware stamped automatically — the new-site template runsdocs_kit:install, so no separate wiring is needed. - Zeitwerk is unaffected:
lib/generators/**is outside the loader's push-dirs; the new files are loaded by Rails' generator system /require_relative.
Test Coverage
spec/generators/migration_spec.rb— theMigrationvalue object: version coercion, block invocation with(root, generator),nil → []warnings, natural version ordering (1.10.0after1.1.0).spec/generators/migration_registry_spec.rb—applicablegap logic (exclusivefrom, ascending order, unknown/ahead sites, theuptoceiling filtering unreleased migrations) andmigrate!ordering + warning collection;.defaultships empty.spec/generators/install_generator_spec.rb— stamping (on install, injected into an existing initializer, updates a stale stamp, idempotent) and the--syncmigration wiring (runs for a stamped site and a pre-feature un-stamped site, prints warnings, no-op at current version, not on a full install).
Verification
-
bundle exec rspec— 780 examples, 0 failures, 94.82% line coverage -
bundle exec rubocop— no offenses - End-to-end drive of the real generator against a throwaway skeleton: stamp on install → roll back → gap-detected migration runs on sync → restamp → idempotent second sync
Test plan for a reviewer
bundle exec rspec spec/generators/
bundle exec rubocop lib/generators/docs_kit/install/