This commit is contained in:
Alex Tran
2026-05-15 10:51:24 -05:00
parent f8244e953b
commit f7f8d38ad5
2 changed files with 320 additions and 34 deletions
+20 -34
View File
@@ -9,9 +9,9 @@
# ./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 minor -m true -r true # 3.0.0 => 3.1.0-rc.1 (start RC)
# ./scripts/pump-version.sh -m true -r true # 3.1.0-rc.1 => 3.1.0-rc.2 (iterate RC)
# ./scripts/pump-version.sh -m true -r finalize # 3.1.0-rc.2 => 3.1.0 (finalize RC)
# ./scripts/pump-version.sh -s minor -m true -r true # 3.0.0 => 3.1.0-rc.0 (start RC)
# ./scripts/pump-version.sh -m true -r true # 3.1.0-rc.0 => 3.1.0-rc.1 (iterate RC)
# ./scripts/pump-version.sh -m true -r finalize # 3.1.0-rc.1 => 3.1.0 (finalize RC)
#
SERVER_PUMP="false"
@@ -72,41 +72,28 @@ else
fi
fi
MAJOR=$(echo "$CURRENT_BASE" | cut -d '.' -f1)
MINOR=$(echo "$CURRENT_BASE" | cut -d '.' -f2)
PATCH=$(echo "$CURRENT_BASE" | 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
if [[ "$SERVER_PUMP" != "major" && "$SERVER_PUMP" != "minor" && "$SERVER_PUMP" != "patch" && "$SERVER_PUMP" != "false" ]]; then
echo 'Expected <major|minor|patch|false> for the server argument'
exit 1
fi
NEXT_BASE=$MAJOR.$MINOR.$PATCH
if [[ "$RC" == "true" ]]; then
if [[ -n "$CURRENT_RC_NUM" ]]; then
# Iterate existing RC
NEXT_RC_NUM=$((CURRENT_RC_NUM + 1))
NEXT_SERVER="${NEXT_BASE}-rc.${NEXT_RC_NUM}"
else
# Start new RC after server bump
NEXT_SERVER="${NEXT_BASE}-rc.1"
fi
elif [[ "$RC" == "finalize" ]]; then
NEXT_SERVER="$NEXT_BASE"
NEXT_SERVER="$CURRENT_SERVER"
if [[ "$SERVER_PUMP" == "false" && "$RC" == "false" ]]; then
echo 'Skipping Server Pump'
else
NEXT_SERVER="$NEXT_BASE"
npm version "$CURRENT_SERVER" --allow-same-version --no-git-tag-version || exit 1
if [[ "$RC" == "true" && -n "$CURRENT_RC_NUM" ]]; then
npm version prerelease --no-git-tag-version || exit 1
elif [[ "$RC" == "true" ]]; then
npm version "pre$SERVER_PUMP" --preid=rc --no-git-tag-version || exit 1
elif [[ "$RC" == "finalize" ]]; then
npm version "$CURRENT_BASE" --no-git-tag-version || exit 1
else
npm version "$SERVER_PUMP" --no-git-tag-version || exit 1
fi
NEXT_SERVER=$(jq -r '.version' package.json)
fi
CURRENT_MOBILE=$(grep "^version: .*+[0-9]\+$" mobile/pubspec.yaml | cut -d "+" -f2)
@@ -123,7 +110,6 @@ 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
+300
View File
@@ -0,0 +1,300 @@
import { spawnSync } from 'node:child_process';
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { dirname, join, resolve } from 'node:path';
import { fileURLToPath } from 'node:url';
import assert from 'node:assert/strict';
import test from 'node:test';
const scriptDir = dirname(fileURLToPath(import.meta.url));
const repoRoot = resolve(scriptDir, '../..');
const scriptUnderTest = join(repoRoot, 'misc/release/pump-version.sh');
const read = (path) => readFileSync(path, 'utf8');
const packageVersion = (dir) => JSON.parse(read(join(dir, 'package.json'))).version;
const shellQuote = (value) => `'${value.replaceAll("'", "'\\''")}'`;
const writeExecutable = (path, contents) => {
writeFileSync(path, contents, { mode: 0o755 });
};
const writePackageJson = (dir, name, version) => {
writeFileSync(
join(dir, 'package.json'),
`${JSON.stringify({ name, version, private: true }, null, 2)}\n`,
);
};
const makeFixture = (t, { rootVersion = '2.7.5', serverVersion = '3.0.0', mobileBuild = 3047 } = {}) => {
const workdir = mkdtempSync(join(tmpdir(), 'pump-version-'));
t.after(() => rmSync(workdir, { recursive: true, force: true }));
const currentBase = serverVersion.replace(/-rc\..+$/, '');
for (const path of [
'bin',
'server',
'packages/cli',
'web',
'e2e',
'packages/sdk',
'misc/release',
'mobile/android/fastlane',
'mobile/ios/Runner',
'machine-learning',
]) {
mkdirSync(join(workdir, path), { recursive: true });
}
writeCommandStubs(workdir);
writePackageJson(workdir, 'immich-monorepo', rootVersion);
writePackageJson(join(workdir, 'server'), 'immich', serverVersion);
writePackageJson(join(workdir, 'packages/cli'), '@immich/cli', serverVersion);
writePackageJson(join(workdir, 'web'), 'immich-web', serverVersion);
writePackageJson(join(workdir, 'e2e'), 'immich-e2e', serverVersion);
writePackageJson(join(workdir, 'packages/sdk'), '@immich/sdk', serverVersion);
writeExecutable(
join(workdir, 'misc/release/archive-version.js'),
`#!/usr/bin/env bash
set -euo pipefail
echo "$*" >>"$PWD/archive-version.calls"
`,
);
writeFileSync(
join(workdir, 'mobile/pubspec.yaml'),
`name: immich_mobile
version: ${serverVersion}+${mobileBuild}
`,
);
writeFileSync(
join(workdir, 'mobile/android/fastlane/Fastfile'),
`lane :gha_release_prod do
gradle(
properties: {
"android.injected.version.code" => ${mobileBuild},
"android.injected.version.name" => "${serverVersion}",
}
)
end
`,
);
writeFileSync(
join(workdir, 'mobile/ios/Runner/Info.plist'),
`<?xml version="1.0" encoding="UTF-8"?>
<plist version="1.0">
<dict>
<key>CFBundleShortVersionString</key>
<string>${currentBase}</string>
</dict>
</plist>
`,
);
return {
path: workdir,
file: (path) => join(workdir, path),
readFile: (path) => read(join(workdir, path)),
hasFile: (path) => {
try {
return read(join(workdir, path)).length > 0;
} catch {
return false;
}
},
run: (...args) =>
spawnSync('bash', [scriptUnderTest, ...args], {
cwd: workdir,
env: {
...process.env,
GITHUB_ENV: join(workdir, 'github_env'),
PATH: `${join(workdir, 'bin')}:${process.env.PATH}`,
},
encoding: 'utf8',
}),
};
};
const writeCommandStubs = (workdir) => {
const realNpm = spawnSync('which', ['npm'], { encoding: 'utf8' }).stdout.trim();
writeExecutable(
join(workdir, 'bin/npm'),
`#!/usr/bin/env bash
set -euo pipefail
real_npm=${shellQuote(realNpm)}
echo "$*" >>"$PWD/npm.calls"
"$real_npm" "$@"
`,
);
writeExecutable(
join(workdir, 'bin/pnpm'),
`#!/usr/bin/env bash
set -euo pipefail
if [[ "\${1:-}" != "version" ]]; then
echo "Unexpected pnpm command: $*" >&2
exit 1
fi
shift
version="\${1:-}"
shift
prefix="."
while [[ $# -gt 0 ]]; do
case "$1" in
--prefix)
prefix="$2"
shift 2
;;
--no-git-tag-version)
shift
;;
*)
echo "Unexpected pnpm argument: $1" >&2
exit 1
;;
esac
done
npm --prefix "$prefix" version "$version" --no-git-tag-version --allow-same-version >/dev/null
`,
);
writeExecutable(
join(workdir, 'bin/mise'),
`#!/usr/bin/env bash
set -euo pipefail
echo "$*" >>"$PWD/mise.calls"
`,
);
writeExecutable(
join(workdir, 'bin/uv'),
`#!/usr/bin/env bash
set -euo pipefail
echo "$*" >>"$PWD/uv.calls"
if [[ "\${1:-}" != "version" ]]; then
echo "Unexpected uv command: $*" >&2
exit 1
fi
shift
directory="."
if [[ "\${1:-}" == "--directory" ]]; then
directory="$2"
shift 2
fi
version="\${1:-}"
mkdir -p "$directory"
cat >"$directory/pyproject.toml" <<PYPROJECT
[project]
version = "$version"
PYPROJECT
`,
);
};
const assertCommandPassed = (result) => {
assert.equal(result.status, 0, result.stderr || result.stdout);
};
const assertPackageVersions = (fixture, expected) => {
assert.equal(packageVersion(fixture.path), expected);
assert.equal(packageVersion(fixture.file('server')), expected);
assert.equal(packageVersion(fixture.file('packages/cli')), expected);
assert.equal(packageVersion(fixture.file('web')), expected);
assert.equal(packageVersion(fixture.file('e2e')), expected);
assert.equal(packageVersion(fixture.file('packages/sdk')), expected);
};
const npmCalls = (fixture) => fixture.readFile('npm.calls').trim().split('\n');
test('starts an RC from the server version when the root package is stale', (t) => {
const fixture = makeFixture(t, { rootVersion: '2.7.5', serverVersion: '3.0.0', mobileBuild: 3047 });
const result = fixture.run('-s', 'minor', '-m', 'true', '-r', 'true');
assertCommandPassed(result);
assertPackageVersions(fixture, '3.1.0-rc.0');
assert.ok(npmCalls(fixture).includes('version preminor --preid=rc --no-git-tag-version'));
assert.match(fixture.readFile('mobile/pubspec.yaml'), /version: 3\.1\.0-rc\.0\+3048/);
assert.match(fixture.readFile('mobile/android/fastlane/Fastfile'), /"android\.injected\.version\.name" => "3\.1\.0-rc\.0"/);
assert.match(fixture.readFile('mobile/android/fastlane/Fastfile'), /"android\.injected\.version\.code" => 3048/);
assert.match(fixture.readFile('mobile/ios/Runner/Info.plist'), /<string>3\.1\.0<\/string>/);
assert.match(fixture.readFile('uv.calls'), /version --directory machine-learning 3\.1\.0rc0/);
assert.equal(fixture.hasFile('archive-version.calls'), false);
assert.match(fixture.readFile('github_env'), /IMMICH_VERSION=v3\.1\.0-rc\.0/);
});
test('iterates an existing RC', (t) => {
const fixture = makeFixture(t, { rootVersion: '2.7.5', serverVersion: '3.1.0-rc.0', mobileBuild: 3048 });
const result = fixture.run('-m', 'false', '-r', 'true');
assertCommandPassed(result);
assertPackageVersions(fixture, '3.1.0-rc.1');
assert.ok(npmCalls(fixture).includes('version prerelease --no-git-tag-version'));
assert.equal(npmCalls(fixture).some((call) => call.startsWith('version prerelease --preid')), false);
assert.match(fixture.readFile('mobile/pubspec.yaml'), /version: 3\.1\.0-rc\.1\+3048/);
assert.match(fixture.readFile('uv.calls'), /version --directory machine-learning 3\.1\.0rc1/);
assert.equal(fixture.hasFile('archive-version.calls'), false);
});
test('finalizes an existing RC', (t) => {
const fixture = makeFixture(t, { rootVersion: '2.7.5', serverVersion: '3.1.0-rc.1', mobileBuild: 3048 });
const result = fixture.run('-m', 'false', '-r', 'finalize');
assertCommandPassed(result);
assertPackageVersions(fixture, '3.1.0');
assert.match(fixture.readFile('mobile/pubspec.yaml'), /version: 3\.1\.0\+3048/);
assert.match(fixture.readFile('mobile/ios/Runner/Info.plist'), /<string>3\.1\.0<\/string>/);
assert.match(fixture.readFile('uv.calls'), /version --directory machine-learning 3\.1\.0/);
assert.match(fixture.readFile('archive-version.calls'), /3\.1\.0/);
});
test('bumps a normal patch release', (t) => {
const fixture = makeFixture(t, { rootVersion: '2.7.5', serverVersion: '3.1.0', mobileBuild: 3048 });
const result = fixture.run('-s', 'patch', '-m', 'true');
assertCommandPassed(result);
assertPackageVersions(fixture, '3.1.1');
assert.match(fixture.readFile('mobile/pubspec.yaml'), /version: 3\.1\.1\+3049/);
assert.match(fixture.readFile('uv.calls'), /version --directory machine-learning 3\.1\.1/);
assert.match(fixture.readFile('archive-version.calls'), /3\.1\.1/);
});
test('bumps mobile only', (t) => {
const fixture = makeFixture(t, { rootVersion: '2.7.5', serverVersion: '3.1.0', mobileBuild: 3048 });
const result = fixture.run('-m', 'true');
assertCommandPassed(result);
assert.equal(packageVersion(fixture.path), '2.7.5');
assert.equal(packageVersion(fixture.file('server')), '3.1.0');
assert.match(fixture.readFile('mobile/pubspec.yaml'), /version: 3\.1\.0\+3049/);
assert.equal(fixture.hasFile('uv.calls'), false);
assert.equal(fixture.hasFile('archive-version.calls'), false);
});
test('rejects starting a new RC while already on an RC', (t) => {
const fixture = makeFixture(t, { rootVersion: '2.7.5', serverVersion: '3.1.0-rc.0', mobileBuild: 3048 });
const result = fixture.run('-s', 'patch', '-r', 'true');
assert.notEqual(result.status, 0);
assert.match(result.stdout, /Cannot start a new RC while still on an RC; finalize first\./);
assert.equal(packageVersion(fixture.path), '2.7.5');
assert.equal(packageVersion(fixture.file('server')), '3.1.0-rc.0');
});