gempilot 0.3.0 → 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/CLAUDE.md +2 -1
- data/README.md +9 -1
- 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 +90 -2
- data/lib/gempilot/cli/commands/bump.rb +5 -4
- data/lib/gempilot/github_release.rb +9 -1
- data/lib/gempilot/project/version.rb +4 -25
- data/lib/gempilot/segmented_version.rb +64 -0
- data/lib/gempilot/version.rb +1 -1
- data/lib/gempilot/version_task.rb +1 -1
- metadata +7 -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/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 |
|
|
@@ -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
|
+
```
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
# Dev version bump semantics — design
|
|
2
|
+
|
|
3
|
+
**Issue:** `2360FFA4-9388-11F1-8474-FE6CB9572C2F` — "rake version:bump[dev] goes backward rather than forward"
|
|
4
|
+
|
|
5
|
+
**Date:** 2026-08-09
|
|
6
|
+
|
|
7
|
+
## Problem
|
|
8
|
+
|
|
9
|
+
`Gempilot::Project::Version#bump(:dev)` appends `.dev1` to the *current* version
|
|
10
|
+
(`0.2.0` → `0.2.0.dev1`). RubyGems treats any version with a letter segment as a
|
|
11
|
+
**prerelease of the version it names** (`Gem::Version` rdoc: "Prereleases sort
|
|
12
|
+
between real releases (newest to oldest): 1.0 > 1.0.b1 > 1.0.a.2 > 0.9"), so
|
|
13
|
+
`0.2.0.dev1 < 0.2.0` — the bump moves the project *backward*. Two related gaps:
|
|
14
|
+
|
|
15
|
+
1. `bump(:patch)` from `X.Y.Z.devN` produces `X.Y.(Z+1)`, overshooting the
|
|
16
|
+
`X.Y.Z` the dev cycle was working toward.
|
|
17
|
+
2. There is no way to bump a fourth integer (`0.2.0` → `0.2.0.1`), the
|
|
18
|
+
rubygems-legal *non-prerelease* shape for tiny follow-up fixes on a released
|
|
19
|
+
version (the Rails security-release pattern, `6.0.3.1`).
|
|
20
|
+
|
|
21
|
+
## Decisions
|
|
22
|
+
|
|
23
|
+
- **Dev versions preview the next patch.** `bump(:dev)` from a release version
|
|
24
|
+
yields the next patch's first dev prerelease (`0.2.0` → `0.2.1.dev1`); from a
|
|
25
|
+
dev version it increments the counter (`0.2.1.dev1` → `0.2.1.dev2`). This is
|
|
26
|
+
the only reading consistent with rubygems ordering, and matches ecosystem
|
|
27
|
+
practice (Rails `7.1.0.beta1` precedes `7.1.0`; Bundler's in-repo `.dev`).
|
|
28
|
+
- **One rule governs every bump: move to the smallest version of the requested
|
|
29
|
+
shape that is strictly greater than the current version.** Consequences:
|
|
30
|
+
numeric bumps from a dev version *finalize* the cycle (`0.2.1.dev3` +
|
|
31
|
+
`patch` → `0.2.1`, not `0.2.2`), and `minor`/`major` redirect it
|
|
32
|
+
(`0.2.1.dev3` + `minor` → `0.3.0`).
|
|
33
|
+
- **New `tiny` segment for the fourth integer.** `bump(:tiny)`: `0.2.0` →
|
|
34
|
+
`0.2.0.1` → `0.2.0.2`. Name chosen by elimination: semver's "build metadata"
|
|
35
|
+
is precedence-ignored (and the `Gem::Version` rdoc uses "build" for the third
|
|
36
|
+
integer); `micro` is Python's third digit; `patchlevel` is Ruby-historical but
|
|
37
|
+
long and confusable with `patch`. `tiny` reads as "smaller than patch", which
|
|
38
|
+
is where it sorts. (Old Rails used TINY for the third digit; accepted.)
|
|
39
|
+
- **No dev target argument** (`bump[dev,minor]` → `0.3.0.dev1`) — YAGNI. The
|
|
40
|
+
shape rule already finalizes hand-edited `M.m.0.devN` / `M.0.0.devN` states
|
|
41
|
+
correctly, so a targeted form can be layered on later without breaking
|
|
42
|
+
anything.
|
|
43
|
+
- **Dev GitHub releases are prereleases.** `release:github` passes
|
|
44
|
+
`--prerelease` to `gh release create` when the tag names a prerelease
|
|
45
|
+
version, so `v0.2.1.dev2` does not become the repo's "Latest" release.
|
|
46
|
+
- **Strict version parsing.** `Version#bump` recognizes exactly three forms —
|
|
47
|
+
`M.m.p`, `M.m.p.N`, `M.m.p.devN` — and raises `ArgumentError` for anything
|
|
48
|
+
else, instead of today's silent first-three-integers coercion.
|
|
49
|
+
|
|
50
|
+
## Semantics
|
|
51
|
+
|
|
52
|
+
Accepted version forms: release `M.m.p`, tiny release `M.m.p.N`, dev prerelease
|
|
53
|
+
`M.m.p.devN` (parse: `/\A(\d+)\.(\d+)\.(\d+)(?:\.(\d+)|\.dev(\d+))?\z/`).
|
|
54
|
+
|
|
55
|
+
Full transition table (rows marked * arise only by hand-editing; the rule is
|
|
56
|
+
total over all three forms regardless of how the version got there):
|
|
57
|
+
|
|
58
|
+
| from ↓ bump → | `major` | `minor` | `patch` | `tiny` | `dev` |
|
|
59
|
+
| ------------- | ------- | ------- | ------- | --------- | ------------ |
|
|
60
|
+
| `1.2.3` | `2.0.0` | `1.3.0` | `1.2.4` | `1.2.3.1` | `1.2.4.dev1` |
|
|
61
|
+
| `1.2.3.1` | `2.0.0` | `1.3.0` | `1.2.4` | `1.2.3.2` | `1.2.4.dev1` |
|
|
62
|
+
| `1.2.4.dev2` | `2.0.0` | `1.3.0` | `1.2.4` | `1.2.4.1` | `1.2.4.dev3` |
|
|
63
|
+
| `1.3.0.dev2`* | `2.0.0` | `1.3.0` | `1.3.0` | `1.3.0.1` | `1.3.0.dev3` |
|
|
64
|
+
| `2.0.0.dev2`* | `2.0.0` | `2.0.0` | `2.0.0` | `2.0.0.1` | `2.0.0.dev3` |
|
|
65
|
+
|
|
66
|
+
All 25 transitions verified strictly increasing under `Gem::Version` (checked
|
|
67
|
+
2026-08-09 against ruby's installed rubygems).
|
|
68
|
+
|
|
69
|
+
Branch logic per segment, where the numeric core is `M.m.p` and `dev?` means
|
|
70
|
+
the current version is a dev prerelease:
|
|
71
|
+
|
|
72
|
+
- `major`: `dev? && m == 0 && p == 0` → `M.0.0` (finalize); else `(M+1).0.0`
|
|
73
|
+
- `minor`: `dev? && p == 0` → `M.m.0` (finalize); else `M.(m+1).0`
|
|
74
|
+
- `patch`: `dev?` → `M.m.p` (finalize); else `M.m.(p+1)`
|
|
75
|
+
- `tiny`: `M.m.p.(N+1)` where `N` is the current fourth integer, else 0.
|
|
76
|
+
(From a dev version this yields `M.m.p.1` — it implicitly finalizes the
|
|
77
|
+
target and adds a tiny level; forward-moving, documented edge case.)
|
|
78
|
+
- `dev`: `dev?` → `M.m.p.dev(N+1)`; else `M.m.(p+1).dev1`
|
|
79
|
+
|
|
80
|
+
## Components
|
|
81
|
+
|
|
82
|
+
### `Gempilot::Project::Version` (modified — `lib/gempilot/project/version.rb`)
|
|
83
|
+
|
|
84
|
+
- Stays `Data.define(:path, :value)` — `write_version!`, `VersionTag`, and all
|
|
85
|
+
call sites are untouched. Parsing is internal to `bump`.
|
|
86
|
+
- `bump(segment)` accepts `:major, :minor, :patch, :tiny, :dev` (symbol or
|
|
87
|
+
string), implements the table above. Unknown segment → `ArgumentError`
|
|
88
|
+
naming the valid segments (as today, plus `tiny`). Unparseable value →
|
|
89
|
+
`ArgumentError` naming the value.
|
|
90
|
+
- `next_version` alias retained; its behavior follows the new `patch` default
|
|
91
|
+
(`0.0.4.dev3` → `0.0.4`, previously `0.0.5`).
|
|
92
|
+
|
|
93
|
+
### `Gempilot::GithubRelease` (modified — `lib/gempilot/github_release.rb`)
|
|
94
|
+
|
|
95
|
+
- `#create` appends `--prerelease` when the tag (sans leading `v`) parses as a
|
|
96
|
+
prerelease per `Gem::Version#prerelease?`. Self-contained — the
|
|
97
|
+
`initialize(tag)` signature and all call sites are unchanged. `#destroy` /
|
|
98
|
+
`#list` untouched.
|
|
99
|
+
|
|
100
|
+
### `Gempilot::VersionTask` (modified — `lib/gempilot/version_task.rb`)
|
|
101
|
+
|
|
102
|
+
- Behavior unchanged (it forwards the segment). Only the `version:bump` desc
|
|
103
|
+
string updates: "Bump version (patch default; segments: major, minor, patch,
|
|
104
|
+
tiny, dev)".
|
|
105
|
+
|
|
106
|
+
### `Gempilot::CLI::Commands::Bump` (modified — `lib/gempilot/cli/commands/bump.rb`)
|
|
107
|
+
|
|
108
|
+
- Allowed segments become `%w[patch minor major tiny dev]`; error message,
|
|
109
|
+
argument desc, command description, and `examples` updated to match.
|
|
110
|
+
|
|
111
|
+
## Testing (TDD — written before implementation)
|
|
112
|
+
|
|
113
|
+
**`spec/gempilot/project/version_spec.rb` (rewritten)**
|
|
114
|
+
|
|
115
|
+
- The full 5×5 transition table above, each cell an example.
|
|
116
|
+
- A monotonicity property spec: for every table cell,
|
|
117
|
+
`Gem::Version.new(new) > Gem::Version.new(old)` — the direct regression spec
|
|
118
|
+
for this issue.
|
|
119
|
+
- Segment as string; unknown segment `ArgumentError`; unparseable value
|
|
120
|
+
(`"1.2"`, `"1.2.3.beta1"`) `ArgumentError`; `next_version` follows patch
|
|
121
|
+
semantics (`0.0.4.dev3` → `0.0.4`).
|
|
122
|
+
|
|
123
|
+
**`spec/gempilot/version_task_spec.rb` (expectations updated)**
|
|
124
|
+
|
|
125
|
+
Fixture starts at `1.0.0.dev3`:
|
|
126
|
+
- `version:bump` (default patch) → `1.0.0` (finalize; was `1.0.1`).
|
|
127
|
+
- `version:bump[dev]` → `1.0.0.dev4` (unchanged).
|
|
128
|
+
- `version:bump[tiny]` → `1.0.0.1` (new example).
|
|
129
|
+
- `version:release[dev]` → commit "Bump version to 1.0.0.dev4", tag
|
|
130
|
+
`v1.0.0.dev4` (unchanged).
|
|
131
|
+
|
|
132
|
+
**`spec/gempilot/github_release_spec.rb` (extended)**
|
|
133
|
+
|
|
134
|
+
- `create` with `v1.2.4.dev1` includes `--prerelease`; with `v1.2.4` and
|
|
135
|
+
`v1.2.3.1` it does not.
|
|
136
|
+
|
|
137
|
+
**`test/gempilot/cli/bump_command_test.rb` (extended)**
|
|
138
|
+
|
|
139
|
+
- `tiny` accepted as a segment; unknown-segment error message lists it.
|
|
140
|
+
|
|
141
|
+
## Docs
|
|
142
|
+
|
|
143
|
+
- `README.md` — `gempilot bump` section gains `tiny` and `dev` examples with a
|
|
144
|
+
one-line explanation of dev semantics; the rake tasks table row for
|
|
145
|
+
`version:bump` mentions the segments.
|
|
146
|
+
- `CLAUDE.md` — version lifecycle bullet: "(patch default, or
|
|
147
|
+
minor/major/tiny/dev)".
|
|
148
|
+
|
|
149
|
+
## Migration
|
|
150
|
+
|
|
151
|
+
A repo sitting on an old-style dev version (e.g. `0.2.0.dev1` created *after*
|
|
152
|
+
`0.2.0` shipped) is already in the broken state this fixes; `bump(:patch)`
|
|
153
|
+
would finalize to the already-released `0.2.0` (and `version:tag` would then
|
|
154
|
+
fail on the existing tag). Recovery is a one-time hand edit of `version.rb`
|
|
155
|
+
past the last published version. Noted in the README dev-example line.
|
|
156
|
+
|
|
157
|
+
## Out of scope
|
|
158
|
+
|
|
159
|
+
- Targeted dev cycles (`bump[dev,minor]`) — compatible later extension.
|
|
160
|
+
- Other prerelease markers (`pre`, `rc`, `beta`) — `dev` sorts before all of
|
|
161
|
+
them, so they remain addable later.
|
|
162
|
+
- RubyGems prerelease yanking; `data/templates/gem/Rakefile.erb` (no change
|
|
163
|
+
needed — it just instantiates `Gempilot::VersionTask`).
|
data/issues.rec
CHANGED
|
@@ -110,8 +110,8 @@ Description: e.g.
|
|
|
110
110
|
+ Zeitwerk::Loader.for_gem.tap do |l|
|
|
111
111
|
+ l.setup
|
|
112
112
|
+ end
|
|
113
|
-
+
|
|
114
|
-
+
|
|
113
|
+
+
|
|
114
|
+
+
|
|
115
115
|
Status: closed
|
|
116
116
|
|
|
117
117
|
Id: 5618BB20-4325-11F1-BF5A-FE6CB9572C2F
|
|
@@ -212,3 +212,91 @@ Description: the github release related tasks are invoked as follows:
|
|
|
212
212
|
+
|
|
213
213
|
+ Also, there is an issue that prevents github releasing from working at all via the old tasks. I get an error every time saying that "tags have already been pushed" or something along those lines. Ensure that is fixed as well
|
|
214
214
|
Status: closed
|
|
215
|
+
|
|
216
|
+
Id: 986E0100-88F4-11F1-B718-FE6CB9572C2D
|
|
217
|
+
Updated: Sun, 26 Jul 2026 09:19:13 -0400
|
|
218
|
+
Title: Make rbs and the other gems GP installs mri only
|
|
219
|
+
Description: This tool is also used with jruby, and it fails irb install due to rbs
|
|
220
|
+
Status: open
|
|
221
|
+
|
|
222
|
+
Id: 3FFE7616-88F5-11F1-8D3B-FE6CB9572C2D
|
|
223
|
+
Updated: Sun, 26 Jul 2026 09:23:54 -0400
|
|
224
|
+
Title: Add betterleaks to gem setup
|
|
225
|
+
Description: Ensure betterleaks is integrated into the commit and build pipelines, via commit hooks, rake tasks, and/or whatever the idiomatic way is to integrate betterleaks into a rubyproject
|
|
226
|
+
Status: open
|
|
227
|
+
|
|
228
|
+
Id: F6C6EB04-8DA4-11F1-91EF-FA41C3164F3A
|
|
229
|
+
Updated: Sat, 01 Aug 2026 12:31:47 +0000
|
|
230
|
+
Title: release:rubygem_push only handles one gem per version
|
|
231
|
+
Description: rake release:rubygems can publish only one gem per version. A project that publishes several -- platform gems each carrying a compiled executable, say -- either publishes one of them and silently leaves the rest, or, once it replaces the push task with a pkg glob, tries to re-push versions already on rubygems and stops halfway through the release.
|
|
232
|
+
+
|
|
233
|
+
+ MECHANISM
|
|
234
|
+
+
|
|
235
|
+
+ gempilot, lib/gempilot/release_tasks.rb: ReleaseTasks#define_rubygems_release wires release:rubygems to [build, release:guard_clean, release:source_control_push, release:rubygem_push], and, as the module's own rdoc says, expects bundler/gem_tasks to have supplied build, release:guard_clean and release:rubygem_push.
|
|
236
|
+
+
|
|
237
|
+
+ bundler 4.0.17, lib/bundler/gem_helper.rb: GemHelper#install_tasks defines
|
|
238
|
+
+
|
|
239
|
+
+ task "release:rubygem_push" => "build" do
|
|
240
|
+
+ rubygem_push(built_gem_path) if gem_push?
|
|
241
|
+
+ end
|
|
242
|
+
+
|
|
243
|
+
+ built_gem_path is the single .gem that #build_gem produced. One build, one gem, one push. Nothing in that chain can carry a second gem, so a project publishing more than one has to replace both build and release:rubygem_push itself.
|
|
244
|
+
+
|
|
245
|
+
+ There is no glob in gempilot's own source to go looking for. The glob is what a downstream project ends up writing once it takes over the push, and it is wrong in a way gempilot's own version:bump triggers.
|
|
246
|
+
+
|
|
247
|
+
+ REPRODUCE
|
|
248
|
+
+
|
|
249
|
+
+ 1. A gem publishing more than one gem per version. aws-session-manager on gempilot 0.3.0 publishes five: x86_64-linux, aarch64-linux, x86_64-darwin and arm64-darwin, each carrying a compiled session-manager-plugin executable, plus the plain ruby gem.
|
|
250
|
+
+ 2. It replaces build, and replaces the push with the obvious implementation:
|
|
251
|
+
+ task(:rubygem_push) { PACKAGES.glob("*.gem").each { sh "gem", "push", it.to_s } }
|
|
252
|
+
+ 3. rake build && rake release:rubygems -- 0.0.1 publishes.
|
|
253
|
+
+ 4. rake version:bump && rake build -- pkg/ now holds both 0.0.1 and 0.0.2 gems. Nothing empties pkg/ between versions; bundler never has, and gempilot's version tasks do not either.
|
|
254
|
+
+ 5. rake release:rubygems -- gem push rejects the 0.0.1 gems ("Repushing of gem versions is not allowed"), sh raises on the non-zero status, and the release aborts with part of 0.0.2 published and the rest not. Re-running repeats the failure, because the stale gems are still there.
|
|
255
|
+
+
|
|
256
|
+
+ FIX APPLIED DOWNSTREAM
|
|
257
|
+
+
|
|
258
|
+
+ aws-session-manager stopped globbing and took the list from the specifications its build packages, so that gem:build and the push share one source of truth:
|
|
259
|
+
+
|
|
260
|
+
+ PACKAGED = [*BUILDS.map { it.package.to_s }, GENERIC_PACKAGE].freeze
|
|
261
|
+
+
|
|
262
|
+
+ namespace :gem do
|
|
263
|
+
+ task build: PACKAGED
|
|
264
|
+
+ end
|
|
265
|
+
+
|
|
266
|
+
+ namespace :release do
|
|
267
|
+
+ task(:rubygem_push) { PACKAGED.each { sh "gem", "push", it } }
|
|
268
|
+
+ end
|
|
269
|
+
+
|
|
270
|
+
+ FIX SUGGESTED HERE
|
|
271
|
+
+
|
|
272
|
+
+ gempilot should own release:rubygem_push rather than inherit bundler's, and push every gem in pkg belonging to the version being released. It cannot know how a project builds, but it knows the name and the version, which is enough:
|
|
273
|
+
+
|
|
274
|
+
+ def define_rubygem_push(project)
|
|
275
|
+
+ clear_task "release:rubygem_push"
|
|
276
|
+
+ task "release:rubygem_push" => "build" do
|
|
277
|
+
+ packages(project).each { sh "gem", "push", it.to_s }
|
|
278
|
+
+ end
|
|
279
|
+
+ end
|
|
280
|
+
+
|
|
281
|
+
+ def packages(project)
|
|
282
|
+
+ stem = "#{project.name}-#{project.version_value}"
|
|
283
|
+
+ project.root.glob("pkg/{#{stem}.gem,#{stem}-*.gem}")
|
|
284
|
+
+ end
|
|
285
|
+
+
|
|
286
|
+
+ Project#name, #version_value and #root already exist in lib/gempilot/project.rb.
|
|
287
|
+
+
|
|
288
|
+
+ Two details worth keeping:
|
|
289
|
+
+
|
|
290
|
+
+ - The brace glob matches gempilot-0.3.0.gem and gempilot-0.3.0-x86_64-linux.gem, and does not match gempilot-0.3.10.gem. A bare "#{stem}*.gem" would match the latter and push the wrong version.
|
|
291
|
+
+ - An empty result should abort. Otherwise a project whose build writes gems elsewhere gets a release that reports success having published nothing, which is worse than the bug being fixed.
|
|
292
|
+
+
|
|
293
|
+
+ Single-gem projects are unaffected: pkg holds one gem for the current version, and it gets pushed, as today. Multi-gem projects stop having to reinvent the push.
|
|
294
|
+
+
|
|
295
|
+
+ If gempilot would rather not own the task, then the ReleaseTasks rdoc should state the contract instead: a project that replaces build must also replace release:rubygem_push, and must scope the push to the version being released rather than globbing pkg.
|
|
296
|
+
Status: open
|
|
297
|
+
|
|
298
|
+
Id: 2360FFA4-9388-11F1-8474-FE6CB9572C2F
|
|
299
|
+
Updated: Sat, 08 Aug 2026 20:20:34 -0400
|
|
300
|
+
Title: rake version:bump[dev] goes backward rather than forward
|
|
301
|
+
Description: rubygems treats 0.2.0.dev as an *older* version than 0.2.0. The way gempilot handles dev version needs to be rethought. Additionally, how does a user trigger a bump on the fourth integer, e.g. 0.2.0.1?
|
|
302
|
+
Status: open
|
|
@@ -8,18 +8,19 @@ module Gempilot
|
|
|
8
8
|
include GemContext
|
|
9
9
|
|
|
10
10
|
usage "[options] [SEGMENT]"
|
|
11
|
-
description "Bump the gem version (patch by default, or minor/major/dev)"
|
|
11
|
+
description "Bump the gem version (patch by default, or minor/major/tiny/dev)"
|
|
12
12
|
|
|
13
13
|
examples [
|
|
14
14
|
"",
|
|
15
15
|
"patch",
|
|
16
16
|
"minor",
|
|
17
17
|
"major",
|
|
18
|
+
"tiny",
|
|
18
19
|
"dev",
|
|
19
20
|
]
|
|
20
21
|
|
|
21
22
|
argument :segment, required: false,
|
|
22
|
-
desc: "Version segment to bump: patch (default), minor, major, or dev"
|
|
23
|
+
desc: "Version segment to bump: patch (default), minor, major, tiny, or dev"
|
|
23
24
|
|
|
24
25
|
def run(segment = "patch")
|
|
25
26
|
detect_gem_context
|
|
@@ -31,9 +32,9 @@ module Gempilot
|
|
|
31
32
|
|
|
32
33
|
def validate_segment(segment)
|
|
33
34
|
segment = segment.downcase
|
|
34
|
-
return segment if %w[patch minor major dev].include?(segment)
|
|
35
|
+
return segment if %w[patch minor major tiny dev].include?(segment)
|
|
35
36
|
|
|
36
|
-
puts colors.red("Unknown segment '#{segment}'. Use patch, minor, major, or dev.")
|
|
37
|
+
puts colors.red("Unknown segment '#{segment}'. Use patch, minor, major, tiny, or dev.")
|
|
37
38
|
exit 1
|
|
38
39
|
end
|
|
39
40
|
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
module Gempilot
|
|
2
|
-
## Manages GitHub releases for a version tag.
|
|
2
|
+
## Manages GitHub releases for a version tag. Tags naming a prerelease
|
|
3
|
+
## version (e.g. +v1.2.4.dev1+) are created as GitHub prereleases.
|
|
3
4
|
class GithubRelease
|
|
4
5
|
include StrictShell
|
|
5
6
|
|
|
@@ -12,6 +13,7 @@ module Gempilot
|
|
|
12
13
|
def create
|
|
13
14
|
sh "gh", "release", "create",
|
|
14
15
|
"--generate-notes", "--fail-on-no-commits",
|
|
16
|
+
*prerelease_flag,
|
|
15
17
|
tag
|
|
16
18
|
end
|
|
17
19
|
|
|
@@ -24,5 +26,11 @@ module Gempilot
|
|
|
24
26
|
def list
|
|
25
27
|
sh "gh", "release", "list"
|
|
26
28
|
end
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
def prerelease_flag
|
|
33
|
+
Gem::Version.new(tag.delete_prefix("v")).prerelease? ? ["--prerelease"] : []
|
|
34
|
+
end
|
|
27
35
|
end
|
|
28
36
|
end
|
|
@@ -1,36 +1,15 @@
|
|
|
1
1
|
module Gempilot
|
|
2
2
|
class Project
|
|
3
|
+
## The project's version file (+path+) and value. Bump arithmetic
|
|
4
|
+
## delegates to SegmentedVersion, so every bump moves strictly forward
|
|
5
|
+
## under RubyGems ordering.
|
|
3
6
|
Version = Data.define(:path, :value) do
|
|
4
7
|
def tag
|
|
5
8
|
"v#{value}"
|
|
6
9
|
end
|
|
7
10
|
|
|
8
11
|
def bump(segment = :patch)
|
|
9
|
-
|
|
10
|
-
when :major, :minor, :patch then bump_semver(segment)
|
|
11
|
-
when :dev then bump_dev
|
|
12
|
-
else raise ArgumentError, "Unknown segment #{segment.inspect}. Use :major, :minor, :patch, or :dev"
|
|
13
|
-
end
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
private
|
|
17
|
-
|
|
18
|
-
def bump_semver(segment)
|
|
19
|
-
major, minor, patch = value.split(".").first(3).map(&:to_i)
|
|
20
|
-
new_value = case segment.to_sym
|
|
21
|
-
when :major then "#{major + 1}.0.0"
|
|
22
|
-
when :minor then "#{major}.#{minor + 1}.0"
|
|
23
|
-
when :patch then "#{major}.#{minor}.#{patch + 1}"
|
|
24
|
-
end
|
|
25
|
-
with(value: new_value)
|
|
26
|
-
end
|
|
27
|
-
|
|
28
|
-
def bump_dev
|
|
29
|
-
if value.match?(/\.dev\d+\z/)
|
|
30
|
-
with(value: value.sub(/\d+\z/) { it.to_i + 1 })
|
|
31
|
-
else
|
|
32
|
-
with(value: "#{value}.dev1")
|
|
33
|
-
end
|
|
12
|
+
with(value: SegmentedVersion.parse(value).bump(segment).to_s)
|
|
34
13
|
end
|
|
35
14
|
|
|
36
15
|
alias_method :next_version, :bump
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
module Gempilot
|
|
2
|
+
## Arithmetic over the three version shapes gempilot accepts: release
|
|
3
|
+
## (+1.2.3+), tiny release (+1.2.3.1+), and dev prerelease (+1.2.3.dev1+).
|
|
4
|
+
##
|
|
5
|
+
## Every bump returns the smallest version of the requested shape that is
|
|
6
|
+
## strictly greater than the current version under RubyGems ordering, so
|
|
7
|
+
## bumping always moves a project forward: a dev bump previews the next
|
|
8
|
+
## patch (+1.2.3+ to +1.2.4.dev1+) and numeric bumps finalize a dev cycle
|
|
9
|
+
## (+1.2.4.dev2+ to +1.2.4+ for +:patch+).
|
|
10
|
+
SegmentedVersion = Data.define(:major, :minor, :patch, :tiny, :dev) do
|
|
11
|
+
def self.parse(string)
|
|
12
|
+
format = /\A(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)(?:\.(?<tiny>\d+)|\.dev(?<dev>\d+))?\z/
|
|
13
|
+
match = format.match(string)
|
|
14
|
+
raise ArgumentError, "cannot parse version: #{string.inspect}" unless match
|
|
15
|
+
|
|
16
|
+
new(**match.named_captures.to_h { |name, digits| [name.to_sym, digits&.to_i] })
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def bump(segment)
|
|
20
|
+
case segment.to_sym
|
|
21
|
+
when :major then bump_major
|
|
22
|
+
when :minor then bump_minor
|
|
23
|
+
when :patch then bump_patch
|
|
24
|
+
when :tiny then bump_tiny
|
|
25
|
+
when :dev then bump_dev
|
|
26
|
+
else raise ArgumentError, "Unknown segment #{segment.inspect}. Use :major, :minor, :patch, :tiny, or :dev"
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def to_s
|
|
31
|
+
[major, minor, patch, tiny, ("dev#{dev}" if dev)].compact.join(".")
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def bump_major
|
|
37
|
+
return finalize if dev && minor.zero? && patch.zero?
|
|
38
|
+
|
|
39
|
+
with(major: major + 1, minor: 0, patch: 0, tiny: nil, dev: nil)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def bump_minor
|
|
43
|
+
return finalize if dev && patch.zero?
|
|
44
|
+
|
|
45
|
+
with(minor: minor + 1, patch: 0, tiny: nil, dev: nil)
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def bump_patch
|
|
49
|
+
dev ? finalize : with(patch: patch + 1, tiny: nil)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def bump_tiny
|
|
53
|
+
with(tiny: (tiny || 0) + 1, dev: nil)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def bump_dev
|
|
57
|
+
dev ? with(dev: dev + 1) : with(patch: patch + 1, tiny: nil, dev: 1)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def finalize
|
|
61
|
+
with(tiny: nil, dev: nil)
|
|
62
|
+
end
|
|
63
|
+
end.freeze
|
|
64
|
+
end
|
data/lib/gempilot/version.rb
CHANGED
|
@@ -50,7 +50,7 @@ module Gempilot
|
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
def define_bump_task(project)
|
|
53
|
-
desc "Bump version (patch default,
|
|
53
|
+
desc "Bump version (patch default; segments: major, minor, patch, tiny, dev)"
|
|
54
54
|
task :bump, [:segment] do |_t, args|
|
|
55
55
|
segment = (args[:segment] || :patch).to_sym
|
|
56
56
|
old_version = project.version
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: gempilot
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.3.
|
|
4
|
+
version: 0.3.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- David Gillis
|
|
@@ -72,6 +72,8 @@ executables:
|
|
|
72
72
|
extensions: []
|
|
73
73
|
extra_rdoc_files: []
|
|
74
74
|
files:
|
|
75
|
+
- ".claude/memory/be-decisive-not-menus.md"
|
|
76
|
+
- ".claude/memory/flagship-shared-rubocop-config-state.md"
|
|
75
77
|
- ".claude/skills/using-command-kit/SKILL.md"
|
|
76
78
|
- ".claude/skills/using-command-kit/cli-example.rb"
|
|
77
79
|
- ".claude/skills/using-command-kit/generator-pattern.rb"
|
|
@@ -107,7 +109,9 @@ files:
|
|
|
107
109
|
- data/templates/new/.keep
|
|
108
110
|
- data/templates/new/command.rb.erb
|
|
109
111
|
- docs/superpowers/plans/2026-07-20-release-task-hierarchy.md
|
|
112
|
+
- docs/superpowers/plans/2026-08-09-dev-version-bump.md
|
|
110
113
|
- docs/superpowers/specs/2026-07-20-release-task-hierarchy-design.md
|
|
114
|
+
- docs/superpowers/specs/2026-08-09-dev-version-bump-design.md
|
|
111
115
|
- exe/gempilot
|
|
112
116
|
- issues.rec
|
|
113
117
|
- lib/core_ext/string/inflection_methods.rb
|
|
@@ -130,6 +134,7 @@ files:
|
|
|
130
134
|
- lib/gempilot/project.rb
|
|
131
135
|
- lib/gempilot/project/version.rb
|
|
132
136
|
- lib/gempilot/release_tasks.rb
|
|
137
|
+
- lib/gempilot/segmented_version.rb
|
|
133
138
|
- lib/gempilot/strict_shell.rb
|
|
134
139
|
- lib/gempilot/version.rb
|
|
135
140
|
- lib/gempilot/version_tag.rb
|
|
@@ -156,7 +161,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
156
161
|
- !ruby/object:Gem::Version
|
|
157
162
|
version: '0'
|
|
158
163
|
requirements: []
|
|
159
|
-
rubygems_version: 4.0.
|
|
164
|
+
rubygems_version: 4.0.18
|
|
160
165
|
specification_version: 4
|
|
161
166
|
summary: A toolkit for creating, managing, and releasing your own rubygems
|
|
162
167
|
test_files: []
|