Created
May 20, 2018 04:42
-
-
Save thecb4/c17880fbc8f485ae4201a6174c3acb82 to your computer and use it in GitHub Desktop.
Beak File for managing DevOps process ... brittle replacement for Fastlane
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
// beak: JohnSundell/ShellOut @ 2.1.0 | |
/* | |
Copyright 2018 The CB4 (Cavelle Benjamin) | |
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), | |
to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, | |
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: | |
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. | |
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, | |
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. | |
*/ | |
/* | |
Uses https://github.com/yonaskolb/Beak as a brittle replacement for Fastlane. | |
*/ | |
import Foundation | |
import ShellOut | |
let iTunesPath = "iTunes" | |
let secretsFile = "secrets.json" | |
let iTunesSecrets = [iTunesPath,secretsFile].joined(separator: "/") | |
struct Secrets: Codable { | |
let bundleIdPrefix: String | |
let projectName: String | |
let appDisplayName: String | |
let teamID: String | |
let appID: String | |
let iTunesConnectUser: String | |
} | |
extension Secrets { | |
static func read(path: String) throws -> Secrets { | |
let text = try String(contentsOfFile: path, encoding: String.Encoding.utf8) | |
let data = try text.data(using: String.Encoding.utf8)! | |
let decoder = JSONDecoder() | |
let decoded = try decoder.decode(Secrets.self, from: data) | |
return decoded | |
} | |
} | |
// https://help.apple.com/itunes-connect/developer/#/devd274dd925 | |
// https://blog.placeit.net/ios-screenshot-sizes/ | |
// https://github.com/tadija/AEXML | |
// https://bou.io/UploadingScreenshotsWithITMSTransporter.html | |
let transporter = "/Applications/Xcode.app/Contents/Applications/Application\\ Loader.app/Contents/itms/bin/iTMSTransporter" | |
public enum DistributionMethod: String { | |
case app_store = "app-store" | |
case ad_hoc = "ad-hoc" | |
case development | |
} | |
public enum Platform: String { | |
case all | |
case macOS | |
case iOS | |
case watchOS | |
case tvOS | |
} | |
public enum Cleanable: String { | |
case xcode | |
case project | |
case carthage | |
} | |
var platform: Platform = .macOS | |
public enum PlatformDestination: String { | |
case macOS_Simulator = "\"platform=OS X\"" | |
case iOS_Simulator = "\"platform=iOS Simulator,name=iPhone 8\"" | |
case watchOS_Simulator = "\"platform=watchOS Simulator,name=Apple Watch - 38mm\"" | |
case tvOS_Simulator = "\"platform=tvOS Simulator,name=Apple TV\"" | |
} | |
let destinations: [ Platform : PlatformDestination ] = [ | |
.macOS: .macOS_Simulator, | |
.iOS: .iOS_Simulator, | |
.watchOS: .watchOS_Simulator, | |
.tvOS: .tvOS_Simulator | |
] | |
/** | |
Removed XCode Project Data | |
- Parameters: | |
- files: files to be cleaned | |
*/ | |
public func clean(files: Cleanable) throws { | |
let secrets = try Secrets.read(path: iTunesSecrets) | |
let project = secrets.projectName | |
print("cleaning \(files)") | |
switch files { | |
case .xcode: | |
_ = try shellOut(to: "rm -rf DerivedData") | |
print("cleaned") | |
case .carthage: | |
_ = try shellOut(to: "rm -rf Carthage") | |
_ = try shellOut(to: "rm -rf Cartfile.resolved") | |
print("cleaned") | |
case .project: | |
_ = try shellOut(to: "rm -rf \(project).xcodeproj") | |
_ = try shellOut(to: "rm -rf DerivedData") | |
} | |
} | |
/** | |
Build Carthage dependencies | |
- Parameters: | |
- platform: platform to build Carthage dependencies for (.macOS, .iOS, .tvOS, .watchOS, , .all) | |
*/ | |
public func build_carthage_dependencies(platform: Platform = .all) throws { | |
print("building carthage dependencies for \(platform)") | |
switch platform { | |
case .macOS, .iOS, .watchOS, .tvOS: | |
let output1 = try shellOut(to: "rm Cartfile.resolved ") | |
print(output1) | |
let output2 = try shellOut(to: "carthage bootstrap --platform \(platform)") | |
print(output2) | |
case .all: | |
let output = try shellOut(to: "carthage bootstrap") | |
print(output) | |
} | |
} | |
/** | |
Build XCode Project | |
- Parameters: | |
- platform: platform to build XCode for (.macOS, .iOS, .tvOS, .watchOS, , .all) | |
*/ | |
public func xcode_build(platform: Platform) throws { | |
let secrets = try Secrets.read(path: iTunesSecrets) | |
let project = secrets.projectName | |
switch platform { | |
case .macOS, .iOS, .watchOS, .tvOS: | |
let action = "xcodebuild clean build -project \(project).xcodeproj -scheme \(project)-\(platform)" | |
let output = try shellOut(to: action) | |
print(output) | |
case .all: | |
print("must chose a platform: macOS | iOS | watchOS | tvOS") | |
} | |
try bump_build() | |
} | |
/** | |
Test XCode Project | |
- Parameters: | |
- platform: platform to build Carthage dependencies for (.macOS, .iOS, .tvOS, .watchOS, .all) | |
*/ | |
public func xcode_test(platform: Platform) throws { | |
let secrets = try Secrets.read(path: iTunesSecrets) | |
let project = secrets.projectName | |
switch platform { | |
case .macOS: | |
guard let destination = destinations[platform] else { fatalError() } | |
let action = "set -o pipefail && xcodebuild test -project \(project).xcodeproj -scheme \(project)-\(platform) -destination \(destination.rawValue) | xcpretty" | |
let output = try shellOut(to: action) | |
print(output) | |
case .iOS, .tvOS: | |
guard let destination = destinations[platform] else { fatalError() } | |
let action = "set -o pipefail && xcodebuild test -project \(project).xcodeproj -scheme \(project)-\(platform) -destination \(destination.rawValue) -enableCodeCoverage YES | xcpretty" | |
let output = try shellOut(to: action) | |
print(output) | |
case .watchOS: | |
print("cannot test watchOS... complain to Apple") | |
case .all: | |
print("must chose a platform: macOS | iOS | watchOS | tvOS") | |
} | |
} | |
/** | |
Installs the product | |
*/ | |
public func install() throws { | |
// implementation here | |
print("installed") | |
} | |
/** | |
Update release from git flow tags | |
- Parameters: | |
- files: files to be cleaned | |
*/ | |
public func bump_release() throws { | |
let output = try shellOut(to: "git rev-parse --abbrev-ref HEAD") | |
let branchInfo = output.split(separator:"/") | |
if branchInfo.count < 2 { | |
print("not a release branch") | |
} else { | |
if (branchInfo[0] == "release") { | |
let semanticVersion = branchInfo[1] | |
// agvtool new-marketing-version | |
let updateVersionOutput = try shellOut(to: "agvtool new-marketing-version \(semanticVersion)") | |
print(updateVersionOutput) | |
} else { | |
print("not a release branch") | |
} | |
} | |
} | |
/** | |
Update build version | |
*/ | |
public func bump_build() throws { | |
//agvtool next-version -all | |
//git rev-list --count develop | |
let output = try shellOut(to: "agvtool next-version -all") | |
print(output) | |
} | |
/** | |
Create app archive | |
- Parameters: | |
- password: iTunes Connect Password | |
- method: Distribution Method | |
*/ | |
public func archive(platform: Platform, method: DistributionMethod = .app_store, uploadSymbols: Bool = true, uploadBitcode: Bool = true) throws { | |
let secrets = try Secrets.read(path: iTunesSecrets) | |
let project = secrets.projectName | |
let teamID = secrets.teamID | |
print("cleaning up iTunes directory") | |
let cleanUpCommand = try shellOut(to: ["rm -rf \(iTunesPath)/\(project)","rm -rf \(iTunesPath)/\(project).xcarchive"]) | |
print(cleanUpCommand) | |
let xarchiveCommand = try shellOut(to: "xcodebuild archive -project \(project).xcodeproj -scheme \(project)-\(platform) -archivePath \(iTunesPath)/\(project).xcarchive") | |
print(xarchiveCommand) | |
let exportOptions = """ | |
<?xml version=\"1.0\" encoding=\"UTF-8\"?> | |
<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd\"> | |
<plist version=\"1.0\"> | |
<dict> | |
<key>teamID</key> | |
<string>\(teamID)</string> | |
<key>method</key> | |
<string>\(method.rawValue)</string> | |
<key>uploadSymbols</key> | |
<\(uploadSymbols.description)/> | |
<key>uploadBitcode</key> | |
<\(uploadBitcode.description)/> | |
</dict> | |
</plist> | |
""" | |
try exportOptions.write(toFile: "\(iTunesPath)/exportOptions.plist", atomically: false, encoding: String.Encoding.utf8) | |
// http://www.matrixprojects.net/p/xcodebuild-export-options-plist/ | |
/* | |
method: (String) The method of distribution, which can be set as any of the following: | |
app-store | |
enterprise | |
ad-hoc | |
development | |
teamID: (String) The development program team identifier. | |
uploadSymbols: (Boolean) Option to include symbols in the generated ipa file. | |
uploadBitcode: (Boolean) Option to include Bitcode. | |
*/ | |
let ipaCommand = try shellOut(to: "xcodebuild -exportArchive -archivePath iTunes/\(project).xcarchive -exportPath \(iTunesPath)/\(project) -exportOptionsPlist \(iTunesPath)/exportOptions.plist") | |
print(ipaCommand) | |
} | |
/** | |
Create app archive | |
- Parameters: | |
- platform: Product platform | |
- password: iTunes Connect Password | |
*/ | |
public func upload(platform: Platform, password: String) throws { | |
let secrets = try Secrets.read(path: iTunesSecrets) | |
let project = secrets.projectName | |
let appID = secrets.appID | |
let bundleIdPrefix = secrets.bundleIdPrefix | |
let user = secrets.iTunesConnectUser | |
// https://gist.github.com/jedi4ever/b1f8b27d4a803d487fa4 | |
let ipaFile = "\(project)-\(platform).ipa" | |
print("cleaning upload directory") | |
let cleanUploadDirCommand = try shellOut(to: "rm -rf upload", at: iTunesPath) | |
print(cleanUploadDirCommand) | |
print("creating upload directory") | |
let makeUploadDirCommand = try shellOut(to: ["mkdir upload", "mkdir upload/\(project).itmsp"], at: iTunesPath) | |
print(makeUploadDirCommand) | |
print("performing md5 check") | |
let md5Command = try shellOut(to: "md5 -q \(ipaFile)", at:"\(iTunesPath)/\(project)") | |
print("getting size of ipa") | |
let sizeCommand = try shellOut(to: "stat -f \"%z\" \(ipaFile)", at:"\(iTunesPath)/\(project)") | |
print("copying ipa") | |
let copyCommand = try shellOut(to: "cp \(project)/\(ipaFile) upload/\(project).itmsp", at: iTunesPath) | |
let semanticVersion = try shellOut(to: "agvtool mvers -terse1") | |
let buildNumber = try shellOut(to: "agvtool vers -terse") | |
let uploadMetaData = """ | |
<?xml version=\"1.0\" encoding=\"UTF-8\"?> | |
<package version=\"software5.4\" xmlns=\"http://apple.com/itunes/importer\"> | |
<software_assets | |
apple_id=\"\(appID)\" | |
bundle_short_version_string=\"\(semanticVersion)\" | |
bundle_version=\"\(buildNumber)\" | |
bundle_identifier=\"\(bundleIdPrefix).\(project)-\(platform)\" | |
app_platform=\"\(platform.rawValue.lowercased())\" | |
> | |
<asset type=\"bundle\"> | |
<data_file> | |
<file_name>\(ipaFile)</file_name> | |
<checksum type=\"md5\">\(md5Command)</checksum> | |
<size>\(sizeCommand)</size> | |
</data_file> | |
</asset> | |
</software_assets> | |
</package> | |
""" | |
print("creating metadata") | |
try uploadMetaData.write(toFile: "\(iTunesPath)/upload/\(project).itmsp/metadata.xml", atomically: false, encoding: String.Encoding.utf8) | |
print("uploading") | |
let uploadCommand = try shellOut(to: "\(transporter) -m upload -f \(iTunesPath)/upload -u \"\(user)\" -p \"\(password)\" -v detailed") | |
print(uploadCommand) | |
} | |
// https://bou.io/UploadingScreenshotsWithITMSTransporter.html | |
/** | |
Download iTunes Connect metadata | |
- Parameters: | |
- password: iTunes Connect Password | |
*/ | |
public func download_metadata(password: String) throws { | |
let secrets = try Secrets.read(path: iTunesSecrets) | |
let sku = secrets.appID | |
let user = secrets.iTunesConnectUser | |
let action = "\(transporter) -m lookupMetadata -u \(user) -p \(password) -apple_id \"\(sku)\" -destination iTunes/download" | |
let downloadCommand = try shellOut(to: action) | |
print(downloadCommand) | |
} | |
/** | |
Deletes the product | |
*/ | |
public func delete() throws { | |
// implementation here | |
print("deleted") | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment