๐ฎ Kettle::Jem
if ci_badges.map(&:color).detect { it != "green"} โ๏ธ let me know on Discord or RubyForum, as I may have missed the notification.
if ci_badges.map(&:color).all? { it == "green"} ๐๏ธ send money so I can do more of this. FLOSS maintenance is now my full-time job.
๐ฃ How will this project approach the September 2025 hostile takeover of RubyGems? ๐๏ธ
I've summarized my thoughts in this blog post.
๐ป Synopsis

Kettle::Jem is an AST-aware gem templating system that keeps hundreds of Ruby gems in sync with a shared template while preserving each project's customizations. Unlike line-based copy/merge tools, Kettle::Jem understands the structure of every file it touches โ Ruby via Prism, YAML via Psych, Markdown via Markly, TOML via tree-sitter, and more โ so template updates land precisely where they belong, and project-specific additions are never clobbered.
Plugin authors can now use the dedicated plugin authoring guide
to build kettle-jem extension gems against the supported plugin seam.
Key Features
- AST-aware merging โ 10 format-specific merge engines (prism, psych, markly, toml, json, jsonc, json5, bash, dotenv, rbs, text)
- Token substitution โ
{KJ|TOKEN}patterns resolved from config, ENV, or auto-derived from gemspec - Freeze blocks โ protect any section from template overwrites with
# kettle-jem:freeze/# kettle-jem:unfreeze - Per-file strategies โ
merge,accept_template,keep_destination, orraw_copy - Multi-phase pipeline โ 11 ordered phases (service_actor-based) from config sync through duplicate checking
- SHA-pinned GitHub Actions โ template
uses:always wins, propagating immutable SHAs - Convergence in one pass โ a single
kettle-jem installapplies all changes; a second run produces zero diff - Selftest divergence check โ CI verifies that project drift stays within a configurable threshold
๐ก Info you can shake a stick at
| Tokens to Remember | |
|---|---|
| Works with MRI Ruby 4 | |
| Support & Community | |
| Source | |
| Documentation | |
| Compliance | |
| Style | |
| Maintainer ๐๏ธ | |
... ๐ |
Compatibility
Compatible with MRI Ruby 4.0.0+, and concordant releases of JRuby, and TruffleRuby.
CI workflows and Appraisals are generated for MRI Ruby 4.0.0+.
This test floor is configured by ruby.test_minimum in .kettle-jem.yml and
may be higher than the gem's runtime compatibility floor when legacy Rubies are
not practical for the current toolchain.
The amazing test matrix is powered by the kettle-dev stack.
How kettle-dev manages complexity in tests
| Gem | Source | Role | Total downloads |
|---|---|---|---|
| appraisal2 | GitHub | multi-dependency Appraisal matrix generation | |
| appraisal2-rubocop | GitHub | RuboCop Appraisal generator integration | |
| kettle-dev | GitHub | development, release, and CI workflow tooling | |
| kettle-soup-cover | GitHub | SimpleCov coverage policy and reporting | |
| kettle-test | GitHub | standard test runner and coverage harness | |
| rubocop-lts | GitHub | Ruby-version-aware linting | |
| turbo_tests2 | GitHub | parallel test execution |
StructuredMerge package family
This gem is part of the StructuredMerge Ruby package family. The implementation inventory, layering model, and backend notes live in the root package-family guide. Shared behavior is defined by the StructuredMerge fixtures and implemented by the Go, Ruby, Rust, and TypeScript repositories.
Merge analysis must enter parsing through tree_haver. Parser-specific gems register concrete TreeHaver backends; substrate gems register grammar mappings and keep shared format or language merge behavior in one place. Missing backends fail closed instead of falling back to direct parser-library calls.
Federated DVCS
Find this repo on federated forges (Coming soon!)
| Federated DVCS Repository | Status | Issues | PRs | Wiki | CI |
|---|---|---|---|---|---|
| ๐งช structuredmerge/structuredmerge-ruby on GitLab | The Truth | ๐ | ๐ | ๐ | ๐ญ Tiny Matrix |
| ๐ง structuredmerge/structuredmerge-ruby on CodeBerg | An Ethical Mirror (Donate) | ๐ | ๐ | โ | โญ๏ธ No Matrix |
| ๐ structuredmerge/structuredmerge-ruby on GitHub | Another Mirror | ๐ | ๐ | ๐ | ๐ฏ Full Matrix |
Enterprise Support 
Available as part of the Tidelift Subscription.
Need enterprise-level guarantees?
The maintainers of this and thousands of other packages are working with Tidelift to deliver commercial support and maintenance for the open source packages you use to build your applications. Save time, reduce risk, and improve code health, while paying the maintainers of the exact packages you use.
- ๐กSubscribe for support guarantees covering all your FLOSS dependencies
- ๐กTidelift is part of Sonar
- ๐กTidelift pays maintainers to maintain the software you depend on!
๐@Pointy Haired Boss: An enterprise support subscription is "never gonna let you down", and supports open source maintainers
Alternatively:
โจ Installation
Install the gem and add to the application's Gemfile by executing:
bundle add kettle-jem
If bundler is not being used to manage dependencies, install the gem by executing:
gem install kettle-jem
โ๏ธ Configuration
Each gem that uses Kettle::Jem has a .kettle-jem.yml file at its root. This file controls
every aspect of how the template is applied.
Minimal Configuration
project_emoji: "๐ฎ"
engines:
- ruby
licenses:
- MIT
tokens:
forge:
gh_user: "your-username"
author:
name: "Your Name"
email: "you@example.com"
Full Configuration Reference
# REQUIRED โ unique emoji used in badges and gemspec summary
project_emoji: "๐ฎ" # ENV override: KJ_PROJECT_EMOJI
# Ruby engines to include in CI matrix (remove to skip)
engines:
- ruby
- jruby
- truffleruby
# The generated direct dependency-heads workflow derives its Bundler lockfile
# platform from `engines`. JRuby gets an isolated temporary lockfile with the
# `universal-java` platform; MRI and TruffleRuby use the checked-in lock
# context. This avoids making one shared lockfile satisfy incompatible engine
# platforms.
# SPDX license identifiers
licenses:
- MIT
# Logo layout in README header: org | project | org_and_project
readme:
top_logo_mode: org
# Bot accounts to exclude from contributor lists
machine_users:
- dependabot
# Maximum allowed divergence (%) for selftest CI check
min_divergence_threshold: 5 # ENV override: KJ_MIN_DIVERGENCE_THRESHOLD
# Default merge behavior applied to all files
defaults:
preference: "template" # template | destination
add_template_only_nodes: true # add nodes that only exist in template
freeze_token: "kettle-jem" # marker for frozen sections
# Token values for {KJ|TOKEN} substitution
tokens:
forge:
gh_user: "github-username" # ENV override: KJ_GH_USER
gl_user: "gitlab-username" # ENV override: KJ_GL_USER
cb_user: "codeberg-username" # ENV override: KJ_CB_USER
sh_user: "sourcehut-user" # ENV override: KJ_SH_USER
author:
name: "Full Name" # ENV override: KJ_AUTHOR_NAME
given_names: "Full" # ENV override: KJ_AUTHOR_GIVEN_NAMES
family_names: "Name" # ENV override: KJ_AUTHOR_FAMILY_NAMES
email: "you@example.com" # ENV override: KJ_AUTHOR_EMAIL
domain: "example.com" # ENV override: KJ_AUTHOR_DOMAIN
orcid: "0000-0000-0000-0000" # ENV override: KJ_AUTHOR_ORCID
funding:
kofi: "username" # ENV override: KJ_FUNDING_KOFI
paypal: "username" # ENV override: KJ_FUNDING_PAYPAL
buymeacoffee: "username" # ENV override: KJ_FUNDING_BUYMEACOFFEE
liberapay: "username" # ENV override: KJ_FUNDING_LIBERAPAY
social:
mastodon: "username" # ENV override: KJ_SOCIAL_MASTODON
bluesky: "user.bsky.social" # ENV override: KJ_SOCIAL_BLUESKY
linktree: "username" # ENV override: KJ_SOCIAL_LINKTREE
devto: "username" # ENV override: KJ_SOCIAL_DEVTO
# Glob-based overrides (first match wins)
patterns:
- path: "certs/**"
strategy: raw_copy
# Per-file overrides
files:
Rakefile:
strategy: merge
preference: destination # preserve local tasks
AGENTS.md:
strategy: accept_template # always use template version
Framework Matrix vs. Appraisals
workflows.preset: framework and workflows.framework_matrix are meant for a
simple 2D matrix: Ruby versions ร one framework gem/version axis. This is a
good fit when you want kettle-jem to generate framework-version modular gemfiles,
Appraisals entries, and CI matrix entries for a single framework dependency.
If you need a deeper or more complex matrix, prefer
kettle-jem-appraisals, which generates Appraisals entries and is the
better fit for Appraisals-style combinations.
Strategies
| Strategy | Behavior |
|---|---|
merge |
Resolve tokens, then AST-merge template + destination (default) |
accept_template |
Resolve tokens, overwrite destination with template result |
keep_destination |
Skip entirely โ no merge, no creation |
raw_copy |
Copy bytes as-is โ no token resolution, no merge (for binary assets) |
raw_copy exists for bootstrap files that may be needed before the full
templating stack is available. Because it bypasses normal template processing,
do not use it for templates that contain {KJ|...} tokens or require
StructuredMerge normalization.
Token Substitution
Tokens use {KJ|TOKEN} syntax and are resolved in priority order:
- ENV variables (highest) โ e.g.,
KJ_AUTHOR_NAME .kettle-jem.ymltokens:section โ explicit values- Auto-derived from gemspec (lowest) โ author name, email, domain
Common tokens:
| Token | Source | |
|---|---|---|
| ` | GEM_NAME` | Gem name from gemspec |
| ` | NAMESPACE` | Ruby module namespace |
| ` | AUTHOR:NAME` | Author full name |
| ` | AUTHOR:EMAIL` | Author email |
| ` | GH:USER` | GitHub username |
| ` | PROJECT_EMOJI` | Project emoji from config |
| ` | MIN_RUBY` | Minimum Ruby version |
| ` | FREEZE_TOKEN` | Freeze marker name |
Freeze Blocks
Protect sections in any file from template overwrites:
# kettle-jem:freeze
gem "my-local-fork", path: "../custom"
# kettle-jem:unfreeze
Content between freeze/unfreeze markers is always preserved from the destination, regardless of what the template contains. Works in all supported formats (Ruby, YAML, Markdown, TOML, JSON, Bash, etc.).
Merge Engine Selection
Kettle::Jem selects the merge engine by file type:
| File Pattern | Merge Engine | Key Behaviors |
|---|---|---|
*.rb, Gemfile, *.gemspec, Rakefile, Appraisals |
Prism::Merge | Three-phase matching, gemspec var renaming |
*.yml, *.yaml |
Psych::Merge | SHA-pinned uses:, per-key preferences |
*.md, *.markdown |
Markly::Merge | Heading/list matching, inner list merge |
*.toml |
Toml::Merge | Sort keys, table matching |
*.json |
Json::Merge | Key-based matching |
*.jsonc |
Json::Merge | With comment preservation |
*.json5 |
Json::Merge | JSON5 keys, strings, comments, and trailing commas |
*.sh, *.bash, .envrc |
Bash::Merge | Block matching |
.env* |
Dotenv::Merge | KEY=value matching |
*.rbs |
RBS::Merge | Type signature matching |
.gitignore |
Text::Merge | Intentional line-based merge |
No silent fallback: If a tree-sitter grammar is unavailable for a file type that requires AST merging, kettle-jem will fail (default) or skip the file โ never silently degrade to text-based merging. See
PARSE_ERROR_MODEbelow.
๐ง Basic Usage
Initial Setup
gem install kettle-jem
cd my-gem
kettle-jem
The setup CLI runs a two-phase bootstrap:
- Bootstrap โ creates
.kettle-jem.yml, installs modular gemfiles, ensures dev dependencies - Bundled โ loads the full runtime and runs
kettle-jem install
Applying Template Updates
After initial setup, re-run the template process to pull in updates:
K_JEM_TEMPLATING=true bundle exec kettle-jem install
This applies all template phases, then runs the local finishing steps such as
bin/setup, curated binstub generation, hooks, and lockfile normalization.
| Phase | Description | Files Affected |
|---|---|---|
| 0 | Config sync | .kettle-jem.yml |
| 1 | Dev container | .devcontainer/ |
| 2 | GitHub workflows | .github/workflows/, FUNDING.yml |
| 3 | Quality config | .qlty/qlty.toml |
| 4 | Modular gemfiles | gemfiles/modular/ |
| 5 | Spec helper | spec/spec_helper.rb |
| 6 | Environment templates | .env.local.example |
| 7 | Remaining files | gemspec, README, LICENSE, Rakefile, โฆ |
| 8 | Git hooks | .git-hooks/ |
| 9 | License files | LICENSE* |
| 10 | Duplicate check | (validation only) |
Each phase is implemented as a composable service_actor actor, enabling per-phase statistics (๐ templates, ๐ created, ๐ pre-existing, ๐ฐ identical, โ๏ธ changed) and future slice-based workflows.
Checking Divergence
CI can verify that a project hasn't drifted too far from the template:
bundle exec rake kettle:jem:selftest
This re-applies the template in a temporary checkout and measures the diff. Output is condensed to two summary lines after the template run:
[selftest] ๐ Report - tmp/template_test/report/summary.md
[selftest] โ
Score: 100.0% ยท Divergence: 0.0% ยท Threshold: fail when divergence reaches 5.0%
If divergence exceeds min_divergence_threshold (default 5%), the check fails.
Workflow-Specific Options
For GitHub Actions workflows, the template always wins for uses: lines
(SHA-pinned action references) while destination wins for job configuration:
# Template updates this SHA automatically:
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0
# Your matrix customizations are preserved:
matrix:
ruby: ["3.2", "3.3", "3.4"]
Per-File Overrides
Override merge behavior for specific files in .kettle-jem.yml:
files:
Rakefile:
strategy: merge
preference: destination # keep your custom tasks
certs/my.pem:
strategy: raw_copy # binary file, no merging
generated/report.md:
strategy: keep_destination # never touch this file
Environment Variables & CLI Options
Kettle::Jem behavior is controlled via environment variables (which double as
Rake task arguments) and CLI flags passed to kettle-jem setup.
Merge & Error Handling
| Variable | CLI Flag | Default | Description |
|---|---|---|---|
FAILURE_MODE |
--failure-mode=VAL |
error |
How general merge failures are handled. error raises and halts; rescue logs a warning and uses unmerged content. |
PARSE_ERROR_MODE |
โ | fail |
How AST parser unavailability is handled. fail raises immediately (recommended); skip warns and preserves the destination file unchanged. There is no text-merge fallback โ AST merge or nothing. |
Task Control
| Variable | CLI Flag | Default | Description |
|---|---|---|---|
allowed |
--allowed=VAL |
true |
Set to false/0/no to require manual review of env file changes before continuing. |
| โ | --interactive |
(off) | Enable interactive prompts (opt-in). Overrides the default non-interactive behavior. |
KETTLE_JEM_VERBOSE |
--verbose |
false |
Show detailed output including per-file messages and setup progress. Overrides the default quiet behavior. |
only |
--only=VAL |
(all) | Comma-separated glob patterns โ only template files matching at least one pattern are processed. |
include |
--include=VAL |
(all) | Comma-separated glob patterns โ additional files to include beyond the default set. |
hook_templates |
--hook_templates=VAL |
(prompt) | Git hook install location: l/local, g/global, or n/none. Also via KETTLE_DEV_HOOK_TEMPLATES. |
KETTLE_JEM_CHECKSUMS |
--checksums=VAL |
template,ignore-dest |
Checksum skip mode. See "Checksum Skip Modes" below. |
| โ | --ignore-checksums |
(off) | Alias for --checksums=off; disables checksum-based skipping. |
Checksum Skip Modes
Kettle::Jem writes managed template state to
.structuredmerge/kettle-jem.lock. The legacy whole-template checksum
inventory now lives under template_state.checksums.
That inventory is useful for coarse template drift state, but it is not used to
skip individual files. File skipping uses files.*.input_fingerprint, a
per-destination digest that includes the selected template source SHA, recipe
identity/version, resolved token digest, and kettle-jem implementation SHA.
Token values are hashed into the fingerprint and are not written to the lockfile.
--checksums accepts comma-separated modes:
| Mode | Behavior |
|---|---|
template |
Skip a destination file only when its input_fingerprint matches the lock. This is the default template-input check. |
ignore-dest |
Do not compare the destination file checksum. This is the default destination behavior. |
dest |
Also require the destination file checksum to match the lock before skipping. dest implies template unless ignore-template is present. |
ignore-template |
Do not compare input_fingerprint; useful only with dest when local destination state should be the only skip gate. |
off |
Disable checksum-based skipping. This cannot be combined with other modes. |
Common combinations:
| Value | Result |
|---|---|
template,ignore-dest |
Default. Retemplate when relevant template input changes; ignore local destination edits. |
dest,template |
Retemplate when relevant template input changes or the destination differs from the prior templated result. |
dest,ignore-template |
Skip only when the destination checksum matches; ignore template input changes. |
off |
Run normal templating without checksum skip shortcuts. |
Config & Identity (KJ_ prefix)
These map directly to .kettle-jem.yml keys, seed freshly created configs,
fill missing keys during config sync, and act as runtime overrides.
| Variable | Description |
|---|---|
KJ_PROJECT_EMOJI |
Project identifying emoji (e.g. ๐ช). Required in config. |
KJ_MIN_DIVERGENCE_THRESHOLD |
Selftest divergence threshold for min_divergence_threshold. |
KJ_AUTHOR_NAME |
Gem author full name |
KJ_AUTHOR_EMAIL |
Gem author email |
KJ_AUTHOR_DOMAIN |
Author website domain (derived from email if unset) |
KJ_AUTHOR_GIVEN_NAMES |
First/given names |
KJ_AUTHOR_FAMILY_NAMES |
Last/family names |
KJ_AUTHOR_ORCID |
ORCID identifier |
FORGE_ORG |
Preferred forge organization or owner used for scaffolded repository URLs when no forge remote is available |
KJ_GH_ORG |
GitHub-specific organization or owner fallback for scaffolded repository URLs |
KJ_GH_USER |
GitHub username used for maintainer profile and sponsor links |
KJ_GL_USER |
GitLab username |
KJ_CB_USER |
Codeberg username |
KJ_SH_USER |
SourceHut username |
Workspace & Funding
| Variable | Description |
|---|---|
KETTLE_DEV_DEV |
Workspace root for local sibling gems. true = ~/src/my; a path = that path; unset/false = released gems. |
KETTLE_DEV_DEBUG |
Set to true for verbose debug output. |
FUNDING_ORG |
OpenCollective organization handle for FUNDING.yml. Auto-derived from git remote if unset. |
OPENCOLLECTIVE_HANDLE |
Alternative to FUNDING_ORG for personal OpenCollective pages. |
KJ_FUNDING_KOFI |
Ko-fi handle for FUNDING.yml |
KJ_FUNDING_PAYPAL |
PayPal handle for FUNDING.yml |
KJ_FUNDING_BUYMEACOFFEE |
Buy Me a Coffee handle for funding links |
KJ_FUNDING_LIBERAPAY |
Liberapay handle for funding links |
KJ_SOCIAL_MASTODON |
Mastodon handle for social/profile links |
KJ_SOCIAL_BLUESKY |
Bluesky handle for social/profile links |
KJ_SOCIAL_LINKTREE |
Linktree handle for social/profile links |
KJ_SOCIAL_DEVTO |
DEV Community handle for social/profile links |
Templating Examples
# Standard template update (quiet, non-interactive โ the default)
K_JEM_TEMPLATING=true bundle exec kettle-jem install
# Verbose output
K_JEM_TEMPLATING=true KETTLE_JEM_VERBOSE=true bundle exec kettle-jem install
# Interactive mode (prompts before each change)
K_JEM_TEMPLATING=true bundle exec kettle-jem install --interactive
# Force re-evaluation of every selected template file
K_JEM_TEMPLATING=true bundle exec kettle-jem install --ignore-checksums
# Retemplate when either template inputs or destination checksums changed
K_JEM_TEMPLATING=true bundle exec kettle-jem install --checksums=dest,template
# Only workflow files, skip unparseable. Scoped template runs skip install
# finishing steps and are intended for surgical file updates.
K_JEM_TEMPLATING=true PARSE_ERROR_MODE=skip bundle exec kettle-jem template --only=".github/**"
# Rescue on merge failure (don't halt)
K_JEM_TEMPLATING=true FAILURE_MODE=rescue bundle exec kettle-jem install
The kettle:jem:* rake tasks are internal targets used by the executable after
it prepares the templating environment; call kettle-jem directly for normal
templating work.
๐ Security
See SECURITY.md.
๐ค Contributing
If you need some ideas of where to help, you could work on adding more code coverage, or if it is already ๐ฏ (see below) check issues or PRs, or use the gem and think about how it could be better.
We so if you make changes, remember to update it.
See CONTRIBUTING.md for more detailed instructions.
๐ Release Instructions
See CONTRIBUTING.md.
Code Coverage
Coverage service badges
๐ช Code of Conduct
Everyone interacting with this project's codebases, issue trackers,
chat rooms and mailing lists agrees to follow the .
๐ Contributors
Made with contributors-img.
Also see GitLab Contributors: https://gitlab.com/structuredmerge/structuredmerge-ruby/-/graphs/main
๐ Versioning
This library follows for its public API where practical.
For most applications, prefer the Pessimistic Version Constraint with two digits of precision.
For example:
spec.add_dependency("kettle-jem", "~> 7.0")
๐ Is "Platform Support" part of the public API? More details inside.
Dropping support for a platform can be a breaking change for affected users. If a release changes supported platforms, it should be called out clearly in the changelog and versioned with that impact in mind.
To get a better understanding of how SemVer is intended to work over a project's lifetime, read this article from the creator of SemVer:
See CHANGELOG.md for a list of releases.
๐ License
The gem is available under the following licenses: AGPL-3.0-only, PolyForm-Small-Business-1.0.0. See LICENSE.md for details.
If none of the available licenses suit your use case, please contact us to discuss a custom commercial license.
ยฉ Copyright
See LICENSE.md for the official copyright notice.
Copyright holders
- Required Notice: Copyright (c) 2026 Peter H. Boling
๐ค A request for help
Maintainers have teeth and need to pay their dentists. After getting laid off in an RIF in March, and encountering difficulty finding a new one, I began spending most of my time building open source tools. I'm hoping to be able to pay for my kids' health insurance this month, so if you value the work I am doing, I need your support. Please consider sponsoring me or the project.
To join the community or get help, use the RubyForum or Discord.
To say "thanks!" โ๏ธ Join the community or ๐๏ธ send money.
Please give the project a star โญ โฅ.
Many parts of this project are actively managed by a kettle-jem smart template utilizing StructuredMerge.org merge contracts.
Thanks for RTFM. โบ๏ธ
| Field | Value |
|---|---|
| Package | kettle-jem |
| Description | ๐ฎ Kettle::Jem provides gem scaffolding, templating, and setup automation using the *-merge gem family for AST-based file merging and token-resolver for template token resolution. Includes MergerConfig presets, YAML recipes, and a complete gem template scaffold. |
| Homepage | https://github.com/structuredmerge/structuredmerge-ruby |
| Source | https://github.com/structuredmerge/structuredmerge-ruby |
| License | AGPL-3.0-only OR PolyForm-Small-Business-1.0.0 |
| Funding | https://github.com/sponsors/pboling, https://ko-fi.com/pboling, https://liberapay.com/pboling/donate, https://thanks.dev/u/gh/pboling, https://tidelift.com/funding/github/rubygems/kettle-jem, https://www.buymeacoffee.com/pboling |
