Skip to content

Commit 79305d3

Browse files
committed
feat: confirmation on regex matches
1 parent f3d8931 commit 79305d3

8 files changed

Lines changed: 76 additions & 11 deletions

File tree

README.md

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ This tool automates the process, allowing you to sync secrets across multiple re
1818

1919
## Usage
2020

21-
**Create a configuration file** (`secrets.config.yaml`) in your central repository:
21+
**Create a configuration file** (`secrets.config.yaml`) in your central repository or local directory:
2222

2323
```yaml
2424
repos:
@@ -29,7 +29,8 @@ envs:
2929
- OVSX_PAT
3030
```
3131
32-
> **Note**: 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>`.
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>`.
3334

3435
### Local usage
3536

@@ -76,7 +77,8 @@ jobs:
7677
node-version: lts/*
7778
7879
- name: Sync Secrets
79-
run: npx gh-secrets-sync
80+
# if regex patterns are used in `repos` or `secrets` must set `--yes` in GitHub Actions
81+
run: npx gh-secrets-sync --yes
8082
env:
8183
GITHUB_PAT: ${{secrets.GITHUB_PAT}}
8284
VSCE_PAT: ${{secrets.VSCE_PAT}}

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@
5050
"prepublishOnly": "pnpm build"
5151
},
5252
"dependencies": {
53+
"@clack/prompts": "catalog:node",
5354
"@octokit/core": "catalog:utils",
5455
"ansis": "catalog:node",
5556
"cac": "catalog:node",

pnpm-lock.yaml

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pnpm-workspace.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ catalogs:
1414
lint-staged: ^16.1.4
1515
simple-git-hooks: ^2.13.1
1616
node:
17+
'@clack/prompts': ^0.11.0
1718
ansis: ^4.1.0
1819
cac: ^6.7.14
1920
execa: ^9.6.0

src/cli.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ try {
2020
.option('--env-prefix <prefix>', 'environment variable prefix', { default: '' })
2121
.option('--repo <repo>', 'central GitHub repository')
2222
.option('--strict', 'Throw error if secret is not found in the environment variables', { default: true })
23+
.option('--yes', 'Prompt to confirm resolved matches when regex patterns are used in repos or secrets', { default: false })
2324
.option('--dry', 'Dry run', { default: false })
2425
.allowUnknownOptions()
2526
.action(async (options: CommandOptions) => {

src/config.ts

Lines changed: 55 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import type { CommandOptions, Repo, Secret, SyncOptions } from './types'
22
import { readFile } from 'node:fs/promises'
33
import process from 'node:process'
4+
import * as p from '@clack/prompts'
45
import c from 'ansis'
56
import { join } from 'pathe'
67
import { parse } from 'yaml'
@@ -35,11 +36,12 @@ async function normalizeConfig(options: CommandOptions): Promise<SyncOptions> {
3536
export async function resolveConfig(options: CommandOptions): Promise<SyncOptions> {
3637
const config = await normalizeConfig(options)
3738

38-
// resolve repos with regex
39+
// resolve repos with regex patterns
3940
if (config.repos.some(r => r.includes('*'))) {
4041
await resolveRepoPatterns(config)
4142
}
4243

44+
// resolve secrets with regex patterns
4345
if (config.secrets.some(s => s.includes('*'))) {
4446
await resolveSecretPatterns(config)
4547
}
@@ -57,25 +59,71 @@ export async function resolveConfig(options: CommandOptions): Promise<SyncOption
5759
async function resolveRepoPatterns(options: SyncOptions) {
5860
const filter = createRegexFilter<'full_name', Repo>(options.repos, 'full_name')
5961

60-
const repos = (await getRepos(options))
62+
let repos = (await getRepos(options))
6163
.filter(i => filter(i) && (options.private || !i.private))
6264
.map(i => i.full_name)
6365

6466
if (repos.length) {
65-
options.repos = [...new Set([...options.repos, ...repos])]
67+
repos = [...new Set([...options.repos, ...repos])]
6668
}
67-
options.repos = options.repos.filter(i => !i.includes('*'))
69+
repos = repos.filter(i => !i.includes('*'))
70+
71+
if (options.yes || !repos.length) {
72+
options.repos = repos
73+
return
74+
}
75+
76+
const result = await p.multiselect<string>({
77+
message: 'Select repos to sync',
78+
options: repos.map(i => ({ label: i, value: i })),
79+
initialValues: repos,
80+
})
81+
82+
if (p.isCancel(result)) {
83+
console.error(c.red('aborting'))
84+
process.exit(1)
85+
}
86+
87+
if (typeof result === 'symbol') {
88+
console.error(c.red('invalid repo selection'))
89+
process.exit(1)
90+
}
91+
92+
options.repos = result
6893
}
6994

7095
async function resolveSecretPatterns(options: SyncOptions) {
7196
const filter = createRegexFilter<'name', Secret>(options.secrets, 'name')
7297

73-
const secrets = (await getRepoSecrets(options))
98+
let secrets = (await getRepoSecrets(options))
7499
.filter(i => filter(i))
75100
.map(i => i.name)
76101

77102
if (secrets.length) {
78-
options.secrets = [...new Set([...options.secrets, ...secrets])]
103+
secrets = [...new Set([...options.secrets, ...secrets])]
79104
}
80-
options.secrets = options.secrets.filter(i => !i.includes('*'))
105+
secrets = secrets.filter(i => !i.includes('*'))
106+
107+
if (options.yes || !secrets.length) {
108+
options.secrets = secrets
109+
return
110+
}
111+
112+
const result = await p.multiselect<string>({
113+
message: 'Select secrets to sync',
114+
options: secrets.map(i => ({ label: i, value: i })),
115+
initialValues: secrets,
116+
})
117+
118+
if (p.isCancel(result)) {
119+
console.error(c.red('aborting'))
120+
process.exit(1)
121+
}
122+
123+
if (typeof result === 'symbol') {
124+
console.error(c.red('invalid secret selection'))
125+
process.exit(1)
126+
}
127+
128+
options.secrets = result
81129
}

src/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,4 +9,5 @@ export const DEFAULT_SYNC_OPTIONS = {
99
repo: '',
1010
dry: false,
1111
strict: true,
12+
yes: false,
1213
} as const

src/types.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,11 @@ export interface CommonOptions {
88
* Throw error if secret is not found in the environment variables
99
*/
1010
strict?: boolean
11+
/**
12+
* Prompt for confirmation when regex patterns are used in `repos` or `secrets`.
13+
* Shows the resolved matches and requires user approval.
14+
*/
15+
yes?: boolean
1116
}
1217

1318
export interface CommandOptions extends CommonOptions {
@@ -67,5 +72,5 @@ export interface Repo {
6772
export interface Secret {
6873
name: string
6974
created_at: string
70-
updated_ad: string
75+
updated_at: string
7176
}

0 commit comments

Comments
 (0)