gempilot 0.2.3 → 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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b4b895f3da5d0d8866cd13f835dd3c1ca71d8d433571c2d7531735636eeb6ad0
4
- data.tar.gz: 878dad31a22bdecdb9ff607ebecf2330432070219edfc3e08b66a5022c205d77
3
+ metadata.gz: ed510959d35011586d23fc0dcc0baff3a453a2acbd1a2d1c8fb4489cee63e6e0
4
+ data.tar.gz: 455ba876df7b3fb40df26932737cae4062a6e6e2210f1fd02b18a231566bd23d
5
5
  SHA512:
6
- metadata.gz: ccfc9410fe3ccd3cc8f49e3ca34f68ef334a50183eb95d0788f09c9a5e972fcb4b1244cdfeee188b9898e25ba1c4542d6c4cf24ee24e54edebd4e8b8e907f6a1
7
- data.tar.gz: e7deb74172aa7bcd75d84f41ad2d80022d9befcd7f1b69990c6f8b9fe6002168d8b25581242dbded279bdd601986c24552b1431573ff0ed62bec8316734e50fe
6
+ metadata.gz: 4ce5185ea5c07adc72435dee47b2f749f4f6146f26a09c8d8c557490e4be1b69b2b048e4c9ab32b8548d043c9a0d393cc32a0d8dd23c94c98cabd3a8737334d9
7
+ data.tar.gz: 6094227a45e9bb173f0455dc155bd3ac7593698a7c41c7ac1482c5e7c44a59a90c547bb20206d3c7887b87a84977fc1f7ca615b2975fe53dc6a8ab85384a83e8
data/.rubocop.yml CHANGED
@@ -15,7 +15,6 @@ plugins:
15
15
  - rubocop-performance
16
16
  - rubocop-rake
17
17
  - rubocop-rspec
18
- - rubocop-design
19
18
 
20
19
  AllCops:
21
20
  NewCops: enable
data/CLAUDE.md CHANGED
@@ -35,4 +35,5 @@ A CLI tool for creating and managing Ruby gems, built on CommandKit.
35
35
  - RuboCop with framework-specific plugins
36
36
  - GitHub Actions CI workflow (`.github/workflows/ci.yml`)
37
37
  - `git ls-files`-based gemspec with glob fallback for non-git repos
38
- - 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`
38
+ - 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`
39
+ - 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
data/README.md CHANGED
@@ -86,7 +86,8 @@ gempilot bump major
86
86
 
87
87
  ### `gempilot release`
88
88
 
89
- Delegates to `rake release` to build and push the gem.
89
+ Delegates to `rake release`, which publishes the current version to all remotes
90
+ (RubyGems + GitHub).
90
91
 
91
92
  ### `gempilot console`
92
93
 
@@ -120,9 +121,12 @@ Generated gems include rake tasks for the full version lifecycle:
120
121
  | `rake version:revert` | Revert the last version bump commit |
121
122
  | `rake version:release` | Bump, commit, and tag (combined) |
122
123
  | `rake version:unrelease` | Untag and reset (combined) |
123
- | `rake version:github:release` | Push and create a GitHub release |
124
- | `rake version:github:unrelease` | Delete the GitHub release |
125
- | `rake version:github:list` | List GitHub releases |
124
+ | `rake release` | Publish the current version to all remotes (RubyGems + GitHub) |
125
+ | `rake release:rubygems` | Build and push the gem to RubyGems |
126
+ | `rake release:github` | Push commit + tag, then create the GitHub release |
127
+ | `rake release:list:github` | List GitHub releases |
128
+ | `rake unrelease` | Delete the release from all remotes that support it |
129
+ | `rake unrelease:github` | Delete the GitHub release and remote tag |
126
130
 
127
131
  ## Development
128
132
 
@@ -25,12 +25,6 @@ AllCops:
25
25
  Exclude:
26
26
  - bin/*
27
27
  - vendor/**/*
28
- - lib/core_ext/**/*
29
- - rakelib/project.rb
30
- - rakelib/project_version.rb
31
- - rakelib/version_tag.rb
32
- - rakelib/github_release.rb
33
- - rakelib/strict_shell.rb
34
28
  <% if @hyphenated -%>
35
29
  - lib/<%= @gem_name %>.rb
36
30
  <% end -%>
@@ -124,17 +118,12 @@ Metrics/BlockLength:
124
118
  <% end -%>
125
119
  Exclude:
126
120
  - "<%= @gem_name %>.gemspec"
127
- - "rakelib/**/*"
128
121
 
129
- # Gemspec and rake files use patterns that trigger Claude cops legitimately.
122
+ # Gemspec uses patterns that trigger Claude cops legitimately.
130
123
  Claude/NoFancyUnicode:
131
124
  Exclude:
132
125
  - "<%= @gem_name %>.gemspec"
133
126
 
134
- Claude/MysteryRegex:
135
- Exclude:
136
- - "rakelib/**/*"
137
-
138
127
  # Zeitwerk loaders are mutable by design — only flag literal mutable values.
139
128
  Style/MutableConstant:
140
129
  EnforcedStyle: literals
@@ -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.
@@ -0,0 +1,229 @@
1
+ # Release task hierarchy — design
2
+
3
+ **Issue:** `2C8EF3C8-83E9-11F1-9256-FE6CB9572C2F` — "gh release tasks need adjusting in task hierarchy"
4
+
5
+ **Date:** 2026-07-20
6
+
7
+ ## Problem
8
+
9
+ Gempilot installs GitHub release tasks under a bespoke `version:github:*` namespace
10
+ (`version:github:release/unrelease/list`). Two things are wrong:
11
+
12
+ 1. **Wrong hierarchy.** GitHub publishing should live in the standard `release` /
13
+ `unrelease` namespace alongside RubyGems publishing, not in a custom
14
+ `version:github:*` corner.
15
+ 2. **GitHub releasing is broken.** The release flow bumps + commits + tags *locally*
16
+ via `version:release` (`version:tag` runs `git tag vX`). When the actual publish
17
+ runs through bundler's `release` task, bundler's `already_tagged?` guard sees the
18
+ tag already exists locally, prints *"Tag vX has already been created."*, and
19
+ **skips `release:source_control_push` entirely** — so the commit and tag never
20
+ reach the remote, and `gh release create vX` has nothing to attach to. This is the
21
+ "tags have already been pushed / doesn't work at all" symptom in the issue.
22
+
23
+ This is an approved **breaking change**. No backwards compatibility for the old
24
+ `version:github:*` tasks.
25
+
26
+ ## Decisions
27
+
28
+ - **Namespace-only interface. No `[remote]` argument.** Target selection lives in the
29
+ task name (`release:github`), not an argument. This is the idiomatic Rake convention
30
+ (task name = what to do; argument = a value), and it avoids a direct collision:
31
+ `bundler/gem_tasks` already defines `task "release", [:remote]` where `remote` means
32
+ the *git remote name* (`origin`) passed to `git push`. Overloading that same word to
33
+ mean "publish target" is exactly what made the original `release[remote]` idea
34
+ confusing. So the final interface uses no argument at all.
35
+ - **Bare `rake release` publishes to all remotes** (RubyGems + GitHub), expressed as a
36
+ pure prerequisite composite — no dispatcher code.
37
+ - **Reuse bundler's building blocks for RubyGems.** `release:rubygems` composes
38
+ bundler's existing `build`, `release:guard_clean`, and `release:rubygem_push` leaf
39
+ tasks via prerequisites. No new object wraps them — this preserves `allowed_push_host`
40
+ (private gem servers), MFA/OTP prompts, and `gem_push=no`.
41
+ - **Fix the bug by overriding the one task that is wrong.** Replace bundler's
42
+ `release:source_control_push` with an idempotent push (no `already_tagged?` guard),
43
+ done the same idiomatic way bundler does it: an explicit `git push <remote> <branch>`
44
+ then `git push <remote> <tag>`. Both remotes share this single task as a prerequisite,
45
+ so both get the fix and the push runs exactly once per release.
46
+ - **Keep the local version lifecycle as-is.** `version:release` / `version:unrelease`
47
+ (local bump → commit → tag) are unchanged. The `version:` namespace and the top-level
48
+ `release` do not collide in Rake.
49
+
50
+ ## Task interface
51
+
52
+ | Task | Behavior |
53
+ | --- | --- |
54
+ | `rake release` | Publish current version to all remotes (RubyGems + GitHub) |
55
+ | `rake release:rubygems` | Build + push the gem to RubyGems |
56
+ | `rake release:github` | Push commit + tag, then create the GitHub release |
57
+ | `rake release:list:github` | List GitHub releases |
58
+ | `rake unrelease` | Delete the release from all remotes that support it (GitHub) |
59
+ | `rake unrelease:github` | Delete the GitHub release and remote tag |
60
+
61
+ `rake unrelease:rubygems` is intentionally undefined — Rake fails with "Don't know how
62
+ to build task", which is the correct signal that RubyGems yanking is out of scope.
63
+
64
+ ## Task definitions
65
+
66
+ Defined in `Gempilot::VersionTask`, replacing the current `define_github_tasks`. Runs
67
+ after `require "bundler/gem_tasks"` (guaranteed by the generated `Rakefile` and by
68
+ gempilot's own `Rakefile`), so bundler's tasks exist when we override them. Each
69
+ override is guarded with `task_defined?` so the definitions also work when bundler's
70
+ gem tasks are absent (e.g. gempilot's own spec suite).
71
+
72
+ ```ruby
73
+ # THE FIX: idempotent push, replacing bundler's already_tagged?-guarded task.
74
+ Rake::Task["release:source_control_push"].clear if Rake::Task.task_defined?("release:source_control_push")
75
+ task "release:source_control_push" do
76
+ Origin.new(project.version_tag).push
77
+ end
78
+
79
+ namespace :release do
80
+ desc "Release the current version to RubyGems"
81
+ task rubygems: %w[build release:guard_clean release:source_control_push release:rubygem_push]
82
+
83
+ desc "Create a GitHub release for the current version"
84
+ task github: "release:source_control_push" do
85
+ GithubRelease.new(project.version_tag).create
86
+ end
87
+
88
+ namespace :list do
89
+ desc "List GitHub releases"
90
+ task(:github) { GithubRelease.new(project.version_tag).list }
91
+ end
92
+ end
93
+
94
+ Rake::Task["release"].clear if Rake::Task.task_defined?("release")
95
+ desc "Release the current version to all remotes"
96
+ task release: %w[release:rubygems release:github]
97
+
98
+ desc "Delete the current release from all remotes that support it"
99
+ task unrelease: %w[unrelease:github]
100
+
101
+ namespace :unrelease do
102
+ desc "Delete the GitHub release for the current version"
103
+ task(:github) { GithubRelease.new(project.version_tag).destroy }
104
+ end
105
+ ```
106
+
107
+ ## Components
108
+
109
+ ### `Gempilot::Origin` (new — `lib/gempilot/origin.rb`)
110
+
111
+ A small domain object (same style as the existing `GithubRelease` / `VersionTag`) that
112
+ pushes the current branch and a given tag to the branch's configured git remote. This
113
+ backs `release:source_control_push` and is the bug fix.
114
+
115
+ - `initialize(tag)` — the version tag string (e.g. `"v1.0.0"`).
116
+ - `#push` — runs, via `StrictShell#sh`:
117
+ - `git push <remote> refs/heads/<branch>`
118
+ - `git push <remote> refs/tags/<tag>`
119
+ - `<branch>` = `git rev-parse --abbrev-ref HEAD`; `<remote>` = the branch's configured
120
+ remote (`git config --get branch.<branch>.remote`), defaulting to `origin` — mirroring
121
+ bundler's own `current_branch` / `default_remote` logic.
122
+ - Idempotent: pushing an already-pushed branch/tag exits 0 ("Everything up-to-date"),
123
+ so re-running a release never fails on the tag. This is the property bundler's guard
124
+ broke.
125
+
126
+ ### `Gempilot::GithubRelease` (modified — `lib/gempilot/github_release.rb`)
127
+
128
+ - `#create` drops its two internal `git push` / `git push --tags` lines. Pushing is now
129
+ `Origin`'s job (the `release:source_control_push` prerequisite runs first). `create`
130
+ becomes purely: `gh release create --generate-notes --fail-on-no-commits <tag>`.
131
+ - `#destroy` and `#list` are unchanged.
132
+
133
+ ### `Gempilot::ReleaseTasks` (new — `lib/gempilot/release_tasks.rb`)
134
+
135
+ The release / unrelease task definitions above, extracted into a focused module that is
136
+ `include`d into `VersionTask`. This is a separate concern from the local version
137
+ lifecycle, and extracting it keeps `VersionTask` under RuboCop's `Metrics/ClassLength`
138
+ (the combined class would otherwise exceed the 100-line limit). All methods are private
139
+ and parameterized by `project` (no reliance on the host's ivars beyond the single
140
+ `define_release_tasks(project)` entry point).
141
+
142
+ ### `Gempilot::VersionTask` (modified — `lib/gempilot/version_task.rb`)
143
+
144
+ - `include ReleaseTasks`.
145
+ - Remove `define_github_tasks`; replace its call in `define_tasks` with
146
+ `define_release_tasks(@project)`.
147
+ - `version:*` local lifecycle tasks and `version:release` / `version:unrelease`
148
+ composites are untouched.
149
+
150
+ There is **no** `RubygemsRelease` object. RubyGems publishing is bundler-task
151
+ composition only.
152
+
153
+ ## Data flow
154
+
155
+ - `rake release:github` → `release:source_control_push` (push commit + tag) →
156
+ `gh release create`.
157
+ - `rake release:rubygems` → `build` → `release:guard_clean` →
158
+ `release:source_control_push` (push commit + tag) → `release:rubygem_push` (gem push).
159
+ - `rake release` → `release:rubygems` (which pushes during its chain), then
160
+ `release:github`. Rake invokes `release:source_control_push` once, so the git push
161
+ happens a single time, before both publishes.
162
+ - `rake unrelease` → `unrelease:github` → `gh release delete --yes --cleanup-tag`
163
+ (removes the GitHub release and the remote tag).
164
+
165
+ ## Error handling
166
+
167
+ - **Unsupported unrelease target** (`rake unrelease:rubygems`): undefined task → Rake's
168
+ native "Don't know how to build task" error. No custom code.
169
+ - **Shell failures**: every git / gh / gem command runs through `StrictShell#sh`, which
170
+ already raises on non-zero exit.
171
+ - **Tag / commit not pushable**: if the local tag does not exist, the `git push
172
+ refs/tags/<tag>` fails loudly via `StrictShell` — the correct signal to run
173
+ `version:release` first. No separate guard task is added (deliberate: the
174
+ `version:release` flow is responsible for producing a clean, tagged commit; adding a
175
+ bundler-independent `guard_clean` is out of scope).
176
+
177
+ ## Testing (TDD)
178
+
179
+ Written before implementation.
180
+
181
+ **`spec/gempilot/origin_spec.rb` (new)**
182
+ - `#push` issues `git push <remote> <branch>` then `git push <remote> <tag>` in order
183
+ (stub `sh`, assert ordered calls; assert branch/remote resolution).
184
+ - **Regression / integration**: in a tmpdir, create a real repo with a bare "origin"
185
+ remote and a tag, run `Origin#push`, assert the commit + tag land on origin; run it a
186
+ **second** time and assert it still succeeds (idempotent) — directly proving the bug
187
+ (bundler's guarded skip) is fixed.
188
+
189
+ **`spec/gempilot/github_release_spec.rb` (modified)**
190
+ - `#create` no longer calls `git push` / `git push --tags`; it calls only
191
+ `gh release create --generate-notes --fail-on-no-commits <tag>`.
192
+ - `#destroy` and `#list` specs unchanged.
193
+
194
+ **`spec/gempilot/version_task_spec.rb` (extended)**
195
+ - **Structure**: `release`, `release:rubygems`, `release:github`, `release:list:github`,
196
+ `unrelease`, `unrelease:github` are defined; `version:github:release`,
197
+ `version:github:unrelease`, `version:github:list` are **not**.
198
+ - **Composites by prerequisite** (no invocation needed): `release` prerequisites ==
199
+ `%w[release:rubygems release:github]`; `release:rubygems` prerequisites ==
200
+ `%w[build release:guard_clean release:source_control_push release:rubygem_push]`;
201
+ `unrelease` prerequisites == `%w[unrelease:github]`.
202
+ - **Behavior by invocation** (stub `Origin` / `GithubRelease` constructors):
203
+ - `release:source_control_push` invokes `Origin#push`.
204
+ - `release:github` runs the push, then `GithubRelease#create`.
205
+ - `release:list:github` invokes `GithubRelease#list`.
206
+ - `unrelease:github` invokes `GithubRelease#destroy`.
207
+ - `release:rubygems` is asserted by prerequisites only (its bundler prerequisites are
208
+ absent in the spec's fresh Rake app, so it is not invoked there).
209
+
210
+ ## Docs
211
+
212
+ - `README.md` — replace the three `version:github:*` rows in the tasks table with the
213
+ new `release` / `unrelease` tasks; update the `gempilot release` note to reflect that
214
+ bare `rake release` now publishes to all remotes.
215
+ - `CLAUDE.md` — update the "Version lifecycle rake tasks" bullet: drop
216
+ `version:github:release/unrelease/list`, add `release` / `release:rubygems` /
217
+ `release:github` / `release:list:github` / `unrelease` / `unrelease:github`.
218
+
219
+ ## Out of scope
220
+
221
+ - `data/templates/gem/Rakefile.erb` needs no change — it already requires
222
+ `bundler/gem_tasks` before `Gempilot::VersionTask.new`, which the override relies on.
223
+ - The stale `rakelib/*` excludes in `data/templates/gem/dotfiles/rubocop.yml.erb`
224
+ (including `rakelib/github_release.rb`) belong to a separate open issue about internal
225
+ concerns leaking to gem users (`B45C988A`). Not touched here.
226
+ - RubyGems yanking (`unrelease:rubygems`).
227
+ - The `gempilot release` CLI command keeps proxying to `rake release`; its behavior
228
+ changes only because bare `rake release` now targets all remotes.
229
+ - Any change to the local `version:*` lifecycle tasks.
data/issues.rec CHANGED
@@ -101,7 +101,7 @@ Id: 0411D4DA-4323-11F1-847C-FE6CB9572C2F
101
101
  Updated: Tue, 28 Apr 2026 12:55:09 -0400
102
102
  Title: rubocop config should not exclude rakelib
103
103
  Description: it is supplied by gempilot. Should not be eliminating author files
104
- Status: open
104
+ Status: closed
105
105
 
106
106
  Id: C72CD130-4324-11F1-ACAE-FE6CB9572C2F
107
107
  Updated: Tue, 28 Apr 2026 13:07:46 -0400
@@ -164,7 +164,51 @@ Description: Has to do with how the ENV var RM_INFO is being interpreted
164
164
  Status: open
165
165
 
166
166
  Id: 8D847572-7273-11F1-B508-FE6CB9572C2E
167
- Updated: Sat, 27 Jun 2026 18:00:04 -0400
167
+ Updated: Fri, 03 Jul 2026 22:38:22 +0000
168
168
  Title: Gem extensions are broken
169
169
  Description: when using gempilot on a gem extension, like 'foo-support', using idiomatic ruby patterns for dir structure, gempilot Project class throws an error
170
- Status: open
170
+ Status: closed
171
+
172
+ Id: B45C988A-83C3-11F1-A32B-FE6CB9572C2F
173
+ Updated: Sun, 19 Jul 2026 18:46:39 -0400
174
+ Title: Gempilot internal concerns leak to gemusers
175
+ Description: Gempilot fresh install leaks the following in .rubocop.yml:
176
+ +
177
+ + AllCops:
178
+ + 1 NewCops: enable
179
+ + 2 TargetRubyVersion: 4.0
180
+ + 3 Exclude:
181
+ + 4 - bin/*
182
+ + 5 - vendor/**/*
183
+ + 6 - lib/core_ext/**/*
184
+ + 7 - rakelib/project.rb
185
+ + 8 - rakelib/project_version.rb
186
+ + 9
187
+ Status: closed
188
+
189
+ Id: 2C8EF3C8-83E9-11F1-9256-FE6CB9572C2F
190
+ Updated: Sun, 19 Jul 2026 23:14:52 -0400
191
+ Title: gh release tasks need adjusting in task heirarchy
192
+ Description: the github release related tasks are invoked as follows:
193
+ + ```
194
+ + rake version:github:release
195
+ + rake version:github:list
196
+ + rake version:github:unrelease
197
+ +
198
+ + ```
199
+ + they should instead be under the main idiomatic release namespace:
200
+ +
201
+ + ```
202
+ + rake release[remote] # release to the remote, e.g. rubygems/github
203
+ + rake release:github # alternative to rake release[github]
204
+ + rake unrelease[remote] # currently only supported for github, not rubygems
205
+ + rake unrelease:github # alternative to rake unrelease[github]
206
+ + rake release:list:github # list GitHub releases
207
+ + rake release:
208
+ + ```
209
+ + where remote is "github"
210
+ +
211
+ + Note that this is a breaking change, and it is approved. Do not provide backwards compatibility for the old tasks. Move forward
212
+ +
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
+ Status: closed
@@ -10,8 +10,6 @@ module Gempilot
10
10
  end
11
11
 
12
12
  def create
13
- sh "git", "push"
14
- sh "git", "push", "--tags"
15
13
  sh "gh", "release", "create",
16
14
  "--generate-notes", "--fail-on-no-commits",
17
15
  tag
@@ -0,0 +1,52 @@
1
+ require "open3"
2
+
3
+ module Gempilot
4
+ ## Pushes the current branch and a release tag to the branch's git remote.
5
+ ## Backs the +release:source_control_push+ task. Idempotent: pushing an
6
+ ## already-pushed branch or tag is a no-op, so re-running a release never
7
+ ## fails on an existing tag (unlike bundler's +already_tagged?+ guard, which
8
+ ## skips the push entirely once the tag exists locally).
9
+ class Origin
10
+ include StrictShell
11
+
12
+ attr_reader :tag
13
+
14
+ def initialize(tag)
15
+ @tag = tag
16
+ end
17
+
18
+ def push
19
+ sh "git", "push", remote, "refs/heads/#{branch}"
20
+ sh "git", "push", remote, "refs/tags/#{tag}"
21
+ end
22
+
23
+ private
24
+
25
+ def branch
26
+ @branch ||= resolve_branch
27
+ end
28
+
29
+ def resolve_branch
30
+ name = capture("git", "rev-parse", "--abbrev-ref", "HEAD")
31
+ raise "Cannot push from a detached HEAD; check out a branch first" if name == "HEAD"
32
+
33
+ name
34
+ end
35
+
36
+ def remote
37
+ @remote ||= configured_remote || "origin"
38
+ end
39
+
40
+ def configured_remote
41
+ out, status = Open3.capture2("git", "config", "--get", "branch.#{branch}.remote")
42
+ out.strip if status.success?
43
+ end
44
+
45
+ def capture(*args)
46
+ out, status = Open3.capture2(*args)
47
+ raise "Command #{args.join(" ").inspect} failed (exit #{status.exitstatus})" unless status.success?
48
+
49
+ out.strip
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,73 @@
1
+ module Gempilot
2
+ ## Rake task definitions for publishing a release to RubyGems and GitHub.
3
+ ## Mixed into VersionTask. Assumes +bundler/gem_tasks+ has been required so the
4
+ ## +build+, +release:guard_clean+, and +release:rubygem_push+ tasks exist (the
5
+ ## generated Rakefile guarantees this). Fixes GitHub releasing by replacing
6
+ ## bundler's +already_tagged?+-guarded +release:source_control_push+ with an
7
+ ## idempotent push.
8
+ module ReleaseTasks
9
+ private
10
+
11
+ def define_release_tasks(project)
12
+ override_source_control_push(project)
13
+ define_release_namespace(project)
14
+ define_root_release_task
15
+ define_unrelease_tasks(project)
16
+ end
17
+
18
+ def override_source_control_push(project)
19
+ clear_task "release:source_control_push"
20
+ task("release:source_control_push") { Origin.new(project.version_tag).push }
21
+ end
22
+
23
+ def define_release_namespace(project)
24
+ namespace :release do
25
+ define_rubygems_release
26
+ define_github_release(project)
27
+ define_release_list(project)
28
+ end
29
+ end
30
+
31
+ def define_rubygems_release
32
+ desc "Release the current version to RubyGems"
33
+ task rubygems: %w[build release:guard_clean release:source_control_push release:rubygem_push]
34
+ end
35
+
36
+ def define_github_release(project)
37
+ desc "Create a GitHub release for the current version"
38
+ task github: "release:source_control_push" do
39
+ GithubRelease.new(project.version_tag).create
40
+ end
41
+ end
42
+
43
+ def define_release_list(project)
44
+ namespace :list do
45
+ desc "List GitHub releases"
46
+ task(:github) { GithubRelease.new(project.version_tag).list }
47
+ end
48
+ end
49
+
50
+ def define_root_release_task
51
+ clear_task "release"
52
+ desc "Release the current version to all remotes"
53
+ task release: %w[release:rubygems release:github]
54
+ end
55
+
56
+ def define_unrelease_tasks(project)
57
+ desc "Delete the current release from all remotes that support it"
58
+ task unrelease: %w[unrelease:github]
59
+ define_unrelease_namespace(project)
60
+ end
61
+
62
+ def define_unrelease_namespace(project)
63
+ namespace :unrelease do
64
+ desc "Delete the GitHub release for the current version"
65
+ task(:github) { GithubRelease.new(project.version_tag).destroy }
66
+ end
67
+ end
68
+
69
+ def clear_task(name)
70
+ Rake::Task[name].clear if Rake::Task.task_defined?(name)
71
+ end
72
+ end
73
+ end
@@ -1,3 +1,3 @@
1
1
  module Gempilot
2
- VERSION = "0.2.3".freeze
2
+ VERSION = "0.2.4".freeze
3
3
  end
@@ -4,6 +4,8 @@ require_relative "../gempilot"
4
4
  module Gempilot
5
5
  ## Rake tasks for version lifecycle management.
6
6
  class VersionTask < Rake::TaskLib
7
+ include ReleaseTasks
8
+
7
9
  attr_reader :project
8
10
 
9
11
  def initialize(root: Dir.pwd)
@@ -17,7 +19,7 @@ module Gempilot
17
19
  def define_tasks
18
20
  define_version_tasks
19
21
  define_version_composite_tasks
20
- define_github_tasks
22
+ define_release_tasks(@project)
21
23
  end
22
24
 
23
25
  def define_version_tasks
@@ -89,20 +91,5 @@ module Gempilot
89
91
  project.refresh_version!
90
92
  end
91
93
  end
92
-
93
- def define_github_tasks
94
- project = @project
95
-
96
- namespace "version:github" do
97
- desc "Create a GitHub release for the current version"
98
- task(:release) { GithubRelease.new(project.version_tag).create }
99
-
100
- desc "Delete the GitHub release for the current version"
101
- task(:unrelease) { GithubRelease.new(project.version_tag).destroy }
102
-
103
- desc "List GitHub releases"
104
- task(:list) { GithubRelease.new(project.version_tag).list }
105
- end
106
- end
107
94
  end
108
95
  end
data/vendor/vendored.gemv CHANGED
Binary file
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.2.3
4
+ version: 0.2.4
5
5
  platform: ruby
6
6
  authors:
7
7
  - David Gillis
@@ -106,6 +106,8 @@ files:
106
106
  - data/templates/gem/test/zeitwerk_test.rb.erb
107
107
  - data/templates/new/.keep
108
108
  - data/templates/new/command.rb.erb
109
+ - docs/superpowers/plans/2026-07-20-release-task-hierarchy.md
110
+ - docs/superpowers/specs/2026-07-20-release-task-hierarchy-design.md
109
111
  - exe/gempilot
110
112
  - issues.rec
111
113
  - lib/core_ext/string/inflection_methods.rb
@@ -124,8 +126,10 @@ files:
124
126
  - lib/gempilot/cli/generator.rb
125
127
  - lib/gempilot/gem_constant.rb
126
128
  - lib/gempilot/github_release.rb
129
+ - lib/gempilot/origin.rb
127
130
  - lib/gempilot/project.rb
128
131
  - lib/gempilot/project/version.rb
132
+ - lib/gempilot/release_tasks.rb
129
133
  - lib/gempilot/strict_shell.rb
130
134
  - lib/gempilot/version.rb
131
135
  - lib/gempilot/version_tag.rb
@@ -151,7 +155,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
151
155
  - !ruby/object:Gem::Version
152
156
  version: '0'
153
157
  requirements: []
154
- rubygems_version: 4.0.15
158
+ rubygems_version: 4.0.16
155
159
  specification_version: 4
156
160
  summary: A toolkit for creating, managing, and releasing your own rubygems
157
161
  test_files: []