From 1948845861b0950576da5dd32093f6773e198f5d Mon Sep 17 00:00:00 2001 From: Andrew Ochsner Date: Fri, 2 Jun 2023 10:04:07 -0500 Subject: [PATCH] add github:environment:create and github:deployKey:create Signed-off-by: Andrew Ochsner --- .changeset/strange-dolls-decide.md | 5 + docs/integrations/github/github-apps.md | 1 + plugins/scaffolder-backend/api-report.md | 44 +++ .../actions/builtin/createBuiltinActions.ts | 8 + .../builtin/github/githubDeployKey.test.ts | 112 ++++++++ .../actions/builtin/github/githubDeployKey.ts | 157 +++++++++++ .../builtin/github/githubEnvironment.test.ts | 258 ++++++++++++++++++ .../builtin/github/githubEnvironment.ts | 200 ++++++++++++++ .../actions/builtin/github/helpers.ts | 4 +- .../actions/builtin/github/index.ts | 2 + 10 files changed, 789 insertions(+), 2 deletions(-) create mode 100644 .changeset/strange-dolls-decide.md create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubDeployKey.test.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubDeployKey.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubEnvironment.test.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubEnvironment.ts diff --git a/.changeset/strange-dolls-decide.md b/.changeset/strange-dolls-decide.md new file mode 100644 index 0000000000..97dc99d2c9 --- /dev/null +++ b/.changeset/strange-dolls-decide.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': minor +--- + +Added `github:deployKey:create` and `github:environment:create` scaffolder actions. You will need to add `read/write` permissions to your GITHUB_TOKEN and/or Github Backstage App for Repository `Administration` (for deploy key functionality) and `Environments` (for Environment functionality) diff --git a/docs/integrations/github/github-apps.md b/docs/integrations/github/github-apps.md index 3cad69f56d..cc84704799 100644 --- a/docs/integrations/github/github-apps.md +++ b/docs/integrations/github/github-apps.md @@ -138,3 +138,4 @@ integration: - `Commit statuses`: `Read-only` - `Variables`: `Read & write` (if templates include GitHub Action Repository Variables) - `Secrets`: `Read & write` (if templates include GitHub Action Repository Secrets) + - `Environments`: `Read & write` (if templates include GitHub Environments) diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 5e18ad15bf..86fadbacb0 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -211,6 +211,50 @@ export function createGithubActionsDispatchAction(options: { JsonObject >; +// @public +export function createGithubDeployKeyAction(options: { + integrations: ScmIntegrationRegistry; +}): TemplateAction_2< + { + repoUrl: string; + publicKey: string; + privateKey: string; + deployKeyName: string; + privateKeySecretName?: string | undefined; + token?: string | undefined; + }, + JsonObject +>; + +// @public +export function createGithubEnvironmentAction(options: { + integrations: ScmIntegrationRegistry; +}): TemplateAction_2< + { + repoUrl: string; + name: string; + deploymentBranchPolicy?: + | { + protected_branches: boolean; + custom_branch_policies: boolean; + } + | undefined; + customBranchPolicyNames?: string[] | undefined; + environmentVariables?: + | { + [key: string]: string; + } + | undefined; + secrets?: + | { + [key: string]: string; + } + | undefined; + token?: string | undefined; + }, + JsonObject +>; + // @public export function createGithubIssuesLabelAction(options: { integrations: ScmIntegrationRegistry; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index 747b5cdaed..f5ea23f26f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -42,6 +42,8 @@ import { } from './filesystem'; import { createGithubActionsDispatchAction, + createGithubDeployKeyAction, + createGithubEnvironmentAction, createGithubIssuesLabelAction, createGithubRepoCreateAction, createGithubRepoPushAction, @@ -194,6 +196,12 @@ export const createBuiltinActions = ( config, githubCredentialsProvider, }), + createGithubEnvironmentAction({ + integrations, + }), + createGithubDeployKeyAction({ + integrations, + }), ]; return actions as TemplateAction[]; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubDeployKey.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubDeployKey.test.ts new file mode 100644 index 0000000000..4df9294ff5 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubDeployKey.test.ts @@ -0,0 +1,112 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PassThrough } from 'stream'; +import { createGithubDeployKeyAction } from './githubDeployKey'; +import { getVoidLogger } from '@backstage/backend-common'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; +import { ConfigReader } from '@backstage/config'; +import { ScmIntegrations } from '@backstage/integration'; + +const mockOctokit = { + rest: { + actions: { + getRepoPublicKey: jest.fn(), + createOrUpdateRepoSecret: jest.fn(), + }, + repos: { + createDeployKey: jest.fn(), + }, + }, +}; +jest.mock('octokit', () => ({ + Octokit: class { + constructor() { + return mockOctokit; + } + }, +})); + +const publicKey = '2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvvcCU='; + +describe('github:deployKey:create', () => { + const config = new ConfigReader({ + integrations: { + github: [ + { host: 'github.com', token: 'tokenlols' }, + { host: 'ghe.github.com' }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + let action: TemplateAction; + + const mockContext = { + input: { + repoUrl: 'github.com?repo=repository&owner=owner', + publicKey: 'pubkey', + privateKey: 'privkey', + deployKeyName: 'Push Tags', + }, + workspacePath: 'lol', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + beforeEach(() => { + jest.resetAllMocks(); + + action = createGithubDeployKeyAction({ + integrations, + }); + }); + + it('should work happy path', async () => { + mockOctokit.rest.actions.getRepoPublicKey.mockResolvedValue({ + data: { + key: publicKey, + key_id: 'keyid', + }, + }); + + await action.handler(mockContext); + + expect(mockOctokit.rest.repos.createDeployKey).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + title: 'Push Tags', + key: 'pubkey', + }); + + expect( + mockOctokit.rest.actions.createOrUpdateRepoSecret, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + secret_name: 'PUSH_TAGS_PRIVATE_KEY', + key_id: 'keyid', + encrypted_value: expect.any(String), + }); + + expect(mockContext.output).toHaveBeenCalledWith( + 'privateKeySecretName', + 'PUSH_TAGS_PRIVATE_KEY', + ); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubDeployKey.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubDeployKey.ts new file mode 100644 index 0000000000..6ad646a89c --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubDeployKey.ts @@ -0,0 +1,157 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InputError } from '@backstage/errors'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; +import { ScmIntegrationRegistry } from '@backstage/integration'; +import { parseRepoUrl } from '../publish/util'; +import { getOctokitOptions } from './helpers'; +import { Octokit } from 'octokit'; +import Sodium from 'libsodium-wrappers'; + +/** + * Creates an `github:deployKey:create` Scaffolder action that creates a Deploy Key + * + * @public + */ +export function createGithubDeployKeyAction(options: { + integrations: ScmIntegrationRegistry; +}) { + const { integrations } = options; + // For more information on how to define custom actions, see + // https://backstage.io/docs/features/software-templates/writing-custom-actions + return createTemplateAction<{ + repoUrl: string; + publicKey: string; + privateKey: string; + deployKeyName: string; + privateKeySecretName?: string; + token?: string; + }>({ + id: 'github:deployKey:create', + description: 'Creates and stores Deploy Keys', + schema: { + input: { + type: 'object', + required: ['repoUrl', 'publicKey', 'privateKey', 'deployKeyName'], + properties: { + repoUrl: { + title: 'Repository Location', + description: `Accepts the format 'github.com?repo=reponame&owner=owner' where 'reponame' is the new repository name and 'owner' is an organization or username`, + type: 'string', + }, + publicKey: { + title: 'SSH Public Key', + description: `Generated from ssh-keygen. Begins with 'ssh-rsa', 'ecdsa-sha2-nistp256', 'ecdsa-sha2-nistp384', 'ecdsa-sha2-nistp521', 'ssh-ed25519', 'sk-ecdsa-sha2-nistp256@openssh.com', or 'sk-ssh-ed25519@openssh.com'.`, + type: 'string', + }, + privateKey: { + title: 'SSH Private Key', + description: `SSH Private Key generated from ssh-keygen`, + type: 'string', + }, + deployKeyName: { + title: 'Deploy Key Name', + description: `Name of the Deploy Key`, + type: 'string', + }, + privateKeySecretName: { + title: 'Private Key GitHub Secret Name', + description: `Name of the GitHub Secret to store the private key related to the Deploy Key. Defaults to: 'KEY_NAME_PRIVATE_KEY' where 'KEY_NAME' is the name of the Deploy Key`, + type: 'string', + }, + token: { + title: 'Authentication Token', + type: 'string', + description: 'The token to use for authorization to GitHub', + }, + }, + }, + output: { + type: 'object', + properties: { + privateKeySecretName: { + title: 'The GitHub Action Repo Secret Name for the Private Key', + type: 'string', + }, + }, + }, + }, + async handler(ctx) { + const { + repoUrl, + publicKey, + privateKey, + deployKeyName, + privateKeySecretName = `${deployKeyName + .split(' ') + .join('_') + .toLocaleUpperCase('en-US')}_PRIVATE_KEY`, + token: providedToken, + } = ctx.input; + + const octokitOptions = await getOctokitOptions({ + integrations, + token: providedToken, + repoUrl: repoUrl, + }); + + const { owner, repo } = parseRepoUrl(repoUrl, integrations); + + if (!owner) { + throw new InputError(`No owner provided for repo ${repoUrl}`); + } + + const client = new Octokit(octokitOptions); + + await client.rest.repos.createDeployKey({ + owner: owner, + repo: repo, + title: deployKeyName, + key: publicKey, + }); + const publicKeyResponse = await client.rest.actions.getRepoPublicKey({ + owner: owner, + repo: repo, + }); + + await Sodium.ready; + const binaryKey = Sodium.from_base64( + publicKeyResponse.data.key, + Sodium.base64_variants.ORIGINAL, + ); + const binarySecret = Sodium.from_string(privateKey); + const encryptedBinarySecret = Sodium.crypto_box_seal( + binarySecret, + binaryKey, + ); + const encryptedBase64Secret = Sodium.to_base64( + encryptedBinarySecret, + Sodium.base64_variants.ORIGINAL, + ); + + await client.rest.actions.createOrUpdateRepoSecret({ + owner: owner, + repo: repo, + secret_name: privateKeySecretName, + encrypted_value: encryptedBase64Secret, + key_id: publicKeyResponse.data.key_id, + }); + + ctx.output('privateKeySecretName', privateKeySecretName); + }, + }); +} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubEnvironment.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubEnvironment.test.ts new file mode 100644 index 0000000000..d158bc122e --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubEnvironment.test.ts @@ -0,0 +1,258 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PassThrough } from 'stream'; +import { createGithubEnvironmentAction } from './githubEnvironment'; +import { getVoidLogger } from '@backstage/backend-common'; +import { TemplateAction } from '@backstage/plugin-scaffolder-node'; +import { ConfigReader } from '@backstage/config'; +import { ScmIntegrations } from '@backstage/integration'; + +const mockOctokit = { + rest: { + actions: { + getRepoPublicKey: jest.fn(), + createEnvironmentVariable: jest.fn(), + createOrUpdateEnvironmentSecret: jest.fn(), + }, + repos: { + createDeploymentBranchPolicy: jest.fn(), + createOrUpdateEnvironment: jest.fn(), + get: jest.fn(), + }, + }, +}; +jest.mock('octokit', () => ({ + Octokit: class { + constructor() { + return mockOctokit; + } + }, +})); + +const publicKey = '2Sg8iYjAxxmI2LvUXpJjkYrMxURPc8r+dB7TJyvvcCU='; + +describe('github:environment:create', () => { + const config = new ConfigReader({ + integrations: { + github: [ + { host: 'github.com', token: 'tokenlols' }, + { host: 'ghe.github.com' }, + ], + }, + }); + + const integrations = ScmIntegrations.fromConfig(config); + let action: TemplateAction; + + const mockContext = { + input: { + repoUrl: 'github.com?repo=repository&owner=owner', + name: 'envname', + }, + workspacePath: 'lol', + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + beforeEach(() => { + mockOctokit.rest.actions.getRepoPublicKey.mockResolvedValue({ + data: { + key: publicKey, + key_id: 'keyid', + }, + }); + mockOctokit.rest.repos.get.mockResolvedValue({ + data: { + id: 'repoid', + }, + }); + + action = createGithubEnvironmentAction({ + integrations, + }); + }); + + afterEach(jest.resetAllMocks); + + it('should work happy path', async () => { + await action.handler(mockContext); + + expect( + mockOctokit.rest.repos.createOrUpdateEnvironment, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + deployment_branch_policy: null, + }); + }); + + it('should work specify deploymentBranchPolicy protected', async () => { + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + deploymentBranchPolicy: { + protected_branches: true, + custom_branch_policies: false, + }, + }, + }); + + expect( + mockOctokit.rest.repos.createOrUpdateEnvironment, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + deployment_branch_policy: { + protected_branches: true, + custom_branch_policies: false, + }, + }); + }); + it('should work specify deploymentBranchPolicy custom', async () => { + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + deploymentBranchPolicy: { + protected_branches: false, + custom_branch_policies: true, + }, + customBranchPolicyNames: ['main', '*.*.*'], + }, + }); + + expect( + mockOctokit.rest.repos.createOrUpdateEnvironment, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + deployment_branch_policy: { + protected_branches: false, + custom_branch_policies: true, + }, + }); + + expect( + mockOctokit.rest.repos.createDeploymentBranchPolicy, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + name: 'main', + }); + expect( + mockOctokit.rest.repos.createDeploymentBranchPolicy, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + name: '*.*.*', + }); + }); + + it('should work specify environment variables', async () => { + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + environmentVariables: { + key1: 'val1', + key2: 'val2', + }, + }, + }); + + expect( + mockOctokit.rest.repos.createOrUpdateEnvironment, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + deployment_branch_policy: null, + }); + + expect( + mockOctokit.rest.actions.createEnvironmentVariable, + ).toHaveBeenCalledTimes(2); + expect( + mockOctokit.rest.actions.createEnvironmentVariable, + ).toHaveBeenCalledWith({ + repository_id: 'repoid', + environment_name: 'envname', + name: 'key1', + value: 'val1', + }); + expect( + mockOctokit.rest.actions.createEnvironmentVariable, + ).toHaveBeenCalledWith({ + repository_id: 'repoid', + environment_name: 'envname', + name: 'key2', + value: 'val2', + }); + }); + + it('should work specify secrets', async () => { + await action.handler({ + ...mockContext, + input: { + ...mockContext.input, + secrets: { + key1: 'val1', + key2: 'val2', + }, + }, + }); + + expect( + mockOctokit.rest.repos.createOrUpdateEnvironment, + ).toHaveBeenCalledWith({ + owner: 'owner', + repo: 'repository', + environment_name: 'envname', + deployment_branch_policy: null, + }); + + expect( + mockOctokit.rest.actions.createOrUpdateEnvironmentSecret, + ).toHaveBeenCalledTimes(2); + expect( + mockOctokit.rest.actions.createOrUpdateEnvironmentSecret, + ).toHaveBeenCalledWith({ + repository_id: 'repoid', + environment_name: 'envname', + secret_name: 'key1', + key_id: 'keyid', + encrypted_value: expect.any(String), + }); + expect( + mockOctokit.rest.actions.createOrUpdateEnvironmentSecret, + ).toHaveBeenCalledWith({ + repository_id: 'repoid', + environment_name: 'envname', + secret_name: 'key2', + key_id: 'keyid', + encrypted_value: expect.any(String), + }); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubEnvironment.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubEnvironment.ts new file mode 100644 index 0000000000..04b33ed259 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/githubEnvironment.ts @@ -0,0 +1,200 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { InputError } from '@backstage/errors'; +import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; +import { ScmIntegrationRegistry } from '@backstage/integration'; +import { parseRepoUrl } from '../publish/util'; +import { getOctokitOptions } from './helpers'; +import { Octokit } from 'octokit'; +import Sodium from 'libsodium-wrappers'; + +/** + * Creates an `github:environment:create` Scaffolder action that creates a Github Environment. + * + * @public + */ +export function createGithubEnvironmentAction(options: { + integrations: ScmIntegrationRegistry; +}) { + const { integrations } = options; + // For more information on how to define custom actions, see + // https://backstage.io/docs/features/software-templates/writing-custom-actions + return createTemplateAction<{ + repoUrl: string; + name: string; + deploymentBranchPolicy?: { + protected_branches: boolean; + custom_branch_policies: boolean; + }; + customBranchPolicyNames?: string[]; + environmentVariables?: { [key: string]: string }; + secrets?: { [key: string]: string }; + token?: string; + }>({ + id: 'github:environment:create', + description: 'Creates Deployment Environments', + schema: { + input: { + type: 'object', + required: ['repoUrl', 'name'], + properties: { + repoUrl: { + title: 'Repository Location', + description: `Accepts the format 'github.com?repo=reponame&owner=owner' where 'reponame' is the new repository name and 'owner' is an organization or username`, + type: 'string', + }, + name: { + title: 'Environment Name', + description: `Name of the deployment environment to create`, + type: 'string', + }, + deploymentBranchPolicy: { + title: 'Deployment Branch Policy', + description: `The type of deployment branch policy for this environment. To allow all branches to deploy, set to null.`, + type: 'object', + required: ['protected_branches', 'custom_branch_policies'], + properties: { + protected_branches: { + title: 'Protected Branches', + description: `Whether only branches with branch protection rules can deploy to this environment. If protected_branches is true, custom_branch_policies must be false; if protected_branches is false, custom_branch_policies must be true.`, + type: 'boolean', + }, + custom_branch_policies: { + title: 'Custom Branch Policies', + description: `Whether only branches that match the specified name patterns can deploy to this environment. If custom_branch_policies is true, protected_branches must be false; if custom_branch_policies is false, protected_branches must be true.`, + type: 'boolean', + }, + }, + }, + customBranchPolicyNames: { + title: 'Custom Branch Policy Name', + description: `The name pattern that branches must match in order to deploy to the environment. + + Wildcard characters will not match /. For example, to match branches that begin with release/ and contain an additional single slash, use release/*/*. For more information about pattern matching syntax, see the Ruby File.fnmatch documentation.`, + type: 'array', + items: { + type: 'string', + }, + }, + environmentVariables: { + title: 'Environment Variables', + description: `Environment variables attached to the deployment environment`, + type: 'object', + }, + secrets: { + title: 'Deployment Secrets', + description: `Secrets attached to the deployment environment`, + type: 'object', + }, + token: { + title: 'Authentication Token', + type: 'string', + description: 'The token to use for authorization to GitHub', + }, + }, + }, + }, + async handler(ctx) { + const { + repoUrl, + name, + deploymentBranchPolicy, + customBranchPolicyNames, + environmentVariables, + secrets, + token: providedToken, + } = ctx.input; + + const octokitOptions = await getOctokitOptions({ + integrations, + token: providedToken, + repoUrl: repoUrl, + }); + + const { owner, repo } = parseRepoUrl(repoUrl, integrations); + + if (!owner) { + throw new InputError(`No owner provided for repo ${repoUrl}`); + } + + const client = new Octokit(octokitOptions); + const repository = await client.rest.repos.get({ + owner: owner, + repo: repo, + }); + + await client.rest.repos.createOrUpdateEnvironment({ + owner: owner, + repo: repo, + environment_name: name, + deployment_branch_policy: deploymentBranchPolicy ?? null, + }); + + if (customBranchPolicyNames) { + for (const item of customBranchPolicyNames) { + await client.rest.repos.createDeploymentBranchPolicy({ + owner: owner, + repo: repo, + environment_name: name, + name: item, + }); + } + } + + for (const [key, value] of Object.entries(environmentVariables ?? {})) { + await client.rest.actions.createEnvironmentVariable({ + repository_id: repository.data.id, + environment_name: name, + name: key, + value, + }); + } + + if (secrets) { + const publicKeyResponse = await client.rest.actions.getRepoPublicKey({ + owner: owner, + repo: repo, + }); + + await Sodium.ready; + const binaryKey = Sodium.from_base64( + publicKeyResponse.data.key, + Sodium.base64_variants.ORIGINAL, + ); + for (const [key, value] of Object.entries(secrets)) { + const binarySecret = Sodium.from_string(value); + const encryptedBinarySecret = Sodium.crypto_box_seal( + binarySecret, + binaryKey, + ); + const encryptedBase64Secret = Sodium.to_base64( + encryptedBinarySecret, + Sodium.base64_variants.ORIGINAL, + ); + + await client.rest.actions.createOrUpdateEnvironmentSecret({ + repository_id: repository.data.id, + environment_name: name, + secret_name: key, + encrypted_value: encryptedBase64Secret, + key_id: publicKeyResponse.data.key_id, + }); + } + } + }, + }); +} diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts index dc1b4eb91c..edef7cec5c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/helpers.ts @@ -98,7 +98,7 @@ export async function createGithubRepoWithCollaboratorsAndTopics( client: Octokit, repo: string, owner: string, - repoVisibility: 'private' | 'internal' | 'public', + repoVisibility: 'private' | 'internal' | 'public' | undefined, description: string | undefined, homepage: string | undefined, deleteBranchOnMerge: boolean, @@ -149,7 +149,7 @@ export async function createGithubRepoWithCollaboratorsAndTopics( name: repo, org: owner, private: repoVisibility === 'private', - // @ts-ignore + // @ts-ignore https://github.com/octokit/types.ts/issues/522 visibility: repoVisibility, description: description, delete_branch_on_merge: deleteBranchOnMerge, diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/index.ts index 6746aef37e..ecc8ce59f2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/github/index.ts @@ -19,3 +19,5 @@ export { createGithubIssuesLabelAction } from './githubIssuesLabel'; export { createGithubRepoCreateAction } from './githubRepoCreate'; export { createGithubRepoPushAction } from './githubRepoPush'; export { createGithubWebhookAction } from './githubWebhook'; +export { createGithubDeployKeyAction } from './githubDeployKey'; +export { createGithubEnvironmentAction } from './githubEnvironment';