├── .editorconfig ├── .github ├── renovate.json └── workflows │ ├── default.yml │ ├── dependencies.yml │ ├── publish_release.yml │ ├── publish_snapshot.yml │ └── run_diffuse.yml ├── .gitignore ├── Advanced.md ├── Changelog.md ├── LICENSE ├── Readme.md ├── android ├── api │ └── android.api ├── build.gradle └── src │ ├── main │ └── kotlin │ │ └── com │ │ └── project │ │ └── starter │ │ ├── modules │ │ ├── extensions │ │ │ └── AndroidPluginExtensions.kt │ │ ├── internal │ │ │ ├── AndroidLint.kt │ │ │ └── AndroidPluginUtils.kt │ │ └── plugins │ │ │ ├── AndroidApplicationPlugin.kt │ │ │ └── AndroidLibraryPlugin.kt │ │ └── quality │ │ └── internal │ │ └── AndroidCoverage.kt │ └── test │ └── kotlin │ └── com │ └── project │ └── starter │ ├── modules │ ├── AndroidApplicationPluginTest.kt │ ├── AndroidLibraryPluginTest.kt │ └── tasks │ │ ├── ConfigurationCacheTest.kt │ │ └── ForbidJavaFilesTaskTest.kt │ ├── quality │ ├── AndroidQualityPluginTest.kt │ └── tasks │ │ └── IssueLinksCheckerTaskTest.kt │ └── versioning │ └── AndroidVersioningPluginTest.kt ├── build.gradle ├── config ├── api │ └── config.api ├── build.gradle └── src │ ├── main │ └── kotlin │ │ └── com │ │ └── project │ │ └── starter │ │ └── config │ │ ├── PluginUtils.kt │ │ ├── extensions │ │ └── RootConfigExtension.kt │ │ └── plugins │ │ └── CommonSettingsPlugin.kt │ └── test │ └── kotlin │ └── com │ └── project │ └── starter │ └── plugins │ └── CommonSettingsPluginTest.kt ├── gradle.properties ├── gradle ├── libs.versions.toml ├── plugins │ ├── build.gradle │ ├── settings.gradle │ └── src │ │ └── main │ │ └── kotlin │ │ └── PublishingPlugin.kt └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── jvm ├── api │ └── jvm.api ├── build.gradle └── src │ ├── main │ └── kotlin │ │ └── com │ │ └── project │ │ └── starter │ │ └── modules │ │ ├── extensions │ │ └── KotlinLibraryConfigExtension.kt │ │ ├── internal │ │ ├── KotlinCoverage.kt │ │ └── Repositories.kt │ │ ├── plugins │ │ ├── ConfigurationPlugin.kt │ │ └── KotlinLibraryPlugin.kt │ │ └── tasks │ │ ├── ForbidJavaFilesTask.kt │ │ ├── ProjectCoverageTask.kt │ │ ├── ProjectLintTask.kt │ │ └── ProjectTestTask.kt │ └── test │ └── kotlin │ └── com │ └── project │ └── starter │ ├── modules │ ├── ConfigurationCacheTest.kt │ ├── KotlinLibraryPluginTest.kt │ └── tasks │ │ └── ForbidJavaFilesTaskTest.kt │ ├── quality │ ├── KotlinQualityPluginTest.kt │ └── tasks │ │ └── IssueLinksCheckerTaskTest.kt │ └── versioning │ └── KotlinVersioningPluginTest.kt ├── multiplatform ├── api │ └── multiplatform.api ├── build.gradle └── src │ ├── main │ └── kotlin │ │ └── com │ │ └── project │ │ └── starter │ │ └── modules │ │ ├── extensions │ │ └── MultiplatfromLibraryConfigExtension.kt │ │ ├── internal │ │ └── MultiplatformCoverage.kt │ │ └── plugins │ │ └── MultiplatformLibraryPlugin.kt │ └── test │ └── kotlin │ └── com │ └── project │ └── starter │ ├── modules │ └── MultiplatformLibraryPluginTest.kt │ ├── quality │ ├── MultiplatformQualityPluginTest.kt │ └── tasks │ │ └── IssueLinksCheckerTaskTest.kt │ └── versioning │ └── MultiplatformVersioningPluginTest.kt ├── quality ├── api │ └── quality.api ├── build.gradle └── src │ ├── main │ ├── kotlin │ │ └── com │ │ │ └── project │ │ │ └── starter │ │ │ └── quality │ │ │ ├── extensions │ │ │ └── JavaSourcesAware.kt │ │ │ ├── internal │ │ │ ├── Detekt.kt │ │ │ ├── Ktlint.kt │ │ │ ├── ResourceLoader.kt │ │ │ └── VersionProperties.kt │ │ │ ├── plugins │ │ │ └── QualityPlugin.kt │ │ │ └── tasks │ │ │ ├── IssueLinksTask.kt │ │ │ └── ProjectCodeStyleTask.kt │ └── resources │ │ └── detekt-config.yml │ └── test │ └── kotlin │ └── com │ └── project │ └── starter │ └── quality │ ├── QualityPluginTest.kt │ └── tasks │ └── IssueLinksCheckerTaskTest.kt ├── sample ├── android │ ├── build.gradle │ ├── gradle.properties │ ├── moduleAndroidApplication │ │ ├── build.gradle │ │ └── src │ │ │ ├── main │ │ │ ├── AndroidManifest.xml │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── starter │ │ │ │ └── sample │ │ │ │ └── AndroidAplicationClass.kt │ │ │ └── test │ │ │ └── kotlin │ │ │ └── com │ │ │ └── starter │ │ │ └── sample │ │ │ └── SampleTestClass.kt │ ├── moduleAndroidLibrary │ │ ├── build.gradle │ │ └── src │ │ │ ├── main │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── starter │ │ │ │ └── sample │ │ │ │ └── AndroidAplicationClass.kt │ │ │ └── test │ │ │ └── kotlin │ │ │ └── com │ │ │ └── starter │ │ │ └── sample │ │ │ └── SampleTestClass.kt │ ├── moduleKotlinLibrary │ │ ├── build.gradle │ │ └── src │ │ │ ├── main │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── starter │ │ │ │ └── sample │ │ │ │ └── AndroidAplicationClass.kt │ │ │ └── test │ │ │ └── kotlin │ │ │ └── com │ │ │ └── starter │ │ │ └── sample │ │ │ └── SampleTestClass.kt │ ├── moduleRoot │ │ ├── build.gradle │ │ ├── moduleAndroidLibrary │ │ │ ├── build.gradle │ │ │ └── src │ │ │ │ ├── main │ │ │ │ └── kotlin │ │ │ │ │ └── com │ │ │ │ │ └── starter │ │ │ │ │ └── sample │ │ │ │ │ └── AndroidAplicationClass.kt │ │ │ │ └── test │ │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── starter │ │ │ │ └── sample │ │ │ │ └── SampleTestClass.kt │ │ └── moduleKotlinLibrary │ │ │ ├── build.gradle │ │ │ └── src │ │ │ ├── main │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── starter │ │ │ │ └── sample │ │ │ │ └── AndroidAplicationClass.kt │ │ │ └── test │ │ │ └── kotlin │ │ │ └── com │ │ │ └── starter │ │ │ └── sample │ │ │ └── SampleTestClass.kt │ ├── moduleRootAndroid │ │ ├── build.gradle │ │ ├── moduleAndroidLibrary │ │ │ ├── build.gradle │ │ │ └── src │ │ │ │ ├── main │ │ │ │ └── kotlin │ │ │ │ │ └── com │ │ │ │ │ └── starter │ │ │ │ │ └── sample │ │ │ │ │ └── AndroidAplicationClass.kt │ │ │ │ └── test │ │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── starter │ │ │ │ └── sample │ │ │ │ └── SampleTestClass.kt │ │ ├── moduleKotlinLibrary │ │ │ ├── build.gradle │ │ │ └── src │ │ │ │ ├── main │ │ │ │ └── kotlin │ │ │ │ │ └── com │ │ │ │ │ └── starter │ │ │ │ │ └── sample │ │ │ │ │ └── AndroidAplicationClass.kt │ │ │ │ └── test │ │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── starter │ │ │ │ └── sample │ │ │ │ └── SampleTestClass.kt │ │ └── src │ │ │ ├── main │ │ │ └── kotlin │ │ │ │ └── com │ │ │ │ └── starter │ │ │ │ └── sample │ │ │ │ └── AndroidAplicationClass.kt │ │ │ └── test │ │ │ └── kotlin │ │ │ └── com │ │ │ └── starter │ │ │ └── sample │ │ │ └── SampleTestClass.kt │ └── settings.gradle └── kotlin │ ├── build.gradle │ ├── gradle.properties │ ├── moduleKotlinLibrary │ ├── build.gradle │ └── src │ │ ├── main │ │ └── kotlin │ │ │ └── com │ │ │ └── starter │ │ │ └── sample │ │ │ └── AndroidAplicationClass.kt │ │ └── test │ │ └── kotlin │ │ └── com │ │ └── starter │ │ └── sample │ │ └── SampleTestClass.kt │ ├── moduleRoot │ ├── build.gradle │ └── moduleKotlinLibrary │ │ ├── build.gradle │ │ └── src │ │ ├── main │ │ └── kotlin │ │ │ └── com │ │ │ └── starter │ │ │ └── sample │ │ │ └── AndroidAplicationClass.kt │ │ └── test │ │ └── kotlin │ │ └── com │ │ └── starter │ │ └── sample │ │ └── SampleTestClass.kt │ └── settings.gradle ├── settings.gradle ├── testing ├── build.gradle └── src │ └── main │ └── kotlin │ └── com │ └── project │ └── starter │ ├── Factories.kt │ ├── GitSetup.kt │ └── WithGradleProjectTest.kt └── versioning ├── api └── versioning.api ├── build.gradle └── src ├── main └── kotlin │ └── com │ └── project │ └── starter │ └── versioning │ └── plugins │ └── VersioningPlugin.kt └── test └── kotlin └── com └── project └── starter └── versioning └── VersioningPluginTest.kt /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.{kt,kts}] 4 | max_line_length = 140 5 | indent_size = 4 6 | insert_final_newline = true 7 | ij_kotlin_allow_trailing_comma = true 8 | ij_kotlin_allow_trailing_comma_on_call_site = true 9 | ktlint_code_style = intellij_idea 10 | ktlint_standard_property-naming = disabled 11 | ktlint_class_signature_rule_force_multiline_when_parameter_count_greater_or_equal_than = 2 12 | -------------------------------------------------------------------------------- /.github/renovate.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://docs.renovatebot.com/renovate-schema.json", 3 | "assignees": [ 4 | "mateuszkwiecinski" 5 | ], 6 | "extends": [ 7 | "config:recommended" 8 | ], 9 | "packageRules": [ 10 | { 11 | "matchUpdateTypes": [ 12 | "minor", 13 | "patch", 14 | "pin", 15 | "digest" 16 | ], 17 | "automerge": true 18 | } 19 | ] 20 | } 21 | -------------------------------------------------------------------------------- /.github/workflows/default.yml: -------------------------------------------------------------------------------- 1 | name: Build Project 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | push: 8 | branches: 9 | - master 10 | 11 | concurrency: 12 | group: ${{ github.workflow }}-${{ github.ref }} 13 | cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} 14 | 15 | jobs: 16 | build: 17 | runs-on: ubuntu-latest 18 | steps: 19 | - uses: actions/checkout@v4 20 | with: 21 | fetch-depth: 0 22 | 23 | - name: Write Gradle build properties to `~/.gradle/gradle.properties` 24 | run: | 25 | mkdir -p ~/.gradle 26 | printf "org.gradle.jvmargs=-Xmx3G -XX:+UseParallelGC\n" >> ~/.gradle/gradle.properties 27 | shell: bash 28 | 29 | - uses: actions/setup-java@v4 30 | with: 31 | distribution: 'temurin' 32 | java-version: 23 33 | 34 | - uses: gradle/actions/wrapper-validation@v4 35 | 36 | - uses: gradle/actions/setup-gradle@v4 37 | 38 | - run: ./gradlew currentVersion 39 | 40 | - run: ./gradlew projectCodestyle --scan 41 | 42 | - run: ./gradlew check --scan 43 | 44 | - run: ./gradlew projectCoverage --scan 45 | 46 | - name: Upload coverage to Codecov 47 | uses: codecov/codecov-action@v5 48 | with: 49 | token: ${{ secrets.CODECOV_TOKEN }} 50 | 51 | - name: Upload test results 52 | if: ${{ always() }} 53 | uses: actions/upload-artifact@v4 54 | with: 55 | name: test-results 56 | path: "${{ github.workspace }}/**/build/reports/tests" 57 | 58 | - name: Upload jacoco report 59 | uses: actions/upload-artifact@v4 60 | with: 61 | name: jacoco-report 62 | path: "${{ github.workspace }}/**/build/reports/jacoco" 63 | 64 | - run: ./gradlew publishToMavenLocal 65 | 66 | - run: ./gradlew publishPlugins --dry-run 67 | 68 | - run: git diff --exit-code 69 | 70 | build-all-sample-android-projects: 71 | runs-on: ubuntu-latest 72 | strategy: 73 | fail-fast: false 74 | matrix: 75 | gradle: [ current, release-candidate ] 76 | task: [ build, projectTest, projectLint, projectCodeStyle, projectCoverage, issueLinksReport ] 77 | name: (Android) Gradle version ${{ matrix.gradle }}, task ${{ matrix.task }} 78 | steps: 79 | - uses: actions/checkout@v4 80 | with: 81 | fetch-depth: 0 82 | 83 | - name: Write Gradle build properties to `~/.gradle/gradle.properties` 84 | run: | 85 | mkdir -p ~/.gradle 86 | printf "org.gradle.jvmargs=-Xmx3G -XX:+UseParallelGC\n" >> ~/.gradle/gradle.properties 87 | shell: bash 88 | 89 | - uses: actions/setup-java@v4 90 | with: 91 | distribution: 'temurin' 92 | java-version: 23 93 | 94 | - name: Run ${{ matrix.task }} 95 | uses: gradle/actions/setup-gradle@v4 96 | with: 97 | gradle-version: ${{ matrix.gradle }} 98 | 99 | - name: Run build in a subdirectory 100 | working-directory: sample/android 101 | run: gradle ${{ matrix.task }} --stacktrace 102 | 103 | build-all-sample-kotlin-projects: 104 | runs-on: ubuntu-latest 105 | strategy: 106 | fail-fast: false 107 | matrix: 108 | gradle: [ current, release-candidate ] 109 | task: [ build, projectTest, projectCodeStyle, projectCoverage, issueLinksReport ] 110 | name: (Kotlin) Gradle ${{ matrix.gradle }}, task ${{ matrix.task }} 111 | steps: 112 | - uses: actions/checkout@v4 113 | with: 114 | fetch-depth: 0 115 | 116 | - name: Write Gradle build properties to `~/.gradle/gradle.properties` 117 | run: | 118 | mkdir -p ~/.gradle 119 | printf "org.gradle.jvmargs=-Xmx3G -XX:+UseParallelGC\n" >> ~/.gradle/gradle.properties 120 | shell: bash 121 | 122 | - uses: actions/setup-java@v4 123 | with: 124 | distribution: 'temurin' 125 | java-version: 23 126 | 127 | - name: Run ${{ matrix.task }} 128 | uses: gradle/actions/setup-gradle@v4 129 | with: 130 | gradle-version: ${{ matrix.gradle }} 131 | 132 | - name: Run build in a subdirectory 133 | working-directory: sample/kotlin 134 | run: gradle ${{ matrix.task }} --stacktrace 135 | -------------------------------------------------------------------------------- /.github/workflows/dependencies.yml: -------------------------------------------------------------------------------- 1 | name: Generate dependency diff 2 | 3 | on: 4 | pull_request: 5 | 6 | concurrency: 7 | group: ${{ github.workflow }}-${{ github.ref }} 8 | cancel-in-progress: true 9 | 10 | jobs: 11 | generate-diff-jvm: 12 | runs-on: ubuntu-latest 13 | 14 | steps: 15 | - uses: actions/checkout@v4 16 | with: 17 | fetch-depth: 0 18 | 19 | - name: Write Gradle build properties to `~/.gradle/gradle.properties` 20 | run: | 21 | mkdir -p ~/.gradle 22 | printf "org.gradle.jvmargs=-Xmx3G -XX:+UseParallelGC\n" >> ~/.gradle/gradle.properties 23 | shell: bash 24 | 25 | - uses: actions/setup-java@v4 26 | with: 27 | distribution: 'temurin' 28 | java-version: 23 29 | 30 | - uses: gradle/actions/setup-gradle@v4 31 | 32 | - run: ./gradlew assemble -m 33 | 34 | - id: dependency-diff-jvm 35 | name: Generate dependency diff 36 | uses: usefulness/dependency-tree-diff-action@v2 37 | with: 38 | configuration: 'runtimeClasspath' 39 | project: 'jvm' 40 | 41 | - uses: peter-evans/find-comment@v3 42 | id: find_comment 43 | with: 44 | issue-number: ${{ github.event.pull_request.number }} 45 | body-includes: Dependency diff (JVM) 46 | 47 | - uses: peter-evans/create-or-update-comment@v4 48 | if: ${{ steps.dependency-diff-jvm.outputs.text-diff != null || steps.find_comment.outputs.comment-id != null }} 49 | with: 50 | body: | 51 | Dependency diff (JVM) 52 | ```diff 53 | ${{ steps.dependency-diff-jvm.outputs.text-diff }} 54 | ``` 55 | edit-mode: replace 56 | comment-id: ${{ steps.find_comment.outputs.comment-id }} 57 | issue-number: ${{ github.event.pull_request.number }} 58 | token: ${{ secrets.GITHUB_TOKEN }} 59 | 60 | generate-diff-android: 61 | runs-on: ubuntu-latest 62 | 63 | steps: 64 | - uses: actions/checkout@v4 65 | with: 66 | fetch-depth: 0 67 | 68 | - name: Write Gradle build properties to `~/.gradle/gradle.properties` 69 | run: | 70 | mkdir -p ~/.gradle 71 | printf "org.gradle.jvmargs=-Xmx3G -XX:+UseParallelGC\n" >> ~/.gradle/gradle.properties 72 | shell: bash 73 | 74 | - uses: actions/setup-java@v4 75 | with: 76 | distribution: 'temurin' 77 | java-version: 23 78 | 79 | - uses: gradle/actions/setup-gradle@v4 80 | 81 | - run: ./gradlew assemble -m 82 | 83 | - id: dependency-diff-android 84 | name: Generate dependency diff 85 | uses: usefulness/dependency-tree-diff-action@v2 86 | with: 87 | configuration: 'runtimeClasspath' 88 | project: 'android' 89 | 90 | - uses: peter-evans/find-comment@v3 91 | id: find_comment 92 | with: 93 | issue-number: ${{ github.event.pull_request.number }} 94 | body-includes: Dependency diff (Android) 95 | 96 | - uses: peter-evans/create-or-update-comment@v4 97 | if: ${{ steps.dependency-diff-android.outputs.text-diff != null || steps.find_comment.outputs.comment-id != null }} 98 | with: 99 | body: | 100 | Dependency diff (Android): 101 | ```diff 102 | ${{ steps.dependency-diff-android.outputs.text-diff }} 103 | ``` 104 | edit-mode: replace 105 | comment-id: ${{ steps.find_comment.outputs.comment-id }} 106 | issue-number: ${{ github.event.pull_request.number }} 107 | token: ${{ secrets.GITHUB_TOKEN }} 108 | -------------------------------------------------------------------------------- /.github/workflows/publish_release.yml: -------------------------------------------------------------------------------- 1 | name: Publish Project to all Maven repositories 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | env: 9 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 10 | 11 | jobs: 12 | build: 13 | runs-on: ubuntu-latest 14 | env: 15 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 16 | OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} 17 | OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} 18 | SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }} 19 | SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} 20 | SIGNING_SECRET_KEY_RING_FILE: ${{ secrets.SIGNING_SECRET_KEY_RING_FILE }} 21 | steps: 22 | - uses: actions/checkout@v4 23 | with: 24 | fetch-depth: 0 25 | 26 | - name: Write Gradle build properties to `~/.gradle/gradle.properties` 27 | run: | 28 | mkdir -p ~/.gradle 29 | printf "org.gradle.jvmargs=-Xmx3G -XX:+UseParallelGC\n" >> ~/.gradle/gradle.properties 30 | shell: bash 31 | 32 | - uses: actions/setup-java@v4 33 | with: 34 | distribution: 'temurin' 35 | java-version: 23 36 | 37 | - uses: gradle/actions/setup-gradle@v4 38 | 39 | - name: Unwrap GPG key 40 | env: 41 | GPG_KEY_CONTENTS: ${{ secrets.GPG_KEY_CONTENTS }} 42 | SIGNING_SECRET_KEY_RING_FILE: ${{ secrets.SIGNING_SECRET_KEY_RING_FILE }} 43 | run: sudo bash -c "echo '$GPG_KEY_CONTENTS' | base64 -d > '$SIGNING_SECRET_KEY_RING_FILE'" 44 | 45 | - run: ./gradlew currentVersion 46 | 47 | - run: ./gradlew publishPlugins -Pgradle.publish.key=${{ secrets.gradle_publish_key }} -Pgradle.publish.secret=${{ secrets.gradle_publish_secret }} 48 | 49 | - run: ./gradlew publishAllPublicationsToGithubRepository 50 | -------------------------------------------------------------------------------- /.github/workflows/publish_snapshot.yml: -------------------------------------------------------------------------------- 1 | name: Publish Project Snapshot 2 | 3 | on: 4 | push: 5 | branches: 6 | - 'master' 7 | schedule: 8 | - cron: '0 3 * * 1,4' 9 | 10 | env: 11 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 12 | 13 | jobs: 14 | build: 15 | runs-on: ubuntu-latest 16 | env: 17 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 18 | OSSRH_USERNAME: ${{ secrets.OSSRH_USERNAME }} 19 | OSSRH_PASSWORD: ${{ secrets.OSSRH_PASSWORD }} 20 | SIGNING_KEY_ID: ${{ secrets.SIGNING_KEY_ID }} 21 | SIGNING_PASSWORD: ${{ secrets.SIGNING_PASSWORD }} 22 | SIGNING_SECRET_KEY_RING_FILE: ${{ secrets.SIGNING_SECRET_KEY_RING_FILE }} 23 | steps: 24 | - uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | 28 | - uses: actions/setup-java@v4 29 | with: 30 | distribution: 'temurin' 31 | java-version: 23 32 | 33 | - uses: gradle/actions/setup-gradle@v4 34 | 35 | - run: ./gradlew currentVersion 36 | 37 | - run: ./gradlew assemble 38 | 39 | - name: Unwrap GPG key 40 | env: 41 | GPG_KEY_CONTENTS: ${{ secrets.GPG_KEY_CONTENTS }} 42 | SIGNING_SECRET_KEY_RING_FILE: ${{ secrets.SIGNING_SECRET_KEY_RING_FILE }} 43 | run: sudo bash -c "echo '$GPG_KEY_CONTENTS' | base64 -d > '$SIGNING_SECRET_KEY_RING_FILE'" 44 | 45 | - name: Publish to Github Package Registry 46 | run: ./gradlew publishAllPublicationsToGithubRepository 47 | 48 | diffuse: 49 | runs-on: ubuntu-latest 50 | steps: 51 | - uses: actions/checkout@v4 52 | with: 53 | fetch-depth: 0 54 | 55 | - name: Write Gradle build properties to `~/.gradle/gradle.properties` 56 | run: | 57 | mkdir -p ~/.gradle 58 | printf "org.gradle.jvmargs=-Xmx3G -XX:+UseParallelGC\n" >> ~/.gradle/gradle.properties 59 | shell: bash 60 | 61 | - uses: actions/setup-java@v4 62 | with: 63 | distribution: 'temurin' 64 | java-version: 23 65 | 66 | - uses: gradle/actions/setup-gradle@v4 67 | 68 | - run: ./gradlew assemble -PskipJarVersion 69 | 70 | - name: Upload diffuse base artifact 71 | uses: actions/cache@v4 72 | with: 73 | path: diffuse-base-file 74 | key: diffuse-${{ github.sha }} 75 | 76 | - name: Check size 77 | run: du -h android/build/libs/android.jar 78 | shell: bash 79 | 80 | - name: Copy diffuse base artifact to be picked by cache save 81 | run: cp android/build/libs/android.jar diffuse-base-file 82 | shell: bash 83 | -------------------------------------------------------------------------------- /.github/workflows/run_diffuse.yml: -------------------------------------------------------------------------------- 1 | name: Diffuse 2 | 3 | on: 4 | pull_request: 5 | branches: 6 | - master 7 | - main 8 | - trunk 9 | - develop 10 | - maine 11 | - mane 12 | 13 | concurrency: 14 | group: ${{ github.workflow }}-${{ github.ref }} 15 | cancel-in-progress: ${{ github.ref != 'refs/heads/master' }} 16 | 17 | jobs: 18 | run-diffuse: 19 | env: 20 | github_token: ${{ secrets.GITHUB_TOKEN }} 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - uses: actions/checkout@v4 25 | with: 26 | fetch-depth: 0 27 | 28 | - name: Write Gradle build properties to `~/.gradle/gradle.properties` 29 | run: | 30 | mkdir -p ~/.gradle 31 | printf "org.gradle.jvmargs=-Xmx3G -XX:+UseParallelGC\n" >> ~/.gradle/gradle.properties 32 | shell: bash 33 | 34 | - uses: actions/setup-java@v4 35 | with: 36 | distribution: 'temurin' 37 | java-version: 23 38 | 39 | - uses: actions/cache@v4 40 | name: Download base 41 | with: 42 | path: diffuse-base-file 43 | key: diffuse-${{ github.event.pull_request.base.sha }}-always-cache-miss 44 | restore-keys: diffuse-${{ github.event.pull_request.base.sha }} 45 | 46 | - uses: gradle/actions/setup-gradle@v4 47 | 48 | - run: ./gradlew assemble -PskipJarVersion 49 | 50 | - id: diffuse 51 | uses: usefulness/diffuse-action@master 52 | with: 53 | old-file-path: diffuse-base-file 54 | new-file-path: android/build/libs/android.jar 55 | 56 | - uses: peter-evans/find-comment@v3 57 | id: find_comment 58 | with: 59 | issue-number: ${{ github.event.pull_request.number }} 60 | body-includes: Diffuse output 61 | 62 | - uses: peter-evans/create-or-update-comment@v4 63 | if: ${{ steps.diffuse.outputs.diff-gh-comment != null || steps.find_comment.outputs.comment-id != null }} 64 | with: 65 | body: | 66 | Diffuse output: 67 | 68 | ${{ steps.diffuse.outputs.diff-gh-comment }} 69 | edit-mode: replace 70 | comment-id: ${{ steps.find_comment.outputs.comment-id }} 71 | issue-number: ${{ github.event.pull_request.number }} 72 | token: ${{ secrets.GITHUB_TOKEN }} 73 | 74 | - uses: actions/upload-artifact@v4 75 | with: 76 | name: diffuse-output 77 | path: ${{ steps.diffuse.outputs.diff-file }} 78 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Ignore Gradle project-specific cache directory 2 | .gradle 3 | .idea 4 | .instant-execution-state 5 | .kotlin 6 | 7 | # Ignore Gradle build output directory 8 | build 9 | 10 | # ignore AGP's properties file just in case when project opened in Android Studio 11 | local.properties 12 | -------------------------------------------------------------------------------- /Advanced.md: -------------------------------------------------------------------------------- 1 | ## Advanced usage 2 | 3 | ### Global configuration 4 | Additional default configuration can be applied by adding to the **root project**'s `build.gradle`. 5 | All submodules will use this config as default 6 | 7 | ``` groovy 8 | plugins { 9 | id("com.starter.config") version("x.y.z") 10 | } 11 | 12 | commonConfig { 13 | javaVersion JavaVersion.VERSION_11 14 | javaFilesAllowed = true 15 | androidPlugin { 16 | compileSdkVersion 31 17 | minSdkVersion 26 18 | targetSdkVersion 31 19 | } 20 | qualityPlugin { 21 | formatOnCompile = false 22 | } 23 | } 24 | ``` 25 | 26 | - `javaVersion` - defines which java version source code is compatible with 27 | - `javaFilesAllowed` - defines if the project can contain java files, fails the build otherwise. 28 | - `androidPlugin`: 29 | - contains values passed to _Android Gradle Plugin_ 30 | - `qualityPlugin`: 31 | - `formatOnCompile` - defines if ktlint should format source code on every compilation 32 | - `versioningPlugin`: 33 | - `enabled` - enables/disables [Versioning Plugin](..#versioning-plugin) 34 | 35 | ### Generating baselines 36 | It is possible to generate baseline for every quality tool available in the project. 37 | - `Android Lint` 38 | Type `rm **/lint-*.xml ; ./gradlew projectLint -PrefreshBaseline --continue` into console 39 | - `Detekt` 40 | Create baseline using [provided configuration](https://github.com/arturbosch/detekt/blob/master/docs/pages/baseline.md) 41 | - `ktlint` 42 | Unfortunately it is not possible to generate `ktlint` baseline. 43 | Proper code style may be achieved by using `./gradlew formatKotlin` task. 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2020 Mateusz Kwieciński 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | ## Starter 2 | ___ 3 | 4 | [![Build Project](https://github.com/usefulness/project-starter/actions/workflows/default.yml/badge.svg?branch=master)](https://github.com/usefulness/project-starter/actions/workflows/default.yml) 5 |  [![ktlint](https://img.shields.io/badge/code%20style-%E2%9D%A4-FF4081.svg)](https://ktlint.github.io/) 6 | 7 | [![version](https://img.shields.io/maven-metadata/v/https/plugins.gradle.org/m2/com/project/starter/jvm/maven-metadata.xml?label=gradle)](https://plugins.gradle.org/search?term=com.starter) 8 | 9 | 10 | ## Motivation 11 | 12 | Maintaining multiple multi-module Android project often requires **copying project configuration across different projects**. 13 | Even when project reaches more advanced stage it is still required to put non-minimal effort to maintain its configuration. 14 | Starting a new project, from the scratch, **takes more than a day** to configure every tool you usually want to use. 15 | Sometimes people create template project or another way of keeping your project configuration in a good shape is using `buildSrc` plugins. 16 | Less code written, ease of sharing between projects but still some part of the code needed to be copied. 17 | 18 | This project goes further and addresses that issue by **exposing set of plugins** useful when approaching multi-module setup with _Gradle_ build system. 19 | 20 | ## Content 21 | 22 | Repository consists of several plugins that makes initial project configuration effortless and easily extensible. 23 | Each module consists of configuration code most commonly used in Android project configuration. 24 | 25 | ### Module plugins 26 | #### Kotlin Library Plugin 27 | Plugin configures [code style tasks](#quality-plugin), hooks for [common tasks](#day-to-day-use), 28 | sets coverage reports generation and manages [versioning](#versioning-plugin) of the artifact 29 | 30 | Apply plugin to **project** level `build.gradle` 31 | 32 | ``` groovy 33 | plugins { 34 | id("com.starter.library.kotlin") version("x.y.z") 35 | } 36 | 37 | // optional config with default values 38 | projectConfig { 39 | javaFilesAllowed false 40 | } 41 | ``` 42 | 43 | - `javaFilesAllowed` - defines if the project can contain java files, fails the build otherwise 44 | 45 | #### Multiplatform Library Plugin 46 | For kotlin multiplatform libraries apply plugin to **project** level `build.gradle` 47 | 48 | ``` groovy 49 | plugins { 50 | id("com.starter.library.multiplatform") version("x.y.z") 51 | } 52 | ``` 53 | 54 | #### Android Application/Library Plugin 55 | In addition to customizations made to [Kotlin Library Plugin](#kotlin-library-plugin) Android plugins 56 | tweaks default Android Gradle Plugin setup by disabling _BuildConfig_ file generation 57 | or recognizing `src/main/kotlin` (and similar) path as a valid source set. 58 | 59 | Android Library plugin requires adding to **project** level `build.gradle`: 60 | 61 | ``` groovy 62 | plugins { 63 | id("com.starter.library.android") version("x.y.z") 64 | // or id("com.starter.application.android") version("x.y.z") 65 | } 66 | 67 | // optional config with default values 68 | projectConfig { 69 | javaFilesAllowed false 70 | coverageExclusions [""] 71 | } 72 | 73 | // overridden settings for single project 74 | android { 75 | defaultConfig { 76 | minSdkVersion 21 77 | } 78 | } 79 | ``` 80 | 81 | - `javaFilesAllowed` - defines if the project can contain java files, fails the build otherwise. 82 | (Useful in large projects where you want to enforce new code written in new modules to be written in Java.) 83 | - `coverageExclusions` - defines jacoco coverage exclusions for specific module 84 | 85 | ##### Day-to-day use 86 | After applying _Library_/_Application_ plugin following tasks become available: 87 | - `./gradlew projectTest` 88 | Runs tests for all modules using either predefined tasks (i.e. `test` for kotlin modules or `testDebugUnitTest` for android libraries) or use customized values. 89 | - `./gradlew projectLint` 90 | Runs Android lint checks against all modules (if custom lint checks are applied then for Kotlin modules too) 91 | - `./gradlew projectCodeStyle` 92 | Verifies if code style matches modern standards using tools such as [`ktlint`](https://github.com/pinterest/ktlint), [`Detekt`](https://github.com/arturbosch/detekt) with predefined config. 93 | - `./gradlew projectCoverage` 94 | Automatically generates test coverage reports for all modules using [`Jacoco`](https://github.com/jacoco/jacoco) 95 | 96 | Those tasks allows you to run tests efficiently for all modules by typing just a single task. 97 | 98 | ### Standalone plugins 99 | #### Quality Plugin 100 | To only configure codestyle tools apply plugin to **project** level `build.gradle` 101 | ``` 102 | plugins { 103 | id("com.starter.quality") version("x.y.z") 104 | } 105 | ``` 106 | which applies and configures code style tasks for the project automatically. 107 | 108 | Tasks available: 109 | - `./gradlew projectCodeStyle` - checks codestyle using all tools 110 | - `./gradlew issueLinksReport` - finds and check state of all issuetracker links linked in code comments 111 | 112 | Quality Plugin gets applied automatically when using any of module _Application_/_Library_ plugins above. 113 | 114 | #### Versioning Plugin 115 | 116 | Uses simple tag-based versioning, in a Configuration Cache compatible way. 117 | 118 | To enable it as a standalone plugin, apply plugin to root project `build.gradle` 119 | ``` 120 | apply plugin: 'com.starter.versioning' 121 | ``` 122 | Versioning plugin gets applied automatically when using any of module _Application_/_Library_ plugins above and can be disabled using [Global Configuration](Advanced.md#global-configuration) 123 | 124 | ### Advanced usage 125 | See [Advanced usage](Advanced.md) 126 | 127 | ## Sample project 128 | Sample [Github Browser](https://github.com/mateuszkwiecinski/github_browser) project - a customized, `buildSrc` based plugin application. 129 | 130 | ## License 131 | The library is available under [MIT License](/LICENSE) and highly benefits from binary dependencies: 132 | - `Kotlinter Gradle` - [License](https://github.com/jeremymailen/kotlinter-gradle/blob/master/LICENSE) 133 | - `axion-relese-plugin` - [License](https://github.com/allegro/axion-release-plugin/blob/master/LICENSE) 134 | - `Kotlin Gradle Plugin` - [License](https://github.com/JetBrains/kotlin#license) 135 | - `Android Gradle Plugin` - [License](https://developer.android.com/license) 136 | - `Detekt` - [License](https://github.com/arturbosch/detekt/blob/master/LICENSE) 137 | -------------------------------------------------------------------------------- /android/api/android.api: -------------------------------------------------------------------------------- 1 | public class com/project/starter/modules/extensions/AndroidApplicationConfigExtension : com/project/starter/modules/extensions/AndroidExtension { 2 | public fun ()V 3 | } 4 | 5 | public abstract class com/project/starter/modules/extensions/AndroidExtension : com/project/starter/quality/extensions/JavaSourcesAware { 6 | public fun ()V 7 | public final fun getCoverageExclusions ()Ljava/util/List; 8 | public fun getJavaFilesAllowed ()Ljava/lang/Boolean; 9 | public final fun setCoverageExclusions (Ljava/util/List;)V 10 | public fun setJavaFilesAllowed (Ljava/lang/Boolean;)V 11 | } 12 | 13 | public class com/project/starter/modules/extensions/AndroidLibraryConfigExtension : com/project/starter/modules/extensions/AndroidExtension { 14 | public fun ()V 15 | } 16 | 17 | public final class com/project/starter/modules/internal/AndroidPluginUtilsKt$inlined$sam$i$org_gradle_api_Action$0 : org/gradle/api/Action { 18 | public fun (Lkotlin/jvm/functions/Function1;)V 19 | public final synthetic fun execute (Ljava/lang/Object;)V 20 | } 21 | 22 | public final class com/project/starter/modules/internal/AndroidPluginUtilsKt$sam$i$org_gradle_api_Action$0 : org/gradle/api/Action { 23 | public fun (Lkotlin/jvm/functions/Function1;)V 24 | public final synthetic fun execute (Ljava/lang/Object;)V 25 | } 26 | 27 | public final class com/project/starter/modules/plugins/AndroidApplicationPlugin : org/gradle/api/Plugin { 28 | public fun ()V 29 | public synthetic fun apply (Ljava/lang/Object;)V 30 | public fun apply (Lorg/gradle/api/Project;)V 31 | } 32 | 33 | public final class com/project/starter/modules/plugins/AndroidLibraryPlugin : org/gradle/api/Plugin { 34 | public fun ()V 35 | public synthetic fun apply (Ljava/lang/Object;)V 36 | public fun apply (Lorg/gradle/api/Project;)V 37 | } 38 | 39 | -------------------------------------------------------------------------------- /android/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java-gradle-plugin") 3 | alias(libs.plugins.droidsonroids.jacocotestkit) 4 | alias(libs.plugins.starter.library.kotlin) 5 | alias(libs.plugins.kotlin.samwithreceiver) 6 | alias(libs.plugins.kotlinx.binarycompatibility) 7 | id("com.starter.publishing") 8 | } 9 | 10 | dependencies { 11 | api libs.agp.gradle.implementation 12 | implementation project(":jvm") 13 | implementation project(":config") 14 | implementation project(":versioning") 15 | implementation project(":quality") 16 | 17 | testRuntimeOnly(libs.junit.platform.launcher) 18 | testImplementation project(":testing") 19 | } 20 | 21 | tasks.withType(Test).configureEach { 22 | useJUnitPlatform() 23 | } 24 | 25 | gradlePlugin { 26 | plugins { 27 | androidLibrary { 28 | id = 'com.starter.library.android' 29 | displayName = 'Android Library Plugin' 30 | implementationClass = 'com.project.starter.modules.plugins.AndroidLibraryPlugin' 31 | } 32 | androidApplication { 33 | id = 'com.starter.application.android' 34 | displayName = 'Android Applicataion Plugin' 35 | implementationClass = 'com.project.starter.modules.plugins.AndroidApplicationPlugin' 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/project/starter/modules/extensions/AndroidPluginExtensions.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.modules.extensions 2 | 3 | import com.project.starter.quality.extensions.JavaSourcesAware 4 | 5 | abstract class AndroidExtension : JavaSourcesAware { 6 | override var javaFilesAllowed: Boolean? = null 7 | var coverageExclusions: List = emptyList() 8 | } 9 | 10 | open class AndroidLibraryConfigExtension : AndroidExtension() 11 | 12 | open class AndroidApplicationConfigExtension : AndroidExtension() 13 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/project/starter/modules/internal/AndroidLint.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.modules.internal 2 | 3 | import com.android.build.api.dsl.Lint 4 | import org.gradle.api.Project 5 | 6 | internal fun Project.configureAndroidLint(lintOptions: Lint) { 7 | val additionalErrors = setOf("UnknownNullness", "KotlinPropertyAccess", "LambdaLast", "NoHardKeywords") 8 | lintOptions.enable += additionalErrors 9 | lintOptions.error += additionalErrors 10 | 11 | lintOptions.disable += setOf("ObsoleteLintCustomCheck", "UseSparseArrays") 12 | 13 | val baseline = file("lint-baseline.xml") 14 | if (baseline.exists() || hasProperty("refreshBaseline")) { 15 | lintOptions.baseline = baseline 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/project/starter/modules/internal/AndroidPluginUtils.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.modules.internal 2 | 3 | import com.android.build.api.dsl.CommonExtension 4 | import com.android.build.api.variant.AndroidComponentsExtension 5 | import com.android.build.gradle.TestedExtension 6 | import com.project.starter.config.extensions.RootConfigExtension 7 | import com.project.starter.config.getByType 8 | import com.project.starter.config.plugins.rootConfig 9 | import com.project.starter.config.withExtension 10 | import com.project.starter.modules.extensions.AndroidExtension 11 | import com.project.starter.modules.tasks.ForbidJavaFilesTask.Companion.registerForbidJavaFilesTask 12 | import com.project.starter.modules.tasks.ProjectCoverageTask.Companion.registerProjectCoverageTask 13 | import com.project.starter.modules.tasks.ProjectLintTask.Companion.registerProjectLintTask 14 | import com.project.starter.modules.tasks.ProjectTestTask.Companion.registerProjectTestTask 15 | import com.project.starter.quality.internal.configureAndroidCoverage 16 | import org.gradle.api.Project 17 | import org.gradle.api.Task 18 | import org.gradle.api.tasks.TaskProvider 19 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 20 | import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile 21 | 22 | internal fun CommonExtension<*, *, *, *, *, *>.configureAndroidPlugin(rootConfig: RootConfigExtension) { 23 | defaultConfig.apply { 24 | compileSdk = rootConfig.android.compileSdkVersion 25 | minSdk = rootConfig.android.minSdkVersion 26 | testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" 27 | } 28 | 29 | compileOptions.apply { 30 | sourceCompatibility = rootConfig.javaVersion 31 | targetCompatibility = rootConfig.javaVersion 32 | } 33 | } 34 | 35 | internal inline fun Project.configureAndroidProject() 36 | where TStarter : AndroidExtension, TAgp : AndroidComponentsExtension<*, *, *> { 37 | val androidComponents = extensions.getByType(TAgp::class.java) 38 | 39 | configureAndroidCoverage(androidComponents) { extensions.getByType(TStarter::class.java).coverageExclusions } 40 | val projectLint = registerProjectLintTask() 41 | val projectTest = registerProjectTestTask() 42 | val projectCoverage = registerProjectCoverageTask() 43 | tasks.withType(KotlinJvmCompile::class.java).configureEach { 44 | compilerOptions.jvmTarget.set(JvmTarget.fromTarget(rootConfig.javaVersion.toString())) 45 | } 46 | 47 | withExtension { projectConfig -> 48 | val javaFilesAllowed = projectConfig.javaFilesAllowed ?: rootConfig.javaFilesAllowed 49 | if (!javaFilesAllowed) { 50 | val forbidJavaFiles = registerForbidJavaFilesTask { task -> 51 | val extension = project.extensions.getByType() 52 | extension.sourceSets.configureEach { 53 | task.source += java.getSourceFiles() 54 | } 55 | } 56 | 57 | tasks.named("preBuild") { 58 | dependsOn(forbidJavaFiles) 59 | } 60 | } 61 | } 62 | 63 | androidComponents.onVariants { variant -> 64 | val capitalizedName = variant.name.replaceFirstChar(Char::titlecase) 65 | projectLint.dependsOn("$path:lint$capitalizedName") 66 | projectTest.dependsOn("$path:test${capitalizedName}UnitTest") 67 | projectCoverage.dependsOn("$path:jacoco${capitalizedName}TestReport") 68 | } 69 | } 70 | 71 | private fun TaskProvider.dependsOn(name: String) { 72 | configure { dependsOn(name) } 73 | } 74 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/project/starter/modules/plugins/AndroidApplicationPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.modules.plugins 2 | 3 | import com.android.build.api.dsl.ApplicationExtension 4 | import com.android.build.api.variant.ApplicationAndroidComponentsExtension 5 | import com.project.starter.config.getByType 6 | import com.project.starter.config.plugins.rootConfig 7 | import com.project.starter.modules.extensions.AndroidApplicationConfigExtension 8 | import com.project.starter.modules.internal.configureAndroidLint 9 | import com.project.starter.modules.internal.configureAndroidPlugin 10 | import com.project.starter.modules.internal.configureAndroidProject 11 | import org.gradle.api.Plugin 12 | import org.gradle.api.Project 13 | 14 | class AndroidApplicationPlugin : Plugin { 15 | 16 | override fun apply(target: Project) = with(target) { 17 | pluginManager.apply("com.android.application") 18 | pluginManager.apply("org.jetbrains.kotlin.android") 19 | pluginManager.apply("com.starter.quality") 20 | pluginManager.apply(ConfigurationPlugin::class.java) 21 | 22 | extensions.create("projectConfig", AndroidApplicationConfigExtension::class.java) 23 | 24 | extensions.getByType().apply { 25 | configureAndroidPlugin(rootConfig) 26 | defaultConfig.targetSdk = rootConfig.android.targetSdkVersion ?: rootConfig.android.compileSdkVersion 27 | configureAndroidLint(lint) 28 | } 29 | 30 | configureAndroidProject() 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/project/starter/modules/plugins/AndroidLibraryPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.modules.plugins 2 | 3 | import com.android.build.api.dsl.LibraryExtension 4 | import com.android.build.api.variant.LibraryAndroidComponentsExtension 5 | import com.project.starter.config.getByType 6 | import com.project.starter.config.plugins.rootConfig 7 | import com.project.starter.modules.extensions.AndroidLibraryConfigExtension 8 | import com.project.starter.modules.internal.configureAndroidLint 9 | import com.project.starter.modules.internal.configureAndroidPlugin 10 | import com.project.starter.modules.internal.configureAndroidProject 11 | import org.gradle.api.Plugin 12 | import org.gradle.api.Project 13 | 14 | class AndroidLibraryPlugin : Plugin { 15 | 16 | override fun apply(target: Project): Unit = with(target) { 17 | pluginManager.apply("com.android.library") 18 | pluginManager.apply("org.jetbrains.kotlin.android") 19 | pluginManager.apply("com.starter.quality") 20 | pluginManager.apply(ConfigurationPlugin::class.java) 21 | 22 | val rootConfig = this.rootConfig 23 | extensions.create("projectConfig", AndroidLibraryConfigExtension::class.java) 24 | 25 | extensions.getByType().apply { 26 | configureAndroidPlugin(rootConfig) 27 | 28 | buildFeatures.buildConfig = false 29 | 30 | configureAndroidLint(lint) 31 | 32 | testOptions.targetSdk = rootConfig.android.targetSdkVersion ?: rootConfig.android.compileSdkVersion 33 | } 34 | extensions.getByType().beforeVariants { variantBuilder -> 35 | if (variantBuilder.productFlavors.isEmpty()) { 36 | variantBuilder.enable = variantBuilder.buildType != "release" 37 | } 38 | } 39 | 40 | configureAndroidProject() 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /android/src/main/kotlin/com/project/starter/quality/internal/AndroidCoverage.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.quality.internal 2 | 3 | import com.android.build.api.variant.AndroidComponentsExtension 4 | import com.android.build.gradle.AppExtension 5 | import com.android.build.gradle.LibraryExtension 6 | import com.project.starter.config.getByType 7 | import com.project.starter.modules.internal.daggerCoverageExclusions 8 | import org.gradle.api.Project 9 | import org.gradle.api.tasks.testing.Test 10 | import org.gradle.testing.jacoco.plugins.JacocoPluginExtension 11 | import org.gradle.testing.jacoco.plugins.JacocoTaskExtension 12 | import org.gradle.testing.jacoco.tasks.JacocoReport 13 | 14 | internal fun Project.configureAndroidCoverage( 15 | androidComponents: AndroidComponentsExtension<*, *, *>, 16 | projectExclusions: () -> List, 17 | ) { 18 | pluginManager.apply("jacoco") 19 | 20 | extensions.configure(JacocoPluginExtension::class.java) { 21 | toolVersion = "0.8.13" 22 | } 23 | tasks.withType(Test::class.java).configureEach { 24 | extensions.getByType(JacocoTaskExtension::class.java).apply { 25 | isIncludeNoLocationClasses = true 26 | excludes = listOf("jdk.internal.*") 27 | } 28 | } 29 | 30 | @Suppress("NoNameShadowing") 31 | androidComponents.onVariants { variant -> 32 | val capitalizedVariant = variant.name.replaceFirstChar(Char::titlecase) 33 | tasks.register("jacoco${capitalizedVariant}TestReport", JacocoReport::class.java) { 34 | val testTask = tasks.getByName("test${capitalizedVariant}UnitTest") 35 | val jacocoTestTaskExtension = testTask.extensions.getByType().apply { 36 | isIncludeNoLocationClasses = true 37 | } 38 | dependsOn(testTask) 39 | group = "verification" 40 | description = "Generates Jacoco coverage reports for the ${variant.name} variant." 41 | 42 | reports { 43 | html.required.set(true) 44 | xml.required.set(true) 45 | } 46 | 47 | val oldVariant = when (val android = project.extensions.getByName("android")) { 48 | is AppExtension -> android.applicationVariants.firstOrNull { it.name == variant.name } 49 | is LibraryExtension -> android.libraryVariants.firstOrNull { it.name == variant.name } 50 | else -> null 51 | } ?: return@register logger.warn("Couldn't find variant ${variant.name}") 52 | val sourceDirs = oldVariant.sourceSets.flatMap { it.javaDirectories + it.kotlinDirectories } 53 | val classesDir = oldVariant.javaCompileProvider.get().destinationDirectory.get().asFile 54 | val jacocoExecutionData = jacocoTestTaskExtension.destinationFile 55 | 56 | val coverageExcludes = excludes + projectExclusions() 57 | val kotlinClassesDir = "${layout.buildDirectory.get()}/tmp/kotlin-classes/${variant.name}" 58 | val kotlinTree = fileTree(mapOf("dir" to kotlinClassesDir, "excludes" to coverageExcludes)) 59 | val javaTree = fileTree(mapOf("dir" to classesDir, "excludes" to coverageExcludes)) 60 | 61 | classDirectories.setFrom(javaTree + kotlinTree) 62 | executionData.setFrom(files(jacocoExecutionData)) 63 | sourceDirectories.setFrom(files(sourceDirs)) 64 | } 65 | } 66 | } 67 | 68 | private val databinding = listOf( 69 | "android/databinding/**/*.class", 70 | "androidx/databinding/**/*.class", 71 | "**/databinding/*Binding.class", 72 | "**/databinding/*BindingImpl.class", 73 | "**/BR.*", 74 | "**/IssuesRegistry.*", 75 | ) 76 | private val framework = listOf( 77 | "**/R.class", 78 | "**/R$*.class", 79 | "**/BuildConfig.*", 80 | "**/Manifest*.*", 81 | ) 82 | 83 | private val excludes = databinding + framework + daggerCoverageExclusions 84 | -------------------------------------------------------------------------------- /android/src/test/kotlin/com/project/starter/modules/AndroidApplicationPluginTest.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.modules 2 | 3 | import com.project.starter.WithGradleProjectTest 4 | import com.project.starter.javaClass 5 | import com.project.starter.kotlinClass 6 | import com.project.starter.kotlinTestClass 7 | import org.assertj.core.api.Assertions.assertThat 8 | import org.gradle.testkit.runner.TaskOutcome 9 | import org.junit.jupiter.api.BeforeEach 10 | import org.junit.jupiter.api.Test 11 | import java.io.File 12 | 13 | internal class AndroidApplicationPluginTest : WithGradleProjectTest() { 14 | 15 | lateinit var rootBuildScript: File 16 | lateinit var module1Root: File 17 | lateinit var module2Root: File 18 | 19 | @BeforeEach 20 | fun setUp() { 21 | rootDirectory.apply { 22 | resolve("settings.gradle").writeText("""include ":module1", ":module2" """) 23 | 24 | rootBuildScript = resolve("build.gradle") { 25 | writeText( 26 | """ 27 | plugins { 28 | id('com.starter.config') 29 | } 30 | 31 | commonConfig { 32 | javaVersion = JavaVersion.VERSION_1_8 // workaround for http://issuetracker.google.com/issues/294137077 33 | } 34 | """.trimIndent(), 35 | ) 36 | } 37 | module1Root = resolve("module1") { 38 | val buildScript = 39 | // language=groovy 40 | """ 41 | plugins { 42 | id('com.starter.application.android') 43 | } 44 | 45 | android { 46 | namespace "com.example.module1" 47 | buildTypes { 48 | debug { } 49 | superType { } 50 | release { } 51 | } 52 | flavorDimensions "version" 53 | productFlavors { 54 | demo { } 55 | full { } 56 | } 57 | } 58 | 59 | dependencies { 60 | testImplementation 'junit:junit:4.13.2' 61 | } 62 | 63 | """.trimIndent() 64 | resolve("build.gradle") { 65 | writeText(buildScript) 66 | } 67 | resolve("src/main/AndroidManifest.xml") { 68 | writeText( 69 | """ 70 | 71 | 72 | """.trimIndent(), 73 | ) 74 | } 75 | resolve("src/main/kotlin/com/example/ValidKotlinFile1.kt") { 76 | writeText(kotlinClass("ValidKotlinFile1")) 77 | } 78 | resolve("src/release/kotlin/com/example/ReleaseModel.kt") { 79 | writeText(kotlinClass("ReleaseModel")) 80 | } 81 | resolve("src/test/kotlin/com/example/Test1.kt") { 82 | writeText(kotlinTestClass("Test1")) 83 | } 84 | } 85 | module2Root = resolve("module2") { 86 | resolve("build.gradle") { 87 | writeText( 88 | """ 89 | plugins { 90 | id('com.starter.application.android') 91 | } 92 | 93 | android { 94 | namespace "com.example.module2" 95 | } 96 | 97 | dependencies { 98 | testImplementation 'junit:junit:4.13.2' 99 | } 100 | 101 | """.trimIndent(), 102 | ) 103 | } 104 | resolve("src/main/AndroidManifest.xml") { 105 | writeText( 106 | """ 107 | 108 | 109 | """.trimIndent(), 110 | ) 111 | } 112 | resolve("src/main/kotlin/com/example/ValidKotlinFile2.kt") { 113 | writeText(kotlinClass("ValidKotlinFile2")) 114 | } 115 | resolve("src/test/kotlin/com/example/Test2.kt") { 116 | writeText(kotlinTestClass("Test2")) 117 | } 118 | } 119 | } 120 | } 121 | 122 | @Test 123 | fun `plugin compiles 'src_main_kotlin' classes`() { 124 | val result = runTask("assemble") 125 | 126 | assertThat(result.task(":module1:assemble")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 127 | assertThat(result.task(":module2:assemble")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 128 | } 129 | 130 | @Test 131 | fun `projectTest runs tests for all modules`() { 132 | val result = runTask("projectTest") 133 | 134 | assertThat(result.task(":module1:testDemoDebugUnitTest")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) 135 | assertThat(result.task(":module2:testDebugUnitTest")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) 136 | assertThat(module1Root.resolve("build/test-results/testDemoDebugUnitTest")).isDirectoryContaining { 137 | it.name.startsWith("TEST-") 138 | } 139 | assertThat(module2Root.resolve("build/test-results/testDebugUnitTest")).isDirectoryContaining { 140 | it.name.startsWith("TEST-") 141 | } 142 | } 143 | 144 | @Test 145 | fun `projectCoverage runs coverage for all modules`() { 146 | val result = runTask("projectCoverage") 147 | 148 | assertThat(result.task(":module1:testDemoDebugUnitTest")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) 149 | assertThat(result.task(":module2:testDebugUnitTest")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) 150 | assertThat(module1Root.resolve("build/reports/jacoco/jacocoDemoDebugTestReport")).isDirectoryContaining { 151 | it.name.startsWith("jacoco") && it.name.endsWith(".xml") 152 | } 153 | assertThat(module2Root.resolve("build/reports/jacoco/jacocoDebugTestReport")).isDirectoryContaining { 154 | it.name.startsWith("jacoco") && it.name.endsWith(".xml") 155 | } 156 | } 157 | 158 | @Test 159 | fun `configures android library extension`() { 160 | val config = 161 | // language=groovy 162 | """ 163 | projectConfig { 164 | javaFilesAllowed = false 165 | coverageExclusions = ["**/view/**"] 166 | } 167 | 168 | """.trimIndent() 169 | module1Root.resolve("build.gradle").appendText(config) 170 | 171 | runTask("help") 172 | } 173 | 174 | @Test 175 | fun `does not fail on java files if settings enabled at project level`() { 176 | val config = 177 | // language=groovy 178 | """ 179 | projectConfig { 180 | javaFilesAllowed = true 181 | } 182 | 183 | """.trimIndent() 184 | module2Root.resolve("build.gradle").appendText(config) 185 | module2Root.resolve("src/main/java/JavaAllowed.java") { 186 | writeText(javaClass(className = "JavaAllowed")) 187 | } 188 | 189 | val result = runTask(":module2:assembleDebug") 190 | 191 | assertThat(result.task(":module2:assembleDebug")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 192 | } 193 | 194 | @Test 195 | fun `fails on java files by default`() { 196 | module2Root.resolve("src/main/java/JavaFile.java") { 197 | writeText(javaClass(className = "JavaFile")) 198 | } 199 | 200 | val result = runTask(":module2:assembleDebug", shouldFail = true) 201 | 202 | assertThat(result.task(":module2:forbidJavaFiles")?.outcome).isEqualTo(TaskOutcome.FAILED) 203 | } 204 | 205 | @Test 206 | fun `configures quality plugin by default`() { 207 | val qualityEnabled = runTask("projectCodeStyle") 208 | 209 | assertThat(qualityEnabled.task(":module1:projectCodeStyle")?.outcome).isNotNull() 210 | assertThat(qualityEnabled.task(":module2:projectCodeStyle")?.outcome).isNotNull() 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /android/src/test/kotlin/com/project/starter/modules/tasks/ConfigurationCacheTest.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.modules.tasks 2 | 3 | import com.project.starter.WithGradleProjectTest 4 | import com.project.starter.javaClass 5 | import org.junit.jupiter.api.BeforeEach 6 | import org.junit.jupiter.api.Test 7 | import java.io.File 8 | 9 | internal class ConfigurationCacheTest : WithGradleProjectTest() { 10 | 11 | lateinit var androidModuleRoot: File 12 | lateinit var kotlinModuleRoot: File 13 | 14 | @BeforeEach 15 | fun setUp() { 16 | rootDirectory.apply { 17 | resolve("settings.gradle").writeText("""include ':module1', ':module2' """) 18 | 19 | resolve("build.gradle") { 20 | writeText( 21 | """ 22 | plugins { 23 | id('com.starter.config') 24 | } 25 | 26 | commonConfig { 27 | javaVersion = JavaVersion.VERSION_1_8 // workaround for http://issuetracker.google.com/issues/294137077 28 | } 29 | """.trimIndent(), 30 | ) 31 | } 32 | androidModuleRoot = resolve("module1") { 33 | // language=groovy 34 | val script = 35 | """ 36 | plugins { 37 | id 'com.starter.library.android' 38 | } 39 | 40 | android { 41 | namespace "com.example.module1" 42 | } 43 | 44 | """.trimIndent() 45 | resolve("build.gradle") { 46 | writeText(script) 47 | } 48 | resolve("src/main/java/ValidJava2.java") { 49 | writeText(javaClass("ValidJava2")) 50 | } 51 | resolve("src/test/java/com/example/ValidJavaTest2.java") { 52 | writeText(javaClass("ValidJavaTest2")) 53 | } 54 | } 55 | kotlinModuleRoot = resolve("module2") { 56 | // language=groovy 57 | val script = 58 | """ 59 | plugins { 60 | id 'com.starter.library.kotlin' 61 | } 62 | 63 | """.trimIndent() 64 | resolve("build.gradle") { 65 | writeText(script) 66 | } 67 | resolve("src/main/java/ValidJava2.java") { 68 | writeText(javaClass("ValidJava2")) 69 | } 70 | resolve("src/test/java/com/example/ValidJavaTest2.java") { 71 | writeText(javaClass("ValidJavaTest2")) 72 | } 73 | } 74 | } 75 | } 76 | 77 | /** 78 | * https://youtrack.jetbrains.com/issue/KT-38498 79 | * https://issuetracker.google.com/issues/156552742 80 | */ 81 | @Test 82 | fun `does not fail with configuration cache`() { 83 | runTask("assemble", "-m", configurationCacheEnabled = true) 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /android/src/test/kotlin/com/project/starter/modules/tasks/ForbidJavaFilesTaskTest.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.modules.tasks 2 | 3 | import com.project.starter.WithGradleProjectTest 4 | import com.project.starter.javaClass 5 | import com.project.starter.kotlinClass 6 | import org.assertj.core.api.Assertions.assertThat 7 | import org.gradle.testkit.runner.TaskOutcome 8 | import org.junit.jupiter.api.BeforeEach 9 | import org.junit.jupiter.api.Test 10 | import java.io.File 11 | 12 | internal class ForbidJavaFilesTaskTest : WithGradleProjectTest() { 13 | 14 | lateinit var main: File 15 | lateinit var test: File 16 | 17 | @BeforeEach 18 | fun setUp() { 19 | rootDirectory.apply { 20 | mkdirs() 21 | resolve("settings.gradle").writeText("""include ":module1", ":parentModule:childModule" """) 22 | 23 | resolve("build.gradle") 24 | resolve("module1") { 25 | val buildScript = 26 | // language=groovy 27 | """ 28 | plugins { 29 | id('com.starter.library.android') 30 | } 31 | 32 | android { 33 | namespace "com.example.module1" 34 | } 35 | 36 | projectConfig { 37 | javaFilesAllowed = false 38 | } 39 | 40 | android { 41 | namespace "com.example.module1" 42 | } 43 | """.trimIndent() 44 | resolve("build.gradle") { 45 | writeText(buildScript) 46 | } 47 | main = resolve("src/main") { 48 | resolve("kotlin/com/example/ValidKotlin.kt") { 49 | writeText(kotlinClass("ValidKotlin")) 50 | } 51 | resolve("java/com/example/KotlinInJavaDir.kt") { 52 | writeText(kotlinClass("KotlinInJavaDir")) 53 | } 54 | } 55 | test = resolve("src/test") { 56 | resolve("kotlin/Test1.kt") { 57 | writeText(kotlinClass("Test1")) 58 | } 59 | } 60 | } 61 | 62 | resolve("parentModule") { 63 | val parentBuildScript = 64 | // language=groovy 65 | """ 66 | plugins { 67 | id('com.starter.library.android') 68 | } 69 | 70 | android { 71 | namespace "com.example.parent" 72 | } 73 | """.trimIndent() 74 | resolve("build.gradle") { 75 | writeText(parentBuildScript) 76 | } 77 | resolve("src/main") { 78 | resolve("kotlin/ValidKotlinInParent.kt") { 79 | writeText(kotlinClass("ValidKotlinInParent")) 80 | } 81 | } 82 | resolve("childModule") { 83 | val childBuildscript = 84 | // language=groovy 85 | """ 86 | plugins { 87 | id('com.starter.library.kotlin') 88 | } 89 | 90 | projectConfig { 91 | javaFilesAllowed = false 92 | } 93 | """.trimIndent() 94 | resolve("build.gradle") { 95 | writeText(childBuildscript) 96 | } 97 | resolve("src/main") { 98 | resolve("kotlin/ValidKotlinInChild.kt") { 99 | writeText(kotlinClass("ValidKotlinInChild")) 100 | } 101 | } 102 | } 103 | } 104 | } 105 | } 106 | 107 | @Test 108 | fun `task passes configuration phase`() { 109 | runTask("help") 110 | } 111 | 112 | @Test 113 | fun `task fails on main sources`() { 114 | main.resolve("java/JavaClass.java") { 115 | writeText(javaClass("JavaClass")) 116 | } 117 | 118 | val result = runTask("assemble", shouldFail = true) 119 | 120 | assertThat(result.task(":module1:forbidJavaFiles")?.outcome).isEqualTo(TaskOutcome.FAILED) 121 | assertThat(result.output).contains("Java files are not allowed within :module1") 122 | } 123 | 124 | @Test 125 | fun `task fails on test sources`() { 126 | test.resolve("java/JavaTest.java") { 127 | writeText(javaClass("JavaTest")) 128 | } 129 | 130 | val result = runTask("assemble", shouldFail = true) 131 | 132 | assertThat(result.task(":module1:forbidJavaFiles")?.outcome).isEqualTo(TaskOutcome.FAILED) 133 | assertThat(result.output).contains("Java files are not allowed within :module1") 134 | } 135 | 136 | @Test 137 | fun `task is cacheable`() { 138 | runTask(":module1:assemble") 139 | 140 | val secondRun = runTask(":module1:assemble") 141 | 142 | assertThat(secondRun.task(":module1:forbidJavaFiles")?.outcome).isNotEqualTo(TaskOutcome.SUCCESS) 143 | } 144 | 145 | @Test 146 | fun `doesn't check generated files`() { 147 | rootDirectory.resolve("build/generated/source/apollo/classes/main/JavaTest.java") { 148 | writeText(javaClass("JavaTest")) 149 | } 150 | 151 | val secondRun = runTask("assemble") 152 | 153 | assertThat(secondRun.task(":module1:forbidJavaFiles")?.outcome).isNotEqualTo(TaskOutcome.SUCCESS) 154 | } 155 | 156 | @Test 157 | fun `does not fail if registered in nested non-android module with android parent`() { 158 | rootDirectory.resolve("parentModule/childModule/src/main/java/JavaClass.java") { 159 | writeText(javaClass("JavaClass")) 160 | } 161 | 162 | val result = runTask(":parentModule:childModule:forbidJavaFiles", shouldFail = true) 163 | 164 | assertThat(result.task(":parentModule:childModule:forbidJavaFiles")?.outcome).isEqualTo(TaskOutcome.FAILED) 165 | } 166 | } 167 | -------------------------------------------------------------------------------- /android/src/test/kotlin/com/project/starter/quality/AndroidQualityPluginTest.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.quality 2 | 3 | import com.project.starter.WithGradleProjectTest 4 | import com.project.starter.kotlinClass 5 | import org.assertj.core.api.Assertions.assertThat 6 | import org.gradle.testkit.runner.TaskOutcome 7 | import org.junit.jupiter.api.BeforeEach 8 | import org.junit.jupiter.api.Test 9 | import java.io.File 10 | 11 | internal class AndroidQualityPluginTest : WithGradleProjectTest() { 12 | 13 | private lateinit var module1Root: File 14 | private lateinit var module2Root: File 15 | 16 | @BeforeEach 17 | fun setUp() { 18 | rootDirectory.apply { 19 | resolve("settings.gradle").writeText( 20 | // language=groovy 21 | """ 22 | plugins { 23 | id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" 24 | } 25 | 26 | dependencyResolutionManagement { 27 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 28 | repositories { 29 | google() 30 | mavenCentral() 31 | } 32 | } 33 | 34 | include ":module1", ":module2" 35 | 36 | """.trimIndent(), 37 | ) 38 | 39 | rootDirectory.resolve("build.gradle").writeText( 40 | // language=groovy 41 | """ 42 | import org.gradle.api.JavaVersion 43 | 44 | plugins { 45 | id('com.starter.config') 46 | } 47 | 48 | commonConfig { 49 | javaVersion = JavaVersion.VERSION_11 50 | } 51 | """.trimIndent(), 52 | ) 53 | module1Root = resolve("module1") { 54 | resolve("build.gradle") { 55 | writeText( 56 | // language=groovy 57 | """ 58 | plugins { 59 | id('com.starter.library.android') 60 | } 61 | 62 | android { 63 | namespace "com.example.module1" 64 | } 65 | 66 | """.trimIndent(), 67 | ) 68 | } 69 | 70 | resolve("src/main/kotlin/com/example/ValidKotlinFile1.kt") { 71 | writeText(kotlinClass("ValidKotlinFile1")) 72 | } 73 | resolve("src/test/kotlin/com/example/ValidKotlinTest1.kt") { 74 | writeText(kotlinClass("ValidKotlinTest1")) 75 | } 76 | } 77 | module2Root = resolve("module2").apply { 78 | mkdirs() 79 | val script = 80 | // language=groovy 81 | """ 82 | import org.gradle.api.JavaVersion 83 | import org.jetbrains.kotlin.gradle.dsl.KotlinCompile 84 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 85 | 86 | plugins { 87 | id('com.starter.quality') 88 | id('com.android.library') 89 | id('kotlin-android') 90 | } 91 | 92 | def targetJavaVersion = JavaVersion.VERSION_11 93 | android { 94 | namespace "com.example.module2" 95 | compileSdkVersion 34 96 | 97 | defaultConfig { 98 | minSdkVersion 31 99 | } 100 | compileOptions { 101 | sourceCompatibility = targetJavaVersion 102 | targetCompatibility = targetJavaVersion 103 | } 104 | } 105 | 106 | kotlin { 107 | jvmToolchain(23) 108 | } 109 | 110 | tasks.withType(KotlinCompile).configureEach { 111 | compilerOptions.jvmTarget = JvmTarget.@Companion.fromTarget(targetJavaVersion.toString()) 112 | } 113 | 114 | """.trimIndent() 115 | resolve("build.gradle").writeText(script) 116 | 117 | resolve("src/main/kotlin/com/example/ValidKotlinFile2.kt") { 118 | writeText(kotlinClass("ValidKotlinFile2")) 119 | } 120 | resolve("src/test/kotlin/com/example/ValidKotlinTest2.kt") { 121 | writeText(kotlinClass("ValidKotlinTest2")) 122 | } 123 | } 124 | } 125 | } 126 | 127 | @Test 128 | fun `projectCodeStyle runs Detekt`() { 129 | val result = runTask("projectCodeStyle") 130 | 131 | assertThat(result.task(":module1:detekt")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 132 | assertThat(result.task(":module2:detekt")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 133 | } 134 | 135 | @Test 136 | fun `projectCodeStyle runs ktlint`() { 137 | val result = runTask("projectCodeStyle") 138 | 139 | assertThat(result.task(":module1:lintKotlinMain")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 140 | assertThat(result.task(":module1:lintKotlinTest")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 141 | assertThat(result.task(":module2:lintKotlinMain")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 142 | assertThat(result.task(":module2:lintKotlinTest")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 143 | } 144 | 145 | @Test 146 | fun `formatOnCompile option enables failing builds if code style errors found`() { 147 | val enableFormatOnCompile = { 148 | val buildscript = 149 | // language=groovy 150 | """ 151 | import org.gradle.api.JavaVersion 152 | 153 | plugins { 154 | id('com.starter.config') 155 | } 156 | 157 | commonConfig { 158 | javaVersion = JavaVersion.VERSION_17 159 | qualityPlugin { 160 | formatOnCompile = true 161 | } 162 | } 163 | """.trimIndent() 164 | rootDirectory.resolve("build.gradle").writeText(buildscript) 165 | } 166 | 167 | module1Root.resolve("src/main/kotlin/WrongFileName.kt") { 168 | writeText(kotlinClass("DifferentClassName")) 169 | } 170 | 171 | val formatOnCompileOff = runTask("assemble") 172 | 173 | assertThat(formatOnCompileOff.task(":module1:formatKotlin")?.outcome).isNull() 174 | assertThat(formatOnCompileOff.task(":module2:formatKotlin")?.outcome).isNull() 175 | 176 | enableFormatOnCompile() 177 | val formatOnCompileOn = runTask("assemble") 178 | 179 | assertThat(formatOnCompileOn.task(":module1:formatKotlin")?.outcome).isNotNull() 180 | assertThat(formatOnCompileOn.task(":module2:formatKotlin")?.outcome).isNotNull() 181 | } 182 | 183 | @Test 184 | fun `detekt fails on invalid class name`() { 185 | module2Root.resolve("src/main/kotlin/com/example/MagicNumber.kt") { 186 | val kotlinClass = 187 | // language=kotlin 188 | """ 189 | class invalidClassName { 190 | var value: Int = 16 191 | } 192 | 193 | """.trimIndent() 194 | writeText(kotlinClass) 195 | } 196 | 197 | val result = runTask("projectCodeStyle", shouldFail = true) 198 | 199 | assertThat(result.task(":module2:detekt")?.outcome).isEqualTo(TaskOutcome.FAILED) 200 | } 201 | } 202 | -------------------------------------------------------------------------------- /android/src/test/kotlin/com/project/starter/quality/tasks/IssueLinksCheckerTaskTest.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.quality.tasks 2 | 3 | import com.project.starter.WithGradleProjectTest 4 | import com.project.starter.javaClass 5 | import org.assertj.core.api.Assertions.assertThat 6 | import org.assertj.core.api.SoftAssertions.assertSoftly 7 | import org.gradle.testkit.runner.TaskOutcome 8 | import org.junit.jupiter.api.BeforeEach 9 | import org.junit.jupiter.api.Disabled 10 | import org.junit.jupiter.api.Test 11 | import java.io.File 12 | 13 | internal class IssueLinksCheckerTaskTest : WithGradleProjectTest() { 14 | 15 | lateinit var androidModuleRoot: File 16 | lateinit var kotlinModuleRoot: File 17 | 18 | @BeforeEach 19 | fun setUp() { 20 | rootDirectory.apply { 21 | resolve("settings.gradle").writeText("""include ':module1', ':module2' """) 22 | 23 | resolve("build.gradle").writeText("") 24 | androidModuleRoot = resolve("module1") { 25 | val script = 26 | // language=groovy 27 | """ 28 | plugins { 29 | id 'com.starter.library.android' 30 | } 31 | 32 | android { 33 | namespace "com.example.module1" 34 | } 35 | 36 | """.trimIndent() 37 | resolve("build.gradle") { 38 | writeText(script) 39 | } 40 | resolve("src/main/java/ValidJava2.java") { 41 | writeText(javaClass("ValidJava2")) 42 | } 43 | resolve("src/test/java/com/example/ValidJavaTest2.java") { 44 | writeText(javaClass("ValidJavaTest2")) 45 | } 46 | } 47 | kotlinModuleRoot = resolve("module2") { 48 | val script = 49 | // language=groovy 50 | """ 51 | plugins { 52 | id 'com.starter.library.kotlin' 53 | } 54 | 55 | """.trimIndent() 56 | resolve("build.gradle") { 57 | writeText(script) 58 | } 59 | resolve("src/main/java/ValidJava2.java") { 60 | writeText(javaClass("ValidJava2")) 61 | } 62 | resolve("src/test/java/com/example/ValidJavaTest2.java") { 63 | writeText(javaClass("ValidJavaTest2")) 64 | } 65 | } 66 | } 67 | } 68 | 69 | @Test 70 | fun `does not warn on regular project`() { 71 | androidModuleRoot.resolve("src/main/kotlin/com/example/ValidKotlin.kt") { 72 | val randomLinks = 73 | // language=kotlin 74 | """ 75 | /** 76 | * https://issuetracker.google.com/issues/145439806 77 | **/ 78 | object ValidKotlin { 79 | // https://www.example.com 80 | } 81 | """.trimIndent() 82 | writeText(randomLinks) 83 | } 84 | 85 | val result = runTask("issueLinksReport") 86 | 87 | assertThat(result.task(":module1:issueLinksReport")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 88 | } 89 | 90 | @Test 91 | @Disabled("Google Issue tracker is not supported yet") 92 | fun `reports issuetracker issues`() { 93 | androidModuleRoot.resolve("src/main/kotlin/com/example/ValidKotlin.kt") { 94 | val randomLinks = 95 | // language=kotlin 96 | """ 97 | /** 98 | * https://news.ycombinator.com/ 99 | **/ 100 | object ValidKotlin { 101 | // https://issuetracker.google.com/issues/121092282 102 | val animations = 0 // Set animation: https://issuetracker.google.com/issues/154643058 103 | } 104 | """.trimIndent() 105 | writeText(randomLinks) 106 | } 107 | 108 | val result = runTask("issueLinksReport") 109 | 110 | assertThat(result.task(":module1:issueLinksReport")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 111 | } 112 | 113 | @Test 114 | fun `reports youtrack issues`() { 115 | val randomLinks = 116 | // language=kotlin 117 | """ 118 | /** 119 | * https://news.ycombinator.com/ 120 | * https://youtrack.jetbrains.com/issue/KT-31666 121 | **/ 122 | object ValidKotlin { 123 | // https://youtrack.jetbrains.com/issue/KT-34230 124 | } 125 | """.trimIndent() 126 | 127 | assertSoftly { softly -> 128 | listOf("module1" to androidModuleRoot, "module2" to kotlinModuleRoot).forEach { (name, folder) -> 129 | folder.resolve("src/main/kotlin/com/example/ValidKotlin.kt") { 130 | writeText(randomLinks) 131 | } 132 | 133 | val result = runTask(":$name:issueLinksReport") 134 | 135 | softly.assertThat(androidModuleRoot.resolve("build/reports/issue_comments.txt")) 136 | .hasContent( 137 | """ 138 | 👉 https://youtrack.jetbrains.com/issue/KT-31666 (Closed) 139 | ✅ https://youtrack.jetbrains.com/issue/KT-34230 (Opened) 140 | """.trimIndent(), 141 | ) 142 | softly.assertThat(result.output).contains("\uD83D\uDC49 https://youtrack.jetbrains.com/issue/KT-31666 (Closed)") 143 | softly.assertThat(result.output).contains("✅ https://youtrack.jetbrains.com/issue/KT-34230 (Opened)") 144 | softly.assertThat(result.task(":$name:issueLinksReport")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 145 | } 146 | } 147 | } 148 | 149 | @Test 150 | fun `reports github issues`() { 151 | androidModuleRoot.resolve("src/main/kotlin/com/example/ValidKotlin.kt") { 152 | val randomLinks = 153 | // language=kotlin 154 | """ 155 | /** 156 | * https://github.com/isaacs/github/issues/5 157 | **/ 158 | object ValidKotlin { 159 | // https://www.example.com 160 | // https://github.com/apollographql/apollo-android/issues/2207 <- closed 161 | } 162 | """.trimIndent() 163 | writeText(randomLinks) 164 | } 165 | 166 | val result = runTask("issueLinksReport") 167 | 168 | assertThat(androidModuleRoot.resolve("build/reports/issue_comments.txt")) 169 | .hasContent( 170 | """ 171 | ✅ https://github.com/isaacs/github/issues/5 (Opened) 172 | 👉 https://github.com/apollographql/apollo-android/issues/2207 (Closed) 173 | """.trimIndent(), 174 | ) 175 | assertThat(result.output).contains("✅ https://github.com/isaacs/github/issues/5 (Opened)") 176 | assertThat(result.output).contains("\uD83D\uDC49 https://github.com/apollographql/apollo-android/issues/2207 (Closed)") 177 | assertThat(result.task(":module1:issueLinksReport")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /android/src/test/kotlin/com/project/starter/versioning/AndroidVersioningPluginTest.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.versioning 2 | 3 | import com.project.starter.WithGradleProjectTest 4 | import com.project.starter.commit 5 | import com.project.starter.setupGit 6 | import com.project.starter.tag 7 | import org.assertj.core.api.Assertions.assertThat 8 | import org.junit.jupiter.api.BeforeEach 9 | import org.junit.jupiter.api.Test 10 | import java.io.File 11 | 12 | internal class AndroidVersioningPluginTest : WithGradleProjectTest() { 13 | 14 | private lateinit var androidAppRoot: File 15 | 16 | @BeforeEach 17 | fun setUp() { 18 | rootDirectory.apply { 19 | resolve("settings.gradle").writeText("""include ":androidApp"""") 20 | 21 | androidAppRoot = resolve("androidApp") { 22 | resolve("build.gradle") { 23 | // language=groovy 24 | val buildscript = 25 | """ 26 | plugins { 27 | id 'com.starter.application.android' 28 | } 29 | 30 | android { 31 | namespace "com.example.module1" 32 | } 33 | 34 | android { 35 | namespace "com.example.module1" 36 | } 37 | 38 | tasks.register("printVersion") { 39 | doLast { 40 | println("version_code=" + android.defaultConfig.versionCode) 41 | println("version_name=" + android.defaultConfig.versionName) 42 | } 43 | } 44 | """.trimIndent() 45 | writeText(buildscript) 46 | } 47 | } 48 | } 49 | } 50 | 51 | @Test 52 | internal fun `sets android application version`() { 53 | rootDirectory.resolve("build.gradle").writeText( 54 | // language=groovy 55 | """ 56 | plugins { 57 | id 'com.starter.versioning' 58 | } 59 | """.trimIndent(), 60 | ) 61 | val git = setupGit() 62 | git.commit("contains 1.2.3 features") 63 | git.tag("v1.2.3") 64 | 65 | val result = runTask("printVersion") 66 | 67 | assertThat(result.output).contains("version_name=1.2.3") 68 | assertThat(result.output).contains("version_code=1002003") 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.plugin.KotlinPluginWrapperKt 2 | 3 | plugins { 4 | alias(libs.plugins.starter.config) 5 | alias(libs.plugins.starter.versioning) 6 | alias(libs.plugins.kotlin.jvm) apply(false) 7 | alias(libs.plugins.starter.library.kotlin) apply(false) 8 | alias(libs.plugins.kotlinx.binarycompatibility) apply(false) 9 | } 10 | 11 | commonConfig { 12 | javaFilesAllowed false 13 | javaVersion JavaVersion.VERSION_11 14 | } 15 | 16 | allprojects { 17 | pluginManager.withPlugin("org.jetbrains.kotlin.jvm") { plugin -> 18 | def kotlinVersion = KotlinPluginWrapperKt.getKotlinPluginVersion(project) 19 | configurations.matching { it.name != "detekt" }.configureEach { 20 | resolutionStrategy.eachDependency { 21 | if (requested.group == 'org.jetbrains.kotlin' && requested.name.startsWith("kotlin")) { 22 | useVersion kotlinVersion 23 | } 24 | } 25 | } 26 | kotlin { 27 | jvmToolchain(libs.versions.java.compilation.get().toInteger()) 28 | } 29 | 30 | tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask).configureEach { 31 | compilerOptions { 32 | freeCompilerArgs.add("-Xlambdas=class") 33 | } 34 | } 35 | } 36 | pluginManager.withPlugin("java-gradle-plugin") { 37 | configurations { 38 | register("testRuntimeDependencies") { 39 | attributes { 40 | // KGP publishes multiple variants https://kotlinlang.org/docs/whatsnew17.html#support-for-gradle-plugin-variants 41 | attribute(Usage.USAGE_ATTRIBUTE, project.objects.named(Usage.class, Usage.JAVA_RUNTIME)) 42 | attribute(Category.CATEGORY_ATTRIBUTE, project.objects.named(Category.class, Category.LIBRARY)) 43 | } 44 | } 45 | } 46 | 47 | // Required to put the Kotlin plugin on the classpath for the functional test suite 48 | tasks.withType(PluginUnderTestMetadata).configureEach { 49 | pluginClasspath.from(configurations.testRuntimeDependencies) 50 | } 51 | 52 | } 53 | tasks.withType(Test).configureEach { 54 | doLast { 55 | Thread.sleep(2000) // https://github.com/gradle/gradle/issues/16603 56 | } 57 | } 58 | 59 | pluginManager.withPlugin("java") { 60 | if (project.hasProperty("skipJarVersion")) { 61 | def projectName = project.name 62 | tasks.named("jar") { 63 | archiveFile.set(layout.buildDirectory.map {it.file("libs/${projectName}.jar")}) 64 | } 65 | } 66 | } 67 | 68 | pluginManager.withPlugin("io.github.usefulness.ktlint-gradle-plugin") { 69 | ktlint { 70 | ktlintVersion = libs.versions.maven.ktlint.get() 71 | } 72 | } 73 | 74 | pluginManager.withPlugin(libs.plugins.kotlin.samwithreceiver.get().pluginId) { 75 | samWithReceiver { 76 | annotation("org.gradle.api.HasImplicitReceiver") 77 | } 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /config/api/config.api: -------------------------------------------------------------------------------- 1 | public final class com/project/starter/config/PluginUtilsKt$sam$i$org_gradle_api_Action$0 : org/gradle/api/Action { 2 | public fun (Lkotlin/jvm/functions/Function1;)V 3 | public final synthetic fun execute (Ljava/lang/Object;)V 4 | } 5 | 6 | public class com/project/starter/config/extensions/AndroidPluginConfig { 7 | public fun ()V 8 | public fun (IILjava/lang/Integer;)V 9 | public synthetic fun (IILjava/lang/Integer;ILkotlin/jvm/internal/DefaultConstructorMarker;)V 10 | public final fun compileSdkVersion (I)V 11 | public final fun getCompileSdkVersion ()I 12 | public final fun getMinSdkVersion ()I 13 | public final fun getTargetSdkVersion ()Ljava/lang/Integer; 14 | public final fun minSdkVersion (I)V 15 | public final fun setCompileSdkVersion (I)V 16 | public final fun setMinSdkVersion (I)V 17 | public final fun setTargetSdkVersion (Ljava/lang/Integer;)V 18 | public final fun targetSdkVersion (I)V 19 | } 20 | 21 | public class com/project/starter/config/extensions/QualityPluginConfig { 22 | public fun ()V 23 | public fun (Z)V 24 | public synthetic fun (ZILkotlin/jvm/internal/DefaultConstructorMarker;)V 25 | public final fun formatOnCompile (Z)V 26 | public final fun getFormatOnCompile ()Z 27 | public final fun setFormatOnCompile (Z)V 28 | } 29 | 30 | public class com/project/starter/config/extensions/RootConfigExtension { 31 | public fun ()V 32 | public fun (Lorg/gradle/api/JavaVersion;Z)V 33 | public synthetic fun (Lorg/gradle/api/JavaVersion;ZILkotlin/jvm/internal/DefaultConstructorMarker;)V 34 | public final fun androidPlugin (Lorg/gradle/api/Action;)V 35 | public final fun getAndroid ()Lcom/project/starter/config/extensions/AndroidPluginConfig; 36 | public final fun getJavaFilesAllowed ()Z 37 | public final fun getJavaVersion ()Lorg/gradle/api/JavaVersion; 38 | public final fun getQuality ()Lcom/project/starter/config/extensions/QualityPluginConfig; 39 | public final fun getVersioning ()Lcom/project/starter/config/extensions/VersioningPluginConfig; 40 | public final fun javaFilesAllowed (Z)V 41 | public final fun javaVersion (Lorg/gradle/api/JavaVersion;)V 42 | public final fun qualityPlugin (Lorg/gradle/api/Action;)V 43 | public final fun setJavaFilesAllowed (Z)V 44 | public final fun setJavaVersion (Lorg/gradle/api/JavaVersion;)V 45 | public final fun versioningPlugin (Lorg/gradle/api/Action;)V 46 | } 47 | 48 | public class com/project/starter/config/extensions/VersioningPluginConfig { 49 | public fun ()V 50 | } 51 | 52 | public final class com/project/starter/config/plugins/CommonSettingsPlugin : org/gradle/api/Plugin { 53 | public fun ()V 54 | public synthetic fun apply (Ljava/lang/Object;)V 55 | public fun apply (Lorg/gradle/api/Project;)V 56 | } 57 | 58 | public final class com/project/starter/config/plugins/CommonSettingsPluginKt { 59 | public static final fun getRootConfig (Lorg/gradle/api/Project;)Lcom/project/starter/config/extensions/RootConfigExtension; 60 | } 61 | 62 | -------------------------------------------------------------------------------- /config/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java-gradle-plugin") 3 | alias(libs.plugins.droidsonroids.jacocotestkit) 4 | alias(libs.plugins.starter.library.kotlin) 5 | alias(libs.plugins.kotlin.samwithreceiver) 6 | alias(libs.plugins.kotlinx.binarycompatibility) 7 | id("com.starter.publishing") 8 | } 9 | 10 | dependencies { 11 | testRuntimeOnly(libs.junit.platform.launcher) 12 | testImplementation project(":testing") 13 | } 14 | 15 | tasks.named("test") { 16 | useJUnitPlatform() 17 | } 18 | 19 | gradlePlugin { 20 | plugins { 21 | commonConfig { 22 | id = 'com.starter.config' 23 | displayName = 'Common Configuration Plugin' 24 | implementationClass = 'com.project.starter.config.plugins.CommonSettingsPlugin' 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /config/src/main/kotlin/com/project/starter/config/PluginUtils.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.config 2 | 3 | import org.gradle.api.Project 4 | import org.gradle.api.plugins.ExtensionContainer 5 | 6 | inline fun ExtensionContainer.getByType(): T = getByType(T::class.java) 7 | 8 | inline fun ExtensionContainer.findByType() = findByType(T::class.java) 9 | 10 | inline fun Project.withExtension(crossinline action: Project.(T) -> Unit) = afterEvaluate { 11 | action(extensions.getByType()) 12 | } 13 | -------------------------------------------------------------------------------- /config/src/main/kotlin/com/project/starter/config/extensions/RootConfigExtension.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.config.extensions 2 | 3 | import org.gradle.api.Action 4 | import org.gradle.api.JavaVersion 5 | 6 | open class RootConfigExtension( 7 | var javaVersion: JavaVersion = JavaVersion.VERSION_11, 8 | var javaFilesAllowed: Boolean = false, 9 | ) { 10 | 11 | val quality = QualityPluginConfig() 12 | val android = AndroidPluginConfig() 13 | val versioning = VersioningPluginConfig() 14 | 15 | fun qualityPlugin(action: Action) = action.execute(quality) 16 | 17 | fun androidPlugin(action: Action) = action.execute(android) 18 | 19 | fun versioningPlugin(action: Action) = action.execute(versioning) 20 | 21 | fun javaVersion(value: JavaVersion) { 22 | javaVersion = value 23 | } 24 | 25 | fun javaFilesAllowed(value: Boolean) { 26 | javaFilesAllowed = value 27 | } 28 | } 29 | 30 | open class QualityPluginConfig(var formatOnCompile: Boolean = false) { 31 | fun formatOnCompile(value: Boolean) { 32 | formatOnCompile = value 33 | } 34 | } 35 | 36 | open class AndroidPluginConfig( 37 | var compileSdkVersion: Int = 35, 38 | var minSdkVersion: Int = 26, 39 | var targetSdkVersion: Int? = null, 40 | ) { 41 | 42 | fun compileSdkVersion(value: Int) { 43 | compileSdkVersion = value 44 | } 45 | 46 | fun minSdkVersion(value: Int) { 47 | minSdkVersion = value 48 | } 49 | 50 | fun targetSdkVersion(value: Int) { 51 | targetSdkVersion = value 52 | } 53 | } 54 | 55 | open class VersioningPluginConfig 56 | -------------------------------------------------------------------------------- /config/src/main/kotlin/com/project/starter/config/plugins/CommonSettingsPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.config.plugins 2 | 3 | import com.project.starter.config.extensions.RootConfigExtension 4 | import org.gradle.api.GradleException 5 | import org.gradle.api.Plugin 6 | import org.gradle.api.Project 7 | 8 | class CommonSettingsPlugin : Plugin { 9 | 10 | override fun apply(target: Project): Unit = with(target) { 11 | if (this != rootProject) { 12 | throw GradleException("Common configuration can be applied to the root project only") 13 | } 14 | extensions.create("commonConfig", RootConfigExtension::class.java) 15 | } 16 | } 17 | 18 | val Project.rootConfig: RootConfigExtension 19 | get() = rootProject.extensions.findByType(RootConfigExtension::class.java) 20 | ?: RootConfigExtension() 21 | -------------------------------------------------------------------------------- /config/src/test/kotlin/com/project/starter/plugins/CommonSettingsPluginTest.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.plugins 2 | 3 | import com.project.starter.WithGradleProjectTest 4 | import com.project.starter.kotlinClass 5 | import org.assertj.core.api.Assertions.assertThat 6 | import org.gradle.testkit.runner.TaskOutcome 7 | import org.junit.jupiter.api.BeforeEach 8 | import org.junit.jupiter.api.Test 9 | import java.io.File 10 | 11 | internal class CommonSettingsPluginTest : WithGradleProjectTest() { 12 | 13 | lateinit var rootBuildScript: File 14 | lateinit var module1Root: File 15 | 16 | @BeforeEach 17 | fun setUp() { 18 | rootDirectory.apply { 19 | resolve("settings.gradle").writeText("""include ":module1" """) 20 | 21 | rootBuildScript = resolve("build.gradle") 22 | module1Root = resolve("module1") { 23 | resolve("build.gradle") { 24 | writeText( 25 | """ 26 | plugins { 27 | id('org.jetbrains.kotlin.jvm') version "1.9.21" 28 | } 29 | 30 | """.trimIndent(), 31 | ) 32 | } 33 | resolve("src/main/kotlin/com/example/ValidKotlinFile1.kt") { 34 | writeText(kotlinClass("ValidKotlinFile1")) 35 | } 36 | } 37 | } 38 | } 39 | 40 | @Test 41 | fun `configures common config extension using property syntax`() { 42 | // language=groovy 43 | val buildscript = 44 | """ 45 | plugins { 46 | id('com.starter.config') 47 | } 48 | 49 | commonConfig { 50 | javaVersion = JavaVersion.VERSION_11 51 | javaFilesAllowed = false 52 | androidPlugin { 53 | compileSdkVersion = 34 54 | minSdkVersion = 31 55 | targetSdkVersion = 34 56 | } 57 | qualityPlugin { 58 | formatOnCompile = true 59 | } 60 | } 61 | """.trimIndent() 62 | rootBuildScript.appendText(buildscript) 63 | 64 | val result = runTask("help") 65 | 66 | assertThat(result.tasks).noneMatch { it.outcome == TaskOutcome.FAILED } 67 | } 68 | 69 | @Test 70 | fun `configures common config extension using function syntax`() { 71 | // language=groovy 72 | val buildscript = 73 | """ 74 | plugins { 75 | id('com.starter.config') 76 | } 77 | 78 | commonConfig { 79 | javaVersion JavaVersion.VERSION_11 80 | javaFilesAllowed false 81 | androidPlugin { 82 | compileSdkVersion 34 83 | minSdkVersion 31 84 | targetSdkVersion 34 85 | } 86 | qualityPlugin { 87 | formatOnCompile true 88 | } 89 | } 90 | """.trimIndent() 91 | rootBuildScript.appendText(buildscript) 92 | 93 | val result = runTask("help") 94 | 95 | assertThat(result.tasks).noneMatch { it.outcome == TaskOutcome.FAILED } 96 | } 97 | 98 | @Test 99 | fun `throws exception if not applied to the root project`() { 100 | // language=groovy 101 | val buildscript = 102 | """ 103 | plugins { 104 | id('com.starter.config') 105 | } 106 | """.trimIndent() 107 | module1Root.resolve("build.gradle").appendText(buildscript) 108 | 109 | val result = runTask("build", shouldFail = true) 110 | 111 | assertThat(result.output).contains("Failed to apply plugin 'com.starter.config'") 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.jvmargs="-XX:+UseParallelGC" 2 | org.gradle.parallel=true 3 | org.gradle.caching=true 4 | org.gradle.vfs.watch=true 5 | -------------------------------------------------------------------------------- /gradle/libs.versions.toml: -------------------------------------------------------------------------------- 1 | [versions] 2 | java-compilation = "23" 3 | google-agp = "8.10.1" 4 | gradle-starter = "0.84.1" 5 | gradle-gradlepublish = "1.3.1" 6 | gradle-jacocotestkit = "1.0.12" 7 | gradle-detekt = "1.23.8" 8 | gradle-doctor = "0.11.0" 9 | mavencentral-kotlin = "2.1.21" 10 | mavencentral-issuechecker = "0.4.0" 11 | mavencentral-junit = "5.13.1" 12 | mavencentral-assertj = "3.27.3" 13 | mavencentral-jgit = "7.3.0.202506031305-r" 14 | mavencentral-ktlint-gradle = "0.10.0" 15 | maven-binarycompatiblity = "0.17.0" 16 | maven-dokka = "2.0.0" 17 | maven-ktlint = "1.6.0" 18 | 19 | [libraries] 20 | agp-gradle-implementation = { module = "com.android.tools.build:gradle", version.ref = "google-agp" } # TODO remove 21 | agp-gradle-api = { module = "com.android.tools.build:gradle-api", version.ref = "google-agp" } 22 | jetbrains-kotlin-jvm-implementation = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin", version.ref = "mavencentral-kotlin" } 23 | jetbrains-kotlin-jvm-api = { module = "org.jetbrains.kotlin:kotlin-gradle-plugin-api", version.ref = "mavencentral-kotlin" } 24 | publishplugin-gradle = { module = "com.gradle.publish:plugin-publish-plugin", version.ref = "gradle-gradlepublish" } 25 | usefulness-issuechecker = { module = "com.github.usefulness:issuechecker", version.ref = "mavencentral-issuechecker" } 26 | usefulness-ktlint = { module = "io.github.usefulness:ktlint-gradle-plugin", version.ref = "mavencentral-ktlint-gradle" } 27 | detekt-gradle = { module = "io.gitlab.arturbosch.detekt:detekt-gradle-plugin", version.ref = "gradle-detekt" } 28 | junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "mavencentral-junit" } 29 | assertj-core = { module = "org.assertj:assertj-core", version.ref = "mavencentral-assertj" } 30 | eclipse-jgit = { module = "org.eclipse.jgit:org.eclipse.jgit", version.ref = "mavencentral-jgit" } 31 | jetbrains-dokka = { module = "org.jetbrains.dokka:dokka-gradle-plugin", version.ref = "maven-dokka" } 32 | ktlint-cli = { module = "com.pinterest.ktlint:ktlint-cli", version.ref = "maven-ktlint" } 33 | junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher" } 34 | 35 | [plugins] 36 | kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "mavencentral-kotlin" } 37 | droidsonroids-jacocotestkit = { id = "pl.droidsonroids.jacoco.testkit", version.ref = "gradle-jacocotestkit" } 38 | starter-config = { id = "com.starter.config", version.ref = "gradle-starter" } 39 | starter-library-kotlin = { id = "com.starter.library.kotlin", version.ref = "gradle-starter" } 40 | starter-versioning = { id = "com.starter.versioning", version.ref = "gradle-starter" } 41 | kotlin-samwithreceiver = { id = "org.jetbrains.kotlin.plugin.sam.with.receiver", version.ref = "mavencentral-kotlin" } 42 | osacky-doctor = { id = "com.osacky.doctor", version.ref = "gradle-doctor" } 43 | kotlinx-binarycompatibility = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "maven-binarycompatiblity" } 44 | -------------------------------------------------------------------------------- /gradle/plugins/build.gradle: -------------------------------------------------------------------------------- 1 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 2 | import org.jetbrains.kotlin.gradle.dsl.KotlinCompile 3 | 4 | plugins { 5 | id 'java-gradle-plugin' 6 | alias(libs.plugins.starter.library.kotlin) 7 | alias(libs.plugins.osacky.doctor) 8 | } 9 | 10 | dependencies { 11 | implementation libs.publishplugin.gradle 12 | implementation libs.jetbrains.dokka 13 | } 14 | 15 | kotlin { 16 | jvmToolchain(libs.versions.java.compilation.get().toInteger()) 17 | } 18 | 19 | gradlePlugin { 20 | plugins { 21 | publishingPlugin { 22 | id = 'com.starter.publishing' 23 | implementationClass = 'PublishingPlugin' 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /gradle/plugins/settings.gradle: -------------------------------------------------------------------------------- 1 | import org.gradle.api.initialization.resolve.RepositoriesMode 2 | 3 | plugins { 4 | id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" 5 | } 6 | 7 | dependencyResolutionManagement { 8 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 9 | repositories { 10 | gradlePluginPortal() 11 | google() 12 | } 13 | versionCatalogs { 14 | create("libs") { 15 | from(files("../libs.versions.toml")) 16 | } 17 | } 18 | } 19 | 20 | rootProject.name = "plugins" 21 | -------------------------------------------------------------------------------- /gradle/plugins/src/main/kotlin/PublishingPlugin.kt: -------------------------------------------------------------------------------- 1 | import org.gradle.api.Plugin 2 | import org.gradle.api.Project 3 | import org.gradle.api.plugins.ExtensionContainer 4 | import org.gradle.api.plugins.JavaPluginExtension 5 | import org.gradle.api.publish.PublishingExtension 6 | import org.gradle.api.publish.maven.MavenPublication 7 | import org.gradle.jvm.tasks.Jar 8 | import org.gradle.language.jvm.tasks.ProcessResources 9 | import org.gradle.plugin.devel.GradlePluginDevelopmentExtension 10 | import org.gradle.plugins.signing.SigningExtension 11 | import org.jetbrains.dokka.gradle.DokkaTask 12 | 13 | class PublishingPlugin : Plugin { 14 | 15 | override fun apply(target: Project) = with(target) { 16 | pluginManager.apply("maven-publish") 17 | pluginManager.apply("com.gradle.plugin-publish") 18 | if (findConfig("SIGNING_PASSWORD").isNotEmpty()) { 19 | pluginManager.apply("signing") 20 | } 21 | 22 | extensions.configure { 23 | withSourcesJar() 24 | withJavadocJar() 25 | } 26 | 27 | pluginManager.withPlugin("org.jetbrains.kotlin.jvm") { 28 | pluginManager.apply("org.jetbrains.dokka") 29 | 30 | tasks.withType(DokkaTask::class.java).configureEach { dokkaTask -> 31 | dokkaTask.notCompatibleWithConfigurationCache("https://github.com/Kotlin/dokka/issues/1217") 32 | } 33 | tasks.named("javadocJar", Jar::class.java) { javadocJar -> 34 | javadocJar.from(tasks.named("dokkaJavadoc")) 35 | } 36 | tasks.named("processResources", ProcessResources::class.java) { processResources -> 37 | processResources.from(rootProject.file("LICENSE")) 38 | } 39 | } 40 | 41 | extensions.configure { 42 | with(repositories) { 43 | maven { maven -> 44 | maven.name = "github" 45 | maven.setUrl("https://maven.pkg.github.com/usefulness/project-starter") 46 | with(maven.credentials) { 47 | username = "usefulness" 48 | password = findConfig("GITHUB_TOKEN") 49 | } 50 | } 51 | } 52 | } 53 | pluginManager.withPlugin("signing") { 54 | with(extensions.extraProperties) { 55 | set("signing.keyId", findConfig("SIGNING_KEY_ID")) 56 | set("signing.password", findConfig("SIGNING_PASSWORD")) 57 | set("signing.secretKeyRingFile", findConfig("SIGNING_SECRET_KEY_RING_FILE")) 58 | } 59 | 60 | extensions.configure("signing") { signing -> 61 | if (findConfig("SIGNING_PASSWORD").isNotEmpty()) { 62 | signing.sign(extensions.getByType(PublishingExtension::class.java).publications) 63 | } 64 | } 65 | } 66 | 67 | extensions.configure { 68 | website.set("https://github.com/usefulness/project-starter/") 69 | vcsUrl.set("https://github.com/usefulness/project-starter.git") 70 | plugins.configureEach { plugin -> 71 | plugin.tags.set(listOf("android", "kotlin", "quickstart", "codestyle", "library", "baseline")) 72 | plugin.description = "Set of plugins that might be useful for Multi-Module Android projects." 73 | } 74 | } 75 | } 76 | 77 | private inline fun ExtensionContainer.configure(crossinline receiver: T.() -> Unit) { 78 | configure(T::class.java) { receiver(it) } 79 | } 80 | } 81 | 82 | private fun Project.findConfig(key: String): String { 83 | return findProperty(key)?.toString() ?: System.getenv(key) ?: "" 84 | } 85 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/usefulness/project-starter/175f7d007d094e79c68a380eaed4d419f48de129/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /jvm/api/jvm.api: -------------------------------------------------------------------------------- 1 | public class com/project/starter/modules/extensions/KotlinLibraryConfigExtension : com/project/starter/quality/extensions/JavaSourcesAware { 2 | public fun ()V 3 | public fun (Ljava/lang/Boolean;)V 4 | public synthetic fun (Ljava/lang/Boolean;ILkotlin/jvm/internal/DefaultConstructorMarker;)V 5 | public fun getJavaFilesAllowed ()Ljava/lang/Boolean; 6 | public fun setJavaFilesAllowed (Ljava/lang/Boolean;)V 7 | } 8 | 9 | public final class com/project/starter/modules/internal/KotlinCoverageKt { 10 | public static final fun getDaggerCoverageExclusions ()Ljava/util/List; 11 | } 12 | 13 | public final class com/project/starter/modules/plugins/ConfigurationPlugin : org/gradle/api/Plugin { 14 | public fun ()V 15 | public synthetic fun apply (Ljava/lang/Object;)V 16 | public fun apply (Lorg/gradle/api/Project;)V 17 | } 18 | 19 | public final class com/project/starter/modules/plugins/KotlinLibraryPlugin : org/gradle/api/Plugin { 20 | public fun ()V 21 | public synthetic fun apply (Ljava/lang/Object;)V 22 | public fun apply (Lorg/gradle/api/Project;)V 23 | } 24 | 25 | public final class com/project/starter/modules/plugins/KotlinLibraryPlugin$inlined$sam$i$org_gradle_api_Action$0 : org/gradle/api/Action { 26 | public fun (Lkotlin/jvm/functions/Function1;)V 27 | public final synthetic fun execute (Ljava/lang/Object;)V 28 | } 29 | 30 | public class com/project/starter/modules/tasks/ForbidJavaFilesTask : org/gradle/api/tasks/SourceTask { 31 | public static final field Companion Lcom/project/starter/modules/tasks/ForbidJavaFilesTask$Companion; 32 | public static final field TASK_NAME Ljava/lang/String; 33 | public fun ()V 34 | public final fun run ()V 35 | } 36 | 37 | public final class com/project/starter/modules/tasks/ForbidJavaFilesTask$Companion { 38 | public final fun registerForbidJavaFilesTask (Lorg/gradle/api/Project;Lkotlin/jvm/functions/Function1;)Lorg/gradle/api/tasks/TaskProvider; 39 | public static synthetic fun registerForbidJavaFilesTask$default (Lcom/project/starter/modules/tasks/ForbidJavaFilesTask$Companion;Lorg/gradle/api/Project;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/gradle/api/tasks/TaskProvider; 40 | } 41 | 42 | public class com/project/starter/modules/tasks/ProjectCoverageTask : org/gradle/api/DefaultTask { 43 | public static final field Companion Lcom/project/starter/modules/tasks/ProjectCoverageTask$Companion; 44 | public static final field TASK_NAME Ljava/lang/String; 45 | public fun ()V 46 | } 47 | 48 | public final class com/project/starter/modules/tasks/ProjectCoverageTask$Companion { 49 | public final fun registerProjectCoverageTask (Lorg/gradle/api/Project;Lkotlin/jvm/functions/Function1;)Lorg/gradle/api/tasks/TaskProvider; 50 | public static synthetic fun registerProjectCoverageTask$default (Lcom/project/starter/modules/tasks/ProjectCoverageTask$Companion;Lorg/gradle/api/Project;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/gradle/api/tasks/TaskProvider; 51 | } 52 | 53 | public class com/project/starter/modules/tasks/ProjectLintTask : org/gradle/api/DefaultTask { 54 | public static final field Companion Lcom/project/starter/modules/tasks/ProjectLintTask$Companion; 55 | public static final field TASK_NAME Ljava/lang/String; 56 | public fun ()V 57 | } 58 | 59 | public final class com/project/starter/modules/tasks/ProjectLintTask$Companion { 60 | public final fun registerProjectLintTask (Lorg/gradle/api/Project;Lkotlin/jvm/functions/Function1;)Lorg/gradle/api/tasks/TaskProvider; 61 | public static synthetic fun registerProjectLintTask$default (Lcom/project/starter/modules/tasks/ProjectLintTask$Companion;Lorg/gradle/api/Project;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/gradle/api/tasks/TaskProvider; 62 | } 63 | 64 | public class com/project/starter/modules/tasks/ProjectTestTask : org/gradle/api/DefaultTask { 65 | public static final field Companion Lcom/project/starter/modules/tasks/ProjectTestTask$Companion; 66 | public static final field TASK_NAME Ljava/lang/String; 67 | public fun ()V 68 | } 69 | 70 | public final class com/project/starter/modules/tasks/ProjectTestTask$Companion { 71 | public final fun registerProjectTestTask (Lorg/gradle/api/Project;Lkotlin/jvm/functions/Function1;)Lorg/gradle/api/tasks/TaskProvider; 72 | public static synthetic fun registerProjectTestTask$default (Lcom/project/starter/modules/tasks/ProjectTestTask$Companion;Lorg/gradle/api/Project;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/gradle/api/tasks/TaskProvider; 73 | } 74 | 75 | -------------------------------------------------------------------------------- /jvm/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java-gradle-plugin") 3 | alias(libs.plugins.droidsonroids.jacocotestkit) 4 | alias(libs.plugins.starter.library.kotlin) 5 | alias(libs.plugins.kotlin.samwithreceiver) 6 | alias(libs.plugins.kotlinx.binarycompatibility) 7 | id("com.starter.publishing") 8 | } 9 | 10 | dependencies { 11 | runtimeOnly(libs.jetbrains.kotlin.jvm.implementation) 12 | api(libs.jetbrains.kotlin.jvm.api) 13 | implementation project(":versioning") 14 | implementation project(":quality") 15 | implementation project(":config") 16 | 17 | testRuntimeOnly(libs.junit.platform.launcher) 18 | testImplementation project(":testing") 19 | } 20 | 21 | tasks.named("test") { 22 | useJUnitPlatform() 23 | } 24 | 25 | gradlePlugin { 26 | plugins { 27 | kotlinLibrary { 28 | id = 'com.starter.library.kotlin' 29 | displayName = 'Kotlin Library Plugin' 30 | implementationClass = 'com.project.starter.modules.plugins.KotlinLibraryPlugin' 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /jvm/src/main/kotlin/com/project/starter/modules/extensions/KotlinLibraryConfigExtension.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.modules.extensions 2 | 3 | import com.project.starter.quality.extensions.JavaSourcesAware 4 | 5 | open class KotlinLibraryConfigExtension(override var javaFilesAllowed: Boolean? = null) : JavaSourcesAware 6 | -------------------------------------------------------------------------------- /jvm/src/main/kotlin/com/project/starter/modules/internal/KotlinCoverage.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.modules.internal 2 | 3 | import com.project.starter.config.getByType 4 | import org.gradle.api.Project 5 | import org.gradle.api.tasks.testing.Test 6 | import org.gradle.testing.jacoco.plugins.JacocoPluginExtension 7 | import org.gradle.testing.jacoco.plugins.JacocoTaskExtension 8 | import org.gradle.testing.jacoco.tasks.JacocoReport 9 | 10 | internal fun Project.configureKotlinCoverage() { 11 | pluginManager.apply("jacoco") 12 | 13 | tasks.withType(Test::class.java).configureEach { 14 | extensions.getByType().apply { 15 | isIncludeNoLocationClasses = true 16 | excludes = daggerCoverageExclusions + "jdk.internal.*" 17 | } 18 | } 19 | extensions.configure(JacocoPluginExtension::class.java) { 20 | toolVersion = "0.8.13" 21 | } 22 | tasks.named("jacocoTestReport", JacocoReport::class.java) { 23 | dependsOn("test") 24 | reports { 25 | xml.required.set(true) 26 | html.required.set(true) 27 | } 28 | } 29 | } 30 | 31 | val daggerCoverageExclusions = listOf( 32 | "**/*_MembersInjector.class", 33 | "**/Dagger*Component.class", 34 | "**/Dagger*Component${"$"}Builder.class", 35 | "**/*_*Factory.class", 36 | "**/*_Factory.class", 37 | ) 38 | -------------------------------------------------------------------------------- /jvm/src/main/kotlin/com/project/starter/modules/internal/Repositories.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.modules.internal 2 | 3 | import org.gradle.api.Project 4 | 5 | internal fun Project.configureRepositories(): Unit = with(repositories) { 6 | runCatching { 7 | mavenCentral() 8 | google { 9 | mavenContent { 10 | val googleLibraries = listOf( 11 | "com\\.android.*", 12 | "androidx.*", 13 | "android\\.arch.*", 14 | "com\\.google\\.android.*", 15 | "com\\.google\\.gms", 16 | "com\\.google\\.test.*", 17 | "com\\.google\\.ads.*", 18 | "com\\.google\\.ar.*", 19 | "com\\.google\\.mlkit.*", 20 | "com\\.google\\.devtools.*", 21 | "com\\.google\\.assistant.*", 22 | "com\\.google\\.oboe.*", 23 | "com\\.google\\.prefab.*", 24 | ) 25 | googleLibraries.forEach(::includeGroupByRegex) 26 | } 27 | } 28 | } 29 | .onFailure { logger.info("Build was configured using FAIL_ON_PROJECT_REPOS option") } 30 | } 31 | -------------------------------------------------------------------------------- /jvm/src/main/kotlin/com/project/starter/modules/plugins/ConfigurationPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.modules.plugins 2 | 3 | import com.project.starter.modules.internal.configureRepositories 4 | import org.gradle.api.Plugin 5 | import org.gradle.api.Project 6 | 7 | class ConfigurationPlugin : Plugin { 8 | 9 | override fun apply(target: Project): Unit = with(target) { 10 | configureRepositories() 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /jvm/src/main/kotlin/com/project/starter/modules/plugins/KotlinLibraryPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.modules.plugins 2 | 3 | import com.project.starter.config.findByType 4 | import com.project.starter.config.plugins.rootConfig 5 | import com.project.starter.config.withExtension 6 | import com.project.starter.modules.extensions.KotlinLibraryConfigExtension 7 | import com.project.starter.modules.internal.configureKotlinCoverage 8 | import com.project.starter.modules.tasks.ForbidJavaFilesTask.Companion.registerForbidJavaFilesTask 9 | import com.project.starter.modules.tasks.ProjectCoverageTask.Companion.registerProjectCoverageTask 10 | import com.project.starter.modules.tasks.ProjectTestTask.Companion.registerProjectTestTask 11 | import org.gradle.api.Plugin 12 | import org.gradle.api.Project 13 | import org.gradle.api.tasks.SourceSetContainer 14 | import org.gradle.api.tasks.compile.JavaCompile 15 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 16 | import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile 17 | 18 | class KotlinLibraryPlugin : Plugin { 19 | 20 | override fun apply(target: Project) = with(target) { 21 | pluginManager.apply("org.jetbrains.kotlin.jvm") 22 | pluginManager.apply("com.starter.quality") 23 | pluginManager.apply(ConfigurationPlugin::class.java) 24 | 25 | extensions.create("projectConfig", KotlinLibraryConfigExtension::class.java) 26 | 27 | tasks.withType(KotlinJvmCompile::class.java).configureEach { 28 | compilerOptions.jvmTarget.set(JvmTarget.fromTarget(rootConfig.javaVersion.toString())) 29 | } 30 | tasks.withType(JavaCompile::class.java).configureEach { 31 | options.release.set(rootConfig.javaVersion.majorVersion.toInt()) 32 | } 33 | registerProjectTestTask { 34 | it.dependsOn("test") 35 | } 36 | 37 | configureKotlinCoverage() 38 | registerProjectCoverageTask { projectCoverage -> 39 | projectCoverage.dependsOn("jacocoTestReport") 40 | } 41 | withExtension { config -> 42 | val javaFilesAllowed = config.javaFilesAllowed ?: rootConfig.javaFilesAllowed 43 | if (!javaFilesAllowed) { 44 | val forbidJavaFiles = registerForbidJavaFilesTask { task -> 45 | project.extensions.findByType()?.configureEach { 46 | if (name == "main" || name == "test") { 47 | task.source += java 48 | } 49 | } 50 | } 51 | tasks.named("compileKotlin") { 52 | dependsOn(forbidJavaFiles) 53 | } 54 | } 55 | } 56 | 57 | pluginManager.withPlugin("java-gradle-plugin") { 58 | tasks.withType(org.jetbrains.kotlin.gradle.tasks.KotlinCompilationTask::class.java).configureEach { 59 | compilerOptions { 60 | freeCompilerArgs.add("-Xlambdas=class") 61 | } 62 | } 63 | } 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /jvm/src/main/kotlin/com/project/starter/modules/tasks/ForbidJavaFilesTask.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.modules.tasks 2 | 3 | import org.gradle.api.GradleException 4 | import org.gradle.api.Project 5 | import org.gradle.api.tasks.CacheableTask 6 | import org.gradle.api.tasks.SourceTask 7 | import org.gradle.api.tasks.TaskAction 8 | import org.gradle.api.tasks.TaskProvider 9 | 10 | @CacheableTask 11 | open class ForbidJavaFilesTask : SourceTask() { 12 | 13 | @TaskAction 14 | fun run() { 15 | source.visit { 16 | if (name.endsWith(".java")) { 17 | logger.error("Error at $file") 18 | throw GradleException("Java files are not allowed within ${project.path}") 19 | } 20 | } 21 | } 22 | 23 | companion object { 24 | 25 | const val TASK_NAME = "forbidJavaFiles" 26 | 27 | fun Project.registerForbidJavaFilesTask(action: (ForbidJavaFilesTask) -> Unit = {}): TaskProvider = 28 | tasks.register(TASK_NAME, ForbidJavaFilesTask::class.java, action) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /jvm/src/main/kotlin/com/project/starter/modules/tasks/ProjectCoverageTask.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.modules.tasks 2 | 3 | import org.gradle.api.DefaultTask 4 | import org.gradle.api.Project 5 | import org.gradle.api.tasks.TaskProvider 6 | 7 | open class ProjectCoverageTask : DefaultTask() { 8 | 9 | init { 10 | description = "Generates code coverage report for the project" 11 | group = "quality" 12 | } 13 | 14 | companion object { 15 | 16 | const val TASK_NAME = "projectCoverage" 17 | 18 | fun Project.registerProjectCoverageTask(action: (ProjectCoverageTask) -> Unit = {}): TaskProvider = 19 | tasks.register(TASK_NAME, ProjectCoverageTask::class.java, action) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /jvm/src/main/kotlin/com/project/starter/modules/tasks/ProjectLintTask.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.modules.tasks 2 | 3 | import org.gradle.api.DefaultTask 4 | import org.gradle.api.Project 5 | import org.gradle.api.tasks.TaskProvider 6 | 7 | open class ProjectLintTask : DefaultTask() { 8 | 9 | init { 10 | description = "Runs Android Lint checks against the whole project" 11 | group = "quality" 12 | } 13 | 14 | companion object { 15 | 16 | const val TASK_NAME = "projectLint" 17 | 18 | fun Project.registerProjectLintTask(action: (ProjectLintTask) -> Unit = {}): TaskProvider = 19 | tasks.register(TASK_NAME, ProjectLintTask::class.java, action) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /jvm/src/main/kotlin/com/project/starter/modules/tasks/ProjectTestTask.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.modules.tasks 2 | 3 | import org.gradle.api.DefaultTask 4 | import org.gradle.api.Project 5 | import org.gradle.api.tasks.TaskProvider 6 | 7 | open class ProjectTestTask : DefaultTask() { 8 | 9 | init { 10 | description = "Runs Unit tests against the whole project" 11 | group = "quality" 12 | } 13 | 14 | companion object { 15 | 16 | const val TASK_NAME = "projectTest" 17 | 18 | fun Project.registerProjectTestTask(action: (ProjectTestTask) -> Unit = {}): TaskProvider = 19 | tasks.register(TASK_NAME, ProjectTestTask::class.java, action) 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /jvm/src/test/kotlin/com/project/starter/modules/ConfigurationCacheTest.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.modules 2 | 3 | import com.project.starter.WithGradleProjectTest 4 | import com.project.starter.javaClass 5 | import org.junit.jupiter.api.BeforeEach 6 | import org.junit.jupiter.api.Disabled 7 | import org.junit.jupiter.api.Test 8 | 9 | internal class ConfigurationCacheTest : WithGradleProjectTest() { 10 | 11 | @BeforeEach 12 | fun setUp() { 13 | rootDirectory.apply { 14 | // language=groovy 15 | val script = 16 | """ 17 | plugins { 18 | id 'com.starter.library.kotlin' 19 | } 20 | """.trimIndent() 21 | resolve("build.gradle") { 22 | writeText(script) 23 | } 24 | resolve("src/main/java/ValidJava2.java") { 25 | writeText(javaClass("ValidJava2")) 26 | } 27 | resolve("src/test/java/com/example/ValidJavaTest2.java") { 28 | writeText(javaClass("ValidJavaTest2")) 29 | } 30 | } 31 | } 32 | 33 | /** 34 | * https://youtrack.jetbrains.com/issue/KT-38498 35 | * https://issuetracker.google.com/issues/156552742 36 | */ 37 | @Disabled("Configuration cache is not yet supported") 38 | @Test 39 | fun `does not fail with configuration cache`() { 40 | runTask("--configuration-cache") 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /jvm/src/test/kotlin/com/project/starter/modules/KotlinLibraryPluginTest.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.modules 2 | 3 | import com.project.starter.WithGradleProjectTest 4 | import com.project.starter.commit 5 | import com.project.starter.javaClass 6 | import com.project.starter.kotlinClass 7 | import com.project.starter.kotlinTestClass 8 | import com.project.starter.setupGit 9 | import com.project.starter.tag 10 | import org.assertj.core.api.Assertions.assertThat 11 | import org.gradle.testkit.runner.TaskOutcome 12 | import org.junit.jupiter.api.BeforeEach 13 | import org.junit.jupiter.api.Test 14 | import java.io.File 15 | 16 | internal class KotlinLibraryPluginTest : WithGradleProjectTest() { 17 | 18 | lateinit var rootBuildScript: File 19 | lateinit var module1Root: File 20 | lateinit var module2Root: File 21 | 22 | @BeforeEach 23 | fun setUp() { 24 | rootDirectory.apply { 25 | mkdirs() 26 | resolve("settings.gradle").writeText("""include ":module1", ":module2" """) 27 | 28 | rootBuildScript = resolve("build.gradle") 29 | module1Root = resolve("module1") { 30 | resolve("build.gradle") { 31 | writeText( 32 | """ 33 | plugins { 34 | id('com.starter.library.kotlin') 35 | } 36 | 37 | dependencies { 38 | testImplementation 'junit:junit:4.13.2' 39 | } 40 | 41 | """.trimIndent(), 42 | ) 43 | } 44 | resolve("src/main/kotlin/com/example/ValidKotlinFile1.kt") { 45 | writeText(kotlinClass("ValidKotlinFile1")) 46 | } 47 | resolve("src/test/kotlin/com/example/Test1.kt") { 48 | writeText(kotlinTestClass("Test1")) 49 | } 50 | } 51 | module2Root = resolve("module2") { 52 | resolve("build.gradle") { 53 | writeText( 54 | """ 55 | plugins { 56 | id('com.starter.library.kotlin') 57 | } 58 | 59 | dependencies { 60 | testImplementation 'junit:junit:4.13.2' 61 | } 62 | 63 | """.trimIndent(), 64 | ) 65 | } 66 | resolve("src/main/kotlin/com/example/ValidKotlinFile2.kt") { 67 | writeText(kotlinClass("ValidKotlinFile2")) 68 | } 69 | resolve("src/test/kotlin/com/example/Test2.kt") { 70 | writeText(kotlinTestClass("Test2")) 71 | } 72 | } 73 | } 74 | } 75 | 76 | @Test 77 | fun `kotlin library plugin compiles 'src_main_kotlin' classes`() { 78 | val result = runTask("assemble") 79 | 80 | assertThat(result.task(":module1:assemble")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) 81 | assertThat(result.task(":module2:assemble")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) 82 | } 83 | 84 | @Test 85 | fun `projectTest runs tests for all modules`() { 86 | val result = runTask("projectTest") 87 | 88 | assertThat(result.task(":module1:test")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) 89 | assertThat(result.task(":module2:test")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) 90 | assertThat(module1Root.resolve("build/test-results/test")).isDirectoryContaining { 91 | it.name.startsWith("TEST-") 92 | } 93 | } 94 | 95 | @Test 96 | fun `projectCoverage runs coverage for all modules`() { 97 | val result = runTask("projectCoverage") 98 | 99 | assertThat(result.task(":module1:test")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) 100 | assertThat(result.task(":module2:test")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) 101 | assertThat(module1Root.resolve("build/reports/jacoco/test")).isDirectoryContaining { 102 | it.name == "jacocoTestReport.xml" 103 | } 104 | } 105 | 106 | @Test 107 | fun `does not fail on java files if failing disabled`() { 108 | module2Root.resolve("build.gradle").appendText( 109 | """ 110 | projectConfig { 111 | javaFilesAllowed = true 112 | } 113 | """.trimIndent(), 114 | ) 115 | module2Root.resolve("src/main/java/JavaClass.java") { 116 | writeText(javaClass("JavaClass")) 117 | } 118 | 119 | val result = runTask(":module2:assemble") 120 | 121 | assertThat(result.task(":module2:assemble")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) 122 | } 123 | 124 | @Test 125 | fun `fails on java files by default`() { 126 | module2Root.resolve("src/main/java/JavaClass.java") { 127 | writeText(javaClass("JavaClass")) 128 | } 129 | 130 | val result = runTask(":module2:assemble", shouldFail = true) 131 | 132 | assertThat(result.task(":module2:forbidJavaFiles")!!.outcome).isEqualTo(TaskOutcome.FAILED) 133 | } 134 | 135 | @Test 136 | fun `configures quality plugin by default`() { 137 | val qualityEnabled = runTask("projectCodeStyle") 138 | 139 | assertThat(qualityEnabled.task(":module1:projectCodeStyle")?.outcome).isNotNull() 140 | assertThat(qualityEnabled.task(":module2:projectCodeStyle")?.outcome).isNotNull() 141 | } 142 | 143 | @Test 144 | fun `configures versioning plugin by default`() { 145 | rootBuildScript.writeText( 146 | // language=groovy 147 | """ 148 | plugins { 149 | id 'com.starter.versioning' 150 | } 151 | """.trimIndent(), 152 | ) 153 | val git = setupGit() 154 | git.tag("v1.2.2") 155 | git.commit("random commit") 156 | 157 | val versioningEnabled = runTask("currentVersion") 158 | 159 | assertThat(versioningEnabled.output).contains("version: 1.3.0-SNAPSHOT") 160 | } 161 | } 162 | -------------------------------------------------------------------------------- /jvm/src/test/kotlin/com/project/starter/modules/tasks/ForbidJavaFilesTaskTest.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.modules.tasks 2 | 3 | import com.project.starter.WithGradleProjectTest 4 | import com.project.starter.javaClass 5 | import com.project.starter.kotlinClass 6 | import org.assertj.core.api.Assertions.assertThat 7 | import org.gradle.testkit.runner.TaskOutcome 8 | import org.junit.jupiter.api.BeforeEach 9 | import org.junit.jupiter.api.Test 10 | import java.io.File 11 | 12 | internal class ForbidJavaFilesTaskTest : WithGradleProjectTest() { 13 | 14 | lateinit var main: File 15 | lateinit var test: File 16 | 17 | @BeforeEach 18 | fun setUp() { 19 | rootDirectory.apply { 20 | mkdirs() 21 | resolve("settings.gradle").writeText("""include ":module1" """) 22 | 23 | resolve("module1") { 24 | // language=groovy 25 | val buildScript = 26 | """ 27 | plugins { 28 | id('com.starter.library.kotlin') 29 | } 30 | 31 | projectConfig { 32 | javaFilesAllowed = false 33 | } 34 | """.trimIndent() 35 | resolve("build.gradle") { 36 | writeText(buildScript) 37 | } 38 | main = resolve("src/main") { 39 | resolve("kotlin/ValidKotlin.kt") { 40 | writeText(kotlinClass("ValidKotlin")) 41 | } 42 | resolve("java/KotlinInJavaDir.kt") { 43 | writeText(kotlinClass("KotlinInJavaDir")) 44 | } 45 | } 46 | test = resolve("src/test") { 47 | resolve("kotlin/Test1.kt") { 48 | writeText(kotlinClass("Test1")) 49 | } 50 | } 51 | } 52 | } 53 | } 54 | 55 | @Test 56 | fun `task passes configuration phase`() { 57 | runTask("help") 58 | } 59 | 60 | @Test 61 | fun `task fails on main sources`() { 62 | main.resolve("java/JavaClass.java") { 63 | writeText(javaClass("JavaClass")) 64 | } 65 | 66 | val result = runTask("assemble", shouldFail = true) 67 | 68 | assertThat(result.task(":module1:forbidJavaFiles")?.outcome).isEqualTo(TaskOutcome.FAILED) 69 | assertThat(result.output).contains("Java files are not allowed within :module1") 70 | } 71 | 72 | @Test 73 | fun `task fails on test sources`() { 74 | test.resolve("java/JavaTest.java") { 75 | writeText(javaClass("JavaTest")) 76 | } 77 | 78 | val result = runTask("assemble", shouldFail = true) 79 | 80 | assertThat(result.task(":module1:forbidJavaFiles")?.outcome).isEqualTo(TaskOutcome.FAILED) 81 | assertThat(result.output).contains("Java files are not allowed within :module1") 82 | } 83 | 84 | @Test 85 | fun `task is cacheable`() { 86 | runTask(":module1:assemble") 87 | 88 | val secondRun = runTask(":module1:assemble") 89 | 90 | assertThat(secondRun.task(":module1:forbidJavaFiles")?.outcome).isNotEqualTo(TaskOutcome.SUCCESS) 91 | } 92 | 93 | @Test 94 | fun `doesn't check generated files`() { 95 | rootDirectory.resolve("build/generated/source/apollo/classes/main/JavaTest.java") { 96 | writeText(javaClass("JavaTest")) 97 | } 98 | 99 | val secondRun = runTask("assemble") 100 | 101 | assertThat(secondRun.task(":module1:forbidJavaFiles")?.outcome).isNotEqualTo(TaskOutcome.SUCCESS) 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /jvm/src/test/kotlin/com/project/starter/quality/KotlinQualityPluginTest.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.quality 2 | 3 | import com.project.starter.WithGradleProjectTest 4 | import com.project.starter.javaClass 5 | import com.project.starter.kotlinClass 6 | import org.assertj.core.api.Assertions.assertThat 7 | import org.gradle.testkit.runner.TaskOutcome 8 | import org.junit.jupiter.api.BeforeEach 9 | import org.junit.jupiter.api.Test 10 | 11 | internal class KotlinQualityPluginTest : WithGradleProjectTest() { 12 | 13 | @BeforeEach 14 | fun setUp() { 15 | rootDirectory.apply { 16 | resolve("build.gradle") { 17 | writeText( 18 | // language=groovy 19 | """ 20 | plugins { 21 | id('com.starter.library.kotlin') 22 | } 23 | 24 | """.trimIndent(), 25 | ) 26 | } 27 | resolve("src/main/kotlin/com/example/ValidKotlinFile1.kt") { 28 | writeText(kotlinClass("ValidKotlinFile1")) 29 | } 30 | resolve("src/test/kotlin/com/example/ValidKotlinTest1.kt") { 31 | writeText(kotlinClass("ValidKotlinTest1")) 32 | } 33 | resolve("src/test/java/com/example/ValidJavaTest1.java") { 34 | writeText(javaClass("ValidJavaTest1")) 35 | } 36 | } 37 | } 38 | 39 | @Test 40 | fun `projectCodeStyle runs Detekt`() { 41 | val result = runTask("projectCodeStyle") 42 | 43 | assertThat(result.task(":detekt")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 44 | } 45 | 46 | @Test 47 | fun `projectCodeStyle runs ktlint`() { 48 | val result = runTask("projectCodeStyle") 49 | 50 | assertThat(result.task(":lintKotlinMain")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 51 | assertThat(result.task(":lintKotlinTest")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /jvm/src/test/kotlin/com/project/starter/quality/tasks/IssueLinksCheckerTaskTest.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.quality.tasks 2 | 3 | import com.project.starter.WithGradleProjectTest 4 | import com.project.starter.javaClass 5 | import org.assertj.core.api.Assertions.assertThat 6 | import org.gradle.testkit.runner.TaskOutcome 7 | import org.junit.jupiter.api.BeforeEach 8 | import org.junit.jupiter.api.Disabled 9 | import org.junit.jupiter.api.Test 10 | 11 | internal class IssueLinksCheckerTaskTest : WithGradleProjectTest() { 12 | 13 | @BeforeEach 14 | fun setUp() { 15 | rootDirectory.apply { 16 | // language=groovy 17 | val script = 18 | """ 19 | plugins { 20 | id 'com.starter.library.kotlin' 21 | } 22 | 23 | """.trimIndent() 24 | resolve("build.gradle") { 25 | writeText(script) 26 | } 27 | resolve("src/main/java/ValidJava2.java") { 28 | writeText(javaClass("ValidJava2")) 29 | } 30 | resolve("src/test/java/com/example/ValidJavaTest2.java") { 31 | writeText(javaClass("ValidJavaTest2")) 32 | } 33 | } 34 | } 35 | 36 | @Test 37 | fun `does not warn on regular project`() { 38 | rootDirectory.resolve("src/main/kotlin/com/example/ValidKotlin.kt") { 39 | // language=kotlin 40 | val randomLinks = 41 | """ 42 | /** 43 | * https://issuetracker.google.com/issues/145439806 44 | **/ 45 | object ValidKotlin { 46 | // https://www.example.com 47 | } 48 | """.trimIndent() 49 | writeText(randomLinks) 50 | } 51 | 52 | val result = runTask("issueLinksReport") 53 | 54 | assertThat(result.task(":issueLinksReport")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 55 | } 56 | 57 | @Test 58 | @Disabled("Google Issue tracker is not supported yet") 59 | fun `reports issuetracker issues`() { 60 | rootDirectory.resolve("src/main/kotlin/com/example/ValidKotlin.kt") { 61 | // language=kotlin 62 | val randomLinks = 63 | """ 64 | /** 65 | * https://news.ycombinator.com/ 66 | **/ 67 | object ValidKotlin { 68 | // https://issuetracker.google.com/issues/121092282 69 | val animations = 0 // Set animation: https://issuetracker.google.com/issues/154643058 70 | } 71 | """.trimIndent() 72 | writeText(randomLinks) 73 | } 74 | 75 | val result = runTask("issueLinksReport") 76 | 77 | assertThat(result.task(":issueLinksReport")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 78 | } 79 | 80 | @Test 81 | fun `reports youtrack issues`() { 82 | // language=kotlin 83 | val randomLinks = 84 | """ 85 | /** 86 | * https://news.ycombinator.com/ 87 | * https://youtrack.jetbrains.com/issue/KT-31666 88 | **/ 89 | object ValidKotlin { 90 | // https://youtrack.jetbrains.com/issue/KT-34230 91 | } 92 | """.trimIndent() 93 | 94 | rootDirectory.resolve("src/main/kotlin/com/example/ValidKotlin.kt") { 95 | writeText(randomLinks) 96 | } 97 | 98 | val result = runTask(":issueLinksReport") 99 | 100 | assertThat(rootDirectory.resolve("build/reports/issue_comments.txt")) 101 | .hasContent( 102 | """ 103 | 👉 https://youtrack.jetbrains.com/issue/KT-31666 (Closed) 104 | ✅ https://youtrack.jetbrains.com/issue/KT-34230 (Opened) 105 | """.trimIndent(), 106 | ) 107 | assertThat(result.output).contains("\uD83D\uDC49 https://youtrack.jetbrains.com/issue/KT-31666 (Closed)") 108 | assertThat(result.output).contains("✅ https://youtrack.jetbrains.com/issue/KT-34230 (Opened)") 109 | assertThat(result.task(":issueLinksReport")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 110 | } 111 | 112 | @Test 113 | fun `reports github issues`() { 114 | rootDirectory.resolve("src/main/kotlin/com/example/ValidKotlin.kt") { 115 | // language=kotlin 116 | val randomLinks = 117 | """ 118 | /** 119 | * https://github.com/isaacs/github/issues/5 120 | **/ 121 | object ValidKotlin { 122 | // https://www.example.com 123 | // https://github.com/apollographql/apollo-android/issues/2207 <- closed 124 | } 125 | """.trimIndent() 126 | writeText(randomLinks) 127 | } 128 | 129 | val result = runTask("issueLinksReport") 130 | 131 | assertThat(rootDirectory.resolve("build/reports/issue_comments.txt")) 132 | .hasContent( 133 | """ 134 | ✅ https://github.com/isaacs/github/issues/5 (Opened) 135 | 👉 https://github.com/apollographql/apollo-android/issues/2207 (Closed) 136 | """.trimIndent(), 137 | ) 138 | assertThat(result.output).contains("✅ https://github.com/isaacs/github/issues/5 (Opened)") 139 | assertThat(result.output).contains("\uD83D\uDC49 https://github.com/apollographql/apollo-android/issues/2207 (Closed)") 140 | assertThat(result.task(":issueLinksReport")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /jvm/src/test/kotlin/com/project/starter/versioning/KotlinVersioningPluginTest.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.versioning 2 | 3 | import com.project.starter.WithGradleProjectTest 4 | import com.project.starter.commit 5 | import com.project.starter.setupGit 6 | import com.project.starter.tag 7 | import org.assertj.core.api.Assertions.assertThat 8 | import org.eclipse.jgit.api.Git 9 | import org.junit.jupiter.api.BeforeEach 10 | import org.junit.jupiter.api.Test 11 | import java.io.File 12 | 13 | internal class KotlinVersioningPluginTest : WithGradleProjectTest() { 14 | 15 | private lateinit var module1Root: File 16 | private lateinit var module2Root: File 17 | private lateinit var rootBuildScript: File 18 | private lateinit var git: Git 19 | 20 | @BeforeEach 21 | fun setUp() { 22 | rootDirectory.apply { 23 | resolve("settings.gradle").writeText("""include ":module1", ":module2" """) 24 | rootBuildScript = resolve("build.gradle") 25 | module1Root = resolve("module1") { 26 | resolve("build.gradle") { 27 | writeText( 28 | """ 29 | plugins { 30 | id 'com.starter.library.kotlin' 31 | } 32 | """.trimIndent(), 33 | ) 34 | } 35 | } 36 | module2Root = resolve("module2") { 37 | resolve("build.gradle") { 38 | writeText( 39 | """ 40 | plugins { 41 | id 'com.starter.library.kotlin' 42 | } 43 | """.trimIndent(), 44 | ) 45 | } 46 | } 47 | } 48 | git = setupGit() 49 | git.tag("v1.1.0") 50 | } 51 | 52 | @Test 53 | fun `sets version to all projects`() { 54 | rootBuildScript.writeText( 55 | // language=groovy 56 | """ 57 | plugins { 58 | id 'com.starter.versioning' 59 | } 60 | """.trimIndent(), 61 | ) 62 | git.commit("features in 1.2.0") 63 | git.tag("v1.2.0") 64 | 65 | val modules = listOf(":module1", ":module2", "") 66 | 67 | modules.forEach { 68 | val moduleResult = runTask("$it:properties") 69 | 70 | assertThat(moduleResult?.output).contains("version: 1.2.0") 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /multiplatform/api/multiplatform.api: -------------------------------------------------------------------------------- 1 | public class com/project/starter/modules/extensions/MultiplatfromLibraryConfigExtension { 2 | public fun ()V 3 | } 4 | 5 | public final class com/project/starter/modules/plugins/MultiplatformLibraryPlugin : org/gradle/api/Plugin { 6 | public fun ()V 7 | public synthetic fun apply (Ljava/lang/Object;)V 8 | public fun apply (Lorg/gradle/api/Project;)V 9 | } 10 | 11 | -------------------------------------------------------------------------------- /multiplatform/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java-gradle-plugin") 3 | alias(libs.plugins.droidsonroids.jacocotestkit) 4 | alias(libs.plugins.starter.library.kotlin) 5 | alias(libs.plugins.kotlin.samwithreceiver) 6 | alias(libs.plugins.kotlinx.binarycompatibility) 7 | id("com.starter.publishing") 8 | } 9 | 10 | dependencies { 11 | runtimeOnly(libs.jetbrains.kotlin.jvm.implementation) 12 | api(libs.jetbrains.kotlin.jvm.api) 13 | implementation project(":jvm") 14 | implementation project(":quality") 15 | implementation project(":config") 16 | 17 | testRuntimeOnly(libs.junit.platform.launcher) 18 | testImplementation project(":testing") 19 | } 20 | 21 | tasks.named("test") { 22 | useJUnitPlatform() 23 | } 24 | 25 | gradlePlugin { 26 | plugins { 27 | kotlinLibrary { 28 | id = 'com.starter.library.multiplatform' 29 | displayName = 'Kotlin Multiplatform Library Plugin' 30 | implementationClass = 'com.project.starter.modules.plugins.MultiplatformLibraryPlugin' 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /multiplatform/src/main/kotlin/com/project/starter/modules/extensions/MultiplatfromLibraryConfigExtension.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.modules.extensions 2 | 3 | open class MultiplatfromLibraryConfigExtension 4 | -------------------------------------------------------------------------------- /multiplatform/src/main/kotlin/com/project/starter/modules/internal/MultiplatformCoverage.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.modules.internal 2 | 3 | import com.project.starter.config.getByType 4 | import org.gradle.api.Project 5 | import org.gradle.api.tasks.testing.Test 6 | import org.gradle.testing.jacoco.plugins.JacocoPluginExtension 7 | import org.gradle.testing.jacoco.plugins.JacocoTaskExtension 8 | import org.gradle.testing.jacoco.tasks.JacocoReport 9 | 10 | internal fun Project.configureMultiplatformCoverage() { 11 | pluginManager.apply("jacoco") 12 | 13 | tasks.withType(Test::class.java).configureEach { 14 | extensions.getByType().apply { 15 | isIncludeNoLocationClasses = true 16 | excludes = listOf("jdk.internal.*") 17 | } 18 | } 19 | 20 | extensions.configure(JacocoPluginExtension::class.java) { 21 | toolVersion = "0.8.13" 22 | } 23 | tasks.register("jacocoTestReport", JacocoReport::class.java) { 24 | dependsOn("jvmTest") 25 | classDirectories.setFrom(layout.buildDirectory.map { buildDir -> buildDir.file("classes/kotlin/jvm/main") }) 26 | sourceDirectories.setFrom(files("src/commonMain/kotlin", "src/jvmMain/kotlin")) 27 | executionData.setFrom(layout.buildDirectory.map { buildDir -> buildDir.file("jacoco/jvmTest.exec") }) 28 | reports { 29 | xml.required.set(true) 30 | html.required.set(true) 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /multiplatform/src/main/kotlin/com/project/starter/modules/plugins/MultiplatformLibraryPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.modules.plugins 2 | 3 | import com.project.starter.config.plugins.rootConfig 4 | import com.project.starter.modules.extensions.MultiplatfromLibraryConfigExtension 5 | import com.project.starter.modules.internal.configureMultiplatformCoverage 6 | import com.project.starter.modules.tasks.ProjectCoverageTask.Companion.registerProjectCoverageTask 7 | import com.project.starter.modules.tasks.ProjectTestTask.Companion.registerProjectTestTask 8 | import org.gradle.api.Plugin 9 | import org.gradle.api.Project 10 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 11 | import org.jetbrains.kotlin.gradle.tasks.KotlinJvmCompile 12 | 13 | class MultiplatformLibraryPlugin : Plugin { 14 | 15 | override fun apply(target: Project): Unit = with(target) { 16 | pluginManager.apply("org.jetbrains.kotlin.multiplatform") 17 | pluginManager.apply("com.starter.quality") 18 | pluginManager.apply(ConfigurationPlugin::class.java) 19 | 20 | extensions.create("projectConfig", MultiplatfromLibraryConfigExtension::class.java) 21 | 22 | registerProjectTestTask { 23 | it.dependsOn("allTests") 24 | } 25 | tasks.withType(KotlinJvmCompile::class.java).configureEach { 26 | compilerOptions.jvmTarget.set(JvmTarget.fromTarget(rootConfig.javaVersion.toString())) 27 | } 28 | 29 | configureMultiplatformCoverage() 30 | registerProjectCoverageTask { projectCoverage -> 31 | projectCoverage.dependsOn("jacocoTestReport") 32 | } 33 | Unit 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /multiplatform/src/test/kotlin/com/project/starter/modules/MultiplatformLibraryPluginTest.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.modules 2 | 3 | import com.project.starter.WithGradleProjectTest 4 | import com.project.starter.commit 5 | import com.project.starter.kotlinClass 6 | import com.project.starter.kotlinMultiplatformTestClass 7 | import com.project.starter.setupGit 8 | import com.project.starter.tag 9 | import org.assertj.core.api.Assertions.assertThat 10 | import org.gradle.testkit.runner.TaskOutcome 11 | import org.junit.jupiter.api.BeforeEach 12 | import org.junit.jupiter.api.Test 13 | import java.io.File 14 | 15 | internal class MultiplatformLibraryPluginTest : WithGradleProjectTest() { 16 | 17 | lateinit var rootBuildScript: File 18 | lateinit var module1Root: File 19 | lateinit var module2Root: File 20 | 21 | @BeforeEach 22 | fun setUp() { 23 | rootDirectory.apply { 24 | mkdirs() 25 | resolve("settings.gradle").writeText( 26 | // language=groovy 27 | """ 28 | include ":module1", ":module2" 29 | 30 | dependencyResolutionManagement { 31 | repositories { 32 | mavenCentral() 33 | } 34 | } 35 | """.trimIndent(), 36 | ) 37 | 38 | rootBuildScript = resolve("build.gradle") { 39 | // language=groovy 40 | writeText( 41 | """ 42 | plugins { 43 | id("com.starter.config") 44 | } 45 | """.trimIndent(), 46 | ) 47 | } 48 | module1Root = resolve("module1") { 49 | resolve("build.gradle") { 50 | writeText( 51 | // language=groovy 52 | """ 53 | plugins { 54 | id('com.starter.library.multiplatform') 55 | } 56 | 57 | kotlin { 58 | jvm() 59 | 60 | sourceSets { 61 | commonTest { 62 | dependencies { 63 | implementation kotlin("test") 64 | } 65 | } 66 | } 67 | } 68 | 69 | """.trimIndent(), 70 | ) 71 | } 72 | resolve("src/commonMain/kotlin/com/example/ValidKotlinFile1.kt") { 73 | writeText(kotlinClass("ValidKotlinFile1")) 74 | } 75 | resolve("src/jvmTest/kotlin/com/example/JvmTest1.kt") { 76 | writeText(kotlinMultiplatformTestClass("JvmTest1")) 77 | } 78 | } 79 | module2Root = resolve("module2") { 80 | resolve("build.gradle") { 81 | writeText( 82 | // language=groovy 83 | """ 84 | plugins { 85 | id('com.starter.library.multiplatform') 86 | } 87 | 88 | kotlin { 89 | jvm() 90 | iosX64() 91 | iosArm64() 92 | iosSimulatorArm64() 93 | 94 | sourceSets { 95 | commonTest { 96 | dependencies { 97 | implementation kotlin("test") 98 | } 99 | } 100 | } 101 | } 102 | 103 | """.trimIndent(), 104 | ) 105 | } 106 | resolve("src/commonMain/kotlin/com/example/ValidKotlinFile1.kt") { 107 | writeText(kotlinClass("ValidKotlinFile1")) 108 | } 109 | resolve("src/commonTest/kotlin/com/example/ValidKotlinTest1.kt") { 110 | writeText(kotlinMultiplatformTestClass("ValidKotlinTest1")) 111 | } 112 | resolve("src/jvmMain/kotlin/com/example/ValidKotlinJvmFile1.kt") { 113 | writeText(kotlinClass("ValidKotlinJvmFile1")) 114 | } 115 | resolve("src/jvmTest/kotlin/com/example/ValidJvmKotlinTest1.kt") { 116 | writeText(kotlinMultiplatformTestClass("ValidJvmKotlinTest1")) 117 | } 118 | resolve("src/iosMain/kotlin/com/example/ValidKotlinIosFile1.kt") { 119 | writeText(kotlinClass("ValidKotlinIosFile1")) 120 | } 121 | resolve("src/iosTest/kotlin/com/example/ValidIosKotlinTest1.kt") { 122 | writeText(kotlinMultiplatformTestClass("ValidIosKotlinTest1")) 123 | } 124 | } 125 | } 126 | } 127 | 128 | @Test 129 | fun `kotlin library plugin compiles 'src_main_kotlin' classes`() { 130 | val result = runTask("assemble") 131 | 132 | assertThat(result.task(":module1:assemble")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) 133 | assertThat(result.task(":module2:assemble")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) 134 | } 135 | 136 | @Test 137 | fun `projectTest runs tests for all modules`() { 138 | val result = runTask("projectTest") 139 | 140 | assertThat(result.task(":module1:allTests")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) 141 | assertThat(result.task(":module2:allTests")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) 142 | } 143 | 144 | @Test 145 | fun `projectCoverage runs coverage for all modules`() { 146 | val result = runTask("projectCoverage") 147 | 148 | assertThat(result.task(":module1:jvmTest")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) 149 | assertThat(result.task(":module2:jvmTest")!!.outcome).isEqualTo(TaskOutcome.SUCCESS) 150 | assertThat(module1Root.resolve("build/reports/jacoco/jacocoTestReport")).isDirectoryContaining { 151 | it.name == "jacocoTestReport.xml" 152 | } 153 | } 154 | 155 | @Test 156 | fun `configures quality plugin by default`() { 157 | val qualityEnabled = runTask("projectCodeStyle") 158 | 159 | assertThat(qualityEnabled.task(":module1:projectCodeStyle")?.outcome).isNotNull() 160 | assertThat(qualityEnabled.task(":module2:projectCodeStyle")?.outcome).isNotNull() 161 | } 162 | 163 | @Test 164 | fun `configures versioning plugin when applied`() { 165 | rootBuildScript.writeText( 166 | // language=groovy 167 | """ 168 | plugins { 169 | id 'com.starter.versioning' 170 | id("com.starter.config") 171 | } 172 | """.trimIndent(), 173 | ) 174 | val git = setupGit() 175 | git.tag("v1.2.2") 176 | git.commit("random commit") 177 | 178 | val versioningEnabled = runTask("currentVersion") 179 | 180 | assertThat(versioningEnabled.output).contains("version: 1.3.0-SNAPSHOT") 181 | } 182 | } 183 | -------------------------------------------------------------------------------- /multiplatform/src/test/kotlin/com/project/starter/quality/MultiplatformQualityPluginTest.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.quality 2 | 3 | import com.project.starter.WithGradleProjectTest 4 | import com.project.starter.kotlinClass 5 | import org.assertj.core.api.Assertions.assertThat 6 | import org.gradle.testkit.runner.TaskOutcome 7 | import org.junit.jupiter.api.BeforeEach 8 | import org.junit.jupiter.api.Test 9 | 10 | internal class MultiplatformQualityPluginTest : WithGradleProjectTest() { 11 | 12 | @BeforeEach 13 | fun setUp() { 14 | rootDirectory.apply { 15 | //language=groovy 16 | val script = 17 | """ 18 | plugins { 19 | id('com.starter.library.multiplatform') 20 | } 21 | 22 | kotlin { 23 | jvm() 24 | iosX64() 25 | iosArm64() 26 | iosSimulatorArm64() 27 | } 28 | """.trimIndent() 29 | resolve("build.gradle") { 30 | writeText(script) 31 | } 32 | resolve("src/commonMain/kotlin/com/example/ValidKotlinFile1.kt") { 33 | writeText(kotlinClass("ValidKotlinFile1")) 34 | } 35 | resolve("src/commonTest/kotlin/com/example/ValidKotlinTest1.kt") { 36 | writeText(kotlinClass("ValidKotlinTest1")) 37 | } 38 | resolve("src/jvmMain/kotlin/com/example/ValidKotlinJvmFile1.kt") { 39 | writeText(kotlinClass("ValidKotlinJvmFile1")) 40 | } 41 | resolve("src/jvmTest/kotlin/com/example/ValidJvmKotlinTest1.kt") { 42 | writeText(kotlinClass("ValidJvmKotlinTest1")) 43 | } 44 | resolve("src/iosMain/kotlin/com/example/ValidKotlinIosFile1.kt") { 45 | writeText(kotlinClass("ValidKotlinIosFile1")) 46 | } 47 | resolve("src/iosTest/kotlin/com/example/ValidIosKotlinTest1.kt") { 48 | writeText(kotlinClass("ValidIosKotlinTest1")) 49 | } 50 | } 51 | } 52 | 53 | @Test 54 | fun `projectCodeStyle runs Detekt`() { 55 | val result = runTask("projectCodeStyle") 56 | 57 | assertThat(result.task(":detekt")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 58 | } 59 | 60 | @Test 61 | fun `projectCodeStyle runs ktlint`() { 62 | val result = runTask("projectCodeStyle") 63 | 64 | assertThat(result.task(":lintKotlinIosMain")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 65 | assertThat(result.task(":lintKotlinJvmMain")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 66 | assertThat(result.task(":lintKotlinCommonMain")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 67 | 68 | assertThat(result.task(":lintKotlinIosTest")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 69 | assertThat(result.task(":lintKotlinJvmTest")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 70 | assertThat(result.task(":lintKotlinCommonTest")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /multiplatform/src/test/kotlin/com/project/starter/quality/tasks/IssueLinksCheckerTaskTest.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.quality.tasks 2 | 3 | import com.project.starter.WithGradleProjectTest 4 | import org.assertj.core.api.Assertions.assertThat 5 | import org.gradle.testkit.runner.TaskOutcome 6 | import org.junit.jupiter.api.BeforeEach 7 | import org.junit.jupiter.api.Test 8 | 9 | internal class IssueLinksCheckerTaskTest : WithGradleProjectTest() { 10 | 11 | @BeforeEach 12 | fun setUp() { 13 | rootDirectory.apply { 14 | //language=groovy 15 | val script = 16 | """ 17 | plugins { 18 | id('com.starter.library.multiplatform') 19 | } 20 | 21 | kotlin { 22 | jvm() 23 | iosX64() 24 | iosArm64() 25 | iosSimulatorArm64() 26 | } 27 | 28 | """.trimIndent() 29 | resolve("build.gradle") { 30 | writeText(script) 31 | } 32 | } 33 | } 34 | 35 | @Test 36 | fun `reports issue tracker issues`() { 37 | //language=kotlin 38 | val randomLinks = 39 | """ 40 | /** 41 | * https://news.ycombinator.com/ 42 | * https://youtrack.jetbrains.com/issue/KT-31666 43 | **/ 44 | object ValidKotlin { 45 | // https://youtrack.jetbrains.com/issue/KT-34230 46 | } 47 | """.trimIndent() 48 | 49 | rootDirectory.resolve("src/commonMain/kotlin/com/example/ValidKotlin.kt") { 50 | writeText(randomLinks) 51 | } 52 | 53 | val result = runTask(":issueLinksReport") 54 | 55 | assertThat(rootDirectory.resolve("build/reports/issue_comments.txt")) 56 | .hasContent( 57 | """ 58 | 👉 https://youtrack.jetbrains.com/issue/KT-31666 (Closed) 59 | ✅ https://youtrack.jetbrains.com/issue/KT-34230 (Opened) 60 | """.trimIndent(), 61 | ) 62 | assertThat(result.output).contains("\uD83D\uDC49 https://youtrack.jetbrains.com/issue/KT-31666 (Closed)") 63 | assertThat(result.output).contains("✅ https://youtrack.jetbrains.com/issue/KT-34230 (Opened)") 64 | assertThat(result.task(":issueLinksReport")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /multiplatform/src/test/kotlin/com/project/starter/versioning/MultiplatformVersioningPluginTest.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.versioning 2 | 3 | import com.project.starter.WithGradleProjectTest 4 | import com.project.starter.commit 5 | import com.project.starter.setupGit 6 | import com.project.starter.tag 7 | import org.assertj.core.api.Assertions.assertThat 8 | import org.eclipse.jgit.api.Git 9 | import org.junit.jupiter.api.BeforeEach 10 | import org.junit.jupiter.api.Test 11 | import java.io.File 12 | 13 | internal class MultiplatformVersioningPluginTest : WithGradleProjectTest() { 14 | 15 | private lateinit var module1Root: File 16 | private lateinit var module2Root: File 17 | private lateinit var rootBuildScript: File 18 | private lateinit var git: Git 19 | 20 | @BeforeEach 21 | fun setUp() { 22 | rootDirectory.apply { 23 | resolve("settings.gradle").writeText("""include ":module1", ":module2" """) 24 | rootBuildScript = resolve("build.gradle") 25 | module1Root = resolve("module1") { 26 | resolve("build.gradle") { 27 | writeText( 28 | """ 29 | plugins { 30 | id 'com.starter.library.multiplatform' 31 | } 32 | 33 | kotlin { 34 | iosX64() 35 | iosArm64() 36 | iosSimulatorArm64() 37 | } 38 | """.trimIndent(), 39 | ) 40 | } 41 | } 42 | module2Root = resolve("module2") { 43 | resolve("build.gradle") { 44 | writeText( 45 | """ 46 | plugins { 47 | id 'com.starter.library.multiplatform' 48 | } 49 | 50 | kotlin { 51 | jvm() 52 | } 53 | """.trimIndent(), 54 | ) 55 | } 56 | } 57 | } 58 | git = setupGit() 59 | git.tag("v1.1.0") 60 | } 61 | 62 | @Test 63 | fun `sets version to all projects`() { 64 | rootBuildScript.writeText( 65 | // language=groovy 66 | """ 67 | plugins { 68 | id 'com.starter.versioning' 69 | } 70 | """.trimIndent(), 71 | ) 72 | git.commit("features in 1.2.0") 73 | git.tag("v1.2.0") 74 | 75 | val modules = listOf(":module1", ":module2", "") 76 | 77 | modules.forEach { 78 | val moduleResult = runTask("$it:properties") 79 | 80 | assertThat(moduleResult?.output).contains("version: 1.2.0") 81 | } 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /quality/api/quality.api: -------------------------------------------------------------------------------- 1 | public abstract interface class com/project/starter/quality/extensions/JavaSourcesAware { 2 | public abstract fun getJavaFilesAllowed ()Ljava/lang/Boolean; 3 | } 4 | 5 | public final class com/project/starter/quality/plugins/QualityPlugin : org/gradle/api/Plugin { 6 | public fun ()V 7 | public synthetic fun apply (Ljava/lang/Object;)V 8 | public fun apply (Lorg/gradle/api/Project;)V 9 | } 10 | 11 | public abstract interface class com/project/starter/quality/tasks/IssueCheckParameters : org/gradle/workers/WorkParameters { 12 | public abstract fun getFiles ()Lorg/gradle/api/file/ConfigurableFileCollection; 13 | public abstract fun getGithubToken ()Lorg/gradle/api/provider/Property; 14 | public abstract fun getReportFile ()Lorg/gradle/api/file/RegularFileProperty; 15 | } 16 | 17 | public abstract class com/project/starter/quality/tasks/IssueLinksTask : org/gradle/api/tasks/SourceTask { 18 | public static final field Companion Lcom/project/starter/quality/tasks/IssueLinksTask$Companion; 19 | public fun (Lorg/gradle/workers/WorkerExecutor;)V 20 | public final fun getGithubToken ()Lorg/gradle/api/provider/Property; 21 | public final fun getReport ()Lorg/gradle/api/file/RegularFileProperty; 22 | public fun getSource ()Lorg/gradle/api/file/FileTree; 23 | public final fun run ()V 24 | } 25 | 26 | public final class com/project/starter/quality/tasks/IssueLinksTask$Companion { 27 | public final fun registerIssueCheckerTask (Lorg/gradle/api/Project;Lkotlin/jvm/functions/Function1;)Lorg/gradle/api/tasks/TaskProvider; 28 | public static synthetic fun registerIssueCheckerTask$default (Lcom/project/starter/quality/tasks/IssueLinksTask$Companion;Lorg/gradle/api/Project;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)Lorg/gradle/api/tasks/TaskProvider; 29 | } 30 | 31 | public final class com/project/starter/quality/tasks/LoggingContext { 32 | public static final field INSTANCE Lcom/project/starter/quality/tasks/LoggingContext; 33 | public static field logger Lorg/gradle/api/logging/Logger; 34 | public final fun getLogger ()Lorg/gradle/api/logging/Logger; 35 | public final fun setLogger (Lorg/gradle/api/logging/Logger;)V 36 | } 37 | 38 | public class com/project/starter/quality/tasks/ProjectCodeStyleTask : org/gradle/api/DefaultTask { 39 | public static final field Companion Lcom/project/starter/quality/tasks/ProjectCodeStyleTask$Companion; 40 | public static final field TASK_NAME Ljava/lang/String; 41 | public fun ()V 42 | } 43 | 44 | public final class com/project/starter/quality/tasks/ProjectCodeStyleTask$Companion { 45 | public final fun addProjectCodeStyleTask (Lorg/gradle/api/Project;Lkotlin/jvm/functions/Function1;)V 46 | public static synthetic fun addProjectCodeStyleTask$default (Lcom/project/starter/quality/tasks/ProjectCodeStyleTask$Companion;Lorg/gradle/api/Project;Lkotlin/jvm/functions/Function1;ILjava/lang/Object;)V 47 | } 48 | 49 | -------------------------------------------------------------------------------- /quality/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java-gradle-plugin") 3 | alias(libs.plugins.droidsonroids.jacocotestkit) 4 | alias(libs.plugins.starter.library.kotlin) 5 | alias(libs.plugins.kotlin.samwithreceiver) 6 | alias(libs.plugins.kotlinx.binarycompatibility) 7 | id("com.starter.publishing") 8 | } 9 | 10 | dependencies { 11 | compileOnly(libs.jetbrains.kotlin.jvm.api) 12 | compileOnly(libs.agp.gradle.api) 13 | api libs.usefulness.ktlint 14 | api libs.detekt.gradle 15 | implementation libs.usefulness.issuechecker 16 | implementation project(":config") 17 | 18 | testImplementation project(":testing") 19 | 20 | testRuntimeOnly(libs.junit.platform.launcher) 21 | testRuntimeDependencies(libs.jetbrains.kotlin.jvm.implementation) 22 | testRuntimeDependencies(libs.agp.gradle.implementation) 23 | } 24 | 25 | tasks.named("test") { 26 | useJUnitPlatform() 27 | } 28 | 29 | tasks.register("generateVersionProperties", WriteProperties) { writeProps -> 30 | def propertiesFile = new File(sourceSets.main.output.resourcesDir, "starter-quality-gradle-plugin.properties") 31 | if (GradleVersion.current() >= GradleVersion.version("8.1")) { 32 | writeProps.destinationFile = propertiesFile 33 | } else { 34 | //noinspection GrDeprecatedAPIUsage 35 | writeProps.outputFile = propertiesFile 36 | } 37 | writeProps.property("ktlint_version", libs.versions.maven.ktlint) 38 | } 39 | 40 | tasks.named("processResources") { 41 | dependsOn("generateVersionProperties") 42 | } 43 | 44 | gradlePlugin { 45 | plugins { 46 | quality { 47 | id = 'com.starter.quality' 48 | displayName = 'Code Quality Plugin' 49 | implementationClass = 'com.project.starter.quality.plugins.QualityPlugin' 50 | } 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /quality/src/main/kotlin/com/project/starter/quality/extensions/JavaSourcesAware.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.quality.extensions 2 | 3 | interface JavaSourcesAware { 4 | val javaFilesAllowed: Boolean? 5 | } 6 | -------------------------------------------------------------------------------- /quality/src/main/kotlin/com/project/starter/quality/internal/Detekt.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.quality.internal 2 | 3 | import com.project.starter.config.plugins.rootConfig 4 | import com.project.starter.quality.plugins.onMultiplatform 5 | import com.project.starter.quality.tasks.ProjectCodeStyleTask 6 | import io.gitlab.arturbosch.detekt.Detekt 7 | import io.gitlab.arturbosch.detekt.DetektPlugin 8 | import io.gitlab.arturbosch.detekt.extensions.DetektExtension 9 | import org.gradle.api.Project 10 | 11 | internal fun Project.configureDetekt() { 12 | pluginManager.apply(DetektPlugin::class.java) 13 | 14 | extensions.configure(DetektExtension::class.java) { 15 | onMultiplatform { 16 | sourceSets.configureEach { 17 | source.from(kotlin.srcDirs) 18 | } 19 | } 20 | 21 | config.setFrom(loadFromResources("detekt-config.yml")) 22 | } 23 | tasks.named("detekt", Detekt::class.java) { 24 | exclude(".*/resources/.*", ".*/build/.*") 25 | } 26 | tasks.named(ProjectCodeStyleTask.TASK_NAME) { 27 | dependsOn("detekt") 28 | } 29 | tasks.withType(Detekt::class.java).configureEach { 30 | jvmTarget = rootConfig.javaVersion.toString() 31 | reports { 32 | html.required.set(false) 33 | xml.required.set(false) 34 | txt.required.set(false) 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /quality/src/main/kotlin/com/project/starter/quality/internal/Ktlint.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.quality.internal 2 | 3 | import com.project.starter.quality.tasks.ProjectCodeStyleTask 4 | import io.github.usefulness.KtlintGradleExtension 5 | import io.github.usefulness.KtlintGradlePlugin 6 | import org.gradle.api.Project 7 | 8 | internal fun Project.configureKtlint() { 9 | pluginManager.apply(KtlintGradlePlugin::class.java) 10 | 11 | extensions.configure(KtlintGradleExtension::class.java) { 12 | experimentalRules.convention(true) 13 | disabledRules.convention( 14 | disabledRules.get() + listOf( 15 | "import-ordering", 16 | "filename", 17 | "experimental:function-signature", 18 | "experimental:property-naming", 19 | ), 20 | ) 21 | ktlintVersion.convention(versionProperties.ktlintVersion()) 22 | } 23 | 24 | tasks.named(ProjectCodeStyleTask.TASK_NAME) { 25 | dependsOn("lintKotlin") 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /quality/src/main/kotlin/com/project/starter/quality/internal/ResourceLoader.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.quality.internal 2 | 3 | import org.gradle.api.Project 4 | import org.gradle.api.provider.Provider 5 | import java.io.File 6 | import java.net.JarURLConnection 7 | 8 | private object ResourceLoader 9 | 10 | internal fun Project.loadFromResources(path: String): Provider { 11 | val configFile = ResourceLoader::class.java.classLoader.getResource(path) 12 | 13 | return provider { 14 | @Suppress("UseIfInsteadOfWhen") 15 | when (val jar = configFile?.openConnection()) { 16 | is JarURLConnection -> resources.text.fromArchiveEntry(jar.jarFileURL, jar.entryName).asFile() 17 | else -> configFile?.let { File(it.file) } 18 | }?.also { 19 | logger.info("Loaded config: $it") 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /quality/src/main/kotlin/com/project/starter/quality/internal/VersionProperties.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.quality.internal 2 | 3 | import java.util.Properties 4 | 5 | internal val versionProperties by lazy(::VersionProperties) 6 | 7 | internal class VersionProperties : Properties() { 8 | init { 9 | load(this.javaClass.getResourceAsStream("/starter-quality-gradle-plugin.properties")) 10 | } 11 | 12 | fun ktlintVersion(): String = getProperty("ktlint_version") 13 | } 14 | -------------------------------------------------------------------------------- /quality/src/main/kotlin/com/project/starter/quality/plugins/QualityPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.quality.plugins 2 | 3 | import com.android.build.api.variant.AndroidComponentsExtension 4 | import com.project.starter.config.findByType 5 | import com.project.starter.config.plugins.rootConfig 6 | import com.project.starter.quality.internal.configureDetekt 7 | import com.project.starter.quality.internal.configureKtlint 8 | import com.project.starter.quality.tasks.IssueLinksTask.Companion.registerIssueCheckerTask 9 | import com.project.starter.quality.tasks.ProjectCodeStyleTask.Companion.addProjectCodeStyleTask 10 | import org.gradle.api.Plugin 11 | import org.gradle.api.Project 12 | import org.gradle.api.tasks.SourceSetContainer 13 | import org.jetbrains.kotlin.gradle.plugin.KotlinSourceSetContainer 14 | 15 | class QualityPlugin : Plugin { 16 | 17 | override fun apply(project: Project) = with(project) { 18 | runCatching { repositories.mavenCentral() } 19 | addProjectCodeStyleTask() 20 | configureKtlint() 21 | configureDetekt() 22 | configureIssueCheckerTask() 23 | configureFormatOnRecompile() 24 | } 25 | 26 | private fun Project.configureIssueCheckerTask() { 27 | val issueCheckerTask = registerIssueCheckerTask { 28 | onMultiplatform { 29 | sourceSets.configureEach { 30 | source += kotlin.sourceDirectories.asFileTree 31 | } 32 | } 33 | onJvm { 34 | this.configureEach { 35 | source += allSource 36 | } 37 | } 38 | report.set(layout.buildDirectory.map { it.file("reports/issue_comments.txt") }) 39 | githubToken.set(provider { properties["GITHUB_TOKEN"]?.toString() }) 40 | } 41 | onAndroid { 42 | onVariants { variant -> 43 | val variantSources = listOfNotNull( 44 | variant.sources.kotlin, 45 | variant.sources.java, 46 | ) 47 | .map { it.all } 48 | issueCheckerTask.configure { source(variantSources) } 49 | } 50 | } 51 | } 52 | 53 | private fun Project.configureFormatOnRecompile() { 54 | pluginManager.withPlugin("org.jetbrains.kotlin.jvm") { 55 | tasks.named("compileKotlin") { 56 | if (rootConfig.quality.formatOnCompile) { 57 | dependsOn("formatKotlin") 58 | } 59 | } 60 | } 61 | pluginManager.withPlugin("com.android.library") { 62 | tasks.named("preBuild") { 63 | if (rootConfig.quality.formatOnCompile) { 64 | dependsOn("formatKotlin") 65 | } 66 | } 67 | } 68 | pluginManager.withPlugin("com.android.application") { 69 | tasks.named("preBuild") { 70 | if (rootConfig.quality.formatOnCompile) { 71 | dependsOn("formatKotlin") 72 | } 73 | } 74 | } 75 | } 76 | } 77 | 78 | internal inline fun Project.onAndroid(crossinline function: AndroidComponentsExtension<*, *, *>.() -> Unit) { 79 | project.extensions.findByName("androidComponents")?.let { (it as? AndroidComponentsExtension<*, *, *>)?.function() } 80 | } 81 | 82 | internal inline fun Project.onMultiplatform(crossinline function: KotlinSourceSetContainer.() -> Unit) { 83 | project.extensions.findByName("kotlin")?.let { (it as? KotlinSourceSetContainer)?.function() } 84 | } 85 | 86 | internal inline fun Project.onJvm(crossinline function: SourceSetContainer.() -> Unit) { 87 | project.extensions.findByType()?.let(function) 88 | } 89 | -------------------------------------------------------------------------------- /quality/src/main/kotlin/com/project/starter/quality/tasks/IssueLinksTask.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.quality.tasks 2 | 3 | import com.starter.issuechecker.CheckResult 4 | import com.starter.issuechecker.IssueChecker 5 | import com.starter.issuechecker.IssueStatus 6 | import com.starter.issuechecker.reportBlocking 7 | import org.gradle.api.Project 8 | import org.gradle.api.file.ConfigurableFileCollection 9 | import org.gradle.api.file.FileTree 10 | import org.gradle.api.file.RegularFileProperty 11 | import org.gradle.api.logging.Logger 12 | import org.gradle.api.logging.Logging 13 | import org.gradle.api.provider.Property 14 | import org.gradle.api.tasks.CacheableTask 15 | import org.gradle.api.tasks.Input 16 | import org.gradle.api.tasks.InputFiles 17 | import org.gradle.api.tasks.Optional 18 | import org.gradle.api.tasks.OutputFile 19 | import org.gradle.api.tasks.PathSensitive 20 | import org.gradle.api.tasks.PathSensitivity 21 | import org.gradle.api.tasks.SourceTask 22 | import org.gradle.api.tasks.TaskAction 23 | import org.gradle.api.tasks.TaskProvider 24 | import org.gradle.work.NormalizeLineEndings 25 | import org.gradle.workers.WorkAction 26 | import org.gradle.workers.WorkParameters 27 | import org.gradle.workers.WorkerExecutor 28 | import javax.inject.Inject 29 | 30 | @CacheableTask 31 | abstract class IssueLinksTask @Inject constructor(private val workerExecutor: WorkerExecutor) : SourceTask() { 32 | 33 | @OutputFile 34 | val report: RegularFileProperty = project.objects.fileProperty() 35 | 36 | @Input 37 | @Optional 38 | val githubToken: Property = project.objects.property(String::class.java) 39 | 40 | init { 41 | description = "Generates report for issue links in code comments" 42 | group = "quality" 43 | } 44 | 45 | @InputFiles 46 | @NormalizeLineEndings 47 | @PathSensitive(PathSensitivity.RELATIVE) 48 | override fun getSource(): FileTree = super.getSource() 49 | 50 | @TaskAction 51 | fun run() { 52 | LoggingContext.logger = logger 53 | source.forEach { chunk -> 54 | workerExecutor.noIsolation().submit(IssueCheckAction::class.java) { 55 | files.from(chunk) 56 | reportFile.set(report.get()) 57 | githubToken.set(githubToken.orNull) 58 | } 59 | } 60 | 61 | workerExecutor.await() 62 | } 63 | 64 | companion object { 65 | 66 | private const val TASK_NAME = "issueLinksReport" 67 | 68 | fun Project.registerIssueCheckerTask(action: IssueLinksTask.() -> Unit = {}): TaskProvider = 69 | tasks.register(TASK_NAME, IssueLinksTask::class.java, action) 70 | } 71 | } 72 | 73 | interface IssueCheckParameters : WorkParameters { 74 | val files: ConfigurableFileCollection 75 | val reportFile: RegularFileProperty 76 | val githubToken: Property 77 | } 78 | 79 | internal abstract class IssueCheckAction : WorkAction { 80 | 81 | private val logger = Logging.getLogger(IssueCheckAction::class.java) 82 | 83 | override fun execute() { 84 | val issueChecker = IssueChecker(config = IssueChecker.Config(githubToken = parameters.githubToken.orNull)) 85 | val output = parameters.reportFile.get().asFile 86 | output.writeText("") 87 | for (file in parameters.files) { 88 | val message = issueChecker.reportBlocking(file.readText()).map { result -> 89 | when (result) { 90 | is CheckResult.Success -> when (result.issueStatus) { 91 | IssueStatus.Open -> "✅ ${result.issueUrl} (Opened)" 92 | IssueStatus.Closed -> "👉 ${result.issueUrl} (Closed)" 93 | } 94 | 95 | is CheckResult.Error -> "❗ ${result.issueUrl} -> error: ${result.throwable.message}" 96 | } 97 | } 98 | 99 | output.appendText(message.joinToString(separator = "\n")) 100 | logger.info("Found ${message.size} issues in ${file.path}") 101 | message.forEach(logger::quiet) 102 | } 103 | } 104 | } 105 | 106 | object LoggingContext { 107 | lateinit var logger: Logger 108 | } 109 | -------------------------------------------------------------------------------- /quality/src/main/kotlin/com/project/starter/quality/tasks/ProjectCodeStyleTask.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.quality.tasks 2 | 3 | import org.gradle.api.DefaultTask 4 | import org.gradle.api.Project 5 | 6 | open class ProjectCodeStyleTask : DefaultTask() { 7 | 8 | init { 9 | description = "Runs code style checks against the whole project" 10 | group = "quality" 11 | } 12 | 13 | companion object { 14 | 15 | const val TASK_NAME = "projectCodeStyle" 16 | 17 | fun Project.addProjectCodeStyleTask(action: (ProjectCodeStyleTask) -> Unit = {}) { 18 | tasks.register(TASK_NAME, ProjectCodeStyleTask::class.java, action) 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /quality/src/test/kotlin/com/project/starter/quality/QualityPluginTest.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.quality 2 | 3 | import com.project.starter.WithGradleProjectTest 4 | import com.project.starter.javaClass 5 | import com.project.starter.kotlinClass 6 | import org.assertj.core.api.Assertions.assertThat 7 | import org.gradle.testkit.runner.TaskOutcome 8 | import org.junit.jupiter.api.BeforeEach 9 | import org.junit.jupiter.api.Test 10 | 11 | internal class QualityPluginTest : WithGradleProjectTest() { 12 | 13 | @BeforeEach 14 | fun setUp() { 15 | rootDirectory.apply { 16 | resolve("build.gradle") { 17 | // language=groovy 18 | writeText( 19 | """ 20 | import org.jetbrains.kotlin.gradle.dsl.KotlinCompile 21 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 22 | 23 | plugins { 24 | id('com.starter.quality') 25 | id('org.jetbrains.kotlin.jvm') 26 | } 27 | 28 | def targetJavaVersion = JavaVersion.VERSION_11 29 | tasks.withType(JavaCompile).configureEach { 30 | options.release.set(targetJavaVersion.majorVersion.toInteger()) 31 | } 32 | tasks.withType(KotlinCompile).configureEach { 33 | compilerOptions.jvmTarget = JvmTarget.fromTarget(targetJavaVersion.toString()) 34 | } 35 | 36 | repositories.mavenCentral() 37 | """.trimIndent(), 38 | ) 39 | } 40 | resolve("src/main/kotlin/com/example/ValidKotlinFile1.kt") { 41 | writeText(kotlinClass("ValidKotlinFile1")) 42 | } 43 | resolve("src/main/java/ValidJava1.java") { 44 | writeText(javaClass("ValidJava1")) 45 | } 46 | resolve("src/debug/java/DebugJava.java") { 47 | writeText(javaClass("DebugJava")) 48 | } 49 | resolve("src/test/kotlin/com/example/ValidKotlinTest1.kt") { 50 | writeText(kotlinClass("ValidKotlinTest1")) 51 | } 52 | resolve("src/test/java/com/example/ValidJavaTest1.java") { 53 | writeText(javaClass("ValidJavaTest1")) 54 | } 55 | } 56 | } 57 | 58 | @Test 59 | fun `projectCodeStyle runs Detekt`() { 60 | val result = runTask("projectCodeStyle") 61 | 62 | assertThat(result.task(":detekt")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 63 | } 64 | 65 | @Test 66 | fun `projectCodeStyle runs ktlint`() { 67 | val result = runTask("projectCodeStyle") 68 | 69 | assertThat(result.task(":lintKotlinMain")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 70 | assertThat(result.task(":lintKotlinTest")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 71 | } 72 | 73 | @Test 74 | fun `formatOnCompile option enables failing builds if code style errors found`() { 75 | val enableFormatOnCompile = { 76 | // language=groovy 77 | val buildscript = 78 | """ 79 | import org.gradle.api.JavaVersion 80 | import org.jetbrains.kotlin.gradle.dsl.KotlinCompile 81 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget 82 | 83 | plugins { 84 | id('com.starter.config') 85 | id('com.starter.quality') 86 | id('org.jetbrains.kotlin.jvm') 87 | } 88 | 89 | commonConfig { 90 | qualityPlugin { 91 | formatOnCompile true 92 | } 93 | } 94 | 95 | kotlin { 96 | jvmToolchain(23) 97 | } 98 | 99 | def targetJavaVersion = JavaVersion.VERSION_11 100 | tasks.withType(JavaCompile).configureEach { 101 | options.release.set(targetJavaVersion.majorVersion.toInteger()) 102 | } 103 | tasks.withType(KotlinCompile).configureEach { 104 | compilerOptions.jvmTarget = JvmTarget.@Companion.fromTarget(targetJavaVersion.toString()) 105 | } 106 | """.trimIndent() 107 | rootDirectory.resolve("build.gradle").writeText(buildscript) 108 | } 109 | 110 | rootDirectory.resolve("src/main/kotlin/WrongFileName.kt") { 111 | writeText(kotlinClass("DifferentClassName")) 112 | } 113 | 114 | val formatOnCompileOff = runTask("assemble") 115 | 116 | assertThat(formatOnCompileOff.task(":formatKotlin")?.outcome).isNull() 117 | 118 | enableFormatOnCompile() 119 | val formatOnCompileOn = runTask("assemble") 120 | 121 | assertThat(formatOnCompileOn.task(":formatKotlin")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 122 | } 123 | 124 | @Test 125 | fun `detekt fails on invalid class name`() { 126 | rootDirectory.resolve("src/main/kotlin/MagicNumber.kt") { 127 | val kotlinClass = 128 | // language=kotlin 129 | """ 130 | class invalidClassName { 131 | var value: Int = 16 132 | } 133 | 134 | """.trimIndent() 135 | writeText(kotlinClass) 136 | } 137 | 138 | val result = runTask("projectCodeStyle", shouldFail = true) 139 | 140 | assertThat(result.task(":detekt")?.outcome).isEqualTo(TaskOutcome.FAILED) 141 | } 142 | } 143 | -------------------------------------------------------------------------------- /quality/src/test/kotlin/com/project/starter/quality/tasks/IssueLinksCheckerTaskTest.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.quality.tasks 2 | 3 | import com.project.starter.WithGradleProjectTest 4 | import com.project.starter.javaClass 5 | import org.assertj.core.api.Assertions.assertThat 6 | import org.gradle.testkit.runner.TaskOutcome 7 | import org.junit.jupiter.api.BeforeEach 8 | import org.junit.jupiter.api.Disabled 9 | import org.junit.jupiter.api.Test 10 | 11 | internal class IssueLinksCheckerTaskTest : WithGradleProjectTest() { 12 | 13 | @BeforeEach 14 | fun setUp() { 15 | rootDirectory.apply { 16 | // language=groovy 17 | val script = 18 | """ 19 | plugins { 20 | id 'com.starter.quality' 21 | id 'org.jetbrains.kotlin.jvm' 22 | } 23 | 24 | 25 | """.trimIndent() 26 | resolve("build.gradle") { 27 | writeText(script) 28 | } 29 | resolve("src/main/java/ValidJava2.java") { 30 | writeText(javaClass("ValidJava2")) 31 | } 32 | resolve("src/test/java/com/example/ValidJavaTest2.java") { 33 | writeText(javaClass("ValidJavaTest2")) 34 | } 35 | } 36 | } 37 | 38 | @Test 39 | fun `does not warn on regular project`() { 40 | rootDirectory.resolve("src/main/kotlin/com/example/ValidKotlin.kt") { 41 | // language=kotlin 42 | val randomLinks = 43 | """ 44 | /** 45 | * https://issuetracker.google.com/issues/145439806 46 | **/ 47 | object ValidKotlin { 48 | // https://www.example.com 49 | } 50 | """.trimIndent() 51 | writeText(randomLinks) 52 | } 53 | 54 | val result = runTask("issueLinksReport") 55 | 56 | assertThat(result.task(":issueLinksReport")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 57 | } 58 | 59 | @Test 60 | @Disabled("Google Issue tracker is not supported yet") 61 | fun `reports issuetracker issues`() { 62 | rootDirectory.resolve("src/main/kotlin/com/example/ValidKotlin.kt") { 63 | // language=kotlin 64 | val randomLinks = 65 | """ 66 | /** 67 | * https://news.ycombinator.com/ 68 | **/ 69 | object ValidKotlin { 70 | // https://issuetracker.google.com/issues/121092282 71 | val animations = 0 // Set animation: https://issuetracker.google.com/issues/154643058 72 | } 73 | """.trimIndent() 74 | writeText(randomLinks) 75 | } 76 | 77 | val result = runTask("issueLinksReport") 78 | 79 | assertThat(result.task(":issueLinksReport")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 80 | } 81 | 82 | @Test 83 | fun `reports youtrack issues`() { 84 | // language=kotlin 85 | val randomLinks = 86 | """ 87 | /** 88 | * https://news.ycombinator.com/ 89 | * https://youtrack.jetbrains.com/issue/KT-31666 90 | **/ 91 | object ValidKotlin { 92 | // https://youtrack.jetbrains.com/issue/KT-34230 93 | } 94 | """.trimIndent() 95 | 96 | rootDirectory.resolve("src/main/kotlin/com/example/ValidKotlin.kt") { 97 | writeText(randomLinks) 98 | } 99 | 100 | val result = runTask(":issueLinksReport") 101 | 102 | assertThat(rootDirectory.resolve("build/reports/issue_comments.txt")) 103 | .hasContent( 104 | """ 105 | 👉 https://youtrack.jetbrains.com/issue/KT-31666 (Closed) 106 | ✅ https://youtrack.jetbrains.com/issue/KT-34230 (Opened) 107 | """.trimIndent(), 108 | ) 109 | assertThat(result.output).contains("\uD83D\uDC49 https://youtrack.jetbrains.com/issue/KT-31666 (Closed)") 110 | assertThat(result.output).contains("✅ https://youtrack.jetbrains.com/issue/KT-34230 (Opened)") 111 | assertThat(result.task(":issueLinksReport")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 112 | } 113 | 114 | @Test 115 | fun `reports github issues`() { 116 | rootDirectory.resolve("src/main/kotlin/com/example/ValidKotlin.kt") { 117 | // language=kotlin 118 | val randomLinks = 119 | """ 120 | /** 121 | * https://github.com/isaacs/github/issues/5 122 | **/ 123 | object ValidKotlin { 124 | // https://www.example.com 125 | // https://github.com/apollographql/apollo-android/issues/2207 <- closed 126 | } 127 | """.trimIndent() 128 | writeText(randomLinks) 129 | } 130 | 131 | val result = runTask("issueLinksReport") 132 | 133 | assertThat(rootDirectory.resolve("build/reports/issue_comments.txt")) 134 | .hasContent( 135 | """ 136 | ✅ https://github.com/isaacs/github/issues/5 (Opened) 137 | 👉 https://github.com/apollographql/apollo-android/issues/2207 (Closed) 138 | """.trimIndent(), 139 | ) 140 | assertThat(result.output).contains("✅ https://github.com/isaacs/github/issues/5 (Opened)") 141 | assertThat(result.output).contains("\uD83D\uDC49 https://github.com/apollographql/apollo-android/issues/2207 (Closed)") 142 | assertThat(result.task(":issueLinksReport")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 143 | } 144 | } 145 | -------------------------------------------------------------------------------- /sample/android/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.starter.config") 3 | id("com.starter.library.android") apply false 4 | } 5 | 6 | commonConfig { 7 | javaFilesAllowed = false 8 | } 9 | -------------------------------------------------------------------------------- /sample/android/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.parallel=true 2 | org.gradle.caching=true 3 | org.gradle.vfs.watch=true 4 | -------------------------------------------------------------------------------- /sample/android/moduleAndroidApplication/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.starter.application.android") 3 | } 4 | 5 | android { 6 | namespace "com.starter.sample.application.android" 7 | } 8 | -------------------------------------------------------------------------------- /sample/android/moduleAndroidApplication/src/main/AndroidManifest.xml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /sample/android/moduleAndroidApplication/src/main/kotlin/com/starter/sample/AndroidAplicationClass.kt: -------------------------------------------------------------------------------- 1 | package com.starter.sample 2 | 3 | class AndroidAplicationClass 4 | -------------------------------------------------------------------------------- /sample/android/moduleAndroidApplication/src/test/kotlin/com/starter/sample/SampleTestClass.kt: -------------------------------------------------------------------------------- 1 | package com.starter.sample 2 | 3 | object SampleTestClass 4 | -------------------------------------------------------------------------------- /sample/android/moduleAndroidLibrary/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.starter.library.android") 3 | } 4 | 5 | android { 6 | namespace "com.starter.sample.library.android" 7 | } 8 | -------------------------------------------------------------------------------- /sample/android/moduleAndroidLibrary/src/main/kotlin/com/starter/sample/AndroidAplicationClass.kt: -------------------------------------------------------------------------------- 1 | package com.starter.sample 2 | 3 | class AndroidAplicationClass 4 | -------------------------------------------------------------------------------- /sample/android/moduleAndroidLibrary/src/test/kotlin/com/starter/sample/SampleTestClass.kt: -------------------------------------------------------------------------------- 1 | package com.starter.sample 2 | 3 | object SampleTestClass 4 | -------------------------------------------------------------------------------- /sample/android/moduleKotlinLibrary/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.starter.library.kotlin") 3 | } 4 | -------------------------------------------------------------------------------- /sample/android/moduleKotlinLibrary/src/main/kotlin/com/starter/sample/AndroidAplicationClass.kt: -------------------------------------------------------------------------------- 1 | package com.starter.sample 2 | 3 | class AndroidAplicationClass 4 | -------------------------------------------------------------------------------- /sample/android/moduleKotlinLibrary/src/test/kotlin/com/starter/sample/SampleTestClass.kt: -------------------------------------------------------------------------------- 1 | package com.starter.sample 2 | 3 | object SampleTestClass 4 | -------------------------------------------------------------------------------- /sample/android/moduleRoot/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.starter.library.kotlin") 3 | } 4 | -------------------------------------------------------------------------------- /sample/android/moduleRoot/moduleAndroidLibrary/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.starter.library.android") 3 | } 4 | 5 | android { 6 | namespace "com.starter.sample.root.library.android" 7 | } 8 | -------------------------------------------------------------------------------- /sample/android/moduleRoot/moduleAndroidLibrary/src/main/kotlin/com/starter/sample/AndroidAplicationClass.kt: -------------------------------------------------------------------------------- 1 | package com.starter.sample 2 | 3 | class AndroidAplicationClass 4 | -------------------------------------------------------------------------------- /sample/android/moduleRoot/moduleAndroidLibrary/src/test/kotlin/com/starter/sample/SampleTestClass.kt: -------------------------------------------------------------------------------- 1 | package com.starter.sample 2 | 3 | object SampleTestClass 4 | -------------------------------------------------------------------------------- /sample/android/moduleRoot/moduleKotlinLibrary/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.starter.library.kotlin") 3 | } 4 | -------------------------------------------------------------------------------- /sample/android/moduleRoot/moduleKotlinLibrary/src/main/kotlin/com/starter/sample/AndroidAplicationClass.kt: -------------------------------------------------------------------------------- 1 | package com.starter.sample 2 | 3 | class AndroidAplicationClass 4 | -------------------------------------------------------------------------------- /sample/android/moduleRoot/moduleKotlinLibrary/src/test/kotlin/com/starter/sample/SampleTestClass.kt: -------------------------------------------------------------------------------- 1 | package com.starter.sample 2 | 3 | object SampleTestClass 4 | -------------------------------------------------------------------------------- /sample/android/moduleRootAndroid/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.starter.library.android") 3 | } 4 | 5 | android { 6 | namespace "com.starter.sample.rootandroid.library" 7 | } -------------------------------------------------------------------------------- /sample/android/moduleRootAndroid/moduleAndroidLibrary/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.starter.library.android") 3 | } 4 | 5 | android { 6 | namespace "com.starter.sample.nested.library.android" 7 | } 8 | -------------------------------------------------------------------------------- /sample/android/moduleRootAndroid/moduleAndroidLibrary/src/main/kotlin/com/starter/sample/AndroidAplicationClass.kt: -------------------------------------------------------------------------------- 1 | package com.starter.sample 2 | 3 | class AndroidAplicationClass 4 | -------------------------------------------------------------------------------- /sample/android/moduleRootAndroid/moduleAndroidLibrary/src/test/kotlin/com/starter/sample/SampleTestClass.kt: -------------------------------------------------------------------------------- 1 | package com.starter.sample 2 | 3 | object SampleTestClass 4 | -------------------------------------------------------------------------------- /sample/android/moduleRootAndroid/moduleKotlinLibrary/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.starter.library.kotlin") 3 | } 4 | -------------------------------------------------------------------------------- /sample/android/moduleRootAndroid/moduleKotlinLibrary/src/main/kotlin/com/starter/sample/AndroidAplicationClass.kt: -------------------------------------------------------------------------------- 1 | package com.starter.sample 2 | 3 | /** 4 | * Sample issue link https://youtrack.jetbrains.com/issue/KT-31641 5 | */ 6 | class AndroidAplicationClass 7 | -------------------------------------------------------------------------------- /sample/android/moduleRootAndroid/moduleKotlinLibrary/src/test/kotlin/com/starter/sample/SampleTestClass.kt: -------------------------------------------------------------------------------- 1 | package com.starter.sample 2 | 3 | /** 4 | * Sample issue link https://github.com/usefulness/easylauncher-gradle-plugin/issues/25 5 | */ 6 | object SampleTestClass 7 | -------------------------------------------------------------------------------- /sample/android/moduleRootAndroid/src/main/kotlin/com/starter/sample/AndroidAplicationClass.kt: -------------------------------------------------------------------------------- 1 | package com.starter.sample 2 | 3 | class AndroidAplicationClass 4 | -------------------------------------------------------------------------------- /sample/android/moduleRootAndroid/src/test/kotlin/com/starter/sample/SampleTestClass.kt: -------------------------------------------------------------------------------- 1 | package com.starter.sample 2 | 3 | object SampleTestClass 4 | -------------------------------------------------------------------------------- /sample/android/settings.gradle: -------------------------------------------------------------------------------- 1 | pluginManagement { 2 | includeBuild("../..") 3 | repositories { 4 | google { 5 | content { 6 | includeGroupByRegex "com\\.android.*" 7 | includeGroupByRegex "androidx.*" 8 | includeGroupByRegex "android.arch.*" 9 | includeGroupByRegex "com\\.google.*" 10 | } 11 | } 12 | gradlePluginPortal { 13 | content { 14 | excludeGroup("com.project.starter") 15 | } 16 | } 17 | } 18 | } 19 | 20 | rootProject.name = "com.project.starter.sample" 21 | 22 | include ":moduleAndroidApplication" 23 | include ":moduleAndroidLibrary" 24 | include ":moduleKotlinLibrary" 25 | 26 | include ":moduleRoot:moduleAndroidLibrary" 27 | include ":moduleRoot:moduleKotlinLibrary" 28 | include ":moduleRootAndroid" 29 | include ":moduleRootAndroid:moduleAndroidLibrary" 30 | include ":moduleRootAndroid:moduleKotlinLibrary" 31 | -------------------------------------------------------------------------------- /sample/kotlin/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.starter.config") 3 | } 4 | 5 | commonConfig { 6 | javaFilesAllowed = false 7 | } 8 | -------------------------------------------------------------------------------- /sample/kotlin/gradle.properties: -------------------------------------------------------------------------------- 1 | org.gradle.parallel=true 2 | org.gradle.caching=true 3 | org.gradle.vfs.watch=true 4 | -------------------------------------------------------------------------------- /sample/kotlin/moduleKotlinLibrary/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.starter.library.kotlin") 3 | } 4 | -------------------------------------------------------------------------------- /sample/kotlin/moduleKotlinLibrary/src/main/kotlin/com/starter/sample/AndroidAplicationClass.kt: -------------------------------------------------------------------------------- 1 | package com.starter.sample 2 | 3 | class AndroidAplicationClass 4 | -------------------------------------------------------------------------------- /sample/kotlin/moduleKotlinLibrary/src/test/kotlin/com/starter/sample/SampleTestClass.kt: -------------------------------------------------------------------------------- 1 | package com.starter.sample 2 | 3 | object SampleTestClass 4 | -------------------------------------------------------------------------------- /sample/kotlin/moduleRoot/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.starter.library.kotlin") 3 | } 4 | -------------------------------------------------------------------------------- /sample/kotlin/moduleRoot/moduleKotlinLibrary/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("com.starter.library.kotlin") 3 | } 4 | -------------------------------------------------------------------------------- /sample/kotlin/moduleRoot/moduleKotlinLibrary/src/main/kotlin/com/starter/sample/AndroidAplicationClass.kt: -------------------------------------------------------------------------------- 1 | package com.starter.sample 2 | 3 | class AndroidAplicationClass 4 | -------------------------------------------------------------------------------- /sample/kotlin/moduleRoot/moduleKotlinLibrary/src/test/kotlin/com/starter/sample/SampleTestClass.kt: -------------------------------------------------------------------------------- 1 | package com.starter.sample 2 | 3 | object SampleTestClass 4 | -------------------------------------------------------------------------------- /sample/kotlin/settings.gradle: -------------------------------------------------------------------------------- 1 | import org.gradle.api.initialization.resolve.RepositoriesMode 2 | 3 | pluginManagement { 4 | includeBuild("../..") 5 | repositories { 6 | gradlePluginPortal { 7 | content { 8 | excludeGroup("com.project.starter") 9 | } 10 | } 11 | } 12 | } 13 | 14 | dependencyResolutionManagement { 15 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 16 | repositories { 17 | google() 18 | mavenCentral() 19 | } 20 | } 21 | 22 | rootProject.name = "com.project.starter.sample" 23 | 24 | include ":moduleKotlinLibrary" 25 | include ":moduleRoot:moduleKotlinLibrary" 26 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | import org.gradle.api.initialization.resolve.RepositoriesMode 2 | 3 | pluginManagement { 4 | repositories { 5 | mavenCentral() 6 | gradlePluginPortal() 7 | } 8 | } 9 | 10 | plugins { 11 | id "com.gradle.develocity" version "4.0.2" 12 | } 13 | 14 | develocity { 15 | buildScan { 16 | termsOfUseUrl = "https://gradle.com/terms-of-service" 17 | termsOfUseAgree = "yes" 18 | 19 | uploadInBackground = System.getenv("CI") == null 20 | publishing.onlyIf { false } 21 | } 22 | } 23 | 24 | dependencyResolutionManagement { 25 | repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) 26 | repositories { 27 | mavenCentral() 28 | gradlePluginPortal() 29 | google() 30 | } 31 | } 32 | 33 | rootProject.name = 'com.project.starter' 34 | 35 | includeBuild("gradle/plugins") 36 | include ":jvm", 37 | ":android", 38 | ":testing", 39 | ":config", 40 | ":quality", 41 | ":versioning", 42 | ":multiplatform" 43 | -------------------------------------------------------------------------------- /testing/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | alias(libs.plugins.starter.library.kotlin) 3 | } 4 | 5 | dependencies { 6 | api gradleTestKit() 7 | api libs.junit.jupiter 8 | api libs.assertj.core 9 | api libs.eclipse.jgit 10 | } 11 | -------------------------------------------------------------------------------- /testing/src/main/kotlin/com/project/starter/Factories.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter 2 | 3 | // language=java 4 | fun javaClass(className: String) = """ 5 | package com.example; 6 | 7 | public class $className { 8 | 9 | } 10 | 11 | """.trimIndent() 12 | 13 | // language=kotlin 14 | fun kotlinClass(className: String) = """ 15 | package com.example 16 | 17 | object $className 18 | 19 | """.trimIndent() 20 | 21 | // language=kotlin 22 | fun kotlinTestClass(className: String) = """ 23 | package com.example 24 | 25 | class $className { 26 | @org.junit.Test 27 | fun test${className.lowercase()}() = Unit 28 | } 29 | 30 | """.trimIndent() 31 | 32 | // language=kotlin 33 | fun kotlinMultiplatformTestClass(className: String) = """ 34 | package com.example 35 | 36 | class $className { 37 | @kotlin.test.Test 38 | fun test${className.lowercase()}() = Unit 39 | } 40 | 41 | """.trimIndent() 42 | -------------------------------------------------------------------------------- /testing/src/main/kotlin/com/project/starter/GitSetup.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter 2 | 3 | import org.eclipse.jgit.api.Git 4 | import org.eclipse.jgit.lib.ConfigConstants.CONFIG_BRANCH_SECTION 5 | import org.eclipse.jgit.lib.ConfigConstants.CONFIG_KEY_MERGE 6 | import org.eclipse.jgit.lib.Constants 7 | 8 | fun WithGradleProjectTest.setupGit(): Git { 9 | val git = Git.init() 10 | .apply { setDirectory(rootDirectory) } 11 | .call() 12 | git.repository.config 13 | .apply { 14 | val branchName = "master" 15 | setString(CONFIG_BRANCH_SECTION, branchName, CONFIG_KEY_MERGE, Constants.R_HEADS + branchName) 16 | } 17 | .save() 18 | rootDirectory.resolve(".gitignore").writeText( 19 | """ 20 | .gradle/ 21 | **/build/ 22 | 23 | # Due jacoco-testkit integration 24 | gradle.properties 25 | """.trimIndent(), 26 | ) 27 | git.commit("init") 28 | 29 | return git 30 | } 31 | 32 | fun Git.checkout(refName: String) { 33 | checkout() 34 | .apply { setName(refName) } 35 | .call() 36 | } 37 | 38 | fun Git.commit(commitMessage: String) { 39 | repository.directory.resolve("File.txt").appendText( 40 | """ 41 | | Text 42 | """.trimMargin(), 43 | ) 44 | add() 45 | .apply { addFilepattern(".") } 46 | .call() 47 | commit() 48 | .apply { 49 | setAllowEmpty(true) 50 | setSign(false) 51 | message = commitMessage 52 | } 53 | .call() 54 | } 55 | 56 | fun Git.tag(tagName: String) { 57 | tag() 58 | .apply { 59 | name = tagName 60 | isAnnotated = false 61 | isSigned = false 62 | } 63 | .call() 64 | } 65 | -------------------------------------------------------------------------------- /testing/src/main/kotlin/com/project/starter/WithGradleProjectTest.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter 2 | 3 | import org.gradle.testkit.runner.BuildResult 4 | import org.gradle.testkit.runner.GradleRunner 5 | import org.junit.jupiter.api.io.TempDir 6 | import java.io.File 7 | import java.io.InputStream 8 | 9 | @Suppress("UnnecessaryAbstractClass") 10 | abstract class WithGradleProjectTest { 11 | 12 | @TempDir 13 | lateinit var rootDirectory: File 14 | 15 | protected fun runTask(vararg taskName: String, shouldFail: Boolean = false, configurationCacheEnabled: Boolean = false): BuildResult = 16 | GradleRunner.create().apply { 17 | forwardOutput() 18 | withPluginClasspath() 19 | withProjectDir(rootDirectory) 20 | 21 | withArguments( 22 | buildList { 23 | addAll(taskName) 24 | if (configurationCacheEnabled) { 25 | add("--configuration-cache") 26 | } 27 | }, 28 | ) 29 | 30 | // https://docs.gradle.org/8.1.1/userguide/configuration_cache.html#config_cache:not_yet_implemented:testkit_build_with_java_agent 31 | if (!configurationCacheEnabled) { 32 | withJaCoCo() 33 | } 34 | }.run { 35 | if (shouldFail) { 36 | buildAndFail() 37 | } else { 38 | build() 39 | } 40 | } 41 | 42 | private fun GradleRunner.withJaCoCo(): GradleRunner { 43 | javaClass.classLoader.getResourceAsStream("testkit-gradle.properties") 44 | ?.toFile(File(projectDir, "gradle.properties")) 45 | return this 46 | } 47 | 48 | private fun InputStream.toFile(file: File) { 49 | use { input -> 50 | file.outputStream().use { input.copyTo(it) } 51 | } 52 | } 53 | 54 | protected fun File.resolve(relative: String, receiver: File.() -> Unit): File = resolve(relative).apply { 55 | parentFile.mkdirs() 56 | receiver() 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /versioning/api/versioning.api: -------------------------------------------------------------------------------- 1 | public final class com/project/starter/versioning/plugins/VersioningPlugin : org/gradle/api/Plugin { 2 | public static final field Companion Lcom/project/starter/versioning/plugins/VersioningPlugin$Companion; 3 | public fun ()V 4 | public synthetic fun apply (Ljava/lang/Object;)V 5 | public fun apply (Lorg/gradle/api/Project;)V 6 | } 7 | 8 | public final class com/project/starter/versioning/plugins/VersioningPlugin$Companion { 9 | } 10 | 11 | public abstract class com/project/starter/versioning/plugins/VersioningPlugin$CurrentVersionTask : org/gradle/api/DefaultTask { 12 | public fun (Lorg/gradle/api/model/ObjectFactory;)V 13 | public final fun getGitVersion ()Lorg/gradle/api/provider/Property; 14 | public final fun run ()V 15 | } 16 | 17 | public final class com/project/starter/versioning/plugins/VersioningPlugin$GitVersion { 18 | public fun (IIIZ)V 19 | public final fun component1 ()I 20 | public final fun component2 ()I 21 | public final fun component3 ()I 22 | public final fun component4 ()Z 23 | public final fun copy (IIIZ)Lcom/project/starter/versioning/plugins/VersioningPlugin$GitVersion; 24 | public static synthetic fun copy$default (Lcom/project/starter/versioning/plugins/VersioningPlugin$GitVersion;IIIZILjava/lang/Object;)Lcom/project/starter/versioning/plugins/VersioningPlugin$GitVersion; 25 | public fun equals (Ljava/lang/Object;)Z 26 | public final fun getDecorated ()Ljava/lang/String; 27 | public final fun getMajor ()I 28 | public final fun getMinor ()I 29 | public final fun getPatch ()I 30 | public final fun getUndecorated ()Ljava/lang/String; 31 | public fun hashCode ()I 32 | public final fun isSnapshot ()Z 33 | public fun toString ()Ljava/lang/String; 34 | } 35 | 36 | public abstract class com/project/starter/versioning/plugins/VersioningPlugin$GitVersionValueSource : org/gradle/api/provider/ValueSource { 37 | public fun (Lorg/gradle/process/ExecOperations;)V 38 | public fun obtain ()Lcom/project/starter/versioning/plugins/VersioningPlugin$GitVersion; 39 | public synthetic fun obtain ()Ljava/lang/Object; 40 | } 41 | 42 | -------------------------------------------------------------------------------- /versioning/build.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id("java-gradle-plugin") 3 | alias(libs.plugins.droidsonroids.jacocotestkit) 4 | alias(libs.plugins.starter.library.kotlin) 5 | alias(libs.plugins.kotlin.samwithreceiver) 6 | alias(libs.plugins.kotlinx.binarycompatibility) 7 | id("com.starter.publishing") 8 | } 9 | 10 | dependencies { 11 | compileOnly libs.agp.gradle.api 12 | implementation project(":config") 13 | 14 | testImplementation project(":testing") 15 | 16 | testRuntimeOnly(libs.junit.platform.launcher) 17 | testRuntimeDependencies(libs.jetbrains.kotlin.jvm.implementation) 18 | } 19 | 20 | tasks.named("test") { 21 | useJUnitPlatform() 22 | } 23 | 24 | gradlePlugin { 25 | plugins { 26 | versioning { 27 | id = 'com.starter.versioning' 28 | displayName = 'Versioning Plugin' 29 | implementationClass = 'com.project.starter.versioning.plugins.VersioningPlugin' 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /versioning/src/main/kotlin/com/project/starter/versioning/plugins/VersioningPlugin.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.versioning.plugins 2 | 3 | import com.project.starter.config.getByType 4 | import org.gradle.api.DefaultTask 5 | import org.gradle.api.GradleException 6 | import org.gradle.api.Plugin 7 | import org.gradle.api.Project 8 | import org.gradle.api.model.ObjectFactory 9 | import org.gradle.api.provider.Property 10 | import org.gradle.api.provider.Provider 11 | import org.gradle.api.provider.ValueSource 12 | import org.gradle.api.provider.ValueSourceParameters 13 | import org.gradle.api.tasks.Input 14 | import org.gradle.api.tasks.TaskAction 15 | import org.gradle.process.ExecOperations 16 | import java.io.ByteArrayOutputStream 17 | import java.nio.charset.Charset 18 | import javax.inject.Inject 19 | 20 | class VersioningPlugin : Plugin { 21 | 22 | override fun apply(target: Project): Unit = with(target) { 23 | if (this != rootProject) throw GradleException("Versioning plugin can be applied to the root project only") 24 | 25 | val gitVersionProvider = providers.of(GitVersionValueSource::class.java) {} 26 | 27 | allprojects { 28 | val get = gitVersionProvider.get() 29 | version = get.decorated 30 | setupAndroidVersioning(gitVersionProvider) 31 | } 32 | 33 | tasks.register("currentVersion", CurrentVersionTask::class.java) { 34 | gitVersion.set(gitVersionProvider) 35 | } 36 | } 37 | 38 | private fun Project.setupAndroidVersioning(gitVersionProvider: Provider) { 39 | pluginManager.withPlugin("com.android.application") { 40 | extensions.getByType().defaultConfig { 41 | val gitVersion = gitVersionProvider.get() 42 | val major = gitVersion.major 43 | val minor = gitVersion.minor 44 | val patch = gitVersion.patch 45 | 46 | versionCode = major * MAJOR_MULTIPLIER + minor * MINOR_MULTIPLIER + patch 47 | versionName = gitVersion.undecorated 48 | } 49 | } 50 | } 51 | 52 | abstract class GitVersionValueSource @Inject constructor(private val execOperations: ExecOperations) : 53 | ValueSource { 54 | 55 | override fun obtain(): GitVersion { 56 | fun defaultVersion() = GitVersion( 57 | major = 0, 58 | minor = 1, 59 | patch = 0, 60 | isSnapshot = true, 61 | ) 62 | 63 | val status = runGit("status", "--porcelain") 64 | val currentTag = runGit("tag", "--points-at").takeIf(String::isNotBlank)?.split("\n")?.maxOrNull() 65 | 66 | val lastTag = if (currentTag == null) { 67 | val lastGitTag = runGit("describe", "--tags", "--abbrev=0", "--always") 68 | val lastReleaseCommit = runGit("rev-parse", lastGitTag) 69 | val lastTags = runGit("tag", "--contains", lastReleaseCommit).split("\n") 70 | lastTags.maxOrNull() 71 | } else { 72 | currentTag 73 | } ?: return defaultVersion() 74 | 75 | val isOnTag = currentTag != null 76 | val isDirty = status.isNotBlank() 77 | 78 | val isSnapshot = !isOnTag || isDirty 79 | 80 | val versionRegex = "([0-9]+)\\.([0-9]+)\\.([0-9]+)".toRegex() 81 | val result = versionRegex.find(lastTag) ?: return defaultVersion() 82 | 83 | val major = result.groups[1]?.value?.toIntOrNull() ?: return defaultVersion() 84 | val minor = result.groups[2]?.value?.toIntOrNull() ?: return defaultVersion() 85 | val patch = result.groups[3]?.value?.toIntOrNull() ?: return defaultVersion() 86 | 87 | return GitVersion( 88 | major = major, 89 | minor = if (isSnapshot) minor + 1 else minor, 90 | patch = if (isSnapshot) 0 else patch, 91 | isSnapshot = isSnapshot, 92 | ) 93 | } 94 | 95 | private fun runGit(vararg args: String) = ByteArrayOutputStream().use { output -> 96 | execOperations.exec { 97 | executable("git") 98 | args(args.toList()) 99 | standardOutput = output 100 | } 101 | 102 | output.toString(Charset.defaultCharset()).trim() 103 | } 104 | } 105 | 106 | abstract class CurrentVersionTask @Inject constructor(objectFactory: ObjectFactory) : DefaultTask() { 107 | 108 | @Input 109 | val gitVersion: Property = objectFactory.property(GitVersion::class.java) 110 | 111 | @TaskAction 112 | fun run() { 113 | logger.quiet("Current version: ${gitVersion.get().decorated}") 114 | } 115 | } 116 | 117 | data class GitVersion( 118 | val major: Int, 119 | val minor: Int, 120 | val patch: Int, 121 | val isSnapshot: Boolean, 122 | ) { 123 | 124 | val undecorated = "$major.$minor.$patch" 125 | val decorated = "$major.$minor.$patch${if (isSnapshot) "-SNAPSHOT" else ""}" 126 | } 127 | 128 | companion object { 129 | private const val MAJOR_MULTIPLIER = 1_000_000 130 | private const val MINOR_MULTIPLIER = 1_000 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /versioning/src/test/kotlin/com/project/starter/versioning/VersioningPluginTest.kt: -------------------------------------------------------------------------------- 1 | package com.project.starter.versioning 2 | 3 | import com.project.starter.WithGradleProjectTest 4 | import com.project.starter.checkout 5 | import com.project.starter.commit 6 | import com.project.starter.setupGit 7 | import com.project.starter.tag 8 | import org.assertj.core.api.Assertions.assertThat 9 | import org.eclipse.jgit.api.Git 10 | import org.gradle.testkit.runner.TaskOutcome 11 | import org.junit.jupiter.api.BeforeEach 12 | import org.junit.jupiter.api.Test 13 | import java.io.File 14 | 15 | internal class VersioningPluginTest : WithGradleProjectTest() { 16 | 17 | private lateinit var module1Root: File 18 | private lateinit var module2Root: File 19 | private lateinit var git: Git 20 | 21 | @BeforeEach 22 | fun setUp() { 23 | rootDirectory.apply { 24 | resolve("settings.gradle").writeText("""include ":module1", ":module2" """) 25 | 26 | resolve("build.gradle").writeText( 27 | """ 28 | plugins { 29 | id 'com.starter.versioning' 30 | } 31 | 32 | """.trimIndent(), 33 | ) 34 | module1Root = resolve("module1") { 35 | resolve("build.gradle") { 36 | writeText( 37 | """ 38 | plugins { 39 | id 'org.jetbrains.kotlin.jvm' 40 | } 41 | """.trimIndent(), 42 | ) 43 | } 44 | } 45 | module2Root = resolve("module2") { 46 | resolve("build.gradle") { 47 | writeText( 48 | """ 49 | plugins { 50 | id 'org.jetbrains.kotlin.jvm' 51 | } 52 | """.trimIndent(), 53 | ) 54 | } 55 | } 56 | } 57 | git = setupGit() 58 | } 59 | 60 | @Test 61 | fun emptyRepo() { 62 | assertThat(runTask("currentVersion").output).contains("0.1.0-SNAPSHOT") 63 | } 64 | 65 | @Test 66 | fun `fails if not applied to root project`() { 67 | module1Root.resolve("build.gradle").writeText( 68 | """ 69 | apply plugin: "com.starter.versioning" 70 | 71 | """.trimIndent(), 72 | ) 73 | 74 | val result = runTask("help", shouldFail = true) 75 | 76 | assertThat(result.output).contains("Versioning plugin can be applied to the root project only") 77 | } 78 | 79 | @Test 80 | fun `sets version to all projects`() { 81 | git.tag("v1.1.0") 82 | git.commit("features in 2.11.1234") 83 | git.tag("v2.11.1234") 84 | 85 | val modules = listOf(":module1", ":module1", "") 86 | 87 | modules.forEach { 88 | val moduleResult = runTask("$it:properties") 89 | 90 | assertThat(moduleResult.output).contains("version: 2.11.1234") 91 | } 92 | } 93 | 94 | @Test 95 | fun `when multiple tags on the same commit`() { 96 | git.tag("v1.1.0") 97 | git.tag("v1.2.1") 98 | assertThat(runTask("currentVersion").output).contains("1.2.1") 99 | 100 | git.commit("test commit") 101 | assertThat(runTask("currentVersion").output).contains("1.3.0-SNAPSHOT") 102 | 103 | git.tag("v1.2.123") 104 | git.tag("v1.3.0") 105 | git.tag("v1.2.146") 106 | assertThat(runTask("currentVersion").output).contains("1.3.0") 107 | 108 | git.commit("after all the mess") 109 | assertThat(runTask("currentVersion").output).contains("1.4.0-SNAPSHOT") 110 | } 111 | 112 | @Test 113 | fun `regular release flow`() { 114 | git.tag("v1.1.0") 115 | assertThat(runTask("currentVersion").output).contains("1.1.0") 116 | 117 | git.commit("contains 1.2.0 features") 118 | assertThat(runTask("currentVersion").output).contains("1.2.0-SNAPSHOT") 119 | 120 | git.tag("v1.2.0") 121 | assertThat(runTask("currentVersion").output).contains("1.2.0") 122 | 123 | git.commit("contains 1.3.0 features") 124 | assertThat(runTask("currentVersion").output).contains("1.3.0-SNAPSHOT") 125 | 126 | git.commit("contains another set of 1.3.0 features") 127 | assertThat(runTask("currentVersion").output).contains("1.3.0-SNAPSHOT") 128 | 129 | git.tag("v1.2.1") 130 | assertThat(runTask("currentVersion").output).contains("1.2.1") 131 | 132 | git.commit("contains 1.3.0 features") 133 | assertThat(runTask("currentVersion").output).contains("1.3.0-SNAPSHOT") 134 | 135 | git.checkout("v1.2.0") 136 | assertThat(runTask("currentVersion").output).contains("1.2.0") 137 | } 138 | 139 | @Test 140 | fun `version on branch`() { 141 | git.tag("v1.1.0") 142 | git.commit("contains 1.2.3 features") 143 | git.tag("v1.2.3") 144 | 145 | git.branchCreate().setName("testBranch").call() 146 | git.checkout("testBranch") 147 | assertThat(runTask("currentVersion").output).contains("1.2.3") 148 | 149 | git.commit("we're on a branch") 150 | assertThat(runTask("currentVersion").output).contains("1.3.0-SNAPSHOT") 151 | 152 | git.checkout("master") 153 | assertThat(runTask("currentVersion").output).contains("1.2.3") 154 | 155 | git.commit("contains 1.2.4 features") 156 | git.tag("v1.2.4") 157 | assertThat(runTask("currentVersion").output).contains("1.2.4") 158 | 159 | git.checkout("testBranch") 160 | assertThat(runTask("currentVersion").output).contains("1.3.0-SNAPSHOT") 161 | } 162 | 163 | @Test 164 | fun configurationCacheCompatibility() { 165 | git.tag("v1.1.0") 166 | git.commit("contains 1.2.0 features") 167 | git.tag("v1.2.0") 168 | git.commit("contains 1.3.0 features") 169 | 170 | val result = runTask("currentVersion", configurationCacheEnabled = true) 171 | assertThat(result.task(":currentVersion")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 172 | assertThat(result.output) 173 | .contains("Calculating task graph as no cached configuration is available for tasks") 174 | .contains("1.3.0-SNAPSHOT") 175 | 176 | val result2 = runTask("currentVersion", configurationCacheEnabled = true) 177 | assertThat(result2.task(":currentVersion")?.outcome).isEqualTo(TaskOutcome.SUCCESS) 178 | assertThat(result2.output) 179 | .contains("Reusing configuration cache") 180 | .contains("1.3.0-SNAPSHOT") 181 | } 182 | } 183 | --------------------------------------------------------------------------------