jdaugherty commented on code in PR #11:
URL: https://github.com/apache/grails-micronaut/pull/11#discussion_r4222679196
##########
build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/CompilePlugin.groovy:
##########
@@ -87,7 +88,8 @@ class CompilePlugin implements Plugin<Project> {
jar.manifest.attributes(
'Implementation-Title': 'Apache Grails',
- 'Implementation-Version': lookupPropertyByType(project,
'grailsVersion', String),
+ 'Implementation-Version': lookupProperty(project,
'projectVersion',
+ lookupPropertyByType(project, 'grailsVersion',
String)),
Review Comment:
Are you trying to pin a version here? Whats the goal of this?
##########
build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/CompilePlugin.groovy:
##########
@@ -155,6 +175,60 @@ class CompilePlugin implements Plugin<Project> {
}
}
+ /**
+ * Writes the Groovy compiler configuration for {@code compile}, including
the shared
+ * reproducibility script and this project's published version.
+ *
+ * <p>The published version is captured in {@code afterEvaluate}, after
the build script assigns
+ * {@code version}. The script file is produced by a task, not written
during configuration, so
+ * {@code clean compileGroovy} in one invocation regenerates it after
{@code clean} deletes
+ * {@code build/}.</p>
+ */
+ private static void configurePublishedVersionScript(Project project,
GroovyCompile compile) {
Review Comment:
Beyond the CME on line 165, this wiring is fragile. It runs in
`afterEvaluate`, realizes every `GroovyCompile` eagerly, creates the writer
with `register(...).get()`, adds a manual `dependsOn`, and overwrites the
script the `configureEach` above just set. A `GroovyCompile` registered after
that callback would silently fall back to grails-core's
`Implementation-Version`.
grails-gradle 8.0.0 solved the same problem with
`GrailsCompilerConfigScriptTask`. Mirroring it would remove most of this:
- a typed task with an `@Input` version property (fed from `project.provider
{ project.version.toString() }`) and an `@OutputFile`
- registered per source set with `sourceSets.configureEach`
- wired from the existing `configureEach` with
`it.groovyOptions.configurationScriptFile.set(writer.flatMap { it.outputFile })`
That gives the compile task an implicit dependency on the writer and needs
no `afterEvaluate`. Not blocking if the CME and the skip list are fixed.
##########
build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/CompilePlugin.groovy:
##########
@@ -145,6 +147,24 @@ class CompilePlugin implements Plugin<Project> {
// when both are present.
it.groovyOptions.configurationScript =
GradleUtils.findRootGrailsCoreDir(project).file('gradle/groovy-compile-configscript.groovy').asFile
+ }
+ // Stamp this repository's published version onto compiled
classes after the build
+ // script has assigned version. Without the stamp, the Grails
transform falls back to
+ // grails-core's Implementation-Version, so a snapshot build
of this plugin would
+ // advertise the released framework version. The script is
generated by a task so
+ // `clean compileGroovy` can recreate it after `clean` deletes
`build/`.
+ project.afterEvaluate {
+ // Applications and plugins built with the Grails Gradle
plugin already stamp
+ // project.version. Replacing their merged compiler script
would drop that plugin's
+ // own configuration. This repository's published plugin does
not apply it.
+ if
(project.plugins.hasPlugin('org.apache.grails.gradle.grails-plugin')
+ ||
project.plugins.hasPlugin('org.apache.grails.gradle.grails-web')
+ ||
project.plugins.hasPlugin('org.apache.grails.gradle.grails-gsp')) {
Review Comment:
The reasoning in this comment doesn't match grails-gradle 8.0.0:
- Only `GrailsPluginGradlePlugin` (`grails-plugin`) stamps
`projectVersion`/`projectName`. Applications (`grails-web`, `grails-app`) don't.
- `GrailsGradlePlugin` reads whatever `configurationScript` the task has at
`taskGraph.whenReady` and folds it into its combined script, so setting ours
wouldn't drop its configuration.
Apps still need to be skipped, but for a different reason: stamping
`projectName` makes `GlobalGrailsClassInjectorTransformation` add
`@GrailsPlugin(name, version)` to every `grails-app` artefact, so app classes
would look plugin-provided.
The list also misses `org.apache.grails.gradle.grails-app` (the base
`GrailsGradlePlugin`), and `grails-gsp` doesn't apply `GrailsGradlePlugin`
itself. Rather than maintain this deny-list, could stamping be opt-in? Gating
on `org.apache.grails.buildsrc.publish` would select exactly
`:grails-micronaut`.
##########
build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/CompilePlugin.groovy:
##########
@@ -155,6 +175,60 @@ class CompilePlugin implements Plugin<Project> {
}
}
+ /**
+ * Writes the Groovy compiler configuration for {@code compile}, including
the shared
+ * reproducibility script and this project's published version.
+ *
+ * <p>The published version is captured in {@code afterEvaluate}, after
the build script assigns
+ * {@code version}. The script file is produced by a task, not written
during configuration, so
+ * {@code clean compileGroovy} in one invocation regenerates it after
{@code clean} deletes
+ * {@code build/}.</p>
+ */
+ private static void configurePublishedVersionScript(Project project,
GroovyCompile compile) {
+ File baseScript = GradleUtils.findRootGrailsCoreDir(project)
+ .file('gradle/groovy-compile-configscript.groovy').asFile
+ File output =
project.layout.buildDirectory.file("groovy-compiler/${compile.name}-config.groovy").get().asFile
+ // Captured here, inside afterEvaluate, so version is the build
script's value and the
+ // writer action does not touch the project. A configuration-time
write would not survive
+ // `clean`, and compileGroovy would then fail because nothing
regenerates its input.
+ String version = project.version?.toString() ?: ''
+ String name = project.name
+ String writerName = "grailsCompilerConfig${compile.name.capitalize()}"
+ // mustRunAfter(clean): `clean compileGroovy` schedules both, and
command-line order is not a
+ // dependency. Without this, clean can delete the script after the
writer creates it.
+ Task writer = project.tasks.register(writerName) { Task task ->
+ task.mustRunAfter('clean')
+
task.inputs.file(baseScript).withPathSensitivity(PathSensitivity.NONE).withPropertyName('reproducibleCompilerScript')
+ task.inputs.property('publishedVersion', version)
+ task.inputs.property('publishedName', name)
+ task.outputs.file(output)
+ task.doLast {
+ writePublishedVersionScript(baseScript, output, version, name)
+ }
+ }.get()
+ compile.dependsOn(writer)
+ compile.groovyOptions.configurationScript = output
+ }
+
+ private static void writePublishedVersionScript(File baseScript, File
output, String version, String name) {
+ output.parentFile.mkdirs()
+ String base = baseScript.exists() ?
baseScript.getText(StandardCharsets.UTF_8.name()) : ''
Review Comment:
Minor: the `''` branch can't be reached. `task.inputs.file(baseScript)`
fails Gradle's input validation when the file is missing, before this action
runs. Either drop the fallback or mark the input `.optional()` if a missing
base script should be supported.
##########
build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/CompilePlugin.groovy:
##########
@@ -145,6 +147,24 @@ class CompilePlugin implements Plugin<Project> {
// when both are present.
it.groovyOptions.configurationScript =
GradleUtils.findRootGrailsCoreDir(project).file('gradle/groovy-compile-configscript.groovy').asFile
+ }
+ // Stamp this repository's published version onto compiled
classes after the build
+ // script has assigned version. Without the stamp, the Grails
transform falls back to
+ // grails-core's Implementation-Version, so a snapshot build
of this plugin would
+ // advertise the released framework version. The script is
generated by a task so
+ // `clean compileGroovy` can recreate it after `clean` deletes
`build/`.
+ project.afterEvaluate {
+ // Applications and plugins built with the Grails Gradle
plugin already stamp
+ // project.version. Replacing their merged compiler script
would drop that plugin's
+ // own configuration. This repository's published plugin does
not apply it.
+ if
(project.plugins.hasPlugin('org.apache.grails.gradle.grails-plugin')
+ ||
project.plugins.hasPlugin('org.apache.grails.gradle.grails-web')
+ ||
project.plugins.hasPlugin('org.apache.grails.gradle.grails-gsp')) {
+ return
+ }
+ project.tasks.withType(GroovyCompile).each { GroovyCompile
compile ->
Review Comment:
**Blocker:** this fails configuration for any non-SNAPSHOT version:
```
./gradlew help -PprojectVersion=8.0.0 # same with 8.0.0-RC1
* What went wrong:
A problem occurred configuring project ':grails-micronaut'.
> java.util.ConcurrentModificationException
at
org.gradle.api.internal.collections.FilteredElementSource$FilteringIterator.findNext(FilteredElementSource.java:118)
...
at
org.apache.grails.buildsrc.CompilePlugin$_configureCompiler_closure6$_closure11.doCall(CompilePlugin.groovy:165)
```
`configurePublishedVersionScript` registers and realizes a new task while
this `.each` is still iterating the live `tasks.withType(GroovyCompile)`
collection. The default version and `8.0.1-SNAPSHOT` configure fine, so
SNAPSHOT-only testing doesn't catch it, but every release or RC version throws.
All three steps in `.github/workflows/release.yml` pass
`-PprojectVersion=${RELEASE_VERSION}`, so the release jobs would fail before
running a task.
Iterating over a copy fixes it. With this change, `-PprojectVersion=8.0.0`
builds and `grails-plugin.xml` gets `version='8.0.0'`:
```suggestion
project.tasks.withType(GroovyCompile).toList().each {
GroovyCompile compile ->
```
A CI step that configures the build with a release version (for example
`./gradlew help -PprojectVersion=0.0.0`) would catch release-only configuration
failures like this before a tag.
##########
build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/CompilePlugin.groovy:
##########
@@ -155,6 +175,60 @@ class CompilePlugin implements Plugin<Project> {
}
}
+ /**
+ * Writes the Groovy compiler configuration for {@code compile}, including
the shared
+ * reproducibility script and this project's published version.
+ *
+ * <p>The published version is captured in {@code afterEvaluate}, after
the build script assigns
+ * {@code version}. The script file is produced by a task, not written
during configuration, so
+ * {@code clean compileGroovy} in one invocation regenerates it after
{@code clean} deletes
+ * {@code build/}.</p>
+ */
+ private static void configurePublishedVersionScript(Project project,
GroovyCompile compile) {
+ File baseScript = GradleUtils.findRootGrailsCoreDir(project)
+ .file('gradle/groovy-compile-configscript.groovy').asFile
Review Comment:
This branch can't be reached: `task.inputs.file(baseScript)` fails input
validation first when the file is missing. And if it were reached, the appended
`withConfig(configuration) { … }` block is valid on its own, so the script
wouldn't be broken. See the note on line 215.
##########
build-logic/plugins/src/test/groovy/org/apache/grails/buildsrc/CompilePluginSpec.groovy:
##########
@@ -116,6 +116,90 @@ class CompilePluginSpec extends Specification {
result.output.contains('TEST_INDY=true')
}
+ def "compiler configuration stamps the published project version"() {
+ given:
+ testProjectDir.resolve('build.gradle').toFile().text = """
+ plugins {
+ id 'groovy'
+ id 'org.apache.grails.buildsrc.compile'
+ }
+
+ version = '8.0.0-SNAPSHOT'
+
+ ext {
+ javaVersion = 21
+ grailsVersion = '8.0.0'
+ formattedBuildDate = '2026-01-01'
+ }
+
+ repositories {
+ mavenCentral()
+ }
+ """
+
+ when:
+ def result = GradleRunner.create()
+ .withProjectDir(testProjectDir.toFile())
+ .withArguments(['grailsCompilerConfigCompileGroovy',
'--stacktrace'])
+ .withPluginClasspath()
+ .build()
+
+ then:
+ result.task(':grailsCompilerConfigCompileGroovy').outcome ==
TaskOutcome.SUCCESS
+ def script =
testProjectDir.resolve('build/groovy-compiler/compileGroovy-config.groovy').toFile().text
+ script.contains("putNodeMetaData('projectVersion', '8.0.0-SNAPSHOT')")
+ script.contains("putNodeMetaData('projectName',")
Review Comment:
Both new specs only use `8.0.0-SNAPSHOT`, which is why the release-version
failure on `CompilePlugin.groovy:165` wasn't caught. A release-version case
here, or the CI configuration check suggested on that comment, would cover it.
Also, this assertion doesn't check the name value, and nothing covers the skip
path for projects that apply the Grails Gradle plugins.
##########
build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/CompilePlugin.groovy:
##########
@@ -155,6 +175,60 @@ class CompilePlugin implements Plugin<Project> {
}
}
+ /**
+ * Writes the Groovy compiler configuration for {@code compile}, including
the shared
+ * reproducibility script and this project's published version.
+ *
+ * <p>The published version is captured in {@code afterEvaluate}, after
the build script assigns
+ * {@code version}. The script file is produced by a task, not written
during configuration, so
+ * {@code clean compileGroovy} in one invocation regenerates it after
{@code clean} deletes
+ * {@code build/}.</p>
+ */
+ private static void configurePublishedVersionScript(Project project,
GroovyCompile compile) {
+ File baseScript = GradleUtils.findRootGrailsCoreDir(project)
+ .file('gradle/groovy-compile-configscript.groovy').asFile
+ File output =
project.layout.buildDirectory.file("groovy-compiler/${compile.name}-config.groovy").get().asFile
+ // Captured here, inside afterEvaluate, so version is the build
script's value and the
+ // writer action does not touch the project. A configuration-time
write would not survive
+ // `clean`, and compileGroovy would then fail because nothing
regenerates its input.
+ String version = project.version?.toString() ?: ''
+ String name = project.name
+ String writerName = "grailsCompilerConfig${compile.name.capitalize()}"
+ // mustRunAfter(clean): `clean compileGroovy` schedules both, and
command-line order is not a
+ // dependency. Without this, clean can delete the script after the
writer creates it.
+ Task writer = project.tasks.register(writerName) { Task task ->
+ task.mustRunAfter('clean')
+
task.inputs.file(baseScript).withPathSensitivity(PathSensitivity.NONE).withPropertyName('reproducibleCompilerScript')
+ task.inputs.property('publishedVersion', version)
+ task.inputs.property('publishedName', name)
+ task.outputs.file(output)
+ task.doLast {
+ writePublishedVersionScript(baseScript, output, version, name)
+ }
+ }.get()
+ compile.dependsOn(writer)
+ compile.groovyOptions.configurationScript = output
Review Comment:
Same as the thread on line 190. See the comment on line 165 for the
`ConcurrentModificationException` this wiring causes.
##########
build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/CompilePlugin.groovy:
##########
@@ -155,6 +175,60 @@ class CompilePlugin implements Plugin<Project> {
}
}
+ /**
+ * Writes the Groovy compiler configuration for {@code compile}, including
the shared
+ * reproducibility script and this project's published version.
+ *
+ * <p>The published version is captured in {@code afterEvaluate}, after
the build script assigns
+ * {@code version}. The script file is produced by a task, not written
during configuration, so
+ * {@code clean compileGroovy} in one invocation regenerates it after
{@code clean} deletes
+ * {@code build/}.</p>
+ */
+ private static void configurePublishedVersionScript(Project project,
GroovyCompile compile) {
+ File baseScript = GradleUtils.findRootGrailsCoreDir(project)
+ .file('gradle/groovy-compile-configscript.groovy').asFile
+ File output =
project.layout.buildDirectory.file("groovy-compiler/${compile.name}-config.groovy").get().asFile
+ // Captured here, inside afterEvaluate, so version is the build
script's value and the
+ // writer action does not touch the project. A configuration-time
write would not survive
+ // `clean`, and compileGroovy would then fail because nothing
regenerates its input.
+ String version = project.version?.toString() ?: ''
+ String name = project.name
+ String writerName = "grailsCompilerConfig${compile.name.capitalize()}"
+ // mustRunAfter(clean): `clean compileGroovy` schedules both, and
command-line order is not a
+ // dependency. Without this, clean can delete the script after the
writer creates it.
+ Task writer = project.tasks.register(writerName) { Task task ->
+ task.mustRunAfter('clean')
+
task.inputs.file(baseScript).withPathSensitivity(PathSensitivity.NONE).withPropertyName('reproducibleCompilerScript')
+ task.inputs.property('publishedVersion', version)
+ task.inputs.property('publishedName', name)
+ task.outputs.file(output)
+ task.doLast {
+ writePublishedVersionScript(baseScript, output, version, name)
+ }
+ }.get()
+ compile.dependsOn(writer)
+ compile.groovyOptions.configurationScript = output
+ }
+
+ private static void writePublishedVersionScript(File baseScript, File
output, String version, String name) {
+ output.parentFile.mkdirs()
+ String base = baseScript.exists() ?
baseScript.getText(StandardCharsets.UTF_8.name()) : ''
+ String script = """${base}
+
+withConfig(configuration) {
+ inline(phase: 'CONVERSION') { source, context, classNode ->
+ classNode.putNodeMetaData('projectVersion',
'${escapeGroovyLiteral(version)}')
+ classNode.putNodeMetaData('projectName',
'${escapeGroovyLiteral(name)}')
+ }
+}
+"""
+ output.setText(script, StandardCharsets.UTF_8.name())
+ }
+
+ private static String escapeGroovyLiteral(String value) {
+ value.replace('\\', '\\\\').replace('\'', '\\\'')
Review Comment:
Not a concern here: the inputs are `project.version` and `project.name`,
which won't contain newlines. grails-gradle's `GrailsPluginGradlePlugin` embeds
the same values without any escaping.
##########
settings.gradle:
##########
@@ -27,11 +27,11 @@ pluginManagement {
configurePluginRepositories(delegate)
}
plugins {
- def grailsVersion = providers.gradleProperty('projectVersion').get()
+ def grailsVersion = providers.gradleProperty('grailsVersion').get()
id 'org.apache.grails.gradle.grails-gsp' version grailsVersion
id 'org.apache.grails.gradle.grails-plugin' version grailsVersion
id 'org.apache.grails.gradle.grails-web' version grailsVersion
- id 'cloud.wondrify.asset-pipeline' version '5.2.0-M3'
+ id 'cloud.wondrify.asset-pipeline' version '5.2.0'
Review Comment:
Not an issue: `grailsVersion` is always set in `gradle.properties`, and this
is the same `.get()` pattern the previous `projectVersion` lookup used.
##########
build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/CompilePlugin.groovy:
##########
@@ -87,7 +88,8 @@ class CompilePlugin implements Plugin<Project> {
jar.manifest.attributes(
'Implementation-Title': 'Apache Grails',
- 'Implementation-Version': lookupPropertyByType(project,
'grailsVersion', String),
+ 'Implementation-Version': lookupProperty(project,
'projectVersion',
+ lookupPropertyByType(project, 'grailsVersion',
String)),
Review Comment:
For context: something here is needed. Without it, the
`SharedPropertyPlugin` change flips the jar's `Implementation-Version` to
`8.0.0`. If keeping the manifest on this repository's version is the goal,
please read `project.version` rather than the `projectVersion` property so the
manifest and the compiler stamp use one source, and mention the manifest change
in the PR description.
##########
build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/CompilePlugin.groovy:
##########
@@ -145,6 +147,24 @@ class CompilePlugin implements Plugin<Project> {
// when both are present.
it.groovyOptions.configurationScript =
GradleUtils.findRootGrailsCoreDir(project).file('gradle/groovy-compile-configscript.groovy').asFile
+ }
+ // Stamp this repository's published version onto compiled
classes after the build
+ // script has assigned version. Without the stamp, the Grails
transform falls back to
+ // grails-core's Implementation-Version, so a snapshot build
of this plugin would
+ // advertise the released framework version. The script is
generated by a task so
+ // `clean compileGroovy` can recreate it after `clean` deletes
`build/`.
Review Comment:
Nit: this comment is indented one level deeper than the
`project.afterEvaluate` it describes and sits right after the closing brace
above, so it reads as part of the previous block.
##########
README.md:
##########
@@ -20,7 +20,7 @@ limitations under the License.
This repository contains the Grails Micronaut plugin, its Micronaut-specific
BOMs, and focused integration examples.
-The build consumes published `org.apache.grails` artifacts from the Grails
8.0.x repositories. Only the four artifacts owned by this repository are built
locally:
+The build consumes the released Grails 8.0.0 artifacts (`grailsVersion` in
`gradle.properties`). This repository's own version stays `8.0.0-SNAPSHOT`
until its release tag. Only the four artifacts owned by this repository are
built locally:
Review Comment:
Nit: hard-coding `8.0.0` here will drift on the next Grails bump. Pointing
at `grailsVersion` in `gradle.properties` is enough.
##########
build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/CompilePlugin.groovy:
##########
@@ -155,6 +175,60 @@ class CompilePlugin implements Plugin<Project> {
}
}
+ /**
+ * Writes the Groovy compiler configuration for {@code compile}, including
the shared
+ * reproducibility script and this project's published version.
+ *
+ * <p>The published version is captured in {@code afterEvaluate}, after
the build script assigns
+ * {@code version}. The script file is produced by a task, not written
during configuration, so
+ * {@code clean compileGroovy} in one invocation regenerates it after
{@code clean} deletes
+ * {@code build/}.</p>
+ */
+ private static void configurePublishedVersionScript(Project project,
GroovyCompile compile) {
+ File baseScript = GradleUtils.findRootGrailsCoreDir(project)
+ .file('gradle/groovy-compile-configscript.groovy').asFile
+ File output =
project.layout.buildDirectory.file("groovy-compiler/${compile.name}-config.groovy").get().asFile
Review Comment:
Agreed, and the bigger problem in this area is the
`ConcurrentModificationException`: registering the writer while line 165
iterates the `GroovyCompile` tasks breaks configuration for release versions.
See the comment on line 165, and the one on line 187 for a lazier design.
##########
build-logic/plugins/src/main/groovy/org/apache/grails/buildsrc/CompilePlugin.groovy:
##########
@@ -155,6 +175,60 @@ class CompilePlugin implements Plugin<Project> {
}
}
+ /**
+ * Writes the Groovy compiler configuration for {@code compile}, including
the shared
+ * reproducibility script and this project's published version.
+ *
+ * <p>The published version is captured in {@code afterEvaluate}, after
the build script assigns
+ * {@code version}. The script file is produced by a task, not written
during configuration, so
+ * {@code clean compileGroovy} in one invocation regenerates it after
{@code clean} deletes
+ * {@code build/}.</p>
+ */
+ private static void configurePublishedVersionScript(Project project,
GroovyCompile compile) {
+ File baseScript = GradleUtils.findRootGrailsCoreDir(project)
+ .file('gradle/groovy-compile-configscript.groovy').asFile
+ File output =
project.layout.buildDirectory.file("groovy-compiler/${compile.name}-config.groovy").get().asFile
+ // Captured here, inside afterEvaluate, so version is the build
script's value and the
+ // writer action does not touch the project. A configuration-time
write would not survive
+ // `clean`, and compileGroovy would then fail because nothing
regenerates its input.
+ String version = project.version?.toString() ?: ''
+ String name = project.name
+ String writerName = "grailsCompilerConfig${compile.name.capitalize()}"
+ // mustRunAfter(clean): `clean compileGroovy` schedules both, and
command-line order is not a
+ // dependency. Without this, clean can delete the script after the
writer creates it.
+ Task writer = project.tasks.register(writerName) { Task task ->
+ task.mustRunAfter('clean')
+
task.inputs.file(baseScript).withPathSensitivity(PathSensitivity.NONE).withPropertyName('reproducibleCompilerScript')
+ task.inputs.property('publishedVersion', version)
+ task.inputs.property('publishedName', name)
+ task.outputs.file(output)
+ task.doLast {
+ writePublishedVersionScript(baseScript, output, version, name)
+ }
+ }.get()
+ compile.dependsOn(writer)
+ compile.groovyOptions.configurationScript = output
+ }
+
+ private static void writePublishedVersionScript(File baseScript, File
output, String version, String name) {
+ output.parentFile.mkdirs()
+ String base = baseScript.exists() ?
baseScript.getText(StandardCharsets.UTF_8.name()) : ''
+ String script = """${base}
Review Comment:
Same as the thread on line 189: unreachable, and the appended block is valid
on its own.
--
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]