diff --git a/CHANGELOG.md b/CHANGELOG.md index 9825741..96fafa0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased +- `play release --dry` uploads nothing: it names the versionCode it would upload and the track changes it would make (the bundle went into a discarded edit, and a long upload could outlive the edit: "This edit has expired"). - `play build`: an absolute `--dir` / `play.build.dir` stays absolute (it was taken as a folder under the project and lost its leading slash); a `gradlew` without the exec bit (committed from Windows) is made executable with a note to commit the mode. The desktop release signs by key path only. ## 0.8.59 ยท 2026-10-29 diff --git a/cli/play/release.ts b/cli/play/release.ts index b1715e2..7ba7c8a 100644 --- a/cli/play/release.ts +++ b/cli/play/release.ts @@ -82,12 +82,20 @@ export async function run(ctx: Ctx): Promise { } let code = fixed ?? ''; let uploaded = false; + // a dry run never uploads: the bundle would go into an edit that is thrown away (14 MB for nothing, and a long + // upload can outlive the edit: "This edit has expired"); its tracks are shown, not PUT (Play refuses a track + // with a versionCode the edit does not have) + let planOnly = false; if (aab) { // a versionCode Play already has (an earlier run uploaded it) is reused, never uploaded twice const info = aabInfo(aab); if ((await usedVersionCodes(play, id)).includes(info.versionCode)) { code = String(info.versionCode); log('play', `versionCode ${code} is on Play already: not uploaded again`); + } else if (a.has('dry')) { + code = String(info.versionCode); + planOnly = true; + log('play', `dry run: would upload ${aab} (${(statSync(aab).size / 1048576).toFixed(1)} MB, versionCode ${code}); not uploaded`); } else { shipPhase('upload', `uploading ${aab}`, 0); log('play', `uploading ${aab} (${(statSync(aab).size / 1048576).toFixed(1)} MB), this can take minutes`); @@ -108,6 +116,7 @@ export async function run(ctx: Ctx): Promise { if (t.status === 'inProgress') release.userFraction = Number(fraction ?? 0.1); const cur = (await play.call<{ releases?: PlayTrackRelease[] }>('GET', `${play.API}/edits/${id}/tracks/${t.track}`).catch(() => ({ releases: [] }))).releases ?? []; if (sameRelease(cur, release)) { log('play', `${t.track}: already ${t.status} ${code}, unchanged`); continue; } + if (planOnly) { log('play', `dry run: ${t.track} <- ${code} (${t.status}, notes in ${notes.length} languages)`); changed++; continue; } await play.call('PUT', `${play.API}/edits/${id}/tracks/${t.track}`, { track: t.track, releases: [release] }); changed++; log('play', `${t.track} <- ${code} (${t.status}, notes in ${notes.length} languages)`); diff --git a/tests/idempotency.test.ts b/tests/idempotency.test.ts index 39d22af..49f3775 100644 --- a/tests/idempotency.test.ts +++ b/tests/idempotency.test.ts @@ -573,6 +573,13 @@ describe('listing, appinfo, data safety and the owner commands: a second run wri expect(s.play.committed).toHaveLength(1); }); + test('play release --dry: no upload, no track PUT (the bundle went into a discarded edit; a long upload outlived it: "This edit has expired")', async () => { + const p = project(CONFIG, { 'store/listing.md': LISTING, 'build/android/app.aab': makeAab('com.example.notes', 43) }); + const r = await twice(() => playRelease(p.ctx([join(p.root, 'build/android/app.aab'), '--internal', 'completed', '--dry']))); + expect(r.first.filter(x => x.includes('/bundles') || x.startsWith('PUT '))).toEqual([]); + expect(s.play.committed).toHaveLength(0); + }); + test('prices import (Play): changed regions written once, then the diff is empty', async () => { const p = project({ ...CONFIG, products: [CONFIG.products![0]!] }, { 'store/prices.csv': 'product,store,region,currency,price\ncom.example.notes.pro,play,DE,EUR,3.49\n' }); await capture(() => playProducts(p.ctx([])));