-
-
Notifications
You must be signed in to change notification settings - Fork 424
Expand file tree
/
Copy pathnext-version.ts
More file actions
108 lines (95 loc) · 3.02 KB
/
next-version.ts
File metadata and controls
108 lines (95 loc) · 3.02 KB
1
2
3
4
5
6
7
8
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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
/**
* Calculates the next semantic version based on conventional commits since the
* last reachable `v*` tag.
*
* Zero external dependencies — uses `child_process` to shell out to `git`.
*
* ### Imported as a module
*
* ```ts
* import { getNextVersion } from './scripts/next-version'
* const { current, next, from } = await getNextVersion()
* ```
*
* ### CLI usage (outputs JSON to stdout)
*
* ```sh
* node scripts/next-version.ts # { "current": "0.1.0", "next": "0.2.0", "from": "v0.1.0" }
* node scripts/next-version.ts --next # 0.2.0
* node scripts/next-version.ts --current # 0.1.0
* node scripts/next-version.ts --from # v0.1.0
* ```
*/
import { execFileSync } from 'node:child_process'
function git(...args: string[]): string {
return execFileSync('git', args, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim()
}
export interface VersionInfo {
/** The current version (from the latest tag), e.g. `"0.1.0"` */
current: string
/** The computed next version, e.g. `"0.2.0"` */
next: string
/** The git ref used as the starting point, e.g. `"v0.1.0"` or an initial commit SHA */
from: string
}
export async function getNextVersion(): Promise<VersionInfo> {
let current: string
let from: string
try {
const tag = git('describe', '--tags', '--abbrev=0', '--match', 'v*')
current = tag.replace(/^v/, '')
from = tag
} catch {
// No reachable tags — start from the initial commit
current = '0.0.0'
from = git('rev-list', '--max-parents=0', 'HEAD')
}
// Collect commit subjects since last tag (exclude merges)
let commits: string[]
try {
const log = git('log', `${from}..HEAD`, '--format=%s%n%b', '--no-merges')
commits = log ? log.split('\n') : []
} catch {
commits = []
}
let hasBreaking = false
let hasFeat = false
let hasFix = false
for (const line of commits) {
if (/BREAKING CHANGE|!:/.test(line)) hasBreaking = true
if (/^feat[:(]/.test(line)) hasFeat = true
if (/^fix[:(]/.test(line)) hasFix = true
}
const [major = 0, minor = 0, patch = 0] = current.split('.').map(Number)
let next: string
if (hasBreaking && major > 0) {
next = `${major + 1}.0.0`
} else if (hasFeat) {
next = `${major}.${minor + 1}.0`
} else if (hasFix || commits.length > 0) {
// Any non-empty diff bumps at least a patch
next = `${major}.${minor}.${patch + 1}`
} else {
// HEAD is exactly on the latest tag
next = current
}
return { current, next, from }
}
// --- CLI entry point ---
const isCLI =
process.argv[1] &&
(process.argv[1].endsWith('/next-version.ts') || process.argv[1].endsWith('/next-version'))
if (isCLI) {
const flag = process.argv[2]
getNextVersion()
.then(info => {
if (flag === '--next') console.log(info.next)
else if (flag === '--current') console.log(info.current)
else if (flag === '--from') console.log(info.from)
else console.log(JSON.stringify(info))
})
.catch(err => {
console.error(err)
process.exit(1)
})
}