diff --git a/.github/gx.toml b/.github/gx.toml index 7beef66..82523c9 100644 --- a/.github/gx.toml +++ b/.github/gx.toml @@ -19,6 +19,6 @@ "plexsystems/container-structure-test-action" = "~0.3.0" [actions.overrides] -"actions/download-artifact" = [{ workflow = ".github/workflows/build.yml", job = "test_image", step = 2, version = "^4" }, { workflow = ".github/workflows/build.yml", job = "scan_image", step = 0, version = "^4" }, { workflow = ".github/workflows/update_version.yml", job = "validate_config_version", step = 2, version = "~6.0.0" }, { workflow = ".github/workflows/update_version.yml", job = "update_docs_and_create_pr", step = 2, version = "~6.0.0" }, { workflow = ".github/workflows/update_version.yml", job = "update_docs_and_create_pr", step = 3, version = "~6.0.0" }, { workflow = ".github/workflows/update_version.yml", job = "update_android_version", step = 2, version = "~6.0.0" }] +"actions/download-artifact" = [{ workflow = ".github/workflows/build.yml", job = "test_image", step = 2, version = "^4" }, { workflow = ".github/workflows/build.yml", job = "scan_image", step = 0, version = "^4" }, { workflow = ".github/workflows/update_version.yml", job = "validate_config_version", step = 2, version = "~6.0.0" }, { workflow = ".github/workflows/update_version.yml", job = "update_docs_and_create_pr", step = 2, version = "~6.0.0" }, { workflow = ".github/workflows/update_version.yml", job = "update_android_version", step = 2, version = "~6.0.0" }, { workflow = ".github/workflows/update_version.yml", job = "update_windows_version", step = 3, version = "~6.0.0" }, { workflow = ".github/workflows/update_version.yml", job = "update_docs_and_create_pr", step = 6, version = "~6.0.0" }] "docker/metadata-action" = [ { 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" }, { workflow = ".github/workflows/windows.yml", job = "test_windows", step = 4, version = "~5.7.0" }, { workflow = ".github/workflows/release.yml", job = "release_windows", step = 2, version = "~5.10.0" }, { workflow = ".github/workflows/build.yml", job = "build_image", step = 7, version = "~5.10.0" }] "docker/scout-action" = [{ workflow = ".github/workflows/build.yml", job = "scan_image", step = 3, version = "~1.20.4" }, { workflow = ".github/workflows/release.yml", job = "record_image", step = 2, version = "~1.18.2" }, { workflow = ".github/workflows/build.yml", job = "scan_image", step = 5, version = "~1.20.4" }] diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 6d7df46..8271794 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -159,6 +159,10 @@ jobs: $buildArgs = $tagArgs + $labelArgs + @( '--build-arg', "flutter_version=${{ env.FLUTTER_VERSION }}", + '--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 }}", '--target', 'flutter', '--file', 'windows.Dockerfile', '.' diff --git a/.github/workflows/update_version.yml b/.github/workflows/update_version.yml index 8b355a3..04da688 100644 --- a/.github/workflows/update_version.yml +++ b/.github/workflows/update_version.yml @@ -83,6 +83,119 @@ jobs: name: flutter_version.json path: ${{ env.FLUTTER_VERSION_PATH }} + update_windows_version: + permissions: + contents: write + needs: update_flutter_version + if: ${{ needs.update_flutter_version.outputs.new_version == 'true' }} + outputs: + version_artifact_id: ${{ steps.upload-version.outputs.artifact-id }} + runs-on: ubuntu-24.04 + steps: + - name: Checkout repository + uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 + + - name: Setup CUE + uses: jaxxstorm/action-install-gh-release@6096f2a2bbfee498ced520b6922ac2c06e990ed2 # v2.1.0 + with: + repo: cue-lang/cue + tag: v0.15.0 + digest: 06925fc1e5174591cef0b1e42ac32cff4271804742cd20893de1793b6d82d460 + + # Parallel pattern with update_android_version: the artifact is downloaded + # to keep job structure symmetric, even though windows fields are not + # currently tied to a specific Flutter tag. + - name: Delete flutter_version.json + run: rm ${{ env.FLUTTER_VERSION_PATH }} + + - name: Download artifact with the new Flutter version + uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 + with: + artifact-ids: ${{ needs.update_flutter_version.outputs.flutter_version_artifact_id }} + path: config + + - name: Resolve latest Git for Windows version + id: git_version + run: | + tag=$(curl -fsSL https://api.github.com/repos/git-for-windows/git/releases/latest | jq -r '.tag_name') + version=${tag#v} + version=${version%.windows.*} + echo "git_version=$version" >> "$GITHUB_OUTPUT" + echo "Resolved Git for Windows version: $version" + + - name: Fetch VS channel manifest + run: curl -fsSL https://aka.ms/vs/17/release/channel -o channel.json + + # The channel manifest only contains product-level entries; per-component + # versions live in VisualStudio.vsman, referenced and SHA-256 pinned from + # the channel via the Microsoft.VisualStudio.Manifests.VisualStudio item. + - name: Resolve VS catalog manifest payload + id: vsman_payload + run: | + url=$(jq -r '.channelItems[] | select(.id=="Microsoft.VisualStudio.Manifests.VisualStudio") | .payloads[0].url' channel.json) + sha=$(jq -r '.channelItems[] | select(.id=="Microsoft.VisualStudio.Manifests.VisualStudio") | .payloads[0].sha256' channel.json) + echo "url=$url" >> "$GITHUB_OUTPUT" + echo "sha=$sha" >> "$GITHUB_OUTPUT" + + - name: Download and verify VS catalog manifest + run: | + curl -fsSL "${{ steps.vsman_payload.outputs.url }}" -o vsman.json + echo "${{ steps.vsman_payload.outputs.sha }} vsman.json" | sha256sum -c - + + - name: Resolve VS BuildTools component versions + id: vs_versions + run: | + cmake=$(jq -r '.packages[] | select(.id=="Microsoft.VisualStudio.Component.VC.CMake.Project") | .version' vsman.json) + vctools=$(jq -r '.packages[] | select(.id=="Microsoft.VisualStudio.Workload.VCTools") | .version' vsman.json) + echo "cmake=$cmake" >> "$GITHUB_OUTPUT" + echo "vctools=$vctools" >> "$GITHUB_OUTPUT" + echo "CMake: $cmake; VCTools: $vctools" + + - name: Write windows block into config/version.json + env: + GIT_VERSION: ${{ steps.git_version.outputs.git_version }} + VS_CMAKE_VERSION: ${{ steps.vs_versions.outputs.cmake }} + VS_VCTOOLS_VERSION: ${{ steps.vs_versions.outputs.vctools }} + run: | + # Win11SDK build id is human-pinned (changes infrequently; tied to OS branding). + win11sdk_build=$(jq -r '.windows.vsBuildTools.windows11Sdk.build' config/version.json) + jq --arg git "$GIT_VERSION" \ + --arg cmake "$VS_CMAKE_VERSION" \ + --arg vctools "$VS_VCTOOLS_VERSION" \ + --argjson sdkbuild "$win11sdk_build" \ + '.windows = { + git: {version: $git}, + vsBuildTools: { + cmakeProject: {version: $cmake}, + windows11Sdk: {build: $sdkbuild}, + vcTools: {version: $vctools} + } + }' config/version.json > config/version.json.tmp + mv config/version.json.tmp config/version.json + + - name: Validate version.json with CUE + run: cue vet config/schema.cue -d '#Version' config/version.json + + # The artifact's file is renamed so it doesn't collide with the Android + # artifact's version.json in update_docs_and_create_pr's merge step. + - name: Stage windows-only artifact + run: cp config/version.json "${RUNNER_TEMP}/version.json.windows" + + - name: Upload artifact with the updated windows block + id: upload-version + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + with: + name: version.json.windows + path: ${{ runner.temp }}/version.json.windows + + - name: Upload VS manifest artifacts for forensics + uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 + with: + name: vs-manifests + path: | + channel.json + vsman.json + update_android_version: permissions: # Allow to write contents to push commits @@ -223,6 +336,7 @@ jobs: needs: - update_flutter_version - update_android_version + - update_windows_version - validate_config_version runs-on: ubuntu-24.04 env: @@ -243,11 +357,33 @@ jobs: - name: Download configuration artifacts uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: - artifact-ids: ${{ needs.update_flutter_version.outputs.flutter_version_artifact_id }},${{ needs.update_android_version.outputs.version_artifact_id }} + artifact-ids: ${{ needs.update_flutter_version.outputs.flutter_version_artifact_id }},${{ needs.update_android_version.outputs.version_artifact_id }},${{ needs.update_windows_version.outputs.version_artifact_id }} path: config - # Download to the configured path instead of separated directories by artifact id + # Download to the configured path instead of separated directories by artifact id. + # Artifacts contain distinct filenames (flutter_version.json, version.json, + # version.json.windows) so merge-multiple is safe. merge-multiple: true + # Merge order: the Android artifact's version.json is the base (it carries + # the new flutter and android blocks). The Windows artifact's windows block + # overlays the windows field. This keeps each producer authoritative over + # its own block. + - name: Merge windows block into version.json + run: | + jq -s '.[0] + {windows: .[1].windows}' config/version.json config/version.json.windows > config/version.json.merged + mv config/version.json.merged config/version.json + rm config/version.json.windows + + - name: Setup CUE + uses: jaxxstorm/action-install-gh-release@6096f2a2bbfee498ced520b6922ac2c06e990ed2 # v2.1.0 + with: + repo: cue-lang/cue + tag: v0.15.0 + digest: 06925fc1e5174591cef0b1e42ac32cff4271804742cd20893de1793b6d82d460 + + - name: Validate merged version.json with CUE + run: cue vet config/schema.cue -d '#Version' config/version.json + - name: Download test artifacts uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 with: diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 83bbc00..94c6a88 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -58,7 +58,13 @@ jobs: - 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 ${{ fromJson(steps.metadata.outputs.json).tags[0] }} --target test + docker build . -f windows.Dockerfile ` + --build-arg flutter_version=${{ env.FLUTTER_VERSION }} ` + --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 }} ` + -t ${{ fromJson(steps.metadata.outputs.json).tags[0] }} --target test docker run --rm ${{ fromJson(steps.metadata.outputs.json).tags[0] }} diff --git a/config/schema.cue b/config/schema.cue index b2627a5..de81749 100644 --- a/config/schema.cue +++ b/config/schema.cue @@ -13,6 +13,21 @@ import "list" version!: =~ "^\\d+.\\d+.\\d+$" } +#SemverQuad: { + version!: =~ "^\\d+\\.\\d+\\.\\d+\\.\\d+$" +} + +#WindowsToolchain: { + git: #SemverPatch + vsBuildTools: { + cmakeProject: #SemverQuad + windows11Sdk: { + build!: int + } + vcTools: #SemverQuad + } +} + #FlutterVersion: { flutter: { channel!: "stable" @@ -36,4 +51,6 @@ import "list" } fastlane!: #SemverPatch + + windows!: #WindowsToolchain } diff --git a/config/version.json b/config/version.json index f33a947..e3b2660 100644 --- a/config/version.json +++ b/config/version.json @@ -28,5 +28,21 @@ }, "fastlane": { "version": "2.234.0" + }, + "windows": { + "git": { + "version": "2.46.0" + }, + "vsBuildTools": { + "cmakeProject": { + "version": "17.14.36510.44" + }, + "windows11Sdk": { + "build": 22621 + }, + "vcTools": { + "version": "17.14.36331.10" + } + } } } diff --git a/docs/src/windows.mdx b/docs/src/windows.mdx index ebd18bc..3e884fe 100644 --- a/docs/src/windows.mdx +++ b/docs/src/windows.mdx @@ -4,6 +4,10 @@ & $Env:ProgramFiles\Docker\Docker\DockerCli.exe -SwitchDaemon +## Toolchain versions + +Windows toolchain versions (Git for Windows, Visual Studio BuildTools components, Windows 11 SDK build) are pinned in `config/version.json` under the `windows` block and validated by `config/schema.cue`. The `update_version.yml` workflow refreshes these alongside Flutter and Android in the monthly upgrade PR, and the Pester suite asserts the installed image matches the manifest on every CI run. + ## TODO 1. Install tools @@ -32,9 +36,7 @@ RUN Invoke-WebRequest -Uri https://aka.ms/vs/17/release/vs_buildtools.exe -OutFi 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. 1. Test that path to powershell.exe exists. -1. Test with a snapshot of flutter config to determine if new feature flags should be enabled or disabled. 1. Test that Build Tools were installed in C:\Program Files (x86)\Microsoft Visual Studio\2022\BuildTools\msbuild\current\bin 1. Check [Windows installation requirements for Flutter](https://docs.flutter.dev/get-started/install/windows/desktop) 1. Add docs explaining to use `$VerbosePreference = 'Continue';` in the SHELL to debug unexpected pwsh problems. diff --git a/docs/windows.md b/docs/windows.md index 7351de7..b92912f 100644 --- a/docs/windows.md +++ b/docs/windows.md @@ -6,6 +6,10 @@ & $Env:ProgramFiles\\Docker\\Docker\\DockerCli.exe -SwitchDaemon +## Toolchain versions + +Windows toolchain versions (Git for Windows, Visual Studio BuildTools components, Windows 11 SDK build) are pinned in `config/version.json` under the `windows` block and validated by `config/schema.cue`. The `update_version.yml` workflow refreshes these alongside Flutter and Android in the monthly upgrade PR, and the Pester suite asserts the installed image matches the manifest on every CI run. + ## TODO 1. Install tools @@ -35,12 +39,10 @@ RUN Invoke-WebRequest -Uri https://aka.ms/vs/17/release/vs_buildtools.exe -OutFi 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. +4. Test that path to powershell.exe exists. +5. Test that Build Tools were installed in C:\\Program Files (x86)\\Microsoft Visual Studio\\2022\\BuildTools\\msbuild\\current\\bin +6. Check [Windows installation requirements for Flutter](https://docs.flutter.dev/get-started/install/windows/desktop) +7. Add docs explaining to use `$VerbosePreference = 'Continue';` in the SHELL to debug unexpected pwsh problems. ## Open issue in windows Docker images repo diff --git a/mise.toml b/mise.toml new file mode 100644 index 0000000..f743dce --- /dev/null +++ b/mise.toml @@ -0,0 +1,2 @@ +[tools] +cue = "0.15.0" diff --git a/openspec/changes/p3-windows-version-schema/design.md b/openspec/changes/p3-windows-version-schema/design.md index 06864cb..811f02b 100644 --- a/openspec/changes/p3-windows-version-schema/design.md +++ b/openspec/changes/p3-windows-version-schema/design.md @@ -4,7 +4,7 @@ The repository already has a strong "manifest is source of truth" discipline for 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. +- Microsoft does not publish a clean, programmatic, monotonic API for VS BuildTools component versions. The closest source is a two-step fetch: the channel manifest at `https://aka.ms/vs/17/release/channel` (JSON, ~90 KB) lists 13 product-level entries all stamped with the overall release version (e.g., `17.14.37314.3`) and references the catalog manifest `VisualStudio.vsman` (JSON, ~17 MB, SHA-256 pinned by the channel) via the `Microsoft.VisualStudio.Manifests.VisualStudio` channel item. `vsman.packages[]` is where per-component versions live (verified 2026-05-21: `VC.CMake.Project` → `17.14.36510.44`, `Windows11SDK.22621` → `17.14.36510.44`, `Workload.VCTools` → `17.14.36331.10`). The channel manifest alone is insufficient. - 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. @@ -29,9 +29,11 @@ Constraints: ## Decisions -### Decision: VS BuildTools component versions are pinned in `config/version.json`, refreshed manually from the channel manifest +### Decision: VS BuildTools component versions are pinned in `config/version.json`, refreshed from the VS catalog manifest (`vsman`) via a two-step fetch -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. +The `update_windows_version` job (1) fetches `https://aka.ms/vs/17/release/channel`, (2) extracts the `Microsoft.VisualStudio.Manifests.VisualStudio` payload URL + SHA-256, (3) downloads that `VisualStudio.vsman` (~17 MB), (4) verifies the SHA, (5) `jq`s `.packages[] | select(.id == ) | .version` for each tracked component, and writes the resolved versions into `config/version.json`. The catalog 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. + +The SHA-pinning of `vsman` by the channel is a free integrity property — the job can trust the catalog without committing it. Also note: `Microsoft.VisualStudio.Component.Windows11SDK.22621` carries the build id (`22621`) in the component **id**, not the version field; the `version` is the VS release stamp. This matches the design's decision to model the SDK build as a bare `int` separate from the `#SemverQuad` version fields. Alternatives considered: @@ -74,7 +76,9 @@ This means the test compares the leading three parts of `git --version` output t - **[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. +- **[Trade-off] `update_windows_version` adds a runtime dependency on `aka.ms/vs/17/release/channel`, `download.visualstudio.microsoft.com` (for `vsman`), 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`. Three more upstreams is incremental. +- **[Trade-off] `update_windows_version` downloads ~17 MB (`VisualStudio.vsman`) per run.** → Acceptable. `update_version.yml` is scheduled `0 0 * * MON-FRI`, but `update_windows_version` is gated on `update_flutter_version.outputs.new_version == 'true'` (same gating pattern as `update_android_version`), so the fetch only fires when Flutter actually bumped — empirically about once a month. On quiet weekdays the job is skipped and nothing is downloaded. No cross-run caching: the catalog would have to be invalidated against the channel anyway, and at ~once-a-month firing the savings don't justify the cache plumbing. +- **[Forensic mitigation] The job uploads the raw `VisualStudio.vsman` (and the channel JSON) as workflow artifacts.** → 90-day retention is enough to diagnose any "why did this PR pick these versions" question without committing the manifest into git. Avoids the noisy-history tradeoff while preserving the forensic record. ## Automated Test Strategy @@ -109,8 +113,12 @@ This means the test compares the leading three parts of `git --version` output t 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. +## Resolved Questions + +- **`#SemverQuad` placement:** lives alongside `#SemverPatch` in `schema.cue`. `schema.cue` is 39 lines today; all five existing version primitives share the file, and there's no per-OS/per-domain split precedent. Splitting on Windows alone would invite drift (someone adds `#SemverQuint` to one file and forgets the other). +- **`vs_BuildTools.exe` URL as build arg:** no. The URL `https://aka.ms/vs/17/release/vs_buildtools.exe` embeds a single version token (`17` = VS 2022). A VS major-version bump (to `/vs/18/…`) is an out-of-band migration — component IDs change, the Pester assertions need rewriting, and `update_version.yml` cannot meaningfully automate it. Adding it as a build arg would be ceremony, not capability. A separate change handles VS 2025 if/when needed. +- **Commit `vsman` / channel manifest into the repo:** no. The channel manifest already SHA-256-pins `vsman`, so reproducibility-from-a-snapshot is built into Microsoft's design — committing it duplicates state Microsoft already authenticates. Forensic access is preserved by uploading both JSONs as workflow artifacts (90-day retention), which avoids polluting git history with vsman's frequent unrelated churn. + ## 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. +- None blocking. The three above are resolved; the original "channel manifest as source" assumption was corrected during research (see Constraints: per-component versions live in `vsman`, not the channel — verified 2026-05-21). diff --git a/openspec/changes/p3-windows-version-schema/proposal.md b/openspec/changes/p3-windows-version-schema/proposal.md index 21fc918..1ab4e0f 100644 --- a/openspec/changes/p3-windows-version-schema/proposal.md +++ b/openspec/changes/p3-windows-version-schema/proposal.md @@ -17,7 +17,7 @@ - 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), + - reads VS BuildTools component versions from the VS catalog manifest `VisualStudio.vsman` (fetched via `aka.ms/vs/17/release/channel` → `Microsoft.VisualStudio.Manifests.VisualStudio` payload, SHA-256-verified; see design for two-step fetch), - 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. @@ -38,5 +38,5 @@ - 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: VS BuildTools component versioning is provided by Microsoft via the VS catalog manifest (`vsman`, ~17 MB, referenced and SHA-pinned by 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/tasks.md b/openspec/changes/p3-windows-version-schema/tasks.md index b2a014a..e0b15fc 100644 --- a/openspec/changes/p3-windows-version-schema/tasks.md +++ b/openspec/changes/p3-windows-version-schema/tasks.md @@ -1,52 +1,58 @@ ## 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). +- [x] 1.1 Add `#SemverQuad: { version!: =~ "^\\d+\\.\\d+\\.\\d+\\.\\d+$" }` to `config/schema.cue` alongside the existing `#SemverPatch`. +- [x] 1.2 Add a `#WindowsToolchain` definition with sub-fields `git: #SemverPatch`, `vsBuildTools.cmakeProject: #SemverQuad`, `vsBuildTools.windows11Sdk.build!: int`, `vsBuildTools.vcTools: #SemverQuad`. +- [x] 1.3 Extend `#Version` to require `windows: #WindowsToolchain`. +- [x] 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. +- [x] 2.1 Read the current values from `windows.Dockerfile`: Git `2.46.0`, Win11SDK build `22621`. Source the four-part component versions from `VisualStudio.vsman` (fetched via `aka.ms/vs/17/release/channel` → `Microsoft.VisualStudio.Manifests.VisualStudio` payload URL; see Decision in design.md). As of 2026-05-21: `Microsoft.VisualStudio.Component.VC.CMake.Project` = `17.14.36510.44`, `Microsoft.VisualStudio.Workload.VCTools` = `17.14.36331.10`. Rerun the query close to merge time so backfilled values match what `windows.yml` will actually install. +- [x] 2.2 Add the `windows` block to `config/version.json` with those four values. +- [x] 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. +- [x] 3.1 Replace `ARG git_version=2.46.0` with `ARG git_version` (no default). +- [x] 3.2 Add `ARG vs_cmake_version`, `ARG vs_win11sdk_build`, `ARG vs_vctools_version` (no defaults) before the `vs_BuildTools.exe` invocation. +- [x] 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). +- [x] 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. +- [x] 4.1 Open `script/setEnvironmentVariables.js`. Add reads for `windows.git.version`, `windows.vsBuildTools.cmakeProject.version`, `windows.vsBuildTools.windows11Sdk.build`, `windows.vsBuildTools.vcTools.version`. +- [x] 4.2 Export each via `core.exportVariable` as `GIT_VERSION`, `VS_CMAKE_VERSION`, `VS_WIN11SDK_BUILD`, `VS_VCTOOLS_VERSION`. +- [x] 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`. +- [x] 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 }}`. +- [x] 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)*`. +- [x] 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`. +- [x] 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`. +- [x] 6.3 Tighten the existing CMake assertion: change pattern from `,version=*` to `,version=$($manifest.windows.vsBuildTools.cmakeProject.version)*`. +- [x] 6.4 Tighten the existing Win11SDK assertion: change to match `Microsoft.VisualStudio.Component.Windows11SDK.$($manifest.windows.vsBuildTools.windows11Sdk.build),version*`. +- [x] 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. +- [x] 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`. +- [x] 7.2 Job step: download the `flutter_version.json` artifact (parallel pattern with `update_android_version`). +- [x] 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`. +- [x] 7.4 Job steps to resolve VS component versions (two-step fetch — the channel manifest alone does NOT contain per-component versions; they live in `vsman`): + - [x] 7.4a `curl -fsSL https://aka.ms/vs/17/release/channel -o channel.json` + - [x] 7.4b Extract the catalog URL and SHA: ``vsman_url=$(jq -r '.channelItems[] | select(.id=="Microsoft.VisualStudio.Manifests.VisualStudio") | .payloads[0].url' channel.json)`` and ``vsman_sha=$(jq -r '.channelItems[] | select(.id=="Microsoft.VisualStudio.Manifests.VisualStudio") | .payloads[0].sha256' channel.json)`` + - [x] 7.4c `curl -fsSL "$vsman_url" -o vsman.json` (~17 MB) + - [x] 7.4d Verify SHA-256: `echo "$vsman_sha vsman.json" | sha256sum -c -`. Fail the job on mismatch. + - [x] 7.4e Extract versions: `jq -r '.packages[] | select(.id=="Microsoft.VisualStudio.Component.VC.CMake.Project") | .version' vsman.json` (and analogously for `Microsoft.VisualStudio.Workload.VCTools`). Document the jq paths in comments so future maintainers can update them if the catalog manifest restructures. + - [x] 7.4f Upload both `channel.json` and `vsman.json` as workflow artifacts (90-day retention) for forensic record. This is the mitigation that lets us skip committing the manifest into git (see resolved question in design.md). +- [x] 7.5 Job step: write the four resolved values into `config/version.json` using `jq` (preserving existing keys). +- [x] 7.6 Job step: validate with `cue vet config/schema.cue -d '#Version' config/version.json`. Fail the job on non-zero exit. +- [x] 7.7 Job step: upload `config/version.json` as an artifact named `version.json.windows` (so it doesn't collide with the Android artifact). +- [x] 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 @@ -55,8 +61,8 @@ ## 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. +- [x] 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. +- [x] 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 diff --git a/script/setEnvironmentVariables.js b/script/setEnvironmentVariables.js index 9ba8cd0..810e6be 100644 --- a/script/setEnvironmentVariables.js +++ b/script/setEnvironmentVariables.js @@ -34,6 +34,19 @@ module.exports = async ({ core }) => { core.exportVariable('ANDROID_PLATFORM_VERSIONS', platforms) core.exportVariable('ANDROID_NDK_VERSION', data.android.ndk.version) core.exportVariable('CMAKE_VERSION', data.android.cmake.version) + core.exportVariable('GIT_VERSION', data.windows.git.version) + core.exportVariable( + 'VS_CMAKE_VERSION', + data.windows.vsBuildTools.cmakeProject.version + ) + core.exportVariable( + 'VS_WIN11SDK_BUILD', + data.windows.vsBuildTools.windows11Sdk.build + ) + core.exportVariable( + 'VS_VCTOOLS_VERSION', + data.windows.vsBuildTools.vcTools.version + ) core.exportVariable( 'IMAGE_REPOSITORY_PATH', `${GITHUB_REPOSITORY_OWNER}/${IMAGE_REPOSITORY_NAME}` diff --git a/test/windows/Windows.Tests.ps1 b/test/windows/Windows.Tests.ps1 index 6da2331..31a8c4e 100644 --- a/test/windows/Windows.Tests.ps1 +++ b/test/windows/Windows.Tests.ps1 @@ -1,7 +1,10 @@ +BeforeAll { + $script:manifest = Get-Content -Raw "config\version.json" | ConvertFrom-Json +} + Describe "Flutter version" { It "Should match the version in config/version.json" { - $manifest = Get-Content "config\version.json" | ConvertFrom-Json - $expectedVersion = $manifest.flutter.version + $expectedVersion = $script:manifest.flutter.version $firstLine = flutter --version 2>&1 | Select-Object -First 1 $firstLine -match 'Flutter (\S+)' | Out-Null @@ -54,6 +57,18 @@ Describe "Flutter doctor" { } } +Describe "Git version" { + It "Should match windows.git.version in config/version.json" { + $expectedVersion = $script:manifest.windows.git.version + + $firstLine = git --version 2>&1 | Select-Object -First 1 + $firstLine -match 'git version (\d+\.\d+\.\d+)' | Out-Null + $actualVersion = $Matches[1] + + $actualVersion | Should -Be $expectedVersion -Because "git --version reported '$actualVersion' but config/version.json specifies '$expectedVersion'" + } +} + 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" @@ -65,18 +80,21 @@ Describe "Windows file structure tests" { } It "CMake version matches" { + $expectedVersion = $script:manifest.windows.vsBuildTools.cmakeProject.version $directoryName = $visualStudioPackages | Select-String -CaseSensitive Microsoft.VisualStudio.Component.VC.CMake.Project - $directoryName | Should -BeLikeExactly "Microsoft.VisualStudio.Component.VC.CMake.Project,version=*" + $directoryName | Should -BeLikeExactly "Microsoft.VisualStudio.Component.VC.CMake.Project,version=$expectedVersion*" } It "Windows11SDK version matches" { + $expectedBuild = $script:manifest.windows.vsBuildTools.windows11Sdk.build $directoryName = $visualStudioPackages | Select-String -CaseSensitive Microsoft.VisualStudio.Component.Windows11SDK - $directoryName | Should -BeLikeExactly "Microsoft.VisualStudio.Component.Windows11SDK.22621,version=*" + $directoryName | Should -BeLikeExactly "Microsoft.VisualStudio.Component.Windows11SDK.$expectedBuild,version=*" } It "VCTools version matches" { + $expectedVersion = $script:manifest.windows.vsBuildTools.vcTools.version $directoryName = $visualStudioPackages | Select-String -CaseSensitive Microsoft.VisualStudio.Workload.VCTools - $directoryName | Should -BeLikeExactly "Microsoft.VisualStudio.Workload.VCTools,version=*" + $directoryName | Should -BeLikeExactly "Microsoft.VisualStudio.Workload.VCTools,version=$expectedVersion*" } } } diff --git a/windows.Dockerfile b/windows.Dockerfile index d16541f..61670d8 100644 --- a/windows.Dockerfile +++ b/windows.Dockerfile @@ -4,7 +4,7 @@ FROM mcr.microsoft.com/windows/servercore:ltsc2025@sha256:83374b6927f7945bb0933d SHELL ["powershell", "-Command", "$ErrorActionPreference = 'Stop'; $ProgressPreference = 'SilentlyContinue';"] -ARG git_version=2.46.0 +ARG git_version ARG git_installation_path="C:\Program Files\Git" # 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. @@ -64,15 +64,19 @@ RUN git clone ` flutter create build_app; +ARG vs_cmake_version +ARG vs_win11sdk_build +ARG vs_vctools_version + # The user ContainerAdministrator must be used because is the one that has permissions to install with vs_BuildTools USER ContainerAdministrator # Download the Build Tools bootstrapper # See https://learn.microsoft.com/en-us/visualstudio/install/build-tools-container?view=vs-2022 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 ` + Start-Process vs_BuildTools.exe -ArgumentList \"--quiet --wait --norestart --nocache ` --add Microsoft.VisualStudio.Component.VC.CMake.Project ` - --add Microsoft.VisualStudio.Component.Windows11SDK.22621 ` - --add Microsoft.VisualStudio.Workload.VCTools' ` + --add Microsoft.VisualStudio.Component.Windows11SDK.${env:vs_win11sdk_build} ` + --add Microsoft.VisualStudio.Workload.VCTools\" ` -Wait; ` Remove-Item vs_BuildTools.exe; USER ContainerUser