Skip to content

Commit cafe359

Browse files
authored
Tag products, bom and release versions during release. (#3038)
1 parent 9e93fff commit cafe359

File tree

4 files changed

+285
-0
lines changed

4 files changed

+285
-0
lines changed

buildSrc/src/main/java/com/google/firebase/gradle/bomgenerator/BomGeneratorTask.java

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,13 @@
2121
import com.google.common.collect.Sets;
2222
import com.google.firebase.gradle.bomgenerator.model.Dependency;
2323
import com.google.firebase.gradle.bomgenerator.model.VersionBump;
24+
import com.google.firebase.gradle.bomgenerator.tagging.GitClient;
25+
import com.google.firebase.gradle.bomgenerator.tagging.ShellExecutor;
2426
import java.io.IOException;
2527
import java.io.InputStream;
2628
import java.net.URL;
2729
import java.nio.file.Path;
30+
import java.nio.file.Paths;
2831
import java.util.HashMap;
2932
import java.util.HashSet;
3033
import java.util.List;
@@ -37,6 +40,7 @@
3740
import javax.xml.parsers.ParserConfigurationException;
3841
import org.eclipse.aether.resolution.VersionRangeResolutionException;
3942
import org.gradle.api.DefaultTask;
43+
import org.gradle.api.logging.Logger;
4044
import org.gradle.api.tasks.TaskAction;
4145
import org.w3c.dom.Document;
4246
import org.w3c.dom.Element;
@@ -178,10 +182,14 @@ public class BomGeneratorTask extends DefaultTask {
178182
* everything in gMaven. This is meant to be a post-release task so that the BoM contains the most
179183
* recent versions of all artifacts.
180184
*
185+
* <p>This task also tags the release candidate commit with the BoM version, the new version of
186+
* releasing products, and the M version of the current release.
187+
*
181188
* <p>Version overrides may be given to this task in a map like so: versionOverrides =
182189
* ["com.google.firebase:firebase-firestore": "17.0.1"]
183190
*/
184191
@TaskAction
192+
// TODO(yifany): needs a more accurate name
185193
public void generateBom() throws Exception {
186194
// Repo Access Setup
187195
RepositoryClient depPopulator = new RepositoryClient();
@@ -242,6 +250,8 @@ public void generateBom() throws Exception {
242250
xmlWriter.writeXmlDocument(outputXmlDoc);
243251
documentationWriter.writeDocumentation(outputDocumentation);
244252
recipeWriter.writeVersionUpdate(outputRecipe);
253+
254+
tagVersions(version, bomDependencies);
245255
}
246256

247257
// Finds the version for the BoM artifact.
@@ -313,4 +323,23 @@ private Map<String, String> getBomMap(String bomVersion) {
313323
throw new RuntimeException("Failed to get contents of BoM version " + bomVersion, e);
314324
}
315325
}
326+
327+
private void tagVersions(String bomVersion, List<Dependency> firebaseDependencies) {
328+
Logger logger = this.getProject().getLogger();
329+
if (!System.getenv().containsKey("FIREBASE_CI")) {
330+
logger.warn("Tagging versions is skipped for non-CI environments.");
331+
return;
332+
}
333+
334+
String mRelease = System.getenv("PULL_BASE_REF");
335+
String rcCommit = System.getenv("PULL_BASE_SHA");
336+
ShellExecutor executor = new ShellExecutor(Paths.get(".").toFile(), logger::lifecycle);
337+
GitClient git = new GitClient(mRelease, rcCommit, executor, logger::lifecycle);
338+
git.tagReleaseVersion();
339+
git.tagBomVersion(bomVersion);
340+
firebaseDependencies.stream()
341+
.filter(d -> d.versionBump() != VersionBump.NONE)
342+
.forEach(d -> git.tagProductVersion(d.artifactId(), d.version()));
343+
git.pushCreatedTags();
344+
}
316345
}
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
// Copyright 2021 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.bomgenerator.tagging;
16+
17+
import java.util.ArrayList;
18+
import java.util.List;
19+
import java.util.StringJoiner;
20+
import java.util.function.Consumer;
21+
22+
public class GitClient {
23+
24+
private final String mRelease;
25+
private final String rcCommit;
26+
private final ShellExecutor executor;
27+
private final Consumer<String> logger;
28+
29+
private final List<String> newTags;
30+
private final Consumer<List<String>> handler; // handles shell outputs of git commands
31+
32+
public GitClient(
33+
String mRelease, String rcCommit, ShellExecutor executor, Consumer<String> logger) {
34+
this.mRelease = mRelease;
35+
this.rcCommit = rcCommit;
36+
this.executor = executor;
37+
this.logger = logger;
38+
39+
this.newTags = new ArrayList<>();
40+
this.handler = this.deriveGitCommandOutputHandlerFromLogger(logger);
41+
this.configureSshCommand();
42+
}
43+
44+
public void tagReleaseVersion() {
45+
this.tag(mRelease);
46+
}
47+
48+
public void tagBomVersion(String version) {
49+
String tag = "bom@" + version;
50+
this.tag(tag);
51+
}
52+
53+
public void tagProductVersion(String product, String version) {
54+
String tag = product + "@" + version;
55+
this.tag(tag);
56+
}
57+
58+
public void pushCreatedTags() {
59+
if (!this.onProw() || newTags.isEmpty()) {
60+
return;
61+
}
62+
63+
StringJoiner joiner = new StringJoiner(" ");
64+
newTags.forEach(joiner::add);
65+
String tags = joiner.toString();
66+
67+
logger.accept("Tags to be pushed: " + tags);
68+
69+
logger.accept("Pushing tags to FirebasePrivate/firebase-android-sdk ...");
70+
String command = String.format("git push origin %s", tags);
71+
executor.execute(command, handler);
72+
}
73+
74+
private boolean onProw() {
75+
return System.getenv().containsKey("FIREBASE_CI");
76+
}
77+
78+
private Consumer<List<String>> deriveGitCommandOutputHandlerFromLogger(Consumer<String> logger) {
79+
return outputs -> outputs.stream().map(output -> "[git] " + output).forEach(logger);
80+
}
81+
82+
private void tag(String tag) {
83+
logger.accept("Creating tag: " + tag);
84+
newTags.add(tag);
85+
String command = String.format("git tag %s %s", tag, rcCommit);
86+
executor.execute(command, handler);
87+
}
88+
89+
private void configureSshCommand() {
90+
if (!this.onProw()) {
91+
return;
92+
}
93+
// TODO(yifany):
94+
// - Change to use environment variables according to the Git doc:
95+
// https://git-scm.com/docs/git-config#Documentation/git-config.txt-coresshCommand
96+
// - Call of `git config core.sshCommand` in prow/config.yaml will become redundant as well
97+
String ssh =
98+
"ssh -i /etc/github-ssh-key/github-ssh"
99+
+ " -o IdentitiesOnly=yes"
100+
+ " -o UserKnownHostsFile=/dev/null"
101+
+ " -o StrictHostKeyChecking=no";
102+
String command = String.format("git config core.sshCommand %s", ssh);
103+
executor.execute(command, handler);
104+
}
105+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
// Copyright 2021 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.bomgenerator.tagging;
16+
17+
import com.google.common.io.CharStreams;
18+
import java.io.BufferedReader;
19+
import java.io.File;
20+
import java.io.IOException;
21+
import java.io.InputStreamReader;
22+
import java.util.List;
23+
import java.util.function.Consumer;
24+
import org.gradle.api.GradleException;
25+
26+
public class ShellExecutor {
27+
private final Runtime runtime;
28+
private final File cwd;
29+
private final Consumer<String> logger;
30+
31+
public ShellExecutor(File cwd, Consumer<String> logger) {
32+
this.runtime = Runtime.getRuntime();
33+
this.cwd = cwd;
34+
this.logger = logger;
35+
}
36+
37+
public void execute(String command, Consumer<List<String>> consumer) {
38+
try {
39+
logger.accept("[shell] Executing: \"" + command + "\" at: " + cwd.getAbsolutePath());
40+
Process p = runtime.exec(command, null, cwd);
41+
int code = p.waitFor();
42+
logger.accept("[shell] Command: \"" + command + "\" returned with code: " + code);
43+
BufferedReader reader = new BufferedReader(new InputStreamReader(p.getInputStream()));
44+
consumer.accept(CharStreams.readLines(reader));
45+
} catch (IOException e) {
46+
throw new GradleException("Failed when executing command: " + command, e);
47+
} catch (InterruptedException e) {
48+
Thread.currentThread().interrupt();
49+
throw new RuntimeException(e);
50+
}
51+
}
52+
}
Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
// Copyright 2021 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.bomgenerator.tagging
16+
17+
import java.util.concurrent.atomic.AtomicReference
18+
import java.util.function.Consumer
19+
import org.junit.Assert
20+
import org.junit.Assume
21+
import org.junit.Before
22+
import org.junit.Rule
23+
import org.junit.Test
24+
import org.junit.rules.TemporaryFolder
25+
import org.junit.runner.RunWith
26+
import org.junit.runners.JUnit4
27+
28+
@RunWith(JUnit4::class)
29+
class GitClientTest {
30+
@Rule @JvmField val testGitDirectory = TemporaryFolder()
31+
private val branch = AtomicReference<String>()
32+
private val commit = AtomicReference<String>()
33+
34+
@Before
35+
fun setup() {
36+
testGitDirectory.newFile("hello.txt").writeText("hello git!")
37+
val executor = ShellExecutor(testGitDirectory.root, System.out::println)
38+
val handler: (List<String>) -> Unit = { it.forEach(System.out::println) }
39+
40+
executor.execute("git init", handler)
41+
executor.execute("git config user.email '[email protected]'", handler)
42+
executor.execute("git config user.name 'GitClientTest'", handler)
43+
executor.execute("git add .", handler)
44+
executor.execute("git commit -m 'init_commit'", handler)
45+
executor.execute("git status", handler)
46+
47+
executor.execute("git rev-parse --abbrev-ref HEAD") { branch.set(it[0]) }
48+
executor.execute("git rev-parse HEAD") { commit.set(it[0]) }
49+
}
50+
51+
@Test
52+
fun `tag M release version succeeds on local file system`() {
53+
val executor = ShellExecutor(testGitDirectory.root, System.out::println)
54+
val git = GitClient(branch.get(), commit.get(), executor, System.out::println)
55+
git.tagReleaseVersion()
56+
executor.execute("git tag --points-at HEAD") {
57+
Assert.assertTrue(it.stream().anyMatch { x -> x.contains(branch.get()) })
58+
}
59+
}
60+
61+
@Test
62+
fun `tag bom version succeeds on local file system`() {
63+
val executor = ShellExecutor(testGitDirectory.root, System.out::println)
64+
val git = GitClient(branch.get(), commit.get(), executor, System.out::println)
65+
git.tagBomVersion("1.2.3")
66+
executor.execute("git tag --points-at HEAD") {
67+
Assert.assertTrue(it.stream().anyMatch { x -> x.contains("[email protected]") })
68+
}
69+
}
70+
71+
@Test
72+
fun `tag product version succeeds on local file system`() {
73+
val executor = ShellExecutor(testGitDirectory.root, System.out::println)
74+
val git = GitClient(branch.get(), commit.get(), executor, System.out::println)
75+
git.tagProductVersion("firebase-database", "1.2.3")
76+
executor.execute("git tag --points-at HEAD") {
77+
Assert.assertTrue(it.stream().anyMatch { x -> x.contains("[email protected]") })
78+
}
79+
}
80+
81+
@Test
82+
fun `tags are pushed to the remote repository`() {
83+
Assume.assumeTrue(System.getenv().containsKey("FIREBASE_CI"))
84+
85+
val mockExecutor = object : ShellExecutor(testGitDirectory.root, System.out::println) {
86+
override fun execute(command: String, consumer: Consumer<List<String>>) {
87+
consumer.accept(listOf("Received command: $command"))
88+
}
89+
}
90+
91+
val outputs = mutableListOf<String>()
92+
val git = GitClient(branch.get(), commit.get(), mockExecutor) { outputs.add(it) }
93+
git.tagBomVersion("1.2.3")
94+
git.tagProductVersion("firebase-functions", "1.2.3")
95+
git.pushCreatedTags()
96+
97+
Assert.assertTrue(outputs.stream().anyMatch { it.contains("git push origin") })
98+
}
99+
}

0 commit comments

Comments
 (0)