Files
adolf/test/scripts/android-release-signing.test.ts
alvis bedb527145
Some checks failed
ClawSweeper Dispatch / dispatch (push) Has been cancelled
CodeQL / Security High (actions) (push) Has been cancelled
CodeQL / Security High (channel-runtime-boundary) (push) Has been cancelled
CodeQL / Security High (core-auth-secrets) (push) Has been cancelled
CodeQL / Security High (mcp-process-tool-boundary) (push) Has been cancelled
CodeQL / Security High (network-ssrf-boundary) (push) Has been cancelled
CodeQL / Security High (plugin-trust-boundary) (push) Has been cancelled
CodeQL / Security High (process-exec-boundary) (push) Has been cancelled
Docs Sync Publish Repo / sync-publish-repo (push) Has been cancelled
Docs / docs (push) Has been cancelled
OpenClaw Stable Main Closeout / Resolve stable release closeout inputs (push) Has been cancelled
OpenClaw Stable Main Closeout / Verify stable main closeout (push) Has been cancelled
Workflow Sanity / no-tabs (push) Has been cancelled
Workflow Sanity / actionlint (push) Has been cancelled
Workflow Sanity / generated-doc-baselines (push) Has been cancelled
CI / runner-admission (push) Has been cancelled
CI / preflight (push) Has been cancelled
CI / security-fast (push) Has been cancelled
CI / pnpm-store-warmup (push) Has been cancelled
CI / build-artifacts (push) Has been cancelled
CI / native-i18n (push) Has been cancelled
CI / ${{ matrix.check_name }} (push) Has been cancelled
CI / ${{ matrix.checkName }} (push) Has been cancelled
CI / checks-node-compat-node22 (push) Has been cancelled
CI / check-bundled-channel-config-metadata (push) Has been cancelled
CI / check-dependencies (push) Has been cancelled
CI / check-guards (push) Has been cancelled
CI / check-lint (push) Has been cancelled
CI / check-prod-types (push) Has been cancelled
CI / check-shrinkwrap (push) Has been cancelled
CI / check-test-types (push) Has been cancelled
CI / check-additional-boundaries-a (push) Has been cancelled
CI / check-additional-boundaries-bcd (push) Has been cancelled
CI / check-additional-extension-bundled (push) Has been cancelled
CI / check-additional-extension-channels (push) Has been cancelled
CI / check-additional-extension-package-boundary (push) Has been cancelled
CI / check-additional-runtime-topology-architecture (push) Has been cancelled
CI / check-session-accessor-boundary (push) Has been cancelled
CI / check-session-transcript-reader-boundary (push) Has been cancelled
CI / check-docs (push) Has been cancelled
CI / skills-python (push) Has been cancelled
CI / macos-swift (push) Has been cancelled
CI / ios-build (push) Has been cancelled
CI / ci-timings-summary (push) Has been cancelled
Native App Locale Refresh / Refresh native fa (push) Has been cancelled
Native App Locale Refresh / Refresh native fr (push) Has been cancelled
Native App Locale Refresh / Refresh native hi (push) Has been cancelled
Native App Locale Refresh / Refresh native id (push) Has been cancelled
Native App Locale Refresh / Refresh native it (push) Has been cancelled
Native App Locale Refresh / Refresh native ja-JP (push) Has been cancelled
Control UI Locale Refresh / plan (push) Has been cancelled
Control UI Locale Refresh / Refresh ${{ matrix.locale }} (push) Has been cancelled
Control UI Locale Refresh / Commit control UI locale refresh (push) Has been cancelled
Live Media Runner Image / Build live media runner image (push) Has been cancelled
Native App Locale Refresh / Refresh native ar (push) Has been cancelled
Native App Locale Refresh / Refresh native de (push) Has been cancelled
Native App Locale Refresh / Refresh native es (push) Has been cancelled
Native App Locale Refresh / Refresh native ko (push) Has been cancelled
Native App Locale Refresh / Refresh native nl (push) Has been cancelled
Native App Locale Refresh / Refresh native pl (push) Has been cancelled
Native App Locale Refresh / Refresh native pt-BR (push) Has been cancelled
Native App Locale Refresh / Refresh native ru (push) Has been cancelled
Native App Locale Refresh / Refresh native sv (push) Has been cancelled
Native App Locale Refresh / Refresh native th (push) Has been cancelled
Native App Locale Refresh / Refresh native tr (push) Has been cancelled
Native App Locale Refresh / Refresh native uk (push) Has been cancelled
Native App Locale Refresh / Refresh native vi (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-CN (push) Has been cancelled
Native App Locale Refresh / Refresh native zh-TW (push) Has been cancelled
Native App Locale Refresh / Commit native locale refresh (push) Has been cancelled
Plugin Init Scaffold Validation / Validate provider scaffold (push) Has been cancelled
Plugin NPM Release / preview_plugins_npm (push) Has been cancelled
Plugin NPM Release / Validate release publish approval (push) Has been cancelled
Plugin NPM Release / preview_plugin_pack (push) Has been cancelled
Plugin NPM Release / publish_plugins_npm (push) Has been cancelled
Sandbox Common Smoke / sandbox-common-smoke (push) Has been cancelled
Website Installer Sync / static (push) Has been cancelled
Website Installer Sync / linux-docker (push) Has been cancelled
Website Installer Sync / macos-installer (push) Has been cancelled
Website Installer Sync / windows-installer (push) Has been cancelled
Website Installer Sync / sync-website (push) Has been cancelled
Vendor OpenClaw source as Adolf fork baseline
Adolf is a fork/vendored clone of github.com/openclaw/openclaw (v2026.6.11),
free to diverge. Tree copied sans upstream .git; upstream remote added for
future syncs. Node pinned to 24 (.nvmrc); engines already require >=22.19.
Preserves docs/ARCHITECTURE.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LeqyaxJF2nbRXJtae2kNB2
2026-07-05 09:36:54 +00:00

298 lines
9.5 KiB
TypeScript

// Android release signing tests cover encrypted signing asset sync and local materialization.
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterEach, describe, expect, it } from "vitest";
const SCRIPT = path.join(process.cwd(), "scripts", "android-release-signing.mjs");
const MATCH_PASSWORD = "test-match-password";
const STORE_PASSWORD = "store_secret_value";
const KEY_PASSWORD = "key_secret_value";
const tempRoots: string[] = [];
function makeTempRoot(): string {
const tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-android-signing-"));
tempRoots.push(tempRoot);
return tempRoot;
}
function runNode(args: string[], env: NodeJS.ProcessEnv = {}) {
try {
const stdout = execFileSync(process.execPath, [SCRIPT, ...args], {
env: { ...process.env, ...env },
encoding: "utf8",
stdio: ["ignore", "pipe", "pipe"],
});
return { ok: true, stdout, stderr: "" };
} catch (error) {
const e = error as { stdout?: unknown; stderr?: unknown };
return {
ok: false,
stdout: formatProcessOutput(e.stdout),
stderr: formatProcessOutput(e.stderr),
};
}
}
function formatProcessOutput(value: unknown): string {
if (Buffer.isBuffer(value)) {
return value.toString("utf8");
}
return typeof value === "string" ? value : "";
}
function runGit(args: string[], cwd?: string, env: NodeJS.ProcessEnv = {}) {
execFileSync("git", args, {
cwd,
env: {
...process.env,
GIT_AUTHOR_NAME: "OpenClaw Test",
GIT_AUTHOR_EMAIL: "test@example.com",
GIT_COMMITTER_NAME: "OpenClaw Test",
GIT_COMMITTER_EMAIL: "test@example.com",
GIT_CONFIG_COUNT: "1",
GIT_CONFIG_KEY_0: "commit.gpgsign",
GIT_CONFIG_VALUE_0: "false",
...env,
},
stdio: ["ignore", "pipe", "pipe"],
});
}
function commandAvailable(command: string): boolean {
try {
execFileSync(command, ["version"], { stdio: "ignore" });
return true;
} catch {
return false;
}
}
function createSigningRepo(tempRoot: string): string {
const seed = path.join(tempRoot, "seed");
const remote = path.join(tempRoot, "apps-signing.git");
fs.mkdirSync(seed, { recursive: true });
runGit(["init", "--initial-branch=main"], seed);
fs.writeFileSync(path.join(seed, "README.md"), "# signing\n");
runGit(["add", "README.md"], seed);
runGit(["commit", "-m", "Initial signing repo"], seed);
runGit(["clone", "--bare", seed, remote], tempRoot);
return remote;
}
function writeManifest(tempRoot: string, signingRepo: string): string {
const manifestPath = path.join(tempRoot, "ReleaseSigning.json");
fs.writeFileSync(
manifestPath,
`${JSON.stringify(
{
signingRepo,
signingBranch: "main",
assetPath: "android/openclaw",
uploadKeystoreEncryptedFile: "upload-keystore.jks.enc",
gradlePropertiesEncryptedFile: "gradle.properties.enc",
materializedRoot: "unused-by-test",
gradlePropertyNames: [
"OPENCLAW_ANDROID_STORE_FILE",
"OPENCLAW_ANDROID_STORE_PASSWORD",
"OPENCLAW_ANDROID_KEY_ALIAS",
"OPENCLAW_ANDROID_KEY_PASSWORD",
],
},
null,
2,
)}\n`,
);
return manifestPath;
}
function writeSigningSources(tempRoot: string) {
const keystorePath = path.join(tempRoot, "upload-keystore.jks");
const propertiesPath = path.join(tempRoot, "source.properties");
fs.writeFileSync(keystorePath, "fake keystore bytes\n");
fs.writeFileSync(
propertiesPath,
[
`OPENCLAW_ANDROID_STORE_PASSWORD=${STORE_PASSWORD}`,
"OPENCLAW_ANDROID_KEY_ALIAS=openclaw-upload",
`OPENCLAW_ANDROID_KEY_PASSWORD=${KEY_PASSWORD}`,
"",
].join("\n"),
);
return { keystorePath, propertiesPath };
}
afterEach(() => {
for (const tempRoot of tempRoots.splice(0)) {
fs.rmSync(tempRoot, { recursive: true, force: true });
}
});
describe("scripts/android-release-signing.mjs", () => {
it.each([
["--mode"],
["--mode", "--manifest"],
["--mode", "-h"],
["--manifest"],
["--manifest", "-h"],
["--workspace", "--mode"],
["--workspace", "-h"],
["--materialized-dir", "--mode"],
["--materialized-dir", "-h"],
["--keystore", "--mode"],
["--keystore", "-h"],
["--properties", "--mode"],
["--properties", "-h"],
])("rejects missing values for %s before release signing work", (...args) => {
const result = runNode(args);
expect(result.ok).toBe(false);
expect(result.stderr).toContain(`Missing value for ${args[0]}.`);
expect(result.stderr).not.toContain("ENOENT");
expect(result.stdout).toBe("");
});
it("documents the canonical Android release signing plan", () => {
const result = runNode(["--mode", "plan"]);
expect(result.ok).toBe(true);
expect(result.stdout).toContain("Signing repo: git@github.com:openclaw/apps-signing.git");
expect(result.stdout).toContain("Signing assets: android/openclaw");
expect(result.stdout).toContain("Materialized output: apps/android/build/release-signing");
expect(result.stdout).toContain("ORG_GRADLE_PROJECT_*");
});
it.runIf(commandAvailable("openssl"))(
"encrypts, pulls, and materializes Android signing assets without printing secrets",
() => {
const tempRoot = makeTempRoot();
const signingRepo = createSigningRepo(tempRoot);
const manifestPath = writeManifest(tempRoot, signingRepo);
const { keystorePath, propertiesPath } = writeSigningSources(tempRoot);
const materializedDir = path.join(tempRoot, "materialized");
const workspace = path.join(materializedDir, "apps-signing");
const env = {
MATCH_PASSWORD,
GIT_AUTHOR_NAME: "OpenClaw Test",
GIT_AUTHOR_EMAIL: "test@example.com",
GIT_COMMITTER_NAME: "OpenClaw Test",
GIT_COMMITTER_EMAIL: "test@example.com",
GIT_CONFIG_COUNT: "1",
GIT_CONFIG_KEY_0: "commit.gpgsign",
GIT_CONFIG_VALUE_0: "false",
};
const push = runNode(
[
"--mode",
"sync-push",
"--manifest",
manifestPath,
"--workspace",
workspace,
"--materialized-dir",
materializedDir,
"--keystore",
keystorePath,
"--properties",
propertiesPath,
],
env,
);
expect(push.ok).toBe(true);
expect(`${push.stdout}${push.stderr}`).not.toContain(STORE_PASSWORD);
expect(`${push.stdout}${push.stderr}`).not.toContain(KEY_PASSWORD);
const remoteCheck = path.join(tempRoot, "remote-check");
runGit(["clone", signingRepo, remoteCheck], tempRoot);
const encryptedProperties = fs.readFileSync(
path.join(remoteCheck, "android", "openclaw", "gradle.properties.enc"),
"utf8",
);
expect(encryptedProperties).not.toContain(STORE_PASSWORD);
expect(encryptedProperties).not.toContain(KEY_PASSWORD);
fs.mkdirSync(materializedDir, { recursive: true });
const stalePropertiesPath = path.join(materializedDir, "gradle.properties");
fs.writeFileSync(stalePropertiesPath, "stale=1\n", { mode: 0o644 });
fs.chmodSync(stalePropertiesPath, 0o644);
const pull = runNode(
[
"--mode",
"sync-pull",
"--manifest",
manifestPath,
"--workspace",
path.join(materializedDir, "pull-workspace"),
"--materialized-dir",
materializedDir,
],
env,
);
expect(pull.ok).toBe(true);
expect(`${pull.stdout}${pull.stderr}`).not.toContain(STORE_PASSWORD);
expect(`${pull.stdout}${pull.stderr}`).not.toContain(KEY_PASSWORD);
expect(fs.readFileSync(path.join(materializedDir, "upload-keystore.jks"), "utf8")).toBe(
"fake keystore bytes\n",
);
const materializedProperties = fs.readFileSync(
path.join(materializedDir, "gradle.properties"),
"utf8",
);
expect(materializedProperties).toContain(
`OPENCLAW_ANDROID_STORE_FILE=${path.join(materializedDir, "upload-keystore.jks")}`,
);
expect(materializedProperties).toContain(`OPENCLAW_ANDROID_STORE_PASSWORD=${STORE_PASSWORD}`);
expect(materializedProperties).toContain("OPENCLAW_ANDROID_KEY_ALIAS=openclaw-upload");
expect(materializedProperties).toContain(`OPENCLAW_ANDROID_KEY_PASSWORD=${KEY_PASSWORD}`);
if (process.platform !== "win32") {
expect(fs.statSync(path.join(materializedDir, "gradle.properties")).mode & 0o777).toBe(
0o600,
);
expect(fs.statSync(path.join(materializedDir, "upload-keystore.jks")).mode & 0o777).toBe(
0o600,
);
}
const check = runNode([
"--mode",
"check",
"--manifest",
manifestPath,
"--materialized-dir",
materializedDir,
]);
expect(check.ok).toBe(true);
},
);
it("requires MATCH_PASSWORD before pushing encrypted signing assets", () => {
const tempRoot = makeTempRoot();
const manifestPath = writeManifest(tempRoot, path.join(tempRoot, "apps-signing.git"));
const { keystorePath, propertiesPath } = writeSigningSources(tempRoot);
const result = runNode([
"--mode",
"sync-push",
"--manifest",
manifestPath,
"--keystore",
keystorePath,
"--properties",
propertiesPath,
]);
expect(result.ok).toBe(false);
expect(result.stderr).toContain("MATCH_PASSWORD is required");
expect(result.stderr).not.toContain(STORE_PASSWORD);
expect(result.stderr).not.toContain(KEY_PASSWORD);
});
});