Upload via Gradle
Install and configure Pgyer's official Gradle plugin to upload Android APKs, and iOS and HarmonyOS builds, to Pgyer from your Gradle build.
Pgyer's official Gradle plugin (plugin ID com.pgyer.upload) uploads apps to Pgyer straight from your Gradle build and returns the download page and QR code. It creates an upload task for each Android build variant, which builds the APK first. It can also upload existing .apk, .ipa, and HarmonyOS .hap files. The plugin works on your machine and in CI/CD. The source is on GitHub.
For help choosing between the API, CLI, CI/CD plugins, MCP, and Agent Skill, see Automated App Uploads and CI/CD Integration.
Prerequisites
- Gradle 8.0 or later, running on Java 17 or later.
- For the Android variant tasks, Android Gradle Plugin (AGP) 8.0 or later.
- Your Pgyer API key, shown on the API information page.
Add the Plugin
Add the plugin to your Android app module's build script. The latest version is on the Gradle Plugin Portal.
plugins {
id("com.android.application")
id("com.pgyer.upload") version "1.0.0"
}
pgyer {
updateDescription = "Fixed the login crash"
}plugins {
id 'com.android.application'
id 'com.pgyer.upload' version '1.0.0'
}
pgyer {
updateDescription = 'Fixed the login crash'
}Declaring Plugin Versions in the Root Project
If your root build script declares plugin versions with apply false (common in projects with a libs.versions.toml version catalog), declare com.pgyer.upload there too, next to com.android.application, and apply it in the app module without a version:
plugins {
id("com.android.application") version "8.13.2" apply false
id("com.pgyer.upload") version "1.0.0" apply false
}plugins {
id("com.android.application")
id("com.pgyer.upload")
}With a version catalog, add pgyer = { id = "com.pgyer.upload", version = "1.0.0" } under [plugins] in gradle/libs.versions.toml, then write alias(libs.plugins.pgyer) apply false in the root project and alias(libs.plugins.pgyer) in the app module.
Don't declare com.pgyer.upload only in the root project while AGP is declared only in a subproject. Declare both in the same build script, or the plugin can't see the Android build variants.
Store the API Key
The plugin reads the API key from, in order: pgyer { apiKey = ... }, the Gradle property pgyer.apiKey, and the PGYER_API_KEY environment variable.
-
On your machine: put it in
~/.gradle/gradle.propertiesin your home directory. That file isn't part of the project, so it never gets committed:~/.gradle/gradle.properties pgyer.apiKey=your_api_key -
In CI/CD: store the API key as a secret and pass it to the build as the
PGYER_API_KEYenvironment variable.
Never put the API key in the project's build.gradle(.kts) or gradle.properties and commit it. The plugin never logs the API key or the install password.
Build and Upload
With com.android.application applied, the plugin creates one upload task per build variant. The task name is uploadPgyer plus the capitalized variant name, for example uploadPgyerRelease, uploadPgyerDebug, or uploadPgyerFreeRelease. Run ./gradlew tasks --group pgyer to list them.
In your project directory, run:
./gradlew uploadPgyerReleaseThe task builds the variant's APK, uploads it to Pgyer, and waits until Pgyer has processed it. The output looks like this:
> Task :app:uploadPgyerRelease
[PGYER] Uploading app-release.apk (12.4 MB)...
[PGYER] Uploaded 10% (1.2 MB)
...
[PGYER] Upload finished. Waiting for PGYER to process the build...
[PGYER] Published PGYER Demo 1.0.0 (build 1)
[PGYER] Download page: https://www.pgyer.com/pgyerdemo
[PGYER] QR code: https://www.pgyer.com/app/qrcodeHistory/...The variant tasks upload APKs, not App Bundles (.aab). If a variant uses ABI or density splits, the task uploads the universal APK. Turn it on with splits { abi { isUniversalApk = true } }, or upload one of the APKs with the PgyerUploadTask described below.
Upload iOS, HarmonyOS, or Existing Builds
PgyerUploadTask uploads any .apk, .ipa, or .hap file, and takes the build type from the file extension. It doesn't need AGP, so you can use it for Kotlin Multiplatform iOS builds, HarmonyOS apps, and prebuilt files:
tasks.register<com.pgyer.upload.PgyerUploadTask>("uploadIpa") {
file = layout.buildDirectory.file("ios/MyApp.ipa")
updateDescription = "iOS beta"
}
tasks.register<com.pgyer.upload.PgyerUploadTask>("uploadHap") {
file = layout.projectDirectory.file("entry/build/default/outputs/default/entry-default-signed.hap")
}tasks.register('uploadIpa', com.pgyer.upload.PgyerUploadTask) {
file = layout.buildDirectory.file('ios/MyApp.ipa')
dependsOn 'buildIpa' // the task that produces the file
}If file comes from another task's output, Gradle runs that task first. Settings you don't set on the task come from the pgyer { } block.
Common Options
Set these in pgyer { }, for one variant, or on a task. All of them take lazy values too, such as providers.environmentVariable("NOTES"). Settings you don't set are not sent, so Pgyer keeps the app's current settings for them.
| Option | Type | Description |
|---|---|---|
apiKey | String | Your Pgyer API key. Required. Defaults to the Gradle property pgyer.apiKey, then the PGYER_API_KEY environment variable |
updateDescription | String | Release notes for this build, shown on the download page |
description | String | The app description. On a task it's called appDescription |
installType | String | Who can install: 1 or public, 2 or password, 3 or invite. When only password is set, password install is used |
password | String | The install password |
installStartDate, installEndDate | String | The install period, as yyyy-MM-dd. Set both |
channelShortcut | String | The short link of the download channel to update |
oversea | Boolean | true stores the build overseas, false in mainland China. When unset, Pgyer chooses by the uploading machine's IP address |
processingTimeout | Int | Seconds to wait for Pgyer to process the build after the upload. Defaults to 600 |
downloadFileName | String | The file name testers get when they download, including the extension. Plain ASCII only. Defaults to the uploaded file's name |
See the plugin README for all options.
Per-Variant Settings
variantFilter chooses which variants get an upload task, and variants sets options for one variant:
pgyer {
updateDescription = "Weekly beta"
// Only release builds get an upload task.
variantFilter { it.buildType == "release" }
variants {
create("paidRelease") {
enabled = false
}
create("freeRelease") {
channelShortcut = "free-beta"
installType = "password"
password = providers.environmentVariable("FREE_BETA_PASSWORD")
}
}
}In the Groovy DSL, a variant block is just its name: variants { freeRelease { channelShortcut = 'free-beta' } }. If a name doesn't match any variant, the plugin prints a warning that lists the variants that exist.
Use the Upload Result
After a successful upload, the build information is written as JSON to build/outputs/pgyer/<variant>.json in the app module (build/outputs/pgyer/<task name>.json for a PgyerUploadTask). The file has every field that buildInfo returns, plus these links:
| Field | Description |
|---|---|
appPgyerURL | The app's download page |
appBuildURL | The page of this exact build |
appQRCodeURL | The QR code image for the download page |
For example, to read the download page in a later step:
jq -r .appPgyerURL app/build/outputs/pgyer/release.jsonUse in CI/CD
Keep the API key in your CI/CD secret store and pass it to the upload step as the PGYER_API_KEY environment variable.
- name: Upload to Pgyer
run: ./gradlew :app:uploadPgyerRelease
env:
PGYER_API_KEY: ${{ secrets.PGYER_API_KEY }}
- name: Show the download page
run: jq -r .appPgyerURL app/build/outputs/pgyer/release.jsonAdd PGYER_API_KEY as a masked variable under Settings → CI/CD → Variables, then run this in a job:
./gradlew :app:uploadPgyerReleaseStore the API key as a Secret text credential, then run this in a Pipeline:
withCredentials([string(credentialsId: 'pgyer-api-key', variable: 'PGYER_API_KEY')]) {
sh './gradlew :app:uploadPgyerRelease'
}You can also upload with the Jenkins plugin.
To use the commit message as release notes, read a Gradle property lazily in the build script and pass it on the command line, for example ./gradlew :app:uploadPgyerRelease -PpgyerNotes="...":
pgyer {
updateDescription = providers.gradleProperty("pgyerNotes").orElse("Built by CI")
}Troubleshooting
| Message | What to do |
|---|---|
No PGYER API key. | Set the PGYER_API_KEY environment variable, or pgyer.apiKey in ~/.gradle/gradle.properties |
PGYER API error 1001, 1002, and other API errors | The log shows Pgyer's error code and what to do, for example for a wrong API key, a sub-account (which can't use the API), missing real-name verification, or full storage |
PGYER is still processing the build after 600 seconds | The upload finished, and the build may still show up in your app list. For large builds, raise processingTimeout |
None of the PGYER API servers could be reached | The plugin tries www.pgyer.com, www.xcxwo.com, and www.pgyerapp.com in turn. Check your network and proxy. To use a proxy, set systemProp.https.proxyHost and systemProp.https.proxyPort in gradle.properties |
Cannot tell the app type of ... | Only .apk, .ipa, and .hap files can be uploaded |
The <variant> variant produces N APKs | The variant uses splits. Turn on a universal APK, or upload one APK with a PgyerUploadTask |
The com.pgyer.upload plugin cannot see the classes of the Android Gradle Plugin | com.pgyer.upload and AGP are declared in different build scripts. See Declaring Plugin Versions in the Root Project |
Unresolved reference or a type mismatch on apiKey = "..." in the Kotlin DSL | Gradle 8.0 and 8.1 don't support = for lazy properties. Use apiKey.set("...") or upgrade Gradle |
Add --info to see the settings sent (with secrets masked) and each processing check, and --stacktrace to see the cause of a failure.