This is an automated email from the ASF dual-hosted git repository. jdaugherty pushed a commit to branch main in repository https://gitbox.apache.org/repos/asf/grails-intellij-plugin.git
commit ebcd7af352e6a5e759476609a4baad1a732b074d Author: James Daugherty <[email protected]> AuthorDate: Sat Jul 18 16:22:10 2026 -0400 Adopt Grails Publish plugin for ASF Maven publishing --- .github/scripts/releaseDistributions.sh | 90 ++++++++++++++++++++ .github/scripts/releaseJarFiles.sh | 143 ++++++++++++++++++++++++++++++++ .github/workflows/release.yml | 50 ++++++++--- build.gradle.kts | 86 +++++++------------ gradle.properties | 2 +- settings.gradle.kts | 31 +++++++ 6 files changed, 333 insertions(+), 69 deletions(-) diff --git a/.github/scripts/releaseDistributions.sh b/.github/scripts/releaseDistributions.sh new file mode 100755 index 0000000..eef4ffa --- /dev/null +++ b/.github/scripts/releaseDistributions.sh @@ -0,0 +1,90 @@ +#!/bin/bash + +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +# ./releaseDistributions.sh <tag> <svn-folder> <username> + +set -euo pipefail + +if [[ $# -ne 3 ]]; then + echo "Usage: $0 <tag> <svn-folder> <username>" >&2 + exit 1 +fi + +RELEASE_TAG="$1" +SVN_FOLDER="$2" +SVN_USER="$3" +read -r -s -p "Password: " SVN_PASS +echo + +if [[ -z "${RELEASE_TAG}" ]]; then + echo "❌ ERROR: Release Tag must not be empty." >&2 + exit 1 +fi +if [[ -z "${SVN_USER}" ]]; then + echo "❌ ERROR: Username must not be empty." >&2 + exit 1 +fi +if [[ -z "${SVN_PASS}" ]]; then + echo "❌ ERROR: Password must not be empty." >&2 + exit 1 +fi + +RELEASE_VERSION="${RELEASE_TAG#v}" +RELEASE_ROOT="https://dist.apache.org/repos/dist/release/grails/${SVN_FOLDER}" +DEV_ROOT="https://dist.apache.org/repos/dist/dev/grails/${SVN_FOLDER}" + +svn_flags=(--non-interactive --trust-server-cert --username "${SVN_USER}" --password "${SVN_PASS}") + +svn_exists() { + local url="$1" + svn ls "${svn_flags[@]}" --depth=empty "${url}" >/dev/null 2>&1 +} + +for folder in $(svn ls "${svn_flags[@]}" "${RELEASE_ROOT}"); do + folder=$(echo "$folder" | sed 's|/$||') + [[ "${folder}" == "${RELEASE_VERSION}" ]] && continue + PRIOR_RELEASE_URL="${RELEASE_ROOT}/${folder}" + read -r -p "Remove old release folder '${folder}' at ${PRIOR_RELEASE_URL}? [y/N] " confirm < /dev/tty + if [[ "${confirm}" =~ ^[Yy](es)?$ ]]; then + echo "🗑️ Deleting old release folder: ${PRIOR_RELEASE_URL}" + svn rm "${svn_flags[@]}" -m "Remove previous release ${folder}" "${PRIOR_RELEASE_URL}" + echo "✅ Deleted old release folder" + else + echo "⏭️ Skipping removal of old release folder: ${PRIOR_RELEASE_URL}" + fi +done + +DEV_VERSION_URL="$DEV_ROOT/${RELEASE_VERSION}" +RELEASE_VERSION_URL="$RELEASE_ROOT/${RELEASE_VERSION}" + +if ! svn_exists "${DEV_VERSION_URL}"; then + echo "❌ ERROR: dev folder for ${RELEASE_VERSION} does not exist at: ${DEV_VERSION_URL}" >&2 + exit 2 +fi + +if svn_exists "${RELEASE_VERSION_URL}"; then + echo "❌ ERROR: release folder for ${RELEASE_VERSION} already exists at: ${RELEASE_VERSION_URL}" >&2 + exit 3 +fi + +echo "🚀 Promoting ${DEV_VERSION_URL} -> ${RELEASE_VERSION_URL}" +svn mv "${svn_flags[@]}" -m "Promote Apache Grails ${RELEASE_VERSION} from dev to release" "${DEV_VERSION_URL}" "${RELEASE_VERSION_URL}" +echo "✅ Promoted" diff --git a/.github/scripts/releaseJarFiles.sh b/.github/scripts/releaseJarFiles.sh new file mode 100755 index 0000000..a18f7c5 --- /dev/null +++ b/.github/scripts/releaseJarFiles.sh @@ -0,0 +1,143 @@ +#!/bin/bash + +# +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. +# + +# ./releaseJarFiles.sh <staging-repo-description> <username> + +set -euo pipefail + +if [[ $# -ne 2 ]]; then + echo "Usage: $0 <staging-repo-description> <username>" >&2 + exit 1 +fi + +STAGING_DESCRIPTION="$1" +NEXUS_USER="$2" +read -r -s -p "Password: " NEXUS_PASS +echo + +if [[ -z "${STAGING_DESCRIPTION}" ]]; then + echo "ERROR: Staging Description must not be empty." >&2 + exit 1 +fi +if [[ -z "${NEXUS_USER}" ]]; then + echo "ERROR: Username must not be empty." >&2 + exit 1 +fi +if [[ -z "${NEXUS_PASS}" ]]; then + echo "ERROR: Password must not be empty." >&2 + exit 1 +fi + +NEXUS_URL="https://repository.apache.org" + +nexusApi() { + local request_method="$1"; shift + local path="$1"; shift + curl -fsS -u "${NEXUS_USER}:${NEXUS_PASS}" \ + -H 'Accept: application/json' \ + -H 'Content-Type: application/json' \ + -X "${request_method}" "${NEXUS_URL}/service/local/${path}" "$@" +} + +wait_for_promotion() { + local repoId="$1" + local timeout_s="${2:-600}" # default 10 minutes + local interval_s="${3:-3}" + local started + started="$(date +%s)" + + echo "Waiting for release promotion to complete (timeout ${timeout_s}s)…" + + while :; do + # 1) If any ERROR appears in activity, fail fast + act="$(nexusApi GET "/staging/repository/${repoId}/activity" || true)" + if [[ -n "$act" ]]; then + err_count="$(jq -r '[.. | objects? | select(has("severity")) | select(.severity=="ERROR")] | length' <<<"$act" 2>/dev/null || echo 0)" + if [[ "$err_count" != "0" ]]; then + echo "ERROR: Staging activity contains failure(s). Aborting." >&2 + # Optionally dump recent relevant lines: + jq -r '.. | objects? | select(has("severity")) | "\(.severity): \(.name // "event") - \(.message // "")"' <<<"$act" || true + return 1 + fi + fi + + # 2) Check transitioning flag — when false after promote, action is done + trans="$(nexusApi GET '/staging/profile_repositories' \ + | jq -r --arg r "$repoId" '.data[]? | select(.repositoryId==$r) | .transitioning' 2>/dev/null || echo "true")" + + if [[ "$trans" == "false" ]]; then + # sanity: make sure we actually saw some "release/promote" activity; otherwise keep waiting a bit + if [[ -n "$act" ]]; then + # did we see any promote/release-ish step? + saw_promote="$(jq -r ' + [ .. | objects? | .name? // empty | ascii_downcase + | select(test("release|promote|finish")) ] | length' <<<"$act" 2>/dev/null || echo 0)" + if [[ "$saw_promote" -gt 0 ]]; then + echo "Promotion appears complete." + return 0 + fi + fi + fi + + # timeout? + now="$(date +%s)" + if (( now - started >= timeout_s )); then + echo "ERROR: Timed out waiting for promotion to complete." >&2 + # Show a short summary to aid debugging + if [[ -n "$act" ]]; then + echo "--- Recent activity snapshot ---" + jq -r '.. | objects? | select(has("severity") or has("name")) | "\(.severity // "")\t\(.name // "")\t\(.message // "")"' <<<"$act" | tail -n 20 || true + fi + return 1 + fi + + sleep "$interval_s" + done +} + +repos_json="$(nexusApi GET '/staging/profile_repositories')" +repoId="$(jq -r --arg d "${STAGING_DESCRIPTION}" '.data[] | select(.description==$d) | .repositoryId' <<<"${repos_json}")" +profileId="$(jq -r --arg d "${STAGING_DESCRIPTION}" '.data[] | select(.description==$d) | .profileId' <<<"${repos_json}")" +state="$(jq -r --arg d "${STAGING_DESCRIPTION}" '.data[] | select(.description==$d) | .type' <<<"${repos_json}")" + +if [[ -z "${repoId}" || -z "${profileId}" ]]; then + echo "ERROR: No staged repository found with description: ${STAGING_DESCRIPTION}" >&2 + exit 2 +fi +echo "Found staged repo: ${repoId} (profile: ${profileId}, state: ${state})" +if [[ "${state}" == "open" ]]; then + echo "ERROR: Staged Repo is not closed: ${STAGING_DESCRIPTION}" >&2 + exit 3 +fi + +if [[ "${state}" == "closed" ]]; then + echo "Promoting (release) ${repoId}…" + nexusApi POST "/staging/profiles/${profileId}/promote" \ + --data "$(jq -n --arg r "${repoId}" --arg d "${STAGING_DESCRIPTION}" '{data:{stagedRepositoryId:$r,description:$d}}')" +fi + +wait_for_promotion "$repoId" 600 3 + +echo "Dropping staging repository ${repoId}…" +nexusApi POST "/staging/profiles/${profileId}/drop" \ + --data "$(jq -n --arg r "$repoId" --arg d "${STAGING_DESCRIPTION}" '{data:{stagedRepositoryId:$r,description:$d}}')" + +echo "Done. Released ${repoId}." \ No newline at end of file diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 438162c..31e4c7f 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,6 +23,7 @@ on: permissions: { } env: DIST_NAME: apache-grails-intellij-plugin + GRAILS_PUBLISH_RELEASE: 'true' JAVA_DISTRIBUTION: liberica JAVA_VERSION: 21 PROJECT_NAME: Apache Grails IntelliJ Plugin @@ -57,13 +58,13 @@ jobs: with: cache-provider: basic # 'basic' uses the MIT-licensed, open-source cache provider; the default 'enhanced' provider (v6+) is proprietary (Gradle commercial Terms of Use) - name: "🏗️ Build & Test" - run: ./gradlew check buildPlugin -PpluginVersion=${VERSION} + run: ./gradlew check buildPlugin -Pversion=${VERSION} - name: "🔏 Sign plugin ZIP for JetBrains Marketplace" env: CERTIFICATE_CHAIN: ${{ secrets.CERTIFICATE_CHAIN }} PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} PRIVATE_KEY_PASSWORD: ${{ secrets.PRIVATE_KEY_PASSWORD }} - run: ./gradlew signPlugin -PpluginVersion=${VERSION} + run: ./gradlew signPlugin -Pversion=${VERSION} - name: "📦 Rename plugin ZIP to distribution name" run: cp build/distributions/Grails-${VERSION}-signed.zip ${DIST_NAME}-${VERSION}-bin.zip - name: "🔐 Set up GPG" # use env var to not expose the key @@ -84,13 +85,41 @@ jobs: ${DIST_NAME}-${VERSION}-bin.zip - name: "📦 Create plugin ZIP checksum" run: sha512sum ${DIST_NAME}-${VERSION}-bin.zip > ${DIST_NAME}-${VERSION}-bin.zip.sha512 - - name: "📤 Stage Maven artifacts to repository.apache.org" + - name: "✨ Create Staging Repository" env: - ASF_NEXUS_USERNAME: ${{ secrets.NEXUS_USERNAME }} - ASF_NEXUS_PASSWORD: ${{ secrets.NEXUS_PASSWORD }} - SIGNING_KEY: ${{ secrets.GRAILS_GPG_KEY }} - SIGNING_PASSPHRASE: ${{ secrets.GPG_PASSPHRASE }} - run: ./gradlew publishPluginZipPublicationToApacheRepository -PpluginVersion=${VERSION} + NEXUS_PUBLISH_USERNAME: ${{ secrets.NEXUS_STAGE_DEPLOYER_USER }} + NEXUS_PUBLISH_PASSWORD: ${{ secrets.NEXUS_STAGE_DEPLOYER_PW }} + NEXUS_PUBLISH_URL: ${{ vars.STAGING_URL }} + NEXUS_PUBLISH_STAGING_PROFILE_ID: ${{ secrets.STAGING_PROFILE_ID }} + NEXUS_PUBLISH_DESCRIPTION: '${{ github.event.repository.name }}:${{ env.VERSION }}' + run: ./gradlew initializeSonatypeStagingRepository -Pversion=${VERSION} + - name: "📤 Publish to Staging Repository" + env: + NEXUS_PUBLISH_USERNAME: ${{ secrets.NEXUS_STAGE_DEPLOYER_USER }} + NEXUS_PUBLISH_PASSWORD: ${{ secrets.NEXUS_STAGE_DEPLOYER_PW }} + NEXUS_PUBLISH_URL: ${{ vars.STAGING_URL }} + NEXUS_PUBLISH_STAGING_PROFILE_ID: ${{ secrets.STAGING_PROFILE_ID }} + NEXUS_PUBLISH_DESCRIPTION: '${{ github.event.repository.name }}:${{ env.VERSION }}' + SIGNING_KEY: ${{ secrets.GPG_KEY_ID }} + run: > + ./gradlew + -x initializeSonatypeStagingRepository + findSonatypeStagingRepository + publishToSonatype + -Pversion=${VERSION} + - name: "✅ Close Staging Repository" + env: + NEXUS_PUBLISH_USERNAME: ${{ secrets.NEXUS_STAGE_DEPLOYER_USER }} + NEXUS_PUBLISH_PASSWORD: ${{ secrets.NEXUS_STAGE_DEPLOYER_PW }} + NEXUS_PUBLISH_URL: ${{ vars.STAGING_URL }} + NEXUS_PUBLISH_STAGING_PROFILE_ID: ${{ secrets.STAGING_PROFILE_ID }} + NEXUS_PUBLISH_DESCRIPTION: '${{ github.event.repository.name }}:${{ env.VERSION }}' + run: > + ./gradlew + -x initializeSonatypeStagingRepository + findSonatypeStagingRepository + closeSonatypeStagingRepository + -Pversion=${VERSION} - name: "🚀 Upload plugin ZIP to GitHub Release" uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0 with: @@ -265,12 +294,11 @@ jobs: PRIVATE_KEY: ${{ secrets.PRIVATE_KEY }} PRIVATE_KEY_PASSWORD: ${{ secrets.PRIVATE_KEY_PASSWORD }} PUBLISH_TOKEN: ${{ secrets.PUBLISH_TOKEN }} - run: ./gradlew publishPlugin -PpluginVersion=${VERSION} + run: ./gradlew publishPlugin -Pversion=${VERSION} - name: "🚀 MANUAL - Release Maven staging repository" run: | echo "::group::Manual Nexus Staging Release" - echo "Log in to https://repository.apache.org, find the org.apache.grails staging" - echo "repository for ${VERSION}, and Release it (artifacts sync to Maven Central)." + echo "Run .github/scripts/releaseJarFiles.sh ${{ github.event.repository.name }}:${VERSION} <ASF_USER>" echo "::endgroup::" - name: "✅ MANUAL - Update ASF Reporter" run: | diff --git a/build.gradle.kts b/build.gradle.kts index b902dfa..d9cfd4c 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -23,12 +23,11 @@ plugins { id("org.jetbrains.kotlin.jvm") version "2.3.0" id("org.jetbrains.intellij.platform") version "2.11.0" id("org.nosphere.apache.rat") version "0.8.1" - id("maven-publish") - id("signing") + id("org.apache.grails.gradle.grails-publish") version "1.0.0-M1" } group = "org.apache.grails.intellij" -version = properties("pluginVersion") +// project version comes from gradle.properties `version` (read by grails-publish at apply time) repositories { mavenCentral() @@ -226,62 +225,35 @@ tasks.rat { outputs.upToDateWhen { false } } -// Publish the plugin ZIP to the ASF Nexus (repository.apache.org). Maven coordinates -// are independent of the JetBrains Marketplace plugin id (org.intellij.grails). -publishing { - publications { - create<MavenPublication>("pluginZip") { - groupId = "org.apache.grails" - artifactId = "grails-intellij-plugin" - version = project.version.toString() - artifact(tasks.buildPlugin) - pom { - name = "Apache Grails IntelliJ Plugin" - description = "IntelliJ IDEA plugin for the Apache Grails framework" - url = "https://github.com/apache/grails-intellij-plugin" - licenses { - license { - name = "The Apache License, Version 2.0" - url = "https://www.apache.org/licenses/LICENSE-2.0.txt" - } - } - scm { - connection = "scm:git:git://github.com/apache/grails-intellij-plugin.git" - developerConnection = "scm:git:ssh://github.com:apache/grails-intellij-plugin.git" - url = "https://github.com/apache/grails-intellij-plugin" - } - developers { - developer { - name = "Apache Grails Team" - url = "https://grails.apache.org/" - } - } - } - } - } - repositories { - maven { - name = "apache" - url = uri( - if (version.toString().endsWith("SNAPSHOT")) { - "https://repository.apache.org/content/repositories/snapshots" - } else { - "https://repository.apache.org/service/local/staging/deploy/maven2" - } - ) - credentials { - username = providers.environmentVariable("ASF_NEXUS_USERNAME").orNull - password = providers.environmentVariable("ASF_NEXUS_PASSWORD").orNull - } - } +// Publish the plugin ZIP to the ASF Nexus via the Grails Publish plugin (same flow as +// grails-core: NEXUS_PUBLISH_* env vars, initialize/publishToSonatype/close staging tasks, +// post-vote promotion with .github/scripts/releaseJarFiles.sh). Maven coordinates are +// independent of the JetBrains Marketplace plugin id (org.intellij.grails). +grailsPublish { + groupId.set("org.apache.grails") + artifactId.set("grails-intellij-plugin") + // the deliverable is the plugin ZIP, not the java component/sources/javadoc jars + addComponents.set(false) + githubSlug.set("apache/grails-intellij-plugin") + license.name = "Apache-2.0" + title.set("Apache Grails IntelliJ Plugin") + desc.set("IntelliJ IDEA plugin for the Apache Grails framework") + developer { + id.set("grails") + name.set("Apache Grails Team") } } -// GPG-sign the Maven publication (ASF release requirement); no-op when the key is absent -signing { - val signingKey = providers.environmentVariable("SIGNING_KEY").orNull - if (signingKey != null) { - useInMemoryPgpKeys(signingKey, providers.environmentVariable("SIGNING_PASSPHRASE").orNull ?: "") - sign(publishing.publications["pluginZip"]) +// grails-publish creates the `maven` publication in afterEvaluate; attach the ZIP lazily +publishing.publications.withType<MavenPublication>().configureEach { + if (name == "maven") { + artifact(tasks.buildPlugin) } } + +// grails-publish 1.0.0-M1 pom generation captures the Project and breaks the +// configuration cache; degrade gracefully for publish runs. Proper fix belongs on the +// grails-gradle-publish 1.x branch. +tasks.withType<org.gradle.api.publish.maven.tasks.GenerateMavenPom>().configureEach { + notCompatibleWithConfigurationCache("grails-publish pom generation captures Project") +} diff --git a/gradle.properties b/gradle.properties index 05a3bb4..d864f09 100644 --- a/gradle.properties +++ b/gradle.properties @@ -14,7 +14,7 @@ platformVersion=2025.3.1 platformJavaVersion=21 pluginSinceBuild=253 -pluginVersion=253.0.0 +version=253.0.0 kotlin.stdlib.default.dependency=false diff --git a/settings.gradle.kts b/settings.gradle.kts index caefd5a..da7b7f4 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -13,6 +13,37 @@ * See the License for the specific language governing permissions and * limitations under the License. */ +pluginManagement { + repositories { + // ASF repos for resolving snapshot/staged builds of the Grails Publish plugin + // ONLY — filtered like grails-core's GrailsRepoSettingsPlugin so these repos + // can never serve any other dependency. + maven { + url = uri("https://repository.apache.org/content/groups/snapshots") + content { + includeVersionByRegex("org[.]apache[.]grails[.]gradle.*", ".*", ".*-SNAPSHOT") + } + mavenContent { + snapshotsOnly() + } + } + maven { + url = uri("https://repository.apache.org/content/groups/staging") + content { + includeModuleByRegex("org[.]apache[.]grails[.]gradle", "grails-publish") + includeModuleByRegex("org[.]apache[.]groovy", "groovy.*") + // the plugins-block marker artifact + includeModule("org.apache.grails.gradle.grails-publish", "org.apache.grails.gradle.grails-publish.gradle.plugin") + } + mavenContent { + releasesOnly() + } + } + mavenCentral() // grails-publish is published to Maven Central, not the plugin portal + gradlePluginPortal() + } +} + rootProject.name = "Grails" include("copyright")
