Skip to content

Implement a task for updating changelog files after a release #5181

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

Merged
merged 13 commits into from
Jul 20, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ import java.io.File
* @see fromString
*/
data class Changelog(val releases: List<ReleaseEntry>) {

override fun toString(): String = releases.joinToString("\n") + "\n"

companion object {
/**
* Regex for finding the version titles in a changelog file.
Expand Down Expand Up @@ -114,7 +117,41 @@ data class ReleaseEntry(
val content: ReleaseContent,
val ktx: ReleaseContent?
) {

override fun toString(): String {
return """
|# ${version ?: "Unreleased"}
|$content
|${ktx?.let { """
|
|## Kotlin
|$it
|""" }.orEmpty()}
"""
.trimMargin()
}

/**
* If there is any content in this release.
*
* Meaning that there is content in either the [base release][content] or [ktx].
*
* @see ReleaseContent.hasContent
*/
fun hasContent() = content.hasContent() || ktx?.hasContent() ?: false

companion object {

/**
* A static instance of a [ReleaseEntry] without any content.
*
* This exists to provide a means for tooling to create new sections explicitly, versus offering
* default values to [ReleaseEntry]
* - as this could lead to edge case scenarios where empty [ReleaseEntry] instances are
* accidentally created.
*/
val Empty = ReleaseEntry(null, ReleaseContent("", emptyList()), null)

/**
* Regex for finding the Kotlin header in a changelog file.
*
Expand Down Expand Up @@ -164,18 +201,12 @@ data class ReleaseEntry(
val ktx = ktxString?.let { ReleaseContent.fromString(it) }
val version = ModuleVersion.fromStringOrNull(versionString)

if (ktx?.hasContent() == false)
throw RuntimeException("KTX header found without any content:\n $string")

return ReleaseEntry(version, content, ktx)
}
}

/**
* If there is any content in this release.
*
* Meaning that there is content in either the [base release][content] or [ktx].
*
* @see ReleaseContent.hasContent
*/
fun hasContent() = content.hasContent() || ktx?.hasContent() ?: false
}

/**
Expand All @@ -186,6 +217,25 @@ data class ReleaseEntry(
* @see fromString
*/
data class ReleaseContent(val subtext: String, val changes: List<Change>) {

override fun toString(): String {
val changes = changes.joinToString("\n")

return when {
subtext.isNotBlank() && changes.isNotBlank() -> "$subtext\n\n$changes"
subtext.isNotBlank() -> subtext
changes.isNotBlank() -> changes
else -> ""
}
}

/**
* If there is any content in this release.
*
* Meaning that there is either [changes] or [subtext] present.
*/
fun hasContent() = changes.isNotEmpty() || subtext.isNotBlank()

companion object {
/**
* Regex for finding the changes in a release.
Expand Down Expand Up @@ -235,7 +285,7 @@ data class ReleaseContent(val subtext: String, val changes: List<Change>) {
* "This release contains a known bug. We will address this in a future bugfix."
* ```
*/
val SUBTEXT_REGEX = Regex("^([^\\*\\s][\\s\\S]+?)(\\n\\n|(?![\\s\\S]))", RegexOption.MULTILINE)
val SUBTEXT_REGEX = Regex("^([^\\*\\s][\\s\\S]+?)(\\n\\n|(?![\\s\\S]))")

/**
* Parses [ReleaseContent] from a [String].
Expand All @@ -254,19 +304,14 @@ data class ReleaseContent(val subtext: String, val changes: List<Change>) {
* @see Change
*/
fun fromString(string: String): ReleaseContent {
val subtext = SUBTEXT_REGEX.find(string)?.value.orEmpty().trim()
val changes = CHANGE_REGEX.findAll(string).map { Change.fromString(it.firstCapturedValue) }
val changes =
CHANGE_REGEX.findAll(string).map { Change.fromString(it.firstCapturedValue) }.toList()
val firstChange = CHANGE_REGEX.find(string)
val subtext = if (firstChange != null) string.substringBefore(firstChange.value) else string

return ReleaseContent(subtext, changes.toList())
return ReleaseContent(subtext.trim(), changes.toList())
}
}

/**
* If there is any content in this release.
*
* Meaning that there is either [changes] or [subtext] present.
*/
fun hasContent() = changes.isNotEmpty() || subtext.isNotBlank()
}

/**
Expand All @@ -277,6 +322,9 @@ data class ReleaseContent(val subtext: String, val changes: List<Change>) {
* @see fromString
*/
data class Change(val type: ChangeType, val message: String) {

override fun toString(): String = "* [$type] $message"

companion object {
/**
* Regex for finding the information about a [Change].
Expand Down Expand Up @@ -331,5 +379,7 @@ enum class ChangeType {
CHANGED,
UNCHANGED,
REMOVED,
DEPRECATED
DEPRECATED;

override fun toString(): String = name.toLowerCase()
}
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,25 @@ fun DefaultTask.tempFile(path: String) = provider { temporaryDir.childFile(path)
*/
fun File.listFilesOrEmpty() = listFiles().orEmpty()

/**
* Copies this file to the specified directory.
*
* The new file will retain the same [name][File.getName] and [extension][File.extension] as this
* file.
*
* @param target The directory to copy the file to.
* @param overwrite Whether to overwrite the file if it already exists.
* @param bufferSize The size of the buffer to use for the copy operation.
* @return The new file.
*
* @see copyTo
*/
fun File.copyToDirectory(
target: File,
overwrite: Boolean = false,
bufferSize: Int = DEFAULT_BUFFER_SIZE
): File = copyTo(target.childFile(name), overwrite, bufferSize)

/**
* Submits a piece of work to be executed asynchronously.
*
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ abstract class MakeReleaseNotesTask : DefaultTask() {
"""
|#### ${metadata.name} Kotlin extensions version $version {: #${metadata.versionName}-ktx_v$versionClassifier}
|
|${unreleased.ktx?.toReleaseNotes() ?: KotlinTransitiveRelease(project.name)}
|${unreleased.ktx?.toReleaseNotes() ?: KTXTransitiveReleaseText(project.name)}
"""
.trimMargin()
.trim()
Expand All @@ -115,32 +115,6 @@ abstract class MakeReleaseNotesTask : DefaultTask() {
releaseNotesFile.asFile.get().writeText(releaseNotes)
}

/**
* Provides default text for releasing KTX libs that are transitively invoked in a release,
* because their parent module is releasing. This only applies to `-ktx` libs, not Kotlin SDKs.
*/
private fun KotlinTransitiveRelease(projectName: String) =
"""
|The Kotlin extensions library transitively includes the updated
|`${ProjectNameToKTXPlaceholder(projectName)}` library. The Kotlin extensions library has no additional
|updates.
"""
.trimMargin()
.trim()

/**
* Maps a project's name to a KTX suitable placeholder.
*
* Some libraries produce artifacts with different coordinates than their project name. This
* method helps to map that gap for [KotlinTransitiveRelease].
*/
private fun ProjectNameToKTXPlaceholder(projectName: String) =
when (projectName) {
"firebase-perf" -> "firebase-performance"
"firebase-appcheck" -> "firebase-appcheck"
else -> projectName
}

/**
* Converts a [ReleaseContent] to a [String] to be used in a release note.
*
Expand Down Expand Up @@ -174,55 +148,6 @@ abstract class MakeReleaseNotesTask : DefaultTask() {
return "* {{${type.name.toLowerCase()}}} $fixedMessage"
}

/**
* Maps the name of a project to its potential [ReleaseNotesMetadata].
*
* @throws StopActionException If a mapping is not found
*/
// TODO() - Should we expose these as firebaselib configuration points; especially for new SDKS?
private fun convertToMetadata(string: String) =
when (string) {
"firebase-abt" -> ReleaseNotesMetadata("{{ab_testing}}", "ab_testing", false)
"firebase-appdistribution" -> ReleaseNotesMetadata("{{appdistro}}", "app-distro", false)
"firebase-appdistribution-api" -> ReleaseNotesMetadata("{{appdistro}} API", "app-distro-api")
"firebase-config" -> ReleaseNotesMetadata("{{remote_config}}", "remote-config")
"firebase-crashlytics" -> ReleaseNotesMetadata("{{crashlytics}}", "crashlytics")
"firebase-crashlytics-ndk" ->
ReleaseNotesMetadata("{{crashlytics}} NDK", "crashlytics-ndk", false)
"firebase-database" -> ReleaseNotesMetadata("{{database}}", "realtime-database")
"firebase-dynamic-links" -> ReleaseNotesMetadata("{{ddls}}", "dynamic-links")
"firebase-firestore" -> ReleaseNotesMetadata("{{firestore}}", "firestore")
"firebase-functions" -> ReleaseNotesMetadata("{{functions_client}}", "functions-client")
"firebase-dynamic-module-support" ->
ReleaseNotesMetadata(
"Dynamic feature modules support",
"dynamic-feature-modules-support",
false
)
"firebase-inappmessaging" -> ReleaseNotesMetadata("{{inappmessaging}}", "inappmessaging")
"firebase-inappmessaging-display" ->
ReleaseNotesMetadata("{{inappmessaging}} Display", "inappmessaging-display")
"firebase-installations" ->
ReleaseNotesMetadata("{{firebase_installations}}", "installations")
"firebase-messaging" -> ReleaseNotesMetadata("{{messaging_longer}}", "messaging")
"firebase-messaging-directboot" ->
ReleaseNotesMetadata("Cloud Messaging Direct Boot", "messaging-directboot", false)
"firebase-ml-modeldownloader" ->
ReleaseNotesMetadata("{{firebase_ml}}", "firebaseml-modeldownloader")
"firebase-perf" -> ReleaseNotesMetadata("{{perfmon}}", "performance")
"firebase-storage" -> ReleaseNotesMetadata("{{firebase_storage_full}}", "storage")
"firebase-appcheck" -> ReleaseNotesMetadata("{{app_check}}", "appcheck")
"firebase-appcheck-debug" ->
ReleaseNotesMetadata("{{app_check}} Debug", "appcheck-debug", false)
"firebase-appcheck-debug-testing" ->
ReleaseNotesMetadata("{{app_check}} Debug Testing", "appcheck-debug-testing", false)
"firebase-appcheck-playintegrity" ->
ReleaseNotesMetadata("{{app_check}} Play integrity", "appcheck-playintegrity", false)
"firebase-appcheck-safetynet" ->
ReleaseNotesMetadata("{{app_check}} SafetyNet", "appcheck-safetynet", false)
else -> throw StopActionException("No metadata mapping found for project: $string")
}

companion object {
/**
* Regex for GitHub issue links in change messages.
Expand Down Expand Up @@ -268,20 +193,3 @@ abstract class MakeReleaseNotesTask : DefaultTask() {
)
}
}

/**
* Provides extra metadata needed to create release notes for a given project.
*
* This data is needed for g3 internal mappings, and does not really have any implications for
* public repo actions.
*
* @property name The variable name for a project in a release note
* @property vesionName The variable name given to the versions of a project
* @property hasKTX The module has a KTX submodule (not to be confused with having KTX files)
* @see MakeReleaseNotesTask
*/
data class ReleaseNotesMetadata(
val name: String,
val versionName: String,
val hasKTX: Boolean = true
)
Loading