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
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
298 lines
9.5 KiB
TypeScript
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);
|
|
});
|
|
});
|