From fac0e80a2ac0132d4b63540adfb45e86ccbb9e46 Mon Sep 17 00:00:00 2001 From: Elias Wilken Date: Tue, 30 Jan 2024 20:40:09 +0100 Subject: [PATCH] tweak exec process launching --- .github/workflows/ci.yml | 23 ++++++ .github/workflows/release.yml | 95 +++++++++++++++++++++++++ Nautik Helper.xcodeproj/project.pbxproj | 4 +- Nautik Helper/StoredCluster.swift | 74 ++++++++++--------- 4 files changed, 161 insertions(+), 35 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 .github/workflows/release.yml diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..4f07a42 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,23 @@ +name: CI + +on: + push: + branches: ['main'] + pull_request: + branches: ['main'] + +jobs: + build: + name: Build and analyze using xcodebuild + runs-on: macos-14 + + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Select Xcode version 15 + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '15' + - name: Build + run: | + xcodebuild clean build analyze -project 'Nautik Helper.xcodeproj' -scheme 'Nautik Helper' | xcpretty && exit ${PIPESTATUS[0]} diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..9701fd2 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,95 @@ +name: Release + +on: + release: + types: [published] + +jobs: + build: + name: Build app bundle and upload it to the release + runs-on: macos-14 + + steps: + - name: Checkout + uses: actions/checkout@v4 + - name: Select Xcode version 15 + uses: maxim-lobanov/setup-xcode@v1 + with: + xcode-version: '15' + # https://docs.github.com/en/actions/deployment/deploying-xcode-applications/installing-an-apple-certificate-on-macos-runners-for-xcode-development#creating-secrets-for-your-certificate-and-provisioning-profile + # https://defn.io/2023/09/22/distributing-mac-apps-with-github-actions + - name: Install the Apple certificate and provisioning profile + env: + # exported from Xcode (Developer ID Application Certificate) + # base64 -i ID_CERTIFICATE.p12 > ID_CERTIFICATE_BASE64 + ID_CERTIFICATE_BASE64: ${{ secrets.ID_CERTIFICATE_BASE64 }} + # openssl rand -hex 32 > ID_CERTIFICATE_PASSWORD + ID_CERTIFICATE_PASSWORD: ${{ secrets.ID_CERTIFICATE_PASSWORD }} + # exported from Xcode (Apple Development Certificate) + # base64 -i BUILD_CERTIFICATE.p12 > BUILD_CERTIFICATE_BASE64 + BUILD_CERTIFICATE_BASE64: ${{ secrets.BUILD_CERTIFICATE_BASE64 }} + # openssl rand -hex 32 > BUILD_CERTIFICATE_PASSWORD + BUILD_CERTIFICATE_PASSWORD: ${{ secrets.BUILD_CERTIFICATE_PASSWORD }} + # openssl rand -hex 32 > KEYCHAIN_PASSWORD + KEYCHAIN_PASSWORD: ${{ secrets.KEYCHAIN_PASSWORD }} + run: | + # create variables + ID_CERTIFICATE_PATH=$RUNNER_TEMP/id_certificate.p12 + BUILD_CERTIFICATE_PATH=$RUNNER_TEMP/build_certificate.p12 + KEYCHAIN_PATH=$RUNNER_TEMP/app-signing.keychain-db + + # import certificates from secrets + echo -n "$ID_CERTIFICATE_BASE64" | base64 --decode -o $ID_CERTIFICATE_PATH + echo -n "$BUILD_CERTIFICATE_BASE64" | base64 --decode -o $BUILD_CERTIFICATE_PATH + + # create temporary keychain + security create-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + security set-keychain-settings -lut 21600 $KEYCHAIN_PATH + security unlock-keychain -p "$KEYCHAIN_PASSWORD" $KEYCHAIN_PATH + + # import certificate to keychain + security import $ID_CERTIFICATE_PATH -P "$ID_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + security import $BUILD_CERTIFICATE_PATH -P "$BUILD_CERTIFICATE_PASSWORD" -A -t cert -f pkcs12 -k $KEYCHAIN_PATH + security list-keychain -d user -s $KEYCHAIN_PATH + - name: Build app + run: | + mkdir -p dist + + xcodebuild \ + archive \ + -project 'Nautik Helper.xcodeproj'/ \ + -scheme 'Nautik Helper' \ + -configuration Release \ + -destination 'generic/platform=macOS' \ + -archivePath 'dist/Nautik Helper.xcarchive' \ + -allowProvisioningUpdates + + xcodebuild \ + -exportArchive \ + -archivePath 'dist/Nautik Helper.xcarchive' \ + -exportOptionsPlist 'Nautik Helper/ExportOptions.plist' \ + -exportPath dist/ \ + -allowProvisioningUpdates + + ditto -c -k --keepParent 'dist/Nautik Helper.app' dist/helper-${{ github.ref }}.zip + + xcrun notarytool submit \ + --wait \ + --key /Users/elias/Downloads/AuthKey_6F3C73566R.p8 \ + --key-id 6F3C73566R \ + --issuer bc562387-e432-4ba9-90bc-bae79d1a299e \ + dist/helper-1.0.0.zip + + xcrun stapler staple 'dist/Nautik Helper.app' + + # https://stackoverflow.com/questions/60608887/what-is-the-most-efficient-way-to-notarize-and-staple-a-zip-containing-a-app + rm dist/helper-1.0.0.zip + ditto -c -k --keepParent 'dist/Nautik Helper.app' dist/helper-${{ github.ref }}.zip + + - name: Upload app bundle to release + uses: svenstaro/upload-release-action@v2 + with: + file: dist/helper-${{ github.ref }}.zip + asset_name: helper-${{ github.ref }}.zip + tag: ${{ github.ref }} + overwrite: true diff --git a/Nautik Helper.xcodeproj/project.pbxproj b/Nautik Helper.xcodeproj/project.pbxproj index a9dd6d4..d869170 100644 --- a/Nautik Helper.xcodeproj/project.pbxproj +++ b/Nautik Helper.xcodeproj/project.pbxproj @@ -338,7 +338,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.0.3; PRODUCT_BUNDLE_IDENTIFIER = io.nautik.Nautik.Helper; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; @@ -370,7 +370,7 @@ "@executable_path/../Frameworks", ); MACOSX_DEPLOYMENT_TARGET = 14.0; - MARKETING_VERSION = 1.0.2; + MARKETING_VERSION = 1.0.3; PRODUCT_BUNDLE_IDENTIFIER = io.nautik.Nautik.Helper; PRODUCT_NAME = "$(TARGET_NAME)"; SWIFT_EMIT_LOC_STRINGS = YES; diff --git a/Nautik Helper/StoredCluster.swift b/Nautik Helper/StoredCluster.swift index 414f66b..64415c3 100644 --- a/Nautik Helper/StoredCluster.swift +++ b/Nautik Helper/StoredCluster.swift @@ -7,34 +7,34 @@ class StoredCluster: Codable, @unchecked Sendable { var keychain: Keychain.KeychainType var position: Double var name: String - + var cluster: Cluster var authInfo: AuthInfo - + var defaultNamespace: String var error: String? - + var kubeConfigDeviceID: UUID var kubeConfigDeviceUser: String var kubeConfigPath: URL var kubeConfigContextName: String var credentialsExpireAt: Date? var lastEvaluation: Date - + init( id: UUID, keychain: Keychain.KeychainType, position: Double, name: String, - + cluster: Cluster, authInfo: AuthInfo, - + defaultNamespace: String, error: String? = nil, - + kubeConfigDeviceID: UUID, kubeConfigDeviceUser: String, kubeConfigPath: URL, @@ -46,14 +46,14 @@ class StoredCluster: Codable, @unchecked Sendable { self.keychain = keychain self.position = position self.name = name - + self.cluster = cluster self.authInfo = authInfo - + self.defaultNamespace = defaultNamespace self.error = error - + self.kubeConfigDeviceID = kubeConfigDeviceID self.kubeConfigDeviceUser = kubeConfigDeviceUser self.kubeConfigPath = kubeConfigPath @@ -61,19 +61,19 @@ class StoredCluster: Codable, @unchecked Sendable { self.credentialsExpireAt = credentialsExpireAt self.lastEvaluation = lastEvaluation } - + static let decoder = { let decoder = JSONDecoder() decoder.dateDecodingStrategy = .iso8601 return decoder }() - + func evaluateAuth() async throws { if let caFile = cluster.certificateAuthority { let caData = try Data(contentsOf: URL(fileURLWithPath: caFile)) cluster.certificateAuthorityData = caData } - + if let tokenFile = authInfo.tokenFile { let token = try String(contentsOf: URL(fileURLWithPath: tokenFile), encoding: .utf8) authInfo.token = token @@ -87,7 +87,7 @@ class StoredCluster: Codable, @unchecked Sendable { let clientKeyData = try Data(contentsOf: URL(fileURLWithPath: clientKeyFile)) authInfo.clientKeyData = clientKeyData } - + if let impersonate = authInfo.impersonate { // TODO } @@ -97,7 +97,7 @@ class StoredCluster: Codable, @unchecked Sendable { if let impersonateUserExtra = authInfo.impersonateUserExtra { // TODO } - + if let authProvider = authInfo.authProvider { // TODO } @@ -107,16 +107,16 @@ class StoredCluster: Codable, @unchecked Sendable { guard let stdout = try executeCommand(command: exec.command, arguments: exec.args) else { throw "Executing \(exec.command) yielded no stdout." } - + do { let credential = try Self.decoder.decode(ExecCredential.self, from: Data(stdout.utf8)) - + await MainActor.run { [weak self] in self?.authInfo.token = credential.status.token - + self?.authInfo.clientCertificateData = credential.status.clientCertificateData.map { $0.data(using: .utf8).map { Data(base64Encoded: $0) } ?? nil } ?? nil self?.authInfo.clientKeyData = credential.status.clientKeyData.map { $0.data(using: .utf8).map { Data(base64Encoded: $0) } ?? nil } ?? nil - + self?.credentialsExpireAt = credential.status.expirationTimestamp } } catch { @@ -130,7 +130,7 @@ class StoredCluster: Codable, @unchecked Sendable { } else { credentialsExpireAt = nil } - + error = nil lastEvaluation = Date.now } @@ -142,7 +142,7 @@ struct ExecCredential: Codable, @unchecked Sendable { let kind: String let spec: Spec let status: Status - + struct Spec: Codable { let cluster: Cluster? let interactive: Bool? @@ -160,7 +160,7 @@ func executeCommand(command: String, arguments: [String]? = nil) throws -> Strin guard let shell = try runProcess(command: "/usr/bin/env", arguments: ["/bin/sh", "-cl", "echo $SHELL"]) else { throw "Couldn't evaluate the user's SHELL." } - + guard let cmdPath = try runProcess(command: "/usr/bin/env", arguments: [shell, "-cl\(shell.contains("zsh") ? "i" : "")", "which \(command)"]) else { throw "Executable \(command) not found in the user's PATH." } @@ -168,10 +168,10 @@ func executeCommand(command: String, arguments: [String]? = nil) throws -> Strin throw "Executable \(command) not found in the user's PATH." } - let stdout = try runProcess(command: "/usr/bin/env", arguments: [shell, "-cl", "\(cmdPath) \(arguments.map { $0.joined(separator: " ") } ?? "")"]) + let stdout = try runProcess(command: "/usr/bin/env", arguments: [shell, "-cl\(shell.contains("zsh") ? "i" : "")", "\(cmdPath) \(arguments.map { $0.joined(separator: " ") } ?? "")"]) return stdout - + func runProcess(command: String, arguments: [String]?, path: String? = nil) throws -> String? { let task = Process() @@ -185,21 +185,29 @@ func executeCommand(command: String, arguments: [String]? = nil) throws -> Strin } } - task.launchPath = command + task.executableURL = URL(fileURLWithPath: command) if let arguments { task.arguments = arguments } - let pipe = Pipe() - task.standardOutput = pipe - task.standardError = pipe + let outputPipe = Pipe() + let errorPipe = Pipe() + task.standardOutput = outputPipe + task.standardError = errorPipe - task.launch() - task.waitUntilExit() + try task.run() + //task.waitUntilExit() - let data = pipe.fileHandleForReading.readDataToEndOfFile() - let string = String(data: data, encoding: .utf8)?.trimmingCharacters(in: .whitespacesAndNewlines) + let outputData = outputPipe.fileHandleForReading.readDataToEndOfFile() + let errorData = errorPipe.fileHandleForReading.readDataToEndOfFile() + + let output = String(decoding: outputData, as: UTF8.self).trimmingCharacters(in: .whitespacesAndNewlines) + let error = String(decoding: errorData, as: UTF8.self) + + if !error.isEmpty { + throw error + } - return string + return output } }