@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/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.2.0](https://github.com/abinnovision/tally-release/compare/core-v0.1.1...core-v0.2.0) (2026-10-15)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* add fixed groups ([#23](https://github.com/abinnovision/tally-release/issues/23)) ([9822ded](https://github.com/abinnovision/tally-release/commit/9822dedcb572b852348e7df3577d101bf4d70e62))
|
|
9
|
+
* **cli:** add plan and explain commands ([#20](https://github.com/abinnovision/tally-release/issues/20)) ([4a68ff1](https://github.com/abinnovision/tally-release/commit/4a68ff10092b44fb994fd62f86a51b7af4bdbd5f))
|
|
10
|
+
* **core:** absorb internal changes as patch by default ([#22](https://github.com/abinnovision/tally-release/issues/22)) ([947b85f](https://github.com/abinnovision/tally-release/commit/947b85fd442ca650f0a47ee2349ffe41b9ea5b69))
|
|
11
|
+
* **core:** absorb unreleased dependencies and propagate releases ([#19](https://github.com/abinnovision/tally-release/issues/19)) ([bb987ba](https://github.com/abinnovision/tally-release/commit/bb987ba04fec52cea611cd0b8c9ed2e851fb0c9c))
|
|
12
|
+
* **core:** add semver scheme and conventional commit parser ([#16](https://github.com/abinnovision/tally-release/issues/16)) ([2012c0b](https://github.com/abinnovision/tally-release/commit/2012c0ba035d551335f14ba709ec6db5d5ea1582))
|
|
13
|
+
* **core:** compute release versions with computePlan ([#18](https://github.com/abinnovision/tally-release/issues/18)) ([0341c3e](https://github.com/abinnovision/tally-release/commit/0341c3eea127abf32c1eda59344481a3ee506e84))
|
|
14
|
+
* **core:** match unit tags and collect plan facts ([#17](https://github.com/abinnovision/tally-release/issues/17)) ([828f9bd](https://github.com/abinnovision/tally-release/commit/828f9bd427b6e69419bdbb00d6570edee0cd9e0e))
|
|
15
|
+
* enforce maintenance branch limits ([#24](https://github.com/abinnovision/tally-release/issues/24)) ([c5e382b](https://github.com/abinnovision/tally-release/commit/c5e382bb1d58430555b85d8076659272353c1e30))
|
|
16
|
+
|
|
3
17
|
## [0.1.1](https://github.com/abinnovision/tally-release/compare/core-v0.1.0...core-v0.1.1) (2026-10-15)
|
|
4
18
|
|
|
5
19
|
|
package/dist/affected.d.ts
CHANGED
|
@@ -2,14 +2,14 @@ import type { Warning } from "./errors.js";
|
|
|
2
2
|
import type { GitReader } from "./plugin.js";
|
|
3
3
|
import type { Workspace } from "./workspace.js";
|
|
4
4
|
export type BaseSource = "cli" | "push-before" | "pr-merge-base" | "merge-group" | "forge-last-success" | "local-merge-base" | "none";
|
|
5
|
-
export type AffectedReason = "direct" | "dependent" | "global" | "all";
|
|
5
|
+
export type AffectedReason = "direct" | "dependent" | "global" | "all" | "group";
|
|
6
6
|
export interface AffectedUnit {
|
|
7
7
|
id: string;
|
|
8
8
|
/** Unit root, `.` for the repo root. */
|
|
9
9
|
path: string;
|
|
10
10
|
plugin: string;
|
|
11
11
|
reason: AffectedReason;
|
|
12
|
-
/** Affected direct dependencies
|
|
12
|
+
/** Affected direct dependencies of a dependent, or affected mates of a group member. */
|
|
13
13
|
via: string[];
|
|
14
14
|
/** True unless the unit's version is `none`. */
|
|
15
15
|
released: boolean;
|
package/dist/affected.js
CHANGED
|
@@ -49,20 +49,31 @@ export const computeAffected = async (workspace, git, opts) => {
|
|
|
49
49
|
if (hits.length > 0) {
|
|
50
50
|
return doc(everything("global"), { all: true, globalInputs: hits }, uniqueSorted(unattributed));
|
|
51
51
|
}
|
|
52
|
+
const mates = new Map(workspace.groups.flatMap((g) => g.members.map((id) => [id, g.members.filter((m) => m !== id)])));
|
|
52
53
|
const affected = new Set(direct);
|
|
53
54
|
const queue = [...direct];
|
|
54
55
|
for (const id of queue) {
|
|
55
|
-
for (const
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
for (const next of [
|
|
57
|
+
...(graph.dependents.get(id) ?? []),
|
|
58
|
+
...(mates.get(id) ?? []),
|
|
59
|
+
]) {
|
|
60
|
+
if (!affected.has(next)) {
|
|
61
|
+
affected.add(next);
|
|
62
|
+
queue.push(next);
|
|
59
63
|
}
|
|
60
64
|
}
|
|
61
65
|
}
|
|
66
|
+
// Precedence: direct, then dependent, then group.
|
|
62
67
|
const result = units
|
|
63
68
|
.filter((u) => affected.has(u.id))
|
|
64
|
-
.map((u) =>
|
|
65
|
-
|
|
66
|
-
|
|
69
|
+
.map((u) => {
|
|
70
|
+
if (direct.has(u.id)) {
|
|
71
|
+
return entry(u, "direct", []);
|
|
72
|
+
}
|
|
73
|
+
const deps = (graph.dependencies.get(u.id) ?? []).filter((d) => affected.has(d));
|
|
74
|
+
return deps.length > 0
|
|
75
|
+
? entry(u, "dependent", deps)
|
|
76
|
+
: entry(u, "group", (mates.get(u.id) ?? []).filter((m) => affected.has(m)));
|
|
77
|
+
});
|
|
67
78
|
return doc(result, { all: false, globalInputs: [] }, uniqueSorted(unattributed));
|
|
68
79
|
};
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
import type { BumpLevel } from "./plugin.js";
|
|
2
|
+
import type { BranchRule } from "./release.js";
|
|
3
|
+
/** The branch rule matching the planned branch. */
|
|
4
|
+
export interface Line {
|
|
5
|
+
rule: string;
|
|
6
|
+
kind: "default" | "maintenance";
|
|
7
|
+
/** Rendered with the rule's captures. */
|
|
8
|
+
channel: string | null;
|
|
9
|
+
/** Maintenance rules: `minor` for `{major}`, `patch` with `{minor}`. */
|
|
10
|
+
maxBump: "minor" | "patch" | null;
|
|
11
|
+
absolute: {
|
|
12
|
+
unit: string | null;
|
|
13
|
+
group: string | null;
|
|
14
|
+
major: number;
|
|
15
|
+
minor?: number;
|
|
16
|
+
} | null;
|
|
17
|
+
}
|
|
18
|
+
/**
|
|
19
|
+
* Validates rule names, `unit`/`group` and channels: `E_BRANCH_RULE`, `E_CHANNEL_WILDCARD`,
|
|
20
|
+
* `E_CHANNEL_INVALID` and `E_CHANNEL_DUPLICATE`.
|
|
21
|
+
*/
|
|
22
|
+
export declare const validateBranches: (rules: readonly BranchRule[]) => void;
|
|
23
|
+
/**
|
|
24
|
+
* The first rule matching `branch`, or null. Rules must be validated. A
|
|
25
|
+
* maintenance rule with `{major}` gets an absolute check for its `unit` or
|
|
26
|
+
* `group`, else for `single`, the repo's only release item.
|
|
27
|
+
*/
|
|
28
|
+
export declare const matchBranch: (rules: readonly BranchRule[], branch: string, single?: ReleaseItem | null) => Line | null;
|
|
29
|
+
/** An ungrouped released unit or a fixed group. */
|
|
30
|
+
export interface ReleaseItem {
|
|
31
|
+
unit: string | null;
|
|
32
|
+
group: string | null;
|
|
33
|
+
}
|
|
34
|
+
/** Major and minor of a version. */
|
|
35
|
+
export interface Core {
|
|
36
|
+
major: number;
|
|
37
|
+
minor: number;
|
|
38
|
+
}
|
|
39
|
+
/** A formatted version with its core. */
|
|
40
|
+
export interface CoreVersion extends Core {
|
|
41
|
+
version: string;
|
|
42
|
+
}
|
|
43
|
+
/** A commit that contributes to a release item's `next`. */
|
|
44
|
+
export interface LimitCommit {
|
|
45
|
+
sha?: string;
|
|
46
|
+
subject: string;
|
|
47
|
+
/** Null for a Release-As only commit. */
|
|
48
|
+
level: BumpLevel | null;
|
|
49
|
+
/** Absorbed unit the change came through. */
|
|
50
|
+
via?: string;
|
|
51
|
+
/** The Release-As value it sets. */
|
|
52
|
+
releaseAs?: CoreVersion;
|
|
53
|
+
}
|
|
54
|
+
export interface LimitItem extends ReleaseItem {
|
|
55
|
+
/** Null on a first release. */
|
|
56
|
+
from: CoreVersion | null;
|
|
57
|
+
/** Null without a pending release. */
|
|
58
|
+
next: CoreVersion | null;
|
|
59
|
+
commits: readonly LimitCommit[];
|
|
60
|
+
}
|
|
61
|
+
/**
|
|
62
|
+
* Maintenance limits: `E_CAP_BASE` when the absolute item's `from` is off the
|
|
63
|
+
* rule's line, `E_CAP` when a release item's `next` leaves the line of its
|
|
64
|
+
* `from`. First releases are not checked. `implicit`: the absolute target is
|
|
65
|
+
* the only release item, not named by the rule.
|
|
66
|
+
*/
|
|
67
|
+
export declare const checkLimits: (line: Line | null, items: readonly LimitItem[], { branch, implicit }: {
|
|
68
|
+
branch: string;
|
|
69
|
+
implicit: boolean;
|
|
70
|
+
}) => void;
|
package/dist/branches.js
ADDED
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
import { TallyError } from "./errors.js";
|
|
2
|
+
import { maxLevel } from "./util.js";
|
|
3
|
+
const SPECIAL = /[\\^$.*+?()[\]{}|/]/gu;
|
|
4
|
+
const PART = /\{(major|minor)\}|\*/gu;
|
|
5
|
+
const CHANNEL = /^[0-9A-Za-z-]+$/u;
|
|
6
|
+
const ruleError = (rule, problem) => new TallyError("E_BRANCH_RULE", `branch rule "${rule.name}" ${problem}`, {
|
|
7
|
+
rule: rule.name,
|
|
8
|
+
});
|
|
9
|
+
/** Anchored regex of a rule name; throws `E_BRANCH_RULE`. */
|
|
10
|
+
const compileRule = (rule) => {
|
|
11
|
+
const { name } = rule;
|
|
12
|
+
if (name === "") {
|
|
13
|
+
throw ruleError(rule, "has an empty name");
|
|
14
|
+
}
|
|
15
|
+
const tokens = name.match(/\{[^{}]*\}/gu) ?? [];
|
|
16
|
+
const unknown = tokens.find((t) => t !== "{major}" && t !== "{minor}");
|
|
17
|
+
if (unknown !== undefined) {
|
|
18
|
+
throw ruleError(rule, `uses unknown token ${unknown}`);
|
|
19
|
+
}
|
|
20
|
+
if (new Set(tokens).size !== tokens.length) {
|
|
21
|
+
throw ruleError(rule, "repeats a token");
|
|
22
|
+
}
|
|
23
|
+
if (tokens.includes("{minor}") && !tokens.includes("{major}")) {
|
|
24
|
+
throw ruleError(rule, "uses {minor} without {major}");
|
|
25
|
+
}
|
|
26
|
+
if (/[{}]/u.test(name.replace(PART, ""))) {
|
|
27
|
+
throw ruleError(rule, "has an unbalanced brace");
|
|
28
|
+
}
|
|
29
|
+
let source = "";
|
|
30
|
+
let last = 0;
|
|
31
|
+
for (const match of name.matchAll(PART)) {
|
|
32
|
+
source += name.slice(last, match.index).replace(SPECIAL, "\\$&");
|
|
33
|
+
source += match[1] === undefined ? ".*" : `(?<${match[1]}>0|[1-9]\\d*)`;
|
|
34
|
+
last = match.index + match[0].length;
|
|
35
|
+
}
|
|
36
|
+
source += name.slice(last).replace(SPECIAL, "\\$&");
|
|
37
|
+
return new RegExp(`^${source}$`, "u");
|
|
38
|
+
};
|
|
39
|
+
const within = (limit, from, next) => next.major === from.major && (limit === "minor" || next.minor === from.minor);
|
|
40
|
+
const itemLabel = ({ unit, group }) => unit === null ? `group "${group ?? ""}"` : `unit "${unit}"`;
|
|
41
|
+
const itemDetails = ({ unit, group }) => unit === null ? { group } : { unit };
|
|
42
|
+
/** `c3 web v2 (major)`, with `via` and Release-As when set. */
|
|
43
|
+
const commitLabel = ({ sha, subject, level, via, releaseAs }) => {
|
|
44
|
+
const notes = [
|
|
45
|
+
level === null
|
|
46
|
+
? undefined
|
|
47
|
+
: via === undefined
|
|
48
|
+
? level
|
|
49
|
+
: `${level} via ${via}`,
|
|
50
|
+
releaseAs && `Release-As ${releaseAs.version}`,
|
|
51
|
+
].filter((n) => n !== undefined);
|
|
52
|
+
return `${sha === undefined ? subject : `${sha} ${subject}`} (${notes.join(", ")})`;
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* Validates rule names, `unit`/`group` and channels: `E_BRANCH_RULE`, `E_CHANNEL_WILDCARD`,
|
|
56
|
+
* `E_CHANNEL_INVALID` and `E_CHANNEL_DUPLICATE`.
|
|
57
|
+
*/
|
|
58
|
+
export const validateBranches = (rules) => {
|
|
59
|
+
const channels = new Map();
|
|
60
|
+
for (const rule of rules) {
|
|
61
|
+
compileRule(rule);
|
|
62
|
+
if (rule.unit !== null && rule.group !== null) {
|
|
63
|
+
throw ruleError(rule, "sets both unit and group");
|
|
64
|
+
}
|
|
65
|
+
if ((rule.unit !== null || rule.group !== null) &&
|
|
66
|
+
(rule.line !== "maintenance" || !rule.name.includes("{major}"))) {
|
|
67
|
+
throw ruleError(rule, "sets unit or group without being a maintenance rule with {major}");
|
|
68
|
+
}
|
|
69
|
+
const { channel } = rule;
|
|
70
|
+
if (channel === null) {
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (rule.name.includes("*")) {
|
|
74
|
+
throw new TallyError("E_CHANNEL_WILDCARD", `branch rule "${rule.name}" contains * and cannot set a channel`, { rule: rule.name, channel });
|
|
75
|
+
}
|
|
76
|
+
const literal = channel.replace(/\{(major|minor)\}/gu, (token) => rule.name.includes(token) ? "0" : token);
|
|
77
|
+
if (!CHANNEL.test(literal) ||
|
|
78
|
+
!/[A-Za-z-]/u.test(channel.replace(/\{(major|minor)\}/gu, ""))) {
|
|
79
|
+
throw new TallyError("E_CHANNEL_INVALID", `channel "${channel}" of branch rule "${rule.name}" must match ${CHANNEL.source}, contain a letter or dash and use only tokens captured by the rule`, { rule: rule.name, channel });
|
|
80
|
+
}
|
|
81
|
+
const other = channels.get(channel);
|
|
82
|
+
if (other !== undefined) {
|
|
83
|
+
throw new TallyError("E_CHANNEL_DUPLICATE", `channel "${channel}" is set by branch rules "${other}" and "${rule.name}"`, { channel, rules: [other, rule.name] });
|
|
84
|
+
}
|
|
85
|
+
channels.set(channel, rule.name);
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* The first rule matching `branch`, or null. Rules must be validated. A
|
|
90
|
+
* maintenance rule with `{major}` gets an absolute check for its `unit` or
|
|
91
|
+
* `group`, else for `single`, the repo's only release item.
|
|
92
|
+
*/
|
|
93
|
+
export const matchBranch = (rules, branch, single = null) => {
|
|
94
|
+
for (const rule of rules) {
|
|
95
|
+
const match = compileRule(rule).exec(branch);
|
|
96
|
+
if (!match) {
|
|
97
|
+
continue;
|
|
98
|
+
}
|
|
99
|
+
const captures = match.groups ?? {};
|
|
100
|
+
const { major, minor } = captures;
|
|
101
|
+
const capped = rule.line === "maintenance" && major !== undefined;
|
|
102
|
+
const target = rule.unit !== null || rule.group !== null
|
|
103
|
+
? { unit: rule.unit, group: rule.group }
|
|
104
|
+
: single;
|
|
105
|
+
return {
|
|
106
|
+
rule: rule.name,
|
|
107
|
+
kind: rule.line,
|
|
108
|
+
channel: rule.channel?.replace(/\{(major|minor)\}/gu, (_, token) => captures[token] ?? "") ?? null,
|
|
109
|
+
maxBump: !capped ? null : minor === undefined ? "minor" : "patch",
|
|
110
|
+
absolute: !capped || target === null
|
|
111
|
+
? null
|
|
112
|
+
: {
|
|
113
|
+
...target,
|
|
114
|
+
major: Number(major),
|
|
115
|
+
...(minor === undefined ? {} : { minor: Number(minor) }),
|
|
116
|
+
},
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
return null;
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Maintenance limits: `E_CAP_BASE` when the absolute item's `from` is off the
|
|
123
|
+
* rule's line, `E_CAP` when a release item's `next` leaves the line of its
|
|
124
|
+
* `from`. First releases are not checked. `implicit`: the absolute target is
|
|
125
|
+
* the only release item, not named by the rule.
|
|
126
|
+
*/
|
|
127
|
+
export const checkLimits = (line, items, { branch, implicit }) => {
|
|
128
|
+
const limit = line?.maxBump ?? null;
|
|
129
|
+
if (line === null || limit === null) {
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
const { absolute } = line;
|
|
133
|
+
const target = items.find((i) => absolute !== null &&
|
|
134
|
+
i.unit === absolute.unit &&
|
|
135
|
+
i.group === absolute.group);
|
|
136
|
+
if (absolute !== null && target?.from) {
|
|
137
|
+
const { from } = target;
|
|
138
|
+
if (from.major !== absolute.major ||
|
|
139
|
+
(absolute.minor !== undefined && from.minor !== absolute.minor)) {
|
|
140
|
+
const expected = [absolute.major, absolute.minor ?? "x"].join(".");
|
|
141
|
+
throw new TallyError("E_CAP_BASE", `branch "${branch}": ${itemLabel(target)}${implicit ? " (the only release item)" : ""} is at ${from.version}, off the ${expected} line of branch rule "${line.rule}"; the branch was cut from or merged with a commit carrying a newer tag, or names a next major, which maintenance rules do not support (use a default-line rule with a channel)`, {
|
|
142
|
+
...itemDetails(target),
|
|
143
|
+
branch,
|
|
144
|
+
rule: line.rule,
|
|
145
|
+
major: absolute.major,
|
|
146
|
+
...(absolute.minor === undefined ? {} : { minor: absolute.minor }),
|
|
147
|
+
from: from.version,
|
|
148
|
+
});
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
for (const item of items) {
|
|
152
|
+
const { from, next } = item;
|
|
153
|
+
if (from === null || next === null || within(limit, from, next)) {
|
|
154
|
+
continue;
|
|
155
|
+
}
|
|
156
|
+
const commits = item.commits.filter((c) => (c.level !== null && maxLevel(c.level, limit) !== limit) ||
|
|
157
|
+
(c.releaseAs !== undefined && !within(limit, from, c.releaseAs)));
|
|
158
|
+
const listed = commits.map(commitLabel).join("; ");
|
|
159
|
+
throw new TallyError("E_CAP", `${itemLabel(item)}: ${from.version} to ${next.version} exceeds the ${limit} limit of branch rule "${line.rule}"${listed === "" ? "" : `; commits: ${listed}`}`, {
|
|
160
|
+
...itemDetails(item),
|
|
161
|
+
rule: line.rule,
|
|
162
|
+
limit,
|
|
163
|
+
from: from.version,
|
|
164
|
+
next: next.version,
|
|
165
|
+
commits,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
};
|
package/dist/builtin.js
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { conventionalCommits } from "./conventional.js";
|
|
2
|
+
import { PLUGIN_API_VERSION } from "./plugin.js";
|
|
3
|
+
import { semver } from "./semver.js";
|
|
4
|
+
/** Plugin name reserved for core built-ins. */
|
|
5
|
+
export const CORE_PLUGIN = "core";
|
|
6
|
+
/** Core built-ins, registered before every configured plugin. */
|
|
7
|
+
export const corePlugin = {
|
|
8
|
+
apiVersion: PLUGIN_API_VERSION,
|
|
9
|
+
name: CORE_PLUGIN,
|
|
10
|
+
schemes: { semver },
|
|
11
|
+
intent: [conventionalCommits],
|
|
12
|
+
};
|
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { Propagate, VersionSource } from "./plugin.js";
|
|
1
|
+
import type { BumpLevel, Propagate, VersionSource } from "./plugin.js";
|
|
2
2
|
/** Config overlay for the unit at a root; creates a config-only unit if none is discovered. */
|
|
3
3
|
export interface UnitOverlay {
|
|
4
4
|
id?: string;
|
|
@@ -13,6 +13,38 @@ export interface UnitOverlay {
|
|
|
13
13
|
propagate?: Propagate;
|
|
14
14
|
/** Labels for filtering, each matching `/^[A-Za-z0-9._-]+$/`. */
|
|
15
15
|
tags?: string[];
|
|
16
|
+
/** First release version, parsed by the unit's scheme. */
|
|
17
|
+
initialVersion?: string;
|
|
18
|
+
}
|
|
19
|
+
/** A branch rule; the first rule whose `name` matches the branch wins. */
|
|
20
|
+
export interface BranchConfig {
|
|
21
|
+
/** Pattern with `*`, `{major}` and `{minor}`. */
|
|
22
|
+
name: string;
|
|
23
|
+
line: "default" | "maintenance";
|
|
24
|
+
/** Prerelease channel, may use the `{major}`/`{minor}` captures of `name`. */
|
|
25
|
+
channel?: string;
|
|
26
|
+
/** Maintenance rules: the unit of the absolute check; excludes `group`. */
|
|
27
|
+
unit?: string;
|
|
28
|
+
/** Maintenance rules: the group of the absolute check; excludes `unit`. */
|
|
29
|
+
group?: string;
|
|
30
|
+
}
|
|
31
|
+
export interface BumpsConfig {
|
|
32
|
+
/** Commit type to level, merged over the defaults. */
|
|
33
|
+
types?: Record<string, BumpLevel>;
|
|
34
|
+
/** Levels used while the major version is 0. */
|
|
35
|
+
preMajor?: {
|
|
36
|
+
breaking?: BumpLevel;
|
|
37
|
+
feat?: BumpLevel;
|
|
38
|
+
};
|
|
39
|
+
/** Level of changes absorbed from unreleased dependencies, default `patch`. */
|
|
40
|
+
absorbed?: "patch" | "minor";
|
|
41
|
+
}
|
|
42
|
+
/** A fixed group: members share one version. Selections are unioned. */
|
|
43
|
+
export interface GroupConfig {
|
|
44
|
+
/** Member unit ids. */
|
|
45
|
+
units?: string[];
|
|
46
|
+
/** Selects released units with any of these tags. */
|
|
47
|
+
tags?: string[];
|
|
16
48
|
}
|
|
17
49
|
export interface DepConfig {
|
|
18
50
|
from: string;
|
|
@@ -31,12 +63,17 @@ export interface TallyConfig {
|
|
|
31
63
|
release?: boolean;
|
|
32
64
|
tagFormat?: string;
|
|
33
65
|
propagate?: Propagate;
|
|
66
|
+
initialVersion?: string;
|
|
34
67
|
};
|
|
35
68
|
/** Overlays keyed by unit root. */
|
|
36
69
|
units?: Record<string, UnitOverlay>;
|
|
37
70
|
deps?: DepConfig[];
|
|
38
71
|
ignore?: string[];
|
|
39
72
|
globalInputs?: string[];
|
|
73
|
+
branches?: BranchConfig[];
|
|
74
|
+
bumps?: BumpsConfig;
|
|
75
|
+
/** Fixed groups by name; names follow the unit id pattern. */
|
|
76
|
+
groups?: Record<string, GroupConfig>;
|
|
40
77
|
}
|
|
41
78
|
/** Global input owned by core. */
|
|
42
79
|
export declare const CONFIG_FILE = "tallyrelease.yaml";
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import type { IntentSource } from "./plugin.js";
|
|
2
|
+
export interface ParsedCommit {
|
|
3
|
+
/** Empty for a non-conventional header. */
|
|
4
|
+
type: string;
|
|
5
|
+
scope: string | undefined;
|
|
6
|
+
breaking: boolean;
|
|
7
|
+
subject: string;
|
|
8
|
+
body: string | undefined;
|
|
9
|
+
pr: number | undefined;
|
|
10
|
+
/** Lowercased footer key to values in message order. */
|
|
11
|
+
footers: Record<string, string[]>;
|
|
12
|
+
}
|
|
13
|
+
/**
|
|
14
|
+
* Parses a commit message per Conventional Commits. The footer block starts at
|
|
15
|
+
* the first paragraph whose first line is `token: value` or `token #value`; a
|
|
16
|
+
* footer value runs until the next such line.
|
|
17
|
+
*/
|
|
18
|
+
export declare const parseCommit: (message: string) => ParsedCommit;
|
|
19
|
+
/** Maps each commit to exactly one commit-derived Change. */
|
|
20
|
+
export declare const conventionalCommits: IntentSource;
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
const HEADER = /^(?<type>[A-Za-z][\w-]*)(?:\((?<scope>[^()]*)\))?(?<bang>!)?: (?<subject>.+)$/u;
|
|
2
|
+
const PR = /\s*\(#(?<pr>\d+)\)$/u;
|
|
3
|
+
const FOOTER = /^(?<key>BREAKING[ -]CHANGE|[A-Za-z][A-Za-z0-9-]*)(?:: | #)(?<value>.*)$/u;
|
|
4
|
+
const QUALIFIED = /^(?<unit>[A-Za-z0-9._-]+)@(?<version>.+)$/u;
|
|
5
|
+
const BREAKING = new Set(["BREAKING CHANGE", "BREAKING-CHANGE"]);
|
|
6
|
+
const releaseAs = (value) => {
|
|
7
|
+
const version = value.split("\n")[0]?.trim() ?? "";
|
|
8
|
+
const match = QUALIFIED.exec(version)?.groups;
|
|
9
|
+
return match?.["unit"] !== undefined && match["version"] !== undefined
|
|
10
|
+
? { unit: match["unit"], version: match["version"] }
|
|
11
|
+
: { version };
|
|
12
|
+
};
|
|
13
|
+
const withPr = (text) => {
|
|
14
|
+
const pr = PR.exec(text)?.groups?.["pr"];
|
|
15
|
+
return pr === undefined
|
|
16
|
+
? { subject: text, pr: undefined }
|
|
17
|
+
: { subject: text.replace(PR, ""), pr: Number(pr) };
|
|
18
|
+
};
|
|
19
|
+
/**
|
|
20
|
+
* Parses a commit message per Conventional Commits. The footer block starts at
|
|
21
|
+
* the first paragraph whose first line is `token: value` or `token #value`; a
|
|
22
|
+
* footer value runs until the next such line.
|
|
23
|
+
*/
|
|
24
|
+
export const parseCommit = (message) => {
|
|
25
|
+
const [first = "", ...rest] = message.replace(/\r\n?/gu, "\n").split("\n");
|
|
26
|
+
const header = first.trim();
|
|
27
|
+
let start = rest.findIndex((line, i) => FOOTER.test(line) && (i === 0 || rest[i - 1]?.trim() === ""));
|
|
28
|
+
start = start === -1 ? rest.length : start;
|
|
29
|
+
const footers = new Map();
|
|
30
|
+
let current;
|
|
31
|
+
let breaking = false;
|
|
32
|
+
for (const line of rest.slice(start)) {
|
|
33
|
+
const match = FOOTER.exec(line)?.groups;
|
|
34
|
+
if (match?.["key"] === undefined) {
|
|
35
|
+
current?.push(line);
|
|
36
|
+
continue;
|
|
37
|
+
}
|
|
38
|
+
breaking ||= BREAKING.has(match["key"]);
|
|
39
|
+
const key = match["key"].toLowerCase();
|
|
40
|
+
current = [(match["value"] ?? "").trim()];
|
|
41
|
+
footers.set(key, [...(footers.get(key) ?? []), current]);
|
|
42
|
+
}
|
|
43
|
+
const body = rest
|
|
44
|
+
.slice(0, start)
|
|
45
|
+
.join("\n")
|
|
46
|
+
.replace(/^(?:[ \t]*\n)+/u, "")
|
|
47
|
+
.trimEnd() || undefined;
|
|
48
|
+
const common = {
|
|
49
|
+
body,
|
|
50
|
+
footers: Object.fromEntries([...footers].map(([key, values]) => [
|
|
51
|
+
key,
|
|
52
|
+
values.map((lines) => lines.join("\n").trimEnd()),
|
|
53
|
+
])),
|
|
54
|
+
};
|
|
55
|
+
const parts = HEADER.exec(header)?.groups;
|
|
56
|
+
if (parts?.["type"] === undefined) {
|
|
57
|
+
return {
|
|
58
|
+
type: "",
|
|
59
|
+
scope: undefined,
|
|
60
|
+
breaking,
|
|
61
|
+
...withPr(header),
|
|
62
|
+
...common,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
return {
|
|
66
|
+
type: parts["type"],
|
|
67
|
+
scope: parts["scope"] === "" ? undefined : parts["scope"],
|
|
68
|
+
breaking: breaking || parts["bang"] !== undefined,
|
|
69
|
+
...withPr(parts["subject"] ?? ""),
|
|
70
|
+
...common,
|
|
71
|
+
};
|
|
72
|
+
};
|
|
73
|
+
/** Maps each commit to exactly one commit-derived Change. */
|
|
74
|
+
export const conventionalCommits = {
|
|
75
|
+
name: "conventional-commits",
|
|
76
|
+
collect: (_ctx, { commits }) => commits.map((commit) => {
|
|
77
|
+
const parsed = parseCommit(commit.message);
|
|
78
|
+
const values = parsed.footers["release-as"] ?? [];
|
|
79
|
+
return {
|
|
80
|
+
sha: commit.sha,
|
|
81
|
+
type: parsed.type,
|
|
82
|
+
breaking: parsed.breaking,
|
|
83
|
+
...(parsed.scope === undefined ? {} : { scope: parsed.scope }),
|
|
84
|
+
subject: parsed.subject,
|
|
85
|
+
...(parsed.body === undefined ? {} : { body: parsed.body }),
|
|
86
|
+
...(parsed.pr === undefined ? {} : { pr: parsed.pr }),
|
|
87
|
+
...(values.length === 0 ? {} : { releaseAs: values.map(releaseAs) }),
|
|
88
|
+
};
|
|
89
|
+
}),
|
|
90
|
+
};
|
package/dist/discover.js
CHANGED
|
@@ -41,7 +41,7 @@ export const discoverUnits = async (active, env) => {
|
|
|
41
41
|
});
|
|
42
42
|
}
|
|
43
43
|
}
|
|
44
|
-
for (const [key, { release, tags, ...overlay }] of Object.entries(env.config.units ?? {})) {
|
|
44
|
+
for (const [key, { release, tags, initialVersion, ...overlay },] of Object.entries(env.config.units ?? {})) {
|
|
45
45
|
const root = normalizeRoot(key);
|
|
46
46
|
const invalid = tags?.find((t) => !UNIT_TAG.test(t));
|
|
47
47
|
if (invalid !== undefined) {
|
|
@@ -52,6 +52,7 @@ export const discoverUnits = async (active, env) => {
|
|
|
52
52
|
entry.draft = { ...entry.draft, ...overlay };
|
|
53
53
|
entry.release = release;
|
|
54
54
|
entry.tags = tags;
|
|
55
|
+
entry.initialVersion = initialVersion;
|
|
55
56
|
}
|
|
56
57
|
else {
|
|
57
58
|
byRoot.set(root, {
|
|
@@ -64,6 +65,7 @@ export const discoverUnits = async (active, env) => {
|
|
|
64
65
|
meta: {},
|
|
65
66
|
release,
|
|
66
67
|
tags,
|
|
68
|
+
initialVersion,
|
|
67
69
|
});
|
|
68
70
|
}
|
|
69
71
|
}
|
|
@@ -91,7 +93,7 @@ export const discoverUnits = async (active, env) => {
|
|
|
91
93
|
const roots = [...byRoot.keys()].sort(compare);
|
|
92
94
|
const { defaults } = env.config;
|
|
93
95
|
return [...byId.values()]
|
|
94
|
-
.map(({ plugin, draft, meta, release, tags }) => {
|
|
96
|
+
.map(({ plugin, draft, meta, release, tags, initialVersion }) => {
|
|
95
97
|
const { root } = draft;
|
|
96
98
|
const nested = roots
|
|
97
99
|
.filter((r) => isInside(r, root))
|
|
@@ -115,6 +117,7 @@ export const discoverUnits = async (active, env) => {
|
|
|
115
117
|
(root === "." ? "v{version}" : "{id}-v{version}"),
|
|
116
118
|
propagate: draft.propagate ?? defaults?.propagate ?? "auto",
|
|
117
119
|
tags: Object.freeze(uniqueSorted(tags ?? [])),
|
|
120
|
+
initialVersion: initialVersion ?? defaults?.initialVersion ?? null,
|
|
118
121
|
meta: Object.freeze(meta),
|
|
119
122
|
});
|
|
120
123
|
})
|
package/dist/errors.d.ts
CHANGED
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import type { Warning } from "./errors.js";
|
|
2
|
+
import type { Plan, PlanChange, PlanOptions, UnitPlan } from "./plan.js";
|
|
3
|
+
import type { GitReader } from "./plugin.js";
|
|
4
|
+
import type { Workspace } from "./workspace.js";
|
|
5
|
+
export interface ExplainedChange extends PlanChange {
|
|
6
|
+
/** Commit files attributed to the unit, or to `via`. */
|
|
7
|
+
files: string[];
|
|
8
|
+
}
|
|
9
|
+
export interface ReleaseAsFooter {
|
|
10
|
+
sha?: string;
|
|
11
|
+
/** Raw value; qualified values without the unit prefix. */
|
|
12
|
+
version: string;
|
|
13
|
+
qualified: boolean;
|
|
14
|
+
/** The value that set `releaseAs`. */
|
|
15
|
+
applied: boolean;
|
|
16
|
+
}
|
|
17
|
+
/** Why a unit is affected and how its version is computed. */
|
|
18
|
+
export interface UnitExplanation {
|
|
19
|
+
/** The plan with `all` set. */
|
|
20
|
+
plan: Plan;
|
|
21
|
+
/** Plan entry; null when the unit is neither affected nor pending. */
|
|
22
|
+
unit: UnitPlan | null;
|
|
23
|
+
/** The unit's fixed group. */
|
|
24
|
+
group: {
|
|
25
|
+
name: string;
|
|
26
|
+
members: string[];
|
|
27
|
+
} | null;
|
|
28
|
+
/** What set `unit.next`: the bump, the initial version or a Release-As value. */
|
|
29
|
+
nextSource: "bump" | "initial" | "release-as" | null;
|
|
30
|
+
/** Changed unit files (reason `direct`) or changed global inputs (`global`). */
|
|
31
|
+
files: string[];
|
|
32
|
+
/** Plan changes with their files, core commit order. */
|
|
33
|
+
changes: ExplainedChange[];
|
|
34
|
+
/** `pinned` is null when the dependency is reached through unreleased units. */
|
|
35
|
+
propagatedFrom: {
|
|
36
|
+
id: string;
|
|
37
|
+
pinned: boolean | null;
|
|
38
|
+
}[];
|
|
39
|
+
/** Release-As footers for the unit since its base tag, core commit order. */
|
|
40
|
+
releaseAs: ReleaseAsFooter[];
|
|
41
|
+
/** Plan warnings of the unit and those of no unit. */
|
|
42
|
+
warnings: Warning[];
|
|
43
|
+
}
|
|
44
|
+
export interface ExplainOptions extends PlanOptions {
|
|
45
|
+
/** Unit id. */
|
|
46
|
+
unit: string;
|
|
47
|
+
}
|
|
48
|
+
/** Explains one unit of the plan of `opts.head`; throws `E_UNIT_UNKNOWN`. */
|
|
49
|
+
export declare const explainUnit: (workspace: Workspace, git: GitReader, { unit: id, ...opts }: ExplainOptions) => Promise<UnitExplanation>;
|