@williamthorsen/release-kit 8.0.1 → 9.0.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 +40 -0
- package/README.md +93 -18
- package/cliff.toml.template +20 -20
- package/dist/esm/bin/release-kit.js +12 -9
- package/dist/esm/buildChangelogEntries.js +5 -4
- package/dist/esm/changelogOverrides.js +4 -3
- package/dist/esm/createGithubRelease.js +2 -2
- package/dist/esm/defineConfig.d.ts +2 -0
- package/dist/esm/defineConfig.js +3 -0
- package/dist/esm/detectPackageManager.js +1 -1
- package/dist/esm/format.js +2 -2
- package/dist/esm/getCommitsSinceTarget.js +1 -1
- package/dist/esm/index.d.ts +2 -2
- package/dist/esm/index.js +1 -0
- package/dist/esm/init/templates.js +6 -10
- package/dist/esm/injectReleaseNotesIntoReadme.js +1 -1
- package/dist/esm/loadValidatedConfig.d.ts +10 -0
- package/dist/esm/loadValidatedConfig.js +26 -0
- package/dist/esm/parseCommitMessage.d.ts +1 -0
- package/dist/esm/parseCommitMessage.js +3 -1
- package/dist/esm/prepareCommand.js +7 -6
- package/dist/esm/previewTagPrefixes.js +2 -5
- package/dist/esm/releasePrepareMono.js +6 -5
- package/dist/esm/renderReleaseNotes.js +2 -2
- package/dist/esm/resolveCommandTags.js +4 -3
- package/dist/esm/stripScope.js +3 -2
- package/dist/esm/sync-labels/generateCommand.d.ts +7 -1
- package/dist/esm/sync-labels/generateCommand.js +45 -15
- package/dist/esm/sync-labels/initCommand.js +47 -7
- package/dist/esm/sync-labels/resolveLabels.d.ts +3 -2
- package/dist/esm/sync-labels/resolveLabels.js +14 -26
- package/dist/esm/sync-labels/retiredConfig.d.ts +2 -0
- package/dist/esm/sync-labels/retiredConfig.js +11 -0
- package/dist/esm/sync-labels/syncCommand.js +4 -0
- package/dist/esm/sync-labels/templates.d.ts +3 -2
- package/dist/esm/sync-labels/templates.js +44 -16
- package/dist/esm/sync-labels/types.d.ts +0 -4
- package/dist/esm/types.d.ts +20 -0
- package/dist/esm/types.js +13 -0
- package/dist/esm/writeReleaseNotesPreviews.js +1 -1
- package/package.json +3 -3
- package/presets/labels/common.yaml +3 -0
- package/dist/esm/sync-labels/loadSyncLabelsConfig.d.ts +0 -3
- package/dist/esm/sync-labels/loadSyncLabelsConfig.js +0 -53
|
@@ -95,8 +95,8 @@ export function releasePrepareMono(config, options) {
|
|
|
95
95
|
tags,
|
|
96
96
|
formatCommand,
|
|
97
97
|
dryRun,
|
|
98
|
-
...(allWarnings.length > 0
|
|
99
|
-
...(project
|
|
98
|
+
...(allWarnings.length > 0 && { warnings: allWarnings }),
|
|
99
|
+
...(project !== undefined && { project }),
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
102
|
function determineDirectBumps(config, options) {
|
|
@@ -487,10 +487,11 @@ function topologicalSort(releaseSet, graph) {
|
|
|
487
487
|
const sortedSet = new Set(sorted);
|
|
488
488
|
const cyclicDirs = [];
|
|
489
489
|
for (const dir of releaseDirs) {
|
|
490
|
-
if (
|
|
491
|
-
|
|
492
|
-
cyclicDirs.push(dir);
|
|
490
|
+
if (sortedSet.has(dir)) {
|
|
491
|
+
continue;
|
|
493
492
|
}
|
|
493
|
+
sorted.push(dir);
|
|
494
|
+
cyclicDirs.push(dir);
|
|
494
495
|
}
|
|
495
496
|
return { sorted, cyclicDirs };
|
|
496
497
|
}
|
|
@@ -50,8 +50,8 @@ function sortSectionsByOrder(sections, order) {
|
|
|
50
50
|
}
|
|
51
51
|
const indexed = sections.map((section, index) => ({ section, index }));
|
|
52
52
|
indexed.sort((a, b) => {
|
|
53
|
-
const priorityA = priority.get(a.section.title) ??
|
|
54
|
-
const priorityB = priority.get(b.section.title) ??
|
|
53
|
+
const priorityA = priority.get(a.section.title) ?? Infinity;
|
|
54
|
+
const priorityB = priority.get(b.section.title) ?? Infinity;
|
|
55
55
|
if (priorityA !== priorityB) {
|
|
56
56
|
return priorityA - priorityB;
|
|
57
57
|
}
|
|
@@ -42,10 +42,11 @@ export async function resolveCommandTags(tags) {
|
|
|
42
42
|
if (tags !== undefined) {
|
|
43
43
|
const availableTagNames = resolvedTags.map((t) => t.tag);
|
|
44
44
|
for (const name of tags) {
|
|
45
|
-
if (
|
|
46
|
-
|
|
47
|
-
process.exit(1);
|
|
45
|
+
if (availableTagNames.includes(name)) {
|
|
46
|
+
continue;
|
|
48
47
|
}
|
|
48
|
+
reportError(`Unknown tag "${name}" in --tags. Available: ${availableTagNames.join(', ')}`);
|
|
49
|
+
process.exit(1);
|
|
49
50
|
}
|
|
50
51
|
resolvedTags = resolvedTags.filter((t) => tags.includes(t.tag));
|
|
51
52
|
}
|
package/dist/esm/stripScope.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { COMMIT_PREPROCESSOR_PATTERNS } from "./parseCommitMessage.js";
|
|
1
|
+
import { COMMIT_PREPROCESSOR_PATTERNS, PIPE_SCOPE_SOURCE } from "./parseCommitMessage.js";
|
|
2
|
+
const PIPE_SCOPE_PATTERN = new RegExp(String.raw `^${PIPE_SCOPE_SOURCE}\|(.*)$`);
|
|
2
3
|
export function stripScope(message) {
|
|
3
4
|
let ticketPrefix = '';
|
|
4
5
|
let remainder = message;
|
|
@@ -9,7 +10,7 @@ export function stripScope(message) {
|
|
|
9
10
|
remainder = remainder.slice(match[0].length);
|
|
10
11
|
}
|
|
11
12
|
}
|
|
12
|
-
const pipeMatch = remainder.match(
|
|
13
|
+
const pipeMatch = remainder.match(PIPE_SCOPE_PATTERN);
|
|
13
14
|
if (pipeMatch) {
|
|
14
15
|
return `${ticketPrefix}${pipeMatch[1]}`;
|
|
15
16
|
}
|
|
@@ -1,4 +1,10 @@
|
|
|
1
|
+
import type { RepoLabelsConfig } from '../types.js';
|
|
1
2
|
import type { LabelDefinition } from './types.js';
|
|
2
3
|
export declare const LABELS_OUTPUT_PATH = ".github/labels.yaml";
|
|
3
4
|
export declare function formatLabelsYaml(labels: LabelDefinition[], presetHashes: Map<string, string>): string;
|
|
4
|
-
export declare function
|
|
5
|
+
export declare function loadRepoLabelsConfig(): Promise<RepoLabelsConfig | undefined>;
|
|
6
|
+
interface GenerateOptions {
|
|
7
|
+
check?: boolean;
|
|
8
|
+
}
|
|
9
|
+
export declare function generateCommand({ check }?: GenerateOptions): Promise<number>;
|
|
10
|
+
export {};
|
|
@@ -1,36 +1,47 @@
|
|
|
1
|
-
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
1
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
3
|
import { reportError } from '@williamthorsen/nmr-core';
|
|
4
4
|
import { stringify } from 'yaml';
|
|
5
|
-
import {
|
|
5
|
+
import { CONFIG_FILE_PATH } from "../loadConfig.js";
|
|
6
|
+
import { loadValidatedConfig } from "../loadValidatedConfig.js";
|
|
6
7
|
import { hashPresetFile } from "./presets.js";
|
|
7
8
|
import { resolveLabels } from "./resolveLabels.js";
|
|
9
|
+
import { checkRetiredSyncLabelsConfig } from "./retiredConfig.js";
|
|
8
10
|
export const LABELS_OUTPUT_PATH = '.github/labels.yaml';
|
|
9
11
|
export function formatLabelsYaml(labels, presetHashes) {
|
|
10
|
-
const headerLines = ['# Generated by release-kit sync-labels — do not edit.', `# Source: ${
|
|
11
|
-
for (const [name, hash] of [...presetHashes
|
|
12
|
+
const headerLines = ['# Generated by release-kit sync-labels — do not edit.', `# Source: ${CONFIG_FILE_PATH}`];
|
|
13
|
+
for (const [name, hash] of [...presetHashes].sort(([a], [b]) => a.localeCompare(b))) {
|
|
12
14
|
headerLines.push(`# ${name} preset hash: ${hash}`);
|
|
13
15
|
}
|
|
14
16
|
const yamlBody = stringify(labels, { singleQuote: true, lineWidth: 0 });
|
|
15
17
|
return headerLines.join('\n') + '\n' + yamlBody;
|
|
16
18
|
}
|
|
17
|
-
export async function
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
config
|
|
19
|
+
export async function loadRepoLabelsConfig() {
|
|
20
|
+
const result = await loadValidatedConfig();
|
|
21
|
+
if (result.status === 'missing') {
|
|
22
|
+
reportError(`No config file found at ${CONFIG_FILE_PATH}. Run \`release-kit sync-labels init\` first.`);
|
|
23
|
+
return undefined;
|
|
21
24
|
}
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
+
if (result.status === 'invalid') {
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
if (result.config.repoLabels === undefined) {
|
|
29
|
+
reportError(`No \`repoLabels\` block found in ${CONFIG_FILE_PATH}. Run \`release-kit sync-labels init\` to seed one.`);
|
|
30
|
+
return undefined;
|
|
31
|
+
}
|
|
32
|
+
return result.config.repoLabels;
|
|
33
|
+
}
|
|
34
|
+
export async function generateCommand({ check = false } = {}) {
|
|
35
|
+
if (checkRetiredSyncLabelsConfig()) {
|
|
25
36
|
return 1;
|
|
26
37
|
}
|
|
27
|
-
|
|
28
|
-
|
|
38
|
+
const repoLabels = await loadRepoLabelsConfig();
|
|
39
|
+
if (repoLabels === undefined) {
|
|
29
40
|
return 1;
|
|
30
41
|
}
|
|
31
42
|
let labels;
|
|
32
43
|
try {
|
|
33
|
-
labels = resolveLabels(
|
|
44
|
+
labels = resolveLabels(repoLabels);
|
|
34
45
|
}
|
|
35
46
|
catch (error) {
|
|
36
47
|
const message = error instanceof Error ? error.message : String(error);
|
|
@@ -38,10 +49,13 @@ export async function generateCommand() {
|
|
|
38
49
|
return 1;
|
|
39
50
|
}
|
|
40
51
|
const presetHashes = new Map();
|
|
41
|
-
for (const presetName of
|
|
52
|
+
for (const presetName of repoLabels.extends ?? []) {
|
|
42
53
|
presetHashes.set(presetName, hashPresetFile(presetName));
|
|
43
54
|
}
|
|
44
55
|
const content = formatLabelsYaml(labels, presetHashes);
|
|
56
|
+
if (check) {
|
|
57
|
+
return compareAgainstCommittedFile(content);
|
|
58
|
+
}
|
|
45
59
|
try {
|
|
46
60
|
mkdirSync(dirname(LABELS_OUTPUT_PATH), { recursive: true });
|
|
47
61
|
writeFileSync(LABELS_OUTPUT_PATH, content, 'utf8');
|
|
@@ -54,3 +68,19 @@ export async function generateCommand() {
|
|
|
54
68
|
console.info(`Generated ${LABELS_OUTPUT_PATH} with ${String(labels.length)} labels.`);
|
|
55
69
|
return 0;
|
|
56
70
|
}
|
|
71
|
+
function compareAgainstCommittedFile(expected) {
|
|
72
|
+
let existing;
|
|
73
|
+
try {
|
|
74
|
+
existing = readFileSync(LABELS_OUTPUT_PATH, 'utf8');
|
|
75
|
+
}
|
|
76
|
+
catch {
|
|
77
|
+
reportError(`${LABELS_OUTPUT_PATH} is missing. Run \`release-kit sync-labels generate\` and commit the result.`);
|
|
78
|
+
return 1;
|
|
79
|
+
}
|
|
80
|
+
if (existing !== expected) {
|
|
81
|
+
reportError(`${LABELS_OUTPUT_PATH} is stale. Run \`release-kit sync-labels generate\` and commit the result.`);
|
|
82
|
+
return 1;
|
|
83
|
+
}
|
|
84
|
+
console.info(`${LABELS_OUTPUT_PATH} is up to date.`);
|
|
85
|
+
return 0;
|
|
86
|
+
}
|
|
@@ -1,10 +1,16 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
1
2
|
import { reportWriteResult, writeFileWithCheck } from '@williamthorsen/nmr-core';
|
|
2
3
|
import { discoverWorkspaces } from "../discoverWorkspaces.js";
|
|
4
|
+
import { CONFIG_FILE_PATH } from "../loadConfig.js";
|
|
5
|
+
import { loadValidatedConfig } from "../loadValidatedConfig.js";
|
|
3
6
|
import { generateCommand, LABELS_OUTPUT_PATH } from "./generateCommand.js";
|
|
4
|
-
import {
|
|
5
|
-
import { buildScopeLabels,
|
|
7
|
+
import { checkRetiredSyncLabelsConfig } from "./retiredConfig.js";
|
|
8
|
+
import { buildScopeLabels, renderRepoLabelsBlock, repoLabelsConfigScript, syncLabelsWorkflow } from "./templates.js";
|
|
6
9
|
const WORKFLOW_PATH = '.github/workflows/sync-labels.yaml';
|
|
7
10
|
export async function syncLabelsInitCommand({ dryRun, force }) {
|
|
11
|
+
if (checkRetiredSyncLabelsConfig()) {
|
|
12
|
+
return 1;
|
|
13
|
+
}
|
|
8
14
|
if (dryRun) {
|
|
9
15
|
console.info('[dry-run mode]');
|
|
10
16
|
}
|
|
@@ -18,20 +24,41 @@ export async function syncLabelsInitCommand({ dryRun, force }) {
|
|
|
18
24
|
process.stderr.write(` Failed to discover workspaces: ${message}\n`);
|
|
19
25
|
return 1;
|
|
20
26
|
}
|
|
21
|
-
const scopeLabels = workspacePaths === undefined ? [] : buildScopeLabels(workspacePaths);
|
|
22
27
|
if (workspacePaths === undefined) {
|
|
23
28
|
console.info(' No pnpm workspaces found (single-package repo)');
|
|
24
29
|
}
|
|
25
30
|
else {
|
|
26
31
|
console.info(` Found ${String(workspacePaths.length)} workspaces`);
|
|
27
32
|
}
|
|
33
|
+
const configExists = existsSync(CONFIG_FILE_PATH);
|
|
34
|
+
const retiredNames = configExists ? await loadRetiredPackageNames() : [];
|
|
35
|
+
if (retiredNames === undefined) {
|
|
36
|
+
return 1;
|
|
37
|
+
}
|
|
38
|
+
const scopeLabels = workspacePaths === undefined ? [] : buildScopeLabels(workspacePaths, retiredNames);
|
|
28
39
|
console.info('\n> Scaffolding files');
|
|
29
40
|
const workflowResult = writeFileWithCheck(WORKFLOW_PATH, syncLabelsWorkflow(), { dryRun, overwrite: force });
|
|
30
|
-
|
|
41
|
+
reportWriteResult(workflowResult, dryRun);
|
|
42
|
+
if (configExists) {
|
|
43
|
+
console.info(`\n> ${CONFIG_FILE_PATH} already exists; add this block to the object passed to defineConfig:\n`);
|
|
44
|
+
console.info(renderRepoLabelsBlock(scopeLabels));
|
|
45
|
+
if (workflowResult.outcome === 'failed') {
|
|
46
|
+
process.stderr.write('Failed to scaffold one or more files.\n');
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
console.info(`
|
|
50
|
+
> Next steps
|
|
51
|
+
1. Paste the block above into ${CONFIG_FILE_PATH}.
|
|
52
|
+
2. Run \`release-kit sync-labels generate\` to produce ${LABELS_OUTPUT_PATH}.
|
|
53
|
+
3. Commit the changes.
|
|
54
|
+
4. Run \`release-kit sync-labels sync\` to apply labels to your GitHub repo.
|
|
55
|
+
`);
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
const configResult = writeFileWithCheck(CONFIG_FILE_PATH, repoLabelsConfigScript(scopeLabels), {
|
|
31
59
|
dryRun,
|
|
32
60
|
overwrite: force,
|
|
33
61
|
});
|
|
34
|
-
reportWriteResult(workflowResult, dryRun);
|
|
35
62
|
reportWriteResult(configResult, dryRun);
|
|
36
63
|
if (workflowResult.outcome === 'failed' || configResult.outcome === 'failed') {
|
|
37
64
|
process.stderr.write('Failed to scaffold one or more files.\n');
|
|
@@ -51,11 +78,24 @@ export async function syncLabelsInitCommand({ dryRun, force }) {
|
|
|
51
78
|
> Next steps
|
|
52
79
|
1. Review the generated files:
|
|
53
80
|
- ${WORKFLOW_PATH}
|
|
54
|
-
- ${
|
|
81
|
+
- ${CONFIG_FILE_PATH}
|
|
55
82
|
- ${LABELS_OUTPUT_PATH}
|
|
56
|
-
2. Customize ${
|
|
83
|
+
2. Customize the \`repoLabels\` block in ${CONFIG_FILE_PATH} as needed, then re-run \`release-kit sync-labels generate\`.
|
|
57
84
|
3. Commit the generated files.
|
|
58
85
|
4. Run \`release-kit sync-labels sync\` to apply labels to your GitHub repo.
|
|
59
86
|
`);
|
|
60
87
|
return 0;
|
|
61
88
|
}
|
|
89
|
+
async function loadRetiredPackageNames() {
|
|
90
|
+
const result = await loadValidatedConfig();
|
|
91
|
+
if (result.status === 'invalid') {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
if (result.status === 'missing') {
|
|
95
|
+
return [];
|
|
96
|
+
}
|
|
97
|
+
return (result.config.retiredPackages ?? []).map((retired) => toUnscopedName(retired.name));
|
|
98
|
+
}
|
|
99
|
+
function toUnscopedName(name) {
|
|
100
|
+
return name.startsWith('@') ? name.slice(name.indexOf('/') + 1) : name;
|
|
101
|
+
}
|
|
@@ -1,2 +1,3 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
1
|
+
import type { RepoLabelsConfig } from '../types.js';
|
|
2
|
+
import type { LabelDefinition } from './types.js';
|
|
3
|
+
export declare function resolveLabels(config: RepoLabelsConfig): LabelDefinition[];
|
|
@@ -1,35 +1,23 @@
|
|
|
1
1
|
import { loadPreset } from "./presets.js";
|
|
2
2
|
export function resolveLabels(config) {
|
|
3
|
-
const
|
|
4
|
-
for (const presetName of config.
|
|
5
|
-
const
|
|
6
|
-
|
|
7
|
-
}
|
|
8
|
-
const customLabels = config.labels ?? [];
|
|
9
|
-
detectCollisions(presetLabels, customLabels);
|
|
10
|
-
return sortLabels([...presetLabels, ...customLabels]);
|
|
11
|
-
}
|
|
12
|
-
function detectCollisions(presetLabels, customLabels) {
|
|
13
|
-
const seenPresetNames = new Set();
|
|
14
|
-
const withinPresetDuplicates = [];
|
|
15
|
-
for (const label of presetLabels) {
|
|
16
|
-
if (seenPresetNames.has(label.name)) {
|
|
17
|
-
withinPresetDuplicates.push(label.name);
|
|
3
|
+
const resolved = new Map();
|
|
4
|
+
for (const presetName of config.extends ?? []) {
|
|
5
|
+
for (const label of loadPreset(presetName)) {
|
|
6
|
+
resolved.set(label.name, label);
|
|
18
7
|
}
|
|
19
|
-
seenPresetNames.add(label.name);
|
|
20
|
-
}
|
|
21
|
-
if (withinPresetDuplicates.length > 0) {
|
|
22
|
-
throw new Error(`Label name collision within presets: the following labels are defined by multiple presets: ${withinPresetDuplicates.join(', ')}. Remove the duplicates or use different presets.`);
|
|
23
8
|
}
|
|
24
|
-
const
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
9
|
+
for (const [name, spec] of Object.entries(config.labels ?? {})) {
|
|
10
|
+
if (spec === null) {
|
|
11
|
+
if (!resolved.has(name)) {
|
|
12
|
+
throw new Error(`Label '${name}' is set to null (remove), but no preset in 'extends' defines it. Fix the name or delete the entry.`);
|
|
13
|
+
}
|
|
14
|
+
resolved.delete(name);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
resolved.set(name, { name, color: spec.color, description: spec.description });
|
|
28
18
|
}
|
|
29
19
|
}
|
|
30
|
-
|
|
31
|
-
throw new Error(`Label name collision: the following labels appear in both presets and custom labels: ${conflicts.join(', ')}. Remove duplicates from your custom labels or use a different name.`);
|
|
32
|
-
}
|
|
20
|
+
return sortLabels([...resolved.values()]);
|
|
33
21
|
}
|
|
34
22
|
function sortLabels(labels) {
|
|
35
23
|
return [...labels].sort((a, b) => a.name.localeCompare(b.name));
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { existsSync } from 'node:fs';
|
|
2
|
+
import { reportError } from '@williamthorsen/nmr-core';
|
|
3
|
+
import { CONFIG_FILE_PATH } from "../loadConfig.js";
|
|
4
|
+
export const RETIRED_SYNC_LABELS_CONFIG_PATH = '.config/sync-labels.config.ts';
|
|
5
|
+
export function checkRetiredSyncLabelsConfig() {
|
|
6
|
+
if (!existsSync(RETIRED_SYNC_LABELS_CONFIG_PATH)) {
|
|
7
|
+
return false;
|
|
8
|
+
}
|
|
9
|
+
reportError(`${RETIRED_SYNC_LABELS_CONFIG_PATH} is no longer read. Move its labels into the \`repoLabels\` block of ${CONFIG_FILE_PATH} (presets move to \`repoLabels.extends\`; each label becomes a \`'name': { color, description }\` entry under \`repoLabels.labels\`), then delete the file.`);
|
|
10
|
+
return true;
|
|
11
|
+
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { execSync } from 'node:child_process';
|
|
2
2
|
import { existsSync } from 'node:fs';
|
|
3
3
|
import { reportError } from '@williamthorsen/nmr-core';
|
|
4
|
+
import { checkRetiredSyncLabelsConfig } from "./retiredConfig.js";
|
|
4
5
|
const WORKFLOW_FILE = '.github/workflows/sync-labels.yaml';
|
|
5
6
|
function checkGhAvailable() {
|
|
6
7
|
try {
|
|
@@ -12,6 +13,9 @@ function checkGhAvailable() {
|
|
|
12
13
|
}
|
|
13
14
|
}
|
|
14
15
|
export function syncLabelsCommand() {
|
|
16
|
+
if (checkRetiredSyncLabelsConfig()) {
|
|
17
|
+
return 1;
|
|
18
|
+
}
|
|
15
19
|
if (!checkGhAvailable()) {
|
|
16
20
|
reportError('The `gh` CLI is not installed or not in PATH. Install it from https://cli.github.com/');
|
|
17
21
|
return 1;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { LabelDefinition } from './types.js';
|
|
2
2
|
export declare function syncLabelsWorkflow(): string;
|
|
3
|
-
export declare function buildScopeLabels(workspacePaths: string[]): LabelDefinition[];
|
|
4
|
-
export declare function
|
|
3
|
+
export declare function buildScopeLabels(workspacePaths: string[], retiredNames?: string[]): LabelDefinition[];
|
|
4
|
+
export declare function renderRepoLabelsBlock(scopeLabels: LabelDefinition[]): string;
|
|
5
|
+
export declare function repoLabelsConfigScript(scopeLabels: LabelDefinition[]): string;
|
|
@@ -6,16 +6,39 @@ name: Sync labels
|
|
|
6
6
|
on:
|
|
7
7
|
workflow_dispatch:
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
9
|
+
# Apply on merge, so that a regenerated labels file cannot sit unapplied.
|
|
10
|
+
push:
|
|
11
|
+
paths:
|
|
12
|
+
- .github/labels.yaml
|
|
12
13
|
|
|
14
|
+
# Preview on review: The check's log has a full computed diff, including deletions of labels not declared by the file.
|
|
15
|
+
pull_request:
|
|
16
|
+
paths:
|
|
17
|
+
- .github/labels.yaml
|
|
18
|
+
|
|
19
|
+
# Permissions are fixed when a run is created and cannot vary by trigger within a job,
|
|
20
|
+
# so applying and previewing are separate jobs.
|
|
13
21
|
jobs:
|
|
22
|
+
# The push arm applies on the default branch alone. A \`branches:\` filter cannot name the default branch,
|
|
23
|
+
# so that the gate lives on the job and this file stays identical across repos.
|
|
14
24
|
sync:
|
|
25
|
+
if: github.event_name != 'pull_request' && (github.event_name != 'push' || github.ref_name == github.event.repository.default_branch)
|
|
26
|
+
permissions:
|
|
27
|
+
contents: read
|
|
28
|
+
issues: write
|
|
29
|
+
uses: williamthorsen/node-monorepo-tools/.github/workflows/sync-labels.reusable.yaml@workflow/sync-labels-v1
|
|
30
|
+
|
|
31
|
+
check:
|
|
32
|
+
if: github.event_name == 'pull_request'
|
|
33
|
+
permissions:
|
|
34
|
+
contents: read
|
|
35
|
+
issues: read
|
|
15
36
|
uses: williamthorsen/node-monorepo-tools/.github/workflows/sync-labels.reusable.yaml@workflow/sync-labels-v1
|
|
37
|
+
with:
|
|
38
|
+
dry-run: true
|
|
16
39
|
`;
|
|
17
40
|
}
|
|
18
|
-
export function buildScopeLabels(workspacePaths) {
|
|
41
|
+
export function buildScopeLabels(workspacePaths, retiredNames = []) {
|
|
19
42
|
const labels = [
|
|
20
43
|
{ name: 'scope:root', color: '00ff96', description: 'Monorepo root configuration' },
|
|
21
44
|
];
|
|
@@ -23,29 +46,34 @@ export function buildScopeLabels(workspacePaths) {
|
|
|
23
46
|
const name = basename(workspacePath);
|
|
24
47
|
labels.push({ name: `scope:${name}`, color: '00ff96', description: `${name} package` });
|
|
25
48
|
}
|
|
49
|
+
for (const retiredName of retiredNames) {
|
|
50
|
+
labels.push({ name: `scope:${retiredName}`, color: '00ff96', description: `${retiredName} package (retired)` });
|
|
51
|
+
}
|
|
26
52
|
return labels;
|
|
27
53
|
}
|
|
28
54
|
function escapeForSingleQuotedString(value) {
|
|
29
55
|
return value.replace(/\\/g, '\\\\').replace(/'/g, String.raw `\'`);
|
|
30
56
|
}
|
|
31
|
-
export function
|
|
32
|
-
const
|
|
57
|
+
export function renderRepoLabelsBlock(scopeLabels) {
|
|
58
|
+
const entries = scopeLabels
|
|
33
59
|
.map((label) => {
|
|
34
60
|
const name = escapeForSingleQuotedString(label.name);
|
|
35
61
|
const color = escapeForSingleQuotedString(label.color);
|
|
36
62
|
const description = escapeForSingleQuotedString(label.description);
|
|
37
|
-
return `
|
|
63
|
+
return ` '${name}': { color: '${color}', description: '${description}' },`;
|
|
38
64
|
})
|
|
39
65
|
.join('\n');
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
};
|
|
66
|
+
const labelsBlock = entries === '' ? ' labels: {},' : ` labels: {\n${entries}\n },`;
|
|
67
|
+
return ` repoLabels: {
|
|
68
|
+
extends: ['common'],
|
|
69
|
+
${labelsBlock}
|
|
70
|
+
},`;
|
|
71
|
+
}
|
|
72
|
+
export function repoLabelsConfigScript(scopeLabels) {
|
|
73
|
+
return `import { defineConfig } from '@williamthorsen/release-kit';
|
|
48
74
|
|
|
49
|
-
export default
|
|
75
|
+
export default defineConfig({
|
|
76
|
+
${renderRepoLabelsBlock(scopeLabels)}
|
|
77
|
+
});
|
|
50
78
|
`;
|
|
51
79
|
}
|
package/dist/esm/types.d.ts
CHANGED
|
@@ -171,6 +171,19 @@ export declare const breakingPolicyValueSchema: z.ZodEnum<{
|
|
|
171
171
|
forbidden: "forbidden";
|
|
172
172
|
required: "required";
|
|
173
173
|
}>;
|
|
174
|
+
export declare const labelSpecSchema: z.ZodObject<{
|
|
175
|
+
color: z.ZodString;
|
|
176
|
+
description: z.ZodString;
|
|
177
|
+
}, z.core.$strict>;
|
|
178
|
+
export type LabelSpec = z.infer<typeof labelSpecSchema>;
|
|
179
|
+
export declare const repoLabelsSchema: z.ZodObject<{
|
|
180
|
+
extends: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
181
|
+
labels: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodObject<{
|
|
182
|
+
color: z.ZodString;
|
|
183
|
+
description: z.ZodString;
|
|
184
|
+
}, z.core.$strict>>>>;
|
|
185
|
+
}, z.core.$strict>;
|
|
186
|
+
export type RepoLabelsConfig = z.infer<typeof repoLabelsSchema>;
|
|
174
187
|
export declare const releaseKitConfigSchema: z.ZodObject<{
|
|
175
188
|
breakingPolicies: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodEnum<{
|
|
176
189
|
optional: "optional";
|
|
@@ -190,6 +203,13 @@ export declare const releaseKitConfigSchema: z.ZodObject<{
|
|
|
190
203
|
releaseNotes: z.ZodOptional<z.ZodObject<{
|
|
191
204
|
shouldInjectIntoReadme: z.ZodOptional<z.ZodBoolean>;
|
|
192
205
|
}, z.core.$strict>>;
|
|
206
|
+
repoLabels: z.ZodOptional<z.ZodObject<{
|
|
207
|
+
extends: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
208
|
+
labels: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNullable<z.ZodObject<{
|
|
209
|
+
color: z.ZodString;
|
|
210
|
+
description: z.ZodString;
|
|
211
|
+
}, z.core.$strict>>>>;
|
|
212
|
+
}, z.core.$strict>>;
|
|
193
213
|
retiredPackages: z.ZodOptional<z.ZodArray<z.ZodObject<{
|
|
194
214
|
name: z.ZodString;
|
|
195
215
|
tagPrefix: z.ZodString;
|
package/dist/esm/types.js
CHANGED
|
@@ -49,6 +49,18 @@ export const versionPatternsSchema = z
|
|
|
49
49
|
})
|
|
50
50
|
.strict();
|
|
51
51
|
export const breakingPolicyValueSchema = z.enum(['forbidden', 'optional', 'required']);
|
|
52
|
+
export const labelSpecSchema = z
|
|
53
|
+
.object({
|
|
54
|
+
color: z.string().min(1),
|
|
55
|
+
description: z.string(),
|
|
56
|
+
})
|
|
57
|
+
.strict();
|
|
58
|
+
export const repoLabelsSchema = z
|
|
59
|
+
.object({
|
|
60
|
+
extends: z.array(z.string().min(1)).optional(),
|
|
61
|
+
labels: z.record(z.string().min(1), labelSpecSchema.nullable()).optional(),
|
|
62
|
+
})
|
|
63
|
+
.strict();
|
|
52
64
|
export const releaseKitConfigSchema = z
|
|
53
65
|
.object({
|
|
54
66
|
breakingPolicies: z.record(z.string(), breakingPolicyValueSchema).optional(),
|
|
@@ -57,6 +69,7 @@ export const releaseKitConfigSchema = z
|
|
|
57
69
|
formatCommand: z.string().min(1).optional(),
|
|
58
70
|
project: projectConfigSchema.optional(),
|
|
59
71
|
releaseNotes: releaseNotesInputSchema.optional(),
|
|
72
|
+
repoLabels: repoLabelsSchema.optional(),
|
|
60
73
|
retiredPackages: z.array(retiredPackageSchema).optional(),
|
|
61
74
|
scopeAliases: z.record(z.string(), z.string()).optional(),
|
|
62
75
|
versionPatterns: versionPatternsSchema.optional(),
|
|
@@ -53,7 +53,7 @@ function writePreviewFile(filePath, content, dryRun) {
|
|
|
53
53
|
const result = writeFileWithCheck(filePath, content, { dryRun: false, overwrite: true });
|
|
54
54
|
if (result.outcome === 'failed') {
|
|
55
55
|
reportError(`Failed to write ${filePath}: ${result.error ?? 'unknown error'}`);
|
|
56
|
-
return { filePath, outcome: 'failed', ...(result.error
|
|
56
|
+
return { filePath, outcome: 'failed', ...(result.error !== undefined && { error: result.error }) };
|
|
57
57
|
}
|
|
58
58
|
if (result.outcome === 'created' || result.outcome === 'overwritten') {
|
|
59
59
|
console.info(dim(` Wrote ${filePath}`));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@williamthorsen/release-kit",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "9.0.0",
|
|
4
4
|
"description": "Version-bumping and changelog-generation toolkit for release workflows",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"homepage": "https://github.com/williamthorsen/node-monorepo-tools/tree/main/packages/release-kit#readme",
|
|
@@ -39,14 +39,14 @@
|
|
|
39
39
|
"semver": "7.8.5",
|
|
40
40
|
"yaml": "2.9.0",
|
|
41
41
|
"zod": "4.4.3",
|
|
42
|
-
"@williamthorsen/nmr-core": "0.7.
|
|
42
|
+
"@williamthorsen/nmr-core": "0.7.2"
|
|
43
43
|
},
|
|
44
44
|
"devDependencies": {
|
|
45
45
|
"@types/semver": "7.7.1",
|
|
46
46
|
"smol-toml": "1.7.0"
|
|
47
47
|
},
|
|
48
48
|
"engines": {
|
|
49
|
-
"node": ">=
|
|
49
|
+
"node": ">=24"
|
|
50
50
|
},
|
|
51
51
|
"publishConfig": {
|
|
52
52
|
"access": "public",
|
|
@@ -1,53 +0,0 @@
|
|
|
1
|
-
import { existsSync } from 'node:fs';
|
|
2
|
-
import path from 'node:path';
|
|
3
|
-
import { isRecord } from "../typeGuards.js";
|
|
4
|
-
export const SYNC_LABELS_CONFIG_PATH = '.config/sync-labels.config.ts';
|
|
5
|
-
export async function loadSyncLabelsConfig() {
|
|
6
|
-
const absoluteConfigPath = path.resolve(process.cwd(), SYNC_LABELS_CONFIG_PATH);
|
|
7
|
-
if (!existsSync(absoluteConfigPath)) {
|
|
8
|
-
return undefined;
|
|
9
|
-
}
|
|
10
|
-
const { createJiti } = await import('jiti');
|
|
11
|
-
const jiti = createJiti(import.meta.url);
|
|
12
|
-
const imported = await jiti.import(absoluteConfigPath);
|
|
13
|
-
if (!isRecord(imported)) {
|
|
14
|
-
throw new Error(`Config file must export an object, got ${Array.isArray(imported) ? 'array' : typeof imported}`);
|
|
15
|
-
}
|
|
16
|
-
const resolved = imported.default ?? imported.config;
|
|
17
|
-
if (resolved === undefined) {
|
|
18
|
-
throw new Error('Config file must have a default export or a named `config` export (e.g., `export default { ... }` or `export const config = { ... }`)');
|
|
19
|
-
}
|
|
20
|
-
return validateSyncLabelsConfig(resolved);
|
|
21
|
-
}
|
|
22
|
-
function validateSyncLabelsConfig(value) {
|
|
23
|
-
if (!isRecord(value)) {
|
|
24
|
-
throw new Error(`Config must be an object, got ${typeof value}`);
|
|
25
|
-
}
|
|
26
|
-
let presets;
|
|
27
|
-
if (value.presets !== undefined) {
|
|
28
|
-
if (!Array.isArray(value.presets) || !value.presets.every((p) => typeof p === 'string')) {
|
|
29
|
-
throw new Error('Config `presets` must be an array of strings');
|
|
30
|
-
}
|
|
31
|
-
presets = value.presets;
|
|
32
|
-
}
|
|
33
|
-
let labels;
|
|
34
|
-
if (value.labels !== undefined) {
|
|
35
|
-
if (!Array.isArray(value.labels)) {
|
|
36
|
-
throw new TypeError('Config `labels` must be an array');
|
|
37
|
-
}
|
|
38
|
-
labels = [];
|
|
39
|
-
for (const label of value.labels) {
|
|
40
|
-
if (!isRecord(label)) {
|
|
41
|
-
throw new Error('Each label must be an object with `name`, `color`, and `description`');
|
|
42
|
-
}
|
|
43
|
-
if (typeof label.name !== 'string' || typeof label.color !== 'string' || typeof label.description !== 'string') {
|
|
44
|
-
throw new TypeError('Each label must have string `name`, `color`, and `description` fields');
|
|
45
|
-
}
|
|
46
|
-
labels.push({ name: label.name, color: label.color, description: label.description });
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
return {
|
|
50
|
-
...(presets !== undefined && { presets }),
|
|
51
|
-
...(labels !== undefined && { labels }),
|
|
52
|
-
};
|
|
53
|
-
}
|