Created
September 8, 2011 19:35
-
-
Save qwzybug/1204447 to your computer and use it in GitHub Desktop.
Ruby script to build, tag, archive, and distribute an app and its dSYM file, in one fell swoop.
This file contains hidden or 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
TF_API_TOKEN: <your TestFlight API token> | |
TF_TEAM_TOKEN: <your TestFlight team token> | |
TF_DISTRIBUTION: <name of distribution list to notify> | |
HOCKEY_APP_ID: <your HockeyApp public App ID> | |
HOCKEY_APP_TOKEN: <your HockeyApp API token> | |
DEVELOPER_PREFIX: <path to your developer directory, e.g., /Developer-4.2> | |
ARCHIVE_DIRECTORY: <path for saving archived builds> | |
DEVELOPER_NAME: <keychain name of developer certificate> | |
PROVISONING_PROFILE: <full path to distribution provisioning profile> | |
BUILD_CONFIGURATION: <OPTIONAL name of the XCode configuration to build, default Release> | |
WORKSPACE: <OPTIONAL xcworkspace path> | |
SCHEME: <OPTIONAL scheme name to build, required if you specify WORKSPACE> |
This file contains hidden or 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 | |
require 'rubygems' | |
require 'json' | |
require 'net/http' | |
require 'net/http/post/multipart' | |
require 'net/https' | |
require 'time' | |
require 'yaml' | |
## | |
## Prepares a build, archives it with its dSYM file, and posts to TestFlight or HockeyApp. | |
## Run from the same directory as your .xcodeproj file. | |
## https://gist.github.com/1204447 | |
## | |
## Expects build_settings.yml, which should look like this: | |
### | |
# TF_API_TOKEN: <your TestFlight API token> | |
# TF_TEAM_TOKEN: <your TestFlight team token> | |
# TF_DISTRIBUTION: <name of distribution list to notify> | |
# | |
## To build for HockeyApp instead of TestFlight, include these lines, and run with `prepare_build hockey` | |
# [HOCKEY_APP_ID: <your HockeyApp public App ID>] | |
# [HOCKEY_APP_TOKEN: <your HockeyApp API token>] | |
# | |
# DEVELOPER_PREFIX: <path to your developer directory, e.g., /Developer-4.2> | |
# ARCHIVE_DIRECTORY: <path for saving archived builds and dSYM files> | |
# | |
# DEVELOPER_NAME: <keychain name of developer certificate> | |
# PROVISONING_PROFILE: <full path to distribution provisioning profile> | |
# [BUILD_CONFIGURATION: <name of the XCode configuration to build, default Release>] | |
# | |
# [WORKSPACE: <workspace name, if you build off a workspace instead of an xcodeproj in the current directory>] | |
# [SCHEME: <scheme name to build from the workspace. required if you specify WORKSPACE>] | |
### | |
## Put it in: | |
## ./build_settings.yml | |
## ./.build_settings.yml | |
## ~/.build_settings.yml | |
## Expects a Version History.txt file in the working directory. | |
## Starting from the top of the file, will include every consecutive | |
## line not starting with "Build" as the current release notes. | |
## E.g., | |
### | |
## - Fix instant crash on every launch | |
## - Change all buttons to puce | |
## - Re-greek all copy | |
## | |
## Build 001 - 2011-08-08T04:04:04Z | |
## - Initial beta | |
### The script will tag the version history file each release. | |
def fail status, msg | |
`rm -rf build` | |
`git checkout -- "Version History.txt"` | |
`git checkout -- "#{@plist_path}"` if @plist_path | |
puts "ERROR: #{msg}" | |
exit status | |
end | |
["./build_settings.yml", "./.build_settings.yml", "#{ENV['HOME']}/.build_settings.yml"].each do |path| | |
if File.exists? path | |
puts "Using settings from #{path}..." | |
CONFIG = YAML::load(open(path)) | |
break | |
end | |
end | |
fail -1, "No build_settings.yml found!" unless CONFIG | |
def check_git_status | |
clean = `git status` =~ /working directory clean/ | |
unless clean | |
puts "Can't prepare a build with a dirty working directory! Commit your work and try again." | |
exit -1 | |
end | |
end | |
def project_file_path | |
xc_proj = Dir["./*.xcodeproj"].first | |
return nil unless xc_proj | |
return "#{xc_proj}/project.pbxproj" | |
end | |
def increment_build_number project_file | |
proj_data = open(project_file).read | |
ms = proj_data.match /INFOPLIST_FILE = \"?(.+\.plist)\"?/ | |
fail -1, "Couldn't find an Info.plist!" unless ms and ms[1] | |
@plist_path = ms[1] | |
plist_data = open(@plist_path).read | |
ms = plist_data.match /<key>CFBundleVersion<\/key>\n\s+<string>(\w+)<\/string>/ | |
fail -1, "Couldn't find a current version number!" unless ms and ms[1] | |
current_version = ms[1].to_i | |
new_version = (current_version + 1).to_s.upcase.rjust(4, '0') | |
plist_data.gsub! /(<key>CFBundleVersion<\/key>\n\s+)<string>(\w+)<\/string>/, "\\1<string>#{new_version}</string>" | |
open(@plist_path, 'w') {|f| f << plist_data} | |
puts "Preparing build #{new_version}..." | |
return new_version | |
end | |
def read_current_release_notes fname = "Version History.txt" | |
notes = open(fname, 'r').read | |
fail -1, "No version history found!" unless notes | |
lines = [] | |
notes.each_line {|l| break if l.match(/^Build/); lines << l} | |
return lines.join | |
end | |
def write_release_notes version, fname = "Version History.txt" | |
notes = open(fname, 'r').read | |
fail -1, "No version history found!" unless notes | |
open("Version History.txt", 'w') do |f| | |
f << "Build #{version} - #{Time.now.utc.iso8601}\n" | |
f << notes | |
end | |
end | |
def build_and_archive version_number | |
puts "Building target..." | |
build_dir = `pwd`.chomp + "/build" | |
configuration = CONFIG['BUILD_CONFIGURATION'] || 'Release' | |
build_command = "#{CONFIG['DEVELOPER_PREFIX']}/usr/bin/xcodebuild" | |
build_command += " -workspace \"#{CONFIG['WORKSPACE']}\" -scheme \"#{CONFIG['SCHEME']}\"" if CONFIG['WORKSPACE'] | |
build_command += " -configuration #{configuration} BUILD_DIR=\"#{build_dir}\" clean build" | |
puts build_command | |
result = `#{build_command}` | |
fail $?, result unless $? == 0 | |
puts "Archiving..." | |
app_paths = Dir["#{build_dir}/#{configuration}-iphoneos/*.app"] | |
fail -1, "No #{configuration} build found!" unless app_paths.length > 0 | |
app_name = app_paths.first.split("/").last.split(".").first | |
out_dir = "#{CONFIG['ARCHIVE_DIRECTORY']}/#{app_name}" | |
result = `mkdir -p "#{out_dir}"` | |
fail $?, result unless $? == 0 | |
out_path = "#{out_dir}/#{app_name}#{version_number}.ipa" | |
result = `/usr/bin/xcrun -sdk iphoneos PackageApplication -v "#{app_paths.first}" -o "#{out_path}" --sign "#{CONFIG['DEVELOPER_NAME']}" --embed "#{CONFIG['PROVISONING_PROFILE']}"` | |
fail $?, result unless $? == 0 | |
dsym_path = Dir["build/#{configuration}-iphoneos/*.app.dSYM"].first | |
dsym_out_path = "#{CONFIG['ARCHIVE_DIRECTORY']}/#{app_name}/#{app_name}#{version_number}.dSYM" | |
`mv "#{dsym_path}" "#{dsym_out_path}"` | |
`rm -rf "#{dsym_out_path}.zip"` # remove any old zip files | |
`cd "#{CONFIG['ARCHIVE_DIRECTORY']}/#{app_name}" && zip -r #{app_name}#{version_number}.dSYM.zip #{app_name}#{version_number}.dSYM` | |
`rm -rf build` | |
puts "Archived to #{out_path}." | |
return out_path | |
end | |
def post_tf_build path, release_notes | |
puts "Posting to TestFlight..." | |
url = URI.parse('http://testflightapp.com/api/builds.json') | |
File.open(path) do |ipa| | |
req = Net::HTTP::Post::Multipart.new url.path, 'file' => UploadIO.new(ipa, "application/octet-stream", path.split("/").last), | |
'api_token' => CONFIG['TF_API_TOKEN'], | |
'team_token' => CONFIG['TF_TEAM_TOKEN'], | |
'notes' => release_notes, | |
'notify' => 'True', | |
'distribution_lists' => CONFIG['TF_DISTRIBUTION'] | |
res = Net::HTTP.start(url.host, url.port) {|http| http.request(req)} | |
fail res.code.to_i, res.body if res.code.to_i < 200 or res.code.to_i > 300 | |
result = JSON.parse res.body | |
return result['install_url'] | |
end | |
end | |
def post_hockey_build ipa_path, release_notes | |
puts "Posting to HockeyApp..." | |
fail -1, "No HOCKEY_APP_ID found in build settings!" unless CONFIG['HOCKEY_APP_ID'] | |
hockey_app_id = CONFIG['HOCKEY_APP_ID'] | |
fail -1, "No HOCKEY_APP_TOKEN found in build settings!" unless CONFIG['HOCKEY_APP_TOKEN'] | |
hockey_app_token = CONFIG['HOCKEY_APP_TOKEN'] | |
dsym_path = ipa_path.gsub(/\.ipa$/, ".dSYM.zip") | |
fail -1, "No dSYM found!" unless File.exists? dsym_path | |
url = URI.parse("https://rink.hockeyapp.net/api/2/apps/#{hockey_app_id}/app_versions") | |
File.open(dsym_path) do |dsym| | |
File.open(ipa_path) do |ipa| | |
req = Net::HTTP::Post::Multipart.new url.path, 'ipa' => UploadIO.new(ipa, "application/octet-stream", ipa_path.split("/").last), | |
'dsym' => UploadIO.new(dsym, "application/octet-stream", dsym_path.split("/").last), | |
'notes' => release_notes, | |
'notes_type' => "1", | |
'notify' => "1", | |
'status' => "2" | |
req['X-HockeyAppToken'] = hockey_app_token | |
http = Net::HTTP.new(url.host, url.port) | |
http.use_ssl = true | |
res = http.request(req) | |
fail res.code.to_i, res.body if res.code.to_i < 200 or res.code.to_i > 300 | |
result = JSON.parse res.body | |
return result['public_url'] | |
end | |
end | |
end | |
def git_tag tag | |
`git commit -am "#{tag}"` | |
`git tag #{tag}` | |
end | |
begin | |
post_to = ARGV.count > 0 ? ARGV[0] : "testflight" | |
fail -1, "Specify either 'testflight' or 'hockey'." unless %w{testflight hockey}.include? post_to | |
check_git_status | |
xc_proj = project_file_path | |
fail -1, "No project file found!" unless xc_proj | |
new_build_number = increment_build_number xc_proj | |
these_release_notes = read_current_release_notes | |
ipa_path = build_and_archive new_build_number | |
install_url = (post_to == 'testflight') ? post_tf_build(ipa_path, these_release_notes) : post_hockey_build(ipa_path, these_release_notes) | |
write_release_notes new_build_number | |
git_tag ipa_path.split("/").last.split(".").first | |
puts "Successfully posted build #{new_build_number}.\n#{install_url}" | |
rescue SystemExit | |
rescue Exception => e | |
fail -1, e | |
end |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment