@tallyrelease/core 0.1.1 → 0.2.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/CHANGELOG.md +14 -0
- package/dist/affected.d.ts +2 -2
- package/dist/affected.js +18 -7
- package/dist/branches.d.ts +70 -0
- package/dist/branches.js +168 -0
- package/dist/builtin.d.ts +5 -0
- package/dist/builtin.js +12 -0
- package/dist/config.d.ts +38 -1
- package/dist/conventional.d.ts +20 -0
- package/dist/conventional.js +90 -0
- package/dist/discover.js +5 -2
- package/dist/errors.d.ts +2 -0
- package/dist/explain.d.ts +49 -0
- package/dist/explain.js +62 -0
- package/dist/facts.d.ts +81 -0
- package/dist/facts.js +250 -0
- package/dist/groups.d.ts +15 -0
- package/dist/groups.js +41 -0
- package/dist/index.d.ts +15 -3
- package/dist/index.js +5 -0
- package/dist/plan.d.ts +108 -0
- package/dist/plan.js +351 -0
- package/dist/plugin.d.ts +8 -1
- package/dist/plugins.d.ts +2 -0
- package/dist/plugins.js +17 -11
- package/dist/propagate.d.ts +22 -0
- package/dist/propagate.js +98 -0
- package/dist/release.d.ts +133 -0
- package/dist/release.js +368 -0
- package/dist/schemes.d.ts +12 -0
- package/dist/schemes.js +22 -0
- package/dist/semver.d.ts +11 -0
- package/dist/semver.js +53 -0
- package/dist/tags.d.ts +40 -0
- package/dist/tags.js +92 -0
- package/dist/testing.d.ts +4 -1
- package/dist/testing.js +28 -1
- package/dist/util.d.ts +3 -0
- package/dist/util.js +3 -0
- package/dist/validate.d.ts +14 -0
- package/dist/validate.js +67 -0
- package/dist/workspace.d.ts +18 -1
- package/dist/workspace.js +17 -0
- package/package.json +4 -2
package/dist/plan.js
ADDED
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
import { computeAffected } from "./affected.js";
|
|
2
|
+
import { checkLimits, matchBranch } from "./branches.js";
|
|
3
|
+
import { collectPlanFacts } from "./facts.js";
|
|
4
|
+
import { propagate } from "./propagate.js";
|
|
5
|
+
import { changeLevel, footerOf, reach, releaseAsWarnings, releaseGroup, releaseUnit, unitIntent, } from "./release.js";
|
|
6
|
+
import { noMatchingTag } from "./tags.js";
|
|
7
|
+
import { maxLevel } from "./util.js";
|
|
8
|
+
import { validateRelease } from "./validate.js";
|
|
9
|
+
const lineWarnings = (workspace, branch, line) => {
|
|
10
|
+
if (!line) {
|
|
11
|
+
return [
|
|
12
|
+
{
|
|
13
|
+
code: "W_NO_BRANCH_RULE",
|
|
14
|
+
message: `no branch rule matches "${branch}"; versions are null`,
|
|
15
|
+
},
|
|
16
|
+
];
|
|
17
|
+
}
|
|
18
|
+
return workspace.units.every((u) => u.version === "none")
|
|
19
|
+
? [
|
|
20
|
+
{
|
|
21
|
+
code: "W_NO_RELEASED_UNITS",
|
|
22
|
+
message: `branch rule "${line.rule}" matches, but no unit is released`,
|
|
23
|
+
},
|
|
24
|
+
]
|
|
25
|
+
: [];
|
|
26
|
+
};
|
|
27
|
+
const planChanges = (intent) => (intent?.changes ?? []).map(({ change, via }) => {
|
|
28
|
+
const releaseAs = change.sha === undefined
|
|
29
|
+
? undefined
|
|
30
|
+
: intent?.releaseAsBySha.get(change.sha);
|
|
31
|
+
return {
|
|
32
|
+
...(change.sha === undefined ? {} : { sha: change.sha }),
|
|
33
|
+
type: change.type,
|
|
34
|
+
breaking: change.breaking,
|
|
35
|
+
...(change.scope === undefined ? {} : { scope: change.scope }),
|
|
36
|
+
subject: change.subject,
|
|
37
|
+
...(change.pr === undefined ? {} : { pr: change.pr }),
|
|
38
|
+
...(via === undefined ? {} : { via }),
|
|
39
|
+
...(releaseAs === undefined ? {} : { releaseAs }),
|
|
40
|
+
};
|
|
41
|
+
});
|
|
42
|
+
/**
|
|
43
|
+
* Release inputs of the released units with a scheme and range; the same set
|
|
44
|
+
* bounds absorption here and propagation in `resolvePlan`.
|
|
45
|
+
*/
|
|
46
|
+
const releaseInputs = (workspace, { facts, schemes }, channel) => {
|
|
47
|
+
const units = new Map(workspace.units.map((u) => [u.id, u]));
|
|
48
|
+
const entries = facts.units.flatMap((unitFacts) => {
|
|
49
|
+
const unit = units.get(unitFacts.id);
|
|
50
|
+
const scheme = unit && schemes.get(unit.scheme);
|
|
51
|
+
const range = facts.ranges.find((r) => r.from === unitFacts.from);
|
|
52
|
+
return unit && scheme && range ? [{ unit, scheme, unitFacts, range }] : [];
|
|
53
|
+
});
|
|
54
|
+
const released = new Set(entries.map((e) => e.unit.id));
|
|
55
|
+
return new Map(entries.map(({ unit, scheme, unitFacts, range }) => [
|
|
56
|
+
unit.id,
|
|
57
|
+
{
|
|
58
|
+
unit,
|
|
59
|
+
scheme,
|
|
60
|
+
settings: workspace.release,
|
|
61
|
+
facts: unitFacts,
|
|
62
|
+
range,
|
|
63
|
+
absorbed: reach(unit.id, workspace.graph.dependencies, released),
|
|
64
|
+
head: facts.head,
|
|
65
|
+
channel,
|
|
66
|
+
},
|
|
67
|
+
]));
|
|
68
|
+
};
|
|
69
|
+
const UNRELEASED = {
|
|
70
|
+
from: null,
|
|
71
|
+
bump: null,
|
|
72
|
+
releaseAs: null,
|
|
73
|
+
releaseAsFooter: null,
|
|
74
|
+
next: null,
|
|
75
|
+
nextSource: null,
|
|
76
|
+
prerelease: null,
|
|
77
|
+
};
|
|
78
|
+
/** The unit's plan entry; undefined when it is not listed. */
|
|
79
|
+
const unitPlan = (entry, all) => {
|
|
80
|
+
const { unit, hit, facts } = entry;
|
|
81
|
+
const release = entry.release ?? UNRELEASED;
|
|
82
|
+
if (!hit && !(release.next !== null && all)) {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
const { prerelease } = release;
|
|
86
|
+
const kind = !hit ? "none" : prerelease === null ? "rebuild" : "prerelease";
|
|
87
|
+
return {
|
|
88
|
+
id: unit.id,
|
|
89
|
+
path: unit.root,
|
|
90
|
+
plugin: unit.plugin,
|
|
91
|
+
scheme: unit.scheme,
|
|
92
|
+
tags: [...unit.tags],
|
|
93
|
+
group: entry.group,
|
|
94
|
+
affected: hit ? { reason: hit.reason, via: hit.via } : null,
|
|
95
|
+
kind,
|
|
96
|
+
baseTag: facts?.baseTag?.name ?? null,
|
|
97
|
+
lineBaseTag: facts?.lineBaseTag?.name ?? null,
|
|
98
|
+
from: release.from,
|
|
99
|
+
bump: release.bump,
|
|
100
|
+
releaseAs: release.releaseAs,
|
|
101
|
+
propagatedFrom: [...entry.propagatedFrom],
|
|
102
|
+
absorbedFrom: [...(entry.intent?.absorbedFrom ?? [])],
|
|
103
|
+
next: release.next,
|
|
104
|
+
version: kind === "prerelease" ? prerelease : null,
|
|
105
|
+
sha: entry.head,
|
|
106
|
+
changes: planChanges(entry.intent),
|
|
107
|
+
};
|
|
108
|
+
};
|
|
109
|
+
/** The only release item (ungrouped released unit or group), else null. */
|
|
110
|
+
const singleItem = (workspace) => {
|
|
111
|
+
const grouped = new Set(workspace.groups.flatMap((g) => g.members));
|
|
112
|
+
const items = [
|
|
113
|
+
...workspace.groups.map((g) => ({ unit: null, group: g.name })),
|
|
114
|
+
...workspace.units
|
|
115
|
+
.filter((u) => u.version !== "none" && !grouped.has(u.id))
|
|
116
|
+
.map((u) => ({ unit: u.id, group: null })),
|
|
117
|
+
];
|
|
118
|
+
return items.length === 1 ? (items[0] ?? null) : null;
|
|
119
|
+
};
|
|
120
|
+
/**
|
|
121
|
+
* Limit input of a release item over its member ids: the changes and
|
|
122
|
+
* Release-As values that produce `next`.
|
|
123
|
+
*/
|
|
124
|
+
const limitItem = (item, units) => {
|
|
125
|
+
const first = item.ids
|
|
126
|
+
.map((id) => units.inputs.get(id))
|
|
127
|
+
.find((i) => i !== undefined);
|
|
128
|
+
if (!first) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
const { scheme, settings } = first;
|
|
132
|
+
const core = (version) => {
|
|
133
|
+
const parsed = version === null ? undefined : scheme.parse(version);
|
|
134
|
+
if (parsed === undefined || version === null) {
|
|
135
|
+
return null;
|
|
136
|
+
}
|
|
137
|
+
const { major, minor } = scheme.core(parsed);
|
|
138
|
+
return { major, minor, version };
|
|
139
|
+
};
|
|
140
|
+
const from = core(item.from);
|
|
141
|
+
// Keyed by sha, file-based changes by identity; members merge.
|
|
142
|
+
const commits = new Map();
|
|
143
|
+
for (const id of item.ids) {
|
|
144
|
+
const input = units.inputs.get(id);
|
|
145
|
+
const intent = units.intents.get(id);
|
|
146
|
+
for (const unitChange of intent?.changes ?? []) {
|
|
147
|
+
const { change, via } = unitChange;
|
|
148
|
+
const key = change.sha ?? change;
|
|
149
|
+
const prev = commits.get(key);
|
|
150
|
+
const level = changeLevel(unitChange, settings, from?.major === 0);
|
|
151
|
+
const entry = {
|
|
152
|
+
...(change.sha === undefined ? {} : { sha: change.sha }),
|
|
153
|
+
subject: change.subject,
|
|
154
|
+
level,
|
|
155
|
+
...(via === undefined ? {} : { via }),
|
|
156
|
+
};
|
|
157
|
+
// The highest level wins; a direct change wins over an absorbed one.
|
|
158
|
+
const kept = prev !== undefined &&
|
|
159
|
+
maxLevel(prev.level, level) === prev.level &&
|
|
160
|
+
(level !== prev.level || via !== undefined);
|
|
161
|
+
commits.set(key, kept
|
|
162
|
+
? prev
|
|
163
|
+
: { ...entry, ...(prev?.releaseAs && { releaseAs: prev.releaseAs }) });
|
|
164
|
+
}
|
|
165
|
+
const releaseAs = intent?.releaseAs;
|
|
166
|
+
if (input && releaseAs) {
|
|
167
|
+
const { sha } = releaseAs;
|
|
168
|
+
const key = sha ?? `Release-As: ${releaseAs.raw}`;
|
|
169
|
+
const { major, minor } = scheme.core(releaseAs.version);
|
|
170
|
+
commits.set(key, {
|
|
171
|
+
...(sha === undefined ? {} : { sha }),
|
|
172
|
+
subject: input.range.changes.find((c) => sha !== undefined && c.change.sha === sha)?.change.subject ?? key,
|
|
173
|
+
level: null,
|
|
174
|
+
...commits.get(key),
|
|
175
|
+
releaseAs: {
|
|
176
|
+
major,
|
|
177
|
+
minor,
|
|
178
|
+
version: scheme.format(releaseAs.version),
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return {
|
|
184
|
+
unit: item.unit,
|
|
185
|
+
group: item.group,
|
|
186
|
+
from,
|
|
187
|
+
next: core(item.next),
|
|
188
|
+
commits: [...commits.values()],
|
|
189
|
+
};
|
|
190
|
+
};
|
|
191
|
+
/** Releases of fixed group members, by unit id, and the group entries. */
|
|
192
|
+
const resolveGroupReleases = ({ facts, affected }, units, warnings) => {
|
|
193
|
+
const hits = new Set(affected.units.map((u) => u.id));
|
|
194
|
+
const releases = new Map();
|
|
195
|
+
const groups = [];
|
|
196
|
+
for (const group of facts.groups) {
|
|
197
|
+
const members = group.members.flatMap((id) => {
|
|
198
|
+
const input = units.inputs.get(id);
|
|
199
|
+
const intent = units.intents.get(id);
|
|
200
|
+
return input && intent
|
|
201
|
+
? [
|
|
202
|
+
{
|
|
203
|
+
input,
|
|
204
|
+
bump: units.propagated.get(id)?.bump ?? intent.bump,
|
|
205
|
+
releaseAs: intent.releaseAs,
|
|
206
|
+
},
|
|
207
|
+
]
|
|
208
|
+
: [];
|
|
209
|
+
});
|
|
210
|
+
if (members.length === 0) {
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
const release = releaseGroup(group, members, warnings);
|
|
214
|
+
for (const { input, releaseAs } of members) {
|
|
215
|
+
releases.set(input.unit.id, {
|
|
216
|
+
...release,
|
|
217
|
+
from: input.facts.baseTag?.version ?? null,
|
|
218
|
+
releaseAs: releaseAs === undefined
|
|
219
|
+
? null
|
|
220
|
+
: input.scheme.format(releaseAs.version),
|
|
221
|
+
releaseAsFooter: footerOf(releaseAs),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
groups.push({
|
|
225
|
+
name: group.name,
|
|
226
|
+
members: [...group.members],
|
|
227
|
+
baseTag: group.baseTag?.name ?? null,
|
|
228
|
+
from: release.from,
|
|
229
|
+
bump: release.bump,
|
|
230
|
+
next: release.next,
|
|
231
|
+
version: group.members.some((id) => hits.has(id))
|
|
232
|
+
? release.prerelease
|
|
233
|
+
: null,
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
return { releases, groups };
|
|
237
|
+
};
|
|
238
|
+
/** `resolvePlan` with the release of each released unit. */
|
|
239
|
+
const resolveReleases = (workspace, input) => {
|
|
240
|
+
const { affected, facts } = input;
|
|
241
|
+
const { units: _, ...doc } = affected;
|
|
242
|
+
const line = matchBranch(workspace.release.branches, affected.branch, singleItem(workspace));
|
|
243
|
+
const warnings = [
|
|
244
|
+
...affected.warnings,
|
|
245
|
+
...lineWarnings(workspace, affected.branch, line),
|
|
246
|
+
...facts.warnings,
|
|
247
|
+
...releaseAsWarnings(workspace.units, facts.ranges),
|
|
248
|
+
];
|
|
249
|
+
const rank = new Map(workspace.graph.order.map((id, i) => [id, i]));
|
|
250
|
+
const ordered = [...workspace.units].sort((a, b) => (rank.get(a.id) ?? 0) - (rank.get(b.id) ?? 0));
|
|
251
|
+
const hits = new Map(affected.units.map((u) => [u.id, u]));
|
|
252
|
+
const factsById = new Map(facts.units.map((f) => [f.id, f]));
|
|
253
|
+
const inputs = releaseInputs(workspace, input, line?.channel ?? null);
|
|
254
|
+
const intents = new Map([...inputs].map(([id, i]) => [id, unitIntent(i, warnings)]));
|
|
255
|
+
const groupOf = new Map(workspace.groups.flatMap((g) => g.members.map((id) => [id, g.name])));
|
|
256
|
+
const propagated = propagate(workspace.graph, workspace.units, new Map([...intents].map(([id, i]) => {
|
|
257
|
+
const group = groupOf.get(id);
|
|
258
|
+
return [
|
|
259
|
+
id,
|
|
260
|
+
{
|
|
261
|
+
bump: i.bump,
|
|
262
|
+
pending: i.bump !== null || i.releaseAs !== undefined,
|
|
263
|
+
...(group === undefined ? {} : { group }),
|
|
264
|
+
},
|
|
265
|
+
];
|
|
266
|
+
})));
|
|
267
|
+
const { releases: groupReleases, groups } = resolveGroupReleases(input, { inputs, intents, propagated }, warnings);
|
|
268
|
+
const units = [];
|
|
269
|
+
const releases = new Map();
|
|
270
|
+
for (const unit of ordered) {
|
|
271
|
+
const { id } = unit;
|
|
272
|
+
const unitInput = inputs.get(id);
|
|
273
|
+
const intent = intents.get(id);
|
|
274
|
+
const state = propagated.get(id);
|
|
275
|
+
const release = groupReleases.get(id) ??
|
|
276
|
+
(unitInput && intent
|
|
277
|
+
? releaseUnit(unitInput, { bump: state?.bump ?? intent.bump, releaseAs: intent.releaseAs }, warnings)
|
|
278
|
+
: undefined);
|
|
279
|
+
if (release) {
|
|
280
|
+
releases.set(id, release);
|
|
281
|
+
}
|
|
282
|
+
const entry = unitPlan({
|
|
283
|
+
unit,
|
|
284
|
+
group: groupOf.get(id) ?? null,
|
|
285
|
+
hit: hits.get(id),
|
|
286
|
+
facts: factsById.get(id),
|
|
287
|
+
intent,
|
|
288
|
+
release,
|
|
289
|
+
propagatedFrom: state?.propagatedFrom ?? [],
|
|
290
|
+
head: facts.head,
|
|
291
|
+
}, input.all);
|
|
292
|
+
if (entry) {
|
|
293
|
+
units.push(entry);
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
const limits = { inputs, intents };
|
|
297
|
+
const rule = workspace.release.branches.find((r) => r.name === line?.rule);
|
|
298
|
+
checkLimits(line, [
|
|
299
|
+
...groups.flatMap((g) => limitItem({
|
|
300
|
+
unit: null,
|
|
301
|
+
group: g.name,
|
|
302
|
+
ids: g.members,
|
|
303
|
+
from: g.from,
|
|
304
|
+
next: g.next,
|
|
305
|
+
}, limits) ?? []),
|
|
306
|
+
...ordered.flatMap((u) => {
|
|
307
|
+
const release = releases.get(u.id);
|
|
308
|
+
return groupOf.has(u.id) || !release
|
|
309
|
+
? []
|
|
310
|
+
: (limitItem({
|
|
311
|
+
unit: u.id,
|
|
312
|
+
group: null,
|
|
313
|
+
ids: [u.id],
|
|
314
|
+
from: release.from,
|
|
315
|
+
next: release.next,
|
|
316
|
+
}, limits) ?? []);
|
|
317
|
+
}),
|
|
318
|
+
], {
|
|
319
|
+
branch: affected.branch,
|
|
320
|
+
implicit: rule?.unit === null && rule.group === null,
|
|
321
|
+
});
|
|
322
|
+
warnings.push(...noMatchingTag(workspace.units, facts.units, facts.tagNames));
|
|
323
|
+
return {
|
|
324
|
+
plan: { ...doc, line, units, groups, released: [], warnings },
|
|
325
|
+
releases,
|
|
326
|
+
};
|
|
327
|
+
};
|
|
328
|
+
/** Release math over collected facts; no I/O. */
|
|
329
|
+
export const resolvePlan = (workspace, input) => resolveReleases(workspace, input).plan;
|
|
330
|
+
/** `computePlan` keeping the collected facts, for `explainUnit`. */
|
|
331
|
+
export const planWithFacts = async (workspace, git, opts) => {
|
|
332
|
+
const schemes = validateRelease(workspace);
|
|
333
|
+
const affected = await computeAffected(workspace, git, opts);
|
|
334
|
+
const facts = await collectPlanFacts(workspace, git, {
|
|
335
|
+
head: opts.head,
|
|
336
|
+
fs: opts.fs,
|
|
337
|
+
...(opts.debug && { debug: opts.debug }),
|
|
338
|
+
});
|
|
339
|
+
const { plan, releases } = resolveReleases(workspace, {
|
|
340
|
+
affected,
|
|
341
|
+
facts,
|
|
342
|
+
schemes,
|
|
343
|
+
all: opts.all ?? false,
|
|
344
|
+
});
|
|
345
|
+
return { plan, facts, releases };
|
|
346
|
+
};
|
|
347
|
+
/**
|
|
348
|
+
* Plans `head`: validates release config, computes the build set, collects
|
|
349
|
+
* git and intent facts and resolves versions and kinds.
|
|
350
|
+
*/
|
|
351
|
+
export const computePlan = async (workspace, git, opts) => (await planWithFacts(workspace, git, opts)).plan;
|
package/dist/plugin.d.ts
CHANGED
|
@@ -99,6 +99,8 @@ export interface Unit {
|
|
|
99
99
|
readonly propagate: Propagate;
|
|
100
100
|
/** From the config overlay; sorted, unique. */
|
|
101
101
|
readonly tags: readonly string[];
|
|
102
|
+
/** Config overlay, else `defaults.initialVersion`; null uses `scheme.initial()`. */
|
|
103
|
+
readonly initialVersion: string | null;
|
|
102
104
|
readonly meta: Readonly<Record<string, Readonly<Record<string, unknown>>>>;
|
|
103
105
|
}
|
|
104
106
|
/** `to` depends on `from`. */
|
|
@@ -166,6 +168,11 @@ export interface IntentSource<O = unknown> {
|
|
|
166
168
|
commits: readonly RawCommit[];
|
|
167
169
|
}) => MaybePromise<Change[]>;
|
|
168
170
|
}
|
|
171
|
+
/** A `Release-As` value; `unit` is set by the qualified form `<unit>@<version>`. */
|
|
172
|
+
export interface ReleaseAsValue {
|
|
173
|
+
unit?: string;
|
|
174
|
+
version: string;
|
|
175
|
+
}
|
|
169
176
|
export interface Change {
|
|
170
177
|
/** Commit-derived changes are attributed via that commit's files. */
|
|
171
178
|
sha?: string;
|
|
@@ -176,7 +183,7 @@ export interface Change {
|
|
|
176
183
|
scope?: string;
|
|
177
184
|
subject: string;
|
|
178
185
|
body?: string;
|
|
179
|
-
releaseAs?:
|
|
186
|
+
releaseAs?: ReleaseAsValue[];
|
|
180
187
|
pr?: number;
|
|
181
188
|
date?: string;
|
|
182
189
|
}
|
package/dist/plugins.d.ts
CHANGED
|
@@ -18,6 +18,8 @@ export interface PluginEnv {
|
|
|
18
18
|
};
|
|
19
19
|
debug?: (message: string) => void;
|
|
20
20
|
}
|
|
21
|
+
/** Frozen `Ctx` factory; warnings are tagged with the plugin name. */
|
|
22
|
+
export declare const pluginCtx: (env: Pick<PluginEnv, "fs" | "repo" | "debug">, warnings: Warning[]) => (name: string, options: unknown) => Ctx;
|
|
21
23
|
/** Resolves enabled plugins in config order and validates their options. */
|
|
22
24
|
export declare const activatePlugins: (env: PluginEnv, warnings: Warning[]) => Promise<ActivePlugin[]>;
|
|
23
25
|
/** Awaits a plugin hook, wrapping thrown errors as `E_PLUGIN_HOOK`. */
|
package/dist/plugins.js
CHANGED
|
@@ -1,5 +1,18 @@
|
|
|
1
|
+
import { CORE_PLUGIN } from "./builtin.js";
|
|
1
2
|
import { TallyError } from "./errors.js";
|
|
2
3
|
import { assertPlugin } from "./plugin.js";
|
|
4
|
+
/** Frozen `Ctx` factory; warnings are tagged with the plugin name. */
|
|
5
|
+
export const pluginCtx = (env, warnings) => (name, options) => Object.freeze({
|
|
6
|
+
options,
|
|
7
|
+
repo: Object.freeze({ name: env.repo.name }),
|
|
8
|
+
fs: env.fs,
|
|
9
|
+
log: Object.freeze({
|
|
10
|
+
warn: (message) => {
|
|
11
|
+
warnings.push({ code: "W_PLUGIN", message, plugin: name });
|
|
12
|
+
},
|
|
13
|
+
debug: (message) => env.debug?.(`${name}: ${message}`),
|
|
14
|
+
}),
|
|
15
|
+
});
|
|
3
16
|
/** Resolves enabled plugins in config order and validates their options. */
|
|
4
17
|
export const activatePlugins = async (env, warnings) => {
|
|
5
18
|
const active = [];
|
|
@@ -11,6 +24,9 @@ export const activatePlugins = async (env, warnings) => {
|
|
|
11
24
|
});
|
|
12
25
|
}
|
|
13
26
|
assertPlugin(plugin);
|
|
27
|
+
if (plugin.name === CORE_PLUGIN) {
|
|
28
|
+
throw new TallyError("E_PLUGIN_NAME", `plugin name "${CORE_PLUGIN}" is reserved for core built-ins`, { plugin: name });
|
|
29
|
+
}
|
|
14
30
|
const { globalInputs, ...rest } = raw ?? {};
|
|
15
31
|
let options = rest;
|
|
16
32
|
if (plugin.options) {
|
|
@@ -21,17 +37,7 @@ export const activatePlugins = async (env, warnings) => {
|
|
|
21
37
|
}
|
|
22
38
|
options = result.value;
|
|
23
39
|
}
|
|
24
|
-
const ctx =
|
|
25
|
-
options,
|
|
26
|
-
repo: Object.freeze({ name: env.repo.name }),
|
|
27
|
-
fs: env.fs,
|
|
28
|
-
log: Object.freeze({
|
|
29
|
-
warn: (message) => {
|
|
30
|
-
warnings.push({ code: "W_PLUGIN", message, plugin: name });
|
|
31
|
-
},
|
|
32
|
-
debug: (message) => env.debug?.(`${name}: ${message}`),
|
|
33
|
-
}),
|
|
34
|
-
});
|
|
40
|
+
const ctx = pluginCtx(env, warnings)(name, options);
|
|
35
41
|
active.push({ plugin, ctx, globalInputs: globalInputs !== false });
|
|
36
42
|
}
|
|
37
43
|
return active;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import type { Graph } from "./graph.js";
|
|
2
|
+
import type { BumpLevel, Unit } from "./plugin.js";
|
|
3
|
+
/** Bump state of a released unit. */
|
|
4
|
+
export interface Pending {
|
|
5
|
+
readonly bump: BumpLevel | null;
|
|
6
|
+
/** Has a bump or a Release-As value. */
|
|
7
|
+
readonly pending: boolean;
|
|
8
|
+
/** Fixed group name. */
|
|
9
|
+
readonly group?: string;
|
|
10
|
+
}
|
|
11
|
+
export interface Propagated extends Pending {
|
|
12
|
+
/** Sorted. */
|
|
13
|
+
readonly propagatedFrom: readonly string[];
|
|
14
|
+
}
|
|
15
|
+
/**
|
|
16
|
+
* Propagation and group fixpoint over released units in graph order. For a
|
|
17
|
+
* pending `from`, the dependent's policy (`auto` = pinned ? patch : rebuild)
|
|
18
|
+
* decides: `patch` raises it to at least patch, `rebuild` changes nothing. A
|
|
19
|
+
* group with a pending member makes every member pending at the members' max
|
|
20
|
+
* bump. Terminates because bumps only increase.
|
|
21
|
+
*/
|
|
22
|
+
export declare const propagate: (graph: Pick<Graph, "edges" | "order">, units: readonly Pick<Unit, "id" | "propagate">[], states: ReadonlyMap<string, Pending>) => Map<string, Propagated>;
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { maxLevel, uniqueSorted } from "./util.js";
|
|
2
|
+
/**
|
|
3
|
+
* Edges between released units, `to` to `from` to `pinned`. A path through
|
|
4
|
+
* unreleased units counts as an edge with the first hop's `pinned`; several
|
|
5
|
+
* paths are OR-combined.
|
|
6
|
+
*/
|
|
7
|
+
const releasedEdges = (edges, released) => {
|
|
8
|
+
const out = new Map();
|
|
9
|
+
for (const e of edges) {
|
|
10
|
+
const list = out.get(e.from) ?? [];
|
|
11
|
+
list.push(e);
|
|
12
|
+
out.set(e.from, list);
|
|
13
|
+
}
|
|
14
|
+
const result = new Map();
|
|
15
|
+
for (const from of released) {
|
|
16
|
+
const seen = new Set();
|
|
17
|
+
const queue = (out.get(from) ?? []).map((e) => ({
|
|
18
|
+
to: e.to,
|
|
19
|
+
pinned: e.pinned,
|
|
20
|
+
}));
|
|
21
|
+
for (const { to, pinned } of queue) {
|
|
22
|
+
const key = `${to}\0${String(pinned)}`;
|
|
23
|
+
if (to === from || seen.has(key)) {
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
seen.add(key);
|
|
27
|
+
if (released.has(to)) {
|
|
28
|
+
const incoming = result.get(to) ?? new Map();
|
|
29
|
+
incoming.set(from, (incoming.get(from) ?? false) || pinned);
|
|
30
|
+
result.set(to, incoming);
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
queue.push(...(out.get(to) ?? []).map((e) => ({ to: e.to, pinned })));
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return result;
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Propagation and group fixpoint over released units in graph order. For a
|
|
41
|
+
* pending `from`, the dependent's policy (`auto` = pinned ? patch : rebuild)
|
|
42
|
+
* decides: `patch` raises it to at least patch, `rebuild` changes nothing. A
|
|
43
|
+
* group with a pending member makes every member pending at the members' max
|
|
44
|
+
* bump. Terminates because bumps only increase.
|
|
45
|
+
*/
|
|
46
|
+
export const propagate = (graph, units, states) => {
|
|
47
|
+
const policies = new Map(units.map((u) => [u.id, u.propagate]));
|
|
48
|
+
const groups = new Map();
|
|
49
|
+
for (const [id, { group }] of states) {
|
|
50
|
+
if (group !== undefined) {
|
|
51
|
+
groups.set(group, [...(groups.get(group) ?? []), id]);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const edges = releasedEdges(graph.edges, new Set(states.keys()));
|
|
55
|
+
const result = new Map([...states].map(([id, s]) => [
|
|
56
|
+
id,
|
|
57
|
+
{ bump: s.bump, pending: s.pending, propagatedFrom: new Set() },
|
|
58
|
+
]));
|
|
59
|
+
let changed = true;
|
|
60
|
+
while (changed) {
|
|
61
|
+
changed = false;
|
|
62
|
+
for (const id of graph.order) {
|
|
63
|
+
const state = result.get(id);
|
|
64
|
+
for (const [from, pinned] of edges.get(id) ?? []) {
|
|
65
|
+
const policy = policies.get(id) ?? "auto";
|
|
66
|
+
const patch = policy === "auto" ? pinned : policy === "patch";
|
|
67
|
+
if (!state ||
|
|
68
|
+
!patch ||
|
|
69
|
+
!result.get(from)?.pending ||
|
|
70
|
+
state.propagatedFrom.has(from)) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
state.propagatedFrom.add(from);
|
|
74
|
+
state.bump = maxLevel(state.bump, "patch");
|
|
75
|
+
state.pending = true;
|
|
76
|
+
changed = true;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
for (const members of groups.values()) {
|
|
80
|
+
const raised = members.flatMap((id) => result.get(id) ?? []);
|
|
81
|
+
if (!raised.some((s) => s.pending)) {
|
|
82
|
+
continue;
|
|
83
|
+
}
|
|
84
|
+
const bump = raised.reduce((level, s) => maxLevel(level, s.bump), null);
|
|
85
|
+
for (const s of raised) {
|
|
86
|
+
if (!s.pending || s.bump !== bump) {
|
|
87
|
+
s.pending = true;
|
|
88
|
+
s.bump = bump;
|
|
89
|
+
changed = true;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
return new Map([...result].map(([id, s]) => [
|
|
95
|
+
id,
|
|
96
|
+
{ ...s, propagatedFrom: uniqueSorted(s.propagatedFrom) },
|
|
97
|
+
]));
|
|
98
|
+
};
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import type { TallyConfig } from "./config.js";
|
|
2
|
+
import type { Warning } from "./errors.js";
|
|
3
|
+
import type { GroupFacts, RangeFacts, UnitFacts } from "./facts.js";
|
|
4
|
+
import type { BumpLevel, Change, Unit, VersionScheme } from "./plugin.js";
|
|
5
|
+
/**
|
|
6
|
+
* Level of one change. While the major is 0, breaking changes use
|
|
7
|
+
* `preMajor.breaking` and changes whose type level is minor `preMajor.feat`.
|
|
8
|
+
* An absorbed change contributes `bumps.absorbed` when its own level is set
|
|
9
|
+
* (`minor` caps it at minor).
|
|
10
|
+
*/
|
|
11
|
+
declare const changeLevel: ({ change, via }: UnitChange, settings: ReleaseSettings, preMajor: boolean) => BumpLevel | null;
|
|
12
|
+
/** The Release-As footer value that applies to a unit. */
|
|
13
|
+
export interface ReleaseAsWinner<V> {
|
|
14
|
+
readonly sha?: string;
|
|
15
|
+
/** As written in the footer. */
|
|
16
|
+
readonly raw: string;
|
|
17
|
+
readonly version: V;
|
|
18
|
+
}
|
|
19
|
+
/** A normalized branch rule. */
|
|
20
|
+
export interface BranchRule {
|
|
21
|
+
readonly name: string;
|
|
22
|
+
readonly line: "default" | "maintenance";
|
|
23
|
+
readonly channel: string | null;
|
|
24
|
+
/** Release item of the absolute maintenance check. */
|
|
25
|
+
readonly unit: string | null;
|
|
26
|
+
readonly group: string | null;
|
|
27
|
+
}
|
|
28
|
+
/** Release config with defaults applied. */
|
|
29
|
+
export interface ReleaseSettings {
|
|
30
|
+
/** Commit type to level. */
|
|
31
|
+
readonly bumps: ReadonlyMap<string, BumpLevel>;
|
|
32
|
+
/** Levels used while `core(from).major == 0`. */
|
|
33
|
+
readonly preMajor: {
|
|
34
|
+
readonly breaking: BumpLevel;
|
|
35
|
+
readonly feat: BumpLevel;
|
|
36
|
+
};
|
|
37
|
+
/** Level cap of absorbed changes with a bump. */
|
|
38
|
+
readonly absorbed: "patch" | "minor";
|
|
39
|
+
/** In config order. */
|
|
40
|
+
readonly branches: readonly BranchRule[];
|
|
41
|
+
}
|
|
42
|
+
/**
|
|
43
|
+
* A Change of a released unit; `via` names the first absorbed unit (by id) a
|
|
44
|
+
* change not attributed to the unit came through.
|
|
45
|
+
*/
|
|
46
|
+
export interface UnitChange {
|
|
47
|
+
readonly change: Change;
|
|
48
|
+
readonly via?: string;
|
|
49
|
+
}
|
|
50
|
+
/** Bump inputs of a released unit, before propagation. */
|
|
51
|
+
export interface UnitIntent<V> {
|
|
52
|
+
/** Own and absorbed level. */
|
|
53
|
+
readonly bump: BumpLevel | null;
|
|
54
|
+
readonly releaseAs: ReleaseAsWinner<V> | undefined;
|
|
55
|
+
/** Core commit order. */
|
|
56
|
+
readonly changes: readonly UnitChange[];
|
|
57
|
+
/** Absorbed units of the changes with `via`, sorted. */
|
|
58
|
+
readonly absorbedFrom: readonly string[];
|
|
59
|
+
/** Release-As value per commit sha, for the plan's change list. */
|
|
60
|
+
readonly releaseAsBySha: ReadonlyMap<string, string>;
|
|
61
|
+
}
|
|
62
|
+
export interface UnitRelease {
|
|
63
|
+
readonly from: string | null;
|
|
64
|
+
readonly bump: BumpLevel | null;
|
|
65
|
+
readonly releaseAs: string | null;
|
|
66
|
+
/** The footer that set `releaseAs`. */
|
|
67
|
+
readonly releaseAsFooter: {
|
|
68
|
+
readonly sha?: string;
|
|
69
|
+
readonly raw: string;
|
|
70
|
+
} | null;
|
|
71
|
+
/** Null without a pending bump or Release-As. */
|
|
72
|
+
readonly next: string | null;
|
|
73
|
+
/** What set `next`. */
|
|
74
|
+
readonly nextSource: "bump" | "initial" | "release-as" | null;
|
|
75
|
+
/** Prerelease of `next` on `channel`; null without a channel or `next`. */
|
|
76
|
+
readonly prerelease: string | null;
|
|
77
|
+
}
|
|
78
|
+
export interface ReleaseInput<V> {
|
|
79
|
+
readonly unit: Unit;
|
|
80
|
+
readonly scheme: VersionScheme<V>;
|
|
81
|
+
readonly settings: ReleaseSettings;
|
|
82
|
+
readonly facts: UnitFacts;
|
|
83
|
+
/** The range of `facts.from`. */
|
|
84
|
+
readonly range: RangeFacts;
|
|
85
|
+
/** Unreleased units absorbed by the unit (`reach`). */
|
|
86
|
+
readonly absorbed: ReadonlySet<string>;
|
|
87
|
+
readonly head: string;
|
|
88
|
+
readonly channel: string | null;
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Unreleased units reachable over `id`'s dependencies, never stepping into or
|
|
92
|
+
* through a released unit.
|
|
93
|
+
*/
|
|
94
|
+
export declare const reach: (id: string, dependencies: ReadonlyMap<string, readonly string[]>, released: ReadonlySet<string>) => Set<string>;
|
|
95
|
+
/** Applies the defaults to `bumps` and `branches`. */
|
|
96
|
+
export declare const releaseSettings: (config: TallyConfig) => ReleaseSettings;
|
|
97
|
+
/**
|
|
98
|
+
* Warns for qualified Release-As values naming unknown or unreleased units and
|
|
99
|
+
* for plain values on commits attributed to no released unit.
|
|
100
|
+
*/
|
|
101
|
+
export declare const releaseAsWarnings: (units: readonly Unit[], ranges: readonly RangeFacts[]) => Warning[];
|
|
102
|
+
/**
|
|
103
|
+
* Own and absorbed bump and Release-As of a released unit. Release-As values
|
|
104
|
+
* are read newest first (core commit order); a single leading `v` is
|
|
105
|
+
* stripped, as release-please does. Footers are not absorbed.
|
|
106
|
+
*/
|
|
107
|
+
export declare const unitIntent: <V>(input: ReleaseInput<V>, warnings: Warning[]) => UnitIntent<V>;
|
|
108
|
+
/** The recorded footer of a Release-As winner. */
|
|
109
|
+
export declare const footerOf: <V>(winner: ReleaseAsWinner<V> | undefined) => UnitRelease["releaseAsFooter"];
|
|
110
|
+
/**
|
|
111
|
+
* `next` and prerelease of a released unit from its bump after propagation;
|
|
112
|
+
* Release-As overrides `next`.
|
|
113
|
+
*/
|
|
114
|
+
export declare const releaseUnit: <V>(input: ReleaseInput<V>, intent: {
|
|
115
|
+
bump: BumpLevel | null;
|
|
116
|
+
releaseAs: ReleaseAsWinner<V> | undefined;
|
|
117
|
+
}, warnings: Warning[]) => UnitRelease;
|
|
118
|
+
/** A fixed group member; `input.facts.fromTag` is the group base tag. */
|
|
119
|
+
export interface GroupMember<V> {
|
|
120
|
+
readonly input: ReleaseInput<V>;
|
|
121
|
+
/** After propagation and group raising. */
|
|
122
|
+
readonly bump: BumpLevel | null;
|
|
123
|
+
readonly releaseAs: ReleaseAsWinner<V> | undefined;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Release of a fixed group with members sorted by id:
|
|
127
|
+
* `next = max(bump(groupFrom, L), max_i releaseAs_i)`, the initial version
|
|
128
|
+
* (max over members) instead of the bump on a first release. One `PreCtx`:
|
|
129
|
+
* first member, group base tag, union of member tags, group line base
|
|
130
|
+
* counter, max total.
|
|
131
|
+
*/
|
|
132
|
+
export declare const releaseGroup: <V>(group: GroupFacts, members: readonly GroupMember<V>[], warnings: Warning[]) => Omit<UnitRelease, "releaseAsFooter">;
|
|
133
|
+
export { changeLevel };
|