From 54ce83b4c28529fc251ac5b9c7d83f19615d7ebd Mon Sep 17 00:00:00 2001 From: Matthew de Detrich Date: Mon, 16 Oct 2023 19:39:54 +0200 Subject: [PATCH] Add ability to sign using sbt-pgp (#33) --- .github/workflows/ci.yml | 3 + README.md | 15 +++ build.sbt | 9 ++ .../pjfanning/sourcedist/GeneratedDist.scala | 8 ++ .../pjfanning/sourcedist/ShaUtils.scala | 13 ++- .../sourcedist/SignedGeneratedDist.scala | 5 + .../sourcedist/SourceDistGenerate.scala | 5 +- .../pjfanning/sourcedist/SourceDistKeys.scala | 8 +- .../sourcedist/SourceDistPlugin.scala | 21 +++- .../sbt-source-dist/signing/build.sbt | 5 + .../signing/project/plugins.sbt | 5 + .../sbt-source-dist/signing/pubring.pgp | Bin 0 -> 2265 bytes .../signing/src/main/scala/hello.scala | 3 + src/sbt-test/sbt-source-dist/signing/test | 6 + test-key.gpg | 105 ++++++++++++++++++ 15 files changed, 200 insertions(+), 11 deletions(-) create mode 100644 src/main/scala/com/github/pjfanning/sourcedist/GeneratedDist.scala create mode 100644 src/main/scala/com/github/pjfanning/sourcedist/SignedGeneratedDist.scala create mode 100644 src/sbt-test/sbt-source-dist/signing/build.sbt create mode 100644 src/sbt-test/sbt-source-dist/signing/project/plugins.sbt create mode 100644 src/sbt-test/sbt-source-dist/signing/pubring.pgp create mode 100644 src/sbt-test/sbt-source-dist/signing/src/main/scala/hello.scala create mode 100644 src/sbt-test/sbt-source-dist/signing/test create mode 100644 test-key.gpg diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1896208..e9f03e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,6 +51,9 @@ jobs: if: matrix.java == 'temurin@8' && steps.setup-java-temurin-8.outputs.cache-hit == 'false' run: sbt '++ ${{ matrix.scala }}' reload +update + - name: Setup key + run: gpg --import test-key.gpg + - name: Check that workflows are up to date run: sbt githubWorkflowCheck diff --git a/README.md b/README.md index 3041a69..25caa08 100644 --- a/README.md +++ b/README.md @@ -34,3 +34,18 @@ You can then generate the source distribution using ``` sbt sourceDistGenerate ``` + +sbt-source-dist also supports signing the final generated source distribution +using [sbt-pgp](https://github.com/sbt/sbt-pgp). Signing the source +distribution in this manner has the added advantage of ensuring that the same +signing key that is used to sign binary maven artifacts is also used for the +source dist. + +You can both generate the source distribution and then sign it using + +``` +sbt signedSourceDistGenerate +``` + +`signedSourceDistGenerate` will re-use the same settings provided by sbt-pgp +so if you need to configure it follow the instructions at https://github.com/sbt/sbt-pgp#usage. diff --git a/build.sbt b/build.sbt index 1ab12f8..6df95e5 100644 --- a/build.sbt +++ b/build.sbt @@ -20,6 +20,8 @@ libraryDependencies ++= Seq( "org.scalatest" %% "scalatest" % "3.2.17" % Test ) +addSbtPlugin("com.github.sbt" % "sbt-pgp" % "2.2.1") + homepage := Some(url("https://github.com/pjfanning/sbt-source-dist")) licenses := Seq("APL2" -> url("https://www.apache.org/licenses/LICENSE-2.0.txt")) @@ -44,6 +46,13 @@ ThisBuild / githubWorkflowBuild := Seq( WorkflowStep.Sbt(name = Some("Build project"), commands = List("test", "scripted")) ) +ThisBuild / githubWorkflowBuildPreamble := Seq( + WorkflowStep.Run( + commands = List("gpg --import test-key.gpg"), + name = Some("Setup key") + ) +) + ThisBuild / githubWorkflowPublish := Seq( WorkflowStep.Sbt( List("ci-release"), diff --git a/src/main/scala/com/github/pjfanning/sourcedist/GeneratedDist.scala b/src/main/scala/com/github/pjfanning/sourcedist/GeneratedDist.scala new file mode 100644 index 0000000..7f80a3c --- /dev/null +++ b/src/main/scala/com/github/pjfanning/sourcedist/GeneratedDist.scala @@ -0,0 +1,8 @@ +package com.github.pjfanning.sourcedist + +import sbt.File + +final case class GeneratedDist(dist: File, checksum: File) { + def toSignedGeneratedDist(detachedSignature: File): SignedGeneratedDist = + SignedGeneratedDist(dist, checksum, detachedSignature) +} diff --git a/src/main/scala/com/github/pjfanning/sourcedist/ShaUtils.scala b/src/main/scala/com/github/pjfanning/sourcedist/ShaUtils.scala index 042b6f9..e6d490b 100644 --- a/src/main/scala/com/github/pjfanning/sourcedist/ShaUtils.scala +++ b/src/main/scala/com/github/pjfanning/sourcedist/ShaUtils.scala @@ -3,22 +3,27 @@ package com.github.pjfanning.sourcedist import java.io.{File, FileInputStream, FileOutputStream, InputStream, OutputStreamWriter} import java.nio.charset.StandardCharsets import java.security.{DigestInputStream, MessageDigest} -import scala.util.Using +import scala.util.{Failure, Success, Using} object ShaUtils { - def writeShaDigest(file: File, keySize: Int): Unit = { + def writeShaDigest(file: File, keySize: Int): File = { val digester = MessageDigest.getInstance(s"SHA-$keySize") Using(new FileInputStream(file)) { fileStream => Using(new DigestInputStream(fileStream, digester)) { digestStream => digestStream.on(true) readStream(digestStream) - val hexDigest = convertBytesToHexadecimal(digester.digest()) - Using(new FileOutputStream(s"${file.getAbsolutePath}.sha$keySize")) { fos => + val hexDigest = convertBytesToHexadecimal(digester.digest()) + val outputFileName = new File(s"${file.getAbsolutePath}.sha$keySize") + Using(new FileOutputStream(outputFileName)) { fos => Using(new OutputStreamWriter(fos, StandardCharsets.UTF_8)) { writer => writer.append(s"$hexDigest ${file.getName}") } } + outputFileName } + }.flatten match { + case Failure(exception) => throw exception + case Success(value) => value } } diff --git a/src/main/scala/com/github/pjfanning/sourcedist/SignedGeneratedDist.scala b/src/main/scala/com/github/pjfanning/sourcedist/SignedGeneratedDist.scala new file mode 100644 index 0000000..ed61cc8 --- /dev/null +++ b/src/main/scala/com/github/pjfanning/sourcedist/SignedGeneratedDist.scala @@ -0,0 +1,5 @@ +package com.github.pjfanning.sourcedist + +import sbt.File + +final case class SignedGeneratedDist(dist: File, checksum: File, detachedSignature: File) diff --git a/src/main/scala/com/github/pjfanning/sourcedist/SourceDistGenerate.scala b/src/main/scala/com/github/pjfanning/sourcedist/SourceDistGenerate.scala index ba3f131..0984d06 100644 --- a/src/main/scala/com/github/pjfanning/sourcedist/SourceDistGenerate.scala +++ b/src/main/scala/com/github/pjfanning/sourcedist/SourceDistGenerate.scala @@ -14,7 +14,7 @@ private[sourcedist] object SourceDistGenerate { suffix: String, incubating: Boolean, logger: ManagedLogger - ): Unit = { + ): GeneratedDist = { val baseDir = new File(homeDir) val gitState = new GitState(baseDir) val files = if (gitState.isUnderGitControl) { @@ -42,8 +42,7 @@ private[sourcedist] object SourceDistGenerate { } else logger.info(s"Creating tar archive at ${toTgzFile.getPath}") TarUtils.tgzFiles(toTgzFile, files, homeDir) - - ShaUtils.writeShaDigest(toTgzFile, 512) + GeneratedDist(toTgzFile, ShaUtils.writeShaDigest(toTgzFile, 512)) } private def listLocalFiles(baseDir: File): Seq[File] = { diff --git a/src/main/scala/com/github/pjfanning/sourcedist/SourceDistKeys.scala b/src/main/scala/com/github/pjfanning/sourcedist/SourceDistKeys.scala index 1f5507d..815ba64 100644 --- a/src/main/scala/com/github/pjfanning/sourcedist/SourceDistKeys.scala +++ b/src/main/scala/com/github/pjfanning/sourcedist/SourceDistKeys.scala @@ -1,6 +1,6 @@ package com.github.pjfanning.sourcedist -import sbt.{File, SettingKey, TaskKey, settingKey, taskKey} +import sbt.{Artifact, File, SettingKey, TaskKey, settingKey, taskKey} trait SourceDistKeys { lazy val sourceDistHomeDir: SettingKey[File] = @@ -19,5 +19,9 @@ trait SourceDistKeys { lazy val sourceDistSuffix: SettingKey[String] = settingKey[String]( "The suffix to be used in the output archive file names, defaults to today's date in YYMMDD format" ) - lazy val sourceDistGenerate: TaskKey[Unit] = taskKey[Unit]("Generate the source distribution packages") + lazy val signedSourceDistGenerate: TaskKey[Option[SignedGeneratedDist]] = taskKey[Option[SignedGeneratedDist]]( + "Signs the source distribution and provides a mapping of files their respective detached signatures" + ) + lazy val sourceDistGenerate: TaskKey[GeneratedDist] = + taskKey[GeneratedDist]("Generate the source distribution packages") } diff --git a/src/main/scala/com/github/pjfanning/sourcedist/SourceDistPlugin.scala b/src/main/scala/com/github/pjfanning/sourcedist/SourceDistPlugin.scala index bd23c49..60a9423 100644 --- a/src/main/scala/com/github/pjfanning/sourcedist/SourceDistPlugin.scala +++ b/src/main/scala/com/github/pjfanning/sourcedist/SourceDistPlugin.scala @@ -1,8 +1,9 @@ package com.github.pjfanning.sourcedist import sbt._ -import Keys._ -import sbt.{AutoPlugin, Setting} +import sbt.Keys._ +import com.jsuereth.sbtpgp.PgpKeys.pgpSigner +import com.jsuereth.sbtpgp.{SbtPgp, gpgExtension} import java.time.LocalDate import java.time.format.DateTimeFormatter @@ -19,6 +20,20 @@ object SourceDistPlugin extends AutoPlugin { LocalRootProject / sourceDistName := (LocalRootProject / name).value, LocalRootProject / sourceDistSuffix := LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE), LocalRootProject / sourceDistIncubating := false, + LocalRootProject / signedSourceDistGenerate := { + val sourceDistGenerated = (LocalRootProject / sourceDistGenerate).value + val r = pgpSigner.value + val skipZ = (pgpSigner / skip).value + val s = streams.value + if (!skipZ) { + Some( + sourceDistGenerated.toSignedGeneratedDist( + r.sign(sourceDistGenerated.dist, new File(sourceDistGenerated.dist + gpgExtension), s) + ) + ) + } else + None + }, LocalRootProject / sourceDistGenerate := SourceDistGenerate.generateSourceDists( homeDir = (LocalRootProject / sourceDistHomeDir).value.getAbsolutePath, prefix = (LocalRootProject / sourceDistName).value, @@ -30,6 +45,8 @@ object SourceDistPlugin extends AutoPlugin { ) ) + override def requires = SbtPgp + override lazy val buildSettings: Seq[Setting[_]] = sourceDistSettings override def trigger = allRequirements diff --git a/src/sbt-test/sbt-source-dist/signing/build.sbt b/src/sbt-test/sbt-source-dist/signing/build.sbt new file mode 100644 index 0000000..17a09c0 --- /dev/null +++ b/src/sbt-test/sbt-source-dist/signing/build.sbt @@ -0,0 +1,5 @@ +scalaVersion := "2.13.10" +version := "0.1.9" +name := "apache-pekko" +sourceDistSuffix := "20230331" +pgpKeyRing := Some(baseDirectory.value / "pubring.pgp") diff --git a/src/sbt-test/sbt-source-dist/signing/project/plugins.sbt b/src/sbt-test/sbt-source-dist/signing/project/plugins.sbt new file mode 100644 index 0000000..79b0c78 --- /dev/null +++ b/src/sbt-test/sbt-source-dist/signing/project/plugins.sbt @@ -0,0 +1,5 @@ +sys.props.get("plugin.version") match { + case Some(x) => addSbtPlugin("com.github.pjfanning" % "sbt-source-dist" % x) + case _ => sys.error("""|The system property 'plugin.version' is not defined. + |Specify this property using the scriptedLaunchOpts -D.""".stripMargin) +} diff --git a/src/sbt-test/sbt-source-dist/signing/pubring.pgp b/src/sbt-test/sbt-source-dist/signing/pubring.pgp new file mode 100644 index 0000000000000000000000000000000000000000..ee42eb132ceffad19b25c20b8f75929e5a4ea181 GIT binary patch literal 2265 zcmajfcRUmh1IO{ZbLXt$viB%Q_St)!9ZqIhXFDpJ5;`jDP=t)^eLqSL8QCi14&@G! z5jrbEMrA+a^?P2=^WXFD=k@*b`+DbrE`XgL(3}HV08JjtawRu8>r%|#O7`BZouXw_ z8ZZpJMGuGn5~13;BC7JMBou#KLJ}wtzBS<)+o(SMg?MSpCH9{hb4!{6xRP3t9Y+MN zH2b2Z_2cA1F$WjN2pq)PbgPk3E3n2!@lAh>l+aV1XeyBQG%aI!u;RRuc6gUK&1lX` zB9GiykH2PF&c4qbD!bl8KC-Ytc>B+j2PZ zC>p6@8o!J#VJjBh<6g`zDhuIB&j8wmZPZ~$7H>!I)sEoRZESNw8{r*^ntdo!3e8c`@BL4&(SpsE%H zU%gk0iqOOFU(k8R!=@d>9yzC{q;`nJI^R-ms8HE(aj`FP?Vtq+%;Y?5@5_jMHs+dU z$(;Dc#D9yQ#;-oPGMBI5(yDU~wZ$G-`_byevpspZRg^9ZbBvkva;P7d^l6R17m3kM zp1&9|OWLAw4Uaq8fUj4k$52yrRH@7Nv}LcaNYg4)yzv9hM?Du9x-zs3&U1A4wcE5B z!#Br^6>rjpoisI-&)Xel%gr@|kJBIk7!Uw#k`DCFJ;#T6P9{Iz|wf14IJ^2>?Nyu>b~omj8^^vCO9&^YUJX#T_cxgk6^z0y@Mf2@{F+ zB0Zn6cXZIZ+&BUsaBV7ouY~OWmUClQn9R=0udS658fBM}_C&(ea<>kpkWgi}_i_7* z8E)*Vowm2$?dH->=3&1k&r{8C<@(nwPr8F~m`>4}RF>keV*?zrQ(0tY=L9pdncxTZ z84syu;}SvyXBPek##4Asg?c{hVI#5LIL4DDHAe~*a_Nzv=C{RP_3^)+{&eC&64dJQ z%ZHIF&}yz`$Xb>`CW5=MM-AA}p>+6bm5V-%<=rN(T*sWMI3`wkrpC$C^r!T;xc@xb zXsxc8q&GMj{upf9Jzrm>KN|Y*1yMAgXOj(<={y%i=qJCB$*1Z4z2;xHVCQO~u_X;1Yjh9*wWye5=^8o<^o zNQLO;5Tg@|E={+i{f5#ru#vRxxuV+ciiFp_8#-0jw-ctPlKnQynVdO;m^t`;3+HngA% z@5Qt!5WH-W+6Dp;;~NDC6-s@BB7;lI3966-DsF*6rR3dDDRPa3#EP}z+9BUAr5$Q` zO}}R;TTddT{VtdlR_cQqO3Y2Q{nw+vMnBX*Thz^hlh(-BEa;*b)#yCo60jgsgcaFl z;7t@;)#Y8)-tknF7jkE3LHWMFjf3A|nXSxvN|k6sWvI+h-=p}-nGx0!@aDcMy<&_w zNi$ZY41Bjon?ZW~dkjTAR%PAjkr$TK`Te^5w$4-D z6`I&mM4V7&jNjTLv?yDX*U+;%olnofOg~RWtQ5dOrB3XBBtNU&zEbO6BqMoC#~^Qq z;9EF(;W-av_NW-}UGpjDvx5FG&_;%tY#gM6__U-t}5NehG z`b=ZL6RN~_HE(E|^WPrfK+0gQe*z=)e}G~7Uq~|JSpaoM6HQ(95@V*oc1Fa5SK#(8TV(1_=q{5AV4uE~&Z zPT<}}H{Z?xAMbAkT@C47E1s{gPoWVKch}EC{NFBG(C<0d4IIC=*q^fc^4f5f%kk^< zNt~2!y)@q5R zmFbNCp>Hdg{YfnK;9a^s!XscS{0T%ECn)(CsD29%Ph;$NAoizxJYZFs$Z1)hmYH}z zp_ZEdWiu~%0DZ?nR_oPmY-vscdNJ6kiTLGD+{D9w>XYDUdvYN z<-`-;01bUMD`Y literal 0 HcmV?d00001 diff --git a/src/sbt-test/sbt-source-dist/signing/src/main/scala/hello.scala b/src/sbt-test/sbt-source-dist/signing/src/main/scala/hello.scala new file mode 100644 index 0000000..dffb3b8 --- /dev/null +++ b/src/sbt-test/sbt-source-dist/signing/src/main/scala/hello.scala @@ -0,0 +1,3 @@ +object Main extends App { + println("hello") +} diff --git a/src/sbt-test/sbt-source-dist/signing/test b/src/sbt-test/sbt-source-dist/signing/test new file mode 100644 index 0000000..e882a0d --- /dev/null +++ b/src/sbt-test/sbt-source-dist/signing/test @@ -0,0 +1,6 @@ +> show pgpPassphrase +> show pgpSigner +> signedSourceDistGenerate +$ exists target/dist/apache-pekko-0.1.9-src-20230331.tgz +$ exists target/dist/apache-pekko-0.1.9-src-20230331.tgz.sha512 +$ exists target/dist/apache-pekko-0.1.9-src-20230331.tgz.asc diff --git a/test-key.gpg b/test-key.gpg new file mode 100644 index 0000000..a8ab159 --- /dev/null +++ b/test-key.gpg @@ -0,0 +1,105 @@ +-----BEGIN PGP PRIVATE KEY BLOCK----- + +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 +=N0Eh +-----END PGP PRIVATE KEY BLOCK-----