Windows builds and moved credentials: play build runs gradlew.bat through cmd /c and takes C:/ keystore paths as absolute (pitfall 98); check names credential paths missing on this machine and a keystore the stored password / alias do not open (keytool) (pitfall 99); play / apple build write <out>/latest.json for CI; CI install from a git clone documented

This commit is contained in:
CRTkafa 2026-10-22 02:10:52 +08:00
parent 4fcee60ba6
commit 73d308164d
9 changed files with 95 additions and 9 deletions

View file

@ -26,6 +26,7 @@
import { chmodSync, existsSync, mkdirSync, mkdtempSync, readdirSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
import { homedir, tmpdir } from 'node:os';
import { isAbsolute, join, resolve } from 'node:path';
import * as rt from './rt';
export interface ServiceAccount { client_email: string; private_key: string; token_uri?: string }
export interface AscKey { keyId: string; issuerId: string; pem: string }
@ -324,7 +325,7 @@ export function androidSigning(root: string): AndroidSigning | null {
const storePassword = value(root, 'ANDROID_KEYSTORE_PASSWORD'), keyAlias = value(root, 'ANDROID_KEY_ALIAS');
if (!storePassword || !keyAlias) throw new Error('ANDROID_KEYSTORE is set but ANDROID_KEYSTORE_PASSWORD / ANDROID_KEY_ALIAS are not (env or .storebound/credentials.json)');
const abs = storeFile.replace(/^~(?=\/)/, homedir());
return { storeFile: abs.startsWith('/') ? abs : join(root, abs), storePassword, keyAlias, keyPassword: value(root, 'ANDROID_KEY_PASSWORD') ?? storePassword };
return { storeFile: isAbsolute(abs) ? abs : join(root, abs), storePassword, keyAlias, keyPassword: value(root, 'ANDROID_KEY_PASSWORD') ?? storePassword };
}
/** App Store Connect vendor number for sales and finance reports (Payments and Financial Reports, top left): the
@ -334,3 +335,33 @@ export function ascVendorNumber(root: string): string | null {
const v = value(root, 'ASC_VENDOR_NUMBER');
return v && /^\d{5,12}$/.test(v.trim()) ? v.trim() : null;
}
/** Credential paths that do not exist (a machine move left another computer's absolute paths) and an upload keystore
* the stored password / alias do not open: each names the field. `keytool` runs only when it is on PATH. */
export function credentialFileIssues(root: string, run: (argv: string[]) => { ok: boolean; out: string } | null = keytool): string[] {
const issues: string[] = [];
const asPath = (v: string) => { const p = v.replace(/^~(?=[/\\])/, homedir()); return isAbsolute(p) ? p : join(root, p); };
for (const k of ['PLAY_SERVICE_ACCOUNT', 'ASC_P8', 'ANDROID_KEYSTORE'] as const) {
let v: string | undefined;
try { v = value(root, k); } catch { continue; }
if (!v || /^\s*\{/.test(v) || /BEGIN [A-Z ]*PRIVATE KEY/.test(v)) continue;
if (!existsSync(asPath(v))) issues.push(`${k}: ${v} does not exist on this machine (moved from another computer?): point it at the file here`);
}
let s: AndroidSigning | null = null;
try { s = androidSigning(root); } catch (e) { issues.push((e as Error).message); }
if (s && existsSync(s.storeFile)) {
const r = run(['-list', '-keystore', s.storeFile, '-storepass', s.storePassword, '-alias', s.keyAlias]);
if (r && !r.ok) issues.push(/password|tampered|Keystore was tampered/i.test(r.out)
? 'ANDROID_KEYSTORE_PASSWORD does not open the upload keystore (keytool -list failed): check the password (android/keystore.properties may hold the right one)'
: /alias|does not exist/i.test(r.out) ? `ANDROID_KEY_ALIAS "${s.keyAlias}" is not in the upload keystore` : `the upload keystore does not open with the stored password / alias: ${r.out.trim().split('\n')[0]}`);
}
return issues;
}
function keytool(argv: string[]): { ok: boolean; out: string } | null {
const bin = process.env.JAVA_HOME ? join(process.env.JAVA_HOME, 'bin', process.platform === 'win32' ? 'keytool.exe' : 'keytool') : 'keytool';
try {
const r = rt.spawnSync([bin, ...argv], { timeout: 20_000 });
return { ok: r.success, out: `${r.stdout.toString()}${r.stderr.toString()}` };
} catch { return null; }
}