Skip to content

Instantly share code, notes, and snippets.

@phoet
Last active October 12, 2015 06:48
Show Gist options
  • Save phoet/3987667 to your computer and use it in GitHub Desktop.
Save phoet/3987667 to your computer and use it in GitHub Desktop.
jenkins release workflow script
#!/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