From 449c6a5ef566f51f17204e03454e661cf3a0cdcf Mon Sep 17 00:00:00 2001 From: Marco Cancellieri Date: Mon, 17 Jul 2023 14:47:43 +0200 Subject: [PATCH] Add support for repository runners (#34) --- Cilicon/Config/GitHubProvisionerConfig.swift | 14 ++++- .../GitHubActionsProvisioner.swift | 8 +-- .../GitHub Actions/GitHubService.swift | 56 +++++++++++++++---- 3 files changed, 58 insertions(+), 20 deletions(-) diff --git a/Cilicon/Config/GitHubProvisionerConfig.swift b/Cilicon/Config/GitHubProvisionerConfig.swift index 38b669c..7de39d3 100644 --- a/Cilicon/Config/GitHubProvisionerConfig.swift +++ b/Cilicon/Config/GitHubProvisionerConfig.swift @@ -7,6 +7,8 @@ struct GitHubProvisionerConfig: Decodable { let appId: Int /// The organization slug let organization: String + /// The repository name + let repository: String? /// Path to the private key `.pem` file downloaded from the Github App page let privateKeyPath: String /// Extra labels to add to the runner @@ -16,7 +18,7 @@ struct GitHubProvisionerConfig: Decodable { let runnerGroup: String? - let organizationURL: URL + let url: URL enum CodingKeys: CodingKey { case apiURL @@ -25,8 +27,9 @@ struct GitHubProvisionerConfig: Decodable { case privateKeyPath case extraLabels case runnerGroup - case organizationURL + case url case downloadLatest + case repository } init(from decoder: Decoder) throws { @@ -34,10 +37,15 @@ struct GitHubProvisionerConfig: Decodable { self.apiURL = try container.decodeIfPresent(URL.self, forKey: .apiURL) self.appId = try container.decode(Int.self, forKey: .appId) self.organization = try container.decode(String.self, forKey: .organization) + self.repository = try container.decodeIfPresent(String.self, forKey: .repository) self.privateKeyPath = try (container.decode(String.self, forKey: .privateKeyPath) as NSString).standardizingPath self.extraLabels = try container.decodeIfPresent([String].self, forKey: .extraLabels) self.runnerGroup = try container.decodeIfPresent(String.self, forKey: .runnerGroup) - self.organizationURL = try container.decodeIfPresent(URL.self, forKey: .organizationURL) ?? URL(string: "https://github.com/\(organization)")! + var fallbackURL = URL(string: "https://github.com/\(organization)")! + if let repo = self.repository { + fallbackURL.appendPathComponent(repo) + } + self.url = try container.decodeIfPresent(URL.self, forKey: .url) ?? fallbackURL self.downloadLatest = try container.decodeIfPresent(Bool.self, forKey: .downloadLatest) ?? true } } diff --git a/Cilicon/Provisioner/GitHub Actions/GitHubActionsProvisioner.swift b/Cilicon/Provisioner/GitHub Actions/GitHubActionsProvisioner.swift index 51dc4d5..825faa2 100644 --- a/Cilicon/Provisioner/GitHub Actions/GitHubActionsProvisioner.swift +++ b/Cilicon/Provisioner/GitHub Actions/GitHubActionsProvisioner.swift @@ -19,12 +19,8 @@ class GitHubActionsProvisioner: Provisioner { } func provision(bundle: VMBundle, sshClient: SSHClient) async throws { - let org = gitHubConfig.organization - let appId = gitHubConfig.appId await SSHLogger.shared.log(string: "[1;35mFetching Github Runner Token[0m\n") - guard let installation = try await service.getInstallations().first(where: { $0.account.login == gitHubConfig.organization }) else { - throw GitHubActionsProvisionerError.githubAppNotInstalled(appID: appId, org: org) - } + let installation = try await service.getInstallation() let authToken = try await service.getInstallationToken(installation: installation) let token = try await service.createRunnerToken(token: authToken.token) @@ -47,7 +43,7 @@ class GitHubActionsProvisioner: Provisioner { var configCommandComponents = [ "~/actions-runner/config.sh", - "--url \(gitHubConfig.organizationURL)", + "--url \(gitHubConfig.url)", "--name '\(runnerName)'", "--token \(token.token)", "--replace", diff --git a/Cilicon/Provisioner/GitHub Actions/GitHubService.swift b/Cilicon/Provisioner/GitHub Actions/GitHubService.swift index dbfb57a..da4ffc3 100644 --- a/Cilicon/Provisioner/GitHub Actions/GitHubService.swift +++ b/Cilicon/Provisioner/GitHub Actions/GitHubService.swift @@ -24,19 +24,44 @@ class GitHubService { self.urlSession = URLSession(configuration: config) } - func installationsURL() -> URL { + private var orgInstallationURL: URL { baseURL - .appendingPathComponent("app") - .appendingPathComponent("installations") + .appendingPathComponent("orgs") + .appendingPathComponent(config.organization) + .appendingPathComponent("installation") + } + + private func repoInstallationURL(repo: String) -> URL { + baseURL + .appendingPathComponent("repos") + .appendingPathComponent(config.organization) + .appendingPathComponent(repo) + .appendingPathComponent("installation") + } + + var installationURL: URL { + if let repo = config.repository { + return repoInstallationURL(repo: repo) + } + return orgInstallationURL } func installationFetchURL(installationId: Int) -> URL { - installationsURL() + baseURL + .appendingPathComponent("app") + .appendingPathComponent("installations") .appendingPathComponent(String(installationId)) .appendingPathComponent("access_tokens") } - func actionsURL() -> URL { + var actionsURL: URL { + if let repo = config.repository { + return repoActionsURL(repo: repo) + } + return orgActionsURL + } + + private var orgActionsURL: URL { baseURL .appendingPathComponent("orgs") .appendingPathComponent(config.organization) @@ -44,25 +69,34 @@ class GitHubService { .appendingPathComponent("runners") } + private func repoActionsURL(repo: String) -> URL { + baseURL + .appendingPathComponent("repos") + .appendingPathComponent(config.organization) + .appendingPathComponent(repo) + .appendingPathComponent("actions") + .appendingPathComponent("runners") + } + func runnerTokenURL() -> URL { - actionsURL() + actionsURL .appendingPathComponent("registration-token") } func runnerDownloadsURL() -> URL { - actionsURL() + actionsURL .appendingPathComponent("downloads") } - func getInstallations() async throws -> [Installation] { + func getInstallation() async throws -> Installation { let jwtToken = try GitHubAppAuthHelper.generateJWTToken(pemPath: config.privateKeyPath, appId: config.appId) let (data, _) = try await authenticatedRequest( - url: installationsURL(), + url: installationURL, method: "GET", token: jwtToken ) - let installations = try decoder.decode([Installation].self, from: data) - return installations + let installation = try decoder.decode(Installation.self, from: data) + return installation } func getInstallationToken(installation: Installation) async throws -> AccessToken {