@afoures/auto-release 0.2.11 → 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/README.md +30 -269
- package/dist/lib/change-file.mjs +4 -3
- package/dist/lib/commands/check.mjs +10 -10
- package/dist/lib/commands/generate-release-pr.mjs +22 -22
- package/dist/lib/commands/init.mjs +40 -199
- package/dist/lib/commands/list.mjs +10 -10
- package/dist/lib/commands/manual-release.mjs +54 -35
- package/dist/lib/commands/record-change.mjs +19 -19
- package/dist/lib/commands/tag-release-commit.mjs +36 -30
- package/dist/lib/config.d.mts +8 -2
- package/dist/lib/config.mjs +13 -13
- package/dist/lib/formatter.mjs +19 -18
- package/dist/lib/types.d.mts +11 -5
- package/dist/lib/utils/branch-protection.mjs +0 -3
- package/dist/lib/utils/git.mjs +1 -1
- package/dist/lib/utils/version.mjs +3 -3
- package/dist/lib/versioning/types.d.mts +4 -4
- package/docs/change-file-anatomy.md +31 -0
- package/docs/commands.md +80 -0
- package/docs/configuration.md +116 -0
- package/docs/lexicon.md +23 -0
- package/docs/recommended-usage.md +155 -0
- package/package.json +15 -14
|
@@ -7,19 +7,21 @@ import { access, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
|
7
7
|
|
|
8
8
|
//#region src/lib/commands/init.ts
|
|
9
9
|
function generate_config_source(options) {
|
|
10
|
-
const {
|
|
10
|
+
const { projects, changes_dir, target_branch, default_release_branch_prefix = "release", git } = options;
|
|
11
11
|
const imports = ["import { define_config } from \"@afoures/auto-release\";"];
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
if (projects.length > 0) {
|
|
13
|
+
const component_types = /* @__PURE__ */ new Set();
|
|
14
|
+
for (const project of projects) for (const component of project.components) component_types.add(component.type);
|
|
15
|
+
if (component_types.size > 0) {
|
|
16
|
+
const components = Array.from(component_types).sort();
|
|
17
|
+
imports.push(`import { ${components.join(", ")} } from "@afoures/auto-release/components";`);
|
|
18
|
+
}
|
|
19
|
+
const versioning_types = /* @__PURE__ */ new Set();
|
|
20
|
+
for (const project of projects) versioning_types.add(project.versioning);
|
|
21
|
+
if (versioning_types.size > 0) {
|
|
22
|
+
const versioning = Array.from(versioning_types).sort();
|
|
23
|
+
imports.push(`import { ${versioning.join(", ")} } from "@afoures/auto-release/versioning";`);
|
|
24
|
+
}
|
|
23
25
|
}
|
|
24
26
|
if (git.platform === "github") imports.push("import { github } from \"@afoures/auto-release/platforms\";");
|
|
25
27
|
else imports.push("import { gitlab } from \"@afoures/auto-release/platforms\";");
|
|
@@ -29,31 +31,31 @@ function generate_config_source(options) {
|
|
|
29
31
|
"export default define_config({"
|
|
30
32
|
];
|
|
31
33
|
if (changes_dir !== ".changes") lines.push(` changes_dir: ${JSON.stringify(changes_dir)},`);
|
|
32
|
-
lines.push("
|
|
33
|
-
|
|
34
|
-
lines.push(` ${JSON.stringify(
|
|
34
|
+
lines.push(" projects: {");
|
|
35
|
+
projects.forEach((project, index) => {
|
|
36
|
+
lines.push(` ${JSON.stringify(project.name)}: {`);
|
|
35
37
|
lines.push(" components: [");
|
|
36
|
-
|
|
38
|
+
project.components.forEach((component) => {
|
|
37
39
|
lines.push(` ${component.type}(${JSON.stringify(component.path)}),`);
|
|
38
40
|
});
|
|
39
41
|
lines.push(" ],");
|
|
40
|
-
lines.push(` versioning: ${
|
|
41
|
-
lines.push(` changelog: ${JSON.stringify(
|
|
42
|
-
lines.push(index ===
|
|
42
|
+
lines.push(` versioning: ${project.versioning}(),`);
|
|
43
|
+
lines.push(` changelog: ${JSON.stringify(project.changelog_path)},`);
|
|
44
|
+
lines.push(index === projects.length - 1 ? " }," : " },");
|
|
43
45
|
});
|
|
44
46
|
lines.push(" },");
|
|
45
47
|
lines.push(" git: {");
|
|
46
48
|
if (git.platform === "github") {
|
|
47
49
|
lines.push(" platform: github({");
|
|
48
|
-
lines.push(` token: process.env.${git.token_env}!,`);
|
|
49
50
|
lines.push(` owner: ${JSON.stringify(git.owner)},`);
|
|
50
51
|
lines.push(` repo: ${JSON.stringify(git.repo)},`);
|
|
52
|
+
lines.push(` token: undefined,`);
|
|
51
53
|
lines.push(" }),");
|
|
52
54
|
} else {
|
|
53
55
|
lines.push(" platform: gitlab({");
|
|
54
|
-
lines.push(` token: process.env.${git.token_env}!,`);
|
|
55
56
|
lines.push(` project_id: ${JSON.stringify(git.project_id)},`);
|
|
56
57
|
if (git.host) lines.push(` host: ${JSON.stringify(git.host)},`);
|
|
58
|
+
lines.push(` token: undefined,`);
|
|
57
59
|
lines.push(" }),");
|
|
58
60
|
}
|
|
59
61
|
if (target_branch !== "main") lines.push(` target_branch: ${JSON.stringify(target_branch)},`);
|
|
@@ -68,9 +70,6 @@ const PACKAGE_MANAGER_LOCKS = {
|
|
|
68
70
|
yarn: ["yarn.lock"],
|
|
69
71
|
bun: ["bun.lockb", "bun.lock"]
|
|
70
72
|
};
|
|
71
|
-
function normalize_app_name(input) {
|
|
72
|
-
return input.trim().toLowerCase().replace(/[^a-z0-9-]/g, "-").replace(/-{2,}/g, "-").replace(/^-|-$/g, "") || "app";
|
|
73
|
-
}
|
|
74
73
|
async function path_exists(target) {
|
|
75
74
|
try {
|
|
76
75
|
await access(target, constants.F_OK);
|
|
@@ -94,6 +93,14 @@ function get_install_command(package_manager) {
|
|
|
94
93
|
default: return "npm install --save-dev @afoures/auto-release";
|
|
95
94
|
}
|
|
96
95
|
}
|
|
96
|
+
function get_exec_list_command(package_manager) {
|
|
97
|
+
switch (package_manager) {
|
|
98
|
+
case "pnpm": return "pnpm exec auto-release list";
|
|
99
|
+
case "yarn": return "yarn exec auto-release list";
|
|
100
|
+
case "bun": return "bunx auto-release list";
|
|
101
|
+
default: return "npx auto-release list";
|
|
102
|
+
}
|
|
103
|
+
}
|
|
97
104
|
async function ensure_package_json(path) {
|
|
98
105
|
if (await path_exists(path)) {
|
|
99
106
|
const content = await readFile(path, "utf-8");
|
|
@@ -108,21 +115,6 @@ async function ensure_package_json(path) {
|
|
|
108
115
|
log.info(`Created package.json for ${template.name}`);
|
|
109
116
|
return template;
|
|
110
117
|
}
|
|
111
|
-
async function create_changes_directories(cwd, changes_dir, apps) {
|
|
112
|
-
const root_dir = join(cwd, changes_dir);
|
|
113
|
-
await mkdir(root_dir, { recursive: true });
|
|
114
|
-
for (const app of apps) await mkdir(join(root_dir, app.name), { recursive: true });
|
|
115
|
-
}
|
|
116
|
-
async function ensure_changelog(app, cwd) {
|
|
117
|
-
const absolute_path = join(cwd, app.changelog_path);
|
|
118
|
-
await mkdir(dirname(absolute_path), { recursive: true });
|
|
119
|
-
if (!await path_exists(absolute_path)) await writeFile(absolute_path, `# ${app.name} changelog\n\nAll notable changes to this project will be documented in this file.\n`, "utf-8");
|
|
120
|
-
}
|
|
121
|
-
function sanitize_env_var(value, fallback) {
|
|
122
|
-
const trimmed = value.trim();
|
|
123
|
-
if (!trimmed) return fallback;
|
|
124
|
-
return trimmed.replace(/[^a-zA-Z0-9_]/g, "_").toUpperCase();
|
|
125
|
-
}
|
|
126
118
|
const init = create_command({
|
|
127
119
|
name: "init",
|
|
128
120
|
description: "Set up auto-release in the current repository",
|
|
@@ -185,16 +177,6 @@ const init = create_command({
|
|
|
185
177
|
return { status: "success" };
|
|
186
178
|
}
|
|
187
179
|
const changes_dir = changes_dir_input.trim();
|
|
188
|
-
const release_prefix_input = await text({
|
|
189
|
-
message: "Release branch prefix",
|
|
190
|
-
initialValue: "release",
|
|
191
|
-
validate: (value = "") => value.trim().length === 0 ? "Release branch prefix cannot be empty" : void 0
|
|
192
|
-
});
|
|
193
|
-
if (isCancel(release_prefix_input)) {
|
|
194
|
-
cancel("Initialization cancelled");
|
|
195
|
-
return { status: "success" };
|
|
196
|
-
}
|
|
197
|
-
const default_release_branch_prefix = release_prefix_input.trim();
|
|
198
180
|
const target_branch_input = await text({
|
|
199
181
|
message: "Target branch (main branch for PRs)",
|
|
200
182
|
initialValue: "main",
|
|
@@ -205,128 +187,6 @@ const init = create_command({
|
|
|
205
187
|
return { status: "success" };
|
|
206
188
|
}
|
|
207
189
|
const target_branch = target_branch_input.trim();
|
|
208
|
-
const app_count_input = await text({
|
|
209
|
-
message: "How many apps should auto-release manage?",
|
|
210
|
-
initialValue: "1",
|
|
211
|
-
validate: (value = "") => {
|
|
212
|
-
const parsed = Number.parseInt(value, 10);
|
|
213
|
-
return Number.isNaN(parsed) || parsed <= 0 ? "Enter a positive number" : void 0;
|
|
214
|
-
}
|
|
215
|
-
});
|
|
216
|
-
if (isCancel(app_count_input)) {
|
|
217
|
-
cancel("Initialization cancelled");
|
|
218
|
-
return { status: "success" };
|
|
219
|
-
}
|
|
220
|
-
const app_count = Number.parseInt(app_count_input, 10);
|
|
221
|
-
const apps = [];
|
|
222
|
-
for (let index = 0; index < app_count; index++) {
|
|
223
|
-
const default_name = index === 0 && package_json.name ? normalize_app_name(package_json.name) : `app-${index + 1}`;
|
|
224
|
-
const app_name_input = await text({
|
|
225
|
-
message: `App #${index + 1} name (lowercase, no spaces)`,
|
|
226
|
-
initialValue: default_name,
|
|
227
|
-
validate: (value = "") => value.trim().length === 0 ? "App name is required" : void 0
|
|
228
|
-
});
|
|
229
|
-
if (isCancel(app_name_input)) {
|
|
230
|
-
cancel("Initialization cancelled");
|
|
231
|
-
return { status: "success" };
|
|
232
|
-
}
|
|
233
|
-
const app_name = normalize_app_name(app_name_input);
|
|
234
|
-
const component_count_input = await text({
|
|
235
|
-
message: `How many components for ${app_name}?`,
|
|
236
|
-
initialValue: "1",
|
|
237
|
-
validate: (value = "") => {
|
|
238
|
-
const parsed = Number.parseInt(value, 10);
|
|
239
|
-
return Number.isNaN(parsed) || parsed <= 0 ? "Enter a positive number" : void 0;
|
|
240
|
-
}
|
|
241
|
-
});
|
|
242
|
-
if (isCancel(component_count_input)) {
|
|
243
|
-
cancel("Initialization cancelled");
|
|
244
|
-
return { status: "success" };
|
|
245
|
-
}
|
|
246
|
-
const component_count = Number.parseInt(component_count_input, 10);
|
|
247
|
-
const components = [];
|
|
248
|
-
for (let comp_index = 0; comp_index < component_count; comp_index++) {
|
|
249
|
-
const component_type_choice = await select({
|
|
250
|
-
message: `Component #${comp_index + 1} type for ${app_name}`,
|
|
251
|
-
options: [
|
|
252
|
-
{
|
|
253
|
-
value: "node",
|
|
254
|
-
label: "Node (package.json)"
|
|
255
|
-
},
|
|
256
|
-
{
|
|
257
|
-
value: "bun",
|
|
258
|
-
label: "Bun (package.json)"
|
|
259
|
-
},
|
|
260
|
-
{
|
|
261
|
-
value: "expo",
|
|
262
|
-
label: "Expo (package.json + app.json)"
|
|
263
|
-
},
|
|
264
|
-
{
|
|
265
|
-
value: "php",
|
|
266
|
-
label: "PHP (composer.json)"
|
|
267
|
-
}
|
|
268
|
-
]
|
|
269
|
-
});
|
|
270
|
-
if (isCancel(component_type_choice)) {
|
|
271
|
-
cancel("Initialization cancelled");
|
|
272
|
-
return { status: "success" };
|
|
273
|
-
}
|
|
274
|
-
const component_type = component_type_choice;
|
|
275
|
-
const default_path = app_count === 1 && component_count === 1 ? "." : `apps/${app_name}`;
|
|
276
|
-
const component_path_input = await text({
|
|
277
|
-
message: `Component #${comp_index + 1} path for ${app_name}`,
|
|
278
|
-
initialValue: default_path,
|
|
279
|
-
validate: (value = "") => value.trim().length === 0 ? "Component path is required" : void 0
|
|
280
|
-
});
|
|
281
|
-
if (isCancel(component_path_input)) {
|
|
282
|
-
cancel("Initialization cancelled");
|
|
283
|
-
return { status: "success" };
|
|
284
|
-
}
|
|
285
|
-
const component_path = component_path_input.trim();
|
|
286
|
-
components.push({
|
|
287
|
-
type: component_type,
|
|
288
|
-
path: component_path
|
|
289
|
-
});
|
|
290
|
-
}
|
|
291
|
-
const changelog_input = await text({
|
|
292
|
-
message: `Changelog path for ${app_name}`,
|
|
293
|
-
initialValue: app_count === 1 ? "CHANGELOG.md" : `apps/${app_name}/CHANGELOG.md`,
|
|
294
|
-
validate: (value = "") => value.trim().length === 0 ? "Changelog path is required" : void 0
|
|
295
|
-
});
|
|
296
|
-
if (isCancel(changelog_input)) {
|
|
297
|
-
cancel("Initialization cancelled");
|
|
298
|
-
return { status: "success" };
|
|
299
|
-
}
|
|
300
|
-
const changelog_path = changelog_input.trim();
|
|
301
|
-
const version_choice = await select({
|
|
302
|
-
message: `Versioning strategy for ${app_name}`,
|
|
303
|
-
initialValue: "semver",
|
|
304
|
-
options: [
|
|
305
|
-
{
|
|
306
|
-
value: "semver",
|
|
307
|
-
label: "Semver (1.2.3) - major, minor, patch"
|
|
308
|
-
},
|
|
309
|
-
{
|
|
310
|
-
value: "calver",
|
|
311
|
-
label: "Calver (YYYY.MM.micro) - feature, fix"
|
|
312
|
-
},
|
|
313
|
-
{
|
|
314
|
-
value: "markver",
|
|
315
|
-
label: "Markver (1.0.0) - marketing, feature, fix"
|
|
316
|
-
}
|
|
317
|
-
]
|
|
318
|
-
});
|
|
319
|
-
if (isCancel(version_choice)) {
|
|
320
|
-
cancel("Initialization cancelled");
|
|
321
|
-
return { status: "success" };
|
|
322
|
-
}
|
|
323
|
-
apps.push({
|
|
324
|
-
name: app_name,
|
|
325
|
-
components,
|
|
326
|
-
changelog_path,
|
|
327
|
-
versioning: version_choice
|
|
328
|
-
});
|
|
329
|
-
}
|
|
330
190
|
const git_choice = await select({
|
|
331
191
|
message: "Which git platform do you use?",
|
|
332
192
|
options: [{
|
|
@@ -354,26 +214,17 @@ const init = create_command({
|
|
|
354
214
|
}
|
|
355
215
|
const repo_input = await text({
|
|
356
216
|
message: "GitHub repository name",
|
|
357
|
-
initialValue: package_json.name ? package_json.name.replace(/^@[^/]+\//, "") :
|
|
217
|
+
initialValue: package_json.name ? package_json.name.replace(/^@[^/]+\//, "") : "",
|
|
358
218
|
validate: (value = "") => value.trim().length === 0 ? "Repository is required" : void 0
|
|
359
219
|
});
|
|
360
220
|
if (isCancel(repo_input)) {
|
|
361
221
|
cancel("Initialization cancelled");
|
|
362
222
|
return { status: "success" };
|
|
363
223
|
}
|
|
364
|
-
const token_env_input = await text({
|
|
365
|
-
message: "Environment variable for the GitHub token",
|
|
366
|
-
initialValue: "GITHUB_TOKEN"
|
|
367
|
-
});
|
|
368
|
-
if (isCancel(token_env_input)) {
|
|
369
|
-
cancel("Initialization cancelled");
|
|
370
|
-
return { status: "success" };
|
|
371
|
-
}
|
|
372
224
|
git_answers = {
|
|
373
225
|
platform: "github",
|
|
374
226
|
owner: owner_input.trim(),
|
|
375
|
-
repo: repo_input.trim()
|
|
376
|
-
token_env: sanitize_env_var(token_env_input, "GITHUB_TOKEN")
|
|
227
|
+
repo: repo_input.trim()
|
|
377
228
|
};
|
|
378
229
|
} else {
|
|
379
230
|
const project_input = await text({
|
|
@@ -392,19 +243,10 @@ const init = create_command({
|
|
|
392
243
|
cancel("Initialization cancelled");
|
|
393
244
|
return { status: "success" };
|
|
394
245
|
}
|
|
395
|
-
const token_env_input = await text({
|
|
396
|
-
message: "Environment variable for the GitLab token",
|
|
397
|
-
initialValue: "GITLAB_TOKEN"
|
|
398
|
-
});
|
|
399
|
-
if (isCancel(token_env_input)) {
|
|
400
|
-
cancel("Initialization cancelled");
|
|
401
|
-
return { status: "success" };
|
|
402
|
-
}
|
|
403
246
|
git_answers = {
|
|
404
247
|
platform: "gitlab",
|
|
405
248
|
project_id: project_input.trim(),
|
|
406
|
-
host: host_input.trim() || void 0
|
|
407
|
-
token_env: sanitize_env_var(token_env_input, "GITLAB_TOKEN")
|
|
249
|
+
host: host_input.trim() || void 0
|
|
408
250
|
};
|
|
409
251
|
}
|
|
410
252
|
const config_path = join(context.cwd, "auto-release.config.ts");
|
|
@@ -424,19 +266,18 @@ const init = create_command({
|
|
|
424
266
|
}
|
|
425
267
|
}
|
|
426
268
|
await writeFile(config_path, generate_config_source({
|
|
427
|
-
|
|
269
|
+
projects: [],
|
|
428
270
|
changes_dir,
|
|
429
271
|
target_branch,
|
|
430
|
-
default_release_branch_prefix,
|
|
431
272
|
git: git_answers
|
|
432
273
|
}), "utf-8");
|
|
433
|
-
await
|
|
434
|
-
for (const app of apps) await ensure_changelog(app, context.cwd);
|
|
274
|
+
await mkdir(join(context.cwd, changes_dir), { recursive: true });
|
|
435
275
|
log.success("Generated auto-release.config.ts");
|
|
436
276
|
log.success(`Change files directory: ${changes_dir}`);
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
277
|
+
log.message("");
|
|
278
|
+
log.message("Next: add projects to auto-release.config.ts");
|
|
279
|
+
log.message(" Edit the `projects` object and add entries with components, versioning, and changelog path.");
|
|
280
|
+
log.message(` Then run \`${get_exec_list_command(package_manager)}\` to verify.`);
|
|
440
281
|
outro("auto-release init complete!");
|
|
441
282
|
return {
|
|
442
283
|
status: "success",
|
|
@@ -8,7 +8,7 @@ import { relative } from "node:path";
|
|
|
8
8
|
//#region src/lib/commands/list.ts
|
|
9
9
|
const list = create_command({
|
|
10
10
|
name: "list",
|
|
11
|
-
description: "List all registered
|
|
11
|
+
description: "List all registered projects with their current version and registered components",
|
|
12
12
|
schema: { config: {
|
|
13
13
|
type: "string",
|
|
14
14
|
description: "Path to config file",
|
|
@@ -26,25 +26,25 @@ const list = create_command({
|
|
|
26
26
|
},
|
|
27
27
|
run: async ({ context: { config, root } }) => {
|
|
28
28
|
const logger = create_logger();
|
|
29
|
-
if (config.
|
|
30
|
-
logger.info("no
|
|
29
|
+
if (config.managed_projects.length === 0) {
|
|
30
|
+
logger.info("no projects registered.");
|
|
31
31
|
return { status: "success" };
|
|
32
32
|
}
|
|
33
|
-
const count = config.
|
|
33
|
+
const count = config.managed_projects.length;
|
|
34
34
|
const warnings = [];
|
|
35
|
-
logger.info(`found ${count}
|
|
35
|
+
logger.info(`found ${count} project${count > 1 ? "s" : ""}:`);
|
|
36
36
|
logger.info("");
|
|
37
|
-
for (const
|
|
38
|
-
const version = await compute_current_version(
|
|
37
|
+
for (const project of config.managed_projects) {
|
|
38
|
+
const version = await compute_current_version(project, { get_file_content: (file_path) => read_file(file_path) });
|
|
39
39
|
if (version === null) {
|
|
40
|
-
warnings.push(`${
|
|
40
|
+
warnings.push(`${project.name} has no version`);
|
|
41
41
|
continue;
|
|
42
42
|
}
|
|
43
|
-
const parts =
|
|
43
|
+
const parts = project.components.flatMap((component) => component.parts.map((part) => ({
|
|
44
44
|
relative_path: relative(root, part.file),
|
|
45
45
|
missing: part.exists === false
|
|
46
46
|
})));
|
|
47
|
-
logger.note(`${
|
|
47
|
+
logger.note(`${project.name} (${version})`, parts.map((part) => `./${part.relative_path} ${part.missing ? "⚠️ missing" : ""}`).join("\n"));
|
|
48
48
|
logger.info("");
|
|
49
49
|
}
|
|
50
50
|
return {
|
|
@@ -57,15 +57,15 @@ const manual_release = create_command({
|
|
|
57
57
|
error: "Not on a branch (detached HEAD). Please checkout a branch first."
|
|
58
58
|
};
|
|
59
59
|
log.info(`Current branch: ${current_branch}`);
|
|
60
|
-
const
|
|
61
|
-
let
|
|
62
|
-
if (
|
|
63
|
-
|
|
64
|
-
log.success(`Defaulting to
|
|
60
|
+
const project_names = config.managed_projects.map((project$1) => project$1.name);
|
|
61
|
+
let project_name;
|
|
62
|
+
if (project_names.length === 1) {
|
|
63
|
+
project_name = project_names[0];
|
|
64
|
+
log.success(`Defaulting to project: ${project_name}`);
|
|
65
65
|
} else {
|
|
66
66
|
const selected = await select({
|
|
67
|
-
message: "Select
|
|
68
|
-
options:
|
|
67
|
+
message: "Select a project to release",
|
|
68
|
+
options: project_names.map((name) => ({
|
|
69
69
|
value: name,
|
|
70
70
|
label: name
|
|
71
71
|
}))
|
|
@@ -74,28 +74,47 @@ const manual_release = create_command({
|
|
|
74
74
|
cancel("Manual release cancelled");
|
|
75
75
|
return { status: "success" };
|
|
76
76
|
}
|
|
77
|
-
|
|
77
|
+
project_name = selected;
|
|
78
78
|
}
|
|
79
|
-
const
|
|
80
|
-
if (!
|
|
79
|
+
const project = config.managed_projects.find((item) => item.name === project_name);
|
|
80
|
+
if (!project) return {
|
|
81
81
|
status: "error",
|
|
82
|
-
error: `
|
|
82
|
+
error: `Project "${project_name}" not found in config`
|
|
83
83
|
};
|
|
84
|
-
const changes = await find_change_files(join(config.changes_dir,
|
|
84
|
+
const changes = await find_change_files(join(config.changes_dir, project_name), { allowed_kinds: project.versioning.allowed_changes });
|
|
85
85
|
if (changes.warnings.length > 0) for (const warning of changes.warnings) log.warn(warning);
|
|
86
86
|
log.success(`Found ${changes.list.length} change file(s)`);
|
|
87
|
-
const current_version = await compute_current_version(
|
|
87
|
+
const current_version = await compute_current_version(project, { get_file_content: (file_path) => read_file(file_path) }) ?? project.versioning.initial_version;
|
|
88
88
|
log.info(`Current version: ${current_version}`);
|
|
89
|
-
const
|
|
89
|
+
const next_version_candidate = project.versioning.bump({
|
|
90
90
|
version: current_version,
|
|
91
91
|
changes: changes.list,
|
|
92
92
|
date: /* @__PURE__ */ new Date()
|
|
93
93
|
});
|
|
94
|
-
log.info(`Next version will be: ${
|
|
94
|
+
log.info(`Next version will be: ${next_version_candidate}`);
|
|
95
|
+
const version_input = await text({
|
|
96
|
+
message: `Enter version [or press Enter to use ${next_version_candidate}]:`,
|
|
97
|
+
placeholder: next_version_candidate,
|
|
98
|
+
validate: (value = "") => {
|
|
99
|
+
const trimmed = value.trim();
|
|
100
|
+
if (trimmed.length === 0) return;
|
|
101
|
+
if (!project.versioning.validate({ version: trimmed })) return `Invalid version format. Expected format compatible with ${project.versioning.allowed_changes.join(", ")}`;
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
if (isCancel(version_input)) {
|
|
105
|
+
cancel("Manual release cancelled");
|
|
106
|
+
return { status: "success" };
|
|
107
|
+
}
|
|
108
|
+
const next_version = version_input.trim() || next_version_candidate;
|
|
109
|
+
log.info(`Using version: ${next_version}`);
|
|
110
|
+
const proposed_tag = config.git.tag_generator({
|
|
111
|
+
project: { name: project.name },
|
|
112
|
+
version: next_version
|
|
113
|
+
});
|
|
95
114
|
const tag_input = await text({
|
|
96
115
|
message: "Enter tag name:",
|
|
97
|
-
placeholder:
|
|
98
|
-
initialValue:
|
|
116
|
+
placeholder: proposed_tag,
|
|
117
|
+
initialValue: proposed_tag,
|
|
99
118
|
validate: (value = "") => {
|
|
100
119
|
if (value.trim().length === 0) return "Tag name is required";
|
|
101
120
|
}
|
|
@@ -126,14 +145,14 @@ const manual_release = create_command({
|
|
|
126
145
|
log.warn(`Could not check remote tag existence: ${error.message}`);
|
|
127
146
|
log.warn("Continuing with local tag check only...");
|
|
128
147
|
}
|
|
129
|
-
const changes_summary = changes.list.map((change) => `
|
|
148
|
+
const changes_summary = changes.list.map((change) => ` ${change.summary.split("\n").join("\n ")}`).join("\n");
|
|
130
149
|
note(`Manual release plan:
|
|
131
|
-
-
|
|
132
|
-
- Current version: ${current_version}
|
|
133
|
-
- Next version: ${next_version}
|
|
134
|
-
- Tag: ${tag}
|
|
135
|
-
- Change files:
|
|
136
|
-
${changes_summary}`, "Release details");
|
|
150
|
+
- Project: ${project_name}
|
|
151
|
+
- Current version: ${current_version}
|
|
152
|
+
- Next version: ${next_version}
|
|
153
|
+
- Tag: ${tag}
|
|
154
|
+
- Change files:
|
|
155
|
+
${changes_summary || " No changes in this release."}`, "Release details");
|
|
137
156
|
const proceed_confirmation = await confirm({
|
|
138
157
|
message: "Proceed with generating changelog and version bump?",
|
|
139
158
|
initialValue: false
|
|
@@ -143,11 +162,11 @@ ${changes_summary}`, "Release details");
|
|
|
143
162
|
return { status: "success" };
|
|
144
163
|
}
|
|
145
164
|
const files_to_stage = [];
|
|
146
|
-
const changes_dir = join(config.changes_dir,
|
|
165
|
+
const changes_dir = join(config.changes_dir, project_name);
|
|
147
166
|
const deleted_change_files = await delete_all_files_from_folder(changes_dir);
|
|
148
167
|
files_to_stage.push(...deleted_change_files);
|
|
149
168
|
log.success(`Deleted ${deleted_change_files.length} change file(s)`);
|
|
150
|
-
for (const component of
|
|
169
|
+
for (const component of project.components) for (const part of component.parts) {
|
|
151
170
|
const initial_content = await read_file(part.file);
|
|
152
171
|
if (initial_content === null) continue;
|
|
153
172
|
const updated_content = part.update_version(initial_content, next_version);
|
|
@@ -155,8 +174,8 @@ ${changes_summary}`, "Release details");
|
|
|
155
174
|
files_to_stage.push(part.file);
|
|
156
175
|
}
|
|
157
176
|
log.success("Updated component versions");
|
|
158
|
-
const formatter =
|
|
159
|
-
const initial_changelog_content = await read_file(
|
|
177
|
+
const formatter = project.versioning.formatter;
|
|
178
|
+
const initial_changelog_content = await read_file(project.changelog);
|
|
160
179
|
const changelog_as_mdast = parse_markdown(initial_changelog_content ?? "");
|
|
161
180
|
const changelog = formatter.transform_markdown(changelog_as_mdast, initial_changelog_content ?? "");
|
|
162
181
|
const updated_changelog_content = formatter.format_changelog({
|
|
@@ -164,15 +183,15 @@ ${changes_summary}`, "Release details");
|
|
|
164
183
|
releases: [{
|
|
165
184
|
version: next_version,
|
|
166
185
|
changes: changes.list
|
|
167
|
-
}, ...changelog.releases.filter((release) => release.version !== next_version)].sort((a, b) =>
|
|
168
|
-
}, {
|
|
169
|
-
await write_file(
|
|
170
|
-
files_to_stage.push(
|
|
186
|
+
}, ...changelog.releases.filter((release) => release.version !== next_version)].sort((a, b) => project.versioning.compare(b.version, a.version))
|
|
187
|
+
}, { project: { name: project_name } });
|
|
188
|
+
await write_file(project.changelog, updated_changelog_content);
|
|
189
|
+
files_to_stage.push(project.changelog);
|
|
171
190
|
log.success("Updated changelog");
|
|
172
191
|
const commit_message_input = await text({
|
|
173
192
|
message: "Enter commit message:",
|
|
174
|
-
placeholder: `release: ${
|
|
175
|
-
initialValue: `release: ${
|
|
193
|
+
placeholder: `release: ${project_name}@${next_version}`,
|
|
194
|
+
initialValue: `release: ${project_name}@${next_version}`,
|
|
176
195
|
validate: (value = "") => {
|
|
177
196
|
if (value.trim().length === 0) return "Commit message is required";
|
|
178
197
|
}
|
|
@@ -188,7 +207,7 @@ ${changes_summary}`, "Release details");
|
|
|
188
207
|
if (diff) note(diff, "Changes to be committed");
|
|
189
208
|
const confirm_commit = await confirm({
|
|
190
209
|
message: "Review the changes above. Proceed with commit?",
|
|
191
|
-
initialValue:
|
|
210
|
+
initialValue: false
|
|
192
211
|
});
|
|
193
212
|
if (isCancel(confirm_commit) || !confirm_commit) {
|
|
194
213
|
await reset(root);
|
|
@@ -67,9 +67,9 @@ const record_change = create_command({
|
|
|
67
67
|
name: "record-change",
|
|
68
68
|
description: "Record a new change",
|
|
69
69
|
schema: {
|
|
70
|
-
|
|
70
|
+
project: {
|
|
71
71
|
type: "string",
|
|
72
|
-
description: "
|
|
72
|
+
description: "Project name"
|
|
73
73
|
},
|
|
74
74
|
type: {
|
|
75
75
|
type: "string",
|
|
@@ -93,40 +93,40 @@ const record_change = create_command({
|
|
|
93
93
|
run: async ({ args, context }) => {
|
|
94
94
|
intro(`record a new change`);
|
|
95
95
|
const config = context.config;
|
|
96
|
-
let
|
|
97
|
-
if (!
|
|
98
|
-
const
|
|
99
|
-
if (
|
|
100
|
-
|
|
101
|
-
log.success(`Defaulting to
|
|
96
|
+
let project_name = args.project;
|
|
97
|
+
if (!project_name) {
|
|
98
|
+
const project_names = config.managed_projects.map((project$1) => project$1.name);
|
|
99
|
+
if (project_names.length === 1) {
|
|
100
|
+
project_name = project_names[0];
|
|
101
|
+
log.success(`Defaulting to project: ${project_name}`);
|
|
102
102
|
} else {
|
|
103
103
|
const selected = await select({
|
|
104
|
-
message: "Select
|
|
105
|
-
options:
|
|
104
|
+
message: "Select project:",
|
|
105
|
+
options: project_names.map((name) => ({
|
|
106
106
|
value: name,
|
|
107
107
|
label: name
|
|
108
108
|
}))
|
|
109
109
|
});
|
|
110
110
|
if (isCancel(selected)) {
|
|
111
|
-
cancel("
|
|
111
|
+
cancel("Project selection cancelled");
|
|
112
112
|
return { status: "success" };
|
|
113
113
|
}
|
|
114
|
-
|
|
114
|
+
project_name = selected;
|
|
115
115
|
}
|
|
116
116
|
}
|
|
117
|
-
const
|
|
118
|
-
if (!
|
|
117
|
+
const project = config.managed_projects.find((item) => item.name === project_name);
|
|
118
|
+
if (!project) return {
|
|
119
119
|
status: "error",
|
|
120
|
-
error: `
|
|
120
|
+
error: `Project "${project_name}" not found in config`
|
|
121
121
|
};
|
|
122
|
-
const valid_types = Array.from(
|
|
122
|
+
const valid_types = Array.from(project.versioning.allowed_changes);
|
|
123
123
|
let change_type = args.type;
|
|
124
124
|
if (!change_type) {
|
|
125
125
|
const selected = await select({
|
|
126
126
|
message: "Select change type:",
|
|
127
127
|
options: valid_types.map((t) => ({
|
|
128
128
|
value: t,
|
|
129
|
-
label:
|
|
129
|
+
label: project.versioning.display_map[t]?.singular ?? t
|
|
130
130
|
}))
|
|
131
131
|
});
|
|
132
132
|
if (isCancel(selected)) {
|
|
@@ -137,7 +137,7 @@ const record_change = create_command({
|
|
|
137
137
|
}
|
|
138
138
|
if (!valid_types.includes(change_type)) return {
|
|
139
139
|
status: "error",
|
|
140
|
-
error: `Invalid change type "${change_type}". Valid types for ${
|
|
140
|
+
error: `Invalid change type "${change_type}". Valid types for ${project_name}: ${valid_types.join(", ")}`
|
|
141
141
|
};
|
|
142
142
|
const initial_slug = humanId({
|
|
143
143
|
separator: "-",
|
|
@@ -155,7 +155,7 @@ const record_change = create_command({
|
|
|
155
155
|
summary: ""
|
|
156
156
|
});
|
|
157
157
|
try {
|
|
158
|
-
const file_path = await save_change_file(change_file, join(config.changes_dir,
|
|
158
|
+
const file_path = await save_change_file(change_file, join(config.changes_dir, project_name));
|
|
159
159
|
const editor = await get_editor(config.changes_dir);
|
|
160
160
|
if (!editor) return {
|
|
161
161
|
status: "error",
|