@afoures/auto-release 0.2.12 → 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/README.md +30 -269
- package/dist/lib/commands/check.mjs +44 -12
- package/dist/lib/commands/generate-release-pr.mjs +115 -75
- package/dist/lib/commands/init.mjs +40 -199
- package/dist/lib/commands/list.mjs +24 -15
- package/dist/lib/commands/manual-release.mjs +51 -32
- 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 +16 -14
- package/dist/lib/formatter.mjs +14 -14
- package/dist/lib/types.d.mts +25 -5
- package/dist/lib/utils/branch-protection.mjs +0 -3
- package/dist/lib/utils/group.mjs +19 -0
- 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 +109 -0
- package/docs/configuration.md +195 -0
- package/docs/lexicon.md +23 -0
- package/docs/recommended-usage.md +155 -0
- package/package.json +18 -15
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# auto-release
|
|
2
2
|
|
|
3
|
-
A release management tool inspired by Changesets and Release Please, designed for monorepos with
|
|
3
|
+
A release management tool inspired by Changesets and Release Please, designed for monorepos with project-centric versioning.
|
|
4
4
|
|
|
5
5
|
## Why auto-release?
|
|
6
6
|
|
|
@@ -8,7 +8,7 @@ Release management should be simple. `auto-release` lets you focus on building f
|
|
|
8
8
|
|
|
9
9
|
**Language agnostic**: Works with any project type. Built-in components for Node, Bun, Expo, and PHP projects, but you can add custom components for anything.
|
|
10
10
|
|
|
11
|
-
**Built for monorepos**: Each
|
|
11
|
+
**Built for monorepos**: Each project can have its own versioning strategy (semver, calver, marketing) and release independently.
|
|
12
12
|
|
|
13
13
|
**Developer-friendly**: Record changes with markdown files as you work. No complex conventions or strict commit messages required.
|
|
14
14
|
|
|
@@ -17,11 +17,13 @@ Release management should be simple. `auto-release` lets you focus on building f
|
|
|
17
17
|
## Quick Start
|
|
18
18
|
|
|
19
19
|
```bash
|
|
20
|
-
npx
|
|
20
|
+
npx @afoures/auto-release@latest init
|
|
21
21
|
# or
|
|
22
|
-
pnpx
|
|
22
|
+
pnpx @afoures/auto-release@latest init
|
|
23
23
|
# or
|
|
24
|
-
|
|
24
|
+
yarn dlx @afoures/auto-release@latest init
|
|
25
|
+
# or
|
|
26
|
+
bunx @afoures/auto-release@latest init
|
|
25
27
|
```
|
|
26
28
|
|
|
27
29
|
This creates `auto-release.config.ts` and sets up the `.changes` directory.
|
|
@@ -29,9 +31,13 @@ This creates `auto-release.config.ts` and sets up the `.changes` directory.
|
|
|
29
31
|
### Manual Installation
|
|
30
32
|
|
|
31
33
|
```bash
|
|
32
|
-
npm install auto-release
|
|
34
|
+
npm install @afoures/auto-release
|
|
35
|
+
# or
|
|
36
|
+
pnpm add @afoures/auto-release
|
|
37
|
+
# or
|
|
38
|
+
yarn add @afoures/auto-release
|
|
33
39
|
# or
|
|
34
|
-
|
|
40
|
+
bunx add @afoures/auto-release
|
|
35
41
|
```
|
|
36
42
|
|
|
37
43
|
Create `auto-release.config.ts`:
|
|
@@ -43,6 +49,13 @@ import { github } from 'auto-release/providers'
|
|
|
43
49
|
import { node } from 'auto-release/components'
|
|
44
50
|
|
|
45
51
|
export default define_config({
|
|
52
|
+
projects: {
|
|
53
|
+
'my-app': {
|
|
54
|
+
components: [node('packages/my-app')],
|
|
55
|
+
versioning: semver(),
|
|
56
|
+
changelog: 'CHANGELOG.md',
|
|
57
|
+
},
|
|
58
|
+
},
|
|
46
59
|
git: {
|
|
47
60
|
platform: github({
|
|
48
61
|
token: process.env.GITHUB_TOKEN!,
|
|
@@ -51,275 +64,23 @@ export default define_config({
|
|
|
51
64
|
}),
|
|
52
65
|
target_branch: 'main',
|
|
53
66
|
},
|
|
54
|
-
apps: {
|
|
55
|
-
'my-app': {
|
|
56
|
-
components: [node('packages/my-app')],
|
|
57
|
-
versioning: semver(),
|
|
58
|
-
changelog: 'CHANGELOG.md',
|
|
59
|
-
},
|
|
60
|
-
},
|
|
61
67
|
})
|
|
62
68
|
```
|
|
63
69
|
|
|
64
|
-
##
|
|
65
|
-
|
|
66
|
-
### 1. Development
|
|
67
|
-
|
|
68
|
-
Make changes and record them:
|
|
69
|
-
|
|
70
|
-
```bash
|
|
71
|
-
auto-release record-change
|
|
72
|
-
```
|
|
73
|
-
|
|
74
|
-
Commit everything including the change file:
|
|
75
|
-
|
|
76
|
-
```bash
|
|
77
|
-
git add .
|
|
78
|
-
git commit -m "feat: add new feature"
|
|
79
|
-
git push
|
|
80
|
-
```
|
|
81
|
-
|
|
82
|
-
### 2. Generate Release PR
|
|
83
|
-
|
|
84
|
-
On `main` branch, CI should automatically run:
|
|
85
|
-
|
|
86
|
-
```bash
|
|
87
|
-
auto-release generate-release-pr
|
|
88
|
-
```
|
|
89
|
-
|
|
90
|
-
This creates/updates a release branch with:
|
|
91
|
-
- Updated versions in component files
|
|
92
|
-
- Generated changelog entries
|
|
93
|
-
- Change files removed
|
|
94
|
-
|
|
95
|
-
### 3. Test on Release Branch
|
|
96
|
-
|
|
97
|
-
CI on release branch runs:
|
|
98
|
-
- Tests and quality checks
|
|
99
|
-
- Build and deploy to test/staging environment
|
|
100
|
-
|
|
101
|
-
### 4. Merge Release PR
|
|
102
|
-
|
|
103
|
-
When ready, merge the release PR to `main`.
|
|
104
|
-
|
|
105
|
-
### 5. Tag and Deploy
|
|
106
|
-
|
|
107
|
-
CI on `main` runs:
|
|
108
|
-
|
|
109
|
-
```bash
|
|
110
|
-
auto-release tag-release-commit
|
|
111
|
-
```
|
|
112
|
-
|
|
113
|
-
This creates git tags for all releases, which can trigger deployment to pre-production and production.
|
|
114
|
-
|
|
115
|
-
## Commands
|
|
116
|
-
|
|
117
|
-
### `init`
|
|
118
|
-
|
|
119
|
-
Set up auto-release in your repository:
|
|
120
|
-
|
|
121
|
-
```bash
|
|
122
|
-
auto-release init
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
Interactively configures apps, versioning strategies, and git platform.
|
|
126
|
-
|
|
127
|
-
### `check`
|
|
128
|
-
|
|
129
|
-
Validate configuration and change files:
|
|
130
|
-
|
|
131
|
-
```bash
|
|
132
|
-
auto-release check
|
|
133
|
-
```
|
|
134
|
-
|
|
135
|
-
Use in CI to ensure everything is valid before merging.
|
|
136
|
-
|
|
137
|
-
### `record-change`
|
|
138
|
-
|
|
139
|
-
Create a new change file:
|
|
140
|
-
|
|
141
|
-
```bash
|
|
142
|
-
# Interactive
|
|
143
|
-
auto-release record-change
|
|
144
|
-
|
|
145
|
-
# Non-interactive
|
|
146
|
-
auto-release record-change --app my-app --type minor
|
|
147
|
-
```
|
|
148
|
-
|
|
149
|
-
### `list`
|
|
150
|
-
|
|
151
|
-
List all apps managed by `auto-release` with their current versions:
|
|
152
|
-
|
|
153
|
-
```bash
|
|
154
|
-
auto-release list
|
|
155
|
-
```
|
|
156
|
-
|
|
157
|
-
### `generate-release-pr`
|
|
158
|
-
|
|
159
|
-
Create or update release PRs:
|
|
160
|
-
|
|
161
|
-
```bash
|
|
162
|
-
# Preview changes
|
|
163
|
-
auto-release generate-release-pr --dry-run
|
|
164
|
-
|
|
165
|
-
# Create/update PRs
|
|
166
|
-
auto-release generate-release-pr
|
|
167
|
-
|
|
168
|
-
# Specific apps only
|
|
169
|
-
auto-release generate-release-pr --app my-app --app another-app
|
|
170
|
-
```
|
|
171
|
-
|
|
172
|
-
### `tag-release-commit`
|
|
173
|
-
|
|
174
|
-
Create git tags and releases for version changes:
|
|
175
|
-
|
|
176
|
-
```bash
|
|
177
|
-
# Preview what would be tagged
|
|
178
|
-
auto-release tag-release-commit --dry-run
|
|
179
|
-
|
|
180
|
-
# Create tags and releases
|
|
181
|
-
auto-release tag-release-commit
|
|
182
|
-
```
|
|
183
|
-
|
|
184
|
-
Compares HEAD with HEAD^1 to detect version changes. Creates tags in format `app-name@version`.
|
|
185
|
-
|
|
186
|
-
### `manual-release`
|
|
187
|
-
|
|
188
|
-
Create a manual release using existing change files:
|
|
189
|
-
|
|
190
|
-
```bash
|
|
191
|
-
auto-release manual-release
|
|
192
|
-
```
|
|
193
|
-
|
|
194
|
-
Useful for local testing or emergency releases.
|
|
195
|
-
|
|
196
|
-
## Configuration
|
|
197
|
-
|
|
198
|
-
### Apps
|
|
199
|
-
|
|
200
|
-
The `apps` object defines each releasable unit:
|
|
201
|
-
|
|
202
|
-
```typescript
|
|
203
|
-
apps: {
|
|
204
|
-
'my-app': {
|
|
205
|
-
// Components: where versions are read/written
|
|
206
|
-
components: [
|
|
207
|
-
node('packages/my-app'),
|
|
208
|
-
node('packages/shared'),
|
|
209
|
-
],
|
|
210
|
-
|
|
211
|
-
// Versioning strategy
|
|
212
|
-
versioning: semver(),
|
|
213
|
-
|
|
214
|
-
// Changelog file path
|
|
215
|
-
changelog: 'apps/my-app/CHANGELOG.md',
|
|
216
|
-
},
|
|
217
|
-
}
|
|
218
|
-
```
|
|
219
|
-
|
|
220
|
-
### Versioning Strategies
|
|
221
|
-
|
|
222
|
-
```typescript
|
|
223
|
-
import { semver, calver, markver } from 'auto-release/versioning'
|
|
224
|
-
|
|
225
|
-
// Semantic versioning: 1.2.3
|
|
226
|
-
versioning: semver() // Change types: major, minor, patch
|
|
227
|
-
|
|
228
|
-
// Calendar versioning: 2025.1.2
|
|
229
|
-
versioning: calver() // Change types: feature, fix
|
|
230
|
-
|
|
231
|
-
// Marketing versioning: 1.0.0
|
|
232
|
-
versioning: markver() // Change types: marketing, feature, fix
|
|
233
|
-
```
|
|
234
|
-
|
|
235
|
-
### Git Platforms
|
|
236
|
-
|
|
237
|
-
#### GitHub
|
|
238
|
-
|
|
239
|
-
```typescript
|
|
240
|
-
import { github } from 'auto-release/providers'
|
|
241
|
-
|
|
242
|
-
git: {
|
|
243
|
-
platform: github({
|
|
244
|
-
token: process.env.GITHUB_TOKEN!,
|
|
245
|
-
owner: 'your-org',
|
|
246
|
-
repo: 'your-repo',
|
|
247
|
-
}),
|
|
248
|
-
target_branch: 'main',
|
|
249
|
-
}
|
|
250
|
-
```
|
|
251
|
-
|
|
252
|
-
#### GitLab
|
|
70
|
+
## Documentation
|
|
253
71
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
token: process.env.GITLAB_TOKEN!,
|
|
260
|
-
project_id: 'your-project-id',
|
|
261
|
-
}),
|
|
262
|
-
target_branch: 'main',
|
|
263
|
-
}
|
|
264
|
-
```
|
|
265
|
-
|
|
266
|
-
### Components
|
|
267
|
-
|
|
268
|
-
Components define version sources:
|
|
269
|
-
|
|
270
|
-
- **`node(path)`**: any node project with package.json
|
|
271
|
-
- **`bun(path)`**: any bun project with package.json
|
|
272
|
-
- **`expo(path)`**: any Expo project with package.json and app.json
|
|
273
|
-
- **`php(path)`**: any PHP project with composer.json
|
|
274
|
-
|
|
275
|
-
```typescript
|
|
276
|
-
import { node, expo, php } from 'auto-release/components'
|
|
277
|
-
|
|
278
|
-
components: [
|
|
279
|
-
node('packages/web'),
|
|
280
|
-
bun('packages/bff'),
|
|
281
|
-
expo('apps/mobile'),
|
|
282
|
-
php('packages/api'),
|
|
283
|
-
]
|
|
284
|
-
```
|
|
285
|
-
|
|
286
|
-
## Change Files
|
|
287
|
-
|
|
288
|
-
Change files are stored in `.changes/<app-name>/` with format:
|
|
289
|
-
|
|
290
|
-
```
|
|
291
|
-
<type>.<slug>.md
|
|
292
|
-
```
|
|
293
|
-
|
|
294
|
-
Examples:
|
|
295
|
-
- `.changes/my-app/major.add-authentication.md`
|
|
296
|
-
- `.changes/my-app/patch.fix-login-bug.md`
|
|
297
|
-
|
|
298
|
-
The change files folder can be customized.
|
|
299
|
-
|
|
300
|
-
### Format
|
|
301
|
-
|
|
302
|
-
**Simple** (title only):
|
|
303
|
-
|
|
304
|
-
```markdown
|
|
305
|
-
Fix authentication bug in login flow
|
|
306
|
-
```
|
|
307
|
-
|
|
308
|
-
**Detailed** (with description):
|
|
309
|
-
|
|
310
|
-
```markdown
|
|
311
|
-
This adds a comprehensive user profile page with:
|
|
312
|
-
- Avatar upload
|
|
313
|
-
- Bio and social links
|
|
314
|
-
- Privacy settings
|
|
315
|
-
```
|
|
72
|
+
- [Configuration](./docs/configuration.md)
|
|
73
|
+
- [Recommended usage](./docs/recommended-usage.md)
|
|
74
|
+
- [Change File anatomy](./docs/change-file-anatomy.md)
|
|
75
|
+
- [Commands](./docs/commands.md)
|
|
76
|
+
- [Lexicon](./docs/lexicon.md)
|
|
316
77
|
|
|
317
78
|
## Philosophy
|
|
318
79
|
|
|
319
|
-
Inspired by Changesets and Release Please, designed for
|
|
80
|
+
Inspired by Changesets and Release Please, designed for project-centric monorepos where:
|
|
320
81
|
|
|
321
|
-
- Multiple
|
|
322
|
-
- Change files are organized by
|
|
82
|
+
- Multiple projects release independently with different versioning strategies
|
|
83
|
+
- Change files are organized by project for clarity
|
|
323
84
|
- Release branches allow testing before production
|
|
324
85
|
- Deployment is integrated with the release process
|
|
325
86
|
|
|
@@ -327,4 +88,4 @@ Inspired by Changesets and Release Please, designed for app-centric monorepos wh
|
|
|
327
88
|
|
|
328
89
|
MIT
|
|
329
90
|
|
|
330
|
-
See [LICENSE](./LICENSE)
|
|
91
|
+
See [LICENSE](./LICENSE)
|
|
@@ -6,10 +6,10 @@ import { find_change_files } from "../change-file.mjs";
|
|
|
6
6
|
import { join } from "node:path";
|
|
7
7
|
|
|
8
8
|
//#region src/lib/commands/check.ts
|
|
9
|
-
async function verify_component_version_consistency(
|
|
9
|
+
async function verify_component_version_consistency(project) {
|
|
10
10
|
const versions = /* @__PURE__ */ new Set();
|
|
11
11
|
const errors = [];
|
|
12
|
-
for (const component of
|
|
12
|
+
for (const component of project.components) for (const part of component.parts) {
|
|
13
13
|
const file_content = await read_file(part.file);
|
|
14
14
|
if (file_content === null) {
|
|
15
15
|
errors.push(`component ${component.root} has no version`);
|
|
@@ -19,14 +19,14 @@ async function verify_component_version_consistency(app) {
|
|
|
19
19
|
versions.add(version);
|
|
20
20
|
}
|
|
21
21
|
if (versions.size === 0) {
|
|
22
|
-
errors.push(`
|
|
22
|
+
errors.push(`project ${project.name} has no versions`);
|
|
23
23
|
return {
|
|
24
24
|
ok: false,
|
|
25
25
|
errors
|
|
26
26
|
};
|
|
27
27
|
}
|
|
28
28
|
if (versions.size > 1) {
|
|
29
|
-
errors.push(`
|
|
29
|
+
errors.push(`project ${project.name} has multiple versions: ${Array.from(versions).join(", ")}`);
|
|
30
30
|
return {
|
|
31
31
|
ok: false,
|
|
32
32
|
errors
|
|
@@ -34,9 +34,9 @@ async function verify_component_version_consistency(app) {
|
|
|
34
34
|
}
|
|
35
35
|
return { ok: true };
|
|
36
36
|
}
|
|
37
|
-
async function validate_changes_files_content(changes_dir,
|
|
37
|
+
async function validate_changes_files_content(changes_dir, project) {
|
|
38
38
|
const errors = [];
|
|
39
|
-
const change_files = await find_change_files(join(changes_dir,
|
|
39
|
+
const change_files = await find_change_files(join(changes_dir, project.name), { allowed_kinds: project.versioning.allowed_changes });
|
|
40
40
|
if (change_files.warnings.length > 0) for (const warning of change_files.warnings) errors.push(warning);
|
|
41
41
|
for (const change_file of change_files.list) if (change_file.summary.length === 0) errors.push(`change file ${change_file.filename} has no summary`);
|
|
42
42
|
if (errors.length > 0) return {
|
|
@@ -45,9 +45,30 @@ async function validate_changes_files_content(changes_dir, app) {
|
|
|
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
|
-
description: "Validate configuration,
|
|
71
|
+
description: "Validate configuration, versions, and change files",
|
|
51
72
|
schema: { config: {
|
|
52
73
|
type: "string",
|
|
53
74
|
description: "Path to config file"
|
|
@@ -62,18 +83,29 @@ 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;
|
|
66
|
-
|
|
67
|
-
|
|
88
|
+
const group_validation = validate_groups(config.managed_projects);
|
|
89
|
+
errors.push(...group_validation.errors);
|
|
90
|
+
warnings.push(...group_validation.warnings);
|
|
91
|
+
for (const project of config.managed_projects) {
|
|
92
|
+
const component_validation = await verify_component_version_consistency(project);
|
|
68
93
|
if (!component_validation.ok) errors.push(...component_validation.errors);
|
|
69
|
-
const changes_validation = await validate_changes_files_content(config.changes_dir,
|
|
94
|
+
const changes_validation = await validate_changes_files_content(config.changes_dir, project);
|
|
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 apps",
|
|
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
|
-
message: "No
|
|
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 = app.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 ${app.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 app.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 = app.versioning.formatter;
|
|
80
|
-
const initial_changelog_content = await read_file(app.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) => app.versioning.compare(b.version, a.version))
|
|
89
|
-
}, { app: { name: app.name } });
|
|
90
|
-
await write_file(app.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}/${app.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 ${app.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: ${app.name}@${next_version}`,
|
|
105
|
-
body: formatter.generate_pr_body({
|
|
106
|
-
app: { name: app.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 };
|