@smoothbricks/cli 0.11.15 → 0.11.16
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/lib/json.d.ts +9 -0
- package/dist/lib/json.d.ts.map +1 -1
- package/dist/lib/json.js +65 -53
- 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.map +1 -1
- package/dist/monorepo/publish-workflow.js +52 -2
- 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/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/lib/json.ts +9 -0
- package/src/monorepo/__tests__/publish-workflow.test.ts +54 -0
- package/src/monorepo/managed-files.test.ts +26 -0
- package/src/monorepo/managed-files.ts +36 -2
- package/src/monorepo/publish-workflow.ts +70 -2
- 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
|
@@ -433,6 +433,43 @@ describe('private npm published-version status', () => {
|
|
|
433
433
|
});
|
|
434
434
|
});
|
|
435
435
|
|
|
436
|
+
it('reads through a repository .npmrc whose own auth line names an unset env', async () => {
|
|
437
|
+
// The shape that cost a day: AxE commits
|
|
438
|
+
// `//host/path:_authToken=${AXE_NPM_PUBLISH_TOKEN}` at the workspace root.
|
|
439
|
+
// npm ranks that project file ABOVE the userconfig this CLI writes, so with
|
|
440
|
+
// the publish env unset it sent the unexpanded value and the registry
|
|
441
|
+
// answered 401 — with a perfectly good read credential in hand.
|
|
442
|
+
await withStatusFixture(async ({ fixture, root, userconfig }) => {
|
|
443
|
+
await fixture.privateRegistry.publishPackage({ name: PRIVATE_PACKAGE, version: PRIVATE_VERSION });
|
|
444
|
+
const resolved = loopbackRegistry(fixture.privateRegistry);
|
|
445
|
+
await writeFile(
|
|
446
|
+
join(root, '.npmrc'),
|
|
447
|
+
`${resolved.scope}:registry=${resolved.registry}\n${resolved.authKey}=\${NOT_SET_ANYWHERE}\n`,
|
|
448
|
+
);
|
|
449
|
+
|
|
450
|
+
// Control: with the userconfig alone the project file wins, so the token
|
|
451
|
+
// npm sends is the unexpanded reference — the registry sees no usable
|
|
452
|
+
// credential. Asserted on the wire, because a fixture that answers
|
|
453
|
+
// anonymous reads would let the server's verdict hide it.
|
|
454
|
+
await npmPublishedVersionExists(root, PRIVATE_PACKAGE, PRIVATE_VERSION, {
|
|
455
|
+
registry: fixture.privateRegistry.registry,
|
|
456
|
+
userconfig,
|
|
457
|
+
});
|
|
458
|
+
const overridden = fixture.privateRegistry.requestsFor('probe').at(-1);
|
|
459
|
+
expect(overridden?.authorization ?? '').not.toContain(FIXTURE_READ_TOKEN);
|
|
460
|
+
|
|
461
|
+
await expect(
|
|
462
|
+
npmPublishedVersionExists(root, PRIVATE_PACKAGE, PRIVATE_VERSION, {
|
|
463
|
+
registry: fixture.privateRegistry.registry,
|
|
464
|
+
userconfig,
|
|
465
|
+
credential: { authKey: resolved.authKey, tokenEnv: FIXTURE_READ_TOKEN_ENV },
|
|
466
|
+
}),
|
|
467
|
+
).resolves.toBe(true);
|
|
468
|
+
const authenticated = fixture.privateRegistry.requestsFor('probe').at(-1);
|
|
469
|
+
expect(authenticated?.authorization ?? '').toContain(FIXTURE_READ_TOKEN);
|
|
470
|
+
});
|
|
471
|
+
});
|
|
472
|
+
|
|
436
473
|
it('reports a genuine not-found as absent', async () => {
|
|
437
474
|
await withStatusFixture(async ({ fixture, root, userconfig }) => {
|
|
438
475
|
fixture.privateRegistry.failWith(404);
|
|
@@ -86,17 +86,37 @@ describe('private npm workflow token selection', () => {
|
|
|
86
86
|
});
|
|
87
87
|
|
|
88
88
|
it('ignores workspace and link specs so a producer install does not look like a registry consume', async () => {
|
|
89
|
+
// Without a declared read token, the .npmrc credential is a publish
|
|
90
|
+
// credential and must never be promoted into the read role by inference.
|
|
89
91
|
await withRepo(
|
|
90
92
|
{
|
|
91
93
|
privatePackage: `${SCOPE}/sdk`,
|
|
92
94
|
rootDeps: { [`${SCOPE}/sdk`]: 'workspace:*' },
|
|
93
95
|
extraDeps: { [`${SCOPE}/other`]: 'link:@priv.test/other' },
|
|
94
96
|
npmrcAuthEnv: PUBLISH_ENV,
|
|
97
|
+
declared: { scope: SCOPE, publishTokenEnv: PUBLISH_ENV },
|
|
98
|
+
},
|
|
99
|
+
(root) => {
|
|
100
|
+
expect(resolvePrivateNpmWorkflowConfig(root)).toEqual({
|
|
101
|
+
scope: SCOPE,
|
|
102
|
+
publishTokenEnv: PUBLISH_ENV,
|
|
103
|
+
});
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
it('renders a declared read token for a producer, whose later releases read the previous tag state', async () => {
|
|
109
|
+
await withRepo(
|
|
110
|
+
{
|
|
111
|
+
privatePackage: `${SCOPE}/sdk`,
|
|
112
|
+
rootDeps: { [`${SCOPE}/sdk`]: 'workspace:*' },
|
|
113
|
+
npmrcAuthEnv: PUBLISH_ENV,
|
|
95
114
|
declared: { scope: SCOPE, readTokenEnv: READ_ENV, publishTokenEnv: PUBLISH_ENV },
|
|
96
115
|
},
|
|
97
116
|
(root) => {
|
|
98
117
|
expect(resolvePrivateNpmWorkflowConfig(root)).toEqual({
|
|
99
118
|
scope: SCOPE,
|
|
119
|
+
readTokenEnv: READ_ENV,
|
|
100
120
|
publishTokenEnv: PUBLISH_ENV,
|
|
101
121
|
});
|
|
102
122
|
},
|
package/src/release/index.ts
CHANGED
|
@@ -48,6 +48,7 @@ export {
|
|
|
48
48
|
type PrivateNpmRegistryConfigErrorKind,
|
|
49
49
|
type PublishDestination,
|
|
50
50
|
privateNpmPublishArgs,
|
|
51
|
+
privateNpmTokenEnvForMode,
|
|
51
52
|
privateNpmUserconfigContent,
|
|
52
53
|
publishPrivateWithDiagnostics,
|
|
53
54
|
type Result as PrivateNpmResult,
|
|
@@ -58,6 +59,7 @@ export {
|
|
|
58
59
|
withPrivateNpmUserconfig,
|
|
59
60
|
} from './private-npm.js';
|
|
60
61
|
|
|
62
|
+
import { PLATFORM_TARGET_GLOBS } from '@smoothbricks/nx-plugin/workspace-config-policy';
|
|
61
63
|
import { ciApiContext } from '../github-ci/api.js';
|
|
62
64
|
import { readProjectTargets } from '../nx/index.js';
|
|
63
65
|
import {
|
|
@@ -114,6 +116,7 @@ import {
|
|
|
114
116
|
npmPublishedVersionExists as npmPublishedVersionExistsOnRegistry,
|
|
115
117
|
type PrivateNpmRegistry,
|
|
116
118
|
privateNpmPublishArgs,
|
|
119
|
+
privateNpmTokenEnvForMode,
|
|
117
120
|
publishPrivateWithDiagnostics,
|
|
118
121
|
selectPublishDestination,
|
|
119
122
|
selectRegistryForPackage,
|
|
@@ -754,7 +757,13 @@ async function publishPrivatePackedPackage(
|
|
|
754
757
|
await publishPrivateWithDiagnostics(
|
|
755
758
|
pkg,
|
|
756
759
|
{
|
|
757
|
-
publish: () =>
|
|
760
|
+
publish: () =>
|
|
761
|
+
runNpm(root, args, {
|
|
762
|
+
NPM_CONFIG_USERCONFIG: userconfig,
|
|
763
|
+
// Env config outranks a committed project `.npmrc` auth line; see
|
|
764
|
+
// npmStatusEnv in private-npm.ts for why that matters here.
|
|
765
|
+
[`npm_config_${registry.authKey}`]: process.env[privateNpmTokenEnvForMode(registry, 'publish')] ?? '',
|
|
766
|
+
}),
|
|
758
767
|
// The open publish userconfig already authenticates this registry,
|
|
759
768
|
// so the failure probe reuses it. Opening a second read-mode
|
|
760
769
|
// userconfig here demanded a read credential the publishing job need
|
|
@@ -764,6 +773,7 @@ async function publishPrivatePackedPackage(
|
|
|
764
773
|
npmPublishedVersionExistsOnRegistry(root, pkg.name, pkg.version, {
|
|
765
774
|
registry: registry.registry,
|
|
766
775
|
userconfig,
|
|
776
|
+
credential: { authKey: registry.authKey, tokenEnv: privateNpmTokenEnvForMode(registry, 'publish') },
|
|
767
777
|
}),
|
|
768
778
|
log: (message) => console.log(message),
|
|
769
779
|
error: (message) => console.error(message),
|
|
@@ -1197,6 +1207,17 @@ function releaseRepairShell(root: string, platformOutputs: readonly string[]): R
|
|
|
1197
1207
|
const sourceSha = await gitHead(root);
|
|
1198
1208
|
const outputs = platformOutputs.map((base) => join(base, sourceSha));
|
|
1199
1209
|
if (outputs.length === 0) {
|
|
1210
|
+
// No producer job handed outputs over, so this runner is the producer:
|
|
1211
|
+
// build the platform legs at the checked-out ref. `build` alone gives a
|
|
1212
|
+
// package only the legs its own graph depends on — the host one and
|
|
1213
|
+
// whatever it declares — and the release gate demands the whole declared
|
|
1214
|
+
// native closure, so a foreign leg it never asked for was simply absent.
|
|
1215
|
+
console.log('Repair pending releases: building cross-platform outputs on this runner.');
|
|
1216
|
+
await githubCiNxRunMany(root, {
|
|
1217
|
+
targets: PLATFORM_TARGET_GLOBS.join(','),
|
|
1218
|
+
projects: releasePackageProjects(packages),
|
|
1219
|
+
allowEmptyProjects: true,
|
|
1220
|
+
});
|
|
1200
1221
|
return;
|
|
1201
1222
|
}
|
|
1202
1223
|
console.log(`Repair pending releases: applying cross-platform outputs from ${outputs.join(', ')}.`);
|
|
@@ -1865,7 +1886,11 @@ async function npmVersionExists(root: string, name: string, version: string): Pr
|
|
|
1865
1886
|
return npmPublishedVersionExistsOnRegistry(root, name, version);
|
|
1866
1887
|
}
|
|
1867
1888
|
return withPrivateNpmUserconfig(registry, 'read', (userconfig) =>
|
|
1868
|
-
npmPublishedVersionExistsOnRegistry(root, name, version, {
|
|
1889
|
+
npmPublishedVersionExistsOnRegistry(root, name, version, {
|
|
1890
|
+
registry: registry.registry,
|
|
1891
|
+
userconfig,
|
|
1892
|
+
credential: { authKey: registry.authKey, tokenEnv: privateNpmTokenEnvForMode(registry, 'read') },
|
|
1893
|
+
}),
|
|
1869
1894
|
);
|
|
1870
1895
|
}
|
|
1871
1896
|
|
|
@@ -247,7 +247,12 @@ export function resolvePrivateNpmWorkflowConfig(root: string): PackagePrivateNpm
|
|
|
247
247
|
return undefined;
|
|
248
248
|
}
|
|
249
249
|
const npmrcEnv = npmrcAuthTokenEnv(root, declared.scope);
|
|
250
|
-
|
|
250
|
+
// A publishing workspace reads the registry too: every release after the
|
|
251
|
+
// first checks the previous tag's durable state, and that check runs in the
|
|
252
|
+
// producer jobs that never see the publish credential. A declared read token
|
|
253
|
+
// therefore always renders; only the .npmrc fallback stays consumer-only, so
|
|
254
|
+
// a publish token is never promoted into a read role by inference.
|
|
255
|
+
const readTokenEnv = declared.readTokenEnv ?? (consumes ? npmrcEnv : undefined);
|
|
251
256
|
const publishTokenEnv = publishes ? (declared.publishTokenEnv ?? npmrcEnv) : undefined;
|
|
252
257
|
if (!readTokenEnv && !publishTokenEnv) {
|
|
253
258
|
return undefined;
|
|
@@ -412,10 +417,30 @@ export async function withPrivateNpmUserconfig<T>(
|
|
|
412
417
|
export interface NpmStatusOptions {
|
|
413
418
|
registry?: string;
|
|
414
419
|
userconfig?: string;
|
|
420
|
+
/** Path-scoped auth key and token env, so the credential can ride env config. */
|
|
421
|
+
credential?: { authKey: string; tokenEnv: string };
|
|
415
422
|
}
|
|
416
423
|
|
|
417
|
-
|
|
418
|
-
|
|
424
|
+
/**
|
|
425
|
+
* npm config precedence is cli > env > project `.npmrc` > userconfig. A
|
|
426
|
+
* repository that commits its own `//host/path:_authToken=${SOME_ENV}` line
|
|
427
|
+
* therefore OVERRIDES the userconfig this module writes, and when that env is
|
|
428
|
+
* unset npm sends the unexpanded value: the registry answers 401 on reads with
|
|
429
|
+
* a perfectly good read credential, naming nothing. So the credential also
|
|
430
|
+
* rides as env config, which the project file cannot outrank, and the token
|
|
431
|
+
* value stays out of every file on disk.
|
|
432
|
+
*/
|
|
433
|
+
function npmStatusEnv(
|
|
434
|
+
userconfig: string | undefined,
|
|
435
|
+
credential?: { authKey: string; tokenEnv: string },
|
|
436
|
+
): Record<string, string> | undefined {
|
|
437
|
+
if (!userconfig) return undefined;
|
|
438
|
+
const env: Record<string, string> = { NPM_CONFIG_USERCONFIG: userconfig };
|
|
439
|
+
const token = credential ? process.env[credential.tokenEnv] : undefined;
|
|
440
|
+
if (credential && token) {
|
|
441
|
+
env[`npm_config_${credential.authKey}`] = token;
|
|
442
|
+
}
|
|
443
|
+
return env;
|
|
419
444
|
}
|
|
420
445
|
|
|
421
446
|
function npmCommandFailedMessage(npmArgs: string[], exitCode: number, stdout = '', stderr = ''): string {
|
|
@@ -439,7 +464,7 @@ export async function npmPublishedVersionExists(
|
|
|
439
464
|
if (options.registry) {
|
|
440
465
|
args.push('--registry', options.registry);
|
|
441
466
|
}
|
|
442
|
-
const result = await runResult('npm', args, root, npmStatusEnv(options.userconfig));
|
|
467
|
+
const result = await runResult('npm', args, root, npmStatusEnv(options.userconfig, options.credential));
|
|
443
468
|
if (result.exitCode === 0) {
|
|
444
469
|
return true;
|
|
445
470
|
}
|
|
@@ -455,7 +480,7 @@ export async function npmPackageExists(root: string, name: string, options: NpmS
|
|
|
455
480
|
if (options.registry) {
|
|
456
481
|
args.push('--registry', options.registry);
|
|
457
482
|
}
|
|
458
|
-
const result = await runResult('npm', args, root, npmStatusEnv(options.userconfig));
|
|
483
|
+
const result = await runResult('npm', args, root, npmStatusEnv(options.userconfig, options.credential));
|
|
459
484
|
if (result.exitCode === 0) {
|
|
460
485
|
return true;
|
|
461
486
|
}
|