diff --git a/.github/actions/bump-version/README.md b/.github/actions/bump-version/README.md new file mode 100644 index 0000000..4a3ebce --- /dev/null +++ b/.github/actions/bump-version/README.md @@ -0,0 +1,78 @@ +# Bump Version Action + +A reusable composite action that bumps semantic versions (semver) based on the specified bump type. It can automatically read from and write to properties files. + +## Inputs + +| Input | Description | Required | Default | Example | +|---------------|---------------------------------------------|----------|---------------------|------------------------------| +| `bump` | Version bump type | Yes | - | `major`, `minor`, or `patch` | +| `file` | Path to properties file containing version | No | `gradle.properties` | `gradle.properties` | +| `version-key` | Key name for version in the properties file | No | `version` | `version` | + +## Outputs + +| Output | Description | Example | +|---------------|-------------------------------|---------| +| `new-version` | The new version after bumping | `1.2.4` | + +## Usage + +### Basic Usage + +The action reads the version from a file, bumps it, and writes it back: + +```yaml +- name: Bump version + id: bump + uses: ./.github/actions/bump-version + with: + bump: patch + # file defaults to gradle.properties + +- name: Use new version + run: echo "New version is ${{ steps.bump.outputs.new-version }}" +``` + +### Custom Properties File + +For non-standard file paths or version keys: + +```yaml +- name: Bump version + id: bump + uses: ./.github/actions/bump-version + with: + bump: minor + file: custom/path/version.properties + version-key: appVersion +``` + +## Examples + +### Version Bumping + +| Bump Type | Input | Output | +|-----------|---------|---------| +| Major | `1.2.3` | `2.0.0` | +| Minor | `1.2.3` | `1.3.0` | +| Patch | `1.2.3` | `1.2.4` | + +### File Format + +The action expects properties files in the format: +```properties +version=1.2.3 +``` + +You can customize the key name using the `version-key` input if your file uses a different format (e.g., `appVersion=1.2.3`). + +## Validation + +The action validates: +- File exists at the specified path +- Version key exists in the file +- Version format matches semver pattern (`X.Y.Z`) +- Bump type is one of: `major`, `minor`, or `patch` + +If validation fails, the action will exit with an error. diff --git a/.github/actions/bump-version/action.yml b/.github/actions/bump-version/action.yml new file mode 100644 index 0000000..49d1ad9 --- /dev/null +++ b/.github/actions/bump-version/action.yml @@ -0,0 +1,73 @@ +name: 'Bump Version' +description: 'Bumps a semantic version based on the specified bump type' + +inputs: + bump: + description: 'Version bump type (major, minor, or patch)' + required: true + file: + description: 'Path to properties file containing version (must have version=X.Y.Z format)' + required: false + default: 'gradle.properties' + version-key: + description: 'Key name for version in the properties file' + required: false + default: 'version' + +outputs: + new-version: + description: 'The new version after bumping' + value: ${{ steps.calculate.outputs.new_version }} + +runs: + using: 'composite' + steps: + - name: Calculate and update version + id: calculate + shell: bash + run: | + FILE="${{ inputs.file }}" + VERSION_KEY="${{ inputs.version-key }}" + BUMP="${{ inputs.bump }}" + + # Read current version from file + if [[ ! -f "$FILE" ]]; then + echo "Error: File '$FILE' not found" + exit 1 + fi + + CURRENT=$(grep "^${VERSION_KEY}=" "$FILE" | cut -d'=' -f2) + if [[ -z "$CURRENT" ]]; then + echo "Error: Could not find '${VERSION_KEY}=' in $FILE" + exit 1 + fi + echo "Read version from $FILE: $CURRENT" + + # Validate current version format + if ! [[ $CURRENT =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Error: Invalid version format '$CURRENT'. Expected format: X.Y.Z" + exit 1 + fi + + # Validate bump type + if [[ ! "$BUMP" =~ ^(major|minor|patch)$ ]]; then + echo "Error: Invalid bump type '$BUMP'. Must be: major, minor, or patch" + exit 1 + fi + + # Parse version + IFS='.' read -r major minor patch <<< "$CURRENT" + + # Calculate new version + case "$BUMP" in + major) NEW="$((major+1)).0.0" ;; + minor) NEW="$major.$((minor+1)).0" ;; + patch) NEW="$major.$minor.$((patch+1))" ;; + esac + + # Update file with new version + sed -i "s/^${VERSION_KEY}=.*/${VERSION_KEY}=$NEW/" "$FILE" + echo "Updated $FILE: ${VERSION_KEY}=$CURRENT → ${VERSION_KEY}=$NEW" + + echo "new_version=$NEW" >> $GITHUB_OUTPUT + echo "Bumped version from $CURRENT to $NEW (bump type: $BUMP)" diff --git a/.github/actions/setup-gradle/README.md b/.github/actions/setup-gradle/README.md new file mode 100644 index 0000000..d6f7f64 --- /dev/null +++ b/.github/actions/setup-gradle/README.md @@ -0,0 +1,37 @@ +# Setup Gradle Action + +A reusable composite action that sets up Java and Gradle. + +## Inputs + +| Input | Description | Required | Default | +|-------------------|-----------------------------------|----------|---------| +| `cache-read-only` | Whether Gradle cache is read-only | No | `true` | + +## Usage + +### Basic Usage (read-only cache) + +```yaml +- name: Checkout + uses: actions/checkout@v4 + +- name: Setup Gradle + uses: ./.github/actions/setup-gradle +``` + +### CI Workflow (write cache for main/develop) +```yaml +- name: Setup Gradle + uses: ./.github/actions/setup-gradle + with: + cache-read-only: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }} +``` + +### Release Workflow (write cache) +```yaml +- name: Setup Gradle + uses: ./.github/actions/setup-gradle + with: + cache-read-only: false +``` diff --git a/.github/actions/setup-gradle/action.yml b/.github/actions/setup-gradle/action.yml new file mode 100644 index 0000000..3473a62 --- /dev/null +++ b/.github/actions/setup-gradle/action.yml @@ -0,0 +1,22 @@ +name: 'Setup Java and Gradle' +description: 'Sets up JDK 17 and Gradle with caching for faster builds' + +inputs: + cache-read-only: + description: 'Whether Gradle cache is read-only' + required: false + default: 'true' + +runs: + using: 'composite' + steps: + - name: Setup Java + uses: actions/setup-java@v4 + with: + java-version: 17 + distribution: 'temurin' + + - name: Setup Gradle + uses: gradle/gradle-build-action@v3 + with: + cache-read-only: ${{ inputs.cache-read-only }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..228ade5 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,56 @@ +name: CI + +on: + pull_request: + push: + branches: [ develop ] + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + format: + name: Check formatting + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Gradle + uses: ./.github/actions/setup-gradle + with: + cache-read-only: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }} + + - name: Check formatting + run: ./gradlew spotlessCheck + + lint: + name: Run detekt + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Gradle + uses: ./.github/actions/setup-gradle + with: + cache-read-only: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }} + + - name: Run detekt + run: ./gradlew detekt + + build: + name: Build and validate + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Gradle + uses: ./.github/actions/setup-gradle + with: + cache-read-only: ${{ github.ref != 'refs/heads/main' && github.ref != 'refs/heads/develop' }} + + - name: Build and validate + run: ./gradlew build :plugin:validatePlugins diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml new file mode 100644 index 0000000..dc70d68 --- /dev/null +++ b/.github/workflows/release.yml @@ -0,0 +1,109 @@ +name: Release + +on: + workflow_dispatch: + inputs: + bump: + description: 'Version bump type' + required: true + type: choice + options: + - patch + - minor + - major + version-properties-file: + description: 'Path to properties file containing version' + required: false + type: string + default: 'gradle.properties' + +concurrency: + group: release + cancel-in-progress: false + +jobs: + publish: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + with: + token: ${{ secrets.STREAM_PUBLIC_BOT_TOKEN }} + ref: develop + + - name: Bump version + id: bump + uses: ./.github/actions/bump-version + with: + bump: ${{ github.event.inputs.bump }} + file: ${{ github.event.inputs.version-properties-file }} + + - name: Commit version file + uses: EndBug/add-and-commit@v9.1.4 + with: + add: ${{ github.event.inputs.version-properties-file }} + message: "AUTOMATION: Version Bump" + default_author: github_actions + push: false + + - name: Push changes to ci-release branch + run: git push origin HEAD:ci-release --force-with-lease + + - name: Setup Gradle + uses: ./.github/actions/setup-gradle + with: + cache-read-only: false + + - name: Build and publish + run: ./gradlew build publish --no-configuration-cache + env: + GRADLE_PUBLISH_KEY: ${{ secrets.GRADLE_PUBLISH_KEY }} + GRADLE_PUBLISH_SECRET: ${{ secrets.GRADLE_PUBLISH_SECRET }} + ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.OSSRH_USERNAME }} + ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.OSSRH_PASSWORD }} + ORG_GRADLE_PROJECT_signingInMemoryKey: ${{ secrets.SIGNING_KEY }} + ORG_GRADLE_PROJECT_signingInMemoryKeyId: ${{ secrets.SIGNING_KEY_ID }} + ORG_GRADLE_PROJECT_signingInMemoryKeyPassword: ${{ secrets.SIGNING_PASSWORD }} + + - name: Create Github Release + uses: ncipollo/release-action@v1.16.0 + with: + generateReleaseNotes: true + token: ${{ secrets.STREAM_PUBLIC_BOT_TOKEN }} + tag: v${{ steps.bump.outputs.new-version }} + commit: ci-release + makeLatest: true + + sync_branches: + needs: publish + name: Sync main and develop with release + runs-on: ubuntu-latest + steps: + - name: Check out code + uses: actions/checkout@v4 + with: + ref: main + fetch-depth: 0 + token: ${{ secrets.STREAM_PUBLIC_BOT_TOKEN }} + + - name: Configure git + run: | + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git config user.name "github-actions[bot]" + + - name: Sync main with ci-release + run: | + git fetch origin ci-release + git merge --ff-only origin/ci-release + + - name: Sync develop with main + run: | + git fetch origin develop + git checkout develop + git merge --no-edit main + + - name: Push both branches + run: | + git push origin main + git push origin develop diff --git a/LICENSE_HEADER b/LICENSE_HEADER new file mode 100644 index 0000000..0246a9c --- /dev/null +++ b/LICENSE_HEADER @@ -0,0 +1,15 @@ +/* + * Copyright (c) 2014-$YEAR Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-build-conventions-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..63de247 --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,29 @@ +plugins { + base + alias(libs.plugins.detekt) apply true + alias(libs.plugins.spotless) apply true +} + +val isSnapshot = System.getenv("SNAPSHOT")?.toBoolean() == true + +allprojects { + group = "io.getstream" + if (isSnapshot) { + version = "$version-SNAPSHOT" + } +} + +spotless { + kotlin { + target("**/*.kt") + targetExclude("**/build/**/*.kt") + ktfmt().kotlinlangStyle() + licenseHeaderFile(rootProject.file("LICENSE_HEADER")) + } +} + +detekt { + autoCorrect = true + toolVersion = libs.versions.detekt.get() + buildUponDefaultConfig = true +} diff --git a/gradle.properties b/gradle.properties index 90b0d33..603fbd9 100644 --- a/gradle.properties +++ b/gradle.properties @@ -6,3 +6,6 @@ org.gradle.configuration-cache=true # Kotlin settings kotlin.code.style=official + +# Project version +version=0.0.0 diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..59711c0 --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,20 @@ +[versions] +agp = "8.11.1" +kotlin = "2.0.21" +detekt = "1.23.8" +spotless = "7.2.1" +kotlinDokka = "2.0.0" +gradlePluginPublish = "2.0.0" +mavenPublish = "0.32.0" + +[libraries] +android-gradle-plugin = { group = "com.android.tools.build", name = "gradle", version.ref = "agp" } +kotlin-gradle-plugin = { group = "org.jetbrains.kotlin", name = "kotlin-gradle-plugin", version.ref = "kotlin" } + +[plugins] +kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" } +dokka = { id = "org.jetbrains.dokka-javadoc", version.ref = "kotlinDokka" } +gradle-plugin-publish = { id = "com.gradle.plugin-publish", version.ref = "gradlePluginPublish" } +maven-publish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" } +detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" } +spotless = { id = "com.diffplug.spotless", version.ref = "spotless" } diff --git a/plugin/build.gradle.kts b/plugin/build.gradle.kts new file mode 100644 index 0000000..89ebd9a --- /dev/null +++ b/plugin/build.gradle.kts @@ -0,0 +1,121 @@ +import com.vanniktech.maven.publish.GradlePublishPlugin +import com.vanniktech.maven.publish.SonatypeHost +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + `kotlin-dsl` + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.dokka) + alias(libs.plugins.maven.publish) + alias(libs.plugins.gradle.plugin.publish) +} + +group = "io.getstream" +description = "Gradle build conventions for Stream Android projects" + +kotlin { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + freeCompilerArgs.add("-Xjvm-default=all-compatibility") + } +} + +dependencies { + compileOnly(gradleKotlinDsl()) + compileOnly(libs.android.gradle.plugin) + compileOnly(libs.kotlin.gradle.plugin) +} + +val repoId = "GetStream/stream-build-conventions-android" +val repoUrl = "https://github.com/$repoId" + +gradlePlugin { + website = repoUrl + vcsUrl = repoUrl + + plugins { + create("androidLibrary") { + id = "io.getstream.android.library" + implementationClass = "io.getstream.android.AndroidLibraryConventionPlugin" + displayName = "Stream Android Library Convention Plugin" + description = "Convention plugin for Stream Android library modules" + } + create("androidApplication") { + id = "io.getstream.android.application" + implementationClass = "io.getstream.android.AndroidApplicationConventionPlugin" + displayName = "Stream Android Application Convention Plugin" + description = "Convention plugin for Stream Android application modules" + } + } +} + +mavenPublishing { + publishToMavenCentral(SonatypeHost.CENTRAL_PORTAL, automaticRelease = true) + configure(GradlePublishPlugin()) + + pom { + name.set("Stream Build Conventions") + description.set(project.description) + url.set(repoUrl) + + licenses { + license { + name.set("Stream License") + url.set("$repoUrl/blob/main/LICENSE") + } + } + + developers { + developer { + id = "aleksandar-apostolov" + name = "Aleksandar Apostolov" + email = "aleksandar.apostolov@getstream.io" + } + developer { + id = "VelikovPetar" + name = "Petar Velikov" + email = "petar.velikov@getstream.io" + } + developer { + id = "andremion" + name = "André Mion" + email = "andre.rego@getstream.io" + } + developer { + id = "rahul-lohra" + name = "Rahul Kumar Lohra" + email = "rahul.lohra@getstream.io" + } + developer { + id = "gpunto" + name = "Gianmarco David" + email = "gianmarco.david@getstream.io" + } + developer { + id = "PratimMallick" + name = "Pratim Mallick" + email = "pratim.mallick@getstream.io" + } + } + + scm { + connection.set("scm:git:git://github.com/$repoId.git") + developerConnection.set("scm:git:ssh://github.com:$repoId.git") + url.set(repoUrl) + } + } +} + +tasks.withType().configureEach { + mustRunAfter(tasks.publishPlugins) +} + +// Publish on the Gradle Plugin Portal only final versions, not snapshots +tasks.publishPlugins { + enabled = System.getenv("SNAPSHOT")?.toBoolean() != true +} + +// Publish on Maven after publishing on the Gradle Plugin Portal +tasks.publish { + dependsOn(tasks.publishPlugins) +} diff --git a/plugin/src/main/kotlin/io/getstream/android/AndroidConventionPlugin.kt b/plugin/src/main/kotlin/io/getstream/android/AndroidConventionPlugin.kt new file mode 100644 index 0000000..4327547 --- /dev/null +++ b/plugin/src/main/kotlin/io/getstream/android/AndroidConventionPlugin.kt @@ -0,0 +1,93 @@ +/* + * Copyright (c) 2014-2025 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-build-conventions-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.getstream.android + +import com.android.build.api.dsl.ApplicationExtension +import com.android.build.api.dsl.CommonExtension +import com.android.build.api.dsl.LibraryExtension +import org.gradle.api.JavaVersion +import org.gradle.api.Plugin +import org.gradle.api.Project +import org.gradle.api.tasks.compile.JavaCompile +import org.gradle.api.tasks.testing.logging.TestExceptionFormat +import org.gradle.kotlin.dsl.configure +import org.gradle.kotlin.dsl.withType +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.dsl.KotlinAndroidProjectExtension + +class AndroidApplicationConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + pluginManager.apply("com.android.application") + + configureAndroid() + } + } +} + +class AndroidLibraryConventionPlugin : Plugin { + override fun apply(target: Project) { + with(target) { + pluginManager.apply("com.android.library") + + configureAndroid() + } + } +} + +private val javaVersion = JavaVersion.VERSION_11 +private val jvmTargetVersion = JvmTarget.JVM_11 + +private inline fun > Project.configureAndroid() { + val commonExtension = extensions.getByType(Ext::class.java) + + commonExtension.apply { + compileOptions { + sourceCompatibility = javaVersion + targetCompatibility = javaVersion + } + + testOptions { + unitTests { + isIncludeAndroidResources = true + isReturnDefaultValues = true + all { + it.testLogging { + events("failed") + showExceptions = true + showCauses = true + showStackTraces = true + exceptionFormat = TestExceptionFormat.FULL + } + } + } + } + } + + tasks.withType().configureEach { + sourceCompatibility = javaVersion.toString() + targetCompatibility = javaVersion.toString() + } + + // Configure the Kotlin plugin if it is applied + pluginManager.withPlugin("org.jetbrains.kotlin.android") { + extensions.configure { + compilerOptions { jvmTarget.set(jvmTargetVersion) } + } + } + + extensions.create("streamAndroid", StreamAndroidExtension::class.java, commonExtension) +} diff --git a/plugin/src/main/kotlin/io/getstream/android/StreamAndroidExtension.kt b/plugin/src/main/kotlin/io/getstream/android/StreamAndroidExtension.kt new file mode 100644 index 0000000..354ff7b --- /dev/null +++ b/plugin/src/main/kotlin/io/getstream/android/StreamAndroidExtension.kt @@ -0,0 +1,66 @@ +/* + * Copyright (c) 2014-2025 Stream.io Inc. All rights reserved. + * + * Licensed under the Stream License; + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://github.com/GetStream/stream-build-conventions-android/blob/main/LICENSE + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package io.getstream.android + +import com.android.build.api.dsl.ApplicationExtension +import com.android.build.api.dsl.CommonExtension +import com.android.build.api.dsl.LibraryExtension + +/** + * Extension for configuring Android SDK versions and other properties in Stream convention plugins. + * + * Example usage: + * ```kotlin + * streamAndroid { + * compileSdk(36) + * minSdk(21) + * targetSdk(36) + * versionName("1.0.0") + * } + * ``` + */ +abstract class StreamAndroidExtension( + private val commonExtension: CommonExtension<*, *, *, *, *, *> +) { + /** Set the Android compileSdk version on the Android extension. */ + fun compileSdk(version: Int) { + commonExtension.compileSdk = version + } + + /** Set the Android minSdk version on the Android extension. */ + fun minSdk(version: Int) { + commonExtension.defaultConfig.minSdk = version + } + + /** Set the Android targetSdk version on the Android extension. */ + fun targetSdk(version: Int) { + when (commonExtension) { + is LibraryExtension -> { + commonExtension.testOptions.targetSdk = version + commonExtension.lint.targetSdk = version + } + + is ApplicationExtension -> { + commonExtension.defaultConfig.targetSdk = version + } + } + } + + /** Set the Android version name on the Android extension, if it's an ApplicationExtension. */ + fun appVersionName(name: String) { + (commonExtension as? ApplicationExtension)?.defaultConfig?.versionName = name + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..47b0f8d --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,10 @@ +dependencyResolutionManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +rootProject.name = "stream-build-conventions-android" +include(":plugin")