@ontrails/trails 1.0.0-beta.42 → 1.0.0-beta.45
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/CHANGELOG.md +70 -0
- package/README.md +1 -1
- package/package.json +21 -17
- package/src/mcp-options.ts +1 -0
- package/src/regrade/audit.ts +482 -0
- package/src/regrade/history.ts +250 -42
- package/src/regrade/live-api-preserve.ts +117 -2
- package/src/regrade/plan-artifact.ts +192 -13
- package/src/regrade/plan-derivation.ts +301 -0
- package/src/regrade/source-transaction.ts +75 -0
- package/src/release/bindings.ts +22 -3
- package/src/release/check.ts +312 -10
- package/src/release/config.ts +10 -0
- package/src/release/index.ts +12 -0
- package/src/release/native-bun-publish.ts +403 -90
- package/src/release/native-bun-registry.ts +102 -37
- package/src/release/package-route-facts.ts +146 -0
- package/src/release/policy.ts +2 -2
- package/src/run-completions-install.ts +4 -4
- package/src/run-trace.ts +4 -4
- package/src/trails/dev-support.ts +1 -1
- package/src/trails/regrade.ts +803 -126
- package/src/trails/release-check.ts +8 -0
- package/src/trails/run-example.ts +2 -2
- package/src/trails/survey.ts +6 -6
- package/src/trails/topo-output-schemas.ts +8 -8
- package/src/trails/topo-reports.ts +44 -44
- package/src/trails/topo-store-support.ts +2 -4
- package/src/trails/warden-guide.ts +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/* oxlint-disable eslint-plugin-jest/require-hook, max-statements, func-style -- release script with module-level flow */
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
3
|
+
* Built-in release flow for public `@ontrails/*` workspace publication.
|
|
4
4
|
*
|
|
5
5
|
* Auto-discovers workspaces from the root `package.json` `workspaces` field,
|
|
6
6
|
* topo-sorts them by `workspace:` dependency edges, enforces manifest-range
|
|
@@ -14,6 +14,15 @@ import { mkdtemp, readdir, rm } from 'node:fs/promises';
|
|
|
14
14
|
import { tmpdir } from 'node:os';
|
|
15
15
|
import { join, relative, resolve } from 'node:path';
|
|
16
16
|
|
|
17
|
+
import {
|
|
18
|
+
checkRegistryPosture,
|
|
19
|
+
classifyPackageRegistryState,
|
|
20
|
+
factsFromRegistryResult,
|
|
21
|
+
npmRegistryVersionProofView,
|
|
22
|
+
npmRegistryView,
|
|
23
|
+
} from './native-bun-registry.js';
|
|
24
|
+
import type { PackageRegistryState } from './native-bun-registry.js';
|
|
25
|
+
|
|
17
26
|
const REPO_ROOT = resolve(process.cwd());
|
|
18
27
|
|
|
19
28
|
/** ANSI color helpers, disabled when stdout is not a TTY or `NO_COLOR` is set. */
|
|
@@ -34,6 +43,7 @@ export interface NativeBunPublishOptions {
|
|
|
34
43
|
readonly tag: string | undefined;
|
|
35
44
|
readonly otp: string | undefined;
|
|
36
45
|
readonly only: readonly string[] | undefined;
|
|
46
|
+
readonly trustedPublishing?: boolean;
|
|
37
47
|
}
|
|
38
48
|
|
|
39
49
|
/** Minimal shape of a workspace `package.json` we care about. */
|
|
@@ -45,6 +55,14 @@ export interface NativeBunPublishPackageJson {
|
|
|
45
55
|
devDependencies?: Record<string, string>;
|
|
46
56
|
peerDependencies?: Record<string, string>;
|
|
47
57
|
optionalDependencies?: Record<string, string>;
|
|
58
|
+
scripts?: Record<string, string>;
|
|
59
|
+
repository?:
|
|
60
|
+
| string
|
|
61
|
+
| {
|
|
62
|
+
directory?: string;
|
|
63
|
+
type?: string;
|
|
64
|
+
url?: string;
|
|
65
|
+
};
|
|
48
66
|
}
|
|
49
67
|
|
|
50
68
|
type DependencyField =
|
|
@@ -60,6 +78,8 @@ export interface NativeBunPublishWorkspace {
|
|
|
60
78
|
readonly path: string;
|
|
61
79
|
readonly isPrivate: boolean;
|
|
62
80
|
readonly workspaceDeps: readonly string[];
|
|
81
|
+
readonly repository?: NativeBunPublishPackageJson['repository'];
|
|
82
|
+
readonly publishLifecycleScripts?: readonly string[];
|
|
63
83
|
}
|
|
64
84
|
|
|
65
85
|
const DEPENDENCY_FIELDS: readonly DependencyField[] = [
|
|
@@ -68,19 +88,32 @@ const DEPENDENCY_FIELDS: readonly DependencyField[] = [
|
|
|
68
88
|
'peerDependencies',
|
|
69
89
|
'optionalDependencies',
|
|
70
90
|
];
|
|
91
|
+
const TRUSTED_REPOSITORY_URL =
|
|
92
|
+
'git+https://github.com/outfitter-dev/trails.git';
|
|
93
|
+
const MINIMUM_TRUSTED_NODE = [22, 14, 0] as const;
|
|
94
|
+
const MINIMUM_TRUSTED_NPM = [11, 5, 1] as const;
|
|
95
|
+
const PUBLISH_ONLY_LIFECYCLE_SCRIPTS = [
|
|
96
|
+
'prepublishOnly',
|
|
97
|
+
'publish',
|
|
98
|
+
'postpublish',
|
|
99
|
+
] as const;
|
|
71
100
|
|
|
72
101
|
const USAGE = `Usage: bun scripts/publish.ts [options]
|
|
73
102
|
|
|
74
|
-
Publish all public @ontrails/* workspaces in
|
|
103
|
+
Publish all public @ontrails/* workspaces in dependency order. Bun packs and
|
|
104
|
+
validates each tarball; npm publishes the resolved tarball to the registry.
|
|
75
105
|
|
|
76
106
|
Options:
|
|
77
107
|
--check Pre-publish verification only. Runs \`bun pm pack --dry-run\`
|
|
78
108
|
(required so \`catalog:\` resolves) and asserts the packed
|
|
79
|
-
manifest has no \`workspace:\` or \`catalog:\` ranges
|
|
109
|
+
manifest has no \`workspace:\` or \`catalog:\` ranges and
|
|
110
|
+
validates trusted-publishing repository metadata. No publishing.
|
|
80
111
|
--dry-run Alias for --check.
|
|
81
112
|
--tag <tag> npm dist-tag. Defaults to .changeset/pre.json tag when in
|
|
82
113
|
prerelease mode, otherwise "latest".
|
|
83
114
|
--otp <code> Two-factor code. Also read from BUN_PUBLISH_OTP.
|
|
115
|
+
--trusted-publishing Require GitHub Actions OIDC and npm trusted-publishing
|
|
116
|
+
prerequisites before attempting the first package.
|
|
84
117
|
--only <name[,name]> Restrict to the named packages (repeatable). Useful for
|
|
85
118
|
partial reruns after a mid-matrix failure.
|
|
86
119
|
-h, --help Show this help and exit.
|
|
@@ -96,6 +129,7 @@ const parseArgs = (argv: readonly string[]): NativeBunPublishOptions => {
|
|
|
96
129
|
let mode: NativeBunPublishOptions['mode'] = 'publish';
|
|
97
130
|
let tag: string | undefined;
|
|
98
131
|
let otp: string | undefined = process.env['BUN_PUBLISH_OTP'] || undefined;
|
|
132
|
+
let trustedPublishing = false;
|
|
99
133
|
const only: string[] = [];
|
|
100
134
|
|
|
101
135
|
const needsValue = (flag: string, value: string | undefined): string => {
|
|
@@ -118,6 +152,8 @@ const parseArgs = (argv: readonly string[]): NativeBunPublishOptions => {
|
|
|
118
152
|
} else if (arg === '--otp') {
|
|
119
153
|
i += 1;
|
|
120
154
|
otp = needsValue('--otp', argv[i]);
|
|
155
|
+
} else if (arg === '--trusted-publishing') {
|
|
156
|
+
trustedPublishing = true;
|
|
121
157
|
} else if (arg === '--only') {
|
|
122
158
|
i += 1;
|
|
123
159
|
const value = needsValue('--only', argv[i]);
|
|
@@ -143,6 +179,7 @@ const parseArgs = (argv: readonly string[]): NativeBunPublishOptions => {
|
|
|
143
179
|
only: only.length > 0 ? only : undefined,
|
|
144
180
|
otp,
|
|
145
181
|
tag,
|
|
182
|
+
trustedPublishing,
|
|
146
183
|
};
|
|
147
184
|
};
|
|
148
185
|
|
|
@@ -334,6 +371,10 @@ const discoverWorkspaces = async (): Promise<NativeBunPublishWorkspace[]> => {
|
|
|
334
371
|
isPrivate: pkg.private === true,
|
|
335
372
|
name: pkg.name,
|
|
336
373
|
path: dir,
|
|
374
|
+
publishLifecycleScripts: PUBLISH_ONLY_LIFECYCLE_SCRIPTS.filter(
|
|
375
|
+
(name) => pkg.scripts?.[name] !== undefined
|
|
376
|
+
),
|
|
377
|
+
repository: pkg.repository,
|
|
337
378
|
version: pkg.version ?? '0.0.0',
|
|
338
379
|
workspaceDeps: collectWorkspaceDeps(pkg),
|
|
339
380
|
});
|
|
@@ -433,23 +474,19 @@ const spawnCapture = async (
|
|
|
433
474
|
return { exitCode, stdout };
|
|
434
475
|
};
|
|
435
476
|
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
*/
|
|
442
|
-
const
|
|
443
|
-
ws: NativeBunPublishWorkspace
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
const tmp = await mkdtemp(join(tmpdir(), 'trails-publish-'));
|
|
477
|
+
interface PackedWorkspaceTarball {
|
|
478
|
+
readonly directory: string;
|
|
479
|
+
readonly path: string;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** Pack through Bun so workspace and catalog ranges resolve before npm sees the manifest. */
|
|
483
|
+
const packWorkspaceTarball = async (
|
|
484
|
+
ws: NativeBunPublishWorkspace
|
|
485
|
+
): Promise<PackedWorkspaceTarball> => {
|
|
486
|
+
const directory = await mkdtemp(join(tmpdir(), 'trails-publish-'));
|
|
447
487
|
try {
|
|
448
|
-
// Use `bun pm pack` so the packed manifest reflects what `bun publish`
|
|
449
|
-
// will upload: workspace: and catalog: ranges are resolved the same way.
|
|
450
|
-
// npm pack does not resolve `catalog:` and would produce false positives.
|
|
451
488
|
const pack = await spawnCapture(
|
|
452
|
-
['bun', 'pm', 'pack', '--destination',
|
|
489
|
+
['bun', 'pm', 'pack', '--destination', directory],
|
|
453
490
|
ws.path
|
|
454
491
|
);
|
|
455
492
|
if (pack.exitCode !== 0) {
|
|
@@ -457,78 +494,133 @@ const assertManifestClean = async (
|
|
|
457
494
|
`bun pm pack failed for ${ws.name} (exit ${pack.exitCode})`
|
|
458
495
|
);
|
|
459
496
|
}
|
|
460
|
-
const
|
|
461
|
-
const tarName =
|
|
462
|
-
typeof
|
|
497
|
+
const entries = await readdir(directory);
|
|
498
|
+
const tarName = entries.find((name) =>
|
|
499
|
+
typeof name === 'string'
|
|
500
|
+
? name.endsWith('.tgz')
|
|
501
|
+
: String(name).endsWith('.tgz')
|
|
463
502
|
);
|
|
464
503
|
if (!tarName) {
|
|
465
504
|
throw new Error(`bun pm pack produced no tarball for ${ws.name}`);
|
|
466
505
|
}
|
|
467
|
-
|
|
506
|
+
return { directory, path: join(directory, String(tarName)) };
|
|
507
|
+
} catch (error) {
|
|
508
|
+
await rm(directory, { force: true, recursive: true });
|
|
509
|
+
throw error;
|
|
510
|
+
}
|
|
511
|
+
};
|
|
468
512
|
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
513
|
+
/** Assert that a Bun-packed tarball contains publishable dependency ranges. */
|
|
514
|
+
const assertPackedManifestClean = async (
|
|
515
|
+
ws: NativeBunPublishWorkspace,
|
|
516
|
+
workspacesByName: ReadonlyMap<string, NativeBunPublishWorkspace>,
|
|
517
|
+
tarPath: string
|
|
518
|
+
): Promise<void> => {
|
|
519
|
+
const extract = Bun.spawn(['tar', '-xOf', tarPath, 'package/package.json'], {
|
|
520
|
+
stderr: 'pipe',
|
|
521
|
+
stdin: 'ignore',
|
|
522
|
+
stdout: 'pipe',
|
|
523
|
+
});
|
|
524
|
+
const [manifestText, tarStderr, extractExit] = await Promise.all([
|
|
525
|
+
new Response(extract.stdout).text(),
|
|
526
|
+
new Response(extract.stderr).text(),
|
|
527
|
+
extract.exited,
|
|
528
|
+
]);
|
|
529
|
+
if (extractExit !== 0) {
|
|
530
|
+
const detail = tarStderr.trim() || '(no stderr output)';
|
|
531
|
+
throw new Error(
|
|
532
|
+
`tar extraction failed for ${ws.name} (exit ${extractExit}): ${detail}`
|
|
472
533
|
);
|
|
473
|
-
|
|
474
|
-
new Response(extract.stdout).text(),
|
|
475
|
-
new Response(extract.stderr).text(),
|
|
476
|
-
extract.exited,
|
|
477
|
-
]);
|
|
478
|
-
if (extractExit !== 0) {
|
|
479
|
-
const detail = tarStderr.trim() || '(no stderr output)';
|
|
480
|
-
throw new Error(
|
|
481
|
-
`tar extraction failed for ${ws.name} (exit ${extractExit}): ${detail}`
|
|
482
|
-
);
|
|
483
|
-
}
|
|
534
|
+
}
|
|
484
535
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
536
|
+
let packedPackage: NativeBunPublishPackageJson;
|
|
537
|
+
try {
|
|
538
|
+
packedPackage = JSON.parse(manifestText) as NativeBunPublishPackageJson;
|
|
539
|
+
} catch (error) {
|
|
540
|
+
throw new Error(
|
|
541
|
+
`Invalid packed package.json for ${ws.name}: ${(error as Error).message}`,
|
|
542
|
+
{ cause: error }
|
|
543
|
+
);
|
|
544
|
+
}
|
|
494
545
|
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
}
|
|
500
|
-
}
|
|
501
|
-
if (offenders.length > 0) {
|
|
502
|
-
const relPath = relative(REPO_ROOT, ws.path);
|
|
503
|
-
const hint =
|
|
504
|
-
' Hint: `bun publish` rewrites these at pack time. Verify the package was packed via bun, not npm.';
|
|
505
|
-
throw new Error(
|
|
506
|
-
`Packed manifest for ${ws.name} (${relPath}) contains forbidden ranges:\n${offenders.join('\n')}\n${hint}`
|
|
507
|
-
);
|
|
546
|
+
const offenders: string[] = [];
|
|
547
|
+
for (const [lineNo, line] of manifestText.split('\n').entries()) {
|
|
548
|
+
if (line.includes('"workspace:') || line.includes('"catalog:')) {
|
|
549
|
+
offenders.push(` line ${lineNo + 1}: ${line.trim()}`);
|
|
508
550
|
}
|
|
509
|
-
|
|
510
|
-
|
|
551
|
+
}
|
|
552
|
+
if (offenders.length > 0) {
|
|
553
|
+
const relPath = relative(REPO_ROOT, ws.path);
|
|
554
|
+
throw new Error(
|
|
555
|
+
`Packed manifest for ${ws.name} (${relPath}) contains forbidden ranges:\n${offenders.join('\n')}\n Hint: publish the Bun-packed tarball instead of packing through npm.`
|
|
511
556
|
);
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
}
|
|
523
|
-
|
|
557
|
+
}
|
|
558
|
+
const sourcePackage = await readJson<NativeBunPublishPackageJson>(
|
|
559
|
+
join(ws.path, 'package.json')
|
|
560
|
+
);
|
|
561
|
+
const mismatches = findPackedFirstPartyDependencyMismatches({
|
|
562
|
+
packageName: ws.name,
|
|
563
|
+
packagePath: ws.path,
|
|
564
|
+
packedPackage,
|
|
565
|
+
sourcePackage,
|
|
566
|
+
workspacesByName,
|
|
567
|
+
});
|
|
568
|
+
if (mismatches.length > 0) {
|
|
569
|
+
throw new Error(mismatches.join('\n'));
|
|
524
570
|
}
|
|
525
571
|
};
|
|
526
572
|
|
|
573
|
+
export const unsupportedPublishLifecycleScripts = (
|
|
574
|
+
workspaces: readonly NativeBunPublishWorkspace[]
|
|
575
|
+
): string[] =>
|
|
576
|
+
workspaces.flatMap((workspace) =>
|
|
577
|
+
workspace.isPrivate
|
|
578
|
+
? []
|
|
579
|
+
: (workspace.publishLifecycleScripts ?? []).map(
|
|
580
|
+
(script) => `${workspace.name} defines unsupported ${script}`
|
|
581
|
+
)
|
|
582
|
+
);
|
|
583
|
+
|
|
584
|
+
/** Validate repository metadata required by npm trusted publishing. */
|
|
585
|
+
export const publishRepositoryMetadataErrors = (
|
|
586
|
+
workspaces: readonly NativeBunPublishWorkspace[]
|
|
587
|
+
): string[] =>
|
|
588
|
+
workspaces.flatMap((workspace) => {
|
|
589
|
+
if (workspace.isPrivate) {
|
|
590
|
+
return [];
|
|
591
|
+
}
|
|
592
|
+
const { repository } = workspace;
|
|
593
|
+
const expectedDirectory = relative(REPO_ROOT, workspace.path);
|
|
594
|
+
if (
|
|
595
|
+
typeof repository !== 'string' &&
|
|
596
|
+
repository?.type === 'git' &&
|
|
597
|
+
repository.url === TRUSTED_REPOSITORY_URL &&
|
|
598
|
+
repository.directory === expectedDirectory
|
|
599
|
+
) {
|
|
600
|
+
return [];
|
|
601
|
+
}
|
|
602
|
+
return [
|
|
603
|
+
`${workspace.name} must declare repository ${TRUSTED_REPOSITORY_URL} with directory ${expectedDirectory}`,
|
|
604
|
+
];
|
|
605
|
+
});
|
|
606
|
+
|
|
527
607
|
/** Run `--check` flow: pack dry-run plus manifest-range assertion per package. */
|
|
528
608
|
const runCheck = async (
|
|
529
609
|
workspaces: readonly NativeBunPublishWorkspace[],
|
|
530
610
|
allWorkspaces: readonly NativeBunPublishWorkspace[]
|
|
531
611
|
): Promise<number> => {
|
|
612
|
+
const repositoryErrors = publishRepositoryMetadataErrors(workspaces);
|
|
613
|
+
if (repositoryErrors.length > 0) {
|
|
614
|
+
throw new Error(
|
|
615
|
+
`Package repository metadata check failed:\n${repositoryErrors.map((error) => `- ${error}`).join('\n')}`
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
const unsupportedLifecycle = unsupportedPublishLifecycleScripts(workspaces);
|
|
619
|
+
if (unsupportedLifecycle.length > 0) {
|
|
620
|
+
throw new Error(
|
|
621
|
+
`Tarball publication cannot honor publish-only lifecycle scripts:\n${unsupportedLifecycle.map((error) => `- ${error}`).join('\n')}`
|
|
622
|
+
);
|
|
623
|
+
}
|
|
532
624
|
const workspacesByName = new Map(allWorkspaces.map((ws) => [ws.name, ws]));
|
|
533
625
|
for (const ws of workspaces) {
|
|
534
626
|
if (ws.isPrivate) {
|
|
@@ -545,7 +637,12 @@ const runCheck = async (
|
|
|
545
637
|
return 1;
|
|
546
638
|
}
|
|
547
639
|
try {
|
|
548
|
-
await
|
|
640
|
+
const packed = await packWorkspaceTarball(ws);
|
|
641
|
+
try {
|
|
642
|
+
await assertPackedManifestClean(ws, workspacesByName, packed.path);
|
|
643
|
+
} finally {
|
|
644
|
+
await rm(packed.directory, { force: true, recursive: true });
|
|
645
|
+
}
|
|
549
646
|
} catch (error) {
|
|
550
647
|
fail((error as Error).message);
|
|
551
648
|
return 1;
|
|
@@ -557,36 +654,246 @@ const runCheck = async (
|
|
|
557
654
|
return 0;
|
|
558
655
|
};
|
|
559
656
|
|
|
657
|
+
const compareNumericVersion = (
|
|
658
|
+
version: string,
|
|
659
|
+
minimum: readonly [number, number, number]
|
|
660
|
+
): number => {
|
|
661
|
+
const actual = version
|
|
662
|
+
.split('.')
|
|
663
|
+
.slice(0, 3)
|
|
664
|
+
.map((part) => Number.parseInt(part, 10));
|
|
665
|
+
if (actual.length !== 3 || actual.some((part) => Number.isNaN(part))) {
|
|
666
|
+
return -1;
|
|
667
|
+
}
|
|
668
|
+
for (let index = 0; index < minimum.length; index += 1) {
|
|
669
|
+
const delta = (actual[index] ?? 0) - (minimum[index] ?? 0);
|
|
670
|
+
if (delta !== 0) {
|
|
671
|
+
return delta;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
return 0;
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
export const trustedPublishingPreflightErrors = ({
|
|
678
|
+
env,
|
|
679
|
+
nodeVersion,
|
|
680
|
+
npmVersion,
|
|
681
|
+
workspaces,
|
|
682
|
+
}: {
|
|
683
|
+
readonly env: Readonly<Record<string, string | undefined>>;
|
|
684
|
+
readonly nodeVersion: string;
|
|
685
|
+
readonly npmVersion: string;
|
|
686
|
+
readonly workspaces: readonly NativeBunPublishWorkspace[];
|
|
687
|
+
}): string[] => {
|
|
688
|
+
const errors: string[] = [];
|
|
689
|
+
if (env['GITHUB_ACTIONS'] !== 'true') {
|
|
690
|
+
errors.push('trusted publishing requires a GitHub Actions runner');
|
|
691
|
+
}
|
|
692
|
+
if (!env['ACTIONS_ID_TOKEN_REQUEST_URL']) {
|
|
693
|
+
errors.push('GitHub OIDC request URL is unavailable (id-token: write)');
|
|
694
|
+
}
|
|
695
|
+
if (!env['ACTIONS_ID_TOKEN_REQUEST_TOKEN']) {
|
|
696
|
+
errors.push('GitHub OIDC request token is unavailable (id-token: write)');
|
|
697
|
+
}
|
|
698
|
+
if (compareNumericVersion(nodeVersion, MINIMUM_TRUSTED_NODE) < 0) {
|
|
699
|
+
errors.push(
|
|
700
|
+
`trusted publishing requires Node >= ${MINIMUM_TRUSTED_NODE.join('.')}; found ${nodeVersion}`
|
|
701
|
+
);
|
|
702
|
+
}
|
|
703
|
+
if (compareNumericVersion(npmVersion, MINIMUM_TRUSTED_NPM) < 0) {
|
|
704
|
+
errors.push(
|
|
705
|
+
`trusted publishing requires npm >= ${MINIMUM_TRUSTED_NPM.join('.')}; found ${npmVersion}`
|
|
706
|
+
);
|
|
707
|
+
}
|
|
708
|
+
errors.push(...publishRepositoryMetadataErrors(workspaces));
|
|
709
|
+
return errors;
|
|
710
|
+
};
|
|
711
|
+
|
|
712
|
+
export const createNpmPublishCommand = ({
|
|
713
|
+
otp,
|
|
714
|
+
tag,
|
|
715
|
+
tarballPath,
|
|
716
|
+
}: {
|
|
717
|
+
readonly otp: string | undefined;
|
|
718
|
+
readonly tag: string;
|
|
719
|
+
readonly tarballPath: string;
|
|
720
|
+
}): string[] => {
|
|
721
|
+
const command = [
|
|
722
|
+
'npm',
|
|
723
|
+
'publish',
|
|
724
|
+
tarballPath,
|
|
725
|
+
'--access',
|
|
726
|
+
'public',
|
|
727
|
+
'--tag',
|
|
728
|
+
tag,
|
|
729
|
+
];
|
|
730
|
+
if (otp) {
|
|
731
|
+
command.push('--otp', otp);
|
|
732
|
+
}
|
|
733
|
+
return command;
|
|
734
|
+
};
|
|
735
|
+
|
|
736
|
+
export const publicationActionForRegistryState = (
|
|
737
|
+
state: PackageRegistryState,
|
|
738
|
+
trustedPublishing = false
|
|
739
|
+
): 'block' | 'publish' | 'skip' => {
|
|
740
|
+
if (state.kind === 'complete') {
|
|
741
|
+
return 'skip';
|
|
742
|
+
}
|
|
743
|
+
if (state.kind === 'first-time-package') {
|
|
744
|
+
return trustedPublishing ? 'block' : 'publish';
|
|
745
|
+
}
|
|
746
|
+
if (state.kind === 'needs-publish') {
|
|
747
|
+
return 'publish';
|
|
748
|
+
}
|
|
749
|
+
return 'block';
|
|
750
|
+
};
|
|
751
|
+
|
|
752
|
+
const registryPublicationBlocker = (
|
|
753
|
+
workspace: NativeBunPublishWorkspace,
|
|
754
|
+
state: PackageRegistryState,
|
|
755
|
+
trustedPublishing: boolean
|
|
756
|
+
): string => {
|
|
757
|
+
if (state.kind === 'first-time-package' && trustedPublishing) {
|
|
758
|
+
return `${workspace.name} is not published yet; bootstrap it with credentialed local tooling, configure its npm trusted publisher, then retry`;
|
|
759
|
+
}
|
|
760
|
+
if (state.kind === 'needs-tag-repair') {
|
|
761
|
+
return `${workspace.name}@${workspace.version} is published but the requested dist-tag points at ${state.currentTagVersion ?? '(missing)'}; repair it with credentialed npm tooling before retrying`;
|
|
762
|
+
}
|
|
763
|
+
if (state.kind === 'tag-points-ahead') {
|
|
764
|
+
return `${workspace.name} dist-tag points ahead at ${state.currentTagVersion}; refusing to publish ${workspace.version}`;
|
|
765
|
+
}
|
|
766
|
+
if (state.kind === 'registry-inaccessible') {
|
|
767
|
+
return `Could not inspect ${workspace.name}: ${state.error}`;
|
|
768
|
+
}
|
|
769
|
+
return `Could not determine whether ${workspace.name}@${workspace.version} is ready`;
|
|
770
|
+
};
|
|
771
|
+
|
|
772
|
+
const assertTrustedPublishingReady = async (
|
|
773
|
+
workspaces: readonly NativeBunPublishWorkspace[]
|
|
774
|
+
): Promise<void> => {
|
|
775
|
+
const npmVersionResult = await spawnCapture(['npm', '--version'], REPO_ROOT);
|
|
776
|
+
if (npmVersionResult.exitCode !== 0) {
|
|
777
|
+
throw new Error('Could not read npm version for trusted publishing');
|
|
778
|
+
}
|
|
779
|
+
const nodeVersionResult = await spawnCapture(
|
|
780
|
+
['node', '--version'],
|
|
781
|
+
REPO_ROOT
|
|
782
|
+
);
|
|
783
|
+
if (nodeVersionResult.exitCode !== 0) {
|
|
784
|
+
throw new Error('Could not read Node version for trusted publishing');
|
|
785
|
+
}
|
|
786
|
+
const errors = trustedPublishingPreflightErrors({
|
|
787
|
+
env: process.env,
|
|
788
|
+
nodeVersion: nodeVersionResult.stdout.trim().replace(/^v/, ''),
|
|
789
|
+
npmVersion: npmVersionResult.stdout.trim(),
|
|
790
|
+
workspaces,
|
|
791
|
+
});
|
|
792
|
+
if (errors.length > 0) {
|
|
793
|
+
throw new Error(
|
|
794
|
+
`Trusted publishing preflight failed:\n${errors.map((error) => `- ${error}`).join('\n')}`
|
|
795
|
+
);
|
|
796
|
+
}
|
|
797
|
+
success('Trusted publishing prerequisites are available');
|
|
798
|
+
};
|
|
799
|
+
|
|
560
800
|
/** Run the actual publish flow sequentially. Aborts on first failure. */
|
|
561
801
|
const runPublish = async (
|
|
562
802
|
workspaces: readonly NativeBunPublishWorkspace[],
|
|
803
|
+
allWorkspaces: readonly NativeBunPublishWorkspace[],
|
|
563
804
|
tag: string,
|
|
564
|
-
otp: string | undefined
|
|
805
|
+
otp: string | undefined,
|
|
806
|
+
trustedPublishing: boolean
|
|
565
807
|
): Promise<number> => {
|
|
808
|
+
if (trustedPublishing) {
|
|
809
|
+
await assertTrustedPublishingReady(workspaces);
|
|
810
|
+
}
|
|
811
|
+
const unsupportedLifecycle = unsupportedPublishLifecycleScripts(workspaces);
|
|
812
|
+
if (unsupportedLifecycle.length > 0) {
|
|
813
|
+
throw new Error(
|
|
814
|
+
`Tarball publication cannot honor publish-only lifecycle scripts:\n${unsupportedLifecycle.map((error) => `- ${error}`).join('\n')}`
|
|
815
|
+
);
|
|
816
|
+
}
|
|
817
|
+
const publicWorkspaces = workspaces.filter(
|
|
818
|
+
(workspace) => !workspace.isPrivate
|
|
819
|
+
);
|
|
820
|
+
const registryResults = await checkRegistryPosture(
|
|
821
|
+
publicWorkspaces,
|
|
822
|
+
npmRegistryView,
|
|
823
|
+
npmRegistryVersionProofView,
|
|
824
|
+
tag
|
|
825
|
+
);
|
|
826
|
+
const registryStates = new Map<string, PackageRegistryState>();
|
|
827
|
+
const registryBlockers: string[] = [];
|
|
828
|
+
for (const workspace of publicWorkspaces) {
|
|
829
|
+
const registryResult = registryResults.find(
|
|
830
|
+
(result) => result.name === workspace.name
|
|
831
|
+
);
|
|
832
|
+
if (!registryResult) {
|
|
833
|
+
registryBlockers.push(
|
|
834
|
+
`Could not read registry posture for ${workspace.name}`
|
|
835
|
+
);
|
|
836
|
+
continue;
|
|
837
|
+
}
|
|
838
|
+
const state = classifyPackageRegistryState(
|
|
839
|
+
factsFromRegistryResult(registryResult)
|
|
840
|
+
);
|
|
841
|
+
registryStates.set(workspace.name, state);
|
|
842
|
+
if (
|
|
843
|
+
publicationActionForRegistryState(state, trustedPublishing) === 'block'
|
|
844
|
+
) {
|
|
845
|
+
registryBlockers.push(
|
|
846
|
+
registryPublicationBlocker(workspace, state, trustedPublishing)
|
|
847
|
+
);
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
if (registryBlockers.length > 0) {
|
|
851
|
+
throw new Error(
|
|
852
|
+
`Registry preflight blocked publication before any package was published:\n${registryBlockers.map((error) => `- ${error}`).join('\n')}`
|
|
853
|
+
);
|
|
854
|
+
}
|
|
855
|
+
const workspacesByName = new Map(allWorkspaces.map((ws) => [ws.name, ws]));
|
|
566
856
|
for (const ws of workspaces) {
|
|
567
857
|
if (ws.isPrivate) {
|
|
568
858
|
info(`Skipping ${ws.name} (private)`);
|
|
569
859
|
continue;
|
|
570
860
|
}
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
'--access',
|
|
576
|
-
'public',
|
|
577
|
-
'--tag',
|
|
578
|
-
tag,
|
|
579
|
-
];
|
|
580
|
-
if (otp) {
|
|
581
|
-
cmd.push('--otp', otp);
|
|
861
|
+
const registryState = registryStates.get(ws.name);
|
|
862
|
+
if (!registryState) {
|
|
863
|
+
fail(`Registry preflight did not produce a state for ${ws.name}`);
|
|
864
|
+
return 1;
|
|
582
865
|
}
|
|
583
|
-
const
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
866
|
+
const publicationAction = publicationActionForRegistryState(
|
|
867
|
+
registryState,
|
|
868
|
+
trustedPublishing
|
|
869
|
+
);
|
|
870
|
+
if (publicationAction === 'skip') {
|
|
871
|
+
success(
|
|
872
|
+
`${ws.name}@${ws.version} already published with ${tag} at target; continuing`
|
|
587
873
|
);
|
|
874
|
+
continue;
|
|
875
|
+
}
|
|
876
|
+
if (publicationAction === 'block') {
|
|
877
|
+
fail(registryPublicationBlocker(ws, registryState, trustedPublishing));
|
|
588
878
|
return 1;
|
|
589
879
|
}
|
|
880
|
+
info(`Publishing ${ws.name}@${ws.version}... (tag=${tag})`);
|
|
881
|
+
const packed = await packWorkspaceTarball(ws);
|
|
882
|
+
try {
|
|
883
|
+
await assertPackedManifestClean(ws, workspacesByName, packed.path);
|
|
884
|
+
const code = await spawnInherit(
|
|
885
|
+
createNpmPublishCommand({ otp, tag, tarballPath: packed.path }),
|
|
886
|
+
ws.path
|
|
887
|
+
);
|
|
888
|
+
if (code !== 0) {
|
|
889
|
+
fail(
|
|
890
|
+
`Failed to publish ${ws.name} (exit ${code}); aborting remaining publishes`
|
|
891
|
+
);
|
|
892
|
+
return 1;
|
|
893
|
+
}
|
|
894
|
+
} finally {
|
|
895
|
+
await rm(packed.directory, { force: true, recursive: true });
|
|
896
|
+
}
|
|
590
897
|
success(`${ws.name}@${ws.version} published`);
|
|
591
898
|
}
|
|
592
899
|
console.log('');
|
|
@@ -635,7 +942,13 @@ export const runNativeBunPublishCli = async (
|
|
|
635
942
|
fail('Could not resolve a dist-tag. Pass --tag <tag> explicitly.');
|
|
636
943
|
return 1;
|
|
637
944
|
}
|
|
638
|
-
return await runPublish(
|
|
945
|
+
return await runPublish(
|
|
946
|
+
selected,
|
|
947
|
+
all,
|
|
948
|
+
tag,
|
|
949
|
+
opts.otp,
|
|
950
|
+
opts.trustedPublishing ?? false
|
|
951
|
+
);
|
|
639
952
|
} catch (error) {
|
|
640
953
|
const msg = error instanceof Error ? error.message : String(error);
|
|
641
954
|
fail(msg);
|