A comprehensive Gradle plugin suite for automating Android build publishing workflows. This plugin provides:
- Version management through Git tags
- Automated changelog generation
- Firebase App Distribution
- Google Play Store publishing
- Jira integration
- Telegram notifications
- Custom plugin support
This plugin suite is designed to be "build friendly" and behave well in CI/CD environments.
-
Lazy configuration (Providers / configuration avoidance)
- Most values are modeled via Gradle
Property/ProviderAPIs and are resolved late. - Tasks are registered using Gradle’s configuration avoidance APIs (so they don’t get realized unless needed).
- Most values are modeled via Gradle
-
Conditional task creation
- Many tasks are registered only when the corresponding configuration is present.
- Examples:
- Jira:
jiraAutomation<Variant>is created only if at least one automation action is enabled. - ClickUp:
clickUpAutomation<Variant>is created only if tag/fixVersion automation is enabled. - Slack/Telegram: distribution tasks are skipped when destinations are not configured.
- Jira:
-
No network calls at configuration time
- Network operations are executed only during task execution (not during Gradle configuration).
-
Worker API for heavy work
- Network uploads and message sending are delegated to Gradle Worker API work actions where applicable.
- This keeps task execution responsive and avoids blocking the main build thread.
-
Shared services for network clients
- External integrations (Slack/Telegram/Jira/Confluence/ClickUp/etc.) use Gradle Shared Build Services.
- This avoids re-creating HTTP clients for each task and improves stability/throughput.
-
Variant-aware wiring via Android Components
- Tasks are wired per Android build variant, with a predictable naming scheme and clear dependencies.
- Tag-based automation (core concepts)
- Migration to build-publish-novo
- Installation
- Examples
- Available Plugins
- Custom Plugin Development
- Troubleshooting
- Contributing
- License
The core idea behind this plugin suite is tag-based automation.
Instead of storing version/build metadata in Gradle properties or CI variables, the plugin uses Git tags as the single source of truth for:
- Build number (used as
versionCodeby default) - Build version (used as
versionNameby default) - Changelog generation window (diff between the last matching tag and HEAD)
-
Deterministic and reproducible
- Tags are part of Git history and can be fetched in any environment (
git fetch --tags).
- Tags are part of Git history and can be fetched in any environment (
-
Decoupled from branches and CI
- The same commit always has the same tag metadata, regardless of which CI system runs it.
-
Flexible automation foundation
- Multiple plugins (Jira/ClickUp/Slack/Telegram/Play/etc.) can rely on the same tag snapshot and changelog, which keeps automation consistent.
The parser treats the last numeric part before -<variant> as the build number, and the preceding numeric parts
as the build version.
Examples:
v1.0.100-debug -> buildVersion = 1.0, buildNumber = 100
v1.2.3.42-release -> buildVersion = 1.2.3, buildNumber = 42
app.2024.15-staging -> buildVersion = 2024, buildNumber = 15
By default, the foundation plugin uses this regex template:
DEFAULT_TAG_PATTERN = ".+\\.(\\d+)-%s"
Where %s is replaced with the Android build variant name.
This means:
- Tags must include the build variant suffix (e.g.
-debug,-release). - Tags must contain at least one numeric group.
Tag matching is variant-aware: each Android variant has its own tag stream because %s is replaced with the
variant name.
This enables a workflow where you can tag the same commit multiple times — once per build type/flavor:
v1.2.3.10-debug
v1.2.3.10-release
Both tags can point to the same commit SHA and are still treated as independent streams, because each variant uses its own pattern and selection.
For each variant, the foundation plugin computes a regex from buildTagPattern (or DEFAULT_TAG_PATTERN) and then:
- lists all Git tags
- filters tags by the regex
- sorts tags primarily by commit order/time and then by extracted build number
- picks:
current: the first tag in the sorted listpreviousInOrder: the second tag in the list (if present)previousOnDifferentCommit: the first tag that points to a different commit (useful when multiple tags point to the same commit)
These values are stored in the tag snapshot JSON and reused by other tasks.
Changelog generation uses previousOnDifferentCommit (exposed as snapshot.previous) as the start of the commit
range, not previousInOrder.
Reason:
- It is valid to have multiple tags pointing to the same commit for the same variant (for example, you restart a CI build or re-run a release job and create a new tag without any new commits).
- In that situation,
previousInOrdermay point to a tag on the same commit, and using it as a range start would produce an empty/duplicate changelog.
By selecting the previous tag on a different commit, the changelog reflects the actual changes since the last code change, while still allowing tag messages/metadata to be attached to the current build.
By default, buildNumber extracted from the tag is used as versionCode.
To keep versioning stable and monotonic:
-
Build numbers must be positive
- The plugin treats
0and negative build numbers as invalid.
- The plugin treats
-
Build numbers must increase within the same variant tag stream
- The tag selection logic validates the last tags to ensure build numbers and commit chronology are consistent.
- If the plugin detects that a “newer” tag has a build number that is not greater than the previous one, it fails with a detailed Gradle error.
This is one of the reasons tags are used as a core automation primitive: they provide a single, auditable, monotonically-increasing sequence per variant.
Configure buildPublishFoundation.output.common.buildTagPattern { ... } to match your tag naming convention.
Kotlin DSL (build.gradle.kts):
buildPublishFoundation {
output {
common {
buildTagPattern {
literal("v")
separator(".")
buildVersion()
separator("-")
buildVariantName()
}
}
}
}Groovy DSL (build.gradle):
buildPublishFoundation {
output {
common {
it.buildTagPattern {
literal('v')
separator('.')
buildVersion()
separator('-')
buildVariantName()
}
}
}
}- Foundation produces a tag snapshot via
getLastTagSnapshot<Variant>. - Other tasks/plugins read that snapshot to:
- compute
versionName/versionCode - generate a changelog (
generateChangelog<Variant>) - attach version info to uploads / notifications
- compute
If no matching tag is found, the foundation plugin can fall back to stub/default values.
This is controlled by output.useStubsForTagAsFallback and output.useDefaultsForVersionsAsFallback.
If you are migrating from an older/legacy version of this plugin suite to the *-novo line, treat it as a
breaking change and do a quick audit of plugin IDs, dependencies, and your tag/versioning setup.
High-level changes introduced in the novo line:
- The plugin is now modular: each integration is a separate Gradle plugin (
foundation,slack,telegram,jira,confluence,clickup,play,firebase). - Common logic is extracted into a shared core library (
ru.kode.android:build-publish-novo-core). - Tag-based automation is variant-aware by default and validates tag ordering and build numbers.
- AppCenter integration was removed (if you used it previously, delete related configuration/tasks and replace with another distribution channel).
Update all plugin IDs to the ru.kode.android.build-publish-novo.* namespace.
Recommendation:
- Search your build logic for
build-publishand update IDs/artifacts accordingly.
If you apply Build Publish plugins from a convention module (build-logic / build-conventions), make sure you use
the novo artifacts, for example:
ru.kode.android:build-publish-novo-core:...ru.kode.android.build-publish-novo.<plugin>:ru.kode.android.build-publish-novo.<plugin>.gradle.plugin:...
Also note that there is no single “all-in-one” plugin anymore: if your old setup had one plugin that configured
multiple integrations, you now add/apply the exact set of novo plugins you need.
The configuration is split into per-plugin extensions.
- Old setup (legacy): typically one root extension or a combined configuration block.
- New setup (novo): configure each integration via its own extension:
buildPublishFoundation { ... }buildPublishSlack { ... }buildPublishTelegram { ... }buildPublishJira { ... }buildPublishConfluence { ... }buildPublishClickUp { ... }buildPublishPlay { ... }buildPublishFirebase { ... }
All other plugins rely on the foundation plugin to run variant configuration. Make sure it is applied in every Android application module that uses any publishing/integration plugin:
ru.kode.android.build-publish-novo.foundation
The novo line is strongly built around tag-based automation.
Before running CI, verify that tags exist and match your variant(s):
./gradlew getLastTagSnapshotRelease
./gradlew printLastIncreasedTagReleaseBreaking change note: in the novo line the legacy getLastTag<Variant> task is renamed to
getLastTagSnapshot<Variant>.
Important behavior changes to account for:
- Build numbers must be positive (
0and negative values are treated as invalid). - Build numbers must increase within the same variant tag stream.
- Tag selection is variant-aware (tags typically end with
-debug,-release, etc.).
What the foundation plugin does per variant:
- resolves a tag regex from
buildTagPattern(or the default) - picks the latest matching tag and writes a JSON snapshot file
- downstream tasks use that snapshot for
versionCode/versionName/ changelog / uploads
If your previous setup used a different tag naming scheme, configure buildPublishFoundation.output.common.buildTagPattern { ... }.
In the novo line, the default versionName is derived from the parsed tag build version only
(for example 1.2 or 1.2.3).
If in the legacy version you relied on versionName including the build number from the tag, configure a different
strategy explicitly, for example BuildVersionNumberNameStrategy.
Kotlin DSL (build.gradle.kts):
import ru.kode.android.build.publish.plugin.core.strategy.BuildVersionNumberNameStrategy
buildPublishFoundation {
output {
buildVariant("internal") {
baseFileName = "android"
versionNameStrategy { BuildVersionNumberNameStrategy }
}
}
}Groovy DSL (build.gradle): Kotlin object strategies are referenced via INSTANCE (no new), while class strategies must be instantiated (use new ...()):
import ru.kode.android.build.publish.plugin.core.strategy.BuildVersionNumberNameStrategy
buildPublishFoundation {
output {
buildVariant('internal') {
baseFileName = 'android'
versionNameStrategy { BuildVersionNumberNameStrategy.INSTANCE }
}
}
}If you configure this from a convention module (build-logic / build-conventions), make sure the module has access
to the core classes by adding ru.kode.android:build-publish-novo-core to its dependencies (see Installation section).
When migrating, re-check all credentials configuration:
- Prefer CI/CD secret variables or
local.propertiesfor local development. - For string secrets, use
providers.environmentVariable("..."). - For some file-based secrets used by Worker API / shared services, resolve file paths eagerly (see the secrets section).
If your legacy setup relied on checked-in secret files, migrate them to CI secret variables. For GitHub Actions, store file content as base64 in a secret and decode it in a pre-step (see the secrets section).
This repository publishes multiple Gradle plugins. The published plugin IDs follow the pattern:
ru.kode.android.build-publish-novo.<plugin>
For example:
ru.kode.android.build-publish-novo.foundationru.kode.android.build-publish-novo.firebaseru.kode.android.build-publish-novo.play
In settings.gradle.kts make sure you have a plugin repository that contains the plugin artifacts.
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
// If you publish to a private Maven repository, add it here.
// maven("https://your-maven-repo.com")
}
}In settings.gradle (Groovy DSL) the equivalent looks like:
// settings.gradle
pluginManagement {
repositories {
gradlePluginPortal()
google()
mavenCentral()
// If you publish to a private Maven repository, add it here.
// maven { url 'https://your-maven-repo.com' }
}
}Apply plugins in the Android application module (the foundation plugin fails fast for library modules and unsupported AGP versions).
// app/build.gradle.kts
plugins {
id("com.android.application")
id("ru.kode.android.build-publish-novo.foundation") version "x.y.z"
}If you use the Version Catalog (libs.versions.toml), you can declare plugin aliases and then apply them
in the plugins { ... } block.
Add plugin aliases to libs.versions.toml:
[plugins]
buildpublish-foundation = { id = "ru.kode.android.build-publish-novo.foundation", version.ref = "build-publish" }
buildpublish-telegram = { id = "ru.kode.android.build-publish-novo.telegram", version.ref = "build-publish" }
buildpublish-confluence = { id = "ru.kode.android.build-publish-novo.confluence", version.ref = "build-publish" }Then apply them in an app module.
Kotlin DSL (build.gradle.kts):
plugins {
id("com.android.application")
alias(libs.plugins.buildpublish.foundation)
alias(libs.plugins.buildpublish.telegram)
alias(libs.plugins.buildpublish.confluence)
}Groovy DSL (build.gradle):
plugins {
id 'com.android.application'
alias(libs.plugins.buildpublish.foundation)
alias(libs.plugins.buildpublish.telegram)
alias(libs.plugins.buildpublish.confluence)
}If you use a build-logic / build-conventions module with convention plugins, you can add the Build Publish
plugins to the convention plugin classpath using the Gradle Version Catalog (libs) and apply them from your
convention plugin.
Example entries (based on the published artifacts):
[libraries]
buildpublish-core = { group = "ru.kode.android", name = "build-publish-novo-core", version.ref = "build-publish-core" }
buildpublish-foundation-plugin = { module = "ru.kode.android.build-publish-novo.foundation:ru.kode.android.build-publish-novo.foundation.gradle.plugin", version.ref = "build-publish" }
buildpublish-telegram-plugin = { module = "ru.kode.android.build-publish-novo.telegram:ru.kode.android.build-publish-novo.telegram.gradle.plugin", version.ref = "build-publish" }
buildpublish-confluence-plugin = { module = "ru.kode.android.build-publish-novo.confluence:ru.kode.android.build-publish-novo.confluence.gradle.plugin", version.ref = "build-publish" }In build-logic/build.gradle.kts (or your build-conventions module), add plugin artifacts as dependencies:
dependencies {
implementation(libs.buildpublish.core)
implementation(libs.buildpublish.foundation.plugin)
implementation(libs.buildpublish.telegram.plugin)
implementation(libs.buildpublish.confluence.plugin)
}Example convention plugin (Kotlin):
import org.gradle.api.Plugin
import org.gradle.api.Project
class AndroidBuildPublishConventionPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.pluginManager.apply("ru.kode.android.build-publish-novo.foundation")
project.pluginManager.apply("ru.kode.android.build-publish-novo.telegram")
project.pluginManager.apply("ru.kode.android.build-publish-novo.confluence")
}
}Then in your app module you apply only your convention plugin:
plugins {
id("your.convention.build-publish")
}Another common approach is using precompiled script plugins inside your build-logic / build-conventions
module.
In that approach you create files like:
build-conventions/src/main/kotlin/your.convention.build-publish.gradle.kts
Gradle will compile this script and generate a plugin automatically.
The plugin id is derived from the file name (your.convention.build-publish).
Example precompiled script plugin:
import org.gradle.api.GradleException
import ru.kode.android.build.publish.plugin.core.enity.BuildVariant
import ru.kode.android.build.publish.plugin.core.enity.Tag
import ru.kode.android.build.publish.plugin.core.strategy.BuildVersionNumberNameStrategy
import ru.kode.android.build.publish.plugin.core.strategy.DEFAULT_VERSION_CODE
import ru.kode.android.build.publish.plugin.core.strategy.VersionCodeStrategy
plugins {
id("com.android.application")
id("ru.kode.android.build-publish-novo.foundation")
id("ru.kode.android.build-publish-novo.telegram")
id("ru.kode.android.build-publish-novo.confluence")
}
buildPublishFoundation {
verboseLogging.set(
providers.environmentVariable("BUILD_VERBOSE_LOGGING")
.map { it.toBoolean() }
.orElse(false)
)
output {
common {
baseFileName = "android"
}
buildVariant("debug") {
baseFileName = "android"
useVersionsFromTag = false
}
buildVariant("internal") {
baseFileName = "android"
versionNameStrategy { BuildVersionNumberNameStrategy }
}
buildVariant("release") {
baseFileName = "android"
versionNameStrategy { BuildVersionNumberNameStrategy }
versionCodeStrategy { ReleaseCodeStrategy }
}
}
changelogCommon {
issueNumberPattern = "PROJECT-\\d+"
issueUrlPrefix = "https://jira.com/browse/"
commitMessageKey = "CHANGELOG"
}
}
private object ReleaseCodeStrategy : VersionCodeStrategy {
override fun build(
buildVariant: BuildVariant,
tag: Tag.Build?,
): Int {
return if (tag != null) {
val major = tag.buildVersion.substringBefore(".").toInt()
val minor = tag.buildVersion.substringAfter(".").toInt()
(major * 1000 + minor) * 1000 + tag.buildNumber
} else DEFAULT_VERSION_CODE
}
}
buildPublishTelegram {
botsCommon {
bot("changelogger") {
botId.set(
providers.environmentVariable("TELEGRAM_CHANGELOGGER_BOT_ID")
.map {
if (it.isBlank()) {
throw GradleException("no TELEGRAM_CHANGELOGGER_BOT_ID defined for telegram reports")
}
it
}
.orElse("")
)
botServerBaseUrl.set(
providers.environmentVariable("BUILD_REPORT_TELEGRAM_BOT_BASE_URL")
.map {
if (it.isBlank()) {
throw GradleException("no BUILD_REPORT_TELEGRAM_BOT_BASE_URL defined for telegram reports")
}
it
}
.orElse("")
)
botServerAuth.username.set(
providers.environmentVariable("BUILD_REPORT_TELEGRAM_BOT_AUTH_USERNAME")
.map {
if (it.isBlank()) {
throw GradleException("no BUILD_REPORT_TELEGRAM_BOT_AUTH_USERNAME defined for telegram reports")
}
it
}
.orElse("")
)
botServerAuth.password.set(
providers.environmentVariable("BUILD_REPORT_TELEGRAM_BOT_AUTH_PASSWORD")
.map {
if (it.isBlank()) {
throw GradleException("no BUILD_REPORT_TELEGRAM_BOT_AUTH_PASSWORD defined for telegram reports")
}
it
}
.orElse("")
)
chat("builds") {
chatId.set(
providers.environmentVariable("BUILD_REPORT_TELEGRAM_CHAT_ID")
.map {
if (it.isBlank()) {
throw GradleException("no BUILD_REPORT_TELEGRAM_CHAT_ID defined for telegram reports")
}
it
}
.orElse("")
)
topicId.set(
providers.environmentVariable("BUILD_REPORT_TELEGRAM_TOPIC_ID")
.map {
if (it.isBlank()) {
throw GradleException("no BUILD_REPORT_TELEGRAM_TOPIC_ID defined for telegram reports")
}
it
}
.orElse("")
)
}
}
changelogCommon {
userMentions(
providers.environmentVariable("BUILD_REPORT_TELEGRAM_USER_MENTIONS")
.map {
if (it.isBlank()) {
throw GradleException("no BUILD_REPORT_TELEGRAM_USER_MENTIONS defined for telegram reports")
}
it.trim().split(",").toList()
}
.orElse(emptyList())
)
destinationBot {
botName = "changelogger"
chatNames("builds")
}
}
distributionCommon {
destinationBot {
botName = "changelogger"
chatNames("builds")
}
}
}
}
buildPublishConfluence {
auth {
common {
baseUrl.set("https://confluence.com")
credentials.username.set(providers.environmentVariable("CONFLUENCE_USER_NAME"))
credentials.password.set(providers.environmentVariable("CONFLUENCE_USER_PASSWORD"))
}
}
distribution {
common {
pageId.set(providers.environmentVariable("CONFLUENCE_PAGE_ID"))
}
}
}Then in the app module you apply the generated convention plugin:
plugins {
id("your.convention.build-publish")
}For CI/CD it is recommended to configure credentials via environment variables and wire them into plugin
configuration using Gradle’s ProviderFactory:
providers.environmentVariable("...")is lazy (safe for configuration avoidance).- You can validate values early and fail the build with a clear message.
For security reasons:
- Do not commit secrets (tokens, passwords, service-account JSON, etc.) into the repository.
- Local development: store secrets in
local.properties(gitignored) or environment variables. - CI/CD: store secrets in your CI/CD secret variables store.
GitHub Actions does not support “secret files” directly. For files (for example JSON credentials), a common approach is to store the file content in a secret as base64 and decode it at runtime.
String secret (for example, bot token):
val telegramBotIdProvider =
providers.environmentVariable("TELEGRAM_CHANGELOGGER_BOT_ID")
.map {
if (it.isBlank()) {
throw GradleException("no TELEGRAM_CHANGELOGGER_BOT_ID defined for telegram reports")
}
it
}
.orElse("")
buildPublishTelegram {
botsCommon {
bot("changelogger") {
botId.set(telegramBotIdProvider)
}
}
}File secret (env var contains file path):
buildPublishClickUp {
auth {
common {
apiTokenFile.set(
providers.environmentVariable("CLICKUP_TOKEN_FILE")
.map {
if (it.isBlank()) {
throw GradleException("no CLICKUP_TOKEN_FILE env var provided")
}
layout.projectDirectory.file(it)
}
)
}
}
}def telegramBotIdProvider = providers.environmentVariable('TELEGRAM_CHANGELOGGER_BOT_ID')
.map {
if (it.isBlank()) {
throw new GradleException('no TELEGRAM_CHANGELOGGER_BOT_ID defined for telegram reports')
}
it
}
.orElse('')
buildPublishTelegram {
botsCommon {
bot('changelogger') {
it.botId.set(telegramBotIdProvider)
}
}
}Some integrations use Gradle Worker API / shared services under the hood. For file-based secrets (for example
Slack uploadApiTokenFile) it can be safer to resolve the environment variable eagerly into a concrete file
path and set the property to an actual file (instead of relying on lazy Provider mapping).
Groovy DSL example:
// NOTE: Need to get it eagerly, because it cannot be resolved correctly in isolated environment
def slackApiTokenFilePath = System.getenv("SLACK_API_KEY") ?: "${rootProject.projectDir}/slack-token.txt"
buildPublishSlack {
bot {
common {
it.uploadApiTokenFile.set(project.file(slackApiTokenFilePath))
}
}
}Kotlin DSL example:
// NOTE: Need to get it eagerly, because it cannot be resolved correctly in isolated environment
val slackApiTokenFilePath = System.getenv("SLACK_API_KEY") ?: "${rootProject.projectDir}/slack-token.txt"
buildPublishSlack {
bot {
common {
uploadApiTokenFile.set(project.file(slackApiTokenFilePath))
}
}
}Example (service account JSON stored in PLAY_ACCOUNT_JSON_B64):
- Encode locally:
base64 -w 0 play-account.json-
Save the resulting value in GitHub repository secrets (e.g.
PLAY_ACCOUNT_JSON_B64). -
Decode it in workflow before Gradle runs:
- name: Decode Play service account
shell: bash
run: |
echo "${{ secrets.PLAY_ACCOUNT_JSON_B64 }}" | base64 --decode > play-account.jsonThen reference the generated file from Gradle, for example:
buildPublishPlay {
auth {
common {
apiTokenFile.set(file("play-account.json"))
}
}
}The project includes several examples to help you get started:
Located in example-project/, this is a complete Android application demonstrating how to use the plugin in a real-world scenario. It includes:
- Multiple build types and flavors
- Integration with Firebase and Play Store
- Example of version management
- Sample build configurations
To use the example project:
- Navigate to the
example-projectdirectory - Run
./gradlew tasksto see available tasks - Try building different variants:
./gradlew assembleDebugor./gradlew assembleRelease
Found in example-plugin/, this demonstrates how to create a custom plugin that extends the build publish functionality. It includes:
- A simple plugin that prints the current Git tag
- Basic plugin structure and configuration
- Integration with the main plugin system
In the plugin-test/ directory, you'll find test implementations for all major plugin features:
- Firebase App Distribution
- Google Play Store publishing
- Jira automation
- Slack notifications and distribution
- Telegram notifications and distribution
- ClickUp task management
- Confluence distribution
These test modules serve as practical references for implementing specific features in your project.
The core plugin that provides essential functionality for build publishing, version management, and changelog generation. This plugin must be applied to all modules that will use any of the publishing plugins.
This plugin supports only:
- Android application modules (
com.android.application) - Android Gradle Plugin 7.4+
- Automatic version management using Git tags
- Changelog generation from commit history
- Build variant support (flavors and build types)
- Customizable version code and name strategies
- Support for multiple output formats (APK, AAB)
// app/build.gradle.kts
plugins {
id("com.android.application")
id("ru.kode.android.build-publish-novo.foundation")
}
buildPublishFoundation {
output {
common {
baseFileName.set("app")
useVersionsFromTag.set(true)
// Matches tags like:
// - v1.0.100-debug
// - v1.2.3.42-release
// The last numeric part is treated as the build number.
buildTagPattern {
literal("v")
separator(".")
buildVersion()
optionalSeparator(".")
anyOptionalSymbols()
separator("-")
buildVariantName()
}
}
}
}// app/build.gradle
plugins {
id 'com.android.application'
id 'ru.kode.android.build-publish-novo.foundation'
}
buildPublishFoundation {
output {
common {
it.baseFileName.set('app')
it.useVersionsFromTag.set(true)
it.buildTagPattern {
literal('v')
separator('.')
buildVersion()
optionalSeparator('.')
anyOptionalSymbols()
separator('-')
buildVariantName()
}
}
}
}buildPublishFoundation {
verboseLogging.set(false)
bodyLogging.set(false)
output {
common {
baseFileName.set("app")
useVersionsFromTag.set(true)
useDefaultsForVersionsAsFallback.set(true)
versionNameStrategy {
ru.kode.android.build.publish.plugin.core.strategy.BuildVersionNumberVariantNameStrategy
}
versionCodeStrategy {
ru.kode.android.build.publish.plugin.core.strategy.BuildVersionCodeStrategy
}
outputApkNameStrategy {
ru.kode.android.build.publish.plugin.core.strategy.VersionedApkNamingStrategy
}
}
buildVariant("debug") {
useStubsForTagAsFallback.set(true)
}
}
changelog {
common {
issueNumberPattern.set("#(\\d+)")
issueUrlPrefix.set("https://your-issue-tracker.com/issue/")
commitMessageKey.set("message")
excludeMessageKey.set(true)
}
}
}buildPublishFoundation {
verboseLogging.set(false)
bodyLogging.set(false)
output {
common {
it.baseFileName.set('app')
it.useVersionsFromTag.set(true)
it.useDefaultsForVersionsAsFallback.set(true)
it.versionNameStrategy {
ru.kode.android.build.publish.plugin.core.strategy.BuildVersionNumberVariantNameStrategy.INSTANCE
}
it.versionCodeStrategy {
ru.kode.android.build.publish.plugin.core.strategy.BuildVersionCodeStrategy.INSTANCE
}
it.outputApkNameStrategy {
ru.kode.android.build.publish.plugin.core.strategy.VersionedApkNamingStrategy.INSTANCE
}
}
buildVariant('debug') {
it.useStubsForTagAsFallback.set(true)
}
}
changelog {
common {
it.issueNumberPattern.set('#(\\d+)')
it.issueUrlPrefix.set('https://your-issue-tracker.com/issue/')
it.commitMessageKey.set('message')
it.excludeMessageKey.set(true)
}
}
}-
verboseLogging- What it does: Enables extra informational logging from build-publish plugins.
- Why you need it: Useful for debugging why a particular config (for example
commonvsbuildVariant(...)) was chosen and what tasks were configured. - When to enable: CI troubleshooting, local debugging.
-
bodyLogging- What it does: Enables logging of HTTP request/response bodies for plugins that talk to external APIs.
- Why you need it: Helps troubleshoot API failures or unexpected responses.
- Warning: Can print sensitive data. Prefer keeping it disabled in CI.
Output configuration is defined per Android build variant using:
common { ... }for defaults applied to all variantsbuildVariant("debug") { ... }to override for a single variant
Properties (applies to each OutputConfig):
-
baseFileName(required)- What it does: Base name used by output file naming strategy.
- Why you need it: Ensures produced APKs are easy to recognize (for example
app-release-...apk).
-
useVersionsFromTag(default:true)- What it does: Reads version info from Git tags.
- Why you need it: Single source of truth for
versionName/versionCodeacross builds. - If disabled: Version values are taken from fallbacks (defaults or Android DSL depending on other settings).
-
useStubsForTagAsFallback(default:true)- What it does: If no matching Git tag is found, allows the build to continue using stub tag values.
- Why you need it: Useful for first CI runs / new branches where tags aren’t present yet.
- Implementation detail: Stub tag values are generated via
ru.kode.android.build.publish.plugin.core.strategy.HardcodedTagGenerationStrategy. - If disabled: Missing tags typically cause the tag snapshot task to fail.
-
useDefaultsForVersionsAsFallback(default:true)- What it does: Uses default version values when tag-derived values are unavailable.
- Why you need it: Lets builds proceed even when tag parsing is temporarily unavailable.
-
buildTagPattern { ... }- What it does: Builds a regex template that is used to find the “latest” tag for each variant.
- Why you need it: Your repository’s tag format must match what the plugin expects.
- Important: The pattern must contain:
- at least one
buildVersion()group ((\\d+)) buildVariantName()(%s) so each variant can have its own tag stream
- at least one
-
versionNameStrategy { ... }- What it does: Defines how
versionNameis computed from a resolved tag. - Why you need it: Different projects encode different info into tags.
- Groovy DSL note: Kotlin
objectstrategies are referenced viaINSTANCE(nonew), for exampleversionNameStrategy { BuildVersionNumberNameStrategy.INSTANCE }. - Common choices:
ru.kode.android.build.publish.plugin.core.strategy.BuildVersionNameStrategyru.kode.android.build.publish.plugin.core.strategy.BuildVersionNumberNameStrategyru.kode.android.build.publish.plugin.core.strategy.BuildVersionNumberVariantNameStrategyru.kode.android.build.publish.plugin.core.strategy.BuildVersionVariantNameStrategyru.kode.android.build.publish.plugin.core.strategy.TagRawNameStrategyru.kode.android.build.publish.plugin.core.strategy.FixedVersionNameStrategy { ... }
- Examples (assume
buildVariant.name = "release", tag present:tag.buildVersion = "1.2",tag.buildNumber = 42,tag.name = "v1.2.42-release"; tag missing:tag = null):BuildVersionNameStrategy- tag present:
1.2 - tag missing:
0.0
- tag present:
BuildVersionNumberNameStrategy- tag present:
1.2.42 - tag missing:
0.0.1
- tag present:
BuildVersionNumberVariantNameStrategy- tag present:
1.2.42-release - tag missing:
0.0-release
- tag present:
BuildVersionVariantNameStrategy- tag present:
1.2-release - tag missing:
0.0-release
- tag present:
TagRawNameStrategy- tag present:
v1.2.42-release - tag missing:
v0.0.1-release
- tag present:
FixedVersionNameStrategy { "my-fixed" }- tag present:
my-fixed - tag missing:
my-fixed
- tag present:
- What it does: Defines how
-
versionCodeStrategy { ... }- What it does: Defines how
versionCodeis computed from a resolved tag. - Why you need it: Allows you to encode semantic versioning or fixed version code rules.
- Groovy DSL note: Kotlin
objectstrategies are referenced viaINSTANCE(nonew), for exampleversionCodeStrategy { BuildVersionCodeStrategy.INSTANCE }. - Common choices:
ru.kode.android.build.publish.plugin.core.strategy.BuildVersionCodeStrategyru.kode.android.build.publish.plugin.core.strategy.SemanticVersionFlattenedCodeStrategyru.kode.android.build.publish.plugin.core.strategy.FixedVersionCodeStrategy { ... }
- Examples (assume tag present:
tag.buildVersion = "1.2",tag.buildNumber = 42; tag missing:tag = null):BuildVersionCodeStrategy- tag present:
42 - tag missing:
1
- tag present:
SemanticVersionFlattenedCodeStrategy(formula:(major * 1000 + minor) * 1000 + buildNumber)- tag present (
1.2+42):1002042 - tag missing:
1
- tag present (
FixedVersionCodeStrategy { 10000 }- tag present:
10000 - tag missing:
10000
- tag present:
- What it does: Defines how
-
outputApkNameStrategy { ... }- What it does: Defines how the final APK file name is computed.
- Why you need it: Produces consistent artifact names for distribution/upload steps.
- Groovy DSL note: Kotlin
objectstrategies are referenced viaINSTANCE(nonew), for exampleoutputApkNameStrategy { VersionedApkNamingStrategy.INSTANCE }. - Common choices:
ru.kode.android.build.publish.plugin.core.strategy.VersionedApkNamingStrategyru.kode.android.build.publish.plugin.core.strategy.SimpleApkNamingStrategyru.kode.android.build.publish.plugin.core.strategy.FixedApkNamingStrategy { ... }
- Examples (assume
baseFileName = "app",outputFileName = "app-release.apk", and tag present:tag.buildVariant = "release",tag.buildNumber = 42; tag missing:tag = null):VersionedApkNamingStrategy- tag present:
app-release-vc42-<DATE>.apk(date format:ddMMyyyy) - tag missing:
app-<DATE>.apk
- tag present:
SimpleApkNamingStrategy- tag present:
app.apk - tag missing:
app.apk
- tag present:
FixedApkNamingStrategy { "my-fixed" }- tag present:
my-fixed.apk - tag missing:
my-fixed.apk
- tag present:
Changelog config defines how commit messages are filtered and how issue links are rendered.
-
issueNumberPattern- What it does: Regex that finds issue identifiers in commit messages.
- Why you need it: Enables clickable issue references in generated changelog.
-
issueUrlPrefix- What it does: Prefix for creating issue URLs.
- Why you need it: Converts issue IDs into full links.
-
commitMessageKey- What it does: Marker used to include only selected commits into changelog.
- Why you need it: Keeps changelog clean (only user-facing changes).
-
excludeMessageKey(default:true)- What it does: Removes the marker key from the final changelog text.
- Why you need it: Lets you keep markers in Git history without exposing them to end users.
| Task Name | Description | Depends On |
|---|---|---|
getLastTagSnapshot<Variant> |
Finds the last matching Git tag and writes a JSON snapshot file | - |
computeVersionCode<Variant> |
Computes versionCode (from tag or fallback) and writes it to a file |
getLastTagSnapshot<Variant> |
computeVersionName<Variant> |
Computes versionName (from tag or fallback) and writes it to a file |
getLastTagSnapshot<Variant> |
computeApkOutputFileName<Variant> |
Computes the final APK output file name and writes it to a file | getLastTagSnapshot<Variant> |
renameApk<Variant> |
AGP artifact transform: copies/renames the produced APK to the computed output name | computeApkOutputFileName<Variant> |
printLastIncreasedTag<Variant> |
Prints the next tag name (build number increment) based on the snapshot | getLastTagSnapshot<Variant> |
generateChangelog<Variant> |
Generates a changelog between last tag and HEAD | getLastTagSnapshot<Variant> |
# Get last tag for debug variant
./gradlew getLastTagSnapshotDebug
# Generate changelog for release variant
./gradlew generateChangelogRelease
# Print last increased tag for staging variant
./gradlew printLastIncreasedTagStagingPublish builds to Firebase App Distribution with support for multiple variants and tester groups.
- Publish APK/AAB to Firebase App Distribution
- Support for multiple build variants
- Tester group management
- Release notes from changelog
- Integration with Firebase service accounts
| Task Name | Description | Depends On |
|---|---|---|
appDistributionUpload<Variant> |
Uploads the current variant artifact to Firebase App Distribution | Created by the official com.google.firebase.appdistribution plugin |
# Upload debug build to Firebase
./gradlew appDistributionUploadDebug-
Add Firebase configuration to your project:
-
Add
google-services.jsonto your app module -
Add Firebase App Distribution plugin to your root build script (so the plugin is on the classpath):
Kotlin DSL:
plugins { id("com.google.firebase.appdistribution") version "<your-firebase-appdistribution-version>" }Groovy DSL:
plugins { id 'com.google.firebase.appdistribution' version '<your-firebase-appdistribution-version>' }
-
-
Configure the plugin:
// app/build.gradle.kts
plugins {
id("com.android.application")
id("ru.kode.android.build-publish-novo.foundation")
id("ru.kode.android.build-publish-novo.firebase")
}
buildPublishFirebase {
distribution {
common {
appId.set("your-firebase-app-id")
serviceCredentialsFile.set(file("path/to/service-account.json"))
artifactType.set(ArtifactType.Bundle)
testerGroup("qa-team")
}
}
}// app/build.gradle
plugins {
id 'com.android.application'
id 'ru.kode.android.build-publish-novo.foundation'
id 'ru.kode.android.build-publish-novo.firebase'
}
buildPublishFirebase {
distribution {
common {
it.appId.set('your-firebase-app-id')
it.serviceCredentialsFile.set(file('path/to/service-account.json'))
it.artifactType.set(ru.kode.android.build.publish.plugin.firebase.config.ArtifactType.Bundle)
it.testerGroup('qa-team')
}
}
}The Firebase plugin configures the official Firebase App Distribution Gradle plugin. Upload tasks are created by Firebase itself and typically look like:
appDistributionUpload<Variant>
buildPublishFirebase {
distribution {
common {
appId.set("your-firebase-app-id")
serviceCredentialsFile.set(file("path/to/service-account.json"))
artifactType.set(ArtifactType.Bundle)
testerGroups("qa-team", "developers")
}
buildVariant("release") {
testerGroup("beta-testers")
}
}
}buildPublishFirebase {
distribution {
common {
it.appId.set('your-firebase-app-id')
it.serviceCredentialsFile.set(file('path/to/service-account.json'))
it.artifactType.set(ru.kode.android.build.publish.plugin.firebase.config.ArtifactType.Bundle)
it.testerGroups('qa-team', 'developers')
}
buildVariant('release') {
it.testerGroup('beta-testers')
}
}
}-
Foundation plugin is required
buildPublishFirebasewires FirebasereleaseNotesFilefrom the foundation changelog output.- Apply
ru.kode.android.build-publish-novo.foundationand configurebuildPublishFoundation { changelog { ... } }if you want meaningful release notes.
-
The Firebase App Distribution Gradle plugin is applied conditionally
- This plugin applies the official
com.google.firebase.appdistributionplugin only if at least onedistribution { ... }config is declared.
- This plugin applies the official
-
Distribution config must exist per-variant
- During variant configuration, if there is no
common { ... }(or no matchingbuildVariant("<name>") { ... }) the build fails with an explicit error asking you to add distribution config.
- During variant configuration, if there is no
Configure distribution per Android variant using:
common { ... }for defaults applied to all variantsbuildVariant("release") { ... }to override for a single variant
Properties (applies to each FirebaseDistributionConfig):
-
appId(required)- What it does: Firebase App ID to upload to.
- Why you need it: Firebase App Distribution requires a target application.
- Where to get it: Firebase Console
Project settings -> General(format like1:1234567890:android:...).
-
serviceCredentialsFile(required)- What it does: Service account JSON used to authenticate uploads.
- Why you need it: Upload requires server-side credentials.
- How to use: Store outside VCS and pass via
file("...").
-
artifactType(required)- What it does: Chooses which artifact to upload.
- Values:
ArtifactType.ApkArtifactType.Bundle
- Why you need it: Firebase needs to know whether to upload APK or AAB.
-
testerGroup("...")/testerGroups(...)(optional)- What it does: Defines which Firebase tester groups receive the release.
- Why you need it: Automates targeting QA/beta groups.
- Notes: Groups must exist in Firebase Console.
Publish builds to Google Play Store with support for multiple tracks and release types.
This integration is based on ideas and implementation details from the community plugin https://github.com/Triple-T/gradle-play-publisher, but the logic is adapted to this repository’s variant-driven build-publish flow and could not be used as-is.
- Publish to Google Play Console
- Support for multiple tracks (internal, alpha, beta, production)
- Release management (draft, in progress, completed)
- Support for release notes in multiple languages
- Integration with Google Play service account
| Task Name | Description | Depends On |
|---|---|---|
playUpload<Variant> |
Uploads a bundle (.aab) to Google Play |
bundle<Variant>, getLastTagSnapshot<Variant> |
# Upload release bundle to internal testing track
./gradlew playUploadRelease
# Override track via CLI options
./gradlew playUploadRelease --trackId=internal --updatePriority=0- Create a service account in Google Play Console
- Download the JSON key file and add it to your project
- Configure the plugin:
// app/build.gradle.kts
plugins {
id("com.android.application")
id("ru.kode.android.build-publish-novo.foundation")
id("ru.kode.android.build-publish-novo.play")
}
buildPublishPlay {
auth {
common {
appId.set("com.example.app")
apiTokenFile.set(file("play-account.json"))
}
}
distribution {
common {
trackId.set("internal")
updatePriority.set(0)
}
}
}buildPublishPlay {
auth {
common {
appId.set("com.example.app")
apiTokenFile.set(file("play-account.json"))
}
}
distribution {
common {
trackId.set("internal")
updatePriority.set(0)
}
buildVariant("release") {
trackId.set("production")
updatePriority.set(1)
}
}
}buildPublishPlay {
auth {
common {
it.appId.set('com.example.app')
it.apiTokenFile.set(file('play-account.json'))
}
}
distribution {
common {
it.trackId.set('internal')
it.updatePriority.set(0)
}
buildVariant('release') {
it.trackId.set('production')
it.updatePriority.set(1)
}
}
}-
Foundation plugin is required
- The Play plugin registers the
playUpload<Variant>task fromBuildPublishPlayExtension.configure(...). - The foundation plugin is responsible for invoking
configure(...)for allBuildPublishConfigurableExtensioninstances, so Play tasks appear only ifru.kode.android.build-publish-novo.foundationis applied.
- The Play plugin registers the
-
Both
authanddistributionmust be configuredauthis used to create Play API network services.distributionis required to configure track/priority.- If there is no matching
common { ... }(the internal common name isdefault) or no matchingbuildVariant("<name>") { ... }, the build fails fast with a “required configuration not found” error.
-
Uploads only support AAB
playUpload<Variant>uploads an Android App Bundle (.aab). If the input file is not.aab, the task fails.
Configure Play authentication per variant using:
common { ... }for defaults applied to all variantsbuildVariant("release") { ... }to override for a single variant
Properties (applies to each PlayAuthConfig):
-
appId(required)- What it does: The applicationId / package name of the app in Google Play Console.
- Why you need it: Used to target the correct app when calling Play Developer API.
-
apiTokenFile(required)- What it does: Service account JSON key file.
- Why you need it: Required to authenticate Play Developer API requests.
- How to use: Store outside VCS and pass via
file("...").
Configure distribution per variant using:
common { ... }for defaults applied to all variantsbuildVariant("release") { ... }to override for a single variant
Properties (applies to each PlayDistributionConfig):
-
trackId(default:internal)- What it does: Target track to publish to.
- Why you need it: Different tracks are used for internal/alpha/beta/production flows.
- Typical values:
internal,alpha,beta,production.
-
updatePriority(default:0)- What it does: In-app update priority (
0..5) sent with the release. - Why you need it: Allows controlling update urgency for supported update flows.
- What it does: In-app update priority (
The upload task supports overriding some inputs via CLI (Gradle task options):
--trackId=internal--updatePriority=0
The task is wired by default to:
getLastTagSnapshot<Variant>(to compute release name metadata)
Note: playUpload<Variant> consumes the bundle output via AGP artifacts (SingleArtifact.BUNDLE).
Send build notifications to Slack channels with detailed build information.
- Send build notifications to Slack
- Customizable message templates
- Support for multiple channels
- Build status and download links
- Changelog preview
| Task Name | Description | Depends On |
|---|---|---|
sendSlackChangelog<Variant> |
Sends the generated changelog to Slack | generateChangelog<Variant>, getLastTagSnapshot<Variant> |
slackDistributionUpload<Variant> |
Uploads APK to Slack channels | getLastTagSnapshot<Variant> |
slackDistributionUploadBundle<Variant> |
Uploads bundle (.aab) to Slack channels |
getLastTagSnapshot<Variant> |
# Send changelog to Slack
./gradlew sendSlackChangelogRelease
# Upload APK to Slack
./gradlew slackDistributionUploadDebug-
Create a Slack webhook URL:
- Go to https://api.slack.com/apps
- Create a new app and enable Incoming Webhooks
- Add the webhook to your workspace
-
Configure the plugin:
// app/build.gradle.kts
plugins {
id("com.android.application")
id("ru.kode.android.build-publish-novo.foundation")
id("ru.kode.android.build-publish-novo.slack")
}
buildPublishSlack {
bot {
common {
webhookUrl.set("https://hooks.slack.com/services/...")
uploadApiTokenFile.set(file("slack-upload-token.txt"))
iconUrl.set("https://example.com/bot.png")
}
}
distribution {
common {
destinationChannel("#releases")
}
}
}// app/build.gradle
plugins {
id 'com.android.application'
id 'ru.kode.android.build-publish-novo.foundation'
id 'ru.kode.android.build-publish-novo.slack'
}
buildPublishSlack {
bot {
common {
it.webhookUrl.set('https://hooks.slack.com/services/...')
it.uploadApiTokenFile.set(file('slack-upload-token.txt'))
it.iconUrl.set('https://example.com/bot.png')
}
}
distribution {
common {
it.destinationChannel('#releases')
}
}
}buildPublishSlack {
bot {
common {
webhookUrl.set("https://hooks.slack.com/services/...")
uploadApiTokenFile.set(file("slack-upload-token.txt"))
iconUrl.set("https://example.com/bot.png")
}
buildVariant("release") {
webhookUrl.set("https://hooks.slack.com/services/...")
uploadApiTokenFile.set(file("slack-upload-token.txt"))
iconUrl.set("https://example.com/release-bot.png")
}
}
changelog {
common {
attachmentColor.set("#36a64f")
userMention("@here")
}
buildVariant("release") {
attachmentColor.set("#3aa3e3")
userMentions("@channel")
}
}
distribution {
common {
destinationChannels("#releases")
}
buildVariant("debug") {
destinationChannels("#android-team")
}
}
}buildPublishSlack {
bot {
common {
it.webhookUrl.set('https://hooks.slack.com/services/...')
it.uploadApiTokenFile.set(file('slack-upload-token.txt'))
it.iconUrl.set('https://example.com/bot.png')
}
buildVariant('release') {
it.webhookUrl.set('https://hooks.slack.com/services/...')
it.uploadApiTokenFile.set(file('slack-upload-token.txt'))
it.iconUrl.set('https://example.com/release-bot.png')
}
}
changelog {
common {
it.attachmentColor.set('#36a64f')
it.userMention('@here')
}
buildVariant('release') {
it.attachmentColor.set('#3aa3e3')
it.userMentions('@channel')
}
}
distribution {
common {
it.destinationChannels('#releases')
}
buildVariant('debug') {
it.destinationChannels('#android-team')
}
}
}-
Foundation plugin is required
- The Slack plugin fails fast if
ru.kode.android.build-publish-novo.foundationis not applied.
- The Slack plugin fails fast if
-
A bot configuration is always required
- For each variant, Slack requires
bot.common { ... }(internallydefault) orbot.buildVariant("<name>") { ... }. - If bot config is missing for a variant, configuration fails.
- For each variant, Slack requires
-
At least one of
changelogordistributionmust be configured- If both are missing for a variant, configuration fails.
-
Distribution tasks are registered only when channels are configured
slackDistributionUpload<Variant>/slackDistributionUploadBundle<Variant>are created only whendistribution { ... }has at least one destination channel.
-
uploadApiTokenFileis required when you run distribution uploads- The plugin may still register distribution tasks without a token file, but execution will fail when the Slack API token is missing.
-
Slack upload can time out but still succeed
- Slack’s API may return a timeout even if the file is uploaded successfully; the plugin logs a warning for this case.
Configure bot connection details per variant.
Properties (applies to each SlackBotConfig):
-
webhookUrl(required)- What it does: Slack Incoming Webhook URL used to post changelog messages.
- Why you need it: Used by
sendSlackChangelog<Variant>.
-
uploadApiTokenFile(required for file uploads)- What it does: File containing a Slack bot/user token for file uploads.
- Why you need it: Required by
slackDistributionUpload*tasks.
-
iconUrl(required for changelog messages)- What it does: Icon URL for Slack message sender.
- Why you need it: Used by
sendSlackChangelog<Variant>.
Properties (applies to each SlackChangelogConfig):
-
attachmentColor(required)- What it does: Hex color used for Slack attachment stripe (e.g.
#36a64f). - Why you need it: Helps visually identify message type.
- What it does: Hex color used for Slack attachment stripe (e.g.
-
userMention(...)/userMentions(...)- What it does: Adds mentions (e.g.
@here,@channel,@username) to the message. - Why you need it: Notifies specific people/groups about a release.
- What it does: Adds mentions (e.g.
Properties (applies to each SlackDistributionConfig):
destinationChannel(...)/destinationChannels(...)(required to create upload tasks)- What it does: Sets channels where artifacts will be shared.
- Common values:
#releases,#android-team.
-
sendSlackChangelog<Variant>supports (CLI options):--changelogFile=/abs/path/to/changelog.md--buildTagSnapshotFile=/abs/path/to/tag.json--baseOutputFileName=MyApp--issueUrlPrefix=https://tracker/browse/--issueNumberPattern=([A-Z]+-\\d+)--iconUrl=https://example.com/icon.png--userMentions=@here--attachmentColor=#36a64f
-
slackDistributionUpload<Variant>/slackDistributionUploadBundle<Variant>supports (CLI options):--distributionFile=/abs/path/to/app.apk(or.aab)--buildTagSnapshotFile=/abs/path/to/tag.json--baseOutputFileName=MyApp--channels=#releases
Send build notifications to Telegram channels or groups.
- Send build notifications to Telegram
- Support for both public and private channels
- Custom message formatting
- Download links and build information
- Changelog preview
| Task Name | Description | Depends On |
|---|---|---|
sendTelegramChangelog<Variant> |
Sends generated changelog to Telegram | generateChangelog<Variant>, getLastTagSnapshot<Variant> |
telegramDistributionUpload<Variant> |
Sends APK distribution notification to Telegram | - |
telegramDistributionUploadBundle<Variant> |
Sends bundle distribution notification to Telegram | - |
telegramLookup<Variant> |
Helps discover chat/topic identifiers via configured bot | - |
# Send changelog
./gradlew sendTelegramChangelogRelease
# Lookup chat/topic IDs (if lookup is configured)
./gradlew telegramLookupDebug-
Create a Telegram bot if it is not exists:
- Message @BotFather on Telegram
- Use
/newbotcommand and follow instructions - Get your bot token
-
Discover
chatIdandtopicIdusingtelegramLookup<Variant>:- Add your bot to the target channel/group/topic
- Send any message to that chat/topic from your Telegram account
- Configure
buildPublishTelegram.lookup { ... }(see Full Configuration below) with:botName= your configured bot name (for examplemain)chatName= part of the chat title to matchtopicName= (optional) part of the topic title to match
- Run the lookup task, for example:
./gradlew telegramLookupDebug
- The task prints
Chat IDandTopic IDin the build output. Copy those values into:bots { ... bot("...") { chat("...") { chatId.set("..."); topicId.set("...") } } }
-
Configure the plugin:
// app/build.gradle.kts
plugins {
id("com.android.application")
id("ru.kode.android.build-publish-novo.foundation")
id("ru.kode.android.build-publish-novo.telegram")
}
buildPublishTelegram {
bots {
common {
bot("main") {
botId.set("your-bot-token")
chat("releases") {
chatId.set("@your-channel")
}
}
}
}
changelog {
common {
destinationBot {
botName.set("main")
chatName("releases")
}
}
}
}// app/build.gradle
plugins {
id 'com.android.application'
id 'ru.kode.android.build-publish-novo.foundation'
id 'ru.kode.android.build-publish-novo.telegram'
}
buildPublishTelegram {
bots {
common {
bot('main') {
it.botId.set('your-bot-token')
chat('releases') {
it.chatId.set('@your-channel')
}
}
}
}
changelog {
common {
destinationBot {
it.botName.set('main')
it.chatName('releases')
}
}
}
}buildPublishTelegram {
bots {
common {
bot("main") {
botId.set("your-bot-token")
// Optional: for self-hosted Bot API
// botServerBaseUrl.set('https://telegram-bot-api.your-company.net')
// botServerAuth.username.set(providers.environmentVariable('TELEGRAM_AUTH_USER'))
// botServerAuth.password.set(providers.environmentVariable('TELEGRAM_AUTH_PASSWORD'))
chat("releases") {
chatId.set("@your-channel")
topicId.set("123")
}
}
}
}
// Optional helper task configuration (enables telegramLookup<Variant>)
lookup {
botName.set("main")
chatName.set("releases")
topicName.set("Android releases")
}
changelog {
common {
userMention("@dev1")
destinationBot {
botName.set("main")
chatName("releases")
}
}
}
distribution {
common {
compressed.set(true)
destinationBot {
botName.set("main")
chatName("releases")
}
}
}
}buildPublishTelegram {
bots {
common {
it.bot('main') {
it.botId.set('your-bot-token')
// Optional: for self-hosted Bot API
// it.botServerBaseUrl.set('https://telegram-bot-api.your-company.net')
// it.botServerAuth.username.set(providers.environmentVariable('TELEGRAM_AUTH_USER'))
// it.botServerAuth.password.set(providers.environmentVariable('TELEGRAM_AUTH_PASSWORD'))
chat('releases') {
it.chatId.set('@your-channel')
it.topicId.set('123')
}
}
}
}
// Optional helper task configuration (enables telegramLookup<Variant>)
lookup {
it.botName.set('main')
it.chatName.set('releases')
it.topicName.set('Android releases')
}
changelog {
common {
it.userMention('@dev1')
it.userMentions('@qa_team')
it.destinationBot {
it.botName.set('main')
it.chatName('releases')
}
}
}
distribution {
common {
it.compressed.set(true)
it.destinationBot {
it.botName.set('main')
it.chatName('releases')
}
}
}
}-
Foundation plugin is required
- The Telegram plugin fails fast if
ru.kode.android.build-publish-novo.foundationis not applied.
- The Telegram plugin fails fast if
-
bots { ... }is always required- If no bots are configured, plugin configuration fails.
-
At least one of
changelog,distribution, orlookupmust be configured- If all three blocks are missing, plugin configuration fails.
-
Distribution tasks are registered only when destination bots exist
telegramDistributionUpload<Variant>/telegramDistributionUploadBundle<Variant>are registered only whendistribution { ... }has at least onedestinationBot { ... }.
-
telegramLookup<Variant>exists only whenlookup { ... }is configured
The Telegram plugin uses a two-level DSL:
- Select a build variant via
common { ... }orbuildVariant("release") { ... }. - Inside that, register one or more bots with
bot("<name>") { ... }.
Properties (applies to each TelegramBotConfig):
-
botId(required)- What it does: Telegram bot token.
- Why you need it: Used to authenticate Bot API calls.
- How to get: Create bot via
@BotFather.
-
botServerBaseUrl(optional)- What it does: Base URL for a self-hosted Telegram Bot API server.
- Why you need it: Only if you’re not using the official
https://api.telegram.org. - When it can be needed:
- If you need to upload large artifacts (for example,
.apk/.aabfiles that are bigger than typical Bot API limits). - Telegram’s official Bot API docs explicitly state that a local Bot API server enables:
- Uploads up to 2000 MB
- Downloads without a size limit
- Docs: https://core.telegram.org/bots/api#using-a-local-bot-api-server
- Server implementation (TDLib): https://github.com/tdlib/telegram-bot-api
- If you need to upload large artifacts (for example,
-
botServerAuth.username/botServerAuth.password(optional)- What it does: HTTP Basic Auth credentials for a protected self-hosted Bot API server.
- Why you need it: Only if your Bot API endpoint requires basic auth.
Chats (inside bot("...") { chat("...") { ... } }):
-
chatId(required)- What it does: Telegram chat identifier.
- Typical values:
@channelusername,-1001234567890,123456789.
-
topicId(optional)- What it does: Thread/topic id in a forum-style chat.
- Why you need it: To send messages to a specific topic.
Properties (applies to each TelegramChangelogConfig):
-
userMention(...)/userMentions(...)- What it does: Adds mentions (for example
@dev1) to the message. - Why you need it: Notifies specific users.
- What it does: Adds mentions (for example
-
destinationBot { ... }(required to actually send changelog)- What it does: Selects which configured bot sends, and which named chats receive messages.
- How to use:
botName.set("main")chatName("releases")(repeat or usechatNames(...)for multiple)
Properties (applies to each TelegramDistributionConfig):
-
destinationBot { ... }(required to create upload tasks)- What it does: Selects bot + chats where the artifact will be uploaded.
- Note: The task uploads the file you provide (APK or AAB) to all configured destinations.
-
compressed(optional, defaultfalse)- What it does: Compresses the distribution file to a
.zipbefore upload. - Why you might need it: Can reduce upload time for large artifacts.
- What it does: Compresses the distribution file to a
Lookup is an optional helper task to debug/verify your bot/chat/topic configuration.
Properties (applies to TelegramLookupConfig):
botName(required)chatName(required)topicName(optional)
-
sendTelegramChangelog<Variant>supports (CLI options):--changelogFile=/abs/path/to/changelog.md--buildTagSnapshotFile=/abs/path/to/tag.json--baseOutputFileName=MyApp--issueUrlPrefix=https://tracker/browse/--issueNumberPattern=([A-Z]+-\\d+)--userMentions=@dev1--destinationBots=<json>
-
telegramDistributionUpload<Variant>/telegramDistributionUploadBundle<Variant>supports (CLI options):--distributionFile=/abs/path/to/app.apk(or.aab)--destinationBots=<json>--compressed=true
-
telegramLookup<Variant>supports (CLI options):--botName=main--chatName=releases--topicName=Android releases
Update Jira tickets with build information.
| Task Name | Description | Depends On |
|---|---|---|
jiraAutomation<Variant> |
Applies Jira automation based on issues found in the changelog | generateChangelog<Variant>, getLastTagSnapshot<Variant> |
# Run Jira automation for the release variant
./gradlew jiraAutomationRelease- Configure Jira credentials (Jira Cloud: use an API token as the password)
- Configure at least one automation feature (label, fix version, or status transition)
// app/build.gradle.kts
plugins {
id("com.android.application")
id("ru.kode.android.build-publish-novo.foundation")
id("ru.kode.android.build-publish-novo.jira")
}
buildPublishJira {
auth {
common {
baseUrl.set("https://your-domain.atlassian.net")
credentials.username.set("your-email@example.com")
credentials.password.set(providers.environmentVariable("JIRA_API_TOKEN"))
}
}
automation {
common {
projectKey.set("PROJ")
// Enable at least one automation action
targetStatusName.set("Ready for QA")
}
}
}// app/build.gradle
plugins {
id 'com.android.application'
id 'ru.kode.android.build-publish-novo.foundation'
id 'ru.kode.android.build-publish-novo.jira'
}
buildPublishJira {
auth {
common {
it.baseUrl.set('https://your-domain.atlassian.net')
it.credentials.username.set('your-email@example.com')
it.credentials.password.set(providers.environmentVariable('JIRA_API_TOKEN'))
}
}
automation {
common {
it.projectKey.set('PROJ')
// Enable at least one automation action
it.targetStatusName.set('Ready for QA')
}
}
}buildPublishJira {
auth {
common {
baseUrl.set("https://your-domain.atlassian.net")
credentials.username.set("your-email@example.com")
credentials.password.set(providers.environmentVariable("JIRA_API_TOKEN"))
}
}
automation {
common {
projectKey.set("PROJ")
// Label / fix version patterns use String.format(...)
// format args order: buildVersion, buildNumber, buildVariant
labelPattern.set("android-%s-%d")
fixVersionPattern.set("%s")
targetStatusName.set("Ready for QA")
}
buildVariant("release") {
projectKey.set("PROJ")
labelPattern.set("release-%s")
fixVersionPattern.set("%s")
targetStatusName.set("Ready for Release")
}
}
}buildPublishJira {
auth {
common {
it.baseUrl.set('https://your-domain.atlassian.net')
it.credentials.username.set('your-email@example.com')
it.credentials.password.set(providers.environmentVariable('JIRA_API_TOKEN'))
}
}
automation {
common {
it.projectKey.set('PROJ')
// format args order: buildVersion, buildNumber, buildVariant
it.labelPattern.set('android-%s-%d')
it.fixVersionPattern.set('%s')
it.targetStatusName.set('Ready for QA')
}
buildVariant('release') {
it.projectKey.set('PROJ')
it.labelPattern.set('release-%s')
it.fixVersionPattern.set('%s')
it.targetStatusName.set('Ready for Release')
}
}
}-
Foundation plugin is required
- The Jira plugin fails fast if
ru.kode.android.build-publish-novo.foundationis not applied.
- The Jira plugin fails fast if
-
Auth configuration is required
- At least
auth.common { ... }(internal common name isdefault) must be configured.
- At least
-
Automation configuration is required per variant
- If there is no matching
automation.common { ... }orautomation.buildVariant("<name>") { ... }, configuration fails.
- If there is no matching
-
The task is created only if at least one automation action is enabled
jiraAutomation<Variant>is registered only if at least one of:labelPatternfixVersionPatterntargetStatusNameis set.
-
Issue keys are extracted from the changelog
- The task scans the generated changelog file using the foundation
issueNumberPattern. - If no issues are found, the task logs an info message and does nothing.
- The task scans the generated changelog file using the foundation
Properties (applies to each JiraAuthConfig):
-
baseUrl(required)- What it does: Base URL of your Jira instance.
- Examples:
https://your-domain.atlassian.net(Cloud)https://jira.your-company.com(Server/Data Center)
-
credentials.username(required)- What it does: Username/email used for authentication.
-
credentials.password(required)- What it does: Password or API token.
- Recommendation: For Jira Cloud use an API token.
Properties (applies to each JiraAutomationConfig):
-
projectKey(required)- What it does: Jira project key (e.g.
PROJ). - Why you need it: Required for version management and status transition lookup.
- What it does: Jira project key (e.g.
-
labelPattern(optional)- What it does: Adds a computed label to each issue found in the changelog.
- How it works: Uses
String.format(pattern, buildVersion, buildNumber, buildVariant). - Example:
android-%s-%d.
-
fixVersionPattern(optional)- What it does: Sets (and creates if needed) a fix version on each issue.
- How it works: Uses
String.format(pattern, buildVersion, buildNumber, buildVariant). - Example:
%s(use only version), orandroid-%s.
-
targetStatusName(optional)- What it does: Transitions issues to the given status (by looking up a matching transition).
- Example:
Ready for QA,Ready for Release.
The task supports overriding inputs via CLI options:
--buildTagSnapshotFile=/abs/path/to/tag.json--changelogFile=/abs/path/to/changelog.md--issueNumberPattern=([A-Z]+-\\d+)--projectKey=PROJ--labelPattern=android-%s-%d--fixVersionPattern=%s--targetStatusName=Ready for QA
Update Confluence pages with release notes.
| Task Name | Description | Depends On |
|---|---|---|
confluenceDistributionUpload<Variant> |
Uploads APK to a Confluence page as an attachment and adds a comment | - |
confluenceDistributionUploadBundle<Variant> |
Uploads bundle (.aab) to a Confluence page as an attachment and adds a comment |
- |
# Upload APK for the release variant
./gradlew confluenceDistributionUploadRelease
# Upload bundle for the release variant
./gradlew confluenceDistributionUploadBundleRelease- Create Confluence API token (Confluence Cloud) or use your account password (Server/Data Center)
- Find the Confluence page id (it is part of the page URL)
- Configure the plugin:
// app/build.gradle.kts
plugins {
id("com.android.application")
id("ru.kode.android.build-publish-novo.foundation")
id("ru.kode.android.build-publish-novo.confluence")
}
buildPublishConfluence {
auth {
common {
baseUrl.set("https://your-domain.atlassian.net/wiki")
credentials.username.set("your-email@example.com")
credentials.password.set(providers.environmentVariable("CONFLUENCE_API_TOKEN"))
}
}
distribution {
common {
pageId.set("12345678")
compressed.set(true)
}
}
}// app/build.gradle
plugins {
id 'com.android.application'
id 'ru.kode.android.build-publish-novo.foundation'
id 'ru.kode.android.build-publish-novo.confluence'
}
buildPublishConfluence {
auth {
common {
it.baseUrl.set('https://your-domain.atlassian.net/wiki')
it.credentials.username.set('your-email@example.com')
it.credentials.password.set(providers.environmentVariable('CONFLUENCE_API_TOKEN'))
}
}
distribution {
common {
it.pageId.set('12345678')
it.compressed.set(true)
}
}
}buildPublishConfluence {
auth {
common {
baseUrl.set("https://your-domain.atlassian.net/wiki")
credentials.username.set("your-email@example.com")
credentials.password.set(providers.environmentVariable("CONFLUENCE_API_TOKEN"))
}
}
distribution {
common {
pageId.set("12345678")
compressed.set(true)
}
buildVariant("release") {
pageId.set("87654321")
}
}
}buildPublishConfluence {
auth {
common {
it.baseUrl.set('https://your-domain.atlassian.net/wiki')
it.credentials.username.set('your-email@example.com')
it.credentials.password.set(providers.environmentVariable('CONFLUENCE_API_TOKEN'))
}
}
distribution {
common {
it.pageId.set('12345678')
it.compressed.set(true)
}
buildVariant('release') {
it.pageId.set('87654321')
}
}
}-
Foundation plugin is required
- The Confluence plugin fails fast if
ru.kode.android.build-publish-novo.foundationis not applied.
- The Confluence plugin fails fast if
-
Auth and distribution configuration are required
authmust be configured (at leastauth.common { ... }, internal common name isdefault).distributionmust be configured for each variant (common { ... }orbuildVariant("<name>") { ... }).
-
The task uploads a file and adds a comment
- After successful upload, the plugin also posts a comment with the uploaded file name.
-
Ensure the artifact exists
- The task uses the variant output produced by the Android build. If the artifact was not built yet, run
assemble<Variant>/bundle<Variant>first.
- The task uses the variant output produced by the Android build. If the artifact was not built yet, run
Properties (applies to each ConfluenceAuthConfig):
-
baseUrl(required)- What it does: Base URL of your Confluence instance.
- Common values:
- Cloud:
https://your-domain.atlassian.net/wiki - Server/Data Center:
https://confluence.your-company.com
- Cloud:
-
credentials.username(required)- What it does: Username/email used for authentication.
-
credentials.password(required)- What it does: Password or API token.
- Recommendation: For Confluence Cloud use an API token.
Properties (applies to each ConfluenceDistributionConfig):
-
pageId(required)- What it does: Id of the Confluence page where the file should be uploaded.
- How to get: It is part of the page URL, for example:
.../wiki/spaces/SPACE/pages/12345678/Page+Title→pageId = 12345678
-
compressed(optional, defaultfalse)- What it does: Compresses the distribution file to a
.zipbefore upload. - Why you might need it: Can reduce upload time for large artifacts.
- What it does: Compresses the distribution file to a
confluenceDistributionUpload<Variant>/confluenceDistributionUploadBundle<Variant>supports (CLI options):--distributionFile=/abs/path/to/app.apk(or.aab)--pageId=12345678--compressed=true
Update ClickUp tasks with build information.
| Task Name | Description | Depends On |
|---|---|---|
clickUpAutomation<Variant> |
Updates ClickUp tasks referenced in the changelog (tags / fix version custom field) | generateChangelog<Variant>, getLastTagSnapshot<Variant> |
# Apply automation for the release variant
./gradlew clickUpAutomationRelease- Create a ClickUp API token (store it in a local file, don’t commit it)
- Configure the plugin:
// app/build.gradle.kts
plugins {
id("com.android.application")
id("ru.kode.android.build-publish-novo.foundation")
id("ru.kode.android.build-publish-novo.clickup")
}
buildPublishClickUp {
auth {
common {
apiTokenFile.set(file("clickup-token.txt"))
}
}
automation {
common {
workspaceName.set("Your Workspace")
// Enable at least one automation action
tagPattern.set("%s")
}
}
}// app/build.gradle
plugins {
id 'com.android.application'
id 'ru.kode.android.build-publish-novo.foundation'
id 'ru.kode.android.build-publish-novo.clickup'
}
buildPublishClickUp {
auth {
common {
it.apiTokenFile.set(file('clickup-token.txt'))
}
}
automation {
common {
it.workspaceName.set('Your Workspace')
// Enable at least one automation action
it.tagPattern.set('%s')
}
}
}buildPublishClickUp {
auth {
common {
apiTokenFile.set(file("clickup-token.txt"))
}
}
automation {
common {
workspaceName.set("Your Workspace")
// Patterns use String.format(pattern, buildVersion, buildNumber, buildVariant)
// Example: v1.2.3 / 42 / release
tagPattern.set("release-%s")
// Fix version automation requires BOTH properties below
fixVersionPattern.set("%s")
fixVersionFieldName.set("Fix Version")
}
buildVariant("release") {
workspaceName.set("Your Workspace")
tagPattern.set("%s")
fixVersionPattern.set("%s")
fixVersionFieldName.set("Fix Version")
}
}
}buildPublishClickUp {
auth {
common {
it.apiTokenFile.set(file('clickup-token.txt'))
}
}
automation {
common {
it.workspaceName.set('Your Workspace')
// format args order: buildVersion, buildNumber, buildVariant
it.tagPattern.set('release-%s')
// Fix version automation requires BOTH properties below
it.fixVersionPattern.set('%s')
it.fixVersionFieldName.set('Fix Version')
}
buildVariant('release') {
it.workspaceName.set('Your Workspace')
it.tagPattern.set('%s')
it.fixVersionPattern.set('%s')
it.fixVersionFieldName.set('Fix Version')
}
}
}-
Foundation plugin is required
- The ClickUp plugin fails fast if
ru.kode.android.build-publish-novo.foundationis not applied.
- The ClickUp plugin fails fast if
-
Auth configuration is required
- At least
auth.common { ... }(internal common name isdefault) must be configured.
- At least
-
Automation configuration is required per variant
- If there is no matching
automation.common { ... }orautomation.buildVariant("<name>") { ... }, configuration fails.
- If there is no matching
-
The task is created only if at least one automation action is enabled
clickUpAutomation<Variant>is registered only if:tagPatternis set, or- both
fixVersionPatternandfixVersionFieldNameare set.
-
Fix version settings must be provided together
- If you set only one of
fixVersionPattern/fixVersionFieldName, configuration fails.
- If you set only one of
-
Issue/task IDs are extracted from the changelog
- The task scans the generated changelog file using the foundation
issueNumberPattern. - If no issues are found, the task logs an info message and does nothing.
- The task scans the generated changelog file using the foundation
Properties (applies to each ClickUpAuthConfig):
apiTokenFile(required)- What it does: File containing your ClickUp API token.
- Why you need it: Used to authenticate ClickUp API requests.
- Notes: Keep it out of VCS.
Properties (applies to each ClickUpAutomationConfig):
-
workspaceName(required)- What it does: ClickUp workspace name.
- Why you need it: Used to resolve custom field ids for fix version updates.
-
tagPattern(optional)- What it does: Adds a tag to each ClickUp task referenced in the changelog.
- How it works: Uses
String.format(pattern, buildVersion, buildNumber, buildVariant). - Example:
release-%s.
-
fixVersionPattern(optional, requiresfixVersionFieldName)- What it does: Computes a fix version value for each task.
- How it works: Uses
String.format(pattern, buildVersion, buildNumber, buildVariant). - Example:
%s.
-
fixVersionFieldName(optional, requiresfixVersionPattern)- What it does: Name of the ClickUp custom field where fix version will be written.
- Example:
Fix Version.
The task supports overriding inputs via CLI options:
--workspaceName=Your Workspace--buildTagSnapshotFile=/abs/path/to/tag.json--changelogFile=/abs/path/to/changelog.md--issueNumberPattern=#(\\d+)--fixVersionPattern=%s--fixVersionFieldName=Fix Version--tagPattern=release-%s
The foundation plugin will automatically configure any Gradle extension that:
- is registered on the project via
project.extensions.create(...), and - has a type that extends
ru.kode.android.build.publish.plugin.core.api.extension.BuildPublishConfigurableExtension.
That mechanism is the intended extension point for adding custom behaviour.
-
Create a Gradle plugin (standard
java-gradle-pluginsetup). -
Create an extension that extends
BuildPublishConfigurableExtension:
abstract class BuildPublishMyExtension : BuildPublishConfigurableExtension() {
abstract val enabled: Property<Boolean>
override fun configure(project: Project, input: ExtensionInput, variant: ApplicationVariant) {
if (!enabled.get()) return
// Register tasks for input.buildVariant.name and wire them to input.output/input.changelog.
}
}- Register the extension in your plugin and require the foundation plugin:
class BuildPublishMyPlugin : Plugin<Project> {
override fun apply(project: Project) {
project.pluginManager.apply("ru.kode.android.build-publish-novo.foundation")
project.extensions.create("buildPublishMy", BuildPublishMyExtension::class.java)
}
}If you develop your own Build Publish-compatible plugin in a separate module (or even a separate repository), you can attach it to the main project without publishing to a remote repository.
- Put your convention plugin in
build-logic/build-conventions. - Add Build Publish plugin artifacts to the convention module dependencies (see Installation section above).
- Apply your convention plugin from app modules.
This is best when the plugin is project-specific and you want it versioned together with the app.
If your custom plugin is a standalone Gradle plugin project (uses java-gradle-plugin), you can attach it as a
composite build.
Kotlin DSL (settings.gradle.kts):
pluginManagement {
includeBuild("../my-build-publish-plugin")
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
}Groovy DSL (settings.gradle):
pluginManagement {
includeBuild("../my-build-publish-plugin")
repositories {
gradlePluginPortal()
google()
mavenCentral()
}
}After that you can apply the custom plugin by its id in any module:
plugins {
id("com.android.application")
id("ru.kode.android.build-publish-novo.foundation")
id("your.custom.build-publish-plugin")
}If your custom plugin project has maven-publish configured, you can publish it locally:
./gradlew publishToMavenLocal
Then add mavenLocal() to the repository list (either pluginManagement.repositories or normal repositories)
and apply the plugin by id.
This approach is convenient for local testing, but composite builds are usually a better long-term workflow.
> Failed to authenticate with Firebase
Solution:
- Verify your
google-services.jsonis in the correct location - Ensure the service account has the necessary permissions in Firebase Console
- Check that the
appIdin your configuration matches your Firebase project
> Failed to upload to Play Store: 403 Forbidden
Solution:
- Verify your service account has the correct permissions in Google Play Console
- Check that the package name in your app matches the one in Play Console
- Ensure the service account is added to your app in Play Console
> No Git tags found for changelog generation
Solution:
- Make sure you have at least one Git tag
- Verify your tag format matches the pattern in
buildTagPattern - Run
git fetch --tagsto ensure all tags are available locally
- Fork the repository
- Create a feature branch (
git checkout -b feature/AmazingFeature) - Commit your changes (
git commit -m 'Add some AmazingFeature') - Push to the branch (
git push origin feature/AmazingFeature) - Open a Pull Request
- Clone the repository
- Open in Android Studio or your favorite IDE
- Run
./gradlew buildto build the project - Use
./gradlew publishToMavenLocalto test your changes locally
This project is licensed under the MIT License.