From ec553d9f834a399f8d0bb476a71230a7f9dd3d9a Mon Sep 17 00:00:00 2001 From: Tarek Belkahia Date: Wed, 4 Sep 2024 12:50:14 +0100 Subject: [PATCH] Fetch, format and display changelog in the CLI update message (#1950) * Fetch, format and display changelog in the CLI update message * Refactor changelog fetching and add unit tests --- maestro-cli/src/main/java/maestro/cli/App.kt | 16 +++- .../main/java/maestro/cli/update/Updates.kt | 38 +++++++++ .../java/maestro/cli/util/ChangeLogUtils.kt | 27 +++++++ .../maestro/cli/util/ChangeLogUtilsTest.kt | 78 +++++++++++++++++++ 4 files changed, 155 insertions(+), 4 deletions(-) create mode 100644 maestro-cli/src/main/java/maestro/cli/util/ChangeLogUtils.kt create mode 100644 maestro-cli/src/test/kotlin/maestro/cli/util/ChangeLogUtilsTest.kt diff --git a/maestro-cli/src/main/java/maestro/cli/App.kt b/maestro-cli/src/main/java/maestro/cli/App.kt index 247a2dd55a..eda343607c 100644 --- a/maestro-cli/src/main/java/maestro/cli/App.kt +++ b/maestro-cli/src/main/java/maestro/cli/App.kt @@ -42,6 +42,7 @@ import picocli.CommandLine.Command import picocli.CommandLine.Option import java.util.Properties import kotlin.system.exitProcess +import maestro.cli.util.ChangeLogUtils @Command( name = "maestro", @@ -147,11 +148,18 @@ fun main(args: Array) { val newVersion = Updates.checkForUpdates() if (newVersion != null) { + Updates.fetchChangelogAsync() System.err.println() - System.err.println( - ("A new version of the Maestro CLI is available ($newVersion). Upgrade command:\n" + - "curl -Ls \"https://get.maestro.mobile.dev\" | bash").box() - ) + val changelog = Updates.getChangelog() + val anchor = newVersion.toString().replace(".", "") + System.err.println(listOf( + "A new version of the Maestro CLI is available ($newVersion).\n", + "See what's new:", + "https://github.com/mobile-dev-inc/maestro/blob/main/CHANGELOG.md#$anchor", + ChangeLogUtils.print(changelog), + "Upgrade command:", + "curl -Ls \"https://get.maestro.mobile.dev\" | bash", + ).joinToString("\n").box()) } if (commandLine.isVersionHelpRequested) { diff --git a/maestro-cli/src/main/java/maestro/cli/update/Updates.kt b/maestro-cli/src/main/java/maestro/cli/update/Updates.kt index 052be8aa2e..c9f5df2351 100644 --- a/maestro-cli/src/main/java/maestro/cli/update/Updates.kt +++ b/maestro-cli/src/main/java/maestro/cli/update/Updates.kt @@ -7,6 +7,8 @@ import maestro.cli.util.EnvUtils.CLI_VERSION import java.util.concurrent.CompletableFuture import java.util.concurrent.Executors import java.util.concurrent.TimeUnit +import maestro.cli.util.ChangeLogUtils +import maestro.cli.util.ChangeLog object Updates { private val DEFAULT_THREAD_FACTORY = Executors.defaultThreadFactory() @@ -15,11 +17,16 @@ object Updates { } private var future: CompletableFuture? = null + private var changelogFuture: CompletableFuture>? = null fun fetchUpdatesAsync() { getFuture() } + fun fetchChangelogAsync() { + getChangelogFuture() + } + fun checkForUpdates(): CliVersion? { // Disable update check, when MAESTRO_DISABLE_UPDATE_CHECK is set to "true" e.g. when installed by a package manager. e.g. nix if (System.getenv("MAESTRO_DISABLE_UPDATE_CHECK")?.toBoolean() == true) { @@ -32,6 +39,18 @@ object Updates { } } + fun getChangelog(): List? { + // Disable update check, when MAESTRO_DISABLE_UPDATE_CHECK is set to "true" e.g. when installed by a package manager. e.g. nix + if (System.getenv("MAESTRO_DISABLE_UPDATE_CHECK")?.toBoolean() == true) { + return null + } + return try { + getChangelogFuture().get(3, TimeUnit.SECONDS) + } catch (e: Exception) { + return null + } + } + private fun fetchUpdates(): CliVersion? { if (CLI_VERSION == null) { return null @@ -46,6 +65,15 @@ object Updates { } } + private fun fetchChangelog(): ChangeLog { + if (CLI_VERSION == null) { + return null + } + val version = fetchUpdates()?.toString() ?: return null + val content = ChangeLogUtils.fetchContent() + return ChangeLogUtils.formatBody(content, version) + } + @Synchronized private fun getFuture(): CompletableFuture { var future = this.future @@ -55,4 +83,14 @@ object Updates { } return future } + + @Synchronized + private fun getChangelogFuture(): CompletableFuture> { + var changelogFuture = this.changelogFuture + if (changelogFuture == null) { + changelogFuture = CompletableFuture.supplyAsync(this::fetchChangelog, EXECUTOR)!! + this.changelogFuture = changelogFuture + } + return changelogFuture + } } diff --git a/maestro-cli/src/main/java/maestro/cli/util/ChangeLogUtils.kt b/maestro-cli/src/main/java/maestro/cli/util/ChangeLogUtils.kt new file mode 100644 index 0000000000..b6ce4e6ead --- /dev/null +++ b/maestro-cli/src/main/java/maestro/cli/util/ChangeLogUtils.kt @@ -0,0 +1,27 @@ +package maestro.cli.util + +import okhttp3.OkHttpClient +import okhttp3.Request + +typealias ChangeLog = List? + +object ChangeLogUtils { + + fun formatBody(content: String?, version: String): ChangeLog = content + ?.split("\n## ")?.map { it.lines() } + ?.first { it.first().startsWith(version) } + ?.drop(1) + ?.map { it.trim().replace("**", "") } + ?.map { it.replace("\\[(.*?)]\\(.*?\\)".toRegex(), "$1") } + ?.filter { it.isNotEmpty() && it.startsWith("- ") } + + fun fetchContent(): String? { + val request = Request.Builder() + .url("https://raw.githubusercontent.com/mobile-dev-inc/maestro/main/CHANGELOG.md") + .build() + return OkHttpClient().newCall(request).execute().body?.string() + } + + fun print(changelog: ChangeLog): String = + changelog?.let { "\n${it.joinToString("\n")}\n" }.orEmpty() +} diff --git a/maestro-cli/src/test/kotlin/maestro/cli/util/ChangeLogUtilsTest.kt b/maestro-cli/src/test/kotlin/maestro/cli/util/ChangeLogUtilsTest.kt new file mode 100644 index 0000000000..d506fd7e68 --- /dev/null +++ b/maestro-cli/src/test/kotlin/maestro/cli/util/ChangeLogUtilsTest.kt @@ -0,0 +1,78 @@ +package maestro.cli.util + +import com.google.common.truth.Truth.assertThat +import java.io.File +import org.junit.jupiter.api.Test + +class ChangeLogUtilsTest { + + private val changelogFile = File(System.getProperty("user.dir"), "../CHANGELOG.md") + + @Test + fun `test format last version`() { + val content = changelogFile.readText() + + val changelog = ChangeLogUtils.formatBody(content, "1.38.1") + + assertThat(changelog).containsExactly( + "- New commands AI visual testing: assertWithAI and assertNoDefectsWithAI", + "- Enable basic support for Maestro uploads while keeping maestro cloud functioning", + ) + } + + @Test + fun `test format link and no paragraph`() { + val content = changelogFile.readText() + + val changelog = ChangeLogUtils.formatBody(content, "1.37.9") + + assertThat(changelog).containsExactly( + "- Revert iOS landscape mode fix (#1916)", + ) + } + + @Test + fun `test format no subheader`() { + val content = changelogFile.readText() + + val changelog = ChangeLogUtils.formatBody(content, "1.37.1") + + assertThat(changelog).containsExactly( + "- Fix crash when `flutter` or `xcodebuild` is not installed (#1839)", + ) + } + + @Test + fun `test format strong no paragraph and no sublist`() { + val content = changelogFile.readText() + + val changelog = ChangeLogUtils.formatBody(content, "1.37.0") + + assertThat(changelog).containsExactly( + "- Sharding tests for parallel execution on many devices šŸŽ‰Ā (#1732 by Kaan)", + "- Reports in HTML (#1750 by Depa Panjie Purnama)", + "- Homebrew is back!", + "- Current platform exposed in JavaScript (#1747 by Dan Caseley)", + "- Control airplaneĀ mode (#1672 by NyCodeGHG)", + "- New `killApp` command (#1727 by Alexandre Favre)", + "- Fix cleaning up retries in iOS driver (#1669)", + "- Fix some commands not respecting custom labels (#1762 by Dan Caseley)", + "- Fix ā€œProtocol family unavailableā€ when rerunning iOS tests (#1671 by Stanisław Chmiela)", + ) + } + + @Test + fun `test print`() { + val content = changelogFile.readText() + val changelog = ChangeLogUtils.formatBody(content, "1.17.1") + + val printed = ChangeLogUtils.print(changelog) + + assertThat(printed).isEqualTo( + "\n" + + "- Tweak: Remove Maestro Studio icon from Mac dock\n" + + "- Tweak: Prefer port 9999 for Maestro Studio app\n" + + "- Fix: Fix Maestro Studio conditional code snippet\n" + ) + } +}