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 <example@example.com>' >> "$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.
[](https://index.scala-lang.org/sbt/sbt-release/sbt-release)
[](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)
* ```<tag-name>``` 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 := (
<developers>{
Seq(
("xuwei-k", "Kenji Yoshida"),
).map { case (id, name) =>
<developer>
<id>{id}</id>
<name>{name}</name>
<url>https://github.com/{id}</url>
</developer>
}
}</developers>
)
================================================
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,
"<release version>"
)) map ParseResult.ReleaseVersion
private[this] val NextVersion: Parser[ParseResult] =
(Space ~> token("next-version") ~> Space ~> token(StringBasic, "<next version>")) map ParseResult.NextVersion
private[this] val TagDefault: Parser[ParseResult] =
(Space ~> token("default-tag-exists-answer") ~> Space ~> token(
StringBasic,
"o|k|a|<tag-name>"
)) 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("<arg>").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"
}
}
}
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
Condensed preview — 84 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (124K chars).
[
{
"path": ".github/dependabot.yml",
"chars": 118,
"preview": "version: 2\nupdates:\n - package-ecosystem: \"github-actions\"\n directory: \"/\"\n schedule:\n interval: \"weekly\"\n"
},
{
"path": ".github/workflows/ci.yml",
"chars": 1464,
"preview": "name: CI\non:\n pull_request:\n push:\n schedule:\n - cron: '0 8 * * 0'\njobs:\n test:\n runs-on: ubuntu-latest\n time"
},
{
"path": ".github/workflows/release.yml",
"chars": 773,
"preview": "name: Release\non:\n push:\n tags: [\"*\"]\njobs:\n publish:\n runs-on: ubuntu-latest\n steps:\n - uses: actions/c"
},
{
"path": ".gitignore",
"chars": 46,
"preview": "target\nproject/target\n.idea\n.idea_modules\n.bsp"
},
{
"path": ".scalafmt.conf",
"chars": 711,
"preview": "version = \"3.10.7\"\nrunner.dialect = Scala212Source3\nmaxColumn = 120\nalign.preset = none\nalign.tokens = []\nrewrite.rules "
},
{
"path": "LICENSE",
"chars": 10854,
"preview": " Apache License\n Version 2.0, January 2004\n http://www.apache.org/licenses/"
},
{
"path": "README.md",
"chars": 17136,
"preview": "# sbt-release\nThis sbt plugin provides a customizable release process that you can add to your project.\n\n[ for a comprehensive int"
},
{
"path": "notes/0.2.markdown",
"chars": 657,
"preview": " * \\[Improvement\\] [#1](https://github.com/gseitz/sbt-release/issues/1): Include the Git tag and the has of the release "
},
{
"path": "notes/0.3.markdown",
"chars": 618,
"preview": " * \\[New\\] [#5](https://github.com/gseitz/sbt-release/issues/5): Added the key `tagName: SettingKey[String](\"release-tag"
},
{
"path": "notes/0.4.markdown",
"chars": 201,
"preview": " * \\[Fix\\] `test` and `publish` are now properly propagated from the root project to aggregated projects.\n * In a multi-"
},
{
"path": "notes/0.5.markdown",
"chars": 565,
"preview": "* `sbt-release` now adopts the sbt plugin best practices.\n* Add release step to run `git push && git push --tags` at the"
},
{
"path": "notes/0.6.markdown",
"chars": 692,
"preview": "* [#17](https://github.com/sbt/sbt-release/issues/17) Added setting for customizing the tag comment. Thanks [Eric Bowman"
},
{
"path": "notes/0.7.1.markdown",
"chars": 172,
"preview": "* Publish for sbt 0.13.\n* Run `clean` before running `test` to avoid stale artifacts (e.g. in case a source file has bee"
},
{
"path": "notes/0.7.markdown",
"chars": 389,
"preview": "* Recursively search parent directories for VCS marker directory. [#25](https://github.com/sbt/sbt-release/pull/25) (Tha"
},
{
"path": "notes/0.8.1.markdown",
"chars": 118,
"preview": "* [#53](https://github.com/sbt/sbt-release/pull/53) Fix for git complaining about version.sbt being out of repository\n"
},
{
"path": "notes/0.8.2.markdown",
"chars": 143,
"preview": "* [#61](https://github.com/sbt/sbt-release/issues/61) Clearer error message when `release with-defaults` fails due to an"
},
{
"path": "notes/0.8.3.markdown",
"chars": 541,
"preview": "* [#55](https://github.com/sbt/sbt-release/issues/55) sbt `0.13.x` doesn't throw an exception after failed tests, thus t"
},
{
"path": "notes/0.8.4.markdown",
"chars": 502,
"preview": "* [#69](https://github.com/sbt/sbt-release/pull/69) Add Subversion support. Thanks [@timvlaer](https://github.com/timvla"
},
{
"path": "notes/0.8.5.markdown",
"chars": 316,
"preview": "* [#82](https://github.com/sbt/sbt-release/pull/82) Ensure that task failure is propagated. Thanks [@jcrobak](https://gi"
},
{
"path": "notes/0.8.markdown",
"chars": 573,
"preview": "* [#29](https://github.com/sbt/sbt-release/issues/29) The `release` task automatically runs a cross release build depend"
},
{
"path": "notes/1.0.5.markdown",
"chars": 635,
"preview": "* [#193][] The `checkSnapshotDependencies` release step now has `enabledCrossBuild` enabled by default.\n* [#185][] Adds "
},
{
"path": "notes/about.markdown",
"chars": 218,
"preview": "[sbt-release](https://github.com/sbt/sbt-release) is a plugin for [sbt](https://github.com/sbt/sbt)\nand brings a customi"
},
{
"path": "project/build.properties",
"chars": 19,
"preview": "sbt.version=1.12.9\n"
},
{
"path": "project/plugins.sbt",
"chars": 199,
"preview": "libraryDependencies += \"org.scala-sbt\" %% \"scripted-plugin\" % sbtVersion.value\n\naddSbtPlugin(\"com.github.sbt\" % \"sbt-ci-"
},
{
"path": "src/main/scala/Compat.scala",
"chars": 5300,
"preview": "package sbtrelease\n\nimport sbt.*\nimport sbt.Def.ScopedKey\nimport sbt.EvaluateTask.extractedTaskConfig\nimport sbt.Evaluat"
},
{
"path": "src/main/scala/ReleaseExtra.scala",
"chars": 15430,
"preview": "package sbtrelease\n\nimport sbt.*\nimport sbt.Keys.*\nimport sbt.Package.ManifestAttributes\nimport sbtrelease.ReleasePlugin"
},
{
"path": "src/main/scala/ReleasePlugin.scala",
"chars": 12412,
"preview": "package sbtrelease\n\nimport java.io.Serializable\nimport sbt.*\nimport sbt.Keys.*\nimport sbt.complete.DefaultParsers.*\nimpo"
},
{
"path": "src/main/scala/Vcs.scala",
"chars": 8990,
"preview": "package sbtrelease\n\nimport java.io.File\nimport sbt.*\nimport sys.process.Process\nimport sys.process.ProcessBuilder\nimport"
},
{
"path": "src/main/scala/Version.scala",
"chars": 5704,
"preview": "package sbtrelease\n\nimport scala.util.matching.Regex\nimport util.control.Exception.*\n\nobject Version {\n sealed trait Bu"
},
{
"path": "src/main/scala/package.scala",
"chars": 329,
"preview": "package object sbtrelease {\n type Versions = (String, String)\n\n def versionFormatError(version: String) =\n sys.erro"
},
{
"path": "src/main/scala-2/LoadCompat.scala",
"chars": 4303,
"preview": "package sbtrelease\n\nimport sbt.*\nimport sbt.Def.ScopedKey\nimport sbt.Keys.resolvedScoped\nimport sbt.Keys.streams\n\n// sbt"
},
{
"path": "src/main/scala-2/ReleasePluginCompat.scala",
"chars": 560,
"preview": "package sbtrelease\n\nimport sbt.*\nimport sbt.Keys.*\nimport sbtrelease.ReleasePlugin.autoImport.ReleaseStep\n\nprivate[sbtre"
},
{
"path": "src/main/scala-3/LoadCompat.scala",
"chars": 488,
"preview": "package sbt\n\nimport sbt.internal.BuildStructure\n\nobject LoadCompat {\n\n def transformSettings(\n thisScope: Scope,\n "
},
{
"path": "src/main/scala-3/ReleasePluginCompat.scala",
"chars": 557,
"preview": "package sbtrelease\n\nimport sbt.*\nimport sbt.Keys.*\nimport sbtrelease.ReleasePlugin.autoImport.ReleaseStep\nimport sbtrele"
},
{
"path": "src/sbt-test/sbt-release/command-line-version-numbers/build.sbt",
"chars": 1036,
"preview": "import ReleaseTransformations._\nimport sbt.complete.DefaultParsers._\n\nname := \"command-line-version-numbers\"\n\npublishTo "
},
{
"path": "src/sbt-test/sbt-release/command-line-version-numbers/project/build.properties",
"chars": 94,
"preview": "# not setting the sbt version explicitly will use the version for the current sbt cross-build\n"
},
{
"path": "src/sbt-test/sbt-release/command-line-version-numbers/project/build.sbt",
"chars": 366,
"preview": "{\n val pluginVersion = System.getProperty(\"plugin.version\")\n if (pluginVersion == null)\n throw new RuntimeException"
},
{
"path": "src/sbt-test/sbt-release/command-line-version-numbers/src/main/scala/Hello.scala",
"chars": 47,
"preview": "object Main extends App {\n println(\"hello\")\n}\n"
},
{
"path": "src/sbt-test/sbt-release/command-line-version-numbers/test",
"chars": 142,
"preview": "-> checkJarFile\n> 'release release-version 36.14.3 next-version 36.14.4-SNAPSHOT'\n> checkJarFile\n> checkContentsOfVersio"
},
{
"path": "src/sbt-test/sbt-release/command-line-version-numbers/version.sbt",
"chars": 42,
"preview": "ThisBuild / version := \"36.14.4-SNAPSHOT\"\n"
},
{
"path": "src/sbt-test/sbt-release/cross/.gitignore",
"chars": 15,
"preview": "target\nglobal/\n"
},
{
"path": "src/sbt-test/sbt-release/cross/A.scala",
"chars": 8,
"preview": "class A\n"
},
{
"path": "src/sbt-test/sbt-release/cross/build.sbt",
"chars": 1056,
"preview": "import sbtrelease.ReleaseStateTransformations._\n\nval Scala213 = \"2.13.18\"\nval Scala212 = \"2.12.21\"\n\nscalaVersion := Scal"
},
{
"path": "src/sbt-test/sbt-release/cross/project/build.sbt",
"chars": 356,
"preview": "{\n val pluginVersion = System.getProperty(\"plugin.version\")\n if (pluginVersion == null)\n throw new RuntimeException"
},
{
"path": "src/sbt-test/sbt-release/cross/test",
"chars": 333,
"preview": "$ exec git init .\n\n> update\n\n$ exec git add .\n$ exec git commit -m init\n\n> reload\n\n> release with-defaults\n> checkTarget"
},
{
"path": "src/sbt-test/sbt-release/cross/version.sbt",
"chars": 40,
"preview": "ThisBuild / version := \"0.1.0-SNAPSHOT\"\n"
},
{
"path": "src/sbt-test/sbt-release/exit-code/.gitignore",
"chars": 15,
"preview": "target\nglobal/\n"
},
{
"path": "src/sbt-test/sbt-release/exit-code/build.sbt",
"chars": 382,
"preview": "// import sbtrelease.ReleaseStateTransformations._\nimport scala.sys.process.Process\n\n// credits for the test to: https:/"
},
{
"path": "src/sbt-test/sbt-release/exit-code/foo.scala",
"chars": 18,
"preview": "This is NOT Scala\n"
},
{
"path": "src/sbt-test/sbt-release/exit-code/project/build.sbt",
"chars": 356,
"preview": "{\n val pluginVersion = System.getProperty(\"plugin.version\")\n if (pluginVersion == null)\n throw new RuntimeException"
},
{
"path": "src/sbt-test/sbt-release/exit-code/test",
"chars": 613,
"preview": "$ exec git init .\n\n> update\n\n$ exec git add .\n$ exec git commit -m init\n\n> reload\n\n> set releaseProcess := Seq(sbtreleas"
},
{
"path": "src/sbt-test/sbt-release/fail-test/build.sbt",
"chars": 280,
"preview": "import ReleaseTransformations._\n\nval createFile: ReleaseStep = { (st: State) =>\n IO.touch(file(\"file\"))\n st\n}\n\nrelease"
},
{
"path": "src/sbt-test/sbt-release/fail-test/project/build.properties",
"chars": 94,
"preview": "# not setting the sbt version explicitly will use the version for the current sbt cross-build\n"
},
{
"path": "src/sbt-test/sbt-release/fail-test/project/build.sbt",
"chars": 356,
"preview": "{\n val pluginVersion = System.getProperty(\"plugin.version\")\n if (pluginVersion == null)\n throw new RuntimeException"
},
{
"path": "src/sbt-test/sbt-release/fail-test/src/test/scala/FailTest.scala",
"chars": 137,
"preview": "import org.scalatest.flatspec.AnyFlatSpec\n\nclass FailSpec extends AnyFlatSpec {\n \"This test\" should \"fail\" in {\n ass"
},
{
"path": "src/sbt-test/sbt-release/fail-test/test",
"chars": 39,
"preview": "$ delete file\n-> release\n$ absent file\n"
},
{
"path": "src/sbt-test/sbt-release/mercurial/.hgignore",
"chars": 15,
"preview": "target\nglobal/\n"
},
{
"path": "src/sbt-test/sbt-release/mercurial/B.scala",
"chars": 8,
"preview": "class B\n"
},
{
"path": "src/sbt-test/sbt-release/mercurial/build.sbt",
"chars": 602,
"preview": "import sbtrelease.ReleaseStateTransformations._\n\nscalaVersion := \"2.13.18\"\n\nreleaseProcess := Seq(\n checkSnapshotDepend"
},
{
"path": "src/sbt-test/sbt-release/mercurial/project/build.sbt",
"chars": 356,
"preview": "{\n val pluginVersion = System.getProperty(\"plugin.version\")\n if (pluginVersion == null)\n throw new RuntimeException"
},
{
"path": "src/sbt-test/sbt-release/mercurial/test",
"chars": 121,
"preview": "-> check\n$ exec hg init .\n\n> update\n\n$ exec hg add .\n$ exec hg commit -m init\n\n> reload\n\n> release with-defaults\n> check"
},
{
"path": "src/sbt-test/sbt-release/mercurial/version.sbt",
"chars": 40,
"preview": "ThisBuild / version := \"0.1.0-SNAPSHOT\"\n"
},
{
"path": "src/sbt-test/sbt-release/skip-tests/.gitignore",
"chars": 15,
"preview": "target\nglobal/\n"
},
{
"path": "src/sbt-test/sbt-release/skip-tests/build.sbt",
"chars": 352,
"preview": "import sbtrelease.ReleaseStateTransformations._\n\nscalaVersion := \"2.13.18\"\n\nlibraryDependencies += \"org.scalatest\" %% \"s"
},
{
"path": "src/sbt-test/sbt-release/skip-tests/project/build.sbt",
"chars": 356,
"preview": "{\n val pluginVersion = System.getProperty(\"plugin.version\")\n if (pluginVersion == null)\n throw new RuntimeException"
},
{
"path": "src/sbt-test/sbt-release/skip-tests/src/test/scala/Test.scala",
"chars": 104,
"preview": "package com.example\n\nclass Test extends org.scalatest.funspec.AnyFunSpec {\n sys.error(\"should fail\")\n}\n"
},
{
"path": "src/sbt-test/sbt-release/skip-tests/test",
"chars": 144,
"preview": "$ exec git init .\n\n> update\n\n$ exec git add .\n$ exec git commit -m init\n\n> reload\n\n-> release with-defaults\n\n> release w"
},
{
"path": "src/sbt-test/sbt-release/skip-tests/version.sbt",
"chars": 40,
"preview": "ThisBuild / version := \"0.1.0-SNAPSHOT\"\n"
},
{
"path": "src/sbt-test/sbt-release/tag-default/.gitignore",
"chars": 15,
"preview": "target\nglobal/\n"
},
{
"path": "src/sbt-test/sbt-release/tag-default/build.sbt",
"chars": 269,
"preview": "import sbtrelease.ReleaseStateTransformations._\n\nscalaVersion := \"2.13.18\"\n\nreleaseProcess := Seq(\n checkSnapshotDepend"
},
{
"path": "src/sbt-test/sbt-release/tag-default/project/build.sbt",
"chars": 356,
"preview": "{\n val pluginVersion = System.getProperty(\"plugin.version\")\n if (pluginVersion == null)\n throw new RuntimeException"
},
{
"path": "src/sbt-test/sbt-release/tag-default/test",
"chars": 700,
"preview": "$ exec git init .\n\n> update\n\n$ exec git add .\n$ exec git commit -m init\n\n> reload\n\n#prerequisite for the use case covere"
},
{
"path": "src/sbt-test/sbt-release/tag-default/version.sbt",
"chars": 40,
"preview": "ThisBuild / version := \"0.1.0-SNAPSHOT\"\n"
},
{
"path": "src/sbt-test/sbt-release/tasks-as-steps/build.sbt",
"chars": 2297,
"preview": "organization := \"com.example\"\nversion := \"1.2.3\"\n\nlazy val myTask = taskKey[Unit](\"My task\")\nlazy val myAggregatedTask ="
},
{
"path": "src/sbt-test/sbt-release/tasks-as-steps/project/build.sbt",
"chars": 356,
"preview": "{\n val pluginVersion = System.getProperty(\"plugin.version\")\n if (pluginVersion == null)\n throw new RuntimeException"
},
{
"path": "src/sbt-test/sbt-release/tasks-as-steps/test",
"chars": 406,
"preview": "-$ exists root-out\n-$ exists sub-out\n\n> release\n\n$ exists root-out/mytask\n$ exists root-out/myaggregatedtask\n$ exists su"
},
{
"path": "src/sbt-test/sbt-release/with-defaults/.gitignore",
"chars": 15,
"preview": "target\nglobal/\n"
},
{
"path": "src/sbt-test/sbt-release/with-defaults/build.sbt",
"chars": 688,
"preview": "import sbt.complete.DefaultParsers._\nimport sbtrelease.ReleaseStateTransformations._\n\nreleaseVersionFile := file(\"versio"
},
{
"path": "src/sbt-test/sbt-release/with-defaults/project/build.sbt",
"chars": 356,
"preview": "{\n val pluginVersion = System.getProperty(\"plugin.version\")\n if (pluginVersion == null)\n throw new RuntimeException"
},
{
"path": "src/sbt-test/sbt-release/with-defaults/test",
"chars": 1358,
"preview": "# Test Suite Preparation\n $ exec git init .\n > update\n $ exec git add .\n $ exec git commit -m init\n > rel"
},
{
"path": "src/sbt-test/sbt-release/with-defaults/version.sbt",
"chars": 40,
"preview": "ThisBuild / version := \"0.1.0-SNAPSHOT\"\n"
},
{
"path": "src/test/scala/VersionSpec.scala",
"chars": 5707,
"preview": "package sbtrelease\n\nimport org.specs2.matcher.MatchResult\nimport org.specs2.mutable.Specification\n\nobject VersionSpec ex"
}
]
About this extraction
This page contains the full source code of the sbt/sbt-release GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 84 files (111.7 KB), approximately 31.7k tokens. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.