Skip to content

feat(generator): version-aware migrations between docs-kit releases in --sync - #56

Merged
mhenrixon merged 1 commit into
mainfrom
issue-41-version-aware-migrations
Jul 10, 2026
Merged

feat(generator): version-aware migrations between docs-kit releases in --sync#56
mhenrixon merged 1 commit into
mainfrom
issue-41-version-aware-migrations

Conversation

@mhenrixon

Copy link
Copy Markdown
Collaborator

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 new are stamped automatically — the new-site template runs docs_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 — the Migration value object: version coercion, block invocation with (root, generator), nil → [] warnings, natural version ordering (1.10.0 after 1.1.0).
  • spec/generators/migration_registry_spec.rbapplicable gap logic (exclusive from, ascending order, unknown/ahead sites, the upto ceiling filtering unreleased migrations) and migrate! ordering + warning collection; .default ships empty.
  • spec/generators/install_generator_spec.rb — stamping (on install, injected into an existing initializer, updates a stale stamp, idempotent) and the --sync migration 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/

https://claude.ai/code/session_01FPQb6z3YwcKRMbvoJhdxnX

…n --sync

## Summary

`docs_kit:install --sync` was version-agnostic: it diffed a site against the
current gem template with no notion of which docs-kit version the site was last
synced from, so it could not apply *ordered* release-to-release migrations.

This adds the mechanism (issue #41):

- **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 + MigrationRegistry** — a Migration is an ordered, versioned
  transform (`to` = the release that introduced it) whose block returns the
  warn-only messages it couldn't safely automate. The registry selects the
  migrations in the half-open range `(last_synced, gem_version]` and runs them
  ascending, collecting warnings.
- **--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 to the current version. Full (non-sync) installs
  don't migrate — they're a fresh scaffold, not an upgrade.

The registry ships EMPTY at 1.0.x: the mechanism is the deliverable, and the
first concrete `1.x → 1.y` transform is a one-line `Migration.new(...)` addition.

## Key invariant (surfaced by an end-to-end drive)

`applicable` caps at the installed gem version (`upto:`, default
`DocsKit::VERSION`). A migration targeting a version above the gem can't apply —
the site can't have that release — and, without the cap, would re-fire on every
sync after the site restamps to the gem version. The `upto` bound makes `--sync`
idempotent.

## 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 new` are stamped automatically (the template runs
  `docs_kit:install`).

## Test Coverage

- spec/generators/migration_spec.rb — Migration value object (version coercion,
  block invocation, nil→[] warnings, natural version ordering).
- spec/generators/migration_registry_spec.rb — applicable() gap logic
  (exclusive from, ascending, unknown/ahead sites, the upto ceiling) and
  migrate!() ordering + warning collection; .default ships empty.
- spec/generators/install_generator_spec.rb — stamping (install + inject into
  existing + update stale + idempotent) and the --sync migration wiring (runs
  for a stamped/pre-feature site, prints warnings, no-op at current version, not
  on a full install).

## Verification

- [x] bundle exec rspec passes (780 examples, 94.82% line coverage)
- [x] bundle exec rubocop passes (no offenses)
- [x] end-to-end drive of the real generator: stamp on install, gap-detected
      migration on sync, restamp, idempotent second sync

Refs #41

Claude-Session: https://claude.ai/code/session_01FPQb6z3YwcKRMbvoJhdxnX
@mhenrixon
mhenrixon merged commit 5027b46 into main Jul 10, 2026
5 checks passed
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.

post-1.0: version-aware migrations between docs-kit releases in docs_kit:install --sync

1 participant