fix(mobile): keep Android release versionCode committed (#7271)

Co-authored-by: Orca <help@stably.ai>
This commit is contained in:
Jinwoo Hong 2026-07-03 15:26:11 -07:00 committed by GitHub
parent f4790e9fac
commit 25896f2cdb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 109 additions and 64 deletions

View File

@ -9,22 +9,8 @@ on:
- 'mobile-android-v*'
workflow_dispatch:
inputs:
bump_patch_version:
description: 'Bump the mobile marketing version patch number before release'
required: false
default: false
type: boolean
release_version:
description: 'Optional exact mobile marketing version override, e.g. 0.0.15'
required: false
type: string
bump_android_version_code:
description: 'Increment the Android versionCode before release'
required: false
default: false
type: boolean
android_version_code:
description: 'Optional exact Android versionCode override'
description: 'Optional exact mobile marketing version assertion, e.g. 0.0.22'
required: false
type: string
publish_github_release:
@ -69,9 +55,6 @@ jobs:
id: release
env:
MOBILE_ANDROID_RELEASE_VERSION: ${{ github.event.inputs.release_version }}
MOBILE_ANDROID_BUMP_PATCH_VERSION: ${{ github.event.inputs.bump_patch_version }}
MOBILE_ANDROID_VERSION_CODE: ${{ github.event.inputs.android_version_code }}
MOBILE_ANDROID_BUMP_VERSION_CODE: ${{ github.event.inputs.bump_android_version_code }}
MOBILE_ANDROID_PUBLISH_RELEASE: ${{ github.event.inputs.publish_github_release }}
run: node scripts/prepare-android-release.mjs

View File

@ -75,7 +75,7 @@
"allowBackup": false,
"permissions": ["RECORD_AUDIO", "MODIFY_AUDIO_SETTINGS"],
"package": "com.stably.orca.mobile",
"versionCode": 2
"versionCode": 4
},
"plugins": [
"expo-router",

View File

@ -22,31 +22,12 @@ function fail(message) {
process.exit(1)
}
function parsePositiveInteger(value, name) {
if (!/^\d+$/.test(value)) {
fail(`${name} must be a positive integer`)
}
const parsed = Number(value)
if (!Number.isSafeInteger(parsed) || parsed <= 0) {
fail(`${name} must be a positive integer`)
}
return parsed
}
function validateSemver(version, name) {
if (!semverPattern.test(version)) {
fail(`${name} must use x.y.z format`)
}
}
function bumpPatchVersion(version) {
validateSemver(version, 'Current mobile version')
const [major, minor, patch] = version.split('.')
return `${major}.${minor}.${Number(patch) + 1}`
}
function writeOutput(name, value) {
const outputPath = process.env.GITHUB_OUTPUT
if (!outputPath) {
@ -74,10 +55,6 @@ const tagVersion = githubRef.startsWith(androidTagRefPrefix)
const requestedVersion = input('MOBILE_ANDROID_RELEASE_VERSION')
const bumpPatch = truthy(input('MOBILE_ANDROID_BUMP_PATCH_VERSION'))
if (requestedVersion && bumpPatch) {
fail('Use either MOBILE_ANDROID_RELEASE_VERSION or MOBILE_ANDROID_BUMP_PATCH_VERSION, not both')
}
if (tagVersion) {
validateSemver(tagVersion, 'Android release tag version')
}
@ -86,43 +63,36 @@ if (requestedVersion) {
validateSemver(requestedVersion, 'MOBILE_ANDROID_RELEASE_VERSION')
}
if (tagVersion && requestedVersion && requestedVersion !== tagVersion) {
fail('MOBILE_ANDROID_RELEASE_VERSION must match the mobile-android-v tag')
if (bumpPatch) {
fail('MOBILE_ANDROID_BUMP_PATCH_VERSION is no longer supported; commit mobile/app.json first')
}
if (tagVersion && bumpPatch) {
fail('MOBILE_ANDROID_BUMP_PATCH_VERSION is only supported for manual branch runs')
if (tagVersion && tagVersion !== currentVersion) {
fail('Android release tag version must match the committed mobile app version')
}
const version =
requestedVersion || tagVersion || (bumpPatch ? bumpPatchVersion(currentVersion) : currentVersion)
if (requestedVersion && requestedVersion !== currentVersion) {
fail('MOBILE_ANDROID_RELEASE_VERSION must match the committed mobile app version')
}
const requestedVersionCode = input('MOBILE_ANDROID_VERSION_CODE')
const bumpVersionCode = truthy(input('MOBILE_ANDROID_BUMP_VERSION_CODE'))
if (requestedVersionCode && bumpVersionCode) {
fail('Use either MOBILE_ANDROID_VERSION_CODE or MOBILE_ANDROID_BUMP_VERSION_CODE, not both')
if (requestedVersionCode || bumpVersionCode) {
// Why: Android rejects lower versionCode installs; release-only bumps leave
// committed dev builds behind shipped APKs and break local testing.
fail('Android versionCode changes must be committed in mobile/app.json before release')
}
const versionCode = requestedVersionCode
? parsePositiveInteger(requestedVersionCode, 'MOBILE_ANDROID_VERSION_CODE')
: bumpVersionCode || version !== currentVersion
? currentVersionCode + 1
: currentVersionCode
expo.version = version
android.versionCode = versionCode
fs.writeFileSync(appConfigPath, `${JSON.stringify(config, null, 2)}\n`)
const tag = `mobile-android-v${version}`
const tag = `mobile-android-v${currentVersion}`
const publishRelease =
githubRef.startsWith(androidTagRefPrefix) || truthy(input('MOBILE_ANDROID_PUBLISH_RELEASE'))
writeOutput('version', version)
writeOutput('android_version_code', String(versionCode))
writeOutput('version', currentVersion)
writeOutput('android_version_code', String(currentVersionCode))
writeOutput('tag', tag)
writeOutput('publish_release', publishRelease ? 'true' : 'false')
console.log(`Prepared Orca Mobile Android ${version} (${versionCode})`)
console.log(`Prepared Orca Mobile Android ${currentVersion} (${currentVersionCode})`)
console.log(`Release tag: ${tag}`)
console.log(`Publish GitHub Release: ${publishRelease ? 'yes' : 'no'}`)

View File

@ -0,0 +1,92 @@
import { execFileSync, spawnSync } from 'node:child_process'
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { fileURLToPath } from 'node:url'
import { afterEach, describe, expect, it } from 'vitest'
const scriptPath = fileURLToPath(
new URL('../../scripts/prepare-android-release.mjs', import.meta.url)
)
const appConfig = {
expo: {
version: '0.0.22',
android: {
versionCode: 4
}
}
}
let tempDirs: string[] = []
function createAppConfig() {
const dir = mkdtempSync(join(tmpdir(), 'orca-android-release-'))
tempDirs.push(dir)
const configPath = join(dir, 'app.json')
const contents = `${JSON.stringify(appConfig, null, 2)}\n`
writeFileSync(configPath, contents)
return { configPath, contents }
}
describe('prepare Android release script', () => {
afterEach(() => {
for (const dir of tempDirs) {
rmSync(dir, { force: true, recursive: true })
}
tempDirs = []
})
it('uses committed Android release identity without mutating app config', () => {
const { configPath, contents } = createAppConfig()
const output = execFileSync(process.execPath, [scriptPath], {
encoding: 'utf8',
env: {
...process.env,
MOBILE_APP_CONFIG_PATH: configPath,
MOBILE_ANDROID_PUBLISH_RELEASE: 'true'
}
})
expect(output).toContain('Prepared Orca Mobile Android 0.0.22 (4)')
expect(output).toContain('Release tag: mobile-android-v0.0.22')
expect(readFileSync(configPath, 'utf8')).toBe(contents)
})
it('rejects release-only Android versionCode bumps', () => {
const { configPath } = createAppConfig()
const result = spawnSync(process.execPath, [scriptPath], {
encoding: 'utf8',
env: {
...process.env,
MOBILE_APP_CONFIG_PATH: configPath,
MOBILE_ANDROID_BUMP_VERSION_CODE: 'true'
}
})
expect(result.status).toBe(1)
expect(result.stderr).toContain(
'Android versionCode changes must be committed in mobile/app.json before release'
)
})
it('rejects release versions that do not match committed app config', () => {
const { configPath } = createAppConfig()
const result = spawnSync(process.execPath, [scriptPath], {
encoding: 'utf8',
env: {
...process.env,
MOBILE_APP_CONFIG_PATH: configPath,
MOBILE_ANDROID_RELEASE_VERSION: '0.0.23'
}
})
expect(result.status).toBe(1)
expect(result.stderr).toContain(
'MOBILE_ANDROID_RELEASE_VERSION must match the committed mobile app version'
)
})
})