498 строки
15 KiB
Ruby
Executable File
498 строки
15 KiB
Ruby
Executable File
#!/usr/bin/env ruby
|
|
# frozen_string_literal: true
|
|
|
|
#/ Usage: release [--dry-run] [--skip-version-bump-check] <version> [min_version]
|
|
#/
|
|
#/ Publish a backup-utils release:
|
|
#/ * Updates the package changelog
|
|
#/ * Bumps the backup-utils version if required
|
|
#/ * Creates the release pull request
|
|
#/ * Merges the release pull request
|
|
#/ * Creates the release draft
|
|
#/ * Tags the release
|
|
#/ * Builds the release assets and uploads them
|
|
#/
|
|
#/ Notes:
|
|
#/ * Needs GH_RELEASE_TOKEN and GH_AUTHOR set in the environment.
|
|
#/ * Export GH_OWNER and GH_REPO if you want to use a different owner/repo
|
|
#/ * Only pull requests labeled with bug, feature or enhancement will show up in the
|
|
#/ release page and the changelog.
|
|
#/ * If this is a X.Y.0 release, a minimum supported version needs to be supplied too.
|
|
#/
|
|
require 'json'
|
|
require 'net/http'
|
|
require 'time'
|
|
require 'erb'
|
|
require 'English'
|
|
|
|
API_HOST = ENV['GH_HOST'] || 'api.github.com'
|
|
API_PORT = 443
|
|
GH_REPO = ENV['GH_REPO'] || 'backup-utils'
|
|
GH_OWNER = ENV['GH_OWNER'] || 'github'
|
|
GH_AUTHOR = ENV['GH_AUTHOR']
|
|
DEB_PKG_NAME = 'github-backup-utils'
|
|
GH_BASE_BRANCH = ENV['GH_BASE_BRANCH'] || 'master' # TODO: should we even allow a default or require all params get set explicitly?
|
|
GH_STABLE_BRANCH = ""
|
|
|
|
CHANGELOG_TMPL = '''<%= package_name %> (<%= package_version %>) UNRELEASED; urgency=medium
|
|
|
|
<%- changes.each do |ch| -%>
|
|
* <%= ch.strip.chomp %>
|
|
<% end -%>
|
|
|
|
-- <%= GH_AUTHOR %> <%= Time.now.utc.strftime("%a, %d %b %Y %H:%M:%S %z") %>
|
|
|
|
'''
|
|
|
|
# Override Kernel.warn
|
|
def warn(msg)
|
|
Kernel.warn msg unless @no_warn
|
|
end
|
|
|
|
def client(host = API_HOST, port = API_PORT)
|
|
@http ||= begin
|
|
c = Net::HTTP.new(host, port)
|
|
c.use_ssl = true
|
|
c
|
|
end
|
|
end
|
|
|
|
def get(path)
|
|
req = Net::HTTP::Get.new(path)
|
|
req['Authorization'] = "token #{release_token}"
|
|
client.request(req)
|
|
end
|
|
|
|
def post(path, body)
|
|
req = Net::HTTP::Post.new(path)
|
|
req['Authorization'] = "token #{release_token}"
|
|
req.body = body
|
|
client.request(req)
|
|
end
|
|
|
|
def post_file(path, body)
|
|
req = Net::HTTP::Post.new(path)
|
|
req['Authorization'] = "token #{release_token}"
|
|
req['Content-Type'] = path.match?(/.*\.tar\.gz$/) ? 'application/tar+gzip' : 'application/vnd.debian.binary-package'
|
|
req.body = body
|
|
client.request(req)
|
|
end
|
|
|
|
def put(path, body)
|
|
req = Net::HTTP::Put.new(path)
|
|
req['Authorization'] = "token #{release_token}"
|
|
req.body = body
|
|
client.request(req)
|
|
end
|
|
|
|
def patch(path, body)
|
|
req = Net::HTTP::Patch.new(path)
|
|
req['Authorization'] = "token #{release_token}"
|
|
req.body = body
|
|
client.request(req)
|
|
end
|
|
|
|
def release_token
|
|
token = ENV['GH_RELEASE_TOKEN']
|
|
raise 'GH_RELEASE_TOKEN environment variable not set' if token.nil?
|
|
|
|
token
|
|
end
|
|
|
|
# Create a lightweight tag
|
|
def tag(name, sha)
|
|
body = {
|
|
"ref": "refs/tags/#{name}",
|
|
"sha": sha
|
|
}.to_json
|
|
res = post("/repos/#{GH_OWNER}/#{GH_REPO}/git/refs", body)
|
|
|
|
raise "Creating tag ref failed (#{res.code})" unless res.is_a? Net::HTTPSuccess
|
|
end
|
|
|
|
def bug_or_feature?(issue_hash)
|
|
return true if issue_hash['labels'].find { |label| ['bug', 'feature', 'enhancement'].include?(label['name']) }
|
|
false
|
|
end
|
|
|
|
def issue_from(issue)
|
|
res = get("/repos/#{GH_OWNER}/#{GH_REPO}/issues/#{issue}")
|
|
raise "Issue ##{issue} not found in #{GH_OWNER}/#{GH_REPO}" unless res.is_a? Net::HTTPSuccess
|
|
|
|
JSON.parse(res.body)
|
|
end
|
|
|
|
def beautify_changes(changes)
|
|
out = []
|
|
changes.each do |chg|
|
|
next unless chg =~ /#(\d+)/
|
|
begin
|
|
issue = issue_from Regexp.last_match(1)
|
|
out << "#{issue['title']} ##{Regexp.last_match(1)}" if bug_or_feature?(issue)
|
|
rescue => e
|
|
warn "Warning: #{e.message}"
|
|
end
|
|
end
|
|
|
|
out
|
|
end
|
|
|
|
def changelog
|
|
puts "building changelog by comparing origin/#{GH_STABLE_BRANCH}...origin/#{GH_BASE_BRANCH}"
|
|
changes = `git log --pretty=oneline origin/#{GH_STABLE_BRANCH}...origin/#{GH_BASE_BRANCH} --reverse --grep "Merge pull request" | sort -t\# -k2`.lines.map(&:strip)
|
|
raise 'Building the changelog failed' if $CHILD_STATUS != 0
|
|
|
|
changes
|
|
end
|
|
|
|
def build_changelog(changes, package_name, package_version)
|
|
ERB.new(CHANGELOG_TMPL, nil, '-').result(binding)
|
|
end
|
|
|
|
def update_changelog(changes, name, version, path = 'debian/changelog')
|
|
raise 'debian/changelog not found' unless File.exist?(path)
|
|
File.open("#{path}.new", 'w') do |f|
|
|
f.puts build_changelog changes, name, version
|
|
f.puts(File.read(path))
|
|
end
|
|
File.rename("#{path}.new", path)
|
|
end
|
|
|
|
def create_release(tag_name, branch, rel_name, rel_body, draft = true)
|
|
body = {
|
|
'tag_name': tag_name,
|
|
'target_commitish': branch,
|
|
'name': rel_name,
|
|
'body': rel_body,
|
|
'draft': draft,
|
|
'prerelease': false
|
|
}.to_json
|
|
res = post("/repos/#{GH_OWNER}/#{GH_REPO}/releases", body)
|
|
|
|
raise "Failed to create release (#{res.code})" unless res.is_a? Net::HTTPSuccess
|
|
|
|
JSON.parse(res.body)
|
|
end
|
|
|
|
def publish_release(release_id)
|
|
body = {
|
|
'draft': false
|
|
}.to_json
|
|
res = patch("/repos/#{GH_OWNER}/#{GH_REPO}/releases/#{release_id}", body)
|
|
|
|
raise "Failed to update release (#{res.code})" unless res.is_a? Net::HTTPSuccess
|
|
end
|
|
|
|
def list_releases
|
|
res = get("/repos/#{GH_OWNER}/#{GH_REPO}/releases")
|
|
raise 'Failed to retrieve releases' unless res.is_a? Net::HTTPSuccess
|
|
|
|
JSON.parse(res.body)
|
|
end
|
|
|
|
def release_available?(tag_name)
|
|
return true if list_releases.find { |r| r['tag_name'] == tag_name }
|
|
|
|
false
|
|
end
|
|
|
|
def bump_version(new_version, min_version = nil, path = 'share/github-backup-utils/version')
|
|
current_version = Gem::Version.new(File.read(path).strip.chomp)
|
|
if !@skip_version_bump_check && (Gem::Version.new(new_version) < current_version)
|
|
raise "New version should be newer than #{current_version}"
|
|
end
|
|
File.open("#{path}.new", 'w') { |f| f.puts new_version }
|
|
File.rename("#{path}.new", path)
|
|
|
|
unless min_version.nil?
|
|
content = File.read('bin/ghe-host-check')
|
|
new_content = content.gsub(/supported_minimum_version="[0-9]\.[0-9]+\.0"/, "supported_minimum_version=\"#{min_version}\"")
|
|
File.open('bin/ghe-host-check', 'w') {|file| file.puts new_content }
|
|
|
|
content = File.read('test/testlib.sh')
|
|
new_content = content.gsub(/GHE_TEST_REMOTE_VERSION:=[0-9]\.[0-9]+\.0/,"GHE_TEST_REMOTE_VERSION:=#{new_version}")
|
|
File.open('test/testlib.sh', 'w') {|file| file.puts new_content }
|
|
end
|
|
end
|
|
|
|
def push_release_branch(version)
|
|
unless (out = `git checkout --quiet -b release-#{version}`)
|
|
raise "Creating release branch failed:\n\n#{out}"
|
|
end
|
|
|
|
unless (out = `git commit --quiet -m 'Bump version: #{version} [ci skip]' debian/changelog share/github-backup-utils/version bin/ghe-host-check test/testlib.sh script/cibuild`)
|
|
raise "Error committing changelog and version:\n\n#{out}"
|
|
end
|
|
|
|
unless (out = `git push --quiet origin release-#{version}`)
|
|
raise "Failed pushing the release branch:\n\n#{out}"
|
|
end
|
|
end
|
|
|
|
def update_stable_branch
|
|
`git checkout --quiet #{GH_STABLE_BRANCH}`
|
|
unless (out = `git merge --quiet --ff-only origin/#{GH_BASE_BRANCH}`)
|
|
warn "Merging #{GH_BASE_BRANCH} into #{GH_STABLE_BRANCH} failed:\n\n#{out}"
|
|
end
|
|
unless (out = `git push --quiet origin #{GH_STABLE_BRANCH}`)
|
|
warn "Failed pushing the #{GH_STABLE_BRANCH} branch:\n\n#{out}"
|
|
end
|
|
end
|
|
|
|
def create_release_pr(version, release_body)
|
|
body = {
|
|
'title': "Bump version: #{version}",
|
|
'body': release_body,
|
|
'head': "release-#{version}",
|
|
'base': GH_BASE_BRANCH
|
|
}.to_json
|
|
res = post("/repos/#{GH_OWNER}/#{GH_REPO}/pulls", body)
|
|
raise "Creating release PR failed (#{res.code})" unless res.is_a? Net::HTTPSuccess
|
|
|
|
JSON.parse(res.body)
|
|
end
|
|
|
|
def merge_pr(number, sha, version)
|
|
body = {
|
|
'commit_title': "Merge pull request ##{number} from github/release-#{version}",
|
|
'commit_message': "Bump version: #{version}",
|
|
'sha': sha,
|
|
'merge_method': 'merge'
|
|
}.to_json
|
|
pr_mergeable? number
|
|
res = put("/repos/#{GH_OWNER}/#{GH_REPO}/pulls/#{number}/merge", body)
|
|
raise "Merging PR failed (#{res.code})" unless res.is_a? Net::HTTPSuccess
|
|
|
|
JSON.parse(res.body)
|
|
end
|
|
|
|
class RetryError < StandardError
|
|
end
|
|
|
|
def pr_mergeable?(number)
|
|
begin
|
|
retries ||= 5
|
|
res = get("/repos/#{GH_OWNER}/#{GH_REPO}/pulls/#{number}")
|
|
raise RetryError if JSON.parse(res.body)['mergeable'].nil?
|
|
mergeable = JSON.parse(res.body)['mergeable']
|
|
rescue RetryError
|
|
sleep 1
|
|
retry unless (retries -= 1).zero?
|
|
raise 'PR is unmergable.'
|
|
end
|
|
|
|
mergeable || false
|
|
end
|
|
|
|
def can_auth?
|
|
!ENV['GH_RELEASE_TOKEN'].nil?
|
|
end
|
|
|
|
def repo_exists?
|
|
res = get("/repos/#{GH_OWNER}/#{GH_REPO}")
|
|
res.is_a? Net::HTTPSuccess
|
|
end
|
|
|
|
def can_build_deb?
|
|
system('which debuild > /dev/null 2>&1')
|
|
end
|
|
|
|
def package_tarball
|
|
unless (out = `script/package-tarball 2>&1`)
|
|
raise "Failed to package tarball:\n\n#{out}"
|
|
end
|
|
out
|
|
end
|
|
|
|
def package_deb
|
|
unless (out = `DEB_BUILD_OPTIONS=nocheck script/package-deb 2>&1`)
|
|
raise "Failed to package Debian package:\n\n#{out}"
|
|
end
|
|
out
|
|
end
|
|
|
|
def attach_assets_to_release(upload_url, release_id, files)
|
|
@http = nil
|
|
client(URI(upload_url.gsub(/{.*}/, '')).host)
|
|
begin
|
|
files.each do |file|
|
|
raw_file = File.open(file).read
|
|
res = post_file("/repos/#{GH_OWNER}/#{GH_REPO}/releases/#{release_id}/assets?name=#{File.basename(file)}", raw_file)
|
|
raise "Failed to attach #{file} to release (#{res.code})" unless res.is_a? Net::HTTPSuccess
|
|
end
|
|
rescue => e
|
|
raise e
|
|
end
|
|
@http = nil
|
|
end
|
|
|
|
def clean_up(version)
|
|
`git checkout --quiet #{GH_BASE_BRANCH}`
|
|
`git fetch --quiet origin --prune`
|
|
`git pull --quiet origin #{GH_BASE_BRANCH} --prune`
|
|
`git branch --quiet -D release-#{version} >/dev/null 2>&1`
|
|
`git push --quiet origin :release-#{version} >/dev/null 2>&1`
|
|
`git branch --quiet -D tmp-packaging >/dev/null 2>&1`
|
|
end
|
|
|
|
def is_base_branch_valid?(branch)
|
|
if branch == "master" || branch.match(/^\d+\.\d+-main$/)
|
|
return true
|
|
else
|
|
return false
|
|
end
|
|
end
|
|
|
|
def get_stable_branch_name(branch)
|
|
## derive the proper stable branch. if the base branch is "master" the stable branch is just "stable"
|
|
## if the base branch is a release branch, the stable branch will be "x.y-stable"
|
|
result = ""
|
|
if branch == "master"
|
|
result = "stable"
|
|
else
|
|
result = branch.gsub(/-main$/, "-stable")
|
|
end
|
|
|
|
result
|
|
end
|
|
|
|
#### All the action starts ####
|
|
if $PROGRAM_NAME == __FILE__
|
|
begin
|
|
## validate base branch. this must either be "master" or a release branch which will match the pattern "x.y-main"
|
|
raise "The branch #{GH_BASE_BRANCH} is not valid for releasing backup-utils. branch name must be master or match x.y-main" if !is_base_branch_valid?(GH_BASE_BRANCH)
|
|
|
|
GH_STABLE_BRANCH = get_stable_branch_name(GH_BASE_BRANCH)
|
|
|
|
puts "base branch = " + GH_BASE_BRANCH
|
|
puts "stable branch = " + GH_STABLE_BRANCH
|
|
|
|
args = ARGV.dup
|
|
dry_run = false
|
|
skip_version_bump_check = false
|
|
if args.include?('--dry-run')
|
|
dry_run = true
|
|
args.delete '--dry-run'
|
|
end
|
|
|
|
if args.include?('--no-warn')
|
|
@no_warn = true
|
|
args.delete '--no-warn'
|
|
end
|
|
|
|
if args.include?('--skip-version-bump-check')
|
|
@skip_version_bump_check = true
|
|
args.delete '--skip-version-bump-check'
|
|
end
|
|
|
|
raise 'Usage: release [--dry-run] [--skip-version-bump-check] <version> [min_version]' if args.empty?
|
|
|
|
begin
|
|
version = Gem::Version.new(args[0])
|
|
min_version = args[1] ? args[1] : nil
|
|
rescue ArgumentError
|
|
raise "Error parsing version #{args[0]}"
|
|
end
|
|
|
|
raise "Minimum supported version is required for X.Y.0 releases\n\nUsage: release [--dry-run] <version> [min_version]" if /[0-9]\.[0-9]+\.0/ =~ version.to_s && min_version.nil?
|
|
|
|
raise "The repo #{GH_REPO} does not exist for #{GH_OWNER}" unless repo_exists?
|
|
|
|
raise 'GH_AUTHOR environment variable is not set' if GH_AUTHOR.nil?
|
|
|
|
release_changes = []
|
|
release_changes = beautify_changes changelog if can_auth?
|
|
release_a = false
|
|
release_a = release_available? "v#{version}"
|
|
|
|
puts "Bumping version to #{version}..."
|
|
bump_version version, min_version
|
|
|
|
if dry_run
|
|
puts "Existing release?: #{release_a}"
|
|
puts "New version: #{version}"
|
|
puts "Min version: #{min_version}" unless min_version.nil?
|
|
puts "Owner: #{GH_OWNER}"
|
|
puts "Repo: #{GH_REPO}"
|
|
puts "Author: #{GH_AUTHOR}"
|
|
puts "Token: #{ENV['GH_RELEASE_TOKEN'] && 'set' || 'unset'}"
|
|
puts "Base branch: #{GH_BASE_BRANCH}"
|
|
puts 'Changelog:'
|
|
if release_changes.empty?
|
|
puts ' => No new bug fixes, enhancements or features.'
|
|
else
|
|
release_changes.each { |c| puts " * #{c}" }
|
|
end
|
|
puts "Changes:"
|
|
puts `git diff --color`
|
|
`git checkout -- share/github-backup-utils/version`
|
|
`git checkout -- bin/ghe-host-check`
|
|
`git checkout -- test/testlib.sh`
|
|
exit
|
|
end
|
|
|
|
raise 'Unable to build Debian pkg: "debuild" not found.' unless can_build_deb?
|
|
|
|
raise "Release #{version} already exists." if release_a
|
|
|
|
`git fetch --quiet origin --prune`
|
|
branches = `git branch --all | grep release-#{version}$`
|
|
unless branches.empty?
|
|
out = "Release branch release-#{version} already exists. "
|
|
out += 'Branches found:'
|
|
branches.each_line { |l| out += "\n* #{l.strip.chomp}" }
|
|
raise out
|
|
end
|
|
|
|
puts 'Updating changelog...'
|
|
update_changelog release_changes, DEB_PKG_NAME, version
|
|
release_body = "Includes general improvements & bug fixes"
|
|
release_body += " and support for GitHub Enterprise Server v#{version}" unless min_version.nil?
|
|
release_changes.each do |c|
|
|
release_body += "\n* #{c}"
|
|
end
|
|
|
|
puts 'Pushing release branch and creating release PR...'
|
|
push_release_branch version
|
|
res = create_release_pr(version, "#{release_body}\n\n/cc @github/backup-utils")
|
|
|
|
puts 'Merging release PR...'
|
|
res = merge_pr res['number'], res['head']['sha'], version
|
|
|
|
puts 'Tagging and publishing release...'
|
|
tag "v#{version}", res['sha']
|
|
|
|
puts 'Creating release...'
|
|
release_title = "GitHub Enterprise Server Backup Utilities v#{version}"
|
|
res = create_release "v#{version}", GH_BASE_BRANCH, release_title, release_body, true
|
|
|
|
# Tidy up before building tarball and deb pkg
|
|
clean_up version
|
|
|
|
puts 'Building release tarball...'
|
|
package_tarball
|
|
|
|
puts 'Building Debian pkg...'
|
|
package_deb
|
|
|
|
puts 'Attaching Debian pkg and tarball to release...'
|
|
base_dir = File.expand_path(File.join(File.dirname(__FILE__), '..'))
|
|
attach_assets_to_release res['upload_url'], res['id'], ["#{base_dir}/dist/#{DEB_PKG_NAME}-v#{version}.tar.gz"]
|
|
attach_assets_to_release res['upload_url'], res['id'], ["#{base_dir}/dist/#{DEB_PKG_NAME}_#{version}_all.deb"]
|
|
|
|
puts 'Publishing release...'
|
|
publish_release res['id']
|
|
|
|
puts 'Cleaning up...'
|
|
clean_up version
|
|
|
|
puts "Updating #{GH_STABLE_BRANCH} branch..."
|
|
update_stable_branch
|
|
|
|
puts 'Released!'
|
|
rescue RuntimeError => e
|
|
$stderr.puts "Error: #{e}"
|
|
exit 1
|
|
end
|
|
end
|