@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/dist/marker.js ADDED
@@ -0,0 +1,48 @@
1
+ import { decodeBase64, encodeBase64 } from "./base64.js";
2
+ const MARKER = /<!-- tallyrelease-plan:v1 ([A-Za-z0-9+/=]*) -->/u;
3
+ const isRelease = (value) => {
4
+ const r = value;
5
+ return (typeof r === "object" &&
6
+ r !== null &&
7
+ typeof r.id === "string" &&
8
+ typeof r.version === "string" &&
9
+ typeof r.tag === "string");
10
+ };
11
+ /** `<!-- tallyrelease-plan:v1 <base64 json> -->` holding only the marker fields. */
12
+ export const encodeMarker = ({ head, releaseCommit, releases, }) => {
13
+ const json = JSON.stringify({
14
+ head,
15
+ releaseCommit,
16
+ releases: releases.map(({ id, version, tag }) => ({ id, version, tag })),
17
+ });
18
+ return `<!-- tallyrelease-plan:v1 ${encodeBase64(json)} -->`;
19
+ };
20
+ /** The marker in a PR body; undefined when missing or malformed. */
21
+ export const decodeMarker = (body) => {
22
+ const data = MARKER.exec(body)?.[1];
23
+ const json = data === undefined ? undefined : decodeBase64(data);
24
+ if (json === undefined) {
25
+ return undefined;
26
+ }
27
+ let value;
28
+ try {
29
+ value = JSON.parse(json);
30
+ }
31
+ catch {
32
+ return undefined;
33
+ }
34
+ const m = value;
35
+ if (typeof m !== "object" ||
36
+ m === null ||
37
+ typeof m.head !== "string" ||
38
+ typeof m.releaseCommit !== "string" ||
39
+ !Array.isArray(m.releases) ||
40
+ !m.releases.every(isRelease)) {
41
+ return undefined;
42
+ }
43
+ return {
44
+ head: m.head,
45
+ releaseCommit: m.releaseCommit,
46
+ releases: m.releases.map(({ id, version, tag }) => ({ id, version, tag })),
47
+ };
48
+ };
package/dist/plan.d.ts CHANGED
@@ -11,6 +11,8 @@ export interface PlanChange {
11
11
  breaking: boolean;
12
12
  scope?: string;
13
13
  subject: string;
14
+ /** `BREAKING CHANGE` footer text. */
15
+ notes?: string;
14
16
  pr?: number;
15
17
  /** Absorbed unit the change came through. */
16
18
  via?: string;
package/dist/plan.js CHANGED
@@ -34,6 +34,7 @@ const planChanges = (intent) => (intent?.changes ?? []).map(({ change, via }) =>
34
34
  breaking: change.breaking,
35
35
  ...(change.scope === undefined ? {} : { scope: change.scope }),
36
36
  subject: change.subject,
37
+ ...(change.notes === undefined ? {} : { notes: change.notes }),
37
38
  ...(change.pr === undefined ? {} : { pr: change.pr }),
38
39
  ...(via === undefined ? {} : { via }),
39
40
  ...(releaseAs === undefined ? {} : { releaseAs }),
package/dist/plugin.d.ts CHANGED
@@ -125,6 +125,8 @@ export interface VersionFile {
125
125
  }
126
126
  export interface WriteCtx {
127
127
  unit: Unit;
128
+ /** All units of the workspace. */
129
+ units: readonly Unit[];
128
130
  /** Unit id to version in the current plan. */
129
131
  versions: ReadonlyMap<string, string>;
130
132
  }
@@ -183,6 +185,8 @@ export interface Change {
183
185
  scope?: string;
184
186
  subject: string;
185
187
  body?: string;
188
+ /** `BREAKING CHANGE` footer text. */
189
+ notes?: string;
186
190
  releaseAs?: ReleaseAsValue[];
187
191
  pr?: number;
188
192
  date?: string;
@@ -1,5 +1,11 @@
1
1
  import type { Graph } from "./graph.js";
2
- import type { BumpLevel, Unit } from "./plugin.js";
2
+ import type { BumpLevel, Edge, Unit } from "./plugin.js";
3
+ /**
4
+ * Edges between released units, `to` to `from` to `pinned`. A path through
5
+ * unreleased units counts as an edge with the first hop's `pinned`; several
6
+ * paths are OR-combined.
7
+ */
8
+ export declare const releasedEdges: (edges: readonly Edge[], released: ReadonlySet<string>) => Map<string, Map<string, boolean>>;
3
9
  /** Bump state of a released unit. */
4
10
  export interface Pending {
5
11
  readonly bump: BumpLevel | null;
package/dist/propagate.js CHANGED
@@ -4,7 +4,7 @@ import { maxLevel, uniqueSorted } from "./util.js";
4
4
  * unreleased units counts as an edge with the first hop's `pinned`; several
5
5
  * paths are OR-combined.
6
6
  */
7
- const releasedEdges = (edges, released) => {
7
+ export const releasedEdges = (edges, released) => {
8
8
  const out = new Map();
9
9
  for (const e of edges) {
10
10
  const list = out.get(e.from) ?? [];
@@ -0,0 +1,35 @@
1
+ import type { ReleasePrMode } from "./config.js";
2
+ import type { MarkerRelease } from "./marker.js";
3
+ import type { Plan } from "./plan.js";
4
+ import type { Workspace } from "./workspace.js";
5
+ /** A group or an ungrouped released unit; releases as one. */
6
+ export interface ReleaseItem {
7
+ name: string;
8
+ kind: "unit" | "group";
9
+ mode: ReleasePrMode;
10
+ /** Sorted member ids; the unit itself for kind `unit`. */
11
+ units: string[];
12
+ /** Stable `next`. */
13
+ version: string;
14
+ /** Version tag of each member, in `units` order. */
15
+ tags: {
16
+ id: string;
17
+ tag: string;
18
+ }[];
19
+ }
20
+ /** Pending release items (stable `next`) in plan unit order. */
21
+ export declare const releaseItems: (workspace: Workspace, plan: Plan) => ReleaseItem[];
22
+ /** One release per item member. */
23
+ export declare const releaseEntries: (items: readonly ReleaseItem[]) => MarkerRelease[];
24
+ /**
25
+ * True when every marker release equals the plan's release of that unit and
26
+ * every item the marker touches is complete. Pending items outside the marker
27
+ * are ignored; an empty marker never equals.
28
+ */
29
+ export declare const planEquals: (marker: readonly MarkerRelease[], plan: Plan, workspace: Workspace) => boolean;
30
+ /**
31
+ * Release PR checks: `releasePr` only on ungrouped units (`E_CONFIG`), item
32
+ * names (`E_RELEASE_ITEM_NAME`) and patch-propagating edges between items in
33
+ * different release PRs (`E_RELEASE_PR_SPLIT_EDGE`).
34
+ */
35
+ export declare const validateReleaseItems: (workspace: Workspace) => void;
@@ -0,0 +1,120 @@
1
+ import { TallyError } from "./errors.js";
2
+ import { releasedEdges } from "./propagate.js";
3
+ import { renderTag } from "./tags.js";
4
+ /** Item of each released unit, by unit id. */
5
+ const itemsByUnit = (workspace) => {
6
+ const { releasePr } = workspace.release;
7
+ const byUnit = new Map();
8
+ for (const { name, members } of workspace.groups) {
9
+ const item = {
10
+ name,
11
+ kind: "group",
12
+ mode: releasePr.groups.get(name) ?? releasePr.default,
13
+ units: [...members],
14
+ };
15
+ for (const id of members) {
16
+ byUnit.set(id, item);
17
+ }
18
+ }
19
+ for (const { id, version } of workspace.units) {
20
+ if (version !== "none" && !byUnit.has(id)) {
21
+ byUnit.set(id, {
22
+ name: id,
23
+ kind: "unit",
24
+ mode: releasePr.units.get(id) ?? releasePr.default,
25
+ units: [id],
26
+ });
27
+ }
28
+ }
29
+ return byUnit;
30
+ };
31
+ /** Why `name` cannot name a release item, else undefined. */
32
+ const itemNameProblem = (name) => {
33
+ if (name === "shared") {
34
+ return "is reserved for the shared release PR";
35
+ }
36
+ return name.startsWith(".") ||
37
+ name.endsWith(".") ||
38
+ name.endsWith(".lock") ||
39
+ name.includes("..")
40
+ ? "does not form a valid branch name"
41
+ : undefined;
42
+ };
43
+ /** Pending release items (stable `next`) in plan unit order. */
44
+ export const releaseItems = (workspace, plan) => {
45
+ const units = new Map(workspace.units.map((u) => [u.id, u]));
46
+ const byUnit = itemsByUnit(workspace);
47
+ const items = new Map();
48
+ for (const { id, next } of plan.units) {
49
+ const item = byUnit.get(id);
50
+ if (next === null || !item || items.has(item.name)) {
51
+ continue;
52
+ }
53
+ items.set(item.name, {
54
+ ...item,
55
+ version: next,
56
+ tags: item.units.flatMap((u) => {
57
+ const unit = units.get(u);
58
+ return unit ? [{ id: u, tag: renderTag(unit, next) }] : [];
59
+ }),
60
+ });
61
+ }
62
+ return [...items.values()];
63
+ };
64
+ /** One release per item member. */
65
+ export const releaseEntries = (items) => items.flatMap((item) => item.tags.map(({ id, tag }) => ({ id, version: item.version, tag })));
66
+ /**
67
+ * True when every marker release equals the plan's release of that unit and
68
+ * every item the marker touches is complete. Pending items outside the marker
69
+ * are ignored; an empty marker never equals.
70
+ */
71
+ export const planEquals = (marker, plan, workspace) => {
72
+ const items = releaseItems(workspace, plan);
73
+ const entries = new Map(releaseEntries(items).map((e) => [e.id, e]));
74
+ const ids = new Set(marker.map((r) => r.id));
75
+ return (marker.length > 0 &&
76
+ marker.every((r) => {
77
+ const entry = entries.get(r.id);
78
+ return entry?.version === r.version && entry.tag === r.tag;
79
+ }) &&
80
+ items.every((item) => item.units.every((id) => ids.has(id)) ||
81
+ !item.units.some((id) => ids.has(id))));
82
+ };
83
+ /**
84
+ * Release PR checks: `releasePr` only on ungrouped units (`E_CONFIG`), item
85
+ * names (`E_RELEASE_ITEM_NAME`) and patch-propagating edges between items in
86
+ * different release PRs (`E_RELEASE_PR_SPLIT_EDGE`).
87
+ */
88
+ export const validateReleaseItems = (workspace) => {
89
+ const byUnit = itemsByUnit(workspace);
90
+ for (const id of workspace.release.releasePr.units.keys()) {
91
+ const item = byUnit.get(id);
92
+ if (item?.kind === "group") {
93
+ throw new TallyError("E_CONFIG", `unit "${id}" sets releasePr but is in group "${item.name}"; set it on the group`, { unit: id, group: item.name });
94
+ }
95
+ }
96
+ const groups = new Set(workspace.groups.map((g) => g.name));
97
+ for (const item of new Set(byUnit.values())) {
98
+ const problem = item.kind === "unit" && groups.has(item.name)
99
+ ? "is also a group name"
100
+ : itemNameProblem(item.name);
101
+ if (problem !== undefined) {
102
+ throw new TallyError("E_RELEASE_ITEM_NAME", `release item "${item.name}" ${problem}`, { item: item.name });
103
+ }
104
+ }
105
+ const policies = new Map(workspace.units.map((u) => [u.id, u.propagate]));
106
+ const pr = (item) => item.mode === "shared" ? "shared" : item.name;
107
+ const edges = releasedEdges(workspace.graph.edges, new Set(byUnit.keys()));
108
+ for (const [to, incoming] of edges) {
109
+ const policy = policies.get(to) ?? "auto";
110
+ for (const [from, pinned] of incoming) {
111
+ const a = byUnit.get(from);
112
+ const b = byUnit.get(to);
113
+ const patch = policy === "auto" ? pinned : policy === "patch";
114
+ if (!a || !b || !patch || pr(a) === pr(b)) {
115
+ continue;
116
+ }
117
+ throw new TallyError("E_RELEASE_PR_SPLIT_EDGE", `edge "${from}" -> "${to}" propagates a patch between release items "${a.name}" and "${b.name}" in different release PRs`, { from, to, items: [a.name, b.name] });
118
+ }
119
+ }
120
+ };
package/dist/release.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { TallyConfig } from "./config.js";
1
+ import type { ReleasePrMode, TallyConfig, VersionTemplate } from "./config.js";
2
2
  import type { Warning } from "./errors.js";
3
3
  import type { GroupFacts, RangeFacts, UnitFacts } from "./facts.js";
4
4
  import type { BumpLevel, Change, Unit, VersionScheme } from "./plugin.js";
@@ -38,6 +38,17 @@ export interface ReleaseSettings {
38
38
  readonly absorbed: "patch" | "minor";
39
39
  /** In config order. */
40
40
  readonly branches: readonly BranchRule[];
41
+ readonly releasePr: {
42
+ readonly default: ReleasePrMode;
43
+ /** Set by unit overlays, by unit id. */
44
+ readonly units: ReadonlyMap<string, ReleasePrMode>;
45
+ /** Set by groups, by group name. */
46
+ readonly groups: ReadonlyMap<string, ReleasePrMode>;
47
+ };
48
+ /** Floating tag formats. */
49
+ readonly floating: readonly string[];
50
+ /** Version file templates by unit id. */
51
+ readonly templates: ReadonlyMap<string, readonly VersionTemplate[]>;
41
52
  }
42
53
  /**
43
54
  * A Change of a released unit; `via` names the first absorbed unit (by id) a
@@ -93,7 +104,7 @@ export interface ReleaseInput<V> {
93
104
  */
94
105
  export declare const reach: (id: string, dependencies: ReadonlyMap<string, readonly string[]>, released: ReadonlySet<string>) => Set<string>;
95
106
  /** Applies the defaults to `bumps` and `branches`. */
96
- export declare const releaseSettings: (config: TallyConfig) => ReleaseSettings;
107
+ export declare const releaseSettings: (config: TallyConfig, units: readonly Unit[]) => ReleaseSettings;
97
108
  /**
98
109
  * Warns for qualified Release-As values naming unknown or unreleased units and
99
110
  * for plain values on commits attributed to no released unit.
package/dist/release.js CHANGED
@@ -1,3 +1,4 @@
1
+ import { normalizeRoot } from "./discover.js";
1
2
  import { compare, maxLevel, uniqueSorted } from "./util.js";
2
3
  const DEFAULT_TYPES = {
3
4
  feat: "minor",
@@ -189,7 +190,7 @@ export const reach = (id, dependencies, released) => {
189
190
  return found;
190
191
  };
191
192
  /** Applies the defaults to `bumps` and `branches`. */
192
- export const releaseSettings = (config) => ({
193
+ export const releaseSettings = (config, units) => ({
193
194
  bumps: new Map(Object.entries({ ...DEFAULT_TYPES, ...config.bumps?.types })),
194
195
  absorbed: config.bumps?.absorbed ?? "patch",
195
196
  preMajor: {
@@ -203,6 +204,23 @@ export const releaseSettings = (config) => ({
203
204
  unit: b.unit ?? null,
204
205
  group: b.group ?? null,
205
206
  })),
207
+ releasePr: {
208
+ default: config.defaults?.releasePr ?? "shared",
209
+ units: new Map(Object.entries(config.units ?? {}).flatMap(([root, overlay]) => {
210
+ const unit = units.find((u) => u.root === normalizeRoot(root));
211
+ return unit && overlay.releasePr
212
+ ? [[unit.id, overlay.releasePr]]
213
+ : [];
214
+ })),
215
+ groups: new Map(Object.entries(config.groups ?? {}).flatMap(([name, group]) => group.releasePr ? [[name, group.releasePr]] : [])),
216
+ },
217
+ floating: config.tags?.floating ?? [],
218
+ templates: new Map(Object.entries(config.units ?? {}).flatMap(([root, overlay]) => {
219
+ const unit = units.find((u) => u.root === normalizeRoot(root));
220
+ return unit && overlay.versionFiles
221
+ ? [[unit.id, overlay.versionFiles]]
222
+ : [];
223
+ })),
206
224
  });
207
225
  /**
208
226
  * Warns for qualified Release-As values naming unknown or unreleased units and
@@ -0,0 +1,25 @@
1
+ import type { Plan, ReleasedUnit } from "./plan.js";
2
+ import type { GitReader } from "./plugin.js";
3
+ import type { Workspace } from "./workspace.js";
4
+ /**
5
+ * `plan` with `released`: stable unit tags whose commit is on HEAD's
6
+ * first-parent chain and not an ancestor of `build.base`. With a null base
7
+ * only tags at HEAD, warning `W_RELEASED_NO_BASE` when earlier releases on the
8
+ * chain are left out.
9
+ */
10
+ export declare const withReleased: (workspace: Workspace, plan: Plan, { git }: {
11
+ git: GitReader;
12
+ }) => Promise<Plan>;
13
+ /**
14
+ * Released entries of the named tags, in `plan.released` order; names that
15
+ * are not stable tags of a released unit are skipped. Callers add the tags a
16
+ * reconcile created by passing the union of those and `plan.released` tags.
17
+ */
18
+ export declare const releasedFromTags: (workspace: Workspace, plan: Plan, { git, tags }: {
19
+ git: GitReader;
20
+ tags: readonly string[];
21
+ }) => Promise<ReleasedUnit[]>;
22
+ /** Tag of the highest stable version of each released unit with one. */
23
+ export declare const highestReleases: (workspace: Workspace, { git }: {
24
+ git: GitReader;
25
+ }) => Promise<Set<string>>;
@@ -0,0 +1,104 @@
1
+ import { renderFloatingTags } from "./floating.js";
2
+ import { schemeRegistry } from "./schemes.js";
3
+ import { matchUnitTags } from "./tags.js";
4
+ import { compare } from "./util.js";
5
+ const stable = (scheme, version) => {
6
+ const v = scheme.parse(version);
7
+ return v !== undefined && !scheme.isPrerelease(v);
8
+ };
9
+ /**
10
+ * Released entries of the stable tags `keep` accepts: oldest release commit
11
+ * on the first-parent chain first, then plan unit order (topological), then
12
+ * tag name.
13
+ */
14
+ const releasedEntries = async (workspace, plan, { git, keep, }) => {
15
+ const matched = matchUnitTags(workspace.units, await git.tags(), schemeRegistry(workspace.schemes));
16
+ const line = plan.line?.kind ?? "default";
17
+ const candidates = workspace.units.flatMap((unit) => {
18
+ const m = matched.get(unit.id);
19
+ if (!m) {
20
+ return [];
21
+ }
22
+ const names = m.tags.map((t) => t.name);
23
+ return m.tags
24
+ .filter((tag) => stable(m.scheme, tag.version))
25
+ .map((tag) => ({
26
+ id: unit.id,
27
+ path: unit.root,
28
+ version: tag.version,
29
+ tag: tag.name,
30
+ floatingTags: renderFloatingTags(unit, tag.version, {
31
+ tags: names,
32
+ scheme: m.scheme,
33
+ formats: workspace.release.floating,
34
+ }),
35
+ releaseSha: tag.sha,
36
+ catchUp: tag.sha !== plan.head,
37
+ pr: null,
38
+ line,
39
+ }));
40
+ });
41
+ const kept = await Promise.all(candidates.map(async (r) => await keep({ name: r.tag, sha: r.releaseSha, version: r.version })));
42
+ const released = candidates.filter((_, i) => kept[i]);
43
+ const shas = [...new Set(released.map((r) => r.releaseSha))];
44
+ const depths = await Promise.all(shas.map(async (sha) => await git.countFirstParent(null, sha)));
45
+ const depth = new Map(shas.map((sha, i) => [sha, depths[i] ?? 0]));
46
+ const rank = new Map(workspace.graph.order.map((id, i) => [id, i]));
47
+ return released.sort((a, b) => (depth.get(a.releaseSha) ?? 0) - (depth.get(b.releaseSha) ?? 0) ||
48
+ (rank.get(a.id) ?? 0) - (rank.get(b.id) ?? 0) ||
49
+ compare(a.tag, b.tag));
50
+ };
51
+ /**
52
+ * `plan` with `released`: stable unit tags whose commit is on HEAD's
53
+ * first-parent chain and not an ancestor of `build.base`. With a null base
54
+ * only tags at HEAD, warning `W_RELEASED_NO_BASE` when earlier releases on the
55
+ * chain are left out.
56
+ */
57
+ export const withReleased = async (workspace, plan, { git }) => {
58
+ const { head } = plan;
59
+ const { base } = plan.build;
60
+ const atBase = base === null
61
+ ? new Set()
62
+ : new Set((await git.tagsMerged(base)).map((t) => t.name));
63
+ let omitted = 0;
64
+ const released = await releasedEntries(workspace, plan, {
65
+ git,
66
+ keep: async (tag) => {
67
+ if (atBase.has(tag.name) ||
68
+ !(await git.isFirstParentAncestor(tag.sha, head))) {
69
+ return false;
70
+ }
71
+ if (base === null && tag.sha !== head) {
72
+ omitted += 1;
73
+ return false;
74
+ }
75
+ return true;
76
+ },
77
+ });
78
+ const warnings = omitted === 0
79
+ ? []
80
+ : [
81
+ {
82
+ code: "W_RELEASED_NO_BASE",
83
+ message: "the build base is unknown; released leaves out earlier releases on the first-parent chain",
84
+ },
85
+ ];
86
+ return { ...plan, released, warnings: [...plan.warnings, ...warnings] };
87
+ };
88
+ /**
89
+ * Released entries of the named tags, in `plan.released` order; names that
90
+ * are not stable tags of a released unit are skipped. Callers add the tags a
91
+ * reconcile created by passing the union of those and `plan.released` tags.
92
+ */
93
+ export const releasedFromTags = async (workspace, plan, { git, tags }) => {
94
+ const names = new Set(tags);
95
+ return await releasedEntries(workspace, plan, {
96
+ git,
97
+ keep: (tag) => names.has(tag.name),
98
+ });
99
+ };
100
+ /** Tag of the highest stable version of each released unit with one. */
101
+ export const highestReleases = async (workspace, { git }) => {
102
+ const matched = matchUnitTags(workspace.units, await git.tags(), schemeRegistry(workspace.schemes));
103
+ return new Set([...matched.values()].flatMap(({ scheme, tags }) => tags.findLast((t) => stable(scheme, t.version))?.name ?? []));
104
+ };
package/dist/tags.d.ts CHANGED
@@ -15,6 +15,8 @@ export interface UnitTag {
15
15
  }
16
16
  /** Compiles `format`; literal parts and `{id}` are escaped, `{version}` must occur once. */
17
17
  export declare const compileTagFormat: (format: string, unitId: string) => TagFormat;
18
+ /** The unit's tag for `version`. */
19
+ export declare const renderTag: (unit: Pick<Unit, "id" | "tagFormat">, version: string) => string;
18
20
  /** Tags of a released unit and its scheme. */
19
21
  export interface UnitTags {
20
22
  readonly scheme: VersionScheme<any>;
package/dist/tags.js CHANGED
@@ -15,6 +15,10 @@ export const compileTagFormat = (format, unitId) => {
15
15
  match: (tag) => re.exec(tag)?.[1],
16
16
  };
17
17
  };
18
+ /** The unit's tag for `version`. */
19
+ export const renderTag = (unit, version) => unit.tagFormat
20
+ .replaceAll("{id}", unit.id)
21
+ .replace("{version}", () => version);
18
22
  /**
19
23
  * Tags of each released unit, sorted by scheme order, then name. Unreleased
20
24
  * units never match. Throws `E_TAG_FORMAT_OVERLAP` if two released units render
@@ -4,7 +4,8 @@ import type { Workspace } from "./workspace.js";
4
4
  /**
5
5
  * Release-only checks, never run by `affected`: scheme registry, schemes, tag
6
6
  * formats and initial versions of released units, group members, branch rules,
7
- * their absolute check targets and channels. Returns the scheme registry.
7
+ * their absolute check targets and channels, release items, floating tag
8
+ * formats and version file templates. Returns the scheme registry.
8
9
  */
9
10
  export declare const validateRelease: (workspace: Workspace) => ReadonlyMap<string, VersionScheme<any>>;
10
11
  /**
package/dist/validate.js CHANGED
@@ -1,12 +1,16 @@
1
1
  import { validateBranches } from "./branches.js";
2
2
  import { TallyError } from "./errors.js";
3
+ import { validateFloating } from "./floating.js";
4
+ import { validateReleaseItems } from "./release-items.js";
3
5
  import { schemeRegistry } from "./schemes.js";
4
6
  import { compileTagFormat, matchUnitTags, noMatchingTag } from "./tags.js";
5
7
  import { uniqueSorted } from "./util.js";
8
+ import { validateTemplates } from "./version-files.js";
6
9
  /**
7
10
  * Release-only checks, never run by `affected`: scheme registry, schemes, tag
8
11
  * formats and initial versions of released units, group members, branch rules,
9
- * their absolute check targets and channels. Returns the scheme registry.
12
+ * their absolute check targets and channels, release items, floating tag
13
+ * formats and version file templates. Returns the scheme registry.
10
14
  */
11
15
  export const validateRelease = (workspace) => {
12
16
  const schemes = schemeRegistry(workspace.schemes);
@@ -55,6 +59,9 @@ export const validateRelease = (workspace) => {
55
59
  });
56
60
  }
57
61
  }
62
+ validateReleaseItems(workspace);
63
+ validateFloating(workspace.release.floating);
64
+ validateTemplates(workspace);
58
65
  return schemes;
59
66
  };
60
67
  /**
@@ -0,0 +1,34 @@
1
+ import type { Warning } from "./errors.js";
2
+ import type { Plan } from "./plan.js";
3
+ import type { ReadonlyFs, VersionFile } from "./plugin.js";
4
+ import type { ReleaseItem } from "./release-items.js";
5
+ import type { Workspace } from "./workspace.js";
6
+ /** Checks template files and patterns (`E_CONFIG`); runs no plugin hook. */
7
+ export declare const validateTemplates: (workspace: Workspace) => void;
8
+ /**
9
+ * Plugin version files by unit id. Throws `E_VERSION_FILE_CLAIM` when a path
10
+ * is a plugin version file of two units or also a template. Templates may
11
+ * share a file.
12
+ */
13
+ export declare const versionFileClaims: (workspace: Workspace) => Promise<ReadonlyMap<string, readonly VersionFile[]>>;
14
+ /**
15
+ * Files of a release of `items` (stable `next`): plugin version files,
16
+ * templates and `<root>/CHANGELOG.md` per unit, read through `fs`. Returns
17
+ * only files whose content changes, and each unit's changelog section;
18
+ * checks claims first. Templates skip first releases and warn
19
+ * `W_VERSION_FILE_NO_MATCH` without any match; a missing version file is
20
+ * `E_VERSION_FILE_MISSING`.
21
+ */
22
+ export declare const releaseFiles: (workspace: Workspace, plan: Plan, opts: {
23
+ /** Files the release is based on. */
24
+ fs: ReadonlyFs;
25
+ items: readonly ReleaseItem[];
26
+ repoUrl: string | null;
27
+ /** `YYYY-MM-DD` changelog heading date. */
28
+ date: string;
29
+ }) => Promise<{
30
+ files: Map<string, string>;
31
+ /** Changelog section of each unit, by unit id. */
32
+ sections: Map<string, string>;
33
+ warnings: Warning[];
34
+ }>;