@smoothbricks/cli 0.1.1 → 0.2.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 +155 -30
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +91 -7
- 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/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 +5 -0
- package/dist/lib/run.d.ts.map +1 -1
- package/dist/lib/run.js +13 -0
- package/dist/lib/workspace.d.ts +15 -1
- package/dist/lib/workspace.d.ts.map +1 -1
- package/dist/lib/workspace.js +25 -2
- 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/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 +22 -2
- package/dist/monorepo/package-policy.d.ts.map +1 -1
- package/dist/monorepo/package-policy.js +1167 -8
- 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 +5 -12
- 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 +24 -0
- package/dist/nx/index.d.ts.map +1 -0
- package/dist/nx/index.js +94 -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 +1 -0
- package/dist/release/core.d.ts.map +1 -1
- package/dist/release/github-release.js +2 -2
- package/dist/release/index.d.ts +25 -0
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +247 -49
- 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 +2 -1
- package/dist/release/orchestration.d.ts.map +1 -1
- package/dist/release/orchestration.js +9 -1
- 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 +1 -0
- package/package.json +15 -3
- package/src/cli.ts +134 -20
- package/src/devenv/index.test.ts +49 -0
- package/src/devenv/index.ts +38 -0
- package/src/lib/cli-package.ts +18 -0
- package/src/lib/run.ts +19 -0
- package/src/lib/workspace.ts +41 -2
- 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/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 +902 -0
- package/src/monorepo/package-policy.ts +1393 -7
- 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 +8 -16
- 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 +64 -0
- package/src/nx/index.ts +119 -0
- package/src/release/__tests__/bootstrap-npm-packages.test.ts +127 -0
- package/src/release/__tests__/candidates.test.ts +217 -0
- package/src/release/__tests__/core-properties.test.ts +4 -4
- package/src/release/__tests__/core-scenarios.test.ts +15 -5
- package/src/release/__tests__/core.test.ts +2 -2
- package/src/release/__tests__/fixture-repo.test.ts +22 -13
- package/src/release/__tests__/github-release.test.ts +13 -8
- 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 +51 -7
- package/src/release/__tests__/publish-plan.test.ts +9 -3
- package/src/release/__tests__/retag-unpublished.test.ts +1 -1
- package/src/release/__tests__/trust-publisher.test.ts +133 -0
- package/src/release/bootstrap-npm-packages.ts +88 -0
- package/src/release/candidates.ts +225 -0
- package/src/release/core.ts +1 -0
- package/src/release/github-release.ts +2 -2
- package/src/release/index.ts +346 -56
- package/src/release/npm-auth.ts +4 -33
- package/src/release/orchestration.ts +12 -2
- package/src/release/publish-plan.ts +1 -1
- package/managed/raw/.git-format-staged.yml +0 -47
|
@@ -8,9 +8,15 @@ import {
|
|
|
8
8
|
runReleaseVersion,
|
|
9
9
|
} from '../orchestration.js';
|
|
10
10
|
|
|
11
|
-
const stable: ReleasePackageInfo = {
|
|
11
|
+
const stable: ReleasePackageInfo = {
|
|
12
|
+
name: '@scope/stable',
|
|
13
|
+
projectName: 'stable',
|
|
14
|
+
path: 'packages/stable',
|
|
15
|
+
version: '1.0.0',
|
|
16
|
+
};
|
|
12
17
|
const prerelease: ReleasePackageInfo = {
|
|
13
18
|
name: '@scope/prerelease',
|
|
19
|
+
projectName: 'prerelease',
|
|
14
20
|
path: 'packages/prerelease',
|
|
15
21
|
version: '2.0.0-beta.1',
|
|
16
22
|
};
|
|
@@ -84,15 +90,43 @@ describe('release orchestration', () => {
|
|
|
84
90
|
});
|
|
85
91
|
|
|
86
92
|
it('runs forced Nx versioning for an untagged HEAD and reports a new release commit', async () => {
|
|
87
|
-
const shell = new RecordingVersionShell({
|
|
93
|
+
const shell = new RecordingVersionShell({
|
|
94
|
+
releasePackagesAtHead: [[], [stable]],
|
|
95
|
+
releaseVersionPackages: [stable, prerelease],
|
|
96
|
+
heads: ['before', 'after'],
|
|
97
|
+
});
|
|
88
98
|
|
|
89
99
|
const result = await runReleaseVersion(shell, { bump: 'patch', dryRun: false });
|
|
90
100
|
|
|
91
101
|
expect(result).toEqual({ mode: 'new', packages: [stable], status: 'new-release' });
|
|
92
|
-
expect(shell.nxRuns).toEqual([{ bump: 'patch', dryRun: false }]);
|
|
102
|
+
expect(shell.nxRuns).toEqual([{ packages: ['@scope/stable', '@scope/prerelease'], bump: 'patch', dryRun: false }]);
|
|
93
103
|
expect(shell.cleanChecks).toBe(1);
|
|
94
104
|
expect(shell.ensureCalls).toEqual([['@scope/stable']]);
|
|
95
105
|
});
|
|
106
|
+
|
|
107
|
+
it('skips auto Nx versioning when no package-local candidates exist', async () => {
|
|
108
|
+
const shell = new RecordingVersionShell({ releasePackagesAtHead: [[]], releaseVersionPackages: [] });
|
|
109
|
+
|
|
110
|
+
const result = await runReleaseVersion(shell, { bump: 'auto', dryRun: false });
|
|
111
|
+
|
|
112
|
+
expect(result).toEqual({ mode: 'none', packages: [], status: 'no-release-needed' });
|
|
113
|
+
expect(shell.nxRuns).toEqual([]);
|
|
114
|
+
expect(shell.cleanChecks).toBe(0);
|
|
115
|
+
expect(shell.ensureCalls).toEqual([]);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
it('runs auto Nx versioning only for selected package-local candidates', async () => {
|
|
119
|
+
const shell = new RecordingVersionShell({
|
|
120
|
+
releasePackagesAtHead: [[], [stable]],
|
|
121
|
+
releaseVersionPackages: [stable],
|
|
122
|
+
heads: ['before', 'after'],
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
const result = await runReleaseVersion(shell, { bump: 'auto', dryRun: false });
|
|
126
|
+
|
|
127
|
+
expect(result).toEqual({ mode: 'new', packages: [stable], status: 'new-release' });
|
|
128
|
+
expect(shell.nxRuns).toEqual([{ packages: ['@scope/stable'], bump: 'auto', dryRun: false }]);
|
|
129
|
+
});
|
|
96
130
|
});
|
|
97
131
|
|
|
98
132
|
function releaseTarget(
|
|
@@ -166,13 +200,19 @@ class RecordingRepairShell implements ReleaseRepairShell<ReleasePackageInfo> {
|
|
|
166
200
|
|
|
167
201
|
class RecordingVersionShell implements ReleaseVersionShell<ReleasePackageInfo> {
|
|
168
202
|
readonly ensureCalls: string[][] = [];
|
|
169
|
-
readonly nxRuns: Array<{ bump: string; dryRun: boolean }> = [];
|
|
203
|
+
readonly nxRuns: Array<{ packages: string[]; bump: string; dryRun: boolean }> = [];
|
|
170
204
|
cleanChecks = 0;
|
|
171
205
|
private readonly releaseBatches: ReleasePackageInfo[][];
|
|
206
|
+
private readonly versionPackages: ReleasePackageInfo[];
|
|
172
207
|
private readonly heads: string[];
|
|
173
208
|
|
|
174
|
-
constructor(options: {
|
|
209
|
+
constructor(options: {
|
|
210
|
+
releasePackagesAtHead: ReleasePackageInfo[][];
|
|
211
|
+
releaseVersionPackages?: ReleasePackageInfo[];
|
|
212
|
+
heads?: string[];
|
|
213
|
+
}) {
|
|
175
214
|
this.releaseBatches = [...options.releasePackagesAtHead];
|
|
215
|
+
this.versionPackages = options.releaseVersionPackages ?? [stable, prerelease];
|
|
176
216
|
this.heads = [...(options.heads ?? [])];
|
|
177
217
|
}
|
|
178
218
|
|
|
@@ -180,6 +220,10 @@ class RecordingVersionShell implements ReleaseVersionShell<ReleasePackageInfo> {
|
|
|
180
220
|
return this.releaseBatches.shift() ?? [];
|
|
181
221
|
}
|
|
182
222
|
|
|
223
|
+
async releaseVersionPackages(): Promise<ReleasePackageInfo[]> {
|
|
224
|
+
return this.versionPackages;
|
|
225
|
+
}
|
|
226
|
+
|
|
183
227
|
async ensureLocalReleaseTags(packages: ReleasePackageInfo[]): Promise<void> {
|
|
184
228
|
this.ensureCalls.push(packageNames(packages));
|
|
185
229
|
}
|
|
@@ -188,8 +232,8 @@ class RecordingVersionShell implements ReleaseVersionShell<ReleasePackageInfo> {
|
|
|
188
232
|
return this.heads.shift() ?? 'head';
|
|
189
233
|
}
|
|
190
234
|
|
|
191
|
-
async runNxReleaseVersion(bump: string, dryRun: boolean): Promise<void> {
|
|
192
|
-
this.nxRuns.push({ bump, dryRun });
|
|
235
|
+
async runNxReleaseVersion(packages: ReleasePackageInfo[], bump: string, dryRun: boolean): Promise<void> {
|
|
236
|
+
this.nxRuns.push({ packages: packageNames(packages), bump, dryRun });
|
|
193
237
|
}
|
|
194
238
|
|
|
195
239
|
async assertCleanGitTree(): Promise<void> {
|
|
@@ -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,7 +7,7 @@ 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 () => {
|
|
@@ -0,0 +1,133 @@
|
|
|
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 { configureTrustedPublishers, type TrustPublisherShell } from '../index.js';
|
|
5
|
+
|
|
6
|
+
const stable: ReleasePackageInfo = {
|
|
7
|
+
name: '@scope/stable',
|
|
8
|
+
projectName: 'stable',
|
|
9
|
+
path: 'packages/stable',
|
|
10
|
+
version: '1.2.3',
|
|
11
|
+
};
|
|
12
|
+
const missing: ReleasePackageInfo = {
|
|
13
|
+
name: '@scope/missing',
|
|
14
|
+
projectName: 'missing',
|
|
15
|
+
path: 'packages/missing',
|
|
16
|
+
version: '2.0.0',
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
describe('trusted publisher setup', () => {
|
|
20
|
+
it('bootstraps missing npm packages before configuring trust', async () => {
|
|
21
|
+
const shell = new RecordingTrustPublisherShell({ packages: [stable, missing], existing: [stable.name] });
|
|
22
|
+
|
|
23
|
+
await configureTrustedPublishers(shell, { bootstrap: true, skipLogin: false, otp: '123456' });
|
|
24
|
+
|
|
25
|
+
expect(shell.events).toEqual([
|
|
26
|
+
'bootstrap:false:false',
|
|
27
|
+
`exists:${stable.name}`,
|
|
28
|
+
`exists:${missing.name}`,
|
|
29
|
+
`trust:${stable.name}:false:123456`,
|
|
30
|
+
`trust:${missing.name}:false:123456`,
|
|
31
|
+
]);
|
|
32
|
+
expect(shell.logins).toBe(0);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it('directs missing packages to trust-publisher --bootstrap', async () => {
|
|
36
|
+
const shell = new RecordingTrustPublisherShell({ packages: [missing], existing: [] });
|
|
37
|
+
|
|
38
|
+
await expect(configureTrustedPublishers(shell, { otp: '123456' })).rejects.toThrow(
|
|
39
|
+
'Run smoo release trust-publisher --bootstrap locally',
|
|
40
|
+
);
|
|
41
|
+
expect(shell.events).toEqual([`exists:${missing.name}`]);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('still logs in for trust when bootstrap finds no missing packages', async () => {
|
|
45
|
+
const shell = new RecordingTrustPublisherShell({ packages: [stable], existing: [stable.name] });
|
|
46
|
+
|
|
47
|
+
await configureTrustedPublishers(shell, { bootstrap: true, otp: '123456' });
|
|
48
|
+
|
|
49
|
+
expect(shell.events).toEqual([
|
|
50
|
+
'bootstrap:false:false',
|
|
51
|
+
`exists:${stable.name}`,
|
|
52
|
+
`trust:${stable.name}:false:123456`,
|
|
53
|
+
]);
|
|
54
|
+
expect(shell.logins).toBe(1);
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('treats npm trust conflicts as already configured', async () => {
|
|
58
|
+
const shell = new RecordingTrustPublisherShell({
|
|
59
|
+
packages: [stable],
|
|
60
|
+
existing: [stable.name],
|
|
61
|
+
alreadyTrusted: [stable.name],
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
await configureTrustedPublishers(shell, { skipLogin: true, otp: '123456' });
|
|
65
|
+
|
|
66
|
+
expect(shell.events).toEqual([`exists:${stable.name}`, `trust:${stable.name}:false:123456`]);
|
|
67
|
+
expect(shell.logs).toContain(`${stable.name}: npm trusted publisher is already configured; skipping.`);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
class RecordingTrustPublisherShell implements TrustPublisherShell<ReleasePackageInfo> {
|
|
72
|
+
readonly repository = 'scope/repo';
|
|
73
|
+
readonly workflow = 'publish.yml';
|
|
74
|
+
readonly events: string[] = [];
|
|
75
|
+
readonly logs: string[] = [];
|
|
76
|
+
readonly errors: string[] = [];
|
|
77
|
+
logins = 0;
|
|
78
|
+
private readonly packages: ReleasePackageInfo[];
|
|
79
|
+
private readonly existing: Set<string>;
|
|
80
|
+
private readonly alreadyTrusted: Set<string>;
|
|
81
|
+
|
|
82
|
+
constructor(options: { packages: ReleasePackageInfo[]; existing: string[]; alreadyTrusted?: string[] }) {
|
|
83
|
+
this.packages = options.packages;
|
|
84
|
+
this.existing = new Set(options.existing);
|
|
85
|
+
this.alreadyTrusted = new Set(options.alreadyTrusted ?? []);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
listReleasePackages(): ReleasePackageInfo[] {
|
|
89
|
+
return this.packages;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
async packageExists(name: string): Promise<boolean> {
|
|
93
|
+
this.events.push(`exists:${name}`);
|
|
94
|
+
return this.existing.has(name);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async bootstrapNpmPackages(options: BootstrapNpmPackagesOptions): Promise<ReleasePackageInfo[]> {
|
|
98
|
+
this.events.push(`bootstrap:${options.dryRun}:${options.skipLogin}`);
|
|
99
|
+
const missingPackages = this.packages.filter((pkg) => !this.existing.has(pkg.name));
|
|
100
|
+
for (const pkg of missingPackages) {
|
|
101
|
+
this.existing.add(pkg.name);
|
|
102
|
+
}
|
|
103
|
+
return missingPackages;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async login(): Promise<void> {
|
|
107
|
+
this.logins += 1;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async trustPublisher(
|
|
111
|
+
pkg: ReleasePackageInfo,
|
|
112
|
+
dryRun: boolean,
|
|
113
|
+
env?: Record<string, string>,
|
|
114
|
+
): Promise<'configured' | 'already-configured'> {
|
|
115
|
+
this.events.push(`trust:${pkg.name}:${dryRun}:${env?.NPM_CONFIG_OTP ?? ''}`);
|
|
116
|
+
if (this.alreadyTrusted.has(pkg.name)) {
|
|
117
|
+
return 'already-configured';
|
|
118
|
+
}
|
|
119
|
+
return 'configured';
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
async promptOtp(packageName: string): Promise<string> {
|
|
123
|
+
throw new Error(`unexpected OTP prompt for ${packageName}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
log(message: string): void {
|
|
127
|
+
this.logs.push(message);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
error(message: string): void {
|
|
131
|
+
this.errors.push(message);
|
|
132
|
+
}
|
|
133
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -0,0 +1,225 @@
|
|
|
1
|
+
import { isRecord } from '../lib/json.js';
|
|
2
|
+
import type { ReleasePackageInfo } from './core.js';
|
|
3
|
+
import { releaseTag } from './core.js';
|
|
4
|
+
|
|
5
|
+
export interface AutoReleaseCandidateShell {
|
|
6
|
+
gitRefExists(ref: string): Promise<boolean>;
|
|
7
|
+
packageChangedFilesSince(ref: string, packagePath: string): Promise<string[]>;
|
|
8
|
+
packageJsonAtRef(ref: string, packagePath: string): Promise<Record<string, unknown> | null>;
|
|
9
|
+
currentPackageJson(packagePath: string): Promise<Record<string, unknown> | null>;
|
|
10
|
+
packageBuildInputPatterns(projectName: string, packagePath: string): Promise<string[]>;
|
|
11
|
+
packageHasHistory(packagePath: string): Promise<boolean>;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
const releasableManifestKeys = [
|
|
15
|
+
'author',
|
|
16
|
+
'bin',
|
|
17
|
+
'browser',
|
|
18
|
+
'bugs',
|
|
19
|
+
'cpu',
|
|
20
|
+
'dependencies',
|
|
21
|
+
'description',
|
|
22
|
+
'engines',
|
|
23
|
+
'exports',
|
|
24
|
+
'files',
|
|
25
|
+
'funding',
|
|
26
|
+
'homepage',
|
|
27
|
+
'imports',
|
|
28
|
+
'keywords',
|
|
29
|
+
'license',
|
|
30
|
+
'main',
|
|
31
|
+
'module',
|
|
32
|
+
'name',
|
|
33
|
+
'optionalDependencies',
|
|
34
|
+
'os',
|
|
35
|
+
'peerDependencies',
|
|
36
|
+
'peerDependenciesMeta',
|
|
37
|
+
'publishConfig',
|
|
38
|
+
'repository',
|
|
39
|
+
'sideEffects',
|
|
40
|
+
'types',
|
|
41
|
+
'typesVersions',
|
|
42
|
+
] as const;
|
|
43
|
+
|
|
44
|
+
export async function autoReleaseCandidatePackages<Package extends ReleasePackageInfo>(
|
|
45
|
+
shell: AutoReleaseCandidateShell,
|
|
46
|
+
packages: Package[],
|
|
47
|
+
): Promise<Package[]> {
|
|
48
|
+
const candidates: Package[] = [];
|
|
49
|
+
for (const pkg of packages) {
|
|
50
|
+
if (await isAutoReleaseCandidate(shell, pkg)) {
|
|
51
|
+
candidates.push(pkg);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
return candidates;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function isAutoReleaseCandidate<Package extends ReleasePackageInfo>(
|
|
58
|
+
shell: AutoReleaseCandidateShell,
|
|
59
|
+
pkg: Package,
|
|
60
|
+
): Promise<boolean> {
|
|
61
|
+
const tagRef = `refs/tags/${releaseTag(pkg)}`;
|
|
62
|
+
if (await shell.gitRefExists(tagRef)) {
|
|
63
|
+
return packageHasReleasableChangesSince(shell, tagRef, pkg);
|
|
64
|
+
}
|
|
65
|
+
return shell.packageHasHistory(pkg.path);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function packageHasReleasableChangesSince<Package extends ReleasePackageInfo>(
|
|
69
|
+
shell: AutoReleaseCandidateShell,
|
|
70
|
+
ref: string,
|
|
71
|
+
pkg: Package,
|
|
72
|
+
): Promise<boolean> {
|
|
73
|
+
const changedFiles = await shell.packageChangedFilesSince(ref, pkg.path);
|
|
74
|
+
if (changedFiles.length === 0) {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
const currentManifest = await shell.currentPackageJson(pkg.path);
|
|
78
|
+
const buildInputPatterns = await shell.packageBuildInputPatterns(pkg.projectName, pkg.path);
|
|
79
|
+
for (const changedFile of changedFiles) {
|
|
80
|
+
if (changedFile === 'package.json') {
|
|
81
|
+
const previousManifest = await shell.packageJsonAtRef(ref, pkg.path);
|
|
82
|
+
if (!previousManifest || !currentManifest || releasableManifestChanged(previousManifest, currentManifest)) {
|
|
83
|
+
return true;
|
|
84
|
+
}
|
|
85
|
+
continue;
|
|
86
|
+
}
|
|
87
|
+
if (isReleasablePackagePath(changedFile, currentManifest, buildInputPatterns)) {
|
|
88
|
+
return true;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function releasableManifestChanged(
|
|
95
|
+
previousManifest: Record<string, unknown>,
|
|
96
|
+
currentManifest: Record<string, unknown>,
|
|
97
|
+
): boolean {
|
|
98
|
+
for (const key of releasableManifestKeys) {
|
|
99
|
+
if (!stableJsonEqual(previousManifest[key], currentManifest[key])) {
|
|
100
|
+
return true;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
return false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isReleasablePackagePath(
|
|
107
|
+
path: string,
|
|
108
|
+
manifest: Record<string, unknown> | null,
|
|
109
|
+
buildInputPatterns: string[],
|
|
110
|
+
): boolean {
|
|
111
|
+
return (
|
|
112
|
+
isBuildInputPath(path, buildInputPatterns) || isPackageMetadataPath(path) || isManifestFilesPath(path, manifest)
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isBuildInputPath(path: string, patterns: string[]): boolean {
|
|
117
|
+
if (isReleaseIgnoredBuildInputPath(path)) {
|
|
118
|
+
return false;
|
|
119
|
+
}
|
|
120
|
+
let matched = false;
|
|
121
|
+
for (const pattern of patterns) {
|
|
122
|
+
const excluded = pattern.startsWith('!');
|
|
123
|
+
const rawPattern = excluded ? pattern.slice(1) : pattern;
|
|
124
|
+
if (matchesBuildInputPattern(path, rawPattern)) {
|
|
125
|
+
matched = !excluded;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
return matched;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function matchesBuildInputPattern(path: string, pattern: string): boolean {
|
|
132
|
+
const normalized = pattern.replace(/^\.\//, '').replace(/\/$/, '');
|
|
133
|
+
if (!normalized) {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
return globPatternToRegExp(normalized).test(path);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function globPatternToRegExp(pattern: string): RegExp {
|
|
140
|
+
let source = '^';
|
|
141
|
+
for (let index = 0; index < pattern.length; index += 1) {
|
|
142
|
+
const char = pattern[index];
|
|
143
|
+
if (char === '*') {
|
|
144
|
+
if (pattern[index + 1] === '*') {
|
|
145
|
+
if (pattern[index + 2] === '/') {
|
|
146
|
+
source += '(?:.*/)?';
|
|
147
|
+
index += 2;
|
|
148
|
+
} else {
|
|
149
|
+
source += '.*';
|
|
150
|
+
index += 1;
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
source += '[^/]*';
|
|
154
|
+
}
|
|
155
|
+
} else {
|
|
156
|
+
source += escapeRegExpChar(char);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
return new RegExp(`${source}$`);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function escapeRegExpChar(char: string | undefined): string {
|
|
163
|
+
if (!char) {
|
|
164
|
+
return '';
|
|
165
|
+
}
|
|
166
|
+
return /[\\^$+?.()|[\]{}]/.test(char) ? `\\${char}` : char;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function isReleaseIgnoredBuildInputPath(path: string): boolean {
|
|
170
|
+
return (
|
|
171
|
+
path === 'package.json' ||
|
|
172
|
+
path === 'tsconfig.test.json' ||
|
|
173
|
+
path.includes('/__tests__/') ||
|
|
174
|
+
path.endsWith('.test.ts') ||
|
|
175
|
+
path.endsWith('.test.tsx') ||
|
|
176
|
+
path.endsWith('.spec.ts') ||
|
|
177
|
+
path.endsWith('.spec.tsx')
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function isPackageMetadataPath(path: string): boolean {
|
|
182
|
+
return /^(README|LICENSE|CHANGELOG)(\.|$)/.test(path);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
function isManifestFilesPath(path: string, manifest: Record<string, unknown> | null): boolean {
|
|
186
|
+
const files = manifest?.files;
|
|
187
|
+
if (!Array.isArray(files)) {
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
for (const entry of files) {
|
|
191
|
+
if (typeof entry !== 'string' || entry.length === 0 || entry.startsWith('!')) {
|
|
192
|
+
continue;
|
|
193
|
+
}
|
|
194
|
+
if (matchesManifestFilesEntry(path, entry)) {
|
|
195
|
+
return true;
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function matchesManifestFilesEntry(path: string, entry: string): boolean {
|
|
202
|
+
const normalized = entry.replace(/^\.\//, '').replace(/\/$/, '');
|
|
203
|
+
if (!normalized || normalized.includes('*')) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
return path === normalized || path.startsWith(`${normalized}/`);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
function stableJsonEqual(left: unknown, right: unknown): boolean {
|
|
210
|
+
return JSON.stringify(stableJson(left)) === JSON.stringify(stableJson(right));
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
function stableJson(value: unknown): unknown {
|
|
214
|
+
if (Array.isArray(value)) {
|
|
215
|
+
return value.map(stableJson);
|
|
216
|
+
}
|
|
217
|
+
if (isRecord(value)) {
|
|
218
|
+
const normalized: Record<string, unknown> = {};
|
|
219
|
+
for (const key of Object.keys(value).sort()) {
|
|
220
|
+
normalized[key] = stableJson(value[key]);
|
|
221
|
+
}
|
|
222
|
+
return normalized;
|
|
223
|
+
}
|
|
224
|
+
return value;
|
|
225
|
+
}
|
package/src/release/core.ts
CHANGED
|
@@ -39,7 +39,7 @@ export async function renderNxProjectChangelogContents(input: RenderNxProjectCha
|
|
|
39
39
|
nxRenderOnlyReleaseConfig,
|
|
40
40
|
false,
|
|
41
41
|
)(nxProjectChangelogArgs(input.pkg, input.previousTag, input.dryRun));
|
|
42
|
-
return projectChangelogContents(result, input.pkg.
|
|
42
|
+
return projectChangelogContents(result, input.pkg.projectName);
|
|
43
43
|
});
|
|
44
44
|
}
|
|
45
45
|
|
|
@@ -65,7 +65,7 @@ export async function createOrUpdateGithubRelease(
|
|
|
65
65
|
export function nxProjectChangelogArgs(pkg: ReleasePackageInfo, previousTag: string | null, dryRun: boolean) {
|
|
66
66
|
const base = {
|
|
67
67
|
version: pkg.version,
|
|
68
|
-
projects: [pkg.
|
|
68
|
+
projects: [pkg.projectName],
|
|
69
69
|
gitCommit: false,
|
|
70
70
|
gitTag: false,
|
|
71
71
|
gitPush: false,
|