@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/dist/release/index.js
CHANGED
|
@@ -5,24 +5,26 @@ 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
|
-
import {
|
|
12
|
+
import { bootstrapNpmPackages, NPM_BOOTSTRAP_DIST_TAG, NPM_BOOTSTRAP_VERSION, } from './bootstrap-npm-packages.js';
|
|
13
|
+
import { autoReleaseCandidatePackages } from './candidates.js';
|
|
14
|
+
import { collectOwnedReleaseTagRecords, pendingReleaseTargets, releaseTag, releaseTagAliases, } from './core.js';
|
|
15
|
+
import { createOrUpdateGithubRelease, renderNxProjectChangelogContents, withNxWorkspaceRoot, } from './github-release.js';
|
|
14
16
|
import { publishWithAuthDiagnostics } from './npm-auth.js';
|
|
15
|
-
import { completeReleaseAtHead as completeReleaseAtHeadWithShell, repairPendingTargets, runReleaseVersion, } from './orchestration.js';
|
|
17
|
+
import { bumpStableReleaseToNext, completeReleaseAtHead as completeReleaseAtHeadWithShell, repairPendingTargets, runReleaseVersion, } from './orchestration.js';
|
|
16
18
|
import { retagUnpublished } from './retag-unpublished.js';
|
|
17
19
|
export async function releaseVersion(root, options) {
|
|
18
20
|
const bump = releaseBumpArg(options.bump);
|
|
19
21
|
const packages = releasePackages(root);
|
|
20
|
-
const projects = releasePackageProjects(packages);
|
|
21
22
|
const result = await runReleaseVersion({
|
|
22
23
|
releasePackagesAtHead: () => releasePackagesAtHead(root, packages),
|
|
24
|
+
releaseVersionPackages: (releaseBump) => releaseVersionPackages(root, packages, releaseBump),
|
|
23
25
|
ensureLocalReleaseTags: (releasePackages) => ensureLocalReleaseTags(root, releasePackages),
|
|
24
26
|
gitHead: () => gitHead(root),
|
|
25
|
-
runNxReleaseVersion: (releaseBump, dryRun) => runNxReleaseVersion(root,
|
|
27
|
+
runNxReleaseVersion: (releasePackages, releaseBump, dryRun) => runNxReleaseVersion(root, releasePackageProjects(releasePackages), releaseBump, dryRun),
|
|
26
28
|
assertCleanGitTree: () => assertCleanGitTree(root),
|
|
27
29
|
}, { bump, dryRun: options.dryRun === true });
|
|
28
30
|
if (result.status === 'already-release-target') {
|
|
@@ -31,6 +33,9 @@ export async function releaseVersion(root, options) {
|
|
|
31
33
|
else if (result.status === 'no-release-needed') {
|
|
32
34
|
console.log('Nx did not create a release commit; no release needed.');
|
|
33
35
|
}
|
|
36
|
+
if (result.status === 'dry-run') {
|
|
37
|
+
await writeReleasePreviewSummary(root, packages, result.packages);
|
|
38
|
+
}
|
|
34
39
|
await writeReleaseGithubOutput(options.githubOutput, result.packages, result.mode);
|
|
35
40
|
}
|
|
36
41
|
export async function releasePublish(root, options) {
|
|
@@ -38,15 +43,20 @@ export async function releasePublish(root, options) {
|
|
|
38
43
|
const packages = await releasePackagesAtHead(root, releasePackages(root));
|
|
39
44
|
if (packages.length === 0) {
|
|
40
45
|
if (bump === 'auto') {
|
|
46
|
+
if (options.dryRun === true) {
|
|
47
|
+
console.log('No release tags found at HEAD because dry-run versioning does not create release commits or tags.');
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
41
50
|
console.log('No release tags found at HEAD; no release to publish.');
|
|
42
51
|
const summary = {
|
|
43
52
|
sha: await gitHead(root),
|
|
44
|
-
dryRun:
|
|
53
|
+
dryRun: false,
|
|
45
54
|
packages,
|
|
46
55
|
pushed: false,
|
|
47
56
|
published: [],
|
|
48
57
|
alreadyPublished: [],
|
|
49
58
|
githubReleases: [],
|
|
59
|
+
githubReleaseLinks: [],
|
|
50
60
|
rerunRequired: false,
|
|
51
61
|
noRelease: true,
|
|
52
62
|
};
|
|
@@ -57,6 +67,10 @@ export async function releasePublish(root, options) {
|
|
|
57
67
|
}
|
|
58
68
|
const summary = await completeReleaseAtHeadWithShell(releaseCompletionShell(root), packages, options.dryRun === true, await newerCommitsRemain(root));
|
|
59
69
|
await writeReleaseSummary(summary);
|
|
70
|
+
const bumpedPackages = await bumpStableReleaseToNext(releaseNextShell(root), packages, options.dryRun === true, summary.rerunRequired);
|
|
71
|
+
if (bumpedPackages.length > 0) {
|
|
72
|
+
console.log(`Prepared next prerelease for ${packageSummary(bumpedPackages)}.`);
|
|
73
|
+
}
|
|
60
74
|
}
|
|
61
75
|
export async function releaseRepairPending(root, options) {
|
|
62
76
|
const branch = await releaseBranch(root);
|
|
@@ -70,61 +84,143 @@ export async function releaseRepairPending(root, options) {
|
|
|
70
84
|
console.log(targets.length === 0
|
|
71
85
|
? 'Repair pending releases: no pending durable state repairs found.'
|
|
72
86
|
: `Repair pending releases: ${targets.length} release target${targets.length === 1 ? '' : 's'} need repair.`);
|
|
87
|
+
for (const target of targets) {
|
|
88
|
+
console.log(`Repair pending releases: target ${target.sha.slice(0, 12)} needs ${repairTargetSummary(target)}.`);
|
|
89
|
+
}
|
|
73
90
|
const summaries = await repairPendingTargets(releaseRepairShell(root), targets, restoreRef, options.dryRun === true);
|
|
74
91
|
await writeRepairSummary(summaries, options.dryRun === true);
|
|
75
92
|
}
|
|
76
93
|
export async function releaseTrustPublisher(root, options) {
|
|
77
94
|
const repository = githubRepositoryFromRootPackage(root);
|
|
78
95
|
const workflow = 'publish.yml';
|
|
79
|
-
|
|
80
|
-
|
|
96
|
+
await configureTrustedPublishers({
|
|
97
|
+
repository,
|
|
98
|
+
workflow,
|
|
99
|
+
listReleasePackages: () => listReleasePackages(root),
|
|
100
|
+
packageExists: (name) => npmPackageExists(root, name),
|
|
101
|
+
bootstrapNpmPackages: (bootstrapOptions) => bootstrapNpmPackages({
|
|
102
|
+
listReleasePackages: () => listReleasePackages(root),
|
|
103
|
+
packageExists: (name) => npmPackageExists(root, name),
|
|
104
|
+
login: () => runLatestNpm(root, ['login', '--auth-type=web']),
|
|
105
|
+
publishPlaceholder: (pkg, env) => publishPlaceholderPackage(root, pkg, env),
|
|
106
|
+
promptOtp: (packageName) => promptForNpmOtp(packageName),
|
|
107
|
+
log: (message) => console.log(message),
|
|
108
|
+
}, bootstrapOptions),
|
|
109
|
+
trustPublisher: (pkg, dryRun, env) => {
|
|
110
|
+
const args = ['trust', 'github', pkg.name, '--file', workflow, '--repo', repository, '--yes'];
|
|
111
|
+
if (dryRun) {
|
|
112
|
+
args.push('--dry-run');
|
|
113
|
+
}
|
|
114
|
+
return runLatestNpmTrust(root, args, env);
|
|
115
|
+
},
|
|
116
|
+
trustedPublishers: (pkg) => listTrustedPublishers(root, pkg),
|
|
117
|
+
log: (message) => console.log(message),
|
|
118
|
+
error: (message) => console.error(message),
|
|
119
|
+
}, options);
|
|
120
|
+
}
|
|
121
|
+
export async function configureTrustedPublishers(shell, options) {
|
|
122
|
+
const packages = shell.listReleasePackages();
|
|
123
|
+
const selectedPackages = selectedTrustPublisherPackages(packages, options.packages ?? []);
|
|
124
|
+
if (selectedPackages.length === 0) {
|
|
81
125
|
throw new Error('No owned release packages found.');
|
|
82
126
|
}
|
|
127
|
+
if (options.bootstrap) {
|
|
128
|
+
await shell.bootstrapNpmPackages({
|
|
129
|
+
dryRun: options.dryRun === true,
|
|
130
|
+
skipLogin: options.skipLogin === true,
|
|
131
|
+
otp: options.bootstrapOtp,
|
|
132
|
+
packages: options.packages ?? [],
|
|
133
|
+
});
|
|
134
|
+
}
|
|
83
135
|
if (!options.dryRun) {
|
|
84
|
-
const packageStates = await Promise.all(
|
|
136
|
+
const packageStates = await Promise.all(selectedPackages.map(async (pkg) => ((await shell.packageExists(pkg.name)) ? null : pkg.name)));
|
|
85
137
|
const missingPackages = packageStates.filter((name) => name !== null);
|
|
86
138
|
if (missingPackages.length > 0) {
|
|
87
139
|
throw new Error('npm trusted publishing can only be configured after packages exist on the registry. ' +
|
|
88
|
-
'
|
|
140
|
+
'Run smoo release trust-publisher --bootstrap locally. ' +
|
|
89
141
|
`Missing packages: ${missingPackages.join(', ')}`);
|
|
90
142
|
}
|
|
91
143
|
}
|
|
92
|
-
if (!options.dryRun && !options.skipLogin) {
|
|
93
|
-
await runLatestNpm(root, ['login', '--auth-type=web']);
|
|
94
|
-
}
|
|
95
144
|
const failedPackages = [];
|
|
96
|
-
for (const pkg of
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
145
|
+
for (const pkg of selectedPackages) {
|
|
146
|
+
if (!options.dryRun) {
|
|
147
|
+
const trustedPublishers = await shell.trustedPublishers(pkg);
|
|
148
|
+
if (hasMatchingGithubTrustedPublisher(trustedPublishers, shell.repository, shell.workflow)) {
|
|
149
|
+
shell.log(`${pkg.name}: npm trusted publisher is already configured; skipping.`);
|
|
150
|
+
continue;
|
|
151
|
+
}
|
|
152
|
+
if (trustedPublishers.length > 0) {
|
|
153
|
+
throw new Error(`${pkg.name}: npm trusted publisher exists but does not match GitHub Actions ${shell.repository}/${shell.workflow}. ` +
|
|
154
|
+
'Use npm trust list to inspect it and npm trust revoke --id <trust-id> before reconfiguring.');
|
|
155
|
+
}
|
|
101
156
|
}
|
|
157
|
+
shell.log(`${pkg.name}: trusting GitHub Actions ${shell.repository}/${shell.workflow}`);
|
|
102
158
|
while (true) {
|
|
103
|
-
const otp = options.dryRun ? undefined : (options.otp ?? (await promptForNpmOtp(pkg.name)));
|
|
104
159
|
try {
|
|
105
|
-
await
|
|
160
|
+
const result = await shell.trustPublisher(pkg, options.dryRun === true);
|
|
161
|
+
if (result === 'already-configured') {
|
|
162
|
+
shell.log(`${pkg.name}: npm trusted publisher is already configured; skipping.`);
|
|
163
|
+
}
|
|
106
164
|
break;
|
|
107
165
|
}
|
|
108
166
|
catch (error) {
|
|
109
|
-
|
|
110
|
-
|
|
167
|
+
shell.error(`${pkg.name}: npm trusted publisher setup failed.`);
|
|
168
|
+
shell.error(error instanceof Error ? error.message : String(error));
|
|
111
169
|
if (!options.dryRun) {
|
|
112
|
-
|
|
170
|
+
shell.error('npm owns trusted publishing authentication. Complete the npm browser challenge, then retry.');
|
|
113
171
|
}
|
|
114
|
-
if (options.
|
|
172
|
+
if (options.dryRun) {
|
|
115
173
|
failedPackages.push(pkg.name);
|
|
116
174
|
break;
|
|
117
175
|
}
|
|
118
|
-
|
|
176
|
+
shell.error('Retrying this package. Press Ctrl-C to stop.');
|
|
119
177
|
}
|
|
120
178
|
}
|
|
121
179
|
}
|
|
122
180
|
if (failedPackages.length > 0) {
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
181
|
+
shell.error(`Trusted publishing was not configured for: ${failedPackages.join(', ')}`);
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
function selectedTrustPublisherPackages(packages, selections) {
|
|
185
|
+
if (selections.length === 0) {
|
|
186
|
+
return packages;
|
|
187
|
+
}
|
|
188
|
+
const byName = new Map(packages.map((pkg) => [pkg.name, pkg]));
|
|
189
|
+
const selected = [];
|
|
190
|
+
const unknown = [];
|
|
191
|
+
for (const name of selections) {
|
|
192
|
+
const pkg = byName.get(name);
|
|
193
|
+
if (pkg) {
|
|
194
|
+
selected.push(pkg);
|
|
126
195
|
}
|
|
196
|
+
else {
|
|
197
|
+
unknown.push(name);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (unknown.length > 0) {
|
|
201
|
+
throw new Error(`Unknown owned release package selection: ${unknown.join(', ')}`);
|
|
127
202
|
}
|
|
203
|
+
return selected;
|
|
204
|
+
}
|
|
205
|
+
function hasMatchingGithubTrustedPublisher(trustedPublishers, repository, workflow) {
|
|
206
|
+
return trustedPublishers.some((trustedPublisher) => trustedPublisher.type === 'github' &&
|
|
207
|
+
trustedPublisher.repository === repository &&
|
|
208
|
+
trustedPublisher.file === workflow);
|
|
209
|
+
}
|
|
210
|
+
export async function releaseBootstrapNpmPackages(root, options) {
|
|
211
|
+
await bootstrapNpmPackages({
|
|
212
|
+
listReleasePackages: () => listReleasePackages(root),
|
|
213
|
+
packageExists: (name) => npmPackageExists(root, name),
|
|
214
|
+
login: () => runLatestNpm(root, ['login', '--auth-type=web']),
|
|
215
|
+
publishPlaceholder: (pkg, env) => publishPlaceholderPackage(root, pkg, env),
|
|
216
|
+
promptOtp: (packageName) => promptForNpmOtp(packageName),
|
|
217
|
+
log: (message) => console.log(message),
|
|
218
|
+
}, {
|
|
219
|
+
dryRun: options.dryRun === true,
|
|
220
|
+
skipLogin: options.skipLogin === true,
|
|
221
|
+
otp: options.otp,
|
|
222
|
+
packages: options.packages ?? [],
|
|
223
|
+
});
|
|
128
224
|
}
|
|
129
225
|
export async function releaseRetagUnpublished(root, options) {
|
|
130
226
|
const toRef = options.to ?? 'HEAD';
|
|
@@ -145,6 +241,7 @@ export async function releaseRetagUnpublished(root, options) {
|
|
|
145
241
|
export async function printReleaseState(root) {
|
|
146
242
|
console.log(JSON.stringify(await getReleaseState(root), null, 2));
|
|
147
243
|
}
|
|
244
|
+
const releasePreviewChangelogs = new Map();
|
|
148
245
|
async function getReleaseState(root) {
|
|
149
246
|
return getReleaseStateForPackages(root, listReleasePackages(root));
|
|
150
247
|
}
|
|
@@ -159,15 +256,14 @@ async function listUnpublishedPackages(root, packages) {
|
|
|
159
256
|
const states = await Promise.all(packages.map(async (pkg) => ({ pkg, published: await npmVersionExists(root, pkg.name, pkg.version) })));
|
|
160
257
|
return states.filter((state) => !state.published).map((state) => state.pkg);
|
|
161
258
|
}
|
|
162
|
-
async function publishPackedPackage(root, pkg, tag, dryRun
|
|
259
|
+
async function publishPackedPackage(root, pkg, tag, dryRun) {
|
|
163
260
|
const tempDir = await mkdtemp(join(tmpdir(), 'smoo-publish-'));
|
|
164
261
|
const tarball = join(tempDir, `${safeTarballPrefix(pkg.name)}-${pkg.version}.tgz`);
|
|
165
262
|
try {
|
|
166
263
|
console.log(`${pkg.name}@${pkg.version}: packing with bun pm pack`);
|
|
167
264
|
await run('bun', ['pm', 'pack', '--filename', tarball, '--ignore-scripts', '--quiet'], join(root, pkg.path));
|
|
168
265
|
await assertPackedWorkspaceDependencies(root, tarball, pkg);
|
|
169
|
-
// npm CLI owns authentication here: trusted publishing OIDC when configured
|
|
170
|
-
// or the temporary NODE_AUTH_TOKEN bootstrap path below before trust exists.
|
|
266
|
+
// npm CLI owns authentication here: trusted publishing OIDC when configured.
|
|
171
267
|
// Bun still produces the tarball so workspace:* dependencies are resolved the
|
|
172
268
|
// same way smoo validates packed packages before release.
|
|
173
269
|
const args = ['publish', tarball, '--access', 'public', '--tag', tag, '--provenance'];
|
|
@@ -175,7 +271,7 @@ async function publishPackedPackage(root, pkg, tag, dryRun, useBootstrapToken) {
|
|
|
175
271
|
args.push('--dry-run');
|
|
176
272
|
}
|
|
177
273
|
await publishWithAuthDiagnostics(pkg, {
|
|
178
|
-
publish: () => runLatestNpmPublish(root, args
|
|
274
|
+
publish: () => runLatestNpmPublish(root, args),
|
|
179
275
|
versionExists: () => npmVersionExists(root, pkg.name, pkg.version),
|
|
180
276
|
log: (message) => console.log(message),
|
|
181
277
|
error: (message) => console.error(message),
|
|
@@ -186,7 +282,6 @@ async function publishPackedPackage(root, pkg, tag, dryRun, useBootstrapToken) {
|
|
|
186
282
|
}
|
|
187
283
|
},
|
|
188
284
|
}, {
|
|
189
|
-
useBootstrapToken,
|
|
190
285
|
tokenPresent: npmAuthTokenPresent(),
|
|
191
286
|
repository: githubRepositoryFromRootPackage(root),
|
|
192
287
|
});
|
|
@@ -198,6 +293,24 @@ async function publishPackedPackage(root, pkg, tag, dryRun, useBootstrapToken) {
|
|
|
198
293
|
function npmAuthTokenPresent() {
|
|
199
294
|
return Boolean(process.env.NODE_AUTH_TOKEN || process.env.NPM_TOKEN || process.env.NPM_CONFIG_USERCONFIG);
|
|
200
295
|
}
|
|
296
|
+
async function publishPlaceholderPackage(root, pkg, env) {
|
|
297
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'smoo-npm-bootstrap-'));
|
|
298
|
+
try {
|
|
299
|
+
await writeFile(join(tempDir, 'package.json'), `${JSON.stringify({
|
|
300
|
+
name: pkg.name,
|
|
301
|
+
version: NPM_BOOTSTRAP_VERSION,
|
|
302
|
+
description: `Bootstrap placeholder for ${pkg.name}. Real releases are published by SmoothBricks CI.`,
|
|
303
|
+
license: stringProperty(pkg.json, 'license') ?? 'UNLICENSED',
|
|
304
|
+
repository: pkg.json.repository,
|
|
305
|
+
publishConfig: { access: 'public' },
|
|
306
|
+
}, null, 2)}\n`);
|
|
307
|
+
await writeFile(join(tempDir, 'README.md'), `# ${pkg.name}\n\nThis is a bootstrap placeholder. Real package versions are published by SmoothBricks release automation.\n`);
|
|
308
|
+
await runLatestNpm(root, ['publish', tempDir, '--access', 'public', '--tag', NPM_BOOTSTRAP_DIST_TAG], env);
|
|
309
|
+
}
|
|
310
|
+
finally {
|
|
311
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
312
|
+
}
|
|
313
|
+
}
|
|
201
314
|
function safeTarballPrefix(name) {
|
|
202
315
|
return name.replace(/^@/, '').replace(/[^a-zA-Z0-9._-]+/g, '-');
|
|
203
316
|
}
|
|
@@ -216,9 +329,25 @@ function releasePackages(root) {
|
|
|
216
329
|
return packages;
|
|
217
330
|
}
|
|
218
331
|
function releasePackageProjects(packages) {
|
|
219
|
-
return packages.map((pkg) => pkg.
|
|
332
|
+
return packages.map((pkg) => pkg.projectName).join(',');
|
|
333
|
+
}
|
|
334
|
+
async function releaseVersionPackages(root, packages, bump) {
|
|
335
|
+
if (bump !== 'auto') {
|
|
336
|
+
return packages;
|
|
337
|
+
}
|
|
338
|
+
return autoReleaseCandidatePackages({
|
|
339
|
+
gitRefExists: (ref) => gitRefExists(root, ref),
|
|
340
|
+
packageChangedFilesSince: (ref, packagePath) => packageChangedFilesSince(root, ref, packagePath),
|
|
341
|
+
packageJsonAtRef: (ref, packagePath) => packageJsonAtRef(root, ref, packagePath),
|
|
342
|
+
currentPackageJson: (packagePath) => currentPackageJson(root, packagePath),
|
|
343
|
+
packageBuildInputPatterns: (projectName, packagePath) => packageBuildInputPatterns(root, projectName, packagePath),
|
|
344
|
+
packageHasHistory: (packagePath) => packageHasHistory(root, packagePath),
|
|
345
|
+
}, packages);
|
|
220
346
|
}
|
|
221
347
|
async function runNxReleaseVersion(root, projects, bump, dryRun) {
|
|
348
|
+
if (dryRun) {
|
|
349
|
+
return runNxReleaseVersionPreview(root, projects, bump);
|
|
350
|
+
}
|
|
222
351
|
// Nx owns local release mutation: package versions, bun.lock updates, the
|
|
223
352
|
// release commit, and annotated tags. smoo owns remote publication after the
|
|
224
353
|
// workflow validates the exact release commit Nx produced.
|
|
@@ -227,10 +356,66 @@ async function runNxReleaseVersion(root, projects, bump, dryRun) {
|
|
|
227
356
|
nxArgs.push(bump);
|
|
228
357
|
}
|
|
229
358
|
nxArgs.push(`--projects=${projects}`, '--git-commit=true', '--git-tag=true', '--git-push=false');
|
|
230
|
-
if (dryRun) {
|
|
231
|
-
nxArgs.push('--dry-run');
|
|
232
|
-
}
|
|
233
359
|
await run('nx', nxArgs, root);
|
|
360
|
+
return [];
|
|
361
|
+
}
|
|
362
|
+
async function runNxReleaseVersionPreview(root, projects, bump) {
|
|
363
|
+
return withNxWorkspaceRoot(root, async () => {
|
|
364
|
+
const { createAPI } = await import('nx/src/command-line/release/version.js');
|
|
365
|
+
const { createAPI: createChangelogAPI } = await import('nx/src/command-line/release/changelog.js');
|
|
366
|
+
const projectNames = projects.split(',').filter(Boolean);
|
|
367
|
+
const result = await createAPI({}, false)({
|
|
368
|
+
specifier: bump === 'auto' ? undefined : bump,
|
|
369
|
+
projects: projectNames,
|
|
370
|
+
gitCommit: true,
|
|
371
|
+
gitTag: true,
|
|
372
|
+
gitPush: false,
|
|
373
|
+
dryRun: true,
|
|
374
|
+
});
|
|
375
|
+
const changelogResult = await createChangelogAPI({
|
|
376
|
+
changelog: {
|
|
377
|
+
workspaceChangelog: false,
|
|
378
|
+
projectChangelogs: { createRelease: false, file: false },
|
|
379
|
+
},
|
|
380
|
+
}, false)({
|
|
381
|
+
projects: projectNames,
|
|
382
|
+
versionData: result.projectsVersionData,
|
|
383
|
+
releaseGraph: result.releaseGraph,
|
|
384
|
+
gitCommit: false,
|
|
385
|
+
gitTag: false,
|
|
386
|
+
gitPush: false,
|
|
387
|
+
stageChanges: false,
|
|
388
|
+
createRelease: false,
|
|
389
|
+
dryRun: true,
|
|
390
|
+
});
|
|
391
|
+
releasePreviewChangelogs.clear();
|
|
392
|
+
for (const [projectName, changelog] of Object.entries(changelogResult.projectChangelogs ?? {})) {
|
|
393
|
+
releasePreviewChangelogs.set(projectName, changelog.contents);
|
|
394
|
+
}
|
|
395
|
+
const packagesByProject = new Map(releasePackages(root).map((pkg) => [pkg.projectName, pkg]));
|
|
396
|
+
return Object.entries(result.projectsVersionData)
|
|
397
|
+
.map(([projectName, data]) => {
|
|
398
|
+
const pkg = packagesByProject.get(projectName);
|
|
399
|
+
if (!pkg || !data.newVersion) {
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
return { ...pkg, version: data.newVersion };
|
|
403
|
+
})
|
|
404
|
+
.filter((pkg) => pkg !== null);
|
|
405
|
+
});
|
|
406
|
+
}
|
|
407
|
+
async function runNxNextPrereleaseVersion(root, projects) {
|
|
408
|
+
await run('nx', [
|
|
409
|
+
'release',
|
|
410
|
+
'version',
|
|
411
|
+
'prerelease',
|
|
412
|
+
`--projects=${projects}`,
|
|
413
|
+
'--preid=next',
|
|
414
|
+
'--git-commit=true',
|
|
415
|
+
'--git-tag=false',
|
|
416
|
+
'--git-push=false',
|
|
417
|
+
'--git-commit-message=chore(release): prepare next prerelease',
|
|
418
|
+
], root);
|
|
234
419
|
}
|
|
235
420
|
async function releasePackagesAtHead(root, packages) {
|
|
236
421
|
return releasePackagesAtRef(root, packages, 'HEAD');
|
|
@@ -299,7 +484,7 @@ async function gitTagsAtRef(root, ref) {
|
|
|
299
484
|
.filter(Boolean);
|
|
300
485
|
}
|
|
301
486
|
async function previousReleaseTag(root, pkg, currentTag) {
|
|
302
|
-
const result = await $ `git tag --list ${`${pkg.
|
|
487
|
+
const result = await $ `git tag --list ${`${pkg.projectName}@*`} --sort=-v:refname --merged ${currentTag}`
|
|
303
488
|
.cwd(root)
|
|
304
489
|
.quiet()
|
|
305
490
|
.nothrow();
|
|
@@ -347,6 +532,11 @@ async function pushReleaseRefs(root, packages) {
|
|
|
347
532
|
await run('git', ['push', '--atomic', remote, ...refspecs], root);
|
|
348
533
|
return true;
|
|
349
534
|
}
|
|
535
|
+
async function pushCurrentBranch(root) {
|
|
536
|
+
const branch = await releaseBranch(root);
|
|
537
|
+
const remote = await releaseRemote(root, branch);
|
|
538
|
+
await run('git', ['push', remote, `HEAD:refs/heads/${branch}`], root);
|
|
539
|
+
}
|
|
350
540
|
function releaseCompletionShell(root) {
|
|
351
541
|
return {
|
|
352
542
|
gitHead: () => gitHead(root),
|
|
@@ -355,12 +545,23 @@ function releaseCompletionShell(root) {
|
|
|
355
545
|
buildReleaseCandidate: (packages) => buildReleaseCandidate(root, packages),
|
|
356
546
|
publishPackage: async (pkg, distTag, dryRun) => {
|
|
357
547
|
const packageExists = dryRun ? true : await npmPackageExists(root, pkg.name);
|
|
358
|
-
|
|
548
|
+
if (!packageExists) {
|
|
549
|
+
throw new Error(missingNpmPackagePublishGuidance(pkg));
|
|
550
|
+
}
|
|
551
|
+
await publishPackedPackage(root, pkg, distTag, dryRun);
|
|
359
552
|
},
|
|
360
553
|
listGithubMissingPackages: (packages) => listMissingGithubReleasePackages(root, packages),
|
|
361
554
|
createGithubRelease: (pkg, dryRun) => createGithubRelease(root, pkg, dryRun),
|
|
362
555
|
};
|
|
363
556
|
}
|
|
557
|
+
function releaseNextShell(root) {
|
|
558
|
+
return {
|
|
559
|
+
bumpStablePackagesToNext: async (packages) => {
|
|
560
|
+
await runNxNextPrereleaseVersion(root, releasePackageProjects(packages));
|
|
561
|
+
await pushCurrentBranch(root);
|
|
562
|
+
},
|
|
563
|
+
};
|
|
564
|
+
}
|
|
364
565
|
function releaseRepairShell(root) {
|
|
365
566
|
return {
|
|
366
567
|
...releaseCompletionShell(root),
|
|
@@ -405,15 +606,23 @@ async function listOwnedReleaseTagRecords(root, ref) {
|
|
|
405
606
|
}
|
|
406
607
|
async function durableReleaseTagState(root, pkg, tag) {
|
|
407
608
|
const packageVersion = `${pkg.name}@${pkg.version}`;
|
|
609
|
+
const githubReleaseTags = releaseTagAliases({ ...pkg, projectName: projectNameFromReleaseTag(tag) });
|
|
408
610
|
const start = Date.now();
|
|
409
|
-
console.log(`${packageVersion}: checking durable state (npm + GitHub Release ${
|
|
611
|
+
console.log(`${packageVersion}: checking durable state (npm + GitHub Release ${githubReleaseTags.join(' or ')}).`);
|
|
410
612
|
const [npmPublished, githubReleasePresent] = await Promise.all([
|
|
411
613
|
npmVersionExists(root, pkg.name, pkg.version),
|
|
412
|
-
|
|
614
|
+
anyGithubReleaseExists(root, githubReleaseTags),
|
|
413
615
|
]);
|
|
414
616
|
console.log(`${packageVersion}: durable state npm=${yesNo(npmPublished)} github=${yesNo(githubReleasePresent)} (${Date.now() - start}ms).`);
|
|
415
617
|
return { npmPublished, githubReleaseExists: githubReleasePresent };
|
|
416
618
|
}
|
|
619
|
+
function projectNameFromReleaseTag(tag) {
|
|
620
|
+
const versionSeparator = tag.lastIndexOf('@');
|
|
621
|
+
if (versionSeparator <= 0) {
|
|
622
|
+
return tag;
|
|
623
|
+
}
|
|
624
|
+
return tag.slice(0, versionSeparator);
|
|
625
|
+
}
|
|
417
626
|
async function gitReleaseTagsByCreatorDate(root) {
|
|
418
627
|
const result = await $ `git for-each-ref --sort=-creatordate --format=${'%(refname:short)%09%(creatordate:unix)%09%(*objectname)%09%(objectname)'} refs/tags`
|
|
419
628
|
.cwd(root)
|
|
@@ -452,7 +661,7 @@ async function createGithubRelease(root, pkg, dryRun) {
|
|
|
452
661
|
const previousTag = await previousReleaseTag(root, pkg, currentTag);
|
|
453
662
|
if (dryRun) {
|
|
454
663
|
await renderNxProjectChangelogContents({ root, pkg, previousTag, dryRun });
|
|
455
|
-
return;
|
|
664
|
+
return null;
|
|
456
665
|
}
|
|
457
666
|
const contents = await renderNxProjectChangelogContents({ root, pkg, previousTag, dryRun });
|
|
458
667
|
await createOrUpdateGithubRelease(pkg, contents, {
|
|
@@ -460,6 +669,7 @@ async function createGithubRelease(root, pkg, dryRun) {
|
|
|
460
669
|
runGhRelease: (args) => run('gh', args, root),
|
|
461
670
|
log: (message) => console.log(message),
|
|
462
671
|
});
|
|
672
|
+
return githubReleaseUrl(root, currentTag);
|
|
463
673
|
}
|
|
464
674
|
function envPresence(name) {
|
|
465
675
|
return `${name}=${process.env[name] ? 'present' : 'missing'}`;
|
|
@@ -494,7 +704,7 @@ async function writeReleaseSummary(summary) {
|
|
|
494
704
|
lines.push(`- Git refs pushed: ${summary.pushed ? 'yes' : 'already current'}`);
|
|
495
705
|
lines.push(`- npm published: ${packageSummary(summary.published)}`);
|
|
496
706
|
lines.push(`- npm already published: ${packageSummary(summary.alreadyPublished)}`);
|
|
497
|
-
lines.push(`- GitHub Releases created/updated: ${
|
|
707
|
+
lines.push(`- GitHub Releases created/updated: ${githubReleaseSummary(summary)}`);
|
|
498
708
|
}
|
|
499
709
|
if (summary.rerunRequired) {
|
|
500
710
|
const message = 'A previous incomplete release was repaired; newer commits remain. Run Publish again.';
|
|
@@ -508,6 +718,32 @@ async function writeReleaseSummary(summary) {
|
|
|
508
718
|
await appendFile(summaryPath, `${text}\n`);
|
|
509
719
|
}
|
|
510
720
|
}
|
|
721
|
+
async function writeReleasePreviewSummary(root, currentPackages, previewPackages) {
|
|
722
|
+
const lines = ['## Release Preview', '', `- Commit: \`${(await gitHead(root)).slice(0, 12)}\``, '- Mode: dry run'];
|
|
723
|
+
if (previewPackages.length === 0) {
|
|
724
|
+
lines.push('- Result: no release needed');
|
|
725
|
+
}
|
|
726
|
+
else {
|
|
727
|
+
lines.push('- Packages:');
|
|
728
|
+
const currentByName = new Map(currentPackages.map((pkg) => [pkg.name, pkg]));
|
|
729
|
+
for (const pkg of previewPackages) {
|
|
730
|
+
lines.push(` - \`${pkg.name}\`: \`${currentByName.get(pkg.name)?.version ?? 'unknown'}\` -> \`${pkg.version}\``);
|
|
731
|
+
}
|
|
732
|
+
for (const pkg of previewPackages) {
|
|
733
|
+
const contents = releasePreviewChangelogs.get(pkg.projectName);
|
|
734
|
+
if (!contents) {
|
|
735
|
+
throw new Error(`Nx did not generate a project changelog for ${pkg.projectName}.`);
|
|
736
|
+
}
|
|
737
|
+
lines.push('', `### ${pkg.name} ${pkg.version}`, '', contents.trim());
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
const text = `${lines.join('\n')}\n`;
|
|
741
|
+
console.log(text.trimEnd());
|
|
742
|
+
const summaryPath = process.env.GITHUB_STEP_SUMMARY;
|
|
743
|
+
if (summaryPath) {
|
|
744
|
+
await appendFile(summaryPath, `${text}\n`);
|
|
745
|
+
}
|
|
746
|
+
}
|
|
511
747
|
async function writeRepairSummary(summaries, dryRun) {
|
|
512
748
|
const lines = ['## Pending Release Repair', '', `- Mode: ${dryRun ? 'dry run' : 'release'}`];
|
|
513
749
|
if (summaries.length === 0) {
|
|
@@ -515,7 +751,7 @@ async function writeRepairSummary(summaries, dryRun) {
|
|
|
515
751
|
}
|
|
516
752
|
else {
|
|
517
753
|
for (const summary of summaries) {
|
|
518
|
-
lines.push(`- Repaired \`${summary.sha.slice(0, 12)}\`: ${packageSummary(summary.packages)}`);
|
|
754
|
+
lines.push(`- ${dryRun ? 'Would repair' : 'Repaired'} \`${summary.sha.slice(0, 12)}\`: ${packageSummary(summary.packages)}`);
|
|
519
755
|
}
|
|
520
756
|
}
|
|
521
757
|
const text = `${lines.join('\n')}\n`;
|
|
@@ -531,6 +767,27 @@ function packageSummary(packages) {
|
|
|
531
767
|
}
|
|
532
768
|
return packages.map((pkg) => `${pkg.name}@${pkg.version}`).join(', ');
|
|
533
769
|
}
|
|
770
|
+
function githubReleaseSummary(summary) {
|
|
771
|
+
if (summary.githubReleaseLinks.length > 0) {
|
|
772
|
+
return summary.githubReleaseLinks.map(({ pkg, url }) => `[${pkg.name}@${pkg.version}](${url})`).join(', ');
|
|
773
|
+
}
|
|
774
|
+
return packageSummary(summary.githubReleases);
|
|
775
|
+
}
|
|
776
|
+
function repairTargetSummary(target) {
|
|
777
|
+
return target.packages
|
|
778
|
+
.map((pkg) => `${pkg.name}@${pkg.version} (${repairReasons(target, pkg).join(' + ')})`)
|
|
779
|
+
.join(', ');
|
|
780
|
+
}
|
|
781
|
+
function repairReasons(target, pkg) {
|
|
782
|
+
const reasons = [];
|
|
783
|
+
if (target.npmPackages.includes(pkg)) {
|
|
784
|
+
reasons.push('npm missing');
|
|
785
|
+
}
|
|
786
|
+
if (target.githubPackages.includes(pkg)) {
|
|
787
|
+
reasons.push('GitHub Release missing');
|
|
788
|
+
}
|
|
789
|
+
return reasons;
|
|
790
|
+
}
|
|
534
791
|
async function fetchReleaseRefs(root, remote, branch) {
|
|
535
792
|
await run('git', ['fetch', '--tags', remote, branch], root);
|
|
536
793
|
}
|
|
@@ -538,6 +795,125 @@ async function gitRefExists(root, ref) {
|
|
|
538
795
|
const result = await $ `git rev-parse --verify ${ref}`.cwd(root).quiet().nothrow();
|
|
539
796
|
return result.exitCode === 0;
|
|
540
797
|
}
|
|
798
|
+
async function packageChangedFilesSince(root, ref, packagePath) {
|
|
799
|
+
const result = await $ `git diff --name-only ${`${ref}..HEAD`} -- ${packagePath}`.cwd(root).quiet().nothrow();
|
|
800
|
+
if (result.exitCode !== 0) {
|
|
801
|
+
throw new Error(`Unable to inspect package changes under ${packagePath}.`);
|
|
802
|
+
}
|
|
803
|
+
const packagePrefix = `${packagePath}/`;
|
|
804
|
+
return decode(result.stdout)
|
|
805
|
+
.split('\n')
|
|
806
|
+
.map((path) => path.trim())
|
|
807
|
+
.filter(Boolean)
|
|
808
|
+
.map((path) => (path.startsWith(packagePrefix) ? path.slice(packagePrefix.length) : path));
|
|
809
|
+
}
|
|
810
|
+
async function packageJsonAtRef(root, ref, packagePath) {
|
|
811
|
+
const result = await $ `git show ${`${ref}:${packagePath}/package.json`}`.cwd(root).quiet().nothrow();
|
|
812
|
+
if (result.exitCode !== 0) {
|
|
813
|
+
return null;
|
|
814
|
+
}
|
|
815
|
+
try {
|
|
816
|
+
const parsed = JSON.parse(decode(result.stdout));
|
|
817
|
+
return isRecord(parsed) ? parsed : null;
|
|
818
|
+
}
|
|
819
|
+
catch {
|
|
820
|
+
return null;
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
async function currentPackageJson(root, packagePath) {
|
|
824
|
+
return readPackageJson(join(root, packagePath, 'package.json'))?.json ?? null;
|
|
825
|
+
}
|
|
826
|
+
async function packageBuildInputPatterns(root, projectName, _packagePath) {
|
|
827
|
+
const project = await nxProjectJson(root, projectName);
|
|
828
|
+
const nxJson = readJsonObject(join(root, 'nx.json')) ?? {};
|
|
829
|
+
return resolveBuildInputPatterns(project, nxJson);
|
|
830
|
+
}
|
|
831
|
+
async function nxProjectJson(root, projectName) {
|
|
832
|
+
const result = await $ `nx show project ${projectName} --json`.cwd(root).quiet();
|
|
833
|
+
const parsed = JSON.parse(decode(result.stdout));
|
|
834
|
+
if (!isRecord(parsed)) {
|
|
835
|
+
throw new Error(`Unable to inspect Nx project ${projectName}.`);
|
|
836
|
+
}
|
|
837
|
+
return parsed;
|
|
838
|
+
}
|
|
839
|
+
function resolveBuildInputPatterns(project, nxJson) {
|
|
840
|
+
const targets = recordProperty(project, 'targets');
|
|
841
|
+
if (!targets) {
|
|
842
|
+
return [];
|
|
843
|
+
}
|
|
844
|
+
return normalizeInputPatterns(collectBuildInputs(targets), nxJson);
|
|
845
|
+
}
|
|
846
|
+
function collectBuildInputs(targets) {
|
|
847
|
+
const build = recordProperty(targets, 'build');
|
|
848
|
+
if (!build) {
|
|
849
|
+
return ['production'];
|
|
850
|
+
}
|
|
851
|
+
const directInputs = stringArrayProperty(build, 'inputs');
|
|
852
|
+
if (directInputs.length > 0) {
|
|
853
|
+
return directInputs;
|
|
854
|
+
}
|
|
855
|
+
const inputs = [];
|
|
856
|
+
for (const dependency of stringArrayProperty(build, 'dependsOn')) {
|
|
857
|
+
if (dependency.startsWith('^')) {
|
|
858
|
+
continue;
|
|
859
|
+
}
|
|
860
|
+
const targetName = dependency.includes(':') ? dependency.split(':')[1] : dependency;
|
|
861
|
+
if (!targetName) {
|
|
862
|
+
continue;
|
|
863
|
+
}
|
|
864
|
+
inputs.push(...stringArrayProperty(recordProperty(targets, targetName), 'inputs'));
|
|
865
|
+
}
|
|
866
|
+
return inputs.length > 0 ? inputs : ['production'];
|
|
867
|
+
}
|
|
868
|
+
function normalizeInputPatterns(inputs, nxJson) {
|
|
869
|
+
const patterns = [];
|
|
870
|
+
const seen = new Set();
|
|
871
|
+
for (const input of inputs) {
|
|
872
|
+
for (const pattern of expandInputPattern(input, nxJson, seen)) {
|
|
873
|
+
patterns.push(pattern);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
return patterns;
|
|
877
|
+
}
|
|
878
|
+
function expandInputPattern(input, nxJson, seen) {
|
|
879
|
+
if (seen.has(input)) {
|
|
880
|
+
return [];
|
|
881
|
+
}
|
|
882
|
+
seen.add(input);
|
|
883
|
+
if (!input.includes('{')) {
|
|
884
|
+
const namedInputs = recordProperty(nxJson, 'namedInputs');
|
|
885
|
+
const namedInput = namedInputs?.[input];
|
|
886
|
+
if (Array.isArray(namedInput)) {
|
|
887
|
+
return namedInput.flatMap((entry) => (typeof entry === 'string' ? expandInputPattern(entry, nxJson, seen) : []));
|
|
888
|
+
}
|
|
889
|
+
return [];
|
|
890
|
+
}
|
|
891
|
+
const excluded = input.startsWith('!');
|
|
892
|
+
const rawInput = excluded ? input.slice(1) : input;
|
|
893
|
+
if (!rawInput.startsWith('{projectRoot}/')) {
|
|
894
|
+
return [];
|
|
895
|
+
}
|
|
896
|
+
const packageRelative = rawInput.slice('{projectRoot}/'.length);
|
|
897
|
+
return [`${excluded ? '!' : ''}${packageRelative}`];
|
|
898
|
+
}
|
|
899
|
+
function recordProperty(record, key) {
|
|
900
|
+
if (!record) {
|
|
901
|
+
return null;
|
|
902
|
+
}
|
|
903
|
+
const value = record[key];
|
|
904
|
+
return isRecord(value) ? value : null;
|
|
905
|
+
}
|
|
906
|
+
function stringArrayProperty(record, key) {
|
|
907
|
+
const value = record?.[key];
|
|
908
|
+
return Array.isArray(value) ? value.filter((entry) => typeof entry === 'string') : [];
|
|
909
|
+
}
|
|
910
|
+
async function packageHasHistory(root, packagePath) {
|
|
911
|
+
const result = await $ `git log --format=%H -- ${packagePath}`.cwd(root).quiet().nothrow();
|
|
912
|
+
if (result.exitCode !== 0) {
|
|
913
|
+
throw new Error(`Unable to inspect package history under ${packagePath}.`);
|
|
914
|
+
}
|
|
915
|
+
return decode(result.stdout).trim().length > 0;
|
|
916
|
+
}
|
|
541
917
|
async function gitIsAncestor(root, ancestor, descendant) {
|
|
542
918
|
const result = await $ `git merge-base --is-ancestor ${ancestor} ${descendant}`.cwd(root).quiet().nothrow();
|
|
543
919
|
return result.exitCode === 0;
|
|
@@ -602,6 +978,17 @@ async function pushRetaggedReleaseTags(root, remote, updates) {
|
|
|
602
978
|
async function githubReleaseExists(root, tag) {
|
|
603
979
|
return (await runStatus('gh', ['release', 'view', tag, '--json', 'tagName'], root, true)) === 0;
|
|
604
980
|
}
|
|
981
|
+
function githubReleaseUrl(root, tag) {
|
|
982
|
+
return `https://github.com/${githubRepositoryFromRootPackage(root)}/releases/tag/${encodeURIComponent(tag)}`;
|
|
983
|
+
}
|
|
984
|
+
async function anyGithubReleaseExists(root, tags) {
|
|
985
|
+
for (const tag of tags) {
|
|
986
|
+
if (await githubReleaseExists(root, tag)) {
|
|
987
|
+
return true;
|
|
988
|
+
}
|
|
989
|
+
}
|
|
990
|
+
return false;
|
|
991
|
+
}
|
|
605
992
|
async function assertRemoteTagExists(root, tag) {
|
|
606
993
|
const branch = await releaseBranch(root);
|
|
607
994
|
const remote = await releaseRemote(root, branch);
|
|
@@ -712,32 +1099,69 @@ function githubRepositoryFromUrl(url) {
|
|
|
712
1099
|
async function runLatestNpm(root, npmArgs, env) {
|
|
713
1100
|
await run('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...npmArgs], root, env);
|
|
714
1101
|
}
|
|
715
|
-
async function
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
return;
|
|
1102
|
+
async function runLatestNpmTrust(root, npmArgs, env) {
|
|
1103
|
+
const status = await runInteractiveStatus('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...npmArgs], root, env);
|
|
1104
|
+
if (status === 0) {
|
|
1105
|
+
return 'configured';
|
|
719
1106
|
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
1107
|
+
throw new Error(`nix shell nixpkgs#nodejs_latest -c npm ${npmArgs.join(' ')} failed with exit code ${status}`);
|
|
1108
|
+
}
|
|
1109
|
+
async function listTrustedPublishers(root, pkg) {
|
|
1110
|
+
const args = ['trust', 'list', pkg.name, '--json'];
|
|
1111
|
+
let result = await runResult('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...args], root);
|
|
1112
|
+
if (result.exitCode !== 0 && /\bEOTP\b/.test(`${result.stdout}\n${result.stderr}`)) {
|
|
1113
|
+
const status = await runInteractiveStatus('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...args], root);
|
|
1114
|
+
if (status !== 0) {
|
|
1115
|
+
throw new Error(`nix shell nixpkgs#nodejs_latest -c npm ${args.join(' ')} failed with exit code ${status}`);
|
|
724
1116
|
}
|
|
725
|
-
await
|
|
726
|
-
return;
|
|
1117
|
+
result = await runResult('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...args], root);
|
|
727
1118
|
}
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
try {
|
|
731
|
-
await writeFile(npmrc, `registry=https://registry.npmjs.org/\n//registry.npmjs.org/:_authToken=${token}\n`);
|
|
732
|
-
await runLatestNpm(root, npmArgs, { NPM_CONFIG_USERCONFIG: npmrc });
|
|
1119
|
+
if (result.exitCode !== 0) {
|
|
1120
|
+
throw new Error(`nix shell nixpkgs#nodejs_latest -c npm ${args.join(' ')} failed with exit code ${result.exitCode}`);
|
|
733
1121
|
}
|
|
734
|
-
|
|
735
|
-
|
|
1122
|
+
return parseTrustedPublishers(result.stdout, pkg.name);
|
|
1123
|
+
}
|
|
1124
|
+
export function parseTrustedPublishers(stdout, packageName) {
|
|
1125
|
+
if (stdout.trim().length === 0) {
|
|
1126
|
+
return [];
|
|
1127
|
+
}
|
|
1128
|
+
const parsed = JSON.parse(stdout);
|
|
1129
|
+
if (parsed === null) {
|
|
1130
|
+
return [];
|
|
736
1131
|
}
|
|
1132
|
+
if (Array.isArray(parsed)) {
|
|
1133
|
+
return parsed.map((value) => parseTrustedPublisher(value, packageName));
|
|
1134
|
+
}
|
|
1135
|
+
if (isRecord(parsed)) {
|
|
1136
|
+
return [parseTrustedPublisher(parsed, packageName)];
|
|
1137
|
+
}
|
|
1138
|
+
throw new Error(`${packageName}: npm trust list returned invalid JSON.`);
|
|
1139
|
+
}
|
|
1140
|
+
function parseTrustedPublisher(value, packageName) {
|
|
1141
|
+
if (!isRecord(value)) {
|
|
1142
|
+
throw new Error(`${packageName}: npm trust list returned invalid trusted publisher entry.`);
|
|
1143
|
+
}
|
|
1144
|
+
const id = stringProperty(value, 'id');
|
|
1145
|
+
const type = stringProperty(value, 'type');
|
|
1146
|
+
if (!id || !type) {
|
|
1147
|
+
throw new Error(`${packageName}: npm trust list returned a trusted publisher without id or type.`);
|
|
1148
|
+
}
|
|
1149
|
+
return {
|
|
1150
|
+
id,
|
|
1151
|
+
type,
|
|
1152
|
+
file: stringProperty(value, 'file') ?? undefined,
|
|
1153
|
+
repository: stringProperty(value, 'repository') ?? undefined,
|
|
1154
|
+
};
|
|
1155
|
+
}
|
|
1156
|
+
async function runLatestNpmPublish(root, npmArgs) {
|
|
1157
|
+
await runLatestNpm(root, npmArgs, { NODE_AUTH_TOKEN: '', NPM_TOKEN: '' });
|
|
1158
|
+
}
|
|
1159
|
+
function missingNpmPackagePublishGuidance(pkg) {
|
|
1160
|
+
return `${pkg.name} does not exist on npm yet. Run smoo release trust-publisher --bootstrap locally before rerunning the Publish workflow.`;
|
|
737
1161
|
}
|
|
738
1162
|
async function promptForNpmOtp(packageName) {
|
|
739
1163
|
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
740
|
-
throw new Error(`npm
|
|
1164
|
+
throw new Error(`npm requires a one-time password for ${packageName}. Pass --otp <code> in non-interactive shells.`);
|
|
741
1165
|
}
|
|
742
1166
|
const mutedOutput = new Writable({
|
|
743
1167
|
write(_chunk, _encoding, callback) {
|