From 6ebb6560183edee9a26e7d3906d2691c77ca8f59 Mon Sep 17 00:00:00 2001 From: CRT Date: Thu, 29 Oct 2026 19:59:49 +0700 Subject: [PATCH] apple listing: a new version inherits the previous version's release type when apple.releaseType is unset; warn when the config differs --- CHANGELOG.md | 1 + cli/apple/listing.ts | 1 + cli/lib/listing-plan.ts | 14 ++++++++++++-- tests/listing-plan.test.ts | 2 +- tests/release-type-inherit.test.ts | 16 ++++++++++++++++ 5 files changed, 31 insertions(+), 3 deletions(-) create mode 100644 tests/release-type-inherit.test.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index 96fafa0..cce219c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ ## Unreleased +- `apple listing` creates a new version with the release type of the newest version before it (MANUAL or AFTER_APPROVAL) when `apple.releaseType` is not set; a first version stays MANUAL. The plan names where the type came from, and warns when `apple.releaseType` differs from the previous version's. - `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. diff --git a/cli/apple/listing.ts b/cli/apple/listing.ts index 698b799..9649aa9 100644 --- a/cli/apple/listing.ts +++ b/cli/apple/listing.ts @@ -157,6 +157,7 @@ export function listingPlanLines(p: AppleListingPlan): string[] { const v = p.versionPlan; const out = [`Version ${p.version} (${platformLabel(p.platform)}; App Store Connect has: ${v.existing.length ? v.existing.map(x => `${x.versionString} ${x.state} ${platformLabel(x.platform)}`).join(', ') : `no ${platformLabel(p.platform)} version yet`})`]; out.push(` ${v.action === 'refuse' ? 'REFUSE' : v.action.toUpperCase()}: ${v.reason}`); + if (v.warning) out.push(` WARN: ${v.warning}`); for (const ch of v.changes) out.push(` ${ch.field}: ${v.action === 'create' ? short(ch.to) : `${short(ch.from)} -> ${short(ch.to)}`}`); if (p.build) out.push(`Build: ${p.build.action === 'refuse' ? 'REFUSE: ' : ''}${p.build.reason}`); out.push(`App info ${p.appInfo.state ? `(${p.appInfo.state})` : ''}: name, subtitle, privacy policy URL per locale ยท ${counts(p.appInfo.locales)}`); diff --git a/cli/lib/listing-plan.ts b/cli/lib/listing-plan.ts index 40edf0c..d6bb289 100644 --- a/cli/lib/listing-plan.ts +++ b/cli/lib/listing-plan.ts @@ -38,6 +38,8 @@ export interface VersionPlan { /** The editable version that is kept or updated. */ id?: string; reason: string; + /** Said in the dry run and the real run (e.g. a release type that differs from the previous version's). */ + warning?: string; existing: VersionInfo[]; changes: FieldDiff[]; attributes: Record; @@ -103,9 +105,17 @@ export function planVersion(rows: { id: string; attributes: Record = { platform, versionString: want, copyright: attrs.copyright, releaseType: attrs.releaseType ?? 'MANUAL' }; + // without apple.releaseType a new version keeps the release type of the newest one before it (Acme 2026-10-29: an + // app the owner set to release after approval got a MANUAL 2.0.2 and would have sat unreleased); a first version is MANUAL + const prev = others.slice().sort((a, b) => compareVersions(b.versionString, a.versionString))[0]; + const prevType = prev ? rows.find(r => r.id === prev.id)?.attributes.releaseType : undefined; + const inherited = !attrs.releaseType && typeof prevType === 'string' && ['MANUAL', 'AFTER_APPROVAL'].includes(prevType) ? prevType : undefined; + const attributes: Record = { platform, versionString: want, copyright: attrs.copyright, releaseType: attrs.releaseType ?? inherited ?? 'MANUAL' }; if (attributes.releaseType === 'SCHEDULED' && attrs.earliestReleaseDate) attributes.earliestReleaseDate = attrs.earliestReleaseDate; - return { ...base, action: 'create', reason: `create version ${want} (no editable ${PLATFORM_LABEL[platform] ?? platform} version)`, changes: Object.entries(attributes).map(([field, to]) => ({ field, from: null, to })), attributes }; + const why = attrs.releaseType ? 'apple.releaseType' : inherited ? `as version ${prev!.versionString}` : prev ? `version ${prev.versionString} has ${String(prevType ?? 'no readable type')}` : 'first version'; + return { ...base, action: 'create', reason: `create version ${want} (no editable ${PLATFORM_LABEL[platform] ?? platform} version), release type ${attributes.releaseType} (${why})`, + changes: Object.entries(attributes).map(([field, to]) => ({ field, from: null, to })), attributes, + ...(attrs.releaseType && typeof prevType === 'string' && prevType !== attrs.releaseType ? { warning: `release type ${attrs.releaseType} (apple.releaseType) differs from version ${prev!.versionString} (${prevType})` } : {}) }; } // ---- translation drafts ---- diff --git a/tests/listing-plan.test.ts b/tests/listing-plan.test.ts index 638b555..a3acfbf 100644 --- a/tests/listing-plan.test.ts +++ b/tests/listing-plan.test.ts @@ -101,7 +101,7 @@ describe('apple listing plan', () => { const p = project({ ...BASE, play: undefined }, { 'store/appstore.md': APPLE_MD }); const calls = ascRoutes([v('v-0', '1.0.0', 'READY_FOR_DISTRIBUTION')]); const j = jsonOut((await capture(() => appleListing(p.ctx(['--version', '1.1.0', '--dry', '--json'])))).out) as any; - expect(j.versionPlan).toMatchObject({ action: 'create', reason: 'create version 1.1.0 (no editable iOS version)' }); + expect(j.versionPlan).toMatchObject({ action: 'create', reason: expect.stringContaining('create version 1.1.0 (no editable iOS version), release type MANUAL') }); expect(j.versionLocales).toBeNull(); await capture(() => appleListing(p.ctx(['--version', '1.1.0']))); expect(methods(calls).filter(m => m.includes('appStoreVersion'))).toEqual(['POST /v1/appStoreVersions', 'POST /v1/appStoreVersionLocalizations']); diff --git a/tests/release-type-inherit.test.ts b/tests/release-type-inherit.test.ts new file mode 100644 index 0000000..f2a8f76 --- /dev/null +++ b/tests/release-type-inherit.test.ts @@ -0,0 +1,16 @@ +import { expect, test } from 'bun:test'; +import { planVersion } from '../cli/lib/listing-plan'; + +const row = (id: string, versionString: string, state: string, releaseType: string) => ({ id, attributes: { versionString, appStoreState: state, appVersionState: state, platform: 'IOS', releaseType } }); + +test('a new version keeps the release type of the newest version before it unless apple.releaseType is set', () => { + const rows = [row('v1', '2.0.0', 'READY_FOR_DISTRIBUTION', 'MANUAL'), row('v2', '2.0.1', 'READY_FOR_DISTRIBUTION', 'AFTER_APPROVAL')]; + const p = planVersion(rows, '2.0.2', { copyright: 'c' }, 'IOS'); + expect(p.action).toBe('create'); + expect(p.attributes.releaseType).toBe('AFTER_APPROVAL'); + expect(p.reason).toContain('as version 2.0.1'); + const set = planVersion(rows, '2.0.2', { copyright: 'c', releaseType: 'MANUAL' }, 'IOS'); + expect(set.attributes.releaseType).toBe('MANUAL'); + expect(set.warning).toContain('differs from version 2.0.1 (AFTER_APPROVAL)'); + expect(planVersion([], '1.0.0', { copyright: 'c' }, 'IOS').attributes.releaseType).toBe('MANUAL'); +});