@smoothbricks/cli 0.1.0 → 0.1.1

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 (92) hide show
  1. package/README.md +208 -83
  2. package/dist/cli.js +50 -20
  3. package/dist/lib/devenv.d.ts +6 -0
  4. package/dist/lib/devenv.d.ts.map +1 -0
  5. package/dist/lib/devenv.js +82 -0
  6. package/dist/lib/workspace.d.ts +5 -0
  7. package/dist/lib/workspace.d.ts.map +1 -1
  8. package/dist/lib/workspace.js +62 -5
  9. package/dist/monorepo/index.d.ts +6 -1
  10. package/dist/monorepo/index.d.ts.map +1 -1
  11. package/dist/monorepo/index.js +38 -5
  12. package/dist/monorepo/lockfile.d.ts +5 -1
  13. package/dist/monorepo/lockfile.d.ts.map +1 -1
  14. package/dist/monorepo/lockfile.js +45 -5
  15. package/dist/monorepo/managed-files.d.ts +1 -1
  16. package/dist/monorepo/managed-files.d.ts.map +1 -1
  17. package/dist/monorepo/managed-files.js +27 -10
  18. package/dist/monorepo/package-hygiene.js +1 -1
  19. package/dist/monorepo/package-policy.d.ts +3 -0
  20. package/dist/monorepo/package-policy.d.ts.map +1 -1
  21. package/dist/monorepo/package-policy.js +122 -19
  22. package/dist/monorepo/packed-manifest.d.ts +4 -0
  23. package/dist/monorepo/packed-manifest.d.ts.map +1 -0
  24. package/dist/monorepo/packed-manifest.js +51 -0
  25. package/dist/monorepo/packed-package.d.ts.map +1 -1
  26. package/dist/monorepo/packed-package.js +6 -0
  27. package/dist/monorepo/packs/index.d.ts.map +1 -1
  28. package/dist/monorepo/packs/index.js +4 -1
  29. package/dist/monorepo/publish-workflow.d.ts +86 -0
  30. package/dist/monorepo/publish-workflow.d.ts.map +1 -0
  31. package/dist/monorepo/publish-workflow.js +343 -0
  32. package/dist/nx-version-actions.cjs +25 -0
  33. package/dist/nx-version-actions.d.cts +6 -0
  34. package/dist/nx-version-actions.d.cts.map +1 -0
  35. package/dist/release/core.d.ts +50 -0
  36. package/dist/release/core.d.ts.map +1 -0
  37. package/dist/release/core.js +97 -0
  38. package/dist/release/github-release.d.ts +46 -0
  39. package/dist/release/github-release.d.ts.map +1 -0
  40. package/dist/release/github-release.js +97 -0
  41. package/dist/release/index.d.ts +14 -5
  42. package/dist/release/index.d.ts.map +1 -1
  43. package/dist/release/index.js +544 -109
  44. package/dist/release/npm-auth.d.ts +17 -0
  45. package/dist/release/npm-auth.d.ts.map +1 -0
  46. package/dist/release/npm-auth.js +51 -0
  47. package/dist/release/orchestration.d.ts +48 -0
  48. package/dist/release/orchestration.d.ts.map +1 -0
  49. package/dist/release/orchestration.js +105 -0
  50. package/dist/release/publish-plan.d.ts +17 -0
  51. package/dist/release/publish-plan.d.ts.map +1 -0
  52. package/dist/release/publish-plan.js +15 -0
  53. package/dist/release/retag-unpublished.d.ts +34 -0
  54. package/dist/release/retag-unpublished.d.ts.map +1 -0
  55. package/dist/release/retag-unpublished.js +77 -0
  56. package/managed/raw/tooling/git-hooks/pre-commit.sh +2 -7
  57. package/managed/templates/github/workflows/ci.yml +2 -0
  58. package/package.json +24 -3
  59. package/src/cli.ts +64 -25
  60. package/src/lib/devenv.test.ts +28 -0
  61. package/src/lib/devenv.ts +89 -0
  62. package/src/lib/workspace.ts +76 -5
  63. package/src/monorepo/__tests__/nx-version-actions.test.ts +75 -0
  64. package/src/monorepo/__tests__/publish-workflow.test.ts +315 -0
  65. package/src/monorepo/index.ts +45 -5
  66. package/src/monorepo/lockfile.ts +52 -7
  67. package/src/monorepo/managed-files.ts +42 -12
  68. package/src/monorepo/package-hygiene.ts +1 -1
  69. package/src/monorepo/package-policy.ts +134 -18
  70. package/src/monorepo/packed-manifest.ts +67 -0
  71. package/src/monorepo/packed-package.ts +7 -0
  72. package/src/monorepo/packs/index.ts +4 -0
  73. package/src/monorepo/publish-workflow.ts +430 -0
  74. package/src/nx-version-actions.cts +36 -0
  75. package/src/release/__tests__/core-properties.test.ts +149 -0
  76. package/src/release/__tests__/core-scenarios.test.ts +177 -0
  77. package/src/release/__tests__/core.test.ts +73 -0
  78. package/src/release/__tests__/fixture-repo.test.ts +305 -0
  79. package/src/release/__tests__/github-release.test.ts +144 -0
  80. package/src/release/__tests__/helpers/fixture-repo.ts +113 -0
  81. package/src/release/__tests__/npm-auth.test.ts +129 -0
  82. package/src/release/__tests__/orchestration.test.ts +202 -0
  83. package/src/release/__tests__/publish-plan.test.ts +61 -0
  84. package/src/release/__tests__/retag-unpublished.test.ts +160 -0
  85. package/src/release/core.ts +170 -0
  86. package/src/release/github-release.ts +134 -0
  87. package/src/release/index.ts +662 -116
  88. package/src/release/npm-auth.ts +122 -0
  89. package/src/release/orchestration.ts +186 -0
  90. package/src/release/publish-plan.ts +37 -0
  91. package/src/release/retag-unpublished.ts +122 -0
  92. package/managed/templates/github/workflows/publish.yml +0 -136
@@ -0,0 +1,17 @@
1
+ import type { ReleasePackageInfo } from './core.js';
2
+ export interface NpmPublishAuthFailureOptions {
3
+ useBootstrapToken: boolean;
4
+ tokenPresent: boolean;
5
+ repository?: string;
6
+ }
7
+ export interface NpmPublishDiagnosticShell {
8
+ publish(): Promise<void>;
9
+ versionExists(): Promise<boolean>;
10
+ log(message: string): void;
11
+ error(message: string): void;
12
+ appendSummary(markdown: string): Promise<void>;
13
+ }
14
+ export declare function publishWithAuthDiagnostics(pkg: Pick<ReleasePackageInfo, 'name' | 'version'>, shell: NpmPublishDiagnosticShell, options: NpmPublishAuthFailureOptions): Promise<void>;
15
+ export declare function npmPublishAuthFailureMessage(pkg: Pick<ReleasePackageInfo, 'name' | 'version'>, options: NpmPublishAuthFailureOptions): string;
16
+ export declare function npmPublishAuthFailureMarkdown(pkg: Pick<ReleasePackageInfo, 'name' | 'version'>, options: NpmPublishAuthFailureOptions): string;
17
+ //# sourceMappingURL=npm-auth.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"npm-auth.d.ts","sourceRoot":"","sources":["../../src/release/npm-auth.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAEpD,MAAM,WAAW,4BAA4B;IAC3C,iBAAiB,EAAE,OAAO,CAAC;IAC3B,YAAY,EAAE,OAAO,CAAC;IACtB,UAAU,CAAC,EAAE,MAAM,CAAC;CACrB;AAED,MAAM,WAAW,yBAAyB;IACxC,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC,CAAC;IACzB,aAAa,IAAI,OAAO,CAAC,OAAO,CAAC,CAAC;IAClC,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,KAAK,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IAC7B,aAAa,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAChD;AAED,wBAAsB,0BAA0B,CAC9C,GAAG,EAAE,IAAI,CAAC,kBAAkB,EAAE,MAAM,GAAG,SAAS,CAAC,EACjD,KAAK,EAAE,yBAAyB,EAChC,OAAO,EAAE,4BAA4B,GACpC,OAAO,CAAC,IAAI,CAAC,CAgBf;AAED,wBAAgB,4BAA4B,CAC1C,GAAG,EAAE,IAAI,CAAC,kBAAkB,EAAE,MAAM,GAAG,SAAS,CAAC,EACjD,OAAO,EAAE,4BAA4B,GACpC,MAAM,CAuCR;AAED,wBAAgB,6BAA6B,CAC3C,GAAG,EAAE,IAAI,CAAC,kBAAkB,EAAE,MAAM,GAAG,SAAS,CAAC,EACjD,OAAO,EAAE,4BAA4B,GACpC,MAAM,CAgCR"}
@@ -0,0 +1,51 @@
1
+ export async function publishWithAuthDiagnostics(pkg, shell, options) {
2
+ try {
3
+ await shell.publish();
4
+ }
5
+ catch (error) {
6
+ const packageVersion = `${pkg.name}@${pkg.version}`;
7
+ if (await shell.versionExists()) {
8
+ shell.log(`${packageVersion}: publish result already visible on npm; continuing.`);
9
+ return;
10
+ }
11
+ shell.error(npmPublishAuthFailureMessage(pkg, options));
12
+ await shell.appendSummary(npmPublishAuthFailureMarkdown(pkg, options));
13
+ throw new Error(`${packageVersion}: npm publish authentication failed. Run smoo release trust-publisher after the package exists on npm; see the warning banner above for details.`, { cause: error });
14
+ }
15
+ }
16
+ export function npmPublishAuthFailureMessage(pkg, options) {
17
+ const packageVersion = `${pkg.name}@${pkg.version}`;
18
+ const lines = [
19
+ `::error title=npm publish authentication failed::${packageVersion} could not be published. This usually means npm trusted publishing is not configured for this package/workflow/repo, or the bootstrap NPM_TOKEN is missing/invalid.`,
20
+ '',
21
+ `🚨 npm publish authentication failed for ${packageVersion}`,
22
+ '',
23
+ ];
24
+ if (options.useBootstrapToken) {
25
+ lines.push('smoo expected a temporary npm automation token because this package does not exist on npm yet.', options.tokenPresent
26
+ ? 'NODE_AUTH_TOKEN/NPM_TOKEN is set, but npm still rejected the bootstrap publish. Check that the token is valid, has publish rights for this scope, and is available to this workflow.'
27
+ : 'NODE_AUTH_TOKEN/NPM_TOKEN is not set. Add a temporary NPM_TOKEN repository secret and rerun the Publish workflow.', '', 'After the first successful publish, run:', ' smoo release trust-publisher', '', 'Then remove the temporary bootstrap token path for future releases.');
28
+ return lines.join('\n');
29
+ }
30
+ lines.push('smoo expected npm trusted publishing/OIDC because this package already exists on npm.', options.tokenPresent
31
+ ? 'NODE_AUTH_TOKEN/NPM_TOKEN is set but unused: smoo intentionally clears token auth for existing packages; npm must authenticate through trusted publishing instead.'
32
+ : 'NODE_AUTH_TOKEN/NPM_TOKEN is not set, which is expected for trusted publishing; npm did not authenticate the workflow as a trusted publisher.', '', 'Fix:', '1. Run locally: smoo release trust-publisher', '2. Ensure the npm trusted publisher uses:', ` repository: ${trustedPublisherRepository(options)}`, ' workflow: publish.yml', '3. Rerun the Publish workflow.', '', 'For first-ever package publishes, add a temporary NPM_TOKEN repository secret, publish once, then run smoo release trust-publisher.');
33
+ return lines.join('\n');
34
+ }
35
+ export function npmPublishAuthFailureMarkdown(pkg, options) {
36
+ const packageVersion = `${pkg.name}@${pkg.version}`;
37
+ const lines = ['## 🚨 npm Publish Authentication Failed', '', `Package: \`${packageVersion}\``, ''];
38
+ if (options.useBootstrapToken) {
39
+ lines.push('smoo expected a temporary npm automation token because this package does not exist on npm yet.', '', options.tokenPresent
40
+ ? '`NODE_AUTH_TOKEN`/`NPM_TOKEN` is set, but npm still rejected the bootstrap publish. Check that the token is valid, has publish rights for this scope, and is available to this workflow.'
41
+ : '`NODE_AUTH_TOKEN`/`NPM_TOKEN` is not set. Add a temporary `NPM_TOKEN` repository secret and rerun the Publish workflow.', '', 'After the first successful publish, run `smoo release trust-publisher`, then remove the temporary bootstrap token path for future releases.');
42
+ return lines.join('\n');
43
+ }
44
+ lines.push('smoo expected npm trusted publishing/OIDC because this package already exists on npm.', '', options.tokenPresent
45
+ ? '`NODE_AUTH_TOKEN`/`NPM_TOKEN` is set but unused: smoo intentionally clears token auth for existing packages; npm must authenticate through trusted publishing instead.'
46
+ : '`NODE_AUTH_TOKEN`/`NPM_TOKEN` is not set, which is expected for trusted publishing; npm did not authenticate the workflow as a trusted publisher.', '', 'Fix:', '', '1. Run locally: `smoo release trust-publisher`', `2. Ensure the npm trusted publisher uses repository \`${trustedPublisherRepository(options)}\` and workflow \`publish.yml\``, '3. Rerun the Publish workflow.', '', 'For first-ever package publishes, add a temporary `NPM_TOKEN` repository secret, publish once, then run `smoo release trust-publisher`.');
47
+ return lines.join('\n');
48
+ }
49
+ function trustedPublisherRepository(options) {
50
+ return options.repository ?? 'the current GitHub repository';
51
+ }
@@ -0,0 +1,48 @@
1
+ import type { ReleasePackageInfo, ReleaseTarget } from './core.js';
2
+ export type ReleaseVersionMode = 'new' | 'none';
3
+ export interface ReleaseSummary<Package extends ReleasePackageInfo = ReleasePackageInfo> {
4
+ sha: string;
5
+ dryRun: boolean;
6
+ packages: Package[];
7
+ pushed: boolean;
8
+ published: Package[];
9
+ alreadyPublished: Package[];
10
+ githubReleases: Package[];
11
+ rerunRequired: boolean;
12
+ noRelease: boolean;
13
+ }
14
+ export interface ReleaseCompletionShell<Package extends ReleasePackageInfo = ReleasePackageInfo> {
15
+ gitHead(): Promise<string>;
16
+ pushReleaseRefs(packages: Package[]): Promise<boolean>;
17
+ listNpmMissingPackages(packages: Package[]): Promise<Package[]>;
18
+ buildReleaseCandidate(packages: Package[]): Promise<void>;
19
+ publishPackage(pkg: Package, distTag: string, dryRun: boolean): Promise<void>;
20
+ listGithubMissingPackages(packages: Package[]): Promise<Package[]>;
21
+ createGithubRelease(pkg: Package, dryRun: boolean): Promise<void>;
22
+ }
23
+ export interface ReleaseRepairShell<Package extends ReleasePackageInfo = ReleasePackageInfo> extends ReleaseCompletionShell<Package> {
24
+ checkout(ref: string): Promise<void>;
25
+ withDevenvEnv<T>(runWithEnv: () => Promise<T>): Promise<T>;
26
+ beforeRepairTarget?(target: ReleaseTarget<Package>): void;
27
+ afterRepairTarget?(target: ReleaseTarget<Package>): void;
28
+ }
29
+ export interface ReleaseVersionShell<Package extends ReleasePackageInfo = ReleasePackageInfo> {
30
+ releasePackagesAtHead(): Promise<Package[]>;
31
+ ensureLocalReleaseTags(packages: Package[]): Promise<void>;
32
+ gitHead(): Promise<string>;
33
+ runNxReleaseVersion(bump: string, dryRun: boolean): Promise<void>;
34
+ assertCleanGitTree(): Promise<void>;
35
+ }
36
+ export interface ReleaseVersionResult<Package extends ReleasePackageInfo = ReleasePackageInfo> {
37
+ mode: ReleaseVersionMode;
38
+ packages: Package[];
39
+ status: 'already-release-target' | 'dry-run' | 'new-release' | 'no-release-needed';
40
+ }
41
+ export declare function runReleaseVersion<Package extends ReleasePackageInfo>(shell: ReleaseVersionShell<Package>, options: {
42
+ bump: string;
43
+ dryRun: boolean;
44
+ }): Promise<ReleaseVersionResult<Package>>;
45
+ export declare function completeReleaseAtHead<Package extends ReleasePackageInfo>(shell: ReleaseCompletionShell<Package>, packages: Package[], dryRun: boolean, rerunRequired: boolean): Promise<ReleaseSummary<Package>>;
46
+ export declare function repairPendingTargets<Package extends ReleasePackageInfo>(shell: ReleaseRepairShell<Package>, targets: Array<ReleaseTarget<Package>>, restoreRef: string, dryRun: boolean): Promise<Array<ReleaseSummary<Package>>>;
47
+ export declare function completeRepairTargetAtHead<Package extends ReleasePackageInfo>(shell: ReleaseCompletionShell<Package>, target: ReleaseTarget<Package>, dryRun: boolean): Promise<ReleaseSummary<Package>>;
48
+ //# sourceMappingURL=orchestration.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"orchestration.d.ts","sourceRoot":"","sources":["../../src/release/orchestration.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,105 @@
1
+ import { planPublishActions } from './publish-plan.js';
2
+ export async function runReleaseVersion(shell, options) {
3
+ if (!options.dryRun) {
4
+ const localRelease = await shell.releasePackagesAtHead();
5
+ if (localRelease.length > 0) {
6
+ await shell.ensureLocalReleaseTags(localRelease);
7
+ return { mode: 'none', packages: localRelease, status: 'already-release-target' };
8
+ }
9
+ }
10
+ const headBeforeVersioning = await shell.gitHead();
11
+ await shell.runNxReleaseVersion(options.bump, options.dryRun);
12
+ if (options.dryRun) {
13
+ return { mode: 'none', packages: [], status: 'dry-run' };
14
+ }
15
+ await shell.assertCleanGitTree();
16
+ const headAfterVersioning = await shell.gitHead();
17
+ const releasedPackages = await shell.releasePackagesAtHead();
18
+ await shell.ensureLocalReleaseTags(releasedPackages);
19
+ if (headAfterVersioning === headBeforeVersioning) {
20
+ if (releasedPackages.length > 0) {
21
+ return { mode: 'none', packages: releasedPackages, status: 'already-release-target' };
22
+ }
23
+ if (options.bump !== 'auto') {
24
+ throw new Error(`Nx did not create a release commit for forced --bump ${options.bump}.`);
25
+ }
26
+ return { mode: 'none', packages: [], status: 'no-release-needed' };
27
+ }
28
+ if (releasedPackages.length === 0) {
29
+ throw new Error('Nx created a release commit without current package release tags at HEAD.');
30
+ }
31
+ return { mode: 'new', packages: releasedPackages, status: 'new-release' };
32
+ }
33
+ export async function completeReleaseAtHead(shell, packages, dryRun, rerunRequired) {
34
+ const npmMissingPackages = await shell.listNpmMissingPackages(packages);
35
+ const githubMissingPackages = dryRun ? packages : await shell.listGithubMissingPackages(packages);
36
+ return completePlannedRelease(shell, {
37
+ sha: await shell.gitHead(),
38
+ dryRun,
39
+ packages,
40
+ pushed: dryRun ? false : await shell.pushReleaseRefs(packages),
41
+ npmMissingPackages,
42
+ githubMissingPackages,
43
+ rerunRequired,
44
+ });
45
+ }
46
+ export async function repairPendingTargets(shell, targets, restoreRef, dryRun) {
47
+ const summaries = [];
48
+ try {
49
+ for (const target of targets) {
50
+ await shell.checkout(target.sha);
51
+ if (target.packages.length === 0) {
52
+ throw new Error(`Release commit ${target.sha} has no release packages after checkout.`);
53
+ }
54
+ shell.beforeRepairTarget?.(target);
55
+ try {
56
+ summaries.push(await shell.withDevenvEnv(() => completeRepairTargetAtHead(shell, target, dryRun)));
57
+ }
58
+ finally {
59
+ shell.afterRepairTarget?.(target);
60
+ }
61
+ }
62
+ }
63
+ finally {
64
+ await shell.checkout(restoreRef);
65
+ }
66
+ return summaries;
67
+ }
68
+ export async function completeRepairTargetAtHead(shell, target, dryRun) {
69
+ return completePlannedRelease(shell, {
70
+ sha: await shell.gitHead(),
71
+ dryRun,
72
+ packages: target.packages,
73
+ pushed: dryRun ? false : await shell.pushReleaseRefs(target.packages),
74
+ npmMissingPackages: target.npmPackages,
75
+ githubMissingPackages: target.githubPackages,
76
+ rerunRequired: false,
77
+ });
78
+ }
79
+ async function completePlannedRelease(shell, input) {
80
+ const plan = planPublishActions({
81
+ releasePackages: input.packages,
82
+ npmMissingPackages: input.npmMissingPackages,
83
+ githubMissingPackages: input.githubMissingPackages,
84
+ });
85
+ if (plan.buildProjects.length > 0) {
86
+ await shell.buildReleaseCandidate(plan.publishPackages.map((action) => action.pkg));
87
+ }
88
+ for (const { pkg, distTag } of plan.publishPackages) {
89
+ await shell.publishPackage(pkg, distTag, input.dryRun);
90
+ }
91
+ for (const pkg of plan.githubReleasePackages) {
92
+ await shell.createGithubRelease(pkg, input.dryRun);
93
+ }
94
+ return {
95
+ sha: input.sha,
96
+ dryRun: input.dryRun,
97
+ packages: input.packages,
98
+ pushed: input.pushed,
99
+ published: input.dryRun ? [] : plan.publishPackages.map((action) => action.pkg),
100
+ alreadyPublished: input.packages.filter((pkg) => !input.npmMissingPackages.includes(pkg)),
101
+ githubReleases: input.dryRun ? [] : plan.githubReleasePackages,
102
+ rerunRequired: input.rerunRequired,
103
+ noRelease: false,
104
+ };
105
+ }
@@ -0,0 +1,17 @@
1
+ import { type ReleasePackageInfo } from './core.js';
2
+ export interface PublishPackageAction<Package extends ReleasePackageInfo = ReleasePackageInfo> {
3
+ pkg: Package;
4
+ distTag: string;
5
+ }
6
+ export interface PublishPlan<Package extends ReleasePackageInfo = ReleasePackageInfo> {
7
+ buildProjects: string[];
8
+ publishPackages: Array<PublishPackageAction<Package>>;
9
+ githubReleasePackages: Package[];
10
+ }
11
+ export interface PublishPlanInput<Package extends ReleasePackageInfo = ReleasePackageInfo> {
12
+ releasePackages: Package[];
13
+ npmMissingPackages: Package[];
14
+ githubMissingPackages: Package[];
15
+ }
16
+ export declare function planPublishActions<Package extends ReleasePackageInfo>(input: PublishPlanInput<Package>): PublishPlan<Package>;
17
+ //# sourceMappingURL=publish-plan.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"publish-plan.d.ts","sourceRoot":"","sources":["../../src/release/publish-plan.ts"],"names":[],"mappings":"AAAA,OAAO,EAAwB,KAAK,kBAAkB,EAAE,MAAM,WAAW,CAAC;AAE1E,MAAM,WAAW,oBAAoB,CAAC,OAAO,SAAS,kBAAkB,GAAG,kBAAkB;IAC3F,GAAG,EAAE,OAAO,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,WAAW,CAAC,OAAO,SAAS,kBAAkB,GAAG,kBAAkB;IAClF,aAAa,EAAE,MAAM,EAAE,CAAC;IACxB,eAAe,EAAE,KAAK,CAAC,oBAAoB,CAAC,OAAO,CAAC,CAAC,CAAC;IACtD,qBAAqB,EAAE,OAAO,EAAE,CAAC;CAClC;AAED,MAAM,WAAW,gBAAgB,CAAC,OAAO,SAAS,kBAAkB,GAAG,kBAAkB;IACvF,eAAe,EAAE,OAAO,EAAE,CAAC;IAC3B,kBAAkB,EAAE,OAAO,EAAE,CAAC;IAC9B,qBAAqB,EAAE,OAAO,EAAE,CAAC;CAClC;AAED,wBAAgB,kBAAkB,CAAC,OAAO,SAAS,kBAAkB,EACnE,KAAK,EAAE,gBAAgB,CAAC,OAAO,CAAC,GAC/B,WAAW,CAAC,OAAO,CAAC,CAWtB"}
@@ -0,0 +1,15 @@
1
+ import { npmDistTagForVersion } from './core.js';
2
+ export function planPublishActions(input) {
3
+ const npmMissingNames = packageNameSet(input.npmMissingPackages);
4
+ const githubMissingNames = packageNameSet(input.githubMissingPackages);
5
+ const npmPackages = input.releasePackages.filter((pkg) => npmMissingNames.has(pkg.name));
6
+ const githubReleasePackages = input.releasePackages.filter((pkg) => githubMissingNames.has(pkg.name));
7
+ return {
8
+ buildProjects: npmPackages.map((pkg) => pkg.name),
9
+ publishPackages: npmPackages.map((pkg) => ({ pkg, distTag: npmDistTagForVersion(pkg.version) })),
10
+ githubReleasePackages,
11
+ };
12
+ }
13
+ function packageNameSet(packages) {
14
+ return new Set(packages.map((pkg) => pkg.name));
15
+ }
@@ -0,0 +1,34 @@
1
+ import { type ReleasePackageInfo } from './core.js';
2
+ export interface RetagUnpublishedOptions {
3
+ tags: string[];
4
+ toRef: string;
5
+ push: boolean;
6
+ dispatch: boolean;
7
+ dryRun: boolean;
8
+ branch: string;
9
+ workflow: string;
10
+ }
11
+ export interface RetagUnpublishedTagUpdate<Package extends ReleasePackageInfo = ReleasePackageInfo> {
12
+ tag: string;
13
+ pkg: Package;
14
+ expectedRemoteObject: string | null;
15
+ }
16
+ export interface RetagUnpublishedShell<Package extends Omit<ReleasePackageInfo, 'version'> = ReleasePackageInfo> {
17
+ listReleasePackages(): Package[];
18
+ resolveRef(ref: string): Promise<string>;
19
+ resolveDispatchRef(branch: string): Promise<string | null>;
20
+ packageVersionAtRef(packagePath: string, ref: string): Promise<string | null>;
21
+ npmVersionExists(name: string, version: string): Promise<boolean>;
22
+ githubReleaseExists(tag: string): Promise<boolean>;
23
+ remoteTagObject(tag: string): Promise<string | null>;
24
+ createOrMoveTag(tag: string, ref: string): Promise<void>;
25
+ pushTags(updates: Array<RetagUnpublishedTagUpdate<Package & {
26
+ version: string;
27
+ }>>): Promise<void>;
28
+ dispatchPublishWorkflow(workflow: string, branch: string): Promise<void>;
29
+ log(message: string): void;
30
+ }
31
+ export declare function retagUnpublished<Package extends Omit<ReleasePackageInfo, 'version'>>(shell: RetagUnpublishedShell<Package>, options: RetagUnpublishedOptions): Promise<Array<RetagUnpublishedTagUpdate<Package & {
32
+ version: string;
33
+ }>>>;
34
+ //# sourceMappingURL=retag-unpublished.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"retag-unpublished.d.ts","sourceRoot":"","sources":["../../src/release/retag-unpublished.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,kBAAkB,EAAwB,MAAM,WAAW,CAAC;AAE1E,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,MAAM,EAAE,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,IAAI,EAAE,OAAO,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;IAClB,MAAM,EAAE,OAAO,CAAC;IAChB,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,yBAAyB,CAAC,OAAO,SAAS,kBAAkB,GAAG,kBAAkB;IAChG,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,OAAO,CAAC;IACb,oBAAoB,EAAE,MAAM,GAAG,IAAI,CAAC;CACrC;AAED,MAAM,WAAW,qBAAqB,CAAC,OAAO,SAAS,IAAI,CAAC,kBAAkB,EAAE,SAAS,CAAC,GAAG,kBAAkB;IAC7G,mBAAmB,IAAI,OAAO,EAAE,CAAC;IACjC,UAAU,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACzC,kBAAkB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC3D,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IAC9E,gBAAgB,CAAC,IAAI,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IAClE,mBAAmB,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;IACnD,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,GAAG,IAAI,CAAC,CAAC;IACrD,eAAe,CAAC,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzD,QAAQ,CAAC,OAAO,EAAE,KAAK,CAAC,yBAAyB,CAAC,OAAO,GAAG;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAClG,uBAAuB,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACzE,GAAG,CAAC,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CAC5B;AAED,wBAAsB,gBAAgB,CAAC,OAAO,SAAS,IAAI,CAAC,kBAAkB,EAAE,SAAS,CAAC,EACxF,KAAK,EAAE,qBAAqB,CAAC,OAAO,CAAC,EACrC,OAAO,EAAE,uBAAuB,GAC/B,OAAO,CAAC,KAAK,CAAC,yBAAyB,CAAC,OAAO,GAAG;IAAE,OAAO,EAAE,MAAM,CAAA;CAAE,CAAC,CAAC,CAAC,CA4E1E"}
@@ -0,0 +1,77 @@
1
+ import { releasePackageForTag } from './core.js';
2
+ export async function retagUnpublished(shell, options) {
3
+ if (options.tags.length === 0) {
4
+ throw new Error('retag-unpublished requires at least one release tag.');
5
+ }
6
+ assertUniqueTags(options.tags);
7
+ const targetSha = await shell.resolveRef(options.toRef);
8
+ if (options.dispatch) {
9
+ const dispatchSha = await shell.resolveDispatchRef(options.branch);
10
+ if (!dispatchSha) {
11
+ throw new Error(`Cannot dispatch ${options.workflow}: remote branch ${options.branch} was not found.`);
12
+ }
13
+ if (dispatchSha !== targetSha) {
14
+ throw new Error(`Cannot dispatch ${options.workflow}: ${options.toRef} resolves to ${targetSha.slice(0, 12)}, but ${options.branch} resolves to ${dispatchSha.slice(0, 12)}. Push the branch first or choose a ref already at the remote branch HEAD.`);
15
+ }
16
+ }
17
+ const packages = shell.listReleasePackages();
18
+ const updates = [];
19
+ for (const tag of options.tags) {
20
+ const match = releasePackageForTag(packages, tag);
21
+ if (!match) {
22
+ throw new Error(`Release tag ${tag} does not match an owned release package.`);
23
+ }
24
+ const versionAtRef = await shell.packageVersionAtRef(match.pkg.path, options.toRef);
25
+ if (versionAtRef !== match.version) {
26
+ throw new Error(`Release tag ${tag} cannot move to ${options.toRef}: ${match.pkg.path}/package.json has version ${versionAtRef ?? 'missing'}, expected ${match.version}.`);
27
+ }
28
+ if (await shell.npmVersionExists(match.pkg.name, match.version)) {
29
+ throw new Error(`Cannot retag ${tag}: ${match.pkg.name}@${match.version} already exists on npm.`);
30
+ }
31
+ if (await shell.githubReleaseExists(tag)) {
32
+ throw new Error(`Cannot retag ${tag}: GitHub Release ${tag} already exists.`);
33
+ }
34
+ updates.push({
35
+ tag,
36
+ pkg: { ...match.pkg, version: match.version },
37
+ expectedRemoteObject: options.push ? await shell.remoteTagObject(tag) : null,
38
+ });
39
+ }
40
+ for (const update of updates) {
41
+ if (options.dryRun) {
42
+ shell.log(`Would move ${update.tag} to ${options.toRef} (${targetSha.slice(0, 12)}).`);
43
+ }
44
+ else {
45
+ await shell.createOrMoveTag(update.tag, options.toRef);
46
+ shell.log(`Moved ${update.tag} to ${options.toRef} (${targetSha.slice(0, 12)}).`);
47
+ }
48
+ }
49
+ if (options.push) {
50
+ if (options.dryRun) {
51
+ shell.log(`Would push ${updates.length} retagged release tag${updates.length === 1 ? '' : 's'}.`);
52
+ }
53
+ else {
54
+ await shell.pushTags(updates);
55
+ shell.log(`Pushed ${updates.length} retagged release tag${updates.length === 1 ? '' : 's'}.`);
56
+ }
57
+ }
58
+ if (options.dispatch) {
59
+ if (options.dryRun) {
60
+ shell.log(`Would dispatch ${options.workflow} on ${options.branch} with bump=auto.`);
61
+ }
62
+ else {
63
+ await shell.dispatchPublishWorkflow(options.workflow, options.branch);
64
+ shell.log(`Dispatched ${options.workflow} on ${options.branch} with bump=auto.`);
65
+ }
66
+ }
67
+ return updates;
68
+ }
69
+ function assertUniqueTags(tags) {
70
+ const seen = new Set();
71
+ for (const tag of tags) {
72
+ if (seen.has(tag)) {
73
+ throw new Error(`Release tag ${tag} was provided more than once.`);
74
+ }
75
+ seen.add(tag);
76
+ }
77
+ }
@@ -10,10 +10,5 @@ set -e -o pipefail
10
10
  git-format-staged
11
11
 
12
12
  # Adding a workspace package is rare and easy to leave half-wired. Run the
13
- # full monorepo validator only for newly staged package manifests; --fail-fast
14
- # keeps pre-commit output focused on the first blocking setup issue.
15
- if ! git diff --cached --quiet --diff-filter=A -- \
16
- 'packages/*/package.json' \
17
- 'targets/*/package.json'; then
18
- smoo monorepo validate --fail-fast
19
- fi
13
+ # full monorepo validator only for newly staged package manifests.
14
+ smoo monorepo validate --fail-fast --only-if-new-workspace-package
@@ -2,6 +2,8 @@ name: CI
2
2
 
3
3
  on:
4
4
  push:
5
+ branches:
6
+ - '**'
5
7
  pull_request:
6
8
 
7
9
  permissions:
package/package.json CHANGED
@@ -1,11 +1,14 @@
1
1
  {
2
2
  "name": "@smoothbricks/cli",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "description": "SmoothBricks monorepo automation CLI",
6
6
  "bin": {
7
7
  "smoo": "./bin/smoo"
8
8
  },
9
+ "scripts": {
10
+ "test": "bun test"
11
+ },
9
12
  "main": "./dist/index.js",
10
13
  "module": "./dist/index.js",
11
14
  "types": "./dist/index.d.ts",
@@ -30,6 +33,11 @@
30
33
  "development": "./src/monorepo/git-config.ts",
31
34
  "import": "./dist/monorepo/git-config.js",
32
35
  "default": "./dist/monorepo/git-config.js"
36
+ },
37
+ "./nx-version-actions": {
38
+ "types": "./dist/nx-version-actions.d.cts",
39
+ "require": "./dist/nx-version-actions.cjs",
40
+ "default": "./dist/nx-version-actions.cjs"
33
41
  }
34
42
  },
35
43
  "files": [
@@ -42,15 +50,28 @@
42
50
  ],
43
51
  "dependencies": {
44
52
  "@arethetypeswrong/cli": "^0.18.2",
45
- "@smoothbricks/validation": "0.0.1",
53
+ "@smoothbricks/validation": "0.1.1",
46
54
  "commander": "^14.0.3",
47
55
  "publint": "^0.3.18",
48
56
  "sherif": "^1.11.1",
49
57
  "tslib": "^2.8.1"
50
58
  },
59
+ "devDependencies": {
60
+ "fast-check": "^4.4.0"
61
+ },
51
62
  "nx": {
52
63
  "targets": {
53
- "lint": {}
64
+ "lint": {},
65
+ "typecheck-tests": {
66
+ "executor": "nx:run-commands",
67
+ "options": {
68
+ "command": "tsc --noEmit -p tsconfig.test.json",
69
+ "cwd": "packages/cli"
70
+ },
71
+ "dependsOn": [
72
+ "build"
73
+ ]
74
+ }
54
75
  },
55
76
  "tags": [
56
77
  "npm:public"
package/src/cli.ts CHANGED
@@ -34,7 +34,8 @@ function buildProgram(): Command {
34
34
  monorepo
35
35
  .command('validate')
36
36
  .option('--fail-fast', 'stop after the first failing validation pack')
37
- .action(async (options: { failFast?: boolean }) => {
37
+ .option('--only-if-new-workspace-package', 'skip validation unless a new workspace package manifest is staged')
38
+ .action(async (options: { failFast?: boolean; onlyIfNewWorkspacePackage?: boolean }) => {
38
39
  const { validateMonorepo } = await import('./monorepo/index.js');
39
40
  await validateMonorepo(await findRepoRoot(), options);
40
41
  });
@@ -58,54 +59,92 @@ function buildProgram(): Command {
58
59
  const { syncBunLockfileVersions } = await import('./monorepo/index.js');
59
60
  syncBunLockfileVersions(await findRepoRoot());
60
61
  });
61
- monorepo.command('list-public-projects').action(async () => {
62
- const { listPublicProjects } = await import('./monorepo/index.js');
63
- console.log(listPublicProjects(await findRepoRoot()));
64
- });
62
+ monorepo
63
+ .command('list-release-packages')
64
+ .option('--fail-empty', 'fail when no owned release packages are found')
65
+ .option('--github-output <path>', 'append projects=<packages> to a GitHub Actions output file')
66
+ .action(async (options: { failEmpty?: boolean; githubOutput?: string }) => {
67
+ const { listReleasePackagesForNx } = await import('./monorepo/index.js');
68
+ const packages = listReleasePackagesForNx(await findRepoRoot(), options);
69
+ if (!options.githubOutput) {
70
+ console.log(packages);
71
+ }
72
+ });
65
73
  monorepo.command('validate-public-tags').action(async () => {
66
74
  const { validatePublicPackageTags } = await import('./monorepo/index.js');
67
75
  validatePublicPackageTags(await findRepoRoot());
68
76
  });
69
- monorepo.command('release-state').action(async () => {
77
+ const release = program.command('release').description('Version, publish, and create GitHub Releases');
78
+ release.command('npm-status').action(async () => {
70
79
  const { printReleaseState } = await import('./release/index.js');
71
80
  await printReleaseState(await findRepoRoot());
72
81
  });
73
-
74
- const release = program.command('release').description('Version, publish, and create GitHub Releases');
82
+ release
83
+ .command('repair-pending')
84
+ .description('Repair incomplete older release commits before releasing the current HEAD')
85
+ .option('--dry-run [dryRun]', 'run without pushing, publishing, or writing GitHub Releases')
86
+ .action(async (options: { dryRun?: string | boolean }) => {
87
+ const { releaseRepairPending } = await import('./release/index.js');
88
+ await releaseRepairPending(await findRepoRoot(), { ...options, dryRun: booleanOption(options.dryRun) });
89
+ });
75
90
  release
76
91
  .command('version')
77
92
  .option('--bump <bump>', 'auto, patch, minor, major, or prerelease', 'auto')
78
- .option('--dry-run [dryRun]', 'run without writing versions, tags, or pushes')
79
- .action(async (options: { bump: string; dryRun?: string | boolean }) => {
93
+ .option('--dry-run [dryRun]', 'run without writing versions or tags')
94
+ .option('--github-output <path>', 'append mode=<mode> and projects=<packages> to a GitHub Actions output file')
95
+ .action(async (options: { bump: string; dryRun?: string | boolean; githubOutput?: string }) => {
80
96
  const { releaseVersion } = await import('./release/index.js');
81
- await releaseVersion(await findRepoRoot(), { bump: options.bump, dryRun: booleanOption(options.dryRun) });
97
+ await releaseVersion(await findRepoRoot(), {
98
+ bump: options.bump,
99
+ dryRun: booleanOption(options.dryRun),
100
+ githubOutput: options.githubOutput,
101
+ });
82
102
  });
83
103
  release
84
104
  .command('publish')
85
105
  .option('--bump <bump>', 'auto, patch, minor, major, or prerelease', 'auto')
86
- .option('--tag <tag>', 'explicit npm dist-tag; must match bump-derived tag')
87
- .option('--npm-tag <npmTag>', 'explicit npm dist-tag; must match bump-derived tag')
88
- .option('--dry-run [dryRun]', 'run without publishing')
89
- .action(async (options: { bump: string; tag?: string; npmTag?: string; dryRun?: string | boolean }) => {
106
+ .option('--dry-run [dryRun]', 'run without pushing, publishing, or writing GitHub Releases')
107
+ .action(async (options: { bump: string; dryRun?: string | boolean }) => {
90
108
  const { releasePublish } = await import('./release/index.js');
91
109
  await releasePublish(await findRepoRoot(), { ...options, dryRun: booleanOption(options.dryRun) });
92
110
  });
93
111
  release
94
- .command('github-release')
95
- .option('--tags <tags>', 'space-separated release tags')
96
- .option('--bump <bump>', 'auto, patch, minor, major, or prerelease', 'auto')
97
- .option('--tag <tag>', 'explicit npm dist-tag; must match bump-derived tag')
98
- .option('--npm-tag <npmTag>', 'explicit npm dist-tag; must match bump-derived tag')
99
- .option('--dry-run [dryRun]', 'skip GitHub Release writes')
112
+ .command('retag-unpublished')
113
+ .description('Move unpublished owned release tags to a later commit without bumping versions')
114
+ .argument('<tag...>', 'owned release tags to move, for example @scope/pkg@1.2.3')
115
+ .option('--to <ref>', 'commit or ref to move tags to', 'HEAD')
116
+ .option('--push', 'push moved tags with force-with-lease')
117
+ .option('--dispatch', 'push moved tags and start publish.yml with bump=auto')
118
+ .option('--remote <remote>', 'git remote used for pushed tags')
119
+ .option('--branch <branch>', 'branch used for publish workflow dispatch')
120
+ .option('--dry-run [dryRun]', 'validate and print the retag operation without mutating refs')
100
121
  .action(
101
- async (options: { tags?: string; bump: string; tag?: string; npmTag?: string; dryRun?: string | boolean }) => {
102
- const { releaseGithubRelease } = await import('./release/index.js');
103
- await releaseGithubRelease(await findRepoRoot(), { ...options, dryRun: booleanOption(options.dryRun) });
122
+ async (
123
+ tags: string[],
124
+ options: {
125
+ to?: string;
126
+ push?: boolean;
127
+ dispatch?: boolean;
128
+ remote?: string;
129
+ branch?: string;
130
+ dryRun?: string | boolean;
131
+ },
132
+ ) => {
133
+ const { releaseRetagUnpublished } = await import('./release/index.js');
134
+ await releaseRetagUnpublished(await findRepoRoot(), {
135
+ tags,
136
+ to: options.to,
137
+ push: options.push === true,
138
+ dispatch: options.dispatch === true,
139
+ remote: options.remote,
140
+ branch: options.branch,
141
+ dryRun: booleanOption(options.dryRun),
142
+ });
104
143
  },
105
144
  );
106
145
  release
107
146
  .command('trust-publisher')
108
- .description('Configure npm trusted publishing for public packages')
147
+ .description('Configure npm trusted publishing for owned release packages')
109
148
  .option('--dry-run [dryRun]', 'show npm trust changes without saving them')
110
149
  .option('--otp <otp>', 'npm one-time password for trust operations')
111
150
  .option('--skip-login', 'skip npm browser login before configuring trust')
@@ -0,0 +1,28 @@
1
+ import { describe, expect, it } from 'bun:test';
2
+ import { mergeDevenvEnv, parseNulEnv } from './devenv.js';
3
+
4
+ describe('devenv environment loader', () => {
5
+ it('parses NUL-separated env output without splitting values on newlines or equals signs', () => {
6
+ const bytes = new TextEncoder().encode('SIMPLE=value\0MULTILINE=line 1\nline 2\0TOKEN=a=b=c\0');
7
+
8
+ expect(parseNulEnv(bytes)).toEqual({
9
+ SIMPLE: 'value',
10
+ MULTILINE: 'line 1\nline 2',
11
+ TOKEN: 'a=b=c',
12
+ });
13
+ });
14
+
15
+ it('merges devenv output over the existing environment instead of replacing it', () => {
16
+ expect(
17
+ mergeDevenvEnv(
18
+ { GH_TOKEN: 'existing-token', NODE_AUTH_TOKEN: 'npm-token', PATH: '/usr/bin' },
19
+ { DEVENV_ROOT: '/repo/tooling/direnv', PATH: '/nix/bin' },
20
+ ),
21
+ ).toEqual({
22
+ DEVENV_ROOT: '/repo/tooling/direnv',
23
+ GH_TOKEN: 'existing-token',
24
+ NODE_AUTH_TOKEN: 'npm-token',
25
+ PATH: '/nix/bin',
26
+ });
27
+ });
28
+ });