From d1739534b2b7d6bb7254cec9e247e64c281fb969 Mon Sep 17 00:00:00 2001 From: Mattia Valzelli Date: Wed, 23 Aug 2023 22:50:20 +0200 Subject: [PATCH] Add runner registration for repository --- .../GitHubCredentialsStore.swift | 3 + .../GitHubCredentialsStoreKeychain.swift | 24 ++++++ .../GitHubService/GitHubRunnerScope.swift | 6 ++ .../Sources/GitHubService/GitHubService.swift | 9 +- .../GitHubServiceLive/GitHubServiceLive.swift | 85 +++++++++++++++---- Packages/Settings/Package.swift | 2 + .../Sources/SettingsStore/SettingsStore.swift | 4 + .../GitHubPrivateKeyPicker.swift | 61 ++++++------- .../GitHubSettings/GitHubSettingsView.swift | 44 +++++++--- .../GitHubSettingsViewModel.swift | 39 +++++++-- .../Sources/SettingsUI/Internal/L10n.swift | 6 ++ .../Supporting files/Localizable.strings | 3 + ...tualMachineResourcesServiceEphemeral.swift | 29 +++++-- ...irtualMachineResourcesServiceFactory.swift | 1 + 14 files changed, 241 insertions(+), 75 deletions(-) create mode 100644 Packages/GitHub/Sources/GitHubService/GitHubRunnerScope.swift diff --git a/Packages/GitHub/Sources/GitHubCredentialsStore/GitHubCredentialsStore.swift b/Packages/GitHub/Sources/GitHubCredentialsStore/GitHubCredentialsStore.swift index 03ddefd..ae081f9 100644 --- a/Packages/GitHub/Sources/GitHubCredentialsStore/GitHubCredentialsStore.swift +++ b/Packages/GitHub/Sources/GitHubCredentialsStore/GitHubCredentialsStore.swift @@ -2,9 +2,12 @@ import Foundation public protocol GitHubCredentialsStore: AnyObject { var organizationName: String? { get async } + var repositoryName: String? { get async } + var ownerName: String? { get async } var appId: String? { get async } var privateKey: Data? { get async } func setOrganizationName(_ organizationName: String?) async + func setRepository(_ repositoryName: String?, withOwner ownerName: String?) async func setAppID(_ appID: String?) async func setPrivateKey(_ privateKeyData: Data?) async } diff --git a/Packages/GitHub/Sources/GitHubCredentialsStoreKeychain/GitHubCredentialsStoreKeychain.swift b/Packages/GitHub/Sources/GitHubCredentialsStoreKeychain/GitHubCredentialsStoreKeychain.swift index 30173ca..d1a4954 100644 --- a/Packages/GitHub/Sources/GitHubCredentialsStoreKeychain/GitHubCredentialsStoreKeychain.swift +++ b/Packages/GitHub/Sources/GitHubCredentialsStoreKeychain/GitHubCredentialsStoreKeychain.swift @@ -6,6 +6,8 @@ import RSAPrivateKey public final actor GitHubCredentialsStoreKeychain: GitHubCredentialsStore { private enum PasswordAccount { static let organizationName = "github.credentials.organizationName" + static let repositoryName = "github.credentials.repositoryName" + static let ownerName = "github.credentials.ownerName" static let appId = "github.credentials.appId" } @@ -18,6 +20,16 @@ public final actor GitHubCredentialsStoreKeychain: GitHubCredentialsStore { return await keychain.password(forAccount: PasswordAccount.organizationName, belongingToService: serviceName) } } + public var repositoryName: String? { + get async { + return await keychain.password(forAccount: PasswordAccount.repositoryName, belongingToService: serviceName) + } + } + public var ownerName: String? { + get async { + return await keychain.password(forAccount: PasswordAccount.ownerName, belongingToService: serviceName) + } + } public var appId: String? { get async { return await keychain.password(forAccount: PasswordAccount.appId, belongingToService: serviceName) @@ -44,6 +56,18 @@ public final actor GitHubCredentialsStoreKeychain: GitHubCredentialsStore { await keychain.removePassword(forAccount: PasswordAccount.organizationName, belongingToService: serviceName) } } + public func setRepository(_ repositoryName: String?, withOwner ownerName: String?) async { + if let repositoryName { + _ = await keychain.setPassword(repositoryName, forAccount: PasswordAccount.repositoryName, belongingToService: serviceName) + } else { + await keychain.removePassword(forAccount: PasswordAccount.repositoryName, belongingToService: serviceName) + } + if let ownerName { + _ = await keychain.setPassword(ownerName, forAccount: PasswordAccount.ownerName, belongingToService: serviceName) + } else { + await keychain.removePassword(forAccount: PasswordAccount.ownerName, belongingToService: serviceName) + } + } public func setAppID(_ appID: String?) async { if let appID { diff --git a/Packages/GitHub/Sources/GitHubService/GitHubRunnerScope.swift b/Packages/GitHub/Sources/GitHubService/GitHubRunnerScope.swift new file mode 100644 index 0000000..b62157e --- /dev/null +++ b/Packages/GitHub/Sources/GitHubService/GitHubRunnerScope.swift @@ -0,0 +1,6 @@ +import Foundation + +public enum GitHubRunnerScope: String, CaseIterable { + case organization + case repo +} diff --git a/Packages/GitHub/Sources/GitHubService/GitHubService.swift b/Packages/GitHub/Sources/GitHubService/GitHubService.swift index 2c403d7..695e483 100644 --- a/Packages/GitHub/Sources/GitHubService/GitHubService.swift +++ b/Packages/GitHub/Sources/GitHubService/GitHubService.swift @@ -1,7 +1,10 @@ import Foundation public protocol GitHubService { - func getAppAccessToken() async throws -> GitHubAppAccessToken - func getRunnerRegistrationToken(with appAccessToken: GitHubAppAccessToken) async throws -> GitHubRunnerRegistrationToken - func getRunnerDownloadURL(with appAccessToken: GitHubAppAccessToken) async throws -> URL + func getAppAccessToken(runnerScope: GitHubRunnerScope) async throws -> GitHubAppAccessToken + func getRunnerRegistrationToken( + with appAccessToken: GitHubAppAccessToken, + runnerScope: GitHubRunnerScope + ) async throws -> GitHubRunnerRegistrationToken + func getRunnerDownloadURL(with appAccessToken: GitHubAppAccessToken, runnerScope: GitHubRunnerScope) async throws -> URL } diff --git a/Packages/GitHub/Sources/GitHubServiceLive/GitHubServiceLive.swift b/Packages/GitHub/Sources/GitHubServiceLive/GitHubServiceLive.swift index dbc2a6b..3e18d48 100644 --- a/Packages/GitHub/Sources/GitHubServiceLive/GitHubServiceLive.swift +++ b/Packages/GitHub/Sources/GitHubServiceLive/GitHubServiceLive.swift @@ -6,6 +6,8 @@ import NetworkingService private enum GitHubServiceLiveError: LocalizedError { case organizationNameUnavailable + case repositoryNameUnavailable + case repositoryOwnerNameUnavailable case appIDUnavailable case privateKeyUnavailable case appIsNotInstalled @@ -15,6 +17,10 @@ private enum GitHubServiceLiveError: LocalizedError { switch self { case .organizationNameUnavailable: return "The organization name is not available" + case .repositoryNameUnavailable: + return "The repository name is not available" + case .repositoryOwnerNameUnavailable: + return "The repository owner name is not available" case .appIDUnavailable: return "The app ID is not available" case .privateKeyUnavailable: @@ -37,8 +43,8 @@ public final class GitHubServiceLive: GitHubService { self.networkingService = networkingService } - public func getAppAccessToken() async throws -> GitHubAppAccessToken { - let appInstallation = try await getAppInstallation() + public func getAppAccessToken(runnerScope: GitHubRunnerScope) async throws -> GitHubAppAccessToken { + let appInstallation = try await getAppInstallation(runnerScope: runnerScope) let installationID = String(appInstallation.id) let appID = String(appInstallation.appId) let url = baseURL.appending(path: "/app/installations/\(installationID)/access_tokens") @@ -53,9 +59,8 @@ public final class GitHubServiceLive: GitHubService { } } - public func getRunnerDownloadURL(with appAccessToken: GitHubAppAccessToken) async throws -> URL { - let organizationName = try await getOrganizationName() - let url = baseURL.appending(path: "/orgs/\(organizationName)/actions/runners/downloads") + public func getRunnerDownloadURL(with appAccessToken: GitHubAppAccessToken, runnerScope: GitHubRunnerScope) async throws -> URL { + let url = try await baseURL.appending(path: runnerScope.runnerDownloadPath(using: credentialsStore)) let request = URLRequest(url: url).addingBearerToken(appAccessToken.rawValue) let downloads = try await networkingService.load([GitHubRunnerDownload].self, from: request).map(\.value) let os = "osx" @@ -66,9 +71,11 @@ public final class GitHubServiceLive: GitHubService { return download.downloadURL } - public func getRunnerRegistrationToken(with appAccessToken: GitHubAppAccessToken) async throws -> GitHubRunnerRegistrationToken { - let organizationName = try await getOrganizationName() - let url = baseURL.appending(path: "/orgs/\(organizationName)/actions/runners/registration-token") + public func getRunnerRegistrationToken( + with appAccessToken: GitHubAppAccessToken, + runnerScope: GitHubRunnerScope + ) async throws -> GitHubRunnerRegistrationToken { + let url = try await baseURL.appending(path: runnerScope.runnerRegistrationPath(using: credentialsStore)) var request = URLRequest(url: url).addingBearerToken(appAccessToken.rawValue) request.httpMethod = "POST" return try await networkingService.load(IntermediateGitHubRunnerRegistrationToken.self, from: request).map { parameters in @@ -78,25 +85,18 @@ public final class GitHubServiceLive: GitHubService { } private extension GitHubServiceLive { - private func getAppInstallation() async throws -> GitHubAppInstallation { + private func getAppInstallation(runnerScope: GitHubRunnerScope) async throws -> GitHubAppInstallation { let url = baseURL.appending(path: "/app/installations") let token = try await getAppJWTToken() let request = URLRequest(url: url).addingBearerToken(token) let appInstallations = try await networkingService.load([GitHubAppInstallation].self, from: request).map(\.value) - let organizationName = await credentialsStore.organizationName - guard let appInstallation = appInstallations.first(where: { $0.account.login == organizationName }) else { + let loginName = await runnerScope.runnerLogin(using: credentialsStore) + guard let appInstallation = appInstallations.first(where: { $0.account.login == loginName }) else { throw GitHubServiceLiveError.appIsNotInstalled } return appInstallation } - private func getOrganizationName() async throws -> String { - guard let organizationName = await credentialsStore.organizationName else { - throw GitHubServiceLiveError.organizationNameUnavailable - } - return organizationName - } - private func getAppJWTToken() async throws -> String { guard let privateKey = await credentialsStore.privateKey else { throw GitHubServiceLiveError.privateKeyUnavailable @@ -115,3 +115,52 @@ private extension URLRequest { return mutableRequest } } + +private extension GitHubRunnerScope { + func runnerRegistrationPath(using credentialsStore: GitHubCredentialsStore) async throws -> String { + switch self { + case .organization: + guard let organizationName = await credentialsStore.organizationName else { + throw GitHubServiceLiveError.organizationNameUnavailable + } + return "/orgs/\(organizationName)/actions/runners/registration-token" + case .repo: + guard let repositoryName = await credentialsStore.repositoryName else { + throw GitHubServiceLiveError.repositoryNameUnavailable + } + guard let ownerName = await credentialsStore.ownerName else { + throw GitHubServiceLiveError.repositoryOwnerNameUnavailable + } + + return "/repos/\(ownerName)/\(repositoryName)/actions/runners/registration-token" + } + } + + func runnerDownloadPath(using credentialsStore: GitHubCredentialsStore) async throws -> String { + switch self { + case .organization: + guard let organizationName = await credentialsStore.organizationName else { + throw GitHubServiceLiveError.organizationNameUnavailable + } + return "/orgs/\(organizationName)/actions/runners/downloads" + case .repo: + guard let repositoryName = await credentialsStore.repositoryName else { + throw GitHubServiceLiveError.repositoryNameUnavailable + } + guard let ownerName = await credentialsStore.ownerName else { + throw GitHubServiceLiveError.repositoryOwnerNameUnavailable + } + + return "/repos/\(ownerName)/\(repositoryName)/actions/runners/downloads" + } + } + + func runnerLogin(using credentialsStore: GitHubCredentialsStore) async -> String? { + switch self { + case .organization: + return await credentialsStore.organizationName + case .repo: + return await credentialsStore.ownerName + } + } +} diff --git a/Packages/Settings/Package.swift b/Packages/Settings/Package.swift index c308fbf..368a2ab 100644 --- a/Packages/Settings/Package.swift +++ b/Packages/Settings/Package.swift @@ -19,10 +19,12 @@ let package = Package( targets: [ .target(name: "Settings"), .target(name: "SettingsStore", dependencies: [ + .product(name: "GitHubService", package: "GitHub"), "Settings" ]), .target(name: "SettingsUI", dependencies: [ .product(name: "GitHubCredentialsStore", package: "GitHub"), + .product(name: "GitHubService", package: "GitHub"), .product(name: "LogExporter", package: "Logging"), "Settings", "SettingsStore", diff --git a/Packages/Settings/Sources/SettingsStore/SettingsStore.swift b/Packages/Settings/Sources/SettingsStore/SettingsStore.swift index 54a75dd..13005c8 100644 --- a/Packages/Settings/Sources/SettingsStore/SettingsStore.swift +++ b/Packages/Settings/Sources/SettingsStore/SettingsStore.swift @@ -1,4 +1,5 @@ import Combine +import GitHubService import Settings import SwiftUI @@ -12,6 +13,7 @@ public final class SettingsStore: ObservableObject { static let gitHubPrivateKeyName = "gitHubPrivateKeyName" static let gitHubRunnerLabels = "gitHubRunnerLabels" static let gitHubRunnerGroup = "gitHubRunnerGroup" + static let githubRunnerScope = "githubRunnerScope" } @AppStorage(AppStorageKey.applicationUIMode) @@ -30,6 +32,8 @@ public final class SettingsStore: ObservableObject { public var gitHubRunnerLabels = "tartelet" @AppStorage(AppStorageKey.gitHubRunnerGroup) public var gitHubRunnerGroup = "" + @AppStorage(AppStorageKey.githubRunnerScope) + public var githubRunnerScope: GitHubRunnerScope = .organization public init() {} diff --git a/Packages/Settings/Sources/SettingsUI/Internal/GitHubSettings/GitHubPrivateKeyPicker.swift b/Packages/Settings/Sources/SettingsUI/Internal/GitHubSettings/GitHubPrivateKeyPicker.swift index 08fcc7f..360c7da 100644 --- a/Packages/Settings/Sources/SettingsUI/Internal/GitHubSettings/GitHubPrivateKeyPicker.swift +++ b/Packages/Settings/Sources/SettingsUI/Internal/GitHubSettings/GitHubPrivateKeyPicker.swift @@ -12,37 +12,40 @@ struct GitHubPrivateKeyPicker: View { } var body: some View { - LabeledContent { - VStack { - HStack { - TextField( - L10n.Settings.Github.privateKey, - text: $filename, - prompt: Text(L10n.Settings.Github.PrivateKey.placeholder) - ) - .labelsHidden() - .disabled(true) - Button { - if let fileURL = presentOpenPanel() { - onSelectFile(fileURL) - } - } label: { - Text(L10n.Settings.Github.PrivateKey.selectFile) - }.disabled(!isEnabled) - } - Text(L10n.Settings.Github.PrivateKey.scopes) - .multilineTextAlignment(.center) - .lineSpacing(4) - .padding(8) - .frame(maxWidth: .infinity) - .foregroundColor(.secondary) - .background { - RoundedRectangle(cornerRadius: 12) - .stroke(.separator, lineWidth: 1) + VStack(spacing: 16) { + LabeledContent { + VStack { + HStack { + TextField( + L10n.Settings.Github.privateKey, + text: $filename, + prompt: Text(L10n.Settings.Github.PrivateKey.placeholder) + ) + .labelsHidden() + .disabled(true) + Button { + if let fileURL = presentOpenPanel() { + onSelectFile(fileURL) + } + } label: { + Text(L10n.Settings.Github.PrivateKey.selectFile) + }.disabled(!isEnabled) } + } + } label: { + Text(L10n.Settings.Github.privateKey) } - } label: { - Text(L10n.Settings.Github.privateKey) + + Text(L10n.Settings.Github.PrivateKey.scopes) + .multilineTextAlignment(.center) + .lineSpacing(4) + .padding(8) + .frame(maxWidth: .infinity) + .foregroundColor(.secondary) + .background { + RoundedRectangle(cornerRadius: 12) + .stroke(.separator, lineWidth: 1) + } } } } diff --git a/Packages/Settings/Sources/SettingsUI/Internal/GitHubSettings/GitHubSettingsView.swift b/Packages/Settings/Sources/SettingsUI/Internal/GitHubSettings/GitHubSettingsView.swift index 4b71de5..fd1dfd0 100644 --- a/Packages/Settings/Sources/SettingsUI/Internal/GitHubSettings/GitHubSettingsView.swift +++ b/Packages/Settings/Sources/SettingsUI/Internal/GitHubSettings/GitHubSettingsView.swift @@ -12,22 +12,42 @@ struct GitHubSettingsView: View { var body: some View { Form { - TextField(L10n.Settings.Github.organizationName, text: $viewModel.organizationName) - .disabled(!viewModel.isSettingsEnabled) - TextField(L10n.Settings.Github.appId, text: $viewModel.appId) - .disabled(!viewModel.isSettingsEnabled) - GitHubPrivateKeyPicker(filename: $viewModel.privateKeyName, isEnabled: viewModel.isSettingsEnabled) { fileURL in - Task { - await viewModel.storePrivateKey(at: fileURL) + Section { + Picker(L10n.Settings.Github.runnerScope, selection: $viewModel.runnerScope) { + ForEach(RunnerScope.allCases, id: \.self) { scope in + Text(scope.rawValue.capitalized) + } + } + .pickerStyle(.segmented) + + switch viewModel.runnerScope { + case .organization: + TextField(L10n.Settings.Github.organizationName, text: $viewModel.organizationName) + .disabled(!viewModel.isSettingsEnabled) + case .repo: + TextField(L10n.Settings.Github.ownerName, text: $viewModel.ownerName) + .disabled(!viewModel.isSettingsEnabled) + TextField(L10n.Settings.Github.repositoryName, text: $viewModel.repositoryName) + .disabled(!viewModel.isSettingsEnabled) } } - Button { - viewModel.openCreateApp() - } label: { - Text(L10n.Settings.Github.createApp) + Section { + TextField(L10n.Settings.Github.appId, text: $viewModel.appId) + .disabled(!viewModel.isSettingsEnabled) + GitHubPrivateKeyPicker(filename: $viewModel.privateKeyName, isEnabled: viewModel.isSettingsEnabled) { fileURL in + Task { + await viewModel.storePrivateKey(at: fileURL) + } + } + Button { + viewModel.openCreateApp() + } label: { + Text(L10n.Settings.Github.createApp) + } + .frame(maxWidth: .infinity, alignment: .trailing) } } - .padding() + .formStyle(.grouped) .task { await viewModel.loadCredentials() } diff --git a/Packages/Settings/Sources/SettingsUI/Internal/GitHubSettings/GitHubSettingsViewModel.swift b/Packages/Settings/Sources/SettingsUI/Internal/GitHubSettings/GitHubSettingsViewModel.swift index 35c83d6..9d93e3b 100644 --- a/Packages/Settings/Sources/SettingsUI/Internal/GitHubSettings/GitHubSettingsViewModel.swift +++ b/Packages/Settings/Sources/SettingsUI/Internal/GitHubSettings/GitHubSettingsViewModel.swift @@ -1,6 +1,7 @@ import AppKit import Combine import GitHubCredentialsStore +import GitHubService import SettingsStore import SwiftUI @@ -8,15 +9,18 @@ import SwiftUI final class GitHubSettingsViewModel: ObservableObject { let settingsStore: SettingsStore @Published var organizationName: String = "" + @Published var repositoryName: String = "" + @Published var ownerName: String = "" @Published var appId: String = "" @Published var privateKeyName = "" + @Published var runnerScope: GitHubRunnerScope @Published private(set) var isSettingsEnabled = true private let credentialsStore: GitHubCredentialsStore private var cancellables: Set = [] private var createAppURL: URL { var url = URL(string: "https://github.com")! - if !organizationName.isEmpty { + if !organizationName.isEmpty, case .organization = runnerScope { url = url.appending(path: "/organizations/\(organizationName)") } return url.appending(path: "/settings/apps") @@ -25,17 +29,36 @@ final class GitHubSettingsViewModel: ObservableObject { init(settingsStore: SettingsStore, credentialsStore: GitHubCredentialsStore, isSettingsEnabled: AnyPublisher) { self.settingsStore = settingsStore self.credentialsStore = credentialsStore + self.runnerScope = settingsStore.githubRunnerScope isSettingsEnabled.assign(to: \.isSettingsEnabled, on: self).store(in: &cancellables) - $organizationName.debounce(for: 0.5, scheduler: DispatchQueue.main).nilIfEmpty().dropFirst().sink { [weak self] organizationName in - Task { - await self?.credentialsStore.setOrganizationName(organizationName) - } - }.store(in: &cancellables) $appId.debounce(for: 0.5, scheduler: DispatchQueue.main).nilIfEmpty().dropFirst().sink { [weak self] appId in Task { await self?.credentialsStore.setAppID(appId) } }.store(in: &cancellables) + $runnerScope + .combineLatest( + $organizationName.nilIfEmpty(), + $ownerName.nilIfEmpty(), + $repositoryName.nilIfEmpty() + ) + .debounce(for: 0.5, scheduler: DispatchQueue.main) + .dropFirst() + .sink { [weak self] runnerScope, organizationName, ownerName, repositoryName in + self?.settingsStore.githubRunnerScope = runnerScope + switch runnerScope { + case .organization: + Task { + await self?.credentialsStore.setOrganizationName(organizationName) + await self?.credentialsStore.setRepository(nil, withOwner: nil) + } + case .repo: + Task { + await self?.credentialsStore.setOrganizationName(nil) + await self?.credentialsStore.setRepository(repositoryName, withOwner: ownerName) + } + } + }.store(in: &cancellables) } func openCreateApp() { @@ -58,12 +81,16 @@ final class GitHubSettingsViewModel: ObservableObject { func loadCredentials() async { organizationName = await credentialsStore.organizationName ?? "" + repositoryName = await credentialsStore.repositoryName ?? "" + ownerName = await credentialsStore.ownerName ?? "" appId = await credentialsStore.appId ?? "" let privateKey = await credentialsStore.privateKey privateKeyName = privateKey != nil ? settingsStore.gitHubPrivateKeyName ?? "" : "" } } +typealias RunnerScope = GitHubRunnerScope + private extension Publisher where Output == String { func nilIfEmpty() -> AnyPublisher { map { !$0.isEmpty ? $0 : nil }.eraseToAnyPublisher() diff --git a/Packages/Settings/Sources/SettingsUI/Internal/L10n.swift b/Packages/Settings/Sources/SettingsUI/Internal/L10n.swift index 47b7c23..bbb63d4 100644 --- a/Packages/Settings/Sources/SettingsUI/Internal/L10n.swift +++ b/Packages/Settings/Sources/SettingsUI/Internal/L10n.swift @@ -40,8 +40,14 @@ internal enum L10n { internal static let createApp = L10n.tr("Localizable", "settings.github.create_app", fallback: "Create GitHub App") /// Organization Name internal static let organizationName = L10n.tr("Localizable", "settings.github.organization_name", fallback: "Organization Name") + /// Owner name + internal static let ownerName = L10n.tr("Localizable", "settings.github.owner_name", fallback: "Owner name") /// Private Key (PEM) internal static let privateKey = L10n.tr("Localizable", "settings.github.private_key", fallback: "Private Key (PEM)") + /// Repository name + internal static let repositoryName = L10n.tr("Localizable", "settings.github.repository_name", fallback: "Repository name") + /// Runner scope + internal static let runnerScope = L10n.tr("Localizable", "settings.github.runner_scope", fallback: "Runner scope") internal enum PrivateKey { /// Select a private key (PEM) internal static let placeholder = L10n.tr("Localizable", "settings.github.private_key.placeholder", fallback: "Select a private key (PEM)") diff --git a/Packages/Settings/Sources/SettingsUI/Supporting files/Localizable.strings b/Packages/Settings/Sources/SettingsUI/Supporting files/Localizable.strings index 4839bf2..cc9ca92 100644 --- a/Packages/Settings/Sources/SettingsUI/Supporting files/Localizable.strings +++ b/Packages/Settings/Sources/SettingsUI/Supporting files/Localizable.strings @@ -20,6 +20,9 @@ "settings.github" = "GitHub"; "settings.github.organization_name" = "Organization Name"; +"settings.github.runner_scope" = "Runner scope"; +"settings.github.owner_name" = "Owner name"; +"settings.github.repository_name" = "Repository name"; "settings.github.app_id" = "App ID"; "settings.github.private_key" = "Private Key (PEM)"; "settings.github.private_key.select_file" = "Select File"; diff --git a/Packages/VirtualMachine/Sources/VirtualMachineResourcesServiceEphemeral/VirtualMachineResourcesServiceEphemeral.swift b/Packages/VirtualMachine/Sources/VirtualMachineResourcesServiceEphemeral/VirtualMachineResourcesServiceEphemeral.swift index ff5c3cd..2c493b2 100644 --- a/Packages/VirtualMachine/Sources/VirtualMachineResourcesServiceEphemeral/VirtualMachineResourcesServiceEphemeral.swift +++ b/Packages/VirtualMachine/Sources/VirtualMachineResourcesServiceEphemeral/VirtualMachineResourcesServiceEphemeral.swift @@ -38,6 +38,7 @@ public struct VirtualMachineResourcesServiceEphemeral: VirtualMachineResourcesSe private let fileSystem: FileSystem private let gitHubService: GitHubService private let gitHubCredentialsStore: GitHubCredentialsStore + private let runnerScope: GitHubRunnerScope private let resourcesCopier: VirtualMachineResourcesCopier private let editorResourcesDirectoryURL: URL private let virtualMachineName: String @@ -48,6 +49,7 @@ public struct VirtualMachineResourcesServiceEphemeral: VirtualMachineResourcesSe fileSystem: FileSystem, gitHubService: GitHubService, gitHubCredentialsStore: GitHubCredentialsStore, + runnerScope: GitHubRunnerScope, resourcesCopier: VirtualMachineResourcesCopier, editorResourcesDirectoryURL: URL, virtualMachineName: String, @@ -57,6 +59,7 @@ public struct VirtualMachineResourcesServiceEphemeral: VirtualMachineResourcesSe self.fileSystem = fileSystem self.gitHubService = gitHubService self.gitHubCredentialsStore = gitHubCredentialsStore + self.runnerScope = runnerScope self.resourcesCopier = resourcesCopier self.editorResourcesDirectoryURL = editorResourcesDirectoryURL self.virtualMachineName = virtualMachineName @@ -66,9 +69,9 @@ public struct VirtualMachineResourcesServiceEphemeral: VirtualMachineResourcesSe public func createResourcesIfNeeded() async throws { let runnerURL = try await getRunnerURL() - let appAccessToken = try await gitHubService.getAppAccessToken() - let runnerToken = try await gitHubService.getRunnerRegistrationToken(with: appAccessToken) - let runnerDownloadURL = try await gitHubService.getRunnerDownloadURL(with: appAccessToken) + let appAccessToken = try await gitHubService.getAppAccessToken(runnerScope: runnerScope) + let runnerToken = try await gitHubService.getRunnerRegistrationToken(with: appAccessToken, runnerScope: runnerScope) + let runnerDownloadURL = try await gitHubService.getRunnerDownloadURL(with: appAccessToken, runnerScope: runnerScope) try fileSystem.createDirectoryIfNeeded(at: directoryURL) if fileSystem.itemExists(at: directoryURL) { try resourcesCopier.copyResources(from: editorResourcesDirectoryURL, to: directoryURL) @@ -99,11 +102,23 @@ public struct VirtualMachineResourcesServiceEphemeral: VirtualMachineResourcesSe private extension VirtualMachineResourcesServiceEphemeral { private func getRunnerURL() async throws -> URL { - let organizationName = try await getOrganizationName() - guard let runnerURL = URL(string: "https://github.com/" + organizationName) else { - throw VirtualMachineResourcesServiceEphemeralError.invalidRunnerURL + switch runnerScope { + case .organization: + let organizationName = try await getOrganizationName() + guard let runnerURL = URL(string: "https://github.com/" + organizationName) else { + throw VirtualMachineResourcesServiceEphemeralError.invalidRunnerURL + } + return runnerURL + case .repo: + guard + let ownerName = await gitHubCredentialsStore.ownerName, + let repositoryName = await gitHubCredentialsStore.repositoryName, + let runnerURL = URL(string: "https://github.com/\(ownerName)/\(repositoryName)") + else { + throw VirtualMachineResourcesServiceEphemeralError.invalidRunnerURL + } + return runnerURL } - return runnerURL } private func getOrganizationName() async throws -> String { diff --git a/Tartelet/Sources/Composition/EphemeralVirtualMachineResourcesServiceFactory.swift b/Tartelet/Sources/Composition/EphemeralVirtualMachineResourcesServiceFactory.swift index 25a2f26..6bc7fe5 100644 --- a/Tartelet/Sources/Composition/EphemeralVirtualMachineResourcesServiceFactory.swift +++ b/Tartelet/Sources/Composition/EphemeralVirtualMachineResourcesServiceFactory.swift @@ -20,6 +20,7 @@ struct EphemeralVirtualMachineResourcesServiceFactory: VirtualMachineResourcesSe fileSystem: fileSystem, gitHubService: gitHubService, gitHubCredentialsStore: gitHubCredentialsStore, + runnerScope: settingsStore.githubRunnerScope, resourcesCopier: resourcesCopier, editorResourcesDirectoryURL: editorResourcesDirectoryURL, virtualMachineName: virtualMachineName,