@smoothbricks/cli 0.11.13 → 0.11.15
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/dist/cli.js +6 -1
- package/dist/monorepo/index.d.ts +2 -0
- package/dist/monorepo/index.d.ts.map +1 -1
- package/dist/monorepo/index.js +1 -1
- package/dist/monorepo/packed-package.d.ts +3 -3
- package/dist/monorepo/packed-package.d.ts.map +1 -1
- package/dist/monorepo/packed-package.js +11 -6
- package/dist/monorepo/packs/index.d.ts +8 -0
- package/dist/monorepo/packs/index.d.ts.map +1 -1
- package/dist/monorepo/packs/index.js +6 -5
- package/dist/monorepo/publish-workflow.js +5 -1
- package/dist/release/github-release.d.ts +16 -0
- package/dist/release/github-release.d.ts.map +1 -1
- package/dist/release/github-release.js +11 -2
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +3 -3
- package/managed/raw/tooling/direnv/devenv.smoo.nix +17 -13
- package/managed/raw/tooling/direnv/github-actions-bootstrap.sh +35 -0
- package/managed/raw/tooling/direnv/secret-references.ts +79 -125
- package/managed/raw/tooling/direnv/setup-environment.ts +81 -26
- package/managed/templates/github/actions/setup-devenv/action.yml +19 -6
- package/package.json +2 -2
- package/src/cli.ts +10 -1
- package/src/monorepo/__tests__/publish-workflow.test.ts +3 -0
- package/src/monorepo/index.ts +6 -1
- package/src/monorepo/packed-package.ts +21 -6
- package/src/monorepo/packs/index.ts +14 -5
- package/src/monorepo/publish-workflow.ts +8 -1
- package/src/monorepo/secret-references.test.ts +52 -131
- package/src/release/github-release.ts +31 -2
- package/src/release/index.ts +3 -2
|
@@ -389,148 +389,69 @@ describe('resolveSecretEnvironment', () => {
|
|
|
389
389
|
});
|
|
390
390
|
|
|
391
391
|
/**
|
|
392
|
-
*
|
|
393
|
-
*
|
|
394
|
-
*
|
|
395
|
-
* never keeps a shell from opening.
|
|
392
|
+
* Redaction is a byte operation on output the caller already captured, so the
|
|
393
|
+
* harness carries both directions as base64: a text-only round trip would
|
|
394
|
+
* hide exactly the invalid-UTF-8 bytes a failing install can emit.
|
|
396
395
|
*/
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
-
}
|
|
396
|
+
const MASK_SCRIPT = `
|
|
397
|
+
const resolver = await import(Bun.argv[1]);
|
|
398
|
+
const { output, values } = JSON.parse(Bun.argv[2]);
|
|
399
|
+
const captured = Buffer.from(output, 'base64');
|
|
400
|
+
const masked = resolver.maskSecretValues(captured, values);
|
|
401
|
+
process.stdout.write(JSON.stringify({
|
|
402
|
+
masked: Buffer.from(masked).toString('base64'),
|
|
403
|
+
captured: captured.toString('base64'),
|
|
404
|
+
}));
|
|
405
|
+
`;
|
|
428
406
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
`export NX_SELF_HOSTED_REMOTE_CACHE_SERVER='${SERVER}'\nexport NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN='ambient-token'\n`,
|
|
435
|
-
);
|
|
436
|
-
},
|
|
407
|
+
describe('maskSecretValues', () => {
|
|
408
|
+
const mask = async (output: Uint8Array, values: readonly string[]): Promise<{ masked: Buffer; captured: Buffer }> => {
|
|
409
|
+
const { stdout, stderr, exitCode } = await runInBootstrap(
|
|
410
|
+
MASK_SCRIPT,
|
|
411
|
+
JSON.stringify({ output: Buffer.from(output).toString('base64'), values }),
|
|
437
412
|
);
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
413
|
+
expect(stderr).toBe('');
|
|
414
|
+
expect(exitCode).toBe(0);
|
|
415
|
+
const value: unknown = JSON.parse(stdout);
|
|
416
|
+
if (
|
|
417
|
+
typeof value !== 'object' ||
|
|
418
|
+
value === null ||
|
|
419
|
+
!('masked' in value) ||
|
|
420
|
+
typeof value.masked !== 'string' ||
|
|
421
|
+
!('captured' in value) ||
|
|
422
|
+
typeof value.captured !== 'string'
|
|
423
|
+
) {
|
|
424
|
+
throw new Error(`bootstrap harness returned a malformed envelope: ${stdout}`);
|
|
425
|
+
}
|
|
426
|
+
return { masked: Buffer.from(value.masked, 'base64'), captured: Buffer.from(value.captured, 'base64') };
|
|
427
|
+
};
|
|
442
428
|
|
|
443
|
-
it('
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
expect(stderr).toContain('NX_REMOTE_CACHE_TOKEN');
|
|
452
|
-
expect(stderr).toContain(SERVER);
|
|
453
|
-
},
|
|
454
|
-
);
|
|
429
|
+
it('replaces every occurrence and leaves the surrounding failure legible', async () => {
|
|
430
|
+
const output = Buffer.from('error: 401 for https://x:hunter2@registry/pkg\nretrying with hunter2\n');
|
|
431
|
+
const { masked, captured } = await mask(output, ['hunter2']);
|
|
432
|
+
expect(masked.toString()).toBe('error: 401 for https://x:*******@registry/pkg\nretrying with *******\n');
|
|
433
|
+
expect(masked.length).toBe(output.length);
|
|
434
|
+
// The caller keeps its captured bytes: redaction returns a copy, and a
|
|
435
|
+
// node Buffer's `slice` would have handed back a view of these.
|
|
436
|
+
expect(captured.toString()).toBe(output.toString());
|
|
455
437
|
});
|
|
456
438
|
|
|
457
|
-
it('
|
|
458
|
-
await
|
|
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
|
-
);
|
|
439
|
+
it('redacts each declared value even when one is a prefix of another', async () => {
|
|
440
|
+
expect((await mask(Buffer.from('AB ABCD AB'), ['ABCD', 'AB'])).masked.toString()).toBe('** **** **');
|
|
472
441
|
});
|
|
473
442
|
|
|
474
|
-
it('
|
|
475
|
-
await
|
|
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
|
-
);
|
|
443
|
+
it('survives a false start on the first byte', async () => {
|
|
444
|
+
expect((await mask(Buffer.from('aab aaab'), ['aab'])).masked.toString()).toBe('*** a***');
|
|
488
445
|
});
|
|
489
446
|
|
|
490
|
-
it('
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
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
|
-
);
|
|
447
|
+
it('masks whole bytes and carries invalid UTF-8 through untouched', async () => {
|
|
448
|
+
const output = Buffer.concat([Buffer.from([0xff, 0xfe]), Buffer.from('kå'), Buffer.from([0x00])]);
|
|
449
|
+
const { masked } = await mask(output, ['kå']);
|
|
450
|
+
expect([...masked]).toEqual([0xff, 0xfe, 0x2a, 0x2a, 0x2a, 0x00]);
|
|
507
451
|
});
|
|
508
452
|
|
|
509
|
-
it('
|
|
510
|
-
|
|
511
|
-
await
|
|
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
|
-
}
|
|
453
|
+
it('leaves output alone when nothing is declared or a value is empty', async () => {
|
|
454
|
+
expect((await mask(Buffer.from('nothing to hide'), [])).masked.toString()).toBe('nothing to hide');
|
|
455
|
+
expect((await mask(Buffer.from('nothing to hide'), [''])).masked.toString()).toBe('nothing to hide');
|
|
535
456
|
});
|
|
536
457
|
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { createRequire } from 'node:module';
|
|
2
3
|
import { tmpdir } from 'node:os';
|
|
3
4
|
import { join } from 'node:path';
|
|
5
|
+
import { pathToFileURL } from 'node:url';
|
|
4
6
|
import type { ChangelogOptions } from 'nx/src/command-line/release/command-object.js';
|
|
5
7
|
import type { NxReleaseConfiguration } from 'nx/src/config/nx-json.js';
|
|
6
8
|
import { type ReleasePackageInfo, releaseTag } from './core.js';
|
|
@@ -34,7 +36,7 @@ export interface GithubReleaseWriteShell {
|
|
|
34
36
|
|
|
35
37
|
export async function renderNxProjectChangelogContents(input: RenderNxProjectChangelogInput): Promise<string> {
|
|
36
38
|
return withNxWorkspaceRoot(input.root, async () => {
|
|
37
|
-
const { createAPI } = await
|
|
39
|
+
const { createAPI } = await importWorkspaceNx(input.root, 'src/command-line/release/changelog.js');
|
|
38
40
|
const result = await createAPI(
|
|
39
41
|
nxRenderOnlyReleaseConfig,
|
|
40
42
|
false,
|
|
@@ -124,8 +126,35 @@ function isPrereleaseVersion(version: string): boolean {
|
|
|
124
126
|
return version.includes('-');
|
|
125
127
|
}
|
|
126
128
|
|
|
129
|
+
/**
|
|
130
|
+
* A module of the WORKSPACE's Nx, not of smoo's own dependency. smoo installs
|
|
131
|
+
* with its own `nx` in the global virtual store, where the workspace's
|
|
132
|
+
* node_modules is invisible: that Nx cannot resolve the workspace's
|
|
133
|
+
* `versionActions` plugin (`Unable to resolve the "versionActions"
|
|
134
|
+
* implementation ... "@smoothbricks/nx-plugin/version-actions"`), and it is
|
|
135
|
+
* a second Nx version driving one release. Every in-process Nx call for a
|
|
136
|
+
* workspace resolves from that workspace's root, exactly as `nx` on its PATH
|
|
137
|
+
* would.
|
|
138
|
+
*/
|
|
139
|
+
interface WorkspaceNxModules {
|
|
140
|
+
'src/utils/workspace-root.js': typeof import('nx/src/utils/workspace-root.js');
|
|
141
|
+
'src/command-line/release/version.js': typeof import('nx/src/command-line/release/version.js');
|
|
142
|
+
'src/command-line/release/changelog.js': typeof import('nx/src/command-line/release/changelog.js');
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
export async function importWorkspaceNx<Subpath extends keyof WorkspaceNxModules>(
|
|
146
|
+
root: string,
|
|
147
|
+
subpath: Subpath,
|
|
148
|
+
): Promise<WorkspaceNxModules[Subpath]> {
|
|
149
|
+
const resolved = createRequire(join(root, 'package.json')).resolve(`nx/${subpath}`);
|
|
150
|
+
// A dynamic specifier types as `any`; the map above is the declared shape of
|
|
151
|
+
// the module the workspace's Nx serves at that subpath.
|
|
152
|
+
const module: WorkspaceNxModules[Subpath] = await import(pathToFileURL(resolved).href);
|
|
153
|
+
return module;
|
|
154
|
+
}
|
|
155
|
+
|
|
127
156
|
export async function withNxWorkspaceRoot<T>(root: string, run: () => Promise<T>): Promise<T> {
|
|
128
|
-
const workspaceRootModule = await
|
|
157
|
+
const workspaceRootModule = await importWorkspaceNx(root, 'src/utils/workspace-root.js');
|
|
129
158
|
const previousWorkspaceRoot = workspaceRootModule.workspaceRoot;
|
|
130
159
|
const previousEnvWorkspaceRoot = process.env.NX_WORKSPACE_ROOT_PATH;
|
|
131
160
|
const previousCwd = process.cwd();
|
package/src/release/index.ts
CHANGED
|
@@ -88,6 +88,7 @@ import {
|
|
|
88
88
|
import {
|
|
89
89
|
createOrUpdateGithubRelease,
|
|
90
90
|
githubReleaseLookupExists,
|
|
91
|
+
importWorkspaceNx,
|
|
91
92
|
renderNxProjectChangelogContents,
|
|
92
93
|
withNxWorkspaceRoot,
|
|
93
94
|
} from './github-release.js';
|
|
@@ -881,8 +882,8 @@ async function runNxReleaseVersion(
|
|
|
881
882
|
|
|
882
883
|
async function runNxReleaseVersionPreview(root: string, projects: string, bump: string): Promise<ReleasePackage[]> {
|
|
883
884
|
return withNxWorkspaceRoot(root, async () => {
|
|
884
|
-
const { createAPI } = await
|
|
885
|
-
const { createAPI: createChangelogAPI } = await
|
|
885
|
+
const { createAPI } = await importWorkspaceNx(root, 'src/command-line/release/version.js');
|
|
886
|
+
const { createAPI: createChangelogAPI } = await importWorkspaceNx(root, 'src/command-line/release/changelog.js');
|
|
886
887
|
const projectNames = projects.split(',').filter(Boolean);
|
|
887
888
|
const result = await createAPI(
|
|
888
889
|
{},
|