├── .editorconfig
├── .github
└── workflows
│ └── ci.yml
├── .gitignore
├── CHANGELOG.md
├── LICENSE
├── README.md
├── RELEASING.md
├── api
└── app-versioning.api
├── build.gradle.kts
├── detekt.yml
├── docs
└── images
│ └── sample.png
├── gradle.properties
├── gradle
├── gradle-daemon-jvm.properties
├── libs.versions.toml
└── wrapper
│ ├── gradle-wrapper.jar
│ └── gradle-wrapper.properties
├── gradlew
├── gradlew.bat
├── settings.gradle.kts
└── src
├── functionalTest
└── kotlin
│ └── io
│ └── github
│ └── reactivecircus
│ └── appversioning
│ ├── AppVersioningPluginIntegrationTest.kt
│ ├── fixtures
│ ├── FixtureRunner.kt
│ └── ProjectTemplates.kt
│ └── tasks
│ ├── GenerateAppVersionInfoTest.kt
│ └── PrintAppVersionInfoTest.kt
├── main
└── kotlin
│ └── io
│ └── github
│ └── reactivecircus
│ └── appversioning
│ ├── AppVersioningExtension.kt
│ ├── AppVersioningPlugin.kt
│ ├── GitTag.kt
│ ├── SemVer.kt
│ ├── VariantInfo.kt
│ ├── internal
│ └── GitClient.kt
│ └── tasks
│ ├── GenerateAppVersionInfo.kt
│ └── PrintAppVersionInfo.kt
└── test
└── kotlin
└── io
└── github
└── reactivecircus
└── appversioning
├── GitTagTest.kt
├── SemVerTest.kt
└── VariantInfoTest.kt
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | insert_final_newline = true
5 | trim_trailing_whitespace = true
6 | max_line_length = 200
7 |
--------------------------------------------------------------------------------
/.github/workflows/ci.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 |
3 | on:
4 | pull_request:
5 | branches:
6 | - main
7 | paths-ignore:
8 | - '**/*.md'
9 | push:
10 | branches:
11 | - main
12 | paths-ignore:
13 | - '**/*.md'
14 |
15 | env:
16 | GRADLE_OPTS: -Dorg.gradle.jvmargs="-Xmx4g -XX:+HeapDumpOnOutOfMemoryError"
17 | TERM: dumb
18 |
19 | jobs:
20 | assemble:
21 | name: Assemble
22 | runs-on: ubuntu-latest
23 | env:
24 | JAVA_TOOL_OPTIONS: -Xmx4g -XX:+UseParallelGC
25 |
26 | steps:
27 | - uses: actions/checkout@v4
28 | - uses: actions/setup-java@v4
29 | with:
30 | distribution: 'zulu'
31 | java-version: '24'
32 | - uses: gradle/actions/setup-gradle@v4
33 | with:
34 | cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
35 | - name: Assemble
36 | run: ./gradlew assemble
37 |
38 | checks:
39 | name: Checks (unit tests, static analysis, plugin validations and binary compatibility API check)
40 | runs-on: ubuntu-latest
41 | env:
42 | JAVA_TOOL_OPTIONS: -Xmx4g -XX:+UseParallelGC
43 |
44 | steps:
45 | - uses: actions/checkout@v4
46 | - uses: actions/setup-java@v4
47 | with:
48 | distribution: 'zulu'
49 | java-version: '24'
50 | - uses: gradle/actions/setup-gradle@v4
51 | with:
52 | cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
53 | - name: Checks
54 | run: ./gradlew test detekt validatePlugins apiCheck
55 |
56 | functional-tests:
57 | name: Functional tests
58 | runs-on: ubuntu-latest
59 | env:
60 | JAVA_TOOL_OPTIONS: -Xmx4g -XX:+UseParallelGC
61 | AGP_VERSION: ${{ matrix.agp-version }}
62 | strategy:
63 | matrix:
64 | agp-version: [ 8.0.2, 8.1.4, 8.2.2, 8.3.2, 8.4.2, 8.5.2, 8.6.1, 8.7.3, 8.8.2, 8.9.2, 8.10.1, 8.11.0-rc01, 8.12.0-alpha03 ]
65 |
66 | steps:
67 | - uses: actions/checkout@v4
68 | - uses: actions/setup-java@v4
69 | with:
70 | distribution: 'zulu'
71 | java-version: '24'
72 | - name: Configure git user
73 | run: |
74 | git config --global user.name "GitHub Action"
75 | git config --global user.email "action@github.com"
76 | - uses: gradle/actions/setup-gradle@v4
77 | with:
78 | cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
79 | - name: Functional tests
80 | run: ./gradlew functionalTest
81 | - name: Upload test reports
82 | if: failure()
83 | uses: actions/upload-artifact@v4
84 | with:
85 | name: test-reports-${{ matrix.agp-version }}
86 | path: build/reports/tests/functionalTest
87 | retention-days: 3
88 |
89 | deploy-snapshot:
90 | name: Deploy snapshot
91 | needs: [assemble, checks, functional-tests]
92 | if: github.ref == 'refs/heads/main'
93 | runs-on: ubuntu-latest
94 | env:
95 | JAVA_TOOL_OPTIONS: -Xmx4g -XX:+UseParallelGC
96 | ORG_GRADLE_PROJECT_mavenCentralUsername: ${{ secrets.SONATYPE_NEXUS_USERNAME }}
97 | ORG_GRADLE_PROJECT_mavenCentralPassword: ${{ secrets.SONATYPE_NEXUS_PASSWORD }}
98 |
99 | steps:
100 | - uses: actions/checkout@v4
101 | - uses: actions/setup-java@v4
102 | with:
103 | distribution: 'zulu'
104 | java-version: '24'
105 | - uses: gradle/actions/setup-gradle@v4
106 | with:
107 | cache-encryption-key: ${{ secrets.GRADLE_ENCRYPTION_KEY }}
108 | - name: Deploy snapshot
109 | run: ./gradlew publish
110 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .gradle
2 | .kotlin/
3 | build/
4 | out/
5 |
6 | # Ignore Gradle GUI config
7 | gradle-app.setting
8 |
9 | # Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored)
10 | !gradle-wrapper.jar
11 |
12 | local.properties
13 |
14 | .idea/
15 | *.iml
16 |
17 | *.DS_Store
18 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # Change Log
2 |
3 | ## Unreleased
4 |
5 | ### Changed
6 | - Minimum Android Gradle Plugin version is now **8.0.0**.
7 | - Remove `compileOnly` dependency on `com.android.tools:common` to support future version of AGP.
8 |
9 | ## 1.4.0
10 |
11 | ### Added
12 | - Support Splits/Multi-APKs and universal APK modes.
13 |
14 | ### Changed
15 | - Compile with AGP 8.9.0.
16 | - Compile with Kotlin 2.1.0.
17 |
18 | ## 1.3.2
19 |
20 | ### Changed
21 | - Compile with AGP 8.3.0.
22 | - Compile with Kotlin 1.9.23.
23 |
24 | ## 1.3.1
25 |
26 | ### Fixed
27 | - Fix an issue where the version info files cannot be found when configuration cache is on - [#30](https://github.com/ReactiveCircus/app-versioning/pull/30)
28 |
29 | ## 1.3.0
30 |
31 | ### Changed
32 | - Compile with AGP 8.0.0.
33 | - Compile with Kotlin 1.8.20.
34 |
35 | ## 1.2.0
36 |
37 | ### Fixed
38 | - Stop depending on `com.android.tools:sdk-common` to support AGP 8.
39 |
40 | ### Changed
41 | - Compile with AGP 7.3.1.
42 | - Compile with Kotlin 1.7.20.
43 |
44 | ## 1.1.2
45 |
46 | ### Fixed
47 | - Fix an issue where changing git HEAD does not invalidate task cache - [#28](https://github.com/ReactiveCircus/app-versioning/pull/28)
48 |
49 | ### Changed
50 | - Compile with AGP 7.1.3.
51 | - Compile with Kotlin 1.6.21.
52 |
53 | ## 1.1.1
54 |
55 | Same as 1.1.0.
56 |
57 | ## 1.1.0
58 |
59 | ### Added
60 | - Support specifying **bare** git repository e.g. `app.git`.
61 |
62 | ### Changed
63 | - Compile with AGP 7.0.3.
64 |
65 | ## 1.0.0
66 |
67 | This is our first stable release. Thanks everyone for trying out the plugin and sending bug reports and feature requests!
68 |
69 | ### Changed
70 | - Compile with AGP 7.0.0.
71 |
72 | ## 1.0.0-rc01
73 |
74 | ### Changed
75 | - Minimum Android Gradle Plugin version is now **7.0.0-beta04**.
76 |
77 | ## 0.10.0
78 |
79 | This is the final release of the plugin that's compatible with **Android Gradle Plugin 4.2**.
80 |
81 | When AGP **7.0.0-beta01** was released, we thought all the APIs we use are stable and can therefore support **4.2.1** which have the same APIs we were using.
82 | Unfortunately **AGP 7.0.0-beta04** moved `ApplicationAndroidComponentsExtension` to a new package and deprecated the old one. In order to move to the new `ApplicationAndroidComponentsExtension`
83 | before our 1.0 release and avoid the overhead of publishing multiple artifacts, we decided to start requiring **AGP 7.0.0-beta04** in the next release.
84 |
85 | ### Changed
86 | - Compile with AGP 7.0.0-rc01.
87 | - Compile with Kotlin 1.5.21.
88 |
89 | ## 0.9.1
90 |
91 | ### Changed
92 | - Minimum Gradle version is **6.8**.
93 |
94 | ## 0.9.0
95 |
96 | ### Changed
97 | - Minimum Android Gradle Plugin version is now **4.2.1**. All versions of **AGP 7.x** are supported.
98 | - Compile with AGP 7.0.0-beta03.
99 | - Compile with Kotlin 1.5.10.
100 |
101 | ## 0.8.1
102 |
103 | ### Added
104 | - Support setting git root directory explicitly when the root Gradle project is not the git root.
105 |
106 | ## 0.8.0
107 |
108 | ### Added
109 | - Support tag filtering with custom glob pattern.
110 |
111 | ### Changed
112 | - Compile with AGP 4.2.0-beta02.
113 | - Target Java 11 bytecode.
114 | - Kotlin 1.4.21.
115 | - Gradle 6.8-rc-4.
116 |
117 | ## 0.7.0
118 |
119 | ### Changed
120 | - Change minimum Android Gradle Plugin version to **4.2.0-beta01**.
121 | - Support AGP 7.0.0-alpha02.
122 | - Kotlin 1.4.20.
123 | - Gradle 6.8-rc-1.
124 |
125 | ## 0.6.0
126 |
127 | ### Added
128 | - Add `VariantInfo` lambda parameters to `overrideVersionVode` and `overrideVersionName` to support customizing `versionCode` and `versionName` based on build variants.
129 |
130 | ### Changed
131 | - Change `AppVersioningPlugin` from `internal` to `public` to support type-safe plugin application in `buildSrc`.
132 | - AGP 4.2.0-alpha16.
133 | - Kotlin 1.4.20-RC.
134 |
135 | ### Fixed
136 | - Disable IR to support applying the plugin from `buildSrc`.
137 |
138 | ## 0.5.0
139 |
140 | The plugin now requires the latest version of Android Gradle Plugin (currently `4.2.0-alpha13`) until the next variant APIs become stable.
141 |
142 | Please use version `0.4.0` if you want to use the plugin with AGP 4.0 or 4.1.
143 |
144 | ## 0.4.0
145 |
146 | This is the final release of the plugin that's compatible with **Android Gradle Plugin 4.0 and 4.1**.
147 |
148 | Starting from the next release, the latest version of AGP (currently `4.2.0-alpha13`) will be required, until the new variant APIs become stable which is expected to happen with the next major version of AGP.
149 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Apache License
2 | Version 2.0, January 2004
3 | http://www.apache.org/licenses/
4 |
5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
6 |
7 | 1. Definitions.
8 |
9 | "License" shall mean the terms and conditions for use, reproduction,
10 | and distribution as defined by Sections 1 through 9 of this document.
11 |
12 | "Licensor" shall mean the copyright owner or entity authorized by
13 | the copyright owner that is granting the License.
14 |
15 | "Legal Entity" shall mean the union of the acting entity and all
16 | other entities that control, are controlled by, or are under common
17 | control with that entity. For the purposes of this definition,
18 | "control" means (i) the power, direct or indirect, to cause the
19 | direction or management of such entity, whether by contract or
20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the
21 | outstanding shares, or (iii) beneficial ownership of such entity.
22 |
23 | "You" (or "Your") shall mean an individual or Legal Entity
24 | exercising permissions granted by this License.
25 |
26 | "Source" form shall mean the preferred form for making modifications,
27 | including but not limited to software source code, documentation
28 | source, and configuration files.
29 |
30 | "Object" form shall mean any form resulting from mechanical
31 | transformation or translation of a Source form, including but
32 | not limited to compiled object code, generated documentation,
33 | and conversions to other media types.
34 |
35 | "Work" shall mean the work of authorship, whether in Source or
36 | Object form, made available under the License, as indicated by a
37 | copyright notice that is included in or attached to the work
38 | (an example is provided in the Appendix below).
39 |
40 | "Derivative Works" shall mean any work, whether in Source or Object
41 | form, that is based on (or derived from) the Work and for which the
42 | editorial revisions, annotations, elaborations, or other modifications
43 | represent, as a whole, an original work of authorship. For the purposes
44 | of this License, Derivative Works shall not include works that remain
45 | separable from, or merely link (or bind by name) to the interfaces of,
46 | the Work and Derivative Works thereof.
47 |
48 | "Contribution" shall mean any work of authorship, including
49 | the original version of the Work and any modifications or additions
50 | to that Work or Derivative Works thereof, that is intentionally
51 | submitted to Licensor for inclusion in the Work by the copyright owner
52 | or by an individual or Legal Entity authorized to submit on behalf of
53 | the copyright owner. For the purposes of this definition, "submitted"
54 | means any form of electronic, verbal, or written communication sent
55 | to the Licensor or its representatives, including but not limited to
56 | communication on electronic mailing lists, source code control systems,
57 | and issue tracking systems that are managed by, or on behalf of, the
58 | Licensor for the purpose of discussing and improving the Work, but
59 | excluding communication that is conspicuously marked or otherwise
60 | designated in writing by the copyright owner as "Not a Contribution."
61 |
62 | "Contributor" shall mean Licensor and any individual or Legal Entity
63 | on behalf of whom a Contribution has been received by Licensor and
64 | subsequently incorporated within the Work.
65 |
66 | 2. Grant of Copyright License. Subject to the terms and conditions of
67 | this License, each Contributor hereby grants to You a perpetual,
68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
69 | copyright license to reproduce, prepare Derivative Works of,
70 | publicly display, publicly perform, sublicense, and distribute the
71 | Work and such Derivative Works in Source or Object form.
72 |
73 | 3. Grant of Patent License. Subject to the terms and conditions of
74 | this License, each Contributor hereby grants to You a perpetual,
75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable
76 | (except as stated in this section) patent license to make, have made,
77 | use, offer to sell, sell, import, and otherwise transfer the Work,
78 | where such license applies only to those patent claims licensable
79 | by such Contributor that are necessarily infringed by their
80 | Contribution(s) alone or by combination of their Contribution(s)
81 | with the Work to which such Contribution(s) was submitted. If You
82 | institute patent litigation against any entity (including a
83 | cross-claim or counterclaim in a lawsuit) alleging that the Work
84 | or a Contribution incorporated within the Work constitutes direct
85 | or contributory patent infringement, then any patent licenses
86 | granted to You under this License for that Work shall terminate
87 | as of the date such litigation is filed.
88 |
89 | 4. Redistribution. You may reproduce and distribute copies of the
90 | Work or Derivative Works thereof in any medium, with or without
91 | modifications, and in Source or Object form, provided that You
92 | meet the following conditions:
93 |
94 | (a) You must give any other recipients of the Work or
95 | Derivative Works a copy of this License; and
96 |
97 | (b) You must cause any modified files to carry prominent notices
98 | stating that You changed the files; and
99 |
100 | (c) You must retain, in the Source form of any Derivative Works
101 | that You distribute, all copyright, patent, trademark, and
102 | attribution notices from the Source form of the Work,
103 | excluding those notices that do not pertain to any part of
104 | the Derivative Works; and
105 |
106 | (d) If the Work includes a "NOTICE" text file as part of its
107 | distribution, then any Derivative Works that You distribute must
108 | include a readable copy of the attribution notices contained
109 | within such NOTICE file, excluding those notices that do not
110 | pertain to any part of the Derivative Works, in at least one
111 | of the following places: within a NOTICE text file distributed
112 | as part of the Derivative Works; within the Source form or
113 | documentation, if provided along with the Derivative Works; or,
114 | within a display generated by the Derivative Works, if and
115 | wherever such third-party notices normally appear. The contents
116 | of the NOTICE file are for informational purposes only and
117 | do not modify the License. You may add Your own attribution
118 | notices within Derivative Works that You distribute, alongside
119 | or as an addendum to the NOTICE text from the Work, provided
120 | that such additional attribution notices cannot be construed
121 | as modifying the License.
122 |
123 | You may add Your own copyright statement to Your modifications and
124 | may provide additional or different license terms and conditions
125 | for use, reproduction, or distribution of Your modifications, or
126 | for any such Derivative Works as a whole, provided Your use,
127 | reproduction, and distribution of the Work otherwise complies with
128 | the conditions stated in this License.
129 |
130 | 5. Submission of Contributions. Unless You explicitly state otherwise,
131 | any Contribution intentionally submitted for inclusion in the Work
132 | by You to the Licensor shall be under the terms and conditions of
133 | this License, without any additional terms or conditions.
134 | Notwithstanding the above, nothing herein shall supersede or modify
135 | the terms of any separate license agreement you may have executed
136 | with Licensor regarding such Contributions.
137 |
138 | 6. Trademarks. This License does not grant permission to use the trade
139 | names, trademarks, service marks, or product names of the Licensor,
140 | except as required for reasonable and customary use in describing the
141 | origin of the Work and reproducing the content of the NOTICE file.
142 |
143 | 7. Disclaimer of Warranty. Unless required by applicable law or
144 | agreed to in writing, Licensor provides the Work (and each
145 | Contributor provides its Contributions) on an "AS IS" BASIS,
146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
147 | implied, including, without limitation, any warranties or conditions
148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
149 | PARTICULAR PURPOSE. You are solely responsible for determining the
150 | appropriateness of using or redistributing the Work and assume any
151 | risks associated with Your exercise of permissions under this License.
152 |
153 | 8. Limitation of Liability. In no event and under no legal theory,
154 | whether in tort (including negligence), contract, or otherwise,
155 | unless required by applicable law (such as deliberate and grossly
156 | negligent acts) or agreed to in writing, shall any Contributor be
157 | liable to You for damages, including any direct, indirect, special,
158 | incidental, or consequential damages of any character arising as a
159 | result of this License or out of the use or inability to use the
160 | Work (including but not limited to damages for loss of goodwill,
161 | work stoppage, computer failure or malfunction, or any and all
162 | other commercial damages or losses), even if such Contributor
163 | has been advised of the possibility of such damages.
164 |
165 | 9. Accepting Warranty or Additional Liability. While redistributing
166 | the Work or Derivative Works thereof, You may choose to offer,
167 | and charge a fee for, acceptance of support, warranty, indemnity,
168 | or other liability obligations and/or rights consistent with this
169 | License. However, in accepting such obligations, You may act only
170 | on Your own behalf and on Your sole responsibility, not on behalf
171 | of any other Contributor, and only if You agree to indemnify,
172 | defend, and hold each Contributor harmless for any liability
173 | incurred by, or claims asserted against, such Contributor by reason
174 | of your accepting any such warranty or additional liability.
175 |
176 | END OF TERMS AND CONDITIONS
177 |
178 | APPENDIX: How to apply the Apache License to your work.
179 |
180 | To apply the Apache License to your work, attach the following
181 | boilerplate notice, with the fields enclosed by brackets "[]"
182 | replaced with your own identifying information. (Don't include
183 | the brackets!) The text should be enclosed in the appropriate
184 | comment syntax for the file format. We also recommend that a
185 | file or class name and description of purpose be included on the
186 | same "printed page" as the copyright notice for easier
187 | identification within third-party archives.
188 |
189 | Copyright [2020] [Yang Chen]
190 |
191 | Licensed under the Apache License, Version 2.0 (the "License");
192 | you may not use this file except in compliance with the License.
193 | You may obtain a copy of the License at
194 |
195 | http://www.apache.org/licenses/LICENSE-2.0
196 |
197 | Unless required by applicable law or agreed to in writing, software
198 | distributed under the License is distributed on an "AS IS" BASIS,
199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
200 | See the License for the specific language governing permissions and
201 | limitations under the License.
202 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # App Versioning
2 |
3 | 
4 | [](https://search.maven.org/search?q=g:io.github.reactivecircus.appversioning)
5 | [](https://opensource.org/licenses/Apache-2.0)
6 |
7 | A Gradle Plugin for lazily generating Android app's `versionCode` & `versionName` from Git tags.
8 |
9 | 
10 |
11 | Android Gradle Plugin 4.0 and 4.1 introduced some [new experimental APIs](https://medium.com/androiddevelopers/new-apis-in-the-android-gradle-plugin-f5325742e614) to support **lazily** computing and setting the `versionCode` and `versionName` for an APK or App Bundle. This plugin builds on top of these APIs to support the common app versioning use cases based on Git.
12 |
13 | This [blogpost](https://dev.to/ychescale9/git-based-android-app-versioning-with-agp-4-0-24ip) should provide more context around Git-based app versioning in general and why this plugin needs to build on top of the new variant APIs introduced in AGP 4.0 / 4.1 which are currently **incubating**.
14 |
15 | ## Android Gradle Plugin version compatibility
16 |
17 | The minimum version of Android Gradle Plugin required is **8.0.0**.
18 |
19 | Version `1.4.0` of the plugin is the final version that's compatible with AGP **7.x** and below.
20 |
21 | Version `0.4.0` of the plugin is the final version that's compatible with AGP **4.0** and **4.1**.
22 |
23 | Version `0.10.0` of the plugin is the final version that's compatible with AGP **4.2**.
24 |
25 | ## Installation
26 |
27 | The **Android App Versioning Gradle Plugin** is available from both [Maven Central](https://search.maven.org/artifact/io.github.reactivecircus.appversioning/app-versioning-gradle-plugin). Make sure you have added `mavenCentral()` to the plugin `repositories`:
28 |
29 | ```kt
30 | // in settings.gradle.kts
31 | pluginManagement {
32 | repositories {
33 | mavenCentral()
34 | }
35 | }
36 | ```
37 |
38 | or
39 |
40 | ```kt
41 | // in root build.gradle.kts
42 | buildscript {
43 | repositories {
44 | mavenCentral()
45 | }
46 | }
47 | ```
48 |
49 | The plugin can now be applied to your **Android Application** module (Gradle subproject).
50 |
51 | Kotlin
52 |
53 | ```kt
54 | plugins {
55 | id("com.android.application")
56 | id("io.github.reactivecircus.app-versioning") version "x.y.z"
57 | }
58 | ```
59 |
60 |
61 |
62 | Groovy
63 |
64 | ```groovy
65 | plugins {
66 | id 'com.android.application'
67 | id 'io.github.reactivecircus.app-versioning' version "x.y.z"
68 | }
69 | ```
70 |
71 |
72 |
73 | ## Usage
74 |
75 | The plugin offers 2 Gradle tasks for each build variant:
76 |
77 | - `generateAppVersionInfoFor` - generates the `versionCode` and `versionName` for the `BuildVariant`. This task is automatically triggered when assembling the APK or AAB e.g. by running `assemble` or `bundle`, and the generated `versionCode` and `versionName` will be injected into the final merged `AndroidManifest`.
78 | - `printAppVersionInfoFor` - prints the latest `versionCode` and `versionName` generated by the plugin to the console if available.
79 |
80 | ### Default behavior
81 |
82 | Without any configurations, by default the plugin will fetch the latest Git tag in the repository, attempt to parse it into a **SemVer** string, and compute the `versionCode` following [positional notation](https://en.wikipedia.org/wiki/Positional_notation):
83 |
84 | ```
85 | versionCode = MAJOR * 10000 + MINOR * 100 + PATCH
86 | ```
87 |
88 | As an example, for a tag `1.3.1` the generated `versionCode` is `1 * 10000 + 3 * 100 + 1 = 10301`.
89 |
90 | The default `versionName` generated will just be the name of the latest Git tag.
91 |
92 | ```
93 | > Task :app:generateAppVersionInfoForRelease
94 | Generated app version code: 10301.
95 | Generated app version name: "1.3.1".
96 | ```
97 |
98 | If the default behavior described above works for you, you are all set to go.
99 |
100 | ### Custom rules
101 |
102 | The plugin lets you define how you want to compute the `versionCode` and `versionName` by implementing lambdas which are evaluated lazily during execution:
103 |
104 | ```kt
105 | appVersioning {
106 | overrideVersionCode { gitTag, providers, variantInfo ->
107 | // TODO generate an Int from the given gitTag, providers, build variant
108 | }
109 |
110 | overrideVersionName { gitTag, providers, variantInfo ->
111 | // TODO generate a String from the given gitTag, providers, build variant
112 | }
113 | }
114 | ```
115 |
116 | `GitTag` is a type-safe representation of a tag encapsulating the `rawTagName`, `commitsSinceLatestTag` and `commitHash`, provided by the plugin.
117 |
118 | `providers` is a `ProviderFactory` instance which is a Gradle API that can be useful for [reading environment variables and system properties lazily](https://docs.gradle.org/current/javadoc/org/gradle/api/provider/ProviderFactory.html).
119 |
120 | `VariantInfo` is an object that encapsulates the build variant information including `buildType`, `flavorName`, and `variantName`.
121 |
122 | #### SemVer-based version code
123 |
124 | The plugin by default reserves 2 digits for each of the **MAJOR**, **MINOR** and **PATCH** components in a SemVer tag.
125 |
126 | To allocate 3 digits per component instead (i.e. each version component can go up to 999):
127 |
128 | Kotlin
129 |
130 | ```kt
131 | import io.github.reactivecircus.appversioning.toSemVer
132 | appVersioning {
133 | overrideVersionCode { gitTag, _, _ ->
134 | val semVer = gitTag.toSemVer()
135 | semVer.major * 1000000 + semVer.minor * 1000 + semVer.patch
136 | }
137 | }
138 | ```
139 |
140 |
141 |
142 | Groovy
143 |
144 | ```groovy
145 | import io.github.reactivecircus.appversioning.SemVer
146 | appVersioning {
147 | overrideVersionCode { gitTag, providers, variantInfo ->
148 | def semVer = SemVer.fromGitTag(gitTag)
149 | semVer.major * 1000000 + semVer.minor * 1000 + semVer.patch
150 | }
151 | }
152 | ```
153 |
154 |
155 |
156 |
157 | `toSemVer()` is an extension function (or `SemVer.fromGitTag(gitTag)` if you use Groovy) provided by the plugin to help create a type-safe `SemVer` object from the `GitTag` by parsing its `rawTagName` field.
158 |
159 | If a Git tag is not fully [SemVer compliant](https://semver.org/#semantic-versioning-specification-semver) (e.g. `1.2`), calling `gitTag.toSemVer()` will throw an exception. In that case we'll need to find another way to compute the `versionCode`.
160 |
161 | #### Using timestamp for version code
162 |
163 | Since the key characteristic for `versionCode` is that it must **monotonically increase** with each app release, a common approach is to use the Epoch / Unix timestamp for `versionCode`:
164 |
165 | Kotlin
166 |
167 | ```kt
168 | import java.time.Instant
169 | appVersioning {
170 | overrideVersionCode { _, _, _ ->
171 | Instant.now().epochSecond.toInt()
172 | }
173 | }
174 | ```
175 |
176 |
177 |
178 | Groovy
179 |
180 | ```groovy
181 | appVersioning {
182 | overrideVersionCode { gitTag, providers, variantInfo ->
183 | Instant.now().epochSecond.intValue()
184 | }
185 | }
186 | ```
187 |
188 |
189 |
190 |
191 | This will generate a monotonically increasing version code every time the `generateAppVersionInfoForRelease` task is run:
192 |
193 | ```
194 | Generated app version code: 1599750437.
195 | ```
196 |
197 | #### Using environment variable
198 |
199 | We can also add a `BUILD_NUMBER` environment variable provided by CI to the `versionCode` or `versionName`. To do this, use the `providers` lambda parameter to create a provider that's only queried during execution:
200 |
201 | Kotlin
202 |
203 | ```kt
204 | import io.github.reactivecircus.appversioning.toSemVer
205 | appVersioning {
206 | overrideVersionCode { gitTag, providers, _ ->
207 | val buildNumber = providers
208 | .environmentVariable("BUILD_NUMBER")
209 | .getOrElse("0").toInt()
210 | val semVer = gitTag.toSemVer()
211 | semVer.major * 10000 + semVer.minor * 100 + semVer.patch + buildNumber
212 | }
213 | }
214 | ```
215 |
216 |
217 |
218 | Groovy
219 |
220 | ```groovy
221 | import io.github.reactivecircus.appversioning.SemVer
222 | appVersioning {
223 | overrideVersionCode { gitTag, providers, variantInfo ->
224 | def buildNumber = providers
225 | .environmentVariable("BUILD_NUMBER")
226 | .getOrElse("0") as Integer
227 | def semVer = SemVer.fromGitTag(gitTag)
228 | semVer.major * 10000 + semVer.minor * 100 + semVer.patch + buildNumber
229 | }
230 | }
231 | ```
232 |
233 |
234 |
235 | `versionName` can be customized with the same approach:
236 |
237 | Kotlin
238 |
239 | ```kt
240 | import io.github.reactivecircus.appversioning.toSemVer
241 | appVersioning {
242 | overrideVersionName { gitTag, providers, _ ->
243 | // a custom versionName combining the tag name, commitHash and an environment variable
244 | val buildNumber = providers
245 | .environmentVariable("BUILD_NUMBER")
246 | .getOrElse("0").toInt()
247 | "${gitTag.rawTagName} - #$buildNumber (${gitTag.commitHash})"
248 | }
249 | }
250 | ```
251 |
252 |
253 |
254 | Groovy
255 |
256 | ```groovy
257 | appVersioning {
258 | overrideVersionName { gitTag, providers, variantInfo ->
259 | // a custom versionName combining the tag name, commitHash and an environment variable
260 | def buildNumber = providers
261 | .environmentVariable("BUILD_NUMBER")
262 | .getOrElse("0") as Integer
263 | "${gitTag.rawTagName} - #$buildNumber (${gitTag.commitHash})".toString()
264 | }
265 | }
266 | ```
267 |
268 |
269 |
270 | #### Custom rules based on build variants
271 |
272 | Sometimes you might want to customize `versionCode` or `versionName` based on the build variants (product flavor, build type). To do this, use the `variantInfo` lambda parameter to query the build variant information when generating custom `versionCode` or `verrsionName`:
273 |
274 | Kotlin
275 |
276 | ```kt
277 | import io.github.reactivecircus.appversioning.toSemVer
278 | appVersioning {
279 | overrideVersionCode { gitTag, _, _ ->
280 | // add 1 to the versionCode for builds with the "paid" product flavor
281 | val offset = if (variantInfo.flavorName == "paid") 1 else 0
282 | val semVer = gitTag.toSemVer()
283 | semVer.major * 10000 + semVer.minor * 100 + semVer.patch + offset
284 | }
285 | overrideVersionName { gitTag, _, variantInfo ->
286 | // append build variant to the versionName for debug builds
287 | val suffix = if (variantInfo.isDebugBuild) " (${variantInfo.variantName})" else ""
288 | gitTag.toString() + suffix
289 | }
290 | }
291 | ```
292 |
293 |
294 |
295 | Groovy
296 |
297 | ```groovy
298 | import io.github.reactivecircus.appversioning.SemVer
299 | appVersioning {
300 | overrideVersionCode { gitTag, providers, variantInfo ->
301 | // add 1 to the versionCode for builds with the "paid" product flavor
302 | def offset
303 | if (variantInfo.flavorName == "paid") {
304 | offset = 1
305 | } else {
306 | offset = 0
307 | }
308 | def semVer = SemVer.fromGitTag(gitTag)
309 | semVer.major * 10000 + semVer.minor * 100 + semVer.patch + offset
310 | }
311 | overrideVersionName { gitTag, providers, variantInfo ->
312 | // append build variant to the versionName for debug builds
313 | def suffix
314 | if (variantInfo.debugBuild == true) {
315 | suffix = " (" + variantInfo.variantName + ")"
316 | } else {
317 | suffix = ""
318 | }
319 | gitTag.toString() + suffix
320 | }
321 | }
322 | ```
323 |
324 |
325 |
326 | ### Tag filtering
327 |
328 | By default the plugin uses the latest tag in the current branch for `versionCode` and `versionName` generation.
329 |
330 | Sometimes it's useful to be able to use the latest tag that follows a specific glob pattern.
331 |
332 | For example a codebase might build and publish 3 different apps separately using the following tag pattern:
333 |
334 | `..[-]+`
335 |
336 | where `app-identifier` is the build metadata component in **SemVer**.
337 |
338 | Some of the possible tags are:
339 |
340 | ```
341 | 1.5.8+app-a
342 | 2.29.0-rc01+app-b
343 | 10.87.9-alpha04+app-c
344 | ```
345 |
346 | To configure the plugin to generate version info specific to `app-b`:
347 |
348 | ```kt
349 | appVersioning {
350 | tagFilter.set("[0-9]*.[0-9]*.[0-9]*+app-b")
351 | }
352 | ```
353 |
354 | ## More configurations
355 |
356 | ### Disabling the plugin
357 |
358 | To disable the plugin such that the `versionCode` and `versionName` defined in the `defaultConfig` block are used instead (if specified):
359 |
360 | ```kt
361 | appVersioning {
362 | /**
363 | * Whether to enable the plugin.
364 | *
365 | * Default is `true`.
366 | */
367 | enabled.set(false)
368 | }
369 | ```
370 |
371 | ### Release build only
372 |
373 | To generate `versionCode` and `versionName` **only** for the `Release` build type:
374 |
375 | ```kt
376 | appVersioning {
377 | /**
378 | * Whether to only generate version name and version code for `release` builds.
379 | *
380 | * Default is `false`.
381 | */
382 | releaseBuildOnly.set(true)
383 | }
384 | ```
385 |
386 | With `releaseBuildOnly` set to `true`, for a project with the default `debug` and `release` build types and no product flavors, the following tasks are available (note the absense of tasks with `Debug` suffix):
387 |
388 | ```
389 | /gradlew tasks --group=versioning
390 |
391 | Versioning tasks
392 | ----------------
393 | generateAppVersionInfoForRelease - Generates app's versionCode and versionName based on git tags for the release variant.
394 | printAppVersionInfoForRelease - Prints the versionCode and versionName generated by Android App Versioning plugin (if available) to the console for the release variant.
395 | ```
396 |
397 | ### Fetching tags if none exists locally
398 |
399 | Sometimes a local checkout may not contain the Git tags (e.g. when cloning was done with `--no-tags`). To fetch git tags from remote when no tags can be found locally:
400 |
401 | ```kt
402 | appVersioning {
403 | /**
404 | * Whether to fetch git tags from remote when no git tag can be found locally.
405 | *
406 | * Default is `false`.
407 | */
408 | fetchTagsWhenNoneExistsLocally.set(true)
409 | }
410 | ```
411 |
412 | ### Custom git root directory
413 |
414 | The plugin assumes the root Gradle project is the git root directory that contains `.git`. If your root Gradle project is not your git root, you can specify it explicitly:
415 |
416 | ```kt
417 | appVersioning {
418 | /**
419 | * Git root directory used for fetching git tags.
420 | * Use this to explicitly set the git root directory when the root Gradle project is not the git root directory.
421 | */
422 | gitRootDirectory.set(rootProject.file("../")) // if the .git directory is in the root Gradle project's parent directory.
423 | }
424 | ```
425 |
426 | ### Bare git repository
427 |
428 | If your `.git` is a symbolic link to a **bare** git repository, you need to explicitly specify the directory of the bare git repository:
429 |
430 | ```kt
431 | appVersioning {
432 | /**
433 | * Bare Git repository directory.
434 | * Use this to explicitly set the directory of a bare git repository (e.g. `app.git`) instead of the standard `.git`.
435 | * Setting this will override the value of [gitRootDirectory] property.
436 | */
437 | bareGitRepoDirectory.set(rootProject.file("../.repo/projects/app.git")) // if the .git directory in the Gradle project root is a symlink to app.git.
438 | }
439 | ```
440 |
441 | ## App versioning on CI
442 |
443 | For performance reason many CI providers only fetch a single commit by default when checking out the repository. For **app-versioning** to work we need to make sure Git tags are also fetched. Here's an example for doing this with [GitHub Actions](https://github.com/actions/checkout):
444 |
445 | ```
446 | - uses: actions/checkout@v4
447 | with:
448 | fetch-depth: 0
449 | ```
450 |
451 | ### Retrieving the generated version code and version name
452 |
453 | Both the `versionCode` and `versionName` generated by **app-versioning** are in the build output directory:
454 |
455 | ```
456 | app/build/outputs/app_versioning//version_code.txt
457 | app/build/outputs/app_versioning//version_name.txt
458 | ```
459 |
460 | We can `cat` the output of these files into variables:
461 |
462 | ```
463 | VERSION_CODE=$(cat app/build/outputs/app_versioning//version_code.txt)
464 | VERSION_NAME=$(cat app/build/outputs/app_versioning//version_name.txt)
465 | ```
466 |
467 | Note that if you need to query these files in a different VM than where the APK (and its version info) was originally generated, you need to make sure these files are "carried over" from the original VM. Otherwise you'll need to run the `generateAppVersionInfoFor` task again to generate these files, but the generated version info might not be the same as what's actually used for the APK (e.g. if you use the Epoch timestamp for `versionCode`).
468 |
469 | Here's an example with GitHub Actions that does the following:
470 |
471 | - in the [Assemble job](https://github.com/ReactiveCircus/streamlined/blob/f0b605627ffaa2a51b37cdec7c2dd846ad3a7dbf/.github/workflows/ci.yml#L33-L72), build the App Bundle and archive / upload the build outputs directory which include the AAB and its R8 mapping file, along with the `version_code.txt` and `version_name.txt` files generated by **app-versioning**.
472 | - later in the [Publish to Play Store job](https://github.com/ReactiveCircus/streamlined/blob/f0b605627ffaa2a51b37cdec7c2dd846ad3a7dbf/.github/workflows/ci.yml#L202-L247), download the previously archived build outputs directory, `cat` the content of `version_code.txt` and `version_name.txt` into variables, upload the R8 mapping file to Bugsnag API with curl and passing the retrieved `$VERSION_CODE` and `$VERSION_NAME` as parameters, and finally upload the AAB to Play Store (without building the AAB or generating the app version info again).
473 |
474 | ## License
475 |
476 | ```
477 | Copyright 2020 Yang Chen
478 |
479 | Licensed under the Apache License, Version 2.0 (the "License");
480 | you may not use this file except in compliance with the License.
481 | You may obtain a copy of the License at
482 |
483 | http://www.apache.org/licenses/LICENSE-2.0
484 |
485 | Unless required by applicable law or agreed to in writing, software
486 | distributed under the License is distributed on an "AS IS" BASIS,
487 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
488 | See the License for the specific language governing permissions and
489 | limitations under the License.
490 | ```
491 |
--------------------------------------------------------------------------------
/RELEASING.md:
--------------------------------------------------------------------------------
1 | # Releasing
2 |
3 | The following steps publish the plugin to both **Maven Central** and **Gradle Plugin Portal**:
4 |
5 | 1. Change the version in top-level `gradle.properties` to a non-SNAPSHOT version.
6 | 2. Update the `CHANGELOG.md` for the impending release.
7 | 3. Update the `README.md` with the new version.
8 | 4. `git commit -am "Prepare for release X.Y.Z."` (where X.Y.Z is the new version).
9 | 5. `./gradlew publishAndReleaseToMavenCentral`
10 | 6. Visit [Sonatype Nexus](https://s01.oss.sonatype.org/) to verify the release has been completed.
11 | 7. `git tag -a X.Y.X -m "X.Y.Z"` (where X.Y.Z is the new version)
12 | 8. Update the top-level `gradle.properties` to the next SNAPSHOT version.
13 | 9. `git commit -am "Prepare next development version."`
14 | 10. `git push && git push --tags`
15 |
16 | If step 5 or 6 fails, drop the Sonatype repo, fix the problem, commit, and start again at step 5.
17 |
--------------------------------------------------------------------------------
/api/app-versioning.api:
--------------------------------------------------------------------------------
1 | public class io/github/reactivecircus/appversioning/AppVersioningExtension {
2 | public static final field Companion Lio/github/reactivecircus/appversioning/AppVersioningExtension$Companion;
3 | public final fun getBareGitRepoDirectory ()Lorg/gradle/api/file/DirectoryProperty;
4 | public final fun getEnabled ()Lorg/gradle/api/provider/Property;
5 | public final fun getFetchTagsWhenNoneExistsLocally ()Lorg/gradle/api/provider/Property;
6 | public final fun getGitRootDirectory ()Lorg/gradle/api/file/DirectoryProperty;
7 | public final fun getReleaseBuildOnly ()Lorg/gradle/api/provider/Property;
8 | public final fun getTagFilter ()Lorg/gradle/api/provider/Property;
9 | public final fun overrideVersionCode (Lgroovy/lang/Closure;)V
10 | public final fun overrideVersionCode (Lkotlin/jvm/functions/Function3;)V
11 | public final fun overrideVersionName (Lgroovy/lang/Closure;)V
12 | public final fun overrideVersionName (Lkotlin/jvm/functions/Function3;)V
13 | }
14 |
15 | public final class io/github/reactivecircus/appversioning/AppVersioningExtension$Companion {
16 | }
17 |
18 | public final class io/github/reactivecircus/appversioning/AppVersioningPlugin : org/gradle/api/Plugin {
19 | public static final field Companion Lio/github/reactivecircus/appversioning/AppVersioningPlugin$Companion;
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 io/github/reactivecircus/appversioning/AppVersioningPlugin$Companion {
26 | }
27 |
28 | public final class io/github/reactivecircus/appversioning/AppVersioningPlugin$inlined$sam$i$org_gradle_api_Action$0 : org/gradle/api/Action {
29 | public fun (Lkotlin/jvm/functions/Function1;)V
30 | public final synthetic fun execute (Ljava/lang/Object;)V
31 | }
32 |
33 | public final class io/github/reactivecircus/appversioning/GitTag {
34 | public fun (Ljava/lang/String;ILjava/lang/String;)V
35 | public final fun component1 ()Ljava/lang/String;
36 | public final fun component2 ()I
37 | public final fun component3 ()Ljava/lang/String;
38 | public final fun copy (Ljava/lang/String;ILjava/lang/String;)Lio/github/reactivecircus/appversioning/GitTag;
39 | public static synthetic fun copy$default (Lio/github/reactivecircus/appversioning/GitTag;Ljava/lang/String;ILjava/lang/String;ILjava/lang/Object;)Lio/github/reactivecircus/appversioning/GitTag;
40 | public fun equals (Ljava/lang/Object;)Z
41 | public final fun getCommitHash ()Ljava/lang/String;
42 | public final fun getCommitsSinceLatestTag ()I
43 | public final fun getRawTagName ()Ljava/lang/String;
44 | public fun hashCode ()I
45 | public fun toString ()Ljava/lang/String;
46 | }
47 |
48 | public final class io/github/reactivecircus/appversioning/SemVer {
49 | public static final field Companion Lio/github/reactivecircus/appversioning/SemVer$Companion;
50 | public fun (IIILjava/lang/String;Ljava/lang/String;)V
51 | public synthetic fun (IIILjava/lang/String;Ljava/lang/String;ILkotlin/jvm/internal/DefaultConstructorMarker;)V
52 | public final fun component1 ()I
53 | public final fun component2 ()I
54 | public final fun component3 ()I
55 | public final fun component4 ()Ljava/lang/String;
56 | public final fun component5 ()Ljava/lang/String;
57 | public final fun copy (IIILjava/lang/String;Ljava/lang/String;)Lio/github/reactivecircus/appversioning/SemVer;
58 | public static synthetic fun copy$default (Lio/github/reactivecircus/appversioning/SemVer;IIILjava/lang/String;Ljava/lang/String;ILjava/lang/Object;)Lio/github/reactivecircus/appversioning/SemVer;
59 | public fun equals (Ljava/lang/Object;)Z
60 | public static final fun fromGitTag (Lio/github/reactivecircus/appversioning/GitTag;)Lio/github/reactivecircus/appversioning/SemVer;
61 | public static final fun fromGitTag (Lio/github/reactivecircus/appversioning/GitTag;Z)Lio/github/reactivecircus/appversioning/SemVer;
62 | public final fun getBuildMetadata ()Ljava/lang/String;
63 | public final fun getMajor ()I
64 | public final fun getMinor ()I
65 | public final fun getPatch ()I
66 | public final fun getPreRelease ()Ljava/lang/String;
67 | public fun hashCode ()I
68 | public fun toString ()Ljava/lang/String;
69 | }
70 |
71 | public final class io/github/reactivecircus/appversioning/SemVer$Companion {
72 | public final fun fromGitTag (Lio/github/reactivecircus/appversioning/GitTag;)Lio/github/reactivecircus/appversioning/SemVer;
73 | public final fun fromGitTag (Lio/github/reactivecircus/appversioning/GitTag;Z)Lio/github/reactivecircus/appversioning/SemVer;
74 | public static synthetic fun fromGitTag$default (Lio/github/reactivecircus/appversioning/SemVer$Companion;Lio/github/reactivecircus/appversioning/GitTag;ZILjava/lang/Object;)Lio/github/reactivecircus/appversioning/SemVer;
75 | }
76 |
77 | public final class io/github/reactivecircus/appversioning/SemVerKt {
78 | public static final fun toSemVer (Lio/github/reactivecircus/appversioning/GitTag;Z)Lio/github/reactivecircus/appversioning/SemVer;
79 | public static synthetic fun toSemVer$default (Lio/github/reactivecircus/appversioning/GitTag;ZILjava/lang/Object;)Lio/github/reactivecircus/appversioning/SemVer;
80 | }
81 |
82 | public final class io/github/reactivecircus/appversioning/VariantInfo : java/io/Serializable {
83 | public static final field Companion Lio/github/reactivecircus/appversioning/VariantInfo$Companion;
84 | public fun (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
85 | public final fun getBuildType ()Ljava/lang/String;
86 | public final fun getFlavorName ()Ljava/lang/String;
87 | public final fun getVariantName ()Ljava/lang/String;
88 | public final fun isDebugBuild ()Z
89 | public final fun isReleaseBuild ()Z
90 | }
91 |
92 | public final class io/github/reactivecircus/appversioning/VariantInfo$Companion {
93 | }
94 |
95 | public final class io/github/reactivecircus/appversioning/internal/CommitId {
96 | public static final synthetic fun box-impl (Ljava/lang/String;)Lio/github/reactivecircus/appversioning/internal/CommitId;
97 | public static fun constructor-impl (Ljava/lang/String;)Ljava/lang/String;
98 | public fun equals (Ljava/lang/Object;)Z
99 | public static fun equals-impl (Ljava/lang/String;Ljava/lang/Object;)Z
100 | public static final fun equals-impl0 (Ljava/lang/String;Ljava/lang/String;)Z
101 | public final fun getValue ()Ljava/lang/String;
102 | public fun hashCode ()I
103 | public static fun hashCode-impl (Ljava/lang/String;)I
104 | public fun toString ()Ljava/lang/String;
105 | public static fun toString-impl (Ljava/lang/String;)Ljava/lang/String;
106 | public final synthetic fun unbox-impl ()Ljava/lang/String;
107 | }
108 |
109 | public final class io/github/reactivecircus/appversioning/internal/GitClient {
110 | public static final field Companion Lio/github/reactivecircus/appversioning/internal/GitClient$Companion;
111 | public synthetic fun (Ljava/io/File;Lkotlin/jvm/internal/DefaultConstructorMarker;)V
112 | public final fun checkoutTag (Ljava/lang/String;)V
113 | public final fun commit-HhBVPuw (Ljava/lang/String;Z)Ljava/lang/String;
114 | public static synthetic fun commit-HhBVPuw$default (Lio/github/reactivecircus/appversioning/internal/GitClient;Ljava/lang/String;ZILjava/lang/Object;)Ljava/lang/String;
115 | public final fun describeLatestTag (Ljava/lang/String;)Ljava/lang/String;
116 | public static synthetic fun describeLatestTag$default (Lio/github/reactivecircus/appversioning/internal/GitClient;Ljava/lang/String;ILjava/lang/Object;)Ljava/lang/String;
117 | public final fun fetchRemoteTags ()V
118 | public final fun listLocalTags ()Ljava/util/List;
119 | public final fun tag-9kPyyPo (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V
120 | public static synthetic fun tag-9kPyyPo$default (Lio/github/reactivecircus/appversioning/internal/GitClient;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;ILjava/lang/Object;)V
121 | }
122 |
123 | public final class io/github/reactivecircus/appversioning/internal/GitClient$Companion {
124 | public final fun initialize (Ljava/io/File;)Lio/github/reactivecircus/appversioning/internal/GitClient;
125 | public final fun open (Ljava/io/File;)Lio/github/reactivecircus/appversioning/internal/GitClient;
126 | }
127 |
128 | public abstract class io/github/reactivecircus/appversioning/tasks/GenerateAppVersionInfo : org/gradle/api/DefaultTask {
129 | public static final field Companion Lio/github/reactivecircus/appversioning/tasks/GenerateAppVersionInfo$Companion;
130 | public static final field TASK_DESCRIPTION_PREFIX Ljava/lang/String;
131 | public static final field TASK_NAME_PREFIX Ljava/lang/String;
132 | public static final field VERSION_CODE_FALLBACK I
133 | public static final field VERSION_NAME_FALLBACK Ljava/lang/String;
134 | public fun (Lorg/gradle/workers/WorkerExecutor;)V
135 | public final fun generate ()V
136 | public abstract fun getFetchTagsWhenNoneExistsLocally ()Lorg/gradle/api/provider/Property;
137 | public abstract fun getGitHead ()Lorg/gradle/api/file/RegularFileProperty;
138 | public abstract fun getGitRefsDirectory ()Lorg/gradle/api/file/DirectoryProperty;
139 | public abstract fun getGroovyVersionCodeCustomizer ()Lorg/gradle/api/provider/Property;
140 | public abstract fun getGroovyVersionNameCustomizer ()Lorg/gradle/api/provider/Property;
141 | public abstract fun getKotlinVersionCodeCustomizer ()Lorg/gradle/api/provider/Property;
142 | public abstract fun getKotlinVersionNameCustomizer ()Lorg/gradle/api/provider/Property;
143 | public abstract fun getRootProjectDirectory ()Lorg/gradle/api/file/DirectoryProperty;
144 | public abstract fun getRootProjectDisplayName ()Lorg/gradle/api/provider/Property;
145 | public abstract fun getTagFilter ()Lorg/gradle/api/provider/Property;
146 | public abstract fun getVariantInfo ()Lorg/gradle/api/provider/Property;
147 | public abstract fun getVersionCodeFile ()Lorg/gradle/api/file/RegularFileProperty;
148 | public abstract fun getVersionNameFile ()Lorg/gradle/api/file/RegularFileProperty;
149 | }
150 |
151 | public final class io/github/reactivecircus/appversioning/tasks/GenerateAppVersionInfo$Companion {
152 | }
153 |
154 | public abstract class io/github/reactivecircus/appversioning/tasks/PrintAppVersionInfo : org/gradle/api/DefaultTask {
155 | public static final field Companion Lio/github/reactivecircus/appversioning/tasks/PrintAppVersionInfo$Companion;
156 | public static final field TASK_DESCRIPTION_PREFIX Ljava/lang/String;
157 | public static final field TASK_NAME_PREFIX Ljava/lang/String;
158 | public fun ()V
159 | public abstract fun getBuildVariantName ()Lorg/gradle/api/provider/Property;
160 | public abstract fun getProjectName ()Lorg/gradle/api/provider/Property;
161 | public abstract fun getVersionCodeFile ()Lorg/gradle/api/file/RegularFileProperty;
162 | public abstract fun getVersionNameFile ()Lorg/gradle/api/file/RegularFileProperty;
163 | public final fun print ()V
164 | }
165 |
166 | public final class io/github/reactivecircus/appversioning/tasks/PrintAppVersionInfo$Companion {
167 | }
168 |
169 |
--------------------------------------------------------------------------------
/build.gradle.kts:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage")
2 |
3 | import com.vanniktech.maven.publish.SonatypeHost
4 | import io.gitlab.arturbosch.detekt.Detekt
5 | import org.gradle.api.tasks.testing.logging.TestLogEvent
6 | import org.jetbrains.kotlin.gradle.dsl.JvmTarget
7 | import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
8 |
9 | plugins {
10 | `kotlin-dsl`
11 | alias(libs.plugins.kotlin.jvm)
12 | alias(libs.plugins.binaryCompatibilityValidator)
13 | alias(libs.plugins.detekt)
14 | alias(libs.plugins.mavenPublish)
15 | }
16 |
17 | group = property("GROUP") as String
18 | version = property("VERSION_NAME") as String
19 |
20 | mavenPublishing {
21 | publishToMavenCentral(SonatypeHost.S01, automaticRelease = true)
22 | signAllPublications()
23 | }
24 |
25 | gradlePlugin {
26 | website.set(property("POM_URL") as String)
27 | vcsUrl.set(property("POM_SCM_URL") as String)
28 | plugins.create("appVersioning") {
29 | id = "io.github.reactivecircus.app-versioning"
30 | displayName = "Android App Versioning Gradle Plugin."
31 | description = "Gradle plugin for lazily generating Android app's versionCode & versionName from Git tags."
32 | tags.set(listOf("android", "versioning"))
33 | implementationClass = "io.github.reactivecircus.appversioning.AppVersioningPlugin"
34 | }
35 | }
36 |
37 | tasks.withType().configureEach {
38 | compilerOptions {
39 | jvmTarget.set(JvmTarget.JVM_17)
40 | freeCompilerArgs.addAll(
41 | "-Xjvm-default=all",
42 | )
43 | }
44 | }
45 |
46 | tasks.withType().configureEach {
47 | sourceCompatibility = JavaVersion.VERSION_17.toString()
48 | targetCompatibility = JavaVersion.VERSION_17.toString()
49 | }
50 |
51 | val fixtureClasspath: Configuration by configurations.creating
52 | tasks.pluginUnderTestMetadata {
53 | pluginClasspath.from(fixtureClasspath)
54 | }
55 |
56 | val functionalTestSourceSet: SourceSet = sourceSets.create("functionalTest") {
57 | compileClasspath += sourceSets["main"].output + configurations["testRuntimeClasspath"]
58 | runtimeClasspath += output + compileClasspath
59 | }
60 |
61 | val functionalTestImplementation: Configuration = configurations.getByName("functionalTestImplementation")
62 | .extendsFrom(configurations.getByName("testImplementation"))
63 |
64 | gradlePlugin.testSourceSets(functionalTestSourceSet)
65 |
66 | val functionalTest by tasks.registering(Test::class) {
67 | failFast = true
68 | testLogging {
69 | events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED)
70 | }
71 | testClassesDirs = functionalTestSourceSet.output.classesDirs
72 | classpath = functionalTestSourceSet.runtimeClasspath
73 | }
74 |
75 | val check by tasks.getting(Task::class) {
76 | dependsOn(functionalTest)
77 | }
78 |
79 | val test by tasks.getting(Test::class) {
80 | maxParallelForks = (Runtime.getRuntime().availableProcessors() / 2).coerceAtLeast(1)
81 | testLogging {
82 | events(TestLogEvent.PASSED, TestLogEvent.SKIPPED, TestLogEvent.FAILED)
83 | }
84 | }
85 |
86 | val fixtureAgpVersion: Provider = providers
87 | .environmentVariable("AGP_VERSION")
88 | .orElse(providers.gradleProperty("AGP_VERSION"))
89 | .orElse(libs.versions.agp)
90 |
91 | dependencies {
92 | compileOnly(libs.agp.build)
93 | testImplementation(libs.junit)
94 | testImplementation(libs.truth)
95 | testImplementation(libs.testParameterInjector)
96 | fixtureClasspath(libs.agp.build.flatMap { dependency ->
97 | fixtureAgpVersion.map { version ->
98 | "${dependency.group}:${dependency.name}:$version"
99 | }
100 | })
101 | }
102 |
103 | detekt {
104 | source.from(files("src/"))
105 | config.from(files("${project.rootDir}/detekt.yml"))
106 | buildUponDefaultConfig = true
107 | allRules = true
108 | }
109 |
110 | tasks.withType().configureEach {
111 | jvmTarget = JvmTarget.JVM_22.target
112 | reports {
113 | html.outputLocation.set(file("build/reports/detekt/${project.name}.html"))
114 | }
115 | }
116 |
117 | val detektFormatting = libs.plugin.detektFormatting.get()
118 |
119 | dependencies.add("detektPlugins", detektFormatting)
120 |
--------------------------------------------------------------------------------
/detekt.yml:
--------------------------------------------------------------------------------
1 | complexity:
2 | LongMethod:
3 | excludes: ["**/test/**","**/functionalTest/**"]
4 | TooManyFunctions:
5 | excludes: ["**/test/**","**/functionalTest/**"]
6 | LargeClass:
7 | excludes: ["**/test/**","**/functionalTest/**"]
8 |
9 | formatting:
10 | MaximumLineLength:
11 | active: false
12 | TrailingCommaOnCallSite:
13 | active: false
14 | TrailingCommaOnDeclarationSite:
15 | active: false
16 | FunctionSignature:
17 | active: false
18 |
19 | naming:
20 | FunctionMaxLength:
21 | active: false
22 |
23 | style:
24 | MaxLineLength:
25 | active: false
26 |
--------------------------------------------------------------------------------
/docs/images/sample.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ReactiveCircus/app-versioning/d313a4b57b67857463681367e27c83581a398a04/docs/images/sample.png
--------------------------------------------------------------------------------
/gradle.properties:
--------------------------------------------------------------------------------
1 | GROUP=io.github.reactivecircus.appversioning
2 | VERSION_NAME=1.5.0-SNAPSHOT
3 |
4 | POM_ARTIFACT_ID=app-versioning-gradle-plugin
5 | POM_NAME=Android App Versioning Gradle Plugin
6 | POM_DESCRIPTION=Gradle plugin for lazily generating Android app's versionCode & versionName from Git tags
7 | POM_PACKAGING=jar
8 |
9 | POM_URL=https://github.com/ReactiveCircus/app-versioning
10 | POM_SCM_URL=https://github.com/ReactiveCircus/app-versioning
11 | POM_SCM_CONNECTION=scm:git:https://github.com/ReactiveCircus/app-versioning.git
12 | POM_SCM_DEV_CONNECTION=scm:git:ssh://git@github.com/ReactiveCircus/app-versioning.git
13 |
14 | POM_LICENCE_NAME=The Apache Software License, Version 2.0
15 | POM_LICENCE_URL=http://www.apache.org/licenses/LICENSE-2.0.txt
16 | POM_LICENCE_DIST=repo
17 |
18 | POM_DEVELOPER_ID=reactivecircus
19 | POM_DEVELOPER_NAME=Reactive Circus
20 |
21 | org.gradle.parallel=true
22 | org.gradle.configureondemand=true
23 | org.gradle.caching=true
24 | org.gradle.configuration-cache=true
25 | org.gradle.configuration-cache.problems=warn
26 | org.gradle.unsafe.isolated-projects=true
27 |
28 | # Kotlin code style
29 | kotlin.code.style=official
30 |
--------------------------------------------------------------------------------
/gradle/gradle-daemon-jvm.properties:
--------------------------------------------------------------------------------
1 | #This file is generated by updateDaemonJvm
2 | toolchainUrl.FREE_BSD.AARCH64=https\://api.foojay.io/disco/v3.0/ids/796901cbf89fc850a3f3bb8ddfe4506c/redirect
3 | toolchainUrl.FREE_BSD.X86_64=https\://api.foojay.io/disco/v3.0/ids/26af28a3208b58b524a51fc519bca85a/redirect
4 | toolchainUrl.LINUX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/796901cbf89fc850a3f3bb8ddfe4506c/redirect
5 | toolchainUrl.LINUX.X86_64=https\://api.foojay.io/disco/v3.0/ids/26af28a3208b58b524a51fc519bca85a/redirect
6 | toolchainUrl.MAC_OS.AARCH64=https\://api.foojay.io/disco/v3.0/ids/761743ab934ff2cc7d8e409918f67acf/redirect
7 | toolchainUrl.MAC_OS.X86_64=https\://api.foojay.io/disco/v3.0/ids/d5f8fa0927b058deea7bc5543fc3e371/redirect
8 | toolchainUrl.UNIX.AARCH64=https\://api.foojay.io/disco/v3.0/ids/796901cbf89fc850a3f3bb8ddfe4506c/redirect
9 | toolchainUrl.UNIX.X86_64=https\://api.foojay.io/disco/v3.0/ids/26af28a3208b58b524a51fc519bca85a/redirect
10 | toolchainUrl.WINDOWS.X86_64=https\://api.foojay.io/disco/v3.0/ids/f765ffc0b109346d16cfa1fed6eb856a/redirect
11 | toolchainVendor=AZUL
12 | toolchainVersion=24
13 |
--------------------------------------------------------------------------------
/gradle/libs.versions.toml:
--------------------------------------------------------------------------------
1 | [versions]
2 | kotlin = "2.1.20"
3 | binaryCompabilityValidator = "0.15.1"
4 | agp = "8.10.1"
5 | detekt = "1.23.7"
6 | mavenPublish = "0.31.0"
7 | junit = "4.13.2"
8 | truth = "1.1.3"
9 | testParameterInjector = "1.18"
10 | toolchainsResolver = "0.10.0"
11 |
12 | [libraries]
13 | plugin-detektFormatting = { module = "io.gitlab.arturbosch.detekt:detekt-formatting", version.ref = "detekt" }
14 |
15 | agp-build = { module = "com.android.tools.build:gradle", version.ref = "agp" }
16 | junit = { module = "junit:junit", version.ref = "junit" }
17 | truth = { module = "com.google.truth:truth", version.ref = "truth" }
18 | testParameterInjector = { group = "com.google.testparameterinjector", name = "test-parameter-injector", version.ref = "testParameterInjector" }
19 |
20 | [plugins]
21 | kotlin-jvm = { id = "org.jetbrains.kotlin.jvm", version.ref = "kotlin" }
22 | binaryCompatibilityValidator = { id = "org.jetbrains.kotlinx.binary-compatibility-validator", version.ref = "binaryCompabilityValidator" }
23 | detekt = { id = "io.gitlab.arturbosch.detekt", version.ref = "detekt" }
24 | mavenPublish = { id = "com.vanniktech.maven.publish", version.ref = "mavenPublish" }
25 |
--------------------------------------------------------------------------------
/gradle/wrapper/gradle-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ReactiveCircus/app-versioning/d313a4b57b67857463681367e27c83581a398a04/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-bin.zip
4 | networkTimeout=10000
5 | validateDistributionUrl=true
6 | zipStoreBase=GRADLE_USER_HOME
7 | zipStorePath=wrapper/dists
8 |
--------------------------------------------------------------------------------
/gradlew:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 |
3 | #
4 | # Copyright © 2015-2021 the original authors.
5 | #
6 | # Licensed under the Apache License, Version 2.0 (the "License");
7 | # you may not use this file except in compliance with the License.
8 | # You may obtain a copy of the License at
9 | #
10 | # https://www.apache.org/licenses/LICENSE-2.0
11 | #
12 | # Unless required by applicable law or agreed to in writing, software
13 | # distributed under the License is distributed on an "AS IS" BASIS,
14 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
15 | # See the License for the specific language governing permissions and
16 | # limitations under the License.
17 | #
18 | # SPDX-License-Identifier: Apache-2.0
19 | #
20 |
21 | ##############################################################################
22 | #
23 | # Gradle start up script for POSIX generated by Gradle.
24 | #
25 | # Important for running:
26 | #
27 | # (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
28 | # noncompliant, but you have some other compliant shell such as ksh or
29 | # bash, then to run this script, type that shell name before the whole
30 | # command line, like:
31 | #
32 | # ksh Gradle
33 | #
34 | # Busybox and similar reduced shells will NOT work, because this script
35 | # requires all of these POSIX shell features:
36 | # * functions;
37 | # * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
38 | # «${var#prefix}», «${var%suffix}», and «$( cmd )»;
39 | # * compound commands having a testable exit status, especially «case»;
40 | # * various built-in commands including «command», «set», and «ulimit».
41 | #
42 | # Important for patching:
43 | #
44 | # (2) This script targets any POSIX shell, so it avoids extensions provided
45 | # by Bash, Ksh, etc; in particular arrays are avoided.
46 | #
47 | # The "traditional" practice of packing multiple parameters into a
48 | # space-separated string is a well documented source of bugs and security
49 | # problems, so this is (mostly) avoided, by progressively accumulating
50 | # options in "$@", and eventually passing that to Java.
51 | #
52 | # Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
53 | # and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
54 | # see the in-line comments for details.
55 | #
56 | # There are tweaks for specific operating systems such as AIX, CygWin,
57 | # Darwin, MinGW, and NonStop.
58 | #
59 | # (3) This script is generated from the Groovy template
60 | # https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
61 | # within the Gradle project.
62 | #
63 | # You can find Gradle at https://github.com/gradle/gradle/.
64 | #
65 | ##############################################################################
66 |
67 | # Attempt to set APP_HOME
68 |
69 | # Resolve links: $0 may be a link
70 | app_path=$0
71 |
72 | # Need this for daisy-chained symlinks.
73 | while
74 | APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
75 | [ -h "$app_path" ]
76 | do
77 | ls=$( ls -ld "$app_path" )
78 | link=${ls#*' -> '}
79 | case $link in #(
80 | /*) app_path=$link ;; #(
81 | *) app_path=$APP_HOME$link ;;
82 | esac
83 | done
84 |
85 | # This is normally unused
86 | # shellcheck disable=SC2034
87 | APP_BASE_NAME=${0##*/}
88 | # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
89 | APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
90 |
91 | # Use the maximum available, or set MAX_FD != -1 to use that value.
92 | MAX_FD=maximum
93 |
94 | warn () {
95 | echo "$*"
96 | } >&2
97 |
98 | die () {
99 | echo
100 | echo "$*"
101 | echo
102 | exit 1
103 | } >&2
104 |
105 | # OS specific support (must be 'true' or 'false').
106 | cygwin=false
107 | msys=false
108 | darwin=false
109 | nonstop=false
110 | case "$( uname )" in #(
111 | CYGWIN* ) cygwin=true ;; #(
112 | Darwin* ) darwin=true ;; #(
113 | MSYS* | MINGW* ) msys=true ;; #(
114 | NONSTOP* ) nonstop=true ;;
115 | esac
116 |
117 | CLASSPATH="\\\"\\\""
118 |
119 |
120 | # Determine the Java command to use to start the JVM.
121 | if [ -n "$JAVA_HOME" ] ; then
122 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
123 | # IBM's JDK on AIX uses strange locations for the executables
124 | JAVACMD=$JAVA_HOME/jre/sh/java
125 | else
126 | JAVACMD=$JAVA_HOME/bin/java
127 | fi
128 | if [ ! -x "$JAVACMD" ] ; then
129 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
130 |
131 | Please set the JAVA_HOME variable in your environment to match the
132 | location of your Java installation."
133 | fi
134 | else
135 | JAVACMD=java
136 | if ! command -v java >/dev/null 2>&1
137 | then
138 | die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
139 |
140 | Please set the JAVA_HOME variable in your environment to match the
141 | location of your Java installation."
142 | fi
143 | fi
144 |
145 | # Increase the maximum file descriptors if we can.
146 | if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
147 | case $MAX_FD in #(
148 | max*)
149 | # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
150 | # shellcheck disable=SC2039,SC3045
151 | MAX_FD=$( ulimit -H -n ) ||
152 | warn "Could not query maximum file descriptor limit"
153 | esac
154 | case $MAX_FD in #(
155 | '' | soft) :;; #(
156 | *)
157 | # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
158 | # shellcheck disable=SC2039,SC3045
159 | ulimit -n "$MAX_FD" ||
160 | warn "Could not set maximum file descriptor limit to $MAX_FD"
161 | esac
162 | fi
163 |
164 | # Collect all arguments for the java command, stacking in reverse order:
165 | # * args from the command line
166 | # * the main class name
167 | # * -classpath
168 | # * -D...appname settings
169 | # * --module-path (only if needed)
170 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
171 |
172 | # For Cygwin or MSYS, switch paths to Windows format before running java
173 | if "$cygwin" || "$msys" ; then
174 | APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
175 | CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
176 |
177 | JAVACMD=$( cygpath --unix "$JAVACMD" )
178 |
179 | # Now convert the arguments - kludge to limit ourselves to /bin/sh
180 | for arg do
181 | if
182 | case $arg in #(
183 | -*) false ;; # don't mess with options #(
184 | /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
185 | [ -e "$t" ] ;; #(
186 | *) false ;;
187 | esac
188 | then
189 | arg=$( cygpath --path --ignore --mixed "$arg" )
190 | fi
191 | # Roll the args list around exactly as many times as the number of
192 | # args, so each arg winds up back in the position where it started, but
193 | # possibly modified.
194 | #
195 | # NB: a `for` loop captures its iteration list before it begins, so
196 | # changing the positional parameters here affects neither the number of
197 | # iterations, nor the values presented in `arg`.
198 | shift # remove old arg
199 | set -- "$@" "$arg" # push replacement arg
200 | done
201 | fi
202 |
203 |
204 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
205 | DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
206 |
207 | # Collect all arguments for the java command:
208 | # * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
209 | # and any embedded shellness will be escaped.
210 | # * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
211 | # treated as '${Hostname}' itself on the command line.
212 |
213 | set -- \
214 | "-Dorg.gradle.appname=$APP_BASE_NAME" \
215 | -classpath "$CLASSPATH" \
216 | -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \
217 | "$@"
218 |
219 | # Stop when "xargs" is not available.
220 | if ! command -v xargs >/dev/null 2>&1
221 | then
222 | die "xargs is not available"
223 | fi
224 |
225 | # Use "xargs" to parse quoted args.
226 | #
227 | # With -n1 it outputs one arg per line, with the quotes and backslashes removed.
228 | #
229 | # In Bash we could simply go:
230 | #
231 | # readarray ARGS < <( xargs -n1 <<<"$var" ) &&
232 | # set -- "${ARGS[@]}" "$@"
233 | #
234 | # but POSIX shell has neither arrays nor command substitution, so instead we
235 | # post-process each arg (as a line of input to sed) to backslash-escape any
236 | # character that might be a shell metacharacter, then use eval to reverse
237 | # that process (while maintaining the separation between arguments), and wrap
238 | # the whole thing up as a single "set" statement.
239 | #
240 | # This will of course break if any of these variables contains a newline or
241 | # an unmatched quote.
242 | #
243 |
244 | eval "set -- $(
245 | printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
246 | xargs -n1 |
247 | sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
248 | tr '\n' ' '
249 | )" '"$@"'
250 |
251 | exec "$JAVACMD" "$@"
252 |
--------------------------------------------------------------------------------
/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=
74 |
75 |
76 | @rem Execute Gradle
77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
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 |
--------------------------------------------------------------------------------
/settings.gradle.kts:
--------------------------------------------------------------------------------
1 | rootProject.name = "app-versioning"
2 |
3 | pluginManagement {
4 | repositories {
5 | gradlePluginPortal {
6 | content {
7 | includeGroupByRegex("org.gradle.*")
8 | }
9 | }
10 | mavenCentral()
11 | }
12 |
13 | val toolchainsResolverVersion = file("$rootDir/gradle/libs.versions.toml")
14 | .readLines()
15 | .first { it.contains("toolchainsResolver") }
16 | .substringAfter("=")
17 | .trim()
18 | .removeSurrounding("\"")
19 |
20 | plugins {
21 | id("org.gradle.toolchains.foojay-resolver-convention") version toolchainsResolverVersion
22 | }
23 | }
24 |
25 | @Suppress("UnstableApiUsage")
26 | dependencyResolutionManagement {
27 | repositories {
28 | mavenCentral()
29 | google()
30 | }
31 | }
32 |
33 | plugins {
34 | id("org.gradle.toolchains.foojay-resolver-convention")
35 | }
36 |
--------------------------------------------------------------------------------
/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/AppVersioningPluginIntegrationTest.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("FunctionName")
2 |
3 | package io.github.reactivecircus.appversioning
4 |
5 | import com.google.common.truth.Truth.assertThat
6 | import com.google.testing.junit.testparameterinjector.TestParameter
7 | import com.google.testing.junit.testparameterinjector.TestParameterInjector
8 | import io.github.reactivecircus.appversioning.fixtures.AppProjectTemplate
9 | import io.github.reactivecircus.appversioning.fixtures.LibraryProjectTemplate
10 | import io.github.reactivecircus.appversioning.fixtures.withFixtureRunner
11 | import io.github.reactivecircus.appversioning.internal.GitClient
12 | import io.github.reactivecircus.appversioning.tasks.GenerateAppVersionInfo
13 | import io.github.reactivecircus.appversioning.tasks.PrintAppVersionInfo
14 | import org.gradle.testkit.runner.TaskOutcome
15 | import org.junit.Rule
16 | import org.junit.Test
17 | import org.junit.rules.TemporaryFolder
18 | import org.junit.runner.RunWith
19 | import java.io.File
20 |
21 | @RunWith(TestParameterInjector::class)
22 | class AppVersioningPluginIntegrationTest {
23 |
24 | @get:Rule
25 | val fixtureDir = TemporaryFolder()
26 |
27 | @Test
28 | fun `plugin cannot be applied to project without Android App plugin`() {
29 | GitClient.initialize(fixtureDir.root)
30 |
31 | withFixtureRunner(
32 | fixtureDir = fixtureDir,
33 | subprojects = listOf(LibraryProjectTemplate())
34 | ).runAndExpectFailure(
35 | "help"
36 | ) {
37 | assertThat(task("help")?.outcome).isNull()
38 | assertThat(output).contains(
39 | "Android App Versioning plugin should only be applied to an Android Application project but project ':library' doesn't have the 'com.android.application' plugin applied."
40 | )
41 | }
42 | }
43 |
44 | @Test
45 | fun `plugin tasks are registered for Android App project without product flavors`() {
46 | GitClient.initialize(fixtureDir.root)
47 |
48 | withFixtureRunner(
49 | fixtureDir = fixtureDir,
50 | subprojects = listOf(AppProjectTemplate())
51 | ).runAndCheckResult(
52 | "tasks",
53 | "--group=versioning"
54 | ) {
55 | assertThat(output).contains("Versioning tasks")
56 | assertThat(output).contains(
57 | "generateAppVersionInfoForDebug - ${GenerateAppVersionInfo.TASK_DESCRIPTION_PREFIX} for the debug variant."
58 | )
59 | assertThat(output).contains(
60 | "generateAppVersionInfoForRelease - ${GenerateAppVersionInfo.TASK_DESCRIPTION_PREFIX} for the release variant."
61 | )
62 | assertThat(output).contains(
63 | "printAppVersionInfoForDebug - ${PrintAppVersionInfo.TASK_DESCRIPTION_PREFIX} for the debug variant."
64 | )
65 | assertThat(output).contains(
66 | "printAppVersionInfoForRelease - ${PrintAppVersionInfo.TASK_DESCRIPTION_PREFIX} for the release variant."
67 | )
68 | }
69 | }
70 |
71 | @Test
72 | fun `plugin tasks are registered for Android App project with product flavors`(
73 | @TestParameter useKts: Boolean
74 | ) {
75 | GitClient.initialize(fixtureDir.root)
76 |
77 | val flavors = listOf("mock", "prod")
78 | withFixtureRunner(
79 | fixtureDir = fixtureDir,
80 | subprojects = listOf(AppProjectTemplate(useKts = useKts, flavors = flavors))
81 | ).runAndCheckResult(
82 | "tasks",
83 | "--group=versioning"
84 | ) {
85 | assertThat(output).contains("Versioning tasks")
86 | assertThat(output).contains(
87 | "generateAppVersionInfoForMockDebug - ${GenerateAppVersionInfo.TASK_DESCRIPTION_PREFIX} for the mockDebug variant."
88 | )
89 | assertThat(output).contains(
90 | "generateAppVersionInfoForProdDebug - ${GenerateAppVersionInfo.TASK_DESCRIPTION_PREFIX} for the prodDebug variant."
91 | )
92 | assertThat(output).contains(
93 | "generateAppVersionInfoForMockRelease - ${GenerateAppVersionInfo.TASK_DESCRIPTION_PREFIX} for the mockRelease variant."
94 | )
95 | assertThat(output).contains(
96 | "generateAppVersionInfoForProdRelease - ${GenerateAppVersionInfo.TASK_DESCRIPTION_PREFIX} for the prodRelease variant."
97 | )
98 | assertThat(output).contains(
99 | "printAppVersionInfoForMockDebug - ${PrintAppVersionInfo.TASK_DESCRIPTION_PREFIX} for the mockDebug variant."
100 | )
101 | assertThat(output).contains(
102 | "printAppVersionInfoForProdDebug - ${PrintAppVersionInfo.TASK_DESCRIPTION_PREFIX} for the prodDebug variant."
103 | )
104 | assertThat(output).contains(
105 | "printAppVersionInfoForMockRelease - ${PrintAppVersionInfo.TASK_DESCRIPTION_PREFIX} for the mockRelease variant."
106 | )
107 | assertThat(output).contains(
108 | "printAppVersionInfoForProdRelease - ${PrintAppVersionInfo.TASK_DESCRIPTION_PREFIX} for the prodRelease variant."
109 | )
110 | }
111 | }
112 |
113 | @Test
114 | fun `plugin tasks are not registered for debug builds when releaseBuildOnly is enabled`() {
115 | GitClient.initialize(fixtureDir.root)
116 |
117 | val extension = """
118 | appVersioning {
119 | releaseBuildOnly.set(true)
120 | }
121 | """.trimIndent()
122 | withFixtureRunner(
123 | fixtureDir = fixtureDir,
124 | subprojects = listOf(AppProjectTemplate(pluginExtension = extension))
125 | ).runAndCheckResult(
126 | "tasks",
127 | "--group=versioning"
128 | ) {
129 | assertThat(output).doesNotContain("generateAppVersionInfoForDebug")
130 | assertThat(output).doesNotContain("printAppVersionInfoForDebug")
131 | }
132 | }
133 |
134 | @Test
135 | fun `plugin tasks are registered for debug builds when releaseBuildOnly is disabled`() {
136 | GitClient.initialize(fixtureDir.root)
137 |
138 | val extension = """
139 | appVersioning {
140 | releaseBuildOnly.set(false)
141 | }
142 | """.trimIndent()
143 | withFixtureRunner(
144 | fixtureDir = fixtureDir,
145 | subprojects = listOf(AppProjectTemplate(pluginExtension = extension))
146 | ).runAndCheckResult(
147 | "tasks",
148 | "--group=versioning"
149 | ) {
150 | assertThat(output).contains(
151 | "generateAppVersionInfoForDebug - ${GenerateAppVersionInfo.TASK_DESCRIPTION_PREFIX} for the debug variant."
152 | )
153 | assertThat(output).contains(
154 | "printAppVersionInfoForDebug - ${PrintAppVersionInfo.TASK_DESCRIPTION_PREFIX} for the debug variant."
155 | )
156 | }
157 | }
158 |
159 | @Test
160 | fun `plugin tasks are registered when plugin is enabled`() {
161 | GitClient.initialize(fixtureDir.root)
162 |
163 | val extension = """
164 | appVersioning {
165 | enabled.set(true)
166 | }
167 | """.trimIndent()
168 | withFixtureRunner(
169 | fixtureDir = fixtureDir,
170 | subprojects = listOf(AppProjectTemplate(pluginExtension = extension))
171 | ).runAndCheckResult(
172 | "tasks",
173 | "--group=versioning"
174 | ) {
175 | assertThat(output).contains("Versioning tasks")
176 | assertThat(output).contains(
177 | "generateAppVersionInfoForRelease - ${GenerateAppVersionInfo.TASK_DESCRIPTION_PREFIX} for the release variant."
178 | )
179 | assertThat(output).contains(
180 | "printAppVersionInfoForRelease - ${PrintAppVersionInfo.TASK_DESCRIPTION_PREFIX} for the release variant."
181 | )
182 | }
183 | }
184 |
185 | @Test
186 | fun `plugin tasks are not registered when plugin is disabled`() {
187 | GitClient.initialize(fixtureDir.root)
188 |
189 | val extension = """
190 | appVersioning {
191 | enabled.set(false)
192 | }
193 | """.trimIndent()
194 | withFixtureRunner(
195 | fixtureDir = fixtureDir,
196 | subprojects = listOf(AppProjectTemplate(pluginExtension = extension))
197 | ).runAndCheckResult(
198 | "tasks",
199 | "--group=versioning"
200 | ) {
201 | assertThat(output).doesNotContain("Versioning tasks")
202 | assertThat(output).contains("No tasks")
203 | assertThat(output).contains("Android App Versioning plugin is disabled.")
204 | }
205 | }
206 |
207 | @Test
208 | fun `plugin generates versionCode and versionName for the assembled APK when assemble task is run`(
209 | @TestParameter useKts: Boolean
210 | ) {
211 | GitClient.initialize(fixtureDir.root).apply {
212 | val commitId = commit(message = "1st commit.")
213 | tag(name = "1.2.3", message = "1st tag", commitId = commitId)
214 | }
215 |
216 | withFixtureRunner(
217 | fixtureDir = fixtureDir,
218 | subprojects = listOf(AppProjectTemplate(useKts = useKts))
219 | ).runAndCheckResult(
220 | "assembleRelease"
221 | ) {
222 | val versionCodeFileContent = File(
223 | fixtureDir.root,
224 | "app/build/outputs/app_versioning/release/version_code.txt"
225 | ).readText()
226 | val versionNameFileContent = File(
227 | fixtureDir.root,
228 | "app/build/outputs/app_versioning/release/version_name.txt"
229 | ).readText()
230 |
231 | assertThat(task(":app:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
232 | assertThat(task(":app:assembleRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
233 |
234 | assertThat(output).contains("Generated app version code: 10203.")
235 | assertThat(output).contains("Generated app version name: \"1.2.3\".")
236 |
237 | assertThat(versionCodeFileContent).isEqualTo("10203")
238 | assertThat(versionNameFileContent).isEqualTo("1.2.3")
239 | }
240 | }
241 |
242 | @Test
243 | fun `plugin generates versionCode and versionName for the assembled APKs when splits-APKs is enabled`(
244 | @TestParameter useKts: Boolean
245 | ) {
246 | GitClient.initialize(fixtureDir.root).apply {
247 | val commitId = commit(message = "1st commit.")
248 | tag(name = "1.2.3", message = "1st tag", commitId = commitId)
249 | }
250 |
251 | withFixtureRunner(
252 | fixtureDir = fixtureDir,
253 | subprojects = listOf(AppProjectTemplate(useKts = useKts, splitsApks = true))
254 | ).runAndCheckResult(
255 | "assembleRelease"
256 | ) {
257 | val versionCodeFileContent = File(
258 | fixtureDir.root,
259 | "app/build/outputs/app_versioning/release/version_code.txt"
260 | ).readText()
261 | val versionNameFileContent = File(
262 | fixtureDir.root,
263 | "app/build/outputs/app_versioning/release/version_name.txt"
264 | ).readText()
265 |
266 | assertThat(task(":app:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
267 | assertThat(task(":app:assembleRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
268 |
269 | assertThat(output).contains("Generated app version code: 10203.")
270 | assertThat(output).contains("Generated app version name: \"1.2.3\".")
271 |
272 | assertThat(versionCodeFileContent).isEqualTo("10203")
273 | assertThat(versionNameFileContent).isEqualTo("1.2.3")
274 | }
275 | }
276 |
277 | @Test
278 | fun `plugin generates versionCode and versionName for the assembled APKs when splits-APKs is enabled and universal mode is on`(
279 | @TestParameter useKts: Boolean
280 | ) {
281 | GitClient.initialize(fixtureDir.root).apply {
282 | val commitId = commit(message = "1st commit.")
283 | tag(name = "1.2.3", message = "1st tag", commitId = commitId)
284 | }
285 |
286 | withFixtureRunner(
287 | fixtureDir = fixtureDir,
288 | subprojects = listOf(AppProjectTemplate(useKts = useKts, splitsApks = true, universalApk = true))
289 | ).runAndCheckResult(
290 | "assembleRelease"
291 | ) {
292 | val versionCodeFileContent = File(
293 | fixtureDir.root,
294 | "app/build/outputs/app_versioning/release/version_code.txt"
295 | ).readText()
296 | val versionNameFileContent = File(
297 | fixtureDir.root,
298 | "app/build/outputs/app_versioning/release/version_name.txt"
299 | ).readText()
300 |
301 | assertThat(task(":app:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
302 | assertThat(task(":app:assembleRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
303 |
304 | assertThat(output).contains("Generated app version code: 10203.")
305 | assertThat(output).contains("Generated app version name: \"1.2.3\".")
306 |
307 | assertThat(versionCodeFileContent).isEqualTo("10203")
308 | assertThat(versionNameFileContent).isEqualTo("1.2.3")
309 | }
310 | }
311 |
312 | @Test
313 | fun `plugin (when disabled) does not generate versionCode and versionName from git tag for the assembled APK when assemble task is run`() {
314 | GitClient.initialize(fixtureDir.root).apply {
315 | val commitId = commit(message = "1st commit.")
316 | tag(name = "1.0.0", message = "1st tag", commitId = commitId)
317 | }
318 |
319 | val extension = """
320 | appVersioning {
321 | enabled.set(false)
322 | }
323 | """.trimIndent()
324 | withFixtureRunner(
325 | fixtureDir = fixtureDir,
326 | subprojects = listOf(AppProjectTemplate(pluginExtension = extension))
327 | ).runAndCheckResult(
328 | "assembleRelease"
329 | ) {
330 | val versionCodeFile = File(
331 | fixtureDir.root,
332 | "app/build/outputs/app_versioning/release/version_code.txt"
333 | )
334 | val versionNameFile = File(
335 | fixtureDir.root,
336 | "app/build/outputs/app_versioning/release/version_name.txt"
337 | )
338 |
339 | assertThat(task(":app:assembleRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
340 |
341 | assertThat(output).doesNotContain("Generated app version code")
342 | assertThat(output).doesNotContain("Generated app version name")
343 |
344 | assertThat(versionCodeFile.exists()).isFalse()
345 | assertThat(versionNameFile.exists()).isFalse()
346 | }
347 | }
348 | }
349 |
--------------------------------------------------------------------------------
/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/fixtures/FixtureRunner.kt:
--------------------------------------------------------------------------------
1 | package io.github.reactivecircus.appversioning.fixtures
2 |
3 | import org.gradle.testkit.runner.BuildResult
4 | import org.gradle.testkit.runner.GradleRunner
5 | import org.gradle.util.GradleVersion
6 | import org.junit.rules.TemporaryFolder
7 | import java.io.File
8 |
9 | fun withFixtureRunner(
10 | fixtureDir: TemporaryFolder,
11 | subprojects: List,
12 | parentDirectoryName: String? = null,
13 | dryRun: Boolean = false
14 | ) = FixtureRunner(
15 | fixtureDir = fixtureDir,
16 | subprojects = subprojects,
17 | parentDirectoryName = parentDirectoryName,
18 | dryRun = dryRun
19 | )
20 |
21 | class FixtureRunner(
22 | fixtureDir: TemporaryFolder,
23 | subprojects: List,
24 | parentDirectoryName: String?,
25 | private val dryRun: Boolean
26 | ) {
27 | private val gradleRoot: File = if (parentDirectoryName.isNullOrBlank()) {
28 | fixtureDir.root
29 | } else {
30 | fixtureDir.root.resolve(parentDirectoryName)
31 | }
32 |
33 | init {
34 | fixtureDir.buildFixture(gradleRoot, subprojects)
35 | }
36 |
37 | fun runAndCheckResult(vararg commands: String, action: BuildResult.() -> Unit) {
38 | val buildResult = runner.withProjectDir(gradleRoot)
39 | .withArguments(buildArguments(commands.toList()))
40 | .withPluginClasspath()
41 | .withGradleVersion(GradleVersion.current().version)
42 | .build()
43 | action(buildResult)
44 | }
45 |
46 | fun runAndExpectFailure(vararg commands: String, action: BuildResult.() -> Unit) {
47 | val buildResult = runner.withProjectDir(gradleRoot)
48 | .withArguments(buildArguments(commands.toList()))
49 | .withPluginClasspath()
50 | .withGradleVersion(GradleVersion.current().version)
51 | .buildAndFail()
52 | action(buildResult)
53 | }
54 |
55 | private fun buildArguments(commands: List): List {
56 | val args = mutableListOf("--stacktrace", "--info")
57 | if (dryRun) {
58 | args.add("--dry-run")
59 | }
60 | args.addAll(commands)
61 | return args
62 | }
63 | }
64 |
65 | private val runner = GradleRunner.create()
66 | .withPluginClasspath()
67 | .withDebug(true)
68 |
69 | private fun TemporaryFolder.buildFixture(gradleRoot: File, subprojects: List) {
70 | // settings.gradle
71 | gradleRoot.resolve("settings.gradle.kts").also { it.parentFile.mkdirs() }
72 | .writeText(
73 | settingsFileContent(
74 | localBuildCacheUri = newFolder("local-cache").toURI().toString(),
75 | subprojects = subprojects
76 | )
77 | )
78 |
79 | // gradle.properties
80 | gradleRoot.resolve("gradle.properties").also { it.parentFile.mkdir() }
81 | .writeText(gradlePropertiesFileContent(enableConfigurationCache = false))
82 |
83 | // subprojects
84 | subprojects.forEach { subproject ->
85 | // build.gradle or build.gradle.kts
86 | val buildFileName = if (subproject.useKts) "build.gradle.kts" else "build.gradle"
87 | gradleRoot.resolve("${subproject.projectName}/$buildFileName").also { it.parentFile.mkdirs() }
88 | .writeText(subproject.buildFileContent)
89 |
90 | // AndroidManifest.xml
91 | gradleRoot.resolve("${subproject.projectName}/src/main/AndroidManifest.xml").also { it.parentFile.mkdirs() }
92 | .writeText(subproject.manifestFileContent)
93 | }
94 | }
95 |
--------------------------------------------------------------------------------
/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/fixtures/ProjectTemplates.kt:
--------------------------------------------------------------------------------
1 | package io.github.reactivecircus.appversioning.fixtures
2 |
3 | fun settingsFileContent(
4 | localBuildCacheUri: String,
5 | subprojects: List
6 | ) = """
7 | dependencyResolutionManagement {
8 | repositories {
9 | mavenCentral()
10 | google()
11 | }
12 | }
13 |
14 | pluginManagement {
15 | repositories {
16 | mavenCentral()
17 | google()
18 | }
19 | }
20 |
21 | buildCache {
22 | local {
23 | directory = "$localBuildCacheUri"
24 | }
25 | }
26 |
27 | rootProject.name = "app-versioning-fixture"
28 |
29 | ${subprojects.map { it.projectName }.joinToString("\n") { "include(\":$it\")" }}
30 | """.trimIndent()
31 |
32 | fun gradlePropertiesFileContent(enableConfigurationCache: Boolean): String {
33 | val configurationCacheProperties = if (enableConfigurationCache) {
34 | """
35 | org.gradle.configuration-cache=true
36 | org.gradle.unsafe.isolated-projects=true
37 | """.trimIndent()
38 | } else {
39 | ""
40 | }
41 | return """
42 | $configurationCacheProperties
43 | """.trimIndent()
44 | }
45 |
46 | sealed class AndroidProjectTemplate {
47 | abstract val projectName: String
48 | abstract val pluginExtension: String?
49 | abstract val useKts: Boolean
50 | abstract val flavors: List
51 |
52 | val buildFileContent: String get() = if (useKts) ktsBuildFileContent else groovyBuildFileContent
53 |
54 | private val isAppProject = this is AppProjectTemplate
55 |
56 | private val ktsBuildFileContent: String
57 | get() {
58 | val flavorConfigs = if (flavors.isNotEmpty()) {
59 | """
60 | flavorDimensions("environment")
61 | productFlavors {
62 | ${flavors.joinToString("\n") { "register(\"$it\") {}" }}
63 | }
64 | """.trimIndent()
65 | } else {
66 | ""
67 | }
68 | val abiConfigs = if (this is AppProjectTemplate) {
69 | if (splitsApks) {
70 | """
71 | splits {
72 | abi {
73 | isEnable = true
74 | reset()
75 | include("armeabi-v7a", "arm64-v8a", "x86", "x86_64")
76 | isUniversalApk = $universalApk
77 | }
78 | }
79 | """.trimIndent()
80 | } else {
81 | ""
82 | }
83 | } else {
84 | ""
85 | }
86 | return """
87 | plugins {
88 | id("com.android.${if (isAppProject) "application" else "library"}")
89 | id("io.github.reactivecircus.app-versioning")
90 | }
91 |
92 | ${pluginExtension ?: ""}
93 |
94 | android {
95 | namespace = "$DEFAULT_PACKAGE_NAME.${projectName.replace("-", ".")}"
96 | compileSdkVersion(34)
97 | buildToolsVersion = "34.0.0"
98 | defaultConfig {
99 | minSdkVersion(21)
100 | targetSdkVersion(34)
101 | }
102 |
103 | lintOptions.isCheckReleaseBuilds = false
104 |
105 | $flavorConfigs
106 |
107 | $abiConfigs
108 | }
109 | """.trimIndent()
110 | }
111 |
112 | private val groovyBuildFileContent: String
113 | get() {
114 | val flavorConfigs = if (flavors.isNotEmpty()) {
115 | """
116 | flavorDimensions "environment"
117 | productFlavors {
118 | ${flavors.joinToString("\n") { "$it {}" }}
119 | }
120 | """.trimIndent()
121 | } else {
122 | ""
123 | }
124 | val abiConfigs = if (this is AppProjectTemplate) {
125 | if (splitsApks) {
126 | """
127 | splits {
128 | abi {
129 | enable true
130 | reset()
131 | include "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
132 | universalApk $universalApk
133 | }
134 | }
135 | """.trimIndent()
136 | } else {
137 | ""
138 | }
139 | } else {
140 | ""
141 | }
142 | return """
143 | plugins {
144 | id 'com.android.${if (isAppProject) "application" else "library"}'
145 | id 'io.github.reactivecircus.app-versioning'
146 | }
147 |
148 | ${pluginExtension ?: ""}
149 |
150 | android {
151 | namespace '$DEFAULT_PACKAGE_NAME.${projectName.replace("-", ".")}'
152 | compileSdkVersion 34
153 | buildToolsVersion "34.0.0"
154 | defaultConfig {
155 | minSdkVersion 21
156 | targetSdkVersion 34
157 | }
158 |
159 | lintOptions {
160 | checkReleaseBuilds false
161 | }
162 |
163 | $flavorConfigs
164 |
165 | $abiConfigs
166 | }
167 | """.trimIndent()
168 | }
169 |
170 | val manifestFileContent: String
171 | get() = """
172 |
173 |
174 | """.trimIndent()
175 |
176 | companion object {
177 | private const val DEFAULT_PACKAGE_NAME = "io.github.reactivecircus.appversioning"
178 | }
179 | }
180 |
181 | class AppProjectTemplate(
182 | override val projectName: String = "app",
183 | override val pluginExtension: String? = null,
184 | override val useKts: Boolean = true,
185 | override val flavors: List = emptyList(),
186 | val splitsApks: Boolean = false,
187 | val universalApk: Boolean = false,
188 | ) : AndroidProjectTemplate()
189 |
190 | class LibraryProjectTemplate(
191 | override val projectName: String = "library",
192 | override val pluginExtension: String? = null,
193 | override val useKts: Boolean = true,
194 | override val flavors: List = emptyList()
195 | ) : AndroidProjectTemplate()
196 |
--------------------------------------------------------------------------------
/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/tasks/GenerateAppVersionInfoTest.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("FunctionName", "DuplicatedCode")
2 |
3 | package io.github.reactivecircus.appversioning.tasks
4 |
5 | import com.google.common.truth.Truth.assertThat
6 | import io.github.reactivecircus.appversioning.fixtures.AppProjectTemplate
7 | import io.github.reactivecircus.appversioning.fixtures.withFixtureRunner
8 | import io.github.reactivecircus.appversioning.internal.GitClient
9 | import org.gradle.testkit.runner.TaskOutcome
10 | import org.junit.Rule
11 | import org.junit.Test
12 | import org.junit.rules.TemporaryFolder
13 | import java.io.File
14 |
15 | class GenerateAppVersionInfoTest {
16 |
17 | @get:Rule
18 | val fixtureDir = TemporaryFolder()
19 |
20 | @Test
21 | fun `GenerateAppVersionInfo fails when root Gradle project is not a git root directory and gitRootDirectory is not provided`() {
22 | withFixtureRunner(
23 | fixtureDir = fixtureDir,
24 | subprojects = listOf(AppProjectTemplate())
25 | ).runAndExpectFailure(
26 | "generateAppVersionInfoForRelease"
27 | ) {
28 | assertThat(task(":app:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.FAILED)
29 | assertThat(output).contains(
30 | "Android App Versioning Gradle Plugin works with git tags but root project 'app-versioning-fixture' is not a git root directory, and a valid gitRootDirectory is not provided."
31 | )
32 | }
33 | }
34 |
35 | @Test
36 | fun `GenerateAppVersionInfo fails when root Gradle project is not a git root directory and gitRootDirectory provided is not a git root directory`() {
37 | val extensions = """
38 | appVersioning {
39 | gitRootDirectory.set(rootProject.file("../"))
40 | }
41 | """.trimIndent()
42 |
43 | withFixtureRunner(
44 | fixtureDir = fixtureDir,
45 | subprojects = listOf(AppProjectTemplate(pluginExtension = extensions)),
46 | parentDirectoryName = "android"
47 | ).runAndExpectFailure(
48 | "generateAppVersionInfoForRelease"
49 | ) {
50 | assertThat(task(":app:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.FAILED)
51 | assertThat(output).contains(
52 | "Android App Versioning Gradle Plugin works with git tags but root project 'app-versioning-fixture' is not a git root directory, and a valid gitRootDirectory is not provided."
53 | )
54 | }
55 | }
56 |
57 | @Test
58 | fun `GenerateAppVersionInfo generates versionCode and versionName when root Gradle project is not a git root directory but gitRootDirectory provided is a git root directory`() {
59 | GitClient.initialize(fixtureDir.root).apply {
60 | val commitId = commit(message = "1st commit.")
61 | tag(name = "1.2.3", message = "1st tag", commitId = commitId)
62 | }
63 |
64 | val versionCodeFile = File(fixtureDir.root, "android/app/build/outputs/app_versioning/release/version_code.txt")
65 | val versionNameFile = File(fixtureDir.root, "android/app/build/outputs/app_versioning/release/version_name.txt")
66 |
67 | val extensions = """
68 | appVersioning {
69 | gitRootDirectory.set(rootProject.file("../"))
70 | }
71 | """.trimIndent()
72 |
73 | withFixtureRunner(
74 | fixtureDir = fixtureDir,
75 | subprojects = listOf(AppProjectTemplate(pluginExtension = extensions)),
76 | parentDirectoryName = "android"
77 | ).runAndCheckResult(
78 | "generateAppVersionInfoForRelease"
79 | ) {
80 | assertThat(versionCodeFile.readText()).isEqualTo("10203")
81 | assertThat(versionNameFile.readText()).isEqualTo("1.2.3")
82 | }
83 | }
84 |
85 | @Test
86 | fun `GenerateAppVersionInfo generates versionCode by converting latest SemVer-compliant git tag to a single integer using positional notation when no custom versionCode generation rule is provided`() {
87 | val gitClient = GitClient.initialize(fixtureDir.root).apply {
88 | val commitId = commit(message = "1st commit.")
89 | tag(name = "0.0.1", message = "1st tag", commitId = commitId)
90 | }
91 |
92 | val versionCodeFile = File(fixtureDir.root, "app/build/outputs/app_versioning/release/version_code.txt")
93 |
94 | val runner = withFixtureRunner(
95 | fixtureDir = fixtureDir,
96 | subprojects = listOf(AppProjectTemplate())
97 | )
98 |
99 | runner.runAndCheckResult(
100 | "generateAppVersionInfoForRelease"
101 | ) {
102 | assertThat(versionCodeFile.readText()).isEqualTo("1")
103 | }
104 |
105 | val commitId2 = gitClient.commit(message = "2nd commit.")
106 | gitClient.tag(name = "0.1.0", message = "2nd tag", commitId = commitId2)
107 |
108 | runner.runAndCheckResult(
109 | "generateAppVersionInfoForRelease"
110 | ) {
111 | assertThat(versionCodeFile.readText()).isEqualTo("100")
112 | }
113 |
114 | val commitId3 = gitClient.commit(message = "3rd commit.")
115 | gitClient.tag(name = "1.0.0", message = "3rd tag", commitId = commitId3)
116 |
117 | runner.runAndCheckResult(
118 | "generateAppVersionInfoForRelease"
119 | ) {
120 | assertThat(versionCodeFile.readText()).isEqualTo("10000")
121 | }
122 |
123 | val commitId4 = gitClient.commit(message = "4th commit.")
124 | gitClient.tag(name = "1.0.99", message = "4th tag", commitId = commitId4)
125 |
126 | runner.runAndCheckResult(
127 | "generateAppVersionInfoForRelease"
128 | ) {
129 | assertThat(versionCodeFile.readText()).isEqualTo("10099")
130 | }
131 |
132 | val commitId5 = gitClient.commit(message = "5th commit.")
133 | gitClient.tag(name = "1.99.99", message = "5th tag", commitId = commitId5)
134 |
135 | runner.runAndCheckResult(
136 | "generateAppVersionInfoForRelease"
137 | ) {
138 | assertThat(versionCodeFile.readText()).isEqualTo("19999")
139 | }
140 |
141 | val commitId6 = gitClient.commit(message = "6th commit.")
142 | gitClient.tag(name = "1.2.3-alpha01+build.567", message = "6th tag", commitId = commitId6)
143 |
144 | runner.runAndCheckResult(
145 | "generateAppVersionInfoForRelease"
146 | ) {
147 | assertThat(versionCodeFile.readText()).isEqualTo("10203")
148 | }
149 |
150 | val commitId7 = gitClient.commit(message = "7th commit.")
151 | gitClient.tag(name = "v3.0.0-SNAPSHOT", message = "7th tag", commitId = commitId7)
152 |
153 | runner.runAndCheckResult(
154 | "generateAppVersionInfoForRelease"
155 | ) {
156 | assertThat(versionCodeFile.readText()).isEqualTo("30000")
157 | }
158 | }
159 |
160 | @Test
161 | fun `GenerateAppVersionInfo fails when latest git tag is not SemVer-compliant and no custom versionCode generation rule is provided`() {
162 | GitClient.initialize(fixtureDir.root).apply {
163 | val commitId = commit(message = "1st commit.")
164 | tag(name = "1.2", message = "1st tag", commitId = commitId)
165 | }
166 |
167 | withFixtureRunner(
168 | fixtureDir = fixtureDir,
169 | subprojects = listOf(AppProjectTemplate())
170 | ).runAndExpectFailure(
171 | "generateAppVersionInfoForRelease"
172 | ) {
173 | assertThat(task(":app:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.FAILED)
174 | assertThat(output).contains(
175 | """
176 | Could not generate versionCode as "1.2" does not follow semantic versioning.
177 | Please either ensure latest git tag follows semantic versioning, or provide a custom rule for generating versionCode using the `overrideVersionCode` lambda.
178 | """.trimIndent()
179 | )
180 | }
181 | }
182 |
183 | @Test
184 | fun `GenerateAppVersionInfo fails when SemVer-compliant git tag has a component that exceeds the maximum digits allocated (2) and no custom versionCode generation rule is provided`() {
185 | GitClient.initialize(fixtureDir.root).apply {
186 | val commitId = commit(message = "1st commit.")
187 | tag(name = "1.2.100", message = "1st tag", commitId = commitId)
188 | }
189 |
190 | withFixtureRunner(
191 | fixtureDir = fixtureDir,
192 | subprojects = listOf(AppProjectTemplate())
193 | ).runAndExpectFailure(
194 | "generateAppVersionInfoForRelease"
195 | ) {
196 | assertThat(task(":app:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.FAILED)
197 | assertThat(output).contains(
198 | """
199 | Could not generate versionCode from "1.2.100" as the SemVer cannot be represented as an Integer.
200 | This is usually because MAJOR or MINOR version is greater than 99, as by default maximum of 2 digits is allowed for MINOR and PATCH components of a SemVer tag.
201 | Another reason might be that the overall positional notation of the SemVer (MAJOR * 10000 + MINOR * 100 + PATCH) is greater than the maximum value of an integer (2147483647).
202 | As a workaround you can provide a custom rule for generating versionCode using the `overrideVersionCode` lambda.
203 | """.trimIndent()
204 | )
205 | }
206 | }
207 |
208 | @Test
209 | fun `GenerateAppVersionInfo generates fallback versionCode and versionName when no git tags exist`() {
210 | GitClient.initialize(fixtureDir.root).apply {
211 | commit(message = "1st commit.")
212 | }
213 |
214 | val versionCodeFile = File(fixtureDir.root, "app/build/outputs/app_versioning/release/version_code.txt")
215 | val versionNameFile = File(fixtureDir.root, "app/build/outputs/app_versioning/release/version_name.txt")
216 |
217 | withFixtureRunner(
218 | fixtureDir = fixtureDir,
219 | subprojects = listOf(AppProjectTemplate())
220 | ).runAndCheckResult(
221 | "generateAppVersionInfoForRelease"
222 | ) {
223 | assertThat(task(":app:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
224 | assertThat(output).contains(
225 | """
226 | No git tags found. Falling back to version code ${GenerateAppVersionInfo.VERSION_CODE_FALLBACK} and version name "${GenerateAppVersionInfo.VERSION_NAME_FALLBACK}".
227 | If you want to fallback to the versionCode and versionName set via the DSL or manifest, or stop generating versionCode and versionName from Git tags:
228 | appVersioning {
229 | enabled.set(false)
230 | }
231 | """.trimIndent()
232 | )
233 | assertThat(versionCodeFile.readText()).isEqualTo(GenerateAppVersionInfo.VERSION_CODE_FALLBACK.toString())
234 | assertThat(versionNameFile.readText()).isEqualTo(GenerateAppVersionInfo.VERSION_NAME_FALLBACK)
235 | }
236 | }
237 |
238 | @Test
239 | fun `GenerateAppVersionInfo generates custom versionCode when custom versionCode generation rule is provided`() {
240 | GitClient.initialize(fixtureDir.root).apply {
241 | val commitId = commit(message = "1st commit.")
242 | tag(name = "1.2.3", message = "1st tag", commitId = commitId)
243 | }
244 |
245 | val versionCodeFile = File(fixtureDir.root, "app/build/outputs/app_versioning/release/version_code.txt")
246 |
247 | val extensions = """
248 | import io.github.reactivecircus.appversioning.toSemVer
249 | appVersioning {
250 | overrideVersionCode { gitTag, providers, _ ->
251 | val buildNumber = providers
252 | .gradleProperty("buildNumber")
253 | .orNull?.toInt()?: 0
254 | val semVer = gitTag.toSemVer()
255 | semVer.major * 1000000 + semVer.minor * 10000 + semVer.patch * 100 + buildNumber
256 | }
257 | }
258 | """.trimIndent()
259 |
260 | withFixtureRunner(
261 | fixtureDir = fixtureDir,
262 | subprojects = listOf(AppProjectTemplate(pluginExtension = extensions))
263 | ).runAndCheckResult(
264 | "generateAppVersionInfoForRelease",
265 | "-PbuildNumber=78"
266 | ) {
267 | assertThat(versionCodeFile.readText()).isEqualTo("1020378")
268 | }
269 | }
270 |
271 | @Test
272 | fun `GenerateAppVersionInfo fails when converting a non-SemVer compliant git tag to SemVer in a custom versionCode generation rule`() {
273 | GitClient.initialize(fixtureDir.root).apply {
274 | val commitId = commit(message = "1st commit.")
275 | tag(name = "1.2", message = "1st tag", commitId = commitId)
276 | }
277 |
278 | val extensions = """
279 | import io.github.reactivecircus.appversioning.toSemVer
280 | appVersioning {
281 | overrideVersionCode { gitTag, providers, _ ->
282 | val semVer = gitTag.toSemVer()
283 | semVer.major * 1000000 + semVer.minor * 10000 + semVer.patch * 100
284 | }
285 | }
286 | """.trimIndent()
287 |
288 | withFixtureRunner(
289 | fixtureDir = fixtureDir,
290 | subprojects = listOf(AppProjectTemplate(pluginExtension = extensions))
291 | ).runAndExpectFailure(
292 | "generateAppVersionInfoForRelease"
293 | ) {
294 | assertThat(task(":app:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.FAILED)
295 | assertThat(output).contains("\"1.2\" is not a valid SemVer.")
296 | }
297 | }
298 |
299 | @Test
300 | fun `GenerateAppVersionInfo generates versionName directly from the latest git tag when no custom versionName generation rule is provided`() {
301 | val gitClient = GitClient.initialize(fixtureDir.root).apply {
302 | val commitId = commit(message = "1st commit.")
303 | tag(name = "0.0.1", message = "1st tag", commitId = commitId)
304 | }
305 |
306 | val versionNameFile = File(fixtureDir.root, "app/build/outputs/app_versioning/release/version_name.txt")
307 |
308 | val extensions = """
309 | appVersioning {
310 | overrideVersionCode { _, _, _ -> 0 }
311 | }
312 | """.trimIndent()
313 |
314 | val runner = withFixtureRunner(
315 | fixtureDir = fixtureDir,
316 | subprojects = listOf(AppProjectTemplate(pluginExtension = extensions))
317 | )
318 |
319 | runner.runAndCheckResult(
320 | "generateAppVersionInfoForRelease"
321 | ) {
322 | assertThat(versionNameFile.readText()).isEqualTo("0.0.1")
323 | }
324 |
325 | val commitId2 = gitClient.commit(message = "2nd commit.")
326 | gitClient.tag(name = "v1.1.0-alpha01", message = "2nd tag", commitId = commitId2)
327 |
328 | runner.runAndCheckResult(
329 | "generateAppVersionInfoForRelease"
330 | ) {
331 | assertThat(versionNameFile.readText()).isEqualTo("v1.1.0-alpha01")
332 | }
333 |
334 | val commitId3 = gitClient.commit(message = "3rd commit.")
335 | gitClient.tag(name = "alpha", message = "3rd tag", commitId = commitId3)
336 |
337 | runner.runAndCheckResult(
338 | "generateAppVersionInfoForRelease"
339 | ) {
340 | assertThat(versionNameFile.readText()).isEqualTo("alpha")
341 | }
342 | }
343 |
344 | @Test
345 | fun `GenerateAppVersionInfo generates custom versionName when custom versionName generation rule is provided`() {
346 | GitClient.initialize(fixtureDir.root).apply {
347 | val commitId = commit(message = "1st commit.")
348 | tag(name = "1.2.3", message = "1st tag", commitId = commitId)
349 | }
350 |
351 | val versionNameFile = File(fixtureDir.root, "app/build/outputs/app_versioning/release/version_name.txt")
352 |
353 | val extensions = """
354 | appVersioning {
355 | overrideVersionName { gitTag, _, _ ->
356 | "Version " + gitTag.toString()
357 | }
358 | }
359 | """.trimIndent()
360 |
361 | withFixtureRunner(
362 | fixtureDir = fixtureDir,
363 | subprojects = listOf(AppProjectTemplate(pluginExtension = extensions))
364 | ).runAndCheckResult(
365 | "generateAppVersionInfoForRelease"
366 | ) {
367 | assertThat(versionNameFile.readText()).isEqualTo("Version 1.2.3")
368 | }
369 | }
370 |
371 | @Test
372 | fun `GenerateAppVersionInfo can generate custom versionCode based on build variant`() {
373 | GitClient.initialize(fixtureDir.root).apply {
374 | val commitId = commit(message = "1st commit.")
375 | tag(name = "1.2.3", message = "1st tag", commitId = commitId)
376 | }
377 |
378 | val versionCodeFileForFreeDebug = File(
379 | fixtureDir.root,
380 | "app/build/outputs/app_versioning/freeDebug/version_code.txt"
381 | )
382 | val versionCodeFileForFreeRelease = File(
383 | fixtureDir.root,
384 | "app/build/outputs/app_versioning/freeRelease/version_code.txt"
385 | )
386 | val versionCodeFileForPaidDebug = File(
387 | fixtureDir.root,
388 | "app/build/outputs/app_versioning/paidDebug/version_code.txt"
389 | )
390 | val versionCodeFileForPaidRelease = File(
391 | fixtureDir.root,
392 | "app/build/outputs/app_versioning/paidRelease/version_code.txt"
393 | )
394 |
395 | val extensions = """
396 | import io.github.reactivecircus.appversioning.toSemVer
397 | appVersioning {
398 | overrideVersionCode { gitTag, _, variantInfo ->
399 | val offset = if (variantInfo.flavorName == "paid") 1 else 0
400 | val semVer = gitTag.toSemVer()
401 | semVer.major * 1000000 + semVer.minor * 10000 + semVer.patch * 100 + offset
402 | }
403 | }
404 | """.trimIndent()
405 |
406 | val flavors = listOf("free", "paid")
407 | val runner = withFixtureRunner(
408 | fixtureDir = fixtureDir,
409 | subprojects = listOf(AppProjectTemplate(pluginExtension = extensions, flavors = flavors))
410 | )
411 |
412 | runner.runAndCheckResult(
413 | "generateAppVersionInfoForFreeDebug"
414 | ) {
415 | assertThat(versionCodeFileForFreeDebug.readText()).isEqualTo("1020300")
416 | }
417 |
418 | runner.runAndCheckResult(
419 | "generateAppVersionInfoForFreeRelease"
420 | ) {
421 | assertThat(versionCodeFileForFreeRelease.readText()).isEqualTo("1020300")
422 | }
423 |
424 | runner.runAndCheckResult(
425 | "generateAppVersionInfoForPaidDebug"
426 | ) {
427 | assertThat(versionCodeFileForPaidDebug.readText()).isEqualTo("1020301")
428 | }
429 |
430 | runner.runAndCheckResult(
431 | "generateAppVersionInfoForPaidRelease"
432 | ) {
433 | assertThat(versionCodeFileForPaidRelease.readText()).isEqualTo("1020301")
434 | }
435 | }
436 |
437 | @Test
438 | fun `GenerateAppVersionInfo can generate custom versionName based on build variant`() {
439 | GitClient.initialize(fixtureDir.root).apply {
440 | val commitId = commit(message = "1st commit.")
441 | tag(name = "1.2.3", message = "1st tag", commitId = commitId)
442 | }
443 |
444 | val versionNameFileForFreeDebug = File(
445 | fixtureDir.root,
446 | "app/build/outputs/app_versioning/freeDebug/version_name.txt"
447 | )
448 | val versionNameFileForFreeRelease = File(
449 | fixtureDir.root,
450 | "app/build/outputs/app_versioning/freeRelease/version_name.txt"
451 | )
452 | val versionNameFileForPaidDebug = File(
453 | fixtureDir.root,
454 | "app/build/outputs/app_versioning/paidDebug/version_name.txt"
455 | )
456 | val versionNameFileForPaidRelease = File(
457 | fixtureDir.root,
458 | "app/build/outputs/app_versioning/paidRelease/version_name.txt"
459 | )
460 |
461 | val extensions = """
462 | appVersioning {
463 | overrideVersionName { gitTag, _, variantInfo ->
464 | val suffix = if (!variantInfo.isReleaseBuild) {
465 | " (" + variantInfo.variantName + ")"
466 | } else {
467 | ""
468 | }
469 | "Version " + gitTag.toString() + suffix
470 | }
471 | }
472 | """.trimIndent()
473 |
474 | val flavors = listOf("free", "paid")
475 | val runner = withFixtureRunner(
476 | fixtureDir = fixtureDir,
477 | subprojects = listOf(AppProjectTemplate(pluginExtension = extensions, flavors = flavors))
478 | )
479 |
480 | runner.runAndCheckResult(
481 | "generateAppVersionInfoForFreeDebug"
482 | ) {
483 | assertThat(versionNameFileForFreeDebug.readText()).isEqualTo("Version 1.2.3 (freeDebug)")
484 | }
485 |
486 | runner.runAndCheckResult(
487 | "generateAppVersionInfoForFreeRelease"
488 | ) {
489 | assertThat(versionNameFileForFreeRelease.readText()).isEqualTo("Version 1.2.3")
490 | }
491 |
492 | runner.runAndCheckResult(
493 | "generateAppVersionInfoForPaidDebug"
494 | ) {
495 | assertThat(versionNameFileForPaidDebug.readText()).isEqualTo("Version 1.2.3 (paidDebug)")
496 | }
497 |
498 | runner.runAndCheckResult(
499 | "generateAppVersionInfoForPaidRelease"
500 | ) {
501 | assertThat(versionNameFileForPaidRelease.readText()).isEqualTo("Version 1.2.3")
502 | }
503 | }
504 |
505 | @Test
506 | fun `GenerateAppVersionInfo generates versionCode and versionName from the latest git tag matching the provided tagFilter pattern when a tagFilter pattern is provided`() {
507 | val gitClient = GitClient.initialize(fixtureDir.root).apply {
508 | commit(message = "Initial commit.")
509 | }
510 |
511 | val versionCodeFileAppA = File(fixtureDir.root, "app-a/build/outputs/app_versioning/release/version_code.txt")
512 | val versionCodeFileAppB = File(fixtureDir.root, "app-b/build/outputs/app_versioning/release/version_code.txt")
513 | val versionCodeFileAppC = File(fixtureDir.root, "app-c/build/outputs/app_versioning/release/version_code.txt")
514 | val versionNameFileAppA = File(fixtureDir.root, "app-a/build/outputs/app_versioning/release/version_name.txt")
515 | val versionNameFileAppB = File(fixtureDir.root, "app-b/build/outputs/app_versioning/release/version_name.txt")
516 | val versionNameFileAppC = File(fixtureDir.root, "app-c/build/outputs/app_versioning/release/version_name.txt")
517 |
518 | val tagFilterAppA = "[0-9]*.[0-9]*.[0-9]*+appA"
519 | val tagFilterAppB = "[0-9]*.[0-9]*.[0-9]*+appB"
520 | val tagFilterAppC = "[0-9]*.[0-9]*.[0-9]*+appC"
521 |
522 | val extensionsAppA = """
523 | appVersioning {
524 | tagFilter.set("$tagFilterAppA")
525 | }
526 | """.trimIndent()
527 |
528 | val extensionsAppB = """
529 | appVersioning {
530 | tagFilter.set("$tagFilterAppB")
531 | }
532 | """.trimIndent()
533 |
534 | val extensionsAppC = """
535 | appVersioning {
536 | tagFilter.set("$tagFilterAppC")
537 | }
538 | """.trimIndent()
539 |
540 | val runner = withFixtureRunner(
541 | fixtureDir = fixtureDir,
542 | subprojects = listOf(
543 | AppProjectTemplate(
544 | projectName = "app-a",
545 | pluginExtension = extensionsAppA
546 | ),
547 | AppProjectTemplate(
548 | projectName = "app-b",
549 | pluginExtension = extensionsAppB
550 | ),
551 | AppProjectTemplate(
552 | projectName = "app-c",
553 | pluginExtension = extensionsAppC
554 | )
555 | )
556 | )
557 |
558 | // 1st appA release
559 | val commitId = gitClient.commit(message = "appA release.")
560 | gitClient.tag(name = "0.1.0+appA", message = "1st tag", commitId = commitId)
561 |
562 | runner.runAndCheckResult(
563 | "generateAppVersionInfoForRelease"
564 | ) {
565 | assertThat(task(":app-a:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
566 | assertThat(versionCodeFileAppA.readText()).isEqualTo("100")
567 | assertThat(versionNameFileAppA.readText()).isEqualTo("0.1.0+appA")
568 |
569 | assertThat(task(":app-b:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
570 | assertThat(versionCodeFileAppB.readText()).isEqualTo(
571 | GenerateAppVersionInfo.VERSION_CODE_FALLBACK.toString()
572 | )
573 | assertThat(versionNameFileAppB.readText()).isEqualTo(GenerateAppVersionInfo.VERSION_NAME_FALLBACK)
574 |
575 | assertThat(task(":app-c:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
576 | assertThat(versionCodeFileAppC.readText()).isEqualTo(
577 | GenerateAppVersionInfo.VERSION_CODE_FALLBACK.toString()
578 | )
579 | assertThat(versionNameFileAppC.readText()).isEqualTo(GenerateAppVersionInfo.VERSION_NAME_FALLBACK)
580 | }
581 |
582 | gitClient.commit(message = "Commit a.")
583 | gitClient.commit(message = "Commit b.")
584 |
585 | // 1st appB release
586 | val commitId2 = gitClient.commit(message = "appB release.")
587 | gitClient.tag(name = "1.2.3-rc01+appB", message = "2nd tag", commitId = commitId2)
588 |
589 | runner.runAndCheckResult(
590 | "generateAppVersionInfoForRelease"
591 | ) {
592 | assertThat(task(":app-a:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
593 | assertThat(versionCodeFileAppA.readText()).isEqualTo("100")
594 | assertThat(versionNameFileAppA.readText()).isEqualTo("0.1.0+appA")
595 |
596 | assertThat(task(":app-b:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
597 | assertThat(versionCodeFileAppB.readText()).isEqualTo("10203")
598 | assertThat(versionNameFileAppB.readText()).isEqualTo("1.2.3-rc01+appB")
599 |
600 | assertThat(task(":app-c:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
601 | assertThat(versionCodeFileAppC.readText()).isEqualTo(
602 | GenerateAppVersionInfo.VERSION_CODE_FALLBACK.toString()
603 | )
604 | assertThat(versionNameFileAppC.readText()).isEqualTo(GenerateAppVersionInfo.VERSION_NAME_FALLBACK)
605 | }
606 |
607 | gitClient.commit(message = "Commit c.")
608 |
609 | // 2nd appB release
610 | val commitId3 = gitClient.commit(message = "appB release.")
611 | gitClient.tag(name = "1.2.3-rc02+appB", message = "3rd tag", commitId = commitId3)
612 |
613 | runner.runAndCheckResult(
614 | "generateAppVersionInfoForRelease"
615 | ) {
616 | assertThat(task(":app-a:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
617 | assertThat(versionCodeFileAppA.readText()).isEqualTo("100")
618 | assertThat(versionNameFileAppA.readText()).isEqualTo("0.1.0+appA")
619 |
620 | assertThat(task(":app-b:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
621 | assertThat(versionCodeFileAppB.readText()).isEqualTo("10203")
622 | assertThat(versionNameFileAppB.readText()).isEqualTo("1.2.3-rc02+appB")
623 |
624 | assertThat(task(":app-c:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
625 | assertThat(versionCodeFileAppC.readText()).isEqualTo(
626 | GenerateAppVersionInfo.VERSION_CODE_FALLBACK.toString()
627 | )
628 | assertThat(versionNameFileAppC.readText()).isEqualTo(GenerateAppVersionInfo.VERSION_NAME_FALLBACK)
629 | }
630 |
631 | // 1st appC release
632 | val commitId4 = gitClient.commit(message = "appC release.")
633 | gitClient.tag(name = "10.3.5-alpha03+appC", message = "4th tag", commitId = commitId4)
634 |
635 | runner.runAndCheckResult(
636 | "generateAppVersionInfoForRelease"
637 | ) {
638 | assertThat(task(":app-a:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
639 | assertThat(versionCodeFileAppA.readText()).isEqualTo("100")
640 | assertThat(versionNameFileAppA.readText()).isEqualTo("0.1.0+appA")
641 |
642 | assertThat(task(":app-b:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
643 | assertThat(versionCodeFileAppB.readText()).isEqualTo("10203")
644 | assertThat(versionNameFileAppB.readText()).isEqualTo("1.2.3-rc02+appB")
645 |
646 | assertThat(task(":app-c:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
647 | assertThat(versionCodeFileAppC.readText()).isEqualTo("100305")
648 | assertThat(versionNameFileAppC.readText()).isEqualTo("10.3.5-alpha03+appC")
649 | }
650 |
651 | gitClient.commit(message = "Commit d.")
652 |
653 | // 2nd appA release
654 | val commitId5 = gitClient.commit(message = "appA release.")
655 | gitClient.tag(name = "0.2.1+appA", message = "4th tag", commitId = commitId5)
656 |
657 | runner.runAndCheckResult(
658 | "generateAppVersionInfoForRelease"
659 | ) {
660 | assertThat(task(":app-a:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
661 | assertThat(versionCodeFileAppA.readText()).isEqualTo("201")
662 | assertThat(versionNameFileAppA.readText()).isEqualTo("0.2.1+appA")
663 |
664 | assertThat(task(":app-b:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
665 | assertThat(versionCodeFileAppB.readText()).isEqualTo("10203")
666 | assertThat(versionNameFileAppB.readText()).isEqualTo("1.2.3-rc02+appB")
667 |
668 | assertThat(task(":app-c:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
669 | assertThat(versionCodeFileAppC.readText()).isEqualTo("100305")
670 | assertThat(versionNameFileAppC.readText()).isEqualTo("10.3.5-alpha03+appC")
671 | }
672 | }
673 |
674 | @Test
675 | fun `overrideVersionCode and overrideVersionName configurations work in groovy`() {
676 | GitClient.initialize(fixtureDir.root).apply {
677 | val commitId = commit(message = "1st commit.")
678 | tag(name = "1.2.3", message = "1st tag", commitId = commitId)
679 | commit(message = "2nd commit.")
680 | }
681 |
682 | val versionCodeFileForDebug = File(fixtureDir.root, "app/build/outputs/app_versioning/debug/version_code.txt")
683 | val versionNameFileForDebug = File(fixtureDir.root, "app/build/outputs/app_versioning/debug/version_name.txt")
684 | val versionCodeFileForRelease = File(
685 | fixtureDir.root,
686 | "app/build/outputs/app_versioning/release/version_code.txt"
687 | )
688 | val versionNameFileForRelease = File(
689 | fixtureDir.root,
690 | "app/build/outputs/app_versioning/release/version_name.txt"
691 | )
692 |
693 | val extensions = """
694 | import io.github.reactivecircus.appversioning.SemVer
695 | appVersioning {
696 | overrideVersionCode { gitTag, providers, variantInfo ->
697 | def semVer = SemVer.fromGitTag(gitTag)
698 | semVer.major * 10000 + semVer.minor * 100 + semVer.patch + gitTag.commitsSinceLatestTag
699 | }
700 | overrideVersionName { gitTag, providers, variantInfo ->
701 | def suffix
702 | if (variantInfo.debugBuild == true) {
703 | suffix = " (" + variantInfo.variantName + ")"
704 | } else {
705 | suffix = ""
706 | }
707 | "Version " + gitTag.toString() + suffix
708 | }
709 | }
710 | """.trimIndent()
711 |
712 | val runner = withFixtureRunner(
713 | fixtureDir = fixtureDir,
714 | subprojects = listOf(AppProjectTemplate(pluginExtension = extensions, useKts = false))
715 | )
716 |
717 | runner.runAndCheckResult(
718 | "generateAppVersionInfoForDebug"
719 | ) {
720 | assertThat(task(":app:generateAppVersionInfoForDebug")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
721 | assertThat(versionCodeFileForDebug.readText()).isEqualTo("10204")
722 | assertThat(versionNameFileForDebug.readText()).isEqualTo("Version 1.2.3 (debug)")
723 | }
724 |
725 | runner.runAndCheckResult(
726 | "generateAppVersionInfoForRelease"
727 | ) {
728 | assertThat(task(":app:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
729 | assertThat(versionCodeFileForRelease.readText()).isEqualTo("10204")
730 | assertThat(versionNameFileForRelease.readText()).isEqualTo("Version 1.2.3")
731 | }
732 | }
733 |
734 | @Test
735 | fun `GenerateAppVersionInfo is incremental without custom versionCode and versionName generation rules`() {
736 | GitClient.initialize(fixtureDir.root).apply {
737 | val commitId = commit(message = "1st commit.")
738 | tag(name = "1.2.3", message = "1st tag", commitId = commitId)
739 | }
740 |
741 | val runner = withFixtureRunner(
742 | fixtureDir = fixtureDir,
743 | subprojects = listOf(AppProjectTemplate())
744 | )
745 |
746 | runner.runAndCheckResult(
747 | "generateAppVersionInfoForRelease"
748 | ) {
749 | assertThat(task(":app:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
750 | }
751 |
752 | runner.runAndCheckResult(
753 | "generateAppVersionInfoForRelease"
754 | ) {
755 | assertThat(task(":app:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.UP_TO_DATE)
756 | }
757 | }
758 |
759 | @Test
760 | fun `GenerateAppVersionInfo is incremental with custom versionCode and versionName generation rules`() {
761 | GitClient.initialize(fixtureDir.root).apply {
762 | val commitId = commit(message = "1st commit.")
763 | tag(name = "1.2.3", message = "1st tag", commitId = commitId)
764 | commit(message = "2nd commit.")
765 | }
766 |
767 | val extensions = """
768 | import io.github.reactivecircus.appversioning.toSemVer
769 | appVersioning {
770 | overrideVersionCode { gitTag, _, _ ->
771 | val semVer = gitTag.toSemVer()
772 | semVer.major * 10000 + semVer.minor * 100 + semVer.patch + gitTag.commitsSinceLatestTag
773 | }
774 | overrideVersionName { gitTag, _, _ ->
775 | "Version " + gitTag.toString()
776 | }
777 | }
778 | """.trimIndent()
779 |
780 | val runner = withFixtureRunner(
781 | fixtureDir = fixtureDir,
782 | subprojects = listOf(AppProjectTemplate(pluginExtension = extensions))
783 | )
784 |
785 | runner.runAndCheckResult(
786 | "generateAppVersionInfoForRelease"
787 | ) {
788 | assertThat(task(":app:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
789 | }
790 |
791 | runner.runAndCheckResult(
792 | "generateAppVersionInfoForRelease"
793 | ) {
794 | assertThat(task(":app:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.UP_TO_DATE)
795 | }
796 | }
797 |
798 | @Test
799 | fun `GenerateAppVersionInfo is cacheable without custom versionCode and versionName generation rules`() {
800 | GitClient.initialize(fixtureDir.root).apply {
801 | val commitId = commit(message = "1st commit.")
802 | tag(name = "1.2.3", message = "1st tag", commitId = commitId)
803 | }
804 |
805 | val runner = withFixtureRunner(
806 | fixtureDir = fixtureDir,
807 | subprojects = listOf(AppProjectTemplate())
808 | )
809 |
810 | runner.runAndCheckResult(
811 | "generateAppVersionInfoForRelease",
812 | "--build-cache"
813 | ) {
814 | assertThat(task(":app:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
815 | }
816 |
817 | runner.runAndCheckResult(
818 | "clean",
819 | "generateAppVersionInfoForRelease",
820 | "--build-cache"
821 | ) {
822 | assertThat(task(":app:clean")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
823 | assertThat(task(":app:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.FROM_CACHE)
824 | }
825 | }
826 |
827 | @Test
828 | fun `GenerateAppVersionInfo is cacheable with custom versionCode and versionName generation rules`() {
829 | GitClient.initialize(fixtureDir.root).apply {
830 | val commitId = commit(message = "1st commit.")
831 | tag(name = "1.2.3", message = "1st tag", commitId = commitId)
832 | commit(message = "2nd commit.")
833 | }
834 |
835 | val extensions = """
836 | import io.github.reactivecircus.appversioning.toSemVer
837 | appVersioning {
838 | overrideVersionCode { gitTag, _, _ ->
839 | val semVer = gitTag.toSemVer()
840 | semVer.major * 10000 + semVer.minor * 100 + semVer.patch + gitTag.commitsSinceLatestTag
841 | }
842 | overrideVersionName { gitTag, _, _ ->
843 | "Version " + gitTag.toString()
844 | }
845 | }
846 | """.trimIndent()
847 |
848 | val runner = withFixtureRunner(
849 | fixtureDir = fixtureDir,
850 | subprojects = listOf(AppProjectTemplate(pluginExtension = extensions))
851 | )
852 |
853 | runner.runAndCheckResult(
854 | "generateAppVersionInfoForRelease",
855 | "--build-cache"
856 | ) {
857 | assertThat(task(":app:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
858 | }
859 |
860 | runner.runAndCheckResult(
861 | "clean",
862 | "generateAppVersionInfoForRelease",
863 | "--build-cache"
864 | ) {
865 | assertThat(task(":app:clean")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
866 | assertThat(task(":app:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.FROM_CACHE)
867 | }
868 | }
869 |
870 | @Test
871 | fun `GenerateAppVersionInfo is incremental for specific build variants`() {
872 | GitClient.initialize(fixtureDir.root).apply {
873 | val commitId = commit(message = "1st commit.")
874 | tag(name = "1.2.3", message = "1st tag", commitId = commitId)
875 | }
876 |
877 | val flavors = listOf("mock", "prod")
878 | val runner = withFixtureRunner(
879 | fixtureDir = fixtureDir,
880 | subprojects = listOf(AppProjectTemplate(flavors = flavors))
881 | )
882 |
883 | runner.runAndCheckResult(
884 | "generateAppVersionInfoForProdRelease"
885 | ) {
886 | assertThat(task(":app:generateAppVersionInfoForProdRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
887 | }
888 |
889 | runner.runAndCheckResult(
890 | "generateAppVersionInfoForMockRelease"
891 | ) {
892 | assertThat(task(":app:generateAppVersionInfoForMockRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
893 | }
894 | }
895 |
896 | @Test
897 | fun `GenerateAppVersionInfo is cacheable for specific build variants`() {
898 | GitClient.initialize(fixtureDir.root).apply {
899 | val commitId = commit(message = "1st commit.")
900 | tag(name = "1.2.3", message = "1st tag", commitId = commitId)
901 | }
902 |
903 | val flavors = listOf("mock", "prod")
904 | val runner = withFixtureRunner(
905 | fixtureDir = fixtureDir,
906 | subprojects = listOf(AppProjectTemplate(flavors = flavors))
907 | )
908 |
909 | runner.runAndCheckResult(
910 | "generateAppVersionInfoForProdRelease",
911 | "--build-cache"
912 | ) {
913 | assertThat(task(":app:generateAppVersionInfoForProdRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
914 | }
915 |
916 | runner.runAndCheckResult(
917 | "clean",
918 | "generateAppVersionInfoForMockRelease",
919 | "--build-cache"
920 | ) {
921 | assertThat(task(":app:clean")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
922 | assertThat(task(":app:generateAppVersionInfoForMockRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
923 | }
924 | }
925 |
926 | @Test
927 | fun `GenerateAppVersionInfo (up-to-date) is re-executed after changing git refs`() {
928 | val gitClient = GitClient.initialize(fixtureDir.root).apply {
929 | val commitId = commit(message = "1st commit.")
930 | tag(name = "1.2.3", message = "1st tag", commitId = commitId)
931 | }
932 |
933 | val runner = withFixtureRunner(
934 | fixtureDir = fixtureDir,
935 | subprojects = listOf(AppProjectTemplate())
936 | )
937 |
938 | runner.runAndCheckResult(
939 | "generateAppVersionInfoForRelease"
940 | ) {
941 | assertThat(task(":app:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
942 | }
943 |
944 | runner.runAndCheckResult(
945 | "generateAppVersionInfoForRelease"
946 | ) {
947 | assertThat(task(":app:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.UP_TO_DATE)
948 | }
949 |
950 | gitClient.commit(message = "2nd commit.")
951 |
952 | runner.runAndCheckResult(
953 | "generateAppVersionInfoForRelease"
954 | ) {
955 | assertThat(task(":app:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
956 | }
957 | }
958 |
959 | @Test
960 | fun `GenerateAppVersionInfo (from cache) is re-executed after changing git refs`() {
961 | val gitClient = GitClient.initialize(fixtureDir.root).apply {
962 | val commitId = commit(message = "1st commit.")
963 | tag(name = "1.2.3", message = "1st tag", commitId = commitId)
964 | }
965 | val commitId2 = gitClient.commit(message = "2nd commit.")
966 |
967 | val runner = withFixtureRunner(
968 | fixtureDir = fixtureDir,
969 | subprojects = listOf(AppProjectTemplate())
970 | )
971 |
972 | runner.runAndCheckResult(
973 | "generateAppVersionInfoForRelease",
974 | "--build-cache"
975 | ) {
976 | assertThat(task(":app:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
977 | }
978 |
979 | runner.runAndCheckResult(
980 | "clean",
981 | "generateAppVersionInfoForRelease",
982 | "--build-cache"
983 | ) {
984 | assertThat(task(":app:clean")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
985 | assertThat(task(":app:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.FROM_CACHE)
986 | }
987 |
988 | gitClient.tag(name = "1.3.0", message = "2nd tag", commitId = commitId2)
989 |
990 | runner.runAndCheckResult(
991 | "clean",
992 | "generateAppVersionInfoForRelease",
993 | "--build-cache"
994 | ) {
995 | assertThat(task(":app:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
996 | }
997 | }
998 |
999 | @Test
1000 | fun `GenerateAppVersionInfo (up-to-date) is re-executed after changing git HEAD`() {
1001 | val gitClient = GitClient.initialize(fixtureDir.root).apply {
1002 | val commitId1 = commit(message = "1st commit.")
1003 | tag(name = "1.2.3", message = "1st tag", commitId = commitId1)
1004 | val commitId2 = commit(message = "2nd commit.")
1005 | tag(name = "1.2.4", message = "2st tag", commitId = commitId2)
1006 | }
1007 |
1008 | val runner = withFixtureRunner(
1009 | fixtureDir = fixtureDir,
1010 | subprojects = listOf(AppProjectTemplate())
1011 | )
1012 |
1013 | runner.runAndCheckResult(
1014 | "generateAppVersionInfoForRelease"
1015 | ) {
1016 | assertThat(task(":app:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
1017 | }
1018 |
1019 | runner.runAndCheckResult(
1020 | "generateAppVersionInfoForRelease"
1021 | ) {
1022 | assertThat(task(":app:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.UP_TO_DATE)
1023 | }
1024 |
1025 | gitClient.checkoutTag(tag = "1.2.3")
1026 |
1027 | runner.runAndCheckResult(
1028 | "generateAppVersionInfoForRelease"
1029 | ) {
1030 | assertThat(task(":app:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
1031 | }
1032 | }
1033 |
1034 | @Test
1035 | fun `GenerateAppVersionInfo (from cache) is re-executed after changing git HEAD`() {
1036 | val gitClient = GitClient.initialize(fixtureDir.root).apply {
1037 | val commitId1 = commit(message = "1st commit.")
1038 | tag(name = "1.2.3", message = "1st tag", commitId = commitId1)
1039 | val commitId2 = commit(message = "2nd commit.")
1040 | tag(name = "1.2.4", message = "2st tag", commitId = commitId2)
1041 | }
1042 |
1043 | val runner = withFixtureRunner(
1044 | fixtureDir = fixtureDir,
1045 | subprojects = listOf(AppProjectTemplate())
1046 | )
1047 |
1048 | runner.runAndCheckResult(
1049 | "generateAppVersionInfoForRelease",
1050 | "--build-cache"
1051 | ) {
1052 | assertThat(task(":app:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
1053 | }
1054 |
1055 | runner.runAndCheckResult(
1056 | "clean",
1057 | "generateAppVersionInfoForRelease",
1058 | "--build-cache"
1059 | ) {
1060 | assertThat(task(":app:clean")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
1061 | assertThat(task(":app:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.FROM_CACHE)
1062 | }
1063 |
1064 | gitClient.checkoutTag(tag = "1.2.3")
1065 |
1066 | runner.runAndCheckResult(
1067 | "clean",
1068 | "generateAppVersionInfoForRelease",
1069 | "--build-cache"
1070 | ) {
1071 | assertThat(task(":app:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
1072 | }
1073 | }
1074 | }
1075 |
--------------------------------------------------------------------------------
/src/functionalTest/kotlin/io/github/reactivecircus/appversioning/tasks/PrintAppVersionInfoTest.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("FunctionName", "DuplicatedCode")
2 |
3 | package io.github.reactivecircus.appversioning.tasks
4 |
5 | import com.google.common.truth.Truth.assertThat
6 | import io.github.reactivecircus.appversioning.fixtures.AppProjectTemplate
7 | import io.github.reactivecircus.appversioning.fixtures.withFixtureRunner
8 | import io.github.reactivecircus.appversioning.internal.GitClient
9 | import org.gradle.testkit.runner.TaskOutcome
10 | import org.junit.Rule
11 | import org.junit.Test
12 | import org.junit.rules.TemporaryFolder
13 |
14 | class PrintAppVersionInfoTest {
15 |
16 | @get:Rule
17 | val fixtureDir = TemporaryFolder()
18 |
19 | @Test
20 | fun `PrintAppVersionInfo prints latest generated versionCode and versionName to the console when they are available`() {
21 | GitClient.initialize(fixtureDir.root).apply {
22 | val commitId = commit(message = "1st commit.")
23 | tag(name = "1.2.3", message = "1st tag", commitId = commitId)
24 | }
25 |
26 | val flavors = listOf("mock", "prod")
27 | val runner = withFixtureRunner(
28 | fixtureDir = fixtureDir,
29 | subprojects = listOf(AppProjectTemplate(flavors = flavors))
30 | )
31 |
32 | runner.runAndCheckResult(
33 | "generateAppVersionInfoForProdRelease"
34 | ) {
35 | assertThat(task(":app:generateAppVersionInfoForProdRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
36 | }
37 |
38 | runner.runAndCheckResult(
39 | "printAppVersionInfoForProdRelease"
40 | ) {
41 | assertThat(task(":app:printAppVersionInfoForProdRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
42 | assertThat(output).contains(
43 | """
44 | App version info generated by Android App Versioning plugin:
45 | Project: ":app"
46 | Build variant: prodRelease
47 | versionCode: 10203
48 | versionName: "1.2.3"
49 | """.trimIndent()
50 | )
51 | }
52 | }
53 |
54 | @Test
55 | fun `PrintAppVersionInfo prints warning message to the console when they are not available`() {
56 | GitClient.initialize(fixtureDir.root).apply {
57 | val commitId = commit(message = "1st commit.")
58 | tag(name = "1.2.3", message = "1st tag", commitId = commitId)
59 | }
60 |
61 | withFixtureRunner(
62 | fixtureDir = fixtureDir,
63 | subprojects = listOf(AppProjectTemplate())
64 | ).runAndCheckResult(
65 | "printAppVersionInfoForRelease"
66 | ) {
67 | assertThat(task(":app:printAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
68 | assertThat(output).contains(
69 | "No app version info (versionCode and versionName) generated by the Android App Versioning plugin for the \"release\" variant of project \":app\" is available."
70 | )
71 | }
72 | }
73 |
74 | @Test
75 | fun `PrintAppVersionInfo prints latest generate versionCode and versionName (or warning message if not available) to the console for each project with AppVersioningPlugin applied`() {
76 | GitClient.initialize(fixtureDir.root).apply {
77 | commit(message = "Initial commit.")
78 | // appA release
79 | val commitId1 = commit(message = "appA release.")
80 | tag(name = "0.1.0+appA", message = "1st tag", commitId = commitId1)
81 | // appB release
82 | val commitId2 = commit(message = "appB release.")
83 | tag(name = "1.2.3+appB", message = "2nd tag", commitId = commitId2)
84 | // appC release
85 | val commitId4 = commit(message = "appC release.")
86 | tag(name = "10.3.5+appC", message = "4th tag", commitId = commitId4)
87 | }
88 |
89 | val tagFilterAppA = "[0-9]*.[0-9]*.[0-9]*+appA"
90 | val tagFilterAppB = "[0-9]*.[0-9]*.[0-9]*+appB"
91 | val tagFilterAppC = "[0-9]*.[0-9]*.[0-9]*+appC"
92 |
93 | val extensionsAppA = """
94 | appVersioning {
95 | tagFilter.set("$tagFilterAppA")
96 | }
97 | """.trimIndent()
98 |
99 | val extensionsAppB = """
100 | appVersioning {
101 | tagFilter.set("$tagFilterAppB")
102 | }
103 | """.trimIndent()
104 |
105 | val extensionsAppC = """
106 | appVersioning {
107 | tagFilter.set("$tagFilterAppC")
108 | }
109 | """.trimIndent()
110 |
111 | val runner = withFixtureRunner(
112 | fixtureDir = fixtureDir,
113 | subprojects = listOf(
114 | AppProjectTemplate(
115 | projectName = "app-a",
116 | pluginExtension = extensionsAppA
117 | ),
118 | AppProjectTemplate(
119 | projectName = "app-b",
120 | pluginExtension = extensionsAppB
121 | ),
122 | AppProjectTemplate(
123 | projectName = "app-c",
124 | pluginExtension = extensionsAppC
125 | )
126 | )
127 | )
128 |
129 | runner.runAndCheckResult(
130 | "app-a:generateAppVersionInfoForRelease",
131 | "app-b:generateAppVersionInfoForRelease"
132 | ) {
133 | assertThat(task(":app-a:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
134 | assertThat(task(":app-b:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
135 | }
136 |
137 | runner.runAndCheckResult(
138 | "printAppVersionInfoForRelease"
139 | ) {
140 | assertThat(task(":app-a:printAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
141 | assertThat(output).contains(
142 | """
143 | App version info generated by Android App Versioning plugin:
144 | Project: ":app-a"
145 | Build variant: release
146 | versionCode: 100
147 | versionName: "0.1.0+appA"
148 | """.trimIndent()
149 | )
150 |
151 | assertThat(task(":app-b:printAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
152 | assertThat(output).contains(
153 | """
154 | App version info generated by Android App Versioning plugin:
155 | Project: ":app-b"
156 | Build variant: release
157 | versionCode: 10203
158 | versionName: "1.2.3+appB"
159 | """.trimIndent()
160 | )
161 |
162 | assertThat(task(":app-a:printAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
163 | assertThat(output).contains(
164 | "No app version info (versionCode and versionName) generated by the Android App Versioning plugin for the \"release\" variant of project \":app-c\" is available."
165 | )
166 | }
167 | }
168 |
169 | @Test
170 | fun `PrintAppVersionInfo is not incremental`() {
171 | GitClient.initialize(fixtureDir.root).apply {
172 | val commitId = commit(message = "1st commit.")
173 | tag(name = "1.2.3", message = "1st tag", commitId = commitId)
174 | }
175 |
176 | val runner = withFixtureRunner(
177 | fixtureDir = fixtureDir,
178 | subprojects = listOf(AppProjectTemplate())
179 | )
180 |
181 | runner.runAndCheckResult(
182 | "generateAppVersionInfoForRelease"
183 | ) {
184 | assertThat(task(":app:generateAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
185 | }
186 |
187 | runner.runAndCheckResult(
188 | "printAppVersionInfoForRelease"
189 | ) {
190 | assertThat(task(":app:printAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
191 | assertThat(output).contains(
192 | """
193 | App version info generated by Android App Versioning plugin:
194 | Project: ":app"
195 | Build variant: release
196 | versionCode: 10203
197 | versionName: "1.2.3"
198 | """.trimIndent()
199 | )
200 | }
201 |
202 | runner.runAndCheckResult(
203 | "printAppVersionInfoForRelease"
204 | ) {
205 | assertThat(task(":app:printAppVersionInfoForRelease")?.outcome).isEqualTo(TaskOutcome.SUCCESS)
206 | assertThat(output).contains(
207 | """
208 | App version info generated by Android App Versioning plugin:
209 | Project: ":app"
210 | Build variant: release
211 | versionCode: 10203
212 | versionName: "1.2.3"
213 | """.trimIndent()
214 | )
215 | }
216 | }
217 | }
218 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/reactivecircus/appversioning/AppVersioningExtension.kt:
--------------------------------------------------------------------------------
1 | package io.github.reactivecircus.appversioning
2 |
3 | import groovy.lang.Closure
4 | import org.gradle.api.model.ObjectFactory
5 | import org.gradle.api.provider.ProviderFactory
6 | import org.gradle.kotlin.dsl.property
7 |
8 | /**
9 | * Extension for [AppVersioningPlugin].
10 | */
11 | @Suppress("UnstableApiUsage", "unused")
12 | open class AppVersioningExtension internal constructor(objects: ObjectFactory) {
13 |
14 | /**
15 | * Whether to enable the plugin.
16 | *
17 | * Default is `true`.
18 | */
19 | val enabled = objects.property().convention(DEFAULT_ENABLED)
20 |
21 | /**
22 | * Whether to only generate version name and version code for `release` builds.
23 | *
24 | * Default is `false`.
25 | */
26 | val releaseBuildOnly = objects.property().convention(DEFAULT_RELEASE_BUILD_ONLY)
27 |
28 | /**
29 | * Whether to fetch git tags from remote when no git tags can be found locally.
30 | *
31 | * Default is `false`.
32 | */
33 | val fetchTagsWhenNoneExistsLocally = objects.property().convention(
34 | DEFAULT_FETCH_TAGS_WHEN_NONE_EXISTS_LOCALLY
35 | )
36 |
37 | /**
38 | * Git root directory used for fetching git tags.
39 | * Use this to explicitly set the git root directory when the root Gradle project is not the git root directory.
40 | */
41 | val gitRootDirectory = objects.directoryProperty().convention(null)
42 |
43 | /**
44 | * Bare Git repository directory.
45 | * Use this to explicitly set the directory of a bare git repository (e.g. `app.git`) instead of the standard `.git`.
46 | * Setting this will override the value of [gitRootDirectory] property.
47 | */
48 | val bareGitRepoDirectory = objects.directoryProperty().convention(null)
49 |
50 | /**
51 | * Custom glob pattern for matching git tags.
52 | */
53 | val tagFilter = objects.property().convention(null)
54 |
55 | /**
56 | * Provides a custom rule for generating versionCode by implementing a [GitTag], [ProviderFactory], [VariantInfo] -> Int lambda.
57 | * [GitTag] is generated from latest git tag lazily by the plugin during task execution.
58 | * [ProviderFactory] can be used for fetching environment variables, Gradle and system properties.
59 | * [VariantInfo] can be used to customize version code based on the build variants (product flavors and build types).
60 | *
61 | * By default the plugin attempts to generate the versionCode by converting a SemVer compliant tag to an integer
62 | * using positional notation: versionCode = MAJOR * 10000 + MINOR * 100 + PATCH
63 | *
64 | * If your tags don't follow semantic versioning, you don't like the default formula used to convert a SemVer tag to versionCode,
65 | * or if you want to fully customize how the versionCode is generated, you can implement this lambda to provide your own versionCode generation rule.
66 | *
67 | * Note that you can use the `gitTag.toSemVer()` extension (or `SemVer.fromGitTag(gitTag)` if you use groovy) to get a type-safe `SemVer` model
68 | * if your custom rule is still based on semantic versioning.
69 | */
70 | fun overrideVersionCode(customizer: VersionCodeCustomizer) {
71 | kotlinVersionCodeCustomizer.set(customizer)
72 | }
73 |
74 | /**
75 | * Same as `overrideVersionCode(customizer: VersionCodeCustomizer)` above but for groovy support.
76 | */
77 | fun overrideVersionCode(customizer: Closure) {
78 | groovyVersionCodeCustomizer.set(customizer.dehydrate())
79 | }
80 |
81 | /**
82 | * Provides a custom rule for generating versionName by implementing a [GitTag], [ProviderFactory], [VariantInfo] -> String lambda.
83 | * [GitTag] is generated from latest git tag lazily by the plugin during task execution.
84 | * [ProviderFactory] can be used for fetching environment variables, Gradle and system properties.
85 | * [VariantInfo] can be used to customize version name based on the build variants (product flavors and build types).
86 | *
87 | * This is useful if you want to fully customize how the versionName is generated.
88 | * If not specified, versionName will be the name of the latest git tag.
89 | */
90 | fun overrideVersionName(customizer: VersionNameCustomizer) {
91 | kotlinVersionNameCustomizer.set(customizer)
92 | }
93 |
94 | /**
95 | * Same as `overrideVersionName(customizer: VersionNameCustomizer)` above but for groovy support.
96 | */
97 | fun overrideVersionName(customizer: Closure) {
98 | groovyVersionNameCustomizer.set(customizer.dehydrate())
99 | }
100 |
101 | /**
102 | * A lambda (Kotlin function type) for specifying a custom rule for generating versionCode.
103 | */
104 | internal val kotlinVersionCodeCustomizer = objects.property()
105 |
106 | /**
107 | * A lambda (Groovy closure) for specifying a custom rule for generating versionCode.
108 | */
109 | internal val groovyVersionCodeCustomizer = objects.property>()
110 |
111 | /**
112 | * A lambda (Kotlin function type) for specifying a custom rule for generating versionName.
113 | */
114 | internal val kotlinVersionNameCustomizer = objects.property()
115 |
116 | /**
117 | * A lambda (Groovy closure) for specifying a custom rule for generating versionName.
118 | */
119 | internal val groovyVersionNameCustomizer = objects.property>()
120 |
121 | companion object {
122 | internal const val DEFAULT_ENABLED = true
123 | internal const val DEFAULT_RELEASE_BUILD_ONLY = false
124 | internal const val DEFAULT_FETCH_TAGS_WHEN_NONE_EXISTS_LOCALLY = false
125 | }
126 | }
127 |
128 | internal typealias VersionCodeCustomizer = (GitTag, ProviderFactory, VariantInfo) -> Int
129 | internal typealias VersionNameCustomizer = (GitTag, ProviderFactory, VariantInfo) -> String
130 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/reactivecircus/appversioning/AppVersioningPlugin.kt:
--------------------------------------------------------------------------------
1 | package io.github.reactivecircus.appversioning
2 |
3 | import com.android.build.api.variant.ApplicationAndroidComponentsExtension
4 | import com.android.build.api.variant.ApplicationVariant
5 | import com.android.build.gradle.AppPlugin
6 | import io.github.reactivecircus.appversioning.tasks.GenerateAppVersionInfo
7 | import io.github.reactivecircus.appversioning.tasks.PrintAppVersionInfo
8 | import org.gradle.api.Plugin
9 | import org.gradle.api.Project
10 | import org.gradle.api.tasks.TaskProvider
11 | import org.gradle.kotlin.dsl.getByType
12 | import org.gradle.kotlin.dsl.withType
13 | import org.gradle.language.nativeplatform.internal.BuildType
14 | import java.io.File
15 | import java.util.Locale
16 | import java.util.concurrent.atomic.AtomicBoolean
17 |
18 | /**
19 | * A plugin that generates and sets the version code and version name for an Android app using the latest git tag.
20 | */
21 | class AppVersioningPlugin : Plugin {
22 |
23 | override fun apply(project: Project) {
24 | val androidAppPluginApplied = AtomicBoolean(false)
25 | val pluginDisabled = AtomicBoolean(false)
26 | val appVersioningExtension = project.extensions.create("appVersioning", AppVersioningExtension::class.java)
27 | project.plugins.withType {
28 | androidAppPluginApplied.set(true)
29 | val extension = project.extensions.getByType()
30 | extension.onVariants(selector = extension.selector().all()) { variant ->
31 | if (pluginDisabled.get()) return@onVariants
32 | if (!appVersioningExtension.enabled.get()) {
33 | project.logger.quiet("Android App Versioning plugin is disabled.")
34 | pluginDisabled.set(true)
35 | return@onVariants
36 | }
37 | if (!appVersioningExtension.releaseBuildOnly.get() || variant.buildType == BuildType.RELEASE.name) {
38 | val generateAppVersionInfo = project.registerGenerateAppVersionInfoTask(
39 | variant = variant,
40 | extension = appVersioningExtension
41 | )
42 |
43 | val generatedVersionCode = generateAppVersionInfo.flatMap {
44 | it.versionCodeFile.map { file -> file.asFile.readText().trim().toInt() }
45 | }
46 | val generatedVersionName = generateAppVersionInfo.flatMap {
47 | it.versionNameFile.map { file -> file.asFile.readText().trim() }
48 | }
49 |
50 | project.registerPrintAppVersionInfoTask(variantName = variant.name)
51 |
52 | variant.outputs.forEach { output ->
53 | output.versionCode.set(generatedVersionCode)
54 | output.versionName.set(generatedVersionName)
55 | }
56 | }
57 | }
58 | }
59 |
60 | project.afterEvaluate {
61 | check(androidAppPluginApplied.get()) {
62 | "Android App Versioning plugin should only be applied to an Android Application project but ${project.displayName} doesn't have the 'com.android.application' plugin applied."
63 | }
64 | }
65 | }
66 |
67 | private fun Project.registerGenerateAppVersionInfoTask(
68 | variant: ApplicationVariant,
69 | extension: AppVersioningExtension,
70 | ): TaskProvider = tasks.register(
71 | "${GenerateAppVersionInfo.TASK_NAME_PREFIX}For${variant.name.capitalize()}",
72 | GenerateAppVersionInfo::class.java
73 | ) {
74 | group = APP_VERSIONING_TASK_GROUP
75 | description = "${GenerateAppVersionInfo.TASK_DESCRIPTION_PREFIX} for the ${variant.name} variant."
76 | gitRefsDirectory.set(findGitRefsDirectory(extension))
77 | gitHead.set(findGitHeadFile(extension))
78 | rootProjectDirectory.set(project.rootProject.rootDir)
79 | rootProjectDisplayName.set(project.rootProject.displayName)
80 | fetchTagsWhenNoneExistsLocally.set(extension.fetchTagsWhenNoneExistsLocally)
81 | tagFilter.set(extension.tagFilter)
82 | kotlinVersionCodeCustomizer.set(extension.kotlinVersionCodeCustomizer)
83 | kotlinVersionNameCustomizer.set(extension.kotlinVersionNameCustomizer)
84 | groovyVersionCodeCustomizer.set(extension.groovyVersionCodeCustomizer)
85 | groovyVersionNameCustomizer.set(extension.groovyVersionNameCustomizer)
86 | versionCodeFile.set(
87 | layout.buildDirectory.file("$APP_VERSIONING_TASK_OUTPUT_DIR/${variant.name}/$VERSION_CODE_RESULT_FILE")
88 | )
89 | versionNameFile.set(
90 | layout.buildDirectory.file("$APP_VERSIONING_TASK_OUTPUT_DIR/${variant.name}/$VERSION_NAME_RESULT_FILE")
91 | )
92 | variantInfo.set(
93 | VariantInfo(
94 | buildType = variant.buildType,
95 | flavorName = variant.flavorName.orEmpty(),
96 | variantName = variant.name
97 | )
98 | )
99 | }
100 |
101 | private fun Project.registerPrintAppVersionInfoTask(
102 | variantName: String
103 | ): TaskProvider = tasks.register(
104 | "${PrintAppVersionInfo.TASK_NAME_PREFIX}For${variantName.capitalize()}",
105 | PrintAppVersionInfo::class.java
106 | ) {
107 | group = APP_VERSIONING_TASK_GROUP
108 | description = "${PrintAppVersionInfo.TASK_DESCRIPTION_PREFIX} for the $variantName variant."
109 |
110 | versionCodeFile.set(
111 | layout.buildDirectory.file("$APP_VERSIONING_TASK_OUTPUT_DIR/$variantName/$VERSION_CODE_RESULT_FILE")
112 | .flatMap { provider { if (it.asFile.exists()) it else null } }
113 | )
114 | versionNameFile.set(
115 | layout.buildDirectory.file("$APP_VERSIONING_TASK_OUTPUT_DIR/$variantName/$VERSION_NAME_RESULT_FILE")
116 | .flatMap { provider { if (it.asFile.exists()) it else null } }
117 | )
118 | projectName.set(project.name)
119 | buildVariantName.set(variantName)
120 | }
121 |
122 | private fun Project.findGitRefsDirectory(extension: AppVersioningExtension): File? {
123 | return when {
124 | extension.bareGitRepoDirectory.isPresent -> extension.bareGitRepoDirectory.let { bareGitRepoDirectory ->
125 | bareGitRepoDirectory.asFile.orNull?.resolve(REFS_DIRECTORY)?.takeIf { it.exists() }
126 | }
127 | extension.gitRootDirectory.isPresent -> extension.gitRootDirectory.let { gitRootDirectory ->
128 | gitRootDirectory.asFile.orNull?.resolve(STANDARD_GIT_REFS_DIRECTORY)?.takeIf { it.exists() }
129 | }
130 | else -> project.rootProject.file(STANDARD_GIT_REFS_DIRECTORY).takeIf { it.exists() }
131 | }
132 | }
133 |
134 | private fun Project.findGitHeadFile(extension: AppVersioningExtension): File? {
135 | return when {
136 | extension.bareGitRepoDirectory.isPresent -> extension.bareGitRepoDirectory.let { bareGitRepoDirectory ->
137 | bareGitRepoDirectory.asFile.orNull?.resolve(HEAD_FILE)?.takeIf { it.exists() }
138 | }
139 | extension.gitRootDirectory.isPresent -> extension.gitRootDirectory.let { gitRootDirectory ->
140 | gitRootDirectory.asFile.orNull?.resolve(STANDARD_GIT_HEAD_FILE)?.takeIf { it.exists() }
141 | }
142 | else -> project.rootProject.file(STANDARD_GIT_HEAD_FILE).takeIf { it.exists() }
143 | }
144 | }
145 |
146 | private fun String.capitalize(): String {
147 | return replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() }
148 | }
149 |
150 | companion object
151 | }
152 |
153 | private const val APP_VERSIONING_TASK_GROUP = "versioning"
154 | private const val APP_VERSIONING_TASK_OUTPUT_DIR = "outputs/app_versioning"
155 | private const val REFS_DIRECTORY = "refs"
156 | private const val HEAD_FILE = "HEAD"
157 | private const val STANDARD_GIT_REFS_DIRECTORY = ".git/$REFS_DIRECTORY"
158 | private const val STANDARD_GIT_HEAD_FILE = ".git/$HEAD_FILE"
159 | private const val VERSION_CODE_RESULT_FILE = "version_code.txt"
160 | private const val VERSION_NAME_RESULT_FILE = "version_name.txt"
161 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/reactivecircus/appversioning/GitTag.kt:
--------------------------------------------------------------------------------
1 | package io.github.reactivecircus.appversioning
2 |
3 | /**
4 | * Type-safe representation of a git tag.
5 | */
6 | data class GitTag(
7 | val rawTagName: String,
8 | val commitsSinceLatestTag: Int,
9 | val commitHash: String
10 | ) {
11 | override fun toString() = rawTagName
12 | }
13 |
14 | /**
15 | * Parses the output of `git describe --tags --long` into a [GitTag].
16 | */
17 | internal fun String.toGitTag(): GitTag {
18 | val result = requireNotNull("(.*)-(\\d+)-g([0-9,a-f]{7,40})\$".toRegex().matchEntire(this)) {
19 | "$this is not a valid git tag."
20 | }
21 | val (rawTagName, commitsSinceLatestTag, commitHash) = result.destructured
22 | return GitTag(rawTagName, commitsSinceLatestTag.toInt(), commitHash)
23 | }
24 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/reactivecircus/appversioning/SemVer.kt:
--------------------------------------------------------------------------------
1 | package io.github.reactivecircus.appversioning
2 |
3 | import kotlin.math.pow
4 |
5 | /**
6 | * Type-safe representation of a version number that follows [Semantic Versioning 2.0.0](https://semver.org/#semantic-versioning-200).
7 | */
8 | data class SemVer(
9 | val major: Int,
10 | val minor: Int,
11 | val patch: Int,
12 | val preRelease: String? = null,
13 | val buildMetadata: String? = null
14 | ) {
15 | override fun toString() = buildString {
16 | append("$major.$minor.$patch")
17 | if (preRelease != null) {
18 | append("-$preRelease")
19 | }
20 | if (buildMetadata != null) {
21 | append("+$buildMetadata")
22 | }
23 | }
24 |
25 | companion object {
26 | @JvmStatic
27 | @JvmOverloads
28 | fun fromGitTag(gitTag: GitTag, allowPrefixV: Boolean = true): SemVer = gitTag.toSemVer(allowPrefixV)
29 | }
30 | }
31 |
32 | /**
33 | * Generates an Integer representation of the [SemVer] using positional notation.
34 | * @param maxDigitsPerComponent number of digits allocated to the MINOR and PATCH components.
35 | * @throws [IllegalArgumentException] when MINOR or PATCH version is out of range allowed by [maxDigitsPerComponent], or when the result is above [Int.MAX_VALUE].
36 | */
37 | @Suppress("MagicNumber")
38 | internal fun SemVer.toInt(maxDigitsPerComponent: Int): Int {
39 | require(maxDigitsPerComponent > 0)
40 | require(patch < 10.0.pow(maxDigitsPerComponent))
41 | require(minor < 10.0.pow(maxDigitsPerComponent))
42 | val result = major * 10.0.pow(maxDigitsPerComponent * 2) + minor * 10.0.pow(maxDigitsPerComponent) + patch
43 | require(result <= Int.MAX_VALUE)
44 | return result.toInt()
45 | }
46 |
47 | /**
48 | * Tries to create a [SemVer] from a [GitTag] by parsing its `rawTagName` field.
49 | * @param allowPrefixV whether prefixing a semantic version with a “v” is allowed.
50 | * @throws [IllegalArgumentException] when the `rawTagName` of the [GitTag] is not a valid [SemVer].
51 | */
52 | @Suppress("DestructuringDeclarationWithTooManyEntries")
53 | fun GitTag.toSemVer(allowPrefixV: Boolean = true): SemVer {
54 | val result = requireNotNull(SEM_VER_REGEX.toRegex().matchEntire(rawTagName)) {
55 | "\"$rawTagName\" is not a valid SemVer."
56 | }
57 | val (prefixV, major, minor, patch, preRelease, buildMetadata) = result.destructured
58 | require(prefixV.isEmpty() || allowPrefixV) {
59 | "\"$rawTagName\" is not a valid SemVer as prefix \"v\" is not allowed unless `allowPrefixV` is set to true."
60 | }
61 | return SemVer(
62 | major = major.toInt(),
63 | minor = minor.toInt(),
64 | patch = patch.toInt(),
65 | preRelease = preRelease.ifEmpty { null },
66 | buildMetadata = buildMetadata.ifEmpty { null }
67 | )
68 | }
69 |
70 | /**
71 | * SemVer regex from https://semver.org/#is-there-a-suggested-regular-expression-regex-to-check-a-semver-string
72 | * allowing an optional "v" prefix.
73 | */
74 | private const val SEM_VER_REGEX =
75 | "^(v)?(0|[1-9]\\d*)\\.(0|[1-9]\\d*)\\.(0|[1-9]\\d*)(?:-((?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*)(?:\\.(?:0|[1-9]\\d*|\\d*[a-zA-Z-][0-9a-zA-Z-]*))*))?(?:\\+([0-9a-zA-Z-]+(?:\\.[0-9a-zA-Z-]+)*))?\$"
76 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/reactivecircus/appversioning/VariantInfo.kt:
--------------------------------------------------------------------------------
1 | package io.github.reactivecircus.appversioning
2 |
3 | import org.gradle.language.nativeplatform.internal.BuildType
4 | import java.io.Serializable
5 |
6 | class VariantInfo(
7 | val buildType: String?,
8 | val flavorName: String,
9 | val variantName: String
10 | ) : Serializable {
11 | val isDebugBuild: Boolean get() = buildType == BuildType.DEBUG.name
12 | val isReleaseBuild: Boolean get() = buildType == BuildType.RELEASE.name
13 |
14 | companion object {
15 | private const val serialVersionUID = 1L
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/reactivecircus/appversioning/internal/GitClient.kt:
--------------------------------------------------------------------------------
1 | package io.github.reactivecircus.appversioning.internal
2 |
3 | import java.io.File
4 | import java.util.concurrent.TimeUnit
5 |
6 | class GitClient private constructor(private val projectDir: File) {
7 |
8 | fun listLocalTags(): List {
9 | return listOf("git", "tag", "--list").execute(projectDir).lines()
10 | }
11 |
12 | fun fetchRemoteTags() {
13 | listOf("git", "fetch", "--tags").execute(projectDir)
14 | }
15 |
16 | fun describeLatestTag(pattern: String? = null): String? {
17 | val command = buildList {
18 | add("git")
19 | add("describe")
20 | if (pattern != null) {
21 | add("--match")
22 | add(pattern)
23 | }
24 | add("--tags")
25 | add("--long")
26 | }
27 | return runCatching { command.execute(projectDir) }.getOrNull()
28 | }
29 |
30 | fun commit(message: String, allowEmpty: Boolean = true): CommitId {
31 | val commitCommand = buildList {
32 | add("git")
33 | add("commit")
34 | if (allowEmpty) {
35 | add("--allow-empty")
36 | }
37 | add("-m")
38 | add(message)
39 | }
40 | commitCommand.execute(projectDir)
41 |
42 | val getCommitIdCommand = listOf("git", "rev-parse", "--short", "HEAD")
43 | return CommitId(getCommitIdCommand.execute(projectDir))
44 | }
45 |
46 | fun tag(name: String, message: String, commitId: CommitId? = null) {
47 | val command = buildList {
48 | add("git")
49 | add("tag")
50 | add("-a")
51 | add(name)
52 | if (commitId != null) {
53 | add(commitId.value)
54 | }
55 | add("-m")
56 | add(message)
57 | }
58 | command.execute(projectDir)
59 | }
60 |
61 | fun checkoutTag(tag: String) {
62 | val commitCommand = buildList {
63 | add("git")
64 | add("checkout")
65 | add(tag)
66 | }
67 | commitCommand.execute(projectDir)
68 | }
69 |
70 | companion object {
71 |
72 | fun initialize(projectDir: File): GitClient {
73 | return GitClient(projectDir).apply {
74 | listOf("git", "init").execute(projectDir)
75 | }
76 | }
77 |
78 | fun open(projectDir: File): GitClient {
79 | require(projectDir.isInValidGitRepo)
80 | return GitClient(projectDir)
81 | }
82 | }
83 | }
84 |
85 | @JvmInline
86 | value class CommitId(val value: String)
87 |
88 | private val File.isInValidGitRepo: Boolean
89 | get() = runCatching {
90 | listOf("git", "rev-parse", "--is-inside-work-tree").execute(this).toBoolean()
91 | }.getOrDefault(false)
92 |
93 | @Suppress("UseCheckOrError")
94 | private fun List.execute(workingDir: File, timeoutInSeconds: Long = DEFAULT_COMMAND_TIMEOUT_SECONDS): String {
95 | val process = ProcessBuilder(this)
96 | .directory(workingDir)
97 | .start()
98 | if (!process.waitFor(timeoutInSeconds, TimeUnit.SECONDS)) {
99 | process.destroy()
100 | throw IllegalStateException("Execution timeout: $this")
101 | }
102 | if (process.exitValue() != 0) {
103 | throw IllegalStateException("Execution failed with exit code: ${process.exitValue()}: $this")
104 | }
105 | return process.inputStream.bufferedReader().readText().trim()
106 | }
107 |
108 | private const val DEFAULT_COMMAND_TIMEOUT_SECONDS = 5L
109 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/reactivecircus/appversioning/tasks/GenerateAppVersionInfo.kt:
--------------------------------------------------------------------------------
1 | @file:Suppress("UnstableApiUsage", "DuplicatedCode")
2 |
3 | package io.github.reactivecircus.appversioning.tasks
4 |
5 | import groovy.lang.Closure
6 | import io.github.reactivecircus.appversioning.GitTag
7 | import io.github.reactivecircus.appversioning.VariantInfo
8 | import io.github.reactivecircus.appversioning.VersionCodeCustomizer
9 | import io.github.reactivecircus.appversioning.VersionNameCustomizer
10 | import io.github.reactivecircus.appversioning.internal.GitClient
11 | import io.github.reactivecircus.appversioning.toGitTag
12 | import io.github.reactivecircus.appversioning.toInt
13 | import io.github.reactivecircus.appversioning.toSemVer
14 | import org.gradle.api.DefaultTask
15 | import org.gradle.api.file.DirectoryProperty
16 | import org.gradle.api.file.RegularFileProperty
17 | import org.gradle.api.logging.Logging
18 | import org.gradle.api.provider.Property
19 | import org.gradle.api.provider.ProviderFactory
20 | import org.gradle.api.tasks.CacheableTask
21 | import org.gradle.api.tasks.IgnoreEmptyDirectories
22 | import org.gradle.api.tasks.Input
23 | import org.gradle.api.tasks.InputDirectory
24 | import org.gradle.api.tasks.InputFile
25 | import org.gradle.api.tasks.Internal
26 | import org.gradle.api.tasks.Optional
27 | import org.gradle.api.tasks.OutputFile
28 | import org.gradle.api.tasks.PathSensitive
29 | import org.gradle.api.tasks.PathSensitivity
30 | import org.gradle.api.tasks.TaskAction
31 | import org.gradle.work.NormalizeLineEndings
32 | import org.gradle.workers.WorkAction
33 | import org.gradle.workers.WorkParameters
34 | import org.gradle.workers.WorkerExecutor
35 | import javax.inject.Inject
36 |
37 | /**
38 | * Generates app's versionCode and versionName based on git tags.
39 | */
40 | @CacheableTask
41 | abstract class GenerateAppVersionInfo @Inject constructor(
42 | private val workerExecutor: WorkerExecutor
43 | ) : DefaultTask() {
44 |
45 | @get:Optional
46 | @get:InputDirectory
47 | @get:PathSensitive(PathSensitivity.RELATIVE)
48 | @get:IgnoreEmptyDirectories
49 | @get:NormalizeLineEndings
50 | abstract val gitRefsDirectory: DirectoryProperty
51 |
52 | @get:Optional
53 | @get:InputFile
54 | @get:PathSensitive(PathSensitivity.RELATIVE)
55 | @get:NormalizeLineEndings
56 | abstract val gitHead: RegularFileProperty
57 |
58 | @get:Internal
59 | abstract val rootProjectDirectory: DirectoryProperty
60 |
61 | @get:Internal
62 | abstract val rootProjectDisplayName: Property
63 |
64 | @get:Input
65 | abstract val fetchTagsWhenNoneExistsLocally: Property
66 |
67 | @get:Optional
68 | @get:Input
69 | abstract val tagFilter: Property
70 |
71 | @get:Optional
72 | @get:Input
73 | abstract val kotlinVersionCodeCustomizer: Property
74 |
75 | @get:Optional
76 | @get:Input
77 | abstract val kotlinVersionNameCustomizer: Property
78 |
79 | @get:Optional
80 | @get:Input
81 | abstract val groovyVersionCodeCustomizer: Property>
82 |
83 | @get:Optional
84 | @get:Input
85 | abstract val groovyVersionNameCustomizer: Property>
86 |
87 | @get:OutputFile
88 | abstract val versionCodeFile: RegularFileProperty
89 |
90 | @get:OutputFile
91 | abstract val versionNameFile: RegularFileProperty
92 |
93 | @get:Input
94 | abstract val variantInfo: Property
95 |
96 | @TaskAction
97 | fun generate() {
98 | workerExecutor.noIsolation().submit(GenerateAppVersionInfoWorkAction::class.java) {
99 | gitRefsDirectory.set(this@GenerateAppVersionInfo.gitRefsDirectory)
100 | gitHead.set(this@GenerateAppVersionInfo.gitHead)
101 | rootProjectDirectory.set(this@GenerateAppVersionInfo.rootProjectDirectory)
102 | rootProjectDisplayName.set(this@GenerateAppVersionInfo.rootProjectDisplayName)
103 | fetchTagsWhenNoneExistsLocally.set(this@GenerateAppVersionInfo.fetchTagsWhenNoneExistsLocally)
104 | tagFilter.set(this@GenerateAppVersionInfo.tagFilter)
105 | kotlinVersionCodeCustomizer.set(this@GenerateAppVersionInfo.kotlinVersionCodeCustomizer)
106 | kotlinVersionNameCustomizer.set(this@GenerateAppVersionInfo.kotlinVersionNameCustomizer)
107 | groovyVersionCodeCustomizer.set(this@GenerateAppVersionInfo.groovyVersionCodeCustomizer)
108 | groovyVersionNameCustomizer.set(this@GenerateAppVersionInfo.groovyVersionNameCustomizer)
109 | versionCodeFile.set(this@GenerateAppVersionInfo.versionCodeFile)
110 | versionNameFile.set(this@GenerateAppVersionInfo.versionNameFile)
111 | variantInfo.set(this@GenerateAppVersionInfo.variantInfo)
112 | }
113 | }
114 |
115 | companion object {
116 | const val TASK_NAME_PREFIX = "generateAppVersionInfo"
117 | const val TASK_DESCRIPTION_PREFIX = "Generates app's versionCode and versionName based on git tags"
118 | const val VERSION_CODE_FALLBACK = 0
119 | const val VERSION_NAME_FALLBACK = ""
120 | }
121 | }
122 |
123 | private interface GenerateAppVersionInfoWorkParameters : WorkParameters {
124 | val gitRefsDirectory: DirectoryProperty
125 | val gitHead: RegularFileProperty
126 | val rootProjectDirectory: DirectoryProperty
127 | val rootProjectDisplayName: Property
128 | val fetchTagsWhenNoneExistsLocally: Property
129 | val tagFilter: Property
130 | val kotlinVersionCodeCustomizer: Property
131 | val kotlinVersionNameCustomizer: Property
132 | val groovyVersionCodeCustomizer: Property>
133 | val groovyVersionNameCustomizer: Property>
134 | val versionCodeFile: RegularFileProperty
135 | val versionNameFile: RegularFileProperty
136 | val variantInfo: Property
137 | }
138 |
139 | private abstract class GenerateAppVersionInfoWorkAction @Inject constructor(
140 | private val providers: ProviderFactory
141 | ) : WorkAction {
142 |
143 | private val logger = Logging.getLogger(GenerateAppVersionInfo::class.java)
144 |
145 | @Suppress("LongMethod")
146 | override fun execute() {
147 | val gitRefsDirectory = parameters.gitRefsDirectory
148 | val gitHead = parameters.gitHead
149 | val rootProjectDirectory = parameters.rootProjectDirectory
150 | val rootProjectDisplayName = parameters.rootProjectDisplayName
151 | val fetchTagsWhenNoneExistsLocally = parameters.fetchTagsWhenNoneExistsLocally
152 | val tagFilter = parameters.tagFilter
153 | val kotlinVersionCodeCustomizer = parameters.kotlinVersionCodeCustomizer
154 | val kotlinVersionNameCustomizer = parameters.kotlinVersionNameCustomizer
155 | val groovyVersionCodeCustomizer = parameters.groovyVersionCodeCustomizer
156 | val groovyVersionNameCustomizer = parameters.groovyVersionNameCustomizer
157 | val versionCodeFile = parameters.versionCodeFile
158 | val versionNameFile = parameters.versionNameFile
159 | val variantInfo = parameters.variantInfo
160 |
161 | check(gitRefsDirectory.isPresent && gitHead.isPresent) {
162 | "Android App Versioning Gradle Plugin works with git tags but ${rootProjectDisplayName.get()} is not a git root directory, and a valid gitRootDirectory is not provided."
163 | }
164 |
165 | val gitClient = GitClient.open(rootProjectDirectory.get().asFile)
166 |
167 | val gitTag: GitTag = gitClient.describeLatestTag(tagFilter.orNull)?.toGitTag() ?: if (fetchTagsWhenNoneExistsLocally.get()) {
168 | val tagsList = gitClient.listLocalTags()
169 | if (tagsList.isEmpty()) {
170 | logger.warn("No git tags found. Fetching tags from remote.")
171 | gitClient.fetchRemoteTags()
172 | }
173 | gitClient.describeLatestTag(tagFilter.orNull)?.toGitTag()
174 | } else {
175 | null
176 | } ?: run {
177 | logger.warn(
178 | """
179 | No git tags found. Falling back to version code ${GenerateAppVersionInfo.VERSION_CODE_FALLBACK} and version name "${GenerateAppVersionInfo.VERSION_NAME_FALLBACK}".
180 | If you want to fallback to the versionCode and versionName set via the DSL or manifest, or stop generating versionCode and versionName from Git tags:
181 | appVersioning {
182 | enabled.set(false)
183 | }
184 | """.trimIndent()
185 | )
186 | versionCodeFile.get().asFile.writeText(GenerateAppVersionInfo.VERSION_CODE_FALLBACK.toString())
187 | versionNameFile.get().asFile.writeText(GenerateAppVersionInfo.VERSION_NAME_FALLBACK)
188 | return
189 | }
190 |
191 | val versionCode: Int = generateVersionCodeFromGitTag(
192 | variantInfo,
193 | gitTag,
194 | kotlinVersionCodeCustomizer,
195 | groovyVersionCodeCustomizer
196 | )
197 | versionCodeFile.get().asFile.writeText(versionCode.toString())
198 | logger.quiet("Generated app version code: $versionCode.")
199 |
200 | val versionName: String = when {
201 | kotlinVersionNameCustomizer.isPresent -> kotlinVersionNameCustomizer.get().invoke(
202 | gitTag,
203 | providers,
204 | variantInfo.get()
205 | )
206 | groovyVersionNameCustomizer.isPresent -> groovyVersionNameCustomizer.get().call(
207 | gitTag,
208 | providers,
209 | variantInfo.get()
210 | )
211 | else -> gitTag.toString()
212 | }
213 | versionNameFile.get().asFile.writeText(versionName)
214 | logger.quiet("Generated app version name: \"$versionName\".")
215 | }
216 |
217 | private fun generateVersionCodeFromGitTag(
218 | variantInfo: Property,
219 | gitTag: GitTag,
220 | kotlinVersionCodeCustomizer: Property,
221 | groovyVersionCodeCustomizer: Property>
222 | ): Int = when {
223 | kotlinVersionCodeCustomizer.isPresent -> kotlinVersionCodeCustomizer.get().invoke(
224 | gitTag,
225 | providers,
226 | variantInfo.get()
227 | )
228 | groovyVersionCodeCustomizer.isPresent -> groovyVersionCodeCustomizer.get().call(
229 | gitTag,
230 | providers,
231 | variantInfo.get()
232 | )
233 | else -> {
234 | // no custom rule for generating versionCode has been provided, attempt calculation based on SemVer
235 | val semVer = runCatching {
236 | gitTag.toSemVer()
237 | }.getOrNull()
238 | checkNotNull(semVer) {
239 | """
240 | Could not generate versionCode as "${gitTag.rawTagName}" does not follow semantic versioning.
241 | Please either ensure latest git tag follows semantic versioning, or provide a custom rule for generating versionCode using the `overrideVersionCode` lambda.
242 | """.trimIndent()
243 | }
244 | runCatching {
245 | semVer.toInt(maxDigitsPerComponent = MAX_DIGITS_PER_SEM_VER_COMPONENT)
246 | }.getOrElse {
247 | Int.MAX_VALUE
248 | throw IllegalStateException(
249 | """
250 | Could not generate versionCode from "${gitTag.rawTagName}" as the SemVer cannot be represented as an Integer.
251 | This is usually because MAJOR or MINOR version is greater than 99, as by default maximum of 2 digits is allowed for MINOR and PATCH components of a SemVer tag.
252 | Another reason might be that the overall positional notation of the SemVer (MAJOR * 10000 + MINOR * 100 + PATCH) is greater than the maximum value of an integer (2147483647).
253 | As a workaround you can provide a custom rule for generating versionCode using the `overrideVersionCode` lambda.
254 | """.trimIndent()
255 | )
256 | }
257 | }
258 | }
259 |
260 | companion object {
261 | private const val MAX_DIGITS_PER_SEM_VER_COMPONENT = 2
262 | }
263 | }
264 |
--------------------------------------------------------------------------------
/src/main/kotlin/io/github/reactivecircus/appversioning/tasks/PrintAppVersionInfo.kt:
--------------------------------------------------------------------------------
1 | package io.github.reactivecircus.appversioning.tasks
2 |
3 | import org.gradle.api.DefaultTask
4 | import org.gradle.api.file.RegularFileProperty
5 | import org.gradle.api.provider.Property
6 | import org.gradle.api.tasks.Internal
7 | import org.gradle.api.tasks.TaskAction
8 |
9 | /**
10 | * Prints the latest versionCode and versionName generated by the Android App Versioning plugin to the console,
11 | * or a warning message if the versionCode and versionName generated by this plugin are not available.
12 | * Note that these might not be the final versionCode and versionName used in the merged manifest file in the APK.
13 | */
14 | abstract class PrintAppVersionInfo : DefaultTask() {
15 |
16 | @get:Internal
17 | abstract val versionCodeFile: RegularFileProperty
18 |
19 | @get:Internal
20 | abstract val versionNameFile: RegularFileProperty
21 |
22 | @get:Internal
23 | abstract val projectName: Property
24 |
25 | @get:Internal
26 | abstract val buildVariantName: Property
27 |
28 | @TaskAction
29 | fun print() {
30 | if (versionCodeFile.isPresent && versionNameFile.isPresent) {
31 | logger.quiet(
32 | """
33 | App version info generated by Android App Versioning plugin:
34 | Project: ":${projectName.get()}"
35 | Build variant: ${buildVariantName.get()}
36 | versionCode: ${versionCodeFile.get().asFile.readText().trim().toInt()}
37 | versionName: "${versionNameFile.get().asFile.readText().trim()}"
38 | """.trimIndent()
39 | )
40 | } else {
41 | logger.warn(
42 | "No app version info (versionCode and versionName) generated by the Android App Versioning plugin for the \"${buildVariantName.get()}\" variant of project \":${projectName.get()}\" is available."
43 | )
44 | }
45 | }
46 |
47 | companion object {
48 | const val TASK_NAME_PREFIX = "printAppVersionInfo"
49 | const val TASK_DESCRIPTION_PREFIX = "Prints the versionCode and versionName generated by Android App Versioning plugin (if available) to the console"
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/src/test/kotlin/io/github/reactivecircus/appversioning/GitTagTest.kt:
--------------------------------------------------------------------------------
1 | package io.github.reactivecircus.appversioning
2 |
3 | import com.google.common.truth.Truth.assertThat
4 | import org.gradle.internal.impldep.org.junit.Assert.assertThrows
5 | import org.junit.Test
6 |
7 | class GitTagTest {
8 |
9 | @Test
10 | fun `valid tag description can be converted to a GitTag`() {
11 | assertThat("0.1.0-3-g9c28ad3".toGitTag())
12 | .isEqualTo(
13 | GitTag(
14 | rawTagName = "0.1.0",
15 | commitsSinceLatestTag = 3,
16 | commitHash = "9c28ad3"
17 | )
18 | )
19 | assertThat("0.1.0-0-g9c28ad3".toGitTag())
20 | .isEqualTo(
21 | GitTag(
22 | rawTagName = "0.1.0",
23 | commitsSinceLatestTag = 0,
24 | commitHash = "9c28ad3"
25 | )
26 | )
27 | assertThat("1.0-20-g36e7453".toGitTag())
28 | .isEqualTo(
29 | GitTag(
30 | rawTagName = "1.0",
31 | commitsSinceLatestTag = 20,
32 | commitHash = "36e7453"
33 | )
34 | )
35 | assertThat("1.0.0-alpha03-20-g36e7453".toGitTag())
36 | .isEqualTo(
37 | GitTag(
38 | rawTagName = "1.0.0-alpha03",
39 | commitsSinceLatestTag = 20,
40 | commitHash = "36e7453"
41 | )
42 | )
43 | assertThat("1.0.0-alpha03-20-g36e7453".toGitTag())
44 | .isEqualTo(
45 | GitTag(
46 | rawTagName = "1.0.0-alpha03",
47 | commitsSinceLatestTag = 20,
48 | commitHash = "36e7453"
49 | )
50 | )
51 | assertThat("1.0.0-alpha+001-20-g36e7453".toGitTag())
52 | .isEqualTo(
53 | GitTag(
54 | rawTagName = "1.0.0-alpha+001",
55 | commitsSinceLatestTag = 20,
56 | commitHash = "36e7453"
57 | )
58 | )
59 | }
60 |
61 | @Test
62 | fun `converting invalid tag description to GitTag throws exception`() {
63 | listOf(
64 | "1.0.0-g36e7453",
65 | "1.0.0-a-g36e7453",
66 | "1.0.0-a3-g36e7453",
67 | "1.0.0-3-36e7453",
68 | "1.0.0-3-g36e745",
69 | "1.0.0-a-g36e745g"
70 | ).forEach { tagDescription ->
71 | assertThrows(IllegalArgumentException::class.java) {
72 | tagDescription.toGitTag()
73 | }
74 | }
75 | }
76 |
77 | @Test
78 | fun `GitTag#toString() returns rawTagName`() {
79 | val gitTag = GitTag(
80 | rawTagName = "1.0.0-alpha03",
81 | commitsSinceLatestTag = 1,
82 | commitHash = "36e7453"
83 | )
84 | assertThat(gitTag.toString())
85 | .isEqualTo(
86 | "1.0.0-alpha03"
87 | )
88 | }
89 | }
90 |
--------------------------------------------------------------------------------
/src/test/kotlin/io/github/reactivecircus/appversioning/SemVerTest.kt:
--------------------------------------------------------------------------------
1 | package io.github.reactivecircus.appversioning
2 |
3 | import com.google.common.truth.Truth.assertThat
4 | import org.gradle.internal.impldep.org.junit.Assert.assertThrows
5 | import org.junit.Test
6 |
7 | class SemVerTest {
8 |
9 | @Test
10 | fun `SemVer can be converted to integer representation using positional notation`() {
11 | assertThat(
12 | SemVer(
13 | major = 0,
14 | minor = 0,
15 | patch = 3
16 | ).toInt(maxDigitsPerComponent = 2)
17 | )
18 | .isEqualTo(3)
19 |
20 | assertThat(
21 | SemVer(
22 | major = 0,
23 | minor = 1,
24 | patch = 3
25 | ).toInt(maxDigitsPerComponent = 2)
26 | )
27 | .isEqualTo(103)
28 |
29 | assertThat(
30 | SemVer(
31 | major = 2,
32 | minor = 1,
33 | patch = 3
34 | ).toInt(maxDigitsPerComponent = 2)
35 | )
36 | .isEqualTo(20103)
37 |
38 | assertThat(
39 | SemVer(
40 | major = 0,
41 | minor = 0,
42 | patch = 99
43 | ).toInt(maxDigitsPerComponent = 2)
44 | )
45 | .isEqualTo(99)
46 |
47 | assertThat(
48 | SemVer(
49 | major = 0,
50 | minor = 99,
51 | patch = 99
52 | ).toInt(maxDigitsPerComponent = 2)
53 | )
54 | .isEqualTo(9999)
55 |
56 | assertThat(
57 | SemVer(
58 | major = 100,
59 | minor = 99,
60 | patch = 99
61 | ).toInt(maxDigitsPerComponent = 2)
62 | )
63 | .isEqualTo(1009999)
64 | }
65 |
66 | @Test
67 | fun `converting SemVer to integer throws exception when maxDigitsPerComponent is not positive`() {
68 | assertThrows(IllegalArgumentException::class.java) {
69 | SemVer(
70 | major = 1,
71 | minor = 2,
72 | patch = 3
73 | ).toInt(maxDigitsPerComponent = -1)
74 | }
75 | assertThrows(IllegalArgumentException::class.java) {
76 | SemVer(
77 | major = 1,
78 | minor = 2,
79 | patch = 3
80 | ).toInt(maxDigitsPerComponent = 0)
81 | }
82 | }
83 |
84 | @Test
85 | fun `converting SemVer to integer throws exception when a version component is out of range`() {
86 | assertThrows(IllegalArgumentException::class.java) {
87 | SemVer(
88 | major = 1,
89 | minor = 2,
90 | patch = 10
91 | ).toInt(maxDigitsPerComponent = 1)
92 | }
93 | assertThrows(IllegalArgumentException::class.java) {
94 | SemVer(
95 | major = 1,
96 | minor = 10,
97 | patch = 3
98 | ).toInt(maxDigitsPerComponent = 1)
99 | }
100 | assertThrows(IllegalArgumentException::class.java) {
101 | SemVer(
102 | major = 1,
103 | minor = 2,
104 | patch = 100
105 | ).toInt(maxDigitsPerComponent = 2)
106 | }
107 | assertThrows(IllegalArgumentException::class.java) {
108 | SemVer(
109 | major = 1,
110 | minor = 100,
111 | patch = 3
112 | ).toInt(maxDigitsPerComponent = 2)
113 | }
114 | }
115 |
116 | @Test
117 | fun `converting SemVer to integer throws exception when result is out of range`() {
118 | assertThat(
119 | SemVer(
120 | major = 0,
121 | minor = 0,
122 | patch = Int.MAX_VALUE
123 | ).toInt(maxDigitsPerComponent = 10)
124 | ).isEqualTo(Int.MAX_VALUE)
125 |
126 | assertThrows(IllegalArgumentException::class.java) {
127 | SemVer(
128 | major = 0,
129 | minor = 1,
130 | patch = Int.MAX_VALUE
131 | ).toInt(maxDigitsPerComponent = 10)
132 | }
133 | }
134 |
135 | @Test
136 | fun `SemVer compliant GitTag can be converted to a SemVer`() {
137 | assertThat(
138 | GitTag(
139 | rawTagName = "0.0.4",
140 | commitsSinceLatestTag = 0,
141 | commitHash = "9c28ad3"
142 | ).toSemVer()
143 | ).isEqualTo(
144 | SemVer(
145 | major = 0,
146 | minor = 0,
147 | patch = 4
148 | )
149 | )
150 | assertThat(
151 | GitTag(
152 | rawTagName = "1.2.3",
153 | commitsSinceLatestTag = 0,
154 | commitHash = "9c28ad3"
155 | ).toSemVer()
156 | ).isEqualTo(
157 | SemVer(
158 | major = 1,
159 | minor = 2,
160 | patch = 3
161 | )
162 | )
163 | assertThat(
164 | GitTag(
165 | rawTagName = "10.20.30",
166 | commitsSinceLatestTag = 0,
167 | commitHash = "9c28ad3"
168 | ).toSemVer()
169 | ).isEqualTo(
170 | SemVer(
171 | major = 10,
172 | minor = 20,
173 | patch = 30
174 | )
175 | )
176 | assertThat(
177 | GitTag(
178 | rawTagName = "1.1.2-prerelease+meta",
179 | commitsSinceLatestTag = 0,
180 | commitHash = "9c28ad3"
181 | ).toSemVer()
182 | ).isEqualTo(
183 | SemVer(
184 | major = 1,
185 | minor = 1,
186 | patch = 2,
187 | preRelease = "prerelease",
188 | buildMetadata = "meta"
189 | )
190 | )
191 | assertThat(
192 | GitTag(
193 | rawTagName = "1.1.2+meta",
194 | commitsSinceLatestTag = 0,
195 | commitHash = "9c28ad3"
196 | ).toSemVer()
197 | ).isEqualTo(
198 | SemVer(
199 | major = 1,
200 | minor = 1,
201 | patch = 2,
202 | buildMetadata = "meta"
203 | )
204 | )
205 | assertThat(
206 | GitTag(
207 | rawTagName = "1.1.2+meta-valid",
208 | commitsSinceLatestTag = 0,
209 | commitHash = "9c28ad3"
210 | ).toSemVer()
211 | ).isEqualTo(
212 | SemVer(
213 | major = 1,
214 | minor = 1,
215 | patch = 2,
216 | buildMetadata = "meta-valid"
217 | )
218 | )
219 | assertThat(
220 | GitTag(
221 | rawTagName = "1.0.0-alpha",
222 | commitsSinceLatestTag = 0,
223 | commitHash = "9c28ad3"
224 | ).toSemVer()
225 | ).isEqualTo(
226 | SemVer(
227 | major = 1,
228 | minor = 0,
229 | patch = 0,
230 | preRelease = "alpha"
231 | )
232 | )
233 | assertThat(
234 | GitTag(
235 | rawTagName = "1.0.0-beta",
236 | commitsSinceLatestTag = 0,
237 | commitHash = "9c28ad3"
238 | ).toSemVer()
239 | ).isEqualTo(
240 | SemVer(
241 | major = 1,
242 | minor = 0,
243 | patch = 0,
244 | preRelease = "beta"
245 | )
246 | )
247 | assertThat(
248 | GitTag(
249 | rawTagName = "1.0.0-alpha.beta",
250 | commitsSinceLatestTag = 0,
251 | commitHash = "9c28ad3"
252 | ).toSemVer()
253 | ).isEqualTo(
254 | SemVer(
255 | major = 1,
256 | minor = 0,
257 | patch = 0,
258 | preRelease = "alpha.beta"
259 | )
260 | )
261 | assertThat(
262 | GitTag(
263 | rawTagName = "1.0.0-alpha.beta.1",
264 | commitsSinceLatestTag = 0,
265 | commitHash = "9c28ad3"
266 | ).toSemVer()
267 | ).isEqualTo(
268 | SemVer(
269 | major = 1,
270 | minor = 0,
271 | patch = 0,
272 | preRelease = "alpha.beta.1"
273 | )
274 | )
275 | assertThat(
276 | GitTag(
277 | rawTagName = "1.0.0-alpha.1",
278 | commitsSinceLatestTag = 0,
279 | commitHash = "9c28ad3"
280 | ).toSemVer()
281 | ).isEqualTo(
282 | SemVer(
283 | major = 1,
284 | minor = 0,
285 | patch = 0,
286 | preRelease = "alpha.1"
287 | )
288 | )
289 | assertThat(
290 | GitTag(
291 | rawTagName = "1.0.0-alpha0.valid",
292 | commitsSinceLatestTag = 0,
293 | commitHash = "9c28ad3"
294 | ).toSemVer()
295 | ).isEqualTo(
296 | SemVer(
297 | major = 1,
298 | minor = 0,
299 | patch = 0,
300 | preRelease = "alpha0.valid"
301 | )
302 | )
303 | assertThat(
304 | GitTag(
305 | rawTagName = "1.0.0-alpha.0valid",
306 | commitsSinceLatestTag = 0,
307 | commitHash = "9c28ad3"
308 | ).toSemVer()
309 | ).isEqualTo(
310 | SemVer(
311 | major = 1,
312 | minor = 0,
313 | patch = 0,
314 | preRelease = "alpha.0valid"
315 | )
316 | )
317 | assertThat(
318 | GitTag(
319 | rawTagName = "1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay",
320 | commitsSinceLatestTag = 0,
321 | commitHash = "9c28ad3"
322 | ).toSemVer()
323 | ).isEqualTo(
324 | SemVer(
325 | major = 1,
326 | minor = 0,
327 | patch = 0,
328 | preRelease = "alpha-a.b-c-somethinglong",
329 | buildMetadata = "build.1-aef.1-its-okay"
330 | )
331 | )
332 | assertThat(
333 | GitTag(
334 | rawTagName = "1.0.0-rc.1+build.1",
335 | commitsSinceLatestTag = 0,
336 | commitHash = "9c28ad3"
337 | ).toSemVer()
338 | ).isEqualTo(
339 | SemVer(
340 | major = 1,
341 | minor = 0,
342 | patch = 0,
343 | preRelease = "rc.1",
344 | buildMetadata = "build.1"
345 | )
346 | )
347 | assertThat(
348 | GitTag(
349 | rawTagName = "1.0.0-alpha.beta.1",
350 | commitsSinceLatestTag = 0,
351 | commitHash = "9c28ad3"
352 | ).toSemVer()
353 | ).isEqualTo(
354 | SemVer(
355 | major = 1,
356 | minor = 0,
357 | patch = 0,
358 | preRelease = "alpha.beta.1"
359 | )
360 | )
361 | assertThat(
362 | GitTag(
363 | rawTagName = "2.0.0-rc.1+build.123",
364 | commitsSinceLatestTag = 0,
365 | commitHash = "9c28ad3"
366 | ).toSemVer()
367 | ).isEqualTo(
368 | SemVer(
369 | major = 2,
370 | minor = 0,
371 | patch = 0,
372 | preRelease = "rc.1",
373 | buildMetadata = "build.123"
374 | )
375 | )
376 | assertThat(
377 | GitTag(
378 | rawTagName = "1.2.3-beta",
379 | commitsSinceLatestTag = 0,
380 | commitHash = "9c28ad3"
381 | ).toSemVer()
382 | ).isEqualTo(
383 | SemVer(
384 | major = 1,
385 | minor = 2,
386 | patch = 3,
387 | preRelease = "beta"
388 | )
389 | )
390 | assertThat(
391 | GitTag(
392 | rawTagName = "10.2.3-DEV-SNAPSHOT",
393 | commitsSinceLatestTag = 0,
394 | commitHash = "9c28ad3"
395 | ).toSemVer()
396 | ).isEqualTo(
397 | SemVer(
398 | major = 10,
399 | minor = 2,
400 | patch = 3,
401 | preRelease = "DEV-SNAPSHOT"
402 | )
403 | )
404 | assertThat(
405 | GitTag(
406 | rawTagName = "1.2.3-SNAPSHOT-123",
407 | commitsSinceLatestTag = 0,
408 | commitHash = "9c28ad3"
409 | ).toSemVer()
410 | ).isEqualTo(
411 | SemVer(
412 | major = 1,
413 | minor = 2,
414 | patch = 3,
415 | preRelease = "SNAPSHOT-123"
416 | )
417 | )
418 | assertThat(
419 | GitTag(
420 | rawTagName = "1.0.0",
421 | commitsSinceLatestTag = 0,
422 | commitHash = "9c28ad3"
423 | ).toSemVer()
424 | ).isEqualTo(
425 | SemVer(
426 | major = 1,
427 | minor = 0,
428 | patch = 0
429 | )
430 | )
431 | assertThat(
432 | GitTag(
433 | rawTagName = "2.0.0",
434 | commitsSinceLatestTag = 0,
435 | commitHash = "9c28ad3"
436 | ).toSemVer()
437 | ).isEqualTo(
438 | SemVer(
439 | major = 2,
440 | minor = 0,
441 | patch = 0
442 | )
443 | )
444 | assertThat(
445 | GitTag(
446 | rawTagName = "1.1.7",
447 | commitsSinceLatestTag = 0,
448 | commitHash = "9c28ad3"
449 | ).toSemVer()
450 | ).isEqualTo(
451 | SemVer(
452 | major = 1,
453 | minor = 1,
454 | patch = 7
455 | )
456 | )
457 | assertThat(
458 | GitTag(
459 | rawTagName = "2.0.0+build.1848",
460 | commitsSinceLatestTag = 0,
461 | commitHash = "9c28ad3"
462 | ).toSemVer()
463 | ).isEqualTo(
464 | SemVer(
465 | major = 2,
466 | minor = 0,
467 | patch = 0,
468 | buildMetadata = "build.1848"
469 | )
470 | )
471 | assertThat(
472 | GitTag(
473 | rawTagName = "2.0.1-alpha.1227",
474 | commitsSinceLatestTag = 0,
475 | commitHash = "9c28ad3"
476 | ).toSemVer()
477 | ).isEqualTo(
478 | SemVer(
479 | major = 2,
480 | minor = 0,
481 | patch = 1,
482 | preRelease = "alpha.1227"
483 | )
484 | )
485 | assertThat(
486 | GitTag(
487 | rawTagName = "1.0.0-alpha+beta",
488 | commitsSinceLatestTag = 0,
489 | commitHash = "9c28ad3"
490 | ).toSemVer()
491 | ).isEqualTo(
492 | SemVer(
493 | major = 1,
494 | minor = 0,
495 | patch = 0,
496 | preRelease = "alpha",
497 | buildMetadata = "beta"
498 | )
499 | )
500 | assertThat(
501 | GitTag(
502 | rawTagName = "1.2.3----RC-SNAPSHOT.12.9.1--.12+788",
503 | commitsSinceLatestTag = 0,
504 | commitHash = "9c28ad3"
505 | ).toSemVer()
506 | ).isEqualTo(
507 | SemVer(
508 | major = 1,
509 | minor = 2,
510 | patch = 3,
511 | preRelease = "---RC-SNAPSHOT.12.9.1--.12",
512 | buildMetadata = "788"
513 | )
514 | )
515 | assertThat(
516 | GitTag(
517 | rawTagName = "1.2.3----R-S.12.9.1--.12+meta",
518 | commitsSinceLatestTag = 0,
519 | commitHash = "9c28ad3"
520 | ).toSemVer()
521 | ).isEqualTo(
522 | SemVer(
523 | major = 1,
524 | minor = 2,
525 | patch = 3,
526 | preRelease = "---R-S.12.9.1--.12",
527 | buildMetadata = "meta"
528 | )
529 | )
530 | assertThat(
531 | GitTag(
532 | rawTagName = "1.2.3----RC-SNAPSHOT.12.9.1--.12",
533 | commitsSinceLatestTag = 0,
534 | commitHash = "9c28ad3"
535 | ).toSemVer()
536 | ).isEqualTo(
537 | SemVer(
538 | major = 1,
539 | minor = 2,
540 | patch = 3,
541 | preRelease = "---RC-SNAPSHOT.12.9.1--.12"
542 | )
543 | )
544 | assertThat(
545 | GitTag(
546 | rawTagName = "1.0.0+0.build.1-rc.10000aaa-kk-0.1",
547 | commitsSinceLatestTag = 0,
548 | commitHash = "9c28ad3"
549 | ).toSemVer()
550 | ).isEqualTo(
551 | SemVer(
552 | major = 1,
553 | minor = 0,
554 | patch = 0,
555 | buildMetadata = "0.build.1-rc.10000aaa-kk-0.1"
556 | )
557 | )
558 | assertThat(
559 | GitTag(
560 | rawTagName = "999999999.99999999.9999999",
561 | commitsSinceLatestTag = 0,
562 | commitHash = "9c28ad3"
563 | ).toSemVer()
564 | ).isEqualTo(
565 | SemVer(
566 | major = 999999999,
567 | minor = 99999999,
568 | patch = 9999999
569 | )
570 | )
571 | assertThat(
572 | GitTag(
573 | rawTagName = "1.0.0-0A.is.legal",
574 | commitsSinceLatestTag = 0,
575 | commitHash = "9c28ad3"
576 | ).toSemVer()
577 | ).isEqualTo(
578 | SemVer(
579 | major = 1,
580 | minor = 0,
581 | patch = 0,
582 | preRelease = "0A.is.legal"
583 | )
584 | )
585 | }
586 |
587 | @Test
588 | fun `converting non-SemVer compliant GitTag to SemVer throws exception`() {
589 | listOf(
590 | "1",
591 | "1.2",
592 | "1.2.3-0123",
593 | "1.2.3-0123.0123",
594 | "1.1.2+.123",
595 | "+invalid",
596 | "-invalid",
597 | "-invalid+invalid",
598 | "-invalid.01",
599 | "alpha",
600 | "alpha.beta",
601 | "alpha.beta.1",
602 | "alpha.1",
603 | "alpha+beta",
604 | "alpha_beta",
605 | "alpha.",
606 | "alpha..",
607 | "beta",
608 | "1.0.0-alpha_beta",
609 | "-alpha.",
610 | "1.0.0-alpha..",
611 | "1.0.0-alpha..1",
612 | "1.0.0-alpha...1",
613 | "1.0.0-alpha....1",
614 | "1.0.0-alpha.....1",
615 | "1.0.0-alpha......1",
616 | "1.0.0-alpha.......1",
617 | "01.1.1",
618 | "1.01.1",
619 | "1.1.01",
620 | "1.2",
621 | "1.2.3.DEV",
622 | "1.2-SNAPSHOT",
623 | "1.2.31.2.3----RC-SNAPSHOT.12.09.1--..12+788",
624 | "1.2-RC-SNAPSHOT",
625 | "-1.0.3-gamma+b7718",
626 | "+justmeta",
627 | "9.8.7+meta+meta",
628 | "9.8.7-whatever+meta+meta",
629 | "99999999999999999999999.999999999999999999.99999999999999999----RC-SNAPSHOT.12.09.1--------------------------------..12"
630 | ).forEach { version ->
631 | assertThrows(IllegalArgumentException::class.java) {
632 | GitTag(
633 | rawTagName = version,
634 | commitsSinceLatestTag = 0,
635 | commitHash = "9c28ad3"
636 | ).toSemVer()
637 | }
638 | }
639 | }
640 |
641 | @Test
642 | fun `SemVer compliant GitTag with a prefix v can be converted to a GitTag when allowPrefixV is true`() {
643 | assertThat(
644 | GitTag(
645 | rawTagName = "v1.2.3",
646 | commitsSinceLatestTag = 3,
647 | commitHash = "9c28ad3"
648 | ).toSemVer(allowPrefixV = true)
649 | )
650 | .isEqualTo(
651 | SemVer(
652 | major = 1,
653 | minor = 2,
654 | patch = 3
655 | )
656 | )
657 | }
658 |
659 | @Test
660 | fun `converting SemVer compliant GitTag with a prefix v throws exception when allowPrefixV is false`() {
661 | assertThrows(IllegalArgumentException::class.java) {
662 | GitTag(
663 | rawTagName = "v1.2.3",
664 | commitsSinceLatestTag = 3,
665 | commitHash = "9c28ad3"
666 | ).toSemVer(allowPrefixV = false)
667 | }
668 | }
669 |
670 | @Test
671 | fun `SemVer can be created from fromGitTag() companion function`() {
672 | val gitTag = GitTag(
673 | rawTagName = "1.2.3",
674 | commitsSinceLatestTag = 0,
675 | commitHash = "9c28ad3"
676 | )
677 | assertThat(gitTag.toSemVer())
678 | .isEqualTo(SemVer.fromGitTag(gitTag))
679 | }
680 |
681 | @Test
682 | fun `SemVer#toString() returns the SemVer in string representation`() {
683 | listOf(
684 | "0.0.4",
685 | "1.2.3",
686 | "10.20.30",
687 | "1.1.2-prerelease+meta",
688 | "1.1.2+meta",
689 | "1.1.2+meta-valid",
690 | "1.0.0-alpha",
691 | "1.0.0-beta",
692 | "1.0.0-alpha.beta",
693 | "1.0.0-alpha.beta.1",
694 | "1.0.0-alpha.1",
695 | "1.0.0-alpha0.valid",
696 | "1.0.0-alpha.0valid",
697 | "1.0.0-alpha-a.b-c-somethinglong+build.1-aef.1-its-okay",
698 | "1.0.0-rc.1+build.1",
699 | "2.0.0-rc.1+build.123",
700 | "1.2.3-beta",
701 | "10.2.3-DEV-SNAPSHOT",
702 | "1.2.3-SNAPSHOT-123",
703 | "1.0.0",
704 | "2.0.0",
705 | "1.1.7",
706 | "2.0.0+build.1848",
707 | "2.0.1-alpha.1227",
708 | "1.0.0-alpha+beta",
709 | "1.2.3----RC-SNAPSHOT.12.9.1--.12+788",
710 | "1.2.3----R-S.12.9.1--.12+meta",
711 | "1.2.3----RC-SNAPSHOT.12.9.1--.12",
712 | "1.0.0+0.build.1-rc.10000aaa-kk-0.1",
713 | "999999999.99999999.9999999",
714 | "1.0.0-0A.is.legal"
715 | ).forEach { version ->
716 | assertThat(
717 | GitTag(
718 | rawTagName = version,
719 | commitsSinceLatestTag = 0,
720 | commitHash = "9c28ad3"
721 | ).toSemVer().toString()
722 | ).isEqualTo(version)
723 | }
724 | }
725 | }
726 |
--------------------------------------------------------------------------------
/src/test/kotlin/io/github/reactivecircus/appversioning/VariantInfoTest.kt:
--------------------------------------------------------------------------------
1 | package io.github.reactivecircus.appversioning
2 |
3 | import com.google.common.truth.Truth.assertThat
4 | import org.junit.Test
5 |
6 | class VariantInfoTest {
7 |
8 | @Test
9 | fun `VariantInfo#isDebugBuild returns true when builtType is 'debug'`() {
10 | assertThat(
11 | VariantInfo(
12 | buildType = "debug",
13 | flavorName = "",
14 | variantName = "debug"
15 | ).isDebugBuild
16 | ).isTrue()
17 |
18 | assertThat(
19 | VariantInfo(
20 | buildType = "release",
21 | flavorName = "",
22 | variantName = "release"
23 | ).isDebugBuild
24 | ).isFalse()
25 |
26 | assertThat(
27 | VariantInfo(
28 | buildType = null,
29 | flavorName = "",
30 | variantName = ""
31 | ).isDebugBuild
32 | ).isFalse()
33 |
34 | assertThat(
35 | VariantInfo(
36 | buildType = "DEBUG",
37 | flavorName = "prod",
38 | variantName = "prodDebug"
39 | ).isDebugBuild
40 | ).isFalse()
41 | }
42 |
43 | @Test
44 | fun `VariantInfo#isReleaseBuild returns true when builtType is 'release'`() {
45 | assertThat(
46 | VariantInfo(
47 | buildType = "release",
48 | flavorName = "",
49 | variantName = "release"
50 | ).isReleaseBuild
51 | ).isTrue()
52 |
53 | assertThat(
54 | VariantInfo(
55 | buildType = "debug",
56 | flavorName = "",
57 | variantName = "debug"
58 | ).isReleaseBuild
59 | ).isFalse()
60 |
61 | assertThat(
62 | VariantInfo(
63 | buildType = null,
64 | flavorName = "",
65 | variantName = ""
66 | ).isReleaseBuild
67 | ).isFalse()
68 |
69 | assertThat(
70 | VariantInfo(
71 | buildType = "RELEASE",
72 | flavorName = "prod",
73 | variantName = "prodRelease"
74 | ).isDebugBuild
75 | ).isFalse()
76 | }
77 | }
78 |
--------------------------------------------------------------------------------