From e9956eb6398c77654ad1d1a8e20399de17eb1c1c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Tue, 7 Jul 2026 05:34:59 +0200 Subject: [PATCH 1/2] Add scheduled workflow to bump bundled Playwright (#9362) Dependabot discovers Microsoft.Playwright.MSTest.v4 and computes the 1.60->1.61 update, but drops the change at PR-assembly time for this package, so no PR is ever opened (unlike Aspire, which shares the same anchor pattern). Confirmed by running the real dependabot-updater-nuget container against the repo. Replace that one flow with a small scheduled workflow + script that looks up the latest stable release on nuget.org and rewrites both coupled values in Directory.Packages.props (the MicrosoftPlaywrightVersion property and the literal PackageVersion) so _ValidateBundledSdkFeatureVersions stays in sync, then opens a PR. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/scripts/update_bundled_playwright.py | 152 ++++++++++++++++++ .../workflows/update-bundled-playwright.yml | 91 +++++++++++ 2 files changed, 243 insertions(+) create mode 100644 .github/scripts/update_bundled_playwright.py create mode 100644 .github/workflows/update-bundled-playwright.yml diff --git a/.github/scripts/update_bundled_playwright.py b/.github/scripts/update_bundled_playwright.py new file mode 100644 index 0000000000..a012e13a23 --- /dev/null +++ b/.github/scripts/update_bundled_playwright.py @@ -0,0 +1,152 @@ +#!/usr/bin/env python3 +"""Bump the MSTest.Sdk-bundled Microsoft.Playwright.MSTest.v4 version. + +Dependabot cannot reliably open this bump on its own: it discovers the package +via the anchor in Directory.Packages.props and even computes the new version, +but drops the change at PR-assembly time for this specific package (see +https://github.com/microsoft/testfx/issues/9362). This script replaces that +one Dependabot flow. + +It looks up the latest STABLE Microsoft.Playwright.MSTest.v4 on nuget.org and, +when newer than what the repo bundles, rewrites BOTH coupled values in +Directory.Packages.props so they stay in sync (the _ValidateBundledSdkFeatureVersions +target fails the build otherwise): + + * the property (consumed by the build / baked into + the shipped SDK template), and + * the literal . + +Modes: + update Query nuget.org and rewrite Directory.Packages.props if a newer + stable version exists. Emits changed/old/new to $GITHUB_OUTPUT. + check Validate the two coupled values are present and in sync (used by the + workflow's pull_request self-test; no network, no writes). +""" +from __future__ import annotations + +import argparse +import json +import os +import re +import sys +import urllib.request +from pathlib import Path + +PACKAGE_ID = "Microsoft.Playwright.MSTest.v4" +FLAT_CONTAINER = ( + "https://api.nuget.org/v3-flatcontainer/" + "microsoft.playwright.mstest.v4/index.json" +) + +REPO_ROOT = Path(__file__).resolve().parents[2] +PROPS_PATH = REPO_ROOT / "Directory.Packages.props" + +PROPERTY_RE = re.compile( + r"()(?P[^<]+)()" +) +PACKAGE_VERSION_RE = re.compile( + r'([^\"]+)(\"\s*/>)" +) + + +def _stable_tuple(version: str) -> tuple[int, ...] | None: + """Return a comparable tuple for a stable version, or None for prereleases.""" + if "-" in version or "+" in version: + return None + parts = version.split(".") + if not all(p.isdigit() for p in parts): + return None + return tuple(int(p) for p in parts) + + +def latest_stable_version() -> str: + with urllib.request.urlopen(FLAT_CONTAINER, timeout=60) as response: + payload = json.load(response) + candidates = [] + for version in payload.get("versions", []): + key = _stable_tuple(version) + if key is not None: + candidates.append((key, version)) + if not candidates: + raise RuntimeError(f"No stable versions found for {PACKAGE_ID}.") + candidates.sort(key=lambda item: item[0]) + return candidates[-1][1] + + +def read_current_versions(text: str) -> tuple[str, str]: + prop_match = PROPERTY_RE.search(text) + pkg_match = PACKAGE_VERSION_RE.search(text) + if prop_match is None: + raise RuntimeError("Could not find in Directory.Packages.props.") + if pkg_match is None: + raise RuntimeError( + f'Could not find in Directory.Packages.props.' + ) + return prop_match.group("version"), pkg_match.group("version") + + +def set_github_output(**pairs: str) -> None: + output_path = os.environ.get("GITHUB_OUTPUT") + if not output_path: + return + with open(output_path, "a", encoding="utf-8") as handle: + for key, value in pairs.items(): + handle.write(f"{key}={value}\n") + + +def cmd_check() -> int: + text = PROPS_PATH.read_text(encoding="utf-8") + prop_version, pkg_version = read_current_versions(text) + if prop_version != pkg_version: + print( + "::error::MicrosoftPlaywrightVersion " + f"('{prop_version}') is out of sync with the {PACKAGE_ID} " + f"PackageVersion ('{pkg_version}')." + ) + return 1 + print(f"OK: bundled Playwright is {prop_version} (property and PackageVersion in sync).") + return 0 + + +def cmd_update() -> int: + text = PROPS_PATH.read_text(encoding="utf-8") + current_property, current_package = read_current_versions(text) + if current_property != current_package: + print( + "::error::Refusing to update: MicrosoftPlaywrightVersion " + f"('{current_property}') and the {PACKAGE_ID} PackageVersion " + f"('{current_package}') already disagree. Reconcile them first." + ) + return 1 + + current = current_property + latest = latest_stable_version() + print(f"Current bundled Playwright: {current}") + print(f"Latest stable on nuget.org: {latest}") + + if _stable_tuple(latest) <= _stable_tuple(current): + print("Already up to date; nothing to do.") + set_github_output(changed="false", old_version=current, new_version=current) + return 0 + + updated = PROPERTY_RE.sub(rf"\g<1>{latest}\g<3>", text, count=1) + updated = PACKAGE_VERSION_RE.sub(rf"\g<1>{latest}\g<3>", updated, count=1) + PROPS_PATH.write_text(updated, encoding="utf-8") + + print(f"Updated bundled Playwright {current} -> {latest}.") + set_github_output(changed="true", old_version=current, new_version=latest) + return 0 + + +def main() -> int: + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("mode", choices=["update", "check"]) + args = parser.parse_args() + if args.mode == "check": + return cmd_check() + return cmd_update() + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/.github/workflows/update-bundled-playwright.yml b/.github/workflows/update-bundled-playwright.yml new file mode 100644 index 0000000000..d55c0ff5ea --- /dev/null +++ b/.github/workflows/update-bundled-playwright.yml @@ -0,0 +1,91 @@ +name: Update bundled Playwright + +# Keeps the MSTest.Sdk-bundled Microsoft.Playwright.MSTest.v4 version current. +# +# Dependabot cannot reliably do this: it discovers the package via the anchor in +# Directory.Packages.props and even computes the new version, but drops the change +# at PR-assembly time for this specific package. See +# https://github.com/microsoft/testfx/issues/9362 for the full investigation. +# +# This workflow replaces that one Dependabot flow: +# - Scheduled / manual runs look up the latest STABLE release on nuget.org and, +# when newer, rewrite BOTH coupled values in Directory.Packages.props +# (the property and the literal PackageVersion, +# which _ValidateBundledSdkFeatureVersions requires to stay in sync) and open +# a PR. +# - Pull-request runs that touch the script or workflow only self-test the +# script (no network writes, no PR) to catch regressions. + +on: + schedule: + # Weekly, Tuesday 07:00 UTC (offset from other scheduled workflow slots). + - cron: '0 7 * * 2' + workflow_dispatch: + pull_request: + paths: + - '.github/scripts/update_bundled_playwright.py' + - '.github/workflows/update-bundled-playwright.yml' + +permissions: + contents: read + +# One in-flight update at a time so scheduled + manual runs don't race on the +# shared update-bundled-playwright/autoupdate branch. +concurrency: + group: update-bundled-playwright + cancel-in-progress: false + +jobs: + self-test: + name: Self-test script + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + - name: Setup Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: '3.12' + - name: Validate Directory.Packages.props is in sync + run: python .github/scripts/update_bundled_playwright.py check + + update: + name: Bump bundled Playwright + if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + steps: + - name: Checkout + uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4.3.1 + + - name: Setup Python + uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0 + with: + python-version: '3.12' + + - name: Compute and apply update + id: bump + run: python .github/scripts/update_bundled_playwright.py update + + - name: Create Pull Request + if: steps.bump.outputs.changed == 'true' + uses: peter-evans/create-pull-request@67ccf781d68cd99b580ae25a5c18a1cc84ffff1f # v7 + with: + commit-message: "Update bundled Playwright for .NET to ${{ steps.bump.outputs.new_version }}" + title: "Update bundled Playwright for .NET to ${{ steps.bump.outputs.new_version }}" + body: | + Bumps the MSTest.Sdk-bundled `Microsoft.Playwright.MSTest.v4` from + `${{ steps.bump.outputs.old_version }}` to `${{ steps.bump.outputs.new_version }}`. + + Updates both the `MicrosoftPlaywrightVersion` property and the matching + `Microsoft.Playwright.MSTest.v4` `PackageVersion` in `Directory.Packages.props` + so `_ValidateBundledSdkFeatureVersions` stays green. + + Opened by the `update-bundled-playwright` workflow because Dependabot + cannot reliably open this bump (see #9362). + branch: update-bundled-playwright/autoupdate + delete-branch: true + labels: dependencies From 918bbc5582aca707de576cbc3b05d7352ba74598 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Amaury=20Lev=C3=A9?= Date: Tue, 7 Jul 2026 05:48:09 +0200 Subject: [PATCH 2/2] Scope concurrency lock to the update job only The workflow-level concurrency group also gated the read-only pull_request self-test job. Move it to the update job so branch-race protection is kept without queuing PR self-tests behind the weekly/manual update runs. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> --- .github/workflows/update-bundled-playwright.yml | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/workflows/update-bundled-playwright.yml b/.github/workflows/update-bundled-playwright.yml index d55c0ff5ea..d4e502b3e4 100644 --- a/.github/workflows/update-bundled-playwright.yml +++ b/.github/workflows/update-bundled-playwright.yml @@ -29,12 +29,6 @@ on: permissions: contents: read -# One in-flight update at a time so scheduled + manual runs don't race on the -# shared update-bundled-playwright/autoupdate branch. -concurrency: - group: update-bundled-playwright - cancel-in-progress: false - jobs: self-test: name: Self-test script @@ -54,6 +48,12 @@ jobs: name: Bump bundled Playwright if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch' runs-on: ubuntu-latest + # One in-flight update at a time so scheduled + manual runs don't race on the + # shared update-bundled-playwright/autoupdate branch. Scoped to this job so + # read-only PR self-tests are never gated behind the update lock. + concurrency: + group: update-bundled-playwright + cancel-in-progress: false permissions: contents: write pull-requests: write