@smoothbricks/cli 0.2.0 → 0.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -24
- 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.d.ts +1 -0
- package/dist/release/candidates.d.ts.map +1 -1
- package/dist/release/candidates.js +12 -0
- 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 +287 -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/raw/tooling/git-hooks/commit-msg.sh +7 -1
- package/managed/raw/tooling/git-hooks/pre-commit.sh +7 -1
- 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 +254 -26
- 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 +109 -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 +13 -0
- package/src/release/core.ts +95 -10
- package/src/release/github-release.ts +1 -1
- package/src/release/index.ts +366 -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>;
|
|
@@ -459,6 +531,7 @@ async function releaseVersionPackages(
|
|
|
459
531
|
return autoReleaseCandidatePackages(
|
|
460
532
|
{
|
|
461
533
|
gitRefExists: (ref) => gitRefExists(root, ref),
|
|
534
|
+
latestStableReleaseRef: (projectName) => latestStableReleaseRef(root, projectName),
|
|
462
535
|
packageChangedFilesSince: (ref, packagePath) => packageChangedFilesSince(root, ref, packagePath),
|
|
463
536
|
packageJsonAtRef: (ref, packagePath) => packageJsonAtRef(root, ref, packagePath),
|
|
464
537
|
currentPackageJson: (packagePath) => currentPackageJson(root, packagePath),
|
|
@@ -470,7 +543,16 @@ async function releaseVersionPackages(
|
|
|
470
543
|
);
|
|
471
544
|
}
|
|
472
545
|
|
|
473
|
-
async function runNxReleaseVersion(
|
|
546
|
+
async function runNxReleaseVersion(
|
|
547
|
+
root: string,
|
|
548
|
+
projects: string,
|
|
549
|
+
bump: string,
|
|
550
|
+
dryRun: boolean,
|
|
551
|
+
): Promise<ReleasePackage[]> {
|
|
552
|
+
if (dryRun) {
|
|
553
|
+
return runNxReleaseVersionPreview(root, projects, bump);
|
|
554
|
+
}
|
|
555
|
+
|
|
474
556
|
// Nx owns local release mutation: package versions, bun.lock updates, the
|
|
475
557
|
// release commit, and annotated tags. smoo owns remote publication after the
|
|
476
558
|
// workflow validates the exact release commit Nx produced.
|
|
@@ -479,10 +561,78 @@ async function runNxReleaseVersion(root: string, projects: string, bump: string,
|
|
|
479
561
|
nxArgs.push(bump);
|
|
480
562
|
}
|
|
481
563
|
nxArgs.push(`--projects=${projects}`, '--git-commit=true', '--git-tag=true', '--git-push=false');
|
|
482
|
-
if (dryRun) {
|
|
483
|
-
nxArgs.push('--dry-run');
|
|
484
|
-
}
|
|
485
564
|
await run('nx', nxArgs, root);
|
|
565
|
+
return [];
|
|
566
|
+
}
|
|
567
|
+
|
|
568
|
+
async function runNxReleaseVersionPreview(root: string, projects: string, bump: string): Promise<ReleasePackage[]> {
|
|
569
|
+
return withNxWorkspaceRoot(root, async () => {
|
|
570
|
+
const { createAPI } = await import('nx/src/command-line/release/version.js');
|
|
571
|
+
const { createAPI: createChangelogAPI } = await import('nx/src/command-line/release/changelog.js');
|
|
572
|
+
const projectNames = projects.split(',').filter(Boolean);
|
|
573
|
+
const result = await createAPI(
|
|
574
|
+
{},
|
|
575
|
+
false,
|
|
576
|
+
)({
|
|
577
|
+
specifier: bump === 'auto' ? undefined : bump,
|
|
578
|
+
projects: projectNames,
|
|
579
|
+
gitCommit: true,
|
|
580
|
+
gitTag: true,
|
|
581
|
+
gitPush: false,
|
|
582
|
+
dryRun: true,
|
|
583
|
+
});
|
|
584
|
+
const changelogResult = await createChangelogAPI(
|
|
585
|
+
{
|
|
586
|
+
changelog: {
|
|
587
|
+
workspaceChangelog: false,
|
|
588
|
+
projectChangelogs: { createRelease: false, file: false },
|
|
589
|
+
},
|
|
590
|
+
},
|
|
591
|
+
false,
|
|
592
|
+
)({
|
|
593
|
+
projects: projectNames,
|
|
594
|
+
versionData: result.projectsVersionData,
|
|
595
|
+
releaseGraph: result.releaseGraph,
|
|
596
|
+
gitCommit: false,
|
|
597
|
+
gitTag: false,
|
|
598
|
+
gitPush: false,
|
|
599
|
+
stageChanges: false,
|
|
600
|
+
createRelease: false,
|
|
601
|
+
dryRun: true,
|
|
602
|
+
});
|
|
603
|
+
releasePreviewChangelogs.clear();
|
|
604
|
+
for (const [projectName, changelog] of Object.entries(changelogResult.projectChangelogs ?? {})) {
|
|
605
|
+
releasePreviewChangelogs.set(projectName, changelog.contents);
|
|
606
|
+
}
|
|
607
|
+
const packagesByProject = new Map(releasePackages(root).map((pkg) => [pkg.projectName, pkg]));
|
|
608
|
+
return Object.entries(result.projectsVersionData)
|
|
609
|
+
.map(([projectName, data]) => {
|
|
610
|
+
const pkg = packagesByProject.get(projectName);
|
|
611
|
+
if (!pkg || !data.newVersion) {
|
|
612
|
+
return null;
|
|
613
|
+
}
|
|
614
|
+
return { ...pkg, version: data.newVersion };
|
|
615
|
+
})
|
|
616
|
+
.filter((pkg): pkg is ReleasePackage => pkg !== null);
|
|
617
|
+
});
|
|
618
|
+
}
|
|
619
|
+
|
|
620
|
+
async function runNxNextPrereleaseVersion(root: string, projects: string): Promise<void> {
|
|
621
|
+
await run(
|
|
622
|
+
'nx',
|
|
623
|
+
[
|
|
624
|
+
'release',
|
|
625
|
+
'version',
|
|
626
|
+
'prerelease',
|
|
627
|
+
`--projects=${projects}`,
|
|
628
|
+
'--preid=next',
|
|
629
|
+
'--git-commit=true',
|
|
630
|
+
'--git-tag=false',
|
|
631
|
+
'--git-push=false',
|
|
632
|
+
'--git-commit-message=chore(release): prepare next prerelease',
|
|
633
|
+
],
|
|
634
|
+
root,
|
|
635
|
+
);
|
|
486
636
|
}
|
|
487
637
|
|
|
488
638
|
async function releasePackagesAtHead(root: string, packages: ReleasePackage[]): Promise<ReleasePackage[]> {
|
|
@@ -567,7 +717,7 @@ async function gitTagsAtRef(root: string, ref: string): Promise<string[]> {
|
|
|
567
717
|
}
|
|
568
718
|
|
|
569
719
|
async function previousReleaseTag(root: string, pkg: ReleasePackage, currentTag: string): Promise<string | null> {
|
|
570
|
-
const result = await $`git tag --list ${`${pkg.
|
|
720
|
+
const result = await $`git tag --list ${`${pkg.projectName}@*`} --sort=-v:refname --merged ${currentTag}`
|
|
571
721
|
.cwd(root)
|
|
572
722
|
.quiet()
|
|
573
723
|
.nothrow();
|
|
@@ -620,6 +770,12 @@ async function pushReleaseRefs(root: string, packages: ReleasePackage[]): Promis
|
|
|
620
770
|
return true;
|
|
621
771
|
}
|
|
622
772
|
|
|
773
|
+
async function pushCurrentBranch(root: string): Promise<void> {
|
|
774
|
+
const branch = await releaseBranch(root);
|
|
775
|
+
const remote = await releaseRemote(root, branch);
|
|
776
|
+
await run('git', ['push', remote, `HEAD:refs/heads/${branch}`], root);
|
|
777
|
+
}
|
|
778
|
+
|
|
623
779
|
function releaseCompletionShell(root: string): ReleaseCompletionShell<ReleasePackage> {
|
|
624
780
|
return {
|
|
625
781
|
gitHead: () => gitHead(root),
|
|
@@ -638,6 +794,15 @@ function releaseCompletionShell(root: string): ReleaseCompletionShell<ReleasePac
|
|
|
638
794
|
};
|
|
639
795
|
}
|
|
640
796
|
|
|
797
|
+
function releaseNextShell(root: string): { bumpStablePackagesToNext(packages: ReleasePackage[]): Promise<void> } {
|
|
798
|
+
return {
|
|
799
|
+
bumpStablePackagesToNext: async (packages) => {
|
|
800
|
+
await runNxNextPrereleaseVersion(root, releasePackageProjects(packages));
|
|
801
|
+
await pushCurrentBranch(root);
|
|
802
|
+
},
|
|
803
|
+
};
|
|
804
|
+
}
|
|
805
|
+
|
|
641
806
|
function releaseRepairShell(root: string): ReleaseCompletionShell<ReleasePackage> & {
|
|
642
807
|
checkout(ref: string): Promise<void>;
|
|
643
808
|
withDevenvEnv<T>(runWithEnv: () => Promise<T>): Promise<T>;
|
|
@@ -692,11 +857,12 @@ async function listOwnedReleaseTagRecords(root: string, ref: string): Promise<Re
|
|
|
692
857
|
|
|
693
858
|
async function durableReleaseTagState(root: string, pkg: Pick<ReleasePackage, 'name' | 'version'>, tag: string) {
|
|
694
859
|
const packageVersion = `${pkg.name}@${pkg.version}`;
|
|
860
|
+
const githubReleaseTags = releaseTagAliases({ ...pkg, projectName: projectNameFromReleaseTag(tag) });
|
|
695
861
|
const start = Date.now();
|
|
696
|
-
console.log(`${packageVersion}: checking durable state (npm + GitHub Release ${
|
|
862
|
+
console.log(`${packageVersion}: checking durable state (npm + GitHub Release ${githubReleaseTags.join(' or ')}).`);
|
|
697
863
|
const [npmPublished, githubReleasePresent] = await Promise.all([
|
|
698
864
|
npmVersionExists(root, pkg.name, pkg.version),
|
|
699
|
-
|
|
865
|
+
anyGithubReleaseExists(root, githubReleaseTags),
|
|
700
866
|
]);
|
|
701
867
|
console.log(
|
|
702
868
|
`${packageVersion}: durable state npm=${yesNo(npmPublished)} github=${yesNo(githubReleasePresent)} (${Date.now() - start}ms).`,
|
|
@@ -704,6 +870,14 @@ async function durableReleaseTagState(root: string, pkg: Pick<ReleasePackage, 'n
|
|
|
704
870
|
return { npmPublished, githubReleaseExists: githubReleasePresent };
|
|
705
871
|
}
|
|
706
872
|
|
|
873
|
+
function projectNameFromReleaseTag(tag: string): string {
|
|
874
|
+
const versionSeparator = tag.lastIndexOf('@');
|
|
875
|
+
if (versionSeparator <= 0) {
|
|
876
|
+
return tag;
|
|
877
|
+
}
|
|
878
|
+
return tag.slice(0, versionSeparator);
|
|
879
|
+
}
|
|
880
|
+
|
|
707
881
|
async function gitReleaseTagsByCreatorDate(root: string): Promise<GitReleaseTagInfo[]> {
|
|
708
882
|
const result =
|
|
709
883
|
await $`git for-each-ref --sort=-creatordate --format=${'%(refname:short)%09%(creatordate:unix)%09%(*objectname)%09%(objectname)'} refs/tags`
|
|
@@ -738,7 +912,7 @@ async function listMissingGithubReleasePackages(root: string, packages: ReleaseP
|
|
|
738
912
|
).filter((pkg): pkg is ReleasePackage => pkg !== null);
|
|
739
913
|
}
|
|
740
914
|
|
|
741
|
-
async function createGithubRelease(root: string, pkg: ReleasePackage, dryRun: boolean): Promise<
|
|
915
|
+
async function createGithubRelease(root: string, pkg: ReleasePackage, dryRun: boolean): Promise<string | null> {
|
|
742
916
|
const currentTag = releaseTag(pkg);
|
|
743
917
|
console.log(`${pkg.name}@${pkg.version}: rendering GitHub Release notes for ${currentTag}.`);
|
|
744
918
|
console.log(`GitHub release auth: ${envPresence('GH_TOKEN')}, ${envPresence('GITHUB_TOKEN')}.`);
|
|
@@ -748,7 +922,7 @@ async function createGithubRelease(root: string, pkg: ReleasePackage, dryRun: bo
|
|
|
748
922
|
const previousTag = await previousReleaseTag(root, pkg, currentTag);
|
|
749
923
|
if (dryRun) {
|
|
750
924
|
await renderNxProjectChangelogContents({ root, pkg, previousTag, dryRun });
|
|
751
|
-
return;
|
|
925
|
+
return null;
|
|
752
926
|
}
|
|
753
927
|
const contents = await renderNxProjectChangelogContents({ root, pkg, previousTag, dryRun });
|
|
754
928
|
await createOrUpdateGithubRelease(pkg, contents, {
|
|
@@ -756,6 +930,7 @@ async function createGithubRelease(root: string, pkg: ReleasePackage, dryRun: bo
|
|
|
756
930
|
runGhRelease: (args) => run('gh', args, root),
|
|
757
931
|
log: (message) => console.log(message),
|
|
758
932
|
});
|
|
933
|
+
return githubReleaseUrl(root, currentTag);
|
|
759
934
|
}
|
|
760
935
|
|
|
761
936
|
function envPresence(name: string): string {
|
|
@@ -793,7 +968,7 @@ async function writeReleaseSummary(summary: ReleaseSummary<ReleasePackage>): Pro
|
|
|
793
968
|
lines.push(`- Git refs pushed: ${summary.pushed ? 'yes' : 'already current'}`);
|
|
794
969
|
lines.push(`- npm published: ${packageSummary(summary.published)}`);
|
|
795
970
|
lines.push(`- npm already published: ${packageSummary(summary.alreadyPublished)}`);
|
|
796
|
-
lines.push(`- GitHub Releases created/updated: ${
|
|
971
|
+
lines.push(`- GitHub Releases created/updated: ${githubReleaseSummary(summary)}`);
|
|
797
972
|
}
|
|
798
973
|
if (summary.rerunRequired) {
|
|
799
974
|
const message = 'A previous incomplete release was repaired; newer commits remain. Run Publish again.';
|
|
@@ -808,13 +983,45 @@ async function writeReleaseSummary(summary: ReleaseSummary<ReleasePackage>): Pro
|
|
|
808
983
|
}
|
|
809
984
|
}
|
|
810
985
|
|
|
986
|
+
async function writeReleasePreviewSummary(
|
|
987
|
+
root: string,
|
|
988
|
+
currentPackages: ReleasePackage[],
|
|
989
|
+
previewPackages: ReleasePackage[],
|
|
990
|
+
): Promise<void> {
|
|
991
|
+
const lines = ['## Release Preview', '', `- Commit: \`${(await gitHead(root)).slice(0, 12)}\``, '- Mode: dry run'];
|
|
992
|
+
if (previewPackages.length === 0) {
|
|
993
|
+
lines.push('- Result: no release needed');
|
|
994
|
+
} else {
|
|
995
|
+
lines.push('- Packages:');
|
|
996
|
+
const currentByName = new Map(currentPackages.map((pkg) => [pkg.name, pkg]));
|
|
997
|
+
for (const pkg of previewPackages) {
|
|
998
|
+
lines.push(` - \`${pkg.name}\`: \`${currentByName.get(pkg.name)?.version ?? 'unknown'}\` -> \`${pkg.version}\``);
|
|
999
|
+
}
|
|
1000
|
+
for (const pkg of previewPackages) {
|
|
1001
|
+
const contents = releasePreviewChangelogs.get(pkg.projectName);
|
|
1002
|
+
if (!contents) {
|
|
1003
|
+
throw new Error(`Nx did not generate a project changelog for ${pkg.projectName}.`);
|
|
1004
|
+
}
|
|
1005
|
+
lines.push('', `### ${pkg.name} ${pkg.version}`, '', contents.trim());
|
|
1006
|
+
}
|
|
1007
|
+
}
|
|
1008
|
+
const text = `${lines.join('\n')}\n`;
|
|
1009
|
+
console.log(text.trimEnd());
|
|
1010
|
+
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
1011
|
+
if (summaryPath) {
|
|
1012
|
+
await appendFile(summaryPath, `${text}\n`);
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
|
|
811
1016
|
async function writeRepairSummary(summaries: Array<ReleaseSummary<ReleasePackage>>, dryRun: boolean): Promise<void> {
|
|
812
1017
|
const lines = ['## Pending Release Repair', '', `- Mode: ${dryRun ? 'dry run' : 'release'}`];
|
|
813
1018
|
if (summaries.length === 0) {
|
|
814
1019
|
lines.push('- Result: no pending releases needed repair');
|
|
815
1020
|
} else {
|
|
816
1021
|
for (const summary of summaries) {
|
|
817
|
-
lines.push(
|
|
1022
|
+
lines.push(
|
|
1023
|
+
`- ${dryRun ? 'Would repair' : 'Repaired'} \`${summary.sha.slice(0, 12)}\`: ${packageSummary(summary.packages)}`,
|
|
1024
|
+
);
|
|
818
1025
|
}
|
|
819
1026
|
}
|
|
820
1027
|
const text = `${lines.join('\n')}\n`;
|
|
@@ -832,6 +1039,30 @@ function packageSummary(packages: ReleasePackage[]): string {
|
|
|
832
1039
|
return packages.map((pkg) => `${pkg.name}@${pkg.version}`).join(', ');
|
|
833
1040
|
}
|
|
834
1041
|
|
|
1042
|
+
function githubReleaseSummary(summary: ReleaseSummary<ReleasePackage>): string {
|
|
1043
|
+
if (summary.githubReleaseLinks.length > 0) {
|
|
1044
|
+
return summary.githubReleaseLinks.map(({ pkg, url }) => `[${pkg.name}@${pkg.version}](${url})`).join(', ');
|
|
1045
|
+
}
|
|
1046
|
+
return packageSummary(summary.githubReleases);
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
function repairTargetSummary(target: ReleaseTarget): string {
|
|
1050
|
+
return target.packages
|
|
1051
|
+
.map((pkg) => `${pkg.name}@${pkg.version} (${repairReasons(target, pkg).join(' + ')})`)
|
|
1052
|
+
.join(', ');
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
function repairReasons(target: ReleaseTarget, pkg: ReleasePackage): string[] {
|
|
1056
|
+
const reasons: string[] = [];
|
|
1057
|
+
if (target.npmPackages.includes(pkg)) {
|
|
1058
|
+
reasons.push('npm missing');
|
|
1059
|
+
}
|
|
1060
|
+
if (target.githubPackages.includes(pkg)) {
|
|
1061
|
+
reasons.push('GitHub Release missing');
|
|
1062
|
+
}
|
|
1063
|
+
return reasons;
|
|
1064
|
+
}
|
|
1065
|
+
|
|
835
1066
|
async function fetchReleaseRefs(root: string, remote: string, branch: string): Promise<void> {
|
|
836
1067
|
await run('git', ['fetch', '--tags', remote, branch], root);
|
|
837
1068
|
}
|
|
@@ -841,6 +1072,28 @@ async function gitRefExists(root: string, ref: string): Promise<boolean> {
|
|
|
841
1072
|
return result.exitCode === 0;
|
|
842
1073
|
}
|
|
843
1074
|
|
|
1075
|
+
async function latestStableReleaseRef(root: string, projectName: string): Promise<string | null> {
|
|
1076
|
+
// List tags matching <projectName>@* sorted newest-first by version, then
|
|
1077
|
+
// return the first non-prerelease tag as a full ref.
|
|
1078
|
+
const pattern = `${projectName}@*`;
|
|
1079
|
+
const result = await $`git tag --list ${pattern} --sort=-v:refname`.cwd(root).quiet().nothrow();
|
|
1080
|
+
if (result.exitCode !== 0) {
|
|
1081
|
+
return null;
|
|
1082
|
+
}
|
|
1083
|
+
const tags = decode(result.stdout)
|
|
1084
|
+
.split('\n')
|
|
1085
|
+
.map((t) => t.trim())
|
|
1086
|
+
.filter(Boolean);
|
|
1087
|
+
const prefix = `${projectName}@`;
|
|
1088
|
+
for (const tagName of tags) {
|
|
1089
|
+
const version = tagName.slice(prefix.length);
|
|
1090
|
+
if (version && !version.includes('-')) {
|
|
1091
|
+
return `refs/tags/${tagName}`;
|
|
1092
|
+
}
|
|
1093
|
+
}
|
|
1094
|
+
return null;
|
|
1095
|
+
}
|
|
1096
|
+
|
|
844
1097
|
async function packageChangedFilesSince(root: string, ref: string, packagePath: string): Promise<string[]> {
|
|
845
1098
|
const result = await $`git diff --name-only ${`${ref}..HEAD`} -- ${packagePath}`.cwd(root).quiet().nothrow();
|
|
846
1099
|
if (result.exitCode !== 0) {
|
|
@@ -1054,6 +1307,19 @@ async function githubReleaseExists(root: string, tag: string): Promise<boolean>
|
|
|
1054
1307
|
return (await runStatus('gh', ['release', 'view', tag, '--json', 'tagName'], root, true)) === 0;
|
|
1055
1308
|
}
|
|
1056
1309
|
|
|
1310
|
+
function githubReleaseUrl(root: string, tag: string): string {
|
|
1311
|
+
return `https://github.com/${githubRepositoryFromRootPackage(root)}/releases/tag/${encodeURIComponent(tag)}`;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
async function anyGithubReleaseExists(root: string, tags: string[]): Promise<boolean> {
|
|
1315
|
+
for (const tag of tags) {
|
|
1316
|
+
if (await githubReleaseExists(root, tag)) {
|
|
1317
|
+
return true;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
return false;
|
|
1321
|
+
}
|
|
1322
|
+
|
|
1057
1323
|
async function assertRemoteTagExists(root: string, tag: string): Promise<void> {
|
|
1058
1324
|
const branch = await releaseBranch(root);
|
|
1059
1325
|
const remote = await releaseRemote(root, branch);
|
|
@@ -1182,17 +1448,68 @@ async function runLatestNpmTrust(
|
|
|
1182
1448
|
npmArgs: string[],
|
|
1183
1449
|
env?: Record<string, string>,
|
|
1184
1450
|
): Promise<TrustPublisherResult> {
|
|
1185
|
-
const
|
|
1186
|
-
|
|
1451
|
+
const status = await runInteractiveStatus(
|
|
1452
|
+
'nix',
|
|
1453
|
+
['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...npmArgs],
|
|
1454
|
+
root,
|
|
1455
|
+
env,
|
|
1456
|
+
);
|
|
1457
|
+
if (status === 0) {
|
|
1187
1458
|
return 'configured';
|
|
1188
1459
|
}
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1460
|
+
throw new Error(`nix shell nixpkgs#nodejs_latest -c npm ${npmArgs.join(' ')} failed with exit code ${status}`);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
async function listTrustedPublishers(root: string, pkg: Pick<ReleasePackage, 'name'>): Promise<TrustedPublisher[]> {
|
|
1464
|
+
const args = ['trust', 'list', pkg.name, '--json'];
|
|
1465
|
+
let result = await runResult('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...args], root);
|
|
1466
|
+
if (result.exitCode !== 0 && /\bEOTP\b/.test(`${result.stdout}\n${result.stderr}`)) {
|
|
1467
|
+
const status = await runInteractiveStatus('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...args], root);
|
|
1468
|
+
if (status !== 0) {
|
|
1469
|
+
throw new Error(`nix shell nixpkgs#nodejs_latest -c npm ${args.join(' ')} failed with exit code ${status}`);
|
|
1470
|
+
}
|
|
1471
|
+
result = await runResult('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...args], root);
|
|
1192
1472
|
}
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1473
|
+
if (result.exitCode !== 0) {
|
|
1474
|
+
throw new Error(
|
|
1475
|
+
`nix shell nixpkgs#nodejs_latest -c npm ${args.join(' ')} failed with exit code ${result.exitCode}`,
|
|
1476
|
+
);
|
|
1477
|
+
}
|
|
1478
|
+
return parseTrustedPublishers(result.stdout, pkg.name);
|
|
1479
|
+
}
|
|
1480
|
+
|
|
1481
|
+
export function parseTrustedPublishers(stdout: string, packageName: string): TrustedPublisher[] {
|
|
1482
|
+
if (stdout.trim().length === 0) {
|
|
1483
|
+
return [];
|
|
1484
|
+
}
|
|
1485
|
+
const parsed = JSON.parse(stdout) as unknown;
|
|
1486
|
+
if (parsed === null) {
|
|
1487
|
+
return [];
|
|
1488
|
+
}
|
|
1489
|
+
if (Array.isArray(parsed)) {
|
|
1490
|
+
return parsed.map((value) => parseTrustedPublisher(value, packageName));
|
|
1491
|
+
}
|
|
1492
|
+
if (isRecord(parsed)) {
|
|
1493
|
+
return [parseTrustedPublisher(parsed, packageName)];
|
|
1494
|
+
}
|
|
1495
|
+
throw new Error(`${packageName}: npm trust list returned invalid JSON.`);
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
function parseTrustedPublisher(value: unknown, packageName: string): TrustedPublisher {
|
|
1499
|
+
if (!isRecord(value)) {
|
|
1500
|
+
throw new Error(`${packageName}: npm trust list returned invalid trusted publisher entry.`);
|
|
1501
|
+
}
|
|
1502
|
+
const id = stringProperty(value, 'id');
|
|
1503
|
+
const type = stringProperty(value, 'type');
|
|
1504
|
+
if (!id || !type) {
|
|
1505
|
+
throw new Error(`${packageName}: npm trust list returned a trusted publisher without id or type.`);
|
|
1506
|
+
}
|
|
1507
|
+
return {
|
|
1508
|
+
id,
|
|
1509
|
+
type,
|
|
1510
|
+
file: stringProperty(value, 'file') ?? undefined,
|
|
1511
|
+
repository: stringProperty(value, 'repository') ?? undefined,
|
|
1512
|
+
};
|
|
1196
1513
|
}
|
|
1197
1514
|
|
|
1198
1515
|
async function runLatestNpmPublish(root: string, npmArgs: string[]): Promise<void> {
|
|
@@ -1206,7 +1523,7 @@ function missingNpmPackagePublishGuidance(pkg: Pick<ReleasePackage, 'name'>): st
|
|
|
1206
1523
|
async function promptForNpmOtp(packageName: string): Promise<string> {
|
|
1207
1524
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
1208
1525
|
throw new Error(
|
|
1209
|
-
`npm
|
|
1526
|
+
`npm requires a one-time password for ${packageName}. Pass --otp <code> in non-interactive shells.`,
|
|
1210
1527
|
);
|
|
1211
1528
|
}
|
|
1212
1529
|
|