Skip to content

Commit b203f79

Browse files
committed
Run CI only for tasks affected by git changes
1 parent 529f893 commit b203f79

File tree

6 files changed

+192
-18
lines changed

6 files changed

+192
-18
lines changed

.circleci/collect_results.sh

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,17 @@ shopt -s globstar
1010
TEST_RESULTS_DIR=./results
1111
mkdir -p $TEST_RESULTS_DIR >/dev/null 2>&1
1212

13-
echo "saving test results"
1413
mkdir -p $TEST_RESULTS_DIR
15-
find workspace/**/build/test-results -name \*.xml -exec sh -c '
14+
15+
mkdir -p workspace
16+
mapfile -t test_result_dirs < <(find workspace -name test-results -type d)
17+
18+
if [[ ${#test_result_dirs[@]} -eq 0 ]]; then
19+
echo "No test results found"
20+
exit 0
21+
fi
22+
23+
echo "saving test results"
24+
find "${test_result_dirs[@]}" -name \*.xml -exec sh -c '
1625
file=$(echo "$0" | rev | cut -d "/" -f 1,2,5 | rev | tr "/" "_")
1726
cp "$0" "$1/$file"' {} $TEST_RESULTS_DIR \;

.circleci/config.continue.yml.j2

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,11 @@ commands:
9999
setup_code:
100100
steps:
101101
- checkout
102+
{% if use_git_changes %}
103+
- run:
104+
name: Fetch base branch
105+
command: git fetch origin {{ pr_base_ref }}
106+
{% endif %}
102107
- run:
103108
name: Checkout merge commit
104109
command: .circleci/checkout_merge_commit.sh
@@ -312,6 +317,9 @@ jobs:
312317
./gradlew clean
313318
<< parameters.gradleTarget >>
314319
-PskipTests
320+
{% if use_git_changes %}
321+
-PgitBaseRef=origin/{{ pr_base_ref }}
322+
{% endif %}
315323
<< pipeline.parameters.gradle_flags >>
316324
--max-workers=8
317325
--rerun-tasks
@@ -411,6 +419,9 @@ jobs:
411419
./gradlew
412420
<< parameters.gradleTarget >>
413421
-PskipTests
422+
{% if use_git_changes %}
423+
-PgitBaseRef=origin/{{ pr_base_ref }}
424+
{% endif %}
414425
-PrunBuildSrcTests
415426
-PtaskPartitionCount=${CIRCLE_NODE_TOTAL} -PtaskPartition=${CIRCLE_NODE_INDEX}
416427
<< pipeline.parameters.gradle_flags >>
@@ -556,6 +567,9 @@ jobs:
556567
./gradlew
557568
<< parameters.gradleTarget >>
558569
<< parameters.gradleParameters >>
570+
{% if use_git_changes %}
571+
-PgitBaseRef=origin/{{ pr_base_ref }}
572+
{% endif %}
559573
-PtaskPartitionCount=${CIRCLE_NODE_TOTAL} -PtaskPartition=${CIRCLE_NODE_INDEX}
560574
<<# parameters.testJvm >>-PtestJvm=<< parameters.testJvm >><</ parameters.testJvm >>
561575
<< pipeline.parameters.gradle_flags >>

.circleci/render_config.py

Lines changed: 13 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@
5252
)
5353
resp.raise_for_status()
5454
except Exception as e:
55-
print(f"Request filed: {e}")
55+
print(f"Request failed: {e}")
5656
time.sleep(1)
5757
continue
5858
data = resp.json()
@@ -63,12 +63,17 @@
6363
labels = {
6464
l.replace("run-tests: ", "") for l in labels if l.startswith("run-tests: ")
6565
}
66+
# get the base reference (e.g. `master`), commit hash is also available at the `sha` field.
67+
pr_base_ref = data.get("base", {}).get("ref")
6668
else:
6769
labels = set()
6870

6971

7072
branch = os.environ.get("CIRCLE_BRANCH", "")
71-
if branch == "master" or branch.startswith("release/v") or "all" in labels:
73+
run_all = "all" in labels
74+
is_master_or_release = branch == "master" or branch.startswith("release/v")
75+
76+
if is_master_or_release or run_all:
7277
all_jdks = ALWAYS_ON_JDKS | MASTER_ONLY_JDKS
7378
else:
7479
all_jdks = ALWAYS_ON_JDKS | (MASTER_ONLY_JDKS & labels)
@@ -83,6 +88,9 @@
8388
is_weekly = os.environ.get("CIRCLE_IS_WEEKLY", "false") == "true"
8489
is_regular = not is_nightly and not is_weekly
8590

91+
# Use git changes detection on PRs
92+
use_git_changes = not run_all and not is_master_or_release and is_regular
93+
8694
vars = {
8795
"is_nightly": is_nightly,
8896
"is_weekly": is_weekly,
@@ -92,12 +100,14 @@
92100
"nocov_jdks": nocov_jdks,
93101
"flaky": branch == "master" or "flaky" in labels or "all" in labels,
94102
"docker_image_prefix": "" if is_nightly else f"{DOCKER_IMAGE_VERSION}-",
103+
"use_git_changes": use_git_changes,
104+
"pr_base_ref": pr_base_ref,
95105
}
96106

97107
print(f"Variables for this build: {vars}")
98108

99109
loader = jinja2.FileSystemLoader(searchpath=SCRIPT_DIR)
100-
env = jinja2.Environment(loader=loader)
110+
env = jinja2.Environment(loader=loader, trim_blocks=True)
101111
tpl = env.get_template(TPL_FILENAME)
102112
out = tpl.render(**vars)
103113

build.gradle

Lines changed: 153 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import java.nio.file.Paths
2+
13
buildscript {
24
dependencies {
35
classpath "pl.allegro.tech.build:axion-release-plugin:1.14.4"
@@ -164,17 +166,164 @@ allprojects { project ->
164166
}
165167
}
166168

169+
Set<Task> getTaskDependenciesRecursive(Task baseTask, Set<Task> visited = []) {
170+
if (visited.contains(baseTask)) {
171+
return []
172+
}
173+
Set<Task> dependencies = [baseTask]
174+
visited.add(baseTask)
175+
for (td in baseTask.taskDependencies) {
176+
for (t in td.getDependencies(baseTask)) {
177+
dependencies.add(t)
178+
dependencies.addAll(getTaskDependenciesRecursive(t, visited))
179+
}
180+
}
181+
return dependencies
182+
}
183+
184+
File relativeToGitRoot(File f) {
185+
return rootProject.projectDir.toPath().relativize(f.absoluteFile.toPath()).toFile()
186+
}
187+
188+
// TODO: Fallback for files not in source sets, based on build.gradle directory (sorted from longest to sortest parent candidate)
189+
String isAffectedBy(Task baseTask, Map<Project, Set<String>> affectedProjects) {
190+
for (Task t in getTaskDependenciesRecursive(baseTask)) {
191+
if (!affectedProjects.containsKey(t.project)) {
192+
continue
193+
}
194+
final Set<String> affectedTasks = affectedProjects.get(t.project)
195+
if (affectedTasks.contains("all")) {
196+
return "${t.project.path}:${t.name}"
197+
}
198+
if (affectedTasks.contains(t.name)) {
199+
return "${t.project.path}:${t.name}"
200+
}
201+
}
202+
return null
203+
}
204+
205+
List<File> getChangedFiles(String baseRef, String newRef) {
206+
final stdout = new StringBuilder()
207+
final stderr = new StringBuilder()
208+
final proc = "git diff --name-only ${baseRef}..${newRef}".execute()
209+
proc.consumeProcessOutput(stdout, stderr)
210+
proc.waitForOrKill(1000)
211+
assert proc.exitValue() == 0, "git diff command failed, stderr: ${stderr}"
212+
def out = stdout.toString().trim()
213+
if (out.isEmpty()) {
214+
return []
215+
}
216+
logger.warn("git diff output: ${out}")
217+
return out.split("\n").collect {
218+
new File(rootProject.projectDir, it.trim())
219+
}
220+
}
221+
222+
rootProject.ext {
223+
useGitChanges = false
224+
}
225+
226+
if (rootProject.hasProperty("gitBaseRef")) {
227+
// -PgitBaseRef sets the base git reference to compare changes to. In CI, this should generally be set to the target
228+
// branch, usually master.
229+
final String baseRef = rootProject.property("gitBaseRef")
230+
// -PgitNewRef sets the new git new reference to compare changes to. This is useful for testing the test selection method
231+
// itself. Otherwise, comparing against current HEAD is what makes sense for CI.
232+
final String newRef = rootProject.hasProperty("gitNewRef") ? rootProject.property("gitNewRef") : "HEAD"
233+
234+
rootProject.ext {
235+
it.changedFiles = getChangedFiles(baseRef, newRef)
236+
useGitChanges = true
237+
}
238+
239+
// The ignoredFiles FileTree selects any file that should not trigger any tasks.
240+
final ignoredFiles = fileTree(rootProject.projectDir) {
241+
include '.gitingore', '.editorconfig'
242+
include '*.md', '**/*.md'
243+
include 'gradlew', 'gradlew.bat', 'mvnw', 'mvnw.cmd'
244+
include 'NOTICE'
245+
include 'static-analysis.datadog.yml'
246+
}
247+
rootProject.changedFiles = rootProject.changedFiles.findAll { !ignoredFiles.contains(it) }
248+
249+
// The globalEffectsFile FileTree selects any file that should trigger all tasks, regardless of gradle dependency
250+
// tracking.
251+
final globalEffectFiles = fileTree(rootProject.projectDir) {
252+
include '.circleci/**'
253+
// TODO: include 'build.gradle'
254+
include 'gradle/**'
255+
}
256+
257+
for (File f in rootProject.changedFiles) {
258+
if (globalEffectFiles.contains(f)) {
259+
logger.warn("Global effect change: ${relativeToGitRoot(f)} (no tasks will be skipped)")
260+
rootProject.useGitChanges = false
261+
break
262+
}
263+
}
264+
265+
if (rootProject.useGitChanges) {
266+
logger.warn("Git change tracking is enabled, base: ${baseRef}")
267+
268+
// Get all projects, sorted by descending path length.
269+
final projects = subprojects.sort { a, b -> b.projectDir.path.length() <=> a.projectDir.path.length() }
270+
for (File f in rootProject.changedFiles) {
271+
Project p = projects.find { f.toString().startsWith(it.projectDir.path + "/") }
272+
if (p == null) {
273+
logger.warn("Changed file: ${relativeToGitRoot(f)} at root project (no tasks will be skipped)")
274+
rootProject.useGitChanges = false
275+
break
276+
}
277+
final relPath = Paths.get(p.projectDir.path).relativize(f.toPath())
278+
final pathComponents = relPath.collect({ it.toString() }).toList()
279+
Map<Project, Set<String>> _affectedProjects = [:]
280+
if (pathComponents.size() < 3) {
281+
logger.warn("Changed file: ${relativeToGitRoot(f)} in project ${p.path} (all)")
282+
_affectedProjects.computeIfAbsent(p, { new HashSet<String>() }).add("all")
283+
} else if (pathComponents[0] == "src" && pathComponents[1] == "testFixturesClasses") {
284+
logger.warn("Changed file: ${relativeToGitRoot(f)} in project ${p.path} (testFixturesClasses)")
285+
_affectedProjects.computeIfAbsent(p, { new HashSet<String>() }).add("testFixturesClasses")
286+
} else if (pathComponents[0] == "src" && pathComponents[1] == "testClasses") {
287+
// TODO: We could include other variants here such as latestTest, etc. But it is safer to assume other/main.
288+
logger.warn("Changed file: ${relativeToGitRoot(f)} in project ${p.path} (testClasses)")
289+
_affectedProjects.computeIfAbsent(p, { new HashSet<String>() }).add("testClasses")
290+
} else if (pathComponents[0] == "src" && pathComponents[1] == "jmhCompileGeneratedClasses") {
291+
logger.warn("Changed file: ${relativeToGitRoot(f)} in project ${p.path} (jmhCompileGeneratedClasses)")
292+
_affectedProjects.computeIfAbsent(p, { new HashSet<String>() }).add("jmhCompileGeneratedClasses")
293+
} else {
294+
logger.warn("Changed file: ${relativeToGitRoot(f)} in project ${p.path} (all)")
295+
_affectedProjects.computeIfAbsent(p, { new HashSet<String>() }).add("all")
296+
}
297+
rootProject.ext {
298+
it.affectedProjects = _affectedProjects
299+
}
300+
}
301+
}
302+
303+
}
167304

168305
def testAggregate(String baseTaskName, includePrefixes, excludePrefixes, boolean coverage = false) {
169-
def createRootTask = { rootTaskName, subProjTaskName ->
306+
def createRootTask = { String rootTaskName, String subProjTaskName ->
170307
tasks.register(rootTaskName) { aggTest ->
171308
subprojects { subproject ->
172309
if (subproject.property("activePartition") && includePrefixes.any { subproject.path.startsWith(it) } && !excludePrefixes.any { subproject.path.startsWith(it) }) {
173-
def testTask = subproject.tasks.findByName(subProjTaskName)
310+
Task testTask = subproject.tasks.findByName(subProjTaskName)
311+
boolean isAffected = true
174312
if (testTask != null) {
175-
aggTest.dependsOn(testTask)
313+
if (rootProject.useGitChanges) {
314+
final fileTrigger = isAffectedBy(testTask, rootProject.property("affectedProjects"))
315+
if (fileTrigger != null) {
316+
logger.warn("Selecting ${subproject.path}:${subProjTaskName} (triggered by ${fileTrigger})")
317+
} else {
318+
logger.warn("Skipping ${subproject.path}:${subProjTaskName} (not affected by changed files)")
319+
isAffected = false
320+
}
321+
}
322+
if (isAffected) {
323+
aggTest.dependsOn(testTask)
324+
}
176325
}
177-
if (coverage) {
326+
if (isAffected && coverage) {
178327
def coverageTask = subproject.tasks.findByName("jacocoTestReport")
179328
if (coverageTask != null) {
180329
aggTest.dependsOn(coverageTask)
Submodule integrations-core updated 2075 files

gradle/configure_tests.gradle

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -114,11 +114,3 @@ project.afterEvaluate {
114114
}
115115
}
116116
}
117-
118-
if (!project.property("activePartition")) {
119-
project.afterEvaluate {
120-
tasks.withType(Test).configureEach {
121-
enabled = false
122-
}
123-
}
124-
}

0 commit comments

Comments
 (0)