@tallyrelease/core 0.2.0 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +11 -0
- package/dist/base64.d.ts +4 -0
- package/dist/base64.js +40 -0
- package/dist/changelog.d.ts +65 -0
- package/dist/changelog.js +259 -0
- package/dist/config.d.ts +22 -0
- package/dist/conventional.js +6 -0
- package/dist/discover.d.ts +2 -0
- package/dist/discover.js +4 -4
- package/dist/floating.d.ts +14 -0
- package/dist/floating.js +44 -0
- package/dist/index.d.ts +10 -1
- package/dist/index.js +6 -0
- package/dist/marker.d.ts +17 -0
- package/dist/marker.js +48 -0
- package/dist/plan.d.ts +2 -0
- package/dist/plan.js +1 -0
- package/dist/plugin.d.ts +4 -0
- package/dist/propagate.d.ts +7 -1
- package/dist/propagate.js +1 -1
- package/dist/release-items.d.ts +35 -0
- package/dist/release-items.js +120 -0
- package/dist/release.d.ts +13 -2
- package/dist/release.js +19 -1
- package/dist/released.d.ts +25 -0
- package/dist/released.js +104 -0
- package/dist/tags.d.ts +2 -0
- package/dist/tags.js +4 -0
- package/dist/validate.d.ts +2 -1
- package/dist/validate.js +8 -1
- package/dist/version-files.d.ts +34 -0
- package/dist/version-files.js +247 -0
- package/dist/workspace.d.ts +3 -1
- package/dist/workspace.js +25 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [0.3.0](https://github.com/abinnovision/tally-release/compare/core-v0.2.0...core-v0.3.0) (2026-10-16)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **cli:** add tallyrelease pr for shared and separate release PRs ([#34](https://github.com/abinnovision/tally-release/issues/34)) ([5dbb294](https://github.com/abinnovision/tally-release/commit/5dbb2944a6f9535511710cbcfa5c6ddfccb14fe8))
|
|
9
|
+
* **cli:** add tallyrelease run and fill released in plans ([#36](https://github.com/abinnovision/tally-release/issues/36)) ([8d90546](https://github.com/abinnovision/tally-release/commit/8d905469ed398a6daaa0e09f4fd9eb9c4c79c818))
|
|
10
|
+
* **core:** add changelog writer, release items and PR marker ([#30](https://github.com/abinnovision/tally-release/issues/30)) ([e74244e](https://github.com/abinnovision/tally-release/commit/e74244ebc4752e3de39614159ef0761f1cd2a33c))
|
|
11
|
+
* tag merged release PRs with tallyrelease release ([#35](https://github.com/abinnovision/tally-release/issues/35)) ([0058045](https://github.com/abinnovision/tally-release/commit/005804542b3f0b18a1a5afdb1a559231f8d10b4d))
|
|
12
|
+
* write version files and changelogs with tallyrelease version ([#33](https://github.com/abinnovision/tally-release/issues/33)) ([5961ce0](https://github.com/abinnovision/tally-release/commit/5961ce0c56b7ddbb4bff2cf154d5b5556212e8cb))
|
|
13
|
+
|
|
3
14
|
## [0.2.0](https://github.com/abinnovision/tally-release/compare/core-v0.1.1...core-v0.2.0) (2026-10-15)
|
|
4
15
|
|
|
5
16
|
|
package/dist/base64.d.ts
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
/** Standard padded base64 of the UTF-8 encoding of `text`. */
|
|
2
|
+
export declare const encodeBase64: (text: string) => string;
|
|
3
|
+
/** Text of padded base64 holding UTF-8; undefined when either is malformed. */
|
|
4
|
+
export declare const decodeBase64: (data: string) => string | undefined;
|
package/dist/base64.js
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const ALPHABET = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
|
|
2
|
+
const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/u;
|
|
3
|
+
/** UTF-8 bytes of `text`; throws on lone surrogates. */
|
|
4
|
+
const utf8 = (text) => [...encodeURIComponent(text).matchAll(/%([0-9A-F]{2})|[^%]/gu)].map(([char, hex]) => hex === undefined ? char.charCodeAt(0) : parseInt(hex, 16));
|
|
5
|
+
/** Standard padded base64 of the UTF-8 encoding of `text`. */
|
|
6
|
+
export const encodeBase64 = (text) => {
|
|
7
|
+
const bytes = utf8(text);
|
|
8
|
+
let out = "";
|
|
9
|
+
for (let i = 0; i < bytes.length; i += 3) {
|
|
10
|
+
const [a = 0, b = 0, c = 0] = bytes.slice(i, i + 3);
|
|
11
|
+
const n = a * 65536 + b * 256 + c;
|
|
12
|
+
const chars = [18, 12, 6, 0].map((s) => ALPHABET[Math.floor(n / 2 ** s) % 64]);
|
|
13
|
+
const pad = 3 - Math.min(3, bytes.length - i);
|
|
14
|
+
out += chars.slice(0, 4 - pad).join("") + "=".repeat(pad);
|
|
15
|
+
}
|
|
16
|
+
return out;
|
|
17
|
+
};
|
|
18
|
+
/** Text of padded base64 holding UTF-8; undefined when either is malformed. */
|
|
19
|
+
export const decodeBase64 = (data) => {
|
|
20
|
+
if (!BASE64.test(data)) {
|
|
21
|
+
return undefined;
|
|
22
|
+
}
|
|
23
|
+
let hex = "";
|
|
24
|
+
for (let i = 0; i < data.length; i += 4) {
|
|
25
|
+
const quad = data.slice(i, i + 4);
|
|
26
|
+
const pad = quad.length - quad.replace(/=+$/u, "").length;
|
|
27
|
+
const n = Array.from({ length: 4 }, (_, k) => quad.charAt(k)).reduce((acc, char) => acc * 64 + Math.max(0, ALPHABET.indexOf(char)), 0);
|
|
28
|
+
const bytes = [16, 8, 0].map((s) => Math.floor(n / 2 ** s) % 256);
|
|
29
|
+
hex += bytes
|
|
30
|
+
.slice(0, 3 - pad)
|
|
31
|
+
.map((b) => `%${b.toString(16).padStart(2, "0")}`)
|
|
32
|
+
.join("");
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
return decodeURIComponent(hex);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return undefined;
|
|
39
|
+
}
|
|
40
|
+
};
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { PlanChange, Plan } from "./plan.js";
|
|
2
|
+
import type { Unit, VersionScheme } from "./plugin.js";
|
|
3
|
+
import type { Workspace } from "./workspace.js";
|
|
4
|
+
/** `<root>/CHANGELOG.md` of a unit. */
|
|
5
|
+
export declare const changelogPath: (unit: Pick<Unit, "root">) => string;
|
|
6
|
+
export interface ChangelogDependency {
|
|
7
|
+
/** Manifest field, such as `dependencies`. */
|
|
8
|
+
field: string;
|
|
9
|
+
name: string;
|
|
10
|
+
version: string;
|
|
11
|
+
}
|
|
12
|
+
export interface ChangelogInput {
|
|
13
|
+
version: string;
|
|
14
|
+
/** `YYYY-MM-DD`, the date the section is generated. */
|
|
15
|
+
date: string;
|
|
16
|
+
/** Repository URL such as `https://github.com/o/r`; null omits all links. */
|
|
17
|
+
repoUrl: string | null;
|
|
18
|
+
/** Previous tag of the unit; null omits the compare link. */
|
|
19
|
+
baseTag: string | null;
|
|
20
|
+
tag: string;
|
|
21
|
+
/** Core commit order. */
|
|
22
|
+
changes: readonly PlanChange[];
|
|
23
|
+
dependencies?: readonly ChangelogDependency[];
|
|
24
|
+
/** Group member: lists a synchronize entry when nothing else is listed. */
|
|
25
|
+
member?: {
|
|
26
|
+
unit: string;
|
|
27
|
+
group: string;
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* One version section in release-please format: heading, breaking notes,
|
|
32
|
+
* sections by commit type with entries sorted like release-please (scope
|
|
33
|
+
* followed by subject, locale order), then dependencies. No trailing newline.
|
|
34
|
+
*/
|
|
35
|
+
export declare const renderChangelogSection: (input: ChangelogInput) => string;
|
|
36
|
+
/**
|
|
37
|
+
* The changelog section of a unit with a stable `next` in `plan`, else
|
|
38
|
+
* undefined. Dependencies are direct edges from units of the same release PR,
|
|
39
|
+
* named after the edge reason (`<field>: <name>`, else `dependencies` and the
|
|
40
|
+
* unit id).
|
|
41
|
+
*/
|
|
42
|
+
export declare const changelogSection: (workspace: Workspace, plan: Plan, opts: {
|
|
43
|
+
id: string;
|
|
44
|
+
repoUrl: string | null;
|
|
45
|
+
date: string;
|
|
46
|
+
/** Units released in the same release PR; dependencies outside it are not listed. */
|
|
47
|
+
prUnits: readonly string[];
|
|
48
|
+
}) => string | undefined;
|
|
49
|
+
/**
|
|
50
|
+
* Inserts `section` on top of the version sections of `content` (undefined or blank:
|
|
51
|
+
* a new file with `# Changelog`). Version headings are `##` or `###` lines,
|
|
52
|
+
* linked or plain. Sections above `baseVersion` are untagged leftovers and are
|
|
53
|
+
* replaced; without a base version, sections not below the new version are.
|
|
54
|
+
* Text before the first version heading and the other sections stay as they
|
|
55
|
+
* are.
|
|
56
|
+
*/
|
|
57
|
+
export declare const updateChangelog: <V>(content: string | undefined, section: string, { baseVersion, scheme, }: {
|
|
58
|
+
baseVersion: string | null;
|
|
59
|
+
scheme: VersionScheme<V>;
|
|
60
|
+
}) => string;
|
|
61
|
+
/**
|
|
62
|
+
* Top version section of unit `id`'s `<root>/CHANGELOG.md`, heading included,
|
|
63
|
+
* read through `read`; undefined without a file or a version section.
|
|
64
|
+
*/
|
|
65
|
+
export declare const releaseNotes: (workspace: Workspace, id: string, read: (path: string) => string | undefined) => string | undefined;
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
import { schemeRegistry } from "./schemes.js";
|
|
2
|
+
import { renderTag } from "./tags.js";
|
|
3
|
+
import { compare } from "./util.js";
|
|
4
|
+
/** Commit types in section order; hidden sections list only breaking or Release-As changes. */
|
|
5
|
+
const SECTIONS = [
|
|
6
|
+
["feat", "Features", false],
|
|
7
|
+
["feature", "Features", false],
|
|
8
|
+
["fix", "Bug Fixes", false],
|
|
9
|
+
["perf", "Performance Improvements", false],
|
|
10
|
+
["revert", "Reverts", false],
|
|
11
|
+
["docs", "Documentation", true],
|
|
12
|
+
["style", "Styles", true],
|
|
13
|
+
["chore", "Miscellaneous Chores", true],
|
|
14
|
+
["refactor", "Code Refactoring", true],
|
|
15
|
+
["test", "Tests", true],
|
|
16
|
+
["build", "Build System", true],
|
|
17
|
+
["ci", "Continuous Integration", true],
|
|
18
|
+
];
|
|
19
|
+
const TITLES = [...new Set(SECTIONS.map(([, title]) => title))];
|
|
20
|
+
const FIELDS = [
|
|
21
|
+
"dependencies",
|
|
22
|
+
"devDependencies",
|
|
23
|
+
"peerDependencies",
|
|
24
|
+
"optionalDependencies",
|
|
25
|
+
];
|
|
26
|
+
const VERSION_HEADING = /^###? (?:\[v?([^\]\s]+)\]|v?(\S+))/u;
|
|
27
|
+
const EDGE_REASON = /^([A-Za-z]+): (.+)$/u;
|
|
28
|
+
/** release-please `htmlEscape`: `<` and `>` outside code spans. */
|
|
29
|
+
const htmlEscape = (text) => text.replace(/``[^`].*[^`]``|`[^`]*`|<|>/gu, (m) => m.length > 1 ? m : m === "<" ? "<" : ">");
|
|
30
|
+
/** Subject with the trailing PR reference the commit header had. */
|
|
31
|
+
const withPr = (change) => change.pr === undefined
|
|
32
|
+
? change.subject
|
|
33
|
+
: `${change.subject} (#${String(change.pr)})`;
|
|
34
|
+
/** Subject with its PR, issue and user references linked. */
|
|
35
|
+
const subjectOf = (change, repoUrl) => {
|
|
36
|
+
const text = htmlEscape(withPr(change));
|
|
37
|
+
if (repoUrl === null) {
|
|
38
|
+
return text;
|
|
39
|
+
}
|
|
40
|
+
const host = repoUrl.split("/").slice(0, 3).join("/");
|
|
41
|
+
return text
|
|
42
|
+
.replace(/#([0-9]+)/gu, (_, n) => `[#${n}](${repoUrl}/issues/${n})`)
|
|
43
|
+
.replace(/\B@([a-z0-9](?:-?[a-z0-9/]){0,38})/gu, (m, user) => user.includes("/") ? m : `[@${user}](${host}/${user})`);
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Breaking note text as release-please renders it: lines of the first
|
|
47
|
+
* paragraph joined by spaces, except that from the first later line starting
|
|
48
|
+
* with `#### `, `* ` or `- ` on, lines are kept below, indented by four
|
|
49
|
+
* spaces. Without notes, the subject.
|
|
50
|
+
*/
|
|
51
|
+
const noteOf = (change, repoUrl) => {
|
|
52
|
+
let text = "";
|
|
53
|
+
let extended = false;
|
|
54
|
+
for (const [i, line] of (change.notes?.split("\n") ?? []).entries()) {
|
|
55
|
+
if (i > 0 && !extended && /^#### |^[*-] /u.test(line)) {
|
|
56
|
+
text = `${text.trim()}\n`;
|
|
57
|
+
extended = true;
|
|
58
|
+
}
|
|
59
|
+
if (line === "") {
|
|
60
|
+
break;
|
|
61
|
+
}
|
|
62
|
+
text += extended ? ` ${line}\n` : `${line} `;
|
|
63
|
+
}
|
|
64
|
+
text = change.notes === undefined ? withPr(change) : text.trim();
|
|
65
|
+
return repoUrl === null
|
|
66
|
+
? text
|
|
67
|
+
: text.replace(/\(#([0-9]+)\)/u, (_, n) => `([#${n}](${repoUrl}/issues/${n}))`);
|
|
68
|
+
};
|
|
69
|
+
const scoped = (scope, text) => `* ${scope === undefined ? "" : `**${scope}:** `}${text}`;
|
|
70
|
+
const via = (change) => change.via === undefined ? "" : ` (via ${change.via})`;
|
|
71
|
+
const dependencyLines = (deps) => {
|
|
72
|
+
const rank = (field) => {
|
|
73
|
+
const i = FIELDS.indexOf(field);
|
|
74
|
+
return i === -1 ? FIELDS.length : i;
|
|
75
|
+
};
|
|
76
|
+
const fields = [...new Set(deps.map((d) => d.field))].sort((a, b) => rank(a) - rank(b) || compare(a, b));
|
|
77
|
+
return [
|
|
78
|
+
"* The following workspace dependencies were updated",
|
|
79
|
+
...fields.flatMap((field) => [
|
|
80
|
+
` * ${field}`,
|
|
81
|
+
...deps
|
|
82
|
+
.filter((d) => d.field === field)
|
|
83
|
+
.sort((a, b) => compare(a.name, b.name))
|
|
84
|
+
.map((d) => ` * ${d.name} bumped to ${d.version}`),
|
|
85
|
+
]),
|
|
86
|
+
].join("\n");
|
|
87
|
+
};
|
|
88
|
+
/** Version of a version heading line, else undefined. */
|
|
89
|
+
const headingVersion = (line, scheme) => {
|
|
90
|
+
const m = VERSION_HEADING.exec(line);
|
|
91
|
+
const raw = m?.[1] ?? m?.[2];
|
|
92
|
+
return raw === undefined ? undefined : scheme.parse(raw);
|
|
93
|
+
};
|
|
94
|
+
/** `<root>/CHANGELOG.md` of a unit. */
|
|
95
|
+
export const changelogPath = (unit) => unit.root === "." ? "CHANGELOG.md" : `${unit.root}/CHANGELOG.md`;
|
|
96
|
+
/**
|
|
97
|
+
* One version section in release-please format: heading, breaking notes,
|
|
98
|
+
* sections by commit type with entries sorted like release-please (scope
|
|
99
|
+
* followed by subject, locale order), then dependencies. No trailing newline.
|
|
100
|
+
*/
|
|
101
|
+
export const renderChangelogSection = (input) => {
|
|
102
|
+
const { repoUrl, changes } = input;
|
|
103
|
+
const heading = input.baseTag !== null && repoUrl !== null
|
|
104
|
+
? `## [${input.version}](${repoUrl}/compare/${input.baseTag}...${input.tag}) (${input.date})`
|
|
105
|
+
: `## ${input.version} (${input.date})`;
|
|
106
|
+
const groups = new Map();
|
|
107
|
+
for (const change of changes) {
|
|
108
|
+
const section = SECTIONS.find(([type]) => type === change.type.toLowerCase());
|
|
109
|
+
const listed = section !== undefined &&
|
|
110
|
+
(!section[2] || change.breaking || change.releaseAs !== undefined);
|
|
111
|
+
if (!section || !listed) {
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
const subject = subjectOf(change, repoUrl);
|
|
115
|
+
const link = repoUrl === null || change.sha === undefined
|
|
116
|
+
? ""
|
|
117
|
+
: ` ([${change.sha.slice(0, 7)}](${repoUrl}/commit/${change.sha}))`;
|
|
118
|
+
const entries = groups.get(section[1]) ?? [];
|
|
119
|
+
entries.push({
|
|
120
|
+
key: `${change.scope ?? ""}${subject}`,
|
|
121
|
+
line: `${scoped(change.scope, subject)}${link}${via(change)}`,
|
|
122
|
+
});
|
|
123
|
+
groups.set(section[1], entries);
|
|
124
|
+
}
|
|
125
|
+
const notes = changes
|
|
126
|
+
.filter((c) => c.breaking)
|
|
127
|
+
.map((c) => `${scoped(c.scope, noteOf(c, repoUrl))}${via(c)}`);
|
|
128
|
+
if (input.member && notes.length === 0 && groups.size === 0) {
|
|
129
|
+
const { unit, group } = input.member;
|
|
130
|
+
groups.set("Miscellaneous Chores", [
|
|
131
|
+
{ key: "", line: scoped(unit, `Synchronize ${group} versions`) },
|
|
132
|
+
]);
|
|
133
|
+
}
|
|
134
|
+
let text = `${heading}\n\n`;
|
|
135
|
+
if (notes.length > 0) {
|
|
136
|
+
text += `\n### ⚠ BREAKING CHANGES\n\n${notes.map((n) => `${n}\n`).join("")}`;
|
|
137
|
+
}
|
|
138
|
+
for (const title of TITLES) {
|
|
139
|
+
const entries = groups.get(title);
|
|
140
|
+
if (entries) {
|
|
141
|
+
const lines = entries
|
|
142
|
+
.sort((a, b) => a.key.localeCompare(b.key, "en"))
|
|
143
|
+
.map((e) => `${e.line}\n`);
|
|
144
|
+
text += `\n### ${title}\n\n${lines.join("")}\n`;
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
text = text.trim();
|
|
148
|
+
const deps = input.dependencies ?? [];
|
|
149
|
+
return deps.length > 0
|
|
150
|
+
? `${text}\n\n\n### Dependencies\n\n${dependencyLines(deps)}`
|
|
151
|
+
: text;
|
|
152
|
+
};
|
|
153
|
+
/**
|
|
154
|
+
* The changelog section of a unit with a stable `next` in `plan`, else
|
|
155
|
+
* undefined. Dependencies are direct edges from units of the same release PR,
|
|
156
|
+
* named after the edge reason (`<field>: <name>`, else `dependencies` and the
|
|
157
|
+
* unit id).
|
|
158
|
+
*/
|
|
159
|
+
export const changelogSection = (workspace, plan, opts) => {
|
|
160
|
+
const { id } = opts;
|
|
161
|
+
const inPr = new Set(opts.prUnits);
|
|
162
|
+
const units = new Map(workspace.units.map((u) => [u.id, u]));
|
|
163
|
+
const planned = new Map(plan.units.map((u) => [u.id, u]));
|
|
164
|
+
const entry = planned.get(id);
|
|
165
|
+
const unit = units.get(id);
|
|
166
|
+
if (!entry || !unit || entry.next === null) {
|
|
167
|
+
return undefined;
|
|
168
|
+
}
|
|
169
|
+
const dependencies = workspace.graph.edges.flatMap((edge) => {
|
|
170
|
+
const version = planned.get(edge.from)?.next;
|
|
171
|
+
if (edge.to !== id ||
|
|
172
|
+
!inPr.has(edge.from) ||
|
|
173
|
+
version === null ||
|
|
174
|
+
version === undefined) {
|
|
175
|
+
return [];
|
|
176
|
+
}
|
|
177
|
+
const parsed = edge.reasons.flatMap((r) => {
|
|
178
|
+
const m = EDGE_REASON.exec(r);
|
|
179
|
+
return m?.[1] === undefined || m[2] === undefined
|
|
180
|
+
? []
|
|
181
|
+
: [{ field: m[1], name: m[2], version }];
|
|
182
|
+
});
|
|
183
|
+
return parsed.length > 0
|
|
184
|
+
? parsed
|
|
185
|
+
: [{ field: "dependencies", name: edge.from, version }];
|
|
186
|
+
});
|
|
187
|
+
return renderChangelogSection({
|
|
188
|
+
version: entry.next,
|
|
189
|
+
date: opts.date,
|
|
190
|
+
repoUrl: opts.repoUrl,
|
|
191
|
+
baseTag: entry.baseTag,
|
|
192
|
+
tag: renderTag(unit, entry.next),
|
|
193
|
+
changes: entry.changes,
|
|
194
|
+
dependencies,
|
|
195
|
+
...(entry.group === null
|
|
196
|
+
? {}
|
|
197
|
+
: { member: { unit: id, group: entry.group } }),
|
|
198
|
+
});
|
|
199
|
+
};
|
|
200
|
+
/**
|
|
201
|
+
* Inserts `section` on top of the version sections of `content` (undefined or blank:
|
|
202
|
+
* a new file with `# Changelog`). Version headings are `##` or `###` lines,
|
|
203
|
+
* linked or plain. Sections above `baseVersion` are untagged leftovers and are
|
|
204
|
+
* replaced; without a base version, sections not below the new version are.
|
|
205
|
+
* Text before the first version heading and the other sections stay as they
|
|
206
|
+
* are.
|
|
207
|
+
*/
|
|
208
|
+
export const updateChangelog = (content, section, { baseVersion, scheme, }) => {
|
|
209
|
+
const lines = (content?.trim() ? content : "# Changelog\n").split("\n");
|
|
210
|
+
const base = baseVersion === null ? undefined : scheme.parse(baseVersion);
|
|
211
|
+
const next = headingVersion(section.split("\n")[0] ?? "", scheme);
|
|
212
|
+
const stale = (v) => base !== undefined
|
|
213
|
+
? scheme.compare(v, base) > 0
|
|
214
|
+
: next !== undefined && scheme.compare(v, next) >= 0;
|
|
215
|
+
const preamble = [];
|
|
216
|
+
const kept = [];
|
|
217
|
+
let keep;
|
|
218
|
+
for (const line of lines) {
|
|
219
|
+
const version = headingVersion(line, scheme);
|
|
220
|
+
if (version !== undefined) {
|
|
221
|
+
keep = !stale(version);
|
|
222
|
+
}
|
|
223
|
+
if (keep === undefined) {
|
|
224
|
+
preamble.push(line);
|
|
225
|
+
}
|
|
226
|
+
else if (keep) {
|
|
227
|
+
kept.push(line);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
const rest = kept.join("\n").trim();
|
|
231
|
+
return `${[preamble.join("\n").trimEnd(), section, rest]
|
|
232
|
+
.filter((part) => part !== "")
|
|
233
|
+
.join("\n\n")}\n`;
|
|
234
|
+
};
|
|
235
|
+
/**
|
|
236
|
+
* Top version section of unit `id`'s `<root>/CHANGELOG.md`, heading included,
|
|
237
|
+
* read through `read`; undefined without a file or a version section.
|
|
238
|
+
*/
|
|
239
|
+
export const releaseNotes = (workspace, id, read) => {
|
|
240
|
+
const unit = workspace.units.find((u) => u.id === id);
|
|
241
|
+
const scheme = unit && schemeRegistry(workspace.schemes).get(unit.scheme);
|
|
242
|
+
const content = unit && read(changelogPath(unit));
|
|
243
|
+
if (!scheme || content === undefined) {
|
|
244
|
+
return undefined;
|
|
245
|
+
}
|
|
246
|
+
const section = [];
|
|
247
|
+
for (const line of content.split("\n")) {
|
|
248
|
+
if (headingVersion(line, scheme) !== undefined) {
|
|
249
|
+
if (section.length > 0) {
|
|
250
|
+
break;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
else if (section.length === 0) {
|
|
254
|
+
continue;
|
|
255
|
+
}
|
|
256
|
+
section.push(line);
|
|
257
|
+
}
|
|
258
|
+
return section.length === 0 ? undefined : section.join("\n").trim();
|
|
259
|
+
};
|
package/dist/config.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { BumpLevel, Propagate, VersionSource } from "./plugin.js";
|
|
2
|
+
/** Release PR of a release item: the shared PR or its own. */
|
|
3
|
+
export type ReleasePrMode = "shared" | "separate";
|
|
2
4
|
/** Config overlay for the unit at a root; creates a config-only unit if none is discovered. */
|
|
3
5
|
export interface UnitOverlay {
|
|
4
6
|
id?: string;
|
|
@@ -15,6 +17,18 @@ export interface UnitOverlay {
|
|
|
15
17
|
tags?: string[];
|
|
16
18
|
/** First release version, parsed by the unit's scheme. */
|
|
17
19
|
initialVersion?: string;
|
|
20
|
+
/** Ungrouped released units only; default `defaults.releasePr`. */
|
|
21
|
+
releasePr?: ReleasePrMode;
|
|
22
|
+
/** Templated text replacements written on release. */
|
|
23
|
+
versionFiles?: VersionTemplate[];
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* A text file holding a unit's version. `file` is relative to the repository
|
|
27
|
+
* root; `pattern` uses `{id}`, `{version}`, `{major}`, `{minor}` and `{patch}`.
|
|
28
|
+
*/
|
|
29
|
+
export interface VersionTemplate {
|
|
30
|
+
file: string;
|
|
31
|
+
pattern: string;
|
|
18
32
|
}
|
|
19
33
|
/** A branch rule; the first rule whose `name` matches the branch wins. */
|
|
20
34
|
export interface BranchConfig {
|
|
@@ -45,6 +59,8 @@ export interface GroupConfig {
|
|
|
45
59
|
units?: string[];
|
|
46
60
|
/** Selects released units with any of these tags. */
|
|
47
61
|
tags?: string[];
|
|
62
|
+
/** Default `defaults.releasePr`. */
|
|
63
|
+
releasePr?: ReleasePrMode;
|
|
48
64
|
}
|
|
49
65
|
export interface DepConfig {
|
|
50
66
|
from: string;
|
|
@@ -64,6 +80,8 @@ export interface TallyConfig {
|
|
|
64
80
|
tagFormat?: string;
|
|
65
81
|
propagate?: Propagate;
|
|
66
82
|
initialVersion?: string;
|
|
83
|
+
/** Default `shared`. */
|
|
84
|
+
releasePr?: ReleasePrMode;
|
|
67
85
|
};
|
|
68
86
|
/** Overlays keyed by unit root. */
|
|
69
87
|
units?: Record<string, UnitOverlay>;
|
|
@@ -74,6 +92,10 @@ export interface TallyConfig {
|
|
|
74
92
|
bumps?: BumpsConfig;
|
|
75
93
|
/** Fixed groups by name; names follow the unit id pattern. */
|
|
76
94
|
groups?: Record<string, GroupConfig>;
|
|
95
|
+
tags?: {
|
|
96
|
+
/** Floating tag formats with `{id}`, `{major}` and `{minor}`. */
|
|
97
|
+
floating?: string[];
|
|
98
|
+
};
|
|
77
99
|
}
|
|
78
100
|
/** Global input owned by core. */
|
|
79
101
|
export declare const CONFIG_FILE = "tallyrelease.yaml";
|
package/dist/conventional.js
CHANGED
|
@@ -76,6 +76,11 @@ export const conventionalCommits = {
|
|
|
76
76
|
collect: (_ctx, { commits }) => commits.map((commit) => {
|
|
77
77
|
const parsed = parseCommit(commit.message);
|
|
78
78
|
const values = parsed.footers["release-as"] ?? [];
|
|
79
|
+
// The last breaking footer wins, as in release-please.
|
|
80
|
+
const notes = [
|
|
81
|
+
...(parsed.footers["breaking change"] ?? []),
|
|
82
|
+
...(parsed.footers["breaking-change"] ?? []),
|
|
83
|
+
].at(-1);
|
|
79
84
|
return {
|
|
80
85
|
sha: commit.sha,
|
|
81
86
|
type: parsed.type,
|
|
@@ -83,6 +88,7 @@ export const conventionalCommits = {
|
|
|
83
88
|
...(parsed.scope === undefined ? {} : { scope: parsed.scope }),
|
|
84
89
|
subject: parsed.subject,
|
|
85
90
|
...(parsed.body === undefined ? {} : { body: parsed.body }),
|
|
91
|
+
...(notes === undefined ? {} : { notes }),
|
|
86
92
|
...(parsed.pr === undefined ? {} : { pr: parsed.pr }),
|
|
87
93
|
...(values.length === 0 ? {} : { releaseAs: values.map(releaseAs) }),
|
|
88
94
|
};
|
package/dist/discover.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Unit } from "./plugin.js";
|
|
2
2
|
import type { ActivePlugin, PluginEnv } from "./plugins.js";
|
|
3
|
+
/** Drops `.` and empty segments; rejects absolute roots and `..` segments. */
|
|
4
|
+
export declare const normalizeRoot: (root: string) => string;
|
|
3
5
|
/**
|
|
4
6
|
* Runs `discover` hooks in config order, applies config overlays and fills
|
|
5
7
|
* defaults. Returns units sorted by id.
|
package/dist/discover.js
CHANGED
|
@@ -4,17 +4,17 @@ import { runHook } from "./plugins.js";
|
|
|
4
4
|
import { compare, uniqueSorted } from "./util.js";
|
|
5
5
|
const UNIT_ID = /^(?!\.\.?$)[A-Za-z0-9._-]+$/;
|
|
6
6
|
const UNIT_TAG = /^[A-Za-z0-9._-]+$/;
|
|
7
|
+
/** Escapes glob metacharacters so a root matches literally. */
|
|
8
|
+
const escapeGlob = (path) => path.replace(/[[\]{}()!*?+@\\]/g, "\\$&");
|
|
9
|
+
const isInside = (path, root) => path !== root && (root === "." || path.startsWith(`${root}/`));
|
|
7
10
|
/** Drops `.` and empty segments; rejects absolute roots and `..` segments. */
|
|
8
|
-
const normalizeRoot = (root) => {
|
|
11
|
+
export const normalizeRoot = (root) => {
|
|
9
12
|
const segments = root.split("/");
|
|
10
13
|
if (root.startsWith("/") || segments.includes("..")) {
|
|
11
14
|
throw new TallyError("E_ROOT_INVALID", `root "${root}" must be repo-relative without ".." segments`, { root });
|
|
12
15
|
}
|
|
13
16
|
return segments.filter((s) => s !== "" && s !== ".").join("/") || ".";
|
|
14
17
|
};
|
|
15
|
-
/** Escapes glob metacharacters so a root matches literally. */
|
|
16
|
-
const escapeGlob = (path) => path.replace(/[[\]{}()!*?+@\\]/g, "\\$&");
|
|
17
|
-
const isInside = (path, root) => path !== root && (root === "." || path.startsWith(`${root}/`));
|
|
18
18
|
/**
|
|
19
19
|
* Runs `discover` hooks in config order, applies config overlays and fills
|
|
20
20
|
* defaults. Returns units sorted by id.
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { Unit, VersionScheme } from "./plugin.js";
|
|
2
|
+
/** Throws `E_CONFIG` for a floating tag format with an unknown token. */
|
|
3
|
+
export declare const validateFloating: (formats: readonly string[]) => void;
|
|
4
|
+
/**
|
|
5
|
+
* Floating tags `version` takes: per format, the rendered name when `version`
|
|
6
|
+
* is stable and not lower than any stable version among the unit's `tags`
|
|
7
|
+
* (tag names, others ignored) that renders to the same name. In format order,
|
|
8
|
+
* without duplicates.
|
|
9
|
+
*/
|
|
10
|
+
export declare const renderFloatingTags: <V>(unit: Pick<Unit, "id" | "tagFormat">, version: string, { tags, scheme, formats, }: {
|
|
11
|
+
tags: readonly string[];
|
|
12
|
+
scheme: VersionScheme<V>;
|
|
13
|
+
formats: readonly string[];
|
|
14
|
+
}) => string[];
|
package/dist/floating.js
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import { TallyError } from "./errors.js";
|
|
2
|
+
import { compileTagFormat } from "./tags.js";
|
|
3
|
+
const TOKEN = /\{([^{}]*)\}/gu;
|
|
4
|
+
const TOKENS = new Set(["id", "major", "minor"]);
|
|
5
|
+
/** Throws `E_CONFIG` for a floating tag format with an unknown token. */
|
|
6
|
+
export const validateFloating = (formats) => {
|
|
7
|
+
for (const format of formats) {
|
|
8
|
+
const unknown = [...format.matchAll(TOKEN)].find(([, token]) => !TOKENS.has(token ?? ""));
|
|
9
|
+
if (unknown) {
|
|
10
|
+
throw new TallyError("E_CONFIG", `floating tag format "${format}" uses unknown token "${unknown[0]}"; use {id}, {major} or {minor}`, { format });
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
};
|
|
14
|
+
/**
|
|
15
|
+
* Floating tags `version` takes: per format, the rendered name when `version`
|
|
16
|
+
* is stable and not lower than any stable version among the unit's `tags`
|
|
17
|
+
* (tag names, others ignored) that renders to the same name. In format order,
|
|
18
|
+
* without duplicates.
|
|
19
|
+
*/
|
|
20
|
+
export const renderFloatingTags = (unit, version, { tags, scheme, formats, }) => {
|
|
21
|
+
const released = scheme.parse(version);
|
|
22
|
+
if (released === undefined || scheme.isPrerelease(released)) {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
const format = compileTagFormat(unit.tagFormat, unit.id);
|
|
26
|
+
const versions = tags.flatMap((tag) => {
|
|
27
|
+
const raw = format.match(tag);
|
|
28
|
+
const v = raw === undefined ? undefined : scheme.parse(raw);
|
|
29
|
+
return v === undefined || scheme.isPrerelease(v) ? [] : [v];
|
|
30
|
+
});
|
|
31
|
+
const render = (pattern, v) => {
|
|
32
|
+
const { major, minor } = scheme.core(v);
|
|
33
|
+
return pattern
|
|
34
|
+
.replaceAll("{id}", unit.id)
|
|
35
|
+
.replaceAll("{major}", String(major))
|
|
36
|
+
.replaceAll("{minor}", String(minor));
|
|
37
|
+
};
|
|
38
|
+
const names = formats.flatMap((pattern) => {
|
|
39
|
+
const name = render(pattern, released);
|
|
40
|
+
const highest = versions.every((v) => render(pattern, v) !== name || scheme.compare(v, released) <= 0);
|
|
41
|
+
return highest ? [name] : [];
|
|
42
|
+
});
|
|
43
|
+
return [...new Set(names)];
|
|
44
|
+
};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,28 +1,37 @@
|
|
|
1
1
|
export declare const version = "0.0.0";
|
|
2
2
|
export type { AffectedDoc, AffectedOptions, AffectedReason, AffectedUnit, BaseSource, } from "./affected.js";
|
|
3
|
+
export type { ChangelogDependency, ChangelogInput } from "./changelog.js";
|
|
3
4
|
export type { ParsedCommit } from "./conventional.js";
|
|
4
5
|
export type { ExplainedChange, ExplainOptions, ReleaseAsFooter, UnitExplanation, } from "./explain.js";
|
|
5
6
|
export type { GroupPlan, Plan, PlanChange, PlanLine, PlanOptions, ReleasedUnit, UnitPlan, } from "./plan.js";
|
|
6
|
-
export type { DepConfig, GroupConfig, TallyConfig, UnitOverlay, } from "./config.js";
|
|
7
|
+
export type { DepConfig, GroupConfig, ReleasePrMode, TallyConfig, UnitOverlay, VersionTemplate, } from "./config.js";
|
|
7
8
|
export type { Warning } from "./errors.js";
|
|
8
9
|
export type { Graph, SourcedEdge } from "./graph.js";
|
|
9
10
|
export type { Group } from "./groups.js";
|
|
10
11
|
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";
|
|
12
|
+
export type { Marker, MarkerRelease } from "./marker.js";
|
|
11
13
|
export type { ActivePlugin, PluginEnv } from "./plugins.js";
|
|
14
|
+
export type { ReleaseItem } from "./release-items.js";
|
|
12
15
|
export type { BranchRule, ReleaseSettings } from "./release.js";
|
|
13
16
|
export type { SchemeEntry } from "./schemes.js";
|
|
14
17
|
export type { SemVer } from "./semver.js";
|
|
15
18
|
export type { IntentEntry, Workspace, WorkspaceInput } from "./workspace.js";
|
|
16
19
|
export { computeAffected } from "./affected.js";
|
|
20
|
+
export { changelogSection, releaseNotes, renderChangelogSection, updateChangelog, } from "./changelog.js";
|
|
17
21
|
export { CONFIG_FILE } from "./config.js";
|
|
18
22
|
export { conventionalCommits, parseCommit } from "./conventional.js";
|
|
19
23
|
export { discoverUnits } from "./discover.js";
|
|
20
24
|
export { TallyError } from "./errors.js";
|
|
21
25
|
export { explainUnit } from "./explain.js";
|
|
26
|
+
export { renderFloatingTags } from "./floating.js";
|
|
22
27
|
export { buildGraph } from "./graph.js";
|
|
23
28
|
export { definePlugin, defaultUnitId, PLUGIN_API_VERSION } from "./plugin.js";
|
|
24
29
|
export { computePlan } from "./plan.js";
|
|
30
|
+
export { decodeMarker, encodeMarker } from "./marker.js";
|
|
25
31
|
export { activatePlugins } from "./plugins.js";
|
|
32
|
+
export { planEquals, releaseEntries, releaseItems } from "./release-items.js";
|
|
33
|
+
export { highestReleases, releasedFromTags, withReleased } from "./released.js";
|
|
26
34
|
export { semver } from "./semver.js";
|
|
27
35
|
export { validateRelease, validateTags } from "./validate.js";
|
|
36
|
+
export { releaseFiles, versionFileClaims } from "./version-files.js";
|
|
28
37
|
export { loadWorkspace } from "./workspace.js";
|
package/dist/index.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
export const version = "0.0.0";
|
|
2
2
|
export { computeAffected } from "./affected.js";
|
|
3
|
+
export { changelogSection, releaseNotes, renderChangelogSection, updateChangelog, } from "./changelog.js";
|
|
3
4
|
export { CONFIG_FILE } from "./config.js";
|
|
4
5
|
export { conventionalCommits, parseCommit } from "./conventional.js";
|
|
5
6
|
export { discoverUnits } from "./discover.js";
|
|
6
7
|
export { TallyError } from "./errors.js";
|
|
7
8
|
export { explainUnit } from "./explain.js";
|
|
9
|
+
export { renderFloatingTags } from "./floating.js";
|
|
8
10
|
export { buildGraph } from "./graph.js";
|
|
9
11
|
export { definePlugin, defaultUnitId, PLUGIN_API_VERSION } from "./plugin.js";
|
|
10
12
|
export { computePlan } from "./plan.js";
|
|
13
|
+
export { decodeMarker, encodeMarker } from "./marker.js";
|
|
11
14
|
export { activatePlugins } from "./plugins.js";
|
|
15
|
+
export { planEquals, releaseEntries, releaseItems } from "./release-items.js";
|
|
16
|
+
export { highestReleases, releasedFromTags, withReleased } from "./released.js";
|
|
12
17
|
export { semver } from "./semver.js";
|
|
13
18
|
export { validateRelease, validateTags } from "./validate.js";
|
|
19
|
+
export { releaseFiles, versionFileClaims } from "./version-files.js";
|
|
14
20
|
export { loadWorkspace } from "./workspace.js";
|
package/dist/marker.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/** A release recorded in a release PR marker. */
|
|
2
|
+
export interface MarkerRelease {
|
|
3
|
+
id: string;
|
|
4
|
+
version: string;
|
|
5
|
+
tag: string;
|
|
6
|
+
}
|
|
7
|
+
/** Release PR marker, version 1. */
|
|
8
|
+
export interface Marker {
|
|
9
|
+
/** Line head the release commit was built on. */
|
|
10
|
+
head: string;
|
|
11
|
+
releaseCommit: string;
|
|
12
|
+
releases: MarkerRelease[];
|
|
13
|
+
}
|
|
14
|
+
/** `<!-- tallyrelease-plan:v1 <base64 json> -->` holding only the marker fields. */
|
|
15
|
+
export declare const encodeMarker: ({ head, releaseCommit, releases, }: Marker) => string;
|
|
16
|
+
/** The marker in a PR body; undefined when missing or malformed. */
|
|
17
|
+
export declare const decodeMarker: (body: string) => Marker | undefined;
|