release-suite 0.1.0 โ 1.0.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/.github/workflows/create-release-pr.yml +30 -17
- package/.github/workflows/publish-on-merge.yml +26 -9
- package/CHANGELOG.md +38 -5
- package/README.md +195 -183
- package/bin/compute-version.js +173 -132
- package/bin/create-tag.js +26 -14
- package/bin/generate-changelog.js +213 -192
- package/bin/generate-release-notes.js +135 -123
- package/bin/preview.js +47 -47
- package/docs/api.md +28 -0
- package/docs/ci.md +210 -0
- package/docs/compute-version.md +204 -0
- package/eslint.config.js +89 -47
- package/lib/git.js +73 -0
- package/lib/utils.js +45 -0
- package/lib/versioning.js +110 -0
- package/package.json +62 -62
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
# ๐ฆ computeVersion
|
|
2
|
+
|
|
3
|
+
`computeVersion()` is the core engine of **Release Suite**. It is responsible for analyzing Git history and determining whether a new semantic version should be released.
|
|
4
|
+
|
|
5
|
+
This document defines its **official, immutable contract**, behavior, limitations, and CLI integration rules.
|
|
6
|
+
|
|
7
|
+
---
|
|
8
|
+
|
|
9
|
+
## ๐ฏ Purpose
|
|
10
|
+
|
|
11
|
+
- Analyze Git commits since the last release
|
|
12
|
+
- Detect semantic version bumps (`major`, `minor`, `patch`)
|
|
13
|
+
- Decide **if** a release should happen
|
|
14
|
+
- Provide deterministic, machine-readable output
|
|
15
|
+
|
|
16
|
+
`computeVersion()` **never mutates files**, **never prints logs**, and **never exits the process**.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## ๐ง Programmatic API
|
|
21
|
+
|
|
22
|
+
### Signature
|
|
23
|
+
|
|
24
|
+
```ts
|
|
25
|
+
computeVersion(options?: {
|
|
26
|
+
cwd?: string;
|
|
27
|
+
}): ComputeVersionResult
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
### Options
|
|
31
|
+
|
|
32
|
+
| Option | Description |
|
|
33
|
+
| ------ | ---------------------------------------------------------------------------------------- |
|
|
34
|
+
| `cwd` | Working directory where Git and `package.json` are resolved. Defaults to `process.cwd()` |
|
|
35
|
+
|
|
36
|
+
---
|
|
37
|
+
|
|
38
|
+
## ๐ Official Return Contract (Frozen)
|
|
39
|
+
|
|
40
|
+
### Type Definition
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
type ComputeVersionResult =
|
|
44
|
+
| {
|
|
45
|
+
hasRelease: true;
|
|
46
|
+
baseVersion: string;
|
|
47
|
+
nextVersion: string;
|
|
48
|
+
bump: "major" | "minor" | "patch";
|
|
49
|
+
commitsAnalyzed: number;
|
|
50
|
+
}
|
|
51
|
+
| {
|
|
52
|
+
hasRelease: false;
|
|
53
|
+
baseVersion: string;
|
|
54
|
+
reason: "no-bump-detected" | "no-commits";
|
|
55
|
+
commitsAnalyzed: number;
|
|
56
|
+
};
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
---
|
|
60
|
+
|
|
61
|
+
## ๐ข Release Detected
|
|
62
|
+
|
|
63
|
+
Returned when at least one commit implies a semantic bump.
|
|
64
|
+
|
|
65
|
+
Example:
|
|
66
|
+
|
|
67
|
+
```json
|
|
68
|
+
{
|
|
69
|
+
"hasRelease": true,
|
|
70
|
+
"baseVersion": "1.4.2",
|
|
71
|
+
"nextVersion": "1.5.0",
|
|
72
|
+
"bump": "minor",
|
|
73
|
+
"commitsAnalyzed": 8
|
|
74
|
+
}
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
---
|
|
78
|
+
|
|
79
|
+
## ๐ก No Release Detected
|
|
80
|
+
|
|
81
|
+
### No commits since last release
|
|
82
|
+
|
|
83
|
+
```json
|
|
84
|
+
{
|
|
85
|
+
"hasRelease": false,
|
|
86
|
+
"reason": "no-commits",
|
|
87
|
+
"baseVersion": "1.4.2",
|
|
88
|
+
"commitsAnalyzed": 0
|
|
89
|
+
}
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### Commits found, but no semantic bump
|
|
93
|
+
|
|
94
|
+
```json
|
|
95
|
+
{
|
|
96
|
+
"hasRelease": false,
|
|
97
|
+
"reason": "no-bump-detected",
|
|
98
|
+
"baseVersion": "1.4.2",
|
|
99
|
+
"commitsAnalyzed": 5
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## ๐งช Semantic Bump Rules
|
|
106
|
+
|
|
107
|
+
The highest bump found **wins**:
|
|
108
|
+
|
|
109
|
+
| Commit Type | Bump |
|
|
110
|
+
| -------------------------- | ------- |
|
|
111
|
+
| `feat!`, `BREAKING CHANGE` | `major` |
|
|
112
|
+
| `feat` | `minor` |
|
|
113
|
+
| `fix`, `perf`, `refactor` | `patch` |
|
|
114
|
+
|
|
115
|
+
Custom prefixes and emojis are supported as long as they resolve to these semantic meanings.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## ๐ Squash & Merge Strategy
|
|
120
|
+
|
|
121
|
+
`computeVersion()` works in **both**:
|
|
122
|
+
|
|
123
|
+
- Full commit history (merge commits)
|
|
124
|
+
- Squash & merge workflows
|
|
125
|
+
|
|
126
|
+
### โ ๏ธ Important Recommendation
|
|
127
|
+
|
|
128
|
+
If your repository uses **Squash & Merge**, configure GitHub to:
|
|
129
|
+
|
|
130
|
+
> **โUse Pull request title and commit detailsโ**
|
|
131
|
+
|
|
132
|
+
And enforce **Conventional Commits** in PR titles:
|
|
133
|
+
|
|
134
|
+
```text
|
|
135
|
+
fix: normalize path resolution
|
|
136
|
+
feat!: drop legacy API support
|
|
137
|
+
```
|
|
138
|
+
|
|
139
|
+
This ensures `computeVersion()` can reliably detect semantic intent.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
## ๐ฅ CLI Integration
|
|
144
|
+
|
|
145
|
+
The CLI wrapper (`rs-compute-version`) is a thin layer on top of `computeVersion()`.
|
|
146
|
+
|
|
147
|
+
### Flags
|
|
148
|
+
|
|
149
|
+
| Flag | Description |
|
|
150
|
+
| ----------- | ---------------------------------------- |
|
|
151
|
+
| `--json` | Outputs the full result as JSON |
|
|
152
|
+
| `--ci` | Enables CI-friendly logging (future use) |
|
|
153
|
+
| `--preview` | Semantic alias (no behavior change) |
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## ๐ฆ CLI Exit Codes (Contract)
|
|
158
|
+
|
|
159
|
+
| Exit Code | Meaning |
|
|
160
|
+
| --------- | ----------------------------- |
|
|
161
|
+
| `0` | Release generated |
|
|
162
|
+
| `10` | No bump detected |
|
|
163
|
+
| `2` | No commits since last release |
|
|
164
|
+
| `1` | Unexpected error |
|
|
165
|
+
|
|
166
|
+
> CI pipelines **must** rely on exit codes, not stdout parsing.
|
|
167
|
+
|
|
168
|
+
---
|
|
169
|
+
|
|
170
|
+
## ๐ซ Explicit Non-Goals
|
|
171
|
+
|
|
172
|
+
`computeVersion()` does **not**:
|
|
173
|
+
|
|
174
|
+
- Modify `package.json`
|
|
175
|
+
- Create Git tags
|
|
176
|
+
- Generate changelogs
|
|
177
|
+
- Access GitHub APIs
|
|
178
|
+
- Enforce commit conventions
|
|
179
|
+
|
|
180
|
+
These responsibilities belong to other tools in Release Suite.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## ๐ง Contract Stability
|
|
185
|
+
|
|
186
|
+
This contract is considered **stable and frozen**.
|
|
187
|
+
|
|
188
|
+
Any breaking change requires:
|
|
189
|
+
|
|
190
|
+
- Major version bump of `release-suite`
|
|
191
|
+
- Explicit migration notes
|
|
192
|
+
- CI-safe transition plan
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## โ
Summary
|
|
197
|
+
|
|
198
|
+
- Deterministic
|
|
199
|
+
- Side-effect free
|
|
200
|
+
- CI-safe
|
|
201
|
+
- Fully testable
|
|
202
|
+
- Explicit failure modes
|
|
203
|
+
|
|
204
|
+
`computeVersion()` is designed to be boring โ and reliable.
|
package/eslint.config.js
CHANGED
|
@@ -1,47 +1,89 @@
|
|
|
1
|
-
// eslint.config.js
|
|
2
|
-
import js from "@eslint/js";
|
|
3
|
-
import prettierConfig from "eslint-config-prettier";
|
|
4
|
-
import pluginImport from "eslint-plugin-import";
|
|
5
|
-
import globals from "globals";
|
|
6
|
-
|
|
7
|
-
export default [
|
|
8
|
-
js.configs.recommended,
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"
|
|
36
|
-
"
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
},
|
|
47
|
-
|
|
1
|
+
// eslint.config.js
|
|
2
|
+
import js from "@eslint/js";
|
|
3
|
+
import prettierConfig from "eslint-config-prettier";
|
|
4
|
+
import pluginImport from "eslint-plugin-import";
|
|
5
|
+
import globals from "globals";
|
|
6
|
+
|
|
7
|
+
export default [
|
|
8
|
+
js.configs.recommended,
|
|
9
|
+
|
|
10
|
+
// =====================
|
|
11
|
+
// BIN (CLI)
|
|
12
|
+
// =====================
|
|
13
|
+
{
|
|
14
|
+
files: ["bin/**/*.js"],
|
|
15
|
+
plugins: {
|
|
16
|
+
import: pluginImport,
|
|
17
|
+
},
|
|
18
|
+
languageOptions: {
|
|
19
|
+
ecmaVersion: "latest",
|
|
20
|
+
sourceType: "module",
|
|
21
|
+
globals: {
|
|
22
|
+
...globals.node,
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
rules: {
|
|
26
|
+
"no-console": "off",
|
|
27
|
+
"no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
|
|
28
|
+
"prefer-const": "warn",
|
|
29
|
+
eqeqeq: ["error", "always"],
|
|
30
|
+
curly: ["error", "all"],
|
|
31
|
+
"import/order": [
|
|
32
|
+
"warn",
|
|
33
|
+
{
|
|
34
|
+
groups: [
|
|
35
|
+
"builtin",
|
|
36
|
+
"external",
|
|
37
|
+
"internal",
|
|
38
|
+
"parent",
|
|
39
|
+
"sibling",
|
|
40
|
+
"index",
|
|
41
|
+
],
|
|
42
|
+
alphabetize: { order: "asc", caseInsensitive: true },
|
|
43
|
+
},
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
// =====================
|
|
49
|
+
// LIB (reusable code)
|
|
50
|
+
// =====================
|
|
51
|
+
{
|
|
52
|
+
files: ["lib/**/*.js"],
|
|
53
|
+
plugins: {
|
|
54
|
+
import: pluginImport,
|
|
55
|
+
},
|
|
56
|
+
languageOptions: {
|
|
57
|
+
ecmaVersion: "latest",
|
|
58
|
+
sourceType: "module",
|
|
59
|
+
globals: {
|
|
60
|
+
...globals.node,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
rules: {
|
|
64
|
+
"no-console": "warn",
|
|
65
|
+
"no-unused-vars": ["warn", { argsIgnorePattern: "^_" }],
|
|
66
|
+
"prefer-const": "warn",
|
|
67
|
+
eqeqeq: ["error", "always"],
|
|
68
|
+
curly: ["error", "all"],
|
|
69
|
+
"import/order": [
|
|
70
|
+
"warn",
|
|
71
|
+
{
|
|
72
|
+
groups: [
|
|
73
|
+
"builtin",
|
|
74
|
+
"external",
|
|
75
|
+
"internal",
|
|
76
|
+
"parent",
|
|
77
|
+
"sibling",
|
|
78
|
+
"index",
|
|
79
|
+
],
|
|
80
|
+
alphabetize: { order: "asc", caseInsensitive: true },
|
|
81
|
+
},
|
|
82
|
+
],
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
prettierConfig,
|
|
86
|
+
{
|
|
87
|
+
ignores: ["dist/**/*.js"],
|
|
88
|
+
},
|
|
89
|
+
];
|
package/lib/git.js
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { run } from "./utils.js";
|
|
2
|
+
|
|
3
|
+
/* ===========================
|
|
4
|
+
* Git helpers
|
|
5
|
+
* =========================== */
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Return the most recent Git tag reachable from HEAD, with a leading "v" prefix removed.
|
|
9
|
+
*
|
|
10
|
+
* Runs `git describe --tags --abbrev=0` in the provided working directory and strips a
|
|
11
|
+
* single leading "v" from the tag name (e.g. "v1.2.3" -> "1.2.3").
|
|
12
|
+
*
|
|
13
|
+
* @param {string} cwd - The working directory in which to run the Git command.
|
|
14
|
+
* @returns {string|null} The most recent tag without a leading "v", or null if no tag is found
|
|
15
|
+
* or the Git command fails.
|
|
16
|
+
*/
|
|
17
|
+
export function getLastTag(cwd) {
|
|
18
|
+
try {
|
|
19
|
+
const tag = run("git describe --tags --abbrev=0", cwd);
|
|
20
|
+
return tag.replace(/^v/, "");
|
|
21
|
+
} catch {
|
|
22
|
+
return null;
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Retrieve commits from Git using a compact, machine-friendly format.
|
|
28
|
+
*
|
|
29
|
+
* Executes `git log <range> --pretty=format:%H%x1f%s%x1f%b` in the given working directory,
|
|
30
|
+
* splits the output by newline, and returns an array of non-empty lines.
|
|
31
|
+
*
|
|
32
|
+
* Each array element is a single string formatted as:
|
|
33
|
+
* "<commit-hash>\x1F<subject>\x1F<body>"
|
|
34
|
+
* where "\x1F" is the ASCII unit separator (0x1F) used to delimit fields.
|
|
35
|
+
*
|
|
36
|
+
* If the git command fails (e.g., not a repository, invalid range, or other error),
|
|
37
|
+
* an empty array is returned.
|
|
38
|
+
*
|
|
39
|
+
* @param {string} range - The git log range to query (e.g. "HEAD", "v1.0.0..HEAD", "master..feature").
|
|
40
|
+
* @param {string} [cwd] - Optional working directory path in which to run the git command.
|
|
41
|
+
* @returns {string[]} Array of commit entries, each as "<hash>\x1F<subject>\x1F<body>". Empty array on failure.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* // Possible return:
|
|
45
|
+
* // ["a1b2c3d4e5f6g7h8i9j0\u001FAdd feature X\u001FImplementation details...", ...]
|
|
46
|
+
*/
|
|
47
|
+
export function getCommits(range, cwd) {
|
|
48
|
+
try {
|
|
49
|
+
return run(
|
|
50
|
+
`git log ${range} --pretty=format:%H%x1f%s%x1f%b`,
|
|
51
|
+
cwd
|
|
52
|
+
)
|
|
53
|
+
.split("\n")
|
|
54
|
+
.filter(Boolean);
|
|
55
|
+
} catch {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Parse a commit line encoded with ASCII Unit Separator characters into its parts.
|
|
62
|
+
*
|
|
63
|
+
* The input is expected to contain fields separated by '\x1f' in the order:
|
|
64
|
+
* hash, subject, body. If subject or body are missing, they default to an empty string.
|
|
65
|
+
*
|
|
66
|
+
* @param {string} line - Raw commit line with fields delimited by '\x1f'.
|
|
67
|
+
* @returns {{hash: string, subject: string, body: string}} An object containing the
|
|
68
|
+
* commit hash, subject, and body.
|
|
69
|
+
*/
|
|
70
|
+
export function parseCommit(line) {
|
|
71
|
+
const [hash, subject = "", body = ""] = line.split("\x1f");
|
|
72
|
+
return { hash, subject, body };
|
|
73
|
+
}
|
package/lib/utils.js
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { execSync } from "node:child_process";
|
|
2
|
+
import fs from "node:fs";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
|
|
5
|
+
/* ===========================
|
|
6
|
+
* Utilities
|
|
7
|
+
* =========================== */
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Execute a shell command synchronously and return its stdout as a trimmed UTF-8 string.
|
|
11
|
+
*
|
|
12
|
+
* @param {string} cmd - The command to execute (passed to child_process.execSync).
|
|
13
|
+
* @param {string} [cwd] - Optional working directory in which to run the command.
|
|
14
|
+
* @returns {string} The command's stdout, decoded as UTF-8 and trimmed of surrounding whitespace.
|
|
15
|
+
* @throws {Error|import('child_process').ExecSyncError} If the command fails or exits with a non-zero status.
|
|
16
|
+
* @see {@link https://nodejs.org/api/child_process.html#child_processexecsynccommand-options|child_process.execSync}
|
|
17
|
+
*/
|
|
18
|
+
export function run(cmd, cwd) {
|
|
19
|
+
return execSync(cmd, { encoding: "utf8", cwd }).trim();
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Read the "version" field from a package.json file in the given directory.
|
|
24
|
+
*
|
|
25
|
+
* Synchronously reads and parses `<cwd>/package.json` and returns its `version`.
|
|
26
|
+
* Any errors (missing file, invalid JSON, missing `version`, etc.) are caught
|
|
27
|
+
* and a default version string of `"0.0.0"` is returned.
|
|
28
|
+
*
|
|
29
|
+
* @param {string} cwd - Path to the directory containing package.json.
|
|
30
|
+
* @returns {string} The package version, or `"0.0.0"` if it cannot be read.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* // returns "1.2.3" if /my/project/package.json contains { "version": "1.2.3" }
|
|
34
|
+
* const v = readPackageVersion('/my/project');
|
|
35
|
+
*/
|
|
36
|
+
export function readPackageVersion(cwd) {
|
|
37
|
+
try {
|
|
38
|
+
const pkg = JSON.parse(
|
|
39
|
+
fs.readFileSync(path.join(cwd, "package.json"), "utf8")
|
|
40
|
+
);
|
|
41
|
+
return pkg.version;
|
|
42
|
+
} catch {
|
|
43
|
+
return "0.0.0";
|
|
44
|
+
}
|
|
45
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/* ===========================
|
|
2
|
+
* Semver detection
|
|
3
|
+
* =========================== */
|
|
4
|
+
|
|
5
|
+
const COMMIT_RE =
|
|
6
|
+
/^(feat|fix|refactor|docs|chore|style|test|build|perf|ci|cleanup|remove)(\(.+\))?(!)?:/i;
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Normalize a subject string by removing leading emoji and trimming whitespace.
|
|
10
|
+
*
|
|
11
|
+
* This function:
|
|
12
|
+
* - Strips a leading colon-style emoji shortcode (e.g. ":smile:"). Consecutive shortcodes
|
|
13
|
+
* without intervening spaces (e.g. ":a::b: ...") are removed as a single leading block.
|
|
14
|
+
* - Removes leading Unicode emoji in the U+1F300โU+1FAFF range (one or more), using a
|
|
15
|
+
* Unicode-aware match.
|
|
16
|
+
* - Trims surrounding whitespace from the resulting string.
|
|
17
|
+
*
|
|
18
|
+
* @param {string} subject - The input subject (e.g. a commit/PR subject).
|
|
19
|
+
* @returns {string} The normalized subject with any leading emoji/shortcodes removed and trimmed.
|
|
20
|
+
*
|
|
21
|
+
* @example
|
|
22
|
+
* normalizeSubject(':sparkles: Add new feature') // 'Add new feature'
|
|
23
|
+
* @example
|
|
24
|
+
* normalizeSubject('๐โจ Deploy') // 'Deploy'
|
|
25
|
+
* @example
|
|
26
|
+
* normalizeSubject(' :a::b:Multiple emojis at start ') // 'Multiple emojis at start'
|
|
27
|
+
*/
|
|
28
|
+
function normalizeSubject(subject) {
|
|
29
|
+
return subject
|
|
30
|
+
// remove emoji at start
|
|
31
|
+
.replace(/^:\S+:\s*/, "")
|
|
32
|
+
// remove unicode emoji at start
|
|
33
|
+
.replace(/^[\u{1F300}-\u{1FAFF}]+\s*/u, "")
|
|
34
|
+
.trim();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Determine the semantic version bump implied by a commit message.
|
|
39
|
+
*
|
|
40
|
+
* The function inspects the commit subject and body using a conventional-commit
|
|
41
|
+
* pattern (COMMIT_RE) and the presence of "BREAKING CHANGE":
|
|
42
|
+
* - Returns "major" if the body contains "BREAKING CHANGE" (case-insensitive)
|
|
43
|
+
* or the commit header contains the conventional "!" breaking-change marker.
|
|
44
|
+
* - Returns "minor" if the header matches COMMIT_RE and the commit type is "feat".
|
|
45
|
+
* - Returns "patch" if the header matches COMMIT_RE and the commit type is "fix".
|
|
46
|
+
* - Returns "none" if no relevant indicators are present.
|
|
47
|
+
*
|
|
48
|
+
* @param {Object} params - Destructured input object.
|
|
49
|
+
* @param {string} params.subject - Commit subject/summary line to be matched against COMMIT_RE.
|
|
50
|
+
* @param {string} params.body - Commit body text used to detect "BREAKING CHANGE".
|
|
51
|
+
* @returns {'major'|'minor'|'patch'|'none'} The semantic version bump type.
|
|
52
|
+
*/
|
|
53
|
+
export function detectBumpType({ subject, body }) {
|
|
54
|
+
const cleanSubject = normalizeSubject(subject);
|
|
55
|
+
|
|
56
|
+
// Ignore revert commits entirely
|
|
57
|
+
if (/^revert\b/i.test(cleanSubject)) return "none";
|
|
58
|
+
|
|
59
|
+
const match = cleanSubject.match(COMMIT_RE);
|
|
60
|
+
|
|
61
|
+
const breaking =
|
|
62
|
+
/BREAKING CHANGE/i.test(body) || (match && match[3] === "!");
|
|
63
|
+
|
|
64
|
+
if (breaking) return "major";
|
|
65
|
+
if (!match) return "none";
|
|
66
|
+
|
|
67
|
+
const type = match[1].toLowerCase();
|
|
68
|
+
if (type === "feat") return "minor";
|
|
69
|
+
if (type === "fix") return "patch";
|
|
70
|
+
|
|
71
|
+
return "none";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Increment a semantic version string.
|
|
76
|
+
*
|
|
77
|
+
* Given a version string in the form "major.minor.patch", this function
|
|
78
|
+
* parses the numeric components (non-numeric or missing parts are treated as 0)
|
|
79
|
+
* and returns a new version string with the requested part bumped:
|
|
80
|
+
* - "major": increments major, resets minor and patch to 0
|
|
81
|
+
* - "minor": increments minor, resets patch to 0
|
|
82
|
+
* - any other value (including omitted): increments patch
|
|
83
|
+
*
|
|
84
|
+
* Parsing details:
|
|
85
|
+
* - Each segment is parsed with parseInt(..., 10); if parsing yields NaN,
|
|
86
|
+
* that segment is treated as 0.
|
|
87
|
+
*
|
|
88
|
+
* @param {string} base - The base version string (e.g. "1.2.3").
|
|
89
|
+
* @param {string} [bump] - The part to bump: "major", "minor", or "patch".
|
|
90
|
+
* If omitted or any other value, the patch is bumped.
|
|
91
|
+
* @returns {string} The new version string in "major.minor.patch" format.
|
|
92
|
+
*
|
|
93
|
+
* @example
|
|
94
|
+
* bumpVersion("1.2.3", "patch"); // => "1.2.4"
|
|
95
|
+
* @example
|
|
96
|
+
* bumpVersion("1.2.3", "minor"); // => "1.3.0"
|
|
97
|
+
* @example
|
|
98
|
+
* bumpVersion("1.2.3", "major"); // => "2.0.0"
|
|
99
|
+
* @example
|
|
100
|
+
* bumpVersion("1", "patch"); // => "1.0.1" (missing parts treated as 0)
|
|
101
|
+
* @example
|
|
102
|
+
* bumpVersion("a.b.c", "minor"); // => "0.1.0" (non-numeric parts treated as 0)
|
|
103
|
+
*/
|
|
104
|
+
export function bumpVersion(base, bump) {
|
|
105
|
+
const [major, minor, patch] = base.split(".").map(n => parseInt(n, 10) || 0);
|
|
106
|
+
|
|
107
|
+
if (bump === "major") return `${major + 1}.0.0`;
|
|
108
|
+
if (bump === "minor") return `${major}.${minor + 1}.0`;
|
|
109
|
+
return `${major}.${minor}.${patch + 1}`;
|
|
110
|
+
}
|