
Gpc Ci Integration
- 24 installs
- 1 repo stars
- Updated August 1, 2026
- yasserstudio/gpc-skills
Helps with ai & agent building tasks.
About
gpc-ci-integration is a Claude Code skill for ai & agent building. It helps solo builders move faster with AI-assisted coding.
- gpc-ci-integration
- AI & Agent Building
- AI-coding skill
Gpc Ci Integration by the numbers
- 24 all-time installs (skills.sh)
- +1 installs in the week ending Jul 27, 2026 (Skillselion tracking)
- Ranked #9,876 of 16,546 AI & Agent Building skills by installs in the Skillselion catalog
- Data as of Aug 2, 2026 (Skillselion catalog sync)
npx skills add https://github.com/yasserstudio/gpc-skills --skill gpc-ci-integrationAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 24 |
|---|---|
| repo stars | ★ 1 |
| Last updated | August 1, 2026 |
| Repository | yasserstudio/gpc-skills ↗ |
What it does
Helps with ai & agent building tasks.
Files
GPC CI Integration
When to use
Use this skill when the task involves:
- Setting up GPC in GitHub Actions, GitLab CI, Bitbucket Pipelines, or CircleCI
- Automating Play Store releases from CI/CD
- Configuring environment variables for CI authentication
- Using GPC's JSON output for scripting and automation
- Implementing vitals-gated rollouts in CI
- Building release pipelines with upload → promote → monitor flows
- Using
--dry-runfor safe CI testing
Inputs required
- CI platform (GitHub Actions, GitLab CI, Bitbucket, CircleCI, or generic)
- Auth method (usually service account via env var)
- Release workflow: upload only, upload + promote, or full pipeline
- Whether vitals gating is desired
- Package name of the Android app
Procedure
Quickest path: GPC GitHub Action (v0.9.81+)
For GitHub Actions users, the GPC GitHub Action is the fastest way to publish to the Play Store. No Node.js setup step, no manual install, no wrapper script.
Available on the GitHub Actions Marketplace.
Minimal usage:
- uses: yasserstudio/gpc-action@v1
with:
service-account-json: ${{ secrets.GPC_SERVICE_ACCOUNT }}
package-name: com.example.app
release-file: app/build/outputs/bundle/release/app-release.aab
track: internalOne step replaces the full install + run + cleanup sequence. The action runs a built-in preflight compliance gate before uploading, so non-compliant AABs are rejected before they reach the Play API.
Migrating from `r0adkll/upload-google-play`? It is a drop-in replacement. The input names are the same; change one line:
# Before:
- uses: r0adkll/upload-google-play@v1
# After:
- uses: yasserstudio/gpc-action@v1The action is a TypeScript action running on Node 24. No additional configuration is required for the migration.
For advanced pipelines (multi-step, vitals gating, changelog generation), continue with the manual workflow patterns below.
0) CI environment behavior
GPC auto-detects CI environments:
- Output: Defaults to JSON when stdout is not a TTY (piped or CI)
- Interactive: Prompts are disabled automatically when
CI=true - Colors: Disabled in non-TTY environments
- Plugin-CI: Writes GitHub Actions step summaries when
$GITHUB_STEP_SUMMARYis available
Environment variables for CI override:
GPC_NO_INTERACTIVE=1 # Explicitly disable prompts
GPC_NO_COLOR=1 # Explicitly disable colors
GPC_OUTPUT=json # Force JSON outputConfig resolution precedence (v0.9.81+): CLI flags override env vars, which override the active profile, which overrides .gpcrc.json, which falls back to defaults.
--service-account / --app flags (highest priority)
GPC_SERVICE_ACCOUNT / GPC_APP env vars
active profile (gpc auth switch <name>)
.gpcrc.json
defaultsPrior to v0.9.81, an active profile silently won over GPC_SERVICE_ACCOUNT/GPC_APP env vars. This is now fixed. If your CI sets GPC_SERVICE_ACCOUNT and a profile is also active, the env var takes effect as expected.
1) GitHub Actions
Minimal — upload to internal track:
name: Upload to Play Store
on:
push:
tags: ['v*']
workflow_dispatch: {}
jobs:
release:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Build AAB
run: ./gradlew bundleRelease
- name: Install GPC
run: npm install -g @gpc-cli/cli --ignore-scripts
- name: Upload to Play Store
env:
GPC_SERVICE_ACCOUNT: ${{ secrets.PLAY_SERVICE_ACCOUNT }}
GPC_APP: com.example.app
run: |
gpc releases upload app/build/outputs/bundle/release/app-release.aab \
--track internal \
--changes-not-sent-for-reviewFull pipeline — upload, check vitals, promote:
name: Release Pipeline
on:
workflow_dispatch:
inputs:
track:
description: 'Target track'
default: 'beta'
rollout:
description: 'Rollout percentage'
default: '100'
jobs:
release:
runs-on: ubuntu-latest
env:
GPC_APP: com.example.app
steps:
- uses: actions/checkout@v4
- name: Install GPC
run: npm install -g @gpc-cli/cli --ignore-scripts
- name: Preflight compliance check
run: gpc preflight app-release.aab --fail-on error --json
- name: Upload release
env:
GPC_SERVICE_ACCOUNT: ${{ secrets.PLAY_SERVICE_ACCOUNT }}
run: |
gpc releases upload app-release.aab \
--track ${{ inputs.track }} \
--rollout ${{ inputs.rollout }} \
--changes-not-sent-for-review \
--mapping-type PROGUARD \
--device-tier-config default
- name: Error if in review
env:
GPC_SERVICE_ACCOUNT: ${{ secrets.PLAY_SERVICE_ACCOUNT }}
run: gpc releases status --error-if-in-review
- name: Check vitals
if: inputs.track == 'production'
env:
GPC_SERVICE_ACCOUNT: ${{ secrets.PLAY_SERVICE_ACCOUNT }}
run: |
gpc vitals crashes --threshold 2.0
gpc vitals anr --threshold 0.47
- name: Release status
if: always()
env:
GPC_SERVICE_ACCOUNT: ${{ secrets.PLAY_SERVICE_ACCOUNT }}
run: gpc releases status --output markdown >> $GITHUB_STEP_SUMMARY
- name: Generate GitHub Release notes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gpc changelog generate | gh release create ${{ github.ref_name }} -F -
# Requires GPC v0.9.61+. --strict flag available to fail CI on jargon.Read:
references/github-actions.md
2) GitLab CI
release:
image: node:22
stage: deploy
variables:
GPC_SERVICE_ACCOUNT: $PLAY_SERVICE_ACCOUNT
GPC_APP: com.example.app
script:
- npm install -g @gpc-cli/cli
- gpc releases upload app-release.aab --track internal --changes-not-sent-for-review
only:
- tags3) Bitbucket Pipelines
pipelines:
tags:
'v*':
- step:
name: Upload to Play Store
image: node:22
script:
- npm install -g @gpc-cli/cli
- gpc releases upload app-release.aab --track internal --changes-not-sent-for-review
deployment: production4) CircleCI
jobs:
release:
docker:
- image: cimg/node:22.0
steps:
- checkout
- run:
name: Upload to Play Store
command: |
npm install -g @gpc-cli/cli
gpc releases upload app-release.aab --track internal --changes-not-sent-for-review
environment:
GPC_APP: com.example.app5) Using standalone binary (no Node.js)
For minimal CI images without Node.js:
- name: Install GPC binary
run: curl -fsSL https://raw.githubusercontent.com/yasserstudio/gpc/main/scripts/install.sh | bash
- name: Upload
run: gpc releases upload app-release.aab --track internal6) Output formats for CI scripting
GPC auto-outputs JSON in CI (non-TTY). Parse with jq:
JUnit output caveat:--output junitis available but testcasenameattributes use generic identifiers (item-1,item-2) for some commands (tracks list,releases status). Prefer--output json | jqfor reliable CI parsing. Use--output junitonly if your test reporter specifically requires JUnit XML format.
CSV/TSV output (v0.9.68+): All commands support --output csv and --output tsv for spreadsheet-friendly or tab-delimited parsing without requiring jq:
# Export release status as CSV (headers on first line)
gpc releases status --output csv > releases.csv
# TSV for awk / cut parsing in shell scripts
gpc vitals crashes --output tsv | awk -F'\t' '{print $2}'
# Force a specific format regardless of TTY detection
GPC_OUTPUT=csv gpc apps listUse CSV/TSV when piping into tools like csvkit, mlr, or spreadsheet imports. Use JSON when you need nested structure or when piping into jq.
# Get version code from upload result
VERSION=$(gpc releases upload app.aab --track beta | jq -r '.data.versionCode')
# Check if vitals are OK
CRASH_RATE=$(gpc vitals crashes --output json | jq -r '.data.crashRate')
# Conditional promotion
if (( $(echo "$CRASH_RATE < 2.0" | bc -l) )); then
gpc releases promote --from beta --to production --rollout 10
fi7) Exit codes for CI logic
| Code | Meaning | CI Action |
|---|---|---|
0 | Success | Continue |
1 | General error | Fail job |
2 | Usage error (bad arguments) | Fix command |
3 | Authentication error | Check secrets |
4 | API error (rate limit, permission) | Retry or fix permissions |
5 | Network error | Retry |
6 | Threshold breach (vitals) | Block promotion |
10 | Plugin error | Check plugin config |
- name: Check vitals
run: gpc vitals crashes --threshold 2.0
continue-on-error: false # Exit code 6 fails the job8) Vitals-gated rollout pattern
- name: Upload to beta
run: |
gpc publish app.aab --track beta \
--changes-not-sent-for-review \
--mapping-type PROGUARD
- name: Wait for crash data
run: sleep 3600 # Wait 1 hour for crash data
- name: Gate on vitals
run: |
gpc vitals crashes --threshold 2.0
gpc vitals anr --threshold 0.47
- name: Promote to production
run: gpc releases promote --from beta --to production --rollout 10Wait for bundle processing before promoting (v0.9.69+)
After uploading a large AAB, bundle processing on Google's side can take 30–120 seconds. Use gpc bundles wait as an explicit gate instead of relying on the built-in Fibonacci polling inside gpc publish:
- name: Upload AAB
run: |
gpc releases upload app-release.aab \
--track internal \
--changes-not-sent-for-review
- name: Wait for bundle processing
run: gpc bundles wait --version-code ${{ env.VERSION_CODE }}
# Exits 0 when ACTIVE, non-zero on timeout or error.
# Prevents INVALID_ARGUMENT errors when promoting before processing completes.
- name: Promote to beta
run: gpc releases promote --from internal --to betaThis is especially useful in multi-job pipelines where upload and promote run in separate jobs.
Gate Play Store release notes on character budget (v0.9.62+)
- name: Verify all locales fit Play Store 500-char budget
run: |
gpc changelog generate --target play-store \
--locales auto \
--app com.example.app \
--strict
# --strict exits 1 if any locale overflows; collects all overflows in one report.Translate release notes on every tag (v0.9.63+)
- name: Generate + translate Play Store release notes
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
# Or OPENAI_API_KEY / GOOGLE_GENERATIVE_AI_API_KEY / AI_GATEWAY_API_KEY
run: |
gpc changelog generate --target play-store \
--locales auto \
--app com.example.app \
--ai \
--strict \
--format json > release-notes.json
# --strict exits 1 if any locale fails to translate or overflows 500 chars.
# --format json emits the `ai` block (provider, model, tokensIn, tokensOut,
# plus runId + costUsd on the Gateway path) for log aggregation.Write translated notes into a Play Store draft (v0.9.64+)
- name: Write translated release notes into Play Store draft
env:
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
run: |
gpc changelog generate --target play-store \
--locales auto \
--ai \
--apply \
--track production
# --apply writes the result into the latest draft release on the track.
# Requires a draft release to exist on the target track.
# Exits with RELEASE_NO_DRAFT (code 1) if no draft found.9) Dry-run for testing pipelines
Test your CI pipeline without making real changes:
- name: Test release pipeline (dry-run)
run: |
gpc releases upload app.aab --track beta --dry-run
gpc releases promote --from beta --to production --rollout 10 --dry-run9a) Handling rejected apps in CI
When Google Play rejects an app update, gpc releases status reports the rejection reason. Use --error-if-in-review to detect and handle in-review or rejected states in your pipeline:
- name: Check for rejection
id: review-check
run: gpc releases status --error-if-in-review
continue-on-error: true
- name: Handle rejection
if: steps.review-check.outcome == 'failure'
run: |
echo "Release was rejected or is still in review."
echo "Check the Play Console for details."
gpc releases status --output json | jq '.data.releases[] | select(.status == "rejected")'
exit 1The --error-if-in-review flag exits with code 4 if any release on the target track is in inReview or rejected status. Use this before uploading a new version to avoid EDIT_CONFLICT errors when a previous submission is still pending review.
10) Markdown output for GitHub step summaries
gpc releases status --output markdown >> $GITHUB_STEP_SUMMARY
gpc vitals overview --output markdown >> $GITHUB_STEP_SUMMARY11) Supply chain security in CI
GPC's own CI uses 12 protection layers. When integrating GPC into your pipeline, follow these practices:
# Pin GPC version (don't rely on @latest in production)
- run: npm install -g @gpc-cli/cli@0.9.82 --ignore-scripts
# Or use the standalone binary (no npm supply chain risk)
- run: curl -fsSL https://raw.githubusercontent.com/yasserstudio/gpc/main/scripts/install.sh | bash
# Pin GitHub Actions to commit SHAs
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6--ignore-scripts on all pnpm/npm install commands (v0.9.74+)
All CI pnpm install and npm install commands must use --ignore-scripts to block lifecycle-script execution by untrusted packages. GPC's own pnpm.onlyBuiltDependencies is set to ["turbo", "esbuild"] in package.json — only those two packages are permitted to run install scripts.
- name: Install dependencies
run: pnpm install --frozen-lockfile --ignore-scriptsLockfile integrity verification (v0.9.74+)
Verify pnpm-lock.yaml has not been tampered with before installing:
- name: Verify lockfile integrity
run: |
sha256sum pnpm-lock.yaml > /tmp/lockfile.sha256
# Compare against the known-good SHA stored as a repo secret or artifact
echo "${{ secrets.LOCKFILE_SHA256 }} pnpm-lock.yaml" | sha256sum -c -Step-scoped secrets (v0.9.74+)
Never set GPC_SERVICE_ACCOUNT at the job level. Always scope it to the specific step that needs it:
# WRONG — secret is available to every step in the job
jobs:
release:
env:
GPC_SERVICE_ACCOUNT: ${{ secrets.PLAY_SERVICE_ACCOUNT }}
# CORRECT — secret only visible to the upload step
- name: Upload release
env:
GPC_SERVICE_ACCOUNT: ${{ secrets.PLAY_SERVICE_ACCOUNT }}
run: gpc releases upload app-release.aab --track internalDeep security scan (v0.9.74+)
GPC ships a pnpm security:deep script that runs deepsec scanning across all packages. Add it to your release pipeline:
- name: Deep security scan
run: pnpm security:deepworkflow_dispatch trigger for manual re-runs (v0.9.74+)
Add workflow_dispatch: {} to every release workflow so engineers can re-trigger failed runs without creating a new tag:
on:
push:
tags: ['v*']
workflow_dispatch: {}For Socket.dev scanning on your own repo, add socket ci to your workflow:
- name: Socket Security Scan
run: |
npm install -g socket@latest --ignore-scripts
socket ci --repo your-repo
env:
SOCKET_SECURITY_API_TOKEN: ${{ secrets.SOCKET_SECURITY_API_TOKEN }}12) APK uploads
GPC auto-detects the file format and uses the correct API endpoint:
# AAB upload (recommended)
- run: gpc releases upload app-release.aab --track internal
# APK upload (auto-detected)
- run: gpc releases upload app-release.apk --track internal
# Draft release (not visible to users until explicitly released)
- run: gpc releases upload app-release.aab --track production --status draft13) Retry and timeout configuration
env:
GPC_MAX_RETRIES: '5'
GPC_TIMEOUT: '60000'
GPC_BASE_DELAY: '2000'
GPC_MAX_DELAY: '120000'
GPC_RATE_LIMIT: '50'Use --retry-log to debug transient failures:
gpc releases upload app.aab --track beta --retry-log retries.logVerification
- CI job completes with exit code 0
gpc releases status(in subsequent step) confirms release is on expected track- GitHub Actions step summary shows release details
- Vitals check exits 0 (below threshold) or 6 (above threshold) as expected
Failure modes / debugging
| Symptom | Likely Cause | Fix |
|---|---|---|
AUTH_INVALID in CI | Secret not set or wrong format | Verify PLAY_SERVICE_ACCOUNT secret contains valid JSON |
Permission denied | Service account lacks Play Console access | Grant access in Play Console → Settings → API access |
| Timeout on upload | Large AAB + slow CI network | Increase GPC_TIMEOUT |
| Rate limited | Too many API calls | Increase GPC_BASE_DELAY, reduce parallelism |
| Exit code 6 | Vitals threshold breached | Review crash/ANR data, fix issues before promoting |
EDIT_CONFLICT | Parallel runs editing same app | Serialize release jobs or use job concurrency limits |
IN_REVIEW or REJECTED | Previous submission pending or rejected | Use --error-if-in-review before uploading; resolve rejection in Play Console first |
| Changes auto-submitted for review | Edit committed without opt-out flag | Add --changes-not-sent-for-review to upload/push commands |
Read:
references/troubleshooting.md
Related skills
- gpc-setup: Initial authentication and configuration
- gpc-release-flow: Release commands and rollout management
- gpc-vitals-monitoring: Vitals metrics and review management
- gpc-metadata-sync: Store listing automation
{
"skill_name": "gpc-ci-integration",
"evals": [
{
"id": 1,
"prompt": "I need to set up a GitHub Actions workflow that automatically uploads our AAB to the internal track whenever we push a tag like v1.2.3. Our app is com.widgetco.app and we already have the service account JSON stored as a GitHub secret called PLAY_SA_KEY. Can you write the workflow file?",
"expected_output": "Complete GitHub Actions workflow YAML that triggers on tag push, installs GPC, and uploads to internal track",
"files": [],
"expectations": [
"Provides a complete .github/workflows/ YAML file",
"Triggers on push tags v* pattern",
"Sets GPC_SERVICE_ACCOUNT from the PLAY_SA_KEY secret",
"Sets GPC_APP to com.widgetco.app",
"Installs GPC with npm install -g gpc",
"Runs gpc releases upload or gpc publish with --track internal"
]
},
{
"id": 2,
"prompt": "Our CI pipeline keeps failing intermittently with NETWORK_ERROR when uploading to Play Store. The AAB is about 80MB and we're on a shared CI runner. Sometimes it works, sometimes it times out. How can I make this more reliable?",
"expected_output": "Configures retry and timeout environment variables to handle transient network failures",
"files": [],
"expectations": [
"Suggests increasing GPC_TIMEOUT (e.g., 120000 for 2 minutes)",
"Suggests increasing GPC_MAX_RETRIES (e.g., 5)",
"Mentions GPC_BASE_DELAY and GPC_MAX_DELAY for backoff tuning",
"Suggests --retry-log for debugging transient failures",
"Shows the env vars in a CI YAML format"
]
},
{
"id": 3,
"prompt": "We want a full release pipeline: upload to beta on tag push, then a manual workflow_dispatch to promote from beta to production with a staged rollout. The promotion step should check vitals first and only promote if crash rate is under 2%. We also want the release status written to the GitHub step summary.",
"expected_output": "Two GitHub Actions workflows: auto-upload on tag and manual promote with vitals gating and step summary",
"files": [],
"expectations": [
"Provides two workflow files or a single file with two jobs",
"First workflow triggers on tag push and uploads to beta",
"Second workflow uses workflow_dispatch for manual promotion",
"Includes gpc vitals crashes --threshold 2.0 before promotion",
"Uses gpc releases promote --from beta --to production with rollout percentage",
"Writes to $GITHUB_STEP_SUMMARY with --output markdown"
]
}
]
}
Bitbucket Pipelines Templates for GPC
Ready-to-use bitbucket-pipelines.yml configurations for GPC.
Authentication setup
Store your service account JSON as a Bitbucket repository variable:
1. Go to Repository settings > Pipelines > Repository variables 2. Add variable PLAY_SA_KEY with the JSON content 3. Check Secured to encrypt it
Basic upload on tag
image: node:20
pipelines:
tags:
'v*':
- step:
name: Upload to internal
script:
- npm install -g @gpc-cli/cli
- gpc validate app-release.aab
- gpc releases upload app-release.aab --track internal --changes-not-sent-for-review
artifacts:
- '*.log'Set GPC_SERVICE_ACCOUNT and GPC_APP as repository variables.
Full pipeline with manual promotion
image: node:20
definitions:
steps:
- step: &install-gpc
name: Install GPC
script:
- npm install -g @gpc-cli/cli
- gpc --version
pipelines:
tags:
'v*':
- step:
name: Upload to beta
script:
- npm install -g @gpc-cli/cli
- gpc releases upload app-release.aab --track beta --changes-not-sent-for-review
- step:
name: Check vitals
trigger: manual
script:
- npm install -g @gpc-cli/cli
- gpc vitals crashes --threshold 1.5
- gpc vitals anr --threshold 0.4
- step:
name: Promote to production
trigger: manual
script:
- npm install -g @gpc-cli/cli
- gpc releases promote --from beta --to production --rollout 10Scheduled vitals check
pipelines:
custom:
vitals-check:
- step:
name: Check app vitals
script:
- npm install -g @gpc-cli/cli
- gpc vitals overview --json
- gpc vitals crashes --threshold 2.0
- gpc vitals anr --threshold 0.5Trigger via Bitbucket Schedules: Pipelines > Schedules > Add schedule and select the vitals-check custom pipeline.
Environment variables
Set these as repository variables (all secured):
| Variable | Value |
|---|---|
GPC_SERVICE_ACCOUNT | Service account JSON content |
GPC_APP | com.example.app |
GPC_MAX_RETRIES | 5 |
GPC_TIMEOUT | 120000 |
Tips
- Use
trigger: manualfor promotion steps - Bitbucket has a 120-minute step timeout — set
GPC_TIMEOUTwell under this - Use
artifactsto save logs and reports - Secured variables are not available in PRs from forks (security feature)
- Use
definitionsfor reusable step templates
GitHub Actions Integration
Authentication Setup
1. Create a repository secret:
- Go to repo → Settings → Secrets and variables → Actions
- Add
PLAY_SERVICE_ACCOUNTcontaining the service account JSON content
2. Reference in workflow — step-scoped only (v0.9.74+):
# Scope GPC_SERVICE_ACCOUNT to the specific upload step, not the job
- name: Upload release
env:
GPC_SERVICE_ACCOUNT: ${{ secrets.PLAY_SERVICE_ACCOUNT }}
run: gpc releases upload app-release.aab --track internalNever set GPC_SERVICE_ACCOUNT at the jobs.<job>.env level — this exposes the secret to every step including third-party actions.
Workflow Templates
Upload on Tag Push
name: Release to Play Store
on:
push:
tags: ['v*']
workflow_dispatch: {}
jobs:
release:
runs-on: ubuntu-latest
env:
GPC_APP: com.example.app
steps:
- uses: actions/checkout@v4
- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: 17
- name: Build AAB
run: ./gradlew bundleRelease
- name: Install GPC
run: npm install -g @gpc-cli/cli --ignore-scripts
- name: Upload to internal track
env:
GPC_SERVICE_ACCOUNT: ${{ secrets.PLAY_SERVICE_ACCOUNT }}
run: |
gpc publish \
app/build/outputs/bundle/release/app-release.aab \
--track internal \
--notes "Release ${GITHUB_REF_NAME}" \
--changes-not-sent-for-review
- name: Release summary
if: always()
env:
GPC_SERVICE_ACCOUNT: ${{ secrets.PLAY_SERVICE_ACCOUNT }}
run: gpc releases status --output markdown >> $GITHUB_STEP_SUMMARYManual Release with Inputs
name: Manual Release
on:
workflow_dispatch:
inputs:
track:
description: Target track
type: choice
options: [internal, alpha, beta, production]
default: internal
rollout:
description: Rollout percentage (production only)
type: number
default: 100
dry_run:
description: Dry run (preview only)
type: boolean
default: false
jobs:
release:
runs-on: ubuntu-latest
env:
GPC_APP: com.example.app
steps:
- uses: actions/checkout@v4
- name: Install GPC
run: npm install -g @gpc-cli/cli --ignore-scripts
- name: Upload release
env:
GPC_SERVICE_ACCOUNT: ${{ secrets.PLAY_SERVICE_ACCOUNT }}
run: |
FLAGS=""
if [ "${{ inputs.dry_run }}" = "true" ]; then
FLAGS="--dry-run"
fi
gpc releases upload app-release.aab \
--track ${{ inputs.track }} \
--rollout ${{ inputs.rollout }} \
--changes-not-sent-for-review \
$FLAGSScheduled Vitals Check
name: Vitals Monitor
on:
schedule:
- cron: '0 9 * * 1-5' # Weekdays at 9am UTC
workflow_dispatch: {}
jobs:
check:
runs-on: ubuntu-latest
env:
GPC_APP: com.example.app
steps:
- name: Install GPC
run: npm install -g @gpc-cli/cli --ignore-scripts
- name: Vitals dashboard
env:
GPC_SERVICE_ACCOUNT: ${{ secrets.PLAY_SERVICE_ACCOUNT }}
run: gpc vitals overview --output markdown >> $GITHUB_STEP_SUMMARY
- name: Check thresholds
env:
GPC_SERVICE_ACCOUNT: ${{ secrets.PLAY_SERVICE_ACCOUNT }}
run: |
gpc vitals crashes --threshold 2.0
gpc vitals anr --threshold 0.47Step Summary Output
GPC supports --output markdown which works perfectly with $GITHUB_STEP_SUMMARY:
# Add release status to step summary
gpc releases status --output markdown >> $GITHUB_STEP_SUMMARY
# Add vitals dashboard to step summary
gpc vitals overview --output markdown >> $GITHUB_STEP_SUMMARY
# Add review summary
gpc reviews list --stars 1-2 --since 7d --output markdown >> $GITHUB_STEP_SUMMARYPlugin-CI Integration
GPC's @gpc-cli/plugin-ci automatically detects GitHub Actions and writes step summaries:
- After each command: markdown table with app, duration, exit code
- On errors: error details with code and message
This happens automatically — no configuration needed.
Concurrency Control
Prevent parallel releases to the same app:
concurrency:
group: play-store-release-${{ github.ref }}
cancel-in-progress: false # Don't cancel running releasesCaching GPC
Speed up workflows by caching GPC installation:
- name: Cache GPC
uses: actions/cache@v4
with:
path: ~/.npm
key: gpc-${{ hashFiles('**/package-lock.json') }}
- name: Install GPC
run: npm install -g @gpc-cli/cliGitLab CI Templates for GPC
Ready-to-use .gitlab-ci.yml configurations for GPC.
Authentication setup
Store your service account JSON as a GitLab CI/CD variable:
1. Go to Settings > CI/CD > Variables 2. Add variable PLAY_SA_KEY with the JSON content 3. Set type to Variable (not File) 4. Mark as Protected and Masked if possible
Basic upload on tag
stages:
- deploy
upload-to-internal:
stage: deploy
image: node:20
only:
- tags
variables:
GPC_SERVICE_ACCOUNT: $PLAY_SA_KEY
GPC_APP: com.example.app
before_script:
- npm install -g @gpc-cli/cli
script:
- gpc validate app/build/outputs/bundle/release/app-release.aab
- gpc releases upload app/build/outputs/bundle/release/app-release.aab --track internal --changes-not-sent-for-reviewFull pipeline with promotion
stages:
- upload
- verify
- promote
variables:
GPC_SERVICE_ACCOUNT: $PLAY_SA_KEY
GPC_APP: com.example.app
GPC_MAX_RETRIES: "5"
GPC_TIMEOUT: "120000"
upload-beta:
stage: upload
image: node:20
only:
- tags
before_script:
- npm install -g @gpc-cli/cli
script:
- gpc releases upload app-release.aab --track beta --changes-not-sent-for-review
- echo "Uploaded to beta"
check-vitals:
stage: verify
image: node:20
only:
- tags
when: delayed
start_in: "24 hours"
before_script:
- npm install -g @gpc-cli/cli
script:
- gpc vitals crashes --threshold 1.5
- gpc vitals anr --threshold 0.4
allow_failure: false
promote-production:
stage: promote
image: node:20
only:
- tags
when: manual
before_script:
- npm install -g @gpc-cli/cli
script:
- gpc releases promote --from beta --to production --rollout 10
- echo "Promoted to production at 10%"Scheduled vitals check
vitals-report:
image: node:20
only:
- schedules
variables:
GPC_SERVICE_ACCOUNT: $PLAY_SA_KEY
GPC_APP: com.example.app
before_script:
- npm install -g @gpc-cli/cli
script:
- gpc vitals overview --json > vitals.json
- gpc vitals crashes --threshold 2.0
- gpc vitals anr --threshold 0.5
artifacts:
paths:
- vitals.json
expire_in: 30 daysIAP sync on merge to main
sync-iap:
stage: deploy
image: node:20
only:
- main
changes:
- products/**/*.json
variables:
GPC_SERVICE_ACCOUNT: $PLAY_SA_KEY
GPC_APP: com.example.app
before_script:
- npm install -g @gpc-cli/cli
script:
- gpc iap sync --dir products/ --dry-run
- gpc iap sync --dir products/Tips
- Use
when: delayedwithstart_infor time-gated stages - Use
when: manualfor promotion steps that need human approval - Use
changes:to run IAP/metadata sync only when relevant files change - Set
GPC_MAX_RETRIESandGPC_TIMEOUTas global variables for shared runners - Use
artifactsto save vitals reports and upload logs
CI Troubleshooting
Authentication Failures
AUTH_INVALID in CI
Most common cause: Secret not set correctly.
Debug steps:
- name: Verify secret exists
run: |
if [ -z "$GPC_SERVICE_ACCOUNT" ]; then
echo "ERROR: GPC_SERVICE_ACCOUNT is not set"
exit 1
fi
echo "Secret length: ${#GPC_SERVICE_ACCOUNT}"Common mistakes: 1. Secret name mismatch (PLAY_SERVICE_ACCOUNT vs GPC_SERVICE_ACCOUNT) 2. Secret contains file path instead of JSON content 3. JSON is truncated (check character limit on CI platform) 4. Wrong env var name in workflow
Permission denied
Service account exists but lacks Play Console permissions.
Fix: 1. Play Console → Settings → API access 2. Find service account → Manage → check permissions 3. Ensure "Release to production" is granted (if deploying to production)
Upload Issues
Timeout on large AAB
env:
GPC_TIMEOUT: '120000' # 2 minutes
GPC_MAX_RETRIES: '5'EDIT_CONFLICT in parallel jobs
Two jobs trying to modify the same app simultaneously.
Fix: Add concurrency control:
concurrency:
group: play-store-${{ env.GPC_APP }}
cancel-in-progress: falseVersion code already used
CI built the same version code twice.
Fix: Ensure version code is unique per build:
// build.gradle
android {
defaultConfig {
versionCode System.getenv("GITHUB_RUN_NUMBER")?.toInteger() ?: 1
}
}Network Issues
Rate limiting (429 errors)
env:
GPC_BASE_DELAY: '2000'
GPC_MAX_DELAY: '120000'
GPC_MAX_RETRIES: '5'
GPC_RATE_LIMIT: '30' # Reduce from default 50Proxy required
env:
HTTPS_PROXY: ${{ secrets.PROXY_URL }}
GPC_CA_CERT: /path/to/ca-bundle.crtExit Code Reference
Use exit codes for conditional CI logic:
- name: Check vitals
id: vitals
run: gpc vitals crashes --threshold 2.0
continue-on-error: true
- name: Promote (only if vitals OK)
if: steps.vitals.outcome == 'success'
run: gpc releases promote --from beta --to production
- name: Alert (if vitals bad)
if: steps.vitals.outcome == 'failure'
run: echo "Vitals threshold breached — promotion blocked"Rejected or In-Review Releases
If a previous submission is still in review or was rejected, uploading a new version may fail with EDIT_CONFLICT.
Detect before uploading:
- name: Check for pending review
run: gpc releases status --error-if-in-reviewThe --error-if-in-review flag exits with code 4 if any release is in inReview or rejected status. Handle it before uploading:
- name: Check for pending review
id: review-check
run: gpc releases status --error-if-in-review
continue-on-error: true
- name: Upload release
if: steps.review-check.outcome == 'success'
run: gpc releases upload app.aab --track beta --changes-not-sent-for-reviewIf a release was rejected, resolve the issue in the Play Console before re-uploading from CI.
Debugging with Verbose Output
- name: Upload with debug logging
run: gpc releases upload app.aab --track beta --verbose --retry-log retries.log
- name: Upload retry log
if: failure()
uses: actions/upload-artifact@v4
with:
name: retry-log
path: retries.logPlatform-Specific Notes
GitLab CI
- Use
$CI_JOB_TOKENfor artifact access, butGPC_SERVICE_ACCOUNTfor Play Store - Variables set in Settings → CI/CD → Variables
Bitbucket Pipelines
- Secrets set in Repository Settings → Pipelines → Repository Variables
- Max secret size is 32KB — base64 encode if needed
CircleCI
- Use Contexts for shared secrets across projects
- Environment variables in Project Settings → Environment Variables
#!/usr/bin/env node
/**
* Detection script for GPC CLI.
* Returns JSON with installation status, version, auth state, and config.
* Used by Claude Code skill system for deterministic environment detection.
*
* Exit codes:
* 0 — GPC detected (may or may not be authenticated)
* 1 — GPC not found
*/
import { execSync } from "node:child_process";
import { existsSync } from "node:fs";
import { join } from "node:path";
function run(cmd) {
try {
return execSync(cmd, { encoding: "utf-8", timeout: 10000 }).trim();
} catch {
return null;
}
}
const result = {
installed: false,
version: null,
installMethod: null,
authStatus: null,
authMethod: null,
profile: null,
envAuth: false,
defaultApp: null,
configFile: null,
nodeVersion: process.version,
};
// Check if gpc is installed globally
const versionOutput = run("gpc --version");
if (!versionOutput) {
// Try npx
const npxVersion = run("npx gpc --version 2>/dev/null");
if (!npxVersion) {
console.log(JSON.stringify(result, null, 2));
process.exit(1);
}
result.version = npxVersion;
result.installed = true;
result.installMethod = "npx";
} else {
result.version = versionOutput;
result.installed = true;
result.installMethod = "global";
}
// Check auth status
const authOutput = run("gpc auth status --json 2>/dev/null");
if (authOutput) {
try {
const auth = JSON.parse(authOutput);
result.authStatus = auth.status || "unknown";
result.authMethod = auth.method || null;
result.profile = auth.profile || null;
} catch {
result.authStatus = "parse_error";
}
}
// Check for env-based auth
if (process.env.GPC_SERVICE_ACCOUNT) {
result.envAuth = true;
}
// Check default app
const configOutput = run("gpc config get app --json 2>/dev/null");
if (configOutput) {
try {
const config = JSON.parse(configOutput);
result.defaultApp = config.value || config.app || null;
} catch {
result.defaultApp = configOutput || null;
}
}
// Check for .gpcrc.json in current directory
const rcPath = join(process.cwd(), ".gpcrc.json");
if (existsSync(rcPath)) {
result.configFile = rcPath;
}
console.log(JSON.stringify(result, null, 2));
process.exit(0);