@smoothbricks/cli 0.11.19 → 0.11.21
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 +68 -5
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +28 -4
- package/dist/lib/json.d.ts +22 -1
- package/dist/lib/json.d.ts.map +1 -1
- package/dist/lib/json.js +15 -3
- package/dist/monorepo/cargo-policy.d.ts +17 -0
- package/dist/monorepo/cargo-policy.d.ts.map +1 -1
- package/dist/monorepo/cargo-policy.js +73 -1
- package/dist/monorepo/index.d.ts.map +1 -1
- package/dist/monorepo/index.js +4 -2
- package/dist/monorepo/packs/index.d.ts.map +1 -1
- package/dist/monorepo/packs/index.js +6 -2
- package/dist/nx/index.d.ts +6 -1
- package/dist/nx/index.d.ts.map +1 -1
- package/dist/nx/index.js +14 -1
- package/dist/secrets/commands.d.ts +9 -5
- package/dist/secrets/commands.d.ts.map +1 -1
- package/dist/secrets/commands.js +122 -49
- package/dist/secrets/index.d.ts +42 -56
- package/dist/secrets/index.d.ts.map +1 -1
- package/dist/secrets/index.js +52 -79
- package/dist/secrets/resolver.d.ts +59 -0
- package/dist/secrets/resolver.d.ts.map +1 -0
- package/dist/secrets/resolver.js +100 -0
- package/dist/secrets/run.d.ts +23 -0
- package/dist/secrets/run.d.ts.map +1 -0
- package/dist/secrets/run.js +130 -0
- package/dist/secrets/status.d.ts +177 -0
- package/dist/secrets/status.d.ts.map +1 -0
- package/dist/secrets/status.js +724 -0
- package/dist/wrangler/deploy-stage.d.ts +1 -1
- package/dist/wrangler/deploy-stage.d.ts.map +1 -1
- package/dist/wrangler/deploy-stage.js +22 -20
- package/dist/wrangler/deployed-version.d.ts.map +1 -1
- package/dist/wrangler/deployed-version.js +7 -12
- package/dist/wrangler/flat-config.d.ts +1 -4
- package/dist/wrangler/flat-config.d.ts.map +1 -1
- package/dist/wrangler/flat-config.js +98 -44
- package/dist/wrangler/prepare-env.d.ts +4 -3
- package/dist/wrangler/prepare-env.d.ts.map +1 -1
- package/dist/wrangler/prepare-env.js +7 -5
- package/dist/wrangler/source-config.d.ts +24 -0
- package/dist/wrangler/source-config.d.ts.map +1 -0
- package/dist/wrangler/source-config.js +110 -0
- package/dist/wrangler/stage.d.ts +55 -23
- package/dist/wrangler/stage.d.ts.map +1 -1
- package/dist/wrangler/stage.js +81 -162
- package/managed/raw/tooling/direnv/devenv.smoo.nix +19 -3
- package/managed/raw/tooling/direnv/secret-references.ts +280 -76
- package/managed/raw/tooling/direnv/setup-environment.ts +58 -2
- package/managed/raw/tsconfig.lib.json +28 -0
- package/package.json +7 -2
- package/src/cli.ts +34 -5
- package/src/lib/json.ts +23 -1
- package/src/monorepo/cargo-policy.test.ts +91 -1
- package/src/monorepo/cargo-policy.ts +87 -1
- package/src/monorepo/index.ts +8 -2
- package/src/monorepo/package-policy.test.ts +50 -19
- package/src/monorepo/packs/index.ts +10 -2
- package/src/monorepo/secret-references.test.ts +412 -6
- package/src/monorepo/setup-environment.test.ts +181 -0
- package/src/nx/index.test.ts +8 -2
- package/src/nx/index.ts +20 -2
- package/src/secrets/commands.test.ts +186 -4
- package/src/secrets/commands.ts +142 -51
- package/src/secrets/index.test.ts +4 -10
- package/src/secrets/index.ts +54 -115
- package/src/secrets/resolver.ts +130 -0
- package/src/secrets/run.test.ts +359 -0
- package/src/secrets/run.ts +148 -0
- package/src/secrets/status.test.ts +164 -0
- package/src/secrets/status.ts +297 -0
- package/src/wrangler/deploy-stage.test.ts +95 -2
- package/src/wrangler/deploy-stage.ts +26 -23
- package/src/wrangler/deployed-version.ts +7 -11
- package/src/wrangler/flat-config.test.ts +9 -4
- package/src/wrangler/flat-config.ts +1 -20
- package/src/wrangler/format-parity.test.ts +433 -0
- package/src/wrangler/prepare-env.ts +7 -5
- package/src/wrangler/source-config.test.ts +102 -0
- package/src/wrangler/source-config.ts +110 -0
- package/src/wrangler/stage.test.ts +61 -32
- package/src/wrangler/stage.ts +124 -173
package/dist/secrets/commands.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `smoo secrets` — the operator side of the reconciliation in ./
|
|
2
|
+
* `smoo secrets` — the operator side of the reconciliation in ./status.ts.
|
|
3
3
|
*
|
|
4
4
|
* Values are handed to `gh` over stdin, never through argv (a process list is
|
|
5
5
|
* world-readable) and never printed. Reading one from the terminal disables
|
|
@@ -16,8 +16,9 @@
|
|
|
16
16
|
*/
|
|
17
17
|
import { spawn, spawnSync } from 'node:child_process';
|
|
18
18
|
import { getWorkspacePackages } from '../lib/workspace.js';
|
|
19
|
-
import {
|
|
19
|
+
import { fetchLocalSecret, localSecretGroups, secretNameMapping, workerSecretNames, workerSecretStages, workflowEnvironments, workflowSecretNames, } from './index.js';
|
|
20
20
|
import { resolveRepository } from './repository.js';
|
|
21
|
+
import { projectSecretsStatus, reconcileSecrets, stringifySecretsStatusDocument, unwiredSecrets, } from './status.js';
|
|
21
22
|
/**
|
|
22
23
|
* Every secret this checkout declares that the repository does not hold, in
|
|
23
24
|
* the order an operator should fix them: the ones a workflow already promises
|
|
@@ -108,19 +109,21 @@ export async function writeRepositorySecret(name, value, repo, environment) {
|
|
|
108
109
|
? { ok: true }
|
|
109
110
|
: { ok: false, reason: `gh ${args.join(' ')} exited ${status ?? 'without status'}` };
|
|
110
111
|
}
|
|
111
|
-
function collectSources(root, repositorySecrets, environmentSecrets = {}) {
|
|
112
|
-
const workspaceDirs = getWorkspacePackages(root).map((pkg) => pkg.path);
|
|
112
|
+
function collectSources(root, workspaceDirs, localSecrets, repositorySecrets, environmentSecrets = {}) {
|
|
113
113
|
const workerSecrets = workerSecretNames(root, workspaceDirs);
|
|
114
114
|
const workflowSecrets = workflowSecretNames(root);
|
|
115
|
-
const localCommands = localSecretCommandNames(root);
|
|
116
115
|
const envNames = [
|
|
117
|
-
...new Set([
|
|
116
|
+
...new Set([
|
|
117
|
+
...Object.values(workerSecrets).flatMap((names) => [...names]),
|
|
118
|
+
...workflowSecrets,
|
|
119
|
+
...localSecrets.map((secret) => secret.name),
|
|
120
|
+
]),
|
|
118
121
|
];
|
|
119
122
|
return {
|
|
120
123
|
workerSecrets,
|
|
121
124
|
workflowSecrets,
|
|
122
125
|
secretNames: secretNameMapping(root, envNames),
|
|
123
|
-
|
|
126
|
+
localSecrets,
|
|
124
127
|
repositorySecrets,
|
|
125
128
|
environmentSecrets,
|
|
126
129
|
};
|
|
@@ -141,47 +144,89 @@ function describe(row, scopeWidth) {
|
|
|
141
144
|
].join(' ');
|
|
142
145
|
}
|
|
143
146
|
/**
|
|
144
|
-
*
|
|
145
|
-
*
|
|
146
|
-
*
|
|
147
|
-
*
|
|
147
|
+
* Everything `status` reports, read from git, `gh` and the checkout before a
|
|
148
|
+
* single line is printed. Gathering ahead of rendering is what lets one run
|
|
149
|
+
* answer either a human or a machine from exactly the same facts: `--json`
|
|
150
|
+
* cannot drift from the table because there is nothing for it to drift from.
|
|
148
151
|
*/
|
|
149
|
-
|
|
152
|
+
async function gatherSecretsStatus(root, options) {
|
|
150
153
|
const resolved = resolveRepository(root, options.repo);
|
|
151
|
-
if (!resolved.ok)
|
|
152
|
-
|
|
153
|
-
return 1;
|
|
154
|
-
}
|
|
154
|
+
if (!resolved.ok)
|
|
155
|
+
return resolved;
|
|
155
156
|
const { repo, source } = resolved.choice;
|
|
156
157
|
const repositorySecrets = readRepositorySecrets(repo);
|
|
157
|
-
if (!repositorySecrets.ok)
|
|
158
|
-
|
|
159
|
-
return 1;
|
|
160
|
-
}
|
|
158
|
+
if (!repositorySecrets.ok)
|
|
159
|
+
return repositorySecrets;
|
|
161
160
|
// The workflows decide which environments a job reads; an explicitly asked
|
|
162
161
|
// for one is shown too, so an operator can inspect a scope before a workflow
|
|
163
162
|
// binds it.
|
|
164
163
|
const boundEnvironments = workflowEnvironments(root);
|
|
165
164
|
const asked = options.env !== undefined && !boundEnvironments.includes(options.env) ? [options.env] : [];
|
|
166
165
|
const environmentSecrets = {};
|
|
167
|
-
const
|
|
168
|
-
for (const
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
166
|
+
const environments = [];
|
|
167
|
+
for (const name of [...boundEnvironments, ...asked]) {
|
|
168
|
+
const bound = boundEnvironments.includes(name);
|
|
169
|
+
const held = readRepositorySecrets(repo, name);
|
|
170
|
+
// An environment that cannot be read is reported, never assumed empty:
|
|
171
|
+
// guessing produces a refusal about a value that may well be set.
|
|
172
|
+
if (!held.ok) {
|
|
173
|
+
environments.push({ name, bound, readable: false, reason: held.reason });
|
|
174
|
+
continue;
|
|
175
|
+
}
|
|
176
|
+
environmentSecrets[name] = held.names;
|
|
177
|
+
environments.push({ name, bound, readable: true, secretCount: held.names.length });
|
|
178
|
+
}
|
|
179
|
+
const workspaceDirs = getWorkspacePackages(root).map((pkg) => pkg.path);
|
|
180
|
+
const stageScopes = workerSecretStages(root, workspaceDirs);
|
|
181
|
+
if (!stageScopes.ok)
|
|
182
|
+
return stageScopes;
|
|
183
|
+
// A `smoo.secrets` declaration smoo cannot read is a refusal, not an empty
|
|
184
|
+
// list: reporting "nothing is fetchable locally" for a manifest that says
|
|
185
|
+
// otherwise is the confident wrong answer.
|
|
186
|
+
const localSecrets = await localSecretGroups(root);
|
|
187
|
+
if (!localSecrets.ok)
|
|
188
|
+
return localSecrets;
|
|
189
|
+
return {
|
|
190
|
+
ok: true,
|
|
191
|
+
document: projectSecretsStatus({
|
|
192
|
+
repository: { repo, source, secretCount: repositorySecrets.names.length },
|
|
193
|
+
environments,
|
|
194
|
+
rows: reconcileSecrets(collectSources(root, workspaceDirs, localSecrets.secrets, repositorySecrets.names, environmentSecrets)),
|
|
195
|
+
stageScopes: stageScopes.byWorker,
|
|
196
|
+
}),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Print every known secret and the scopes holding it, or - with `json` - the
|
|
201
|
+
* same facts as one document on stdout and nothing else. Exit code 1 when a
|
|
202
|
+
* workflow promises a value no scope a bound job reads can supply — that
|
|
203
|
+
* combination is the one that fails a deploy, and it fails talking about the
|
|
204
|
+
* value rather than the missing secret. `--json` reports that refusal in
|
|
205
|
+
* `unsatisfied` and still exits 1: a machine-readable status that always
|
|
206
|
+
* succeeded would be a status nobody could gate on.
|
|
207
|
+
*/
|
|
208
|
+
export async function secretsStatus(root, options) {
|
|
209
|
+
const gathered = await gatherSecretsStatus(root, options);
|
|
210
|
+
if (!gathered.ok) {
|
|
211
|
+
console.error(gathered.reason);
|
|
212
|
+
return 1;
|
|
213
|
+
}
|
|
214
|
+
const { document } = gathered;
|
|
215
|
+
const { repo, source } = document.repository;
|
|
216
|
+
if (options.json) {
|
|
217
|
+
console.log(stringifySecretsStatusDocument(document));
|
|
218
|
+
return document.unsatisfied.length > 0 ? 1 : 0;
|
|
174
219
|
}
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
const
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
for (const [environment, names] of Object.entries(environmentSecrets)) {
|
|
181
|
-
console.log(`environment ${environment}, holding ${names.length} secrets`);
|
|
220
|
+
const rows = document.secrets;
|
|
221
|
+
console.log(`repository ${repo} (${source}), holding ${document.repository.secretCount} secrets`);
|
|
222
|
+
for (const environment of document.environments) {
|
|
223
|
+
if (environment.readable)
|
|
224
|
+
console.log(`environment ${environment.name}, holding ${environment.secretCount} secrets`);
|
|
182
225
|
}
|
|
183
|
-
for (const
|
|
184
|
-
|
|
226
|
+
for (const environment of document.environments) {
|
|
227
|
+
if (environment.readable)
|
|
228
|
+
continue;
|
|
229
|
+
console.log(`environment ${environment.name} could not be read, so nothing below claims what it holds: ${environment.reason}`);
|
|
185
230
|
}
|
|
186
231
|
const scopeWidth = Math.max('held by'.length, ...rows.map((row) => heldBy(row).length));
|
|
187
232
|
console.log(`${'held by'.padEnd(scopeWidth)} ${'name'.padEnd(32)} workflow local declared by`);
|
|
@@ -195,6 +240,22 @@ export function secretsStatus(root, options) {
|
|
|
195
240
|
'declare it in smoo.github.deploySecrets to have CI supply it.');
|
|
196
241
|
}
|
|
197
242
|
}
|
|
243
|
+
// A declared `group` that disagrees with what this repository's own
|
|
244
|
+
// declarations derive is the override working — and a silent override is
|
|
245
|
+
// how the next reader loses an hour wondering why a `.npmrc` credential
|
|
246
|
+
// resolves at shell entry. It is stated here, where a human is already
|
|
247
|
+
// looking at every declared secret.
|
|
248
|
+
const overridden = rows.filter((row) => row.localGroup !== undefined && row.localGroup.resolves !== row.localGroup.derived);
|
|
249
|
+
if (overridden.length > 0) {
|
|
250
|
+
console.log('');
|
|
251
|
+
for (const row of overridden) {
|
|
252
|
+
const local = row.localGroup;
|
|
253
|
+
if (local === undefined)
|
|
254
|
+
continue;
|
|
255
|
+
console.log(`note: ${row.name} declares group \`${local.resolves}\`, overriding the \`${local.derived}\` this ` +
|
|
256
|
+
`repository's declarations derive; \`smoo secrets run ${local.resolves} <command>\` is what resolves it.`);
|
|
257
|
+
}
|
|
258
|
+
}
|
|
198
259
|
// A value an environment holds is not a value to duplicate at repository
|
|
199
260
|
// scope: offering it invites two sources of truth for one credential, and
|
|
200
261
|
// the table above already shows where it lives. Only a name no scope holds
|
|
@@ -209,23 +270,21 @@ export function secretsStatus(root, options) {
|
|
|
209
270
|
console.log(`set them all, one prompt each: smoo secrets set -R ${repo}`);
|
|
210
271
|
console.log(`set one: smoo secrets set ${needed[0]?.repositorySecret ?? 'NAME'} -R ${repo}`);
|
|
211
272
|
}
|
|
212
|
-
|
|
213
|
-
if (unsatisfied.length === 0)
|
|
273
|
+
if (document.unsatisfied.length === 0)
|
|
214
274
|
return 0;
|
|
215
275
|
console.log('');
|
|
216
|
-
for (const
|
|
217
|
-
const
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
console.error(` smoo secrets set ${row.repositorySecret} -R ${repo}`);
|
|
276
|
+
for (const { name, repositorySecret, missingIn } of document.unsatisfied) {
|
|
277
|
+
const scopes = missingIn.length > 0 ? missingIn.join(', ') : 'the repository';
|
|
278
|
+
console.error(`missing: ${name} — a workflow passes secrets.${name} and no value exists in ${scopes}.`);
|
|
279
|
+
if (missingIn.length === 0) {
|
|
280
|
+
console.error(` smoo secrets set ${repositorySecret} -R ${repo}`);
|
|
222
281
|
continue;
|
|
223
282
|
}
|
|
224
|
-
for (const environment of
|
|
283
|
+
for (const environment of missingIn) {
|
|
225
284
|
const named = environment.includes(' ') ? `'${environment}'` : environment;
|
|
226
|
-
console.error(` smoo secrets set ${
|
|
285
|
+
console.error(` smoo secrets set ${repositorySecret} -R ${repo} --env ${named}`);
|
|
227
286
|
}
|
|
228
|
-
console.error(` or one value for every environment: smoo secrets set ${
|
|
287
|
+
console.error(` or one value for every environment: smoo secrets set ${repositorySecret} -R ${repo}`);
|
|
229
288
|
}
|
|
230
289
|
return 1;
|
|
231
290
|
}
|
|
@@ -290,7 +349,13 @@ export async function secretsSet(root, name, options) {
|
|
|
290
349
|
}
|
|
291
350
|
environmentSecrets[environment] = inEnvironment.names;
|
|
292
351
|
}
|
|
293
|
-
const
|
|
352
|
+
const workspaceDirs = getWorkspacePackages(root).map((pkg) => pkg.path);
|
|
353
|
+
const local = await localSecretGroups(root);
|
|
354
|
+
if (!local.ok) {
|
|
355
|
+
console.error(local.reason);
|
|
356
|
+
return 1;
|
|
357
|
+
}
|
|
358
|
+
const rows = reconcileSecrets(collectSources(root, workspaceDirs, local.secrets, held.names, environmentSecrets));
|
|
294
359
|
const needed = environment === undefined ? secretsNeedingValues(rows) : secretsMissingInEnvironment(rows, environment);
|
|
295
360
|
const target = environment === undefined ? `${repo} (${source})` : `environment ${environment} on ${repo} (${source})`;
|
|
296
361
|
if (needed.length === 0) {
|
|
@@ -349,7 +414,15 @@ export async function secretsSync(root, options) {
|
|
|
349
414
|
}
|
|
350
415
|
const { repo } = resolved.choice;
|
|
351
416
|
const environment = options.env;
|
|
352
|
-
const
|
|
417
|
+
const local = await localSecretGroups(root);
|
|
418
|
+
if (!local.ok) {
|
|
419
|
+
console.error(local.reason);
|
|
420
|
+
return 1;
|
|
421
|
+
}
|
|
422
|
+
// Every group: a repository secret store holds the credential whatever
|
|
423
|
+
// resolves it locally, and a `smoo secrets run` group is about which
|
|
424
|
+
// command pays the provider prompt, not about which values CI needs.
|
|
425
|
+
const names = local.secrets.map((secret) => secret.name);
|
|
353
426
|
if (names.length === 0) {
|
|
354
427
|
console.error('smoo.secrets declares no fetch commands; nothing to sync.');
|
|
355
428
|
return 1;
|
package/dist/secrets/index.d.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Reads the three places a repository's secrets are described, all of which
|
|
3
|
+
* smoo already owns:
|
|
4
4
|
*
|
|
5
5
|
* 1. **Workers declare names.** `.dev.vars.example` per wrangler project — the
|
|
6
|
-
* file `wrangler types --env-file` reads and `prepare-env` prompts from
|
|
6
|
+
* file `wrangler types --env-file` reads and `prepare-env` prompts from —
|
|
7
|
+
* and `smoo.wrangler.secretStages` beside it says which stages each name
|
|
8
|
+
* belongs to.
|
|
7
9
|
* 2. **Workflows declare where values come from.** `smoo.github.deploySecrets`
|
|
8
10
|
* and `e2eSecrets` map an env name to a repository secret, and the managed
|
|
9
11
|
* workflows render exactly those into the job environment.
|
|
@@ -17,58 +19,12 @@
|
|
|
17
19
|
* `STRIPE_PUBLISHABLE_KEY`, `ci.yml` passes `secrets.STRIPE_PUBLISHABLE_KEY`,
|
|
18
20
|
* the repository holds no such secret, and the empty value surfaces as the
|
|
19
21
|
* payment library's own `publishable_key_mismatch` refusal.
|
|
22
|
+
*
|
|
23
|
+
* The join itself is ./status.ts: pure, and the owner of the document type
|
|
24
|
+
* these readings are projected into.
|
|
20
25
|
*/
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
name: string;
|
|
24
|
-
/** The repository secret this env name reads from, by convention or declaration. */
|
|
25
|
-
repositorySecret: string;
|
|
26
|
-
/** Wrangler projects whose `.dev.vars.example` declares it. */
|
|
27
|
-
declaredByWorkers: string[];
|
|
28
|
-
/** True when a managed workflow renders `secrets.<name>` into a job. */
|
|
29
|
-
suppliedByWorkflow: boolean;
|
|
30
|
-
/** True when `smoo.secrets` can fetch it for a developer shell. */
|
|
31
|
-
fetchableLocally: boolean;
|
|
32
|
-
/** True when the repository holds a secret of this name. */
|
|
33
|
-
onRepository: boolean;
|
|
34
|
-
/**
|
|
35
|
-
* GitHub Environments holding their own value for this name. A job bound to
|
|
36
|
-
* an environment reads that value in preference to the repository's, which
|
|
37
|
-
* is how one name carries test credentials on a preview stage and live ones
|
|
38
|
-
* in production.
|
|
39
|
-
*/
|
|
40
|
-
heldByEnvironment: string[];
|
|
41
|
-
}
|
|
42
|
-
export interface SecretSources {
|
|
43
|
-
/** Worker label -> env names it declares. */
|
|
44
|
-
workerSecrets: Record<string, readonly string[]>;
|
|
45
|
-
/** Env names a managed workflow passes into a job. */
|
|
46
|
-
workflowSecrets: readonly string[];
|
|
47
|
-
/** Env name -> repository secret, by convention with declared exceptions. */
|
|
48
|
-
secretNames: Readonly<Record<string, string>>;
|
|
49
|
-
/** Env names `smoo.secrets` can fetch locally. */
|
|
50
|
-
localCommands: readonly string[];
|
|
51
|
-
/** Repository secret names GitHub currently holds. */
|
|
52
|
-
repositorySecrets: readonly string[];
|
|
53
|
-
/** Environment name -> secret names that environment holds. */
|
|
54
|
-
environmentSecrets?: Readonly<Record<string, readonly string[]>>;
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* One row per name known to any source, sorted, so a reader sees the whole
|
|
58
|
-
* picture rather than one source's view of it.
|
|
59
|
-
*/
|
|
60
|
-
export declare function reconcileSecrets(sources: SecretSources): SecretRow[];
|
|
61
|
-
/**
|
|
62
|
-
* The rows a CI deploy cannot satisfy: a Worker declares the name, a workflow
|
|
63
|
-
* promises to pass it, and the repository has no value to pass. Reported
|
|
64
|
-
* separately from "declared but not wired into any workflow", because the
|
|
65
|
-
* remedies differ — set a secret, versus declare it in `smoo.github`.
|
|
66
|
-
*/
|
|
67
|
-
export declare function unsatisfiedSecrets(rows: readonly SecretRow[], boundEnvironments?: readonly string[]): SecretRow[];
|
|
68
|
-
/** Bound environments that lack their own value and cannot fall back to the repository. */
|
|
69
|
-
export declare function environmentsMissing(row: SecretRow, boundEnvironments: readonly string[]): string[];
|
|
70
|
-
/** Worker-declared names no managed workflow passes: a CI deploy will run without them. */
|
|
71
|
-
export declare function unwiredSecrets(rows: readonly SecretRow[]): SecretRow[];
|
|
26
|
+
import { type SecretStageMap } from '../wrangler/stage-secrets.js';
|
|
27
|
+
import { type GroupedSecret } from './resolver.js';
|
|
72
28
|
/** Env names the managed workflows pass into a job, from the declarations that render them. */
|
|
73
29
|
export declare function workflowSecretNames(root: string): string[];
|
|
74
30
|
/**
|
|
@@ -87,8 +43,38 @@ export declare function workflowEnvironments(root: string): string[];
|
|
|
87
43
|
export declare function secretNameMapping(root: string, envNames: readonly string[]): Record<string, string>;
|
|
88
44
|
/** Every wrangler project's declared secret names, keyed by the project directory. */
|
|
89
45
|
export declare function workerSecretNames(root: string, workspaceDirs: readonly string[]): Record<string, string[]>;
|
|
90
|
-
/**
|
|
91
|
-
|
|
46
|
+
/**
|
|
47
|
+
* Every wrangler project's `smoo.wrangler.secretStages`, keyed by the project
|
|
48
|
+
* directory — the same keys `workerSecretNames` uses, so a name's scopes and
|
|
49
|
+
* its declarations are joined on one label.
|
|
50
|
+
*
|
|
51
|
+
* A malformed block is a refusal rather than a throw: it is the same class of
|
|
52
|
+
* problem as a repository `gh` will not talk to, and `smoo secrets status`
|
|
53
|
+
* reports those instead of dying with a stack. Silently reading it as "no
|
|
54
|
+
* scopes" is the one thing this must not do — that is the direction where
|
|
55
|
+
* every secret quietly reaches every stage.
|
|
56
|
+
*/
|
|
57
|
+
export declare function workerSecretStages(root: string, workspaceDirs: readonly string[]): {
|
|
58
|
+
ok: true;
|
|
59
|
+
byWorker: Record<string, SecretStageMap>;
|
|
60
|
+
} | {
|
|
61
|
+
ok: false;
|
|
62
|
+
reason: string;
|
|
63
|
+
};
|
|
64
|
+
/**
|
|
65
|
+
* Every `smoo.secrets` entry with the group that resolves it, sorted by name,
|
|
66
|
+
* or a refusal naming the declaration that could not be read. The groups come
|
|
67
|
+
* from ./resolver.ts — the same file shell entry routes with — so a status
|
|
68
|
+
* table and a `smoo secrets run` cannot disagree about which group resolves
|
|
69
|
+
* a name.
|
|
70
|
+
*/
|
|
71
|
+
export declare function localSecretGroups(root: string): Promise<{
|
|
72
|
+
ok: true;
|
|
73
|
+
secrets: GroupedSecret[];
|
|
74
|
+
} | {
|
|
75
|
+
ok: false;
|
|
76
|
+
reason: string;
|
|
77
|
+
}>;
|
|
92
78
|
/**
|
|
93
79
|
* Resolve one declared secret through its command. The value is returned, never
|
|
94
80
|
* logged: callers hand it to `gh secret set` over stdin.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/secrets/index.ts"],"names":[],"mappings":"AAAA
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/secrets/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAQH,OAAO,EAAsB,KAAK,cAAc,EAAE,MAAM,8BAA8B,CAAC;AACvF,OAAO,EAAE,KAAK,aAAa,EAAoB,MAAM,eAAe,CAAC;AAErE,+FAA+F;AAC/F,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAkB1D;AAED;;;;;;;GAOG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAW3D;AAoDD;;;GAGG;AACH,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,QAAQ,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAMnG;AAED,sFAAsF;AACtF,wBAAgB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,aAAa,EAAE,SAAS,MAAM,EAAE,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,EAAE,CAAC,CAS1G;AAED;;;;;;;;;;GAUG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,aAAa,EAAE,SAAS,MAAM,EAAE,GAC/B;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC,MAAM,EAAE,cAAc,CAAC,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAYxF;AAED;;;;;;GAMG;AACH,wBAAsB,iBAAiB,CACrC,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,OAAO,EAAE,aAAa,EAAE,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAOjF;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAC9B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,GACX;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAqB7D"}
|
package/dist/secrets/index.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
2
|
+
* Reads the three places a repository's secrets are described, all of which
|
|
3
|
+
* smoo already owns:
|
|
4
4
|
*
|
|
5
5
|
* 1. **Workers declare names.** `.dev.vars.example` per wrangler project — the
|
|
6
|
-
* file `wrangler types --env-file` reads and `prepare-env` prompts from
|
|
6
|
+
* file `wrangler types --env-file` reads and `prepare-env` prompts from —
|
|
7
|
+
* and `smoo.wrangler.secretStages` beside it says which stages each name
|
|
8
|
+
* belongs to.
|
|
7
9
|
* 2. **Workflows declare where values come from.** `smoo.github.deploySecrets`
|
|
8
10
|
* and `e2eSecrets` map an env name to a repository secret, and the managed
|
|
9
11
|
* workflows render exactly those into the job environment.
|
|
@@ -17,6 +19,9 @@
|
|
|
17
19
|
* `STRIPE_PUBLISHABLE_KEY`, `ci.yml` passes `secrets.STRIPE_PUBLISHABLE_KEY`,
|
|
18
20
|
* the repository holds no such secret, and the empty value surfaces as the
|
|
19
21
|
* payment library's own `publishable_key_mismatch` refusal.
|
|
22
|
+
*
|
|
23
|
+
* The join itself is ./status.ts: pure, and the owner of the document type
|
|
24
|
+
* these readings are projected into.
|
|
20
25
|
*/
|
|
21
26
|
import { spawnSync } from 'node:child_process';
|
|
22
27
|
import { existsSync, readdirSync, readFileSync } from 'node:fs';
|
|
@@ -24,78 +29,8 @@ import { join } from 'node:path';
|
|
|
24
29
|
import { repositoryOwnerFromUrl, repositorySecretMapping } from '../lib/secret-names.js';
|
|
25
30
|
import { readPackageJsonObject, repositoryInfo } from '../lib/workspace.js';
|
|
26
31
|
import { parseDevVarsExample } from '../wrangler/prepare-env.js';
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
* picture rather than one source's view of it.
|
|
30
|
-
*/
|
|
31
|
-
export function reconcileSecrets(sources) {
|
|
32
|
-
const declaredByAnyWorker = Object.values(sources.workerSecrets).flatMap((names) => [...names]);
|
|
33
|
-
const names = new Set([...declaredByAnyWorker, ...sources.workflowSecrets, ...sources.localCommands]);
|
|
34
|
-
// A repository secret that already carries a known env name is that name's
|
|
35
|
-
// row, not a row of its own: listing ACME_GITHUB_CLIENT_SECRET beside
|
|
36
|
-
// GITHUB_CLIENT_SECRET would report one value as two secrets, one of them
|
|
37
|
-
// permanently "declared by nothing".
|
|
38
|
-
const carriesKnownEnvName = new Set(names.size > 0 ? [...names].map((name) => sources.secretNames[name] ?? name) : []);
|
|
39
|
-
// A value only an environment holds is still a value: leaving it out of the
|
|
40
|
-
// rows is how an operator ends up hunting for a secret that is already set.
|
|
41
|
-
const addUndeclared = (secret) => {
|
|
42
|
-
if (!carriesKnownEnvName.has(secret))
|
|
43
|
-
names.add(secret);
|
|
44
|
-
};
|
|
45
|
-
for (const secret of sources.repositorySecrets)
|
|
46
|
-
addUndeclared(secret);
|
|
47
|
-
for (const held of Object.values(sources.environmentSecrets ?? {})) {
|
|
48
|
-
for (const secret of held)
|
|
49
|
-
addUndeclared(secret);
|
|
50
|
-
}
|
|
51
|
-
return [...names]
|
|
52
|
-
.sort((left, right) => left.localeCompare(right))
|
|
53
|
-
.map((name) => ({
|
|
54
|
-
name,
|
|
55
|
-
repositorySecret: sources.secretNames[name] ?? name,
|
|
56
|
-
declaredByWorkers: Object.entries(sources.workerSecrets)
|
|
57
|
-
.filter(([, declared]) => declared.includes(name))
|
|
58
|
-
.map(([label]) => label)
|
|
59
|
-
.sort((left, right) => left.localeCompare(right)),
|
|
60
|
-
suppliedByWorkflow: sources.workflowSecrets.includes(name),
|
|
61
|
-
fetchableLocally: sources.localCommands.includes(name),
|
|
62
|
-
onRepository: sources.repositorySecrets.includes(sources.secretNames[name] ?? name),
|
|
63
|
-
heldByEnvironment: Object.entries(sources.environmentSecrets ?? {})
|
|
64
|
-
.filter(([, held]) => held.includes(sources.secretNames[name] ?? name))
|
|
65
|
-
.map(([environment]) => environment)
|
|
66
|
-
.sort((left, right) => left.localeCompare(right)),
|
|
67
|
-
}));
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* The rows a CI deploy cannot satisfy: a Worker declares the name, a workflow
|
|
71
|
-
* promises to pass it, and the repository has no value to pass. Reported
|
|
72
|
-
* separately from "declared but not wired into any workflow", because the
|
|
73
|
-
* remedies differ — set a secret, versus declare it in `smoo.github`.
|
|
74
|
-
*/
|
|
75
|
-
export function unsatisfiedSecrets(rows, boundEnvironments = []) {
|
|
76
|
-
return rows.filter((row) => {
|
|
77
|
-
if (!row.suppliedByWorkflow)
|
|
78
|
-
return false;
|
|
79
|
-
if (row.onRepository)
|
|
80
|
-
return false;
|
|
81
|
-
// With no environment bound, the repository is the only scope a job reads.
|
|
82
|
-
// With environments bound, each one can carry the value instead - so the
|
|
83
|
-
// name is satisfied only when every bound environment holds it.
|
|
84
|
-
if (boundEnvironments.length === 0)
|
|
85
|
-
return true;
|
|
86
|
-
return !boundEnvironments.every((environment) => row.heldByEnvironment.includes(environment));
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
/** Bound environments that lack their own value and cannot fall back to the repository. */
|
|
90
|
-
export function environmentsMissing(row, boundEnvironments) {
|
|
91
|
-
if (row.onRepository)
|
|
92
|
-
return [];
|
|
93
|
-
return boundEnvironments.filter((environment) => !row.heldByEnvironment.includes(environment));
|
|
94
|
-
}
|
|
95
|
-
/** Worker-declared names no managed workflow passes: a CI deploy will run without them. */
|
|
96
|
-
export function unwiredSecrets(rows) {
|
|
97
|
-
return rows.filter((row) => row.declaredByWorkers.length > 0 && !row.suppliedByWorkflow);
|
|
98
|
-
}
|
|
32
|
+
import { readSecretStageMap } from '../wrangler/stage-secrets.js';
|
|
33
|
+
import { readSecretGroups } from './resolver.js';
|
|
99
34
|
/** Env names the managed workflows pass into a job, from the declarations that render them. */
|
|
100
35
|
export function workflowSecretNames(root) {
|
|
101
36
|
const manifest = readPackageJsonObject(join(root, 'package.json'));
|
|
@@ -222,10 +157,48 @@ export function workerSecretNames(root, workspaceDirs) {
|
|
|
222
157
|
}
|
|
223
158
|
return byWorker;
|
|
224
159
|
}
|
|
225
|
-
/**
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
160
|
+
/**
|
|
161
|
+
* Every wrangler project's `smoo.wrangler.secretStages`, keyed by the project
|
|
162
|
+
* directory — the same keys `workerSecretNames` uses, so a name's scopes and
|
|
163
|
+
* its declarations are joined on one label.
|
|
164
|
+
*
|
|
165
|
+
* A malformed block is a refusal rather than a throw: it is the same class of
|
|
166
|
+
* problem as a repository `gh` will not talk to, and `smoo secrets status`
|
|
167
|
+
* reports those instead of dying with a stack. Silently reading it as "no
|
|
168
|
+
* scopes" is the one thing this must not do — that is the direction where
|
|
169
|
+
* every secret quietly reaches every stage.
|
|
170
|
+
*/
|
|
171
|
+
export function workerSecretStages(root, workspaceDirs) {
|
|
172
|
+
const byWorker = {};
|
|
173
|
+
for (const dir of workspaceDirs) {
|
|
174
|
+
if (!existsSync(join(root, dir, '.dev.vars.example')))
|
|
175
|
+
continue;
|
|
176
|
+
try {
|
|
177
|
+
const scopes = readSecretStageMap(join(root, dir));
|
|
178
|
+
if (Object.keys(scopes).length > 0)
|
|
179
|
+
byWorker[dir] = scopes;
|
|
180
|
+
}
|
|
181
|
+
catch (error) {
|
|
182
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
return { ok: true, byWorker };
|
|
186
|
+
}
|
|
187
|
+
/**
|
|
188
|
+
* Every `smoo.secrets` entry with the group that resolves it, sorted by name,
|
|
189
|
+
* or a refusal naming the declaration that could not be read. The groups come
|
|
190
|
+
* from ./resolver.ts — the same file shell entry routes with — so a status
|
|
191
|
+
* table and a `smoo secrets run` cannot disagree about which group resolves
|
|
192
|
+
* a name.
|
|
193
|
+
*/
|
|
194
|
+
export async function localSecretGroups(root) {
|
|
195
|
+
try {
|
|
196
|
+
const secrets = await readSecretGroups(root);
|
|
197
|
+
return { ok: true, secrets: secrets.sort((left, right) => left.name.localeCompare(right.name)) };
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
return { ok: false, reason: error instanceof Error ? error.message : String(error) };
|
|
201
|
+
}
|
|
229
202
|
}
|
|
230
203
|
/**
|
|
231
204
|
* Resolve one declared secret through its command. The value is returned, never
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The bootstrap secret resolver, as the CLI sees it.
|
|
3
|
+
*
|
|
4
|
+
* `managed/raw/tooling/direnv/secret-references.ts` holds the one
|
|
5
|
+
* implementation of THE RULE — which group resolves where, and what CI and a
|
|
6
|
+
* cowshed workspace refuse instead. A managed repository runs the copy smoo
|
|
7
|
+
* writes into its own `tooling/direnv/secret-references.ts` at shell entry;
|
|
8
|
+
* `smoo secrets run` runs THIS package's copy of the same file. One
|
|
9
|
+
* implementation, so the command and the shell cannot disagree about a group.
|
|
10
|
+
*
|
|
11
|
+
* It is loaded, not statically imported, and that is forced rather than
|
|
12
|
+
* chosen: the file is a raw bootstrap script that must load before any
|
|
13
|
+
* workspace package and before the Typia transform exists, so it lives
|
|
14
|
+
* outside `src` — outside this package's compiled `rootDir`, which a static
|
|
15
|
+
* import may not cross. A dynamic `import()` reaches it in both layouts
|
|
16
|
+
* (`src/secrets/` and `dist/secrets/` are each two directories below the
|
|
17
|
+
* package root), and typia validates what comes back: the same boundary
|
|
18
|
+
* ../monorepo/packed-package.ts puts around a foreign module. `require` is
|
|
19
|
+
* not the alternative — under a Bun loader plugin, which this package's own
|
|
20
|
+
* tests preload, every module is async and `require` of one throws.
|
|
21
|
+
*/
|
|
22
|
+
/** A declared `smoo.secrets` entry with its group settled. */
|
|
23
|
+
export interface GroupedSecret {
|
|
24
|
+
name: string;
|
|
25
|
+
/** The group that resolves it: the declared one when stated, `derivedGroup` otherwise. */
|
|
26
|
+
group: string;
|
|
27
|
+
/**
|
|
28
|
+
* What the repository's own declarations imply: `registry` for a variable
|
|
29
|
+
* `.npmrc` interpolates, `nx-cache` for the declared cache token, `shell`
|
|
30
|
+
* for everything else. Differs from `group` exactly when an entry overrides
|
|
31
|
+
* the derivation.
|
|
32
|
+
*/
|
|
33
|
+
derivedGroup: string;
|
|
34
|
+
}
|
|
35
|
+
/** A declared secret a request declined to resolve, and how to supply it. */
|
|
36
|
+
export interface DeferredSecret {
|
|
37
|
+
name: string;
|
|
38
|
+
group: string;
|
|
39
|
+
guidance: string;
|
|
40
|
+
}
|
|
41
|
+
/** What belongs in a child's environment, and what was deliberately left out of it. */
|
|
42
|
+
export interface SecretResolution {
|
|
43
|
+
values: Record<string, string>;
|
|
44
|
+
deferred: DeferredSecret[];
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Every `smoo.secrets` entry of the repository at `root`, each with the group
|
|
48
|
+
* that resolves it. Rejects with the offending declaration named — never a
|
|
49
|
+
* value — when the manifest or `.npmrc` cannot be read.
|
|
50
|
+
*/
|
|
51
|
+
export declare function readSecretGroups(root: string): Promise<GroupedSecret[]>;
|
|
52
|
+
/**
|
|
53
|
+
* Resolve exactly one group's secrets for one child process. Everything
|
|
54
|
+
* outside the group comes back deferred rather than resolved, so a run for
|
|
55
|
+
* one group never triggers another group's provider command. Rejects with the
|
|
56
|
+
* resolver's aggregated refusal, which names variables and groups only.
|
|
57
|
+
*/
|
|
58
|
+
export declare function resolveSecretGroup(root: string, group: string): Promise<SecretResolution>;
|
|
59
|
+
//# sourceMappingURL=resolver.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"resolver.d.ts","sourceRoot":"","sources":["../../src/secrets/resolver.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAKH,8DAA8D;AAC9D,MAAM,WAAW,aAAa;IAC5B,IAAI,EAAE,MAAM,CAAC;IACb,0FAA0F;IAC1F,KAAK,EAAE,MAAM,CAAC;IACd;;;;;OAKG;IACH,YAAY,EAAE,MAAM,CAAC;CACtB;AAED,6EAA6E;AAC7E,MAAM,WAAW,cAAc;IAC7B,IAAI,EAAE,MAAM,CAAC;IACb,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,uFAAuF;AACvF,MAAM,WAAW,gBAAgB;IAC/B,MAAM,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IAC/B,QAAQ,EAAE,cAAc,EAAE,CAAC;CAC5B;AAoDD;;;;GAIG;AACH,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,aAAa,EAAE,CAAC,CAO7E;AAED;;;;;GAKG;AACH,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC,CAO/F"}
|