@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,122 @@
1
+ import type { ReleasePackageInfo } from './core.js';
2
+
3
+ export interface NpmPublishAuthFailureOptions {
4
+ useBootstrapToken: boolean;
5
+ tokenPresent: boolean;
6
+ repository?: string;
7
+ }
8
+
9
+ export interface NpmPublishDiagnosticShell {
10
+ publish(): Promise<void>;
11
+ versionExists(): Promise<boolean>;
12
+ log(message: string): void;
13
+ error(message: string): void;
14
+ appendSummary(markdown: string): Promise<void>;
15
+ }
16
+
17
+ export async function publishWithAuthDiagnostics(
18
+ pkg: Pick<ReleasePackageInfo, 'name' | 'version'>,
19
+ shell: NpmPublishDiagnosticShell,
20
+ options: NpmPublishAuthFailureOptions,
21
+ ): Promise<void> {
22
+ try {
23
+ await shell.publish();
24
+ } catch (error) {
25
+ const packageVersion = `${pkg.name}@${pkg.version}`;
26
+ if (await shell.versionExists()) {
27
+ shell.log(`${packageVersion}: publish result already visible on npm; continuing.`);
28
+ return;
29
+ }
30
+ shell.error(npmPublishAuthFailureMessage(pkg, options));
31
+ await shell.appendSummary(npmPublishAuthFailureMarkdown(pkg, options));
32
+ throw new Error(
33
+ `${packageVersion}: npm publish authentication failed. Run smoo release trust-publisher after the package exists on npm; see the warning banner above for details.`,
34
+ { cause: error },
35
+ );
36
+ }
37
+ }
38
+
39
+ export function npmPublishAuthFailureMessage(
40
+ pkg: Pick<ReleasePackageInfo, 'name' | 'version'>,
41
+ options: NpmPublishAuthFailureOptions,
42
+ ): string {
43
+ const packageVersion = `${pkg.name}@${pkg.version}`;
44
+ const lines = [
45
+ `::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.`,
46
+ '',
47
+ `๐Ÿšจ npm publish authentication failed for ${packageVersion}`,
48
+ '',
49
+ ];
50
+ if (options.useBootstrapToken) {
51
+ lines.push(
52
+ 'smoo expected a temporary npm automation token because this package does not exist on npm yet.',
53
+ options.tokenPresent
54
+ ? '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.'
55
+ : 'NODE_AUTH_TOKEN/NPM_TOKEN is not set. Add a temporary NPM_TOKEN repository secret and rerun the Publish workflow.',
56
+ '',
57
+ 'After the first successful publish, run:',
58
+ ' smoo release trust-publisher',
59
+ '',
60
+ 'Then remove the temporary bootstrap token path for future releases.',
61
+ );
62
+ return lines.join('\n');
63
+ }
64
+
65
+ lines.push(
66
+ 'smoo expected npm trusted publishing/OIDC because this package already exists on npm.',
67
+ options.tokenPresent
68
+ ? 'NODE_AUTH_TOKEN/NPM_TOKEN is set but unused: smoo intentionally clears token auth for existing packages; npm must authenticate through trusted publishing instead.'
69
+ : 'NODE_AUTH_TOKEN/NPM_TOKEN is not set, which is expected for trusted publishing; npm did not authenticate the workflow as a trusted publisher.',
70
+ '',
71
+ 'Fix:',
72
+ '1. Run locally: smoo release trust-publisher',
73
+ '2. Ensure the npm trusted publisher uses:',
74
+ ` repository: ${trustedPublisherRepository(options)}`,
75
+ ' workflow: publish.yml',
76
+ '3. Rerun the Publish workflow.',
77
+ '',
78
+ 'For first-ever package publishes, add a temporary NPM_TOKEN repository secret, publish once, then run smoo release trust-publisher.',
79
+ );
80
+ return lines.join('\n');
81
+ }
82
+
83
+ export function npmPublishAuthFailureMarkdown(
84
+ pkg: Pick<ReleasePackageInfo, 'name' | 'version'>,
85
+ options: NpmPublishAuthFailureOptions,
86
+ ): string {
87
+ const packageVersion = `${pkg.name}@${pkg.version}`;
88
+ const lines = ['## ๐Ÿšจ npm Publish Authentication Failed', '', `Package: \`${packageVersion}\``, ''];
89
+ if (options.useBootstrapToken) {
90
+ lines.push(
91
+ 'smoo expected a temporary npm automation token because this package does not exist on npm yet.',
92
+ '',
93
+ options.tokenPresent
94
+ ? '`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.'
95
+ : '`NODE_AUTH_TOKEN`/`NPM_TOKEN` is not set. Add a temporary `NPM_TOKEN` repository secret and rerun the Publish workflow.',
96
+ '',
97
+ 'After the first successful publish, run `smoo release trust-publisher`, then remove the temporary bootstrap token path for future releases.',
98
+ );
99
+ return lines.join('\n');
100
+ }
101
+
102
+ lines.push(
103
+ 'smoo expected npm trusted publishing/OIDC because this package already exists on npm.',
104
+ '',
105
+ options.tokenPresent
106
+ ? '`NODE_AUTH_TOKEN`/`NPM_TOKEN` is set but unused: smoo intentionally clears token auth for existing packages; npm must authenticate through trusted publishing instead.'
107
+ : '`NODE_AUTH_TOKEN`/`NPM_TOKEN` is not set, which is expected for trusted publishing; npm did not authenticate the workflow as a trusted publisher.',
108
+ '',
109
+ 'Fix:',
110
+ '',
111
+ '1. Run locally: `smoo release trust-publisher`',
112
+ `2. Ensure the npm trusted publisher uses repository \`${trustedPublisherRepository(options)}\` and workflow \`publish.yml\``,
113
+ '3. Rerun the Publish workflow.',
114
+ '',
115
+ 'For first-ever package publishes, add a temporary `NPM_TOKEN` repository secret, publish once, then run `smoo release trust-publisher`.',
116
+ );
117
+ return lines.join('\n');
118
+ }
119
+
120
+ function trustedPublisherRepository(options: Pick<NpmPublishAuthFailureOptions, 'repository'>): string {
121
+ return options.repository ?? 'the current GitHub repository';
122
+ }
@@ -0,0 +1,186 @@
1
+ import type { ReleasePackageInfo, ReleaseTarget } from './core.js';
2
+ import { planPublishActions } from './publish-plan.js';
3
+
4
+ export type ReleaseVersionMode = 'new' | 'none';
5
+
6
+ export interface ReleaseSummary<Package extends ReleasePackageInfo = ReleasePackageInfo> {
7
+ sha: string;
8
+ dryRun: boolean;
9
+ packages: Package[];
10
+ pushed: boolean;
11
+ published: Package[];
12
+ alreadyPublished: Package[];
13
+ githubReleases: Package[];
14
+ rerunRequired: boolean;
15
+ noRelease: boolean;
16
+ }
17
+
18
+ export interface ReleaseCompletionShell<Package extends ReleasePackageInfo = ReleasePackageInfo> {
19
+ gitHead(): Promise<string>;
20
+ pushReleaseRefs(packages: Package[]): Promise<boolean>;
21
+ listNpmMissingPackages(packages: Package[]): Promise<Package[]>;
22
+ buildReleaseCandidate(packages: Package[]): Promise<void>;
23
+ publishPackage(pkg: Package, distTag: string, dryRun: boolean): Promise<void>;
24
+ listGithubMissingPackages(packages: Package[]): Promise<Package[]>;
25
+ createGithubRelease(pkg: Package, dryRun: boolean): Promise<void>;
26
+ }
27
+
28
+ export interface ReleaseRepairShell<Package extends ReleasePackageInfo = ReleasePackageInfo>
29
+ extends ReleaseCompletionShell<Package> {
30
+ checkout(ref: string): Promise<void>;
31
+ withDevenvEnv<T>(runWithEnv: () => Promise<T>): Promise<T>;
32
+ beforeRepairTarget?(target: ReleaseTarget<Package>): void;
33
+ afterRepairTarget?(target: ReleaseTarget<Package>): void;
34
+ }
35
+
36
+ export interface ReleaseVersionShell<Package extends ReleasePackageInfo = ReleasePackageInfo> {
37
+ releasePackagesAtHead(): Promise<Package[]>;
38
+ ensureLocalReleaseTags(packages: Package[]): Promise<void>;
39
+ gitHead(): Promise<string>;
40
+ runNxReleaseVersion(bump: string, dryRun: boolean): Promise<void>;
41
+ assertCleanGitTree(): Promise<void>;
42
+ }
43
+
44
+ export interface ReleaseVersionResult<Package extends ReleasePackageInfo = ReleasePackageInfo> {
45
+ mode: ReleaseVersionMode;
46
+ packages: Package[];
47
+ status: 'already-release-target' | 'dry-run' | 'new-release' | 'no-release-needed';
48
+ }
49
+
50
+ export async function runReleaseVersion<Package extends ReleasePackageInfo>(
51
+ shell: ReleaseVersionShell<Package>,
52
+ options: { bump: string; dryRun: boolean },
53
+ ): Promise<ReleaseVersionResult<Package>> {
54
+ if (!options.dryRun) {
55
+ const localRelease = await shell.releasePackagesAtHead();
56
+ if (localRelease.length > 0) {
57
+ await shell.ensureLocalReleaseTags(localRelease);
58
+ return { mode: 'none', packages: localRelease, status: 'already-release-target' };
59
+ }
60
+ }
61
+
62
+ const headBeforeVersioning = await shell.gitHead();
63
+ await shell.runNxReleaseVersion(options.bump, options.dryRun);
64
+ if (options.dryRun) {
65
+ return { mode: 'none', packages: [], status: 'dry-run' };
66
+ }
67
+
68
+ await shell.assertCleanGitTree();
69
+ const headAfterVersioning = await shell.gitHead();
70
+ const releasedPackages = await shell.releasePackagesAtHead();
71
+ await shell.ensureLocalReleaseTags(releasedPackages);
72
+ if (headAfterVersioning === headBeforeVersioning) {
73
+ if (releasedPackages.length > 0) {
74
+ return { mode: 'none', packages: releasedPackages, status: 'already-release-target' };
75
+ }
76
+ if (options.bump !== 'auto') {
77
+ throw new Error(`Nx did not create a release commit for forced --bump ${options.bump}.`);
78
+ }
79
+ return { mode: 'none', packages: [], status: 'no-release-needed' };
80
+ }
81
+ if (releasedPackages.length === 0) {
82
+ throw new Error('Nx created a release commit without current package release tags at HEAD.');
83
+ }
84
+ return { mode: 'new', packages: releasedPackages, status: 'new-release' };
85
+ }
86
+
87
+ export async function completeReleaseAtHead<Package extends ReleasePackageInfo>(
88
+ shell: ReleaseCompletionShell<Package>,
89
+ packages: Package[],
90
+ dryRun: boolean,
91
+ rerunRequired: boolean,
92
+ ): Promise<ReleaseSummary<Package>> {
93
+ const npmMissingPackages = await shell.listNpmMissingPackages(packages);
94
+ const githubMissingPackages = dryRun ? packages : await shell.listGithubMissingPackages(packages);
95
+ return completePlannedRelease(shell, {
96
+ sha: await shell.gitHead(),
97
+ dryRun,
98
+ packages,
99
+ pushed: dryRun ? false : await shell.pushReleaseRefs(packages),
100
+ npmMissingPackages,
101
+ githubMissingPackages,
102
+ rerunRequired,
103
+ });
104
+ }
105
+
106
+ export async function repairPendingTargets<Package extends ReleasePackageInfo>(
107
+ shell: ReleaseRepairShell<Package>,
108
+ targets: Array<ReleaseTarget<Package>>,
109
+ restoreRef: string,
110
+ dryRun: boolean,
111
+ ): Promise<Array<ReleaseSummary<Package>>> {
112
+ const summaries: Array<ReleaseSummary<Package>> = [];
113
+ try {
114
+ for (const target of targets) {
115
+ await shell.checkout(target.sha);
116
+ if (target.packages.length === 0) {
117
+ throw new Error(`Release commit ${target.sha} has no release packages after checkout.`);
118
+ }
119
+ shell.beforeRepairTarget?.(target);
120
+ try {
121
+ summaries.push(await shell.withDevenvEnv(() => completeRepairTargetAtHead(shell, target, dryRun)));
122
+ } finally {
123
+ shell.afterRepairTarget?.(target);
124
+ }
125
+ }
126
+ } finally {
127
+ await shell.checkout(restoreRef);
128
+ }
129
+ return summaries;
130
+ }
131
+
132
+ export async function completeRepairTargetAtHead<Package extends ReleasePackageInfo>(
133
+ shell: ReleaseCompletionShell<Package>,
134
+ target: ReleaseTarget<Package>,
135
+ dryRun: boolean,
136
+ ): Promise<ReleaseSummary<Package>> {
137
+ return completePlannedRelease(shell, {
138
+ sha: await shell.gitHead(),
139
+ dryRun,
140
+ packages: target.packages,
141
+ pushed: dryRun ? false : await shell.pushReleaseRefs(target.packages),
142
+ npmMissingPackages: target.npmPackages,
143
+ githubMissingPackages: target.githubPackages,
144
+ rerunRequired: false,
145
+ });
146
+ }
147
+
148
+ async function completePlannedRelease<Package extends ReleasePackageInfo>(
149
+ shell: ReleaseCompletionShell<Package>,
150
+ input: {
151
+ sha: string;
152
+ dryRun: boolean;
153
+ packages: Package[];
154
+ pushed: boolean;
155
+ npmMissingPackages: Package[];
156
+ githubMissingPackages: Package[];
157
+ rerunRequired: boolean;
158
+ },
159
+ ): Promise<ReleaseSummary<Package>> {
160
+ const plan = planPublishActions({
161
+ releasePackages: input.packages,
162
+ npmMissingPackages: input.npmMissingPackages,
163
+ githubMissingPackages: input.githubMissingPackages,
164
+ });
165
+ if (plan.buildProjects.length > 0) {
166
+ await shell.buildReleaseCandidate(plan.publishPackages.map((action) => action.pkg));
167
+ }
168
+ for (const { pkg, distTag } of plan.publishPackages) {
169
+ await shell.publishPackage(pkg, distTag, input.dryRun);
170
+ }
171
+ for (const pkg of plan.githubReleasePackages) {
172
+ await shell.createGithubRelease(pkg, input.dryRun);
173
+ }
174
+
175
+ return {
176
+ sha: input.sha,
177
+ dryRun: input.dryRun,
178
+ packages: input.packages,
179
+ pushed: input.pushed,
180
+ published: input.dryRun ? [] : plan.publishPackages.map((action) => action.pkg),
181
+ alreadyPublished: input.packages.filter((pkg) => !input.npmMissingPackages.includes(pkg)),
182
+ githubReleases: input.dryRun ? [] : plan.githubReleasePackages,
183
+ rerunRequired: input.rerunRequired,
184
+ noRelease: false,
185
+ };
186
+ }
@@ -0,0 +1,37 @@
1
+ import { npmDistTagForVersion, type ReleasePackageInfo } from './core.js';
2
+
3
+ export interface PublishPackageAction<Package extends ReleasePackageInfo = ReleasePackageInfo> {
4
+ pkg: Package;
5
+ distTag: string;
6
+ }
7
+
8
+ export interface PublishPlan<Package extends ReleasePackageInfo = ReleasePackageInfo> {
9
+ buildProjects: string[];
10
+ publishPackages: Array<PublishPackageAction<Package>>;
11
+ githubReleasePackages: Package[];
12
+ }
13
+
14
+ export interface PublishPlanInput<Package extends ReleasePackageInfo = ReleasePackageInfo> {
15
+ releasePackages: Package[];
16
+ npmMissingPackages: Package[];
17
+ githubMissingPackages: Package[];
18
+ }
19
+
20
+ export function planPublishActions<Package extends ReleasePackageInfo>(
21
+ input: PublishPlanInput<Package>,
22
+ ): PublishPlan<Package> {
23
+ const npmMissingNames = packageNameSet(input.npmMissingPackages);
24
+ const githubMissingNames = packageNameSet(input.githubMissingPackages);
25
+ const npmPackages = input.releasePackages.filter((pkg) => npmMissingNames.has(pkg.name));
26
+ const githubReleasePackages = input.releasePackages.filter((pkg) => githubMissingNames.has(pkg.name));
27
+
28
+ return {
29
+ buildProjects: npmPackages.map((pkg) => pkg.name),
30
+ publishPackages: npmPackages.map((pkg) => ({ pkg, distTag: npmDistTagForVersion(pkg.version) })),
31
+ githubReleasePackages,
32
+ };
33
+ }
34
+
35
+ function packageNameSet(packages: ReleasePackageInfo[]): Set<string> {
36
+ return new Set(packages.map((pkg) => pkg.name));
37
+ }
@@ -0,0 +1,122 @@
1
+ import { type ReleasePackageInfo, releasePackageForTag } from './core.js';
2
+
3
+ export interface RetagUnpublishedOptions {
4
+ tags: string[];
5
+ toRef: string;
6
+ push: boolean;
7
+ dispatch: boolean;
8
+ dryRun: boolean;
9
+ branch: string;
10
+ workflow: string;
11
+ }
12
+
13
+ export interface RetagUnpublishedTagUpdate<Package extends ReleasePackageInfo = ReleasePackageInfo> {
14
+ tag: string;
15
+ pkg: Package;
16
+ expectedRemoteObject: string | null;
17
+ }
18
+
19
+ export interface RetagUnpublishedShell<Package extends Omit<ReleasePackageInfo, 'version'> = ReleasePackageInfo> {
20
+ listReleasePackages(): Package[];
21
+ resolveRef(ref: string): Promise<string>;
22
+ resolveDispatchRef(branch: string): Promise<string | null>;
23
+ packageVersionAtRef(packagePath: string, ref: string): Promise<string | null>;
24
+ npmVersionExists(name: string, version: string): Promise<boolean>;
25
+ githubReleaseExists(tag: string): Promise<boolean>;
26
+ remoteTagObject(tag: string): Promise<string | null>;
27
+ createOrMoveTag(tag: string, ref: string): Promise<void>;
28
+ pushTags(updates: Array<RetagUnpublishedTagUpdate<Package & { version: string }>>): Promise<void>;
29
+ dispatchPublishWorkflow(workflow: string, branch: string): Promise<void>;
30
+ log(message: string): void;
31
+ }
32
+
33
+ export async function retagUnpublished<Package extends Omit<ReleasePackageInfo, 'version'>>(
34
+ shell: RetagUnpublishedShell<Package>,
35
+ options: RetagUnpublishedOptions,
36
+ ): Promise<Array<RetagUnpublishedTagUpdate<Package & { version: string }>>> {
37
+ if (options.tags.length === 0) {
38
+ throw new Error('retag-unpublished requires at least one release tag.');
39
+ }
40
+ assertUniqueTags(options.tags);
41
+
42
+ const targetSha = await shell.resolveRef(options.toRef);
43
+ if (options.dispatch) {
44
+ const dispatchSha = await shell.resolveDispatchRef(options.branch);
45
+ if (!dispatchSha) {
46
+ throw new Error(`Cannot dispatch ${options.workflow}: remote branch ${options.branch} was not found.`);
47
+ }
48
+ if (dispatchSha !== targetSha) {
49
+ throw new Error(
50
+ `Cannot dispatch ${options.workflow}: ${options.toRef} resolves to ${targetSha.slice(0, 12)}, but ${
51
+ options.branch
52
+ } resolves to ${dispatchSha.slice(0, 12)}. Push the branch first or choose a ref already at the remote branch HEAD.`,
53
+ );
54
+ }
55
+ }
56
+
57
+ const packages = shell.listReleasePackages();
58
+ const updates: Array<RetagUnpublishedTagUpdate<Package & { version: string }>> = [];
59
+ for (const tag of options.tags) {
60
+ const match = releasePackageForTag(packages, tag);
61
+ if (!match) {
62
+ throw new Error(`Release tag ${tag} does not match an owned release package.`);
63
+ }
64
+ const versionAtRef = await shell.packageVersionAtRef(match.pkg.path, options.toRef);
65
+ if (versionAtRef !== match.version) {
66
+ throw new Error(
67
+ `Release tag ${tag} cannot move to ${options.toRef}: ${match.pkg.path}/package.json has version ${
68
+ versionAtRef ?? 'missing'
69
+ }, expected ${match.version}.`,
70
+ );
71
+ }
72
+ if (await shell.npmVersionExists(match.pkg.name, match.version)) {
73
+ throw new Error(`Cannot retag ${tag}: ${match.pkg.name}@${match.version} already exists on npm.`);
74
+ }
75
+ if (await shell.githubReleaseExists(tag)) {
76
+ throw new Error(`Cannot retag ${tag}: GitHub Release ${tag} already exists.`);
77
+ }
78
+
79
+ updates.push({
80
+ tag,
81
+ pkg: { ...match.pkg, version: match.version },
82
+ expectedRemoteObject: options.push ? await shell.remoteTagObject(tag) : null,
83
+ });
84
+ }
85
+
86
+ for (const update of updates) {
87
+ if (options.dryRun) {
88
+ shell.log(`Would move ${update.tag} to ${options.toRef} (${targetSha.slice(0, 12)}).`);
89
+ } else {
90
+ await shell.createOrMoveTag(update.tag, options.toRef);
91
+ shell.log(`Moved ${update.tag} to ${options.toRef} (${targetSha.slice(0, 12)}).`);
92
+ }
93
+ }
94
+ if (options.push) {
95
+ if (options.dryRun) {
96
+ shell.log(`Would push ${updates.length} retagged release tag${updates.length === 1 ? '' : 's'}.`);
97
+ } else {
98
+ await shell.pushTags(updates);
99
+ shell.log(`Pushed ${updates.length} retagged release tag${updates.length === 1 ? '' : 's'}.`);
100
+ }
101
+ }
102
+ if (options.dispatch) {
103
+ if (options.dryRun) {
104
+ shell.log(`Would dispatch ${options.workflow} on ${options.branch} with bump=auto.`);
105
+ } else {
106
+ await shell.dispatchPublishWorkflow(options.workflow, options.branch);
107
+ shell.log(`Dispatched ${options.workflow} on ${options.branch} with bump=auto.`);
108
+ }
109
+ }
110
+
111
+ return updates;
112
+ }
113
+
114
+ function assertUniqueTags(tags: string[]): void {
115
+ const seen = new Set<string>();
116
+ for (const tag of tags) {
117
+ if (seen.has(tag)) {
118
+ throw new Error(`Release tag ${tag} was provided more than once.`);
119
+ }
120
+ seen.add(tag);
121
+ }
122
+ }
@@ -1,136 +0,0 @@
1
- name: Publish
2
-
3
- on:
4
- workflow_dispatch:
5
- inputs:
6
- bump:
7
- type: choice
8
- description:
9
- Use auto for conventional commits, or force a semver bump. Prerelease publishes to next; all others publish to
10
- latest.
11
- options: [auto, patch, minor, major, prerelease]
12
- default: auto
13
- dry_run:
14
- type: boolean
15
- description: Run release commands without writing versions, tags, publishes, or GitHub Releases.
16
- default: false
17
-
18
- permissions:
19
- contents: write
20
- id-token: write
21
-
22
- concurrency:
23
- group: release-${{ github.ref }}
24
- cancel-in-progress: false
25
-
26
- defaults:
27
- run:
28
- working-directory: tooling/direnv
29
-
30
- jobs:
31
- publish:
32
- runs-on: ubuntu-latest
33
- env:
34
- NIX_STORE_NAR: ${{ github.workspace }}/nix-store.nar
35
- GH_TOKEN: ${{ github.token }}
36
- steps:
37
- # --- Setup --------------------------------------------------------------
38
-
39
- # Step 1: GitHub adds "Set up job" automatically
40
- # Step 2
41
- - name: ๐Ÿ“ฅ Checkout
42
- uses: actions/checkout@v6.0.2
43
- with:
44
- filter: blob:none
45
- fetch-depth: 0
46
-
47
- # Step 3. Composite action internals do not affect top-level job step
48
- # anchors; update these comments if top-level steps move.
49
- - name: ๐Ÿงฑ Setup Nix/devenv
50
- id: setup
51
- uses: ./.github/actions/setup-devenv
52
-
53
- # --- Validation ---------------------------------------------------------
54
-
55
- # Release validation intentionally does not restore persisted Nx task
56
- # cache. Nx may reuse tasks produced earlier in this same job, but publish
57
- # never relies on task outputs restored from CI cache.
58
-
59
- # Step 4
60
- - name: โœ… Check managed monorepo files
61
- run: smoo monorepo check
62
-
63
- # Step 5
64
- - name: ๐Ÿ“ฆ Resolve release projects
65
- id: projects
66
- run: echo "projects=$(smoo monorepo list-public-projects)" >> "$GITHUB_OUTPUT"
67
-
68
- # Step 6
69
- - name: ๐Ÿ”จ Build
70
- run: smoo github-ci nx-run-many --targets build --projects "${{ steps.projects.outputs.projects }}"
71
-
72
- # Step 7
73
- - name: ๐Ÿ” Lint
74
- run: smoo github-ci nx-run-many --targets lint --projects "${{ steps.projects.outputs.projects }}"
75
-
76
- # Step 8
77
- - name: ๐Ÿงช Unit Tests
78
- run: smoo github-ci nx-run-many --targets test --projects "${{ steps.projects.outputs.projects }}"
79
-
80
- # Step 9
81
- - name: ๐Ÿ“Ž Upload trace DBs
82
- if: ${{ failure() }}
83
- uses: actions/upload-artifact@v4
84
- with:
85
- name: trace-results-${{ github.run_id }}
86
- path: packages/*/.trace-results.db
87
- if-no-files-found: ignore
88
- retention-days: 14
89
- include-hidden-files: true
90
-
91
- # Step 10
92
- - name: โœ… Validate monorepo config
93
- run: smoo monorepo validate
94
-
95
- # --- Release ------------------------------------------------------------
96
-
97
- # Step 11
98
- - name: ๐Ÿค– Configure release author
99
- run:
100
- git config user.name "github-actions[bot]" && git config user.email
101
- "41898282+github-actions[bot]@users.noreply.github.com"
102
-
103
- # Step 12
104
- - name: ๐Ÿท๏ธ Version, tag, and push packages
105
- run: smoo release version --bump "${{ inputs.bump }}" --dry-run "${{ inputs.dry_run }}"
106
-
107
- # Step 13
108
- - name: ๐Ÿ”– Collect release tags
109
- id: tags
110
- run: echo "tags=$(git tag --points-at HEAD | tr '\n' ' ')" >> "$GITHUB_OUTPUT"
111
-
112
- # Step 14
113
- - name: ๐Ÿ“ฆ Publish packages
114
- # smoo packs with Bun, then publishes tarballs with npm. npm uses OIDC
115
- # trusted publishing when configured; NODE_AUTH_TOKEN is only a temporary
116
- # bootstrap fallback before packages have trusted publishers.
117
- env:
118
- NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }}
119
- run: smoo release publish --bump "${{ inputs.bump }}" --dry-run "${{ inputs.dry_run }}"
120
-
121
- # Step 15
122
- - name: ๐Ÿš€ Create or update GitHub Releases
123
- if: ${{ inputs.dry_run != true }}
124
- run:
125
- smoo release github-release --tags "${{ steps.tags.outputs.tags }}" --bump "${{ inputs.bump }}" --dry-run "${{
126
- inputs.dry_run }}"
127
-
128
- # --- Cleanup ------------------------------------------------------------
129
-
130
- # Step 16
131
- - name: ๐Ÿงน Cleanup and cache Nix/devenv
132
- if: ${{ always() }}
133
- uses: ./.github/actions/save-nix-devenv
134
- with:
135
- nix-cache-hit: ${{ steps.setup.outputs.nix-cache-hit }}
136
- devenv-cache-hit: ${{ steps.setup.outputs.devenv-cache-hit }}