diff --git a/.gitignore b/.gitignore index 96ef862..136d405 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ target/ .idea/ +_site/ diff --git a/README.md b/README.md index bb4ee28..a2781bb 100644 --- a/README.md +++ b/README.md @@ -1,14 +1,32 @@ # git-mkver -Git-mkver uses git tags, branch names and commit messages to determine -the next version of the software to release. +Helps version your software and patch version numbers into the build. +For more information head to the [project site][https://git-mkver.github.com]. + +## Features + +- Determine next version based on: + - Last tagged commit + - [Conventional Commits][https://www.conventionalcommits.org/] + - Branch names + - Manual tagging +- Next version conforms to [Semantic Versioning][https://semver.org/] scheme +- Patch the next version into the build: + - Java + - C# + - Many others, fully configurable +- Tag the current commit with the next version + +All of this can be configured based on the branch name so release/master branches get different +version numbers to develop or feature branches. ## Installation Download the binary for your os from the releases page and copy to somewhere on your path. + ## Usage Basic usage is to just call `git mkver next` and it will tell you the next diff --git a/build.sbt b/build.sbt index 9622714..8c18532 100644 --- a/build.sbt +++ b/build.sbt @@ -6,10 +6,16 @@ ThisBuild / organization := "net.cardnell" lazy val root = (project in file(".")) .settings( - name := "git-mkver", - //libraryDependencies += "com.github.xuwei-k" %% "optparse-applicative" % "0.8.2", - libraryDependencies += "org.typelevel" %% "cats-core" % "2.0.0", - libraryDependencies += "com.monovore" %% "decline" % "1.0.0", - libraryDependencies += "com.github.pathikrit" %% "better-files" % "3.8.0", - libraryDependencies += scalaTest % Test + name := "git-mkver", + libraryDependencies += "org.typelevel" %% "cats-core" % "2.0.0", + libraryDependencies += "com.monovore" %% "decline" % "1.0.0", + libraryDependencies += "com.github.pathikrit" %% "better-files" % "3.8.0", + libraryDependencies += "dev.zio" %% "zio-config" % "1.0.0-RC14", + libraryDependencies += "dev.zio" %% "zio-config-typesafe" % "1.0.0-RC14", + libraryDependencies += scalaTest % Test, + + assemblyMergeStrategy in assembly := { + case PathList("META-INF", xs @ _*) => MergeStrategy.discard + case x => MergeStrategy.first + } ) diff --git a/docs/Gemfile b/docs/Gemfile new file mode 100644 index 0000000..37f5eaa --- /dev/null +++ b/docs/Gemfile @@ -0,0 +1,2 @@ +source 'https://rubygems.org' +gem 'github-pages', group: :jekyll_plugins diff --git a/docs/Gemfile.lock b/docs/Gemfile.lock new file mode 100644 index 0000000..81918a8 --- /dev/null +++ b/docs/Gemfile.lock @@ -0,0 +1,248 @@ +GEM + remote: https://rubygems.org/ + specs: + activesupport (6.0.2.2) + concurrent-ruby (~> 1.0, >= 1.0.2) + i18n (>= 0.7, < 2) + minitest (~> 5.1) + tzinfo (~> 1.1) + zeitwerk (~> 2.2) + addressable (2.7.0) + public_suffix (>= 2.0.2, < 5.0) + coffee-script (2.4.1) + coffee-script-source + execjs + coffee-script-source (1.11.1) + colorator (1.1.0) + commonmarker (0.17.13) + ruby-enum (~> 0.5) + concurrent-ruby (1.1.6) + dnsruby (1.61.3) + addressable (~> 2.5) + em-websocket (0.5.1) + eventmachine (>= 0.12.9) + http_parser.rb (~> 0.6.0) + ethon (0.12.0) + ffi (>= 1.3.0) + eventmachine (1.2.7) + execjs (2.7.0) + faraday (1.0.1) + multipart-post (>= 1.2, < 3) + ffi (1.12.2) + forwardable-extended (2.6.0) + gemoji (3.0.1) + github-pages (204) + github-pages-health-check (= 1.16.1) + jekyll (= 3.8.5) + jekyll-avatar (= 0.7.0) + jekyll-coffeescript (= 1.1.1) + jekyll-commonmark-ghpages (= 0.1.6) + jekyll-default-layout (= 0.1.4) + jekyll-feed (= 0.13.0) + jekyll-gist (= 1.5.0) + jekyll-github-metadata (= 2.13.0) + jekyll-mentions (= 1.5.1) + jekyll-optional-front-matter (= 0.3.2) + jekyll-paginate (= 1.1.0) + jekyll-readme-index (= 0.3.0) + jekyll-redirect-from (= 0.15.0) + jekyll-relative-links (= 0.6.1) + jekyll-remote-theme (= 0.4.1) + jekyll-sass-converter (= 1.5.2) + jekyll-seo-tag (= 2.6.1) + jekyll-sitemap (= 1.4.0) + jekyll-swiss (= 1.0.0) + jekyll-theme-architect (= 0.1.1) + jekyll-theme-cayman (= 0.1.1) + jekyll-theme-dinky (= 0.1.1) + jekyll-theme-hacker (= 0.1.1) + jekyll-theme-leap-day (= 0.1.1) + jekyll-theme-merlot (= 0.1.1) + jekyll-theme-midnight (= 0.1.1) + jekyll-theme-minimal (= 0.1.1) + jekyll-theme-modernist (= 0.1.1) + jekyll-theme-primer (= 0.5.4) + jekyll-theme-slate (= 0.1.1) + jekyll-theme-tactile (= 0.1.1) + jekyll-theme-time-machine (= 0.1.1) + jekyll-titles-from-headings (= 0.5.3) + jemoji (= 0.11.1) + kramdown (= 1.17.0) + liquid (= 4.0.3) + mercenary (~> 0.3) + minima (= 2.5.1) + nokogiri (>= 1.10.4, < 2.0) + rouge (= 3.13.0) + terminal-table (~> 1.4) + github-pages-health-check (1.16.1) + addressable (~> 2.3) + dnsruby (~> 1.60) + octokit (~> 4.0) + public_suffix (~> 3.0) + typhoeus (~> 1.3) + html-pipeline (2.12.3) + activesupport (>= 2) + nokogiri (>= 1.4) + http_parser.rb (0.6.0) + i18n (0.9.5) + concurrent-ruby (~> 1.0) + jekyll (3.8.5) + addressable (~> 2.4) + colorator (~> 1.0) + em-websocket (~> 0.5) + i18n (~> 0.7) + jekyll-sass-converter (~> 1.0) + jekyll-watch (~> 2.0) + kramdown (~> 1.14) + liquid (~> 4.0) + mercenary (~> 0.3.3) + pathutil (~> 0.9) + rouge (>= 1.7, < 4) + safe_yaml (~> 1.0) + jekyll-avatar (0.7.0) + jekyll (>= 3.0, < 5.0) + jekyll-coffeescript (1.1.1) + coffee-script (~> 2.2) + coffee-script-source (~> 1.11.1) + jekyll-commonmark (1.3.1) + commonmarker (~> 0.14) + jekyll (>= 3.7, < 5.0) + jekyll-commonmark-ghpages (0.1.6) + commonmarker (~> 0.17.6) + jekyll-commonmark (~> 1.2) + rouge (>= 2.0, < 4.0) + jekyll-default-layout (0.1.4) + jekyll (~> 3.0) + jekyll-feed (0.13.0) + jekyll (>= 3.7, < 5.0) + jekyll-gist (1.5.0) + octokit (~> 4.2) + jekyll-github-metadata (2.13.0) + jekyll (>= 3.4, < 5.0) + octokit (~> 4.0, != 4.4.0) + jekyll-mentions (1.5.1) + html-pipeline (~> 2.3) + jekyll (>= 3.7, < 5.0) + jekyll-optional-front-matter (0.3.2) + jekyll (>= 3.0, < 5.0) + jekyll-paginate (1.1.0) + jekyll-readme-index (0.3.0) + jekyll (>= 3.0, < 5.0) + jekyll-redirect-from (0.15.0) + jekyll (>= 3.3, < 5.0) + jekyll-relative-links (0.6.1) + jekyll (>= 3.3, < 5.0) + jekyll-remote-theme (0.4.1) + addressable (~> 2.0) + jekyll (>= 3.5, < 5.0) + rubyzip (>= 1.3.0) + jekyll-sass-converter (1.5.2) + sass (~> 3.4) + jekyll-seo-tag (2.6.1) + jekyll (>= 3.3, < 5.0) + jekyll-sitemap (1.4.0) + jekyll (>= 3.7, < 5.0) + jekyll-swiss (1.0.0) + jekyll-theme-architect (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-cayman (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-dinky (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-hacker (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-leap-day (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-merlot (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-midnight (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-minimal (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-modernist (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-primer (0.5.4) + jekyll (> 3.5, < 5.0) + jekyll-github-metadata (~> 2.9) + jekyll-seo-tag (~> 2.0) + jekyll-theme-slate (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-tactile (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-theme-time-machine (0.1.1) + jekyll (~> 3.5) + jekyll-seo-tag (~> 2.0) + jekyll-titles-from-headings (0.5.3) + jekyll (>= 3.3, < 5.0) + jekyll-watch (2.2.1) + listen (~> 3.0) + jemoji (0.11.1) + gemoji (~> 3.0) + html-pipeline (~> 2.2) + jekyll (>= 3.0, < 5.0) + kramdown (1.17.0) + liquid (4.0.3) + listen (3.2.1) + rb-fsevent (~> 0.10, >= 0.10.3) + rb-inotify (~> 0.9, >= 0.9.10) + mercenary (0.3.6) + mini_portile2 (2.4.0) + minima (2.5.1) + jekyll (>= 3.5, < 5.0) + jekyll-feed (~> 0.9) + jekyll-seo-tag (~> 2.1) + minitest (5.14.0) + multipart-post (2.1.1) + nokogiri (1.10.9) + mini_portile2 (~> 2.4.0) + octokit (4.18.0) + faraday (>= 0.9) + sawyer (~> 0.8.0, >= 0.5.3) + pathutil (0.16.2) + forwardable-extended (~> 2.6) + public_suffix (3.1.1) + rb-fsevent (0.10.3) + rb-inotify (0.10.1) + ffi (~> 1.0) + rouge (3.13.0) + ruby-enum (0.8.0) + i18n + rubyzip (2.3.0) + safe_yaml (1.0.5) + sass (3.7.4) + sass-listen (~> 4.0.0) + sass-listen (4.0.0) + rb-fsevent (~> 0.9, >= 0.9.4) + rb-inotify (~> 0.9, >= 0.9.7) + sawyer (0.8.2) + addressable (>= 2.3.5) + faraday (> 0.8, < 2.0) + terminal-table (1.8.0) + unicode-display_width (~> 1.1, >= 1.1.1) + thread_safe (0.3.6) + typhoeus (1.3.1) + ethon (>= 0.9.0) + tzinfo (1.2.6) + thread_safe (~> 0.1) + unicode-display_width (1.7.0) + zeitwerk (2.3.0) + +PLATFORMS + ruby + +DEPENDENCIES + github-pages + +BUNDLED WITH + 2.1.4 diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 0000000..c741881 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-slate \ No newline at end of file diff --git a/docs/_includes/navbar.html b/docs/_includes/navbar.html new file mode 100644 index 0000000..eb9b9b7 --- /dev/null +++ b/docs/_includes/navbar.html @@ -0,0 +1,9 @@ + diff --git a/docs/_layouts/default.html b/docs/_layouts/default.html new file mode 100644 index 0000000..d6752ac --- /dev/null +++ b/docs/_layouts/default.html @@ -0,0 +1,65 @@ + + + + + + + + + + {% seo %} + + + + + +
+
+ {% if site.github.is_project_page %} + View on GitHub + {% endif %} + +

{{ site.title | default: site.github.repository_name }}

+

{{ site.description | default: site.github.project_tagline }}

+ + {% if site.show_downloads %} +
+ Download this project as a .zip file + Download this project as a tar.gz file +
+ {% endif %} +
+
+ + +{% include navbar.html %} + + +
+
+ {{ content }} +
+
+ + + + +{% if site.google_analytics %} + +{% endif %} + + \ No newline at end of file diff --git a/docs/config_reference.md b/docs/config_reference.md new file mode 100644 index 0000000..b9ef52d --- /dev/null +++ b/docs/config_reference.md @@ -0,0 +1,48 @@ +# Configuration + +git-mkver comes with a default configuration. It can be overriden by creating a `mkver.conf` file. git-mkver will search +for this file in the current working directory. + +The application uses the HOCON format. More details on the specification can be found +[here][https://github.com/lightbend/config/blob/master/HOCON.md]. + +## mkver.conf + +```hocon +# d +defaults { + prefix: v + tagMessageFormat: "release %ver - buildno: %bn" + tagParts: VersionBuildMetadata + #minimumVersionIncrement: Major|Minor|Patch|PreRelease|None + patches: [ + helm-chart + csproj + ] +} +branches: [ + { + name: "master" + tag: true + tagParts: Version + } + { + name: ".*" + tag: false + } +] +patches: [ + { + name: helm-chart + filePatterns: ["**/Chart.yaml"] + find: "version: .*" + replace: "version: \"%ver\"" + } + { + name: csproj + filePatterns: ["**/*.csproj"] + find: ".*" + replace: "%ver" + } +] +``` \ No newline at end of file diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..75d3ab4 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,20 @@ +Helps version your software and patch version numbers into the build. + +## Features + +- Determine next version based on: + - Last tagged commit + - [Conventional Commits](https://www.conventionalcommits.org/) + - Branch names + - Manual tagging +- Next version conforms to [Semantic Versioning](https://semver.org/) scheme +- Patch the next version into the build: + - Java + - C# + - Many others, fully configurable +- Tag the current commit with the next version + +All of this can be configured based on the branch name so release/master branches get different +version numbers to develop or feature branches. + + diff --git a/docs/usage.md b/docs/usage.md new file mode 100644 index 0000000..391844c --- /dev/null +++ b/docs/usage.md @@ -0,0 +1,49 @@ +## Usage + +Basic usage is to just call `git mkver next` and it will tell you the next +version of the software if you publish now. + +``` +$ git mkver next +0.4.0 +``` + +### Tagging + +If you would like to publish a version mkver can tag the current commit. + +``` +$ git mkver tag +``` + +This will apply an annotated tag from the `next` command to the current commit. + +### Patching versions in files + +If you would like to patch version numbers in files prior to building and tagging then +you can use the `patch` command. The files to be patched and the replacements are +defined in the `mkver.yaml` config file. A large number of standard patches come +pre-defined. + +``` +$ git mkver patch +``` + +### Usage Patterns + + +Developers commit to master or work on feature branches: + +- Any commit containing `feat:` will bump the minor version +- Any commit containing `fix:` will bump the patch version + +The build script run by the build server would look something like: + +``` +nextVer=$(git mkver next) +git tag -a -m "New Version" "v$nextVer" +# Publish artifacts +``` + +To control the frequency of releases, include these steps only on manually +triggered builds. diff --git a/src/main/resources/application.conf b/src/main/resources/application.conf new file mode 100644 index 0000000..9e52c2d --- /dev/null +++ b/src/main/resources/application.conf @@ -0,0 +1,35 @@ +defaults { + prefix: v + tagMessageFormat: "release %ver - buildno: %bn" + tagParts: VersionBuildMetadata + #minimumVersionIncrement: Major|Minor|Patch|PreRelease|None + patches: [ + helm-chart + csproj + ] +} +branches: [ + { + name: "master" + tag: true + tagParts: Version + } + { + name: ".*" + tag: false + } +] +patches: [ + { + name: helm-chart + filePatterns: ["**/Chart.yaml"] + find: "version: .*" + replace: "version: \"%ver\"" + } + { + name: csproj + filePatterns: ["**/*.csproj"] + find: ".*" + replace: "%ver" + } +] diff --git a/src/main/resources/mkver.yaml b/src/main/resources/mkver.yaml deleted file mode 100644 index aad2525..0000000 --- a/src/main/resources/mkver.yaml +++ /dev/null @@ -1,23 +0,0 @@ -defaults: - prefix: v - minimumVersionIncrement: Major|Minor|Patch|PreRelease - patches: - - helm-chart - - csproj - -branches: - - name: "master" - tag: true - tagParts: Version - tagMessageFormat: "release %v - buildno: %bn" - preReleaseFormat: "" - buildMetadataFormat: "" - - name: ".*" - tag: false - patches: - - helm-chart - - csproj - -patches: - - name: helm-chart - files: "**/Chart.yaml" diff --git a/src/main/scala/net/cardnell/mkver/AppConfig.scala b/src/main/scala/net/cardnell/mkver/AppConfig.scala new file mode 100644 index 0000000..3a4d53e --- /dev/null +++ b/src/main/scala/net/cardnell/mkver/AppConfig.scala @@ -0,0 +1,150 @@ +package net.cardnell.mkver + +import zio.IO +import zio.config._ +import ConfigDescriptor._ +import zio.config.PropertyTree._ +import zio.config.ConfigDocs._ +import ConfigDocs.Details._ +import better.files.File +import com.typesafe.config.ConfigFactory +import zio.config.typesafe.{TypeSafeConfigSource, TypesafeConfig} + + +case class BranchConfig(name: String, + prefix: String, + tag: Boolean, + tagParts: TagParts, + tagMessageFormat: String, + preReleaseName: String, + buildMetadataFormat: String, + patches: List[String]) + +case class BranchConfigOpt(name: String, + prefix: Option[String], + tag: Option[Boolean], + tagParts: Option[TagParts], + tagMessageFormat: Option[String], + preReleaseName: Option[String], + buildMetadataFormat: Option[String], + patches: Option[List[String]]) + +object BranchConfig { + val nameDesc = string("name").describe("regex to match branch name on") + val prefixDesc = string("prefix").describe("prefix for git tags") + val tagDesc = boolean("tag").describe("whether to actually tag this branch when `mkver tag` is called") + val tagPartsDesc = string("tagParts")(TagParts.apply, TagParts.unapply).describe("") + val tagMessageFormatDesc = string("tagMessageFormat").describe("") + val preReleaseNameDesc = string("preReleaseName").describe("") + val buildMetadataFormatDesc = string("buildMetadataFormat").describe("format string to produce build metadata part of a semantic version") + val patchesDesc = list(string("patches")).describe("Patch configs to be applied") + + val branchConfigDesc = ( + nameDesc.default(".*") |@| + prefixDesc.default("v") |@| + tagDesc.default(false) |@| + tagPartsDesc.default(TagParts.VersionBuildMetadata) |@| + tagMessageFormatDesc.default("release %ver") |@| + preReleaseNameDesc.default("rc.") |@| + buildMetadataFormatDesc.default("%br.%sh") |@| + patchesDesc.default(Nil) + )(BranchConfig.apply, BranchConfig.unapply) + + val branchConfigOptDesc = ( + nameDesc |@| + prefixDesc.optional |@| + tagDesc.optional |@| + tagPartsDesc.optional |@| + tagMessageFormatDesc.optional |@| + preReleaseNameDesc.optional |@| + buildMetadataFormatDesc.optional |@| + patchesDesc.optional + )(BranchConfigOpt.apply, BranchConfigOpt.unapply) +} + +sealed trait TagParts +object TagParts { + case object Version extends TagParts + //case object VersionPreRelease extends TagParts + case object VersionBuildMetadata extends TagParts + //case object VersionPreReleaseBuildMetadata extends TagParts + + def apply(tagParts: String): TagParts = { + tagParts match { + case "Version" => Version + //case "VersionPreRelease" => VersionPreRelease + case "VersionBuildMetadata" => VersionBuildMetadata + //case "VersionPreReleaseBuildMetadata" => VersionPreReleaseBuildMetadata + } + } + + def unapply(arg: TagParts): Option[String] = Some(arg.toString) +} + +case class PatchConfig(name: String, filePatterns: List[String], find: String, replace: String) + +object PatchConfig { + val patchConfigDesc = ( + string("name").describe("Name of patch, referenced from branch configs") |@| + list(string("filePatterns").describe("Files to apply find and replace in. Supports ** and * glob patterns.")) |@| + string("find").describe("Regex to find in file") |@| + string("replace").describe("Replacement string. Can include version format strings (see help)") + )(PatchConfig.apply, PatchConfig.unapply) +} + +case class AppConfig(defaults: BranchConfig, branches: List[BranchConfigOpt], patches: List[PatchConfig], formats: List[String]) + +object AppConfig { + val appConfigDesc = ( + nested("defaults")(BranchConfig.branchConfigDesc) |@| + nested("branches")(list(BranchConfig.branchConfigOptDesc)) |@| + nested("patches")(list(PatchConfig.patchConfigDesc)) |@| + list(string("formats")).default(Nil) + )(AppConfig.apply, AppConfig.unapply) + + def getBranchConfig(currentBranch: String): BranchConfig = { + val appConfig = getAppConfig() + val defaults = appConfig.defaults + + val branchConfig = appConfig.branches.find { bc => currentBranch.matches(bc.name) } + + branchConfig.map { bc => + BranchConfig( + name = bc.name, + prefix = bc.prefix.getOrElse(defaults.prefix), + tag = bc.tag.getOrElse(defaults.tag), + tagParts = bc.tagParts.getOrElse(defaults.tagParts), + tagMessageFormat = bc.tagMessageFormat.getOrElse(defaults.tagMessageFormat), + preReleaseName = bc.preReleaseName.getOrElse(defaults.preReleaseName), + buildMetadataFormat = bc.buildMetadataFormat.getOrElse(defaults.buildMetadataFormat), + patches = bc.patches.getOrElse(defaults.patches) + ) + }.getOrElse(defaults) + } + + def getPatchConfigs(branchConfig: BranchConfig): List[PatchConfig] = { + val allPatchConfigs = getAppConfig().patches.map(it => (it.name, it)).toMap + branchConfig.patches.map(allPatchConfigs.get(_).orElse(sys.error("Can't find patch config")).get) + } + + def getAppConfig(): AppConfig = { + val hocon = if (File("mkver.conf").exists) { + TypeSafeConfigSource.fromTypesafeConfig(ConfigFactory.parseFile(new java.io.File("mkver.conf"))) + // TODO Use this when in ZIO land + // TypeSafeConfigSource.fromHoconFile(new java.io.File("mkver.conf")) + } else { + TypeSafeConfigSource.fromTypesafeConfig(ConfigFactory.load("application.conf")) + } + + val config = + hocon match { + case Left(value) => sys.error("Unable to load config: " + value) + case Right(source) => read(AppConfig.appConfigDesc from source) + } + + config match { + case Left(value) => sys.error("Unable to parse config: " + value) + case Right(result) => result + } + } +} diff --git a/src/main/scala/net/cardnell/mkver/Config.scala b/src/main/scala/net/cardnell/mkver/Config.scala deleted file mode 100644 index 299de3e..0000000 --- a/src/main/scala/net/cardnell/mkver/Config.scala +++ /dev/null @@ -1,26 +0,0 @@ -package net.cardnell.mkver - -import scala.util.matching.Regex - -case class BranchConfig(name: Regex, - prefix: String, - tag: Boolean, - tagParts: TagParts, - tagMessageFormat: String, - preReleaseName: String, - buildMetadataFormat: String, - patches: List[PatchConfig]) - -sealed trait TagParts -object TagParts { - case object Version extends TagParts - case object VersionPreRelease extends TagParts - case object VersionBuildMetadata extends TagParts - case object VersionPreReleaseBuildMetadata extends TagParts -} - -case class PatchConfig(val name: String, val filePatterns: List[String], val findRegex: String, val replace: String) - -object Config { - -} diff --git a/src/main/scala/net/cardnell/mkver/Main.scala b/src/main/scala/net/cardnell/mkver/Main.scala index 0e1b13e..f64851d 100644 --- a/src/main/scala/net/cardnell/mkver/Main.scala +++ b/src/main/scala/net/cardnell/mkver/Main.scala @@ -10,18 +10,13 @@ case class ProcessResult(stdout: String, stderr: String, exitCode: Int) object Main { - val patchConfigs = List( - PatchConfig("helm-chart", List("**/Chart.yaml"), "version: .*", "version: \"%ver\""), - PatchConfig("csproj", List("**/*.csproj"), ".*", "%ver") - ) - def main(args: Array[String]): Unit = { CommandLineArgs.mkverCommand.parse(args, sys.env) match { case Left(help) => System.err.println(help) sys.exit(1) - case Right(NextOpts(_)) => - runNext() + case Right(nextOps@NextOpts(_)) => + runNext(nextOps) case Right(TagOpts(_)) => runTag() case Right(PatchOpts(_)) => @@ -34,18 +29,21 @@ object Main { sys.exit(0) } - def runNext(): Unit = { + def runNext(nextOpts: NextOpts): Unit = { checkGitRepo() - val currentBranch = exec("git rev-parse --abbrev-ref HEAD").stdout - val config = getConfig(currentBranch) + val currentBranch = getCurrentBranch() + val config = AppConfig.getBranchConfig(currentBranch) val nextVersionData = getNextVersion(config, currentBranch) - println(formatTag(config, nextVersionData)) + val output = nextOpts.format.map { format => + VariableReplacer(nextVersionData).replace(format) + }.getOrElse(formatTag(config, nextVersionData)) + println(output) } def runTag(): Unit = { checkGitRepo() - val currentBranch = exec("git rev-parse --abbrev-ref HEAD").stdout - val config = getConfig(currentBranch) + val currentBranch = getCurrentBranch() + val config = AppConfig.getBranchConfig(currentBranch) val nextVersion = getNextVersion(config, currentBranch) val tag = formatTag(config, nextVersion) val tagMessage = VariableReplacer(nextVersion).replace(config.tagMessageFormat) @@ -54,21 +52,18 @@ object Main { } } - def runPatch() = { - val currentBranch = exec("git rev-parse --abbrev-ref HEAD").stdout - val config = getConfig(currentBranch) + def runPatch(): Unit = { + val currentBranch = getCurrentBranch() + val config = AppConfig.getBranchConfig(currentBranch) val nextVersion = getNextVersion(config, currentBranch) - patchConfigs.foreach { patch => - val regex = patch.findRegex.r + AppConfig.getPatchConfigs(config).foreach { patch => + val regex = patch.find.r val replacement = VariableReplacer(nextVersion).replace(patch.replace) - println(replacement) patch.filePatterns.foreach { filePattern => File.currentWorkingDirectory.glob(filePattern, includePath = false).foreach { file => - println(s"checking $file") - val newLines = file.lines().map { line => - regex.replaceAllIn(line, replacement) - } - file.overwrite(newLines.mkString(System.lineSeparator())) + println(s"patching: $file, replacement: $replacement") + val newContent = regex.replaceAllIn(file.contentAsString, replacement) + file.overwrite(newContent) } } } @@ -81,12 +76,4 @@ object Main { System.exit(output.exitCode) } } - - def getConfig(currentBranch: String): BranchConfig = { - if (currentBranch == "master") { - BranchConfig("master".r, "v", true, TagParts.Version, "release %v", "rc", "%sh", patchConfigs) - } else { - BranchConfig(".*".r, "v", false, TagParts.VersionBuildMetadata, "release %v", "rc", "%br.%sh", patchConfigs) - } - } } diff --git a/src/main/scala/net/cardnell/mkver/MkVer.scala b/src/main/scala/net/cardnell/mkver/MkVer.scala index 264cf81..5d9ddfe 100644 --- a/src/main/scala/net/cardnell/mkver/MkVer.scala +++ b/src/main/scala/net/cardnell/mkver/MkVer.scala @@ -63,6 +63,10 @@ object MkVer { lastVersionTag match { case version(major, minor, patch, prerelease, buildmetadata) => Version(major.toInt, minor.toInt, patch.toInt, Option(prerelease), Option(buildmetadata)) + case _ => + System.err.println(s"warning: unable to parse last tag. ($lastVersionTag) doesn't match a SemVer pattern") + System.exit(1) + Version(0, 0, 0, None, None) } } @@ -72,9 +76,9 @@ object MkVer { val buildMetaData = VariableReplacer(versionData).replace(config.buildMetadataFormat) config.tagParts match { case TagParts.Version => version - case TagParts.VersionPreRelease => s"$version-$preRelease" + //case TagParts.VersionPreRelease => s"$version-$preRelease" case TagParts.VersionBuildMetadata =>s"$version+$buildMetaData" - case TagParts.VersionPreReleaseBuildMetadata =>s"$version-$preRelease+$buildMetaData" + //case TagParts.VersionPreReleaseBuildMetadata =>s"$version-$preRelease+$buildMetaData" } } @@ -135,6 +139,16 @@ object MkVer { } } + def getCurrentBranch(): String = { + if (sys.env.contains("BUILD_SOURCEBRANCH")) { + // Azure Devops Pipeline + sys.env("BUILD_SOURCEBRANCH").replace("refs/heads/", "") + } else { + // TODO better fallback if we in detached head mode like build systems do + exec("git rev-parse --abbrev-ref HEAD").stdout + } + } + def exec(command: String): ProcessResult = { exec(command.split(" ")) } diff --git a/src/test/scala/net/cardnell/mkver/ConfigSpec.scala b/src/test/scala/net/cardnell/mkver/ConfigSpec.scala new file mode 100644 index 0000000..d3b444c --- /dev/null +++ b/src/test/scala/net/cardnell/mkver/ConfigSpec.scala @@ -0,0 +1,28 @@ +package net.cardnell.mkver + +import com.typesafe.config.ConfigFactory +import org.scalatest.flatspec.AnyFlatSpec +import org.scalatest.matchers.should.Matchers +import zio.config.read +import zio.config.typesafe.TypeSafeConfigSource +import zio.config.typesafe.TypeSafeConfigSource.fromHoconString +import zio.{App, Has, ZEnv, ZIO, ZLayer, console} + +class ConfigSpec extends AnyFlatSpec with Matchers { + + "config" should "load" in { + //val c = TypeSafeConfigSource.fromDefaultLoader + val c = TypeSafeConfigSource.fromTypesafeConfig(ConfigFactory.load("application.conf")) + println(c) + val config = + c match { + case Left(value) => Left(value) + case Right(source) => read(AppConfig.appConfigDesc from source) + } + + println(config) + + assert(config != null) + } + +}