jdaugherty commented on code in PR #16528: URL: https://github.com/apache/grails-core/pull/16528#discussion_r4190559320
########## build-logic/docs-core/src/main/groovy/grails/doc/gradle/ConvertAsciidocTask.groovy: ########## @@ -0,0 +1,165 @@ +/* + * 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 grails.doc.gradle + +import javax.inject.Inject + +import groovy.transform.CompileStatic + +import org.gradle.api.DefaultTask +import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.file.FileSystemOperations +import org.gradle.api.file.FileVisitDetails +import org.gradle.api.model.ObjectFactory +import org.gradle.api.provider.ListProperty +import org.gradle.api.provider.MapProperty +import org.gradle.api.provider.Property +import org.gradle.api.tasks.CacheableTask +import org.gradle.api.tasks.Input +import org.gradle.api.tasks.InputDirectory +import org.gradle.api.tasks.Internal +import org.gradle.api.tasks.OutputDirectory +import org.gradle.api.tasks.PathSensitive +import org.gradle.api.tasks.PathSensitivity +import org.gradle.api.tasks.TaskAction +import org.gradle.api.tasks.util.PatternSet +import org.gradle.workers.WorkQueue +import org.gradle.workers.WorkerExecutor + +/** + * Converts the AsciiDoc documents of a directory to HTML pages. + * + * <p>Every document matching {@link #getIncludes()} and not {@link #getExcludes()} - by default + * every {@code .adoc} file whose name does not start with an underscore - is converted on its + * own, with its own directory as the base directory, to the same relative path under + * {@link #getOutputDir()}. The {@code images} directory of the source directory is copied + * alongside the pages.</p> + * + * <p>Like the Asciidoctor Gradle plugin, the task sets {@code revnumber} and the + * {@code gradle-project-name}, {@code gradle-project-group} and {@code gradle-project-version} + * attributes from the project, and {@code includedir} to the source directory, unless + * {@link #getAttributes()} sets them.</p> + * + * <p>The conversion runs in a forked worker process, like {@link PublishGuideTask}, so the JRuby + * runtime AsciidoctorJ starts never lands in the Gradle daemon.</p> + * + * @since 8.0 + */ +@CompileStatic +@CacheableTask +abstract class ConvertAsciidocTask extends DefaultTask { + + @InputDirectory + @PathSensitive(PathSensitivity.RELATIVE) + abstract DirectoryProperty getSourceDir() + + @Input + abstract ListProperty<String> getIncludes() + + @Input + abstract ListProperty<String> getExcludes() + + /** + * The document attributes. Values are converted to strings before the documents are converted. + */ + @Input + abstract MapProperty<String, Object> getAttributes() + + /** The attributes describing the project, which {@link #getAttributes()} can override. */ + @Input + abstract MapProperty<String, String> getProjectAttributes() + + @OutputDirectory + abstract DirectoryProperty getOutputDir() + + /** The JVM arguments of the worker process the documents are converted in. */ + @Internal + abstract ListProperty<String> getJvmArgs() + + /** The maximum heap of the worker process the documents are converted in. */ + @Internal + abstract Property<String> getMaxHeapSize() + + @Inject + abstract WorkerExecutor getWorkerExecutor() + + @Inject + abstract FileSystemOperations getFileSystemOperations() + + @Inject + abstract ObjectFactory getObjects() + + ConvertAsciidocTask() { + group = 'documentation' + description = 'Converts the AsciiDoc documents to HTML.' + includes.convention(['**/*.adoc']) + excludes.convention(['**/_*']) Review Comment: Added in b1fef67: `ConvertAsciidocTaskSpec` runs the task in TestKit builds and covers the `_*` exclude, the `images` copy, `revnumber`, `gradle-project-*` and `includedir`, an attribute overriding a project attribute, included-file changes and build-cache relocation. `FetchTagsTaskSpec` covers the tag listing. ########## build-logic/vulnerability-scan/src/main/groovy/org/apache/grails/buildsrc/scan/OsvVulnerabilityService.groovy: ########## @@ -0,0 +1,190 @@ +/* + * 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.buildsrc.scan + +import java.net.http.HttpRequest +import java.nio.charset.StandardCharsets +import java.nio.file.Path +import java.time.Duration +import java.util.concurrent.ConcurrentHashMap + +import groovy.json.JsonOutput +import groovy.transform.CompileDynamic +import groovy.transform.CompileStatic + +import org.gradle.api.file.DirectoryProperty +import org.gradle.api.provider.Property +import org.gradle.api.services.BuildService +import org.gradle.api.services.BuildServiceParameters + +/** + * Looks the vulnerabilities of Maven artifacts up in the <a href="https://osv.dev">OSV</a> database. + * + * <p>Every answer is kept on disk for {@link Parameters#getCacheTtl() a while}, so builds that run again shortly + * after, in this or another checkout sharing the cache directory, do not ask the API again. Entries older than that + * are fetched again.</p> + * + * <p>The service is shared by the scan tasks of every project and asks the API one task at a time, which keeps the + * build well under the API's rate limits.</p> + */ +@CompileStatic +abstract class OsvVulnerabilityService implements BuildService<Parameters>, AutoCloseable { + + static final String NAME = 'OSV' + static final String DEFAULT_API_URL = 'https://api.osv.dev' + static final Duration DEFAULT_CACHE_TTL = Duration.ofHours(1) + + private static final int BATCH_SIZE = 1000 + + interface Parameters extends BuildServiceParameters { + + /** The OSV API, {@value OsvVulnerabilityService#DEFAULT_API_URL} unless a test points it elsewhere. */ + Property<String> getApiUrl() + + /** Where the answers of the API are kept between builds, shared with the other databases. */ + DirectoryProperty getCacheDirectory() + + /** How long an answer is used before the API is asked again. */ + Property<Duration> getCacheTtl() + } + + private final Map<String, List<String>> idsByCoordinate = new ConcurrentHashMap<>() + private final Map<String, Map<String, Object>> vulnerabilitiesById = new ConcurrentHashMap<>() + private final ApiClient client = new ApiClient('OSV API') + private ApiCache queryCache + private ApiCache vulnerabilityCache + + /** + * Returns the vulnerabilities known for each of the given {@code group:artifact:version} coordinates, as the + * OSV records of them. + */ + synchronized Map<String, List<Map<String, Object>>> vulnerabilities(Collection<String> coordinates) { + Collection<String> unique = coordinates.toUnique() + List<String> missing = unique.findAll { String coordinate -> idsFor(coordinate) == null }.toList() + if (missing) { + queryIds(missing) + } + + Map<String, List<Map<String, Object>>> vulnerabilities = [:] + for (String coordinate : unique) { + vulnerabilities.put(coordinate, idsByCoordinate.get(coordinate).collect { String id -> vulnerability(id) }) + } + vulnerabilities + } + + @Override + void close() { + client.close() + } + + private List<String> idsFor(String coordinate) { + List<String> ids = idsByCoordinate.get(coordinate) + if (ids == null) { + Map<String, Object> cached = queryCache().read(coordinate) + if (cached != null) { + ids = (List<String>) cached.get('ids') + idsByCoordinate.put(coordinate, ids) + } + } + ids + } + + private void queryIds(List<String> coordinates) { + coordinates.collate(BATCH_SIZE).each { List<String> batch -> + Map<String, List<String>> ids = batch.collectEntries { [(it): new ArrayList<String>()] } + Map<String, String> pageTokens = [:] + List<String> pending = batch + while (pending) { + List<Map<String, Object>> queries = pending.collect { String coordinate -> query(coordinate, pageTokens.get(coordinate)) } + List<Map<String, Object>> results = resultsOf(ApiClient.json(client.send(post('/v1/querybatch', [queries: queries])))) + pageTokens.clear() + pending.eachWithIndex { String coordinate, int index -> + Map<String, Object> result = results[index] ?: [:] Review Comment: Added in b1fef67, with a spec that answers a batch with fewer results than queries and checks that nothing was cached for the coordinates left out. ########## build-logic/vulnerability-scan/build.gradle: ########## @@ -16,26 +16,29 @@ * specific language governing permissions and limitations * under the License. */ + plugins { id 'groovy-gradle-plugin' } version = '0.0.1' group = 'org.apache.grails.buildsrc' -// The Sonatype scan plugin is a fat jar that bundles Guava, Groovy, Jackson and other libraries without -// relocating them. It is kept in its own project so that those bundled classes never reach the settings -// classpath that every build shares when it requests a plugin from ':build-logic'; there they would shadow -// the real dependencies of other settings plugins (for example the Guava the dependency-analysis plugin -// requires). -project.layout.projectDirectory.file('../../gradle.properties').asFile.withInputStream { - Properties props = new Properties() - props.load(it) - project.ext.gradleProperties = props +apply { + from project.layout.projectDirectory.file('../../dependencies.gradle') } dependencies { - implementation "org.sonatype.gradle.plugins:scan-gradle-plugin:${gradleProperties.sonatypeScanPluginVersion}" + testImplementation "org.spockframework:spock-core:${gradleBomDependencyVersions['gradle-spock.version']}" + testImplementation gradleTestKit() + testRuntimeOnly 'org.junit.platform:junit-platform-launcher' +} + +tasks.named('test', Test) { Review Comment: Added in b1fef67: a `Build Logic` job in the CI workflow runs `./gradlew check` in `build-logic`. ########## build-logic/docs-core/src/main/groovy/grails/doc/git/FetchTagsTask.groovy: ########## @@ -27,52 +27,73 @@ import javax.inject.Inject import groovy.transform.CompileStatic +import org.gradle.api.DefaultTask import org.gradle.api.Project +import org.gradle.api.file.DirectoryProperty import org.gradle.api.file.RegularFileProperty import org.gradle.api.model.ObjectFactory import org.gradle.api.provider.Property import org.gradle.api.tasks.CacheableTask -import org.gradle.api.tasks.Exec import org.gradle.api.tasks.Input +import org.gradle.api.tasks.Internal import org.gradle.api.tasks.OutputFile +import org.gradle.api.tasks.TaskAction +import org.gradle.process.ExecOperations +import org.gradle.process.ExecSpec +/** + * Writes the tags of the repository, newest first, to {@link #getTagsFile()}; outside a git + * repository it writes {@link #getDefaultTag()} instead. + */ @CompileStatic @CacheableTask -abstract class FetchTagsTask extends Exec { +abstract class FetchTagsTask extends DefaultTask { @Input final Property<LocalDateTime> cacheDate // allows for forcing refreshing the tags, defaults to once a day @Input final Property<String> defaultTag // if no git repo present + @Input + final Property<Boolean> gitRepository + + @Internal + final DirectoryProperty repositoryDir + @OutputFile final RegularFileProperty tagsFile + @Inject + abstract ExecOperations getExecOperations() + @Inject FetchTagsTask(ObjectFactory objectFactory, Project project) { group = 'documentation' cacheDate = objectFactory.property(LocalDateTime).convention(LocalDate.now().atStartOfDay()) Review Comment: Fixed in b1fef67: the task is an `@UntrackedTask` now and the unused `cacheDate` is gone. `FetchTagsTaskSpec` adds a tag between two runs and checks that the reused entry lists it. ########## build-logic/vulnerability-scan/src/main/groovy/org/apache/grails/buildsrc/scan/ApiClient.groovy: ########## @@ -0,0 +1,120 @@ +/* + * 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.buildsrc.scan + +import java.net.http.HttpClient +import java.net.http.HttpRequest +import java.net.http.HttpResponse +import java.time.Duration + +import groovy.json.JsonSlurper +import groovy.transform.CompileStatic + +import org.gradle.api.GradleException +import org.gradle.api.logging.Logger +import org.gradle.api.logging.Logging + +/** + * Sends the requests to a vulnerability database, retrying those that were throttled or failed on the way. + */ +@CompileStatic +class ApiClient implements AutoCloseable { + + private static final Logger LOG = Logging.getLogger(ApiClient) + private static final int MAX_ATTEMPTS = 4 + private static final int MAX_ERROR_BODY = 500 + + private final String name + private HttpClient httpClient + + /** + * @param name what the API is called in messages, such as {@code OSV API} + */ + ApiClient(String name) { + this.name = name + } + + /** + * Sends the request and returns the answer, once it is a {@code 200}. A request that was throttled, failed + * with a server error or did not get through is sent again, after the time the API asks for if it does. + * + * @param hints what to tell the user when the API answers with one of these statuses + */ + HttpResponse<String> send(HttpRequest request, Map<Integer, String> hints = [:]) { + IOException failure = null + for (int attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) { + try { + HttpResponse<String> response = client().send(request, HttpResponse.BodyHandlers.ofString()) + int status = response.statusCode() + if (status == 200) { + return response + } + if (status != 429 && status < 500) { + String body = response.body() ?: '' + throw new GradleException("The ${name} answered ${request.method()} ${request.uri()} with ${status}" + + "${hints.get(status) ? " (${hints.get(status)})" : ''}: " + + "${body.length() > MAX_ERROR_BODY ? body.substring(0, MAX_ERROR_BODY) + '…' : body}") + } + LOG.info('The {} answered {} {} with {}, attempt {} of {}', name, request.method(), request.uri(), + status, attempt, MAX_ATTEMPTS) + if (attempt < MAX_ATTEMPTS) { + sleep(retryDelay(response, attempt)) + } + } + catch (IOException e) { + failure = e + LOG.info('Could not reach the {} at {}, attempt {} of {}', name, request.uri(), attempt, MAX_ATTEMPTS, e) + if (attempt < MAX_ATTEMPTS) { + sleep(Duration.ofSeconds(1L << attempt).toMillis()) + } + } + } + throw new GradleException("The ${name} did not answer ${request.method()} ${request.uri()} after " + Review Comment: Fixed in b1fef67: once the attempts run out, the message has the last status and body ("still answered ... with 429 after 4 attempts: ..."), and the wait uses `Thread.sleep`, turning an interrupt into a `BuildCancelledException`. A spec covers the throttled case. -- 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]
