@tallyrelease/core 0.5.0-beta.47 → 0.5.0-beta.52
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/changelog.d.ts +2 -0
- package/dist/changelog.js +1 -1
- package/dist/codes.d.ts +1 -1
- package/dist/codes.js +5 -1
- package/dist/config.d.ts +3 -1
- package/dist/plan.d.ts +2 -1
- package/dist/plan.js +29 -2
- package/dist/plugin.d.ts +13 -0
- package/dist/util.d.ts +5 -0
- package/dist/util.js +16 -0
- package/dist/version-files.d.ts +6 -4
- package/dist/version-files.js +112 -15
- package/dist/workspace.d.ts +5 -0
- package/dist/workspace.js +4 -0
- package/package.json +1 -1
package/dist/changelog.d.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { PlanChange, Plan } from "./model/plan-types.js";
|
|
2
2
|
import type { Unit, VersionScheme } from "./plugin.js";
|
|
3
3
|
import type { Workspace } from "./workspace.js";
|
|
4
|
+
/** Version of a version heading line, else undefined. */
|
|
5
|
+
export declare const headingVersion: <V>(line: string, scheme: VersionScheme<V>) => V | undefined;
|
|
4
6
|
/** `<root>/CHANGELOG.md` of a unit. */
|
|
5
7
|
export declare const changelogPath: (unit: Pick<Unit, "root">) => string;
|
|
6
8
|
export interface ChangelogDependency {
|
package/dist/changelog.js
CHANGED
|
@@ -85,7 +85,7 @@ const dependencyLines = (deps) => {
|
|
|
85
85
|
].join("\n");
|
|
86
86
|
};
|
|
87
87
|
/** Version of a version heading line, else undefined. */
|
|
88
|
-
const headingVersion = (line, scheme) => {
|
|
88
|
+
export const headingVersion = (line, scheme) => {
|
|
89
89
|
const m = VERSION_HEADING.exec(line);
|
|
90
90
|
const raw = m?.[1] ?? m?.[2];
|
|
91
91
|
return raw === undefined ? undefined : scheme.parse(raw);
|
package/dist/codes.d.ts
CHANGED
|
@@ -22,5 +22,5 @@ export declare const defineCodes: <R extends Record<string, CodeDoc>>(docs: R &
|
|
|
22
22
|
/**
|
|
23
23
|
* Codes raised by core. Codes are only added, never removed or renamed.
|
|
24
24
|
*/
|
|
25
|
-
export declare const coreCodes: CodeRegistry<"E_BRANCH_RULE" | "E_CAP" | "E_CAP_BASE" | "E_CHANNEL_DUPLICATE" | "E_CHANNEL_INVALID" | "E_CHANNEL_WILDCARD" | "E_CONFIG" | "E_DUPLICATE_ROOT" | "E_DUPLICATE_UNIT" | "E_EDGE_SELF" | "E_EDGE_UNKNOWN_UNIT" | "E_GIT_DATE" | "E_GROUP_NOT_RELEASED" | "E_GROUP_OVERLAP" | "E_GROUP_SCHEME" | "E_GROUP_UNKNOWN_UNIT" | "E_INITIAL_VERSION" | "E_PLUGIN_API" | "E_PLUGIN_HOOK" | "E_PLUGIN_NAME" | "E_PLUGIN_OPTIONS" | "E_PLUGIN_UNKNOWN" | "E_RELEASE_ITEM_NAME" | "E_RELEASE_PR_SPLIT_EDGE" | "E_ROOT_INVALID" | "E_ROOT_MISSING" | "E_SCHEME_CLASH" | "E_SCHEME_UNKNOWN" | "E_TAG_FORMAT" | "E_TAG_FORMAT_OVERLAP" | "E_UNIT_GLOB" | "E_UNIT_ID" | "E_UNIT_TAG" | "E_UNIT_UNKNOWN" | "E_VERSION_FILE_CLAIM" | "E_VERSION_FILE_MISSING" | "E_VERSION_INVALID" | "W_CYCLE" | "W_GROUP_EMPTY" | "W_INITIAL_BELOW_PRERELEASE" | "W_NO_BRANCH_RULE" | "W_NO_MATCHING_TAG" | "W_NO_RELEASED_UNITS" | "W_PLUGIN" | "W_RELEASED_NO_BASE" | "W_RELEASE_AS_CONFLICT" | "W_RELEASE_AS_IGNORED" | "W_RELEASE_AS_INVALID" | "W_RELEASE_AS_UNKNOWN" | "W_RELEASE_NOT_BUILT" | "W_UNIT_GLOB_UNMATCHED" | "W_UNIT_UNKNOWN" | "W_VERSION_FILE_NO_MATCH">;
|
|
25
|
+
export declare const coreCodes: CodeRegistry<"E_BRANCH_RULE" | "E_CAP" | "E_CAP_BASE" | "E_CHANNEL_DUPLICATE" | "E_CHANNEL_INVALID" | "E_CHANNEL_WILDCARD" | "E_CONFIG" | "E_DUPLICATE_ROOT" | "E_DUPLICATE_UNIT" | "E_EDGE_SELF" | "E_EDGE_UNKNOWN_UNIT" | "E_GIT_DATE" | "E_GROUP_NOT_RELEASED" | "E_GROUP_OVERLAP" | "E_GROUP_SCHEME" | "E_GROUP_UNKNOWN_UNIT" | "E_INITIAL_VERSION" | "E_PLUGIN_API" | "E_PLUGIN_HOOK" | "E_PLUGIN_NAME" | "E_PLUGIN_OPTIONS" | "E_PLUGIN_UNKNOWN" | "E_RELEASE_ITEM_NAME" | "E_RELEASE_PR_SPLIT_EDGE" | "E_ROOT_INVALID" | "E_ROOT_MISSING" | "E_SCHEME_CLASH" | "E_SCHEME_UNKNOWN" | "E_TAG_FORMAT" | "E_TAG_FORMAT_OVERLAP" | "E_UNIT_GLOB" | "E_UNIT_ID" | "E_UNIT_TAG" | "E_UNIT_UNKNOWN" | "E_VERSION_FILE_CLAIM" | "E_VERSION_FILE_MISSING" | "E_VERSION_INVALID" | "W_CYCLE" | "W_GROUP_EMPTY" | "W_INITIAL_BELOW_PRERELEASE" | "W_MARKER_NO_MATCH" | "W_NO_BRANCH_RULE" | "W_NO_MATCHING_TAG" | "W_NO_RELEASED_UNITS" | "W_PLUGIN" | "W_RELEASED_NO_BASE" | "W_RELEASE_AS_CONFLICT" | "W_RELEASE_AS_IGNORED" | "W_RELEASE_AS_INVALID" | "W_RELEASE_AS_UNKNOWN" | "W_RELEASE_NOT_BUILT" | "W_UNIT_GLOB_UNMATCHED" | "W_UNIT_UNKNOWN" | "W_VERSION_FILE_NO_MATCH">;
|
|
26
26
|
export {};
|
package/dist/codes.js
CHANGED
|
@@ -146,7 +146,7 @@ export const coreCodes = defineCodes({
|
|
|
146
146
|
fix: "Pass an existing unit id. List ids with `tallyrelease graph`.",
|
|
147
147
|
},
|
|
148
148
|
E_VERSION_FILE_CLAIM: {
|
|
149
|
-
cause: "A file is the version file of two units,
|
|
149
|
+
cause: "A file is the version file of two units, is both a plugin version file and a `versionFiles` template target, or is a `versionFiles` marker file of two units (markers carry no unit id).",
|
|
150
150
|
fix: "Remove the duplicate `versionFiles` entry, or set one file per unit.",
|
|
151
151
|
},
|
|
152
152
|
E_VERSION_FILE_MISSING: {
|
|
@@ -169,6 +169,10 @@ export const coreCodes = defineCodes({
|
|
|
169
169
|
cause: "A unit's initial version does not exceed an existing prerelease tag of that unit.",
|
|
170
170
|
fix: "Raise `initialVersion` above the prerelease tag named in the message, or remove the stale tag.",
|
|
171
171
|
},
|
|
172
|
+
W_MARKER_NO_MATCH: {
|
|
173
|
+
cause: "A `versionFiles` entry without `pattern` names a file with no line that carries an `x-tallyrelease-version` marker and a version of the unit's scheme, so the file is not updated for the release.",
|
|
174
|
+
fix: "Add `# x-tallyrelease-version` or `<!-- x-tallyrelease-version -->` to the line that holds the version, set a `pattern`, or remove the entry.",
|
|
175
|
+
},
|
|
172
176
|
W_NO_BRANCH_RULE: {
|
|
173
177
|
cause: "No `branches` rule matches the branch, so versions in the plan are null.",
|
|
174
178
|
fix: "Add a rule for the branch under `branches`, for example `{ name: main, line: default }`, or pass `--branch`.",
|
package/dist/config.d.ts
CHANGED
|
@@ -29,10 +29,12 @@ export interface UnitOverlay {
|
|
|
29
29
|
/**
|
|
30
30
|
* A text file holding a unit's version. `file` is relative to the repository
|
|
31
31
|
* root; `pattern` uses `{id}`, `{version}`, `{major}`, `{minor}` and `{patch}`.
|
|
32
|
+
* Without `pattern`, lines marked `# x-tallyrelease-version` or
|
|
33
|
+
* `<!-- x-tallyrelease-version -->` are rewritten.
|
|
32
34
|
*/
|
|
33
35
|
export interface VersionTemplate {
|
|
34
36
|
file: string;
|
|
35
|
-
pattern
|
|
37
|
+
pattern?: string;
|
|
36
38
|
}
|
|
37
39
|
/** A branch rule; the first rule whose `name` matches the branch wins. */
|
|
38
40
|
export interface BranchConfig {
|
package/dist/plan.d.ts
CHANGED
|
@@ -16,6 +16,7 @@ export interface PlanResult {
|
|
|
16
16
|
export declare const planWithFacts: (workspace: Workspace, git: GitReader, opts: PlanOptions) => Promise<PlanResult>;
|
|
17
17
|
/**
|
|
18
18
|
* Plans `head`: validates release config, computes the build set, collects
|
|
19
|
-
* git and intent facts
|
|
19
|
+
* git and intent facts, resolves versions and kinds and runs the plugins'
|
|
20
|
+
* `validate` hooks.
|
|
20
21
|
*/
|
|
21
22
|
export declare const computePlan: (workspace: Workspace, git: GitReader, opts: PlanOptions) => Promise<Plan>;
|
package/dist/plan.js
CHANGED
|
@@ -3,11 +3,12 @@ import { checkLimits, matchBranch } from "./branches.js";
|
|
|
3
3
|
import { changeLevel, reach } from "./bump.js";
|
|
4
4
|
import { coreCodes } from "./codes.js";
|
|
5
5
|
import { collectPlanFacts } from "./facts.js";
|
|
6
|
+
import { pluginCtx, runHook } from "./plugins.js";
|
|
6
7
|
import { propagate } from "./propagate.js";
|
|
7
8
|
import { releaseAsWarnings } from "./release-as.js";
|
|
8
9
|
import { footerOf, releaseGroup, releaseUnit, unitIntent } from "./release.js";
|
|
9
10
|
import { noMatchingTag } from "./tags.js";
|
|
10
|
-
import { maxLevel } from "./util.js";
|
|
11
|
+
import { frozenCopy, maxLevel } from "./util.js";
|
|
11
12
|
import { validateRelease } from "./validate.js";
|
|
12
13
|
const lineWarnings = (workspace, branch, line) => {
|
|
13
14
|
if (!line) {
|
|
@@ -323,6 +324,30 @@ const resolveReleases = (workspace, input) => {
|
|
|
323
324
|
releases,
|
|
324
325
|
};
|
|
325
326
|
};
|
|
327
|
+
/** Warnings of the plugins' `validate` hooks, run in config order on one frozen copy of the plan. */
|
|
328
|
+
const pluginValidation = async (workspace, plan, opts) => {
|
|
329
|
+
const warnings = [];
|
|
330
|
+
const ctx = pluginCtx({ ...opts, repo: { name: workspace.repo } }, warnings);
|
|
331
|
+
let frozen;
|
|
332
|
+
for (const { plugin, options } of workspace.plugins) {
|
|
333
|
+
const { validate } = plugin;
|
|
334
|
+
if (validate) {
|
|
335
|
+
const copy = (frozen ??= frozenCopy(plan));
|
|
336
|
+
// eslint-disable-next-line no-await-in-loop -- hooks run in config order
|
|
337
|
+
const messages = await runHook(plugin.name, "validate", async () => {
|
|
338
|
+
const result = await validate(ctx(plugin.name, options), copy);
|
|
339
|
+
if (result !== undefined && !Array.isArray(result)) {
|
|
340
|
+
throw new TypeError("validate must return string[] or nothing");
|
|
341
|
+
}
|
|
342
|
+
return result ?? [];
|
|
343
|
+
});
|
|
344
|
+
for (const message of messages) {
|
|
345
|
+
warnings.push(coreCodes.warning("W_PLUGIN", message, { plugin: plugin.name }));
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
349
|
+
return warnings;
|
|
350
|
+
};
|
|
326
351
|
/** Release math over collected facts; no I/O. */
|
|
327
352
|
export const resolvePlan = (workspace, input) => resolveReleases(workspace, input).plan;
|
|
328
353
|
/** `computePlan` keeping the collected facts, for `explainUnit`. */
|
|
@@ -340,10 +365,12 @@ export const planWithFacts = async (workspace, git, opts) => {
|
|
|
340
365
|
schemes: workspace.schemes,
|
|
341
366
|
all: opts.all ?? false,
|
|
342
367
|
});
|
|
368
|
+
plan.warnings.push(...(await pluginValidation(workspace, plan, opts)));
|
|
343
369
|
return { plan, facts, releases };
|
|
344
370
|
};
|
|
345
371
|
/**
|
|
346
372
|
* Plans `head`: validates release config, computes the build set, collects
|
|
347
|
-
* git and intent facts
|
|
373
|
+
* git and intent facts, resolves versions and kinds and runs the plugins'
|
|
374
|
+
* `validate` hooks.
|
|
348
375
|
*/
|
|
349
376
|
export const computePlan = async (workspace, git, opts) => (await planWithFacts(workspace, git, opts)).plan;
|
package/dist/plugin.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { Plan } from "./model/plan-types.js";
|
|
1
2
|
import type { StandardSchemaV1 } from "@standard-schema/spec";
|
|
2
3
|
export type MaybePromise<T> = T | Promise<T>;
|
|
3
4
|
export declare const PLUGIN_API_VERSION = 1;
|
|
@@ -16,6 +17,18 @@ export interface Plugin<O = unknown> {
|
|
|
16
17
|
versionFiles?: (ctx: Ctx<O>, unit: Unit) => MaybePromise<VersionFile[]>;
|
|
17
18
|
schemes?: Record<string, VersionScheme<any>>;
|
|
18
19
|
intent?: IntentSource<O>[];
|
|
20
|
+
/** Checks the computed plan; returned messages become `W_PLUGIN` warnings. */
|
|
21
|
+
validate?: (ctx: Ctx<O>, plan: Plan) => MaybePromise<string[] | void>;
|
|
22
|
+
/**
|
|
23
|
+
* Called only for units owned by this plugin with a changelog section.
|
|
24
|
+
* Returns the section that replaces the default `section`; it must start
|
|
25
|
+
* with a version heading of the released version and have no other.
|
|
26
|
+
*/
|
|
27
|
+
changelog?: (ctx: Ctx<O>, input: {
|
|
28
|
+
unit: Unit;
|
|
29
|
+
plan: Plan;
|
|
30
|
+
section: string;
|
|
31
|
+
}) => MaybePromise<string>;
|
|
19
32
|
}
|
|
20
33
|
export interface Ctx<O = unknown> {
|
|
21
34
|
readonly options: O;
|
package/dist/util.d.ts
CHANGED
|
@@ -5,3 +5,8 @@ export declare const compare: (a: string, b: string) => number;
|
|
|
5
5
|
export declare const uniqueSorted: (values: Iterable<string>) => string[];
|
|
6
6
|
/** The higher of two levels; null is lowest. */
|
|
7
7
|
export declare const maxLevel: (a: BumpLevel | null, b: BumpLevel | null) => BumpLevel | null;
|
|
8
|
+
/**
|
|
9
|
+
* Deep-frozen copy of a JSON value, for hooks that must not change it.
|
|
10
|
+
* Expensive (a full copy plus a walk over it): make one per pass and share it.
|
|
11
|
+
*/
|
|
12
|
+
export declare const frozenCopy: <T>(value: T) => T;
|
package/dist/util.js
CHANGED
|
@@ -5,3 +5,19 @@ export const compare = (a, b) => a < b ? -1 : a > b ? 1 : 0;
|
|
|
5
5
|
export const uniqueSorted = (values) => [...new Set(values)].sort(compare);
|
|
6
6
|
/** The higher of two levels; null is lowest. */
|
|
7
7
|
export const maxLevel = (a, b) => a === null || (b !== null && LEVELS.indexOf(b) > LEVELS.indexOf(a)) ? b : a;
|
|
8
|
+
/** Freezes every nested object; walks the whole value, so keep it off hot paths. */
|
|
9
|
+
const deepFreeze = (value) => {
|
|
10
|
+
if (typeof value === "object" && value !== null) {
|
|
11
|
+
Object.values(value).forEach(deepFreeze);
|
|
12
|
+
Object.freeze(value);
|
|
13
|
+
}
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Deep-frozen copy of a JSON value, for hooks that must not change it.
|
|
17
|
+
* Expensive (a full copy plus a walk over it): make one per pass and share it.
|
|
18
|
+
*/
|
|
19
|
+
export const frozenCopy = (value) => {
|
|
20
|
+
const copy = JSON.parse(JSON.stringify(value));
|
|
21
|
+
deepFreeze(copy);
|
|
22
|
+
return copy;
|
|
23
|
+
};
|
package/dist/version-files.d.ts
CHANGED
|
@@ -7,16 +7,18 @@ import type { Workspace } from "./workspace.js";
|
|
|
7
7
|
export declare const validateTemplates: (workspace: Workspace) => void;
|
|
8
8
|
/**
|
|
9
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
|
|
11
|
-
* share a file.
|
|
10
|
+
* is a plugin version file of two units or also a template, or a marker file
|
|
11
|
+
* of two units. Templates may share a file.
|
|
12
12
|
*/
|
|
13
13
|
export declare const versionFileClaims: (workspace: Workspace) => Promise<ReadonlyMap<string, readonly VersionFile[]>>;
|
|
14
14
|
/**
|
|
15
15
|
* Files of a release of `items` (stable `next`): plugin version files,
|
|
16
|
-
* templates and `<root>/CHANGELOG.md` per unit, read through `fs
|
|
16
|
+
* templates, markers and `<root>/CHANGELOG.md` per unit, read through `fs`; the
|
|
17
|
+
* owning plugin's `changelog` hook may replace a unit's section. Returns
|
|
17
18
|
* only files whose content changes, and each unit's changelog section;
|
|
18
19
|
* checks claims first. Templates skip first releases and warn
|
|
19
|
-
* `W_VERSION_FILE_NO_MATCH` without any match
|
|
20
|
+
* `W_VERSION_FILE_NO_MATCH` without any match, markers warn
|
|
21
|
+
* `W_MARKER_NO_MATCH`; a missing version file is
|
|
20
22
|
* `E_VERSION_FILE_MISSING`.
|
|
21
23
|
*/
|
|
22
24
|
export declare const releaseFiles: (workspace: Workspace, plan: Plan, opts: {
|
package/dist/version-files.js
CHANGED
|
@@ -1,12 +1,15 @@
|
|
|
1
|
-
import { changelogPath, changelogSection, updateChangelog, } from "./changelog.js";
|
|
1
|
+
import { changelogPath, changelogSection, headingVersion, updateChangelog, } from "./changelog.js";
|
|
2
2
|
import { coreCodes } from "./codes.js";
|
|
3
3
|
import { normalizeRoot } from "./discover.js";
|
|
4
|
-
import { runHook } from "./plugins.js";
|
|
4
|
+
import { pluginCtx, runHook } from "./plugins.js";
|
|
5
|
+
import { frozenCopy } from "./util.js";
|
|
5
6
|
const TOKEN = /\{([^{}]*)\}/gu;
|
|
6
7
|
const SPECIAL = /[\\^$.*+?()[\]{}|/]/gu;
|
|
7
8
|
const CORE_TOKENS = ["major", "minor", "patch"];
|
|
8
9
|
const VERSION = "\\d+\\.\\d+\\.\\d+(?:-[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?(?:\\+[0-9A-Za-z-]+(?:\\.[0-9A-Za-z-]+)*)?";
|
|
9
10
|
const ALNUM = /[0-9A-Za-z]/u;
|
|
11
|
+
const MARKERS = ["# x-tallyrelease-version", "<!-- x-tallyrelease-version -->"];
|
|
12
|
+
const WORD = /[0-9A-Za-z](?:[0-9A-Za-z.+-]*[0-9A-Za-z])?/gu;
|
|
10
13
|
const escape = (text) => text.replace(SPECIAL, "\\$&");
|
|
11
14
|
const configError = (pattern, unit, problem) => coreCodes.error("E_CONFIG", `version file pattern "${pattern}" of unit "${unit}" ${problem}`, { unit, pattern });
|
|
12
15
|
/**
|
|
@@ -107,6 +110,32 @@ const applyTemplate = (content, { pattern, unit, from, next, scheme, }) => {
|
|
|
107
110
|
});
|
|
108
111
|
return { content: replaced, hit };
|
|
109
112
|
};
|
|
113
|
+
/**
|
|
114
|
+
* Replaces, on each marker line, the first word the scheme parses (after an
|
|
115
|
+
* optional `v`) with `next`. `hit` is false when no marker line has one.
|
|
116
|
+
*/
|
|
117
|
+
const applyMarkers = (content, { next, scheme }) => {
|
|
118
|
+
let hit = false;
|
|
119
|
+
const lines = content.split("\n").map((line) => {
|
|
120
|
+
if (!MARKERS.some((m) => line.includes(m))) {
|
|
121
|
+
return line;
|
|
122
|
+
}
|
|
123
|
+
for (const m of line.matchAll(WORD)) {
|
|
124
|
+
const [word] = m;
|
|
125
|
+
const start = word.startsWith("v") && scheme.parse(word.slice(1)) !== undefined
|
|
126
|
+
? m.index + 1
|
|
127
|
+
: scheme.parse(word) !== undefined
|
|
128
|
+
? m.index
|
|
129
|
+
: undefined;
|
|
130
|
+
if (start !== undefined) {
|
|
131
|
+
hit = true;
|
|
132
|
+
return `${line.slice(0, start)}${next}${line.slice(m.index + word.length)}`;
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return line;
|
|
136
|
+
});
|
|
137
|
+
return { content: lines.join("\n"), hit };
|
|
138
|
+
};
|
|
110
139
|
/** Repo-relative template file; `E_CONFIG` for `.`, absolute or `..` paths. */
|
|
111
140
|
const templateFile = (file, unit) => {
|
|
112
141
|
const invalid = () => coreCodes.error("E_CONFIG", `version file "${file}" of unit "${unit}" must be a repo-relative file without ".." segments`, { unit, file });
|
|
@@ -140,18 +169,34 @@ const versionFileStep = async (staged, step, pluginFiles) => {
|
|
|
140
169
|
staged.files.set(file.path, written);
|
|
141
170
|
}
|
|
142
171
|
};
|
|
143
|
-
/**
|
|
172
|
+
/**
|
|
173
|
+
* Rewrites the version templates and markers of the unit; templates skip
|
|
174
|
+
* first releases.
|
|
175
|
+
*/
|
|
144
176
|
const templateStep = (staged, step) => {
|
|
145
177
|
const { workspace, item, unit, scheme, from } = step;
|
|
146
178
|
const warnings = [];
|
|
147
|
-
const
|
|
148
|
-
for (const template of templates) {
|
|
179
|
+
for (const template of workspace.release.templates.get(unit.id) ?? []) {
|
|
149
180
|
const { pattern } = template;
|
|
150
181
|
const file = templateFile(template.file, unit.id);
|
|
182
|
+
if (pattern === undefined) {
|
|
183
|
+
const result = applyMarkers(required(staged, file, unit.id), {
|
|
184
|
+
next: item.version,
|
|
185
|
+
scheme,
|
|
186
|
+
});
|
|
187
|
+
staged.files.set(file, result.content);
|
|
188
|
+
if (!result.hit) {
|
|
189
|
+
warnings.push(coreCodes.warning("W_MARKER_NO_MATCH", `version file "${file}" of unit "${unit.id}" has no x-tallyrelease-version marker with a version`, { unit: unit.id }));
|
|
190
|
+
}
|
|
191
|
+
continue;
|
|
192
|
+
}
|
|
193
|
+
if (from === null) {
|
|
194
|
+
continue;
|
|
195
|
+
}
|
|
151
196
|
const result = applyTemplate(required(staged, file, unit.id), {
|
|
152
197
|
pattern,
|
|
153
198
|
unit: unit.id,
|
|
154
|
-
from
|
|
199
|
+
from,
|
|
155
200
|
next: item.version,
|
|
156
201
|
scheme,
|
|
157
202
|
});
|
|
@@ -162,15 +207,49 @@ const templateStep = (staged, step) => {
|
|
|
162
207
|
}
|
|
163
208
|
return warnings;
|
|
164
209
|
};
|
|
210
|
+
/**
|
|
211
|
+
* The owning plugin's `changelog` section, which must have one version
|
|
212
|
+
* heading, of the released version, on its first line; else `section`.
|
|
213
|
+
*/
|
|
214
|
+
const pluginSection = async (step, section) => {
|
|
215
|
+
const { workspace, item, unit, scheme, ctx, hookPlan } = step;
|
|
216
|
+
const owner = workspace.plugins.find((p) => p.plugin.name === unit.plugin);
|
|
217
|
+
const hook = owner?.plugin.changelog;
|
|
218
|
+
if (!owner || !hook) {
|
|
219
|
+
return section;
|
|
220
|
+
}
|
|
221
|
+
return await runHook(unit.plugin, "changelog", async () => {
|
|
222
|
+
const result = await hook(ctx(unit.plugin, owner.options), {
|
|
223
|
+
unit,
|
|
224
|
+
plan: hookPlan(),
|
|
225
|
+
section,
|
|
226
|
+
});
|
|
227
|
+
if (typeof result !== "string") {
|
|
228
|
+
throw new TypeError("changelog must return a string");
|
|
229
|
+
}
|
|
230
|
+
const text = result.trim();
|
|
231
|
+
const [first = "", ...rest] = text.split("\n");
|
|
232
|
+
const version = headingVersion(first, scheme);
|
|
233
|
+
const next = scheme.parse(item.version);
|
|
234
|
+
if (version === undefined ||
|
|
235
|
+
next === undefined ||
|
|
236
|
+
scheme.compare(version, next) !== 0 ||
|
|
237
|
+
rest.some((line) => headingVersion(line, scheme) !== undefined)) {
|
|
238
|
+
throw new Error(`section of unit "${unit.id}" must start with a heading of version ${item.version} and have no other version heading`);
|
|
239
|
+
}
|
|
240
|
+
return text;
|
|
241
|
+
});
|
|
242
|
+
};
|
|
165
243
|
/** Updates the unit's changelog; returns the new section, if any. */
|
|
166
|
-
const changelogStep = (staged, step) => {
|
|
244
|
+
const changelogStep = async (staged, step) => {
|
|
167
245
|
const { workspace, plan, unit, scheme, from, repoUrl, date, prUnits } = step;
|
|
168
|
-
const
|
|
246
|
+
const fallback = changelogSection(workspace, plan, {
|
|
169
247
|
id: unit.id,
|
|
170
248
|
repoUrl,
|
|
171
249
|
date,
|
|
172
250
|
prUnits,
|
|
173
251
|
});
|
|
252
|
+
const section = fallback === undefined ? undefined : await pluginSection(step, fallback);
|
|
174
253
|
if (section !== undefined) {
|
|
175
254
|
const path = changelogPath(unit);
|
|
176
255
|
staged.files.set(path, updateChangelog(staged.read(path), section, {
|
|
@@ -185,14 +264,16 @@ export const validateTemplates = (workspace) => {
|
|
|
185
264
|
for (const [id, templates] of workspace.release.templates) {
|
|
186
265
|
for (const { file, pattern } of templates) {
|
|
187
266
|
templateFile(file, id);
|
|
188
|
-
|
|
267
|
+
if (pattern !== undefined) {
|
|
268
|
+
compileTemplate(pattern, id);
|
|
269
|
+
}
|
|
189
270
|
}
|
|
190
271
|
}
|
|
191
272
|
};
|
|
192
273
|
/**
|
|
193
274
|
* Plugin version files by unit id. Throws `E_VERSION_FILE_CLAIM` when a path
|
|
194
|
-
* is a plugin version file of two units or also a template
|
|
195
|
-
* share a file.
|
|
275
|
+
* is a plugin version file of two units or also a template, or a marker file
|
|
276
|
+
* of two units. Templates may share a file.
|
|
196
277
|
*/
|
|
197
278
|
export const versionFileClaims = async (workspace) => {
|
|
198
279
|
const files = await workspace.versionFiles();
|
|
@@ -206,23 +287,33 @@ export const versionFileClaims = async (workspace) => {
|
|
|
206
287
|
claims.set(path, id);
|
|
207
288
|
}
|
|
208
289
|
}
|
|
290
|
+
const markers = new Map();
|
|
209
291
|
for (const [id, templates] of workspace.release.templates) {
|
|
210
|
-
for (const { file } of templates) {
|
|
292
|
+
for (const { file, pattern } of templates) {
|
|
211
293
|
const path = templateFile(file, id);
|
|
212
294
|
const owner = claims.get(path);
|
|
213
295
|
if (owner !== undefined) {
|
|
214
296
|
throw coreCodes.error("E_VERSION_FILE_CLAIM", `version file template of unit "${id}" targets "${path}", a version file of unit "${owner}"`, { path, units: [owner, id] });
|
|
215
297
|
}
|
|
298
|
+
if (pattern === undefined) {
|
|
299
|
+
const marked = markers.get(path);
|
|
300
|
+
if (marked !== undefined && marked !== id) {
|
|
301
|
+
throw coreCodes.error("E_VERSION_FILE_CLAIM", `version file "${path}" holds markers of units "${marked}" and "${id}"`, { path, units: [marked, id] });
|
|
302
|
+
}
|
|
303
|
+
markers.set(path, id);
|
|
304
|
+
}
|
|
216
305
|
}
|
|
217
306
|
}
|
|
218
307
|
return files;
|
|
219
308
|
};
|
|
220
309
|
/**
|
|
221
310
|
* Files of a release of `items` (stable `next`): plugin version files,
|
|
222
|
-
* templates and `<root>/CHANGELOG.md` per unit, read through `fs
|
|
311
|
+
* templates, markers and `<root>/CHANGELOG.md` per unit, read through `fs`; the
|
|
312
|
+
* owning plugin's `changelog` hook may replace a unit's section. Returns
|
|
223
313
|
* only files whose content changes, and each unit's changelog section;
|
|
224
314
|
* checks claims first. Templates skip first releases and warn
|
|
225
|
-
* `W_VERSION_FILE_NO_MATCH` without any match
|
|
315
|
+
* `W_VERSION_FILE_NO_MATCH` without any match, markers warn
|
|
316
|
+
* `W_MARKER_NO_MATCH`; a missing version file is
|
|
226
317
|
* `E_VERSION_FILE_MISSING`.
|
|
227
318
|
*/
|
|
228
319
|
export const releaseFiles = async (workspace, plan, opts) => {
|
|
@@ -240,6 +331,9 @@ export const releaseFiles = async (workspace, plan, opts) => {
|
|
|
240
331
|
};
|
|
241
332
|
const sections = new Map();
|
|
242
333
|
const warnings = [];
|
|
334
|
+
const ctx = pluginCtx({ fs, repo: { name: workspace.repo } }, warnings);
|
|
335
|
+
let frozen;
|
|
336
|
+
const hookPlan = () => (frozen ??= frozenCopy(plan));
|
|
243
337
|
for (const item of items) {
|
|
244
338
|
const prUnits = items
|
|
245
339
|
.filter((i) => pr(i) === pr(item))
|
|
@@ -262,11 +356,14 @@ export const releaseFiles = async (workspace, plan, opts) => {
|
|
|
262
356
|
prUnits,
|
|
263
357
|
repoUrl,
|
|
264
358
|
date,
|
|
359
|
+
ctx,
|
|
360
|
+
hookPlan,
|
|
265
361
|
};
|
|
266
362
|
// eslint-disable-next-line no-await-in-loop -- files are written in order
|
|
267
363
|
await versionFileStep(staged, step, pluginFiles.get(id) ?? []);
|
|
268
364
|
warnings.push(...templateStep(staged, step));
|
|
269
|
-
|
|
365
|
+
// eslint-disable-next-line no-await-in-loop -- files are written in order
|
|
366
|
+
const section = await changelogStep(staged, step);
|
|
270
367
|
if (section !== undefined) {
|
|
271
368
|
sections.set(id, section);
|
|
272
369
|
}
|
package/dist/workspace.d.ts
CHANGED
|
@@ -36,6 +36,11 @@ export interface Workspace {
|
|
|
36
36
|
readonly schemes: ReadonlyMap<string, VersionScheme<any>>;
|
|
37
37
|
/** Core sources first, then enabled plugins in config order. */
|
|
38
38
|
readonly intents: readonly IntentEntry[];
|
|
39
|
+
/** Core first, then enabled plugins in config order, with their options. */
|
|
40
|
+
readonly plugins: readonly {
|
|
41
|
+
readonly plugin: Plugin;
|
|
42
|
+
readonly options: unknown;
|
|
43
|
+
}[];
|
|
39
44
|
/** Bumps and branch rules with defaults applied. */
|
|
40
45
|
readonly release: ReleaseSettings;
|
|
41
46
|
/** Fixed groups, sorted by name. */
|
package/dist/workspace.js
CHANGED
|
@@ -87,6 +87,10 @@ export const loadWorkspace = async (input) => {
|
|
|
87
87
|
warnings: [...warnings, ...graph.warnings],
|
|
88
88
|
schemes: schemeRegistry(active.map((a) => a.plugin)),
|
|
89
89
|
intents,
|
|
90
|
+
plugins: active.map(({ plugin, ctx }) => ({
|
|
91
|
+
plugin,
|
|
92
|
+
options: ctx.options,
|
|
93
|
+
})),
|
|
90
94
|
release: releaseSettings(input.config, units),
|
|
91
95
|
groups,
|
|
92
96
|
versionFiles: versionFilesOf(active, units),
|