Skip to content
This repository was archived by the owner on Jul 1, 2024. It is now read-only.

Commit 5348df7

Browse files
authored
✨ Add script to manage repository secrets & vars (#71)
1 parent 85dff8d commit 5348df7

File tree

8 files changed

+280
-0
lines changed

8 files changed

+280
-0
lines changed

.eslintrc.json

+3
Original file line numberDiff line numberDiff line change
@@ -28,16 +28,19 @@
2828
"default_branch",
2929
"default_workflow_permissions",
3030
"delete_branch_on_merge",
31+
"encrypted_value",
3132
"full_name",
3233
"has_issues",
3334
"has_projects",
3435
"has_wiki",
3536
"html_url",
3637
"installation_id",
3738
"issue_number",
39+
"key_id",
3840
"new_name",
3941
"node_id",
4042
"per_page",
43+
"secret_name",
4144
"secret_scanning",
4245
"security_and_analysis",
4346
"squash_merge_commit_message",

package-lock.json

+1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
"scripts/dependabot-config",
1212
"scripts/release-config",
1313
"scripts/repository-labels",
14+
"scripts/repository-secrets",
1415
"scripts/repository-settings",
1516
"scripts/workflow-shas"
1617
],

scripts/repository-secrets/cli.js

+6
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
#!/usr/bin/env node
2+
3+
import {run} from '@octoherd/cli/run'
4+
import {script} from './script.js'
5+
6+
run(script)

scripts/repository-secrets/license

+20
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
The MIT License (MIT)
2+
3+
Copyright (c) Stefan Stölzle <[email protected]> (github.com/stoe)
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
13+
14+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20+
THE SOFTWARE.
+42
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
{
2+
"name": "@stoe/octoherd-script-repository-secrets",
3+
"version": "0.0.0-development",
4+
"type": "module",
5+
"description": "Manage repository Action secrets and variables",
6+
"keywords": [
7+
"octohed-script",
8+
"github-actions",
9+
"secrets",
10+
"variables"
11+
],
12+
"author": {
13+
"name": "Stefan Stölzle",
14+
"email": "[email protected]",
15+
"url": "https://github.com/stoe"
16+
},
17+
"license": "MIT",
18+
"repository": "https://github.com/stoe/octoherd-scripts",
19+
"publishConfig": {
20+
"access": "public",
21+
"registry": "https://npm.pkg.github.com"
22+
},
23+
"engines": {
24+
"node": ">=16",
25+
"npm": ">=8"
26+
},
27+
"exports": "./script.js",
28+
"bin": {
29+
"octoherd-script-repository-secrets": "./cli.js"
30+
},
31+
"scripts": {
32+
"format": "npx prettier --ignore-path ../../.prettierignore --config-precedence prefer-file --write . && eslint '*.js' --fix",
33+
"pretest": "npx eslint-config-prettier ../../.eslintrc.json",
34+
"test": "eslint '*.js'"
35+
},
36+
"dependencies": {
37+
"@octoherd/cli": "^3.6.5",
38+
"js-yaml": "^4.1.0",
39+
"libsodium-wrappers": "^0.7.11"
40+
},
41+
"prettier": "@github/prettier-config"
42+
}

scripts/repository-secrets/readme.md

+40
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# octoherd-script: repository-secrets
2+
3+
[![repository-secrets version](https://img.shields.io/github/package-json/v/stoe/octoherd-scripts?filename=scripts%2Frepository-secrets%2Fpackage.json)](https://github.com/stoe/octoherd-scripts/pkgs/npm/octoherd-script-repository-secrets)
4+
5+
> Replace Action versions with SHAs
6+
>
7+
> [@octoherd](https://github.com/octoherd/) helps to keep your GitHub repositories in line.
8+
9+
## Usage
10+
11+
```sh
12+
$ npx @stoe/octoherd-script-repository-secrets \
13+
--octoherd-token ghp_000000000000000000000000000000000000 \
14+
--octoherd-repos "stoe/*" \
15+
--path .secrets.yml
16+
```
17+
18+
## Options
19+
20+
| option | type | description |
21+
| ----------- | ------- | --------------------------------------------------------------------------- |
22+
| `--dry-run` | boolean | show what would be done (default `false`) |
23+
| `--path` | string | path to the secrets file in YAML format, see below (default `.secrets.yml`) |
24+
25+
`.secrets.yml` format
26+
27+
```yaml
28+
# .secrets.yml
29+
secrets:
30+
SECRET_NAME: value
31+
ANOTHER_SECRET: value
32+
33+
variables:
34+
VARIABLE_NAME: value
35+
ANOTHER_VARIABLE: value
36+
```
37+
38+
## License
39+
40+
[MIT](license)

scripts/repository-secrets/script.js

+167
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
import _sodium from 'libsodium-wrappers'
2+
import {load} from 'js-yaml'
3+
import {readFileSync} from 'fs'
4+
5+
/**
6+
* @param {import('@octoherd/cli').Octokit} octokit
7+
* @param {import('@octoherd/cli').Repository} repository
8+
* @param {object} options
9+
* @param {string} [options.path=.secrets.yml]
10+
* @param {boolean} [options.dryRun=false]
11+
*/
12+
export async function script(octokit, repository, {path = '.secrets.yml', dryRun = false}) {
13+
const {
14+
archived,
15+
disabled,
16+
fork,
17+
name: repo,
18+
owner: {login: owner},
19+
size,
20+
clone_url: url,
21+
} = repository
22+
23+
// skip archived, disabled, forked and empty repos
24+
if (archived || disabled || fork || size === 0) return
25+
26+
try {
27+
// read secrets from file
28+
const buff = readFileSync(path, 'utf-8')
29+
const {secrets, variables} = await load(buff)
30+
31+
// fail if no secrets or variables found
32+
if (!secrets && !variables) {
33+
octokit.log.error(`❌ no secrets nor variables found in ${path}`)
34+
return
35+
}
36+
37+
// repository secrets
38+
if (secrets) {
39+
// https://docs.github.com/en/rest/actions/secrets#get-a-repository-public-key
40+
const {
41+
data: {key_id, key},
42+
} = await octokit.request('GET /repos/{owner}/{repo}/actions/secrets/public-key', {
43+
owner,
44+
repo,
45+
})
46+
47+
for (const secret of Object.keys(secrets)) {
48+
const secretName = secret
49+
const secretValue = secrets[secret]
50+
51+
const encryptedValue = await encrypt(key, secretValue)
52+
53+
if (dryRun) {
54+
octokit.log.info(` 🐢 dry-run create or update secret ${secretName}`)
55+
} else {
56+
try {
57+
// https://docs.github.com/en/rest/actions/secrets#create-or-update-a-repository-secret
58+
const {status} = await octokit.request('PUT /repos/{owner}/{repo}/actions/secrets/{secret_name}', {
59+
owner,
60+
repo,
61+
secret_name: secretName,
62+
encrypted_value: encryptedValue,
63+
key_id,
64+
})
65+
66+
octokit.log.info(` 🛡️ ${status === 201 ? 'created' : 'updated'} ${secretName}`)
67+
} catch (error) {
68+
octokit.log.error({error: error.message, secret: secretName}, ` ❌ create or update secret ${secretName}`)
69+
}
70+
}
71+
}
72+
}
73+
74+
// repository variables
75+
if (variables) {
76+
for (const variable of Object.keys(variables)) {
77+
const variableName = variable
78+
const variableValue = variables[variable]
79+
80+
let create = false
81+
let value = null
82+
83+
try {
84+
// https://docs.github.com/en/rest/actions/variables#get-a-repository-variable
85+
const {
86+
data: {value: v},
87+
} = await octokit.request('GET /repos/{owner}/{repo}/actions/variables/{name}', {
88+
owner,
89+
repo,
90+
name: variableName,
91+
})
92+
93+
value = v
94+
} catch (error) {
95+
create = true
96+
}
97+
98+
if (variableValue === value) {
99+
octokit.log.info(` 🙊 no change for variable ${variableName}`)
100+
continue
101+
}
102+
103+
if (dryRun) {
104+
octokit.log.info(` 🐢 dry-run ${create ? 'create' : 'update'} variable ${variableName}`)
105+
} else {
106+
try {
107+
if (create) {
108+
// https://docs.github.com/en/rest/actions/variables#create-a-repository-variable
109+
await octokit.request('POST /repos/{owner}/{repo}/actions/variables', {
110+
owner,
111+
repo,
112+
name: variableName,
113+
value: variableValue,
114+
})
115+
} else {
116+
// https://docs.github.com/en/rest/actions/variables#create-a-repository-variable
117+
await octokit.request('PATCH /repos/{owner}/{repo}/actions/variables/{name}', {
118+
owner,
119+
repo,
120+
name: variableName,
121+
value: variableValue,
122+
})
123+
}
124+
125+
octokit.log.info(` 🛡️ ${create ? 'created' : 'updated'} ${variableName}`)
126+
} catch (error) {
127+
octokit.log.error({error: error.message}, ` ❌ ${create ? 'create' : 'update'} variable ${variableName}`)
128+
}
129+
}
130+
}
131+
}
132+
133+
octokit.log.info(` ✅ ${url}`)
134+
return
135+
} catch (error) {
136+
// eslint-disable-next-line no-console
137+
console.error(error)
138+
octokit.log.error(`❌ ${error.message}`)
139+
}
140+
}
141+
142+
/**
143+
* Encrypt a secret using a public key.
144+
* https://www.npmjs.com/package/libsodium-wrappers
145+
*
146+
* @function encrypt
147+
* @async
148+
*
149+
* @param {string} key
150+
* @param {string} secret
151+
*
152+
* @returns {Promise<string>}
153+
*/
154+
const encrypt = async (key, secret) => {
155+
await _sodium.ready
156+
const sodium = _sodium
157+
158+
// convert base64 key & secret to Uint8Array.
159+
const binkey = sodium.from_base64(key, sodium.base64_variants.ORIGINAL)
160+
const binsec = sodium.from_string(secret)
161+
162+
// encrypt the secret using LibSodium
163+
const encBytes = sodium.crypto_box_seal(binsec, binkey)
164+
165+
// convert encrypted Uint8Array to Base64
166+
return sodium.to_base64(encBytes, sodium.base64_variants.ORIGINAL)
167+
}

0 commit comments

Comments
 (0)