@ontrails/trails 1.0.0-beta.30 → 1.0.0-beta.39
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 +153 -0
- package/README.md +3 -3
- package/package.json +17 -16
- package/src/app.ts +27 -6
- package/src/cli.ts +3 -2
- package/src/mcp-app.ts +2 -0
- package/src/mcp-options.ts +20 -5
- package/src/regrade/config.ts +152 -0
- package/src/regrade/history.ts +320 -0
- package/src/regrade/live-api-preserve.ts +8 -0
- package/src/regrade/plan-artifact.ts +265 -0
- package/src/release/index.ts +17 -0
- package/src/release/lock-roundtrip-smoke.ts +230 -0
- package/src/release/notes-cli.ts +171 -0
- package/src/release/notes.ts +390 -0
- package/src/release/pack-coherence.ts +1 -3
- package/src/release/smoke.ts +11 -1
- package/src/release/wayfinder-dogfood-smoke.ts +1 -1
- package/src/run-schema.ts +34 -1
- package/src/trails/compile.ts +4 -5
- package/src/trails/create-adapter.ts +19 -79
- package/src/trails/deprecate.ts +1 -1
- package/src/trails/guide.ts +5 -5
- package/src/trails/load-app.ts +47 -116
- package/src/trails/regrade.ts +3017 -27
- package/src/trails/release-smoke.ts +2 -1
- package/src/trails/revise.ts +1 -1
- package/src/trails/survey.ts +37 -50
- package/src/trails/topo-activation.ts +14 -385
- package/src/trails/topo-read-support.ts +28 -24
- package/src/trails/topo-store-support.ts +21 -12
- package/src/trails/topo-support.ts +1 -0
- package/src/trails/topo.ts +2 -2
- package/src/trails/validate.ts +1 -1
- package/src/trails/version-lifecycle-support.ts +11 -11
- package/src/trails/warden-guide.ts +9 -4
- package/src/trails/warden.ts +15 -0
- package/src/trails/wayfind.ts +64 -12
|
@@ -0,0 +1,390 @@
|
|
|
1
|
+
/* oxlint-disable max-lines-per-function, max-statements -- release-note parsing keeps source extraction explicit. */
|
|
2
|
+
import { readdir } from 'node:fs/promises';
|
|
3
|
+
import { join } from 'node:path';
|
|
4
|
+
|
|
5
|
+
const DEFAULT_REPO = 'outfitter-dev/trails';
|
|
6
|
+
|
|
7
|
+
interface PackageJson {
|
|
8
|
+
readonly name?: string;
|
|
9
|
+
readonly private?: boolean;
|
|
10
|
+
readonly version?: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
interface ReleaseNotesWorkspace {
|
|
14
|
+
readonly isPrivate: boolean;
|
|
15
|
+
readonly name: string;
|
|
16
|
+
readonly path: string;
|
|
17
|
+
readonly version: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface ReleaseNotesChange {
|
|
21
|
+
readonly commit?: string | undefined;
|
|
22
|
+
readonly packages: readonly string[];
|
|
23
|
+
readonly summary: string;
|
|
24
|
+
readonly url?: string | undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface ReleaseNotesPackageVersion {
|
|
28
|
+
readonly name: string;
|
|
29
|
+
readonly version: string;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ReleaseNotesInput {
|
|
33
|
+
readonly changes: readonly ReleaseNotesChange[];
|
|
34
|
+
readonly distTag: string;
|
|
35
|
+
readonly mode: 'github-release' | 'release-pr';
|
|
36
|
+
readonly packageVersions: readonly ReleaseNotesPackageVersion[];
|
|
37
|
+
readonly repo?: string | undefined;
|
|
38
|
+
readonly version: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
interface ChangelogEntryInput {
|
|
42
|
+
readonly changelog: string;
|
|
43
|
+
readonly packageName: string;
|
|
44
|
+
readonly version: string;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ReleaseNotesParsedChange {
|
|
48
|
+
readonly commit?: string | undefined;
|
|
49
|
+
readonly packageName: string;
|
|
50
|
+
readonly summary: string;
|
|
51
|
+
readonly url?: string | undefined;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface ReleaseNotesCollectOptions {
|
|
55
|
+
readonly distTag?: string | undefined;
|
|
56
|
+
readonly mode: ReleaseNotesInput['mode'];
|
|
57
|
+
readonly repo?: string | undefined;
|
|
58
|
+
readonly repoRoot: string;
|
|
59
|
+
readonly version?: string | undefined;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
interface CurrentParsedChange {
|
|
63
|
+
readonly commit?: string | undefined;
|
|
64
|
+
readonly packageName: string;
|
|
65
|
+
readonly summaryLines: string[];
|
|
66
|
+
readonly url?: string | undefined;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const readJson = async <T>(path: string): Promise<T> =>
|
|
70
|
+
(await Bun.file(path).json()) as T;
|
|
71
|
+
|
|
72
|
+
const discoverWorkspaceDirs = async (
|
|
73
|
+
repoRoot: string,
|
|
74
|
+
patterns: readonly string[]
|
|
75
|
+
): Promise<string[]> => {
|
|
76
|
+
const dirs: string[] = [];
|
|
77
|
+
|
|
78
|
+
for (const pattern of patterns) {
|
|
79
|
+
if (pattern.endsWith('/*')) {
|
|
80
|
+
const parent = join(repoRoot, pattern.slice(0, -2));
|
|
81
|
+
const entries = await readdir(parent, { withFileTypes: true });
|
|
82
|
+
for (const entry of entries) {
|
|
83
|
+
if (!entry.isDirectory()) {
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
const dir = join(parent, entry.name);
|
|
87
|
+
if (await Bun.file(join(dir, 'package.json')).exists()) {
|
|
88
|
+
dirs.push(dir);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
continue;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const dir = join(repoRoot, pattern);
|
|
95
|
+
if (await Bun.file(join(dir, 'package.json')).exists()) {
|
|
96
|
+
dirs.push(dir);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return dirs;
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const discoverReleaseNotesWorkspaces = async (
|
|
104
|
+
repoRoot: string,
|
|
105
|
+
{ includePrivate }: { readonly includePrivate: boolean }
|
|
106
|
+
): Promise<ReleaseNotesWorkspace[]> => {
|
|
107
|
+
const root = await readJson<{ workspaces?: string[] }>(
|
|
108
|
+
join(repoRoot, 'package.json')
|
|
109
|
+
);
|
|
110
|
+
const dirs = await discoverWorkspaceDirs(repoRoot, root.workspaces ?? []);
|
|
111
|
+
const workspaces: ReleaseNotesWorkspace[] = [];
|
|
112
|
+
|
|
113
|
+
for (const dir of dirs) {
|
|
114
|
+
const pkg = await readJson<PackageJson>(join(dir, 'package.json'));
|
|
115
|
+
if (
|
|
116
|
+
typeof pkg.name !== 'string' ||
|
|
117
|
+
!pkg.name.startsWith('@ontrails/') ||
|
|
118
|
+
typeof pkg.version !== 'string' ||
|
|
119
|
+
(pkg.private === true && !includePrivate)
|
|
120
|
+
) {
|
|
121
|
+
continue;
|
|
122
|
+
}
|
|
123
|
+
workspaces.push({
|
|
124
|
+
isPrivate: pkg.private === true,
|
|
125
|
+
name: pkg.name,
|
|
126
|
+
path: dir.slice(repoRoot.length + 1),
|
|
127
|
+
version: pkg.version,
|
|
128
|
+
});
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
return workspaces.toSorted((a, b) => a.name.localeCompare(b.name));
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
const readTrailsVersion = async (repoRoot: string): Promise<string> => {
|
|
135
|
+
const pkg = await readJson<PackageJson>(
|
|
136
|
+
join(repoRoot, 'apps', 'trails', 'package.json')
|
|
137
|
+
);
|
|
138
|
+
if (!pkg.version) {
|
|
139
|
+
throw new Error('Missing version in apps/trails/package.json');
|
|
140
|
+
}
|
|
141
|
+
return pkg.version;
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const distTagForVersion = (version: string): string =>
|
|
145
|
+
version.includes('-')
|
|
146
|
+
? (version.split('-')[1]?.split('.')[0] ?? 'beta')
|
|
147
|
+
: 'latest';
|
|
148
|
+
|
|
149
|
+
export const extractChangelogEntry = (
|
|
150
|
+
changelog: string,
|
|
151
|
+
version: string
|
|
152
|
+
): string | undefined => {
|
|
153
|
+
const lines = changelog.split('\n');
|
|
154
|
+
const heading = `## ${version}`;
|
|
155
|
+
const start = lines.findIndex((line) => line.trim() === heading);
|
|
156
|
+
if (start === -1) {
|
|
157
|
+
return undefined;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
let end = lines.length;
|
|
161
|
+
for (let index = start + 1; index < lines.length; index += 1) {
|
|
162
|
+
if (/^##\s+\S/u.test(lines[index] ?? '')) {
|
|
163
|
+
end = index;
|
|
164
|
+
break;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return lines
|
|
169
|
+
.slice(start + 1, end)
|
|
170
|
+
.join('\n')
|
|
171
|
+
.trim();
|
|
172
|
+
};
|
|
173
|
+
|
|
174
|
+
const normalizeSummary = (lines: readonly string[]): string =>
|
|
175
|
+
lines.join(' ').replaceAll(/\s+/gu, ' ').trim();
|
|
176
|
+
|
|
177
|
+
const parseChangeStart = (
|
|
178
|
+
line: string
|
|
179
|
+
): Omit<ReleaseNotesParsedChange, 'packageName'> | undefined => {
|
|
180
|
+
if (
|
|
181
|
+
!line.startsWith('- ') ||
|
|
182
|
+
line.startsWith('- Updated dependencies') ||
|
|
183
|
+
line.startsWith('- @ontrails/')
|
|
184
|
+
) {
|
|
185
|
+
return undefined;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
const linked = line.match(/^- \[`?([0-9a-f]{7,40})`?\]\(([^)]+)\):\s*(.*)$/u);
|
|
189
|
+
if (linked) {
|
|
190
|
+
return {
|
|
191
|
+
commit: linked[1],
|
|
192
|
+
summary: linked[3] ?? '',
|
|
193
|
+
url: linked[2],
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const raw = line.match(/^- ([0-9a-f]{7,40}):\s*(.*)$/u);
|
|
198
|
+
if (raw) {
|
|
199
|
+
return {
|
|
200
|
+
commit: raw[1],
|
|
201
|
+
summary: raw[2] ?? '',
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return { summary: line.slice(2) };
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
const parseChangelogEntryChanges = ({
|
|
209
|
+
changelog,
|
|
210
|
+
packageName,
|
|
211
|
+
version,
|
|
212
|
+
}: ChangelogEntryInput): ReleaseNotesParsedChange[] => {
|
|
213
|
+
const entry = extractChangelogEntry(changelog, version);
|
|
214
|
+
if (!entry) {
|
|
215
|
+
return [];
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const changes: ReleaseNotesParsedChange[] = [];
|
|
219
|
+
let current: CurrentParsedChange | undefined;
|
|
220
|
+
|
|
221
|
+
const flush = () => {
|
|
222
|
+
if (!current) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
const summary = normalizeSummary(current.summaryLines);
|
|
226
|
+
if (summary) {
|
|
227
|
+
changes.push({
|
|
228
|
+
...(current.commit === undefined ? {} : { commit: current.commit }),
|
|
229
|
+
packageName: current.packageName,
|
|
230
|
+
summary,
|
|
231
|
+
...(current.url === undefined ? {} : { url: current.url }),
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
for (const line of entry.split('\n')) {
|
|
237
|
+
const start = parseChangeStart(line);
|
|
238
|
+
if (start) {
|
|
239
|
+
flush();
|
|
240
|
+
current = {
|
|
241
|
+
...(start.commit === undefined ? {} : { commit: start.commit }),
|
|
242
|
+
packageName,
|
|
243
|
+
summaryLines: [start.summary],
|
|
244
|
+
...(start.url === undefined ? {} : { url: start.url }),
|
|
245
|
+
};
|
|
246
|
+
continue;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (current && line.startsWith(' ') && line.trim()) {
|
|
250
|
+
current.summaryLines.push(line.trim());
|
|
251
|
+
continue;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
if (line.startsWith('- ')) {
|
|
255
|
+
flush();
|
|
256
|
+
current = undefined;
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
flush();
|
|
261
|
+
return changes;
|
|
262
|
+
};
|
|
263
|
+
|
|
264
|
+
export const dedupeReleaseChanges = (
|
|
265
|
+
changes: readonly ReleaseNotesParsedChange[]
|
|
266
|
+
): ReleaseNotesChange[] => {
|
|
267
|
+
const byKey = new Map<string, ReleaseNotesChange>();
|
|
268
|
+
|
|
269
|
+
for (const change of changes) {
|
|
270
|
+
const key = `${change.commit ?? ''}\n${change.summary}`;
|
|
271
|
+
const existing = byKey.get(key);
|
|
272
|
+
if (existing) {
|
|
273
|
+
byKey.set(key, {
|
|
274
|
+
...existing,
|
|
275
|
+
packages: [
|
|
276
|
+
...new Set([...existing.packages, change.packageName]),
|
|
277
|
+
].toSorted(),
|
|
278
|
+
});
|
|
279
|
+
continue;
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
byKey.set(key, {
|
|
283
|
+
...(change.commit === undefined ? {} : { commit: change.commit }),
|
|
284
|
+
packages: [change.packageName],
|
|
285
|
+
summary: change.summary,
|
|
286
|
+
...(change.url === undefined ? {} : { url: change.url }),
|
|
287
|
+
});
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
return [...byKey.values()];
|
|
291
|
+
};
|
|
292
|
+
|
|
293
|
+
const renderCommitLink = (change: ReleaseNotesChange, repo: string): string => {
|
|
294
|
+
if (!change.commit) {
|
|
295
|
+
return '';
|
|
296
|
+
}
|
|
297
|
+
const short = change.commit.slice(0, 7);
|
|
298
|
+
const url =
|
|
299
|
+
change.url ?? `https://github.com/${repo}/commit/${change.commit}`;
|
|
300
|
+
return `[\`${short}\`](${url}): `;
|
|
301
|
+
};
|
|
302
|
+
|
|
303
|
+
const renderPackages = (packages: readonly string[]): string =>
|
|
304
|
+
packages.map((name) => `\`${name}\``).join(', ');
|
|
305
|
+
|
|
306
|
+
export const renderReleaseNotes = ({
|
|
307
|
+
changes,
|
|
308
|
+
distTag,
|
|
309
|
+
mode,
|
|
310
|
+
packageVersions,
|
|
311
|
+
repo = DEFAULT_REPO,
|
|
312
|
+
version,
|
|
313
|
+
}: ReleaseNotesInput): string => {
|
|
314
|
+
const intro =
|
|
315
|
+
mode === 'release-pr'
|
|
316
|
+
? `${packageVersions.length} \`@ontrails/*\` packages will be bumped to \`${version}\`.`
|
|
317
|
+
: `Published the publishable \`@ontrails/*\` package set at \`${version}\` on the \`${distTag}\` dist-tag.`;
|
|
318
|
+
const highlights = changes.slice(0, 5);
|
|
319
|
+
const lines = [`# Release ${version}`, '', intro, '', '## Highlights', ''];
|
|
320
|
+
|
|
321
|
+
if (highlights.length === 0) {
|
|
322
|
+
lines.push('- No user-facing changes were detected in package changelogs.');
|
|
323
|
+
} else {
|
|
324
|
+
lines.push(...highlights.map((change) => `- ${change.summary}`));
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
lines.push('', '## Changes', '');
|
|
328
|
+
if (changes.length === 0) {
|
|
329
|
+
lines.push('- No user-facing changes were detected in package changelogs.');
|
|
330
|
+
} else {
|
|
331
|
+
lines.push(
|
|
332
|
+
...changes.map(
|
|
333
|
+
(change) =>
|
|
334
|
+
`- ${renderCommitLink(change, repo)}${change.summary} Packages: ${renderPackages(change.packages)}`
|
|
335
|
+
)
|
|
336
|
+
);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
lines.push(
|
|
340
|
+
'',
|
|
341
|
+
'<details>',
|
|
342
|
+
'<summary>Package Versions</summary>',
|
|
343
|
+
'',
|
|
344
|
+
...packageVersions.map((pkg) => `- \`${pkg.name}@${pkg.version}\``),
|
|
345
|
+
'',
|
|
346
|
+
'</details>',
|
|
347
|
+
''
|
|
348
|
+
);
|
|
349
|
+
|
|
350
|
+
return lines.join('\n');
|
|
351
|
+
};
|
|
352
|
+
|
|
353
|
+
export const collectReleaseNotesInput = async ({
|
|
354
|
+
distTag,
|
|
355
|
+
mode,
|
|
356
|
+
repo,
|
|
357
|
+
repoRoot,
|
|
358
|
+
version,
|
|
359
|
+
}: ReleaseNotesCollectOptions): Promise<ReleaseNotesInput> => {
|
|
360
|
+
const resolvedVersion = version ?? (await readTrailsVersion(repoRoot));
|
|
361
|
+
const resolvedDistTag = distTag ?? distTagForVersion(resolvedVersion);
|
|
362
|
+
const workspaces = await discoverReleaseNotesWorkspaces(repoRoot, {
|
|
363
|
+
includePrivate: mode === 'release-pr',
|
|
364
|
+
});
|
|
365
|
+
const packageVersions = workspaces.map((workspace) => ({
|
|
366
|
+
name: workspace.name,
|
|
367
|
+
version: workspace.version,
|
|
368
|
+
}));
|
|
369
|
+
const parsedChangesByPackage = await Promise.all(
|
|
370
|
+
workspaces.map(async (workspace: ReleaseNotesWorkspace) =>
|
|
371
|
+
parseChangelogEntryChanges({
|
|
372
|
+
changelog: await Bun.file(
|
|
373
|
+
join(repoRoot, workspace.path, 'CHANGELOG.md')
|
|
374
|
+
).text(),
|
|
375
|
+
packageName: workspace.name,
|
|
376
|
+
version: resolvedVersion,
|
|
377
|
+
})
|
|
378
|
+
)
|
|
379
|
+
);
|
|
380
|
+
const parsedChanges = parsedChangesByPackage.flat();
|
|
381
|
+
|
|
382
|
+
return {
|
|
383
|
+
changes: dedupeReleaseChanges(parsedChanges),
|
|
384
|
+
distTag: resolvedDistTag,
|
|
385
|
+
mode,
|
|
386
|
+
packageVersions,
|
|
387
|
+
...(repo === undefined ? {} : { repo }),
|
|
388
|
+
version: resolvedVersion,
|
|
389
|
+
};
|
|
390
|
+
};
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { readFile, readdir, writeFile } from 'node:fs/promises';
|
|
2
2
|
import { join, relative } from 'node:path';
|
|
3
|
+
import { escapeRegExp } from '@ontrails/core';
|
|
3
4
|
|
|
4
5
|
export interface ReleasePackCoherenceInput {
|
|
5
6
|
readonly branchName?: string | undefined;
|
|
@@ -234,9 +235,6 @@ export const findLockfileWorkspaceMetadataMismatches = ({
|
|
|
234
235
|
return mismatches;
|
|
235
236
|
};
|
|
236
237
|
|
|
237
|
-
const escapeRegExp = (text: string): string =>
|
|
238
|
-
text.replaceAll(/[.*+?^${}()|[\]\\]/gu, '\\$&');
|
|
239
|
-
|
|
240
238
|
const findWorkspaceBlockRange = (
|
|
241
239
|
text: string,
|
|
242
240
|
workspacePath: string
|
package/src/release/smoke.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { runLockRoundtripSmoke } from './lock-roundtrip-smoke.js';
|
|
2
|
+
import type { LockRoundtripSmokeResult } from './lock-roundtrip-smoke.js';
|
|
1
3
|
import { runPackedArtifactsSmoke } from './packed-artifacts-smoke.js';
|
|
2
4
|
import type { PackedArtifactsSmokeResult } from './packed-artifacts-smoke.js';
|
|
3
5
|
import { runWayfinderDogfoodSmoke } from './wayfinder-dogfood-smoke.js';
|
|
@@ -5,12 +7,14 @@ import type { WayfinderDogfoodSmokeResult } from './wayfinder-dogfood-smoke.js';
|
|
|
5
7
|
|
|
6
8
|
export const releaseSmokeCheckValues = [
|
|
7
9
|
'all',
|
|
10
|
+
'lock-roundtrip',
|
|
8
11
|
'packed-artifacts',
|
|
9
12
|
'wayfinder-dogfood',
|
|
10
13
|
] as const;
|
|
11
14
|
export type ReleaseSmokeCheck = (typeof releaseSmokeCheckValues)[number];
|
|
12
15
|
|
|
13
16
|
export type ReleaseSmokeCheckResult =
|
|
17
|
+
| LockRoundtripSmokeResult
|
|
14
18
|
| PackedArtifactsSmokeResult
|
|
15
19
|
| WayfinderDogfoodSmokeResult;
|
|
16
20
|
|
|
@@ -23,7 +27,9 @@ export interface ReleaseSmokeResult {
|
|
|
23
27
|
const checksForInput = (
|
|
24
28
|
check: ReleaseSmokeCheck
|
|
25
29
|
): readonly Exclude<ReleaseSmokeCheck, 'all'>[] =>
|
|
26
|
-
check === 'all'
|
|
30
|
+
check === 'all'
|
|
31
|
+
? ['lock-roundtrip', 'packed-artifacts', 'wayfinder-dogfood']
|
|
32
|
+
: [check];
|
|
27
33
|
|
|
28
34
|
export const runReleaseSmoke = async (
|
|
29
35
|
check: ReleaseSmokeCheck
|
|
@@ -31,6 +37,10 @@ export const runReleaseSmoke = async (
|
|
|
31
37
|
const results: ReleaseSmokeCheckResult[] = [];
|
|
32
38
|
|
|
33
39
|
for (const selectedCheck of checksForInput(check)) {
|
|
40
|
+
if (selectedCheck === 'lock-roundtrip') {
|
|
41
|
+
results.push(await runLockRoundtripSmoke());
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
34
44
|
if (selectedCheck === 'packed-artifacts') {
|
|
35
45
|
results.push(await runPackedArtifactsSmoke());
|
|
36
46
|
continue;
|
|
@@ -138,7 +138,7 @@ const writeOperatorAppWrapper = async (tempRoot: string): Promise<void> => {
|
|
|
138
138
|
const appModuleUrl = pathToFileURL(join(repoRoot, 'apps/trails/src/app.ts'));
|
|
139
139
|
await writeFile(
|
|
140
140
|
join(srcDir, 'app.ts'),
|
|
141
|
-
`export { app,
|
|
141
|
+
`export { app, trailsOverlays } from ${JSON.stringify(appModuleUrl.href)};\n`
|
|
142
142
|
);
|
|
143
143
|
};
|
|
144
144
|
|
package/src/run-schema.ts
CHANGED
|
@@ -14,6 +14,18 @@ const writeJson = (value: unknown): void => {
|
|
|
14
14
|
process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
|
|
15
15
|
};
|
|
16
16
|
|
|
17
|
+
const pathStartsWith = (
|
|
18
|
+
path: readonly string[],
|
|
19
|
+
prefix: readonly string[]
|
|
20
|
+
): boolean =>
|
|
21
|
+
path.length > prefix.length &&
|
|
22
|
+
prefix.every((segment, index) => path[index] === segment);
|
|
23
|
+
|
|
24
|
+
const childCommandsForPath = (
|
|
25
|
+
schema: ReturnType<typeof deriveCliSchema>,
|
|
26
|
+
path: readonly string[]
|
|
27
|
+
) => schema.commands.filter((entry) => pathStartsWith(entry.commandPath, path));
|
|
28
|
+
|
|
17
29
|
export const attachSchemaCommand = (
|
|
18
30
|
program: Command,
|
|
19
31
|
commands: readonly CliCommand[]
|
|
@@ -30,12 +42,33 @@ export const attachSchemaCommand = (
|
|
|
30
42
|
}
|
|
31
43
|
|
|
32
44
|
const command = findCliSchemaCommand(schema, path);
|
|
45
|
+
const children = childCommandsForPath(schema, path);
|
|
33
46
|
if (command === undefined) {
|
|
47
|
+
if (children.length > 0) {
|
|
48
|
+
writeJson({
|
|
49
|
+
namespace: {
|
|
50
|
+
commandPath: path,
|
|
51
|
+
commands: children,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
return;
|
|
55
|
+
}
|
|
56
|
+
|
|
34
57
|
process.stderr.write(`Unknown CLI command: ${path.join(' ')}\n`);
|
|
35
58
|
process.exitCode = 1;
|
|
36
59
|
return;
|
|
37
60
|
}
|
|
38
61
|
|
|
39
|
-
writeJson(
|
|
62
|
+
writeJson(
|
|
63
|
+
children.length === 0
|
|
64
|
+
? { command }
|
|
65
|
+
: {
|
|
66
|
+
command,
|
|
67
|
+
namespace: {
|
|
68
|
+
commandPath: path,
|
|
69
|
+
commands: children,
|
|
70
|
+
},
|
|
71
|
+
}
|
|
72
|
+
);
|
|
40
73
|
});
|
|
41
74
|
};
|
package/src/trails/compile.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { trail } from '@ontrails/core';
|
|
2
|
-
import type {
|
|
2
|
+
import type { Result, Topo } from '@ontrails/core';
|
|
3
|
+
import type { TopoGraphOverlayRegistration } from '@ontrails/topographer';
|
|
3
4
|
import { z } from 'zod';
|
|
4
5
|
|
|
5
6
|
import { withFreshOperatorApp } from './operator-context.js';
|
|
@@ -13,11 +14,9 @@ import {
|
|
|
13
14
|
export const compileCurrentTopo = async (
|
|
14
15
|
app: Topo,
|
|
15
16
|
options?: {
|
|
16
|
-
readonly cliAliases?:
|
|
17
|
-
| Readonly<Record<string, readonly CliCommandAliasInput[]>>
|
|
18
|
-
| undefined;
|
|
19
17
|
readonly force?: boolean | undefined;
|
|
20
18
|
readonly rootDir?: string;
|
|
19
|
+
readonly overlays?: readonly TopoGraphOverlayRegistration[] | undefined;
|
|
21
20
|
}
|
|
22
21
|
): Promise<Result<TopoExportReport, Error>> => exportCurrentTopo(app, options);
|
|
23
22
|
|
|
@@ -36,8 +35,8 @@ export const compileTrail = trail('compile', {
|
|
|
36
35
|
blaze: async (input: CompileTrailInput, ctx) =>
|
|
37
36
|
withFreshOperatorApp(input, ctx, ({ lease, rootDir }) =>
|
|
38
37
|
compileCurrentTopo(lease.app, {
|
|
39
|
-
cliAliases: lease.cliAliases,
|
|
40
38
|
force: input.force,
|
|
39
|
+
overlays: lease.overlays,
|
|
41
40
|
rootDir,
|
|
42
41
|
})
|
|
43
42
|
),
|
|
@@ -13,9 +13,16 @@ import type {
|
|
|
13
13
|
AdapterTargetConformanceManifest,
|
|
14
14
|
AdapterTargetPlacement,
|
|
15
15
|
} from '@ontrails/adapter-kit';
|
|
16
|
-
import {
|
|
17
|
-
|
|
18
|
-
|
|
16
|
+
import {
|
|
17
|
+
findWorkspacePackage,
|
|
18
|
+
listWorkspacePatterns,
|
|
19
|
+
Result,
|
|
20
|
+
trail,
|
|
21
|
+
ValidationError,
|
|
22
|
+
} from '@ontrails/core';
|
|
23
|
+
import type { WorkspaceRootManifest } from '@ontrails/core';
|
|
24
|
+
import { existsSync, readFileSync, realpathSync } from 'node:fs';
|
|
25
|
+
import { dirname, join, resolve } from 'node:path';
|
|
19
26
|
import { z } from 'zod';
|
|
20
27
|
|
|
21
28
|
import {
|
|
@@ -69,20 +76,11 @@ interface AdapterOperationPlan {
|
|
|
69
76
|
readonly targetKey: string;
|
|
70
77
|
}
|
|
71
78
|
|
|
72
|
-
interface RootManifest {
|
|
73
|
-
readonly workspaces?: unknown;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
79
|
interface WorkspacePackageManifest {
|
|
77
80
|
readonly exports?: unknown;
|
|
78
81
|
readonly name?: unknown;
|
|
79
82
|
}
|
|
80
83
|
|
|
81
|
-
interface WorkspacePackageName {
|
|
82
|
-
readonly name: string;
|
|
83
|
-
readonly workspacePath: string;
|
|
84
|
-
}
|
|
85
|
-
|
|
86
84
|
interface LocalNamedReexport {
|
|
87
85
|
readonly local: string;
|
|
88
86
|
readonly specifier: string;
|
|
@@ -123,27 +121,6 @@ const readJson = <T>(path: string): T | undefined => {
|
|
|
123
121
|
}
|
|
124
122
|
};
|
|
125
123
|
|
|
126
|
-
const workspacePatternsFromManifest = (
|
|
127
|
-
manifest: RootManifest | undefined
|
|
128
|
-
): readonly string[] => {
|
|
129
|
-
const { workspaces } = manifest ?? {};
|
|
130
|
-
if (Array.isArray(workspaces)) {
|
|
131
|
-
return workspaces.filter(
|
|
132
|
-
(pattern): pattern is string => typeof pattern === 'string'
|
|
133
|
-
);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const packages =
|
|
137
|
-
workspaces && typeof workspaces === 'object' && !Array.isArray(workspaces)
|
|
138
|
-
? (workspaces as Record<string, unknown>)['packages']
|
|
139
|
-
: undefined;
|
|
140
|
-
return Array.isArray(packages)
|
|
141
|
-
? packages.filter(
|
|
142
|
-
(pattern): pattern is string => typeof pattern === 'string'
|
|
143
|
-
)
|
|
144
|
-
: [];
|
|
145
|
-
};
|
|
146
|
-
|
|
147
124
|
const normalizeWorkspacePattern = (pattern: string): string =>
|
|
148
125
|
pattern.replace(/^\.\//u, '').replace(/\/+$/u, '');
|
|
149
126
|
|
|
@@ -169,54 +146,14 @@ const rootWorkspaceIncludesPath = (
|
|
|
169
146
|
rootDir: string,
|
|
170
147
|
workspacePath: string
|
|
171
148
|
): boolean => {
|
|
172
|
-
const rootManifest = readJson<
|
|
173
|
-
|
|
149
|
+
const rootManifest = readJson<WorkspaceRootManifest>(
|
|
150
|
+
join(rootDir, 'package.json')
|
|
151
|
+
);
|
|
152
|
+
return listWorkspacePatterns(rootManifest).some((pattern) =>
|
|
174
153
|
workspacePatternCoversPath(pattern, workspacePath)
|
|
175
154
|
);
|
|
176
155
|
};
|
|
177
156
|
|
|
178
|
-
const workspaceDirsForPattern = (
|
|
179
|
-
rootDir: string,
|
|
180
|
-
pattern: string
|
|
181
|
-
): readonly string[] => {
|
|
182
|
-
if (!pattern.endsWith('/*')) {
|
|
183
|
-
const workspaceDir = join(rootDir, pattern);
|
|
184
|
-
return existsSync(workspaceDir) ? [workspaceDir] : [];
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
const groupDir = join(rootDir, pattern.slice(0, -2));
|
|
188
|
-
if (!existsSync(groupDir)) {
|
|
189
|
-
return [];
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
return readdirSync(groupDir, { withFileTypes: true })
|
|
193
|
-
.filter((entry) => entry.isDirectory())
|
|
194
|
-
.map((entry) => join(groupDir, entry.name))
|
|
195
|
-
.toSorted();
|
|
196
|
-
};
|
|
197
|
-
|
|
198
|
-
const findWorkspacePackageName = (
|
|
199
|
-
rootDir: string,
|
|
200
|
-
packageName: string
|
|
201
|
-
): WorkspacePackageName | undefined => {
|
|
202
|
-
const rootManifest = readJson<RootManifest>(join(rootDir, 'package.json'));
|
|
203
|
-
for (const pattern of workspacePatternsFromManifest(rootManifest)) {
|
|
204
|
-
for (const workspaceDir of workspaceDirsForPattern(rootDir, pattern)) {
|
|
205
|
-
const manifest = readJson<WorkspacePackageManifest>(
|
|
206
|
-
join(workspaceDir, 'package.json')
|
|
207
|
-
);
|
|
208
|
-
if (manifest?.name === packageName) {
|
|
209
|
-
return {
|
|
210
|
-
name: packageName,
|
|
211
|
-
workspacePath: relative(rootDir, workspaceDir),
|
|
212
|
-
};
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
}
|
|
216
|
-
|
|
217
|
-
return undefined;
|
|
218
|
-
};
|
|
219
|
-
|
|
220
157
|
const resolvePhysicalRootDir = (
|
|
221
158
|
rootDir: string
|
|
222
159
|
): Result<string, ValidationError> => {
|
|
@@ -882,10 +819,13 @@ const buildExtractedPlan = (
|
|
|
882
819
|
if (!packageNamePattern.test(packageName)) {
|
|
883
820
|
return fail(packageNameMessage);
|
|
884
821
|
}
|
|
885
|
-
const existingPackage =
|
|
822
|
+
const existingPackage = findWorkspacePackage<WorkspacePackageManifest>(
|
|
823
|
+
rootDir,
|
|
824
|
+
packageName
|
|
825
|
+
);
|
|
886
826
|
if (existingPackage) {
|
|
887
827
|
return fail(
|
|
888
|
-
`Workspace package name "${
|
|
828
|
+
`Workspace package name "${packageName}" already exists at ${existingPackage.workspacePath}.`
|
|
889
829
|
);
|
|
890
830
|
}
|
|
891
831
|
|