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

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

commit 1efab952fa3928ec7fc090a5e54f00813e68fd5b
Author: James Daugherty <[email protected]>
AuthorDate: Mon Jul 13 08:21:27 2026 -0400

    Fix command ordering in profile.yml
---
 .../profiles/tasks/ProfileCompilerTask.groovy      |  4 +-
 .../profiles/tasks/ProfileCompilerTaskSpec.groovy  | 72 ++++++++++++++++++++++
 2 files changed, 75 insertions(+), 1 deletion(-)

diff --git 
a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/profiles/tasks/ProfileCompilerTask.groovy
 
b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/profiles/tasks/ProfileCompilerTask.groovy
index 1cb990f892..78ba09bc79 100644
--- 
a/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/profiles/tasks/ProfileCompilerTask.groovy
+++ 
b/grails-gradle/plugins/src/main/groovy/org/grails/gradle/plugin/profiles/tasks/ProfileCompilerTask.groovy
@@ -206,7 +206,9 @@ abstract class ProfileCompilerTask extends AbstractCompile {
             f.name.endsWith('.yml')
         } ?: []) as List<File>
 
-        Map<String, String> commandNames = [:]
+        // Use a sorted map so the generated commands ordering is deterministic
+        // (the file tree iteration order is filesystem-dependent) and the 
build is reproducible
+        Map<String, String> commandNames = new TreeMap<>()
         for (File f in groovySourceFiles) {
             def fn = f.name
             commandNames.put(fn - '.groovy', fn)
diff --git 
a/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/profiles/tasks/ProfileCompilerTaskSpec.groovy
 
b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/profiles/tasks/ProfileCompilerTaskSpec.groovy
new file mode 100644
index 0000000000..58dde37c0c
--- /dev/null
+++ 
b/grails-gradle/plugins/src/test/groovy/org/grails/gradle/plugin/profiles/tasks/ProfileCompilerTaskSpec.groovy
@@ -0,0 +1,72 @@
+/*
+ *  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 org.grails.gradle.plugin.profiles.tasks
+
+import org.gradle.api.Project
+import org.gradle.testfixtures.ProjectBuilder
+import org.yaml.snakeyaml.Yaml
+import spock.lang.Specification
+import spock.lang.TempDir
+
+/**
+ * Unit-level tests for {@link ProfileCompilerTask} using {@link 
ProjectBuilder}.
+ *
+ * <p>Verifies that the generated {@code profile.yml} is reproducible - the 
{@code commands}
+ * entries are emitted in a deterministic (sorted) order regardless of the 
filesystem's
+ * directory iteration order.</p>
+ *
+ * @since 8.0
+ */
+class ProfileCompilerTaskSpec extends Specification {
+
+    @TempDir
+    File tmpDir
+
+    def "commands are written to profile.yml in a deterministic sorted 
order"() {
+        given: 'a commands directory populated in a non-alphabetical order'
+        Project project = 
ProjectBuilder.builder().withProjectDir(tmpDir).build()
+        File commandsDir = new File(tmpDir, 'commands')
+        commandsDir.mkdirs()
+        // Intentionally create the files out of alphabetical order
+        ['run-app.groovy', 'assemble.groovy', 'clean.yml', 'test-app.groovy', 
'compile.yml', 'add-property.groovy'].each {
+            new File(commandsDir, it) << 'description "test"\n'
+        }
+
+        and: 'a ProfileCompilerTask pointed at that directory'
+        ProfileCompilerTask task = project.tasks.create('compileProfile', 
ProfileCompilerTask)
+        task.commandsDirectory.set(commandsDir)
+        task.templatesDirectory.set((File) null)
+        task.skeletonDirectory.set((File) null)
+        task.classpath = project.files()
+
+        when: 'the profile is generated'
+        task.execute()
+
+        then: 'the commands map is present and sorted alphabetically by 
command name'
+        Map profileData = new 
Yaml().load(task.profileFile.get().asFile.newReader())
+        Map commands = (Map) profileData.commands
+        new ArrayList(commands.keySet()) == ['add-property', 'assemble', 
'clean', 'compile', 'run-app', 'test-app']
+
+        and: 'each command maps to its source file name'
+        commands['add-property'] == 'add-property.groovy'
+        commands['clean'] == 'clean.yml'
+        commands['compile'] == 'compile.yml'
+        commands['run-app'] == 'run-app.groovy'
+    }
+}

Reply via email to