diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 3dc1e0d..b80e5bf 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -34,7 +34,11 @@ "Bash(done)", "Bash(grep:*)", "Bash(ls:*)", - "Bash(xargs:*)" + "Bash(xargs:*)", + "Bash(./gradlew tasks:*)", + "Bash(./gradlew jreleaserConfig 2>&1 | tail -30)", + "Bash(find:*)", + "Bash(chmod:*)" ] } } diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6b3e5b0..87317ef 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -73,8 +73,17 @@ jobs: JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME }} JRELEASER_MAVENCENTRAL_PASSWORD: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD }} - - name: Release to Maven Central portal - run: ./gradlew jreleaserFullRelease + - name: Deploy to Maven Central portal + run: ./infra/release-with-retry.sh 900 100 20 + env: + JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME }} + JRELEASER_MAVENCENTRAL_PASSWORD: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_PASSWORD }} + JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY }} + JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }} + JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }} + + - name: Generate changelog + run: ./gradlew jreleaserChangelog --no-daemon env: JRELEASER_GITHUB_TOKEN: ${{ secrets.FLAMINGOCK_JRELEASER_GITHUB_TOKEN }} JRELEASER_MAVENCENTRAL_USERNAME: ${{ secrets.FLAMINGOCK_JRELEASER_MAVENCENTRAL_USERNAME }} @@ -82,3 +91,15 @@ jobs: JRELEASER_GPG_PUBLIC_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PUBLIC_KEY }} JRELEASER_GPG_SECRET_KEY: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_SECRET_KEY }} JRELEASER_GPG_PASSPHRASE: ${{ secrets.FLAMINGOCK_JRELEASER_GPG_PASSPHRASE }} + + - name: Create GitHub release + run: | + VERSION=$(grep '^version' build.gradle.kts | head -1 | sed 's/.*"\(.*\)".*/\1/') + PRERELEASE_FLAG=$(echo "$VERSION" | grep -qiE '(beta|alpha|rc|snapshot)' && echo "--prerelease" || echo "") + gh release create "v${VERSION}" \ + --title "Release v${VERSION}" \ + --notes-file build/jreleaser/release/CHANGELOG.md \ + $PRERELEASE_FLAG \ + --repo "${{ github.repository }}" + env: + GH_TOKEN: ${{ secrets.FLAMINGOCK_JRELEASER_GITHUB_TOKEN }} diff --git a/build.gradle.kts b/build.gradle.kts index f70511a..17756de 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -2,11 +2,12 @@ plugins { `java-library` `maven-publish` id("com.diffplug.spotless") version "6.25.0" + id("org.jreleaser") version "1.15.0" } group = "io.flamingock" -version = "1.0.0-SNAPSHOT" +version = "1.0.0-beta.7" val flamingockVersion = "1.2.0-beta.1" @@ -37,7 +38,8 @@ java { toolchain { languageVersion.set(JavaLanguageVersion.of(8)) } - + withSourcesJar() + withJavadocJar() } tasks.withType().configureEach { @@ -67,19 +69,43 @@ publishing { description.set("MongoDB change templates for document database operations using Flamingock") url.set("https://flamingock.io") + organization { + name.set("Flamingock") + url.set("https://flamingock.io") + } + + issueManagement { + system.set("GitHub") + url.set("https://github.com/flamingock/flamingock-java-template-mongodb/issues") + } + licenses { license { - name.set("Apache License, Version 2.0") + name.set("Apache-2.0") url.set("https://www.apache.org/licenses/LICENSE-2.0") } } developers { developer { - id.set("flamingock") - name.set("Flamingock Team") - organization.set("Flamingock") - organizationUrl.set("https://flamingock.io") + id.set("dieppa") + name.set("Antonio Perez Dieppa") + email.set("dieppa@flamingock.io") + } + developer { + id.set("osantana") + name.set("Oscar Santana") + email.set("osantana@flamingock.io") + } + developer { + id.set("bercianor") + name.set("Berciano Ramiro") + email.set("bercianor@flamingock.io") + } + developer { + id.set("dfrigolet") + name.set("Daniel Frigolet") + email.set("dfrigolet@flamingock.io") } } @@ -91,6 +117,11 @@ publishing { } } } + repositories { + maven { + url = uri(layout.buildDirectory.dir("staging-deploy")) + } + } } val licenseHeaderText = """/* @@ -130,3 +161,54 @@ afterEvaluate { description = "Check license headers (manual task - not part of build)" } } + +jreleaser { + project { + inceptionYear = "2024" + author("dieppa") + author("osantana") + author("bercianor") + author("dfrigolet") + description = "MongoDB change templates for document database operations using Flamingock" + } + signing { + active = org.jreleaser.model.Active.ALWAYS + armored = true + } + deploy { + maven { + mavenCentral { + create("sonatype") { + active = org.jreleaser.model.Active.ALWAYS + url = "https://central.sonatype.com/api/v1/publisher" + stagingRepository("build/staging-deploy") + applyMavenCentralRules = true + maxRetries = 90 + retryDelay = 20 + } + } + } + } + release { + github { + overwrite = true + changelog { + formatted = org.jreleaser.model.Active.ALWAYS + preset = "conventional-commits" + } + prerelease { + pattern = ".*(?:beta|alpha|rc|snapshot).*" + } + } + } +} + +tasks.register("createStagingDeployFolder") { + doLast { + mkdir(layout.buildDirectory.dir("staging-deploy")) + } +} + +tasks.matching { it.name == "publish" }.configureEach { + finalizedBy("createStagingDeployFolder") +} diff --git a/infra/release-with-retry.sh b/infra/release-with-retry.sh new file mode 100755 index 0000000..16283cb --- /dev/null +++ b/infra/release-with-retry.sh @@ -0,0 +1,134 @@ +#!/bin/bash +set -euo pipefail + +# ------------------------------------------------------------------- +# Maven Central release script with curl-based upload fallback. +# +# JReleaser's Feign HTTP client has a hard read-timeout (~6 min) that +# cannot be reliably overridden via Gradle DSL for large bundles. +# This script first attempts jreleaserDeploy; if that fails, it falls +# back to uploading the bundle via curl with a generous timeout, +# then polls the Central Portal for deployment status. +# ------------------------------------------------------------------- + +UPLOAD_TIMEOUT=${1:-900} # curl upload timeout in seconds (default 15 min) +MAX_STATUS_CHECKS=${2:-100} # max status poll attempts +STATUS_CHECK_INTERVAL=${3:-20} # seconds between status polls + +CENTRAL_API="https://central.sonatype.com/api/v1/publisher" + +# Credentials from environment (set by GitHub Actions) +USERNAME="${JRELEASER_MAVENCENTRAL_USERNAME:?JRELEASER_MAVENCENTRAL_USERNAME not set}" +PASSWORD="${JRELEASER_MAVENCENTRAL_PASSWORD:?JRELEASER_MAVENCENTRAL_PASSWORD not set}" +AUTH_TOKEN=$(echo -n "${USERNAME}:${PASSWORD}" | base64) + +# --- Step 1: Try jreleaserDeploy (signs artifacts and creates bundle) --- +echo "==> Attempting jreleaserDeploy (signs + bundles + uploads)..." +if ./gradlew jreleaserDeploy --no-daemon --stacktrace; then + echo "==> jreleaserDeploy succeeded" + exit 0 +fi + +echo "==> jreleaserDeploy failed (likely upload timeout). Falling back to curl upload." + +# --- Step 2: Locate the bundle created by JReleaser --- +BUNDLE_DIR="build/jreleaser/deploy/mavenCentral/sonatype" +BUNDLE=$(find "$BUNDLE_DIR" -name "*.zip" -type f 2>/dev/null | head -1) + +if [ -z "$BUNDLE" ]; then + echo "ERROR: No bundle zip found in $BUNDLE_DIR" + echo "Contents of build/jreleaser/deploy/:" + find build/jreleaser/deploy/ -type f 2>/dev/null || true + exit 1 +fi + +echo "==> Found bundle: $BUNDLE" + +# --- Step 3: Upload bundle via curl with retries --- +upload_bundle() { + local attempt=$1 + echo "==> Upload attempt $attempt — timeout ${UPLOAD_TIMEOUT}s" + + HTTP_RESPONSE=$(curl -s -w "\n%{http_code}" \ + --max-time "$UPLOAD_TIMEOUT" \ + --connect-timeout 60 \ + -X POST "$CENTRAL_API/upload" \ + -H "Authorization: Bearer $AUTH_TOKEN" \ + -F "bundle=@$BUNDLE;type=application/octet-stream" \ + -F "publishingType=AUTOMATIC" \ + 2>&1) || true + + HTTP_CODE=$(echo "$HTTP_RESPONSE" | tail -1) + RESPONSE_BODY=$(echo "$HTTP_RESPONSE" | sed '$d') + + echo "==> Upload response: HTTP $HTTP_CODE" + + if [ "$HTTP_CODE" = "201" ]; then + DEPLOYMENT_ID="$RESPONSE_BODY" + echo "==> Upload successful. Deployment ID: $DEPLOYMENT_ID" + return 0 + else + echo "==> Upload failed: $RESPONSE_BODY" + return 1 + fi +} + +DEPLOYMENT_ID="" +for attempt in 1 2 3 4 5; do + if upload_bundle "$attempt"; then + break + fi + if [ "$attempt" -eq 5 ]; then + echo "ERROR: All upload attempts failed" + exit 1 + fi + echo "==> Retrying upload in 30 seconds..." + sleep 30 +done + +# --- Step 4: Poll for deployment status --- +if [ -z "$DEPLOYMENT_ID" ]; then + echo "ERROR: No deployment ID obtained" + exit 1 +fi + +echo "==> Polling deployment status (max $MAX_STATUS_CHECKS checks, every ${STATUS_CHECK_INTERVAL}s)..." + +for (( i=1; i<=MAX_STATUS_CHECKS; i++ )); do + sleep "$STATUS_CHECK_INTERVAL" + + STATUS_RESPONSE=$(curl -s \ + --max-time 60 \ + -X POST "$CENTRAL_API/status?id=$DEPLOYMENT_ID" \ + -H "Authorization: Bearer $AUTH_TOKEN" \ + -H "Accept: application/json" \ + 2>&1) || true + + DEPLOYMENT_STATE=$(echo "$STATUS_RESPONSE" | grep -o '"deploymentState":"[^"]*"' | head -1 | cut -d'"' -f4) + + echo " [$i/$MAX_STATUS_CHECKS] Deployment state: ${DEPLOYMENT_STATE:-UNKNOWN}" + + case "$DEPLOYMENT_STATE" in + PUBLISHED) + echo "==> Deployment published successfully!" + exit 0 + ;; + FAILED) + echo "ERROR: Deployment failed on Maven Central" + echo "Response: $STATUS_RESPONSE" + exit 1 + ;; + VALIDATED|PUBLISHING) + echo " Deployment is progressing..." + ;; + PENDING|VALIDATING) + echo " Still processing..." + ;; + *) + echo " Unexpected state, continuing to poll..." + ;; + esac +done + +echo "ERROR: Timed out waiting for deployment status after $MAX_STATUS_CHECKS checks" +exit 1