diff --git a/.eslintrc b/.eslintrc index b657845..101995b 100644 --- a/.eslintrc +++ b/.eslintrc @@ -1,11 +1,19 @@ { - "extends": ["oclif", "oclif-typescript"], + "extends": [ + "oclif", + "oclif-typescript" + ], "rules": { "@typescript-eslint/member-delimiter-style": { "multiline": { "delimiter": "none", "requireLast": false } - } + }, + "indent": [ + { + "SwitchCase": 1 + } + ] } -} +} \ No newline at end of file diff --git a/.prettierrc b/.prettierrc index 2e6de72..f77c6e0 100644 --- a/.prettierrc +++ b/.prettierrc @@ -3,5 +3,6 @@ "tabWidth": 2, "semi": false, "singleQuote": true, - "bracketSpacing": false + "bracketSpacing": false, + "arrowParens": "avoid" } diff --git a/README.md b/README.md index ffa2a6b..22d08fd 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ $ npm install -g @anomalyhq/github-secrets-cli $ ghs COMMAND running command... $ ghs (-v|--version|version) -@anomalyhq/github-secrets-cli/1.1.0 darwin-x64 node-v12.10.0 +@anomalyhq/github-secrets-cli/1.2.0 darwin-x64 node-v14.15.4 $ ghs --help [COMMAND] USAGE $ ghs COMMAND @@ -40,6 +40,7 @@ USAGE * [`ghs secrets:get`](#ghs-secretsget) * [`ghs secrets:remove`](#ghs-secretsremove) * [`ghs secrets:set`](#ghs-secretsset) +* [`ghs secrets:sync FILE`](#ghs-secretssync-file) ## `ghs config:get` @@ -50,7 +51,7 @@ USAGE $ ghs config:get ``` -_See code: [src/commands/config/get.ts](https://github.com/anomaly/github-secrets-cli/blob/v1.1.0/src/commands/config/get.ts)_ +_See code: [src/commands/config/get.ts](https://github.com/anomaly/github-secrets-cli/blob/v1.2.0/src/commands/config/get.ts)_ ## `ghs config:set` @@ -66,7 +67,7 @@ OPTIONS -t, --personalAccessToken=personalAccessToken Your GitHub Personal Access Token. ``` -_See code: [src/commands/config/set.ts](https://github.com/anomaly/github-secrets-cli/blob/v1.1.0/src/commands/config/set.ts)_ +_See code: [src/commands/config/set.ts](https://github.com/anomaly/github-secrets-cli/blob/v1.2.0/src/commands/config/set.ts)_ ## `ghs help [COMMAND]` @@ -83,7 +84,7 @@ OPTIONS --all see all commands in CLI ``` -_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.0/src/commands/help.ts)_ +_See code: [@oclif/plugin-help](https://github.com/oclif/plugin-help/blob/v3.2.1/src/commands/help.ts)_ ## `ghs secrets:get` @@ -100,7 +101,7 @@ OPTIONS -t, --personalAccessToken=personalAccessToken Your GitHub Personal Access Token. ``` -_See code: [src/commands/secrets/get.ts](https://github.com/anomaly/github-secrets-cli/blob/v1.1.0/src/commands/secrets/get.ts)_ +_See code: [src/commands/secrets/get.ts](https://github.com/anomaly/github-secrets-cli/blob/v1.2.0/src/commands/secrets/get.ts)_ ## `ghs secrets:remove` @@ -119,7 +120,7 @@ OPTIONS -y, --autoYes Skips user confirmation. ``` -_See code: [src/commands/secrets/remove.ts](https://github.com/anomaly/github-secrets-cli/blob/v1.1.0/src/commands/secrets/remove.ts)_ +_See code: [src/commands/secrets/remove.ts](https://github.com/anomaly/github-secrets-cli/blob/v1.2.0/src/commands/secrets/remove.ts)_ ## `ghs secrets:set` @@ -140,7 +141,29 @@ OPTIONS -t, --personalAccessToken=personalAccessToken Your GitHub Personal Access Token. ``` -_See code: [src/commands/secrets/set.ts](https://github.com/anomaly/github-secrets-cli/blob/v1.1.0/src/commands/secrets/set.ts)_ +_See code: [src/commands/secrets/set.ts](https://github.com/anomaly/github-secrets-cli/blob/v1.2.0/src/commands/secrets/set.ts)_ + +## `ghs secrets:sync FILE` + +Add/Update multiple secrets from one file. + +``` +USAGE + $ ghs secrets:sync FILE + +ARGUMENTS + FILE Path to the file to read from. + +OPTIONS + -b, --base64 base64 the values before encoding. + -f, --format=env|json|yaml (required) File format to parse secrets from. + -h, --help show CLI help + -o, --org=org Organisation the repo belongs to. + -r, --repo=repo Name of the repo. + -t, --personalAccessToken=personalAccessToken Your GitHub Personal Access Token. +``` + +_See code: [src/commands/secrets/sync.ts](https://github.com/anomaly/github-secrets-cli/blob/v1.2.0/src/commands/secrets/sync.ts)_ diff --git a/package.json b/package.json index c9dce79..ed31d47 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@anomalyhq/github-secrets-cli", "description": "Github secrets manager", - "version": "1.1.0", + "version": "1.2.0", "author": "Brendon Lees @brendon1555", "bin": { "ghs": "./bin/run" @@ -11,10 +11,13 @@ "@oclif/command": "^1", "@oclif/config": "^1", "@oclif/errors": "^1", - "@oclif/plugin-help": "^3", - "@octokit/request": "^5.4.5", + "@oclif/plugin-help": "^3.2.1", + "@octokit/request": "^5.4.13", + "@types/js-yaml": "^4.0.0", "cli-ux": "^5.5.0", - "fs-extra": "^9.0.1", + "dotenv": "^8.2.0", + "fs-extra": "^9.1.0", + "js-yaml": "^4.0.0", "node-emoji": "^1.10.0", "tslib": "^1", "tweetsodium": "^0.0.5" @@ -23,9 +26,9 @@ "@oclif/dev-cli": "^1", "@oclif/test": "^1", "@types/chai": "^4", - "@types/fs-extra": "^9.0.1", + "@types/fs-extra": "^9.0.6", "@types/mocha": "^5", - "@types/node": "^14.0.20", + "@types/node": "^14.14.22", "@types/node-emoji": "^1.8.1", "chai": "^4", "eslint": "^5.13", diff --git a/src/commands/secrets/set.ts b/src/commands/secrets/set.ts index 0f3e3e4..17b4470 100644 --- a/src/commands/secrets/set.ts +++ b/src/commands/secrets/set.ts @@ -110,10 +110,9 @@ export default class SecretsSet extends Command { key_id: token.key_id, } ) - this.log('secret updated') + this.log(`Updated secret: ${flags.secret}`) } catch (error) { - this.error(error) - this.log('unable to update secret') + this.error(`Unable to update secret: ${flags.secret} \n${error}`) } } catch (error) { this.error(new CLIError(error), {exit: 1}) diff --git a/src/commands/secrets/sync.ts b/src/commands/secrets/sync.ts new file mode 100644 index 0000000..ad21f77 --- /dev/null +++ b/src/commands/secrets/sync.ts @@ -0,0 +1,148 @@ +import {Command, flags} from '@oclif/command' +import {CLIError} from '@oclif/errors' +import dotenv from 'dotenv' +import {request} from '@octokit/request' +import fs from 'fs-extra' +import sodium from 'tweetsodium' +import {configuration} from '../../utils/config' +import yaml from 'js-yaml' + +type Env = {[env: string]: string} + +export default class SecretsSync extends Command { + static description = 'Add/Update multiple secrets from one file.' + + static flags = { + help: flags.help({char: 'h'}), + personalAccessToken: flags.string({ + char: 't', + description: 'Your GitHub Personal Access Token.', + required: false, + }), + org: flags.string({ + char: 'o', + description: 'Organisation the repo belongs to.', + required: false, + }), + repo: flags.string({ + char: 'r', + description: 'Name of the repo.', + required: false, + }), + base64: flags.boolean({ + char: 'b', + description: 'base64 the values before encoding.', + required: false, + default: false, + }), + format: flags.string({ + char: 'f', + description: 'File format to parse secrets from.', + required: true, + options: ['env', 'json', 'yaml'], + }), + } + + static args = [ + { + name: 'file', + required: true, + description: 'Path to the file to read from.', + }, + ] + + async run() { + const {args, flags} = this.parse(SecretsSync) + + try { + if (!args.file) { + this.error(new CLIError('Please provide a file')) + } + const messageString = fs.readFileSync(args.file, 'utf-8') + + let output: Env = {} + + switch (flags.format) { + case 'env': + output = dotenv.parse(messageString, {debug: true}) + // Check that the parser could read anything valid from the file + if ( + Object.keys(output).length === 0 || + output.constructor !== Object + ) { + this.error('File is either empty or not a valid .env format') + } + break + case 'json': + try { + output = JSON.parse(messageString) + // Check that the parser could read anything valid from the file + } catch (error) { + this.error('File is either empty or not a valid json format') + } + break + case 'yaml': + output = yaml.load(messageString) as Env + // Check that the parser could read anything valid from the file + if ( + Object.keys(output).length === 0 || + output?.constructor !== Object + ) { + this.error('File is either empty or not a valid yaml format') + } + break + } + + const conf = await configuration(this) + + const requestWithAuth = request.defaults({ + headers: { + authorization: `token ${ + flags.personalAccessToken ?? conf.personalAccessToken + }`, + }, + }) + + const {data: token} = await requestWithAuth( + 'GET /repos/{owner}/{repo}/actions/secrets/public-key', + { + owner: flags.org ?? conf.org, + repo: flags.repo ?? conf.repo, + } + ) + + Object.keys(output).forEach(async key => { + let value = output[key] + if (flags.base64) { + value = Buffer.from(value).toString('base64') + } + + const messageBytes = Buffer.from(value) + + const keyBytes = Buffer.from(token.key, 'base64') + + const encryptedBytes = sodium.seal(messageBytes, keyBytes) + + const encrypted = Buffer.from(encryptedBytes).toString('base64') + + try { + await requestWithAuth( + 'PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}', + { + owner: flags.org ?? conf.org, + repo: flags.repo ?? conf.repo, + secret_name: key, + encrypted_value: encrypted, + key_id: token.key_id, + } + ) + this.log(`Updated secret: ${key}`) + } catch (error) { + this.error(`Unable to update secret: ${key} \n${error}`) + } + }) + } catch (error) { + this.error(new CLIError(error), {exit: 1}) + } + } +} diff --git a/test/commands/secrets/sync.test.ts b/test/commands/secrets/sync.test.ts new file mode 100644 index 0000000..8daf1ae --- /dev/null +++ b/test/commands/secrets/sync.test.ts @@ -0,0 +1,17 @@ +import {expect, test} from '@oclif/test' + +describe('secrets:sync', () => { + test + .stdout() + .command(['secrets:sync']) + .it('runs hello', ctx => { + expect(ctx.stdout).to.contain('hello world') + }) + + test + .stdout() + .command(['secrets:sync', '--name', 'jeff']) + .it('runs hello --name jeff', ctx => { + expect(ctx.stdout).to.contain('hello jeff') + }) +}) diff --git a/yarn.lock b/yarn.lock index 4e696cf..609f1b1 100644 --- a/yarn.lock +++ b/yarn.lock @@ -199,6 +199,22 @@ widest-line "^3.1.0" wrap-ansi "^4.0.0" +"@oclif/plugin-help@^3.2.1": + version "3.2.1" + resolved "https://registry.yarnpkg.com/@oclif/plugin-help/-/plugin-help-3.2.1.tgz#0265ef2a7a8a37b0ed64957fb4f1ddac4b457d61" + integrity sha512-vq7rn16TrQmjX3Al/k1Z5iBZWZ3HE8fDXs52OmDJmmTqryPSNvURH9WCAsqr0PODYCSR17Hy1VTzS0x7vVVLEQ== + dependencies: + "@oclif/command" "^1.5.20" + "@oclif/config" "^1.15.1" + "@oclif/errors" "^1.2.2" + chalk "^2.4.1" + indent-string "^4.0.0" + lodash.template "^4.4.0" + string-width "^4.2.0" + strip-ansi "^6.0.0" + widest-line "^3.1.0" + wrap-ansi "^4.0.0" + "@oclif/screen@^1.0.3": version "1.0.4" resolved "https://registry.yarnpkg.com/@oclif/screen/-/screen-1.0.4.tgz#b740f68609dfae8aa71c3a6cab15d816407ba493" @@ -234,10 +250,10 @@ deprecation "^2.0.0" once "^1.4.0" -"@octokit/request@^5.4.5": - version "5.4.12" - resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.4.12.tgz#b04826fa934670c56b135a81447be2c1723a2ffc" - integrity sha512-MvWYdxengUWTGFpfpefBBpVmmEYfkwMoxonIB3sUGp5rhdgwjXL1ejo6JbgzG/QD9B/NYt/9cJX1pxXeSIUCkg== +"@octokit/request@^5.4.13": + version "5.4.13" + resolved "https://registry.yarnpkg.com/@octokit/request/-/request-5.4.13.tgz#eec5987b3e96f984fc5f41967e001170c6d23a18" + integrity sha512-WcNRH5XPPtg7i1g9Da5U9dvZ6YbTffw9BN2rVezYiE7couoSyaRsw0e+Tl8uk1fArHE7Dn14U7YqUDy59WaqEw== dependencies: "@octokit/endpoint" "^6.0.1" "@octokit/request-error" "^2.0.0" @@ -266,10 +282,10 @@ resolved "https://registry.yarnpkg.com/@types/eslint-visitor-keys/-/eslint-visitor-keys-1.0.0.tgz#1ee30d79544ca84d68d4b3cdb0af4f205663dd2d" integrity sha512-OCutwjDZ4aFS6PB1UZ988C4YgwlBHJd6wCeQqaLdmadZ/7e+w79+hbMUFC1QXDNCmdyoRfAFdm0RypzwR+Qpag== -"@types/fs-extra@^9.0.1": - version "9.0.5" - resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.5.tgz#2afb76a43a4bef80a363b94b314d0ca1694fc4f8" - integrity sha512-wr3t7wIW1c0A2BIJtdVp4EflriVaVVAsCAIHVzzh8B+GiFv9X1xeJjCs4upRXtzp7kQ6lP5xvskjoD4awJ1ZeA== +"@types/fs-extra@^9.0.6": + version "9.0.6" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-9.0.6.tgz#488e56b77299899a608b8269719c1d133027a6ab" + integrity sha512-ecNRHw4clCkowNOBJH1e77nvbPxHYnWIXMv1IAoG/9+MYGkgoyr3Ppxr7XYFNL41V422EDhyV4/4SSK8L2mlig== dependencies: "@types/node" "*" @@ -281,6 +297,11 @@ "@types/minimatch" "*" "@types/node" "*" +"@types/js-yaml@^4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/js-yaml/-/js-yaml-4.0.0.tgz#d1a11688112091f2c711674df3a65ea2f47b5dfb" + integrity sha512-4vlpCM5KPCL5CfGmTbpjwVKbISRYhduEJvvUWsH5EB7QInhEj94XPZ3ts/9FPiLZFqYO0xoW4ZL8z2AabTGgJA== + "@types/json-schema@^7.0.3": version "7.0.5" resolved "https://registry.yarnpkg.com/@types/json-schema/-/json-schema-7.0.5.tgz#dcce4430e64b443ba8945f0290fb564ad5bac6dd" @@ -311,10 +332,10 @@ resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.12.tgz#0b1d86f8c40141091285dea02e4940df73bba43f" integrity sha512-ASH8OPHMNlkdjrEdmoILmzFfsJICvhBsFfAum4aKZ/9U4B6M6tTmTPh+f3ttWdD74CEGV5XvXWkbyfSdXaTd7g== -"@types/node@^14.0.20": - version "14.14.13" - resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.13.tgz#9e425079799322113ae8477297ae6ef51b8e0cdf" - integrity sha512-vbxr0VZ8exFMMAjCW8rJwaya0dMCDyYW2ZRdTyjtrCvJoENMpdUHOT/eTzvgyA5ZnqRZ/sI0NwqAxNHKYokLJQ== +"@types/node@^14.14.22": + version "14.14.22" + resolved "https://registry.yarnpkg.com/@types/node/-/node-14.14.22.tgz#0d29f382472c4ccf3bd96ff0ce47daf5b7b84b18" + integrity sha512-g+f/qj/cNcqKkc3tFqlXOYjrmZA+jNBiDzbP3kH+B+otKFqAdPgVTGP1IeKRdMml/aE69as5S4FqtxAbl+LaMw== "@types/sinon@*": version "9.0.9" @@ -461,6 +482,11 @@ argparse@^1.0.7: dependencies: sprintf-js "~1.0.2" +argparse@^2.0.1: + version "2.0.1" + resolved "https://registry.yarnpkg.com/argparse/-/argparse-2.0.1.tgz#246f50f3ca78a3240f6c997e8a9bd1eac49e4b38" + integrity sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q== + array-union@^2.1.0: version "2.1.0" resolved "https://registry.yarnpkg.com/array-union/-/array-union-2.1.0.tgz#b798420adbeb1de828d84acd8a2e23d3efe85e8d" @@ -850,6 +876,11 @@ doctrine@^3.0.0: dependencies: esutils "^2.0.2" +dotenv@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/dotenv/-/dotenv-8.2.0.tgz#97e619259ada750eea3e4ea3e26bceea5424b16a" + integrity sha512-8sJ78ElpbDJBHNeBzUbUVLsqKdccaa/BXF1uPTw3GrvQTBgrQrtObr2mUrE38vzYd8cEv+m/JBfDLioYcfXoaw== + "emoji-regex@>=6.0.0 <=6.1.1": version "6.1.1" resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-6.1.1.tgz#c6cd0ec1b0642e2a3c67a1137efc5e796da4f88e" @@ -1270,15 +1301,15 @@ fs-extra@^8.1: jsonfile "^4.0.0" universalify "^0.1.0" -fs-extra@^9.0.1: - version "9.0.1" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.0.1.tgz#910da0062437ba4c39fedd863f1675ccfefcb9fc" - integrity sha512-h2iAoN838FqAFJY2/qVpzFXy+EBxfVE220PalAqQLDVsFOHLJrZvut5puAbCdNv6WJk+B8ihI+k0c7JK5erwqQ== +fs-extra@^9.1.0: + version "9.1.0" + resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-9.1.0.tgz#5954460c764a8da2094ba3554bf839e6b9a7c86d" + integrity sha512-hcg3ZmepS30/7BSFqRvoo3DOMQu7IjqxO5nCDt+zM9XWjb33Wg7ziNT+Qvqbuc3+gWpzO02JubVyk2G4Zvo1OQ== dependencies: at-least-node "^1.0.0" graceful-fs "^4.2.0" jsonfile "^6.0.1" - universalify "^1.0.0" + universalify "^2.0.0" fs.realpath@^1.0.0: version "1.0.0" @@ -1695,6 +1726,13 @@ js-yaml@^3.13.1: argparse "^1.0.7" esprima "^4.0.0" +js-yaml@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/js-yaml/-/js-yaml-4.0.0.tgz#f426bc0ff4b4051926cd588c71113183409a121f" + integrity sha512-pqon0s+4ScYUvX30wxQi3PogGFAlUyH0awepWvwkj4jD4v+ova3RiYw8bmA6x2rDrEaj8i/oWKoRxpVNW+Re8Q== + dependencies: + argparse "^2.0.1" + jsesc@^2.5.1: version "2.5.2" resolved "https://registry.yarnpkg.com/jsesc/-/jsesc-2.5.2.tgz#80564d2e483dacf6e8ef209650a67df3f0c283a4" @@ -2924,6 +2962,11 @@ universalify@^1.0.0: resolved "https://registry.yarnpkg.com/universalify/-/universalify-1.0.0.tgz#b61a1da173e8435b2fe3c67d29b9adf8594bd16d" integrity sha512-rb6X1W158d7pRQBg5gkR8uPaSfiids68LTJQYOtEUhoJUWBdaQHsuT/EUduxXYxcrt4r5PJ4fuHW1MHT6p0qug== +universalify@^2.0.0: + version "2.0.0" + resolved "https://registry.yarnpkg.com/universalify/-/universalify-2.0.0.tgz#75a4984efedc4b08975c5aeb73f530d02df25717" + integrity sha512-hAZsKq7Yy11Zu1DE0OzWjw7nnLZmJZYTDZZyEFHZdUhV8FkH5MCfoU1XMaxXovpyW5nq5scPqq0ZDP9Zyl04oQ== + uri-js@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/uri-js/-/uri-js-4.2.2.tgz#94c540e1ff772956e2299507c010aea6c8838eb0"