gempilot 0.2.4 → 0.3.1
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.
- checksums.yaml +4 -4
- data/.claude/memory/be-decisive-not-menus.md +14 -0
- data/.claude/memory/flagship-shared-rubocop-config-state.md +16 -0
- data/.rubocop.yml +0 -14
- data/CLAUDE.md +2 -1
- data/README.md +9 -1
- data/data/templates/gem/Rakefile.erb +2 -10
- data/data/templates/gem/lib/gem_name.rb.erb +1 -2
- data/data/templates/gem/lib/gem_name_extension.rb.erb +1 -2
- data/data/templates/gem/test/test_helper.rb.erb +5 -2
- data/docs/superpowers/plans/2026-08-09-dev-version-bump.md +483 -0
- data/docs/superpowers/specs/2026-08-09-dev-version-bump-design.md +163 -0
- data/issues.rec +94 -6
- data/lib/gempilot/cli/commands/bump.rb +5 -4
- data/lib/gempilot/cli/gem_builder.rb +13 -1
- data/lib/gempilot/gem_constant.rb +8 -8
- data/lib/gempilot/github_release.rb +9 -1
- data/lib/gempilot/project/version.rb +4 -25
- data/lib/gempilot/project.rb +9 -1
- data/lib/gempilot/segmented_version.rb +64 -0
- data/lib/gempilot/version.rb +1 -1
- data/lib/gempilot/version_task.rb +1 -1
- data/lib/gempilot/zeitwerk_task.rb +61 -0
- metadata +8 -2
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 686fd797469009843d44529b0831634baf163cbd007a7d625f9605e340aa021a
|
|
4
|
+
data.tar.gz: f1144a8db6d9a54e8b4cfb146aae7d4d240c92f87a08cf4501589241934cfcbc
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 5ae0fdd86a89ec6855ae589c6c64fc28d97e71df117f81a35440ca9029333d962171daf080c8d0b24adafe2e83a4e41e2cb0bd989399d8df9cc79dfb2da6d9b0
|
|
7
|
+
data.tar.gz: abcea8380c013d64f9f22d8c82a59ffbb18a24ff5bcd0be017c0a640450cec51afc88fb676c0004f797f0a54404ca24e4acc8a2183cd836719dab0e117532580
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: be-decisive-not-menus
|
|
3
|
+
description: "David wants decisive action — make the clear right move, don't present option-menus or re-ask"
|
|
4
|
+
metadata:
|
|
5
|
+
node_type: memory
|
|
6
|
+
type: feedback
|
|
7
|
+
originSessionId: 8ab20c6a-4c03-4f0a-9acd-e12099699ad8
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
David wants decisive execution, not deliberation theater. He corrected me repeatedly (and bluntly) this session for presenting choices instead of acting, re-offering things already done, and asking permission for obvious next steps.
|
|
11
|
+
|
|
12
|
+
**Why:** He's the maintainer and knows his codebase; a menu of options he has to adjudicate is friction, not help. "and?" from him means "keep going / do the next obvious thing." "do the right thing" means "use your judgment and execute."
|
|
13
|
+
|
|
14
|
+
**How to apply:** When something's already done, say it's done — don't re-offer it. When there's a clear best move (green reviewed PR → merge it; verified-safe branch cleanup → do it), just make it and report the result. Reserve questions for genuine forks where his answer changes the outcome, and even then lead with a one-word-approvable recommendation instead of a survey. Don't narrate options you won't pursue. Confirm only truly hard-to-reverse or outward-facing actions — and expect him to push you past even those with "just do it."
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: flagship-shared-rubocop-config-state
|
|
3
|
+
description: "Status of issue 79AE436E (shared RuboCop config + eject) — designed 2026-07-20, not yet built"
|
|
4
|
+
metadata:
|
|
5
|
+
node_type: memory
|
|
6
|
+
type: project
|
|
7
|
+
originSessionId: 8ab20c6a-4c03-4f0a-9acd-e12099699ad8
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
The flagship **79AE436E** ("own shared RuboCop config via `inherit_gem` + eject hatch") is **designed but not implemented** as of 2026-07-20. Design spec committed at `docs/superpowers/specs/2026-07-20-shared-rubocop-config-eject-design.md` on branch `file-issue-own-shared-config` (that branch = the flagship WIP: issue-filing commit + design doc only; the 6 backlog fixes that were briefly on it shipped separately via PR #20 and are now in master).
|
|
11
|
+
|
|
12
|
+
Approved direction: `inherit_gem`; gempilot ships `config/rubocop/{base,minitest,rspec}.yml`; generated `.rubocop.yml` shrinks to near-zero (gem-name excludes become globs `*.gemspec`/`lib/*-*.rb`; drop `TargetRubyVersion` since RuboCop reads it from the gemspec's `required_ruby_version`); new `gempilot eject` inlines config + cuts the link; generated Gemfile pins gempilot.
|
|
13
|
+
|
|
14
|
+
Before/while building: **(1) OPEN decision** — CI-break mitigation policy; **pin** is the recommended default but David has NOT confirmed it. **(2) First step is a SPIKE** — verify RuboCop loads `plugins:` (rubocop-claude etc.) *through* an `inherit_gem` config; if not, the plugins list stays in the local file. See [[gempilot-generated-gem-rubocop-gate]].
|
|
15
|
+
|
|
16
|
+
Other open backlog item: **741FFD05** (reduce deps). The gemspec's 4 runtime deps (`command_kit`, `rake`, `warning`, `zeitwerk`) are all used — leave them; reduction is purely Gemfile dev deps. Recommended: drop `benchmark`/`observer`/`rbs`; keep `irb`/`repl_type_completor` (bin/console), `rdoc` (docs), `debug`. (`warning` is the one narrow runtime dep — only silences the VERSION-reload warnings in `Project#fetch_version`.)
|
data/.rubocop.yml
CHANGED
|
@@ -279,17 +279,3 @@ RSpec/NamedSubject:
|
|
|
279
279
|
# have_file_content matcher accepts a filename string in expect()
|
|
280
280
|
RSpec/ExpectActual:
|
|
281
281
|
Enabled: false
|
|
282
|
-
|
|
283
|
-
Design/PositionalArguments:
|
|
284
|
-
Enabled: false
|
|
285
|
-
|
|
286
|
-
Design/KeywordArguments:
|
|
287
|
-
Enabled: false
|
|
288
|
-
|
|
289
|
-
Design/TotalArguments:
|
|
290
|
-
Enabled: false
|
|
291
|
-
|
|
292
|
-
Design/AgentNounClassName:
|
|
293
|
-
Exclude:
|
|
294
|
-
- 'lib/gempilot/generator.rb'
|
|
295
|
-
- 'test/gempilot/cli/generator_test.rb'
|
data/CLAUDE.md
CHANGED
|
@@ -10,7 +10,7 @@ A CLI tool for creating and managing Ruby gems, built on CommandKit.
|
|
|
10
10
|
- `gempilot create` — Scaffold a new gem (templates in `data/templates/gem/`)
|
|
11
11
|
- `gempilot new` — Generate a class, module, or command in an existing gem (templates in `data/templates/new/`)
|
|
12
12
|
- `gempilot destroy` — Remove a class, module, or command from an existing gem
|
|
13
|
-
- `gempilot bump` — Bump version in `version.rb` (patch default, or minor/major)
|
|
13
|
+
- `gempilot bump` — Bump version in `version.rb` (patch default, or minor/major/tiny/dev)
|
|
14
14
|
- `gempilot release` — Thin proxy to `rake release`
|
|
15
15
|
- `gempilot console` — Thin proxy to `bin/console`
|
|
16
16
|
|
|
@@ -21,6 +21,7 @@ A CLI tool for creating and managing Ruby gems, built on CommandKit.
|
|
|
21
21
|
- Generator module (`lib/gempilot/cli/generator.rb`) provides template rendering via ERB
|
|
22
22
|
- GemContext module (`lib/gempilot/cli/gem_context.rb`) shared by new, destroy, release, console
|
|
23
23
|
- `GemConstant` value object (`lib/gempilot/gem_constant.rb`) owns constant→namespace/path resolution for `new`/`destroy`; constants are rooted at the gem module by construction
|
|
24
|
+
- `SegmentedVersion` value object (`lib/gempilot/segmented_version.rb`) owns version parsing and bump arithmetic; every bump moves to the smallest version of the requested shape greater than the current version (RubyGems ordering)
|
|
24
25
|
- CommandKit::Commands::AutoLoad maps filenames in `commands/` to command names
|
|
25
26
|
|
|
26
27
|
### Testing
|
data/README.md
CHANGED
|
@@ -82,8 +82,16 @@ Bump the version in `lib/<gem>/version.rb`.
|
|
|
82
82
|
gempilot bump # patch (default)
|
|
83
83
|
gempilot bump minor
|
|
84
84
|
gempilot bump major
|
|
85
|
+
gempilot bump tiny # fourth integer for tiny follow-ups: 0.2.0 -> 0.2.0.1
|
|
86
|
+
gempilot bump dev # preview of the next patch: 0.2.0 -> 0.2.1.dev1
|
|
85
87
|
```
|
|
86
88
|
|
|
89
|
+
A `dev` version previews the next patch, so it always sorts ahead of the
|
|
90
|
+
current release (`0.2.0 < 0.2.1.dev1 < 0.2.1`), and a numeric bump finalizes
|
|
91
|
+
the cycle (`gempilot bump` from `0.2.1.dev3` gives `0.2.1`). A repo holding an
|
|
92
|
+
old-style dev version — one created *after* its base version shipped — needs a
|
|
93
|
+
one-time hand edit of `version.rb` past the last published version.
|
|
94
|
+
|
|
87
95
|
### `gempilot release`
|
|
88
96
|
|
|
89
97
|
Delegates to `rake release`, which publishes the current version to all remotes
|
|
@@ -113,7 +121,7 @@ Generated gems include rake tasks for the full version lifecycle:
|
|
|
113
121
|
| Task | Description |
|
|
114
122
|
|------|-------------|
|
|
115
123
|
| `rake version:current` | Display the current version |
|
|
116
|
-
| `rake version:bump` |
|
|
124
|
+
| `rake version:bump` | Bump the version (patch default; major/minor/patch/tiny/dev) |
|
|
117
125
|
| `rake version:commit` | Commit the version file change |
|
|
118
126
|
| `rake version:tag` | Create a git tag for the version |
|
|
119
127
|
| `rake version:untag` | Delete the version git tag |
|
|
@@ -18,16 +18,8 @@ RuboCop::RakeTask.new
|
|
|
18
18
|
require "gempilot/version_task"
|
|
19
19
|
Gempilot::VersionTask.new
|
|
20
20
|
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
task :validate do
|
|
24
|
-
ruby "-e", <<~RUBY
|
|
25
|
-
require '<%= @require_path %>'
|
|
26
|
-
<%= @module_name %>::LOADER.eager_load(force: true)
|
|
27
|
-
puts 'Zeitwerk: All files loaded successfully.'
|
|
28
|
-
RUBY
|
|
29
|
-
end
|
|
30
|
-
end
|
|
21
|
+
require "gempilot/zeitwerk_task"
|
|
22
|
+
Gempilot::ZeitwerkTask.new
|
|
31
23
|
|
|
32
24
|
<% if @test_framework == :minitest -%>
|
|
33
25
|
task default: [:test, :rubocop]
|
|
@@ -9,8 +9,7 @@ require "zeitwerk"
|
|
|
9
9
|
<% end -%>
|
|
10
10
|
<%= " " * i %>module <%= part %>
|
|
11
11
|
<% end -%>
|
|
12
|
-
<%= " " * @module_parts.size %>LOADER = Zeitwerk::Loader.for_gem_extension(<%= @base_module %>)
|
|
13
|
-
<%= " " * @module_parts.size %>LOADER.setup
|
|
12
|
+
<%= " " * @module_parts.size %>LOADER = Zeitwerk::Loader.for_gem_extension(<%= @base_module %>).tap(&:setup)
|
|
14
13
|
|
|
15
14
|
<%= " " * @module_parts.size %>##
|
|
16
15
|
<%= " " * @module_parts.size %># Base error class for <%= @gem_name %>.
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
ENV["MT_NO_PLUGINS"] = "1" unless ENV["RM_INFO"]
|
|
2
2
|
|
|
3
3
|
require "minitest/autorun"
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
|
|
5
|
+
unless ENV["RM_INFO"]
|
|
6
|
+
require "minitest/reporters"
|
|
7
|
+
Minitest::Reporters.use! [Minitest::Reporters::DefaultReporter.new(color: true)]
|
|
8
|
+
end
|
|
6
9
|
|
|
7
10
|
require "<%= @require_path %>"
|
|
@@ -0,0 +1,483 @@
|
|
|
1
|
+
# Forward-Moving Version Bumps Implementation Plan
|
|
2
|
+
|
|
3
|
+
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
|
4
|
+
|
|
5
|
+
**Goal:** Make every `version:bump` segment move strictly forward under RubyGems ordering, add a `tiny` fourth-integer segment, and mark dev GitHub releases as prereleases.
|
|
6
|
+
|
|
7
|
+
**Architecture:** A new `Gempilot::SegmentedVersion` value object (Data class) owns parsing of the three accepted version shapes (`M.m.p`, `M.m.p.N`, `M.m.p.devN`) and all bump arithmetic; `Project::Version#bump` becomes a thin delegation, keeping its `Data.define(:path, :value)` surface untouched. `GithubRelease#create` derives prerelease status from its tag string. Spec: `docs/superpowers/specs/2026-08-09-dev-version-bump-design.md`.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Ruby (Zeitwerk autoloading), RSpec (`spec/`, library), Minitest (`test/`, CLI), RuboCop.
|
|
10
|
+
|
|
11
|
+
## Global Constraints
|
|
12
|
+
|
|
13
|
+
- Semantics rule (from spec): every bump yields the smallest version of the requested shape strictly greater than the current version. Full transition table is in the spec and is reproduced in Task 1's test code — treat that table as normative.
|
|
14
|
+
- Double-quoted strings (`Style/StringLiterals: double_quotes`); NO `# frozen_string_literal:` comments (`EnforcedStyle: never`); trailing commas in multiline literals/arguments.
|
|
15
|
+
- Ruby ≥ 3.4 idioms are fine (`it` block parameter is used throughout the codebase); rdoc `##` doc blocks (never YARD tags), no blank line between `##` block and the definition.
|
|
16
|
+
- Zeitwerk: new file `lib/gempilot/segmented_version.rb` MUST define `Gempilot::SegmentedVersion` (the eager-load spec `spec/zeitwerk_spec.rb` enforces this).
|
|
17
|
+
- Verification commands: `bundle exec rspec <file>` for focused runs, `bundle exec rake default` (= `test` + `spec` + `rubocop`) for the full gate. Baseline before Task 1: 145 examples, 0 failures, 60 files no rubocop offenses.
|
|
18
|
+
- Commit messages: plain imperative, no conventional-commit prefixes (repo style: "Add issue", "Add dev version bump design spec").
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
### Task 1: SegmentedVersion arithmetic + Version#bump rewrite
|
|
23
|
+
|
|
24
|
+
**Files:**
|
|
25
|
+
- Create: `lib/gempilot/segmented_version.rb`
|
|
26
|
+
- Modify: `lib/gempilot/project/version.rb` (full rewrite shown below)
|
|
27
|
+
- Test: `spec/gempilot/project/version_spec.rb` (full rewrite shown below)
|
|
28
|
+
- Test: `spec/gempilot/version_task_spec.rb:35-38` (one expectation flip)
|
|
29
|
+
|
|
30
|
+
**Interfaces:**
|
|
31
|
+
- Consumes: `Gempilot::Project::Version` `Data.define(:path, :value)` (existing), `Data#with` (Ruby core).
|
|
32
|
+
- Produces: `Gempilot::SegmentedVersion.parse(string) → SegmentedVersion` (raises `ArgumentError` on unrecognized shapes), `SegmentedVersion#bump(segment) → SegmentedVersion` for `:major/:minor/:patch/:tiny/:dev` (symbol or string; raises `ArgumentError` otherwise), `SegmentedVersion#to_s → String`. `Version#bump(segment = :patch)` / `#next_version` keep their existing signatures — later tasks rely only on those.
|
|
33
|
+
|
|
34
|
+
- [ ] **Step 1: Rewrite the version spec with the normative transition table**
|
|
35
|
+
|
|
36
|
+
Replace the entire contents of `spec/gempilot/project/version_spec.rb` with:
|
|
37
|
+
|
|
38
|
+
```ruby
|
|
39
|
+
require "spec_helper"
|
|
40
|
+
|
|
41
|
+
RSpec.describe Gempilot::Project::Version do
|
|
42
|
+
let(:path) { Pathname("lib/my_gem/version.rb") }
|
|
43
|
+
|
|
44
|
+
def version(value)
|
|
45
|
+
described_class.new(path: path, value: value)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
describe "#tag" do
|
|
49
|
+
it "prepends v to the value" do
|
|
50
|
+
expect(version("1.2.3").tag).to eq("v1.2.3")
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
describe "#bump" do
|
|
55
|
+
transitions = {
|
|
56
|
+
"1.2.3" => { major: "2.0.0", minor: "1.3.0", patch: "1.2.4", tiny: "1.2.3.1", dev: "1.2.4.dev1" },
|
|
57
|
+
"1.2.3.1" => { major: "2.0.0", minor: "1.3.0", patch: "1.2.4", tiny: "1.2.3.2", dev: "1.2.4.dev1" },
|
|
58
|
+
"1.2.4.dev2" => { major: "2.0.0", minor: "1.3.0", patch: "1.2.4", tiny: "1.2.4.1", dev: "1.2.4.dev3" },
|
|
59
|
+
"1.3.0.dev2" => { major: "2.0.0", minor: "1.3.0", patch: "1.3.0", tiny: "1.3.0.1", dev: "1.3.0.dev3" },
|
|
60
|
+
"2.0.0.dev2" => { major: "2.0.0", minor: "2.0.0", patch: "2.0.0", tiny: "2.0.0.1", dev: "2.0.0.dev3" },
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
transitions.each do |from, bumps|
|
|
64
|
+
bumps.each do |segment, to|
|
|
65
|
+
it "bumps #{from} to #{to} for #{segment}" do
|
|
66
|
+
expect(version(from).bump(segment).value).to eq(to)
|
|
67
|
+
end
|
|
68
|
+
end
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
it "moves every transition strictly forward under Gem::Version ordering" do
|
|
72
|
+
transitions.each do |from, bumps|
|
|
73
|
+
bumps.each_value do |to|
|
|
74
|
+
expect(Gem::Version.new(to)).to be > Gem::Version.new(from)
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
it "bumps patch by default" do
|
|
80
|
+
expect(version("1.2.3").bump.value).to eq("1.2.4")
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
it "accepts segment as a string" do
|
|
84
|
+
expect(version("1.0.0").bump("minor").value).to eq("1.1.0")
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
it "raises for an unknown segment" do
|
|
88
|
+
expect { version("1.0.0").bump(:hotfix) }.to raise_error(ArgumentError, /unknown segment/i)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
it "raises for a two-integer version" do
|
|
92
|
+
expect { version("1.2").bump }.to raise_error(ArgumentError, /cannot parse/i)
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
it "raises for a non-dev prerelease version" do
|
|
96
|
+
expect { version("1.2.3.beta1").bump }.to raise_error(ArgumentError, /cannot parse/i)
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
describe "#next_version" do
|
|
101
|
+
it "finalizes a dev version to its target" do
|
|
102
|
+
expect(version("0.0.4.dev3").next_version.value).to eq("0.0.4")
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
it "bumps the patch of a release version" do
|
|
106
|
+
expect(version("1.0.99").next_version.value).to eq("1.0.100")
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
- [ ] **Step 2: Flip the stale expectation in the version task spec**
|
|
113
|
+
|
|
114
|
+
In `spec/gempilot/version_task_spec.rb`, the fixture version is `1.0.0.dev3`. Change the `version:bump` default example (currently expects `"1.0.1"`) to:
|
|
115
|
+
|
|
116
|
+
```ruby
|
|
117
|
+
it "finalizes a dev version to its target by default" do
|
|
118
|
+
Rake::Task["version:bump"].invoke
|
|
119
|
+
expect(version_in_file).to eq("1.0.0")
|
|
120
|
+
end
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
(The `version:bump[dev]` → `"1.0.0.dev4"` and both `version:release` examples are already correct under the new semantics — do not touch them.)
|
|
124
|
+
|
|
125
|
+
- [ ] **Step 3: Run the specs to verify they fail for the right reason**
|
|
126
|
+
|
|
127
|
+
Run: `bundle exec rspec spec/gempilot/project/version_spec.rb spec/gempilot/version_task_spec.rb --no-color`
|
|
128
|
+
Expected: FAILURES including `expected: "1.2.4.dev1" got: "1.2.3.dev1"` (dev goes backward today), `expected: "1.2.4" got: "1.2.5"` (patch overshoots a dev target), every `:tiny` cell erroring with `ArgumentError: Unknown segment :tiny` (old code rejects the segment), the `"1.2"` example failing with `NoMethodError` instead of `ArgumentError` (old code coerces then crashes on the missing patch integer), the `"1.2.3.beta1"` example failing with "expected ArgumentError but nothing was raised" (old code silently coerces to `1.2.4`), and `expected: "1.0.0" got: "1.0.1"` in the task spec. The `#tag`, string-segment, and unknown-segment examples still pass.
|
|
129
|
+
|
|
130
|
+
- [ ] **Step 4: Create the SegmentedVersion value object**
|
|
131
|
+
|
|
132
|
+
Create `lib/gempilot/segmented_version.rb`:
|
|
133
|
+
|
|
134
|
+
```ruby
|
|
135
|
+
module Gempilot
|
|
136
|
+
## Arithmetic over the three version shapes gempilot accepts: release
|
|
137
|
+
## (+1.2.3+), tiny release (+1.2.3.1+), and dev prerelease (+1.2.3.dev1+).
|
|
138
|
+
##
|
|
139
|
+
## Every bump returns the smallest version of the requested shape that is
|
|
140
|
+
## strictly greater than the current version under RubyGems ordering, so
|
|
141
|
+
## bumping always moves a project forward: a dev bump previews the next
|
|
142
|
+
## patch (+1.2.3+ to +1.2.4.dev1+) and numeric bumps finalize a dev cycle
|
|
143
|
+
## (+1.2.4.dev2+ to +1.2.4+ for +:patch+).
|
|
144
|
+
SegmentedVersion = Data.define(:major, :minor, :patch, :tiny, :dev) do
|
|
145
|
+
def self.parse(string)
|
|
146
|
+
format = /\A(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(?:\.(?<tiny>\d+)|\.dev(?<dev>\d+))?\z/
|
|
147
|
+
match = format.match(string)
|
|
148
|
+
raise ArgumentError, "cannot parse version: #{string.inspect}" unless match
|
|
149
|
+
|
|
150
|
+
new(**match.named_captures.to_h { |name, digits| [name.to_sym, digits&.to_i] })
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def bump(segment)
|
|
154
|
+
case segment.to_sym
|
|
155
|
+
when :major then bump_major
|
|
156
|
+
when :minor then bump_minor
|
|
157
|
+
when :patch then bump_patch
|
|
158
|
+
when :tiny then bump_tiny
|
|
159
|
+
when :dev then bump_dev
|
|
160
|
+
else raise ArgumentError, "Unknown segment #{segment.inspect}. Use :major, :minor, :patch, :tiny, or :dev"
|
|
161
|
+
end
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def to_s
|
|
165
|
+
[major, minor, patch, tiny, ("dev#{dev}" if dev)].compact.join(".")
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
private
|
|
169
|
+
|
|
170
|
+
def bump_major
|
|
171
|
+
return finalize if dev && minor.zero? && patch.zero?
|
|
172
|
+
|
|
173
|
+
with(major: major + 1, minor: 0, patch: 0, tiny: nil, dev: nil)
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def bump_minor
|
|
177
|
+
return finalize if dev && patch.zero?
|
|
178
|
+
|
|
179
|
+
with(minor: minor + 1, patch: 0, tiny: nil, dev: nil)
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def bump_patch
|
|
183
|
+
dev ? finalize : with(patch: patch + 1, tiny: nil)
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def bump_tiny
|
|
187
|
+
with(tiny: (tiny || 0) + 1, dev: nil)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
def bump_dev
|
|
191
|
+
dev ? with(dev: dev + 1) : with(patch: patch + 1, tiny: nil, dev: 1)
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
def finalize
|
|
195
|
+
with(tiny: nil, dev: nil)
|
|
196
|
+
end
|
|
197
|
+
end.freeze
|
|
198
|
+
end
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
Notes for the implementer:
|
|
202
|
+
- The regex lives in a method-local (`format`), not a constant — a constant inside the `Data.define` block would trip `Lint/ConstantDefinitionInBlock`.
|
|
203
|
+
- `bump` dispatches to five one-branch private methods to stay under `Metrics/CyclomaticComplexity` (max 7).
|
|
204
|
+
- `finalize` drops the prerelease/tiny suffix — it encodes "the smallest version of a numeric shape greater than a dev version is the dev version's own target".
|
|
205
|
+
|
|
206
|
+
- [ ] **Step 5: Rewrite Version#bump as a delegation**
|
|
207
|
+
|
|
208
|
+
Replace the entire contents of `lib/gempilot/project/version.rb` with:
|
|
209
|
+
|
|
210
|
+
```ruby
|
|
211
|
+
module Gempilot
|
|
212
|
+
class Project
|
|
213
|
+
## The project's version file (+path+) and value. Bump arithmetic
|
|
214
|
+
## delegates to SegmentedVersion, so every bump moves strictly forward
|
|
215
|
+
## under RubyGems ordering.
|
|
216
|
+
Version = Data.define(:path, :value) do
|
|
217
|
+
def tag
|
|
218
|
+
"v#{value}"
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
def bump(segment = :patch)
|
|
222
|
+
with(value: SegmentedVersion.parse(value).bump(segment).to_s)
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
alias_method :next_version, :bump
|
|
226
|
+
end.freeze
|
|
227
|
+
end
|
|
228
|
+
end
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
- [ ] **Step 6: Run the specs to verify they pass**
|
|
232
|
+
|
|
233
|
+
Run: `bundle exec rspec spec/gempilot/project/version_spec.rb spec/gempilot/version_task_spec.rb --no-color`
|
|
234
|
+
Expected: PASS, 0 failures (34 + task-spec examples). If `zeitwerk` cannot resolve the new constant, the filename does not match `Gempilot::SegmentedVersion` — fix the filename, not the loader.
|
|
235
|
+
|
|
236
|
+
- [ ] **Step 7: Run the full gate**
|
|
237
|
+
|
|
238
|
+
Run: `bundle exec rake default 2>&1 | tail -8`
|
|
239
|
+
Expected: minitest green, all RSpec examples green (count grows from 145), `no offenses detected`.
|
|
240
|
+
|
|
241
|
+
- [ ] **Step 8: Commit**
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
git add lib/gempilot/segmented_version.rb lib/gempilot/project/version.rb spec/gempilot/project/version_spec.rb spec/gempilot/version_task_spec.rb
|
|
245
|
+
git commit -m "Make version bumps move strictly forward"
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
### Task 2: `tiny` segment on the rake and CLI surfaces
|
|
251
|
+
|
|
252
|
+
**Files:**
|
|
253
|
+
- Modify: `lib/gempilot/version_task.rb:53` (desc string only)
|
|
254
|
+
- Modify: `lib/gempilot/cli/commands/bump.rb`
|
|
255
|
+
- Test: `spec/gempilot/version_task_spec.rb` (add one example)
|
|
256
|
+
- Test: `test/gempilot/cli/bump_command_test.rb` (add one test)
|
|
257
|
+
|
|
258
|
+
**Interfaces:**
|
|
259
|
+
- Consumes: `Version#bump` accepting `:tiny` (Task 1); rake `version:bump[segment]` forwards the segment string verbatim (existing behavior — no logic change needed there).
|
|
260
|
+
- Produces: `gempilot bump tiny` and `rake version:bump[tiny]` as documented user commands; CLI allowed-segment list `%w[patch minor major tiny dev]`.
|
|
261
|
+
|
|
262
|
+
- [ ] **Step 1: Add the failing rake-task example**
|
|
263
|
+
|
|
264
|
+
In `spec/gempilot/version_task_spec.rb`, inside `describe "version:bump"`, add:
|
|
265
|
+
|
|
266
|
+
```ruby
|
|
267
|
+
it "bumps the tiny segment" do
|
|
268
|
+
Rake::Task["version:bump"].invoke("tiny")
|
|
269
|
+
expect(version_in_file).to eq("1.0.0.1")
|
|
270
|
+
end
|
|
271
|
+
```
|
|
272
|
+
|
|
273
|
+
(Fixture is `1.0.0.dev3`; `tiny` finalizes the core and appends `.1`.)
|
|
274
|
+
|
|
275
|
+
- [ ] **Step 2: Add the failing CLI test**
|
|
276
|
+
|
|
277
|
+
In `test/gempilot/cli/bump_command_test.rb`, after `test_bump_dev_invokes_rake`, add:
|
|
278
|
+
|
|
279
|
+
```ruby
|
|
280
|
+
def test_bump_tiny_invokes_rake
|
|
281
|
+
calls = recorded_system_calls { |cmd| cmd.main(["tiny"]) }
|
|
282
|
+
|
|
283
|
+
assert_includes calls, ["bundle", "exec", "rake", "version:bump[tiny]"]
|
|
284
|
+
end
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
- [ ] **Step 3: Run both to verify the CLI test fails and the rake example passes**
|
|
288
|
+
|
|
289
|
+
Run: `bundle exec rspec spec/gempilot/version_task_spec.rb --no-color && bundle exec rake test 2>&1 | tail -4`
|
|
290
|
+
Expected: the rake-task example PASSES already (the task forwards any segment and Task 1 taught `Version#bump` about `:tiny`) — it is a pin, not a driver. The minitest CLI test FAILS: `Bump#validate_segment` rejects `tiny` and exits 1 before calling rake.
|
|
291
|
+
|
|
292
|
+
- [ ] **Step 4: Teach the CLI command the tiny segment**
|
|
293
|
+
|
|
294
|
+
In `lib/gempilot/cli/commands/bump.rb` apply these four edits:
|
|
295
|
+
|
|
296
|
+
```ruby
|
|
297
|
+
description "Bump the gem version (patch by default, or minor/major/tiny/dev)"
|
|
298
|
+
|
|
299
|
+
examples [
|
|
300
|
+
"",
|
|
301
|
+
"patch",
|
|
302
|
+
"minor",
|
|
303
|
+
"major",
|
|
304
|
+
"tiny",
|
|
305
|
+
"dev",
|
|
306
|
+
]
|
|
307
|
+
|
|
308
|
+
argument :segment, required: false,
|
|
309
|
+
desc: "Version segment to bump: patch (default), minor, major, tiny, or dev"
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
and in `validate_segment`:
|
|
313
|
+
|
|
314
|
+
```ruby
|
|
315
|
+
return segment if %w[patch minor major tiny dev].include?(segment)
|
|
316
|
+
|
|
317
|
+
puts colors.red("Unknown segment '#{segment}'. Use patch, minor, major, tiny, or dev.")
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
- [ ] **Step 5: Update the rake task description**
|
|
321
|
+
|
|
322
|
+
In `lib/gempilot/version_task.rb`, `define_bump_task`:
|
|
323
|
+
|
|
324
|
+
```ruby
|
|
325
|
+
desc "Bump version (patch default; segments: major, minor, patch, tiny, dev)"
|
|
326
|
+
```
|
|
327
|
+
|
|
328
|
+
- [ ] **Step 6: Run the full gate**
|
|
329
|
+
|
|
330
|
+
Run: `bundle exec rake default 2>&1 | tail -8`
|
|
331
|
+
Expected: all green, no offenses. (`test_bump_fails_with_invalid_segment` uses `hotfix`, which stays invalid.)
|
|
332
|
+
|
|
333
|
+
- [ ] **Step 7: Commit**
|
|
334
|
+
|
|
335
|
+
```bash
|
|
336
|
+
git add lib/gempilot/version_task.rb lib/gempilot/cli/commands/bump.rb spec/gempilot/version_task_spec.rb test/gempilot/cli/bump_command_test.rb
|
|
337
|
+
git commit -m "Add tiny segment to bump interfaces"
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
---
|
|
341
|
+
|
|
342
|
+
### Task 3: Mark dev GitHub releases as prereleases
|
|
343
|
+
|
|
344
|
+
**Files:**
|
|
345
|
+
- Modify: `lib/gempilot/github_release.rb`
|
|
346
|
+
- Test: `spec/gempilot/github_release_spec.rb`
|
|
347
|
+
|
|
348
|
+
**Interfaces:**
|
|
349
|
+
- Consumes: `GithubRelease.new(tag)` with tag strings like `"v1.2.4.dev1"` (signature unchanged); `Gem::Version#prerelease?` (rubygems, always loaded).
|
|
350
|
+
- Produces: `gh release create` invoked with `--prerelease` when and only when the tag names a prerelease version.
|
|
351
|
+
|
|
352
|
+
- [ ] **Step 1: Add the failing specs**
|
|
353
|
+
|
|
354
|
+
In `spec/gempilot/github_release_spec.rb`, inside `describe "#create"`, add two contexts after the existing examples:
|
|
355
|
+
|
|
356
|
+
```ruby
|
|
357
|
+
context "with a dev prerelease tag" do
|
|
358
|
+
let(:tag) { "v1.2.4.dev1" }
|
|
359
|
+
|
|
360
|
+
it "marks the release as a prerelease" do
|
|
361
|
+
release.create
|
|
362
|
+
args = ["gh", "release", "create", "--generate-notes", "--fail-on-no-commits", "--prerelease", tag]
|
|
363
|
+
expect(release).to have_received(:sh).with(*args)
|
|
364
|
+
end
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
context "with a tiny release tag" do
|
|
368
|
+
let(:tag) { "v1.2.3.1" }
|
|
369
|
+
|
|
370
|
+
it "does not mark the release as a prerelease" do
|
|
371
|
+
release.create
|
|
372
|
+
args = ["gh", "release", "create", "--generate-notes", "--fail-on-no-commits", tag]
|
|
373
|
+
expect(release).to have_received(:sh).with(*args)
|
|
374
|
+
end
|
|
375
|
+
end
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
- [ ] **Step 2: Run to verify the dev context fails**
|
|
379
|
+
|
|
380
|
+
Run: `bundle exec rspec spec/gempilot/github_release_spec.rb --no-color`
|
|
381
|
+
Expected: FAIL — the dev-tag example receives no `--prerelease` argument; the tiny-tag example passes.
|
|
382
|
+
|
|
383
|
+
- [ ] **Step 3: Implement the prerelease flag**
|
|
384
|
+
|
|
385
|
+
In `lib/gempilot/github_release.rb`, replace `#create` and append a private section after `#list`:
|
|
386
|
+
|
|
387
|
+
```ruby
|
|
388
|
+
def create
|
|
389
|
+
sh "gh", "release", "create",
|
|
390
|
+
"--generate-notes", "--fail-on-no-commits",
|
|
391
|
+
*prerelease_flag,
|
|
392
|
+
tag
|
|
393
|
+
end
|
|
394
|
+
```
|
|
395
|
+
|
|
396
|
+
```ruby
|
|
397
|
+
private
|
|
398
|
+
|
|
399
|
+
def prerelease_flag
|
|
400
|
+
Gem::Version.new(tag.delete_prefix("v")).prerelease? ? ["--prerelease"] : []
|
|
401
|
+
end
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
Also extend the class doc block's first line to:
|
|
405
|
+
|
|
406
|
+
```ruby
|
|
407
|
+
## Manages GitHub releases for a version tag. Tags naming a prerelease
|
|
408
|
+
## version (e.g. +v1.2.4.dev1+) are created as GitHub prereleases.
|
|
409
|
+
```
|
|
410
|
+
|
|
411
|
+
- [ ] **Step 4: Run the spec, then the full gate**
|
|
412
|
+
|
|
413
|
+
Run: `bundle exec rspec spec/gempilot/github_release_spec.rb --no-color && bundle exec rake default 2>&1 | tail -8`
|
|
414
|
+
Expected: PASS everywhere, no offenses.
|
|
415
|
+
|
|
416
|
+
- [ ] **Step 5: Commit**
|
|
417
|
+
|
|
418
|
+
```bash
|
|
419
|
+
git add lib/gempilot/github_release.rb spec/gempilot/github_release_spec.rb
|
|
420
|
+
git commit -m "Mark dev GitHub releases as prereleases"
|
|
421
|
+
```
|
|
422
|
+
|
|
423
|
+
---
|
|
424
|
+
|
|
425
|
+
### Task 4: Documentation
|
|
426
|
+
|
|
427
|
+
**Files:**
|
|
428
|
+
- Modify: `README.md` (bump section ~line 77-85; version tasks table row ~line 116)
|
|
429
|
+
- Modify: `CLAUDE.md` (bump command bullet; architecture list)
|
|
430
|
+
|
|
431
|
+
**Interfaces:**
|
|
432
|
+
- Consumes: final semantics from Tasks 1-3.
|
|
433
|
+
- Produces: user-facing docs; no code.
|
|
434
|
+
|
|
435
|
+
- [ ] **Step 1: Update the README bump section**
|
|
436
|
+
|
|
437
|
+
Replace the `gempilot bump` code block and add one paragraph after it:
|
|
438
|
+
|
|
439
|
+
````markdown
|
|
440
|
+
```bash
|
|
441
|
+
gempilot bump # patch (default)
|
|
442
|
+
gempilot bump minor
|
|
443
|
+
gempilot bump major
|
|
444
|
+
gempilot bump tiny # fourth integer for tiny follow-ups: 0.2.0 -> 0.2.0.1
|
|
445
|
+
gempilot bump dev # preview of the next patch: 0.2.0 -> 0.2.1.dev1
|
|
446
|
+
```
|
|
447
|
+
|
|
448
|
+
A `dev` version previews the next patch, so it always sorts ahead of the
|
|
449
|
+
current release (`0.2.0 < 0.2.1.dev1 < 0.2.1`), and a numeric bump finalizes
|
|
450
|
+
the cycle (`gempilot bump` from `0.2.1.dev3` gives `0.2.1`). A repo holding an
|
|
451
|
+
old-style dev version — one created *after* its base version shipped — needs a
|
|
452
|
+
one-time hand edit of `version.rb` past the last published version.
|
|
453
|
+
````
|
|
454
|
+
|
|
455
|
+
- [ ] **Step 2: Update the README version tasks table row**
|
|
456
|
+
|
|
457
|
+
```markdown
|
|
458
|
+
| `rake version:bump` | Bump the version (patch default; major/minor/patch/tiny/dev) |
|
|
459
|
+
```
|
|
460
|
+
|
|
461
|
+
- [ ] **Step 3: Update CLAUDE.md**
|
|
462
|
+
|
|
463
|
+
Change the bump command bullet to:
|
|
464
|
+
|
|
465
|
+
```markdown
|
|
466
|
+
- `gempilot bump` — Bump version in `version.rb` (patch default, or minor/major/tiny/dev)
|
|
467
|
+
```
|
|
468
|
+
|
|
469
|
+
and add to the Architecture list, after the `GemConstant` bullet:
|
|
470
|
+
|
|
471
|
+
```markdown
|
|
472
|
+
- `SegmentedVersion` value object (`lib/gempilot/segmented_version.rb`) owns version parsing and bump arithmetic; every bump moves to the smallest version of the requested shape greater than the current version (RubyGems ordering)
|
|
473
|
+
```
|
|
474
|
+
|
|
475
|
+
- [ ] **Step 4: Full gate and commit**
|
|
476
|
+
|
|
477
|
+
Run: `bundle exec rake default 2>&1 | tail -8`
|
|
478
|
+
Expected: all green (docs only — this is a regression guard).
|
|
479
|
+
|
|
480
|
+
```bash
|
|
481
|
+
git add README.md CLAUDE.md
|
|
482
|
+
git commit -m "Update docs for forward-moving version bumps"
|
|
483
|
+
```
|