From db953b9a1c48450359a4aa9c1caad78213bf816d Mon Sep 17 00:00:00 2001 From: Andrea Vibelli Date: Mon, 2 Nov 2026 15:16:07 +0100 Subject: [PATCH] Add sbt-release workflow Release an SBT project to Maven Central (Sonatype Central Portal) with sbt-release and sbt-pgp, like gradle-release.yml for Gradle: commit and tag the release version, publish the signed artifacts with sonaRelease, commit the next snapshot version, push, and create the GitHub release. A second release of the same ref is queued instead of cancelling a run midway, which could leave a version published without its tag and version commits. --- .github/workflows/sbt-release.yml | 165 ++++++++++++++++++++++++++++++ README.md | 97 ++++++++++++++++++ 2 files changed, 262 insertions(+) create mode 100644 .github/workflows/sbt-release.yml diff --git a/.github/workflows/sbt-release.yml b/.github/workflows/sbt-release.yml new file mode 100644 index 0000000..143a163 --- /dev/null +++ b/.github/workflows/sbt-release.yml @@ -0,0 +1,165 @@ +name: SBT Release to Maven Central + +on: + workflow_call: + inputs: + project_name: + description: "Name in / format. Prevents forks to run the job" + required: true + type: string + + ref_to_release: + description: "Branch to release. Default: Github default branch" + required: false + type: string + # default has to be a static string, no variables allowed + default: "" + + release_version: + description: "Version to release. Default: chosen by sbt-release (current version without -SNAPSHOT)" + required: false + type: string + default: "" + + next_version: + description: "Next snapshot version. Default: chosen by sbt-release (releaseVersionBump)" + required: false + type: string + default: "" + + additional_release_args: + description: "Extra sbt-release arguments, e.g. skip-tests" + required: false + type: string + default: "" + + java_version: + description: "The Java version to use to compile. Default: 8" + required: false + type: string + default: "8" + + resolvers: + description: "SBT resolvers to configure" + required: false + type: string + default: | + resolvers ++= Seq( + "Sonatype Snapshots" at "https://central.sonatype.com/repository/maven-snapshots", + "Artima" at "https://repo.artima.com/releases", + "MRRC" at "https://maven.repository.redhat.com/ga/" + ) + + fetch_all_commits: + description: "Whether to fetch all commits. Default: false" + required: false + type: boolean + default: false + + secrets: + SONATYPE_USERNAME: + required: true + SONATYPE_PASSWORD: + required: true + GPG_PRIVATE_KEY: + required: true + GPG_PASSPHRASE: + required: true + +# queue, don't cancel, a second release of the same ref: cancelling a run midway (e.g. after sonaRelease, before +# pushChanges) would leave a version published on Maven Central without its tag and version commits +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: false + +jobs: + release: + if: github.repository == inputs.project_name + runs-on: ubuntu-latest + + permissions: + contents: write # Allows pushing tags and commits + + env: + FINAL_REF: ${{ inputs.ref_to_release || github.event.repository.default_branch }} + + steps: + - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + ref: "${{ env.FINAL_REF }}" + fetch-depth: ${{ inputs.fetch_all_commits == true && '0' || '1' }} + persist-credentials: false + fetch-tags: true + + - name: Configure Git User + run: | + git config --global user.name "github-actions[bot]" + git config --global user.email "41898282+github-actions[bot]@users.noreply.github.com" + + - name: Set up JDK '${{ inputs.java_version }}' + uses: actions/setup-java@b843ebf9b913146b38da2eaa7aeddf76074ae1ad # v6.1.0 + with: + java-version: "${{ inputs.java_version }}" + distribution: "temurin" + cache: sbt + # imports the key into the gpg keyring, used by sbt-pgp + gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }} + + - name: Setup SBT + uses: sbt/setup-sbt@2e222825582620cc38d2a54e674f3c01b7c14f5d # v1.1.24 + + - name: Configure Resolvers and Sonatype credentials + shell: bash + env: + RESOLVERS: ${{ inputs.resolvers }} + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + run: | + mkdir -p "$HOME/.sbt/1.0" + printf '%s\n' "$RESOLVERS" > "$HOME/.sbt/1.0/resolvers.sbt" + printf '%s\n' \ + "realm=Sonatype Nexus Repository Manager" \ + "host=central.sonatype.com" \ + "user=$SONATYPE_USERNAME" \ + "password=$SONATYPE_PASSWORD" > "$HOME/.sbt/.credentials" + + - name: Set git remote to HTTPS token URL + run: git remote set-url origin "https://x-access-token:${GITHUB_TOKEN}@github.com/${{ github.repository }}.git" + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Run sbt-release + shell: bash + env: + RELEASE_VERSION: ${{ inputs.release_version }} + NEXT_VERSION: ${{ inputs.next_version }} + ADDITIONAL_RELEASE_ARGS: ${{ inputs.additional_release_args }} + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + PGP_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} + run: | + RELEASE_ARGS="with-defaults" + if [ -n "${RELEASE_VERSION}" ]; then + RELEASE_ARGS="${RELEASE_ARGS} release-version ${RELEASE_VERSION}" + fi + if [ -n "${NEXT_VERSION}" ]; then + RELEASE_ARGS="${RELEASE_ARGS} next-version ${NEXT_VERSION}" + fi + if [ -n "${ADDITIONAL_RELEASE_ARGS}" ]; then + RELEASE_ARGS="${RELEASE_ARGS} ${ADDITIONAL_RELEASE_ARGS}" + fi + sbt -v -Dfile.encoding=UTF8 "release ${RELEASE_ARGS}" < /dev/null + + - name: Create the GH Release + run: | + TAGS=$(git tag --sort=-creatordate | head -n 2) + LATEST_TAG=$(echo "$TAGS" | sed -n '1p') + PREVIOUS_TAG=$(echo "$TAGS" | sed -n '2p') + if [ -n "${PREVIOUS_TAG}" ]; then + echo "Creating release notes between ${LATEST_TAG} and ${PREVIOUS_TAG}" + gh release create --target "${FINAL_REF}" --generate-notes --notes-start-tag "${PREVIOUS_TAG}" "${LATEST_TAG}" + else + gh release create --target "${FINAL_REF}" --generate-notes "${LATEST_TAG}" + fi + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index c0d3d48..974390d 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,7 @@ - [Gradle Snapshot (`gradle-snapshot.yml`)](#gradle-snapshot-gradle-snapshotyml) - [Gradle Release (`gradle-release.yml`)](#gradle-release-gradle-releaseyml) - [SBT Snapshot (`sbt-snapshot.yml`)](#sbt-snapshot-sbt-snapshotyml) + - [SBT Release (`sbt-release.yml`)](#sbt-release-sbt-releaseyml) - [Maven Set Version (`maven-set-version.yml`)](#maven-set-version-maven-set-versionyml) - [Jib Image Updater (`maven-jib-image-updater.yml`)](#jib-image-updater-maven-jib-image-updateryml) - [Validate GitHub Action (`validate-gh-action.yml`)](#validate-github-action-validate-gh-actionyml) @@ -366,6 +367,102 @@ Workflow for deploying snapshot versions to Sonatype Nexus, similar to its Maven The workflow writes `~/.sbt/.credentials` for SBT to authenticate against the snapshot repository. +## SBT Release (`sbt-release.yml`) + +Workflow for releasing an SBT project to Maven Central (Sonatype Central Portal). Gated to the upstream repo so forks cannot accidentally publish. + +**Prerequisites**: The calling project must use sbt 1.11 or later (for `localStaging` and `sonaRelease`), and apply +[`sbt-release`](https://github.com/sbt/sbt-release) and [`sbt-pgp`](https://github.com/sbt/sbt-pgp), with the version +in `version.sbt` (the file updated by `sbt-release`). + +- **Tasks**: Checkout code, configure Git, set up Java (importing the GPG key) and SBT, configure the resolvers and + `~/.sbt/.credentials` for `central.sonatype.com`, set HTTPS token on `origin`, run `sbt "release with-defaults"` + (which, with the release process below, commits the release version, tags it, publishes the signed artifacts to the + Central Portal, commits the next snapshot version, and pushes everything), then creates the GitHub Release. +- **Inputs**: The following inputs are available to be overridden + * `project_name` (required): Name in `/` format, the job only runs in that repository + * `ref_to_release` (default: `''`): Branch to release (a branch, so that `sbt-release` can push to it) + * `release_version` (default: `''`): Version to release, passed as `release-version` + * `next_version` (default: `''`): Next snapshot version, passed as `next-version` + * `additional_release_args` (default: `''`): Extra `sbt-release` arguments, e.g. `skip-tests` + * `java_version` (default: `8`) + * `resolvers` (default: same as `sbt-ci.yml`) + * `fetch_all_commits` (default: `false`) +- **Secrets**: `SONATYPE_USERNAME`, `SONATYPE_PASSWORD`, `GPG_PRIVATE_KEY`, `GPG_PASSPHRASE` +- **Permissions**: `contents: write` + +`sbt-release` pushes the release and next-version commits, and the tag, with `GITHUB_TOKEN`: the release fails at the +very end, after publishing to Maven Central, if branch protection doesn't allow `github-actions[bot]` to push to the +released branch. A second release of the same ref is queued, not cancelled, so that a run is never stopped midway. + +**Required configuration in the calling project**: + +```scala +// project/plugins.sbt +addSbtPlugin("com.github.sbt" % "sbt-pgp" % "") +addSbtPlugin("com.github.sbt" % "sbt-release" % "") +``` + +```scala +// build.sbt +ThisBuild / publishTo := { + if (isSnapshot.value) Some("Sonatype Nexus Repository Manager" at "https://central.sonatype.com/repository/maven-snapshots/") + else localStaging.value +} +ThisBuild / credentials += Credentials(Path.userHome / ".sbt" / ".credentials") +// plus the metadata required by Maven Central: description, homepage, licenses, scmInfo, developers + +releaseProcess := { + import sbtrelease.ReleaseStateTransformations._ + Seq[ReleaseStep]( + checkSnapshotDependencies, + inquireVersions, + runClean, + runTest, + setReleaseVersion, + commitReleaseVersion, // its check, run before any step, aborts on uncommitted changes + tagRelease, + releaseStepCommand("publishSigned"), + releaseStepCommand("sonaRelease"), + setNextVersion, + commitNextVersion, + pushChanges + ) +} +``` + +
+Example caller workflow + +```yaml +name: SBT Release + +on: + workflow_dispatch: + inputs: + release_version: + description: '[Optional] Version to release. Default: the version in version.sbt without -SNAPSHOT' + required: false + type: string + default: '' + +jobs: + call-sbt-release: + permissions: + contents: write + uses: project-ncl/shared-github-actions/.github/workflows/sbt-release.yml@ # + with: + project_name: 'project-ncl/' + release_version: ${{ inputs.release_version }} + secrets: + SONATYPE_USERNAME: ${{ secrets.SONATYPE_USERNAME }} + SONATYPE_PASSWORD: ${{ secrets.SONATYPE_PASSWORD }} + GPG_PRIVATE_KEY: ${{ secrets.GPG_PRIVATE_KEY }} + GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} +``` + +
+ ## Maven Set Version (`maven-set-version.yml`) Workflow to update the version in a Maven `pom.xml`. This can be manually run by going to the GitHub Actions tab and selecting the workflow.