Repository: sbt/sbt-release Branch: master Commit: a46ca1b45dff Files: 84 Total size: 111.7 KB Directory structure: gitextract_na5jo4qf/ ├── .github/ │ ├── dependabot.yml │ └── workflows/ │ ├── ci.yml │ └── release.yml ├── .gitignore ├── .scalafmt.conf ├── LICENSE ├── README.md ├── build.sbt ├── notes/ │ ├── 0.1.markdown │ ├── 0.2.markdown │ ├── 0.3.markdown │ ├── 0.4.markdown │ ├── 0.5.markdown │ ├── 0.6.markdown │ ├── 0.7.1.markdown │ ├── 0.7.markdown │ ├── 0.8.1.markdown │ ├── 0.8.2.markdown │ ├── 0.8.3.markdown │ ├── 0.8.4.markdown │ ├── 0.8.5.markdown │ ├── 0.8.markdown │ ├── 1.0.5.markdown │ └── about.markdown ├── project/ │ ├── build.properties │ └── plugins.sbt └── src/ ├── main/ │ ├── scala/ │ │ ├── Compat.scala │ │ ├── ReleaseExtra.scala │ │ ├── ReleasePlugin.scala │ │ ├── Vcs.scala │ │ ├── Version.scala │ │ └── package.scala │ ├── scala-2/ │ │ ├── LoadCompat.scala │ │ └── ReleasePluginCompat.scala │ └── scala-3/ │ ├── LoadCompat.scala │ └── ReleasePluginCompat.scala ├── sbt-test/ │ └── sbt-release/ │ ├── command-line-version-numbers/ │ │ ├── build.sbt │ │ ├── project/ │ │ │ ├── build.properties │ │ │ └── build.sbt │ │ ├── src/ │ │ │ └── main/ │ │ │ └── scala/ │ │ │ └── Hello.scala │ │ ├── test │ │ └── version.sbt │ ├── cross/ │ │ ├── .gitignore │ │ ├── A.scala │ │ ├── build.sbt │ │ ├── project/ │ │ │ └── build.sbt │ │ ├── test │ │ └── version.sbt │ ├── exit-code/ │ │ ├── .gitignore │ │ ├── build.sbt │ │ ├── foo.scala │ │ ├── project/ │ │ │ └── build.sbt │ │ └── test │ ├── fail-test/ │ │ ├── build.sbt │ │ ├── project/ │ │ │ ├── build.properties │ │ │ └── build.sbt │ │ ├── src/ │ │ │ └── test/ │ │ │ └── scala/ │ │ │ └── FailTest.scala │ │ └── test │ ├── mercurial/ │ │ ├── .hgignore │ │ ├── B.scala │ │ ├── build.sbt │ │ ├── project/ │ │ │ └── build.sbt │ │ ├── test │ │ └── version.sbt │ ├── skip-tests/ │ │ ├── .gitignore │ │ ├── build.sbt │ │ ├── project/ │ │ │ └── build.sbt │ │ ├── src/ │ │ │ └── test/ │ │ │ └── scala/ │ │ │ └── Test.scala │ │ ├── test │ │ └── version.sbt │ ├── tag-default/ │ │ ├── .gitignore │ │ ├── build.sbt │ │ ├── project/ │ │ │ └── build.sbt │ │ ├── test │ │ └── version.sbt │ ├── tasks-as-steps/ │ │ ├── build.sbt │ │ ├── project/ │ │ │ └── build.sbt │ │ └── test │ └── with-defaults/ │ ├── .gitignore │ ├── build.sbt │ ├── project/ │ │ └── build.sbt │ ├── test │ └── version.sbt └── test/ └── scala/ └── VersionSpec.scala ================================================ FILE CONTENTS ================================================ ================================================ FILE: .github/dependabot.yml ================================================ version: 2 updates: - package-ecosystem: "github-actions" directory: "/" schedule: interval: "weekly" ================================================ FILE: .github/workflows/ci.yml ================================================ name: CI on: pull_request: push: schedule: - cron: '0 8 * * 0' jobs: test: runs-on: ubuntu-latest timeout-minutes: 40 strategy: fail-fast: false matrix: include: - java: 8 sbt_version: "1.4.9" - java: 17 sbt_version: "2" - java: 8 - java: 25 steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 with: java-version: ${{matrix.java}} distribution: zulu - uses: sbt/setup-sbt@508b753e53cb6095967669e0911487d2b9bc9f41 # v1.1.22 - uses: coursier/cache-action@90c37294538be80a558fd665531fcdc2b467b475 # v8.1.0 - run: | git config --global user.email "example@example.com" git config --global user.name "example" echo '[ui]' > "$HOME/.hgrc" echo 'username = example ' >> "$HOME/.hgrc" - run: sbt "+ scalafmtCheckAll" scalafmtSbtCheck - run: sbt -v "set pluginCrossBuild / sbtVersion := \"${{matrix.sbt_version}}\"" test scripted if: ${{ matrix.sbt_version != '' && matrix.sbt_version != '2' }} - run: sbt -v test scripted if: ${{ matrix.sbt_version == '' }} - run: sbt -v "++ 3.x" Test/compile test scripted if: ${{ matrix.sbt_version == '2' }} - run: rm -rf "$HOME/.ivy2/local" || true ================================================ FILE: .github/workflows/release.yml ================================================ name: Release on: push: tags: ["*"] jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 with: fetch-depth: 0 - uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5.2.0 with: java-version: 17 distribution: temurin - uses: sbt/setup-sbt@508b753e53cb6095967669e0911487d2b9bc9f41 # v1.1.22 - run: sbt ci-release if: ${{ github.repository_owner == 'sbt' }} env: PGP_PASSPHRASE: ${{ secrets.PGP_PASSPHRASE }} PGP_SECRET: ${{ secrets.PGP_SECRET }} SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} ================================================ FILE: .gitignore ================================================ target project/target .idea .idea_modules .bsp ================================================ FILE: .scalafmt.conf ================================================ version = "3.10.7" runner.dialect = Scala212Source3 maxColumn = 120 align.preset = none align.tokens = [] rewrite.rules = [ExpandImportSelectors, PreferCurlyFors] rewrite.imports.contiguousGroups = "no" rewrite.imports.groups = [[".*"]] rewrite.imports.sort = ascii continuationIndent.callSite = 2 continuationIndent.defnSite = 2 docstrings.style = keep includeCurlyBraceInSelectChains = false optIn.breakChainOnFirstMethodDot = false trailingCommas = preserve newlines.topLevelStatementBlankLines = [ { blanks { after = 1 } maxNest = 0 regex = "Import|Class|Trait|Object" } ] project.layout = StandardConvention rewrite.scala3.convertToNewSyntax = true rewrite.scala3.newSyntax.control = false ================================================ FILE: LICENSE ================================================ Apache License Version 2.0, January 2004 http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 1. Definitions. "License" shall mean the terms and conditions for use, reproduction, and distribution as defined by Sections 1 through 9 of this document. "Licensor" shall mean the copyright owner or entity authorized by the copyright owner that is granting the License. "Legal Entity" shall mean the union of the acting entity and all other entities that control, are controlled by, or are under common control with that entity. For the purposes of this definition, "control" means (i) the power, direct or indirect, to cause the direction or management of such entity, whether by contract or otherwise, or (ii) ownership of fifty percent (50%) or more of the outstanding shares, or (iii) beneficial ownership of such entity. "You" (or "Your") shall mean an individual or Legal Entity exercising permissions granted by this License. "Source" form shall mean the preferred form for making modifications, including but not limited to software source code, documentation source, and configuration files. "Object" form shall mean any form resulting from mechanical transformation or translation of a Source form, including but not limited to compiled object code, generated documentation, and conversions to other media types. "Work" shall mean the work of authorship, whether in Source or Object form, made available under the License, as indicated by a copyright notice that is included in or attached to the work (an example is provided in the Appendix below). "Derivative Works" shall mean any work, whether in Source or Object form, that is based on (or derived from) the Work and for which the editorial revisions, annotations, elaborations, or other modifications represent, as a whole, an original work of authorship. For the purposes of this License, Derivative Works shall not include works that remain separable from, or merely link (or bind by name) to the interfaces of, the Work and Derivative Works thereof. "Contribution" shall mean any work of authorship, including the original version of the Work and any modifications or additions to that Work or Derivative Works thereof, that is intentionally submitted to Licensor for inclusion in the Work by the copyright owner or by an individual or Legal Entity authorized to submit on behalf of the copyright owner. For the purposes of this definition, "submitted" means any form of electronic, verbal, or written communication sent to the Licensor or its representatives, including but not limited to communication on electronic mailing lists, source code control systems, and issue tracking systems that are managed by, or on behalf of, the Licensor for the purpose of discussing and improving the Work, but excluding communication that is conspicuously marked or otherwise designated in writing by the copyright owner as "Not a Contribution." "Contributor" shall mean Licensor and any individual or Legal Entity on behalf of whom a Contribution has been received by Licensor and subsequently incorporated within the Work. 2. Grant of Copyright License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare Derivative Works of, publicly display, publicly perform, sublicense, and distribute the Work and such Derivative Works in Source or Object form. 3. Grant of Patent License. Subject to the terms and conditions of this License, each Contributor hereby grants to You a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable (except as stated in this section) patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer the Work, where such license applies only to those patent claims licensable by such Contributor that are necessarily infringed by their Contribution(s) alone or by combination of their Contribution(s) with the Work to which such Contribution(s) was submitted. If You institute patent litigation against any entity (including a cross-claim or counterclaim in a lawsuit) alleging that the Work or a Contribution incorporated within the Work constitutes direct or contributory patent infringement, then any patent licenses granted to You under this License for that Work shall terminate as of the date such litigation is filed. 4. Redistribution. You may reproduce and distribute copies of the Work or Derivative Works thereof in any medium, with or without modifications, and in Source or Object form, provided that You meet the following conditions: (a) You must give any other recipients of the Work or Derivative Works a copy of this License; and (b) You must cause any modified files to carry prominent notices stating that You changed the files; and (c) You must retain, in the Source form of any Derivative Works that You distribute, all copyright, patent, trademark, and attribution notices from the Source form of the Work, excluding those notices that do not pertain to any part of the Derivative Works; and (d) If the Work includes a "NOTICE" text file as part of its distribution, then any Derivative Works that You distribute must include a readable copy of the attribution notices contained within such NOTICE file, excluding those notices that do not pertain to any part of the Derivative Works, in at least one of the following places: within a NOTICE text file distributed as part of the Derivative Works; within the Source form or documentation, if provided along with the Derivative Works; or, within a display generated by the Derivative Works, if and wherever such third-party notices normally appear. The contents of the NOTICE file are for informational purposes only and do not modify the License. You may add Your own attribution notices within Derivative Works that You distribute, alongside or as an addendum to the NOTICE text from the Work, provided that such additional attribution notices cannot be construed as modifying the License. You may add Your own copyright statement to Your modifications and may provide additional or different license terms and conditions for use, reproduction, or distribution of Your modifications, or for any such Derivative Works as a whole, provided Your use, reproduction, and distribution of the Work otherwise complies with the conditions stated in this License. 5. Submission of Contributions. Unless You explicitly state otherwise, any Contribution intentionally submitted for inclusion in the Work by You to the Licensor shall be under the terms and conditions of this License, without any additional terms or conditions. Notwithstanding the above, nothing herein shall supersede or modify the terms of any separate license agreement you may have executed with Licensor regarding such Contributions. 6. Trademarks. This License does not grant permission to use the trade names, trademarks, service marks, or product names of the Licensor, except as required for reasonable and customary use in describing the origin of the Work and reproducing the content of the NOTICE file. 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in writing, Licensor provides the Work (and each Contributor provides its Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied, including, without limitation, any warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining the appropriateness of using or redistributing the Work and assume any risks associated with Your exercise of permissions under this License. 8. Limitation of Liability. In no event and under no legal theory, whether in tort (including negligence), contract, or otherwise, unless required by applicable law (such as deliberate and grossly negligent acts) or agreed to in writing, shall any Contributor be liable to You for damages, including any direct, indirect, special, incidental, or consequential damages of any character arising as a result of this License or out of the use or inability to use the Work (including but not limited to damages for loss of goodwill, work stoppage, computer failure or malfunction, or any and all other commercial damages or losses), even if such Contributor has been advised of the possibility of such damages. 9. Accepting Warranty or Additional Liability. While redistributing the Work or Derivative Works thereof, You may choose to offer, and charge a fee for, acceptance of support, warranty, indemnity, or other liability obligations and/or rights consistent with this License. However, in accepting such obligations, You may act only on Your own behalf and on Your sole responsibility, not on behalf of any other Contributor, and only if You agree to indemnify, defend, and hold each Contributor harmless for any liability incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. END OF TERMS AND CONDITIONS APPENDIX: How to apply the Apache License to your work. To apply the Apache License to your work, attach the following boilerplate notice, with the fields enclosed by brackets "[]" replaced with your own identifying information. (Don't include the brackets!) The text should be enclosed in the appropriate comment syntax for the file format. We also recommend that a file or class name and description of purpose be included on the same "printed page" as the copyright notice for easier identification within third-party archives. Copyright [yyyy] [name of copyright owner] Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. You may obtain a copy of the License at http://www.apache.org/licenses/LICENSE-2.0 Unless required by applicable law or agreed to in writing, software distributed under the License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the License for the specific language governing permissions and limitations under the License. ================================================ FILE: README.md ================================================ # sbt-release This sbt plugin provides a customizable release process that you can add to your project. [![sbt-release sbt 1](https://index.scala-lang.org/sbt/sbt-release/sbt-release/latest-by-scala-version.svg?platform=sbt1)](https://index.scala-lang.org/sbt/sbt-release/sbt-release) [![sbt-release sbt 2](https://index.scala-lang.org/sbt/sbt-release/sbt-release/latest-by-scala-version.svg?platform=sbt2)](https://index.scala-lang.org/sbt/sbt-release/sbt-release) **Notice:** This README contains information for the latest release. Please refer to the documents for a specific version by looking up the respective [tag](https://github.com/sbt/sbt-release/tags). ## Requirements * sbt 1.x * The version of the project should follow the semantic versioning scheme on [semver.org](https://www.semver.org) with the following additions: * The minor and bugfix (and beyond) part of the version are optional. * There is no limit to the number of subversions you may have. * The appendix after the bugfix part must be alphanumeric (`[0-9a-zA-Z]`) but may also contain dash characters `-`. * These are all valid version numbers: * 1.2.3 * 1.2.3-SNAPSHOT * 1.2beta1 * 1.2-beta.1 * 1.2 * 1 * 1-BETA17 * 1.2.3.4.5 * 1.2.3.4.5-SNAPSHOT * A [publish repository](https://www.scala-sbt.org/1.x/docs/Publishing.html) configured. (Required only for the default release process. See further below for release process customizations.) * git [optional] ## Usage Add the following lines to `./project/plugins.sbt`. See the section [Using Plugins](https://www.scala-sbt.org/1.x/docs/Using-Plugins.html) in the sbt website for more information. ```scala addSbtPlugin("com.github.sbt" % "sbt-release" % "1.4.0") ``` ## version.sbt Since the build definition is actual Scala code, it's not as straight forward to change something in the middle of it as it is with an XML definition. For this reason, *sbt-release* won't ever touch your build definition files, but instead writes the new release or development version to a file defined by the setting `releaseVersionFile`, which is set to **`file("version.sbt")`** by default and points to `$PROJECT_ROOT/version.sbt`. By default the version is set on the build level (using `ThisBuild / version`). This behavior can be controlled by setting `releaseUseGlobalVersion` to `false`, after which a version like `version := "1.2.3"` will be written to `version.sbt`. ## Release Process The default release process consists of the following tasks: 1. Check that the working directory is a git repository and the repository has no outstanding changes. Also prints the hash of the last commit to the console. 1. If there are any snapshot dependencies, ask the user whether to continue or not (default: no). 1. Ask the user for the `release version` and the `next development version`. Sensible defaults are provided. 1. Run `clean`. 1. Run `test`, if any test fails, the release process is aborted. 1. Write `ThisBuild / version := "$releaseVersion"` to the file `version.sbt` and also apply this setting to the current [build state](https://www.scala-sbt.org/1.x/docs/Core-Principles.html#Introduction+to+build+state). 1. Commit the changes in `version.sbt`. 1. Tag the previous commit with `v$version` (eg. `v1.2`, `v1.2.3`). 1. Run `publish`. 1. Write `ThisBuild / version := "nextVersion"` to the file `version.sbt` and also apply this setting to the current build state. 1. Commit the changes in `version.sbt`. In case of a failure of a task, the release process is aborted. ### Non-interactive release You can run a non-interactive release by providing the argument `with-defaults` (tab completion works) to the `release` command. For all interactions, the following default value will be chosen: * Continue with snapshots dependencies: no * Release Version: current version without the qualifier (eg. `1.2-SNAPSHOT` -> `1.2`) * Next Version: increase the minor version segment of the current version and set the qualifier to '-SNAPSHOT' (eg. `1.2.1-SNAPSHOT` -> `1.3.0-SNAPSHOT`) * VCS tag: default is abort if the tag already exists. It is possible to override the answer to VCS by ```default-tag-exists-answer``` with one of: * ```o``` override * ```k``` do not overwrite * ```a``` abort (default) * `````` an explicit custom tag name (e.g. ```1.2-M3```) * VCS push: * Abort if no remote tracking branch is set up. * Abort if remote tracking branch cannot be checked (eg. via `git fetch`). * Abort if the remote tracking branch has unmerged commits. ### Set release version and next version as command arguments You can set the release version using the argument `release-version` and next version with `next-version`. Example: release release-version 1.0.99 next-version 1.2.0-SNAPSHOT ### Skipping tests For that emergency release at 2am on a Sunday, you can optionally avoid running any tests by providing the `skip-tests` argument to the `release` command. ### Cross building during a release Since version 0.7, *sbt-release* comes with built-in support for [cross building](https://www.scala-sbt.org/1.x/docs/Cross-Build.html) and cross publishing. A cross release can be triggered in two ways: 1. via the setting `releaseCrossBuild` (by default set to `false`) 1. by using the option `cross` for the `release` command `> release cross with-defaults` Combining both ways of steering a cross release, it is possible to generally disable automatic detection of cross release by using `releaseCrossBuild := false` and running `release cross`. Of the predefined release steps, the `clean`, `test`, and `publish` release steps are set up for cross building. A cross release behaves analogous to using the `+` command: 1. If no `crossScalaVersions` are set, then running `release` or `release cross` will not trigger a cross release (i.e. run the release with the scala version specified in the setting `scalaVersion`). 1. If the `crossScalaVersions` setting is set, then only these scala versions will be used. Make sure to include the regular/default `scalaVersion` in the `crossScalaVersions` setting as well. Note that setting running `release cross` on a root project with `crossScalaVersions` set to `Nil` will not release anything. In the section *Customizing the release process* we take a look at how to define a `ReleaseStep` to participate in a cross build. ### Versioning Strategies As of version 0.8, *sbt-release* comes with several strategies for computing the next snapshot version via the `releaseVersionBump` setting. These strategies are defined in `sbtrelease.Version.Bump`. By default, the `Next` strategy is used: * `Major`: always bumps the *major* part of the version * `Minor`: always bumps the *minor* part of the version * `Bugfix`: always bumps the *bugfix* part of the version * `Nano`: always bumps the *nano* part of the version * `Next` (**default**): bumps the last version part, including the qualifier (e.g. `0.17` -> `0.18`, `0.11.7` -> `0.11.8`, `3.22.3.4.91` -> `3.22.3.4.92`, `1.0.0-RC1` -> `1.0.0-RC2`) * `NextStable`: bumps exactly like `Next` except that any prerelease qualifier is excluded (e.g. `1.0.0-RC1` -> `1.0.0`) Users can set their preferred versioning strategy in `build.sbt` as follows: ```sbt releaseVersionBump := sbtrelease.Version.Bump.Major ``` ### Default Versioning The default settings make use of the helper class [`Version`](https://github.com/sbt/sbt-release/blob/master/src/main/scala/Version.scala) that ships with *sbt-release*. `releaseVersion`: The current version in version.sbt, without the "-SNAPSHOT" ending. So, if `version.sbt` contains `1.0.0-SNAPSHOT`, the release version will be set to `1.0.0`. `releaseNextVersion`: The "bumped" version according to the versioning strategy (explained above), including the `-SNAPSHOT` ending. So, if `releaseVersion` is `1.0.0`, `releaseNextVersion` will be `1.0.1-SNAPSHOT`. ### Custom Versioning *sbt-release* comes with two settings for deriving the release version and the next development version from a given version. These derived versions are used for the suggestions/defaults in the prompt and for non-interactive releases. Let's take a look at the types: ```scala val releaseVersion : TaskKey[String => String] val releaseNextVersion : TaskKey[String => String] ``` If you want to customize the versioning, keep the following in mind: * `releaseVersion` * input: the current development version * output: the release version * `releaseNextVersion` * input: the release version (either automatically 'chosen' in a non-interactive build or from user input) * output: the next development version ### Custom VCS messages *sbt-release* has built in support to commit/push to Git, Mercurial and Subversion repositories. The messages for the tag and the commits can be customized to your needs with these settings: ```scala val releaseTagComment : TaskKey[String] val releaseCommitMessage : TaskKey[String] val releaseNextCommitMessage : TaskKey[String] // defaults releaseTagComment := s"Releasing ${(ThisBuild / version).value}", releaseCommitMessage := s"Setting version to ${(ThisBuild / version).value}", releaseNextCommitMessage := s"Setting version to ${(ThisBuild / version).value}", ``` ### Publishing signed releases SBT is able to publish signed releases using the [sbt-pgp plugin](https://github.com/sbt/sbt-pgp). After setting that up for your project, you can then tell *sbt-release* to use it by setting the `releasePublishArtifactsAction` key: ```scala releasePublishArtifactsAction := PgpKeys.publishSigned.value ```` ## Customizing the release process ### Not all releases are created equal The release process can be customized to the project's needs. * Not using Git? Then rip it out. * Want to check for the existence of release notes at the start of the release and then publish it with [posterous-sbt](https://github.com/n8han/posterous-sbt) at the end? Just add the release step. The release process is defined by [State](https://www.scala-sbt.org/1.x/docs/Build-State.html) transformation functions (`State => State`), for which *sbt-release* defines this case class: ```scala case class ReleaseStep ( action: State => State, check: State => State = identity, enableCrossBuild: Boolean = false ) ``` The function `action` is used to perform the actual release step. Additionally, each release step can provide a `check` function that is run at the beginning of the release and can be used to prevent the release from running because of an unsatisfied invariant (i.e. the release step for publishing artifacts checks that publishTo is properly set up). The property `enableCrossBuild` tells *sbt-release* whether or not a particular `ReleaseStep` needs to be executed for the specified `crossScalaVersions`. The sequence of `ReleaseStep`s that make up the release process is stored in the setting `releaseProcess: SettingKey[Seq[ReleaseStep]]`. The state transformations functions used in *sbt-release* are the same as the action/body part of a no-argument command. You can read more about [building commands](https://www.scala-sbt.org/1.x/docs/Commands.html) in the sbt website. ### Release Steps There are basically 2 ways to creating a new `ReleaseStep`: #### Defining your own release steps You can define your own state tansformation functions, just like *sbt-release* does, for example: ```scala val checkOrganization = ReleaseStep(action = st => { // extract the build state val extracted = Project.extract(st) // retrieve the value of the organization SettingKey val org = extracted.get(Keys.organization) if (org.startsWith("com.acme")) sys.error("Hey, no need to release a toy project!") st }) ``` We will later see how to let this release step participate in the release process. #### Reusing already defined tasks Sometimes you just want to run an existing task or command. This is especially useful if the task raises an error in case something went wrong and therefore interrupts the release process. *sbt-release* comes with a few convenience functions for converting tasks and commands to release steps: * `releaseStepTask` - Run an individual task. Does not aggregate builds. * `releaseStepTaskAggregated` - Run an aggregated task. * `releaseStepInputTask` - Run an input task, optionally taking the input to pass to it. * `releaseStepCommand` - Run a command. For example: ```scala releaseProcess := Seq[ReleaseStep]( releaseStepInputTask(testOnly, " com.example.MyTest"), releaseStepInputTask(scripted), releaseStepTask(subproject / publishSigned), releaseStepCommand("sonaRelease") ) ``` I highly recommend to make yourself familiar with the [State API](https://www.scala-sbt.org/1.x/docs/Build-State.html) before you continue your journey to a fully customized release process. ### Can we finally customize that release process, please? Yes, and as a start, let's take a look at the [default definition](https://github.com/sbt/sbt-release/blob/v1.4.0/src/main/scala/ReleasePlugin.scala#L262-L274) of `releaseProcess`: #### The default release process ```scala import ReleaseTransformations._ // ... releaseProcess := Seq[ReleaseStep]( checkSnapshotDependencies, // : ReleaseStep inquireVersions, // : ReleaseStep runClean, // : ReleaseStep runTest, // : ReleaseStep setReleaseVersion, // : ReleaseStep commitReleaseVersion, // : ReleaseStep, performs the initial git checks tagRelease, // : ReleaseStep publishArtifacts, // : ReleaseStep, checks whether `publishTo` is properly set up setNextVersion, // : ReleaseStep commitNextVersion, // : ReleaseStep pushChanges // : ReleaseStep, also checks that an upstream branch is properly configured ) ``` The names of the individual steps of the release process are pretty much self-describing. Notice how we can just reuse the `publish` task by utilizing the `releaseTask` helper function, but keep in mind that it needs to be properly scoped (more info on [Scopes](https://www.scala-sbt.org/1.x/docs/Scopes.html)). Note, the `commitReleaseVersion` step requires that the working directory has no untracked files by default. It will abort the release in this case. You may disable this check by setting the `releaseIgnoreUntrackedFiles` key to `true`. #### No Git, and no toy projects! Let's modify the previous release process and remove the Git related steps, who uses that anyway. ```scala import ReleaseTransformations._ // ... ReleaseKeys.releaseProcess := Seq[ReleaseStep]( checkOrganization, // Look Ma', my own release step! checkSnapshotDependencies, inquireVersions, runTest, setReleaseVersion, publishArtifacts, setNextVersion ) ``` Overall, the process stayed pretty much the same: * The Git related steps were left out. * Our `checkOrganization` task was added in the beginning, just to be sure this is a serious project. #### Release notes anyone? Now let's also add steps for [posterous-sbt](https://github.com/n8han/posterous-sbt): ```scala import posterous.Publish._ import ReleaseTransformations._ // ... val publishReleaseNotes = (ref: ProjectRef) => ReleaseStep( check = releaseStepTaskAggregated(check in Posterous in ref), // upfront check action = releaseStepTaskAggregated(publish in Posterous in ref) // publish release notes ) // ... ReleaseKeys.releaseProcess <<= thisProjectRef apply { ref => import ReleaseStateTransformations._ Seq[ReleaseStep]( checkOrganization, checkSnapshotDependencies, inquireVersions, runTest, setReleaseVersion, publishArtifacts, publishReleaseNotes(ref) // we need to forward `thisProjectRef` for proper scoping of the underlying tasks setNextVersion ) } ``` The `check` part of the release step is run at the start, to make sure we have everything set up to post the release notes later on. After publishing the actual build artifacts, we also publish the release notes. ## Credits Thank you, [Jason](https://github.com/retronym) and [Mark](https://github.com/harrah), for your feedback and ideas. ## Contributors [Johannes Rudolph](https://github.com/jrudolph), [Espen Wiborg](https://github.com/espenhw), [Eric Bowman](https://github.com/ebowman), [Petteri Valkonen](https://github.com/pvalkone), [Gary Coady](https://github.com/garycoady), [Alexey Alekhin](https://github.com/laughedelic), [Andrew Gustafson](https://github.com/agustafson), [Paul Davies](https://github.com/paulmdavies), [Stanislav Savulchik](https://github.com/savulchik), [Tim Van Laer](https://github.com/timvlaer), [Lars Hupel](https://github.com/larsrh) ## License Copyright (c) 2011-2014 Gerolf Seitz Published under the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0.txt) ================================================ FILE: build.sbt ================================================ lazy val `sbt-release` = project in file(".") organization := "com.github.sbt" name := "sbt-release" crossScalaVersions += "3.8.2" pluginCrossBuild / sbtVersion := { scalaBinaryVersion.value match { case "2.12" => (pluginCrossBuild / sbtVersion).value case _ => "2.0.0-RC11" } } homepage := Some(url("https://github.com/sbt/sbt-release")) licenses := Seq("Apache-2.0" -> url("http://www.apache.org/licenses/LICENSE-2.0")) publishMavenStyle := true scalacOptions ++= Seq("-deprecation", "-feature", "-language:implicitConversions") scalacOptions ++= { scalaBinaryVersion.value match { case "3" => Nil case _ => Seq("-release:8") } } val unusedWarnings = Def.setting( scalaBinaryVersion.value match { case "2.12" => Seq("-Ywarn-unused:imports") case _ => Seq( "-Wunused:imports", "-Wconf:msg=is no longer supported for vararg splices:error", ) } ) scalacOptions ++= unusedWarnings.value Seq(Compile, Test).flatMap(c => c / console / scalacOptions --= unusedWarnings.value) def hash(): String = sys.process.Process("git rev-parse HEAD").lineStream_!.head Compile / doc / scalacOptions ++= { Seq( "-sourcepath", (LocalRootProject / baseDirectory).value.getAbsolutePath, "-doc-source-url", s"https://github.com/sbt/sbt-release/tree/${hash()}€{FILE_PATH}.scala" ) } libraryDependencies ++= Seq("org.specs2" %% "specs2-core" % "4.23.0" % "test") // Scripted enablePlugins(SbtPlugin) scriptedLaunchOpts := { scriptedLaunchOpts.value ++ Seq( "-Xmx1024M", "-Dsbt.build.onchange=warn", "-Dplugin.version=" + version.value ) } scriptedBufferLog := false pomExtra := ( { Seq( ("xuwei-k", "Kenji Yoshida"), ).map { case (id, name) => {id} {name} https://github.com/{id} } } ) ================================================ FILE: notes/0.1.markdown ================================================ * Initial release, see the [README](https://github.com/gseitz/sbt-release/blob/master/README.md) for a comprehensive introduction. ================================================ FILE: notes/0.2.markdown ================================================ * \[Improvement\] [#1](https://github.com/gseitz/sbt-release/issues/1): Include the Git tag and the has of the release commit in the JAR manifest. * \[Improvement\] [#3](https://github.com/gseitz/sbt-release/issues/3): Default version policy should increase the minor number, not the micro/bugfix * \[Improvement\] [#4](https://github.com/gseitz/sbt-release/issues/4): The suggested next development version should be based on the entered release version. * \[Improvement\] Allow easier customization of the release/next version. More info in the section *Custom versioning* in the [README](https://github.com/gseitz/sbt-release/blob/master/README.md). ================================================ FILE: notes/0.3.markdown ================================================ * \[New\] [#5](https://github.com/gseitz/sbt-release/issues/5): Added the key `tagName: SettingKey[String]("release-tag-name")` for easier customization of the used git tag name. Default setting: `tagName <<= (version in ThisBuild)(v => "v"+v)` * \[Fix\] [#6](https://github.com/gseitz/sbt-release/issues/6): The git command on Windows should be `git.exe`. Preliminary fix until the [sbt-git-plugin](http://github.com/jsuereth/sbt-git-plugin) is ready. * \[Improvement] [#7](https://github.com/gseitz/sbt-release/issues/7): Tests are not executed after setting the release version anymore (only before the switch). ================================================ FILE: notes/0.4.markdown ================================================ * \[Fix\] `test` and `publish` are now properly propagated from the root project to aggregated projects. * In a multi-project build, `Release.releaseSettings` should be mixed into every sub-project. ================================================ FILE: notes/0.5.markdown ================================================ * `sbt-release` now adopts the sbt plugin best practices. * Add release step to run `git push && git push --tags` at the end of the release process. * Release steps can now contribute up-front sanity checks (eg. the git-push step checks if a remote repository is configured). * Added Mercurial support. Thanks [@espenhw](https://github.com/espenhw) for the contribution. * The appropriate VCS (Git or Mercurial, in that order) is automatically detected. * `sbt-release` is now published to the [scala-sbt community repository](http://scalasbt.artifactoryonline.com) ================================================ FILE: notes/0.6.markdown ================================================ * [#17](https://github.com/sbt/sbt-release/issues/17) Added setting for customizing the tag comment. Thanks [Eric Bowman](https://github.com/ebowman). * [#18](https://github.com/sbt/sbt-release/issues/18) Some release steps didn't properly use the default choice for non-interactive builds (`with-defaults`). * Added setting for customizing the commit message (defaults to `Setting version to x.y.z`). * [#19](https://github.com/sbt/sbt-release/issues/19) Make `tagName`, `tagComment` and `commitMessage` a `TaskKey[String]`, so they are only evaluated when they're needed (i.e. during a release instead of everytime sbt is started). Thanks again, [Eric Bowman](https://github.com/ebowman). ================================================ FILE: notes/0.7.1.markdown ================================================ * Publish for sbt 0.13. * Run `clean` before running `test` to avoid stale artifacts (e.g. in case a source file has been deleted, but the class file is still in `target`) ================================================ FILE: notes/0.7.markdown ================================================ * Recursively search parent directories for VCS marker directory. [#25](https://github.com/sbt/sbt-release/pull/25) (Thanks [pvalkone](https://github.com/pvalkone)) * Add support for cross building/publishing. [#11](https://github.com/sbt/sbt-release/issues/11) * Print an informational message that `git push` writes to standard error. [#20](https://github.com/sbt/sbt-release/issues/20) ================================================ FILE: notes/0.8.1.markdown ================================================ * [#53](https://github.com/sbt/sbt-release/pull/53) Fix for git complaining about version.sbt being out of repository ================================================ FILE: notes/0.8.2.markdown ================================================ * [#61](https://github.com/sbt/sbt-release/issues/61) Clearer error message when `release with-defaults` fails due to an already existing tag. ================================================ FILE: notes/0.8.3.markdown ================================================ * [#55](https://github.com/sbt/sbt-release/issues/55) sbt `0.13.x` doesn't throw an exception after failed tests, thus the release would still go ahead. Thanks [@garycoady](https://github.com/garycoady)! * [#62](https://github.com/sbt/sbt-release/issues/62) Fix current working directory for VCS commands. Thanks [@laughedelic](https://github.com/laughedelic)! * [#64](https://github.com/sbt/sbt-release/issues/64) Allow writing the version string without `in ThisBuild`. This can be controlled via the setting `release-use-global-version`. ================================================ FILE: notes/0.8.4.markdown ================================================ * [#69](https://github.com/sbt/sbt-release/pull/69) Add Subversion support. Thanks [@timvlaer](https://github.com/timvlaer)! * [#70](https://github.com/sbt/sbt-release/issues/70) Fixed failed attempt at detecting the need for cross building. Cross building needs to enabled by either setting `ReleaseKeys.crossBuild := true` or launch the release with `release cross`. * [#78](https://github.com/sbt/sbt-release/pull/78) Use HEAD to determine current hash. Thanks [@larsrh](https://github.com/larsrh)! ================================================ FILE: notes/0.8.5.markdown ================================================ * [#82](https://github.com/sbt/sbt-release/pull/82) Ensure that task failure is propagated. Thanks [@jcrobak](https://github.com/jcrobak)! * [#49](https://github.com/sbt/sbt-release/pull/49) Make the used publish action configurable (for easier integration with sbt-pgp). Thanks [@jroper](https://github.com/jroper) ================================================ FILE: notes/0.8.markdown ================================================ * [#29](https://github.com/sbt/sbt-release/issues/29) The `release` task automatically runs a cross release build depending on whether `crossScalaVersions` contains a scala version other than the one defined by the `scalaVersion` setting key. * [#46](https://github.com/sbt/sbt-release/pull/46) Added a setting to more conveniently control the next version. (See [Convenient versioning](https://github.com/sbt/sbt-release#convenient-versioning)) * [#48](https://github.com/sbt/sbt-release/issues/48) Show the appropriate version pattern in the version format error message. ================================================ FILE: notes/1.0.5.markdown ================================================ * [#193][] The `checkSnapshotDependencies` release step now has `enabledCrossBuild` enabled by default. * [#185][] Adds a `default-tag-exists-answer` option to the `release` command to customise the default response to a tag already existing in the `tagRelease` release step. * [#194][] Makes `releaseVersion`, `releaseNextVersion`, and `releaseVersionBump` task keys instead of setting keys. [v1.0.4...v1.0.5](https://github.com/sbt/sbt-release/compare/v1.0.4%E2%80%A6v1.0.5) [#193]: https://github.com/sbt/sbt-release/pull/193 [#185]: https://github.com/sbt/sbt-release/pull/185 [#194]: https://github.com/sbt/sbt-release/pull/194 ================================================ FILE: notes/about.markdown ================================================ [sbt-release](https://github.com/sbt/sbt-release) is a plugin for [sbt](https://github.com/sbt/sbt) and brings a customizable release process to your projects - think maven-release-plugin, but without the scary parts. ================================================ FILE: project/build.properties ================================================ sbt.version=1.12.9 ================================================ FILE: project/plugins.sbt ================================================ libraryDependencies += "org.scala-sbt" %% "scripted-plugin" % sbtVersion.value addSbtPlugin("com.github.sbt" % "sbt-ci-release" % "1.11.2") addSbtPlugin("org.scalameta" % "sbt-scalafmt" % "2.5.6") ================================================ FILE: src/main/scala/Compat.scala ================================================ package sbtrelease import sbt.* import sbt.Def.ScopedKey import sbt.EvaluateTask.extractedTaskConfig import sbt.EvaluateTask.nodeView import sbt.EvaluateTask.runTask import sbt.EvaluateTask.withStreams import sbt.Keys.* import sbt.internal.Act import sbt.internal.Aggregation import sbt.internal.Aggregation.KeyValue import sbt.internal.ExtendableKeyIndex import sbt.std.Transform.DummyTaskMap import scala.language.reflectiveCalls object Compat { import Utilities.* def runTaskAggregated[T](taskKey: TaskKey[T], state: State): (State, Result[Seq[KeyValue[T]]]) = { import EvaluateTask.* val extra = DummyTaskMap(Nil) val extracted = state.extract val config = extractedTaskConfig(extracted, extracted.structure, state) val rkey = Utilities.resolve(taskKey.scopedKey, extracted) val keys = Aggregation.aggregate(rkey, ScopeMask(), extracted.structure.extra) val tasks = Act.keyValues(extracted.structure)(keys) val toRun = tasks.map { case KeyValue(k, t) => t.map(v => KeyValue(k, v)) }.join val roots = tasks.map { case KeyValue(k, _) => k } val (newS, result) = withStreams(extracted.structure, state) { str => val transform = nodeView(state, str, roots, extra) runTask(toRun, state, str, extracted.structure.index.triggers, config)(using transform) } (newS, result) } def projectScope(project: Reference): Scope = Scope(Select(project), Zero, Zero, Zero) // checking if publishTo is configured def checkPublishTo(st: State): State = { // getPublishTo fails if no publish repository is set up for projects with `skip in publish := false`. val ex = st.extract val ref = ex.get(thisProjectRef) val (_, skipPublish) = ex.runTask(ref / publish / skip, st) if (!skipPublish) { Classpaths.getPublishTo(ex.runTask(ref / (Global / publishTo), st)._2) } st } val FailureCommand = sbt.Exec("--failure--", None, None) def excludeKeys(keys: Set[AttributeKey[?]]): Setting[?] => Boolean = _.key match { case ScopedKey(Scope(_, Zero, Zero, _), key) if keys.contains(key) => true case _ => false } def crossVersions(st: State): Seq[String] = { // copied from https://github.com/sbt/sbt/blob/2d7ec47b13e02526174f897cca0aef585bd7b128/main/src/main/scala/sbt/Cross.scala#L40 val proj = Project.extract(st) import proj.* crossVersions(proj, currentRef) } private def crossVersions(extracted: Extracted, proj: ProjectRef): Seq[String] = { import extracted.* ((proj / crossScalaVersions) get structure.data) getOrElse { // reading scalaVersion is a one-time deal ((proj / scalaVersion) get structure.data).toSeq } } type Command = sbt.Exec @deprecated("will be removed") private[sbtrelease] def command2String(command: Command): String = command.commandLine @deprecated("will be removed") private[sbtrelease] def string2Exex(s: String): Command = sbt.Exec(s, None, None) // type aliases type StructureIndex = sbt.internal.StructureIndex type BuildStructure = sbt.internal.BuildStructure val BuildStreams = sbt.internal.BuildStreams type BuildUtil[Proj] = sbt.internal.BuildUtil[Proj] val BuildUtil = sbt.internal.BuildUtil val Index = sbt.internal.Index type KeyIndex = sbt.internal.KeyIndex val KeyIndex = sbt.internal.KeyIndex type LoadedBuildUnit = sbt.internal.LoadedBuildUnit // https://github.com/sbt/sbt/issues/3792 private[sbtrelease] def keyIndexApply( known: Iterable[ScopedKey[?]], projects: Map[URI, Set[String]], configurations: Map[String, Seq[Configuration]] ): ExtendableKeyIndex = try { // for sbt 1.1 KeyIndex .asInstanceOf[{ def apply( known: Iterable[ScopedKey[?]], projects: Map[URI, Set[String]], configurations: Map[String, Seq[Configuration]] ): ExtendableKeyIndex }] .apply(known = known, projects = projects, configurations = configurations) } catch { case _: NoSuchMethodException => // for sbt 1.0.x KeyIndex .asInstanceOf[{ def apply( known: Iterable[ScopedKey[?]], projects: Map[URI, Set[String]], ): ExtendableKeyIndex }] .apply(known = known, projects = projects) } // https://github.com/sbt/sbt/issues/3792 private[sbtrelease] def keyIndexAggregate( known: Iterable[ScopedKey[?]], extra: BuildUtil[?], projects: Map[URI, Set[String]], configurations: Map[String, Seq[Configuration]] ) = try { // for sbt 1.1 KeyIndex .asInstanceOf[{ def aggregate( known: Iterable[ScopedKey[?]], extra: BuildUtil[?], projects: Map[URI, Set[String]], configurations: Map[String, Seq[Configuration]] ): ExtendableKeyIndex }] .aggregate(known = known, extra = extra, projects = projects, configurations = configurations) } catch { case _: NoSuchMethodException => // for sbt 1.0.x KeyIndex .asInstanceOf[{ def aggregate( known: Iterable[ScopedKey[?]], extra: BuildUtil[?], projects: Map[URI, Set[String]] ): ExtendableKeyIndex }] .aggregate(known = known, extra = extra, projects = projects) } } ================================================ FILE: src/main/scala/ReleaseExtra.scala ================================================ package sbtrelease import sbt.* import sbt.Keys.* import sbt.Package.ManifestAttributes import sbtrelease.ReleasePlugin.autoImport.* import sbtrelease.ReleasePlugin.autoImport.ReleaseKeys.* import scala.annotation.tailrec import scala.sys.process.ProcessLogger object ReleaseStateTransformations { import Utilities.* lazy val checkSnapshotDependencies: ReleaseStep = ReleaseStep( { (st: State) => val thisRef = st.extract.get(thisProjectRef) val (newSt, result) = Compat.runTaskAggregated(thisRef / releaseSnapshotDependencies, st) val snapshotDeps = result.toEither match { case Right(value) => value.flatMap(_.value) case Left(cause) => sys.error("Error checking for snapshot dependencies: " + cause) } if (snapshotDeps.nonEmpty) { val useDefaults = extractDefault(newSt, "n") st.log.warn("Snapshot dependencies detected:\n" + snapshotDeps.mkString("\n")) useDefaults orElse SimpleReader.readLine("Do you want to continue (y/n)? [n] ") match { case Yes() => case _ => sys.error("Aborting release due to snapshot dependencies.") } } newSt }, enableCrossBuild = true ) lazy val inquireVersions: ReleaseStep = { (st: State) => val extracted = Project.extract(st) val useDefs = st.get(useDefaults).getOrElse(false) val currentV = extracted.get(version) val releaseFunc = extracted.runTask(releaseVersion, st)._2 val suggestedReleaseV = releaseFunc(currentV) st.log.info("Press enter to use the default value") // flatten the Option[Option[String]] as the get returns an Option, and the value inside is an Option val releaseV = readVersion(suggestedReleaseV, "Release version [%s] : ", useDefs, st.get(commandLineReleaseVersion).flatten) val nextFunc = extracted.runTask(releaseNextVersion, st)._2 val suggestedNextV = nextFunc(releaseV) // flatten the Option[Option[String]] as the get returns an Option, and the value inside is an Option val nextV = readVersion(suggestedNextV, "Next version [%s] : ", useDefs, st.get(commandLineNextVersion).flatten) st.put(versions, (releaseV, nextV)) } lazy val runClean: ReleaseStep = ReleasePluginCompat.runClean lazy val runTest: ReleaseStep = ReleaseStep( action = { (st: State) => if (!st.get(skipTests).getOrElse(false)) { val extracted = Project.extract(st) val ref = extracted.get(thisProjectRef) extracted.runAggregated(ref / Test / ReleasePluginCompat.testTask, st) } else st }, enableCrossBuild = true ) lazy val setReleaseVersion: ReleaseStep = setVersion(_._1) lazy val setNextVersion: ReleaseStep = setVersion(_._2) val globalVersionString = "ThisBuild / version := \"%s\"" private[this] val globalVersionStringOldSyntax = "version in ThisBuild := \"%s\"" val versionString = "version := \"%s\"" private[sbtrelease] def setVersion(selectVersion: Versions => String): ReleaseStep = { (st: State) => val vs = st .get(versions) .getOrElse(sys.error("No versions are set! Was this release part executed before inquireVersions?")) val selected = selectVersion(vs) st.log.info(s"Setting version to '${selected}'.") val useGlobal = st.extract.get(releaseUseGlobalVersion) val versionStr = ( if (useGlobal) { val v = Project.extract(st).get(sbtVersion) // use new slash syntax if sbt 1.1 or later // https://github.com/sbt/sbt/commit/21bd7c3a91a3407826 if (v.startsWith("0") || v.startsWith("1.0")) { globalVersionStringOldSyntax } else { globalVersionString } } else { versionString } ) format selected writeVersion(st, versionStr) reapply( Seq( if (useGlobal) ThisBuild / version := selected else version := selected ), st ) } private def vcs(st: State): Vcs = { st.extract .get(releaseVcs) .getOrElse(sys.error("Aborting release. Working directory is not a repository of a recognized VCS.")) } private def writeVersion(st: State, versionString: String): Unit = { val file = st.extract.get(releaseVersionFile) IO.writeLines(file, Seq(versionString)) } private[sbtrelease] lazy val initialVcsChecks = { (st: State) => val extracted = Project.extract(st) val hasUntrackedFiles = vcs(st).hasUntrackedFiles val hasModifiedFiles = vcs(st).hasModifiedFiles if (hasModifiedFiles) { sys.error(s"""Aborting release: unstaged modified files | |Modified files: | |${vcs(st).modifiedFiles.mkString(" - ", "\n", "")} """.stripMargin) } if (hasUntrackedFiles && !extracted.get(releaseIgnoreUntrackedFiles)) { sys.error( s"""Aborting release: untracked files. Remove them or specify 'releaseIgnoreUntrackedFiles := true' in settings | |Untracked files: | |${vcs(st).untrackedFiles.mkString(" - ", "\n", "")} """.stripMargin ) } st.log.info("Starting release process off commit: " + vcs(st).currentHash) st } lazy val commitReleaseVersion = ReleaseStep(commitReleaseVersionAction, initialVcsChecks) private[sbtrelease] lazy val commitReleaseVersionAction = { (st: State) => val newState = commitVersion(st, releaseCommitMessage) reapply( Seq[Setting[?]]( packageOptions += ManifestAttributes( "Vcs-Release-Hash" -> vcs(st).currentHash ) ), newState ) } lazy val commitNextVersion = { (st: State) => commitVersion(st, releaseNextCommitMessage) } private[sbtrelease] def commitVersion = { (st: State, commitMessage: TaskKey[String]) => val log = toProcessLogger(st) val file = st.extract.get(releaseVersionFile).getCanonicalFile val base = vcs(st).baseDir.getCanonicalFile val sign = st.extract.get(releaseVcsSign) val signOff = st.extract.get(releaseVcsSignOff) val relativePath = IO .relativize(base, file) .getOrElse(s"Version file [${file}] is outside of this VCS repository with base directory [${base}]!") vcs(st).add(relativePath) !! log val status = vcs(st).status.!!.trim val newState = if (status.nonEmpty) { val (state, msg) = st.extract.runTask(commitMessage, st) vcs(state).commit(msg, sign, signOff) ! log state } else { // nothing to commit. this happens if the version.sbt file hasn't changed. st } newState } lazy val tagRelease: ReleaseStep = { (st: State) => val defaultChoice = st.get(tagDefault) match { case Some(Some(td)) => Some(td) case _ => extractDefault(st, "a") } @tailrec def findTag(tag: String): Option[String] = { if (vcs(st).existsTag(tag)) { defaultChoice orElse SimpleReader.readLine( s"Tag [${tag}] exists! Overwrite, keep or abort or enter a new tag (o/k/a)? [a] " ) match { case Some("" | "a" | "A") => sys.error(s"Tag [${tag}] already exists. Aborting release!") case Some("k" | "K") => st.log.warn(s"The current tag [${tag}] does not point to the commit for this release!") None case Some("o" | "O") => st.log.warn( s"Overwriting a tag can cause problems if others have already seen the tag (see `${vcs(st).commandName} help tag`)!" ) Some(tag) case Some(newTag) => findTag(newTag) case None => sys.error("No tag entered. Aborting release!") } } else { Some(tag) } } val (tagState, tag) = st.extract.runTask(releaseTagName, st) val (commentState, comment) = st.extract.runTask(releaseTagComment, tagState) val tagToUse = findTag(tag) val sign = st.extract.get(releaseVcsSign) val log = toProcessLogger(commentState) tagToUse.foreach(vcs(commentState).tag(_, comment, sign) !! log) tagToUse map (t => reapply( Seq[Setting[?]]( packageOptions += ManifestAttributes("Vcs-Release-Tag" -> t) ), commentState ) ) getOrElse commentState } lazy val pushChanges: ReleaseStep = ReleaseStep(pushChangesAction, checkUpstream) private[sbtrelease] lazy val checkUpstream = { (st: State) => if (!vcs(st).hasUpstream) { sys.error( "No tracking branch is set up. Either configure a remote tracking branch, or remove the pushChanges release part." ) } val defaultChoice = extractDefault(st, "n") val log = toProcessLogger(st) st.log.info(s"Checking remote [${vcs(st).trackingRemote}] ...") if (vcs(st).checkRemote(vcs(st).trackingRemote) ! log != 0) { defaultChoice orElse SimpleReader.readLine("Error while checking remote. Still continue (y/n)? [n] ") match { case Yes() => // do nothing case _ => sys.error("Aborting the release!") } } if (vcs(st).isBehindRemote) { defaultChoice orElse SimpleReader.readLine( "The upstream branch has unmerged commits. A subsequent push will fail! Continue (y/n)? [n] " ) match { case Yes() => // do nothing case _ => sys.error("Merge the upstream commits and run `release` again.") } } st } private def toProcessLogger(st: State): ProcessLogger = new ProcessLogger { override def err(s: => String): Unit = st.log.info(s) override def out(s: => String): Unit = st.log.info(s) override def buffer[T](f: => T): T = st.log.buffer(f) } private[sbtrelease] lazy val pushChangesAction = { (st: State) => val defaultChoice = extractDefault(st, "y") val log = toProcessLogger(st) val vc = vcs(st) if (vc.hasUpstream) { defaultChoice orElse SimpleReader.readLine("Push changes to the remote repository (y/n)? [y] ") match { case Yes() | Some("") => val processLogger: ProcessLogger = if (vc.isInstanceOf[Git]) { // Git outputs to standard error, so use a logger that redirects stderr to info vc.stdErrorToStdOut(log) } else log vc.pushChanges !! processLogger case _ => st.log.warn("Remember to push the changes yourself!") } } else { st.log.info( s"Changes were NOT pushed, because no upstream branch is configured for the local branch [${vcs(st).currentBranch}]" ) } st } lazy val publishArtifacts = ReleaseStep( action = runPublishArtifactsAction, check = Compat.checkPublishTo, enableCrossBuild = true ) private[sbtrelease] lazy val runPublishArtifactsAction = { (st: State) => val extracted = st.extract val ref = extracted.get(thisProjectRef) extracted.runAggregated(ref / (Global / releasePublishArtifactsAction), st) } def readVersion(ver: String, prompt: String, useDef: Boolean, commandLineVersion: Option[String]): String = { commandLineVersion.getOrElse( if (useDef) ver else SimpleReader.readLine(prompt format ver) match { case Some("") => ver case Some(input) => Version(input).map(_.string).getOrElse(versionFormatError(input)) case None => sys.error("No version provided!") } ) } def reapply(settings: Seq[Setting[?]], state: State): State = { val extracted = state.extract import extracted.* val append = LoadCompat.transformSettings(Compat.projectScope(currentRef), currentRef.build, rootProject, settings) // We don't want even want to be able to save the settings that are applied to the session during the release cycle. // Just using an empty string works fine and in case the user calls `session save`, empty lines will be generated. val newSession = session.appendSettings(append map (a => (a, List.empty[String]))) BuiltinCommands.reapply(newSession, structure, state) } def crossExclude(s: Setting[?]): Boolean = Compat.excludeKeys(Set(scalaVersion.key, scalaHome.key))(s) // This is a copy of the state function for the command Cross.switchVersion private[sbtrelease] def switchScalaVersion(state: State, version: String): State = { val x = Project.extract(state) import x.{*, given} state.log.info("Setting scala version to " + version) val add = (GlobalScope / scalaVersion := version) :: (GlobalScope / scalaHome := None) :: Nil val cleared = session.mergeSettings.filterNot(crossExclude) val newStructure = LoadCompat.reapply(add ++ cleared, structure) Project.setProject(session, newStructure, state) } private[sbtrelease] def runCrossBuild(func: State => State): State => State = { state => val x = Project.extract(state) import x.* val versions = Compat.crossVersions(state) val current = (currentRef / scalaVersion) get structure.data val finalS = versions.foldLeft(state) { case (s, v) => func(switchScalaVersion(s, v)) } current.map(switchScalaVersion(finalS, _)).getOrElse(finalS) } } object ExtraReleaseCommands { import ReleaseStateTransformations.* private lazy val initialVcsChecksCommandKey = "release-vcs-checks" lazy val initialVcsChecksCommand = Command.command(initialVcsChecksCommandKey)(initialVcsChecks) private lazy val checkSnapshotDependenciesCommandKey = "release-check-snapshot-dependencies" lazy val checkSnapshotDependenciesCommand = Command.command(checkSnapshotDependenciesCommandKey)(checkSnapshotDependencies) private lazy val inquireVersionsCommandKey = "release-inquire-versions" lazy val inquireVersionsCommand = Command.command(inquireVersionsCommandKey)(inquireVersions) private lazy val setReleaseVersionCommandKey = "release-set-release-version" lazy val setReleaseVersionCommand = Command.command(setReleaseVersionCommandKey)(setReleaseVersion) private lazy val setNextVersionCommandKey = "release-set-next-version" lazy val setNextVersionCommand = Command.command(setNextVersionCommandKey)(setNextVersion) private lazy val commitReleaseVersionCommandKey = "release-commit-release-version" lazy val commitReleaseVersionCommand = Command.command(commitReleaseVersionCommandKey)(commitReleaseVersion) private lazy val commitNextVersionCommandKey = "release-commit-next-version" lazy val commitNextVersionCommand = Command.command(commitNextVersionCommandKey)(commitNextVersion) private lazy val tagReleaseCommandKey = "release-tag-release" lazy val tagReleaseCommand = Command.command(tagReleaseCommandKey)(tagRelease) private lazy val pushChangesCommandKey = "release-push-changes" lazy val pushChangesCommand = Command.command(pushChangesCommandKey)(pushChanges) } object Utilities { implicit class StateW(st: State) { def extract = Project.extract(st) } @deprecated("will be removed") private[sbtrelease] def stateW(st: State): StateW = new StateW(st) private[sbtrelease] def resolve[T](key: ScopedKey[T], extracted: Extracted): ScopedKey[T] = Project.mapScope(Scope.resolveScope(GlobalScope, extracted.currentRef.build, extracted.rootProject))(key.scopedKey) object Yes { def unapply(s: Option[String]) = s.exists(_.toLowerCase == "y") } def extractDefault(st: State, default: String): Option[String] = { val useDefs = st.get(useDefaults).getOrElse(false) if (useDefs) Some(default) else None } } ================================================ FILE: src/main/scala/ReleasePlugin.scala ================================================ package sbtrelease import java.io.Serializable import sbt.* import sbt.Keys.* import sbt.complete.DefaultParsers.* import sbt.complete.Parser import sbtrelease.Version.Bump object ReleasePlugin extends AutoPlugin { object autoImport { @transient val releaseSnapshotDependencies = taskKey[Seq[ModuleID]]("Calculate the snapshot dependencies for a build") val releaseProcess = settingKey[Seq[ReleaseStep]]("The release process") @transient val releaseVersion = taskKey[String => String]("The release version") @transient val releaseNextVersion = taskKey[String => String]("The next release version") @transient val releaseVersionBump = taskKey[Version.Bump]("How the version should be incremented") @transient val releaseTagName = taskKey[String]("The name of the tag") @transient val releaseTagComment = taskKey[String]("The comment to use when tagging") @transient val releaseCommitMessage = taskKey[String]("The commit message to use when tagging") @transient val releaseNextCommitMessage = taskKey[String]("The commit message to use for next iteration") val releaseCrossBuild = settingKey[Boolean]("Whether the release should be cross built") val releaseVersionFile = settingKey[File]("The file to write the version to") val releaseUseGlobalVersion = settingKey[Boolean]("Whether to use a global version") val releaseIgnoreUntrackedFiles = settingKey[Boolean]("Whether to ignore untracked files") val releaseVcsSign = settingKey[Boolean]("Whether to sign VCS commits and tags") val releaseVcsSignOff = settingKey[Boolean]("Whether to signoff VCS commits") val releaseVcs = settingKey[Option[Vcs]]("The VCS to use") @transient val releasePublishArtifactsAction = taskKey[Unit]("The action that should be performed to publish artifacts") lazy val ReleaseTransformations = sbtrelease.ReleaseStateTransformations case class ReleaseStep(action: State => State, check: State => State = identity, enableCrossBuild: Boolean = false) object ReleaseStep { implicit def func2ReleasePart(f: State => State): ReleaseStep = ReleaseStep(f) implicit def releasePart2Func(rp: ReleaseStep): State => State = rp.action } @deprecated("Use releaseStepTaskAggregated", "1.0.0") def releaseTask[T](key: TaskKey[T]) = { (st: State) => Project.extract(st).runAggregated(key, st) } /** * Convert the given task key to a release step action. */ def releaseStepTask[T](key: TaskKey[T]) = { (st: State) => Project.extract(st).runTask(key, st)._1 } /** * Convert the given task key to a release step action that gets run aggregated. */ def releaseStepTaskAggregated[T](key: TaskKey[T]): State => State = { (st: State) => Project.extract(st).runAggregated(key, st) } /** * Convert the given input task key and input to a release step action. */ def releaseStepInputTask[T](key: InputKey[T], input: String = ""): State => State = { (st: State) => import EvaluateTask.* val extracted = Project.extract(st) val inputTask = extracted.get(Scoped.scopedSetting(key.scope, key.key)) val task = Parser.parse(input, inputTask.parser(st)) match { case Right(t) => t case Left(msg) => sys.error(s"Invalid programmatic input:\n$msg") } val config = extractedTaskConfig(extracted, extracted.structure, st) withStreams(extracted.structure, st) { str => val nv = nodeView(st, str, key :: Nil) val (newS, result) = runTask(task, st, str, extracted.structure.index.triggers, config)(using nv) (newS, processResult2(result)) }._1 } /** * Convert the given command and input to a release step action */ def releaseStepCommand(command: Command, input: String = ""): State => State = { (st: State) => Parser.parse(input, command.parser(st)) match { case Right(cmd) => cmd() case Left(msg) => sys.error(s"Invalid programmatic input:\n$msg") } } /** * Convert the given command string to a release step action */ def releaseStepCommand(command: String): State => State = { (st: State) => Parser.parse(command, st.combinedParser) match { case Right(cmd) => cmd() case Left(msg) => sys.error(s"Invalid programmatic input:\n$msg") } } /** * Convert the given command string to a release step action, preserving and invoking remaining commands */ def releaseStepCommandAndRemaining(command: String): State => State = { (initState: State) => import Compat.* @annotation.tailrec def runCommand(command: Compat.Command, state: State): State = { val nextState = Parser.parse(command.commandLine, state.combinedParser) match { case Right(cmd) => cmd() case Left(msg) => sys.error(s"Invalid programmatic input:\n$msg") } nextState.remainingCommands.toList match { case Nil => nextState.copy(remainingCommands = initState.remainingCommands) case Compat.FailureCommand :: tail => nextState.copy(remainingCommands = FailureCommand +: initState.remainingCommands) case head :: tail => runCommand(head, nextState.copy(remainingCommands = tail)) } } runCommand(Exec(command, None, None), initState.copy(remainingCommands = Nil)) } object ReleaseKeys { val versions = AttributeKey[Versions]("releaseVersions") val commandLineReleaseVersion = AttributeKey[Option[String]]("release-input-release-version") val commandLineNextVersion = AttributeKey[Option[String]]("release-input-next-version") val useDefaults = AttributeKey[Boolean]("releaseUseDefaults") val skipTests = AttributeKey[Boolean]("releaseSkipTests") val cross = AttributeKey[Boolean]("releaseCross") val tagDefault = AttributeKey[Option[String]]("release-default-tag-exists-answer") private lazy val releaseCommandKey = "release" private val FailureCommand = Compat.FailureCommand private[this] val WithDefaults: Parser[ParseResult] = (Space ~> token("with-defaults")) ^^^ ParseResult.WithDefaults private[this] val SkipTests: Parser[ParseResult] = (Space ~> token("skip-tests")) ^^^ ParseResult.SkipTests private[this] val CrossBuild: Parser[ParseResult] = (Space ~> token("cross")) ^^^ ParseResult.CrossBuild private[this] val ReleaseVersion: Parser[ParseResult] = (Space ~> token("release-version") ~> Space ~> token( StringBasic, "" )) map ParseResult.ReleaseVersion private[this] val NextVersion: Parser[ParseResult] = (Space ~> token("next-version") ~> Space ~> token(StringBasic, "")) map ParseResult.NextVersion private[this] val TagDefault: Parser[ParseResult] = (Space ~> token("default-tag-exists-answer") ~> Space ~> token( StringBasic, "o|k|a|" )) map ParseResult.TagDefault private[this] sealed abstract class ParseResult extends Product with Serializable private[this] object ParseResult { final case class ReleaseVersion(value: String) extends ParseResult object ReleaseVersion extends (String => ParseResult) final case class NextVersion(value: String) extends ParseResult object NextVersion extends (String => ParseResult) final case class TagDefault(value: String) extends ParseResult object TagDefault extends (String => ParseResult) case object WithDefaults extends ParseResult case object SkipTests extends ParseResult case object CrossBuild extends ParseResult } private[this] val releaseParser: Parser[Seq[ParseResult]] = (ReleaseVersion | NextVersion | WithDefaults | SkipTests | CrossBuild | TagDefault).* val releaseCommand: Command = Command(releaseCommandKey)(_ => releaseParser) { (st, args) => val extracted = Project.extract(st) val releaseParts = extracted.get(releaseProcess) val crossEnabled = extracted.get(releaseCrossBuild) || args.contains(ParseResult.CrossBuild) val startState = st .copy(onFailure = Some(FailureCommand)) .put(useDefaults, args.contains(ParseResult.WithDefaults)) .put(skipTests, args.contains(ParseResult.SkipTests)) .put(cross, crossEnabled) .put(tagDefault, args.collectFirst { case ParseResult.TagDefault(value) => value }) .put(commandLineReleaseVersion, args.collectFirst { case ParseResult.ReleaseVersion(value) => value }) .put(commandLineNextVersion, args.collectFirst { case ParseResult.NextVersion(value) => value }) val initialChecks = releaseParts.map(_.check) def filterFailure(f: State => State)(s: State): State = { s.remainingCommands match { case FailureCommand :: tail => s.fail case _ => f(s) } } val removeFailureCommand = { (s: State) => s.remainingCommands match { case FailureCommand :: tail => s.copy(remainingCommands = tail) case _ => s } } val failureCheck = { (s: State) => filterFailure(_.copy(onFailure = Some(FailureCommand)))(s) } val process = releaseParts.map { step => if (step.enableCrossBuild && crossEnabled) { filterFailure(ReleaseStateTransformations.runCrossBuild(step.action)) _ } else filterFailure(step.action) _ } initialChecks.foreach(_(startState)) Function.chain( (process :+ removeFailureCommand).flatMap(Seq(_, failureCheck)) )(startState) } } } import ReleaseStateTransformations.* import autoImport.* import autoImport.ReleaseKeys.* override def trigger = allRequirements val runtimeVersion = Def.task { val v1 = (ThisBuild / version).value val v2 = version.value if (releaseUseGlobalVersion.value) v1 else v2 } override def projectSettings = Seq[Setting[?]]( releaseSnapshotDependencies := { val moduleIds = ReleasePluginCompat.moduleIds.value val snapshots = moduleIds.filter(m => m.isChanging || m.revision.endsWith("-SNAPSHOT")) snapshots }, releaseVersion := { rawVersion => Version(rawVersion).map { version => releaseVersionBump.value match { case Bump.Next => if (version.isSnapshot) { version.withoutSnapshot.unapply } else { expectedSnapshotVersionError(rawVersion) } case _ => version.withoutQualifier.unapply } }.getOrElse(versionFormatError(rawVersion)) }, releaseVersionBump := Version.Bump.default, releaseNextVersion := { ver => Version(ver).map(_.bump(releaseVersionBump.value).asSnapshot.unapply).getOrElse(versionFormatError(ver)) }, releaseUseGlobalVersion := true, releaseCrossBuild := false, releaseTagName := s"v${runtimeVersion.value}", releaseTagComment := s"Releasing ${runtimeVersion.value}", releaseCommitMessage := s"Setting version to ${runtimeVersion.value}", releaseNextCommitMessage := s"Setting version to ${runtimeVersion.value}", releaseVcs := Vcs.detect(baseDirectory.value), releaseVcsSign := false, releaseVcsSignOff := false, releaseVersionFile := baseDirectory.value / "version.sbt", releasePublishArtifactsAction := publish.value, releaseIgnoreUntrackedFiles := false, releaseProcess := Seq[ReleaseStep]( checkSnapshotDependencies, inquireVersions, runClean, runTest, setReleaseVersion, commitReleaseVersion, tagRelease, publishArtifacts, setNextVersion, commitNextVersion, pushChanges ), commands += releaseCommand ) lazy val extraReleaseCommands = { import ExtraReleaseCommands.* Seq[Setting[?]]( commands ++= Seq( checkSnapshotDependenciesCommand, inquireVersionsCommand, setReleaseVersionCommand, setNextVersionCommand, initialVcsChecksCommand, commitReleaseVersionCommand, commitNextVersionCommand, tagReleaseCommand, pushChangesCommand ) ) } } ================================================ FILE: src/main/scala/Vcs.scala ================================================ package sbtrelease import java.io.File import sbt.* import sys.process.Process import sys.process.ProcessBuilder import sys.process.ProcessLogger trait Vcs { val commandName: String val baseDir: File def cmd(args: Any*): ProcessBuilder def status: ProcessBuilder def currentHash: String def add(files: String*): ProcessBuilder def commit(message: String, sign: Boolean, signOff: Boolean): ProcessBuilder def existsTag(name: String): Boolean def checkRemote(remote: String): ProcessBuilder def tag(name: String, comment: String, sign: Boolean): ProcessBuilder def hasUpstream: Boolean def trackingRemote: String def isBehindRemote: Boolean def pushChanges: ProcessBuilder def currentBranch: String def hasUntrackedFiles: Boolean = untrackedFiles.nonEmpty def untrackedFiles: Seq[String] def hasModifiedFiles: Boolean = modifiedFiles.nonEmpty def modifiedFiles: Seq[String] protected def executableName(command: String) = { val maybeOsName = sys.props.get("os.name").map(_.toLowerCase) val maybeIsWindows = maybeOsName.filter(_.contains("windows")) maybeIsWindows.map(_ => command + ".exe").getOrElse(command) } protected val devnull: ProcessLogger = new ProcessLogger { override def out(s: => String): Unit = {} override def err(s: => String): Unit = {} override def buffer[T](f: => T): T = f } def stdErrorToStdOut(delegate: ProcessLogger): ProcessLogger = new ProcessLogger { override def out(s: => String): Unit = delegate.out(s) override def err(s: => String): Unit = delegate.out(s) override def buffer[T](f: => T): T = delegate.buffer(f) } } object Vcs { def detect(dir: File): Option[Vcs] = { Stream(Git, Mercurial, Subversion).flatMap(comp => comp.isRepository(dir).map(comp.mkVcs(_))).headOption } } trait GitLike extends Vcs { private lazy val exec = executableName(commandName) def cmd(args: Any*): ProcessBuilder = Process(exec +: args.map(_.toString), baseDir) def add(files: String*) = cmd(("add" +: files)*) } trait VcsCompanion { protected val markerDirectory: String // Using the new git worktree feature the dir is now a file, so checking for exists should be enough def isRepository(dir: File): Option[File] = if (new File(dir, markerDirectory).exists) Some(dir) else Option(dir.getParentFile).flatMap(isRepository) def mkVcs(baseDir: File): Vcs } object Mercurial extends VcsCompanion { protected val markerDirectory = ".hg" def mkVcs(baseDir: File) = new Mercurial(baseDir) } class Mercurial(val baseDir: File) extends Vcs with GitLike { val commandName = "hg" private def andSign(sign: Boolean, proc: ProcessBuilder) = if (sign) proc #&& cmd("sign") else proc def status = cmd("status") def currentHash = cmd("identify", "-i").!!.trim def existsTag(name: String) = cmd("tags").!!.linesIterator.exists(_.endsWith(" " + name)) def commit(message: String, sign: Boolean, signOff: Boolean) = andSign(sign, cmd("commit", "-m", message)) def tag(name: String, comment: String, sign: Boolean) = andSign(sign, cmd("tag", "-f", "-m", comment, name)) def hasUpstream = cmd("paths", "default") ! devnull == 0 def trackingRemote = "default" def isBehindRemote = cmd("incoming", "-b", ".", "-q") ! devnull == 0 def pushChanges = cmd("push", "-b", ".") def currentBranch = cmd("branch").!!.trim // FIXME: This is utterly bogus, but I cannot find a good way... def checkRemote(remote: String) = cmd("id", "-n") def untrackedFiles = cmd("status", "-un").lineStream def modifiedFiles = cmd("status", "-mn").lineStream } object Git extends VcsCompanion { protected val markerDirectory = ".git" def mkVcs(baseDir: File) = new Git(baseDir) private final case class GitFlag(on: Boolean, flag: String) } class Git(val baseDir: File) extends Vcs with GitLike { val commandName = "git" import Git.GitFlag private lazy val trackingBranchCmd = cmd("config", s"branch.${currentBranch}.merge") private def trackingBranch: String = trackingBranchCmd.!!.trim.stripPrefix("refs/heads/") private lazy val trackingRemoteCmd: ProcessBuilder = cmd("config", s"branch.${currentBranch}.remote") def trackingRemote: String = trackingRemoteCmd.!!.trim def hasUpstream = trackingRemoteCmd ! devnull == 0 && trackingBranchCmd ! devnull == 0 def currentBranch = cmd("symbolic-ref", "HEAD").!!.trim.stripPrefix("refs/heads/") def currentHash = revParse("HEAD") private def revParse(name: String) = cmd("rev-parse", name).!!.trim def isBehindRemote = (cmd("rev-list", s"${currentBranch}..${trackingRemote}/${trackingBranch}") !! devnull).trim.nonEmpty private def withFlags(flags: Seq[GitFlag])(args: String*): Seq[String] = { val appended = flags.collect { case GitFlag(true, flag) => s"-$flag" } args ++ appended } def commit(message: String, sign: Boolean, signOff: Boolean) = { val gitFlags = List(GitFlag(sign, "S"), GitFlag(signOff, "s")) cmd(withFlags(gitFlags)("commit", "-m", message)*) } def tag(name: String, comment: String, sign: Boolean) = cmd(withFlags(List(GitFlag(sign, "s")))("tag", "-f", "-a", name, "-m", comment)*) def existsTag(name: String) = cmd("show-ref", "--quiet", "--tags", "--verify", "refs/tags/" + name) ! devnull == 0 def checkRemote(remote: String) = fetch(remote) def fetch(remote: String) = cmd("fetch", remote) def status = cmd("status", "--porcelain") def pushChanges = pushCurrentBranch #&& pushTags private def pushCurrentBranch = { val localBranch = currentBranch cmd("push", trackingRemote, s"${localBranch}:${trackingBranch}") } private def pushTags = cmd("push", "--tags", trackingRemote) def untrackedFiles = cmd("ls-files", "--other", "--exclude-standard").lineStream def modifiedFiles = cmd("ls-files", "--modified", "--exclude-standard").lineStream } object Subversion extends VcsCompanion { override def mkVcs(baseDir: File): Vcs = new Subversion(baseDir) override protected val markerDirectory: String = ".svn" } class Subversion(val baseDir: File) extends Vcs { override val commandName = "svn" private lazy val exec = executableName(commandName) override def cmd(args: Any*): ProcessBuilder = Process(exec +: args.map(_.toString), baseDir) override def modifiedFiles = cmd("status", "-q").lineStream override def untrackedFiles = cmd("status").lineStream.filter(_.startsWith("?")) override def add(files: String*) = { val filesToAdd = files.filterNot(isFileUnderVersionControl) if (!filesToAdd.isEmpty) cmd(("add" +: filesToAdd)*) else noop } override def commit(message: String, sign: Boolean, signOff: Boolean) = { require(!sign, "Signing not supported in Subversion.") require(!signOff, "Signing off not supported in Subversion.") cmd("commit", "-m", message) } override def currentBranch: String = workingDirSvnUrl.substring(workingDirSvnUrl.lastIndexOf("/") + 1) override def pushChanges: ProcessBuilder = commit("push changes", false, false) override def isBehindRemote: Boolean = false override def trackingRemote: String = "" override def hasUpstream: Boolean = true override def tag(name: String, comment: String, sign: Boolean): ProcessBuilder = { require(!sign, "Signing not supported in Subversion.") val tagUrl = getSvnTagUrl(name) if (existsTag(name)) { val deleteTagComment = comment + ", \ndelete tag " + name + " to create a new one." cmd("del", tagUrl, "-m", deleteTagComment).!! } cmd("copy", workingDirSvnUrl, tagUrl, "-m", comment) } override def checkRemote(remote: String): ProcessBuilder = noop override def existsTag(name: String): Boolean = { Try(cmd("info", getSvnTagUrl(name)).!!).nonEmpty } override def currentHash: String = "" override def status: ProcessBuilder = cmd("status", "-q") lazy val workingDirSvnUrl: String = { val svnInfo = cmd("info").!! val svnInfoUrlKey = "URL: " val urlStartIdx = svnInfo.indexOf(svnInfoUrlKey) + svnInfoUrlKey.length svnInfo.substring(urlStartIdx, svnInfo.indexOf('\n', urlStartIdx) - 1) } lazy val repoRoot: String = { val svnBaseUrlEndIdxOptions = List( workingDirSvnUrl.indexOf("/trunk"), workingDirSvnUrl.indexOf("/branches"), workingDirSvnUrl.indexOf("/tags") ).filter(_ >= 0) require( !svnBaseUrlEndIdxOptions.isEmpty, "No /trunk, /branches or /tags part found in svn url. Base url cannot be extracted." ) val svnBaseUrlEndIdx = svnBaseUrlEndIdxOptions.head workingDirSvnUrl.substring(0, svnBaseUrlEndIdx + 1) } private def getSvnTagUrl(name: String): String = repoRoot + "tags/" + name private def isFileUnderVersionControl(file: String): Boolean = Try(cmd("info", file).!!).nonEmpty private def noop: ProcessBuilder = status } private[sbtrelease] object Try { def apply[A](f: => A): Option[A] = scala.util.control.Exception.allCatch.opt(f) } ================================================ FILE: src/main/scala/Version.scala ================================================ package sbtrelease import scala.util.matching.Regex import util.control.Exception.* object Version { sealed trait Bump { def bump: Version => Version } object Bump { /** * Strategy to always bump the major version by default. Ex. 1.0.0 would be bumped to 2.0.0 */ case object Major extends Bump { def bump: Version => Version = _.bumpMajor } /** * Strategy to always bump the minor version by default. Ex. 1.0.0 would be bumped to 1.1.0 */ case object Minor extends Bump { def bump: Version => Version = _.bumpMinor } /** * Strategy to always bump the bugfix version by default. Ex. 1.0.0 would be bumped to 1.0.1 */ case object Bugfix extends Bump { def bump: Version => Version = _.bumpBugfix } /** * Strategy to always bump the nano version by default. Ex. 1.0.0.0 would be bumped to 1.0.0.1 */ case object Nano extends Bump { def bump: Version => Version = _.bumpNano } /** * Strategy to always increment to the next version from smallest to greatest, including prerelease versions * Ex: * Major: 1 becomes 2 * Minor: 1.0 becomes 1.1 * Bugfix: 1.0.0 becomes 1.0.1 * Nano: 1.0.0.0 becomes 1.0.0.1 * Qualifier with version number: 1.0-RC1 becomes 1.0-RC2 * Qualifier without version number: 1.0-alpha becomes 1.0 */ case object Next extends Bump { def bump: Version => Version = _.bumpNext } /** * Strategy to always increment to the next version from smallest to greatest, excluding prerelease versions * Ex: * Major: 1 becomes 2 * Minor: 1.0 becomes 1.1 * Bugfix: 1.0.0 becomes 1.0.1 * Nano: 1.0.0.0 becomes 1.0.0.1 * Qualifier with version number: 1.0-RC1 becomes 1.0 * Qualifier without version number: 1.0-alpha becomes 1.0 */ case object NextStable extends Bump { def bump: Version => Version = _.bumpNextStable } val default: Bump = Next } val VersionR: Regex = """([0-9]+)((?:\.[0-9]+)+)?([\.\-0-9a-zA-Z]*)?""".r val PreReleaseQualifierR: Regex = """[\.-](?i:rc|m|alpha|beta)[\.-]?[0-9]*""".r def apply(s: String): Option[Version] = { allCatch opt { val VersionR(maj, subs, qual) = s // parse the subversions (if any) to a Seq[Int] val subSeq: Seq[Int] = Option(subs) map { str => // split on . and remove empty strings str.split('.').filterNot(_.trim.isEmpty).map(_.toInt).toSeq } getOrElse Nil Version(maj.toInt, subSeq, Option(qual).filterNot(_.isEmpty)) } } } case class Version(major: Int, subversions: Seq[Int], qualifier: Option[String]) { @deprecated("Use .bumpNext or .bumpNextStable instead") def bump: Version = bumpNext def bumpNext: Version = { val bumpedPrereleaseVersionOpt = qualifier.collect { case rawQualifier @ Version.PreReleaseQualifierR() => val qualifierEndsWithNumberRegex = """[0-9]*$""".r val opt = for { versionNumberQualifierStr <- qualifierEndsWithNumberRegex.findFirstIn(rawQualifier) versionNumber <- Try(versionNumberQualifierStr.toInt) .toRight( new Exception( s"Version number not parseable to a number. Version number received: $versionNumberQualifierStr" ) ) .toOption newVersionNumber = versionNumber + 1 newQualifier = rawQualifier.replaceFirst(versionNumberQualifierStr, newVersionNumber.toString) } yield Version(major, subversions, Some(newQualifier)) opt.getOrElse(this.withoutQualifier) } bumpNextGeneric(bumpedPrereleaseVersionOpt) } private def bumpNextGeneric(bumpedPrereleaseVersionOpt: Option[Version]): Version = { def maybeBumpedLastSubversion = bumpSubversionOpt(subversions.length - 1) def bumpedMajor = copy(major = major + 1) bumpedPrereleaseVersionOpt.orElse(maybeBumpedLastSubversion).getOrElse(bumpedMajor) } def bumpNextStable: Version = { val bumpedPrereleaseVersionOpt = qualifier.collect { case Version.PreReleaseQualifierR() => withoutQualifier } bumpNextGeneric(bumpedPrereleaseVersionOpt) } def bumpMajor: Version = copy(major = major + 1, subversions = Seq.fill(subversions.length)(0)) def bumpMinor: Version = maybeBumpSubversion(0) def bumpBugfix: Version = maybeBumpSubversion(1) def bumpNano: Version = maybeBumpSubversion(2) def maybeBumpSubversion(idx: Int): Version = bumpSubversionOpt(idx) getOrElse this private def bumpSubversionOpt(idx: Int) = { val bumped = subversions.drop(idx) val reset = bumped.drop(1).length bumped.headOption map { head => val patch = (head + 1) +: Seq.fill(reset)(0) copy(subversions = subversions.patch(idx, patch, patch.length)) } } def bump(bumpType: Version.Bump): Version = bumpType.bump(this) def withoutQualifier: Version = copy(qualifier = None) def asSnapshot: Version = copy(qualifier = qualifier.map { qualifierStr => s"$qualifierStr-SNAPSHOT" }.orElse(Some("-SNAPSHOT"))) def isSnapshot: Boolean = qualifier.exists { qualifierStr => val snapshotRegex = """(^.*)-SNAPSHOT$""".r qualifierStr.matches(snapshotRegex.regex) } def withoutSnapshot: Version = copy(qualifier = qualifier.flatMap { qualifierStr => val snapshotRegex = """-SNAPSHOT""".r val newQualifier = snapshotRegex.replaceFirstIn(qualifierStr, "") if (newQualifier == qualifierStr) { None } else { Some(newQualifier) } }) @deprecated("Use .unapply instead") def string: String = unapply def unapply: String = "" + major + mkString(subversions) + qualifier.getOrElse("") private def mkString(parts: Seq[Int]) = parts.map("." + _).mkString } ================================================ FILE: src/main/scala/package.scala ================================================ package object sbtrelease { type Versions = (String, String) def versionFormatError(version: String) = sys.error(s"Version [$version] format is not compatible with " + Version.VersionR.pattern.toString) def expectedSnapshotVersionError(version: String) = sys.error(s"Expected snapshot version. Received: $version") } ================================================ FILE: src/main/scala-2/LoadCompat.scala ================================================ package sbtrelease import sbt.* import sbt.Def.ScopedKey import sbt.Keys.resolvedScoped import sbt.Keys.streams // sbt.Load was made private in sbt 1.0 // the core developers recommend copying the required methods: https://github.com/sbt/sbt/issues/3296#issuecomment-315218050 object LoadCompat { import Compat.* def transformSettings( thisScope: Scope, uri: URI, rootProject: URI => String, settings: Seq[Setting[?]] ): Seq[Setting[?]] = Project.transform(Scope.resolveScope(thisScope, uri, rootProject), settings) // Reevaluates settings after modifying them. Does not recompile or reload any build components. def reapply(newSettings: Seq[Setting[?]], structure: BuildStructure)(implicit display: Show[ScopedKey[?]] ): BuildStructure = { val transformed = finalTransforms(newSettings) val (compiledMap, newData) = Def.makeWithCompiledMap(transformed)(using structure.delegates, structure.scopeLocal, display) val newIndex = structureIndex( newData, transformed, index => BuildUtil(structure.root, structure.units, index, newData), structure.units ) val newStreams = BuildStreams.mkStreams(structure.units, structure.root, newData) new BuildStructure( units = structure.units, root = structure.root, settings = transformed, data = newData, index = newIndex, streams = newStreams, delegates = structure.delegates, scopeLocal = structure.scopeLocal, compiledMap = compiledMap, ) } // map dependencies on the special tasks: // 1. the scope of 'streams' is the same as the defining key and has the task axis set to the defining key // 2. the defining key is stored on constructed tasks: used for error reporting among other things // 3. resolvedScoped is replaced with the defining key as a value // Note: this must be idempotent. def finalTransforms(ss: Seq[Setting[?]]): Seq[Setting[?]] = { def mapSpecial(to: ScopedKey[?]) = new (ScopedKey ~> ScopedKey) { def apply[T](key: ScopedKey[T]) = if (key.key == streams.key) ScopedKey(Scope.fillTaskAxis(Scope.replaceThis(to.scope)(key.scope), to.key), key.key) else key } def setDefining[T] = (key: ScopedKey[T], value: T) => value match { case tk: Task[t] => setDefinitionKey(tk, key).asInstanceOf[T] case ik: InputTask[t] => ik.mapTask(tk => setDefinitionKey(tk, key)).asInstanceOf[T] case _ => value } def setResolved(defining: ScopedKey[?]) = new (ScopedKey ~> Option) { def apply[T](key: ScopedKey[T]): Option[T] = key.key match { case resolvedScoped.key => Some(defining.asInstanceOf[T]) case _ => None } } ss.map(s => s mapConstant setResolved(s.key) mapReferenced mapSpecial(s.key) mapInit setDefining) } def structureIndex( data: Settings[Scope], settings: Seq[Setting[?]], extra: KeyIndex => BuildUtil[?], projects: Map[URI, LoadedBuildUnit] ): StructureIndex = { val keys = Index.allKeys(settings) val attributeKeys = Index.attributeKeys(data) ++ keys.map(_.key) val scopedKeys = keys ++ data.allKeys((s, k) => ScopedKey(s, k)).toVector val projectsMap = projects.map { case (k, v) => k -> v.defined.keySet } val configsMap: Map[String, Seq[Configuration]] = projects.values.flatMap(bu => bu.defined map { case (k, v) => (k, v.configurations) }).toMap val keyIndex = keyIndexApply(scopedKeys.toVector, projectsMap, configsMap) val aggIndex = keyIndexAggregate(scopedKeys.toVector, extra(keyIndex), projectsMap, configsMap) new StructureIndex( Index.stringToKeyMap(attributeKeys), Index.taskToKeyMap(data), Index.triggers(data), keyIndex, aggIndex ) } def setDefinitionKey[T](tk: Task[T], key: ScopedKey[?]): Task[T] = if (isDummy(tk)) tk else Task(tk.info.set(Keys.taskDefinitionKey, key), tk.work) private def isDummy(t: Task[?]): Boolean = t.info.attributes.get(isDummyTask) getOrElse false private val Invisible = Int.MaxValue private val isDummyTask = AttributeKey[Boolean]( "is-dummy-task", "Internal: used to identify dummy tasks. sbt injects values for these tasks at the start of task execution.", Invisible ) } ================================================ FILE: src/main/scala-2/ReleasePluginCompat.scala ================================================ package sbtrelease import sbt.* import sbt.Keys.* import sbtrelease.ReleasePlugin.autoImport.ReleaseStep private[sbtrelease] object ReleasePluginCompat { def testTask: TaskKey[?] = sbt.Keys.test val runClean: ReleaseStep = ReleaseStep( action = { st => val extracted = Project.extract(st) val ref = extracted.get(thisProjectRef) extracted.runAggregated(ref / (Global / clean), st) } ) val moduleIds: Def.Initialize[Task[Seq[ModuleID]]] = Def.task( (Runtime / managedClasspath).value.flatMap(_.get(moduleID.key)) ) } ================================================ FILE: src/main/scala-3/LoadCompat.scala ================================================ package sbt import sbt.internal.BuildStructure object LoadCompat { def transformSettings( thisScope: Scope, uri: URI, rootProject: URI => String, settings: Seq[Setting[?]] ): Seq[Setting[?]] = sbt.internal.Load.transformSettings(thisScope, uri, rootProject, settings) def reapply( newSettings: Seq[Setting[?]], structure: BuildStructure )(using display: Show[ScopedKey[?]]): BuildStructure = sbt.internal.Load.reapply(newSettings, structure) } ================================================ FILE: src/main/scala-3/ReleasePluginCompat.scala ================================================ package sbtrelease import sbt.* import sbt.Keys.* import sbtrelease.ReleasePlugin.autoImport.ReleaseStep import sbtrelease.ReleasePlugin.autoImport.releaseStepCommandAndRemaining private[sbtrelease] object ReleasePluginCompat { def testTask: TaskKey[?] = sbt.Keys.testFull val runClean: ReleaseStep = releaseStepCommandAndRemaining(BasicCommandStrings.CleanFull) val moduleIds: Def.Initialize[Task[Seq[ModuleID]]] = Def.task( (Runtime / managedClasspath).value.flatMap(_.get(Keys.moduleIDStr)).map(Classpaths.moduleIdJsonKeyFormat.read) ) } ================================================ FILE: src/sbt-test/sbt-release/command-line-version-numbers/build.sbt ================================================ import ReleaseTransformations._ import sbt.complete.DefaultParsers._ name := "command-line-version-numbers" publishTo := Some(Resolver.file("file", file("."))) releaseProcess := Seq[ReleaseStep]( checkSnapshotDependencies, inquireVersions, runTest, setReleaseVersion, publishArtifacts, setNextVersion ) scalaVersion := "2.13.18" val checkContentsOfVersionSbt = inputKey[Unit]("Check that the contents of version.sbt is as expected") val parser = Space ~> StringBasic checkContentsOfVersionSbt := { val expected = parser.parsed val versionFile = baseDirectory.value / "version.sbt" assert(IO.read(versionFile).contains(expected), s"does not contains ${expected} in ${versionFile}") } InputKey[Unit]("checkJarFile") := { val dir = file( if (sbtVersion.value.startsWith("1")) { s"target/scala-${scalaBinaryVersion.value}" } else { s"target/out/jvm/scala-${scalaVersion.value}/command-line-version-numbers" } ) assert((dir / "command-line-version-numbers_2.13-36.14.3.jar").isFile) } ================================================ FILE: src/sbt-test/sbt-release/command-line-version-numbers/project/build.properties ================================================ # not setting the sbt version explicitly will use the version for the current sbt cross-build ================================================ FILE: src/sbt-test/sbt-release/command-line-version-numbers/project/build.sbt ================================================ { val pluginVersion = System.getProperty("plugin.version") if (pluginVersion == null) throw new RuntimeException("""|The system property 'plugin.version' is not defined. |Specify this property using the scriptedLaunchOpts -D.""".stripMargin) else { addSbtPlugin("com.github.sbt" % "sbt-release" % pluginVersion) } } ================================================ FILE: src/sbt-test/sbt-release/command-line-version-numbers/src/main/scala/Hello.scala ================================================ object Main extends App { println("hello") } ================================================ FILE: src/sbt-test/sbt-release/command-line-version-numbers/test ================================================ -> checkJarFile > 'release release-version 36.14.3 next-version 36.14.4-SNAPSHOT' > checkJarFile > checkContentsOfVersionSbt 36.14.4-SNAPSHOT ================================================ FILE: src/sbt-test/sbt-release/command-line-version-numbers/version.sbt ================================================ ThisBuild / version := "36.14.4-SNAPSHOT" ================================================ FILE: src/sbt-test/sbt-release/cross/.gitignore ================================================ target global/ ================================================ FILE: src/sbt-test/sbt-release/cross/A.scala ================================================ class A ================================================ FILE: src/sbt-test/sbt-release/cross/build.sbt ================================================ import sbtrelease.ReleaseStateTransformations._ val Scala213 = "2.13.18" val Scala212 = "2.12.21" scalaVersion := Scala213 crossScalaVersions := Scala213 :: Scala212 :: Nil releaseCrossBuild := false releaseProcess := Seq( checkSnapshotDependencies, inquireVersions, runClean, runTest, setReleaseVersion, commitReleaseVersion, tagRelease, setNextVersion, commitNextVersion ) name := "sbt-release-cross-test" InputKey[Unit]("checkTargetDir") := { import complete.DefaultParsers._ val args = spaceDelimited("").parsed val exists = args(1) match { case "exists" => true case "not-exists" => false } val dir = file { if (sbtVersion.value.startsWith("1")) { val scalaBinaryV = args(0) s"target/scala-${scalaBinaryV}/classes" } else { val scalaV = args(0) match { case "2.12" => Scala212 case "2.13" => Scala213 } s"target/out/jvm/scala-${scalaV}/sbt-release-cross-test/classes" } } assert(dir.isDirectory == exists) } ================================================ FILE: src/sbt-test/sbt-release/cross/project/build.sbt ================================================ { val pluginVersion = System.getProperty("plugin.version") if (pluginVersion == null) throw new RuntimeException("""|The system property 'plugin.version' is not defined. |Specify this property using the scriptedLaunchOpts -D.""".stripMargin) else addSbtPlugin("com.github.sbt" % "sbt-release" % pluginVersion) } ================================================ FILE: src/sbt-test/sbt-release/cross/test ================================================ $ exec git init . > update $ exec git add . $ exec git commit -m init > reload > release with-defaults > checkTargetDir 2.13 exists > checkTargetDir 2.12 not-exists > clean > checkTargetDir 2.12 not-exists > checkTargetDir 2.13 not-exists > release with-defaults cross > checkTargetDir 2.13 exists > checkTargetDir 2.12 exists ================================================ FILE: src/sbt-test/sbt-release/cross/version.sbt ================================================ ThisBuild / version := "0.1.0-SNAPSHOT" ================================================ FILE: src/sbt-test/sbt-release/exit-code/.gitignore ================================================ target global/ ================================================ FILE: src/sbt-test/sbt-release/exit-code/build.sbt ================================================ // import sbtrelease.ReleaseStateTransformations._ import scala.sys.process.Process // credits for the test to: https://github.com/rossabaker/sbt-release-exit-code publishTo := Some(Resolver.file("file", new File(Path.userHome.absolutePath + "/.m2/repository"))) val failingTask = taskKey[Unit]("A task that will fail") failingTask := { throw new IllegalStateException("Meh") } ================================================ FILE: src/sbt-test/sbt-release/exit-code/foo.scala ================================================ This is NOT Scala ================================================ FILE: src/sbt-test/sbt-release/exit-code/project/build.sbt ================================================ { val pluginVersion = System.getProperty("plugin.version") if (pluginVersion == null) throw new RuntimeException("""|The system property 'plugin.version' is not defined. |Specify this property using the scriptedLaunchOpts -D.""".stripMargin) else addSbtPlugin("com.github.sbt" % "sbt-release" % pluginVersion) } ================================================ FILE: src/sbt-test/sbt-release/exit-code/test ================================================ $ exec git init . > update $ exec git add . $ exec git commit -m init > reload > set releaseProcess := Seq(sbtrelease.ReleaseStateTransformations.runTest) -> release with-defaults > set releaseProcess := Seq(releaseStepCommand("show version")) > release with-defaults > set releaseProcess := Seq(sbtrelease.ReleaseStateTransformations.runTest, releaseStepCommand("show version")) -> release with-defaults > set releaseProcess := Seq(releaseStepCommandAndRemaining("show version")) > release with-defaults > set releaseProcess := Seq(releaseStepCommandAndRemaining("failingTask")) -> release with-defaults ================================================ FILE: src/sbt-test/sbt-release/fail-test/build.sbt ================================================ import ReleaseTransformations._ val createFile: ReleaseStep = { (st: State) => IO.touch(file("file")) st } releaseProcess := Seq[ReleaseStep](runTest, createFile) scalaVersion := "2.13.18" libraryDependencies += "org.scalatest" %% "scalatest-flatspec" % "3.2.19" % "test" ================================================ FILE: src/sbt-test/sbt-release/fail-test/project/build.properties ================================================ # not setting the sbt version explicitly will use the version for the current sbt cross-build ================================================ FILE: src/sbt-test/sbt-release/fail-test/project/build.sbt ================================================ { val pluginVersion = System.getProperty("plugin.version") if (pluginVersion == null) throw new RuntimeException("""|The system property 'plugin.version' is not defined. |Specify this property using the scriptedLaunchOpts -D.""".stripMargin) else addSbtPlugin("com.github.sbt" % "sbt-release" % pluginVersion) } ================================================ FILE: src/sbt-test/sbt-release/fail-test/src/test/scala/FailTest.scala ================================================ import org.scalatest.flatspec.AnyFlatSpec class FailSpec extends AnyFlatSpec { "This test" should "fail" in { assert(false) } } ================================================ FILE: src/sbt-test/sbt-release/fail-test/test ================================================ $ delete file -> release $ absent file ================================================ FILE: src/sbt-test/sbt-release/mercurial/.hgignore ================================================ target global/ ================================================ FILE: src/sbt-test/sbt-release/mercurial/B.scala ================================================ class B ================================================ FILE: src/sbt-test/sbt-release/mercurial/build.sbt ================================================ import sbtrelease.ReleaseStateTransformations._ scalaVersion := "2.13.18" releaseProcess := Seq( checkSnapshotDependencies, inquireVersions, runClean, runTest, setReleaseVersion, commitReleaseVersion, tagRelease, setNextVersion, commitNextVersion ) name := "sbt-release-test-mercurial" InputKey[Unit]("check") := { val f = file( if (sbtVersion.value.startsWith("1")) { s"target/scala-${scalaBinaryVersion.value}/classes/B.class" } else { s"target/out/jvm/scala-${scalaVersion.value}/sbt-release-test-mercurial/classes/B.class" } ) assert(f.isFile) } ================================================ FILE: src/sbt-test/sbt-release/mercurial/project/build.sbt ================================================ { val pluginVersion = System.getProperty("plugin.version") if (pluginVersion == null) throw new RuntimeException("""|The system property 'plugin.version' is not defined. |Specify this property using the scriptedLaunchOpts -D.""".stripMargin) else addSbtPlugin("com.github.sbt" % "sbt-release" % pluginVersion) } ================================================ FILE: src/sbt-test/sbt-release/mercurial/test ================================================ -> check $ exec hg init . > update $ exec hg add . $ exec hg commit -m init > reload > release with-defaults > check ================================================ FILE: src/sbt-test/sbt-release/mercurial/version.sbt ================================================ ThisBuild / version := "0.1.0-SNAPSHOT" ================================================ FILE: src/sbt-test/sbt-release/skip-tests/.gitignore ================================================ target global/ ================================================ FILE: src/sbt-test/sbt-release/skip-tests/build.sbt ================================================ import sbtrelease.ReleaseStateTransformations._ scalaVersion := "2.13.18" libraryDependencies += "org.scalatest" %% "scalatest-funspec" % "3.2.19" % "test" releaseProcess := Seq( checkSnapshotDependencies, inquireVersions, runClean, runTest, setReleaseVersion, commitReleaseVersion, tagRelease, setNextVersion, commitNextVersion ) ================================================ FILE: src/sbt-test/sbt-release/skip-tests/project/build.sbt ================================================ { val pluginVersion = System.getProperty("plugin.version") if (pluginVersion == null) throw new RuntimeException("""|The system property 'plugin.version' is not defined. |Specify this property using the scriptedLaunchOpts -D.""".stripMargin) else addSbtPlugin("com.github.sbt" % "sbt-release" % pluginVersion) } ================================================ FILE: src/sbt-test/sbt-release/skip-tests/src/test/scala/Test.scala ================================================ package com.example class Test extends org.scalatest.funspec.AnyFunSpec { sys.error("should fail") } ================================================ FILE: src/sbt-test/sbt-release/skip-tests/test ================================================ $ exec git init . > update $ exec git add . $ exec git commit -m init > reload -> release with-defaults > release with-defaults skip-tests ================================================ FILE: src/sbt-test/sbt-release/skip-tests/version.sbt ================================================ ThisBuild / version := "0.1.0-SNAPSHOT" ================================================ FILE: src/sbt-test/sbt-release/tag-default/.gitignore ================================================ target global/ ================================================ FILE: src/sbt-test/sbt-release/tag-default/build.sbt ================================================ import sbtrelease.ReleaseStateTransformations._ scalaVersion := "2.13.18" releaseProcess := Seq( checkSnapshotDependencies, inquireVersions, runClean, runTest, setReleaseVersion, commitReleaseVersion, tagRelease, setNextVersion, commitNextVersion ) ================================================ FILE: src/sbt-test/sbt-release/tag-default/project/build.sbt ================================================ { val pluginVersion = System.getProperty("plugin.version") if (pluginVersion == null) throw new RuntimeException("""|The system property 'plugin.version' is not defined. |Specify this property using the scriptedLaunchOpts -D.""".stripMargin) else addSbtPlugin("com.github.sbt" % "sbt-release" % pluginVersion) } ================================================ FILE: src/sbt-test/sbt-release/tag-default/test ================================================ $ exec git init . > update $ exec git add . $ exec git commit -m init > reload #prerequisite for the use case covered $ exec git tag v0.1.0 #fail since it try to overwrite tag v0.1.0 that is already present -> release with-defaults #fail since default action is to abort on tag already present -> release with-defaults default-tag-exists-answer a #succeed since we let overwrite existing tag > release with-defaults default-tag-exists-answer o #succeed since we do not overwrite existing tag > release with-defaults default-tag-exists-answer k #succeed since ask to tag with an explicit tag that is different from the previous one > release with-defaults default-tag-exists-answer v0.1.0-debug ================================================ FILE: src/sbt-test/sbt-release/tag-default/version.sbt ================================================ ThisBuild / version := "0.1.0-SNAPSHOT" ================================================ FILE: src/sbt-test/sbt-release/tasks-as-steps/build.sbt ================================================ organization := "com.example" version := "1.2.3" lazy val myTask = taskKey[Unit]("My task") lazy val myAggregatedTask = taskKey[Unit]("My aggregated task") lazy val myInputTask = inputKey[Unit]("My input task") lazy val testOutputDir = settingKey[File]("") lazy val root: Project = (project in file(".")) .aggregate(sub) .settings( myAggregatedTaskSetting, testOutputDir := file("root-out"), myTask := { IO.write(testOutputDir.value / "mytask", "ran") }, myInputTask := { val file = Def.spaceDelimited().parsed.headOption.getOrElse("myinputtask") IO.write(testOutputDir.value / file, "ran") }, commands ++= Seq(myCommand, myInputCommand, myCommand2, myInputCommand2), releaseProcess := Seq[ReleaseStep]( releaseStepTask(myTask), releaseStepTaskAggregated(root / myAggregatedTask), releaseStepInputTask(myInputTask), releaseStepInputTask(myInputTask, " custominputtask"), releaseStepCommand(myCommand), releaseStepCommand(myInputCommand), releaseStepCommand(myInputCommand, " custominputcommand"), releaseStepCommand("mycommand2"), releaseStepCommand("myinputcommand2"), releaseStepCommand("myinputcommand2 custominputcommand2") ), ) lazy val sub = (project in file("sub")).settings( myAggregatedTaskSetting, testOutputDir := file("sub-out"), ) def myAggregatedTaskSetting = myAggregatedTask := { IO.write(testOutputDir.value / "myaggregatedtask", "ran") } lazy val myCommand = Command.command("mycommand") { state => IO.write(Project.extract(state).get(testOutputDir) / "mycommand", "ran") state } lazy val myInputCommand = Command.make("myinputcommand") { state => Def.spaceDelimited().map { args => () => val file = args.headOption.getOrElse("myinputcommand") IO.write(Project.extract(state).get(testOutputDir) / file, "ran") state } } lazy val myCommand2 = Command.command("mycommand2") { state => IO.write(Project.extract(state).get(testOutputDir) / "mycommand2", "ran") state } lazy val myInputCommand2 = Command.make("myinputcommand2") { state => Def.spaceDelimited().map { args => () => val file = args.headOption.getOrElse("myinputcommand2") IO.write(Project.extract(state).get(testOutputDir) / file, "ran") state } } ================================================ FILE: src/sbt-test/sbt-release/tasks-as-steps/project/build.sbt ================================================ { val pluginVersion = System.getProperty("plugin.version") if (pluginVersion == null) throw new RuntimeException("""|The system property 'plugin.version' is not defined. |Specify this property using the scriptedLaunchOpts -D.""".stripMargin) else addSbtPlugin("com.github.sbt" % "sbt-release" % pluginVersion) } ================================================ FILE: src/sbt-test/sbt-release/tasks-as-steps/test ================================================ -$ exists root-out -$ exists sub-out > release $ exists root-out/mytask $ exists root-out/myaggregatedtask $ exists sub-out/myaggregatedtask $ exists root-out/myinputtask $ exists root-out/custominputtask $ exists root-out/mycommand $ exists root-out/myinputcommand $ exists root-out/custominputcommand $ exists root-out/mycommand2 $ exists root-out/myinputcommand2 $ exists root-out/custominputcommand2 ================================================ FILE: src/sbt-test/sbt-release/with-defaults/.gitignore ================================================ target global/ ================================================ FILE: src/sbt-test/sbt-release/with-defaults/build.sbt ================================================ import sbt.complete.DefaultParsers._ import sbtrelease.ReleaseStateTransformations._ releaseVersionFile := file("version.sbt") releaseProcess := Seq( checkSnapshotDependencies, inquireVersions, runClean, runTest, setReleaseVersion, commitReleaseVersion, tagRelease, setNextVersion, commitNextVersion ) val checkContentsOfVersionSbt = inputKey[Unit]("Check that the contents of version.sbt is as expected") val parser = Space ~> StringBasic checkContentsOfVersionSbt := { val expected = parser.parsed val versionFile = ((baseDirectory).value) / "version.sbt" assert(IO.read(versionFile).contains(expected), s"does not contains ${expected} in ${versionFile}") } ================================================ FILE: src/sbt-test/sbt-release/with-defaults/project/build.sbt ================================================ { val pluginVersion = System.getProperty("plugin.version") if (pluginVersion == null) throw new RuntimeException("""|The system property 'plugin.version' is not defined. |Specify this property using the scriptedLaunchOpts -D.""".stripMargin) else addSbtPlugin("com.github.sbt" % "sbt-release" % pluginVersion) } ================================================ FILE: src/sbt-test/sbt-release/with-defaults/test ================================================ # Test Suite Preparation $ exec git init . > update $ exec git add . $ exec git commit -m init > reload # SCENARIO: When no release versions are specified in the release command # TEST: Should fail to release if "with-defaults" is not specified -> release # TEST: Should succeed if "with-defaults" is specified > release with-defaults # SCENARIO: When default bumping strategy is used # Test Scenario Preparation > 'release release-version 0.9.9 next-version 1.0.0-RC1-SNAPSHOT' > reload > checkContentsOfVersionSbt 1.0.0-RC1-SNAPSHOT # TEST: Snapshot version should be correctly set > release with-defaults > checkContentsOfVersionSbt 1.0.0-RC2-SNAPSHOT # TEST: Release version should be correctly set $ exec git reset --hard HEAD~1 > reload > checkContentsOfVersionSbt 1.0.0-RC1 # SCENARIO: When NextStable bumping strategy is used # TEST: Snapshot version should be correctly set $ exec git reset --hard HEAD~1 > set releaseVersionBump := sbtrelease.Version.Bump.NextStable > release with-defaults > checkContentsOfVersionSbt 1.0.1-SNAPSHOT # TEST: Release version should be correctly set $ exec git reset --hard HEAD~1 > reload > checkContentsOfVersionSbt 1.0.0 ================================================ FILE: src/sbt-test/sbt-release/with-defaults/version.sbt ================================================ ThisBuild / version := "0.1.0-SNAPSHOT" ================================================ FILE: src/test/scala/VersionSpec.scala ================================================ package sbtrelease import org.specs2.matcher.MatchResult import org.specs2.mutable.Specification object VersionSpec extends Specification { def version(v: String) = Version(v) match { case Some(parsed) => parsed case None => sys.error("Can't parse version " + v) } "Next Version bumping" should { def testBumpNext(input: String, expectedOutput: String): MatchResult[Any] = version(input).bumpNext.unapply must_== expectedOutput def testBumpNextStable(input: String, expectedOutput: String): MatchResult[Any] = version(input).bumpNextStable.unapply must_== expectedOutput def testBothBumpNextStrategies(input: String, expectedOutput: String): MatchResult[Any] = { testBumpNext(input, expectedOutput) testBumpNextStable(input, expectedOutput) } "bump the major version if there's only a major version" in { testBothBumpNextStrategies("1", "2") } "bump the minor version if there's only a minor version" in { testBothBumpNextStrategies("1.2", "1.3") } "bump the bugfix version if there's only a bugfix version" in { testBothBumpNextStrategies("1.2.3", "1.2.4") } "bump the nano version if there's only a nano version" in { testBothBumpNextStrategies("1.2.3.4", "1.2.3.5") } "drop the qualifier if it's a pre release and there is no version number at the end" in { testBothBumpNextStrategies("1-rc", "1") testBothBumpNextStrategies("1.0-rc", "1.0") testBothBumpNextStrategies("1.0.0-rc", "1.0.0") testBothBumpNextStrategies("1.0.0.0-rc", "1.0.0.0") testBothBumpNextStrategies("1-beta", "1") testBothBumpNextStrategies("1-alpha", "1") } "when the qualifier includes a pre release with a version number at the end" >> { "and Next is the bumping strategy" >> { "should bump the qualifier" in { testBumpNext("1-rc1", "1-rc2") testBumpNext("1.2-rc1", "1.2-rc2") testBumpNext("1.2.3-rc1", "1.2.3-rc2") testBumpNext("1-RC1", "1-RC2") testBumpNext("1-M1", "1-M2") testBumpNext("1-rc-1", "1-rc-2") testBumpNext("1-rc.1", "1-rc.2") testBumpNext("1-beta-1", "1-beta-2") testBumpNext("1-beta.1", "1-beta.2") } } "and NextStable is the bumping strategy" >> { "should remove the qualifier" in { testBumpNextStable("1-rc1", "1") testBumpNextStable("1.2-rc1", "1.2") testBumpNextStable("1.2.3-rc1", "1.2.3") testBumpNextStable("1-RC1", "1") testBumpNextStable("1-M1", "1") testBumpNextStable("1-rc-1", "1") testBumpNextStable("1-rc.1", "1") testBumpNextStable("1-beta-1", "1") testBumpNextStable("1-beta.1", "1") } } } "never drop the qualifier if it's a final release" >> { "when release is major" in { testBothBumpNextStrategies("1-Final", "2-Final") } "when release is minor" in { testBothBumpNextStrategies("1.2-Final", "1.3-Final") } "when release is subversion" in { testBothBumpNextStrategies("1.2.3-Final", "1.2.4-Final") } "when release is nano" in { testBothBumpNextStrategies("1.2.3.4-Final", "1.2.3.5-Final") } } } "Major Version bumping" should { def bumpMajor(v: String) = version(v).bumpMajor.unapply "bump the major version and reset other versions" in { bumpMajor("1.2.3.4.5") must_== "2.0.0.0.0" } "not drop the qualifier" in { bumpMajor("1.2.3.4.5-alpha") must_== "2.0.0.0.0-alpha" } } "Minor Version bumping" should { def bumpMinor(v: String) = version(v).bumpMinor.unapply "bump the minor version" in { bumpMinor("1.2") must_== "1.3" } "bump the minor version and reset other subversions" in { bumpMinor("1.2.3.4.5") must_== "1.3.0.0.0" } "not bump the major version when no minor version" in { bumpMinor("1") must_== "1" } "not drop the qualifier" in { bumpMinor("1.2.3.4.5-alpha") must_== "1.3.0.0.0-alpha" } } "Subversion bumping" should { def bumpSubversion(v: String)(i: Int) = version(v).maybeBumpSubversion(i).unapply "bump the subversion" in { bumpSubversion("1.2")(0) must_== "1.3" } "bump the subversion and reset lower subversions" in { bumpSubversion("1.2.3.4.5")(0) must_== "1.3.0.0.0" } "not change anything with an invalid subversion index" in { bumpSubversion("1.2-beta")(1) must_== "1.2-beta" } "not drop the qualifier" in { bumpSubversion("1.2.3.4.5-alpha")(2) must_== "1.2.3.5.0-alpha" } } "#isSnapshot" should { "return true when -SNAPSHOT is appended with another qualifier" in { version("1.0.0-RC1-SNAPSHOT").isSnapshot must_== true } "return false when -SNAPSHOT is not appended but another qualifier exists" in { version("1.0.0-RC1").isSnapshot must_== false } "return false when neither -SNAPSHOT nor qualifier are appended" in { version("1.0.0").isSnapshot must_== false } } "#asSnapshot" should { def snapshot(v: String) = version(v).asSnapshot.unapply "include qualifier if it exists" in { snapshot("1.0.0-RC1") must_== "1.0.0-RC1-SNAPSHOT" } "have no qualifier if none exists" in { snapshot("1.0.0") must_== "1.0.0-SNAPSHOT" } } "#withoutSnapshot" should { "remove the snapshot normally" in { version("1.0.0-SNAPSHOT").withoutSnapshot.unapply must_== "1.0.0" } "remove the snapshot without removing the qualifier" in { version("1.0.0-RC1-SNAPSHOT").withoutSnapshot.unapply must_== "1.0.0-RC1" } } }