├── .github └── workflows │ └── nebula.yml ├── .gitignore ├── CHANGELOG.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── build.gradle ├── gradle.lockfile ├── gradle.properties ├── gradle ├── gradle-daemon-jvm.properties └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat ├── settings.gradle ├── src ├── groovyCompile │ └── groovycConfig.groovy ├── integTest │ └── groovy │ │ └── nebula │ │ └── plugin │ │ └── release │ │ ├── GitVersioningIntegrationSpecSpec.groovy │ │ ├── GitVersioningIntegrationTestKitSpec.groovy │ │ ├── Issue187IntegrationSpec.groovy │ │ ├── ReleasePluginConfiguredVersionIntegrationSpec.groovy │ │ ├── ReleasePluginGitStateIntegrationSpec.groovy │ │ ├── ReleasePluginIntegrationSpec.groovy │ │ ├── ReleasePluginIvyStatusIntegrationSpec.groovy │ │ ├── ReleasePluginMultiprojectIntegrationSpec.groovy │ │ ├── ReleasePluginMultiprojectNoRootPluginApplyIntegrationSpec.groovy │ │ ├── ReleasePluginNewProjectIntegrationSpec.groovy │ │ ├── ReleasePluginNoCommitIntegrationSpec.groovy │ │ ├── ReleasePluginNoRepoIntegrationSpec.groovy │ │ └── SnapshotResolutionIntegrationSpec.groovy ├── main │ └── groovy │ │ └── nebula │ │ └── plugin │ │ └── release │ │ ├── ErrorMessageFormatter.groovy │ │ ├── FeatureFlags.groovy │ │ ├── NetflixOssStrategies.groovy │ │ ├── OverrideStrategies.groovy │ │ ├── ReleaseCheck.groovy │ │ ├── ReleaseExtension.groovy │ │ ├── ReleasePlugin.groovy │ │ ├── VersionSanitizerUtil.groovy │ │ ├── git │ │ ├── GitBuildService.groovy │ │ ├── base │ │ │ ├── BaseReleasePlugin.groovy │ │ │ ├── DefaultVersionStrategy.groovy │ │ │ ├── PrepareTask.groovy │ │ │ ├── ReleasePluginExtension.groovy │ │ │ ├── ReleaseTask.groovy │ │ │ ├── ReleaseVersion.groovy │ │ │ ├── ShortenRefUtil.groovy │ │ │ ├── TagStrategy.groovy │ │ │ ├── VersionStrategy.groovy │ │ │ └── package-info.groovy │ │ ├── command │ │ │ ├── GitReadCommand.groovy │ │ │ ├── GitReadCommandParameters.groovy │ │ │ ├── GitWriteCommand.groovy │ │ │ ├── GitWriteCommandParameters.groovy │ │ │ └── package-info.groovy │ │ ├── model │ │ │ ├── Branch.groovy │ │ │ ├── Commit.groovy │ │ │ ├── TagRef.groovy │ │ │ └── package-info.groovy │ │ ├── opinion │ │ │ ├── Strategies.groovy │ │ │ ├── TimestampPrecision.groovy │ │ │ ├── TimestampUtil.groovy │ │ │ └── package-info.groovy │ │ └── semver │ │ │ ├── ChangeScope.groovy │ │ │ ├── NearestVersion.groovy │ │ │ ├── NearestVersionLocator.groovy │ │ │ ├── PartialSemVerStrategy.groovy │ │ │ ├── RebuildVersionStrategy.groovy │ │ │ ├── SemVerStrategy.groovy │ │ │ ├── SemVerStrategyState.groovy │ │ │ ├── StrategyUtil.groovy │ │ │ └── package-info.groovy │ │ └── util │ │ ├── ConfigureUtil.groovy │ │ ├── ObjectUtil.java │ │ └── ReleaseTasksUtil.groovy └── test │ └── groovy │ └── nebula │ └── plugin │ └── release │ ├── ErrorMessageFormatterSpec.groovy │ ├── OverrideStrategiesSpec.groovy │ ├── ReleasePluginConfigurationAvoidanceSpec.groovy │ ├── ReleasePluginOptionalDepsSpec.groovy │ ├── VersionSanitizerSpec.groovy │ ├── git │ ├── base │ │ ├── ReleasePluginExtensionSpec.groovy │ │ └── TagStrategySpec.groovy │ ├── opinion │ │ └── StrategiesSpec.groovy │ └── semver │ │ ├── RebuildVersionStrategySpec.groovy │ │ ├── SemVerStrategySpec.groovy │ │ └── StrategyUtilSpec.groovy │ └── util │ └── ReleaseTasksUtilSpec.groovy └── stale_outputs_checked /.github/workflows/nebula.yml: -------------------------------------------------------------------------------- 1 | name: Nebula Build 2 | on: 3 | push: 4 | branches: 5 | - '*' 6 | tags: 7 | - v*.*.* 8 | - v*.*.*-rc.* 9 | pull_request: 10 | 11 | jobs: 12 | validation: 13 | name: "Gradle Wrapper Validation" 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: gradle/wrapper-validation-action@v1 18 | buildmultijdk: 19 | if: (!startsWith(github.ref, 'refs/tags/v')) 20 | needs: validation 21 | runs-on: ubuntu-latest 22 | strategy: 23 | matrix: 24 | # test against latest update of some major Java version(s), as well as specific LTS version(s) 25 | java: [8, 17, 21] 26 | name: Gradle Build without Publish 27 | steps: 28 | - uses: actions/checkout@v1 29 | - name: Setup git user 30 | run: | 31 | git config --global user.name "Nebula Plugin Maintainers" 32 | git config --global user.email "nebula-plugins-oss@netflix.com" 33 | - name: Set up JDKs 34 | uses: actions/setup-java@v4 35 | with: 36 | distribution: 'zulu' 37 | java-version: | 38 | 8 39 | ${{ matrix.java }} 40 | java-package: jdk 41 | - uses: actions/cache@v4 42 | id: gradle-cache 43 | with: 44 | path: ~/.gradle/caches 45 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/gradle/dependency-locks/*.lockfile') }} 46 | restore-keys: | 47 | - ${{ runner.os }}-gradle- 48 | - uses: actions/cache@v4 49 | id: gradle-wrapper-cache 50 | with: 51 | path: ~/.gradle/wrapper 52 | key: ${{ runner.os }}-gradlewrapper-${{ hashFiles('gradle/wrapper/*') }} 53 | restore-keys: | 54 | - ${{ runner.os }}-gradlewrapper- 55 | - name: Gradle build 56 | run: ./gradlew --info --stacktrace build 57 | env: 58 | JDK_VERSION_FOR_TESTS: ${{ matrix.java }} 59 | validatepluginpublication: 60 | if: startsWith(github.ref, 'refs/tags/v') 61 | needs: validation 62 | runs-on: ubuntu-latest 63 | name: Gradle Plugin Publication Validation 64 | env: 65 | NETFLIX_OSS_SONATYPE_USERNAME: ${{ secrets.ORG_SONATYPE_USERNAME }} 66 | NETFLIX_OSS_SONATYPE_PASSWORD: ${{ secrets.ORG_SONATYPE_PASSWORD }} 67 | steps: 68 | - uses: actions/checkout@v1 69 | - name: Setup git user 70 | run: | 71 | git config --global user.name "Nebula Plugin Maintainers" 72 | git config --global user.email "nebula-plugins-oss@netflix.com" 73 | - name: Set up JDKs 74 | uses: actions/setup-java@v4 75 | with: 76 | distribution: 'zulu' 77 | java-version: | 78 | 8 79 | 21 80 | java-package: jdk 81 | - uses: actions/cache@v4 82 | id: gradle-cache 83 | with: 84 | path: ~/.gradle/caches 85 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/gradle/dependency-locks/*.lockfile') }} 86 | restore-keys: | 87 | - ${{ runner.os }}-gradle- 88 | - uses: actions/cache@v4 89 | id: gradle-wrapper-cache 90 | with: 91 | path: ~/.gradle/wrapper 92 | key: ${{ runner.os }}-gradlewrapper-${{ hashFiles('gradle/wrapper/*') }} 93 | restore-keys: | 94 | - ${{ runner.os }}-gradlewrapper- 95 | - name: Verify plugin publication 96 | if: | 97 | startsWith(github.ref, 'refs/tags/v') && 98 | (!contains(github.ref, '-rc.')) 99 | run: ./gradlew --stacktrace -Dgradle.publish.key=${{ secrets.gradlePublishKey }} -Dgradle.publish.secret=${{ secrets.gradlePublishSecret }} -Prelease.useLastTag=true final publishPlugin --validate-only -x check -x signPluginMavenPublication 100 | publish: 101 | if: startsWith(github.ref, 'refs/tags/v') 102 | needs: validatepluginpublication 103 | runs-on: ubuntu-latest 104 | name: Gradle Build and Publish 105 | env: 106 | NETFLIX_OSS_SONATYPE_USERNAME: ${{ secrets.ORG_SONATYPE_USERNAME }} 107 | NETFLIX_OSS_SONATYPE_PASSWORD: ${{ secrets.ORG_SONATYPE_PASSWORD }} 108 | NETFLIX_OSS_SIGNING_KEY: ${{ secrets.ORG_SIGNING_KEY }} 109 | NETFLIX_OSS_SIGNING_PASSWORD: ${{ secrets.ORG_SIGNING_PASSWORD }} 110 | NETFLIX_OSS_REPO_USERNAME: ${{ secrets.ORG_NETFLIXOSS_USERNAME }} 111 | NETFLIX_OSS_REPO_PASSWORD: ${{ secrets.ORG_NETFLIXOSS_PASSWORD }} 112 | steps: 113 | - uses: actions/checkout@v1 114 | - name: Setup git user 115 | run: | 116 | git config --global user.name "Nebula Plugin Maintainers" 117 | git config --global user.email "nebula-plugins-oss@netflix.com" 118 | - name: Set up JDKs 119 | uses: actions/setup-java@v4 120 | with: 121 | distribution: 'zulu' 122 | java-version: | 123 | 8 124 | 21 125 | java-package: jdk 126 | - uses: actions/cache@v4 127 | id: gradle-cache 128 | with: 129 | path: ~/.gradle/caches 130 | key: ${{ runner.os }}-gradle-${{ hashFiles('**/gradle/dependency-locks/*.lockfile') }} 131 | restore-keys: | 132 | - ${{ runner.os }}-gradle- 133 | - uses: actions/cache@v4 134 | id: gradle-wrapper-cache 135 | with: 136 | path: ~/.gradle/wrapper 137 | key: ${{ runner.os }}-gradlewrapper-${{ hashFiles('gradle/wrapper/*') }} 138 | restore-keys: | 139 | - ${{ runner.os }}-gradlewrapper- 140 | - name: Publish candidate 141 | if: | 142 | startsWith(github.ref, 'refs/tags/v') && 143 | contains(github.ref, '-rc.') 144 | run: ./gradlew --info --stacktrace -Prelease.useLastTag=true candidate 145 | - name: Publish release 146 | if: | 147 | startsWith(github.ref, 'refs/tags/v') && 148 | (!contains(github.ref, '-rc.')) 149 | run: ./gradlew --info --stacktrace -Dgradle.publish.key=${{ secrets.gradlePublishKey }} -Dgradle.publish.secret=${{ secrets.gradlePublishSecret }} -Prelease.useLastTag=true final 150 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | build/ 3 | .gradletasknamecache 4 | 5 | # Ignore Gradle GUI config 6 | gradle-app.setting 7 | 8 | .idea/ 9 | *.iml 10 | *.ipr 11 | *.iws 12 | out/ 13 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | 6.3.0 / 2018-02-08 2 | ================== 3 | 4 | * Allow releasing a final release from older commits within a master branch even when there are newer commits tagged as candidates 5 | 6 | 6.2.0 / 2018-01-09 7 | ================== 8 | 9 | * Handle ArtifactoryTask if using build-info-extractor 4.6.0 via @lavcraft (Kirill Tolkachev) 10 | 11 | 6.1.1 / 2017-12-12 12 | ================== 13 | 14 | * Cleanup a stray println 15 | 16 | 6.1.0 / 2017-10-25 17 | ================== 18 | 19 | * Add in capability for travis to specify a special branch e.g. 2.x or 3.2.x and produce properly versioned snapshots 20 | 21 | 6.0.2 / 2017-09-20 22 | ================== 23 | 24 | * Remove optional dependencies so they don't leak onto the classpath when resolved from the plugin portal. It was causing bintray to be upgraded to a version that cannot be compiled against on Gradle 3 and later (only affects the plugin at compile time, not at runtime) 25 | 26 | 6.0.1 / 2017-09-06 27 | ================== 28 | 29 | * Builds that fail with Final and candidate builds require all changes to be committed into Git. will now list the files that have not been committed. 30 | 31 | 6.0.0 / 2017-05-23 32 | ================== 33 | 34 | * BREAKING: Java 8 is now required 35 | * gradle-git upgraded to 1.7.1 36 | * jgit transitively upgraded to 4.6.1.201703071140-r to address [Bug 498759: jgit shows submodule as modified path](https://bugs.eclipse.org/bugs/show_bug.cgi?id=498759) 37 | 38 | 5.0.0 / 2017-04-19 39 | ================== 40 | 41 | * Add some tasks and rework task ordering 42 | * BREAKING: `snapshot`, `devSnapshot`, `candidate`, `final` are no longer finalized by release, they now depend on `release` 43 | * Added `snapshotSetup`, `devSnapshotSetup`, `candidateSetup`, `finalSetup` added if you need to run specific things early in the process 44 | * Added `postRelease` task for tasks you want to happen after release (which tags the repo), we have moved publishing to be called by this step 45 | 46 | 4.2.0 / 2017-03-31 47 | ================== 48 | 49 | * Calculate version in repository with no commits 50 | * Allow pushing tags from detached branch 51 | * Better handle branch patterns that would error out semver when put in dev version 52 | 53 | 4.0.1 / 2016-02-05 54 | ================== 55 | 56 | * Fix tasks task so it can run on multiprojects 57 | 58 | 4.0.0 / 2016-02-04 59 | ================== 60 | 61 | * Potential BREAKING change: Removing assumptions on whether to enable bintray and artifactory publishes based on whether a user uses the `devSnapshot`, `snapshot`, `candidate`, or `final` tasks. These should be on more opinionated plugins. 62 | 63 | 3.2.0 / 2016-02-01 64 | ================== 65 | 66 | * Use specific versions of dependencies to prevent dynamic versions in the gradle plugin portal 67 | * upgrade nebula-bintray-plugin 3.2.0 -> 3.3.1 68 | * upgrade gradle-gt plugin 1.3.2 -> 1.4.1 69 | 70 | 3.1.3 / 2016-01-28 71 | ================== 72 | 73 | * Remove need for initial tag 74 | * Better error message for release/[invalid semver pattern] 75 | 76 | 3.1.2 / 2015-12-09 77 | ================== 78 | 79 | * Better error reporting for missing init tag and uncommitted changes 80 | 81 | 3.1.1 / 2015-12-08 82 | ================== 83 | 84 | * Allow to customize the location of Git root 85 | 86 | 3.1.0 / 2015-10-26 87 | ================== 88 | 89 | * Update ivy status and project.status on publish 90 | * `devSnapshot` and `snapshot` will leave status at integration 91 | * `candidate` will set ivy and project status to candidate 92 | * `final` will set ivy and project status to release 93 | * Also depend on artifactory tasks if creating devSnapshot 94 | * Warn rather than fail when the project contains no git repository or the git repository has no commits. 95 | 96 | 3.0.5 / 2015-10-19 97 | ================== 98 | 99 | * Republish correctly 100 | 101 | 3.0.4 / 2015-10-19 102 | ================== 103 | 104 | * gradle-git to 1.3.2 // publishing issue 105 | 106 | 3.0.3 / 2015-10-06 107 | ================== 108 | 109 | * gradle-git to 1.3.1 110 | 111 | 3.0.2 / 2015-09-18 112 | ================== 113 | 114 | * BUGFIX: Allow release from rc tag 115 | 116 | 3.0.1 / 2015-09-09 117 | ================== 118 | 119 | * BUGFIX: fix ordering so we don't release if tests are broken 120 | 121 | 3.0.0 / 2015-09-04 122 | ================== 123 | 124 | * Move to gradle-git 1.3.0 125 | * Plugin built against gradle 2.6 126 | * New travis release process 127 | 128 | 2.2.7 / 2015-07-14 129 | ================== 130 | 131 | * Move to gradle-git 1.2.0 132 | * Only calculate version once for multiprojects 133 | 134 | 2.2.6 / 2015-06-18 135 | ================== 136 | 137 | * Move to gradle-git 1.1.0 138 | 139 | 2.2.5 / 2015-02-09 140 | ================== 141 | 142 | * Add ability to use major.minor.x branches along with major.x branches. 143 | * Update nebula dependencies to newest releases on 2.2.x branches. 144 | 145 | 2.2.4 / 2015-01-19 146 | ================== 147 | 148 | * Modify -Prelease.useLastTag so that it doesn't attempt to push tags to the remote 149 | 150 | 2.2.3 / 2014-12-11 151 | ================== 152 | 153 | * Fix to still have `devSnapshot` task work if a user changes the default versioning strategy 154 | 155 | 2.2.2 / 2014-12-05 156 | ================== 157 | 158 | * Minor change to allow users to configure the default versioning scheme via gradle-git's release extension 159 | 160 | 2.2.1 / 2014-12-05 161 | ================== 162 | 163 | * Add nebula-release properties file so this can be used as a plugin 164 | * rename package from nebula.plugins.release to nebula.plugin.release 165 | 166 | 2.2.0 / 2014-12-04 (removed from jcenter) 167 | ========================================= 168 | 169 | * does not include META-INF/gradle-plugins properties file 170 | * Initial release 171 | * Skip straight to 2.2.x to show built and compatible with gradle 2.2 172 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Nebula 2 | 3 | If you would like to contribute code you can do so through GitHub by forking the repository and sending a pull request. When submitting code, please make every effort to follow existing conventions and style in order to keep the code as readable as possible. 4 | 5 | ## License 6 | 7 | By contributing your code, you agree to license your contribution under the terms of the [Apache License v2.0](http://www.apache.org/licenses/LICENSE-2.0). Your contributions should also include the following header: 8 | 9 | ``` 10 | /** 11 | * Copyright 2016 the original author or authors. 12 | * 13 | * Licensed under the Apache License, Version 2.0 (the "License"); 14 | * you may not use this file except in compliance with the License. 15 | * You may obtain a copy of the License at 16 | * 17 | * http://www.apache.org/licenses/LICENSE-2.0 18 | * 19 | * Unless required by applicable law or agreed to in writing, software 20 | * distributed under the License is distributed on an "AS IS" BASIS, 21 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 22 | * See the License for the specific language governing permissions and 23 | * limitations under the License. 24 | */ 25 | ``` 26 | -------------------------------------------------------------------------------- /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 {yyyy} {name of copyright owner} 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 | 203 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2023 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | 17 | 18 | plugins { 19 | id 'com.netflix.nebula.plugin-plugin' version '21.2.2' 20 | } 21 | 22 | description 'Release opinions on top of gradle-git' 23 | 24 | group = 'com.netflix.nebula' 25 | 26 | contacts { 27 | 'nebula-plugins-oss@netflix.com' { 28 | moniker 'Nebula Plugins Maintainers' 29 | github 'nebula-plugins' 30 | } 31 | } 32 | 33 | 34 | compileGroovy.groovyOptions.configurationScript = file('src/groovyCompile/groovycConfig.groovy') 35 | 36 | 37 | dependencies { 38 | implementation 'com.github.zafarkhaja:java-semver:0.9.0' 39 | implementation platform("com.fasterxml.jackson:jackson-bom:2.14.2") 40 | 41 | compileOnly platform("com.fasterxml.jackson:jackson-bom:2.11.0") 42 | testImplementation 'org.eclipse.jgit:org.eclipse.jgit:5.7.0.202003110725-r' 43 | testImplementation ('org.ajoberstar.grgit:grgit-core:4.1.1') { 44 | exclude group: 'org.codehaus.groovy', module: 'groovy' 45 | } 46 | } 47 | 48 | gradlePlugin { 49 | plugins { 50 | nebulaRelease { 51 | id = 'com.netflix.nebula.release' 52 | displayName = 'Nebula Release plugin' 53 | description = project.description 54 | implementationClass = 'nebula.plugin.release.ReleasePlugin' 55 | tags.set(['nebula', 'release', 'versioning', 'semver']) 56 | } 57 | nebulaReleaseLegacy { 58 | id = 'nebula.release' 59 | displayName = 'Nebula Release plugin' 60 | description = project.description 61 | implementationClass = 'nebula.plugin.release.ReleasePlugin' 62 | tags.set(['nebula', 'release', 'versioning', 'semver']) 63 | } 64 | } 65 | } 66 | 67 | -------------------------------------------------------------------------------- /gradle.lockfile: -------------------------------------------------------------------------------- 1 | # This is a Gradle generated file for dependency locking. 2 | # Manual edits can break the build and are not advised. 3 | # This file is expected to be part of source control. 4 | cglib:cglib-nodep:3.2.2=integTestRuntimeClasspath,testRuntimeClasspath 5 | com.fasterxml.jackson:jackson-bom:2.14.2=compileClasspath,integTestCompileClasspath,integTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 6 | com.github.zafarkhaja:java-semver:0.9.0=compileClasspath,integTestCompileClasspath,integTestRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath 7 | com.googlecode.javaewah:JavaEWAH:1.1.12=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath 8 | com.netflix.nebula:nebula-test:10.5.0=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath 9 | junit:junit:4.13.2=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath 10 | org.ajoberstar.grgit:grgit-core:4.1.1=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath 11 | org.apiguardian:apiguardian-api:1.1.2=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath 12 | org.codehaus.groovy:groovy:3.0.12=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath 13 | org.eclipse.jgit:org.eclipse.jgit:5.13.0.202109080827-r=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath 14 | org.hamcrest:hamcrest-core:1.3=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath 15 | org.hamcrest:hamcrest:2.2=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath 16 | org.junit.platform:junit-platform-commons:1.9.0=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath 17 | org.junit.platform:junit-platform-engine:1.9.0=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath 18 | org.objenesis:objenesis:2.4=integTestRuntimeClasspath,testRuntimeClasspath 19 | org.opentest4j:opentest4j:1.2.0=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath 20 | org.slf4j:slf4j-api:1.7.30=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath 21 | org.spockframework:spock-core:2.3-groovy-3.0=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath 22 | org.spockframework:spock-junit4:2.3-groovy-3.0=integTestCompileClasspath,integTestRuntimeClasspath,testCompileClasspath,testRuntimeClasspath 23 | empty=annotationProcessor,integTestAnnotationProcessor,testAnnotationProcessor 24 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | systemProp.nebula.features.coreLockingSupport=true 2 | -------------------------------------------------------------------------------- /gradle/gradle-daemon-jvm.properties: -------------------------------------------------------------------------------- 1 | toolchainVersion=21 -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nebula-plugins/nebula-release-plugin/94fdb789b4aad0552fd81d53171f640144b0b923/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionSha256Sum=7197a12f450794931532469d4ff21a59ea2c1cd59a3ec3f89c035c3c420a6999 4 | distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.2-bin.zip 5 | networkTimeout=10000 6 | validateDistributionUrl=true 7 | zipStoreBase=GRADLE_USER_HOME 8 | zipStorePath=wrapper/dists 9 | -------------------------------------------------------------------------------- /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=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 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 | org.gradle.wrapper.GradleWrapperMain \ 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=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 74 | 75 | 76 | @rem Execute Gradle 77 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* 78 | 79 | :end 80 | @rem End local scope for the variables with windows NT shell 81 | if %ERRORLEVEL% equ 0 goto mainEnd 82 | 83 | :fail 84 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 85 | rem the _cmd.exe /c_ return code! 86 | set EXIT_CODE=%ERRORLEVEL% 87 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 88 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 89 | exit /b %EXIT_CODE% 90 | 91 | :mainEnd 92 | if "%OS%"=="Windows_NT" endlocal 93 | 94 | :omega 95 | -------------------------------------------------------------------------------- /settings.gradle: -------------------------------------------------------------------------------- 1 | plugins { 2 | id 'com.gradle.develocity' version '3.19' 3 | } 4 | 5 | develocity { 6 | buildScan { 7 | termsOfUseUrl = "https://gradle.com/help/legal-terms-of-use" 8 | termsOfUseAgree = 'yes' 9 | } 10 | } 11 | 12 | rootProject.name="nebula-release-plugin" 13 | -------------------------------------------------------------------------------- /src/groovyCompile/groovycConfig.groovy: -------------------------------------------------------------------------------- 1 | import groovy.transform.CompileStatic 2 | import org.codehaus.groovy.control.customizers.builder.CompilerCustomizationBuilder 3 | 4 | CompilerCustomizationBuilder.withConfig(configuration) { 5 | ast(CompileStatic) 6 | } -------------------------------------------------------------------------------- /src/integTest/groovy/nebula/plugin/release/GitVersioningIntegrationSpecSpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2015 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release 17 | 18 | /** 19 | * Tests for {@link GitVersioningIntegrationTestKitSpec}. 20 | */ 21 | class GitVersioningIntegrationSpecSpec extends GitVersioningIntegrationTestKitSpec { 22 | @Override def setupBuild() {} 23 | 24 | def 'inferred version is parsed'() { 25 | when: 26 | def version = inferredVersion('Inferred project: inferred-version-is-parsed, version: 1.0.0') 27 | 28 | then: 29 | version == normal('1.0.0') 30 | } 31 | 32 | def 'inferred version is parsed from multiline input'() { 33 | when: 34 | def version = inferredVersion('Some\nDummy\nText\nInferred project: inferred-version-is-parsed-from-multiline-input, version: 1.0.0\nAfter\nText') 35 | 36 | then: 37 | version == normal('1.0.0') 38 | } 39 | 40 | def 'missing inferred version throws IAE'() { 41 | when: 42 | inferredVersion('') 43 | 44 | then: 45 | thrown(IllegalArgumentException) 46 | } 47 | } -------------------------------------------------------------------------------- /src/integTest/groovy/nebula/plugin/release/GitVersioningIntegrationTestKitSpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2015 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release 17 | 18 | import com.github.zafarkhaja.semver.Version 19 | import nebula.test.IntegrationSpec 20 | import nebula.test.IntegrationTestKitSpec 21 | import org.ajoberstar.grgit.Grgit 22 | 23 | import java.nio.file.Files 24 | 25 | abstract class GitVersioningIntegrationTestKitSpec extends IntegrationTestKitSpec { 26 | protected Grgit git 27 | protected Grgit originGit 28 | 29 | def setup() { 30 | def origin = new File(projectDir.parent, "${projectDir.name}.git") 31 | if (origin.exists()) { 32 | origin.deleteDir() 33 | } 34 | origin.mkdirs() 35 | 36 | ['build.gradle', 'settings.gradle'].each { 37 | def file = new File(projectDir, it) 38 | if(!file.exists()) { 39 | file.createNewFile() 40 | } 41 | Files.move(file.toPath(), new File(origin, it).toPath()) 42 | } 43 | 44 | originGit = Grgit.init(dir: origin) 45 | originGit.add(patterns: ['build.gradle', 'settings.gradle', '.gitignore'] as Set) 46 | originGit.commit(message: 'Initial checkout') 47 | 48 | if (originGit.branch.current().name == 'main') { 49 | originGit.checkout(branch: 'master', createBranch: true) 50 | originGit.branch.remove(names: ['main']) 51 | } 52 | 53 | git = Grgit.clone(dir: projectDir, uri: origin.absolutePath) as Grgit 54 | 55 | new File(projectDir, '.gitignore') << '''.gradle-test-kit 56 | .gradle 57 | build/ 58 | gradle.properties'''.stripIndent() 59 | 60 | // Enable configuration cache :) 61 | new File(projectDir, 'gradle.properties') << '''org.gradle.configuration-cache=true'''.stripIndent() 62 | 63 | setupBuild() 64 | 65 | 66 | git.add(patterns: ['build.gradle', 'settings.gradle', '.gitignore']) 67 | git.commit(message: 'Setup') 68 | git.push() 69 | } 70 | 71 | abstract def setupBuild() 72 | 73 | def cleanup() { 74 | if (git) git.close() 75 | if (originGit) originGit.close() 76 | } 77 | 78 | def Version normal(String version) { 79 | Version.valueOf(version) 80 | } 81 | 82 | def Version dev(String version) { 83 | normal("${version}${git.head().abbreviatedId}") 84 | } 85 | 86 | def Version inferredVersionForTask(String... args) { 87 | def result = runTasks(args) 88 | inferredVersion(result.output) 89 | } 90 | 91 | def Version inferredVersion(String standardOutput) { 92 | inferredVersion(standardOutput, moduleName) 93 | } 94 | 95 | def Version inferredVersion(String standardOutput, String projectName) { 96 | def matcher = standardOutput =~ /Inferred project: (.*), version: (.*)/ 97 | if (matcher.size() > 0) { 98 | def project = matcher[0][1] as String 99 | def version = matcher[0][2] as String 100 | assert project == projectName 101 | normal(version) 102 | } else { 103 | throw new IllegalArgumentException("Could not find inferred version using $matcher") 104 | } 105 | } 106 | } 107 | -------------------------------------------------------------------------------- /src/integTest/groovy/nebula/plugin/release/Issue187IntegrationSpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2020 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release 17 | 18 | import org.gradle.api.plugins.JavaPlugin 19 | import spock.lang.Ignore 20 | import spock.lang.Issue 21 | import spock.lang.Unroll 22 | 23 | @Ignore("We need to visit the configuration/evaluation of extension") 24 | @Issue("Inconsistent versioning for SNAPSHOT stage https://github.com/nebula-plugins/nebula-release-plugin/issues/187") 25 | class Issue187IntegrationSpec extends GitVersioningIntegrationTestKitSpec { 26 | @Override 27 | def setupBuild() { 28 | buildFile << """ 29 | plugins { 30 | id 'com.netflix.nebula.release' 31 | id 'java' 32 | } 33 | ext.dryRun = true 34 | group = 'test' 35 | 36 | """.stripIndent() 37 | 38 | git.add(patterns: ['build.gradle', '.gitignore'] as Set) 39 | git.tag.add(name: 'v0.2.2') 40 | } 41 | 42 | def 'should infer same version for SNAPSHOT when using build and snapshot task without scope'() { 43 | buildFile << """ 44 | release { 45 | defaultVersionStrategy = nebula.plugin.release.git.opinion.Strategies.SNAPSHOT 46 | } 47 | """ 48 | 49 | when: 50 | def resultBuild = runTasks('build') 51 | 52 | then: 53 | resultBuild.output.contains('version: 0.3.0-SNAPSHOT') 54 | 55 | when: 56 | def resultSnapshot = runTasks('snapshot') 57 | 58 | then: 59 | resultSnapshot.output.contains('version: 0.3.0-SNAPSHOT') 60 | } 61 | 62 | @Unroll 63 | def 'should infer same version for SNAPSHOT when using build and snapshot task with scope #scope'() { 64 | buildFile << """ 65 | release { 66 | defaultVersionStrategy = nebula.plugin.release.git.opinion.Strategies.SNAPSHOT 67 | } 68 | """ 69 | 70 | when: 71 | def resultBuild = runTasks('build', "-Prelease.scope=${scope}") 72 | 73 | then: 74 | resultBuild.output.contains("version: ${expectedVersion}") 75 | 76 | when: 77 | def resultSnapshot = runTasks('snapshot', "-Prelease.scope=${scope}") 78 | 79 | then: 80 | resultSnapshot.output.contains("version: ${expectedVersion}") 81 | 82 | where: 83 | scope | expectedVersion 84 | 'major' | '1.0.0-SNAPSHOT' 85 | 'minor' | '0.3.0-SNAPSHOT' 86 | 'patch' | '0.2.3-SNAPSHOT' 87 | } 88 | 89 | @Unroll 90 | def 'infer #expectedVersion for #task task when not using snapshot strategy'() { 91 | when: 92 | def resultBuild = runTasks('build') 93 | 94 | then: 95 | resultBuild.output.contains('version: 0.3.0-dev') 96 | 97 | when: 98 | def resultSnapshot = runTasks(task) 99 | 100 | then: 101 | resultSnapshot.output.contains("version: $expectedVersion") 102 | 103 | where: 104 | task | expectedVersion 105 | 'devSnapshot' | '0.3.0-dev' 106 | 'candidate' | '0.3.0-rc.1' 107 | 'final' | '0.3.0' 108 | } 109 | 110 | @Unroll 111 | def 'infer release version #expectedReleaseVersion and build version #expectedBuildVersion for #task task when not using snapshot strategy with scope #scope'() { 112 | when: 113 | def resultBuild = runTasks('build', "-Prelease.scope=${scope}") 114 | 115 | then: 116 | resultBuild.output.contains("version: $expectedBuildVersion") 117 | 118 | when: 119 | def resultSnapshot = runTasks(task, "-Prelease.scope=${scope}") 120 | 121 | then: 122 | resultSnapshot.output.contains("version: $expectedReleaseVersion") 123 | 124 | where: 125 | task | scope | expectedReleaseVersion | expectedBuildVersion 126 | 'devSnapshot' | 'patch' | '0.2.3-dev' | '0.2.3-dev' 127 | 'devSnapshot' | 'minor' | '0.3.0-dev' | '0.3.0-dev' 128 | 'devSnapshot' | 'major' | '1.0.0-dev' | '1.0.0-dev' 129 | 'candidate' | 'patch' | '0.2.3-rc.1' | '0.2.3-dev' 130 | 'candidate' | 'minor' | '0.3.0-rc.1' | '0.3.0-dev' 131 | 'candidate' | 'major' | '1.0.0-rc.1' | '1.0.0-dev' 132 | 'final' | 'patch' | '0.2.3' | '0.2.3-dev' 133 | 'final' | 'minor' | '0.3.0' | '0.3.0-dev' 134 | 'final' | 'major' | '1.0.0' | '1.0.0-dev' 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/integTest/groovy/nebula/plugin/release/ReleasePluginConfiguredVersionIntegrationSpec.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.release 2 | 3 | class ReleasePluginConfiguredVersionIntegrationSpec extends GitVersioningIntegrationTestKitSpec { 4 | 5 | @Override 6 | def setupBuild() { 7 | buildFile << """ 8 | plugins { 9 | id 'com.netflix.nebula.release' 10 | } 11 | allprojects { 12 | apply plugin: 'com.netflix.nebula.release' 13 | } 14 | 15 | subprojects { 16 | ext.dryRun = true 17 | group = 'test' 18 | apply plugin: 'java' 19 | } 20 | 21 | version = '1.0.0' 22 | """ 23 | 24 | addSubproject('test-release-common', '// hello') 25 | addSubproject('test-release-client', '''\ 26 | dependencies { 27 | implementation project(':test-release-common') 28 | } 29 | '''.stripIndent()) 30 | 31 | git.tag.add(name: 'v0.0.1') 32 | git.commit(message: 'Another commit') 33 | git.add(patterns: ['build.gradle', '.gitignore', 'settings.gradle', 34 | 'test-release-common/build.gradle', 'test-release-client/build.gradle'] as Set) 35 | } 36 | 37 | def 'should fail build if version is set in build file'() { 38 | when: 39 | def results = runTasksAndFail('final') 40 | 41 | then: 42 | results.output.contains('version should not be set in build file when using nebula-release plugin. Instead use `-Prelease.version` parameter') 43 | } 44 | 45 | } -------------------------------------------------------------------------------- /src/integTest/groovy/nebula/plugin/release/ReleasePluginGitStateIntegrationSpec.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.release 2 | 3 | 4 | class ReleasePluginGitStateIntegrationSpec extends GitVersioningIntegrationTestKitSpec { 5 | 6 | @Override 7 | def setupBuild() { 8 | buildFile << """ 9 | plugins { 10 | id 'com.netflix.nebula.release' 11 | id 'java' 12 | } 13 | """ 14 | } 15 | 16 | def 'test that final and candidate do not work with uncommitted changes'() { 17 | setup: 18 | buildFile << "// force an uncommitted change to file" 19 | 20 | when: 21 | def finalFail = runTasksAndFail("final") 22 | 23 | then: 24 | finalFail.output.contains('require all changes to be committed into Git') 25 | 26 | when: 27 | def candidateFail = runTasksAndFail("candidate") 28 | 29 | then: 30 | candidateFail.output.contains('require all changes to be committed into Git') 31 | } 32 | 33 | def 'ensure plugin does NOT throw an error when a good init tag is present'() { 34 | setup: 35 | ['my-feature-branch', 'super-duper', 'v1.0', 'v0.1.0'].each { git.tag.add(name: it) } 36 | 37 | expect: 38 | runTasks("devSnapshot") 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/integTest/groovy/nebula/plugin/release/ReleasePluginIvyStatusIntegrationSpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015-2019 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release 17 | 18 | import org.gradle.testkit.runner.BuildResult 19 | 20 | class ReleasePluginIvyStatusIntegrationSpec extends GitVersioningIntegrationTestKitSpec { 21 | 22 | public static final String REPO_LOCATION = 'build/ivytest' 23 | 24 | @Override 25 | def setupBuild() { 26 | buildFile << """ 27 | plugins { 28 | id 'com.netflix.nebula.release' 29 | id 'java' 30 | id 'ivy-publish' 31 | } 32 | ext.dryRun = true 33 | group = 'test' 34 | 35 | 36 | publishing { 37 | repositories { 38 | ivy { 39 | name = 'ivytest' 40 | url = '$REPO_LOCATION' 41 | } 42 | } 43 | publications { 44 | ivy(IvyPublication) { 45 | from components.java 46 | } 47 | } 48 | } 49 | 50 | tasks.release.dependsOn 'publishIvyPublicationToIvytestRepository' 51 | 52 | 53 | abstract class PrintStatus extends DefaultTask { 54 | @Input 55 | abstract Property getProjectStatus() 56 | 57 | @TaskAction 58 | void printStatus() { 59 | println "Project Status: \${projectStatus.get()}" 60 | } 61 | } 62 | 63 | def printStatus = tasks.register('printStatus', PrintStatus) 64 | project.afterEvaluate { 65 | printStatus.configure { 66 | projectStatus.set(project.status) 67 | } 68 | } 69 | """.stripIndent() 70 | 71 | settingsFile << '''\ 72 | rootProject.name='statuscheck' 73 | '''.stripIndent() 74 | 75 | git.tag.add(name: 'v0.0.1') 76 | git.add(patterns: ['build.gradle', '.gitignore', 'settings.gradle'] as Set) 77 | } 78 | 79 | def loadIvyFileViaVersionLookup(BuildResult result) { 80 | loadIvyFile(inferredVersion(result.output, 'statuscheck').toString()) 81 | } 82 | 83 | def loadIvyFile(String version) { 84 | new XmlSlurper().parse(new File(projectDir, "${REPO_LOCATION}/test/statuscheck/${version}/ivy-${version}.xml")) 85 | } 86 | 87 | def 'snapshot leaves integration status'() { 88 | when: 89 | def result = runTasks('snapshot') 90 | 91 | then: 92 | def xml = loadIvyFileViaVersionLookup(result) 93 | xml.info.@status == 'integration' 94 | } 95 | 96 | def 'snapshot leaves project.status as integration'() { 97 | when: 98 | def result = runTasks('snapshot', 'printStatus') 99 | 100 | then: 101 | result.output.contains 'Project Status: integration' 102 | } 103 | 104 | def 'devSnapshot leaves integration status'() { 105 | when: 106 | def result = runTasks('devSnapshot') 107 | 108 | then: 109 | def xml = loadIvyFileViaVersionLookup(result) 110 | xml.info.@status == 'integration' 111 | } 112 | 113 | def 'immutableSnapshot leaves integration status'() { 114 | when: 115 | def result = runTasks('immutableSnapshot') 116 | 117 | then: 118 | def xml = loadIvyFileViaVersionLookup(result) 119 | xml.info.@status == 'integration' 120 | } 121 | 122 | def 'candidate sets candidate status'() { 123 | when: 124 | def result = runTasks('candidate') 125 | 126 | then: 127 | def xml = loadIvyFileViaVersionLookup(result) 128 | xml.info.@status == 'candidate' 129 | } 130 | 131 | 132 | def 'final sets release status'() { 133 | when: 134 | def result = runTasks('final') 135 | 136 | then: 137 | def xml = loadIvyFileViaVersionLookup(result) 138 | xml.info.@status == 'release' 139 | } 140 | 141 | } 142 | -------------------------------------------------------------------------------- /src/integTest/groovy/nebula/plugin/release/ReleasePluginMultiprojectIntegrationSpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2016 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release 17 | 18 | import spock.lang.Ignore 19 | import spock.lang.Unroll 20 | 21 | class ReleasePluginMultiprojectIntegrationSpec extends GitVersioningIntegrationTestKitSpec { 22 | @Override 23 | def setupBuild() { 24 | buildFile << """\ 25 | plugins { 26 | id 'com.netflix.nebula.release' 27 | } 28 | allprojects { 29 | apply plugin: 'com.netflix.nebula.release' 30 | } 31 | 32 | subprojects { 33 | ext.dryRun = true 34 | group = 'test' 35 | apply plugin: 'java' 36 | } 37 | """.stripIndent() 38 | 39 | addSubproject('test-release-common', '// hello') 40 | addSubproject('test-release-client', '''\ 41 | dependencies { 42 | implementation project(':test-release-common') 43 | } 44 | '''.stripIndent()) 45 | 46 | git.tag.add(name: 'v0.0.1') 47 | git.commit(message: 'Another commit') 48 | git.add(patterns: ['build.gradle', '.gitignore', 'settings.gradle', 49 | 'test-release-common/build.gradle', 'test-release-client/build.gradle'] as Set) 50 | } 51 | 52 | def 'choose release version'() { 53 | when: 54 | def results = runTasks('final') 55 | 56 | then: 57 | inferredVersion(results.output) == normal('0.1.0') 58 | new File(projectDir, 'test-release-common/build/libs/test-release-common-0.1.0.jar').exists() 59 | new File(projectDir, 'test-release-client/build/libs/test-release-client-0.1.0.jar').exists() 60 | } 61 | 62 | def 'choose candidate version'() { 63 | when: 64 | def results = runTasks('candidate') 65 | 66 | then: 67 | inferredVersion(results.output) == normal('0.1.0-rc.1') 68 | new File(projectDir, 'test-release-common/build/libs/test-release-common-0.1.0-rc.1.jar').exists() 69 | new File(projectDir, 'test-release-client/build/libs/test-release-client-0.1.0-rc.1.jar').exists() 70 | } 71 | 72 | def 'build defaults to dev version'() { 73 | when: 74 | def results = runTasks('build') 75 | 76 | then: 77 | inferredVersion(results.output) == dev('0.1.0-dev.2+') 78 | new File(projectDir, 'test-release-common/build/libs').list().find { 79 | it =~ /test-release-common-0\.1\.0-dev\.2\+/ 80 | } != null 81 | new File(projectDir, 'test-release-client/build/libs').list().find { 82 | it =~ /test-release-client-0\.1\.0-dev\.2\+/ 83 | } != null 84 | } 85 | 86 | def 'build defaults to dev version, non-standard branch name included in version string'() { 87 | git.checkout(branch: 'testexample', createBranch: true) 88 | 89 | when: 90 | def results = runTasks('build') 91 | 92 | then: 93 | inferredVersion(results.output) == dev('0.1.0-dev.2+') 94 | new File(projectDir, 'test-release-common/build/libs').list().find { 95 | it =~ /test-release-common-0\.1\.0-dev\.2\+testexample\./ 96 | } != null 97 | new File(projectDir, 'test-release-client/build/libs').list().find { 98 | it =~ /test-release-client-0\.1\.0-dev\.2\+testexample\./ 99 | } != null 100 | } 101 | 102 | def 'tasks does not fail'() { 103 | given: 104 | buildFile << """\ 105 | allprojects { 106 | apply plugin: 'org.gradle.publishing' 107 | apply plugin: 'java' 108 | } 109 | """.stripIndent() 110 | when: 111 | runTasks('tasks', '--all') 112 | 113 | then: 114 | noExceptionThrown() 115 | } 116 | 117 | @Ignore("Revisit this once publihsing plugin is configuration cache ready") 118 | def 'tasks task does not fail with our publishing plugin'() { 119 | buildFile << """ 120 | buildscript { 121 | repositories { mavenCentral() } 122 | dependencies { 123 | classpath 'com.netflix.nebula:nebula-publishing-plugin:20.3.0' 124 | } 125 | } 126 | 127 | allprojects { 128 | apply plugin: 'org.gradle.publishing' 129 | apply plugin: 'java' 130 | } 131 | 132 | subprojects { sub -> 133 | apply plugin: 'com.netflix.nebula.ivy-publish' 134 | apply plugin: 'com.netflix.nebula.javadoc-jar' 135 | apply plugin: 'com.netflix.nebula.source-jar' 136 | 137 | 138 | publishing { 139 | repositories { 140 | ivy { 141 | name 'localIvy' 142 | url = 'build/localivy' 143 | } 144 | } 145 | } 146 | } 147 | """.stripIndent() 148 | 149 | when: 150 | runTasks('tasks', '--all', '--warning-mode', 'all') 151 | 152 | then: 153 | noExceptionThrown() 154 | 155 | when: 156 | def r = runTasks('snapshot', '-m') 157 | 158 | then: 159 | noExceptionThrown() 160 | def output = r.standardOutput 161 | output.contains(':release SKIPPED') 162 | output.contains(':test-release-common:generateDescriptorFileForNebulaIvyPublication SKIPPED') 163 | } 164 | 165 | @Unroll('multiproject release task does not push for #task') 166 | def 'multiproject release task does not push'() { 167 | given: 168 | String originalRemoteHeadCommit = originGit.head().abbreviatedId 169 | 170 | buildFile << '// add a comment' 171 | git.add(patterns: ['build.gradle']) 172 | git.commit(message: 'commenting build.gradle') 173 | 174 | when: 175 | def results = runTasks(task) 176 | 177 | then: 178 | originalRemoteHeadCommit == originGit.head().abbreviatedId 179 | 180 | where: 181 | task << ['devSnapshot', 'snapshot'] 182 | } 183 | } 184 | -------------------------------------------------------------------------------- /src/integTest/groovy/nebula/plugin/release/ReleasePluginMultiprojectNoRootPluginApplyIntegrationSpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2025 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release 17 | 18 | class ReleasePluginMultiprojectNoRootPluginApplyIntegrationSpec extends GitVersioningIntegrationTestKitSpec { 19 | @Override 20 | def setupBuild() { 21 | buildFile << """\ 22 | plugins { 23 | id('com.netflix.nebula.release').apply(false) 24 | } 25 | subprojects { 26 | apply plugin: 'com.netflix.nebula.release' 27 | ext.dryRun = true 28 | group = 'test' 29 | apply plugin: 'java' 30 | } 31 | """.stripIndent() 32 | 33 | addSubproject('test-release-common', '// hello') 34 | addSubproject('test-release-client', '''\ 35 | dependencies { 36 | implementation project(':test-release-common') 37 | } 38 | '''.stripIndent()) 39 | 40 | git.tag.add(name: 'v0.0.1') 41 | git.commit(message: 'Another commit') 42 | git.add(patterns: ['build.gradle', '.gitignore', 'settings.gradle', 43 | 'test-release-common/build.gradle', 'test-release-client/build.gradle'] as Set) 44 | } 45 | 46 | def 'fail if com.netflix.nebula.release plugin is not applied at rootProject level'() { 47 | expect: 48 | runTasksAndFail('final').output.contains("com.netflix.nebula.release plugin should always be applied at the rootProject level") 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/integTest/groovy/nebula/plugin/release/ReleasePluginNewProjectIntegrationSpec.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.release 2 | import nebula.test.IntegrationTestKitSpec 3 | import org.ajoberstar.grgit.Grgit 4 | 5 | /** 6 | * Verify the behavior of nebula-release under various states for a new project (e.g. no git repo yet initialized, no 7 | * initial commit) 8 | */ 9 | class ReleasePluginNewProjectIntegrationSpec extends IntegrationTestKitSpec { 10 | def 'release tasks unavailable when git repository has no commits'() { 11 | setup: // equivalent of having completed `git init` but no initial commit 12 | def origin = new File(projectDir.parent, "${projectDir.name}.git") 13 | origin.mkdirs() 14 | Grgit.init(dir: origin) 15 | 16 | when: 17 | buildFile << """ 18 | plugins { 19 | id 'com.netflix.nebula.release' 20 | id 'java' 21 | } 22 | """ 23 | 24 | then: 25 | runTasks('build') 26 | runTasksAndFail('snapshot') 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/integTest/groovy/nebula/plugin/release/ReleasePluginNoCommitIntegrationSpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release 17 | 18 | import nebula.test.IntegrationTestKitSpec 19 | import org.ajoberstar.grgit.Grgit 20 | 21 | class ReleasePluginNoCommitIntegrationSpec extends IntegrationTestKitSpec { 22 | Grgit repo 23 | 24 | def 'repo with no commits does not throw errors'() { 25 | given: 26 | repo = Grgit.init(dir: projectDir) 27 | buildFile << """\ 28 | plugins { 29 | id 'com.netflix.nebula.release' 30 | id 'java' 31 | } 32 | 33 | ext.dryRun = true 34 | group = 'test' 35 | 36 | 37 | task showVersion { 38 | doLast { 39 | logger.lifecycle "Version in task: \${version.toString()}" 40 | } 41 | } 42 | """.stripIndent() 43 | 44 | when: 45 | def results = runTasks('showVersion') 46 | 47 | then: 48 | results.output.contains 'Version in task: 0.1.0-dev.0.uncommitted' 49 | } 50 | 51 | def 'repo with no commits does not throw errors - replace dev with immutable snapshot'() { 52 | given: 53 | repo = Grgit.init(dir: projectDir) 54 | new File(buildFile.parentFile, "gradle.properties").text = """ 55 | nebula.release.features.replaceDevWithImmutableSnapshot=true 56 | """ 57 | buildFile << """\ 58 | plugins { 59 | id 'com.netflix.nebula.release' 60 | id 'java' 61 | } 62 | 63 | ext.dryRun = true 64 | group = 'test' 65 | 66 | task showVersion { 67 | doLast { 68 | logger.lifecycle "Version in task: \${version.toString()}" 69 | } 70 | } 71 | """.stripIndent() 72 | 73 | when: 74 | def results = runTasks('showVersion') 75 | 76 | then: 77 | results.output.contains 'Version in task: 0.1.0-snapshot.' 78 | results.output.contains '.uncommitted' 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/integTest/groovy/nebula/plugin/release/ReleasePluginNoRepoIntegrationSpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2017 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release 17 | 18 | import nebula.test.IntegrationTestKitSpec 19 | import org.gradle.api.plugins.JavaPlugin 20 | 21 | class ReleasePluginNoRepoIntegrationSpec extends IntegrationTestKitSpec { 22 | def 'calculate version with no commits'() { 23 | given: 24 | buildFile << """\ 25 | plugins { 26 | id 'com.netflix.nebula.release' 27 | id 'java' 28 | } 29 | ext.dryRun = true 30 | group = 'test' 31 | 32 | task showVersion { 33 | doLast { 34 | logger.lifecycle "Version in task: \${version.toString()}" 35 | } 36 | } 37 | """.stripIndent() 38 | 39 | when: 40 | def results = runTasks('showVersion') 41 | 42 | then: 43 | results.output.contains 'Version in task: 0.1.0-dev.0.uncommitted' 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/integTest/groovy/nebula/plugin/release/SnapshotResolutionIntegrationSpec.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.release 2 | 3 | import nebula.test.IntegrationTestKitSpec 4 | import nebula.test.dependencies.DependencyGraphBuilder 5 | import nebula.test.dependencies.GradleDependencyGenerator 6 | 7 | import org.gradle.util.GradleVersion 8 | 9 | class SnapshotResolutionIntegrationSpec extends IntegrationTestKitSpec { 10 | 11 | def setup() { 12 | if (GradleVersion.current().baseVersion < GradleVersion.version("7.0")) { 13 | settingsFile << "enableFeaturePreview('VERSION_ORDERING_V2')" 14 | } 15 | // Enable configuration cache :) 16 | new File(projectDir, 'gradle.properties') << '''org.gradle.configuration-cache=true'''.stripIndent() 17 | } 18 | 19 | def 'choose immutableSnapshot version if no release candidate'() { 20 | def graph = new DependencyGraphBuilder() 21 | .addModule('test:a:0.0.1') 22 | .addModule('test:a:0.0.2-snapshot.20190708102343+23sd') 23 | .build() 24 | File mavenrepo = new GradleDependencyGenerator(graph, "${projectDir}/testrepogen").generateTestMavenRepo() 25 | 26 | buildFile << """\ 27 | apply plugin: 'java' 28 | repositories { 29 | maven { url = '${mavenrepo.absolutePath}' } 30 | } 31 | 32 | 33 | dependencies { 34 | implementation 'test:a:0.+' 35 | } 36 | """.stripIndent() 37 | 38 | when: 39 | def result = runTasks('dependencies') 40 | 41 | then: 42 | result.output.contains("test:a:0.+ -> 0.0.2-snapshot.20190708102343+23sd") 43 | } 44 | 45 | def 'choose devSnapshot version if no release candidate'() { 46 | def graph = new DependencyGraphBuilder() 47 | .addModule('test:a:0.0.1') 48 | .addModule('test:a:0.0.2-dev.1+23sd') 49 | .build() 50 | File mavenrepo = new GradleDependencyGenerator(graph, "${projectDir}/testrepogen").generateTestMavenRepo() 51 | 52 | buildFile << """\ 53 | apply plugin: 'java' 54 | repositories { 55 | maven { url = '${mavenrepo.absolutePath}' } 56 | } 57 | 58 | 59 | dependencies { 60 | implementation 'test:a:0.+' 61 | } 62 | """.stripIndent() 63 | 64 | when: 65 | def result = runTasks('dependencies') 66 | 67 | then: 68 | result.output.contains("test:a:0.+ -> 0.0.2-dev.1+23sd") 69 | } 70 | 71 | def 'choose immutable version instead of candidate if both is present'() { 72 | def graph = new DependencyGraphBuilder() 73 | .addModule('test:a:0.0.1') 74 | .addModule('test:a:0.0.2-rc.1') 75 | .addModule('test:a:0.0.2-snapshot.20190708102343+23sd') 76 | .build() 77 | File mavenrepo = new GradleDependencyGenerator(graph, "${projectDir}/testrepogen").generateTestMavenRepo() 78 | 79 | buildFile << """\ 80 | apply plugin: 'java' 81 | repositories { 82 | maven { url = '${mavenrepo.absolutePath}' } 83 | } 84 | 85 | 86 | dependencies { 87 | implementation 'test:a:0.+' 88 | } 89 | """.stripIndent() 90 | 91 | when: 92 | def result = runTasks('dependencies') 93 | 94 | then: 95 | result.output.contains("test:a:0.+ -> 0.0.2-snapshot.") 96 | } 97 | 98 | def 'choose candidate version instead of devSnapshot if release candidate is present'() { 99 | def graph = new DependencyGraphBuilder() 100 | .addModule('test:a:0.0.1') 101 | .addModule('test:a:0.0.2-rc.1') 102 | .addModule('test:a:0.0.2-rc.1.dev.1+23sd') 103 | .build() 104 | File mavenrepo = new GradleDependencyGenerator(graph, "${projectDir}/testrepogen").generateTestMavenRepo() 105 | 106 | buildFile << """\ 107 | apply plugin: 'java' 108 | repositories { 109 | maven { url = '${mavenrepo.absolutePath}' } 110 | } 111 | 112 | 113 | dependencies { 114 | implementation 'test:a:0.+' 115 | } 116 | """.stripIndent() 117 | 118 | when: 119 | def result = runTasks('dependencies') 120 | 121 | then: 122 | result.output.contains("test:a:0.+ -> 0.0.2-rc.1") 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/ErrorMessageFormatter.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.release 2 | 3 | class ErrorMessageFormatter { 4 | static final String ROOT_CAUSE = 'Final and candidate builds require all changes to be committed into Git.' 5 | static final String NEW_LINE = sprintf("%n") 6 | 7 | static String format(String status) { 8 | if(!status) { 9 | return "" 10 | } 11 | 12 | StringBuilder sb = new StringBuilder(ROOT_CAUSE) 13 | sb.append(NEW_LINE) 14 | sb.append(status) 15 | sb.append(NEW_LINE) 16 | return sb.toString() 17 | } 18 | 19 | } 20 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/FeatureFlags.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.release 2 | 3 | import nebula.plugin.release.git.opinion.TimestampPrecision 4 | import org.gradle.api.Project 5 | 6 | class FeatureFlags { 7 | public static final String NEBULA_RELEASE_REPLACE_DEV_SNAPSHOT_WITH_IMMUTABLE_SNAPSHOT = "nebula.release.features.replaceDevWithImmutableSnapshot" 8 | public static final String NEBULA_RELEASE_IMMUTABLE_SNAPSHOT_TIMESTAMP_PRECISION = "nebula.release.features.immutableSnapshot.timestampPrecision" 9 | 10 | static boolean isDevSnapshotReplacementEnabled(Project project) { 11 | return project.findProperty(NEBULA_RELEASE_REPLACE_DEV_SNAPSHOT_WITH_IMMUTABLE_SNAPSHOT)?.toString()?.toBoolean() 12 | } 13 | 14 | static TimestampPrecision immutableSnapshotTimestampPrecision(Project project) { 15 | return project.hasProperty(NEBULA_RELEASE_IMMUTABLE_SNAPSHOT_TIMESTAMP_PRECISION) ? 16 | TimestampPrecision.from(project.findProperty(NEBULA_RELEASE_IMMUTABLE_SNAPSHOT_TIMESTAMP_PRECISION).toString()) 17 | : TimestampPrecision.MINUTES 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/NetflixOssStrategies.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release 17 | 18 | import groovy.transform.CompileDynamic 19 | 20 | import nebula.plugin.release.git.opinion.Strategies 21 | import nebula.plugin.release.git.semver.ChangeScope 22 | import nebula.plugin.release.git.semver.PartialSemVerStrategy 23 | import nebula.plugin.release.git.semver.SemVerStrategy 24 | import nebula.plugin.release.git.semver.SemVerStrategyState 25 | import org.gradle.api.GradleException 26 | import org.gradle.api.Project 27 | 28 | import java.util.regex.Pattern 29 | 30 | import static nebula.plugin.release.git.semver.StrategyUtil.* 31 | 32 | @CompileDynamic 33 | class NetflixOssStrategies { 34 | 35 | private static final String TRAVIS_BRANCH_PROP = 'release.travisBranch' 36 | 37 | static SemVerStrategy SNAPSHOT(Project project) { 38 | Strategies.SNAPSHOT.copyWith(normalStrategy: getScopes(project)) 39 | } 40 | 41 | static SemVerStrategy DEVELOPMENT(Project project) { 42 | Strategies.DEVELOPMENT.copyWith( 43 | normalStrategy: getScopes(project), 44 | buildMetadataStrategy: developmentMetadataStrategy(project)) 45 | } 46 | 47 | static SemVerStrategy IMMUTABLE_SNAPSHOT(Project project) { 48 | Strategies.IMMUTABLE_SNAPSHOT.copyWith( 49 | normalStrategy: getScopes(project), 50 | buildMetadataStrategy: developmentMetadataStrategy(project)) 51 | } 52 | 53 | static SemVerStrategy PRE_RELEASE(Project project) { 54 | Strategies.PRE_RELEASE.copyWith(normalStrategy: getScopes(project)) 55 | } 56 | 57 | static SemVerStrategy FINAL(Project project) { 58 | Strategies.FINAL.copyWith(normalStrategy: getScopes(project)) 59 | } 60 | 61 | private static getScopes(Project project) { 62 | Object travisReleaseBranch = (project.hasProperty(TRAVIS_BRANCH_PROP)) ? project.property(TRAVIS_BRANCH_PROP) : null 63 | final PartialSemVerStrategy TRAVIS_BRANCH_MAJOR_X = fromTravisPropertyPattern(travisReleaseBranch, ~/^(\d+)\.x$/) 64 | final PartialSemVerStrategy TRAVIS_BRANCH_MAJOR_MINOR_X = fromTravisPropertyPattern(travisReleaseBranch, ~/^(\d+)\.(\d+)\.x$/) 65 | final PartialSemVerStrategy NEAREST_HIGHER_ANY = nearestHigherAny() 66 | one(Strategies.Normal.USE_SCOPE_PROP, 67 | TRAVIS_BRANCH_MAJOR_X, TRAVIS_BRANCH_MAJOR_MINOR_X, 68 | Strategies.Normal.ENFORCE_GITFLOW_BRANCH_MAJOR_X, Strategies.Normal.ENFORCE_BRANCH_MAJOR_X, 69 | Strategies.Normal.ENFORCE_GITFLOW_BRANCH_MAJOR_MINOR_X, Strategies.Normal.ENFORCE_BRANCH_MAJOR_MINOR_X, 70 | NEAREST_HIGHER_ANY, Strategies.Normal.useScope(ChangeScope.MINOR)) 71 | } 72 | 73 | private static PartialSemVerStrategy fromTravisPropertyPattern(Object travisReleaseBranch, Pattern pattern) { 74 | return closure { SemVerStrategyState state -> 75 | if (travisReleaseBranch) { 76 | def branch = travisReleaseBranch 77 | def m = branch =~ pattern 78 | if (m) { 79 | def major = m.groupCount() >= 1 ? parseIntOrZero(m[0][1]) : -1 80 | def minor = m.groupCount() >= 2 ? parseIntOrZero(m[0][2]) : -1 81 | 82 | def normal = state.nearestVersion.normal 83 | def majorDiff = major - normal.majorVersion 84 | def minorDiff = minor - normal.minorVersion 85 | 86 | if (majorDiff == 1 && minor <= 0) { 87 | // major is off by one and minor is either 0 or not in the branch name 88 | return incrementNormalFromScope(state, ChangeScope.MAJOR) 89 | } else if (minorDiff == 1 && minor > 0) { 90 | // minor is off by one and specified in the branch name 91 | return incrementNormalFromScope(state, ChangeScope.MINOR) 92 | } else if (majorDiff == 0 && minorDiff == 0 && minor >= 0) { 93 | // major and minor match, both are specified in branch name 94 | return incrementNormalFromScope(state, ChangeScope.PATCH) 95 | } else if (majorDiff == 0 && minor < 0) { 96 | // only major specified in branch name and already matches 97 | return state 98 | } else { 99 | throw new GradleException("Invalid branch (${state.currentBranch.name}) for nearest normal (${normal}).") 100 | } 101 | } 102 | } 103 | 104 | return state 105 | } 106 | } 107 | 108 | /** 109 | * If the nearest any is higher from the nearest normal, sets the 110 | * normal component to the nearest any's normal component. Otherwise 111 | * do nothing. 112 | * 113 | *

114 | * For example, if the nearest any is {@code 1.2.3-alpha.1} and the 115 | * nearest normal is {@code 1.2.2}, this will infer the normal 116 | * component as {@code 1.2.3}. 117 | *

118 | */ 119 | static private PartialSemVerStrategy nearestHigherAny() { 120 | return closure { SemVerStrategyState state -> 121 | def nearest = state.nearestVersion 122 | if (nearest.any.lessThanOrEqualTo(nearest.normal)) { 123 | return state 124 | } else { 125 | return state.copyWith(inferredNormal: nearest.any.normalVersion) 126 | } 127 | } 128 | } 129 | 130 | static private PartialSemVerStrategy developmentMetadataStrategy(Project project) { 131 | return { state -> 132 | def nebulaReleaseExtension = project.extensions.findByType(ReleaseExtension) 133 | boolean needsBranchMetadata = true 134 | nebulaReleaseExtension.releaseBranchPatterns.each { 135 | if (state.currentBranch.name =~ it) { 136 | needsBranchMetadata = false 137 | } 138 | } 139 | String shortenedBranch = (state.currentBranch.name =~ nebulaReleaseExtension.shortenedBranchPattern)[0][1] 140 | shortenedBranch = shortenedBranch.replaceAll(/[_\/-]/, '.') 141 | def metadata = needsBranchMetadata ? "${shortenedBranch}.${state.currentHead.abbreviatedId}" : state.currentHead.abbreviatedId 142 | state.copyWith(inferredBuildMetadata: metadata) 143 | } 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/OverrideStrategies.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2019 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release 17 | 18 | import com.github.zafarkhaja.semver.UnexpectedCharacterException 19 | import com.github.zafarkhaja.semver.Version 20 | import groovy.transform.CompileDynamic 21 | import nebula.plugin.release.git.GitBuildService 22 | import nebula.plugin.release.git.base.ReleasePluginExtension 23 | import nebula.plugin.release.git.base.ReleaseVersion 24 | import nebula.plugin.release.git.base.VersionStrategy 25 | import nebula.plugin.release.git.model.TagRef 26 | import nebula.plugin.release.git.opinion.TimestampPrecision 27 | import nebula.plugin.release.git.opinion.TimestampUtil 28 | import nebula.plugin.release.git.semver.NearestVersionLocator 29 | import nebula.plugin.release.util.ReleaseTasksUtil 30 | import org.gradle.api.GradleException 31 | import org.gradle.api.Project 32 | import org.slf4j.Logger 33 | import org.slf4j.LoggerFactory 34 | 35 | /** 36 | * Strategies for setting the version externally from the build. 37 | */ 38 | class OverrideStrategies { 39 | 40 | 41 | static class ReleaseLastTagStrategy implements VersionStrategy { 42 | private static final String NOT_SUPPLIED = 'release-strategy-is-not-supplied' 43 | private static final Logger logger = LoggerFactory.getLogger(ReleaseLastTagStrategy) 44 | 45 | Project project 46 | String propertyName 47 | 48 | ReleaseLastTagStrategy(Project project, String propertyName = ReleaseTasksUtil.USE_LAST_TAG_PROPERTY) { 49 | this.project = project 50 | this.propertyName = propertyName 51 | } 52 | 53 | @Override 54 | String getName() { 55 | return 'use-last-tag' 56 | } 57 | 58 | @Override 59 | boolean selector(Project project, GitBuildService gitBuildService) { 60 | def shouldSelect = project.hasProperty(propertyName) ? project.property(propertyName).toString().toBoolean() : false 61 | 62 | if (shouldSelect) { 63 | project.tasks.getByName('release').enabled = false // remove tagging op since already tagged 64 | } 65 | 66 | shouldSelect 67 | } 68 | 69 | 70 | @CompileDynamic 71 | @Override 72 | ReleaseVersion infer(Project project, GitBuildService gitBuildService) { 73 | def tagStrategy = project.extensions.getByType(ReleasePluginExtension).tagStrategy 74 | def locate = new NearestVersionLocator(gitBuildService, tagStrategy).locate() 75 | String releaseStage 76 | try { 77 | releaseStage = project.property('release.stage') 78 | } catch(MissingPropertyException e) { 79 | releaseStage = NOT_SUPPLIED 80 | logger.debug("ExtraPropertiesExtension 'release.stage' was not supplied", e.getMessage()) 81 | logger.info("Note: It is recommended to supply a release strategy of to make 'useLastTag' most explicit. Please add one to your list of tasks.") 82 | } 83 | 84 | if (releaseStage == 'dev' || releaseStage == 'snapshot' || releaseStage == 'SNAPSHOT') { 85 | throw new GradleException("Cannot use useLastTag with snapshot, immutableSnapshot and devSnapshot tasks") 86 | } 87 | 88 | if (locate.distanceFromAny == 0) { 89 | if(releaseStage == NOT_SUPPLIED && (locate.any.toString().contains('-dev.') || locate.any.toString().contains('-SNAPSHOT') || locate.any.toString().contains('-snapshot.') )) { 90 | throw new GradleException("Current commit has a snapshot, immutableSnapshot or devSnapshot tag. 'useLastTag' requires a prerelease or final tag.") 91 | } 92 | 93 | Version version = locate.any 94 | def preReleaseVersion = version.preReleaseVersion 95 | if (releaseStage == 'rc') { 96 | if (!(preReleaseVersion ==~ /rc\.\d+/)) { 97 | throw new GradleException("Current tag ($version) does not appear to be a pre-release version. A pre-release version MAY be denoted by appending a hyphen and a series of dot separated identifiers immediately following the patch version. For more information, please refer to https://semver.org/") 98 | } 99 | } 100 | if (releaseStage == 'final') { 101 | if (preReleaseVersion) { 102 | throw new GradleException("Current tag ($version) does not appear to be a final version. final task can not be used with prerelease versions. A pre-release version MAY be denoted by appending a hyphen and a series of dot separated identifiers immediately following the patch version. For more information, please refer to https://semver.org/") 103 | } 104 | } 105 | 106 | String inferredVersion = locate.any.toString() 107 | if(VersionSanitizerUtil.hasSanitizeFlag(project)) { 108 | inferredVersion = VersionSanitizerUtil.sanitize(inferredVersion) 109 | } 110 | 111 | logger.debug("Using version ${inferredVersion} with ${releaseStage == NOT_SUPPLIED ? "a non-supplied release strategy" : "${releaseStage} release strategy"}") 112 | return new ReleaseVersion(inferredVersion, null, false) 113 | } else { 114 | List headTags = gitBuildService.headTags() 115 | if (headTags.isEmpty()) { 116 | throw new GradleException("Current commit does not have a tag") 117 | } else { 118 | throw new GradleException("Current commit has following tags: ${headTags.collect{it.name}} but they were not recognized as valid versions" ) 119 | } 120 | } 121 | } 122 | } 123 | 124 | static class GradlePropertyStrategy implements VersionStrategy { 125 | static final String PROPERTY_NAME = 'release.version' 126 | static final String VERSION_VERIFICATION_PROPERTY_NAME = 'release.ignoreSuppliedVersionVerification' 127 | 128 | Project project 129 | String propertyName 130 | 131 | GradlePropertyStrategy(Project project, String propertyName = PROPERTY_NAME) { 132 | this.project = project 133 | this.propertyName = propertyName 134 | } 135 | 136 | @Override 137 | String getName() { 138 | 'gradle-properties' 139 | } 140 | 141 | @Override 142 | boolean selector(Project project, GitBuildService gitBuildService) { 143 | project.hasProperty(propertyName) 144 | } 145 | 146 | 147 | @Override 148 | ReleaseVersion infer(Project project, GitBuildService gitBuildService) { 149 | String requestedVersion = project.property(propertyName).toString() 150 | if (requestedVersion == null || requestedVersion.isEmpty()) { 151 | throw new GradleException('Supplied release.version is empty') 152 | } 153 | 154 | if(VersionSanitizerUtil.hasSanitizeFlag(project)) { 155 | requestedVersion = VersionSanitizerUtil.sanitize(requestedVersion.toString()) 156 | } 157 | 158 | boolean isValidVersion = validateRequestedVersion(requestedVersion) 159 | if(!isValidVersion) { 160 | throw new GradleException("Supplied release.version ($requestedVersion) is not valid per semver spec. For more information, please refer to https://semver.org/") 161 | } 162 | 163 | new ReleaseVersion(requestedVersion, null, true) 164 | } 165 | 166 | private boolean validateRequestedVersion(String version) { 167 | if(project.hasProperty(VERSION_VERIFICATION_PROPERTY_NAME)) { 168 | return true 169 | } 170 | 171 | try { 172 | Version.valueOf(version[0] == 'v' ? version[1..-1] : version) 173 | return true 174 | } catch(UnexpectedCharacterException e) { 175 | return false 176 | } 177 | } 178 | 179 | } 180 | 181 | static class NoCommitStrategy implements VersionStrategy { 182 | @Override 183 | String getName() { 184 | 'no-commit' 185 | } 186 | 187 | @Override 188 | boolean selector(Project project, GitBuildService gitBuildService) { 189 | return !gitBuildService.hasCommit() 190 | } 191 | 192 | @Override 193 | ReleaseVersion infer(Project project, GitBuildService gitBuildService) { 194 | boolean replaceDevSnapshots = FeatureFlags.isDevSnapshotReplacementEnabled(project) 195 | if(replaceDevSnapshots) { 196 | TimestampPrecision immutableSnapshotTimestampPrecision = FeatureFlags.immutableSnapshotTimestampPrecision(project) 197 | new ReleaseVersion("0.1.0-snapshot.${TimestampUtil.getUTCFormattedTimestamp(immutableSnapshotTimestampPrecision)}.uncommitted", null, false) 198 | } else { 199 | new ReleaseVersion('0.1.0-dev.0.uncommitted', null, false) 200 | } 201 | } 202 | } 203 | 204 | } 205 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/ReleaseCheck.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2017 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release 17 | 18 | import org.gradle.api.DefaultTask 19 | import org.gradle.api.GradleException 20 | import org.gradle.api.tasks.Input 21 | import org.gradle.api.tasks.TaskAction 22 | import org.gradle.work.DisableCachingByDefault 23 | 24 | @DisableCachingByDefault 25 | class ReleaseCheck extends DefaultTask { 26 | @Input 27 | String branchName 28 | @Input 29 | ReleaseExtension patterns 30 | @Input 31 | boolean isSnapshotRelease 32 | 33 | @TaskAction 34 | void check() { 35 | if (patterns.allowReleaseFromDetached) { 36 | return 37 | } 38 | boolean includeMatch = patterns.releaseBranchPatterns.isEmpty() 39 | 40 | patterns.releaseBranchPatterns.each { String pattern -> 41 | if (getBranchName() ==~ pattern) includeMatch = true 42 | } 43 | 44 | boolean excludeMatch = false 45 | patterns.excludeBranchPatterns.each { String pattern -> 46 | if (getBranchName() ==~ pattern) excludeMatch = true 47 | } 48 | 49 | if (!includeMatch && !isSnapshotRelease) { 50 | String message = "Branch ${getBranchName()} does not match one of the included patterns: ${patterns.releaseBranchPatterns}" 51 | logger.error(message) 52 | throw new GradleException(message) 53 | } 54 | 55 | if (excludeMatch) { 56 | String message = "Branch ${getBranchName()} matched an excluded pattern: ${patterns.excludeBranchPatterns}" 57 | logger.error(message) 58 | throw new GradleException(message) 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/ReleaseExtension.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2017 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release 17 | 18 | class ReleaseExtension { 19 | Set releaseBranchPatterns = [/master/, /HEAD/, /main/, /(release(-|\/))?\d+(\.\d+)?\.x/, /v?\d+\.\d+\.\d+/] as Set 20 | Set excludeBranchPatterns = [] as Set 21 | 22 | /** 23 | * This should be a regex pattern with one(1) capture group. By default shortens the typical 24 | * {bugfix|feature|hotfix|release}/branch-name to branch-name. The prefix is optional and a 25 | * dash may be used instead of the forward slash. 26 | */ 27 | String shortenedBranchPattern = /(?:(?:bugfix|feature|hotfix|release)(?:-|\/))?(.+)/ 28 | 29 | Boolean allowReleaseFromDetached = false 30 | 31 | Boolean checkRemoteBranchOnRelease = false 32 | 33 | void addReleaseBranchPattern(String pattern) { 34 | releaseBranchPatterns.add(pattern) 35 | } 36 | 37 | void addExcludeBranchPattern(String pattern) { 38 | excludeBranchPatterns.add(pattern) 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/VersionSanitizerUtil.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.release 2 | 3 | import org.gradle.api.Project 4 | 5 | class VersionSanitizerUtil { 6 | 7 | static final String SANITIZE_PROPERTY_NAME = 'release.sanitizeVersion' 8 | 9 | private static final String SANITIZE_REGEX = "[^A-Za-z0-9_.-]" 10 | private static final String SANITIZE_REPLACEMENT_CHAR = '.' 11 | 12 | static String sanitize(String version) { 13 | return version.replaceAll(SANITIZE_REGEX, SANITIZE_REPLACEMENT_CHAR) 14 | } 15 | 16 | static boolean hasSanitizeFlag(Project project) { 17 | if(!project.hasProperty(SANITIZE_PROPERTY_NAME)) { 18 | return false 19 | } 20 | 21 | return project.property(SANITIZE_PROPERTY_NAME)?.toString()?.toBoolean() 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/git/base/BaseReleasePlugin.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release.git.base 17 | 18 | import groovy.transform.CompileDynamic 19 | import nebula.plugin.release.git.GitBuildService 20 | import org.gradle.api.Plugin 21 | import org.gradle.api.Project 22 | import org.gradle.api.provider.Provider 23 | 24 | /** 25 | * Plugin providing the base structure of gradle-git's flavor of release 26 | * behavior. The plugin can be applied using the {@code org.ajoberstar.release-base} id. 27 | * 28 | *

29 | * The plugin adds the {@link ReleasePluginExtension} and a {@code release} task. 30 | *

31 | * 32 | * @see nebula.plugin.release.git.opinion.Strategies 33 | * @see Wiki Doc 34 | */ 35 | @CompileDynamic 36 | class BaseReleasePlugin implements Plugin { 37 | 38 | private static final String PREPARE_TASK_NAME = 'prepare' 39 | private static final String RELEASE_TASK_NAME = 'release' 40 | private Provider gitBuildService 41 | 42 | void apply(Project project) { 43 | ReleasePluginExtension releasePluginExtension = project.extensions.create('release', ReleasePluginExtension, project) 44 | File gitRoot = project.hasProperty('git.root') ? project.file(project.property('git.root')) : project.rootProject.projectDir 45 | this.gitBuildService = project.getGradle().getSharedServices().registerIfAbsent("gitBuildService", GitBuildService.class, spec -> { 46 | spec.getParameters().getGitRootDir().set(gitRoot) 47 | }) 48 | addPrepareTask(project, releasePluginExtension) 49 | addReleaseTask(project, releasePluginExtension) 50 | } 51 | 52 | private void addPrepareTask(Project project, ReleasePluginExtension extension) { 53 | def prepareTask = project.tasks.register(PREPARE_TASK_NAME, PrepareTask) 54 | prepareTask.configure { 55 | it.gitService.set(gitBuildService) 56 | it.description = 'Verifies that the project could be released.' 57 | project.version.toString() 58 | if(!(project.version instanceof String) && project.version.inferredVersion) { 59 | it.projectVersion.set(project.version.inferredVersion) 60 | } 61 | it.remote.set(extension.remote) 62 | } 63 | 64 | project.tasks.configureEach { task -> 65 | if (task.name != PREPARE_TASK_NAME) { 66 | task.shouldRunAfter PREPARE_TASK_NAME 67 | } 68 | } 69 | 70 | } 71 | 72 | 73 | private void addReleaseTask(Project project, ReleasePluginExtension extension) { 74 | def releaseTask = project.tasks.register(RELEASE_TASK_NAME, ReleaseTask) 75 | releaseTask.configure { 76 | it.description = 'Releases this project.' 77 | it.dependsOn project.tasks.named(PREPARE_TASK_NAME) 78 | project.version.toString() 79 | if(!(project.version instanceof String) && project.version.inferredVersion) { 80 | it.projectVersion.set(project.version.inferredVersion) 81 | } 82 | it.tagStrategy.set(extension.tagStrategy) 83 | it.remote.set(extension.remote) 84 | it.gitService.set(gitBuildService) 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/git/base/DefaultVersionStrategy.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release.git.base 17 | 18 | import nebula.plugin.release.git.GitBuildService 19 | import org.gradle.api.Project 20 | 21 | /** 22 | * Strategy to infer a version from the project's and Git repository's state. This 23 | * also supports being selected as a default strategy. This is a temporary interface 24 | * and should be replaced in some other way in gradle-git 2.0.0. 25 | * @see nebula.plugin.release.git.semver.SemVerStrategy 26 | * @see nebula.plugin.release.git.opinion.Strategies 27 | */ 28 | interface DefaultVersionStrategy extends VersionStrategy { 29 | 30 | /** 31 | * Determines if the strategy can be used as a default strategy for inferring 32 | * the project's version. A return of {@code false} does not mean that the 33 | * strategy cannot be used as the default. 34 | * @param project the project the version should be inferred for 35 | * @param gitOps the class to talk to git using native git calls 36 | * @return {@code true} if the strategy can be used to infer the version 37 | */ 38 | boolean defaultSelector(Project project, GitBuildService gitBuildService) 39 | } 40 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/git/base/PrepareTask.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.release.git.base 2 | 3 | import nebula.plugin.release.git.GitBuildService 4 | import org.gradle.api.DefaultTask 5 | import org.gradle.api.GradleException 6 | import org.gradle.api.provider.Property 7 | import org.gradle.api.services.ServiceReference 8 | import org.gradle.api.tasks.Input 9 | import org.gradle.api.tasks.Optional 10 | import org.gradle.api.tasks.TaskAction 11 | import org.gradle.work.DisableCachingByDefault 12 | 13 | @DisableCachingByDefault(because = "Publishing related tasks should not be cacheable") 14 | abstract class PrepareTask extends DefaultTask { 15 | 16 | @Input 17 | abstract Property getRemote() 18 | 19 | @Input 20 | @Optional 21 | abstract Property getProjectVersion() 22 | 23 | @ServiceReference("gitBuildService") 24 | abstract Property getGitService() 25 | 26 | @TaskAction 27 | void prepare() { 28 | if(!projectVersion.isPresent()) { 29 | throw new GradleException("version should not be set in build file when using nebula-release plugin. Instead use `-Prelease.version` parameter") 30 | } 31 | logger.info('Fetching changes from remote: {}', remote.get()) 32 | GitBuildService gitBuildService = gitService.get() 33 | gitBuildService.fetch(remote.get()) 34 | 35 | boolean currentBranchIsBehindRemote = gitBuildService.isCurrentBranchBehindRemote(remote.get()) 36 | // if branch is tracking another, make sure it's not behind 37 | if (currentBranchIsBehindRemote) { 38 | throw new GradleException('Current branch is behind the tracked branch. Cannot release.') 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/git/base/ReleasePluginExtension.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release.git.base 17 | 18 | import groovy.transform.CompileDynamic 19 | import nebula.plugin.release.git.GitBuildService 20 | import nebula.plugin.release.util.ConfigureUtil 21 | 22 | import org.gradle.api.GradleException 23 | import org.gradle.api.Project 24 | import org.slf4j.Logger 25 | import org.slf4j.LoggerFactory 26 | 27 | import javax.inject.Inject 28 | 29 | /** 30 | * Extension providing configuration options for gradle-git's release plugins. 31 | * 32 | *

33 | * Sets the version to a {@link DelayedVersion} which will infer the version 34 | * when {@code toString()} is called on it. A strategy will be selected from the 35 | * ones configured on this extension and then used to infer the version. 36 | *

37 | * 38 | * @see BaseReleasePlugin 39 | */ 40 | class ReleasePluginExtension { 41 | private static final Logger logger = LoggerFactory.getLogger(ReleasePluginExtension) 42 | protected final Project project 43 | private final Map versionStrategies = [:] 44 | 45 | /** 46 | * The strategy to use when creating a tag for the inferred version. 47 | */ 48 | final TagStrategy tagStrategy = new TagStrategy() 49 | 50 | /** 51 | * The strategy to use if all of the ones in {@code versionStrategies} return 52 | * false from their {@code selector()} methods. This strategy can be, but is 53 | * not required to be, one from {@code versionStrategies}. 54 | */ 55 | VersionStrategy defaultVersionStrategy 56 | 57 | /** 58 | * Used to execute git read and write operations 59 | */ 60 | GitBuildService gitBuildService 61 | 62 | /** 63 | * The remote to fetch changes from and push changes to. 64 | */ 65 | String remote = 'origin' 66 | 67 | @CompileDynamic 68 | @Inject 69 | ReleasePluginExtension(Project project) { 70 | File gitRoot = project.hasProperty('git.root') ? project.file(project.property('git.root')) : project.rootProject.projectDir 71 | this.project = project 72 | this.gitBuildService = project.getGradle().getSharedServices().registerIfAbsent("gitBuildService", GitBuildService.class, spec -> { 73 | spec.getParameters().getGitRootDir().set(gitRoot) 74 | }).get() 75 | def sharedVersion = new DelayedVersion() 76 | project.rootProject.allprojects { Project p -> 77 | p.version = sharedVersion 78 | } 79 | } 80 | 81 | /** 82 | * Gets all strategies in the order they were inserted into the extension. 83 | */ 84 | List getVersionStrategies() { 85 | return versionStrategies.collect { key, value -> value }.asImmutable() 86 | } 87 | 88 | /** 89 | * Adds a strategy to the extension. If the strategy has the same name as 90 | * one already configured, it will replace the existing one. 91 | */ 92 | void versionStrategy(VersionStrategy strategy) { 93 | versionStrategies[strategy.name] = strategy 94 | } 95 | 96 | /** 97 | * Configures the tag strategy with the provided closure. 98 | */ 99 | void tagStrategy(Closure closure) { 100 | ConfigureUtil.configure(closure, tagStrategy) 101 | } 102 | 103 | // TODO: Decide if this should be thread-safe. 104 | private class DelayedVersion implements Serializable { 105 | ReleaseVersion inferredVersion 106 | 107 | @CompileDynamic 108 | private void infer() { 109 | VersionStrategy selectedStrategy = versionStrategies.find { strategy -> 110 | strategy.selector(project, gitBuildService) 111 | } 112 | 113 | if (!selectedStrategy) { 114 | boolean useDefault 115 | if (defaultVersionStrategy instanceof DefaultVersionStrategy) { 116 | useDefault = defaultVersionStrategy.defaultSelector(project, gitBuildService) 117 | } else { 118 | useDefault = defaultVersionStrategy?.selector(project, gitBuildService) 119 | } 120 | 121 | if (useDefault) { 122 | logger.info('Falling back to default strategy: {}', defaultVersionStrategy.name) 123 | selectedStrategy = defaultVersionStrategy 124 | } else { 125 | throw new GradleException('No version strategies were selected. Run build with --info for more detail.') 126 | } 127 | } 128 | 129 | inferredVersion = selectedStrategy.infer(project, gitBuildService) 130 | } 131 | 132 | @Override 133 | String toString() { 134 | if (!inferredVersion) { 135 | infer() 136 | } 137 | return inferredVersion.version 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/git/base/ReleaseTask.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.release.git.base 2 | 3 | import nebula.plugin.release.git.GitBuildService 4 | import org.gradle.api.DefaultTask 5 | import org.gradle.api.GradleException 6 | import org.gradle.api.provider.Property 7 | import org.gradle.api.services.ServiceReference 8 | import org.gradle.api.tasks.Input 9 | import org.gradle.api.tasks.Internal 10 | import org.gradle.api.tasks.Optional 11 | import org.gradle.api.tasks.TaskAction 12 | import org.gradle.work.DisableCachingByDefault 13 | 14 | @DisableCachingByDefault(because = "Publishing related tasks should not be cacheable") 15 | abstract class ReleaseTask extends DefaultTask { 16 | @Internal 17 | abstract Property getTagStrategy() 18 | 19 | @Input 20 | @Optional 21 | abstract Property getProjectVersion() 22 | 23 | @Input 24 | abstract Property getRemote() 25 | 26 | @ServiceReference("gitBuildService") 27 | abstract Property getGitService() 28 | 29 | @TaskAction 30 | void release() { 31 | if(!projectVersion.isPresent()) { 32 | throw new GradleException("version should not be set in build file when using nebula-release plugin. Instead use `-Prelease.version` parameter") 33 | } 34 | GitBuildService gitBuildService = gitService.get() 35 | String tagName = tagStrategy.get().maybeCreateTag(gitBuildService, projectVersion.get()) 36 | if (tagName) { 37 | logger.info('Pushing changes in {} to {}', tagName, remote.get()) 38 | gitBuildService.pushTag(remote.get(), tagName) 39 | } else { 40 | logger.info('No new tags to push for {}', remote.get()) 41 | } 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/git/base/ReleaseVersion.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release.git.base 17 | 18 | 19 | import groovy.transform.Immutable 20 | 21 | /** 22 | * Represents an inferred version and any related metadata to be used after the 23 | * inference. 24 | */ 25 | @Immutable 26 | class ReleaseVersion implements Serializable { 27 | /** 28 | * The version that should be used by the project. 29 | */ 30 | String version 31 | /** 32 | * The latest version, as determined by the strategy's logic. 33 | */ 34 | String previousVersion 35 | /** 36 | * Whether or not to create a tag for the release. 37 | */ 38 | boolean createTag 39 | } 40 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/git/base/ShortenRefUtil.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.release.git.base 2 | 3 | class ShortenRefUtil { 4 | /** Prefix for branch refs */ 5 | public static final String R_HEADS = "refs/heads/" 6 | 7 | /** Prefix for remotes refs */ 8 | public static final String R_REMOTES = "refs/remotes/" 9 | 10 | /** Prefix for tag refs */ 11 | public static final String R_TAGS = "refs/tags/" 12 | 13 | static String shortenRefName(String refName) { 14 | if (refName.startsWith(R_HEADS)) 15 | return refName.substring(R_HEADS.length()) 16 | if (refName.startsWith(R_TAGS)) 17 | return refName.substring(R_TAGS.length()) 18 | if (refName.startsWith(R_REMOTES)) 19 | return refName.substring(R_REMOTES.length()) 20 | return refName 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/git/base/TagStrategy.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release.git.base 17 | 18 | import groovy.transform.CompileDynamic 19 | import nebula.plugin.release.git.GitBuildService 20 | import org.slf4j.Logger 21 | import org.slf4j.LoggerFactory 22 | 23 | /** 24 | * Strategy for creating a Git tag associated with a release. 25 | */ 26 | @CompileDynamic 27 | class TagStrategy implements Serializable { 28 | 29 | /** 30 | * Closure taking a version String as an argument and returning a string to be used as a tag name. 31 | */ 32 | Closure toTagString 33 | 34 | 35 | TagStrategy() { 36 | setPrefixNameWithV(true) 37 | } 38 | 39 | private static final Logger logger = LoggerFactory.getLogger(TagStrategy) 40 | 41 | /** 42 | * Added for backwards compatibility. 43 | * @param prefix whether or not to prefix the tag with a 'v' 44 | */ 45 | void setPrefixNameWithV(boolean prefix) { 46 | toTagString = { versionString -> prefix ? "v${versionString}" : versionString } 47 | } 48 | 49 | /** 50 | * Closure taking a {@link ReleaseVersion} as an argument and returning 51 | * a string to be used as the tag's message. 52 | */ 53 | Closure generateMessage = { ReleaseVersion version -> "Release of ${version.version}" } 54 | 55 | /** 56 | * If the release version specifies a tag should be created, create a tag 57 | * using the provided {@code Grgit} instance and this instance's state to 58 | * determine the tag name and message. 59 | * @param gitOps the git operations to use 60 | * @param version the version to create the tag for 61 | * @return the name of the tag created, or {@code null} if it wasn't 62 | */ 63 | String maybeCreateTag(GitBuildService gitBuildService, ReleaseVersion version) { 64 | if (version.createTag) { 65 | String name = toTagString(version.version) 66 | String message = generateMessage(version) 67 | 68 | logger.warn('Tagging repository as {}', name) 69 | gitBuildService.createTag(name, message) 70 | return name 71 | } else { 72 | return null 73 | } 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/git/base/VersionStrategy.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release.git.base 17 | 18 | import nebula.plugin.release.git.GitBuildService 19 | import org.gradle.api.Project 20 | 21 | /** 22 | * Strategy to infer a version from the project's and Git repository's state. 23 | * @see nebula.plugin.release.git.semver.SemVerStrategy 24 | * @see nebula.plugin.release.git.opinion.Strategies 25 | */ 26 | interface VersionStrategy { 27 | /** 28 | * The name of the strategy. 29 | * @return the name of the strategy 30 | */ 31 | String getName() 32 | 33 | 34 | /** 35 | * Determines if the strategy should be used to infer the project's version. 36 | * A return of {@code false} does not mean that the strategy cannot be used 37 | * as the default. 38 | * @param project the project the version should be inferred for 39 | * @param gitOps the Git operations to use 40 | * @return {@code true} if the strategy should be used to infer the version 41 | */ 42 | boolean selector(Project project, GitBuildService gitBuildService) 43 | 44 | 45 | /** 46 | * Infers the project version from the repository. 47 | * @param project the project the version should be inferred for 48 | * @param gitOps the Git operations to use 49 | * @return the inferred version 50 | */ 51 | ReleaseVersion infer(Project project, GitBuildService gitBuildService) 52 | } 53 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/git/base/package-info.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /** 17 | * The core structure of gradle-git's flavor of release behavior. The code 18 | * provided in this package can be used on its own, as long as users provide 19 | * their own implementation of {@code VersionStrategy}. 20 | */ 21 | package nebula.plugin.release.git.base 22 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/git/command/GitReadCommand.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.release.git.command 2 | 3 | import groovy.transform.CompileDynamic 4 | import org.gradle.api.Action 5 | import org.gradle.api.GradleException 6 | import org.gradle.api.provider.ValueSource 7 | import org.gradle.process.ExecOperations 8 | import org.gradle.process.ExecSpec 9 | import org.slf4j.Logger 10 | import org.slf4j.LoggerFactory 11 | 12 | import javax.inject.Inject 13 | import java.nio.charset.Charset 14 | 15 | /** 16 | * These read only git commands use ValueSource approach for configuration cache 17 | * @see {@link https://docs.gradle.org/8.4/userguide/configuration_cache.html#config_cache:requirements:external_processes} 18 | */ 19 | abstract class GitReadCommand implements ValueSource { 20 | @Inject 21 | abstract ExecOperations getExecOperations() 22 | 23 | @CompileDynamic 24 | String executeGitCommand(Object ... args) { 25 | File rootDir = parameters.rootDir.get() 26 | ByteArrayOutputStream output = new ByteArrayOutputStream() 27 | ByteArrayOutputStream error = new ByteArrayOutputStream() 28 | List commandLineArgs = ["git", "--git-dir=${rootDir.absolutePath}/.git".toString(), "--work-tree=${rootDir.absolutePath}".toString()] 29 | commandLineArgs.addAll(args) 30 | execOperations.exec(new Action() { 31 | @Override 32 | void execute(ExecSpec execSpec) { 33 | execSpec.setCommandLine(commandLineArgs) 34 | execSpec.standardOutput = output 35 | execSpec.errorOutput = error 36 | } 37 | }) 38 | def errorMsg = new String(error.toByteArray(), Charset.defaultCharset()) 39 | if (errorMsg) { 40 | throw new GradleException(errorMsg) 41 | } 42 | return new String(output.toByteArray(), Charset.defaultCharset()) 43 | } 44 | 45 | @CompileDynamic 46 | String executeGitCommandWithErrorIgnore(Object ... args) { 47 | File rootDir = parameters.rootDir.get() 48 | ByteArrayOutputStream output = new ByteArrayOutputStream() 49 | ByteArrayOutputStream error = new ByteArrayOutputStream() 50 | List commandLineArgs = ["git", "--git-dir=${rootDir.absolutePath}/.git".toString(), "--work-tree=${rootDir.absolutePath}".toString()] 51 | commandLineArgs.addAll(args) 52 | execOperations.exec { 53 | it.setCommandLine(commandLineArgs) 54 | it.standardOutput = output 55 | it.errorOutput = error 56 | } 57 | String result = new String(output.toByteArray(), Charset.defaultCharset()) 58 | if(result) { 59 | return result 60 | } 61 | def errorMsg = new String(error.toByteArray(), Charset.defaultCharset()) 62 | if(errorMsg) { 63 | throw new GradleException(errorMsg) 64 | } 65 | } 66 | } 67 | 68 | abstract class UsernameFromLog extends GitReadCommand { 69 | @Override 70 | String obtain() { 71 | try { 72 | return executeGitCommand( "--no-pager", "log","--format=format:%an", "-n", "1") 73 | .replaceAll("\n", "").trim() 74 | } catch (Exception e) { 75 | return null 76 | } 77 | } 78 | } 79 | 80 | abstract class EmailFromLog extends GitReadCommand { 81 | @Override 82 | String obtain() { 83 | try { 84 | return executeGitCommand( "--no-pager", "log","--format=format:%ae", "-n", "1") 85 | .replaceAll("\n", "").trim() 86 | } catch (Exception e) { 87 | return null 88 | } 89 | } 90 | } 91 | /** 92 | * Returns current branch name 93 | * ex. git rev-parse --abbrev-ref HEAD -> configuration-cache-support 94 | */ 95 | abstract class CurrentBranch extends GitReadCommand { 96 | @Override 97 | String obtain() { 98 | try { 99 | return executeGitCommand( "rev-parse", "--abbrev-ref", "HEAD") 100 | .replaceAll("\n", "").trim() 101 | } catch (Exception e) { 102 | return null 103 | } 104 | } 105 | } 106 | 107 | /** 108 | * Uses git describe to find a given tag in the history of the current branch 109 | * ex. git describe HEAD --tags --match v10.0.0 -> v10.0.0-220-ga00baaa 110 | */ 111 | abstract class DescribeHeadWithTag extends GitReadCommand { 112 | @Override 113 | String obtain() { 114 | try { 115 | return executeGitCommand( "describe", "HEAD", "--tags", "--long", "--match", "v*") 116 | } catch (Exception e) { 117 | if(e.message.contains("is externally known as") || e.message.contains('warning: tag ')) { 118 | return e.message.takeBetween("tag '", "'") 119 | } 120 | return null 121 | } 122 | } 123 | } 124 | 125 | abstract class DescribeHeadWithTagWithExclude extends GitReadCommand { 126 | @Override 127 | String obtain() { 128 | try { 129 | return executeGitCommandWithErrorIgnore( "describe", "HEAD", "--tags", "--long", "--match", "v*", "--exclude", "*-rc.*") 130 | } catch (Exception e) { 131 | return null 132 | } 133 | } 134 | } 135 | /** 136 | * Uses git describe to find a given tag in the history of the current branch 137 | * ex. git describe HEAD --tags --match v10.0.0 -> v10.0.0-220-ga00baaa 138 | */ 139 | abstract class TagsPointingAt extends GitReadCommand { 140 | @Override 141 | String obtain() { 142 | try { 143 | return executeGitCommand( "tag", "--points-at", parameters.commit.get()) 144 | } catch (Exception e) { 145 | return null 146 | } 147 | } 148 | } 149 | 150 | /** 151 | * Uses git describe to find a given tag in the history of the current branch 152 | * ex. git describe HEAD --tags --match v10.0.0 -> v10.0.0-220-ga00baaa 153 | */ 154 | abstract class CommitFromTag extends GitReadCommand { 155 | @Override 156 | String obtain() { 157 | try { 158 | return executeGitCommand( "rev-list", "-n", '1', parameters.tag.get()) 159 | } catch (Exception e) { 160 | return null 161 | } 162 | } 163 | } 164 | 165 | /** 166 | * Uses to determine if a given repo has any commit 167 | */ 168 | abstract class AnyCommit extends GitReadCommand { 169 | @Override 170 | String obtain() { 171 | try { 172 | return executeGitCommand( "rev-list", "-n", "1", "--all") 173 | .replaceAll("\n", "").trim() 174 | } catch (Exception e) { 175 | return null 176 | } 177 | } 178 | } 179 | 180 | 181 | /** 182 | * Used to determine if a tag is pointing to current head 183 | * ex. git tag --points-at HEAD -> v10.0.0 184 | */ 185 | abstract class HeadTags extends GitReadCommand { 186 | 187 | @Override 188 | String obtain() { 189 | try { 190 | return executeGitCommand( "tag", "--points-at", "HEAD") 191 | } catch (Exception e) { 192 | return null 193 | } 194 | } 195 | } 196 | 197 | /** 198 | * Used to check if the current branch is behind the remote one 199 | * ex. git rev-list --count --left-only @{u}...HEAD -> 1 = behind, 0 = up-to-date 200 | */ 201 | abstract class IsCurrentBranchBehindRemote extends GitReadCommand { 202 | 203 | @Override 204 | String obtain() { 205 | try { 206 | return executeGitCommand( "rev-list", "--count", "--left-only", "@{u}...HEAD").replaceAll("\n", "").trim() 207 | } catch (Exception e) { 208 | return null 209 | } 210 | } 211 | } 212 | 213 | /** 214 | * Used to check if the current directory is a git repo 215 | * ex. git rev-parse --is-inside-work-tree -> true OR 216 | * git rev-parse --is-inside-work-tree -> fatal: not a git repository (or any of the parent directories): .git when there isn't a repo 217 | */ 218 | abstract class IsGitRepo extends GitReadCommand { 219 | 220 | @Override 221 | String obtain() { 222 | try { 223 | return !executeGitCommand( "rev-parse").contains("fatal: not a git repository") 224 | } catch (Exception e) { 225 | return false 226 | } 227 | } 228 | } 229 | 230 | /** 231 | * Used to verify if the current branch is tracking a remote branch 232 | * ex. git rev-parse --abbrev-ref --symbolic-full-name @{u} -> origin/main 233 | * ex. git rev-parse --abbrev-ref --symbolic-full-name @{u} -> fatal: no upstream configured for branch 'configuration-cache-support' when there isn't a remote branch 234 | */ 235 | abstract class IsTrackingRemoteBranch extends GitReadCommand { 236 | @Override 237 | String obtain() { 238 | try { 239 | return executeGitCommand( "rev-parse", "--abbrev-ref", "--symbolic-full-name", "@{u}") 240 | } catch (Exception e) { 241 | return null 242 | } 243 | } 244 | } 245 | 246 | /** 247 | * Returns all the tag refs for current branch 248 | * ex. git show-ref --tags can result in: 249 | * 8e6c4c925a54dbe827f043d21cd7a2a01b97fbac refs/tags/v15.3.0 250 | * b95875abf10cd3fdf5253c6be20658c2682b82e1 refs/tags/v15.3.1 251 | * dd097a0f29af8b54091a0f72521d052bc0d739dd refs/tags/v16.0.0 252 | */ 253 | abstract class RefTags extends GitReadCommand { 254 | 255 | @Override 256 | String obtain() { 257 | try { 258 | return executeGitCommand( "show-ref", "--tags") 259 | } catch (Exception e) { 260 | return null 261 | } 262 | } 263 | } 264 | 265 | /** 266 | * This returns the number of commits in HEAD without a tag. 267 | * Mostly used when we can't find tags and we want to return some information on how many commits we are behind 268 | * ex. git rev-list --count HEAD -> 578 269 | */ 270 | abstract class RevListCountHead extends GitReadCommand { 271 | 272 | @Override 273 | String obtain() { 274 | try { 275 | return executeGitCommand( "rev-list", "--count", "HEAD") 276 | } catch (Exception e) { 277 | return null 278 | } 279 | } 280 | } 281 | 282 | /** 283 | * Returns the current HEAD commit 284 | * ex. git rev-parse HEAD -> 8e6c4c925a54dbe827f043d21cd7a2a01b97fbac 285 | */ 286 | abstract class RevParseHead extends GitReadCommand { 287 | @Override 288 | String obtain() { 289 | try { 290 | return executeGitCommand( "rev-parse", "HEAD") 291 | .replaceAll("\n", "").trim() 292 | } catch (Exception e) { 293 | return null 294 | } 295 | } 296 | } 297 | 298 | /** 299 | * Returns the status of the current repo in a more machine readable format 300 | * ex. git status --porcelain -> M buildSrc/src/main/groovy/nebula/plugin/release/OverrideStrategies.groovy 301 | */ 302 | abstract class StatusPorcelain extends GitReadCommand { 303 | 304 | @Override 305 | String obtain() { 306 | try { 307 | return executeGitCommand( "status", "--porcelain") 308 | } catch (Exception e) { 309 | return null 310 | } 311 | } 312 | } 313 | 314 | /** 315 | * Retrieves a given Git config key with its value for a given scope 316 | */ 317 | abstract class GetGitConfigValue extends GitReadCommand { 318 | private static final Logger logger = LoggerFactory.getLogger(GetGitConfigValue) 319 | @Override 320 | String obtain() { 321 | try { 322 | if(parameters.getGitConfigScope().isPresent()) { 323 | return executeGitCommand( "config", parameters.getGitConfigScope().get(), "--includes", parameters.getGitConfigKey().get()) 324 | } else { 325 | return executeGitCommand( "config", parameters.getGitConfigKey().get()) 326 | } 327 | } catch (Exception e) { 328 | logger.debug("Could not get git config {} {}", parameters.getGitConfigScope().isPresent() ? parameters.gitConfigScope.get() : "", parameters.getGitConfigKey().get()) 329 | return null 330 | } 331 | } 332 | } 333 | 334 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/git/command/GitReadCommandParameters.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.release.git.command 2 | 3 | import org.gradle.api.provider.Property 4 | import org.gradle.api.provider.ValueSourceParameters 5 | 6 | interface GitReadCommandParameters extends ValueSourceParameters { 7 | Property getRootDir() 8 | Property getGitConfigScope() 9 | Property getGitConfigKey() 10 | Property getGitConfigValue() 11 | Property getCommit() 12 | Property getTag() 13 | } 14 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/git/command/GitWriteCommand.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.release.git.command 2 | 3 | import groovy.transform.CompileDynamic 4 | import org.gradle.api.GradleException 5 | import org.gradle.api.provider.ValueSource 6 | import org.gradle.process.ExecOperations 7 | 8 | import javax.inject.Inject 9 | import java.nio.charset.Charset 10 | 11 | abstract class GitWriteCommand implements ValueSource { 12 | @Inject 13 | abstract ExecOperations getExecOperations() 14 | 15 | @CompileDynamic 16 | String executeGitCommand(Object... args) { 17 | File rootDir = parameters.rootDir.get() 18 | ByteArrayOutputStream output = new ByteArrayOutputStream() 19 | ByteArrayOutputStream error = new ByteArrayOutputStream() 20 | List commandLineArgs = ["git", "--git-dir=${rootDir.absolutePath}/.git".toString(), "--work-tree=${rootDir.absolutePath}".toString()] 21 | commandLineArgs.addAll(args) 22 | execOperations.exec { 23 | it.ignoreExitValue = true 24 | it.setCommandLine(commandLineArgs) 25 | it.standardOutput = output 26 | it.errorOutput = error 27 | } 28 | def errorMsg = new String(error.toByteArray(), Charset.defaultCharset()) 29 | if (errorMsg) { 30 | throw new GradleException(errorMsg) 31 | } 32 | return new String(output.toByteArray(), Charset.defaultCharset()) 33 | } 34 | } 35 | 36 | /** 37 | * Pushes a tag to a remote 38 | */ 39 | abstract class PushTag extends GitWriteCommand { 40 | @Override 41 | String obtain() { 42 | try { 43 | return executeGitCommand("push", parameters.remote.get(), parameters.tag.get()) 44 | } catch (Exception e) { 45 | throw e 46 | } 47 | } 48 | } 49 | 50 | /** 51 | * Creates a tag with a given message 52 | */ 53 | abstract class CreateTag extends GitWriteCommand { 54 | @Override 55 | String obtain() { 56 | try { 57 | return executeGitCommand( "tag", "-a", parameters.tag.get(), "-m", parameters.tagMessage.get()) 58 | } catch (Exception e) { 59 | throw e 60 | } 61 | } 62 | } 63 | 64 | 65 | /** 66 | * Creates a tag with a given message 67 | */ 68 | abstract class FetchChanges extends GitWriteCommand { 69 | @Override 70 | String obtain() { 71 | try { 72 | return executeGitCommand( "fetch", parameters.remote.get()) 73 | } catch (Exception e) { 74 | return null 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/git/command/GitWriteCommandParameters.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.release.git.command 2 | 3 | import org.gradle.api.provider.Property 4 | import org.gradle.api.provider.ValueSourceParameters 5 | 6 | interface GitWriteCommandParameters extends ValueSourceParameters { 7 | Property getRootDir() 8 | Property getRemote() 9 | Property getTag() 10 | Property getTagMessage() 11 | } 12 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/git/command/package-info.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /** 17 | * Opinionated plugin and set of strategies build on top of the base package. 18 | */ 19 | package nebula.plugin.release.git.command 20 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/git/model/Branch.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.release.git.model 2 | 3 | import groovy.transform.Immutable 4 | import nebula.plugin.release.git.base.ShortenRefUtil 5 | 6 | @Immutable 7 | class Branch { 8 | /** 9 | * The fully qualified name of this branch. 10 | */ 11 | String fullName 12 | 13 | /** 14 | * The simple name of the branch. 15 | * @return the simple name 16 | */ 17 | String getName() { 18 | return ShortenRefUtil.shortenRefName(fullName) 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/git/model/Commit.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.release.git.model 2 | 3 | import groovy.transform.Immutable 4 | 5 | @Immutable 6 | class Commit { 7 | String id 8 | /** 9 | * The abbreviated hash of the commit. 10 | */ 11 | String abbreviatedId 12 | } 13 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/git/model/TagRef.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.release.git.model 2 | 3 | import com.github.zafarkhaja.semver.Version 4 | import groovy.transform.Sortable 5 | import nebula.plugin.release.git.base.ShortenRefUtil 6 | 7 | @Sortable(includes = ['version']) 8 | class TagRef { 9 | 10 | String name 11 | Version version 12 | String commit 13 | 14 | TagRef(String name, String commit) { 15 | this.name = name 16 | this.commit = commit 17 | this.version = parseTag(name) 18 | } 19 | 20 | TagRef(String name) { 21 | String tag = ShortenRefUtil.shortenRefName(name) 22 | this.name = tag 23 | this.version = parseTag(tag) 24 | } 25 | 26 | static fromRef(String ref) { 27 | List parts = ref.split(' ').toList() 28 | String tag = ShortenRefUtil.shortenRefName(parts[1]) 29 | return new TagRef(tag, parts[0]) 30 | } 31 | 32 | 33 | private static Version parseTag(String name) { 34 | try { 35 | Version.valueOf(name[0] == 'v' ? name[1..-1] : name) 36 | } catch (Exception e) { 37 | null 38 | } 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/git/model/package-info.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /** 17 | * Opinionated plugin and set of strategies build on top of the base package. 18 | */ 19 | package nebula.plugin.release.git.model 20 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/git/opinion/TimestampPrecision.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.release.git.opinion 2 | 3 | import java.time.ZoneOffset 4 | import java.time.format.DateTimeFormatter 5 | 6 | enum TimestampPrecision { 7 | DAYS(createDateTimeFormatter("yyyyMMdd")), 8 | HOURS(createDateTimeFormatter("yyyyMMddHH")), 9 | MINUTES(createDateTimeFormatter("yyyyMMddHHmm")), 10 | SECONDS(createDateTimeFormatter("yyyyMMddHHmmss")), 11 | MILLISECONDS(createDateTimeFormatter("YYYYMMddHHmmssSSS")) 12 | 13 | final DateTimeFormatter dateTimeFormatter 14 | 15 | private TimestampPrecision(DateTimeFormatter formatter) { 16 | this.dateTimeFormatter = formatter 17 | } 18 | 19 | static createDateTimeFormatter(String format) { 20 | return DateTimeFormatter 21 | .ofPattern(format) 22 | .withZone(ZoneOffset.UTC) 23 | } 24 | 25 | static TimestampPrecision from(String precision) { 26 | try { 27 | return valueOf(precision.toUpperCase()) 28 | } catch (Exception e) { 29 | return MINUTES 30 | } 31 | } 32 | } -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/git/opinion/TimestampUtil.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2019 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release.git.opinion 17 | 18 | import java.time.Instant 19 | import java.time.LocalDateTime 20 | import java.time.ZoneOffset 21 | 22 | class TimestampUtil { 23 | 24 | static String getUTCFormattedTimestamp() { 25 | return getUTCFormattedTimestamp(TimestampPrecision.MINUTES) 26 | } 27 | 28 | static String getUTCFormattedTimestamp(TimestampPrecision timestampPrecision) { 29 | return LocalDateTime 30 | .ofInstant(Instant.now(), ZoneOffset.UTC) 31 | .format(timestampPrecision.dateTimeFormatter) 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/git/opinion/package-info.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /** 17 | * Opinionated plugin and set of strategies build on top of the base package. 18 | */ 19 | package nebula.plugin.release.git.opinion 20 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/git/semver/ChangeScope.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release.git.semver 17 | 18 | enum ChangeScope { 19 | MAJOR, 20 | MINOR, 21 | PATCH 22 | } 23 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/git/semver/NearestVersion.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release.git.semver 17 | 18 | import groovy.transform.Immutable 19 | 20 | import com.github.zafarkhaja.semver.Version 21 | 22 | /** 23 | * Nearest version tags reachable from the current HEAD. The version 0.0.0 24 | * will be returned for any 25 | * @since 0.8.0 26 | */ 27 | @Immutable(knownImmutableClasses=[Version]) 28 | class NearestVersion { 29 | /** 30 | * The nearest version that is tagged. 31 | */ 32 | Version any 33 | 34 | /** 35 | * The nearest normal (i.e. non-prerelease) version that is tagged. 36 | */ 37 | Version normal 38 | 39 | /** 40 | * The number of commits since {@code any} reachable from HEAD. 41 | */ 42 | int distanceFromAny 43 | 44 | /** 45 | * The number of commits since {@code normal} reachable from HEAD. 46 | */ 47 | int distanceFromNormal 48 | } 49 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/git/semver/NearestVersionLocator.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release.git.semver 17 | 18 | import com.github.zafarkhaja.semver.Version 19 | import groovy.transform.CompileDynamic 20 | import nebula.plugin.release.git.GitBuildService 21 | import nebula.plugin.release.git.base.TagStrategy 22 | import org.gradle.api.GradleException 23 | import org.slf4j.Logger 24 | import org.slf4j.LoggerFactory 25 | 26 | /** 27 | * Locates the nearest Tag whose names can be 28 | * parsed as a {@link com.github.zafarkhaja.semver.Version version}. Both the 29 | * absolute nearest version tag and the nearest "normal version" tag are 30 | * included. 31 | * 32 | *

33 | * Primarily used as part of version inference to determine the previous 34 | * version. 35 | *

36 | * 37 | * @since 0.8.0 38 | */ 39 | @CompileDynamic 40 | class NearestVersionLocator { 41 | private static final Logger logger = LoggerFactory.getLogger(NearestVersionLocator) 42 | private static final Version UNKNOWN = Version.valueOf('0.0.0') 43 | 44 | final TagStrategy strategy 45 | final GitBuildService gitBuildService 46 | 47 | NearestVersionLocator(GitBuildService gitBuildService, TagStrategy strategy) { 48 | this.strategy = strategy 49 | this.gitBuildService = gitBuildService 50 | } 51 | 52 | /** 53 | * Locate the nearest version in the given repository 54 | * starting from the current HEAD. 55 | * 56 | *

57 | * The nearest tag is determined by getting a commit log between 58 | * the tag and {@code HEAD}. The version tag with the smallest 59 | * log from a pure count of commits will have its version returned. If two 60 | * version tags have a log of the same size, the versions will be compared 61 | * to find the one with the highest precedence according to semver rules. 62 | * For example, {@code 1.0.0} has higher precedence than {@code 1.0.0-rc.2}. 63 | * For tags with logs of the same size and versions of the same precedence 64 | * it is undefined which will be returned. 65 | *

66 | * 67 | *

68 | * Two versions will be returned: the "any" version and the "normal" version. 69 | * "Any" is the absolute nearest tagged version. "Normal" is the nearest 70 | * tagged version that does not include a pre-release segment. 71 | *

72 | * 73 | * Defaults to {@code HEAD}. 74 | * @return the version corresponding to the nearest tag 75 | */ 76 | NearestVersion locate() { 77 | logger.debug('Locate beginning on branch: {}', gitBuildService.currentBranch) 78 | def normal = getLatestTagWithDistance(true) 79 | def any = getLatestTagWithDistance(false) 80 | logger.debug('Nearest release: {}, nearest any: {}.', normal, any) 81 | return new NearestVersion(any.version, normal.version, any.distance, normal.distance) 82 | } 83 | 84 | private getLatestTagWithDistance(boolean excludePreReleases) { 85 | try { 86 | String result = gitBuildService.describeHeadWithTags(excludePreReleases) 87 | if(!result) { 88 | return [version: UNKNOWN, distance: gitBuildService.getCommitCountForHead()] 89 | } 90 | 91 | String[] parts = result.split('-') 92 | if(parts.size() < 3) { 93 | return [version: parseTag(parts[0], true), distance: 0] 94 | } 95 | 96 | String foundTag = parts.size() == 4 ? parts[0..1].join('-') : parts[0] 97 | String commit = gitBuildService.findCommitForTag(foundTag) 98 | List allTagsForCommit = gitBuildService.getTagsPointingAt(commit).collect { 99 | parseTag(it) 100 | }.findAll { 101 | it && excludePreReleases ? !it.preReleaseVersion : true 102 | } 103 | 104 | if(!allTagsForCommit || allTagsForCommit.every { !it }) { 105 | Version version = parseTag(foundTag, true) 106 | if(version.preReleaseVersion && excludePreReleases) { 107 | return [version: UNKNOWN, distance: gitBuildService.getCommitCountForHead()] 108 | } 109 | return [version: parseTag(foundTag, true), distance: parts[parts.size() - 2]?.toInteger()] 110 | } 111 | 112 | def highest = allTagsForCommit.min { a, b -> 113 | (a <=> b) * -1 114 | } 115 | return [version: highest, distance: parts[parts.size() - 2]?.toInteger()] 116 | } catch (Exception e) { 117 | return [version: UNKNOWN, distance: gitBuildService.getCommitCountForHead()] 118 | } 119 | } 120 | 121 | private static Version parseTag(String name, boolean failOnInvalid = false) { 122 | try { 123 | Version.valueOf(name[0] == 'v' ? name[1..-1] : name) 124 | } catch (Exception e) { 125 | if(!failOnInvalid) { 126 | return null 127 | } 128 | 129 | throw new GradleException("Current commit has following tags: ${name} but they were not recognized as valid versions" ) 130 | } 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/git/semver/PartialSemVerStrategy.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release.git.semver 17 | 18 | /** 19 | * Strategy to infer portions of a semantic version. 20 | * @see SemVerStrategy 21 | */ 22 | interface PartialSemVerStrategy { 23 | /** 24 | * Infers a portion of a semantic version and returns the new state 25 | * to be used as inference continues. 26 | */ 27 | SemVerStrategyState infer(SemVerStrategyState state) 28 | } 29 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/git/semver/RebuildVersionStrategy.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release.git.semver 17 | 18 | import groovy.transform.CompileDynamic 19 | import nebula.plugin.release.git.GitBuildService 20 | import nebula.plugin.release.git.base.ReleaseVersion 21 | import nebula.plugin.release.git.base.VersionStrategy 22 | import org.gradle.api.Project 23 | import org.slf4j.Logger 24 | import org.slf4j.LoggerFactory 25 | 26 | /** 27 | * Strategy that infers the version based on the tag on the current 28 | * HEAD. 29 | */ 30 | @CompileDynamic 31 | class RebuildVersionStrategy implements VersionStrategy { 32 | private static final Logger logger = LoggerFactory.getLogger(RebuildVersionStrategy) 33 | public static final RebuildVersionStrategy INSTANCE = new RebuildVersionStrategy() 34 | 35 | private RebuildVersionStrategy() { 36 | // just hiding the constructor 37 | } 38 | 39 | @Override 40 | String getName() { 41 | return 'rebuild' 42 | } 43 | 44 | /** 45 | * Determines whether this strategy should be used to infer the version. 46 | *
    47 | *
  • Return {@code false}, if any project properties starting with "release." are set.
  • 48 | *
  • Return {@code false}, if there aren't any tags on the current HEAD that can be parsed as a version.
  • 49 | *
  • Return {@code true}, otherwise.
  • 50 | *
51 | */ 52 | @Override 53 | boolean selector(Project project, GitBuildService gitBuildService) { 54 | def clean = gitBuildService.isCleanStatus() 55 | def propsSpecified = project.hasProperty(SemVerStrategy.SCOPE_PROP) || project.hasProperty(SemVerStrategy.STAGE_PROP) 56 | def headVersion = getHeadVersion(gitBuildService) 57 | 58 | if (clean && !propsSpecified && headVersion) { 59 | logger.info('Using {} strategy because repo is clean, no "release." properties found and head version is {}', name, headVersion) 60 | return true 61 | } else { 62 | logger.info('Skipping {} strategy because clean is {}, "release." properties are {} and head version is {}', name, clean, propsSpecified, headVersion) 63 | return false 64 | } 65 | } 66 | 67 | /** 68 | * Infers the version based on the version tag on the current HEAD with the 69 | * highest precendence. 70 | */ 71 | @Override 72 | ReleaseVersion infer(Project project, GitBuildService gitBuildService) { 73 | String version = getHeadVersion(gitBuildService) 74 | def releaseVersion = new ReleaseVersion(version, version, false) 75 | logger.debug('Inferred version {} by strategy {}', releaseVersion, name) 76 | return releaseVersion 77 | } 78 | 79 | 80 | private String getHeadVersion(GitBuildService gitBuildService) { 81 | return gitBuildService.headTags().findAll { 82 | it != null 83 | }.max()?.version?.toString() 84 | } 85 | } 86 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/git/semver/SemVerStrategy.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release.git.semver 17 | 18 | import groovy.transform.CompileDynamic 19 | import groovy.transform.Immutable 20 | import groovy.transform.PackageScope 21 | 22 | import com.github.zafarkhaja.semver.Version 23 | import nebula.plugin.release.FeatureFlags 24 | import nebula.plugin.release.VersionSanitizerUtil 25 | import nebula.plugin.release.git.GitBuildService 26 | import nebula.plugin.release.git.base.DefaultVersionStrategy 27 | import nebula.plugin.release.git.base.ReleasePluginExtension 28 | import nebula.plugin.release.git.base.ReleaseVersion 29 | import nebula.plugin.release.git.model.Branch 30 | import nebula.plugin.release.git.model.Commit 31 | import org.gradle.api.GradleException 32 | import org.gradle.api.Project 33 | 34 | import org.slf4j.Logger 35 | import org.slf4j.LoggerFactory 36 | 37 | /** 38 | * Strategy to infer versions that comply with Semantic Versioning. 39 | * @see PartialSemVerStrategy 40 | * @see SemVerStrategyState 41 | * @see Wiki Doc 42 | */ 43 | @CompileDynamic 44 | @Immutable(copyWith=true, knownImmutableClasses=[PartialSemVerStrategy]) 45 | final class SemVerStrategy implements DefaultVersionStrategy { 46 | private static final Logger logger = LoggerFactory.getLogger(SemVerStrategy) 47 | static final String SCOPE_PROP = 'release.scope' 48 | static final String STAGE_PROP = 'release.stage' 49 | 50 | /** 51 | * The name of the strategy. 52 | */ 53 | String name 54 | 55 | /** 56 | * The stages supported by this strategy. 57 | */ 58 | SortedSet stages 59 | 60 | /** 61 | * Whether or not this strategy can be used if the repo has uncommited changes. 62 | */ 63 | boolean allowDirtyRepo 64 | 65 | /** 66 | * The strategy used to infer the normal component of the version. There is no enforcement that 67 | * this strategy only modify that part of the state. 68 | */ 69 | PartialSemVerStrategy normalStrategy 70 | 71 | /** 72 | * The strategy used to infer the pre-release component of the version. There is no enforcement that 73 | * this strategy only modify that part of the state. 74 | */ 75 | PartialSemVerStrategy preReleaseStrategy 76 | 77 | /** 78 | * The strategy used to infer the build metadata component of the version. There is no enforcement that 79 | * this strategy only modify that part of the state. 80 | */ 81 | PartialSemVerStrategy buildMetadataStrategy 82 | 83 | /** 84 | * Whether or not to create tags for versions inferred by this strategy. 85 | */ 86 | boolean createTag 87 | 88 | /** 89 | * Whether or not to enforce that versions inferred by this strategy are of higher precedence 90 | * than the nearest any. 91 | */ 92 | boolean enforcePrecedence 93 | 94 | @Override 95 | boolean defaultSelector(Project project, GitBuildService gitBuildService) { 96 | String stage = getPropertyOrNull(project, STAGE_PROP) 97 | if (stage != null && !stages.contains(stage)) { 98 | logger.info('Skipping {} default strategy because stage ({}) is not one of: {}', name, stage, stages) 99 | return false 100 | } else if (!allowDirtyRepo && !gitBuildService.isCleanStatus()) { 101 | logger.info('Skipping {} default strategy because repo is dirty.', name) 102 | return false 103 | } else { 104 | String status = gitBuildService.isCleanStatus() ? 'clean' : 'dirty' 105 | logger.info('Using {} default strategy because repo is {} and no stage defined', name, status) 106 | return true 107 | } 108 | } 109 | 110 | 111 | /** 112 | * Determines whether this strategy should be used to infer the version. 113 | *
    114 | *
  • Return {@code false}, if the {@code release.stage} is not one listed in the {@code stages} property.
  • 115 | *
  • Return {@code false}, if the repository has uncommitted changes and {@code allowDirtyRepo} is {@code false}.
  • 116 | *
  • Return {@code true}, otherwise.
  • 117 | *
118 | */ 119 | @Override 120 | boolean selector(Project project, GitBuildService gitBuildService) { 121 | String stage = getPropertyOrNull(project, STAGE_PROP) 122 | if (stage == null || !stages.contains(stage)) { 123 | logger.info('Skipping {} strategy because stage ({}) is not one of: {}', name, stage, stages) 124 | return false 125 | } else if (!allowDirtyRepo && !gitBuildService.isCleanStatus()) { 126 | logger.info('Skipping {} strategy because repo is dirty.', name) 127 | return false 128 | } else { 129 | logger.info('Using {} strategy because repo is not dirty (or allowed to be dirty) and stage ({}) is one of: {}', name, stage, stages) 130 | return true 131 | } 132 | } 133 | 134 | /** 135 | * Infers the version to use for this build. Uses the normal, pre-release, and build metadata 136 | * strategies in order to infer the version. If the {@code release.stage} is not set, uses the 137 | * first value in the {@code stages} set (i.e. the one with the lowest precedence). After inferring 138 | * the version precedence will be enforced, if required by this strategy. 139 | */ 140 | @CompileDynamic 141 | @Override 142 | ReleaseVersion infer(Project project, GitBuildService gitBuildService) { 143 | def tagStrategy = project.extensions.getByType(ReleasePluginExtension).tagStrategy 144 | return doInfer(project, gitBuildService, new NearestVersionLocator(gitBuildService, tagStrategy)) 145 | } 146 | 147 | @PackageScope 148 | ReleaseVersion doInfer(Project project, GitBuildService gitBuildService, NearestVersionLocator locator) { 149 | ChangeScope scope = getPropertyOrNull(project, SCOPE_PROP).with { scope -> 150 | scope == null ? null : ChangeScope.valueOf(scope.toUpperCase()) 151 | } 152 | String stage = getPropertyOrNull(project, STAGE_PROP) ?: stages.first() 153 | if (!stages.contains(stage)) { 154 | throw new GradleException("Stage ${stage} is not one of ${stages} allowed for strategy ${name}.") 155 | } 156 | logger.info('Beginning version inference using {} strategy and input scope ({}) and stage ({})', name, scope, stage) 157 | 158 | NearestVersion nearestVersion = locator.locate() 159 | logger.debug('Located nearest version: {}', nearestVersion) 160 | 161 | String currentHead = gitBuildService.head 162 | SemVerStrategyState state = new SemVerStrategyState( 163 | timestampPrecision: FeatureFlags.immutableSnapshotTimestampPrecision(project), 164 | scopeFromProp: scope, 165 | stageFromProp: stage, 166 | currentHead: new Commit(id: currentHead, abbreviatedId: currentHead.take(7)), 167 | currentBranch: new Branch(fullName: gitBuildService.currentBranch), 168 | repoDirty: !gitBuildService.cleanStatus, 169 | nearestVersion: nearestVersion 170 | ) 171 | 172 | Version version = StrategyUtil.all( 173 | normalStrategy, preReleaseStrategy, buildMetadataStrategy).infer(state).toVersion() 174 | 175 | 176 | String versionAsString = version.toString() 177 | if(VersionSanitizerUtil.hasSanitizeFlag(project)) { 178 | versionAsString = VersionSanitizerUtil.sanitize(version.toString()) 179 | } 180 | 181 | logger.warn('Inferred project: {}, version: {}', project.name, versionAsString) 182 | 183 | if (enforcePrecedence && version < nearestVersion.any) { 184 | throw new GradleException("Based on previous tags in this branch the nearest version is ${nearestVersion.any} You're attempting to release ${version} based on the tag recently pushed. Please look at https://github.com/nebula-plugins/nebula-release-plugin/wiki/Error-Messages-Explained#orggradleapigradleexception-inferred-version-cannot-be-lower-than-nearest-required-by-selected-strategy") 185 | } 186 | 187 | 188 | return new ReleaseVersion(versionAsString, nearestVersion.normal.toString(), createTag) 189 | } 190 | 191 | private static String getPropertyOrNull(Project project, String name) { 192 | return project.hasProperty(name) ? project.property(name) : null 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/git/semver/SemVerStrategyState.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release.git.semver 17 | 18 | import com.github.zafarkhaja.semver.Version 19 | import groovy.transform.CompileDynamic 20 | 21 | import groovy.transform.Immutable 22 | import groovy.transform.ToString 23 | import nebula.plugin.release.git.model.Branch 24 | import nebula.plugin.release.git.model.Commit 25 | import nebula.plugin.release.git.opinion.TimestampPrecision 26 | 27 | /** 28 | * Working state used by {@link PartialSemVerStrategy}. 29 | */ 30 | @Immutable(copyWith = true, knownImmutableClasses = [Commit, Branch, NearestVersion]) 31 | @ToString(includeNames = true, excludes = ['currentHead']) 32 | @CompileDynamic 33 | final class SemVerStrategyState { 34 | ChangeScope scopeFromProp 35 | String stageFromProp 36 | Commit currentHead 37 | Branch currentBranch 38 | boolean repoDirty 39 | NearestVersion nearestVersion 40 | String inferredNormal 41 | String inferredPreRelease 42 | String inferredBuildMetadata 43 | TimestampPrecision timestampPrecision 44 | 45 | Version toVersion() { 46 | return new Version.Builder().with { 47 | normalVersion = inferredNormal 48 | preReleaseVersion = inferredPreRelease 49 | buildMetadata = inferredBuildMetadata 50 | build() 51 | } 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/git/semver/StrategyUtil.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release.git.semver 17 | 18 | import groovy.transform.CompileDynamic 19 | 20 | /** 21 | * Utility class to more easily create {@link PartialSemVerStrategy} instances. 22 | */ 23 | final class StrategyUtil { 24 | private StrategyUtil() { 25 | throw new AssertionError('Cannot instantiate this class.' as Object) 26 | } 27 | 28 | /** 29 | * Creates a strategy backed by the given closure. It should accept and return 30 | * a {@link SemVerStrategyState}. 31 | */ 32 | static final PartialSemVerStrategy closure(Closure behavior) { 33 | return new ClosureBackedPartialSemVerStrategy(behavior) 34 | } 35 | 36 | /** 37 | * Creates a strategy that applies all of the given strategies in order. 38 | */ 39 | static final PartialSemVerStrategy all(PartialSemVerStrategy... strategies) { 40 | return new ApplyAllChainedPartialSemVerStrategy(strategies as List) 41 | } 42 | 43 | /** 44 | * Creates a strategy that applies each strategy in order, until one changes 45 | * the state, which is then returned. 46 | */ 47 | static final PartialSemVerStrategy one(PartialSemVerStrategy... strategies) { 48 | return new ChooseOneChainedPartialSemVerStrategy(strategies as List) 49 | } 50 | 51 | /** 52 | * Returns the int value of a string or returns 0 if it cannot be parsed. 53 | */ 54 | static final int parseIntOrZero(String str) { 55 | try { 56 | return Integer.parseInt(str) 57 | } catch (NumberFormatException e) { 58 | return 0 59 | } 60 | } 61 | 62 | /** 63 | * Increments the nearest normal version using the specified scope. 64 | */ 65 | @CompileDynamic 66 | static final SemVerStrategyState incrementNormalFromScope(SemVerStrategyState state, ChangeScope scope) { 67 | def oldNormal = state.nearestVersion.normal 68 | switch (scope) { 69 | case ChangeScope.MAJOR: 70 | return state.copyWith(inferredNormal: oldNormal.incrementMajorVersion()) 71 | case ChangeScope.MINOR: 72 | return state.copyWith(inferredNormal: oldNormal.incrementMinorVersion()) 73 | case ChangeScope.PATCH: 74 | return state.copyWith(inferredNormal: oldNormal.incrementPatchVersion()) 75 | default: 76 | return state 77 | } 78 | } 79 | 80 | private static class ClosureBackedPartialSemVerStrategy implements PartialSemVerStrategy { 81 | private final Closure behavior 82 | 83 | ClosureBackedPartialSemVerStrategy(Closure behavior) { 84 | this.behavior = behavior 85 | } 86 | 87 | @Override 88 | SemVerStrategyState infer(SemVerStrategyState state) { 89 | return behavior(state) 90 | } 91 | } 92 | 93 | private static class ApplyAllChainedPartialSemVerStrategy implements PartialSemVerStrategy { 94 | private final List strategies 95 | 96 | ApplyAllChainedPartialSemVerStrategy(List strategies) { 97 | this.strategies = strategies 98 | } 99 | 100 | @Override 101 | SemVerStrategyState infer(SemVerStrategyState initialState) { 102 | return strategies.inject(initialState) { state, strategy -> 103 | strategy.infer(state) 104 | } 105 | } 106 | } 107 | 108 | private static class ChooseOneChainedPartialSemVerStrategy implements PartialSemVerStrategy { 109 | private final List strategies 110 | 111 | ChooseOneChainedPartialSemVerStrategy(List strategies) { 112 | this.strategies = strategies 113 | } 114 | 115 | @Override 116 | SemVerStrategyState infer(SemVerStrategyState oldState) { 117 | def result = strategies.findResult { strategy -> 118 | def newState = strategy.infer(oldState) 119 | oldState == newState ? null : newState 120 | } 121 | return result ?: oldState 122 | } 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/git/semver/package-info.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | /** 17 | * Support for version strategies that support semantic versioning. 18 | */ 19 | package nebula.plugin.release.git.semver 20 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/util/ConfigureUtil.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.release.util 2 | 3 | import groovy.transform.CompileDynamic 4 | import org.codehaus.groovy.runtime.GeneratedClosure 5 | import org.gradle.internal.metaobject.ConfigureDelegate 6 | import org.gradle.util.Configurable 7 | import org.gradle.util.internal.ClosureBackedAction 8 | 9 | import javax.annotation.Nullable 10 | 11 | class ConfigureUtil { 12 | public static final int DELEGATE_FIRST = 1 13 | public static final int OWNER_ONLY = 2 14 | 15 | /** 16 | *

Configures {@code target} with {@code configureClosure}, via the {@link Configurable} interface if necessary.

17 | * 18 | *

If {@code target} does not implement {@link Configurable} interface, it is set as the delegate of a clone of 19 | * {@code configureClosure} with a resolve strategy of {@code DELEGATE_FIRST}.

20 | * 21 | *

If {@code target} does implement the {@link Configurable} interface, the {@code configureClosure} will be passed to 22 | * {@code delegate}'s {@link Configurable#configure(Closure)} method.

23 | * 24 | * @param configureClosure The configuration closure 25 | * @param target The object to be configured 26 | * @return The delegate param 27 | */ 28 | static T configure(@Nullable Closure configureClosure, T target) { 29 | if (configureClosure == null) { 30 | return target 31 | } 32 | 33 | if (target instanceof Configurable) { 34 | ((Configurable) target).configure(configureClosure) 35 | } else { 36 | configureTarget(configureClosure, target, new ConfigureDelegate(configureClosure, target)) 37 | } 38 | 39 | return target 40 | } 41 | 42 | @CompileDynamic 43 | private static void configureTarget(Closure configureClosure, T target, ConfigureDelegate closureDelegate) { 44 | if (!(configureClosure instanceof GeneratedClosure)) { 45 | new ClosureBackedAction(configureClosure, DELEGATE_FIRST, false).execute(target) 46 | return; 47 | } 48 | 49 | // Hackery to make closure execution faster, by short-circuiting the expensive property and method lookup on Closure 50 | Closure withNewOwner = configureClosure.rehydrate(target, closureDelegate, configureClosure.getThisObject()) 51 | new ClosureBackedAction(withNewOwner, OWNER_ONLY, false).execute(target) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/util/ObjectUtil.java: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release.util; 17 | 18 | import java.util.concurrent.Callable; 19 | 20 | import groovy.lang.Closure; 21 | 22 | 23 | /** 24 | * Utility class for general {@code Object} related operations. 25 | * @since 0.1.0 26 | */ 27 | public final class ObjectUtil { 28 | /** 29 | * Cannot instantiate 30 | * @throws AssertionError always 31 | */ 32 | private ObjectUtil() { 33 | throw new AssertionError("Cannot instantiate this class"); 34 | } 35 | 36 | /** 37 | * Unpacks the given object by recursively 38 | * calling the {@code call()} method if the 39 | * object is a {@code Closure} or {@code Callable}. 40 | * @param obj the object to unpack 41 | * @return the unpacked value of the object 42 | */ 43 | @SuppressWarnings("rawtypes") 44 | public static Object unpack(Object obj) { 45 | Object value = obj; 46 | while (value != null) { 47 | if (value instanceof Closure) { 48 | value = ((Closure) value).call(); 49 | } else if (value instanceof Callable) { 50 | try { 51 | value = ((Callable) value).call(); 52 | } catch (Exception e) { 53 | throw new RuntimeException(e); 54 | } 55 | } else { 56 | return value; 57 | } 58 | } 59 | return value; 60 | } 61 | 62 | /** 63 | * Unpacks the given object to its {@code String} 64 | * value. Same behavior as the other {@code unpack} 65 | * method ending with a call to {@code toString()}. 66 | * @param obj the value to unpack 67 | * @return the unpacked string value 68 | * @see ObjectUtil#unpack(Object) 69 | */ 70 | public static String unpackString(Object obj) { 71 | Object value = unpack(obj); 72 | return value == null ? null : value.toString(); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/groovy/nebula/plugin/release/util/ReleaseTasksUtil.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.release.util 2 | 3 | import org.gradle.api.Project 4 | 5 | class ReleaseTasksUtil { 6 | 7 | static final String SNAPSHOT_TASK_NAME = 'snapshot' 8 | static final String SNAPSHOT_TASK_NAME_OPTIONAL_COLON = ":$SNAPSHOT_TASK_NAME" 9 | static final String SNAPSHOT_SETUP_TASK_NAME = 'snapshotSetup' 10 | static final String DEV_SNAPSHOT_TASK_NAME = 'devSnapshot' 11 | static final String DEV_SNAPSHOT_SETUP_TASK_NAME = 'devSnapshotSetup' 12 | static final String DEV_SNAPSHOT_TASK_NAME_OPTIONAL_COLON = ":$DEV_SNAPSHOT_TASK_NAME" 13 | static final String DEV_SNAPSHOT_SETUP_TASK_NAME_OPTIONAL_COLON = ":$DEV_SNAPSHOT_SETUP_TASK_NAME" 14 | static final String IMMUTABLE_SNAPSHOT_TASK_NAME = 'immutableSnapshot' 15 | static final String IMMUTABLE_SNAPSHOT_SETUP_TASK_NAME = 'immutableSnapshotSetup' 16 | static final String IMMUTABLE_SNAPSHOT_TASK_NAME_OPTIONAL_COLON = ":$IMMUTABLE_SNAPSHOT_TASK_NAME" 17 | static final String CANDIDATE_TASK_NAME = 'candidate' 18 | static final String CANDIDATE_TASK_NAME_OPTIONAL_COLON = ":$CANDIDATE_TASK_NAME" 19 | static final String CANDIDATE_SETUP_TASK_NAME = 'candidateSetup' 20 | static final String FINAL_TASK_NAME = 'final' 21 | static final String FINAL_TASK_NAME_WITH_OPTIONAL_COLON = ":$FINAL_TASK_NAME" 22 | static final String FINAL_SETUP_TASK_NAME = 'finalSetup' 23 | static final String RELEASE_CHECK_TASK_NAME = 'releaseCheck' 24 | static final String NEBULA_RELEASE_EXTENSION_NAME = 'nebulaRelease' 25 | static final String POST_RELEASE_TASK_NAME = 'postRelease' 26 | static final String USE_LAST_TAG_PROPERTY = 'release.useLastTag' 27 | 28 | static boolean isUsingLatestTag(Project project) { 29 | return project.hasProperty(USE_LAST_TAG_PROPERTY) && project.property(USE_LAST_TAG_PROPERTY).toString().toBoolean() 30 | } 31 | 32 | static boolean isReleaseTaskThatRequiresTagging(List cliTasks) { 33 | def hasCandidate = cliTasks.contains(CANDIDATE_TASK_NAME) || cliTasks.contains(CANDIDATE_TASK_NAME_OPTIONAL_COLON) 34 | def hasFinal = cliTasks.contains(FINAL_TASK_NAME) || cliTasks.contains(FINAL_TASK_NAME_WITH_OPTIONAL_COLON) 35 | return hasCandidate || hasFinal 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/test/groovy/nebula/plugin/release/ErrorMessageFormatterSpec.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.release 2 | 3 | import org.ajoberstar.grgit.Status 4 | import spock.lang.Specification 5 | 6 | class ErrorMessageFormatterSpec extends Specification { 7 | ErrorMessageFormatter formatter = new ErrorMessageFormatter() 8 | 9 | def 'should not have a message for clean status'() { 10 | given: 11 | def status = "" 12 | 13 | expect: 14 | formatter.format(status) == "" 15 | } 16 | 17 | def 'should list pending changes'() { 18 | given: 19 | def status = """ 20 | M src/main/groovy/nebula/plugin/release/ErrorMessageFormatter.groovy 21 | M src/main/groovy/nebula/plugin/release/ReleasePlugin.groovy 22 | AM src/main/groovy/nebula/plugin/release/git/GitProviders.groovy 23 | M src/test/groovy/nebula/plugin/release/ErrorMessageFormatterSpec.groovy 24 | """ 25 | String expected = [ 26 | ErrorMessageFormatter.ROOT_CAUSE, 27 | " M src/main/groovy/nebula/plugin/release/ErrorMessageFormatter.groovy\n" + 28 | " M src/main/groovy/nebula/plugin/release/ReleasePlugin.groovy\n" + 29 | "AM src/main/groovy/nebula/plugin/release/git/GitProviders.groovy\n" + 30 | " M src/test/groovy/nebula/plugin/release/ErrorMessageFormatterSpec.groovy\n", 31 | ].join(sprintf(ErrorMessageFormatter.NEW_LINE)) 32 | 33 | when: 34 | String message = formatter.format(status) 35 | 36 | then: 37 | message.stripIndent().replaceAll("\n",'') == expected.stripIndent().replaceAll("\n",'') 38 | } 39 | 40 | } 41 | -------------------------------------------------------------------------------- /src/test/groovy/nebula/plugin/release/OverrideStrategiesSpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2014-2015 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release 17 | 18 | import nebula.test.ProjectSpec 19 | import nebula.plugin.release.git.base.BaseReleasePlugin 20 | import nebula.plugin.release.git.base.ReleasePluginExtension 21 | 22 | class OverrideStrategiesSpec extends ProjectSpec { 23 | def 'able to set via gradle property'() { 24 | setup: 25 | project.ext.set('release.version', '42.5.0') 26 | 27 | when: 28 | project.plugins.apply(BaseReleasePlugin) 29 | def releaseExtension = project.extensions.findByType(ReleasePluginExtension) 30 | releaseExtension.with { 31 | versionStrategy new OverrideStrategies.GradlePropertyStrategy(project, 'release.version') 32 | } 33 | 34 | then: 35 | project.version.toString() == '42.5.0' 36 | } 37 | 38 | def 'able to set via gradle property and sanitize'() { 39 | setup: 40 | project.ext.set('release.version', '42.5.0-rc.1+feature') 41 | project.ext.set('release.sanitizeVersion', true) 42 | 43 | when: 44 | project.plugins.apply(BaseReleasePlugin) 45 | def releaseExtension = project.extensions.findByType(ReleasePluginExtension) 46 | releaseExtension.with { 47 | versionStrategy new OverrideStrategies.GradlePropertyStrategy(project, 'release.version') 48 | } 49 | 50 | then: 51 | project.version.toString() == '42.5.0-rc.1.feature' 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/test/groovy/nebula/plugin/release/ReleasePluginConfigurationAvoidanceSpec.groovy: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright 2016 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release 17 | 18 | import nebula.test.ProjectSpec 19 | import nebula.plugin.release.git.base.BaseReleasePlugin 20 | import nebula.plugin.release.git.base.ReleasePluginExtension 21 | 22 | class ReleasePluginConfigurationAvoidanceSpec extends ProjectSpec { 23 | def 'able to configure tasks lazily'() { 24 | setup: 25 | project.tasks.register('sync') { 26 | throw new Exception('Should not be configured') 27 | } 28 | 29 | when: 30 | project.plugins.apply(BaseReleasePlugin) 31 | 32 | then: 33 | notThrown(Exception) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/test/groovy/nebula/plugin/release/ReleasePluginOptionalDepsSpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2015 Netflix, Inc. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release 17 | 18 | import nebula.test.ProjectSpec 19 | import org.ajoberstar.grgit.Grgit 20 | import spock.lang.Unroll 21 | 22 | class ReleasePluginOptionalDepsSpec extends ProjectSpec { 23 | Grgit git 24 | 25 | def setup() { 26 | git = Grgit.init(dir: projectDir) 27 | git.commit(message: 'initial commit') 28 | git.tag.add(name: 'v0.0.1') 29 | } 30 | 31 | @Unroll('verify isClassPresent determines #className #presenceString present') 32 | def 'verify isClassPresent'() { 33 | given: 34 | def myPlugin = project.plugins.apply(ReleasePlugin) 35 | 36 | expect: 37 | myPlugin.isClassPresent(className) == presence 38 | 39 | where: 40 | className | presence 41 | 'example.nebula.DoesNotExist' | false 42 | 'java.util.List' | true 43 | 44 | presenceString = presence ? 'is' : 'is not' 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/test/groovy/nebula/plugin/release/VersionSanitizerSpec.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.release 2 | 3 | import spock.lang.Specification 4 | import spock.lang.Unroll 5 | 6 | class VersionSanitizerSpec extends Specification { 7 | 8 | @Unroll("sanitize #version results in #expected") 9 | def 'sanitize version'() { 10 | when: 11 | String sanitizedVersion = VersionSanitizerUtil.sanitize(version) 12 | 13 | then: 14 | sanitizedVersion == expected 15 | 16 | where: 17 | version | expected 18 | "0.0.1" | "0.0.1" 19 | "0.0.1-rc.1" | "0.0.1-rc.1" 20 | "0.0.1-alpha" | "0.0.1-alpha" 21 | "0.0.1-SNAPSHOT" | "0.0.1-SNAPSHOT" 22 | "0.0.1-snapshot-2019070708092902" | "0.0.1-snapshot-2019070708092902" 23 | "0.0.1-dev.1+4dcsd" | "0.0.1-dev.1.4dcsd" 24 | "0.0.1-dev.1+branchName.4dcsd" | "0.0.1-dev.1.branchName.4dcsd" 25 | "0.0.1-dev.1+my.feature.4dcsd" | "0.0.1-dev.1.my.feature.4dcsd" 26 | "0.1.0-dev.0.uncommitted" | "0.1.0-dev.0.uncommitted" 27 | "0.1.0-dev.0.uncommitted+4dcsd" | "0.1.0-dev.0.uncommitted.4dcsd" 28 | "0.1.0-dev.0.uncommitted&4dcsd" | "0.1.0-dev.0.uncommitted.4dcsd" 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /src/test/groovy/nebula/plugin/release/git/base/ReleasePluginExtensionSpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release.git.base 17 | 18 | import nebula.plugin.release.git.GitBuildService 19 | 20 | import org.gradle.api.GradleException 21 | import org.gradle.api.Project 22 | import org.gradle.testfixtures.ProjectBuilder 23 | 24 | import spock.lang.Specification 25 | 26 | class ReleasePluginExtensionSpec extends Specification { 27 | def 'infers default version if selector returns false for all but default'() { 28 | given: 29 | Project project = ProjectBuilder.builder().build() 30 | ReleasePluginExtension extension = new ReleasePluginExtension(project) 31 | extension.gitBuildService = GroovyMock(GitBuildService) 32 | extension.versionStrategy([ 33 | getName: { 'b' }, 34 | selector: { proj, git -> false }, 35 | infer: { proj, git -> new ReleaseVersion('1.0.0', null, true) }] as VersionStrategy) 36 | extension.defaultVersionStrategy = [ 37 | getName: { 'a' }, 38 | selector: { proj, git -> true }, 39 | infer: { proj, git -> new ReleaseVersion('1.2.3', null, true) }] as VersionStrategy 40 | expect: 41 | project.version.toString() == '1.2.3' 42 | } 43 | 44 | def 'infers using first strategy selector returns true for'() { 45 | Project project = ProjectBuilder.builder().build() 46 | ReleasePluginExtension extension = new ReleasePluginExtension(project) 47 | extension.gitBuildService = GroovyMock(GitBuildService) 48 | extension.versionStrategy([ 49 | getName: { 'b' }, 50 | selector: { proj, gitOps -> false }, 51 | infer: { proj, git -> new ReleaseVersion('1.0.0', null, true) }] as VersionStrategy) 52 | extension.versionStrategy([ 53 | getName: { 'a' }, 54 | selector: { proj, gitOps -> true }, 55 | infer: { proj, git -> new ReleaseVersion('1.2.3', null, true) }] as VersionStrategy) 56 | expect: 57 | project.version.toString() == '1.2.3' 58 | } 59 | 60 | def 'infers using first strategy selector returns true for in order'() { 61 | Project project = ProjectBuilder.builder().build() 62 | ReleasePluginExtension extension = new ReleasePluginExtension(project) 63 | extension.gitBuildService = GroovyMock(GitBuildService) 64 | extension.versionStrategy([ 65 | getName: { 'b' }, 66 | selector: { proj, gitOps -> true }, 67 | infer: { proj, git -> new ReleaseVersion('1.0.0', null, true) }] as VersionStrategy) 68 | extension.versionStrategy([ 69 | getName: { 'a' }, 70 | selector: { proj, gitOps -> true }, 71 | infer: { proj, git -> new ReleaseVersion('1.2.3', null, true) }] as VersionStrategy) 72 | expect: 73 | project.version.toString() == '1.0.0' 74 | } 75 | 76 | def 'infer uses default if it has default selector that passes when selector doesnt'() { 77 | given: 78 | Project project = ProjectBuilder.builder().build() 79 | ReleasePluginExtension extension = new ReleasePluginExtension(project) 80 | extension.gitBuildService = GroovyMock(GitBuildService) 81 | extension.versionStrategy([ 82 | getName: { 'b' }, 83 | selector: { proj, gitOps -> false }, 84 | infer: { proj, git -> new ReleaseVersion('1.0.0', null, true) }] as VersionStrategy) 85 | extension.defaultVersionStrategy = [ 86 | getName: { 'a' }, 87 | selector: { proj, gitOps -> false }, 88 | defaultSelector: { proj, gitOps -> true }, 89 | infer: { proj, git -> new ReleaseVersion('1.2.3', null, true) }] as DefaultVersionStrategy 90 | expect: 91 | project.version.toString() == '1.2.3' 92 | } 93 | 94 | def 'infer fails if no strategy selected including the default strategy'() { 95 | given: 96 | Project project = ProjectBuilder.builder().build() 97 | ReleasePluginExtension extension = new ReleasePluginExtension(project) 98 | extension.gitBuildService = GroovyMock(GitBuildService) 99 | extension.versionStrategy([ 100 | getName: { 'b' }, 101 | selector: { proj, gitOps -> false }, 102 | infer: { proj, git -> new ReleaseVersion('1.0.0', null, true) }] as VersionStrategy) 103 | extension.defaultVersionStrategy = [ 104 | getName: { 'a' }, 105 | selector: { proj, gitOps -> false }, 106 | infer: { proj, git -> new ReleaseVersion('1.2.3', null, true) }] as VersionStrategy 107 | when: 108 | project.version.toString() 109 | then: 110 | thrown(GradleException) 111 | } 112 | 113 | def 'infer fails if no strategy selected and no default set'() { 114 | Project project = ProjectBuilder.builder().build() 115 | ReleasePluginExtension extension = new ReleasePluginExtension(project) 116 | extension.gitBuildService = GroovyMock(GitBuildService) 117 | extension.versionStrategy([ 118 | getName: { 'b' }, 119 | selector: { proj, gitOps -> false }, 120 | infer: { proj, git -> new ReleaseVersion('1.0.0', null, true) }] as VersionStrategy) 121 | extension.versionStrategy([ 122 | getName: { 'a' }, 123 | selector: { proj, gitOps -> false }, 124 | infer: { proj, git -> new ReleaseVersion('1.2.3', null, true) }] as VersionStrategy) 125 | when: 126 | project.version.toString() 127 | then: 128 | thrown(GradleException) 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/test/groovy/nebula/plugin/release/git/base/TagStrategySpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2025 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release.git.base 17 | 18 | import nebula.plugin.release.git.GitBuildService 19 | import spock.lang.Specification 20 | 21 | class TagStrategySpec extends Specification { 22 | def 'maybeCreateTag with version create tag true will create a tag'() { 23 | given: 24 | GitBuildService gitBuildService = GroovyMock() 25 | 1 * gitBuildService.createTag('v1.2.3', 'Release of 1.2.3') 26 | 27 | expect: 28 | new TagStrategy().maybeCreateTag(gitBuildService, new ReleaseVersion('1.2.3', null, true)) == 'v1.2.3' 29 | } 30 | 31 | def 'maybeCreateTag with version create tag false does not create a tag'() { 32 | given: 33 | GitBuildService gitBuildService = GroovyMock() 34 | 0 * gitBuildService.createTag(*_) 35 | 36 | expect: 37 | new TagStrategy().maybeCreateTag(gitBuildService, new ReleaseVersion('1.2.3', null, false)) == null 38 | } 39 | 40 | def 'maybeCreateTag with version create tag true and prefix name with v false will create a tag'() { 41 | setup: 42 | GitBuildService gitBuildService = GroovyMock() 43 | 44 | when: 45 | def strategy = new TagStrategy() 46 | strategy.prefixNameWithV = false 47 | strategy.maybeCreateTag(gitBuildService, new ReleaseVersion('1.2.3', null, true)) == '1.2.3' 48 | 49 | then: 50 | 1 * gitBuildService.createTag('1.2.3', 'Release of 1.2.3') 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/test/groovy/nebula/plugin/release/git/semver/RebuildVersionStrategySpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2023 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release.git.semver 17 | 18 | import nebula.plugin.release.git.GitBuildService 19 | import nebula.plugin.release.git.base.BaseReleasePlugin 20 | import nebula.plugin.release.git.base.ReleaseVersion 21 | import nebula.plugin.release.git.model.TagRef 22 | import org.gradle.api.Project 23 | import org.gradle.testfixtures.ProjectBuilder 24 | import spock.lang.Specification 25 | 26 | class RebuildVersionStrategySpec extends Specification { 27 | RebuildVersionStrategy strategy = new RebuildVersionStrategy() 28 | GitBuildService gitBuildService = GroovyMock() 29 | 30 | def getProject(Map properties) { 31 | Project p = ProjectBuilder.builder().withName("testproject").build() 32 | p.apply plugin: BaseReleasePlugin 33 | properties.each { k, v -> 34 | p.ext[k] = v 35 | } 36 | p 37 | } 38 | 39 | def 'selector returns false if repo is dirty'() { 40 | given: 41 | mockClean(false) 42 | Project project = getProject([:]) 43 | mockTagsAtHead('v1.0.0') 44 | expect: 45 | !strategy.selector(project, gitBuildService) 46 | } 47 | 48 | def 'selector returns false if any release properties are set'() { 49 | given: 50 | mockClean(true) 51 | Project project = getProject('release.scope': 'value') 52 | mockTagsAtHead('v1.0.0') 53 | expect: 54 | !strategy.selector(project, gitBuildService) 55 | } 56 | 57 | def 'selector returns false if no version tag at HEAD'() { 58 | given: 59 | mockClean(true) 60 | Project project = getProject([:]) 61 | mockTagsAtHead('non-version-tag') 62 | expect: 63 | !strategy.selector(project, gitBuildService) 64 | } 65 | 66 | def 'selector returns true if rebuild is attempted'() { 67 | given: 68 | mockClean(true) 69 | Project project = getProject([:]) 70 | mockTagsAtHead('v0.1.1', 'v1.0.0', '0.19.1') 71 | expect: 72 | strategy.selector(project, gitBuildService) 73 | } 74 | 75 | def 'infer returns HEAD version is inferred and previous with create tag false'() { 76 | given: 77 | mockClean(true) 78 | Project project = getProject([:]) 79 | mockTagsAtHead('v0.1.1', 'v1.0.0', '0.19.1') 80 | expect: 81 | strategy.infer(project, gitBuildService) == new ReleaseVersion('1.0.0', '1.0.0', false) 82 | } 83 | 84 | private void mockTagsAtHead(String... tagNames) { 85 | gitBuildService.headTags() >> tagNames.collect { new TagRef("refs/tags/${it}") } 86 | } 87 | 88 | 89 | private void mockClean(boolean clean) { 90 | gitBuildService.isCleanStatus() >> clean 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/test/groovy/nebula/plugin/release/git/semver/SemVerStrategySpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release.git.semver 17 | 18 | import com.github.zafarkhaja.semver.Version 19 | import nebula.plugin.release.git.GitBuildService 20 | import nebula.plugin.release.git.base.ReleaseVersion 21 | import nebula.plugin.release.git.model.Branch 22 | import org.gradle.api.GradleException 23 | import org.gradle.api.Project 24 | import spock.lang.Specification 25 | 26 | class SemVerStrategySpec extends Specification { 27 | Project project = GroovyMock() 28 | GitBuildService gitBuildService = GroovyMock() 29 | 30 | def 'selector returns false if stage is not set to valid value'() { 31 | given: 32 | def strategy = new SemVerStrategy(stages: ['one', 'two'] as SortedSet) 33 | mockStage(stageProp) 34 | expect: 35 | !strategy.selector(project, gitBuildService) 36 | where: 37 | stageProp << [null, 'test'] 38 | } 39 | 40 | 41 | def 'selector returns false if repo is dirty and not allowed to be'() { 42 | given: 43 | def strategy = new SemVerStrategy(stages: ['one'] as SortedSet, allowDirtyRepo: false) 44 | mockStage('one') 45 | mockRepoClean(false) 46 | expect: 47 | !strategy.selector(project, gitBuildService) 48 | } 49 | 50 | def 'selector returns true if repo is dirty and allowed and other criteria met'() { 51 | given: 52 | def strategy = new SemVerStrategy(stages: ['one'] as SortedSet, allowDirtyRepo: true) 53 | mockStage('one') 54 | mockRepoClean(false) 55 | mockCurrentBranch() 56 | expect: 57 | strategy.selector(project, gitBuildService) 58 | } 59 | 60 | def 'selector returns true if all criteria met'() { 61 | given: 62 | def strategy = new SemVerStrategy(stages: ['one', 'and'] as SortedSet, allowDirtyRepo: false) 63 | mockStage('one') 64 | mockRepoClean(true) 65 | mockCurrentBranch() 66 | expect: 67 | strategy.selector(project, gitBuildService) 68 | } 69 | 70 | def 'default selector returns false if stage is defined but not set to valid value'() { 71 | given: 72 | def strategy = new SemVerStrategy(stages: ['one', 'two'] as SortedSet) 73 | mockStage('test') 74 | expect: 75 | !strategy.defaultSelector(project, gitBuildService) 76 | } 77 | 78 | def 'default selector returns true if stage is not defined'() { 79 | given: 80 | def strategy = new SemVerStrategy(stages: ['one', 'two'] as SortedSet) 81 | mockStage(null) 82 | mockRepoClean(true) 83 | expect: 84 | strategy.defaultSelector(project, gitBuildService) 85 | } 86 | 87 | def 'default selector returns false if repo is dirty and not allowed to be'() { 88 | given: 89 | def strategy = new SemVerStrategy(stages: ['one'] as SortedSet, allowDirtyRepo: false) 90 | mockStage(stageProp) 91 | mockRepoClean(false) 92 | expect: 93 | !strategy.defaultSelector(project, gitBuildService) 94 | where: 95 | stageProp << [null, 'one'] 96 | } 97 | 98 | def 'default selector returns true if repo is dirty and allowed and other criteria met'() { 99 | given: 100 | def strategy = new SemVerStrategy(stages: ['one'] as SortedSet, allowDirtyRepo: true) 101 | mockStage('one') 102 | mockRepoClean(false) 103 | mockCurrentBranch() 104 | expect: 105 | strategy.defaultSelector(project, gitBuildService) 106 | } 107 | 108 | def 'default selector returns true if all criteria met'() { 109 | given: 110 | def strategy = new SemVerStrategy(stages: ['one', 'and'] as SortedSet, allowDirtyRepo: false) 111 | mockStage('one') 112 | mockRepoClean(true) 113 | mockCurrentBranch() 114 | expect: 115 | strategy.defaultSelector(project, gitBuildService) 116 | } 117 | 118 | def 'infer returns correct version'() { 119 | given: 120 | mockScope(scope) 121 | mockStage(stage) 122 | mockRepoClean(false) 123 | mockCurrentBranch() 124 | def nearest = new NearestVersion( 125 | normal: Version.valueOf('1.2.2'), 126 | any: Version.valueOf(nearestAny)) 127 | def locator = mockLocator(nearest) 128 | def strategy = mockStrategy(scope, stage, nearest, createTag, enforcePrecedence) 129 | expect: 130 | strategy.doInfer(project, gitBuildService, locator) == new ReleaseVersion('1.2.3-beta.1+abc123', '1.2.2', createTag) 131 | where: 132 | scope | stage | nearestAny | createTag | enforcePrecedence 133 | 'patch' | 'one' | '1.2.3' | true | false 134 | 'minor' | 'one' | '1.2.2' | true | true 135 | 'major' | 'one' | '1.2.2' | false | true 136 | 'patch' | null | '1.2.2' | false | true 137 | } 138 | 139 | def 'infer fails if stage is not listed in stages property'() { 140 | given: 141 | mockStage('other') 142 | def strategy = new SemVerStrategy(stages: ['one'] as SortedSet) 143 | when: 144 | strategy.doInfer(project, gitBuildService, null) 145 | then: 146 | thrown(GradleException) 147 | } 148 | 149 | def 'infer fails if precedence enforced and violated'() { 150 | given: 151 | mockRepoClean(false) 152 | mockCurrentBranch() 153 | def nearest = new NearestVersion(any: Version.valueOf('1.2.3')) 154 | def locator = mockLocator(nearest) 155 | def strategy = mockStrategy(null, 'and', nearest, false, true) 156 | when: 157 | strategy.doInfer(project, gitBuildService, locator) 158 | then: 159 | thrown(GradleException) 160 | } 161 | 162 | private def mockScope(String scopeProp) { 163 | (0..1) * project.hasProperty('release.scope') >> (scopeProp as boolean) 164 | (0..1) * project.property('release.scope') >> scopeProp 165 | } 166 | 167 | private def mockStage(String stageProp) { 168 | (0..1) * project.hasProperty('release.stage') >> (stageProp as boolean) 169 | (0..1) * project.property('release.stage') >> stageProp 170 | } 171 | 172 | private def mockRepoClean(boolean isClean) { 173 | (0..2) * gitBuildService.isCleanStatus() >> isClean 174 | } 175 | 176 | private def mockCurrentBranch() { 177 | (0..1) * gitBuildService.currentBranch >> 'refs/heads/master' 178 | (0..1) * gitBuildService.head >> 'refs/heads/master' 179 | } 180 | 181 | private def mockLocator(NearestVersion nearest) { 182 | NearestVersionLocator locator = Mock() 183 | locator.locate() >> nearest 184 | return locator 185 | } 186 | 187 | private def mockStrategy(String scope, String stage, NearestVersion nearest, boolean createTag, boolean enforcePrecedence) { 188 | PartialSemVerStrategy normal = Mock() 189 | PartialSemVerStrategy preRelease = Mock() 190 | PartialSemVerStrategy buildMetadata = Mock() 191 | SemVerStrategyState initial = new SemVerStrategyState([ 192 | scopeFromProp: scope?.toUpperCase(), 193 | stageFromProp: stage ?: 'and', 194 | currentHead: null, 195 | currentBranch: new Branch(fullName: 'refs/heads/master'), 196 | repoDirty: true, 197 | nearestVersion: nearest]) 198 | SemVerStrategyState afterNormal = initial.copyWith(inferredNormal: '1.2.3') 199 | SemVerStrategyState afterPreRelease = afterNormal.copyWith(inferredPreRelease: 'beta.1') 200 | SemVerStrategyState afterBuildMetadata = afterPreRelease.copyWith(inferredBuildMetadata: 'abc123') 201 | 202 | 1 * normal.infer(_) >> afterNormal 203 | 1 * preRelease.infer(_) >> afterPreRelease 204 | 1 * buildMetadata.infer(_) >> afterBuildMetadata 205 | 206 | 207 | return new SemVerStrategy( 208 | stages: ['one', 'and'] as SortedSet, 209 | normalStrategy: normal, 210 | preReleaseStrategy: preRelease, 211 | buildMetadataStrategy: buildMetadata, 212 | createTag: createTag, 213 | enforcePrecedence: enforcePrecedence 214 | ) 215 | } 216 | } 217 | -------------------------------------------------------------------------------- /src/test/groovy/nebula/plugin/release/git/semver/StrategyUtilSpec.groovy: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright 2012-2017 the original author or authors. 3 | * 4 | * Licensed under the Apache License, Version 2.0 (the "License"); 5 | * you may not use this file except in compliance with the License. 6 | * You may obtain a copy of the License at 7 | * 8 | * http://www.apache.org/licenses/LICENSE-2.0 9 | * 10 | * Unless required by applicable law or agreed to in writing, software 11 | * distributed under the License is distributed on an "AS IS" BASIS, 12 | * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | * See the License for the specific language governing permissions and 14 | * limitations under the License. 15 | */ 16 | package nebula.plugin.release.git.semver 17 | 18 | import spock.lang.Specification 19 | 20 | class StrategyUtilSpec extends Specification { 21 | SemVerStrategyState initialState = new SemVerStrategyState([:]) 22 | 23 | def 'closure backed uses behavior passed in'() { 24 | expect: 25 | stringReplace('a').infer(initialState) == new SemVerStrategyState(inferredPreRelease: 'a') 26 | } 27 | 28 | def 'choose one returns the first that changes state'() { 29 | given: 30 | def chain = StrategyUtil.one(nothing(), stringReplace('a'), stringReplace('b'), nothing()) 31 | expect: 32 | chain.infer(initialState) == new SemVerStrategyState(inferredPreRelease: 'a') 33 | } 34 | 35 | def 'apply all uses all strategies in order'() { 36 | given: 37 | def chain = StrategyUtil.all(stringAppend('a'), stringAppend('b'), stringAppend('c')) 38 | expect: 39 | chain.infer(initialState) == new SemVerStrategyState(inferredPreRelease: 'a.b.c') 40 | } 41 | 42 | private def nothing() { 43 | return StrategyUtil.closure { state -> state } 44 | } 45 | 46 | private def stringReplace(String str) { 47 | return StrategyUtil.closure { state -> state.copyWith(inferredPreRelease: str) } 48 | } 49 | 50 | private def stringAppend(String str) { 51 | return StrategyUtil.closure { state -> 52 | def preRelease = state.inferredPreRelease ? "${state.inferredPreRelease}.${str}" : str 53 | return state.copyWith(inferredPreRelease: preRelease) 54 | } 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/test/groovy/nebula/plugin/release/util/ReleaseTasksUtilSpec.groovy: -------------------------------------------------------------------------------- 1 | package nebula.plugin.release.util 2 | 3 | import nebula.test.ProjectSpec 4 | import spock.lang.Unroll 5 | 6 | class ReleaseTasksUtilSpec extends ProjectSpec { 7 | 8 | def 'verify isUsingLatestTag reads the project property accordingly'() { 9 | given: 10 | project 11 | 12 | expect: 13 | !ReleaseTasksUtil.isUsingLatestTag(project) 14 | 15 | when: 16 | project.ext.'release.useLastTag' = false 17 | 18 | then: 19 | !ReleaseTasksUtil.isUsingLatestTag(project) 20 | 21 | when: 22 | project.ext.'release.useLastTag' = true 23 | 24 | then: 25 | ReleaseTasksUtil.isUsingLatestTag(project) 26 | } 27 | 28 | @Unroll 29 | def 'checking release task for #tasks results in #expected'() { 30 | expect: 31 | ReleaseTasksUtil.isReleaseTaskThatRequiresTagging(tasks) == expected 32 | 33 | where: 34 | tasks || expected 35 | ['build'] || false 36 | ['build', 'devSnapshot'] || false 37 | ['build', 'candidate'] || true 38 | ['build', 'immutableSnapshot'] || false 39 | ['build', 'final'] || true 40 | [':devSnapshot'] || false 41 | ['devSnapshot'] || false 42 | [':snapshot'] || false 43 | ['snapshot'] || false 44 | ['candidate'] || true 45 | [':candidate'] || true 46 | ['immutableSnapshot'] || false 47 | [':immutableSnapshot'] || false 48 | ['final'] || true 49 | [':final'] || true 50 | } 51 | 52 | } 53 | -------------------------------------------------------------------------------- /stale_outputs_checked: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/nebula-plugins/nebula-release-plugin/94fdb789b4aad0552fd81d53171f640144b0b923/stale_outputs_checked --------------------------------------------------------------------------------