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

@ -10,6 +10,9 @@
// .storebound/credentials.json (ANDROID_KEYSTORE, ANDROID_KEYSTORE_PASSWORD, ANDROID_KEY_ALIAS, ANDROID_KEY_PASSWORD)
// goes to Gradle as injected signing properties through the environment, never on the command line; without it the
// project's own signingConfig signs. Local only: Play is only read (version codes).
// Before Gradle runs, the app module's build file must read -PversionCode / -PversionName in defaultConfig (a literal
// or an env var keeps the AAB at that value; android.injected.version.* does not override it for bundleRelease): a
// warning with the exact line to use. After the build, an AAB whose versionCode is not the planned one is a hard stop.
import { copyFileSync, existsSync, mkdirSync, writeFileSync } from 'node:fs';
import { join, relative } from 'node:path';
import { androidSigning } from '../lib/credentials';
@ -17,7 +20,7 @@ import type { Ctx } from '../lib/ctx';
import { need } from '../lib/ctx';
import { done, log, printJson, warn } from '../lib/log';
import { pump } from '../lib/runner';
import { aabInfo, findBundles, gradleArgs, pickGradle, signingEnv } from '../lib/gradle';
import { aabInfo, appBuildFile, findBundles, gradleArgs, pickGradle, signingEnv, versionSourceIssues, versionSourceMessage } from '../lib/gradle';
import { nextVersionCode } from '../lib/release';
import { capacitorProject, webStep } from '../lib/web-assets';
import { androidSdk, androidSdkEnv, androidSdkMissing } from '../lib/tools';
@ -61,9 +64,12 @@ export async function run(ctx: Ctx): Promise<void> {
// the SDK as Android Studio finds it (ANDROID_HOME is often unset in non-interactive shells and local.properties is
// gitignored): handed to Gradle through the environment, local.properties is never written
const sdk = androidSdk([t.dir]);
// the build file must read -PversionCode / -PversionName (Flutter passes --build-number / --build-name instead)
const buildFile = t.framework === 'flutter' ? null : appBuildFile(t);
const versionWarnings = buildFile ? versionSourceIssues(buildFile).map(i => versionSourceMessage(i, relative(root, buildFile), t.framework)) : [];
const plan = { framework: t.framework, dir: relative(root, t.dir) || '.', module: t.module, task: t.framework === 'flutter' ? 'flutter build appbundle' : t.task,
versionCode: code || null, versionName: versionName ?? null, signing: signing ? `upload key from ${process.env.ANDROID_KEYSTORE ? 'env' : '.storebound/credentials.json'} (alias ${signing.keyAlias})` : 'the project\'s own signingConfig',
androidSdk: sdk.path ? `${sdk.path} (${sdk.source})` : null, command: argv.join(' '), out: relative(root, out) || '.', notes: t.notes };
androidSdk: sdk.path ? `${sdk.path} (${sdk.source})` : null, command: argv.join(' '), out: relative(root, out) || '.', notes: t.notes, versionWarnings };
const webOpts = { config, root, cap, store: 'play' as const, tag: TAG, noPrebuild: a.has('no-prebuild'), allowStale: a.has('allow-stale-web') };
if (dry) {
const web = await webStep({ ...webOpts, dry: true, quiet: json });
@ -72,6 +78,7 @@ export async function run(ctx: Ctx): Promise<void> {
log(TAG, `signing: ${plan.signing}`);
if (sdk.path) log(TAG, `Android SDK: ${sdk.path} (from ${sdk.source})`); else warn(TAG, androidSdkMissing(sdk));
for (const n of t.notes) log(TAG, n);
for (const w of versionWarnings) warn(TAG, w);
log(TAG, `would run (in ${relative(root, cwd) || '.'}): ${plan.command}`);
done(TAG, 'build planned (dry run)');
return;
@ -82,6 +89,7 @@ export async function run(ctx: Ctx): Promise<void> {
if (signing && !existsSync(signing.storeFile)) throw new Error('ANDROID_KEYSTORE: the keystore file does not exist');
if (!signing) warn(TAG, 'no ANDROID_KEYSTORE: the project\'s own release signingConfig must sign, else Play refuses the bundle');
for (const n of t.notes) log(TAG, n);
for (const w of versionWarnings) warn(TAG, w);
log(TAG, `$ ${plan.command}`);
const started = Date.now();
mkdirSync(out, { recursive: true });
@ -100,7 +108,11 @@ export async function run(ctx: Ctx): Promise<void> {
if (!aab) throw new Error(`build finished but no new .aab under ${relative(root, t.dir)}/${t.module}/build/outputs/bundle`);
const info = aabInfo(aab);
if (info.packageName !== pkg) warn(TAG, `the AAB's package is ${info.packageName}, the config says ${pkg}`);
if (code && info.versionCode !== code) warn(TAG, `the AAB has versionCode ${info.versionCode}, not ${code}: the build file sets it itself; read -PversionCode there or pass that number to play upload knowingly`);
// the hard stop: an AAB with another versionCode than planned is never copied to the output folder
if (code && info.versionCode !== code) {
const fix = buildFile ? versionSourceIssues(buildFile).filter(i => i.field === 'versionCode').map(i => ` Use in defaultConfig of ${relative(root, buildFile)}: ${i.fix}`).join('') : '';
throw new Error(`the AAB has versionCode ${info.versionCode}, not ${code}: the build file sets it itself and ignores -PversionCode (android.injected.version.code does not override it).${fix || ' Read -PversionCode in defaultConfig.'} Then build again. (Built: ${relative(root, aab)}, not copied.)`);
}
const dest = join(out, `${info.packageName}-${info.versionCode}.aab`);
copyFileSync(aab, dest);
const result = { aab: dest, packageName: info.packageName, versionCode: info.versionCode, versionName: info.versionName ?? null, web: web.bundle };