class Release
- Includes
Shell: #system, #execute, #readlines
Regenerates release content from its base without modifying the checkout.
Definitions
BUMPS = {"patch" => [nil, nil, 1], "minor" => [nil, 1, 0], "major" => [1, 0, 0]}.freeze
Supported stable version increments.
def initialize(root)
Signature
-
parameter
rootString The repository containing the commits to compare.
Implementation
def initialize(root)
@root = File.expand_path(root)
end
def resolve(reference)
Resolve a commit before passing it to other Git commands.
Implementation
def resolve(reference)
git("rev-parse", "--verify", "--end-of-options", "#{reference}^{commit}").strip
end
def worktree(reference)
Yield a temporary detached worktree and remove it even on failure.
Implementation
def worktree(reference)
commit = resolve(reference)
Dir.mktmpdir("bake-gem-release-") do |directory|
path = File.join(directory, "source")
system("git", "worktree", "add", "--detach", path, commit, chdir: @root, out: File::NULL)
begin
yield path
ensure
system("git", "worktree", "remove", "--force", path, chdir: @root, out: File::NULL)
end
end
end
def bake(path, *arguments, **options)
Run Bake tasks in a fresh interpreter, avoiding cached version constants.
Signature
-
parameter
pathString The checkout in which to run the tasks.
-
parameter
argumentsArray(String) Task names and command line arguments.
-
parameter
optionsHash Task options; nil values use the task defaults.
Implementation
def bake(path, *arguments, **options)
Dir.mktmpdir("bake-gem-result-") do |directory|
result = File.join(directory, "result.json")
options.each{|key, value| arguments << "#{key}=#{value}" unless value.nil?}
system("bake", *arguments, "output", "file=#{result}", "format=json", chdir: path)
JSON.parse(File.read(result), symbolize_names: true)
end
end
def metadata(reference)
Inspect a committed gem in isolation.
Implementation
def metadata(reference)
worktree(reference) {|path| bake(path, "gem:metadata")}
end
def bump(previous, target)
Validate an ordinary patch, minor, or major transition.
Implementation
def bump(previous, target)
unless previous.match?(/\A\d+\.\d+\.\d+\z/) && target.match?(/\A\d+\.\d+\.\d+\z/)
raise "Release validation supports stable three-part versions only."
end
BUMPS.each do |name, increment|
version = Version.new(previous.split(".").map(&:to_i), nil).increment(increment)
return name if version.join == target
end
raise "Unsupported release transition: #{previous} -> #{target}."
end
def validate(base:, candidate: "HEAD", optional: false)
Compare an independently generated release tree to the candidate, including added and deleted files.
Signature
-
parameter
baseString Current target commit, or the merged commit's first parent when publishing.
-
parameter
candidateString Proposed or merged release commit.
-
parameter
optionalBoolean Allow ordinary PRs which do not change the version.
Implementation
def validate(base:, candidate: "HEAD", optional: false)
base = resolve(base)
candidate = resolve(candidate)
proposed = metadata(candidate)
worktree(base) do |path|
previous = bake(path, "gem:metadata")
raise "Release changes the gem name." unless proposed[:name] == previous[:name]
return nil if optional && proposed[:version] == previous[:version]
increment = bump(previous[:version], proposed[:version])
generated = bake(path, "gem:release:version:increment", BUMPS.fetch(increment).join(","))
raise "Generated version does not match proposal." unless generated[:version] == proposed[:version]
system("git", "add", "--all", chdir: path)
expected = readlines("git", "write-tree", chdir: path).join.strip
diff = git("diff", "--no-ext-diff", "--no-textconv", candidate, expected, "--")
raise "Release content is stale or contains unrelated changes. Expected changes:\n#{diff}" unless diff.empty?
return proposed.merge(base: base, commit: candidate, bump: increment)
end
end