@logickernel/agileflow 0.17.0 → 0.20.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 +54 -127
- package/docs/guides/github-actions.md +110 -0
- package/docs/guides/gitlab-ci.md +128 -0
- package/docs/guides/other-ci.md +93 -0
- package/docs/index.md +58 -0
- package/docs/reference/cli.md +124 -0
- package/docs/reference/conventional-commits.md +133 -0
- package/docs/start-here/getting-started.md +83 -0
- package/package.json +1 -1
- package/src/git-push.js +2 -4
- package/src/github-push.js +2 -4
- package/src/gitlab-push.js +3 -5
- package/src/index.js +18 -42
- package/src/utils.js +109 -7
- package/docs/README.md +0 -60
- package/docs/best-practices.md +0 -306
- package/docs/branching-strategy.md +0 -347
- package/docs/cli-reference.md +0 -301
- package/docs/configuration.md +0 -217
- package/docs/conventional-commits.md +0 -252
- package/docs/getting-started.md +0 -231
- package/docs/installation.md +0 -300
- package/docs/migration-guide.md +0 -303
- package/docs/release-management.md +0 -309
- package/docs/troubleshooting.md +0 -367
- package/docs/version-centric-cicd.md +0 -289
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# CLI Reference
|
|
2
|
+
|
|
3
|
+
## Installation
|
|
4
|
+
|
|
5
|
+
Run without installing:
|
|
6
|
+
```bash
|
|
7
|
+
npx @logickernel/agileflow
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
Or install globally:
|
|
11
|
+
```bash
|
|
12
|
+
npm install -g @logickernel/agileflow
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## Commands
|
|
18
|
+
|
|
19
|
+
### `agileflow` (no command)
|
|
20
|
+
|
|
21
|
+
Analyzes the repository and prints the current version, next version, commits, and changelog. Does not create or modify anything.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
agileflow
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
```
|
|
28
|
+
Commits since current version (3):
|
|
29
|
+
a1b2c3d feat: add dark mode
|
|
30
|
+
d4e5f6a fix: resolve login timeout
|
|
31
|
+
7g8h9i0 docs: update README
|
|
32
|
+
|
|
33
|
+
Current version: v1.4.2
|
|
34
|
+
New version: v1.5.0
|
|
35
|
+
|
|
36
|
+
Changelog:
|
|
37
|
+
|
|
38
|
+
### Features
|
|
39
|
+
- add dark mode
|
|
40
|
+
|
|
41
|
+
### Bug fixes
|
|
42
|
+
- resolve login timeout
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
If no bump is needed, `New version` shows `no bump needed` and no changelog is printed.
|
|
46
|
+
|
|
47
|
+
---
|
|
48
|
+
|
|
49
|
+
### `agileflow push [remote]`
|
|
50
|
+
|
|
51
|
+
Creates an annotated git tag and pushes it to the specified remote. Uses standard git commands — requires git credentials to be configured.
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
agileflow push # pushes to origin
|
|
55
|
+
agileflow push upstream # pushes to a different remote
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
If no bump is needed, exits without creating a tag.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
### `agileflow gitlab`
|
|
63
|
+
|
|
64
|
+
Creates a version tag via the GitLab API. Designed for GitLab CI pipelines.
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
agileflow gitlab
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Required environment variable:**
|
|
71
|
+
- `AGILEFLOW_TOKEN` — GitLab access token with `api` scope and `Maintainer` role
|
|
72
|
+
|
|
73
|
+
**Provided automatically by GitLab CI:**
|
|
74
|
+
- `CI_SERVER_HOST` — GitLab server hostname
|
|
75
|
+
- `CI_PROJECT_PATH` — Project path (e.g., `group/project`)
|
|
76
|
+
- `CI_COMMIT_SHA` — Commit to tag
|
|
77
|
+
|
|
78
|
+
---
|
|
79
|
+
|
|
80
|
+
### `agileflow github`
|
|
81
|
+
|
|
82
|
+
Creates a version tag via the GitHub API. Designed for GitHub Actions workflows.
|
|
83
|
+
|
|
84
|
+
```bash
|
|
85
|
+
agileflow github
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
**Required environment variable:**
|
|
89
|
+
- `AGILEFLOW_TOKEN` — GitHub Personal Access Token with `Contents: Read and write` permission
|
|
90
|
+
|
|
91
|
+
**Provided automatically by GitHub Actions:**
|
|
92
|
+
- `GITHUB_REPOSITORY` — Repository (e.g., `owner/repo`)
|
|
93
|
+
- `GITHUB_SHA` — Commit to tag
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
### `agileflow version`
|
|
98
|
+
|
|
99
|
+
Prints the AgileFlow tool version.
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
agileflow version
|
|
103
|
+
# 0.17.0
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
### `agileflow --help`
|
|
109
|
+
|
|
110
|
+
Prints usage information.
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
agileflow --help
|
|
114
|
+
agileflow -h
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Exit codes
|
|
120
|
+
|
|
121
|
+
| Code | Meaning |
|
|
122
|
+
|------|---------|
|
|
123
|
+
| `0` | Success (including "no bump needed") |
|
|
124
|
+
| `1` | Error (authentication failure, git error, API error, unknown command) |
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Conventional Commits
|
|
2
|
+
|
|
3
|
+
AgileFlow uses [Conventional Commits](https://www.conventionalcommits.org/) to determine the next semantic version and generate changelogs.
|
|
4
|
+
|
|
5
|
+
## Format
|
|
6
|
+
|
|
7
|
+
```
|
|
8
|
+
<type>[optional scope]: <description>
|
|
9
|
+
|
|
10
|
+
[optional body]
|
|
11
|
+
|
|
12
|
+
[optional footer(s)]
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
Examples:
|
|
16
|
+
```
|
|
17
|
+
feat: add user authentication
|
|
18
|
+
fix(api): handle timeout errors
|
|
19
|
+
feat!: remove deprecated endpoints
|
|
20
|
+
docs: update README
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
---
|
|
24
|
+
|
|
25
|
+
## Version impact
|
|
26
|
+
|
|
27
|
+
| Commit | Example | Before v1.0.0 | v1.0.0 and after |
|
|
28
|
+
|--------|---------|---------------|------------------|
|
|
29
|
+
| Breaking change | `feat!: redesign API` | minor bump | major bump |
|
|
30
|
+
| `feat` | `feat: add login` | minor bump | minor bump |
|
|
31
|
+
| `fix` | `fix: resolve crash` | patch bump | patch bump |
|
|
32
|
+
| Everything else | `docs: update README` | no bump | no bump |
|
|
33
|
+
|
|
34
|
+
When multiple commits exist since the last tag, the highest-priority bump wins.
|
|
35
|
+
|
|
36
|
+
### Marking breaking changes
|
|
37
|
+
|
|
38
|
+
```bash
|
|
39
|
+
# Using ! after the type
|
|
40
|
+
feat!: remove deprecated API endpoints
|
|
41
|
+
|
|
42
|
+
# Using a BREAKING CHANGE footer
|
|
43
|
+
feat: change response format
|
|
44
|
+
|
|
45
|
+
BREAKING CHANGE: Response now uses camelCase instead of snake_case
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## Commit types
|
|
51
|
+
|
|
52
|
+
| Type | Use for | Changelog |
|
|
53
|
+
|------|---------|-----------|
|
|
54
|
+
| `feat` | New functionality | Yes — Features |
|
|
55
|
+
| `fix` | Bug fixes | Yes — Bug fixes |
|
|
56
|
+
| `perf` | Performance improvements | Yes — Performance improvements |
|
|
57
|
+
| `refactor` | Code restructuring (no behavior change) | Yes — Other changes |
|
|
58
|
+
| `docs` | Documentation only | Yes — Documentation |
|
|
59
|
+
| `ci` | CI/CD configuration | Yes — Other changes |
|
|
60
|
+
| `test` | Tests only | No |
|
|
61
|
+
| `style` | Formatting, whitespace | No |
|
|
62
|
+
| `chore` | Maintenance tasks, work in progress | No |
|
|
63
|
+
| `build` | Build system changes | No |
|
|
64
|
+
| `revert` | Revert a previous commit | No |
|
|
65
|
+
|
|
66
|
+
Types not in this table appear under "Other changes" in the changelog.
|
|
67
|
+
|
|
68
|
+
---
|
|
69
|
+
|
|
70
|
+
## Choosing the right type
|
|
71
|
+
|
|
72
|
+
1. **Adds new functionality users can use?** → `feat`
|
|
73
|
+
2. **Fixes broken functionality?** → `fix`
|
|
74
|
+
3. **Work in progress or maintenance with no user impact?** → `chore` (excluded from changelog)
|
|
75
|
+
4. **Performance improvement?** → `perf`
|
|
76
|
+
5. **Refactoring internal code?** → `refactor`
|
|
77
|
+
6. **Breaking any existing behavior?** → add `!` after the type (e.g., `feat!:`, `fix!:`)
|
|
78
|
+
|
|
79
|
+
---
|
|
80
|
+
|
|
81
|
+
## Best practices
|
|
82
|
+
|
|
83
|
+
Use present tense: `feat: add login` not `feat: added login`
|
|
84
|
+
|
|
85
|
+
Be specific: `fix: prevent timeout on uploads larger than 100MB` not `fix: fix timeout`
|
|
86
|
+
|
|
87
|
+
Use `chore:` for work in progress so it doesn't add noise to the changelog:
|
|
88
|
+
```bash
|
|
89
|
+
# ✅ Won't appear in changelog
|
|
90
|
+
chore: scaffold form validation module
|
|
91
|
+
|
|
92
|
+
# ❌ Will appear as "Other changes" — misleading
|
|
93
|
+
refactor: scaffold form validation module
|
|
94
|
+
```
|
|
95
|
+
|
|
96
|
+
Use scopes to clarify context when helpful:
|
|
97
|
+
```bash
|
|
98
|
+
feat(auth): add OAuth2 support
|
|
99
|
+
fix(api): handle empty response body
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Changelog format
|
|
105
|
+
|
|
106
|
+
AgileFlow groups commits by type:
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
v1.5.0
|
|
110
|
+
|
|
111
|
+
### Features
|
|
112
|
+
- add dark mode
|
|
113
|
+
- add keyboard shortcuts
|
|
114
|
+
|
|
115
|
+
### Bug fixes
|
|
116
|
+
- resolve login timeout
|
|
117
|
+
- fix pagination on mobile
|
|
118
|
+
|
|
119
|
+
### Performance improvements
|
|
120
|
+
- optimize image loading
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
Breaking changes are highlighted:
|
|
124
|
+
|
|
125
|
+
```
|
|
126
|
+
v2.0.0
|
|
127
|
+
|
|
128
|
+
### Features
|
|
129
|
+
- BREAKING: remove deprecated v1 API endpoints
|
|
130
|
+
- add new v2 API
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
Non-conventional commits (no `type:` prefix) appear under "Other changes" and trigger no version bump.
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# Getting Started
|
|
2
|
+
|
|
3
|
+
## Run it now
|
|
4
|
+
|
|
5
|
+
No installation needed. Run AgileFlow in any git repository:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx @logickernel/agileflow
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
This is a read-only preview — it never creates tags or modifies anything.
|
|
12
|
+
|
|
13
|
+
### Understanding the output
|
|
14
|
+
|
|
15
|
+
```
|
|
16
|
+
Commits since current version (3):
|
|
17
|
+
a1b2c3d feat: add dark mode
|
|
18
|
+
d4e5f6a fix: resolve login timeout
|
|
19
|
+
7g8h9i0 docs: update README
|
|
20
|
+
|
|
21
|
+
Current version: v1.4.2
|
|
22
|
+
New version: v1.5.0
|
|
23
|
+
|
|
24
|
+
Changelog:
|
|
25
|
+
|
|
26
|
+
### Features
|
|
27
|
+
- add dark mode
|
|
28
|
+
|
|
29
|
+
### Bug fixes
|
|
30
|
+
- resolve login timeout
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
- **Current version** — the highest semver tag found in the commit history
|
|
34
|
+
- **New version** — calculated from the commit types since that tag
|
|
35
|
+
- **Changelog** — grouped by commit type; `docs`, `chore`, and `style` commits are omitted from the changelog but still analyzed for versioning
|
|
36
|
+
|
|
37
|
+
If all commits since the last tag are `docs`, `chore`, `style`, or other non-bumping types, AgileFlow prints `no bump needed` and skips tag creation.
|
|
38
|
+
|
|
39
|
+
### Starting from scratch
|
|
40
|
+
|
|
41
|
+
No version tags yet? AgileFlow starts from `v0.0.0`:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
git commit -m "feat: initial project setup"
|
|
45
|
+
npx @logickernel/agileflow
|
|
46
|
+
# New version: v0.1.0
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
### Creating v1.0.0
|
|
50
|
+
|
|
51
|
+
`v1.0.0` signals your first stable release. Create it manually when you're ready:
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
git tag -a v1.0.0 -m "First stable release"
|
|
55
|
+
git push origin v1.0.0
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
After `v1.0.0`, breaking changes bump the major version (e.g., `v2.0.0`). Before it, they bump minor (e.g., `v0.2.0`).
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Set up CI/CD
|
|
63
|
+
|
|
64
|
+
Pick your platform:
|
|
65
|
+
|
|
66
|
+
- [GitHub Actions](../guides/github-actions.md)
|
|
67
|
+
- [GitLab CI](../guides/gitlab-ci.md)
|
|
68
|
+
- [Other CI/CD](../guides/other-ci.md)
|
|
69
|
+
|
|
70
|
+
---
|
|
71
|
+
|
|
72
|
+
## How version bumps are calculated
|
|
73
|
+
|
|
74
|
+
AgileFlow picks the highest-priority bump across all commits since the last tag:
|
|
75
|
+
|
|
76
|
+
| Commit | Example | Before v1.0.0 | v1.0.0 and after |
|
|
77
|
+
|--------|---------|---------------|------------------|
|
|
78
|
+
| Breaking change | `feat!: redesign API` | minor bump | major bump |
|
|
79
|
+
| New feature | `feat: add login` | minor bump | minor bump |
|
|
80
|
+
| Bug fix | `fix: resolve crash` | patch bump | patch bump |
|
|
81
|
+
| Everything else | `docs: update README` | no bump | no bump |
|
|
82
|
+
|
|
83
|
+
See [Conventional Commits](../reference/conventional-commits.md) for the full format reference.
|
package/package.json
CHANGED
package/src/git-push.js
CHANGED
|
@@ -14,7 +14,7 @@ const os = require('os');
|
|
|
14
14
|
* @param {boolean} quiet - If true, suppress success message
|
|
15
15
|
* @returns {Promise<void>}
|
|
16
16
|
*/
|
|
17
|
-
async function pushTag(tagName, message,
|
|
17
|
+
async function pushTag(tagName, message, remote = 'origin') {
|
|
18
18
|
const safeTag = String(tagName).replace(/"/g, '\\"');
|
|
19
19
|
const safeRemote = String(remote).replace(/"/g, '\\"');
|
|
20
20
|
|
|
@@ -29,9 +29,7 @@ async function pushTag(tagName, message, quiet = false, remote = 'origin') {
|
|
|
29
29
|
// Push to remote
|
|
30
30
|
execSync(`git push "${safeRemote}" "${safeTag}"`, { stdio: 'pipe' });
|
|
31
31
|
|
|
32
|
-
|
|
33
|
-
console.log(`Tag ${tagName} created and pushed successfully.`);
|
|
34
|
-
}
|
|
32
|
+
console.log(`Tag ${tagName} created and pushed successfully.`);
|
|
35
33
|
} finally {
|
|
36
34
|
// Clean up temp file
|
|
37
35
|
try {
|
package/src/github-push.js
CHANGED
|
@@ -130,7 +130,7 @@ function makeRequest({ method, path, accessToken, body }) {
|
|
|
130
130
|
* @param {boolean} quiet - If true, suppress success message
|
|
131
131
|
* @returns {Promise<void>}
|
|
132
132
|
*/
|
|
133
|
-
async function pushTag(tagName, message,
|
|
133
|
+
async function pushTag(tagName, message, remote = 'origin') {
|
|
134
134
|
const accessToken = process.env.AGILEFLOW_TOKEN;
|
|
135
135
|
const repository = process.env.GITHUB_REPOSITORY;
|
|
136
136
|
const commitSha = process.env.GITHUB_SHA;
|
|
@@ -155,9 +155,7 @@ async function pushTag(tagName, message, quiet = false, remote = 'origin') {
|
|
|
155
155
|
|
|
156
156
|
await createTagViaAPI(tagName, message || tagName, repository, accessToken, commitSha);
|
|
157
157
|
|
|
158
|
-
|
|
159
|
-
console.log(`Tag ${tagName} created and pushed successfully.`);
|
|
160
|
-
}
|
|
158
|
+
console.log(`Tag ${tagName} created and pushed successfully.`);
|
|
161
159
|
}
|
|
162
160
|
|
|
163
161
|
module.exports = {
|
package/src/gitlab-push.js
CHANGED
|
@@ -96,7 +96,7 @@ function createTagViaAPI(tagName, message, projectPath, serverHost, accessToken,
|
|
|
96
96
|
* @param {string} message - The tag message
|
|
97
97
|
* @returns {Promise<void>}
|
|
98
98
|
*/
|
|
99
|
-
async function pushTag(tagName, message,
|
|
99
|
+
async function pushTag(tagName, message, remote = 'origin') {
|
|
100
100
|
const accessToken = process.env.AGILEFLOW_TOKEN;
|
|
101
101
|
const serverHost = process.env.CI_SERVER_HOST;
|
|
102
102
|
const projectPath = process.env.CI_PROJECT_PATH;
|
|
@@ -134,10 +134,8 @@ async function pushTag(tagName, message, quiet = false, remote = 'origin') {
|
|
|
134
134
|
|
|
135
135
|
await createTagViaAPI(tagName, message || tagName, projectPath, serverHost, accessToken, commitSha);
|
|
136
136
|
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
console.log(`Tag ${tagName} created and pushed successfully.\nView the build pipelines at: ${commitUrl}`);
|
|
140
|
-
}
|
|
137
|
+
const commitUrl = `https://${process.env.CI_SERVER_HOST}/${process.env.CI_PROJECT_PATH}/-/commit/${process.env.CI_COMMIT_SHA}/pipelines`;
|
|
138
|
+
console.log(`Tag ${tagName} created and pushed successfully.\nView the build pipelines at: ${commitUrl}`);
|
|
141
139
|
}
|
|
142
140
|
|
|
143
141
|
module.exports = {
|
package/src/index.js
CHANGED
|
@@ -18,9 +18,7 @@ Commands:
|
|
|
18
18
|
version Print the agileflow tool version
|
|
19
19
|
|
|
20
20
|
Options:
|
|
21
|
-
--quiet Only output the next version (or empty if no bump)
|
|
22
21
|
-h, --help Show this help message
|
|
23
|
-
-v, --version Show version number
|
|
24
22
|
|
|
25
23
|
For more information, visit: https://code.logickernel.com/tools/agileflow
|
|
26
24
|
`);
|
|
@@ -29,7 +27,7 @@ For more information, visit: https://code.logickernel.com/tools/agileflow
|
|
|
29
27
|
/**
|
|
30
28
|
* Valid options that can be passed to commands.
|
|
31
29
|
*/
|
|
32
|
-
const VALID_OPTIONS = ['--
|
|
30
|
+
const VALID_OPTIONS = ['--help', '-h'];
|
|
33
31
|
|
|
34
32
|
/**
|
|
35
33
|
* Valid commands.
|
|
@@ -39,11 +37,9 @@ const VALID_COMMANDS = ['push', 'gitlab', 'github', 'version'];
|
|
|
39
37
|
/**
|
|
40
38
|
* Parses command line arguments and validates them.
|
|
41
39
|
* @param {Array<string>} args - Command line arguments
|
|
42
|
-
* @returns {{quiet: boolean}}
|
|
43
40
|
* @throws {Error} If invalid options are found
|
|
44
41
|
*/
|
|
45
42
|
function parseArgs(args) {
|
|
46
|
-
// Check for invalid options
|
|
47
43
|
for (const arg of args) {
|
|
48
44
|
if (arg.startsWith('--') && !VALID_OPTIONS.includes(arg)) {
|
|
49
45
|
throw new Error(`Unknown option: ${arg}`);
|
|
@@ -52,28 +48,15 @@ function parseArgs(args) {
|
|
|
52
48
|
throw new Error(`Unknown option: ${arg}`);
|
|
53
49
|
}
|
|
54
50
|
}
|
|
55
|
-
|
|
56
|
-
return {
|
|
57
|
-
quiet: args.includes('--quiet'),
|
|
58
|
-
};
|
|
59
51
|
}
|
|
60
52
|
|
|
61
53
|
/**
|
|
62
54
|
* Displays version info to the console.
|
|
63
55
|
* @param {{currentVersion: string|null, newVersion: string|null, commits: Array, changelog: string}} info
|
|
64
|
-
* @param {boolean} quiet - Only output the new version
|
|
65
56
|
*/
|
|
66
|
-
function displayVersionInfo(info
|
|
57
|
+
function displayVersionInfo(info) {
|
|
67
58
|
const { currentVersion, newVersion, commits, changelog } = info;
|
|
68
|
-
|
|
69
|
-
if (quiet) {
|
|
70
|
-
if (newVersion) {
|
|
71
|
-
console.log(newVersion);
|
|
72
|
-
}
|
|
73
|
-
return;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
|
|
59
|
+
|
|
77
60
|
// List commits
|
|
78
61
|
console.log(`Commits since current version (${commits.length}):`);
|
|
79
62
|
for (const commit of commits) {
|
|
@@ -92,20 +75,17 @@ function displayVersionInfo(info, quiet) {
|
|
|
92
75
|
/**
|
|
93
76
|
* Handles a push command.
|
|
94
77
|
* @param {string} pushType - 'push', 'gitlab', or 'github'
|
|
95
|
-
* @param {
|
|
78
|
+
* @param {string} remote
|
|
96
79
|
*/
|
|
97
|
-
async function handlePushCommand(pushType,
|
|
80
|
+
async function handlePushCommand(pushType, remote = 'origin') {
|
|
98
81
|
const info = await processVersionInfo();
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
// Skip push if no version bump needed
|
|
82
|
+
|
|
83
|
+
displayVersionInfo(info);
|
|
84
|
+
|
|
104
85
|
if (!info.newVersion) {
|
|
105
86
|
return;
|
|
106
87
|
}
|
|
107
|
-
|
|
108
|
-
// Get the appropriate push module
|
|
88
|
+
|
|
109
89
|
let pushModule;
|
|
110
90
|
switch (pushType) {
|
|
111
91
|
case 'push':
|
|
@@ -118,23 +98,19 @@ async function handlePushCommand(pushType, options, remote = 'origin') {
|
|
|
118
98
|
pushModule = require('./github-push');
|
|
119
99
|
break;
|
|
120
100
|
}
|
|
121
|
-
|
|
122
|
-
// Create tag message from changelog
|
|
101
|
+
|
|
123
102
|
const tagMessage = info.changelog || info.newVersion;
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
await pushModule.pushTag(info.newVersion, tagMessage, options.quiet, remote);
|
|
103
|
+
|
|
104
|
+
console.log(`\nCreating tag ${info.newVersion}...`);
|
|
105
|
+
|
|
106
|
+
await pushModule.pushTag(info.newVersion, tagMessage, remote);
|
|
130
107
|
}
|
|
131
108
|
|
|
132
109
|
async function main() {
|
|
133
110
|
const [, , cmd, ...rest] = process.argv;
|
|
134
111
|
|
|
135
|
-
let options;
|
|
136
112
|
try {
|
|
137
|
-
|
|
113
|
+
parseArgs(cmd ? [cmd, ...rest] : rest);
|
|
138
114
|
} catch (err) {
|
|
139
115
|
console.error(`Error: ${err.message}`);
|
|
140
116
|
console.error();
|
|
@@ -150,7 +126,7 @@ async function main() {
|
|
|
150
126
|
}
|
|
151
127
|
|
|
152
128
|
// Handle version
|
|
153
|
-
if (cmd === '
|
|
129
|
+
if (cmd === 'version') {
|
|
154
130
|
console.log(version);
|
|
155
131
|
process.exit(0);
|
|
156
132
|
}
|
|
@@ -158,7 +134,7 @@ async function main() {
|
|
|
158
134
|
// Handle push commands
|
|
159
135
|
if (cmd === 'push' || cmd === 'gitlab' || cmd === 'github') {
|
|
160
136
|
const remote = rest.find(arg => !arg.startsWith('-')) || 'origin';
|
|
161
|
-
await handlePushCommand(cmd,
|
|
137
|
+
await handlePushCommand(cmd, remote);
|
|
162
138
|
return;
|
|
163
139
|
}
|
|
164
140
|
|
|
@@ -181,7 +157,7 @@ async function main() {
|
|
|
181
157
|
|
|
182
158
|
// Default: show version info
|
|
183
159
|
const info = await processVersionInfo();
|
|
184
|
-
displayVersionInfo(info
|
|
160
|
+
displayVersionInfo(info);
|
|
185
161
|
}
|
|
186
162
|
|
|
187
163
|
process.on('unhandledRejection', (err) => {
|
package/src/utils.js
CHANGED
|
@@ -224,9 +224,57 @@ function parseConventionalCommit(message) {
|
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
/**
|
|
227
|
-
*
|
|
228
|
-
* @param {Array} commits -
|
|
229
|
-
* @returns {{latestVersion: string|null,
|
|
227
|
+
* Finds the highest semver version tag in a list of commits.
|
|
228
|
+
* @param {Array} commits - The commits to scan
|
|
229
|
+
* @returns {{latestVersion: string|null, taggedCommitHash: string|null}}
|
|
230
|
+
*/
|
|
231
|
+
function findLatestVersionTag(commits) {
|
|
232
|
+
if (!commits?.length) {
|
|
233
|
+
return { latestVersion: null, taggedCommitHash: null };
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let bestHash = null;
|
|
237
|
+
let bestVersion = null;
|
|
238
|
+
|
|
239
|
+
for (let i = 0; i < commits.length; i++) {
|
|
240
|
+
const semverTags = commits[i].tags?.filter(tag => SEMVER_PATTERN.test(tag));
|
|
241
|
+
if (!semverTags?.length) continue;
|
|
242
|
+
|
|
243
|
+
const highest = semverTags.sort((a, b) => {
|
|
244
|
+
const pa = parseVersion(a);
|
|
245
|
+
const pb = parseVersion(b);
|
|
246
|
+
if (pb.major !== pa.major) return pb.major - pa.major;
|
|
247
|
+
if (pb.minor !== pa.minor) return pb.minor - pa.minor;
|
|
248
|
+
return pb.patch - pa.patch;
|
|
249
|
+
})[0];
|
|
250
|
+
|
|
251
|
+
if (!bestVersion) {
|
|
252
|
+
bestVersion = highest;
|
|
253
|
+
bestHash = commits[i].hash;
|
|
254
|
+
} else {
|
|
255
|
+
const pBest = parseVersion(bestVersion);
|
|
256
|
+
const pCand = parseVersion(highest);
|
|
257
|
+
const isHigher =
|
|
258
|
+
pCand.major > pBest.major ||
|
|
259
|
+
(pCand.major === pBest.major && pCand.minor > pBest.minor) ||
|
|
260
|
+
(pCand.major === pBest.major && pCand.minor === pBest.minor && pCand.patch > pBest.patch);
|
|
261
|
+
if (isHigher) {
|
|
262
|
+
bestVersion = highest;
|
|
263
|
+
bestHash = commits[i].hash;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
return { latestVersion: bestVersion, taggedCommitHash: bestHash };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Expands commit info by finding the latest version tag and returning
|
|
273
|
+
* only commits since that tag. Uses positional slicing from a date-sorted
|
|
274
|
+
* commit list — for accurate results with merge commits, prefer using
|
|
275
|
+
* findLatestVersionTag + getCommitsSinceTag (as processVersionInfo does).
|
|
276
|
+
* @param {Array} commits - All commits (date-sorted, newest first)
|
|
277
|
+
* @returns {{latestVersion: string|null, commits: Array}}
|
|
230
278
|
*/
|
|
231
279
|
function expandCommitInfo(commits) {
|
|
232
280
|
if (!commits?.length) {
|
|
@@ -548,6 +596,45 @@ function getAllBranchCommits(branch) {
|
|
|
548
596
|
}
|
|
549
597
|
}
|
|
550
598
|
|
|
599
|
+
/**
|
|
600
|
+
* Retrieves commits reachable from the branch but not from the given tag,
|
|
601
|
+
* using git's native range selection so that date ordering cannot cause
|
|
602
|
+
* commits from merged branches to be missed.
|
|
603
|
+
* @param {string} tagHash - The commit hash of the version tag
|
|
604
|
+
* @param {string} branchSha - The resolved SHA of the branch tip
|
|
605
|
+
* @returns {Array<{hash: string, datetime: string, author: string, message: string, tags: Array<string>}>}
|
|
606
|
+
*/
|
|
607
|
+
function getCommitsSinceTag(tagHash, branchSha) {
|
|
608
|
+
const tagMap = buildTagMap();
|
|
609
|
+
const RS = '\x1E';
|
|
610
|
+
const COMMIT_SEP = `${RS}${RS}`;
|
|
611
|
+
|
|
612
|
+
try {
|
|
613
|
+
const logCmd = `git log --format=%H${RS}%ai${RS}%an${RS}%B${COMMIT_SEP} ${tagHash}..${branchSha}`;
|
|
614
|
+
const output = runWithOutput(logCmd).trim();
|
|
615
|
+
if (!output) return [];
|
|
616
|
+
|
|
617
|
+
return output
|
|
618
|
+
.split(COMMIT_SEP)
|
|
619
|
+
.filter(block => block.trim())
|
|
620
|
+
.map(block => {
|
|
621
|
+
const parts = block.split(RS);
|
|
622
|
+
if (parts.length < 4) return null;
|
|
623
|
+
const hash = parts[0].trim();
|
|
624
|
+
return {
|
|
625
|
+
hash,
|
|
626
|
+
datetime: parts[1].trim(),
|
|
627
|
+
author: parts[2].trim(),
|
|
628
|
+
message: parts.slice(3).join(RS).trim(),
|
|
629
|
+
tags: tagMap.get(hash) || [],
|
|
630
|
+
};
|
|
631
|
+
})
|
|
632
|
+
.filter(Boolean);
|
|
633
|
+
} catch {
|
|
634
|
+
return [];
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
|
|
551
638
|
/**
|
|
552
639
|
* Processes version information for the current branch.
|
|
553
640
|
* @returns {Promise<{currentVersion: string|null, newVersion: string|null, commits: Array, changelog: string}>}
|
|
@@ -556,12 +643,25 @@ async function processVersionInfo() {
|
|
|
556
643
|
ensureGitRepo();
|
|
557
644
|
const branch = getCurrentBranch();
|
|
558
645
|
fetchTags();
|
|
559
|
-
|
|
646
|
+
|
|
560
647
|
const allCommits = getAllBranchCommits(branch);
|
|
561
|
-
const
|
|
562
|
-
|
|
648
|
+
const { latestVersion, taggedCommitHash } = findLatestVersionTag(allCommits);
|
|
649
|
+
|
|
650
|
+
let commits;
|
|
651
|
+
if (taggedCommitHash) {
|
|
652
|
+
// Use git's range selection (tag..HEAD) to correctly identify all commits
|
|
653
|
+
// since the tag, regardless of commit date ordering. This fixes an issue
|
|
654
|
+
// where commits from merged feature branches could be missed because their
|
|
655
|
+
// dates are older than the tagged commit.
|
|
656
|
+
const branchSha = allCommits[0]?.hash;
|
|
657
|
+
commits = branchSha ? getCommitsSinceTag(taggedCommitHash, branchSha) : [];
|
|
658
|
+
} else {
|
|
659
|
+
commits = allCommits;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
const expandedInfo = { latestVersion, commits };
|
|
563
663
|
const { newVersion, changelog } = calculateNextVersionAndChangelog(expandedInfo);
|
|
564
|
-
|
|
664
|
+
|
|
565
665
|
return {
|
|
566
666
|
currentVersion: latestVersion,
|
|
567
667
|
newVersion,
|
|
@@ -577,6 +677,8 @@ module.exports = {
|
|
|
577
677
|
getCurrentBranch,
|
|
578
678
|
fetchTags,
|
|
579
679
|
getAllBranchCommits,
|
|
680
|
+
findLatestVersionTag,
|
|
681
|
+
getCommitsSinceTag,
|
|
580
682
|
expandCommitInfo,
|
|
581
683
|
calculateNextVersionAndChangelog,
|
|
582
684
|
processVersionInfo,
|