gem_kit

Two gems, split along the line that matters: what a library needs while it is running, and what its maintainer needs while releasing it.

Gem Half Depends on
gem_kit GemKit::Deprecate — declare a deprecation nothing beyond RubyGems
gem_kit-release gem kit … — enforce it at release time gem_kit, thor

A deprecation is a dated promise: it names its replacement and the version the old name stops existing in. The promise is declared at runtime and enforced at release time — which is exactly why these are two gems. A library that deprecates a name should not thereby acquire a release toolchain, and only the enforcing end needs one installed.

# Gemfile
gem "gem_kit"                              # runtime: declaring
gem "gem_kit-release", group: :development # release:  enforcing

Declaring — gem_kit

A method:

class Session
  extend GemKit::Deprecate

  def old_reset = new_reset
  deprecate :old_reset, "Session#new_reset", "5.0"
end

A renamed or moved constant — leave the old name as a subclass of the new one:

class Completion < New::Completion
  extend GemKit::Deprecate
  superseded_by "New::Completion", "5.0"
end

Both keep working, warn on use naming the caller, and register the deadline:

NOTE: Session#old_reset is deprecated; use Session#new_reset instead.
It will be removed in 5.0
Session#old_reset called from app.rb:12.

Use :none when there genuinely is no replacement. Gem::Deprecate.skip_during silences the warnings, so a suite can exercise the old path in quiet.

Enforcing — gem_kit-release

Installing it registers one gem command. There is nothing to wire up — no Rakefile, no binstubs:

gem kit setup                 # write DEPRECATIONS.md and RELEASE.md into your project
gem kit bump minor            # move the version
gem kit changelog --write     # have an AI CLI write the entry
gem kit changelog             # lint it
gem kit deprecations          # what is still outstanding
gem kit release --dry-run     # run the gates
gem kit tag --push            # tag it
Command What it does
gem kit setup Writes DEPRECATIONS.md and RELEASE.md, rendered for your gem's name and versions. A generator, so --force, --skip and --pretend all work.
`gem kit bump <major\ minor\ patch>` Rewrites the version file. Refuses to bump onto a deprecation deadline; --force overrides.
gem kit changelog [VERSION] Lints CHANGELOG.md. With a version, checks that version is ready to release.
gem kit changelog --write Hands the entry to the configured AI CLI.
gem kit deprecations [VERSION] Lists what is outstanding. With a version, fails if any come due — a CI gate.
gem kit release [--dry-run] Gates, then builds and pushes.
gem kit tag [--push] Tags v<version>, refusing if it exists.

Everything sits behind one gem command rather than six, so nothing here can collide with a command RubyGems ships now or adds later — gem check, gem build, gem push and gem setup all already exist.

The command line is Thor, so gem kit lists the commands and gem kit help <command> prints one command's arguments and options. gem kit setup is a Thor::Group generator — the same machinery behind rails generate — which is where its create / identical / conflict reporting and its --force, --skip and --pretend come from.

Everything is read out of your .gemspec. Override only what cannot be inferred:

GemKit::Release.configure do |config|
  config.changelog        = "HISTORY.md"   # default: CHANGELOG.md
  config.version_file     = "lib/x.rb"     # default: lib/<name>/version.rb
  config.require_path     = "x"            # default: <name>, hyphens as slashes
  config.test_command     = "bin/test"
  config.changelog_writer = "claude"       # the CLI that writes the entry
end

In a repository with more than one gemspec

Like this one. Every command takes --gem, and refuses to guess without it:

$ gem kit changelog
2 gemspecs in /home/you/src/gem_kit (gem_kit-release, gem_kit); name one with --gem

$ gem kit changelog --gem gem_kit
CHANGELOG-gem_kit.md is clean.

Three things follow from a repository holding several gems, and gem kit arranges all three itself:

One gem Several
Changelog CHANGELOG.md CHANGELOG-<gem>.md
Release document RELEASE.md RELEASE-<gem>.md
Tag v1.2.3 <gem>-v1.2.3

The changelog has to be per-gem: the release gate asks whether the version being cut is the topmost released section, and two gems interleaved in one file means the older one never is — so the second gem could never be released. The tag has to be per-gem because v1.2.3 says which version but not which gem, which is fine until two of them are at the same one. DEPRECATIONS.md is written once for the repository, because one deprecation policy governs everything in it.

Extending it

gem kit takes commands from other gems. GemKit::Release.plugin is the seam:

# lib/gem_kit/plugin.rb, in a gem of your own
require "gem_kit/release/cli"

GemKit::Release.plugin do
  desc "lint", "Check this gem for the things gems get wrong"
  def lint = GemKit::Plugin::Lint.new(options).call
end

The block is evaluated on the Thor class, so the whole Thor DSL is in scope — desc, long_desc, method_option, map, and register for a Thor::Group generator. A command added this way is indistinguishable from a built-in one: it appears in gem kit, takes --gem, and gets a help page.

Ship a lib/rubygems_plugin.rb that requires your file and RubyGems loads it on every gem invocation, the same way this gem is loaded. See gem_kit-plugin for a worked example.

Layout

lib/gem_kit.rb              gem_kit
lib/gem_kit/deprecate.rb
lib/gem_kit/release.rb      gem_kit-release
lib/gem_kit/release/
lib/rubygems_plugin.rb
template/                   the gem template this repository used to be

One Gemfile, one Gemfile.lock, one gemset.nix, one flake.nix and one lefthook.yml cover both gems. The dependencies are listed in the Gemfile outright rather than through gemspec, which is the only sane answer when two gemspecs share one bundle — and, separately, what bundlerEnv needs, since it resolves against a store directory holding only a Gemfile and a lockfile.

Each gemspec names its own files rather than globbing lib/**/*.rb, because a glob would put the whole release toolchain inside the runtime gem.

Template

template/ holds the gem template this repository started as: clone it, run bin/01-rename-gem, and you have a gem.

Development

direnv allow      # or: nix develop
bin/test

License

MIT