@smoothbricks/cli 0.11.12 → 0.11.13
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 +104 -20
- package/dist/lib/json.d.ts +51 -0
- package/dist/lib/json.d.ts.map +1 -1
- package/dist/lib/json.js +111 -67
- package/dist/monorepo/ci-workflow.d.ts +27 -2
- package/dist/monorepo/ci-workflow.d.ts.map +1 -1
- package/dist/monorepo/ci-workflow.js +151 -11
- package/dist/monorepo/managed-files.d.ts +3 -1
- package/dist/monorepo/managed-files.d.ts.map +1 -1
- package/dist/monorepo/managed-files.js +3 -0
- package/dist/monorepo/publish-workflow.d.ts +8 -1
- package/dist/monorepo/publish-workflow.d.ts.map +1 -1
- package/dist/monorepo/publish-workflow.js +15 -5
- package/managed/raw/tooling/direnv/devenv.smoo.nix +22 -2
- package/managed/raw/tooling/direnv/secret-references.ts +248 -51
- package/managed/templates/github/actions/save-nix-devenv/action.yml +2 -2
- package/managed/templates/github/actions/setup-devenv/action.yml +43 -5
- package/package.json +2 -2
- package/src/lib/json.ts +52 -0
- package/src/monorepo/__tests__/ci-workflow.test.ts +183 -0
- package/src/monorepo/__tests__/publish-workflow.test.ts +29 -0
- package/src/monorepo/ci-workflow.ts +180 -11
- package/src/monorepo/managed-files.ts +6 -0
- package/src/monorepo/publish-workflow.ts +24 -4
- package/src/monorepo/secret-references.test.ts +175 -2
|
@@ -136,14 +136,18 @@ async function resolveInBootstrap(options: {
|
|
|
136
136
|
|
|
137
137
|
/** Builds a fixture repository whose package.json and .npmrc are the resolver's real inputs. */
|
|
138
138
|
async function withFixture(
|
|
139
|
-
options: { secrets: Record<string, SecretSpec>; npmrc?: string },
|
|
139
|
+
options: { secrets: Record<string, SecretSpec>; npmrc?: string; remoteCache?: unknown },
|
|
140
140
|
run: (root: string) => Promise<void>,
|
|
141
141
|
): Promise<void> {
|
|
142
142
|
const root = await mkdtemp(join(tmpdir(), 'smoo-secret-references-'));
|
|
143
143
|
try {
|
|
144
144
|
await writeFile(
|
|
145
145
|
join(root, 'package.json'),
|
|
146
|
-
JSON.stringify({
|
|
146
|
+
JSON.stringify({
|
|
147
|
+
name: 'fixture',
|
|
148
|
+
version: '0.0.0',
|
|
149
|
+
smoo: { secrets: options.secrets, remoteCache: options.remoteCache },
|
|
150
|
+
}),
|
|
147
151
|
);
|
|
148
152
|
if (options.npmrc !== undefined) {
|
|
149
153
|
await writeFile(join(root, '.npmrc'), options.npmrc);
|
|
@@ -249,6 +253,28 @@ describe('resolveSecretEnvironment', () => {
|
|
|
249
253
|
});
|
|
250
254
|
});
|
|
251
255
|
|
|
256
|
+
it('an unreachable cache-token provider does not block the install, while other secrets still refuse', async () => {
|
|
257
|
+
await withFixture(
|
|
258
|
+
{
|
|
259
|
+
secrets: {
|
|
260
|
+
NX_REMOTE_CACHE_TOKEN: { command: ['definitely-not-a-real-smoo-binary-xyz'] },
|
|
261
|
+
SMOO_TOKEN: { command: emit('tok-from-provider') },
|
|
262
|
+
},
|
|
263
|
+
remoteCache: { server: 'https://nx-cache.example.net', tokenSecret: 'NX_REMOTE_CACHE_TOKEN' },
|
|
264
|
+
},
|
|
265
|
+
async (root) => {
|
|
266
|
+
// A cache is an optimization: its provider being down installs
|
|
267
|
+
// dependencies anyway. Every other declared secret keeps its refusal.
|
|
268
|
+
expect(await resolveInBootstrap({ root, env: {} })).toEqual({
|
|
269
|
+
resolved: { SMOO_TOKEN: 'tok-from-provider' },
|
|
270
|
+
});
|
|
271
|
+
const withoutCacheDeclaration = await resolveInBootstrap({ root, env: { CI: 'true' } });
|
|
272
|
+
expect(withoutCacheDeclaration.error).toContain('SMOO_TOKEN');
|
|
273
|
+
expect(withoutCacheDeclaration.error).not.toContain('NX_REMOTE_CACHE_TOKEN');
|
|
274
|
+
},
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
|
|
252
278
|
it('a present variable is not reported while another refuses in CI', async () => {
|
|
253
279
|
await withFixture(
|
|
254
280
|
{
|
|
@@ -361,3 +387,150 @@ describe('resolveSecretEnvironment', () => {
|
|
|
361
387
|
}
|
|
362
388
|
});
|
|
363
389
|
});
|
|
390
|
+
|
|
391
|
+
/**
|
|
392
|
+
* The remote cache half is exercised the way the managed shell runs it — the
|
|
393
|
+
* raw script as a program, its stdout `eval`-ed — because that round trip is
|
|
394
|
+
* the contract: shell text on stdout, guidance on stderr, and a status that
|
|
395
|
+
* never keeps a shell from opening.
|
|
396
|
+
*/
|
|
397
|
+
describe('remote cache shell export', () => {
|
|
398
|
+
const SERVER = 'https://nx-cache.example.net';
|
|
399
|
+
|
|
400
|
+
async function exportFor(root: string, env: Record<string, string>): Promise<{ stdout: string; stderr: string }> {
|
|
401
|
+
const proc = Bun.spawn({
|
|
402
|
+
cmd: ['bun', RAW_RESOLVER, root],
|
|
403
|
+
env: { PATH: process.env['PATH'], HOME: process.env['HOME'], ...env },
|
|
404
|
+
stdin: 'ignore',
|
|
405
|
+
stdout: 'pipe',
|
|
406
|
+
stderr: 'pipe',
|
|
407
|
+
});
|
|
408
|
+
const [stdout, stderr, exitCode] = await Promise.all([
|
|
409
|
+
new Response(proc.stdout).text(),
|
|
410
|
+
new Response(proc.stderr).text(),
|
|
411
|
+
proc.exited,
|
|
412
|
+
]);
|
|
413
|
+
// A cache is an optimization: nothing it does may fail shell entry.
|
|
414
|
+
expect(exitCode).toBe(0);
|
|
415
|
+
return { stdout, stderr };
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/** The value a shell ends up with after eval-ing the script's output. */
|
|
419
|
+
async function evaluated(root: string, env: Record<string, string>, name: string): Promise<string> {
|
|
420
|
+
const { stdout } = await exportFor(root, env);
|
|
421
|
+
const shell = Bun.spawnSync({
|
|
422
|
+
cmd: ['sh', '-c', `${stdout}printf %s "\${${name}:-}"`],
|
|
423
|
+
env: { PATH: process.env['PATH'] ?? '/usr/bin:/bin' },
|
|
424
|
+
});
|
|
425
|
+
expect(shell.exitCode).toBe(0);
|
|
426
|
+
return shell.stdout.toString();
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
it('exports the pair from an ambient token and nothing from an absent declaration', async () => {
|
|
430
|
+
await withFixture(
|
|
431
|
+
{ secrets: {}, remoteCache: { server: SERVER, tokenSecret: 'NX_REMOTE_CACHE_TOKEN' } },
|
|
432
|
+
async (root) => {
|
|
433
|
+
expect((await exportFor(root, { NX_REMOTE_CACHE_TOKEN: 'ambient-token' })).stdout).toBe(
|
|
434
|
+
`export NX_SELF_HOSTED_REMOTE_CACHE_SERVER='${SERVER}'\nexport NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN='ambient-token'\n`,
|
|
435
|
+
);
|
|
436
|
+
},
|
|
437
|
+
);
|
|
438
|
+
await withFixture({ secrets: {} }, async (root) => {
|
|
439
|
+
expect(await exportFor(root, { NX_REMOTE_CACHE_TOKEN: 'ambient-token' })).toEqual({ stdout: '', stderr: '' });
|
|
440
|
+
});
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
it('exports nothing and names the variable when the token has no value', async () => {
|
|
444
|
+
await withFixture(
|
|
445
|
+
{ secrets: {}, remoteCache: { server: SERVER, tokenSecret: 'NX_REMOTE_CACHE_TOKEN' } },
|
|
446
|
+
async (root) => {
|
|
447
|
+
const { stdout, stderr } = await exportFor(root, {});
|
|
448
|
+
// Half a pair is worse than none: Nx enables its cache on the server
|
|
449
|
+
// alone and then fails every task on 401.
|
|
450
|
+
expect(stdout).toBe('');
|
|
451
|
+
expect(stderr).toContain('NX_REMOTE_CACHE_TOKEN');
|
|
452
|
+
expect(stderr).toContain(SERVER);
|
|
453
|
+
},
|
|
454
|
+
);
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it('reports a broken provider as a lost cache, never as a failed install', async () => {
|
|
458
|
+
await withFixture(
|
|
459
|
+
{
|
|
460
|
+
secrets: { NX_REMOTE_CACHE_TOKEN: { command: ['definitely-not-a-real-smoo-binary-xyz'] } },
|
|
461
|
+
remoteCache: { server: SERVER, tokenSecret: 'NX_REMOTE_CACHE_TOKEN' },
|
|
462
|
+
},
|
|
463
|
+
async (root) => {
|
|
464
|
+
const { stdout, stderr } = await exportFor(root, {});
|
|
465
|
+
expect(stdout).toBe('');
|
|
466
|
+
expect(stderr).toContain('NX_REMOTE_CACHE_TOKEN');
|
|
467
|
+
expect(stderr).toContain('remote cache off');
|
|
468
|
+
// Nothing here stopped an install, and the message may not say it did.
|
|
469
|
+
expect(stderr).not.toContain('dependencies were not installed');
|
|
470
|
+
},
|
|
471
|
+
);
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
it('leaves a server the environment already carries alone', async () => {
|
|
475
|
+
await withFixture(
|
|
476
|
+
{ secrets: {}, remoteCache: { server: SERVER, tokenSecret: 'NX_REMOTE_CACHE_TOKEN' } },
|
|
477
|
+
async (root) => {
|
|
478
|
+
// A CI job env names the address its own runners reach; the public
|
|
479
|
+
// origin must not replace it.
|
|
480
|
+
expect(
|
|
481
|
+
await exportFor(root, {
|
|
482
|
+
NX_SELF_HOSTED_REMOTE_CACHE_SERVER: 'http://10.89.0.1:8765',
|
|
483
|
+
NX_REMOTE_CACHE_TOKEN: 'ambient-token',
|
|
484
|
+
}),
|
|
485
|
+
).toEqual({ stdout: '', stderr: '' });
|
|
486
|
+
},
|
|
487
|
+
);
|
|
488
|
+
});
|
|
489
|
+
|
|
490
|
+
it('resolves a provider-declared token, and only that one secret', async () => {
|
|
491
|
+
await withFixture(
|
|
492
|
+
{
|
|
493
|
+
secrets: {
|
|
494
|
+
NX_REMOTE_CACHE_TOKEN: { command: emit('provider-token') },
|
|
495
|
+
SMOO_OTHER: { command: ['definitely-not-a-real-smoo-binary-xyz'] },
|
|
496
|
+
},
|
|
497
|
+
remoteCache: { server: SERVER, tokenSecret: 'NX_REMOTE_CACHE_TOKEN' },
|
|
498
|
+
},
|
|
499
|
+
async (root) => {
|
|
500
|
+
// The unrunnable sibling proves no other declared secret is resolved
|
|
501
|
+
// here, and that none of them reaches the shell.
|
|
502
|
+
const { stdout } = await exportFor(root, {});
|
|
503
|
+
expect(stdout).toContain("export NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN='provider-token'");
|
|
504
|
+
expect(stdout).not.toContain('SMOO_OTHER');
|
|
505
|
+
},
|
|
506
|
+
);
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
it('survives eval with a token full of shell metacharacters', async () => {
|
|
510
|
+
const hostile = `it's $(touch /tmp/smoo-cache-pwned) \`x\` "q" \\`;
|
|
511
|
+
await withFixture(
|
|
512
|
+
{ secrets: {}, remoteCache: { server: SERVER, tokenSecret: 'NX_REMOTE_CACHE_TOKEN' } },
|
|
513
|
+
async (root) => {
|
|
514
|
+
expect(
|
|
515
|
+
await evaluated(root, { NX_REMOTE_CACHE_TOKEN: hostile }, 'NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN'),
|
|
516
|
+
).toBe(hostile);
|
|
517
|
+
},
|
|
518
|
+
);
|
|
519
|
+
});
|
|
520
|
+
|
|
521
|
+
it('refuses a declaration Nx could not use, naming the field', async () => {
|
|
522
|
+
for (const remoteCache of [
|
|
523
|
+
{ server: `${SERVER}/`, tokenSecret: 'NX_REMOTE_CACHE_TOKEN' },
|
|
524
|
+
{ server: '', tokenSecret: 'NX_REMOTE_CACHE_TOKEN' },
|
|
525
|
+
{ server: SERVER, tokenSecret: 'has-dash' },
|
|
526
|
+
{ server: SERVER },
|
|
527
|
+
'https://nx-cache.example.net',
|
|
528
|
+
]) {
|
|
529
|
+
await withFixture({ secrets: {}, remoteCache }, async (root) => {
|
|
530
|
+
const { stdout, stderr } = await exportFor(root, { NX_REMOTE_CACHE_TOKEN: 'ambient-token' });
|
|
531
|
+
expect(stdout).toBe('');
|
|
532
|
+
expect(stderr).toContain('smoo.remoteCache');
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
});
|
|
536
|
+
});
|