diff --git a/.github/workflows/prepare-release.yml b/.github/workflows/prepare-release.yml index 1bf9a61e8c..4a405d8ccb 100644 --- a/.github/workflows/prepare-release.yml +++ b/.github/workflows/prepare-release.yml @@ -17,10 +17,6 @@ on: - prepatch - prerelease - release - mobileBump: - description: 'Bump mobile build number' - required: false - type: boolean skipTranslations: description: 'Skip translations' required: false @@ -79,8 +75,7 @@ jobs: - name: Bump version env: RELEASE_TYPE: ${{ inputs.releaseType }} - MOBILE_BUMP: ${{ inputs.mobileBump }} - run: mise //:release --type "${RELEASE_TYPE}" --mobile "${MOBILE_BUMP}" + run: mise //:release --type "${RELEASE_TYPE}" - id: output run: | diff --git a/packages/scripts/src/cli.ts b/packages/scripts/src/cli.ts index c89a0f142c..95e50e9770 100644 --- a/packages/scripts/src/cli.ts +++ b/packages/scripts/src/cli.ts @@ -23,32 +23,25 @@ export const cli = (argv: string[]) => { .choices(RELEASE_TYPES) .makeOptionMandatory(), ) - .addOption( - new Option('-m, --mobile ', 'pump mobile build number') - .choices(['true', 'false']) - .default('false'), - ) - .action( - ({ type, mobile }: { type: ReleaseType; mobile: 'true' | 'false' }) => { - try { - console.log(handleRelease({ type, mobile: mobile === 'true' })); - } catch (error) { - if (error instanceof ReleaseInputError) { - console.log(program.usage()); - process.exit(1); - } - - if (error instanceof ReleaseError) { - console.log( - `Invalid pump: ${type}. Pumping from ${error.version} to ${error.newVersion} is not allowed.`, - ); - process.exit(1); - } - - throw error; + .action(({ type }: { type: ReleaseType }) => { + try { + console.log(handleRelease({ type })); + } catch (error) { + if (error instanceof ReleaseInputError) { + console.log(program.usage()); + process.exit(1); } - }, - ); + + if (error instanceof ReleaseError) { + console.log( + `Invalid pump: ${type}. Pumping from ${error.version} to ${error.newVersion} is not allowed.`, + ); + process.exit(1); + } + + throw error; + } + }); return program.parse(argv); }; diff --git a/packages/scripts/src/commands/release.spec.ts b/packages/scripts/src/commands/release.spec.ts index de769ebdc6..0b1be22302 100644 --- a/packages/scripts/src/commands/release.spec.ts +++ b/packages/scripts/src/commands/release.spec.ts @@ -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'); + }); +}); diff --git a/packages/scripts/src/commands/release.ts b/packages/scripts/src/commands/release.ts index 68ef1dbf0e..307b66165a 100644 --- a/packages/scripts/src/commands/release.ts +++ b/packages/scripts/src/commands/release.ts @@ -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) => { diff --git a/packages/scripts/src/types.ts b/packages/scripts/src/types.ts index 9bc23f87b5..586fdb8763 100644 --- a/packages/scripts/src/types.ts +++ b/packages/scripts/src/types.ts @@ -11,7 +11,7 @@ export const RELEASE_TYPES = [ ] as const; export type ReleaseType = (typeof RELEASE_TYPES)[number]; -export type ReleaseOptions = { type: string; mobile: boolean }; +export type ReleaseOptions = { type: string }; export class TextFile { constructor(private file: string) {}