codeconsole commented on code in PR #39:
URL: 
https://github.com/apache/grails-gradle-publish/pull/39#discussion_r4116562416


##########
plugin/src/main/groovy/org/apache/grails/gradle/publish/GrailsPublishGradlePlugin.groovy:
##########
@@ -806,6 +931,28 @@ Note: if project properties are used, the properties must 
be defined prior to ap
         ] : null
     }
 
+    /**
+     * The extra artifact usually lives in a classes directory, such as 
META-INF/grails-plugin.xml. Publishing it from
+     * there would write its signature into that directory, which other tasks 
read, such as the jar tasks, so publish a
+     * copy instead.
+     */
+    private static void addExtraArtifact(Project project, MavenPublication 
publication, Map<String, String> extraArtifact) {
+        File source = new File(extraArtifact.source)
+        TaskProvider<Copy> copyTask = 
project.tasks.register('grailsPublishExtraArtifact', Copy) { Copy copy ->
+            copy.from(source)
+            
copy.into(project.layout.buildDirectory.dir('grails-publish/extra-artifact'))
+            // the file is written by the task producing the classes directory 
it lives in, and the jar tasks package
+            // those directories, so depending on them builds it
+            copy.dependsOn(project.tasks.withType(Jar))

Review Comment:
   Looks good, thanks. The dry-run test is a good guard, since it fails with 
the old `withType(Jar)` dependency.



##########
plugin/src/main/groovy/org/apache/grails/gradle/publish/GrailsPublishGradlePlugin.groovy:
##########
@@ -137,89 +171,127 @@ The credentials and connection url must be specified as 
a project property or an
 
 When using `NEXUS_PUBLISH`, either the property `signing.secretKeyRingFile` 
must be set to the path of the GPG keyring file or local gpg must be configured 
to sign artifacts.
 
-Note: if project properties are used, the properties must be defined prior to 
applying this plugin.
+Note: properties are read from the root project's gradle.properties, the one 
in the Gradle user home, -P or ORG_GRADLE_PROJECT_ environment variables, or 
from the project applying this plugin: its own gradle.properties, or its build 
script before the plugin is applied. Properties set on parent projects, 
including in the gradle.properties of a parent project's directory, are not 
read.
 """
     }
 
+    /**
+     * Finds a property set via `ext` on the given project itself, or a Gradle 
property.
+     *
+     * The project's own extra properties are checked first, so a value set in 
its build script overrides a Gradle property,
+     * as it does with {@link Project#findProperty}. Properties set on parent 
projects are deliberately not read: resolving
+     * them implicitly is removed in Gradle 10, and reading them explicitly is 
not allowed with Isolated Projects.
+     */
+    @PackageScope
+    static Object findProjectProperty(Project project, String name) {
+        def extraProperties = project.extensions.extraProperties
+        if (extraProperties.has(name)) {
+            return extraProperties.get(name)
+        }
+        project.providers.gradleProperty(name).orNull
+    }
+
+    /**
+     * Finds a publish type property. These fall back to a default when unset, 
so a value that is only set on a parent
+     * project, which is no longer read, would silently change where artifacts 
are published. Fail the build instead.
+     */
+    private Object findPublishTypeProperty(Project project, String name) {
+        Object value = findProjectProperty(project, name)
+        // with Isolated Projects, parent projects cannot be inspected, and 
such builds never relied on reading them
+        if (value == null && !buildFeatures.isolatedProjects.active.get()) {
+            for (Project parent = project.parent; parent != null; parent = 
parent.parent) {
+                if (parent.extensions.extraProperties.has(name)) {
+                    throw new InvalidUserDataException("The property `${name}` 
is set on ${parent} but not on ${project}. " +
+                            'The Grails Publish plugin does not read 
properties from parent projects. ' +
+                            "Set `${name}` in the root project's 
gradle.properties, with -P${name}=..., or on ${project} " +
+                            'itself, in its gradle.properties or in its build 
script before applying the plugin.')
+                }
+            }
+        }
+        value
+    }
+
     @Override
     void apply(Project project) {
-        project.rootProject.logger.info("Applying Grails Publish Gradle Plugin 
for `${project.name}`...");
+        LOG.info('Applying Grails Publish Gradle Plugin for `{}`...', 
project.name)
         if (project.extensions.findByName('grailsPublish') == null) {
             project.extensions.create('grailsPublish', GrailsPublishExtension)
         }
-        final String nexusPublishUrl = project.findProperty('nexusPublishUrl') 
?: System.getenv('NEXUS_PUBLISH_URL') ?: ''
-        final String nexusPublishSnapshotUrl = 
project.findProperty('nexusPublishSnapshotUrl') ?: 
System.getenv('NEXUS_PUBLISH_SNAPSHOT_URL') ?: ''
-        final String nexusPublishUsername = 
project.findProperty('nexusPublishUsername') ?: 
System.getenv('NEXUS_PUBLISH_USERNAME') ?: ''
-        final String nexusPublishPassword = 
project.findProperty('nexusPublishPassword') ?: 
System.getenv('NEXUS_PUBLISH_PASSWORD') ?: ''
-        final String nexusPublishStagingProfileId = 
project.findProperty('nexusPublishStagingProfileId') ?: 
System.getenv('NEXUS_PUBLISH_STAGING_PROFILE_ID') ?: ''
-        final String nexusPublishDescription = 
project.findProperty('nexusPublishDescription') ?: 
System.getenv('NEXUS_PUBLISH_DESCRIPTION') ?: ''
+        final String nexusPublishUrl = findProjectProperty(project, 
'nexusPublishUrl') ?: System.getenv('NEXUS_PUBLISH_URL') ?: ''
+        final String nexusPublishSnapshotUrl = findProjectProperty(project, 
'nexusPublishSnapshotUrl') ?: System.getenv('NEXUS_PUBLISH_SNAPSHOT_URL') ?: ''

Review Comment:
   Thanks, this covers the case I was worried about. One follow-up: the check 
runs even when the project doesn't publish through Nexus, and the new test 
shows it. `properties-from-parent-project` is a snapshot with the default 
publish types, so it publishes with Maven, yet `:subproject:assemble` fails. A 
build that keeps `ext.nexusPublishUrl` on its root would fail every snapshot 
and local build, such as `test`, not only the release that uses the URL.
   
   The URLs are only used inside `if (useNexusPublish)`, so they could be read 
there, once `useNexusPublish` is known. A release would still fail before 
anything reaches oss.sonatype.org. Failing on every build does surface the 
migration sooner, though, so I'll leave that call to you.
   
   A smaller one: for these two properties, the error message could mention 
`NEXUS_PUBLISH_URL` / `NEXUS_PUBLISH_SNAPSHOT_URL`, since setting the 
environment variable fixes it too.
   
   Neither blocks the merge for me.



##########
plugin/src/main/groovy/org/apache/grails/gradle/publish/GrailsPublishExtension.groovy:
##########
@@ -194,7 +197,9 @@ class GrailsPublishExtension {
         testRepositoryPath = objects.directoryProperty().convention(null as 
Directory)
         pomCustomization = objects.property(Closure).convention(null as 
Closure)
         addComponents = objects.property(Boolean).convention(true)
-        publicationName = objects.property(String).convention('maven')
+        publicationName = objects.property(String).convention(project.provider 
{
+            isGradlePluginProject(project) ? 'pluginMaven' : 'maven'

Review Comment:
   Looks good, thanks. The README and the Javadoc match the new check, and the 
`automatedPublishing = false` test covers it.



##########
plugin/src/functionalTest/groovy/org/apache/grails/gradle/publish/ReleaseSigningSpec.groovy:
##########
@@ -0,0 +1,189 @@
+/*
+ *  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.apache.grails.gradle.publish
+
+import org.gradle.testkit.runner.GradleRunner
+import spock.lang.Requires
+import spock.lang.Shared
+
+import java.nio.file.Files
+import java.nio.file.Path
+import java.util.concurrent.TimeUnit
+
+/**
+ * Signs and publishes releases with a throwaway GPG key, to cover the sign 
and publish task wiring.
+ */
+@Requires({ ReleaseSigningSpec.gpgAvailable() })
+class ReleaseSigningSpec extends GradleSpecification {
+
+    @Shared
+    Path gnupgHome
+
+    @Shared
+    String keyId
+
+    List<File> toCleanup = []
+
+    void setupSpec() {
+        // keep the path short, since gpg-agent's socket path is limited in 
length
+        gnupgHome = Files.createTempDirectory('gpg')
+        gpg('--batch', '--passphrase', '', '--quick-gen-key', 'Throwaway Test 
Key <[email protected]>', 'rsa2048', 'sign', 'never')
+        keyId = gpg('--list-keys', '--with-colons').readLines()
+                .find { it.startsWith('pub:') }
+                .split(':')[4]
+                .takeRight(8)
+    }
+
+    void cleanup() {
+        toCleanup.each { it.deleteDir() }
+    }
+
+    void cleanupSpec() {
+        runGpgconf('--kill', 'gpg-agent')
+        gnupgHome.toFile().deleteDir()
+    }
+
+    def "a signed release signs every published file - #description"() {
+        given:
+        File repository = File.createTempDir('release-repository')
+        File mavenLocal = File.createTempDir('release-maven-local')
+        toCleanup << repository << mavenLocal
+
+        and:
+        GradleRunner runner = setupTestResourceProject('other-artifacts', 
fixture)
+        runner = setGradleProperty('projectVersion', '0.0.1', runner)
+        runner = setGradleProperty('releasePublishType', 'MAVEN_PUBLISH', 
runner)
+        runner = setGradleProperty('mavenPublishUrl', repository.absolutePath, 
runner)
+        runner = addEnvironmentVariable('GRAILS_PUBLISH_RELEASE', 'true', 
runner)
+        // the build environment is otherwise empty, and signing runs the gpg 
command
+        runner = addEnvironmentVariable('PATH', System.getenv('PATH'), runner)
+        runner = addEnvironmentVariable('GNUPGHOME', 
gnupgHome.toAbsolutePath().toString(), runner)
+        runner = addEnvironmentVariable('SIGNING_KEY', keyId, runner)
+        environment.each { String key, String value ->
+            runner = addEnvironmentVariable(key, value, runner)
+        }
+
+        when:
+        executeTask('publish', ['publishToMavenLocal', 
"-Dmaven.repo.local=${mavenLocal.absolutePath}".toString()], runner)
+
+        then:
+        List<File> published = publishedFiles(repository)
+        published
+        published.findAll { !signatureVerifies(it) } == []
+
+        and:
+        List<File> publishedLocally = publishedFiles(mavenLocal)
+        publishedLocally
+        publishedLocally.findAll { !signatureVerifies(it) } == []
+
+        where:
+        fixture                  | environment                              | 
description
+        'simple-project'         | [:]                                      | 
'one publication'
+        'additional-publication' | [:]                                      | 
'an additional publication'
+        'gradle-plugin-project'  | [:]                                      | 
'Gradle plugin project, java-gradle-plugin applied last'
+        'gradle-plugin-project'  | [APPLY_JAVA_GRADLE_PLUGIN_FIRST: 'true'] | 
'Gradle plugin project, java-gradle-plugin applied first'
+        'gradle-plugin-project'  | [PUBLISH_SHARED_ARTIFACTS: 'true']       | 
'publications sharing artifacts'
+    }
+
+    def "a signed release publishes the grails-plugin.xml without signing it 
inside the classes directory"() {
+        given:
+        File repository = File.createTempDir('release-repository')
+        toCleanup << repository
+
+        and:
+        GradleRunner runner = setupTestResourceProject('other-artifacts', 
'grails-plugin-project')
+        runner = setGradleProperty('projectVersion', '0.0.1', runner)
+        runner = setGradleProperty('releasePublishType', 'MAVEN_PUBLISH', 
runner)
+        runner = setGradleProperty('mavenPublishUrl', repository.absolutePath, 
runner)
+        runner = addEnvironmentVariable('GRAILS_PUBLISH_RELEASE', 'true', 
runner)
+        runner = addEnvironmentVariable('PATH', System.getenv('PATH'), runner)
+        runner = addEnvironmentVariable('GNUPGHOME', 
gnupgHome.toAbsolutePath().toString(), runner)
+        runner = addEnvironmentVariable('SIGNING_KEY', keyId, runner)
+
+        and: 'the grails-plugin.xml exists, as the plugin only publishes it 
when it does at configuration time'
+        executeTask('classes', runner)
+
+        when:
+        executeTask('publish', runner)
+
+        then: 'the grails-plugin.xml is published and signed'
+        File publishedPluginXml = new File(repository, 
'org/grails/example/grails-plugin-project/0.0.1/grails-plugin-project-0.0.1-plugin.xml')
+        publishedPluginXml.text == '<plugin name="grails-plugin-project"/>'
+        signatureVerifies(publishedPluginXml)
+
+        and: 'its signature is not written into the classes directory, which 
other tasks read'
+        !new File(runner.projectDir, 
'build/classes/groovy/main/META-INF/grails-plugin.xml.asc').exists()
+    }
+
+    private static List<File> publishedFiles(File directory) {
+        List<File> files = []
+        directory.eachFileRecurse { File file ->
+            if (file.name ==~ /.*\.(jar|pom|module)/) {
+                files << file
+            }
+        }
+        files
+    }
+
+    /**
+     * Whether the file's .asc signature exists and verifies against the 
throwaway key.
+     */
+    private boolean signatureVerifies(File file) {
+        File signature = new File("${file.path}.asc")
+        if (!signature.exists()) {
+            return false
+        }
+        try {
+            gpg('--batch', '--verify', signature.path, file.path)
+            true
+        } catch (IllegalStateException ignored) {
+            false
+        }
+    }
+
+    private String gpg(String... arguments) {
+        run(['gpg', '--homedir', gnupgHome.toString()] + arguments.toList())
+    }
+
+    private void runGpgconf(String... arguments) {
+        try {
+            run(['gpgconf', '--homedir', gnupgHome.toString()] + 
arguments.toList())
+        } catch (Exception ignored) {
+            // the agent is gone with the temporary directory either way
+        }
+    }
+
+    private static String run(List<String> command) {
+        Process process = new 
ProcessBuilder(command).redirectErrorStream(true).start()
+        String output = process.inputStream.text
+        if (!process.waitFor(2, TimeUnit.MINUTES) || process.exitValue() != 0) 
{
+            throw new IllegalStateException("${command.join(' ')} 
failed:\n${output}")
+        }
+        output

Review Comment:
   Looks good, and thanks for checking the timeout path too.



-- 
This is an automated message from the Apache Git Service.
To respond to the message, please log on to GitHub and use the
URL above to go to the specific comment.

To unsubscribe, e-mail: [email protected]

For queries about this service, please contact Infrastructure at:
[email protected]

Reply via email to