@ontrails/trails 1.0.0-beta.43 → 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 +15 -0
- package/package.json +21 -16
- package/src/release/bindings.ts +22 -3
- package/src/release/index.ts +7 -0
- package/src/release/native-bun-publish.ts +403 -90
- package/src/release/native-bun-registry.ts +102 -37
- package/src/release/policy.ts +2 -2
- package/src/trails/regrade.ts +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,20 @@
|
|
|
1
1
|
# trails
|
|
2
2
|
|
|
3
|
+
## 1.0.0-beta.45
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`f1bd093`](https://github.com/outfitter-dev/trails/commit/f1bd09395fcf81db0bcb8657030288877c2e26e6): Recognize conditional, aliased, and parenthesized Result provenance while invalidating provenance after reassignment across the implementation-return and redundant-error-wrap rules.
|
|
8
|
+
- [`872a815`](https://github.com/outfitter-dev/trails/commit/872a815243cae63fae5b16022f102d125bc78ac5): Require strict registry checks to prove exact-version consumer availability instead of trusting package access or dist-tags alone.
|
|
9
|
+
|
|
10
|
+
## 1.0.0-beta.44
|
|
11
|
+
|
|
12
|
+
### Patch Changes
|
|
13
|
+
|
|
14
|
+
- [`b1fbe57`](https://github.com/outfitter-dev/trails/commit/b1fbe574e6f44d1fecb5e3a000270955c0a77b7b): Publish Bun-validated package tarballs through an npm trusted-publishing adapter
|
|
15
|
+
binding, add exact repository metadata for each public workspace package, and
|
|
16
|
+
correct the native Bun release descriptor to its pack-only runtime boundary.
|
|
17
|
+
|
|
3
18
|
## 1.0.0-beta.43
|
|
4
19
|
|
|
5
20
|
### Minor Changes
|
package/package.json
CHANGED
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ontrails/trails",
|
|
3
|
-
"version": "1.0.0-beta.
|
|
3
|
+
"version": "1.0.0-beta.45",
|
|
4
|
+
"repository": {
|
|
5
|
+
"type": "git",
|
|
6
|
+
"url": "git+https://github.com/outfitter-dev/trails.git",
|
|
7
|
+
"directory": "apps/trails"
|
|
8
|
+
},
|
|
4
9
|
"bin": {
|
|
5
10
|
"trails": "./bin/trails.ts"
|
|
6
11
|
},
|
|
@@ -27,25 +32,25 @@
|
|
|
27
32
|
},
|
|
28
33
|
"dependencies": {
|
|
29
34
|
"@clack/prompts": "^1.1.0",
|
|
30
|
-
"@ontrails/adapter-kit": "^1.0.0-beta.
|
|
31
|
-
"@ontrails/cli": "^1.0.0-beta.
|
|
32
|
-
"@ontrails/commander": "^1.0.0-beta.
|
|
33
|
-
"@ontrails/config": "^1.0.0-beta.
|
|
34
|
-
"@ontrails/core": "^1.0.0-beta.
|
|
35
|
-
"@ontrails/http": "^1.0.0-beta.
|
|
36
|
-
"@ontrails/mcp": "^1.0.0-beta.
|
|
37
|
-
"@ontrails/observability": "^1.0.0-beta.
|
|
38
|
-
"@ontrails/permits": "^1.0.0-beta.
|
|
39
|
-
"@ontrails/regrade": "^1.0.0-beta.
|
|
40
|
-
"@ontrails/source": "^1.0.0-beta.
|
|
41
|
-
"@ontrails/topography": "^1.0.0-beta.
|
|
42
|
-
"@ontrails/warden": "^1.0.0-beta.
|
|
35
|
+
"@ontrails/adapter-kit": "^1.0.0-beta.45",
|
|
36
|
+
"@ontrails/cli": "^1.0.0-beta.45",
|
|
37
|
+
"@ontrails/commander": "^1.0.0-beta.45",
|
|
38
|
+
"@ontrails/config": "^1.0.0-beta.45",
|
|
39
|
+
"@ontrails/core": "^1.0.0-beta.45",
|
|
40
|
+
"@ontrails/http": "^1.0.0-beta.45",
|
|
41
|
+
"@ontrails/mcp": "^1.0.0-beta.45",
|
|
42
|
+
"@ontrails/observability": "^1.0.0-beta.45",
|
|
43
|
+
"@ontrails/permits": "^1.0.0-beta.45",
|
|
44
|
+
"@ontrails/regrade": "^1.0.0-beta.45",
|
|
45
|
+
"@ontrails/source": "^1.0.0-beta.45",
|
|
46
|
+
"@ontrails/topography": "^1.0.0-beta.45",
|
|
47
|
+
"@ontrails/warden": "^1.0.0-beta.45",
|
|
43
48
|
"commander": "^14.0.3",
|
|
44
49
|
"typescript": "^5.9.3",
|
|
45
50
|
"zod": "^4.3.5"
|
|
46
51
|
},
|
|
47
52
|
"devDependencies": {
|
|
48
|
-
"@ontrails/cloudflare": "^1.0.0-beta.
|
|
49
|
-
"@ontrails/testing": "^1.0.0-beta.
|
|
53
|
+
"@ontrails/cloudflare": "^1.0.0-beta.45",
|
|
54
|
+
"@ontrails/testing": "^1.0.0-beta.45"
|
|
50
55
|
}
|
|
51
56
|
}
|
package/src/release/bindings.ts
CHANGED
|
@@ -27,13 +27,32 @@ export interface ReleaseBindingDescriptor {
|
|
|
27
27
|
readonly runtime: string;
|
|
28
28
|
}
|
|
29
29
|
|
|
30
|
-
export const
|
|
30
|
+
export const nativeBunPackBinding = {
|
|
31
31
|
boundary: 'trails-owned',
|
|
32
|
-
capabilities: ['pack-check'
|
|
32
|
+
capabilities: ['pack-check'],
|
|
33
33
|
description:
|
|
34
|
-
'
|
|
34
|
+
'Native Bun release binding for Trails-owned package packing and validation.',
|
|
35
35
|
id: 'release.binding.native-bun',
|
|
36
36
|
kind: 'native',
|
|
37
37
|
placement: 'same-package',
|
|
38
38
|
runtime: 'bun',
|
|
39
39
|
} satisfies ReleaseBindingDescriptor;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* @deprecated Use `nativeBunPackBinding`. This alias preserves the exported
|
|
43
|
+
* name while correcting its descriptor to the Bun-owned pack boundary.
|
|
44
|
+
*/
|
|
45
|
+
export const nativeBunReleaseBinding = Object.freeze({
|
|
46
|
+
...nativeBunPackBinding,
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
export const npmReleaseAdapterBinding = {
|
|
50
|
+
boundary: 'foreign',
|
|
51
|
+
capabilities: ['publish', 'registry-preflight'],
|
|
52
|
+
description:
|
|
53
|
+
'Same-package npm adapter binding for trusted publication, registry preflight, and lockstep recovery.',
|
|
54
|
+
id: 'release.binding.npm',
|
|
55
|
+
kind: 'adapter',
|
|
56
|
+
placement: 'same-package',
|
|
57
|
+
runtime: 'npm',
|
|
58
|
+
} satisfies ReleaseBindingDescriptor;
|
package/src/release/index.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export {
|
|
2
|
+
nativeBunPackBinding,
|
|
2
3
|
nativeBunReleaseBinding,
|
|
4
|
+
npmReleaseAdapterBinding,
|
|
3
5
|
releaseBindingCapabilityValues,
|
|
4
6
|
releaseBindingKindValues,
|
|
5
7
|
releaseBindingPlacementValues,
|
|
@@ -55,8 +57,12 @@ export {
|
|
|
55
57
|
type ReleaseRuleInput,
|
|
56
58
|
} from './config.js';
|
|
57
59
|
export {
|
|
60
|
+
createNpmPublishCommand,
|
|
58
61
|
findPackedFirstPartyDependencyMismatches,
|
|
62
|
+
publicationActionForRegistryState,
|
|
59
63
|
runNativeBunPublishCli,
|
|
64
|
+
trustedPublishingPreflightErrors,
|
|
65
|
+
unsupportedPublishLifecycleScripts,
|
|
60
66
|
type NativeBunPublishOptions,
|
|
61
67
|
type NativeBunPublishPackageJson,
|
|
62
68
|
type NativeBunPublishWorkspace,
|
|
@@ -79,6 +85,7 @@ export {
|
|
|
79
85
|
checkRegistryPosture,
|
|
80
86
|
classifyPackageRegistryState,
|
|
81
87
|
discoverRegistryWorkspaces,
|
|
88
|
+
factsFromRegistryResult,
|
|
82
89
|
formatDistTagSummary,
|
|
83
90
|
npmRegistryVersionView,
|
|
84
91
|
npmRegistryView,
|
|
@@ -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);
|
|
@@ -43,6 +43,7 @@ export type RegistryResult =
|
|
|
43
43
|
readonly status: 'published';
|
|
44
44
|
readonly version: string;
|
|
45
45
|
readonly versionPublished: boolean | undefined;
|
|
46
|
+
readonly versionProof?: RegistryVersionProof | undefined;
|
|
46
47
|
readonly workspaceVersion: string;
|
|
47
48
|
}
|
|
48
49
|
| {
|
|
@@ -82,18 +83,20 @@ export interface PackageRegistryFacts {
|
|
|
82
83
|
readonly error?: string | undefined;
|
|
83
84
|
}
|
|
84
85
|
|
|
86
|
+
/** Consumer-facing evidence for an exact package version. */
|
|
87
|
+
export type RegistryVersionProof =
|
|
88
|
+
| { readonly kind: 'consumer-pack'; readonly published: true }
|
|
89
|
+
| { readonly kind: 'exact-metadata'; readonly published: true }
|
|
90
|
+
| { readonly kind: 'unavailable'; readonly published: false };
|
|
91
|
+
|
|
85
92
|
/**
|
|
86
93
|
* Classify a package's registry state from two orthogonal facts — whether the
|
|
87
94
|
* target version is published, and where the dist-tag points relative to it —
|
|
88
95
|
* plus reachability. Members are mutually exclusive by construction.
|
|
89
96
|
*
|
|
90
|
-
* `
|
|
91
|
-
*
|
|
92
|
-
* `undefined`
|
|
93
|
-
* release-policy behavior. When the tag already points at the target, an
|
|
94
|
-
* unprobed (`undefined`) state counts as published and yields `complete`, which
|
|
95
|
-
* matches the policy `versionPublished ?? true` default. `undefined` means the
|
|
96
|
-
* exact-version probe was not run, including policy inputs and compatibility
|
|
97
|
+
* `complete` requires affirmative consumer proof. A matching dist-tag cannot
|
|
98
|
+
* substitute for exact-version metadata or an equivalent package fetch.
|
|
99
|
+
* `undefined` means the consumer probe was not run, including compatibility
|
|
97
100
|
* callers that supply an injected registry view without a version probe.
|
|
98
101
|
*/
|
|
99
102
|
export const classifyPackageRegistryState = (
|
|
@@ -120,9 +123,9 @@ export const classifyPackageRegistryState = (
|
|
|
120
123
|
return { currentTagVersion: expectedTagVersion, kind: 'tag-points-ahead' };
|
|
121
124
|
}
|
|
122
125
|
if (tagAtTarget) {
|
|
123
|
-
return versionPublished ===
|
|
124
|
-
? { kind: '
|
|
125
|
-
: { kind: '
|
|
126
|
+
return versionPublished === true
|
|
127
|
+
? { kind: 'complete' }
|
|
128
|
+
: { kind: 'needs-publish' };
|
|
126
129
|
}
|
|
127
130
|
if (versionPublished === true) {
|
|
128
131
|
return { currentTagVersion: expectedTagVersion, kind: 'needs-tag-repair' };
|
|
@@ -131,13 +134,16 @@ export const classifyPackageRegistryState = (
|
|
|
131
134
|
};
|
|
132
135
|
|
|
133
136
|
/** Map a registry probe result into the classifier's fact shape. */
|
|
134
|
-
const
|
|
137
|
+
export const factsFromRegistryResult = (
|
|
138
|
+
result: RegistryResult
|
|
139
|
+
): PackageRegistryFacts => {
|
|
135
140
|
if (result.status === 'published') {
|
|
136
141
|
return {
|
|
137
142
|
expectedTagVersion: result.expectedTagVersion,
|
|
138
143
|
status: 'published',
|
|
139
144
|
targetVersion: result.workspaceVersion,
|
|
140
|
-
versionPublished:
|
|
145
|
+
versionPublished:
|
|
146
|
+
result.versionProof?.published ?? result.versionPublished,
|
|
141
147
|
};
|
|
142
148
|
}
|
|
143
149
|
if (result.status === 'inaccessible') {
|
|
@@ -165,7 +171,8 @@ Options:
|
|
|
165
171
|
--tag <tag> Expected npm dist-tag. Defaults to .changeset/pre.json
|
|
166
172
|
tag while in prerelease mode, otherwise "latest".
|
|
167
173
|
--require-published Fail when any workspace package is missing from npm.
|
|
168
|
-
Use after publication to
|
|
174
|
+
Use after publication to require exact metadata or
|
|
175
|
+
equivalent consumer package-fetch proof.
|
|
169
176
|
-h, --help Show this help and exit.
|
|
170
177
|
|
|
171
178
|
Exit codes: 0 success, 1 registry posture failure, 2 arg-parse error.`;
|
|
@@ -459,12 +466,23 @@ export type RegistryVersionView = (
|
|
|
459
466
|
version: string
|
|
460
467
|
) => Promise<boolean | undefined>;
|
|
461
468
|
|
|
469
|
+
/** Probe with evidence that distinguishes metadata from package fetch proof. */
|
|
470
|
+
export type RegistryVersionProofView = (
|
|
471
|
+
name: string,
|
|
472
|
+
version: string
|
|
473
|
+
) => Promise<RegistryVersionProof>;
|
|
474
|
+
|
|
475
|
+
export type RegistryVersionProbeView = (
|
|
476
|
+
name: string,
|
|
477
|
+
version: string
|
|
478
|
+
) => Promise<boolean | RegistryVersionProof | undefined>;
|
|
479
|
+
|
|
462
480
|
const UNKNOWN_REGISTRY_VERSION_STATE: { readonly published?: boolean } = {};
|
|
463
481
|
const unknownRegistryVersionView: RegistryVersionView = async () =>
|
|
464
482
|
UNKNOWN_REGISTRY_VERSION_STATE.published;
|
|
465
483
|
|
|
466
|
-
export const
|
|
467
|
-
(runNpm: NpmCommandRunner = runNpmCommand):
|
|
484
|
+
export const createNpmRegistryVersionProofView =
|
|
485
|
+
(runNpm: NpmCommandRunner = runNpmCommand): RegistryVersionProofView =>
|
|
468
486
|
async (name, version) => {
|
|
469
487
|
const { exitCode, stderr, stdout } = await runNpm([
|
|
470
488
|
'view',
|
|
@@ -474,12 +492,14 @@ export const createNpmRegistryVersionView =
|
|
|
474
492
|
]);
|
|
475
493
|
|
|
476
494
|
if (exitCode === 0) {
|
|
477
|
-
return JSON.parse(stdout.trim()) === version
|
|
495
|
+
return JSON.parse(stdout.trim()) === version
|
|
496
|
+
? { kind: 'exact-metadata', published: true }
|
|
497
|
+
: { kind: 'unavailable', published: false };
|
|
478
498
|
}
|
|
479
|
-
if (
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
499
|
+
if (
|
|
500
|
+
!isNpmExactVersionMissingOutput(stdout, stderr) &&
|
|
501
|
+
!isNpmNotFoundOutput(stdout, stderr)
|
|
502
|
+
) {
|
|
483
503
|
throw new Error(
|
|
484
504
|
stderr.trim() || `npm view failed for ${name}@${version}`
|
|
485
505
|
);
|
|
@@ -496,21 +516,35 @@ export const createNpmRegistryVersionView =
|
|
|
496
516
|
packResult.stdout,
|
|
497
517
|
name,
|
|
498
518
|
version
|
|
499
|
-
)
|
|
519
|
+
)
|
|
520
|
+
? { kind: 'consumer-pack', published: true }
|
|
521
|
+
: { kind: 'unavailable', published: false };
|
|
500
522
|
}
|
|
501
523
|
if (isNpmExactVersionMissingOutput(packResult.stdout, packResult.stderr)) {
|
|
502
|
-
return false;
|
|
524
|
+
return { kind: 'unavailable', published: false };
|
|
503
525
|
}
|
|
504
526
|
if (isNpmNotFoundOutput(packResult.stdout, packResult.stderr)) {
|
|
505
|
-
return false;
|
|
527
|
+
return { kind: 'unavailable', published: false };
|
|
506
528
|
}
|
|
507
529
|
throw new Error(
|
|
508
530
|
packResult.stderr.trim() || `npm pack failed for ${name}@${version}`
|
|
509
531
|
);
|
|
510
532
|
};
|
|
511
533
|
|
|
534
|
+
export const createNpmRegistryVersionView = (
|
|
535
|
+
runNpm: NpmCommandRunner = runNpmCommand
|
|
536
|
+
): RegistryVersionView => {
|
|
537
|
+
const proofView = createNpmRegistryVersionProofView(runNpm);
|
|
538
|
+
return async (name, version) => {
|
|
539
|
+
const proof = await proofView(name, version);
|
|
540
|
+
return proof.published;
|
|
541
|
+
};
|
|
542
|
+
};
|
|
543
|
+
|
|
512
544
|
export const npmRegistryVersionView: RegistryVersionView =
|
|
513
545
|
createNpmRegistryVersionView();
|
|
546
|
+
export const npmRegistryVersionProofView: RegistryVersionProofView =
|
|
547
|
+
createNpmRegistryVersionProofView();
|
|
514
548
|
|
|
515
549
|
/** Run async tasks with a bounded number in flight, preserving input order. */
|
|
516
550
|
const mapBounded = async <T, R>(
|
|
@@ -536,7 +570,7 @@ const mapBounded = async <T, R>(
|
|
|
536
570
|
const checkWorkspaceRegistryPosture = async (
|
|
537
571
|
workspace: RegistryWorkspace,
|
|
538
572
|
view: RegistryView,
|
|
539
|
-
versionView:
|
|
573
|
+
versionView: RegistryVersionProbeView,
|
|
540
574
|
expectedTag: string
|
|
541
575
|
): Promise<RegistryResult> => {
|
|
542
576
|
try {
|
|
@@ -549,13 +583,19 @@ const checkWorkspaceRegistryPosture = async (
|
|
|
549
583
|
};
|
|
550
584
|
}
|
|
551
585
|
const distTags = registry['dist-tags'] ?? {};
|
|
586
|
+
const versionProbe = await versionView(workspace.name, workspace.version);
|
|
587
|
+
const versionProof =
|
|
588
|
+
typeof versionProbe === 'object' ? versionProbe : undefined;
|
|
589
|
+
const versionPublished =
|
|
590
|
+
typeof versionProbe === 'object' ? versionProbe.published : versionProbe;
|
|
552
591
|
return {
|
|
553
592
|
distTags,
|
|
554
593
|
expectedTagVersion: distTags[expectedTag],
|
|
555
594
|
name: workspace.name,
|
|
556
595
|
status: 'published',
|
|
557
596
|
version: registry.version ?? '(unknown)',
|
|
558
|
-
|
|
597
|
+
...(versionProof === undefined ? {} : { versionProof }),
|
|
598
|
+
versionPublished,
|
|
559
599
|
workspaceVersion: workspace.version,
|
|
560
600
|
};
|
|
561
601
|
} catch (error) {
|
|
@@ -569,14 +609,14 @@ const checkWorkspaceRegistryPosture = async (
|
|
|
569
609
|
};
|
|
570
610
|
|
|
571
611
|
type CheckRegistryPostureArgs =
|
|
572
|
-
| readonly [versionView:
|
|
612
|
+
| readonly [versionView: RegistryVersionProbeView, expectedTag: string]
|
|
573
613
|
| readonly [expectedTag: string];
|
|
574
614
|
|
|
575
615
|
const normalizeCheckRegistryPostureArgs = (
|
|
576
616
|
args: CheckRegistryPostureArgs
|
|
577
617
|
): {
|
|
578
618
|
readonly expectedTag: string;
|
|
579
|
-
readonly versionView:
|
|
619
|
+
readonly versionView: RegistryVersionProbeView;
|
|
580
620
|
} => {
|
|
581
621
|
if (args.length === 1) {
|
|
582
622
|
return { expectedTag: args[0], versionView: unknownRegistryVersionView };
|
|
@@ -592,7 +632,7 @@ export function checkRegistryPosture(
|
|
|
592
632
|
export function checkRegistryPosture(
|
|
593
633
|
workspaces: readonly RegistryWorkspace[],
|
|
594
634
|
view: RegistryView,
|
|
595
|
-
versionView:
|
|
635
|
+
versionView: RegistryVersionProbeView,
|
|
596
636
|
expectedTag: string
|
|
597
637
|
): Promise<RegistryResult[]>;
|
|
598
638
|
export async function checkRegistryPosture(
|
|
@@ -624,6 +664,19 @@ const normalizeRegistryCheckPhase = (
|
|
|
624
664
|
return phaseOrRequirePublished ? 'published' : 'ready';
|
|
625
665
|
};
|
|
626
666
|
|
|
667
|
+
const targetVersionFailure = (result: RegistryResult): string => {
|
|
668
|
+
if (result.status !== 'published') {
|
|
669
|
+
return 'is not published';
|
|
670
|
+
}
|
|
671
|
+
if (result.versionProof?.kind === 'unavailable') {
|
|
672
|
+
return 'lacks exact-version metadata and consumer pack proof';
|
|
673
|
+
}
|
|
674
|
+
if (result.versionPublished === undefined) {
|
|
675
|
+
return 'publish state was not probed';
|
|
676
|
+
}
|
|
677
|
+
return 'is not published';
|
|
678
|
+
};
|
|
679
|
+
|
|
627
680
|
export const registryPostureErrors = (
|
|
628
681
|
results: readonly RegistryResult[],
|
|
629
682
|
expectedTag: string,
|
|
@@ -632,7 +685,7 @@ export const registryPostureErrors = (
|
|
|
632
685
|
const phase = normalizeRegistryCheckPhase(phaseOrRequirePublished);
|
|
633
686
|
const errors: string[] = [];
|
|
634
687
|
for (const result of results) {
|
|
635
|
-
const state = classifyPackageRegistryState(
|
|
688
|
+
const state = classifyPackageRegistryState(factsFromRegistryResult(result));
|
|
636
689
|
if (state.kind === 'registry-inaccessible') {
|
|
637
690
|
errors.push(`${result.name}: registry probe failed: ${state.error}`);
|
|
638
691
|
continue;
|
|
@@ -649,12 +702,8 @@ export const registryPostureErrors = (
|
|
|
649
702
|
if (state.kind === 'first-time-package') {
|
|
650
703
|
errors.push(`${result.name}: package is missing from the registry`);
|
|
651
704
|
} else if (state.kind === 'needs-publish') {
|
|
652
|
-
const targetState =
|
|
653
|
-
result.status === 'published' && result.versionPublished === undefined
|
|
654
|
-
? 'publish state was not probed'
|
|
655
|
-
: 'is not published';
|
|
656
705
|
errors.push(
|
|
657
|
-
`${result.name}: target version ${result.workspaceVersion} ${
|
|
706
|
+
`${result.name}: target version ${result.workspaceVersion} ${targetVersionFailure(result)}`
|
|
658
707
|
);
|
|
659
708
|
} else if (state.kind === 'needs-tag-repair') {
|
|
660
709
|
errors.push(
|
|
@@ -673,8 +722,18 @@ export const formatDistTagSummary = (
|
|
|
673
722
|
);
|
|
674
723
|
|
|
675
724
|
const formatTargetVersionStatus = (
|
|
725
|
+
proof: RegistryVersionProof | undefined,
|
|
676
726
|
versionPublished: boolean | undefined
|
|
677
727
|
): string => {
|
|
728
|
+
if (proof?.kind === 'exact-metadata') {
|
|
729
|
+
return 'exact-version metadata available';
|
|
730
|
+
}
|
|
731
|
+
if (proof?.kind === 'consumer-pack') {
|
|
732
|
+
return 'exact-version metadata unavailable, consumer pack available';
|
|
733
|
+
}
|
|
734
|
+
if (proof?.kind === 'unavailable') {
|
|
735
|
+
return 'exact-version metadata and consumer pack unavailable';
|
|
736
|
+
}
|
|
678
737
|
if (versionPublished === true) {
|
|
679
738
|
return 'target version published';
|
|
680
739
|
}
|
|
@@ -691,7 +750,10 @@ const printResults = (
|
|
|
691
750
|
console.log(`Registry preflight for dist-tag "${expectedTag}"`);
|
|
692
751
|
for (const result of results) {
|
|
693
752
|
if (result.status === 'published') {
|
|
694
|
-
const targetStatus = formatTargetVersionStatus(
|
|
753
|
+
const targetStatus = formatTargetVersionStatus(
|
|
754
|
+
result.versionProof,
|
|
755
|
+
result.versionPublished
|
|
756
|
+
);
|
|
695
757
|
console.log(
|
|
696
758
|
`✓ ${result.name}@${result.workspaceVersion}: package exists, ${targetStatus} (registry version ${result.version}, expected ${expectedTag}=${result.expectedTagVersion ?? 'missing'}, tags ${formatDistTagSummary(result.distTags)})`
|
|
697
759
|
);
|
|
@@ -709,11 +771,14 @@ const normalizeRegistryPreflightViews = (
|
|
|
709
771
|
view: RegistryView | undefined,
|
|
710
772
|
versionView: RegistryVersionView | undefined
|
|
711
773
|
): {
|
|
712
|
-
readonly versionView:
|
|
774
|
+
readonly versionView: RegistryVersionProbeView;
|
|
713
775
|
readonly view: RegistryView;
|
|
714
776
|
} => {
|
|
715
777
|
if (view === undefined) {
|
|
716
|
-
return {
|
|
778
|
+
return {
|
|
779
|
+
versionView: npmRegistryVersionProofView,
|
|
780
|
+
view: npmRegistryView,
|
|
781
|
+
};
|
|
717
782
|
}
|
|
718
783
|
return { versionView: versionView ?? unknownRegistryVersionView, view };
|
|
719
784
|
};
|
package/src/release/policy.ts
CHANGED
|
@@ -999,13 +999,13 @@ const registryPackagesFromResults = (
|
|
|
999
999
|
const readRegistryPackages = async (
|
|
1000
1000
|
distTag: string
|
|
1001
1001
|
): Promise<readonly ReleasePolicyRegistryPackage[]> => {
|
|
1002
|
-
const { checkRegistryPosture, npmRegistryView,
|
|
1002
|
+
const { checkRegistryPosture, npmRegistryView, npmRegistryVersionProofView } =
|
|
1003
1003
|
await import('./native-bun-registry.js');
|
|
1004
1004
|
const workspaces = await discoverRegistryWorkspaces(repoRoot);
|
|
1005
1005
|
const results = await checkRegistryPosture(
|
|
1006
1006
|
workspaces,
|
|
1007
1007
|
npmRegistryView,
|
|
1008
|
-
|
|
1008
|
+
npmRegistryVersionProofView,
|
|
1009
1009
|
distTag
|
|
1010
1010
|
);
|
|
1011
1011
|
return registryPackagesFromResults(results);
|
package/src/trails/regrade.ts
CHANGED
|
@@ -3520,7 +3520,7 @@ export const regradeTrail = trail('regrade', {
|
|
|
3520
3520
|
)
|
|
3521
3521
|
: await runClassModeRegrade(input, rootDirResult.value, configScope);
|
|
3522
3522
|
if (reportResult.isErr()) {
|
|
3523
|
-
return
|
|
3523
|
+
return reportResult;
|
|
3524
3524
|
}
|
|
3525
3525
|
const outputResult = validateOutput(
|
|
3526
3526
|
regradeReportOutput,
|