From be782e3febb6d5347fd37d67c84c6805fec4ebf8 Mon Sep 17 00:00:00 2001 From: Finn Voorhees Date: Mon, 11 Dec 2023 19:57:40 +0000 Subject: [PATCH] Working version --- .swiftformat | 20 +++++ Package.resolved | 50 +++++++++++ Package.swift | 8 +- Sources/xcc.swift | 208 +++++++++++++++++++++++++++++++++++++++++++--- 4 files changed, 272 insertions(+), 14 deletions(-) create mode 100644 .swiftformat create mode 100644 Package.resolved diff --git a/.swiftformat b/.swiftformat new file mode 100644 index 0000000..879e079 --- /dev/null +++ b/.swiftformat @@ -0,0 +1,20 @@ +--enable blankLineAfterImports +--enable blankLinesBetweenImports +--enable blockComments +--enable docComments +--enable isEmpty +--enable markTypes +--enable organizeDeclarations + +--disable numberFormatting +--disable redundantNilInit +--disable trailingCommas +--disable wrapMultilineStatementBraces + +--ifdef no-indent +--funcattributes same-line +--typeattributes same-line +--varattributes same-line +--ranges no-space +--header strip +--selfrequired log,debug,info,notice,warning,trace,error,critical,fault diff --git a/Package.resolved b/Package.resolved new file mode 100644 index 0000000..147d5eb --- /dev/null +++ b/Package.resolved @@ -0,0 +1,50 @@ +{ + "pins" : [ + { + "identity" : "appstoreconnect-swift-sdk", + "kind" : "remoteSourceControl", + "location" : "git@github.com:AvdLee/appstoreconnect-swift-sdk.git", + "state" : { + "revision" : "d345a2bcacdaa053ee7c758f05c97f668f24d18a", + "version" : "3.0.1" + } + }, + { + "identity" : "swift-argument-parser", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-argument-parser.git", + "state" : { + "revision" : "c8ed701b513cf5177118a175d85fbbbcd707ab41", + "version" : "1.3.0" + } + }, + { + "identity" : "swift-crypto", + "kind" : "remoteSourceControl", + "location" : "https://github.com/apple/swift-crypto.git", + "state" : { + "revision" : "60f13f60c4d093691934dc6cfdf5f508ada1f894", + "version" : "2.6.0" + } + }, + { + "identity" : "swifttui", + "kind" : "remoteSourceControl", + "location" : "https://github.com/Finnvoor/SwiftTUI.git", + "state" : { + "revision" : "c4a996bf0d9252ab6d308a0c8b624b7ac21eef79", + "version" : "1.0.0" + } + }, + { + "identity" : "urlqueryencoder", + "kind" : "remoteSourceControl", + "location" : "https://github.com/CreateAPI/URLQueryEncoder.git", + "state" : { + "revision" : "4ce950479707ea109f229d7230ec074a133b15d7", + "version" : "0.2.1" + } + } + ], + "version" : 2 +} diff --git a/Package.swift b/Package.swift index f42e547..2ba091d 100644 --- a/Package.swift +++ b/Package.swift @@ -1,20 +1,22 @@ // swift-tools-version: 5.9 -// The swift-tools-version declares the minimum version of Swift required to build this package. import PackageDescription let package = Package( name: "xcc", + platforms: [.macOS(.v11)], dependencies: [ .package(url: "https://github.com/apple/swift-argument-parser.git", from: "1.2.0"), + .package(url: "git@github.com:AvdLee/appstoreconnect-swift-sdk.git", from: "3.0.1"), + .package(url: "https://github.com/Finnvoor/SwiftTUI.git", from: "1.0.0") ], targets: [ - // Targets are the basic building blocks of a package, defining a module or a test suite. - // Targets can depend on other targets in this package and products from dependencies. .executableTarget( name: "xcc", dependencies: [ .product(name: "ArgumentParser", package: "swift-argument-parser"), + .product(name: "AppStoreConnect-Swift-SDK", package: "AppStoreConnect-Swift-SDK"), + .product(name: "SwiftTUI", package: "SwiftTUI") ] ), ] diff --git a/Sources/xcc.swift b/Sources/xcc.swift index d742375..368cffb 100644 --- a/Sources/xcc.swift +++ b/Sources/xcc.swift @@ -1,14 +1,200 @@ -// The Swift Programming Language -// https://docs.swift.org/swift-book -// -// Swift Argument Parser -// https://swiftpackageindex.com/apple/swift-argument-parser/documentation - +import AppStoreConnect_Swift_SDK import ArgumentParser +import Foundation +import SwiftTUI + +// MARK: - xcc + +@main struct xcc: AsyncParsableCommand { + @Option var issuerID: String? + @Option var privateKeyID: String? + @Option var privateKey: String? + + @Option var product: String? + @Option var workflow: String? + @Option var reference: String? + + mutating func run() async throws { + Termios.enterRawMode() + defer { Termios.pop() } + signal(SIGINT) { _ in + Cursor.show() + fflush(stdout) + signal(SIGINT, SIG_DFL) + raise(SIGINT) + } + + let issuerID = issuerID ?? ProcessInfo.processInfo.environment["XCC_ISSUER_ID"] + let privateKeyID = privateKeyID ?? ProcessInfo.processInfo.environment["XCC_PRIVATE_KEY_ID"] + let privateKey = privateKey ?? ProcessInfo.processInfo.environment["XCC_PRIVATE_KEY"] + + guard let issuerID else { throw ValidationError("Missing Issuer ID. Create an API key at https://appstoreconnect.apple.com/access/api") } + guard let privateKeyID else { throw ValidationError("Missing Private Key ID. Create an API key at https://appstoreconnect.apple.com/access/api") } + guard var privateKey else { throw ValidationError("Missing Private Key. Create an API key at https://appstoreconnect.apple.com/access/api") } + + privateKey = privateKey.replacingOccurrences(of: "\n", with: "") + if privateKey.hasPrefix("-----BEGIN PRIVATE KEY-----") { + privateKey.removeFirst("-----BEGIN PRIVATE KEY-----".count) + } + if privateKey.hasSuffix("-----END PRIVATE KEY-----") { + privateKey.removeLast("-----END PRIVATE KEY-----".count) + } + + let configuration = try APIConfiguration( + issuerID: issuerID, + privateKeyID: privateKeyID, + privateKey: privateKey + ) + let provider = APIProvider(configuration: configuration) + + let products = try await provider.request( + APIEndpoint.v1.ciProducts.get(parameters: .init( + fieldsCiProducts: [.bundleID, .name], + include: [.bundleID] + )) + ).data + + let selectedProduct = if let product { + products.first(where: { $0.attributes?.name == product }) + } else { + chooseFromList(products, prompt: "Select a product") + } + guard let selectedProduct else { + throw Error.couldNotFindProduct(availableProducts: products) + } + + let workflows = try await provider.request( + APIEndpoint.v1.ciProducts.id(selectedProduct.id).workflows.get() + ).data + + let selectedWorkflow = if let workflow { + workflows.first(where: { $0.attributes?.name == workflow }) + } else { + chooseFromList(workflows, prompt: "Select a workflow") + } + guard let selectedWorkflow else { + throw Error.couldNotFindWorkflow(availableWorkflows: workflows) + } + + let repository = try await provider.request( + APIEndpoint.v1.ciWorkflows.id(selectedWorkflow.id).repository.get() + ).data + + let gitReferences = try await provider.request( + APIEndpoint.v1.scmRepositories.id(repository.id).gitReferences.get() + ).data + + let selectedGitReference = if let reference { + gitReferences.first(where: { $0.attributes?.name == reference }) + } else { + chooseFromList(gitReferences, prompt: "Select a reference") + } + guard let selectedGitReference else { + throw Error.couldNotFindReference(availableReferences: gitReferences) + } + + _ = try? await provider.request(APIEndpoint.v1.ciBuildRuns.post(.init( + data: .init( + type: .ciBuildRuns, + relationships: .init( + workflow: .init(data: .init( + type: .ciWorkflows, + id: selectedWorkflow.id + )), + sourceBranchOrTag: .init(data: .init( + type: .scmGitReferences, + id: selectedGitReference.id + )) + ) + ) + ))).data + + print("✓ ".brightGreen.bold + "Build started") + } +} + +// MARK: xcc.Error + +extension xcc { + enum Error: LocalizedError { + case missingIssuerID + case missingPrivateKeyID + case missingPrivateKey + + case couldNotFindProduct(availableProducts: [CiProduct]) + case couldNotFindWorkflow(availableWorkflows: [CiWorkflow]) + case couldNotFindReference(availableReferences: [ScmGitReference]) + + // MARK: Internal + + var errorDescription: String? { + switch self { + case .missingIssuerID: """ + Missing Issuer ID. Create an API key at https://appstoreconnect.apple.com/access/api and specify an Issuer ID using one of the following: + - Pass it to xcc as a flag (--issuer-id ) + - Set an environment variable (XCC_ISSUER_ID=) + """ + + case .missingPrivateKeyID: """ + Missing Private Key ID. Create an API key at https://appstoreconnect.apple.com/access/api and specify a Private Key ID using one of the following: + - Pass it to xcc as a flag (--private-key-id ) + - Set an environment variable (XCC_PRIVATE_KEY_ID=) + """ + + case .missingPrivateKey: """ + Missing Private Key. Create an API key at https://appstoreconnect.apple.com/access/api and specify a Private Key using one of the following: + - Pass it to xcc as a flag (--private-key ) + - Set an environment variable (XCC_PRIVATE_KEY=) + """ + + case let .couldNotFindProduct(products): """ + Could not find product with specified name. Available products: + \(products.compactMap(\.attributes?.name).map { "- \($0)" }.joined(separator: "\n")) + """ + + case let .couldNotFindWorkflow(workflows): """ + Could not find workflow with specified name. Available workflows: + \(workflows.compactMap(\.attributes?.name).map { "- \($0)" }.joined(separator: "\n")) + """ + + case let .couldNotFindReference(references): """ + Could not find reference with specified name. Available references: + \(references.compactMap(\.attributes?.name).map { "- \($0)" }.joined(separator: "\n")) + """ + } + } + } +} + +// MARK: - CiProduct + CustomStringConvertible + +extension CiProduct: CustomStringConvertible { + public var description: String { + let name = attributes?.name ?? "Unknown" + let bundleID = relationships?.bundleID?.data + return "\(name) \((bundleID.map { "(\($0.id))" } ?? "").faint)" + } +} + +// MARK: - CiWorkflow + CustomStringConvertible + +extension CiWorkflow: CustomStringConvertible { + public var description: String { + attributes?.name ?? "Unknown" + } +} + +// MARK: - ScmGitReference + CustomStringConvertible -@main -struct xcc: ParsableCommand { - mutating func run() throws { - print("Hello, world!") +extension ScmGitReference: CustomStringConvertible { + public var description: String { + switch attributes?.kind { + case .branch: + "(branch) ".faint + (attributes?.name ?? "") + case .tag: + " (tag) ".faint + (attributes?.name ?? "") + case nil: + attributes?.name ?? "" + } } -} \ No newline at end of file +}