@ontrails/trails 0.2.0 → 0.2.2
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 +16 -0
- package/README.md +6 -4
- package/package.json +16 -16
- package/src/release/index.ts +4 -2
- package/src/release/lock-roundtrip-workspace.ts +1 -0
- package/src/release/native-bun-registry-preflight.ts +333 -0
- package/src/release/native-bun-registry.ts +14 -178
- package/src/release/policy.ts +8 -0
- package/src/scaffold-version-sync.ts +6 -0
- package/src/scaffold-versions.generated.ts +1 -0
- package/src/trails/create-scaffold.ts +1 -0
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,21 @@
|
|
|
1
1
|
# trails
|
|
2
2
|
|
|
3
|
+
## 0.2.2
|
|
4
|
+
|
|
5
|
+
### Patch Changes
|
|
6
|
+
|
|
7
|
+
- [`0ae6a5d`](https://github.com/outfitter-dev/trails/commit/0ae6a5de15fd36bff2997a163b2ce64772151514): Wait up to two minutes for npm metadata and dist-tags to converge during post-publish verification. Retain confirmed package proofs, bound in-flight npm probes by the same deadline, and fail immediately on registry access errors or newer tags without retrying publication.
|
|
8
|
+
|
|
9
|
+
## 0.2.1
|
|
10
|
+
|
|
11
|
+
### Patch Changes
|
|
12
|
+
|
|
13
|
+
- [`d785a07`](https://github.com/outfitter-dev/trails/commit/d785a07b458899a7da14999e9b7856b8a7446e66): Align current installation examples with the 0.2.1 scaffold correction and keep Homebrew version checks explicit while preserving the 0.2.0 repair guidance.
|
|
14
|
+
- [`d785a07`](https://github.com/outfitter-dev/trails/commit/d785a07b458899a7da14999e9b7856b8a7446e66): Keep automatic release eligibility when canonical version generation updates the root Bun lockfile and bundled skill framework metadata, while retaining the existing release provenance checks and rejecting other plugin files.
|
|
15
|
+
- [`fdef61f`](https://github.com/outfitter-dev/trails/commit/fdef61feeef9b2250d537640db147cb9a9e484bd): Update package installation guidance to reflect the verified npm and Homebrew 0.2.0 release, with the published CLI scaffold repair linked from its README.
|
|
16
|
+
- [`fdef61f`](https://github.com/outfitter-dev/trails/commit/fdef61feeef9b2250d537640db147cb9a9e484bd): Generate an explicit `@types/node` development dependency in fresh standalone and workspace apps so Node built-in imports resolve in cold installs.
|
|
17
|
+
- [`ef3c802`](https://github.com/outfitter-dev/trails/commit/ef3c802f7e5b6ca92b32b11d76c26517ef388b13): Exclude unrelated `.worktrees` checkouts from lock round-trip copies while preserving tracked inputs and explicitly selected lock fixtures.
|
|
18
|
+
|
|
3
19
|
## 0.2.0
|
|
4
20
|
|
|
5
21
|
### Patch Changes
|
package/README.md
CHANGED
|
@@ -2,17 +2,19 @@
|
|
|
2
2
|
|
|
3
3
|
Command-line tools for working with Trails projects.
|
|
4
4
|
|
|
5
|
-
Use the canonical [Homebrew installation guide](../../docs/getting-started.md#installation) for installation, updates, and removal.
|
|
5
|
+
Use the canonical [Homebrew installation guide](../../docs/getting-started.md#installation) for installation, updates, and removal. The CLI requires Bun at runtime; use `trails --version` to check the installed release.
|
|
6
6
|
|
|
7
7
|
Use the CLI to scaffold a Trails app, add surfaces, inspect the current topo, run warden checks, manage draft state, and keep local Trails project state tidy.
|
|
8
8
|
|
|
9
|
-
These
|
|
9
|
+
These installation examples target Trails `0.2.1` on the normal npm release line.
|
|
10
10
|
|
|
11
11
|
```bash
|
|
12
|
-
bunx @ontrails/trails@0.2.
|
|
13
|
-
bunx @ontrails/trails@0.2.
|
|
12
|
+
bunx @ontrails/trails@0.2.1 create my-app --permit '{"id":"local-dev","scopes":["project:write"]}'
|
|
13
|
+
bunx @ontrails/trails@0.2.1 create my-app --workspace --permit '{"id":"local-dev","scopes":["project:write"]}'
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
+
Apps generated by the published `0.2.0` CLI need an explicit Node types dependency before typechecking. Follow the [0.2.0 scaffold repair](../../docs/getting-started.md#repair-apps-generated-by-020).
|
|
17
|
+
|
|
16
18
|
Common workflows:
|
|
17
19
|
|
|
18
20
|
- `trails create` starts a standalone app by default; `--workspace` creates a configured workspace with that app under `apps/<name>`. Generated surfaces live under `bin/`, app source stays side-effect-free, and `--dry-run` returns the complete write plan without touching disk. Workspace creation rejects targets nested beneath an existing configured workspace while allowing a child workspace beneath an ancestor standalone Config. On a rerun, a preserved app entry also rejects when its runtime-selected topo ID can be statically proven to conflict with `workspace.apps`; dynamic, indirect, and post-construction mutation forms remain preserved and are validated by runtime commands such as `trails compile`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ontrails/trails",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.2",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "git+https://github.com/outfitter-dev/trails.git",
|
|
@@ -33,25 +33,25 @@
|
|
|
33
33
|
"dependencies": {
|
|
34
34
|
"@clack/prompts": "^1.1.0",
|
|
35
35
|
"@manypkg/get-packages": "1.1.3",
|
|
36
|
-
"@ontrails/adapter-kit": "^0.2.
|
|
37
|
-
"@ontrails/cli": "^0.2.
|
|
38
|
-
"@ontrails/commander": "^0.2.
|
|
39
|
-
"@ontrails/config": "^0.2.
|
|
40
|
-
"@ontrails/core": "^0.2.
|
|
41
|
-
"@ontrails/http": "^0.2.
|
|
42
|
-
"@ontrails/mcp": "^0.2.
|
|
43
|
-
"@ontrails/observability": "^0.2.
|
|
44
|
-
"@ontrails/permits": "^0.2.
|
|
45
|
-
"@ontrails/regrade": "^0.2.
|
|
46
|
-
"@ontrails/source": "^0.2.
|
|
47
|
-
"@ontrails/topography": "^0.2.
|
|
48
|
-
"@ontrails/warden": "^0.2.
|
|
36
|
+
"@ontrails/adapter-kit": "^0.2.2",
|
|
37
|
+
"@ontrails/cli": "^0.2.2",
|
|
38
|
+
"@ontrails/commander": "^0.2.2",
|
|
39
|
+
"@ontrails/config": "^0.2.2",
|
|
40
|
+
"@ontrails/core": "^0.2.2",
|
|
41
|
+
"@ontrails/http": "^0.2.2",
|
|
42
|
+
"@ontrails/mcp": "^0.2.2",
|
|
43
|
+
"@ontrails/observability": "^0.2.2",
|
|
44
|
+
"@ontrails/permits": "^0.2.2",
|
|
45
|
+
"@ontrails/regrade": "^0.2.2",
|
|
46
|
+
"@ontrails/source": "^0.2.2",
|
|
47
|
+
"@ontrails/topography": "^0.2.2",
|
|
48
|
+
"@ontrails/warden": "^0.2.2",
|
|
49
49
|
"commander": "^14.0.3",
|
|
50
50
|
"typescript": "5.9.3",
|
|
51
51
|
"zod": "^4.3.5"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
|
-
"@ontrails/cloudflare": "^0.2.
|
|
55
|
-
"@ontrails/testing": "^0.2.
|
|
54
|
+
"@ontrails/cloudflare": "^0.2.2",
|
|
55
|
+
"@ontrails/testing": "^0.2.2"
|
|
56
56
|
}
|
|
57
57
|
}
|
package/src/release/index.ts
CHANGED
|
@@ -116,8 +116,6 @@ export {
|
|
|
116
116
|
npmRegistryVersionView,
|
|
117
117
|
npmRegistryView,
|
|
118
118
|
registryPostureErrors,
|
|
119
|
-
runRegistryPreflight,
|
|
120
|
-
runRegistryPreflightCli,
|
|
121
119
|
type PackageRegistryFacts,
|
|
122
120
|
type PackageRegistryState,
|
|
123
121
|
type RegistryCheckPhase,
|
|
@@ -127,6 +125,10 @@ export {
|
|
|
127
125
|
type RegistryView,
|
|
128
126
|
type RegistryWorkspace,
|
|
129
127
|
} from './native-bun-registry.js';
|
|
128
|
+
export {
|
|
129
|
+
runRegistryPreflight,
|
|
130
|
+
runRegistryPreflightCli,
|
|
131
|
+
} from './native-bun-registry-preflight.js';
|
|
130
132
|
export {
|
|
131
133
|
collectReleaseNotesInput,
|
|
132
134
|
dedupeReleaseChanges,
|
|
@@ -39,6 +39,7 @@ const isDisposablePath = (segments: readonly string[]): boolean =>
|
|
|
39
39
|
part === '.tmp-tests' ||
|
|
40
40
|
part === '.trails-tmp' ||
|
|
41
41
|
part === '.turbo' ||
|
|
42
|
+
part === '.worktrees' ||
|
|
42
43
|
(part === 'worktrees' && segments[index - 1] === '.claude') ||
|
|
43
44
|
(part === 'notes' && segments[index - 1] === '.agents')
|
|
44
45
|
);
|
|
@@ -0,0 +1,333 @@
|
|
|
1
|
+
/* oxlint-disable max-statements -- release preflight CLI with explicit reporting */
|
|
2
|
+
import { join, resolve } from 'node:path';
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
checkRegistryPosture,
|
|
6
|
+
classifyPackageRegistryState,
|
|
7
|
+
createNpmRegistryVersionProofView,
|
|
8
|
+
createNpmRegistryView,
|
|
9
|
+
discoverRegistryWorkspaces,
|
|
10
|
+
factsFromRegistryResult,
|
|
11
|
+
formatDistTagSummary,
|
|
12
|
+
registryPostureErrors,
|
|
13
|
+
runNpmRegistryCommand,
|
|
14
|
+
} from './native-bun-registry.js';
|
|
15
|
+
import type {
|
|
16
|
+
RegistryPreflightOptions,
|
|
17
|
+
RegistryResult,
|
|
18
|
+
RegistryVersionProof,
|
|
19
|
+
RegistryVersionView,
|
|
20
|
+
RegistryView,
|
|
21
|
+
RegistryWorkspace,
|
|
22
|
+
} from './native-bun-registry.js';
|
|
23
|
+
|
|
24
|
+
const REPO_ROOT = resolve(process.cwd());
|
|
25
|
+
|
|
26
|
+
interface RegistryVerificationRuntime {
|
|
27
|
+
readonly now: () => number;
|
|
28
|
+
readonly sleep: (ms: number) => Promise<void>;
|
|
29
|
+
readonly timeoutMs: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
interface RegistryVerificationReport {
|
|
33
|
+
readonly attempts: number;
|
|
34
|
+
readonly elapsedMs: number;
|
|
35
|
+
readonly results: readonly RegistryResult[];
|
|
36
|
+
readonly state: 'complete' | 'failed' | 'timeout';
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
type RegistryProbe = (
|
|
40
|
+
workspaces: readonly RegistryWorkspace[],
|
|
41
|
+
signal?: AbortSignal
|
|
42
|
+
) => Promise<RegistryResult[]>;
|
|
43
|
+
|
|
44
|
+
const verificationRuntime: RegistryVerificationRuntime = {
|
|
45
|
+
now: () => performance.now(),
|
|
46
|
+
sleep: async (ms) => {
|
|
47
|
+
await Bun.sleep(ms);
|
|
48
|
+
},
|
|
49
|
+
timeoutMs: 120_000,
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
const USAGE = `Usage: bun scripts/check-registry-preflight.ts [options]
|
|
53
|
+
|
|
54
|
+
Read-only npm registry preflight for public @ontrails/* workspaces.
|
|
55
|
+
|
|
56
|
+
Options:
|
|
57
|
+
--tag <tag> Expected npm dist-tag. Defaults to .changeset/pre.json
|
|
58
|
+
tag while in prerelease mode, otherwise "latest".
|
|
59
|
+
--require-published Fail when any workspace package is missing from npm.
|
|
60
|
+
Use after publication to require exact metadata or
|
|
61
|
+
equivalent consumer package-fetch proof. Wait up to
|
|
62
|
+
two minutes for missing metadata or lagging tags.
|
|
63
|
+
-h, --help Show this help and exit.
|
|
64
|
+
|
|
65
|
+
Exit codes: 0 success, 1 registry posture failure, 2 arg-parse error.`;
|
|
66
|
+
|
|
67
|
+
const parseArgs = (argv: readonly string[]): RegistryPreflightOptions => {
|
|
68
|
+
let requirePublished = false;
|
|
69
|
+
let tag: string | undefined;
|
|
70
|
+
|
|
71
|
+
const needsValue = (flag: string, value: string | undefined): string => {
|
|
72
|
+
if (value === undefined || value.startsWith('--')) {
|
|
73
|
+
console.error(`${flag} requires a value`);
|
|
74
|
+
console.error(USAGE);
|
|
75
|
+
process.exit(2);
|
|
76
|
+
}
|
|
77
|
+
return value;
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
let i = 0;
|
|
81
|
+
while (i < argv.length) {
|
|
82
|
+
const arg = argv[i] as string;
|
|
83
|
+
if (arg === '--require-published') {
|
|
84
|
+
requirePublished = true;
|
|
85
|
+
} else if (arg === '--tag') {
|
|
86
|
+
i += 1;
|
|
87
|
+
tag = needsValue('--tag', argv[i]);
|
|
88
|
+
} else if (arg === '-h' || arg === '--help') {
|
|
89
|
+
console.log(USAGE);
|
|
90
|
+
process.exit(0);
|
|
91
|
+
} else {
|
|
92
|
+
console.error(`Unknown argument: ${arg}`);
|
|
93
|
+
console.error(USAGE);
|
|
94
|
+
process.exit(2);
|
|
95
|
+
}
|
|
96
|
+
i += 1;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
return { requirePublished, tag };
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const resolveDefaultTag = async (): Promise<string> => {
|
|
103
|
+
const prePath = join(REPO_ROOT, '.changeset', 'pre.json');
|
|
104
|
+
if (!(await Bun.file(prePath).exists())) {
|
|
105
|
+
return 'latest';
|
|
106
|
+
}
|
|
107
|
+
const pre = (await Bun.file(prePath).json()) as {
|
|
108
|
+
mode?: string;
|
|
109
|
+
tag?: string;
|
|
110
|
+
};
|
|
111
|
+
if (pre.mode !== 'pre') {
|
|
112
|
+
return 'latest';
|
|
113
|
+
}
|
|
114
|
+
if (typeof pre.tag === 'string' && pre.tag.length > 0) {
|
|
115
|
+
return pre.tag;
|
|
116
|
+
}
|
|
117
|
+
throw new Error(`${prePath} is in prerelease mode but has no tag`);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const formatTargetVersionStatus = (
|
|
121
|
+
proof: RegistryVersionProof | undefined,
|
|
122
|
+
versionPublished: boolean | undefined
|
|
123
|
+
): string => {
|
|
124
|
+
if (proof?.kind === 'exact-metadata') {
|
|
125
|
+
return 'exact-version metadata available';
|
|
126
|
+
}
|
|
127
|
+
if (proof?.kind === 'consumer-pack') {
|
|
128
|
+
return 'exact-version metadata unavailable, consumer pack available';
|
|
129
|
+
}
|
|
130
|
+
if (proof?.kind === 'unavailable') {
|
|
131
|
+
return 'exact-version metadata and consumer pack unavailable';
|
|
132
|
+
}
|
|
133
|
+
if (versionPublished === true) {
|
|
134
|
+
return 'target version published';
|
|
135
|
+
}
|
|
136
|
+
if (versionPublished === false) {
|
|
137
|
+
return 'target version not published yet';
|
|
138
|
+
}
|
|
139
|
+
return 'target version publish state unknown';
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
const printResults = (
|
|
143
|
+
results: readonly RegistryResult[],
|
|
144
|
+
expectedTag: string
|
|
145
|
+
): void => {
|
|
146
|
+
console.log(`Registry preflight for dist-tag "${expectedTag}"`);
|
|
147
|
+
for (const result of results) {
|
|
148
|
+
if (result.status === 'published') {
|
|
149
|
+
const targetStatus = formatTargetVersionStatus(
|
|
150
|
+
result.versionProof,
|
|
151
|
+
result.versionPublished
|
|
152
|
+
);
|
|
153
|
+
console.log(
|
|
154
|
+
`✓ ${result.name}@${result.workspaceVersion}: package exists, ${targetStatus} (registry version ${result.version}, expected ${expectedTag}=${result.expectedTagVersion ?? 'missing'}, tags ${formatDistTagSummary(result.distTags)})`
|
|
155
|
+
);
|
|
156
|
+
} else if (result.status === 'missing') {
|
|
157
|
+
console.log(
|
|
158
|
+
`• ${result.name}@${result.workspaceVersion}: first-time package candidate (not found on registry)`
|
|
159
|
+
);
|
|
160
|
+
} else {
|
|
161
|
+
console.log(`✗ ${result.name}: registry probe failed: ${result.error}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
};
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* Wait for npm observations to converge after publication, retaining complete
|
|
168
|
+
* package proofs. The shared registry classifier remains the verdict owner;
|
|
169
|
+
* only missing versions/packages and lagging tags earn another read-only probe.
|
|
170
|
+
* The internal runtime seam makes the deadline and backoff testable without
|
|
171
|
+
* exposing release tuning flags or sleeping through regression tests.
|
|
172
|
+
*/
|
|
173
|
+
export const verifyPublishedRegistry = async (
|
|
174
|
+
workspaces: readonly RegistryWorkspace[],
|
|
175
|
+
expectedTag: string,
|
|
176
|
+
probe: RegistryProbe,
|
|
177
|
+
runtime: RegistryVerificationRuntime = verificationRuntime
|
|
178
|
+
): Promise<RegistryVerificationReport> => {
|
|
179
|
+
const start = runtime.now();
|
|
180
|
+
const deadline = start + runtime.timeoutMs;
|
|
181
|
+
const controller = new AbortController();
|
|
182
|
+
const timer = setTimeout(() => controller.abort(), runtime.timeoutMs);
|
|
183
|
+
const results = new Map<string, RegistryResult>();
|
|
184
|
+
let pending = workspaces;
|
|
185
|
+
let attempts = 0;
|
|
186
|
+
let delayMs = 5000;
|
|
187
|
+
const report = (
|
|
188
|
+
state: RegistryVerificationReport['state']
|
|
189
|
+
): RegistryVerificationReport => ({
|
|
190
|
+
attempts,
|
|
191
|
+
elapsedMs: runtime.now() - start,
|
|
192
|
+
results: [...results.values()],
|
|
193
|
+
state,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
try {
|
|
197
|
+
while (pending.length > 0) {
|
|
198
|
+
if (controller.signal.aborted || runtime.now() >= deadline) {
|
|
199
|
+
controller.abort();
|
|
200
|
+
return report('timeout');
|
|
201
|
+
}
|
|
202
|
+
attempts += 1;
|
|
203
|
+
const observed = await probe(pending, controller.signal);
|
|
204
|
+
const timedOut = controller.signal.aborted || runtime.now() >= deadline;
|
|
205
|
+
for (const result of observed) {
|
|
206
|
+
// Aborting npm must not erase the last useful propagation evidence.
|
|
207
|
+
if (
|
|
208
|
+
timedOut &&
|
|
209
|
+
result.status === 'inaccessible' &&
|
|
210
|
+
results.has(result.name)
|
|
211
|
+
) {
|
|
212
|
+
continue;
|
|
213
|
+
}
|
|
214
|
+
results.set(result.name, result);
|
|
215
|
+
}
|
|
216
|
+
const pendingNames = new Set(
|
|
217
|
+
observed
|
|
218
|
+
.filter(
|
|
219
|
+
(result) =>
|
|
220
|
+
classifyPackageRegistryState(
|
|
221
|
+
factsFromRegistryResult(result, expectedTag)
|
|
222
|
+
).kind !== 'complete'
|
|
223
|
+
)
|
|
224
|
+
.map(({ name }) => name)
|
|
225
|
+
);
|
|
226
|
+
pending = pending.filter(({ name }) => pendingNames.has(name));
|
|
227
|
+
if (pending.length === 0) {
|
|
228
|
+
return report('complete');
|
|
229
|
+
}
|
|
230
|
+
if (timedOut) {
|
|
231
|
+
controller.abort();
|
|
232
|
+
return report('timeout');
|
|
233
|
+
}
|
|
234
|
+
if (
|
|
235
|
+
registryPostureErrors(observed, expectedTag, 'ready').length > 0 ||
|
|
236
|
+
observed.some(
|
|
237
|
+
(result) =>
|
|
238
|
+
result.status === 'published' &&
|
|
239
|
+
factsFromRegistryResult(result, expectedTag).versionPublished ===
|
|
240
|
+
undefined
|
|
241
|
+
)
|
|
242
|
+
) {
|
|
243
|
+
return report('failed');
|
|
244
|
+
}
|
|
245
|
+
const waitMs = Math.min(delayMs, deadline - runtime.now());
|
|
246
|
+
console.log(
|
|
247
|
+
`Waiting for registry propagation: ${pending.length}/${workspaces.length} packages pending; retrying in ${waitMs / 1000}s (${pending.map(({ name }) => name).join(', ')}).`
|
|
248
|
+
);
|
|
249
|
+
await runtime.sleep(waitMs);
|
|
250
|
+
delayMs = Math.min(delayMs * 2, 30_000);
|
|
251
|
+
}
|
|
252
|
+
return report('complete');
|
|
253
|
+
} finally {
|
|
254
|
+
clearTimeout(timer);
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
|
|
258
|
+
const createRegistryProbe =
|
|
259
|
+
(
|
|
260
|
+
expectedTag: string,
|
|
261
|
+
view: RegistryView | undefined,
|
|
262
|
+
versionView: RegistryVersionView | undefined
|
|
263
|
+
): RegistryProbe =>
|
|
264
|
+
async (workspaces, signal) => {
|
|
265
|
+
if (view !== undefined) {
|
|
266
|
+
return versionView === undefined
|
|
267
|
+
? checkRegistryPosture(workspaces, view, expectedTag)
|
|
268
|
+
: checkRegistryPosture(workspaces, view, versionView, expectedTag);
|
|
269
|
+
}
|
|
270
|
+
const runNpm = (args: readonly string[]) =>
|
|
271
|
+
runNpmRegistryCommand(args, signal);
|
|
272
|
+
return checkRegistryPosture(
|
|
273
|
+
workspaces,
|
|
274
|
+
createNpmRegistryView(runNpm),
|
|
275
|
+
createNpmRegistryVersionProofView(runNpm),
|
|
276
|
+
expectedTag
|
|
277
|
+
);
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
/** Run the read-only readiness check or bounded post-publish verification. */
|
|
281
|
+
export const runRegistryPreflight = async (
|
|
282
|
+
options: RegistryPreflightOptions,
|
|
283
|
+
view?: RegistryView,
|
|
284
|
+
versionView?: RegistryVersionView
|
|
285
|
+
): Promise<number> => {
|
|
286
|
+
const expectedTag = options.tag ?? (await resolveDefaultTag());
|
|
287
|
+
const workspaces = await discoverRegistryWorkspaces();
|
|
288
|
+
const probe = createRegistryProbe(expectedTag, view, versionView);
|
|
289
|
+
const verification = options.requirePublished
|
|
290
|
+
? await verifyPublishedRegistry(workspaces, expectedTag, probe)
|
|
291
|
+
: undefined;
|
|
292
|
+
const results = verification?.results ?? (await probe(workspaces));
|
|
293
|
+
printResults(results, expectedTag);
|
|
294
|
+
const errors = registryPostureErrors(
|
|
295
|
+
results,
|
|
296
|
+
expectedTag,
|
|
297
|
+
options.requirePublished ? 'published' : 'ready'
|
|
298
|
+
);
|
|
299
|
+
if (verification?.state === 'timeout') {
|
|
300
|
+
console.error(
|
|
301
|
+
`\nPost-publish registry verification timed out after ${Math.round(verification.elapsedMs / 1000)}s (${verification.attempts} probes). No publication was retried. Inspect the remaining registry observations before recovery.`
|
|
302
|
+
);
|
|
303
|
+
} else if (errors.length > 0) {
|
|
304
|
+
console.error(
|
|
305
|
+
options.requirePublished
|
|
306
|
+
? '\nPost-publish registry verification failed; no publication was retried:'
|
|
307
|
+
: '\nRegistry preflight failed:'
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
if (errors.length > 0 || verification?.state === 'timeout') {
|
|
311
|
+
for (const error of errors) {
|
|
312
|
+
console.error(`- ${error}`);
|
|
313
|
+
}
|
|
314
|
+
return 1;
|
|
315
|
+
}
|
|
316
|
+
console.log('\nRegistry preflight passed.');
|
|
317
|
+
return 0;
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
export const runRegistryPreflightCli = async (
|
|
321
|
+
args: readonly string[] = process.argv.slice(2)
|
|
322
|
+
): Promise<number> => {
|
|
323
|
+
try {
|
|
324
|
+
return await runRegistryPreflight(parseArgs(args));
|
|
325
|
+
} catch (error) {
|
|
326
|
+
console.error(error instanceof Error ? error.message : String(error));
|
|
327
|
+
return 1;
|
|
328
|
+
}
|
|
329
|
+
};
|
|
330
|
+
|
|
331
|
+
if (import.meta.main) {
|
|
332
|
+
process.exit(await runRegistryPreflightCli(process.argv.slice(2)));
|
|
333
|
+
}
|
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
/* oxlint-disable max-statements -- release preflight CLI with explicit reporting */
|
|
2
1
|
import { readdir } from 'node:fs/promises';
|
|
3
2
|
import { join, relative, resolve } from 'node:path';
|
|
4
3
|
|
|
@@ -179,55 +178,6 @@ export const factsFromRegistryResult = (
|
|
|
179
178
|
};
|
|
180
179
|
};
|
|
181
180
|
|
|
182
|
-
const USAGE = `Usage: bun scripts/check-registry-preflight.ts [options]
|
|
183
|
-
|
|
184
|
-
Read-only npm registry preflight for public @ontrails/* workspaces.
|
|
185
|
-
|
|
186
|
-
Options:
|
|
187
|
-
--tag <tag> Expected npm dist-tag. Defaults to .changeset/pre.json
|
|
188
|
-
tag while in prerelease mode, otherwise "latest".
|
|
189
|
-
--require-published Fail when any workspace package is missing from npm.
|
|
190
|
-
Use after publication to require exact metadata or
|
|
191
|
-
equivalent consumer package-fetch proof.
|
|
192
|
-
-h, --help Show this help and exit.
|
|
193
|
-
|
|
194
|
-
Exit codes: 0 success, 1 registry posture failure, 2 arg-parse error.`;
|
|
195
|
-
|
|
196
|
-
const parseArgs = (argv: readonly string[]): RegistryPreflightOptions => {
|
|
197
|
-
let requirePublished = false;
|
|
198
|
-
let tag: string | undefined;
|
|
199
|
-
|
|
200
|
-
const needsValue = (flag: string, value: string | undefined): string => {
|
|
201
|
-
if (value === undefined || value.startsWith('--')) {
|
|
202
|
-
console.error(`${flag} requires a value`);
|
|
203
|
-
console.error(USAGE);
|
|
204
|
-
process.exit(2);
|
|
205
|
-
}
|
|
206
|
-
return value;
|
|
207
|
-
};
|
|
208
|
-
|
|
209
|
-
let i = 0;
|
|
210
|
-
while (i < argv.length) {
|
|
211
|
-
const arg = argv[i] as string;
|
|
212
|
-
if (arg === '--require-published') {
|
|
213
|
-
requirePublished = true;
|
|
214
|
-
} else if (arg === '--tag') {
|
|
215
|
-
i += 1;
|
|
216
|
-
tag = needsValue('--tag', argv[i]);
|
|
217
|
-
} else if (arg === '-h' || arg === '--help') {
|
|
218
|
-
console.log(USAGE);
|
|
219
|
-
process.exit(0);
|
|
220
|
-
} else {
|
|
221
|
-
console.error(`Unknown argument: ${arg}`);
|
|
222
|
-
console.error(USAGE);
|
|
223
|
-
process.exit(2);
|
|
224
|
-
}
|
|
225
|
-
i += 1;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
return { requirePublished, tag };
|
|
229
|
-
};
|
|
230
|
-
|
|
231
181
|
const readJson = async <T>(path: string): Promise<T> => {
|
|
232
182
|
const file = Bun.file(path);
|
|
233
183
|
if (!(await file.exists())) {
|
|
@@ -244,21 +194,6 @@ const errorCode = (error: unknown): string | undefined => {
|
|
|
244
194
|
return typeof code === 'string' ? code : undefined;
|
|
245
195
|
};
|
|
246
196
|
|
|
247
|
-
const resolveDefaultTag = async (): Promise<string> => {
|
|
248
|
-
const prePath = join(REPO_ROOT, '.changeset', 'pre.json');
|
|
249
|
-
if (!(await Bun.file(prePath).exists())) {
|
|
250
|
-
return 'latest';
|
|
251
|
-
}
|
|
252
|
-
const pre = await readJson<{ mode?: string; tag?: string }>(prePath);
|
|
253
|
-
if (pre.mode !== 'pre') {
|
|
254
|
-
return 'latest';
|
|
255
|
-
}
|
|
256
|
-
if (typeof pre.tag === 'string' && pre.tag.length > 0) {
|
|
257
|
-
return pre.tag;
|
|
258
|
-
}
|
|
259
|
-
throw new Error(`${prePath} is in prerelease mode but has no tag`);
|
|
260
|
-
};
|
|
261
|
-
|
|
262
197
|
const discoverWorkspaceDirs = async (
|
|
263
198
|
repoRoot: string,
|
|
264
199
|
patterns: readonly string[]
|
|
@@ -348,14 +283,21 @@ const readSpawnResult = async (
|
|
|
348
283
|
return { exitCode, stderr, stdout };
|
|
349
284
|
};
|
|
350
285
|
|
|
351
|
-
|
|
352
|
-
|
|
286
|
+
/** Run a read-only npm registry command, aborting its subprocess at the verification deadline. */
|
|
287
|
+
export const runNpmRegistryCommand = async (
|
|
288
|
+
args: readonly string[],
|
|
289
|
+
signal?: AbortSignal
|
|
290
|
+
): Promise<NpmCommandResult> => {
|
|
291
|
+
signal?.throwIfAborted();
|
|
292
|
+
return readSpawnResult(
|
|
353
293
|
Bun.spawn(['npm', ...args], {
|
|
354
294
|
stderr: 'pipe',
|
|
355
295
|
stdin: 'ignore',
|
|
356
296
|
stdout: 'pipe',
|
|
297
|
+
...(signal === undefined ? {} : { killSignal: 'SIGKILL', signal }),
|
|
357
298
|
})
|
|
358
299
|
);
|
|
300
|
+
};
|
|
359
301
|
|
|
360
302
|
const isNpmNotFoundOutput = (stdout: string, stderr: string): boolean => {
|
|
361
303
|
const combined = `${stdout}\n${stderr}`;
|
|
@@ -453,7 +395,7 @@ const npmDistTagRegistryView = async (
|
|
|
453
395
|
};
|
|
454
396
|
|
|
455
397
|
export const createNpmRegistryView =
|
|
456
|
-
(runNpm: NpmCommandRunner =
|
|
398
|
+
(runNpm: NpmCommandRunner = runNpmRegistryCommand): RegistryView =>
|
|
457
399
|
async (name) => {
|
|
458
400
|
const { exitCode, stderr, stdout } = await runNpm([
|
|
459
401
|
'view',
|
|
@@ -498,7 +440,9 @@ const unknownRegistryVersionView: RegistryVersionView = async () =>
|
|
|
498
440
|
UNKNOWN_REGISTRY_VERSION_STATE.published;
|
|
499
441
|
|
|
500
442
|
export const createNpmRegistryVersionProofView =
|
|
501
|
-
(
|
|
443
|
+
(
|
|
444
|
+
runNpm: NpmCommandRunner = runNpmRegistryCommand
|
|
445
|
+
): RegistryVersionProofView =>
|
|
502
446
|
async (name, version) => {
|
|
503
447
|
const { exitCode, stderr, stdout } = await runNpm([
|
|
504
448
|
'view',
|
|
@@ -548,7 +492,7 @@ export const createNpmRegistryVersionProofView =
|
|
|
548
492
|
};
|
|
549
493
|
|
|
550
494
|
export const createNpmRegistryVersionView = (
|
|
551
|
-
runNpm: NpmCommandRunner =
|
|
495
|
+
runNpm: NpmCommandRunner = runNpmRegistryCommand
|
|
552
496
|
): RegistryVersionView => {
|
|
553
497
|
const proofView = createNpmRegistryVersionProofView(runNpm);
|
|
554
498
|
return async (name, version) => {
|
|
@@ -738,111 +682,3 @@ export const formatDistTagSummary = (
|
|
|
738
682
|
SUMMARY_DIST_TAGS.map((tag) => `${tag}=${distTags[tag] ?? 'missing'}`).join(
|
|
739
683
|
', '
|
|
740
684
|
);
|
|
741
|
-
|
|
742
|
-
const formatTargetVersionStatus = (
|
|
743
|
-
proof: RegistryVersionProof | undefined,
|
|
744
|
-
versionPublished: boolean | undefined
|
|
745
|
-
): string => {
|
|
746
|
-
if (proof?.kind === 'exact-metadata') {
|
|
747
|
-
return 'exact-version metadata available';
|
|
748
|
-
}
|
|
749
|
-
if (proof?.kind === 'consumer-pack') {
|
|
750
|
-
return 'exact-version metadata unavailable, consumer pack available';
|
|
751
|
-
}
|
|
752
|
-
if (proof?.kind === 'unavailable') {
|
|
753
|
-
return 'exact-version metadata and consumer pack unavailable';
|
|
754
|
-
}
|
|
755
|
-
if (versionPublished === true) {
|
|
756
|
-
return 'target version published';
|
|
757
|
-
}
|
|
758
|
-
if (versionPublished === false) {
|
|
759
|
-
return 'target version not published yet';
|
|
760
|
-
}
|
|
761
|
-
return 'target version publish state unknown';
|
|
762
|
-
};
|
|
763
|
-
|
|
764
|
-
const printResults = (
|
|
765
|
-
results: readonly RegistryResult[],
|
|
766
|
-
expectedTag: string
|
|
767
|
-
): void => {
|
|
768
|
-
console.log(`Registry preflight for dist-tag "${expectedTag}"`);
|
|
769
|
-
for (const result of results) {
|
|
770
|
-
if (result.status === 'published') {
|
|
771
|
-
const targetStatus = formatTargetVersionStatus(
|
|
772
|
-
result.versionProof,
|
|
773
|
-
result.versionPublished
|
|
774
|
-
);
|
|
775
|
-
console.log(
|
|
776
|
-
`✓ ${result.name}@${result.workspaceVersion}: package exists, ${targetStatus} (registry version ${result.version}, expected ${expectedTag}=${result.expectedTagVersion ?? 'missing'}, tags ${formatDistTagSummary(result.distTags)})`
|
|
777
|
-
);
|
|
778
|
-
} else if (result.status === 'missing') {
|
|
779
|
-
console.log(
|
|
780
|
-
`• ${result.name}@${result.workspaceVersion}: first-time package candidate (not found on registry)`
|
|
781
|
-
);
|
|
782
|
-
} else {
|
|
783
|
-
console.log(`✗ ${result.name}: registry probe failed: ${result.error}`);
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
};
|
|
787
|
-
|
|
788
|
-
const normalizeRegistryPreflightViews = (
|
|
789
|
-
view: RegistryView | undefined,
|
|
790
|
-
versionView: RegistryVersionView | undefined
|
|
791
|
-
): {
|
|
792
|
-
readonly versionView: RegistryVersionProbeView;
|
|
793
|
-
readonly view: RegistryView;
|
|
794
|
-
} => {
|
|
795
|
-
if (view === undefined) {
|
|
796
|
-
return {
|
|
797
|
-
versionView: npmRegistryVersionProofView,
|
|
798
|
-
view: npmRegistryView,
|
|
799
|
-
};
|
|
800
|
-
}
|
|
801
|
-
return { versionView: versionView ?? unknownRegistryVersionView, view };
|
|
802
|
-
};
|
|
803
|
-
|
|
804
|
-
export const runRegistryPreflight = async (
|
|
805
|
-
options: RegistryPreflightOptions,
|
|
806
|
-
view?: RegistryView,
|
|
807
|
-
versionView?: RegistryVersionView
|
|
808
|
-
): Promise<number> => {
|
|
809
|
-
const registryViews = normalizeRegistryPreflightViews(view, versionView);
|
|
810
|
-
const expectedTag = options.tag ?? (await resolveDefaultTag());
|
|
811
|
-
const workspaces = await discoverRegistryWorkspaces();
|
|
812
|
-
const results = await checkRegistryPosture(
|
|
813
|
-
workspaces,
|
|
814
|
-
registryViews.view,
|
|
815
|
-
registryViews.versionView,
|
|
816
|
-
expectedTag
|
|
817
|
-
);
|
|
818
|
-
printResults(results, expectedTag);
|
|
819
|
-
const errors = registryPostureErrors(
|
|
820
|
-
results,
|
|
821
|
-
expectedTag,
|
|
822
|
-
options.requirePublished ? 'published' : 'ready'
|
|
823
|
-
);
|
|
824
|
-
if (errors.length > 0) {
|
|
825
|
-
console.error('\nRegistry preflight failed:');
|
|
826
|
-
for (const error of errors) {
|
|
827
|
-
console.error(`- ${error}`);
|
|
828
|
-
}
|
|
829
|
-
return 1;
|
|
830
|
-
}
|
|
831
|
-
console.log('\nRegistry preflight passed.');
|
|
832
|
-
return 0;
|
|
833
|
-
};
|
|
834
|
-
|
|
835
|
-
export const runRegistryPreflightCli = async (
|
|
836
|
-
args: readonly string[] = process.argv.slice(2)
|
|
837
|
-
): Promise<number> => {
|
|
838
|
-
try {
|
|
839
|
-
return await runRegistryPreflight(parseArgs(args));
|
|
840
|
-
} catch (error) {
|
|
841
|
-
console.error(error instanceof Error ? error.message : String(error));
|
|
842
|
-
return 1;
|
|
843
|
-
}
|
|
844
|
-
};
|
|
845
|
-
|
|
846
|
-
if (import.meta.main) {
|
|
847
|
-
process.exit(await runRegistryPreflightCli(process.argv.slice(2)));
|
|
848
|
-
}
|
package/src/release/policy.ts
CHANGED
|
@@ -728,6 +728,14 @@ const evaluateGeneratedReleaseDiff = (
|
|
|
728
728
|
let hasPackageVersionFile = false;
|
|
729
729
|
|
|
730
730
|
for (const file of changedFiles) {
|
|
731
|
+
if (
|
|
732
|
+
(file.path === 'bun.lock' ||
|
|
733
|
+
file.path === 'plugin/skills/trails/SKILL.md') &&
|
|
734
|
+
file.status === 'M'
|
|
735
|
+
) {
|
|
736
|
+
continue;
|
|
737
|
+
}
|
|
738
|
+
|
|
731
739
|
if (file.path === '.changeset/pre.json' && file.status === 'M') {
|
|
732
740
|
continue;
|
|
733
741
|
}
|
|
@@ -95,6 +95,12 @@ const loadScaffoldVersions = async (
|
|
|
95
95
|
'devDependencies',
|
|
96
96
|
rootPackageJsonPath
|
|
97
97
|
),
|
|
98
|
+
nodeTypes: requireValue(
|
|
99
|
+
devDeps['@types/node'],
|
|
100
|
+
'@types/node',
|
|
101
|
+
'devDependencies',
|
|
102
|
+
rootPackageJsonPath
|
|
103
|
+
),
|
|
98
104
|
oxfmt: requireValue(
|
|
99
105
|
devDeps['oxfmt'],
|
|
100
106
|
'oxfmt',
|
|
@@ -92,6 +92,7 @@ const generateAppPackageJson = (name: string): string => {
|
|
|
92
92
|
Object.entries({
|
|
93
93
|
'@ontrails/trails': ontrailsPackageRange,
|
|
94
94
|
'@types/bun': scaffoldDependencyVersions.bunTypes,
|
|
95
|
+
'@types/node': scaffoldDependencyVersions.nodeTypes,
|
|
95
96
|
oxfmt: scaffoldDependencyVersions.oxfmt,
|
|
96
97
|
oxlint: scaffoldDependencyVersions.oxlint,
|
|
97
98
|
typescript: scaffoldDependencyVersions.typescript,
|