galbe 0.15.3 → 0.15.4
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/package.json +3 -16
- package/scripts/release.ts +196 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "galbe",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.4",
|
|
4
4
|
"description": "Fast, lightweight and highly customizable JavaScript web framework based on Bun",
|
|
5
5
|
"author": "Pierre Caillaud M (https://github.com/pierre-cm)",
|
|
6
6
|
"type": "module",
|
|
@@ -35,12 +35,11 @@
|
|
|
35
35
|
"scripts": {
|
|
36
36
|
"test": "bun test",
|
|
37
37
|
"typecheck": "tsc --noEmit --emitDeclarationOnly false",
|
|
38
|
-
"release": "release
|
|
38
|
+
"release": "bun run scripts/release.ts"
|
|
39
39
|
},
|
|
40
40
|
"devDependencies": {
|
|
41
41
|
"@types/bun": "latest",
|
|
42
|
-
"openapi-types": "^12.1.3"
|
|
43
|
-
"release-it": "^17.1.1"
|
|
42
|
+
"openapi-types": "^12.1.3"
|
|
44
43
|
},
|
|
45
44
|
"peerDependencies": {
|
|
46
45
|
"typescript": "^5.0.0"
|
|
@@ -52,17 +51,5 @@
|
|
|
52
51
|
"acorn-walk": "^8.3.0",
|
|
53
52
|
"chokidar": "^3.6.0",
|
|
54
53
|
"commander": "^11.1.0"
|
|
55
|
-
},
|
|
56
|
-
"release-it": {
|
|
57
|
-
"git": {
|
|
58
|
-
"pushRepo": "git@github.com:pierre-cm/galbe.git"
|
|
59
|
-
},
|
|
60
|
-
"github": {
|
|
61
|
-
"requireBranch": "main",
|
|
62
|
-
"release": "true"
|
|
63
|
-
},
|
|
64
|
-
"npm": {
|
|
65
|
-
"publish": false
|
|
66
|
-
}
|
|
67
54
|
}
|
|
68
55
|
}
|
|
@@ -0,0 +1,196 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
|
|
3
|
+
import { $ } from 'bun'
|
|
4
|
+
import { existsSync } from 'fs'
|
|
5
|
+
|
|
6
|
+
const PKG_PATH = new URL('../package.json', import.meta.url).pathname
|
|
7
|
+
const CHANGELOG_PATH = new URL('../CHANGELOG.md', import.meta.url).pathname
|
|
8
|
+
const REMOTE = 'git@github.com:pierre-cm/galbe.git'
|
|
9
|
+
|
|
10
|
+
type BumpType = 'major' | 'minor' | 'patch'
|
|
11
|
+
type CommitKind = 'breaking' | 'feat' | 'fix'
|
|
12
|
+
|
|
13
|
+
interface Commit {
|
|
14
|
+
kind: CommitKind
|
|
15
|
+
scope?: string
|
|
16
|
+
message: string
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
// --- arg parsing ---
|
|
20
|
+
|
|
21
|
+
const argv = process.argv.slice(2)
|
|
22
|
+
const isCI = argv.includes('--ci')
|
|
23
|
+
const scopeIdx = argv.indexOf('--scope')
|
|
24
|
+
const scopeArg = scopeIdx !== -1 ? argv[scopeIdx + 1] : null
|
|
25
|
+
|
|
26
|
+
if (scopeArg && !['patch', 'minor', 'major'].includes(scopeArg)) {
|
|
27
|
+
console.error(`error: --scope must be patch, minor, or major`)
|
|
28
|
+
process.exit(1)
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// --- helpers ---
|
|
32
|
+
|
|
33
|
+
function classify(subject: string): Commit | null {
|
|
34
|
+
if (/^(Release \d|Merge branch)/.test(subject)) return null
|
|
35
|
+
|
|
36
|
+
if (subject.includes('BREAKING CHANGE') || /^[^:]+!:/.test(subject))
|
|
37
|
+
return { kind: 'breaking', message: subject.replace(/^[^:]+!?:\s*/, '') }
|
|
38
|
+
|
|
39
|
+
const feat = subject.match(/^feat(?:\(([^)]+)\))?:\s*(.+)/)
|
|
40
|
+
if (feat) return { kind: 'feat', scope: feat[1], message: feat[2] }
|
|
41
|
+
|
|
42
|
+
const fix = subject.match(/^fix(?:\(([^)]+)\))?:\s*(.+)/)
|
|
43
|
+
if (fix) return { kind: 'fix', scope: fix[1], message: fix[2] }
|
|
44
|
+
|
|
45
|
+
return null
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function bump(version: string, type: BumpType): string {
|
|
49
|
+
const [maj, min, pat] = version.split('.').map(Number)
|
|
50
|
+
if (type === 'major') return `${maj + 1}.0.0`
|
|
51
|
+
if (type === 'minor') return `${maj}.${min + 1}.0`
|
|
52
|
+
return `${maj}.${min}.${pat + 1}`
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function suggest(commits: Commit[], preRelease: boolean): BumpType {
|
|
56
|
+
if (!preRelease && commits.some(c => c.kind === 'breaking')) return 'major'
|
|
57
|
+
if (commits.some(c => c.kind === 'breaking') || commits.some(c => c.kind === 'feat')) return 'minor'
|
|
58
|
+
return 'patch'
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function formatEntry(version: string, commits: Commit[], date: string): string {
|
|
62
|
+
const sections: string[] = []
|
|
63
|
+
const fmt = (c: Commit) => `- ${c.scope ? `**${c.scope}**: ` : ''}${c.message}`
|
|
64
|
+
|
|
65
|
+
const breaking = commits.filter(c => c.kind === 'breaking')
|
|
66
|
+
const feats = commits.filter(c => c.kind === 'feat')
|
|
67
|
+
const fixes = commits.filter(c => c.kind === 'fix')
|
|
68
|
+
|
|
69
|
+
if (breaking.length) sections.push(`### Breaking Changes\n${breaking.map(fmt).join('\n')}`)
|
|
70
|
+
if (feats.length) sections.push(`### Features\n${feats.map(fmt).join('\n')}`)
|
|
71
|
+
if (fixes.length) sections.push(`### Fixes\n${fixes.map(fmt).join('\n')}`)
|
|
72
|
+
|
|
73
|
+
return `## ${version} — ${date}\n\n${sections.join('\n\n')}`
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function prependChangelog(entry: string, existing: string): string {
|
|
77
|
+
if (!existing) return `# Changelog\n\n${entry}\n`
|
|
78
|
+
if (existing.startsWith('# Changelog')) {
|
|
79
|
+
const body = existing.replace(/^# Changelog\s*/, '')
|
|
80
|
+
return `# Changelog\n\n${entry}\n\n${body}`
|
|
81
|
+
}
|
|
82
|
+
return `# Changelog\n\n${entry}\n\n${existing}`
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// --- main ---
|
|
86
|
+
|
|
87
|
+
async function main() {
|
|
88
|
+
if (!isCI) {
|
|
89
|
+
const branch = (await $`git rev-parse --abbrev-ref HEAD`.quiet().text()).trim()
|
|
90
|
+
if (branch !== 'main') {
|
|
91
|
+
console.error(`error: must be on main branch (currently on '${branch}')`)
|
|
92
|
+
process.exit(1)
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const dirty = (await $`git status --porcelain`.quiet().text()).trim()
|
|
96
|
+
if (dirty) {
|
|
97
|
+
console.error('error: working tree has uncommitted changes')
|
|
98
|
+
process.exit(1)
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const lastTag = await $`git describe --tags --abbrev=0`
|
|
103
|
+
.quiet()
|
|
104
|
+
.text()
|
|
105
|
+
.catch(() => '')
|
|
106
|
+
.then(t => t.trim())
|
|
107
|
+
|
|
108
|
+
const logRange = lastTag ? `${lastTag}..HEAD` : 'HEAD'
|
|
109
|
+
const rawLog = (await $`git log --pretty=format:%s ${logRange}`.quiet().text()).trim()
|
|
110
|
+
|
|
111
|
+
const commits = rawLog ? (rawLog.split('\n').map(classify).filter(Boolean) as Commit[]) : []
|
|
112
|
+
|
|
113
|
+
const pkg = await Bun.file(PKG_PATH).json()
|
|
114
|
+
const current: string = pkg.version
|
|
115
|
+
const preRelease = current.startsWith('0.')
|
|
116
|
+
|
|
117
|
+
console.log(`\ncurrent: ${current}${lastTag ? ` (since tag ${lastTag})` : ''}`)
|
|
118
|
+
|
|
119
|
+
if (!commits.length && !scopeArg) {
|
|
120
|
+
console.log('no feat/fix commits since last release — nothing to do.')
|
|
121
|
+
process.exit(0)
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let next: string
|
|
125
|
+
|
|
126
|
+
if (scopeArg) {
|
|
127
|
+
// CI / non-interactive: scope is given directly
|
|
128
|
+
next = bump(current, scopeArg as BumpType)
|
|
129
|
+
console.log(`scope: ${scopeArg} → ${next}`)
|
|
130
|
+
} else {
|
|
131
|
+
// Interactive
|
|
132
|
+
const breaking = commits.filter(c => c.kind === 'breaking')
|
|
133
|
+
const feats = commits.filter(c => c.kind === 'feat')
|
|
134
|
+
const fixes = commits.filter(c => c.kind === 'fix')
|
|
135
|
+
|
|
136
|
+
if (breaking.length) { console.log('\n breaking:'); breaking.forEach(c => console.log(` - ${c.message}`)) }
|
|
137
|
+
if (feats.length) { console.log('\n features:'); feats.forEach(c => console.log(` - ${c.scope ? `[${c.scope}] ` : ''}${c.message}`)) }
|
|
138
|
+
if (fixes.length) { console.log('\n fixes:'); fixes.forEach(c => console.log(` - ${c.scope ? `[${c.scope}] ` : ''}${c.message}`)) }
|
|
139
|
+
|
|
140
|
+
const recommended = suggest(commits, preRelease)
|
|
141
|
+
const versions = { patch: bump(current, 'patch'), minor: bump(current, 'minor'), major: bump(current, 'major') }
|
|
142
|
+
const defaultChoice = recommended === 'patch' ? '1' : recommended === 'minor' ? '2' : '3'
|
|
143
|
+
|
|
144
|
+
console.log(`\n 1) patch → ${versions.patch}${recommended === 'patch' ? ' ←' : ''}`)
|
|
145
|
+
console.log(` 2) minor → ${versions.minor}${recommended === 'minor' ? ' ←' : ''}`)
|
|
146
|
+
console.log(` 3) major → ${versions.major}${recommended === 'major' ? ' ←' : ''}`)
|
|
147
|
+
console.log(` 4) custom`)
|
|
148
|
+
if (preRelease) console.log(`\n note: ${current} is pre-release (0.x) — breaking changes capped to minor`)
|
|
149
|
+
|
|
150
|
+
const choice = prompt(`\nbump type [${defaultChoice}]:`)?.trim() || defaultChoice
|
|
151
|
+
|
|
152
|
+
if (choice === '1') next = versions.patch
|
|
153
|
+
else if (choice === '2') next = versions.minor
|
|
154
|
+
else if (choice === '3') next = versions.major
|
|
155
|
+
else if (choice === '4') {
|
|
156
|
+
next = prompt('version:')?.trim() ?? ''
|
|
157
|
+
if (!/^\d+\.\d+\.\d+$/.test(next)) {
|
|
158
|
+
console.error('error: invalid semver format')
|
|
159
|
+
process.exit(1)
|
|
160
|
+
}
|
|
161
|
+
} else {
|
|
162
|
+
console.error('error: invalid choice')
|
|
163
|
+
process.exit(1)
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
const ok = prompt(`\nrelease ${next}? (y/N):`)?.trim().toLowerCase()
|
|
167
|
+
if (ok !== 'y') { console.log('aborted.'); process.exit(0) }
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
console.log()
|
|
171
|
+
|
|
172
|
+
const date = new Date().toISOString().split('T')[0]
|
|
173
|
+
const entry = formatEntry(next, commits, date)
|
|
174
|
+
|
|
175
|
+
const existing = existsSync(CHANGELOG_PATH) ? await Bun.file(CHANGELOG_PATH).text() : ''
|
|
176
|
+
await Bun.write(CHANGELOG_PATH, prependChangelog(entry, existing))
|
|
177
|
+
console.log('updated CHANGELOG.md')
|
|
178
|
+
|
|
179
|
+
pkg.version = next
|
|
180
|
+
await Bun.write(PKG_PATH, JSON.stringify(pkg, null, 2) + '\n')
|
|
181
|
+
console.log(`bumped package.json → ${next}`)
|
|
182
|
+
|
|
183
|
+
await $`git add package.json CHANGELOG.md`
|
|
184
|
+
await $`git commit -m "Release ${next}"`
|
|
185
|
+
await $`git tag ${next}`
|
|
186
|
+
await $`git push ${REMOTE} main --tags`
|
|
187
|
+
console.log(`pushed commit and tag ${next}`)
|
|
188
|
+
|
|
189
|
+
await $`gh release create ${next} --title "Release ${next}" --notes ${entry}`
|
|
190
|
+
console.log(`\ncreated GitHub release ${next}`)
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
main().catch(e => {
|
|
194
|
+
console.error(e.message ?? e)
|
|
195
|
+
process.exit(1)
|
|
196
|
+
})
|