Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
165 changes: 165 additions & 0 deletions .github/workflows/sbt-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,165 @@
name: SBT Release to Maven Central

on:
workflow_call:
inputs:
project_name:
description: "Name in <organisation>/<project_name> 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 }}
97 changes: 97 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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 `<organisation>/<project_name>` 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" % "<latest>")
addSbtPlugin("com.github.sbt" % "sbt-release" % "<latest>")
```

```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
)
}
```

<details>
<summary>Example caller workflow</summary>

```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@<sha> # <tag>
with:
project_name: 'project-ncl/<project>'
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 }}
```

</details>

## 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.

Expand Down
Loading