diff --git a/CHANGELOG.md b/CHANGELOG.md index 545fbc3..9e9512a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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). diff --git a/cli/lib/gradle.ts b/cli/lib/gradle.ts index e71ecac..9d868bc 100644 --- a/cli/lib/gradle.ts +++ b/cli/lib/gradle.ts @@ -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. */ diff --git a/cli/play/build.ts b/cli/play/build.ts index 936f40e..8fd4295 100644 --- a/cli/play/build.ts +++ b/cli/play/build.ts @@ -8,8 +8,8 @@ // unless --version-code is given, runs `gradlew :app:bundleRelease` (Flutter: flutter build appbundle) and copies the // AAB to /-.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 diff --git a/skills/store-pitfalls/SKILL.md b/skills/store-pitfalls/SKILL.md index 6454049..0ee01c1 100644 --- a/skills/store-pitfalls/SKILL.md +++ b/skills/store-pitfalls/SKILL.md @@ -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`). diff --git a/tests/build-sign.test.ts b/tests/build-sign.test.ts index 33c7bb7..9e3faa8 100644 --- a/tests/build-sign.test.ts +++ b/tests/build-sign.test.ts @@ -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'); });