Skip to content

Instantly share code, notes, and snippets.

@thecb4
Created May 20, 2018 04:42
Show Gist options
  • Save thecb4/c17880fbc8f485ae4201a6174c3acb82 to your computer and use it in GitHub Desktop.
Save thecb4/c17880fbc8f485ae4201a6174c3acb82 to your computer and use it in GitHub Desktop.
Beak File for managing DevOps process ... brittle replacement for Fastlane
// 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