Files
openclaw/test/scripts/android-release-artifacts.test.ts
Peter Steinberger 054e3675e1 feat(android): publish signed APKs with stable releases (#101212)
* feat(android): publish signed release APKs

* fix(android): preserve fallback corrections

* test(android): isolate APK signer fixtures

* docs: refresh generated docs map
2026-07-07 00:39:59 +01:00

92 lines
3.3 KiB
TypeScript

import { spawnSync } from "node:child_process";
import fs from "node:fs";
import path from "node:path";
import { afterEach } from "vitest";
import { describe, expect, it } from "vitest";
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
const SCRIPT = "apps/android/scripts/build-release-artifacts.ts";
const APK_CERTIFICATE_SHA256 = "80dbc62315ea216dd6e8a7060735a866ddc464a48ed50fef29ff0550468b9a63";
const tempRoots = useAutoCleanupTempDirTracker(afterEach);
function run(args: string[], env: NodeJS.ProcessEnv = {}) {
return spawnSync(process.execPath, ["--import", "tsx", SCRIPT, ...args], {
cwd: process.cwd(),
encoding: "utf8",
env: { ...process.env, ...env },
});
}
function fakeApkSigner(certificateSha256: string, signerCount = 1) {
const tempRoot = tempRoots.make("openclaw-apksigner-");
const buildToolsDir = path.join(tempRoot, "build-tools", "36.0.0");
fs.mkdirSync(buildToolsDir, { recursive: true });
const apkSignerPath = path.join(buildToolsDir, "apksigner");
const signerLines = Array.from(
{ length: signerCount },
(_, index) => `Signer #${index + 1} certificate SHA-256 digest: ${certificateSha256}`,
);
fs.writeFileSync(
apkSignerPath,
`#!/bin/sh\nprintf '%s\\n' ${signerLines.map((line) => `'${line}'`).join(" ")}\n`,
);
fs.chmodSync(apkSignerPath, 0o755);
const apkPath = path.join(tempRoot, "OpenClaw-Android.apk");
fs.writeFileSync(apkPath, "fake apk bytes");
return { apkPath, sdkRoot: tempRoot };
}
describe("Android release artifacts", () => {
it("selects only the signed third-party APK for GitHub distribution", () => {
const result = run(["--artifact", "third-party", "--dry-run"]);
expect(result.status).toBe(0);
expect(result.stdout).toContain("Release artifact: third-party apk");
expect(result.stdout).toContain("Gradle task: :app:assembleThirdPartyRelease");
expect(result.stdout).not.toContain("Release artifact: play aab");
});
it("rejects unknown artifact selectors", () => {
const result = run(["--artifact", "debug", "--dry-run"]);
expect(result.status).toBe(1);
expect(result.stderr).toContain("--artifact must be one of: all, play, third-party");
});
it("accepts the pinned standalone APK signing certificate", () => {
const { apkPath, sdkRoot } = fakeApkSigner(APK_CERTIFICATE_SHA256);
const result = run(["--verify-apk", apkPath], {
ANDROID_HOME: sdkRoot,
ANDROID_SDK_ROOT: sdkRoot,
});
expect(result.status).toBe(0);
expect(result.stdout).toContain("Verified pinned APK signing certificate");
});
it("rejects an APK signed by another certificate", () => {
const { apkPath, sdkRoot } = fakeApkSigner("a".repeat(64));
const result = run(["--verify-apk", apkPath], {
ANDROID_HOME: sdkRoot,
ANDROID_SDK_ROOT: sdkRoot,
});
expect(result.status).toBe(1);
expect(result.stderr).toContain("APK signing certificate mismatch");
});
it("rejects APKs with multiple signers", () => {
const { apkPath, sdkRoot } = fakeApkSigner(APK_CERTIFICATE_SHA256, 2);
const result = run(["--verify-apk", apkPath], {
ANDROID_HOME: sdkRoot,
ANDROID_SDK_ROOT: sdkRoot,
});
expect(result.status).toBe(1);
expect(result.stderr).toContain("Expected exactly one SHA-256 signing certificate");
});
});