bake-gem 0.11.0 → 0.12.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9350c6f56e7017043a435dad7b6a1c33c8e5094607928bd584bef68701ee143e
4
- data.tar.gz: 1e4ec888c6149c52f91575edc84dd5c65940349cb74cb7a3ec20e71678af693d
3
+ metadata.gz: 7b25175c2d6cf7d1761d1e907fcd305627d7d8aeb394877b02133ebb4b04ce83
4
+ data.tar.gz: dc59a56876e85654723fa2b1e600078f99f9c70b34b4e7538796788f9d34f3c9
5
5
  SHA512:
6
- metadata.gz: 7571129ebde2c248ca42c6b30231b2299a8485f8a47ae62eb46e56bb626ffde738d5878e7273b8482cea821a68e89260f161e11fa6c114b0b19835f9f3ba093e
7
- data.tar.gz: 0eb9329e17cf0d610d9da131717fd20bc20f671f6574afa405f41c1231aa8d5ebf4b4e1e030d16335a1ea08dd069fd4abc5dc51df4c3f66ddbeba9582234cf2e
6
+ metadata.gz: 53bbab1dc7b1ce0ee92b176624a9a0032ffebd2a482c13477187c6e03965871f1f95c132e84aad5f0361930dc32cd3f2827df5e36cdb5a4e8f405ba8faa17198
7
+ data.tar.gz: 6af3f18bee98f04af4c700c4282873eec88d927945e43176c9a24cff5dbc78eb3f88097d817e43373243c51894c41c4a41d855a949a343d827ef9ecd908fca5b
checksums.yaml.gz.sig CHANGED
Binary file
data/agent.md ADDED
@@ -0,0 +1,61 @@
1
+ # Agent
2
+
3
+ ## Context
4
+
5
+ This section provides links to documentation from installed packages. It is automatically generated and may be updated by running `bake agent:context:install`.
6
+
7
+ **Important:** Before performing any code, documentation, or analysis tasks, always read and apply the full content of any relevant documentation referenced in the following sections. These context files contain authoritative standards and best practices for documentation, code style, and project-specific workflows. **Do not proceed with any actions until you have read and incorporated the guidance from relevant context files.**
8
+
9
+ **Setup Instructions:** If the referenced files are not present or if dependencies have been updated, run `bake agent:context:install` to install the latest context files.
10
+
11
+ ### agent-context
12
+
13
+ Install and manage context files from Ruby gems.
14
+
15
+ #### [Getting Started](.context/agent-context/getting-started.md)
16
+
17
+ This guide explains how to use `agent-context`, a tool for discovering and installing contextual information from Ruby gems to help AI agents.
18
+
19
+ ### decode
20
+
21
+ Code analysis for documentation generation.
22
+
23
+ #### [Getting Started with Decode](.context/decode/getting-started.md)
24
+
25
+ The Decode gem provides programmatic access to Ruby code structure and metadata. It can parse Ruby files and extract definitions, comments, and documentation pragmas, enabling code analysis, documentation generation, and other programmatic manipulations of Ruby codebases.
26
+
27
+ #### [Documentation Coverage](.context/decode/coverage.md)
28
+
29
+ This guide explains how to test and monitor documentation coverage in your Ruby projects using the Decode gem's built-in bake tasks.
30
+
31
+ #### [Ruby Documentation](.context/decode/ruby-documentation.md)
32
+
33
+ This guide covers documentation practices and pragmas supported by the Decode gem for documenting Ruby code. These pragmas provide structured documentation that can be parsed and used to generate API documentation and achieve complete documentation coverage.
34
+
35
+ #### [Setting Up RBS Types and Steep Type Checking for Ruby Gems](.context/decode/types.md)
36
+
37
+ This guide covers the process for establishing robust type checking in Ruby gems using RBS and Steep, focusing on automated generation from source documentation and proper validation.
38
+
39
+ ### sus
40
+
41
+ A fast and scalable test runner.
42
+
43
+ #### [Using Sus Testing Framework](.context/sus/usage.md)
44
+
45
+ Sus is a modern Ruby testing framework that provides a clean, BDD-style syntax for writing tests. It's designed to be fast, simple, and expressive.
46
+
47
+ #### [Mocking](.context/sus/mocking.md)
48
+
49
+ There are two types of mocking in sus: `receive` and `mock`. The `receive` matcher is a subset of full mocking and is used to set expectations on method calls, while `mock` can be used to replace method implementations or set up more complex behavior.
50
+
51
+ #### [Shared Test Behaviors and Fixtures](.context/sus/shared.md)
52
+
53
+ Sus provides shared test contexts which can be used to define common behaviours or tests that can be reused across one or more test files.
54
+
55
+ ### sus-fixtures-console
56
+
57
+ Test fixtures for capturing Console output.
58
+
59
+ #### [Getting Started](.context/sus-fixtures-console/getting-started.md)
60
+
61
+ This guide explains how to use the `Sus::Fixtures::Console` gem to redirect console logging output during tests.
@@ -3,10 +3,6 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2021-2025, by Samuel Williams.
5
5
 
6
- require_relative "../../../lib/bake/gem/shell"
7
-
8
- include Bake::Gem::Shell
9
-
10
6
  # Increment the patch number of the current version.
11
7
  def patch
12
8
  commit([nil, nil, 1], message: "Bump patch version.")
@@ -36,9 +32,7 @@ def commit(bump, message: "Bump version.")
36
32
  version_path = context.lookup("gem:release:version:increment").call(bump, message: message)
37
33
 
38
34
  if version_path
39
- system("git", "checkout", "-b", "release-v#{gemspec.version}")
40
- system("git", "add", version_path, chdir: context.root)
41
- system("git", "commit", "-m", message, chdir: context.root)
35
+ branch_name = helper.create_release_branch(version_path, message: message)
42
36
  else
43
37
  raise "Could not find version number!"
44
38
  end
@@ -46,6 +40,6 @@ def commit(bump, message: "Bump version.")
46
40
  return {
47
41
  version: gemspec.version,
48
42
  version_path: version_path,
49
- branch: "release-v#{gemspec.version}",
43
+ branch: branch_name,
50
44
  }
51
45
  end
@@ -3,10 +3,6 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2021-2025, by Samuel Williams.
5
5
 
6
- require_relative "../../../lib/bake/gem/shell"
7
-
8
- include Bake::Gem::Shell
9
-
10
6
  # Increment the patch number of the current version.
11
7
  def patch
12
8
  commit([nil, nil, 1], message: "Bump patch version.")
@@ -59,8 +55,7 @@ def commit(bump, message: "Bump version.")
59
55
  version_path = increment(bump, message: message)
60
56
 
61
57
  if version_path
62
- system("git", "add", "--all", chdir: context.root)
63
- system("git", "commit", "-m", message, chdir: context.root)
58
+ helper.commit_version_changes(message: message)
64
59
  else
65
60
  raise "Could not find version number!"
66
61
  end
@@ -0,0 +1,35 @@
1
+
2
+ # frozen_string_literal: true
3
+
4
+ # Released under the MIT License.
5
+ # Copyright, 2021-2025, by Samuel Williams.
6
+
7
+ # Bump the patch version and release the gem in one command.
8
+ # @parameter tag [Boolean] Whether to tag the release.
9
+ def patch(tag: true)
10
+ version_commit_task = context.lookup("gem:release:version:commit")
11
+ version_commit_task.call([nil, nil, 1], message: "Bump patch version.")
12
+
13
+ release_task = context.lookup("gem:release")
14
+ release_task.call(tag: tag)
15
+ end
16
+
17
+ # Bump the minor version and release the gem in one command.
18
+ # @parameter tag [Boolean] Whether to tag the release.
19
+ def minor(tag: true)
20
+ version_commit_task = context.lookup("gem:release:version:commit")
21
+ version_commit_task.call([nil, 1, 0], message: "Bump minor version.")
22
+
23
+ release_task = context.lookup("gem:release")
24
+ release_task.call(tag: tag)
25
+ end
26
+
27
+ # Bump the major version and release the gem in one command.
28
+ # @parameter tag [Boolean] Whether to tag the release.
29
+ def major(tag: true)
30
+ version_commit_task = context.lookup("gem:release:version:commit")
31
+ version_commit_task.call([1, 0, 0], message: "Bump major version.")
32
+
33
+ release_task = context.lookup("gem:release")
34
+ release_task.call(tag: tag)
35
+ end
data/bake/gem.rb CHANGED
@@ -3,14 +3,13 @@
3
3
  # Released under the MIT License.
4
4
  # Copyright, 2021-2025, by Samuel Williams.
5
5
 
6
- require_relative "../lib/bake/gem/helper"
7
- require_relative "../lib/bake/gem/shell"
8
-
9
- include Bake::Gem::Shell
10
-
6
+ # Initialize the gem context with helper for gem operations.
7
+ # @parameter context [Bake::Context] The bake execution context.
11
8
  def initialize(context)
12
9
  super(context)
13
10
 
11
+ require_relative "../lib/bake/gem/helper"
12
+
14
13
  @helper = Bake::Gem::Helper.new(context.root)
15
14
  end
16
15
 
@@ -18,9 +17,7 @@ attr :helper
18
17
 
19
18
  # List all the files that will be included in the gem:
20
19
  def files
21
- @helper.gemspec.files.each do |path|
22
- $stdout.puts path
23
- end
20
+ @helper.gemspec.files
24
21
  end
25
22
 
26
23
  # Build the gem into the pkg directory.
@@ -54,44 +51,24 @@ def release(tag: true)
54
51
  @helper.guard_clean
55
52
 
56
53
  version = @helper.gemspec.version
54
+ current_branch = @helper.current_branch
57
55
 
58
- if tag
59
- name = "v#{version}"
60
- system("git", "fetch", "--all", "--tags")
61
- system("git", "tag", name)
62
- end
56
+ tag_name = @helper.create_release_tag(tag: tag, version: version)
63
57
 
64
58
  begin
65
- path = @helper.build_gem
59
+ path = @helper.build_gem_in_worktree
66
60
  @helper.push_gem(path: path)
67
61
  rescue => error
68
- system("git", "tag", "--delete", name)
62
+ @helper.delete_git_tag(tag_name) if tag_name
69
63
  raise
70
64
  end
71
65
 
72
- # If we are on a branch, push, otherwise just push the tags (assuming shallow checkout):
73
- if current_branch
74
- system("git", "push")
75
- end
76
-
77
- system("git", "push", "--tags")
66
+ @helper.push_release(current_branch: current_branch)
78
67
 
79
68
  return {
80
69
  name: @helper.gemspec.name,
81
70
  version: @helper.gemspec.version,
82
71
  package_path: path,
83
- tag: name,
72
+ tag: tag_name,
84
73
  }
85
74
  end
86
-
87
- private
88
-
89
- # Figure out if there is a current branch, if not, return `nil`.
90
- def current_branch
91
- # We originally used this but it is not supported by older versions of git.
92
- # readlines("git", "branch", "--show-current").first&.chomp
93
-
94
- readlines("git", "symbolic-ref", "--short", "--quiet", "HEAD").first&.chomp
95
- rescue
96
- nil
97
- end
@@ -0,0 +1,228 @@
1
+ # Getting Started
2
+
3
+ This guide explains how to use `bake-gem` to release gems safely and efficiently.
4
+
5
+ ## Installation
6
+
7
+ Add the `bake-gem` gem to your project:
8
+
9
+ ``` bash
10
+ $ bundle add bake-gem
11
+ ```
12
+
13
+ You may prefer to keep it in a separate `maintenance` group:
14
+
15
+ ``` ruby
16
+ group :maintenance, optional: true do
17
+ gem "bake-gem"
18
+ end
19
+ ```
20
+
21
+ ## Usage
22
+
23
+ Before using `bake-gem`, ensure you have:
24
+
25
+ 1. A properly configured `gemspec` file in your project root
26
+ 2. A clean git repository (no uncommitted changes)
27
+ 3. Your gem's version file (typically `lib/your_gem/version.rb`)
28
+ 4. RubyGems credentials configured for publishing
29
+
30
+ ### Local Release Process
31
+
32
+ The most typical process for releasing a gem locally:
33
+
34
+ ``` bash
35
+ $ bake gem:release:version:patch gem:release
36
+ ```
37
+
38
+ This command will:
39
+ 1. **Guard against consecutive version bumps** - Prevents accidentally bumping version twice
40
+ 2. **Check repository cleanliness** - Ensures no uncommitted changes
41
+ 3. **Increment the version** - Updates your version file (patch/minor/major)
42
+ 4. **Commit the version change** - Creates a commit with the version bump
43
+ 5. **Build the gem in a clean worktree** - Isolates the build process
44
+ 6. **Push to RubyGems** - Publishes your gem
45
+ 7. **Create and push git tags** - Tags the release
46
+
47
+ ### Version Increment Options
48
+
49
+ Choose the appropriate version increment:
50
+
51
+ ``` bash
52
+ # For bug fixes (1.0.0 -> 1.0.1)
53
+ $ bake gem:release:version:patch gem:release
54
+
55
+ # For new features (1.0.0 -> 1.1.0)
56
+ $ bake gem:release:version:minor gem:release
57
+
58
+ # For breaking changes (1.0.0 -> 2.0.0)
59
+ $ bake gem:release:version:major gem:release
60
+ ```
61
+
62
+ ## Advanced Workflows
63
+
64
+ ### Automated CI/CD Pipeline
65
+
66
+ For releasing gems via automated pipelines, use a two-step process:
67
+
68
+ #### Step 1: Create Release Branch (Locally)
69
+
70
+ ``` bash
71
+ # Create a release branch with version bump
72
+ $ bake gem:release:branch:patch # or minor/major
73
+ ```
74
+
75
+ This will:
76
+ - Create a new branch named `releases/v[new-version]`
77
+ - Bump the gem version
78
+ - Commit the version change
79
+ - Push the branch to origin
80
+
81
+ #### Step 2: Release from CI (After Merge)
82
+
83
+ Once the release branch is merged into main:
84
+
85
+ ``` bash
86
+ $ export RUBYGEMS_HOST=https://rubygems.org
87
+ $ export GEM_HOST_API_KEY=your_api_key
88
+
89
+ $ bake gem:release
90
+ ```
91
+
92
+ ### Individual Commands
93
+
94
+ You can also run individual steps:
95
+
96
+ ``` bash
97
+ # Just build the gem
98
+ $ bake gem:build
99
+
100
+ # Install the gem locally for testing
101
+ $ bake gem:install
102
+
103
+ # List files that will be included in the gem
104
+ $ bake gem:files
105
+
106
+ # Build without signing
107
+ $ bake gem:build signing_key=false
108
+ ```
109
+
110
+ ## Safety Features
111
+
112
+ `bake-gem` includes several safety features:
113
+
114
+ ### Consecutive Version Bump Prevention
115
+ The tool automatically prevents consecutive version bumps by checking the last commit message. If the last commit was already a version bump (e.g., "Bump patch version."), it will raise an error.
116
+
117
+ ### Clean Worktree Building
118
+ Gems are built in isolated git worktrees to ensure the build environment exactly matches your committed code, preventing issues with uncommitted changes affecting the build.
119
+
120
+ ### Repository Cleanliness Check
121
+ Before any release operation, the tool ensures your repository has no uncommitted changes.
122
+
123
+ ## Configuration
124
+
125
+ ### Gem Signing
126
+
127
+ To sign your gems, ensure your gemspec includes:
128
+
129
+ ``` ruby
130
+ spec.signing_key = "path/to/private_key.pem"
131
+ spec.cert_chain = ["path/to/certificate.pem"]
132
+ ```
133
+
134
+ Or disable signing explicitly:
135
+
136
+ ``` bash
137
+ $ bake gem:build signing_key=false
138
+ ```
139
+
140
+ ### RubyGems Configuration
141
+
142
+ For automated releases, set these environment variables:
143
+
144
+ ``` bash
145
+ export RUBYGEMS_HOST=https://rubygems.org # or your private gem server
146
+ export GEM_HOST_API_KEY=your_api_key
147
+ ```
148
+
149
+ ## Examples
150
+
151
+ ### Complete Release Example
152
+
153
+ ``` bash
154
+ # 1. Ensure clean repository
155
+ $ git status
156
+
157
+ # 2. Run tests
158
+ $ bundle exec rake test # or your test command
159
+
160
+ # 3. Release with patch version increment
161
+ $ bake gem:release:version:patch gem:release
162
+
163
+ # Output:
164
+ # Updated version: v1.2.4
165
+ # Successfully built RubyGem
166
+ # Name: my-gem
167
+ # Version: 1.2.4
168
+ # File: my-gem-1.2.4.gem
169
+ # Pushing gem to https://rubygems.org...
170
+ # Tagged: v1.2.4
171
+ ```
172
+
173
+ ### Branch-based Release Example
174
+
175
+ ``` bash
176
+ # Create release branch
177
+ $ bake gem:release:branch:minor
178
+ # Creates branch: releases/v1.3.0
179
+ # Commits version bump
180
+ # Pushes branch
181
+
182
+ # After code review and merge:
183
+ $ git checkout main
184
+ $ git pull
185
+ $ bake gem:release
186
+ ```
187
+
188
+ ## Troubleshooting
189
+
190
+ ### Common Issues
191
+
192
+ **"Repository has uncommitted changes"**
193
+ ```bash
194
+ $ git status # Check what's uncommitted
195
+ $ git add . # Stage changes
196
+ $ git commit -m "Prepare for release" # Or stash them
197
+ ```
198
+
199
+ **"Last commit appears to be a version bump"**
200
+ ```bash
201
+ # Make some changes first, or use --force if intentional
202
+ $ git log -1 # Check the last commit message
203
+ ```
204
+
205
+ **"Multiple gemspecs found"**
206
+ ```bash
207
+ # Specify which gemspec to use or remove extras
208
+ $ ls *.gemspec
209
+ ```
210
+
211
+ **"No version file found"**
212
+ ```bash
213
+ # Ensure your version file follows the expected pattern:
214
+ # VERSION = "1.0.0"
215
+ ```
216
+
217
+ ### Getting Help
218
+
219
+ List all available gem commands:
220
+ ``` bash
221
+ $ bake list | grep gem
222
+ ```
223
+
224
+ Get help for specific commands:
225
+ ``` bash
226
+ $ bake gem:release --help
227
+ ```
228
+ ````
@@ -0,0 +1,13 @@
1
+ # Automatically generated context index for Utopia::Project guides.
2
+ # Do not edit then files in this directory directly, instead edit the guides and then run `bake utopia:project:agent:context:update`.
3
+ ---
4
+ description: Release management for Ruby gems.
5
+ metadata:
6
+ documentation_uri: https://ioquatix.github.io/bake-gem/
7
+ funding_uri: https://github.com/sponsors/ioquatix/
8
+ source_code_uri: https://github.com/ioquatix/bake-gem.git
9
+ files:
10
+ - path: getting-started.md
11
+ title: Getting Started
12
+ description: This guide explains how to use `bake-gem` to release gems safely and
13
+ efficiently.
@@ -6,11 +6,13 @@
6
6
  require "rubygems"
7
7
  require "rubygems/package"
8
8
  require "fileutils"
9
+ require "tmpdir"
9
10
 
10
11
  require_relative "shell"
11
12
 
12
13
  module Bake
13
14
  module Gem
15
+ # Represents a gem version with support for parsing and incrementing version numbers.
14
16
  class Version
15
17
  LINE_PATTERN = /VERSION = ['"](?<version>(?<parts>\d+\.\d+\.\d+)(-(?<suffix>.*?))?)['"]/
16
18
 
@@ -28,11 +30,16 @@ module Bake
28
30
  end
29
31
  end
30
32
 
33
+ # Initialize a new version with the given parts and optional suffix.
34
+ # @parameter parts [Array(Integer)] The version number parts (e.g., [1, 2, 3] for "1.2.3").
35
+ # @parameter suffix [String | Nil] The optional version suffix (e.g., "alpha", "beta").
31
36
  def initialize(parts, suffix)
32
37
  @parts = parts
33
38
  @suffix = suffix
34
39
  end
35
40
 
41
+ # Check if this version represents a release version.
42
+ # @returns [Boolean] True if the version has no suffix, indicating it's a release version.
36
43
  def release?
37
44
  @suffix.nil?
38
45
  end
@@ -51,10 +58,13 @@ module Bake
51
58
  "v#{join}"
52
59
  end
53
60
 
61
+ # Increment the version according to the provided bump specification.
62
+ # @parameter bump [Array(Integer | Nil)] Array specifying how to increment each version part.
63
+ # @returns [Version] Self, for method chaining.
54
64
  def increment(bump)
55
65
  bump.each_with_index do |increment, index|
56
66
  if index > @parts.size
57
- @suffix = bump[index..-1].join(".")
67
+ @suffix = bump[index..].join(".")
58
68
  break
59
69
  end
60
70
 
@@ -63,30 +73,49 @@ module Bake
63
73
  elsif increment == 0
64
74
  @parts[index] = 0
65
75
  end
76
+ # If increment is nil, we don't change that part of the version
66
77
  end
67
78
 
68
79
  return self
69
80
  end
70
81
  end
71
82
 
83
+ # Helper class for performing gem-related operations like building, installing, and publishing gems.
72
84
  class Helper
73
85
  include Shell
74
86
 
87
+ # Initialize a new helper with the specified root directory and optional gemspec.
88
+ # @parameter root [String] The root directory of the gem project.
89
+ # @parameter gemspec [Gem::Specification | Nil] The gemspec to use, or nil to find it automatically.
75
90
  def initialize(root = Dir.pwd, gemspec: nil)
76
91
  @root = root
77
92
  @gemspec = gemspec || find_gemspec
78
93
  end
79
94
 
95
+ # @attribute [String] The root directory of the gem project.
80
96
  attr :root
97
+
98
+ # @attribute [Gem::Specification] The gemspec for the gem.
81
99
  attr :gemspec
82
100
 
101
+ # Find the path to the version.rb file in the gem.
102
+ # @returns [String | Nil] The path to the version file, or nil if not found.
83
103
  def version_path
84
- @gemspec&.files.grep(/lib(.*?)\/version.rb/).first
104
+ if @gemspec
105
+ @gemspec.files.grep(/lib(.*?)\/version.rb/).first
106
+ end
85
107
  end
86
108
 
109
+ # Update the version number in the version file according to the bump specification.
110
+ # @parameter bump [Array(Integer)] Array specifying how to increment each version part.
111
+ # @parameter version_path [String] The path to the version file.
112
+ # @returns [String | Boolean] The path to the version file if updated, or false if no version file found.
87
113
  def update_version(bump, version_path = self.version_path)
88
114
  return false unless version_path
89
115
 
116
+ # Guard against consecutive version bumps
117
+ guard_last_commit_not_version_bump
118
+
90
119
  lines = File.readlines(version_path)
91
120
  new_version = nil
92
121
 
@@ -107,6 +136,9 @@ module Bake
107
136
  end
108
137
  end
109
138
 
139
+ # Verify that the repository has no uncommitted changes.
140
+ # @returns [Boolean] True if the repository is clean.
141
+ # @raises [RuntimeError] If there are uncommitted changes in the repository.
110
142
  def guard_clean
111
143
  lines = readlines("git", "status", "--porcelain", chdir: @root)
112
144
 
@@ -117,14 +149,37 @@ module Bake
117
149
  return true
118
150
  end
119
151
 
152
+ # Verify that the last commit was not a version bump.
153
+ # @returns [Boolean] True if the last commit was not a version bump.
154
+ # @raises [RuntimeError] If the last commit was a version bump.
155
+ def guard_last_commit_not_version_bump
156
+ # Get the last commit message:
157
+ begin
158
+ last_commit_message = readlines("git", "log", "-1", "--pretty=format:%s", chdir: @root).first&.strip
159
+ rescue CommandExecutionError => error
160
+ # If git log fails (e.g., no commits yet), skip the check:
161
+ if error.exit_code == 128
162
+ return true
163
+ else
164
+ raise
165
+ end
166
+ end
167
+
168
+ if last_commit_message && last_commit_message.match?(/^Bump (patch|minor|major|version)( version)?\.?$/i)
169
+ raise "Last commit appears to be a version bump: #{last_commit_message.inspect}. Cannot bump version consecutively."
170
+ end
171
+
172
+ return true
173
+ end
174
+
120
175
  # @parameter root [String] The root path for package files.
121
176
  # @parameter signing_key [String | Nil] The signing key to use for signing the package.
122
177
  # @returns [String] The path to the built gem package.
123
178
  def build_gem(root: "pkg", signing_key: nil)
124
179
  # Ensure the output directory exists:
125
- FileUtils.mkdir_p("pkg")
180
+ FileUtils.mkdir_p(root)
126
181
 
127
- output_path = File.join("pkg", @gemspec.file_name)
182
+ output_path = File.join(root, @gemspec.file_name)
128
183
 
129
184
  if signing_key == false
130
185
  @gemspec.signing_key = nil
@@ -137,14 +192,119 @@ module Bake
137
192
  ::Gem::Package.build(@gemspec, false, false, output_path)
138
193
  end
139
194
 
195
+ # Install the gem using the `gem install` command.
196
+ # @parameter arguments [Array] Additional arguments to pass to `gem install`.
197
+ # @parameter path [String] The path to the gem file to install.
140
198
  def install_gem(*arguments, path: @gemspec.file_name)
141
199
  system("gem", "install", path, *arguments)
142
200
  end
143
201
 
202
+ # Push the gem to a gem repository using the `gem push` command.
203
+ # @parameter arguments [Array] Additional arguments to pass to `gem push`.
204
+ # @parameter path [String] The path to the gem file to push.
144
205
  def push_gem(*arguments, path: @gemspec.file_name)
145
206
  system("gem", "push", path, *arguments)
146
207
  end
147
208
 
209
+ # Build the gem in a clean worktree for better isolation
210
+ # @parameter root [String] The root path for package files.
211
+ # @parameter signing_key [String | Nil] The signing key to use for signing the package.
212
+ # @returns [String] The path to the built gem package.
213
+ def build_gem_in_worktree(root: "pkg", signing_key: nil)
214
+ original_pkg_path = File.join(@root, root)
215
+
216
+ # Create a unique temporary path for the worktree
217
+ timestamp = Time.now.strftime("%Y%m%d-%H%M%S-%N")
218
+ worktree_path = File.join(Dir.tmpdir, "bake-gem-build-#{timestamp}")
219
+
220
+ begin
221
+ # Create worktree from current HEAD
222
+ unless system("git", "worktree", "add", worktree_path, "HEAD", chdir: @root)
223
+ raise "Failed to create git worktree. Make sure you have at least one commit in the repository."
224
+ end
225
+
226
+ # Create helper for the worktree
227
+ worktree_helper = self.class.new(worktree_path)
228
+
229
+ # Build gem directly into the target pkg directory
230
+ output_path = worktree_helper.build_gem(root: original_pkg_path, signing_key: signing_key)
231
+
232
+ output_path
233
+ ensure
234
+ # Clean up the worktree
235
+ system("git", "worktree", "remove", worktree_path, "--force", chdir: @root)
236
+ end
237
+ end
238
+
239
+ # Create a release branch, add the version file, and commit the changes.
240
+ # @parameter version_path [String] The path to the version file that was updated.
241
+ # @parameter message [String] The commit message to use.
242
+ # @returns [String] The name of the created branch.
243
+ def create_release_branch(version_path, message: "Bump version.")
244
+ branch_name = "release-v#{@gemspec.version}"
245
+
246
+ system("git", "checkout", "-b", branch_name, chdir: @root)
247
+ system("git", "add", version_path, chdir: @root)
248
+ system("git", "commit", "-m", message, chdir: @root)
249
+
250
+ return branch_name
251
+ end
252
+
253
+ # Commit version changes to the current branch.
254
+ # @parameter message [String] The commit message to use.
255
+ def commit_version_changes(message: "Bump version.")
256
+ system("git", "add", "--all", chdir: @root)
257
+ system("git", "commit", "-m", message, chdir: @root)
258
+ end
259
+
260
+ # Fetch remote tags and create a release tag for the specified version.
261
+ # @parameter tag [Boolean] Whether to tag the release.
262
+ # @parameter version [String] The version to tag.
263
+ # @returns [String | Nil] The tag name if created, nil otherwise.
264
+ def create_release_tag(tag: true, version:)
265
+ tag_name = nil
266
+
267
+ if tag
268
+ tag_name = "v#{version}"
269
+ system("git", "fetch", "--all", "--tags", chdir: @root)
270
+ system("git", "tag", tag_name, chdir: @root)
271
+ end
272
+
273
+ return tag_name
274
+ end
275
+
276
+ # Delete a git tag.
277
+ # @parameter tag_name [String] The name of the tag to delete.
278
+ def delete_git_tag(tag_name)
279
+ system("git", "tag", "--delete", tag_name, chdir: @root)
280
+ end
281
+
282
+ # Push changes and tags to the remote repository.
283
+ # @parameter current_branch [String | Nil] The current branch name, or nil if not on a branch.
284
+ def push_release(current_branch: nil)
285
+ # If we are on a branch, push, otherwise just push the tags (assuming shallow checkout):
286
+ if current_branch
287
+ system("git", "push", chdir: @root)
288
+ end
289
+
290
+ system("git", "push", "--tags", chdir: @root)
291
+ end
292
+
293
+ # Figure out if there is a current branch, if not, return `nil`.
294
+ # @returns [String | Nil] The current branch name, or nil if not on a branch.
295
+ def current_branch
296
+ # We originally used this but it is not supported by older versions of git.
297
+ # readlines("git", "branch", "--show-current").first&.chomp
298
+
299
+ readlines("git", "symbolic-ref", "--short", "--quiet", "HEAD", chdir: @root).first&.chomp
300
+ rescue CommandExecutionError
301
+ nil
302
+ end
303
+
304
+ # Find a gemspec file in the root directory.
305
+ # @parameter glob [String] The glob pattern to use for finding gemspec files.
306
+ # @returns [Gem::Specification | Nil] The loaded gemspec, or nil if none found.
307
+ # @raises [RuntimeError] If multiple gemspec files are found.
148
308
  def find_gemspec(glob = "*.gemspec")
149
309
  paths = Dir.glob(glob, base: @root).sort
150
310
 
@@ -153,7 +313,7 @@ module Bake
153
313
  end
154
314
 
155
315
  if path = paths.first
156
- return ::Gem::Specification.load(path)
316
+ return ::Gem::Specification.load(File.expand_path(path, @root))
157
317
  end
158
318
  end
159
319
  end
@@ -8,7 +8,29 @@ require "console/event/spawn"
8
8
 
9
9
  module Bake
10
10
  module Gem
11
+ # Exception raised when a command execution fails.
12
+ class CommandExecutionError < RuntimeError
13
+ def initialize(message, status)
14
+ super(message)
15
+ @status = status
16
+ end
17
+
18
+ # @attribute [Process::Status] The status object of the failed command.
19
+ attr_reader :status
20
+
21
+ # Helper method for convenience.
22
+ def exit_code
23
+ @status.exitstatus
24
+ end
25
+ end
26
+
27
+ # Provides shell command execution methods with proper logging and error handling.
11
28
  module Shell
29
+ # Execute a system command with logging and error handling.
30
+ # @parameter arguments [Array] The command and its arguments to execute.
31
+ # @parameter options [Hash] Additional options to pass to Process.spawn.
32
+ # @returns [Boolean] True if the command executed successfully.
33
+ # @raises [CommandExecutionError] If the command fails.
12
34
  def system(*arguments, **options)
13
35
  Console::Event::Spawn.for(*arguments, **options).emit(self)
14
36
 
@@ -19,13 +41,19 @@ module Bake
19
41
  pid, status = Process.wait2(pid) if pid
20
42
 
21
43
  unless status.success?
22
- raise "Failed to execute #{arguments}: #{status}!"
44
+ raise Bake::Gem::CommandExecutionError.new("Failed to execute #{arguments}: #{status}!", status)
23
45
  end
24
46
 
25
47
  return true
26
48
  end
27
49
  end
28
50
 
51
+ # Execute a command and yield its output to a block.
52
+ # @parameter arguments [Array] The command and its arguments to execute.
53
+ # @parameter options [Hash] Additional options to pass to Process.spawn.
54
+ # @yields {|input| ...} The input stream from the executed command.
55
+ # @returns [Object] The return value of the block.
56
+ # @raises [CommandExecutionError] If the command fails.
29
57
  def execute(*arguments, **options)
30
58
  Console::Event::Spawn.for(*arguments, **options).emit(self)
31
59
 
@@ -39,12 +67,17 @@ module Bake
39
67
  pid, status = Process.wait2(pid)
40
68
 
41
69
  unless status.success?
42
- raise "Failed to execute #{arguments}: #{status}!"
70
+ raise Bake::Gem::CommandExecutionError.new("Failed to execute #{arguments}: #{status}!", status)
43
71
  end
44
72
  end
45
73
  end
46
74
  end
47
75
 
76
+ # Execute a command and return its output as an array of lines.
77
+ # @parameter arguments [Array] The command and its arguments to execute.
78
+ # @parameter options [Hash] Additional options to pass to Process.spawn.
79
+ # @returns [Array(String)] The output lines from the executed command.
80
+ # @raises [CommandExecutionError] If the command fails.
48
81
  def readlines(*arguments, **options)
49
82
  execute(*arguments, **options) do |output|
50
83
  return output.readlines
@@ -5,6 +5,6 @@
5
5
 
6
6
  module Bake
7
7
  module Gem
8
- VERSION = "0.11.0"
8
+ VERSION = "0.12.0"
9
9
  end
10
10
  end
data/lib/bake/gem.rb CHANGED
@@ -4,3 +4,10 @@
4
4
  # Copyright, 2021-2024, by Samuel Williams.
5
5
 
6
6
  require_relative "gem/version"
7
+
8
+ # @namespace
9
+ module Bake
10
+ # @namespace
11
+ module Gem
12
+ end
13
+ end
data/readme.md CHANGED
@@ -8,16 +8,34 @@ Provides bake tasks for common gem release workflows.
8
8
 
9
9
  Please see the [project documentation](https://ioquatix.github.io/bake-gem/) for more details.
10
10
 
11
- - [Getting Started](https://ioquatix.github.io/bake-gem/guides/getting-started/index) - This guide explains how to use `bake-gem` to release gems.
11
+ - [Getting Started](https://ioquatix.github.io/bake-gem/guides/getting-started/index) - This guide explains how to use `bake-gem` to release gems safely and efficiently.
12
12
 
13
13
  ## Releases
14
14
 
15
15
  Please see the [project releases](https://ioquatix.github.io/bake-gem/releases/index) for all releases.
16
16
 
17
+ ### v0.12.0
18
+
19
+ - Add `guard_last_commit_not_version_bump` method to prevent consecutive version bumps.
20
+ - Add `build_gem_in_worktree` method for building gems in isolated git worktrees.
21
+ - Improve shell command execution with better error handling and specific exit code access.
22
+
23
+ ### v0.11.1
24
+
25
+ - Better integration with `bake`'s default output.
26
+
17
27
  ### v0.11.0
18
28
 
19
29
  - Improved bake task return values.
20
30
 
31
+ ### v0.10.0
32
+
33
+ - Better handling of versions.
34
+
35
+ ### v0.9.0
36
+
37
+ - Add `after_gem_release_version_increment` hook.
38
+
21
39
  ## See Also
22
40
 
23
41
  - [Bake](https://github.com/ioquatix/bake) — The bake task execution tool.
data/releases.md CHANGED
@@ -1,5 +1,23 @@
1
1
  # Releases
2
2
 
3
+ ## v0.12.0
4
+
5
+ - Add `guard_last_commit_not_version_bump` method to prevent consecutive version bumps.
6
+ - Add `build_gem_in_worktree` method for building gems in isolated git worktrees.
7
+ - Improve shell command execution with better error handling and specific exit code access.
8
+
9
+ ## v0.11.1
10
+
11
+ - Better integration with `bake`'s default output.
12
+
3
13
  ## v0.11.0
4
14
 
5
15
  - Improved bake task return values.
16
+
17
+ ## v0.10.0
18
+
19
+ - Better handling of versions.
20
+
21
+ ## v0.9.0
22
+
23
+ - Add `after_gem_release_version_increment` hook.
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: bake-gem
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.11.0
4
+ version: 0.12.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Samuel Williams
@@ -56,9 +56,13 @@ executables: []
56
56
  extensions: []
57
57
  extra_rdoc_files: []
58
58
  files:
59
+ - agent.md
59
60
  - bake/gem.rb
61
+ - bake/gem/release.rb
60
62
  - bake/gem/release/branch.rb
61
63
  - bake/gem/release/version.rb
64
+ - context/getting-started.md
65
+ - context/index.yaml
62
66
  - lib/bake/gem.rb
63
67
  - lib/bake/gem/helper.rb
64
68
  - lib/bake/gem/shell.rb
@@ -87,7 +91,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
87
91
  - !ruby/object:Gem::Version
88
92
  version: '0'
89
93
  requirements: []
90
- rubygems_version: 3.6.7
94
+ rubygems_version: 3.6.9
91
95
  specification_version: 4
92
96
  summary: Release management for Ruby gems.
93
97
  test_files: []
metadata.gz.sig CHANGED
Binary file