Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 85 additions & 0 deletions .claude/skills/playwright-triage/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
---
name: playwright-triage
description: Triage and fix one microsoft/playwright-dotnet issue, or do a maintainer's task, unattended inside the Issue triage GitHub Actions workflow (.github/workflows/triage.yml), which a needs-triage, needs-triage-mac or needs-triage-win label starts, or a maintainer starts by hand with an issue, a task or both. Reproduces the issue on the runner, then leaves a verified fix proposal for the workflow to open as a draft PR, an investigation note for it to post on the issue, or an upstream bug report for a maintainer to file in microsoft/playwright. Only for that workflow.
---

# Playwright .NET Issue Triage

You run unattended in the [Issue triage](../../../.github/workflows/triage.yml) workflow of
`microsoft/playwright-dotnet`. First read the shared guide, `$AGENT_GUIDES_DIR/triage.md`, a copy of
[microsoft/playwright/.github/actions/run-agent/guides/triage.md](https://github.com/microsoft/playwright/blob/main/.github/actions/run-agent/guides/triage.md),
and then [CLAUDE.md](../../../CLAUDE.md), both in full. The guide says how to triage an issue in any
Playwright repo. CLAUDE.md says how to build, test and format this one, and where a regression test
goes. This file covers what neither has: the checkouts, how to run a published version, and the
outcome that this repo adds, an upstream bug report.

## The checkouts

`$REPO_DIR` is the checkout of `microsoft/playwright-dotnet` that the guide describes. The driver is
downloaded (`./build.sh --download-driver`), the solution is built (`dotnet build ./src`), and
Chromium, Firefox and WebKit are installed
(`pwsh src/Playwright/bin/Debug/netstandard2.0/playwright.ps1 install --with-deps`). It is the
working directory, and the checkout that the guides refer to. The clone is shallow: run
`git fetch --unshallow --tags origin` in it before you bisect or use `git tag --contains`. .NET SDK
8 and 10 are installed, as in CI. Tests run with `-f net8.0`.

`$PLAYWRIGHT_DIR` is a shallow clone of `microsoft/playwright` at `main`. The .NET client is a port
of the JavaScript client in `packages/playwright-core/src/client/`. The API docs are in
`docs/src/api/`, the API generator is `utils/doclint/generateDotnetApi.js`, and the upstream tests
are in `tests/page/` and `tests/library/`. The clone is not at the commit of the driver:
`<DriverVersion>` in `src/Common/Version.props` names that version, `main` is close to it, and a
release has the tag `v<DriverVersion>`. The clone is not built. Before you run an upstream test in
it, run `npm ci`, `npm run build` and `npx playwright install` there. Then `npm run ctest <spec>`,
`npm run ftest <spec>` and `npm run wtest <spec>` run one spec file on Chromium, Firefox and WebKit.

## Published versions

To run a published version, for example to bisect, create
`$SCRATCH_DIR/playwright-dotnet-<n>/<version>/` and make a console project there:

```bash
dotnet new console -n repro -f net8.0 -o . && dotnet add package Microsoft.Playwright --version <version>
dotnet build && pwsh bin/Debug/net8.0/playwright.ps1 install chromium
dotnet run
```

Put the repro in `Program.cs` as top-level statements, as `src/Playwright.Examples/Program.cs`
does. `playwright.ps1 install` fetches the browsers of that version. Match the change to commits in
`$REPO_DIR` with `git log v<good>..v<bad> -- src/Playwright/`, and to the JavaScript client in
`$PLAYWRIGHT_DIR` with the same tags.

## Upstream bugs

The .NET client talks to the Node driver that `microsoft/playwright` publishes, and many bugs that
are reported here are in the driver. That fix lands in `microsoft/playwright`. The family is one
team, so this is routing, not a request to the reporter. Find the owner with the same calls in both
clients: write the repro as a .NET test in `$REPO_DIR` first, then write the same calls as an
upstream test in `$PLAYWRIGHT_DIR` and run it there. When the Node test fails the same way, the root
cause is upstream. When only the .NET test fails, the bug is in the port: compare the method in
`src/Playwright/Core/` with the same method in the JavaScript client, and fix it here.

The interfaces in `src/Playwright/API/Generated/` and the types in
`src/Playwright/Transport/Protocol/Generated/` are generated by `utils/doclint/generateDotnetApi.js`
and `utils/generate_dotnet_channels.js` from the upstream docs and protocol. Never edit them by
hand. A missing method, option or type is an upstream change, in `docs/src/api/` (the `langs`
annotations decide what .NET gets) or in the generator. .NET-only overloads are hand-written in
`src/Playwright/API/Supplements/`, and the implementations are in `src/Playwright/Core/`.

For an upstream root cause, create `$OUTPUT_DIR/upstream/` with:

- `report.md`: the report for a maintainer to file in `microsoft/playwright`, for an engineer who
did not read this issue: what fails, expected versus actual, the driver version and the browsers,
and the condensed Node test from `$PLAYWRIGHT_DIR` as the repro. Say that .NET shows the same.
- `report.json`: `{"title": "..."}`, at most 256 characters.

The comment job appends the report to your comment, for a maintainer to file. The bot never files
it. Still write the comment as the note, without the report in it. Write `report.json` last, when
`report.md` and the comment are done. Like `proposal.json`, it marks the report as ready.

## PRs from third parties

Do not run a PR that changes a `.csproj`, `.props` or `.sln` file, `nuget.config`, `build.sh`,
`src/tools/Playwright.Tooling/` (the driver download), `src/Playwright/build/` (MSBuild targets that
run on every build), `.github/`, `.azure-pipelines/`, `utils/`, `package.json`, `package-lock.json`,
or the generated files under `src/Playwright/API/Generated/` and
`src/Playwright/Transport/Protocol/Generated/`.
36 changes: 36 additions & 0 deletions .github/actions/setup-triage-runner/action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: 'Set up the triage runner'
description: >
Prepares the environment that the triage and revision agents work in: the checkout built as CI
builds it, with the browsers and the Copilot CLI installed, and a checkout of microsoft/playwright
to read the client that this repo ports. Exports PLAYWRIGHT_DIR, the path of that checkout, and
CI_ENV_EXEC, the wrapper from microsoft/playwright that runs a command under xvfb on Linux and at
normal I/O priority on Windows.
runs:
using: composite
steps:
- uses: microsoft/playwright/.github/actions/ci-env@main
- uses: actions/setup-dotnet@a98b56852c35b8e3190ac28c8c2271da59106c68 # v6.0.0
with:
dotnet-version: |
8.0.x
10.0.x
- uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version: '24'
- name: Download the driver and build
shell: bash
run: |
./build.sh --download-driver
dotnet build ./src
- name: Install the browsers
shell: bash
run: pwsh src/Playwright/bin/Debug/netstandard2.0/playwright.ps1 install --with-deps
- name: Check out microsoft/playwright
shell: bash
run: |
dir="${RUNNER_TEMP//\\//}/playwright"
git clone --quiet --depth 1 https://github.com/microsoft/playwright "$dir"
echo "PLAYWRIGHT_DIR=$dir" >> "$GITHUB_ENV"
- name: Install the Copilot CLI
shell: bash
run: npm install -g @github/copilot
132 changes: 132 additions & 0 deletions .github/workflows/revision.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
# Copilot addresses the maintainers' review comments on a draft PR that the triage workflow opened,
# without write credentials. A fresh job pushes the new commit to the PR's branch using the app, so
# CI runs on it. The steps that are the same for every Playwright repo are actions in
# microsoft/playwright, and the shared guide they carry says how to revise; the prompt below has
# what is specific to this repo.
name: PR revision

on:
# Apply needs-revision, needs-revision-mac or needs-revision-win; re-apply it to trigger another pass.
# pull_request_target always runs this file from main; the jobs only accept the triage app's own PRs.
pull_request_target:
types: [labeled]

permissions: {}

jobs:
prepare:
name: Collect the maintainers' comments
if: >-
github.repository == 'microsoft/playwright-dotnet' &&
contains(fromJSON('["needs-revision","needs-revision-mac","needs-revision-win"]'), github.event.label.name) &&
github.event.pull_request.state == 'open' &&
github.event.pull_request.user.login == 'microsoft-playwright-automation[bot]' &&
github.event.pull_request.head.repo.full_name == github.repository
runs-on: ubuntu-latest
permissions:
pull-requests: write
env:
PLATFORM: ${{ fromJSON('{"needs-revision":"linux","needs-revision-mac":"mac","needs-revision-win":"windows"}')[github.event.label.name] }}
outputs:
head: ${{ steps.collect.outputs.head }}
base: ${{ steps.collect.outputs.base }}
branch: ${{ steps.collect.outputs.branch }}
platform: ${{ env.PLATFORM }}
steps:
- name: Collect the PR and the maintainers' comments
id: collect
uses: microsoft/playwright/.github/actions/collect-review@main
with:
pull-request: ${{ github.event.pull_request.number }}
- name: Post the run link
env:
GH_TOKEN: ${{ github.token }}
PR: ${{ github.event.pull_request.number }}
PLATFORM_NAME: ${{ fromJSON('{"linux":"Linux","mac":"macOS","windows":"Windows"}')[env.PLATFORM] }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
body=$(printf "Hi, I'm the Playwright bot. I started to address the review comments on %s: [agent run](%s)\n<!-- playwright-revision-started -->" "$PLATFORM_NAME" "$RUN_URL")
gh pr comment "$PR" --body "$body" || echo "::warning::Could not post the run link"

revise:
name: Revise PR ${{ github.event.pull_request.number }} (${{ needs.prepare.outputs.platform }})
needs: prepare
runs-on: ${{ fromJSON('{"linux":"playwright-x64-ubuntu24-64-core","mac":"macos-15-xlarge","windows":"playwright-x64-server2025-64-core"}')[needs.prepare.outputs.platform] }}
timeout-minutes: 240
concurrency:
group: revision-${{ github.event.pull_request.number }}
cancel-in-progress: false
defaults:
run:
shell: bash
env:
PR: ${{ github.event.pull_request.number }}
HEAD_SHA: ${{ needs.prepare.outputs.head }}
BASE_SHA: ${{ needs.prepare.outputs.base }}
TRIAGE_PLATFORM: ${{ needs.prepare.outputs.platform }}
permissions:
copilot-requests: write
outputs:
has_revision: ${{ steps.agent.outputs.has-result }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.prepare.outputs.head }}
persist-credentials: false
- name: Fetch the base and record the directories
run: |
git fetch --no-tags --depth=1 origin "$BASE_SHA"
echo "OUTPUT_DIR=${RUNNER_TEMP//\\//}/revision" >> "$GITHUB_ENV"
echo "REVIEW_DIR=${RUNNER_TEMP//\\//}/review" >> "$GITHUB_ENV"
echo "SCRATCH_DIR=${RUNNER_TEMP//\\//}/scratch" >> "$GITHUB_ENV"
echo "REPO_DIR=${GITHUB_WORKSPACE//\\//}" >> "$GITHUB_ENV"
mkdir -p "${RUNNER_TEMP//\\//}/revision" "${RUNNER_TEMP//\\//}/scratch"
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: review
path: ${{ runner.temp }}/review
- name: Set up the runner
uses: ./.github/actions/setup-triage-runner
- name: Revise with Copilot CLI
id: agent
# No GitHub tools and no URL access: the agent reads the PR only from the review the prepare job filtered.
uses: microsoft/playwright/.github/actions/run-agent@main
with:
prompt: |
Follow the shared guide revision.md and this repo's CLAUDE.md. Read both in full before starting. $PLAYWRIGHT_DIR is a shallow clone of microsoft/playwright at main, for reading the JavaScript client that this repo ports.

Pull request: https://github.com/microsoft/playwright-dotnet/pull/${{ github.event.pull_request.number }}
output-dir: ${{ env.OUTPUT_DIR }}
result-file: commit/commit.json
artifact-name: revision
exec: ${{ env.CI_ENV_EXEC }}

push:
name: Push the revision
needs: [prepare, revise]
# commit.json is written last, so a finished revision is pushed even when the job failed later.
if: ${{ !cancelled() && needs.revise.outputs.has_revision == 'true' }}
runs-on: ubuntu-latest
steps:
- uses: actions/create-github-app-token@64ab729a6294000f5aa4a4bc0b73d1f32bc45510 # v3.3.0
id: app-token
with:
app-id: ${{ vars.PLAYWRIGHT_APP_ID }}
private-key: ${{ secrets.PLAYWRIGHT_PRIVATE_KEY }}
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ needs.prepare.outputs.head }}
token: ${{ steps.app-token.outputs.token }}
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: revision
path: ${{ runner.temp }}/revision
# Never execute code or scripts from the proposed changes in the app job.
- name: Validate and push
uses: microsoft/playwright/.github/actions/publish-patch@main
with:
token: ${{ steps.app-token.outputs.token }}
dir: ${{ runner.temp }}/revision/commit
patch: revision.patch
metadata: commit.json
branch: ${{ needs.prepare.outputs.branch }}
Loading
Loading