From 792b91c4451caf2d0034fc35441afd1bb20ee90d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Eligio=20Mari=C3=B1o?= <22875166+gmeligio@users.noreply.github.com> Date: Sun, 10 May 2026 16:15:33 +0200 Subject: [PATCH] ci: test windows image (#339) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - **`windows.Dockerfile`** — fixes the `COPY` source path from `./test/Windows.Tests.ps1` to `./test/windows/Windows.Tests.ps1` (root cause of the 12-month "in_progress forever" state); adds `COPY ./config/version.json` to the `test` stage; replaces the commented `CMD` with a real `CMD` so `docker run`/`docker compose run` invokes Pester without arguments. - **`test/windows/Windows.Tests.ps1`** — fixes the `VC.CMake.Project` pattern typo (`,versiona*` → `,version=*`) and standardises all three VS-component patterns to `,version=*`; adds a `Flutter version` test that reads `config/version.json` and asserts `flutter --version` inside the container reports the same version; adds a `Flutter doctor` test with a per-line parser (skip disabled platforms, fail on any non-`[✓]` for Windows toolchain lines, fail on `[✗]` elsewhere). - **`script/RunPester.ps1`** — forces `[Console]::OutputEncoding = UTF8` so the `[✓]`/`[!]`/`[✗]` doctor glyphs survive the `windows-2025` runner's default OEM codepage. - **`test/windows/`** — deletes the dead `ory/dockertest` Go skeleton (`main.go`, `main_test.go`, `go.mod`, `go.sum`) that was never wired into CI and had its only meaningful assertion commented out. - **`.github/workflows/windows.yml`** — deletes three commented-out blocks (`Scan with Docker Scout`, `Push to Docker Hub`, `validate_version` job referencing the deleted `config/version.cue`); drops the now-unused elevated permissions (`packages: write`, `pull-requests: write`, `security-events: write`). --------- Co-authored-by: verified-commit[bot] <180343340+verified-commit[bot]@users.noreply.github.com> Co-authored-by: Claude Opus 4.7 (1M context) --- .github/gx.lock | 12 ++ .github/gx.toml | 3 + .github/workflows/release.yml | 2 + .github/workflows/windows.yml | 70 ++-------- docker-compose.yml | 8 ++ docs/src/package.json | 3 +- windows.md => docs/src/windows.mdx | 18 +-- docs/windows.md | 109 +++++++++++++++ .../p1-fix-windows-ci-tests/.openspec.yaml | 2 + .../changes/p1-fix-windows-ci-tests/design.md | 118 ++++++++++++++++ .../p1-fix-windows-ci-tests/proposal.md | 32 +++++ .../specs/windows-image-testing/spec.md | 129 ++++++++++++++++++ .../changes/p1-fix-windows-ci-tests/tasks.md | 50 +++++++ .../p2-release-windows-image/.openspec.yaml | 2 + .../p2-release-windows-image/design.md | 97 +++++++++++++ .../p2-release-windows-image/proposal.md | 30 ++++ .../specs/windows-image-release/spec.md | 64 +++++++++ .../changes/p2-release-windows-image/tasks.md | 49 +++++++ .../p3-windows-version-schema/.openspec.yaml | 2 + .../p3-windows-version-schema/design.md | 116 ++++++++++++++++ .../p3-windows-version-schema/proposal.md | 42 ++++++ .../specs/flutter-version-update/spec.md | 33 +++++ .../specs/windows-version-tracking/spec.md | 101 ++++++++++++++ .../p3-windows-version-schema/tasks.md | 63 +++++++++ script/InstallPester.ps1 | 4 + script/RunPester.ps1 | 9 ++ .../{test.sh => test_android_from_linux.sh} | 0 ...indows.sh => test_android_from_windows.sh} | 0 test/windows/Windows.Tests.ps1 | 83 +++++++++++ test/windows/main_test.go | 0 windows.Dockerfile | 36 +++++ 31 files changed, 1221 insertions(+), 66 deletions(-) rename windows.md => docs/src/windows.mdx (88%) create mode 100644 docs/windows.md create mode 100644 openspec/changes/p1-fix-windows-ci-tests/.openspec.yaml create mode 100644 openspec/changes/p1-fix-windows-ci-tests/design.md create mode 100644 openspec/changes/p1-fix-windows-ci-tests/proposal.md create mode 100644 openspec/changes/p1-fix-windows-ci-tests/specs/windows-image-testing/spec.md create mode 100644 openspec/changes/p1-fix-windows-ci-tests/tasks.md create mode 100644 openspec/changes/p2-release-windows-image/.openspec.yaml create mode 100644 openspec/changes/p2-release-windows-image/design.md create mode 100644 openspec/changes/p2-release-windows-image/proposal.md create mode 100644 openspec/changes/p2-release-windows-image/specs/windows-image-release/spec.md create mode 100644 openspec/changes/p2-release-windows-image/tasks.md create mode 100644 openspec/changes/p3-windows-version-schema/.openspec.yaml create mode 100644 openspec/changes/p3-windows-version-schema/design.md create mode 100644 openspec/changes/p3-windows-version-schema/proposal.md create mode 100644 openspec/changes/p3-windows-version-schema/specs/flutter-version-update/spec.md create mode 100644 openspec/changes/p3-windows-version-schema/specs/windows-version-tracking/spec.md create mode 100644 openspec/changes/p3-windows-version-schema/tasks.md create mode 100644 script/InstallPester.ps1 create mode 100644 script/RunPester.ps1 rename script/{test.sh => test_android_from_linux.sh} (100%) mode change 100755 => 100644 rename script/{test_windows.sh => test_android_from_windows.sh} (100%) mode change 100755 => 100644 create mode 100644 test/windows/Windows.Tests.ps1 delete mode 100644 test/windows/main_test.go diff --git a/.github/gx.lock b/.github/gx.lock index 7c1b384..1eea1ac 100644 --- a/.github/gx.lock +++ b/.github/gx.lock @@ -25,6 +25,12 @@ version = "v3.6.0" [resolutions."docker/metadata-action"."^5"] version = "v5.10.0" +[resolutions."docker/metadata-action"."~5.10.0"] +version = "v5.10.0" + +[resolutions."docker/metadata-action"."~5.7.0"] +version = "v5.7.0" + [resolutions."docker/scout-action"."^1"] version = "v1.18.2" @@ -106,6 +112,12 @@ repository = "docker/metadata-action" ref_type = "tag" date = "2025-11-27T12:36:24Z" +[actions."docker/metadata-action"."v5.7.0"] +sha = "902fa8ec7d6ecbf8d84d538b9b233a880e428804" +repository = "docker/metadata-action" +ref_type = "release" +date = "2025-02-26T15:31:35Z" + [actions."docker/scout-action"."v1.18.2"] sha = "f8c776824083494ab0d56b8105ba2ca85c86e4de" repository = "docker/scout-action" diff --git a/.github/gx.toml b/.github/gx.toml index ed59bdf..8b47d33 100644 --- a/.github/gx.toml +++ b/.github/gx.toml @@ -17,3 +17,6 @@ "peter-evans/create-pull-request" = "^7" "peter-evans/dockerhub-description" = "^5" "plexsystems/container-structure-test-action" = "~0.3.0" + +[actions.overrides] +"docker/metadata-action" = [{ workflow = ".github/workflows/build.yml", job = "test_image", step = 5, version = "~5.10.0" }, { workflow = ".github/workflows/ci.yml", job = "test_image", step = 5, version = "~5.10.0" }, { workflow = ".github/workflows/release.yml", job = "release_android", step = 2, version = "~5.10.0" }, { workflow = ".github/workflows/windows.yml", job = "test_windows", step = 3, version = "~5.7.0" }] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 813208c..f3c7fda 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -49,6 +49,8 @@ jobs: - name: Set up Docker Buildx uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1 + with: + buildkitd-flags: --debug - name: Login to Docker Hub uses: docker/login-action@5e57cd118135c172c3672efd75eb46360885c0ef # v3.6.0 diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 66392cf..58e3263 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -12,16 +12,9 @@ concurrency: jobs: test_windows: - permissions: - # Allow to write packages for the docker/scout-action to write a comment - packages: write - # Allow to write pull requests for the docker/scout-action to write a comment - pull-requests: write - # Allow to write security events for github/codeql-action/upload-sarif to upload SARIF results - security-events: write runs-on: windows-2025 env: - IMAGE_REPOSITORY_NAME: flutter-android + IMAGE_REPOSITORY_NAME: flutter-windows VERSION_MANIFEST: config/version.json steps: - name: Checkout repository @@ -44,56 +37,21 @@ jobs: const script = require('./script/setEnvironmentVariables.js') return await script({ core }) - # - name: Load image metadata - # uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 - # id: metadata - # with: - # images: | - # ${{ env.IMAGE_REPOSITORY_PATH }} - # tags: | - # type=raw,value=${{ env.FLUTTER_VERSION }} + - name: Load image metadata + uses: docker/metadata-action@902fa8ec7d6ecbf8d84d538b9b233a880e428804 # v5.7.0 + id: metadata + with: + images: | + ${{ env.IMAGE_REPOSITORY_PATH }} + tags: | + type=raw,value=${{ env.FLUTTER_VERSION }} - # - name: Set up Docker Buildx - # uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0 - - - name: Build image and push to local Docker daemon + # Docker Buildx is not supported for Windows containers + # so we'll use direct docker build commands + - name: Test image and push to local Docker daemon shell: powershell run: | - docker build . -f windows.Dockerfile --build-arg flutter_version=${{ env.FLUTTER_VERSION }} -t ${{ env.IMAGE_REPOSITORY_PATH }} + docker build . -f windows.Dockerfile --build-arg flutter_version=${{ env.FLUTTER_VERSION }} -t ${{ fromJson(steps.metadata.outputs.json).tags[0] }} --target test - # - name: Build image and push to local Docker daemon - # uses: docker/build-push-action@471d1dc4e07e5cdedd4c2171150001c434f0b7a4 # v6.15.0 - # with: - # file: windows.Dockerfile - # load: true - # cache-from: type=gha - # cache-to: type=gha,mode=max - # labels: ${{ steps.metadata.outputs.labels }} - # tags: ${{ steps.metadata.outputs.tags }} - # target: android - # build-args: | - # flutter_version=${{ env.FLUTTER_VERSION }} + docker run --rm ${{ fromJson(steps.metadata.outputs.json).tags[0] }} - # - name: Test image - # uses: plexsystems/container-structure-test-action@c0a028aa96e8e82ae35be556040340cbb3e280ca # v0.3.0 - # with: - # image: ${{ fromJSON(steps.metadata.outputs.json).tags[0] }} - # config: test/android.yml - - # # TODO: Parallelize testing and vulnerability scanning - # - name: Scan with Docker Scout - # id: docker-scout - # uses: docker/scout-action@0133ff88fe16d4a412dc4827a8fccbccb6b583e0 # v1.16.3 - # with: - # command: compare, recommendations - # # Use the Docker Hub image that is the first tag in the metadata - # image: local://${{ fromJson(steps.metadata.outputs.json).tags[0] }} - # # github-token is needed to be able to write the PR comment - # github-token: ${{ github.token }} - # only-fixed: true - # organization: ${{ secrets.DOCKER_HUB_USERNAME }} - # # sarif-file: output.sarif.json - # to-env: prod - # # Enable debug logging when needed - # # debug: true - # # verbose-debug: true diff --git a/docker-compose.yml b/docker-compose.yml index b8f4627..d35e46f 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -34,5 +34,13 @@ services: windows: build: dockerfile: ./windows.Dockerfile + target: flutter args: flutter_version: $FLUTTER_VERSION + + windows-test: + build: + dockerfile: ./windows.Dockerfile + target: test + args: + flutter_version: $FLUTTER_VERSION \ No newline at end of file diff --git a/docs/src/package.json b/docs/src/package.json index 7ccaa1f..6e2d8b0 100644 --- a/docs/src/package.json +++ b/docs/src/package.json @@ -5,8 +5,9 @@ "main": "index.js", "type": "module", "scripts": { - "build": "npm run readme && npm run contributing && npm run license", + "build": "npm run readme && npm run windows && npm run contributing && npm run license", "readme": "cross-env NODE_ENV=production node compile.js readme.mdx ../../readme.md", + "windows": "cross-env NODE_ENV=production node compile.js windows.mdx ../windows.md", "license": "cross-env NODE_ENV=production node compile.js license.mdx ../../LICENSE.md", "contributing": "cross-env NODE_ENV=production node compile.js contributing.mdx ../contributing.md", "test": "echo \"Error: no test specified\" && exit 1" diff --git a/windows.md b/docs/src/windows.mdx similarity index 88% rename from windows.md rename to docs/src/windows.mdx index 96f88ba..ebd18bc 100644 --- a/windows.md +++ b/docs/src/windows.mdx @@ -8,7 +8,7 @@ 1. Install tools -```powershell` +```powershell # # needed? No # --add Microsoft.Component.MSBuild' ` # # needed? No @@ -29,6 +29,7 @@ RUN Invoke-WebRequest -Uri https://aka.ms/vs/17/release/vs_buildtools.exe -OutFi Remove-Item vs_BuildTools.exe; ``` +1. Read dependencies from [flutter_tools](https://github.com/flutter/flutter/blob/master/packages/flutter_tools/lib/src/windows/visual_studio.dart). 1. Check how it can be run in Github actions. 1. Check how it can be run in Gitlab CI/CD. 1. Test where is installed. @@ -73,13 +74,12 @@ Debug with `curl -A github165 -v https://mcr.microsoft.com/v2/powershell/manifes 1. Enable Windows Developer Settings to solve error: - >Building with plugins requires symlink support. - > - >Please enable Developer Mode in your system settings. Run - > start ms-settings:developers - >to open settings. - ```powershell + # >Building with plugins requires symlink support. + # > + # >Please enable Developer Mode in your system settings. Run + # > start ms-settings:developers + # >to open settings. reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" /t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1" ``` @@ -87,7 +87,7 @@ Debug with `curl -A github165 -v https://mcr.microsoft.com/v2/powershell/manifes 1. Docker version must be pinned in Github workflow to avoid breaking changes: with escaping `\"` syntax inside RUN directive, etc. - 1. Packaging tool in Windows: . It uses the executables: + 1. Packaging tool in Windows: [msix](https://pub.dev/packages/msix) . It uses the executables: - [makeappx.exe](https://learn.microsoft.com/en-us/windows/win32/appxpkg/make-appx-package--makeappx-exe-) - [makepri.exe](https://learn.microsoft.com/en-us/windows/uwp/app-resources/makepri-exe-command-options) @@ -108,7 +108,7 @@ Debug with `curl -A github165 -v https://mcr.microsoft.com/v2/powershell/manifes - According to the [msstore guide](https://learn.microsoft.com/en-us/windows/apps/publish/msstore-dev-cli/commands?pivots=msstoredevcli-installer-linux#installation), It will be needed to install Microsoft.NetCore.Component.Runtime.8.0 with vs_BuildTools - 1. From => install This is currently the primary tool to publish to Microsoft Store + 1. From [github.com/tauu/flutter-windows-builder/Dockerfile](https://github.com/tauu/flutter-windows-builder/blob/main/Dockerfile) => install [github.com/microsoft/StoreBroker](https://github.com/microsoft/StoreBroker) This is currently the primary tool to publish to Microsoft Store - Not installed right now diff --git a/docs/windows.md b/docs/windows.md new file mode 100644 index 0000000..7351de7 --- /dev/null +++ b/docs/windows.md @@ -0,0 +1,109 @@ + + +# Windows + +## Swich between Linux and Windows containers + +& $Env:ProgramFiles\\Docker\\Docker\\DockerCli.exe -SwitchDaemon + +## TODO + +1. Install tools + +```powershell + # # needed? No +# --add Microsoft.Component.MSBuild' ` + # # needed? No + # --add Microsoft.VisualStudio.Component.TestTools.BuildTools ` + # # needed? No + # --add Microsoft.VisualStudio.Component.VC.ASAN ` + # # needed? no + # # --add Microsoft.VisualStudio.Component.VC.Tools.x86.x64 ` +RUN Invoke-WebRequest -Uri https://aka.ms/vs/17/release/vs_buildtools.exe -OutFile vs_BuildTools.exe; ` + Start-Process vs_BuildTools.exe -ArgumentList '--quiet --wait --norestart --nocache ` + # # needed? yes + # --add Microsoft.VisualStudio.Component.VC.CMake.Project ` + # # needed? Yes + # --add Microsoft.VisualStudio.Component.Windows11SDK.22621 ` + # # needed? + # --add Microsoft.VisualStudio.Workload.VCTools' ` + -Wait; ` + Remove-Item vs_BuildTools.exe; + +``` + +1. Read dependencies from [flutter\_tools](https://github.com/flutter/flutter/blob/master/packages/flutter%5Ftools/lib/src/windows/visual%5Fstudio.dart). +2. Check how it can be run in Github actions. +3. Check how it can be run in Gitlab CI/CD. +4. Test where is installed. +5. Test that path to powershell.exe exists. +6. Test with a snapshot of flutter config to determine if new feature flags should be enabled or disabled. +7. Test that Build Tools were installed in C:\\Program Files (x86)\\Microsoft Visual Studio\\2022\\BuildTools\\msbuild\\current\\bin +8. Check [Windows installation requirements for Flutter](https://docs.flutter.dev/get-started/install/windows/desktop) +9. Add docs explaining to use `$VerbosePreference = 'Continue';` in the SHELL to debug unexpected pwsh problems. + +## Open issue in windows Docker images repo + +1. Some images can be pulled while others give error: +```text +Error response from daemon: Get "https://mcr.microsoft.com/v2/": read tcp [2a0c:5a84:e100:e501::a97c]:58039->[2603:1061:f:101::10]:443: wsarecv: An existing connection was forcibly closed by the remote host. +``` + +Debug with `curl -A github165 -v https://mcr.microsoft.com/v2/powershell/manifests/lts-nanoserver-ltsc2022` + +## Contribute flutter upstream + +1. Remove `WHERE` in bin\\internal\\shared.bat and use instead: +```batch +pwsh.exe -Command "exit" >nul 2>&1 && ( + SET powershell_executable=pwsh.exe + ) || powershell.exe -Command "exit" >nul 2>&1 && ( + SET powershell_executable=PowerShell.exe + ) || ( + ECHO Error: PowerShell executable not found. 1>&2 + ECHO Either pwsh.exe or PowerShell.exe must be in your PATH. 1>&2 + EXIT 1 + ) +``` +2. Find if the executable should be pwsh or powershell and put it in a service to remove the hardcoded "powershell" in multiple places, like in: + * dev\\devicelab\\lib\\framework\\running\_processes.dart + * packages\\flutter\_tools\\lib\\src\\windows\\windows\_version\_validator.dart + +## Steps to reproduce in Docker + +1. Enable Windows Developer Settings to solve error: +```powershell +# >Building with plugins requires symlink support. +# > +# >Please enable Developer Mode in your system settings. Run +# > start ms-settings:developers +# >to open settings. +reg add "HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\AppModelUnlock" /t REG_DWORD /f /v "AllowDevelopmentWithoutDevLicense" /d "1" +``` +2. For CI/CD + 1. Docker version must be pinned in Github workflow to avoid breaking changes: with escaping `\"` syntax inside RUN directive, etc. + 2. Packaging tool in Windows: [msix](https://pub.dev/packages/msix) . It uses the executables: + * [makeappx.exe](https://learn.microsoft.com/en-us/windows/win32/appxpkg/make-appx-package--makeappx-exe-) + * [makepri.exe](https://learn.microsoft.com/en-us/windows/uwp/app-resources/makepri-exe-command-options) + * [signtool.exe](https://learn.microsoft.com/en-us/dotnet/framework/tools/signtool-exe) + * certificate + * Make a note that --install-certificate should be "false" or configured because the certificate can't be installed as ContainerUser. + ```powershell + # OK + Import-PfxCertificate -FilePath "C:\Users\ContainerUser\AppData\Local\Pub\Cache\hosted\pub.dev\msix-3.16.8\lib\assets\test_certificate.pfx" -Password (ConvertTo-SecureString -AsPlainText -Force "1234") -CertStoreLocation Cert:\LocalMachine\Root + # Doesn't work + Import-PfxCertificate -FilePath "C:\Users\ContainerUser\AppData\Local\Pub\Cache\hosted\pub.dev\msix-3.16.8\lib\assets\test_certificate.pfx" -Password (ConvertTo-SecureString -AsPlainText -Force "1234") + ``` + 3. Install msstore CLI It seems behind StoreBroker but it looks that it's going to be the primary and recommended way to publish to Microsoft Store + * According to the [msstore guide](https://learn.microsoft.com/en-us/windows/apps/publish/msstore-dev-cli/commands?pivots=msstoredevcli-installer-linux#installation), It will be needed to install Microsoft.NetCore.Component.Runtime.8.0 with vs\_BuildTools + 4. From [github.com/tauu/flutter-windows-builder/Dockerfile](https://github.com/tauu/flutter-windows-builder/blob/main/Dockerfile) \=> install [github.com/microsoft/StoreBroker](https://github.com/microsoft/StoreBroker) This is currently the primary tool to publish to Microsoft Store + * Not installed right now + 5. Install the [Windows App Certification Kit](https://learn.microsoft.com/en-us/windows/uwp/debug-test-perf/windows-app-certification-kit) or the [Windows SDK that already includes it](https://developer.microsoft.com/en-us/windows/downloads/windows-sdk/) + * Installed currently by one of the workloads in vs\_BuildTools + +## References + +* [How environment variables work on Windows containers?](https://blog.sixeyed.com/windows-weekly-dockerfile-14-environment-variables/) +* [Windows deployment in Flutter](https://docs.flutter.dev/deployment/windows) +* [vs\_BuildTools workloads](https://learn.microsoft.com/en-us/visualstudio/install/workload-component-id-vs-build-tools?view=vs-2022&preserve-view=true) +* Useful Dockerfile \ No newline at end of file diff --git a/openspec/changes/p1-fix-windows-ci-tests/.openspec.yaml b/openspec/changes/p1-fix-windows-ci-tests/.openspec.yaml new file mode 100644 index 0000000..0478d8f --- /dev/null +++ b/openspec/changes/p1-fix-windows-ci-tests/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-09 diff --git a/openspec/changes/p1-fix-windows-ci-tests/design.md b/openspec/changes/p1-fix-windows-ci-tests/design.md new file mode 100644 index 0000000..9cfca49 --- /dev/null +++ b/openspec/changes/p1-fix-windows-ci-tests/design.md @@ -0,0 +1,118 @@ +## Context + +PR #339 has 11 commits over ~12 months and is currently in a state where the Windows CI job either fails the `COPY` step or never produces meaningful signal. The accumulated changes overlap three concerns: (1) fixing the test pipeline, (2) adding a Go-based dockertest harness, (3) renaming `script/test.sh` files. This change keeps only (1). It treats Pester running *inside* the test-target container as the single verification mechanism for the Windows image, mirroring how the Android image uses `container-structure-test`. + +Constraints: + +- `windows-2025` is the only viable runner. There is no Windows-container support in `docker/build-push-action`, no Buildx cache, and the full image build (Flutter clone + VS BuildTools install) takes 30–60 minutes per run. +- The `gx`-managed action pinning regime (commit `846ffd6`, spec `actions-version-tracking`) requires every `uses:` to be SHA-pinned with a `# vX.Y.Z` comment. New actions added to `windows.yml` must go through `.github/gx.toml`. +- `config/version.json` is the single source of truth for `flutter.version` (spec `flutter-version-update`). The Pester suite must read it, not hardcode. + +## Goals / Non-Goals + +**Goals:** + +- The `test_windows` PR check goes from "in_progress forever / red on COPY" to "green on a healthy image, red on a regression." +- The Pester suite has at least one positive assertion on Flutter behavior (version, doctor) rather than only inspecting the on-disk VS package directories. +- `test/windows/` contains exactly one form of test (Pester). No dead skeletons. +- Everything that PR #339 added but did not finish is either finished or removed; nothing is left in a half-implemented state. + +**Non-Goals:** + +- Publishing the `flutter-windows` image on tag (covered by `p2-release-windows-image`). +- Tracking the VS BuildTools / Win11 SDK / CMake versions in `config/version.json` and through Renovate (covered by `p3-windows-version-schema`). +- Reducing the Windows CI run time. The job will remain slow; this change accepts that. +- Adding Docker Scout vulnerability scanning for the Windows image. The commented-out block is deleted; reintroducing it is left to a separate change if/when Scout becomes valuable for the Windows base image. +- Adding the `validate_version` job to `windows.yml`. The same CUE validation runs in `build.yml`'s `validate_version_files` job already; duplicating it on `windows-2025` adds runner cost with no new signal. + +## Decisions + +### Decision: Pester is the only verification harness; the Go/dockertest skeleton is deleted + +The Go module under `test/windows/` (commit `df7666e`) is removed in this change. Reasons: + +- It is not invoked by any CI workflow. +- Its only useful assertion (the Pester `Exec` block in `main_test.go:38-49`) is commented out. +- It runs the test image as `flutter-docker-image-windows-test:latest` without ever building it, so even uncommented it would fail. +- Pester running *inside* the container is the natural fit: the assertions are about the file system and toolchain *of the container*, which is awkward to express through `dockertest.Exec` from a Linux Go process. + +Alternatives considered: + +- **Wire the Go harness into CI.** Rejected: doubles the test infrastructure for no new signal, and the harness would still need a Windows host to run Windows containers — the same `windows-2025` runner constraint. +- **Keep the harness as a placeholder.** Rejected: dead code rots; unmaintained `go.mod` will collect `govulncheck` noise from Renovate. + +### Decision: The Flutter version assertion reads `config/version.json` at test time, not via a build arg + +The Pester test computes the expected version by parsing `config/version.json` (already `COPY`'d into the test stage as part of the `flutter` stage's checkout, or freshly copied in the `test` stage). Alternatives: + +- **Hardcode the version in the test.** Rejected: drifts on every Flutter upgrade; defeats the point of `flutter-version-update`. +- **Pass via `--build-arg expected_flutter_version` and bake into env.** Rejected: extra plumbing; the `flutter_version` build arg is already the source of truth fed to `git clone --branch`. Reading the manifest directly catches the case where someone passes a build arg that doesn't match the manifest. + +### Decision: `flutter doctor` failure mode + +`flutter doctor` produces lines like `[✓]`, `[!]`, `[✗]` (mapped from `ValidationType.success/partial/missing` in `packages/flutter_tools/lib/src/doctor_validator.dart`). The test applies a per-line rule based on the platform header: + +- **Disabled platforms** (`Android`, `iOS`, `macOS`, `Linux`, `Web`, `Chrome`): skipped entirely. These are explicitly turned off by `flutter config --no-enable-*` so any marker on them is irrelevant. +- **Owned-toolchain lines** (`Windows Version`, `Visual Studio - develop Windows apps`): fail unless the marker is `[✓]`. Both `[!]` and `[✗]` fail here. This is intentional: `WindowsVersionValidator` emits `[!]` when the Topaz OFD security module is detected (real build interference), and `VisualStudioValidator` emits `[!]` when VS is too old, needs reboot, has an incomplete install, is not launchable, is missing required components, or is missing the Windows 10 SDK — every one of which is a regression class this image must not ship. Sources: `packages/flutter_tools/lib/src/windows/{windows_version_validator,visual_studio_validator}.dart` in `flutter/flutter`. +- **Other lines** (`Flutter`, `Connected device`, `Network resources`, etc.): fail only on `[✗]`. `[!]` here is informational (e.g., no devices connected), expected in a CI container. + +The leaner "fail on `[✗]` only" rule was rejected: it would let a PR that drops `Microsoft.VisualStudio.Workload.VCTools` or `Windows11SDK.22621` from the Dockerfile pass with a `[!] Visual Studio` line, defeating the point of the smoke test. + +### Decision: VS component pattern fix uses `,version=*` + +The on-disk format for VS package directories is `,version=`. The current pattern `,versiona*` is a typo. The pattern `,version=*` is the minimum specific match that distinguishes a real install directory from any other coincident directory. Using `*` alone (no `,version=` anchor) would accept directories like `Microsoft.VisualStudio.Component.VC.CMake.Project_alt,…` which is too loose. + +### Decision: `ENTRYPOINT` and `CMD` in the test stage both target `RunPester.ps1` + +The `test` stage **resets** `ENTRYPOINT` to exec-form `["powershell", "-NoLogo", "-NoProfile", "-File"]` and sets `CMD` to `[".\\script\\RunPester.ps1"]`. This is required because the parent `flutter` stage uses a **shell-form** `ENTRYPOINT "C:\Users\ContainerUser\docker_entrypoint.ps1"` (the analytics-toggle script). Per Docker's documented `ENTRYPOINT`/`CMD` interaction, a shell-form `ENTRYPOINT` runs under PowerShell `-Command` and does **not** append `CMD` args — Docker emits the warning "Shell-form ENTRYPOINT and exec-form CMD may have unexpected results", and `docker run .\script\RunPester.ps1` fails with `hcs::System::CreateProcess … 0x2 file not found` because the workflow's argument is treated as a separate executable. + +With exec-form `ENTRYPOINT` in the test stage: + +- `docker run ` invokes `powershell -NoLogo -NoProfile -File .\script\RunPester.ps1` (uses `CMD`). +- `docker run .\test\OtherTest.ps1` swaps in a different script (overrides `CMD`). +- The CI workflow runs `docker run --rm ` with no explicit script argument; the `CMD` is the source of truth. + +The analytics-toggle entrypoint inherited from the `flutter` stage is intentionally not preserved here — the test image doesn't need runtime analytics control, and the inherited shell-form is the bug source. + +An earlier draft of this proposal kept the workflow's explicit `.\script\RunPester.ps1` arg "as redundant but harmless." That was wrong: it was the failure trigger when combined with the inherited shell-form `ENTRYPOINT`. The arg has been removed from the workflow. + +## Risks / Trade-offs + +- **[Risk] Build duration on `windows-2025` may exceed the GitHub Actions timeout for free-tier runners.** → Mitigation: this repo is not free-tier-constrained (see `release_android` already running multi-job pipelines). The `concurrency` block in `windows.yml` cancels stale runs so a force-push doesn't queue multiple builds. No further mitigation in this change; if duration becomes a blocker, layer caching in a follow-up. +- **[Risk] `flutter doctor` output format is not a stable contract; Flutter could change `[✗]` to a different marker.** → Mitigation: the doctor parser is small and lives in the Pester test, so a Flutter upgrade that breaks it produces a single, localized red test rather than silent passes. The `flutter-version-update` spec already requires a passing CI before merge, so any format break is caught at upgrade time, not in production. +- **[Trade-off] Removing the Go harness is a one-way door** for any future contributor who wants to add Linux-host-driven dockertest assertions. → Acceptable: such a future contributor can re-add the module deliberately, against a real requirement, instead of the current orphaned skeleton. +- **[Trade-off] The `validate_version` and `scout-action` blocks are deleted rather than left commented.** → Acceptable: commented code that references a deleted file (`config/version.cue`) is misleading. Deletion forces the next iteration to think through what they actually need rather than uncomment dead code. + +## Automated Test Strategy + +This change is itself a test infrastructure change. Verification of the change works on two levels: + +- **Self-test (the only level that matters for shipping)**: the `test_windows` job on PR #339 (or its replacement PR) goes green. That single check is the success criterion. It exercises every change in this proposal end-to-end: the `COPY` path is correct (build succeeds), the `versiona*` typo is fixed (VS-component test passes), the manifest-driven version test passes (Flutter version read from `config/version.json` matches what `flutter --version` reports), the doctor smoke test passes, the `CMD` is set (the workflow runs Pester and gets a non-zero exit on failure). +- **No new test infrastructure**: Pester is already installed via `script/InstallPester.ps1`; no new tooling is added. The change is a *reduction* in tooling (Go module removed). + +There is no unit-test layer below the Pester suite because the assertions are inherently integration-level — they require the real image to run. Local verification by contributors uses `docker compose run --rm windows-test` (which starts to work as part of this change). + +## Observability + +- **Failure surface**: every assertion is a Pester test. Pester emits per-test pass/fail with file:line in the workflow log. `Invoke-Pester -Configuration @{Output=@{Verbosity='Detailed'}}` (already configured in `script/RunPester.ps1`) shows the failing assertion's expected vs. actual. +- **No silent failures possible**: `RunPester.ps1` ends with `Exit $LASTEXITCODE`, so any Pester test failure propagates to a non-zero `docker run` exit, which fails the workflow step. The `set -e`-equivalent for PowerShell (`$ErrorActionPreference = 'Stop'`) is already configured in the test stage's `SHELL` directive. +- **Build-stage failures** (e.g., a future bad `COPY` path) surface as standard `docker build` errors with the failing instruction in the workflow log. There is no need for additional logging because the failing layer is named in the error. +- **No telemetry sent off-platform**: GitHub Actions logs are the entire observability surface. Maintainers monitor `gh run list --workflow=windows.yml --limit 5` (or the PR check UI). + +## Migration Plan + +1. Land this change on PR #339 (or replace #339 with a fresh PR built off the current `windows` branch). +2. Force-push the branch after fixing the `COPY` and pattern, and confirm `test_windows` goes from "in_progress forever" to a green check. +3. Delete the Go module files in the same commit as the Dockerfile fix; rerun the workflow to confirm no path now references `test/windows/main*.go`. +4. Squash-merge PR #339 with a non-empty body referencing this proposal. +5. No rollback is needed because every change is additive to the test surface or is a deletion of unused code; if the new Pester tests are wrong, they fail loudly and a follow-up fix applies — there is no production behavior to revert. + +## Resolved Questions + +- **Doctor `[!]` semantics on Windows-toolchain lines.** *Resolved 2026-05-10:* `[!]` on `Windows Version` and `Visual Studio - develop Windows apps` fails the test, same as `[✗]`. Captured in the "`flutter doctor` failure mode" decision above. Source: `WindowsVersionValidator` and `VisualStudioValidator` in `flutter/flutter`. +- **`dart-flutter-telemetry.config` path resolution under `ContainerUser`.** *Resolved 2026-05-10:* The existing assertion path `$env:APPDATA\.dart-tool\dart-flutter-telemetry.config` is correct. `package:unified_analytics` (used by both `flutter` and `dart` CLIs) reads `Platform.environment['AppData']` on Windows and joins `.dart-tool/dart-flutter-telemetry.config`. The Dockerfile runs the disable-analytics commands as `ContainerUser` and the test runs as `ContainerUser`, so `$env:APPDATA` resolves to the same path in both phases. Sources: `pkgs/unified_analytics/lib/src/{utils,initializer,constants}.dart` in `dart-lang/tools`. No change needed. + +## Open Questions + +- Should `windows.yml` upload Pester output as a workflow artifact (e.g., NUnit XML) for easier triage? *Tentatively no for this change* — the inline log is sufficient and adds no maintenance. Reopen if maintainers find themselves repeatedly digging through long Detailed-verbosity logs. +- Should there be a smoke test that runs `flutter create` + `flutter build windows` end-to-end in the test stage? *Out of scope here* — the build is already exercised in the `flutter` stage via `flutter create build_app; flutter build windows;` (Dockerfile lines 64, 81). Re-running it inside the `test` stage would multiply build time without new signal. Revisit if a regression slips past the existing build step. diff --git a/openspec/changes/p1-fix-windows-ci-tests/proposal.md b/openspec/changes/p1-fix-windows-ci-tests/proposal.md new file mode 100644 index 0000000..a3cb904 --- /dev/null +++ b/openspec/changes/p1-fix-windows-ci-tests/proposal.md @@ -0,0 +1,32 @@ +## Why + +PR #339 ("ci: test windows image") has been open for ~12 months and still cannot turn green: `windows.Dockerfile` copies `./test/Windows.Tests.ps1` from a path that no longer exists (the file was moved to `./test/windows/` when the dockertest skeleton was added), and the Pester pattern for the `VC.CMake.Project` package has a typo (`,versiona*`) that would never match a real Visual Studio package directory. As a result the `flutter-windows` image has *zero* automated verification on every PR, while the `flutter-android` image runs `container-structure-test` and Docker Scout. This change is what's needed to actually land PR #339 and start producing a meaningful CI signal for the Windows image. + +## What Changes + +- Fix `windows.Dockerfile` `COPY` to source `./test/windows/Windows.Tests.ps1` (the real path). +- Fix `test/windows/Windows.Tests.ps1` `BeLikeExactly` pattern: `,versiona*` → `,version=*` to match the pattern actually written by `vs_BuildTools.exe`. +- Add a Flutter version assertion that reads `config/version.json` and asserts `flutter --version` inside the container reports the same `flutter.version`. This converts the test job from "image builds" to "image is the version we shipped." +- Add a `flutter doctor` smoke assertion that fails the test when doctor reports any error (warnings on platform-specific tooling are tolerated). +- Set a default `CMD` in the `test` stage of `windows.Dockerfile` so `docker compose run windows-test` (and equivalent local invocations) actually runs Pester instead of exiting silently. The CI workflow continues to invoke `RunPester.ps1` explicitly. +- Remove the dead `test/windows/main.go`, `test/windows/main_test.go`, `test/windows/go.mod`, `test/windows/go.sum`. The `ory/dockertest` harness was scaffolded in commit `df7666e` but never wired into CI, never builds the image it tries to run, and has its only useful assertion (the Pester `Exec`) commented out. Pester running inside the container is the chosen verification mechanism. +- Either delete or wire up the two commented-out blocks in `.github/workflows/windows.yml`: the `docker/scout-action` step and the `validate_version` job (which still references the deleted `config/version.cue`). This change deletes them because Scout/version-validation parity is out of scope; the follow-up changes (`p2`, `p3`) reintroduce them deliberately. +- Set a non-empty body on PR #339 describing the test surface. + +## Capabilities + +### New Capabilities + +- `windows-image-testing`: defines what `.github/workflows/windows.yml` and `test/windows/Windows.Tests.ps1` are required to verify about the `flutter-windows` Docker image on every pull request — Flutter version match, doctor health, presence of pinned Visual Studio components, and analytics-disabled telemetry config. + +### Modified Capabilities + +_None._ The Windows image previously had no spec; the existing `flutter-version-update` and `actions-version-tracking` specs are not touched. + +## Impact + +- Affected files: `windows.Dockerfile`, `test/windows/Windows.Tests.ps1`, `.github/workflows/windows.yml`, `script/RunPester.ps1` (no change expected, but inputs change), `docker-compose.yml` (windows-test service still works). +- Removed files: `test/windows/main.go`, `test/windows/main_test.go`, `test/windows/go.mod`, `test/windows/go.sum`. +- No release/publish behavior changes — `release.yml` is untouched. Distribution of the Windows image is the explicit subject of `p2-release-windows-image`. +- No version manifest changes — `config/schema.cue` is untouched. Tracking VS BuildTools / Win11 SDK / CMake versions in `config/version.json` is the explicit subject of `p3-windows-version-schema`. +- Risk: the only CI signal here is a slow (`windows-2025`, multi-hour) Windows container build. This change does not address build duration; a green check is the success criterion, not a fast green check. diff --git a/openspec/changes/p1-fix-windows-ci-tests/specs/windows-image-testing/spec.md b/openspec/changes/p1-fix-windows-ci-tests/specs/windows-image-testing/spec.md new file mode 100644 index 0000000..4ff56a0 --- /dev/null +++ b/openspec/changes/p1-fix-windows-ci-tests/specs/windows-image-testing/spec.md @@ -0,0 +1,129 @@ +## ADDED Requirements + +### Requirement: Pull request CI verifies the Windows image on every PR + +The `.github/workflows/windows.yml` workflow SHALL run on every `pull_request` event, build `windows.Dockerfile` with `--target test`, and run the Pester suite at `test/windows/Windows.Tests.ps1` inside that image. The workflow SHALL fail the PR check if the image build fails, if any Pester test fails, or if Pester exits non-zero. + +The experience context is the maintainer reviewing a PR that touches `windows.Dockerfile`, `script/InstallPester.ps1`, `script/RunPester.ps1`, or `test/windows/**` — they get a single red/green check rather than having to build the multi-hour Windows image locally. + +#### Scenario: PR check is green when the image is healthy + +- **GIVEN** a PR whose `windows.Dockerfile` builds successfully on `windows-2025` +- **AND** every Pester test in `test/windows/Windows.Tests.ps1` passes inside the resulting `test`-target image +- **WHEN** the `test_windows` job runs +- **THEN** the job exits 0 +- **AND** the `test_windows` check on the PR is reported as success + +#### Scenario: PR check is red when a Pester test fails + +- **GIVEN** a PR whose `test`-target image builds successfully +- **AND** at least one Pester test fails (e.g., the Flutter version inside the image does not match `config/version.json`) +- **WHEN** `script/RunPester.ps1` runs +- **THEN** the script exits non-zero (it propagates `$LASTEXITCODE` from `Invoke-Pester`) +- **AND** the `test_windows` job is reported as failed on the PR + +#### Scenario: PR check is red when the Dockerfile cannot be built + +- **GIVEN** a PR that breaks `windows.Dockerfile` (for example, by referencing a `COPY` source path that does not exist) +- **WHEN** the `test_windows` job runs `docker build ... --target test` +- **THEN** the build exits non-zero +- **AND** the `test_windows` job is reported as failed on the PR + +### Requirement: Tests assert the Flutter version inside the image matches `config/version.json` + +The Pester suite SHALL include a test that runs `flutter --version` inside the running container and asserts the reported semver equals `flutter.version` from `config/version.json` at the commit being tested. The version SHALL be read from the manifest, not hardcoded in the test file. + +The experience context is the CI engineer pulling `flutter-windows:` and expecting the in-container Flutter to match the tag — a silent drift between manifest and image is the failure mode this requirement prevents. + +#### Scenario: Manifest and image agree + +- **GIVEN** `config/version.json` declares `flutter.version == "X.Y.Z"` +- **AND** the image was built with `--build-arg flutter_version=X.Y.Z` +- **WHEN** the Flutter version Pester test runs +- **THEN** `flutter --version` inside the container reports `Flutter X.Y.Z` +- **AND** the test passes + +#### Scenario: Manifest and image disagree + +- **GIVEN** `config/version.json` declares `flutter.version == "X.Y.Z"` +- **AND** the image was built with `--build-arg flutter_version=X.Y.W` (any other version) +- **WHEN** the Flutter version Pester test runs +- **THEN** the test fails with a message naming both versions + +### Requirement: Tests assert `flutter doctor` reports no errors + +The Pester suite SHALL include a test that runs `flutter doctor` inside the container and fails when doctor reports any line classified as an error. Warnings on platform-specific tooling that is intentionally not installed (e.g., Android, iOS, macOS, Linux desktop, Chrome) SHALL NOT fail the test. + +The experience context is the developer who runs `docker run flutter-windows flutter doctor` after pulling the image and expects a clean report for the Windows desktop toolchain — Pester catches regressions before the image is published. + +#### Scenario: Doctor reports a clean Windows toolchain + +- **GIVEN** the image was built successfully with VS BuildTools (CMake, Win11SDK, VCTools workload) installed +- **WHEN** the doctor Pester test runs +- **THEN** `flutter doctor` reports `[✓] Windows Version` and `[✓] Visual Studio - develop Windows apps` +- **AND** the test passes + +#### Scenario: Doctor reports a Windows-toolchain error + +- **GIVEN** a PR that removes the `Microsoft.VisualStudio.Workload.VCTools` line from `windows.Dockerfile` +- **AND** the image still builds (the workload removal does not break the build itself) +- **WHEN** the doctor Pester test runs +- **THEN** `flutter doctor` reports `[✗] Visual Studio` (or equivalent error marker) +- **AND** the test fails + +### Requirement: Tests assert presence of pinned Visual Studio components + +The Pester suite SHALL assert that the directories at `$env:ProgramData\Microsoft\VisualStudio\Packages\` contain entries matching the components installed by `windows.Dockerfile`: `Microsoft.VisualStudio.Component.VC.CMake.Project`, `Microsoft.VisualStudio.Component.Windows11SDK.22621`, and `Microsoft.VisualStudio.Workload.VCTools`. The match pattern SHALL accept any installed `version=...` suffix. + +The experience context is detecting silent removal or rename of a VS component in the Dockerfile — the package directory is the on-disk evidence that the component installed. + +#### Scenario: All three components match + +- **GIVEN** the image was built from the current `windows.Dockerfile` +- **WHEN** the VS-component Pester tests run +- **THEN** each of `VC.CMake.Project`, `Windows11SDK.22621`, and `Workload.VCTools` matches `*,version=*` +- **AND** all three tests pass + +#### Scenario: Pattern correctly accepts the on-disk format + +- **GIVEN** a real package directory `Microsoft.VisualStudio.Component.VC.CMake.Project,version=17.13.35919.96` +- **WHEN** the `BeLikeExactly` assertion runs against pattern `Microsoft.VisualStudio.Component.VC.CMake.Project,version=*` +- **THEN** the assertion passes + +### Requirement: Tests assert Flutter and Dart telemetry are disabled + +The Pester suite SHALL assert that the dart-flutter telemetry config at `$env:APPDATA\.dart-tool\dart-flutter-telemetry.config` exists and contains `reporting=0`. + +The experience context is the privacy-conscious user pulling the image and expecting analytics to be off by default — the test prevents a Dockerfile change from silently re-enabling telemetry. + +#### Scenario: Telemetry is disabled + +- **GIVEN** the image was built with `flutter config --no-analytics; dart --disable-analytics;` as currently in `windows.Dockerfile` +- **WHEN** the telemetry Pester test runs +- **THEN** `dart-flutter-telemetry.config` contains `reporting=0` +- **AND** the test passes + +### Requirement: The `test` Dockerfile stage is self-running by default + +The `test` stage of `windows.Dockerfile` SHALL declare a `CMD` (or equivalent) that invokes `script/RunPester.ps1`, so that `docker run ` (and `docker compose run windows-test`) executes the Pester suite without requiring the caller to pass a command. + +The experience context is the contributor who runs the test image locally — they should not need to know the exact PowerShell incantation to invoke Pester. + +#### Scenario: Local invocation runs the suite + +- **GIVEN** a test image built with `docker compose build windows-test` +- **WHEN** the contributor runs `docker compose run --rm windows-test` +- **THEN** Pester executes against `.\test` +- **AND** the container exits with the Pester exit code + +### Requirement: No dead Go/dockertest harness in `test/windows/` + +The repository SHALL NOT contain a Go module under `test/windows/` unless that module is invoked by at least one CI job. The `ory/dockertest` skeleton (`main.go`, `main_test.go`, `go.mod`, `go.sum`) introduced in commit `df7666e` SHALL be removed because Pester running inside the container is the chosen verification mechanism. + +The experience context is the contributor reading `test/windows/` and trying to determine which file is the source of truth — a dead harness alongside live Pester tests is a confusion hazard. + +#### Scenario: Repository contains no orphan Go test files for Windows + +- **WHEN** a contributor lists `test/windows/` +- **THEN** the listing contains `Windows.Tests.ps1` (and any newly added Pester files) +- **AND** the listing does not contain `main.go`, `main_test.go`, `go.mod`, or `go.sum` diff --git a/openspec/changes/p1-fix-windows-ci-tests/tasks.md b/openspec/changes/p1-fix-windows-ci-tests/tasks.md new file mode 100644 index 0000000..9c0de84 --- /dev/null +++ b/openspec/changes/p1-fix-windows-ci-tests/tasks.md @@ -0,0 +1,50 @@ +## 1. Fix the broken Dockerfile copy and Pester typo + +- [x] 1.1 In `windows.Dockerfile`, change the `COPY ./test/Windows.Tests.ps1` line to source `./test/windows/Windows.Tests.ps1`; keep the destination `.\test\Windows.Tests.ps1`. +- [x] 1.2 In `test/windows/Windows.Tests.ps1`, change the CMake assertion pattern from `,versiona*` to `,version=*`. Apply the same `,version=*` form to the Win11SDK and VCTools assertions for consistency. + +## 2. Make the test stage self-running + +- [x] 2.1 In `windows.Dockerfile`, replace the trailing `# CMD Invoke-Pester ...` comment in the `test` stage with `CMD ["powershell", "-NoLogo", "-NoProfile", "-File", ".\\script\\RunPester.ps1"]` (or equivalent that invokes `RunPester.ps1`). +- [x] 2.2 Verify locally that `docker compose run --rm windows-test` runs Pester and exits with the Pester exit code. (Skip if no Windows host available; rely on the CI run for confirmation.) + +## 3. Add the Flutter version Pester test + +- [x] 3.1 In `windows.Dockerfile`'s `test` stage, add `COPY ./config/version.json .\config\version.json` so the manifest is available at test time. +- [x] 3.2 In `test/windows/Windows.Tests.ps1`, add a new `Describe "Flutter version"` block with a test that: + - reads `config\version.json` via `Get-Content | ConvertFrom-Json`; + - extracts `flutter.version`; + - runs `flutter --version` and parses the first line into a semver string; + - asserts the parsed version equals the manifest version, with a failure message naming both values. + +## 4. Add the `flutter doctor` smoke test + +- [x] 4.1 In `test/windows/Windows.Tests.ps1`, add a `Describe "Flutter doctor"` block that runs `flutter doctor` and captures stdout. At the top of `script/RunPester.ps1` (or in a `BeforeAll` for this Describe), force `[Console]::OutputEncoding = [System.Text.Encoding]::UTF8` so the `[✓]`/`[!]`/`[✗]` glyphs survive PowerShell's default OEM encoding on `windows-2025`. +- [x] 4.2 Implement a parser that classifies each line by its platform header and applies the per-line rule from the `flutter doctor` failure-mode decision in `design.md`: + - **Skip** lines whose header is one of `Android`, `iOS`, `macOS`, `Linux`, `Web`, `Chrome` (intentionally disabled via `flutter config --no-enable-*`). + - **Fail unless `[✓]`** for headers that start with `Windows Version` or `Visual Studio` (any `[!]` or `[✗]` here is a real toolchain regression — see the Flutter validator sources cited in `design.md`). + - **Fail only on `[✗]`** for any other header (e.g., `Flutter`, `Connected device`, `Network resources`); `[!]` on these is informational in a CI container. + - To survive encoding edge cases, match markers by character class (e.g., `^\[(✓|✔|!|✗|✘|x|X)\]`) rather than literal codepoints, mapping `✓/✔` → pass, `!` → partial, `✗/✘/x/X` → fail. +- [x] 4.3 The test passes when at least the `Windows Version` and `Visual Studio - develop Windows apps` lines are tagged `[✓]` and no other non-skipped line is tagged `[✗]`. + +## 5. Delete the dead Go/dockertest harness + +- [x] 5.1 Delete `test/windows/main.go`, `test/windows/main_test.go`, `test/windows/go.mod`, `test/windows/go.sum`. +- [x] 5.2 Confirm that no workflow under `.github/workflows/` still references Go or `dockertest` (`grep -r "dockertest\|go test\|go mod" .github/workflows/`). + +## 6. Clean up commented-out workflow blocks + +- [x] 6.1 In `.github/workflows/windows.yml`, delete the commented-out `Scan with Docker Scout` step block. +- [x] 6.2 In `.github/workflows/windows.yml`, delete the commented-out `Push to Docker Hub` step block (release path is the subject of `p2-release-windows-image`). +- [x] 6.3 In `.github/workflows/windows.yml`, delete the commented-out `validate_version` job block (it references the deleted `config/version.cue`). + +## 7. Verify and ship + +- [x] 7.1 Push the branch; wait for the `test_windows` job in `.github/workflows/windows.yml` to complete on `windows-2025`. +- [ ] 7.2 Confirm the job exits 0 with all Pester tests reporting `Passed`. +- [x] 7.3 Update PR #339 (or open a replacement) with a non-empty body referencing this proposal: link to `openspec/changes/p1-fix-windows-ci-tests/proposal.md` and list the assertions now enforced. +- [ ] 7.4 Merge. + +## 8. Archive + +- [ ] 8.1 After merge, archive this change by running the `openspec-archive-change` flow so the `windows-image-testing` spec is promoted to `openspec/specs/windows-image-testing/spec.md`. diff --git a/openspec/changes/p2-release-windows-image/.openspec.yaml b/openspec/changes/p2-release-windows-image/.openspec.yaml new file mode 100644 index 0000000..0478d8f --- /dev/null +++ b/openspec/changes/p2-release-windows-image/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-09 diff --git a/openspec/changes/p2-release-windows-image/design.md b/openspec/changes/p2-release-windows-image/design.md new file mode 100644 index 0000000..585133f --- /dev/null +++ b/openspec/changes/p2-release-windows-image/design.md @@ -0,0 +1,97 @@ +## Context + +`release.yml` is the publishing workflow for tagged releases. It currently has five jobs, all Android-scoped: `release_android`, `update_description`, `record_image`, `set_bootstrap_image`, `create_github_release`. The Windows Dockerfile and the `windows.yml` PR-test workflow exist but are not connected to release. PR #339's `windows.yml` even has a commented-out `Push to Docker Hub` step (lines 84-88) hinting at this gap. + +Constraints: + +- Docker Buildx and `docker/build-push-action` cache features do not work for Windows containers (already noted in `windows.yml`'s comment "Docker Buildx is not supported for Windows containers"). The release job must use `docker build` + `docker push` directly, like `windows.yml` does. +- The `gx`-managed pinning regime (spec `actions-version-tracking`) requires every new `uses:` to be SHA-pinned via `.github/gx.toml`. This change reuses actions already pinned by `release_android` (`actions/checkout`, `docker/metadata-action`, `docker/login-action`, `actions/github-script`), so no `gx.toml` edit is needed. +- The three-registry fan-out (Docker Hub + GHCR + Quay) is not negotiable — it's the pre-existing distribution promise from `release_android`. +- `windows-2025` runner cost: each release run adds 30–60 minutes of `windows-2025` minutes. The repository already pays for this in the PR-test workflow per `p1`, so this is incremental cost on tag pushes only. + +## Goals / Non-Goals + +**Goals:** + +- A single tag push publishes both `flutter-android:X.Y.Z` and `flutter-windows:X.Y.Z` to Docker Hub, GHCR, and Quay. +- The Windows release path is *operationally identical* to the Android release path from a user's perspective: same registries, same tag scheme, same OCI labels. +- Failure isolation between architectures: Windows runner flake does not block Android publishing. +- `workflow_dispatch` continues to allow manual re-runs without re-tagging. + +**Non-Goals:** + +- Updating Docker Hub description for the Windows image (`peter-evans/dockerhub-description`). The current `update_description` job is Android-scoped against `readme.md`. Adding a Windows variant requires a separate readme and a separate Docker Hub repo description; out of scope. +- Recording Windows image vulnerabilities in Docker Scout (`record_image` job). Scout's Windows-base-image coverage is limited and the existing `windows.yml` already commented out the Scout block (line 65 onwards) for that reason. If/when Scout becomes useful for Windows, add it in a follow-up. +- Updating the `FLUTTER_VERSION` repo variable from a Windows job (`set_bootstrap_image`). That variable bootstraps `test_gradle` against the Android image; Windows has no analogous bootstrap need today. +- Including the Windows image in the GitHub Release notes generated by `create_github_release`. The release notes are tag-scoped, not image-scoped, so they continue to apply. +- Multi-arch manifests (`linux/amd64` + `windows/amd64` under one tag). Windows containers cannot share a manifest list with Linux containers in practice; users pull the right image for their host. Status quo. + +## Decisions + +### Decision: Add a single `release_windows` job, not a matrix on `release_android` + +A `strategy.matrix` over `[android, windows]` would conflict on `runs-on` (`ubuntu-24.04` vs `windows-2025`), `dockerfile` (`android.Dockerfile` vs `windows.Dockerfile`), `target` (`android` vs `flutter`), and the `clean-runner-disk` step (Linux-only). The result would be a matrix mostly composed of conditionals — less readable than two parallel jobs sharing the metadata pattern. Two-job design wins for clarity. + +Alternatives considered: + +- **Reusable workflow** with the registry login + metadata + push pattern factored out. Rejected: only two callers (Android and Windows), and the steps differ enough (Buildx vs. plain docker build) that the reusable surface would have ~5 inputs to model two callers. Not worth it. + +### Decision: No Buildx on the Windows job; use plain `docker build` + per-registry `docker push` + +`docker/build-push-action` does support Windows containers in some configurations, but the `cache-from: type=gha` / `cache-to: type=gha,mode=max` pattern used by `release_android` is Linux-only. Trying to use the action without GHA cache offers no benefit over plain `docker build`. The Windows job builds with `docker build` and pushes to each registry with three explicit `docker push` calls (or one `docker push --all-tags`). + +Tags are produced by `docker/metadata-action` exactly as in the Android job; the script then `docker tag`s the local build to each registry-prefixed name and pushes. + +### Decision: Three registry logins, all reusing existing secrets + +The job runs `docker/login-action` three times (Docker Hub, GHCR, Quay) using the same secrets as `release_android`: `DOCKER_HUB_USERNAME` / `DOCKER_HUB_TOKEN` for Docker Hub, `github.actor` / `github.token` for GHCR, `QUAY_USERNAME` / `QUAY_ROBOT_TOKEN` for Quay. No new secret rotation is needed. + +### Decision: No `needs:` dependency between `release_android` and `release_windows` + +Independent jobs run in parallel. If Windows build fails, Android still publishes. The workflow run as a whole reports failure, but the Android image is live. This matches how multi-arch open-source distributions usually behave: don't hold back one platform on another's flake. + +Alternative considered: + +- **`release_windows: needs: release_android`** so Android validates first. Rejected: if Android fails, the tag is half-cut anyway and Windows would only matter as a postmortem; in the common case (both pass) it just adds 30–60 minutes to the wall-clock of the Windows publish. + +### Decision: Image build target is `flutter`, not a new release-only target + +`windows.Dockerfile` already has the `flutter` stage as its production stage and `test` as the test stage. The release job builds `--target flutter`. `windows.yml` (PR test) builds `--target test`. This split is correct and matches `android.Dockerfile`. + +## Risks / Trade-offs + +- **[Risk] Windows runner is flaky and tags ship with no Windows image.** → Mitigation: `workflow_dispatch` re-run lets a maintainer recover without re-tagging. The release notes are tag-scoped (Android already publishes), so a delayed Windows publish is observable but not a failed release. +- **[Risk] Quay or GHCR push fails after Docker Hub push succeeds.** → Mitigation: `docker push` per-registry is idempotent; a re-run of the `release_windows` job pushes any missing tags. Document that the job is safe to re-run. +- **[Risk] `windows-2025` minutes cost grows by 30–60 min per tag.** → Acceptable: tag cadence is roughly monthly (`flutter-version-update` PRs land on stable bumps). Annualized cost is bounded. +- **[Trade-off] No Docker Hub description update for the Windows image.** → Acceptable: users discover the Windows variant via the GitHub README, which already documents both. A separate Docker Hub repo (`flutter-windows`) will appear bare for now. +- **[Trade-off] No Scout scan / SARIF upload for Windows.** → Acceptable: the Scout coverage gap on Windows base images is well-known. Code-scanning dashboard remains Android-only until Scout matures. + +## Automated Test Strategy + +- **Pre-merge verification (the only level that matters here):** the `release.yml` workflow itself does not run on `pull_request`; it runs on `push: tags: *` and `workflow_dispatch`. Therefore this change cannot be verified by a normal PR check. The verification path is: + 1. Land the PR with the new `release_windows` job. + 2. Use `workflow_dispatch` against an existing tag (e.g., the most recent stable Flutter tag) to trigger a one-shot run before the next stable bump. + 3. Confirm the three pushed images exist via `docker manifest inspect`. +- **Post-merge ongoing verification:** every monthly tag exercises the path. The `flutter-version-update` PR pipeline (spec: `flutter-version-update`) already gates that the Android image is healthy before tagging; once `p1-fix-windows-ci-tests` is in, the Windows image is also gated by its `test_windows` PR check on the upgrade PR. So the release-time signal is "PR was green and merged → tag was cut → release builds against a verified image." +- **No new test infrastructure**: the change is GitHub Actions YAML. The `metadata-action` and `login-action` are battle-tested in `release_android`. + +## Observability + +- **Failure surface**: the `release_windows` job appears as its own check on the workflow run. Failures show in the GitHub Actions UI exactly like `release_android` failures do today. +- **Per-registry push errors**: `docker push` errors are emitted to stdout by the Docker daemon and end up in the workflow log under the push step. No additional logging is needed. +- **Image digest visible after push**: each `docker push` prints the resulting digest. Copying that digest into the run summary is a nice-to-have but not required; `docker manifest inspect` from any host gives the same answer post-hoc. +- **No silent failures possible**: each `docker push` is its own step (or sub-command) and propagates its exit code. The job step fails on any non-zero exit. +- **Maintainer dashboard**: `gh run list --workflow=release.yml --limit 5` shows the most recent releases with per-job status; this is the existing observability surface and continues to work. + +## Migration Plan + +1. Land `p1-fix-windows-ci-tests` first so the Windows image is actually verified per-PR. +2. Open a PR adding the `release_windows` job to `release.yml`. The PR's `pull_request` checks do not exercise `release.yml` (it runs on `push: tags`); land it on the strength of YAML review + pinned-action diff. +3. After merge, manually run `release.yml` via `workflow_dispatch` against the most recent stable tag to validate the new job end-to-end *before* the next monthly upgrade PR cycle. If something is wrong, hotfix in a follow-up PR rather than waiting for a real release. +4. The next `flutter-version-update` PR that lands and is tagged exercises the path automatically. +5. Rollback strategy: if a regression appears (e.g., Windows registry credentials are wrong), revert the PR. Tags already pushed continue to publish only Android until the revert is itself reverted. + +## Open Questions + +- Should `release_windows` `needs: release_android` after all, to gate Windows publication on Android publication and reduce the chance of a half-released tag visible to users? *Tentative answer: no* (per the parallel-jobs decision above). Reopen if maintainers see frequent partial releases in practice. +- Should the `update_description` job on Docker Hub gain a parallel `update_description_windows` step pointing at a `readme-windows.md`? *Out of scope here* — split if/when there's a Windows-specific readme worth maintaining. diff --git a/openspec/changes/p2-release-windows-image/proposal.md b/openspec/changes/p2-release-windows-image/proposal.md new file mode 100644 index 0000000..7b4e96e --- /dev/null +++ b/openspec/changes/p2-release-windows-image/proposal.md @@ -0,0 +1,30 @@ +## Why + +`release.yml` only builds and publishes the `flutter-android` image on tag. The Windows Dockerfile, the `windows-2025` test workflow, and the `IMAGE_REPOSITORY_NAME: flutter-windows` env var in `windows.yml` all imply a `flutter-windows` image is shipped — but no release path actually pushes it. Once `p1-fix-windows-ci-tests` lands and CI verifies the image, users still cannot `docker pull /flutter-windows:`. This change adds a `release_windows` job to `release.yml` that mirrors `release_android` for the Windows artifact, so cutting a tag publishes both images. + +## What Changes + +- Add a new `release_windows` job to `.github/workflows/release.yml` that runs on `windows-2025`, builds `windows.Dockerfile` with `--target flutter`, and pushes the resulting image to Docker Hub, GitHub Container Registry, and Quay.io with the `` tag (matching the existing Android tagging convention). +- Reuse `script/setEnvironmentVariables.js` and `docker/metadata-action` exactly as `release_android` does, so the tag/label conventions stay identical across architectures. +- Login steps reuse the existing `DOCKER_HUB_*`, `QUAY_*`, and `GHCR` credentials. No new secrets are introduced. +- The new job runs in parallel with `release_android` (no `needs:` dependency between them) so a Windows build failure does not block Android publishing and vice versa. +- The downstream `update_description`, `record_image`, `set_bootstrap_image`, and `create_github_release` jobs that currently `needs: release_android` are NOT changed: they remain Android-scoped because the Docker Hub description, Scout environment, bootstrap-image variable, and changelog all currently reference Android only. Generalizing them is out of scope. +- The `test_windows` PR check from `p1` SHALL be a required check before tags can be cut, but the actual gating is repository-settings-only and not in this PR's diff. + +## Capabilities + +### New Capabilities + +- `windows-image-release`: defines what `release.yml` must do on a tag push so that a `flutter-windows:` image is published to the same set of registries as the Android image. + +### Modified Capabilities + +_None._ The existing `flutter-version-update` and `actions-version-tracking` specs are unaffected. `release_android` is unchanged. + +## Impact + +- Affected files: `.github/workflows/release.yml` (one new job added), `.github/gx.toml` and `.github/gx.lock` (new entries if any new actions are introduced — none expected; the new job uses actions already pinned via the Android job). +- Depends on: `p1-fix-windows-ci-tests` landed (so the image is verified before publishing), but does not depend on `p3-windows-version-schema` (versioning of the Windows artifact follows the existing `flutter.version` convention). +- Operational impact: every tag push triggers an additional `windows-2025` run, which currently takes 30–60 minutes. Tag-push to first-Windows-image-published wall-clock time grows by that amount. +- Cost: `windows-2025` runner minutes are billed; the budget impact should be reviewed before this lands. +- Risk: a flaky Windows build will block release tags. Mitigation is captured in the design (manual workflow_dispatch fallback already exists in `release.yml` and continues to work for Android). diff --git a/openspec/changes/p2-release-windows-image/specs/windows-image-release/spec.md b/openspec/changes/p2-release-windows-image/specs/windows-image-release/spec.md new file mode 100644 index 0000000..7ac748e --- /dev/null +++ b/openspec/changes/p2-release-windows-image/specs/windows-image-release/spec.md @@ -0,0 +1,64 @@ +## ADDED Requirements + +### Requirement: Tag push publishes a `flutter-windows` image to all release registries + +When a tag matching `*` is pushed to the repository, the `release_windows` job in `.github/workflows/release.yml` SHALL build `windows.Dockerfile` with `--target flutter` and `--build-arg flutter_version=`, and SHALL push the resulting image to Docker Hub, GitHub Container Registry, and Quay.io under the repository name `flutter-windows` with the tag equal to the Flutter version. + +The experience context is the CI engineer who, on the day a new Flutter stable lands, expects to run `docker pull docker.io//flutter-windows:` and find the image at the same tag they already use for `flutter-android`. + +#### Scenario: Tag push fans out to all three registries + +- **GIVEN** a tag `X.Y.Z` is pushed to the repository +- **WHEN** the `release_windows` job completes successfully +- **THEN** `docker.io//flutter-windows:X.Y.Z` exists +- **AND** `ghcr.io//flutter-windows:X.Y.Z` exists +- **AND** `quay.io//flutter-windows:X.Y.Z` exists + +#### Scenario: Tag-image consistency + +- **WHEN** any of the three published `flutter-windows:X.Y.Z` images is pulled and `flutter --version` is invoked inside it +- **THEN** the reported Flutter version is exactly `X.Y.Z` + +### Requirement: Windows release runs in parallel with Android release + +The `release_windows` job SHALL NOT declare a `needs:` dependency on `release_android`, and `release_android` SHALL NOT declare a `needs:` dependency on `release_windows`. A failure in one SHALL NOT cancel the other. + +The experience context is the maintainer cutting a release: they accept that one architecture may publish while the other fails, and prefer fixing the failed one in a follow-up tag rather than blocking both. + +#### Scenario: Android publishes when Windows build fails + +- **GIVEN** a tag is pushed +- **AND** the `release_windows` job fails (e.g., transient `windows-2025` runner issue) +- **AND** the `release_android` job succeeds +- **WHEN** the workflow run completes +- **THEN** Android images are published at all three registries +- **AND** the workflow run is reported as failed (because at least one job failed) +- **AND** the failure surface is the `release_windows` job specifically, not `release_android` + +### Requirement: Windows release uses the same metadata conventions as Android release + +The `release_windows` job SHALL use `docker/metadata-action` with the `images` input set to the same three registry namespaces and the `tags` input set to `type=raw,value=${{ env.FLUTTER_VERSION }}`, mirroring the Android job. The image labels (`org.opencontainers.image.*`) produced by `metadata-action` SHALL be applied to the built image via `docker/build-push-action`'s `labels` input. + +The experience context is the operator inspecting `docker inspect /flutter-windows:X.Y.Z` and `docker inspect /flutter-android:X.Y.Z` and finding the same set of OCI labels (description, source, revision, version) populated with the same values. + +#### Scenario: Labels match Android conventions + +- **GIVEN** a successful `release_windows` run for tag `X.Y.Z` +- **WHEN** an operator runs `docker inspect docker.io//flutter-windows:X.Y.Z` and inspects the `Labels` map +- **THEN** the keys `org.opencontainers.image.source`, `org.opencontainers.image.revision`, `org.opencontainers.image.version`, and `org.opencontainers.image.title` are all present +- **AND** `org.opencontainers.image.version` equals `X.Y.Z` +- **AND** `org.opencontainers.image.revision` equals the commit SHA of the tag + +### Requirement: Manual `workflow_dispatch` rebuild remains available for Windows + +The `release.yml` workflow SHALL continue to declare `workflow_dispatch:`, and the `release_windows` job SHALL be runnable via `workflow_dispatch` with the `FLUTTER_VERSION` env var set from `github.ref_name`, so that a maintainer can rebuild a single tag's Windows image without re-cutting the Git tag. + +The experience context is the maintainer recovering from a transient Windows runner failure: they re-run the workflow on the existing tag instead of force-pushing a new one. + +#### Scenario: Manual rebuild produces a fresh image + +- **GIVEN** a tag `X.Y.Z` exists in the repository +- **AND** the prior `release_windows` run for that tag failed +- **WHEN** a maintainer triggers `release.yml` via `workflow_dispatch` selecting ref `X.Y.Z` +- **THEN** `release_windows` builds and pushes `flutter-windows:X.Y.Z` to all three registries +- **AND** the existing image digests at those tags are overwritten by the new digests diff --git a/openspec/changes/p2-release-windows-image/tasks.md b/openspec/changes/p2-release-windows-image/tasks.md new file mode 100644 index 0000000..7cff6df --- /dev/null +++ b/openspec/changes/p2-release-windows-image/tasks.md @@ -0,0 +1,49 @@ +## 1. Add the `release_windows` job to `release.yml` + +- [ ] 1.1 Open `.github/workflows/release.yml` and add a new job `release_windows` after `release_android`. Set `runs-on: windows-2025`, `permissions.packages: write`, `env.IMAGE_REPOSITORY_NAME: flutter-windows`, `env.VERSION_MANIFEST: config/version.json`. +- [ ] 1.2 Add a `Checkout repository` step using the same SHA-pinned `actions/checkout` already in use elsewhere in the file. +- [ ] 1.3 Add a `Read environment variables from the version manifest` step using `actions/github-script` and `script/setEnvironmentVariables.js`, identical to `release_android`. +- [ ] 1.4 Add a `Load image metadata` step using `docker/metadata-action` with `images:` set to `${{ env.IMAGE_REPOSITORY_PATH }}`, `ghcr.io/${{ env.IMAGE_REPOSITORY_PATH }}`, `quay.io/${{ env.IMAGE_REPOSITORY_PATH }}` and `tags: type=raw,value=${{ env.FLUTTER_VERSION }}`. + +## 2. Wire registry logins + +- [ ] 2.1 Add `Login to Docker Hub` step using `docker/login-action` with `${{ secrets.DOCKER_HUB_USERNAME }}` / `${{ secrets.DOCKER_HUB_TOKEN }}`. +- [ ] 2.2 Add `Login to GitHub Container Registry` step with `registry: ghcr.io`, `${{ github.actor }}` / `${{ github.token }}`. +- [ ] 2.3 Add `Login to Quay.io` step with `registry: quay.io`, `${{ secrets.QUAY_USERNAME }}` / `${{ secrets.QUAY_ROBOT_TOKEN }}`. + +## 3. Build and push the Windows image + +- [ ] 3.1 Add a `Build image` step running `docker build . -f windows.Dockerfile --target flutter --build-arg flutter_version=${{ env.FLUTTER_VERSION }}` followed by `docker tag` calls that apply each metadata-action tag to the local image. +- [ ] 3.2 Add the OCI labels emitted by `metadata-action` to the build using `--label` arguments (or pipe the labels via a script step that iterates `${{ steps.metadata.outputs.labels }}`). +- [ ] 3.3 Add a `Push to registries` step that runs `docker push` for each tag in `${{ steps.metadata.outputs.tags }}` (one push per registry-prefixed tag). + +## 4. Confirm parallelism and isolation from `release_android` + +- [ ] 4.1 Verify the new job has no `needs:` line and no `if:` line keying on `release_android` outcome — it must run in parallel. +- [ ] 4.2 Verify the existing `update_description`, `record_image`, `set_bootstrap_image`, and `create_github_release` jobs still `needs: release_android` only, not `release_windows`. + +## 5. Confirm gx pinning compliance + +- [ ] 5.1 Confirm every `uses:` action in the new job is already entered in `.github/gx.toml` (it should be, since they all appear in `release_android`). +- [ ] 5.2 Run `gx tidy` locally; the diff should be empty. If it isn't, commit the gx-managed updates with the change. +- [ ] 5.3 Run `gx lint` locally to confirm SHA pinning is correct. + +## 6. Pre-merge dry run + +- [ ] 6.1 Push the branch and open a PR. The `pull_request` checks do not exercise `release.yml`, so the PR is evaluated on YAML review only. +- [ ] 6.2 After merge, use `workflow_dispatch` to trigger `release.yml` against the most recent stable Flutter tag. +- [ ] 6.3 Confirm `release_windows` exits 0 and the three published manifests exist: + - `docker manifest inspect docker.io//flutter-windows:` + - `docker manifest inspect ghcr.io//flutter-windows:` + - `docker manifest inspect quay.io//flutter-windows:` +- [ ] 6.4 Confirm `docker.io//flutter-android:` is unaffected by the workflow_dispatch run (its digest matches what was published at the original tag time). + +## 7. Confirm OCI labels and version match + +- [ ] 7.1 Run `docker pull docker.io//flutter-windows:` and `docker inspect` it. +- [ ] 7.2 Confirm `Labels["org.opencontainers.image.version"]` equals `` and `Labels["org.opencontainers.image.revision"]` equals the tag's commit SHA. +- [ ] 7.3 Run the image and confirm `flutter --version` reports ``. + +## 8. Archive + +- [ ] 8.1 After merge and successful first real (non-dispatch) release, archive this change so the `windows-image-release` spec is promoted to `openspec/specs/`. diff --git a/openspec/changes/p3-windows-version-schema/.openspec.yaml b/openspec/changes/p3-windows-version-schema/.openspec.yaml new file mode 100644 index 0000000..0478d8f --- /dev/null +++ b/openspec/changes/p3-windows-version-schema/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-05-09 diff --git a/openspec/changes/p3-windows-version-schema/design.md b/openspec/changes/p3-windows-version-schema/design.md new file mode 100644 index 0000000..06864cb --- /dev/null +++ b/openspec/changes/p3-windows-version-schema/design.md @@ -0,0 +1,116 @@ +## Context + +The repository already has a strong "manifest is source of truth" discipline for Linux/Android: `config/version.json` declares versions, `config/schema.cue` validates them, `setEnvironmentVariables.js` exports them, `update_version.yml` refreshes them, and `flutter-version-update` is the spec that ties it together. The Windows side has none of this — `windows.Dockerfile` carries hardcoded `git_version=2.46.0`, hardcoded `Windows11SDK.22621`, and no version metadata for the CMake or VCTools components beyond their *names*. After `p1` lands, the Pester suite asserts presence; this change extends to asserting *exact versions*, so the manifest becomes the single point of truth for "which Windows toolchain produced this image." + +Constraints: + +- Microsoft does not publish a clean, programmatic, monotonic API for VS BuildTools component versions. The closest source is the channel manifest at `https://aka.ms/vs/17/release/channel` (JSON), which is used by Microsoft's own VS installers but has nondeterministic ordering and changes structure occasionally. +- The Windows 11 SDK build number (`22621`) is essentially a Microsoft branding choice; it changes infrequently (Win11 22H2, 23H2 etc.) and is not strictly tied to VS BuildTools versions. +- Git for Windows publishes clean GitHub releases at `git-for-windows/git`, but the tag naming is `vM.m.p.windows.N` — the `.windows.N` suffix needs to be stripped before storing as a clean semver. +- `cue` already vendors well in this repo; adding `#SemverQuad` is a one-line addition. +- `setEnvironmentVariables.js` is the chokepoint — every workflow consumes it. Adding fields there ripples to every downstream job for free. + +## Goals / Non-Goals + +**Goals:** + +- `config/version.json` carries every version that the Windows image embeds. +- `windows.Dockerfile` has no version literals — every version is a build arg. +- The Pester suite asserts exact versions, not "any version is fine." +- The monthly upgrade PR carries Windows updates alongside Flutter and Android. +- A maintainer reading `git log -p config/version.json` sees Windows toolchain changes at the same fidelity as Android toolchain changes. + +**Non-Goals:** + +- Auto-detecting the latest VS BuildTools component versions from Microsoft. The channel manifest is too unstable to drive blind; this change reads it but pins the parsed output, gated behind a manual review step (see decisions). +- Tracking the Microsoft Windows Server Core base image (`mcr.microsoft.com/windows/servercore:ltsc2022@sha256:…`) under the manifest. That digest is already SHA-pinned in the Dockerfile FROM line; Renovate's Docker manager handles it. The `windows` block in `version.json` covers tools installed *on top of* the base, not the base itself. +- Running the Windows update job on `windows-2025`. The job that *fetches* the new versions is a Linux job (it just reads URLs and edits JSON). The image is rebuilt by `windows.yml` and `release.yml` on `windows-2025`. +- Renaming Android-side fields for consistency. `android.buildTools.version` and `windows.vsBuildTools.cmakeProject.version` look asymmetric but match each ecosystem's idioms. + +## Decisions + +### Decision: VS BuildTools component versions are pinned in `config/version.json`, refreshed manually from the channel manifest + +The `update_windows_version` job reads `https://aka.ms/vs/17/release/channel` and writes the resolved component versions into `config/version.json`. However, the channel manifest occasionally drops or renames components, so this is treated as a *suggestion* not a *truth*: the PR is opened with the new values, but the Pester suite then verifies that those values actually install. If they don't, the PR fails and a human pins by hand. + +Alternatives considered: + +- **Pin the four versions in the schema (in `schema.cue`) and require a human edit to bump.** Rejected: same review burden, but no automation. The channel-manifest read is cheap and 99% of the time correct. +- **Use Renovate's `vsBuildTools` datasource.** Rejected: no such datasource exists. Adding one is out of scope. + +### Decision: Schema additions are `#SemverPatch` (Git), `#SemverQuad` (CMake/VCTools), `int` (Win11SDK build) + +Git for Windows publishes three-part versions (e.g., `2.46.0`); `#SemverPatch` matches. + +VS components publish four-part versions (e.g., `17.13.35919.96`); a new `#SemverQuad: { version!: =~ "^\\d+\\.\\d+\\.\\d+\\.\\d+$" }` is added to `schema.cue`. + +Win11 SDK is identified by a build id, not a semver; modeled as a bare integer. This matches Microsoft's documentation conventions. + +### Decision: Build args have no defaults + +`windows.Dockerfile` ARG declarations remove all default values. This makes the build fail loudly if a workflow forgets to pass a build arg. The alternative (keep defaults as a fallback) was rejected because a default value is a second source of truth that drifts from `version.json`. + +### Decision: Pester reads `config/version.json` once at the top of `Describe`, not in every test + +A `BeforeAll` block parses the manifest into PowerShell variables. This keeps each test focused on the assertion, not the parsing. PowerShell's `ConvertFrom-Json` returns nested PSObjects; `$manifest.windows.git.version` is the access pattern. + +### Decision: `update_windows_version` runs in parallel with `update_android_version` + +Both `needs: update_flutter_version` and gate on `result == 'true'`. They emit separate artifacts that `update_docs_and_create_pr` merges. This mirrors the parallelism decision in `p2-release-windows-image`. + +Alternative considered: + +- **Sequence Windows after Android.** Rejected: no shared state. The Android job needs `flutter_version.json` (downloaded from the artifact), and the Windows job needs the same — the dependency is on the Flutter job, not on each other. + +### Decision: Stripping `.windows.N` suffix from Git for Windows tags + +Tag `v2.46.0.windows.1` → store `2.46.0`. The `.windows.1` revision number changes when Git for Windows publishes a fix without a Git upstream change; the underlying Git binary is identical from Flutter's perspective. Storing the upstream Git version aligns with what users see when they run `git --version` (which reports `git version 2.46.0.windows.1` — but the test parses `2.46.0` from the leading three parts). + +This means the test compares the leading three parts of `git --version` output to `windows.git.version`. The trailing `.windows.N` is informational, not asserted. + +## Risks / Trade-offs + +- **[Risk] The VS channel manifest changes structure and the update job starts producing nonsense versions.** → Mitigation: `cue vet` rejects values that don't match `#SemverQuad`. The PR fails its own validation. A human manually pins the right values until the channel-manifest reader is fixed. +- **[Risk] Microsoft yanks a VS component version (it has happened) and the image cannot be rebuilt for a tag that pinned the yanked version.** → Mitigation: the existing tag's image is already pushed and immutable. Future tags use new versions. Old-tag rebuilds (`workflow_dispatch` per `p2`) might fail until the manifest is updated; document this as a known limitation. +- **[Risk] Tightening Pester to exact versions makes the test brittle.** → Acceptable: that's the point. Drift detection is a feature. +- **[Trade-off] More fields in `version.json` mean more review surface for upgrade PRs.** → Acceptable: the alternative is hidden state in the Dockerfile, which has no review surface at all. +- **[Trade-off] `update_windows_version` adds a runtime dependency on `aka.ms/vs/17/release/channel` and `api.github.com/repos/git-for-windows/git`.** → Acceptable: the workflow already depends on `storage.googleapis.com/flutter_infra_release` and `raw.githubusercontent.com/flutter/flutter`. Two more upstreams is incremental. + +## Automated Test Strategy + +- **Schema validation (existing path):** `validate_version_files` job in `build.yml` and `validate_config_version` job in `update_version.yml` already run `cue vet`. Once the `windows` block is required by `#Version`, any `version.json` missing the block fails these jobs. No new test infrastructure. +- **Unit-level test for `setEnvironmentVariables.js`:** none added. The script is small and has no test today; this change preserves the status quo. A regression in env-var emission would surface as a build-arg-not-set failure in `windows.yml` (the build fails loudly because no defaults are kept). +- **Integration test (the load-bearing layer):** the Pester suite running on `windows-2025`. After this change, the suite reads `config/version.json` and asserts toolchain versions match. A CUE-valid manifest that doesn't match the actually-installed image fails the test. +- **End-to-end (monthly):** `update_version.yml` runs on schedule; the PR it opens carries the new `windows` block; `windows.yml` runs against that PR; the Pester suite gates the merge. If Microsoft changed something incompatibly, the PR fails CI and a maintainer intervenes. +- **No new test files** — the Pester suite is extended, not duplicated. The schema test surface is the existing `cue vet` step. + +## Observability + +- **CUE failures**: `cue vet` prints field-level errors with paths like `windows.vsBuildTools.cmakeProject.version: invalid value …`. The workflow log is the surface. +- **Pester version-mismatch failures**: the test failure message names both the manifest value and the in-image value (e.g., `Expected git --version to report 2.46.0; got 2.45.2`). This is how a maintainer diagnoses why a PR is red. +- **Update-job failures**: if `update_windows_version` cannot reach the VS channel manifest or the GitHub API, the step fails with a 4xx/5xx in the curl/jq pipeline. The job logs the URL it tried. +- **No silent partial updates**: `update_docs_and_create_pr` consumes the artifacts of every prior job. If `update_windows_version` did not upload an artifact, the PR-creation job fails on the missing artifact rather than opening a half-baked PR. +- **Dashboard surface**: existing — `gh run list --workflow=update_version.yml`. No new dashboards needed. + +## Migration Plan + +1. Land `p1-fix-windows-ci-tests` first (the Pester suite must exist to be tightened). +2. Land `p2-release-windows-image` ideally before this, so the Windows image is published; then this change extends what's published with version metadata. +3. Open a PR with: + - `config/schema.cue` adding `#SemverQuad` and the `#Version.windows` field. + - `config/version.json` backfilled with current values from `windows.Dockerfile`. + - `windows.Dockerfile` ARGs without defaults. + - `script/setEnvironmentVariables.js` exporting the new env vars. + - `windows.yml` (and `release.yml` if `p2` landed) passing the new env vars as `--build-arg`. + - `test/windows/Windows.Tests.ps1` reading the manifest and tightening assertions. + - `update_version.yml` adding the `update_windows_version` job and merging its artifact in `update_docs_and_create_pr`. +4. PR's own `windows.yml` run is the verification. Green = the manifest values match the image; red = somebody mistyped one of the four values during backfill. +5. Merge. +6. Wait for the next scheduled `update_version.yml` run; it should produce a PR that includes a `windows` block diff. Review and merge as usual. +7. Rollback strategy: if `update_windows_version` produces consistently-bad PRs, mark it `if: false` in a follow-up PR. The schema and Dockerfile changes do not need rolling back; they are stable. + +## Open Questions + +- Should `#SemverQuad` live alongside `#SemverPatch` in `schema.cue` or in a separate `windows.cue`? *Tentative: same file.* `schema.cue` is small and the cohesion is high. +- Should the `windows.Dockerfile`'s `vs_BuildTools.exe` URL be a build arg too? *Tentative: no.* The URL is `https://aka.ms/vs/17/release/vs_buildtools.exe` which Microsoft promises to keep stable per major VS version. If that promise breaks, this is a separate change. +- Should the channel manifest be cached in the repo (committed) so the update job is reproducible offline? *Tentative: no.* That would mean tracking churn that doesn't affect us. The runtime dependency is acceptable. diff --git a/openspec/changes/p3-windows-version-schema/proposal.md b/openspec/changes/p3-windows-version-schema/proposal.md new file mode 100644 index 0000000..21fc918 --- /dev/null +++ b/openspec/changes/p3-windows-version-schema/proposal.md @@ -0,0 +1,42 @@ +## Why + +`config/version.json` is the single source of truth for Android tooling versions (`buildTools`, `cmake`, `ndk`, `gradle`, `cmdlineTools`, `platforms`) and Flutter itself, and `config/schema.cue` validates it. The Windows build, by contrast, has hardcoded version-bearing strings scattered across `windows.Dockerfile` (Git for Windows `2.46.0`, `Microsoft.VisualStudio.Component.VC.CMake.Project`, `Microsoft.VisualStudio.Component.Windows11SDK.22621`, `Microsoft.VisualStudio.Workload.VCTools`) with no manifest entry, no CUE constraint, and no Renovate-driven update path. After `p1-fix-windows-ci-tests` lands, the Pester suite asserts these components are *present*, but cannot assert they are at any particular version because there is no source of truth to compare against. This change extends `config/schema.cue` and `config/version.json` with a `windows` block, plumbs the values into `windows.Dockerfile` build args, and adds a corresponding `update_windows_version` job to `update_version.yml` so monthly upgrade PRs cover the Windows toolchain alongside Flutter and Android. + +## What Changes + +- Add a `#WindowsToolchain` definition to `config/schema.cue` and constrain `#Version` to include a top-level `windows: #WindowsToolchain` field. Initial fields: + - `git: #SemverPatch` (Git for Windows version, currently hardcoded as `2.46.0`), + - `vsBuildTools.cmakeProject: #SemverPatch` (the four-part version VS reports as `version=A.B.C.D`; we model it as a `#SemverQuad` introduced alongside), + - `vsBuildTools.windows11Sdk: { build!: int }` (the `22621` from `Windows11SDK.22621` — already a numeric build id), + - `vsBuildTools.vcTools: #SemverQuad` (workload version reported by VS). +- Add `#SemverQuad: { version!: =~ "^\\d+\\.\\d+\\.\\d+\\.\\d+$" }` to `schema.cue` (VS components publish four-part versions; the existing `#SemverPatch` rejects them). +- Populate the new `windows` block in `config/version.json` with the values currently hardcoded in `windows.Dockerfile`. This is a one-time backfill; the values do not change. +- In `windows.Dockerfile`, replace the hardcoded `git_version=2.46.0` ARG default with no default (force the build arg), and add `vs_cmake_version`, `vs_win11sdk_build`, `vs_vctools_version` build args. The `--add Microsoft.VisualStudio.Component.Windows11SDK.` argument is composed from the build arg. +- In `script/setEnvironmentVariables.js`, surface the new fields as env vars (`GIT_VERSION`, `VS_CMAKE_VERSION`, `VS_WIN11SDK_BUILD`, `VS_VCTOOLS_VERSION`) so `windows.yml` and `release.yml` (post-`p2`) can pass them to the build. +- In `windows.yml` and (after `p2`) `release.yml`'s `release_windows`, pass these env vars as `--build-arg` values to `docker build`. +- In the Pester suite (added by `p1`), tighten the VS component assertions from `*,version=*` to `*,version=*`, and assert Git's reported `git --version` matches `windows.git.version`. +- Add a new `update_windows_version` job to `update_version.yml`, parallel to `update_android_version` (both gated by `update_flutter_version`'s `result == 'true'` output). The job: + - reads upstream Git for Windows latest release from `https://api.github.com/repos/git-for-windows/git/releases/latest`, + - reads VS BuildTools component versions from the channel manifest (or pins them; see design), + - writes the new fields into `config/version.json` and uploads the artifact for the `validate_config_version` and `update_docs_and_create_pr` jobs to consume. +- The Windows-relevant fields fall under the existing `flutter-version-update` PR cadence — same upgrade PR carries both Android and Windows updates. + +## Capabilities + +### New Capabilities + +- `windows-version-tracking`: defines what `config/version.json`, `config/schema.cue`, and `update_version.yml` must guarantee about Windows toolchain versions — that they live in the manifest, are CUE-validated, are passed into the Dockerfile build, are asserted by the Pester suite at the manifest's pinned values, and are refreshed monthly by the upgrade PR. + +### Modified Capabilities + +- `flutter-version-update`: the requirement "Upgrade PR contains a coherent, validated `version.json`" is extended so the PR's `version.json` also passes `cue vet` against the new `windows` block. The existing scenario that asserts Android `buildTools.version` matches Flutter's `packages.txt` is unchanged; a new scenario asserts Git for Windows tracks the latest GitHub-released Git for Windows tag. +- `windows-image-testing` (introduced by `p1`): the VS component requirements are tightened from `*,version=*` to exact-version match against `config/version.json`'s `windows` block; a new requirement asserts Git's reported version matches the manifest. + +## Impact + +- Affected files: `config/schema.cue`, `config/version.json`, `config/flutter_version.json` (unchanged — this is `#Version` not `#FlutterVersion`), `windows.Dockerfile`, `script/setEnvironmentVariables.js`, `.github/workflows/windows.yml`, `.github/workflows/update_version.yml`, `.github/workflows/release.yml` (after `p2` lands), `test/windows/Windows.Tests.ps1`. +- Cross-cutting: this is the largest of the three changes. Touches schema, manifest, dockerfile, two workflows, the version-update node script, and the test suite. +- Depends on: `p1-fix-windows-ci-tests` landed (Pester tests exist to tighten); `p2-release-windows-image` ideally landed (so the build args also flow through release). +- Does not depend on the Renovate-via-`gx` integration (`actions-version-tracking`) since Windows toolchain versions are tracked in `config/version.json` like Android, not in `gx.toml`. Renovate is unaffected. +- Risk: VS BuildTools component versioning is provided by Microsoft via the channel manifest; the source of truth and the update API have less stability than Flutter's `releases_linux.json`. Mitigation in design. +- Risk: tightening Pester assertions to exact versions means a Microsoft-side patch bump to a VS component will fail CI even though the image still works. Mitigation: track at the build-id level for Win11SDK (already coarse), at the publisher's `version=` for the others, accept that an upgrade PR is required when Microsoft ships a patch. diff --git a/openspec/changes/p3-windows-version-schema/specs/flutter-version-update/spec.md b/openspec/changes/p3-windows-version-schema/specs/flutter-version-update/spec.md new file mode 100644 index 0000000..151306a --- /dev/null +++ b/openspec/changes/p3-windows-version-schema/specs/flutter-version-update/spec.md @@ -0,0 +1,33 @@ +## MODIFIED Requirements + +### Requirement: Upgrade PR contains a coherent, validated `version.json` + +When the workflow opens an upgrade PR, the included `config/version.json` SHALL satisfy `cue vet config/schema.cue -d '#Version'` and SHALL contain the Android `buildTools.version` listed for that exact Flutter tag in `engine/src/flutter/tools/android_sdk/packages.txt` upstream. The same `version.json` SHALL also contain a `windows.git.version` equal to the latest non-prerelease tag at `https://api.github.com/repos/git-for-windows/git/releases/latest` (with any `.windows.N` suffix stripped) and the VS BuildTools component versions sourced from the deterministic source documented in `p3-windows-version-schema`'s design. + +The experience context is the CI engineer reviewing or merging the upgrade PR — they observe that downstream image builds will not silently regress on Android tooling *or* on Windows tooling. + +#### Scenario: Build-tools version tracks the new Flutter tag + +- **GIVEN** the workflow is opening an upgrade PR for Flutter `X.Y.Z` +- **AND** Flutter's `engine/src/flutter/tools/android_sdk/packages.txt` at tag `X.Y.Z` lists `build-tools;A.B.C` +- **WHEN** the PR is created +- **THEN** `config/version.json` in the PR contains `android.buildTools.version == "A.B.C"` + +#### Scenario: Generated config is schema-valid + +- **GIVEN** the workflow has produced a candidate `config/version.json` +- **WHEN** the `validate_config_version` job runs +- **THEN** `cue vet config/schema.cue -d '#Version' config/version.json` exits 0 +- **AND** the workflow only proceeds to open the PR if validation passes + +#### Scenario: Git for Windows tracks the latest published tag + +- **GIVEN** `https://api.github.com/repos/git-for-windows/git/releases/latest` returns an asset whose underlying Git semver is `M.m.p` +- **WHEN** the upgrade PR is created +- **THEN** `config/version.json` in the PR contains `windows.git.version == "M.m.p"` + +#### Scenario: Windows toolchain block is schema-valid + +- **GIVEN** the workflow has produced a candidate `config/version.json` containing the new `windows` block +- **WHEN** the `validate_config_version` job runs +- **THEN** `cue vet` passes against the `windows` block as well as the existing `flutter` and `android` blocks diff --git a/openspec/changes/p3-windows-version-schema/specs/windows-version-tracking/spec.md b/openspec/changes/p3-windows-version-schema/specs/windows-version-tracking/spec.md new file mode 100644 index 0000000..ff5e968 --- /dev/null +++ b/openspec/changes/p3-windows-version-schema/specs/windows-version-tracking/spec.md @@ -0,0 +1,101 @@ +## ADDED Requirements + +### Requirement: `config/version.json` declares the Windows toolchain versions + +`config/version.json` SHALL contain a top-level `windows` object with the following fields, each validated by `config/schema.cue` `#Version`: + +- `windows.git.version` — Git for Windows release version, three-part semver (e.g., `2.46.0`). +- `windows.vsBuildTools.cmakeProject.version` — Visual Studio CMake component version, four-part (e.g., `17.13.35919.96`). +- `windows.vsBuildTools.windows11Sdk.build` — Windows 11 SDK build number as integer (e.g., `22621`). +- `windows.vsBuildTools.vcTools.version` — Visual Studio VCTools workload version, four-part. + +The experience context is the maintainer or CI engineer reading `config/version.json` to know exactly which Windows toolchain a given image tag was built against — without having to grep Dockerfiles. + +#### Scenario: Manifest validates against schema + +- **GIVEN** the current `config/version.json` and `config/schema.cue` +- **WHEN** `cue vet config/schema.cue -d '#Version' config/version.json` runs +- **THEN** the command exits 0 + +#### Scenario: Missing `windows` block fails validation + +- **GIVEN** a candidate `config/version.json` with no `windows` field +- **WHEN** `cue vet config/schema.cue -d '#Version' config/version.json` runs +- **THEN** the command exits non-zero with an error naming the missing `windows` field + +### Requirement: `windows.Dockerfile` builds from manifest values, not hardcoded constants + +`windows.Dockerfile` SHALL accept the build args `flutter_version`, `git_version`, `vs_cmake_version`, `vs_win11sdk_build`, and `vs_vctools_version`, with no default values. The `--add Microsoft.VisualStudio.Component.Windows11SDK.${vs_win11sdk_build}` invocation in the Dockerfile SHALL substitute the build-arg value, and the Git installer download SHALL use the `git_version` build arg in the URL and filename. + +The experience context is the contributor changing the Windows toolchain: they edit one place (`config/version.json`), regenerate, and the build picks up the change. + +#### Scenario: Build with manifest values succeeds + +- **GIVEN** `config/version.json` declares the four windows version fields +- **AND** `windows.yml` passes them as `--build-arg` from the env vars exported by `setEnvironmentVariables.js` +- **WHEN** the test_windows job runs `docker build` +- **THEN** the build completes and the resulting image has VS components installed at the manifest-declared versions + +#### Scenario: Build without manifest values fails fast + +- **WHEN** a developer runs `docker build -f windows.Dockerfile .` without any `--build-arg` +- **THEN** the build fails on the first ARG-using step +- **AND** the error message names the missing build argument + +### Requirement: `setEnvironmentVariables.js` exports Windows fields as env vars + +`script/setEnvironmentVariables.js` SHALL read `windows.git.version`, `windows.vsBuildTools.cmakeProject.version`, `windows.vsBuildTools.windows11Sdk.build`, and `windows.vsBuildTools.vcTools.version` from `config/version.json` and export them as `GIT_VERSION`, `VS_CMAKE_VERSION`, `VS_WIN11SDK_BUILD`, and `VS_VCTOOLS_VERSION` to the GitHub Actions environment. + +The experience context is `windows.yml` and `release.yml` reading exactly the same env vars to feed `docker build` — a single point of plumbing. + +#### Scenario: Workflow env contains the windows fields + +- **WHEN** the `Read environment variables from the version manifest` step runs in any workflow +- **THEN** the workflow's environment contains `GIT_VERSION`, `VS_CMAKE_VERSION`, `VS_WIN11SDK_BUILD`, `VS_VCTOOLS_VERSION` with values matching `config/version.json` + +### Requirement: Pester suite asserts exact toolchain versions + +The Pester suite at `test/windows/Windows.Tests.ps1` SHALL read `config/version.json` (already copied into the test stage by `p1-fix-windows-ci-tests`) and assert that: + +- `git --version` reports a version equal to `windows.git.version`, +- the `Microsoft.VisualStudio.Component.VC.CMake.Project,version=` directory's `` equals `windows.vsBuildTools.cmakeProject.version`, +- the `Microsoft.VisualStudio.Component.Windows11SDK.` directory's `` equals `windows.vsBuildTools.windows11Sdk.build`, +- the `Microsoft.VisualStudio.Workload.VCTools,version=` directory's `` equals `windows.vsBuildTools.vcTools.version`. + +The experience context is the reviewer of an upgrade PR: any drift between the manifest the PR proposes and the image actually produced is caught as a hard test failure, not silent semantics drift. + +#### Scenario: Manifest and image agree on every Windows version + +- **GIVEN** the test image was built with the build args derived from the current `config/version.json` +- **WHEN** the Pester suite runs +- **THEN** all four version assertions pass + +#### Scenario: Manifest claims a version the image does not have + +- **GIVEN** a PR that bumps `windows.git.version` to a version different from the build arg actually passed to the image +- **WHEN** the Pester suite runs +- **THEN** the Git version test fails with a message naming both the manifest value and the in-image value + +### Requirement: Monthly upgrade PR includes Windows toolchain updates + +The `update_version.yml` workflow SHALL include a job that updates the `windows` block in `config/version.json` whenever it runs. The job SHALL: + +- read the latest Git for Windows release from `https://api.github.com/repos/git-for-windows/git/releases/latest` and write the resolved version to `windows.git.version`, +- write the VS BuildTools component versions from a deterministic source (see design — channel manifest or pinned config), +- emit a CUE-validated `config/version.json` that is consumed by `validate_config_version` and `update_docs_and_create_pr`. + +The experience context is the maintainer who merges the monthly upgrade PR and expects both Android and Windows toolchains to be bumped in the same PR — a single review surface. + +#### Scenario: Monthly run produces a Windows-aware upgrade PR + +- **GIVEN** a scheduled run of `update_version.yml` where Flutter has a new stable +- **AND** Git for Windows has a new release since the last run +- **WHEN** the workflow opens its upgrade PR +- **THEN** the PR's `config/version.json` has a bumped `windows.git.version` +- **AND** the PR's `config/version.json` passes `cue vet` against `#Version` + +#### Scenario: No Windows update needed in this cycle + +- **GIVEN** Git for Windows and VS BuildTools component versions match what is already in `config/version.json` +- **WHEN** the upgrade PR is composed +- **THEN** the PR still opens (because Flutter changed) but the `windows` block is unchanged byte-for-byte diff --git a/openspec/changes/p3-windows-version-schema/tasks.md b/openspec/changes/p3-windows-version-schema/tasks.md new file mode 100644 index 0000000..b2a014a --- /dev/null +++ b/openspec/changes/p3-windows-version-schema/tasks.md @@ -0,0 +1,63 @@ +## 1. Extend the CUE schema + +- [ ] 1.1 Add `#SemverQuad: { version!: =~ "^\\d+\\.\\d+\\.\\d+\\.\\d+$" }` to `config/schema.cue` alongside the existing `#SemverPatch`. +- [ ] 1.2 Add a `#WindowsToolchain` definition with sub-fields `git: #SemverPatch`, `vsBuildTools.cmakeProject: #SemverQuad`, `vsBuildTools.windows11Sdk.build!: int`, `vsBuildTools.vcTools: #SemverQuad`. +- [ ] 1.3 Extend `#Version` to require `windows: #WindowsToolchain`. +- [ ] 1.4 Run `cue vet config/schema.cue -d '#Version' config/version.json` and confirm it fails (because `version.json` doesn't yet have the `windows` block — the next group fixes this). + +## 2. Backfill `config/version.json` + +- [ ] 2.1 Read the current values from `windows.Dockerfile`: Git `2.46.0`, Win11SDK build `22621`. Run `vs_BuildTools.exe` introspection (or look at a successful `windows-2025` build's package directory listing in a recent CI run) to find the four-part versions actually installed for `Microsoft.VisualStudio.Component.VC.CMake.Project` and `Microsoft.VisualStudio.Workload.VCTools`. +- [ ] 2.2 Add the `windows` block to `config/version.json` with those four values. +- [ ] 2.3 Run `cue vet config/schema.cue -d '#Version' config/version.json`; it should now exit 0. + +## 3. Remove version literals from `windows.Dockerfile` + +- [ ] 3.1 Replace `ARG git_version=2.46.0` with `ARG git_version` (no default). +- [ ] 3.2 Add `ARG vs_cmake_version`, `ARG vs_win11sdk_build`, `ARG vs_vctools_version` (no defaults) before the `vs_BuildTools.exe` invocation. +- [ ] 3.3 Replace the literal `--add Microsoft.VisualStudio.Component.Windows11SDK.22621` with `--add Microsoft.VisualStudio.Component.Windows11SDK.${env:vs_win11sdk_build}` (or the correct PowerShell expansion syntax inside the RUN command). +- [ ] 3.4 The CMake and VCTools `--add` lines do not embed versions in the component id — versions are picked up from the channel and asserted later by Pester. Leave the `--add` lines alone for these. + +## 4. Surface the new fields via `setEnvironmentVariables.js` + +- [ ] 4.1 Open `script/setEnvironmentVariables.js`. Add reads for `windows.git.version`, `windows.vsBuildTools.cmakeProject.version`, `windows.vsBuildTools.windows11Sdk.build`, `windows.vsBuildTools.vcTools.version`. +- [ ] 4.2 Export each via `core.exportVariable` as `GIT_VERSION`, `VS_CMAKE_VERSION`, `VS_WIN11SDK_BUILD`, `VS_VCTOOLS_VERSION`. +- [ ] 4.3 Confirm existing exported variables are unchanged. + +## 5. Wire build args into the workflows + +- [ ] 5.1 In `.github/workflows/windows.yml`, update the `docker build` invocation in the `Test image and push to local Docker daemon` step to pass `--build-arg git_version=${{ env.GIT_VERSION }}`, `--build-arg vs_cmake_version=${{ env.VS_CMAKE_VERSION }}`, `--build-arg vs_win11sdk_build=${{ env.VS_WIN11SDK_BUILD }}`, `--build-arg vs_vctools_version=${{ env.VS_VCTOOLS_VERSION }}`. +- [ ] 5.2 If `p2-release-windows-image` is already merged, apply the same `--build-arg` changes to the `release_windows` job in `.github/workflows/release.yml`. + +## 6. Tighten the Pester assertions + +- [ ] 6.1 In `test/windows/Windows.Tests.ps1`, add a `BeforeAll` block that reads `config\version.json` via `Get-Content -Raw | ConvertFrom-Json` into `$manifest`. +- [ ] 6.2 Add a Git version test that runs `git --version`, parses the leading three-part semver (`X.Y.Z` from `git version X.Y.Z.windows.N`), and asserts equality with `$manifest.windows.git.version`. +- [ ] 6.3 Tighten the existing CMake assertion: change pattern from `,version=*` to `,version=$($manifest.windows.vsBuildTools.cmakeProject.version)*`. +- [ ] 6.4 Tighten the existing Win11SDK assertion: change to match `Microsoft.VisualStudio.Component.Windows11SDK.$($manifest.windows.vsBuildTools.windows11Sdk.build),version*`. +- [ ] 6.5 Tighten the existing VCTools assertion: change pattern from `,version=*` to `,version=$($manifest.windows.vsBuildTools.vcTools.version)*`. + +## 7. Add `update_windows_version` to `update_version.yml` + +- [ ] 7.1 Add a job `update_windows_version` after `update_flutter_version`. Set `runs-on: ubuntu-24.04`, `needs: update_flutter_version`, `if: ${{ needs.update_flutter_version.outputs.new_version == 'true' }}`. Note: this job runs in parallel with `update_android_version`. +- [ ] 7.2 Job step: download the `flutter_version.json` artifact (parallel pattern with `update_android_version`). +- [ ] 7.3 Job step: `curl -fsSL https://api.github.com/repos/git-for-windows/git/releases/latest | jq -r '.tag_name'`; strip leading `v` and trailing `.windows.N`; export as `GIT_VERSION_NEW`. +- [ ] 7.4 Job step: `curl -fsSL https://aka.ms/vs/17/release/channel | jq …` to extract the `Microsoft.VisualStudio.Component.VC.CMake.Project` and `Microsoft.VisualStudio.Workload.VCTools` component versions. Document the jq path in a comment so future maintainers can update it if the channel manifest restructures. +- [ ] 7.5 Job step: write the four resolved values into `config/version.json` using `jq` (preserving existing keys). +- [ ] 7.6 Job step: validate with `cue vet config/schema.cue -d '#Version' config/version.json`. Fail the job on non-zero exit. +- [ ] 7.7 Job step: upload `config/version.json` as an artifact named `version.json.windows` (so it doesn't collide with the Android artifact). +- [ ] 7.8 In `update_docs_and_create_pr`, add a `download` step for the new artifact and a merge step that combines the Android-emitted `version.json` and the Windows-emitted `version.json` into one. Document the merge order: Android artifact wins for `flutter`/`android` keys, Windows artifact wins for `windows` keys. + +## 8. Validate end-to-end + +- [ ] 8.1 Push the branch; the `windows.yml` PR check builds the image with the new build args and runs the tightened Pester suite. Confirm green. +- [ ] 8.2 Run `update_version.yml` via `workflow_dispatch` (which runs unconditionally on the dispatch branch); confirm a draft PR is opened with a non-empty `windows` block diff. (If Flutter is not changing, the dispatch will be a no-op; force a dispatch on a test branch where `flutter_version.json` has been hand-edited.) + +## 9. Documentation + +- [ ] 9.1 Update `docs/src/windows.mdx` (and the auto-generated `docs/windows.md`) to note that the Windows toolchain versions are pinned in `config/version.json` and refreshed by the monthly upgrade PR. +- [ ] 9.2 Remove now-completed TODO items in `docs/windows.md` referring to "test where is installed" and "snapshot of flutter config" — those are covered by the Pester suite now. + +## 10. Archive + +- [ ] 10.1 After merge and confirmation that the next scheduled `update_version.yml` produces a sensible Windows-bumping PR, archive this change so the `windows-version-tracking` spec is promoted to `openspec/specs/` and the `flutter-version-update` delta is applied to the existing spec there. diff --git a/script/InstallPester.ps1 b/script/InstallPester.ps1 new file mode 100644 index 0000000..e26791c --- /dev/null +++ b/script/InstallPester.ps1 @@ -0,0 +1,4 @@ +Install-PackageProvider -Name NuGet -Force +Install-Module -Name Pester -Force -SkipPublisherCheck + +Write-Host "Pester installation completed successfully" diff --git a/script/RunPester.ps1 b/script/RunPester.ps1 new file mode 100644 index 0000000..8879992 --- /dev/null +++ b/script/RunPester.ps1 @@ -0,0 +1,9 @@ +[Console]::OutputEncoding = [System.Text.Encoding]::UTF8 + +$pesterConfig = New-PesterConfiguration; +$pesterConfig.Output.Verbosity = 'Detailed'; +$pesterConfig.Run.Exit = $true; +$pesterConfig.Run.Path = ".\test"; + +Invoke-Pester -Configuration $pesterConfig; +Exit $LASTEXITCODE; diff --git a/script/test.sh b/script/test_android_from_linux.sh old mode 100755 new mode 100644 similarity index 100% rename from script/test.sh rename to script/test_android_from_linux.sh diff --git a/script/test_windows.sh b/script/test_android_from_windows.sh old mode 100755 new mode 100644 similarity index 100% rename from script/test_windows.sh rename to script/test_android_from_windows.sh diff --git a/test/windows/Windows.Tests.ps1 b/test/windows/Windows.Tests.ps1 new file mode 100644 index 0000000..6da2331 --- /dev/null +++ b/test/windows/Windows.Tests.ps1 @@ -0,0 +1,83 @@ +Describe "Flutter version" { + It "Should match the version in config/version.json" { + $manifest = Get-Content "config\version.json" | ConvertFrom-Json + $expectedVersion = $manifest.flutter.version + + $firstLine = flutter --version 2>&1 | Select-Object -First 1 + $firstLine -match 'Flutter (\S+)' | Out-Null + $actualVersion = $Matches[1] + + $actualVersion | Should -Be $expectedVersion -Because "flutter --version reported '$actualVersion' but config/version.json specifies '$expectedVersion'" + } +} + +Describe "Flutter doctor" { + BeforeAll { + # Flutter doctor on Windows uses U+221A (SQUARE ROOT) for pass and ASCII X for fail. + # On other platforms it uses U+2713/U+2717. Keep this source pure ASCII so Windows + # PowerShell 5.x does not choke on the file encoding. + $script:passMarkers = @([char]0x221A, [char]0x2713, [char]0x2714) + $script:failMarkers = @('X', 'x', [char]0x2717, [char]0x2718) + $script:doctorOutput = flutter doctor 2>&1 + } + + It "Should report a healthy Windows toolchain with no unexpected errors" { + $skippedPlatforms = @('Android', 'iOS', 'macOS', 'Linux', 'Web', 'Chrome') + $failures = @() + $expectedMark = "[" + [char]0x221A + "]" + + foreach ($line in $script:doctorOutput) { + if ($line -match '^\[(.)\] (.+)$') { + $marker = $Matches[1] + $header = $Matches[2] + + $skip = $false + foreach ($platform in $skippedPlatforms) { + if ($header -like "$platform*") { $skip = $true; break } + } + if ($skip) { continue } + + $isPass = $script:passMarkers -contains $marker + $isFail = $script:failMarkers -contains $marker + + if ($header -like 'Windows Version*' -or $header -like 'Visual Studio*') { + if (-not $isPass) { + $failures += "[$marker] $header (expected $expectedMark)" + } + } elseif ($isFail) { + $failures += "[$marker] $header" + } + } + } + + $failures | Should -BeNullOrEmpty -Because ($failures -join '; ') + } +} + +Describe "Windows file structure tests" { + It "Should have specific file content in dart telemetry config" { + "$env:APPDATA\.dart-tool\dart-flutter-telemetry.config" | Should -FileContentMatchExactly "reporting=0" + } + + Context "VisualStudio components" { + BeforeAll { + $visualStudioPackages = (Get-ChildItem $env:ProgramData\Microsoft\VisualStudio\Packages).Name + } + + It "CMake version matches" { + $directoryName = $visualStudioPackages | Select-String -CaseSensitive Microsoft.VisualStudio.Component.VC.CMake.Project + $directoryName | Should -BeLikeExactly "Microsoft.VisualStudio.Component.VC.CMake.Project,version=*" + } + + It "Windows11SDK version matches" { + $directoryName = $visualStudioPackages | Select-String -CaseSensitive Microsoft.VisualStudio.Component.Windows11SDK + $directoryName | Should -BeLikeExactly "Microsoft.VisualStudio.Component.Windows11SDK.22621,version=*" + } + + It "VCTools version matches" { + $directoryName = $visualStudioPackages | Select-String -CaseSensitive Microsoft.VisualStudio.Workload.VCTools + $directoryName | Should -BeLikeExactly "Microsoft.VisualStudio.Workload.VCTools,version=*" + } + } +} + \ No newline at end of file diff --git a/test/windows/main_test.go b/test/windows/main_test.go deleted file mode 100644 index e69de29..0000000 diff --git a/windows.Dockerfile b/windows.Dockerfile index b63cad3..d16541f 100644 --- a/windows.Dockerfile +++ b/windows.Dockerfile @@ -83,6 +83,42 @@ RUN flutter build windows; WORKDIR "$USERPROFILE" COPY ./script/docker_windows_entrypoint.ps1 "docker_entrypoint.ps1" +# hadolint ignore=DL3025 ENTRYPOINT "C:\Users\ContainerUser\docker_entrypoint.ps1" RUN Remove-Item -Recurse build_app; + +#----------------------------------------------- +#----------------------------------------------- +#----------------------------------------------- + +FROM flutter as test + +SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] + +# TODO: Find a way to pass $env:USERPROFILE instead of hardcoding C:\Users\ContainerUser. It's hardcoded because environment variables in Windows container works by setting for the Machine scope and that will have $env:USERPROFILE as C:\Users\ContainerAdministrator instead. +ENV USERPROFILE="C:\Users\ContainerUser" + +WORKDIR "$USERPROFILE" + +# Install Pester +COPY ./script/InstallPester.ps1 ".\InstallPester.ps1" + +# Administrator rights are required to install modules in 'C:\Program Files\WindowsPowerShell\Modules' +USER ContainerAdministrator +RUN ".\InstallPester.ps1"; ` + Remove-Item ".\InstallPester.ps1"; ` + Import-Module Pester; +USER ContainerUser + +# Run the tests +COPY ./config/version.json ".\config\version.json" +COPY ./test/windows/Windows.Tests.ps1 ".\test\Windows.Tests.ps1" +COPY ./script/RunPester.ps1 ".\script\RunPester.ps1" + +# Reset the inherited shell-form ENTRYPOINT from the flutter stage. The test image runs Pester, +# not the analytics-toggle entrypoint, and shell-form ENTRYPOINT prevents CMD args from being +# appended cleanly (Docker emits "Shell-form ENTRYPOINT and exec-form CMD may have unexpected +# results" otherwise). +ENTRYPOINT ["powershell", "-NoLogo", "-NoProfile", "-File"] +CMD [".\\script\\RunPester.ps1"]