Deploy to Maven Central #73
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: Deploy to Maven Central | |
on: workflow_dispatch | |
concurrency: | |
group: "${{ github.workflow }}-${{ github.ref }}" | |
cancel-in-progress: true | |
permissions: | |
contents: write | |
pages: write | |
id-token: write | |
env: | |
STAGING_HOST: "central.sonatype.com" | |
jobs: | |
tag-release: | |
name: Tag release | |
runs-on: ubuntu-latest | |
outputs: | |
INITIAL_REF_POSITION: ${{ steps.create-tag.outputs.INITIAL_REF_POSITION }} | |
TAG: ${{ steps.create-tag.outputs.TAG }} | |
VERSION: ${{ steps.create-tag.outputs.VERSION }} | |
steps: | |
- uses: actions/checkout@v4 | |
with: | |
ref: ${{ github.ref }} | |
- uses: actions/setup-java@v4 | |
with: | |
distribution: temurin | |
java-version: 24 | |
- name: Cache Dependencies | |
uses: actions/cache@v4 | |
with: | |
path: ~/.m2/repository/* | |
key: "${{ runner.OS }}-maven-${{ hashFiles('**/pom.xml') }}-tag-release" | |
- name: Configure Git User | |
run: | | |
git config user.email "cowwoc2020@gmail.com" | |
git config user.name "Gili Tzabari" | |
- name: Create tag | |
id: create-tag | |
run: | | |
echo "INITIAL_REF_POSITION=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" | |
# Maven command-line options: | |
# --batch-mode: recommended in CI to inform maven to not run in interactive mode (less logs) | |
# -V: strongly recommended in CI, will display the JDK and Maven versions in use. | |
# Very useful to be quickly sure the selected versions were the ones you think. | |
# -e: Display stack-traces on failure | |
# | |
# "release:prepare" must skip integration tests because "binaries-on-path-test" requires artifacts | |
# to be deployed to local repository. | |
./mvnw release:prepare --batch-mode -V -e -Darguments="-Dinvoker.skip=true" | |
# Getting the current git tag: https://stackoverflow.com/a/50465671/14731 | |
TAG=$(git describe --tag --abbrev=0) | |
# Setting a GitHub Action output parameter: | |
# https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-output-parameter | |
echo "TAG=${TAG}" >> "$GITHUB_OUTPUT" | |
# Extracting the release version number: https://stackoverflow.com/a/16623897/14731 | |
echo "VERSION=${TAG#"release-"}" >> "$GITHUB_OUTPUT" | |
build: | |
needs: tag-release | |
name: Build | |
uses: ./.github/workflows/reusable-build.yml | |
secrets: inherit | |
permissions: | |
contents: read | |
actions: read | |
checks: write | |
packages: write | |
with: | |
COMMIT_ID: ${{ needs.tag-release.outputs.TAG }} | |
VERSION: ${{ needs.tag-release.outputs.VERSION }} | |
FOR_RELEASE: true | |
deploy: | |
needs: [ tag-release, build ] | |
name: Deploy | |
runs-on: ubuntu-latest | |
outputs: | |
ID: ${{ steps.upload.outputs.ID }} | |
steps: | |
- uses: actions/checkout@v4 | |
with: | |
ref: ${{ github.ref }} | |
- name: Configure Git User | |
run: | | |
git config user.email "cowwoc2020@gmail.com" | |
git config user.name "Gili Tzabari" | |
- name: Download bundles | |
uses: actions/download-artifact@v4 | |
with: | |
path: "${{ github.workspace }}/bundles" | |
- name: Generate metadata | |
shell: bash | |
env: | |
MAVEN_GPG_PRIVATE_KEY: ${{ secrets.MAVEN_GPG_PRIVATE_KEY }} | |
MAVEN_GPG_PASSPHRASE: ${{ secrets.MAVEN_GPG_PASSPHRASE }} | |
run: | | |
# Define the temporary file location for the private key | |
GPG_KEY_FILE="$(mktemp)" | |
# Write the private key to the temp file | |
echo "${MAVEN_GPG_PRIVATE_KEY}" > "${GPG_KEY_FILE}" | |
# Import the private key into the GPG keychain | |
IMPORTED_KEY=$(gpg --batch --import "${GPG_KEY_FILE}" 2>&1) | |
# Remove the temporary key file immediately after import | |
shred --remove "${GPG_KEY_FILE}" | |
GPG_KEY_ID=$(echo "$IMPORTED_KEY" | tr -d '\r' | awk '/^gpg: key .*: secret key imported$/ \ | |
{ print $3; exit }') | |
# Strip away trailing colon | |
GPG_KEY_ID=${GPG_KEY_ID%:} | |
if [ -z "${GPG_KEY_ID}" ]; then | |
echo "Error: Could not parse the secret key's ID" | |
exit 1 | |
fi | |
# --batch requires the use of the key's fingerprint | |
# Based on https://unix.stackexchange.com/a/743986/60809 | |
export GPG_KEY_ID=$(gpg --list-secret-keys --with-colons | awk -F: -v keyid="${GPG_KEY_ID}" ' | |
$1 == "sec" && $5 == keyid { found=1; next } | |
found && $1 == "fpr" {print $10; exit} | |
') | |
# Define the temporary file location for the passphrase | |
export GPG_PASSPHRASE_FILE="$(mktemp)" | |
# Write the passphrase to the temp file | |
echo "${MAVEN_GPG_PASSPHRASE}" > "${GPG_PASSPHRASE_FILE}" | |
# Setup a cleanup trap to remove the key after the job completes | |
cleanup_gpg() | |
{ | |
gpg --batch --yes --delete-secret-keys "${GPG_KEY_ID}" | |
gpg --batch --yes --delete-keys "${GPG_KEY_ID}" | |
shred --remove --force "${GPG_PASSPHRASE_FILE}" | |
unset GPG_PASSPHRASE_FILE | |
} | |
trap cleanup_gpg EXIT | |
# Create the expected directory structure | |
mkdir -p publish/ | |
mv bundles/requirements-bundle/.m2/repository/* publish/ | |
VERSION=${{ needs.tag-release.outputs.VERSION }} | |
rm -rf docs/api/${VERSION} | |
mkdir --parents docs/api/${VERSION} | |
mv bundles/requirements-bundle/work/requirements/requirements/docs/api/${VERSION} docs/api/${VERSION} | |
rm -rf bundles | |
# Sign and generate checksums for all JAR and POM files | |
cd publish/ | |
find . -type f \( -name "*.jar" -o -name "*.pom" \) -exec sh -c ' | |
for file; do | |
# Reminder: this block only sees exported variables | |
cat "${GPG_PASSPHRASE_FILE}" | gpg --batch --pinentry-mode loopback --passphrase-fd 0 --yes \ | |
--detach-sign --armor --local-user ${GPG_KEY_ID} -o "${file}.asc" "${file}" | |
md5sum "${file}" | awk "{print \$1}" > "${file}.md5" | |
sha1sum "${file}" | awk "{print \$1}" > "${file}.sha1" | |
sha256sum "${file}" | awk "{print \$1}" > "${file}.sha256" | |
sha512sum "${file}" | awk "{print \$1}" > "${file}.sha512" | |
done | |
' sh {} + | |
# Clean up proactively | |
cleanup_gpg | |
trap - EXIT | |
zip -r "../requirements-${{ needs.tag-release.outputs.VERSION }}.zip" . | |
cd .. | |
- name: Commit documentation | |
run: | | |
VERSION=${{ needs.tag-release.outputs.VERSION }} | |
git add docs/api/${VERSION} | |
git commit -m "Publishing documentation for version ${VERSION}" | |
git push | |
- name: Upload bundle | |
id: upload | |
env: | |
# The credentials correspond to the user token returned by https://central.sonatype.com/account | |
# See https://central.sonatype.org/publish/publish-portal-api/#authentication-authorization | |
MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} | |
MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} | |
VERSION: ${{ needs.tag-release.outputs.VERSION }} | |
run: | | |
BEARER_TOKEN=$(printf "${MAVEN_CENTRAL_USERNAME}:${MAVEN_CENTRAL_PASSWORD}" | base64) | |
ID=$(curl --request POST --show-error --fail \ | |
--header "Authorization: Bearer ${BEARER_TOKEN}" \ | |
--form "bundle=@requirements-${VERSION}.zip" \ | |
--form "name=requirements-${VERSION}" \ | |
--form "publishingType=USER_MANAGED" \ | |
https://central.sonatype.com/api/v1/publisher/upload) | |
echo "ID=${ID}" >> $GITHUB_ENV | |
- name: Poll for deployment status | |
env: | |
MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} | |
MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} | |
run: | | |
BEARER_TOKEN=$(printf "${MAVEN_CENTRAL_USERNAME}:${MAVEN_CENTRAL_PASSWORD}" | base64) | |
while true; do | |
response=$(curl --request POST --show-error --fail --silent \ | |
--header "Authorization: Bearer ${BEARER_TOKEN}" \ | |
"https://central.sonatype.com/api/v1/publisher/status?id=${ID}") | |
status=$(echo "${response}" | jq -r '.deploymentState') | |
echo "Current status: ${status}" | |
if [[ "${status}" == "VALIDATED" ]]; then | |
echo "Deployment validated successfully!" | |
exit 0 | |
elif [[ "${status}" == "FAILED" ]]; then | |
echo "Deployment failed!" | |
exit 1 | |
fi | |
sleep 5 | |
done | |
- name: Publish release | |
env: | |
MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} | |
MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} | |
run: | | |
BEARER_TOKEN=$(printf "${MAVEN_CENTRAL_USERNAME}:${MAVEN_CENTRAL_PASSWORD}" | base64) | |
response=$(curl --request POST --show-error --fail --write-out "%{response_code}" --silent \ | |
--output /dev/null --header "Authorization: Bearer ${BEARER_TOKEN}" \ | |
"https://central.sonatype.com/api/v1/publisher/deployment/${ID}") | |
if [[ "${response}" == "204" ]]; then | |
echo "Release published successfully!" | |
exit 0 | |
else | |
echo "Unexpected response code: ${response}" | |
exit 1 | |
fi | |
on-failure: | |
name: On failure | |
needs: [ tag-release, build, deploy ] | |
runs-on: ubuntu-latest | |
if: ${{ failure() || cancelled() }} | |
steps: | |
- uses: actions/checkout@v4 | |
with: | |
ref: ${{ github.ref }} | |
fetch-depth: 0 | |
- name: Install Java | |
uses: actions/setup-java@v4 | |
with: | |
distribution: temurin | |
java-version: 8 | |
- name: Drop the bundle | |
if: needs.deploy.outputs.ID != '' | |
env: | |
MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }} | |
MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }} | |
run: > | |
BEARER_TOKEN=$(printf "${MAVEN_CENTRAL_USERNAME}:${MAVEN_CENTRAL_PASSWORD}" | base64) | |
curl --request DELETE --show-error --fail \ | |
--header "Authorization: Bearer ${BEARER_TOKEN}" \ | |
"https://central.sonatype.com/api/v1/publisher/status?id=${{ needs.deploy.outputs.ID }}") | |
- name: Restore the workflow ref to its original position | |
if: needs.tag-release.outputs.INITIAL_REF_POSITION != '' | |
run: | | |
CURRENT_REF_POSITION=$(git rev-parse HEAD) | |
if [ "${CURRENT_REF_POSITION}" != "${{ needs.tag-release.outputs.INITIAL_REF_POSITION }}" ]; then | |
git reset --hard ${{ needs.tag-release.outputs.INITIAL_REF_POSITION }} | |
if [ "${{ github.ref_type }}" == "tag" ]; then | |
git ${{ github.ref_type }} -f ${{ github.ref_name }} | |
fi | |
git push -f origin ${{ github.ref_name }} | |
fi | |
- name: Delete tag | |
if: needs.tag-release.outputs.TAG != '' | |
run: | | |
git push --delete origin ${{ needs.tag-release.outputs.TAG }} |