feat: derive mobile build number from semver (#30569)

This commit is contained in:
bo0tzz authored and GitHub committed 2026-08-11 12:45:32 +00:00
1 parent 9273a4ca2a
commit cf17dcbfee
5 files changed
+104 -41

No files matched your search

@@ -1,11 +1,16 @@
import semver from 'semver';
import { beforeEach, describe, expect, it, vi } from 'vitest';
import {
resolveArchivedVersions,
ArchivedVersion,
getMobileBuild,
getNewVersion,
ReleaseError,
} from './release';
const mobileBuild = (version: string) =>
getMobileBuild(semver.parse(version) as semver.SemVer);
const archived = (label: string): ArchivedVersion => ({
label,
url: `https://docs.${label}.archive.immich.app`,
@@ -137,3 +142,56 @@ describe(resolveArchivedVersions.name, () => {
]);
});
});
describe(getMobileBuild.name, () => {
it('should encode the version', () => {
expect(mobileBuild('3.1.0')).toBe(3_010_099);
expect(mobileBuild('3.1.4')).toBe(3_010_499);
expect(mobileBuild('3.2.0-rc.0')).toBe(3_020_000);
expect(mobileBuild('3.2.0-rc.3')).toBe(3_020_003);
});
it('should be above the last hand-maintained build number', () => {
expect(mobileBuild('3.1.0')).toBeGreaterThan(3057);
});
it('should stay under the play store limit', () => {
expect(mobileBuild('99.99.99')).toBeLessThan(2_100_000_000);
});
it('should increase across a release cycle', () => {
const versions = [
'3.1.0',
'3.1.1',
'3.2.0-rc.0',
'3.2.0-rc.1',
'3.2.0',
'3.2.1',
'3.3.0-rc.0',
'4.0.0-rc.0',
'4.0.0',
];
const builds = versions.map((version) => mobileBuild(version));
expect(builds).toEqual([...builds].sort((a, b) => a - b));
expect(new Set(builds).size).toBe(builds.length);
});
it('should rank a release above its own candidates', () => {
expect(mobileBuild('3.2.0')).toBeGreaterThan(mobileBuild('3.2.0-rc.98'));
});
it('should rank a patch on an older line below the newer line', () => {
expect(mobileBuild('3.1.5')).toBeLessThan(mobileBuild('3.2.0'));
});
it('should refuse versions it cannot encode', () => {
expect(() => mobileBuild('100.0.0')).toThrow('Cannot derive');
expect(() => mobileBuild('3.100.0')).toThrow('Cannot derive');
expect(() => mobileBuild('3.1.100')).toThrow('Cannot derive');
expect(() => mobileBuild('3.1.0-rc.99')).toThrow('Cannot derive');
expect(() => mobileBuild('3.1.0-beta')).toThrow('Cannot derive');
expect(() => mobileBuild('3.1.0-rc.-1')).toThrow('Cannot derive');
expect(() => mobileBuild('3.1.0-rc.x')).toThrow('Cannot derive');
});
});
+26 -9
View File
@@ -35,7 +35,7 @@ const Files = {
},
};
export const handleRelease = ({ type, mobile }: ReleaseOptions) => {
export const handleRelease = ({ type }: ReleaseOptions) => {
const versionRaw = getVersion();
const newVersionRaw = getNewVersion(versionRaw, type);
const newVersion = semver.parse(normalize(newVersionRaw));
@@ -43,8 +43,7 @@ export const handleRelease = ({ type, mobile }: ReleaseOptions) => {
throw new ReleaseInputError();
}
const newVersionNoRc = `${newVersion.major}.${newVersion.minor}.${newVersion.patch}`;
const mobileBuild = getMobileBuild();
const newMobileBuild = mobile ? mobileBuild + 1 : mobileBuild;
const newMobileBuild = getMobileBuild(newVersion);
// pump versions everywhere
@@ -189,14 +188,32 @@ export const getNewVersion = (versionRaw: string, type: string) => {
return newVersionRaw;
};
const getMobileBuild = () => {
const pubspec = new TextFile(Files.Mobile.Pubspec).read();
const match = pubspec.match(/^version: .*\+(\d+)$/m);
if (!match) {
throw new Error('Could not find mobile build number in pubspec.yaml');
const RADIX = 100;
const STABLE = RADIX - 1;
export const getMobileBuild = (version: SemVer) => {
const { major, minor, patch, prerelease } = version;
const candidate = prerelease[1];
const digit = typeof candidate === 'number' ? candidate : STABLE;
const valid =
major < RADIX &&
minor < RADIX &&
patch < RADIX &&
(prerelease.length === 0 || digit < STABLE);
if (!valid) {
throw new Error(
`Cannot derive a mobile build number from ${version.format()}`,
);
}
return Number(match[1]);
return (
major * Math.pow(RADIX, 3) +
minor * Math.pow(RADIX, 2) +
patch * RADIX +
digit
);
};
const pump = (path: string, pattern: RegExp, replacement: string) => {