gempilot 0.2.3 → 0.3.0

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