@logickernel/agileflow 0.2.2 → 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 +133 -211
- package/docs/README.md +39 -24
- package/docs/best-practices.md +201 -234
- package/docs/branching-strategy.md +153 -254
- package/docs/cli-reference.md +84 -64
- package/docs/configuration.md +154 -167
- package/docs/conventional-commits.md +207 -160
- package/docs/getting-started.md +162 -117
- package/docs/installation.md +244 -106
- package/docs/migration-guide.md +212 -299
- package/docs/release-management.md +195 -384
- package/docs/troubleshooting.md +276 -250
- package/docs/version-centric-cicd.md +239 -116
- package/package.json +3 -2
- package/src/index.js +47 -7
- package/src/utils.js +43 -13
|
@@ -1,166 +1,289 @@
|
|
|
1
|
-
# Version-Centric CI/CD
|
|
1
|
+
# Version-Centric CI/CD
|
|
2
2
|
|
|
3
|
-
AgileFlow
|
|
3
|
+
AgileFlow enables a **version-centric approach** to CI/CD where versioning is decoupled from build and deployment. This architecture simplifies pipelines and provides flexibility.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## The Decoupled Architecture
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
7
|
+
```
|
|
8
|
+
┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐
|
|
9
|
+
│ Merge to main │ │ Tag: v1.2.3 │ │ Build/Deploy │
|
|
10
|
+
│ │ ──────▶ │ │ ──────▶ │ │
|
|
11
|
+
│ AgileFlow │ │ (event) │ │ Your pipelines │
|
|
12
|
+
│ creates tag │ │ │ │ │
|
|
13
|
+
└─────────────────┘ └─────────────────┘ └─────────────────┘
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
### How It Works
|
|
17
|
+
|
|
18
|
+
1. **On merge to main**: AgileFlow analyzes commits and creates a version tag
|
|
19
|
+
2. **Tag creation event**: Triggers your build and deploy pipelines
|
|
20
|
+
3. **Build/Deploy**: Uses the tag as the version identifier
|
|
21
|
+
|
|
22
|
+
### Benefits
|
|
23
|
+
|
|
24
|
+
- **Separation of concerns** — Versioning is independent from build/deploy
|
|
25
|
+
- **Flexibility** — Any process can hook into tag creation
|
|
26
|
+
- **Simplicity** — Each pipeline has one responsibility
|
|
27
|
+
- **Reusability** — Same build pipeline for all versions
|
|
28
|
+
- **Auditability** — Clear version trail for every deployment
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## Traditional vs. Version-Centric
|
|
33
|
+
|
|
34
|
+
### Traditional (Coupled)
|
|
9
35
|
|
|
10
|
-
```
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
36
|
+
```yaml
|
|
37
|
+
# Everything in one pipeline
|
|
38
|
+
on: push to main
|
|
39
|
+
→ calculate version
|
|
40
|
+
→ build
|
|
41
|
+
→ deploy staging
|
|
42
|
+
→ deploy production
|
|
16
43
|
```
|
|
17
44
|
|
|
18
|
-
**Problems
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
45
|
+
**Problems:**
|
|
46
|
+
- Complex, monolithic pipelines
|
|
47
|
+
- Version logic mixed with build logic
|
|
48
|
+
- Hard to rerun individual steps
|
|
49
|
+
|
|
50
|
+
### Version-Centric (Decoupled)
|
|
51
|
+
|
|
52
|
+
```yaml
|
|
53
|
+
# Pipeline 1: Versioning
|
|
54
|
+
on: push to main
|
|
55
|
+
→ AgileFlow creates tag
|
|
56
|
+
|
|
57
|
+
# Pipeline 2: Release
|
|
58
|
+
on: tag created
|
|
59
|
+
→ build with tag version
|
|
60
|
+
→ deploy staging
|
|
61
|
+
→ deploy production
|
|
35
62
|
```
|
|
36
63
|
|
|
37
|
-
**Benefits
|
|
38
|
-
-
|
|
39
|
-
-
|
|
40
|
-
-
|
|
41
|
-
- **Consistent Testing**: All tests run against the same version that will be deployed
|
|
42
|
-
- **Clear Audit Trail**: Every deployment is tied to a specific, documented version
|
|
64
|
+
**Benefits:**
|
|
65
|
+
- Simple, focused pipelines
|
|
66
|
+
- Versioning completely separate
|
|
67
|
+
- Easy to rerun builds for any version
|
|
43
68
|
|
|
44
|
-
|
|
69
|
+
---
|
|
45
70
|
|
|
46
|
-
|
|
71
|
+
## Implementation
|
|
47
72
|
|
|
48
|
-
###
|
|
49
|
-
- **Purpose**: Generate semantic version and comprehensive release notes
|
|
50
|
-
- **Output**: `VERSION` variable available to all subsequent stages
|
|
51
|
-
- **Automation**: Uses AgileFlow tool to analyze commit history and determine next version
|
|
52
|
-
- **Artifacts**: Version tag pushed to repository, release notes generated
|
|
73
|
+
### GitHub Actions
|
|
53
74
|
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
75
|
+
**Versioning workflow** (`.github/workflows/version.yml`):
|
|
76
|
+
```yaml
|
|
77
|
+
name: Version
|
|
78
|
+
on:
|
|
79
|
+
push:
|
|
80
|
+
branches: [main]
|
|
59
81
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
-
|
|
63
|
-
|
|
64
|
-
-
|
|
82
|
+
jobs:
|
|
83
|
+
version:
|
|
84
|
+
runs-on: ubuntu-latest
|
|
85
|
+
steps:
|
|
86
|
+
- uses: actions/checkout@v4
|
|
87
|
+
with:
|
|
88
|
+
fetch-depth: 0
|
|
65
89
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
-
|
|
69
|
-
- **Benefits**: Identical behavior across all environments
|
|
70
|
-
- **Rollback**: Simple version-based rollback (e.g., "rollback to v1.2.2")
|
|
90
|
+
- uses: actions/setup-node@v4
|
|
91
|
+
with:
|
|
92
|
+
node-version: '20'
|
|
71
93
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
94
|
+
- name: Create version tag
|
|
95
|
+
env:
|
|
96
|
+
AGILEFLOW_TOKEN: ${{ secrets.AGILEFLOW_TOKEN }}
|
|
97
|
+
run: npx @logickernel/agileflow github
|
|
98
|
+
```
|
|
77
99
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
100
|
+
**Release workflow** (`.github/workflows/release.yml`):
|
|
101
|
+
```yaml
|
|
102
|
+
name: Release
|
|
103
|
+
on:
|
|
104
|
+
push:
|
|
105
|
+
tags:
|
|
106
|
+
- 'v*'
|
|
82
107
|
|
|
83
|
-
|
|
108
|
+
jobs:
|
|
109
|
+
build:
|
|
110
|
+
runs-on: ubuntu-latest
|
|
111
|
+
steps:
|
|
112
|
+
- uses: actions/checkout@v4
|
|
84
113
|
|
|
85
|
-
|
|
114
|
+
- name: Get version
|
|
115
|
+
run: echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV
|
|
116
|
+
|
|
117
|
+
- name: Build
|
|
118
|
+
run: docker build -t myapp:$VERSION .
|
|
119
|
+
|
|
120
|
+
deploy-staging:
|
|
121
|
+
needs: build
|
|
122
|
+
runs-on: ubuntu-latest
|
|
123
|
+
environment: staging
|
|
124
|
+
steps:
|
|
125
|
+
- run: kubectl set image deployment/myapp myapp=myapp:$VERSION
|
|
126
|
+
|
|
127
|
+
deploy-production:
|
|
128
|
+
needs: build
|
|
129
|
+
runs-on: ubuntu-latest
|
|
130
|
+
environment: production
|
|
131
|
+
steps:
|
|
132
|
+
- run: kubectl set image deployment/myapp myapp=myapp:$VERSION
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### GitLab CI
|
|
86
136
|
|
|
87
137
|
```yaml
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
-
|
|
138
|
+
stages:
|
|
139
|
+
- version
|
|
140
|
+
- build
|
|
141
|
+
- deploy
|
|
91
142
|
|
|
92
|
-
#
|
|
93
|
-
|
|
94
|
-
stage:
|
|
143
|
+
# Versioning - runs on merge to main
|
|
144
|
+
agileflow:
|
|
145
|
+
stage: version
|
|
146
|
+
image: node:20-alpine
|
|
95
147
|
script:
|
|
96
|
-
-
|
|
97
|
-
|
|
148
|
+
- npx @logickernel/agileflow gitlab
|
|
149
|
+
rules:
|
|
150
|
+
- if: '$CI_COMMIT_BRANCH == "main"'
|
|
98
151
|
|
|
99
|
-
# Build
|
|
152
|
+
# Build - runs on tag creation
|
|
100
153
|
build:
|
|
101
154
|
stage: build
|
|
102
155
|
script:
|
|
103
|
-
- docker build -t myapp:$
|
|
104
|
-
- docker push myapp:$
|
|
105
|
-
|
|
106
|
-
-
|
|
107
|
-
|
|
108
|
-
# Deploy stage deploys the same version everywhere
|
|
109
|
-
deploy-testing:
|
|
110
|
-
stage: deploy
|
|
111
|
-
script:
|
|
112
|
-
- kubectl set image deployment/myapp myapp=myapp:${VERSION}
|
|
113
|
-
environment:
|
|
114
|
-
name: testing
|
|
115
|
-
needs:
|
|
116
|
-
- build
|
|
156
|
+
- docker build -t myapp:$CI_COMMIT_TAG .
|
|
157
|
+
- docker push myapp:$CI_COMMIT_TAG
|
|
158
|
+
rules:
|
|
159
|
+
- if: '$CI_COMMIT_TAG =~ /^v/'
|
|
117
160
|
|
|
161
|
+
# Deploy - runs on tag creation
|
|
118
162
|
deploy-staging:
|
|
119
163
|
stage: deploy
|
|
120
164
|
script:
|
|
121
|
-
- kubectl set image deployment/myapp myapp=myapp:$
|
|
165
|
+
- kubectl set image deployment/myapp myapp=myapp:$CI_COMMIT_TAG
|
|
122
166
|
environment:
|
|
123
167
|
name: staging
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
- build
|
|
168
|
+
rules:
|
|
169
|
+
- if: '$CI_COMMIT_TAG =~ /^v/'
|
|
127
170
|
|
|
128
171
|
deploy-production:
|
|
129
172
|
stage: deploy
|
|
130
173
|
script:
|
|
131
|
-
- kubectl set image deployment/myapp myapp=myapp:$
|
|
174
|
+
- kubectl set image deployment/myapp myapp=myapp:$CI_COMMIT_TAG
|
|
132
175
|
environment:
|
|
133
176
|
name: production
|
|
134
177
|
when: manual
|
|
135
|
-
|
|
136
|
-
-
|
|
178
|
+
rules:
|
|
179
|
+
- if: '$CI_COMMIT_TAG =~ /^v/'
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
---
|
|
137
183
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
184
|
+
## Version-Centric Deployments
|
|
185
|
+
|
|
186
|
+
### All Environments Use the Same Version
|
|
187
|
+
|
|
188
|
+
```
|
|
189
|
+
Tag v1.2.3
|
|
190
|
+
│
|
|
191
|
+
├──▶ Build: myapp:v1.2.3
|
|
192
|
+
│
|
|
193
|
+
├──▶ Staging: myapp:v1.2.3
|
|
194
|
+
│
|
|
195
|
+
└──▶ Production: myapp:v1.2.3
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
No environment drift — every environment runs identical code.
|
|
199
|
+
|
|
200
|
+
### Simple Rollbacks
|
|
201
|
+
|
|
202
|
+
```bash
|
|
203
|
+
# Rollback = deploy previous tag
|
|
204
|
+
kubectl set image deployment/myapp myapp=myapp:v1.2.2
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
### Clear Audit Trail
|
|
208
|
+
|
|
209
|
+
```bash
|
|
210
|
+
# What version is running?
|
|
211
|
+
kubectl get deployment myapp -o jsonpath='{.spec.template.spec.containers[0].image}'
|
|
212
|
+
# myapp:v1.2.3
|
|
213
|
+
|
|
214
|
+
# What's in that version?
|
|
215
|
+
git show v1.2.3
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
---
|
|
219
|
+
|
|
220
|
+
## Advanced Patterns
|
|
221
|
+
|
|
222
|
+
### Conditional Deployments
|
|
223
|
+
|
|
224
|
+
Deploy only specific version types:
|
|
225
|
+
|
|
226
|
+
```yaml
|
|
227
|
+
# Only deploy minor/major versions to production
|
|
228
|
+
deploy-production:
|
|
229
|
+
rules:
|
|
230
|
+
- if: '$CI_COMMIT_TAG =~ /^v\d+\.\d+\.0$/'
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
### Multiple Services
|
|
234
|
+
|
|
235
|
+
Same version for all services in a monorepo:
|
|
236
|
+
|
|
237
|
+
```yaml
|
|
238
|
+
build-backend:
|
|
239
|
+
script:
|
|
240
|
+
- docker build -t backend:$CI_COMMIT_TAG ./backend
|
|
241
|
+
|
|
242
|
+
build-frontend:
|
|
243
|
+
script:
|
|
244
|
+
- docker build -t frontend:$CI_COMMIT_TAG ./frontend
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
### Notifications
|
|
248
|
+
|
|
249
|
+
Announce new versions:
|
|
250
|
+
|
|
251
|
+
```yaml
|
|
252
|
+
notify:
|
|
141
253
|
script:
|
|
142
|
-
-
|
|
143
|
-
|
|
144
|
-
|
|
254
|
+
- |
|
|
255
|
+
curl -X POST "$SLACK_WEBHOOK" \
|
|
256
|
+
-d "{\"text\": \"Released $CI_COMMIT_TAG\"}"
|
|
257
|
+
rules:
|
|
258
|
+
- if: '$CI_COMMIT_TAG =~ /^v/'
|
|
145
259
|
```
|
|
146
260
|
|
|
261
|
+
---
|
|
262
|
+
|
|
147
263
|
## Key Advantages
|
|
148
264
|
|
|
149
|
-
1. **Eliminates
|
|
150
|
-
2. **Simplifies
|
|
151
|
-
3. **
|
|
152
|
-
4. **
|
|
153
|
-
5. **
|
|
154
|
-
|
|
265
|
+
1. **Eliminates environment drift** — All environments run identical versions
|
|
266
|
+
2. **Simplifies operations** — Work with versions, not branch states
|
|
267
|
+
3. **Enables easy rollbacks** — Just redeploy a previous tag
|
|
268
|
+
4. **Provides clear audit trail** — Every deployment tied to a version
|
|
269
|
+
5. **Decouples concerns** — Versioning separate from build/deploy
|
|
270
|
+
|
|
271
|
+
---
|
|
155
272
|
|
|
156
273
|
## Migration Path
|
|
157
274
|
|
|
158
|
-
If
|
|
275
|
+
If using a traditional coupled approach:
|
|
276
|
+
|
|
277
|
+
1. **Add AgileFlow** — Create versioning workflow
|
|
278
|
+
2. **Add tag-triggered workflow** — For build/deploy
|
|
279
|
+
3. **Test both workflows** — Verify tags trigger releases
|
|
280
|
+
4. **Remove old logic** — Clean up version calculation from build pipeline
|
|
281
|
+
|
|
282
|
+
---
|
|
159
283
|
|
|
160
|
-
|
|
161
|
-
2. **Gradually Simplify**: Remove environment-specific branches over time
|
|
162
|
-
3. **Update Deployments**: Modify deployment scripts to use `${VERSION}` variable
|
|
163
|
-
4. **Standardize Testing**: Run all tests against the versioned artifacts
|
|
164
|
-
5. **Document Changes**: Update runbooks to reference versions instead of branches
|
|
284
|
+
## Related Documentation
|
|
165
285
|
|
|
166
|
-
|
|
286
|
+
- [Getting Started](./getting-started.md) — Quick start
|
|
287
|
+
- [Installation Guide](./installation.md) — Setup instructions
|
|
288
|
+
- [Branching Strategy](./branching-strategy.md) — Git workflow
|
|
289
|
+
- [Best Practices](./best-practices.md) — Recommended patterns
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@logickernel/agileflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Automatic semantic versioning and changelog generation based on conventional commits",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"bin": {
|
|
@@ -14,7 +14,8 @@
|
|
|
14
14
|
"README.md"
|
|
15
15
|
],
|
|
16
16
|
"scripts": {
|
|
17
|
-
"test": "echo \"Error: no test specified\" && exit 1"
|
|
17
|
+
"test": "echo \"Error: no test specified\" && exit 1",
|
|
18
|
+
"prepack": "chmod +x bin/agileflow"
|
|
18
19
|
},
|
|
19
20
|
"keywords": [
|
|
20
21
|
"semantic-versioning",
|
package/src/index.js
CHANGED
|
@@ -12,9 +12,9 @@ Usage:
|
|
|
12
12
|
|
|
13
13
|
Commands:
|
|
14
14
|
<none> Prints the current version, next version, commits, and changelog
|
|
15
|
-
push Push a semantic version tag to the remote repository
|
|
16
|
-
gitlab
|
|
17
|
-
github
|
|
15
|
+
push Push a semantic version tag to the remote repository
|
|
16
|
+
gitlab Create a semantic version tag via GitLab API (for GitLab CI)
|
|
17
|
+
github Create a semantic version tag via GitHub API (for GitHub Actions)
|
|
18
18
|
|
|
19
19
|
Options:
|
|
20
20
|
--quiet Only output the next version (or empty if no bump)
|
|
@@ -26,11 +26,32 @@ For more information, visit: https://code.logickernel.com/tools/agileflow
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
/**
|
|
29
|
-
*
|
|
29
|
+
* Valid options that can be passed to commands.
|
|
30
|
+
*/
|
|
31
|
+
const VALID_OPTIONS = ['--quiet', '--help', '-h', '--version', '-v'];
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Valid commands.
|
|
35
|
+
*/
|
|
36
|
+
const VALID_COMMANDS = ['push', 'gitlab', 'github'];
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Parses command line arguments and validates them.
|
|
30
40
|
* @param {Array<string>} args - Command line arguments
|
|
31
41
|
* @returns {{quiet: boolean}}
|
|
42
|
+
* @throws {Error} If invalid options are found
|
|
32
43
|
*/
|
|
33
44
|
function parseArgs(args) {
|
|
45
|
+
// Check for invalid options
|
|
46
|
+
for (const arg of args) {
|
|
47
|
+
if (arg.startsWith('--') && !VALID_OPTIONS.includes(arg)) {
|
|
48
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
49
|
+
}
|
|
50
|
+
if (arg.startsWith('-') && !arg.startsWith('--') && !VALID_OPTIONS.includes(arg)) {
|
|
51
|
+
throw new Error(`Unknown option: ${arg}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
34
55
|
return {
|
|
35
56
|
quiet: args.includes('--quiet'),
|
|
36
57
|
};
|
|
@@ -61,7 +82,7 @@ function displayVersionInfo(info, quiet) {
|
|
|
61
82
|
}
|
|
62
83
|
|
|
63
84
|
console.log(`\nCurrent version: ${currentVersion || 'none'}`);
|
|
64
|
-
console.log(`New version:
|
|
85
|
+
console.log(`New version: ${newVersion || 'no bump needed'}`);
|
|
65
86
|
if (changelog) {
|
|
66
87
|
console.log(`\nChangelog:\n\n${changelog}`);
|
|
67
88
|
}
|
|
@@ -113,7 +134,17 @@ async function handlePushCommand(pushType, options) {
|
|
|
113
134
|
|
|
114
135
|
async function main() {
|
|
115
136
|
const [, , cmd, ...rest] = process.argv;
|
|
116
|
-
|
|
137
|
+
|
|
138
|
+
let options;
|
|
139
|
+
try {
|
|
140
|
+
options = parseArgs(cmd ? [cmd, ...rest] : rest);
|
|
141
|
+
} catch (err) {
|
|
142
|
+
console.error(`Error: ${err.message}`);
|
|
143
|
+
console.error();
|
|
144
|
+
printHelp();
|
|
145
|
+
process.exit(1);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
117
148
|
|
|
118
149
|
// Handle help
|
|
119
150
|
if (cmd === '-h' || cmd === '--help' || cmd === 'help') {
|
|
@@ -134,13 +165,22 @@ async function main() {
|
|
|
134
165
|
}
|
|
135
166
|
|
|
136
167
|
// Unknown command (not an option)
|
|
137
|
-
if (cmd && !cmd.startsWith('--')) {
|
|
168
|
+
if (cmd && !cmd.startsWith('--') && !cmd.startsWith('-')) {
|
|
138
169
|
console.error(`Error: Unknown command "${cmd}"`);
|
|
139
170
|
console.error();
|
|
140
171
|
printHelp();
|
|
141
172
|
process.exit(1);
|
|
142
173
|
}
|
|
143
174
|
|
|
175
|
+
// Invalid option (starts with -- but not valid)
|
|
176
|
+
if (cmd && cmd.startsWith('--') && !VALID_OPTIONS.includes(cmd)) {
|
|
177
|
+
console.error(`Error: Unknown option "${cmd}"`);
|
|
178
|
+
console.error();
|
|
179
|
+
printHelp();
|
|
180
|
+
process.exit(1);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
144
184
|
// Default: show version info
|
|
145
185
|
const info = await processVersionInfo();
|
|
146
186
|
displayVersionInfo(info, options.quiet);
|
package/src/utils.js
CHANGED
|
@@ -170,13 +170,16 @@ function extractIssueReference(message) {
|
|
|
170
170
|
* @param {string} subject - First line of commit message
|
|
171
171
|
* @param {Object} parsed - Parsed conventional commit info
|
|
172
172
|
* @param {string} fullMessage - Full commit message
|
|
173
|
+
* @param {boolean} isBreakingSection - Whether this is for the breaking changes section
|
|
173
174
|
* @returns {string} Formatted description
|
|
174
175
|
*/
|
|
175
|
-
function formatChangelogDescription(subject, parsed, fullMessage) {
|
|
176
|
+
function formatChangelogDescription(subject, parsed, fullMessage, isBreakingSection = false) {
|
|
176
177
|
if (!parsed) return subject;
|
|
177
178
|
let description = parsed.description;
|
|
178
179
|
const isBreaking = parsed.breaking || /BREAKING CHANGE:/i.test(fullMessage);
|
|
179
|
-
|
|
180
|
+
|
|
181
|
+
// Only add BREAKING prefix if not in breaking changes section
|
|
182
|
+
if (isBreaking && !isBreakingSection) {
|
|
180
183
|
description = `BREAKING: ${description}`;
|
|
181
184
|
}
|
|
182
185
|
return description;
|
|
@@ -229,13 +232,25 @@ function applyVersionBump(current, bump) {
|
|
|
229
232
|
}
|
|
230
233
|
}
|
|
231
234
|
|
|
235
|
+
/**
|
|
236
|
+
* Checks if a commit is a breaking change.
|
|
237
|
+
* @param {Object} commit - Commit object
|
|
238
|
+
* @param {Object} parsed - Parsed conventional commit info
|
|
239
|
+
* @returns {boolean}
|
|
240
|
+
*/
|
|
241
|
+
function isBreakingChange(commit, parsed) {
|
|
242
|
+
if (!parsed) return false;
|
|
243
|
+
return parsed.breaking || /BREAKING CHANGE:/i.test(commit.message);
|
|
244
|
+
}
|
|
245
|
+
|
|
232
246
|
/**
|
|
233
247
|
* Analyzes commits to determine version bump requirements.
|
|
234
248
|
* @param {Array} commits - Array of commit objects
|
|
235
|
-
* @returns {{hasBreaking: boolean, hasFeat: boolean, hasPatchTypes: boolean, commitsByType: Object}}
|
|
249
|
+
* @returns {{hasBreaking: boolean, hasFeat: boolean, hasPatchTypes: boolean, commitsByType: Object, breakingCommits: Array}}
|
|
236
250
|
*/
|
|
237
251
|
function analyzeCommitsForVersioning(commits) {
|
|
238
252
|
const commitsByType = Object.fromEntries(TYPE_ORDER.map(t => [t, []]));
|
|
253
|
+
const breakingCommits = [];
|
|
239
254
|
let hasBreaking = false, hasFeat = false, hasPatchTypes = false;
|
|
240
255
|
|
|
241
256
|
for (const commit of commits) {
|
|
@@ -243,18 +258,23 @@ function analyzeCommitsForVersioning(commits) {
|
|
|
243
258
|
if (!parsed) continue;
|
|
244
259
|
|
|
245
260
|
const { type, breaking } = parsed;
|
|
246
|
-
const isBreaking =
|
|
261
|
+
const isBreaking = isBreakingChange(commit, parsed);
|
|
247
262
|
|
|
248
|
-
if (isBreaking)
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
263
|
+
if (isBreaking) {
|
|
264
|
+
hasBreaking = true;
|
|
265
|
+
breakingCommits.push(commit);
|
|
266
|
+
} else {
|
|
267
|
+
// Only add to type sections if not breaking
|
|
268
|
+
if (type === 'feat') hasFeat = true;
|
|
269
|
+
else if (PATCH_TYPES.includes(type)) hasPatchTypes = true;
|
|
270
|
+
|
|
271
|
+
if (commitsByType[type]) {
|
|
272
|
+
commitsByType[type].push(commit);
|
|
273
|
+
}
|
|
254
274
|
}
|
|
255
275
|
}
|
|
256
276
|
|
|
257
|
-
return { hasBreaking, hasFeat, hasPatchTypes, commitsByType };
|
|
277
|
+
return { hasBreaking, hasFeat, hasPatchTypes, commitsByType, breakingCommits };
|
|
258
278
|
}
|
|
259
279
|
|
|
260
280
|
/**
|
|
@@ -270,9 +290,10 @@ function capitalize(str) {
|
|
|
270
290
|
/**
|
|
271
291
|
* Generates changelog entries for a commit type section.
|
|
272
292
|
* @param {Array} commits - Commits of this type
|
|
293
|
+
* @param {boolean} isBreakingSection - Whether this is for the breaking changes section
|
|
273
294
|
* @returns {Array<string>} Changelog lines
|
|
274
295
|
*/
|
|
275
|
-
function generateTypeChangelog(commits) {
|
|
296
|
+
function generateTypeChangelog(commits, isBreakingSection = false) {
|
|
276
297
|
const byScope = {};
|
|
277
298
|
const noScope = [];
|
|
278
299
|
|
|
@@ -283,7 +304,7 @@ function generateTypeChangelog(commits) {
|
|
|
283
304
|
const subject = commit.message.split('\n')[0].trim();
|
|
284
305
|
const entry = {
|
|
285
306
|
scope: parsed.scope,
|
|
286
|
-
description: formatChangelogDescription(subject, parsed, commit.message),
|
|
307
|
+
description: formatChangelogDescription(subject, parsed, commit.message, isBreakingSection),
|
|
287
308
|
issueRef: extractIssueReference(commit.message) || '',
|
|
288
309
|
};
|
|
289
310
|
|
|
@@ -323,6 +344,15 @@ function calculateNextVersionAndChangelog(expandedInfo) {
|
|
|
323
344
|
|
|
324
345
|
// Generate changelog
|
|
325
346
|
const changelogLines = [];
|
|
347
|
+
|
|
348
|
+
// Add breaking changes section first if any
|
|
349
|
+
if (analysis.breakingCommits.length > 0) {
|
|
350
|
+
changelogLines.push('BREAKING CHANGES:');
|
|
351
|
+
changelogLines.push(...generateTypeChangelog(analysis.breakingCommits, true));
|
|
352
|
+
changelogLines.push('');
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
// Add regular type sections
|
|
326
356
|
for (const type of TYPE_ORDER) {
|
|
327
357
|
const typeCommits = analysis.commitsByType[type];
|
|
328
358
|
if (!typeCommits?.length) continue;
|