diff --git a/packages/devtools_app/lib/src/framework/about_dialog.dart b/packages/devtools_app/lib/src/framework/about_dialog.dart index 46a6967bd90..0803ff994b4 100644 --- a/packages/devtools_app/lib/src/framework/about_dialog.dart +++ b/packages/devtools_app/lib/src/framework/about_dialog.dart @@ -38,7 +38,8 @@ class DevToolsAboutDialog extends StatelessWidget { 'release notes', style: theme.linkTextStyle, ), - onTap: () => releaseNotesController.toggleVisibility(true), + onTap: () => + unawaited(releaseNotesController.openLatestReleaseNotes()), ), ], ), diff --git a/packages/devtools_app/lib/src/framework/release_notes/release_notes.dart b/packages/devtools_app/lib/src/framework/release_notes/release_notes.dart index 4c20ea70899..03ecd9df04f 100644 --- a/packages/devtools_app/lib/src/framework/release_notes/release_notes.dart +++ b/packages/devtools_app/lib/src/framework/release_notes/release_notes.dart @@ -15,7 +15,10 @@ import '../../shared/side_panel.dart'; final _log = Logger('release_notes'); -const debugTestReleaseNotes = false; +// This is not const because it is manipulated for testing as well as for +// local development. +bool debugTestReleaseNotes = false; +const debugUseStagedFlutterWebsite = false; const releaseNotesKey = Key('release_notes'); class ReleaseNotesViewer extends SidePanelViewer { @@ -37,17 +40,17 @@ class ReleaseNotesController extends SidePanelController { static const _unsupportedPathSyntax = '{{site.url}}'; - String get _flutterDocsSite => debugTestReleaseNotes + String get _flutterDocsSite => debugUseStagedFlutterWebsite ? 'https://flutter-website-dt-staging.web.app' : 'https://docs.flutter.dev'; void _init() { if (debugTestReleaseNotes || server.isDevToolsServerAvailable) { - _maybeFetchReleaseNotes(); + _maybeShowReleaseNotes(); } } - void _maybeFetchReleaseNotes() async { + void _maybeShowReleaseNotes() async { SemanticVersion previousVersion = SemanticVersion(); if (server.isDevToolsServerAvailable) { final lastReleaseNotesShownVersion = @@ -56,63 +59,102 @@ class ReleaseNotesController extends SidePanelController { previousVersion = SemanticVersion.parse(lastReleaseNotesShownVersion); } } + await _fetchAndShowReleaseNotes( + versionFloor: debugTestReleaseNotes ? null : previousVersion, + ); + } + + /// Fetches and shows the most recent release notes for the current DevTools + /// version, decreasing the patch version by 1 each time until we find release + /// notes or until we hit [versionFloor]. + Future _fetchAndShowReleaseNotes({ + SemanticVersion? versionFloor, + }) async { + versionFloor ??= SemanticVersion(); + // Parse the current version instead of using [devtools.version] directly to // strip off any build metadata (any characters following a '+' character). // Release notes will be hosted on the Flutter website with a version number // that does not contain any build metadata. - final parsedCurrentVersion = SemanticVersion.parse(devtools.version); - final parsedCurrentVersionStr = parsedCurrentVersion.toString(); - if (parsedCurrentVersion > previousVersion) { - try { - await _fetchReleaseNotes(parsedCurrentVersion); - } catch (e) { - // Fail gracefully if we cannot find release notes for the current - // version of DevTools. - markdownText = null; - toggleVisibility(false); - _log.warning( - 'Warning: could not find release notes for DevTools version ' - '$parsedCurrentVersionStr. $e', - ); + final parsedVersion = SemanticVersion.parse(devtools.version); + var notesVersion = latestVersionToCheckForReleaseNotes(parsedVersion); + try { + // Try all patch versions for this major.minor combination until we find + // release notes (e.g. 2.11.4 -> 2.11.3 -> 2.11.2 -> ...). + final attemptedVersions = []; + var attempts = notesVersion.patch; + while (attempts >= 0 && notesVersion > versionFloor) { + final versionString = notesVersion.toString(); + try { + String releaseNotesMarkdown = await http.read( + Uri.parse(_releaseNotesUrl(versionString)), + ); + // This is a workaround so that the images in release notes will appear. + // The {{site.url}} syntax is best practices for the flutter website + // repo, where these release notes are hosted, so we are performing this + // workaround on our end to ensure the images render properly. + releaseNotesMarkdown = releaseNotesMarkdown.replaceAll( + _unsupportedPathSyntax, + _flutterDocsSite, + ); + + markdown.value = releaseNotesMarkdown; + toggleVisibility(true); + if (server.isDevToolsServerAvailable) { + unawaited( + server.setLastShownReleaseNotesVersion(versionString), + ); + } + return; + } catch (e) { + attempts--; + attemptedVersions.add(versionString); + if (attempts < 0) { + // ignore: avoid-throw-in-catch-block, false positive + throw Exception( + 'Could not find release notes for DevTools versions ' + '${attemptedVersions.join(', ')}.' + '\n$e', + ); + } + notesVersion = notesVersion.downgrade(downgradePatch: true); + } } + } catch (e) { + // Fail gracefully if we cannot find release notes for the current + // version of DevTools. + markdown.value = null; + toggleVisibility(false); + _log.warning('Warning: $e'); } } - Future _fetchReleaseNotes(SemanticVersion version) async { - final currentVersionString = version.toString(); - - // Try all patch versions for this major.minor combination until we find - // release notes (e.g. 2.11.4 -> 2.11.3 -> 2.11.2 -> ...). - var attempts = version.patch; - while (attempts >= 0) { - final versionString = version.toString(); - try { - String releaseNotesMarkdown = await http.read( - Uri.parse(_releaseNotesUrl(versionString)), - ); - // This is a workaround so that the images in release notes will appear. - // The {{site.url}} syntax is best practices for the flutter website - // repo, where these release notes are hosted, so we are performing this - // workaround on our end to ensure the images render properly. - releaseNotesMarkdown = releaseNotesMarkdown.replaceAll( - _unsupportedPathSyntax, - _flutterDocsSite, - ); + @visibleForTesting + SemanticVersion latestVersionToCheckForReleaseNotes( + SemanticVersion currentVersion, + ) { + // If the current version is a pre-release, downgrade the minor to find the + // previous DevTools release, and start looking for release notes from this + // value. Release notes will never be published for pre-release versions. + if (currentVersion.isPreRelease) { + // It is very unlikely the patch value of the DevTools version will ever + // be above this number. This is a safe number to start looking for + // release notes at. + const safeStartPatch = 10; + currentVersion = SemanticVersion( + major: currentVersion.major, + minor: currentVersion.minor - 1, + patch: safeStartPatch, + ); + } + return currentVersion; + } - markdownText = releaseNotesMarkdown; - toggleVisibility(true); - unawaited( - server.setLastShownReleaseNotesVersion(currentVersionString), - ); - return; - } catch (_) { - attempts--; - if (attempts < 0) { - rethrow; - } - version = version.downgrade(downgradePatch: true); - } + Future openLatestReleaseNotes() async { + if (markdown.value == null) { + await _fetchAndShowReleaseNotes(); } + toggleVisibility(true); } String _releaseNotesUrl(String currentVersion) { diff --git a/packages/devtools_app/lib/src/shared/side_panel.dart b/packages/devtools_app/lib/src/shared/side_panel.dart index bdbd0b553d3..7faf62a59a3 100644 --- a/packages/devtools_app/lib/src/shared/side_panel.dart +++ b/packages/devtools_app/lib/src/shared/side_panel.dart @@ -194,9 +194,7 @@ class SidePanel extends AnimatedWidget { } class SidePanelController { - ValueListenable get markdown => _markdown; - - final _markdown = ValueNotifier(null); + final markdown = ValueNotifier(null); ValueListenable get isVisible => _isVisible; @@ -205,8 +203,4 @@ class SidePanelController { void toggleVisibility(bool visible) { _isVisible.value = visible; } - - set markdownText(String? markdownText) { - _markdown.value = markdownText; - } } diff --git a/packages/devtools_app/release_notes/README.md b/packages/devtools_app/release_notes/README.md index 88864879ef8..3b462dbb78c 100644 --- a/packages/devtools_app/release_notes/README.md +++ b/packages/devtools_app/release_notes/README.md @@ -1,5 +1,5 @@ ## Generating Release notes -- Release notes for DevTools are hosted on the flutter website (see [archive](https://docs.flutter.dev/development/tools/devtools/release-notes)). +- Release notes for DevTools are hosted on the flutter website (see [archive](https://docs.flutter.dev/tools/devtools/release-notes)). - To add release notes for the latest release, create a PR with the appropriate changes for your release. - The [NEXT_RELEASE_NOTES.md](NEXT_RELEASE_NOTES.md) file contains the running release notes for the current version. - see example [PR](https://github.com/flutter/website/pull/6791) for an idea of how to add those to the Flutter website. @@ -7,7 +7,7 @@ - Test these changes locally before creating the PR. - See [README.md](https://github.com/flutter/website/blob/main/README.md) for getting setup to run the Flutter website locally. - - Release notes can be found at [http://localhost:4002/development/tools/devtools/release-notes/](http://localhost:4002/development/tools/devtools/release-notes/) + - Release notes can be found at [http://localhost:4002/development/tools/devtools/release-notes/](http://localhost:4002/tools/devtools/release-notes/) - Once you are satisfied with the release notes - stage the Flutter website on Firebase @@ -47,7 +47,7 @@ for getting setup to run the Flutter website locally. ``` ### Testing the release notes in DevTools -- In `release_notes.dart` flip the `debugTestReleaseNotes` flag to true. +- In `release_notes.dart` flip the `debugTestReleaseNotes` and `debugUseStagedFlutterWebsite` flags to true. - from the main `devtools/` directory, run the following: ```shell @@ -64,4 +64,3 @@ Serving DevTools at http://127.0.0.1:57336. - Visit the DevTools link - verify the release notes viewer displays the new release notes as expected. - diff --git a/packages/devtools_app/test/shared/release_notes_test.dart b/packages/devtools_app/test/shared/release_notes_test.dart new file mode 100644 index 00000000000..62f803875da --- /dev/null +++ b/packages/devtools_app/test/shared/release_notes_test.dart @@ -0,0 +1,29 @@ +// Copyright 2023 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:devtools_app/devtools_app.dart'; +import 'package:devtools_shared/devtools_shared.dart'; +import 'package:flutter_test/flutter_test.dart'; + +void main() { + group('$ReleaseNotesController', () { + late ReleaseNotesController controller; + setUp(() { + debugTestReleaseNotes = true; + controller = ReleaseNotesController(); + }); + + test('latestVersionToCheckForReleaseNotes', () { + var version = controller.latestVersionToCheckForReleaseNotes( + SemanticVersion.parse('2.24.5-dev.1'), + ); + expect(version.toString(), '2.23.10'); + + version = controller.latestVersionToCheckForReleaseNotes( + SemanticVersion.parse('2.24.1'), + ); + expect(version.toString(), '2.24.1'); + }); + }); +}