Skip to content

Commit bcbd496

Browse files
committed
Generate changelog during release
1 parent b585920 commit bcbd496

2 files changed

Lines changed: 147 additions & 0 deletions

File tree

build.sbt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,8 @@ releaseProcess := Seq[ReleaseStep](
7474
setReleaseVersion,
7575
commitReleaseVersion,
7676
tagRelease,
77+
generateReleaseChangelog,
78+
commitChangelog,
7779
releaseStepCommandAndRemaining("^ publishSigned"),
7880
setNextVersion,
7981
commitNextVersion,

project/ChangelogPlugin.scala

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import sbt._
2+
import sbt.Keys._
3+
import sbt.complete._
4+
import sbt.complete.DefaultParsers._
5+
import sbtrelease.ReleasePlugin.autoImport._
6+
import sbtrelease.{ReleasePlugin, Vcs}
7+
8+
import scala.sys.process._
9+
10+
/**
11+
* == Changelog Plugin ==
12+
*
13+
* This plugins manages the `CHANGELOG.md` file via the `github_changelog_generator` tool.
14+
* Make sure to follow the instructions on the project page and `github_changelog_generator`
15+
* is available.
16+
*
17+
* You will also need a Github token during the release process. `github_changelog_generator`
18+
* supports environment files or username/password combinations, but at this point the ChangelogPlugin
19+
* doesn't support them. The plugin also relies on the sensible defaults from `github_changelog_generator`,
20+
* which means
21+
* - the project name and organization are picked up automatically
22+
* - the output file is named CHANGELOG.md
23+
* - the changelog file includes everything (issues, pull requests)
24+
* - generated for all available version tasks
25+
* - runs in verbose mode
26+
*
27+
* @see [[https://github.com/skywinder/github-changelog-generator]]
28+
* @see [[https://github.com/skywinder/github-changelog-generator/wiki/Advanced-change-log-generation-examples]]
29+
*/
30+
object ChangelogPlugin extends AutoPlugin {
31+
32+
override def requires: Plugins = ReleasePlugin
33+
34+
override def trigger = AllRequirements
35+
36+
object autoImport {
37+
38+
/**
39+
* Generates the changelog during a release
40+
*/
41+
val generateReleaseChangelog = ReleaseStep(generateChangelogStep)
42+
43+
/**
44+
* Commits the generated changelog.
45+
*/
46+
val commitChangelog = ReleaseStep(commitChangelogStep)
47+
48+
/**
49+
* Task that executes the `github_changelog_generator`.
50+
*/
51+
val generateChangelog: InputKey[Unit] =
52+
inputKey[Unit]("executes the github_changelog_generator to update the CHANGELOG.MD")
53+
54+
/**
55+
* Github token used for the changelog generation
56+
*/
57+
val generateChangelogToken: SettingKey[Option[String]] =
58+
settingKey[Option[String]]("github token used for the changelog generation")
59+
}
60+
61+
import autoImport._
62+
63+
private case class GithubChangeLogParameters(token: String)
64+
65+
private val githubChangeLogParser: Parser[GithubChangeLogParameters] = {
66+
(Space ~ token("--token") ~ Space ~> StringBasic).map(GithubChangeLogParameters)
67+
}
68+
69+
override def projectSettings: Seq[Setting[_]] =
70+
Seq(generateChangelogToken := None, generateChangelog := {
71+
val log = streams.value.log
72+
val parameters = githubChangeLogParser.parsed
73+
Seq("github_changelog_generator", "--token", parameters.token) ! log match {
74+
case 0 => log.success("CHANGELOG.md updated successfully")
75+
case n => sys.error(s"Failed updating CHANGELOG.md. Process existed with status code $n")
76+
}
77+
})
78+
79+
private def generateChangelogStep(state: State): State = {
80+
val extracted = Project.extract(state)
81+
val predefinedToken = extracted.get(generateChangelogToken)
82+
83+
val githubToken = readToken(predefinedToken)
84+
85+
val (newState, _) = extracted.runInputTask(generateChangelog, s" --token $githubToken", state)
86+
newState
87+
}
88+
89+
private def commitChangelogStep(state: State): State = {
90+
val log = toProcessLogger(state)
91+
val base = vcs(state).baseDir
92+
val sign = Project.extract(state).get(releaseVcsSign)
93+
val changelogFile = base / "CHANGELOG.md"
94+
95+
val relativePath = IO
96+
.relativize(base, changelogFile)
97+
.getOrElse(
98+
"Version file [%s] is outside of this VCS repository with base directory [%s]!" format (changelogFile, base)
99+
)
100+
101+
vcs(state).add(relativePath) !! log
102+
val vcsAddOutput = (vcs(state).status !!).trim
103+
if (vcsAddOutput.isEmpty) {
104+
state.log.info("CHANGELOG.md hasn't been changed.")
105+
} else {
106+
vcs(state).commit("Update changelog", sign) ! log
107+
}
108+
109+
state
110+
}
111+
112+
/**
113+
* Extracts the used vcs.
114+
*
115+
* Copied from the sbt-release plugin.
116+
* @param state sbt state
117+
* @return vcs implementation
118+
*/
119+
private def vcs(state: State): Vcs =
120+
Project
121+
.extract(state)
122+
.get(releaseVcs)
123+
.getOrElse(sys.error("Aborting release. Working directory is not a repository of a recognized VCS."))
124+
125+
/**
126+
* Creates a ProcessLogger from the current sbt state.
127+
*
128+
* Copied from the sbt-release plugin.
129+
* @param state
130+
* @return a process logger
131+
*/
132+
private def toProcessLogger(state: State): ProcessLogger = new ProcessLogger {
133+
override def err(s: => String): Unit = state.log.info(s)
134+
override def out(s: => String): Unit = state.log.info(s)
135+
override def buffer[T](f: => T): T = state.log.buffer(f)
136+
}
137+
138+
private def readToken(predefinedToken: Option[String]): String =
139+
predefinedToken.getOrElse(SimpleReader.readLine("Github token: ") match {
140+
case Some(input) if input.trim.isEmpty => sys.error("No token provided")
141+
case Some(input) => input
142+
case None => sys.error("No token provided")
143+
})
144+
145+
}

0 commit comments

Comments
 (0)