gempilot 0.2.2 → 0.2.4
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/.rubocop.yml +15 -1
- data/CLAUDE.md +2 -9
- data/README.md +8 -4
- data/data/templates/gem/dotfiles/rubocop.yml.erb +1 -12
- data/docs/superpowers/plans/2026-07-20-release-task-hierarchy.md +583 -0
- data/docs/superpowers/specs/2026-07-20-release-task-hierarchy-design.md +229 -0
- data/issues.rec +74 -3
- data/lib/gempilot/cli/commands/console.rb +2 -3
- data/lib/gempilot/cli/commands/create.rb +6 -3
- data/lib/gempilot/cli/commands/new.rb +2 -3
- data/lib/gempilot/cli/commands/release.rb +2 -3
- data/lib/gempilot/cli/generator.rb +2 -1
- data/lib/gempilot/github_release.rb +0 -2
- data/lib/gempilot/origin.rb +52 -0
- data/lib/gempilot/project.rb +25 -5
- data/lib/gempilot/release_tasks.rb +73 -0
- data/lib/gempilot/version.rb +1 -1
- data/lib/gempilot/version_task.rb +3 -16
- data/vendor/vendored.gemv +0 -0
- metadata +8 -14
- data/docs/command_kit_comparison.md +0 -249
- data/docs/command_kit_reference.md +0 -517
- data/docs/plans/2026-02-18-gempilot-add-command.md +0 -718
- data/docs/superpowers/plans/2026-04-01-rubocop-new-config.md +0 -838
- data/docs/superpowers/plans/2026-04-06-dogfood-inflectable.md +0 -659
- data/docs/superpowers/plans/2026-04-06-inflection-tests-and-erb-rename.md +0 -166
- data/docs/superpowers/plans/2026-04-06-integrate-version-tools.md +0 -162
- data/docs/superpowers/plans/2026-04-06-new-readme.md +0 -185
- data/docs/superpowers/plans/2026-06-09-address-review-critiques.md +0 -679
- data/docs/version-management-redesign.md +0 -44
- data/notes.md +0 -31
|
@@ -0,0 +1,583 @@
|
|
|
1
|
+
# Release Task Hierarchy 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:** Move GitHub publishing out of the `version:github:*` namespace into the idiomatic `release` / `unrelease` namespace, and fix GitHub releasing (bundler's `already_tagged?` guard silently skips the git push).
|
|
6
|
+
|
|
7
|
+
**Architecture:** A new `Gempilot::Origin` object does an idempotent commit+tag push and replaces bundler's buggy `release:source_control_push`. RubyGems publishing reuses bundler's own `build` / `release:guard_clean` / `release:rubygem_push` leaf tasks via prerequisites; GitHub publishing uses the existing (slimmed) `GithubRelease`. All task wiring lives in a new `Gempilot::ReleaseTasks` module mixed into `VersionTask`. Bare `rake release` is a pure prerequisite composite over both remotes — no arguments, no dispatcher code.
|
|
8
|
+
|
|
9
|
+
**Tech Stack:** Ruby, Rake (`Rake::TaskLib`, `bundler/gem_tasks`), Zeitwerk autoloading, RSpec (unit specs) + Minitest (generator tests), RuboCop (`rubocop-claude`/`-rspec`/`-performance`/`-rake`/`-design`).
|
|
10
|
+
|
|
11
|
+
**Spec:** `docs/superpowers/specs/2026-07-20-release-task-hierarchy-design.md`
|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## Preconditions
|
|
16
|
+
|
|
17
|
+
- Branch `release-task-hierarchy` is checked out.
|
|
18
|
+
- `bundle install` works and the baseline is green:
|
|
19
|
+
- `bundle exec rake spec` → `106 examples, 0 failures`
|
|
20
|
+
- `bundle exec rake test` → `108 runs, 0 failures`
|
|
21
|
+
- `bundle exec rubocop` → `no offenses detected`
|
|
22
|
+
- If `bundle install` fails with `Bundler::Plugin::Index::SourceConflict (Source(s) 'vault' ...)`, align the local plugin index to the installed vault plugin version, then reinstall:
|
|
23
|
+
```bash
|
|
24
|
+
ruby -e 'f=".bundle/plugin/index"; File.write(f, File.read(f).gsub("bundler-source-vault-0.1.2","bundler-source-vault-0.1.5"))'
|
|
25
|
+
bundle install
|
|
26
|
+
```
|
|
27
|
+
(`.bundle/` is not tracked by git — this is a local-environment fix only.)
|
|
28
|
+
|
|
29
|
+
## Final target — task interface
|
|
30
|
+
|
|
31
|
+
| Task | Behavior |
|
|
32
|
+
| --- | --- |
|
|
33
|
+
| `rake release` | Publish current version to all remotes (RubyGems + GitHub) |
|
|
34
|
+
| `rake release:rubygems` | Build + push the gem to RubyGems |
|
|
35
|
+
| `rake release:github` | Push commit + tag, then create the GitHub release |
|
|
36
|
+
| `rake release:list:github` | List GitHub releases |
|
|
37
|
+
| `rake unrelease` | Delete the release from all remotes that support it (GitHub) |
|
|
38
|
+
| `rake unrelease:github` | Delete the GitHub release and remote tag |
|
|
39
|
+
|
|
40
|
+
`version:github:release/unrelease/list` are removed. `version:*` lifecycle tasks and the `version:release`/`version:unrelease` composites are unchanged.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## Task 1: `Gempilot::Origin` (idempotent commit + tag push)
|
|
45
|
+
|
|
46
|
+
**Files:**
|
|
47
|
+
- Create: `lib/gempilot/origin.rb`
|
|
48
|
+
- Test: `spec/gempilot/origin_spec.rb`
|
|
49
|
+
|
|
50
|
+
This is the bug fix: an always-push, no-guard replacement for bundler's `release:source_control_push`. Mirrors bundler's `git_push` (push the branch ref, then the tag ref, to the branch's configured remote — defaulting to `origin`).
|
|
51
|
+
|
|
52
|
+
- [ ] **Step 1: Write the failing spec**
|
|
53
|
+
|
|
54
|
+
Create `spec/gempilot/origin_spec.rb`:
|
|
55
|
+
|
|
56
|
+
```ruby
|
|
57
|
+
require "spec_helper"
|
|
58
|
+
|
|
59
|
+
RSpec.describe Gempilot::Origin do
|
|
60
|
+
subject(:origin) { described_class.new("v1.2.3") }
|
|
61
|
+
|
|
62
|
+
around do |example|
|
|
63
|
+
Dir.mktmpdir("origin_spec") { |dir| Dir.chdir(dir) { example.run } }
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
before do
|
|
67
|
+
system("git", "init", "--quiet", "-b", "main", ".")
|
|
68
|
+
system("git", "config", "user.email", "test@test.com")
|
|
69
|
+
system("git", "config", "user.name", "Test")
|
|
70
|
+
system("git", "commit", "--allow-empty", "--quiet", "-m", "init")
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
describe "#push" do
|
|
74
|
+
before { allow(origin).to receive(:sh) }
|
|
75
|
+
|
|
76
|
+
it "pushes the current branch, then the tag, to the resolved remote", :aggregate_failures do
|
|
77
|
+
origin.push
|
|
78
|
+
expect(origin).to have_received(:sh).with("git", "push", "origin", "refs/heads/main").ordered
|
|
79
|
+
expect(origin).to have_received(:sh).with("git", "push", "origin", "refs/tags/v1.2.3").ordered
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
describe "#push against a real remote" do
|
|
84
|
+
before do
|
|
85
|
+
system("git", "clone", "--quiet", "--bare", ".", "origin.git")
|
|
86
|
+
system("git", "remote", "add", "origin", "origin.git")
|
|
87
|
+
system("git", "tag", "v1.2.3")
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
it "lands the tag on the remote and stays idempotent on re-run", :aggregate_failures do
|
|
91
|
+
origin.push
|
|
92
|
+
expect(`git --git-dir=origin.git tag`.strip).to eq("v1.2.3")
|
|
93
|
+
expect { described_class.new("v1.2.3").push }.not_to raise_error
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
- [ ] **Step 2: Run the spec, verify it fails**
|
|
100
|
+
|
|
101
|
+
Run: `bundle exec rspec spec/gempilot/origin_spec.rb`
|
|
102
|
+
Expected: FAIL — `uninitialized constant Gempilot::Origin` (or `NameError`).
|
|
103
|
+
|
|
104
|
+
- [ ] **Step 3: Implement `Gempilot::Origin`**
|
|
105
|
+
|
|
106
|
+
Create `lib/gempilot/origin.rb`:
|
|
107
|
+
|
|
108
|
+
```ruby
|
|
109
|
+
require "open3"
|
|
110
|
+
|
|
111
|
+
module Gempilot
|
|
112
|
+
## Pushes the current branch and a release tag to the branch's git remote.
|
|
113
|
+
## Backs the +release:source_control_push+ task. Idempotent: pushing an
|
|
114
|
+
## already-pushed branch or tag is a no-op, so re-running a release never
|
|
115
|
+
## fails on an existing tag (unlike bundler's +already_tagged?+ guard, which
|
|
116
|
+
## skips the push entirely once the tag exists locally).
|
|
117
|
+
class Origin
|
|
118
|
+
include StrictShell
|
|
119
|
+
|
|
120
|
+
attr_reader :tag
|
|
121
|
+
|
|
122
|
+
def initialize(tag)
|
|
123
|
+
@tag = tag
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def push
|
|
127
|
+
sh "git", "push", remote, "refs/heads/#{branch}"
|
|
128
|
+
sh "git", "push", remote, "refs/tags/#{tag}"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
private
|
|
132
|
+
|
|
133
|
+
def branch
|
|
134
|
+
@branch ||= capture("git", "rev-parse", "--abbrev-ref", "HEAD")
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def remote
|
|
138
|
+
@remote ||= configured_remote || "origin"
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
def configured_remote
|
|
142
|
+
out, status = Open3.capture2("git", "config", "--get", "branch.#{branch}.remote")
|
|
143
|
+
out.strip if status.success?
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
def capture(*args)
|
|
147
|
+
out, status = Open3.capture2(*args)
|
|
148
|
+
raise "Command #{args.join(" ").inspect} failed" unless status.success?
|
|
149
|
+
|
|
150
|
+
out.strip
|
|
151
|
+
end
|
|
152
|
+
end
|
|
153
|
+
end
|
|
154
|
+
```
|
|
155
|
+
|
|
156
|
+
- [ ] **Step 4: Run the spec, verify it passes**
|
|
157
|
+
|
|
158
|
+
Run: `bundle exec rspec spec/gempilot/origin_spec.rb`
|
|
159
|
+
Expected: PASS — `3 examples, 0 failures` (2 `#push` examples across the two describe blocks). RuboCop next.
|
|
160
|
+
|
|
161
|
+
- [ ] **Step 5: Lint the new files**
|
|
162
|
+
|
|
163
|
+
Run: `bundle exec rubocop lib/gempilot/origin.rb spec/gempilot/origin_spec.rb`
|
|
164
|
+
Expected: `no offenses detected`. If any offense appears, fix it and re-run.
|
|
165
|
+
|
|
166
|
+
- [ ] **Step 6: Commit**
|
|
167
|
+
|
|
168
|
+
```bash
|
|
169
|
+
git add lib/gempilot/origin.rb spec/gempilot/origin_spec.rb
|
|
170
|
+
git commit -m "Add Gempilot::Origin for idempotent commit+tag push"
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
---
|
|
174
|
+
|
|
175
|
+
## Task 2: Slim `GithubRelease#create`
|
|
176
|
+
|
|
177
|
+
**Files:**
|
|
178
|
+
- Modify: `lib/gempilot/github_release.rb`
|
|
179
|
+
- Test: `spec/gempilot/github_release_spec.rb`
|
|
180
|
+
|
|
181
|
+
Pushing is now `Origin`'s job (the `release:source_control_push` prerequisite). `GithubRelease#create` should only call `gh release create`.
|
|
182
|
+
|
|
183
|
+
- [ ] **Step 1: Update the spec to drop the git-push expectations**
|
|
184
|
+
|
|
185
|
+
In `spec/gempilot/github_release_spec.rb`, replace the entire `describe "#create"` block:
|
|
186
|
+
|
|
187
|
+
```ruby
|
|
188
|
+
describe "#create" do
|
|
189
|
+
it "creates a release with generated notes" do
|
|
190
|
+
release.create
|
|
191
|
+
args = ["gh", "release", "create", "--generate-notes", "--fail-on-no-commits", tag]
|
|
192
|
+
expect(release).to have_received(:sh).with(*args)
|
|
193
|
+
end
|
|
194
|
+
end
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
(Leave the `#destroy` and `#list` describe blocks unchanged.)
|
|
198
|
+
|
|
199
|
+
- [ ] **Step 2: Run the spec, verify `#create` now fails**
|
|
200
|
+
|
|
201
|
+
Run: `bundle exec rspec spec/gempilot/github_release_spec.rb`
|
|
202
|
+
Expected: The `#create` example passes already (it only asserts the `gh` call), but confirm the whole file is green. If green, the current `create` still issues `git push` calls harmlessly. To make the test *drive* the change, also assert no git push:
|
|
203
|
+
|
|
204
|
+
Add this example inside `describe "#create"`:
|
|
205
|
+
|
|
206
|
+
```ruby
|
|
207
|
+
it "does not push git refs itself" do
|
|
208
|
+
release.create
|
|
209
|
+
expect(release).not_to have_received(:sh).with("git", any_args)
|
|
210
|
+
end
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Run again: `bundle exec rspec spec/gempilot/github_release_spec.rb`
|
|
214
|
+
Expected: FAIL — `does not push git refs itself` fails because `create` still runs `git push`.
|
|
215
|
+
|
|
216
|
+
- [ ] **Step 3: Remove the git push from `create`**
|
|
217
|
+
|
|
218
|
+
In `lib/gempilot/github_release.rb`, change `create` from:
|
|
219
|
+
|
|
220
|
+
```ruby
|
|
221
|
+
def create
|
|
222
|
+
sh "git", "push"
|
|
223
|
+
sh "git", "push", "--tags"
|
|
224
|
+
sh "gh", "release", "create",
|
|
225
|
+
"--generate-notes", "--fail-on-no-commits",
|
|
226
|
+
tag
|
|
227
|
+
end
|
|
228
|
+
```
|
|
229
|
+
|
|
230
|
+
to:
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
def create
|
|
234
|
+
sh "gh", "release", "create",
|
|
235
|
+
"--generate-notes", "--fail-on-no-commits",
|
|
236
|
+
tag
|
|
237
|
+
end
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
- [ ] **Step 4: Run the spec, verify it passes**
|
|
241
|
+
|
|
242
|
+
Run: `bundle exec rspec spec/gempilot/github_release_spec.rb`
|
|
243
|
+
Expected: PASS — all examples green.
|
|
244
|
+
|
|
245
|
+
- [ ] **Step 5: Lint**
|
|
246
|
+
|
|
247
|
+
Run: `bundle exec rubocop lib/gempilot/github_release.rb spec/gempilot/github_release_spec.rb`
|
|
248
|
+
Expected: `no offenses detected`.
|
|
249
|
+
|
|
250
|
+
- [ ] **Step 6: Commit**
|
|
251
|
+
|
|
252
|
+
```bash
|
|
253
|
+
git add lib/gempilot/github_release.rb spec/gempilot/github_release_spec.rb
|
|
254
|
+
git commit -m "Slim GithubRelease#create: push is now Origin's job"
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
---
|
|
258
|
+
|
|
259
|
+
## Task 3: `ReleaseTasks` module + rewire `VersionTask`
|
|
260
|
+
|
|
261
|
+
**Files:**
|
|
262
|
+
- Create: `lib/gempilot/release_tasks.rb`
|
|
263
|
+
- Modify: `lib/gempilot/version_task.rb`
|
|
264
|
+
- Test: `spec/gempilot/version_task_spec.rb`
|
|
265
|
+
|
|
266
|
+
The new `release`/`unrelease` task tree, extracted into a module (keeps `VersionTask` under `Metrics/ClassLength`) and `include`d into `VersionTask`.
|
|
267
|
+
|
|
268
|
+
- [ ] **Step 1: Write the failing spec additions**
|
|
269
|
+
|
|
270
|
+
In `spec/gempilot/version_task_spec.rb`, add these two `describe` blocks inside the top-level `RSpec.describe Gempilot::VersionTask do ... end` (e.g. after the existing `describe "version:release"` block, before the final `end`):
|
|
271
|
+
|
|
272
|
+
```ruby
|
|
273
|
+
describe "release task hierarchy" do
|
|
274
|
+
it "defines the release and unrelease tasks", :aggregate_failures do
|
|
275
|
+
%w[release release:rubygems release:github release:list:github unrelease unrelease:github].each do |name|
|
|
276
|
+
expect(Rake::Task).to be_task_defined(name)
|
|
277
|
+
end
|
|
278
|
+
end
|
|
279
|
+
|
|
280
|
+
it "removes the old version:github tasks", :aggregate_failures do
|
|
281
|
+
%w[version:github:release version:github:unrelease version:github:list].each do |name|
|
|
282
|
+
expect(Rake::Task).not_to be_task_defined(name)
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
it "composes release from the per-remote tasks" do
|
|
287
|
+
expect(Rake::Task["release"].prerequisites).to eq(%w[release:rubygems release:github])
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
it "builds release:rubygems from bundler's own tasks" do
|
|
291
|
+
chain = %w[build release:guard_clean release:source_control_push release:rubygem_push]
|
|
292
|
+
expect(Rake::Task["release:rubygems"].prerequisites).to eq(chain)
|
|
293
|
+
end
|
|
294
|
+
|
|
295
|
+
it "composes unrelease from the github task" do
|
|
296
|
+
expect(Rake::Task["unrelease"].prerequisites).to eq(%w[unrelease:github])
|
|
297
|
+
end
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
describe "release task behavior" do
|
|
301
|
+
let(:origin) { instance_double(Gempilot::Origin, push: nil) }
|
|
302
|
+
let(:github) { instance_double(Gempilot::GithubRelease, create: nil, destroy: nil, list: nil) }
|
|
303
|
+
|
|
304
|
+
before do
|
|
305
|
+
allow(Gempilot::Origin).to receive(:new).and_return(origin)
|
|
306
|
+
allow(Gempilot::GithubRelease).to receive(:new).and_return(github)
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
it "release:source_control_push pushes via Origin" do
|
|
310
|
+
Rake::Task["release:source_control_push"].invoke
|
|
311
|
+
expect(origin).to have_received(:push)
|
|
312
|
+
end
|
|
313
|
+
|
|
314
|
+
it "release:github pushes, then creates the release", :aggregate_failures do
|
|
315
|
+
Rake::Task["release:github"].invoke
|
|
316
|
+
expect(origin).to have_received(:push)
|
|
317
|
+
expect(github).to have_received(:create)
|
|
318
|
+
end
|
|
319
|
+
|
|
320
|
+
it "release:list:github lists releases" do
|
|
321
|
+
Rake::Task["release:list:github"].invoke
|
|
322
|
+
expect(github).to have_received(:list)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
it "unrelease:github destroys the release" do
|
|
326
|
+
Rake::Task["unrelease:github"].invoke
|
|
327
|
+
expect(github).to have_received(:destroy)
|
|
328
|
+
end
|
|
329
|
+
end
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
- [ ] **Step 2: Run the spec, verify it fails**
|
|
333
|
+
|
|
334
|
+
Run: `bundle exec rspec spec/gempilot/version_task_spec.rb`
|
|
335
|
+
Expected: FAIL — `release`/`release:github`/etc. are not defined; several examples error with "Don't know how to build task 'release:github'" / `be_task_defined` returns false.
|
|
336
|
+
|
|
337
|
+
- [ ] **Step 3: Create the `ReleaseTasks` module**
|
|
338
|
+
|
|
339
|
+
Create `lib/gempilot/release_tasks.rb`:
|
|
340
|
+
|
|
341
|
+
```ruby
|
|
342
|
+
module Gempilot
|
|
343
|
+
## Rake task definitions for publishing a release to RubyGems and GitHub.
|
|
344
|
+
## Mixed into VersionTask. Assumes +bundler/gem_tasks+ has been required so the
|
|
345
|
+
## +build+, +release:guard_clean+, and +release:rubygem_push+ tasks exist (the
|
|
346
|
+
## generated Rakefile guarantees this). Fixes GitHub releasing by replacing
|
|
347
|
+
## bundler's +already_tagged?+-guarded +release:source_control_push+ with an
|
|
348
|
+
## idempotent push.
|
|
349
|
+
module ReleaseTasks
|
|
350
|
+
private
|
|
351
|
+
|
|
352
|
+
def define_release_tasks(project)
|
|
353
|
+
override_source_control_push(project)
|
|
354
|
+
define_release_namespace(project)
|
|
355
|
+
define_root_release_task
|
|
356
|
+
define_unrelease_tasks(project)
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def override_source_control_push(project)
|
|
360
|
+
clear_task "release:source_control_push"
|
|
361
|
+
task("release:source_control_push") { Origin.new(project.version_tag).push }
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def define_release_namespace(project)
|
|
365
|
+
namespace :release do
|
|
366
|
+
define_rubygems_release
|
|
367
|
+
define_github_release(project)
|
|
368
|
+
define_release_list(project)
|
|
369
|
+
end
|
|
370
|
+
end
|
|
371
|
+
|
|
372
|
+
def define_rubygems_release
|
|
373
|
+
desc "Release the current version to RubyGems"
|
|
374
|
+
task rubygems: %w[build release:guard_clean release:source_control_push release:rubygem_push]
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
def define_github_release(project)
|
|
378
|
+
desc "Create a GitHub release for the current version"
|
|
379
|
+
task github: "release:source_control_push" do
|
|
380
|
+
GithubRelease.new(project.version_tag).create
|
|
381
|
+
end
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def define_release_list(project)
|
|
385
|
+
namespace :list do
|
|
386
|
+
desc "List GitHub releases"
|
|
387
|
+
task(:github) { GithubRelease.new(project.version_tag).list }
|
|
388
|
+
end
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
def define_root_release_task
|
|
392
|
+
clear_task "release"
|
|
393
|
+
desc "Release the current version to all remotes"
|
|
394
|
+
task release: %w[release:rubygems release:github]
|
|
395
|
+
end
|
|
396
|
+
|
|
397
|
+
def define_unrelease_tasks(project)
|
|
398
|
+
desc "Delete the current release from all remotes that support it"
|
|
399
|
+
task unrelease: %w[unrelease:github]
|
|
400
|
+
define_unrelease_namespace(project)
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
def define_unrelease_namespace(project)
|
|
404
|
+
namespace :unrelease do
|
|
405
|
+
desc "Delete the GitHub release for the current version"
|
|
406
|
+
task(:github) { GithubRelease.new(project.version_tag).destroy }
|
|
407
|
+
end
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def clear_task(name)
|
|
411
|
+
Rake::Task[name].clear if Rake::Task.task_defined?(name)
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
end
|
|
415
|
+
```
|
|
416
|
+
|
|
417
|
+
- [ ] **Step 4: Rewire `VersionTask` to use the module**
|
|
418
|
+
|
|
419
|
+
In `lib/gempilot/version_task.rb`:
|
|
420
|
+
|
|
421
|
+
First, add the include at the top of the class body (right after `class VersionTask < Rake::TaskLib`):
|
|
422
|
+
|
|
423
|
+
```ruby
|
|
424
|
+
class VersionTask < Rake::TaskLib
|
|
425
|
+
include ReleaseTasks
|
|
426
|
+
|
|
427
|
+
attr_reader :project
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
Then change `define_tasks` from:
|
|
431
|
+
|
|
432
|
+
```ruby
|
|
433
|
+
def define_tasks
|
|
434
|
+
define_version_tasks
|
|
435
|
+
define_version_composite_tasks
|
|
436
|
+
define_github_tasks
|
|
437
|
+
end
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
to:
|
|
441
|
+
|
|
442
|
+
```ruby
|
|
443
|
+
def define_tasks
|
|
444
|
+
define_version_tasks
|
|
445
|
+
define_version_composite_tasks
|
|
446
|
+
define_release_tasks(@project)
|
|
447
|
+
end
|
|
448
|
+
```
|
|
449
|
+
|
|
450
|
+
Then delete the entire `define_github_tasks` method (the `namespace "version:github" do ... end` method) — from `def define_github_tasks` through its closing `end`.
|
|
451
|
+
|
|
452
|
+
- [ ] **Step 5: Run the version_task spec, verify it passes**
|
|
453
|
+
|
|
454
|
+
Run: `bundle exec rspec spec/gempilot/version_task_spec.rb`
|
|
455
|
+
Expected: PASS — all examples green (existing `version:bump`/`version:release` plus the new hierarchy/behavior examples).
|
|
456
|
+
|
|
457
|
+
- [ ] **Step 6: Run the full suite + lint**
|
|
458
|
+
|
|
459
|
+
Run: `bundle exec rake spec && bundle exec rake test && bundle exec rubocop`
|
|
460
|
+
Expected: `rake spec` green, `rake test` green, RuboCop `no offenses detected`. In particular confirm `lib/gempilot/version_task.rb` reports no `Metrics/ClassLength` offense.
|
|
461
|
+
|
|
462
|
+
- [ ] **Step 7: Sanity-check the task tree renders and old tasks are gone**
|
|
463
|
+
|
|
464
|
+
Run (from the repo root — gempilot dogfoods its own `VersionTask`):
|
|
465
|
+
```bash
|
|
466
|
+
bundle exec rake -T release ; bundle exec rake -T unrelease ; bundle exec rake -AT | grep -c 'version:github' || true
|
|
467
|
+
```
|
|
468
|
+
Expected: `release`, `release:rubygems`, `release:github`, `release:list:github`, `unrelease`, `unrelease:github` appear; the `version:github` grep count is `0`.
|
|
469
|
+
|
|
470
|
+
- [ ] **Step 8: Commit**
|
|
471
|
+
|
|
472
|
+
```bash
|
|
473
|
+
git add lib/gempilot/release_tasks.rb lib/gempilot/version_task.rb spec/gempilot/version_task_spec.rb
|
|
474
|
+
git commit -m "Move GitHub publishing into idiomatic release/unrelease namespace"
|
|
475
|
+
```
|
|
476
|
+
|
|
477
|
+
---
|
|
478
|
+
|
|
479
|
+
## Task 4: Update docs
|
|
480
|
+
|
|
481
|
+
**Files:**
|
|
482
|
+
- Modify: `README.md`
|
|
483
|
+
- Modify: `CLAUDE.md`
|
|
484
|
+
|
|
485
|
+
No tests — documentation only. Make the exact edits below.
|
|
486
|
+
|
|
487
|
+
- [ ] **Step 1: Update the README `gempilot release` note**
|
|
488
|
+
|
|
489
|
+
In `README.md`, replace:
|
|
490
|
+
|
|
491
|
+
```markdown
|
|
492
|
+
Delegates to `rake release` to build and push the gem.
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
with:
|
|
496
|
+
|
|
497
|
+
```markdown
|
|
498
|
+
Delegates to `rake release`, which publishes the current version to all remotes
|
|
499
|
+
(RubyGems + GitHub).
|
|
500
|
+
```
|
|
501
|
+
|
|
502
|
+
- [ ] **Step 2: Update the README task table**
|
|
503
|
+
|
|
504
|
+
In `README.md`, replace these three rows:
|
|
505
|
+
|
|
506
|
+
```markdown
|
|
507
|
+
| `rake version:github:release` | Push and create a GitHub release |
|
|
508
|
+
| `rake version:github:unrelease` | Delete the GitHub release |
|
|
509
|
+
| `rake version:github:list` | List GitHub releases |
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
with:
|
|
513
|
+
|
|
514
|
+
```markdown
|
|
515
|
+
| `rake release` | Publish the current version to all remotes (RubyGems + GitHub) |
|
|
516
|
+
| `rake release:rubygems` | Build and push the gem to RubyGems |
|
|
517
|
+
| `rake release:github` | Push commit + tag, then create the GitHub release |
|
|
518
|
+
| `rake release:list:github` | List GitHub releases |
|
|
519
|
+
| `rake unrelease` | Delete the release from all remotes that support it |
|
|
520
|
+
| `rake unrelease:github` | Delete the GitHub release and remote tag |
|
|
521
|
+
```
|
|
522
|
+
|
|
523
|
+
(The `rake version:release` and `rake version:unrelease` rows stay.)
|
|
524
|
+
|
|
525
|
+
- [ ] **Step 3: Update CLAUDE.md**
|
|
526
|
+
|
|
527
|
+
In `CLAUDE.md`, replace this bullet:
|
|
528
|
+
|
|
529
|
+
```markdown
|
|
530
|
+
- Version lifecycle rake tasks installed via `Gempilot::VersionTask.new` (a `Rake::TaskLib`): `version:current/bump/commit/tag/untag/reset/revert`, composite `version:release`/`version:unrelease`, and `version:github:release/unrelease/list`
|
|
531
|
+
```
|
|
532
|
+
|
|
533
|
+
with:
|
|
534
|
+
|
|
535
|
+
```markdown
|
|
536
|
+
- Version lifecycle rake tasks installed via `Gempilot::VersionTask.new` (a `Rake::TaskLib`): `version:current/bump/commit/tag/untag/reset/revert` and composite `version:release`/`version:unrelease`
|
|
537
|
+
- Publishing rake tasks (mixed into `VersionTask` via `Gempilot::ReleaseTasks`): `release` (all remotes), `release:rubygems`, `release:github`, `release:list:github`, `unrelease`, `unrelease:github`. These override and reuse bundler's `bundler/gem_tasks` release chain; `release:source_control_push` is replaced with `Gempilot::Origin` to push commit+tag idempotently
|
|
538
|
+
```
|
|
539
|
+
|
|
540
|
+
- [ ] **Step 4: Verify no stale references remain**
|
|
541
|
+
|
|
542
|
+
Run: `grep -rn "version:github" README.md CLAUDE.md`
|
|
543
|
+
Expected: no output.
|
|
544
|
+
|
|
545
|
+
- [ ] **Step 5: Commit**
|
|
546
|
+
|
|
547
|
+
```bash
|
|
548
|
+
git add README.md CLAUDE.md docs/superpowers/specs/2026-07-20-release-task-hierarchy-design.md
|
|
549
|
+
git commit -m "Docs: document release/unrelease task hierarchy"
|
|
550
|
+
```
|
|
551
|
+
|
|
552
|
+
---
|
|
553
|
+
|
|
554
|
+
## Task 5: Full verification
|
|
555
|
+
|
|
556
|
+
**Files:** none (verification only).
|
|
557
|
+
|
|
558
|
+
- [ ] **Step 1: Run the complete default suite**
|
|
559
|
+
|
|
560
|
+
Run: `bundle exec rake`
|
|
561
|
+
Expected: `task default: [:test, :spec, :rubocop]` runs all three — Minitest green (`0 failures`), RSpec green (`0 failures`), RuboCop `no offenses detected`.
|
|
562
|
+
|
|
563
|
+
- [ ] **Step 2: Verify Zeitwerk naming for the new files**
|
|
564
|
+
|
|
565
|
+
Run: `bundle exec rake zeitwerk:validate`
|
|
566
|
+
Expected: `Zeitwerk: All files loaded successfully.` (confirms `lib/gempilot/origin.rb` → `Gempilot::Origin` and `lib/gempilot/release_tasks.rb` → `Gempilot::ReleaseTasks` load cleanly under eager load).
|
|
567
|
+
|
|
568
|
+
- [ ] **Step 3: Confirm the working tree is clean and review the branch**
|
|
569
|
+
|
|
570
|
+
Run: `git status` and `git log --oneline master..HEAD`
|
|
571
|
+
Expected: clean working tree; commits for Origin, GithubRelease slim, release/unrelease move, and docs.
|
|
572
|
+
|
|
573
|
+
---
|
|
574
|
+
|
|
575
|
+
## Self-review notes (traceability to spec)
|
|
576
|
+
|
|
577
|
+
- **Task hierarchy** (spec §"Task interface"/"Task definitions") → Task 3 (module + rewire) + Task 3 Step 1 structure specs.
|
|
578
|
+
- **Bug fix — idempotent push replacing `already_tagged?`** (spec §"Problem"/`Origin`) → Task 1 (`Origin`, incl. real-remote idempotency regression test) + Task 3 `override_source_control_push`.
|
|
579
|
+
- **`GithubRelease` slimmed** (spec §"Components") → Task 2.
|
|
580
|
+
- **RubyGems reuses bundler leaf tasks** (spec §"Decisions") → Task 3 `define_rubygems_release` + prerequisite spec.
|
|
581
|
+
- **`unrelease:rubygems` errors naturally** (spec §"Error handling") → covered by not defining it; documented, no code.
|
|
582
|
+
- **Docs** (spec §"Docs") → Task 4.
|
|
583
|
+
- **`Rakefile.erb` unchanged** (spec §"Out of scope") → no task; load order already correct.
|