@afoures/auto-release 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/lib/commands/check.mjs +34 -2
- package/dist/lib/commands/generate-release-pr.mjs +114 -74
- package/dist/lib/commands/list.mjs +20 -11
- package/dist/lib/config.mjs +3 -1
- package/dist/lib/formatter.mjs +1 -2
- package/dist/lib/types.d.mts +14 -0
- package/dist/lib/utils/group.mjs +19 -0
- package/docs/commands.md +35 -6
- package/docs/configuration.md +79 -0
- package/package.json +5 -3
|
@@ -45,6 +45,27 @@ async function validate_changes_files_content(changes_dir, project) {
|
|
|
45
45
|
};
|
|
46
46
|
return { ok: true };
|
|
47
47
|
}
|
|
48
|
+
function validate_groups(projects) {
|
|
49
|
+
const errors = [];
|
|
50
|
+
const warnings = [];
|
|
51
|
+
const group_names = new Set(projects.map((p) => p.release_group));
|
|
52
|
+
for (const group_name of group_names) {
|
|
53
|
+
const project_with_same_name = projects.find((p) => p.name === group_name);
|
|
54
|
+
if (project_with_same_name && project_with_same_name.release_group !== group_name) errors.push(`Group name "${group_name}" conflicts with project name. Groups and projects must have unique names.`);
|
|
55
|
+
}
|
|
56
|
+
const group_names_array = Array.from(group_names);
|
|
57
|
+
for (let i = 0; i < group_names_array.length; i++) for (let j = i + 1; j < group_names_array.length; j++) {
|
|
58
|
+
const group_a = group_names_array[i];
|
|
59
|
+
const group_b = group_names_array[j];
|
|
60
|
+
if (group_a.toLowerCase() === group_b.toLowerCase() && group_a !== group_b) warnings.push(`Groups "${group_a}" and "${group_b}" have similar names (case insensitive match). Consider using consistent casing.`);
|
|
61
|
+
}
|
|
62
|
+
const special_char_pattern = /[^a-zA-Z0-9_-]/;
|
|
63
|
+
for (const group_name of group_names) if (special_char_pattern.test(group_name)) warnings.push(`Group name "${group_name}" contains special characters. Consider using only alphanumeric, hyphens, and underscores for compatibility.`);
|
|
64
|
+
return {
|
|
65
|
+
errors,
|
|
66
|
+
warnings
|
|
67
|
+
};
|
|
68
|
+
}
|
|
48
69
|
const check = create_command({
|
|
49
70
|
name: "check",
|
|
50
71
|
description: "Validate configuration, versions, and change files",
|
|
@@ -62,7 +83,11 @@ const check = create_command({
|
|
|
62
83
|
run: async ({ context }) => {
|
|
63
84
|
const logger = create_logger();
|
|
64
85
|
const errors = [];
|
|
86
|
+
const warnings = [];
|
|
65
87
|
const config = context.config;
|
|
88
|
+
const group_validation = validate_groups(config.managed_projects);
|
|
89
|
+
errors.push(...group_validation.errors);
|
|
90
|
+
warnings.push(...group_validation.warnings);
|
|
66
91
|
for (const project of config.managed_projects) {
|
|
67
92
|
const component_validation = await verify_component_version_consistency(project);
|
|
68
93
|
if (!component_validation.ok) errors.push(...component_validation.errors);
|
|
@@ -70,10 +95,17 @@ const check = create_command({
|
|
|
70
95
|
if (!changes_validation.ok) errors.push(...changes_validation.errors);
|
|
71
96
|
}
|
|
72
97
|
const valid = errors.length === 0;
|
|
73
|
-
if (valid) logger.success("All validations passed!");
|
|
74
|
-
else {
|
|
98
|
+
if (valid && warnings.length === 0) logger.success("All validations passed!");
|
|
99
|
+
else if (valid) {
|
|
100
|
+
logger.warn("Validations passed with warnings:");
|
|
101
|
+
warnings.forEach((warning) => logger.warn(` ${warning}`));
|
|
102
|
+
} else {
|
|
75
103
|
logger.error("Detected errors:");
|
|
76
104
|
errors.forEach((err) => logger.error(` ${err}`));
|
|
105
|
+
if (warnings.length > 0) {
|
|
106
|
+
logger.warn("Warnings:");
|
|
107
|
+
warnings.forEach((warning) => logger.warn(` ${warning}`));
|
|
108
|
+
}
|
|
77
109
|
}
|
|
78
110
|
if (valid) return {
|
|
79
111
|
status: "success",
|
|
@@ -6,6 +6,7 @@ import { find_change_files } from "../change-file.mjs";
|
|
|
6
6
|
import { diff, reset } from "../utils/git.mjs";
|
|
7
7
|
import { compute_current_version } from "../utils/version.mjs";
|
|
8
8
|
import { parse_markdown } from "../utils/mdast.mjs";
|
|
9
|
+
import { group_projects } from "../utils/group.mjs";
|
|
9
10
|
import { join } from "node:path";
|
|
10
11
|
|
|
11
12
|
//#region src/lib/commands/generate-release-pr.ts
|
|
@@ -17,11 +18,6 @@ const generate_release_pr = create_command({
|
|
|
17
18
|
type: "string",
|
|
18
19
|
description: "Path to config file"
|
|
19
20
|
},
|
|
20
|
-
filter: {
|
|
21
|
-
type: "string",
|
|
22
|
-
description: "Only generate release PRs for the specified projects",
|
|
23
|
-
multiple: true
|
|
24
|
-
},
|
|
25
21
|
"dry-run": {
|
|
26
22
|
type: "boolean",
|
|
27
23
|
description: "Show what would be done without making changes"
|
|
@@ -37,79 +33,21 @@ const generate_release_pr = create_command({
|
|
|
37
33
|
root: git_root || config.folder
|
|
38
34
|
};
|
|
39
35
|
},
|
|
40
|
-
run: async ({ args: {
|
|
36
|
+
run: async ({ args: { "dry-run": dry_run = false }, context: { config, root } }) => {
|
|
41
37
|
const logger = create_logger();
|
|
42
|
-
const
|
|
43
|
-
if (
|
|
38
|
+
const project_groups = group_projects(config.managed_projects);
|
|
39
|
+
if (project_groups.length === 0) return {
|
|
44
40
|
status: "success",
|
|
45
41
|
message: "No projects to release"
|
|
46
42
|
};
|
|
47
|
-
for (const
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
date: /* @__PURE__ */ new Date()
|
|
56
|
-
});
|
|
57
|
-
const changes_by_kind = /* @__PURE__ */ new Map();
|
|
58
|
-
for (const change of changes.list) {
|
|
59
|
-
const existing = changes_by_kind.get(change.kind) ?? [];
|
|
60
|
-
existing.push(change);
|
|
61
|
-
changes_by_kind.set(change.kind, existing);
|
|
62
|
-
}
|
|
63
|
-
const message_lines = [];
|
|
64
|
-
const display_map = project.versioning.display_map;
|
|
65
|
-
for (const [kind, kind_changes] of changes_by_kind.entries()) {
|
|
66
|
-
const label = display_map[kind]?.plural ?? display_map[kind]?.singular ?? kind;
|
|
67
|
-
message_lines.push(`\n${label}:`);
|
|
68
|
-
for (const change of kind_changes) message_lines.push(` ${change.summary}`);
|
|
69
|
-
}
|
|
70
|
-
logger.note(`Release ${project.name} ${next_version}`, message_lines.join("\n"));
|
|
71
|
-
if (dry_run) continue;
|
|
72
|
-
await delete_all_files_from_folder(changes_dir);
|
|
73
|
-
for (const component of project.components) for (const part of component.parts) {
|
|
74
|
-
const initial_content = await read_file(part.file);
|
|
75
|
-
if (initial_content === null) continue;
|
|
76
|
-
const updated_content = part.update_version(initial_content, next_version);
|
|
77
|
-
await write_file(part.file, updated_content);
|
|
78
|
-
}
|
|
79
|
-
const formatter = project.versioning.formatter;
|
|
80
|
-
const initial_changelog_content = await read_file(project.changelog);
|
|
81
|
-
const changelog_as_mdast = parse_markdown(initial_changelog_content ?? "");
|
|
82
|
-
const changelog = formatter.transform_markdown(changelog_as_mdast, initial_changelog_content ?? "");
|
|
83
|
-
const updated_changelog_content = formatter.format_changelog({
|
|
84
|
-
...changelog,
|
|
85
|
-
releases: [{
|
|
86
|
-
version: next_version,
|
|
87
|
-
changes: changes.list
|
|
88
|
-
}, ...changelog.releases.filter((release) => release.version !== next_version)].sort((a, b) => project.versioning.compare(b.version, a.version))
|
|
89
|
-
}, { project: { name: project.name } });
|
|
90
|
-
await write_file(project.changelog, updated_changelog_content);
|
|
91
|
-
const file_operations = await diff(root);
|
|
92
|
-
await reset(root);
|
|
93
|
-
const platform = config.git.platform;
|
|
94
|
-
const release_branch_name = `${config.git.default_release_branch_prefix}/${project.name}`;
|
|
95
|
-
await platform.create_or_update_branch({
|
|
96
|
-
branch_name: release_branch_name,
|
|
97
|
-
base_branch_name: config.git.target_branch,
|
|
98
|
-
file_operations,
|
|
99
|
-
commit_message: `chore: prepare release ${project.name}@${next_version}`
|
|
100
|
-
});
|
|
101
|
-
await platform.create_or_update_pull_request({
|
|
102
|
-
head_branch_name: release_branch_name,
|
|
103
|
-
base_branch_name: config.git.target_branch,
|
|
104
|
-
title: `release: ${project.name}@${next_version}`,
|
|
105
|
-
body: formatter.generate_pr_body({
|
|
106
|
-
project: { name: project.name },
|
|
107
|
-
current_version,
|
|
108
|
-
next_version,
|
|
109
|
-
changes: changes.list
|
|
110
|
-
}),
|
|
111
|
-
draft: true
|
|
112
|
-
});
|
|
43
|
+
for (const group of project_groups) if (await release_group(group, {
|
|
44
|
+
config,
|
|
45
|
+
root,
|
|
46
|
+
dry_run,
|
|
47
|
+
logger
|
|
48
|
+
}) === "skipped") {
|
|
49
|
+
logger.info(`Skipping group "${group.name}" - no projects with changes`);
|
|
50
|
+
continue;
|
|
113
51
|
}
|
|
114
52
|
if (dry_run) return {
|
|
115
53
|
status: "success",
|
|
@@ -121,6 +59,108 @@ const generate_release_pr = create_command({
|
|
|
121
59
|
};
|
|
122
60
|
}
|
|
123
61
|
});
|
|
62
|
+
async function release_group(group, { config, root, dry_run, logger }) {
|
|
63
|
+
const project_releases = [];
|
|
64
|
+
for (const project of group.projects) {
|
|
65
|
+
const changes_dir = join(config.changes_dir, project.name);
|
|
66
|
+
const changes_result = await find_change_files(changes_dir, { allowed_kinds: project.versioning.allowed_changes });
|
|
67
|
+
if (changes_result.warnings.length > 0) for (const warning of changes_result.warnings) logger.warn(warning);
|
|
68
|
+
if (project.options.skip_release_if_no_change_file && changes_result.list.length === 0) continue;
|
|
69
|
+
const current_version = await compute_current_version(project, { get_file_content: (file_path) => read_file(file_path) }) ?? project.versioning.initial_version;
|
|
70
|
+
const next_version = project.versioning.bump({
|
|
71
|
+
version: current_version,
|
|
72
|
+
changes: changes_result.list,
|
|
73
|
+
date: /* @__PURE__ */ new Date()
|
|
74
|
+
});
|
|
75
|
+
const changes_by_kind = /* @__PURE__ */ new Map();
|
|
76
|
+
for (const change of changes_result.list) {
|
|
77
|
+
const existing = changes_by_kind.get(change.kind) ?? [];
|
|
78
|
+
existing.push(change);
|
|
79
|
+
changes_by_kind.set(change.kind, existing);
|
|
80
|
+
}
|
|
81
|
+
const message_lines = [];
|
|
82
|
+
const display_map = project.versioning.display_map;
|
|
83
|
+
for (const [kind, kind_changes] of changes_by_kind.entries()) {
|
|
84
|
+
const label = display_map[kind]?.plural ?? display_map[kind]?.singular ?? kind;
|
|
85
|
+
message_lines.push(`\n${label}:`);
|
|
86
|
+
for (const change of kind_changes) message_lines.push(` ${change.summary}`);
|
|
87
|
+
}
|
|
88
|
+
logger.note(`Release ${project.name} ${next_version}`, message_lines.join("\n"));
|
|
89
|
+
if (dry_run) continue;
|
|
90
|
+
const file_operations = await collect_project_file_operations(project, {
|
|
91
|
+
changes_dir,
|
|
92
|
+
changes: changes_result.list,
|
|
93
|
+
current_version,
|
|
94
|
+
next_version,
|
|
95
|
+
root
|
|
96
|
+
});
|
|
97
|
+
project_releases.push({
|
|
98
|
+
project,
|
|
99
|
+
current_version,
|
|
100
|
+
next_version,
|
|
101
|
+
changes: changes_result.list,
|
|
102
|
+
file_operations
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
if (project_releases.length === 0) return "skipped";
|
|
106
|
+
if (dry_run) return "processed";
|
|
107
|
+
const all_file_operations = project_releases.flatMap((release) => release.file_operations);
|
|
108
|
+
const platform = config.git.platform;
|
|
109
|
+
const release_branch_name = `${config.git.default_release_branch_prefix}/${group.name}`;
|
|
110
|
+
await platform.create_or_update_branch({
|
|
111
|
+
branch_name: release_branch_name,
|
|
112
|
+
base_branch_name: config.git.target_branch,
|
|
113
|
+
file_operations: all_file_operations,
|
|
114
|
+
commit_message: `chore: prepare releases for ${group.name}`
|
|
115
|
+
});
|
|
116
|
+
const pr_body = generate_pr_body(project_releases);
|
|
117
|
+
const pr_title = generate_pr_title(project_releases);
|
|
118
|
+
await platform.create_or_update_pull_request({
|
|
119
|
+
head_branch_name: release_branch_name,
|
|
120
|
+
base_branch_name: config.git.target_branch,
|
|
121
|
+
title: pr_title,
|
|
122
|
+
body: pr_body,
|
|
123
|
+
draft: true
|
|
124
|
+
});
|
|
125
|
+
return "processed";
|
|
126
|
+
}
|
|
127
|
+
async function collect_project_file_operations(project, { changes_dir, changes, current_version: _current_version, next_version, root }) {
|
|
128
|
+
await delete_all_files_from_folder(changes_dir);
|
|
129
|
+
for (const component of project.components) for (const part of component.parts) {
|
|
130
|
+
const initial_content = await read_file(part.file);
|
|
131
|
+
if (initial_content === null) continue;
|
|
132
|
+
const updated_content = part.update_version(initial_content, next_version);
|
|
133
|
+
await write_file(part.file, updated_content);
|
|
134
|
+
}
|
|
135
|
+
const formatter = project.versioning.formatter;
|
|
136
|
+
const initial_changelog_content = await read_file(project.changelog);
|
|
137
|
+
const changelog_as_mdast = parse_markdown(initial_changelog_content ?? "");
|
|
138
|
+
const changelog = formatter.transform_markdown(changelog_as_mdast, initial_changelog_content ?? "");
|
|
139
|
+
const updated_changelog_content = formatter.format_changelog({
|
|
140
|
+
...changelog,
|
|
141
|
+
releases: [{
|
|
142
|
+
version: next_version,
|
|
143
|
+
changes
|
|
144
|
+
}, ...changelog.releases.filter((release) => release.version !== next_version)].sort((a, b) => project.versioning.compare(b.version, a.version))
|
|
145
|
+
}, { project: { name: project.name } });
|
|
146
|
+
await write_file(project.changelog, updated_changelog_content);
|
|
147
|
+
const file_operations = await diff(root);
|
|
148
|
+
await reset(root);
|
|
149
|
+
return file_operations;
|
|
150
|
+
}
|
|
151
|
+
function generate_pr_body(project_releases) {
|
|
152
|
+
return ["This PR is managed by [auto-release](https://github.com/afoures/auto-release). Do not edit it manually.", ...project_releases.map((release) => {
|
|
153
|
+
return release.project.versioning.formatter.generate_pr_body({
|
|
154
|
+
project: { name: release.project.name },
|
|
155
|
+
current_version: release.current_version,
|
|
156
|
+
next_version: release.next_version,
|
|
157
|
+
changes: release.changes
|
|
158
|
+
});
|
|
159
|
+
})].join("\n\n");
|
|
160
|
+
}
|
|
161
|
+
function generate_pr_title(project_releases) {
|
|
162
|
+
return `release: ${project_releases.map((release) => `${release.project.name}@${release.next_version}`).join(", ")}`;
|
|
163
|
+
}
|
|
124
164
|
|
|
125
165
|
//#endregion
|
|
126
166
|
export { generate_release_pr };
|
|
@@ -3,6 +3,7 @@ import { create_logger } from "../utils/logger.mjs";
|
|
|
3
3
|
import { read_file } from "../utils/fs.mjs";
|
|
4
4
|
import { find_nearest_config } from "../config.mjs";
|
|
5
5
|
import { compute_current_version } from "../utils/version.mjs";
|
|
6
|
+
import { group_projects, is_multi_project_group } from "../utils/group.mjs";
|
|
6
7
|
import { relative } from "node:path";
|
|
7
8
|
|
|
8
9
|
//#region src/lib/commands/list.ts
|
|
@@ -30,21 +31,29 @@ const list = create_command({
|
|
|
30
31
|
logger.info("no projects registered.");
|
|
31
32
|
return { status: "success" };
|
|
32
33
|
}
|
|
34
|
+
const project_groups = group_projects(config.managed_projects);
|
|
33
35
|
const count = config.managed_projects.length;
|
|
36
|
+
const group_count = project_groups.length;
|
|
34
37
|
const warnings = [];
|
|
35
|
-
logger.info(`found ${count} project${count > 1 ? "s" : ""}:`);
|
|
38
|
+
logger.info(`found ${count} project${count > 1 ? "s" : ""} in ${group_count} group${group_count > 1 ? "s" : ""}:`);
|
|
36
39
|
logger.info("");
|
|
37
|
-
for (const
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
40
|
+
for (const group of project_groups) {
|
|
41
|
+
const is_multi = is_multi_project_group(group);
|
|
42
|
+
logger.info(`Group: ${group.name} (${group.projects.length} project${group.projects.length > 1 ? "s" : ""})`);
|
|
43
|
+
for (const project of group.projects) {
|
|
44
|
+
const version = await compute_current_version(project, { get_file_content: (file_path) => read_file(file_path) });
|
|
45
|
+
if (version === null) {
|
|
46
|
+
warnings.push(`${project.name} has no version`);
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
const parts = project.components.flatMap((component) => component.parts.map((part) => ({
|
|
50
|
+
relative_path: relative(root, part.file),
|
|
51
|
+
missing: part.exists === false
|
|
52
|
+
})));
|
|
53
|
+
const indent = is_multi ? " " : "";
|
|
54
|
+
logger.info(`${indent}${project.name} (${version})`);
|
|
55
|
+
for (const part of parts) logger.info(`${indent} ./${part.relative_path}${part.missing ? " ⚠️ missing" : ""}`);
|
|
42
56
|
}
|
|
43
|
-
const parts = project.components.flatMap((component) => component.parts.map((part) => ({
|
|
44
|
-
relative_path: relative(root, part.file),
|
|
45
|
-
missing: part.exists === false
|
|
46
|
-
})));
|
|
47
|
-
logger.note(`${project.name} (${version})`, parts.map((part) => `./${part.relative_path} ${part.missing ? "⚠️ missing" : ""}`).join("\n"));
|
|
48
57
|
logger.info("");
|
|
49
58
|
}
|
|
50
59
|
return {
|
package/dist/lib/config.mjs
CHANGED
|
@@ -43,7 +43,9 @@ var InternalConfig = class {
|
|
|
43
43
|
return {
|
|
44
44
|
name,
|
|
45
45
|
...definition,
|
|
46
|
-
components
|
|
46
|
+
components,
|
|
47
|
+
release_group: definition.release_group || this.#config.default_project_config?.release_group || name,
|
|
48
|
+
options: { skip_release_if_no_change_file: definition.options?.skip_release_if_no_change_file || this.#config.default_project_config?.options?.skip_release_if_no_change_file || false }
|
|
47
49
|
};
|
|
48
50
|
});
|
|
49
51
|
}
|
package/dist/lib/formatter.mjs
CHANGED
|
@@ -114,8 +114,7 @@ function default_formatter({ allowed_changes, display_map }) {
|
|
|
114
114
|
},
|
|
115
115
|
generate_pr_body({ project, current_version, next_version, changes }) {
|
|
116
116
|
const lines = [];
|
|
117
|
-
lines.push(
|
|
118
|
-
lines.push(`## Automated release for \`${project.name}\``);
|
|
117
|
+
lines.push(`# Automated release for \`${project.name}\``);
|
|
119
118
|
lines.push(`Version: \`${current_version}\` → \`${next_version}\``);
|
|
120
119
|
lines.push("");
|
|
121
120
|
lines.push("## Changelog");
|
package/dist/lib/types.d.mts
CHANGED
|
@@ -20,6 +20,12 @@ interface AutoReleaseConfig {
|
|
|
20
20
|
version: string;
|
|
21
21
|
}) => string;
|
|
22
22
|
};
|
|
23
|
+
default_project_config?: {
|
|
24
|
+
release_group?: string;
|
|
25
|
+
options?: {
|
|
26
|
+
skip_release_if_no_change_file?: boolean;
|
|
27
|
+
};
|
|
28
|
+
};
|
|
23
29
|
projects: Record<string, ProjectDefinition>;
|
|
24
30
|
}
|
|
25
31
|
/**
|
|
@@ -29,12 +35,20 @@ interface ProjectDefinition {
|
|
|
29
35
|
components: Array<Component>;
|
|
30
36
|
versioning: VersionManager;
|
|
31
37
|
changelog: string;
|
|
38
|
+
release_group?: string;
|
|
39
|
+
options?: {
|
|
40
|
+
skip_release_if_no_change_file?: boolean;
|
|
41
|
+
};
|
|
32
42
|
}
|
|
33
43
|
type ManagedProject = {
|
|
34
44
|
name: string;
|
|
35
45
|
components: Array<ResolvedComponent>;
|
|
36
46
|
versioning: VersionManager;
|
|
37
47
|
changelog: string;
|
|
48
|
+
release_group: string;
|
|
49
|
+
options: {
|
|
50
|
+
skip_release_if_no_change_file: boolean;
|
|
51
|
+
};
|
|
38
52
|
};
|
|
39
53
|
//#endregion
|
|
40
54
|
export { AutoReleaseConfig, ManagedProject };
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
//#region src/lib/utils/group.ts
|
|
2
|
+
function group_projects(projects) {
|
|
3
|
+
const groups = /* @__PURE__ */ new Map();
|
|
4
|
+
for (const project of projects) {
|
|
5
|
+
const existing = groups.get(project.release_group) ?? [];
|
|
6
|
+
existing.push(project);
|
|
7
|
+
groups.set(project.release_group, existing);
|
|
8
|
+
}
|
|
9
|
+
return [...groups.entries()].sort(([a], [b]) => a.localeCompare(b)).map(([name, group_projects$1]) => ({
|
|
10
|
+
name,
|
|
11
|
+
projects: group_projects$1.sort((a, b) => a.name.localeCompare(b.name))
|
|
12
|
+
}));
|
|
13
|
+
}
|
|
14
|
+
function is_multi_project_group(group) {
|
|
15
|
+
return group.projects.length > 1;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
//#endregion
|
|
19
|
+
export { group_projects, is_multi_project_group };
|
package/docs/commands.md
CHANGED
|
@@ -12,12 +12,20 @@ Interactively configures projects, versioning strategies, and git platform.
|
|
|
12
12
|
|
|
13
13
|
## `check`
|
|
14
14
|
|
|
15
|
-
Validate configuration and
|
|
15
|
+
Validate configuration, change files, and release groups:
|
|
16
16
|
|
|
17
17
|
```bash
|
|
18
18
|
auto-release check
|
|
19
19
|
```
|
|
20
20
|
|
|
21
|
+
**Validations:**
|
|
22
|
+
|
|
23
|
+
- Component version consistency
|
|
24
|
+
- Change file content
|
|
25
|
+
- Group name conflicts (group names cannot match project names)
|
|
26
|
+
- Similar group names (case-insensitive)
|
|
27
|
+
- Group name special characters
|
|
28
|
+
|
|
21
29
|
Use in CI to ensure everything is valid before merging.
|
|
22
30
|
|
|
23
31
|
## `record-change`
|
|
@@ -34,15 +42,31 @@ auto-release record-change --project my-app --type minor
|
|
|
34
42
|
|
|
35
43
|
## `list`
|
|
36
44
|
|
|
37
|
-
List all projects managed by `auto-release` with their current versions
|
|
45
|
+
List all projects managed by `auto-release` with their current versions, grouped by `release_group`:
|
|
38
46
|
|
|
39
47
|
```bash
|
|
40
48
|
auto-release list
|
|
41
49
|
```
|
|
42
50
|
|
|
51
|
+
**Output example:**
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
found 5 projects in 3 groups:
|
|
55
|
+
|
|
56
|
+
Group: frontend (2 projects)
|
|
57
|
+
web-app (1.2.3)
|
|
58
|
+
./apps/web/package.json
|
|
59
|
+
mobile-app (2.0.1)
|
|
60
|
+
./apps/mobile/package.json
|
|
61
|
+
|
|
62
|
+
Group: api-service (1 project)
|
|
63
|
+
api-service (0.5.0)
|
|
64
|
+
./services/api/Cargo.toml
|
|
65
|
+
```
|
|
66
|
+
|
|
43
67
|
## `generate-release-pr`
|
|
44
68
|
|
|
45
|
-
Create or update release PRs:
|
|
69
|
+
Create or update release PRs based on change files:
|
|
46
70
|
|
|
47
71
|
```bash
|
|
48
72
|
# Preview changes
|
|
@@ -50,11 +74,16 @@ auto-release generate-release-pr --dry-run
|
|
|
50
74
|
|
|
51
75
|
# Create/update PRs
|
|
52
76
|
auto-release generate-release-pr
|
|
53
|
-
|
|
54
|
-
# Specific projects only
|
|
55
|
-
auto-release generate-release-pr --filter my-app --filter another-app
|
|
56
77
|
```
|
|
57
78
|
|
|
79
|
+
Projects are grouped by `release_group` in the configuration. Projects in the same group are released together in a single PR.
|
|
80
|
+
|
|
81
|
+
**PR Structure:**
|
|
82
|
+
|
|
83
|
+
- **Branch**: `release/<group-name>` (e.g., `release/frontend`)
|
|
84
|
+
- **Title**: `release: project-a@1.0.0, project-b@2.0.0` (lists all projects with versions)
|
|
85
|
+
- **Body**: Contains sections for each project's changelog
|
|
86
|
+
|
|
58
87
|
## `tag-release-commit`
|
|
59
88
|
|
|
60
89
|
Create git tags and releases for version changes:
|
package/docs/configuration.md
CHANGED
|
@@ -22,6 +22,85 @@ projects: {
|
|
|
22
22
|
}
|
|
23
23
|
```
|
|
24
24
|
|
|
25
|
+
## Grouping Projects
|
|
26
|
+
|
|
27
|
+
Projects can be grouped to release together in a single pull/merge request using the `release_group` option.
|
|
28
|
+
|
|
29
|
+
### Default Behavior
|
|
30
|
+
|
|
31
|
+
By default, each project is its own group (releases individually). The group name defaults to the project name.
|
|
32
|
+
|
|
33
|
+
### Custom Groups
|
|
34
|
+
|
|
35
|
+
```typescript
|
|
36
|
+
export default define_config({
|
|
37
|
+
projects: {
|
|
38
|
+
"web-app": {
|
|
39
|
+
release_group: "frontend", // Groups with other "frontend" projects
|
|
40
|
+
components: [...],
|
|
41
|
+
versioning: semver(),
|
|
42
|
+
changelog: "./CHANGELOG.md",
|
|
43
|
+
},
|
|
44
|
+
"mobile-app": {
|
|
45
|
+
release_group: "frontend", // Same group - released together
|
|
46
|
+
components: [...],
|
|
47
|
+
versioning: semver(),
|
|
48
|
+
changelog: "./CHANGELOG.md",
|
|
49
|
+
},
|
|
50
|
+
"api": {
|
|
51
|
+
// No release_group - defaults to "api" (individual release)
|
|
52
|
+
components: [...],
|
|
53
|
+
versioning: semver(),
|
|
54
|
+
changelog: "./CHANGELOG.md",
|
|
55
|
+
},
|
|
56
|
+
},
|
|
57
|
+
});
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
**Resulting behavior:**
|
|
61
|
+
|
|
62
|
+
- Changes to `web-app` or `mobile-app` create a single PR with both projects
|
|
63
|
+
- Changes to `api` create an individual PR
|
|
64
|
+
- Branch name: `release/frontend` for grouped, `release/api` for individual
|
|
65
|
+
- PR title: `release: web-app@1.2.0, mobile-app@2.0.0` for grouped
|
|
66
|
+
|
|
67
|
+
### Default Project Config
|
|
68
|
+
|
|
69
|
+
Set default options for all projects:
|
|
70
|
+
|
|
71
|
+
```typescript
|
|
72
|
+
export default define_config({
|
|
73
|
+
default_project_config: {
|
|
74
|
+
release_group: "shared", // All projects default to this group
|
|
75
|
+
options: {
|
|
76
|
+
skip_release_if_no_change_file: true,
|
|
77
|
+
},
|
|
78
|
+
},
|
|
79
|
+
projects: { ... },
|
|
80
|
+
});
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
### Options
|
|
84
|
+
|
|
85
|
+
#### `skip_release_if_no_change_file`
|
|
86
|
+
|
|
87
|
+
When `true`, skip creating a release PR for projects without change files.
|
|
88
|
+
|
|
89
|
+
```typescript
|
|
90
|
+
projects: {
|
|
91
|
+
"my-app": {
|
|
92
|
+
components: [...],
|
|
93
|
+
versioning: semver(),
|
|
94
|
+
changelog: "CHANGELOG.md",
|
|
95
|
+
options: {
|
|
96
|
+
skip_release_if_no_change_file: true,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
}
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Useful for grouped projects where some projects may not have changes in every release cycle.
|
|
103
|
+
|
|
25
104
|
## Versioning Strategies
|
|
26
105
|
|
|
27
106
|
```typescript
|
package/package.json
CHANGED
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@afoures/auto-release",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.0",
|
|
4
4
|
"description": "A file based release management tool for monorepos",
|
|
5
5
|
"homepage": "https://github.com/afoures/auto-release#readme",
|
|
6
6
|
"bugs": {
|
|
7
7
|
"url": "https://github.com/afoures/auto-release/issues"
|
|
8
8
|
},
|
|
9
|
+
"license": "MIT",
|
|
9
10
|
"author": "Antoine Fourès <contact@afoures.com>",
|
|
10
11
|
"repository": {
|
|
11
12
|
"type": "git",
|
|
12
13
|
"url": "git+https://github.com/afoures/auto-release.git"
|
|
13
14
|
},
|
|
14
|
-
"license": "MIT",
|
|
15
15
|
"bin": {
|
|
16
16
|
"auto-release": "./dist/bin.mjs"
|
|
17
17
|
},
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"package.json",
|
|
22
22
|
"README.md"
|
|
23
23
|
],
|
|
24
|
+
"keywords": [],
|
|
24
25
|
"type": "module",
|
|
25
26
|
"main": "./dist/index.mjs",
|
|
26
27
|
"module": "./dist/index.mjs",
|
|
@@ -72,6 +73,7 @@
|
|
|
72
73
|
"test": "vitest",
|
|
73
74
|
"typecheck": "tsc --noEmit",
|
|
74
75
|
"lint": "oxlint",
|
|
75
|
-
"format": "oxfmt"
|
|
76
|
+
"format": "oxfmt",
|
|
77
|
+
"auto-release.local": "node ./dist/bin.mjs"
|
|
76
78
|
}
|
|
77
79
|
}
|