Signed AAB guardrails, injected signing args, IPA version train check, Play notes lint at build
This commit is contained in:
parent
707db2ebc6
commit
7960063e0e
12 changed files with 181 additions and 14 deletions
|
|
@ -8,7 +8,8 @@
|
|||
// unless --version-code is given, runs `gradlew :app:bundleRelease` (Flutter: flutter build appbundle) and copies the
|
||||
// AAB to <out>/<package>-<versionCode>.aab, checked by reading its manifest. Signing: the upload key from env or
|
||||
// .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
|
||||
// goes to Gradle as injected signing properties (-Pandroid.injected.signing.* and the ORG_GRADLE_PROJECT_ environment; the
|
||||
// passwords are masked in every printed command); 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
|
||||
|
|
@ -20,16 +21,25 @@ 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, appBuildFile, findBundles, gradleArgs, pickGradle, signingEnv, versionSourceIssues, versionSourceMessage } from '../lib/gradle';
|
||||
import { aabInfo, appBuildFile, assertAabSigned, maskArgs, 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';
|
||||
import { playClient } from './client';
|
||||
import { readPlayCopy, refusePlayNotes } from './listing';
|
||||
import { usedVersionCodes } from './next-version-code';
|
||||
import * as rt from '../lib/rt';
|
||||
|
||||
const TAG = 'build';
|
||||
|
||||
/** Release notes that name iOS / App Store are refused at release time: refuse them before the long build. A missing
|
||||
* listing file is not a build problem. */
|
||||
export function notesGuard(ctx: Ctx): void {
|
||||
let notes: { language: string; text: string }[];
|
||||
try { notes = readPlayCopy(ctx).filter(c => c.whatsNew).map(c => ({ language: c.lang, text: c.whatsNew })); } catch { return; }
|
||||
refusePlayNotes(ctx, notes, 'play build (release notes of the listing)');
|
||||
}
|
||||
|
||||
export async function run(ctx: Ctx): Promise<void> {
|
||||
const { config, root, resolve } = ctx.loaded;
|
||||
const a = ctx.args;
|
||||
|
|
@ -60,7 +70,7 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
const versionName = a.get('version-name');
|
||||
const signing = androidSigning(root);
|
||||
const hasWrapper = existsSync(join(t.dir, process.platform === 'win32' ? 'gradlew.bat' : 'gradlew'));
|
||||
const argv = gradleArgs(t, { versionCode: code || undefined, versionName, flavor, hasWrapper });
|
||||
const argv = gradleArgs(t, { versionCode: code || undefined, versionName, flavor, hasWrapper, signing });
|
||||
const cwd = t.framework === 'flutter' ? root : t.dir;
|
||||
// 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
|
||||
|
|
@ -70,7 +80,7 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
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, versionWarnings };
|
||||
androidSdk: sdk.path ? `${sdk.path} (${sdk.source})` : null, command: maskArgs(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 });
|
||||
|
|
@ -85,6 +95,7 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
return;
|
||||
}
|
||||
if (!sdk.path) throw new Error(androidSdkMissing(sdk));
|
||||
notesGuard(ctx);
|
||||
log(TAG, `Android SDK: ${sdk.path} (from ${sdk.source})`);
|
||||
const web = await webStep({ ...webOpts, dry: false });
|
||||
if (signing && !existsSync(signing.storeFile)) throw new Error('ANDROID_KEYSTORE: the keystore file does not exist');
|
||||
|
|
@ -108,6 +119,8 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
const aab = findBundles([join(t.dir, t.module, 'build', 'outputs', 'bundle'), join(root, 'build', 'app', 'outputs', 'bundle')], started - 1000)[0];
|
||||
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);
|
||||
// Play refuses an unsigned bundle (403) only at upload: stop here, naming the fix
|
||||
assertAabSigned(aab, relative(root, aab));
|
||||
if (info.packageName !== pkg) warn(TAG, `the AAB's package is ${info.packageName}, the config says ${pkg}`);
|
||||
// the hard stop: an AAB with another versionCode than planned is never copied to the output folder
|
||||
if (code && info.versionCode !== code) {
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ import { createHash } from 'node:crypto';
|
|||
import type { Ctx } from '../lib/ctx';
|
||||
import { confirm, need } from '../lib/ctx';
|
||||
import { done, log, warn, printJson } from '../lib/log';
|
||||
import { aabInfo } from '../lib/gradle';
|
||||
import { aabInfo, assertAabSigned } from '../lib/gradle';
|
||||
import { playTargetSdkProblem, targetSdkFile } from '../lib/store-minimums';
|
||||
import { playClient } from './client';
|
||||
import { usedVersionCodes } from './next-version-code';
|
||||
|
|
@ -52,6 +52,7 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
if (live && !a.has('dry') && !(await confirm(ctx, `Release to PRODUCTION as "${live.status}" (public once Google approves)?`))) return;
|
||||
|
||||
if (aab) {
|
||||
assertAabSigned(aab);
|
||||
const sdkWhy = playTargetSdkProblem(aabInfo(aab).targetSdk, { where: targetSdkFile(root) });
|
||||
if (sdkWhy) throw new Error(`${aab}: not released, ${sdkWhy}`);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,7 +9,7 @@ import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|||
import { join, relative } from 'node:path';
|
||||
import type { Ctx } from '../lib/ctx';
|
||||
import { confirm, need } from '../lib/ctx';
|
||||
import { aabInfo } from '../lib/gradle';
|
||||
import { aabInfo, assertAabSigned } from '../lib/gradle';
|
||||
import { playTargetSdkProblem, targetSdkFile } from '../lib/store-minimums';
|
||||
import { done, log, printJson } from '../lib/log';
|
||||
import { nextVersionCode } from '../lib/release';
|
||||
|
|
@ -43,6 +43,7 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
// Play refuses bundles below its current target API level (cli/lib/store-minimums.ts): stop before any network call
|
||||
const sdkWhy = playTargetSdkProblem(info.targetSdk, { where: targetSdkFile(root) });
|
||||
if (sdkWhy) throw new Error(`${relative(root, aab)}: not uploaded, ${sdkWhy}`);
|
||||
assertAabSigned(aab, relative(root, aab));
|
||||
const size = statSync(aab).size;
|
||||
const dry = a.has('dry');
|
||||
const json = a.has('json');
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue