@smoothbricks/cli 0.11.15 → 0.11.17

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.
Files changed (70) hide show
  1. package/README.md +43 -0
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +44 -0
  4. package/dist/github-ci/index.d.ts +1 -1
  5. package/dist/github-ci/index.d.ts.map +1 -1
  6. package/dist/github-ci/index.js +7 -10
  7. package/dist/lib/json.d.ts +9 -0
  8. package/dist/lib/json.d.ts.map +1 -1
  9. package/dist/lib/json.js +65 -53
  10. package/dist/lib/secret-names.d.ts +35 -0
  11. package/dist/lib/secret-names.d.ts.map +1 -0
  12. package/dist/lib/secret-names.js +61 -0
  13. package/dist/monorepo/ci-workflow.d.ts.map +1 -1
  14. package/dist/monorepo/ci-workflow.js +15 -6
  15. package/dist/monorepo/managed-files.d.ts +9 -0
  16. package/dist/monorepo/managed-files.d.ts.map +1 -1
  17. package/dist/monorepo/managed-files.js +28 -2
  18. package/dist/monorepo/publish-workflow.d.ts +10 -1
  19. package/dist/monorepo/publish-workflow.d.ts.map +1 -1
  20. package/dist/monorepo/publish-workflow.js +110 -22
  21. package/dist/release/index.d.ts +1 -1
  22. package/dist/release/index.d.ts.map +1 -1
  23. package/dist/release/index.js +26 -4
  24. package/dist/release/private-npm.d.ts +5 -0
  25. package/dist/release/private-npm.d.ts.map +1 -1
  26. package/dist/release/private-npm.js +26 -5
  27. package/dist/secrets/commands.d.ts +45 -0
  28. package/dist/secrets/commands.d.ts.map +1 -0
  29. package/dist/secrets/commands.js +189 -0
  30. package/dist/secrets/index.d.ts +83 -0
  31. package/dist/secrets/index.d.ts.map +1 -0
  32. package/dist/secrets/index.js +149 -0
  33. package/dist/wrangler/deploy-stage.d.ts +13 -2
  34. package/dist/wrangler/deploy-stage.d.ts.map +1 -1
  35. package/dist/wrangler/deploy-stage.js +39 -54
  36. package/dist/wrangler/deployed-version.d.ts +44 -0
  37. package/dist/wrangler/deployed-version.d.ts.map +1 -0
  38. package/dist/wrangler/deployed-version.js +87 -0
  39. package/dist/wrangler/live-version.d.ts +146 -0
  40. package/dist/wrangler/live-version.d.ts.map +1 -0
  41. package/dist/wrangler/live-version.js +326 -0
  42. package/managed/raw/tooling/direnv/devenv.smoo.nix +4 -2
  43. package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +4 -1
  44. package/managed/raw/tooling/direnv/toolchain-stamp.ts +88 -0
  45. package/managed/templates/github/actions/setup-devenv/action.yml +1 -1
  46. package/package.json +2 -2
  47. package/src/cli.ts +46 -1
  48. package/src/github-ci/index.test.ts +92 -8
  49. package/src/github-ci/index.ts +7 -10
  50. package/src/lib/json.ts +9 -0
  51. package/src/lib/secret-names.ts +70 -0
  52. package/src/monorepo/__tests__/ci-workflow.test.ts +27 -2
  53. package/src/monorepo/__tests__/publish-workflow.test.ts +85 -13
  54. package/src/monorepo/ci-workflow.ts +17 -6
  55. package/src/monorepo/managed-files.test.ts +26 -0
  56. package/src/monorepo/managed-files.ts +36 -2
  57. package/src/monorepo/publish-workflow.ts +135 -21
  58. package/src/monorepo/secret-references.test.ts +1 -1
  59. package/src/release/__tests__/private-npm-status.test.ts +37 -0
  60. package/src/release/__tests__/private-npm-workflow.test.ts +20 -0
  61. package/src/release/index.ts +27 -2
  62. package/src/release/private-npm.ts +30 -5
  63. package/src/secrets/commands.ts +210 -0
  64. package/src/secrets/index.test.ts +92 -0
  65. package/src/secrets/index.ts +183 -0
  66. package/src/wrangler/deploy-stage.test.ts +190 -12
  67. package/src/wrangler/deploy-stage.ts +72 -45
  68. package/src/wrangler/deployed-version.test.ts +150 -0
  69. package/src/wrangler/deployed-version.ts +130 -0
  70. package/src/wrangler/live-version.ts +363 -0
@@ -15,7 +15,8 @@ import { parseNxJsonText, parseNxProjectJsonText, parsePackageJsonText, } from '
15
15
  import { decode, run, runInteractiveStatus, runResult, runText } from '../lib/run.js';
16
16
  import { getWorkspacePackages, listPublicReleasePackages, listReleasePackages, readPackageJson, repositoryInfo, } from '../lib/workspace.js';
17
17
  export { assertPackedArtifact, releasePack, resolvePackClosure, } from './pack.js';
18
- export { assertPrivatePackage, checkPrivatePackage, isPrivatePackage, npmPackageExists as npmPackageExistsOnRegistry, npmPublishedVersionExists as npmPublishedVersionExistsOnRegistry, privateNpmPublishArgs, privateNpmUserconfigContent, publishPrivateWithDiagnostics, requirePrivateNpmRegistry, resolvePrivateNpmRegistry, selectPublishDestination, selectRegistryForPackage, withPrivateNpmUserconfig, } from './private-npm.js';
18
+ export { assertPrivatePackage, checkPrivatePackage, isPrivatePackage, npmPackageExists as npmPackageExistsOnRegistry, npmPublishedVersionExists as npmPublishedVersionExistsOnRegistry, privateNpmPublishArgs, privateNpmTokenEnvForMode, privateNpmUserconfigContent, publishPrivateWithDiagnostics, requirePrivateNpmRegistry, resolvePrivateNpmRegistry, selectPublishDestination, selectRegistryForPackage, withPrivateNpmUserconfig, } from './private-npm.js';
19
+ import { PLATFORM_TARGET_GLOBS } from '@smoothbricks/nx-plugin/workspace-config-policy';
19
20
  import { ciApiContext } from '../github-ci/api.js';
20
21
  import { readProjectTargets } from '../nx/index.js';
21
22
  import { bootstrapNpmPackages, NPM_BOOTSTRAP_DIST_TAG, NPM_BOOTSTRAP_VERSION, } from './bootstrap-npm-packages.js';
@@ -26,7 +27,7 @@ import { createOrUpdateGithubRelease, githubReleaseLookupExists, importWorkspace
26
27
  import { publishWithAuthDiagnostics } from './npm-auth.js';
27
28
  import { bumpStableReleaseToNext, collectRepairPlatformOutputs, completeReleaseAtHead as completeReleaseAtHeadWithShell, prepareReleaseCandidate, repairPendingTargets, runReleaseTag, runReleaseVersion, } from './orchestration.js';
28
29
  import { packReleaseTarball } from './pack.js';
29
- import { assertPrivatePackage, npmPackageExists as npmPackageExistsOnRegistry, npmPublishedVersionExists as npmPublishedVersionExistsOnRegistry, privateNpmPublishArgs, publishPrivateWithDiagnostics, selectPublishDestination, selectRegistryForPackage, withPrivateNpmUserconfig, } from './private-npm.js';
30
+ import { assertPrivatePackage, npmPackageExists as npmPackageExistsOnRegistry, npmPublishedVersionExists as npmPublishedVersionExistsOnRegistry, privateNpmPublishArgs, privateNpmTokenEnvForMode, publishPrivateWithDiagnostics, selectPublishDestination, selectRegistryForPackage, withPrivateNpmUserconfig, } from './private-npm.js';
30
31
  import { retagUnpublished } from './retag-unpublished.js';
31
32
  import { forgejoAuthHeaders, forgejoReleaseLookupExists, resolveSourceReleaseEndpoint, resolveSourceRepository, sourceReleaseTokenEnvNames, sourceReleaseUrl, } from './source-release.js';
32
33
  export async function releaseVersion(root, options) {
@@ -426,7 +427,12 @@ async function publishPrivatePackedPackage(root, pkg, tag, dryRun, registry) {
426
427
  }
427
428
  await withPrivateNpmUserconfig(registry, 'publish', async (userconfig) => {
428
429
  await publishPrivateWithDiagnostics(pkg, {
429
- publish: () => runNpm(root, args, { NPM_CONFIG_USERCONFIG: userconfig }),
430
+ publish: () => runNpm(root, args, {
431
+ NPM_CONFIG_USERCONFIG: userconfig,
432
+ // Env config outranks a committed project `.npmrc` auth line; see
433
+ // npmStatusEnv in private-npm.ts for why that matters here.
434
+ [`npm_config_${registry.authKey}`]: process.env[privateNpmTokenEnvForMode(registry, 'publish')] ?? '',
435
+ }),
430
436
  // The open publish userconfig already authenticates this registry,
431
437
  // so the failure probe reuses it. Opening a second read-mode
432
438
  // userconfig here demanded a read credential the publishing job need
@@ -435,6 +441,7 @@ async function publishPrivatePackedPackage(root, pkg, tag, dryRun, registry) {
435
441
  versionExists: () => npmPublishedVersionExistsOnRegistry(root, pkg.name, pkg.version, {
436
442
  registry: registry.registry,
437
443
  userconfig,
444
+ credential: { authKey: registry.authKey, tokenEnv: privateNpmTokenEnvForMode(registry, 'publish') },
438
445
  }),
439
446
  log: (message) => console.log(message),
440
447
  error: (message) => console.error(message),
@@ -782,6 +789,17 @@ function releaseRepairShell(root, platformOutputs) {
782
789
  const sourceSha = await gitHead(root);
783
790
  const outputs = platformOutputs.map((base) => join(base, sourceSha));
784
791
  if (outputs.length === 0) {
792
+ // No producer job handed outputs over, so this runner is the producer:
793
+ // build the platform legs at the checked-out ref. `build` alone gives a
794
+ // package only the legs its own graph depends on — the host one and
795
+ // whatever it declares — and the release gate demands the whole declared
796
+ // native closure, so a foreign leg it never asked for was simply absent.
797
+ console.log('Repair pending releases: building cross-platform outputs on this runner.');
798
+ await githubCiNxRunMany(root, {
799
+ targets: PLATFORM_TARGET_GLOBS.join(','),
800
+ projects: releasePackageProjects(packages),
801
+ allowEmptyProjects: true,
802
+ });
785
803
  return;
786
804
  }
787
805
  console.log(`Repair pending releases: applying cross-platform outputs from ${outputs.join(', ')}.`);
@@ -1366,7 +1384,11 @@ async function npmVersionExists(root, name, version) {
1366
1384
  assertPublicStatusAllowed(root, name);
1367
1385
  return npmPublishedVersionExistsOnRegistry(root, name, version);
1368
1386
  }
1369
- return withPrivateNpmUserconfig(registry, 'read', (userconfig) => npmPublishedVersionExistsOnRegistry(root, name, version, { registry: registry.registry, userconfig }));
1387
+ return withPrivateNpmUserconfig(registry, 'read', (userconfig) => npmPublishedVersionExistsOnRegistry(root, name, version, {
1388
+ registry: registry.registry,
1389
+ userconfig,
1390
+ credential: { authKey: registry.authKey, tokenEnv: privateNpmTokenEnvForMode(registry, 'read') },
1391
+ }));
1370
1392
  }
1371
1393
  async function npmPackageExists(root, name) {
1372
1394
  const registry = selectRegistryForPackage(root, name);
@@ -103,6 +103,11 @@ export declare function withPrivateNpmUserconfig<T>(resolved: PrivateNpmRegistry
103
103
  export interface NpmStatusOptions {
104
104
  registry?: string;
105
105
  userconfig?: string;
106
+ /** Path-scoped auth key and token env, so the credential can ride env config. */
107
+ credential?: {
108
+ authKey: string;
109
+ tokenEnv: string;
110
+ };
106
111
  }
107
112
  /**
108
113
  * Failure-aware registry status: only a genuine not-found result means absent.
@@ -1 +1 @@
1
- {"version":3,"file":"private-npm.d.ts","sourceRoot":"","sources":["../../src/release/private-npm.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"private-npm.d.ts","sourceRoot":"","sources":["../../src/release/private-npm.ts"],"names":[],"mappings":"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"}
@@ -179,7 +179,12 @@ export function resolvePrivateNpmWorkflowConfig(root) {
179
179
  return undefined;
180
180
  }
181
181
  const npmrcEnv = npmrcAuthTokenEnv(root, declared.scope);
182
- const readTokenEnv = consumes ? (declared.readTokenEnv ?? npmrcEnv) : undefined;
182
+ // A publishing workspace reads the registry too: every release after the
183
+ // first checks the previous tag's durable state, and that check runs in the
184
+ // producer jobs that never see the publish credential. A declared read token
185
+ // therefore always renders; only the .npmrc fallback stays consumer-only, so
186
+ // a publish token is never promoted into a read role by inference.
187
+ const readTokenEnv = declared.readTokenEnv ?? (consumes ? npmrcEnv : undefined);
183
188
  const publishTokenEnv = publishes ? (declared.publishTokenEnv ?? npmrcEnv) : undefined;
184
189
  if (!readTokenEnv && !publishTokenEnv) {
185
190
  return undefined;
@@ -312,8 +317,24 @@ export async function withPrivateNpmUserconfig(resolved, mode, fn) {
312
317
  await rm(dir, { recursive: true, force: true });
313
318
  }
314
319
  }
315
- function npmStatusEnv(userconfig) {
316
- return userconfig ? { NPM_CONFIG_USERCONFIG: userconfig } : undefined;
320
+ /**
321
+ * npm config precedence is cli > env > project `.npmrc` > userconfig. A
322
+ * repository that commits its own `//host/path:_authToken=${SOME_ENV}` line
323
+ * therefore OVERRIDES the userconfig this module writes, and when that env is
324
+ * unset npm sends the unexpanded value: the registry answers 401 on reads with
325
+ * a perfectly good read credential, naming nothing. So the credential also
326
+ * rides as env config, which the project file cannot outrank, and the token
327
+ * value stays out of every file on disk.
328
+ */
329
+ function npmStatusEnv(userconfig, credential) {
330
+ if (!userconfig)
331
+ return undefined;
332
+ const env = { NPM_CONFIG_USERCONFIG: userconfig };
333
+ const token = credential ? process.env[credential.tokenEnv] : undefined;
334
+ if (credential && token) {
335
+ env[`npm_config_${credential.authKey}`] = token;
336
+ }
337
+ return env;
317
338
  }
318
339
  function npmCommandFailedMessage(npmArgs, exitCode, stdout = '', stderr = '') {
319
340
  const detail = `${stdout}\n${stderr}`.trim();
@@ -330,7 +351,7 @@ export async function npmPublishedVersionExists(root, name, version, options = {
330
351
  if (options.registry) {
331
352
  args.push('--registry', options.registry);
332
353
  }
333
- const result = await runResult('npm', args, root, npmStatusEnv(options.userconfig));
354
+ const result = await runResult('npm', args, root, npmStatusEnv(options.userconfig, options.credential));
334
355
  if (result.exitCode === 0) {
335
356
  return true;
336
357
  }
@@ -345,7 +366,7 @@ export async function npmPackageExists(root, name, options = {}) {
345
366
  if (options.registry) {
346
367
  args.push('--registry', options.registry);
347
368
  }
348
- const result = await runResult('npm', args, root, npmStatusEnv(options.userconfig));
369
+ const result = await runResult('npm', args, root, npmStatusEnv(options.userconfig, options.credential));
349
370
  if (result.exitCode === 0) {
350
371
  return true;
351
372
  }
@@ -0,0 +1,45 @@
1
+ /**
2
+ * `smoo secrets` — the operator side of the reconciliation in ./index.ts.
3
+ *
4
+ * Values are handed to `gh` over stdin, never through argv (a process list is
5
+ * world-readable) and never printed. Reading one from the terminal disables
6
+ * echo, so a pasted key does not end up in a scrollback buffer or a screen
7
+ * share.
8
+ */
9
+ /** Repository secret names GitHub currently holds, or a refusal naming what failed. */
10
+ export declare function readRepositorySecrets(repo: string | undefined): {
11
+ ok: true;
12
+ names: string[];
13
+ } | {
14
+ ok: false;
15
+ reason: string;
16
+ };
17
+ /** Write one secret, value over stdin so it never reaches argv. */
18
+ export declare function writeRepositorySecret(name: string, value: string, repo: string | undefined): Promise<{
19
+ ok: true;
20
+ } | {
21
+ ok: false;
22
+ reason: string;
23
+ }>;
24
+ /**
25
+ * Print every known secret with its sources. Exit code 1 when a workflow
26
+ * promises a value the repository does not hold — that combination is the one
27
+ * that fails a deploy, and it fails talking about the value rather than the
28
+ * missing secret.
29
+ */
30
+ export declare function secretsStatus(root: string, options: {
31
+ repo?: string;
32
+ }): number;
33
+ /** Set one secret from a pasted value. */
34
+ export declare function secretsSet(name: string, options: {
35
+ repo?: string;
36
+ }): Promise<number>;
37
+ /**
38
+ * Push every locally fetchable secret to the repository. Only names
39
+ * `smoo.secrets` declares a command for: everything else has no source here
40
+ * and must be pasted with `smoo secrets set`.
41
+ */
42
+ export declare function secretsSync(root: string, options: {
43
+ repo?: string;
44
+ }): Promise<number>;
45
+ //# sourceMappingURL=commands.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"commands.d.ts","sourceRoot":"","sources":["../../src/secrets/commands.ts"],"names":[],"mappings":"AAAA;;;;;;;GAOG;AAgBH,uFAAuF;AACvF,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,MAAM,GAAG,SAAS,GACvB;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,MAAM,EAAE,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAuB/D;AAED,mEAAmE;AACnE,wBAAsB,qBAAqB,CACzC,IAAI,EAAE,MAAM,EACZ,KAAK,EAAE,MAAM,EACb,IAAI,EAAE,MAAM,GAAG,SAAS,GACvB,OAAO,CAAC;IAAE,EAAE,EAAE,IAAI,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,MAAM,EAAE,MAAM,CAAA;CAAE,CAAC,CAWvD;AA8BD;;;;;GAKG;AACH,wBAAgB,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,MAAM,CAgC9E;AA8BD,0CAA0C;AAC1C,wBAAsB,UAAU,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAa1F;AAED;;;;GAIG;AACH,wBAAsB,WAAW,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE;IAAE,IAAI,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAuB3F"}
@@ -0,0 +1,189 @@
1
+ /**
2
+ * `smoo secrets` — the operator side of the reconciliation in ./index.ts.
3
+ *
4
+ * Values are handed to `gh` over stdin, never through argv (a process list is
5
+ * world-readable) and never printed. Reading one from the terminal disables
6
+ * echo, so a pasted key does not end up in a scrollback buffer or a screen
7
+ * share.
8
+ */
9
+ import { spawn, spawnSync } from 'node:child_process';
10
+ import { getWorkspacePackages } from '../lib/workspace.js';
11
+ import { fetchLocalSecret, localSecretCommandNames, reconcileSecrets, secretNameMapping, unsatisfiedSecrets, unwiredSecrets, workerSecretNames, workflowSecretNames, } from './index.js';
12
+ /** Repository secret names GitHub currently holds, or a refusal naming what failed. */
13
+ export function readRepositorySecrets(repo) {
14
+ const args = ['secret', 'list', '--json', 'name'];
15
+ if (repo)
16
+ args.push('--repo', repo);
17
+ const result = spawnSync('gh', args, { encoding: 'utf8' });
18
+ if (result.status !== 0) {
19
+ return {
20
+ ok: false,
21
+ reason: `gh ${args.join(' ')} exited ${result.status ?? 'without status'}: ${result.stderr.trim()}`,
22
+ };
23
+ }
24
+ const parsed = JSON.parse(result.stdout);
25
+ if (!Array.isArray(parsed)) {
26
+ return { ok: false, reason: 'gh secret list did not return a list' };
27
+ }
28
+ const names = [];
29
+ for (const row of parsed) {
30
+ if (typeof row !== 'object' || row === null || !('name' in row))
31
+ continue;
32
+ // `in` narrows the property to unknown, so the typeof check below is the
33
+ // only thing that admits it — no assertion about gh's output shape.
34
+ const name = row.name;
35
+ if (typeof name === 'string')
36
+ names.push(name);
37
+ }
38
+ return { ok: true, names: names.sort((left, right) => left.localeCompare(right)) };
39
+ }
40
+ /** Write one secret, value over stdin so it never reaches argv. */
41
+ export async function writeRepositorySecret(name, value, repo) {
42
+ const args = ['secret', 'set', name];
43
+ if (repo)
44
+ args.push('--repo', repo);
45
+ const child = spawn('gh', args, { stdio: ['pipe', 'inherit', 'inherit'] });
46
+ child.stdin.end(value);
47
+ const status = await new Promise((resolvePromise) => {
48
+ child.on('close', (code) => resolvePromise(code));
49
+ });
50
+ return status === 0
51
+ ? { ok: true }
52
+ : { ok: false, reason: `gh ${args.join(' ')} exited ${status ?? 'without status'}` };
53
+ }
54
+ function collectSources(root, repositorySecrets) {
55
+ const workspaceDirs = getWorkspacePackages(root).map((pkg) => pkg.path);
56
+ const workerSecrets = workerSecretNames(root, workspaceDirs);
57
+ const workflowSecrets = workflowSecretNames(root);
58
+ const localCommands = localSecretCommandNames(root);
59
+ const envNames = [
60
+ ...new Set([...Object.values(workerSecrets).flatMap((names) => [...names]), ...workflowSecrets, ...localCommands]),
61
+ ];
62
+ return {
63
+ workerSecrets,
64
+ workflowSecrets,
65
+ secretNames: secretNameMapping(root, envNames),
66
+ localCommands,
67
+ repositorySecrets,
68
+ };
69
+ }
70
+ function describe(row) {
71
+ const where = row.declaredByWorkers.length > 0 ? row.declaredByWorkers.join(', ') : '—';
72
+ return [
73
+ row.onRepository ? 'set ' : 'ABSENT',
74
+ row.name.padEnd(32),
75
+ row.suppliedByWorkflow ? 'workflow' : ' ',
76
+ row.fetchableLocally ? 'local' : ' ',
77
+ where,
78
+ ].join(' ');
79
+ }
80
+ /**
81
+ * Print every known secret with its sources. Exit code 1 when a workflow
82
+ * promises a value the repository does not hold — that combination is the one
83
+ * that fails a deploy, and it fails talking about the value rather than the
84
+ * missing secret.
85
+ */
86
+ export function secretsStatus(root, options) {
87
+ const repositorySecrets = readRepositorySecrets(options.repo);
88
+ if (!repositorySecrets.ok) {
89
+ console.error(repositorySecrets.reason);
90
+ return 1;
91
+ }
92
+ const rows = reconcileSecrets(collectSources(root, repositorySecrets.names));
93
+ console.log('state name workflow local declared by');
94
+ for (const row of rows)
95
+ console.log(describe(row));
96
+ const unsatisfied = unsatisfiedSecrets(rows);
97
+ const unwired = unwiredSecrets(rows);
98
+ if (unwired.length > 0) {
99
+ console.log('');
100
+ for (const row of unwired) {
101
+ console.log(`note: ${row.name} is declared by ${row.declaredByWorkers.join(', ')} but no managed workflow passes it; ` +
102
+ 'declare it in smoo.github.deploySecrets to have CI supply it.');
103
+ }
104
+ }
105
+ if (unsatisfied.length > 0) {
106
+ console.log('');
107
+ for (const row of unsatisfied) {
108
+ console.error(`missing: ${row.name} — a workflow passes secrets.${row.name} and the repository has no value.`);
109
+ console.error(` set it with: smoo secrets set ${row.name}${options.repo ? ` --repo ${options.repo}` : ''}`);
110
+ }
111
+ return 1;
112
+ }
113
+ return 0;
114
+ }
115
+ /** Read a value with echo disabled when stdin is a terminal; otherwise read piped input. */
116
+ async function readSecretValue(prompt) {
117
+ if (!process.stdin.isTTY) {
118
+ const chunks = [];
119
+ for await (const chunk of process.stdin)
120
+ chunks.push(Buffer.from(chunk));
121
+ return Buffer.concat(chunks).toString('utf8').replace(/\n$/, '');
122
+ }
123
+ process.stderr.write(prompt);
124
+ process.stdin.setRawMode(true);
125
+ let value = '';
126
+ try {
127
+ for await (const chunk of process.stdin) {
128
+ const text = Buffer.from(chunk).toString('utf8');
129
+ if (text === '\r' || text === '\n' || text === '\u0004')
130
+ break;
131
+ if (text === '\u0003')
132
+ throw new Error('cancelled');
133
+ if (text === '\u007f') {
134
+ value = value.slice(0, -1);
135
+ continue;
136
+ }
137
+ value += text;
138
+ }
139
+ }
140
+ finally {
141
+ process.stdin.setRawMode(false);
142
+ process.stderr.write('\n');
143
+ }
144
+ return value;
145
+ }
146
+ /** Set one secret from a pasted value. */
147
+ export async function secretsSet(name, options) {
148
+ const value = await readSecretValue(`Value for ${name} (not echoed): `);
149
+ if (value.length === 0) {
150
+ console.error(`${name}: refusing to set an empty value.`);
151
+ return 1;
152
+ }
153
+ const written = await writeRepositorySecret(name, value, options.repo);
154
+ if (!written.ok) {
155
+ console.error(written.reason);
156
+ return 1;
157
+ }
158
+ console.log(`set ${name}`);
159
+ return 0;
160
+ }
161
+ /**
162
+ * Push every locally fetchable secret to the repository. Only names
163
+ * `smoo.secrets` declares a command for: everything else has no source here
164
+ * and must be pasted with `smoo secrets set`.
165
+ */
166
+ export async function secretsSync(root, options) {
167
+ const names = localSecretCommandNames(root);
168
+ if (names.length === 0) {
169
+ console.error('smoo.secrets declares no fetch commands; nothing to sync.');
170
+ return 1;
171
+ }
172
+ let failed = 0;
173
+ for (const name of names) {
174
+ const fetched = fetchLocalSecret(root, name);
175
+ if (!fetched.ok) {
176
+ console.error(`skip ${name}: ${fetched.reason}`);
177
+ failed += 1;
178
+ continue;
179
+ }
180
+ const written = await writeRepositorySecret(name, fetched.value, options.repo);
181
+ if (!written.ok) {
182
+ console.error(`fail ${name}: ${written.reason}`);
183
+ failed += 1;
184
+ continue;
185
+ }
186
+ console.log(`set ${name}`);
187
+ }
188
+ return failed === 0 ? 0 : 1;
189
+ }
@@ -0,0 +1,83 @@
1
+ /**
2
+ * Reconciles the three places a repository's secrets are described, all of
3
+ * which smoo already owns:
4
+ *
5
+ * 1. **Workers declare names.** `.dev.vars.example` per wrangler project — the
6
+ * file `wrangler types --env-file` reads and `prepare-env` prompts from.
7
+ * 2. **Workflows declare where values come from.** `smoo.github.deploySecrets`
8
+ * and `e2eSecrets` map an env name to a repository secret, and the managed
9
+ * workflows render exactly those into the job environment.
10
+ * 3. **A developer shell declares how to fetch one locally.** `smoo.secrets`
11
+ * names a command whose stdout is the value.
12
+ *
13
+ * Nothing joined them, so a Worker could declare a secret no workflow supplies
14
+ * and no repository holds. That fails at deploy time, on a stage, with a
15
+ * fail-closed message about the value rather than about the missing
16
+ * declaration — the shape that costs an afternoon: a Worker declares
17
+ * `STRIPE_PUBLISHABLE_KEY`, `ci.yml` passes `secrets.STRIPE_PUBLISHABLE_KEY`,
18
+ * the repository holds no such secret, and the empty value surfaces as the
19
+ * payment library's own `publishable_key_mismatch` refusal.
20
+ */
21
+ /** Where an env name is described, which repository secret carries it, and whether that exists. */
22
+ export interface SecretRow {
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
+ export interface SecretSources {
36
+ /** Worker label -> env names it declares. */
37
+ workerSecrets: Record<string, readonly string[]>;
38
+ /** Env names a managed workflow passes into a job. */
39
+ workflowSecrets: readonly string[];
40
+ /** Env name -> repository secret, by convention with declared exceptions. */
41
+ secretNames: Readonly<Record<string, string>>;
42
+ /** Env names `smoo.secrets` can fetch locally. */
43
+ localCommands: readonly string[];
44
+ /** Repository secret names GitHub currently holds. */
45
+ repositorySecrets: readonly string[];
46
+ }
47
+ /**
48
+ * One row per name known to any source, sorted, so a reader sees the whole
49
+ * picture rather than one source's view of it.
50
+ */
51
+ export declare function reconcileSecrets(sources: SecretSources): SecretRow[];
52
+ /**
53
+ * The rows a CI deploy cannot satisfy: a Worker declares the name, a workflow
54
+ * promises to pass it, and the repository has no value to pass. Reported
55
+ * separately from "declared but not wired into any workflow", because the
56
+ * remedies differ — set a secret, versus declare it in `smoo.github`.
57
+ */
58
+ export declare function unsatisfiedSecrets(rows: readonly SecretRow[]): SecretRow[];
59
+ /** Worker-declared names no managed workflow passes: a CI deploy will run without them. */
60
+ export declare function unwiredSecrets(rows: readonly SecretRow[]): SecretRow[];
61
+ /** Env names the managed workflows pass into a job, from the declarations that render them. */
62
+ export declare function workflowSecretNames(root: string): string[];
63
+ /**
64
+ * Env name -> repository secret for every name in play, following the naming
65
+ * convention and honouring the declared exceptions a repository still needs.
66
+ */
67
+ export declare function secretNameMapping(root: string, envNames: readonly string[]): Record<string, string>;
68
+ /** Every wrangler project's declared secret names, keyed by the project directory. */
69
+ export declare function workerSecretNames(root: string, workspaceDirs: readonly string[]): Record<string, string[]>;
70
+ /** Names `smoo.secrets` declares a fetch command for. */
71
+ export declare function localSecretCommandNames(root: string): string[];
72
+ /**
73
+ * Resolve one declared secret through its command. The value is returned, never
74
+ * logged: callers hand it to `gh secret set` over stdin.
75
+ */
76
+ export declare function fetchLocalSecret(root: string, name: string): {
77
+ ok: true;
78
+ value: string;
79
+ } | {
80
+ ok: false;
81
+ reason: string;
82
+ };
83
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/secrets/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AASH,mGAAmG;AACnG,MAAM,WAAW,SAAS;IACxB,IAAI,EAAE,MAAM,CAAC;IACb,oFAAoF;IACpF,gBAAgB,EAAE,MAAM,CAAC;IACzB,+DAA+D;IAC/D,iBAAiB,EAAE,MAAM,EAAE,CAAC;IAC5B,wEAAwE;IACxE,kBAAkB,EAAE,OAAO,CAAC;IAC5B,mEAAmE;IACnE,gBAAgB,EAAE,OAAO,CAAC;IAC1B,4DAA4D;IAC5D,YAAY,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,aAAa;IAC5B,6CAA6C;IAC7C,aAAa,EAAE,MAAM,CAAC,MAAM,EAAE,SAAS,MAAM,EAAE,CAAC,CAAC;IACjD,sDAAsD;IACtD,eAAe,EAAE,SAAS,MAAM,EAAE,CAAC;IACnC,6EAA6E;IAC7E,WAAW,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IAC9C,kDAAkD;IAClD,aAAa,EAAE,SAAS,MAAM,EAAE,CAAC;IACjC,sDAAsD;IACtD,iBAAiB,EAAE,SAAS,MAAM,EAAE,CAAC;CACtC;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,OAAO,EAAE,aAAa,GAAG,SAAS,EAAE,CA0BpE;AAED;;;;;GAKG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,SAAS,SAAS,EAAE,GAAG,SAAS,EAAE,CAE1E;AAED,2FAA2F;AAC3F,wBAAgB,cAAc,CAAC,IAAI,EAAE,SAAS,SAAS,EAAE,GAAG,SAAS,EAAE,CAEtE;AAED,+FAA+F;AAC/F,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAkB1D;AAED;;;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,yDAAyD;AACzD,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,MAAM,EAAE,CAG9D;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"}
@@ -0,0 +1,149 @@
1
+ /**
2
+ * Reconciles the three places a repository's secrets are described, all of
3
+ * which smoo already owns:
4
+ *
5
+ * 1. **Workers declare names.** `.dev.vars.example` per wrangler project — the
6
+ * file `wrangler types --env-file` reads and `prepare-env` prompts from.
7
+ * 2. **Workflows declare where values come from.** `smoo.github.deploySecrets`
8
+ * and `e2eSecrets` map an env name to a repository secret, and the managed
9
+ * workflows render exactly those into the job environment.
10
+ * 3. **A developer shell declares how to fetch one locally.** `smoo.secrets`
11
+ * names a command whose stdout is the value.
12
+ *
13
+ * Nothing joined them, so a Worker could declare a secret no workflow supplies
14
+ * and no repository holds. That fails at deploy time, on a stage, with a
15
+ * fail-closed message about the value rather than about the missing
16
+ * declaration — the shape that costs an afternoon: a Worker declares
17
+ * `STRIPE_PUBLISHABLE_KEY`, `ci.yml` passes `secrets.STRIPE_PUBLISHABLE_KEY`,
18
+ * the repository holds no such secret, and the empty value surfaces as the
19
+ * payment library's own `publishable_key_mismatch` refusal.
20
+ */
21
+ import { spawnSync } from 'node:child_process';
22
+ import { existsSync, readFileSync } from 'node:fs';
23
+ import { join } from 'node:path';
24
+ import { repositoryOwnerFromUrl, repositorySecretMapping } from '../lib/secret-names.js';
25
+ import { readPackageJsonObject, repositoryInfo } from '../lib/workspace.js';
26
+ import { parseDevVarsExample } from '../wrangler/prepare-env.js';
27
+ /**
28
+ * One row per name known to any source, sorted, so a reader sees the whole
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
+ for (const secret of sources.repositorySecrets) {
40
+ if (!carriesKnownEnvName.has(secret))
41
+ names.add(secret);
42
+ }
43
+ return [...names]
44
+ .sort((left, right) => left.localeCompare(right))
45
+ .map((name) => ({
46
+ name,
47
+ repositorySecret: sources.secretNames[name] ?? name,
48
+ declaredByWorkers: Object.entries(sources.workerSecrets)
49
+ .filter(([, declared]) => declared.includes(name))
50
+ .map(([label]) => label)
51
+ .sort((left, right) => left.localeCompare(right)),
52
+ suppliedByWorkflow: sources.workflowSecrets.includes(name),
53
+ fetchableLocally: sources.localCommands.includes(name),
54
+ onRepository: sources.repositorySecrets.includes(sources.secretNames[name] ?? name),
55
+ }));
56
+ }
57
+ /**
58
+ * The rows a CI deploy cannot satisfy: a Worker declares the name, a workflow
59
+ * promises to pass it, and the repository has no value to pass. Reported
60
+ * separately from "declared but not wired into any workflow", because the
61
+ * remedies differ — set a secret, versus declare it in `smoo.github`.
62
+ */
63
+ export function unsatisfiedSecrets(rows) {
64
+ return rows.filter((row) => row.suppliedByWorkflow && !row.onRepository);
65
+ }
66
+ /** Worker-declared names no managed workflow passes: a CI deploy will run without them. */
67
+ export function unwiredSecrets(rows) {
68
+ return rows.filter((row) => row.declaredByWorkers.length > 0 && !row.suppliedByWorkflow);
69
+ }
70
+ /** Env names the managed workflows pass into a job, from the declarations that render them. */
71
+ export function workflowSecretNames(root) {
72
+ const manifest = readPackageJsonObject(join(root, 'package.json'));
73
+ const smoo = manifest?.smoo;
74
+ const github = smoo?.github;
75
+ const names = new Set([
76
+ ...Object.keys(github?.deploySecrets ?? {}),
77
+ ...Object.keys(github?.e2eSecrets ?? {}),
78
+ ]);
79
+ if (smoo?.remoteCache?.tokenSecret)
80
+ names.add(smoo.remoteCache.tokenSecret);
81
+ for (const origin of github?.cargoCredentials?.gitOrigins ?? []) {
82
+ if (origin.tokenEnv)
83
+ names.add(origin.tokenEnv);
84
+ }
85
+ for (const source of github?.sourceCheckouts ?? []) {
86
+ if (source.tokenEnv)
87
+ names.add(source.tokenEnv);
88
+ }
89
+ if (smoo?.privateNpm?.readTokenEnv)
90
+ names.add(smoo.privateNpm.readTokenEnv);
91
+ if (smoo?.privateNpm?.publishTokenEnv)
92
+ names.add(smoo.privateNpm.publishTokenEnv);
93
+ return [...names].sort((left, right) => left.localeCompare(right));
94
+ }
95
+ /**
96
+ * Env name -> repository secret for every name in play, following the naming
97
+ * convention and honouring the declared exceptions a repository still needs.
98
+ */
99
+ export function secretNameMapping(root, envNames) {
100
+ const manifest = readPackageJsonObject(join(root, 'package.json'));
101
+ const declared = { ...(manifest?.smoo?.github?.deploySecrets ?? {}), ...(manifest?.smoo?.github?.e2eSecrets ?? {}) };
102
+ const repository = manifest ? repositoryInfo(manifest) : null;
103
+ const owner = repository ? (repositoryOwnerFromUrl(repository.url) ?? '') : '';
104
+ return repositorySecretMapping(envNames, owner, declared);
105
+ }
106
+ /** Every wrangler project's declared secret names, keyed by the project directory. */
107
+ export function workerSecretNames(root, workspaceDirs) {
108
+ const byWorker = {};
109
+ for (const dir of workspaceDirs) {
110
+ const examplePath = join(root, dir, '.dev.vars.example');
111
+ if (!existsSync(examplePath))
112
+ continue;
113
+ const names = parseDevVarsExample(readFileSync(examplePath, 'utf8'));
114
+ if (names.length > 0)
115
+ byWorker[dir] = names;
116
+ }
117
+ return byWorker;
118
+ }
119
+ /** Names `smoo.secrets` declares a fetch command for. */
120
+ export function localSecretCommandNames(root) {
121
+ const manifest = readPackageJsonObject(join(root, 'package.json'));
122
+ return Object.keys(manifest?.smoo?.secrets ?? {}).sort((left, right) => left.localeCompare(right));
123
+ }
124
+ /**
125
+ * Resolve one declared secret through its command. The value is returned, never
126
+ * logged: callers hand it to `gh secret set` over stdin.
127
+ */
128
+ export function fetchLocalSecret(root, name) {
129
+ const manifest = readPackageJsonObject(join(root, 'package.json'));
130
+ const declared = manifest?.smoo?.secrets?.[name];
131
+ if (!declared) {
132
+ return { ok: false, reason: `smoo.secrets declares no command for ${name}` };
133
+ }
134
+ const [command, ...args] = declared.command;
135
+ const result = spawnSync(command, args, { encoding: 'utf8' });
136
+ if (result.status !== 0) {
137
+ // Name the command, never its output: a failing secret fetch prints
138
+ // credentials often enough that it is worth refusing to.
139
+ return {
140
+ ok: false,
141
+ reason: `${name}: \`${declared.command.join(' ')}\` exited ${result.status ?? 'without status'}`,
142
+ };
143
+ }
144
+ const value = result.stdout.replace(/\n$/, '');
145
+ if (value.length === 0) {
146
+ return { ok: false, reason: `${name}: \`${declared.command.join(' ')}\` produced no value` };
147
+ }
148
+ return { ok: true, value };
149
+ }