@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 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
@@ -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 'auto-release'
47
- import { semver } from 'auto-release/versioning'
48
- import { github } from 'auto-release/providers'
49
- import { node } from 'auto-release/components'
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
- 'my-app': {
54
- components: [node('packages/my-app')],
53
+ "my-app": {
54
+ components: [node("packages/my-app")],
55
55
  versioning: semver(),
56
- changelog: 'CHANGELOG.md',
56
+ changelog: "CHANGELOG.md",
57
57
  },
58
58
  },
59
59
  git: {
60
60
  platform: github({
61
61
  token: process.env.GITHUB_TOKEN!,
62
- owner: 'your-org',
63
- repo: 'your-repo',
62
+ owner: "your-org",
63
+ repo: "your-repo",
64
64
  }),
65
- target_branch: 'main',
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 };
@@ -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: "creation" });
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: `chore: prepare releases for ${group.name}`
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, join(config.changes_dir, project_name));
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",
@@ -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 `[See the changelog for ${project.name}@${version} release notes](${file})`;
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 || []) 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",
@@ -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
 
@@ -6,18 +6,18 @@ The `projects` object defines each releasable unit:
6
6
 
7
7
  ```typescript
8
8
  projects: {
9
- 'my-app': {
9
+ "my-app": {
10
10
  // Components: where versions are read/written
11
11
  components: [
12
- node('packages/my-app'),
13
- node('packages/shared'),
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: 'apps/my-app/CHANGELOG.md',
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 'auto-release/versioning'
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 'auto-release/providers'
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: 'your-org',
130
- repo: 'your-repo',
129
+ owner: "your-org",
130
+ repo: "your-repo",
131
131
  }),
132
- target_branch: 'main',
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 'auto-release/providers'
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: 'your-project-id',
145
+ project_id: "your-project-id",
146
146
  }),
147
- target_branch: 'main',
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 'auto-release/components'
187
+ import { node, expo, php } from "@afoures/auto-release/components"
188
188
 
189
189
  components: [
190
- node('packages/web'),
191
- bun('packages/bff'),
192
- expo('apps/mobile'),
193
- php('packages/api'),
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.4.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",