<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Yasser's studio</title>
    <description>The latest articles on DEV Community by Yasser's studio (@yasserstudio).</description>
    <link>https://dev.to/yasserstudio</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F1162623%2F8c436a9f-4810-4198-96e9-678637c5b2b3.png</url>
      <title>DEV Community: Yasser's studio</title>
      <link>https://dev.to/yasserstudio</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/yasserstudio"/>
    <language>en</language>
    <item>
      <title>I built the first Android publishing CLI with Managed Google Play support</title>
      <dc:creator>Yasser's studio</dc:creator>
      <pubDate>Sat, 11 Apr 2026 16:30:08 +0000</pubDate>
      <link>https://dev.to/yasserstudio/i-built-the-first-android-publishing-cli-with-managed-google-play-support-1kd5</link>
      <guid>https://dev.to/yasserstudio/i-built-the-first-android-publishing-cli-with-managed-google-play-support-1kd5</guid>
      <description>&lt;p&gt;If you ship a private Android app to enterprise customers, you've probably noticed something:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;No publishing CLI supports it.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;Fastlane supply, the most popular Android publishing tool, wraps the standard Google Play Publisher API. Gradle Play Publisher does the same. They both handle public Play Store releases well (upload AAB, promote tracks, manage rollouts), but neither touches the &lt;a href="https://developers.google.com/android/work/play/custom-app-api" rel="noopener noreferrer"&gt;Play Custom App Publishing API&lt;/a&gt; (the API for publishing &lt;em&gt;private&lt;/em&gt; apps to Managed Google Play).&lt;/p&gt;

&lt;p&gt;If you need to publish a private app, you have two options today:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Click through Google Play Console for every release (takes 1-2 hours)&lt;/li&gt;
&lt;li&gt;Write your own Google API client against the Custom App API from scratch&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;I just shipped v0.9.56 of &lt;a href="https://github.com/yasserstudio/gpc" rel="noopener noreferrer"&gt;GPC&lt;/a&gt; (a Play Console CLI I've been building for the past few months) with native support for this API. GPC is the first Android publishing CLI to wrap the Play Custom App Publishing API end to end.&lt;/p&gt;

&lt;p&gt;Now option 3 exists:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gpc enterprise publish ./app.aab &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--account&lt;/span&gt; 1234567890 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--title&lt;/span&gt; &lt;span class="s2"&gt;"My Internal App"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--org-id&lt;/span&gt; customer-org
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Five minutes instead of two hours.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's a "private app" on Managed Google Play?
&lt;/h2&gt;

&lt;p&gt;Managed Google Play is Google's app store for enterprise-managed Android devices. Apps fall into two buckets:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Public apps&lt;/strong&gt; are regular Play Store apps that enterprises can approve and distribute to their employees. Nothing special on the publishing side.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Private apps&lt;/strong&gt; are custom apps distributed &lt;em&gt;exclusively&lt;/em&gt; to specific enterprise customers. Invisible to the public Play Store. Created via a separate Google API called the Play Custom App Publishing API.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This matters for you if you build:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;An internal app for your own company's managed Android fleet&lt;/li&gt;
&lt;li&gt;A vertical SaaS product with a per-customer Android client&lt;/li&gt;
&lt;li&gt;A line-of-business app that only one enterprise customer installs&lt;/li&gt;
&lt;li&gt;A private B2B tool distributed through Managed Google Play&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If your app is public Play Store fare, skip this article. GPC's regular commands work for you and always have.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why nobody wrapped this API
&lt;/h2&gt;

&lt;p&gt;The Play Custom App Publishing API is tiny. Google's &lt;a href="https://playcustomapp.googleapis.com/$discovery/rest?version=v1" rel="noopener noreferrer"&gt;discovery document&lt;/a&gt; exposes exactly one method:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;POST /playcustomapp/v1/accounts/{account}/customApps
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;That single method creates a new private app and uploads the bundle. There's no list method. No update method. No delete method. Apps created through this API are permanently private and can never be converted to public apps later.&lt;/p&gt;

&lt;p&gt;Because the API is small and the audience is narrow, nobody has productized a wrapper for it. The tools that handle 99% of Android publishing (Fastlane, gradle-play-publisher) target the 99% use case: public Play Store releases.&lt;/p&gt;

&lt;p&gt;But if you're in the 1% shipping to enterprise, every release is a painful manual slog in Play Console. The Custom App flow requires you to:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Open Play Console&lt;/li&gt;
&lt;li&gt;Click through the Create app wizard&lt;/li&gt;
&lt;li&gt;Manually enter title and language&lt;/li&gt;
&lt;li&gt;Upload the AAB&lt;/li&gt;
&lt;li&gt;Click through organization assignment&lt;/li&gt;
&lt;li&gt;Confirm a permanent-privacy warning&lt;/li&gt;
&lt;li&gt;Wait for Google's backend to process the upload&lt;/li&gt;
&lt;li&gt;Copy the assigned package name somewhere so you can reference it later&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;One release takes 1-2 hours. If you're shipping weekly, that's 4-8 hours a month of manual clicking.&lt;/p&gt;

&lt;h2&gt;
  
  
  The multipart resumable upload gotcha
&lt;/h2&gt;

&lt;p&gt;When I first tried to wrap this API, I hit an interesting challenge. The Play Custom App Publishing API uses &lt;strong&gt;multipart resumable upload&lt;/strong&gt;: the JSON metadata (title, language code, target organizations) and the bundle binary travel together in a single resumable session.&lt;/p&gt;

&lt;p&gt;The session-initiation POST carries the JSON metadata in its body. Subsequent PUT requests stream the binary in chunks to the session URI Google returns.&lt;/p&gt;

&lt;p&gt;This is different from the standard Publisher API, which uses simple &lt;code&gt;uploadType=media&lt;/code&gt; uploads where the body is just raw binary. GPC already had a resumable upload helper, but it was designed for the Publisher API pattern (empty initial POST body). I had to extend it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Before: initial session POST sent an empty body&lt;/span&gt;
&lt;span class="c1"&gt;// After: optional initialMetadata parameter drives a JSON body on init&lt;/span&gt;
&lt;span class="k"&gt;export&lt;/span&gt; &lt;span class="kr"&gt;interface&lt;/span&gt; &lt;span class="nx"&gt;ResumableUploadOptions&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="nl"&gt;chunkSize&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="kr"&gt;number&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="nl"&gt;onProgress&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;event&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;UploadProgressEvent&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="k"&gt;void&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="c1"&gt;// ... existing options&lt;/span&gt;

  &lt;span class="cm"&gt;/**
   * Optional JSON metadata to include in the initial session-initiation POST.
   * When present, the initial request uses Content-Type: application/json; charset=UTF-8
   * and the serialized body. When omitted, the initial request sends an empty body
   * (default Publisher API behavior).
   */&lt;/span&gt;
  &lt;span class="nl"&gt;initialMetadata&lt;/span&gt;&lt;span class="p"&gt;?:&lt;/span&gt; &lt;span class="nx"&gt;object&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This generalization turned out to be valuable beyond just the Custom App API: any Google API that follows the same "metadata in init POST, binary in chunks" pattern can now use the same helper.&lt;/p&gt;

&lt;h2&gt;
  
  
  What the command actually looks like
&lt;/h2&gt;

&lt;p&gt;The CLI surface is intentionally simple:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# One-shot publish (most common path)&lt;/span&gt;
gpc enterprise publish ./app.aab &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--account&lt;/span&gt; 1234567890 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--title&lt;/span&gt; &lt;span class="s2"&gt;"My Internal Tool"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--org-id&lt;/span&gt; customer-org &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--yes&lt;/span&gt;                      &lt;span class="c"&gt;# skip confirmation in CI&lt;/span&gt;

&lt;span class="c"&gt;# Explicit-arg form&lt;/span&gt;
gpc enterprise create &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--account&lt;/span&gt; 1234567890 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--bundle&lt;/span&gt; ./app.aab &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--title&lt;/span&gt; &lt;span class="s2"&gt;"My Internal Tool"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--org-id&lt;/span&gt; customer-org

&lt;span class="c"&gt;# Multiple target organizations&lt;/span&gt;
gpc enterprise publish ./app.aab &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--account&lt;/span&gt; 1234567890 &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--title&lt;/span&gt; &lt;span class="s2"&gt;"Multi-Customer Client"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--org-id&lt;/span&gt; org-acme  &lt;span class="nt"&gt;--org-name&lt;/span&gt; &lt;span class="s2"&gt;"Acme Corp"&lt;/span&gt; &lt;span class="se"&gt;\&lt;/span&gt;
  &lt;span class="nt"&gt;--org-id&lt;/span&gt; org-beta  &lt;span class="nt"&gt;--org-name&lt;/span&gt; &lt;span class="s2"&gt;"Beta Inc"&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The &lt;code&gt;--account&lt;/code&gt; argument is the long integer you read from the Play Console URL:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;https://play.google.com/console/developers/1234567890/...
                                             ^^^^^^^^^^
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;It's not a Google Workspace organization ID. Not a Cloud Identity ID. Just the developer account ID. (This confused me for a while building the feature, and it confuses users hitting the docs for the first time too.)&lt;/p&gt;

&lt;h2&gt;
  
  
  One-way door: permanently private
&lt;/h2&gt;

&lt;p&gt;Apps created through this API cannot be made public later. GPC prints a confirmation prompt before every create/publish call to make sure you meant it:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;⚠  This will publish a PRIVATE app to Managed Google Play.

   Private apps created via this API are permanently private and cannot be converted
   to public apps later.

   Developer account: 1234567890
   Title:             My Internal Tool
   Language:          en_US
   Bundle:            ./app.aab
   Organizations:     customer-org

Continue with private app creation? [y/N]:
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;Pass &lt;code&gt;--yes&lt;/code&gt; to skip the prompt in CI or non-interactive environments. The CLI will refuse to proceed on a non-TTY without &lt;code&gt;--yes&lt;/code&gt;, which prevents accidental creates from unattended scripts.&lt;/p&gt;

&lt;h2&gt;
  
  
  What happens after the initial publish
&lt;/h2&gt;

&lt;p&gt;Here's the part that surprised me while building this feature: &lt;strong&gt;after a private app is created, it becomes a regular app in your developer account.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;It has a &lt;code&gt;packageName&lt;/code&gt; that Google assigns (something like &lt;code&gt;com.google.customapp.A1B2C3D4E5&lt;/code&gt;). You can't influence the package name. But once the app exists, you can use every other GPC command against it:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Save the package name from the create step&lt;/span&gt;
&lt;span class="nv"&gt;APP_ID&lt;/span&gt;&lt;span class="o"&gt;=&lt;/span&gt;com.google.customapp.A1B2C3D4E5

&lt;span class="c"&gt;# Upload a new version like any other app&lt;/span&gt;
gpc &lt;span class="nt"&gt;--app&lt;/span&gt; &lt;span class="nv"&gt;$APP_ID&lt;/span&gt; releases upload ./app-v2.aab &lt;span class="nt"&gt;--track&lt;/span&gt; production

&lt;span class="c"&gt;# Promote between tracks&lt;/span&gt;
gpc &lt;span class="nt"&gt;--app&lt;/span&gt; &lt;span class="nv"&gt;$APP_ID&lt;/span&gt; releases promote &lt;span class="nt"&gt;--from&lt;/span&gt; beta &lt;span class="nt"&gt;--to&lt;/span&gt; production &lt;span class="nt"&gt;--rollout&lt;/span&gt; 10

&lt;span class="c"&gt;# Sync listings&lt;/span&gt;
gpc &lt;span class="nt"&gt;--app&lt;/span&gt; &lt;span class="nv"&gt;$APP_ID&lt;/span&gt; listings push &lt;span class="nt"&gt;--dir&lt;/span&gt; metadata/

&lt;span class="c"&gt;# Query crash rates&lt;/span&gt;
gpc &lt;span class="nt"&gt;--app&lt;/span&gt; &lt;span class="nv"&gt;$APP_ID&lt;/span&gt; vitals crashes &lt;span class="nt"&gt;--threshold&lt;/span&gt; 2.0
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;None of this requires the Custom App API. It all goes through the regular Play Publisher API, same as any other app in your account. The private app is private in &lt;em&gt;distribution&lt;/em&gt; (only visible to the organizations you assigned at create time), but administratively it's just another draft app.&lt;/p&gt;

&lt;p&gt;The only thing you can't do programmatically is add or remove target enterprise organizations after creation. Google's API doesn't expose that. You have to open Play Console UI and do it there.&lt;/p&gt;

&lt;h2&gt;
  
  
  CI/CD recipe
&lt;/h2&gt;

&lt;p&gt;For teams that want to automate private app publishing from a pipeline, here's a working GitHub Actions example:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight yaml"&gt;&lt;code&gt;&lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Publish private app&lt;/span&gt;

&lt;span class="na"&gt;on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;workflow_dispatch&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;inputs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="na"&gt;flow&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
        &lt;span class="na"&gt;description&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s2"&gt;"&lt;/span&gt;&lt;span class="s"&gt;Initial&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;publish&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;or&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;version&lt;/span&gt;&lt;span class="nv"&gt; &lt;/span&gt;&lt;span class="s"&gt;update?"&lt;/span&gt;
        &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;choice&lt;/span&gt;
        &lt;span class="na"&gt;options&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;[&lt;/span&gt;&lt;span class="nv"&gt;update&lt;/span&gt;&lt;span class="pi"&gt;,&lt;/span&gt; &lt;span class="nv"&gt;initial&lt;/span&gt;&lt;span class="pi"&gt;]&lt;/span&gt;

&lt;span class="na"&gt;jobs&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
  &lt;span class="na"&gt;publish&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
    &lt;span class="na"&gt;runs-on&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;ubuntu-latest&lt;/span&gt;
    &lt;span class="na"&gt;steps&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/checkout@v4&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;uses&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;actions/setup-node@v4&lt;/span&gt;
        &lt;span class="na"&gt;with&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;node-version&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="m"&gt;24&lt;/span&gt;
      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;npm install -g @gpc-cli/cli&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Write service account key&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;echo '${{ secrets.GPC_SA_KEY }}' &amp;gt; /tmp/sa.json&lt;/span&gt;
        &lt;span class="na"&gt;env&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt;
          &lt;span class="na"&gt;GPC_SERVICE_ACCOUNT_KEY&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;/tmp/sa.json&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Initial publish&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inputs.flow == 'initial'&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;gpc enterprise --account ${{ secrets.DEVELOPER_ACCOUNT_ID }} \&lt;/span&gt;
            &lt;span class="s"&gt;publish ./app.aab \&lt;/span&gt;
            &lt;span class="s"&gt;--title "Internal Tools" \&lt;/span&gt;
            &lt;span class="s"&gt;--org-id ${{ secrets.CUSTOMER_ORG_ID }} \&lt;/span&gt;
            &lt;span class="s"&gt;--yes&lt;/span&gt;

      &lt;span class="pi"&gt;-&lt;/span&gt; &lt;span class="na"&gt;name&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;Update existing&lt;/span&gt;
        &lt;span class="na"&gt;if&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="s"&gt;inputs.flow == 'update'&lt;/span&gt;
        &lt;span class="na"&gt;run&lt;/span&gt;&lt;span class="pi"&gt;:&lt;/span&gt; &lt;span class="pi"&gt;|&lt;/span&gt;
          &lt;span class="s"&gt;gpc --app com.google.customapp.A1B2C3D4E5 \&lt;/span&gt;
              &lt;span class="s"&gt;releases upload ./app.aab --track production&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The workflow takes a choice input that routes to either the one-time &lt;code&gt;gpc enterprise publish&lt;/code&gt; path or the ongoing &lt;code&gt;gpc releases upload&lt;/code&gt; path.&lt;/p&gt;

&lt;h2&gt;
  
  
  Required setup (one-time)
&lt;/h2&gt;

&lt;p&gt;Before you can run &lt;code&gt;gpc enterprise publish&lt;/code&gt; for the first time, you need four things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;A Google Play developer account.&lt;/strong&gt; The one whose ID you'll pass as &lt;code&gt;--account&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The Play Custom App Publishing API enabled&lt;/strong&gt; in a Google Cloud project. Enable it &lt;a href="https://console.cloud.google.com/apis/library/playcustomapp.googleapis.com" rel="noopener noreferrer"&gt;here&lt;/a&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A service account with the "create and publish private apps" permission&lt;/strong&gt; in Play Console. This is a Play Console permission (Account permissions, not per-app), separate from Google Cloud IAM.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Organization IDs&lt;/strong&gt; for the enterprise customers you want to target. Your customers provide these.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;GPC's &lt;code&gt;gpc doctor&lt;/code&gt; command includes a probe for this API. Run it to verify setup before your first publish attempt:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gpc doctor
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Look for:&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;✓ Play Custom App Publishing API is reachable
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

&lt;p&gt;If the probe fails, the error message points at the specific setup step that's incomplete.&lt;/p&gt;

&lt;h2&gt;
  
  
  Install
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# npm (Node.js 20+)&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @gpc-cli/cli

&lt;span class="c"&gt;# Homebrew (macOS/Linux)&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;yasserstudio/tap/gpc

&lt;span class="c"&gt;# Standalone binary (no Node.js required)&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/yasserstudio/gpc/main/scripts/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h2&gt;
  
  
  Free to use
&lt;/h2&gt;

&lt;p&gt;GPC's source is on &lt;a href="https://github.com/yasserstudio/gpc" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt;. It's free to use for commercial and personal projects. If you hit a bug or have a feature request, open an issue.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Full release notes:&lt;/strong&gt; &lt;a href="https://github.com/yasserstudio/gpc/releases/tag/v0.9.56" rel="noopener noreferrer"&gt;https://github.com/yasserstudio/gpc/releases/tag/v0.9.56&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;Enterprise publishing guide:&lt;/strong&gt; &lt;a href="https://yasserstudio.github.io/gpc/guide/enterprise-publishing" rel="noopener noreferrer"&gt;https://yasserstudio.github.io/gpc/guide/enterprise-publishing&lt;/a&gt;&lt;br&gt;
&lt;strong&gt;All commands:&lt;/strong&gt; &lt;a href="https://yasserstudio.github.io/gpc/commands/" rel="noopener noreferrer"&gt;https://yasserstudio.github.io/gpc/commands/&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Next on the roadmap: a v0.9.57 batch of API client fixes (uncovered during an audit that led to the v0.9.56 enterprise rewrite), then v1.0.0 stable.&lt;/p&gt;

&lt;p&gt;If you're shipping to Managed Google Play and GPC saves you time, I'd love to hear about it.&lt;/p&gt;

</description>
      <category>android</category>
      <category>mobile</category>
      <category>cli</category>
      <category>devtools</category>
    </item>
    <item>
      <title>I shipped a release to 10% of users. Crash rate started climbing. I found out 20 minutes later when I happened to open the Play Console.</title>
      <dc:creator>Yasser's studio</dc:creator>
      <pubDate>Mon, 06 Apr 2026 12:21:12 +0000</pubDate>
      <link>https://dev.to/yasserstudio/i-shipped-a-release-to-10-of-users-crash-rate-started-climbing-i-found-out-20-minutes-later-when-4mi6</link>
      <guid>https://dev.to/yasserstudio/i-shipped-a-release-to-10-of-users-crash-rate-started-climbing-i-found-out-20-minutes-later-when-4mi6</guid>
      <description>&lt;p&gt;I shipped a release to 10% of users. Crash rate started climbing. I found out 20 minutes later when I happened to open the Play Console.&lt;/p&gt;

&lt;p&gt;That's too late. At 10%, you're already affecting real users. By the time you halt the rollout, investigate, and push a fix, you've lost reviews and trust you won't get back.&lt;/p&gt;

&lt;p&gt;The frustrating part: the data was there the whole time. Google's API had my crash rate. I just wasn't checking it before the rollout progressed.&lt;/p&gt;

&lt;p&gt;The gap in most Android CI pipelines&lt;/p&gt;

&lt;p&gt;A typical Android release pipeline does three things:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Build the AAB&lt;/li&gt;
&lt;li&gt;Run unit tests&lt;/li&gt;
&lt;li&gt;Upload to the Play Store&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;That's it. The pipeline considers itself done after the upload.&lt;/p&gt;

&lt;p&gt;But "shipped" and "healthy" are not the same thing. Nothing in that pipeline asks: is the app actually in good shape right now? Is it safe to keep rolling out?&lt;/p&gt;

&lt;p&gt;The answer to both questions is sitting in the Play Developer Reporting API. It just needs to be checked.&lt;/p&gt;

&lt;p&gt;gpc vitals crashes --threshold&lt;/p&gt;

&lt;p&gt;gpc vitals crashes --threshold 2.0&lt;/p&gt;

&lt;p&gt;That's the gate. If the crash rate exceeds 2.0%, GPC exits with code 6. Your CI sees a non-zero exit code. The pipeline stops.&lt;/p&gt;

&lt;p&gt;Same for ANR:&lt;/p&gt;

&lt;p&gt;gpc vitals anr --threshold 0.47&lt;/p&gt;

&lt;p&gt;0.47% is Google's own "bad behavior" threshold for ANR. Exceed it and Play Console flags your app. Gate on it in CI and you find out before Google does.&lt;/p&gt;

&lt;p&gt;What exit code 6 means&lt;/p&gt;

&lt;p&gt;GPC uses semantic exit codes. Exit code 6 specifically means "quality threshold breached" -- not a crash, not an auth failure, not a network error. Your CI can react differently to each:&lt;/p&gt;

&lt;p&gt;gpc vitals crashes --threshold 2.0 --json&lt;br&gt;
  EXIT_CODE=$?&lt;/p&gt;

&lt;p&gt;case $EXIT_CODE in&lt;br&gt;
    0) echo "Vitals healthy" ;;&lt;br&gt;
    6) echo "Threshold breached -- halting rollout" ;;&lt;br&gt;
    3) echo "Auth error -- check your service account" ;;&lt;br&gt;
    4) echo "API error -- check permissions" ;;&lt;br&gt;
  esac&lt;/p&gt;

&lt;p&gt;Google's actual thresholds&lt;/p&gt;

&lt;p&gt;For reference, here are the levels Google Play uses internally:&lt;/p&gt;

&lt;p&gt;┌────────────┬──────────────────────────┬─────────────────────────────────────────┐&lt;br&gt;
  │   Metric   │ Google warning threshold │              What happens               │&lt;br&gt;
  ├────────────┼──────────────────────────┼─────────────────────────────────────────┤&lt;br&gt;
  │ Crash rate │ 1.09%                    │ Play Console warning, visibility impact │&lt;br&gt;
  ├────────────┼──────────────────────────┼─────────────────────────────────────────┤&lt;br&gt;
  │ ANR rate   │ 0.47%                    │ Play Console warning, visibility impact │&lt;br&gt;
  └────────────┴──────────────────────────┴─────────────────────────────────────────┘&lt;/p&gt;

&lt;p&gt;A practical approach: set your CI gate at 1.5x the Google threshold. That gives you a window to catch regressions before Google flags your app.&lt;/p&gt;

&lt;p&gt;# Conservative: catch it before Google does&lt;br&gt;
  gpc vitals crashes --threshold 1.5&lt;/p&gt;

&lt;p&gt;# Standard: roughly 2x Google's threshold&lt;br&gt;
  gpc vitals crashes --threshold 2.0&lt;/p&gt;

&lt;p&gt;# Emergency: last line of defense&lt;br&gt;
  gpc vitals crashes --threshold 3.0&lt;/p&gt;

&lt;p&gt;In CI: one step before the rollout increase&lt;/p&gt;

&lt;p&gt;The pattern that works best is running a vitals gate before each rollout percentage increase, not just before the initial upload.&lt;/p&gt;

&lt;p&gt;# GitHub Actions&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;name: Check crash rate before rollout increase&lt;br&gt;
run: gpc vitals crashes --threshold 2.0 --json&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;name: Increase rollout to 25%&lt;br&gt;
run: gpc releases rollout increase --track production --to 25&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If the vitals check exits 6, the increase step never runs. No manual intervention needed.&lt;/p&gt;

&lt;p&gt;For a full gate covering both crash and ANR:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;name: Vitals gate&lt;br&gt;
run: |&lt;br&gt;
  gpc vitals crashes --threshold 2.0 --json&lt;br&gt;
  gpc vitals anr --threshold 0.47 --json&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;name: Increase rollout to 50%&lt;br&gt;
run: gpc releases rollout increase --track production --to 50&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Handling a breach&lt;/p&gt;

&lt;p&gt;When a threshold is breached, you probably want to do more than just fail the job. You want to halt the active rollout too:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;name: Vitals gate&lt;br&gt;
id: vitals&lt;br&gt;
run: gpc vitals crashes --threshold 2.0 --json&lt;br&gt;
continue-on-error: true&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;name: Halt rollout on breach&lt;br&gt;
if: steps.vitals.outcome == 'failure'&lt;br&gt;
run: |&lt;br&gt;
  echo ":⚠️:Crash threshold breached -- halting rollout"&lt;br&gt;
  gpc releases rollout halt --track production --json&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Now the pipeline detects the breach, stops the rollout automatically, and surfaces a warning in your CI logs.&lt;/p&gt;

&lt;p&gt;The full staged rollout pattern&lt;/p&gt;

&lt;p&gt;This is how I structure releases now. Vitals gate at every step:&lt;/p&gt;

&lt;p&gt;Upload AAB to internal track&lt;br&gt;
      |&lt;br&gt;
      v&lt;br&gt;
  Wait 24-48h for data&lt;br&gt;
      |&lt;br&gt;
      v&lt;br&gt;
  Vitals gate -----&amp;gt; BREACH? --&amp;gt; Halt, alert, fix&lt;br&gt;
      |&lt;br&gt;
      | PASS&lt;br&gt;
      v&lt;br&gt;
  Increase to 10%&lt;br&gt;
      |&lt;br&gt;
      v&lt;br&gt;
  Wait 24-48h&lt;br&gt;
      |&lt;br&gt;
      v&lt;br&gt;
  Vitals gate -----&amp;gt; BREACH? --&amp;gt; Halt, alert, fix&lt;br&gt;
      |&lt;br&gt;
      | PASS&lt;br&gt;
      v&lt;br&gt;
  Increase to 50%&lt;br&gt;
      |&lt;br&gt;
      v&lt;br&gt;
  Wait 24-48h&lt;br&gt;
      |&lt;br&gt;
      v&lt;br&gt;
  Vitals gate -----&amp;gt; BREACH? --&amp;gt; Halt, alert, fix&lt;br&gt;
      |&lt;br&gt;
      | PASS&lt;br&gt;
      v&lt;br&gt;
  Complete rollout (100%)&lt;/p&gt;

&lt;p&gt;Bad releases get caught early, at low rollout percentages, before they affect most of your users.&lt;/p&gt;

&lt;p&gt;The overview command for a quick health check&lt;/p&gt;

&lt;p&gt;If you want a full picture before you start a rollout:&lt;/p&gt;

&lt;p&gt;gpc vitals overview&lt;/p&gt;

&lt;p&gt;{&lt;br&gt;
    "crashRate":           { "value": 1.2,  "threshold": "bad" },&lt;br&gt;
    "anrRate":             { "value": 0.3,  "threshold": "good" },&lt;br&gt;
    "slowStartRate":       { "value": 5.1,  "threshold": "acceptable" },&lt;br&gt;
    "slowRenderingRate":   { "value": 2.8,  "threshold": "good" },&lt;br&gt;
    "excessiveWakeupRate": { "value": 0.1,  "threshold": "good" },&lt;br&gt;
    "stuckWakelockRate":   { "value": 0.05, "threshold": "good" }&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;Six metrics in one call. Add it as a pre-release sanity check before any upload.&lt;/p&gt;

&lt;p&gt;Quick context on GPC&lt;/p&gt;

&lt;p&gt;If you're new to this series: GPC is a CLI that covers the entire Google Play Developer API. 215 endpoints. 7 TypeScript packages. 1,874 tests.&lt;/p&gt;

&lt;p&gt;Previous articles:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: I built a CLI that covers the entire Google Play Developer API&lt;/li&gt;
&lt;li&gt;Part 2: I replaced 4 Play Console tabs with one terminal command&lt;/li&gt;
&lt;li&gt;Part 3: I stopped submitting to Google Play without running this first&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try it&lt;/p&gt;

&lt;p&gt;# npm&lt;br&gt;
  npm install -g @gpc-cli/cli&lt;/p&gt;

&lt;p&gt;# Homebrew&lt;br&gt;
  brew install yasserstudio/tap/gpc&lt;/p&gt;

&lt;p&gt;# Standalone binary (no Node.js required)&lt;br&gt;
  curl -fsSL &lt;a href="https://raw.githubusercontent.com/yasserstudio/gpc/main/scripts/install.sh" rel="noopener noreferrer"&gt;https://raw.githubusercontent.com/yasserstudio/gpc/main/scripts/install.sh&lt;/a&gt; | sh&lt;/p&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;p&gt;gpc auth login&lt;br&gt;
  gpc vitals crashes --threshold 2.0&lt;/p&gt;

&lt;p&gt;Docs | GitHub | Vitals gates docs&lt;/p&gt;

&lt;p&gt;Free to use. Code is on GitHub.&lt;/p&gt;

&lt;p&gt;Does your release pipeline gate on app health before increasing rollout? Curious what thresholds people use in practice.&lt;/p&gt;

</description>
      <category>android</category>
      <category>mobile</category>
      <category>discuss</category>
      <category>testing</category>
    </item>
    <item>
      <title>I stopped submitting to Google Play without running this first</title>
      <dc:creator>Yasser's studio</dc:creator>
      <pubDate>Thu, 02 Apr 2026 09:53:18 +0000</pubDate>
      <link>https://dev.to/yasserstudio/i-stopped-submitting-to-google-play-without-running-this-first-3lk2</link>
      <guid>https://dev.to/yasserstudio/i-stopped-submitting-to-google-play-without-running-this-first-3lk2</guid>
      <description>&lt;p&gt;Last year I uploaded an AAB to the Play Console, waited four days for review, and got rejected.&lt;/p&gt;

&lt;p&gt;The reason: targetSdkVersion was one level below the new minimum. Google had bumped the requirement that month. I hadn't noticed.&lt;/p&gt;

&lt;p&gt;I fixed it, re-uploaded, waited another three days. That's a week lost for a one-line fix in my build config.&lt;/p&gt;

&lt;p&gt;The frustrating part: I could have caught that before uploading. The information was right there in the manifest. I just didn't check.&lt;/p&gt;

&lt;p&gt;The pattern&lt;/p&gt;

&lt;p&gt;Most Play Store rejections follow the same pattern:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Upload your AAB&lt;/li&gt;
&lt;li&gt;Wait 1-7 days for review&lt;/li&gt;
&lt;li&gt;Get a short rejection email&lt;/li&gt;
&lt;li&gt;Fix something you could have checked locally&lt;/li&gt;
&lt;li&gt;Re-upload, wait again&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;And the rejection reasons are almost always predictable. targetSdk below the minimum. A restricted permission you didn't declare properly. A third-party billing SDK that violates Play's billing&lt;br&gt;
  policy. A hardcoded API key in the bundle.&lt;/p&gt;

&lt;p&gt;None of these require Google's review to catch. They're all checkable from the file itself.&lt;/p&gt;

&lt;p&gt;gpc preflight&lt;/p&gt;

&lt;p&gt;$ gpc preflight app.aab&lt;/p&gt;

&lt;p&gt;9 scanners run against your AAB file. Entirely offline. No API calls, no credentials, no network.&lt;/p&gt;

&lt;p&gt;Here's what each scanner checks:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Manifest validation&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;The big one. Parses your AndroidManifest.xml from the bundle and checks:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;targetSdkVersion below Google's current minimum (critical)&lt;/li&gt;
&lt;li&gt;debuggable flag left on in a release build&lt;/li&gt;
&lt;li&gt;testOnly flag set&lt;/li&gt;
&lt;li&gt;Missing android:exported on components (required since API 31)&lt;/li&gt;
&lt;li&gt;Missing foregroundServiceType (required since API 34)&lt;/li&gt;
&lt;li&gt;Cleartext HTTP traffic enabled&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;✗ CRITICAL  targetSdkVersion 33 is below minimum 35&lt;br&gt;
          Google Play requires targetSdkVersion &amp;gt;= 35 for new app updates.&lt;br&gt;
          → Update targetSdkVersion in your build.gradle to 35 or higher.&lt;/p&gt;

&lt;p&gt;This single check would have saved me that week.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Permissions audit&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Flags 16+ restricted Google Play permissions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;SMS and call log access (heavily restricted since 2019)&lt;/li&gt;
&lt;li&gt;QUERY_ALL_PACKAGES (requires declaration)&lt;/li&gt;
&lt;li&gt;Background location&lt;/li&gt;
&lt;li&gt;Accessibility service&lt;/li&gt;
&lt;li&gt;VPN service&lt;/li&gt;
&lt;li&gt;INSTALL_PACKAGES&lt;/li&gt;
&lt;li&gt;REQUEST_DELETE_PACKAGES&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Each finding includes a note about what data safety declarations you'll need.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Native library architecture&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Checks your native libraries for 64-bit compliance:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;arm64-v8a is required&lt;/li&gt;
&lt;li&gt;x86_64 recommended&lt;/li&gt;
&lt;li&gt;Warns if native libs total exceeds 150MB uncompressed&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Metadata validation&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;If you pass --metadata &lt;/p&gt;, it checks your Fastlane-format store listings:

&lt;ul&gt;
&lt;li&gt;Title under 30 characters&lt;/li&gt;
&lt;li&gt;Short description under 80 characters&lt;/li&gt;
&lt;li&gt;Full description under 4,000 characters&lt;/li&gt;
&lt;li&gt;At least 2 phone screenshots&lt;/li&gt;
&lt;li&gt;Privacy policy URL present&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Secrets detection&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Scans your source directory (with --source &lt;/p&gt;) for hardcoded credentials:

&lt;ul&gt;
&lt;li&gt;AWS access keys (AKIA...)&lt;/li&gt;
&lt;li&gt;Google API keys (AIza...)&lt;/li&gt;
&lt;li&gt;Stripe live keys (sk_live_...)&lt;/li&gt;
&lt;li&gt;RSA/EC/DSA private keys&lt;/li&gt;
&lt;li&gt;Firebase config keys&lt;/li&gt;
&lt;li&gt;Generic bearer tokens&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;You'd be surprised how often these end up in a bundle.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Billing compliance&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Detects non-Play billing SDKs that violate Google's billing policy:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Stripe SDK&lt;/li&gt;
&lt;li&gt;Braintree&lt;/li&gt;
&lt;li&gt;PayPal&lt;/li&gt;
&lt;li&gt;Razorpay&lt;/li&gt;
&lt;li&gt;Adyen&lt;/li&gt;
&lt;li&gt;Square&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;These are flagged as warnings. Some apps have exemptions, but most don't.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;Privacy and tracking&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Identifies tracking SDKs and cross-references with your permissions:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Facebook SDK, Adjust, AppsFlyer, Amplitude, Mixpanel, Branch, CleverTap&lt;/li&gt;
&lt;li&gt;Advertising ID usage&lt;/li&gt;
&lt;li&gt;Data collection indicators vs declared permissions&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Policy heuristics&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Pattern-matches for common policy traps:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;COPPA/Families indicators (child-targeted features + data collection)&lt;/li&gt;
&lt;li&gt;Financial app indicators (SMS access + autofill)&lt;/li&gt;
&lt;li&gt;Health app indicators (body sensor permissions)&lt;/li&gt;
&lt;li&gt;User-generated content indicators&lt;/li&gt;
&lt;li&gt;System alert window (overlay) permission&lt;/li&gt;
&lt;/ul&gt;

&lt;ol&gt;
&lt;li&gt;Size analysis&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Breaks down your bundle size by category:&lt;/p&gt;

&lt;p&gt;ℹ INFO  Total size: 64.8 MB compressed, 212.6 MB uncompressed&lt;br&gt;
          909 files. Breakdown: native-libs: 31.1 MB, dex: 7.2 MB,&lt;br&gt;
          assets: 1.1 MB, resources: 0.8 MB, signing: 0.1 MB&lt;/p&gt;

&lt;p&gt;Warns if total download size exceeds the configurable limit (default: 150 MB). Flags individual native libs over 50 MB and assets over 30 MB.&lt;/p&gt;

&lt;p&gt;Severity levels&lt;/p&gt;

&lt;p&gt;Every finding has a severity:&lt;/p&gt;

&lt;p&gt;┌──────────┬───────────────┬───────────────────────────────────────┐&lt;br&gt;
  │  Level   │     Icon      │                Meaning                │&lt;br&gt;
  ├──────────┼───────────────┼───────────────────────────────────────┤&lt;br&gt;
  │ critical │ ✗ (red, bold) │ Will almost certainly cause rejection │&lt;br&gt;
  ├──────────┼───────────────┼───────────────────────────────────────┤&lt;br&gt;
  │ error    │ ✗ (red)       │ Likely to cause rejection             │&lt;br&gt;
  ├──────────┼───────────────┼───────────────────────────────────────┤&lt;br&gt;
  │ warning  │ ⚠ (yellow)    │ Worth reviewing, may cause issues     │&lt;br&gt;
  ├──────────┼───────────────┼───────────────────────────────────────┤&lt;br&gt;
  │ info     │ ℹ (gray)      │ Informational, no action needed       │&lt;br&gt;
  └──────────┴───────────────┴───────────────────────────────────────┘&lt;/p&gt;

&lt;p&gt;By default, the command exits code 6 if any finding is error or higher. Override with --fail-on:&lt;/p&gt;

&lt;p&gt;gpc preflight app.aab --fail-on critical    # only fail on critical&lt;br&gt;
  gpc preflight app.aab --fail-on warning     # strict mode&lt;/p&gt;

&lt;p&gt;In CI&lt;/p&gt;

&lt;p&gt;Add one step before your upload:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;&lt;p&gt;name: Pre-submission check&lt;br&gt;
run: gpc preflight app.aab&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;name: Upload to Play Store&lt;br&gt;
run: gpc releases upload app.aab --track internal&lt;/p&gt;&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If preflight fails (exit code 6), the upload never happens. You find out in your CI logs instead of in a rejection email three days later.&lt;/p&gt;

&lt;p&gt;Configuration&lt;/p&gt;

&lt;p&gt;For teams, drop a .preflightrc.json in your project root:&lt;/p&gt;

&lt;p&gt;{&lt;br&gt;
    "failOn": "error",&lt;br&gt;
    "targetSdkMinimum": 35,&lt;br&gt;
    "maxDownloadSizeMb": 150,&lt;br&gt;
    "allowedPermissions": ["android.permission.CAMERA"],&lt;br&gt;
    "disabledRules": ["billing-third-party"],&lt;br&gt;
    "severityOverrides": {&lt;br&gt;
      "size-total-warning": "info"&lt;br&gt;
    }&lt;br&gt;
  }&lt;/p&gt;

&lt;p&gt;Whitelist permissions your app legitimately needs. Disable rules that don't apply. Override severities for your specific situation.&lt;/p&gt;

&lt;p&gt;What it doesn't catch&lt;/p&gt;

&lt;p&gt;To be clear about the limits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;It can't check your app's runtime behavior&lt;/li&gt;
&lt;li&gt;It can't verify your data safety form matches reality&lt;/li&gt;
&lt;li&gt;It can't predict policy changes Google hasn't announced&lt;/li&gt;
&lt;li&gt;It can't check things that require running the app (ANR patterns, performance)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;It checks what's checkable from the file and your source. The static stuff. The things that are embarrassing to get rejected for because they were right there the whole time.&lt;/p&gt;

&lt;p&gt;Run specific scanners&lt;/p&gt;

&lt;p&gt;Don't need all 9? Run individual scanners:&lt;/p&gt;

&lt;p&gt;gpc preflight manifest app.aab          # manifest only&lt;br&gt;
  gpc preflight permissions app.aab       # permissions only&lt;br&gt;
  gpc preflight metadata ./fastlane       # metadata only&lt;br&gt;
  gpc preflight codescan ./src            # secrets + billing + privacy&lt;/p&gt;

&lt;p&gt;Quick context on GPC&lt;/p&gt;

&lt;p&gt;If you're new here: GPC is a CLI that covers the entire Google Play Developer API. 208 endpoints. 7 TypeScript packages. 1,863 tests.&lt;/p&gt;

&lt;p&gt;Previous articles in this series:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;Part 1: I built a CLI that covers the entire Google Play Developer API&lt;/li&gt;
&lt;li&gt;Part 2: I replaced 4 Play Console tabs with one terminal command&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Try it&lt;/p&gt;

&lt;p&gt;# npm&lt;br&gt;
  npm install -g @gpc-cli/cli&lt;/p&gt;

&lt;p&gt;# Homebrew&lt;br&gt;
  brew install yasserstudio/tap/gpc&lt;/p&gt;

&lt;p&gt;# Standalone binary (no Node.js required)&lt;br&gt;
  curl -fsSL &lt;a href="https://raw.githubusercontent.com/yasserstudio/gpc/main/scripts/install.sh" rel="noopener noreferrer"&gt;https://raw.githubusercontent.com/yasserstudio/gpc/main/scripts/install.sh&lt;/a&gt; | sh&lt;/p&gt;

&lt;p&gt;Then:&lt;/p&gt;

&lt;p&gt;gpc preflight your-app.aab&lt;br&gt;
  gpc preflight your-app.aab --source ./src --metadata ./fastlane&lt;/p&gt;

&lt;p&gt;Docs | GitHub | Preflight docs&lt;/p&gt;

&lt;p&gt;Free to use. Code is on GitHub.&lt;/p&gt;

&lt;p&gt;What's your pre-submission process look like? Manual checklist, automated, or just upload and hope for the best?&lt;/p&gt;

</description>
      <category>android</category>
      <category>ai</category>
      <category>mobile</category>
      <category>devtools</category>
    </item>
    <item>
      <title>I replaced 4 Play Console tabs with one terminal command</title>
      <dc:creator>Yasser's studio</dc:creator>
      <pubDate>Wed, 01 Apr 2026 09:09:21 +0000</pubDate>
      <link>https://dev.to/yasserstudio/i-replaced-4-play-console-tabs-with-one-terminal-command-3mj7</link>
      <guid>https://dev.to/yasserstudio/i-replaced-4-play-console-tabs-with-one-terminal-command-3mj7</guid>
      <description>&lt;p&gt;I used to start every morning the same way.&lt;/p&gt;

&lt;p&gt;Open the Play Console. Check the releases tab. Switch to the vitals tab. Look at crash rates. Look at ANR rates. Open the reviews tab. Scan for 1-star reviews. Cross-reference everything in my head.&lt;/p&gt;

&lt;p&gt;Four tabs. Ten minutes. Every single day.&lt;/p&gt;

&lt;p&gt;And I still wasn't sure if I had the full picture.&lt;/p&gt;

&lt;h2&gt;
  
  
  One command instead of four tabs
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nv"&gt;$ &lt;/span&gt;gpc status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;App: com.example.app  (fetched just now)

RELEASES
  production   v142   completed    -
  beta         v143   inProgress  10%
  internal     v144   draft        -

VITALS  (last 7 days)
  crashes     0.80% ↓  ✓    anr         0.20% ↓  ✓
  slow starts 2.10%    ✓    slow render 4.30%    ⚠

REVIEWS  (last 30 days)
  ★ 4.6   142 new   89% positive   ↑ from 4.4
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Ten API calls fire in parallel. Full picture in under 3 seconds.&lt;/p&gt;

&lt;p&gt;Releases by track with rollout percentages. Four vitals metrics with trend arrows showing whether things are getting better or worse. Review sentiment with rating trends.&lt;/p&gt;

&lt;p&gt;That's &lt;code&gt;gpc status&lt;/code&gt;. It replaced the tab-switching ritual I used to do after every deploy.&lt;/p&gt;

&lt;h2&gt;
  
  
  Most release tools stop at uploads. This one doesn't.
&lt;/h2&gt;

&lt;p&gt;Fastlane supply pushes your AAB and syncs metadata. That's valuable. But it's only half the story.&lt;/p&gt;

&lt;p&gt;After you ship, you need visibility. Is the crash rate spiking on the new version? Did ANR rates jump? Are users leaving angry reviews about the change you just pushed?&lt;/p&gt;

&lt;p&gt;Nothing else answers those questions from the terminal. &lt;code&gt;gpc status&lt;/code&gt; closes the loop between "I shipped a release" and "here's what happened after."&lt;/p&gt;

&lt;p&gt;Ship and monitor from the same tool.&lt;/p&gt;

&lt;h2&gt;
  
  
  Three workflows where this changes things
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. The morning check
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gpc status &lt;span class="nt"&gt;--all-apps&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Runs the full status check for every app in your profiles. Up to 5 apps, one command. If any app has a threshold breach, the command exits code 6.&lt;/p&gt;

&lt;p&gt;This replaced my entire morning Play Console routine. I open the terminal, run one command, and know exactly where everything stands.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. Watching a rollout in real time
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gpc status &lt;span class="nt"&gt;--watch&lt;/span&gt; 60 &lt;span class="nt"&gt;--notify&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Polls every 60 seconds. Clears the screen before each update. Sends a desktop notification when a threshold is breached or clears.&lt;/p&gt;

&lt;p&gt;Leave it running on a secondary monitor during a staged rollout. Your crash rate ticks up past your threshold? You know immediately. Not 30 minutes later when you remember to check the Console.&lt;/p&gt;

&lt;p&gt;After the rollout, check what changed:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gpc status &lt;span class="nt"&gt;--since-last&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;Changes since 1h ago:
  Version:    141 → 142
  Crash rate: 1.80% → 0.80% (-1.00%) ✓
  ANR rate:   0.30% → 0.20% (-0.10%) ✓
  Reviews:    4.4★ → 4.6★ (+0.2) ✓
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  3. Automated health gates in CI
&lt;/h3&gt;

&lt;p&gt;This is where &lt;code&gt;gpc status&lt;/code&gt; goes from "nice to have" to "catches problems before users do."&lt;/p&gt;

&lt;p&gt;Gate a rollout on vitals:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gpc status &lt;span class="nt"&gt;--sections&lt;/span&gt; vitals
gpc releases rollout increase &lt;span class="nt"&gt;--track&lt;/span&gt; production &lt;span class="nt"&gt;--to&lt;/span&gt; 50
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If vitals are breached, &lt;code&gt;gpc status&lt;/code&gt; exits code 6 and the rollout never happens. No dashboards. No manual checks. Just a threshold and an exit code.&lt;/p&gt;

&lt;p&gt;Post-deploy snapshot:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gpc status &lt;span class="nt"&gt;--format&lt;/span&gt; summary &lt;span class="nt"&gt;--refresh&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;com.example.app · v142 production · crashes 0.80% ↓ ✓ · ANR 0.20% ↓ ✓ · avg 4.6★ · 142 reviews
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;One line. Add it as the last step in your deploy pipeline. Something to look at when reviewing CI logs the next morning.&lt;/p&gt;

&lt;p&gt;Parse specific metrics:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# Get crash rate&lt;/span&gt;
gpc status &lt;span class="nt"&gt;--output&lt;/span&gt; json | jq &lt;span class="s1"&gt;'.vitals.crashes.value'&lt;/span&gt;

&lt;span class="c"&gt;# Check if all vitals are passing&lt;/span&gt;
gpc status &lt;span class="nt"&gt;--output&lt;/span&gt; json | jq &lt;span class="nt"&gt;-e&lt;/span&gt; &lt;span class="s1"&gt;'[.vitals | to_entries[].value.status] | all(. == "ok")'&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The JSON output gives you values, thresholds, trend direction, and status for each metric. Build whatever monitoring logic you need on top.&lt;/p&gt;

&lt;h2&gt;
  
  
  The details that matter
&lt;/h2&gt;

&lt;p&gt;&lt;strong&gt;Thresholds are configurable.&lt;/strong&gt; The defaults (2% crash, 1% ANR, 5% slow starts, 10% slow render) work for most apps. Override them globally in &lt;code&gt;.gpcrc.json&lt;/code&gt;, per-project with &lt;code&gt;gpc config set&lt;/code&gt;, or per-run with &lt;code&gt;--threshold crashes=1.5,anr=0.5&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Caching is built in.&lt;/strong&gt; Results are cached for 1 hour by default. Subsequent runs in the same window cost zero API calls. Force a fresh fetch with &lt;code&gt;--refresh&lt;/code&gt;. Read from cache only with &lt;code&gt;--cached&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Section filtering saves quota.&lt;/strong&gt; &lt;code&gt;--sections vitals&lt;/code&gt; skips the releases and reviews API calls entirely. Fetch only what you need.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Exit codes are semantic.&lt;/strong&gt; &lt;code&gt;0&lt;/code&gt; means all clear. &lt;code&gt;2&lt;/code&gt; means usage error. &lt;code&gt;4&lt;/code&gt; means API error. &lt;code&gt;6&lt;/code&gt; means a vitals threshold was breached. Your CI knows why something failed without parsing log output.&lt;/p&gt;

&lt;h2&gt;
  
  
  Quick context on GPC
&lt;/h2&gt;

&lt;p&gt;If you didn't read the &lt;a href="https://dev.to/yasserstudio/i-built-a-cli-that-covers-the-entire-google-play-developer-api-5d3a"&gt;first article in this series&lt;/a&gt;: GPC is a CLI that maps the entire Google Play Developer API. 204 endpoints. 7 TypeScript packages. 1,845 tests with 90%+ coverage on every core package.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;gpc status&lt;/code&gt; is one of 40+ commands. But it's the one I run the most.&lt;/p&gt;

&lt;h2&gt;
  
  
  Try it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="c"&gt;# npm&lt;/span&gt;
npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @gpc-cli/cli

&lt;span class="c"&gt;# Homebrew&lt;/span&gt;
brew &lt;span class="nb"&gt;install &lt;/span&gt;yasserstudio/tap/gpc

&lt;span class="c"&gt;# Standalone binary (no Node.js required)&lt;/span&gt;
curl &lt;span class="nt"&gt;-fsSL&lt;/span&gt; https://raw.githubusercontent.com/yasserstudio/gpc/main/scripts/install.sh | sh
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;gpc auth login
gpc status
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;a href="https://yasserstudio.github.io/gpc" rel="noopener noreferrer"&gt;Docs&lt;/a&gt; | &lt;a href="https://github.com/yasserstudio/gpc" rel="noopener noreferrer"&gt;GitHub&lt;/a&gt; | &lt;a href="https://yasserstudio.github.io/gpc/commands/status" rel="noopener noreferrer"&gt;Status command docs&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;Free to use. Code is on GitHub.&lt;/p&gt;

&lt;p&gt;What does your post-release monitoring look like? Are you checking dashboards manually, or do you have something automated? Genuinely curious.&lt;/p&gt;

</description>
      <category>android</category>
      <category>typescript</category>
      <category>cli</category>
      <category>devtools</category>
    </item>
    <item>
      <title>I built a CLI that covers the entire Google Play Developer API</title>
      <dc:creator>Yasser's studio</dc:creator>
      <pubDate>Tue, 31 Mar 2026 08:53:17 +0000</pubDate>
      <link>https://dev.to/yasserstudio/i-built-a-cli-that-covers-the-entire-google-play-developer-api-5d3a</link>
      <guid>https://dev.to/yasserstudio/i-built-a-cli-that-covers-the-entire-google-play-developer-api-5d3a</guid>
      <description>&lt;p&gt;I was uploading an AAB to the Play Console last year and thought — why am I still clicking through this in 2026?&lt;/p&gt;

&lt;p&gt;Every Android release is the same ritual. Open the Console, upload the bundle, fill in release notes, pick a track, set the rollout percentage, click through confirmation screens. Fifteen minutes of clicking for&lt;br&gt;
  something that should be one command.&lt;/p&gt;

&lt;p&gt;So I looked at what exists.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Fastlane supply&lt;/strong&gt; requires Ruby, Bundler, and about 150 gems. It covers roughly 20 of the 204 Google Play API endpoints. Uploads and metadata — that's about it. No reviews, no vitals, no subscriptions.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;gradle-play-publisher&lt;/strong&gt; is tied to Gradle. You can't use it in a standalone script or outside a build.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Custom bash scripts&lt;/strong&gt; work until the person who wrote them leaves your team.&lt;/p&gt;

&lt;p&gt;Nothing covers the full API. So I built one.&lt;/p&gt;

&lt;h2&gt;
  
  
  What is GPC?
&lt;/h2&gt;

&lt;p&gt;GPC is a command-line interface that maps the entire Google Play Developer API v3 — 204 endpoints — to simple, consistent commands.&lt;/p&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;
bash
  # Upload and release
  gpc releases upload app.aab --track beta

  # Check vitals
  gpc vitals crashes --version 142

  # Read reviews
  gpc reviews list --stars 1-2 --since 7d

  # Manage subscriptions
  gpc subscriptions list

  It runs on Node.js, installs via npm, Homebrew, or as a standalone binary (no Node.js required). Cold start is under 500ms. Works on macOS, Linux, and Windows.

  It's not just a release tool

  The part that surprised me during development was how much of the Play Console is actually accessible through the API that nobody builds tools for.

  You can sync store listings across 70+ languages. Pull crash data and ANR rates into your monitoring pipeline. Gate a rollout on vitals — if your crash rate spikes, GPC refuses to increase the rollout and tells
  you why. Manage subscriptions, verify purchases, convert pricing across regions.

  But two commands ended up being my most used:

  Preflight — catch policy violations before Google does

  gpc preflight app.aab


Runs 9 parallel scanners on your AAB — entirely offline, no API calls. Checks targetSdk compliance, missing exported flags, restricted permissions, hardcoded API keys, non-Play billing SDKs, privacy/tracking issues, COPPA flags, download size, and store listing metadata.

You get a findings report with severity levels. In CI, exit code 6 means something needs fixing before you upload.

I built this because I was tired of uploading a bundle and finding out two hours later that it was rejected. Now it's the first thing that runs in my pipeline.

Status — your app's health in one command

gpc status

Fires 10 parallel API calls and gives you a snapshot: active releases by track, crash rate, ANR rate, slow start rate, slow render rate, and recent reviews — with trend arrows and threshold indicators.

Add --watch 30 and it polls every 30 seconds. Add --notify and you get a desktop notification when a threshold is breached. --since-last shows what changed since your last check.

This replaced the "open three Console tabs and cross-reference" workflow I used to do after every release.

Built for CI/CD

  I designed GPC for pipelines from day one.

  - name: Upload to Play Store
    env:
      GPC_SERVICE_ACCOUNT: ${{ secrets.SA_KEY }}
    run: |
      npm install -g @gpc-cli/cli
      gpc preflight app.aab
      gpc releases upload app.aab --track internal
      gpc status --format summary

  A few things that make this work well:

  - TTY-aware output — tables in your terminal, JSON when piped. No flags needed.
  - Semantic exit codes — 3 means auth failed, 4 means API error, 6 means your crash rate threshold was breached. Your CI can react differently to each.
  - --dry-run on every write operation — test your pipeline without shipping to production.
  - Vitals gating — block a rollout increase if crash/ANR rates exceed your threshold. One flag.

  The architecture

  GPC is a TypeScript monorepo with 7 publishable packages:

  ┌─────────────────────┬──────────────────────────────────────────┐
  │       Package       │               What it does               │
  ├─────────────────────┼──────────────────────────────────────────┤
  │ @gpc-cli/cli        │ The CLI you run                          │
  ├─────────────────────┼──────────────────────────────────────────┤
  │ @gpc-cli/core       │ Business logic and command orchestration │
  ├─────────────────────┼──────────────────────────────────────────┤
  │ @gpc-cli/api        │ Typed Google Play API client             │
  ├─────────────────────┼──────────────────────────────────────────┤
  │ @gpc-cli/auth       │ Service account, OAuth, ADC              │
  ├─────────────────────┼──────────────────────────────────────────┤
  │ @gpc-cli/config     │ Config loading, profiles, env vars       │
  ├─────────────────────┼──────────────────────────────────────────┤
  │ @gpc-cli/plugin-sdk │ Plugin interface for extensions          │
  ├─────────────────────┼──────────────────────────────────────────┤
  │ @gpc-cli/plugin-ci  │ CI/CD helpers                            │
  └─────────────────────┴──────────────────────────────────────────┘


The key decision was TypeScript over Go. The existing play-console-cli is written in Go — fast binary, zero deps. But Android developers already have Node.js (React Native, build tools), npm is already in every
  CI pipeline, and @gpc-cli/api works as a standalone SDK in any Node.js project.

  Go gives you a binary. TypeScript gives you an ecosystem.

  Where it stands

  - 204 Google Play API endpoints — all covered
  - 1,845 tests, 90%+ line coverage on every core package
  - Available on npm, Homebrew, and as a standalone binary
  - Fastlane metadata format compatible (drop-in for store listings)
  - Plugin system with lifecycle hooks
  - Interactive mode for when you don't remember the flags

  It's pre-1.0, but it's the most tested project I've shipped, and I use it for my own apps.

  Get started

# npm
  npm install -g @gpc-cli/cli

# Homebrew
  brew install yasserstudio/tap/gpc

# Standalone binary
  curl -fsSL https://raw.githubusercontent.com/yasserstudio/gpc/main/scripts/install.sh | sh

  - Docs: yasserstudio.github.io/gpc
  - GitHub: github.com/yasserstudio/gpc

It's free to use. Code is on GitHub.

If you ship Android apps — what's the one thing about the Play Console you wish you could automate? I'm genuinely curious and happy to prioritize it.
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;

</description>
      <category>android</category>
      <category>typescript</category>
      <category>cli</category>
      <category>devtools</category>
    </item>
  </channel>
</rss>
