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'))
+ }
+ }
+}