Updates are agent steps: --yes ships an update of a live app (Play production, App Store submit / release) without an owner gate; the first release stays the owner's (exit 3 OWNER_STEP, --first-release); apple release is a ship step
This commit is contained in:
parent
c247b7e54a
commit
ae9d26d3f6
28 changed files with 562 additions and 44 deletions
|
|
@ -1,15 +1,21 @@
|
|||
// Release management for an App Store version.
|
||||
// storebound apple release --phased on|pause|resume|complete [--version 1.2.0] [--dry] [--yes]
|
||||
// storebound apple release --manual-release [--version 1.2.0] [--yes]
|
||||
// storebound apple release --manual-release [--version 1.2.0] [--first-release] [--yes] [--json]
|
||||
// Both take --platform ios|macos|tvos|visionos; without it the platform that has the version this needs is used (both: asks).
|
||||
// Phased release: 7-day automatic rollout to users with automatic updates (1, 2, 5, 10, 20, 50, 100 %); anyone can
|
||||
// still update by hand from the App Store. Pause holds it (up to 30 days in total), it is NOT a rollback: users who
|
||||
// updated keep the build. "complete" releases to everyone at once.
|
||||
// --manual-release releases an approved version (PENDING_DEVELOPER_RELEASE). Public and irreversible: asks first.
|
||||
// Neither --manual-release nor resume / complete is offered to agents through `storebound mcp`.
|
||||
// --manual-release releases an approved version (PENDING_DEVELOPER_RELEASE). Public and irreversible: asks first
|
||||
// (--yes in scripts). Releasing an UPDATE of an app already on sale is an agent step (no owner gate, also in a
|
||||
// pipeline; under the ship-step contract: exit 7 when the version is released already). The first-ever release of an
|
||||
// app with --yes is exit 3 OWNER_STEP unless the owner approved it (--first-release, or bypass mode;
|
||||
// cli/lib/release-kind.ts). Neither --manual-release nor resume / complete is offered to agents through `storebound mcp`.
|
||||
import type { Ctx } from '../lib/ctx';
|
||||
import { confirm, need } from '../lib/ctx';
|
||||
import { done, log } from '../lib/log';
|
||||
import { done, log, printJson } from '../lib/log';
|
||||
import { shipNoop, shipReport, shipRun } from '../lib/ship-step';
|
||||
import { consoleUrl } from '../lib/console-links';
|
||||
import { appleReleaseKind, enforceFirstRelease } from '../lib/release-kind';
|
||||
import { type PhasedAction, phasedRequest } from '../lib/release';
|
||||
import { createOnce } from '../lib/upsert';
|
||||
import { ascClient, type J } from './asc';
|
||||
|
|
@ -27,6 +33,19 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
const appId = await c.app();
|
||||
const want = a.get('version');
|
||||
const state = (v: J): string => v.attributes.appVersionState ?? v.attributes.appStoreState;
|
||||
shipReport({ version: want, url: consoleUrl('asc.version') });
|
||||
// the ship-step contract: the version is released already (and none waits for release): nothing to do, exit 7
|
||||
if (manual && shipRun()) {
|
||||
const every = await allVersions(c, appId);
|
||||
const waiting = every.some((x: J) => state(x) === 'PENDING_DEVELOPER_RELEASE' && (!want || x.attributes.versionString === want));
|
||||
const out = every.find((x: J) => ['READY_FOR_SALE', 'READY_FOR_DISTRIBUTION', 'PROCESSING_FOR_DISTRIBUTION'].includes(state(x)) && (!want || x.attributes.versionString === want));
|
||||
if (!waiting && out) {
|
||||
shipNoop({ version: String(out.attributes.versionString), state: state(out) });
|
||||
if (a.has('json')) printJson({ version: String(out.attributes.versionString), state: state(out), released: true, alreadyThere: true });
|
||||
done('apple', `${String(out.attributes.versionString)} is ${state(out)} already: nothing released`);
|
||||
return;
|
||||
}
|
||||
}
|
||||
// the platform: --platform, else the one with the version this needs (both: asks)
|
||||
const needs = (x: J): boolean => (!want || x.attributes.versionString === want) && (manual ? state(x) === 'PENDING_DEVELOPER_RELEASE'
|
||||
: phased === 'on' ? !['READY_FOR_DISTRIBUTION', 'READY_FOR_SALE', 'REPLACED_WITH_NEW_VERSION', 'REMOVED_FROM_SALE'].includes(state(x))
|
||||
|
|
@ -44,10 +63,19 @@ export async function run(ctx: Ctx): Promise<void> {
|
|||
if (state(v) !== 'PENDING_DEVELOPER_RELEASE') throw new Error(`${tag}: only an approved version waiting for developer release can be released`);
|
||||
const pr = (await c.maybe(`/v1/appStoreVersions/${v.id}/appStoreVersionPhasedRelease`))?.data;
|
||||
const how = pr ? 'phased over 7 days (phased release is on)' : 'to everyone at once (no phased release)';
|
||||
if (dry) { log('apple', `would release ${tag} ${how} (dry)`); return; }
|
||||
const kind = appleReleaseKind(versions.map((x: J) => ({ versionString: String(x.attributes.versionString), state: state(x) })));
|
||||
shipReport({ version: String(v.attributes.versionString), state: state(v) });
|
||||
if (dry) {
|
||||
log('apple', `would release ${tag} ${how} (dry): ${kind.kind === 'update' ? 'update of an app on sale, no owner step' : 'FIRST release of this app (the owner approves it once: --first-release)'} (${kind.why})`);
|
||||
if (a.has('json')) printJson({ version: String(v.attributes.versionString), state: state(v), releaseKind: kind.kind, dryRun: true });
|
||||
return;
|
||||
}
|
||||
if (!enforceFirstRelease(ctx, kind, { store: 'apple', what: 'the first release of the app', consoleUrl: consoleUrl('asc.version') })) return;
|
||||
if (!(await confirm(ctx, `Release ${tag} on the App Store now, ${how}? This is public.`))) return;
|
||||
await c.asc('POST', '/v1/appStoreVersionReleaseRequests', { data: { type: 'appStoreVersionReleaseRequests',
|
||||
relationships: { appStoreVersion: { data: { type: 'appStoreVersions', id: v.id } } } } });
|
||||
shipReport({ state: 'PENDING_APPLE_RELEASE' });
|
||||
if (a.has('json')) printJson({ version: String(v.attributes.versionString), state: 'PENDING_APPLE_RELEASE', releaseKind: kind.kind, released: true });
|
||||
done('apple', `release requested for ${tag}; the store updates within a few hours`);
|
||||
return;
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue