diff --git a/.github/dependabot.yaml b/.github/dependabot.yaml index 80e427a7d..12aac2a33 100644 --- a/.github/dependabot.yaml +++ b/.github/dependabot.yaml @@ -12,7 +12,8 @@ updates: labels: - "autosubmit" - package-ecosystem: "pub" - directory: "/" + directories: + - "/**" schedule: interval: "daily" labels: diff --git a/.github/workflows/post_summaries.yaml b/.github/workflows/post_summaries.yaml new file mode 100644 index 000000000..7c6e8b1d6 --- /dev/null +++ b/.github/workflows/post_summaries.yaml @@ -0,0 +1,22 @@ +# 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_dispatch: # This means the job is manual-only. + # TODO(polina): re-enable the workflow_run trigger in a follow-up PRs. + # workflow_run: + # workflows: + # - Publish + # types: + # - completed + +jobs: + upload: + uses: dart-lang/ecosystem/.github/workflows/post_summaries.yaml@main + permissions: + pull-requests: write diff --git a/.github/workflows/publish.yaml b/.github/workflows/publish.yaml new file mode 100644 index 000000000..c59f1b615 --- /dev/null +++ b/.github/workflows/publish.yaml @@ -0,0 +1,31 @@ +# 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: + workflow_dispatch: # This means the job is manual-only. + # TODO(polina): re-enable the pull_request trigger in a follow-up PRs. + # 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/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/dev_tools/catalog_gallery/linux/flutter/generated_plugins.cmake b/dev_tools/catalog_gallery/linux/flutter/generated_plugins.cmake index a2eef970f..8e2a1900c 100644 --- a/dev_tools/catalog_gallery/linux/flutter/generated_plugins.cmake +++ b/dev_tools/catalog_gallery/linux/flutter/generated_plugins.cmake @@ -8,7 +8,6 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST - jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/dev_tools/catalog_gallery/macos/Flutter/GeneratedPluginRegistrant.swift b/dev_tools/catalog_gallery/macos/Flutter/GeneratedPluginRegistrant.swift index 7b265476e..ad1073ab5 100644 --- a/dev_tools/catalog_gallery/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/dev_tools/catalog_gallery/macos/Flutter/GeneratedPluginRegistrant.swift @@ -12,5 +12,5 @@ import video_player_avfoundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) - VideoPlayerPlugin.register(with: registry.registrar(forPlugin: "VideoPlayerPlugin")) + FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) } diff --git a/dev_tools/composer/linux/flutter/generated_plugins.cmake b/dev_tools/composer/linux/flutter/generated_plugins.cmake index a1bc1781f..c085ca836 100644 --- a/dev_tools/composer/linux/flutter/generated_plugins.cmake +++ b/dev_tools/composer/linux/flutter/generated_plugins.cmake @@ -10,7 +10,6 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST - jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/dev_tools/composer/macos/Flutter/GeneratedPluginRegistrant.swift b/dev_tools/composer/macos/Flutter/GeneratedPluginRegistrant.swift index 4825c9db2..802a33c8d 100644 --- a/dev_tools/composer/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/dev_tools/composer/macos/Flutter/GeneratedPluginRegistrant.swift @@ -15,6 +15,6 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) ScreenRetrieverMacosPlugin.register(with: registry.registrar(forPlugin: "ScreenRetrieverMacosPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) - VideoPlayerPlugin.register(with: registry.registrar(forPlugin: "VideoPlayerPlugin")) + FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) WindowManagerPlugin.register(with: registry.registrar(forPlugin: "WindowManagerPlugin")) } diff --git a/dev_tools/composer/pubspec.yaml b/dev_tools/composer/pubspec.yaml index 1eee937e8..f637117b3 100644 --- a/dev_tools/composer/pubspec.yaml +++ b/dev_tools/composer/pubspec.yaml @@ -4,7 +4,6 @@ name: composer publish_to: "none" -version: 0.1.0 environment: sdk: ">=3.10.0 <4.0.0" diff --git a/dev_tools/composer/windows/flutter/generated_plugins.cmake b/dev_tools/composer/windows/flutter/generated_plugins.cmake index 79fabcfb8..f74fccabd 100644 --- a/dev_tools/composer/windows/flutter/generated_plugins.cmake +++ b/dev_tools/composer/windows/flutter/generated_plugins.cmake @@ -11,7 +11,6 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST - jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/docs/contributing/README.md b/docs/contributing/README.md index 132385aff..71b74401a 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,22 @@ 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/`: -## Key commands +- If you are on mac and use VS Code, open the script and press `⇧⌘B` (see [.vscode/tasks.json](../../.vscode/tasks.json)). +- Otherwise, you can invoke the scripts on the command line from any directory. -- **Run all checks and tests:** +## pubspec.lock files - ```bash - ./tool/run_all_tests_and_fixes.sh - ``` +`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 using the latest version of the beta channel (run `flutter channel beta && flutter upgrade` to make sure you're on the right one). + + + +[Semver]: https://semver.org/ diff --git a/docs/contributing/publishing.md b/docs/contributing/publishing.md new file mode 100644 index 000000000..080bdd120 --- /dev/null +++ b/docs/contributing/publishing.md @@ -0,0 +1,85 @@ + +# 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 to reserve the name or to test the package; not ready for general use yet. +3. **Published**: any other package. Each has its own version cadence on pub.dev. + +## About `resolution: workspace` + +`resolution: workspace` in a `pubspec.yaml`: + +1. Tells Dart to share dependency resolution and a lockfile with the monorepo. + +2. Tells to use current repo as a source for the package, not pub.dev (for local runs). + +Note that a package can opt out (by omitting `resolution: workspace`) to have separate dependency resolution. + +## `-dev` vs non-`-dev` (production ready) versions + +The packages code should be always release ready. That means: + +1. Use `-dev` version (format `0.1.0-dev002`) 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 (where `` is a three-digit, zero padded integer like `-dev003`), 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. (There is no detailed guidance how to define this flag yet. It should be outlined when it is needed. Please create an issue if you need it soon.) + +## 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 + +## How upgrade of dependencies (for both siblings and third party) happens? + +### For local development runs + +For packages with `resolution: workspace` in their pubspec.yaml, pub resolves every sibling from its local source directory — not from pub.dev, as long as its `version:` satisfies the consumer's constraint. + +If a local bump escapes that constraint (e.g. `^0.9.0` → `0.10.0`), you must update the consumer's `pubspec.yaml` in the same PR. While `dart pub` natively silently falls back to the published version on pub.dev, **our `test_and_fix` CI suite contains a verification step that will explicitly throw an error** and fail your PR if internal workspace version constraints are not met. + +### For runs by external packages + +After a new version of a dependency (including sibling package in this repo) is published, this is how upgrade will happen: + +1. [Dependabot] detects the new version on pub.dev and opens a PR per dependency, bumping the constraint in each consuming `pubspec.yaml`. See [About Dependabot version updates] for details. +2. The PR runs `publish / validate` and the rest of CI. +3. A maintainer reviews and merges the PR. + +TODO: Consume solution for [dependabot issue][dependabot/dependabot-core#15057] when it is fixed. + +[Dependabot]: ../../.github/dependabot.yaml +[About Dependabot version updates]: https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/about-dependabot-version-updates +[dependabot/dependabot-core#15057]: https://github.com/dependabot/dependabot-core/issues/15057 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/examples/simple_chat/linux/flutter/generated_plugins.cmake b/examples/simple_chat/linux/flutter/generated_plugins.cmake index a2eef970f..8e2a1900c 100644 --- a/examples/simple_chat/linux/flutter/generated_plugins.cmake +++ b/examples/simple_chat/linux/flutter/generated_plugins.cmake @@ -8,7 +8,6 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST - jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/examples/simple_chat/macos/Flutter/GeneratedPluginRegistrant.swift b/examples/simple_chat/macos/Flutter/GeneratedPluginRegistrant.swift index 7b265476e..ad1073ab5 100644 --- a/examples/simple_chat/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/examples/simple_chat/macos/Flutter/GeneratedPluginRegistrant.swift @@ -12,5 +12,5 @@ import video_player_avfoundation func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { AudioplayersDarwinPlugin.register(with: registry.registrar(forPlugin: "AudioplayersDarwinPlugin")) UrlLauncherPlugin.register(with: registry.registrar(forPlugin: "UrlLauncherPlugin")) - VideoPlayerPlugin.register(with: registry.registrar(forPlugin: "VideoPlayerPlugin")) + FVPVideoPlayerPlugin.register(with: registry.registrar(forPlugin: "FVPVideoPlayerPlugin")) } diff --git a/examples/simple_chat/pubspec.yaml b/examples/simple_chat/pubspec.yaml index 7ece9fb7e..de0848b3e 100644 --- a/examples/simple_chat/pubspec.yaml +++ b/examples/simple_chat/pubspec.yaml @@ -4,7 +4,6 @@ name: simple_chat publish_to: "none" -version: 0.1.0 environment: sdk: ">=3.10.0 <4.0.0" diff --git a/examples/simple_chat/windows/flutter/generated_plugins.cmake b/examples/simple_chat/windows/flutter/generated_plugins.cmake index 158064786..97b61367a 100644 --- a/examples/simple_chat/windows/flutter/generated_plugins.cmake +++ b/examples/simple_chat/windows/flutter/generated_plugins.cmake @@ -9,7 +9,6 @@ list(APPEND FLUTTER_PLUGIN_LIST ) list(APPEND FLUTTER_FFI_PLUGIN_LIST - jni ) set(PLUGIN_BUNDLED_LIBRARIES) diff --git a/examples/verdure/client/pubspec.yaml b/examples/verdure/client/pubspec.yaml index bbed4f878..8b636dce4 100644 --- a/examples/verdure/client/pubspec.yaml +++ b/examples/verdure/client/pubspec.yaml @@ -7,7 +7,6 @@ description: >- A sample of a Flutter client interacting with a Python-based A2A (Agent-to-Agent) server for landscape design. publish_to: none -version: 0.1.0 environment: sdk: ">=3.10.0 <4.0.0" 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..204b2180f 100644 --- a/packages/genai_primitives/CHANGELOG.md +++ b/packages/genai_primitives/CHANGELOG.md @@ -1,8 +1,8 @@ # `genai_primitives` Changelog -## 0.2.4 (in progress) +## 0.2.4-dev001 -- **Refactor**: Update core framework to v0.9 (#546dab9be). +- **Feature**: Use `log` instead of `print` in example ([#546dab9be](https://github.com/flutter/genui/commit/546dab9be)). ## 0.2.3 diff --git a/packages/genai_primitives/pubspec.yaml b/packages/genai_primitives/pubspec.yaml index cfd0ee9c7..2e1add26f 100644 --- a/packages/genai_primitives/pubspec.yaml +++ b/packages/genai_primitives/pubspec.yaml @@ -4,7 +4,7 @@ name: genai_primitives description: A set of primitives for working with generative AI. -version: 0.2.3 +version: 0.2.4-dev001 homepage: https://github.com/flutter/genui/tree/main/packages/genai_primitives license: BSD-3-Clause issue_tracker: https://github.com/flutter/genui/issues 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/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/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/tool/fix_copyright/pubspec.yaml b/tool/fix_copyright/pubspec.yaml index 61c7a88be..236d9c0c8 100644 --- a/tool/fix_copyright/pubspec.yaml +++ b/tool/fix_copyright/pubspec.yaml @@ -4,7 +4,7 @@ name: fix_copyright description: A command line app to fix copyright headers. -version: 0.1.0 +publish_to: none environment: sdk: ">=3.10.0 <4.0.0" 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(); -} diff --git a/tool/test_and_fix/lib/src/verifiers/workspace_verifier.dart b/tool/test_and_fix/lib/src/verifiers/workspace_verifier.dart new file mode 100644 index 000000000..9780c4ec0 --- /dev/null +++ b/tool/test_and_fix/lib/src/verifiers/workspace_verifier.dart @@ -0,0 +1,148 @@ +// 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:logging/logging.dart'; +import 'package:pub_semver/pub_semver.dart'; +import 'package:yaml/yaml.dart'; + +/// Verifies that all internal dependencies within the workspace satisfy their +/// declared version constraints. +/// +/// In a standard Dart workspace, if a local package's version falls outside +/// the constraint specified by a sibling consumer, `dart pub` will silently +/// fall back to resolving that package from `pub.dev`. This class prevents +/// that error-prone behavior by explicitly failing the build when local +/// workspace constraints are not met. +class WorkspaceVerifier { + WorkspaceVerifier({required this.fs, Logger? logger}) + : _log = logger ?? Logger('WorkspaceVerifier'); + + final FileSystem fs; + final Logger _log; + + Future verify({ + required Directory repoRoot, + required List projects, + }) async { + _log.info('\n=== Workspace Version Constraints Verification ===\n'); + + final Map localPackages = {}; + final Map packagePubspecs = {}; + final Map packagePaths = {}; + + // 1. Gather all local workspace packages and their versions + for (final project in projects) { + final File pubspecFile = project.childFile('pubspec.yaml'); + if (!pubspecFile.existsSync()) continue; + + try { + final Object? yaml = loadYaml(pubspecFile.readAsStringSync()); + if (yaml is YamlMap) { + final name = yaml['name']?.toString(); + final versionStr = yaml['version']?.toString(); + + if (name != null && name.isNotEmpty && versionStr != null) { + try { + final version = Version.parse(versionStr); + localPackages[name] = version; + packagePubspecs[name] = yaml; + packagePaths[name] = fs.path.relative( + project.path, + from: repoRoot.path, + ); + } on FormatException { + _log.warning( + 'Warning: Could not parse version "$versionStr" ' + 'for package $name.', + ); + } + } + } + } catch (e) { + _log.warning('Failed to parse ${pubspecFile.path}: $e'); + } + } + + // 2. Verify all internal dependencies against the actual local versions + var allPassed = true; + + for (final MapEntry entry in packagePubspecs.entries) { + final String consumerName = entry.key; + final YamlMap consumerYaml = entry.value; + final String consumerPath = packagePaths[consumerName]!; + + var passedForPackage = true; + + void checkDependencies(String depsKey) { + final Object? deps = consumerYaml[depsKey]; + if (deps is YamlMap) { + for (final MapEntry depEntry in deps.entries) { + final depName = depEntry.key.toString(); + + // Only care about dependencies that exist in our workspace + if (localPackages.containsKey(depName)) { + final Object? constraintObj = depEntry.value; + String constraintStr; + + if (constraintObj is String) { + constraintStr = constraintObj; + } else if (constraintObj is YamlMap && + constraintObj.containsKey('version')) { + constraintStr = constraintObj['version'].toString(); + } else { + // Either a path dependency or unconstrained, skip + continue; + } + + try { + final constraint = VersionConstraint.parse(constraintStr); + final Version actualVersion = localPackages[depName]!; + + if (!constraint.allows(actualVersion)) { + _log.severe( + '❌ Error in $consumerPath: depends on $depName ' + '$constraintStr but local version is $actualVersion.', + ); + passedForPackage = false; + allPassed = false; + } + } on FormatException { + _log.warning( + 'Warning: Could not parse constraint "$constraintStr" ' + 'for dependency $depName in $consumerName.', + ); + } + } + } + } + } + + checkDependencies('dependencies'); + checkDependencies('dev_dependencies'); + + if (passedForPackage) { + _log.info( + '✅ $consumerPath: all internal workspace constraints satisfied.', + ); + } + } + + if (!allPassed) { + _log.severe('\n❌ Workspace version constraint verification failed.'); + _log.severe( + 'Dart workspace resolution will silently fall back to pub.dev ' + 'if local constraints are not met.', + ); + _log.severe( + 'Please update the failing constraints to allow the local ' + 'sibling version.', + ); + return false; + } + + _log.info('\n🎉 All workspace version constraints passed successfully!'); + return true; + } +} diff --git a/tool/test_and_fix/lib/test_and_fix.dart b/tool/test_and_fix/lib/test_and_fix.dart index 068dceec4..b305b7412 100644 --- a/tool/test_and_fix/lib/test_and_fix.dart +++ b/tool/test_and_fix/lib/test_and_fix.dart @@ -16,6 +16,7 @@ import 'package:process_runner/process_runner.dart'; import 'package:yaml/yaml.dart'; import 'src/verifiers/coverage_verifier.dart'; +import 'src/verifiers/workspace_verifier.dart'; class TestAndFix { TestAndFix({ @@ -40,6 +41,16 @@ class TestAndFix { }) async { root ??= fs.currentDirectory; final List projects = await findProjects(root, all: all); + + final workspaceVerifier = WorkspaceVerifier(fs: fs, logger: _log); + final bool workspaceValid = await workspaceVerifier.verify( + repoRoot: root, + projects: projects, + ); + if (!workspaceValid) { + return false; + } + final testedProjects = []; final jobs = []; final bool skipNonTestJobs = coverage || updateBaseline; diff --git a/tool/test_and_fix/pubspec.yaml b/tool/test_and_fix/pubspec.yaml index 9e2eadbaa..c39caa069 100644 --- a/tool/test_and_fix/pubspec.yaml +++ b/tool/test_and_fix/pubspec.yaml @@ -4,7 +4,6 @@ name: test_and_fix description: A command-line tool to run tests and apply fixes to the genui monorepo. -version: 0.1.0 publish_to: none environment: @@ -14,7 +13,9 @@ resolution: workspace # Add regular dependencies here. dependencies: - analyzer: ^9.0.0 + # analyzer 10.0.2+ requires meta ^1.18.0, but the Flutter SDK currently + # pins meta 1.17.0. Cap below 10.0.2 until Flutter ships a newer meta. + analyzer: ">=8.0.0 <10.0.2" args: ^2.7.0 coverage: ^1.15.0 file: ^7.0.1 @@ -24,6 +25,7 @@ dependencies: path: ^1.9.1 process: ^5.0.5 process_runner: ^4.2.4 + pub_semver: ^2.2.0 yaml: ^3.1.3 dev_dependencies: