# apkgo ![GitHub release](https://img.shields.io/github/v/release/KevinGong2013/apkgo) ![build](https://img.shields.io/github/actions/workflow/status/kevingong2013/apkgo/release.yml?style=flat-square) ![go](https://img.shields.io/github/go-mod/go-version/kevingong2013/apkgo?style=flat-square) [![license](https://img.shields.io/badge/license-PolyForm--Noncommercial--1.0.0-blue?style=flat-square)](./LICENSE) [![skills.sh](https://skills.sh/b/KevinGong2013/apkgo)](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.