Skip to content

Commit 83f4c59

Browse files
authored
Adds CI gradle tasks. (#145)
* Adds CI gradle tasks. The tasks determine what tests to run based on the contents of the PR as opposed to testing the whole repo. * Addressed review comments.
1 parent a13019b commit 83f4c59

File tree

4 files changed

+252
-0
lines changed

4 files changed

+252
-0
lines changed
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright 2018 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.gradle.plugins.ci
16+
17+
import groovy.transform.builder.Builder
18+
19+
import java.util.regex.Pattern
20+
import org.gradle.api.Project
21+
22+
/** Determines a set of subprojects that own the 'changedPaths'. */
23+
class AffectedProjectFinder {
24+
Project project;
25+
Set<String> changedPaths;
26+
27+
@Builder
28+
AffectedProjectFinder(Project project,
29+
Set<String> changedPaths,
30+
List<Pattern> ignorePaths) {
31+
this.project = project
32+
this.changedPaths = changedPaths.findAll {
33+
for(def path : ignorePaths) {
34+
if(it ==~ path) {
35+
return false
36+
}
37+
}
38+
return true
39+
}
40+
}
41+
42+
Set<Project> find() {
43+
Set<String> paths = changedPaths.collect()
44+
def projects = changedSubProjects(project, paths)
45+
46+
if(!containsRootProject(projects)) {
47+
return projects
48+
}
49+
return project.subprojects
50+
}
51+
52+
/**
53+
* Performs a post-order project tree traversal and returns a set of projects that own the
54+
* 'changedPaths'.
55+
*/
56+
private static Set<Project> changedSubProjects(Project project, Set<String> changedPaths) {
57+
// project.subprojects include all descendents of a given project, we only want immediate
58+
// children.
59+
Set<Project> immediateChildProjects = project.subprojects.findAll { it.parent == project }
60+
61+
Set<Project> projects = immediateChildProjects.collectMany {
62+
changedSubProjects(it, changedPaths)
63+
}
64+
def relativePath = project.rootDir.toURI().relativize(project.projectDir.toURI()).toString()
65+
66+
Iterator itr = changedPaths.iterator()
67+
while (itr.hasNext()) {
68+
def file = itr.next()
69+
if (file.startsWith(relativePath)) {
70+
itr.remove()
71+
projects.add(project)
72+
}
73+
}
74+
return projects
75+
}
76+
77+
private static boolean containsRootProject(Set<Project> projects) {
78+
return projects.any { it.rootProject == it };
79+
}
80+
}
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
// Copyright 2018 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.gradle.plugins.ci
16+
17+
import java.util.regex.Pattern
18+
19+
/** Contains plugin configuration properties. */
20+
class ContinuousIntegrationExtension {
21+
/** List of paths that the plugin should ignore when querying the Git commit. */
22+
List<Pattern> ignorePaths = []
23+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
// Copyright 2018 Google LLC
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
package com.google.firebase.gradle.plugins.ci
16+
17+
import org.gradle.api.Plugin
18+
import org.gradle.api.Project
19+
import org.gradle.api.Task
20+
21+
22+
/**
23+
* Provides 'checkChanged' and 'connectedCheckChanged' tasks to the root project.
24+
*
25+
* <p>The task definition is dynamic and depends on the latest git changes in the project. Namely
26+
* it gets a list of changed files from the latest Git pull/merge and determines which subprojects
27+
* the files belong to. Then, for each affected project, it declares a dependency on the
28+
* 'checkDependents' or 'connectedCheckChanged' task respectively in that project.
29+
*
30+
* <p>Note: If the commits contain a file that does not belong to any subproject, *all* subprojects
31+
* will be built.
32+
*/
33+
class ContinuousIntegrationPlugin implements Plugin<Project> {
34+
35+
@Override
36+
void apply(Project project) {
37+
38+
def extension = project.extensions.create(
39+
"firebaseContinuousIntegration",
40+
ContinuousIntegrationExtension)
41+
42+
project.configure(project.subprojects) {
43+
def checkDependents = it.task('checkDependents') {}
44+
def connectedCheckDependents = it.task('connectedCheckDependents')
45+
46+
configurations.all {
47+
if (it.name == 'debugUnitTestRuntimeClasspath') {
48+
checkDependents.dependsOn(configurations
49+
.debugUnitTestRuntimeClasspath.getTaskDependencyFromProjectDependency(
50+
false, "checkDependents"))
51+
checkDependents.dependsOn 'check'
52+
}
53+
54+
if (it.name == 'debugAndroidTestRuntimeClasspath') {
55+
connectedCheckDependents.dependsOn(configurations
56+
.debugAndroidTestRuntimeClasspath.getTaskDependencyFromProjectDependency(
57+
false, "connectedCheckDependents"))
58+
connectedCheckDependents.dependsOn 'connectedCheck'
59+
}
60+
61+
if (it.name == 'annotationProcessor') {
62+
connectedCheckDependents.dependsOn(configurations
63+
.annotationProcessor.getTaskDependencyFromProjectDependency(
64+
false, "connectedCheckDependents"))
65+
checkDependents.dependsOn(configurations
66+
.annotationProcessor.getTaskDependencyFromProjectDependency(
67+
false, "checkDependents"))
68+
}
69+
}
70+
71+
afterEvaluate {
72+
// non-android projects need to define the custom configurations due to the way
73+
// getTaskDependencyFromProjectDependency works.
74+
if (!isAndroidProject(it)) {
75+
configurations {
76+
debugUnitTestRuntimeClasspath
77+
debugAndroidTestRuntimeClasspath
78+
annotationProcessor
79+
}
80+
// noop task to avoid having to handle the edge-case of tasks not being
81+
// defined.
82+
tasks.maybeCreate('connectedCheck')
83+
tasks.maybeCreate('check')
84+
}
85+
}
86+
}
87+
88+
def affectedProjects = AffectedProjectFinder.builder()
89+
.project(project)
90+
.changedPaths(changedPaths(project.rootDir))
91+
.ignorePaths(extension.ignorePaths)
92+
.build()
93+
.find()
94+
95+
project.task('checkChanged') { task ->
96+
task.group = 'verification'
97+
task.description = 'Runs the check task in all changed projects.'
98+
affectedProjects.each {
99+
task.dependsOn("$it.path:checkDependents")
100+
}
101+
}
102+
project.task('connectedCheckChanged') { task ->
103+
task.group = 'verification'
104+
task.description = 'Runs the connectedCheck task in all changed projects.'
105+
affectedProjects.each {
106+
task.dependsOn("$it.path:connectedCheckDependents")
107+
}
108+
}
109+
110+
project.task('ciTasksSanityCheck') {
111+
doLast {
112+
[':firebase-common', ':tools:errorprone'].each { projectName ->
113+
def task = project.project(projectName).tasks.findByName('checkDependents')
114+
def dependents = task.taskDependencies.getDependencies(task).collect { it.path}
115+
116+
def expectedDependents = [
117+
'database',
118+
'firestore',
119+
'functions',
120+
'storage'].collect { ":firebase-$it:checkDependents"}
121+
assert expectedDependents.intersect(dependents) == expectedDependents :
122+
"$projectName:checkDependents does not depend on expected projects"
123+
}
124+
}
125+
}
126+
}
127+
128+
private static Set<String> changedPaths(File workDir) {
129+
return 'git diff --name-only --submodule=diff HEAD@{0} HEAD@{1}'
130+
.execute([], workDir)
131+
.text
132+
.readLines()
133+
}
134+
135+
private static final ANDROID_PLUGINS = ["com.android.application", "com.android.library",
136+
"com.android.test"]
137+
138+
private static boolean isAndroidProject(Project project) {
139+
ANDROID_PLUGINS.find { plugin -> project.plugins.hasPlugin(plugin) }
140+
}
141+
}

root-project.gradle

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,14 @@ ext {
4747
}
4848

4949
apply plugin: com.google.firebase.gradle.plugins.publish.PublishingPlugin
50+
apply plugin: com.google.firebase.gradle.plugins.ci.ContinuousIntegrationPlugin
51+
52+
firebaseContinuousIntegration {
53+
ignorePaths = [
54+
/.*\.gitignore$/,
55+
/.*.md$/,
56+
]
57+
}
5058

5159
configure(subprojects) {
5260
repositories {

0 commit comments

Comments
 (0)