diff --git a/.agent/skills/genui-helper/SKILL.md b/.agent/skills/genui-helper/SKILL.md index d74154b53..e1e10efd9 100644 --- a/.agent/skills/genui-helper/SKILL.md +++ b/.agent/skills/genui-helper/SKILL.md @@ -51,9 +51,9 @@ When creating a new UI component in `genui`: ## References - A2UI Specification - - Available in the submodule at @packages/genui/submodules/a2ui - - The specification documentation is available in @packages/genui/submodules/a2ui/specification/v0.9/docs - - The specification schemas are available in @packages/genui/submodules/a2ui/specification/v0.9/json + - Available in the submodule at @submodules/a2ui + - The specification documentation is available in @submodules/a2ui/specification/v0.9/docs + - The specification schemas are available in @submodules/a2ui/specification/v0.9/json - Because it is a submodule, you may need to update the submodule to get the latest specification. - To find out details of a specific dart compiler diagnostic message, use the following url format to look up the details: - https://dart.dev/tools/diagnostics/ diff --git a/.github/workflows/post_summaries.yaml b/.github/workflows/post_summaries.yaml new file mode 100644 index 000000000..8e9171ae7 --- /dev/null +++ b/.github/workflows/post_summaries.yaml @@ -0,0 +1,20 @@ +# Copyright 2025 The Flutter Authors. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# A CI configuration for pub-publish to write comments on PRs. + +name: Comment on the pull request + +on: + workflow_run: + workflows: + - Publish + types: + - completed + +jobs: + upload: + uses: dart-lang/ecosystem/.github/workflows/post_summaries.yaml@main + permissions: + pull-requests: write \ No newline at end of file diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 000000000..81f6ff8cd --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,29 @@ +# Copyright 2025 The Flutter Authors. +# Use of this source code is governed by a BSD-style license that can be +# found in the LICENSE file. + +# A CI configuration to auto-publish pub packages. + +name: Publish + +on: + pull_request: + branches: [ main ] + types: [opened, synchronize, reopened, labeled, unlabeled] + push: + # Match -v publish tags + tags: [ '[A-z0-9]+-v[0-9]+.[0-9]+.[0-9]+' ] + +jobs: + publish: + if: ${{ github.repository_owner == 'flutter' }} + uses: dart-lang/ecosystem/.github/workflows/publish.yaml@main + with: + # See https://github.com/dart-lang/ecosystem/tree/main/pkgs/firehose#options + sdk: beta # version of dart sdk to use for publishing + use-flutter: true + write-comments: false + checkout_submodules: false + permissions: + id-token: write + pull-requests: write diff --git a/.gitmodules b/.gitmodules index 8d7b0c972..60a71f889 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,7 +1,7 @@ -[submodule "packages/json_schema_builder/submodules/JSON-Schema-Test-Suite"] - path = packages/json_schema_builder/submodules/JSON-Schema-Test-Suite +[submodule "JSON-Schema-Test-Suite"] + path = submodules/JSON-Schema-Test-Suite url = https://github.com/json-schema-org/JSON-Schema-Test-Suite.git [submodule "a2ui"] - path = packages/genui/submodules/a2ui + path = submodules/a2ui url = https://github.com/google/A2UI.git branch = main diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2e4e2ef32..24d040932 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -1,25 +1,10 @@ -# Contributing to GenUI for Flutter +# Contributing to this repository -## Guidelines +## Coding guidelines -Please follow [Flutter contributor guidelines][flutter_guidelines]. - -## Run Examples - -To run examples: - -1. Configure Firebase as described in [README.md][readme_md]. -2. Run `flutter run`. - -NOTE: For Google-internal projects see go/flutter-genui-internal. - -## Shell scripts - -To run a script in `tool/`, open the script in VSCode and press ⇧⌘B. - -## Detailed documentation for contributors - -See [docs/contributing.md](docs/contributing.md). +Please follow: + * [Flutter-wide contributor guidelines][flutter_guidelines]. + * [A2UI-specific guidelines](docs/contributing/README.md). ## Issue triage @@ -57,7 +42,7 @@ of the front-line triage include: ### Periodic second-line triage -### Bi-weekly during the planning meeting +#### Bi-weekly during the planning meeting Check that existing issues are labeled and organized appropriately: @@ -66,7 +51,7 @@ Check that existing issues are labeled and organized appropriately: * Set a milestone to all [P0 and P1 issues][p0_p1_issues_without_milestone]. * Add all [projectless open issues][projectless_open_issues] to the "genui" project. -### Weekly during the planning meeting +#### Weekly during the planning meeting Triage issues ready for second-line review: @@ -82,40 +67,15 @@ Triage issues ready for second-line review: At the end of a triage session, the untriaged issue list should be as close to empty as possible. -## Versioning - -We use [Semver] for package versioning, although before 1.0.0, we will be -incrementing only the minor number for breaking changes and the patch number for -non-breaking changes. After 1.0.0, we will be using standard Semver, bumping the -major number for breaking changes. - -We release the following packages in lock step, -with the same version number, so when one is released, they are all released: - -* `genui` -* `genui_a2a` -* `genui_firebase_ai` -* `genui_google_generative_ui` - -These packages are released independently on their own schedule, with their -own version number: +## Internal information -* `genai_primitives` -* `json_schema_builder` +For Google-internal information see go/a2ui-internal. -"Releasing" consititutes manually publishing them all to [pub.dev] after the -pull request containing the version bump has passed CI. The packages must be -published by someone with permission to publish under the labs.flutter.org -owner. -Use the [release tool](tool/release/README.md) to help automate the process of -releasing a new version. + -[pub.dev]: https://pub.dev -[Semver]: https://semver.org/ [for-front-line]: https://github.com/flutter/genui/issues?q=is%3Aissue%20state%3Aopen%20-label%3AP0%20%20-label%3AP1%20-label%3AP2%20%20-label%3AP3%20-label%3Afront-line-handled [flutter_guidelines]: https://github.com/flutter/flutter/blob/master/CONTRIBUTING.md -[readme_md]: packages/genui/README.md#configure-firebase-ai-logic [assigned_p2_p3_issues]: https://github.com/flutter/genui/issues?q=is%3Aopen%20is%3Aissue%20label%3AP2%2CP3%20assignee%3A* [p0_p1_issues_without_milestone]: https://github.com/flutter/genui/issues?q=is%3Aopen%20is%3Aissue%20label%3AP1%2CP0%20no%3Amilestone [projectless_open_issues]: https://github.com/flutter/genui/issues?q=is%3Aopen%20is%3Aissue%20no%3Aproject @@ -124,14 +84,3 @@ releasing a new version. [P1]: https://github.com/flutter/genui/labels?q=P1 [P2]: https://github.com/flutter/genui/labels?q=P2 [P3]: https://github.com/flutter/genui/labels?q=P3 - -## pubspec.lock files - -`pubspec.lock` files are not git ignored to make the bots faster. - -If you include `pubspec.lock` file to your PR, make sure to run `flutter pub upgrade`, -when your Flutter is latest at beta channel. - -## Internal information - -For Google-internal information see go/a2ui-internal. diff --git a/analysis_options.yaml b/analysis_options.yaml index 60b7cd58f..c5c631aed 100644 --- a/analysis_options.yaml +++ b/analysis_options.yaml @@ -6,7 +6,7 @@ include: package:lints/recommended.yaml analyzer: exclude: - - 'packages/genui/submodules/**' + - 'submodules/**' language: strict-casts: true strict-inference: true diff --git a/docs/contributing/README.md b/docs/contributing/README.md index 132385aff..a589e8e3f 100644 --- a/docs/contributing/README.md +++ b/docs/contributing/README.md @@ -1,13 +1,17 @@ -# GenUI specifications +# Contributing to this repository This folder provides guidance for contributors, targeted at both AI models and human developers. -## Index of Specifications +## Index of specifications This directory contains the following specifications: -- [Style Guide](styleguide.md) +- [Style guide](styleguide.md) +- [Design](design.md) +- [Pull requests](pull_requests.md) +- [Publishing](publishing.md) +- [Examples](../../examples/README.md) ## Note for AI models @@ -23,15 +27,21 @@ I have read and understood ./docs/contributing/README.md. 1. Documentation in the repository (all .md files) should be clear, consistent, concise and up-to-date. 2. Documentation should not contain details that are easy to infer from the code. 3. If code does not match the documentation, there should be TODO comments in the code to signal the discrepancy should be resolved. +4. For documentation use [sentence case for headings](https://developers.google.com/style/capitalization#capitalization-in-titles-and-headings). -## Code reviews +## Shell scripts -Do not review pull requests when they are in draft state, unless explicitly requested by the author. +To run a script in `tool/`, open the script in VSCode and press ⇧⌘B. -## Key commands -- **Run all checks and tests:** - ```bash - ./tool/run_all_tests_and_fixes.sh - ``` +## pubspec.lock files + +`pubspec.lock` files are not git ignored to make the bots faster. + +If you include `pubspec.lock` file to your PR, make sure to run `flutter pub upgrade`, +when your Flutter is latest at beta channel. + + + +[Semver]: https://semver.org/ diff --git a/docs/contributing/publishing.md b/docs/contributing/publishing.md new file mode 100644 index 000000000..d41109888 --- /dev/null +++ b/docs/contributing/publishing.md @@ -0,0 +1,55 @@ + +# Publishing + +Publishing to [pub.dev](https://pub.dev) happens automatically via GitHub Actions, with the help of +[firehose rules](https://github.com/dart-lang/ecosystem/tree/main/pkgs/firehose). + +There are two CI workflows that enable this automation: + +1. [post_summaries.yaml](../../.github/workflows/post_summaries.yaml) - job `publish / validate` runs on pre-submit. +2. [publish.yaml](../../.github/workflows/publish.yaml) - job `publish / publish` runs on tagging. + +## Passing the publish / validate job + +In general, the job [publish / validate](https://github.com/flutter/genui/actions/workflows/post_summaries.yaml) checks if all pub.dev packages are ready for publishing. + +To make sure your PR passes this validation, follow [firehose rules](https://github.com/dart-lang/ecosystem/tree/main/pkgs/firehose). + +## Package categories + +Packages in this repo fall into the following categories: + +1. **Not published**: `pubspec.yaml` contains `publish_to: none`. Workspace tools and example apps that are never pushed to pub.dev. +2. **Not yet published**: the package's `version:` ends with a `-dev` suffix (see "`-dev` vs non-`-dev`" below). Published to pub.dev only to reserve the name; not ready for general use yet. +3. **Published**: any other package. Each has its own version cadence on pub.dev. + +Note: `resolution: workspace` in a `pubspec.yaml` is a tooling concern — it tells Dart to share dependency resolution and a lockfile with the monorepo, and it does **not** by itself imply anything about release cadence. A package can opt out of the workspace (omit `resolution: workspace`) to avoid circular dependencies or unrelated update churn while still being a published package. + +## `-dev` vs non-`-dev` (production ready) versions + +The packages code should be always release ready. That means: + +1. Use `-dev` version if **at least one** of the following statements is true: + + 1.1. The package is planned to be released in the future. In this case it is published with `-dev` suffix in order to reserve the package name. + + 1.2. The package's changes touch only non-publishable code or docs (like tests, tools, or not-publishable docs). + + You can publish `-dev` versions, if you need it for development. + +2. If your feature is partially implemented, hide the feature's code behind a false-by-default flag, and use **release-ready** version. + +## Versioning + +We use [Semver] for package versioning, although before 1.0.0, we will be +incrementing only the minor number for breaking changes and the patch number for +non-breaking changes. After 1.0.0, we will be using standard Semver, bumping the +major number for breaking changes. + + + +[Semver]: https://semver.org/ + +## How publishing happens? + +TODO(polina-c): add information, https://github.com/google/A2UI/issues/1383 diff --git a/docs/contributing/pull_requests.md b/docs/contributing/pull_requests.md new file mode 100644 index 000000000..f39833ce3 --- /dev/null +++ b/docs/contributing/pull_requests.md @@ -0,0 +1,17 @@ +# Authoring pull requests + +## Make your PR easy to review + +1. Make sure your PR has meaningful title and description. +2. Make sure your PR is not too large. Smaller PRs are easier to review. +3. Separate code reorgs from feature changes. + +## CI presubmit errors + +You may get CI presubmit errors on pull requests for several reasons. This section explains how to fix some of the less obvious ones. + +### From `publish / validate` job + +In general, the job checks if all [pub.dev](https://pub.dev) packages are release ready. + +See [publishing.md](publishing.md) for more details. diff --git a/packages/a2ui_core/CHANGELOG.md b/packages/a2ui_core/CHANGELOG.md index 35062b993..02e519a76 100644 --- a/packages/a2ui_core/CHANGELOG.md +++ b/packages/a2ui_core/CHANGELOG.md @@ -1,5 +1,5 @@ # `a2ui_core` Changelog -## 0.0.1 (in progress) +## 0.0.1-dev002 - Initial version. \ No newline at end of file diff --git a/packages/genai_primitives/CHANGELOG.md b/packages/genai_primitives/CHANGELOG.md index f7cea1865..2462a5388 100644 --- a/packages/genai_primitives/CHANGELOG.md +++ b/packages/genai_primitives/CHANGELOG.md @@ -1,9 +1,5 @@ # `genai_primitives` Changelog -## 0.2.4 (in progress) - -- **Refactor**: Update core framework to v0.9 (#546dab9be). - ## 0.2.3 - **Feature**: Add methods `copyWith` and `concatenate` to `ChatMessage` (#760). diff --git a/packages/genui/CHANGELOG.md b/packages/genui/CHANGELOG.md index e7f995bc7..1111a7413 100644 --- a/packages/genui/CHANGELOG.md +++ b/packages/genui/CHANGELOG.md @@ -1,6 +1,6 @@ # `genui` Changelog -## (WIP) +## 0.9.1 - **Feature**: Updated example/README.md. diff --git a/packages/genui/pubspec.yaml b/packages/genui/pubspec.yaml index a4db1ea46..7ff74989c 100644 --- a/packages/genui/pubspec.yaml +++ b/packages/genui/pubspec.yaml @@ -4,14 +4,14 @@ name: genui description: Generates and displays generative user interfaces (GenUI) in Flutter using AI. -version: 0.9.0 +version: 0.9.1 homepage: https://github.com/flutter/genui/tree/main/packages/genui license: BSD-3-Clause issue_tracker: https://github.com/flutter/genui/issues environment: sdk: ">=3.10.0 <4.0.0" - flutter: ">=3.35.7 <4.0.0" + flutter: ">=3.35.7" resolution: workspace diff --git a/packages/json_schema_builder/.pubignore b/packages/json_schema_builder/.pubignore deleted file mode 100644 index 28b2338cf..000000000 --- a/packages/json_schema_builder/.pubignore +++ /dev/null @@ -1 +0,0 @@ -submodules/JSON-Schema-Test-Suite/** diff --git a/packages/json_schema_builder/pubspec.yaml b/packages/json_schema_builder/pubspec.yaml index a633b0320..0c359cbc4 100644 --- a/packages/json_schema_builder/pubspec.yaml +++ b/packages/json_schema_builder/pubspec.yaml @@ -4,7 +4,7 @@ name: json_schema_builder description: A full-featured package used to build and validate JSON schemas in Dart. -version: 0.1.3 +version: 0.1.4 homepage: https://github.com/flutter/genui/tree/main/packages/json_schema_builder license: BSD-3-Clause issue_tracker: https://github.com/flutter/genui/issues diff --git a/packages/json_schema_builder/test/test_suite_test.dart b/packages/json_schema_builder/test/test_suite_test.dart index e3668ec2f..a534737fa 100644 --- a/packages/json_schema_builder/test/test_suite_test.dart +++ b/packages/json_schema_builder/test/test_suite_test.dart @@ -12,9 +12,11 @@ import 'package:test/test.dart'; void main() { final testSuiteDir = Directory( - 'submodules/JSON-Schema-Test-Suite/tests/draft2020-12', + '../../submodules/JSON-Schema-Test-Suite/tests/draft2020-12', + ); + final remoteDir = Directory( + '../../submodules/JSON-Schema-Test-Suite/remotes', ); - final remoteDir = Directory('submodules/JSON-Schema-Test-Suite/remotes'); // Optional tests are not required to pass for full compliance. final optionalTestSuiteDir = Directory('${testSuiteDir.path}/optional'); diff --git a/pubspec.yaml b/pubspec.yaml index ea45f65a1..2607aad4d 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -24,7 +24,6 @@ workspace: - tool/e2e - tool/fix_copyright - - tool/release - tool/test_and_fix flutter: diff --git a/packages/json_schema_builder/submodules/JSON-Schema-Test-Suite b/submodules/JSON-Schema-Test-Suite similarity index 100% rename from packages/json_schema_builder/submodules/JSON-Schema-Test-Suite rename to submodules/JSON-Schema-Test-Suite diff --git a/packages/genui/submodules/a2ui b/submodules/a2ui similarity index 100% rename from packages/genui/submodules/a2ui rename to submodules/a2ui diff --git a/tool/fix_copyright/pubspec.yaml b/tool/fix_copyright/pubspec.yaml index 61c7a88be..551014096 100644 --- a/tool/fix_copyright/pubspec.yaml +++ b/tool/fix_copyright/pubspec.yaml @@ -4,6 +4,7 @@ name: fix_copyright description: A command line app to fix copyright headers. +publish_to: none version: 0.1.0 environment: diff --git a/tool/release/README.md b/tool/release/README.md deleted file mode 100644 index 67a54fc82..000000000 --- a/tool/release/README.md +++ /dev/null @@ -1,77 +0,0 @@ -# Monorepo Release Tool - -This Dart-based command-line tool automates the package publishing process for this monorepo using a safe, two-stage workflow. - -## Prerequisites - -#### Permissions to publish a package to pub.dev - -Make sure you have 'admin' permissions for the [labs.flutter.dev publisher](https://pub.dev/publishers/labs.flutter.dev), which you can verify on the [admin page](https://pub.dev/publishers/labs.flutter.dev/admin). - -If you do not have permissions, ask an existing admin from the linked page to add you. - -## How to release GenUI SDK - -The process is a two-stage publish workflow. It is split into two distinct commands, `bump` and `publish`, -to separate release preparation from the act of publishing. - -### 0. Update Dependencies - -Before running `bump`, make sure you are using the latest Flutter stable release, and update dependencies to the latest stable versions. This can be done by running: - -```bash -dart pub upgrade --major-versions -``` - -Also, use Antigravity or Gemini CLI to update `CHANGELOG.md` files. You can use a prompt like: - -```txt -Look at the git diffs since the tag and add any missing changelog entries for breaking and other changes to each of the packages which have CHANGELOG.md files. -``` - -Where `` is the tag of the previous release. For example, if the previous release was `genui-0.6.1`, then the command would be: - -```txt -Look at the git diffs since the genui-0.6.1 tag and add any missing changelog entries for breaking and other changes to each of the packages which have CHANGELOG.md files. -``` - -### 1. Prepare for Publish with `bump` - -First, run the `bump` command to prepare the repository for a new release. This will bump the version numbers, finalize the changelogs, and upgrade dependencies. After running this command, you should review the changes, make any necessary manual adjustments, and then commit the changes to your version control system. - -**Syntax:** - -```bash -dart run tool/release/bin/release.dart bump --level -``` - -**`` can be one of:** - -- `breaking`: Increments the major version for breaking changes. -- `major`: Increments the major version. -- `minor`: Increments the minor version for new features. -- `patch`: Increments the patch version for bug fixes. - -### 2. Publish and Prepare for Next Publish Cycle with `publish` - -After you have committed the changes from the `bump` command, you can publish the new version. The `publish` command will publish the packages, create git tags, and then prepare the repository for the next development cycle by adding a new `(in progress)` section to top of the CHANGELOG.md files. - -By default, `publish` runs in dry-run mode, which simulates the publish process without actually uploading packages. - -**Command:** - -```bash -dart run tool/release/bin/release.dart publish -``` - -#### Actual Publish - -To perform a real publish, use the `--force` flag. The tool will first perform a dry run. If successful, it will prompt for confirmation before proceeding. - -**Command:** - -```bash -dart run tool/release/bin/release.dart publish --force -``` - -After a successful publish, the tool will create local git tags for each published package and print the command needed to push them to the remote repository. You should then push the tags, and commit the new changes to the `CHANGELOG.md` files to start the next development cycle. diff --git a/tool/release/bin/release.dart b/tool/release/bin/release.dart deleted file mode 100644 index 7dd8e0a16..000000000 --- a/tool/release/bin/release.dart +++ /dev/null @@ -1,141 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io' as io; -import 'dart:io' show IOSink, Platform, exit; - -import 'package:args/args.dart'; -import 'package:file/file.dart'; -import 'package:file/local.dart'; -import 'package:process_runner/process_runner.dart'; -import 'package:release/release.dart'; -import 'package:release/src/exceptions.dart'; - -Future main(List arguments) async { - exit(await run(arguments)); -} - -Future run( - List arguments, { - IOSink? stdout, - IOSink? stderr, -}) async { - final IOSink actualStdout = stdout ?? io.stdout; - final IOSink actualStderr = stderr ?? io.stderr; - final parser = ArgParser() - ..addFlag( - 'help', - abbr: 'h', - negatable: false, - help: 'Print this usage information.', - ); - - final bumpParser = ArgParser() - ..addOption( - 'level', - abbr: 'l', - allowed: ['breaking', 'major', 'minor', 'patch'], - help: 'The level to bump the version by.', - mandatory: true, - ); - parser.addCommand('bump', bumpParser); - - final publishParser = ArgParser() - ..addFlag( - 'force', - abbr: 'f', - negatable: false, - help: 'Actually publish packages and create tags.', - ); - parser.addCommand('publish', publishParser); - parser.addCommand('help'); - - void printUsage({IOSink? sink}) { - final IOSink actualSink = sink ?? actualStdout; - actualSink.writeln( - 'Usage: dart run tool/release/bin/release.dart [options]', - ); - actualSink.writeln(parser.usage); - } - - final ArgResults argResults; - try { - argResults = parser.parse(arguments); - } on FormatException catch (e) { - actualStderr.writeln(e.message); - printUsage(sink: actualStderr); - return 1; - } - - if (argResults['help'] as bool) { - printUsage(); - return 0; - } - - if (argResults.command == null) { - printUsage(sink: actualStderr); - return 1; - } - - final fileSystem = const LocalFileSystem(); - final processRunner = ProcessRunner(); - - // Find the repo root, assuming the script is in /tool/release/bin - final File scriptFile = fileSystem.file(Platform.script.toFilePath()); - Directory repoDir = scriptFile.parent.parent.parent.parent; - - if (!repoDir.childFile('pubspec.yaml').existsSync()) { - // Fallback or check if we are in the wrong place? - // Try to find the root by looking up. - Directory current = scriptFile.parent; - while (current.path != current.parent.path) { - if (current.childFile('pubspec.yaml').existsSync() && - current.childDirectory('packages').existsSync()) { - repoDir = current; - break; - } - current = current.parent; - } - } - - final tool = ReleaseTool( - fileSystem: fileSystem, - processRunner: processRunner, - repoRoot: repoDir, - stdinReader: io.stdin.readLineSync, - ); - - final ArgResults command = argResults.command!; - try { - switch (command.name) { - case 'bump': - await tool.bump(command['level'] as String); - break; - case 'publish': - await tool.publish(force: command['force'] as bool); - break; - case 'help': - if (command.rest.isEmpty) { - printUsage(); - } else { - final String subcommand = command.rest.first; - final ArgParser? subParser = parser.commands[subcommand]; - if (subParser == null) { - actualStderr.writeln('Unknown command: $subcommand'); - printUsage(sink: actualStderr); - return 1; - } - actualStdout.writeln( - 'Usage: dart run tool/release/bin/release.dart $subcommand [options]', - ); - actualStdout.writeln(subParser.usage); - } - break; - } - } on ReleaseException catch (e) { - actualStderr.writeln(e); - return 1; - } - return 0; -} diff --git a/tool/release/lib/release.dart b/tool/release/lib/release.dart deleted file mode 100644 index f90ee806d..000000000 --- a/tool/release/lib/release.dart +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io'; - -import 'package:file/file.dart'; -import 'package:process_runner/process_runner.dart'; - -import 'src/bump.dart'; -import 'src/publish.dart'; -import 'src/utils.dart'; - -export 'src/bump.dart'; -export 'src/publish.dart'; - -class ReleaseTool { - final FileSystem fileSystem; - final ProcessRunner processRunner; - final Directory repoRoot; - - late final BumpCommand _bumpCommand; - late final PublishCommand _publishCommand; - - ReleaseTool({ - required this.fileSystem, - required this.processRunner, - required this.repoRoot, - required StdinReader stdinReader, - Printer? printer, - }) { - final Printer print = - printer ?? ((String message) => stdout.writeln(message)); - _bumpCommand = BumpCommand( - fileSystem: fileSystem, - processRunner: processRunner, - repoRoot: repoRoot, - printer: print, - ); - _publishCommand = PublishCommand( - fileSystem: fileSystem, - processRunner: processRunner, - repoRoot: repoRoot, - stdinReader: stdinReader, - printer: print, - ); - } - - Future bump(String bumpLevel) => _bumpCommand.run(bumpLevel); - - Future publish({required bool force}) => - _publishCommand.run(force: force); -} diff --git a/tool/release/lib/src/bump.dart b/tool/release/lib/src/bump.dart deleted file mode 100644 index 70f867713..000000000 --- a/tool/release/lib/src/bump.dart +++ /dev/null @@ -1,124 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:file/file.dart'; -import 'package:path/path.dart' as p; -import 'package:process_runner/process_runner.dart'; - -import 'exceptions.dart'; -import 'utils.dart'; - -class BumpCommand { - final FileSystem fileSystem; - final ProcessRunner processRunner; - final Directory repoRoot; - final Printer printer; - - BumpCommand({ - required this.fileSystem, - required this.processRunner, - required this.repoRoot, - required this.printer, - }); - - Future run(String bumpLevel) async { - final List packages = await findPackages(repoRoot, printer); - - for (final packageDir in packages) { - printer('Processing package: ${p.basename(packageDir.path)}'); - await _bumpVersion(packageDir, bumpLevel); - final String newVersion = await getPackageVersion(packageDir); - await _updateChangelog(packageDir, newVersion); - } - - printer('Upgrading dependencies in the monorepo...'); - await _upgradeDependencies(); - printer('Bump command finished.'); - } - - Future _bumpVersion(Directory packageDir, String level) async { - final ProcessRunnerResult result = await processRunner.runProcess( - ['dart', 'pub', 'bump', level], - workingDirectory: packageDir, - failOk: true, - ); - if (result.exitCode != 0) { - printer('Error bumping version in ${packageDir.path}: ${result.stderr}'); - throw ReleaseException( - 'Error bumping version in ${packageDir.path}: ${result.stderr}', - ); - } - printer('Bumped $level version in ${p.basename(packageDir.path)}'); - } - - Future _updateChangelog(Directory packageDir, String newVersion) async { - final String packageName = p.basename(packageDir.path); - final File changelogFile = fileSystem.file( - p.join(packageDir.path, 'CHANGELOG.md'), - ); - final title = '# `$packageName` Changelog\n'; - - if (!await changelogFile.exists()) { - printer( - 'Warning: CHANGELOG.md not found in ${packageDir.path}, ' - 'creating one.', - ); - await changelogFile.writeAsString('$title\n## $newVersion\n\n'); - return; - } - - String content = await changelogFile.readAsString(); - List lines = content.split('\n'); - - // Ensure the title is present and correct - if (lines.isEmpty || !lines[0].startsWith('# `$packageName` Changelog')) { - // Remove any existing incorrect title - if (lines.isNotEmpty && lines[0].startsWith('# ')) { - lines.removeAt(0); - // Remove potential blank lines after the old title - while (lines.isNotEmpty && lines[0].trim().isEmpty) { - lines.removeAt(0); - } - } - content = '$title\n${lines.join('\n')}'; - lines = content.split('\n'); - } - - // Find the top-most version entry and update it. - final versionHeader = '## $newVersion'; - var versionHeaderIndex = -1; - for (var i = 0; i < lines.length; i++) { - if (lines[i].startsWith('## ')) { - versionHeaderIndex = i; - break; - } - } - - if (versionHeaderIndex != -1) { - lines[versionHeaderIndex] = versionHeader; - } else { - // If no version entry exists, add one. - var insertIndex = 1; - while (insertIndex < lines.length && lines[insertIndex].trim().isEmpty) { - insertIndex++; - } - lines.insert(insertIndex, versionHeader); - lines.insert(insertIndex + 1, ''); // Blank line after new entry - } - - await changelogFile.writeAsString(lines.join('\n')); - printer('Updated CHANGELOG.md in ${packageDir.path}'); - } - - Future _upgradeDependencies() async { - final ProcessRunnerResult result = await processRunner.runProcess( - ['dart', 'pub', 'upgrade', '--major-versions'], - workingDirectory: fileSystem.directory(repoRoot), - failOk: true, - ); - if (result.exitCode != 0) { - printer('Error running pub upgrade: ${result.stderr}'); - } - } -} diff --git a/tool/release/lib/src/exceptions.dart b/tool/release/lib/src/exceptions.dart deleted file mode 100644 index 773426f6d..000000000 --- a/tool/release/lib/src/exceptions.dart +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -class ReleaseException implements Exception { - final String message; - - ReleaseException(this.message); - - @override - String toString() => message; -} diff --git a/tool/release/lib/src/publish.dart b/tool/release/lib/src/publish.dart deleted file mode 100644 index 5483566d4..000000000 --- a/tool/release/lib/src/publish.dart +++ /dev/null @@ -1,217 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:file/file.dart'; -import 'package:path/path.dart' as p; -import 'package:process_runner/process_runner.dart'; -import 'package:pub_semver/pub_semver.dart'; - -import 'exceptions.dart'; -import 'utils.dart'; - -typedef StdinReader = String? Function(); - -class PublishCommand { - final FileSystem fileSystem; - final ProcessRunner processRunner; - final Directory repoRoot; - final StdinReader stdinReader; - final Printer printer; - - PublishCommand({ - required this.fileSystem, - required this.processRunner, - required this.repoRoot, - required this.stdinReader, - required this.printer, - }); - - Future run({required bool force}) async { - final List packages = await findPackages(repoRoot, printer); - - if (!await _performDryRun(packages)) { - throw ReleaseException('Dry run failed.'); - } - - if (!force) { - printer('Dry run successful. The following tags would be created:'); - for (final packageDir in packages) { - final String packageName = p.basename(packageDir.path); - final String version = await getPackageVersion(packageDir); - printer(' $packageName-$version'); - } - printer('Run with --force to publish.'); - return; - } - - printer('\nProceed with publishing? (yes/No)'); - final String? confirmation = stdinReader()?.toLowerCase(); - if (confirmation != 'yes' && confirmation != 'y') { - printer('Publish aborted.'); - return; - } - - final Map versionsToPublish = await _getVersionsToPublish( - packages, - ); - - await _performPublish(packages); - await _createTags(versionsToPublish); - await _prepareNextCycle(packages); - } - - Future _performDryRun(List packages) async { - printer('--- Starting Dry Run ---'); - var dryRunFailed = false; - final accumulatedProblems = []; - for (final packageDir in packages) { - final String packageName = p.basename(packageDir.path); - printer('Dry running publish for $packageName...'); - final ProcessRunnerResult result = await processRunner.runProcess( - ['dart', 'pub', 'publish', '--dry-run'], - workingDirectory: packageDir, - failOk: true, - ); - printer(result.stdout); - if (result.exitCode != 0) { - // Check and see if the problem was actual errors or just warnings, etc. - // Warning output includes "Package has 2 warnings." - // Failed output includes: - // "your package is missing some requirements" - if (result.stderr.contains( - 'your package is missing some requirements', - )) { - accumulatedProblems.add('ERROR: Dry run failed for $packageName'); - dryRunFailed = true; - } else { - accumulatedProblems.add( - 'WARNING: Dry run has some warnings or hints for $packageName', - ); - } - printer(result.stderr); - } else { - accumulatedProblems.add('Dry run for $packageName successful.'); - } - } - printer('--- Dry Run Finished ---'); - printer(accumulatedProblems.join('\n')); - return !dryRunFailed; - } - - Future> _getVersionsToPublish( - List packages, - ) async { - final versionsToPublish = {}; - for (final packageDir in packages) { - final String packageName = p.basename(packageDir.path); - versionsToPublish[packageName] = await getPackageVersion(packageDir); - } - return versionsToPublish; - } - - Future _performPublish(List packages) async { - printer('--- Starting Actual Publish ---'); - for (final packageDir in packages) { - final String packageName = p.basename(packageDir.path); - printer('Publishing $packageName...'); - final ProcessRunnerResult result = await processRunner.runProcess( - ['dart', 'pub', 'publish', '--force'], - workingDirectory: packageDir, - failOk: true, - printOutput: true, - ); - if (result.exitCode != 0) { - throw ReleaseException('Failed to publish $packageName'); - } - printer('$packageName published successfully.'); - } - printer('--- Publish Finished ---'); - } - - Future _createTags(Map versionsToPublish) async { - printer('\n--- Creating Git Tags ---'); - for (final MapEntry entry in versionsToPublish.entries) { - final tagName = '${entry.key}-${entry.value}'; - printer('Creating tag: $tagName'); - final ProcessRunnerResult result = await processRunner.runProcess( - ['git', 'tag', tagName], - workingDirectory: repoRoot, - failOk: true, - ); - if (result.exitCode != 0) { - printer('ERROR: Failed to create tag $tagName'); - printer(result.stderr); - // Don't exit, just warn - } - } - printer('--- Tagging Finished ---'); - printer('\nTo push tags, run: "git push upstream --tags"'); - } - - Future _prepareNextCycle(List packages) async { - printer('\n--- Preparing for next development cycle ---'); - for (final packageDir in packages) { - final String newVersion = await getPackageVersion(packageDir); - var version = Version.parse(newVersion); - await _addNewChangelogSection( - packageDir, - version.nextPatch.canonicalizedVersion, - ); - } - printer('--- Next cycle preparation finished ---'); - } - - Future _addNewChangelogSection( - Directory packageDir, - String newVersion, - ) async { - final String packageName = p.basename(packageDir.path); - - final File changelogFile = fileSystem.file( - p.join(packageDir.path, 'CHANGELOG.md'), - ); - final title = '# `$packageName` Changelog\n'; - String content; - if (!await changelogFile.exists()) { - content = '$title\n## $newVersion (in progress)\n\n'; - await changelogFile.writeAsString(content); - printer('Created and updated CHANGELOG.md in ${packageDir.path}'); - return; - } - - content = await changelogFile.readAsString(); - if (content.contains('(in progress)')) { - printer( - 'CHANGELOG.md in ${packageDir.path} already has an ' - '"in progress" section. Skipping.', - ); - return; - } - List lines = content.split('\n'); - - // Ensure the title is present and correct - if (lines.isEmpty || !lines[0].startsWith('# `$packageName` Changelog')) { - if (lines.isNotEmpty && lines[0].startsWith('# ')) { - lines.removeAt(0); - while (lines.isNotEmpty && lines[0].trim().isEmpty) { - lines.removeAt(0); - } - } - lines.insert(0, title); - } - - // Insert the new entry after the title and any blank lines - var insertIndex = 1; - while (insertIndex < lines.length && lines[insertIndex].trim().isEmpty) { - insertIndex++; - } - - final newEntry = '## $newVersion (in progress)'; - lines.insert(insertIndex, ''); // Blank line before new entry - lines.insert(insertIndex, newEntry); - - await changelogFile.writeAsString(lines.join('\n')); - printer('Added new section to CHANGELOG.md in ${packageDir.path}'); - } -} diff --git a/tool/release/lib/src/utils.dart b/tool/release/lib/src/utils.dart deleted file mode 100644 index a89256786..000000000 --- a/tool/release/lib/src/utils.dart +++ /dev/null @@ -1,45 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'package:file/file.dart'; -import 'package:path/path.dart' as p; -import 'package:yaml/yaml.dart'; - -const excludedPackages = ['json_schema_builder', 'genai_primitives']; - -Future> findPackages( - Directory repoRoot, - Printer printer, -) async { - final Directory packagesDir = repoRoot.childDirectory('packages'); - if (!await packagesDir.exists()) { - printer('Error: packages directory not found at ${packagesDir.path}'); - return []; - } - - final packages = []; - await for (final FileSystemEntity entity in packagesDir.list()) { - if (entity is Directory) { - final String packageName = p.basename(entity.path); - if (excludedPackages.contains(packageName)) { - printer('Skipping excluded package: $packageName'); - continue; - } - final File pubspecFile = entity.childFile('pubspec.yaml'); - if (await pubspecFile.exists()) { - packages.add(entity); - } - } - } - return packages; -} - -Future getPackageVersion(Directory packageDir) async { - final File pubspecFile = packageDir.childFile('pubspec.yaml'); - final String content = await pubspecFile.readAsString(); - final yamlMap = loadYaml(content) as Map; - return yamlMap['version'] as String; -} - -typedef Printer = void Function(String message); diff --git a/tool/release/pubspec.yaml b/tool/release/pubspec.yaml deleted file mode 100644 index 6a1c7d34c..000000000 --- a/tool/release/pubspec.yaml +++ /dev/null @@ -1,23 +0,0 @@ -# Copyright 2025 The Flutter Authors. -# Use of this source code is governed by a BSD-style license that can be -# found in the LICENSE file. - -name: release -description: A tool for managing releases in the genui monorepo. -publish_to: none - -environment: - sdk: ">=3.10.0 <4.0.0" - -resolution: workspace - -dependencies: - args: ^2.7.0 - file: ^7.0.1 - path: ^1.9.1 - process_runner: ^4.2.4 - pub_semver: ^2.2.0 - yaml: ^3.1.3 - -dev_dependencies: - test: ^1.26.2 diff --git a/tool/release/test/bump_test.dart b/tool/release/test/bump_test.dart deleted file mode 100644 index 01a6ad32a..000000000 --- a/tool/release/test/bump_test.dart +++ /dev/null @@ -1,114 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io'; - -import 'package:file/memory.dart'; -import 'package:file/src/interface/directory.dart'; -import 'package:process_runner/process_runner.dart'; -import 'package:process_runner/test/fake_process_manager.dart'; -import 'package:release/release.dart'; -import 'package:test/test.dart'; - -void main() { - group('BumpCommand', () { - late MemoryFileSystem fileSystem; - late FakeProcessManager processManager; - late ReleaseTool releaseTool; - late Directory repoRoot; - late Directory packageADir; - - setUp(() { - fileSystem = MemoryFileSystem(); - repoRoot = fileSystem.systemTempDirectory.createTempSync('genui_repo'); - processManager = FakeProcessManager((input) {}); // Stdin callback - releaseTool = ReleaseTool( - fileSystem: fileSystem, - processRunner: ProcessRunner(processManager: processManager), - repoRoot: repoRoot, - stdinReader: () => null, // Not used in bump tests - printer: (_) {}, - ); - - final Directory packagesDir = repoRoot.childDirectory('packages'); - packagesDir.createSync(recursive: true); - - packageADir = packagesDir.childDirectory('package_a'); - packageADir.createSync(); - packageADir.childFile('pubspec.yaml').writeAsStringSync(''' -name: package_a -version: 1.0.0 -'''); - packageADir.childFile('CHANGELOG.md').writeAsStringSync(''' -## 1.0.0 - -- Initial release. -'''); - - final Directory excludedPackage = packagesDir.childDirectory( - 'json_schema_builder', - ); - excludedPackage.createSync(); - excludedPackage.childFile('pubspec.yaml').writeAsStringSync(''' -name: json_schema_builder -version: 0.1.0 -'''); - }); - - test('should bump patch version and update CHANGELOG', () async { - packageADir.childFile('CHANGELOG.md').writeAsStringSync(''' -# `package_a` Changelog - -## 1.0.1 (in progress) - -- Work in progress. - -## 1.0.0 - -- Initial release. -'''); - processManager.fakeResults = { - FakeInvocationRecord(const [ - 'dart', - 'pub', - 'bump', - 'patch', - ], workingDirectory: packageADir.path): [ - () { - packageADir.childFile('pubspec.yaml').writeAsStringSync(''' -name: package_a -version: 1.0.1 -'''); - return ProcessResult(0, 0, '', ''); - }(), - ], - FakeInvocationRecord(const [ - 'dart', - 'pub', - 'upgrade', - '--major-versions', - ], workingDirectory: repoRoot.path): [ - ProcessResult(0, 0, '', ''), - ], - }; - - await releaseTool.bump('patch'); - - final String pubspecContent = packageADir - .childFile('pubspec.yaml') - .readAsStringSync(); - expect(pubspecContent, contains('version: 1.0.1')); - - final String changelogContent = packageADir - .childFile('CHANGELOG.md') - .readAsStringSync(); - expect( - changelogContent, - startsWith( - '# `package_a` Changelog\n\n## 1.0.1\n\n- Work in progress.', - ), - ); - }); - }); -} diff --git a/tool/release/test/publish_test.dart b/tool/release/test/publish_test.dart deleted file mode 100644 index f34e2bad2..000000000 --- a/tool/release/test/publish_test.dart +++ /dev/null @@ -1,196 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// Use of this source code is governed by a BSD-style license that can be -// found in the LICENSE file. - -import 'dart:io'; - -import 'package:file/file.dart'; -import 'package:file/memory.dart'; -import 'package:process_runner/process_runner.dart'; -import 'package:process_runner/test/fake_process_manager.dart'; -import 'package:release/release.dart'; -import 'package:release/src/utils.dart'; -import 'package:test/test.dart'; - -void main() { - group('PublishCommand', () { - late MemoryFileSystem fileSystem; - late FakeProcessManager processManager; - late Directory repoRoot; - late Directory packageADir; - late List fakeStdinLines; - late int stdinReadIndex; - - String? fakeStdinReader() { - if (stdinReadIndex < fakeStdinLines.length) { - return fakeStdinLines[stdinReadIndex++]; - } - return null; - } - - ReleaseTool buildReleaseTool({Printer? printer}) { - return ReleaseTool( - fileSystem: fileSystem, - processRunner: ProcessRunner(processManager: processManager), - repoRoot: repoRoot, - stdinReader: fakeStdinReader, - printer: printer, - ); - } - - setUp(() { - fileSystem = MemoryFileSystem(); - repoRoot = fileSystem.systemTempDirectory.createTempSync('genui_repo'); - processManager = FakeProcessManager((input) {}); // Stdin callback - fakeStdinLines = []; - stdinReadIndex = 0; - - final Directory packagesDir = repoRoot.childDirectory('packages'); - packagesDir.createSync(recursive: true); - - packageADir = packagesDir.childDirectory('package_a'); - packageADir.createSync(); - packageADir.childFile('pubspec.yaml').writeAsStringSync(''' -name: package_a -version: 1.2.3 -'''); - - final Directory excludedPackage = packagesDir.childDirectory( - 'json_schema_builder', - ); - excludedPackage.createSync(); - excludedPackage.childFile('pubspec.yaml').writeAsStringSync(''' -name: json_schema_builder -version: 0.1.0 -'''); - }); - - test( - 'PublishCommand dry run should only call dry-run and print tags', - () async { - final printOutput = []; - final ReleaseTool releaseTool = buildReleaseTool( - printer: printOutput.add, - ); - - processManager.fakeResults = { - FakeInvocationRecord(const [ - 'dart', - 'pub', - 'publish', - '--dry-run', - ], workingDirectory: packageADir.path): [ - ProcessResult(0, 0, '', ''), - ], - }; - - await releaseTool.publish(force: false); - - expect(processManager.invocations.length, 1); - expect(processManager.invocations[0].invocation.skip(1), [ - 'pub', - 'publish', - '--dry-run', - ]); - expect(printOutput.join('\n'), contains('package_a-1.2.3')); - }, - ); - - test('PublishCommand publish --force with yes should publish, tag, and ' - 'update changelog', () async { - fakeStdinLines = ['yes']; - final ReleaseTool releaseTool = buildReleaseTool(printer: (_) {}); - packageADir.childFile('CHANGELOG.md').writeAsStringSync(''' -# `package_a` Changelog - -## 1.2.3 - -- Release version. -'''); - - processManager.fakeResults = { - FakeInvocationRecord(const [ - 'dart', - 'pub', - 'publish', - '--dry-run', - ], workingDirectory: packageADir.path): [ - ProcessResult(0, 0, '', ''), - ], - FakeInvocationRecord(const [ - 'dart', - 'pub', - 'publish', - '--force', - ], workingDirectory: packageADir.path): [ - ProcessResult(0, 0, '', ''), - ], - FakeInvocationRecord(const [ - 'git', - 'tag', - 'package_a-1.2.3', - ], workingDirectory: repoRoot.path): [ - ProcessResult(0, 0, '', ''), - ], - }; - - await releaseTool.publish(force: true); - - expect(processManager.invocations.length, 3); - expect(processManager.invocations[0].invocation.skip(1), [ - 'pub', - 'publish', - '--dry-run', - ]); - expect(processManager.invocations[1].invocation.skip(1), [ - 'pub', - 'publish', - '--force', - ]); - expect(processManager.invocations[2].invocation.skip(1), [ - 'tag', - 'package_a-1.2.3', - ]); - - final String pubspecContent = packageADir - .childFile('pubspec.yaml') - .readAsStringSync(); - expect(pubspecContent, contains('version: 1.2.3')); - - final String changelogContent = packageADir - .childFile('CHANGELOG.md') - .readAsStringSync(); - expect( - changelogContent, - startsWith( - '# `package_a` Changelog\n\n## 1.2.4 (in progress)\n\n## 1.2.3\n\n' - '- Release version.', - ), - ); - }); - - test('PublishCommand publish --force with no should abort', () async { - fakeStdinLines = ['no']; - final ReleaseTool releaseTool = buildReleaseTool(printer: (_) {}); - - processManager.fakeResults = { - FakeInvocationRecord(const [ - 'dart', - 'pub', - 'publish', - '--dry-run', - ], workingDirectory: packageADir.path): [ - ProcessResult(0, 0, '', ''), - ], - }; - - await releaseTool.publish(force: true); - expect(processManager.invocations.length, 1); - expect(processManager.invocations[0].invocation.skip(1), [ - 'pub', - 'publish', - '--dry-run', - ]); - }); - }); -} diff --git a/tool/release/test/release_test.dart b/tool/release/test/release_test.dart deleted file mode 100644 index 62d664c2e..000000000 --- a/tool/release/test/release_test.dart +++ /dev/null @@ -1,153 +0,0 @@ -// Copyright 2025 The Flutter Authors. -// 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:convert'; -import 'dart:io'; - -import 'package:test/test.dart'; - -import '../bin/release.dart' as app; - -void main() { - group('release.dart CLI', () { - late InMemoryIOSink stdout; - late InMemoryIOSink stderr; - - setUp(() { - stdout = InMemoryIOSink(); - stderr = InMemoryIOSink(); - }); - - test('--help prints usage to stdout', () async { - final int exitCode = await app.run( - ['--help'], - stdout: stdout, - stderr: stderr, - ); - expect(exitCode, 0, reason: 'Exit code should be 0'); - expect( - stdout.toString(), - contains('Usage: dart run tool/release/bin/release.dart'), - reason: 'Stdout should contain usage', - ); - expect( - stdout.toString(), - contains('Print this usage information.'), - reason: 'Stdout should contain help description', - ); - expect(stderr.toString(), isEmpty, reason: 'Stderr should be empty'); - }); - - test('help command prints usage to stdout', () async { - final int exitCode = await app.run( - ['help'], - stdout: stdout, - stderr: stderr, - ); - expect(exitCode, 0); - expect( - stdout.toString(), - contains('Usage: dart run tool/release/bin/release.dart'), - ); - expect(stderr.toString(), isEmpty); - }); - - test('no arguments prints usage to stderr and exits with 1', () async { - final int exitCode = await app.run([], stdout: stdout, stderr: stderr); - expect(exitCode, 1); - expect( - stderr.toString(), - contains('Usage: dart run tool/release/bin/release.dart'), - ); - expect(stdout.toString(), isEmpty); - }); - - test('unknown command prints usage to stderr and exits with 1', () async { - final int exitCode = await app.run( - ['unknown'], - stdout: stdout, - stderr: stderr, - ); - expect(exitCode, 1); - expect( - stderr.toString(), - contains('Usage: dart run tool/release/bin/release.dart'), - ); - }); - - test('help unknown_command prints error to stderr', () async { - final int exitCode = await app.run( - ['help', 'unknown'], - stdout: stdout, - stderr: stderr, - ); - expect(exitCode, 1); - expect(stderr.toString(), contains('Unknown command: unknown')); - expect( - stderr.toString(), - contains('Usage: dart run tool/release/bin/release.dart'), - ); - }); - }); -} - -class InMemoryIOSink implements IOSink { - final StringBuffer _buffer = StringBuffer(); - final Completer _doneCompleter = Completer(); - - @override - Encoding encoding = utf8; - - @override - void add(List data) { - _buffer.write(encoding.decode(data)); - } - - @override - void addError(Object error, [StackTrace? stackTrace]) { - _buffer.writeln('Error: $error'); - } - - @override - Future addStream(Stream> stream) async { - await for (final chunk in stream) { - add(chunk); - } - } - - @override - Future close() async { - _doneCompleter.complete(); - } - - @override - Future get done => _doneCompleter.future; - - @override - Future flush() async {} - - @override - void write(Object? object) { - _buffer.write(object); - } - - @override - void writeAll(Iterable objects, [String separator = '']) { - _buffer.writeAll(objects, separator); - } - - @override - void writeCharCode(int charCode) { - _buffer.writeCharCode(charCode); - } - - @override - void writeln([Object? object = '']) { - _buffer.writeln(object); - } - - @override - String toString() => _buffer.toString(); -}