Download scripts/android-release-signing.mjs from SaylorTwift/openclaw: direct link, hf CLI and curl.
- Browser
- Download file 17 kB
-
https://huggingface.co/SaylorTwift/openclaw/resolve/main/scripts/android-release-signing.mjs
- Command line
-
hf download hf://SaylorTwift/openclaw/scripts/android-release-signing.mjs
-
curl -L -o android-release-signing.mjs https://huggingface.co/SaylorTwift/openclaw/resolve/main/scripts/android-release-signing.mjs
17 kB
| import fs from "node:fs"; | |
| import path from "node:path"; | |
| import process from "node:process"; | |
| import { runAndroidSigningCommandSync } from "./lib/android-release-signing-process.mjs"; | |
| import { parseFlagArgs, stringFlag } from "./lib/arg-utils.runtime.mjs"; | |
| import { isRecord } from "./lib/record-shared.mjs"; | |
| import { resolveRepoRoot } from "./lib/repo-root.mjs"; | |
| const rootDir = resolveRepoRoot(import.meta.url); | |
| const defaultManifestPath = path.join(rootDir, "apps", "android", "Config", "ReleaseSigning.json"); | |
| const requiredPropertyNames = [ | |
| "OPENCLAW_ANDROID_STORE_FILE", | |
| "OPENCLAW_ANDROID_STORE_PASSWORD", | |
| "OPENCLAW_ANDROID_KEY_ALIAS", | |
| "OPENCLAW_ANDROID_KEY_PASSWORD", | |
| ]; | |
| const sourceRequiredPropertyNames = requiredPropertyNames.filter( | |
| (name) => name !== "OPENCLAW_ANDROID_STORE_FILE", | |
| ); | |
| function usage() { | |
| process.stdout.write(`Usage: | |
| scripts/android-release-signing.mjs --mode plan | |
| scripts/android-release-signing.mjs --mode check | |
| scripts/android-release-signing.mjs --mode materialize | |
| scripts/android-release-signing.mjs --mode sync-pull | |
| scripts/android-release-signing.mjs --mode sync-push --keystore PATH --properties PATH | |
| Options: | |
| --manifest PATH Defaults to apps/android/Config/ReleaseSigning.json. | |
| --workspace PATH Defaults to <materializedRoot>/apps-signing. | |
| --materialized-dir PATH Defaults to materializedRoot from the manifest. | |
| --keystore PATH Upload keystore source for --mode sync-push. | |
| --properties PATH Signing properties source for --mode sync-push. | |
| sync-pull and sync-push use MATCH_PASSWORD to decrypt/encrypt Android release | |
| signing assets in the shared apps-signing repository. | |
| `); | |
| } | |
| function parseArgs(argv) { | |
| const options = { | |
| mode: "", | |
| manifestPath: defaultManifestPath, | |
| workspace: "", | |
| materializedDir: "", | |
| keystorePath: process.env.OPENCLAW_ANDROID_UPLOAD_KEYSTORE || "", | |
| propertiesPath: process.env.OPENCLAW_ANDROID_SIGNING_PROPERTIES || "", | |
| }; | |
| const helpIndex = argv.findIndex((arg) => arg === "-h" || arg === "--help"); | |
| parseFlagArgs( | |
| helpIndex === -1 ? argv : argv.slice(0, helpIndex), | |
| options, | |
| [ | |
| stringFlag("--mode", "mode", { | |
| allowInline: false, | |
| missingValueMessage: "Missing value for --mode.", | |
| rejectShortOptions: true, | |
| repeatable: true, | |
| }), | |
| ...[ | |
| ["--manifest", "manifestPath"], | |
| ["--workspace", "workspace"], | |
| ["--materialized-dir", "materializedDir"], | |
| ["--keystore", "keystorePath"], | |
| ["--properties", "propertiesPath"], | |
| ].map(([flag, key]) => | |
| stringFlag(flag, key, { | |
| allowInline: false, | |
| missingValueMessage: `Missing value for ${flag}.`, | |
| rejectShortOptions: true, | |
| repeatable: true, | |
| transform: path.resolve, | |
| }), | |
| ), | |
| ], | |
| { | |
| ignoreDoubleDash: false, | |
| onUnhandledArg(arg) { | |
| throw new Error(`Unknown argument: ${arg}`); | |
| }, | |
| }, | |
| ); | |
| if (helpIndex !== -1) { | |
| usage(); | |
| process.exit(0); | |
| } | |
| if (!options.mode) { | |
| throw new Error("Missing required --mode."); | |
| } | |
| return options; | |
| } | |
| function requireString(value, key) { | |
| if (typeof value !== "string" || value.trim() === "") { | |
| throw new Error(`Android release signing manifest missing ${key}.`); | |
| } | |
| return value.trim(); | |
| } | |
| function requireGradlePropertyNames(value) { | |
| if ( | |
| !Array.isArray(value) || | |
| !value.every((name) => typeof name === "string") || | |
| value.length !== requiredPropertyNames.length || | |
| !requiredPropertyNames.every((name) => value.includes(name)) | |
| ) { | |
| throw new Error( | |
| `Android release signing manifest must list Gradle properties: ${requiredPropertyNames.join(", ")}.`, | |
| ); | |
| } | |
| return value; | |
| } | |
| function readManifest(manifestPath) { | |
| const value = JSON.parse(fs.readFileSync(manifestPath, "utf8")); | |
| const parsed = isRecord(value) ? value : {}; | |
| const manifest = { | |
| signingRepo: requireString(parsed.signingRepo, "signingRepo"), | |
| signingBranch: requireString(parsed.signingBranch, "signingBranch"), | |
| assetPath: requireString(parsed.assetPath, "assetPath"), | |
| uploadKeystoreEncryptedFile: requireString( | |
| parsed.uploadKeystoreEncryptedFile, | |
| "uploadKeystoreEncryptedFile", | |
| ), | |
| gradlePropertiesEncryptedFile: requireString( | |
| parsed.gradlePropertiesEncryptedFile, | |
| "gradlePropertiesEncryptedFile", | |
| ), | |
| apkCertificateSha256: requireString(parsed.apkCertificateSha256, "apkCertificateSha256"), | |
| materializedRoot: requireString(parsed.materializedRoot, "materializedRoot"), | |
| gradlePropertyNames: requireGradlePropertyNames(parsed.gradlePropertyNames), | |
| }; | |
| if (!/^[a-f0-9]{64}$/u.test(manifest.apkCertificateSha256)) { | |
| throw new Error( | |
| "Android release signing manifest apkCertificateSha256 must be 64 lowercase hex digits.", | |
| ); | |
| } | |
| return manifest; | |
| } | |
| function relativePath(filePath) { | |
| const relative = path.relative(rootDir, filePath); | |
| return relative && !relative.startsWith("..") ? relative : filePath; | |
| } | |
| function resolveMaterializedDir(manifest, options) { | |
| return options.materializedDir || path.resolve(rootDir, manifest.materializedRoot); | |
| } | |
| function resolveWorkspace(manifest, options) { | |
| return options.workspace || path.join(resolveMaterializedDir(manifest, options), "apps-signing"); | |
| } | |
| function assertWorkspaceInsideMaterialized(workspace, materializedDir) { | |
| const resolvedWorkspace = path.resolve(workspace); | |
| const resolvedMaterializedDir = path.resolve(materializedDir); | |
| const relative = path.relative(resolvedMaterializedDir, resolvedWorkspace); | |
| if (!relative || relative.startsWith("..") || path.isAbsolute(relative)) { | |
| throw new Error( | |
| `Android signing workspace must be inside ${relativePath(resolvedMaterializedDir)}.`, | |
| ); | |
| } | |
| } | |
| function assetDir(workspace, manifest) { | |
| return path.join(workspace, manifest.assetPath); | |
| } | |
| function encryptedKeystorePath(workspace, manifest) { | |
| return path.join(assetDir(workspace, manifest), manifest.uploadKeystoreEncryptedFile); | |
| } | |
| function encryptedPropertiesPath(workspace, manifest) { | |
| return path.join(assetDir(workspace, manifest), manifest.gradlePropertiesEncryptedFile); | |
| } | |
| function materializedKeystorePath(materializedDir) { | |
| return path.join(materializedDir, "upload-keystore.jks"); | |
| } | |
| function materializedPropertiesPath(materializedDir) { | |
| return path.join(materializedDir, "gradle.properties"); | |
| } | |
| function requireMatchPassword() { | |
| if (!process.env.MATCH_PASSWORD || process.env.MATCH_PASSWORD.trim() === "") { | |
| throw new Error("MATCH_PASSWORD is required for Android release signing sync."); | |
| } | |
| } | |
| function run(command, args, options = {}) { | |
| runAndroidSigningCommandSync(command, args, { | |
| cwd: options.cwd, | |
| env: options.env || process.env, | |
| stdio: options.stdio || "pipe", | |
| }); | |
| } | |
| function runText(command, args, options = {}) { | |
| return runAndroidSigningCommandSync(command, args, { | |
| cwd: options.cwd, | |
| env: options.env || process.env, | |
| encoding: "utf8", | |
| stdio: ["ignore", "pipe", "pipe"], | |
| }); | |
| } | |
| function cloneSigningRepo(manifest, workspace, materializedDir) { | |
| assertWorkspaceInsideMaterialized(workspace, materializedDir); | |
| fs.rmSync(workspace, { recursive: true, force: true }); | |
| fs.mkdirSync(path.dirname(workspace), { recursive: true }); | |
| run("git", ["clone", "--branch", manifest.signingBranch, manifest.signingRepo, workspace]); | |
| } | |
| function opensslCrypt({ decrypt, inputPath, outputPath }) { | |
| requireMatchPassword(); | |
| fs.mkdirSync(path.dirname(outputPath), { recursive: true }); | |
| if (decrypt) { | |
| fs.rmSync(outputPath, { force: true }); | |
| } | |
| const args = [ | |
| "enc", | |
| "-aes-256-cbc", | |
| "-pbkdf2", | |
| "-md", | |
| "sha256", | |
| ...(decrypt ? ["-d"] : ["-salt"]), | |
| "-in", | |
| inputPath, | |
| "-out", | |
| outputPath, | |
| "-pass", | |
| "env:MATCH_PASSWORD", | |
| ]; | |
| const previousUmask = decrypt ? process.umask(0o077) : undefined; | |
| try { | |
| run("openssl", args); | |
| } finally { | |
| if (previousUmask !== undefined) { | |
| process.umask(previousUmask); | |
| } | |
| } | |
| if (decrypt) { | |
| fs.chmodSync(outputPath, 0o600); | |
| } | |
| } | |
| function readProperties(filePath) { | |
| const properties = new Map(); | |
| for (const rawLine of fs.readFileSync(filePath, "utf8").split(/\r?\n/u)) { | |
| const line = rawLine.trim(); | |
| if (!line || line.startsWith("#")) { | |
| continue; | |
| } | |
| const separator = line.indexOf("="); | |
| if (separator <= 0) { | |
| throw new Error(`Invalid signing properties line in ${relativePath(filePath)}.`); | |
| } | |
| const key = line.slice(0, separator).trim(); | |
| const value = line.slice(separator + 1).trim(); | |
| if (!key || !value) { | |
| throw new Error(`Invalid empty signing property in ${relativePath(filePath)}.`); | |
| } | |
| properties.set(key, value); | |
| } | |
| return properties; | |
| } | |
| function requireProperties(properties, names, filePath) { | |
| const missing = names.filter((name) => !properties.get(name)); | |
| if (missing.length > 0) { | |
| throw new Error( | |
| `${relativePath(filePath)} is missing Android signing properties: ${missing.join(", ")}.`, | |
| ); | |
| } | |
| } | |
| function writeMaterializedProperties(materializedDir, sourceProperties) { | |
| const keystorePath = materializedKeystorePath(materializedDir); | |
| const propertiesPath = materializedPropertiesPath(materializedDir); | |
| const tempPath = `${propertiesPath}.${process.pid}.tmp`; | |
| const properties = new Map(sourceProperties); | |
| properties.set("OPENCLAW_ANDROID_STORE_FILE", keystorePath); | |
| requireProperties(properties, requiredPropertyNames, propertiesPath); | |
| const content = [ | |
| "# Generated by scripts/android-release-signing.mjs.", | |
| "# Contains decrypted Android release signing values. Do not commit.", | |
| ...requiredPropertyNames.map((name) => `${name}=${properties.get(name)}`), | |
| "", | |
| ].join("\n"); | |
| try { | |
| fs.writeFileSync(tempPath, content, { mode: 0o600 }); | |
| fs.chmodSync(tempPath, 0o600); | |
| fs.renameSync(tempPath, propertiesPath); | |
| fs.chmodSync(propertiesPath, 0o600); | |
| } finally { | |
| fs.rmSync(tempPath, { force: true }); | |
| } | |
| } | |
| function validateMaterializedSigning(materializedDir) { | |
| const keystorePath = materializedKeystorePath(materializedDir); | |
| const propertiesPath = materializedPropertiesPath(materializedDir); | |
| if (!fs.existsSync(keystorePath) || fs.statSync(keystorePath).size === 0) { | |
| throw new Error( | |
| `Missing materialized Android upload keystore at ${relativePath(keystorePath)}.`, | |
| ); | |
| } | |
| if (!fs.existsSync(propertiesPath)) { | |
| throw new Error( | |
| `Missing materialized Android signing properties at ${relativePath(propertiesPath)}.`, | |
| ); | |
| } | |
| const properties = readProperties(propertiesPath); | |
| requireProperties(properties, requiredPropertyNames, propertiesPath); | |
| if (properties.get("OPENCLAW_ANDROID_STORE_FILE") !== keystorePath) { | |
| throw new Error( | |
| `${relativePath(propertiesPath)} must point OPENCLAW_ANDROID_STORE_FILE at ${relativePath(keystorePath)}.`, | |
| ); | |
| } | |
| } | |
| function writePlan(manifest, options) { | |
| const materializedDir = resolveMaterializedDir(manifest, options); | |
| process.stdout.write(`Android release signing plan | |
| Signing repo: ${manifest.signingRepo} | |
| Signing branch: ${manifest.signingBranch} | |
| Signing assets: ${manifest.assetPath} | |
| Encrypted upload keystore: ${manifest.uploadKeystoreEncryptedFile} | |
| Encrypted Gradle properties: ${manifest.gradlePropertiesEncryptedFile} | |
| Pinned APK certificate SHA-256: ${manifest.apkCertificateSha256} | |
| Materialized output: ${relativePath(materializedDir)} | |
| Gradle bridge: Fastlane exports ORG_GRADLE_PROJECT_* values from the materialized properties file. | |
| `); | |
| } | |
| function writeSigningRepoManifest(workspace, manifest) { | |
| const signingManifestPath = path.join(assetDir(workspace, manifest), "manifest.json"); | |
| const signingManifest = { | |
| version: 1, | |
| assetPath: manifest.assetPath, | |
| uploadKeystoreEncryptedFile: manifest.uploadKeystoreEncryptedFile, | |
| gradlePropertiesEncryptedFile: manifest.gradlePropertiesEncryptedFile, | |
| apkCertificateSha256: manifest.apkCertificateSha256, | |
| gradlePropertyNames: requiredPropertyNames, | |
| }; | |
| fs.writeFileSync(signingManifestPath, `${JSON.stringify(signingManifest, null, 2)}\n`); | |
| } | |
| function materialize(manifest, options) { | |
| const workspace = resolveWorkspace(manifest, options); | |
| const materializedDir = resolveMaterializedDir(manifest, options); | |
| const tempPropertiesPath = path.join(materializedDir, ".gradle.properties.decrypted.tmp"); | |
| assertWorkspaceInsideMaterialized(workspace, materializedDir); | |
| if (!fs.existsSync(encryptedKeystorePath(workspace, manifest))) { | |
| throw new Error( | |
| `Missing encrypted Android upload keystore in signing repo at ${manifest.assetPath}/${manifest.uploadKeystoreEncryptedFile}.`, | |
| ); | |
| } | |
| if (!fs.existsSync(encryptedPropertiesPath(workspace, manifest))) { | |
| throw new Error( | |
| `Missing encrypted Android signing properties in signing repo at ${manifest.assetPath}/${manifest.gradlePropertiesEncryptedFile}.`, | |
| ); | |
| } | |
| fs.mkdirSync(materializedDir, { recursive: true }); | |
| opensslCrypt({ | |
| decrypt: true, | |
| inputPath: encryptedKeystorePath(workspace, manifest), | |
| outputPath: materializedKeystorePath(materializedDir), | |
| }); | |
| try { | |
| opensslCrypt({ | |
| decrypt: true, | |
| inputPath: encryptedPropertiesPath(workspace, manifest), | |
| outputPath: tempPropertiesPath, | |
| }); | |
| const properties = readProperties(tempPropertiesPath); | |
| requireProperties(properties, sourceRequiredPropertyNames, tempPropertiesPath); | |
| writeMaterializedProperties(materializedDir, properties); | |
| } finally { | |
| fs.rmSync(tempPropertiesPath, { force: true }); | |
| } | |
| validateMaterializedSigning(materializedDir); | |
| process.stdout.write( | |
| `Materialized Android release signing assets in ${relativePath(materializedDir)}.\n`, | |
| ); | |
| } | |
| function syncPull(manifest, options) { | |
| const workspace = resolveWorkspace(manifest, options); | |
| const materializedDir = resolveMaterializedDir(manifest, options); | |
| cloneSigningRepo(manifest, workspace, materializedDir); | |
| materialize(manifest, options); | |
| } | |
| function requirePushSources(options) { | |
| if (!options.keystorePath) { | |
| throw new Error( | |
| "Missing Android upload keystore source. Pass --keystore or set OPENCLAW_ANDROID_UPLOAD_KEYSTORE.", | |
| ); | |
| } | |
| if (!options.propertiesPath) { | |
| throw new Error( | |
| "Missing Android signing properties source. Pass --properties or set OPENCLAW_ANDROID_SIGNING_PROPERTIES.", | |
| ); | |
| } | |
| if (!fs.existsSync(options.keystorePath) || fs.statSync(options.keystorePath).size === 0) { | |
| throw new Error( | |
| `Android upload keystore source is missing or empty: ${relativePath(options.keystorePath)}.`, | |
| ); | |
| } | |
| if (!fs.existsSync(options.propertiesPath)) { | |
| throw new Error( | |
| `Android signing properties source is missing: ${relativePath(options.propertiesPath)}.`, | |
| ); | |
| } | |
| const properties = readProperties(options.propertiesPath); | |
| requireProperties(properties, sourceRequiredPropertyNames, options.propertiesPath); | |
| } | |
| function syncPush(manifest, options) { | |
| requireMatchPassword(); | |
| requirePushSources(options); | |
| const workspace = resolveWorkspace(manifest, options); | |
| cloneSigningRepo(manifest, workspace, resolveMaterializedDir(manifest, options)); | |
| fs.mkdirSync(assetDir(workspace, manifest), { recursive: true }); | |
| opensslCrypt({ | |
| decrypt: false, | |
| inputPath: options.keystorePath, | |
| outputPath: encryptedKeystorePath(workspace, manifest), | |
| }); | |
| opensslCrypt({ | |
| decrypt: false, | |
| inputPath: options.propertiesPath, | |
| outputPath: encryptedPropertiesPath(workspace, manifest), | |
| }); | |
| writeSigningRepoManifest(workspace, manifest); | |
| run("git", ["add", manifest.assetPath], { cwd: workspace }); | |
| const status = runText("git", ["status", "--porcelain"], { cwd: workspace }).trim(); | |
| if (!status) { | |
| process.stdout.write("Android release signing assets were already up to date.\n"); | |
| return; | |
| } | |
| run("git", ["commit", "-m", "Update Android release signing assets"], { cwd: workspace }); | |
| run("git", ["push", "origin", manifest.signingBranch], { cwd: workspace }); | |
| process.stdout.write("Pushed encrypted Android release signing assets.\n"); | |
| } | |
| try { | |
| const options = parseArgs(process.argv.slice(2)); | |
| const manifest = readManifest(options.manifestPath); | |
| if (options.mode === "plan") { | |
| writePlan(manifest, options); | |
| } else if (options.mode === "check") { | |
| validateMaterializedSigning(resolveMaterializedDir(manifest, options)); | |
| process.stdout.write("Android release signing materialization is valid.\n"); | |
| } else if (options.mode === "materialize") { | |
| materialize(manifest, options); | |
| } else if (options.mode === "sync-pull") { | |
| syncPull(manifest, options); | |
| } else if (options.mode === "sync-push") { | |
| syncPush(manifest, options); | |
| } else { | |
| throw new Error(`Unknown mode: ${options.mode}`); | |
| } | |
| } catch (error) { | |
| process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n`); | |
| process.exit(1); | |
| } | |