Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,20 @@ turnkey_client: turnkey_client_inputs/public_api.swagger.json turnkey_client_inp

clean:
rm -rf turnkey_client

.PHONY: changeset
changeset:
ruby tool/changeset.rb

.PHONY: version
version:
ruby tool/changeset_version.rb

.PHONY: changelog
changelog:
ruby tool/changeset_changelog.rb

.PHONY: prepare-release
prepare-release:
ruby tool/changeset_version.rb
ruby tool/changeset_changelog.rb
15 changes: 15 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,21 @@ And run:
$ rubocop
```

## Contributing

Before opening a PR containing your changes, please create a changeset detailing the package bump and a brief note on what has changed.
> [!NOTE]
> - The note is what will be added to the changelog
> - Quick version bump guide:
> - patch: Bug fixes and small changes (0.0.1 → 0.0.2)
> - minor: New features, backwards compatible (0.0.1 → 0.1.0)
> - major: Breaking changes (0.0.1 → 1.0.0)
**Run this make cmd to create a new changeset:**
```sh
$ make changeset
```

## Releasing on Rubygems.org

To build and release:
Expand Down
85 changes: 85 additions & 0 deletions tool/changeset.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
#!/usr/bin/env ruby
require 'fileutils'
require_relative 'changeset_lib'

include ChangesetLib

def prompt_bump
puts 'Select bump type:'
puts ' 1) patch'
puts ' 2) minor'
puts ' 3) major'

loop do
print 'Choice (1-3): '
case $stdin.gets&.strip
when '1' then return 'patch'
when '2' then return 'minor'
when '3' then return 'major'
else puts 'Invalid choice, please enter 1, 2, or 3.'
end
end
end

def prompt_line(label)
print label
$stdin.gets&.chomp || ''
end

def prompt_multiline
lines = []
loop do
line = $stdin.gets
break if line.nil?
break if line.strip == '.'
lines << line.chomp
end
lines.pop while !lines.empty? && lines.last.strip.empty?
lines.empty? ? '_No additional notes._' : lines.join("\n")
end

def build_changeset_content(title:, date:, bump:, note:)
<<~CONTENT
---
title: "#{escape_yaml_string(title)}"
date: "#{date}"
bump: "#{bump}"
---

#{note}
CONTENT
end

def main
puts '=== Create Changeset ==='
puts

bump = prompt_bump
puts

title = prompt_line('Short title for this change: ')
abort('Error: title cannot be empty.') if title.strip.empty?

puts
puts 'Enter a longer description (markdown allowed).'
puts 'End input with a single "." on its own line:'
note = prompt_multiline

now = Time.now
filename = "#{format_timestamp(now)}-#{slugify(title)}.md"
FileUtils.mkdir_p(CHANGESET_DIR)

filepath = File.join(CHANGESET_DIR, filename)
content = build_changeset_content(
title: title.strip,
date: date_only(now),
bump: bump,
note: note
)

File.write(filepath, content)
puts
puts "Changeset written to #{filepath}"
end

main
84 changes: 84 additions & 0 deletions tool/changeset_changelog.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
#!/usr/bin/env ruby
require 'fileutils'
require_relative 'changeset_lib'

include ChangesetLib

def build_release_section(version, date, changes)
by_bump = { 'patch' => [], 'minor' => [], 'major' => [] }

changes.each do |c|
key = %w[patch minor major].include?(c['bump']) ? c['bump'] : 'patch'
by_bump[key] << c
end

lines = []
lines << "## #{version} -- #{date}"
lines << ''

[
['patch', 'Patch Changes'],
['minor', 'Minor Changes'],
['major', 'Major Changes']
].each do |key, heading|
next if by_bump[key].empty?

lines << "### #{heading}"
by_bump[key].each do |change|
note = change['note']
title = change['title']
if note == '_No additional notes._'
lines << "- #{title}"
else
lines << "- #{note}"
end
end
lines << ''
end

lines.join("\n") + "\n"
end

def merge_changelog(existing, new_section)
if existing.strip.empty?
return "#{CHANGELOG_HEADER}\n\n#{new_section}"
end

trimmed = existing.lstrip
unless trimmed.start_with?(CHANGELOG_HEADER)
return "#{CHANGELOG_HEADER}\n\n#{new_section}#{existing}"
end

# Insert new section right after the "# Changelog" header line
lines = existing.split("\n")
header_line = lines.first
rest = lines[1..].join("\n").lstrip

result = "#{header_line}\n\n#{new_section}"
result += rest unless rest.empty?
result
end

def main
meta = read_release_meta
version = meta['toVersion']
date = meta['date']
changes = meta['changes']

if changes.nil? || changes.empty?
puts 'No changes in release metadata -- nothing to changelog.'
return
end

new_section = build_release_section(version, date, changes)

existing = File.exist?(CHANGELOG_FILE) ? File.read(CHANGELOG_FILE) : ''
merged = merge_changelog(existing, new_section)
File.write(CHANGELOG_FILE, merged)
puts "Updated #{CHANGELOG_FILE} for v#{version}"

delete_processed_changesets(meta)
puts 'Deleted processed changesets and release metadata.'
end

main
165 changes: 165 additions & 0 deletions tool/changeset_lib.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
require 'json'
require 'yaml'
require 'fileutils'

module ChangesetLib
CHANGESET_DIR = '.changesets'
RELEASE_META_FILE = '_current_release.json'
VERSION_FILE = 'turnkey_client/lib/turnkey_client/version.rb'
CONFIG_FILE = 'turnkey_client_inputs/config.json'
CHANGELOG_FILE = 'CHANGELOG.md'
CHANGELOG_HEADER = '# Changelog'

ChangesetEntry = Struct.new(:path, :title, :date, :bump, :note, keyword_init: true)

def slugify(str)
slug = str.downcase
.gsub(/[^a-z0-9\s_-]/, '')
.gsub(/[\s_-]+/, '-')
.gsub(/\A-+|-+\z/, '')
slug.empty? ? 'changeset' : slug
end

def format_timestamp(time)
time.strftime('%Y%m%d-%H%M%S')
end

def date_only(time)
time.strftime('%Y-%m-%d')
end

def today_date
date_only(Time.now)
end

def escape_yaml_string(str)
str.gsub('\\', '\\\\\\\\').gsub('"', '\\"')
end

def parse_version(version_str)
base = version_str.strip.split(/[-+]/, 2).first
parts = base.split('.')
unless parts.length == 3 && parts.all? { |p| p.match?(/\A\d+\z/) }
raise "Invalid version format: '#{version_str}', expected X.Y.Z"
end
parts.map(&:to_i)
end

def next_version(current, bump)
major, minor, patch = parse_version(current)
case bump
when 'major'
"#{major + 1}.0.0"
when 'minor'
"#{major}.#{minor + 1}.0"
when 'patch'
"#{major}.#{minor}.#{patch + 1}"
else
raise "Unknown bump type: '#{bump}'"
end
end

def bump_level(bump)
case bump
when 'patch' then 1
when 'minor' then 2
when 'major' then 3
else raise "Unknown bump type: '#{bump}'"
end
end

def max_bump(bumps)
bumps.max_by { |b| bump_level(b) }
end

def read_current_version
unless File.exist?(VERSION_FILE)
raise "Cannot read version: #{VERSION_FILE} does not exist"
end
content = File.read(VERSION_FILE)
match = content.match(/VERSION\s*=\s*['"]([^'"]+)['"]/)
unless match
raise "Cannot find VERSION constant in #{VERSION_FILE}"
end
match[1]
end

def write_version_rb(new_version)
content = File.read(VERSION_FILE)
updated = content.sub(/VERSION\s*=\s*['"][^'"]+['"]/, "VERSION = '#{new_version}'")
File.write(VERSION_FILE, updated)
end

def write_config_json(new_version)
content = File.read(CONFIG_FILE)
data = JSON.parse(content)
data['gemVersion'] = new_version
File.write(CONFIG_FILE, JSON.pretty_generate(data) + "\n")
end

def load_changesets
dir = CHANGESET_DIR
return [] unless Dir.exist?(dir)

Dir.glob(File.join(dir, '*.md'))
.reject { |f| File.basename(f).start_with?('_') }
.sort
.map { |f| parse_changeset_file(f) }
.compact
end

def parse_changeset_file(path)
raw = File.read(path).strip
unless raw.start_with?('---')
warn "warning: #{path} does not start with frontmatter delimiter, skipping"
return nil
end

parts = raw.split(/^---\s*$/m)
if parts.length < 3
warn "warning: #{path} has malformed frontmatter, skipping"
return nil
end

frontmatter = YAML.safe_load(parts[1])
body = parts[2..].join('---').strip
body = '_No additional notes._' if body.empty?

ChangesetEntry.new(
path: path,
title: frontmatter['title'],
date: frontmatter['date'] || today_date,
bump: frontmatter['bump'] || 'patch',
note: body
)
rescue => e
warn "warning: failed to parse changeset #{path}: #{e.message}"
nil
end

def read_release_meta
meta_path = File.join(CHANGESET_DIR, RELEASE_META_FILE)
unless File.exist?(meta_path)
raise "No release metadata found at #{meta_path}. Run `make version` first."
end
JSON.parse(File.read(meta_path))
end

def write_release_meta(meta)
FileUtils.mkdir_p(CHANGESET_DIR)
meta_path = File.join(CHANGESET_DIR, RELEASE_META_FILE)
File.write(meta_path, JSON.pretty_generate(meta) + "\n")
end

def delete_processed_changesets(meta)
(meta['changes'] || []).each do |change|
path = change['changesetPath']
if path && File.exist?(path)
File.delete(path)
end
end

meta_path = File.join(CHANGESET_DIR, RELEASE_META_FILE)
File.delete(meta_path) if File.exist?(meta_path)
end
end
Loading
Loading