93 lines
3.3 KiB
JavaScript
93 lines
3.3 KiB
JavaScript
import {existsSync, readFileSync, readdirSync} from "node:fs"
|
|
import {mkdtemp, readFile, rm, writeFile} from "node:fs/promises"
|
|
import {tmpdir} from "node:os"
|
|
import {join} from "node:path"
|
|
import {HOUR, MINUTE, ago, ms, now, sleep} from "@welshman/lib"
|
|
import {keystoreEnv} from "../lib/android.mjs"
|
|
import {missingEnv, root} from "../lib/context.mjs"
|
|
import {fdroidPackage, packageToken, signedApk, unsignedApk} from "../lib/fdroid.mjs"
|
|
import {output, run} from "../lib/shell.mjs"
|
|
|
|
const sdk =
|
|
process.env.ANDROID_HOME ??
|
|
readFileSync(join(root, "android/local.properties"), "utf-8").match(/^sdk\.dir=(.+)$/m)?.[1]
|
|
|
|
const apksigner = () => {
|
|
const tools = join(sdk, "build-tools")
|
|
const [latest] = readdirSync(tools).sort((a, b) => b.localeCompare(a, "en", {numeric: true}))
|
|
|
|
return join(tools, latest, "apksigner")
|
|
}
|
|
|
|
export default {
|
|
name: "fdroid-sign",
|
|
title: "Sign the release workflow's F-Droid build with the distribution key",
|
|
missing: () => [
|
|
...missingEnv("ANDROID_KEYSTORE_PATH", "ANDROID_KEYSTORE_PASSWORD", "ANDROID_KEYSTORE_ALIAS"),
|
|
...(packageToken ? [] : ["GITEA_TOKEN"]),
|
|
...(sdk && existsSync(join(sdk, "build-tools")) ? [] : ["the Android SDK's build-tools"]),
|
|
],
|
|
setup: [
|
|
"Signs with the same ANDROID_KEYSTORE_* key as the apk step. GITEA_TOKEN needs the",
|
|
"write:package scope. apksigner comes from the Android SDK in ANDROID_HOME, or the sdk.dir",
|
|
"Android Studio writes to android/local.properties.",
|
|
],
|
|
run: async () => {
|
|
const api = fdroidPackage(packageToken)
|
|
const started = now()
|
|
let unsigned = await api.download(unsignedApk)
|
|
|
|
while (!unsigned) {
|
|
if (started < ago(2 * HOUR)) {
|
|
throw new Error(
|
|
`The release workflow hasn't uploaded ${api.url(unsignedApk)} after two hours`,
|
|
)
|
|
}
|
|
|
|
console.log(`Waiting for the release workflow to upload ${unsignedApk}`)
|
|
await sleep(ms(MINUTE))
|
|
unsigned = await api.download(unsignedApk)
|
|
}
|
|
|
|
const work = await mkdtemp(join(tmpdir(), "flotilla-fdroid-sign-"))
|
|
|
|
try {
|
|
const signing = keystoreEnv("ANDROID")
|
|
|
|
await writeFile(join(work, unsignedApk), unsigned)
|
|
|
|
// F-Droid copies this signature onto its own build, so nothing but the signature may change
|
|
await run(
|
|
apksigner(),
|
|
[
|
|
"sign",
|
|
"--ks",
|
|
signing.ANDROID_KEYSTORE_PATH,
|
|
"--ks-key-alias",
|
|
signing.ANDROID_KEYSTORE_ALIAS,
|
|
"--ks-pass",
|
|
"env:ANDROID_KEYSTORE_PASSWORD",
|
|
"--key-pass",
|
|
"env:ANDROID_KEYSTORE_ALIAS_PASSWORD",
|
|
"--alignment-preserved",
|
|
"--out",
|
|
join(work, signedApk),
|
|
join(work, unsignedApk),
|
|
],
|
|
{env: {...process.env, ...signing}},
|
|
)
|
|
|
|
const recipe = readFileSync(join(root, "fdroid/metadata/social.flotilla.fdroid.yml"), "utf-8")
|
|
const allowed = recipe.match(/^AllowedAPKSigningKeys: (\w+)$/m)[1]
|
|
const certificates = output(apksigner(), ["verify", "--print-certs", join(work, signedApk)])
|
|
|
|
if (!certificates.includes(`certificate SHA-256 digest: ${allowed}`)) {
|
|
throw new Error(`${signedApk} isn't signed with the key the recipe allows, ${allowed}`)
|
|
}
|
|
|
|
console.log(await api.upload(signedApk, await readFile(join(work, signedApk))))
|
|
} finally {
|
|
await rm(work, {recursive: true, force: true})
|
|
}
|
|
},
|
|
}
|