feat: add prerelease support to pump version (#28922)

refactor: pump script
This commit is contained in:
Jason Rasmussen
2026-06-09 14:42:10 -04:00
committed by GitHub
parent d3438cf4a7
commit 8b5385f94b
11 changed files with 507 additions and 256 deletions
+6 -2
View File
@@ -10,9 +10,13 @@ on:
type: choice
options:
- 'false'
- major
- minor
- patch
- premajor
- preminor
- prepatch
- prerelease
- release
mobileBump:
description: 'Bump mobile build number'
required: false
@@ -74,7 +78,7 @@ jobs:
env:
SERVER_BUMP: ${{ inputs.serverBump }}
MOBILE_BUMP: ${{ inputs.mobileBump }}
run: misc/release/pump-version.sh -s "${SERVER_BUMP}" -m "${MOBILE_BUMP}"
run: pnpm --silent release -s "${SERVER_BUMP}" -m "${MOBILE_BUMP}"
- id: output
run: echo "version=$IMMICH_VERSION" >> $GITHUB_OUTPUT
+32
View File
@@ -28,6 +28,10 @@ jobs:
with:
github-token: ${{ steps.token.outputs.token }}
filters: |
root:
- 'misc/**'
- 'pnpm-lock.yaml'
- 'mise.toml'
i18n:
- 'i18n/**'
- 'mise.toml'
@@ -62,6 +66,34 @@ jobs:
- '.github/workflows/test.yml'
force-events: 'workflow_dispatch'
root-unit-tests:
name: Test the root workspace
needs: pre-job
if: ${{ fromJSON(needs.pre-job.outputs.should_run).root == true }}
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- id: token
uses: immich-app/devtools/actions/create-workflow-token@9db058b2e6eec20e07760b0e17a0505c78ec3191 # create-workflow-token-action-v2.0.1
with:
client-id: ${{ secrets.PUSH_O_MATIC_APP_CLIENT_ID }}
private-key: ${{ secrets.PUSH_O_MATIC_APP_KEY }}
- name: Checkout code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
with:
persist-credentials: false
token: ${{ steps.token.outputs.token }}
- name: Setup Mise
uses: immich-app/devtools/actions/use-mise@7b8610a904d57da241e4ddba17fa62b62b15aed4 # use-mise-action-v2.0.2
with:
github_token: ${{ steps.token.outputs.token }}
- name: Run unit tests
run: pnpm test
server-unit-tests:
name: Test & Lint Server
needs: pre-job
+1
View File
@@ -60,6 +60,7 @@
"explorer.fileNesting.patterns": {
"*.dart": "${capture}.g.dart,${capture}.gr.dart,${capture}.drift.dart",
"*.ts": "${capture}.spec.ts,${capture}.mock.ts",
"*.js": "${capture}.spec.js,${capture}.mock.js",
"package.json": "package-lock.json, yarn.lock, pnpm-lock.yaml, bun.lockb, bun.lock, pnpm-workspace.yaml, .pnpmfile.cjs"
},
"search.exclude": {
+5 -4
View File
@@ -1,9 +1,10 @@
#! /usr/bin/env node
const { readFileSync, writeFileSync } = require('node:fs');
import { readFileSync, writeFileSync } from 'node:fs';
const asVersion = (item) => {
const { label, url } = item;
const [major, minor, patch] = label.substring(1).split('.').map(Number);
const [version] = label.substring(1).split('-');
const [major, minor, patch] = version.split('.').map(Number);
return { major, minor, patch, label, url };
};
@@ -31,7 +32,7 @@ for (const item of versions) {
) {
versions = versions.filter((item) => item.label !== version.label);
console.log(
`Removed ${version.label} (replaced with ${lastVersion.label})`
`Removed ${version.label} (replaced with ${lastVersion.label})`,
);
continue;
}
@@ -41,5 +42,5 @@ for (const item of versions) {
writeFileSync(
filename,
JSON.stringify([newVersion, ...versions], null, 2) + '\n'
JSON.stringify([newVersion, ...versions], null, 2) + '\n',
);
+18 -30
View File
@@ -3,12 +3,14 @@
#
# Pump one or both of the server/mobile versions in appropriate files
#
# usage: './scripts/pump-version.sh -s <major|minor|patch> <-m> <true|false>
# usage: './scripts/pump-version.sh -s <minor|patch|premajor|preminor|prepatch|prerelease> <-m> <true|false>
#
# examples:
# ./scripts/pump-version.sh -s major # 1.0.0+50 => 2.0.0+50
# ./scripts/pump-version.sh -s minor -m true # 1.0.0+50 => 1.1.0+51
# ./scripts/pump-version.sh -m true # 1.0.0+50 => 1.0.0+51
# ./scripts/pump-version.sh -s major # 1.0.0+50 => 2.0.0+50
# ./scripts/pump-version.sh -s minor -m true # 1.0.0+50 => 1.1.0+51
# ./scripts/pump-version.sh -s premajor # 1.0.0+50 => 2.0.0-rc.0+50
# ./scripts/pump-version.sh -s prerelease # 2.0.0-rc.0+50 => 2.0.0-rc.1+50
# ./scripts/pump-version.sh -m true # 1.0.0+50 => 1.0.0+51
#
SERVER_PUMP="false"
@@ -25,31 +27,15 @@ while getopts 's:m:' flag; do
esac
done
CURRENT_SERVER=$(jq -r '.version' server/package.json)
MAJOR=$(echo "$CURRENT_SERVER" | cut -d '.' -f1)
MINOR=$(echo "$CURRENT_SERVER" | cut -d '.' -f2)
PATCH=$(echo "$CURRENT_SERVER" | cut -d '.' -f3)
if [[ $SERVER_PUMP == "major" ]]; then
MAJOR=$((MAJOR + 1))
MINOR=0
PATCH=0
elif [[ $SERVER_PUMP == "minor" ]]; then
MINOR=$((MINOR + 1))
PATCH=0
elif [[ $SERVER_PUMP == "patch" ]]; then
PATCH=$((PATCH + 1))
elif [[ $SERVER_PUMP == "false" ]]; then
echo 'Skipping Server Pump'
else
echo 'Expected <major|minor|patch|false> for the server argument'
CURRENT_SERVER=$(jq -r '.version' package.json)
if ! NEXT_SERVER=$(pnpm --silent pump "$CURRENT_SERVER" "$SERVER_PUMP"); then
echo "Fatal: failed to pump server version: $NEXT_SERVER" >&2
exit 1
fi
NEXT_SERVER=$MAJOR.$MINOR.$PATCH
CURRENT_MOBILE=$(grep "^version: .*+[0-9]\+$" mobile/pubspec.yaml | cut -d "+" -f2)
NEXT_MOBILE=$CURRENT_MOBILE
if [[ $MOBILE_PUMP == "true" ]]; then
set $((NEXT_MOBILE++))
elif [[ $MOBILE_PUMP == "false" ]]; then
@@ -59,15 +45,17 @@ else
exit 1
fi
if [ "$CURRENT_SERVER" != "$NEXT_SERVER" ]; then
echo "Pumping Server: $CURRENT_SERVER => $NEXT_SERVER"
pnpm version "$NEXT_SERVER" --no-git-tag-version
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix server
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix packages/cli
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix web
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix e2e
pnpm version "$NEXT_SERVER" --no-git-tag-version --prefix packages/sdk
pnpm version "$NEXT_SERVER" --no-git-tag-version --no-git-checks
pnpm version "$NEXT_SERVER" --no-git-tag-version --no-git-checks --prefix server
pnpm version "$NEXT_SERVER" --no-git-tag-version --no-git-checks --prefix packages/cli
pnpm version "$NEXT_SERVER" --no-git-tag-version --no-git-checks --prefix web
pnpm version "$NEXT_SERVER" --no-git-tag-version --no-git-checks --prefix e2e
pnpm version "$NEXT_SERVER" --no-git-tag-version --no-git-checks --prefix packages/sdk
# copy version to open-api spec
mise run //:open-api
+7
View File
@@ -0,0 +1,7 @@
import { pump } from './pump.js';
const [versionRaw, type] = process.argv.slice(2);
const { message, exitCode } = pump(versionRaw, type);
console.log(message);
process.exit(exitCode);
+105
View File
@@ -0,0 +1,105 @@
import semver, { SemVer } from 'semver';
const printUsage = () => {
return {
message:
'Usage: ./pump_cli.js <semver> <minor|patch|premajor|preminor|prepatch|prerelease|release>',
exitCode: 1,
};
};
const isPrerelease = (version) => version.prerelease.length > 0;
/**
* @param {SemVer} version
* @returns {boolean}
*/
const inc = (version, type) => `v${semver.inc(version, type, {}, 'rc')}`;
/** @param {string} version */
const normalize = (version) => {
if (version.startsWith('v')) {
version = version.slice(1);
}
return version;
};
/**
* @param {string} versionRaw
* @param {string} type
*/
export const pump = (versionRaw, type) => {
if (!versionRaw) {
return printUsage();
}
versionRaw = normalize(versionRaw);
const version = semver.parse(versionRaw);
if (!version) {
return printUsage();
}
let newVersionRaw;
let valid = true;
switch (type) {
case 'patch':
case 'prepatch':
case 'minor':
case 'preminor':
case 'premajor': {
newVersionRaw = inc(version, type);
// can only use while not in a prerelease
valid = !isPrerelease(version);
break;
}
case 'prerelease': {
newVersionRaw = inc(version, type);
// can only use while in a prerelease
valid = isPrerelease(version);
break;
}
case 'release': {
// drop prerelease part
newVersionRaw = `${version.major}.${version.minor}.${version.patch}`;
// can only use to promote a prerelease to a release (no version change)
valid = isPrerelease(version);
break;
}
default: {
return printUsage();
}
}
if (!newVersionRaw) {
return printUsage();
}
newVersionRaw = normalize(newVersionRaw);
const newVersion = semver.parse(newVersionRaw);
if (!newVersion) {
return printUsage();
}
const invalidUpgrade =
isPrerelease(version) &&
!isPrerelease(newVersion) &&
(version.major !== newVersion.major ||
version.minor !== newVersion.minor ||
version.patch !== newVersion.patch);
if (!valid || invalidUpgrade) {
return {
message: `Invalid pump: ${type}. Pumping from ${versionRaw} to ${newVersionRaw} is not allowed.`,
exitCode: 1,
};
}
return { message: newVersionRaw, exitCode: 0 };
};
+87
View File
@@ -0,0 +1,87 @@
import { describe, expect, it } from 'vitest';
import { pump } from './pump';
describe(pump.name, () => {
describe('usage', () => {
it.each([
[],
['2.7.5'],
['2.7.5', 'invalid'],
['invalid', 'patch'],
['2.7.5', 'major'],
])('should not accept $0, $1 as inputs', (version, type) => {
expect(pump(version, type)).toEqual({
message: expect.stringContaining('Usage: '),
exitCode: 1,
});
});
});
describe('transitions', () => {
const valid = [
{
name: 'patch',
items: [['patch', '2.7.5', '2.7.6']],
},
{
name: 'prepatch',
items: [
['prepatch', '2.7.5', '2.7.6-rc.0'],
['prerelease', '2.7.6-rc.0', '2.7.6-rc.1'],
['release', '2.7.6-rc.1', '2.7.6'],
],
},
{
name: 'minor',
items: [['minor', '2.7.5', '2.8.0']],
},
{
name: 'preminor',
items: [
['preminor', '2.7.5', '2.8.0-rc.0'],
['prerelease', '2.8.0-rc.0', '2.8.0-rc.1'],
['release', '2.8.0-rc.1', '2.8.0'],
],
},
{
name: 'premajor',
items: [
['premajor', '2.7.5', '3.0.0-rc.0'],
['prerelease', '3.0.0-rc.0', '3.0.0-rc.1'],
['release', '3.0.0-rc.1', '3.0.0'],
],
},
];
for (const group of valid) {
describe(group.name, () => {
it.each(group.items)(
'should allow a $0 from $1 to $2',
(type, version, next) => {
expect(pump(version, type)).toEqual({
message: next,
exitCode: 0,
});
},
);
});
}
describe('invalid', () => {
it.each([
['patch', 'v3.0.0-rc.0'],
['prepatch', 'v3.0.0-rc.0'],
['minor', 'v3.0.0-rc.0'],
['preminor', 'v3.0.0-rc.0'],
['premajor', 'v3.0.0-rc.0'],
['prerelease', 'v3.0.0'],
['release', 'v3.0.0'],
])('should not allow a $0 on $1', (type, version) => {
expect(pump(version, type)).toEqual({
message: expect.stringContaining('Invalid pump'),
exitCode: 1,
});
});
});
});
});
+9 -2
View File
@@ -2,17 +2,24 @@
"name": "immich-monorepo",
"version": "2.7.5",
"description": "Monorepo for Immich",
"type": "module",
"private": true,
"scripts": {
"format": "prettier --cache --check i18n/",
"format:fix": "prettier --cache --write --list-different i18n"
"format:fix": "prettier --cache --write --list-different i18n",
"test": "vitest",
"release": "./misc/release/pump-version.sh",
"pump": "node ./misc/release/pump-wrapper.js"
},
"packageManager": "pnpm@11.4.0",
"engines": {
"pnpm": ">=10.0.0"
},
"devDependencies": {
"@types/node": "^24.12.4",
"prettier": "^3.8.3",
"prettier-plugin-sort-json": "^4.2.0"
"prettier-plugin-sort-json": "^4.2.0",
"semver": "^7.8.1",
"vitest": "^4.1.8"
}
}
+230 -218
View File
File diff suppressed because it is too large Load Diff
+7
View File
@@ -0,0 +1,7 @@
import { defineConfig } from 'vitest/config';
export default defineConfig({
test: {
include: ['misc/**/*.spec.js'],
},
});