@smoothbricks/cli 0.11.13 → 0.11.14

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 CHANGED
@@ -84,9 +84,14 @@ function buildProgram() {
84
84
  .option('--fail-fast', 'stop after the first failing validation pack')
85
85
  .option('--only-if-new-workspace-package', 'skip validation unless a new workspace package manifest is staged')
86
86
  .option('--verbose', 'print validation progress and successful checks')
87
+ .option('--projects <names>', 'comma-separated Nx project names to build and pack-validate (a release selection); default: every project')
87
88
  .action(async (options) => {
88
89
  const { validateMonorepo } = await import('./monorepo/index.js');
89
- await validateMonorepo(await findRepoRoot(), options);
90
+ const projects = options.projects
91
+ ?.split(',')
92
+ .map((name) => name.trim())
93
+ .filter((name) => name.length > 0);
94
+ await validateMonorepo(await findRepoRoot(), { ...options, projects });
90
95
  });
91
96
  monorepo.command('update').action(async () => {
92
97
  const { updateManagedFiles } = await import('./monorepo/index.js');
@@ -9,6 +9,8 @@ export interface ValidateOptions {
9
9
  onlyIfNewWorkspacePackage?: boolean;
10
10
  fix?: boolean;
11
11
  verbose?: boolean;
12
+ /** Nx project names to build and pack-validate; see MonorepoContext.projects. */
13
+ projects?: readonly string[];
12
14
  }
13
15
  export interface ValidateCommitMessageOptions {
14
16
  fix?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/monorepo/index.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AA2BxD,MAAM,WAAW,WAAW;IAC1B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,yBAAyB,CAAC,EAAE,OAAO,CAAC;IACpC,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,4BAA4B;IAC3C,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,0BAA0B;IACzC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,uBAAuB;IACtC,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,qBAAqB;IACpC,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAQpF;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBjG;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBpE;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBrG;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAElE;AAED,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,OAAO,GAAE,4BAAiC,EAC1C,IAAI,SAAgB,GACnB,IAAI,CAoBN;AAED,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,0BAA+B,GAAG,MAAM,CAW3G;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAI5D;AAED,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,uBAAuB,EAChC,KAAK,GAAE,qBAAoD,GAC1D,OAAO,CAAC,IAAI,CAAC,CA8Bf;AAED,OAAO,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/monorepo/index.ts"],"names":[],"mappings":"AAWA,OAAO,EAAE,uBAAuB,EAAE,MAAM,iBAAiB,CAAC;AAC1D,OAAO,EAAE,uBAAuB,EAAE,MAAM,eAAe,CAAC;AA2BxD,MAAM,WAAW,WAAW;IAC1B,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB;AAED,MAAM,WAAW,eAAe;IAC9B,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,yBAAyB,CAAC,EAAE,OAAO,CAAC;IACpC,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,iFAAiF;IACjF,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,4BAA4B;IAC3C,GAAG,CAAC,EAAE,OAAO,CAAC;CACf;AAED,MAAM,WAAW,0BAA0B;IACzC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AAED,MAAM,WAAW,uBAAuB;IACtC,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB;AAED,MAAM,WAAW,qBAAqB;IACpC,GAAG,CAAC,OAAO,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,wBAAsB,YAAY,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAQpF;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,eAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqBjG;AAED,wBAAsB,kBAAkB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAkBpE;AAED,wBAAsB,iBAAiB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE;IAAE,IAAI,CAAC,EAAE,OAAO,CAAA;CAAO,GAAG,OAAO,CAAC,IAAI,CAAC,CAuBrG;AAED,wBAAsB,gBAAgB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAElE;AAED,wBAAgB,yBAAyB,CACvC,IAAI,EAAE,MAAM,GAAG,SAAS,EACxB,OAAO,GAAE,4BAAiC,EAC1C,IAAI,SAAgB,GACnB,IAAI,CAoBN;AAED,wBAAgB,4BAA4B,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,GAAE,0BAA+B,GAAG,MAAM,CAW3G;AAED,wBAAgB,yBAAyB,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAI5D;AAED,wBAAsB,gBAAgB,CACpC,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE,uBAAuB,EAChC,KAAK,GAAE,qBAAoD,GAC1D,OAAO,CAAC,IAAI,CAAC,CA8Bf;AAED,OAAO,EAAE,uBAAuB,EAAE,uBAAuB,EAAE,CAAC"}
@@ -24,7 +24,7 @@ export async function validateMonorepo(root, options = {}) {
24
24
  if (options.onlyIfNewWorkspacePackage && !(await hasNewWorkspacePackage(root))) {
25
25
  return;
26
26
  }
27
- const result = await runValidatePacks({ root, syncRuntime: false, verbose: options.verbose === true }, options);
27
+ const result = await runValidatePacks({ root, syncRuntime: false, verbose: options.verbose === true, projects: options.projects }, options);
28
28
  if (result.failures > 0) {
29
29
  const checkNoun = result.failedChecks === 1 ? 'check' : 'checks';
30
30
  const problemNoun = result.failures === 1 ? 'problem' : 'problems';
@@ -1,5 +1,5 @@
1
1
  export declare function validatePackedPublishablePackages(root: string): Promise<number>;
2
- export declare function validatePackedPublishablePackagePublint(root: string): Promise<number>;
3
- export declare function validatePackedPublishablePackageManifest(root: string): Promise<number>;
4
- export declare function validatePackedPublishablePackageTypes(root: string): Promise<number>;
2
+ export declare function validatePackedPublishablePackagePublint(root: string, projects?: readonly string[]): Promise<number>;
3
+ export declare function validatePackedPublishablePackageManifest(root: string, projects?: readonly string[]): Promise<number>;
4
+ export declare function validatePackedPublishablePackageTypes(root: string, projects?: readonly string[]): Promise<number>;
5
5
  //# sourceMappingURL=packed-package.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"packed-package.d.ts","sourceRoot":"","sources":["../../src/monorepo/packed-package.ts"],"names":[],"mappings":"AAeA,wBAAsB,iCAAiC,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAMrF;AAED,wBAAsB,uCAAuC,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAM3F;AAED,wBAAsB,wCAAwC,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAM5F;AAED,wBAAsB,qCAAqC,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAMzF"}
1
+ {"version":3,"file":"packed-package.d.ts","sourceRoot":"","sources":["../../src/monorepo/packed-package.ts"],"names":[],"mappings":"AAeA,wBAAsB,iCAAiC,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAMrF;AAQD,wBAAsB,uCAAuC,CAC3D,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,GAC3B,OAAO,CAAC,MAAM,CAAC,CAMjB;AAED,wBAAsB,wCAAwC,CAC5D,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,GAC3B,OAAO,CAAC,MAAM,CAAC,CAMjB;AAED,wBAAsB,qCAAqC,CACzD,IAAI,EAAE,MAAM,EACZ,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,GAC3B,OAAO,CAAC,MAAM,CAAC,CAMjB"}
@@ -15,23 +15,28 @@ export async function validatePackedPublishablePackages(root) {
15
15
  (await validatePackedPublishablePackageManifest(root)) +
16
16
  (await validatePackedPublishablePackageTypes(root)));
17
17
  }
18
- export async function validatePackedPublishablePackagePublint(root) {
18
+ /** The publishable packages, narrowed to `projects` (Nx names) when given. */
19
+ function selectedPublishablePackages(root, projects) {
20
+ const all = listPublishablePackages(root);
21
+ return projects?.length ? all.filter((pkg) => projects.includes(pkg.projectName)) : all;
22
+ }
23
+ export async function validatePackedPublishablePackagePublint(root, projects) {
19
24
  let failures = 0;
20
- for (const pkg of listPublishablePackages(root)) {
25
+ for (const pkg of selectedPublishablePackages(root, projects)) {
21
26
  failures += await validatePackedPublishablePackageTool(root, pkg, validatePublint);
22
27
  }
23
28
  return failures;
24
29
  }
25
- export async function validatePackedPublishablePackageManifest(root) {
30
+ export async function validatePackedPublishablePackageManifest(root, projects) {
26
31
  let failures = 0;
27
- for (const pkg of listPublishablePackages(root)) {
32
+ for (const pkg of selectedPublishablePackages(root, projects)) {
28
33
  failures += await validatePackedPublishablePackageTool(root, pkg, validatePackedManifest);
29
34
  }
30
35
  return failures;
31
36
  }
32
- export async function validatePackedPublishablePackageTypes(root) {
37
+ export async function validatePackedPublishablePackageTypes(root, projects) {
33
38
  let failures = 0;
34
- for (const pkg of listPublishablePackages(root)) {
39
+ for (const pkg of selectedPublishablePackages(root, projects)) {
35
40
  failures += await validatePackedPublishablePackageTool(root, pkg, validateAttw);
36
41
  }
37
42
  return failures;
@@ -4,6 +4,14 @@ export interface MonorepoContext {
4
4
  root: string;
5
5
  syncRuntime: boolean;
6
6
  verbose?: boolean;
7
+ /**
8
+ * Nx project names the validation is about, when a caller knows: the
9
+ * release's candidates. The build phase and the packed-package packs scope
10
+ * to them; manifest-level packs still see the whole repository, because a
11
+ * package's hygiene does not depend on which packages ship today. Absent,
12
+ * everything is validated and everything is built.
13
+ */
14
+ projects?: readonly string[];
7
15
  }
8
16
  export interface ValidatePackOptions {
9
17
  failFast?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/monorepo/packs/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,cAAc,EAAsB,MAAM,mBAAmB,CAAC;AAO5E,OAAO,EAKL,KAAK,sBAAsB,EAS5B,MAAM,sBAAsB,CAAC;AAU9B,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAClD,WAAW,CAAC,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACzD,YAAY,CAAC,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1D,gBAAgB,CAAC,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;IAClE,iBAAiB,CAAC,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;CACpE;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,SAAS,YAAY,EAAE,CAAC;IAChC,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,eAAe,EAAE,OAAO,CAAC,EAAE,mBAAmB,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;CAC9F;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;CACtB;AA8ID,0DAA0D;AAC1D,eAAO,MAAM,YAAY,gBAAQ,CAAC;AAElC,wBAAsB,YAAY,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAItE;AAED,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,eAAe,EACpB,OAAO,GAAE,mBAAwB,EACjC,KAAK,GAAE,oBAAyB,GAC/B,OAAO,CAAC,kBAAkB,CAAC,CA0B7B;AAuMD,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,cAAc,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAexG"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/monorepo/packs/index.ts"],"names":[],"mappings":"AAGA,OAAO,EAAE,KAAK,cAAc,EAAsB,MAAM,mBAAmB,CAAC;AAO5E,OAAO,EAKL,KAAK,sBAAsB,EAS5B,MAAM,sBAAsB,CAAC;AAU9B,MAAM,WAAW,eAAe;IAC9B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,OAAO,CAAC;IACrB,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB;;;;;;OAMG;IACH,QAAQ,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CAC9B;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,GAAG,CAAC,EAAE,OAAO,CAAC;IACd,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB;AAED,MAAM,WAAW,YAAY;IAC3B,IAAI,EAAE,MAAM,CAAC;IACb,IAAI,CAAC,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAClD,WAAW,CAAC,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IACzD,YAAY,CAAC,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC;IAC1D,gBAAgB,CAAC,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;IAClE,iBAAiB,CAAC,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;CACpE;AAED,MAAM,WAAW,oBAAoB;IACnC,KAAK,CAAC,EAAE,SAAS,YAAY,EAAE,CAAC;IAChC,QAAQ,CAAC,EAAE,CAAC,GAAG,EAAE,eAAe,EAAE,OAAO,CAAC,EAAE,mBAAmB,KAAK,OAAO,CAAC,MAAM,CAAC,GAAG,MAAM,CAAC;CAC9F;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,YAAY,EAAE,MAAM,CAAC;CACtB;AA8ID,0DAA0D;AAC1D,eAAO,MAAM,YAAY,gBAAQ,CAAC;AAElC,wBAAsB,YAAY,CAAC,GAAG,EAAE,eAAe,GAAG,OAAO,CAAC,IAAI,CAAC,CAItE;AAED,wBAAsB,gBAAgB,CACpC,GAAG,EAAE,eAAe,EACpB,OAAO,GAAE,mBAAwB,EACjC,KAAK,GAAE,oBAAyB,GAC/B,OAAO,CAAC,kBAAkB,CAAC,CA0B7B;AAwMD,wBAAgB,wBAAwB,CAAC,QAAQ,EAAE,cAAc,EAAE,GAAG,GAAG,CAAC,MAAM,EAAE,sBAAsB,CAAC,CAexG"}
@@ -118,19 +118,19 @@ const packs = [
118
118
  {
119
119
  name: 'packed-package-publint',
120
120
  validatePostBuild(ctx) {
121
- return validatePackedPublishablePackagePublint(ctx.root);
121
+ return validatePackedPublishablePackagePublint(ctx.root, ctx.projects);
122
122
  },
123
123
  },
124
124
  {
125
125
  name: 'packed-package-manifest',
126
126
  validatePostBuild(ctx) {
127
- return validatePackedPublishablePackageManifest(ctx.root);
127
+ return validatePackedPublishablePackageManifest(ctx.root, ctx.projects);
128
128
  },
129
129
  },
130
130
  {
131
131
  name: 'packed-package-types',
132
132
  validatePostBuild(ctx) {
133
- return validatePackedPublishablePackageTypes(ctx.root);
133
+ return validatePackedPublishablePackageTypes(ctx.root, ctx.projects);
134
134
  },
135
135
  },
136
136
  {
@@ -272,9 +272,10 @@ async function runBuild(ctx, options = {}) {
272
272
  if (options.verbose) {
273
273
  printCheckHeading('build', true);
274
274
  }
275
+ const args = ['run-many', '-t', 'build', ...(ctx.projects?.length ? ['-p', ctx.projects.join(',')] : [])];
275
276
  const result = options.verbose
276
- ? { exitCode: await runStatus('nx', ['run-many', '-t', 'build'], ctx.root, false), stdout: '', stderr: '' }
277
- : await runResult('nx', ['run-many', '-t', 'build'], ctx.root);
277
+ ? { exitCode: await runStatus('nx', args, ctx.root, false), stdout: '', stderr: '' }
278
+ : await runResult('nx', args, ctx.root);
278
279
  const status = result.exitCode;
279
280
  if (status !== 0) {
280
281
  if (!options.verbose) {
@@ -441,7 +441,11 @@ function yamlLinesForStep(step, options) {
441
441
  'include-hidden-files: true',
442
442
  ], 'failure()');
443
443
  case PublishWorkflowStepKind.ValidateMonorepoConfig:
444
- return conditionalRunStep(step, 'smoo monorepo validate');
444
+ // Scoped to the release's candidates: the build phase and the packed-package
445
+ // checks cover what ships. Unscoped, validate compiled every project - on a
446
+ // release touching no Rust that was 3m30 of the cowshed CLI on the critical
447
+ // path, for a binary the release did not contain.
448
+ return conditionalRunStep(step, `smoo monorepo validate --projects "${githubExpression('steps.version.outputs.projects')}"`);
445
449
  case PublishWorkflowStepKind.TagRelease:
446
450
  return tagReleaseStepLines(step.name);
447
451
  case PublishWorkflowStepKind.PublishRelease:
@@ -274,16 +274,16 @@
274
274
  # 5. The shared setup-environment.ts bootstraps repository dependencies; a
275
275
  # failure aborts shell entry instead of yielding a half-working shell.
276
276
  # Repo-owned enterShell bodies merge after this prologue.
277
- # 6. The declared Nx remote cache (`smoo.remoteCache`), if the repository
278
- # has one. secret-references.ts prints the two variables Nx reads the
279
- # server and the access token and prints nothing at all unless the
280
- # declared token has a value, because Nx accepts only 200 or 404 from a
281
- # cache server: an unauthenticated one fails every task on 401 instead
282
- # of missing quietly. It is eval-ed into THIS shell because that is
283
- # where Nx runs; the export covers those two variables only, so the rule
284
- # that keeps `smoo.secrets` inside the setup child still holds. An
285
- # inherited server wins, so a CI job env is never overwritten, and a
286
- # missing token costs a stderr line rather than shell entry.
277
+ # 6. The declared Nx remote cache (`smoo.remoteCache`) is NOT resolved
278
+ # here. Shell entry happens on every direnv reload and every
279
+ # `devenv shell -- <command>`, and a provider command that runs then is a
280
+ # credential prompt on every one of them (1Password authorises per
281
+ # requesting process lineage, and this one is new each time). Nx reads
282
+ # NX_SELF_HOSTED_REMOTE_CACHE_SERVER and _ACCESS_TOKEN from the
283
+ # environment when it runs: CI injects them into the job, a developer
284
+ # exports the token once in the terminal that wants the cache (for
285
+ # example `op signin`, then the declared command), and every nested
286
+ # shell inherits it. Absent, Nx runs with the local cache only.
287
287
  # 7. GOROOT is unset rather than set. With devenv's Go pinned to the patch
288
288
  # release ttsc vendors, a GOROOT crossing cannot misfire on version at all,
289
289
  # so this is belt-and-braces rather than the fix — it keeps the isolation
@@ -322,7 +322,12 @@
322
322
  # swaps the SDK a build was compiled against.
323
323
  # Nx's fallback includes HOME or TMPDIR, either of which can exceed the
324
324
  # Unix socket limit in a checkout. Reuse devenv's short runtime directory,
325
- # while preserving an explicit directory supplied by Cowshed or the caller.
325
+ # which is keyed on this checkout's devenv root and so is one per
326
+ # workspace. Unconditionally: an inherited value is another workspace's
327
+ # shell (a shed entered from the host, a second repository from the first,
328
+ # a subprocess of either), and one socket dir for two workspaces makes the
329
+ # daemon refuse whichever came second ("received a message from a
330
+ # different workspace"). Nobody supplies this deliberately.
326
331
  (lib.mkBefore ''
327
332
  cd "$DEVENV_ROOT/../.."
328
333
  export PATH="$("$PWD/tooling/direnv/repo-path")"
@@ -349,8 +354,7 @@
349
354
  export GOFLAGS="''${GOFLAGS:--trimpath}"
350
355
  unset GOROOT
351
356
  bun "$DEVENV_ROOT/setup-environment.ts" || exit $?
352
- eval "$(bun "$DEVENV_ROOT/secret-references.ts" "$PWD")"
353
- export NX_SOCKET_DIR="''${NX_SOCKET_DIR:-$DEVENV_RUNTIME/nx}"
357
+ export NX_SOCKET_DIR="$DEVENV_RUNTIME/nx"
354
358
  mkdir -p "$NX_SOCKET_DIR"
355
359
  ${lib.optionalString pkgs.stdenv.isDarwin ''
356
360
  unset CC CXX
@@ -1,16 +1,14 @@
1
1
  /**
2
- * Provider-neutral local secret resolution for smoo-managed repositories, and
3
- * the developer-shell half of the declared Nx remote cache.
2
+ * Provider-neutral local secret resolution for smoo-managed repositories.
4
3
  *
5
4
  * Reads the root package.json `smoo.secrets` map and `smoo.remoteCache` block
6
5
  * — the bootstrap twins of `PackageSecretCommand`, `PackageSmooConfig.secrets`
7
6
  * and `PackageRemoteCacheConfig` in packages/cli/src/lib/json.ts. This file is
8
7
  * a managed raw script: it runs from `tooling/direnv/setup-environment.ts`
9
- * BEFORE `bun install`, and from the managed devenv `enterShell` before that,
10
- * when no workspace package and no Typia transform exist yet, so both shapes
11
- * are hand-validated here against exactly what json.ts declares. Keep them
12
- * aligned; smoo's Typia validation fails the manifest at generation time for
13
- * anything this file would reject at runtime.
8
+ * BEFORE `bun install`, when no workspace package and no Typia transform exist
9
+ * yet, so both shapes are hand-validated here against exactly what json.ts
10
+ * declares. Keep them aligned; smoo's Typia validation fails the manifest at
11
+ * generation time for anything this file would reject at runtime.
14
12
  *
15
13
  * Routing per declared variable, in first-match order:
16
14
  *
@@ -29,22 +27,14 @@
29
27
  * Error text names variables and exit codes only: secret values, provider
30
28
  * stdout/stderr, and command arguments are never echoed.
31
29
  *
32
- * One declared variable is exempt from blocking an install: the one
33
- * `smoo.remoteCache.tokenSecret` names. A remote cache is an optimization, so
34
- * an unreachable secret provider must not keep dependencies from installing or
35
- * a shell from opening; that variable is resolved by the cache export below,
36
- * where failure costs a stderr line.
37
- *
38
- * Run as a program (`bun secret-references.ts [root]`), the script prints the
39
- * remote cache's `export` lines for the shell to `eval` and nothing else: Nx
40
- * runs in the developer's shell, not in the setup child, and the export is
41
- * limited to those two variables so `smoo.secrets` stays process-local. It
42
- * prints nothing when the repository declares no cache, when the shell already
43
- * carries a server (a CI job env is never overwritten), or when the declared
44
- * token has no value — Nx accepts only 200 or 404 from a cache server, so a
45
- * server it cannot authenticate to fails every task instead of missing. Why
46
- * the cache is off is said on stderr; the exit status stays 0, because a
47
- * missing cache credential must never keep a shell from opening.
30
+ * The variable `smoo.remoteCache.tokenSecret` names is never resolved here,
31
+ * nor anywhere at shell entry. A remote cache is an optimization, and shell
32
+ * entry happens on every direnv reload and every `devenv shell -- <command>`:
33
+ * a provider command run there is a credential prompt on each of them. Nx
34
+ * reads NX_SELF_HOSTED_REMOTE_CACHE_SERVER and _ACCESS_TOKEN from the
35
+ * environment it runs in — CI injects them, a developer exports the token
36
+ * once in the terminal that wants the cache and every nested shell inherits
37
+ * them. Absent, Nx keeps to its local cache.
48
38
  */
49
39
  import { existsSync, readFileSync } from 'node:fs';
50
40
  import { join } from 'node:path';
@@ -135,10 +125,6 @@ function parseSecretSpec(name: string, spec: unknown): SecretSpec {
135
125
  return { command: [first, ...rest] };
136
126
  }
137
127
 
138
- /** The two variables Nx reads for its self-hosted HTTP cache. */
139
- const REMOTE_CACHE_SERVER = 'NX_SELF_HOSTED_REMOTE_CACHE_SERVER';
140
- const REMOTE_CACHE_ACCESS_TOKEN = 'NX_SELF_HOSTED_REMOTE_CACHE_ACCESS_TOKEN';
141
-
142
128
  /**
143
129
  * The declared remote cache as a local shell needs it. `internalServer` is
144
130
  * deliberately absent: it is the address a managed runner reaches inside its
@@ -353,85 +339,6 @@ export async function resolveSecretEnvironment(
353
339
  });
354
340
  }
355
341
 
356
- /**
357
- * What the shell should do about the declared remote cache. Each case is a
358
- * value, not an exception: three of the four are ordinary, and only one is
359
- * worth saying out loud.
360
- */
361
- export type RemoteCacheOutcome =
362
- | { readonly kind: 'undeclared' }
363
- | { readonly kind: 'inherited' }
364
- | { readonly kind: 'unavailable'; readonly reason: string }
365
- | { readonly kind: 'exported'; readonly env: Readonly<Record<string, string>> };
366
-
367
- /**
368
- * The remote cache variables this shell should export, or why it exports none.
369
- * An inherited server always wins, so a CI job env — which carries the address
370
- * its own runners reach — is never overwritten by the public one. The token is
371
- * taken from the environment when it is there, and otherwise from the one
372
- * `smoo.secrets` entry that declares it — resolving that single variable, so
373
- * no other declared secret ever reaches the shell. A repository that declares
374
- * the cache token as a provider secret therefore pays that one command twice
375
- * per shell entry, once here and once in the setup child's own aggregate
376
- * resolution; an ambient token pays nothing.
377
- */
378
- export async function resolveRemoteCacheOutcome(options: SecretResolutionOptions): Promise<RemoteCacheOutcome> {
379
- const env = options.env ?? process.env;
380
- const packageJson = readPackageJson(options.root);
381
- const spec = parseSmooRemoteCache(packageJson);
382
- if (spec === null) {
383
- return { kind: 'undeclared' };
384
- }
385
- if (isNonemptyEnvValue(env[REMOTE_CACHE_SERVER])) {
386
- return { kind: 'inherited' };
387
- }
388
- const ambient = env[spec.tokenSecret];
389
- if (isNonemptyEnvValue(ambient)) {
390
- return {
391
- kind: 'exported',
392
- env: { [REMOTE_CACHE_SERVER]: spec.server, [REMOTE_CACHE_ACCESS_TOKEN]: ambient },
393
- };
394
- }
395
- const declared = parseSmooSecrets(packageJson)[spec.tokenSecret];
396
- if (declared === undefined) {
397
- return {
398
- kind: 'unavailable',
399
- reason: `${spec.tokenSecret} is unset and no smoo.secrets entry declares it, so ${spec.server} would be asked for cache entries with no credential`,
400
- };
401
- }
402
- // A cache token is not registry routing, so the cowshed `.npmrc` exclusion
403
- // cannot apply to it; every other rule in the header does.
404
- const outcome = await routeSecret(spec.tokenSecret, declared, {
405
- env,
406
- registryIntentEnvs: new Set(),
407
- run: options.runCommand ?? runSecretCommand,
408
- });
409
- switch (outcome.kind) {
410
- case 'resolved':
411
- return {
412
- kind: 'exported',
413
- env: { [REMOTE_CACHE_SERVER]: spec.server, [REMOTE_CACHE_ACCESS_TOKEN]: outcome.value },
414
- };
415
- case 'failed':
416
- return { kind: 'unavailable', reason: `${spec.tokenSecret}: ${outcome.guidance}` };
417
- case 'env-wins':
418
- // Only reachable if an ambient value appeared between the check above
419
- // and this call; the export happens on the next shell entry.
420
- return { kind: 'unavailable', reason: `${spec.tokenSecret} was set after it was read` };
421
- }
422
- }
423
-
424
- /**
425
- * POSIX `export` lines for a shell to `eval`. Single quotes are the only
426
- * quoting a value cannot escape from, so each value is single-quoted with its
427
- * own quotes spliced out — a token is opaque bytes, never assumed shell-safe.
428
- */
429
- export function shellExportLines(env: Readonly<Record<string, string>>): string {
430
- return Object.entries(env)
431
- .map(([name, value]) => `export ${name}='${value.replaceAll("'", "'\\''")}'\n`)
432
- .join('');
433
- }
434
-
435
342
  function readPackageJson(root: string): unknown {
436
343
  const packageJsonPath = join(root, 'package.json');
437
344
  let text: string;
@@ -452,27 +359,8 @@ function readPackageJson(root: string): unknown {
452
359
  }
453
360
  return parsed;
454
361
  }
455
-
456
362
  function readNpmrcText(root: string): string | null {
457
363
  const npmrcPath = join(root, '.npmrc');
458
364
  if (!existsSync(npmrcPath)) return null;
459
365
  return readFileSync(npmrcPath, 'utf8');
460
366
  }
461
-
462
- // Program mode, run by the managed devenv shell as
463
- // `eval "$(bun "$DEVENV_ROOT/secret-references.ts" "$PWD")"`. stdout is
464
- // therefore shell text and carries nothing else; everything a human should
465
- // read goes to stderr, and the status stays 0 so a shell always opens.
466
- if (import.meta.main) {
467
- const root = Bun.argv[2] ?? process.cwd();
468
- try {
469
- const outcome = await resolveRemoteCacheOutcome({ root });
470
- if (outcome.kind === 'exported') {
471
- process.stdout.write(shellExportLines(outcome.env));
472
- } else if (outcome.kind === 'unavailable') {
473
- console.error(`smoo: Nx remote cache off — ${outcome.reason}`);
474
- }
475
- } catch (error) {
476
- console.error(`smoo: Nx remote cache off — ${error instanceof Error ? error.message : String(error)}`);
477
- }
478
- }
@@ -109,6 +109,11 @@ runs:
109
109
  # stable path (a symlink would not do: Nix canonicalises) and devenv is
110
110
  # evaluated from there; every later step still runs in the checkout, which
111
111
  # is the same tree. DEVENV_WORKDIR names the path the devenv steps use.
112
+ # The mount target is the instance's own /work, not the shared bind: only
113
+ # the path's spelling must be stable, and a bind onto the idmapped cache
114
+ # dataset failed inside the job (move_mount: ENOENT) where a container-fs
115
+ # target does not. If the mount is refused the job goes on from the
116
+ # checkout and pays the evaluation: this is an optimisation, never a gate.
112
117
  - name: 📂 Locate devenv state on the shared bind
113
118
  if: steps.runner-kind.outputs.host == 'true'
114
119
  shell: bash
@@ -119,13 +124,19 @@ runs:
119
124
  run: |
120
125
  lane=$(printf '%s' "$DEVENV_STATE_LANE" | tr '[:upper:]' '[:lower:]' | tr -cs 'a-z0-9._-' '-')
121
126
  root="/var/cache/ci/devenv/$DEVENV_STATE_REPO/${lane:-default}/$DEVENV_STATE_JOB"
122
- mkdir -p "$root/.devenv" "$root/.direnv" "$root/workspace"
123
- sudo mount --bind "$GITHUB_WORKSPACE" "$root/workspace"
127
+ mkdir -p "$root/.devenv" "$root/.direnv"
124
128
  rm -rf tooling/direnv/.devenv tooling/direnv/.direnv
125
129
  ln -s "$root/.devenv" tooling/direnv/.devenv
126
130
  ln -s "$root/.direnv" tooling/direnv/.direnv
127
- echo "DEVENV_WORKDIR=$root/workspace" >> "$GITHUB_ENV"
128
- echo "devenv state: $root (workspace bind-mounted at $root/workspace)"
131
+ stable="/work/$DEVENV_STATE_REPO/${lane:-default}/$DEVENV_STATE_JOB"
132
+ if mkdir -p "$stable" && sudo mount --bind "$GITHUB_WORKSPACE" "$stable"; then
133
+ echo "DEVENV_WORKDIR=$stable" >> "$GITHUB_ENV"
134
+ echo "devenv state: $root (workspace bind-mounted at $stable)"
135
+ else
136
+ echo "DEVENV_WORKDIR=$GITHUB_WORKSPACE" >> "$GITHUB_ENV"
137
+ echo "::warning::stable devenv path unavailable (bind mount refused); evaluating from the checkout"
138
+ echo "devenv state: $root (workspace at $GITHUB_WORKSPACE)"
139
+ fi
129
140
 
130
141
  - name: 📂 Devenv runs in the checkout
131
142
  if: steps.runner-kind.outputs.host != 'true'
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smoothbricks/cli",
3
- "version": "0.11.13",
3
+ "version": "0.11.14",
4
4
  "type": "module",
5
5
  "description": "SmoothBricks monorepo automation CLI",
6
6
  "bin": {
@@ -64,7 +64,7 @@
64
64
  ],
65
65
  "dependencies": {
66
66
  "@arethetypeswrong/core": "^0.18.2",
67
- "@smoothbricks/nx-plugin": "0.4.7",
67
+ "@smoothbricks/nx-plugin": "0.4.8",
68
68
  "@smoothbricks/validation": "0.1.8",
69
69
  "commander": "^14.0.3",
70
70
  "make-synchronized": "^0.8.0",
package/src/cli.ts CHANGED
@@ -89,15 +89,24 @@ function buildProgram(): Command {
89
89
  .option('--fail-fast', 'stop after the first failing validation pack')
90
90
  .option('--only-if-new-workspace-package', 'skip validation unless a new workspace package manifest is staged')
91
91
  .option('--verbose', 'print validation progress and successful checks')
92
+ .option(
93
+ '--projects <names>',
94
+ 'comma-separated Nx project names to build and pack-validate (a release selection); default: every project',
95
+ )
92
96
  .action(
93
97
  async (options: {
94
98
  fix?: boolean;
95
99
  failFast?: boolean;
96
100
  onlyIfNewWorkspacePackage?: boolean;
97
101
  verbose?: boolean;
102
+ projects?: string;
98
103
  }) => {
99
104
  const { validateMonorepo } = await import('./monorepo/index.js');
100
- await validateMonorepo(await findRepoRoot(), options);
105
+ const projects = options.projects
106
+ ?.split(',')
107
+ .map((name) => name.trim())
108
+ .filter((name) => name.length > 0);
109
+ await validateMonorepo(await findRepoRoot(), { ...options, projects });
101
110
  },
102
111
  );
103
112
  monorepo.command('update').action(async () => {
@@ -38,6 +38,9 @@ const codebaseWorkflowOptions: PublishWorkflowDefinitionOptions = {
38
38
  describe('publish workflow definition', () => {
39
39
  it('renders the checked-in local publish workflow copy', async () => {
40
40
  const rendered = renderPublishWorkflowYaml(codebaseWorkflowOptions);
41
+ // validate builds and pack-checks the release's candidates, not every project:
42
+ // unscoped it compiled binaries the release did not contain.
43
+ expect(rendered).toContain('smoo monorepo validate --projects "${{ steps.version.outputs.projects }}"');
41
44
  const packageRoot = join(import.meta.dir, '..', '..', '..');
42
45
  await expect(readFile(join(packageRoot, '..', '..', '.github/workflows/publish.yml'), 'utf8')).resolves.toBe(
43
46
  rendered,
@@ -47,6 +47,8 @@ export interface ValidateOptions {
47
47
  onlyIfNewWorkspacePackage?: boolean;
48
48
  fix?: boolean;
49
49
  verbose?: boolean;
50
+ /** Nx project names to build and pack-validate; see MonorepoContext.projects. */
51
+ projects?: readonly string[];
50
52
  }
51
53
 
52
54
  export interface ValidateCommitMessageOptions {
@@ -85,7 +87,10 @@ export async function validateMonorepo(root: string, options: ValidateOptions =
85
87
  if (options.onlyIfNewWorkspacePackage && !(await hasNewWorkspacePackage(root))) {
86
88
  return;
87
89
  }
88
- const result = await runValidatePacks({ root, syncRuntime: false, verbose: options.verbose === true }, options);
90
+ const result = await runValidatePacks(
91
+ { root, syncRuntime: false, verbose: options.verbose === true, projects: options.projects },
92
+ options,
93
+ );
89
94
  if (result.failures > 0) {
90
95
  const checkNoun = result.failedChecks === 1 ? 'check' : 'checks';
91
96
  const problemNoun = result.failures === 1 ? 'problem' : 'problems';
@@ -21,25 +21,40 @@ export async function validatePackedPublishablePackages(root: string): Promise<n
21
21
  );
22
22
  }
23
23
 
24
- export async function validatePackedPublishablePackagePublint(root: string): Promise<number> {
24
+ /** The publishable packages, narrowed to `projects` (Nx names) when given. */
25
+ function selectedPublishablePackages(root: string, projects?: readonly string[]): PackageInfo[] {
26
+ const all = listPublishablePackages(root);
27
+ return projects?.length ? all.filter((pkg) => projects.includes(pkg.projectName)) : all;
28
+ }
29
+
30
+ export async function validatePackedPublishablePackagePublint(
31
+ root: string,
32
+ projects?: readonly string[],
33
+ ): Promise<number> {
25
34
  let failures = 0;
26
- for (const pkg of listPublishablePackages(root)) {
35
+ for (const pkg of selectedPublishablePackages(root, projects)) {
27
36
  failures += await validatePackedPublishablePackageTool(root, pkg, validatePublint);
28
37
  }
29
38
  return failures;
30
39
  }
31
40
 
32
- export async function validatePackedPublishablePackageManifest(root: string): Promise<number> {
41
+ export async function validatePackedPublishablePackageManifest(
42
+ root: string,
43
+ projects?: readonly string[],
44
+ ): Promise<number> {
33
45
  let failures = 0;
34
- for (const pkg of listPublishablePackages(root)) {
46
+ for (const pkg of selectedPublishablePackages(root, projects)) {
35
47
  failures += await validatePackedPublishablePackageTool(root, pkg, validatePackedManifest);
36
48
  }
37
49
  return failures;
38
50
  }
39
51
 
40
- export async function validatePackedPublishablePackageTypes(root: string): Promise<number> {
52
+ export async function validatePackedPublishablePackageTypes(
53
+ root: string,
54
+ projects?: readonly string[],
55
+ ): Promise<number> {
41
56
  let failures = 0;
42
- for (const pkg of listPublishablePackages(root)) {
57
+ for (const pkg of selectedPublishablePackages(root, projects)) {
43
58
  failures += await validatePackedPublishablePackageTool(root, pkg, validateAttw);
44
59
  }
45
60
  return failures;
@@ -36,6 +36,14 @@ export interface MonorepoContext {
36
36
  root: string;
37
37
  syncRuntime: boolean;
38
38
  verbose?: boolean;
39
+ /**
40
+ * Nx project names the validation is about, when a caller knows: the
41
+ * release's candidates. The build phase and the packed-package packs scope
42
+ * to them; manifest-level packs still see the whole repository, because a
43
+ * package's hygiene does not depend on which packages ship today. Absent,
44
+ * everything is validated and everything is built.
45
+ */
46
+ projects?: readonly string[];
39
47
  }
40
48
 
41
49
  export interface ValidatePackOptions {
@@ -174,19 +182,19 @@ const packs: MonorepoPack[] = [
174
182
  {
175
183
  name: 'packed-package-publint',
176
184
  validatePostBuild(ctx) {
177
- return validatePackedPublishablePackagePublint(ctx.root);
185
+ return validatePackedPublishablePackagePublint(ctx.root, ctx.projects);
178
186
  },
179
187
  },
180
188
  {
181
189
  name: 'packed-package-manifest',
182
190
  validatePostBuild(ctx) {
183
- return validatePackedPublishablePackageManifest(ctx.root);
191
+ return validatePackedPublishablePackageManifest(ctx.root, ctx.projects);
184
192
  },
185
193
  },
186
194
  {
187
195
  name: 'packed-package-types',
188
196
  validatePostBuild(ctx) {
189
- return validatePackedPublishablePackageTypes(ctx.root);
197
+ return validatePackedPublishablePackageTypes(ctx.root, ctx.projects);
190
198
  },
191
199
  },
192
200
  {
@@ -369,9 +377,10 @@ async function runBuild(ctx: MonorepoContext, options: ValidatePackOptions = {})
369
377
  if (options.verbose) {
370
378
  printCheckHeading('build', true);
371
379
  }
380
+ const args = ['run-many', '-t', 'build', ...(ctx.projects?.length ? ['-p', ctx.projects.join(',')] : [])];
372
381
  const result = options.verbose
373
- ? { exitCode: await runStatus('nx', ['run-many', '-t', 'build'], ctx.root, false), stdout: '', stderr: '' }
374
- : await runResult('nx', ['run-many', '-t', 'build'], ctx.root);
382
+ ? { exitCode: await runStatus('nx', args, ctx.root, false), stdout: '', stderr: '' }
383
+ : await runResult('nx', args, ctx.root);
375
384
  const status = result.exitCode;
376
385
  if (status !== 0) {
377
386
  if (!options.verbose) {
@@ -614,7 +614,14 @@ function yamlLinesForStep(step: PublishWorkflowStep, options: PublishWorkflowDef
614
614
  'failure()',
615
615
  );
616
616
  case PublishWorkflowStepKind.ValidateMonorepoConfig:
617
- return conditionalRunStep(step, 'smoo monorepo validate');
617
+ // Scoped to the release's candidates: the build phase and the packed-package
618
+ // checks cover what ships. Unscoped, validate compiled every project - on a
619
+ // release touching no Rust that was 3m30 of the cowshed CLI on the critical
620
+ // path, for a binary the release did not contain.
621
+ return conditionalRunStep(
622
+ step,
623
+ `smoo monorepo validate --projects "${githubExpression('steps.version.outputs.projects')}"`,
624
+ );
618
625
  case PublishWorkflowStepKind.TagRelease:
619
626
  return tagReleaseStepLines(step.name);
620
627
  case PublishWorkflowStepKind.PublishRelease:
@@ -387,150 +387,3 @@ describe('resolveSecretEnvironment', () => {
387
387
  }
388
388
  });
389
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
- });