@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.
- package/README.md +43 -0
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +44 -0
- package/dist/github-ci/index.d.ts +1 -1
- package/dist/github-ci/index.d.ts.map +1 -1
- package/dist/github-ci/index.js +7 -10
- package/dist/lib/json.d.ts +9 -0
- package/dist/lib/json.d.ts.map +1 -1
- package/dist/lib/json.js +65 -53
- package/dist/lib/secret-names.d.ts +35 -0
- package/dist/lib/secret-names.d.ts.map +1 -0
- package/dist/lib/secret-names.js +61 -0
- package/dist/monorepo/ci-workflow.d.ts.map +1 -1
- package/dist/monorepo/ci-workflow.js +15 -6
- package/dist/monorepo/managed-files.d.ts +9 -0
- package/dist/monorepo/managed-files.d.ts.map +1 -1
- package/dist/monorepo/managed-files.js +28 -2
- package/dist/monorepo/publish-workflow.d.ts +10 -1
- package/dist/monorepo/publish-workflow.d.ts.map +1 -1
- package/dist/monorepo/publish-workflow.js +110 -22
- package/dist/release/index.d.ts +1 -1
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +26 -4
- package/dist/release/private-npm.d.ts +5 -0
- package/dist/release/private-npm.d.ts.map +1 -1
- package/dist/release/private-npm.js +26 -5
- package/dist/secrets/commands.d.ts +45 -0
- package/dist/secrets/commands.d.ts.map +1 -0
- package/dist/secrets/commands.js +189 -0
- package/dist/secrets/index.d.ts +83 -0
- package/dist/secrets/index.d.ts.map +1 -0
- package/dist/secrets/index.js +149 -0
- package/dist/wrangler/deploy-stage.d.ts +13 -2
- package/dist/wrangler/deploy-stage.d.ts.map +1 -1
- package/dist/wrangler/deploy-stage.js +39 -54
- package/dist/wrangler/deployed-version.d.ts +44 -0
- package/dist/wrangler/deployed-version.d.ts.map +1 -0
- package/dist/wrangler/deployed-version.js +87 -0
- package/dist/wrangler/live-version.d.ts +146 -0
- package/dist/wrangler/live-version.d.ts.map +1 -0
- package/dist/wrangler/live-version.js +326 -0
- package/managed/raw/tooling/direnv/devenv.smoo.nix +4 -2
- package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +4 -1
- package/managed/raw/tooling/direnv/toolchain-stamp.ts +88 -0
- package/managed/templates/github/actions/setup-devenv/action.yml +1 -1
- package/package.json +2 -2
- package/src/cli.ts +46 -1
- package/src/github-ci/index.test.ts +92 -8
- package/src/github-ci/index.ts +7 -10
- package/src/lib/json.ts +9 -0
- package/src/lib/secret-names.ts +70 -0
- package/src/monorepo/__tests__/ci-workflow.test.ts +27 -2
- package/src/monorepo/__tests__/publish-workflow.test.ts +85 -13
- package/src/monorepo/ci-workflow.ts +17 -6
- package/src/monorepo/managed-files.test.ts +26 -0
- package/src/monorepo/managed-files.ts +36 -2
- package/src/monorepo/publish-workflow.ts +135 -21
- package/src/monorepo/secret-references.test.ts +1 -1
- package/src/release/__tests__/private-npm-status.test.ts +37 -0
- package/src/release/__tests__/private-npm-workflow.test.ts +20 -0
- package/src/release/index.ts +27 -2
- package/src/release/private-npm.ts +30 -5
- package/src/secrets/commands.ts +210 -0
- package/src/secrets/index.test.ts +92 -0
- package/src/secrets/index.ts +183 -0
- package/src/wrangler/deploy-stage.test.ts +190 -12
- package/src/wrangler/deploy-stage.ts +72 -45
- package/src/wrangler/deployed-version.test.ts +150 -0
- package/src/wrangler/deployed-version.ts +130 -0
- package/src/wrangler/live-version.ts +363 -0
package/dist/release/index.js
CHANGED
|
@@ -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, {
|
|
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, {
|
|
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":"AAIA,OAAO,KAAK,EAAe,uBAAuB,EAAE,MAAM,gBAAgB,CAAC;AAE3E,OAAO,EAGL,KAAK,WAAW,EAIjB,MAAM,qBAAqB,CAAC;AAE7B,uEAAuE;AACvE,MAAM,MAAM,MAAM,CAAC,CAAC,EAAE,CAAC,IAAI;IAAE,EAAE,EAAE,IAAI,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,GAAG;IAAE,EAAE,EAAE,KAAK,CAAC;IAAC,KAAK,EAAE,CAAC,CAAA;CAAE,CAAC;AAE5E,MAAM,WAAW,kBAAkB;IACjC,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,MAAM,CAAC;IACjB,kFAAkF;IAClF,OAAO,EAAE,MAAM,CAAC;IAChB,YAAY,EAAE,MAAM,CAAC;IACrB,eAAe,CAAC,EAAE,MAAM,CAAC;CAC1B;AAED,MAAM,MAAM,iCAAiC,GACzC,sBAAsB,GACtB,iBAAiB,GACjB,iBAAiB,GACjB,eAAe,CAAC;AAEpB,MAAM,WAAW,6BAA6B;IAC5C,IAAI,EAAE,iCAAiC,CAAC;IACxC,wEAAwE;IACxE,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,KAAK,cAAc,GAAG,MAAM,CAAC,kBAAkB,EAAE,6BAA6B,CAAC,CAAC;AAMhF;;;;;;;;;;;;;GAaG;AACH,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,cAAc,CAwFtE;AAoFD;;;;;;GAMG;AACH,wBAAgB,+BAA+B,CAAC,IAAI,EAAE,MAAM,GAAG,uBAAuB,GAAG,SAAS,
|
|
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
|
-
|
|
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
|
-
|
|
316
|
-
|
|
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
|
+
}
|