@smoothbricks/cli 0.1.1 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +155 -30
- package/dist/cli.d.ts.map +1 -1
- package/dist/cli.js +91 -7
- package/dist/devenv/index.d.ts +10 -0
- package/dist/devenv/index.d.ts.map +1 -0
- package/dist/devenv/index.js +27 -0
- package/dist/lib/cli-package.d.ts +4 -0
- package/dist/lib/cli-package.d.ts.map +1 -0
- package/dist/lib/cli-package.js +15 -0
- package/dist/lib/run.d.ts +5 -0
- package/dist/lib/run.d.ts.map +1 -1
- package/dist/lib/run.js +13 -0
- package/dist/lib/workspace.d.ts +15 -1
- package/dist/lib/workspace.d.ts.map +1 -1
- package/dist/lib/workspace.js +25 -2
- package/dist/monorepo/commit-msg.d.ts +8 -1
- package/dist/monorepo/commit-msg.d.ts.map +1 -1
- package/dist/monorepo/commit-msg.js +85 -7
- package/dist/monorepo/index.d.ts +19 -2
- package/dist/monorepo/index.d.ts.map +1 -1
- package/dist/monorepo/index.js +104 -16
- package/dist/monorepo/managed-files.js +1 -1
- package/dist/monorepo/nx-sync.d.ts +2 -1
- package/dist/monorepo/nx-sync.d.ts.map +1 -1
- package/dist/monorepo/nx-sync.js +8 -2
- package/dist/monorepo/package-hygiene.d.ts +6 -2
- package/dist/monorepo/package-hygiene.d.ts.map +1 -1
- package/dist/monorepo/package-hygiene.js +12 -4
- package/dist/monorepo/package-policy.d.ts +22 -2
- package/dist/monorepo/package-policy.d.ts.map +1 -1
- package/dist/monorepo/package-policy.js +1167 -8
- package/dist/monorepo/packed-package.d.ts +3 -0
- package/dist/monorepo/packed-package.d.ts.map +1 -1
- package/dist/monorepo/packed-package.js +68 -36
- package/dist/monorepo/packs/index.d.ts +20 -1
- package/dist/monorepo/packs/index.d.ts.map +1 -1
- package/dist/monorepo/packs/index.js +238 -19
- package/dist/monorepo/publish-workflow.d.ts +0 -4
- package/dist/monorepo/publish-workflow.d.ts.map +1 -1
- package/dist/monorepo/publish-workflow.js +5 -12
- package/dist/monorepo/tool-validation.d.ts +22 -0
- package/dist/monorepo/tool-validation.d.ts.map +1 -0
- package/dist/monorepo/tool-validation.js +399 -0
- package/dist/nx/index.d.ts +24 -0
- package/dist/nx/index.d.ts.map +1 -0
- package/dist/nx/index.js +94 -0
- package/dist/release/bootstrap-npm-packages.d.ts +19 -0
- package/dist/release/bootstrap-npm-packages.d.ts.map +1 -0
- package/dist/release/bootstrap-npm-packages.js +58 -0
- package/dist/release/candidates.d.ts +11 -0
- package/dist/release/candidates.d.ts.map +1 -0
- package/dist/release/candidates.js +181 -0
- package/dist/release/core.d.ts +1 -0
- package/dist/release/core.d.ts.map +1 -1
- package/dist/release/github-release.js +2 -2
- package/dist/release/index.d.ts +25 -0
- package/dist/release/index.d.ts.map +1 -1
- package/dist/release/index.js +247 -49
- package/dist/release/npm-auth.d.ts +0 -1
- package/dist/release/npm-auth.d.ts.map +1 -1
- package/dist/release/npm-auth.js +4 -16
- package/dist/release/orchestration.d.ts +2 -1
- package/dist/release/orchestration.d.ts.map +1 -1
- package/dist/release/orchestration.js +9 -1
- package/dist/release/publish-plan.js +1 -1
- package/managed/raw/git-format-staged.yml +54 -0
- package/managed/raw/tooling/git-hooks/commit-msg.sh +1 -1
- package/managed/templates/github/actions/setup-devenv/action.yml +1 -7
- package/managed/templates/github/workflows/ci.yml +1 -0
- package/package.json +15 -3
- package/src/cli.ts +134 -20
- package/src/devenv/index.test.ts +49 -0
- package/src/devenv/index.ts +38 -0
- package/src/lib/cli-package.ts +18 -0
- package/src/lib/run.ts +19 -0
- package/src/lib/workspace.ts +41 -2
- package/src/monorepo/__tests__/publish-workflow.test.ts +16 -9
- package/src/monorepo/commit-msg.test.ts +45 -0
- package/src/monorepo/commit-msg.ts +107 -7
- package/src/monorepo/index.ts +142 -16
- package/src/monorepo/managed-files.ts +1 -1
- package/src/monorepo/nx-sync.ts +9 -2
- package/src/monorepo/package-hygiene.test.ts +34 -0
- package/src/monorepo/package-hygiene.ts +26 -4
- package/src/monorepo/package-policy.test.ts +902 -0
- package/src/monorepo/package-policy.ts +1393 -7
- package/src/monorepo/packed-package.ts +89 -37
- package/src/monorepo/packs/index.test.ts +189 -0
- package/src/monorepo/packs/index.ts +311 -20
- package/src/monorepo/publish-workflow.ts +8 -16
- package/src/monorepo/setup-test-tracing.test.ts +120 -0
- package/src/monorepo/tool-validation.test.ts +261 -0
- package/src/monorepo/tool-validation.ts +462 -0
- package/src/nx/index.test.ts +64 -0
- package/src/nx/index.ts +119 -0
- package/src/release/__tests__/bootstrap-npm-packages.test.ts +127 -0
- package/src/release/__tests__/candidates.test.ts +217 -0
- package/src/release/__tests__/core-properties.test.ts +4 -4
- package/src/release/__tests__/core-scenarios.test.ts +15 -5
- package/src/release/__tests__/core.test.ts +2 -2
- package/src/release/__tests__/fixture-repo.test.ts +22 -13
- package/src/release/__tests__/github-release.test.ts +13 -8
- package/src/release/__tests__/helpers/fixture-repo.ts +1 -0
- package/src/release/__tests__/npm-auth.test.ts +12 -21
- package/src/release/__tests__/orchestration.test.ts +51 -7
- package/src/release/__tests__/publish-plan.test.ts +9 -3
- package/src/release/__tests__/retag-unpublished.test.ts +1 -1
- package/src/release/__tests__/trust-publisher.test.ts +133 -0
- package/src/release/bootstrap-npm-packages.ts +88 -0
- package/src/release/candidates.ts +225 -0
- package/src/release/core.ts +1 -0
- package/src/release/github-release.ts +2 -2
- package/src/release/index.ts +346 -56
- package/src/release/npm-auth.ts +4 -33
- package/src/release/orchestration.ts +12 -2
- package/src/release/publish-plan.ts +1 -1
- package/managed/raw/.git-format-staged.yml +0 -47
package/src/release/index.ts
CHANGED
|
@@ -5,16 +5,24 @@ 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, 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,
|
|
19
27
|
} from './core.js';
|
|
20
28
|
import { createOrUpdateGithubRelease, renderNxProjectChangelogContents } from './github-release.js';
|
|
@@ -46,10 +54,19 @@ export interface ReleaseRepairPendingOptions {
|
|
|
46
54
|
|
|
47
55
|
export interface ReleaseTrustPublisherOptions {
|
|
48
56
|
dryRun?: boolean;
|
|
57
|
+
bootstrap?: boolean;
|
|
49
58
|
otp?: string;
|
|
59
|
+
bootstrapOtp?: string;
|
|
50
60
|
skipLogin?: boolean;
|
|
51
61
|
}
|
|
52
62
|
|
|
63
|
+
export interface ReleaseBootstrapNpmPackagesOptions {
|
|
64
|
+
dryRun?: boolean;
|
|
65
|
+
skipLogin?: boolean;
|
|
66
|
+
otp?: string;
|
|
67
|
+
packages?: string[];
|
|
68
|
+
}
|
|
69
|
+
|
|
53
70
|
export interface ReleaseRetagUnpublishedOptions {
|
|
54
71
|
tags: string[];
|
|
55
72
|
to?: string;
|
|
@@ -63,13 +80,14 @@ export interface ReleaseRetagUnpublishedOptions {
|
|
|
63
80
|
export async function releaseVersion(root: string, options: ReleaseVersionOptions): Promise<void> {
|
|
64
81
|
const bump = releaseBumpArg(options.bump);
|
|
65
82
|
const packages = releasePackages(root);
|
|
66
|
-
const projects = releasePackageProjects(packages);
|
|
67
83
|
const result = await runReleaseVersion(
|
|
68
84
|
{
|
|
69
85
|
releasePackagesAtHead: () => releasePackagesAtHead(root, packages),
|
|
86
|
+
releaseVersionPackages: (releaseBump) => releaseVersionPackages(root, packages, releaseBump),
|
|
70
87
|
ensureLocalReleaseTags: (releasePackages) => ensureLocalReleaseTags(root, releasePackages),
|
|
71
88
|
gitHead: () => gitHead(root),
|
|
72
|
-
runNxReleaseVersion: (releaseBump, dryRun) =>
|
|
89
|
+
runNxReleaseVersion: (releasePackages, releaseBump, dryRun) =>
|
|
90
|
+
runNxReleaseVersion(root, releasePackageProjects(releasePackages), releaseBump, dryRun),
|
|
73
91
|
assertCleanGitTree: () => assertCleanGitTree(root),
|
|
74
92
|
},
|
|
75
93
|
{ bump, dryRun: options.dryRun === true },
|
|
@@ -134,63 +152,152 @@ export async function releaseRepairPending(root: string, options: ReleaseRepairP
|
|
|
134
152
|
export async function releaseTrustPublisher(root: string, options: ReleaseTrustPublisherOptions): Promise<void> {
|
|
135
153
|
const repository = githubRepositoryFromRootPackage(root);
|
|
136
154
|
const workflow = 'publish.yml';
|
|
137
|
-
|
|
155
|
+
await configureTrustedPublishers(
|
|
156
|
+
{
|
|
157
|
+
repository,
|
|
158
|
+
workflow,
|
|
159
|
+
listReleasePackages: () => listReleasePackages(root),
|
|
160
|
+
packageExists: (name) => npmPackageExists(root, name),
|
|
161
|
+
bootstrapNpmPackages: (bootstrapOptions) =>
|
|
162
|
+
bootstrapNpmPackages(
|
|
163
|
+
{
|
|
164
|
+
listReleasePackages: () => listReleasePackages(root),
|
|
165
|
+
packageExists: (name) => npmPackageExists(root, name),
|
|
166
|
+
login: () => runLatestNpm(root, ['login', '--auth-type=web']),
|
|
167
|
+
publishPlaceholder: (pkg, env) => publishPlaceholderPackage(root, pkg, env),
|
|
168
|
+
promptOtp: (packageName) => promptForNpmOtp(packageName),
|
|
169
|
+
log: (message) => console.log(message),
|
|
170
|
+
},
|
|
171
|
+
bootstrapOptions,
|
|
172
|
+
),
|
|
173
|
+
login: () => runLatestNpm(root, ['login', '--auth-type=web']),
|
|
174
|
+
trustPublisher: (pkg, dryRun, env) => {
|
|
175
|
+
const args = ['trust', 'github', pkg.name, '--file', workflow, '--repo', repository, '--yes'];
|
|
176
|
+
if (dryRun) {
|
|
177
|
+
args.push('--dry-run');
|
|
178
|
+
}
|
|
179
|
+
return runLatestNpmTrust(root, args, env);
|
|
180
|
+
},
|
|
181
|
+
promptOtp: (packageName) => promptForNpmOtp(packageName),
|
|
182
|
+
log: (message) => console.log(message),
|
|
183
|
+
error: (message) => console.error(message),
|
|
184
|
+
},
|
|
185
|
+
options,
|
|
186
|
+
);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export interface TrustPublisherShell<Package extends ReleasePackageInfo = ReleasePackageInfo> {
|
|
190
|
+
repository: string;
|
|
191
|
+
workflow: string;
|
|
192
|
+
listReleasePackages(): Package[];
|
|
193
|
+
packageExists(name: string): Promise<boolean>;
|
|
194
|
+
bootstrapNpmPackages(options: BootstrapNpmPackagesOptions): Promise<Package[]>;
|
|
195
|
+
login(): Promise<void>;
|
|
196
|
+
trustPublisher(pkg: Package, dryRun: boolean, env?: Record<string, string>): Promise<TrustPublisherResult>;
|
|
197
|
+
promptOtp(packageName: string): Promise<string>;
|
|
198
|
+
log(message: string): void;
|
|
199
|
+
error(message: string): void;
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
export type TrustPublisherResult = 'configured' | 'already-configured';
|
|
203
|
+
|
|
204
|
+
export async function configureTrustedPublishers<Package extends ReleasePackageInfo>(
|
|
205
|
+
shell: TrustPublisherShell<Package>,
|
|
206
|
+
options: ReleaseTrustPublisherOptions,
|
|
207
|
+
): Promise<void> {
|
|
208
|
+
const packages = shell.listReleasePackages();
|
|
138
209
|
if (packages.length === 0) {
|
|
139
210
|
throw new Error('No owned release packages found.');
|
|
140
211
|
}
|
|
141
212
|
|
|
213
|
+
let bootstrapLoggedIn = false;
|
|
214
|
+
if (options.bootstrap) {
|
|
215
|
+
const bootstrapped = await shell.bootstrapNpmPackages({
|
|
216
|
+
dryRun: options.dryRun === true,
|
|
217
|
+
skipLogin: options.skipLogin === true,
|
|
218
|
+
otp: options.bootstrapOtp,
|
|
219
|
+
packages: [],
|
|
220
|
+
});
|
|
221
|
+
bootstrapLoggedIn = bootstrapped.length > 0 && !options.dryRun && !options.skipLogin;
|
|
222
|
+
}
|
|
223
|
+
|
|
142
224
|
if (!options.dryRun) {
|
|
143
225
|
const packageStates = await Promise.all(
|
|
144
|
-
packages.map(async (pkg) => ((await
|
|
226
|
+
packages.map(async (pkg) => ((await shell.packageExists(pkg.name)) ? null : pkg.name)),
|
|
145
227
|
);
|
|
146
228
|
const missingPackages = packageStates.filter((name): name is string => name !== null);
|
|
147
229
|
if (missingPackages.length > 0) {
|
|
148
230
|
throw new Error(
|
|
149
231
|
'npm trusted publishing can only be configured after packages exist on the registry. ' +
|
|
150
|
-
'
|
|
232
|
+
'Run smoo release trust-publisher --bootstrap locally. ' +
|
|
151
233
|
`Missing packages: ${missingPackages.join(', ')}`,
|
|
152
234
|
);
|
|
153
235
|
}
|
|
154
236
|
}
|
|
155
237
|
|
|
156
|
-
if (!options.dryRun && !options.skipLogin) {
|
|
157
|
-
await
|
|
238
|
+
if (!options.dryRun && !options.skipLogin && !bootstrapLoggedIn) {
|
|
239
|
+
await shell.login();
|
|
158
240
|
}
|
|
159
241
|
|
|
160
242
|
const failedPackages: string[] = [];
|
|
161
243
|
for (const pkg of packages) {
|
|
162
|
-
|
|
163
|
-
const args = ['trust', 'github', pkg.name, '--file', workflow, '--repo', repository, '--yes'];
|
|
164
|
-
if (options.dryRun) {
|
|
165
|
-
args.push('--dry-run');
|
|
166
|
-
}
|
|
244
|
+
shell.log(`${pkg.name}: trusting GitHub Actions ${shell.repository}/${shell.workflow}`);
|
|
167
245
|
while (true) {
|
|
168
|
-
const otp = options.dryRun ? undefined : (options.otp ?? (await
|
|
246
|
+
const otp = options.dryRun ? undefined : (options.otp ?? (await shell.promptOtp(pkg.name)));
|
|
169
247
|
try {
|
|
170
|
-
|
|
248
|
+
const result = await shell.trustPublisher(
|
|
249
|
+
pkg,
|
|
250
|
+
options.dryRun === true,
|
|
251
|
+
otp ? { NPM_CONFIG_OTP: otp } : undefined,
|
|
252
|
+
);
|
|
253
|
+
if (result === 'already-configured') {
|
|
254
|
+
shell.log(`${pkg.name}: npm trusted publisher is already configured; skipping.`);
|
|
255
|
+
}
|
|
171
256
|
break;
|
|
172
257
|
} catch (error) {
|
|
173
|
-
|
|
174
|
-
|
|
258
|
+
shell.error(`${pkg.name}: npm trusted publisher setup failed.`);
|
|
259
|
+
shell.error(error instanceof Error ? error.message : String(error));
|
|
175
260
|
if (!options.dryRun) {
|
|
176
|
-
|
|
261
|
+
shell.error('npm OTP codes are single-use. Generate a fresh OTP before retrying this package.');
|
|
177
262
|
}
|
|
178
263
|
if (options.otp || options.dryRun) {
|
|
179
264
|
failedPackages.push(pkg.name);
|
|
180
265
|
break;
|
|
181
266
|
}
|
|
182
|
-
|
|
267
|
+
shell.error('Retrying this package. Press Ctrl-C to stop.');
|
|
183
268
|
}
|
|
184
269
|
}
|
|
185
270
|
}
|
|
186
271
|
if (failedPackages.length > 0) {
|
|
187
|
-
|
|
272
|
+
shell.error(`Trusted publishing was not configured for: ${failedPackages.join(', ')}`);
|
|
188
273
|
if (options.otp) {
|
|
189
|
-
|
|
274
|
+
shell.error('Rerun smoo release trust-publisher without --otp to enter a fresh OTP for each failed package.');
|
|
190
275
|
}
|
|
191
276
|
}
|
|
192
277
|
}
|
|
193
278
|
|
|
279
|
+
export async function releaseBootstrapNpmPackages(
|
|
280
|
+
root: string,
|
|
281
|
+
options: ReleaseBootstrapNpmPackagesOptions,
|
|
282
|
+
): Promise<void> {
|
|
283
|
+
await bootstrapNpmPackages(
|
|
284
|
+
{
|
|
285
|
+
listReleasePackages: () => listReleasePackages(root),
|
|
286
|
+
packageExists: (name) => npmPackageExists(root, name),
|
|
287
|
+
login: () => runLatestNpm(root, ['login', '--auth-type=web']),
|
|
288
|
+
publishPlaceholder: (pkg, env) => publishPlaceholderPackage(root, pkg, env),
|
|
289
|
+
promptOtp: (packageName) => promptForNpmOtp(packageName),
|
|
290
|
+
log: (message) => console.log(message),
|
|
291
|
+
},
|
|
292
|
+
{
|
|
293
|
+
dryRun: options.dryRun === true,
|
|
294
|
+
skipLogin: options.skipLogin === true,
|
|
295
|
+
otp: options.otp,
|
|
296
|
+
packages: options.packages ?? [],
|
|
297
|
+
},
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
|
|
194
301
|
export async function releaseRetagUnpublished(root: string, options: ReleaseRetagUnpublishedOptions): Promise<void> {
|
|
195
302
|
const toRef = options.to ?? 'HEAD';
|
|
196
303
|
const dispatch = options.dispatch === true;
|
|
@@ -243,21 +350,14 @@ async function listUnpublishedPackages(root: string, packages: ReleasePackage[])
|
|
|
243
350
|
return states.filter((state) => !state.published).map((state) => state.pkg);
|
|
244
351
|
}
|
|
245
352
|
|
|
246
|
-
async function publishPackedPackage(
|
|
247
|
-
root: string,
|
|
248
|
-
pkg: ReleasePackage,
|
|
249
|
-
tag: string,
|
|
250
|
-
dryRun: boolean,
|
|
251
|
-
useBootstrapToken: boolean,
|
|
252
|
-
): Promise<void> {
|
|
353
|
+
async function publishPackedPackage(root: string, pkg: ReleasePackage, tag: string, dryRun: boolean): Promise<void> {
|
|
253
354
|
const tempDir = await mkdtemp(join(tmpdir(), 'smoo-publish-'));
|
|
254
355
|
const tarball = join(tempDir, `${safeTarballPrefix(pkg.name)}-${pkg.version}.tgz`);
|
|
255
356
|
try {
|
|
256
357
|
console.log(`${pkg.name}@${pkg.version}: packing with bun pm pack`);
|
|
257
358
|
await run('bun', ['pm', 'pack', '--filename', tarball, '--ignore-scripts', '--quiet'], join(root, pkg.path));
|
|
258
359
|
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.
|
|
360
|
+
// npm CLI owns authentication here: trusted publishing OIDC when configured.
|
|
261
361
|
// Bun still produces the tarball so workspace:* dependencies are resolved the
|
|
262
362
|
// same way smoo validates packed packages before release.
|
|
263
363
|
const args = ['publish', tarball, '--access', 'public', '--tag', tag, '--provenance'];
|
|
@@ -267,7 +367,7 @@ async function publishPackedPackage(
|
|
|
267
367
|
await publishWithAuthDiagnostics(
|
|
268
368
|
pkg,
|
|
269
369
|
{
|
|
270
|
-
publish: () => runLatestNpmPublish(root, args
|
|
370
|
+
publish: () => runLatestNpmPublish(root, args),
|
|
271
371
|
versionExists: () => npmVersionExists(root, pkg.name, pkg.version),
|
|
272
372
|
log: (message) => console.log(message),
|
|
273
373
|
error: (message) => console.error(message),
|
|
@@ -279,7 +379,6 @@ async function publishPackedPackage(
|
|
|
279
379
|
},
|
|
280
380
|
},
|
|
281
381
|
{
|
|
282
|
-
useBootstrapToken,
|
|
283
382
|
tokenPresent: npmAuthTokenPresent(),
|
|
284
383
|
repository: githubRepositoryFromRootPackage(root),
|
|
285
384
|
},
|
|
@@ -293,6 +392,38 @@ function npmAuthTokenPresent(): boolean {
|
|
|
293
392
|
return Boolean(process.env.NODE_AUTH_TOKEN || process.env.NPM_TOKEN || process.env.NPM_CONFIG_USERCONFIG);
|
|
294
393
|
}
|
|
295
394
|
|
|
395
|
+
async function publishPlaceholderPackage(
|
|
396
|
+
root: string,
|
|
397
|
+
pkg: ReleasePackage,
|
|
398
|
+
env?: Record<string, string>,
|
|
399
|
+
): Promise<void> {
|
|
400
|
+
const tempDir = await mkdtemp(join(tmpdir(), 'smoo-npm-bootstrap-'));
|
|
401
|
+
try {
|
|
402
|
+
await writeFile(
|
|
403
|
+
join(tempDir, 'package.json'),
|
|
404
|
+
`${JSON.stringify(
|
|
405
|
+
{
|
|
406
|
+
name: pkg.name,
|
|
407
|
+
version: NPM_BOOTSTRAP_VERSION,
|
|
408
|
+
description: `Bootstrap placeholder for ${pkg.name}. Real releases are published by SmoothBricks CI.`,
|
|
409
|
+
license: stringProperty(pkg.json, 'license') ?? 'UNLICENSED',
|
|
410
|
+
repository: pkg.json.repository,
|
|
411
|
+
publishConfig: { access: 'public' },
|
|
412
|
+
},
|
|
413
|
+
null,
|
|
414
|
+
2,
|
|
415
|
+
)}\n`,
|
|
416
|
+
);
|
|
417
|
+
await writeFile(
|
|
418
|
+
join(tempDir, 'README.md'),
|
|
419
|
+
`# ${pkg.name}\n\nThis is a bootstrap placeholder. Real package versions are published by SmoothBricks release automation.\n`,
|
|
420
|
+
);
|
|
421
|
+
await runLatestNpm(root, ['publish', tempDir, '--access', 'public', '--tag', NPM_BOOTSTRAP_DIST_TAG], env);
|
|
422
|
+
} finally {
|
|
423
|
+
await rm(tempDir, { recursive: true, force: true });
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
296
427
|
function safeTarballPrefix(name: string): string {
|
|
297
428
|
return name.replace(/^@/, '').replace(/[^a-zA-Z0-9._-]+/g, '-');
|
|
298
429
|
}
|
|
@@ -314,7 +445,29 @@ function releasePackages(root: string): ReleasePackage[] {
|
|
|
314
445
|
}
|
|
315
446
|
|
|
316
447
|
function releasePackageProjects(packages: ReleasePackage[]): string {
|
|
317
|
-
return packages.map((pkg) => pkg.
|
|
448
|
+
return packages.map((pkg) => pkg.projectName).join(',');
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
async function releaseVersionPackages(
|
|
452
|
+
root: string,
|
|
453
|
+
packages: ReleasePackage[],
|
|
454
|
+
bump: string,
|
|
455
|
+
): Promise<ReleasePackage[]> {
|
|
456
|
+
if (bump !== 'auto') {
|
|
457
|
+
return packages;
|
|
458
|
+
}
|
|
459
|
+
return autoReleaseCandidatePackages(
|
|
460
|
+
{
|
|
461
|
+
gitRefExists: (ref) => gitRefExists(root, ref),
|
|
462
|
+
packageChangedFilesSince: (ref, packagePath) => packageChangedFilesSince(root, ref, packagePath),
|
|
463
|
+
packageJsonAtRef: (ref, packagePath) => packageJsonAtRef(root, ref, packagePath),
|
|
464
|
+
currentPackageJson: (packagePath) => currentPackageJson(root, packagePath),
|
|
465
|
+
packageBuildInputPatterns: (projectName, packagePath) =>
|
|
466
|
+
packageBuildInputPatterns(root, projectName, packagePath),
|
|
467
|
+
packageHasHistory: (packagePath) => packageHasHistory(root, packagePath),
|
|
468
|
+
},
|
|
469
|
+
packages,
|
|
470
|
+
);
|
|
318
471
|
}
|
|
319
472
|
|
|
320
473
|
async function runNxReleaseVersion(root: string, projects: string, bump: string, dryRun: boolean): Promise<void> {
|
|
@@ -475,7 +628,10 @@ function releaseCompletionShell(root: string): ReleaseCompletionShell<ReleasePac
|
|
|
475
628
|
buildReleaseCandidate: (packages) => buildReleaseCandidate(root, packages),
|
|
476
629
|
publishPackage: async (pkg, distTag, dryRun) => {
|
|
477
630
|
const packageExists = dryRun ? true : await npmPackageExists(root, pkg.name);
|
|
478
|
-
|
|
631
|
+
if (!packageExists) {
|
|
632
|
+
throw new Error(missingNpmPackagePublishGuidance(pkg));
|
|
633
|
+
}
|
|
634
|
+
await publishPackedPackage(root, pkg, distTag, dryRun);
|
|
479
635
|
},
|
|
480
636
|
listGithubMissingPackages: (packages) => listMissingGithubReleasePackages(root, packages),
|
|
481
637
|
createGithubRelease: (pkg, dryRun) => createGithubRelease(root, pkg, dryRun),
|
|
@@ -685,6 +841,140 @@ async function gitRefExists(root: string, ref: string): Promise<boolean> {
|
|
|
685
841
|
return result.exitCode === 0;
|
|
686
842
|
}
|
|
687
843
|
|
|
844
|
+
async function packageChangedFilesSince(root: string, ref: string, packagePath: string): Promise<string[]> {
|
|
845
|
+
const result = await $`git diff --name-only ${`${ref}..HEAD`} -- ${packagePath}`.cwd(root).quiet().nothrow();
|
|
846
|
+
if (result.exitCode !== 0) {
|
|
847
|
+
throw new Error(`Unable to inspect package changes under ${packagePath}.`);
|
|
848
|
+
}
|
|
849
|
+
const packagePrefix = `${packagePath}/`;
|
|
850
|
+
return decode(result.stdout)
|
|
851
|
+
.split('\n')
|
|
852
|
+
.map((path) => path.trim())
|
|
853
|
+
.filter(Boolean)
|
|
854
|
+
.map((path) => (path.startsWith(packagePrefix) ? path.slice(packagePrefix.length) : path));
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
async function packageJsonAtRef(
|
|
858
|
+
root: string,
|
|
859
|
+
ref: string,
|
|
860
|
+
packagePath: string,
|
|
861
|
+
): Promise<Record<string, unknown> | null> {
|
|
862
|
+
const result = await $`git show ${`${ref}:${packagePath}/package.json`}`.cwd(root).quiet().nothrow();
|
|
863
|
+
if (result.exitCode !== 0) {
|
|
864
|
+
return null;
|
|
865
|
+
}
|
|
866
|
+
try {
|
|
867
|
+
const parsed = JSON.parse(decode(result.stdout));
|
|
868
|
+
return isRecord(parsed) ? parsed : null;
|
|
869
|
+
} catch {
|
|
870
|
+
return null;
|
|
871
|
+
}
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
async function currentPackageJson(root: string, packagePath: string): Promise<Record<string, unknown> | null> {
|
|
875
|
+
return readPackageJson(join(root, packagePath, 'package.json'))?.json ?? null;
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
async function packageBuildInputPatterns(root: string, projectName: string, _packagePath: string): Promise<string[]> {
|
|
879
|
+
const project = await nxProjectJson(root, projectName);
|
|
880
|
+
const nxJson = readJsonObject(join(root, 'nx.json')) ?? {};
|
|
881
|
+
return resolveBuildInputPatterns(project, nxJson);
|
|
882
|
+
}
|
|
883
|
+
|
|
884
|
+
async function nxProjectJson(root: string, projectName: string): Promise<Record<string, unknown>> {
|
|
885
|
+
const result = await $`nx show project ${projectName} --json`.cwd(root).quiet();
|
|
886
|
+
const parsed = JSON.parse(decode(result.stdout));
|
|
887
|
+
if (!isRecord(parsed)) {
|
|
888
|
+
throw new Error(`Unable to inspect Nx project ${projectName}.`);
|
|
889
|
+
}
|
|
890
|
+
return parsed;
|
|
891
|
+
}
|
|
892
|
+
|
|
893
|
+
function resolveBuildInputPatterns(project: Record<string, unknown>, nxJson: Record<string, unknown>): string[] {
|
|
894
|
+
const targets = recordProperty(project, 'targets');
|
|
895
|
+
if (!targets) {
|
|
896
|
+
return [];
|
|
897
|
+
}
|
|
898
|
+
return normalizeInputPatterns(collectBuildInputs(targets), nxJson);
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
function collectBuildInputs(targets: Record<string, unknown>): string[] {
|
|
902
|
+
const build = recordProperty(targets, 'build');
|
|
903
|
+
if (!build) {
|
|
904
|
+
return ['production'];
|
|
905
|
+
}
|
|
906
|
+
const directInputs = stringArrayProperty(build, 'inputs');
|
|
907
|
+
if (directInputs.length > 0) {
|
|
908
|
+
return directInputs;
|
|
909
|
+
}
|
|
910
|
+
const inputs: string[] = [];
|
|
911
|
+
for (const dependency of stringArrayProperty(build, 'dependsOn')) {
|
|
912
|
+
if (dependency.startsWith('^')) {
|
|
913
|
+
continue;
|
|
914
|
+
}
|
|
915
|
+
const targetName = dependency.includes(':') ? dependency.split(':')[1] : dependency;
|
|
916
|
+
if (!targetName) {
|
|
917
|
+
continue;
|
|
918
|
+
}
|
|
919
|
+
inputs.push(...stringArrayProperty(recordProperty(targets, targetName), 'inputs'));
|
|
920
|
+
}
|
|
921
|
+
return inputs.length > 0 ? inputs : ['production'];
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
function normalizeInputPatterns(inputs: string[], nxJson: Record<string, unknown>): string[] {
|
|
925
|
+
const patterns: string[] = [];
|
|
926
|
+
const seen = new Set<string>();
|
|
927
|
+
for (const input of inputs) {
|
|
928
|
+
for (const pattern of expandInputPattern(input, nxJson, seen)) {
|
|
929
|
+
patterns.push(pattern);
|
|
930
|
+
}
|
|
931
|
+
}
|
|
932
|
+
return patterns;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
function expandInputPattern(input: string, nxJson: Record<string, unknown>, seen: Set<string>): string[] {
|
|
936
|
+
if (seen.has(input)) {
|
|
937
|
+
return [];
|
|
938
|
+
}
|
|
939
|
+
seen.add(input);
|
|
940
|
+
if (!input.includes('{')) {
|
|
941
|
+
const namedInputs = recordProperty(nxJson, 'namedInputs');
|
|
942
|
+
const namedInput = namedInputs?.[input];
|
|
943
|
+
if (Array.isArray(namedInput)) {
|
|
944
|
+
return namedInput.flatMap((entry) => (typeof entry === 'string' ? expandInputPattern(entry, nxJson, seen) : []));
|
|
945
|
+
}
|
|
946
|
+
return [];
|
|
947
|
+
}
|
|
948
|
+
const excluded = input.startsWith('!');
|
|
949
|
+
const rawInput = excluded ? input.slice(1) : input;
|
|
950
|
+
if (!rawInput.startsWith('{projectRoot}/')) {
|
|
951
|
+
return [];
|
|
952
|
+
}
|
|
953
|
+
const packageRelative = rawInput.slice('{projectRoot}/'.length);
|
|
954
|
+
return [`${excluded ? '!' : ''}${packageRelative}`];
|
|
955
|
+
}
|
|
956
|
+
|
|
957
|
+
function recordProperty(record: Record<string, unknown> | null, key: string): Record<string, unknown> | null {
|
|
958
|
+
if (!record) {
|
|
959
|
+
return null;
|
|
960
|
+
}
|
|
961
|
+
const value = record[key];
|
|
962
|
+
return isRecord(value) ? value : null;
|
|
963
|
+
}
|
|
964
|
+
|
|
965
|
+
function stringArrayProperty(record: Record<string, unknown> | null, key: string): string[] {
|
|
966
|
+
const value = record?.[key];
|
|
967
|
+
return Array.isArray(value) ? value.filter((entry): entry is string => typeof entry === 'string') : [];
|
|
968
|
+
}
|
|
969
|
+
|
|
970
|
+
async function packageHasHistory(root: string, packagePath: string): Promise<boolean> {
|
|
971
|
+
const result = await $`git log --format=%H -- ${packagePath}`.cwd(root).quiet().nothrow();
|
|
972
|
+
if (result.exitCode !== 0) {
|
|
973
|
+
throw new Error(`Unable to inspect package history under ${packagePath}.`);
|
|
974
|
+
}
|
|
975
|
+
return decode(result.stdout).trim().length > 0;
|
|
976
|
+
}
|
|
977
|
+
|
|
688
978
|
async function gitIsAncestor(root: string, ancestor: string, descendant: string): Promise<boolean> {
|
|
689
979
|
const result = await $`git merge-base --is-ancestor ${ancestor} ${descendant}`.cwd(root).quiet().nothrow();
|
|
690
980
|
return result.exitCode === 0;
|
|
@@ -887,30 +1177,30 @@ async function runLatestNpm(root: string, npmArgs: string[], env?: Record<string
|
|
|
887
1177
|
await run('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...npmArgs], root, env);
|
|
888
1178
|
}
|
|
889
1179
|
|
|
890
|
-
async function
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
1180
|
+
async function runLatestNpmTrust(
|
|
1181
|
+
root: string,
|
|
1182
|
+
npmArgs: string[],
|
|
1183
|
+
env?: Record<string, string>,
|
|
1184
|
+
): Promise<TrustPublisherResult> {
|
|
1185
|
+
const result = await runResult('nix', ['shell', 'nixpkgs#nodejs_latest', '-c', 'npm', ...npmArgs], root, env);
|
|
1186
|
+
if (result.exitCode === 0) {
|
|
1187
|
+
return 'configured';
|
|
894
1188
|
}
|
|
895
|
-
const
|
|
896
|
-
if (
|
|
897
|
-
|
|
898
|
-
throw new Error(
|
|
899
|
-
'First publish for a package requires NODE_AUTH_TOKEN or NPM_TOKEN until trusted publishing exists.',
|
|
900
|
-
);
|
|
901
|
-
}
|
|
902
|
-
await runLatestNpm(root, npmArgs);
|
|
903
|
-
return;
|
|
1189
|
+
const output = `${result.stdout}\n${result.stderr}`;
|
|
1190
|
+
if (/\bE409\b|\b409 Conflict\b/.test(output)) {
|
|
1191
|
+
return 'already-configured';
|
|
904
1192
|
}
|
|
1193
|
+
throw new Error(
|
|
1194
|
+
`nix shell nixpkgs#nodejs_latest -c npm ${npmArgs.join(' ')} failed with exit code ${result.exitCode}`,
|
|
1195
|
+
);
|
|
1196
|
+
}
|
|
905
1197
|
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
}
|
|
912
|
-
await rm(tempDir, { recursive: true, force: true });
|
|
913
|
-
}
|
|
1198
|
+
async function runLatestNpmPublish(root: string, npmArgs: string[]): Promise<void> {
|
|
1199
|
+
await runLatestNpm(root, npmArgs, { NODE_AUTH_TOKEN: '', NPM_TOKEN: '' });
|
|
1200
|
+
}
|
|
1201
|
+
|
|
1202
|
+
function missingNpmPackagePublishGuidance(pkg: Pick<ReleasePackage, 'name'>): string {
|
|
1203
|
+
return `${pkg.name} does not exist on npm yet. Run smoo release trust-publisher --bootstrap locally before rerunning the Publish workflow.`;
|
|
914
1204
|
}
|
|
915
1205
|
|
|
916
1206
|
async function promptForNpmOtp(packageName: string): Promise<string> {
|
package/src/release/npm-auth.ts
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import type { ReleasePackageInfo } from './core.js';
|
|
2
2
|
|
|
3
3
|
export interface NpmPublishAuthFailureOptions {
|
|
4
|
-
useBootstrapToken: boolean;
|
|
5
4
|
tokenPresent: boolean;
|
|
6
5
|
repository?: string;
|
|
7
6
|
}
|
|
@@ -30,7 +29,7 @@ export async function publishWithAuthDiagnostics(
|
|
|
30
29
|
shell.error(npmPublishAuthFailureMessage(pkg, options));
|
|
31
30
|
await shell.appendSummary(npmPublishAuthFailureMarkdown(pkg, options));
|
|
32
31
|
throw new Error(
|
|
33
|
-
`${packageVersion}: npm publish authentication failed. Run smoo release trust-publisher
|
|
32
|
+
`${packageVersion}: npm publish authentication failed. Run smoo release trust-publisher; see the warning banner above for details.`,
|
|
34
33
|
{ cause: error },
|
|
35
34
|
);
|
|
36
35
|
}
|
|
@@ -42,26 +41,11 @@ export function npmPublishAuthFailureMessage(
|
|
|
42
41
|
): string {
|
|
43
42
|
const packageVersion = `${pkg.name}@${pkg.version}`;
|
|
44
43
|
const lines = [
|
|
45
|
-
`::error title=npm publish authentication failed::${packageVersion} could not be published. This usually means npm trusted publishing is not configured for this package/workflow/repo
|
|
44
|
+
`::error title=npm publish authentication failed::${packageVersion} could not be published. This usually means npm trusted publishing is not configured for this package/workflow/repo.`,
|
|
46
45
|
'',
|
|
47
46
|
`🚨 npm publish authentication failed for ${packageVersion}`,
|
|
48
47
|
'',
|
|
49
48
|
];
|
|
50
|
-
if (options.useBootstrapToken) {
|
|
51
|
-
lines.push(
|
|
52
|
-
'smoo expected a temporary npm automation token because this package does not exist on npm yet.',
|
|
53
|
-
options.tokenPresent
|
|
54
|
-
? 'NODE_AUTH_TOKEN/NPM_TOKEN is set, but npm still rejected the bootstrap publish. Check that the token is valid, has publish rights for this scope, and is available to this workflow.'
|
|
55
|
-
: 'NODE_AUTH_TOKEN/NPM_TOKEN is not set. Add a temporary NPM_TOKEN repository secret and rerun the Publish workflow.',
|
|
56
|
-
'',
|
|
57
|
-
'After the first successful publish, run:',
|
|
58
|
-
' smoo release trust-publisher',
|
|
59
|
-
'',
|
|
60
|
-
'Then remove the temporary bootstrap token path for future releases.',
|
|
61
|
-
);
|
|
62
|
-
return lines.join('\n');
|
|
63
|
-
}
|
|
64
|
-
|
|
65
49
|
lines.push(
|
|
66
50
|
'smoo expected npm trusted publishing/OIDC because this package already exists on npm.',
|
|
67
51
|
options.tokenPresent
|
|
@@ -75,7 +59,7 @@ export function npmPublishAuthFailureMessage(
|
|
|
75
59
|
' workflow: publish.yml',
|
|
76
60
|
'3. Rerun the Publish workflow.',
|
|
77
61
|
'',
|
|
78
|
-
'For first-ever package publishes,
|
|
62
|
+
'For first-ever package publishes, run locally: smoo release trust-publisher --bootstrap.',
|
|
79
63
|
);
|
|
80
64
|
return lines.join('\n');
|
|
81
65
|
}
|
|
@@ -86,19 +70,6 @@ export function npmPublishAuthFailureMarkdown(
|
|
|
86
70
|
): string {
|
|
87
71
|
const packageVersion = `${pkg.name}@${pkg.version}`;
|
|
88
72
|
const lines = ['## 🚨 npm Publish Authentication Failed', '', `Package: \`${packageVersion}\``, ''];
|
|
89
|
-
if (options.useBootstrapToken) {
|
|
90
|
-
lines.push(
|
|
91
|
-
'smoo expected a temporary npm automation token because this package does not exist on npm yet.',
|
|
92
|
-
'',
|
|
93
|
-
options.tokenPresent
|
|
94
|
-
? '`NODE_AUTH_TOKEN`/`NPM_TOKEN` is set, but npm still rejected the bootstrap publish. Check that the token is valid, has publish rights for this scope, and is available to this workflow.'
|
|
95
|
-
: '`NODE_AUTH_TOKEN`/`NPM_TOKEN` is not set. Add a temporary `NPM_TOKEN` repository secret and rerun the Publish workflow.',
|
|
96
|
-
'',
|
|
97
|
-
'After the first successful publish, run `smoo release trust-publisher`, then remove the temporary bootstrap token path for future releases.',
|
|
98
|
-
);
|
|
99
|
-
return lines.join('\n');
|
|
100
|
-
}
|
|
101
|
-
|
|
102
73
|
lines.push(
|
|
103
74
|
'smoo expected npm trusted publishing/OIDC because this package already exists on npm.',
|
|
104
75
|
'',
|
|
@@ -112,7 +83,7 @@ export function npmPublishAuthFailureMarkdown(
|
|
|
112
83
|
`2. Ensure the npm trusted publisher uses repository \`${trustedPublisherRepository(options)}\` and workflow \`publish.yml\``,
|
|
113
84
|
'3. Rerun the Publish workflow.',
|
|
114
85
|
'',
|
|
115
|
-
'For first-ever package publishes,
|
|
86
|
+
'For first-ever package publishes, run `smoo release trust-publisher --bootstrap` locally.',
|
|
116
87
|
);
|
|
117
88
|
return lines.join('\n');
|
|
118
89
|
}
|
|
@@ -35,9 +35,10 @@ export interface ReleaseRepairShell<Package extends ReleasePackageInfo = Release
|
|
|
35
35
|
|
|
36
36
|
export interface ReleaseVersionShell<Package extends ReleasePackageInfo = ReleasePackageInfo> {
|
|
37
37
|
releasePackagesAtHead(): Promise<Package[]>;
|
|
38
|
+
releaseVersionPackages(bump: string): Promise<Package[]>;
|
|
38
39
|
ensureLocalReleaseTags(packages: Package[]): Promise<void>;
|
|
39
40
|
gitHead(): Promise<string>;
|
|
40
|
-
runNxReleaseVersion(bump: string, dryRun: boolean): Promise<void>;
|
|
41
|
+
runNxReleaseVersion(packages: Package[], bump: string, dryRun: boolean): Promise<void>;
|
|
41
42
|
assertCleanGitTree(): Promise<void>;
|
|
42
43
|
}
|
|
43
44
|
|
|
@@ -59,8 +60,17 @@ export async function runReleaseVersion<Package extends ReleasePackageInfo>(
|
|
|
59
60
|
}
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
const versionPackages = await shell.releaseVersionPackages(options.bump);
|
|
64
|
+
if (versionPackages.length === 0) {
|
|
65
|
+
if (options.bump !== 'auto') {
|
|
66
|
+
// invariant throw: the CLI resolves forced bumps from the full owned release package set.
|
|
67
|
+
throw new Error(`No release packages were selected for forced --bump ${options.bump}.`);
|
|
68
|
+
}
|
|
69
|
+
return { mode: 'none', packages: [], status: 'no-release-needed' };
|
|
70
|
+
}
|
|
71
|
+
|
|
62
72
|
const headBeforeVersioning = await shell.gitHead();
|
|
63
|
-
await shell.runNxReleaseVersion(options.bump, options.dryRun);
|
|
73
|
+
await shell.runNxReleaseVersion(versionPackages, options.bump, options.dryRun);
|
|
64
74
|
if (options.dryRun) {
|
|
65
75
|
return { mode: 'none', packages: [], status: 'dry-run' };
|
|
66
76
|
}
|
|
@@ -26,7 +26,7 @@ export function planPublishActions<Package extends ReleasePackageInfo>(
|
|
|
26
26
|
const githubReleasePackages = input.releasePackages.filter((pkg) => githubMissingNames.has(pkg.name));
|
|
27
27
|
|
|
28
28
|
return {
|
|
29
|
-
buildProjects: npmPackages.map((pkg) => pkg.
|
|
29
|
+
buildProjects: npmPackages.map((pkg) => pkg.projectName),
|
|
30
30
|
publishPackages: npmPackages.map((pkg) => ({ pkg, distTag: npmDistTagForVersion(pkg.version) })),
|
|
31
31
|
githubReleasePackages,
|
|
32
32
|
};
|