@smoothbricks/cli 0.1.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +161 -32
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +110 -10
- package/dist/devenv/index.d.ts +10 -0
- package/dist/devenv/index.d.ts.map +1 -0
- package/dist/devenv/index.js +27 -0
- package/dist/generate/index.d.ts +20 -0
- package/dist/generate/index.d.ts.map +1 -0
- package/dist/generate/index.js +47 -0
- package/dist/lib/cli-package.d.ts +4 -0
- package/dist/lib/cli-package.d.ts.map +1 -0
- package/dist/lib/cli-package.js +15 -0
- package/dist/lib/run.d.ts +6 -0
- package/dist/lib/run.d.ts.map +1 -1
- package/dist/lib/run.js +32 -0
- package/dist/lib/workspace.d.ts +25 -1
- package/dist/lib/workspace.d.ts.map +1 -1
- package/dist/lib/workspace.js +79 -17
- package/dist/monorepo/commit-msg.d.ts +8 -1
- package/dist/monorepo/commit-msg.d.ts.map +1 -1
- package/dist/monorepo/commit-msg.js +85 -7
- package/dist/monorepo/index.d.ts +19 -2
- package/dist/monorepo/index.d.ts.map +1 -1
- package/dist/monorepo/index.js +104 -16
- package/dist/monorepo/lockfile.d.ts.map +1 -1
- package/dist/monorepo/lockfile.js +9 -0
- package/dist/monorepo/managed-files.js +1 -1
- package/dist/monorepo/nx-sync.d.ts +2 -1
- package/dist/monorepo/nx-sync.d.ts.map +1 -1
- package/dist/monorepo/nx-sync.js +8 -2
- package/dist/monorepo/package-hygiene.d.ts +6 -2
- package/dist/monorepo/package-hygiene.d.ts.map +1 -1
- package/dist/monorepo/package-hygiene.js +12 -4
- package/dist/monorepo/package-policy.d.ts +13 -4
- package/dist/monorepo/package-policy.d.ts.map +1 -1
- package/dist/monorepo/package-policy.js +208 -113
- package/dist/monorepo/packed-package.d.ts +3 -0
- package/dist/monorepo/packed-package.d.ts.map +1 -1
- package/dist/monorepo/packed-package.js +68 -36
- package/dist/monorepo/packs/index.d.ts +20 -1
- package/dist/monorepo/packs/index.d.ts.map +1 -1
- package/dist/monorepo/packs/index.js +238 -19
- package/dist/monorepo/publish-workflow.d.ts +0 -4
- package/dist/monorepo/publish-workflow.d.ts.map +1 -1
- package/dist/monorepo/publish-workflow.js +7 -15
- package/dist/monorepo/tool-validation.d.ts +22 -0
- package/dist/monorepo/tool-validation.d.ts.map +1 -0
- package/dist/monorepo/tool-validation.js +399 -0
- package/dist/nx/index.d.ts +25 -0
- package/dist/nx/index.d.ts.map +1 -0
- package/dist/nx/index.js +112 -0
- package/dist/release/bootstrap-npm-packages.d.ts +19 -0
- package/dist/release/bootstrap-npm-packages.d.ts.map +1 -0
- package/dist/release/bootstrap-npm-packages.js +58 -0
- package/dist/release/candidates.d.ts +11 -0
- package/dist/release/candidates.d.ts.map +1 -0
- package/dist/release/candidates.js +181 -0
- package/dist/release/core.d.ts +4 -1
- package/dist/release/core.d.ts.map +1 -1
- package/dist/release/core.js +76 -9
- package/dist/release/github-release.d.ts +1 -0
- package/dist/release/github-release.d.ts.map +1 -1
- package/dist/release/github-release.js +3 -3
- package/dist/release/index.d.ts +32 -1
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +488 -64
- package/dist/release/npm-auth.d.ts +0 -1
- package/dist/release/npm-auth.d.ts.map +1 -1
- package/dist/release/npm-auth.js +4 -16
- package/dist/release/orchestration.d.ts +11 -2
- package/dist/release/orchestration.d.ts.map +1 -1
- package/dist/release/orchestration.js +41 -3
- package/dist/release/publish-plan.js +1 -1
- package/managed/raw/git-format-staged.yml +54 -0
- package/managed/raw/tooling/git-hooks/commit-msg.sh +1 -1
- package/managed/templates/github/actions/setup-devenv/action.yml +1 -7
- package/managed/templates/github/workflows/ci.yml +4 -2
- package/package.json +15 -8
- package/src/cli.ts +153 -22
- package/src/devenv/index.test.ts +49 -0
- package/src/devenv/index.ts +38 -0
- package/src/generate/index.ts +92 -0
- package/src/lib/cli-package.ts +18 -0
- package/src/lib/run.ts +44 -0
- package/src/lib/workspace.ts +112 -17
- package/src/monorepo/__tests__/publish-workflow.test.ts +16 -9
- package/src/monorepo/commit-msg.test.ts +45 -0
- package/src/monorepo/commit-msg.ts +107 -7
- package/src/monorepo/index.ts +142 -16
- package/src/monorepo/lockfile.ts +9 -0
- package/src/monorepo/managed-files.ts +1 -1
- package/src/monorepo/nx-sync.ts +9 -2
- package/src/monorepo/package-hygiene.test.ts +34 -0
- package/src/monorepo/package-hygiene.ts +26 -4
- package/src/monorepo/package-policy.test.ts +1130 -0
- package/src/monorepo/package-policy.ts +237 -114
- package/src/monorepo/packed-package.ts +89 -37
- package/src/monorepo/packs/index.test.ts +189 -0
- package/src/monorepo/packs/index.ts +311 -20
- package/src/monorepo/publish-workflow.ts +10 -19
- package/src/monorepo/setup-test-tracing.test.ts +120 -0
- package/src/monorepo/tool-validation.test.ts +261 -0
- package/src/monorepo/tool-validation.ts +462 -0
- package/src/nx/index.test.ts +73 -0
- package/src/nx/index.ts +139 -0
- package/src/release/__tests__/bootstrap-npm-packages.test.ts +127 -0
- package/src/release/__tests__/candidates.test.ts +262 -0
- package/src/release/__tests__/core-properties.test.ts +6 -6
- package/src/release/__tests__/core-scenarios.test.ts +27 -20
- package/src/release/__tests__/core.test.ts +52 -5
- package/src/release/__tests__/fixture-repo.test.ts +90 -31
- package/src/release/__tests__/github-release.test.ts +22 -24
- package/src/release/__tests__/helpers/fixture-repo.ts +1 -0
- package/src/release/__tests__/npm-auth.test.ts +12 -21
- package/src/release/__tests__/orchestration.test.ts +142 -12
- package/src/release/__tests__/publish-plan.test.ts +9 -3
- package/src/release/__tests__/retag-unpublished.test.ts +10 -10
- package/src/release/__tests__/trust-publisher.test.ts +165 -0
- package/src/release/bootstrap-npm-packages.ts +88 -0
- package/src/release/candidates.ts +225 -0
- package/src/release/core.ts +96 -10
- package/src/release/github-release.ts +3 -3
- package/src/release/index.ts +658 -74
- package/src/release/npm-auth.ts +4 -33
- package/src/release/orchestration.ts +60 -5
- package/src/release/publish-plan.ts +1 -1
- package/dist/nx-version-actions.cjs +0 -25
- package/dist/nx-version-actions.d.cts +0 -6
- package/dist/nx-version-actions.d.cts.map +0 -1
- package/managed/raw/.git-format-staged.yml +0 -47
- package/src/nx-version-actions.cts +0 -36
package/src/release/index.ts
CHANGED
|
@@ -5,21 +5,35 @@ import { createInterface } from 'node:readline/promises';
|
|
|
5
5
|
import { Writable } from 'node:stream';
|
|
6
6
|
import { $ } from 'bun';
|
|
7
7
|
import { withDevenvEnv } from '../lib/devenv.js';
|
|
8
|
-
import { isRecord, stringProperty } from '../lib/json.js';
|
|
9
|
-
import { decode, run, runStatus } from '../lib/run.js';
|
|
8
|
+
import { isRecord, readJsonObject, stringProperty } from '../lib/json.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
|
+
import {
|
|
13
|
+
type BootstrapNpmPackagesOptions,
|
|
14
|
+
bootstrapNpmPackages,
|
|
15
|
+
NPM_BOOTSTRAP_DIST_TAG,
|
|
16
|
+
NPM_BOOTSTRAP_VERSION,
|
|
17
|
+
} from './bootstrap-npm-packages.js';
|
|
18
|
+
import { autoReleaseCandidatePackages } from './candidates.js';
|
|
12
19
|
import {
|
|
13
20
|
type ReleaseTagRecord as CoreReleaseTagRecord,
|
|
14
21
|
type ReleaseTarget as CoreReleaseTarget,
|
|
15
22
|
collectOwnedReleaseTagRecords,
|
|
16
23
|
type GitReleaseTagInfo,
|
|
17
24
|
pendingReleaseTargets,
|
|
25
|
+
type ReleasePackageInfo,
|
|
18
26
|
releaseTag,
|
|
27
|
+
releaseTagAliases,
|
|
19
28
|
} from './core.js';
|
|
20
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
createOrUpdateGithubRelease,
|
|
31
|
+
renderNxProjectChangelogContents,
|
|
32
|
+
withNxWorkspaceRoot,
|
|
33
|
+
} from './github-release.js';
|
|
21
34
|
import { publishWithAuthDiagnostics } from './npm-auth.js';
|
|
22
35
|
import {
|
|
36
|
+
bumpStableReleaseToNext,
|
|
23
37
|
completeReleaseAtHead as completeReleaseAtHeadWithShell,
|
|
24
38
|
type ReleaseCompletionShell,
|
|
25
39
|
type ReleaseSummary,
|
|
@@ -46,8 +60,17 @@ export interface ReleaseRepairPendingOptions {
|
|
|
46
60
|
|
|
47
61
|
export interface ReleaseTrustPublisherOptions {
|
|
48
62
|
dryRun?: boolean;
|
|
49
|
-
|
|
63
|
+
bootstrap?: boolean;
|
|
64
|
+
bootstrapOtp?: string;
|
|
65
|
+
skipLogin?: boolean;
|
|
66
|
+
packages?: string[];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export interface ReleaseBootstrapNpmPackagesOptions {
|
|
70
|
+
dryRun?: boolean;
|
|
50
71
|
skipLogin?: boolean;
|
|
72
|
+
otp?: string;
|
|
73
|
+
packages?: string[];
|
|
51
74
|
}
|
|
52
75
|
|
|
53
76
|
export interface ReleaseRetagUnpublishedOptions {
|
|
@@ -63,13 +86,14 @@ export interface ReleaseRetagUnpublishedOptions {
|
|
|
63
86
|
export async function releaseVersion(root: string, options: ReleaseVersionOptions): Promise<void> {
|
|
64
87
|
const bump = releaseBumpArg(options.bump);
|
|
65
88
|
const packages = releasePackages(root);
|
|
66
|
-
const projects = releasePackageProjects(packages);
|
|
67
89
|
const result = await runReleaseVersion(
|
|
68
90
|
{
|
|
69
91
|
releasePackagesAtHead: () => releasePackagesAtHead(root, packages),
|
|
92
|
+
releaseVersionPackages: (releaseBump) => releaseVersionPackages(root, packages, releaseBump),
|
|
70
93
|
ensureLocalReleaseTags: (releasePackages) => ensureLocalReleaseTags(root, releasePackages),
|
|
71
94
|
gitHead: () => gitHead(root),
|
|
72
|
-
runNxReleaseVersion: (releaseBump, dryRun) =>
|
|
95
|
+
runNxReleaseVersion: (releasePackages, releaseBump, dryRun) =>
|
|
96
|
+
runNxReleaseVersion(root, releasePackageProjects(releasePackages), releaseBump, dryRun),
|
|
73
97
|
assertCleanGitTree: () => assertCleanGitTree(root),
|
|
74
98
|
},
|
|
75
99
|
{ bump, dryRun: options.dryRun === true },
|
|
@@ -79,6 +103,9 @@ export async function releaseVersion(root: string, options: ReleaseVersionOption
|
|
|
79
103
|
} else if (result.status === 'no-release-needed') {
|
|
80
104
|
console.log('Nx did not create a release commit; no release needed.');
|
|
81
105
|
}
|
|
106
|
+
if (result.status === 'dry-run') {
|
|
107
|
+
await writeReleasePreviewSummary(root, packages, result.packages);
|
|
108
|
+
}
|
|
82
109
|
await writeReleaseGithubOutput(options.githubOutput, result.packages, result.mode);
|
|
83
110
|
}
|
|
84
111
|
|
|
@@ -87,15 +114,22 @@ export async function releasePublish(root: string, options: ReleasePublishOption
|
|
|
87
114
|
const packages = await releasePackagesAtHead(root, releasePackages(root));
|
|
88
115
|
if (packages.length === 0) {
|
|
89
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
|
+
}
|
|
90
123
|
console.log('No release tags found at HEAD; no release to publish.');
|
|
91
124
|
const summary: ReleaseSummary<ReleasePackage> = {
|
|
92
125
|
sha: await gitHead(root),
|
|
93
|
-
dryRun:
|
|
126
|
+
dryRun: false,
|
|
94
127
|
packages,
|
|
95
128
|
pushed: false,
|
|
96
129
|
published: [],
|
|
97
130
|
alreadyPublished: [],
|
|
98
131
|
githubReleases: [],
|
|
132
|
+
githubReleaseLinks: [],
|
|
99
133
|
rerunRequired: false,
|
|
100
134
|
noRelease: true,
|
|
101
135
|
};
|
|
@@ -111,6 +145,15 @@ export async function releasePublish(root: string, options: ReleasePublishOption
|
|
|
111
145
|
await newerCommitsRemain(root),
|
|
112
146
|
);
|
|
113
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
|
+
}
|
|
114
157
|
}
|
|
115
158
|
|
|
116
159
|
export async function releaseRepairPending(root: string, options: ReleaseRepairPendingOptions): Promise<void> {
|
|
@@ -127,6 +170,9 @@ export async function releaseRepairPending(root: string, options: ReleaseRepairP
|
|
|
127
170
|
? 'Repair pending releases: no pending durable state repairs found.'
|
|
128
171
|
: `Repair pending releases: ${targets.length} release target${targets.length === 1 ? '' : 's'} need repair.`,
|
|
129
172
|
);
|
|
173
|
+
for (const target of targets) {
|
|
174
|
+
console.log(`Repair pending releases: target ${target.sha.slice(0, 12)} needs ${repairTargetSummary(target)}.`);
|
|
175
|
+
}
|
|
130
176
|
const summaries = await repairPendingTargets(releaseRepairShell(root), targets, restoreRef, options.dryRun === true);
|
|
131
177
|
await writeRepairSummary(summaries, options.dryRun === true);
|
|
132
178
|
}
|
|
@@ -134,61 +180,193 @@ export async function releaseRepairPending(root: string, options: ReleaseRepairP
|
|
|
134
180
|
export async function releaseTrustPublisher(root: string, options: ReleaseTrustPublisherOptions): Promise<void> {
|
|
135
181
|
const repository = githubRepositoryFromRootPackage(root);
|
|
136
182
|
const workflow = 'publish.yml';
|
|
137
|
-
|
|
138
|
-
|
|
183
|
+
await configureTrustedPublishers(
|
|
184
|
+
{
|
|
185
|
+
repository,
|
|
186
|
+
workflow,
|
|
187
|
+
listReleasePackages: () => listReleasePackages(root),
|
|
188
|
+
packageExists: (name) => npmPackageExists(root, name),
|
|
189
|
+
bootstrapNpmPackages: (bootstrapOptions) =>
|
|
190
|
+
bootstrapNpmPackages(
|
|
191
|
+
{
|
|
192
|
+
listReleasePackages: () => listReleasePackages(root),
|
|
193
|
+
packageExists: (name) => npmPackageExists(root, name),
|
|
194
|
+
login: () => runLatestNpm(root, ['login', '--auth-type=web']),
|
|
195
|
+
publishPlaceholder: (pkg, env) => publishPlaceholderPackage(root, pkg, env),
|
|
196
|
+
promptOtp: (packageName) => promptForNpmOtp(packageName),
|
|
197
|
+
log: (message) => console.log(message),
|
|
198
|
+
},
|
|
199
|
+
bootstrapOptions,
|
|
200
|
+
),
|
|
201
|
+
trustPublisher: (pkg, dryRun, env) => {
|
|
202
|
+
const args = ['trust', 'github', pkg.name, '--file', workflow, '--repo', repository, '--yes'];
|
|
203
|
+
if (dryRun) {
|
|
204
|
+
args.push('--dry-run');
|
|
205
|
+
}
|
|
206
|
+
return runLatestNpmTrust(root, args, env);
|
|
207
|
+
},
|
|
208
|
+
trustedPublishers: (pkg) => listTrustedPublishers(root, pkg),
|
|
209
|
+
log: (message) => console.log(message),
|
|
210
|
+
error: (message) => console.error(message),
|
|
211
|
+
},
|
|
212
|
+
options,
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
export interface TrustPublisherShell<Package extends ReleasePackageInfo = ReleasePackageInfo> {
|
|
217
|
+
repository: string;
|
|
218
|
+
workflow: string;
|
|
219
|
+
listReleasePackages(): Package[];
|
|
220
|
+
packageExists(name: string): Promise<boolean>;
|
|
221
|
+
bootstrapNpmPackages(options: BootstrapNpmPackagesOptions): Promise<Package[]>;
|
|
222
|
+
trustPublisher(pkg: Package, dryRun: boolean, env?: Record<string, string>): Promise<TrustPublisherResult>;
|
|
223
|
+
trustedPublishers(pkg: Package): Promise<TrustedPublisher[]>;
|
|
224
|
+
log(message: string): void;
|
|
225
|
+
error(message: string): void;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export type TrustPublisherResult = 'configured' | 'already-configured';
|
|
229
|
+
|
|
230
|
+
export interface TrustedPublisher {
|
|
231
|
+
id: string;
|
|
232
|
+
type: string;
|
|
233
|
+
file?: string;
|
|
234
|
+
repository?: string;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export async function configureTrustedPublishers<Package extends ReleasePackageInfo>(
|
|
238
|
+
shell: TrustPublisherShell<Package>,
|
|
239
|
+
options: ReleaseTrustPublisherOptions,
|
|
240
|
+
): Promise<void> {
|
|
241
|
+
const packages = shell.listReleasePackages();
|
|
242
|
+
const selectedPackages = selectedTrustPublisherPackages(packages, options.packages ?? []);
|
|
243
|
+
if (selectedPackages.length === 0) {
|
|
139
244
|
throw new Error('No owned release packages found.');
|
|
140
245
|
}
|
|
141
246
|
|
|
247
|
+
if (options.bootstrap) {
|
|
248
|
+
await shell.bootstrapNpmPackages({
|
|
249
|
+
dryRun: options.dryRun === true,
|
|
250
|
+
skipLogin: options.skipLogin === true,
|
|
251
|
+
otp: options.bootstrapOtp,
|
|
252
|
+
packages: options.packages ?? [],
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
|
|
142
256
|
if (!options.dryRun) {
|
|
143
257
|
const packageStates = await Promise.all(
|
|
144
|
-
|
|
258
|
+
selectedPackages.map(async (pkg) => ((await shell.packageExists(pkg.name)) ? null : pkg.name)),
|
|
145
259
|
);
|
|
146
260
|
const missingPackages = packageStates.filter((name): name is string => name !== null);
|
|
147
261
|
if (missingPackages.length > 0) {
|
|
148
262
|
throw new Error(
|
|
149
263
|
'npm trusted publishing can only be configured after packages exist on the registry. ' +
|
|
150
|
-
'
|
|
264
|
+
'Run smoo release trust-publisher --bootstrap locally. ' +
|
|
151
265
|
`Missing packages: ${missingPackages.join(', ')}`,
|
|
152
266
|
);
|
|
153
267
|
}
|
|
154
268
|
}
|
|
155
269
|
|
|
156
|
-
if (!options.dryRun && !options.skipLogin) {
|
|
157
|
-
await runLatestNpm(root, ['login', '--auth-type=web']);
|
|
158
|
-
}
|
|
159
|
-
|
|
160
270
|
const failedPackages: string[] = [];
|
|
161
|
-
for (const pkg of
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
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
|
+
}
|
|
166
284
|
}
|
|
285
|
+
|
|
286
|
+
shell.log(`${pkg.name}: trusting GitHub Actions ${shell.repository}/${shell.workflow}`);
|
|
167
287
|
while (true) {
|
|
168
|
-
const otp = options.dryRun ? undefined : (options.otp ?? (await promptForNpmOtp(pkg.name)));
|
|
169
288
|
try {
|
|
170
|
-
await
|
|
289
|
+
const result = await shell.trustPublisher(pkg, options.dryRun === true);
|
|
290
|
+
if (result === 'already-configured') {
|
|
291
|
+
shell.log(`${pkg.name}: npm trusted publisher is already configured; skipping.`);
|
|
292
|
+
}
|
|
171
293
|
break;
|
|
172
294
|
} catch (error) {
|
|
173
|
-
|
|
174
|
-
|
|
295
|
+
shell.error(`${pkg.name}: npm trusted publisher setup failed.`);
|
|
296
|
+
shell.error(error instanceof Error ? error.message : String(error));
|
|
175
297
|
if (!options.dryRun) {
|
|
176
|
-
|
|
298
|
+
shell.error('npm owns trusted publishing authentication. Complete the npm browser challenge, then retry.');
|
|
177
299
|
}
|
|
178
|
-
if (options.
|
|
300
|
+
if (options.dryRun) {
|
|
179
301
|
failedPackages.push(pkg.name);
|
|
180
302
|
break;
|
|
181
303
|
}
|
|
182
|
-
|
|
304
|
+
shell.error('Retrying this package. Press Ctrl-C to stop.');
|
|
183
305
|
}
|
|
184
306
|
}
|
|
185
307
|
}
|
|
186
308
|
if (failedPackages.length > 0) {
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
309
|
+
shell.error(`Trusted publishing was not configured for: ${failedPackages.join(', ')}`);
|
|
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);
|
|
190
329
|
}
|
|
191
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
|
+
);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
export async function releaseBootstrapNpmPackages(
|
|
351
|
+
root: string,
|
|
352
|
+
options: ReleaseBootstrapNpmPackagesOptions,
|
|
353
|
+
): Promise<void> {
|
|
354
|
+
await bootstrapNpmPackages(
|
|
355
|
+
{
|
|
356
|
+
listReleasePackages: () => listReleasePackages(root),
|
|
357
|
+
packageExists: (name) => npmPackageExists(root, name),
|
|
358
|
+
login: () => runLatestNpm(root, ['login', '--auth-type=web']),
|
|
359
|
+
publishPlaceholder: (pkg, env) => publishPlaceholderPackage(root, pkg, env),
|
|
360
|
+
promptOtp: (packageName) => promptForNpmOtp(packageName),
|
|
361
|
+
log: (message) => console.log(message),
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
dryRun: options.dryRun === true,
|
|
365
|
+
skipLogin: options.skipLogin === true,
|
|
366
|
+
otp: options.otp,
|
|
367
|
+
packages: options.packages ?? [],
|
|
368
|
+
},
|
|
369
|
+
);
|
|
192
370
|
}
|
|
193
371
|
|
|
194
372
|
export async function releaseRetagUnpublished(root: string, options: ReleaseRetagUnpublishedOptions): Promise<void> {
|
|
@@ -218,6 +396,7 @@ interface ReleaseState {
|
|
|
218
396
|
}
|
|
219
397
|
|
|
220
398
|
type ReleasePackage = ReturnType<typeof listReleasePackages>[number];
|
|
399
|
+
const releasePreviewChangelogs = new Map<string, string>();
|
|
221
400
|
|
|
222
401
|
type ReleaseTarget = CoreReleaseTarget<ReleasePackage>;
|
|
223
402
|
type ReleaseTagRecord = CoreReleaseTagRecord<ReleasePackage>;
|
|
@@ -243,21 +422,14 @@ async function listUnpublishedPackages(root: string, packages: ReleasePackage[])
|
|
|
243
422
|
return states.filter((state) => !state.published).map((state) => state.pkg);
|
|
244
423
|
}
|
|
245
424
|
|
|
246
|
-
async function publishPackedPackage(
|
|
247
|
-
root: string,
|
|
248
|
-
pkg: ReleasePackage,
|
|
249
|
-
tag: string,
|
|
250
|
-
dryRun: boolean,
|
|
251
|
-
useBootstrapToken: boolean,
|
|
252
|
-
): Promise<void> {
|
|
425
|
+
async function publishPackedPackage(root: string, pkg: ReleasePackage, tag: string, dryRun: boolean): Promise<void> {
|
|
253
426
|
const tempDir = await mkdtemp(join(tmpdir(), 'smoo-publish-'));
|
|
254
427
|
const tarball = join(tempDir, `${safeTarballPrefix(pkg.name)}-${pkg.version}.tgz`);
|
|
255
428
|
try {
|
|
256
429
|
console.log(`${pkg.name}@${pkg.version}: packing with bun pm pack`);
|
|
257
430
|
await run('bun', ['pm', 'pack', '--filename', tarball, '--ignore-scripts', '--quiet'], join(root, pkg.path));
|
|
258
431
|
await assertPackedWorkspaceDependencies(root, tarball, pkg);
|
|
259
|
-
// npm CLI owns authentication here: trusted publishing OIDC when configured
|
|
260
|
-
// or the temporary NODE_AUTH_TOKEN bootstrap path below before trust exists.
|
|
432
|
+
// npm CLI owns authentication here: trusted publishing OIDC when configured.
|
|
261
433
|
// Bun still produces the tarball so workspace:* dependencies are resolved the
|
|
262
434
|
// same way smoo validates packed packages before release.
|
|
263
435
|
const args = ['publish', tarball, '--access', 'public', '--tag', tag, '--provenance'];
|
|
@@ -267,7 +439,7 @@ async function publishPackedPackage(
|
|
|
267
439
|
await publishWithAuthDiagnostics(
|
|
268
440
|
pkg,
|
|
269
441
|
{
|
|
270
|
-
publish: () => runLatestNpmPublish(root, args
|
|
442
|
+
publish: () => runLatestNpmPublish(root, args),
|
|
271
443
|
versionExists: () => npmVersionExists(root, pkg.name, pkg.version),
|
|
272
444
|
log: (message) => console.log(message),
|
|
273
445
|
error: (message) => console.error(message),
|
|
@@ -279,7 +451,6 @@ async function publishPackedPackage(
|
|
|
279
451
|
},
|
|
280
452
|
},
|
|
281
453
|
{
|
|
282
|
-
useBootstrapToken,
|
|
283
454
|
tokenPresent: npmAuthTokenPresent(),
|
|
284
455
|
repository: githubRepositoryFromRootPackage(root),
|
|
285
456
|
},
|
|
@@ -293,6 +464,38 @@ function npmAuthTokenPresent(): boolean {
|
|
|
293
464
|
return Boolean(process.env.NODE_AUTH_TOKEN || process.env.NPM_TOKEN || process.env.NPM_CONFIG_USERCONFIG);
|
|
294
465
|
}
|
|
295
466
|
|
|
467
|
+
async function publishPlaceholderPackage(
|
|
468
|
+
root: string,
|
|
469
|
+
pkg: ReleasePackage,
|
|
470
|
+
env?: Record<string, string>,
|
|
471
|
+
): Promise<void> {
|
|
472
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'smoo-npm-bootstrap-'));
|
|
473
|
+
try {
|
|
474
|
+
await writeFile(
|
|
475
|
+
join(tempDir, 'package.json'),
|
|
476
|
+
`${JSON.stringify(
|
|
477
|
+
{
|
|
478
|
+
name: pkg.name,
|
|
479
|
+
version: NPM_BOOTSTRAP_VERSION,
|
|
480
|
+
description: `Bootstrap placeholder for ${pkg.name}. Real releases are published by SmoothBricks CI.`,
|
|
481
|
+
license: stringProperty(pkg.json, 'license') ?? 'UNLICENSED',
|
|
482
|
+
repository: pkg.json.repository,
|
|
483
|
+
publishConfig: { access: 'public' },
|
|
484
|
+
},
|
|
485
|
+
null,
|
|
486
|
+
2,
|
|
487
|
+
)}\n`,
|
|
488
|
+
);
|
|
489
|
+
await writeFile(
|
|
490
|
+
join(tempDir, 'README.md'),
|
|
491
|
+
`# ${pkg.name}\n\nThis is a bootstrap placeholder. Real package versions are published by SmoothBricks release automation.\n`,
|
|
492
|
+
);
|
|
493
|
+
await runLatestNpm(root, ['publish', tempDir, '--access', 'public', '--tag', NPM_BOOTSTRAP_DIST_TAG], env);
|
|
494
|
+
} finally {
|
|
495
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
496
|
+
}
|
|
497
|
+
}
|
|
498
|
+
|
|
296
499
|
function safeTarballPrefix(name: string): string {
|
|
297
500
|
return name.replace(/^@/, '').replace(/[^a-zA-Z0-9._-]+/g, '-');
|
|
298
501
|
}
|
|
@@ -314,10 +517,41 @@ function releasePackages(root: string): ReleasePackage[] {
|
|
|
314
517
|
}
|
|
315
518
|
|
|
316
519
|
function releasePackageProjects(packages: ReleasePackage[]): string {
|
|
317
|
-
return packages.map((pkg) => pkg.
|
|
520
|
+
return packages.map((pkg) => pkg.projectName).join(',');
|
|
318
521
|
}
|
|
319
522
|
|
|
320
|
-
async function
|
|
523
|
+
async function releaseVersionPackages(
|
|
524
|
+
root: string,
|
|
525
|
+
packages: ReleasePackage[],
|
|
526
|
+
bump: string,
|
|
527
|
+
): Promise<ReleasePackage[]> {
|
|
528
|
+
if (bump !== 'auto') {
|
|
529
|
+
return packages;
|
|
530
|
+
}
|
|
531
|
+
return autoReleaseCandidatePackages(
|
|
532
|
+
{
|
|
533
|
+
gitRefExists: (ref) => gitRefExists(root, ref),
|
|
534
|
+
packageChangedFilesSince: (ref, packagePath) => packageChangedFilesSince(root, ref, packagePath),
|
|
535
|
+
packageJsonAtRef: (ref, packagePath) => packageJsonAtRef(root, ref, packagePath),
|
|
536
|
+
currentPackageJson: (packagePath) => currentPackageJson(root, packagePath),
|
|
537
|
+
packageBuildInputPatterns: (projectName, packagePath) =>
|
|
538
|
+
packageBuildInputPatterns(root, projectName, packagePath),
|
|
539
|
+
packageHasHistory: (packagePath) => packageHasHistory(root, packagePath),
|
|
540
|
+
},
|
|
541
|
+
packages,
|
|
542
|
+
);
|
|
543
|
+
}
|
|
544
|
+
|
|
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
|
+
|
|
321
555
|
// Nx owns local release mutation: package versions, bun.lock updates, the
|
|
322
556
|
// release commit, and annotated tags. smoo owns remote publication after the
|
|
323
557
|
// workflow validates the exact release commit Nx produced.
|
|
@@ -326,10 +560,78 @@ async function runNxReleaseVersion(root: string, projects: string, bump: string,
|
|
|
326
560
|
nxArgs.push(bump);
|
|
327
561
|
}
|
|
328
562
|
nxArgs.push(`--projects=${projects}`, '--git-commit=true', '--git-tag=true', '--git-push=false');
|
|
329
|
-
if (dryRun) {
|
|
330
|
-
nxArgs.push('--dry-run');
|
|
331
|
-
}
|
|
332
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
|
+
);
|
|
333
635
|
}
|
|
334
636
|
|
|
335
637
|
async function releasePackagesAtHead(root: string, packages: ReleasePackage[]): Promise<ReleasePackage[]> {
|
|
@@ -414,7 +716,7 @@ async function gitTagsAtRef(root: string, ref: string): Promise<string[]> {
|
|
|
414
716
|
}
|
|
415
717
|
|
|
416
718
|
async function previousReleaseTag(root: string, pkg: ReleasePackage, currentTag: string): Promise<string | null> {
|
|
417
|
-
const result = await $`git tag --list ${`${pkg.
|
|
719
|
+
const result = await $`git tag --list ${`${pkg.projectName}@*`} --sort=-v:refname --merged ${currentTag}`
|
|
418
720
|
.cwd(root)
|
|
419
721
|
.quiet()
|
|
420
722
|
.nothrow();
|
|
@@ -467,6 +769,12 @@ async function pushReleaseRefs(root: string, packages: ReleasePackage[]): Promis
|
|
|
467
769
|
return true;
|
|
468
770
|
}
|
|
469
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
|
+
|
|
470
778
|
function releaseCompletionShell(root: string): ReleaseCompletionShell<ReleasePackage> {
|
|
471
779
|
return {
|
|
472
780
|
gitHead: () => gitHead(root),
|
|
@@ -475,13 +783,25 @@ function releaseCompletionShell(root: string): ReleaseCompletionShell<ReleasePac
|
|
|
475
783
|
buildReleaseCandidate: (packages) => buildReleaseCandidate(root, packages),
|
|
476
784
|
publishPackage: async (pkg, distTag, dryRun) => {
|
|
477
785
|
const packageExists = dryRun ? true : await npmPackageExists(root, pkg.name);
|
|
478
|
-
|
|
786
|
+
if (!packageExists) {
|
|
787
|
+
throw new Error(missingNpmPackagePublishGuidance(pkg));
|
|
788
|
+
}
|
|
789
|
+
await publishPackedPackage(root, pkg, distTag, dryRun);
|
|
479
790
|
},
|
|
480
791
|
listGithubMissingPackages: (packages) => listMissingGithubReleasePackages(root, packages),
|
|
481
792
|
createGithubRelease: (pkg, dryRun) => createGithubRelease(root, pkg, dryRun),
|
|
482
793
|
};
|
|
483
794
|
}
|
|
484
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
|
+
|
|
485
805
|
function releaseRepairShell(root: string): ReleaseCompletionShell<ReleasePackage> & {
|
|
486
806
|
checkout(ref: string): Promise<void>;
|
|
487
807
|
withDevenvEnv<T>(runWithEnv: () => Promise<T>): Promise<T>;
|
|
@@ -536,11 +856,12 @@ async function listOwnedReleaseTagRecords(root: string, ref: string): Promise<Re
|
|
|
536
856
|
|
|
537
857
|
async function durableReleaseTagState(root: string, pkg: Pick<ReleasePackage, 'name' | 'version'>, tag: string) {
|
|
538
858
|
const packageVersion = `${pkg.name}@${pkg.version}`;
|
|
859
|
+
const githubReleaseTags = releaseTagAliases({ ...pkg, projectName: projectNameFromReleaseTag(tag) });
|
|
539
860
|
const start = Date.now();
|
|
540
|
-
console.log(`${packageVersion}: checking durable state (npm + GitHub Release ${
|
|
861
|
+
console.log(`${packageVersion}: checking durable state (npm + GitHub Release ${githubReleaseTags.join(' or ')}).`);
|
|
541
862
|
const [npmPublished, githubReleasePresent] = await Promise.all([
|
|
542
863
|
npmVersionExists(root, pkg.name, pkg.version),
|
|
543
|
-
|
|
864
|
+
anyGithubReleaseExists(root, githubReleaseTags),
|
|
544
865
|
]);
|
|
545
866
|
console.log(
|
|
546
867
|
`${packageVersion}: durable state npm=${yesNo(npmPublished)} github=${yesNo(githubReleasePresent)} (${Date.now() - start}ms).`,
|
|
@@ -548,6 +869,14 @@ async function durableReleaseTagState(root: string, pkg: Pick<ReleasePackage, 'n
|
|
|
548
869
|
return { npmPublished, githubReleaseExists: githubReleasePresent };
|
|
549
870
|
}
|
|
550
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
|
+
|
|
551
880
|
async function gitReleaseTagsByCreatorDate(root: string): Promise<GitReleaseTagInfo[]> {
|
|
552
881
|
const result =
|
|
553
882
|
await $`git for-each-ref --sort=-creatordate --format=${'%(refname:short)%09%(creatordate:unix)%09%(*objectname)%09%(objectname)'} refs/tags`
|
|
@@ -582,7 +911,7 @@ async function listMissingGithubReleasePackages(root: string, packages: ReleaseP
|
|
|
582
911
|
).filter((pkg): pkg is ReleasePackage => pkg !== null);
|
|
583
912
|
}
|
|
584
913
|
|
|
585
|
-
async function createGithubRelease(root: string, pkg: ReleasePackage, dryRun: boolean): Promise<
|
|
914
|
+
async function createGithubRelease(root: string, pkg: ReleasePackage, dryRun: boolean): Promise<string | null> {
|
|
586
915
|
const currentTag = releaseTag(pkg);
|
|
587
916
|
console.log(`${pkg.name}@${pkg.version}: rendering GitHub Release notes for ${currentTag}.`);
|
|
588
917
|
console.log(`GitHub release auth: ${envPresence('GH_TOKEN')}, ${envPresence('GITHUB_TOKEN')}.`);
|
|
@@ -592,7 +921,7 @@ async function createGithubRelease(root: string, pkg: ReleasePackage, dryRun: bo
|
|
|
592
921
|
const previousTag = await previousReleaseTag(root, pkg, currentTag);
|
|
593
922
|
if (dryRun) {
|
|
594
923
|
await renderNxProjectChangelogContents({ root, pkg, previousTag, dryRun });
|
|
595
|
-
return;
|
|
924
|
+
return null;
|
|
596
925
|
}
|
|
597
926
|
const contents = await renderNxProjectChangelogContents({ root, pkg, previousTag, dryRun });
|
|
598
927
|
await createOrUpdateGithubRelease(pkg, contents, {
|
|
@@ -600,6 +929,7 @@ async function createGithubRelease(root: string, pkg: ReleasePackage, dryRun: bo
|
|
|
600
929
|
runGhRelease: (args) => run('gh', args, root),
|
|
601
930
|
log: (message) => console.log(message),
|
|
602
931
|
});
|
|
932
|
+
return githubReleaseUrl(root, currentTag);
|
|
603
933
|
}
|
|
604
934
|
|
|
605
935
|
function envPresence(name: string): string {
|
|
@@ -637,7 +967,7 @@ async function writeReleaseSummary(summary: ReleaseSummary<ReleasePackage>): Pro
|
|
|
637
967
|
lines.push(`- Git refs pushed: ${summary.pushed ? 'yes' : 'already current'}`);
|
|
638
968
|
lines.push(`- npm published: ${packageSummary(summary.published)}`);
|
|
639
969
|
lines.push(`- npm already published: ${packageSummary(summary.alreadyPublished)}`);
|
|
640
|
-
lines.push(`- GitHub Releases created/updated: ${
|
|
970
|
+
lines.push(`- GitHub Releases created/updated: ${githubReleaseSummary(summary)}`);
|
|
641
971
|
}
|
|
642
972
|
if (summary.rerunRequired) {
|
|
643
973
|
const message = 'A previous incomplete release was repaired; newer commits remain. Run Publish again.';
|
|
@@ -652,13 +982,45 @@ async function writeReleaseSummary(summary: ReleaseSummary<ReleasePackage>): Pro
|
|
|
652
982
|
}
|
|
653
983
|
}
|
|
654
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
|
+
|
|
655
1015
|
async function writeRepairSummary(summaries: Array<ReleaseSummary<ReleasePackage>>, dryRun: boolean): Promise<void> {
|
|
656
1016
|
const lines = ['## Pending Release Repair', '', `- Mode: ${dryRun ? 'dry run' : 'release'}`];
|
|
657
1017
|
if (summaries.length === 0) {
|
|
658
1018
|
lines.push('- Result: no pending releases needed repair');
|
|
659
1019
|
} else {
|
|
660
1020
|
for (const summary of summaries) {
|
|
661
|
-
lines.push(
|
|
1021
|
+
lines.push(
|
|
1022
|
+
`- ${dryRun ? 'Would repair' : 'Repaired'} \`${summary.sha.slice(0, 12)}\`: ${packageSummary(summary.packages)}`,
|
|
1023
|
+
);
|
|
662
1024
|
}
|
|
663
1025
|
}
|
|
664
1026
|
const text = `${lines.join('\n')}\n`;
|
|
@@ -676,6 +1038,30 @@ function packageSummary(packages: ReleasePackage[]): string {
|
|
|
676
1038
|
return packages.map((pkg) => `${pkg.name}@${pkg.version}`).join(', ');
|
|
677
1039
|
}
|
|
678
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
|
+
|
|
679
1065
|
async function fetchReleaseRefs(root: string, remote: string, branch: string): Promise<void> {
|
|
680
1066
|
await run('git', ['fetch', '--tags', remote, branch], root);
|
|
681
1067
|
}
|
|
@@ -685,6 +1071,140 @@ async function gitRefExists(root: string, ref: string): Promise<boolean> {
|
|
|
685
1071
|
return result.exitCode === 0;
|
|
686
1072
|
}
|
|
687
1073
|
|
|
1074
|
+
async function packageChangedFilesSince(root: string, ref: string, packagePath: string): Promise<string[]> {
|
|
1075
|
+
const result = await $`git diff --name-only ${`${ref}..HEAD`} -- ${packagePath}`.cwd(root).quiet().nothrow();
|
|
1076
|
+
if (result.exitCode !== 0) {
|
|
1077
|
+
throw new Error(`Unable to inspect package changes under ${packagePath}.`);
|
|
1078
|
+
}
|
|
1079
|
+
const packagePrefix = `${packagePath}/`;
|
|
1080
|
+
return decode(result.stdout)
|
|
1081
|
+
.split('\n')
|
|
1082
|
+
.map((path) => path.trim())
|
|
1083
|
+
.filter(Boolean)
|
|
1084
|
+
.map((path) => (path.startsWith(packagePrefix) ? path.slice(packagePrefix.length) : path));
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
async function packageJsonAtRef(
|
|
1088
|
+
root: string,
|
|
1089
|
+
ref: string,
|
|
1090
|
+
packagePath: string,
|
|
1091
|
+
): Promise<Record<string, unknown> | null> {
|
|
1092
|
+
const result = await $`git show ${`${ref}:${packagePath}/package.json`}`.cwd(root).quiet().nothrow();
|
|
1093
|
+
if (result.exitCode !== 0) {
|
|
1094
|
+
return null;
|
|
1095
|
+
}
|
|
1096
|
+
try {
|
|
1097
|
+
const parsed = JSON.parse(decode(result.stdout));
|
|
1098
|
+
return isRecord(parsed) ? parsed : null;
|
|
1099
|
+
} catch {
|
|
1100
|
+
return null;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
|
|
1104
|
+
async function currentPackageJson(root: string, packagePath: string): Promise<Record<string, unknown> | null> {
|
|
1105
|
+
return readPackageJson(join(root, packagePath, 'package.json'))?.json ?? null;
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
async function packageBuildInputPatterns(root: string, projectName: string, _packagePath: string): Promise<string[]> {
|
|
1109
|
+
const project = await nxProjectJson(root, projectName);
|
|
1110
|
+
const nxJson = readJsonObject(join(root, 'nx.json')) ?? {};
|
|
1111
|
+
return resolveBuildInputPatterns(project, nxJson);
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
async function nxProjectJson(root: string, projectName: string): Promise<Record<string, unknown>> {
|
|
1115
|
+
const result = await $`nx show project ${projectName} --json`.cwd(root).quiet();
|
|
1116
|
+
const parsed = JSON.parse(decode(result.stdout));
|
|
1117
|
+
if (!isRecord(parsed)) {
|
|
1118
|
+
throw new Error(`Unable to inspect Nx project ${projectName}.`);
|
|
1119
|
+
}
|
|
1120
|
+
return parsed;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1123
|
+
function resolveBuildInputPatterns(project: Record<string, unknown>, nxJson: Record<string, unknown>): string[] {
|
|
1124
|
+
const targets = recordProperty(project, 'targets');
|
|
1125
|
+
if (!targets) {
|
|
1126
|
+
return [];
|
|
1127
|
+
}
|
|
1128
|
+
return normalizeInputPatterns(collectBuildInputs(targets), nxJson);
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
function collectBuildInputs(targets: Record<string, unknown>): string[] {
|
|
1132
|
+
const build = recordProperty(targets, 'build');
|
|
1133
|
+
if (!build) {
|
|
1134
|
+
return ['production'];
|
|
1135
|
+
}
|
|
1136
|
+
const directInputs = stringArrayProperty(build, 'inputs');
|
|
1137
|
+
if (directInputs.length > 0) {
|
|
1138
|
+
return directInputs;
|
|
1139
|
+
}
|
|
1140
|
+
const inputs: string[] = [];
|
|
1141
|
+
for (const dependency of stringArrayProperty(build, 'dependsOn')) {
|
|
1142
|
+
if (dependency.startsWith('^')) {
|
|
1143
|
+
continue;
|
|
1144
|
+
}
|
|
1145
|
+
const targetName = dependency.includes(':') ? dependency.split(':')[1] : dependency;
|
|
1146
|
+
if (!targetName) {
|
|
1147
|
+
continue;
|
|
1148
|
+
}
|
|
1149
|
+
inputs.push(...stringArrayProperty(recordProperty(targets, targetName), 'inputs'));
|
|
1150
|
+
}
|
|
1151
|
+
return inputs.length > 0 ? inputs : ['production'];
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function normalizeInputPatterns(inputs: string[], nxJson: Record<string, unknown>): string[] {
|
|
1155
|
+
const patterns: string[] = [];
|
|
1156
|
+
const seen = new Set<string>();
|
|
1157
|
+
for (const input of inputs) {
|
|
1158
|
+
for (const pattern of expandInputPattern(input, nxJson, seen)) {
|
|
1159
|
+
patterns.push(pattern);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
return patterns;
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
function expandInputPattern(input: string, nxJson: Record<string, unknown>, seen: Set<string>): string[] {
|
|
1166
|
+
if (seen.has(input)) {
|
|
1167
|
+
return [];
|
|
1168
|
+
}
|
|
1169
|
+
seen.add(input);
|
|
1170
|
+
if (!input.includes('{')) {
|
|
1171
|
+
const namedInputs = recordProperty(nxJson, 'namedInputs');
|
|
1172
|
+
const namedInput = namedInputs?.[input];
|
|
1173
|
+
if (Array.isArray(namedInput)) {
|
|
1174
|
+
return namedInput.flatMap((entry) => (typeof entry === 'string' ? expandInputPattern(entry, nxJson, seen) : []));
|
|
1175
|
+
}
|
|
1176
|
+
return [];
|
|
1177
|
+
}
|
|
1178
|
+
const excluded = input.startsWith('!');
|
|
1179
|
+
const rawInput = excluded ? input.slice(1) : input;
|
|
1180
|
+
if (!rawInput.startsWith('{projectRoot}/')) {
|
|
1181
|
+
return [];
|
|
1182
|
+
}
|
|
1183
|
+
const packageRelative = rawInput.slice('{projectRoot}/'.length);
|
|
1184
|
+
return [`${excluded ? '!' : ''}${packageRelative}`];
|
|
1185
|
+
}
|
|
1186
|
+
|
|
1187
|
+
function recordProperty(record: Record<string, unknown> | null, key: string): Record<string, unknown> | null {
|
|
1188
|
+
if (!record) {
|
|
1189
|
+
return null;
|
|
1190
|
+
}
|
|
1191
|
+
const value = record[key];
|
|
1192
|
+
return isRecord(value) ? value : null;
|
|
1193
|
+
}
|
|
1194
|
+
|
|
1195
|
+
function stringArrayProperty(record: Record<string, unknown> | null, key: string): string[] {
|
|
1196
|
+
const value = record?.[key];
|
|
1197
|
+
return Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string') : [];
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
async function packageHasHistory(root: string, packagePath: string): Promise<boolean> {
|
|
1201
|
+
const result = await $`git log --format=%H -- ${packagePath}`.cwd(root).quiet().nothrow();
|
|
1202
|
+
if (result.exitCode !== 0) {
|
|
1203
|
+
throw new Error(`Unable to inspect package history under ${packagePath}.`);
|
|
1204
|
+
}
|
|
1205
|
+
return decode(result.stdout).trim().length > 0;
|
|
1206
|
+
}
|
|
1207
|
+
|
|
688
1208
|
async function gitIsAncestor(root: string, ancestor: string, descendant: string): Promise<boolean> {
|
|
689
1209
|
const result = await $`git merge-base --is-ancestor ${ancestor} ${descendant}`.cwd(root).quiet().nothrow();
|
|
690
1210
|
return result.exitCode === 0;
|
|
@@ -764,6 +1284,19 @@ async function githubReleaseExists(root: string, tag: string): Promise<boolean>
|
|
|
764
1284
|
return (await runStatus('gh', ['release', 'view', tag, '--json', 'tagName'], root, true)) === 0;
|
|
765
1285
|
}
|
|
766
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
|
+
|
|
767
1300
|
async function assertRemoteTagExists(root: string, tag: string): Promise<void> {
|
|
768
1301
|
const branch = await releaseBranch(root);
|
|
769
1302
|
const remote = await releaseRemote(root, branch);
|
|
@@ -887,36 +1420,87 @@ async function runLatestNpm(root: string, npmArgs: string[], env?: Record<string
|
|
|
887
1420
|
await run('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...npmArgs], root, env);
|
|
888
1421
|
}
|
|
889
1422
|
|
|
890
|
-
async function
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
1423
|
+
async function runLatestNpmTrust(
|
|
1424
|
+
root: string,
|
|
1425
|
+
npmArgs: string[],
|
|
1426
|
+
env?: Record<string, string>,
|
|
1427
|
+
): Promise<TrustPublisherResult> {
|
|
1428
|
+
const status = await runInteractiveStatus(
|
|
1429
|
+
'nix',
|
|
1430
|
+
['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...npmArgs],
|
|
1431
|
+
root,
|
|
1432
|
+
env,
|
|
1433
|
+
);
|
|
1434
|
+
if (status === 0) {
|
|
1435
|
+
return 'configured';
|
|
894
1436
|
}
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
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}`);
|
|
901
1447
|
}
|
|
902
|
-
await
|
|
903
|
-
return;
|
|
1448
|
+
result = await runResult('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...args], root);
|
|
904
1449
|
}
|
|
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
|
+
}
|
|
905
1457
|
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
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.`);
|
|
913
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
|
+
};
|
|
1490
|
+
}
|
|
1491
|
+
|
|
1492
|
+
async function runLatestNpmPublish(root: string, npmArgs: string[]): Promise<void> {
|
|
1493
|
+
await runLatestNpm(root, npmArgs, { NODE_AUTH_TOKEN: '', NPM_TOKEN: '' });
|
|
1494
|
+
}
|
|
1495
|
+
|
|
1496
|
+
function missingNpmPackagePublishGuidance(pkg: Pick<ReleasePackage, 'name'>): string {
|
|
1497
|
+
return `${pkg.name} does not exist on npm yet. Run smoo release trust-publisher --bootstrap locally before rerunning the Publish workflow.`;
|
|
914
1498
|
}
|
|
915
1499
|
|
|
916
1500
|
async function promptForNpmOtp(packageName: string): Promise<string> {
|
|
917
1501
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
918
1502
|
throw new Error(
|
|
919
|
-
`npm
|
|
1503
|
+
`npm requires a one-time password for ${packageName}. Pass --otp <code> in non-interactive shells.`,
|
|
920
1504
|
);
|
|
921
1505
|
}
|
|
922
1506
|
|