Skip to content

Commit 4305a05

Browse files
committed
wip: set and remove secret wotk with and without env
1 parent 931e6d0 commit 4305a05

File tree

7 files changed

+149
-71
lines changed

7 files changed

+149
-71
lines changed

src/commands/rm-secret/index.ts

Lines changed: 21 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,43 +1,40 @@
1+
/* eslint-disable no-await-in-loop */
12
import {Command} from '@oclif/core'
2-
import getGithubToken from '../../helpers/get-github-token'
33
import {info} from '../../helpers/logger'
4-
import {validateRepoNames} from '../../helpers/validations'
5-
import removeSecret from '../../helpers/rm-secret-helpers/rm-secrets'
4+
import {validateRepoNames, validateSecrets} from '../../helpers/validations'
5+
import secretVarsFlags from '../../helpers/set-vars-helpers/secret-vars-flags'
6+
import repositoryFactory from '../../repositories/repository-factory'
7+
import encryptSecret from '../../set-secret-helpers/encrypt-secret'
8+
import {getPublicKey} from '../../set-secret-helpers/get-public-key'
69
import RmSecretFlags from '../../helpers/rm-secret-helpers/rm-secret-flags'
710

8-
export default class RmSecret extends Command {
9-
static description = 'remove a secret from a repository or environment'
11+
export default class SetSecret extends Command {
12+
static description = 'describe the command here'
1013

1114
static examples = [
1215
`
1316
you must have a personal github token to set the first time that uses this tool
14-
$ github-automation rm-secret -r OWNER/NAME1 OWNER/NAME2 ... OWNER/NAMEn --secret-name SECRET_NAME1 SECRET_NAME2 ... SECRET_NAMEN
15-
$ github-automation rm-secret -r OWNER/NAME1 OWNER/NAME2 ... OWNER/NAMEn -n SECRET_NAME1 SECRET_NAME2 ... SECRET_NAMEN
17+
$ github-automation set-secret -r OWNER/NAME1 OWNER/NAME2 ... OWNER/NAMEn --secrets SECRET_NAME1:SECRET_VALUE_1 SECRET_NAME2:SECRET_VALUE_2 ... SECRET_NAMEN:SECRET_VALUE_N
18+
$ github-automation set-secret -r OWNER/NAME1 OWNER/NAME2 ... OWNER/NAMEn -s SECRET_NAME1:SECRET_VALUE_1 SECRET_NAME2 ... SECRET_NAMEN -x SECRETVALUE1 SECRETVALUE2:SECRET_VALUE_2 ... SECRETVALUEN:SECRET_VALUE_N
1619
`,
1720
]
1821

19-
static usage='rm-secret -r REPOS -n NAMES -e ENVIRONMENT_NAME'
22+
static usage='set-secret -r REPOS -n NAMES -x VALUES'
2023

2124
static strict = false
2225

2326
static flags = RmSecretFlags
24-
async run(): Promise<void> {
25-
const {flags} = await this.parse(RmSecret)
26-
validateRepoNames(flags.repositories)
27-
28-
const token = await getGithubToken(flags.organization)
29-
30-
const varsToSet = []
31-
for (const repo of flags.repositories) {
32-
for (const [, name] of flags['secret-name'].entries()) {
33-
varsToSet.push(removeSecret({token, owner: flags.organization, repo, name, environment_name: flags.environment}))
34-
}
35-
}
3627

37-
await Promise.all(varsToSet)
38-
for (const repo of flags.repositories) {
39-
for (const [, name] of flags['secret-name'].entries()) {
40-
this.log(info(`Removed secret ${name} in org: ${flags.organization} in repo: ${repo}`))
28+
async run(): Promise<void> {
29+
const {flags: {organization, repositories, secrets, environment}} = await this.parse(SetSecret)
30+
validateRepoNames(repositories)
31+
const octoFactory = repositoryFactory.get('octokit')
32+
for (const repo of repositories) {
33+
this.log(info(`Removing secrets in org: ${organization} in repo: ${repo}`))
34+
for (const secret of secrets) {
35+
this.log(info(`Removing secret ${secret} in org: ${organization} in repo: ${repo} ${environment ? `in environment: ${environment}` : ''}`))
36+
await octoFactory.removeSecret({owner: organization, repo, secret_name: secret, environment})
37+
this.log(info(`Updated secret ${secret} in org: ${organization} in repo: ${repo} ${environment ? `in environment: ${environment}` : ''}`))
4138
}
4239
}
4340
}

src/commands/set-secret/index.ts

Lines changed: 22 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
1+
/* eslint-disable no-await-in-loop */
12
import {Command} from '@oclif/core'
2-
import getGithubToken from '../../helpers/get-github-token'
33
import {info} from '../../helpers/logger'
4-
import updateSecret from '../../set-secret-helpers/update-secret'
5-
import encryptSecrets from '../../set-secret-helpers/encrypt-secret'
6-
import {validateEqualLengths, validateRepoNames} from '../../helpers/validations'
4+
import {validateRepoNames, validateSecrets} from '../../helpers/validations'
75
import secretVarsFlags from '../../helpers/set-vars-helpers/secret-vars-flags'
6+
import repositoryFactory from '../../repositories/repository-factory'
7+
import encryptSecret from '../../set-secret-helpers/encrypt-secret'
8+
import {getPublicKey} from '../../set-secret-helpers/get-public-key'
89

910
export default class SetSecret extends Command {
1011
static description = 'describe the command here'
1112

1213
static examples = [
1314
`
1415
you must have a personal github token to set the first time that uses this tool
15-
$ github-automation set-secret -r OWNER/NAME1 OWNER/NAME2 ... OWNER/NAMEn --secret-name SECRET_NAME1 SECRET_NAME2 ... SECRET_NAMEN --secret-value SECRETVALUE1 SECRETVALUE2 ... SECRETVALUEN
16-
$ github-automation set-secret -r OWNER/NAME1 OWNER/NAME2 ... OWNER/NAMEn -n SECRET_NAME1 SECRET_NAME2 ... SECRET_NAMEN -x SECRETVALUE1 SECRETVALUE2 ... SECRETVALUEN
16+
$ github-automation set-secret -r OWNER/NAME1 OWNER/NAME2 ... OWNER/NAMEn --secrets SECRET_NAME1:SECRET_VALUE_1 SECRET_NAME2:SECRET_VALUE_2 ... SECRET_NAMEN:SECRET_VALUE_N
17+
$ github-automation set-secret -r OWNER/NAME1 OWNER/NAME2 ... OWNER/NAMEn -s SECRET_NAME1:SECRET_VALUE_1 SECRET_NAME2 ... SECRET_NAMEN -x SECRETVALUE1 SECRETVALUE2:SECRET_VALUE_2 ... SECRETVALUEN:SECRET_VALUE_N
1718
`,
1819
]
1920

@@ -24,24 +25,21 @@ export default class SetSecret extends Command {
2425
static flags = secretVarsFlags
2526

2627
async run(): Promise<void> {
27-
const {flags} = await this.parse(SetSecret)
28-
validateEqualLengths(flags['secret-name'], flags['secret-value'])
29-
validateRepoNames(flags.repositories)
30-
31-
const token = await getGithubToken(flags.organization)
32-
const secretsToEncrypt = []
33-
for (const repo of flags.repositories) {
34-
for (const [index, secret] of flags['secret-value'].entries()) {
35-
secretsToEncrypt.push(encryptSecrets({token, value: secret, org: flags.organization, repo, name: flags['secret-name'][index], environment: flags.environment}))
36-
}
37-
}
38-
39-
const promisesEncrypted = await Promise.all(secretsToEncrypt)
40-
const updateSecretsPromises = flags.environment ? promisesEncrypted.map(encriptedData => updateSecret({...encriptedData, env: flags.environment}, token)) : promisesEncrypted.map(encriptedData => updateSecret(encriptedData, token))
41-
await Promise.all(updateSecretsPromises)
42-
for (const repo of flags.repositories) {
43-
for (const [index, secret] of flags['secret-value'].entries()) {
44-
this.log(info(`Updated secret ${flags['secret-name'][index]} with value ${secret} in org: ${flags.organization} in repo: ${repo}`))
28+
const {flags: {organization, repositories, secrets, environment}} = await this.parse(SetSecret)
29+
validateSecrets(secrets)
30+
validateRepoNames(repositories)
31+
const octoFactory = repositoryFactory.get('octokit')
32+
for (const repo of repositories) {
33+
this.log(info(`Updating secrets in org: ${organization} in repo: ${repo}`))
34+
for (const secret of secrets) {
35+
const [name, value] = secret.split(':')
36+
this.log(info(`Generating Key for secret ${name} with value ${value} in org: ${organization} in repo: ${repo} ${environment ? `in environment: ${environment}` : ''}`))
37+
const {data: publicKey} = await octoFactory.getPublicKey({owner: organization, repo, environment})
38+
this.log(info(`Encrypting secret ${name} with value ${value} in org: ${organization} in repo: ${repo} ${environment ? `in environment: ${environment}` : ''}`))
39+
const encryptedValue = await encryptSecret({value, publicKey})
40+
this.log(info(`Updating secret ${name} with value ${value} in org: ${organization} in repo: ${repo} ${environment ? `in environment: ${environment}` : ''}`))
41+
await octoFactory.updateSecret({owner: organization, repo, secret_name: name, encrypted_value: encryptedValue, key_id: publicKey.key_id, environment})
42+
this.log(info(`Updated secret ${name} with value ${value} in org: ${organization} in repo: ${repo} ${environment ? `in environment: ${environment}` : ''}`))
4543
}
4644
}
4745
}

src/helpers/rm-secret-helpers/rm-secret-flags.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ const RmSecretFlags = {
1717
description: 'If is set the env should be activated in the specified environment and create it if not exist',
1818
required: false,
1919
}),
20-
'secret-name': Flags.string({
21-
char: 'n',
20+
secrets: Flags.string({
21+
char: 's',
2222
description: 'Can be multiples secret names separated by space',
2323
required: true,
2424
multiple: true,

src/helpers/set-vars-helpers/secret-vars-flags.ts

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,19 +17,12 @@ const FlagsSecretAndVars = {
1717
description: 'If is set the env should be activated in the specified environment and create it if not exist',
1818
required: false,
1919
}),
20-
'secret-name': Flags.string({
21-
char: 'n',
22-
description: 'Can be multiples secret names separated by space',
20+
secrets: Flags.string({
21+
char: 's',
22+
description: 'Can be multiples secret names separated by : ej: name:secret',
2323
required: true,
2424
multiple: true,
2525
}),
26-
'secret-value': Flags.string({
27-
required: true,
28-
description: 'Can be multiples secret values separated by space',
29-
char: 'x',
30-
multiple: true,
31-
}),
32-
3326
help: Flags.help({char: 'h'}),
3427
}
3528
export default FlagsSecretAndVars

src/helpers/validations.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,3 +12,12 @@ export const validateEqualLengths = (names:string[], values:string[]):boolean|vo
1212
throw new Error('Secrets and values must be the same length')
1313
}
1414
}
15+
16+
export const validateSecrets = (secrets:string[]):boolean|void => {
17+
const okSecrets = secrets.every((secret: string) => {
18+
return /^[\w-]+:[\w-]+$/.test(secret)
19+
})
20+
if (!okSecrets) {
21+
throw new Error('The secret string must only contain numbers leters and dash and name and value must be separated by :')
22+
}
23+
}

src/repositories/octokit-repository.ts

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
import {Endpoints} from '@octokit/types'
2+
import {ux} from '@oclif/core'
3+
24
import octokitClient from './clients/octokit-client'
35
type listReposResponse = Endpoints['GET /orgs/{org}/repos']['response'];
46
type getEnvironmentsResponse = Endpoints['GET /repos/{owner}/{repo}/environments']['response'];
@@ -16,6 +18,9 @@ type removeCollaboratorParams=Endpoints['DELETE /repos/{owner}/{repo}/collaborat
1618
type addCollaboratorResponse=Endpoints['PUT /repos/{owner}/{repo}/collaborators/{username}']['response'];
1719
type addCollaboratorParams=Endpoints['PUT /repos/{owner}/{repo}/collaborators/{username}']['parameters'];
1820
type removeEnvironmentResponse=Endpoints['DELETE /repos/{owner}/{repo}/environments/{environment_name}']['response'];
21+
type getPublicKeyResponse=Endpoints['GET /repositories/{repository_id}/environments/{environment_name}/secrets/public-key']['response'];
22+
type updateSecretResponse=Endpoints['PUT /repositories/{repository_id}/environments/{environment_name}/secrets/{secret_name}']['response'];
23+
type removeSecretResponse=Endpoints['DELETE /repositories/{repository_id}/environments/{environment_name}/secrets/{secret_name}']['response'];
1924
export default {
2025
async setEnvironmentVariable({owner, repo, name, environment_name, value}:{owner:string, repo:string, name:string, environment_name:string, value: string}):Promise<postVariableResponse> {
2126
const octokit = await octokitClient({org: owner})
@@ -144,7 +149,6 @@ export default {
144149
},
145150
})
146151
},
147-
148152
async protectBranch({owner, repo, branch, countReviewers}:{owner:string, repo:string, branch:string, countReviewers:number},
149153
):Promise<protectBranchResponse> {
150154
const octokit = await octokitClient({org: owner})
@@ -202,4 +206,84 @@ export default {
202206
},
203207
})
204208
},
209+
async getPublicKey({owner, repo, environment}:{owner:string, repo:string, environment?:string}):Promise<getPublicKeyResponse> {
210+
const octokit = await octokitClient({org: owner})
211+
if (environment) {
212+
const {data: {environments}} = await this.getEnvironments({organization: owner, repository: repo})
213+
if (!environments?.find(env => env.name === environment)) {
214+
const confirm = await ux.confirm('The environment does not exist. Would you like to create it? (yes/no)')
215+
if (confirm) {
216+
await this.defineEnvironment({owner, repo, environment_name: environment})
217+
} else {
218+
throw new Error(`Environment ${environment} does not exist`)
219+
}
220+
}
221+
222+
const repoId = await this.getRepositoryId({owner, repo})
223+
return octokit.request('GET /repositories/{repository_id}/environments/{environment_name}/secrets/public-key', {
224+
repository_id: repoId,
225+
environment_name: environment,
226+
})
227+
}
228+
229+
return octokit.request('GET /repos/{owner}/{repo}/actions/secrets/public-key', {
230+
owner,
231+
repo,
232+
})
233+
},
234+
async updateSecret({owner, repo, secret_name, encrypted_value, key_id, environment}:{owner:string, repo:string, secret_name:string, encrypted_value:string, key_id:string, environment?:string}):Promise<updateSecretResponse> {
235+
const octokit = await octokitClient({org: owner})
236+
237+
if (environment) {
238+
const repoId = await this.getRepositoryId({owner, repo})
239+
240+
return octokit.request('PUT /repositories/{repository_id}/environments/{environment_name}/secrets/{secret_name}', {
241+
repository_id: repoId,
242+
environment_name: environment,
243+
secret_name,
244+
encrypted_value,
245+
key_id,
246+
headers: {
247+
'X-GitHub-Api-Version': '2022-11-28',
248+
},
249+
})
250+
}
251+
252+
return octokit.request('PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}', {
253+
owner,
254+
repo,
255+
secret_name,
256+
encrypted_value,
257+
key_id,
258+
headers: {
259+
'X-GitHub-Api-Version': '2022-11-28',
260+
},
261+
})
262+
},
263+
async removeSecret({owner, repo, secret_name, environment}:{owner:string, repo:string, secret_name:string, environment?:string}):Promise<removeSecretResponse> {
264+
const octokit = await octokitClient({org: owner})
265+
266+
if (environment) {
267+
const repoId = await this.getRepositoryId({owner, repo})
268+
269+
return octokit.request('DELETE /repositories/{repository_id}/environments/{environment_name}/secrets/{secret_name}', {
270+
repository_id: repoId,
271+
environment_name: environment,
272+
secret_name,
273+
headers: {
274+
'X-GitHub-Api-Version': '2022-11-28',
275+
},
276+
})
277+
}
278+
279+
return octokit.request('DELETE /repos/{owner}/{repo}/actions/secrets/{secret_name}', {
280+
owner,
281+
repo,
282+
secret_name,
283+
headers: {
284+
'X-GitHub-Api-Version': '2022-11-28',
285+
},
286+
})
287+
},
288+
205289
}
Lines changed: 7 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,18 @@
1-
import {getPublicEnvKey} from './get-public-env-key'
2-
import {getPublicKey} from './get-public-key'
31
import * as tweetnacl from 'tweetnacl-ts'
42
interface EncryptSecretsArgs{
5-
token:string;
63
value: string;
7-
org: string;
8-
repo: string;
9-
name:string;
10-
environment?:string;
4+
publicKey:{
5+
key_id: string;
6+
key: string;
7+
}
118
}
12-
const encryptSecrets = async ({token, value, org, repo, name, environment}:EncryptSecretsArgs): Promise<{encryptedValue: string;keyId: string; name: string; value:string; org:string; repo: string}> => {
13-
const {key, key_id: keyId} = environment ? await getPublicEnvKey(token, org, repo, environment) : await getPublicKey(token, org, repo)
9+
const encryptSecrets = async ({value, publicKey}:EncryptSecretsArgs): Promise<string> => {
10+
const {key} = publicKey
1411
const messageBytes = Buffer.from(value)
1512
const keyBytes = Buffer.from(key, 'base64')
1613
const encryptedBytes = tweetnacl.sealedbox(messageBytes, keyBytes)
1714
const encryptedValue = Buffer.from(encryptedBytes).toString('base64')
18-
return {encryptedValue, keyId, name, value, org, repo}
15+
return encryptedValue
1916
}
2017

2118
export default encryptSecrets

0 commit comments

Comments
 (0)