mirror of
https://github.com/openclaw/openclaw.git
synced 2026-08-28 05:16:23 -06:00
android: add pinned release versioning
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
# Shared Android version defaults.
|
||||
# Source of truth: apps/android/version.json
|
||||
# Generated by scripts/android-sync-versioning.ts.
|
||||
|
||||
OPENCLAW_ANDROID_VERSION_NAME=2026.6.2
|
||||
OPENCLAW_ANDROID_VERSION_CODE=2026060201
|
||||
@@ -1,6 +1,24 @@
|
||||
import com.android.build.api.variant.impl.VariantOutputImpl
|
||||
import java.util.Properties
|
||||
|
||||
val dnsjavaInetAddressResolverService = "META-INF/services/java.net.spi.InetAddressResolverProvider"
|
||||
val openClawAndroidVersionFile = rootProject.file("Config/Version.properties")
|
||||
val openClawAndroidVersionProperties =
|
||||
Properties().apply {
|
||||
if (!openClawAndroidVersionFile.isFile) {
|
||||
error("Missing Android version properties. Run `pnpm android:version:sync`.")
|
||||
}
|
||||
openClawAndroidVersionFile.inputStream().use(::load)
|
||||
}
|
||||
|
||||
fun requireOpenClawAndroidVersionProperty(name: String): String =
|
||||
openClawAndroidVersionProperties.getProperty(name)?.trim()?.takeIf { it.isNotEmpty() }
|
||||
?: error("Missing $name in Config/Version.properties. Run `pnpm android:version:sync`.")
|
||||
|
||||
val openClawAndroidVersionName = requireOpenClawAndroidVersionProperty("OPENCLAW_ANDROID_VERSION_NAME")
|
||||
val openClawAndroidVersionCode =
|
||||
requireOpenClawAndroidVersionProperty("OPENCLAW_ANDROID_VERSION_CODE").toIntOrNull()
|
||||
?: error("OPENCLAW_ANDROID_VERSION_CODE must be an integer in Config/Version.properties.")
|
||||
|
||||
val androidStoreFile = providers.gradleProperty("OPENCLAW_ANDROID_STORE_FILE").orNull?.takeIf { it.isNotBlank() }
|
||||
val androidStorePassword = providers.gradleProperty("OPENCLAW_ANDROID_STORE_PASSWORD").orNull?.takeIf { it.isNotBlank() }
|
||||
@@ -65,8 +83,8 @@ android {
|
||||
applicationId = "ai.openclaw.app"
|
||||
minSdk = 31
|
||||
targetSdk = 36
|
||||
versionCode = 2026060201
|
||||
versionName = "2026.6.2"
|
||||
versionCode = openClawAndroidVersionCode
|
||||
versionName = openClawAndroidVersionName
|
||||
ndk {
|
||||
// Support all major ABIs — native libs are tiny (~47 KB per ABI)
|
||||
abiFilters += listOf("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"version": "2026.6.2",
|
||||
"versionCode": 2026060201
|
||||
}
|
||||
@@ -1443,6 +1443,10 @@
|
||||
"android:test": "node scripts/run-android-gradle.mjs :app:testPlayDebugUnitTest",
|
||||
"android:test:integration": "node scripts/run-with-env.mjs OPENCLAW_LIVE_TEST=1 OPENCLAW_LIVE_ANDROID_NODE=1 -- node scripts/run-vitest.mjs run --config test/vitest/vitest.live.config.ts src/gateway/android-node.capabilities.live.test.ts",
|
||||
"android:test:third-party": "node scripts/run-android-gradle.mjs :app:testThirdPartyDebugUnitTest",
|
||||
"android:version": "node --import tsx scripts/android-version.ts --json",
|
||||
"android:version:check": "node --import tsx scripts/android-sync-versioning.ts --check",
|
||||
"android:version:pin": "node --import tsx scripts/android-pin-version.ts",
|
||||
"android:version:sync": "node --import tsx scripts/android-sync-versioning.ts --write",
|
||||
"audit:seams": "node scripts/audit-seams.mjs",
|
||||
"build": "node scripts/build-all.mjs",
|
||||
"build:ci-artifacts": "node scripts/build-all.mjs ciArtifacts",
|
||||
|
||||
@@ -0,0 +1,188 @@
|
||||
// Android Pin Version script supports OpenClaw repository automation.
|
||||
import path from "node:path";
|
||||
import {
|
||||
canonicalAndroidVersionCode,
|
||||
normalizeAndroidVersionCode,
|
||||
normalizePinnedAndroidVersion,
|
||||
resolveAndroidVersion,
|
||||
resolveGatewayVersionForAndroidRelease,
|
||||
syncAndroidVersioning,
|
||||
writeAndroidVersionManifest,
|
||||
} from "./lib/android-version.ts";
|
||||
|
||||
type CliOptions = {
|
||||
explicitVersion: string | null;
|
||||
explicitVersionCode: number | null;
|
||||
fromGateway: boolean;
|
||||
rootDir: string;
|
||||
sync: boolean;
|
||||
};
|
||||
|
||||
export type PinAndroidVersionResult = {
|
||||
previousVersion: string | null;
|
||||
previousVersionCode: number | null;
|
||||
nextVersion: string;
|
||||
nextVersionCode: number;
|
||||
packageVersion: string | null;
|
||||
versionFilePath: string;
|
||||
syncedPaths: string[];
|
||||
};
|
||||
|
||||
function usage(): string {
|
||||
return [
|
||||
"Usage: node --import tsx scripts/android-pin-version.ts (--from-gateway | --version <YYYY.M.PATCH>) [--version-code <int>] [--no-sync] [--root dir]",
|
||||
"",
|
||||
"Examples:",
|
||||
" node --import tsx scripts/android-pin-version.ts --from-gateway",
|
||||
" node --import tsx scripts/android-pin-version.ts --version 2026.6.5",
|
||||
" node --import tsx scripts/android-pin-version.ts --version 2026.6.5 --version-code 2026060502",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
export function parseArgs(argv: string[]): CliOptions {
|
||||
let explicitVersion: string | null = null;
|
||||
let explicitVersionCode: number | null = null;
|
||||
let fromGateway = false;
|
||||
let rootDir = path.resolve(".");
|
||||
let sync = true;
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
switch (arg) {
|
||||
case "--from-gateway": {
|
||||
fromGateway = true;
|
||||
break;
|
||||
}
|
||||
case "--version": {
|
||||
explicitVersion = argv[index + 1] ?? null;
|
||||
index += 1;
|
||||
break;
|
||||
}
|
||||
case "--version-code": {
|
||||
const value = argv[index + 1];
|
||||
if (!value) {
|
||||
throw new Error("Missing value for --version-code.");
|
||||
}
|
||||
explicitVersionCode = Number.parseInt(value, 10);
|
||||
index += 1;
|
||||
break;
|
||||
}
|
||||
case "--no-sync": {
|
||||
sync = false;
|
||||
break;
|
||||
}
|
||||
case "--root": {
|
||||
const value = argv[index + 1];
|
||||
if (!value) {
|
||||
throw new Error("Missing value for --root.");
|
||||
}
|
||||
rootDir = path.resolve(value);
|
||||
index += 1;
|
||||
break;
|
||||
}
|
||||
case "-h":
|
||||
case "--help": {
|
||||
console.log(`${usage()}\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (fromGateway === (explicitVersion !== null)) {
|
||||
throw new Error("Choose exactly one of --from-gateway or --version <YYYY.M.PATCH>.");
|
||||
}
|
||||
|
||||
if (explicitVersion !== null && !explicitVersion.trim()) {
|
||||
throw new Error("Missing value for --version.");
|
||||
}
|
||||
|
||||
return { explicitVersion, explicitVersionCode, fromGateway, rootDir, sync };
|
||||
}
|
||||
|
||||
export function pinAndroidVersion(params: CliOptions): PinAndroidVersionResult {
|
||||
const rootDir = path.resolve(params.rootDir);
|
||||
let previousVersion: string | null;
|
||||
let previousVersionCode: number | null;
|
||||
try {
|
||||
const currentVersion = resolveAndroidVersion(rootDir);
|
||||
previousVersion = currentVersion.canonicalVersion;
|
||||
previousVersionCode = currentVersion.versionCode;
|
||||
} catch {
|
||||
previousVersion = null;
|
||||
previousVersionCode = null;
|
||||
}
|
||||
|
||||
const gatewayVersion = params.fromGateway
|
||||
? resolveGatewayVersionForAndroidRelease(rootDir)
|
||||
: null;
|
||||
const packageVersion = gatewayVersion?.packageVersion ?? null;
|
||||
const nextVersion =
|
||||
gatewayVersion?.pinnedAndroidVersion ??
|
||||
normalizePinnedAndroidVersion(params.explicitVersion ?? "");
|
||||
const nextVersionCode =
|
||||
params.explicitVersionCode === null
|
||||
? (gatewayVersion?.versionCode ?? canonicalAndroidVersionCode(nextVersion))
|
||||
: normalizeAndroidVersionCode(params.explicitVersionCode, nextVersion);
|
||||
const versionFilePath = writeAndroidVersionManifest(nextVersion, nextVersionCode, rootDir);
|
||||
const syncedPaths = params.sync
|
||||
? syncAndroidVersioning({ mode: "write", rootDir }).updatedPaths
|
||||
: [];
|
||||
|
||||
return {
|
||||
previousVersion,
|
||||
previousVersionCode,
|
||||
nextVersion,
|
||||
nextVersionCode,
|
||||
packageVersion,
|
||||
versionFilePath,
|
||||
syncedPaths,
|
||||
};
|
||||
}
|
||||
|
||||
export async function main(argv: string[]): Promise<number> {
|
||||
try {
|
||||
const options = parseArgs(argv);
|
||||
const result = pinAndroidVersion(options);
|
||||
const sourceText = result.packageVersion
|
||||
? ` from gateway version ${result.packageVersion}`
|
||||
: "";
|
||||
process.stdout.write(
|
||||
`Pinned Android version to ${result.nextVersion} (${result.nextVersionCode})${sourceText}.\n`,
|
||||
);
|
||||
if (
|
||||
result.previousVersion &&
|
||||
(result.previousVersion !== result.nextVersion ||
|
||||
result.previousVersionCode !== result.nextVersionCode)
|
||||
) {
|
||||
process.stdout.write(
|
||||
`Previous pinned Android version: ${result.previousVersion} (${result.previousVersionCode}).\n`,
|
||||
);
|
||||
}
|
||||
process.stdout.write(
|
||||
`Updated version manifest: ${path.relative(process.cwd(), result.versionFilePath)}\n`,
|
||||
);
|
||||
if (options.sync) {
|
||||
if (result.syncedPaths.length === 0) {
|
||||
process.stdout.write("Android versioning artifacts already up to date.\n");
|
||||
} else {
|
||||
process.stdout.write(
|
||||
`Updated Android versioning artifacts:\n- ${result.syncedPaths.map((filePath) => path.relative(process.cwd(), filePath)).join("\n- ")}\n`,
|
||||
);
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
} catch (error) {
|
||||
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
if (import.meta.url === `file://${process.argv[1]}`) {
|
||||
const exitCode = await main(process.argv.slice(2));
|
||||
if (exitCode !== 0) {
|
||||
process.exit(exitCode);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
// Android Sync Versioning script supports OpenClaw repository automation.
|
||||
import path from "node:path";
|
||||
import { syncAndroidVersioning } from "./lib/android-version.ts";
|
||||
|
||||
type Mode = "check" | "write";
|
||||
|
||||
export function parseArgs(argv: string[]): { mode: Mode; rootDir: string } {
|
||||
let mode: Mode = "write";
|
||||
let rootDir = path.resolve(".");
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
switch (arg) {
|
||||
case "--check": {
|
||||
mode = "check";
|
||||
break;
|
||||
}
|
||||
case "--write": {
|
||||
mode = "write";
|
||||
break;
|
||||
}
|
||||
case "--root": {
|
||||
const value = argv[index + 1];
|
||||
if (!value) {
|
||||
throw new Error("Missing value for --root.");
|
||||
}
|
||||
rootDir = path.resolve(value);
|
||||
index += 1;
|
||||
break;
|
||||
}
|
||||
case "-h":
|
||||
case "--help": {
|
||||
console.log(
|
||||
"Usage: node --import tsx scripts/android-sync-versioning.ts [--write|--check] [--root dir]",
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { mode, rootDir };
|
||||
}
|
||||
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const result = syncAndroidVersioning({ mode: options.mode, rootDir: options.rootDir });
|
||||
|
||||
if (options.mode === "check") {
|
||||
process.stdout.write("Android versioning artifacts are up to date.\n");
|
||||
} else if (result.updatedPaths.length === 0) {
|
||||
process.stdout.write("Android versioning artifacts already up to date.\n");
|
||||
} else {
|
||||
process.stdout.write(
|
||||
`Updated Android versioning artifacts:\n- ${result.updatedPaths.map((filePath) => path.relative(process.cwd(), filePath)).join("\n- ")}\n`,
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
// Android Version script supports OpenClaw repository automation.
|
||||
import path from "node:path";
|
||||
import { resolveAndroidVersion } from "./lib/android-version.ts";
|
||||
|
||||
type CliOptions = {
|
||||
field: string | null;
|
||||
format: "json" | "shell";
|
||||
rootDir: string;
|
||||
};
|
||||
|
||||
function parseArgs(argv: string[]): CliOptions {
|
||||
let field: string | null = null;
|
||||
let format: "json" | "shell" = "json";
|
||||
let rootDir = path.resolve(".");
|
||||
|
||||
for (let index = 0; index < argv.length; index += 1) {
|
||||
const arg = argv[index];
|
||||
switch (arg) {
|
||||
case "--field": {
|
||||
field = argv[index + 1] ?? null;
|
||||
index += 1;
|
||||
break;
|
||||
}
|
||||
case "--json": {
|
||||
format = "json";
|
||||
break;
|
||||
}
|
||||
case "--shell": {
|
||||
format = "shell";
|
||||
break;
|
||||
}
|
||||
case "--root": {
|
||||
const value = argv[index + 1];
|
||||
if (!value) {
|
||||
throw new Error("Missing value for --root.");
|
||||
}
|
||||
rootDir = path.resolve(value);
|
||||
index += 1;
|
||||
break;
|
||||
}
|
||||
case "-h":
|
||||
case "--help": {
|
||||
console.log(
|
||||
`Usage: node --import tsx scripts/android-version.ts [--json|--shell] [--field name] [--root dir]\n`,
|
||||
);
|
||||
process.exit(0);
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Unknown argument: ${arg}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { field, format, rootDir };
|
||||
}
|
||||
|
||||
const options = parseArgs(process.argv.slice(2));
|
||||
const version = resolveAndroidVersion(options.rootDir);
|
||||
|
||||
if (options.field) {
|
||||
const value = version[options.field as keyof typeof version];
|
||||
if (value === undefined) {
|
||||
throw new Error(`Unknown Android version field '${options.field}'.`);
|
||||
}
|
||||
process.stdout.write(`${value}\n`);
|
||||
process.exit(0);
|
||||
}
|
||||
|
||||
if (options.format === "shell") {
|
||||
process.stdout.write(
|
||||
[
|
||||
`OPENCLAW_ANDROID_VERSION_NAME=${version.canonicalVersion}`,
|
||||
`OPENCLAW_ANDROID_VERSION_CODE=${version.versionCode}`,
|
||||
].join("\n") + "\n",
|
||||
);
|
||||
} else {
|
||||
process.stdout.write(`${JSON.stringify(version, null, 2)}\n`);
|
||||
}
|
||||
@@ -30,7 +30,8 @@ const PUBLIC_EXTENSION_CONTRACT_RE =
|
||||
*/
|
||||
export const RELEASE_METADATA_PATHS = new Set([
|
||||
"CHANGELOG.md",
|
||||
"apps/android/app/build.gradle.kts",
|
||||
"apps/android/Config/Version.properties",
|
||||
"apps/android/version.json",
|
||||
"apps/ios/CHANGELOG.md",
|
||||
"apps/ios/Config/Version.xcconfig",
|
||||
"apps/ios/fastlane/metadata/en-US/release_notes.txt",
|
||||
|
||||
@@ -269,6 +269,7 @@ export function createChangedCheckPlan(result, options = {}) {
|
||||
? ["--staged"]
|
||||
: ["--base", options.base ?? "origin/main", "--head", options.head ?? "HEAD"]),
|
||||
]);
|
||||
add("Android version sync", ["android:version:check"]);
|
||||
add("iOS version sync", ["ios:version:check"]);
|
||||
add("config schema baseline", ["config:schema:check"]);
|
||||
add("config docs baseline", ["config:docs:check"]);
|
||||
|
||||
@@ -6,7 +6,8 @@ import path from "node:path";
|
||||
import { RELEASE_METADATA_PATHS } from "./changed-lanes.mjs";
|
||||
|
||||
const VERSION_ONLY_TEXT_PATHS = new Set([
|
||||
"apps/android/app/build.gradle.kts",
|
||||
"apps/android/Config/Version.properties",
|
||||
"apps/android/version.json",
|
||||
"apps/ios/Config/Version.xcconfig",
|
||||
"apps/ios/version.json",
|
||||
"apps/macos/Sources/OpenClaw/Resources/Info.plist",
|
||||
|
||||
@@ -0,0 +1,210 @@
|
||||
// Android Version script supports OpenClaw repository automation.
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
|
||||
const ANDROID_VERSION_FILE = "apps/android/version.json";
|
||||
const ANDROID_VERSION_PROPERTIES_FILE = "apps/android/Config/Version.properties";
|
||||
|
||||
const PINNED_ANDROID_VERSION_PATTERN = /^(\d{4}\.\d{1,2}\.[1-9]\d*)$/u;
|
||||
const GATEWAY_VERSION_PATTERN = /^(\d{4}\.\d{1,2}\.[1-9]\d*)(?:-(?:alpha\.\d+|beta\.\d+|\d+))?$/u;
|
||||
|
||||
type AndroidVersionManifest = {
|
||||
version: string;
|
||||
versionCode: number;
|
||||
};
|
||||
|
||||
export type ResolvedAndroidVersion = {
|
||||
canonicalVersion: string;
|
||||
versionCode: number;
|
||||
versionFilePath: string;
|
||||
versionPropertiesPath: string;
|
||||
};
|
||||
|
||||
type SyncAndroidVersioningMode = "check" | "write";
|
||||
|
||||
function normalizeTrailingNewline(value: string): string {
|
||||
return value.endsWith("\n") ? value : `${value}\n`;
|
||||
}
|
||||
|
||||
export function normalizePinnedAndroidVersion(rawVersion: string): string {
|
||||
const trimmed = rawVersion.trim();
|
||||
if (!trimmed) {
|
||||
throw new Error(`Missing Android version in ${ANDROID_VERSION_FILE}.`);
|
||||
}
|
||||
|
||||
const match = PINNED_ANDROID_VERSION_PATTERN.exec(trimmed);
|
||||
if (!match) {
|
||||
throw new Error(
|
||||
`Invalid Android version '${rawVersion}'. Expected pinned release version like 2026.6.5.`,
|
||||
);
|
||||
}
|
||||
|
||||
return match[1] ?? trimmed;
|
||||
}
|
||||
|
||||
export function normalizeGatewayVersionToPinnedAndroidVersion(rawVersion: string): string {
|
||||
const trimmed = rawVersion.trim().replace(/^v/u, "");
|
||||
if (!trimmed) {
|
||||
throw new Error("Missing root package.json version.");
|
||||
}
|
||||
|
||||
const match = GATEWAY_VERSION_PATTERN.exec(trimmed);
|
||||
if (!match) {
|
||||
throw new Error(
|
||||
`Invalid gateway version '${rawVersion}'. Expected YYYY.M.PATCH, YYYY.M.PATCH-alpha.N, YYYY.M.PATCH-beta.N, or YYYY.M.PATCH-N.`,
|
||||
);
|
||||
}
|
||||
|
||||
return match[1] ?? trimmed;
|
||||
}
|
||||
|
||||
export function canonicalAndroidVersionCode(version: string): number {
|
||||
const canonicalVersion = normalizePinnedAndroidVersion(version);
|
||||
const [year, rawMonth, rawPatch] = canonicalVersion.split(".");
|
||||
const month = rawMonth?.padStart(2, "0");
|
||||
const patch = rawPatch?.padStart(2, "0");
|
||||
const versionCode = Number.parseInt(`${year}${month}${patch}01`, 10);
|
||||
if (!Number.isInteger(versionCode)) {
|
||||
throw new Error(`Unable to derive Android versionCode from ${canonicalVersion}.`);
|
||||
}
|
||||
return versionCode;
|
||||
}
|
||||
|
||||
export function normalizeAndroidVersionCode(rawVersionCode: number, version: string): number {
|
||||
if (!Number.isInteger(rawVersionCode) || rawVersionCode <= 0 || rawVersionCode > 2_100_000_000) {
|
||||
throw new Error(
|
||||
`Invalid Android versionCode '${rawVersionCode}'. Expected a positive integer no greater than 2100000000.`,
|
||||
);
|
||||
}
|
||||
|
||||
const prefix = canonicalAndroidVersionCode(version).toString().slice(0, -2);
|
||||
const raw = rawVersionCode.toString();
|
||||
const suffix = Number.parseInt(raw.slice(prefix.length), 10);
|
||||
if (
|
||||
!raw.startsWith(prefix) ||
|
||||
raw.length !== prefix.length + 2 ||
|
||||
!Number.isInteger(suffix) ||
|
||||
suffix < 1 ||
|
||||
suffix > 99
|
||||
) {
|
||||
throw new Error(
|
||||
`Invalid Android versionCode '${rawVersionCode}'. Expected ${prefix}01 through ${prefix}99 for version ${version}.`,
|
||||
);
|
||||
}
|
||||
|
||||
return rawVersionCode;
|
||||
}
|
||||
|
||||
function readRootPackageVersion(rootDir = path.resolve(".")): string {
|
||||
const packageJsonPath = path.join(rootDir, "package.json");
|
||||
const parsed = JSON.parse(readFileSync(packageJsonPath, "utf8")) as { version?: unknown };
|
||||
const version = typeof parsed.version === "string" ? parsed.version.trim() : "";
|
||||
if (!version) {
|
||||
throw new Error(`Missing package.json version in ${packageJsonPath}.`);
|
||||
}
|
||||
return version;
|
||||
}
|
||||
|
||||
export function resolveGatewayVersionForAndroidRelease(rootDir = path.resolve(".")): {
|
||||
packageVersion: string;
|
||||
pinnedAndroidVersion: string;
|
||||
versionCode: number;
|
||||
} {
|
||||
const packageVersion = readRootPackageVersion(rootDir);
|
||||
const pinnedAndroidVersion = normalizeGatewayVersionToPinnedAndroidVersion(packageVersion);
|
||||
return {
|
||||
packageVersion,
|
||||
pinnedAndroidVersion,
|
||||
versionCode: canonicalAndroidVersionCode(pinnedAndroidVersion),
|
||||
};
|
||||
}
|
||||
|
||||
function readAndroidVersionManifest(rootDir = path.resolve(".")): AndroidVersionManifest {
|
||||
const versionFilePath = path.join(rootDir, ANDROID_VERSION_FILE);
|
||||
return JSON.parse(readFileSync(versionFilePath, "utf8")) as AndroidVersionManifest;
|
||||
}
|
||||
|
||||
export function writeAndroidVersionManifest(
|
||||
version: string,
|
||||
versionCode: number | null,
|
||||
rootDir = path.resolve("."),
|
||||
): string {
|
||||
const versionFilePath = path.join(rootDir, ANDROID_VERSION_FILE);
|
||||
const normalizedVersion = normalizePinnedAndroidVersion(version);
|
||||
const normalizedVersionCode = normalizeAndroidVersionCode(
|
||||
versionCode ?? canonicalAndroidVersionCode(normalizedVersion),
|
||||
normalizedVersion,
|
||||
);
|
||||
const nextContent = `${JSON.stringify(
|
||||
{ version: normalizedVersion, versionCode: normalizedVersionCode },
|
||||
null,
|
||||
2,
|
||||
)}\n`;
|
||||
writeFileSync(versionFilePath, nextContent, "utf8");
|
||||
return versionFilePath;
|
||||
}
|
||||
|
||||
export function resolveAndroidVersion(rootDir = path.resolve(".")): ResolvedAndroidVersion {
|
||||
const versionFilePath = path.join(rootDir, ANDROID_VERSION_FILE);
|
||||
const versionPropertiesPath = path.join(rootDir, ANDROID_VERSION_PROPERTIES_FILE);
|
||||
const manifest = readAndroidVersionManifest(rootDir);
|
||||
const canonicalVersion = normalizePinnedAndroidVersion(manifest.version ?? "");
|
||||
const versionCode = normalizeAndroidVersionCode(manifest.versionCode, canonicalVersion);
|
||||
|
||||
return {
|
||||
canonicalVersion,
|
||||
versionCode,
|
||||
versionFilePath,
|
||||
versionPropertiesPath,
|
||||
};
|
||||
}
|
||||
|
||||
export function renderAndroidVersionProperties(version: ResolvedAndroidVersion): string {
|
||||
return `# Shared Android version defaults.\n# Source of truth: apps/android/version.json\n# Generated by scripts/android-sync-versioning.ts.\n\nOPENCLAW_ANDROID_VERSION_NAME=${version.canonicalVersion}\nOPENCLAW_ANDROID_VERSION_CODE=${version.versionCode}\n`;
|
||||
}
|
||||
|
||||
function syncFile(params: {
|
||||
mode: SyncAndroidVersioningMode;
|
||||
path: string;
|
||||
nextContent: string;
|
||||
label: string;
|
||||
}): boolean {
|
||||
const nextContent = normalizeTrailingNewline(params.nextContent);
|
||||
const currentContent = readFileSync(params.path, "utf8");
|
||||
if (currentContent === nextContent) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (params.mode === "check") {
|
||||
throw new Error(`${params.label} is stale: ${path.relative(process.cwd(), params.path)}`);
|
||||
}
|
||||
|
||||
writeFileSync(params.path, nextContent, "utf8");
|
||||
return true;
|
||||
}
|
||||
|
||||
export function syncAndroidVersioning(params?: {
|
||||
mode?: SyncAndroidVersioningMode;
|
||||
rootDir?: string;
|
||||
}): {
|
||||
updatedPaths: string[];
|
||||
} {
|
||||
const mode = params?.mode ?? "write";
|
||||
const rootDir = path.resolve(params?.rootDir ?? ".");
|
||||
const version = resolveAndroidVersion(rootDir);
|
||||
const nextVersionProperties = renderAndroidVersionProperties(version);
|
||||
const updatedPaths: string[] = [];
|
||||
|
||||
if (
|
||||
syncFile({
|
||||
mode,
|
||||
path: version.versionPropertiesPath,
|
||||
nextContent: nextVersionProperties,
|
||||
label: "Android version properties",
|
||||
})
|
||||
) {
|
||||
updatedPaths.push(version.versionPropertiesPath);
|
||||
}
|
||||
|
||||
return { updatedPaths };
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
// Android Pin Version tests cover android pin version script behavior.
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { parseArgs, pinAndroidVersion } from "../../scripts/android-pin-version.ts";
|
||||
import { resolveAndroidVersion } from "../../scripts/lib/android-version.ts";
|
||||
import {
|
||||
installAndroidFixtureCleanup,
|
||||
writeAndroidFixture,
|
||||
} from "./android-version.test-support.ts";
|
||||
|
||||
installAndroidFixtureCleanup();
|
||||
|
||||
describe("parseArgs", () => {
|
||||
it("requires exactly one pin source", () => {
|
||||
expect(() => parseArgs([])).toThrow(
|
||||
"Choose exactly one of --from-gateway or --version <YYYY.M.PATCH>",
|
||||
);
|
||||
expect(() => parseArgs(["--from-gateway", "--version", "2026.6.5"])).toThrow(
|
||||
"Choose exactly one of --from-gateway or --version <YYYY.M.PATCH>",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("pinAndroidVersion", () => {
|
||||
it("pins an explicit Android release version and syncs generated artifacts", () => {
|
||||
const rootDir = writeAndroidFixture({
|
||||
version: "2026.6.2",
|
||||
versionCode: 2026060201,
|
||||
prefix: "openclaw-android-pin-",
|
||||
});
|
||||
|
||||
const result = pinAndroidVersion({
|
||||
explicitVersion: "2026.6.5",
|
||||
explicitVersionCode: null,
|
||||
fromGateway: false,
|
||||
rootDir,
|
||||
sync: true,
|
||||
});
|
||||
|
||||
expect(result.previousVersion).toBe("2026.6.2");
|
||||
expect(result.previousVersionCode).toBe(2026060201);
|
||||
expect(result.nextVersion).toBe("2026.6.5");
|
||||
expect(result.nextVersionCode).toBe(2026060501);
|
||||
expect(result.packageVersion).toBeNull();
|
||||
expect(resolveAndroidVersion(rootDir).canonicalVersion).toBe("2026.6.5");
|
||||
expect(
|
||||
fs.readFileSync(path.join(rootDir, "apps", "android", "version.json"), "utf8"),
|
||||
).toContain('"versionCode": 2026060501');
|
||||
expect(
|
||||
fs.readFileSync(
|
||||
path.join(rootDir, "apps", "android", "Config", "Version.properties"),
|
||||
"utf8",
|
||||
),
|
||||
).toContain("OPENCLAW_ANDROID_VERSION_NAME=2026.6.5");
|
||||
expect(result.syncedPaths).toHaveLength(1);
|
||||
});
|
||||
|
||||
it("pins from the current gateway version without carrying prerelease suffixes", () => {
|
||||
const rootDir = writeAndroidFixture({
|
||||
version: "2026.6.2",
|
||||
versionCode: 2026060201,
|
||||
packageVersion: "2026.6.5-beta.3",
|
||||
prefix: "openclaw-android-pin-",
|
||||
});
|
||||
|
||||
const result = pinAndroidVersion({
|
||||
explicitVersion: null,
|
||||
explicitVersionCode: null,
|
||||
fromGateway: true,
|
||||
rootDir,
|
||||
sync: true,
|
||||
});
|
||||
|
||||
expect(result.nextVersion).toBe("2026.6.5");
|
||||
expect(result.nextVersionCode).toBe(2026060501);
|
||||
expect(result.packageVersion).toBe("2026.6.5-beta.3");
|
||||
expect(resolveAndroidVersion(rootDir).canonicalVersion).toBe("2026.6.5");
|
||||
});
|
||||
|
||||
it("allows explicit versionCode increments for another build on the same train", () => {
|
||||
const rootDir = writeAndroidFixture({
|
||||
version: "2026.6.2",
|
||||
versionCode: 2026060201,
|
||||
prefix: "openclaw-android-pin-",
|
||||
});
|
||||
|
||||
const result = pinAndroidVersion({
|
||||
explicitVersion: "2026.6.2",
|
||||
explicitVersionCode: 2026060202,
|
||||
fromGateway: false,
|
||||
rootDir,
|
||||
sync: true,
|
||||
});
|
||||
|
||||
expect(result.nextVersion).toBe("2026.6.2");
|
||||
expect(result.nextVersionCode).toBe(2026060202);
|
||||
expect(resolveAndroidVersion(rootDir).versionCode).toBe(2026060202);
|
||||
});
|
||||
|
||||
it("can skip syncing checked-in artifacts when requested", () => {
|
||||
const rootDir = writeAndroidFixture({
|
||||
version: "2026.6.2",
|
||||
versionCode: 2026060201,
|
||||
versionProperties: "stale\n",
|
||||
prefix: "openclaw-android-pin-",
|
||||
});
|
||||
|
||||
const result = pinAndroidVersion({
|
||||
explicitVersion: "2026.6.5",
|
||||
explicitVersionCode: null,
|
||||
fromGateway: false,
|
||||
rootDir,
|
||||
sync: false,
|
||||
});
|
||||
|
||||
expect(result.syncedPaths).toHaveLength(0);
|
||||
expect(
|
||||
fs.readFileSync(
|
||||
path.join(rootDir, "apps", "android", "Config", "Version.properties"),
|
||||
"utf8",
|
||||
),
|
||||
).toBe("stale\n");
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,40 @@
|
||||
// Android version test support provides shared fixtures for Android script tests.
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { afterEach } from "vitest";
|
||||
import { cleanupTempDirs, makeTempDir } from "../helpers/temp-dir.js";
|
||||
|
||||
const tempDirs: string[] = [];
|
||||
|
||||
export function installAndroidFixtureCleanup(): void {
|
||||
afterEach(() => {
|
||||
cleanupTempDirs(tempDirs);
|
||||
});
|
||||
}
|
||||
|
||||
export function writeAndroidFixture(params: {
|
||||
version: string;
|
||||
versionCode: number;
|
||||
packageVersion?: string;
|
||||
versionProperties?: string;
|
||||
prefix?: string;
|
||||
}): string {
|
||||
const rootDir = makeTempDir(tempDirs, params.prefix ?? "openclaw-android-version-");
|
||||
fs.mkdirSync(path.join(rootDir, "apps", "android", "Config"), { recursive: true });
|
||||
fs.writeFileSync(
|
||||
path.join(rootDir, "package.json"),
|
||||
`${JSON.stringify({ version: params.packageVersion ?? "2026.6.2" }, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(rootDir, "apps", "android", "version.json"),
|
||||
`${JSON.stringify({ version: params.version, versionCode: params.versionCode }, null, 2)}\n`,
|
||||
"utf8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(rootDir, "apps", "android", "Config", "Version.properties"),
|
||||
params.versionProperties ?? "",
|
||||
"utf8",
|
||||
);
|
||||
return rootDir;
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// Android Version tests cover android version script behavior.
|
||||
import path from "node:path";
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
canonicalAndroidVersionCode,
|
||||
normalizeGatewayVersionToPinnedAndroidVersion,
|
||||
renderAndroidVersionProperties,
|
||||
resolveAndroidVersion,
|
||||
resolveGatewayVersionForAndroidRelease,
|
||||
} from "../../scripts/lib/android-version.ts";
|
||||
import {
|
||||
installAndroidFixtureCleanup,
|
||||
writeAndroidFixture,
|
||||
} from "./android-version.test-support.ts";
|
||||
|
||||
installAndroidFixtureCleanup();
|
||||
|
||||
describe("resolveAndroidVersion", () => {
|
||||
it("parses pinned release versions and Android version codes", () => {
|
||||
const rootDir = writeAndroidFixture({
|
||||
version: "2026.6.2",
|
||||
versionCode: 2026060201,
|
||||
});
|
||||
|
||||
expect(resolveAndroidVersion(rootDir)).toEqual({
|
||||
canonicalVersion: "2026.6.2",
|
||||
versionCode: 2026060201,
|
||||
versionFilePath: path.join(rootDir, "apps/android/version.json"),
|
||||
versionPropertiesPath: path.join(rootDir, "apps/android/Config/Version.properties"),
|
||||
});
|
||||
});
|
||||
|
||||
it("rejects semver-only versions", () => {
|
||||
const rootDir = writeAndroidFixture({
|
||||
version: "1.2.3",
|
||||
versionCode: 2026060201,
|
||||
});
|
||||
|
||||
expect(() => resolveAndroidVersion(rootDir)).toThrow(
|
||||
"Expected pinned release version like 2026.6.5",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects prerelease suffixes in the pinned Android version file", () => {
|
||||
const rootDir = writeAndroidFixture({
|
||||
version: "2026.6.2-beta.1",
|
||||
versionCode: 2026060201,
|
||||
});
|
||||
|
||||
expect(() => resolveAndroidVersion(rootDir)).toThrow(
|
||||
"Expected pinned release version like 2026.6.5",
|
||||
);
|
||||
});
|
||||
|
||||
it("rejects version codes that do not match the pinned version date", () => {
|
||||
const rootDir = writeAndroidFixture({
|
||||
version: "2026.6.2",
|
||||
versionCode: 2026060301,
|
||||
});
|
||||
|
||||
expect(() => resolveAndroidVersion(rootDir)).toThrow(
|
||||
"Expected 2026060201 through 2026060299 for version 2026.6.2",
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("gateway version normalization", () => {
|
||||
it("keeps stable gateway release values", () => {
|
||||
expect(normalizeGatewayVersionToPinnedAndroidVersion("2026.6.2")).toBe("2026.6.2");
|
||||
});
|
||||
|
||||
it("strips prerelease suffixes when pinning from gateway version", () => {
|
||||
expect(normalizeGatewayVersionToPinnedAndroidVersion("2026.6.2-beta.3")).toBe("2026.6.2");
|
||||
expect(normalizeGatewayVersionToPinnedAndroidVersion("2026.6.2-alpha.1")).toBe("2026.6.2");
|
||||
});
|
||||
|
||||
it("derives the default Play-compatible versionCode from the pinned version", () => {
|
||||
expect(canonicalAndroidVersionCode("2026.6.2")).toBe(2026060201);
|
||||
});
|
||||
|
||||
it("reads and normalizes the root package version for Android releases", () => {
|
||||
const rootDir = writeAndroidFixture({
|
||||
version: "2026.6.2",
|
||||
versionCode: 2026060201,
|
||||
packageVersion: "2026.6.5-beta.3",
|
||||
});
|
||||
|
||||
expect(resolveGatewayVersionForAndroidRelease(rootDir)).toEqual({
|
||||
packageVersion: "2026.6.5-beta.3",
|
||||
pinnedAndroidVersion: "2026.6.5",
|
||||
versionCode: 2026060501,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("renderAndroidVersionProperties", () => {
|
||||
it("renders checked-in defaults from the pinned Android version", () => {
|
||||
const rootDir = writeAndroidFixture({
|
||||
version: "2026.6.2",
|
||||
versionCode: 2026060201,
|
||||
});
|
||||
const version = resolveAndroidVersion(rootDir);
|
||||
|
||||
expect(renderAndroidVersionProperties(version)).toContain(
|
||||
"OPENCLAW_ANDROID_VERSION_NAME=2026.6.2",
|
||||
);
|
||||
expect(renderAndroidVersionProperties(version)).toContain(
|
||||
"OPENCLAW_ANDROID_VERSION_CODE=2026060201",
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -1172,7 +1172,8 @@ describe("scripts/changed-lanes", () => {
|
||||
it("keeps release metadata commits off the full changed gate", () => {
|
||||
const result = detectChangedLanes([
|
||||
"CHANGELOG.md",
|
||||
"apps/android/app/build.gradle.kts",
|
||||
"apps/android/Config/Version.properties",
|
||||
"apps/android/version.json",
|
||||
"apps/ios/CHANGELOG.md",
|
||||
"apps/ios/Config/Version.xcconfig",
|
||||
"apps/ios/fastlane/metadata/en-US/release_notes.txt",
|
||||
@@ -1197,6 +1198,7 @@ describe("scripts/changed-lanes", () => {
|
||||
"scripts/generate-npm-shrinkwrap.mjs",
|
||||
"deps:patches:check",
|
||||
"release-metadata:check",
|
||||
"android:version:check",
|
||||
"ios:version:check",
|
||||
"config:schema:check",
|
||||
"config:docs:check",
|
||||
|
||||
Reference in New Issue
Block a user