Skip to content

Commit 3228ebf

Browse files
committed
fix: add bump-version script
Assisted-by: ClaudeCode:claude-sonnet-5 Signed-off-by: Maksim Sukharev <antreesy.web@gmail.com>
1 parent 8ace673 commit 3228ebf

2 files changed

Lines changed: 258 additions & 0 deletions

File tree

Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,9 @@ validate-release:
2626
prepare-changelog:
2727
node ./scripts/prepare-changelog.mjs $(BRANCHES)
2828

29+
bump-version:
30+
node ./scripts/bump-version.mjs $(PR)
31+
2932
release: appstore create-tag
3033

3134
build-dev: composer-install-dev build-js

scripts/bump-version.mjs

Lines changed: 255 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,255 @@
1+
/*
2+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
3+
* SPDX-License-Identifier: AGPL-3.0-or-later
4+
*/
5+
6+
/**
7+
* Check out a backport changelog PR (or an existing branch), bump the
8+
* version in appinfo/info.xml and package.json, then commit the result.
9+
*
10+
* Requires: git, npm.
11+
*
12+
* Usage:
13+
* node scripts/bump-version.mjs <changelog-pr-number|branch-name>
14+
*
15+
* Arguments:
16+
* <changelog-pr-number> GitHub PR number of the backported changelog PR
17+
* <branch-name> An existing branch to bump directly. Checked out
18+
* locally if present, otherwise fetched and tracked
19+
* from origin. Pulled either way.
20+
*
21+
* Options:
22+
* -h, --help Show this help
23+
*/
24+
25+
import { existsSync, readFileSync, writeFileSync } from 'node:fs'
26+
import process from 'node:process'
27+
import { branchExists, ghFetch, parseArgs, preflight, print, requireCleanWorktree, run } from './cli-utils.mjs'
28+
import { getCurrentBranch, incrementVersion, readLocalInfoVersion } from './release-utils.mjs'
29+
30+
const REPO = 'nextcloud/spreed'
31+
32+
/** Print usage information and exit. */
33+
function usage() {
34+
print.log(`Usage: node scripts/bump-version.mjs <changelog-pr-number|branch-name>
35+
36+
Check out the given changelog backport PR (or an existing branch), bump the
37+
version in appinfo/info.xml and package.json, then commit the result.
38+
39+
Arguments:
40+
<changelog-pr-number> GitHub PR number of the backported changelog PR
41+
<branch-name> An existing branch to bump directly. Checked out
42+
locally if present, otherwise fetched and tracked
43+
from origin. Pulled either way.
44+
45+
Options:
46+
-h, --help Show this help message
47+
48+
Examples:
49+
node scripts/bump-version.mjs 18500
50+
node scripts/bump-version.mjs stable33
51+
`)
52+
process.exit(0)
53+
}
54+
55+
/**
56+
* Parse CLI arguments.
57+
*
58+
* @return {{type: 'pr'|'branch', value: string}} the checkout target
59+
*/
60+
function parseArguments() {
61+
let target = null
62+
63+
parseArgs(process.argv.slice(2), {
64+
usage,
65+
onPositional: (arg) => {
66+
target = /^\d+$/.test(arg) ? { type: 'pr', value: arg } : { type: 'branch', value: arg }
67+
},
68+
})
69+
70+
if (!target) {
71+
print.err('A changelog PR number or branch name is required')
72+
usage()
73+
}
74+
75+
return target
76+
}
77+
78+
/** Check required tools are available; exit if not. */
79+
function checkPreflight() {
80+
if (!preflight(['git', 'npm'])) {
81+
process.exit(1)
82+
}
83+
}
84+
85+
/**
86+
* Check out the changelog PR branch, via GitHub's refs/pull/<N>/head mirror
87+
* (works for fork PRs too, no `gh` needed).
88+
*
89+
* @param {string} prNumber the changelog PR number
90+
* @return {Promise<string>} the checked-out branch name
91+
*/
92+
async function checkoutPr(prNumber) {
93+
print.section(`Checking out PR #${prNumber}`)
94+
95+
const pr = await ghFetch(`/repos/${REPO}/pulls/${prNumber}`)
96+
const branchName = pr.head.ref
97+
98+
run('git', ['fetch', 'origin', `pull/${prNumber}/head`])
99+
run('git', ['checkout', '-B', branchName, 'FETCH_HEAD'])
100+
101+
const currentBranch = getCurrentBranch()
102+
print.ok(`Now on branch: ${currentBranch}`)
103+
104+
return currentBranch
105+
}
106+
107+
/**
108+
* Check out the given branch: switch to it if it exists locally, otherwise
109+
* fetch and track it from origin. Pulls the latest changes either way.
110+
*
111+
* @param {string} branchName the branch to check out
112+
* @return {string} the checked-out branch name
113+
*/
114+
function checkoutBranch(branchName) {
115+
print.section(`Checking out branch '${branchName}'`)
116+
117+
const onOrigin = branchExists(`origin/${branchName}`)
118+
119+
if (branchExists(branchName)) {
120+
run('git', ['checkout', branchName])
121+
} else if (onOrigin) {
122+
run('git', ['fetch', 'origin', branchName])
123+
run('git', ['checkout', '-b', branchName, `origin/${branchName}`])
124+
} else {
125+
print.err(`Branch '${branchName}' not found locally or on origin`)
126+
process.exit(1)
127+
}
128+
129+
// Only pull when there's a remote counterpart to pull from.
130+
if (onOrigin) {
131+
run('git', ['pull', '--ff-only', 'origin', branchName])
132+
}
133+
134+
const currentBranch = getCurrentBranch()
135+
print.ok(`Now on branch: ${currentBranch}`)
136+
137+
return currentBranch
138+
}
139+
140+
/**
141+
* Read the current version from appinfo/info.xml and compute the next one.
142+
*
143+
* @param {string} currentBranch the branch being released, for the report header
144+
* @return {{currentVersion: string, nextVersion: string}} the current and next version
145+
*/
146+
function readAndIncrementVersion(currentBranch) {
147+
if (!existsSync('appinfo/info.xml')) {
148+
print.err('appinfo/info.xml not found')
149+
process.exit(1)
150+
}
151+
152+
const currentVersion = readLocalInfoVersion()
153+
if (!currentVersion) {
154+
print.err('Could not read version from appinfo/info.xml')
155+
process.exit(1)
156+
}
157+
158+
const nextVersion = incrementVersion(currentVersion)
159+
if (nextVersion === null) {
160+
print.err(`Invalid semantic version in appinfo/info.xml: ${currentVersion}`)
161+
process.exit(1)
162+
}
163+
164+
print.header([` Bump version: v${currentVersion} → v${nextVersion}`, ` Branch: ${currentBranch}`])
165+
166+
return { currentVersion, nextVersion }
167+
}
168+
169+
/**
170+
* Bump the version in appinfo/info.xml and verify the edit landed.
171+
*
172+
* @param {string} currentVersion the version currently in the file
173+
* @param {string} nextVersion the version to write
174+
*/
175+
function bumpInfoXml(currentVersion, nextVersion) {
176+
print.section('Bumping appinfo/info.xml')
177+
178+
const xml = readFileSync('appinfo/info.xml', 'utf-8')
179+
writeFileSync(
180+
'appinfo/info.xml',
181+
xml.replace(`<version>${currentVersion}</version>`, `<version>${nextVersion}</version>`),
182+
)
183+
184+
const verify = readLocalInfoVersion()
185+
if (verify !== nextVersion) {
186+
print.err(`Version mismatch after edit — expected ${nextVersion}, got ${verify}`)
187+
process.exit(1)
188+
}
189+
print.ok(`appinfo/info.xml → ${nextVersion}`)
190+
}
191+
192+
/**
193+
* Bump the version in package.json via `npm version` and verify its output.
194+
*
195+
* @param {string} nextVersion the version to bump to
196+
*/
197+
function bumpPackageJson(nextVersion) {
198+
print.section('Bumping package.json')
199+
200+
// npm version prints the new version prefixed with 'v', e.g. v25.0.1
201+
const npmOutput = run('npm', ['version', '--no-git-tag-version', nextVersion], { capture: true }).replace(/^v/, '')
202+
203+
if (npmOutput !== nextVersion) {
204+
print.err(`npm version returned '${npmOutput}', expected '${nextVersion}'`)
205+
process.exit(1)
206+
}
207+
print.ok(`package.json → ${nextVersion}`)
208+
}
209+
210+
/**
211+
* Commit the version bump.
212+
*
213+
* @param {string} nextVersion the version being released, for the commit message
214+
*/
215+
function commitChanges(nextVersion) {
216+
print.section('Committing')
217+
218+
run('git', ['add', 'appinfo/info.xml', 'package.json', 'package-lock.json'])
219+
run('git', ['commit', '-s', '-m', `chore(release): Prepare release v${nextVersion}`]) // -s for DCO
220+
221+
print.ok(`Committed: chore(release): Prepare release v${nextVersion}`)
222+
}
223+
224+
/**
225+
* Print the final push instructions. Push itself stays manual.
226+
*
227+
* @param {string} currentBranch the branch to push
228+
*/
229+
function printDone(currentBranch) {
230+
print.header(' Done — push when ready:')
231+
print.log()
232+
print.log(` git push origin ${currentBranch}`)
233+
print.log()
234+
}
235+
236+
/** Run the version bump. */
237+
async function main() {
238+
const target = parseArguments()
239+
checkPreflight()
240+
requireCleanWorktree()
241+
242+
const currentBranch = target.type === 'pr' ? await checkoutPr(target.value) : checkoutBranch(target.value)
243+
const { currentVersion, nextVersion } = readAndIncrementVersion(currentBranch)
244+
245+
bumpInfoXml(currentVersion, nextVersion)
246+
bumpPackageJson(nextVersion)
247+
commitChanges(nextVersion)
248+
249+
printDone(currentBranch)
250+
}
251+
252+
main().catch((err) => {
253+
print.err(err.message)
254+
process.exit(1)
255+
})

0 commit comments

Comments
 (0)