Skip to content

Commit bedde50

Browse files
authored
Finish PostReleasePlugin (#5254)
Per [b/292140435](https://b.corp.google.com/issues/292140435), This implements a task to handle conversion of project level dependencies to pinned dependencies post release. Relevant documentation explaining everything is added as well. The following is also fixed by this PR: - [b/296419486](https://b.corp.google.com/issues/296419486) - Connect `updatePinnedDependencies` task with `PostReleasePlugin` - [b/296419551](https://b.corp.google.com/issues/296419551) - Implement a `postReleaseCleanup` task - [b/296419351](https://b.corp.google.com/issues/296419351) - Add tests for the `updatePinnedDependencies` task - [b/296420102](https://b.corp.google.com/issues/296420102) - Implement a common test interface for dynamically created projects - [b/296420881](https://b.corp.google.com/issues/296420881) - Migrate `PublishingPluginTests` to the new testing interface - [b/296422065](https://b.corp.google.com/issues/296422065) - Migrate `PublishingPluginTests` to kotest assertions
1 parent 6349a09 commit bedde50

File tree

8 files changed

+710
-295
lines changed

8 files changed

+710
-295
lines changed

buildSrc/src/main/java/com/google/firebase/gradle/plugins/BaseFirebaseLibraryPlugin.kt

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,3 +276,18 @@ fun FirebaseLibraryExtension.resolveExternalAndroidLibraries() =
276276
*/
277277
val FirebaseLibraryExtension.artifactName: String
278278
get() = "$mavenName:$version"
279+
280+
/**
281+
* Fetches the latest version for this SDK from GMaven.
282+
*
283+
* Uses [GmavenHelper] to make the request.
284+
*/
285+
val FirebaseLibraryExtension.latestVersion: ModuleVersion
286+
get() {
287+
val latestVersion = GmavenHelper(groupId.get(), artifactId.get()).getLatestReleasedVersion()
288+
289+
return ModuleVersion.fromStringOrNull(latestVersion)
290+
?: throw RuntimeException(
291+
"Invalid format for ModuleVersion for module '$artifactName':\n $latestVersion"
292+
)
293+
}

buildSrc/src/main/java/com/google/firebase/gradle/plugins/KotlinUtils.kt

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -187,6 +187,29 @@ fun <T : Any?> Iterable<T>.toPairOrFirst(): Pair<T, T?> = first() to last().take
187187
*/
188188
fun <T> List<T>.separateAt(index: Int) = slice(0 until index) to slice(index..lastIndex)
189189

190+
/**
191+
* Maps any instances of the [regex] found in this list to the provided [transform].
192+
*
193+
* For example:
194+
* ```kotlin
195+
* listOf("mom", "mommy", "momma", "dad").replaceMatches(Regex(".*mom.*")) {
196+
* it.value.takeUnless { it.contains("y") }?.drop(1)
197+
* } // ["om", "mommy", "omma", "dad"]
198+
* ```
199+
*
200+
* @param regex the [Regex] to use to match against values in this list
201+
* @param transform a callback to call with [MathResults][MatchResult] when matches are found. If
202+
* the [transform] returns null, then the value remains unchanged.
203+
*/
204+
fun List<String>.replaceMatches(regex: Regex, transform: (MatchResult) -> String?) = map {
205+
val newValue = regex.find(it)?.let(transform)
206+
if (newValue != null) {
207+
it.replace(regex, newValue)
208+
} else {
209+
it
210+
}
211+
}
212+
190213
/**
191214
* Returns the value of the first capture group.
192215
*

buildSrc/src/main/java/com/google/firebase/gradle/plugins/PostReleasePlugin.kt

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -21,13 +21,28 @@ import org.gradle.kotlin.dsl.register
2121
/**
2222
* Facilitates the creation of Post Release Tasks.
2323
*
24-
* TODO() - Add Additional information. Will probably do such whenever we get closer to completion.
25-
* TODO() - Add a postReleaseCleanup task when all tasks are ready- as to avoid RO overlooking steps
24+
* At the end of a release, we need to update the state of the master branch according to various
25+
* "cleanup" tasks. These tasks are defined in this plugin, and attached to releasing SDKs.
26+
*
27+
* The primary parent task that this plugin creates is `postReleaseCleanup`- which runs all the
28+
* clean up tasks in one go.
29+
*
30+
* *Note that this task should be ran on the release branch- or more appropriately the merge-back
31+
* branch*
32+
*
33+
* @see registerVersionBumpTask
34+
* @see registerMoveUnreleasedChangesTask
35+
* @see registerUpdatePinnedDependenciesTask
2636
*/
2737
class PostReleasePlugin : Plugin<Project> {
2838
override fun apply(project: Project) {
29-
registerVersionBumpTask(project)
30-
registerMoveUnreleasedChangesTask(project)
39+
val versionBump = registerVersionBumpTask(project)
40+
val moveUnreleasedChanges = registerMoveUnreleasedChangesTask(project)
41+
val updatePinnedDependencies = registerUpdatePinnedDependenciesTask(project)
42+
43+
project.tasks.register("postReleaseCleanup") {
44+
dependsOn(versionBump, moveUnreleasedChanges, updatePinnedDependencies)
45+
}
3146
}
3247

3348
/**
@@ -37,7 +52,7 @@ class PostReleasePlugin : Plugin<Project> {
3752
* is set to the current version of said module. After a release, this `version` should be bumped
3853
* up to differentiate between code at HEAD, and the latest released version.
3954
*
40-
* @see [VersionBumpTask]
55+
* @see VersionBumpTask
4156
*
4257
* @param project the [Project] to register this task to
4358
*/
@@ -52,10 +67,28 @@ class PostReleasePlugin : Plugin<Project> {
5267
* moved into a seperate section that specifies the version it went out with, and the `Unreleased`
5368
* section should be wiped clear for new changes to come; for the next release.
5469
*
55-
* @see [MoveUnreleasedChangesTask]
70+
* @see MoveUnreleasedChangesTask
5671
*
5772
* @param project the [Project] to register this task to
5873
*/
5974
fun registerMoveUnreleasedChangesTask(project: Project) =
6075
project.tasks.register<MoveUnreleasedChangesTask>("moveUnreleasedChanges")
76+
77+
/**
78+
* Registers the `updatePinnedDependencies` for the provided [Project]
79+
*
80+
* If a given SDK needs to use unreleased features from a dependent SDK they change their pinned
81+
* dependency to a project level dependency, until the features are released. After a release, we
82+
* need to convert these project level dependencies back to pinned dependencies- with the latest
83+
* released version attached.
84+
*
85+
* @see UpdatePinnedDependenciesTask
86+
*
87+
* @param project the [Project] to register this task to
88+
*/
89+
fun registerUpdatePinnedDependenciesTask(project: Project) =
90+
project.tasks.register<UpdatePinnedDependenciesTask>("updatePinnedDependencies") {
91+
buildFile.set(project.buildFile)
92+
outputFile.set(project.buildFile)
93+
}
6194
}
Lines changed: 159 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,159 @@
1+
// Copyright 2023 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
16+
17+
import java.io.File
18+
import org.gradle.api.DefaultTask
19+
import org.gradle.api.provider.Property
20+
import org.gradle.api.tasks.InputFile
21+
import org.gradle.api.tasks.OutputFile
22+
import org.gradle.api.tasks.StopExecutionException
23+
import org.gradle.api.tasks.TaskAction
24+
25+
/**
26+
* Changes the project level dependencies in a project's build file to pinned dependencies.
27+
*
28+
* Computes project level dependencies outside of the project's library group, looks for instances
29+
* of them in the `dependencies` block in the build file, and replaces them with pinned versions.
30+
* GMaven is used to figure out the latest version to use for a given dependency.
31+
*
32+
* For example, given the following input:
33+
* ```
34+
* dependencies {
35+
* implementation(project(":appcheck:firebase-appcheck-interop"))
36+
* }
37+
* ```
38+
*
39+
* The output would be:
40+
* ```
41+
* dependencies {
42+
* implementation("com.google.firebase:firebase-appcheck-interop:17.0.1")
43+
* }
44+
* ```
45+
*
46+
* *Assuming that `17.0.1` is the latest version of `firebase-appcheck-interop`*
47+
*
48+
* @property buildFile A [File] that should be used as the source to update from. Typically the
49+
* `build.gradle` or `build.gradle.kts` file for a given project.
50+
* @property outputFile A [File] that should be used to write the new text to. Typically the same as
51+
* the input file ([buildFile]).
52+
* @see PostReleasePlugin
53+
*/
54+
abstract class UpdatePinnedDependenciesTask : DefaultTask() {
55+
@get:[InputFile]
56+
abstract val buildFile: Property<File>
57+
58+
@get:[OutputFile]
59+
abstract val outputFile: Property<File>
60+
61+
@TaskAction
62+
fun updateBuildFileDependencies() {
63+
val dependenciesToChange = findProjectLevelDependenciesToChange()
64+
65+
if (dependenciesToChange.isEmpty()) throw StopExecutionException("No libraries to change.")
66+
67+
val buildFileContent = buildFile.get().readLines()
68+
val updatedContent = replaceProjectLevelDependencies(buildFileContent, dependenciesToChange)
69+
70+
validateDependenciesHaveChanged(dependenciesToChange, buildFileContent, updatedContent)
71+
72+
outputFile.get().writeText(updatedContent.joinToString("\n") + "\n")
73+
}
74+
75+
private fun validateDependenciesHaveChanged(
76+
dependenciesToChange: List<FirebaseLibraryExtension>,
77+
oldContent: List<String>,
78+
updatedContent: List<String>
79+
) {
80+
if (oldContent == updatedContent)
81+
throw RuntimeException(
82+
"Expected the following project level dependencies, but found none: " +
83+
"${dependenciesToChange.joinToString("\n") { it.mavenName }}"
84+
)
85+
86+
val diff = oldContent.diff(updatedContent)
87+
val changedLines = diff.mapNotNull { it.first ?: it.second }
88+
89+
val (librariesCorrectlyChanged, linesChangedIncorrectly) =
90+
dependenciesToChange.partition { lib -> changedLines.any { it.contains(lib.path) } }
91+
92+
val librariesNotChanged = dependenciesToChange - librariesCorrectlyChanged
93+
94+
if (linesChangedIncorrectly.isNotEmpty())
95+
throw RuntimeException(
96+
"The following lines were caught by our REGEX, but should not have been:\n ${linesChangedIncorrectly.joinToString("\n")}"
97+
)
98+
99+
if (librariesNotChanged.isNotEmpty())
100+
throw RuntimeException(
101+
"The following libraries were not found, but should have been:\n ${librariesNotChanged.joinToString("\n") { it.mavenName }}"
102+
)
103+
104+
if (librariesCorrectlyChanged.size > dependenciesToChange.size)
105+
throw RuntimeException(
106+
"Too many libraries were caught by our change, possible REGEX false positive:\n ${changedLines.joinToString("\n")}"
107+
)
108+
}
109+
110+
private fun findProjectLevelDependenciesToChange(): List<FirebaseLibraryExtension> {
111+
val firebaseLibrary = project.firebaseLibrary
112+
113+
return firebaseLibrary.projectLevelDependencies - firebaseLibrary.librariesToRelease
114+
}
115+
116+
private val FirebaseLibraryExtension.projectLevelDependencies: List<FirebaseLibraryExtension>
117+
get() = resolveProjectLevelDependencies().filterNot { it.path in DEPENDENCIES_TO_IGNORE }
118+
119+
private fun replaceProjectLevelDependencies(
120+
buildFileContent: List<String>,
121+
libraries: List<FirebaseLibraryExtension>
122+
) =
123+
buildFileContent.replaceMatches(DEPENDENCY_REGEX) {
124+
val projectName = it.firstCapturedValue
125+
val projectToChange = libraries.find { it.path == projectName }
126+
val latestVersion = projectToChange?.latestVersion
127+
128+
latestVersion?.let { "\"${projectToChange.mavenName}:$latestVersion\"" }
129+
}
130+
131+
companion object {
132+
/**
133+
* Regex used in finding project level dependencies and their respective project.
134+
*
135+
* The regex can be broken down as such:
136+
*
137+
* `(N whitespace)(N letters)(a space or bracket)project((a single or double quote): (anything
138+
* besides whitespace)(a single or double quote))`
139+
*
140+
* For example, given the following input:
141+
* ```
142+
* dependencies {
143+
* implementation 'com.google.firebase:firebase-annotations:16.2.0'
144+
* implementation 'com.google.firebase:firebase-common:20.3.1'
145+
* implementation project(':firebase-installations')
146+
* implementation 'com.google.firebase:firebase-database-collection:18.0.1'
147+
* ```
148+
*
149+
* The following group would be captured:
150+
* ```
151+
* :firebase-installations
152+
* ```
153+
*/
154+
val DEPENDENCY_REGEX =
155+
Regex("(?<=\\s{1,20}\\w{1,20}(?:\\s|\\())project\\((?:'|\")(:\\S+)(?:'|\")\\)")
156+
157+
val DEPENDENCIES_TO_IGNORE = listOf(":protolite-well-known-types")
158+
}
159+
}

0 commit comments

Comments
 (0)