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

@ -4,7 +4,7 @@ import { chmodSync, existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, ut
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { installedProfiles } from '../cli/apple/build';
import { androidSigning, ascKeyFile } from '../cli/lib/credentials';
import { androidSigning, ascKeyFile, credentialFileIssues } from '../cli/lib/credentials';
import { buildCsr, csrContent, newSigningKey, spkiOfKey } from '../cli/lib/csr';
import { aabInfo, appBuildFile, findBundles, gradleArgs, pickGradle, signingEnv, validatePlayBuild, versionSourceIssues, versionSourceMessage } from '../cli/lib/gradle';
import { parseBinaryPlist, parseMobileprovision, parsePlist, parseXmlPlist, toXmlPlist } from '../cli/lib/plist';
@ -269,8 +269,10 @@ describe('gradle projects', () => {
touch(rn, 'android/settings.gradle'); touch(rn, 'android/gradlew', '#!/bin/sh'); touch(rn, 'android/app/build.gradle');
const t = pickGradle(rn);
expect(t).toMatchObject({ framework: 'react-native', module: 'app', task: ':app:bundleRelease' });
expect(gradleArgs(t, { versionCode: 7, versionName: '1.2.0', hasWrapper: true })).toEqual(['./gradlew', ':app:bundleRelease', '--no-daemon', '--console=plain',
expect(gradleArgs(t, { versionCode: 7, versionName: '1.2.0', hasWrapper: true }, 'linux')).toEqual(['./gradlew', ':app:bundleRelease', '--no-daemon', '--console=plain',
'-Pandroid.injected.version.code=7', '-PversionCode=7', '-Pandroid.injected.version.name=1.2.0', '-PversionName=1.2.0']);
// Windows: a bare gradlew.bat is looked up on PATH only; cmd /c runs the one in the project folder
expect(gradleArgs(t, { hasWrapper: true }, 'win32').slice(0, 4)).toEqual(['cmd', '/c', 'gradlew.bat', ':app:bundleRelease']);
expect(pickGradle(rn, { flavor: 'prod' }).task).toBe(':app:bundleProdRelease');
const unity = dir('unity');
touch(unity, 'Export/settings.gradle'); touch(unity, 'Export/launcher/build.gradle'); touch(unity, 'Export/unityLibrary/build.gradle');
@ -360,12 +362,26 @@ describe('credentials for builds', () => {
delete process.env.ANDROID_KEYSTORE;
expect(androidSigning(d)).toEqual({ storeFile: join(d, 'keys/upload.jks'), storePassword: 'pw', keyAlias: 'upload', keyPassword: 'pw' });
expect(androidSigning(dir('nocreds'))).toBeNull();
// a Windows absolute path is used as it is, not joined onto the project folder
if (process.platform === 'win32') {
const w = dir('wincreds');
touch(w, '.storebound/credentials.json', JSON.stringify({ ANDROID_KEYSTORE: 'C:/keys/upload.jks', ANDROID_KEYSTORE_PASSWORD: 'pw', ANDROID_KEY_ALIAS: 'upload' }));
expect(androidSigning(w)?.storeFile).toBe('C:/keys/upload.jks');
}
// another machine's paths and a keystore the stored password does not open are named by field
const m = dir('moved');
touch(m, 'keys/upload.jks', 'x');
touch(m, '.storebound/credentials.json', JSON.stringify({ ANDROID_KEYSTORE: 'keys/upload.jks', ANDROID_KEYSTORE_PASSWORD: 'wrong', ANDROID_KEY_ALIAS: 'upload', PLAY_SERVICE_ACCOUNT: '/Users/someone/keys/play.json' }));
const issues = credentialFileIssues(m, () => ({ ok: false, out: 'keytool error: java.io.IOException: Keystore was tampered with, or password was incorrect' }));
expect(issues.some(i => i.startsWith('PLAY_SERVICE_ACCOUNT: /Users/someone/keys/play.json does not exist'))).toBe(true);
expect(issues.some(i => i.startsWith('ANDROID_KEYSTORE_PASSWORD does not open'))).toBe(true);
expect(credentialFileIssues(m, () => ({ ok: true, out: '' })).some(i => i.startsWith('ANDROID'))).toBe(false);
const { pem } = newSigningKey();
const p8 = createPrivateKey(pem).export({ type: 'pkcs8', format: 'pem' }).toString();
Object.assign(process.env, { ASC_KEY_ID: 'KEYID00001', ASC_ISSUER_ID: '00000000-0000-0000-0000-000000000000', ASC_P8: p8 });
const k = ascKeyFile(d);
expect(readFileSync(k.keyPath, 'utf8')).toBe(p8);
expect(k.keyPath.split('/').pop()).toBe(['AuthKey', 'KEYID00001.p8'].join('_'));
expect(k.keyPath.split(/[\\/]/).pop()).toBe(['AuthKey', 'KEYID00001.p8'].join('_'));
k.cleanup();
expect(() => readFileSync(k.keyPath)).toThrow();
} finally { for (const key of ['ASC_KEY_ID', 'ASC_ISSUER_ID', 'ASC_P8']) if (saved[key] === undefined) delete process.env[key]; else process.env[key] = saved[key]; }