From 0dfe50039f77c3edfa6b66c6af1ea26fb7924443 Mon Sep 17 00:00:00 2001 From: Kenzie Davisson <43759233+kenzieschmoll@users.noreply.github.com> Date: Thu, 2 May 2024 10:26:04 -0700 Subject: [PATCH] Add a command to generate release notes on the flutter website (#7689) --- packages/devtools_app/release_notes/README.md | 29 +-- tool/RELEASE_INSTRUCTIONS.md | 18 +- tool/lib/commands/release_notes_helper.dart | 238 ++++++++++++++++++ tool/lib/devtools_command_runner.dart | 2 + 4 files changed, 262 insertions(+), 25 deletions(-) create mode 100644 tool/lib/commands/release_notes_helper.dart diff --git a/packages/devtools_app/release_notes/README.md b/packages/devtools_app/release_notes/README.md index 49c084ec1ba..5d8506e2f37 100644 --- a/packages/devtools_app/release_notes/README.md +++ b/packages/devtools_app/release_notes/README.md @@ -46,22 +46,19 @@ viewer can be used efficiently. Release notes for DevTools are hosted on the Flutter website. They are indexed at 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: - - 1. Copy the markdown from [NEXT_RELEASE_NOTES.md](NEXT_RELEASE_NOTES.md) over - to the Flutter website. This file contains the running release notes for - the current DevTools version. - - See this [PR](https://github.com/flutter/website/pull/10113) for - an example of how to add these notes to the Flutter website. - 2. Copy any images from the `images/` directory over to the Flutter website. - - Make sure to copy all images over to the proper website directory: - - `.../tools/devtools/release-notes/images-/` - - Make sure to update all image links in the markdown with the `site_url` tag: - - `/tools/devtools/release-notes/images-/` - 3. Once you are satisfied with the release notes, - create a new branch directly on the `flutter/website` repo and open a PR, - and then proceed to the testing steps below. +### Prerequisite + +Before continuing, ensure you have your local environment set up for +[contributing](https://github.com/flutter/website) to the `flutter/website` repo. + +### Creating the release notes PR + +Draft release notes on a local `flutter/website` branch using the following command: +```console +devtools_tool release-notes -w /Users/me/absolute/path/to/flutter/website +``` + +Clean up the drafted notes on your local `flutter/website` branch and open a PR. ### Testing the release notes in DevTools diff --git a/tool/RELEASE_INSTRUCTIONS.md b/tool/RELEASE_INSTRUCTIONS.md index 075e579fcd4..3516090ea08 100644 --- a/tool/RELEASE_INSTRUCTIONS.md +++ b/tool/RELEASE_INSTRUCTIONS.md @@ -195,6 +195,15 @@ just released into the Dart SDK (the hash you updated the DEPS file with): version for the tag is automatically determined from `packages/devtools/pubspec.yaml` so there is no need to manually enter the version. +### Verify and Submit the release notes + +1. Follow the instructions outlined in the release notes +[README.md](https://github.com/flutter/devtools/blob/master/packages/devtools_app/release_notes/README.md) +to add DevTools release notes to Flutter website and test them in DevTools. +2. Once release notes are submitted to the Flutter website, send an announcement to +[g/flutter-internal-announce](http://g/flutter-internal-announce) with a link to +the new release notes. + ### Prepare DevTools for the next beta release 1. Update the DevTools version for the next release: ```shell @@ -207,15 +216,6 @@ just released into the Dart SDK (the hash you updated the DEPS file with): - Go to https://github.com/flutter/devtools/pulls to see the pull request that ends up being created -### Verify and Submit the release notes - -1. Follow the instructions outlined in the release notes -[README.md](https://github.com/flutter/devtools/blob/master/packages/devtools_app/release_notes/README.md) -to add DevTools release notes to Flutter website and test them in DevTools. -2. Once release notes are submitted to the Flutter website, send an announcement to -[g/flutter-internal-announce](http://g/flutter-internal-announce) with a link to -the new release notes. - ## Dev release into the Dart SDK master branch To publish a dev release follow just section [Update the DevTools hash in the Dart SDK](#update-the-devtools-hash-in-the-dart-sdk) diff --git a/tool/lib/commands/release_notes_helper.dart b/tool/lib/commands/release_notes_helper.dart new file mode 100644 index 00000000000..038ec7f50f4 --- /dev/null +++ b/tool/lib/commands/release_notes_helper.dart @@ -0,0 +1,238 @@ +// Copyright 2024 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 'dart:async'; +import 'dart:io'; + +import 'package:args/command_runner.dart'; +import 'package:cli_util/cli_logging.dart'; +import 'package:devtools_tool/model.dart'; +import 'package:devtools_tool/utils.dart'; +import 'package:io/io.dart'; +import 'package:path/path.dart' as p; + +class ReleaseNotesCommand extends Command { + ReleaseNotesCommand() { + argParser.addOption( + _websiteRepoPath, + abbr: 'w', + help: 'The absolute path to the flutter/website repo clone on disk.', + ); + } + + static const _websiteRepoPath = 'website-repo'; + + @override + String get description => + 'Creates a PR on the flutter/website repo with the current release notes.'; + + @override + String get name => 'release-notes'; + + @override + FutureOr? run() async { + final log = Logger.standard(); + final processManager = ProcessManager(); + + final devToolsReleaseNotesDirectory = Directory( + p.join( + DevToolsRepo.getInstance().devtoolsAppDirectoryPath, + 'release_notes', + ), + ); + final devToolsReleaseNotes = _DevToolsReleaseNotes.fromFile( + File(p.join(devToolsReleaseNotesDirectory.path, 'NEXT_RELEASE_NOTES.md')), + ); + final releaseNotesVersion = devToolsReleaseNotes.version; + log.stdout( + 'Drafting release notes for DevTools version $releaseNotesVersion...', + ); + + // Create a local branch on the flutter/website repo. + final websiteRepoPath = argResults![_websiteRepoPath] as String; + try { + await processManager.runAll( + commands: [ + CliCommand.git(['stash']), + CliCommand.git(['checkout', 'main']), + CliCommand.git(['pull']), + CliCommand.git(['submodule', 'update', '--init', '--recursive']), + CliCommand.git( + ['checkout', '-b', 'devtools-release-notes-$releaseNotesVersion'], + ), + ], + workingDirectory: websiteRepoPath, + ); + } catch (e) { + log.stderr( + 'Something went wrong while trying to prepare a branch on the ' + 'flutter/website repo. Please make sure your flutter/website clone ' + 'is set up as specified by the contributing instructions: ' + 'https://github.com/flutter/website?tab=readme-ov-file#contributing.' + '\n\n$e', + ); + return; + } + + final websiteReleaseNotesDir = Directory( + p.join( + websiteRepoPath, + 'src', + 'content', + 'tools', + 'devtools', + 'release-notes', + ), + ); + if (!websiteReleaseNotesDir.existsSync()) { + throw FileSystemException( + 'Website release notes directory does not exist.', + websiteReleaseNotesDir.path, + ); + } + + // Write the 'release-notes-.md' file. + File( + p.join( + websiteReleaseNotesDir.path, + 'release-notes-$releaseNotesVersion.md', + ), + ) + ..createSync() + ..writeAsStringSync( + '''--- +short-title: $releaseNotesVersion release notes +description: Release notes for Dart and Flutter DevTools version $releaseNotesVersion. +toc: false +--- + +{% include ./release-notes-$releaseNotesVersion-src.md %} +''', + flush: true, + ); + + // Create the 'release-notes--src.md' file. + final releaseNotesSrcMd = File( + p.join( + websiteReleaseNotesDir.path, + 'release-notes-$releaseNotesVersion-src.md', + ), + )..createSync(); + + final srcLines = devToolsReleaseNotes.srcLines; + + // Copy release notes images and fix image reference paths. + if (devToolsReleaseNotes.imageLineIndices.isNotEmpty) { + // This set of release notes contains images. Perform the line + // transformations and copy the image files. + final websiteImagesDirName = 'images-$releaseNotesVersion'; + final devtoolsImagesDir = + Directory(p.join(devToolsReleaseNotesDirectory.path, 'images')); + final websiteImagesDir = Directory( + p.join(websiteReleaseNotesDir.path, websiteImagesDirName), + )..createSync(); + await copyPath(devtoolsImagesDir.path, websiteImagesDir.path); + + // Remove the .gitkeep file that was copied over. + File(p.join(websiteImagesDir.path, '.gitkeep')).deleteSync(); + + for (final index in devToolsReleaseNotes.imageLineIndices) { + final line = srcLines[index]; + final transformed = line.replaceFirst( + _DevToolsReleaseNotes._imagePathMarker, + '/tools/devtools/release-notes/$websiteImagesDirName', + ); + srcLines[index] = transformed; + } + } + + // Write the 'release-notes--src.md' file, including any updates for + // image paths. + releaseNotesSrcMd.writeAsStringSync( + srcLines.joinWithNewLine(), + flush: true, + ); + + // Write the 'devtools_releases.yml' file. + final releasesYml = + File(p.join(websiteRepoPath, 'src', '_data', 'devtools_releases.yml')); + if (!releasesYml.existsSync()) { + throw FileSystemException( + 'The devtools_releases.yml file does not exist.', + releasesYml.path, + ); + } + final releasesYmlContent = + releasesYml.readAsStringSync().replaceFirst('releases:', '''releases: + - '$releaseNotesVersion\''''); + releasesYml.writeAsStringSync(releasesYmlContent, flush: true); + + log.stdout( + 'Release notes successfully drafted in a local flutter/website branch. ' + 'Please clean them up by deleting empty sections and fixing any grammar ' + 'mistakes or typos.\n\nCreate a PR on the flutter/website repo when you are ' + 'finished.', + ); + } +} + +class _DevToolsReleaseNotes { + _DevToolsReleaseNotes._({ + required this.file, + required this.version, + required this.srcLines, + required this.imageLineIndices, + }); + + factory _DevToolsReleaseNotes.fromFile(File file) { + if (!file.existsSync()) { + throw FileSystemException( + 'NEXT_RELEASE_NOTES.md file does not exist.', + file.path, + ); + } + + final rawLines = file.readAsLinesSync(); + + late String version; + late int titleLineIndex; + final versionRegExp = RegExp(r"\d+\.\d+\.\d+"); + for (int i = 0; i < rawLines.length; i++) { + final line = rawLines[i]; + final matches = versionRegExp.allMatches(line); + if (matches.isEmpty) continue; + // This match should be from the line "# DevTools release notes". + version = matches.first.group(0)!; + // This is the markdown title where the release notes src begins. + titleLineIndex = i; + break; + } + + // TODO(kenz): one nice polish task could be to remove sections that are + // empty (i.e. sections that have the line + // "TODO: Remove this section if there are not any general updates."). + final srcLines = rawLines.sublist(titleLineIndex); + final imageLineIndices = {}; + for (int i = 0; i < srcLines.length; i++) { + final line = srcLines[i]; + if (line.contains(_imagePathMarker)) { + imageLineIndices.add(i); + } + } + + return _DevToolsReleaseNotes._( + file: file, + version: version, + srcLines: srcLines, + imageLineIndices: imageLineIndices, + ); + } + + final File file; + final String version; + final List srcLines; + final Set imageLineIndices; + + static const _imagePathMarker = './images/'; +} diff --git a/tool/lib/devtools_command_runner.dart b/tool/lib/devtools_command_runner.dart index 58c6b52bbf7..9d5caf44823 100644 --- a/tool/lib/devtools_command_runner.dart +++ b/tool/lib/devtools_command_runner.dart @@ -7,6 +7,7 @@ import 'package:args/command_runner.dart'; import 'package:devtools_tool/commands/build.dart'; import 'package:devtools_tool/commands/fix_goldens.dart'; import 'package:devtools_tool/commands/generate_code.dart'; +import 'package:devtools_tool/commands/release_notes_helper.dart'; import 'package:devtools_tool/commands/serve.dart'; import 'package:devtools_tool/commands/sync.dart'; import 'package:devtools_tool/commands/tag_version.dart'; @@ -35,6 +36,7 @@ class DevToolsCommandRunner extends CommandRunner { addCommand(ListCommand()); addCommand(PubGetCommand()); addCommand(ReleaseHelperCommand()); + addCommand(ReleaseNotesCommand()); addCommand(RepoCheckCommand()); addCommand(RollbackCommand()); addCommand(ServeCommand());