gh-secrets-sync 0.0.2 → 0.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +47 -10
- package/dist/cli.mjs +239 -81
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -1,32 +1,53 @@
|
|
|
1
1
|
# GitHub Secrets Sync
|
|
2
2
|
|
|
3
|
+
[![npm version][npm-version-src]][npm-version-href]
|
|
4
|
+
[![bundle][bundle-src]][bundle-href]
|
|
5
|
+
[![JSDocs][jsdocs-src]][jsdocs-href]
|
|
6
|
+
[![License][license-src]][license-href]
|
|
7
|
+
|
|
3
8
|
A CLI tool to batch sync GitHub Actions secrets across multiple repositories. Sync secrets from a central repository to target repositories using GitHub CI.
|
|
4
9
|
|
|
5
|
-
## Why
|
|
10
|
+
## Why?
|
|
6
11
|
|
|
7
12
|
Managing GitHub Actions secrets across multiple repositories can be tedious:
|
|
8
13
|
|
|
9
14
|
- **Manual repetition**: You need to manually add the same secret to each repository
|
|
10
|
-
- **Time-consuming**: Updating a secret across many repos requires visiting each one individually
|
|
11
15
|
- **Error-prone**: Easy to forget to update a secret in one of the repositories
|
|
12
16
|
|
|
13
17
|
This tool automates the process, allowing you to sync secrets across multiple repositories with a single command.
|
|
14
18
|
|
|
15
19
|
## Usage
|
|
16
20
|
|
|
17
|
-
|
|
21
|
+
**Create a configuration file** (`secrets.config.yaml`) in your central repository or local directory:
|
|
18
22
|
|
|
19
23
|
```yaml
|
|
20
24
|
repos:
|
|
21
|
-
- owner/vscode
|
|
22
|
-
- owner/vscode-extension2
|
|
25
|
+
- owner/vscode-*
|
|
23
26
|
|
|
24
27
|
envs:
|
|
25
28
|
- VSCE_PAT
|
|
26
29
|
- OVSX_PAT
|
|
27
30
|
```
|
|
28
31
|
|
|
29
|
-
|
|
32
|
+
> [!NOTE]
|
|
33
|
+
> Both `repos` and `envs` support `*` wildcards. For `repos`, the tool lists all repositories accessible by your token and filters by the pattern (e.g., `owner/vscode-*`). For `envs`, wildcards are expanded by listing secrets from the central repository and matching by name. The central repository is auto-detected in GitHub Actions (from the checked-out repo); for local runs, pass `--repo <owner/repo>`.
|
|
34
|
+
|
|
35
|
+
### Local usage
|
|
36
|
+
|
|
37
|
+
If GitHub CI feels too complex, you can simply run it locally:
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
# Set your token and secret values in env
|
|
41
|
+
export GITHUB_PAT=...
|
|
42
|
+
export VSCE_PAT=...
|
|
43
|
+
export OVSX_PAT=...
|
|
44
|
+
|
|
45
|
+
npx gh-secrets-sync
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### GitHub CI usage
|
|
49
|
+
|
|
50
|
+
**Set up GitHub CI** in your central repository:
|
|
30
51
|
|
|
31
52
|
```yaml
|
|
32
53
|
# .github/workflows/sync-secrets.yml
|
|
@@ -37,8 +58,10 @@ permissions:
|
|
|
37
58
|
|
|
38
59
|
on:
|
|
39
60
|
push:
|
|
40
|
-
branches:
|
|
41
|
-
|
|
61
|
+
branches: [main]
|
|
62
|
+
schedule:
|
|
63
|
+
- cron: '0 0 * * *'
|
|
64
|
+
workflow_dispatch:
|
|
42
65
|
|
|
43
66
|
jobs:
|
|
44
67
|
sync:
|
|
@@ -54,14 +77,15 @@ jobs:
|
|
|
54
77
|
node-version: lts/*
|
|
55
78
|
|
|
56
79
|
- name: Sync Secrets
|
|
57
|
-
|
|
80
|
+
# if regex patterns are used in `repos` or `secrets` must set `--yes` in GitHub Actions
|
|
81
|
+
run: npx gh-secrets-sync --yes
|
|
58
82
|
env:
|
|
59
83
|
GITHUB_PAT: ${{secrets.GITHUB_PAT}}
|
|
60
84
|
VSCE_PAT: ${{secrets.VSCE_PAT}}
|
|
61
85
|
OVSX_PAT: ${{secrets.OVSX_PAT}}
|
|
62
86
|
```
|
|
63
87
|
|
|
64
|
-
|
|
88
|
+
**Configure secrets in your central repository**:
|
|
65
89
|
- Go to your central repository Settings > Secrets and variables > Actions
|
|
66
90
|
- Add `GITHUB_PAT` as a repository secret (this is your GitHub Personal Access Token)
|
|
67
91
|
- Add `VSCE_PAT` and `OVSX_PAT` as repository secrets
|
|
@@ -81,3 +105,16 @@ jobs:
|
|
|
81
105
|
## License
|
|
82
106
|
|
|
83
107
|
[MIT](./LICENSE) License © [jinghaihan](https://github.com/jinghaihan)
|
|
108
|
+
|
|
109
|
+
<!-- Badges -->
|
|
110
|
+
|
|
111
|
+
[npm-version-src]: https://img.shields.io/npm/v/gh-secrets-sync?style=flat&colorA=080f12&colorB=1fa669
|
|
112
|
+
[npm-version-href]: https://npmjs.com/package/gh-secrets-sync
|
|
113
|
+
[npm-downloads-src]: https://img.shields.io/npm/dm/gh-secrets-sync?style=flat&colorA=080f12&colorB=1fa669
|
|
114
|
+
[npm-downloads-href]: https://npmjs.com/package/gh-secrets-sync
|
|
115
|
+
[bundle-src]: https://img.shields.io/bundlephobia/minzip/gh-secrets-sync?style=flat&colorA=080f12&colorB=1fa669&label=minzip
|
|
116
|
+
[bundle-href]: https://bundlephobia.com/result?p=gh-secrets-sync
|
|
117
|
+
[license-src]: https://img.shields.io/badge/license-MIT-blue.svg?style=flat&colorA=080f12&colorB=1fa669
|
|
118
|
+
[license-href]: https://github.com/jinghaihan/gh-secrets-sync/LICENSE
|
|
119
|
+
[jsdocs-src]: https://img.shields.io/badge/jsdocs-reference-080f12?style=flat&colorA=080f12&colorB=1fa669
|
|
120
|
+
[jsdocs-href]: https://www.jsdocs.io/package/gh-secrets-sync
|
package/dist/cli.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import process from 'node:process';
|
|
2
2
|
import c from 'ansis';
|
|
3
3
|
import { cac } from 'cac';
|
|
4
|
-
import {
|
|
4
|
+
import { readFile } from 'node:fs/promises';
|
|
5
|
+
import * as p from '@clack/prompts';
|
|
5
6
|
import { join } from 'pathe';
|
|
6
7
|
import { parse } from 'yaml';
|
|
7
8
|
import { Octokit } from '@octokit/core';
|
|
@@ -9,43 +10,28 @@ import Spinner from 'yocto-spinner';
|
|
|
9
10
|
import sodium from 'libsodium-wrappers';
|
|
10
11
|
|
|
11
12
|
const name = "gh-secrets-sync";
|
|
12
|
-
const version = "0.
|
|
13
|
+
const version = "0.1.1";
|
|
13
14
|
|
|
14
15
|
const DEFAULT_SYNC_OPTIONS = {
|
|
15
16
|
config: "./secrets.config.yaml",
|
|
16
17
|
repos: [],
|
|
17
18
|
secrets: [],
|
|
18
19
|
envPrefix: "",
|
|
20
|
+
apiVersion: "2022-11-28",
|
|
21
|
+
private: false,
|
|
22
|
+
baseUrl: "github.com",
|
|
23
|
+
repo: "",
|
|
19
24
|
dry: false,
|
|
20
|
-
strict: true
|
|
25
|
+
strict: true,
|
|
26
|
+
yes: false
|
|
21
27
|
};
|
|
22
|
-
const GITHUB_API_VERSION = "2022-11-28";
|
|
23
28
|
|
|
24
|
-
function
|
|
25
|
-
const
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
options.secrets = [options.secrets];
|
|
31
|
-
const config2 = { ...DEFAULT_SYNC_OPTIONS, ...options };
|
|
32
|
-
config2.token = config2.token || process.env.GITHUB_PAT || process.env.GITHUB_TOKEN || "";
|
|
33
|
-
if (config2.repos.length && config2.secrets.length) {
|
|
34
|
-
return config2;
|
|
35
|
-
}
|
|
36
|
-
const configContent = parse(readFileSync(join(cwd, config2.config), "utf-8"));
|
|
37
|
-
config2.repos = Array.isArray(configContent.repos) ? configContent.repos : [];
|
|
38
|
-
config2.secrets = Array.isArray(configContent.envs) ? configContent.envs : [];
|
|
39
|
-
return config2;
|
|
40
|
-
};
|
|
41
|
-
const config = getConfig();
|
|
42
|
-
if (!config.token)
|
|
43
|
-
throw new Error(c.red("Please provide a GitHub token"));
|
|
44
|
-
if (!config.repos)
|
|
45
|
-
throw new Error(c.red("Please provide repos to sync"));
|
|
46
|
-
if (!config.secrets)
|
|
47
|
-
throw new Error(c.red("Please provide secrets to sync"));
|
|
48
|
-
return config;
|
|
29
|
+
function createRegexFilter(options, field) {
|
|
30
|
+
const list = options.filter((i) => i.includes("*"));
|
|
31
|
+
if (list.length === 0 || list.includes("*"))
|
|
32
|
+
return () => true;
|
|
33
|
+
const regexes = list.map((r) => new RegExp(r));
|
|
34
|
+
return (i) => regexes.some((regex) => regex.test(i[field]));
|
|
49
35
|
}
|
|
50
36
|
|
|
51
37
|
async function encrypt(value, publicKey) {
|
|
@@ -59,85 +45,257 @@ async function encrypt(value, publicKey) {
|
|
|
59
45
|
function formatToken(token) {
|
|
60
46
|
return `Bearer ${token}`;
|
|
61
47
|
}
|
|
48
|
+
function parseRepo(repoPath) {
|
|
49
|
+
const [owner, repo] = repoPath.split("/");
|
|
50
|
+
return { owner, repo };
|
|
51
|
+
}
|
|
62
52
|
|
|
53
|
+
async function readTokenFromGitHubCli() {
|
|
54
|
+
try {
|
|
55
|
+
return await execCommand("gh", ["auth", "token"]);
|
|
56
|
+
} catch {
|
|
57
|
+
return "";
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
async function getGitHubRepo(baseUrl) {
|
|
61
|
+
const url = await execCommand("git", ["config", "--get", "remote.origin.url"]);
|
|
62
|
+
const escapedBaseUrl = baseUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
63
|
+
const regex = new RegExp(`${escapedBaseUrl}[/:]([\\w\\d._-]+?)\\/([\\w\\d._-]+?)(\\.git)?$`, "i");
|
|
64
|
+
const match = regex.exec(url);
|
|
65
|
+
if (!match)
|
|
66
|
+
throw new Error(`Can not parse GitHub repo from url ${url}`);
|
|
67
|
+
return `${match[1]}/${match[2]}`;
|
|
68
|
+
}
|
|
69
|
+
async function execCommand(cmd, args) {
|
|
70
|
+
const { execa } = await import('execa');
|
|
71
|
+
const res = await execa(cmd, args);
|
|
72
|
+
return res.stdout.trim();
|
|
73
|
+
}
|
|
63
74
|
async function getRepoPublicKey(repoPath, config) {
|
|
64
|
-
const octokit =
|
|
65
|
-
auth: formatToken(config.token)
|
|
66
|
-
});
|
|
67
|
-
const spinner = Spinner({ text: c.blue(`Fetching public key for ${repoPath}`) }).start();
|
|
75
|
+
const octokit = createOctokit(config);
|
|
68
76
|
const { owner, repo } = parseRepo(repoPath);
|
|
69
77
|
const url = `GET /repos/${owner}/${repo}/actions/secrets/public-key`;
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
78
|
+
return withSpinner(
|
|
79
|
+
`Fetching public key for ${repoPath}`,
|
|
80
|
+
`Public key fetched successfully for ${repoPath}`,
|
|
81
|
+
`Failed to fetch public key for ${repoPath}`,
|
|
82
|
+
`Fetch Public Key: ${c.yellow(url)}`,
|
|
83
|
+
{ key: "", key_id: "" },
|
|
84
|
+
config,
|
|
85
|
+
async () => {
|
|
86
|
+
const { status, data } = await octokit.request(
|
|
87
|
+
url,
|
|
88
|
+
{
|
|
89
|
+
owner,
|
|
90
|
+
repo,
|
|
91
|
+
headers: {
|
|
92
|
+
"X-GitHub-Api-Version": config.apiVersion
|
|
93
|
+
}
|
|
79
94
|
}
|
|
95
|
+
);
|
|
96
|
+
if (status !== 200) {
|
|
97
|
+
throw new Error(`HTTP ${status}: ${JSON.stringify(data)}`);
|
|
80
98
|
}
|
|
81
|
-
|
|
82
|
-
if (status !== 200) {
|
|
83
|
-
spinner.error(c.red(`Failed to fetch public key for ${repoPath}: ${status}`));
|
|
84
|
-
console.error(c.red(JSON.stringify(data, null, 2)));
|
|
85
|
-
process.exit(1);
|
|
99
|
+
return data;
|
|
86
100
|
}
|
|
87
|
-
|
|
88
|
-
} else {
|
|
89
|
-
console.log();
|
|
90
|
-
console.log(c.yellow(`Get a repository public key: ${repoPath}`));
|
|
91
|
-
}
|
|
92
|
-
spinner.success(c.green(`Public key fetched successfully for ${repoPath}`));
|
|
93
|
-
return res;
|
|
101
|
+
);
|
|
94
102
|
}
|
|
95
103
|
async function createOrUpdateRepoSecret(secretName, secretValue, publicKey, repoPath, config) {
|
|
96
|
-
const octokit =
|
|
97
|
-
auth: formatToken(config.token)
|
|
98
|
-
});
|
|
104
|
+
const octokit = createOctokit(config);
|
|
99
105
|
secretName = `${config.envPrefix}${secretName}`;
|
|
100
|
-
const spinner = Spinner({ text: c.blue(`Create or update ${secretName} for ${repoPath}`) }).start();
|
|
101
106
|
const { owner, repo } = parseRepo(repoPath);
|
|
102
107
|
const url = `PUT /repos/${owner}/${repo}/actions/secrets/${secretName}`;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
108
|
+
return withSpinner(
|
|
109
|
+
`Create or update ${secretName} for ${repoPath}`,
|
|
110
|
+
`${secretName} created/updated successfully for ${repoPath}`,
|
|
111
|
+
`Failed to create or update ${secretName} for ${repoPath}`,
|
|
112
|
+
`Create or Update Secret: ${c.yellow(url)}`,
|
|
113
|
+
void 0,
|
|
114
|
+
config,
|
|
115
|
+
async () => {
|
|
116
|
+
const { status, data } = await octokit.request(
|
|
117
|
+
url,
|
|
118
|
+
{
|
|
119
|
+
owner,
|
|
120
|
+
repo,
|
|
121
|
+
secret_name: secretName,
|
|
122
|
+
encrypted_value: await encrypt(secretValue, publicKey),
|
|
123
|
+
key_id: publicKey.key_id,
|
|
124
|
+
headers: {
|
|
125
|
+
"X-GitHub-Api-Version": config.apiVersion
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
);
|
|
129
|
+
if (status !== 201 && status !== 204) {
|
|
130
|
+
throw new Error(`HTTP ${status}: ${JSON.stringify(data)}`);
|
|
131
|
+
}
|
|
132
|
+
return data;
|
|
133
|
+
}
|
|
134
|
+
);
|
|
135
|
+
}
|
|
136
|
+
async function getRepos(config) {
|
|
137
|
+
const octokit = createOctokit(config);
|
|
138
|
+
const url = "GET /user/repos";
|
|
139
|
+
return withSpinner(
|
|
140
|
+
"Fetching repositories",
|
|
141
|
+
"Repositories fetched successfully",
|
|
142
|
+
"Failed to fetch repositories",
|
|
143
|
+
`Fetch Repositories: ${c.yellow(url)}`,
|
|
144
|
+
[],
|
|
145
|
+
config,
|
|
146
|
+
async () => {
|
|
147
|
+
const { status, data } = await octokit.request(url, {
|
|
148
|
+
headers: {
|
|
149
|
+
"X-GitHub-Api-Version": config.apiVersion
|
|
150
|
+
}
|
|
151
|
+
});
|
|
152
|
+
if (status !== 200) {
|
|
153
|
+
throw new Error(`HTTP ${status}: ${JSON.stringify(data)}`);
|
|
154
|
+
}
|
|
155
|
+
return data;
|
|
156
|
+
}
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
async function getRepoSecrets(config) {
|
|
160
|
+
const repoPath = config.repo || await getGitHubRepo(config.baseUrl);
|
|
161
|
+
if (!repoPath)
|
|
162
|
+
return [];
|
|
163
|
+
const octokit = createOctokit(config);
|
|
164
|
+
const { owner, repo } = parseRepo(repoPath);
|
|
165
|
+
const url = `GET /repos/${owner}/${repo}/actions/secrets`;
|
|
166
|
+
return withSpinner(
|
|
167
|
+
`Fetching secrets for ${repoPath}`,
|
|
168
|
+
`Secrets fetched successfully for ${repoPath}`,
|
|
169
|
+
`Failed to fetch secrets for ${repoPath}`,
|
|
170
|
+
`Fetch Secrets: ${c.yellow(url)}`,
|
|
171
|
+
[],
|
|
172
|
+
config,
|
|
173
|
+
async () => {
|
|
174
|
+
const { status, data } = await octokit.request(url, {
|
|
109
175
|
owner,
|
|
110
176
|
repo,
|
|
111
|
-
secret_name: secretName,
|
|
112
|
-
encrypted_value: await encrypt(secretValue, publicKey),
|
|
113
|
-
key_id: publicKey.key_id,
|
|
114
177
|
headers: {
|
|
115
|
-
"X-GitHub-Api-Version":
|
|
178
|
+
"X-GitHub-Api-Version": config.apiVersion
|
|
116
179
|
}
|
|
180
|
+
});
|
|
181
|
+
if (status !== 200) {
|
|
182
|
+
throw new Error(`HTTP ${status}: ${JSON.stringify(data)}`);
|
|
117
183
|
}
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
184
|
+
return data.secrets;
|
|
185
|
+
}
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
function createOctokit(config) {
|
|
189
|
+
return new Octokit({ auth: formatToken(config.token) });
|
|
190
|
+
}
|
|
191
|
+
async function withSpinner(loadingText, successText, failedText, dryText, defaultValue, config, request) {
|
|
192
|
+
const spinner = Spinner({ text: c.blue(loadingText) }).start();
|
|
193
|
+
let result;
|
|
194
|
+
if (!config.dry) {
|
|
195
|
+
try {
|
|
196
|
+
result = await request();
|
|
197
|
+
spinner.success(c.green(successText));
|
|
198
|
+
} catch (error) {
|
|
199
|
+
spinner.error(c.red(failedText));
|
|
200
|
+
console.error(c.red(JSON.stringify(error, null, 2)));
|
|
122
201
|
process.exit(1);
|
|
123
202
|
}
|
|
124
|
-
operation = status === 201 ? "created" : "updated";
|
|
125
|
-
res = data;
|
|
126
203
|
} else {
|
|
127
204
|
console.log();
|
|
128
|
-
console.log(c.yellow(
|
|
205
|
+
console.log(c.yellow(dryText));
|
|
206
|
+
result = defaultValue;
|
|
129
207
|
}
|
|
130
|
-
|
|
131
|
-
return res;
|
|
208
|
+
return result;
|
|
132
209
|
}
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
210
|
+
|
|
211
|
+
async function normalizeConfig(options) {
|
|
212
|
+
const cwd = options.cwd || process.cwd();
|
|
213
|
+
if (typeof options.repos === "string")
|
|
214
|
+
options.repos = [options.repos];
|
|
215
|
+
if (typeof options.secrets === "string")
|
|
216
|
+
options.secrets = [options.secrets];
|
|
217
|
+
const config = { ...DEFAULT_SYNC_OPTIONS, ...options };
|
|
218
|
+
config.token = config.token || process.env.GITHUB_PAT || process.env.GITHUB_TOKEN || await readTokenFromGitHubCli();
|
|
219
|
+
if (config.repos.length && config.secrets.length) {
|
|
220
|
+
return config;
|
|
221
|
+
}
|
|
222
|
+
const configContent = parse(await readFile(join(cwd, config.config), "utf-8"));
|
|
223
|
+
config.repos = Array.isArray(configContent.repos) ? configContent.repos : [];
|
|
224
|
+
config.secrets = Array.isArray(configContent.envs) ? configContent.envs : [];
|
|
225
|
+
return config;
|
|
226
|
+
}
|
|
227
|
+
async function resolveConfig(options) {
|
|
228
|
+
const config = await normalizeConfig(options);
|
|
229
|
+
if (config.repos.some((r) => r.includes("*"))) {
|
|
230
|
+
await resolveRepoPatterns(config);
|
|
231
|
+
}
|
|
232
|
+
if (config.secrets.some((s) => s.includes("*"))) {
|
|
233
|
+
await resolveSecretPatterns(config);
|
|
234
|
+
}
|
|
235
|
+
if (!config.token)
|
|
236
|
+
throw new Error(c.red("Please provide a GitHub token"));
|
|
237
|
+
if (!config.repos)
|
|
238
|
+
throw new Error(c.red("Please provide repos to sync"));
|
|
239
|
+
if (!config.secrets)
|
|
240
|
+
throw new Error(c.red("Please provide secrets to sync"));
|
|
241
|
+
return config;
|
|
242
|
+
}
|
|
243
|
+
async function resolveRepoPatterns(options) {
|
|
244
|
+
const filter = createRegexFilter(options.repos, "full_name");
|
|
245
|
+
let repos = (await getRepos(options)).filter((i) => filter(i) && (options.private || !i.private)).map((i) => i.full_name);
|
|
246
|
+
if (repos.length) {
|
|
247
|
+
repos = [.../* @__PURE__ */ new Set([...options.repos, ...repos])];
|
|
248
|
+
}
|
|
249
|
+
repos = repos.filter((i) => !i.includes("*"));
|
|
250
|
+
if (options.yes || !repos.length) {
|
|
251
|
+
options.repos = repos;
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
const result = await p.multiselect({
|
|
255
|
+
message: "Select repos to sync",
|
|
256
|
+
options: repos.map((i) => ({ label: i, value: i })),
|
|
257
|
+
initialValues: repos
|
|
258
|
+
});
|
|
259
|
+
if (p.isCancel(result)) {
|
|
260
|
+
console.error(c.red("aborting"));
|
|
261
|
+
process.exit(1);
|
|
262
|
+
}
|
|
263
|
+
if (typeof result === "symbol") {
|
|
264
|
+
console.error(c.red("invalid repo selection"));
|
|
265
|
+
process.exit(1);
|
|
266
|
+
}
|
|
267
|
+
options.repos = result;
|
|
268
|
+
}
|
|
269
|
+
async function resolveSecretPatterns(options) {
|
|
270
|
+
const filter = createRegexFilter(options.secrets, "name");
|
|
271
|
+
let secrets = (await getRepoSecrets(options)).filter((i) => filter(i)).map((i) => i.name);
|
|
272
|
+
if (secrets.length) {
|
|
273
|
+
secrets = [.../* @__PURE__ */ new Set([...options.secrets, ...secrets])];
|
|
274
|
+
}
|
|
275
|
+
secrets = secrets.filter((i) => !i.includes("*"));
|
|
276
|
+
if (options.yes || !secrets.length) {
|
|
277
|
+
options.secrets = secrets;
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const result = await p.multiselect({
|
|
281
|
+
message: "Select secrets to sync",
|
|
282
|
+
options: secrets.map((i) => ({ label: i, value: i })),
|
|
283
|
+
initialValues: secrets
|
|
284
|
+
});
|
|
285
|
+
if (p.isCancel(result)) {
|
|
286
|
+
console.error(c.red("aborting"));
|
|
287
|
+
process.exit(1);
|
|
288
|
+
}
|
|
289
|
+
if (typeof result === "symbol") {
|
|
290
|
+
console.error(c.red("invalid secret selection"));
|
|
291
|
+
process.exit(1);
|
|
292
|
+
}
|
|
293
|
+
options.secrets = result;
|
|
136
294
|
}
|
|
137
295
|
|
|
138
296
|
try {
|
|
139
297
|
const cli = cac("gh-secrets-sync");
|
|
140
|
-
cli.command("").option("--config <config>", "secrets config file", { default: "./secrets.config.yaml" }).option("--secrets <secrets...>", "secrets name to sync", { default: [] }).option("--token <token>", "GitHub token").option("--env-prefix <prefix>", "environment variable prefix", { default: "" }).option("--strict", "Throw error if secret is not found in the environment variables", { default: true }).option("--dry", "Dry run", { default: false }).allowUnknownOptions().action(async (options) => {
|
|
298
|
+
cli.command("").option("--config <config>", "secrets config file", { default: "./secrets.config.yaml" }).option("--secrets <secrets...>", "secrets name to sync", { default: [] }).option("--token <token>", "GitHub token").option("--api-version <version>", "GitHub API version", { default: "2022-11-28" }).option("--private", "Detect private repositories", { default: false }).option("--env-prefix <prefix>", "environment variable prefix", { default: "" }).option("--repo <repo>", "central GitHub repository").option("--strict", "Throw error if secret is not found in the environment variables", { default: true }).option("--yes", "Prompt to confirm resolved matches when regex patterns are used in repos or secrets", { default: false }).option("--dry", "Dry run", { default: false }).allowUnknownOptions().action(async (options) => {
|
|
141
299
|
console.log(`${c.yellow(name)} ${c.dim(`v${version}`)}`);
|
|
142
300
|
console.log();
|
|
143
301
|
const config = await resolveConfig(options);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "gh-secrets-sync",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.1.1",
|
|
5
5
|
"description": "CLI tool to batch sync GitHub Actions secrets across multiple repositories.",
|
|
6
6
|
"author": "jinghaihan",
|
|
7
7
|
"license": "MIT",
|
|
@@ -35,13 +35,13 @@
|
|
|
35
35
|
"dist"
|
|
36
36
|
],
|
|
37
37
|
"dependencies": {
|
|
38
|
+
"@clack/prompts": "^0.11.0",
|
|
38
39
|
"@octokit/core": "^7.0.3",
|
|
39
40
|
"ansis": "^4.1.0",
|
|
40
41
|
"cac": "^6.7.14",
|
|
41
42
|
"execa": "^9.6.0",
|
|
42
43
|
"libsodium-wrappers": "^0.7.15",
|
|
43
44
|
"pathe": "^2.0.3",
|
|
44
|
-
"unconfig": "^7.3.2",
|
|
45
45
|
"yaml": "^2.8.1",
|
|
46
46
|
"yocto-spinner": "^1.0.0"
|
|
47
47
|
},
|