#!/usr/bin/env bun
import { $ } from "bun" ;
import { dirname, join } from "node:path" ;
import { fileURLToPath } from "node:url" ;
const scriptDir = dirname(fileURLToPath(import .meta.url));
const androidDir = join(scriptDir, ".." );
const buildGradlePath = join(androidDir, "app" , "build.gradle.kts" );
const releaseOutputDir = join(androidDir, "build" , "release-bundles" );
const releaseVariants = [
{
flavorName: "play" ,
gradleTask: ":app:bundlePlayRelease" ,
bundlePath: join(androidDir, "app" , "build" , "outputs" , "bundle" , "playRelease" , "app-play-release.aab" ),
},
{
flavorName: "third-party" ,
gradleTask: ":app:bundleThirdPartyRelease" ,
bundlePath: join(
androidDir,
"app" ,
"build" ,
"outputs" ,
"bundle" ,
"thirdPartyRelease" ,
"app-thirdParty-release.aab" ,
),
},
] as const ;
type VersionState = {
versionName: string;
versionCode: number;
};
type ParsedVersionMatches = {
versionNameMatch: RegExpMatchArray;
versionCodeMatch: RegExpMatchArray;
};
function formatVersionName(date: Date): string {
const year = date.getFullYear();
const month = date.getMonth() + 1 ;
const day = date.getDate();
return `${year}.${month}.${day}`;
}
function formatVersionCodePrefix(date: Date): string {
const year = date.getFullYear().toString();
const month = (date.getMonth() + 1 ).toString().padStart(2 , "0" );
const day = date.getDate().toString().padStart(2 , "0" );
return `${year}${month}${day}`;
}
function parseVersionMatches(buildGradleText: string): ParsedVersionMatches {
const versionCodeMatch = buildGradleText.match(/versionCode = (\d+)/);
const versionNameMatch = buildGradleText.match(/versionName = "([^" ]+)"/);
if (!versionCodeMatch || !versionNameMatch) {
throw new Error(`Couldn't parse versionName/versionCode from ${buildGradlePath}`);
}
return { versionCodeMatch, versionNameMatch };
}
function resolveNextVersionCode(currentVersionCode: number, todayPrefix: string): number {
const currentRaw = currentVersionCode.toString();
let nextSuffix = 0 ;
if (currentRaw.startsWith(todayPrefix)) {
const suffixRaw = currentRaw.slice(todayPrefix.length);
nextSuffix = (suffixRaw ? Number.parseInt(suffixRaw, 10 ) : 0 ) + 1 ;
}
if (!Number.isInteger(nextSuffix) || nextSuffix < 0 || nextSuffix > 99 ) {
throw new Error(
`Can't auto-bump Android versionCode for ${todayPrefix}: next suffix ${nextSuffix} is invalid`,
);
}
return Number.parseInt(`${todayPrefix}${nextSuffix.toString().padStart(2 , "0" )}`, 10 );
}
function resolveNextVersion(buildGradleText: string, date: Date): VersionState {
const { versionCodeMatch } = parseVersionMatches(buildGradleText);
const currentVersionCode = Number.parseInt(versionCodeMatch[1 ] ?? "" , 10 );
if (!Number.isInteger(currentVersionCode)) {
throw new Error(`Invalid Android versionCode in ${buildGradlePath}`);
}
const versionName = formatVersionName(date);
const versionCode = resolveNextVersionCode(currentVersionCode, formatVersionCodePrefix(date));
return { versionName, versionCode };
}
function updateBuildGradleVersions(buildGradleText: string, nextVersion: VersionState): string {
return buildGradleText
.replace(/versionCode = \d+/, `versionCode = ${nextVersion.versionCode}`)
.replace(/versionName = "[^" ]+"/, `versionName = " ${nextVersion.versionName}"`);
}
async function sha256Hex(path: string): Promise<string> {
const buffer = await Bun.file(path).arrayBuffer();
const digest = await crypto.subtle.digest("SHA-256" , buffer);
return Array.from(new Uint8Array(digest), (byte ) => byte .toString(16 ).padStart(2 , "0" )).join("" );
}
async function verifyBundleSignature(path: string): Promise<void > {
await $`jarsigner -verify ${path}`.quiet();
}
async function copyBundle(sourcePath: string, destinationPath: string): Promise<void > {
const sourceFile = Bun.file(sourcePath);
if (!(await sourceFile.exists())) {
throw new Error(`Signed bundle missing at ${sourcePath}`);
}
await Bun.write(destinationPath, sourceFile);
}
async function main() {
const buildGradleFile = Bun.file(buildGradlePath);
const originalText = await buildGradleFile.text();
const nextVersion = resolveNextVersion(originalText, new Date());
const updatedText = updateBuildGradleVersions(originalText, nextVersion);
if (updatedText === originalText) {
throw new Error("Android version bump produced no change" );
}
console.log(`Android versionName -> ${nextVersion.versionName}`);
console.log(`Android versionCode -> ${nextVersion.versionCode}`);
await Bun.write(buildGradlePath, updatedText);
await $`mkdir -p ${releaseOutputDir}`;
try {
await $`./gradlew ${releaseVariants[0 ].gradleTask} ${releaseVariants[1 ].gradleTask}`.cwd(androidDir);
} catch (error) {
await Bun.write(buildGradlePath, originalText);
throw error;
}
for (const variant of releaseVariants) {
const outputPath = join(
releaseOutputDir,
`openclaw-${nextVersion.versionName}-${variant.flavorName}-release.aab`,
);
await copyBundle(variant.bundlePath, outputPath);
await verifyBundleSignature(outputPath);
const hash = await sha256Hex(outputPath);
console.log(`Signed AAB (${variant.flavorName}): ${outputPath}`);
console.log(`SHA-256 (${variant.flavorName}): ${hash}`);
}
}
await main();
Messung V0.5 in Prozent C=99 H=97 G=97
¤ Dauer der Verarbeitung: 0.9 Sekunden
(vorverarbeitet am 2026-06-10)
¤
*© Formatika GbR, Deutschland