bruh 0.1.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 +7 -0
- data/CHANGELOG.md +26 -0
- data/LICENSE.txt +21 -0
- data/README.md +98 -0
- data/exe/haskbrew +7 -0
- data/lib/bruh/bottle.rb +209 -0
- data/lib/bruh/cabal.rb +77 -0
- data/lib/bruh/changelog.rb +62 -0
- data/lib/bruh/cli.rb +191 -0
- data/lib/bruh/config.rb +134 -0
- data/lib/bruh/hackage.rb +159 -0
- data/lib/bruh/homebrew.rb +223 -0
- data/lib/bruh/version.rb +5 -0
- data/lib/bruh.rb +68 -0
- metadata +242 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 61ba8a98c7f510ce257e743caf74457ad517b7bdaa6ac3b82e7fc2422efdcc0a
|
4
|
+
data.tar.gz: c5cab658a2748857fb15db2cefb89d68410085e0ecba91e8c116022bbab01805
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 9f4b12f569e004394f5c295ce42228e296891c7dce96b5b02c7366888321bac81b3c472235eb6ff05512019b099280fcdba2e51ab067be39cf6b724170f397a8
|
7
|
+
data.tar.gz: ee4004de98bb5799343aa5ea51ac17b405a4a7609d34fd1d3fed28474303f02de7f60de211503420505073f07f48f8593ae29438172afe07a7caeddeda8b858c
|
data/CHANGELOG.md
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
# Changelog
|
2
|
+
|
3
|
+
## [Unreleased]
|
4
|
+
|
5
|
+
## [0.1.0] - 2025-04-05
|
6
|
+
|
7
|
+
Initial release of Bruh.
|
8
|
+
|
9
|
+
### Added
|
10
|
+
- Cabal file parsing and version management
|
11
|
+
- Changelog handling for release notes
|
12
|
+
- Hackage publishing support with documentation
|
13
|
+
- Homebrew formula generation and updates
|
14
|
+
- Bottle building and publishing to GitHub
|
15
|
+
- Configuration management with TOML
|
16
|
+
- Interactive and non-interactive modes
|
17
|
+
- CLI interface with Thor
|
18
|
+
- Full test coverage
|
19
|
+
|
20
|
+
### Features
|
21
|
+
- HackagePublisher for authentication and package uploads
|
22
|
+
- HomebrewFormula for template-based formula management
|
23
|
+
- BottleBuilder for building and uploading bottles
|
24
|
+
- CLI interface with Thor
|
25
|
+
- ConfigManager for storing settings in TOML
|
26
|
+
- ChangelogHandler for release notes
|
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2025 Fuzz Leonard
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,98 @@
|
|
1
|
+
# Bruh
|
2
|
+
|
3
|
+
Bruh is a Ruby gem that automates the release process for Haskell packages to
|
4
|
+
both Hackage and Homebrew. It streamlines the workflow of releasing Haskell
|
5
|
+
packages by handling version updates, changelog management, Hackage publishing,
|
6
|
+
and Homebrew formula updates.
|
7
|
+
|
8
|
+
## Features
|
9
|
+
|
10
|
+
- **Cabal Integration**: Parse and update Cabal files with version bumps
|
11
|
+
- **Changelog Management**: Auto-update CHANGELOG.md with new version entries
|
12
|
+
- **Hackage Publishing**: Publish packages to Hackage with documentation
|
13
|
+
- **Homebrew Formula Generation**: Update Homebrew formulas with new versions and SHAs
|
14
|
+
- **Bottle Building**: Build and publish Homebrew bottles to GitHub
|
15
|
+
- **Non-interactive Mode**: Support for CI/CD pipelines and automated testing
|
16
|
+
|
17
|
+
## Installation
|
18
|
+
|
19
|
+
Install the gem by executing:
|
20
|
+
|
21
|
+
```bash
|
22
|
+
gem install bruh
|
23
|
+
```
|
24
|
+
|
25
|
+
## Usage
|
26
|
+
|
27
|
+
### Command Line Interface
|
28
|
+
|
29
|
+
The primary interface is through the `bruh` command:
|
30
|
+
|
31
|
+
```bash
|
32
|
+
# Interactive release process
|
33
|
+
bruh release
|
34
|
+
|
35
|
+
# Non-interactive release with specific version
|
36
|
+
bruh release --non-interactive --version 0.1.2
|
37
|
+
|
38
|
+
# Skip specific steps
|
39
|
+
bruh release --skip-hackage --skip-bottles
|
40
|
+
|
41
|
+
# Show version
|
42
|
+
bruh version
|
43
|
+
```
|
44
|
+
|
45
|
+
### Non-interactive Release Script
|
46
|
+
|
47
|
+
For CI/CD pipelines, you can use the included release script:
|
48
|
+
|
49
|
+
```bash
|
50
|
+
# Basic usage
|
51
|
+
bin/release --version 0.1.2
|
52
|
+
|
53
|
+
# Skip specific steps
|
54
|
+
bin/release --version 0.1.2 --skip-hackage --skip-bottles
|
55
|
+
|
56
|
+
# Run in interactive mode
|
57
|
+
bin/release --interactive
|
58
|
+
```
|
59
|
+
|
60
|
+
### Configuration
|
61
|
+
|
62
|
+
Bruh stores configuration in `~/.config/bruh/config.toml`. For non-interactive usage, you should set up your Hackage credentials:
|
63
|
+
|
64
|
+
```bash
|
65
|
+
# Setup credentials
|
66
|
+
bruh config setup
|
67
|
+
|
68
|
+
# Manually set credentials
|
69
|
+
bruh config set hackage_username "your-username"
|
70
|
+
bruh config set hackage_password "your-password"
|
71
|
+
bruh config set github_token "your-token"
|
72
|
+
```
|
73
|
+
|
74
|
+
## Development
|
75
|
+
|
76
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt.
|
77
|
+
|
78
|
+
### Running Tests
|
79
|
+
|
80
|
+
```bash
|
81
|
+
# Run all tests
|
82
|
+
bundle exec rake test
|
83
|
+
|
84
|
+
# Run a specific test
|
85
|
+
bundle exec ruby -I lib:test test/test_cabal.rb -n test_name
|
86
|
+
```
|
87
|
+
|
88
|
+
## Contributing
|
89
|
+
|
90
|
+
1. Fork the repository
|
91
|
+
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
|
92
|
+
3. Commit your changes (`git commit -am 'Add amazing feature'`)
|
93
|
+
4. Push to the branch (`git push origin feature/amazing-feature`)
|
94
|
+
5. Create a new Pull Request
|
95
|
+
|
96
|
+
## License
|
97
|
+
|
98
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/exe/haskbrew
ADDED
data/lib/bruh/bottle.rb
ADDED
@@ -0,0 +1,209 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'fileutils'
|
5
|
+
require 'json'
|
6
|
+
require 'sorbet-runtime'
|
7
|
+
|
8
|
+
module Bruh
|
9
|
+
# Builds and manages Homebrew bottles
|
10
|
+
class Bottle
|
11
|
+
extend T::Sig
|
12
|
+
|
13
|
+
sig { params(interactive: T::Boolean).void }
|
14
|
+
def initialize(interactive: true)
|
15
|
+
@interactive = interactive
|
16
|
+
@tap_dir = T.let(find_homebrew_tap, T.nilable(String))
|
17
|
+
end
|
18
|
+
|
19
|
+
sig { params(version: String).returns(T.nilable(T::Hash[Symbol, T.untyped])) }
|
20
|
+
def build(version)
|
21
|
+
return nil unless @tap_dir
|
22
|
+
|
23
|
+
return nil if @interactive && !yes_no_prompt('Do you want to build a Homebrew bottle for this release?')
|
24
|
+
|
25
|
+
puts "Building bottle for #{package_name} formula..."
|
26
|
+
|
27
|
+
Dir.chdir(@tap_dir) do
|
28
|
+
# Clean environment - uninstall any existing version
|
29
|
+
system("brew uninstall --force #{package_name} 2>/dev/null || true")
|
30
|
+
|
31
|
+
# Ensure tap is properly set up
|
32
|
+
tap_name = "#{repo_owner}/tap"
|
33
|
+
system("brew untap #{tap_name} 2>/dev/null || true")
|
34
|
+
system("brew tap #{tap_name} \"$(pwd)\"")
|
35
|
+
|
36
|
+
# Install with build-bottle flag
|
37
|
+
unless system("brew install --build-bottle #{tap_name}/#{package_name}")
|
38
|
+
puts 'Failed to install formula with --build-bottle flag'
|
39
|
+
return nil
|
40
|
+
end
|
41
|
+
|
42
|
+
# Create bottle with JSON output
|
43
|
+
bottle_cmd = 'brew bottle --json '
|
44
|
+
bottle_cmd += "--root-url=\"https://github.com/#{repo_owner}/#{repo_name}/releases/download/v#{version}\" "
|
45
|
+
bottle_cmd += "#{tap_name}/#{package_name}"
|
46
|
+
unless system(bottle_cmd)
|
47
|
+
puts 'Failed to create bottle'
|
48
|
+
return nil
|
49
|
+
end
|
50
|
+
|
51
|
+
# Find the JSON file created by brew bottle
|
52
|
+
bottle_json = Dir['*.json'].max_by { |f| File.mtime(f) }
|
53
|
+
|
54
|
+
unless bottle_json && File.exist?(bottle_json)
|
55
|
+
puts 'Could not find bottle JSON file'
|
56
|
+
return nil
|
57
|
+
end
|
58
|
+
|
59
|
+
# Parse the JSON file
|
60
|
+
bottle_data = JSON.parse(File.read(bottle_json))
|
61
|
+
|
62
|
+
# Extract filenames and other data
|
63
|
+
first_entry = bottle_data.values.first
|
64
|
+
first_tag = first_entry['bottle']['tags'].values.first
|
65
|
+
|
66
|
+
bottle_info = {
|
67
|
+
version: version,
|
68
|
+
expected_filename: first_tag['filename'],
|
69
|
+
local_filename: first_tag['local_filename'],
|
70
|
+
sha256: first_tag['sha256'],
|
71
|
+
macos_version: first_entry['bottle']['tags'].keys.first.to_s.split('_')[1]
|
72
|
+
}
|
73
|
+
|
74
|
+
# Extract rebuild number from filename if present
|
75
|
+
bottle_info[:rebuild] = ::Regexp.last_match(1).to_i if bottle_info[:local_filename] =~ /bottle\.(\d+)/
|
76
|
+
|
77
|
+
# Verify the bottle file exists
|
78
|
+
unless File.exist?(bottle_info[:local_filename])
|
79
|
+
puts "Bottle file not found: #{bottle_info[:local_filename]}"
|
80
|
+
return nil
|
81
|
+
end
|
82
|
+
|
83
|
+
# Create bottles directory and move the file
|
84
|
+
FileUtils.mkdir_p('bottles')
|
85
|
+
FileUtils.mv(bottle_info[:local_filename], 'bottles/')
|
86
|
+
|
87
|
+
puts "Bottle created successfully: bottles/#{bottle_info[:local_filename]}"
|
88
|
+
bottle_info
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
sig { params(version: String, bottle_info: T::Hash[Symbol, T.untyped]).returns(T::Boolean) }
|
93
|
+
def upload_to_github(version, bottle_info)
|
94
|
+
# Check for GitHub CLI
|
95
|
+
unless system('which gh > /dev/null 2>&1')
|
96
|
+
puts 'GitHub CLI (gh) not found. Please install it to automate bottle uploads.'
|
97
|
+
return false
|
98
|
+
end
|
99
|
+
|
100
|
+
return false if @interactive && !yes_no_prompt('Do you want to upload bottle to GitHub?')
|
101
|
+
|
102
|
+
# Remember current directory
|
103
|
+
original_dir = Dir.pwd
|
104
|
+
|
105
|
+
# Change to the tap directory
|
106
|
+
Dir.chdir(T.must(@tap_dir)) do
|
107
|
+
# Check if release exists
|
108
|
+
release_exists = system("gh release view \"v#{version}\" &>/dev/null")
|
109
|
+
|
110
|
+
unless release_exists
|
111
|
+
puts "Release v#{version} doesn't exist. Creating it now..."
|
112
|
+
release_cmd = "gh release create \"v#{version}\" --title \"Release v#{version}\" "
|
113
|
+
release_cmd += "--notes \"Release v#{version} with Homebrew bottle support.\""
|
114
|
+
unless system(release_cmd)
|
115
|
+
puts 'Failed to create GitHub release'
|
116
|
+
return false
|
117
|
+
end
|
118
|
+
end
|
119
|
+
|
120
|
+
# Rename the bottle file to expected filename
|
121
|
+
bottle_src = File.join('bottles', bottle_info[:local_filename])
|
122
|
+
bottle_dst = File.join('bottles', bottle_info[:expected_filename])
|
123
|
+
|
124
|
+
FileUtils.mv(bottle_src, bottle_dst) unless bottle_src == bottle_dst
|
125
|
+
|
126
|
+
# Upload to GitHub release
|
127
|
+
puts "Uploading bottle to GitHub release v#{version}..."
|
128
|
+
unless system("gh release upload \"v#{version}\" \"#{bottle_dst}\" --clobber")
|
129
|
+
puts 'Failed to upload bottle to GitHub'
|
130
|
+
Dir.chdir(original_dir)
|
131
|
+
return false
|
132
|
+
end
|
133
|
+
|
134
|
+
puts 'Bottle uploaded successfully to GitHub release!'
|
135
|
+
end
|
136
|
+
|
137
|
+
# Change back to original directory
|
138
|
+
Dir.chdir(original_dir)
|
139
|
+
true
|
140
|
+
end
|
141
|
+
|
142
|
+
private
|
143
|
+
|
144
|
+
sig { returns(T.nilable(String)) }
|
145
|
+
def find_homebrew_tap
|
146
|
+
# Try common locations
|
147
|
+
potential_paths = [
|
148
|
+
'../homebrew-tap',
|
149
|
+
'~/homebrew-tap',
|
150
|
+
'~/Projects/homebrew-tap'
|
151
|
+
]
|
152
|
+
|
153
|
+
potential_paths.each do |path|
|
154
|
+
expanded = File.expand_path(path)
|
155
|
+
return expanded if Dir.exist?(expanded)
|
156
|
+
end
|
157
|
+
|
158
|
+
if @interactive
|
159
|
+
puts 'Homebrew tap directory not found in common locations.'
|
160
|
+
puts 'Please enter the path to your Homebrew tap directory:'
|
161
|
+
tap_dir = gets.chomp.strip
|
162
|
+
return tap_dir if !tap_dir.empty? && Dir.exist?(tap_dir)
|
163
|
+
end
|
164
|
+
|
165
|
+
nil
|
166
|
+
end
|
167
|
+
|
168
|
+
sig { returns(String) }
|
169
|
+
def package_name
|
170
|
+
# Extract package name from current directory
|
171
|
+
File.basename(Dir.pwd)
|
172
|
+
end
|
173
|
+
|
174
|
+
sig { returns(T::Array[String]) }
|
175
|
+
def repo_info
|
176
|
+
# Extract owner and repo from git remote
|
177
|
+
remote = `git remote get-url origin`.chomp
|
178
|
+
if remote =~ %r{github\.com[:/]([^/]+)/([^/]+)\.git}
|
179
|
+
owner = T.let(T.must(::Regexp.last_match(1)), String)
|
180
|
+
repo = T.let(T.must(::Regexp.last_match(2)), String)
|
181
|
+
return [owner, repo]
|
182
|
+
end
|
183
|
+
|
184
|
+
%w[user repo]
|
185
|
+
end
|
186
|
+
|
187
|
+
sig { returns(String) }
|
188
|
+
def repo_owner
|
189
|
+
T.must(repo_info[0])
|
190
|
+
end
|
191
|
+
|
192
|
+
sig { returns(String) }
|
193
|
+
def repo_name
|
194
|
+
T.must(repo_info[1])
|
195
|
+
end
|
196
|
+
|
197
|
+
sig { params(message: String, default_no: T::Boolean).returns(T::Boolean) }
|
198
|
+
def yes_no_prompt(message, default_no: true)
|
199
|
+
return true unless @interactive
|
200
|
+
|
201
|
+
default = default_no ? '[y/N]' : '[Y/n]'
|
202
|
+
print "#{message} #{default} "
|
203
|
+
response = gets.chomp.downcase
|
204
|
+
return response.start_with?('y') if default_no
|
205
|
+
|
206
|
+
!response.start_with?('n')
|
207
|
+
end
|
208
|
+
end
|
209
|
+
end
|
data/lib/bruh/cabal.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'sorbet-runtime'
|
5
|
+
|
6
|
+
module Bruh
|
7
|
+
# Minimal Cabal file handler for version management
|
8
|
+
class Cabal
|
9
|
+
extend T::Sig
|
10
|
+
|
11
|
+
sig { returns(String) }
|
12
|
+
attr_reader :version
|
13
|
+
|
14
|
+
sig { returns(String) }
|
15
|
+
attr_reader :name
|
16
|
+
|
17
|
+
sig { returns(String) }
|
18
|
+
attr_reader :file_path
|
19
|
+
|
20
|
+
sig { params(file_path: String).void }
|
21
|
+
def initialize(file_path)
|
22
|
+
@file_path = file_path
|
23
|
+
@content = T.let(File.read(file_path), String)
|
24
|
+
|
25
|
+
# Extract name and version with specific regexes
|
26
|
+
@name = T.let(extract_field('name'), String)
|
27
|
+
@version = T.let(extract_field('version'), String)
|
28
|
+
end
|
29
|
+
|
30
|
+
sig { params(new_version: String).returns(String) }
|
31
|
+
def update_version(new_version)
|
32
|
+
# Update the version field in the file content
|
33
|
+
updated_content = @content.sub(/^version:\s*[0-9.]+/, "version: #{new_version}")
|
34
|
+
|
35
|
+
# Only write if something actually changed
|
36
|
+
if updated_content != @content
|
37
|
+
File.write(@file_path, updated_content)
|
38
|
+
# These variables are already declared in initialize, so we don't use T.let again
|
39
|
+
@content = updated_content
|
40
|
+
@version = new_version
|
41
|
+
end
|
42
|
+
|
43
|
+
new_version
|
44
|
+
end
|
45
|
+
|
46
|
+
# Get essential dependencies for version calculations
|
47
|
+
sig { returns(T::Array[String]) }
|
48
|
+
def dependencies
|
49
|
+
# Extract all build-depends lines
|
50
|
+
deps = []
|
51
|
+
|
52
|
+
@content.scan(/build-depends:.*?(?=\n\S|\Z)/m).each do |match|
|
53
|
+
# Extract package names without version constraints
|
54
|
+
match_str = T.cast(match, String)
|
55
|
+
match_str.scan(/\b([a-zA-Z][a-zA-Z0-9-]*)[,\s<>=]/).each do |dep_arr|
|
56
|
+
# dep_arr is an array where the first element is the capture group
|
57
|
+
deps << dep_arr[0]
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
deps.uniq
|
62
|
+
end
|
63
|
+
|
64
|
+
private
|
65
|
+
|
66
|
+
sig { params(field: String).returns(String) }
|
67
|
+
def extract_field(field)
|
68
|
+
# Use a specific regex targeting the field at the beginning of a line
|
69
|
+
# followed by colon and whitespace
|
70
|
+
if @content =~ /^#{field}:\s*([^\n]+)/
|
71
|
+
T.must(::Regexp.last_match(1)).strip
|
72
|
+
else
|
73
|
+
''
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
@@ -0,0 +1,62 @@
|
|
1
|
+
# typed: strict
|
2
|
+
# frozen_string_literal: true
|
3
|
+
|
4
|
+
require 'date'
|
5
|
+
require 'sorbet-runtime'
|
6
|
+
|
7
|
+
module Bruh
|
8
|
+
# Manages changelog updates and version entries
|
9
|
+
class Changelog
|
10
|
+
extend T::Sig
|
11
|
+
|
12
|
+
sig { params(version: String, project_root: String, interactive: T::Boolean).returns(T::Boolean) }
|
13
|
+
def self.update(version, project_root, interactive: true)
|
14
|
+
changelog_path = File.join(project_root, 'CHANGELOG.md')
|
15
|
+
today = Date.today.strftime('%Y-%m-%d')
|
16
|
+
|
17
|
+
# Create file if it doesn't exist
|
18
|
+
File.write(changelog_path, "# Changelog\n\n") unless File.exist?(changelog_path)
|
19
|
+
|
20
|
+
content = File.read(changelog_path)
|
21
|
+
|
22
|
+
# Check if this version already exists in changelog
|
23
|
+
return true if content.include?("## [#{version}]")
|
24
|
+
|
25
|
+
# Insert new version entry after the header
|
26
|
+
new_content = content.sub(/^# Changelog/,
|
27
|
+
"# Changelog\n\n## [#{version}] - #{today}\n\n- Add your changes here\n\n")
|
28
|
+
|
29
|
+
File.write(changelog_path, new_content)
|
30
|
+
|
31
|
+
# Open editor for user to edit changelog if in interactive mode
|
32
|
+
if interactive
|
33
|
+
editor = ENV['EDITOR'] || 'nano'
|
34
|
+
puts 'Opening CHANGELOG.md for editing. Please add your release notes.'
|
35
|
+
system("#{editor} #{changelog_path}")
|
36
|
+
puts 'Press Enter to continue with the release process...'
|
37
|
+
gets
|
38
|
+
end
|
39
|
+
|
40
|
+
true
|
41
|
+
end
|
42
|
+
|
43
|
+
sig { params(project_root: String).returns(T::Hash[String, String]) }
|
44
|
+
def self.generate_release_notes(project_root)
|
45
|
+
changelog_path = File.join(project_root, 'CHANGELOG.md')
|
46
|
+
|
47
|
+
return { version: 'unknown', notes: 'No changelog found' } unless File.exist?(changelog_path)
|
48
|
+
|
49
|
+
content = File.read(changelog_path)
|
50
|
+
|
51
|
+
# Extract the latest version entry
|
52
|
+
if content =~ /## \[([^\]]+)\][^\n]*\n\n(.*?)(?=\n## |\Z)/m
|
53
|
+
version = ::Regexp.last_match(1)
|
54
|
+
notes = T.must(::Regexp.last_match(2)).strip
|
55
|
+
|
56
|
+
return { version: version, notes: notes }
|
57
|
+
end
|
58
|
+
|
59
|
+
{ version: 'unknown', notes: 'Could not extract release notes' }
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|