Files
404-Layers/scripts/release.mjs
ncode 1d2cbda50d Ship a signed release APK
The release build type reuses the debug signing config, so a downloaded APK
installs over an existing one instead of demanding an uninstall. This never
goes to Play, which rejects debug keys anyway, so there is no private key
worth safeguarding for years and no way to strand anyone on a version that
can no longer be updated.

That choice is load-bearing in one direction: a release build is not
DEBUGGABLE, so adb shell run-as stops reading app data back out, and there is
no in-app restore. A signature change would have cost the drill progress.

npm run release stages release/404-Layers-<version>.apk with a checksum and
verifies the signature, since an unsigned APK builds fine and only fails at
install time, on someone else's phone.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-17 21:51:01 +03:00

45 lines
1.8 KiB
JavaScript

#!/usr/bin/env node
/**
* Builds the release APK and stages it under release/ under a name a stranger
* can recognise, next to the checksum to verify a download against.
*
* It verifies the signature rather than trusting that the signing config took:
* an unsigned APK builds happily and only fails at install time, on someone
* else's phone.
*/
import { createHash } from "node:crypto";
import { execFileSync } from "node:child_process";
import { copyFileSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const root = join(dirname(fileURLToPath(import.meta.url)), "..");
const { version } = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
execFileSync("npm", ["run", "apk:release"], { cwd: root, stdio: "inherit" });
const built = join(root, "android/app/build/outputs/apk/release/app-release.apk");
const name = `404-Layers-${version}.apk`;
const out = join(root, "release", name);
mkdirSync(dirname(out), { recursive: true });
copyFileSync(built, out);
const bytes = readFileSync(out);
const sha = createHash("sha256").update(bytes).digest("hex");
writeFileSync(`${out}.sha256`, `${sha} ${name}\n`);
// build-tools is pinned in CLAUDE.md; a missing apksigner should fail loudly.
const sdk = process.env.ANDROID_HOME ?? join(process.env.HOME ?? "", "Android/Sdk");
const certs = execFileSync(join(sdk, "build-tools/36.1.0/apksigner"), ["verify", "--print-certs", out], {
encoding: "utf8",
});
const digest = certs.match(/certificate SHA-256 digest: (\w+)/)?.[1] ?? "";
if (digest === "") throw new Error(`apksigner reported no certificate:\n${certs}`);
console.log(`\n${name} ${(bytes.length / 1e6).toFixed(2)} MB`);
console.log(`file sha256 ${sha}`);
console.log(`cert sha256 ${digest}`);
console.log(`\nStaged at ${out}`);