diff --git a/README.md b/README.md index 4935066df..8dca0ff7b 100644 --- a/README.md +++ b/README.md @@ -5,7 +5,7 @@ A Scala library to fetch dependencies from Maven / Ivy repositories [![Build Status](https://travis-ci.org/coursier/coursier.svg?branch=master)](https://travis-ci.org/coursier/coursier) -[![Build status (Windows)](https://ci.appveyor.com/api/projects/status/trtum5b7washfbj9?svg=true)](https://ci.appveyor.com/project/coursier/coursier) +[![Build status (Windows)](https://ci.appveyor.com/api/projects/status/yy3svc6ukqpykw5s?svg=true)](https://ci.appveyor.com/project/alexarchambault/coursier-a7n6k) [![Join the chat at https://gitter.im/coursier/coursier](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/coursier/coursier?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Maven Central](https://img.shields.io/maven-central/v/io.get-coursier/coursier_2.11.svg)](https://maven-badges.herokuapp.com/maven-central/io.get-coursier/coursier_2.11) [![Scaladoc](http://javadoc-badge.appspot.com/io.get-coursier/coursier_2.11.svg?label=scaladoc)](http://javadoc-badge.appspot.com/io.get-coursier/coursier_2.11) diff --git a/doc/README.md b/doc/README.md index e86d00110..c85b43913 100644 --- a/doc/README.md +++ b/doc/README.md @@ -5,7 +5,7 @@ A Scala library to fetch dependencies from Maven / Ivy repositories [![Build Status](https://travis-ci.org/coursier/coursier.svg?branch=master)](https://travis-ci.org/coursier/coursier) -[![Build status (Windows)](https://ci.appveyor.com/api/projects/status/trtum5b7washfbj9?svg=true)](https://ci.appveyor.com/project/coursier/coursier) +[![Build status (Windows)](https://ci.appveyor.com/api/projects/status/yy3svc6ukqpykw5s?svg=true)](https://ci.appveyor.com/project/alexarchambault/coursier-a7n6k) [![Join the chat at https://gitter.im/coursier/coursier](https://badges.gitter.im/Join%20Chat.svg)](https://gitter.im/coursier/coursier?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Maven Central](https://img.shields.io/maven-central/v/io.get-coursier/coursier_2.11.svg)](https://maven-badges.herokuapp.com/maven-central/io.get-coursier/coursier_2.11) [![Scaladoc](http://javadoc-badge.appspot.com/io.get-coursier/coursier_2.11.svg?label=scaladoc)](http://javadoc-badge.appspot.com/io.get-coursier/coursier_2.11) diff --git a/project/Appveyor.scala b/project/Appveyor.scala new file mode 100644 index 000000000..f0983e17f --- /dev/null +++ b/project/Appveyor.scala @@ -0,0 +1,31 @@ +import argonaut._ +import argonaut.Argonaut._ +import argonaut.ArgonautShapeless._ + +import sbt.Logger + +object Appveyor { + + final case class Build( + buildId: Long, + branch: String, + commitId: String, + status: String + ) + + def branchLastBuild(repo: String, branch: String, log: Logger): Build = { + + final case class Response(build: Build) + + val url = s"https://ci.appveyor.com/api/projects/$repo/branch/$branch" + val rawResp = HttpUtil.fetch(url, log) + + rawResp.decodeEither[Response] match { + case Left(err) => + sys.error(s"Error decoding response from $url: $err") + case Right(resp) => + resp.build + } + } + +} diff --git a/project/HttpUtil.scala b/project/HttpUtil.scala new file mode 100644 index 000000000..1cc3637bf --- /dev/null +++ b/project/HttpUtil.scala @@ -0,0 +1,57 @@ +import java.io.{ByteArrayOutputStream, InputStream} +import java.net.{HttpURLConnection, URL, URLConnection} +import java.nio.charset.StandardCharsets + +import sbt.Logger + +object HttpUtil { + + private def readFully(is: InputStream): Array[Byte] = { + val buffer = new ByteArrayOutputStream + val data = Array.ofDim[Byte](16384) + + var nRead = 0 + while ({ + nRead = is.read(data, 0, data.length) + nRead != -1 + }) + buffer.write(data, 0, nRead) + + buffer.flush() + buffer.toByteArray + } + + def fetch(url: String, log: Logger): String = { + + val url0 = new URL(url) + + log.info(s"Fetching $url") + + val (rawResp, code) = { + + var conn: URLConnection = null + var httpConn: HttpURLConnection = null + var is: InputStream = null + + try { + conn = url0.openConnection() + httpConn = conn.asInstanceOf[HttpURLConnection] + httpConn.setRequestProperty("Accept", "application/vnd.travis-ci.2+json") + is = conn.getInputStream + + (readFully(is), httpConn.getResponseCode) + } finally { + if (is != null) + is.close() + if (httpConn != null) + httpConn.disconnect() + } + } + + if (code / 100 != 2) + sys.error(s"Unexpected response code when getting $url: $code") + + new String(rawResp, StandardCharsets.UTF_8) + } + +} diff --git a/project/Publish.scala b/project/Publish.scala index a18005312..50b8a03e4 100644 --- a/project/Publish.scala +++ b/project/Publish.scala @@ -62,6 +62,6 @@ object Publish { } ) - lazy val released = pomStuff ++ pushToSonatypeStuff + lazy val released = pomStuff ++ pushToSonatypeStuff ++ Release.settings } diff --git a/project/Release.scala b/project/Release.scala new file mode 100644 index 000000000..911ecb3c1 --- /dev/null +++ b/project/Release.scala @@ -0,0 +1,287 @@ +import java.nio.charset.StandardCharsets +import java.nio.file.Files +import java.util.regex.Pattern + +import com.typesafe.sbt.pgp.PgpKeys +import sbt._ +import sbt.Keys._ +import sbtrelease.ReleasePlugin.autoImport._ +import sbtrelease.ReleasePlugin.autoImport.ReleaseTransformations._ + +import scala.io._ + +object Release { + + implicit final class StateOps(val state: State) extends AnyVal { + def vcs: sbtrelease.Vcs = + Project.extract(state).get(releaseVcs).getOrElse { + sys.error("VCS not set") + } + } + + val checkTravisStatus = ReleaseStep { state => + + val currentHash = state.vcs.currentHash + + val build = Travis.builds("coursier/coursier", state.log) + .find { build => + build.job_ids.headOption.exists { id => + Travis.job(id, state.log).commit.sha == currentHash + } + } + .getOrElse { + sys.error(s"Status for commit $currentHash not found on Travis") + } + + state.log.info(s"Found build ${build.id.value} for commit $currentHash, state: ${build.state}") + + build.state match { + case "passed" => + case _ => + sys.error(s"Build for $currentHash in state ${build.state}") + } + + state + } + + val checkAppveyorStatus = ReleaseStep { state => + + val currentHash = state.vcs.currentHash + + val build = Appveyor.branchLastBuild("alexarchambault/coursier-a7n6k", "master", state.log) + + state.log.info(s"Found last build ${build.buildId} for branch master, status: ${build.status}") + + if (build.commitId != currentHash) + sys.error(s"Last master Appveyor build corresponds to commit ${build.commitId}, expected $currentHash") + + if (build.status != "success") + sys.error(s"Last master Appveyor build status: ${build.status}") + + state + } + + val previousReleaseVersion = AttributeKey[String]("previousReleaseVersion") + val initialVersion = AttributeKey[String]("initialVersion") + + val saveInitialVersion = ReleaseStep { state => + val currentVer = Project.extract(state).get(version) + state.put(initialVersion, currentVer) + } + + def versionChanges(state: State): Boolean = { + + val initialVer = state.get(initialVersion).getOrElse { + sys.error(s"${initialVersion.label} key not set") + } + val (_, nextVer) = state.get(ReleaseKeys.versions).getOrElse { + sys.error(s"${ReleaseKeys.versions.label} key not set") + } + + initialVer == nextVer + } + + + val updateVersionPattern = "(?m)^VERSION=.*$".r + def updateVersionInScript(file: File, newVersion: String): Unit = { + val content = Source.fromFile(file)(Codec.UTF8).mkString + + updateVersionPattern.findAllIn(content).toVector match { + case Seq() => sys.error(s"Found no matches in $file") + case Seq(_) => + case _ => sys.error(s"Found too many matches in $file") + } + + val newContent = updateVersionPattern.replaceAllIn(content, "VERSION=" + newVersion) + Files.write(file.toPath, newContent.getBytes(StandardCharsets.UTF_8)) + } + + + val updateScripts = ReleaseStep { state => + + val (releaseVer, _) = state.get(ReleaseKeys.versions).getOrElse { + sys.error(s"${ReleaseKeys.versions.label} key not set") + } + + val scriptsDir = Project.extract(state).get(baseDirectory.in(ThisBuild)) / "scripts" + val scriptFiles = Seq( + scriptsDir / "generate-launcher.sh", + scriptsDir / "generate-sbt-launcher.sh" + ) + + val vcs = state.vcs + + for (f <- scriptFiles) { + updateVersionInScript(f, releaseVer) + vcs.add(f.getAbsolutePath).!!(state.log) + } + + state + } + + val updateLaunchers = ReleaseStep { state => + + val baseDir = Project.extract(state).get(baseDirectory.in(ThisBuild)) + val scriptsDir = baseDir / "scripts" + val scriptFiles = Seq( + (scriptsDir / "generate-launcher.sh") -> (baseDir / "coursier"), + (scriptsDir / "generate-sbt-launcher.sh") -> (baseDir / "csbt") + ) + + val vcs = state.vcs + + for ((f, output) <- scriptFiles) { + sbt.Process(Seq(f.getAbsolutePath, "-f")).!!(state.log) + vcs.add(output.getAbsolutePath).!!(state.log) + } + + state + } + + val savePreviousReleaseVersion = ReleaseStep { state => + + val cmd = Seq(state.vcs.commandName, "tag", "-l") + + val tag = scala.sys.process.Process(cmd) + .!! + .linesIterator + .toVector + .lastOption + .getOrElse { + sys.error(s"Found no tags when running ${cmd.mkString(" ")}") + } + + val ver = + if (tag.startsWith("v")) + tag.stripPrefix("v") + else + sys.error(s"Last tag '$tag' doesn't start with 'v'") + + state.put(previousReleaseVersion, ver) + } + + val updateTutReadme = ReleaseStep { state => + + val previousVer = state.get(previousReleaseVersion).getOrElse { + sys.error(s"${previousReleaseVersion.label} key not set") + } + val (releaseVer, _) = state.get(ReleaseKeys.versions).getOrElse { + sys.error(s"${ReleaseKeys.versions.label} key not set") + } + + val readmeFile = Project.extract(state).get(baseDirectory.in(ThisBuild)) / "doc" / "README.md" + val pattern = Pattern.quote(previousVer).r + + val content = Source.fromFile(readmeFile)(Codec.UTF8).mkString + val newContent = pattern.replaceAllIn(content, releaseVer) + Files.write(readmeFile.toPath, newContent.getBytes(StandardCharsets.UTF_8)) + + state.vcs.add(readmeFile.getAbsolutePath).!!(state.log) + + state + } + + val stageReadme = ReleaseStep { state => + + val baseDir = Project.extract(state).get(baseDirectory.in(ThisBuild)) + val processedReadmeFile = baseDir / "README.md" + + state.vcs.add(processedReadmeFile.getAbsolutePath).!!(state.log) + + state + } + + + val coursierVersionPattern = s"(?m)^${Pattern.quote("def coursierVersion = \"")}[^${'"'}]*${Pattern.quote("\"")}$$".r + + val updatePluginsSbt = ReleaseStep { state => + + val vcs = state.vcs + + val (releaseVer, _) = state.get(ReleaseKeys.versions).getOrElse { + sys.error(s"${ReleaseKeys.versions.label} key not set") + } + + val baseDir = Project.extract(state).get(baseDirectory.in(ThisBuild)) + val pluginsSbtFile = baseDir / "project" / "plugins.sbt" + val projectPluginsSbtFile = baseDir / "project" / "project" / "plugins.sbt" + + val files = Seq( + pluginsSbtFile, + projectPluginsSbtFile + ) + + for (f <- files) { + val content = Source.fromFile(f)(Codec.UTF8).mkString + + coursierVersionPattern.findAllIn(content).toVector match { + case Seq() => sys.error(s"Found no matches in $f") + case Seq(_) => + case _ => sys.error(s"Found too many matches in $f") + } + + val newContent = coursierVersionPattern.replaceAllIn(content, "def coursierVersion = \"" + releaseVer + "\"") + Files.write(f.toPath, newContent.getBytes(StandardCharsets.UTF_8)) + vcs.add(f.getAbsolutePath).!!(state.log) + } + + state + } + + val commitUpdates = ReleaseStep( + action = { state => + + val (releaseVer, _) = state.get(ReleaseKeys.versions).getOrElse { + sys.error(s"${ReleaseKeys.versions.label} key not set") + } + + state.vcs.commit(s"Updates for $releaseVer", sign = true).!(state.log) + + state + }, + check = { state => + + val vcs = state.vcs + + if (vcs.hasModifiedFiles) + sys.error("Aborting release: unstaged modified files") + + if (vcs.hasUntrackedFiles && !Project.extract(state).get(releaseIgnoreUntrackedFiles)) + sys.error( + "Aborting release: untracked files. Remove them or specify 'releaseIgnoreUntrackedFiles := true' in settings" + ) + + state + } + ) + + + val settings = Seq( + releaseProcess := Seq[ReleaseStep]( + checkTravisStatus, + checkAppveyorStatus, + savePreviousReleaseVersion, + checkSnapshotDependencies, + inquireVersions, + saveInitialVersion, + setReleaseVersion, + commitReleaseVersion, + publishArtifacts, + releaseStepCommand("sonatypeRelease"), + updateScripts, + updateLaunchers, + updateTutReadme, + releaseStepCommand("tut"), + stageReadme, + updatePluginsSbt, + commitUpdates, + tagRelease, + setNextVersion, + commitNextVersion, + ReleaseStep(_.reload), + pushChanges + ), + releasePublishArtifactsAction := PgpKeys.publishSigned.value + ) + +} diff --git a/project/Travis.scala b/project/Travis.scala new file mode 100644 index 000000000..e6878d2b3 --- /dev/null +++ b/project/Travis.scala @@ -0,0 +1,82 @@ +import argonaut._ +import argonaut.Argonaut._ +import argonaut.ArgonautShapeless._ + +import sbt.Logger + +object Travis { + + final case class BuildId(value: Long) extends AnyVal + object BuildId { + implicit val decode: DecodeJson[BuildId] = + DecodeJson.LongDecodeJson.map(BuildId(_)) + } + + final case class JobId(value: Long) extends AnyVal + object JobId { + implicit val decode: DecodeJson[JobId] = + DecodeJson.LongDecodeJson.map(JobId(_)) + } + + final case class CommitId(value: Long) extends AnyVal + object CommitId { + implicit val decode: DecodeJson[CommitId] = + DecodeJson.LongDecodeJson.map(CommitId(_)) + } + + final case class Build( + id: BuildId, + job_ids: List[JobId], + pull_request: Boolean, + state: String, + commit_id: CommitId + ) + + final case class Builds( + builds: List[Build] + ) + + final case class Commit( + id: CommitId, + sha: String, + branch: String + ) + + final case class JobDetails( + state: String + ) + + final case class Job( + commit: Commit, + job: JobDetails + ) + + + def builds(repo: String, log: Logger): List[Build] = { + + val url = s"https://api.travis-ci.org/repos/$repo/builds" + val resp = HttpUtil.fetch(url, log) + + resp.decodeEither[Builds] match { + case Left(err) => + sys.error(s"Error decoding response from $url: $err") + case Right(builds) => + log.info(s"Got ${builds.builds.length} builds") + builds.builds + } + } + + def job(id: JobId, log: Logger): Job = { + + val url = s"https://api.travis-ci.org/jobs/${id.value}" + val resp = HttpUtil.fetch(url, log) + + resp.decodeEither[Job] match { + case Left(err) => + sys.error(s"Error decoding response from $url: $err") + case Right(job) => + job + } + } + +} \ No newline at end of file diff --git a/project/build.properties b/project/build.properties index a6e117b61..64317fdae 100644 --- a/project/build.properties +++ b/project/build.properties @@ -1 +1 @@ -sbt.version=0.13.8 +sbt.version=0.13.15 diff --git a/project/plugins.sbt b/project/plugins.sbt index 7f76b208a..ffa8bf444 100644 --- a/project/plugins.sbt +++ b/project/plugins.sbt @@ -1,10 +1,27 @@ -addSbtPlugin("org.xerial.sbt" % "sbt-pack" % "0.8.2") -addSbtPlugin("org.scala-js" % "sbt-scalajs" % "0.6.15") -addSbtPlugin("com.jsuereth" % "sbt-pgp" % "1.0.0") -addSbtPlugin("org.scoverage" % "sbt-scoverage" % "1.4.0") -addSbtPlugin("org.tpolecat" % "tut-plugin" % "0.4.8") -addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.0-RC1") -addSbtPlugin("io.get-coursier" % "sbt-shading" % "1.0.0-RC1") -addSbtPlugin("com.typesafe.sbt" % "sbt-proguard" % "0.2.2") -addSbtPlugin("com.typesafe" % "sbt-mima-plugin" % "0.1.13") -libraryDependencies += "org.scala-sbt" % "scripted-plugin" % sbtVersion.value + +plugins_( + "io.get-coursier" % "sbt-coursier" % coursierVersion, + "com.typesafe" % "sbt-mima-plugin" % "0.1.13", + "org.xerial.sbt" % "sbt-pack" % "0.8.2", + "com.jsuereth" % "sbt-pgp" % "1.0.0", + "com.typesafe.sbt" % "sbt-proguard" % "0.2.2", + "com.github.gseitz" % "sbt-release" % "1.0.4", + "org.scala-js" % "sbt-scalajs" % "0.6.15", + "org.scoverage" % "sbt-scoverage" % "1.4.0", + "io.get-coursier" % "sbt-shading" % coursierVersion, + "org.xerial.sbt" % "sbt-sonatype" % "1.1", + "org.tpolecat" % "tut-plugin" % "0.4.8" +) + +libs ++= Seq( + "org.scala-sbt" % "scripted-plugin" % sbtVersion.value, + compilerPlugin("org.scalamacros" % "paradise" % "2.1.0" cross CrossVersion.full), // for shapeless / auto type class derivations + "com.github.alexarchambault" %% "argonaut-shapeless_6.2" % "1.2.0-M5" +) + +// important: this line is matched / substituted during releases (via sbt-release) +def coursierVersion = "1.0.0-RC1" + + +def plugins_(modules: ModuleID*) = modules.map(addSbtPlugin) +def libs = libraryDependencies diff --git a/project/project/plugins.sbt b/project/project/plugins.sbt index dfe274c01..f9b32135b 100644 --- a/project/project/plugins.sbt +++ b/project/project/plugins.sbt @@ -1 +1,4 @@ -addSbtPlugin("io.get-coursier" % "sbt-coursier" % "1.0.0-RC1") +addSbtPlugin("io.get-coursier" % "sbt-coursier" % coursierVersion) + +// important: this line is matched / substituted during releases (via sbt-release) +def coursierVersion = "1.0.0-RC1" diff --git a/scripts/generate-sbt-launcher.sh b/scripts/generate-sbt-launcher.sh index be0cc7fd1..364980aa5 100755 --- a/scripts/generate-sbt-launcher.sh +++ b/scripts/generate-sbt-launcher.sh @@ -1,12 +1,14 @@ #!/usr/bin/env bash set -e -COURSIER_VERSION=1.0.0-M15-2 +VERSION=1.0.0-M15-2 "$(dirname "$0")/../coursier" bootstrap \ - "io.get-coursier:sbt-launcher_2.12:$COURSIER_VERSION" \ + "io.get-coursier:sbt-launcher_2.12:$VERSION" \ -r sonatype:releases \ + --no-default \ -i launcher \ -I launcher:org.scala-sbt:launcher-interface:1.0.0 \ -o csbt \ - -J -Djline.shutdownhook=false + -J -Djline.shutdownhook=false \ + "$@" diff --git a/tests/shared/src/test/scala/coursier/test/CentralTests.scala b/tests/shared/src/test/scala/coursier/test/CentralTests.scala index 8dceeb69d..f43283819 100644 --- a/tests/shared/src/test/scala/coursier/test/CentralTests.scala +++ b/tests/shared/src/test/scala/coursier/test/CentralTests.scala @@ -151,7 +151,7 @@ object CentralTests extends TestSuite { ): Future[T] = async { val res = await(resolve(deps, extraRepo = extraRepo)) - assert(res.errors.isEmpty) + assert(res.metadataErrors.isEmpty) assert(res.conflicts.isEmpty) assert(res.isDone) @@ -444,7 +444,7 @@ object CentralTests extends TestSuite { val res = await(resolve(deps)) - assert(res.errors.isEmpty) + assert(res.metadataErrors.isEmpty) assert(res.conflicts.isEmpty) assert(res.isDone) @@ -479,7 +479,7 @@ object CentralTests extends TestSuite { val res = await(resolve(deps)) - assert(res.errors.isEmpty) + assert(res.metadataErrors.isEmpty) assert(res.conflicts.isEmpty) assert(res.isDone)