@smoothbricks/cli 0.11.16 → 0.11.18

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.
Files changed (79) hide show
  1. package/README.md +65 -4
  2. package/dist/cli.d.ts.map +1 -1
  3. package/dist/cli.js +47 -0
  4. package/dist/github-ci/index.d.ts +1 -1
  5. package/dist/github-ci/index.d.ts.map +1 -1
  6. package/dist/github-ci/index.js +7 -10
  7. package/dist/lib/secret-names.d.ts +35 -0
  8. package/dist/lib/secret-names.d.ts.map +1 -0
  9. package/dist/lib/secret-names.js +61 -0
  10. package/dist/monorepo/ci-workflow.d.ts +56 -0
  11. package/dist/monorepo/ci-workflow.d.ts.map +1 -1
  12. package/dist/monorepo/ci-workflow.js +214 -8
  13. package/dist/monorepo/managed-files.d.ts +23 -0
  14. package/dist/monorepo/managed-files.d.ts.map +1 -1
  15. package/dist/monorepo/managed-files.js +39 -1
  16. package/dist/monorepo/publish-workflow.d.ts +10 -1
  17. package/dist/monorepo/publish-workflow.d.ts.map +1 -1
  18. package/dist/monorepo/publish-workflow.js +58 -20
  19. package/dist/release/github-release.d.ts +10 -0
  20. package/dist/release/github-release.d.ts.map +1 -1
  21. package/dist/release/github-release.js +11 -5
  22. package/dist/release/index.d.ts.map +1 -1
  23. package/dist/release/index.js +4 -5
  24. package/dist/secrets/commands.d.ts +84 -0
  25. package/dist/secrets/commands.d.ts.map +1 -0
  26. package/dist/secrets/commands.js +375 -0
  27. package/dist/secrets/index.d.ts +103 -0
  28. package/dist/secrets/index.d.ts.map +1 -0
  29. package/dist/secrets/index.js +255 -0
  30. package/dist/secrets/repository.d.ts +60 -0
  31. package/dist/secrets/repository.d.ts.map +1 -0
  32. package/dist/secrets/repository.js +145 -0
  33. package/dist/wrangler/cloudflare.d.ts +7 -0
  34. package/dist/wrangler/cloudflare.d.ts.map +1 -1
  35. package/dist/wrangler/cloudflare.js +10 -0
  36. package/dist/wrangler/deploy-stage.d.ts +13 -2
  37. package/dist/wrangler/deploy-stage.d.ts.map +1 -1
  38. package/dist/wrangler/deploy-stage.js +89 -78
  39. package/dist/wrangler/deployed-version.d.ts +44 -0
  40. package/dist/wrangler/deployed-version.d.ts.map +1 -0
  41. package/dist/wrangler/deployed-version.js +87 -0
  42. package/dist/wrangler/live-version.d.ts +146 -0
  43. package/dist/wrangler/live-version.d.ts.map +1 -0
  44. package/dist/wrangler/live-version.js +326 -0
  45. package/dist/wrangler/stage-secrets.d.ts +57 -0
  46. package/dist/wrangler/stage-secrets.d.ts.map +1 -0
  47. package/dist/wrangler/stage-secrets.js +178 -0
  48. package/managed/raw/tooling/direnv/devenv.smoo.nix +9 -1
  49. package/managed/raw/tooling/git-hooks/pre-push.sh +30 -29
  50. package/package.json +2 -2
  51. package/src/cli.ts +69 -1
  52. package/src/github-ci/index.test.ts +92 -8
  53. package/src/github-ci/index.ts +7 -10
  54. package/src/lib/secret-names.ts +70 -0
  55. package/src/monorepo/__tests__/ci-workflow.test.ts +188 -2
  56. package/src/monorepo/__tests__/publish-workflow.test.ts +33 -15
  57. package/src/monorepo/ci-workflow.ts +294 -8
  58. package/src/monorepo/managed-files.test.ts +28 -1
  59. package/src/monorepo/managed-files.ts +56 -2
  60. package/src/monorepo/package-policy.test.ts +1 -1
  61. package/src/monorepo/publish-workflow.ts +65 -19
  62. package/src/monorepo/secret-references.test.ts +1 -1
  63. package/src/release/__tests__/github-release.test.ts +8 -4
  64. package/src/release/__tests__/private-npm-status.test.ts +2 -2
  65. package/src/release/github-release.ts +13 -5
  66. package/src/release/index.ts +4 -4
  67. package/src/secrets/commands.test.ts +28 -0
  68. package/src/secrets/commands.ts +412 -0
  69. package/src/secrets/index.test.ts +209 -0
  70. package/src/secrets/index.ts +287 -0
  71. package/src/secrets/repository.test.ts +98 -0
  72. package/src/secrets/repository.ts +164 -0
  73. package/src/wrangler/cloudflare.ts +18 -0
  74. package/src/wrangler/deploy-stage.test.ts +448 -23
  75. package/src/wrangler/deploy-stage.ts +142 -83
  76. package/src/wrangler/deployed-version.test.ts +150 -0
  77. package/src/wrangler/deployed-version.ts +130 -0
  78. package/src/wrangler/live-version.ts +363 -0
  79. package/src/wrangler/stage-secrets.ts +146 -0
@@ -49,6 +49,7 @@ const synchronizedPrettier = makeModuleSynchronized<typeof PrettierModule>(impor
49
49
 
50
50
  export type PublishWorkflowBump = 'auto' | 'patch' | 'minor' | 'major' | 'prerelease';
51
51
  export type PublishWorkflowCondition =
52
+ | 'plan-mode-not-none'
52
53
  | 'version-mode-not-none'
53
54
  | 'deploy-production'
54
55
  | 'deploy-production-standalone'
@@ -65,6 +66,7 @@ export enum PublishWorkflowStepKind {
65
66
  ConfigureReleaseAuthor = 'configure-release-author',
66
67
  BuildNxVersionActions = 'build-nx-version-actions',
67
68
  RepairPendingReleases = 'repair-pending-releases',
69
+ PlanRelease = 'plan-release',
68
70
  VersionRelease = 'version-release',
69
71
  CheckManagedMonorepoFiles = 'check-managed-monorepo-files',
70
72
  Build = 'build',
@@ -159,6 +161,12 @@ export interface PublishWorkflowCallbacks {
159
161
  buildNxVersionActions(): Promise<void>;
160
162
  repairPendingReleases(input: { dryRun: boolean }): Promise<void>;
161
163
  versionRelease(input: { bump: PublishWorkflowBump; dryRun: boolean }): Promise<PublishWorkflowVersionOutputs>;
164
+ /**
165
+ * The selection the bump WILL make, computed without writing anything, so the
166
+ * gates that run before the version commit know what to run over. Defaults to
167
+ * a dry-run `versionRelease` when a caller supplies none.
168
+ */
169
+ planRelease?(input: { bump: PublishWorkflowBump }): Promise<PublishWorkflowVersionOutputs>;
162
170
  checkManagedMonorepoFiles(): Promise<void>;
163
171
  nxRunMany(input: { target: PublishWorkflowNxTarget; projects: string[] }): Promise<void>;
164
172
  uploadTraceDbs(): Promise<void>;
@@ -183,6 +191,9 @@ type PublishWorkflowStepInput = Omit<PublishWorkflowStep, 'number'>;
183
191
 
184
192
  export function definePublishWorkflow(options: PublishWorkflowDefinitionOptions = {}): PublishWorkflowDefinition {
185
193
  const versionMode = githubExpression('steps.version.outputs.mode');
194
+ // Steps that run before the bump must label themselves from the plan: the
195
+ // version step's outputs do not exist yet, so the interpolation renders empty.
196
+ const planMode = githubExpression('steps.plan.outputs.mode');
186
197
  if (options.release === false) {
187
198
  return { steps: defineDeployOnlyWorkflowSteps(options) };
188
199
  }
@@ -224,30 +235,37 @@ export function definePublishWorkflow(options: PublishWorkflowDefinitionOptions
224
235
  kind: PublishWorkflowStepKind.RepairPendingReleases,
225
236
  name: '🧯 Repair pending releases',
226
237
  },
227
- { kind: PublishWorkflowStepKind.VersionRelease, name: '🔢 Version release', id: 'version' },
238
+ // Plan first, gate second, bump third. Lint and test hash the source, so
239
+ // running them BEFORE the version commit means their task hashes match
240
+ // the ones ci already computed for the same source — the publish reuses
241
+ // that cache instead of re-running 189 tasks. Build stays after the bump
242
+ // on purpose: an artifact built from pre-bump sources would ship the
243
+ // previous version string.
244
+ { kind: PublishWorkflowStepKind.PlanRelease, name: '🧭 Plan release', id: 'plan' },
228
245
  {
229
246
  kind: PublishWorkflowStepKind.CheckManagedMonorepoFiles,
230
- name: `✅ Check managed monorepo files (${versionMode})`,
231
- condition: 'version-mode-not-none',
232
- },
233
- {
234
- kind: PublishWorkflowStepKind.Build,
235
- name: `🔨 Build (${versionMode})`,
236
- condition: 'version-mode-not-none',
237
- nxTarget: 'build',
247
+ name: `✅ Check managed monorepo files (${planMode})`,
248
+ condition: 'plan-mode-not-none',
238
249
  },
239
250
  {
240
251
  kind: PublishWorkflowStepKind.Lint,
241
- name: `🔍 Lint (${versionMode})`,
242
- condition: 'version-mode-not-none',
252
+ name: `🔍 Lint (${planMode})`,
253
+ condition: 'plan-mode-not-none',
243
254
  nxTarget: 'lint',
244
255
  },
245
256
  {
246
257
  kind: PublishWorkflowStepKind.UnitTests,
247
- name: `🧪 Unit Tests (${versionMode})`,
248
- condition: 'version-mode-not-none',
258
+ name: `🧪 Unit Tests (${planMode})`,
259
+ condition: 'plan-mode-not-none',
249
260
  nxTarget: 'test',
250
261
  },
262
+ { kind: PublishWorkflowStepKind.VersionRelease, name: '🔢 Version release', id: 'version' },
263
+ {
264
+ kind: PublishWorkflowStepKind.Build,
265
+ name: `🔨 Build (${versionMode})`,
266
+ condition: 'version-mode-not-none',
267
+ nxTarget: 'build',
268
+ },
251
269
  { kind: PublishWorkflowStepKind.UploadTraceDbs, name: '📎 Upload trace DBs', condition: 'failure' },
252
270
  {
253
271
  kind: PublishWorkflowStepKind.ValidateMonorepoConfig,
@@ -302,10 +320,11 @@ export async function runPublishWorkflow(
302
320
  ): Promise<PublishWorkflowRunResult> {
303
321
  let setupOutputs: PublishWorkflowSetupOutputs = { nixCacheHit: '', devenvCacheHit: '' };
304
322
  let version: PublishWorkflowVersionOutputs = { mode: 'none', projects: [] };
323
+ let plan: PublishWorkflowVersionOutputs = { mode: 'none', projects: [] };
305
324
  let failed = false;
306
325
  let failure: unknown;
307
326
  for (const step of workflow.steps) {
308
- if (!shouldRunStep(step, version, failed, context.inputs)) {
327
+ if (!shouldRunStep(step, version, failed, context.inputs, plan)) {
309
328
  continue;
310
329
  }
311
330
  try {
@@ -331,6 +350,11 @@ export async function runPublishWorkflow(
331
350
  dryRun: context.inputs.dryRun,
332
351
  });
333
352
  break;
353
+ case PublishWorkflowStepKind.PlanRelease:
354
+ plan = context.callbacks.planRelease
355
+ ? await context.callbacks.planRelease({ bump: context.inputs.bump })
356
+ : await context.callbacks.versionRelease({ bump: context.inputs.bump, dryRun: true });
357
+ break;
334
358
  case PublishWorkflowStepKind.VersionRelease:
335
359
  version = await context.callbacks.versionRelease(context.inputs);
336
360
  break;
@@ -343,7 +367,12 @@ export async function runPublishWorkflow(
343
367
  if (!step.nxTarget) {
344
368
  throw new Error(`Workflow step ${step.kind} is missing an Nx target.`);
345
369
  }
346
- await context.callbacks.nxRunMany({ target: step.nxTarget, projects: version.projects });
370
+ // Lint and test run before the bump exists, so they select from the
371
+ // plan; build runs after it and selects from the written version.
372
+ await context.callbacks.nxRunMany({
373
+ target: step.nxTarget,
374
+ projects: step.kind === PublishWorkflowStepKind.Build ? version.projects : plan.projects,
375
+ });
347
376
  break;
348
377
  case PublishWorkflowStepKind.UploadTraceDbs:
349
378
  await context.callbacks.uploadTraceDbs();
@@ -383,7 +412,11 @@ function shouldRunStep(
383
412
  version: PublishWorkflowVersionOutputs,
384
413
  failed: boolean,
385
414
  inputs: PublishWorkflowInputs,
415
+ plan: PublishWorkflowVersionOutputs = version,
386
416
  ): boolean {
417
+ if (step.condition === 'plan-mode-not-none') {
418
+ return plan.mode !== 'none';
419
+ }
387
420
  if (step.condition === 'version-mode-not-none') {
388
421
  return version.mode !== 'none';
389
422
  }
@@ -581,6 +614,16 @@ function yamlLinesForStep(step: PublishWorkflowStep, options: PublishWorkflowDef
581
614
  ...privateNpmPublisherStepEnv(options),
582
615
  ` run: smoo release repair-pending --dry-run "${githubExpression('inputs.dry_run')}"`,
583
616
  ];
617
+ case PublishWorkflowStepKind.PlanRelease:
618
+ // The same selection the bump will make, computed without writing: the
619
+ // gates need `mode` and `projects` before any version commit exists.
620
+ return [
621
+ ` - name: ${step.name}`,
622
+ ' id: plan',
623
+ ' run:',
624
+ ` smoo release version --bump "${githubExpression('inputs.bump')}" --projects "${githubExpression('inputs.projects')}" --dry-run "true" --github-output`,
625
+ ' "$GITHUB_OUTPUT"',
626
+ ];
584
627
  case PublishWorkflowStepKind.VersionRelease:
585
628
  return [
586
629
  ` - name: ${step.name}`,
@@ -601,12 +644,12 @@ function yamlLinesForStep(step: PublishWorkflowStep, options: PublishWorkflowDef
601
644
  case PublishWorkflowStepKind.Lint:
602
645
  return conditionalRunStep(
603
646
  step,
604
- `smoo github-ci nx-run-many --targets lint --projects "${githubExpression('steps.version.outputs.projects')}"`,
647
+ `smoo github-ci nx-run-many --targets lint --projects "${githubExpression('steps.plan.outputs.projects')}"`,
605
648
  );
606
649
  case PublishWorkflowStepKind.UnitTests:
607
650
  return conditionalRunStep(
608
651
  step,
609
- `smoo github-ci nx-run-many --targets test --projects "${githubExpression('steps.version.outputs.projects')}"`,
652
+ `smoo github-ci nx-run-many --targets test --projects "${githubExpression('steps.plan.outputs.projects')}"`,
610
653
  );
611
654
  case PublishWorkflowStepKind.UploadTraceDbs:
612
655
  return artifactStepLines(
@@ -695,8 +738,11 @@ function deployProductionStep(step: PublishWorkflowStep, options: PublishWorkflo
695
738
  }
696
739
 
697
740
  function conditionalRunStep(step: PublishWorkflowStep, run: string): string[] {
698
- const condition = "steps.version.outputs.mode != 'none'";
699
- return [` - name: ${step.name}`, ` if: ${condition}`, ` run: ${run}`];
741
+ // A step gated on the plan must read the PLAN's mode: it runs before the
742
+ // version step exists, and `steps.version.outputs.mode` is empty there, which
743
+ // silently skips the gate rather than running it.
744
+ const source = step.condition === 'plan-mode-not-none' ? 'plan' : 'version';
745
+ return [` - name: ${step.name}`, ` if: steps.${source}.outputs.mode != 'none'`, ` run: ${run}`];
700
746
  }
701
747
 
702
748
  function siblingSourceCheckoutStepLines(options: PublishWorkflowDefinitionOptions): string[] {
@@ -206,7 +206,7 @@ describe('registryAuthEnvNames', () => {
206
206
 
207
207
  it('collects ${VAR} references from .npmrc text', async () => {
208
208
  expect(
209
- await names('@axe.sc:registry=https://npm.example.net\n//npm.example.net/:_authToken=${SMOO_READ_TOKEN}\n'),
209
+ await names('@acme:registry=https://npm.example.net\n//npm.example.net/:_authToken=${SMOO_READ_TOKEN}\n'),
210
210
  ).toEqual(['SMOO_READ_TOKEN']);
211
211
  });
212
212
 
@@ -3,6 +3,7 @@ import { readFile } from 'node:fs/promises';
3
3
  import type { ReleasePackageInfo } from '../core.js';
4
4
  import {
5
5
  createOrUpdateGithubRelease,
6
+ DEPENDENCY_ONLY_RELEASE_NOTES,
6
7
  type GithubReleaseWriteShell,
7
8
  githubReleaseLookupExists,
8
9
  nxProjectChangelogArgs,
@@ -63,10 +64,13 @@ describe('GitHub release helpers', () => {
63
64
  ).toBe('generated release notes');
64
65
  });
65
66
 
66
- it('fails when Nx omits the requested project changelog', () => {
67
- expect(() => projectChangelogContents({ projectChangelogs: {} }, 'pkg')).toThrow(
68
- 'Nx did not generate a project changelog for pkg.',
69
- );
67
+ it('renders a dependency-only body when Nx generates no project changelog', () => {
68
+ // Nx bumps a project "because a dependency was bumped", and such a version
69
+ // carries no commits of its own — so no changelog is the correct output
70
+ // rather than a failure. Refusing here aborted a publish AFTER the new
71
+ // versions had been written to every manifest, leaving the release half
72
+ // done: one dependency-only package took down the whole run.
73
+ expect(projectChangelogContents({ projectChangelogs: {} }, 'pkg')).toBe(DEPENDENCY_ONLY_RELEASE_NOTES);
70
74
  });
71
75
 
72
76
  it('distinguishes missing releases from transient GitHub lookup failures', () => {
@@ -434,8 +434,8 @@ describe('private npm published-version status', () => {
434
434
  });
435
435
 
436
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.
437
+ // The shape that cost a day: a repository commits
438
+ // `//host/path:_authToken=${NPM_PUBLISH_TOKEN}` at the workspace root.
439
439
  // npm ranks that project file ABOVE the userconfig this CLI writes, so with
440
440
  // the publish env unset it sent the unexpanded value and the registry
441
441
  // answered 401 — with a perfectly good read credential in hand.
@@ -80,12 +80,20 @@ export function nxProjectChangelogArgs(pkg: ReleasePackageInfo, previousTag: str
80
80
  return previousTag ? { ...base, from: previousTag } : { ...base, firstRelease: true };
81
81
  }
82
82
 
83
+ /**
84
+ * The release body for a version that carries no commits of its own.
85
+ *
86
+ * Nx bumps a project when a dependency it depends on was bumped, and reports
87
+ * that reason itself ("because a dependency was bumped"). Such a version has no
88
+ * commits touching the project, so Nx generates no changelog for it — which is
89
+ * correct, not a failure. Treating the absence as fatal made one dependency-only
90
+ * package abort a whole publish after the versions had already been written.
91
+ */
92
+ export const DEPENDENCY_ONLY_RELEASE_NOTES =
93
+ '_No changelog entries for this package: this version was released because one of its dependencies was bumped._';
94
+
83
95
  export function projectChangelogContents(result: ProjectChangelogLookupResult, projectName: string): string {
84
- const changelog = result.projectChangelogs?.[projectName];
85
- if (!changelog) {
86
- throw new Error(`Nx did not generate a project changelog for ${projectName}.`);
87
- }
88
- return changelog.contents;
96
+ return result.projectChangelogs?.[projectName]?.contents ?? DEPENDENCY_ONLY_RELEASE_NOTES;
89
97
  }
90
98
 
91
99
  export function githubReleaseLookupExists(tag: string, exitCode: number, stdout: string, stderr: string): boolean {
@@ -89,6 +89,7 @@ import {
89
89
  } from './core.js';
90
90
  import {
91
91
  createOrUpdateGithubRelease,
92
+ DEPENDENCY_ONLY_RELEASE_NOTES,
92
93
  githubReleaseLookupExists,
93
94
  importWorkspaceNx,
94
95
  renderNxProjectChangelogContents,
@@ -1596,10 +1597,9 @@ async function writeReleasePreviewSummary(
1596
1597
  lines.push(` - \`${pkg.name}\`: \`${currentByName.get(pkg.name)?.version ?? 'unknown'}\` -> \`${pkg.version}\``);
1597
1598
  }
1598
1599
  for (const pkg of previewPackages) {
1599
- const contents = releasePreviewChangelogs.get(pkg.projectName);
1600
- if (!contents) {
1601
- throw new Error(`Nx did not generate a project changelog for ${pkg.projectName}.`);
1602
- }
1600
+ // A dependency-only bump has no commits of its own, so Nx generates no
1601
+ // changelog for it. The preview says so rather than refusing to render.
1602
+ const contents = releasePreviewChangelogs.get(pkg.projectName) ?? DEPENDENCY_ONLY_RELEASE_NOTES;
1603
1603
  lines.push('', `### ${pkg.name} ${pkg.version}`, '', contents.trim());
1604
1604
  }
1605
1605
  }
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { secretsMissingInEnvironment } from './commands.js';
3
+ import { reconcileSecrets } from './index.js';
4
+
5
+ const sources = {
6
+ workerSecrets: { 'targets/billing': ['PAYMENTS_PUBLISHABLE_KEY', 'PAYMENTS_SECRET_KEY'] },
7
+ workflowSecrets: ['PAYMENTS_PUBLISHABLE_KEY', 'PAYMENTS_SECRET_KEY'],
8
+ localCommands: [],
9
+ secretNames: {
10
+ PAYMENTS_PUBLISHABLE_KEY: 'PAYMENTS_PUBLISHABLE_KEY',
11
+ PAYMENTS_SECRET_KEY: 'PAYMENTS_SECRET_KEY',
12
+ },
13
+ repositorySecrets: ['PAYMENTS_PUBLISHABLE_KEY', 'PAYMENTS_SECRET_KEY', 'RETIRED_TOKEN'],
14
+ environmentSecrets: { production: ['PAYMENTS_PUBLISHABLE_KEY'] },
15
+ };
16
+
17
+ describe('what `smoo secrets set --env` walks', () => {
18
+ it('offers a name the repository holds and the environment does not, and no undeclared one', () => {
19
+ // Overriding one stage's credentials is the reason to write at environment
20
+ // scope: skipping names the repository already holds would leave a live
21
+ // value unsettable. RETIRED_TOKEN is absent from the environment too, but
22
+ // nothing declares it - prompting for it would ask an operator to copy a
23
+ // stale value forward.
24
+ const walked = secretsMissingInEnvironment(reconcileSecrets(sources), 'production');
25
+
26
+ expect(walked.map((row) => row.name)).toEqual(['PAYMENTS_SECRET_KEY']);
27
+ });
28
+ });