apple iap-submit: DEVELOPER_REJECTED IAP / subscription / group versions (left by a canceled submission) are submitted as they are, plan says 'add (developer rejected, re-submitted as is)', --new-version only for APPROVED / REJECTED, empty draft reused; fake ASC models cancel and re-add. play build / check: warn before Gradle when defaultConfig does not read -PversionCode / -PversionName, with the exact Groovy / Kotlin line (built with AGP 8.13); android.injected.version.code does not override bundleRelease; post-build versionCode mismatch is a hard stop. play-production-release plan: 'Do you want to restart your review?' dialog, Restart review is never-press. Pitfalls 31-33

This commit is contained in:
CRTkafa 2026-10-12 11:32:05 +08:00
parent 41d2ffcc24
commit 0b1451800c
18 changed files with 399 additions and 26 deletions

View file

@ -6,7 +6,7 @@ import { join } from 'node:path';
import { installedProfiles } from '../cli/apple/build';
import { androidSigning, ascKeyFile } from '../cli/lib/credentials';
import { buildCsr, csrContent, newSigningKey, spkiOfKey } from '../cli/lib/csr';
import { aabInfo, findBundles, gradleArgs, pickGradle, signingEnv, validatePlayBuild } from '../cli/lib/gradle';
import { aabInfo, appBuildFile, findBundles, gradleArgs, pickGradle, signingEnv, validatePlayBuild, versionSourceIssues, versionSourceMessage } from '../cli/lib/gradle';
import { parseBinaryPlist, parseMobileprovision, parsePlist, parseXmlPlist, toXmlPlist } from '../cli/lib/plist';
import {
type ApiCert, type ApiProfile, certSha1, certSpki, certificateReq, explainCertError, exportOptions, parseIdentities, pickCertificate, planCapabilities,
@ -301,6 +301,46 @@ describe('gradle projects', () => {
});
});
// play build passes -PversionCode / -PversionName; seen live (2026-10-12): a Capacitor app/build.gradle read versionCode
// from an env var, so the AAB stayed at 1. Checked with AGP 8.13 the same day: android.injected.version.code does not
// override a literal for bundleRelease; the Groovy and Kotlin lines below give the passed values (and the default without).
describe('versionCode / versionName source in the build file', () => {
const gv = (body: string) => `plugins { id 'com.android.application' }\nandroid {\n namespace 'com.example.game'\n defaultConfig {\n applicationId 'com.example.game' // don't touch\n${body}\n }\n}\n`;
const kv = (body: string) => `plugins { id("com.android.application") }\nandroid {\n namespace = "com.example.game"\n defaultConfig {\n applicationId = "com.example.game"\n${body}\n }\n}\n`;
const kinds = (file: string, src: string) => versionSourceIssues(file, src).map(i => [i.field, i.kind, i.line, i.current, i.fix]);
test('Groovy: literal and env var warn with the exact line; findProperty does not', () => {
expect(kinds('app/build.gradle', gv(" versionCode 3\n versionName '1.4.0'"))).toEqual([
['versionCode', 'literal', 6, '3', "versionCode((project.findProperty('versionCode') ?: 3) as Integer)"],
['versionName', 'literal', 7, '1.4.0', "versionName project.findProperty('versionName') ?: '1.4.0'"]]);
expect(kinds('app/build.gradle', gv(" versionCode System.getenv('VERSION_CODE') ? System.getenv('VERSION_CODE').toInteger() : 1\n versionName System.getenv('VERSION_NAME') ?: \"1.0.2\"")).map(k => k.slice(0, 4))).toEqual([
['versionCode', 'other', 6, '1'], ['versionName', 'other', 7, '1.0.2']]);
expect(kinds('app/build.gradle', gv(" versionCode((project.findProperty('versionCode') ?: 1) as Integer)\n versionName project.findProperty('versionName') ?: '1.0'"))).toEqual([]);
expect(kinds('app/build.gradle', gv(" versionCode project.hasProperty('versionCode') ? project.property('versionCode').toInteger() : 1\n versionName project.findProperty('versionName') ?: '1.0'"))).toEqual([]);
// commented out lines do not count; nothing set at all is said too
expect(kinds('app/build.gradle', gv(" // versionCode 1\n /* versionName '1.0' */")).map(k => [k[0], k[1]])).toEqual([['versionCode', 'missing'], ['versionName', 'missing']]);
});
test('Kotlin: literal and env var warn with the exact line; findProperty does not', () => {
expect(kinds('app/build.gradle.kts', kv(' versionCode = 5\n versionName = "2.0"'))).toEqual([
['versionCode', 'literal', 6, '5', 'versionCode = (project.findProperty("versionCode") as String?)?.toInt() ?: 5'],
['versionName', 'literal', 7, '2.0', 'versionName = project.findProperty("versionName") as String? ?: "2.0"']]);
expect(kinds('app/build.gradle.kts', kv(' versionCode = System.getenv("VERSION_CODE")?.toInt() ?: 1\n versionName = System.getenv("VERSION_NAME") ?: "1.0"')).map(k => k.slice(0, 4))).toEqual([
['versionCode', 'other', 6, '1'], ['versionName', 'other', 7, '1.0']]);
expect(kinds('app/build.gradle.kts', kv(' versionCode = (project.findProperty("versionCode") as String?)?.toInt() ?: 1\n versionName = project.findProperty("versionName") as String? ?: "1.0"'))).toEqual([]);
});
test('the message names the file and line, says the injected property does not help, and gives the line', () => {
const [i] = versionSourceIssues('app/build.gradle', gv(" versionCode System.getenv('VERSION_CODE') as Integer\n versionName project.findProperty('versionName') ?: '1.0'"));
const m = versionSourceMessage(i!, 'android/app/build.gradle', 'capacitor');
expect(m).toContain('android/app/build.gradle:6: versionCode does not come from the project property versionCode');
expect(m).toContain('-PversionCode from play build is ignored');
expect(m).toContain('android.injected.version.* does not override it for bundleRelease');
expect(m).toContain("Use in defaultConfig: versionCode((project.findProperty('versionCode') ?: 1) as Integer)");
const d = dir('gradle-kts');
touch(d, 'app/build.gradle.kts', kv(' versionCode = 1'));
expect(appBuildFile({ dir: d, module: 'app' })).toBe(join(d, 'app/build.gradle.kts'));
expect(appBuildFile({ dir: d, module: 'other' })).toBeNull();
});
});
describe('credentials for builds', () => {
test('android upload key from .storebound/credentials.json; inline ASC key goes to a private temp file', () => {
const d = dir('creds');
@ -372,6 +412,38 @@ describe('CLI dry runs (no store access)', () => {
expect(readFileSync(marker, 'utf8').trim()).toBe(`${sdk}|${sdk}`);
expect(readFileSync(join(d, 'android/local.properties'), 'utf8')).toBe(props);
});
test('play build --dry: a build file that does not read -PversionCode is warned about before Gradle, with the line to use', () => {
const d = project('cli-play-vc');
touch(d, 'android/app/build.gradle', "android {\n defaultConfig {\n versionCode System.getenv('VERSION_CODE') ? System.getenv('VERSION_CODE').toInteger() : 1\n versionName project.findProperty('versionName') ?: '1.0'\n }\n}\n");
let r = cli(d, ['play', 'build', '--dry', '--json', '--version-code', '12']);
expect(r.exitCode).toBe(0);
const j = JSON.parse(r.stdout.toString());
expect(j.versionWarnings).toHaveLength(1);
expect(j.versionWarnings[0]).toContain("android/app/build.gradle:3: versionCode does not come from the project property versionCode");
expect(j.versionWarnings[0]).toContain("versionCode((project.findProperty('versionCode') ?: 1) as Integer)");
r = cli(d, ['play', 'build', '--dry', '--version-code', '12']);
expect(r.stderr.toString()).toContain('-PversionCode from play build is ignored');
touch(d, 'android/app/build.gradle', "android {\n defaultConfig {\n versionCode((project.findProperty('versionCode') ?: 1) as Integer)\n versionName project.findProperty('versionName') ?: '1.0'\n }\n}\n");
r = cli(d, ['play', 'build', '--dry', '--json', '--version-code', '12']);
expect(JSON.parse(r.stdout.toString()).versionWarnings).toEqual([]);
});
test('play build: an AAB with another versionCode than planned is a hard stop, not copied', () => {
const d = project('cli-play-stop');
touch(d, 'android/app/build.gradle', "android {\n defaultConfig {\n versionCode 1\n versionName '1.0'\n }\n}\n");
const built = join(d, 'prebuilt.aab');
writeFileSync(built, makeAab('com.example.game', 1, '1.0'));
touch(d, 'android/gradlew', `#!/bin/sh\nmkdir -p app/build/outputs/bundle/release && cp "${built}" app/build/outputs/bundle/release/app-release.aab\n`);
chmodSync(join(d, 'android/gradlew'), 0o755);
const sdk = dir('sdk-folder-stop');
mkdirSync(join(sdk, 'platform-tools'), { recursive: true });
const r = Bun.spawnSync([process.execPath, join(ROOT, 'cli/index.ts'), 'play', 'build', '--version-code', '12', '--no-prebuild', '--allow-stale-web'], { cwd: d, stdout: 'pipe', stderr: 'pipe',
env: { ...process.env, ASC_KEY_ID: '', ASC_ISSUER_ID: '', ASC_P8: '', PLAY_SERVICE_ACCOUNT: '', HOME: dir('clihome-stop'), ANDROID_HOME: sdk, ANDROID_SDK_ROOT: '', ANDROID_KEYSTORE: '' } });
expect(r.exitCode).toBe(1);
expect(r.stderr.toString()).toContain('versionCode is a literal (versionCode 1)');
expect(r.stderr.toString()).toContain('the AAB has versionCode 1, not 12');
expect(r.stderr.toString()).toContain("Use in defaultConfig of android/app/build.gradle: versionCode((project.findProperty('versionCode') ?: 1) as Integer)");
expect(existsSync(join(d, 'build/android/com.example.game-1.aab'))).toBe(false);
});
test('play upload refuses production', () => {
const d = project('cli-up');
writeFileSync(join(d, 'app.aab'), makeAab('com.example.game', 3));