@smoothbricks/cli 0.1.1 → 0.3.0
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/README.md +161 -32
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +110 -10
- package/dist/devenv/index.d.ts +10 -0
- package/dist/devenv/index.d.ts.map +1 -0
- package/dist/devenv/index.js +27 -0
- package/dist/generate/index.d.ts +20 -0
- package/dist/generate/index.d.ts.map +1 -0
- package/dist/generate/index.js +47 -0
- package/dist/lib/cli-package.d.ts +4 -0
- package/dist/lib/cli-package.d.ts.map +1 -0
- package/dist/lib/cli-package.js +15 -0
- package/dist/lib/run.d.ts +6 -0
- package/dist/lib/run.d.ts.map +1 -1
- package/dist/lib/run.js +32 -0
- package/dist/lib/workspace.d.ts +25 -1
- package/dist/lib/workspace.d.ts.map +1 -1
- package/dist/lib/workspace.js +79 -17
- package/dist/monorepo/commit-msg.d.ts +8 -1
- package/dist/monorepo/commit-msg.d.ts.map +1 -1
- package/dist/monorepo/commit-msg.js +85 -7
- package/dist/monorepo/index.d.ts +19 -2
- package/dist/monorepo/index.d.ts.map +1 -1
- package/dist/monorepo/index.js +104 -16
- package/dist/monorepo/lockfile.d.ts.map +1 -1
- package/dist/monorepo/lockfile.js +9 -0
- package/dist/monorepo/managed-files.js +1 -1
- package/dist/monorepo/nx-sync.d.ts +2 -1
- package/dist/monorepo/nx-sync.d.ts.map +1 -1
- package/dist/monorepo/nx-sync.js +8 -2
- package/dist/monorepo/package-hygiene.d.ts +6 -2
- package/dist/monorepo/package-hygiene.d.ts.map +1 -1
- package/dist/monorepo/package-hygiene.js +12 -4
- package/dist/monorepo/package-policy.d.ts +13 -4
- package/dist/monorepo/package-policy.d.ts.map +1 -1
- package/dist/monorepo/package-policy.js +208 -113
- package/dist/monorepo/packed-package.d.ts +3 -0
- package/dist/monorepo/packed-package.d.ts.map +1 -1
- package/dist/monorepo/packed-package.js +68 -36
- package/dist/monorepo/packs/index.d.ts +20 -1
- package/dist/monorepo/packs/index.d.ts.map +1 -1
- package/dist/monorepo/packs/index.js +238 -19
- package/dist/monorepo/publish-workflow.d.ts +0 -4
- package/dist/monorepo/publish-workflow.d.ts.map +1 -1
- package/dist/monorepo/publish-workflow.js +7 -15
- package/dist/monorepo/tool-validation.d.ts +22 -0
- package/dist/monorepo/tool-validation.d.ts.map +1 -0
- package/dist/monorepo/tool-validation.js +399 -0
- package/dist/nx/index.d.ts +25 -0
- package/dist/nx/index.d.ts.map +1 -0
- package/dist/nx/index.js +112 -0
- package/dist/release/bootstrap-npm-packages.d.ts +19 -0
- package/dist/release/bootstrap-npm-packages.d.ts.map +1 -0
- package/dist/release/bootstrap-npm-packages.js +58 -0
- package/dist/release/candidates.d.ts +11 -0
- package/dist/release/candidates.d.ts.map +1 -0
- package/dist/release/candidates.js +181 -0
- package/dist/release/core.d.ts +4 -1
- package/dist/release/core.d.ts.map +1 -1
- package/dist/release/core.js +76 -9
- package/dist/release/github-release.d.ts +1 -0
- package/dist/release/github-release.d.ts.map +1 -1
- package/dist/release/github-release.js +3 -3
- package/dist/release/index.d.ts +32 -1
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +488 -64
- package/dist/release/npm-auth.d.ts +0 -1
- package/dist/release/npm-auth.d.ts.map +1 -1
- package/dist/release/npm-auth.js +4 -16
- package/dist/release/orchestration.d.ts +11 -2
- package/dist/release/orchestration.d.ts.map +1 -1
- package/dist/release/orchestration.js +41 -3
- package/dist/release/publish-plan.js +1 -1
- package/managed/raw/git-format-staged.yml +54 -0
- package/managed/raw/tooling/git-hooks/commit-msg.sh +1 -1
- package/managed/templates/github/actions/setup-devenv/action.yml +1 -7
- package/managed/templates/github/workflows/ci.yml +4 -2
- package/package.json +15 -8
- package/src/cli.ts +153 -22
- package/src/devenv/index.test.ts +49 -0
- package/src/devenv/index.ts +38 -0
- package/src/generate/index.ts +92 -0
- package/src/lib/cli-package.ts +18 -0
- package/src/lib/run.ts +44 -0
- package/src/lib/workspace.ts +112 -17
- package/src/monorepo/__tests__/publish-workflow.test.ts +16 -9
- package/src/monorepo/commit-msg.test.ts +45 -0
- package/src/monorepo/commit-msg.ts +107 -7
- package/src/monorepo/index.ts +142 -16
- package/src/monorepo/lockfile.ts +9 -0
- package/src/monorepo/managed-files.ts +1 -1
- package/src/monorepo/nx-sync.ts +9 -2
- package/src/monorepo/package-hygiene.test.ts +34 -0
- package/src/monorepo/package-hygiene.ts +26 -4
- package/src/monorepo/package-policy.test.ts +1130 -0
- package/src/monorepo/package-policy.ts +237 -114
- package/src/monorepo/packed-package.ts +89 -37
- package/src/monorepo/packs/index.test.ts +189 -0
- package/src/monorepo/packs/index.ts +311 -20
- package/src/monorepo/publish-workflow.ts +10 -19
- package/src/monorepo/setup-test-tracing.test.ts +120 -0
- package/src/monorepo/tool-validation.test.ts +261 -0
- package/src/monorepo/tool-validation.ts +462 -0
- package/src/nx/index.test.ts +73 -0
- package/src/nx/index.ts +139 -0
- package/src/release/__tests__/bootstrap-npm-packages.test.ts +127 -0
- package/src/release/__tests__/candidates.test.ts +262 -0
- package/src/release/__tests__/core-properties.test.ts +6 -6
- package/src/release/__tests__/core-scenarios.test.ts +27 -20
- package/src/release/__tests__/core.test.ts +52 -5
- package/src/release/__tests__/fixture-repo.test.ts +90 -31
- package/src/release/__tests__/github-release.test.ts +22 -24
- package/src/release/__tests__/helpers/fixture-repo.ts +1 -0
- package/src/release/__tests__/npm-auth.test.ts +12 -21
- package/src/release/__tests__/orchestration.test.ts +142 -12
- package/src/release/__tests__/publish-plan.test.ts +9 -3
- package/src/release/__tests__/retag-unpublished.test.ts +10 -10
- package/src/release/__tests__/trust-publisher.test.ts +165 -0
- package/src/release/bootstrap-npm-packages.ts +88 -0
- package/src/release/candidates.ts +225 -0
- package/src/release/core.ts +96 -10
- package/src/release/github-release.ts +3 -3
- package/src/release/index.ts +658 -74
- package/src/release/npm-auth.ts +4 -33
- package/src/release/orchestration.ts +60 -5
- package/src/release/publish-plan.ts +1 -1
- package/dist/nx-version-actions.cjs +0 -25
- package/dist/nx-version-actions.d.cts +0 -6
- package/dist/nx-version-actions.d.cts.map +0 -1
- package/managed/raw/.git-format-staged.yml +0 -47
- package/src/nx-version-actions.cts +0 -36
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { describe, expect, it } from 'bun:test';
|
|
2
|
-
import type
|
|
2
|
+
import { type ReleasePackageInfo, type ReleaseTarget, releaseTag } from '../core.js';
|
|
3
3
|
import {
|
|
4
|
+
bumpStableReleaseToNext,
|
|
4
5
|
completeReleaseAtHead,
|
|
5
6
|
type ReleaseRepairShell,
|
|
6
7
|
type ReleaseVersionShell,
|
|
@@ -8,9 +9,15 @@ import {
|
|
|
8
9
|
runReleaseVersion,
|
|
9
10
|
} from '../orchestration.js';
|
|
10
11
|
|
|
11
|
-
const stable: ReleasePackageInfo = {
|
|
12
|
+
const stable: ReleasePackageInfo = {
|
|
13
|
+
name: '@scope/stable',
|
|
14
|
+
projectName: 'stable',
|
|
15
|
+
path: 'packages/stable',
|
|
16
|
+
version: '1.0.0',
|
|
17
|
+
};
|
|
12
18
|
const prerelease: ReleasePackageInfo = {
|
|
13
19
|
name: '@scope/prerelease',
|
|
20
|
+
projectName: 'prerelease',
|
|
14
21
|
path: 'packages/prerelease',
|
|
15
22
|
version: '2.0.0-beta.1',
|
|
16
23
|
};
|
|
@@ -27,7 +34,7 @@ describe('release orchestration', () => {
|
|
|
27
34
|
expect(shell.builds).toEqual([['@scope/stable']]);
|
|
28
35
|
expect(shell.publishes).toEqual([{ name: '@scope/stable', distTag: 'latest', dryRun: false }]);
|
|
29
36
|
expect(shell.githubCreates).toEqual([{ name: '@scope/prerelease', dryRun: false }]);
|
|
30
|
-
expect(shell.pushes).toEqual([['@
|
|
37
|
+
expect(shell.pushes).toEqual([['stable@1.0.0', 'prerelease@2.0.0-beta.1']]);
|
|
31
38
|
expect(summaries).toHaveLength(1);
|
|
32
39
|
expect(summaries[0]?.published.map((pkg) => pkg.name)).toEqual(['@scope/stable']);
|
|
33
40
|
expect(summaries[0]?.githubReleases.map((pkg) => pkg.name)).toEqual(['@scope/prerelease']);
|
|
@@ -44,6 +51,33 @@ describe('release orchestration', () => {
|
|
|
44
51
|
expect(shell.githubCreates).toEqual([{ name: '@scope/prerelease', dryRun: false }]);
|
|
45
52
|
});
|
|
46
53
|
|
|
54
|
+
it('dry-run repair reports planned targets without checkout, devenv, build, publish, or GitHub calls', async () => {
|
|
55
|
+
const target = releaseTarget('github-only', [prerelease], [], [prerelease]);
|
|
56
|
+
const shell = new RecordingRepairShell();
|
|
57
|
+
|
|
58
|
+
const summaries = await repairPendingTargets(shell, [target], 'restore-ref', true);
|
|
59
|
+
|
|
60
|
+
expect(shell.checkouts).toEqual([]);
|
|
61
|
+
expect(shell.devenvLoads).toBe(0);
|
|
62
|
+
expect(shell.builds).toEqual([]);
|
|
63
|
+
expect(shell.publishes).toEqual([]);
|
|
64
|
+
expect(shell.githubCreates).toEqual([]);
|
|
65
|
+
expect(summaries).toEqual([
|
|
66
|
+
{
|
|
67
|
+
sha: 'github-only',
|
|
68
|
+
dryRun: true,
|
|
69
|
+
packages: [prerelease],
|
|
70
|
+
pushed: false,
|
|
71
|
+
published: [],
|
|
72
|
+
alreadyPublished: [prerelease],
|
|
73
|
+
githubReleases: [],
|
|
74
|
+
githubReleaseLinks: [],
|
|
75
|
+
rerunRequired: false,
|
|
76
|
+
noRelease: false,
|
|
77
|
+
},
|
|
78
|
+
]);
|
|
79
|
+
});
|
|
80
|
+
|
|
47
81
|
it('publishes a partial HEAD release by building npm-missing packages and creating missing GitHub Releases', async () => {
|
|
48
82
|
const shell = new RecordingRepairShell({ npmMissing: ['@scope/stable'], githubMissing: ['@scope/prerelease'] });
|
|
49
83
|
|
|
@@ -54,9 +88,13 @@ describe('release orchestration', () => {
|
|
|
54
88
|
expect(shell.builds).toEqual([['@scope/stable']]);
|
|
55
89
|
expect(shell.publishes).toEqual([{ name: '@scope/stable', distTag: 'latest', dryRun: false }]);
|
|
56
90
|
expect(shell.githubCreates).toEqual([{ name: '@scope/prerelease', dryRun: false }]);
|
|
91
|
+
expect(shell.pushes).toEqual([['stable@1.0.0', 'prerelease@2.0.0-beta.1']]);
|
|
57
92
|
expect(summary.published.map((pkg) => pkg.name)).toEqual(['@scope/stable']);
|
|
58
93
|
expect(summary.alreadyPublished.map((pkg) => pkg.name)).toEqual(['@scope/prerelease']);
|
|
59
94
|
expect(summary.githubReleases.map((pkg) => pkg.name)).toEqual(['@scope/prerelease']);
|
|
95
|
+
expect(summary.githubReleaseLinks).toEqual([
|
|
96
|
+
{ pkg: prerelease, url: 'https://github.test/prerelease@2.0.0-beta.1' },
|
|
97
|
+
]);
|
|
60
98
|
expect(summary.rerunRequired).toBe(true);
|
|
61
99
|
});
|
|
62
100
|
|
|
@@ -71,6 +109,7 @@ describe('release orchestration', () => {
|
|
|
71
109
|
expect(summary.published).toEqual([]);
|
|
72
110
|
expect(summary.alreadyPublished.map((pkg) => pkg.name)).toEqual(['@scope/stable', '@scope/prerelease']);
|
|
73
111
|
expect(summary.githubReleases).toEqual([]);
|
|
112
|
+
expect(summary.githubReleaseLinks).toEqual([]);
|
|
74
113
|
});
|
|
75
114
|
|
|
76
115
|
it('does not run Nx versioning when HEAD is already a release target', async () => {
|
|
@@ -84,15 +123,79 @@ describe('release orchestration', () => {
|
|
|
84
123
|
});
|
|
85
124
|
|
|
86
125
|
it('runs forced Nx versioning for an untagged HEAD and reports a new release commit', async () => {
|
|
87
|
-
const shell = new RecordingVersionShell({
|
|
126
|
+
const shell = new RecordingVersionShell({
|
|
127
|
+
releasePackagesAtHead: [[], [stable]],
|
|
128
|
+
releaseVersionPackages: [stable, prerelease],
|
|
129
|
+
heads: ['before', 'after'],
|
|
130
|
+
});
|
|
88
131
|
|
|
89
132
|
const result = await runReleaseVersion(shell, { bump: 'patch', dryRun: false });
|
|
90
133
|
|
|
91
134
|
expect(result).toEqual({ mode: 'new', packages: [stable], status: 'new-release' });
|
|
92
|
-
expect(shell.nxRuns).toEqual([{ bump: 'patch', dryRun: false }]);
|
|
135
|
+
expect(shell.nxRuns).toEqual([{ packages: ['@scope/stable', '@scope/prerelease'], bump: 'patch', dryRun: false }]);
|
|
93
136
|
expect(shell.cleanChecks).toBe(1);
|
|
94
137
|
expect(shell.ensureCalls).toEqual([['@scope/stable']]);
|
|
95
138
|
});
|
|
139
|
+
|
|
140
|
+
it('skips auto Nx versioning when no package-local candidates exist', async () => {
|
|
141
|
+
const shell = new RecordingVersionShell({ releasePackagesAtHead: [[]], releaseVersionPackages: [] });
|
|
142
|
+
|
|
143
|
+
const result = await runReleaseVersion(shell, { bump: 'auto', dryRun: false });
|
|
144
|
+
|
|
145
|
+
expect(result).toEqual({ mode: 'none', packages: [], status: 'no-release-needed' });
|
|
146
|
+
expect(shell.nxRuns).toEqual([]);
|
|
147
|
+
expect(shell.cleanChecks).toBe(0);
|
|
148
|
+
expect(shell.ensureCalls).toEqual([]);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('runs auto Nx versioning only for selected package-local candidates', async () => {
|
|
152
|
+
const shell = new RecordingVersionShell({
|
|
153
|
+
releasePackagesAtHead: [[], [stable]],
|
|
154
|
+
releaseVersionPackages: [stable],
|
|
155
|
+
heads: ['before', 'after'],
|
|
156
|
+
});
|
|
157
|
+
|
|
158
|
+
const result = await runReleaseVersion(shell, { bump: 'auto', dryRun: false });
|
|
159
|
+
|
|
160
|
+
expect(result).toEqual({ mode: 'new', packages: [stable], status: 'new-release' });
|
|
161
|
+
expect(shell.nxRuns).toEqual([{ packages: ['@scope/stable'], bump: 'auto', dryRun: false }]);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it('reports dry-run version packages resolved by Nx without creating a release mode', async () => {
|
|
165
|
+
const preview = { ...stable, version: '1.1.0' };
|
|
166
|
+
const shell = new RecordingVersionShell({ releasePackagesAtHead: [[]], releaseVersionPackages: [stable], preview });
|
|
167
|
+
|
|
168
|
+
const result = await runReleaseVersion(shell, { bump: 'auto', dryRun: true });
|
|
169
|
+
|
|
170
|
+
expect(result).toEqual({ mode: 'none', packages: [preview], status: 'dry-run' });
|
|
171
|
+
expect(shell.nxRuns).toEqual([{ packages: ['@scope/stable'], bump: 'auto', dryRun: true }]);
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
it('bumps stable release packages to next after publish completion', async () => {
|
|
175
|
+
const shell = new RecordingNextShell();
|
|
176
|
+
|
|
177
|
+
const bumped = await bumpStableReleaseToNext(shell, [stable, prerelease], false, false);
|
|
178
|
+
|
|
179
|
+
expect(bumped.map((pkg) => pkg.name)).toEqual(['@scope/stable']);
|
|
180
|
+
expect(shell.bumped).toEqual([['@scope/stable']]);
|
|
181
|
+
});
|
|
182
|
+
|
|
183
|
+
it('does not bump prerelease-only or dry-run publishes to next', async () => {
|
|
184
|
+
const shell = new RecordingNextShell();
|
|
185
|
+
|
|
186
|
+
await expect(bumpStableReleaseToNext(shell, [prerelease], false, false)).resolves.toEqual([]);
|
|
187
|
+
await expect(bumpStableReleaseToNext(shell, [stable], true, false)).resolves.toEqual([]);
|
|
188
|
+
|
|
189
|
+
expect(shell.bumped).toEqual([]);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('does not bump stable packages to next when newer branch commits remain', async () => {
|
|
193
|
+
const shell = new RecordingNextShell();
|
|
194
|
+
|
|
195
|
+
await expect(bumpStableReleaseToNext(shell, [stable], false, true)).resolves.toEqual([]);
|
|
196
|
+
|
|
197
|
+
expect(shell.bumped).toEqual([]);
|
|
198
|
+
});
|
|
96
199
|
});
|
|
97
200
|
|
|
98
201
|
function releaseTarget(
|
|
@@ -127,7 +230,7 @@ class RecordingRepairShell implements ReleaseRepairShell<ReleasePackageInfo> {
|
|
|
127
230
|
}
|
|
128
231
|
|
|
129
232
|
async pushReleaseRefs(packages: ReleasePackageInfo[]): Promise<boolean> {
|
|
130
|
-
this.pushes.push(
|
|
233
|
+
this.pushes.push(packages.map((pkg) => releaseTag(pkg)));
|
|
131
234
|
return true;
|
|
132
235
|
}
|
|
133
236
|
|
|
@@ -149,8 +252,9 @@ class RecordingRepairShell implements ReleaseRepairShell<ReleasePackageInfo> {
|
|
|
149
252
|
return packages.filter((pkg) => this.githubMissing.has(pkg.name));
|
|
150
253
|
}
|
|
151
254
|
|
|
152
|
-
async createGithubRelease(pkg: ReleasePackageInfo, dryRun: boolean): Promise<
|
|
255
|
+
async createGithubRelease(pkg: ReleasePackageInfo, dryRun: boolean): Promise<string | null> {
|
|
153
256
|
this.githubCreates.push({ name: pkg.name, dryRun });
|
|
257
|
+
return dryRun ? null : `https://github.test/${releaseTag(pkg)}`;
|
|
154
258
|
}
|
|
155
259
|
|
|
156
260
|
async checkout(ref: string): Promise<void> {
|
|
@@ -166,20 +270,33 @@ class RecordingRepairShell implements ReleaseRepairShell<ReleasePackageInfo> {
|
|
|
166
270
|
|
|
167
271
|
class RecordingVersionShell implements ReleaseVersionShell<ReleasePackageInfo> {
|
|
168
272
|
readonly ensureCalls: string[][] = [];
|
|
169
|
-
readonly nxRuns: Array<{ bump: string; dryRun: boolean }> = [];
|
|
273
|
+
readonly nxRuns: Array<{ packages: string[]; bump: string; dryRun: boolean }> = [];
|
|
170
274
|
cleanChecks = 0;
|
|
171
275
|
private readonly releaseBatches: ReleasePackageInfo[][];
|
|
276
|
+
private readonly versionPackages: ReleasePackageInfo[];
|
|
172
277
|
private readonly heads: string[];
|
|
173
|
-
|
|
174
|
-
|
|
278
|
+
private readonly preview: ReleasePackageInfo[];
|
|
279
|
+
|
|
280
|
+
constructor(options: {
|
|
281
|
+
releasePackagesAtHead: ReleasePackageInfo[][];
|
|
282
|
+
releaseVersionPackages?: ReleasePackageInfo[];
|
|
283
|
+
heads?: string[];
|
|
284
|
+
preview?: ReleasePackageInfo | ReleasePackageInfo[];
|
|
285
|
+
}) {
|
|
175
286
|
this.releaseBatches = [...options.releasePackagesAtHead];
|
|
287
|
+
this.versionPackages = options.releaseVersionPackages ?? [stable, prerelease];
|
|
176
288
|
this.heads = [...(options.heads ?? [])];
|
|
289
|
+
this.preview = Array.isArray(options.preview) ? options.preview : options.preview ? [options.preview] : [];
|
|
177
290
|
}
|
|
178
291
|
|
|
179
292
|
async releasePackagesAtHead(): Promise<ReleasePackageInfo[]> {
|
|
180
293
|
return this.releaseBatches.shift() ?? [];
|
|
181
294
|
}
|
|
182
295
|
|
|
296
|
+
async releaseVersionPackages(): Promise<ReleasePackageInfo[]> {
|
|
297
|
+
return this.versionPackages;
|
|
298
|
+
}
|
|
299
|
+
|
|
183
300
|
async ensureLocalReleaseTags(packages: ReleasePackageInfo[]): Promise<void> {
|
|
184
301
|
this.ensureCalls.push(packageNames(packages));
|
|
185
302
|
}
|
|
@@ -188,8 +305,13 @@ class RecordingVersionShell implements ReleaseVersionShell<ReleasePackageInfo> {
|
|
|
188
305
|
return this.heads.shift() ?? 'head';
|
|
189
306
|
}
|
|
190
307
|
|
|
191
|
-
async runNxReleaseVersion(
|
|
192
|
-
|
|
308
|
+
async runNxReleaseVersion(
|
|
309
|
+
packages: ReleasePackageInfo[],
|
|
310
|
+
bump: string,
|
|
311
|
+
dryRun: boolean,
|
|
312
|
+
): Promise<ReleasePackageInfo[]> {
|
|
313
|
+
this.nxRuns.push({ packages: packageNames(packages), bump, dryRun });
|
|
314
|
+
return this.preview;
|
|
193
315
|
}
|
|
194
316
|
|
|
195
317
|
async assertCleanGitTree(): Promise<void> {
|
|
@@ -197,6 +319,14 @@ class RecordingVersionShell implements ReleaseVersionShell<ReleasePackageInfo> {
|
|
|
197
319
|
}
|
|
198
320
|
}
|
|
199
321
|
|
|
322
|
+
class RecordingNextShell {
|
|
323
|
+
readonly bumped: string[][] = [];
|
|
324
|
+
|
|
325
|
+
async bumpStablePackagesToNext(packages: ReleasePackageInfo[]): Promise<void> {
|
|
326
|
+
this.bumped.push(packageNames(packages));
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
200
330
|
function packageNames(packages: ReleasePackageInfo[]): string[] {
|
|
201
331
|
return packages.map((pkg) => pkg.name);
|
|
202
332
|
}
|
|
@@ -2,9 +2,15 @@ import { describe, expect, it } from 'bun:test';
|
|
|
2
2
|
import type { ReleasePackageInfo } from '../core.js';
|
|
3
3
|
import { planPublishActions } from '../publish-plan.js';
|
|
4
4
|
|
|
5
|
-
const stable: ReleasePackageInfo = {
|
|
5
|
+
const stable: ReleasePackageInfo = {
|
|
6
|
+
name: '@scope/stable',
|
|
7
|
+
projectName: 'stable',
|
|
8
|
+
path: 'packages/stable',
|
|
9
|
+
version: '1.0.0',
|
|
10
|
+
};
|
|
6
11
|
const prerelease: ReleasePackageInfo = {
|
|
7
12
|
name: '@scope/prerelease',
|
|
13
|
+
projectName: 'prerelease',
|
|
8
14
|
path: 'packages/prerelease',
|
|
9
15
|
version: '2.0.0-beta.1',
|
|
10
16
|
};
|
|
@@ -27,7 +33,7 @@ describe('publish plan', () => {
|
|
|
27
33
|
githubMissingPackages: [],
|
|
28
34
|
});
|
|
29
35
|
|
|
30
|
-
expect(plan.buildProjects).toEqual(['
|
|
36
|
+
expect(plan.buildProjects).toEqual(['stable']);
|
|
31
37
|
expect(plan.publishPackages).toEqual([{ pkg: stable, distTag: 'latest' }]);
|
|
32
38
|
expect(plan.githubReleasePackages).toEqual([]);
|
|
33
39
|
});
|
|
@@ -51,7 +57,7 @@ describe('publish plan', () => {
|
|
|
51
57
|
githubMissingPackages: [],
|
|
52
58
|
});
|
|
53
59
|
|
|
54
|
-
expect(plan.buildProjects).toEqual(['
|
|
60
|
+
expect(plan.buildProjects).toEqual(['stable', 'prerelease']);
|
|
55
61
|
expect(plan.publishPackages).toEqual([
|
|
56
62
|
{ pkg: stable, distTag: 'latest' },
|
|
57
63
|
{ pkg: prerelease, distTag: 'next' },
|
|
@@ -7,31 +7,31 @@ import {
|
|
|
7
7
|
retagUnpublished,
|
|
8
8
|
} from '../retag-unpublished.js';
|
|
9
9
|
|
|
10
|
-
const pkg: ReleasePackageInfo = { name: '@scope/pkg', path: 'packages/pkg', version: '1.2.3' };
|
|
10
|
+
const pkg: ReleasePackageInfo = { name: '@scope/pkg', projectName: 'pkg', path: 'packages/pkg', version: '1.2.3' };
|
|
11
11
|
|
|
12
12
|
describe('retag unpublished releases', () => {
|
|
13
13
|
it('moves unpublished owned tags, pushes with a remote lease, and dispatches publish auto', async () => {
|
|
14
14
|
const shell = new RecordingRetagShell({
|
|
15
15
|
versionAtRef: '1.2.3',
|
|
16
|
-
remoteTagObjects: new Map([['
|
|
16
|
+
remoteTagObjects: new Map([['pkg@1.2.3', 'old-tag-object']]),
|
|
17
17
|
dispatchSha: 'target-sha',
|
|
18
18
|
});
|
|
19
19
|
|
|
20
20
|
const updates = await retagUnpublished(shell, retagOptions({ push: true, dispatch: true }));
|
|
21
21
|
|
|
22
|
-
expect(updates).toEqual([{ tag: '
|
|
23
|
-
expect(shell.movedTags).toEqual([{ tag: '
|
|
24
|
-
expect(shell.pushed).toEqual([[{ tag: '
|
|
22
|
+
expect(updates).toEqual([{ tag: 'pkg@1.2.3', pkg, expectedRemoteObject: 'old-tag-object' }]);
|
|
23
|
+
expect(shell.movedTags).toEqual([{ tag: 'pkg@1.2.3', ref: 'HEAD' }]);
|
|
24
|
+
expect(shell.pushed).toEqual([[{ tag: 'pkg@1.2.3', expectedRemoteObject: 'old-tag-object' }]]);
|
|
25
25
|
expect(shell.dispatched).toEqual([{ workflow: 'publish.yml', branch: 'main' }]);
|
|
26
26
|
expect(shell.dispatchRefLookups).toEqual(['main']);
|
|
27
|
-
expect(shell.remoteTagLookups).toEqual(['
|
|
27
|
+
expect(shell.remoteTagLookups).toEqual(['pkg@1.2.3']);
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
it('rejects tags whose package version already exists on npm', async () => {
|
|
31
31
|
const shell = new RecordingRetagShell({ versionAtRef: '1.2.3', npmPublished: true });
|
|
32
32
|
|
|
33
33
|
await expect(retagUnpublished(shell, retagOptions())).rejects.toThrow(
|
|
34
|
-
'Cannot retag
|
|
34
|
+
'Cannot retag pkg@1.2.3: @scope/pkg@1.2.3 already exists on npm.',
|
|
35
35
|
);
|
|
36
36
|
|
|
37
37
|
expect(shell.movedTags).toEqual([]);
|
|
@@ -42,7 +42,7 @@ describe('retag unpublished releases', () => {
|
|
|
42
42
|
const shell = new RecordingRetagShell({ versionAtRef: '1.2.4' });
|
|
43
43
|
|
|
44
44
|
await expect(retagUnpublished(shell, retagOptions())).rejects.toThrow(
|
|
45
|
-
'Release tag
|
|
45
|
+
'Release tag pkg@1.2.3 cannot move to HEAD: packages/pkg/package.json has version 1.2.4, expected 1.2.3.',
|
|
46
46
|
);
|
|
47
47
|
});
|
|
48
48
|
|
|
@@ -66,7 +66,7 @@ describe('retag unpublished releases', () => {
|
|
|
66
66
|
expect(shell.movedTags).toEqual([]);
|
|
67
67
|
expect(shell.pushed).toEqual([]);
|
|
68
68
|
expect(shell.dispatched).toEqual([]);
|
|
69
|
-
expect(shell.logs).toContain('Would move
|
|
69
|
+
expect(shell.logs).toContain('Would move pkg@1.2.3 to HEAD (target-sha).');
|
|
70
70
|
expect(shell.logs).toContain('Would push 1 retagged release tag.');
|
|
71
71
|
expect(shell.logs).toContain('Would dispatch publish.yml on main with bump=auto.');
|
|
72
72
|
});
|
|
@@ -74,7 +74,7 @@ describe('retag unpublished releases', () => {
|
|
|
74
74
|
|
|
75
75
|
function retagOptions(overrides: Partial<RetagUnpublishedOptions> = {}): RetagUnpublishedOptions {
|
|
76
76
|
return {
|
|
77
|
-
tags: ['
|
|
77
|
+
tags: ['pkg@1.2.3'],
|
|
78
78
|
toRef: 'HEAD',
|
|
79
79
|
push: false,
|
|
80
80
|
dispatch: false,
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { describe, expect, it } from 'bun:test';
|
|
2
|
+
import type { BootstrapNpmPackagesOptions } from '../bootstrap-npm-packages.js';
|
|
3
|
+
import type { ReleasePackageInfo } from '../core.js';
|
|
4
|
+
import {
|
|
5
|
+
configureTrustedPublishers,
|
|
6
|
+
parseTrustedPublishers,
|
|
7
|
+
type TrustedPublisher,
|
|
8
|
+
type TrustPublisherShell,
|
|
9
|
+
} from '../index.js';
|
|
10
|
+
|
|
11
|
+
const stable: ReleasePackageInfo = {
|
|
12
|
+
name: '@scope/stable',
|
|
13
|
+
projectName: 'stable',
|
|
14
|
+
path: 'packages/stable',
|
|
15
|
+
version: '1.2.3',
|
|
16
|
+
};
|
|
17
|
+
const missing: ReleasePackageInfo = {
|
|
18
|
+
name: '@scope/missing',
|
|
19
|
+
projectName: 'missing',
|
|
20
|
+
path: 'packages/missing',
|
|
21
|
+
version: '2.0.0',
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
describe('trusted publisher setup', () => {
|
|
25
|
+
it('bootstraps missing npm packages before configuring trust', async () => {
|
|
26
|
+
const shell = new RecordingTrustPublisherShell({ packages: [stable, missing], existing: [stable.name] });
|
|
27
|
+
|
|
28
|
+
await configureTrustedPublishers(shell, { bootstrap: true, skipLogin: false });
|
|
29
|
+
|
|
30
|
+
expect(shell.events).toEqual([
|
|
31
|
+
'bootstrap:false:false',
|
|
32
|
+
`exists:${stable.name}`,
|
|
33
|
+
`exists:${missing.name}`,
|
|
34
|
+
`list:${stable.name}`,
|
|
35
|
+
`trust:${stable.name}:false`,
|
|
36
|
+
`list:${missing.name}`,
|
|
37
|
+
`trust:${missing.name}:false`,
|
|
38
|
+
]);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('directs missing packages to trust-publisher --bootstrap', async () => {
|
|
42
|
+
const shell = new RecordingTrustPublisherShell({ packages: [missing], existing: [] });
|
|
43
|
+
|
|
44
|
+
await expect(configureTrustedPublishers(shell, {})).rejects.toThrow(
|
|
45
|
+
'Run smoo release trust-publisher --bootstrap locally',
|
|
46
|
+
);
|
|
47
|
+
expect(shell.events).toEqual([`exists:${missing.name}`]);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('does not pre-login before trust operations', async () => {
|
|
51
|
+
const shell = new RecordingTrustPublisherShell({ packages: [stable], existing: [stable.name] });
|
|
52
|
+
|
|
53
|
+
await configureTrustedPublishers(shell, { bootstrap: true });
|
|
54
|
+
|
|
55
|
+
expect(shell.events).toEqual([
|
|
56
|
+
'bootstrap:false:false',
|
|
57
|
+
`exists:${stable.name}`,
|
|
58
|
+
`list:${stable.name}`,
|
|
59
|
+
`trust:${stable.name}:false`,
|
|
60
|
+
]);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('skips packages with matching trusted publishers', async () => {
|
|
64
|
+
const shell = new RecordingTrustPublisherShell({
|
|
65
|
+
packages: [stable],
|
|
66
|
+
existing: [stable.name],
|
|
67
|
+
trustedPublishers: {
|
|
68
|
+
[stable.name]: [{ id: 'trusted-1', type: 'github', file: 'publish.yml', repository: 'scope/repo' }],
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
await configureTrustedPublishers(shell, { skipLogin: true });
|
|
73
|
+
|
|
74
|
+
expect(shell.events).toEqual([`exists:${stable.name}`, `list:${stable.name}`]);
|
|
75
|
+
expect(shell.logs).toContain(`${stable.name}: npm trusted publisher is already configured; skipping.`);
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
it('rejects mismatched trusted publishers', async () => {
|
|
79
|
+
const shell = new RecordingTrustPublisherShell({
|
|
80
|
+
packages: [stable],
|
|
81
|
+
existing: [stable.name],
|
|
82
|
+
trustedPublishers: {
|
|
83
|
+
[stable.name]: [{ id: 'trusted-1', type: 'github', file: 'other.yml', repository: 'scope/repo' }],
|
|
84
|
+
},
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await expect(configureTrustedPublishers(shell, { skipLogin: true })).rejects.toThrow(
|
|
88
|
+
'npm trusted publisher exists but does not match',
|
|
89
|
+
);
|
|
90
|
+
expect(shell.events).toEqual([`exists:${stable.name}`, `list:${stable.name}`]);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
it('configures only selected packages', async () => {
|
|
94
|
+
const shell = new RecordingTrustPublisherShell({
|
|
95
|
+
packages: [stable, missing],
|
|
96
|
+
existing: [stable.name, missing.name],
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
await configureTrustedPublishers(shell, { skipLogin: true, packages: [missing.name] });
|
|
100
|
+
|
|
101
|
+
expect(shell.events).toEqual([`exists:${missing.name}`, `list:${missing.name}`, `trust:${missing.name}:false`]);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it('parses empty npm trust list output as no trusted publishers', () => {
|
|
105
|
+
expect(parseTrustedPublishers('', stable.name)).toEqual([]);
|
|
106
|
+
expect(parseTrustedPublishers('null', stable.name)).toEqual([]);
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
class RecordingTrustPublisherShell implements TrustPublisherShell<ReleasePackageInfo> {
|
|
111
|
+
readonly repository = 'scope/repo';
|
|
112
|
+
readonly workflow = 'publish.yml';
|
|
113
|
+
readonly events: string[] = [];
|
|
114
|
+
readonly logs: string[] = [];
|
|
115
|
+
readonly errors: string[] = [];
|
|
116
|
+
private readonly packages: ReleasePackageInfo[];
|
|
117
|
+
private readonly existing: Set<string>;
|
|
118
|
+
private readonly trustedPublisherByPackage: Record<string, TrustedPublisher[]>;
|
|
119
|
+
|
|
120
|
+
constructor(options: {
|
|
121
|
+
packages: ReleasePackageInfo[];
|
|
122
|
+
existing: string[];
|
|
123
|
+
trustedPublishers?: Record<string, TrustedPublisher[]>;
|
|
124
|
+
}) {
|
|
125
|
+
this.packages = options.packages;
|
|
126
|
+
this.existing = new Set(options.existing);
|
|
127
|
+
this.trustedPublisherByPackage = options.trustedPublishers ?? {};
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
listReleasePackages(): ReleasePackageInfo[] {
|
|
131
|
+
return this.packages;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
async packageExists(name: string): Promise<boolean> {
|
|
135
|
+
this.events.push(`exists:${name}`);
|
|
136
|
+
return this.existing.has(name);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
async bootstrapNpmPackages(options: BootstrapNpmPackagesOptions): Promise<ReleasePackageInfo[]> {
|
|
140
|
+
this.events.push(`bootstrap:${options.dryRun}:${options.skipLogin}`);
|
|
141
|
+
const missingPackages = this.packages.filter((pkg) => !this.existing.has(pkg.name));
|
|
142
|
+
for (const pkg of missingPackages) {
|
|
143
|
+
this.existing.add(pkg.name);
|
|
144
|
+
}
|
|
145
|
+
return missingPackages;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async trustPublisher(pkg: ReleasePackageInfo, dryRun: boolean): Promise<'configured' | 'already-configured'> {
|
|
149
|
+
this.events.push(`trust:${pkg.name}:${dryRun}`);
|
|
150
|
+
return 'configured';
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
async trustedPublishers(pkg: ReleasePackageInfo): Promise<TrustedPublisher[]> {
|
|
154
|
+
this.events.push(`list:${pkg.name}`);
|
|
155
|
+
return this.trustedPublisherByPackage[pkg.name] ?? [];
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
log(message: string): void {
|
|
159
|
+
this.logs.push(message);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
error(message: string): void {
|
|
163
|
+
this.errors.push(message);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import type { ReleasePackageInfo } from './core.js';
|
|
2
|
+
|
|
3
|
+
export const NPM_BOOTSTRAP_VERSION = '0.0.0-bootstrap.0';
|
|
4
|
+
export const NPM_BOOTSTRAP_DIST_TAG = 'bootstrap';
|
|
5
|
+
|
|
6
|
+
export interface BootstrapNpmPackagesOptions {
|
|
7
|
+
dryRun: boolean;
|
|
8
|
+
skipLogin: boolean;
|
|
9
|
+
packages: string[];
|
|
10
|
+
otp?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface BootstrapNpmPackagesShell<Package extends ReleasePackageInfo = ReleasePackageInfo> {
|
|
14
|
+
listReleasePackages(): Package[];
|
|
15
|
+
packageExists(name: string): Promise<boolean>;
|
|
16
|
+
login(): Promise<void>;
|
|
17
|
+
publishPlaceholder(pkg: Package, env?: Record<string, string>): Promise<void>;
|
|
18
|
+
promptOtp(packageName: string): Promise<string>;
|
|
19
|
+
log(message: string): void;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export async function bootstrapNpmPackages<Package extends ReleasePackageInfo>(
|
|
23
|
+
shell: BootstrapNpmPackagesShell<Package>,
|
|
24
|
+
options: BootstrapNpmPackagesOptions,
|
|
25
|
+
): Promise<Package[]> {
|
|
26
|
+
const packages = selectedReleasePackages(shell.listReleasePackages(), options.packages);
|
|
27
|
+
if (packages.length === 0) {
|
|
28
|
+
throw new Error('No owned release packages found.');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const missing: Package[] = [];
|
|
32
|
+
for (const pkg of packages) {
|
|
33
|
+
if (await shell.packageExists(pkg.name)) {
|
|
34
|
+
shell.log(`${pkg.name}: already exists on npm; skipping placeholder bootstrap.`);
|
|
35
|
+
} else {
|
|
36
|
+
missing.push(pkg);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (missing.length === 0) {
|
|
41
|
+
shell.log('All selected owned release packages already exist on npm.');
|
|
42
|
+
return [];
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
shell.log(
|
|
46
|
+
`Bootstrap npm placeholders (${NPM_BOOTSTRAP_VERSION}, dist-tag ${NPM_BOOTSTRAP_DIST_TAG}): ${missing
|
|
47
|
+
.map((pkg) => pkg.name)
|
|
48
|
+
.join(', ')}`,
|
|
49
|
+
);
|
|
50
|
+
if (options.dryRun) {
|
|
51
|
+
return missing;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
if (!options.skipLogin) {
|
|
55
|
+
await shell.login();
|
|
56
|
+
}
|
|
57
|
+
for (const pkg of missing) {
|
|
58
|
+
shell.log(`${pkg.name}: publishing npm placeholder.`);
|
|
59
|
+
const otp = options.otp ?? (await shell.promptOtp(pkg.name));
|
|
60
|
+
await shell.publishPlaceholder(pkg, { NPM_CONFIG_OTP: otp });
|
|
61
|
+
}
|
|
62
|
+
shell.log('Bootstrap complete. Run smoo release trust-publisher before the first CI publish.');
|
|
63
|
+
return missing;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function selectedReleasePackages<Package extends ReleasePackageInfo>(
|
|
67
|
+
packages: Package[],
|
|
68
|
+
selections: string[],
|
|
69
|
+
): Package[] {
|
|
70
|
+
if (selections.length === 0) {
|
|
71
|
+
return packages;
|
|
72
|
+
}
|
|
73
|
+
const byName = new Map(packages.map((pkg) => [pkg.name, pkg]));
|
|
74
|
+
const selected: Package[] = [];
|
|
75
|
+
const unknown: string[] = [];
|
|
76
|
+
for (const name of selections) {
|
|
77
|
+
const pkg = byName.get(name);
|
|
78
|
+
if (pkg) {
|
|
79
|
+
selected.push(pkg);
|
|
80
|
+
} else {
|
|
81
|
+
unknown.push(name);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (unknown.length > 0) {
|
|
85
|
+
throw new Error(`Unknown owned release package selection: ${unknown.join(', ')}`);
|
|
86
|
+
}
|
|
87
|
+
return selected;
|
|
88
|
+
}
|