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.
Files changed (32) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +15 -1
  3. data/CLAUDE.md +2 -9
  4. data/README.md +8 -4
  5. data/data/templates/gem/dotfiles/rubocop.yml.erb +1 -12
  6. data/docs/superpowers/plans/2026-07-20-release-task-hierarchy.md +583 -0
  7. data/docs/superpowers/specs/2026-07-20-release-task-hierarchy-design.md +229 -0
  8. data/issues.rec +74 -3
  9. data/lib/gempilot/cli/commands/console.rb +2 -3
  10. data/lib/gempilot/cli/commands/create.rb +6 -3
  11. data/lib/gempilot/cli/commands/new.rb +2 -3
  12. data/lib/gempilot/cli/commands/release.rb +2 -3
  13. data/lib/gempilot/cli/generator.rb +2 -1
  14. data/lib/gempilot/github_release.rb +0 -2
  15. data/lib/gempilot/origin.rb +52 -0
  16. data/lib/gempilot/project.rb +25 -5
  17. data/lib/gempilot/release_tasks.rb +73 -0
  18. data/lib/gempilot/version.rb +1 -1
  19. data/lib/gempilot/version_task.rb +3 -16
  20. data/vendor/vendored.gemv +0 -0
  21. metadata +8 -14
  22. data/docs/command_kit_comparison.md +0 -249
  23. data/docs/command_kit_reference.md +0 -517
  24. data/docs/plans/2026-02-18-gempilot-add-command.md +0 -718
  25. data/docs/superpowers/plans/2026-04-01-rubocop-new-config.md +0 -838
  26. data/docs/superpowers/plans/2026-04-06-dogfood-inflectable.md +0 -659
  27. data/docs/superpowers/plans/2026-04-06-inflection-tests-and-erb-rename.md +0 -166
  28. data/docs/superpowers/plans/2026-04-06-integrate-version-tools.md +0 -162
  29. data/docs/superpowers/plans/2026-04-06-new-readme.md +0 -185
  30. data/docs/superpowers/plans/2026-06-09-address-review-critiques.md +0 -679
  31. data/docs/version-management-redesign.md +0 -44
  32. data/notes.md +0 -31
@@ -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
@@ -26,7 +26,7 @@ Id: F0833E66-A35C-4054-884B-F90C81F08838
26
26
  Updated: Mon, 02 Mar 2026 10:37:33 -0500
27
27
  Title: Error handling should be graceful
28
28
  Description: I did "gempilot create", and entered "A foo" as the title, and it crashed horrifically. This particular example should have validation at the minimum. Need to also use command kit's error bubbling/handling mechanism to swallow errors and print message to user. Can always use verbose or debug mode to see full trace.
29
- Status: open
29
+ Status: closed
30
30
 
31
31
  Id: 741FFD05-9BD7-44EB-BB99-E7D705F12681
32
32
  Updated: Mon, 02 Mar 2026 11:25:24 -0500
@@ -68,7 +68,7 @@ Id: 74D0FD90-0421-4452-8591-2798EE6FD23E
68
68
  Updated: Sat, 28 Mar 2026 12:59:40 -0400
69
69
  Title: Publish gem
70
70
  Description: At some point the repo should be moved to public viz, and the gem should be published.
71
- Status: open
71
+ Status: closed
72
72
 
73
73
  Id: 8C356024-3122-11F1-89F3-FE6CB9572C2F
74
74
  Updated: Mon, 06 Apr 2026 00:00:00 -0400
@@ -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
@@ -141,3 +141,74 @@ Description: "
141
141
  + ```
142
142
  + "
143
143
  Status: closed
144
+
145
+ Id: 1C46F71C-70E9-11F1-B65B-FE6CB9572C2E
146
+ Updated: Thu, 25 Jun 2026 18:56:32 -0400
147
+ Title: Incorrectly creates test file
148
+ Description: When doing `gempilot new Reversal::Server`, in a project called "reversal-store", I get:
149
+ +
150
+ + gempilot new class Reversal::Server
151
+ +
152
+ + Adding class Reversal::Server...
153
+ +
154
+ + create lib/reversal/server.rb
155
+ + create test/reversal/store_test.rb
156
+ +
157
+ + The test file is incorrectly named. Should be server_test.rb
158
+ Status: open
159
+
160
+ Id: F5486B48-71DB-11F1-980F-FE6CB9572C2E
161
+ Updated: Fri, 26 Jun 2026 23:54:55 -0400
162
+ Title: Rubymine <=> Minitest usage causes "no reporters allowed error"
163
+ Description: Has to do with how the ENV var RM_INFO is being interpreted
164
+ Status: open
165
+
166
+ Id: 8D847572-7273-11F1-B508-FE6CB9572C2E
167
+ Updated: Fri, 03 Jul 2026 22:38:22 +0000
168
+ Title: Gem extensions are broken
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: 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
@@ -27,9 +27,8 @@ module Gempilot
27
27
  end
28
28
 
29
29
  def print_console_banner
30
- puts colors.bright_white("Starting console for ") +
31
- colors.bold(colors.cyan(@gem_name)) +
32
- colors.bright_white("...")
30
+ styled_name = colors.bold(colors.cyan(@gem_name))
31
+ puts colors.bright_white("Starting console for ") + styled_name + colors.bright_white("...")
33
32
  puts
34
33
  end
35
34
  end
@@ -156,7 +156,8 @@ module Gempilot
156
156
 
157
157
  puts
158
158
  puts colors.bright_black("An executable in exe/ lets users run your gem from the command line.")
159
- options[:exe] = ask_yes_or_no(colors.green("Create an executable"), default: false)
159
+ prompt = colors.green("Create an executable")
160
+ options[:exe] = ask_yes_or_no(prompt, default: false)
160
161
  end
161
162
 
162
163
  def collect_git_options
@@ -169,7 +170,8 @@ module Gempilot
169
170
 
170
171
  puts
171
172
  puts colors.bright_black("Initialize a git repository with an initial commit.")
172
- options[:git] = ask_yes_or_no(colors.green("Initialize git repo"), default: true)
173
+ prompt = colors.green("Initialize git repo")
174
+ options[:git] = ask_yes_or_no(prompt, default: true)
173
175
  end
174
176
 
175
177
  def collect_branch
@@ -181,8 +183,9 @@ module Gempilot
181
183
  end
182
184
 
183
185
  def print_header
186
+ styled_name = colors.bold(colors.cyan(@gem_name))
184
187
  puts
185
- puts colors.bright_white("Creating gem '") + colors.bold(colors.cyan(@gem_name)) + colors.bright_white("'...")
188
+ puts colors.bright_white("Creating gem '") + styled_name + colors.bright_white("'...")
186
189
  puts
187
190
  end
188
191
 
@@ -77,10 +77,9 @@ module Gempilot
77
77
  end
78
78
 
79
79
  def print_adding_banner(kind, label)
80
+ styled_label = colors.bold(colors.cyan(label))
80
81
  puts
81
- puts colors.bright_white("Adding #{kind} ") +
82
- colors.bold(colors.cyan(label)) +
83
- colors.bright_white("...")
82
+ puts colors.bright_white("Adding #{kind} ") + styled_label + colors.bright_white("...")
84
83
  puts
85
84
  end
86
85
 
@@ -18,9 +18,8 @@ module Gempilot
18
18
  private
19
19
 
20
20
  def print_release_banner
21
- puts colors.bright_white("Releasing ") +
22
- colors.bold(colors.cyan(@gem_name)) +
23
- colors.bright_white("...")
21
+ styled_name = colors.bold(colors.cyan(@gem_name))
22
+ puts colors.bright_white("Releasing ") + styled_name + colors.bright_white("...")
24
23
  puts
25
24
  end
26
25
 
@@ -37,8 +37,9 @@ module Gempilot
37
37
  end
38
38
 
39
39
  def print_action(command, dest, source: nil)
40
+ styled_command = colors.bold(colors.green(command))
40
41
  line = +""
41
- line << "\t" << colors.bold(colors.green(command))
42
+ line << "\t" << styled_command
42
43
  line << "\t" << colors.green(source) if source
43
44
  line << "\t" << colors.green(dest) if dest
44
45
 
@@ -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
@@ -1,8 +1,9 @@
1
- require "pathname"
2
1
  require "warning"
3
2
 
4
3
  module Gempilot
5
4
  ## Introspects a gem project to discover its name, module, and version.
5
+ ## Works for both regular gems (+lib/my_gem.rb+) and extension gems whose
6
+ ## entry point nests deeper (+lib/my_gem/extension.rb+ for +my_gem-extension+).
6
7
  class Project
7
8
  class ProjectIntrospectionError < StandardError; end
8
9
 
@@ -29,11 +30,11 @@ module Gempilot
29
30
  end
30
31
 
31
32
  def name
32
- lib_project.basename.to_s
33
+ project_segments.join("-")
33
34
  end
34
35
 
35
36
  def klass
36
- Object.const_get(name.camelize)
37
+ Object.const_get(project_segments.map(&:camelize).join("::"))
37
38
  end
38
39
 
39
40
  def version
@@ -67,6 +68,10 @@ module Gempilot
67
68
 
68
69
  private
69
70
 
71
+ def project_segments
72
+ lib_project.relative_path_from(lib).each_filename.to_a
73
+ end
74
+
70
75
  def with_version_file
71
76
  version.path.open(File::RDWR, 0o644) do |f|
72
77
  f.flock File::LOCK_EX
@@ -76,8 +81,7 @@ module Gempilot
76
81
  end
77
82
 
78
83
  def fetch_lib_project
79
- files = lib.glob("*.rb")
80
- dirs = files.map { it.sub_ext("") }.select(&:directory?)
84
+ dirs = shallowest_entry_dirs
81
85
  case dirs.count
82
86
  in 0 then raise ProjectIntrospectionError, "Could not identify project dir"
83
87
  in (2..)
@@ -87,6 +91,22 @@ module Gempilot
87
91
  end
88
92
  end
89
93
 
94
+ def shallowest_entry_dirs
95
+ entry_dirs.group_by { depth_below_lib(it) }
96
+ .min_by(&:first)
97
+ &.last || []
98
+ end
99
+
100
+ def entry_dirs
101
+ lib.glob("**/*.rb")
102
+ .map { it.sub_ext("") }
103
+ .select(&:directory?)
104
+ end
105
+
106
+ def depth_below_lib(path)
107
+ path.relative_path_from(lib).each_filename.count
108
+ end
109
+
90
110
  def fetch_version
91
111
  Warning.ignore(REDEFINITION_WARNING)
92
112
  Warning.ignore(REINITIALIZATION_WARNING)
@@ -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.2".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
Binary file