kube_schema 1.11.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/CHANGELOG.md CHANGED
@@ -4,7 +4,132 @@ All notable changes to this project are documented here.
4
4
 
5
5
  This file starts at 1.10.0; for earlier releases see the git history.
6
6
 
7
- ## [1.11.0]
7
+ ## [Unreleased]
8
+
9
+ ## [2.0.0] - 2026-10-17
10
+
11
+ ### Added
12
+
13
+ - `require "kube_schema"` now works alongside `require "kube/schema"`.
14
+ `lib/kube_schema.rb` is a new entry point that loads the library;
15
+ `Kube::Schema::VERSION` is unchanged.
16
+
17
+ - `Kube::Schema::Lazy` — `Scanner` (byte-offset index over the schema files),
18
+ `Index`/`Entry`, `Spill` (temp-file backing for registered schemas), and
19
+ `Source`, which stands in for a compiled schema: it answers `#value`,
20
+ `#valid?`, `#validate` and `#ref`, loading the definition for property names
21
+ and compiling behind a `WeakRef` so repeated validations reuse a live schema
22
+ while the GC can still reclaim it.
23
+
24
+ - `Kube::MissingSchemaError`, raised when a definition cannot be read at
25
+ validation time. A vanished spilled temp file says so explicitly, since that
26
+ means the `Tempfile` was collected or the tmpdir swept, rather than the
27
+ install being broken.
28
+
29
+ ### Changed
30
+
31
+ - **Schemas are compiled per definition, on demand, and collected afterwards.**
32
+ `Instance#schemer` used to parse `v<version>.json`, merge every
33
+ `*-definitions.json` into it, and hand the whole document to
34
+ `JSONSchemer.schema` — one compiled tree held at the class level for the
35
+ life of the process. That tree was **97,381 `JSONSchemer::Schema` nodes and
36
+ ~406 MB resident** (one `JSONSchemer::Configuration` per node, plus ~230k
37
+ vocabulary keyword objects and 1.2M hashes), built in full whether you
38
+ validated one resource or none.
39
+
40
+ Validating a Deployment through the public API now looks like this:
41
+
42
+ baseline rss= 26.8 MB schemas= 0
43
+ Instance.new("1.33") rss= 26.8 MB schemas= 0
44
+ instance["Deployment"] rss= 68.5 MB schemas= 0
45
+ Deployment.new(...) rss= 68.5 MB schemas= 0
46
+ after valid? rss= 68.5 MB schemas= 147
47
+ after GC.start rss= 42.4 MB schemas= 16
48
+
49
+ Nothing is compiled until something is validated, a Deployment needs 147
50
+ schema nodes rather than 97,381, and they are collectable the moment the
51
+ validation is done — about **90% less memory** than before.
52
+
53
+ This works because `json_schemer` resolves `$ref` lazily:
54
+ `Draft202012::Vocab::Core::Ref#ref_schema` is memoized inside `#validate`,
55
+ and an unresolved URI goes through `ref_resolver`. So a single definition is
56
+ compiled as its own root, its `#/definitions/...` pointers are rewritten to
57
+ `kube:///<key>`, and a resolver loads referenced definitions on demand —
58
+ which means the cost scales with the instance being validated, not with the
59
+ size of the schema document.
60
+
61
+ - **`Kube::Schema.register` no longer compiles the registered schema.** It
62
+ writes the definition to a temp file and stores an index entry, so a
63
+ registered CRD takes the same lazy path as a built-in type. Registering the
64
+ largest bundled CRD (1.1 MB of `io.argoproj.v1alpha1.Workflow`) now compiles
65
+ nothing at registration time; its 9,656 nodes appear on first `#valid?` and
66
+ collect afterwards. `reset_custom_schemas!` removes the temp files.
67
+
68
+ - `Instance.schemers` is now `Instance.catalogs`, holding a
69
+ `Kube::Schema::Lazy::Index` (definition key to file offset, group/version/kind,
70
+ and property names) instead of a compiled schema. The index is built by
71
+ scanning the JSON for definition boundaries, so the schema documents are
72
+ never parsed whole and nothing but offsets and small metadata is retained.
73
+ Callers that reached into `Instance.schemers` must update to `Instance.catalogs`
74
+ and expect a `Lazy::Index`, not a `JSONSchemer::Schema`.
75
+
76
+ - **The version constant file moved to `lib/kube_schema/version.rb`** from
77
+ `lib/kube/schema/version.rb`. `gem kit bump` derives the path from the gem's
78
+ name and rewrites the literal in place, so the `version.rb.erb` template is
79
+ gone. External code that `require`d `kube/schema/version` directly must now
80
+ `require "kube_schema/version"`; the `Kube::Schema::VERSION` constant itself
81
+ is unchanged.
82
+
83
+ - **Releases run on `gem kit`, not on `bin/*`.** `bin/increment-version`,
84
+ `bin/release-gem` and `bin/tag-version` are gone, replaced by the
85
+ [gem_kit-release](https://github.com/n-at-han-k/gem_kit) RubyGems plugin:
86
+ `gem kit bump`, `gem kit changelog`, `gem kit release` and `gem kit tag`.
87
+ The three scripts between them bumped the version, compared it against
88
+ RubyGems, built, pushed and tagged. What they never did was check that the
89
+ release was documented or that promises made about it had been kept, and
90
+ `gem kit` refuses to build without both — see [RELEASE.md](RELEASE.md) and
91
+ [DEPRECATIONS.md](DEPRECATIONS.md), which are now in the repository.
92
+
93
+ `gem_kit` is a new runtime dependency, so a deprecation can be *declared*
94
+ (`GemKit::Deprecate`) where the deprecated name lives; it has no
95
+ dependencies of its own beyond RubyGems'. `gem_kit-release`, which does the
96
+ enforcing, is in the `release` bundle group and is nobody's dependency.
97
+
98
+ - **Tests run on [scampi](https://github.com/general-intelligence-systems/scampi)
99
+ instead of RSpec.** The co-located specs moved out of
100
+ `if __FILE__ == $0` blocks and into each file's `__END__` section, so they no
101
+ longer load in production — Ruby stops parsing at `__END__`. `bin/test` is now
102
+ `bundle exec scampi`, which discovers every `.rb` carrying an `__END__` spec
103
+ section and emits TAP version 14. The assertions are scampi's `should` chain
104
+ rather than `expect`, and the devshell gained `ripgrep`, which scampi shells
105
+ out to for discovery.
106
+
107
+ ### Removed
108
+
109
+ - `bin/increment-version`, `bin/release-gem` and `bin/tag-version`. Use
110
+ `gem kit bump`, `gem kit release` and `gem kit tag` instead.
111
+ - `lib/kube/schema/version.rb.erb`. `gem kit bump` edits the version literal
112
+ in `lib/kube_schema/version.rb` directly.
113
+
114
+ ## [1.12.0] - 2026-09-28
115
+
116
+ ### Added
117
+
118
+ - Ruby pattern matching support. `Resource#deconstruct_keys` exposes `to_h`,
119
+ so resources match against hash patterns all the way down; `Manifest#deconstruct`
120
+ exposes the resource array, so manifests match against array patterns.
121
+
122
+ resource => {metadata: {name:}}
123
+
124
+ Deployment = Kube::Schema["Deployment"]
125
+ manifest => [Deployment => dep, *rest]
126
+
127
+ Note that `Kube::Schema["Deployment"]` cannot be written inline in a pattern:
128
+ Ruby parses `Const[...]` as array-pattern syntax, not a method call. Assign
129
+ the class to a constant, or use a pin expression —
130
+ `^(Kube::Schema["Deployment"])`.
131
+
132
+ ## [1.11.0] - 2026-09-17
8
133
 
9
134
  ### Added
10
135
 
@@ -15,7 +140,7 @@ This file starts at 1.10.0; for earlier releases see the git history.
15
140
  from core resource/subresource strings — e.g. building RBAC rules — without
16
141
  hardcoding a group list.
17
142
 
18
- ## [1.10.0]
143
+ ## [1.10.0] - 2026-08-03
19
144
 
20
145
  ### Added
21
146
 
data/DEPRECATIONS.md ADDED
@@ -0,0 +1,172 @@
1
+ # Deprecations
2
+
3
+ A deprecation in kube_schema is a **dated promise**: it names the replacement
4
+ *and* the version the old name stops existing in. That promise is
5
+ machine-readable — every declaration registers itself, and the release tooling
6
+ refuses to ship a version that breaks one.
7
+
8
+ The mechanism is [`GemKit::Deprecate`](https://rubygems.org/gems/gem_kit),
9
+ built on [`Gem::Deprecate`](https://docs.ruby-lang.org/en/master/Gem/Deprecate.html).
10
+
11
+ ## The rules
12
+
13
+ 1. **Never delete a public name outright.** Leave it working, deprecated, until
14
+ its deadline.
15
+ 2. **Every deprecation names a removal version.** The current version is 2.0.0, so the usual deadline is 3.0.
16
+ 3. **Removals happen in major versions only.** A minor or patch release never
17
+ takes a name away.
18
+ 4. **The deadline is enforced, not remembered.** `gem kit bump` and `gem kit release`
19
+ both refuse to move to a version that has a deprecation coming due.
20
+ 5. **Deprecating something is a changelog entry** — under `### Deprecated`,
21
+ naming the replacement and the removal version.
22
+
23
+ ## Deprecating a method
24
+
25
+ ```ruby
26
+ class Session
27
+ extend GemKit::Deprecate
28
+
29
+ def new_reset
30
+ # ...
31
+ end
32
+
33
+ def old_reset = new_reset
34
+
35
+ deprecate :old_reset, "Session#new_reset", "3.0"
36
+ end
37
+ ```
38
+
39
+ The old method keeps working and warns on every call, naming the caller:
40
+
41
+ ```
42
+ NOTE: Session#old_reset is deprecated; use Session#new_reset instead.
43
+ It will be removed in 3.0
44
+ Session#old_reset called from app.rb:12.
45
+ ```
46
+
47
+ Use `:none` as the replacement when there genuinely isn't one:
48
+
49
+ ```ruby
50
+ deprecate :old_reset, :none, "3.0"
51
+ ```
52
+
53
+ For a class method, follow the `Gem::Deprecate` idiom — the registry records it
54
+ against the class, not its singleton:
55
+
56
+ ```ruby
57
+ class << self
58
+ extend GemKit::Deprecate
59
+ deprecate :some_class_method, "Other.method", "3.0"
60
+ end
61
+ ```
62
+
63
+ ## Deprecating a renamed or moved constant
64
+
65
+ Keep the old constant as a subclass of the new one and declare the rename in
66
+ its body:
67
+
68
+ ```ruby
69
+ module Old
70
+ class Thing < New::Thing
71
+ extend GemKit::Deprecate
72
+ superseded_by "New::Thing", "3.0"
73
+ end
74
+ end
75
+ ```
76
+
77
+ Old code keeps running unchanged; instantiating the old name warns and points at
78
+ the new one. The whole shim is those four lines — the implementation lives in
79
+ one place.
80
+
81
+ It is `superseded_by` rather than `deprecate_constant` because `Module` already
82
+ has a method by that name, and shadowing it would break callers who use it.
83
+
84
+ ## Finding what is outstanding
85
+
86
+ ```sh
87
+ gem kit deprecations
88
+ ```
89
+
90
+ ```
91
+ 1 outstanding deprecation(s) (current version 2.0.0):
92
+ 3.0 Session#old_reset -> Session#new_reset
93
+ lib/session.rb:19
94
+ ```
95
+
96
+ Pass a version to ask "what comes due here?" — it exits non-zero if anything
97
+ does, which is what makes it usable as a gate in CI:
98
+
99
+ ```sh
100
+ gem kit deprecations 3.0.0
101
+ ```
102
+
103
+ Programmatically, the same data:
104
+
105
+ ```ruby
106
+ GemKit::Deprecate.registry # every declaration
107
+ GemKit::Deprecate.pending("3.0.0") # deadlines that have arrived
108
+ GemKit::Deprecate.upcoming("3.0.0") # still in their grace period
109
+ ```
110
+
111
+ Each entry carries `name`, `replacement`, `removed_in` and `declared_at`.
112
+
113
+ ## Paying the debt
114
+
115
+ When a major version comes around, the bump is blocked until the deprecated
116
+ code is actually gone:
117
+
118
+ ```
119
+ $ gem kit bump major
120
+ ERROR: Refusing to bump 2.0.0 -> 3.0.0:
121
+
122
+ 3.0 Session#old_reset -> Session#new_reset
123
+ lib/session.rb:19
124
+
125
+ Remove them, then bump. Override with --force.
126
+ ```
127
+
128
+ So the order of work is:
129
+
130
+ 1. `gem kit deprecations 3.0.0` — read the list.
131
+ 2. Delete each deprecated name and its specs. For a constant shim, that means
132
+ deleting the whole file.
133
+ 3. Update anything in `examples/` and the docs still using the old name.
134
+ 4. Record the removals in `CHANGELOG.md` under `### Removed`.
135
+ 5. `gem kit bump major` — now it goes through.
136
+
137
+ `--force` exists for the case where you have decided to extend a grace period,
138
+ and it prints what it is overriding. It is not the normal path: extending a
139
+ deadline properly means editing the declaration's version, which keeps the
140
+ registry honest.
141
+
142
+ ## Testing deprecated code
143
+
144
+ `Gem::Deprecate.skip_during` silences these warnings too, so a spec can
145
+ exercise the old path without noise:
146
+
147
+ ```ruby
148
+ Gem::Deprecate.skip_during do
149
+ legacy.old_reset
150
+ end
151
+ ```
152
+
153
+ To assert *that* something warns, stub the single funnel every warning goes
154
+ through:
155
+
156
+ ```ruby
157
+ captured = []
158
+ original = GemKit::Deprecate.method(:warn)
159
+ GemKit::Deprecate.define_singleton_method(:warn) { |message| captured << message }
160
+ begin
161
+ legacy.old_reset
162
+ ensure
163
+ GemKit::Deprecate.define_singleton_method(:warn, original)
164
+ end
165
+ ```
166
+
167
+ ## See also
168
+
169
+ - [RELEASE.md](RELEASE.md) — where the deprecation gates sit in the release
170
+ process.
171
+ - The changelog — its `### Deprecated` and `### Removed` sections are the
172
+ user-facing half of all this.
data/Gemfile CHANGED
@@ -2,11 +2,21 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
 
5
- gemspec
5
+ gem "gem_kit", "~> 0.2"
6
+ gem "json_schemer", "~> 2.5.0"
7
+ gem "rubyshell", "~> 1.5.0"
8
+
9
+ gem "lefthook", "~> 2.1"
10
+ gem "rake", "~> 13.0"
11
+ gem "scampi", "~> 1.0"
12
+ gem "rubocop"
13
+ gem "decode", "~> 0.27"
14
+
15
+ group :release do
16
+ gem "gem_kit-release", "~> 0.3"
17
+ end
6
18
 
7
19
  group :maintenance, optional: true do
8
20
  gem "utopia-project"
9
- gem "bake-gem"
10
21
  gem "bake-modernize"
11
- gem "bake-releases"
12
22
  end
data/Gemfile.lock CHANGED
@@ -1,13 +1,7 @@
1
- PATH
2
- remote: .
3
- specs:
4
- kube_schema (1.11.0)
5
- json_schemer (~> 2.5.0)
6
- rubyshell (~> 1.5.0)
7
-
8
1
  GEM
9
2
  remote: https://rubygems.org/
10
3
  specs:
4
+ ast (2.4.3)
11
5
  async (2.39.0)
12
6
  console (~> 1.29)
13
7
  fiber-annotation
@@ -46,8 +40,6 @@ GEM
46
40
  bake (0.24.1)
47
41
  bigdecimal
48
42
  samovar (~> 2.1)
49
- bake-gem (0.13.1)
50
- console (~> 1.25)
51
43
  bake-modernize (0.54.1)
52
44
  async-http
53
45
  async-ollama (~> 0.10)
@@ -55,9 +47,6 @@ GEM
55
47
  build-files (~> 1.6)
56
48
  markly (~> 0.13)
57
49
  rugged
58
- bake-releases (0.5.4)
59
- bake (~> 0.21)
60
- markly (~> 0.8)
61
50
  bigdecimal (4.1.2)
62
51
  build-files (1.10.2)
63
52
  concurrent-ruby (1.3.6)
@@ -69,7 +58,6 @@ GEM
69
58
  decode (0.27.0)
70
59
  prism
71
60
  rbs
72
- diff-lcs (1.6.2)
73
61
  erb (6.0.4)
74
62
  falcon (0.55.3)
75
63
  async
@@ -88,6 +76,10 @@ GEM
88
76
  fiber-local (1.1.0)
89
77
  fiber-storage
90
78
  fiber-storage (1.0.1)
79
+ gem_kit (0.2.0)
80
+ gem_kit-release (0.3.2)
81
+ gem_kit (>= 0.1, < 1.0)
82
+ thor (~> 1.3)
91
83
  hana (1.3.7)
92
84
  http-accept (2.2.1)
93
85
  io-console (0.8.2)
@@ -105,7 +97,9 @@ GEM
105
97
  hana (~> 1.3)
106
98
  regexp_parser (~> 2.0)
107
99
  simpleidn (~> 0.2)
100
+ language_server-protocol (3.17.0.6)
108
101
  lefthook (2.1.10)
102
+ lint_roller (1.1.0)
109
103
  localhost (1.7.0)
110
104
  logger (1.7.0)
111
105
  mail (2.9.0)
@@ -133,6 +127,10 @@ GEM
133
127
  net-smtp (0.5.1)
134
128
  net-protocol
135
129
  openssl (4.0.1)
130
+ parallel (2.3.0)
131
+ parser (3.3.12.0)
132
+ ast (~> 2.4.1)
133
+ racc
136
134
  pp (0.6.3)
137
135
  prettyprint
138
136
  prettyprint (0.2.0)
@@ -152,11 +150,13 @@ GEM
152
150
  psych (5.3.1)
153
151
  date
154
152
  stringio
153
+ racc (1.8.1)
155
154
  rack (3.2.6)
156
155
  rackula (1.4.1)
157
156
  falcon (~> 0.46)
158
157
  samovar (~> 2.1)
159
158
  variant
159
+ rainbow (3.1.1)
160
160
  rake (13.4.2)
161
161
  rbs (4.0.2)
162
162
  logger
@@ -169,31 +169,38 @@ GEM
169
169
  regexp_parser (2.12.0)
170
170
  reline (0.6.3)
171
171
  io-console (~> 0.5)
172
- rspec (3.13.2)
173
- rspec-core (~> 3.13.0)
174
- rspec-expectations (~> 3.13.0)
175
- rspec-mocks (~> 3.13.0)
176
- rspec-core (3.13.6)
177
- rspec-support (~> 3.13.0)
178
- rspec-expectations (3.13.5)
179
- diff-lcs (>= 1.2.0, < 2.0)
180
- rspec-support (~> 3.13.0)
181
- rspec-mocks (3.13.8)
182
- diff-lcs (>= 1.2.0, < 2.0)
183
- rspec-support (~> 3.13.0)
184
- rspec-support (3.13.7)
172
+ rubocop (1.92.0)
173
+ json (>= 2.3)
174
+ language_server-protocol (~> 3.17.0.2)
175
+ lint_roller (~> 1.1.0)
176
+ parallel (>= 1.10)
177
+ parser (>= 3.3.0.2)
178
+ rainbow (>= 2.2.2, < 4.0)
179
+ regexp_parser (>= 2.11.3, < 3.0)
180
+ rubocop-ast (>= 1.49.0, < 2.0)
181
+ ruby-progressbar (~> 1.7)
182
+ unicode-display_width (>= 2.4.0, < 4.0)
183
+ rubocop-ast (1.50.0)
184
+ parser (>= 3.3.7.2)
185
+ prism (~> 1.7)
186
+ ruby-progressbar (1.13.0)
185
187
  rubyshell (1.5.0)
186
188
  rugged (1.9.0)
187
189
  samovar (2.4.1)
188
190
  console (~> 1.0)
189
191
  mapping (~> 1.0)
192
+ scampi (1.0.0)
190
193
  simpleidn (0.2.3)
191
194
  string-format (0.2.0)
192
195
  stringio (3.2.0)
196
+ thor (1.5.0)
193
197
  thread-local (1.1.0)
194
198
  timeout (0.6.1)
195
199
  traces (0.18.2)
196
200
  tsort (0.2.0)
201
+ unicode-display_width (3.3.0)
202
+ unicode-emoji (~> 4.3)
203
+ unicode-emoji (4.3.0)
197
204
  utopia (2.32.1)
198
205
  bake (~> 0.20)
199
206
  concurrent-ruby (~> 1.2)
@@ -226,14 +233,17 @@ PLATFORMS
226
233
  x86_64-linux
227
234
 
228
235
  DEPENDENCIES
229
- bake-gem
230
236
  bake-modernize
231
- bake-releases
232
- kube_schema!
237
+ decode (~> 0.27)
238
+ gem_kit (~> 0.2)
239
+ gem_kit-release (~> 0.3)
240
+ json_schemer (~> 2.5.0)
233
241
  lefthook (~> 2.1)
234
242
  rake (~> 13.0)
235
- rspec (~> 3.13)
243
+ rubocop
244
+ rubyshell (~> 1.5.0)
245
+ scampi (~> 1.0)
236
246
  utopia-project
237
247
 
238
248
  BUNDLED WITH
239
- 2.6.9
249
+ 2.7.2
data/RELEASE.md ADDED
@@ -0,0 +1,161 @@
1
+ # Releasing kube_schema
2
+
3
+ The whole process, in order:
4
+
5
+ ```sh
6
+ bin/test # 1. green suite
7
+ gem kit bump minor # 2. bump (prints what to run next)
8
+ gem kit changelog --write # 3. write the entry
9
+ gem kit changelog <VERSION> # 4. check it
10
+ git commit -am "Release ..." # 5. commit the bump + changelog
11
+ gem kit release # 6. gate, build, push, tag
12
+ ```
13
+
14
+ Steps 2 and 6 are gates: they refuse to proceed when something is missing.
15
+ Everything below is what they check and why.
16
+
17
+ ## Versioning
18
+
19
+ kube_schema is [semver](https://semver.org/). The version lives in one place —
20
+ `lib/kube_schema/version.rb` — and is only ever changed by `gem kit bump`.
21
+
22
+ | Segment | When | What it may contain |
23
+ | --- | --- | --- |
24
+ | **major** | A public name disappears or changes meaning | Removals of deprecated names, breaking signature changes |
25
+ | **minor** | New public surface, backwards compatible | New classes and methods; new deprecations |
26
+ | **patch** | Nothing new, nothing gone | Bug fixes, docs, internals |
27
+
28
+ Two rules follow from this, and both are enforced in code:
29
+
30
+ - **Removals only ever land in a major version.** A name promised to disappear
31
+ in 3.0 disappears in 3.0.0, not in a patch.
32
+ - **Deprecating is a minor.** Adding a deprecation puts no obligation on the
33
+ user *yet*, so it does not need a major — but it starts the clock. See
34
+ [DEPRECATIONS.md](DEPRECATIONS.md).
35
+
36
+ ## 1. Green suite
37
+
38
+ ```sh
39
+ bin/test
40
+ ```
41
+
42
+ A red suite is not a release candidate; nothing downstream checks this for you.
43
+
44
+ ## 2. Bump
45
+
46
+ ```sh
47
+ gem kit bump <major|minor|patch>
48
+ ```
49
+
50
+ Rewrites `lib/kube_schema/version.rb` — by rendering its `.erb` template if there is
51
+ one, otherwise by substituting the version literal in place — and prints the
52
+ transition:
53
+
54
+ ```
55
+ 2.0.0 -> ...
56
+
57
+ now run: gem kit changelog --write
58
+ ```
59
+
60
+ **It refuses to bump onto a deprecation deadline.** If any registered
61
+ deprecation is due at the new version, it lists them with their source lines
62
+ and fails. Remove the deprecated code first — that is the point of the promise.
63
+ `--force` overrides and says what it overrode. Deprecations *not* yet due are
64
+ reported for information, not blocked on.
65
+
66
+ ## 3. Changelog
67
+
68
+ ```sh
69
+ gem kit changelog --write
70
+ ```
71
+
72
+ Hands the entry to the AI CLI named by `config.changelog_writer` (default:
73
+ `claude`), with a prompt describing the format and this project's conventions.
74
+ It reads the commits since the last tag and edits `CHANGELOG.md` only.
75
+
76
+ Review what it writes. It is a first draft with the commits in front of it, not
77
+ an oracle — it cannot know which of two changes mattered to users.
78
+
79
+ The format is [Keep a Changelog](https://keepachangelog.com/en/1.1.0/):
80
+
81
+ ```md
82
+ ## [Unreleased]
83
+
84
+ ## [2.0.0] - 2026-10-17
85
+
86
+ ### Added
87
+
88
+ - Something users can now do.
89
+
90
+ ### Deprecated
91
+
92
+ - `Old::Name` — use `New::Name` instead. Removed in 3.0.
93
+ ```
94
+
95
+ Entries go under `Added`, `Changed`, `Deprecated`, `Removed`, `Fixed` or
96
+ `Security` — those six and no others. `[Unreleased]` stays at the top, emptied
97
+ of anything that shipped.
98
+
99
+ ## 4. Lint the changelog
100
+
101
+ ```sh
102
+ gem kit changelog # format only
103
+ gem kit changelog 2.0.0 # format, plus "is this version ready?"
104
+ ```
105
+
106
+ Checks the title, that every heading is `## [Unreleased]` or
107
+ `## [1.2.3] - YYYY-MM-DD`, that versions are valid, dated, unique and ordered
108
+ newest-first, that `###` sections are one of the six types and none are empty —
109
+ and, given a version, that it has a non-empty section at the top of the released
110
+ list. Every problem is reported as `CHANGELOG.md:<line> <what>`.
111
+
112
+ ## 5. Commit
113
+
114
+ The version bump, the lockfile and the changelog belong in one commit, before
115
+ anything is pushed to RubyGems. A published gem whose changelog is still
116
+ unwritten in git is the failure this process exists to prevent.
117
+
118
+ ## 6. Release
119
+
120
+ ```sh
121
+ gem kit release # or --dry-run, which is the CI check
122
+ ```
123
+
124
+ Three gates, all before `gem build` runs:
125
+
126
+ 1. **Changelog** — this version needs its own non-empty, correctly formatted
127
+ section, sitting at the top of the released list.
128
+ 2. **Deprecations** — nothing promised to disappear in this version may still
129
+ be in the tree.
130
+ 3. **The working tree** — everything committed. A gem built from uncommitted
131
+ files is a gem whose source exists nowhere, and step 2 and step 3 above
132
+ leave exactly two such files behind. `--allow-dirty` overrides it.
133
+
134
+ Then `gem build`, `gem push`, and the tag — `v2.0.0`,
135
+ created and pushed *after* the gem, so a tag never names a version that failed
136
+ to publish. `--no-tag` skips it.
137
+
138
+ Requires RubyGems push credentials.
139
+
140
+ The tag matters beyond bookkeeping: it is what the next
141
+ `gem kit changelog --write` uses to find the commit range, so a
142
+ missing one makes the following release's changelog harder to write.
143
+
144
+ `gem kit tag` still exists for tagging on its own.
145
+
146
+ ## When something goes wrong
147
+
148
+ **Published a broken gem.** Don't delete it — `gem yank kube_schema -v X.Y.Z` if
149
+ it is genuinely dangerous, otherwise ship a patch. Yanking a version other
150
+ people have already locked to breaks their builds.
151
+
152
+ **Bumped but the release failed.** The bump is just a file. Fix the cause and
153
+ re-run `gem kit release`; there is no need to un-bump.
154
+
155
+ **Changelog written for the wrong version.** Edit the heading and re-run
156
+ `gem kit changelog <version>`. Nothing downstream caches it.
157
+
158
+ ## See also
159
+
160
+ - [DEPRECATIONS.md](DEPRECATIONS.md) — the deprecation policy the gates enforce.
161
+ - [CHANGELOG.md](CHANGELOG.md).
data/Rakefile CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  task :test do
4
- Dir["lib/**/*.rb"].each { |f| sh "ruby", f }
4
+ sh "bundle", "exec", "scampi"
5
5
  end
6
6
 
7
7
  task default: :test
data/bin/rubocop ADDED
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env bash
2
+
3
+ exec bundle exec rubocop "$@"
data/bin/test CHANGED
@@ -1,10 +1,3 @@
1
- #!/usr/bin/env ruby
2
- # frozen_string_literal: true
1
+ #!/usr/bin/env sh
3
2
 
4
- Dir.chdir(File.expand_path("..", __dir__))
5
-
6
- files = ARGV.empty? ? Dir.glob("lib/**/*.rb").sort : ARGV
7
-
8
- files.each do |f|
9
- system("bundle", "exec", "ruby", f, exception: true)
10
- end
3
+ exec bundle exec scampi "$@"