diff --git a/.github/scripts/dnd-sbt b/.github/scripts/dnd-sbt new file mode 100755 index 0000000..682d612 --- /dev/null +++ b/.github/scripts/dnd-sbt @@ -0,0 +1,17 @@ +#!/bin/bash -exu +SCRIPT_HOME="$(cd "$(dirname "$0")"; pwd)" +COMPONENT_HOME="$(cd "${SCRIPT_HOME}/../.."; pwd)" + +cd "${COMPONENT_HOME}" + +tmp_dir=$(mktemp -d -t dnd-sbt-XXXXXXXXXX) +cp -R . ${tmp_dir} + +docker run \ + -v /var/run/docker.sock:/var/run/docker.sock \ + -v /usr/bin/docker:/usr/bin/docker \ + -v ${HOME}/.docker/config.json:/root/.docker/config.json \ + -v ${tmp_dir}:/root/ \ + -e GITHUB_TOKEN=${GITHUB_TOKEN} \ + sbtscala/scala-sbt:eclipse-temurin-jammy-21.0.2_13_1.9.9_2.12.19 \ + /bin/bash -c "git config --global --add safe.directory /root; sbt ${1}" diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 1f0e8f9..ff9e8bc 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -1,51 +1,36 @@ name: CI - on: pull_request: paths: - - '**/*.scala' - - '**/*.sbt' - - '.scalafmt.conf' - - 'project/**' - - '.github/workflows/ci.yaml' + - .github/workflows/ci.yaml + - .sbtopts + - build.sbt + - .scalafmt.conf + - project/** + - src/** + +defaults: + run: + shell: bash env: - SBT_OPTS: "-Xmx2G -XX:+UseG1GC -Xss2M" GITHUB_TOKEN: ${{ secrets.READ_PACKAGES }} jobs: - lint: - runs-on: ubuntu-latest - + code-check: + runs-on: self-hosted + container: + image: sbtscala/scala-sbt:eclipse-temurin-jammy-21.0.2_13_1.9.9_2.12.19 + options: --user 1001:1001 steps: - uses: actions/checkout@v4 - - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '21' - - uses: sbt/setup-sbt@v1 - with: - sbt-runner-version: 1.9.9 - - run: sbt scalafmtCheckAll - run: sbt headerCheckAll - + - run: sbt scalafmtCheckAll test: - runs-on: ubuntu-latest + runs-on: self-hosted + container: + image: sbtscala/scala-sbt:eclipse-temurin-jammy-21.0.2_13_1.9.9_2.12.19 + options: --user 1001:1001 steps: - uses: actions/checkout@v4 - - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '21' - - uses: sbt/setup-sbt@v1 - with: - sbt-runner-version: 1.9.9 - - name: Run tests - run: sbt clean test - - - name: Upload test results - if: always() - uses: actions/upload-artifact@v4 - with: - name: test-results - path: target/test-results + - run: sbt clean compile Test/compile diff --git a/.github/workflows/docker-ci.yaml b/.github/workflows/docker-ci.yaml index 741f2b5..db7be94 100644 --- a/.github/workflows/docker-ci.yaml +++ b/.github/workflows/docker-ci.yaml @@ -1,25 +1,70 @@ name: Docker CI - on: pull_request: paths: - .github/workflows/docker-ci.yaml + - .github/scripts/** - build.sbt + - src/** + +env: + GITHUB_TOKEN: ${{ secrets.READ_PACKAGES }} jobs: - docker-build: - runs-on: ubuntu-latest + build-and-test: + name: Build & Test + runs-on: self-hosted steps: - uses: actions/checkout@v4 - - uses: docker/setup-buildx-action@v3 - - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '21' - - uses: sbt/setup-sbt@v1 - with: - sbt-runner-version: 1.9.9 + - name: Build Docker image - env: - GITHUB_TOKEN: ${{ secrets.READ_PACKAGES }} - run: sbt docker/Docker/publishLocal + run: | + .github/scripts/dnd-sbt Docker/publishLocal + IMAGE_NAME=$(.github/scripts/dnd-sbt printDockerImageName | grep DOCKER_IMAGE | cut -d= -f2) + echo "IMAGE=${IMAGE_NAME}" >> $GITHUB_ENV + + - name: Test image - run container + run: | + CONTAINER_ID=$(docker run -d -p 50051 ${IMAGE}) + echo "CONTAINER_ID=${CONTAINER_ID}" >> $GITHUB_ENV + HOST_PORT=$(docker port ${CONTAINER_ID} 50051 | cut -d':' -f2) + echo "HOST_PORT=${HOST_PORT}" >> $GITHUB_ENV + sleep 15 + + - name: Test image - verify service is running + run: | + nc -z localhost ${HOST_PORT} + if [ $? -ne 0 ]; then + echo "Service check failed!" + exit 1 + fi + + - name: Cleanup container + if: always() + run: | + if [ ! -z "${CONTAINER_ID}" ]; then + docker stop ${CONTAINER_ID} + docker rm ${CONTAINER_ID} + fi + + security-scan: + name: Security Scan + runs-on: self-hosted + steps: + - uses: actions/checkout@v4 + + - name: Build Docker image + run: | + .github/scripts/dnd-sbt Docker/publishLocal + IMAGE_NAME=$(.github/scripts/dnd-sbt printDockerImageName | grep DOCKER_IMAGE | cut -d= -f2) + echo "IMAGE=${IMAGE_NAME}" >> $GITHUB_ENV + + - name: Run Trivy vulnerability scanner + uses: aquasecurity/trivy-action@master + with: + image-ref: ${{ env.IMAGE }} + format: 'table' + exit-code: '1' + ignore-unfixed: true + vuln-type: 'os,library' + severity: 'CRITICAL,HIGH' diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml index 1ddee5c..629a522 100644 --- a/.github/workflows/publish.yaml +++ b/.github/workflows/publish.yaml @@ -11,34 +11,24 @@ env: jobs: publish-jars: - runs-on: ubuntu-latest + runs-on: self-hosted + container: + image: sbtscala/scala-sbt:eclipse-temurin-jammy-21.0.2_13_1.9.9_2.12.19 + options: --user 1001:1001 steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '21' - - uses: sbt/setup-sbt@v1 - with: - sbt-runner-version: 1.9.9 - - name: publish + - name: sbt publish run: sbt clean publish publish-docker-image: runs-on: self-hosted + outputs: + should_trigger_deploy: ${{ steps.should_trigger_deploy.outputs.should_trigger_deploy }} steps: - uses: actions/checkout@v4 with: fetch-depth: 0 - - uses: actions/setup-java@v4 - with: - distribution: 'temurin' - java-version: '21' - - uses: sbt/setup-sbt@v1 - with: - sbt-runner-version: 1.9.9 - - uses: docker/setup-buildx-action@v3 - uses: docker/login-action@v3 with: registry: ghcr.io @@ -46,11 +36,20 @@ jobs: password: ${{ secrets.WRITE_PACKAGES }} logout: false - name: publish docker images - run: sbt docker/Docker/publish + run: .github/scripts/dnd-sbt Docker/publish + - name: set should_trigger_deploy + id: should_trigger_deploy + shell: bash + run: | + pattern='^refs/tags/v[0-9]+\.0\.0$' + echo "should_trigger_deploy=$([[ "$GITHUB_REF" =~ $pattern ]] && echo false || echo true)" >> $GITHUB_OUTPUT gh-release: needs: [publish-jars, publish-docker-image] runs-on: self-hosted steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 - uses: softprops/action-gh-release@v2 with: token: ${{ secrets.RAW_CI_PAT }} @@ -58,3 +57,25 @@ jobs: draft: false prerelease: ${{ contains(github.ref_name, '-') }} tag_name: ${{ github.ref_name }} + trigger-deploy: + needs: publish-docker-image + if: needs.publish-docker-image.outputs.should_trigger_deploy == 'true' + runs-on: ubuntu-latest + steps: + - name: tag without 'v' prefix + id: extract_tag + run: echo "version=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + - name: trigger mvp-deployer workflow + uses: peter-evans/repository-dispatch@v3 + with: + token: ${{ secrets.RAW_CI_PAT }} + repository: raw-labs/mvp-deployer + event-type: das-salesforce-integration-cd + client-payload: |- + { + "aws_region": "eu-west-1", + "raw_version": "${{ steps.extract_tag.outputs.version }}", + "target_env": "integration", + "loaded_vars": "integration", + "deployer_version": "latest" + } diff --git a/README.md b/README.md index 7c5bd09..5776daf 100644 --- a/README.md +++ b/README.md @@ -11,12 +11,30 @@ ## How to use -First you need to build the project: +### Prerequisites + +You need to have [sbt](https://www.scala-sbt.org/) installed to build the project. + +You can install sbt using [sdkman](https://sdkman.io/): +```bash +$ sdk install sbt +``` + +### Running the server + +You can run the server with the following command: ```bash -$ sbt "project docker" "docker:publishLocal" +$ sbt run ``` -This will create a docker image with the name `das-excel`. +### Docker + +To run the server in a docker container you need to follow these steps: + +First, you need to build the project: +```bash +$ sbt "docker:publishLocal" +``` Then you can run the image with the following command: ```bash diff --git a/build.sbt b/build.sbt index e2d7b2b..c92833c 100644 --- a/build.sbt +++ b/build.sbt @@ -1,92 +1,12 @@ -import java.nio.file.Paths - -import sbt.* -import sbt.Keys.* - -import com.typesafe.sbt.packager.docker.{Cmd, LayeredMapping} - -ThisBuild / credentials += Credentials( - "GitHub Package Registry", - "maven.pkg.github.com", - "raw-labs", - sys.env.getOrElse("GITHUB_TOKEN", "")) - -lazy val commonSettings = Seq( - homepage := Some(url("https://www.raw-labs.com/")), - organization := "com.raw-labs", - organizationName := "RAW Labs SA", - organizationHomepage := Some(url("https://www.raw-labs.com/")), - // Use cached resolution of dependencies - // http://www.scala-sbt.org/0.13/docs/Cached-Resolution.html - updateOptions := updateOptions.in(Global).value.withCachedResolution(true), - resolvers += "RAW Labs GitHub Packages" at "https://maven.pkg.github.com/raw-labs/_") - -lazy val buildSettings = Seq( - scalaVersion := "2.13.15", - javacOptions ++= Seq("-source", "21", "-target", "21"), - scalacOptions ++= Seq( - "-feature", - "-unchecked", - "-deprecation", - "-Xlint:-stars-align,_", - "-Ywarn-dead-code", - "-Ywarn-macros:after", // Fix for false warning of unused implicit arguments in traits/interfaces. - "-Ypatmat-exhaust-depth", - "160")) - -lazy val compileSettings = Seq( - Compile / doc / sources := Seq.empty, - Compile / packageDoc / mappings := Seq(), - Compile / packageSrc / publishArtifact := true, - Compile / packageDoc / publishArtifact := false, - Compile / packageBin / packageOptions += Package.ManifestAttributes( - "Automatic-Module-Name" -> name.value.replace('-', '.')), - // Ensure Java annotations get compiled first, so that they are accessible from Scala. - compileOrder := CompileOrder.JavaThenScala) - -lazy val testSettings = Seq( - // Ensuring tests are run in a forked JVM for isolation. - Test / fork := true, - // Disabling parallel execution of tests. - // Test / parallelExecution := false, - // Pass system properties starting with "raw." to the forked JVMs. - Test / javaOptions ++= { - import scala.collection.JavaConverters.* - val props = System.getProperties - props - .stringPropertyNames() - .asScala - .filter(_.startsWith("raw.")) - .map(key => s"-D$key=${props.getProperty(key)}") - .toSeq - }, - // Set up heap dump options for out-of-memory errors. - Test / javaOptions ++= Seq( - "-XX:+HeapDumpOnOutOfMemoryError", - s"-XX:HeapDumpPath=${Paths.get(sys.env.getOrElse("SBT_FORK_OUTPUT_DIR", "target/test-results")).resolve("heap-dumps")}"), - Test / publishArtifact := true) - -val isCI = sys.env.getOrElse("CI", "false").toBoolean - -lazy val publishSettings = Seq( - versionScheme := Some("early-semver"), - publish / skip := false, - publishMavenStyle := true, - publishTo := Some("GitHub raw-labs Apache Maven Packages" at "https://maven.pkg.github.com/raw-labs/das-sqlite"), - publishConfiguration := publishConfiguration.value.withOverwrite(isCI)) - -lazy val strictBuildSettings = - commonSettings ++ compileSettings ++ buildSettings ++ testSettings ++ Seq(scalacOptions ++= Seq("-Xfatal-warnings")) +import SbtDASPlugin.autoImport.* lazy val root = (project in file(".")) + .enablePlugins(SbtDASPlugin) .settings( - name := "das-sqlite", - strictBuildSettings, - publishSettings, + repoNameSetting := "das-sqlite", libraryDependencies ++= Seq( // DAS - "com.raw-labs" %% "das-server-scala" % "0.5.0" % "compile->compile;test->test", - "com.raw-labs" %% "protocol-das" % "1.0.0" % "compile->compile;test->test", + "com.raw-labs" %% "das-server-scala" % "0.6.0" % "compile->compile;test->test", // Sqlite "org.xerial" % "sqlite-jdbc" % "3.49.1.0", // Jackson (for JSON handling) @@ -97,93 +17,7 @@ lazy val root = (project in file(".")) "org.mockito" % "mockito-core" % "5.12.0" % Test, "org.scalatestplus" %% "mockito-5-12" % "3.2.19.0" % Test, // ScalaTest / containers - "com.dimafeng" %% "testcontainers-scala-scalatest" % "0.41.8" % Test)) - -val amzn_jdk_version = "21.0.4.7-1" -val amzn_corretto_bin = s"java-21-amazon-corretto-jdk_${amzn_jdk_version}_amd64.deb" -val amzn_corretto_bin_dl_url = s"https://corretto.aws/downloads/resources/${amzn_jdk_version.replace('-', '.')}" - -lazy val dockerSettings = strictBuildSettings ++ Seq( - name := "das-sqlite-server", - dockerBaseImage := s"--platform=amd64 debian:bookworm-slim", - dockerLabels ++= Map( - "vendor" -> "RAW Labs SA", - "product" -> "das-sqlite-server", - "image-type" -> "final", - "org.opencontainers.image.source" -> "https://github.com/raw-labs/das-sqlite"), - Docker / daemonUser := "raw", - dockerExposedVolumes := Seq("/var/log/raw"), - dockerExposedPorts := Seq(50051), - dockerEnvVars := Map("PATH" -> s"${(Docker / defaultLinuxInstallLocation).value}/bin:$$PATH"), - // We remove the automatic switch to USER 1001:0. - // We we want to run as root to install the JDK, also later we will switch to a non-root user. - dockerCommands := dockerCommands.value.filterNot { - case Cmd("USER", args @ _*) => args.contains("1001:0") - case cmd => false - }, - dockerCommands ++= Seq( - Cmd( - "RUN", - s"""set -eux \\ - && apt-get update \\ - && apt-get install -y --no-install-recommends \\ - curl wget ca-certificates gnupg software-properties-common fontconfig java-common \\ - && wget $amzn_corretto_bin_dl_url/$amzn_corretto_bin \\ - && dpkg --install $amzn_corretto_bin \\ - && rm -f $amzn_corretto_bin \\ - && apt-get purge -y --auto-remove -o APT::AutoRemove::RecommendsImportant=false \\ - wget gnupg software-properties-common"""), - Cmd("USER", "raw")), - dockerEnvVars += "LANG" -> "C.UTF-8", - dockerEnvVars += "JAVA_HOME" -> "/usr/lib/jvm/java-21-amazon-corretto", - Compile / doc / sources := Seq.empty, // Do not generate scaladocs - // Skip docs to speed up build - Compile / packageDoc / mappings := Seq(), - updateOptions := updateOptions.value.withLatestSnapshots(true), - Linux / linuxPackageMappings += packageTemplateMapping(s"/var/lib/${packageName.value}")(), - bashScriptDefines := { - val ClasspathPattern = "declare -r app_classpath=\"(.*)\"\n".r - bashScriptDefines.value.map { - case ClasspathPattern(classpath) => s""" - |declare -r app_classpath="$${app_home}/../conf:$classpath" - |""".stripMargin - case _ @entry => entry - } - }, - Docker / dockerLayerMappings := (Docker / dockerLayerMappings).value.map { - case lm @ LayeredMapping(Some(1), file, path) => { - val fileName = java.nio.file.Paths.get(path).getFileName.toString - if (!fileName.endsWith(".jar")) { - // If it is not a jar, put it on the top layer. Configuration files and other small files. - LayeredMapping(Some(2), file, path) - } else if (fileName.startsWith("com.raw-labs") && fileName.endsWith(".jar")) { - // If it is one of our jars, also top layer. These will change often. - LayeredMapping(Some(2), file, path) - } else { - // Otherwise it is a 3rd party library, which only changes when we change dependencies, so leave it in layer 1 - lm - } - } - case lm @ _ => lm - }, - Compile / mainClass := Some("com.rawlabs.das.server.DASServer"), - Docker / dockerAutoremoveMultiStageIntermediateImages := false, - dockerAlias := dockerAlias.value.withTag(Option(version.value.replace("+", "-"))), - dockerAliases := { - val devRegistry = sys.env.getOrElse("DEV_REGISTRY", "ghcr.io/raw-labs/das-sqlite") - val releaseRegistry = sys.env.get("RELEASE_DOCKER_REGISTRY") - val baseAlias = dockerAlias.value.withRegistryHost(Some(devRegistry)) - - releaseRegistry match { - case Some(releaseReg) => Seq(baseAlias, dockerAlias.value.withRegistryHost(Some(releaseReg))) - case None => Seq(baseAlias) - } - }) - -lazy val docker = (project in file("docker")) - .dependsOn(root % "compile->compile;test->test") - .enablePlugins(JavaAppPackaging, DockerPlugin) - .settings( - strictBuildSettings, - dockerSettings, - libraryDependencies += "com.raw-labs" %% "das-server-scala" % "0.5.0" % "compile->compile;test->test") + "com.dimafeng" %% "testcontainers-scala-scalatest" % "0.41.8" % Test), + dependencyOverrides ++= Seq( + "io.netty" % "netty-handler" % "4.1.118.Final" + )) diff --git a/project/SbtDASPlugin.scala b/project/SbtDASPlugin.scala new file mode 100644 index 0000000..3fb4ab7 --- /dev/null +++ b/project/SbtDASPlugin.scala @@ -0,0 +1,157 @@ +import sbt.* +import sbt.Keys.* + +import com.typesafe.sbt.SbtNativePackager.{Docker, Linux} +import com.typesafe.sbt.packager.Keys.* +import com.typesafe.sbt.packager.archetypes.JavaAppPackaging +import com.typesafe.sbt.packager.docker.DockerPlugin.autoImport.dockerLayerMappings +import com.typesafe.sbt.packager.docker.{DockerPlugin, LayeredMapping} +import com.typesafe.sbt.packager.linux.Mapper.packageTemplateMapping + +object SbtDASPlugin extends AutoPlugin { + + // We require Docker + JavaAppPackaging so projects automatically get them. + override def requires = DockerPlugin && JavaAppPackaging + override def trigger = allRequirements + + object autoImport { + // The only project-specific setting that we want devs to override in build.sbt + val repoNameSetting = settingKey[String]("Repository/project name for Docker, publishing, etc.") + } + import autoImport._ + + // Hardcoded organization name, username, etc., since everything else is "common" + private val orgUsername = "raw-labs" + private val orgName = "com.raw-labs" + + // We inline these... + override def projectSettings: Seq[Setting[_]] = Seq( + // Add GitHub credentials + credentials += Credentials( + "GitHub Package Registry", + "maven.pkg.github.com", + orgUsername, + sys.env.getOrElse("GITHUB_TOKEN", "")), + + // Basic metadata + homepage := Some(url("https://www.raw-labs.com/")), + organization := orgName, + organizationName := "RAW Labs SA", + organizationHomepage := Some(url("https://www.raw-labs.com/")), + + // Use cached resolution + updateOptions := updateOptions.in(Global).value.withCachedResolution(true), + + // Add local Maven + RAW Labs Package Registry resolvers + resolvers ++= Seq(Resolver.mavenLocal, "RAW Labs GitHub Packages" at s"https://maven.pkg.github.com/raw-labs/_"), + + // We use Scala 2.13 + scalaVersion := "2.13.15", + + // Compile settings + Compile / doc / sources := Seq.empty, + Compile / packageDoc / mappings := Seq(), + Compile / packageSrc / publishArtifact := true, + Compile / packageDoc / publishArtifact := false, + Compile / packageBin / packageOptions += + Package.ManifestAttributes("Automatic-Module-Name" -> name.value.replace('-', '.')), + compileOrder := CompileOrder.JavaThenScala, + Compile / run / fork := true, + Compile / mainClass := Some("com.rawlabs.das.server.DASServer"), + + // Test settings + Test / fork := true, + Test / publishArtifact := true, + + // Make all warnings fatal + scalacOptions ++= Seq("-Xfatal-warnings"), + + // Publish settings + versionScheme := Some("early-semver"), + publish / skip := false, + publishMavenStyle := true, + publishTo := { + val repoName = repoNameSetting.value + Some(s"GitHub $orgUsername Apache Maven Packages" at s"https://maven.pkg.github.com/$orgUsername/$repoName") + }, + // Overwrite artifacts in CI + publishConfiguration := { + val isCI = sys.env.getOrElse("CI", "false").toBoolean + publishConfiguration.value.withOverwrite(isCI) + }, + + // Let the `name` of this project follow our custom repoNameSetting + name := repoNameSetting.value, + + // Docker settings + Docker / packageName := s"${repoNameSetting.value}-server", + dockerBaseImage := "eclipse-temurin:21-jre", + dockerLabels ++= Map( + "vendor" -> "RAW Labs SA", + "product" -> s"${repoNameSetting.value}-server", + "image-type" -> "final", + "org.opencontainers.image.source" -> s"https://github.com/$orgUsername/${repoNameSetting.value}"), + Docker / daemonUser := "raw", + Docker / daemonUserUid := Some("1001"), + Docker / daemonGroup := "raw", + Docker / daemonGroupGid := Some("1001"), + dockerExposedVolumes := Seq("/var/log/raw"), + dockerExposedPorts := Seq(50051), + dockerEnvVars := Map("PATH" -> s"${(Docker / defaultLinuxInstallLocation).value}/bin:$$PATH", "LANG" -> "C.UTF-8"), + updateOptions := updateOptions.value.withLatestSnapshots(true), + + // Make /var/lib/ be an explicit Linux package mapping + Linux / linuxPackageMappings += packageTemplateMapping(s"/var/lib/${(Docker / packageName).value}")(), + + // Modify the bash script so we can prepend our conf folder + bashScriptDefines := { + val pattern = "declare -r app_classpath=\"(.*)\"\n".r + bashScriptDefines.value.map { + case pattern(cp) => + s""" + |declare -r app_classpath="$${app_home}/../conf:$cp" + |""".stripMargin + case other => other + } + }, + + // Put certain jars on the top layer for Docker layering + Docker / dockerLayerMappings := { + (Docker / dockerLayerMappings).value.map { + case lm @ LayeredMapping(Some(1), file, path) => + val fileName = java.nio.file.Paths.get(path).getFileName.toString + if (!fileName.endsWith(".jar")) { + // If it is not a jar, put it on the top layer + LayeredMapping(Some(2), file, path) + } else if (fileName.startsWith("com.raw-labs") && fileName.endsWith(".jar")) { + // Our own jars -> top layer + LayeredMapping(Some(2), file, path) + } else { + // 3rd party jars stay in layer 1 + lm + } + case lm => lm + } + }, + + // Clean up Docker version tag + Docker / version := { + val ver = version.value + ver.replaceAll("[+]", "-").replaceAll("[^\\w.-]", "-") + }, + + // Let Docker push to GHCR by default + dockerAlias := { + val devRegistry = s"ghcr.io/$orgUsername/${repoNameSetting.value}" + dockerAlias.value.withRegistryHost(Some(devRegistry)) + }, + + // Define the printDockerImageName task + printDockerImageName := { + val alias = (Docker / dockerAlias).value + println(s"DOCKER_IMAGE=$alias") + }) + + // A task key for printing the Docker name + val printDockerImageName = taskKey[Unit]("Prints the full Docker image name that will be produced") +}