@williamthorsen/release-kit 0.2.3 → 1.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.
@@ -1,33 +1,38 @@
1
1
  import { execSync } from "node:child_process";
2
2
  import { bumpAllVersions } from "./bumpAllVersions.js";
3
+ import { DEFAULT_VERSION_PATTERNS, DEFAULT_WORK_TYPES } from "./defaults.js";
3
4
  import { determineBumpType } from "./determineBumpType.js";
4
5
  import { generateChangelog } from "./generateChangelogs.js";
5
6
  import { getCommitsSinceTarget } from "./getCommitsSinceTarget.js";
6
7
  import { parseCommitMessage } from "./parseCommitMessage.js";
7
8
  function releasePrepareMono(config, options) {
8
9
  const { dryRun, bumpOverride } = options;
10
+ const workTypes = config.workTypes ?? { ...DEFAULT_WORK_TYPES };
11
+ const versionPatterns = config.versionPatterns ?? { ...DEFAULT_VERSION_PATTERNS };
9
12
  const tags = [];
10
13
  for (const component of config.components) {
14
+ const name = component.dir;
11
15
  console.info(`
12
- Processing component: ${component.tagPrefix}`);
16
+ Processing component: ${name}`);
13
17
  console.info(" Finding commits since last release...");
14
18
  const { tag, commits } = getCommitsSinceTarget(component.tagPrefix, component.paths);
15
- console.info(` Found ${commits.length} commits since ${tag ?? "the beginning"}`);
19
+ const since = tag === void 0 ? "(no previous release found)" : `since ${tag}`;
20
+ console.info(` Found ${commits.length} commits ${since}`);
16
21
  if (commits.length === 0) {
17
- console.info(` No changes for ${component.tagPrefix}. Skipping.`);
22
+ console.info(` No changes for ${name} ${since}. Skipping.`);
18
23
  continue;
19
24
  }
20
25
  let releaseType;
21
26
  if (bumpOverride === void 0) {
22
- const parsedCommits = commits.map((c) => parseCommitMessage(c.message, c.hash, config.workTypes, config.workspaceAliases)).filter((c) => c !== void 0);
27
+ const parsedCommits = commits.map((c) => parseCommitMessage(c.message, c.hash, workTypes, config.workspaceAliases)).filter((c) => c !== void 0);
23
28
  console.info(` Parsed ${parsedCommits.length} typed commits`);
24
- releaseType = determineBumpType(parsedCommits, config.workTypes);
29
+ releaseType = determineBumpType(parsedCommits, workTypes, versionPatterns);
25
30
  } else {
26
31
  releaseType = bumpOverride;
27
32
  console.info(` Using bump override: ${releaseType}`);
28
33
  }
29
34
  if (releaseType === void 0) {
30
- console.info(` No release-worthy changes for ${component.tagPrefix}. Skipping.`);
35
+ console.info(` No release-worthy changes for ${name} ${since}. Skipping.`);
31
36
  continue;
32
37
  }
33
38
  console.info(` Bumping versions (${releaseType})...`);
@@ -1,3 +1,9 @@
1
- import type { MonorepoReleaseConfig, ReleaseConfig } from './types.ts';
2
- export declare const RELEASE_TAGS_FILE = ".release-tags";
1
+ import type { MonorepoReleaseConfig, ReleaseConfig, ReleaseType } from './types.ts';
2
+ export declare const RELEASE_TAGS_FILE = "/tmp/release-kit/.release-tags";
3
+ export declare function parseArgs(argv: string[]): {
4
+ dryRun: boolean;
5
+ bumpOverride: ReleaseType | undefined;
6
+ only: string[] | undefined;
7
+ };
3
8
  export declare function runReleasePrepare(config: MonorepoReleaseConfig | ReleaseConfig): void;
9
+ export declare function writeReleaseTags(tags: string[], dryRun: boolean): void;
@@ -1,7 +1,8 @@
1
- import { writeFileSync } from "node:fs";
1
+ import { mkdirSync, writeFileSync } from "node:fs";
2
+ import { dirname } from "node:path";
2
3
  import { releasePrepare } from "./releasePrepare.js";
3
4
  import { releasePrepareMono } from "./releasePrepareMono.js";
4
- const RELEASE_TAGS_FILE = ".release-tags";
5
+ const RELEASE_TAGS_FILE = "/tmp/release-kit/.release-tags";
5
6
  const VALID_BUMP_TYPES = ["major", "minor", "patch"];
6
7
  function isReleaseType(value) {
7
8
  return VALID_BUMP_TYPES.includes(value);
@@ -13,18 +14,14 @@ function showHelp() {
13
14
  console.info(`
14
15
  Usage: runReleasePrepare [options]
15
16
 
16
- This script is designed for CI use via: gh workflow run release.yaml
17
+ Legacy entry point for release preparation. Prefer the CLI:
18
+ npx @williamthorsen/release-kit prepare
17
19
 
18
20
  Options:
19
21
  --dry-run Run without modifying any files
20
22
  --bump=major|minor|patch Override the bump type for all components
21
23
  --only=name1,name2 Only process the named components (comma-separated, monorepo only)
22
24
  --help Show this help message
23
-
24
- Examples:
25
- tsx .github/scripts/release-prepare.ts --dry-run
26
- tsx .github/scripts/release-prepare.ts --bump=minor
27
- tsx .github/scripts/release-prepare.ts --only=arrays,strings --dry-run
28
25
  `);
29
26
  }
30
27
  function parseArgs(argv) {
@@ -77,19 +74,14 @@ function runReleasePrepare(config) {
77
74
  }
78
75
  let effectiveConfig = config;
79
76
  if (only !== void 0) {
80
- const knownPrefixes = config.components.map((c) => c.tagPrefix);
81
- const filtered = config.components.filter((c) => {
82
- const name = c.tagPrefix.replace(/-v$/, "");
83
- return only.includes(name);
84
- });
77
+ const knownNames = config.components.map((c) => c.dir);
85
78
  for (const name of only) {
86
- const matchesKnown = knownPrefixes.includes(`${name}-v`);
87
- if (!matchesKnown) {
88
- const knownNames = knownPrefixes.map((p) => p.replace(/-v$/, "")).join(", ");
89
- console.error(`Error: Unknown component "${name}". Known components: ${knownNames}`);
79
+ if (!knownNames.includes(name)) {
80
+ console.error(`Error: Unknown component "${name}". Known components: ${knownNames.join(", ")}`);
90
81
  process.exit(1);
91
82
  }
92
83
  }
84
+ const filtered = config.components.filter((c) => only.includes(c.dir));
93
85
  effectiveConfig = { ...config, components: filtered };
94
86
  }
95
87
  try {
@@ -108,10 +100,13 @@ function writeReleaseTags(tags, dryRun) {
108
100
  console.info(` [dry-run] Would write ${RELEASE_TAGS_FILE}: ${tags.join(" ")}`);
109
101
  return;
110
102
  }
103
+ mkdirSync(dirname(RELEASE_TAGS_FILE), { recursive: true });
111
104
  writeFileSync(RELEASE_TAGS_FILE, tags.join("\n"), "utf8");
112
105
  console.info(` Wrote ${RELEASE_TAGS_FILE}: ${tags.join(" ")}`);
113
106
  }
114
107
  export {
115
108
  RELEASE_TAGS_FILE,
116
- runReleasePrepare
109
+ parseArgs,
110
+ runReleasePrepare,
111
+ writeReleaseTags
117
112
  };
@@ -1,10 +1,25 @@
1
1
  export type ReleaseType = 'major' | 'minor' | 'patch';
2
2
  export interface WorkTypeConfig {
3
- type: string;
4
3
  header: string;
5
- bump: ReleaseType;
6
4
  aliases?: string[];
7
5
  }
6
+ export interface VersionPatterns {
7
+ major: string[];
8
+ minor: string[];
9
+ }
10
+ export interface ReleaseKitConfig {
11
+ components?: ComponentOverride[];
12
+ versionPatterns?: VersionPatterns;
13
+ workTypes?: Record<string, WorkTypeConfig>;
14
+ formatCommand?: string;
15
+ cliffConfigPath?: string;
16
+ workspaceAliases?: Record<string, string>;
17
+ }
18
+ export interface ComponentOverride {
19
+ dir: string;
20
+ tagPrefix?: string;
21
+ shouldExclude?: boolean;
22
+ }
8
23
  export interface Commit {
9
24
  message: string;
10
25
  hash: string;
@@ -18,6 +33,7 @@ export interface ParsedCommit {
18
33
  breaking: boolean;
19
34
  }
20
35
  export interface ComponentConfig {
36
+ dir: string;
21
37
  tagPrefix: string;
22
38
  packageFiles: string[];
23
39
  changelogPaths: string[];
@@ -25,7 +41,8 @@ export interface ComponentConfig {
25
41
  }
26
42
  export interface MonorepoReleaseConfig {
27
43
  components: ComponentConfig[];
28
- workTypes: WorkTypeConfig[];
44
+ workTypes?: Record<string, WorkTypeConfig>;
45
+ versionPatterns?: VersionPatterns;
29
46
  formatCommand?: string;
30
47
  cliffConfigPath?: string;
31
48
  workspaceAliases?: Record<string, string>;
@@ -34,7 +51,8 @@ export interface ReleaseConfig {
34
51
  tagPrefix: string;
35
52
  packageFiles: string[];
36
53
  changelogPaths: string[];
37
- workTypes: WorkTypeConfig[];
54
+ workTypes?: Record<string, WorkTypeConfig>;
55
+ versionPatterns?: VersionPatterns;
38
56
  formatCommand?: string;
39
57
  cliffConfigPath?: string;
40
58
  workspaceAliases?: Record<string, string>;
@@ -0,0 +1,5 @@
1
+ import type { ReleaseKitConfig } from './types.ts';
2
+ export declare function validateConfig(raw: unknown): {
3
+ config: ReleaseKitConfig;
4
+ errors: string[];
5
+ };
@@ -0,0 +1,143 @@
1
+ function validateConfig(raw) {
2
+ const errors = [];
3
+ if (!isRecord(raw)) {
4
+ return { config: {}, errors: ["Config must be an object"] };
5
+ }
6
+ const config = {};
7
+ const knownFields = /* @__PURE__ */ new Set([
8
+ "components",
9
+ "versionPatterns",
10
+ "workTypes",
11
+ "formatCommand",
12
+ "cliffConfigPath",
13
+ "workspaceAliases"
14
+ ]);
15
+ for (const key of Object.keys(raw)) {
16
+ if (!knownFields.has(key)) {
17
+ errors.push(`Unknown field: '${key}'`);
18
+ }
19
+ }
20
+ validateComponents(raw.components, config, errors);
21
+ validateVersionPatterns(raw.versionPatterns, config, errors);
22
+ validateWorkTypes(raw.workTypes, config, errors);
23
+ validateStringField("formatCommand", raw.formatCommand, config, errors);
24
+ validateStringField("cliffConfigPath", raw.cliffConfigPath, config, errors);
25
+ validateWorkspaceAliases(raw.workspaceAliases, config, errors);
26
+ return { config, errors };
27
+ }
28
+ function isRecord(value) {
29
+ return typeof value === "object" && value !== null && !Array.isArray(value);
30
+ }
31
+ function isStringArray(value) {
32
+ return Array.isArray(value) && value.every((item) => typeof item === "string");
33
+ }
34
+ function validateComponents(value, config, errors) {
35
+ if (value === void 0) return;
36
+ if (!Array.isArray(value)) {
37
+ errors.push("'components' must be an array");
38
+ return;
39
+ }
40
+ const components = [];
41
+ for (const [i, entry] of value.entries()) {
42
+ if (!isRecord(entry)) {
43
+ errors.push(`components[${i}]: must be an object`);
44
+ continue;
45
+ }
46
+ if (typeof entry.dir !== "string" || entry.dir === "") {
47
+ errors.push(`components[${i}]: 'dir' is required`);
48
+ continue;
49
+ }
50
+ const component = { dir: entry.dir };
51
+ if (entry.tagPrefix !== void 0) {
52
+ if (typeof entry.tagPrefix === "string") {
53
+ component.tagPrefix = entry.tagPrefix;
54
+ } else {
55
+ errors.push(`components[${i}]: 'tagPrefix' must be a string`);
56
+ }
57
+ }
58
+ if (entry.shouldExclude !== void 0) {
59
+ if (typeof entry.shouldExclude === "boolean") {
60
+ component.shouldExclude = entry.shouldExclude;
61
+ } else {
62
+ errors.push(`components[${i}]: 'shouldExclude' must be a boolean`);
63
+ }
64
+ }
65
+ components.push(component);
66
+ }
67
+ config.components = components;
68
+ }
69
+ function validateVersionPatterns(value, config, errors) {
70
+ if (value === void 0) return;
71
+ if (!isRecord(value)) {
72
+ errors.push("'versionPatterns' must be an object");
73
+ return;
74
+ }
75
+ if (!isStringArray(value.major)) {
76
+ errors.push("versionPatterns.major: expected string array");
77
+ }
78
+ if (!isStringArray(value.minor)) {
79
+ errors.push("versionPatterns.minor: expected string array");
80
+ }
81
+ if (isStringArray(value.major) && isStringArray(value.minor)) {
82
+ config.versionPatterns = { major: value.major, minor: value.minor };
83
+ }
84
+ }
85
+ function validateWorkTypes(value, config, errors) {
86
+ if (value === void 0) return;
87
+ if (!isRecord(value) || Array.isArray(value)) {
88
+ errors.push("'workTypes' must be a record (object)");
89
+ return;
90
+ }
91
+ const workTypes = {};
92
+ for (const [key, entry] of Object.entries(value)) {
93
+ if (!isRecord(entry)) {
94
+ errors.push(`workTypes.${key}: must be an object`);
95
+ continue;
96
+ }
97
+ if (typeof entry.header !== "string") {
98
+ errors.push(`workTypes.${key}: 'header' is required and must be a string`);
99
+ continue;
100
+ }
101
+ const wtEntry = { header: entry.header };
102
+ if (entry.aliases !== void 0) {
103
+ if (isStringArray(entry.aliases)) {
104
+ wtEntry.aliases = entry.aliases;
105
+ } else {
106
+ errors.push(`workTypes.${key}: 'aliases' must be a string array`);
107
+ }
108
+ }
109
+ workTypes[key] = wtEntry;
110
+ }
111
+ config.workTypes = workTypes;
112
+ }
113
+ function validateStringField(fieldName, value, config, errors) {
114
+ if (value === void 0) return;
115
+ if (typeof value !== "string") {
116
+ errors.push(`'${fieldName}' must be a string`);
117
+ return;
118
+ }
119
+ config[fieldName] = value;
120
+ }
121
+ function validateWorkspaceAliases(value, config, errors) {
122
+ if (value === void 0) return;
123
+ if (!isRecord(value)) {
124
+ errors.push("'workspaceAliases' must be a record (object)");
125
+ return;
126
+ }
127
+ const aliases = {};
128
+ let valid = true;
129
+ for (const [key, v] of Object.entries(value)) {
130
+ if (typeof v === "string") {
131
+ aliases[key] = v;
132
+ } else {
133
+ errors.push(`workspaceAliases.${key}: value must be a string`);
134
+ valid = false;
135
+ }
136
+ }
137
+ if (valid) {
138
+ config.workspaceAliases = aliases;
139
+ }
140
+ }
141
+ export {
142
+ validateConfig
143
+ };