Pgyer Docs
CI/CD & Dev Tools

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.

app/build.gradle.kts
plugins {
    id("com.android.application")
    id("com.pgyer.upload") version "1.0.0"
}

pgyer {
    updateDescription = "Fixed the login crash"
}
app/build.gradle
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:

build.gradle.kts
plugins {
    id("com.android.application") version "8.13.2" apply false
    id("com.pgyer.upload") version "1.0.0" apply false
}
app/build.gradle.kts
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.properties in 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_KEY environment 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 uploadPgyerRelease

The 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:

build.gradle.kts
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")
}
build.gradle
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.

OptionTypeDescription
apiKeyStringYour Pgyer API key. Required. Defaults to the Gradle property pgyer.apiKey, then the PGYER_API_KEY environment variable
updateDescriptionStringRelease notes for this build, shown on the download page
descriptionStringThe app description. On a task it's called appDescription
installTypeStringWho can install: 1 or public, 2 or password, 3 or invite. When only password is set, password install is used
passwordStringThe install password
installStartDate, installEndDateStringThe install period, as yyyy-MM-dd. Set both
channelShortcutStringThe short link of the download channel to update
overseaBooleantrue stores the build overseas, false in mainland China. When unset, Pgyer chooses by the uploading machine's IP address
processingTimeoutIntSeconds to wait for Pgyer to process the build after the upload. Defaults to 600
downloadFileNameStringThe 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:

app/build.gradle.kts
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:

FieldDescription
appPgyerURLThe app's download page
appBuildURLThe page of this exact build
appQRCodeURLThe 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.json

Use 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.json

Add PGYER_API_KEY as a masked variable under Settings → CI/CD → Variables, then run this in a job:

./gradlew :app:uploadPgyerRelease

Store 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="...":

app/build.gradle.kts
pgyer {
    updateDescription = providers.gradleProperty("pgyerNotes").orElse("Built by CI")
}

Troubleshooting

MessageWhat 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 errorsThe 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 secondsThe 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 reachedThe 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 APKsThe 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 Plugincom.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 DSLGradle 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.

On this page