@afoures/auto-release 0.4.0 → 0.5.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 +11 -11
- package/dist/lib/change-file.d.mts +4 -0
- package/dist/lib/change-file.mjs +25 -6
- package/dist/lib/commands/generate-release-pr.mjs +2 -2
- package/dist/lib/commands/record-change.mjs +5 -2
- package/dist/lib/formatter.mjs +1 -1
- package/dist/lib/platforms/github.mjs +9 -2
- package/dist/lib/utils/fs.mjs +8 -1
- package/docs/change-file-anatomy.md +3 -3
- package/docs/configuration.md +17 -17
- package/package.json +2 -2
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
|
-
|
|
22
|
+
pnpm dlx @afoures/auto-release@latest init
|
|
23
23
|
# or
|
|
24
24
|
yarn dlx @afoures/auto-release@latest init
|
|
25
25
|
# or
|
|
@@ -43,26 +43,26 @@ bunx add @afoures/auto-release
|
|
|
43
43
|
Create `auto-release.config.ts`:
|
|
44
44
|
|
|
45
45
|
```typescript
|
|
46
|
-
import { define_config } from
|
|
47
|
-
import { semver } from
|
|
48
|
-
import { github } from
|
|
49
|
-
import { node } from
|
|
46
|
+
import { define_config } from "@afoures/auto-release";
|
|
47
|
+
import { semver } from "@afoures/auto-release/versioning";
|
|
48
|
+
import { github } from "@afoures/auto-release/providers";
|
|
49
|
+
import { node } from "@afoures/auto-release/components";
|
|
50
50
|
|
|
51
51
|
export default define_config({
|
|
52
52
|
projects: {
|
|
53
|
-
|
|
54
|
-
components: [node(
|
|
53
|
+
"my-app": {
|
|
54
|
+
components: [node("packages/my-app")],
|
|
55
55
|
versioning: semver(),
|
|
56
|
-
changelog:
|
|
56
|
+
changelog: "CHANGELOG.md",
|
|
57
57
|
},
|
|
58
58
|
},
|
|
59
59
|
git: {
|
|
60
60
|
platform: github({
|
|
61
61
|
token: process.env.GITHUB_TOKEN!,
|
|
62
|
-
owner:
|
|
63
|
-
repo:
|
|
62
|
+
owner: "your-org",
|
|
63
|
+
repo: "your-repo",
|
|
64
64
|
}),
|
|
65
|
-
target_branch:
|
|
65
|
+
target_branch: "main",
|
|
66
66
|
},
|
|
67
67
|
})
|
|
68
68
|
```
|
|
@@ -3,12 +3,16 @@ declare class ChangeFile<kind extends string> {
|
|
|
3
3
|
#private;
|
|
4
4
|
constructor(props: {
|
|
5
5
|
kind: kind;
|
|
6
|
+
index?: number;
|
|
6
7
|
slug?: string;
|
|
7
8
|
summary: string;
|
|
9
|
+
birthtime?: Date;
|
|
8
10
|
});
|
|
9
11
|
get kind(): kind;
|
|
12
|
+
get index(): number;
|
|
10
13
|
get summary(): string;
|
|
11
14
|
get filename(): string;
|
|
15
|
+
get birthtime(): Date;
|
|
12
16
|
}
|
|
13
17
|
//#endregion
|
|
14
18
|
export { ChangeFile };
|
package/dist/lib/change-file.mjs
CHANGED
|
@@ -1,30 +1,40 @@
|
|
|
1
|
-
import { list_files, read_file, write_file } from "./utils/fs.mjs";
|
|
1
|
+
import { get_file_stats, list_files, read_file, write_file } from "./utils/fs.mjs";
|
|
2
2
|
import { basename, join } from "node:path";
|
|
3
3
|
import { regex } from "arkregex";
|
|
4
4
|
import { humanId } from "human-id";
|
|
5
5
|
|
|
6
6
|
//#region src/lib/change-file.ts
|
|
7
|
-
const CHANGE_FILENAME_REGEX = regex("^(?<kind>[a-z0-9-]+)\\.(?<slug>[a-z0-9-]+)\\.md$");
|
|
7
|
+
const CHANGE_FILENAME_REGEX = regex("^(?<kind>[a-z0-9-]+)\\.(?:(?<index>\\d+)-)?(?<slug>[a-z0-9-]+)\\.md$");
|
|
8
8
|
var ChangeFile = class {
|
|
9
9
|
#kind;
|
|
10
|
+
#index;
|
|
10
11
|
#slug;
|
|
11
12
|
#summary;
|
|
13
|
+
#birthtime;
|
|
12
14
|
constructor(props) {
|
|
13
15
|
this.#kind = props.kind;
|
|
16
|
+
this.#index = props.index ?? 1;
|
|
14
17
|
this.#slug = props.slug ?? humanId({
|
|
15
18
|
separator: "-",
|
|
16
19
|
capitalize: false
|
|
17
20
|
});
|
|
18
21
|
this.#summary = props.summary;
|
|
22
|
+
this.#birthtime = props.birthtime ?? /* @__PURE__ */ new Date(0);
|
|
19
23
|
}
|
|
20
24
|
get kind() {
|
|
21
25
|
return this.#kind;
|
|
22
26
|
}
|
|
27
|
+
get index() {
|
|
28
|
+
return this.#index;
|
|
29
|
+
}
|
|
23
30
|
get summary() {
|
|
24
31
|
return this.#summary;
|
|
25
32
|
}
|
|
26
33
|
get filename() {
|
|
27
|
-
return `${this.#kind}.${this.#slug}.md`;
|
|
34
|
+
return `${this.#kind}.${this.#index}-${this.#slug}.md`;
|
|
35
|
+
}
|
|
36
|
+
get birthtime() {
|
|
37
|
+
return this.#birthtime;
|
|
28
38
|
}
|
|
29
39
|
};
|
|
30
40
|
async function save_change_file(change_file, to) {
|
|
@@ -36,21 +46,25 @@ async function save_change_file(change_file, to) {
|
|
|
36
46
|
async function parse_change_file(path) {
|
|
37
47
|
const filename = basename(path);
|
|
38
48
|
const match = CHANGE_FILENAME_REGEX.exec(filename);
|
|
39
|
-
if (!match) return /* @__PURE__ */ new Error(`Invalid change filename format: ${path} (expected: <kind>.<slug>.md)`);
|
|
49
|
+
if (!match) return /* @__PURE__ */ new Error(`Invalid change filename format: ${path} (expected: <kind>.<index>-<slug>.md)`);
|
|
40
50
|
let file_content = await read_file(path);
|
|
41
51
|
file_content = file_content?.trim() ?? null;
|
|
42
52
|
if (file_content === null) return /* @__PURE__ */ new Error(`Change file is missing: ${path}`);
|
|
43
53
|
if (!file_content) return /* @__PURE__ */ new Error("Change file is empty");
|
|
44
54
|
const [title, ...rest] = file_content.split("\n");
|
|
45
55
|
const text = [`- ${title}`, ...rest.map((line) => ` ${line}`)].join("\n");
|
|
56
|
+
const index = match.groups.index ? parseInt(match.groups.index, 10) : 1;
|
|
57
|
+
const birthtime = (await get_file_stats(path))?.birthtime ?? /* @__PURE__ */ new Date(0);
|
|
46
58
|
return new ChangeFile({
|
|
59
|
+
index,
|
|
47
60
|
slug: match.groups.slug,
|
|
48
61
|
kind: match.groups.kind,
|
|
49
|
-
summary: text
|
|
62
|
+
summary: text,
|
|
63
|
+
birthtime
|
|
50
64
|
});
|
|
51
65
|
}
|
|
52
66
|
async function find_change_files(folder, { allowed_kinds }) {
|
|
53
|
-
const files = await list_files(folder, { sort: "
|
|
67
|
+
const files = await list_files(folder, { sort: "name" });
|
|
54
68
|
const list = [];
|
|
55
69
|
const warnings = [];
|
|
56
70
|
for (const filename of files) {
|
|
@@ -67,6 +81,11 @@ async function find_change_files(folder, { allowed_kinds }) {
|
|
|
67
81
|
}
|
|
68
82
|
list.push(change_file_or_error);
|
|
69
83
|
}
|
|
84
|
+
list.sort((a, b) => {
|
|
85
|
+
if (a.kind !== b.kind) return a.kind.localeCompare(b.kind);
|
|
86
|
+
if (a.index !== b.index) return a.index - b.index;
|
|
87
|
+
return a.birthtime.getTime() - b.birthtime.getTime();
|
|
88
|
+
});
|
|
70
89
|
return {
|
|
71
90
|
list,
|
|
72
91
|
warnings
|
|
@@ -105,16 +105,16 @@ async function release_group(group, { config, root, dry_run, logger }) {
|
|
|
105
105
|
if (project_releases.length === 0) return "skipped";
|
|
106
106
|
if (dry_run) return "processed";
|
|
107
107
|
const all_file_operations = project_releases.flatMap((release) => release.file_operations);
|
|
108
|
+
const pr_title = generate_pr_title(project_releases);
|
|
108
109
|
const platform = config.git.platform;
|
|
109
110
|
const release_branch_name = `${config.git.default_release_branch_prefix}/${group.name}`;
|
|
110
111
|
await platform.create_or_update_branch({
|
|
111
112
|
branch_name: release_branch_name,
|
|
112
113
|
base_branch_name: config.git.target_branch,
|
|
113
114
|
file_operations: all_file_operations,
|
|
114
|
-
commit_message:
|
|
115
|
+
commit_message: pr_title
|
|
115
116
|
});
|
|
116
117
|
const pr_body = generate_pr_body(project_releases);
|
|
117
|
-
const pr_title = generate_pr_title(project_releases);
|
|
118
118
|
await platform.create_or_update_pull_request({
|
|
119
119
|
head_branch_name: release_branch_name,
|
|
120
120
|
base_branch_name: config.git.target_branch,
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { create_command } from "../cli.mjs";
|
|
2
2
|
import { exists, read_file } from "../utils/fs.mjs";
|
|
3
3
|
import { find_nearest_config } from "../config.mjs";
|
|
4
|
-
import { ChangeFile, save_change_file } from "../change-file.mjs";
|
|
4
|
+
import { ChangeFile, find_change_files, save_change_file } from "../change-file.mjs";
|
|
5
5
|
import { exec as exec$1 } from "../utils/exec.mjs";
|
|
6
6
|
import { cancel, intro, isCancel, log, select, text } from "@clack/prompts";
|
|
7
7
|
import { join, relative } from "node:path";
|
|
@@ -139,6 +139,8 @@ const record_change = create_command({
|
|
|
139
139
|
status: "error",
|
|
140
140
|
error: `Invalid change type "${change_type}". Valid types for ${project_name}: ${valid_types.join(", ")}`
|
|
141
141
|
};
|
|
142
|
+
const project_change_dir = join(config.changes_dir, project_name);
|
|
143
|
+
const next_index = (await find_change_files(project_change_dir, { allowed_kinds: valid_types })).list.filter((f) => f.kind === change_type).length + 1;
|
|
142
144
|
const initial_slug = humanId({
|
|
143
145
|
separator: "-",
|
|
144
146
|
capitalize: false
|
|
@@ -151,11 +153,12 @@ const record_change = create_command({
|
|
|
151
153
|
const slug = generate_slug(description_input) || initial_slug;
|
|
152
154
|
const change_file = new ChangeFile({
|
|
153
155
|
kind: change_type,
|
|
156
|
+
index: next_index,
|
|
154
157
|
slug,
|
|
155
158
|
summary: ""
|
|
156
159
|
});
|
|
157
160
|
try {
|
|
158
|
-
const file_path = await save_change_file(change_file,
|
|
161
|
+
const file_path = await save_change_file(change_file, project_change_dir);
|
|
159
162
|
const editor = await get_editor(config.changes_dir);
|
|
160
163
|
if (!editor) return {
|
|
161
164
|
status: "error",
|
package/dist/lib/formatter.mjs
CHANGED
|
@@ -110,7 +110,7 @@ function default_formatter({ allowed_changes, display_map }) {
|
|
|
110
110
|
generate_release_notes({ project, version }) {
|
|
111
111
|
const hash = version.replaceAll(".", "");
|
|
112
112
|
const file = `${project.changelog}#${hash}`;
|
|
113
|
-
return `
|
|
113
|
+
return `See the changelog for release notes: [${project.name}@${version}](${file})`;
|
|
114
114
|
},
|
|
115
115
|
generate_pr_body({ project, current_version, next_version, changes }) {
|
|
116
116
|
const lines = [];
|
|
@@ -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 || [])
|
|
41
|
-
if (
|
|
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",
|
package/dist/lib/utils/fs.mjs
CHANGED
|
@@ -69,6 +69,13 @@ async function list_files(dir, options) {
|
|
|
69
69
|
return [];
|
|
70
70
|
}
|
|
71
71
|
}
|
|
72
|
+
async function get_file_stats(path) {
|
|
73
|
+
try {
|
|
74
|
+
return await stat(path);
|
|
75
|
+
} catch {
|
|
76
|
+
return null;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
72
79
|
|
|
73
80
|
//#endregion
|
|
74
|
-
export { delete_all_files_from_folder, exists, list_files, read_file, write_file };
|
|
81
|
+
export { delete_all_files_from_folder, exists, get_file_stats, list_files, read_file, write_file };
|
|
@@ -3,13 +3,13 @@
|
|
|
3
3
|
Change files are stored in `.changes/<project-name>/` with format:
|
|
4
4
|
|
|
5
5
|
```
|
|
6
|
-
<type>.<slug>.md
|
|
6
|
+
<type>.<index>-<slug>.md
|
|
7
7
|
```
|
|
8
8
|
|
|
9
9
|
Examples:
|
|
10
10
|
|
|
11
|
-
- `.changes/my-app/major.add-authentication.md`
|
|
12
|
-
- `.changes/my-app/patch.fix-login-bug.md`
|
|
11
|
+
- `.changes/my-app/major.1-add-authentication.md`
|
|
12
|
+
- `.changes/my-app/patch.1-fix-login-bug.md`
|
|
13
13
|
|
|
14
14
|
The change files folder can be customized.
|
|
15
15
|
|
package/docs/configuration.md
CHANGED
|
@@ -6,18 +6,18 @@ The `projects` object defines each releasable unit:
|
|
|
6
6
|
|
|
7
7
|
```typescript
|
|
8
8
|
projects: {
|
|
9
|
-
|
|
9
|
+
"my-app": {
|
|
10
10
|
// Components: where versions are read/written
|
|
11
11
|
components: [
|
|
12
|
-
node(
|
|
13
|
-
node(
|
|
12
|
+
node("packages/my-app"),
|
|
13
|
+
node("packages/shared"),
|
|
14
14
|
],
|
|
15
15
|
|
|
16
16
|
// Versioning strategy
|
|
17
17
|
versioning: semver(),
|
|
18
18
|
|
|
19
19
|
// Changelog file path
|
|
20
|
-
changelog:
|
|
20
|
+
changelog: "apps/my-app/CHANGELOG.md",
|
|
21
21
|
},
|
|
22
22
|
}
|
|
23
23
|
```
|
|
@@ -104,7 +104,7 @@ Useful for grouped projects where some projects may not have changes in every re
|
|
|
104
104
|
## Versioning Strategies
|
|
105
105
|
|
|
106
106
|
```typescript
|
|
107
|
-
import { semver, calver, markver } from
|
|
107
|
+
import { semver, calver, markver } from "@afoures/auto-release/versioning"
|
|
108
108
|
|
|
109
109
|
// Semantic versioning: 1.2.3
|
|
110
110
|
versioning: semver() // Change types: major, minor, patch
|
|
@@ -121,15 +121,15 @@ versioning: markver() // Change types: marketing, feature, fix
|
|
|
121
121
|
### GitHub
|
|
122
122
|
|
|
123
123
|
```typescript
|
|
124
|
-
import { github } from
|
|
124
|
+
import { github } from "@afoures/auto-release/providers"
|
|
125
125
|
|
|
126
126
|
git: {
|
|
127
127
|
platform: github({
|
|
128
128
|
token: process.env.GITHUB_TOKEN!,
|
|
129
|
-
owner:
|
|
130
|
-
repo:
|
|
129
|
+
owner: "your-org",
|
|
130
|
+
repo: "your-repo",
|
|
131
131
|
}),
|
|
132
|
-
target_branch:
|
|
132
|
+
target_branch: "main",
|
|
133
133
|
tag_generator: ({ project, version }) => `${project.name}-${version}`,
|
|
134
134
|
}
|
|
135
135
|
```
|
|
@@ -137,14 +137,14 @@ git: {
|
|
|
137
137
|
### GitLab
|
|
138
138
|
|
|
139
139
|
```typescript
|
|
140
|
-
import { gitlab } from
|
|
140
|
+
import { gitlab } from "@afoures/auto-release/providers"
|
|
141
141
|
|
|
142
142
|
git: {
|
|
143
143
|
platform: gitlab({
|
|
144
144
|
token: process.env.GITLAB_TOKEN!,
|
|
145
|
-
project_id:
|
|
145
|
+
project_id: "your-project-id",
|
|
146
146
|
}),
|
|
147
|
-
target_branch:
|
|
147
|
+
target_branch: "main",
|
|
148
148
|
}
|
|
149
149
|
```
|
|
150
150
|
|
|
@@ -184,12 +184,12 @@ Components define version sources:
|
|
|
184
184
|
- **`php(path)`**: any PHP project with composer.json
|
|
185
185
|
|
|
186
186
|
```typescript
|
|
187
|
-
import { node, expo, php } from
|
|
187
|
+
import { node, expo, php } from "@afoures/auto-release/components"
|
|
188
188
|
|
|
189
189
|
components: [
|
|
190
|
-
node(
|
|
191
|
-
bun(
|
|
192
|
-
expo(
|
|
193
|
-
php(
|
|
190
|
+
node("packages/web"),
|
|
191
|
+
bun("packages/bff"),
|
|
192
|
+
expo("apps/mobile"),
|
|
193
|
+
php("packages/api"),
|
|
194
194
|
]
|
|
195
195
|
```
|
package/package.json
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@afoures/auto-release",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.0",
|
|
4
4
|
"description": "A file based release management tool for monorepos",
|
|
5
|
+
"keywords": [],
|
|
5
6
|
"homepage": "https://github.com/afoures/auto-release#readme",
|
|
6
7
|
"bugs": {
|
|
7
8
|
"url": "https://github.com/afoures/auto-release/issues"
|
|
@@ -21,7 +22,6 @@
|
|
|
21
22
|
"package.json",
|
|
22
23
|
"README.md"
|
|
23
24
|
],
|
|
24
|
-
"keywords": [],
|
|
25
25
|
"type": "module",
|
|
26
26
|
"main": "./dist/index.mjs",
|
|
27
27
|
"module": "./dist/index.mjs",
|