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
52 changes: 52 additions & 0 deletions .github/scripts/create-central-bundle.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
#!/usr/bin/env bash

set -euo pipefail

if [[ $# -ne 2 ]]; then
echo "usage: $0 <version> <central-bundle.zip>" >&2
exit 1
fi

version=$1
bundle=$2
staging_root="target/central-bundle-staging"
destination="${staging_root}/io/rstream/rstream/${version}"

if [[ ! "$version" =~ ^[0-9]+(\.[0-9]+){2}$ ]]; then
echo "invalid release version: ${version}" >&2
exit 1
fi
if [[ -e "$staging_root" || -e "$bundle" ]]; then
echo "Maven Central bundle output already exists" >&2
exit 1
fi

mkdir -p "$destination" "$(dirname "$bundle")"
artifacts=(
"target/rstream-${version}.pom"
"target/rstream-${version}.jar"
"target/rstream-${version}-sources.jar"
"target/rstream-${version}-javadoc.jar"
)
for artifact in "${artifacts[@]}"; do
for source in "$artifact" "${artifact}.asc"; do
if [[ ! -f "$source" ]]; then
echo "signed Maven artifact is missing: ${source}" >&2
exit 1
fi
cp "$source" "$destination/"
done
done

for artifact in "$destination"/*; do
md5sum "$artifact" | awk '{print $1}' > "${artifact}.md5"
sha1sum "$artifact" | awk '{print $1}' > "${artifact}.sha1"
sha256sum "$artifact" | awk '{print $1}' > "${artifact}.sha256"
sha512sum "$artifact" | awk '{print $1}' > "${artifact}.sha512"
done

bundle=$(cd "$(dirname "$bundle")" && pwd)/$(basename "$bundle")
(
cd "$staging_root"
find io -type f -print | LC_ALL=C sort | zip -q "$bundle" -@
)
112 changes: 112 additions & 0 deletions .github/scripts/publish-maven-central.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
#!/usr/bin/env bash

set -euo pipefail

if [[ $# -ne 2 ]]; then
echo "usage: $0 <version> <central-bundle.zip>" >&2
exit 1
fi

version=$1
bundle=$2
repository_url="https://repo1.maven.org/maven2/io/rstream/rstream/${version}"
work_directory=$(mktemp -d)
trap 'rm -rf "$work_directory"' EXIT

"$(dirname "$0")/verify-central-bundle.sh" "$version" "$bundle"
unzip -q "$bundle" -d "$work_directory/bundle"
candidate_directory="${work_directory}/bundle/io/rstream/rstream/${version}"

verify_publication() {
local attempts=$1
local files=(
"rstream-${version}.pom"
"rstream-${version}.pom.asc"
"rstream-${version}.jar"
"rstream-${version}.jar.asc"
"rstream-${version}-sources.jar"
"rstream-${version}-sources.jar.asc"
"rstream-${version}-javadoc.jar"
"rstream-${version}-javadoc.jar.asc"
)
local attempt filename
for ((attempt = 1; attempt <= attempts; attempt++)); do
local complete=true
for filename in "${files[@]}"; do
if ! curl --fail --silent --show-error --location \
--output "${work_directory}/${filename}" "${repository_url}/${filename}"; then
complete=false
break
fi
if ! cmp --silent "${candidate_directory}/${filename}" "${work_directory}/${filename}"; then
echo "published Maven artifact differs from candidate: ${filename}" >&2
exit 1
fi
done
if [[ "$complete" == true ]]; then
return 0
fi
if ((attempt < attempts)); then
sleep 10
fi
done
return 1
}

publication_status=$(curl --silent --location --output /dev/null --write-out '%{http_code}' \
"${repository_url}/rstream-${version}.pom")
if [[ "$publication_status" == 200 ]]; then
if ! verify_publication 12; then
echo "existing Maven Central release is incomplete" >&2
exit 1
fi
exit 0
fi
if [[ "$publication_status" != 404 ]]; then
echo "Maven Central availability check returned HTTP ${publication_status}" >&2
exit 1
fi

: "${MAVEN_CENTRAL_USERNAME:?MAVEN_CENTRAL_USERNAME is required}"
: "${MAVEN_CENTRAL_PASSWORD:?MAVEN_CENTRAL_PASSWORD is required}"
authorization=$(printf '%s:%s' "$MAVEN_CENTRAL_USERNAME" "$MAVEN_CENTRAL_PASSWORD" | base64 | tr -d '\n')
printf '::add-mask::%s\n' "$authorization"
deployment_id=$(curl --fail --silent --show-error \
--header "Authorization: Bearer ${authorization}" \
--form "bundle=@${bundle};type=application/octet-stream" \
"https://central.sonatype.com/api/v1/publisher/upload?publishingType=AUTOMATIC&name=rstream-${version}")
if [[ ! "$deployment_id" =~ ^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$ ]]; then
echo "Maven Central returned an invalid deployment ID" >&2
exit 1
fi

for _ in {1..90}; do
status=$(curl --fail --silent --show-error --request POST \
--header "Authorization: Bearer ${authorization}" \
"https://central.sonatype.com/api/v1/publisher/status?id=${deployment_id}")
state=$(jq -r '.deploymentState' <<<"$status")
case "$state" in
PUBLISHED)
break
;;
FAILED)
jq '.errors' <<<"$status" >&2
exit 1
;;
PENDING | VALIDATING | VALIDATED | PUBLISHING)
sleep 10
;;
*)
echo "unexpected Maven Central deployment state: ${state}" >&2
exit 1
;;
esac
done
if [[ "$state" != PUBLISHED ]]; then
echo "Maven Central deployment did not reach PUBLISHED" >&2
exit 1
fi
if ! verify_publication 60; then
echo "Maven Central release did not become publicly verifiable" >&2
exit 1
fi
49 changes: 49 additions & 0 deletions .github/scripts/verify-central-bundle.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
#!/usr/bin/env bash

set -euo pipefail

if [[ $# -ne 2 ]]; then
echo "usage: $0 <version> <central-bundle.zip>" >&2
exit 1
fi

version=$1
bundle=$2
prefix="io/rstream/rstream/${version}"

if [[ ! "$version" =~ ^[0-9]+(\.[0-9]+){2}$ ]]; then
echo "invalid release version: ${version}" >&2
exit 1
fi
if [[ ! -f "$bundle" ]]; then
echo "Maven Central bundle is missing: ${bundle}" >&2
exit 1
fi

unzip -tq "$bundle" >/dev/null
if unzip -Z1 "$bundle" | awk -v prefix="${prefix}/" '
/^\// || /(^|\/)\.\.($|\/)/ || index($0, prefix) != 1 { invalid = 1 }
END { exit invalid }
'; then
:
else
echo "Maven Central bundle contains an unsafe path" >&2
exit 1
fi

required=(
"rstream-${version}.pom"
"rstream-${version}.jar"
"rstream-${version}-sources.jar"
"rstream-${version}-javadoc.jar"
)
entries=$(unzip -Z1 "$bundle")
for filename in "${required[@]}"; do
for suffix in "" .asc; do
expected="${prefix}/${filename}${suffix}"
if ! grep -Fxq "$expected" <<<"$entries"; then
echo "Maven Central bundle is missing ${expected}" >&2
exit 1
fi
done
done
107 changes: 88 additions & 19 deletions .github/workflows/publish.yml
Original file line number Diff line number Diff line change
@@ -1,33 +1,102 @@
name: Publish
name: Promote stable release

on:
release:
types:
- published
workflow_dispatch:
inputs:
release_tag:
description: Reviewed candidate tag to publish
required: true
type: string

permissions:
contents: read
actions: read
contents: write

concurrency:
group: stable-release
cancel-in-progress: false

jobs:
publish:
name: Publish approved Maven Central bundle
if: ${{ github.actor == vars.CI_ALLOWED_ACTOR }}
environment: stable-release
runs-on: ubuntu-latest
environment: maven-central
steps:
- name: Find matching release candidate
id: release
shell: bash
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ inputs.release_tag }}
run: |
set -euo pipefail
if [[ ! "$RELEASE_TAG" =~ ^v[0-9]+(\.[0-9]+){2}$ ]]; then
echo "invalid release tag: ${RELEASE_TAG}" >&2
exit 1
fi
if [[ "$(gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${RELEASE_TAG}" --jq '.draft')" != true ]]; then
echo "GitHub release ${RELEASE_TAG} must still be a draft" >&2
exit 1
fi
version=${RELEASE_TAG#v}
latest_tag=$(gh api "repos/${GITHUB_REPOSITORY}/releases/latest" --jq '.tag_name' 2>/dev/null || true)
if [[ -n "$latest_tag" && "$(printf '%s\n' "${latest_tag#v}" "$version" | sort -V | tail -n 1)" != "$version" ]]; then
echo "refusing to promote ${RELEASE_TAG} after newer release ${latest_tag}" >&2
exit 1
fi
tag_ref=$(gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${RELEASE_TAG}")
tag_sha=$(jq -r '.object.sha' <<<"$tag_ref")
if [[ "$(jq -r '.object.type' <<<"$tag_ref")" == tag ]]; then
tag_sha=$(gh api "repos/${GITHUB_REPOSITORY}/git/tags/${tag_sha}" --jq '.object.sha')
fi
candidate_run=$(gh api --method GET \
"repos/${GITHUB_REPOSITORY}/actions/workflows/release-candidate.yml/runs" \
-f branch="$RELEASE_TAG" -f event=push -f status=success -f per_page=20 \
--jq ".workflow_runs | map(select(.head_sha == \"${tag_sha}\")) | first | .id")
if [[ -z "$candidate_run" || "$candidate_run" == null ]]; then
echo "no successful release candidate run matches ${RELEASE_TAG}" >&2
exit 1
fi
{
echo "tag=${RELEASE_TAG}"
echo "version=${version}"
echo "run_id=${candidate_run}"
} >> "$GITHUB_OUTPUT"
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
with:
ref: ${{ steps.release.outputs.tag }}
persist-credentials: false
- uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
- uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4
with:
distribution: temurin
java-version: "21"
cache: maven
server-id: central
server-username: MAVEN_CENTRAL_USERNAME
server-password: MAVEN_CENTRAL_PASSWORD
gpg-private-key: ${{ secrets.GPG_PRIVATE_KEY }}
gpg-passphrase: MAVEN_GPG_PASSPHRASE
- name: Publish to Maven Central
name: release-candidate-${{ steps.release.outputs.version }}
path: candidate
repository: ${{ github.repository }}
run-id: ${{ steps.release.outputs.run_id }}
github-token: ${{ github.token }}
- name: Verify candidate integrity
shell: bash
env:
VERSION: ${{ steps.release.outputs.version }}
run: |
set -euo pipefail
(cd candidate && sha256sum --check SHA256SUMS)
./.github/scripts/verify-central-bundle.sh "$VERSION" candidate/central-bundle.zip
- name: Publish and verify Maven Central bundle
shell: bash
env:
MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
MAVEN_CENTRAL_PASSWORD: ${{ secrets.MAVEN_CENTRAL_PASSWORD }}
MAVEN_GPG_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }}
run: mvn -B -Prelease deploy
MAVEN_CENTRAL_USERNAME: ${{ secrets.MAVEN_CENTRAL_USERNAME }}
VERSION: ${{ steps.release.outputs.version }}
run: ./.github/scripts/publish-maven-central.sh "$VERSION" candidate/central-bundle.zip
- name: Publish GitHub release
env:
GH_TOKEN: ${{ github.token }}
RELEASE_TAG: ${{ steps.release.outputs.tag }}
run: |
set -euo pipefail
gh release edit "$RELEASE_TAG" --repo "$GITHUB_REPOSITORY" --draft=false --latest
if [[ "$(gh api "repos/${GITHUB_REPOSITORY}/releases/tags/${RELEASE_TAG}" --jq '.draft')" != false ]]; then
echo "GitHub release is still a draft: ${RELEASE_TAG}" >&2
exit 1
fi
Loading