@smoothbricks/cli 0.11.19 → 0.11.20
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 -1
- package/dist/nx/index.d.ts +6 -0
- package/dist/nx/index.d.ts.map +1 -1
- package/dist/nx/index.js +14 -0
- 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/packs/index.ts +10 -1
- 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 -0
- package/src/nx/index.ts +20 -0
- 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
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
2
3
|
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
3
4
|
import { tmpdir } from 'node:os';
|
|
4
5
|
import { join, resolve } from 'node:path';
|
|
@@ -32,7 +33,8 @@ const RESOLVE_SCRIPT = `
|
|
|
32
33
|
const resolver = await import(Bun.argv[1]);
|
|
33
34
|
const request = JSON.parse(Bun.argv[2]);
|
|
34
35
|
try {
|
|
35
|
-
|
|
36
|
+
const { values, deferred } = await resolver.resolveSecretEnvironment(request);
|
|
37
|
+
process.stdout.write(JSON.stringify({ resolved: values, deferred }));
|
|
36
38
|
} catch (error) {
|
|
37
39
|
process.stdout.write(JSON.stringify({ error: error instanceof Error ? error.message : String(error) }));
|
|
38
40
|
}
|
|
@@ -54,16 +56,44 @@ const names = [...resolver.registryAuthEnvNames(npmrc)];
|
|
|
54
56
|
process.stdout.write(JSON.stringify({ names }));
|
|
55
57
|
`;
|
|
56
58
|
|
|
59
|
+
const GROUPS_SCRIPT = `
|
|
60
|
+
const resolver = await import(Bun.argv[1]);
|
|
61
|
+
const { root } = JSON.parse(Bun.argv[2]);
|
|
62
|
+
const groups = resolver.readSecretGroups(root).map(({ name, group, derivedGroup }) => ({ name, group, derivedGroup }));
|
|
63
|
+
process.stdout.write(JSON.stringify({ groups }));
|
|
64
|
+
`;
|
|
65
|
+
|
|
57
66
|
interface BootstrapOutcome {
|
|
58
67
|
readonly resolved?: Record<string, string>;
|
|
68
|
+
readonly deferred?: readonly { readonly name: string; readonly group: string; readonly guidance: string }[];
|
|
59
69
|
readonly parsed?: unknown;
|
|
60
70
|
readonly error?: string;
|
|
61
71
|
}
|
|
62
72
|
|
|
73
|
+
interface GroupListing {
|
|
74
|
+
readonly groups: { name: string; group: string; derivedGroup: string }[];
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
function isGroupListing(value: unknown): value is GroupListing {
|
|
78
|
+
if (typeof value !== 'object' || value === null || !('groups' in value) || !Array.isArray(value.groups)) return false;
|
|
79
|
+
return value.groups.every(
|
|
80
|
+
(entry: unknown) =>
|
|
81
|
+
typeof entry === 'object' &&
|
|
82
|
+
entry !== null &&
|
|
83
|
+
'name' in entry &&
|
|
84
|
+
typeof entry.name === 'string' &&
|
|
85
|
+
'group' in entry &&
|
|
86
|
+
typeof entry.group === 'string' &&
|
|
87
|
+
'derivedGroup' in entry &&
|
|
88
|
+
typeof entry.derivedGroup === 'string',
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
63
92
|
/** Narrows a harness JSON envelope with `in` checks instead of assertions: unknown shapes fail the test. Field-level expectations below do the precise checking. */
|
|
64
93
|
function isBootstrapOutcome(value: unknown): value is BootstrapOutcome {
|
|
65
94
|
if (typeof value !== 'object' || value === null) return false;
|
|
66
95
|
if ('resolved' in value && (typeof value.resolved !== 'object' || value.resolved === null)) return false;
|
|
96
|
+
if ('deferred' in value && !Array.isArray(value.deferred)) return false;
|
|
67
97
|
if ('error' in value && typeof value.error !== 'string') return false;
|
|
68
98
|
return true;
|
|
69
99
|
}
|
|
@@ -96,6 +126,39 @@ const emit = (text: string): readonly [string, ...string[]] => [
|
|
|
96
126
|
'-e',
|
|
97
127
|
`process.stdout.write(${JSON.stringify(text)})`,
|
|
98
128
|
];
|
|
129
|
+
|
|
130
|
+
/** `emit`, plus one line appended to `ledger` every time the command actually runs. */
|
|
131
|
+
const emitAndRecord = (text: string, ledger: string): readonly [string, ...string[]] => [
|
|
132
|
+
process.execPath,
|
|
133
|
+
'-e',
|
|
134
|
+
`require('node:fs').appendFileSync(${JSON.stringify(ledger)}, '1\\n');process.stdout.write(${JSON.stringify(text)})`,
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* A ledger a provider command appends to, and the count of what it recorded.
|
|
139
|
+
* Counting invocations is the only direct evidence that a path ran no
|
|
140
|
+
* provider command: an absent value is also what a silently failing provider
|
|
141
|
+
* leaves behind, and "no credential prompt" is the whole point of the rule.
|
|
142
|
+
*/
|
|
143
|
+
async function withProviderLedger(
|
|
144
|
+
run: (ledger: { readonly path: string; readonly invocations: () => number }) => Promise<void>,
|
|
145
|
+
): Promise<void> {
|
|
146
|
+
const dir = await mkdtemp(join(tmpdir(), 'smoo-provider-ledger-'));
|
|
147
|
+
const path = join(dir, 'invocations');
|
|
148
|
+
try {
|
|
149
|
+
await run({
|
|
150
|
+
path,
|
|
151
|
+
invocations: () =>
|
|
152
|
+
existsSync(path)
|
|
153
|
+
? readFileSync(path, 'utf8')
|
|
154
|
+
.split('\n')
|
|
155
|
+
.filter((line) => line.length > 0).length
|
|
156
|
+
: 0,
|
|
157
|
+
});
|
|
158
|
+
} finally {
|
|
159
|
+
await rm(dir, { recursive: true, force: true });
|
|
160
|
+
}
|
|
161
|
+
}
|
|
99
162
|
/**
|
|
100
163
|
* Runs one script against the raw module in a plain Bun child. The child env
|
|
101
164
|
* carries only PATH/HOME so parent suite state (CI, workspace tokens) cannot
|
|
@@ -124,16 +187,33 @@ async function runInBootstrap(
|
|
|
124
187
|
async function resolveInBootstrap(options: {
|
|
125
188
|
root: string;
|
|
126
189
|
env?: Record<string, string | undefined>;
|
|
190
|
+
/**
|
|
191
|
+
* Defaults to the group shell entry resolves, which is what a direnv
|
|
192
|
+
* reload performs. The resolver itself declares no default — resolving the
|
|
193
|
+
* wrong group is a credential prompt — so every case that is not shell
|
|
194
|
+
* entry names its group out loud.
|
|
195
|
+
*/
|
|
196
|
+
group?: string;
|
|
127
197
|
}): Promise<BootstrapOutcome> {
|
|
128
198
|
const { stdout, stderr, exitCode } = await runInBootstrap(
|
|
129
199
|
RESOLVE_SCRIPT,
|
|
130
|
-
JSON.stringify({ root: options.root, env: options.env ?? {} }),
|
|
200
|
+
JSON.stringify({ root: options.root, env: options.env ?? {}, group: options.group ?? 'shell' }),
|
|
131
201
|
);
|
|
132
202
|
expect(stderr).toBe('');
|
|
133
203
|
expect(exitCode).toBe(0);
|
|
134
204
|
return mustOutcome(stdout);
|
|
135
205
|
}
|
|
136
206
|
|
|
207
|
+
/** The groups this repository's own declarations settle on, per declared variable. */
|
|
208
|
+
async function groupsInBootstrap(root: string): Promise<{ name: string; group: string; derivedGroup: string }[]> {
|
|
209
|
+
const { stdout, stderr, exitCode } = await runInBootstrap(GROUPS_SCRIPT, JSON.stringify({ root }));
|
|
210
|
+
expect(stderr).toBe('');
|
|
211
|
+
expect(exitCode).toBe(0);
|
|
212
|
+
const value: unknown = JSON.parse(stdout);
|
|
213
|
+
if (!isGroupListing(value)) throw new Error(`bootstrap harness returned a malformed listing: ${stdout}`);
|
|
214
|
+
return value.groups;
|
|
215
|
+
}
|
|
216
|
+
|
|
137
217
|
/** Builds a fixture repository whose package.json and .npmrc are the resolver's real inputs. */
|
|
138
218
|
async function withFixture(
|
|
139
219
|
options: { secrets: Record<string, SecretSpec>; npmrc?: string; remoteCache?: unknown },
|
|
@@ -195,6 +275,19 @@ describe('smoo.secrets shape validation', () => {
|
|
|
195
275
|
expect((await parse({ smoo: { secrets: { 'a b': { command: ['op'] } } } })).error).toContain('a b');
|
|
196
276
|
expect((await parse({ smoo: { secrets: { '': { command: ['op'] } } } })).error).toContain('smoo.secrets');
|
|
197
277
|
});
|
|
278
|
+
|
|
279
|
+
it('accepts any group label the command line can name, and rejects one it cannot', async () => {
|
|
280
|
+
// The shape is validated, never the value: a repository naming a group
|
|
281
|
+
// of its own must not need a new smoo release to run it.
|
|
282
|
+
expect(
|
|
283
|
+
(await parse({ smoo: { secrets: { SMOO_TOKEN: { command: ['op'], group: 'deploy-eu' } } } })).parsed,
|
|
284
|
+
).toEqual({ SMOO_TOKEN: { command: ['op'], group: 'deploy-eu' } });
|
|
285
|
+
for (const malformed of ['', 'two words', 7]) {
|
|
286
|
+
const outcome = await parse({ smoo: { secrets: { SMOO_TOKEN: { command: ['op'], group: malformed } } } });
|
|
287
|
+
expect(outcome.error).toContain('SMOO_TOKEN');
|
|
288
|
+
expect(outcome.error).toContain('group');
|
|
289
|
+
}
|
|
290
|
+
});
|
|
198
291
|
});
|
|
199
292
|
|
|
200
293
|
describe('registryAuthEnvNames', () => {
|
|
@@ -215,6 +308,78 @@ describe('registryAuthEnvNames', () => {
|
|
|
215
308
|
});
|
|
216
309
|
});
|
|
217
310
|
|
|
311
|
+
/**
|
|
312
|
+
* The group is derived from declarations the repository already carries, so
|
|
313
|
+
* a manifest states a group only where those cannot: nobody restates a fact
|
|
314
|
+
* the repo already declares.
|
|
315
|
+
*/
|
|
316
|
+
describe('group derivation', () => {
|
|
317
|
+
const REGISTRY_NPMRC = '//npm.example.net/:_authToken=${SMOO_NPM_TOKEN}\n';
|
|
318
|
+
|
|
319
|
+
it('derives registry from a .npmrc reference, nx-cache from the cache token, and shell from neither', async () => {
|
|
320
|
+
await withFixture(
|
|
321
|
+
{
|
|
322
|
+
npmrc: REGISTRY_NPMRC,
|
|
323
|
+
secrets: {
|
|
324
|
+
SMOO_NPM_TOKEN: { command: emit('x') },
|
|
325
|
+
NX_REMOTE_CACHE_TOKEN: { command: emit('x') },
|
|
326
|
+
SMOO_TOKEN: { command: emit('x') },
|
|
327
|
+
},
|
|
328
|
+
remoteCache: { server: 'https://nx-cache.example.net', tokenSecret: 'NX_REMOTE_CACHE_TOKEN' },
|
|
329
|
+
},
|
|
330
|
+
async (root) => {
|
|
331
|
+
expect(await groupsInBootstrap(root)).toEqual([
|
|
332
|
+
{ name: 'SMOO_NPM_TOKEN', group: 'registry', derivedGroup: 'registry' },
|
|
333
|
+
{ name: 'NX_REMOTE_CACHE_TOKEN', group: 'nx-cache', derivedGroup: 'nx-cache' },
|
|
334
|
+
{ name: 'SMOO_TOKEN', group: 'shell', derivedGroup: 'shell' },
|
|
335
|
+
]);
|
|
336
|
+
},
|
|
337
|
+
);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it('a declared group wins, and the derivation it overrode stays visible', async () => {
|
|
341
|
+
await withFixture(
|
|
342
|
+
{
|
|
343
|
+
npmrc: REGISTRY_NPMRC,
|
|
344
|
+
secrets: {
|
|
345
|
+
SMOO_NPM_TOKEN: { command: emit('x'), group: 'shell' },
|
|
346
|
+
SMOO_TOKEN: { command: emit('x'), group: 'deploy' },
|
|
347
|
+
},
|
|
348
|
+
},
|
|
349
|
+
async (root) => {
|
|
350
|
+
expect(await groupsInBootstrap(root)).toEqual([
|
|
351
|
+
{ name: 'SMOO_NPM_TOKEN', group: 'shell', derivedGroup: 'registry' },
|
|
352
|
+
{ name: 'SMOO_TOKEN', group: 'deploy', derivedGroup: 'shell' },
|
|
353
|
+
]);
|
|
354
|
+
},
|
|
355
|
+
);
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('the cache token stays nx-cache even when .npmrc also references it', async () => {
|
|
359
|
+
await withFixture(
|
|
360
|
+
{
|
|
361
|
+
npmrc: '//npm.example.net/:_authToken=${NX_REMOTE_CACHE_TOKEN}\n',
|
|
362
|
+
secrets: { NX_REMOTE_CACHE_TOKEN: { command: emit('x') } },
|
|
363
|
+
remoteCache: { server: 'https://nx-cache.example.net', tokenSecret: 'NX_REMOTE_CACHE_TOKEN' },
|
|
364
|
+
},
|
|
365
|
+
async (root) => {
|
|
366
|
+
// The by-name exclusion this replaced was applied before the .npmrc
|
|
367
|
+
// reference was ever consulted; the derivation keeps that order, so
|
|
368
|
+
// one pathological manifest behaves exactly as it did.
|
|
369
|
+
expect(await groupsInBootstrap(root)).toEqual([
|
|
370
|
+
{ name: 'NX_REMOTE_CACHE_TOKEN', group: 'nx-cache', derivedGroup: 'nx-cache' },
|
|
371
|
+
]);
|
|
372
|
+
},
|
|
373
|
+
);
|
|
374
|
+
});
|
|
375
|
+
|
|
376
|
+
it('a repository declaring nothing has no groups', async () => {
|
|
377
|
+
await withFixture({ secrets: {} }, async (root) => {
|
|
378
|
+
expect(await groupsInBootstrap(root)).toEqual([]);
|
|
379
|
+
});
|
|
380
|
+
});
|
|
381
|
+
});
|
|
382
|
+
|
|
218
383
|
describe('resolveSecretEnvironment', () => {
|
|
219
384
|
it('an existing nonempty environment value wins and no command runs', async () => {
|
|
220
385
|
await withFixture(
|
|
@@ -222,7 +387,10 @@ describe('resolveSecretEnvironment', () => {
|
|
|
222
387
|
async (root) => {
|
|
223
388
|
// The declared command cannot run at all, so a resolution attempt would
|
|
224
389
|
// fail loudly; an empty outcome proves the existing value preempted it.
|
|
225
|
-
expect(await resolveInBootstrap({ root, env: { SMOO_TOKEN: 'already-set' } })).toEqual({
|
|
390
|
+
expect(await resolveInBootstrap({ root, env: { SMOO_TOKEN: 'already-set' } })).toEqual({
|
|
391
|
+
resolved: {},
|
|
392
|
+
deferred: [],
|
|
393
|
+
});
|
|
226
394
|
},
|
|
227
395
|
);
|
|
228
396
|
});
|
|
@@ -231,16 +399,23 @@ describe('resolveSecretEnvironment', () => {
|
|
|
231
399
|
await withFixture({ secrets: { SMOO_TOKEN: { command: emit('tok-from-provider') } } }, async (root) => {
|
|
232
400
|
expect(await resolveInBootstrap({ root, env: { SMOO_TOKEN: '' } })).toEqual({
|
|
233
401
|
resolved: { SMOO_TOKEN: 'tok-from-provider' },
|
|
402
|
+
deferred: [],
|
|
234
403
|
});
|
|
235
404
|
});
|
|
236
405
|
});
|
|
237
406
|
|
|
238
407
|
it('trims one terminal newline, LF or CRLF, preserving interior content', async () => {
|
|
239
408
|
await withFixture({ secrets: { SMOO_TOKEN: { command: emit('tok-from-provider\n') } } }, async (root) => {
|
|
240
|
-
expect(await resolveInBootstrap({ root, env: {} })).toEqual({
|
|
409
|
+
expect(await resolveInBootstrap({ root, env: {} })).toEqual({
|
|
410
|
+
resolved: { SMOO_TOKEN: 'tok-from-provider' },
|
|
411
|
+
deferred: [],
|
|
412
|
+
});
|
|
241
413
|
});
|
|
242
414
|
await withFixture({ secrets: { SMOO_TOKEN: { command: emit('tok\r\nmid\r\n') } } }, async (root) => {
|
|
243
|
-
expect(await resolveInBootstrap({ root, env: {} })).toEqual({
|
|
415
|
+
expect(await resolveInBootstrap({ root, env: {} })).toEqual({
|
|
416
|
+
resolved: { SMOO_TOKEN: 'tok\r\nmid' },
|
|
417
|
+
deferred: [],
|
|
418
|
+
});
|
|
244
419
|
});
|
|
245
420
|
});
|
|
246
421
|
|
|
@@ -265,16 +440,89 @@ describe('resolveSecretEnvironment', () => {
|
|
|
265
440
|
async (root) => {
|
|
266
441
|
// A cache is an optimization: its provider being down installs
|
|
267
442
|
// dependencies anyway. Every other declared secret keeps its refusal.
|
|
443
|
+
// The cache token is not excluded by name any more — it is in group
|
|
444
|
+
// `nx-cache`, which shell entry defers, so its provider is never
|
|
445
|
+
// reached to be unreachable.
|
|
268
446
|
expect(await resolveInBootstrap({ root, env: {} })).toEqual({
|
|
269
447
|
resolved: { SMOO_TOKEN: 'tok-from-provider' },
|
|
448
|
+
deferred: [
|
|
449
|
+
{ name: 'NX_REMOTE_CACHE_TOKEN', group: 'nx-cache', guidance: expect.stringContaining('nx-cache') },
|
|
450
|
+
],
|
|
270
451
|
});
|
|
271
452
|
const withoutCacheDeclaration = await resolveInBootstrap({ root, env: { CI: 'true' } });
|
|
272
453
|
expect(withoutCacheDeclaration.error).toContain('SMOO_TOKEN');
|
|
454
|
+
// CI refuses what shell entry needs, and the cache token is not that:
|
|
455
|
+
// nothing shell entry runs reads `smoo.remoteCache`.
|
|
273
456
|
expect(withoutCacheDeclaration.error).not.toContain('NX_REMOTE_CACHE_TOKEN');
|
|
274
457
|
},
|
|
275
458
|
);
|
|
276
459
|
});
|
|
277
460
|
|
|
461
|
+
it('the declared cache token is deferred at shell entry and its provider never runs', async () => {
|
|
462
|
+
await withProviderLedger(async (ledger) => {
|
|
463
|
+
await withFixture(
|
|
464
|
+
{
|
|
465
|
+
secrets: { NX_REMOTE_CACHE_TOKEN: { command: emitAndRecord('cache-tok', ledger.path) } },
|
|
466
|
+
remoteCache: { server: 'https://nx-cache.example.net', tokenSecret: 'NX_REMOTE_CACHE_TOKEN' },
|
|
467
|
+
},
|
|
468
|
+
async (root) => {
|
|
469
|
+
// Named in the result rather than silently omitted, which is what
|
|
470
|
+
// the by-name exclusion this replaces used to do.
|
|
471
|
+
expect(await resolveInBootstrap({ root, env: {} })).toEqual({
|
|
472
|
+
resolved: {},
|
|
473
|
+
deferred: [
|
|
474
|
+
{
|
|
475
|
+
name: 'NX_REMOTE_CACHE_TOKEN',
|
|
476
|
+
group: 'nx-cache',
|
|
477
|
+
guidance: expect.stringContaining('smoo secrets run nx-cache'),
|
|
478
|
+
},
|
|
479
|
+
],
|
|
480
|
+
});
|
|
481
|
+
expect(ledger.invocations()).toBe(0);
|
|
482
|
+
|
|
483
|
+
// And a cowshed workspace does not refuse it either: the gateway
|
|
484
|
+
// rule is about registry credentials, and this is not one.
|
|
485
|
+
expect(await resolveInBootstrap({ root, env: { COWSHED_WORKSPACE_TOKEN: 'gateway-session' } })).toEqual({
|
|
486
|
+
resolved: {},
|
|
487
|
+
deferred: [
|
|
488
|
+
{
|
|
489
|
+
name: 'NX_REMOTE_CACHE_TOKEN',
|
|
490
|
+
group: 'nx-cache',
|
|
491
|
+
guidance: expect.stringContaining('smoo secrets run nx-cache'),
|
|
492
|
+
},
|
|
493
|
+
],
|
|
494
|
+
});
|
|
495
|
+
expect(ledger.invocations()).toBe(0);
|
|
496
|
+
},
|
|
497
|
+
);
|
|
498
|
+
});
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('a deliberate nx-cache run resolves the cache token nothing else resolves', async () => {
|
|
502
|
+
await withProviderLedger(async (ledger) => {
|
|
503
|
+
await withFixture(
|
|
504
|
+
{
|
|
505
|
+
secrets: {
|
|
506
|
+
NX_REMOTE_CACHE_TOKEN: { command: emitAndRecord('cache-tok', ledger.path) },
|
|
507
|
+
SMOO_TOKEN: { command: emitAndRecord('shell-tok', ledger.path) },
|
|
508
|
+
},
|
|
509
|
+
remoteCache: { server: 'https://nx-cache.example.net', tokenSecret: 'NX_REMOTE_CACHE_TOKEN' },
|
|
510
|
+
},
|
|
511
|
+
async (root) => {
|
|
512
|
+
const outcome = await resolveInBootstrap({ root, env: {}, group: 'nx-cache' });
|
|
513
|
+
|
|
514
|
+
expect(outcome.resolved).toEqual({ NX_REMOTE_CACHE_TOKEN: 'cache-tok' });
|
|
515
|
+
expect(outcome.deferred).toEqual([
|
|
516
|
+
{ name: 'SMOO_TOKEN', group: 'shell', guidance: expect.stringContaining('smoo secrets run shell') },
|
|
517
|
+
]);
|
|
518
|
+
// One group, one provider invocation: the shell secret's command
|
|
519
|
+
// did not run for a cache-token request.
|
|
520
|
+
expect(ledger.invocations()).toBe(1);
|
|
521
|
+
},
|
|
522
|
+
);
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
|
|
278
526
|
it('a present variable is not reported while another refuses in CI', async () => {
|
|
279
527
|
await withFixture(
|
|
280
528
|
{
|
|
@@ -291,7 +539,7 @@ describe('resolveSecretEnvironment', () => {
|
|
|
291
539
|
);
|
|
292
540
|
});
|
|
293
541
|
|
|
294
|
-
it('cowshed
|
|
542
|
+
it('a cowshed workspace refuses a registry credential rather than deferring it', async () => {
|
|
295
543
|
await withFixture(
|
|
296
544
|
{
|
|
297
545
|
npmrc: '//npm.example.net/:_authToken=${SMOO_NPM_TOKEN}\n',
|
|
@@ -315,11 +563,169 @@ describe('resolveSecretEnvironment', () => {
|
|
|
315
563
|
async (root) => {
|
|
316
564
|
expect(await resolveInBootstrap({ root, env: { COWSHED_WORKSPACE_TOKEN: 'gateway-session' } })).toEqual({
|
|
317
565
|
resolved: { SMOO_GENERIC: 'generic-tok' },
|
|
566
|
+
deferred: [],
|
|
318
567
|
});
|
|
319
568
|
},
|
|
320
569
|
);
|
|
321
570
|
});
|
|
322
571
|
|
|
572
|
+
// A registry credential is the second instance of the rule the resolver's
|
|
573
|
+
// header states: shell entry never runs a provider command for a credential
|
|
574
|
+
// that only a network operation needs. The reference is what makes it one.
|
|
575
|
+
const REGISTRY_NPMRC = '@acme:registry=https://npm.example.net\n//npm.example.net/:_authToken=${SMOO_NPM_TOKEN}\n';
|
|
576
|
+
|
|
577
|
+
it('shell entry defers a .npmrc-referenced variable and never runs its provider', async () => {
|
|
578
|
+
await withProviderLedger(async (ledger) => {
|
|
579
|
+
await withFixture(
|
|
580
|
+
{
|
|
581
|
+
npmrc: REGISTRY_NPMRC,
|
|
582
|
+
secrets: { SMOO_NPM_TOKEN: { command: emitAndRecord('SHELL-ENTRY-RESOLVED-VALUE', ledger.path) } },
|
|
583
|
+
},
|
|
584
|
+
async (root) => {
|
|
585
|
+
const outcome = await resolveInBootstrap({ root, env: {}, group: 'shell' });
|
|
586
|
+
|
|
587
|
+
expect(outcome.resolved).toEqual({});
|
|
588
|
+
expect(outcome.error).toBeUndefined();
|
|
589
|
+
expect(ledger.invocations()).toBe(0);
|
|
590
|
+
// A failing install prints `- <name>: <guidance>`, so both halves
|
|
591
|
+
// have to be here: which variable, and the one command that
|
|
592
|
+
// supplies it.
|
|
593
|
+
expect(outcome.deferred).toEqual([
|
|
594
|
+
{
|
|
595
|
+
name: 'SMOO_NPM_TOKEN',
|
|
596
|
+
group: 'registry',
|
|
597
|
+
guidance: expect.stringContaining('smoo secrets run registry <command>'),
|
|
598
|
+
},
|
|
599
|
+
]);
|
|
600
|
+
},
|
|
601
|
+
);
|
|
602
|
+
});
|
|
603
|
+
});
|
|
604
|
+
|
|
605
|
+
it('a declared variable .npmrc does not reference still resolves at shell entry', async () => {
|
|
606
|
+
await withProviderLedger(async (ledger) => {
|
|
607
|
+
await withFixture(
|
|
608
|
+
{ npmrc: REGISTRY_NPMRC, secrets: { SMOO_GENERIC: { command: emitAndRecord('generic-tok', ledger.path) } } },
|
|
609
|
+
async (root) => {
|
|
610
|
+
// The same .npmrc, a variable it does not name: the deferral keys on
|
|
611
|
+
// the reference, not on a registry existing somewhere in the repo.
|
|
612
|
+
expect(await resolveInBootstrap({ root, env: {}, group: 'shell' })).toEqual({
|
|
613
|
+
resolved: { SMOO_GENERIC: 'generic-tok' },
|
|
614
|
+
deferred: [],
|
|
615
|
+
});
|
|
616
|
+
expect(ledger.invocations()).toBe(1);
|
|
617
|
+
},
|
|
618
|
+
);
|
|
619
|
+
});
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
it('a deliberate registry run resolves the variable shell entry deferred, and nothing else', async () => {
|
|
623
|
+
await withProviderLedger(async (ledger) => {
|
|
624
|
+
await withFixture(
|
|
625
|
+
{
|
|
626
|
+
npmrc: REGISTRY_NPMRC,
|
|
627
|
+
secrets: {
|
|
628
|
+
SMOO_NPM_TOKEN: { command: emitAndRecord('registry-tok', ledger.path) },
|
|
629
|
+
SMOO_TOKEN: { command: emitAndRecord('shell-tok', ledger.path) },
|
|
630
|
+
},
|
|
631
|
+
},
|
|
632
|
+
async (root) => {
|
|
633
|
+
const outcome = await resolveInBootstrap({ root, env: {}, group: 'registry' });
|
|
634
|
+
|
|
635
|
+
expect(outcome.resolved).toEqual({ SMOO_NPM_TOKEN: 'registry-tok' });
|
|
636
|
+
// The shell secret is deferred, not resolved: one group, one
|
|
637
|
+
// provider invocation. A wrapper that resolved every declared
|
|
638
|
+
// secret would have run two.
|
|
639
|
+
expect(outcome.deferred).toEqual([
|
|
640
|
+
{ name: 'SMOO_TOKEN', group: 'shell', guidance: expect.stringContaining('smoo secrets run shell') },
|
|
641
|
+
]);
|
|
642
|
+
expect(ledger.invocations()).toBe(1);
|
|
643
|
+
},
|
|
644
|
+
);
|
|
645
|
+
});
|
|
646
|
+
});
|
|
647
|
+
|
|
648
|
+
it('a declared group is what shell entry resolves, derivation notwithstanding', async () => {
|
|
649
|
+
await withProviderLedger(async (ledger) => {
|
|
650
|
+
await withFixture(
|
|
651
|
+
{
|
|
652
|
+
npmrc: REGISTRY_NPMRC,
|
|
653
|
+
secrets: { SMOO_NPM_TOKEN: { command: emitAndRecord('registry-tok', ledger.path), group: 'shell' } },
|
|
654
|
+
},
|
|
655
|
+
async (root) => {
|
|
656
|
+
// The repository says this credential belongs to the shell, and
|
|
657
|
+
// that is the whole of the rule: the override is not a hint.
|
|
658
|
+
expect(await resolveInBootstrap({ root, env: {}, group: 'shell' })).toEqual({
|
|
659
|
+
resolved: { SMOO_NPM_TOKEN: 'registry-tok' },
|
|
660
|
+
deferred: [],
|
|
661
|
+
});
|
|
662
|
+
expect(ledger.invocations()).toBe(1);
|
|
663
|
+
},
|
|
664
|
+
);
|
|
665
|
+
});
|
|
666
|
+
});
|
|
667
|
+
|
|
668
|
+
it('an exported registry credential wins over the deferral', async () => {
|
|
669
|
+
await withProviderLedger(async (ledger) => {
|
|
670
|
+
await withFixture(
|
|
671
|
+
{ npmrc: REGISTRY_NPMRC, secrets: { SMOO_NPM_TOKEN: { command: emitAndRecord('x', ledger.path) } } },
|
|
672
|
+
async (root) => {
|
|
673
|
+
// Nothing to defer and nothing to run: the developer who exported it
|
|
674
|
+
// for this terminal already answered the question.
|
|
675
|
+
expect(await resolveInBootstrap({ root, env: { SMOO_NPM_TOKEN: 'exported' }, group: 'shell' })).toEqual({
|
|
676
|
+
resolved: {},
|
|
677
|
+
deferred: [],
|
|
678
|
+
});
|
|
679
|
+
expect(ledger.invocations()).toBe(0);
|
|
680
|
+
},
|
|
681
|
+
);
|
|
682
|
+
});
|
|
683
|
+
});
|
|
684
|
+
|
|
685
|
+
it('CI refuses a missing registry credential instead of deferring it', async () => {
|
|
686
|
+
await withProviderLedger(async (ledger) => {
|
|
687
|
+
await withFixture(
|
|
688
|
+
{ npmrc: REGISTRY_NPMRC, secrets: { SMOO_NPM_TOKEN: { command: emitAndRecord('x', ledger.path) } } },
|
|
689
|
+
async (root) => {
|
|
690
|
+
// CI outranks the registry rule and is unchanged by it: a job that
|
|
691
|
+
// needs the credential says so at setup rather than discovering it
|
|
692
|
+
// at the first fetch, and there is no wrapper to run there.
|
|
693
|
+
const outcome = await resolveInBootstrap({ root, env: { CI: 'true' }, group: 'shell' });
|
|
694
|
+
|
|
695
|
+
expect(outcome.error).toContain('SMOO_NPM_TOKEN');
|
|
696
|
+
expect(outcome.error).toContain('inject');
|
|
697
|
+
expect(outcome.error).not.toContain('smoo secrets run');
|
|
698
|
+
expect(ledger.invocations()).toBe(0);
|
|
699
|
+
},
|
|
700
|
+
);
|
|
701
|
+
});
|
|
702
|
+
});
|
|
703
|
+
|
|
704
|
+
it('a cowshed workspace refuses a registry credential for a network operation too', async () => {
|
|
705
|
+
await withProviderLedger(async (ledger) => {
|
|
706
|
+
await withFixture(
|
|
707
|
+
{
|
|
708
|
+
npmrc: REGISTRY_NPMRC,
|
|
709
|
+
secrets: { SMOO_NPM_TOKEN: { command: emitAndRecord('LOCAL-RESOLVED-VALUE', ledger.path) } },
|
|
710
|
+
},
|
|
711
|
+
async (root) => {
|
|
712
|
+
// The gateway is the enrollment path there, so the wrapper does not
|
|
713
|
+
// become a way around it.
|
|
714
|
+
const outcome = await resolveInBootstrap({
|
|
715
|
+
root,
|
|
716
|
+
env: { COWSHED_WORKSPACE_TOKEN: 'gateway-session' },
|
|
717
|
+
group: 'registry',
|
|
718
|
+
});
|
|
719
|
+
|
|
720
|
+
expect(outcome.error).toContain('SMOO_NPM_TOKEN');
|
|
721
|
+
expect(outcome.error).toContain('cowshed gateway');
|
|
722
|
+
expect(outcome.error).not.toContain('LOCAL-RESOLVED-VALUE');
|
|
723
|
+
expect(ledger.invocations()).toBe(0);
|
|
724
|
+
},
|
|
725
|
+
);
|
|
726
|
+
});
|
|
727
|
+
});
|
|
728
|
+
|
|
323
729
|
it('provider failures name the variable, the exit code, and nothing secret-shaped', async () => {
|
|
324
730
|
await withFixture(
|
|
325
731
|
{
|