@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/explain.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { attribution, matcher } from "./attribution.js";
|
|
2
|
+
import { TallyError } from "./errors.js";
|
|
3
|
+
import { planWithFacts } from "./plan.js";
|
|
4
|
+
/** Explains one unit of the plan of `opts.head`; throws `E_UNIT_UNKNOWN`. */
|
|
5
|
+
export const explainUnit = async (workspace, git, { unit: id, ...opts }) => {
|
|
6
|
+
if (!workspace.units.some((u) => u.id === id)) {
|
|
7
|
+
throw new TallyError("E_UNIT_UNKNOWN", `unknown unit "${id}"`, {
|
|
8
|
+
unit: id,
|
|
9
|
+
});
|
|
10
|
+
}
|
|
11
|
+
const { plan, facts, releases } = await planWithFacts(workspace, git, {
|
|
12
|
+
...opts,
|
|
13
|
+
all: true,
|
|
14
|
+
});
|
|
15
|
+
const unit = plan.units.find((u) => u.id === id) ?? null;
|
|
16
|
+
const ignored = matcher(workspace.ignore);
|
|
17
|
+
const isGlobal = matcher(workspace.globalInputs);
|
|
18
|
+
const owners = attribution(workspace.units);
|
|
19
|
+
const owned = (files, owner) => files.filter((f) => !ignored(f) && !isGlobal(f) && owners(f).includes(owner));
|
|
20
|
+
const { base } = plan.build;
|
|
21
|
+
const reason = unit?.affected?.reason;
|
|
22
|
+
const files = reason === "global"
|
|
23
|
+
? plan.build.globalInputs
|
|
24
|
+
: reason === "direct" && base !== null
|
|
25
|
+
? owned(await git.diffFiles(base, opts.head), id)
|
|
26
|
+
: [];
|
|
27
|
+
const unitFacts = facts.units.find((f) => f.id === id);
|
|
28
|
+
const commitFiles = new Map(unitFacts
|
|
29
|
+
? (await git.log(unitFacts.from, opts.head)).map((c) => [c.sha, c.files])
|
|
30
|
+
: []);
|
|
31
|
+
const changes = (unit?.changes ?? []).map((c) => ({
|
|
32
|
+
...c,
|
|
33
|
+
files: owned(commitFiles.get(c.sha ?? "") ?? [], c.via ?? id),
|
|
34
|
+
}));
|
|
35
|
+
const propagatedFrom = (unit?.propagatedFrom ?? []).map((from) => ({
|
|
36
|
+
id: from,
|
|
37
|
+
pinned: workspace.graph.edges.find((e) => e.from === from && e.to === id)
|
|
38
|
+
?.pinned ?? null,
|
|
39
|
+
}));
|
|
40
|
+
const range = facts.ranges.find((r) => r.from === unitFacts?.from);
|
|
41
|
+
const footers = (unitFacts ? (range?.changes ?? []) : []).flatMap(({ change, units }) => (change.releaseAs ?? [])
|
|
42
|
+
.filter((r) => r.unit === id || (r.unit === undefined && units.includes(id)))
|
|
43
|
+
.map((r) => ({
|
|
44
|
+
...(change.sha === undefined ? {} : { sha: change.sha }),
|
|
45
|
+
version: r.version,
|
|
46
|
+
qualified: r.unit !== undefined,
|
|
47
|
+
})));
|
|
48
|
+
const winner = releases.get(id)?.releaseAsFooter;
|
|
49
|
+
const applied = footers.findIndex((f) => winner && f.sha === winner.sha && f.version === winner.raw);
|
|
50
|
+
const group = workspace.groups.find((g) => g.members.includes(id));
|
|
51
|
+
return {
|
|
52
|
+
plan,
|
|
53
|
+
unit,
|
|
54
|
+
group: group ? { name: group.name, members: [...group.members] } : null,
|
|
55
|
+
nextSource: releases.get(id)?.nextSource ?? null,
|
|
56
|
+
files,
|
|
57
|
+
changes,
|
|
58
|
+
propagatedFrom,
|
|
59
|
+
releaseAs: footers.map((f, i) => ({ ...f, applied: i === applied })),
|
|
60
|
+
warnings: plan.warnings.filter((w) => w.unit === id || w.unit === undefined),
|
|
61
|
+
};
|
|
62
|
+
};
|
package/dist/facts.d.ts
ADDED
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
import type { Warning } from "./errors.js";
|
|
2
|
+
import type { Change, GitReader, ReadonlyFs } from "./plugin.js";
|
|
3
|
+
import type { UnitTag } from "./tags.js";
|
|
4
|
+
import type { Workspace } from "./workspace.js";
|
|
5
|
+
export interface FactsOptions {
|
|
6
|
+
head: string;
|
|
7
|
+
fs: ReadonlyFs;
|
|
8
|
+
debug?: (message: string) => void;
|
|
9
|
+
}
|
|
10
|
+
/** A Change with the ids of the units it is attributed to. */
|
|
11
|
+
export interface AttributedChange {
|
|
12
|
+
readonly change: Change;
|
|
13
|
+
/** Sorted. */
|
|
14
|
+
readonly units: readonly string[];
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Changes of `(from, head]`, in core commit order, attributed to all units. A
|
|
18
|
+
* unit's consumers read only its own range (`UnitFacts.from`), filtered to the
|
|
19
|
+
* unit and the units it absorbs.
|
|
20
|
+
*/
|
|
21
|
+
export interface RangeFacts {
|
|
22
|
+
readonly from: string | null;
|
|
23
|
+
readonly changes: readonly AttributedChange[];
|
|
24
|
+
}
|
|
25
|
+
export interface UnitFacts {
|
|
26
|
+
readonly id: string;
|
|
27
|
+
/** Fixed group name. */
|
|
28
|
+
readonly group: string | null;
|
|
29
|
+
/**
|
|
30
|
+
* Range key, null from the root: the base tag commit; for a group member
|
|
31
|
+
* its own base tag when that descends from the group base tag, else the
|
|
32
|
+
* group base tag.
|
|
33
|
+
*/
|
|
34
|
+
readonly from: string | null;
|
|
35
|
+
/** Every matched tag, reachable or not, scheme-sorted. */
|
|
36
|
+
readonly tags: readonly UnitTag[];
|
|
37
|
+
/** Highest stable tag merged into head. */
|
|
38
|
+
readonly baseTag: UnitTag | null;
|
|
39
|
+
/** Source of the `from` version: the group base tag for members, else `baseTag`. */
|
|
40
|
+
readonly fromTag: UnitTag | null;
|
|
41
|
+
/** Highest stable tag on head's first-parent chain. */
|
|
42
|
+
readonly lineBaseTag: UnitTag | null;
|
|
43
|
+
readonly sinceLineBase: number;
|
|
44
|
+
readonly total: number;
|
|
45
|
+
/** Changes since the base tag attributed to this unit, core commit order. */
|
|
46
|
+
readonly changes: readonly AttributedChange[];
|
|
47
|
+
/** Qualified Release-As values naming this unit, core commit order. */
|
|
48
|
+
readonly releaseAs: readonly {
|
|
49
|
+
sha?: string;
|
|
50
|
+
version: string;
|
|
51
|
+
}[];
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* A fixed group. Base and line base are the highest member tags as for a
|
|
55
|
+
* unit; equal versions prefer the tag whose commit descends from the other's.
|
|
56
|
+
*/
|
|
57
|
+
export interface GroupFacts {
|
|
58
|
+
readonly name: string;
|
|
59
|
+
readonly members: readonly string[];
|
|
60
|
+
/** Highest stable member tag merged into head. */
|
|
61
|
+
readonly baseTag: UnitTag | null;
|
|
62
|
+
/** Highest stable member tag on head's first-parent chain. */
|
|
63
|
+
readonly lineBaseTag: UnitTag | null;
|
|
64
|
+
readonly sinceLineBase: number;
|
|
65
|
+
}
|
|
66
|
+
/** Git and intent data of every released unit, collected before release math. */
|
|
67
|
+
export interface PlanFacts {
|
|
68
|
+
readonly head: string;
|
|
69
|
+
/** Released units, sorted by id. */
|
|
70
|
+
readonly units: readonly UnitFacts[];
|
|
71
|
+
/** One entry per distinct base tag commit. */
|
|
72
|
+
readonly ranges: readonly RangeFacts[];
|
|
73
|
+
/** Sorted by name. */
|
|
74
|
+
readonly groups: readonly GroupFacts[];
|
|
75
|
+
/** Every tag name, sorted. */
|
|
76
|
+
readonly tagNames: readonly string[];
|
|
77
|
+
/** Warnings logged by intent sources and for unknown explicit units. */
|
|
78
|
+
readonly warnings: readonly Warning[];
|
|
79
|
+
}
|
|
80
|
+
/** Runs every `GitReader` and intent source call a plan needs. */
|
|
81
|
+
export declare const collectPlanFacts: (workspace: Workspace, git: GitReader, opts: FactsOptions) => Promise<PlanFacts>;
|
package/dist/facts.js
ADDED
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
import { attribution, matcher } from "./attribution.js";
|
|
2
|
+
import { TallyError } from "./errors.js";
|
|
3
|
+
import { pluginCtx, runHook } from "./plugins.js";
|
|
4
|
+
import { schemeRegistry } from "./schemes.js";
|
|
5
|
+
import { matchUnitTags } from "./tags.js";
|
|
6
|
+
import { compare, uniqueSorted } from "./util.js";
|
|
7
|
+
const instant = (commit) => {
|
|
8
|
+
const time = Date.parse(commit.date);
|
|
9
|
+
if (Number.isNaN(time)) {
|
|
10
|
+
throw new TallyError("E_GIT_DATE", `commit ${commit.sha} has an invalid committer date "${commit.date}"`, { sha: commit.sha, date: commit.date });
|
|
11
|
+
}
|
|
12
|
+
return time;
|
|
13
|
+
};
|
|
14
|
+
/** Committer date descending, compared as instants, then sha. */
|
|
15
|
+
const byCommitOrder = (a, b) => instant(b) - instant(a) || compare(a.sha, b.sha);
|
|
16
|
+
const releaseAsKey = (r) => `${r.unit ?? ""}@${r.version}`;
|
|
17
|
+
/** Keeps the first commit-derived Change per dedupe key, unioning `releaseAs`. */
|
|
18
|
+
const dedupe = (changes) => {
|
|
19
|
+
const kept = [];
|
|
20
|
+
const index = new Map();
|
|
21
|
+
for (const change of changes) {
|
|
22
|
+
const key = change.sha === undefined
|
|
23
|
+
? undefined
|
|
24
|
+
: [
|
|
25
|
+
change.sha,
|
|
26
|
+
change.type,
|
|
27
|
+
String(change.breaking),
|
|
28
|
+
uniqueSorted(change.units ?? []).join(","),
|
|
29
|
+
].join("\0");
|
|
30
|
+
const at = key === undefined ? undefined : index.get(key);
|
|
31
|
+
const prior = at === undefined ? undefined : kept[at];
|
|
32
|
+
if (at === undefined || !prior) {
|
|
33
|
+
if (key !== undefined) {
|
|
34
|
+
index.set(key, kept.length);
|
|
35
|
+
}
|
|
36
|
+
kept.push(change);
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (change.releaseAs) {
|
|
40
|
+
const values = new Map([...(prior.releaseAs ?? []), ...change.releaseAs].map((r) => [
|
|
41
|
+
releaseAsKey(r),
|
|
42
|
+
r,
|
|
43
|
+
]));
|
|
44
|
+
kept[at] = { ...prior, releaseAs: [...values.values()] };
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return kept;
|
|
48
|
+
};
|
|
49
|
+
/** Runs every `GitReader` and intent source call a plan needs. */
|
|
50
|
+
export const collectPlanFacts = async (workspace, git, opts) => {
|
|
51
|
+
const { head } = opts;
|
|
52
|
+
const warnings = [];
|
|
53
|
+
const ctx = pluginCtx({ ...opts, repo: { name: workspace.repo } }, warnings);
|
|
54
|
+
const intents = workspace.intents.map((entry) => ({
|
|
55
|
+
...entry,
|
|
56
|
+
ctx: ctx(entry.plugin, entry.options),
|
|
57
|
+
}));
|
|
58
|
+
const schemes = schemeRegistry(workspace.schemes);
|
|
59
|
+
const allTags = await git.tags();
|
|
60
|
+
const tags = matchUnitTags(workspace.units, allTags, schemes);
|
|
61
|
+
const merged = new Set((await git.tagsMerged(head)).map((t) => t.name));
|
|
62
|
+
const total = await git.countFirstParent(null, head);
|
|
63
|
+
const ignored = matcher(workspace.ignore);
|
|
64
|
+
const isGlobal = matcher(workspace.globalInputs);
|
|
65
|
+
const owners = attribution(workspace.units);
|
|
66
|
+
const known = new Set(workspace.units.map((u) => u.id));
|
|
67
|
+
// File-less commits belong to every released unit (release-please parity).
|
|
68
|
+
const released = workspace.units
|
|
69
|
+
.filter((u) => u.version !== "none")
|
|
70
|
+
.map((u) => u.id);
|
|
71
|
+
const warned = new Set();
|
|
72
|
+
const warnUnknown = (plugin, changes) => {
|
|
73
|
+
for (const id of changes.flatMap((c) => c.units ?? [])) {
|
|
74
|
+
const key = `${plugin}\0${id}`;
|
|
75
|
+
if (!known.has(id) && !warned.has(key)) {
|
|
76
|
+
warned.add(key);
|
|
77
|
+
warnings.push({
|
|
78
|
+
code: "W_UNIT_UNKNOWN",
|
|
79
|
+
message: `intent names unknown unit "${id}"`,
|
|
80
|
+
plugin,
|
|
81
|
+
});
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
};
|
|
85
|
+
const counts = new Map();
|
|
86
|
+
const countSince = async (sha) => {
|
|
87
|
+
const cached = counts.get(sha);
|
|
88
|
+
if (cached !== undefined) {
|
|
89
|
+
return cached;
|
|
90
|
+
}
|
|
91
|
+
const count = await git.countFirstParent(sha, head);
|
|
92
|
+
counts.set(sha, count);
|
|
93
|
+
return count;
|
|
94
|
+
};
|
|
95
|
+
const ranges = new Map();
|
|
96
|
+
const range = async (from) => {
|
|
97
|
+
const cached = ranges.get(from);
|
|
98
|
+
if (cached) {
|
|
99
|
+
return cached;
|
|
100
|
+
}
|
|
101
|
+
const commits = Object.freeze([...(await git.log(from, head))].sort(byCommitOrder));
|
|
102
|
+
const position = new Map(commits.map((c, i) => [c.sha, i]));
|
|
103
|
+
const commitUnits = new Map(commits.map((c) => [
|
|
104
|
+
c.sha,
|
|
105
|
+
c.files.length === 0
|
|
106
|
+
? released
|
|
107
|
+
: uniqueSorted(c.files
|
|
108
|
+
.filter((f) => !ignored(f) && !isGlobal(f))
|
|
109
|
+
.flatMap(owners)),
|
|
110
|
+
]));
|
|
111
|
+
const changes = [];
|
|
112
|
+
for (const { plugin, source, ctx: sourceCtx } of intents) {
|
|
113
|
+
// eslint-disable-next-line no-await-in-loop -- hooks run sequentially in config order
|
|
114
|
+
const collected = await runHook(plugin, "intent", () => source.collect(sourceCtx, { from, to: head, commits }));
|
|
115
|
+
warnUnknown(plugin, collected);
|
|
116
|
+
changes.push(...collected);
|
|
117
|
+
}
|
|
118
|
+
const attributed = dedupe(changes)
|
|
119
|
+
.map((change) => ({
|
|
120
|
+
change,
|
|
121
|
+
units: change.units
|
|
122
|
+
? uniqueSorted(change.units.filter((id) => known.has(id)))
|
|
123
|
+
: (commitUnits.get(change.sha ?? "") ?? []),
|
|
124
|
+
}))
|
|
125
|
+
.sort((a, b) => (position.get(a.change.sha ?? "") ?? commits.length) -
|
|
126
|
+
(position.get(b.change.sha ?? "") ?? commits.length));
|
|
127
|
+
const result = { from, changes: attributed };
|
|
128
|
+
ranges.set(from, result);
|
|
129
|
+
return result;
|
|
130
|
+
};
|
|
131
|
+
const ancestry = new Map();
|
|
132
|
+
const isAncestor = async (a, b) => {
|
|
133
|
+
const key = `${a}\0${b}`;
|
|
134
|
+
const cached = ancestry.get(key);
|
|
135
|
+
if (cached !== undefined) {
|
|
136
|
+
return cached;
|
|
137
|
+
}
|
|
138
|
+
const result = await git.isAncestor(a, b);
|
|
139
|
+
ancestry.set(key, result);
|
|
140
|
+
return result;
|
|
141
|
+
};
|
|
142
|
+
/** The higher tag; equal versions prefer the tag descending from the other. */
|
|
143
|
+
const higher = async (scheme, a, b) => {
|
|
144
|
+
if (a === null || b === null) {
|
|
145
|
+
return a ?? b;
|
|
146
|
+
}
|
|
147
|
+
const x = scheme.parse(a.version);
|
|
148
|
+
const y = scheme.parse(b.version);
|
|
149
|
+
// Members with another scheme fail validateRelease (E_GROUP_SCHEME).
|
|
150
|
+
if (x === undefined || y === undefined) {
|
|
151
|
+
return a;
|
|
152
|
+
}
|
|
153
|
+
const order = scheme.compare(y, x);
|
|
154
|
+
if (order !== 0) {
|
|
155
|
+
return order > 0 ? b : a;
|
|
156
|
+
}
|
|
157
|
+
return a.sha !== b.sha && (await isAncestor(a.sha, b.sha)) ? b : a;
|
|
158
|
+
};
|
|
159
|
+
const bases = new Map();
|
|
160
|
+
for (const unit of workspace.units) {
|
|
161
|
+
const matched = tags.get(unit.id);
|
|
162
|
+
if (!matched) {
|
|
163
|
+
// Unreleased.
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const { scheme } = matched;
|
|
167
|
+
const stable = matched.tags.filter((t) => merged.has(t.name) && !scheme.isPrerelease(scheme.parse(t.version)));
|
|
168
|
+
let lineBaseTag = null;
|
|
169
|
+
for (const tag of [...stable].reverse()) {
|
|
170
|
+
// eslint-disable-next-line no-await-in-loop -- stops at the first match
|
|
171
|
+
if (await git.isFirstParentAncestor(tag.sha, head)) {
|
|
172
|
+
lineBaseTag = tag;
|
|
173
|
+
break;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
bases.set(unit.id, { scheme, baseTag: stable.at(-1) ?? null, lineBaseTag });
|
|
177
|
+
}
|
|
178
|
+
const sinceLine = async (tag) => tag ? await countSince(tag.sha) : total;
|
|
179
|
+
const groups = [];
|
|
180
|
+
for (const { name, members } of workspace.groups) {
|
|
181
|
+
let baseTag = null;
|
|
182
|
+
let lineBaseTag = null;
|
|
183
|
+
for (const id of members) {
|
|
184
|
+
const own = bases.get(id);
|
|
185
|
+
if (own) {
|
|
186
|
+
// eslint-disable-next-line no-await-in-loop -- memoized ancestry
|
|
187
|
+
baseTag = await higher(own.scheme, baseTag, own.baseTag);
|
|
188
|
+
// eslint-disable-next-line no-await-in-loop -- memoized ancestry
|
|
189
|
+
lineBaseTag = await higher(own.scheme, lineBaseTag, own.lineBaseTag);
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
groups.push({
|
|
193
|
+
name,
|
|
194
|
+
members,
|
|
195
|
+
baseTag,
|
|
196
|
+
lineBaseTag,
|
|
197
|
+
// eslint-disable-next-line no-await-in-loop -- memoized per line base
|
|
198
|
+
sinceLineBase: await sinceLine(lineBaseTag),
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
const groupOf = new Map(groups.flatMap((g) => g.members.map((id) => [id, g])));
|
|
202
|
+
const units = [];
|
|
203
|
+
for (const unit of workspace.units) {
|
|
204
|
+
const matched = tags.get(unit.id);
|
|
205
|
+
const own = bases.get(unit.id);
|
|
206
|
+
if (!matched || !own) {
|
|
207
|
+
continue;
|
|
208
|
+
}
|
|
209
|
+
const { baseTag, lineBaseTag } = own;
|
|
210
|
+
const group = groupOf.get(unit.id);
|
|
211
|
+
const fromTag = group ? group.baseTag : baseTag;
|
|
212
|
+
const rangeBase = group &&
|
|
213
|
+
baseTag &&
|
|
214
|
+
fromTag &&
|
|
215
|
+
baseTag.sha !== fromTag.sha &&
|
|
216
|
+
// eslint-disable-next-line no-await-in-loop -- memoized ancestry
|
|
217
|
+
(await isAncestor(fromTag.sha, baseTag.sha))
|
|
218
|
+
? baseTag
|
|
219
|
+
: fromTag;
|
|
220
|
+
// eslint-disable-next-line no-await-in-loop -- intent sources run sequentially
|
|
221
|
+
const { from, changes } = await range(rangeBase?.sha ?? null);
|
|
222
|
+
units.push({
|
|
223
|
+
id: unit.id,
|
|
224
|
+
group: group?.name ?? null,
|
|
225
|
+
from,
|
|
226
|
+
tags: matched.tags,
|
|
227
|
+
baseTag,
|
|
228
|
+
fromTag,
|
|
229
|
+
lineBaseTag,
|
|
230
|
+
// eslint-disable-next-line no-await-in-loop -- memoized per line base
|
|
231
|
+
sinceLineBase: await sinceLine(lineBaseTag),
|
|
232
|
+
total,
|
|
233
|
+
changes: changes.filter((c) => c.units.includes(unit.id)),
|
|
234
|
+
releaseAs: changes.flatMap(({ change }) => (change.releaseAs ?? [])
|
|
235
|
+
.filter((r) => r.unit === unit.id)
|
|
236
|
+
.map((r) => ({
|
|
237
|
+
...(change.sha === undefined ? {} : { sha: change.sha }),
|
|
238
|
+
version: r.version,
|
|
239
|
+
}))),
|
|
240
|
+
});
|
|
241
|
+
}
|
|
242
|
+
return {
|
|
243
|
+
head,
|
|
244
|
+
units,
|
|
245
|
+
ranges: [...ranges.values()],
|
|
246
|
+
groups,
|
|
247
|
+
tagNames: uniqueSorted(allTags.map((t) => t.name)),
|
|
248
|
+
warnings,
|
|
249
|
+
};
|
|
250
|
+
};
|
package/dist/groups.d.ts
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { GroupConfig } from "./config.js";
|
|
2
|
+
import type { Warning } from "./errors.js";
|
|
3
|
+
import type { Unit } from "./plugin.js";
|
|
4
|
+
/** A fixed group: its members release together with one version. */
|
|
5
|
+
export interface Group {
|
|
6
|
+
readonly name: string;
|
|
7
|
+
/** Sorted unit ids. */
|
|
8
|
+
readonly members: readonly string[];
|
|
9
|
+
}
|
|
10
|
+
/**
|
|
11
|
+
* Resolves group members: `units` by id, `tags` to released units with any of
|
|
12
|
+
* the tags. Throws `E_GROUP_UNKNOWN_UNIT` and `E_GROUP_OVERLAP`; a group
|
|
13
|
+
* selecting nothing warns `W_GROUP_EMPTY` and is dropped.
|
|
14
|
+
*/
|
|
15
|
+
export declare const resolveGroups: (units: readonly Unit[], config: Readonly<Record<string, GroupConfig>> | undefined, warnings: Warning[]) => Group[];
|
package/dist/groups.js
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { TallyError } from "./errors.js";
|
|
2
|
+
import { compare, uniqueSorted } from "./util.js";
|
|
3
|
+
/**
|
|
4
|
+
* Resolves group members: `units` by id, `tags` to released units with any of
|
|
5
|
+
* the tags. Throws `E_GROUP_UNKNOWN_UNIT` and `E_GROUP_OVERLAP`; a group
|
|
6
|
+
* selecting nothing warns `W_GROUP_EMPTY` and is dropped.
|
|
7
|
+
*/
|
|
8
|
+
export const resolveGroups = (units, config, warnings) => {
|
|
9
|
+
const known = new Set(units.map((u) => u.id));
|
|
10
|
+
const owner = new Map();
|
|
11
|
+
const groups = [];
|
|
12
|
+
for (const name of Object.keys(config ?? {}).sort(compare)) {
|
|
13
|
+
const { units: ids = [], tags = [] } = config?.[name] ?? {};
|
|
14
|
+
const unknown = ids.find((id) => !known.has(id));
|
|
15
|
+
if (unknown !== undefined) {
|
|
16
|
+
throw new TallyError("E_GROUP_UNKNOWN_UNIT", `group "${name}" names unknown unit "${unknown}"`, { group: name, unit: unknown });
|
|
17
|
+
}
|
|
18
|
+
const members = uniqueSorted([
|
|
19
|
+
...ids,
|
|
20
|
+
...units
|
|
21
|
+
.filter((u) => u.version !== "none" && u.tags.some((t) => tags.includes(t)))
|
|
22
|
+
.map((u) => u.id),
|
|
23
|
+
]);
|
|
24
|
+
if (members.length === 0) {
|
|
25
|
+
warnings.push({
|
|
26
|
+
code: "W_GROUP_EMPTY",
|
|
27
|
+
message: `group "${name}" selects no unit`,
|
|
28
|
+
});
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
for (const id of members) {
|
|
32
|
+
const other = owner.get(id);
|
|
33
|
+
if (other !== undefined) {
|
|
34
|
+
throw new TallyError("E_GROUP_OVERLAP", `unit "${id}" is in groups "${other}" and "${name}"`, { unit: id, groups: [other, name] });
|
|
35
|
+
}
|
|
36
|
+
owner.set(id, name);
|
|
37
|
+
}
|
|
38
|
+
groups.push({ name, members });
|
|
39
|
+
}
|
|
40
|
+
return groups;
|
|
41
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,16 +1,28 @@
|
|
|
1
1
|
export declare const version = "0.0.0";
|
|
2
2
|
export type { AffectedDoc, AffectedOptions, AffectedReason, AffectedUnit, BaseSource, } from "./affected.js";
|
|
3
|
-
export type {
|
|
3
|
+
export type { ParsedCommit } from "./conventional.js";
|
|
4
|
+
export type { ExplainedChange, ExplainOptions, ReleaseAsFooter, UnitExplanation, } from "./explain.js";
|
|
5
|
+
export type { GroupPlan, Plan, PlanChange, PlanLine, PlanOptions, ReleasedUnit, UnitPlan, } from "./plan.js";
|
|
6
|
+
export type { DepConfig, GroupConfig, TallyConfig, UnitOverlay, } from "./config.js";
|
|
4
7
|
export type { Warning } from "./errors.js";
|
|
5
8
|
export type { Graph, SourcedEdge } from "./graph.js";
|
|
6
|
-
export type {
|
|
9
|
+
export type { Group } from "./groups.js";
|
|
10
|
+
export type { BumpLevel, Change, Ctx, Edge, EdgeDraft, GitReader, IntentSource, MaybePromise, Plugin, PreCtx, Propagate, RawCommit, ReadonlyFs, ReleaseAsValue, TagRef, Unit, UnitDraft, VersionFile, VersionMode, VersionScheme, VersionSource, WriteCtx, } from "./plugin.js";
|
|
7
11
|
export type { ActivePlugin, PluginEnv } from "./plugins.js";
|
|
8
|
-
export type {
|
|
12
|
+
export type { BranchRule, ReleaseSettings } from "./release.js";
|
|
13
|
+
export type { SchemeEntry } from "./schemes.js";
|
|
14
|
+
export type { SemVer } from "./semver.js";
|
|
15
|
+
export type { IntentEntry, Workspace, WorkspaceInput } from "./workspace.js";
|
|
9
16
|
export { computeAffected } from "./affected.js";
|
|
10
17
|
export { CONFIG_FILE } from "./config.js";
|
|
18
|
+
export { conventionalCommits, parseCommit } from "./conventional.js";
|
|
11
19
|
export { discoverUnits } from "./discover.js";
|
|
12
20
|
export { TallyError } from "./errors.js";
|
|
21
|
+
export { explainUnit } from "./explain.js";
|
|
13
22
|
export { buildGraph } from "./graph.js";
|
|
14
23
|
export { definePlugin, defaultUnitId, PLUGIN_API_VERSION } from "./plugin.js";
|
|
24
|
+
export { computePlan } from "./plan.js";
|
|
15
25
|
export { activatePlugins } from "./plugins.js";
|
|
26
|
+
export { semver } from "./semver.js";
|
|
27
|
+
export { validateRelease, validateTags } from "./validate.js";
|
|
16
28
|
export { loadWorkspace } from "./workspace.js";
|
package/dist/index.js
CHANGED
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
export const version = "0.0.0";
|
|
2
2
|
export { computeAffected } from "./affected.js";
|
|
3
3
|
export { CONFIG_FILE } from "./config.js";
|
|
4
|
+
export { conventionalCommits, parseCommit } from "./conventional.js";
|
|
4
5
|
export { discoverUnits } from "./discover.js";
|
|
5
6
|
export { TallyError } from "./errors.js";
|
|
7
|
+
export { explainUnit } from "./explain.js";
|
|
6
8
|
export { buildGraph } from "./graph.js";
|
|
7
9
|
export { definePlugin, defaultUnitId, PLUGIN_API_VERSION } from "./plugin.js";
|
|
10
|
+
export { computePlan } from "./plan.js";
|
|
8
11
|
export { activatePlugins } from "./plugins.js";
|
|
12
|
+
export { semver } from "./semver.js";
|
|
13
|
+
export { validateRelease, validateTags } from "./validate.js";
|
|
9
14
|
export { loadWorkspace } from "./workspace.js";
|
package/dist/plan.d.ts
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import type { AffectedDoc, AffectedOptions, AffectedReason } from "./affected.js";
|
|
2
|
+
import type { Line } from "./branches.js";
|
|
3
|
+
import type { PlanFacts } from "./facts.js";
|
|
4
|
+
import type { BumpLevel, GitReader, ReadonlyFs, VersionScheme } from "./plugin.js";
|
|
5
|
+
import type { UnitRelease } from "./release.js";
|
|
6
|
+
import type { Workspace } from "./workspace.js";
|
|
7
|
+
export interface PlanChange {
|
|
8
|
+
/** Absent for file-based changes. */
|
|
9
|
+
sha?: string;
|
|
10
|
+
type: string;
|
|
11
|
+
breaking: boolean;
|
|
12
|
+
scope?: string;
|
|
13
|
+
subject: string;
|
|
14
|
+
pr?: number;
|
|
15
|
+
/** Absorbed unit the change came through. */
|
|
16
|
+
via?: string;
|
|
17
|
+
/** Release-As value of this commit for the unit. */
|
|
18
|
+
releaseAs?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface UnitPlan {
|
|
21
|
+
id: string;
|
|
22
|
+
/** Unit root, `.` for the repo root. */
|
|
23
|
+
path: string;
|
|
24
|
+
plugin: string;
|
|
25
|
+
scheme: string;
|
|
26
|
+
tags: string[];
|
|
27
|
+
group: string | null;
|
|
28
|
+
affected: {
|
|
29
|
+
reason: AffectedReason;
|
|
30
|
+
via: string[];
|
|
31
|
+
} | null;
|
|
32
|
+
kind: "prerelease" | "rebuild" | "none";
|
|
33
|
+
baseTag: string | null;
|
|
34
|
+
lineBaseTag: string | null;
|
|
35
|
+
/** Null on a first release. */
|
|
36
|
+
from: string | null;
|
|
37
|
+
bump: BumpLevel | null;
|
|
38
|
+
releaseAs: string | null;
|
|
39
|
+
propagatedFrom: string[];
|
|
40
|
+
absorbedFrom: string[];
|
|
41
|
+
next: string | null;
|
|
42
|
+
version: string | null;
|
|
43
|
+
/** The planned commit. */
|
|
44
|
+
sha: string;
|
|
45
|
+
/** Core commit order. */
|
|
46
|
+
changes: PlanChange[];
|
|
47
|
+
}
|
|
48
|
+
export interface GroupPlan {
|
|
49
|
+
name: string;
|
|
50
|
+
members: string[];
|
|
51
|
+
baseTag: string | null;
|
|
52
|
+
from: string | null;
|
|
53
|
+
bump: BumpLevel | null;
|
|
54
|
+
next: string | null;
|
|
55
|
+
version: string | null;
|
|
56
|
+
}
|
|
57
|
+
export interface ReleasedUnit {
|
|
58
|
+
id: string;
|
|
59
|
+
path: string;
|
|
60
|
+
version: string;
|
|
61
|
+
tag: string;
|
|
62
|
+
floatingTags: string[];
|
|
63
|
+
releaseSha: string;
|
|
64
|
+
catchUp: boolean;
|
|
65
|
+
pr: number | null;
|
|
66
|
+
line: "default" | "maintenance";
|
|
67
|
+
}
|
|
68
|
+
/** `tallyrelease plan --json`, schema 1. */
|
|
69
|
+
export interface Plan extends Omit<AffectedDoc, "units"> {
|
|
70
|
+
line: Line | null;
|
|
71
|
+
/** Topological order, cycles by id. */
|
|
72
|
+
units: UnitPlan[];
|
|
73
|
+
/** Sorted by name. */
|
|
74
|
+
groups: GroupPlan[];
|
|
75
|
+
/** Stable releases in `(build.base, head]`; empty until filled. */
|
|
76
|
+
released: ReleasedUnit[];
|
|
77
|
+
}
|
|
78
|
+
export interface PlanOptions extends AffectedOptions {
|
|
79
|
+
/** Files at `head`, for intent source contexts. */
|
|
80
|
+
fs: ReadonlyFs;
|
|
81
|
+
/** Also list released units outside the build set with a pending release. */
|
|
82
|
+
all?: boolean;
|
|
83
|
+
debug?: (message: string) => void;
|
|
84
|
+
}
|
|
85
|
+
/** Inputs of `resolvePlan`, all collected before release math. */
|
|
86
|
+
export interface ResolveInput {
|
|
87
|
+
affected: AffectedDoc;
|
|
88
|
+
facts: PlanFacts;
|
|
89
|
+
schemes: ReadonlyMap<string, VersionScheme<any>>;
|
|
90
|
+
all: boolean;
|
|
91
|
+
}
|
|
92
|
+
/** Release math over collected facts; no I/O. */
|
|
93
|
+
export declare const resolvePlan: (workspace: Workspace, input: ResolveInput) => Plan;
|
|
94
|
+
/** A plan with the facts it was resolved from. */
|
|
95
|
+
export interface PlanResult {
|
|
96
|
+
plan: Plan;
|
|
97
|
+
facts: PlanFacts;
|
|
98
|
+
/** Releases of the released units with a scheme. */
|
|
99
|
+
releases: ReadonlyMap<string, UnitRelease>;
|
|
100
|
+
}
|
|
101
|
+
/** `computePlan` keeping the collected facts, for `explainUnit`. */
|
|
102
|
+
export declare const planWithFacts: (workspace: Workspace, git: GitReader, opts: PlanOptions) => Promise<PlanResult>;
|
|
103
|
+
/**
|
|
104
|
+
* Plans `head`: validates release config, computes the build set, collects
|
|
105
|
+
* git and intent facts and resolves versions and kinds.
|
|
106
|
+
*/
|
|
107
|
+
export declare const computePlan: (workspace: Workspace, git: GitReader, opts: PlanOptions) => Promise<Plan>;
|
|
108
|
+
export type { Line as PlanLine } from "./branches.js";
|