@smoothbricks/cli 0.2.0 → 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 +27 -23
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +19 -3
- 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/run.d.ts +1 -0
- package/dist/lib/run.d.ts.map +1 -1
- package/dist/lib/run.js +19 -0
- package/dist/lib/workspace.d.ts +10 -0
- package/dist/lib/workspace.d.ts.map +1 -1
- package/dist/lib/workspace.js +55 -16
- package/dist/monorepo/lockfile.d.ts.map +1 -1
- package/dist/monorepo/lockfile.js +9 -0
- package/dist/monorepo/package-policy.d.ts +6 -17
- package/dist/monorepo/package-policy.d.ts.map +1 -1
- package/dist/monorepo/package-policy.js +63 -1127
- package/dist/monorepo/publish-workflow.js +2 -3
- package/dist/nx/index.d.ts +1 -0
- package/dist/nx/index.d.ts.map +1 -1
- package/dist/nx/index.js +23 -5
- package/dist/release/candidates.js +1 -1
- package/dist/release/core.d.ts +3 -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 +1 -1
- package/dist/release/index.d.ts +9 -3
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +265 -39
- package/dist/release/orchestration.d.ts +10 -2
- package/dist/release/orchestration.d.ts.map +1 -1
- package/dist/release/orchestration.js +33 -3
- package/managed/templates/github/workflows/ci.yml +3 -2
- package/package.json +7 -12
- package/src/cli.ts +21 -4
- package/src/generate/index.ts +92 -0
- package/src/lib/run.ts +25 -0
- package/src/lib/workspace.ts +72 -16
- package/src/monorepo/lockfile.ts +9 -0
- package/src/monorepo/package-policy.test.ts +250 -22
- package/src/monorepo/package-policy.ts +88 -1351
- package/src/monorepo/publish-workflow.ts +2 -3
- package/src/nx/index.test.ts +9 -0
- package/src/nx/index.ts +25 -5
- package/src/release/__tests__/candidates.test.ts +53 -8
- package/src/release/__tests__/core-properties.test.ts +2 -2
- package/src/release/__tests__/core-scenarios.test.ts +12 -15
- package/src/release/__tests__/core.test.ts +50 -3
- package/src/release/__tests__/fixture-repo.test.ts +68 -18
- package/src/release/__tests__/github-release.test.ts +9 -16
- package/src/release/__tests__/orchestration.test.ts +91 -5
- package/src/release/__tests__/retag-unpublished.test.ts +9 -9
- package/src/release/__tests__/trust-publisher.test.ts +65 -33
- package/src/release/candidates.ts +1 -1
- package/src/release/core.ts +95 -10
- package/src/release/github-release.ts +1 -1
- package/src/release/index.ts +343 -49
- package/src/release/orchestration.ts +50 -5
- 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/src/nx-version-actions.cts +0 -36
package/src/release/index.ts
CHANGED
|
@@ -6,7 +6,7 @@ import { Writable } from 'node:stream';
|
|
|
6
6
|
import { $ } from 'bun';
|
|
7
7
|
import { withDevenvEnv } from '../lib/devenv.js';
|
|
8
8
|
import { isRecord, readJsonObject, stringProperty } from '../lib/json.js';
|
|
9
|
-
import { decode, run, runResult, runStatus } from '../lib/run.js';
|
|
9
|
+
import { decode, run, runInteractiveStatus, runResult, runStatus } from '../lib/run.js';
|
|
10
10
|
import { listReleasePackages, readPackageJson, repositoryInfo } from '../lib/workspace.js';
|
|
11
11
|
import { readPackedPackageJson, validatePackedWorkspaceDependencies } from '../monorepo/packed-manifest.js';
|
|
12
12
|
import {
|
|
@@ -24,10 +24,16 @@ import {
|
|
|
24
24
|
pendingReleaseTargets,
|
|
25
25
|
type ReleasePackageInfo,
|
|
26
26
|
releaseTag,
|
|
27
|
+
releaseTagAliases,
|
|
27
28
|
} from './core.js';
|
|
28
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
createOrUpdateGithubRelease,
|
|
31
|
+
renderNxProjectChangelogContents,
|
|
32
|
+
withNxWorkspaceRoot,
|
|
33
|
+
} from './github-release.js';
|
|
29
34
|
import { publishWithAuthDiagnostics } from './npm-auth.js';
|
|
30
35
|
import {
|
|
36
|
+
bumpStableReleaseToNext,
|
|
31
37
|
completeReleaseAtHead as completeReleaseAtHeadWithShell,
|
|
32
38
|
type ReleaseCompletionShell,
|
|
33
39
|
type ReleaseSummary,
|
|
@@ -55,9 +61,9 @@ export interface ReleaseRepairPendingOptions {
|
|
|
55
61
|
export interface ReleaseTrustPublisherOptions {
|
|
56
62
|
dryRun?: boolean;
|
|
57
63
|
bootstrap?: boolean;
|
|
58
|
-
otp?: string;
|
|
59
64
|
bootstrapOtp?: string;
|
|
60
65
|
skipLogin?: boolean;
|
|
66
|
+
packages?: string[];
|
|
61
67
|
}
|
|
62
68
|
|
|
63
69
|
export interface ReleaseBootstrapNpmPackagesOptions {
|
|
@@ -97,6 +103,9 @@ export async function releaseVersion(root: string, options: ReleaseVersionOption
|
|
|
97
103
|
} else if (result.status === 'no-release-needed') {
|
|
98
104
|
console.log('Nx did not create a release commit; no release needed.');
|
|
99
105
|
}
|
|
106
|
+
if (result.status === 'dry-run') {
|
|
107
|
+
await writeReleasePreviewSummary(root, packages, result.packages);
|
|
108
|
+
}
|
|
100
109
|
await writeReleaseGithubOutput(options.githubOutput, result.packages, result.mode);
|
|
101
110
|
}
|
|
102
111
|
|
|
@@ -105,15 +114,22 @@ export async function releasePublish(root: string, options: ReleasePublishOption
|
|
|
105
114
|
const packages = await releasePackagesAtHead(root, releasePackages(root));
|
|
106
115
|
if (packages.length === 0) {
|
|
107
116
|
if (bump === 'auto') {
|
|
117
|
+
if (options.dryRun === true) {
|
|
118
|
+
console.log(
|
|
119
|
+
'No release tags found at HEAD because dry-run versioning does not create release commits or tags.',
|
|
120
|
+
);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
108
123
|
console.log('No release tags found at HEAD; no release to publish.');
|
|
109
124
|
const summary: ReleaseSummary<ReleasePackage> = {
|
|
110
125
|
sha: await gitHead(root),
|
|
111
|
-
dryRun:
|
|
126
|
+
dryRun: false,
|
|
112
127
|
packages,
|
|
113
128
|
pushed: false,
|
|
114
129
|
published: [],
|
|
115
130
|
alreadyPublished: [],
|
|
116
131
|
githubReleases: [],
|
|
132
|
+
githubReleaseLinks: [],
|
|
117
133
|
rerunRequired: false,
|
|
118
134
|
noRelease: true,
|
|
119
135
|
};
|
|
@@ -129,6 +145,15 @@ export async function releasePublish(root: string, options: ReleasePublishOption
|
|
|
129
145
|
await newerCommitsRemain(root),
|
|
130
146
|
);
|
|
131
147
|
await writeReleaseSummary(summary);
|
|
148
|
+
const bumpedPackages = await bumpStableReleaseToNext(
|
|
149
|
+
releaseNextShell(root),
|
|
150
|
+
packages,
|
|
151
|
+
options.dryRun === true,
|
|
152
|
+
summary.rerunRequired,
|
|
153
|
+
);
|
|
154
|
+
if (bumpedPackages.length > 0) {
|
|
155
|
+
console.log(`Prepared next prerelease for ${packageSummary(bumpedPackages)}.`);
|
|
156
|
+
}
|
|
132
157
|
}
|
|
133
158
|
|
|
134
159
|
export async function releaseRepairPending(root: string, options: ReleaseRepairPendingOptions): Promise<void> {
|
|
@@ -145,6 +170,9 @@ export async function releaseRepairPending(root: string, options: ReleaseRepairP
|
|
|
145
170
|
? 'Repair pending releases: no pending durable state repairs found.'
|
|
146
171
|
: `Repair pending releases: ${targets.length} release target${targets.length === 1 ? '' : 's'} need repair.`,
|
|
147
172
|
);
|
|
173
|
+
for (const target of targets) {
|
|
174
|
+
console.log(`Repair pending releases: target ${target.sha.slice(0, 12)} needs ${repairTargetSummary(target)}.`);
|
|
175
|
+
}
|
|
148
176
|
const summaries = await repairPendingTargets(releaseRepairShell(root), targets, restoreRef, options.dryRun === true);
|
|
149
177
|
await writeRepairSummary(summaries, options.dryRun === true);
|
|
150
178
|
}
|
|
@@ -170,7 +198,6 @@ export async function releaseTrustPublisher(root: string, options: ReleaseTrustP
|
|
|
170
198
|
},
|
|
171
199
|
bootstrapOptions,
|
|
172
200
|
),
|
|
173
|
-
login: () => runLatestNpm(root, ['login', '--auth-type=web']),
|
|
174
201
|
trustPublisher: (pkg, dryRun, env) => {
|
|
175
202
|
const args = ['trust', 'github', pkg.name, '--file', workflow, '--repo', repository, '--yes'];
|
|
176
203
|
if (dryRun) {
|
|
@@ -178,7 +205,7 @@ export async function releaseTrustPublisher(root: string, options: ReleaseTrustP
|
|
|
178
205
|
}
|
|
179
206
|
return runLatestNpmTrust(root, args, env);
|
|
180
207
|
},
|
|
181
|
-
|
|
208
|
+
trustedPublishers: (pkg) => listTrustedPublishers(root, pkg),
|
|
182
209
|
log: (message) => console.log(message),
|
|
183
210
|
error: (message) => console.error(message),
|
|
184
211
|
},
|
|
@@ -192,38 +219,43 @@ export interface TrustPublisherShell<Package extends ReleasePackageInfo = Releas
|
|
|
192
219
|
listReleasePackages(): Package[];
|
|
193
220
|
packageExists(name: string): Promise<boolean>;
|
|
194
221
|
bootstrapNpmPackages(options: BootstrapNpmPackagesOptions): Promise<Package[]>;
|
|
195
|
-
login(): Promise<void>;
|
|
196
222
|
trustPublisher(pkg: Package, dryRun: boolean, env?: Record<string, string>): Promise<TrustPublisherResult>;
|
|
197
|
-
|
|
223
|
+
trustedPublishers(pkg: Package): Promise<TrustedPublisher[]>;
|
|
198
224
|
log(message: string): void;
|
|
199
225
|
error(message: string): void;
|
|
200
226
|
}
|
|
201
227
|
|
|
202
228
|
export type TrustPublisherResult = 'configured' | 'already-configured';
|
|
203
229
|
|
|
230
|
+
export interface TrustedPublisher {
|
|
231
|
+
id: string;
|
|
232
|
+
type: string;
|
|
233
|
+
file?: string;
|
|
234
|
+
repository?: string;
|
|
235
|
+
}
|
|
236
|
+
|
|
204
237
|
export async function configureTrustedPublishers<Package extends ReleasePackageInfo>(
|
|
205
238
|
shell: TrustPublisherShell<Package>,
|
|
206
239
|
options: ReleaseTrustPublisherOptions,
|
|
207
240
|
): Promise<void> {
|
|
208
241
|
const packages = shell.listReleasePackages();
|
|
209
|
-
|
|
242
|
+
const selectedPackages = selectedTrustPublisherPackages(packages, options.packages ?? []);
|
|
243
|
+
if (selectedPackages.length === 0) {
|
|
210
244
|
throw new Error('No owned release packages found.');
|
|
211
245
|
}
|
|
212
246
|
|
|
213
|
-
let bootstrapLoggedIn = false;
|
|
214
247
|
if (options.bootstrap) {
|
|
215
|
-
|
|
248
|
+
await shell.bootstrapNpmPackages({
|
|
216
249
|
dryRun: options.dryRun === true,
|
|
217
250
|
skipLogin: options.skipLogin === true,
|
|
218
251
|
otp: options.bootstrapOtp,
|
|
219
|
-
packages: [],
|
|
252
|
+
packages: options.packages ?? [],
|
|
220
253
|
});
|
|
221
|
-
bootstrapLoggedIn = bootstrapped.length > 0 && !options.dryRun && !options.skipLogin;
|
|
222
254
|
}
|
|
223
255
|
|
|
224
256
|
if (!options.dryRun) {
|
|
225
257
|
const packageStates = await Promise.all(
|
|
226
|
-
|
|
258
|
+
selectedPackages.map(async (pkg) => ((await shell.packageExists(pkg.name)) ? null : pkg.name)),
|
|
227
259
|
);
|
|
228
260
|
const missingPackages = packageStates.filter((name): name is string => name !== null);
|
|
229
261
|
if (missingPackages.length > 0) {
|
|
@@ -235,21 +267,26 @@ export async function configureTrustedPublishers<Package extends ReleasePackageI
|
|
|
235
267
|
}
|
|
236
268
|
}
|
|
237
269
|
|
|
238
|
-
if (!options.dryRun && !options.skipLogin && !bootstrapLoggedIn) {
|
|
239
|
-
await shell.login();
|
|
240
|
-
}
|
|
241
|
-
|
|
242
270
|
const failedPackages: string[] = [];
|
|
243
|
-
for (const pkg of
|
|
271
|
+
for (const pkg of selectedPackages) {
|
|
272
|
+
if (!options.dryRun) {
|
|
273
|
+
const trustedPublishers = await shell.trustedPublishers(pkg);
|
|
274
|
+
if (hasMatchingGithubTrustedPublisher(trustedPublishers, shell.repository, shell.workflow)) {
|
|
275
|
+
shell.log(`${pkg.name}: npm trusted publisher is already configured; skipping.`);
|
|
276
|
+
continue;
|
|
277
|
+
}
|
|
278
|
+
if (trustedPublishers.length > 0) {
|
|
279
|
+
throw new Error(
|
|
280
|
+
`${pkg.name}: npm trusted publisher exists but does not match GitHub Actions ${shell.repository}/${shell.workflow}. ` +
|
|
281
|
+
'Use npm trust list to inspect it and npm trust revoke --id <trust-id> before reconfiguring.',
|
|
282
|
+
);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
|
|
244
286
|
shell.log(`${pkg.name}: trusting GitHub Actions ${shell.repository}/${shell.workflow}`);
|
|
245
287
|
while (true) {
|
|
246
|
-
const otp = options.dryRun ? undefined : (options.otp ?? (await shell.promptOtp(pkg.name)));
|
|
247
288
|
try {
|
|
248
|
-
const result = await shell.trustPublisher(
|
|
249
|
-
pkg,
|
|
250
|
-
options.dryRun === true,
|
|
251
|
-
otp ? { NPM_CONFIG_OTP: otp } : undefined,
|
|
252
|
-
);
|
|
289
|
+
const result = await shell.trustPublisher(pkg, options.dryRun === true);
|
|
253
290
|
if (result === 'already-configured') {
|
|
254
291
|
shell.log(`${pkg.name}: npm trusted publisher is already configured; skipping.`);
|
|
255
292
|
}
|
|
@@ -258,9 +295,9 @@ export async function configureTrustedPublishers<Package extends ReleasePackageI
|
|
|
258
295
|
shell.error(`${pkg.name}: npm trusted publisher setup failed.`);
|
|
259
296
|
shell.error(error instanceof Error ? error.message : String(error));
|
|
260
297
|
if (!options.dryRun) {
|
|
261
|
-
shell.error('npm
|
|
298
|
+
shell.error('npm owns trusted publishing authentication. Complete the npm browser challenge, then retry.');
|
|
262
299
|
}
|
|
263
|
-
if (options.
|
|
300
|
+
if (options.dryRun) {
|
|
264
301
|
failedPackages.push(pkg.name);
|
|
265
302
|
break;
|
|
266
303
|
}
|
|
@@ -270,10 +307,44 @@ export async function configureTrustedPublishers<Package extends ReleasePackageI
|
|
|
270
307
|
}
|
|
271
308
|
if (failedPackages.length > 0) {
|
|
272
309
|
shell.error(`Trusted publishing was not configured for: ${failedPackages.join(', ')}`);
|
|
273
|
-
|
|
274
|
-
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function selectedTrustPublisherPackages<Package extends ReleasePackageInfo>(
|
|
314
|
+
packages: Package[],
|
|
315
|
+
selections: string[],
|
|
316
|
+
): Package[] {
|
|
317
|
+
if (selections.length === 0) {
|
|
318
|
+
return packages;
|
|
319
|
+
}
|
|
320
|
+
const byName = new Map(packages.map((pkg) => [pkg.name, pkg]));
|
|
321
|
+
const selected: Package[] = [];
|
|
322
|
+
const unknown: string[] = [];
|
|
323
|
+
for (const name of selections) {
|
|
324
|
+
const pkg = byName.get(name);
|
|
325
|
+
if (pkg) {
|
|
326
|
+
selected.push(pkg);
|
|
327
|
+
} else {
|
|
328
|
+
unknown.push(name);
|
|
275
329
|
}
|
|
276
330
|
}
|
|
331
|
+
if (unknown.length > 0) {
|
|
332
|
+
throw new Error(`Unknown owned release package selection: ${unknown.join(', ')}`);
|
|
333
|
+
}
|
|
334
|
+
return selected;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function hasMatchingGithubTrustedPublisher(
|
|
338
|
+
trustedPublishers: TrustedPublisher[],
|
|
339
|
+
repository: string,
|
|
340
|
+
workflow: string,
|
|
341
|
+
): boolean {
|
|
342
|
+
return trustedPublishers.some(
|
|
343
|
+
(trustedPublisher) =>
|
|
344
|
+
trustedPublisher.type === 'github' &&
|
|
345
|
+
trustedPublisher.repository === repository &&
|
|
346
|
+
trustedPublisher.file === workflow,
|
|
347
|
+
);
|
|
277
348
|
}
|
|
278
349
|
|
|
279
350
|
export async function releaseBootstrapNpmPackages(
|
|
@@ -325,6 +396,7 @@ interface ReleaseState {
|
|
|
325
396
|
}
|
|
326
397
|
|
|
327
398
|
type ReleasePackage = ReturnType<typeof listReleasePackages>[number];
|
|
399
|
+
const releasePreviewChangelogs = new Map<string, string>();
|
|
328
400
|
|
|
329
401
|
type ReleaseTarget = CoreReleaseTarget<ReleasePackage>;
|
|
330
402
|
type ReleaseTagRecord = CoreReleaseTagRecord<ReleasePackage>;
|
|
@@ -470,7 +542,16 @@ async function releaseVersionPackages(
|
|
|
470
542
|
);
|
|
471
543
|
}
|
|
472
544
|
|
|
473
|
-
async function runNxReleaseVersion(
|
|
545
|
+
async function runNxReleaseVersion(
|
|
546
|
+
root: string,
|
|
547
|
+
projects: string,
|
|
548
|
+
bump: string,
|
|
549
|
+
dryRun: boolean,
|
|
550
|
+
): Promise<ReleasePackage[]> {
|
|
551
|
+
if (dryRun) {
|
|
552
|
+
return runNxReleaseVersionPreview(root, projects, bump);
|
|
553
|
+
}
|
|
554
|
+
|
|
474
555
|
// Nx owns local release mutation: package versions, bun.lock updates, the
|
|
475
556
|
// release commit, and annotated tags. smoo owns remote publication after the
|
|
476
557
|
// workflow validates the exact release commit Nx produced.
|
|
@@ -479,10 +560,78 @@ async function runNxReleaseVersion(root: string, projects: string, bump: string,
|
|
|
479
560
|
nxArgs.push(bump);
|
|
480
561
|
}
|
|
481
562
|
nxArgs.push(`--projects=${projects}`, '--git-commit=true', '--git-tag=true', '--git-push=false');
|
|
482
|
-
if (dryRun) {
|
|
483
|
-
nxArgs.push('--dry-run');
|
|
484
|
-
}
|
|
485
563
|
await run('nx', nxArgs, root);
|
|
564
|
+
return [];
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
async function runNxReleaseVersionPreview(root: string, projects: string, bump: string): Promise<ReleasePackage[]> {
|
|
568
|
+
return withNxWorkspaceRoot(root, async () => {
|
|
569
|
+
const { createAPI } = await import('nx/src/command-line/release/version.js');
|
|
570
|
+
const { createAPI: createChangelogAPI } = await import('nx/src/command-line/release/changelog.js');
|
|
571
|
+
const projectNames = projects.split(',').filter(Boolean);
|
|
572
|
+
const result = await createAPI(
|
|
573
|
+
{},
|
|
574
|
+
false,
|
|
575
|
+
)({
|
|
576
|
+
specifier: bump === 'auto' ? undefined : bump,
|
|
577
|
+
projects: projectNames,
|
|
578
|
+
gitCommit: true,
|
|
579
|
+
gitTag: true,
|
|
580
|
+
gitPush: false,
|
|
581
|
+
dryRun: true,
|
|
582
|
+
});
|
|
583
|
+
const changelogResult = await createChangelogAPI(
|
|
584
|
+
{
|
|
585
|
+
changelog: {
|
|
586
|
+
workspaceChangelog: false,
|
|
587
|
+
projectChangelogs: { createRelease: false, file: false },
|
|
588
|
+
},
|
|
589
|
+
},
|
|
590
|
+
false,
|
|
591
|
+
)({
|
|
592
|
+
projects: projectNames,
|
|
593
|
+
versionData: result.projectsVersionData,
|
|
594
|
+
releaseGraph: result.releaseGraph,
|
|
595
|
+
gitCommit: false,
|
|
596
|
+
gitTag: false,
|
|
597
|
+
gitPush: false,
|
|
598
|
+
stageChanges: false,
|
|
599
|
+
createRelease: false,
|
|
600
|
+
dryRun: true,
|
|
601
|
+
});
|
|
602
|
+
releasePreviewChangelogs.clear();
|
|
603
|
+
for (const [projectName, changelog] of Object.entries(changelogResult.projectChangelogs ?? {})) {
|
|
604
|
+
releasePreviewChangelogs.set(projectName, changelog.contents);
|
|
605
|
+
}
|
|
606
|
+
const packagesByProject = new Map(releasePackages(root).map((pkg) => [pkg.projectName, pkg]));
|
|
607
|
+
return Object.entries(result.projectsVersionData)
|
|
608
|
+
.map(([projectName, data]) => {
|
|
609
|
+
const pkg = packagesByProject.get(projectName);
|
|
610
|
+
if (!pkg || !data.newVersion) {
|
|
611
|
+
return null;
|
|
612
|
+
}
|
|
613
|
+
return { ...pkg, version: data.newVersion };
|
|
614
|
+
})
|
|
615
|
+
.filter((pkg): pkg is ReleasePackage => pkg !== null);
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
async function runNxNextPrereleaseVersion(root: string, projects: string): Promise<void> {
|
|
620
|
+
await run(
|
|
621
|
+
'nx',
|
|
622
|
+
[
|
|
623
|
+
'release',
|
|
624
|
+
'version',
|
|
625
|
+
'prerelease',
|
|
626
|
+
`--projects=${projects}`,
|
|
627
|
+
'--preid=next',
|
|
628
|
+
'--git-commit=true',
|
|
629
|
+
'--git-tag=false',
|
|
630
|
+
'--git-push=false',
|
|
631
|
+
'--git-commit-message=chore(release): prepare next prerelease',
|
|
632
|
+
],
|
|
633
|
+
root,
|
|
634
|
+
);
|
|
486
635
|
}
|
|
487
636
|
|
|
488
637
|
async function releasePackagesAtHead(root: string, packages: ReleasePackage[]): Promise<ReleasePackage[]> {
|
|
@@ -567,7 +716,7 @@ async function gitTagsAtRef(root: string, ref: string): Promise<string[]> {
|
|
|
567
716
|
}
|
|
568
717
|
|
|
569
718
|
async function previousReleaseTag(root: string, pkg: ReleasePackage, currentTag: string): Promise<string | null> {
|
|
570
|
-
const result = await $`git tag --list ${`${pkg.
|
|
719
|
+
const result = await $`git tag --list ${`${pkg.projectName}@*`} --sort=-v:refname --merged ${currentTag}`
|
|
571
720
|
.cwd(root)
|
|
572
721
|
.quiet()
|
|
573
722
|
.nothrow();
|
|
@@ -620,6 +769,12 @@ async function pushReleaseRefs(root: string, packages: ReleasePackage[]): Promis
|
|
|
620
769
|
return true;
|
|
621
770
|
}
|
|
622
771
|
|
|
772
|
+
async function pushCurrentBranch(root: string): Promise<void> {
|
|
773
|
+
const branch = await releaseBranch(root);
|
|
774
|
+
const remote = await releaseRemote(root, branch);
|
|
775
|
+
await run('git', ['push', remote, `HEAD:refs/heads/${branch}`], root);
|
|
776
|
+
}
|
|
777
|
+
|
|
623
778
|
function releaseCompletionShell(root: string): ReleaseCompletionShell<ReleasePackage> {
|
|
624
779
|
return {
|
|
625
780
|
gitHead: () => gitHead(root),
|
|
@@ -638,6 +793,15 @@ function releaseCompletionShell(root: string): ReleaseCompletionShell<ReleasePac
|
|
|
638
793
|
};
|
|
639
794
|
}
|
|
640
795
|
|
|
796
|
+
function releaseNextShell(root: string): { bumpStablePackagesToNext(packages: ReleasePackage[]): Promise<void> } {
|
|
797
|
+
return {
|
|
798
|
+
bumpStablePackagesToNext: async (packages) => {
|
|
799
|
+
await runNxNextPrereleaseVersion(root, releasePackageProjects(packages));
|
|
800
|
+
await pushCurrentBranch(root);
|
|
801
|
+
},
|
|
802
|
+
};
|
|
803
|
+
}
|
|
804
|
+
|
|
641
805
|
function releaseRepairShell(root: string): ReleaseCompletionShell<ReleasePackage> & {
|
|
642
806
|
checkout(ref: string): Promise<void>;
|
|
643
807
|
withDevenvEnv<T>(runWithEnv: () => Promise<T>): Promise<T>;
|
|
@@ -692,11 +856,12 @@ async function listOwnedReleaseTagRecords(root: string, ref: string): Promise<Re
|
|
|
692
856
|
|
|
693
857
|
async function durableReleaseTagState(root: string, pkg: Pick<ReleasePackage, 'name' | 'version'>, tag: string) {
|
|
694
858
|
const packageVersion = `${pkg.name}@${pkg.version}`;
|
|
859
|
+
const githubReleaseTags = releaseTagAliases({ ...pkg, projectName: projectNameFromReleaseTag(tag) });
|
|
695
860
|
const start = Date.now();
|
|
696
|
-
console.log(`${packageVersion}: checking durable state (npm + GitHub Release ${
|
|
861
|
+
console.log(`${packageVersion}: checking durable state (npm + GitHub Release ${githubReleaseTags.join(' or ')}).`);
|
|
697
862
|
const [npmPublished, githubReleasePresent] = await Promise.all([
|
|
698
863
|
npmVersionExists(root, pkg.name, pkg.version),
|
|
699
|
-
|
|
864
|
+
anyGithubReleaseExists(root, githubReleaseTags),
|
|
700
865
|
]);
|
|
701
866
|
console.log(
|
|
702
867
|
`${packageVersion}: durable state npm=${yesNo(npmPublished)} github=${yesNo(githubReleasePresent)} (${Date.now() - start}ms).`,
|
|
@@ -704,6 +869,14 @@ async function durableReleaseTagState(root: string, pkg: Pick<ReleasePackage, 'n
|
|
|
704
869
|
return { npmPublished, githubReleaseExists: githubReleasePresent };
|
|
705
870
|
}
|
|
706
871
|
|
|
872
|
+
function projectNameFromReleaseTag(tag: string): string {
|
|
873
|
+
const versionSeparator = tag.lastIndexOf('@');
|
|
874
|
+
if (versionSeparator <= 0) {
|
|
875
|
+
return tag;
|
|
876
|
+
}
|
|
877
|
+
return tag.slice(0, versionSeparator);
|
|
878
|
+
}
|
|
879
|
+
|
|
707
880
|
async function gitReleaseTagsByCreatorDate(root: string): Promise<GitReleaseTagInfo[]> {
|
|
708
881
|
const result =
|
|
709
882
|
await $`git for-each-ref --sort=-creatordate --format=${'%(refname:short)%09%(creatordate:unix)%09%(*objectname)%09%(objectname)'} refs/tags`
|
|
@@ -738,7 +911,7 @@ async function listMissingGithubReleasePackages(root: string, packages: ReleaseP
|
|
|
738
911
|
).filter((pkg): pkg is ReleasePackage => pkg !== null);
|
|
739
912
|
}
|
|
740
913
|
|
|
741
|
-
async function createGithubRelease(root: string, pkg: ReleasePackage, dryRun: boolean): Promise<
|
|
914
|
+
async function createGithubRelease(root: string, pkg: ReleasePackage, dryRun: boolean): Promise<string | null> {
|
|
742
915
|
const currentTag = releaseTag(pkg);
|
|
743
916
|
console.log(`${pkg.name}@${pkg.version}: rendering GitHub Release notes for ${currentTag}.`);
|
|
744
917
|
console.log(`GitHub release auth: ${envPresence('GH_TOKEN')}, ${envPresence('GITHUB_TOKEN')}.`);
|
|
@@ -748,7 +921,7 @@ async function createGithubRelease(root: string, pkg: ReleasePackage, dryRun: bo
|
|
|
748
921
|
const previousTag = await previousReleaseTag(root, pkg, currentTag);
|
|
749
922
|
if (dryRun) {
|
|
750
923
|
await renderNxProjectChangelogContents({ root, pkg, previousTag, dryRun });
|
|
751
|
-
return;
|
|
924
|
+
return null;
|
|
752
925
|
}
|
|
753
926
|
const contents = await renderNxProjectChangelogContents({ root, pkg, previousTag, dryRun });
|
|
754
927
|
await createOrUpdateGithubRelease(pkg, contents, {
|
|
@@ -756,6 +929,7 @@ async function createGithubRelease(root: string, pkg: ReleasePackage, dryRun: bo
|
|
|
756
929
|
runGhRelease: (args) => run('gh', args, root),
|
|
757
930
|
log: (message) => console.log(message),
|
|
758
931
|
});
|
|
932
|
+
return githubReleaseUrl(root, currentTag);
|
|
759
933
|
}
|
|
760
934
|
|
|
761
935
|
function envPresence(name: string): string {
|
|
@@ -793,7 +967,7 @@ async function writeReleaseSummary(summary: ReleaseSummary<ReleasePackage>): Pro
|
|
|
793
967
|
lines.push(`- Git refs pushed: ${summary.pushed ? 'yes' : 'already current'}`);
|
|
794
968
|
lines.push(`- npm published: ${packageSummary(summary.published)}`);
|
|
795
969
|
lines.push(`- npm already published: ${packageSummary(summary.alreadyPublished)}`);
|
|
796
|
-
lines.push(`- GitHub Releases created/updated: ${
|
|
970
|
+
lines.push(`- GitHub Releases created/updated: ${githubReleaseSummary(summary)}`);
|
|
797
971
|
}
|
|
798
972
|
if (summary.rerunRequired) {
|
|
799
973
|
const message = 'A previous incomplete release was repaired; newer commits remain. Run Publish again.';
|
|
@@ -808,13 +982,45 @@ async function writeReleaseSummary(summary: ReleaseSummary<ReleasePackage>): Pro
|
|
|
808
982
|
}
|
|
809
983
|
}
|
|
810
984
|
|
|
985
|
+
async function writeReleasePreviewSummary(
|
|
986
|
+
root: string,
|
|
987
|
+
currentPackages: ReleasePackage[],
|
|
988
|
+
previewPackages: ReleasePackage[],
|
|
989
|
+
): Promise<void> {
|
|
990
|
+
const lines = ['## Release Preview', '', `- Commit: \`${(await gitHead(root)).slice(0, 12)}\``, '- Mode: dry run'];
|
|
991
|
+
if (previewPackages.length === 0) {
|
|
992
|
+
lines.push('- Result: no release needed');
|
|
993
|
+
} else {
|
|
994
|
+
lines.push('- Packages:');
|
|
995
|
+
const currentByName = new Map(currentPackages.map((pkg) => [pkg.name, pkg]));
|
|
996
|
+
for (const pkg of previewPackages) {
|
|
997
|
+
lines.push(` - \`${pkg.name}\`: \`${currentByName.get(pkg.name)?.version ?? 'unknown'}\` -> \`${pkg.version}\``);
|
|
998
|
+
}
|
|
999
|
+
for (const pkg of previewPackages) {
|
|
1000
|
+
const contents = releasePreviewChangelogs.get(pkg.projectName);
|
|
1001
|
+
if (!contents) {
|
|
1002
|
+
throw new Error(`Nx did not generate a project changelog for ${pkg.projectName}.`);
|
|
1003
|
+
}
|
|
1004
|
+
lines.push('', `### ${pkg.name} ${pkg.version}`, '', contents.trim());
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
const text = `${lines.join('\n')}\n`;
|
|
1008
|
+
console.log(text.trimEnd());
|
|
1009
|
+
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
1010
|
+
if (summaryPath) {
|
|
1011
|
+
await appendFile(summaryPath, `${text}\n`);
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
|
|
811
1015
|
async function writeRepairSummary(summaries: Array<ReleaseSummary<ReleasePackage>>, dryRun: boolean): Promise<void> {
|
|
812
1016
|
const lines = ['## Pending Release Repair', '', `- Mode: ${dryRun ? 'dry run' : 'release'}`];
|
|
813
1017
|
if (summaries.length === 0) {
|
|
814
1018
|
lines.push('- Result: no pending releases needed repair');
|
|
815
1019
|
} else {
|
|
816
1020
|
for (const summary of summaries) {
|
|
817
|
-
lines.push(
|
|
1021
|
+
lines.push(
|
|
1022
|
+
`- ${dryRun ? 'Would repair' : 'Repaired'} \`${summary.sha.slice(0, 12)}\`: ${packageSummary(summary.packages)}`,
|
|
1023
|
+
);
|
|
818
1024
|
}
|
|
819
1025
|
}
|
|
820
1026
|
const text = `${lines.join('\n')}\n`;
|
|
@@ -832,6 +1038,30 @@ function packageSummary(packages: ReleasePackage[]): string {
|
|
|
832
1038
|
return packages.map((pkg) => `${pkg.name}@${pkg.version}`).join(', ');
|
|
833
1039
|
}
|
|
834
1040
|
|
|
1041
|
+
function githubReleaseSummary(summary: ReleaseSummary<ReleasePackage>): string {
|
|
1042
|
+
if (summary.githubReleaseLinks.length > 0) {
|
|
1043
|
+
return summary.githubReleaseLinks.map(({ pkg, url }) => `[${pkg.name}@${pkg.version}](${url})`).join(', ');
|
|
1044
|
+
}
|
|
1045
|
+
return packageSummary(summary.githubReleases);
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
function repairTargetSummary(target: ReleaseTarget): string {
|
|
1049
|
+
return target.packages
|
|
1050
|
+
.map((pkg) => `${pkg.name}@${pkg.version} (${repairReasons(target, pkg).join(' + ')})`)
|
|
1051
|
+
.join(', ');
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function repairReasons(target: ReleaseTarget, pkg: ReleasePackage): string[] {
|
|
1055
|
+
const reasons: string[] = [];
|
|
1056
|
+
if (target.npmPackages.includes(pkg)) {
|
|
1057
|
+
reasons.push('npm missing');
|
|
1058
|
+
}
|
|
1059
|
+
if (target.githubPackages.includes(pkg)) {
|
|
1060
|
+
reasons.push('GitHub Release missing');
|
|
1061
|
+
}
|
|
1062
|
+
return reasons;
|
|
1063
|
+
}
|
|
1064
|
+
|
|
835
1065
|
async function fetchReleaseRefs(root: string, remote: string, branch: string): Promise<void> {
|
|
836
1066
|
await run('git', ['fetch', '--tags', remote, branch], root);
|
|
837
1067
|
}
|
|
@@ -1054,6 +1284,19 @@ async function githubReleaseExists(root: string, tag: string): Promise<boolean>
|
|
|
1054
1284
|
return (await runStatus('gh', ['release', 'view', tag, '--json', 'tagName'], root, true)) === 0;
|
|
1055
1285
|
}
|
|
1056
1286
|
|
|
1287
|
+
function githubReleaseUrl(root: string, tag: string): string {
|
|
1288
|
+
return `https://github.com/${githubRepositoryFromRootPackage(root)}/releases/tag/${encodeURIComponent(tag)}`;
|
|
1289
|
+
}
|
|
1290
|
+
|
|
1291
|
+
async function anyGithubReleaseExists(root: string, tags: string[]): Promise<boolean> {
|
|
1292
|
+
for (const tag of tags) {
|
|
1293
|
+
if (await githubReleaseExists(root, tag)) {
|
|
1294
|
+
return true;
|
|
1295
|
+
}
|
|
1296
|
+
}
|
|
1297
|
+
return false;
|
|
1298
|
+
}
|
|
1299
|
+
|
|
1057
1300
|
async function assertRemoteTagExists(root: string, tag: string): Promise<void> {
|
|
1058
1301
|
const branch = await releaseBranch(root);
|
|
1059
1302
|
const remote = await releaseRemote(root, branch);
|
|
@@ -1182,17 +1425,68 @@ async function runLatestNpmTrust(
|
|
|
1182
1425
|
npmArgs: string[],
|
|
1183
1426
|
env?: Record<string, string>,
|
|
1184
1427
|
): Promise<TrustPublisherResult> {
|
|
1185
|
-
const
|
|
1186
|
-
|
|
1428
|
+
const status = await runInteractiveStatus(
|
|
1429
|
+
'nix',
|
|
1430
|
+
['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...npmArgs],
|
|
1431
|
+
root,
|
|
1432
|
+
env,
|
|
1433
|
+
);
|
|
1434
|
+
if (status === 0) {
|
|
1187
1435
|
return 'configured';
|
|
1188
1436
|
}
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1437
|
+
throw new Error(`nix shell nixpkgs#nodejs_latest -c npm ${npmArgs.join(' ')} failed with exit code ${status}`);
|
|
1438
|
+
}
|
|
1439
|
+
|
|
1440
|
+
async function listTrustedPublishers(root: string, pkg: Pick<ReleasePackage, 'name'>): Promise<TrustedPublisher[]> {
|
|
1441
|
+
const args = ['trust', 'list', pkg.name, '--json'];
|
|
1442
|
+
let result = await runResult('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...args], root);
|
|
1443
|
+
if (result.exitCode !== 0 && /\bEOTP\b/.test(`${result.stdout}\n${result.stderr}`)) {
|
|
1444
|
+
const status = await runInteractiveStatus('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...args], root);
|
|
1445
|
+
if (status !== 0) {
|
|
1446
|
+
throw new Error(`nix shell nixpkgs#nodejs_latest -c npm ${args.join(' ')} failed with exit code ${status}`);
|
|
1447
|
+
}
|
|
1448
|
+
result = await runResult('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...args], root);
|
|
1192
1449
|
}
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1450
|
+
if (result.exitCode !== 0) {
|
|
1451
|
+
throw new Error(
|
|
1452
|
+
`nix shell nixpkgs#nodejs_latest -c npm ${args.join(' ')} failed with exit code ${result.exitCode}`,
|
|
1453
|
+
);
|
|
1454
|
+
}
|
|
1455
|
+
return parseTrustedPublishers(result.stdout, pkg.name);
|
|
1456
|
+
}
|
|
1457
|
+
|
|
1458
|
+
export function parseTrustedPublishers(stdout: string, packageName: string): TrustedPublisher[] {
|
|
1459
|
+
if (stdout.trim().length === 0) {
|
|
1460
|
+
return [];
|
|
1461
|
+
}
|
|
1462
|
+
const parsed = JSON.parse(stdout) as unknown;
|
|
1463
|
+
if (parsed === null) {
|
|
1464
|
+
return [];
|
|
1465
|
+
}
|
|
1466
|
+
if (Array.isArray(parsed)) {
|
|
1467
|
+
return parsed.map((value) => parseTrustedPublisher(value, packageName));
|
|
1468
|
+
}
|
|
1469
|
+
if (isRecord(parsed)) {
|
|
1470
|
+
return [parseTrustedPublisher(parsed, packageName)];
|
|
1471
|
+
}
|
|
1472
|
+
throw new Error(`${packageName}: npm trust list returned invalid JSON.`);
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
function parseTrustedPublisher(value: unknown, packageName: string): TrustedPublisher {
|
|
1476
|
+
if (!isRecord(value)) {
|
|
1477
|
+
throw new Error(`${packageName}: npm trust list returned invalid trusted publisher entry.`);
|
|
1478
|
+
}
|
|
1479
|
+
const id = stringProperty(value, 'id');
|
|
1480
|
+
const type = stringProperty(value, 'type');
|
|
1481
|
+
if (!id || !type) {
|
|
1482
|
+
throw new Error(`${packageName}: npm trust list returned a trusted publisher without id or type.`);
|
|
1483
|
+
}
|
|
1484
|
+
return {
|
|
1485
|
+
id,
|
|
1486
|
+
type,
|
|
1487
|
+
file: stringProperty(value, 'file') ?? undefined,
|
|
1488
|
+
repository: stringProperty(value, 'repository') ?? undefined,
|
|
1489
|
+
};
|
|
1196
1490
|
}
|
|
1197
1491
|
|
|
1198
1492
|
async function runLatestNpmPublish(root: string, npmArgs: string[]): Promise<void> {
|
|
@@ -1206,7 +1500,7 @@ function missingNpmPackagePublishGuidance(pkg: Pick<ReleasePackage, 'name'>): st
|
|
|
1206
1500
|
async function promptForNpmOtp(packageName: string): Promise<string> {
|
|
1207
1501
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
1208
1502
|
throw new Error(
|
|
1209
|
-
`npm
|
|
1503
|
+
`npm requires a one-time password for ${packageName}. Pass --otp <code> in non-interactive shells.`,
|
|
1210
1504
|
);
|
|
1211
1505
|
}
|
|
1212
1506
|
|