@afoures/auto-release 0.3.0 → 0.4.1

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 CHANGED
@@ -19,7 +19,7 @@ Release management should be simple. `auto-release` lets you focus on building f
19
19
  ```bash
20
20
  npx @afoures/auto-release@latest init
21
21
  # or
22
- pnpx @afoures/auto-release@latest init
22
+ pnpm dlx @afoures/auto-release@latest init
23
23
  # or
24
24
  yarn dlx @afoures/auto-release@latest init
25
25
  # or
@@ -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: { filter, "dry-run": dry_run = false }, context: { config, root } }) => {
36
+ run: async ({ args: { "dry-run": dry_run = false }, context: { config, root } }) => {
41
37
  const logger = create_logger();
42
- const filtered_projects = filter ? config.managed_projects.filter((project) => filter.includes(project.name)) : config.managed_projects;
43
- if (filtered_projects.length === 0) return {
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 project of filtered_projects) {
48
- const changes_dir = join(config.changes_dir, project.name);
49
- const changes = await find_change_files(changes_dir, { allowed_kinds: project.versioning.allowed_changes });
50
- if (changes.warnings.length > 0) for (const warning of changes.warnings) logger.warn(warning);
51
- const current_version = await compute_current_version(project, { get_file_content: (file_path) => read_file(file_path) }) ?? project.versioning.initial_version;
52
- const next_version = project.versioning.bump({
53
- version: current_version,
54
- changes: changes.list,
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 pr_title = generate_pr_title(project_releases);
109
+ const platform = config.git.platform;
110
+ const release_branch_name = `${config.git.default_release_branch_prefix}/${group.name}`;
111
+ await platform.create_or_update_branch({
112
+ branch_name: release_branch_name,
113
+ base_branch_name: config.git.target_branch,
114
+ file_operations: all_file_operations,
115
+ commit_message: pr_title
116
+ });
117
+ const pr_body = generate_pr_body(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 project of config.managed_projects) {
38
- const version = await compute_current_version(project, { get_file_content: (file_path) => read_file(file_path) });
39
- if (version === null) {
40
- warnings.push(`${project.name} has no version`);
41
- continue;
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 {
@@ -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
  }
@@ -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("This PR is managed by `[auto-release](https://github.com/afoures/auto-release)`. Do not edit it manually.");
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");
@@ -37,8 +37,15 @@ function github(options) {
37
37
  files_to_delete.add(op.previous_path);
38
38
  }
39
39
  const tree_entries = [];
40
- for (const entry of base_tree.tree || []) if (entry.type === "blob" && !files_to_delete.has(entry.path)) {
41
- if (!files_to_modify.has(entry.path)) tree_entries.push({
40
+ for (const entry of base_tree.tree || []) {
41
+ if (entry.type !== "blob") continue;
42
+ if (files_to_delete.has(entry.path)) tree_entries.push({
43
+ path: entry.path,
44
+ mode: "100644",
45
+ type: "blob",
46
+ sha: null
47
+ });
48
+ else if (!files_to_modify.has(entry.path)) tree_entries.push({
42
49
  path: entry.path,
43
50
  mode: "100644",
44
51
  type: "blob",
@@ -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 change files:
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:
@@ -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.0",
3
+ "version": "0.4.1",
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
  }