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
|
@@ -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
|
@@ -95,7 +95,7 @@ Id: 4EB6E51C-4322-11F1-B1B2-FE6CB9572C2F
|
|
|
95
95
|
Updated: Tue, 28 Apr 2026 12:50:05 -0400
|
|
96
96
|
Title: Existing git repo on gempilot create needs new commit message
|
|
97
97
|
Description: Currently it commits "Initial commit". Inappropriate for a git repo that already has a history
|
|
98
|
-
Status:
|
|
98
|
+
Status: closed
|
|
99
99
|
|
|
100
100
|
Id: 0411D4DA-4323-11F1-847C-FE6CB9572C2F
|
|
101
101
|
Updated: Tue, 28 Apr 2026 12:55:09 -0400
|
|
@@ -112,19 +112,19 @@ Description: e.g.
|
|
|
112
112
|
+ end
|
|
113
113
|
+
|
|
114
114
|
+
|
|
115
|
-
Status:
|
|
115
|
+
Status: closed
|
|
116
116
|
|
|
117
117
|
Id: 5618BB20-4325-11F1-BF5A-FE6CB9572C2F
|
|
118
118
|
Updated: Tue, 28 Apr 2026 13:11:46 -0400
|
|
119
119
|
Title: Move zeitwerk check task into gempilot
|
|
120
120
|
Description: Can't iterate on it when its not managed by gempilot. A new feature needs to be added
|
|
121
|
-
Status:
|
|
121
|
+
Status: closed
|
|
122
122
|
|
|
123
123
|
Id: 6BE168E4-4325-11F1-BE0F-FE6CB9572C2F
|
|
124
124
|
Updated: Tue, 28 Apr 2026 13:12:22 -0400
|
|
125
125
|
Title: Add zeitwerk:all task
|
|
126
126
|
Description: Add rake task that runs LOADER.all_expected_cpaths and prints in a nice table, similar to rails routes
|
|
127
|
-
Status:
|
|
127
|
+
Status: closed
|
|
128
128
|
|
|
129
129
|
Id: 56DB9634-4327-11F1-8D0F-FE6CB9572C2F
|
|
130
130
|
Updated: Fri, 05 Jun 2026 00:00:00 -0400
|
|
@@ -155,13 +155,13 @@ Description: When doing `gempilot new Reversal::Server`, in a project called "re
|
|
|
155
155
|
+ create test/reversal/store_test.rb
|
|
156
156
|
+
|
|
157
157
|
+ The test file is incorrectly named. Should be server_test.rb
|
|
158
|
-
Status:
|
|
158
|
+
Status: closed
|
|
159
159
|
|
|
160
160
|
Id: F5486B48-71DB-11F1-980F-FE6CB9572C2E
|
|
161
161
|
Updated: Fri, 26 Jun 2026 23:54:55 -0400
|
|
162
162
|
Title: Rubymine <=> Minitest usage causes "no reporters allowed error"
|
|
163
163
|
Description: Has to do with how the ENV var RM_INFO is being interpreted
|
|
164
|
-
Status:
|
|
164
|
+
Status: closed
|
|
165
165
|
|
|
166
166
|
Id: 8D847572-7273-11F1-B508-FE6CB9572C2E
|
|
167
167
|
Updated: Fri, 03 Jul 2026 22:38:22 +0000
|
|
@@ -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
|
|
|
@@ -97,9 +97,21 @@ module Gempilot
|
|
|
97
97
|
cd @gem_name do
|
|
98
98
|
sh "git", "init", "-q", "-b", @branch
|
|
99
99
|
sh "git", "add", "."
|
|
100
|
-
sh "git", "commit", "-q", "-m",
|
|
100
|
+
sh "git", "commit", "-q", "-m", commit_message
|
|
101
101
|
end
|
|
102
102
|
end
|
|
103
|
+
|
|
104
|
+
# "Initial commit." is wrong when scaffolding into a directory that is
|
|
105
|
+
# already a repository with history (e.g. a cloned remote), so name the
|
|
106
|
+
# commit after the gem in that case.
|
|
107
|
+
def commit_message
|
|
108
|
+
existing_history? ? "Add #{@gem_name} gem scaffolding." : "Initial commit."
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def existing_history?
|
|
112
|
+
system("git", "rev-parse", "--verify", "--quiet", "HEAD",
|
|
113
|
+
out: File::NULL, err: File::NULL)
|
|
114
|
+
end
|
|
103
115
|
end
|
|
104
116
|
end
|
|
105
117
|
end
|
|
@@ -30,18 +30,14 @@ module Gempilot
|
|
|
30
30
|
|
|
31
31
|
## Path to the constant's source file, e.g. +lib/my_gem/services/auth.rb+.
|
|
32
32
|
def lib_path
|
|
33
|
-
|
|
33
|
+
path_for("lib", ".rb")
|
|
34
34
|
end
|
|
35
35
|
|
|
36
36
|
## Path to the constant's test file for +framework+ (+:rspec+ or
|
|
37
|
-
## +:minitest+)
|
|
37
|
+
## +:minitest+). Mirrors +lib_path+ so the test always tracks the source,
|
|
38
|
+
## including for multi-segment (hyphenated) gem modules.
|
|
38
39
|
def test_path(framework)
|
|
39
|
-
|
|
40
|
-
if framework == :rspec
|
|
41
|
-
"#{File.join("spec", require_path, *rest)}_spec.rb"
|
|
42
|
-
else
|
|
43
|
-
"#{File.join("test", require_path, *rest)}_test.rb"
|
|
44
|
-
end
|
|
40
|
+
framework == :rspec ? path_for("spec", "_spec.rb") : path_for("test", "_test.rb")
|
|
45
41
|
end
|
|
46
42
|
|
|
47
43
|
private
|
|
@@ -57,5 +53,9 @@ module Gempilot
|
|
|
57
53
|
def path_segments
|
|
58
54
|
parts.map(&:underscore)
|
|
59
55
|
end
|
|
56
|
+
|
|
57
|
+
def path_for(root, suffix)
|
|
58
|
+
"#{File.join(root, *path_segments)}#{suffix}"
|
|
59
|
+
end
|
|
60
60
|
end
|
|
61
61
|
end
|
|
@@ -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
|
data/lib/gempilot/project.rb
CHANGED
|
@@ -33,8 +33,16 @@ module Gempilot
|
|
|
33
33
|
project_segments.join("-")
|
|
34
34
|
end
|
|
35
35
|
|
|
36
|
+
def require_path
|
|
37
|
+
project_segments.join("/")
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def module_name
|
|
41
|
+
project_segments.map(&:camelize).join("::")
|
|
42
|
+
end
|
|
43
|
+
|
|
36
44
|
def klass
|
|
37
|
-
Object.const_get(
|
|
45
|
+
Object.const_get(module_name)
|
|
38
46
|
end
|
|
39
47
|
|
|
40
48
|
def version
|
|
@@ -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
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
require "rake/tasklib"
|
|
2
|
+
require_relative "../gempilot"
|
|
3
|
+
|
|
4
|
+
module Gempilot
|
|
5
|
+
## Rake tasks for validating and inspecting a gem's Zeitwerk loader.
|
|
6
|
+
##
|
|
7
|
+
## Owned by gempilot and consumed by generated gems via
|
|
8
|
+
## <tt>require "gempilot/zeitwerk_task"; Gempilot::ZeitwerkTask.new</tt>, so
|
|
9
|
+
## the logic rolls forward on a gempilot bump instead of being copied into
|
|
10
|
+
## every gem's Rakefile. Each task boots a clean child process so eager
|
|
11
|
+
## loading surfaces naming errors without polluting the Rake process.
|
|
12
|
+
class ZeitwerkTask < Rake::TaskLib
|
|
13
|
+
attr_reader :project
|
|
14
|
+
|
|
15
|
+
def initialize(root: Dir.pwd)
|
|
16
|
+
super()
|
|
17
|
+
@project = Project.new(root)
|
|
18
|
+
define_tasks
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
private
|
|
22
|
+
|
|
23
|
+
def define_tasks
|
|
24
|
+
namespace :zeitwerk do
|
|
25
|
+
define_validate_task
|
|
26
|
+
define_all_task
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def define_validate_task
|
|
31
|
+
desc "Verify all files follow Zeitwerk naming conventions"
|
|
32
|
+
task(:validate) { ruby "-Ilib", "-e", validate_script }
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def define_all_task
|
|
36
|
+
desc "List every constant Zeitwerk manages and the file it expects"
|
|
37
|
+
task(:all) { ruby "-Ilib", "-e", all_script }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def loader
|
|
41
|
+
"#{project.module_name}::LOADER"
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def validate_script
|
|
45
|
+
<<~RUBY
|
|
46
|
+
require '#{project.require_path}'
|
|
47
|
+
#{loader}.eager_load(force: true)
|
|
48
|
+
puts 'Zeitwerk: All files loaded successfully.'
|
|
49
|
+
RUBY
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def all_script
|
|
53
|
+
<<~RUBY
|
|
54
|
+
require '#{project.require_path}'
|
|
55
|
+
rows = #{loader}.all_expected_cpaths.sort_by(&:last)
|
|
56
|
+
width = rows.map { |_path, cpath| cpath.length }.max || 0
|
|
57
|
+
rows.each { |path, cpath| puts format("%-\#{width}s %s", cpath, path) }
|
|
58
|
+
RUBY
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
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.
|
|
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,10 +134,12 @@ 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
|
|
136
141
|
- lib/gempilot/version_task.rb
|
|
142
|
+
- lib/gempilot/zeitwerk_task.rb
|
|
137
143
|
- vendor/vendored.gemv
|
|
138
144
|
homepage: https://github.com/gillisd/gempilot
|
|
139
145
|
licenses:
|
|
@@ -155,7 +161,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
|
155
161
|
- !ruby/object:Gem::Version
|
|
156
162
|
version: '0'
|
|
157
163
|
requirements: []
|
|
158
|
-
rubygems_version: 4.0.
|
|
164
|
+
rubygems_version: 4.0.18
|
|
159
165
|
specification_version: 4
|
|
160
166
|
summary: A toolkit for creating, managing, and releasing your own rubygems
|
|
161
167
|
test_files: []
|