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