Last active
October 12, 2015 06:48
-
-
Save phoet/3987667 to your computer and use it in GitHub Desktop.
jenkins release workflow script
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env ruby | |
# encoding: UTF-8 | |
require 'json' | |
require 'pry' | |
class ReleaseError < StandardError; end | |
class Logger | |
def self.log(*output) | |
output = output.flatten + [""] | |
puts(output) | |
end | |
def self.debug(*output) | |
log(output) | |
end | |
def self.info(*output) | |
log("*" * 100, output, "*" * 100) | |
end | |
end | |
class CLI | |
def self.cmd(command) | |
Logger.debug("executing: '#{command}'") | |
output = `#{command}` | |
raise ReleaseError.new("command finished with bad exit code #{$?}") unless $?.success? | |
output.chomp | |
end | |
end | |
class Jenkins | |
BUILD_MAPPINGS = { | |
:develop => :dev, | |
:next_release => :prod, | |
:next_hotfix => :prod, | |
:master => :prod, | |
} | |
HOST = "http://jenkins:8080" | |
attr_reader :job, :json | |
def initialize(job, json) | |
@job = job | |
@json = json | |
end | |
def check_status! | |
if json["color"] != "blue" | |
raise ReleaseError.new("#{job}-build build not ready, fix that first!") | |
end | |
end | |
def last_build | |
json["lastBuild"]["number"] | |
end | |
def to_s | |
json.to_s | |
end | |
def revision | |
self.class.from_build(job, last_build) | |
end | |
def update_config(version) | |
config = load_config | |
config = config.gsub(%r((>origin/)([^/]+/)([^<]+)), '\1\2' + version) | |
File.open("config.xml", "w+") { |f| f.write(config) } | |
url = "#{HOST}/job/#{job}/config.xml" | |
file = "config.xml" | |
HTTP.post(url, file) | |
ensure | |
FileUtils.rm("config.xml") rescue nil | |
end | |
def load_config | |
url = "#{HOST}/job/#{job}/config.xml" | |
HTTP.get(url) | |
end | |
def enable | |
url = "#{HOST}/job/#{job}/enable" | |
HTTP.post(url) | |
end | |
def build | |
url = "#{HOST}/job/#{job}/build" | |
HTTP.post(url) | |
end | |
def prepare(version) | |
self.class.disable_all_prod_builds | |
update_config(version) | |
enable | |
end | |
def run(version) | |
prepare(version) | |
build | |
end | |
def self.disable_all_prod_builds | |
BUILD_MAPPINGS.select { |key, value| value == :prod }.keys.each do |job| | |
url = "#{HOST}/job/#{job}/disable" | |
HTTP.post(url) | |
end | |
end | |
def self.from_job(job) | |
url = "#{HOST}/job/#{job}/api/json" | |
data = HTTP.get(url) | |
self.new(job, JSON.parse(data)) | |
end | |
def self.from_build(job, build) | |
url = "#{HOST}/job/#{job}/#{build}/api/json" | |
data = HTTP.get(url) | |
json = JSON.parse(data) | |
last_rev = json["actions"].find {|action| action["lastBuiltRevision"]}["lastBuiltRevision"] | |
last_rev["SHA1"] | |
end | |
end | |
class HTTP | |
def self.get(url) | |
CLI.cmd("curl -X GET '#{url}' -s") | |
end | |
def self.post(url, file=nil) | |
file = "-d @#{file}" if file | |
CLI.cmd("curl -X POST #{file} '#{url}' -s") | |
end | |
end | |
class Version | |
attr_reader :mayor, :minor, :fix | |
def initialize(mayor, minor, fix) | |
@mayor = mayor | |
@minor = minor | |
@fix = fix | |
end | |
def next_by_type(type) | |
send("next_#{type}") | |
end | |
def next_release | |
"#{mayor}.#{minor + 1}.0" | |
end | |
def next_hotfix | |
"#{mayor}.#{minor}.#{fix + 1}" | |
end | |
def self.latest_version | |
last = CLI.cmd("git describe --tags `git rev-list --tags --max-count=1`") | |
last = last.strip.gsub("v", "") | |
match = last.match(/(\d+\.)(\d+)\.(\d+)/) | |
new(match[1].to_i, match[2].to_i, match[3].to_i) | |
end | |
end | |
class GIT | |
def self.start_release_branch(revision, version) | |
CLI.cmd("git stash -u") | |
CLI.cmd("git fetch") | |
CLI.cmd("git checkout develop") | |
CLI.cmd("git reset #{revision} --hard") | |
CLI.cmd("git checkout -b release/#{version}") | |
CLI.cmd("git push -u origin release/#{version}") | |
end | |
def self.start_hotfix_branch(version) | |
CLI.cmd("git stash -u") | |
CLI.cmd("git fetch") | |
CLI.cmd("git checkout master") | |
CLI.cmd("git reset --hard origin/master") | |
CLI.cmd("git checkout -b hotfix/#{version}") | |
CLI.cmd("git push -u origin hotfix/#{version}") | |
end | |
def self.check_branch(type, revision, version) | |
CLI.cmd("git checkout #{type}/#{version}") | |
if (current_revision = CLI.cmd("git rev-parse HEAD")) != revision | |
raise ReleaseError.new("the revision of the #{type} branch does not match the jenkins revision: '#{revision}' != '#{current_revision}'") | |
end | |
end | |
def self.finish_branch(type, version) | |
CLI.cmd("git stash -u") | |
CLI.cmd("git fetch") | |
CLI.cmd("git checkout #{type}/#{version}") | |
CLI.cmd("git pull origin #{type}/#{version}") | |
CLI.cmd("git checkout develop") | |
CLI.cmd("git pull origin develop") | |
CLI.cmd("git merge #{type}/#{version} -m \"Merge branch '#{type}/#{version}' into develop\" --no-ff") | |
CLI.cmd("git checkout master") | |
CLI.cmd("git pull origin master") | |
CLI.cmd("git merge #{type}/#{version} -m \"Merge branch '#{type}/#{version}' into master\" --no-ff") | |
CLI.cmd("git tag v#{version} -m \"v#{version}\"") | |
CLI.cmd("git push origin master") | |
CLI.cmd("git push origin develop") | |
CLI.cmd("git push origin --tags") | |
end | |
end | |
class GoLive | |
TYPES = [:release, :hotfix] | |
COMMANDS = [:start, :finish] | |
attr_reader :type, :command, :version | |
def initialize(type, command, version) | |
@type = type | |
@command = command | |
@version = version | |
end | |
def execute | |
Logger.info("BEGIN #{command} NEXT #{type} FOR #{version}") | |
send(command) | |
Logger.info("END #{command} NEXT #{type} FOR #{version}") | |
end | |
def start | |
raise "not implemented" | |
end | |
def finish_it(job) | |
job.check_status! | |
revision = job.revision | |
GIT.check_branch(type, revision, version) | |
GIT.finish_branch(type, version) | |
master = Jenkins.from_job(:master) | |
master.run(version) | |
end | |
def self.valid_version?(version) | |
version =~ /^\d+\.\d+\.\d+$/ | |
end | |
def self.from_args(args) | |
raise ReleaseError.new("invalid number of arguments\nrun via: script/release #{TYPES.join('|')} #{COMMANDS.join('|')} [VERSION]") if args.size < 2 | |
type = args[0].to_sym | |
raise ReleaseError.new("invalid type argument: #{type}\nchoose one of those: #{TYPES.join(', ')}") unless TYPES.include?(type) | |
command = args[1].to_sym | |
raise ReleaseError.new("invalid command argument: #{command}\nchoose one of those: #{COMMANDS.join(', ')}") unless COMMANDS.include?(command) | |
if args.size == 3 | |
version = args[2] | |
raise ReleaseError.new("invalid version argument: #{version}\nchoose a format like: 6.0.0") unless self.valid_version?(version) | |
else | |
Logger.info("NO VERSION GIVEN, RETRIEVING VERSION FROM GIT") | |
version = Version.latest_version.next_by_type(type) | |
end | |
Object.module_eval("#{type}".capitalize).new(type, command, version) | |
end | |
end | |
class Release < GoLive | |
def start | |
develop = Jenkins.from_job(:develop) | |
develop.check_status! | |
revision = develop.revision | |
GIT.start_release_branch(revision, version) | |
job = Jenkins.from_job(:next_release) | |
job.run(version) | |
end | |
def finish | |
job = Jenkins.from_job(:next_release) | |
finish_it(job) | |
end | |
end | |
class Hotfix < GoLive | |
def start | |
GIT.start_hotfix_branch(version) | |
job = Jenkins.from_job(:next_hotfix) | |
job.prepare(version) | |
end | |
def finish | |
job = Jenkins.from_job(:next_hotfix) | |
finish_it(job) | |
end | |
end | |
begin | |
GoLive.from_args(ARGV).execute | |
rescue ReleaseError => e | |
puts "B#{'=' * 98}D" | |
puts e | |
puts "B#{'=' * 98}D" | |
exit 1 | |
rescue | |
puts $! | |
puts $!.backtrace | |
exit 1 | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment