This is an automated email from the ASF dual-hosted git repository.

jdaugherty pushed a commit to branch 7.0.x
in repository https://gitbox.apache.org/repos/asf/grails-core.git

commit 508bfa1a270201137a0fb7f6afd7c736b3472fbf
Author: James Daugherty <jdaughe...@jdresources.net>
AuthorDate: Wed Sep 10 13:55:26 2025 -0400

    chore: add scripts to help automate post-vote processes
---
 .asf.yaml                               |   7 ++
 .github/scripts/releaseDistributions.sh |  83 +++++++++++++++++++
 .github/scripts/releaseJarFiles.sh      | 141 ++++++++++++++++++++++++++++++++
 .github/vote_templates/groovy_pmc.txt   |   1 +
 .github/vote_templates/staged.txt       |   7 +-
 .github/workflows/release.yml           | 108 ++++++++++++++++++------
 6 files changed, 317 insertions(+), 30 deletions(-)

diff --git a/.asf.yaml b/.asf.yaml
index 92fe9f6c4a..60f332e399 100644
--- a/.asf.yaml
+++ b/.asf.yaml
@@ -18,6 +18,13 @@
 github:
   # deployment environments to delay releasing until the necessary steps have 
completed
   environments:
+    close:
+      required_reviewers:
+        - id: grails-committers
+          type: Team
+        - id: jdaugherty
+          type: User
+      wait_timer: 0
     docs:
       required_reviewers:
         - id: grails-committers
diff --git a/.github/scripts/releaseDistributions.sh 
b/.github/scripts/releaseDistributions.sh
new file mode 100755
index 0000000000..d935db6ea6
--- /dev/null
+++ b/.github/scripts/releaseDistributions.sh
@@ -0,0 +1,83 @@
+#!/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> <username> <password>
+
+set -euo pipefail
+
+if [[ $# -ne 3 ]]; then
+  echo "Usage: $0 <tag> <username> <password>" >&2
+  exit 1
+fi
+
+RELEASE_TAG="$1"
+RELEASE_VERSION="${RELEASE_TAG#v}"
+SVN_USER="$2"
+SVN_PASS="$3"
+RELEASE_ROOT="https://dist.apache.org/repos/dist/release/incubator/grails/core";
+DEV_ROOT="https://dist.apache.org/repos/dist/dev/incubator/grails/core";
+
+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
+
+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
+}
+
+old_release_folder="$(svn ls "${svn_flags[@]}" "${RELEASE_ROOT}" | awk -F/ 
'NF{print $1; exit}')"
+if [[ -n "${old_release_folder}" ]]; then
+  PRIOR_RELEASE_URL="${RELEASE_ROOT}/${old_release_folder}"
+  echo "đŸ—‘ī¸ Deleting old release folder: ${PRIOR_RELEASE_URL}"
+  svn rm "${svn_flags[@]}" -m "Remove previous release ${old_release_folder}" 
"${PRIOR_RELEASE_URL}"
+  echo "✅ Deleted old release folder"
+else
+  echo "â„šī¸ No existing release subfolder found under ${RELEASE_ROOT}"
+fi
+
+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 (incubating) 
${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 0000000000..8421782d74
--- /dev/null
+++ b/.github/scripts/releaseJarFiles.sh
@@ -0,0 +1,141 @@
+#!/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> <password>
+
+set -euo pipefail
+
+if [[ $# -ne 3 ]]; then
+  echo "Usage: $0 <staging repo description> <username> <password>" >&2
+  exit 1
+fi
+
+NEXUS_URL="https://repository.apache.org";
+STAGING_DESCRIPTION="$1"
+NEXUS_USER="$2"
+NEXUS_PASS="$3"
+
+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
+
+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/vote_templates/groovy_pmc.txt 
b/.github/vote_templates/groovy_pmc.txt
index 14a16d2ad5..b205127d9c 100644
--- a/.github/vote_templates/groovy_pmc.txt
+++ b/.github/vote_templates/groovy_pmc.txt
@@ -1,4 +1,5 @@
 Hi Everyone,
+
 The Apache Grails community has voted to approve the release of Apache Grails 
${VERSION}.
 
 As the incubation host, we now kindly request the Groovy PMC to review & 
approve our ASF release.
diff --git a/.github/vote_templates/staged.txt 
b/.github/vote_templates/staged.txt
index 63f2f1e575..af21ff9f42 100644
--- a/.github/vote_templates/staged.txt
+++ b/.github/vote_templates/staged.txt
@@ -4,12 +4,7 @@ I am happy to start the VOTE thread for an Apache Grails 
(incubating) release of
 
 
 Please note the following build reproducibility issues for this release:
-1. grails-cli-7.0.0-RC1-all.jar # caused by extension module merge from the 
shadowJar (need to strip date from property file) & the plugin.xml signature is 
being included
-- Created https://github.com/apache/grails-core/issues/14951 to address this 
in a future release
-2. grails-converters-7.0.0-RC1.jar # contents identical on decompile
-3. grails-datamapping-core-test-7.0.0-RC1-tests.jar # contents identical on 
decompile
-4. grails-datamapping-tck-7.0.0-RC1.jar # contents identical on decompile
-5. grails-forge-core-7.0.0-RC1-javadoc.jar # due to groovy 3 in forge, we need 
to update to groovy 4 to fix this
+1. grails-forge-core-${VERSION}-javadoc.jar # due to groovy 3 in forge, we 
need to update to groovy 4 to fix this
 
 Given that these differences are known, explainable, and immaterial, we are 
moving forward with a release vote.
 
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 9b8dc39731..9f7f35ecf1 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -23,7 +23,7 @@ env:
   GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
 jobs:
   publish:
-    name: "Stage Files"
+    name: "Stage Jar Files"
     permissions:
       contents: write  #  to create release & upload wrapper
       issues: write  #  to modify milestones
@@ -361,6 +361,7 @@ jobs:
           set -e
           gh release --repo ${{ github.repository }} delete-asset v${{ 
needs.publish.outputs.release_version }} PUBLISHED_ARTIFACTS.txt --yes
   upload:
+    name: "Upload Distributions to dist.apache.org"
     needs: [ publish, source ]
     runs-on: ubuntu-24.04
     permissions:
@@ -470,7 +471,7 @@ jobs:
           VERSION_COMMIT_ID: ${{ needs.publish.outputs.commit_hash }}
         run: |
           export DIST_SVN_REVISION=$(awk '/Last Changed Rev:/ {print $4}' 
dev-repo/DIST_SVN_REVISION.txt)
-          
+          echo "::group::Grails PPMC Vote Email"
           echo "*************************************************"
           echo "Subject: [VOTE] Release Apache Grails (incubating) ${VERSION}"
           echo "*************************************************"
@@ -478,6 +479,7 @@ jobs:
           echo "*************************************************"
           cat grails-core/.github/vote_templates/staged.txt | envsubst
           echo "*************************************************"
+          echo "::endgroup::"
       - name: 'Groovy Vote Email'
         env:
           VERSION: ${{ needs.publish.outputs.release_version }}
@@ -485,6 +487,7 @@ jobs:
         run: |
           export DIST_SVN_REVISION=$(awk '/Last Changed Rev:/ {print $4}' 
dev-repo/DIST_SVN_REVISION.txt)
           
+          echo "::group::Groovy PMC Vote Email"
           echo "*************************************************"
           echo "Subject: [VOTE] Approval of Apache Grails (incubating) 
${VERSION} release by Groovy PMC"
           echo "*************************************************"
@@ -492,25 +495,60 @@ jobs:
           echo "*************************************************"
           cat grails-core/.github/vote_templates/groovy_pmc.txt | envsubst
           echo "*************************************************"
-      - name: 'Announcement Email'
-        env:
-          VERSION: ${{ needs.publish.outputs.release_version }}
-          VERSION_COMMIT_ID: ${{ needs.publish.outputs.commit_hash }}
-          PREVIOUS_VERSION: 'TODO_PREVIOUS_VERSION'
-        run: |
-          export DIST_SVN_REVISION=$(awk '/Last Changed Rev:/ {print $4}' 
dev-repo/DIST_SVN_REVISION.txt)
-          
-          echo "*************************************************"
-          echo "Subject: [ANNOUNCE] Apache Grails (incubating) ${VERSION}"
-          echo "*************************************************"
-          echo "Body:"
-          echo "*************************************************"
-          cat grails-core/.github/vote_templates/announce.txt | envsubst
-          echo "*************************************************"
+          echo "::endgroup::"
+  release:
+    environment: release
+    name: "VOTE SUCCEEDED - Release Artifacts"
+    needs: [ publish, source, upload ]
+    runs-on: ubuntu-24.04
+    steps:
+      - name: "Output Agent IP" # in the event RAO blocks this agent, this can 
be used to debug it
+        run: curl -s https://api.ipify.org
+      - name: "Setup SVN and Tools"
+        run: sudo apt-get install -y subversion subversion-tools tree 
gettext-base
+      - name: "đŸ—ŗ Grails PPMC Vote Confirmation - MANUAL"
+        run: |
+          echo "::group::Manual Confirmation"
+          echo "🔎 This step is a placeholder that the vote confirmation on 
d...@grails.apache.org completed successfully."
+          echo "::endgroup::"
+      - name: "đŸ—ŗ Groovy PMC Vote Confirmation - MANUAL"
+        run: |
+          echo "::group::Manual Confirmation"
+          echo "🔎 This step is a placeholder that the vote confirmation on 
d...@grails.apache.org completed successfully."
+          echo "::endgroup::"
+      - name: "🚀 Release JAR files - MANUAL"
+        run: |
+          echo "::group::Manual Jar Promotion"
+          echo "Run .github/scripts/releaseJarFiles.sh 'v${{ 
needs.publish.outputs.release_version }}' '${{ 
needs.publish.outputs.extract_repository_name }}:${{ 
needs.publish.outputs.release_version }}' ASF_USER ASF_PASS"
+          echo "::endgroup::"
+      - name: "🚀 Release distribution artifacts - MANUAL"
+        run: |
+          echo "::group::Manual ASF Artifact Promotion"
+          echo "Run github/scripts/releaseDistributions.sh 'v${{ 
needs.publish.outputs.release_version }}' ASF_USER ASF_PASS"
+          echo "::endgroup::"
+      - name: "✅ Update ASF Reporter - MANUAL"
+        run: |
+          echo "::group::Manual ASF Reporter Update"
+          TODAY=$(date +"%Y-%m-%d")
+          echo "Check email & update https://reporter.apache.org to mark the 
release ${{ needs.publish.outputs.release_version }} as complete as of ${TODAY}"
+          echo "Note: this is a place holder; currently this is not possible 
since Groovy sponsors us instead of the incubator PMC"
+          echo "::endgroup::"
+      - name: "✅ Deploy grails-forge - MANUAL"
+        run: |
+          echo "::group::Manual grails-forge deployment"
+          echo "Kick off Forge-* workflow for version ${{ 
needs.publish.outputs.release_version }} based on the below rules:"
+          cat <<'EOF'
+          * RELEASE - GA releases only
+          * NEXT - Milestones and Release Candidate
+          * SNAPSHOT - current or next version snapshot
+          * PREV - previous release version
+          * PREV-SNAPSHOT - previous version snapshot
+          EOF
+          echo "::endgroup::"
   docs:
     environment: docs
-    name: "Publish Documentation"
-    needs: [ publish, source, upload ]
+    name: "VOTE SUCCEEDED - Publish Documentation"
+    needs: [ publish, source, upload ] # TODO Once we have confirmed release 
won't fail, add it as a dependency here
     runs-on: ubuntu-24.04
     steps:
       - name: "đŸ“Ĩ Checkout repository"
@@ -544,9 +582,9 @@ jobs:
           VERSION: ${{ needs.publish.outputs.release_version }}
   sdkman:
     environment: sdkman
-    name: "Release to SDKMAN!"
+    name: "VOTE SUCCEEDED - Release to SDKMAN!"
     runs-on: ubuntu-24.04
-    needs: [ publish, source, upload ]
+    needs: [ publish, source, upload ] # TODO Once we have confirmed release 
won't fail, add it as a dependency here
     steps:
       - name: "đŸ“Ĩ Checkout repository"
         uses: actions/checkout@v4
@@ -576,10 +614,10 @@ jobs:
         env:
           GVM_SDKVENDOR_KEY: ${{ secrets.GVM_SDKVENDOR_KEY }}
           GVM_SDKVENDOR_TOKEN: ${{ secrets.GVM_SDKVENDOR_TOKEN }}
-  release:
-    name: "Close Release"
+  close:
+    name: "VOTE SUCCEEDED - Close Release"
     environment: release
-    needs: [ publish, source, upload, docs, sdkman ]
+    needs: [ publish, source, upload, docs, sdkman ] # TODO Once we have 
confirmed release won't fail, add it as a dependency here
     runs-on: ubuntu-24.04
     permissions:
       contents: write # required for gradle.properties revert
@@ -606,3 +644,25 @@ jobs:
         uses: apache/grails-github-actions/post-release@asf
         env:
           RELEASE_SCRIPT_PATH: '.github/scripts/setSnapshotGrailsVersion.sh'
+      - name: '🌎 Create Blog Post - MANUAL'
+        run: |
+          echo "::group::Blog Post Creation - MANUAL"
+          echo "Publish a blog post on https://grails.apache.org/blog/ about 
the new release ${{ needs.publish.outputs.release_version }} using the repo 
https://github.com/apache/grails-static-website";
+          echo "::endgroup::"
+      - name: 'Announcement Email'
+        env:
+          VERSION: ${{ needs.publish.outputs.release_version }}
+          VERSION_COMMIT_ID: ${{ needs.publish.outputs.commit_hash }}
+          PREVIOUS_VERSION: 'TODO_PREVIOUS_VERSION'
+        run: |
+          export DIST_SVN_REVISION=$(awk '/Last Changed Rev:/ {print $4}' 
dev-repo/DIST_SVN_REVISION.txt)
+          
+          echo "::group::Announcement Email"
+          echo "*************************************************"
+          echo "Subject: [ANNOUNCE] Apache Grails (incubating) ${VERSION}"
+          echo "*************************************************"
+          echo "Body:"
+          echo "*************************************************"
+          cat grails-core/.github/vote_templates/announce.txt | envsubst
+          echo "*************************************************"
+          echo "::endgroup::"

Reply via email to