play build: signing passwords only through the child environment, never argv

This commit is contained in:
CRTkafa 2026-10-23 08:51:07 +08:00
parent 7960063e0e
commit e4ebbb825d
5 changed files with 13 additions and 12 deletions

View file

@ -6,7 +6,7 @@
- `storebound apps ignore|unignore [--store apple|play]`: hide dormant apps, or one store side of an app, from watch, reads and the desktop (MCP apps_ignore / apps_unignore).
- Reviews: a store read that fails is an error, not an empty "no new reviews" list; the Reviews tab notes Apple API lag and Play's 7-day window (pitfall 106).
- Desktop: live store thumbnails load right away, also in a hidden window.
- `play build` passes the upload key (ANDROID_KEYSTORE, ...) to Gradle as `-Pandroid.injected.signing.*` too (passwords masked in the printed command), so a build file that signs only from a local keystore.properties no longer gives an unsigned AAB in CI (pitfall 107).
- `play build` passes the upload key (ANDROID_KEYSTORE, ...) to Gradle as injected signing properties (store file and alias as `-P` arguments, the passwords only through `ORG_GRADLE_PROJECT_*` environment variables, never on argv), so a build file that signs only from a local keystore.properties no longer gives an unsigned AAB in CI (pitfall 107).
- `play build`, `play upload` and `play release` of a local AAB verify it is signed (META-INF entries, `jarsigner -verify` when a JDK exists) and fail with the fix before any upload.
- `apple build --version` and `apple upload` refuse an IPA whose CFBundleShortVersionString is not the intended / editable App Store version; `--allow-version-mismatch` overrides (pitfall 108).
- Play release notes that name iOS, iPhone or the App Store are caught by `storebound check` and `play build`, not only at release time (pitfall 109).

View file

@ -70,11 +70,10 @@ export function gradleArgs(t: GradleTarget, o: { versionCode?: number; versionNa
/** The upload key as Gradle project properties (-Pandroid.injected.signing.*). A project whose build file signs only
* from a local keystore.properties (absent in CI) gives an unsigned AAB otherwise (Play: 403 "All uploaded bundles
* must be signed"). The passwords are on this argv: print it only through maskArgs. */
* must be signed"). Only the file and alias go on argv; the passwords travel through signingEnv (ORG_GRADLE_PROJECT_*), never argv. */
export function signingArgs(s: AndroidSigning | null): string[] {
if (!s) return [];
return [`-Pandroid.injected.signing.store.file=${s.storeFile}`, `-Pandroid.injected.signing.store.password=${s.storePassword}`,
`-Pandroid.injected.signing.key.alias=${s.keyAlias}`, `-Pandroid.injected.signing.key.password=${s.keyPassword}`];
return [`-Pandroid.injected.signing.store.file=${s.storeFile}`, `-Pandroid.injected.signing.key.alias=${s.keyAlias}`];
}
/** An argv for logs and plans: the signing passwords are replaced by ***. Pure. */

View file

@ -8,8 +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 (-Pandroid.injected.signing.* and the ORG_GRADLE_PROJECT_ environment; the
// passwords are masked in every printed command); without it the
// goes to Gradle as injected signing properties (store file and alias as -P arguments, the passwords only through
// ORG_GRADLE_PROJECT_* in the child environment, never argv); 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

View file

@ -1290,9 +1290,8 @@ that fails without the fix (see CONTRIBUTING.md).
upload key in `ANDROID_KEYSTORE` / `ANDROID_KEYSTORE_PASSWORD` / `ANDROID_KEY_ALIAS` / `ANDROID_KEY_PASSWORD` reached
Gradle only through `ORG_GRADLE_PROJECT_*`, which that build file does not use; the AAB came out unsigned and the
refusal came only at upload.
- **Do:** set the four variables (env or `.storebound/credentials.json`); `play build` now also passes them as
`-Pandroid.injected.signing.store.file|store.password|key.alias|key.password`. The printed command and the plan show
the passwords as `***`.
- **Do:** set the four variables (env or `.storebound/credentials.json`); `play build` now also passes `-Pandroid.injected.signing.store.file` and `.key.alias`; the
passwords go only through `ORG_GRADLE_PROJECT_android.injected.signing.*.password` in the child environment, never argv.
- **Guard:** `signingArgs` / `maskArgs` (`cli/lib/gradle.ts`); right after the build, and before `play upload` /
`play release` of a local AAB, `assertAabSigned` checks the META-INF `*.SF` + `*.RSA|DSA|EC` entries (and runs
`jarsigner -verify` when a JDK is on PATH) and fails with the fix, before any upload (`tests/build-sign.test.ts`).

View file

@ -532,13 +532,16 @@ describe('signed AAB, masked signing, IPA version train, release notes', () => {
test('gradleArgs passes the injected signing properties, maskArgs hides the passwords', () => {
const argv = gradleArgs(t, { hasWrapper: true, signing: s }, 'linux');
expect(argv).toContain('-Pandroid.injected.signing.store.file=/k/upload.jks');
expect(argv).toContain('-Pandroid.injected.signing.store.password=pw-store');
expect(argv).toContain('-Pandroid.injected.signing.key.alias=upload');
expect(argv).toContain('-Pandroid.injected.signing.key.password=pw-key');
expect(argv.join(' ')).not.toContain('pw-store');
expect(argv.join(' ')).not.toContain('pw-key');
expect(argv.join(' ')).not.toContain('password');
const env = signingEnv(s);
expect(env['ORG_GRADLE_PROJECT_android.injected.signing.store.password']).toBe('pw-store');
expect(env['ORG_GRADLE_PROJECT_android.injected.signing.key.password']).toBe('pw-key');
const shown = maskArgs(argv).join(' ');
expect(shown).not.toContain('pw-store');
expect(shown).not.toContain('pw-key');
expect(shown).toContain('store.password=***');
expect(shown).toContain('key.alias=upload');
expect(gradleArgs(t, { hasWrapper: true }, 'linux').join(' ')).not.toContain('signing');
});