From cdd62056ddcf10db1e51c06294b3d381d75d11a9 Mon Sep 17 00:00:00 2001 From: Mansour Koura Date: Thu, 30 Jul 2026 14:24:01 +0300 Subject: [PATCH] feat(api): add release lifecycle states --- CHANGELOG.md | 4 +++ README.md | 1 + docs/cli/releases.mdx | 27 +++++++++++++++ docs/reference/operations.mdx | 3 +- packages/cli/src/cli.ts | 31 +++++++++++++++++ packages/core/src/core.test.ts | 50 +++++++++++++++++++++++++++ packages/core/src/index.ts | 4 +++ packages/core/src/ops-part1.ts | 17 +++++++++ packages/core/src/resources/tracks.ts | 28 +++++++++++++++ skills/gps-release/SKILL.md | 11 ++++-- 10 files changed, 173 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index eb0bcbd..9681a50 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## Unreleased + +- Add read-only release lifecycle states with exact version-code matching. + ## 0.1.0 - Initial release: `@appeeky/google-play-store-core`, `gps` CLI, `gps-mcp` diff --git a/README.md b/README.md index 6e53fe5..fc7097e 100644 --- a/README.md +++ b/README.md @@ -110,6 +110,7 @@ Details: [docs/authentication-accounts.mdx](./docs/authentication-accounts.mdx). ```bash gps --package com.example.app deploy --file ./app.aab --track internal --confirm gps --package com.example.app tracks promote --from internal --to beta --confirm +gps --package com.example.app tracks releases production --version-code 123 --json gps --package com.example.app tracks rollout production --user-fraction 0.1 --confirm gps --package com.example.app tracks halt production --confirm ``` diff --git a/docs/cli/releases.mdx b/docs/cli/releases.mdx index c39a120..20a0126 100644 --- a/docs/cli/releases.mdx +++ b/docs/cli/releases.mdx @@ -18,9 +18,36 @@ gps -p com.example.app deploy \ ```bash gps -p com.example.app tracks list gps -p com.example.app tracks get production +gps -p com.example.app tracks releases production --version-code 123 --json gps -p com.example.app tracks promote --from beta --to production --user-fraction 0.05 --confirm gps -p com.example.app tracks rollout production --user-fraction 0.25 --confirm gps -p com.example.app tracks halt production --confirm ``` +## Release lifecycle + +This command reads the lifecycle state without opening an edit: + +```bash +gps -p com.example.app tracks releases production --version-code 123 --json +``` + +Match the exact version code in release gates. + +`RELEASE_LIFECYCLE_STATE_APPROVED_NOT_PUBLISHED` means Google approved the release. +You must publish these changes in Play Console. +The Android Publisher API does not support that final action. +See the [release lifecycle API](https://developers.google.com/android-publisher/api-ref/rest/v3/applications.tracks.releases). +See the [managed publishing guide](https://support.google.com/googleplay/android-developer/answer/9859654). + +The command also supports MCP and `gps call`: + +```bash +gps call gps_list_release_summaries '{ + "packageName": "com.example.app", + "track": "production", + "versionCode": 123 +}' +``` + Guides: [Deploy a release](/guides/deploy-release), [Staged rollout](/guides/staged-rollout). diff --git a/docs/reference/operations.mdx b/docs/reference/operations.mdx index 75e232e..91948e6 100644 --- a/docs/reference/operations.mdx +++ b/docs/reference/operations.mdx @@ -13,7 +13,7 @@ Write operations are marked **write** and require `--confirm` on the CLI (or age Most Publisher tools also accept an optional `account` argument for multi-key setups. See [Multi-account and apps](/authentication-accounts). -Total operations: **129** +Total operations: **130** ## accounts @@ -53,6 +53,7 @@ Total operations: **129** | `gps_list_tracks` | read | List release tracks | | `gps_get_track` | read | Get a release track | | `gps_list_track_releases` | read | List releases on a track | +| `gps_list_release_summaries` | read | List release lifecycle states on a track | | `gps_update_track` | **write** | Update a track inside a committed edit | | `gps_promote_release` | **write** | Promote a release between tracks | | `gps_update_rollout` | **write** | Update staged rollout fraction | diff --git a/packages/cli/src/cli.ts b/packages/cli/src/cli.ts index 894424e..60e5f2a 100644 --- a/packages/cli/src/cli.ts +++ b/packages/cli/src/cli.ts @@ -429,6 +429,37 @@ export function buildProgram(): Command { printJson(result); }); + tracks + .command("releases") + .description("List release lifecycle states without opening an edit") + .argument("") + .option("--version-code ", "Match an exact active artifact version code") + .action(async (track: string, cmdOpts: { versionCode?: string }) => { + const opts = program.opts(); + const client = await clientFrom(opts); + const packageName = pkg(opts); + const result = await client.tracks.listReleaseSummaries(packageName, track, { + versionCode: cmdOpts.versionCode, + }); + if (cmdOpts.versionCode !== undefined && result.length === 0) { + throw new Error( + `No active release with version code ${cmdOpts.versionCode} exists on track ${track}`, + ); + } + if (opts.json) printJson(result); + else + printTable( + result.map((release) => ({ + release: release.releaseName, + track: release.track, + versionCodes: release.activeArtifacts + ?.map((artifact) => artifact.versionCode) + .join(","), + lifecycleState: release.releaseLifecycleState, + })), + ); + }); + tracks .command("promote") .description("Promote a release from one track to another") diff --git a/packages/core/src/core.test.ts b/packages/core/src/core.test.ts index bcaa219..5e50f7c 100644 --- a/packages/core/src/core.test.ts +++ b/packages/core/src/core.test.ts @@ -190,6 +190,56 @@ describe("edits.withEdit", () => { }); }); +describe("tracks.listReleaseSummaries", () => { + it("reads lifecycle state and matches the exact version code", async () => { + let parent: string | undefined; + const publisher = { + applications: { + tracks: { + releases: { + list: async (params: { parent?: string }) => { + parent = params.parent; + return { + data: { + releases: [ + { + releaseName: "1.2.3", + track: "production", + activeArtifacts: [{ versionCode: 123 }], + releaseLifecycleState: + "RELEASE_LIFECYCLE_STATE_APPROVED_NOT_PUBLISHED", + }, + { + releaseName: "1.2.2", + track: "production", + activeArtifacts: [{ versionCode: 122 }], + releaseLifecycleState: "RELEASE_LIFECYCLE_STATE_PUBLISHED", + }, + ], + }, + }; + }, + }, + }, + }, + } as unknown as Parameters[0]; + + const client = PlayStoreClient.fromPublisher(publisher, { readOnly: true }); + const releases = await client.tracks.listReleaseSummaries( + "com.example.app", + "production", + { versionCode: 123 }, + ); + + expect(parent).toBe("applications/com.example.app/tracks/production"); + expect(releases).toHaveLength(1); + expect(releases[0]?.activeArtifacts?.[0]?.versionCode).toBe(123); + expect(releases[0]?.releaseLifecycleState).toBe( + "RELEASE_LIFECYCLE_STATE_APPROVED_NOT_PUBLISHED", + ); + }); +}); + describe("config multi-account", () => { it("registers accounts and apps and finds package owners", async () => { const { mkdtempSync, rmSync } = await import("node:fs"); diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index 5f6ca7f..2c79187 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -53,3 +53,7 @@ export { ClientRegistry, type ClientRegistryOptions } from "./registry.js"; export type { DeployOptions, DeployResult } from "./resources/deploy.js"; export type { ImageType } from "./resources/listings.js"; +export type { + ReleaseSummary, + ListReleaseSummariesOptions, +} from "./resources/tracks.js"; diff --git a/packages/core/src/ops-part1.ts b/packages/core/src/ops-part1.ts index 0a80c9b..45c65bd 100644 --- a/packages/core/src/ops-part1.ts +++ b/packages/core/src/ops-part1.ts @@ -146,6 +146,23 @@ export const GPS_OPS: GpsOp[] = [ run: async (c, a) => c.tracks.listReleases(asString(a.packageName, "packageName"), asString(a.track, "track")), }, + { + name: "gps_list_release_summaries", + group: "tracks", + summary: "List release lifecycle states on a track", + write: false, + shape: { + packageName: pkg, + track: z.string(), + versionCode: z.union([z.string(), z.number()]).optional(), + }, + run: async (c, a) => + c.tracks.listReleaseSummaries( + asString(a.packageName, "packageName"), + asString(a.track, "track"), + { versionCode: a.versionCode as string | number | undefined }, + ), + }, { name: "gps_update_track", group: "tracks", diff --git a/packages/core/src/resources/tracks.ts b/packages/core/src/resources/tracks.ts index af5a80c..ef9a94d 100644 --- a/packages/core/src/resources/tracks.ts +++ b/packages/core/src/resources/tracks.ts @@ -4,6 +4,11 @@ import { assertPackageName, assertTrack } from "../validation.js"; export type Track = androidpublisher_v3.Schema$Track; export type TrackRelease = androidpublisher_v3.Schema$TrackRelease; +export type ReleaseSummary = androidpublisher_v3.Schema$ReleaseSummary; + +export interface ListReleaseSummariesOptions { + versionCode?: number | string; +} export class TracksApi { constructor(private readonly client: PlayStoreClient) {} @@ -98,6 +103,29 @@ export class TracksApi { }); } + async listReleaseSummaries( + packageName: string, + track: string, + options: ListReleaseSummariesOptions = {}, + ): Promise { + assertPackageName(packageName); + assertTrack(track); + return this.client.request(async () => { + const res = await this.client.publisher.applications.tracks.releases.list({ + parent: `applications/${packageName}/tracks/${track}`, + }); + const releases = res.data.releases ?? []; + if (options.versionCode === undefined) return releases; + + const versionCode = String(options.versionCode); + return releases.filter((release) => + release.activeArtifacts?.some( + (artifact) => String(artifact.versionCode) === versionCode, + ), + ); + }); + } + /** Convenience: open ephemeral edit, list tracks, discard edit */ async listCommitted(packageName: string): Promise { return this.client.edits.withEphemeralEdit(packageName, async (editId) => { diff --git a/skills/gps-release/SKILL.md b/skills/gps-release/SKILL.md index b2ccdf7..f0bad0d 100644 --- a/skills/gps-release/SKILL.md +++ b/skills/gps-release/SKILL.md @@ -22,7 +22,13 @@ gps --package $PKG tracks promote --from internal --to beta --confirm gps --package $PKG tracks promote --from beta --to production --user-fraction 0.1 --confirm ``` -4. Expand or halt: +4. Read the lifecycle state for the exact version: + +```bash +gps --package $PKG tracks releases production --version-code $VERSION_CODE --json +``` + +5. Expand or halt: ```bash gps --package $PKG tracks list --json @@ -35,4 +41,5 @@ gps --package $PKG tracks halt production --confirm - Never skip `--confirm` on writes. - Prefer internal/alpha before production. - Use `--validate-only` when dry-running supported commands. -- MCP: `gps_deploy_app`, `gps_promote_release`, `gps_update_rollout`, `gps_halt_release`. +- Publish approved managed releases in Play Console. +- MCP: `gps_deploy_app`, `gps_promote_release`, `gps_list_release_summaries`, `gps_update_rollout`, `gps_halt_release`.