You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@grails.apache.org by ja...@apache.org on 2026/05/03 11:58:49 UTC

(grails-static-website) 09/09: Add recordCompanionRelease Gradle task and matching release-companion workflow

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

jamesfredley pushed a commit to branch multi-version-support
in repository https://gitbox.apache.org/repos/asf/grails-static-website.git

commit e64bca93f3b6f4b06a1bddeeee7589f85e1969d9
Author: James Fredley <ja...@users.noreply.github.com>
AuthorDate: Sat May 2 22:25:09 2026 -0400

    Add recordCompanionRelease Gradle task and matching release-companion workflow
    
    Companion artifacts (grails-spring-security, grails-redis, grails-quartz, grails-github-actions, grails-publish, ...) ship independently of Grails core, so each one has its own release cadence and needs its own bump path now that their versions live in conf/releases.yml's companionArtifacts: section.
    
    RecordCompanionReleaseTask performs a line-based update of the existing entry's 'version:' field so comments and indentation in releases.yml are preserved (a SnakeYAML round-trip would erase them). It locates the right block by walking the file looking for the 'companionArtifacts:' key, then the matching 'N':' major key, then the matching 'artifactId: <name>' entry, then rewrites the next 'version:' line. Smoke-tested by bumping grails-redis 5.0.1 -> 5.0.2 and confirming the diff is e [...]
    
    First-time addition of a brand-new companion artifact still requires a manual edit of releases.yml (mirrorDirectory, releaseNotesRepo, and displayName all need PMC-supplied values that vary per plugin) - the task fails with a clear error message in that case rather than guessing.
    
    release-companion.yml is a parallel GitHub Actions workflow modelled on release.yml: workflow_dispatch with grails_major + artifact_id + artifact_version inputs, runs the new task, then commits and pushes the single-line diff. The existing release.yml workflow is unchanged and still drives Grails core releases.
    
    Assisted-by: claude-code:claude-opus-4-7
---
 .github/workflows/release-companion.yml            |  46 +++++
 .../website/gradle/GrailsWebsitePlugin.groovy      |   7 +
 .../gradle/tasks/RecordCompanionReleaseTask.groovy | 186 +++++++++++++++++++++
 3 files changed, 239 insertions(+)

diff --git a/.github/workflows/release-companion.yml b/.github/workflows/release-companion.yml
new file mode 100644
index 00000000000..e35036905b5
--- /dev/null
+++ b/.github/workflows/release-companion.yml
@@ -0,0 +1,46 @@
+name: Release Companion Artifact
+on:
+  workflow_dispatch:
+    inputs:
+      grails_major:
+        description: 'Grails major version this companion ships under (e.g. 7, 8)'
+        required: true
+      artifact_id:
+        description: 'Companion artifactId, e.g. grails-redis, grails-spring-security'
+        required: true
+      artifact_version:
+        description: 'New companion version (e.g. 5.0.2)'
+        required: true
+jobs:
+  release-companion:
+    runs-on: ubuntu-latest
+    env:
+      GIT_USER_NAME: grails-build
+      GIT_USER_EMAIL: grails-build@users.noreply.github.com
+    steps:
+      - name: Checkout repository
+        uses: actions/checkout@v4
+        with:
+          token: ${{ secrets.GITHUB_TOKEN }}
+      - name: Set up JDK 17
+        uses: actions/setup-java@v4
+        with:
+          distribution: temurin
+          java-version: 17
+      - name: Record companion release
+        run: |
+          ./gradlew --no-daemon recordCompanionRelease \
+            -PgrailsMajor=${{ github.event.inputs.grails_major }} \
+            -PartifactId=${{ github.event.inputs.artifact_id }} \
+            -PartifactVersion=${{ github.event.inputs.artifact_version }}
+      - name: Commit companion release
+        run: |
+          git config user.name ${GIT_USER_NAME}
+          git config user.email ${GIT_USER_EMAIL}
+          git add conf/releases.yml
+          git commit -m "Update conf/releases.yml" -m "Bump ${ARTIFACT} to ${VERSION} for Grails ${MAJOR}"
+          git push
+        env:
+          MAJOR: ${{ github.event.inputs.grails_major }}
+          ARTIFACT: ${{ github.event.inputs.artifact_id }}
+          VERSION: ${{ github.event.inputs.artifact_version }}
diff --git a/buildSrc/src/main/groovy/website/gradle/GrailsWebsitePlugin.groovy b/buildSrc/src/main/groovy/website/gradle/GrailsWebsitePlugin.groovy
index fe7277a6a3f..a6f0d6aa672 100644
--- a/buildSrc/src/main/groovy/website/gradle/GrailsWebsitePlugin.groovy
+++ b/buildSrc/src/main/groovy/website/gradle/GrailsWebsitePlugin.groovy
@@ -42,6 +42,7 @@ import website.gradle.tasks.PluginsTask
 import website.gradle.tasks.ProfilesTask
 import website.gradle.tasks.PublishMainSiteTask
 import website.gradle.tasks.QuestionsTask
+import website.gradle.tasks.RecordCompanionReleaseTask
 import website.gradle.tasks.RecordReleaseTask
 import website.gradle.tasks.RenderSiteTask
 import website.gradle.tasks.SitemapTask
@@ -131,6 +132,12 @@ class GrailsWebsitePlugin implements Plugin<Project> {
         // `./gradlew recordRelease -PreleaseVersion=X.Y.Z`.
         RecordReleaseTask.register(project)
 
+        // Bumps the version of an existing companion artifact entry under
+        // companionArtifacts:'N':. The release-companion.yml workflow invokes
+        // `./gradlew recordCompanionRelease -PgrailsMajor=N -PartifactId=name
+        //  -PartifactVersion=X.Y.Z` on each plugin's release day.
+        RecordCompanionReleaseTask.register(project)
+
         // Validates conf/guides.yml against the schema.
         // `-PvalidationMode=shape|existence|both` selects the rule set.
         ValidateGuidesTask.register(project)
diff --git a/buildSrc/src/main/groovy/website/gradle/tasks/RecordCompanionReleaseTask.groovy b/buildSrc/src/main/groovy/website/gradle/tasks/RecordCompanionReleaseTask.groovy
new file mode 100644
index 00000000000..8198a6767bd
--- /dev/null
+++ b/buildSrc/src/main/groovy/website/gradle/tasks/RecordCompanionReleaseTask.groovy
@@ -0,0 +1,186 @@
+/*
+ *  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.
+ */
+package website.gradle.tasks
+
+import groovy.transform.CompileStatic
+
+import org.gradle.api.DefaultTask
+import org.gradle.api.GradleException
+import org.gradle.api.Project
+import org.gradle.api.file.RegularFileProperty
+import org.gradle.api.provider.Property
+import org.gradle.api.tasks.Input
+import org.gradle.api.tasks.OutputFile
+import org.gradle.api.tasks.TaskAction
+import org.gradle.api.tasks.TaskProvider
+
+/**
+ * Bumps the {@code version:} field of an existing companion artifact entry in
+ * {@code conf/releases.yml}. Companion artifacts (Spring Security, Redis,
+ * Quartz, GitHub Actions, Gradle Publish, ...) ship independently of Grails
+ * core, so each one has its own release cadence and needs its own bump task.
+ *
+ * <p>Invoked from {@code .github/workflows/release-companion.yml} on a
+ * {@code workflow_dispatch}, with three inputs:
+ * <pre>
+ *   ./gradlew recordCompanionRelease \
+ *       -PgrailsMajor=7 \
+ *       -PartifactId=grails-redis \
+ *       -PartifactVersion=5.0.2
+ * </pre>
+ *
+ * <p>The task performs a line-based update so existing comments and
+ * indentation in {@code releases.yml} are preserved (a YAML round-trip via
+ * SnakeYAML would erase them). It looks for the {@code 'N':} block under
+ * {@code companionArtifacts:}, then within that block locates the entry whose
+ * {@code artifactId:} matches and rewrites the next {@code version:} line.
+ *
+ * <p>This task only updates an existing entry. First-time addition of a brand
+ * new companion artifact for a major requires a manual edit of
+ * {@code conf/releases.yml} (e.g. a one-time PR when a new plugin is
+ * provisioned by the PMC), since the schema of a new entry can vary
+ * (mirrorDirectory, releaseNotesRepo, displayName all need PMC-supplied
+ * values).
+ */
+@CompileStatic
+abstract class RecordCompanionReleaseTask extends DefaultTask {
+
+    static final String NAME = 'recordCompanionRelease'
+    static final String GROUP = 'release'
+
+    @Input
+    abstract Property<String> getGrailsMajor()
+
+    @Input
+    abstract Property<String> getArtifactId()
+
+    @Input
+    abstract Property<String> getArtifactVersion()
+
+    @OutputFile
+    abstract RegularFileProperty getReleasesYaml()
+
+    @TaskAction
+    void recordCompanionRelease() {
+        String major = grailsMajor.get()?.trim()
+        String artifact = artifactId.get()?.trim()
+        String version = artifactVersion.get()?.trim()
+        if (!major || !artifact || !version) {
+            throw new GradleException(
+                    'recordCompanionRelease requires -PgrailsMajor=<N> -PartifactId=<name> -PartifactVersion=<X.Y.Z>.')
+        }
+
+        File yaml = releasesYaml.get().asFile
+        if (!yaml.isFile()) {
+            throw new GradleException(
+                    "Expected ${yaml.absolutePath} to exist. Did the working tree drift?".toString())
+        }
+
+        List<String> lines = yaml.readLines('UTF-8')
+        boolean inCompanionSection = false
+        boolean inMajorSection = false
+        boolean foundArtifact = false
+        boolean updated = false
+        String majorMarker = "'${major}':".toString()
+        String artifactMarker = "artifactId: ${artifact}".toString()
+
+        for (int i = 0; i < lines.size(); i++) {
+            String line = lines[i]
+            String trimmed = line.trim()
+            // Strip the YAML list-item prefix "- " so we can compare against
+            // an artifactId without worrying about which entry happens to be
+            // the first (sequence-bullet-bearing) one in the major's block.
+            String normalized = trimmed.startsWith('- ') ? trimmed.substring(2) : trimmed
+
+            if (trimmed == 'companionArtifacts:' || trimmed.startsWith('companionArtifacts:')) {
+                inCompanionSection = true
+                continue
+            }
+
+            // Leaving the companionArtifacts: section once we hit another
+            // top-level key (a non-blank line with no leading whitespace).
+            if (inCompanionSection && trimmed && !line.startsWith(' ') && !line.startsWith('\t')
+                    && !trimmed.startsWith('#')) {
+                inCompanionSection = false
+                inMajorSection = false
+            }
+
+            if (inCompanionSection && trimmed.startsWith(majorMarker)) {
+                inMajorSection = true
+                continue
+            }
+
+            // Within companionArtifacts, hitting another quoted single-segment
+            // major key (e.g. "'8':") means we've left our major.
+            if (inMajorSection && trimmed ==~ /^'\d+':$/ && trimmed != majorMarker) {
+                inMajorSection = false
+            }
+
+            if (inMajorSection && !foundArtifact && normalized == artifactMarker) {
+                foundArtifact = true
+                continue
+            }
+
+            if (foundArtifact && trimmed.startsWith('version:')) {
+                // Preserve the original indentation; rewrite only the value
+                // and (re-apply) single-quote wrapping for consistency with
+                // the rest of the companion entries written by the previous
+                // commit's data migration.
+                String leadingWs = ''
+                for (int j = 0; j < line.length(); j++) {
+                    char ch = line.charAt(j)
+                    if (ch == ' ' as char || ch == '\t' as char) {
+                        leadingWs += ch
+                    } else {
+                        break
+                    }
+                }
+                lines[i] = "${leadingWs}version: '${version}'".toString()
+                updated = true
+                break
+            }
+        }
+
+        if (!updated) {
+            throw new GradleException(
+                    "Could not find companionArtifact entry artifactId=${artifact} under Grails major '${major}' in ${yaml.name}. ".toString() +
+                            'For a brand-new companion artifact, add the entry to conf/releases.yml manually first.')
+        }
+
+        yaml.text = lines.join(System.lineSeparator()) + System.lineSeparator()
+        logger.lifecycle("Updated companion ${artifact} to ${version} for Grails ${major} in ${yaml.name}.")
+    }
+
+    static TaskProvider<RecordCompanionReleaseTask> register(Project project) {
+        project.tasks.register(NAME, RecordCompanionReleaseTask) { task ->
+            task.group = GROUP
+            task.description =
+                    'Bump a companion artifact version in conf/releases.yml. ' +
+                            'Pass via -PgrailsMajor=N -PartifactId=name -PartifactVersion=X.Y.Z.'
+            task.grailsMajor.set(
+                    project.providers.gradleProperty('grailsMajor').orElse(''))
+            task.artifactId.set(
+                    project.providers.gradleProperty('artifactId').orElse(''))
+            task.artifactVersion.set(
+                    project.providers.gradleProperty('artifactVersion').orElse(''))
+            task.releasesYaml.set(
+                    project.rootProject.layout.projectDirectory.file('conf/releases.yml'))
+        }
+    }
+}