# apkgo
   [](./LICENSE) [](https://skills.sh/KevinGong2013/apkgo)
๐ **Language / ่ฏญ่จ**: **English** ยท [็ฎไฝไธญๆ](./README.md)
One command to publish an APK to every major Chinese Android app store.
> **Don't want to run CI or touch a terminal?** Try the hosted [**apkgo cloud**](https://apkgo.baici.tech) โ release from a browser, with credentials stored server-side, multi-user collaboration, and full release history. No install, no ops, friendly enough for ops and PM teammates. Beyond the Android stores the CLI covers, the cloud also publishes to **iOS (App Store)** and **HarmonyOS**, and **monitors app review and on-shelf (live) status in real time**.
## Install
**macOS / Linux** โ one-liner that detects OS/arch, downloads, and verifies SHA-256:
```bash
curl -fsSL https://apkgo.com.cn/install.sh | sh
```
> Defaults to `/usr/local/bin`. If it isn't writable, the script will tell you to either use `sudo` or set `APKGO_INSTALL_DIR=$HOME/.local/bin`.
> Pin a version: `APKGO_VERSION=v4.0.0 sh`.
**Other methods:**
```bash
# AI Agent Skill (works with Claude Code, Cursor, Windsurf, and 40+ agents)
npx skills add KevinGong2013/apkgo
# Go
go install github.com/KevinGong2013/apkgo/v4@latest
# Docker
docker pull ghcr.io/kevingong2013/apkgo:latest
```
Manual download / Windows
```bash
# macOS (Apple Silicon)
curl -fsSL https://github.com/KevinGong2013/apkgo/releases/latest/download/apkgo_Darwin_arm64.tar.gz | tar xz -C /usr/local/bin apkgo
# macOS (Intel)
curl -fsSL https://github.com/KevinGong2013/apkgo/releases/latest/download/apkgo_Darwin_x86_64.tar.gz | tar xz -C /usr/local/bin apkgo
# Linux (x86_64)
curl -fsSL https://github.com/KevinGong2013/apkgo/releases/latest/download/apkgo_Linux_x86_64.tar.gz | tar xz -C /usr/local/bin apkgo
# Linux (arm64)
curl -fsSL https://github.com/KevinGong2013/apkgo/releases/latest/download/apkgo_Linux_arm64.tar.gz | tar xz -C /usr/local/bin apkgo
# Windows (PowerShell)
# Download apkgo_Windows_x86_64.zip from https://github.com/KevinGong2013/apkgo/releases/latest
# Extract and add apkgo.exe to PATH
```
> If `apkgo version` reports `unknown command`, or the help lists only `doctor` / `upload`, an old apkgo earlier on your PATH is shadowing the new one (typically v1, left in `%USERPROFILE%\go\bin` by a `go install` without `/v4`; an older v3 install can shadow it the same way). Run `where apkgo` (macOS/Linux: `which -a apkgo`) to list every copy and delete the stale one.
## Quick start
```bash
# 1. Generate a config file
apkgo init
# 2. Fill in store credentials
vim apkgo.yaml
# 3. Upload
apkgo upload -f app.apk
```
## Usage
### Upload
```bash
# Upload to every configured store
apkgo upload -f app.apk
# Upload to specific stores
apkgo upload -f app.apk --store huawei,xiaomi
# With release notes
apkgo upload -f app.apk --notes "Fixed login issue"
apkgo upload -f app.apk --notes-file CHANGELOG.md
# Split-arch APKs
apkgo upload -f app-arm32.apk --file64 app-arm64.apk
# Validate without uploading
apkgo upload -f app.apk --dry-run
# Upload vivo to its sandbox; dry-run every other store
apkgo upload -f app.apk --sandbox
# Upload straight from an http(s) URL (fetched to a temp file; add headers for private URLs, repeatable)
apkgo upload -f https://artifacts.example.com/app-v1.apk --store huawei
apkgo upload -f https://private.example.com/app.apk --fetch-header "Authorization: Bearer xxx"
# Scheduled release (RFC3339 time)
apkgo upload -f app.apk --release-time 2026-06-20T10:00:00+08:00
# Update the store listing (intro / description / icon / screenshots) with this version
apkgo upload -f app.apk --listing listing.yaml
# Google Play AAB (.aab only goes to googleplay)
apkgo upload -f app-release.aab -s googleplay
```
- When `-f` is a URL, huawei / honor / vivo have the store pull the package from that URL directly (honor only for files โฅ `url_push_min_mb`, default 100 MB; 0 = always); other stores get it downloaded and uploaded by apkgo.
- `--release-time` is supported by huawei, harmony, honor, xiaomi, oppo, vivo, samsung and tencent; other stores release immediately.
- File types: `.apk` goes to every Android store; `.aab` is googleplay-only; a HarmonyOS `.app` is harmony-only.
`--sandbox` and `--dry-run` are mutually exclusive. vivo is currently the only sandbox-capable store: it makes real sandbox API calls, while every other target receives the same local validation as `--dry-run`. Sandbox runs skip all hooks, upload history, lifecycle event callbacks, and upload telemetry. The top-level result contains `"sandbox": true`; the vivo result contains `"sandbox": true`; other stores contain `"dry_run": true`.
The vivo [sandbox environment](https://dev.vivo.com.cn/documentCenter/doc/327#s-l67kfh1m) has separate application data, `access_key`, and secrets from production. Follow the [online test environment instructions](https://dev.vivo.com.cn/documentCenter/doc/327#s-b9qi52f4) to create the application and request separate sandbox credentials first. Each sandbox API is limited to 100 calls per day.
#### Update the store listing (`--listing`)
A release can also update the store listing โ **one-line intro, long description, icon and screenshots**. The listing is written after the package upload and before submission, so it is reviewed together with the new version. There is no listing-only update, and the app name is never changed (it must match the APK label, APP ๅคๆก and ่ฝฏ่). Only the store's default language is updated.
```bash
apkgo upload -f app.apk --listing listing.yaml --dry-run # validate against every store's spec first
apkgo upload -f app.apk --listing listing.yaml
```
```yaml
# listing.yaml โ defaults plus per-store overrides; relative paths resolve against this file
brief: One-line intro
description_file: desc.md # or description: |
icon: assets/icon-512.png
screenshots: [assets/s1.png, assets/s2.png, assets/s3.png, assets/s4.png]
stores:
oppo:
brief: ๅไธๅญไปฅๅ
็็ฎไป # OPPO: โค13 chars, no punctuation or spaces
huawei:
icon: assets/icon-216.png # Huawei wants a 216ร216 icon
```
- Every field is optional; omitted fields keep the store's current value. `stores.` overrides the defaults; an instance such as `script.cdn` inherits the `script` override.
- Before any upload each target store's listing is validated (lengths, image format/size/bytes, screenshot count) and **all problems are reported at once** with exit code 3; `--dry-run` validates too.
- Successful results list the submitted fields in `listing` (not for `already_done` results). pgyer and fir don't support listings โ apkgo warns and uploads without it; `script` receives it as `Listing` in its stdin JSON.
- To start from what the stores already have, `apkgo listing -p --out ./listing` reads every store's current listing (read-only), saves the images under `./listing/assets` and writes `./listing/listing.yaml`, ready to edit and pass to `--listing`. Each store's values go under its own `stores:` entry with no shared defaults, so a store that can't be read is absent from the file and left unchanged on upload. OPPO, vivo and Huawei return all four fields. An image whose only problem is its format (OPPO may serve WEBP but takes only PNG/JPEG back) is converted to a format the store accepts. Honor may return reduced-size previews that fail its own upload spec; those are left out (the store keeps its originals). Tencent returns text only; Xiaomi has no query API. Existing text that wouldn't pass the store's spec is still written, with a warning in the output's `notes`.
- Verification status: listing sync has been submitted end to end with real accounts on Huawei, Honor, OPPO, vivo, Xiaomi and Tencent. HarmonyOS, Meizu, Samsung and Google Play are implemented from the official docs but not yet verified against a real account โ please open an [issue](https://github.com/KevinGong2013/apkgo/issues) if something is off.
- `apkgo stores` prints each store's authoritative `listing` spec. Summary:
| Store | Intro | Description | Icon | Screenshots |
|---|---|---|---|---|
| huawei | โค80 | โค8000 | PNG 216ร216, โค500KB (WebP โค100KB) | 9:16 (450ร800 suggested), โค2MB (WebP โค100KB), 3โ5 |
| harmony | โค80, must differ from description | โค8000 | PNG/WebP 216ร216 or 1024ร1024 | โฅ1080ร1920 at 9:16, 3โ10 |
| honor | โค80 | โค8000 | PNG/JPG 512ร512, โค200KB | 1080ร1920, โค5MB, 3โ5 |
| vivo | 5โ16 CJK chars | 50โ1000 | square PNG 256โ512px, โค500KB | 1080ร1920, โค2MB, 3โ5 |
| oppo | โค13, no punctuation/spaces | โฅ20 | PNG 512ร512, โค1MB | 1080ร1920, โค1MB, 2โ5 |
| xiaomi | โค17 CJK chars, no trailing punctuation | โ | PNG 512ร512 | 1080ร1920 or 1920ร1080 (not mixed), โค5MB, 3โ5 |
| meizu | โ | 100โ1000 | PNG/JPG | PNG/JPG, 16:9 at 720ร1280, 1012ร1800, 1080ร1920 or 1440ร2560 (either orientation), โฅ3 |
| samsung | โค40 bytes | โค4000 bytes | PNG 512ร512, โค1MB | 320โ3840px, aspect โค2:1, 4โ8 |
| tencent | 5โ15 | 60โ500 | PNG 512ร512, โค200KB | โค1MB, same size, 4โ5 |
| googleplay | โค80 | โค4000 | PNG 512ร512, โค1MB | 320โ3840px, aspect โค2:1, 2โ8 |
#### HarmonyOS (้ธฟ่) release
```bash
# Upload the signed .app pack built by DevEco Studio to AppGallery (HarmonyOS)
apkgo upload -f demo-default-signed.app -s harmony --notes "HarmonyOS 5 support"
apkgo audit -f demo-default-signed.app -s harmony # review status
apkgo doctor -f demo-default-signed.app -s harmony # credentials / app ID / release permission
```
The `harmony` store uses the AppGallery Connect HarmonyOS Publishing API and
**reuses the huawei Service Account credentials** (one AGC account covers both
Android and HarmonyOS apps): resolve the appId by bundleName
(`packageTypes=7`) โ `upload-url/for-obs` โ PUT the `.app` โ bind the package
โ wait for AGC to parse it โ submit for review. Only **`.app` packs** are
accepted (AGC rejects bare `.hap` modules). When `-f` is a HarmonyOS package
and no `-s` is given, Android stores are skipped automatically; naming one
explicitly is an error. `--release-time` is supported; `--file64` and URL
pass-through are not.
### Review status
A successful upload only means "submitted (in review)". Query the review outcome separately with `audit`:
```bash
apkgo audit -p com.example.app
apkgo audit -f app.apk -s tencent,huawei
# Poll until every store resolves (approved / rejected / withdrawn) or -t elapses
apkgo audit -p com.example.app --watch --interval 1m -t 1h
```
Supported today: huawei, harmony, honor, xiaomi, oppo, vivo, meizu, tencent, samsung.
### Initialize config
```bash
# Generate a config containing every supported store
apkgo init
# Only generate config for the stores you need
apkgo init --store huawei,xiaomi
# Custom config path
apkgo init -c production.yaml
```
### List supported stores
```bash
apkgo stores
# Print only the store names in the current config (no credentials) using the built-in YAML parser
apkgo -c apkgo.yaml stores --configured
```
### Doctor (validate store credentials)
`doctor` checks whether your configured credentials and permissions are correct **without uploading any file**, so you don't discover problems halfway through a real release:
```bash
# Validate every configured store
apkgo doctor
# Validate a specific store
apkgo doctor -s huawei
# Pass a package name for deeper checks (package mapping, release permission, ...)
apkgo doctor -s huawei -p com.example.app
apkgo doctor -s huawei -f app.apk # auto-extract package from APK
```
Exit code is 1 if any probe fails. Every store except googleplay and script is supported; unimplemented ones report `doctor not implemented`.
## Configuration file
`apkgo.yaml`:
```yaml
# Optional: how often to check for a new apkgo release, e.g. "30d", "7d"; "0" disables
# update_check: "7d"
# hooks are optional; omit if you don't need them
hooks:
before: "./scripts/validate.sh" # runs before any upload
after: "./scripts/notify.sh" # runs after all uploads
stores:
huawei:
# Recommended: service account (PS256 JWT)
service_account_file: "/secure/path/huawei-sa.json"
# or: service_account: ""
# app_id: "" # optional, auto-detected from package name when omitted
before: "./scripts/before-huawei.sh" # optional, runs before this store's upload
after: "./scripts/after-huawei.sh" # optional, runs after this store's upload
timeout: 8m # optional, per-store timeout (overrides the global --timeout)
harmony:
# HarmonyOS apps: same credentials as huawei โ reuse the same Service Account
service_account_file: "/secure/path/huawei-sa.json"
# app_id: "" # optional, resolved by bundleName (HarmonyOS apps only)
# lang: "zh-CN" # optional, release-notes language; defaults to the app's default language in AGC
# chinese_mainland_flag: "1" # optional, required by AGC when the developer is registered outside mainland China
xiaomi:
email: "your@email.com"
private_key: "your-private-key" # the value Xiaomi's console calls "interface key" (used as `password` by their SDK)
# cert_file: "/secure/path/xiaomi-pubkey.cer" # optional, defaults to built-in dev.api.public.cer (also accepts cert: or cert: )
oppo:
client_id: "your-client-id" # 19-digit number
client_secret: "your-client-secret"
vivo:
access_key: "your-access-key"
access_secret: "your-access-secret"
sandbox_access_key: "your-sandbox-access-key" # used by --sandbox
sandbox_access_secret: "your-sandbox-access-secret" # used by --sandbox
honor:
client_id: "your-client-id"
client_secret: "your-client-secret"
# app_id: "" # optional, resolved by package name when omitted
# url_push_min_mb: "100" # optional, with -f only files โฅ this size (MB) are pulled by honor from the URL; 0 = always
meizu:
client_id: "your-client-id" # Meizu open platform client credential
client_secret: "your-client-secret"
tencent:
user_id: "your-user-id"
access_secret: "your-access-secret"
app_id: "your-app-id"
# Multi-app: app_id_map: '{"com.foo":"111","com.bar":"222"}'
samsung:
service_account_id: "your-service-account-id" # Seller Portal service account ID
private_key: "-----BEGIN PRIVATE KEY-----..." # RSA private key (PEM) from Seller Portal
content_id: "your-content-id" # the app's content ID in Galaxy Store
googleplay:
json_key_file: "/secure/path/play-sa.json" # service account JSON key
package_name: "com.example.app"
# track: "production" # optional: production (default) / beta / alpha / internal
pgyer:
api_key: "your-pgyer-api-key"
fir:
api_token: "your-fir-api-token"
# Single script
script:
command: "./deploy.sh"
# Multiple script instances (script.)
script.cdn-upload:
command: "./upload-cdn.sh"
script.dingtalk:
command: "./notify-dingtalk.sh"
```
#### Hooks
Hooks are optional; if not configured they don't run. Hook scripts receive a JSON context on stdin and control the flow via exit code:
- `before` hook fails (non-zero exit) โ upload is aborted
- `after` hook fails โ logged as a warning, result is unaffected
- Auto-injected env vars: `APKGO_STORE`, `APKGO_PACKAGE`, `APKGO_VERSION`
- stderr output is captured as the error message
- Run via `sh -c` (`cmd /C` on Windows)
- `apk.platform` is `android` (APK/AAB) or `harmony` (HarmonyOS `.app`)
**Global before hook** (`hooks.before`) stdin:
```json
{
"file_path": "/path/to/app.apk",
"apk": {"platform": "android", "package": "com.example.app", "version_name": "1.0.0", "version_code": 1, "app_name": "MyApp"},
"stores": ["huawei", "xiaomi"]
}
```
**Global after hook** (`hooks.after`) stdin:
```json
{
"file_path": "/path/to/app.apk",
"apk": {"platform": "android", "package": "com.example.app", "version_name": "1.0.0", "version_code": 1, "app_name": "MyApp"},
"results": [
{"store": "huawei", "success": true, "category": "success", "duration_ms": 12300},
{"store": "xiaomi", "success": false, "error": "auth failed", "category": "auth_failed", "duration_ms": 400}
]
}
```
**Per-store before hook** (`stores..before`) stdin:
```json
{
"file_path": "/path/to/app.apk",
"apk": {"platform": "android", "package": "com.example.app", "version_name": "1.0.0", "version_code": 1, "app_name": "MyApp"},
"store": "huawei"
}
```
**Per-store after hook** (`stores..after`) stdin:
```json
{
"file_path": "/path/to/app.apk",
"apk": {"platform": "android", "package": "com.example.app", "version_name": "1.0.0", "version_code": 1, "app_name": "MyApp"},
"store": "huawei",
"result": {"store": "huawei", "success": true, "category": "success", "duration_ms": 12300}
}
```
Example hook script (DingTalk notification after upload):
```bash
#!/bin/bash
input=$(cat)
results=$(echo "$input" | jq -r '.results[] | "\(.store): \(if .success then "โ" else "โ "+.error end)"')
curl -s -X POST "$DINGTALK_WEBHOOK" \
-H "Content-Type: application/json" \
-d "{\"msgtype\":\"text\",\"text\":{\"content\":\"APK release done v${APKGO_VERSION}\n${results}\"}}"
```
### Environment variables
In CI/CD you can supply credentials purely via env vars without a config file:
```bash
# Format: APKGO__=value
export APKGO_HUAWEI_SERVICE_ACCOUNT="$(base64 -w0 huawei-sa.json)" # recommended
export APKGO_XIAOMI_EMAIL="your@email.com"
export APKGO_XIAOMI_PRIVATE_KEY="your-interface-key"
export APKGO_XIAOMI_CERT="$(base64 -w0 xiaomi-pubkey.cer)"
export APKGO_OPPO_CLIENT_ID="your-19-digit-id"
export APKGO_OPPO_CLIENT_SECRET="your-secret"
export APKGO_VIVO_ACCESS_KEY="your-key"
export APKGO_VIVO_ACCESS_SECRET="your-secret"
export APKGO_MEIZU_CLIENT_ID="your-client-id"
export APKGO_MEIZU_CLIENT_SECRET="your-secret"
export APKGO_TENCENT_USER_ID="your-user-id"
export APKGO_TENCENT_ACCESS_SECRET="your-secret"
export APKGO_TENCENT_APP_ID="your-app-id"
# Multi-app: APKGO_TENCENT_APP_ID_MAP='{"com.foo":"111","com.bar":"222"}'
export APKGO_SAMSUNG_SERVICE_ACCOUNT_ID="your-sa-id"
export APKGO_SAMSUNG_PRIVATE_KEY="$(cat samsung-private-key.pem)"
export APKGO_SAMSUNG_CONTENT_ID="your-content-id"
export APKGO_GOOGLEPLAY_JSON_KEY_FILE="/secure/path/play-sa.json"
export APKGO_GOOGLEPLAY_PACKAGE_NAME="com.example.app"
export APKGO_PGYER_API_KEY="your-pgyer-key"
export APKGO_FIR_API_TOKEN="your-fir-token"
# Env vars override fields of the same name in the config file
# A config file is not required if everything is configured through env vars
apkgo upload -f app.apk --store huawei
```
### Credentials never on disk (cloud worker / CI / multi-tenant)
`--creds-from` lets apkgo read JSON-encoded credentials from a non-disk source. An orchestrator can pull credentials from a secrets manager (Vault / AWS SM / GCP SM, etc.) and inject them straight into the child process via stdin or a file descriptor โ **never written to disk, never put into env**:
```bash
# Option A: stdin
vault read -format=json secret/apkgo | jq .data \
| apkgo upload -f app.apk --creds-from=stdin
# Option B: file descriptor (when stdin is already taken)
apkgo upload -f app.apk --creds-from=fd:3 3<<<"$(vault-creds-as-json)"
```
The JSON shape mirrors the YAML one-to-one:
```json
{
"stores": {
"huawei": {"service_account": ""},
"tencent": {
"user_id": "...",
"access_secret": "...",
"app_id_map": "{\"com.foo\":\"111\"}"
}
},
"hooks": {"before": "...", "after": "..."}
}
```
apkgo zeroes out the input bytes immediately after parsing so secrets don't linger in buffers. When `--creds-from` is set, both `--config` and `APKGO_*` env vars are ignored.
### Where to get credentials
| Store | Console | Notes |
|------|-----------|------|
| Huawei | [AppGallery Connect](https://developer.huawei.com/consumer/cn/console) | Users & permissions > Service account ([details](#huawei-appgallery-connect)) |
| HarmonyOS | [AppGallery Connect](https://developer.huawei.com/consumer/cn/console) | Same Service Account as Huawei |
| Xiaomi | [Xiaomi Open Platform](https://dev.mi.com) | Account > Interface key ([details](#xiaomi-open-platform)) |
| OPPO | [OPPO Open Platform](https://open.oppomobile.com) | Management > API key management ([details](#oppo-open-platform)) |
| vivo | [vivo Open Platform](https://dev.vivo.com.cn) | Account > API access ([details](#vivo-open-platform)) |
| Honor | [Honor Developer](https://developer.honor.com) | API management ([details](#honor-developer-platform)) |
| Meizu | [Flyme Open Platform](https://open.flyme.cn) | Console > client credentials ([details](#meizu-flyme)) |
| Tencent | [Tencent Open Platform](https://app.open.qq.com) | App > Account > API publish > Apply ([details](#tencent-app-store-yingyongbao)) |
| Samsung | [Seller Portal](https://seller.samsungapps.com) | Assistance > API Service > service account ID + private key |
| Google Play | [Play Console](https://play.google.com/console) | Service account JSON key with release permission on the app |
| Pgyer | [pgyer.com](https://www.pgyer.com/account/api) | Account > API key ([details](#pgyer)) |
| fir.im | [betaqr.com.cn](https://www.betaqr.com.cn) | Account > API Token ([details](#firim)) |
Each vendor's credential application flow is governed by their official docs (linked below). This README only describes **what is apkgo-specific**: which fields you need, how `doctor` validates them, and any non-obvious behavior.
#### Huawei AppGallery Connect
๐ Official docs: [Service Account integration](https://developer.huawei.com/consumer/cn/doc/AppGallery-connect-Guides/agcapi-getstarted-0000001111845114#section1785535363715)
Use a **developer-level** service account (PS256 JWT auth) โ not a project-level one, which would be rejected by the publish API. Hand the downloaded JSON credential straight to apkgo:
```yaml
stores:
huawei:
service_account_file: "/secure/path/huawei-sa.json"
# or inline base64(JSON): service_account: "ewogICJrZXlfaWQiOiAi..."
```
The legacy `client_id` + `client_secret` form is still accepted but deprecated by Huawei.
```bash
apkgo doctor -s huawei -p com.example.app
```
Three probes: `token` / `appid-list` (package name โ appId) / `release-permission` (app release permission).
#### Xiaomi Open Platform
๐ Official docs: [API upload](https://dev.mi.com/xiaomihyperos/documentation/detail?pId=1134)
From the Xiaomi console's "interface key" page you need the **interface key** (called `password` inside the SDK). The **public-key certificate** defaults to the built-in official Xiaomi certificate (`dev.api.public.cer`, valid through 2123), but can be customized via `cert_file` or `cert`.
```yaml
stores:
xiaomi:
email: ""
private_key: ""
# optional: defaults to built-in dev.api.public.cer
# cert_file: "/secure/path/xiaomi-pubkey.cer"
# also accepted: cert: "-----BEGIN CERTIFICATE-----..." or base64(.cer)
```
```bash
apkgo doctor -s xiaomi -p com.example.app
```
#### OPPO Open Platform
๐ Official docs: [Publish API guide](https://open.oppomobile.com/new/developmentDoc/info?id=10998)
```yaml
stores:
oppo:
client_id: "<19-digit number>"
client_secret: ""
```
```bash
apkgo doctor -s oppo -p com.example.app
```
OPPO's publish flow is asynchronous. apkgo handles two non-obvious states automatically: on `911216 task processing`, it skips the publish call and waits for the task to settle; on `911215 app under review`, it treats the result as success (already in the review queue).
#### vivo Open Platform
๐ Official docs: [Open API guide](https://dev.vivo.com.cn/documentCenter/doc/326)
```yaml
stores:
vivo:
access_key: "<...>"
access_secret: "<...>"
sandbox_access_key: "<...>"
sandbox_access_secret: "<...>"
```
```bash
apkgo doctor -s vivo -p com.example.app
```
vivo's error codes come in two layers: gateway `code` plus business `subCode`. apkgo recognizes both layers and surfaces the original message verbatim (e.g. `[15042] please upload an APK signed with the same signature as before...`).
For a sandbox upload, create the app in vivo's isolated sandbox first, configure the separate sandbox credentials above, then run `apkgo upload -f app.apk --sandbox`. Production credentials are not used by that command.
#### Honor Developer Platform
๐ Official docs: [Publish API guide](https://developer.honor.com/cn/doc/guides/101159)
```yaml
stores:
honor:
client_id: "<...>"
client_secret: "<...>"
# app_id: "" # optional; resolved by package name when omitted
```
```bash
apkgo doctor -s honor -p com.example.app
```
The doctor `app-detail` probe pre-checks the *app introduction* (intro) field โ Honor requires it to be filled in the console, otherwise `update-language-info` fails with `[20076] app introduction is empty`. Fill it in the console before releasing.
#### Meizu (Flyme)
๐ Official docs: [Open publish API](https://open.flyme.cn/docs?id=333)
```yaml
stores:
meizu:
client_id: ""
client_secret: ""
```
```bash
apkgo doctor -s meizu -p com.example.app
```
Uploads submit a new version of an app that is already listed โ the first release (qualifications, ICP filing, screenshots) must be done in the Flyme console. Meizu accepts 64-bit packages only; a split-arch upload (`--file64`) sends the 64-bit APK. If the latest version was rejected (ๅฎกๆ ธไธ้่ฟ), apkgo automatically resubmits through the fail-app update endpoint instead of the regular publish one. Every version needs an icon and screenshots: a missing icon is taken from the APK, but when Meizu has no screenshots on record for the app the upload stops before sending the APK โ supply at least 3 with `--listing`, or add them in the console.
#### Tencent App Store (YingYongBao)
๐ Official docs: [API upload integration](https://wikinew.open.qq.com/index.html#/iwiki/4015262492)
Tencent provides no list endpoint and no packageโid reverse lookup, so `app_id` must be supplied explicitly. To serve multiple apps from one yaml, use `app_id_map`:
```yaml
stores:
tencent:
user_id: ""
access_secret: ""
# Single app:
app_id: ""
# Multi-app: matched by APK package name
# app_id_map: '{"com.example.foo":"111","com.example.bar":"222"}'
```
```bash
apkgo doctor -s tencent -p com.example.app
```
Publish is asynchronous. apkgo polls `query_app_update_status` until `audit_status` reaches a terminal state (max 5 minutes); a timeout is treated as success (the task has been handed off to Tencent).
#### Pgyer
๐ Official docs: [API upload](https://www.pgyer.com/doc/view/app_upload)
```yaml
stores:
pgyer:
api_key: "<...>"
```
```bash
apkgo doctor -s pgyer -p com.example.app
```
#### fir.im
๐ Official docs: [betaqr.com.cn/docs](https://www.betaqr.com.cn/docs)
```yaml
stores:
fir:
api_token: "<...>"
```
```bash
apkgo doctor -s fir
```
> โ ๏ธ **fir uploads require a real-name verified account**. Without it, the `/apps` endpoint refuses with `cannot upload app without real-name verification`. Complete real-name verification in the console first.
## AI agent integration
apkgo's output format is designed for AI agents and automation:
**Structured JSON output** (stdout):
```json
{
"apk": {"package": "com.example.app", "version_name": "1.0.0", "version_code": 1},
"results": [
{"store": "huawei", "success": true, "category": "success", "duration_ms": 12300},
{"store": "oppo", "success": true, "category": "already_done", "duration_ms": 3200},
{"store": "xiaomi", "success": false, "category": "policy_block", "error": "signature mismatch...", "duration_ms": 400}
]
}
```
**Category (retry-decision hints)**: every result carries a `category` field that buckets each vendor's wildly different error codes into a small set friendly to a cloud orchestrator, so the parent process doesn't have to parse Chinese error messages. Possible values:
| Category | Meaning | Suggested handling |
|---|---|---|
| `success` | Upload succeeded | mark done |
| `already_done` | Version is already on the store side (e.g. OPPO 911215 under review) | mark done, do not retry |
| `auth_failed` | Bad credentials | ask user to fix the secret, do not retry |
| `network_retry` | Network timeout / 5xx | retry with backoff |
| `store_busy` | Store rate-limit / previous task still running | wait a few minutes and retry |
| `policy_block` | Business rule rejection (signature mismatch, review denied, ...) | hand to user, do not retry |
| `config_invalid` | Backend metadata missing (intro / category / publisher entity) | ask user to fill it in the console, do not retry |
| `unknown` | Not yet classified | default to "non-retryable" |
**Semantic exit codes**:
| Code | Meaning |
|------|------|
| 0 | All succeeded |
| 1 | Partial failure |
| 2 | All failed |
| 3 | Input error |
**Discoverable config schema**:
```bash
apkgo stores # returns each store's required config fields
```
**Non-interactive**: no prompts, no confirmations โ fits headless environments.
**Live progress stream (NDJSON)**: when a parent process forks apkgo and wants real-time progress, add `--progress-stream` and stdout becomes one JSON event per line:
```bash
apkgo upload -f app.apk --progress-stream
```
```json
{"type":"start","apk":{"package":"...","version_name":"1.2.0","version_code":120},"stores":["huawei","xiaomi"]}
{"type":"phase","store":"huawei","phase":"auth"}
{"type":"phase","store":"huawei","phase":"uploading"}
{"type":"total","store":"huawei","total_bytes":62914560}
{"type":"bytes","store":"huawei","sent":7045120,"total":62914560}
{"type":"bytes","store":"huawei","sent":23560192,"total":62914560}
{"type":"phase","store":"huawei","phase":"submitting"}
{"type":"result","store":"huawei","success":true,"duration_ms":34570}
{"type":"done","apk":{...},"results":[...]}
```
`bytes` events are throttled to roughly one every ~100ms; concurrent stores each get their own stream, distinguished by the `store` field. Go parent-process consumer:
```go
cmd := exec.CommandContext(ctx, "apkgo", "upload", "-f", apkPath, "--progress-stream")
out, _ := cmd.StdoutPipe()
cmd.Start()
sc := bufio.NewScanner(out)
for sc.Scan() {
var evt map[string]any
json.Unmarshal(sc.Bytes(), &evt)
switch evt["type"] {
case "bytes":
ui.UpdateProgress(evt["store"].(string), evt["sent"].(float64), evt["total"].(float64))
case "result":
ui.MarkStoreDone(evt["store"].(string), evt["success"].(bool))
case "done":
ui.Finish(evt["results"])
}
}
cmd.Wait()
```
## Embed in Go (SDK)
apkgo is both a CLI **and** a Go library. Cloud workers, custom CI tooling, and IDE plugins can import `pkg/apkgo` directly to drive the upload pipeline, skipping the spawn-subprocess + parse-stdout dance.
```go
import (
"context"
"github.com/KevinGong2013/apkgo/pkg/apkgo"
"github.com/KevinGong2013/apkgo/pkg/config"
"github.com/KevinGong2013/apkgo/pkg/uploader"
)
cfg := &config.Config{
Stores: map[string]map[string]string{
"huawei": {"service_account": vault.Get("huawei-sa-base64")},
"tencent": {
"user_id": "...",
"access_secret": vault.Get("tencent-secret"),
"app_id_map": `{"com.foo":"111","com.bar":"222"}`,
},
},
}
result, err := apkgo.Run(ctx, apkgo.Job{
APKFile: "https://artifacts.example.com/v1.2.0.apk",
Stores: []string{"huawei", "tencent"},
Notes: "Bug fixes",
Config: cfg,
Progress: uploader.NopManager, // or NewNDJSONManager(w) for streamed events
})
```
Features:
- **Zero global state**: `apkgo.Run` does not touch `slog.Default` or set the process exit code, so it's safe to call from a long-running process.
- **URL inputs**: `APKFile` / `APKFile64` accept a local path or http(s) URL (auto-fetched to a temp file, cleaned up on exit).
- **Pluggable progress**: the `uploader.ProgressManager` interface lets you plug in mpb / NDJSON / a custom implementation.
- **`error` only for pre-upload failures**: the `error` returned by `Run` only covers fetch / parse / config phases; once uploading begins, each store's failure lives in `Result.Results[i].Error`.
- **Cloud-worker-friendly extras**:
```go
apkgo.Run(ctx, apkgo.Job{
// ... config / file / stores / etc
// 1. Per-job logger (cloud injects job_id / tenant_id / trace_id)
Logger: slog.New(handler).With("job_id", id, "tenant", tid),
// 2. Metric emit hook (store.start / store.end / hook.run)
Events: func(ev uploader.Event) {
if ev.Type == uploader.EventStoreEnd {
prom.UploadCounter.WithLabelValues(ev.Store, string(ev.Result.Category)).Inc()
prom.UploadDuration.WithLabelValues(ev.Store).Observe(ev.Duration.Seconds())
}
},
})
```
Each store can also have its own timeout in yaml (overriding the global `--timeout`):
```yaml
stores:
huawei:
service_account_file: "..."
timeout: 8m # waiting on submit polling โ be generous
pgyer:
api_key: "..."
timeout: 30s # simple upload โ keep tight
```
For the full API, see the godoc for [`pkg/apkgo`](pkg/apkgo).
## CI/CD examples
### GitHub Actions
```yaml
- name: Upload to app stores
env:
APKGO_HUAWEI_SERVICE_ACCOUNT: ${{ secrets.HUAWEI_SERVICE_ACCOUNT }} # base64(JSON credential)
APKGO_XIAOMI_EMAIL: ${{ secrets.XIAOMI_EMAIL }}
APKGO_XIAOMI_PRIVATE_KEY: ${{ secrets.XIAOMI_PRIVATE_KEY }}
APKGO_XIAOMI_CERT: ${{ secrets.XIAOMI_CERT }} # base64(.cer file)
APKGO_OPPO_CLIENT_ID: ${{ secrets.OPPO_CLIENT_ID }}
APKGO_OPPO_CLIENT_SECRET: ${{ secrets.OPPO_CLIENT_SECRET }}
APKGO_VIVO_ACCESS_KEY: ${{ secrets.VIVO_ACCESS_KEY }}
APKGO_VIVO_ACCESS_SECRET: ${{ secrets.VIVO_ACCESS_SECRET }}
APKGO_TENCENT_USER_ID: ${{ secrets.TENCENT_USER_ID }}
APKGO_TENCENT_ACCESS_SECRET: ${{ secrets.TENCENT_ACCESS_SECRET }}
APKGO_TENCENT_APP_ID_MAP: ${{ secrets.TENCENT_APP_ID_MAP }} # multi-app: '{"com.foo":"111",...}'
run: |
apkgo upload \
-f app/build/outputs/apk/release/app-release.apk \
--notes-file CHANGELOG.md \
--store huawei,xiaomi,oppo,vivo,tencent \
--timeout 15m
```
### Docker
```bash
docker run --rm \
-v $(pwd)/apkgo.yaml:/apkgo.yaml \
-v $(pwd)/app.apk:/app.apk \
ghcr.io/kevingong2013/apkgo:latest \
upload -f /app.apk --notes "Bug fixes"
```
## Sync config across machines
Encrypted export/import for safely sharing store credentials between machines or with CI:
```bash
# Machine A: encrypted export
apkgo config export --out config.enc
# Enter a password (or set the APKGO_CONFIG_KEY env var)
# Push to a private repo
git add config.enc && git commit -m "sync config" && git push
# Machine B: decrypted import
git pull
apkgo config import config.enc
```
Non-interactive use in CI:
```yaml
- name: Import apkgo config
env:
APKGO_CONFIG_KEY: ${{ secrets.APKGO_CONFIG_KEY }}
run: apkgo config import config.enc
```
Encryption: AES-256-GCM with scrypt key derivation; a wrong password produces a clear error.
## All commands
```
apkgo init [-s store1,store2] [-c config.yaml]
apkgo upload -f [--file64 ] [-s stores] [-n notes] [--notes-file path]
[--release-time ] [--listing ] [--fetch-header "Name: value"] [--dry-run | --sandbox]
[--progress-stream] [-t timeout]
apkgo audit [-s stores] (-f | -p ) [--watch] [--interval 30s]
apkgo listing [-s stores] (-f | -p ) [--out ]
apkgo doctor [-s stores] [-f | -p ]
apkgo config export --out
apkgo config import
apkgo stores [--configured] [-o json|text]
apkgo history [-n limit]
apkgo upgrade
apkgo version [-o json|text]
```
## Global flags
```
-c, --config config file path (default: apkgo.yaml)
--creds-from read JSON credentials from stdin or fd:N (overrides --config)
-o, --output output format: json or text (default: json)
-t, --timeout global timeout (default: 10m)
-v, --verbose verbose logs to stderr
--http-trace record every HTTP request/response with the stores to this file (env: APKGO_HTTP_TRACE)
```
### Troubleshooting: recording the exchanges with the stores
When a publish doesn't turn out as expected, run it again with `--http-trace trace.jsonl` (works with `upload` / `audit` / `doctor` / `listing`). apkgo appends every request to and response from the stores to that file, one JSON object per line: method, URL, request headers and body, status, response headers and body, duration, transport errors.
- Credentials are masked: `client_secret`, `access_token`, `sign`, `Authorization`, the signatures in signed URLs and the like are written as `[REDACTED ]` (an empty value stays empty, so "the signature was empty" remains visible). Masking goes by field name and account identifiers (client ids, the Xiaomi account e-mail) are left as they are, so still treat the file as sensitive and keep it on your machine.
- File contents are never recorded: an APK or image upload is recorded as its form fields plus each file's field name, file name and size.
- Text request and response bodies are kept up to 256 KB.
- Off by default; without the flag nothing changes.
## License
[PolyForm Noncommercial License 1.0.0](./LICENSE) โ free for noncommercial use; contact the author for commercial licensing.