@plexus-ms/lib 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bookkeeping.d.ts +26 -0
- package/dist/bookkeeping.d.ts.map +1 -0
- package/dist/bookkeeping.js +51 -0
- package/dist/bookkeeping.js.map +1 -0
- package/dist/consumers.d.ts +31 -0
- package/dist/consumers.d.ts.map +1 -0
- package/dist/consumers.js +70 -0
- package/dist/consumers.js.map +1 -0
- package/dist/context.d.ts +7 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +19 -3
- package/dist/context.js.map +1 -1
- package/dist/frontmatter.d.ts +9 -0
- package/dist/frontmatter.d.ts.map +1 -0
- package/dist/frontmatter.js +42 -0
- package/dist/frontmatter.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +7 -0
- package/dist/index.js.map +1 -1
- package/dist/io.d.ts +5 -1
- package/dist/io.d.ts.map +1 -1
- package/dist/io.js +5 -1
- package/dist/io.js.map +1 -1
- package/dist/plan.d.ts +2 -0
- package/dist/plan.d.ts.map +1 -1
- package/dist/plan.js.map +1 -1
- package/dist/propagate.d.ts +20 -0
- package/dist/propagate.d.ts.map +1 -0
- package/dist/propagate.js +226 -0
- package/dist/propagate.js.map +1 -0
- package/dist/requirements.d.ts +27 -0
- package/dist/requirements.d.ts.map +1 -0
- package/dist/requirements.js +188 -0
- package/dist/requirements.js.map +1 -0
- package/dist/ship.d.ts +18 -0
- package/dist/ship.d.ts.map +1 -0
- package/dist/ship.js +131 -0
- package/dist/ship.js.map +1 -0
- package/dist/train.d.ts +45 -0
- package/dist/train.d.ts.map +1 -0
- package/dist/train.js +102 -0
- package/dist/train.js.map +1 -0
- package/package.json +2 -2
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
// propagate: bring every consumer of the presets up to a train (§ 3 PLX-Updates).
|
|
2
|
+
//
|
|
3
|
+
// `copier update` at every consumer site in the workspace (tenant repo roots
|
|
4
|
+
// and their apps/*), then the marker synced to the answers file, lockfiles
|
|
5
|
+
// refreshed where manifests moved, and the consumers' hand-owned workflows
|
|
6
|
+
// pinned to the train. A consumer is touched only on its expected branch and
|
|
7
|
+
// with no tracked modifications (consumers.ts). Propagation never aborts the
|
|
8
|
+
// run: a failing or conflicting site is recorded and the rest continues; the
|
|
9
|
+
// summary says what needs a human. Reviewing diffs and committing the
|
|
10
|
+
// consumers stays human.
|
|
11
|
+
//
|
|
12
|
+
// The dry run is an honest preview: each consumer is cloned into a scratch
|
|
13
|
+
// directory and copier-updated there from the LOCAL preset checkout at main,
|
|
14
|
+
// so the diff and any conflicts show before a tag exists. That scratch work
|
|
15
|
+
// goes through `Context.stage`: performed in every mode, since nothing the
|
|
16
|
+
// operator owns changes.
|
|
17
|
+
//
|
|
18
|
+
// plx propagate ~/dev # to the current train
|
|
19
|
+
// plx -n propagate ~/dev # preview in scratch clones
|
|
20
|
+
// plx -y propagate ~/dev --train 0.66.0 # unattended: copier takes defaults
|
|
21
|
+
import { join, relative } from 'node:path';
|
|
22
|
+
import { bumpWorkflowRefs, hasConflictMarkers, hasUnmergedEntries, syncMarker, WORKFLOW_REF } from "./bookkeeping.js";
|
|
23
|
+
import { findConsumers, presetSource } from "./consumers.js";
|
|
24
|
+
import { ANSWERS_FILE, parseAnswers } from "./copier.js";
|
|
25
|
+
import { MARKER_FILE } from "./marker.js";
|
|
26
|
+
import { overview } from "./overview.js";
|
|
27
|
+
import { run, write } from "./plan.js";
|
|
28
|
+
import { TRAIN_REPOS, UPSTREAM } from "./train.js";
|
|
29
|
+
/** True when nothing in the summary needs a human. */
|
|
30
|
+
export const isClean = (s) => s.conflicted.length === 0 && s.failed.length === 0 && s.skipped.length === 0;
|
|
31
|
+
const PREVIEW_BASE = 'ship-base';
|
|
32
|
+
export async function propagate(ctx, workspace, train) {
|
|
33
|
+
const summary = { train, preview: ctx.dryRun, clean: [], conflicted: [], failed: [], skipped: [] };
|
|
34
|
+
const upstream = join(workspace, UPSTREAM);
|
|
35
|
+
const tree = await overview(ctx, workspace);
|
|
36
|
+
const upstreamDir = tree.children.find((c) => c.path === upstream);
|
|
37
|
+
const presets = TRAIN_REPOS.filter((r) => upstreamDir?.children.some((c) => c.name === r && c.template));
|
|
38
|
+
const consumers = await findConsumers(ctx, tree, presets);
|
|
39
|
+
for (const c of consumers) {
|
|
40
|
+
if (!c.skipped)
|
|
41
|
+
continue;
|
|
42
|
+
ctx.say(`⚠ ${c.rel}: ${c.skipped} — skipped (fix it, then re-run propagate)`);
|
|
43
|
+
summary.skipped.push({ path: c.rel, reason: c.skipped });
|
|
44
|
+
}
|
|
45
|
+
const active = consumers.filter((c) => !c.skipped);
|
|
46
|
+
const scratch = ctx.dryRun ? (await ctx.stage(run('mktemp', ['-d'])))?.trim() : undefined;
|
|
47
|
+
const clones = new Map();
|
|
48
|
+
/** The preview clone of a consumer, made on first use, with the base marked for diffing manifests. */
|
|
49
|
+
const cloneOf = async (consumer) => {
|
|
50
|
+
let clone = clones.get(consumer.path);
|
|
51
|
+
if (!clone) {
|
|
52
|
+
clone = join(scratch ?? '', consumer.rel);
|
|
53
|
+
await ctx.stage(run('git', ['clone', '--quiet', consumer.path, clone]));
|
|
54
|
+
await ctx.stage(run('git', ['tag', PREVIEW_BASE], { cwd: clone }));
|
|
55
|
+
clones.set(consumer.path, clone);
|
|
56
|
+
}
|
|
57
|
+
return clone;
|
|
58
|
+
};
|
|
59
|
+
// Preview commits are plain plumbing on a throwaway clone: unsigned, no hooks, a stand-in identity.
|
|
60
|
+
const previewCommit = (clone, message) => ctx
|
|
61
|
+
.stage(run('git', ['add', '-A'], { cwd: clone }))
|
|
62
|
+
.then(() => ctx.stage(run('git', [
|
|
63
|
+
'-c',
|
|
64
|
+
'commit.gpgsign=false',
|
|
65
|
+
'-c',
|
|
66
|
+
'user.name=plx',
|
|
67
|
+
'-c',
|
|
68
|
+
'user.email=plx@localhost',
|
|
69
|
+
'commit',
|
|
70
|
+
'--quiet',
|
|
71
|
+
'--no-verify',
|
|
72
|
+
'--allow-empty',
|
|
73
|
+
'-m',
|
|
74
|
+
message,
|
|
75
|
+
], { cwd: clone })));
|
|
76
|
+
try {
|
|
77
|
+
for (const preset of presets) {
|
|
78
|
+
const sites = active.flatMap((c) => c.sites.filter((s) => s.preset === preset).map((s) => [c, s]));
|
|
79
|
+
if (sites.length === 0)
|
|
80
|
+
ctx.say(`• no consumers of ${presetSource(preset)} found under ${workspace}`);
|
|
81
|
+
for (const [consumer, site] of sites)
|
|
82
|
+
await updateSite(ctx, consumer, site, join(upstream, preset), summary, cloneOf, previewCommit);
|
|
83
|
+
}
|
|
84
|
+
await refreshLockfiles(ctx, active, summary, clones);
|
|
85
|
+
await bumpConsumerRefs(ctx, consumers, train);
|
|
86
|
+
}
|
|
87
|
+
finally {
|
|
88
|
+
if (scratch)
|
|
89
|
+
await ctx.stage(run('rm', ['-rf', scratch]));
|
|
90
|
+
}
|
|
91
|
+
return summary;
|
|
92
|
+
}
|
|
93
|
+
async function updateSite(ctx, consumer, site, localPreset, summary, cloneOf, previewCommit) {
|
|
94
|
+
// The repo the update lands in and the site inside it: the consumer and the
|
|
95
|
+
// site themselves, or their counterparts in the preview clone.
|
|
96
|
+
let repo = consumer.path;
|
|
97
|
+
let target = site.path;
|
|
98
|
+
try {
|
|
99
|
+
if (ctx.dryRun) {
|
|
100
|
+
ctx.say(`→ ${site.rel}: copier update (preview from ${localPreset}@main)`);
|
|
101
|
+
repo = await cloneOf(consumer);
|
|
102
|
+
target = join(repo, relative(consumer.path, site.path));
|
|
103
|
+
// Point the clone's answers at the local preset so the preview reads it;
|
|
104
|
+
// copier refuses a dirty subproject, hence the commit. Template tasks
|
|
105
|
+
// (mise install, hook setup) are the real run's business.
|
|
106
|
+
const answers = await ctx.readFile(join(site.path, ANSWERS_FILE));
|
|
107
|
+
await ctx.stage(write(join(target, ANSWERS_FILE), answers.replace(/^_src_path: .*$/m, `_src_path: ${localPreset}`)));
|
|
108
|
+
await previewCommit(repo, `plx preview: ${site.rel} reads ${localPreset} from the local checkout`);
|
|
109
|
+
await ctx.stage(run('copier', ['update', '--skip-answered', '--trust', '--defaults', '--skip-tasks', '--vcs-ref', 'main'], {
|
|
110
|
+
cwd: target,
|
|
111
|
+
}));
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
ctx.say(`→ ${site.rel}: copier update`);
|
|
115
|
+
// Unattended, a new question takes its default; on a terminal copier asks.
|
|
116
|
+
const args = ['update', '--skip-answered', '--trust', ...(ctx.yes ? ['--defaults'] : [])];
|
|
117
|
+
await ctx.apply([run('copier', args, { cwd: target, interactive: true })]);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
catch (error) {
|
|
121
|
+
ctx.say(` ✗ FAILED — copier update exited nonzero${detail(error)}`);
|
|
122
|
+
summary.failed.push(site.rel);
|
|
123
|
+
return;
|
|
124
|
+
}
|
|
125
|
+
// copier applies the template diff with `git apply --3way`, which leaves
|
|
126
|
+
// unmerged index entries and inline markers when a hunk does not apply.
|
|
127
|
+
const status = await ctx.read('git', ['status', '--porcelain'], { cwd: repo });
|
|
128
|
+
let conflict = hasUnmergedEntries(status);
|
|
129
|
+
const changed = (await ctx.read('git', ['diff', '--name-only'], { cwd: repo })).split('\n').filter(Boolean);
|
|
130
|
+
for (const f of changed) {
|
|
131
|
+
const text = await ctx.readFile(join(repo, f)).catch(() => '');
|
|
132
|
+
if (hasConflictMarkers(text))
|
|
133
|
+
conflict = true;
|
|
134
|
+
}
|
|
135
|
+
await syncSiteMarker(ctx, site, target);
|
|
136
|
+
if (conflict) {
|
|
137
|
+
ctx.say(' ✗ CONFLICTS — resolve the <<<<<<< markers, then git add:');
|
|
138
|
+
summary.conflicted.push(site.rel);
|
|
139
|
+
}
|
|
140
|
+
else {
|
|
141
|
+
ctx.say(' ✓ merged clean:');
|
|
142
|
+
summary.clean.push(site.rel);
|
|
143
|
+
}
|
|
144
|
+
for (const line of status.split('\n').filter(Boolean))
|
|
145
|
+
ctx.say(` ${line}`);
|
|
146
|
+
// Each preview site is committed in the clone so the next site's status lists only its own changes.
|
|
147
|
+
if (ctx.dryRun)
|
|
148
|
+
await previewCommit(repo, `plx preview: ${site.rel} updated`);
|
|
149
|
+
}
|
|
150
|
+
/** The marker's `plexus` follows the answers file's `_commit` after an update (bookkeeping.ts). */
|
|
151
|
+
async function syncSiteMarker(ctx, site, target) {
|
|
152
|
+
const marker = await ctx.readFile(join(target, MARKER_FILE)).catch(() => undefined);
|
|
153
|
+
if (marker === undefined)
|
|
154
|
+
return;
|
|
155
|
+
const { commit } = parseAnswers(await ctx.readFile(join(target, ANSWERS_FILE)));
|
|
156
|
+
if (!commit)
|
|
157
|
+
return;
|
|
158
|
+
const synced = syncMarker(marker, commit);
|
|
159
|
+
if (synced === marker)
|
|
160
|
+
return;
|
|
161
|
+
ctx.say(` marker: plexus → ${commit} in ${site.rel}/${MARKER_FILE}`);
|
|
162
|
+
const step = write(join(target, MARKER_FILE), synced);
|
|
163
|
+
if (ctx.dryRun)
|
|
164
|
+
await ctx.stage(step);
|
|
165
|
+
else
|
|
166
|
+
await ctx.apply([step]);
|
|
167
|
+
}
|
|
168
|
+
/** Refresh the lockfile once per consumer whose manifests the updates touched. */
|
|
169
|
+
async function refreshLockfiles(ctx, consumers, summary, clones) {
|
|
170
|
+
const manifests = ['*package.json', 'pnpm-workspace.yaml'];
|
|
171
|
+
for (const c of consumers) {
|
|
172
|
+
if ((await ctx.readFile(join(c.path, 'pnpm-workspace.yaml')).catch(() => undefined)) === undefined)
|
|
173
|
+
continue;
|
|
174
|
+
const repo = ctx.dryRun ? clones.get(c.path) : c.path;
|
|
175
|
+
if (!repo)
|
|
176
|
+
continue;
|
|
177
|
+
const range = ctx.dryRun ? [PREVIEW_BASE] : [];
|
|
178
|
+
const unchanged = await ctx
|
|
179
|
+
.read('git', ['diff', '--quiet', ...range, '--', ...manifests], { cwd: repo })
|
|
180
|
+
.then(() => true)
|
|
181
|
+
.catch(() => false);
|
|
182
|
+
if (unchanged)
|
|
183
|
+
continue;
|
|
184
|
+
ctx.say(`→ ${c.rel}: pnpm install (manifests changed)`);
|
|
185
|
+
try {
|
|
186
|
+
await ctx.apply([run('mise', ['exec', '--', 'pnpm', 'install', '--reporter=silent'], { cwd: c.path })]);
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
ctx.say(` ✗ pnpm install failed${detail(error)}`);
|
|
190
|
+
summary.failed.push(`${c.rel}/pnpm-lock.yaml`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
/** Consumers' hand-owned workflows pin ci-cd by tag; lockstep them to the train. */
|
|
195
|
+
async function bumpConsumerRefs(ctx, consumers, train) {
|
|
196
|
+
for (const c of consumers) {
|
|
197
|
+
const dir = join(c.path, '.github', 'workflows');
|
|
198
|
+
const entries = await ctx.readDir(dir).catch(() => []);
|
|
199
|
+
const files = entries
|
|
200
|
+
.filter((e) => !e.directory && /\.ya?ml$/.test(e.name))
|
|
201
|
+
.map((e) => e.name)
|
|
202
|
+
.sort();
|
|
203
|
+
if (files.length === 0)
|
|
204
|
+
continue;
|
|
205
|
+
if (c.skipped) {
|
|
206
|
+
ctx.say(`⚠ ${c.rel}: workflow refs not bumped (skipped above)`);
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
for (const f of files) {
|
|
210
|
+
const text = await ctx.readFile(join(dir, f));
|
|
211
|
+
if (!text.includes(WORKFLOW_REF))
|
|
212
|
+
continue;
|
|
213
|
+
const bumped = bumpWorkflowRefs(text, train);
|
|
214
|
+
if (bumped === text)
|
|
215
|
+
continue;
|
|
216
|
+
ctx.say(`→ ${c.rel}: ${f} ci-cd refs → @${train}`);
|
|
217
|
+
await ctx.apply([write(join(dir, f), bumped)]);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
const detail = (error) => {
|
|
222
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
223
|
+
const last = message.trim().split('\n').at(-1) ?? '';
|
|
224
|
+
return last && !/exited with/.test(last) ? `: ${last}` : '';
|
|
225
|
+
};
|
|
226
|
+
//# sourceMappingURL=propagate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"propagate.js","sourceRoot":"","sources":["../src/propagate.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { Context } from './context.ts';
|
|
2
|
+
export type PageMode = 'map' | 'module';
|
|
3
|
+
export interface Scan {
|
|
4
|
+
/** `✗ <name>: line N: ...`, in page order. */
|
|
5
|
+
defects: string[];
|
|
6
|
+
/** The requirement lines of a standard module; empty for anything else. */
|
|
7
|
+
requirements: string[];
|
|
8
|
+
/** The frontmatter fields the cross-page rules need. */
|
|
9
|
+
code?: string;
|
|
10
|
+
maturity?: string;
|
|
11
|
+
binds?: string;
|
|
12
|
+
depends: string[];
|
|
13
|
+
consumes: string[];
|
|
14
|
+
}
|
|
15
|
+
export declare function scanPage(text: string, name: string, mode: PageMode): Scan;
|
|
16
|
+
/** The requirement lines of a page, defects ignored: what `changed` compares across trains. */
|
|
17
|
+
export declare const extractRequirements: (text: string) => string[];
|
|
18
|
+
export declare const MAP_PAGE = "essentials/standard.md";
|
|
19
|
+
export interface CompendiumCheck {
|
|
20
|
+
defects: string[];
|
|
21
|
+
contracts: number;
|
|
22
|
+
approaches: number;
|
|
23
|
+
}
|
|
24
|
+
/** Every module page of a compendium checkout: `<dir>/<file>`, sorted, index pages left out. */
|
|
25
|
+
export declare function modulePages(ctx: Context, compendium: string): Promise<string[]>;
|
|
26
|
+
export declare function checkCompendium(ctx: Context, compendium: string): Promise<CompendiumCheck>;
|
|
27
|
+
//# sourceMappingURL=requirements.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"requirements.d.ts","sourceRoot":"","sources":["../src/requirements.ts"],"names":[],"mappings":"AAsBA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAE5C,MAAM,MAAM,QAAQ,GAAG,KAAK,GAAG,QAAQ,CAAC;AAExC,MAAM,WAAW,IAAI;IACnB,8CAA8C;IAC9C,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,2EAA2E;IAC3E,YAAY,EAAE,MAAM,EAAE,CAAC;IACvB,wDAAwD;IACxD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAgBD,wBAAgB,QAAQ,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,QAAQ,GAAG,IAAI,CAwEzE;AAED,+FAA+F;AAC/F,eAAO,MAAM,mBAAmB,GAAI,MAAM,MAAM,KAAG,MAAM,EAA+C,CAAC;AAEzG,eAAO,MAAM,QAAQ,2BAA2B,CAAC;AAEjD,MAAM,WAAW,eAAe;IAC9B,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,MAAM,CAAC;CACpB;AAED,gGAAgG;AAChG,wBAAsB,WAAW,CAAC,GAAG,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,MAAM,EAAE,CAAC,CAQrF;AAED,wBAAsB,eAAe,CAAC,GAAG,EAAE,OAAO,EAAE,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,eAAe,CAAC,CAuDhG"}
|
|
@@ -0,0 +1,188 @@
|
|
|
1
|
+
// The requirements check (§ 1.8 PLX), the compendium's `requirements.sh`
|
|
2
|
+
// moved: a verb, not a system.
|
|
3
|
+
//
|
|
4
|
+
// scanPage one page: its defects, and for a standard module its
|
|
5
|
+
// requirement lines, `§ <section> <text>`, in page order
|
|
6
|
+
// checkCompendium the map and every module page, plus the cross-page rules:
|
|
7
|
+
// unique codes, known dependencies, dependencies that respect
|
|
8
|
+
// the seams (§ 1.3 PLX)
|
|
9
|
+
//
|
|
10
|
+
// The map carries no requirements and no BCP 14 keyword. A module at standard
|
|
11
|
+
// maturity carries its requirements as blockquoted bullets ("> - ") under
|
|
12
|
+
// numbered section headings; a keyword anywhere else, a blockquote that is not
|
|
13
|
+
// such a bullet, or any keyword or blockquote in a module below standard
|
|
14
|
+
// maturity is a defect. The trailing "## Maintaining" section is prose
|
|
15
|
+
// throughout. Extraction is structural: no classification, no heuristics.
|
|
16
|
+
// `ship` compares a module's extracted lines across trains to stamp `changed`,
|
|
17
|
+
// so the extractor and the linter are one function and cannot disagree.
|
|
18
|
+
//
|
|
19
|
+
// plx requirements <compendium> # lint; exit 1 on any defect
|
|
20
|
+
// plx requirements --extract contracts/app.md
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
import { CATALOG_DIRS } from "./catalog.js";
|
|
23
|
+
const KEYWORD = /MUST|SHOULD|MAY/;
|
|
24
|
+
/** A BCP 14 keyword in a line, the informal plurals (MUSTs, SHOULDs, MAYs) not counting. */
|
|
25
|
+
const hasKeyword = (line) => KEYWORD.test(line.replace(/MUSTs|SHOULDs|MAYs/g, ''));
|
|
26
|
+
/** A bracketed, comma-separated list as the frontmatter writes it: `[A, B]`. */
|
|
27
|
+
const list = (value) => value
|
|
28
|
+
.replace(/^\s*\[/, '')
|
|
29
|
+
.replace(/\].*$/, '')
|
|
30
|
+
.split(',')
|
|
31
|
+
.map((s) => s.trim())
|
|
32
|
+
.filter(Boolean);
|
|
33
|
+
export function scanPage(text, name, mode) {
|
|
34
|
+
const scan = { defects: [], requirements: [], depends: [], consumes: [] };
|
|
35
|
+
const lines = text.split('\n');
|
|
36
|
+
const defect = (n, msg) => scan.defects.push(`✗ ${name}: line ${n + 1}: ${msg}`);
|
|
37
|
+
let i = 0;
|
|
38
|
+
if (lines[0] === '---') {
|
|
39
|
+
for (i = 1; i < lines.length && lines[i] !== '---'; i++) {
|
|
40
|
+
const line = lines[i] ?? '';
|
|
41
|
+
const [key = '', ...rest] = line.split(/\s+/);
|
|
42
|
+
const value = rest.join(' ');
|
|
43
|
+
if (key === 'maturity:')
|
|
44
|
+
scan.maturity = rest[0];
|
|
45
|
+
else if (key === 'code:')
|
|
46
|
+
scan.code = rest[0];
|
|
47
|
+
else if (key === 'binds:')
|
|
48
|
+
scan.binds = rest[0];
|
|
49
|
+
else if (key === 'depends:')
|
|
50
|
+
scan.depends = list(value);
|
|
51
|
+
else if (key === 'consumes:')
|
|
52
|
+
scan.consumes = list(value);
|
|
53
|
+
else if (key === 'kind:')
|
|
54
|
+
defect(i, 'kind is the folder, contracts/ or approaches/, not a field');
|
|
55
|
+
}
|
|
56
|
+
i++;
|
|
57
|
+
}
|
|
58
|
+
let inCode = false;
|
|
59
|
+
let maintaining = false;
|
|
60
|
+
let started = false;
|
|
61
|
+
let section = '';
|
|
62
|
+
for (; i < lines.length; i++) {
|
|
63
|
+
const line = lines[i] ?? '';
|
|
64
|
+
if (line.startsWith('```')) {
|
|
65
|
+
inCode = !inCode;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (inCode)
|
|
69
|
+
continue;
|
|
70
|
+
if (line === '## Maintaining') {
|
|
71
|
+
maintaining = true;
|
|
72
|
+
section = '';
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
const heading = /^#+ § (\S+)/.exec(line);
|
|
76
|
+
if (heading) {
|
|
77
|
+
if (maintaining)
|
|
78
|
+
defect(i, 'numbered section after Maintaining');
|
|
79
|
+
section = heading[1] ?? '';
|
|
80
|
+
started = true;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (line.startsWith('> - ')) {
|
|
84
|
+
if (mode === 'map')
|
|
85
|
+
defect(i, `requirement in the map: ${line}`);
|
|
86
|
+
else if (scan.maturity !== 'standard')
|
|
87
|
+
defect(i, `requirement in a module at ${scan.maturity} maturity: ${line}`);
|
|
88
|
+
else if (maintaining)
|
|
89
|
+
defect(i, `requirement under Maintaining: ${line}`);
|
|
90
|
+
else if (!started)
|
|
91
|
+
defect(i, 'requirement before the first § heading');
|
|
92
|
+
else
|
|
93
|
+
scan.requirements.push(`§ ${section} ${line.slice(4)}`);
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (line.startsWith('>')) {
|
|
97
|
+
defect(i, 'blockquote line is not a "> - " requirement bullet');
|
|
98
|
+
continue;
|
|
99
|
+
}
|
|
100
|
+
if (hasKeyword(line))
|
|
101
|
+
defect(i, `BCP 14 keyword outside a requirement: ${line}`);
|
|
102
|
+
}
|
|
103
|
+
if (mode === 'module') {
|
|
104
|
+
const { maturity = '', binds = '', code = '' } = scan;
|
|
105
|
+
// Reported at the page's last line, like awk's NR at END.
|
|
106
|
+
const n = text.endsWith('\n') ? lines.length - 1 : lines.length;
|
|
107
|
+
if (!/^(note|candidate|standard)$/.test(maturity))
|
|
108
|
+
defect(n - 1, `maturity must be note, candidate or standard, not "${maturity}"`);
|
|
109
|
+
if (!/^(repo|app|host|organisation)$/.test(binds))
|
|
110
|
+
defect(n - 1, `binds must be repo, app, host or organisation, not "${binds}"`);
|
|
111
|
+
if (maturity === 'note' && code !== '')
|
|
112
|
+
defect(n - 1, 'a note carries no code');
|
|
113
|
+
if (maturity !== 'note' && !/^PLX-[A-Z][A-Za-z]+$/.test(code))
|
|
114
|
+
defect(n - 1, `code must be PLX- plus one capitalised word, not "${code}"`);
|
|
115
|
+
}
|
|
116
|
+
return scan;
|
|
117
|
+
}
|
|
118
|
+
/** The requirement lines of a page, defects ignored: what `changed` compares across trains. */
|
|
119
|
+
export const extractRequirements = (text) => scanPage(text, '', 'module').requirements;
|
|
120
|
+
export const MAP_PAGE = 'essentials/standard.md';
|
|
121
|
+
/** Every module page of a compendium checkout: `<dir>/<file>`, sorted, index pages left out. */
|
|
122
|
+
export async function modulePages(ctx, compendium) {
|
|
123
|
+
const pages = [];
|
|
124
|
+
for (const dir of Object.keys(CATALOG_DIRS)) {
|
|
125
|
+
const entries = await ctx.readDir(join(compendium, dir));
|
|
126
|
+
for (const e of entries.filter((e) => !e.directory && e.name.endsWith('.md') && e.name !== 'index.md').sort())
|
|
127
|
+
pages.push(`${dir}/${e.name}`);
|
|
128
|
+
}
|
|
129
|
+
return pages;
|
|
130
|
+
}
|
|
131
|
+
export async function checkCompendium(ctx, compendium) {
|
|
132
|
+
const defects = [];
|
|
133
|
+
defects.push(...scanPage(await ctx.readFile(join(compendium, MAP_PAGE)), MAP_PAGE, 'map').defects);
|
|
134
|
+
const pages = await modulePages(ctx, compendium);
|
|
135
|
+
const scans = new Map();
|
|
136
|
+
for (const page of pages) {
|
|
137
|
+
const scan = scanPage(await ctx.readFile(join(compendium, page)), page, 'module');
|
|
138
|
+
defects.push(...scan.defects);
|
|
139
|
+
scans.set(page, scan);
|
|
140
|
+
}
|
|
141
|
+
// Module codes are unique across pages.
|
|
142
|
+
const byCode = new Map();
|
|
143
|
+
for (const [page, s] of scans)
|
|
144
|
+
if (s.code)
|
|
145
|
+
byCode.set(s.code, [...(byCode.get(s.code) ?? []), page]);
|
|
146
|
+
for (const [code, where] of byCode)
|
|
147
|
+
if (where.length > 1)
|
|
148
|
+
defects.push(`✗ duplicate module code: ${code}`);
|
|
149
|
+
// Every declared dependency names a module that exists.
|
|
150
|
+
for (const s of scans.values())
|
|
151
|
+
for (const dep of s.depends)
|
|
152
|
+
if (!byCode.has(dep))
|
|
153
|
+
defects.push(`✗ unknown module in depends: ${dep}`);
|
|
154
|
+
// Dependencies respect the seams (§ 1.3 PLX): a contract depends only on
|
|
155
|
+
// contracts, a dependency never crosses from the dev side (repo, app) to the
|
|
156
|
+
// ops side (host, organisation) or back, and what crosses a seam is listed
|
|
157
|
+
// under consumes and is a contract.
|
|
158
|
+
const kindOf = (page) => (page.startsWith('contracts/') ? 'contract' : 'approach');
|
|
159
|
+
const side = (binds) => (binds === 'repo' || binds === 'app' ? 'dev' : 'ops');
|
|
160
|
+
const kinds = new Map([...byCode].map(([code, [page = '']]) => [code, kindOf(page)]));
|
|
161
|
+
const binds = new Map([...byCode].map(([code, [page = '']]) => [code, scans.get(page)?.binds ?? '']));
|
|
162
|
+
for (const [page, s] of scans) {
|
|
163
|
+
const m = s.code ?? page.replace(/.*\//, '');
|
|
164
|
+
const kind = kindOf(page);
|
|
165
|
+
for (const c of s.consumes) {
|
|
166
|
+
if (kind === 'contract')
|
|
167
|
+
defects.push(`✗ ${m} is a contract and consumes ${c}; a contract has no other side to consume`);
|
|
168
|
+
else if (kinds.has(c) && kinds.get(c) !== 'contract')
|
|
169
|
+
defects.push(`✗ ${m} consumes ${c}, which is an approach; only a contract is consumed across a seam`);
|
|
170
|
+
else if (!kinds.has(c))
|
|
171
|
+
defects.push(`✗ ${m} consumes unknown module ${c}`);
|
|
172
|
+
}
|
|
173
|
+
for (const d of s.depends) {
|
|
174
|
+
if (!kinds.has(d))
|
|
175
|
+
continue;
|
|
176
|
+
if (kind === 'contract' && kinds.get(d) !== 'contract')
|
|
177
|
+
defects.push(`✗ ${m} is a contract and depends on the approach ${d}`);
|
|
178
|
+
if (side(s.binds ?? '') !== side(binds.get(d) ?? '') && kinds.get(d) !== 'contract')
|
|
179
|
+
defects.push(`✗ ${m} (${s.binds}) depends on ${d} (${binds.get(d)}) across the dev/ops seam; only a contract may be depended on across a seam`);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
return {
|
|
183
|
+
defects,
|
|
184
|
+
contracts: pages.filter((p) => p.startsWith('contracts/')).length,
|
|
185
|
+
approaches: pages.filter((p) => p.startsWith('approaches/')).length,
|
|
186
|
+
};
|
|
187
|
+
}
|
|
188
|
+
//# sourceMappingURL=requirements.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"requirements.js","sourceRoot":"","sources":["../src/requirements.ts"],"names":[],"mappings":"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"}
|
package/dist/ship.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { Context } from './context.ts';
|
|
2
|
+
import { type Summary } from './propagate.ts';
|
|
3
|
+
export interface ShipOptions {
|
|
4
|
+
/** The workspace: the upstream umbrella and the consumers sit under it. */
|
|
5
|
+
workspace: string;
|
|
6
|
+
/** The train version; default: minor+1 over the latest, or the current one when only propagating. */
|
|
7
|
+
train?: string;
|
|
8
|
+
/** Skip the release; only propagate to the consumers. */
|
|
9
|
+
propagateOnly?: boolean;
|
|
10
|
+
/** The date stamped on the map, `YYYY-MM-DD`; default: today. */
|
|
11
|
+
today?: string;
|
|
12
|
+
}
|
|
13
|
+
export interface ShipResult extends Summary {
|
|
14
|
+
/** The version released, or already current when only propagating. */
|
|
15
|
+
released: boolean;
|
|
16
|
+
}
|
|
17
|
+
export declare function ship(ctx: Context, options: ShipOptions): Promise<ShipResult>;
|
|
18
|
+
//# sourceMappingURL=ship.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ship.d.ts","sourceRoot":"","sources":["../src/ship.ts"],"names":[],"mappings":"AA8BA,OAAO,KAAK,EAAE,OAAO,EAAE,MAAM,cAAc,CAAC;AAG5C,OAAO,EAAa,KAAK,OAAO,EAAE,MAAM,gBAAgB,CAAC;AAIzD,MAAM,WAAW,WAAW;IAC1B,2EAA2E;IAC3E,SAAS,EAAE,MAAM,CAAC;IAClB,qGAAqG;IACrG,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yDAAyD;IACzD,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,iEAAiE;IACjE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,UAAW,SAAQ,OAAO;IACzC,sEAAsE;IACtE,QAAQ,EAAE,OAAO,CAAC;CACnB;AAED,wBAAsB,IAAI,CAAC,GAAG,EAAE,OAAO,EAAE,OAAO,EAAE,WAAW,GAAG,OAAO,CAAC,UAAU,CAAC,CASlF"}
|
package/dist/ship.js
ADDED
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
// ship: the release train (§ 1.5 PLX). Every train repo ships under ONE
|
|
2
|
+
// version, tagged identically whether or not it changed:
|
|
3
|
+
//
|
|
4
|
+
// preflight (every train repo on main, clean, not behind origin, the latest
|
|
5
|
+
// tags in agreement) → write the train bookkeeping (bookkeeping.ts: galaxy
|
|
6
|
+
// version, the preset's collection pin, workflow-ref examples, the map's
|
|
7
|
+
// version and timestamp, `changed` on every standard module whose extracted
|
|
8
|
+
// requirements moved) and commit it → tag every train repo → push →
|
|
9
|
+
// propagate to every consumer (propagate.ts).
|
|
10
|
+
//
|
|
11
|
+
// Substance commits stay human; ship writes only the mechanical bookkeeping.
|
|
12
|
+
// A failure between the first tag and the last push leaves a half-tagged
|
|
13
|
+
// train; the recovery is a re-run naming the same version, which skips the
|
|
14
|
+
// repos already tagged and pushes the rest.
|
|
15
|
+
//
|
|
16
|
+
// plx ship ~/dev # next train = minor+1 over the latest
|
|
17
|
+
// plx ship ~/dev --train 1.0.0 # explicit train version
|
|
18
|
+
// plx -n ship ~/dev # preview: list the bookkeeping, tags and
|
|
19
|
+
// # the propagation in scratch clones
|
|
20
|
+
// plx -y ship ~/dev # unattended: no prompt, copier takes defaults
|
|
21
|
+
import { join } from 'node:path';
|
|
22
|
+
import { bumpCollectionPin, bumpGalaxyVersion, bumpWorkflowRefs, stampChanged, stampStandard, today as todayOf, WORKFLOW_REF, } from "./bookkeeping.js";
|
|
23
|
+
import { frontmatterField } from "./frontmatter.js";
|
|
24
|
+
import { run, write } from "./plan.js";
|
|
25
|
+
import { propagate } from "./propagate.js";
|
|
26
|
+
import { extractRequirements, MAP_PAGE, modulePages } from "./requirements.js";
|
|
27
|
+
import { preflightRelease, readTrain, resolveVersion, UPSTREAM } from "./train.js";
|
|
28
|
+
export async function ship(ctx, options) {
|
|
29
|
+
const upstream = join(options.workspace, UPSTREAM);
|
|
30
|
+
const propagateOnly = options.propagateOnly ?? false;
|
|
31
|
+
const train = await readTrain(ctx, upstream);
|
|
32
|
+
if (!propagateOnly)
|
|
33
|
+
await preflightRelease(ctx, train);
|
|
34
|
+
const next = resolveVersion(train, options.train, propagateOnly);
|
|
35
|
+
if (!propagateOnly)
|
|
36
|
+
await release(ctx, train, next, options.today ?? todayOf());
|
|
37
|
+
const summary = await propagate(ctx, options.workspace, next);
|
|
38
|
+
return { ...summary, released: !propagateOnly };
|
|
39
|
+
}
|
|
40
|
+
async function release(ctx, train, next, today) {
|
|
41
|
+
ctx.say(`→ train ${train.last ?? 'none'} → ${next}`);
|
|
42
|
+
for (const repo of train.repos) {
|
|
43
|
+
if (repo.pending.length === 0)
|
|
44
|
+
ctx.say(` ${repo.name}: no changes since ${repo.latest} — tag only`);
|
|
45
|
+
else {
|
|
46
|
+
ctx.say(` ${repo.name} (since ${repo.latest ?? 'the beginning'}):`);
|
|
47
|
+
for (const line of repo.pending)
|
|
48
|
+
ctx.say(` ${line}`);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (!(await ctx.confirm(`tag all ${train.repos.length} repos as ${next} and push?`)))
|
|
52
|
+
throw new Error('aborted');
|
|
53
|
+
try {
|
|
54
|
+
const plan = await planBookkeeping(ctx, train, next, today);
|
|
55
|
+
await ctx.apply(plan);
|
|
56
|
+
for (const repo of train.repos) {
|
|
57
|
+
const tagged = await ctx
|
|
58
|
+
.read('git', ['rev-parse', '-q', '--verify', `refs/tags/${next}`], { cwd: repo.path })
|
|
59
|
+
.then(() => true)
|
|
60
|
+
.catch(() => false);
|
|
61
|
+
if (tagged)
|
|
62
|
+
ctx.say(`• ${repo.name}: already tagged ${next}`);
|
|
63
|
+
else
|
|
64
|
+
await ctx.apply([run('git', ['tag', '-a', next, '-m', `train ${next}`], { cwd: repo.path })]);
|
|
65
|
+
await ctx.apply([run('git', ['push', 'origin', 'main', next], { cwd: repo.path })]);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
70
|
+
throw new Error(`${message}\n✗ ship stopped during the release; re-run with --train ${next} to finish it (tagged repos are skipped)`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
/** The bookkeeping writes, then one commit per train repo that has any. */
|
|
74
|
+
async function planBookkeeping(ctx, train, next, today) {
|
|
75
|
+
const plan = [];
|
|
76
|
+
const touched = new Set();
|
|
77
|
+
const repo = (name) => {
|
|
78
|
+
const r = train.repos.find((r) => r.name === name);
|
|
79
|
+
if (!r)
|
|
80
|
+
throw new Error(`${name} is not a train repo`);
|
|
81
|
+
return r;
|
|
82
|
+
};
|
|
83
|
+
/** Plan a write when the transform changes the file, saying what it does; a missing file is left alone. */
|
|
84
|
+
const edit = async (r, rel, what, transform) => {
|
|
85
|
+
const path = join(r.path, rel);
|
|
86
|
+
const text = await ctx.readFile(path).catch(() => undefined);
|
|
87
|
+
if (text === undefined)
|
|
88
|
+
return;
|
|
89
|
+
const edited = transform(text);
|
|
90
|
+
if (edited === text)
|
|
91
|
+
return;
|
|
92
|
+
ctx.say(` ${r.name}: ${rel}: ${what}`);
|
|
93
|
+
plan.push(write(path, edited));
|
|
94
|
+
touched.add(r);
|
|
95
|
+
};
|
|
96
|
+
await edit(repo('platform'), 'ansible/galaxy.yml', `version → ${next}`, (t) => bumpGalaxyVersion(t, next));
|
|
97
|
+
// Only the plexus.platform collection's pin: a second collection in the same file stays untouched.
|
|
98
|
+
await edit(repo('preset-repo-platform'), 'template/requirements.yml', `plexus.platform pin → ${next}`, (t) => bumpCollectionPin(t, next));
|
|
99
|
+
for (const r of train.repos) {
|
|
100
|
+
const files = await ctx
|
|
101
|
+
.read('git', ['grep', '-l', WORKFLOW_REF, '--', '*.yml', '*.yaml', '*.jinja'], { cwd: r.path })
|
|
102
|
+
.catch(() => '');
|
|
103
|
+
for (const f of files.split('\n').filter(Boolean))
|
|
104
|
+
await edit(r, f, `ci-cd refs → @${next}`, (t) => bumpWorkflowRefs(t, next));
|
|
105
|
+
}
|
|
106
|
+
// The map carries the train version and the date it was cut (§ 1.5 PLX); a
|
|
107
|
+
// standard module is stamped `changed` when its extracted requirement lines
|
|
108
|
+
// differ from the previous train's, read with the same extractor the
|
|
109
|
+
// compendium's check runs, so the two never disagree.
|
|
110
|
+
const comp = repo('compendium');
|
|
111
|
+
await edit(comp, MAP_PAGE, `version → ${next}, timestamp → ${today}`, (t) => stampStandard(t, next, today));
|
|
112
|
+
for (const page of await modulePages(ctx, comp.path)) {
|
|
113
|
+
const text = await ctx.readFile(join(comp.path, page));
|
|
114
|
+
if (frontmatterField(text, 'maturity') !== 'standard')
|
|
115
|
+
continue;
|
|
116
|
+
const previous = comp.tag
|
|
117
|
+
? await ctx.read('git', ['show', `${comp.tag}:${page}`], { cwd: comp.path }).catch(() => '')
|
|
118
|
+
: '';
|
|
119
|
+
if (extractRequirements(text).join('\n') === extractRequirements(previous).join('\n'))
|
|
120
|
+
continue;
|
|
121
|
+
await edit(comp, page, `requirements changed since ${comp.latest ?? 'the beginning'} → changed: ${next}`, (t) => stampChanged(t, next));
|
|
122
|
+
}
|
|
123
|
+
for (const r of train.repos) {
|
|
124
|
+
if (!touched.has(r))
|
|
125
|
+
continue;
|
|
126
|
+
plan.push(run('git', ['add', '-A'], { cwd: r.path }));
|
|
127
|
+
plan.push(run('git', ['commit', '--quiet', '-m', `chore: train ${next} bookkeeping`], { cwd: r.path }));
|
|
128
|
+
}
|
|
129
|
+
return plan;
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=ship.js.map
|