-
Notifications
You must be signed in to change notification settings - Fork 314
Convert ci_jobs.gradle to a convention plugin and extension
#9838
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| package datadog.gradle.plugin.ci | ||
|
|
||
| import org.gradle.api.Project | ||
| import org.gradle.api.Task | ||
| import org.gradle.kotlin.dsl.extra | ||
|
|
||
| /** | ||
| * Checks if a task is affected by git changes | ||
| */ | ||
| internal fun isAffectedBy(baseTask: Task, affectedProjects: Map<Project, Set<String>>): String? { | ||
| val visited = mutableSetOf<Task>() | ||
| val queue = mutableListOf(baseTask) | ||
|
|
||
| while (queue.isNotEmpty()) { | ||
| val t = queue.removeAt(0) | ||
| if (visited.contains(t)) { | ||
| continue | ||
| } | ||
| visited.add(t) | ||
|
|
||
| val affectedTasks = affectedProjects[t.project] | ||
| if (affectedTasks != null) { | ||
| if (affectedTasks.contains("all")) { | ||
| return "${t.project.path}:${t.name}" | ||
| } | ||
| if (affectedTasks.contains(t.name)) { | ||
| return "${t.project.path}:${t.name}" | ||
| } | ||
| } | ||
|
|
||
| t.taskDependencies.getDependencies(t).forEach { queue.add(it) } | ||
| } | ||
| return null | ||
| } | ||
|
|
||
| /** | ||
| * Creates a single aggregate root task that depends on matching subproject tasks | ||
| */ | ||
| private fun Project.createRootTask( | ||
| rootTaskName: String, | ||
| subProjTaskName: String, | ||
| includePrefixes: List<String>, | ||
| excludePrefixes: List<String>, | ||
| forceCoverage: Boolean | ||
| ) { | ||
| val coverage = forceCoverage || rootProject.hasProperty("checkCoverage") | ||
| tasks.register(rootTaskName) { | ||
| subprojects.forEach { subproject -> | ||
| val activePartition = subproject.extra.get("activePartition") as Boolean | ||
| if (activePartition && | ||
| includePrefixes.any { subproject.path.startsWith(it) } && | ||
| !excludePrefixes.any { subproject.path.startsWith(it) }) { | ||
|
|
||
| val testTask = subproject.tasks.findByName(subProjTaskName) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. note: Also, related to this other comment #9838 (comment) (and as such for another PR), There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Ooh right! Noted, thanks. |
||
| var isAffected = true | ||
|
|
||
| if (testTask != null) { | ||
| val useGitChanges = rootProject.extra.get("useGitChanges") as Boolean | ||
| if (useGitChanges) { | ||
| @Suppress("UNCHECKED_CAST") | ||
| val affectedProjects = rootProject.extra.get("affectedProjects") as Map<Project, Set<String>> | ||
| val fileTrigger = isAffectedBy(testTask, affectedProjects) | ||
| if (fileTrigger != null) { | ||
| logger.warn("Selecting ${subproject.path}:$subProjTaskName (triggered by $fileTrigger)") | ||
| } else { | ||
| logger.warn("Skipping ${subproject.path}:$subProjTaskName (not affected by changed files)") | ||
| isAffected = false | ||
| } | ||
| } | ||
| if (isAffected) { | ||
| dependsOn(testTask) | ||
| } | ||
| } | ||
|
|
||
| if (isAffected && coverage) { | ||
| val coverageTask = subproject.tasks.findByName("jacocoTestReport") | ||
| if (coverageTask != null) { | ||
| dependsOn(coverageTask) | ||
| } | ||
| val verificationTask = subproject.tasks.findByName("jacocoTestCoverageVerification") | ||
| if (verificationTask != null) { | ||
| dependsOn(verificationTask) | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
| } | ||
|
|
||
| /** | ||
| * Creates aggregate test tasks for CI using createRootTask() above | ||
| * | ||
| * Creates three subtasks for the given base task name: | ||
| * - ${baseTaskName}Test - runs allTests | ||
| * - ${baseTaskName}LatestDepTest - runs allLatestDepTests | ||
| * - ${baseTaskName}Check - runs check | ||
| */ | ||
| fun Project.testAggregate( | ||
| baseTaskName: String, | ||
| includePrefixes: List<String>, | ||
| excludePrefixes: List<String> = emptyList(), | ||
| forceCoverage: Boolean = false | ||
| ) { | ||
| createRootTask("${baseTaskName}Test", "allTests", includePrefixes, excludePrefixes, forceCoverage) | ||
| createRootTask("${baseTaskName}LatestDepTest", "allLatestDepTests", includePrefixes, excludePrefixes, forceCoverage) | ||
| createRootTask("${baseTaskName}Check", "check", includePrefixes, excludePrefixes, forceCoverage) | ||
| } | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,132 @@ | ||
| /* | ||
| * This plugin defines a set of tasks to be used in CI. These aggregate tasks support partitioning (to parallelize | ||
| * jobs) with -PtaskPartitionCount and -PtaskPartition, and limiting tasks to those affected by git changes | ||
| * with -PgitBaseRef. | ||
| */ | ||
|
|
||
| import datadog.gradle.plugin.ci.isAffectedBy | ||
| import java.io.File | ||
| import kotlin.math.abs | ||
|
|
||
| // Set up activePartition property on all projects | ||
| allprojects { | ||
| extra.set("activePartition", true) | ||
|
|
||
| val shouldUseTaskPartitions = rootProject.hasProperty("taskPartitionCount") && rootProject.hasProperty("taskPartition") | ||
| if (shouldUseTaskPartitions) { | ||
| val taskPartitionCount = rootProject.property("taskPartitionCount") as String | ||
| val taskPartition = rootProject.property("taskPartition") as String | ||
| val currentTaskPartition = abs(project.path.hashCode() % taskPartitionCount.toInt()) | ||
| extra.set("activePartition", currentTaskPartition == taskPartition.toInt()) | ||
| } | ||
| } | ||
|
|
||
| fun relativeToGitRoot(f: File): File { | ||
| return rootProject.projectDir.toPath().relativize(f.absoluteFile.toPath()).toFile() | ||
| } | ||
|
|
||
| fun getChangedFiles(baseRef: String, newRef: String): List<File> { | ||
| val stdout = StringBuilder() | ||
| val stderr = StringBuilder() | ||
|
|
||
| val proc = Runtime.getRuntime().exec(arrayOf("git", "diff", "--name-only", "$baseRef..$newRef")) | ||
| proc.inputStream.bufferedReader().use { stdout.append(it.readText()) } | ||
| proc.errorStream.bufferedReader().use { stderr.append(it.readText()) } | ||
| proc.waitFor() | ||
| require(proc.exitValue() == 0) { "git diff command failed, stderr: $stderr" } | ||
|
|
||
| val out = stdout.toString().trim() | ||
| if (out.isEmpty()) { | ||
| return emptyList() | ||
| } | ||
|
|
||
| logger.debug("git diff output: $out") | ||
| return out.split("\n").map { File(rootProject.projectDir, it.trim()) } | ||
| } | ||
|
|
||
| // Initialize git change tracking | ||
| rootProject.extra.set("useGitChanges", false) | ||
|
|
||
| if (rootProject.hasProperty("gitBaseRef")) { | ||
| val baseRef = rootProject.property("gitBaseRef") as String | ||
| val newRef = if (rootProject.hasProperty("gitNewRef")) { | ||
| rootProject.property("gitNewRef") as String | ||
| } else { | ||
| "HEAD" | ||
| } | ||
|
|
||
| val changedFiles = getChangedFiles(baseRef, newRef) | ||
| rootProject.extra.set("changedFiles", changedFiles) | ||
| rootProject.extra.set("useGitChanges", true) | ||
|
|
||
| val ignoredFiles = fileTree(rootProject.projectDir) { | ||
| include(".gitignore", ".editorconfig") | ||
| include("*.md", "**/*.md") | ||
| include("gradlew", "gradlew.bat", "mvnw", "mvnw.cmd") | ||
| include("NOTICE") | ||
| include("static-analysis.datadog.yml") | ||
| } | ||
|
|
||
| changedFiles.forEach { f -> | ||
| if (ignoredFiles.contains(f)) { | ||
| logger.warn("Ignoring changed file: ${relativeToGitRoot(f)}") | ||
| } | ||
| } | ||
|
|
||
| val filteredChangedFiles = changedFiles.filter { !ignoredFiles.contains(it) } | ||
| rootProject.extra.set("changedFiles", filteredChangedFiles) | ||
|
|
||
| val globalEffectFiles = fileTree(rootProject.projectDir) { | ||
| include(".gitlab/**") | ||
| include("build.gradle") | ||
| include("gradle/**") | ||
| } | ||
|
|
||
| for (f in filteredChangedFiles) { | ||
| if (globalEffectFiles.contains(f)) { | ||
| logger.warn("Global effect change: ${relativeToGitRoot(f)} (no tasks will be skipped)") | ||
| rootProject.extra.set("useGitChanges", false) | ||
| break | ||
| } | ||
| } | ||
|
|
||
| if (rootProject.extra.get("useGitChanges") as Boolean) { | ||
| logger.warn("Git change tracking is enabled: $baseRef..$newRef") | ||
|
|
||
| val projects = subprojects.sortedByDescending { it.projectDir.path.length } | ||
| val affectedProjects = mutableMapOf<Project, MutableSet<String>>() | ||
|
|
||
| // Path prefixes mapped to affected task names. A file not matching any of these prefixes will affect all tasks in | ||
| // the project ("all" can be used a task name to explicitly state the same). Only the first matching prefix is used. | ||
| val matchers = listOf( | ||
| mapOf("prefix" to "src/testFixtures/", "task" to "testFixturesClasses"), | ||
| mapOf("prefix" to "src/test/", "task" to "testClasses"), | ||
| mapOf("prefix" to "src/jmh/", "task" to "jmhCompileGeneratedClasses") | ||
| ) | ||
|
|
||
| for (f in filteredChangedFiles) { | ||
| val p = projects.find { f.toString().startsWith(it.projectDir.path + "/") } | ||
| if (p == null) { | ||
| logger.warn("Changed file: ${relativeToGitRoot(f)} at root project (no task will be skipped)") | ||
| rootProject.extra.set("useGitChanges", false) | ||
| break | ||
| } | ||
|
|
||
| // Make sure path separator is / | ||
| val relPath = p.projectDir.toPath().relativize(f.toPath()).joinToString("/") | ||
| val task = matchers.find { relPath.startsWith(it["prefix"]!!) }?.get("task") ?: "all" | ||
| logger.warn("Changed file: ${relativeToGitRoot(f)} in project ${p.path} ($task)") | ||
| affectedProjects.computeIfAbsent(p) { mutableSetOf() }.add(task) | ||
| } | ||
|
|
||
| rootProject.extra.set("affectedProjects", affectedProjects) | ||
| } | ||
| } | ||
|
|
||
| tasks.register("runMuzzle") { | ||
| val muzzleSubprojects = subprojects.filter { p -> | ||
| val activePartition = p.extra.get("activePartition") as Boolean | ||
| activePartition && p.plugins.hasPlugin("java") && p.plugins.hasPlugin("muzzle") | ||
| } | ||
| dependsOn(muzzleSubprojects.map { p -> "${p.path}:muzzle" }) | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. thought: Outside the scope of this PR as well, there might be an opportunity to wrap this calculation to a provider as well. |
||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I created a
CIJobsExtensions.ktfile separate from thedatadog.ci-jobs.gradle.ktsplugin file because these functions needed to available to thebuild.gradle.ktsbuild script that applies the plugin itself. It also seemed like too much logic to include in thebuild.gradle.ktsfile directly. Let me know if a different organization method would be better (and any other thoughts of course)!There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this should land ins some package there, like
datadog.ci.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I included
package datadog.cinow!Uh oh!
There was an error while loading. Please reload this page.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Sorry, I took another look and I've seen other plugins do put their things in here
datadog.gradle.plugin. What do you think about moving this todatadog.gradle.plugin.ci?Unresolving for visibility
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Good idea! That makes sense.