Skip to content

Commit edf9e4e

Browse files
committed
chore: roll bot
1 parent 255d3b4 commit edf9e4e

8 files changed

Lines changed: 364 additions & 40 deletions

File tree

‎.claude/skills/playwright-roll/SKILL.md‎

Lines changed: 23 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: playwright-roll
3-
description: Roll Playwright Python to a new microsoft/playwright commit. Pins the commit in DRIVER_VERSION, builds the driver and generates the API from .upstream/, and ports the upstream diff of the client and the tests line by line.
3+
description: Roll Playwright Python to a new microsoft/playwright commit. Pins the commit in DRIVER_VERSION, generates the API and builds the driver from .upstream/, and ports the upstream diff of the client and the tests line by line.
44
---
55

66
# Rolling Playwright Python
@@ -39,14 +39,7 @@ new=$(cat DRIVER_VERSION)
3939
driver reports the version of `packages/playwright-core/package.json` at that commit:
4040
`1.66.0` on a release tag, `1.67.0-next` on `main`.
4141

42-
## 2. Build
43-
44-
```sh
45-
python -m build --wheel # the driver from .upstream/, and the wheel
46-
playwright install chromium
47-
```
48-
49-
## 3. Generate
42+
## 2. Generate
5043

5144
```sh
5245
./scripts/update_api.sh
@@ -60,7 +53,8 @@ The API generator (`scripts/codegen/generate_api.js`) reads the docs in
6053
error in `_generated.py`, and a new parameter in the protocol spec is a mypy error at
6154
the channel call in `_impl/`: each `ChannelOwner` subclass declares its channel type
6255
(`_channel: "PageChannel"`), so mypy checks every call. Step 4 fixes them. Until then,
63-
the client may fail to talk to the new driver.
56+
the client may fail to talk to the new driver. Generating needs no driver: it reads
57+
only `.upstream/`.
6458

6559
To see what changed in the public API:
6660

@@ -69,6 +63,13 @@ git show HEAD:playwright/async_api/_generated.py > /tmp/old.py
6963
python scripts/codegen/compare_api.py /tmp/old.py playwright/async_api/_generated.py --show all
7064
```
7165

66+
## 3. Build
67+
68+
```sh
69+
python -m build --wheel # the driver from .upstream/, and the wheel with the files of step 2
70+
playwright install chromium
71+
```
72+
7273
## 4. Port
7374

7475
```sh
@@ -78,7 +79,7 @@ python scripts/upstream.py changes
7879
`changes` lists each upstream file that changed between `$old` and `$new`, in groups,
7980
with the Python file to port it to. The files that upstream generates from the
8081
protocol spec (`channels.d.ts`, `validator.ts`) are in the "Generated" group with the
81-
docs and the spec: step 3 regenerated their Python counterparts. A spec file that
82+
docs and the spec: step 2 regenerated their Python counterparts. A spec file that
8283
`tests/upstream_not_ported.txt` lists shows its reason instead of a Python file. For
8384
each other file, read the diff and port each hunk, in order:
8485

@@ -131,14 +132,18 @@ Then run `mypy playwright` until it is clean.
131132
## 5. Check
132133

133134
```sh
134-
pre-commit run --all-files
135-
python scripts/check_test_registry.py
136135
./test.sh
137-
./scripts/update_api.sh && git status # no new changes: the generated files come from .upstream/
138136
playwright install chromium firefox webkit
139137
python scripts/update_versions.py # the browser versions in README.md
138+
./scripts/lint.sh
140139
```
141140

141+
`./scripts/lint.sh` is the last check. It checks that `.upstream/` is at the pin with no
142+
local changes, that the generated files are what `.upstream/` generates, `pre-commit`
143+
on every file (the formatters, flake8, mypy, pyright) and the test registry. It stops at
144+
the first check that fails: fix the cause and run it again. The roll is not done until
145+
it passes. The roll workflow runs it again after you.
146+
142147
`./test.sh` runs every test, on many workers. For a failure in a file that the roll did
143148
not change:
144149

@@ -150,9 +155,10 @@ not change:
150155
in the PR.
151156

152157
Title the PR `chore: roll Playwright to <version> (<first 10 characters of $new>)`,
153-
with the version from step 1. List what you ported and what you could not finish, and
154-
put `git -C .upstream log --oneline --reverse "$old..$new"` of the changed paths in a
155-
`<details>` block.
158+
with the version from step 1. In the description, list what you ported and what you
159+
could not finish, and the upstream commits that the roll brings in, in a `<details>`
160+
block: `python scripts/upstream.py log` prints them, with links to microsoft/playwright.
161+
The roll workflow adds that list itself.
156162

157163
## Reference: the docs decide the public API
158164

‎.github/workflows/ci.yml‎

Lines changed: 3 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -39,13 +39,9 @@ jobs:
3939
- name: Get Playwright
4040
run: python scripts/upstream.py checkout
4141
- name: Lint
42-
run: pre-commit run --show-diff-on-failure --color=always --all-files
43-
- name: Check the tests against the upstream test registry
44-
run: python scripts/check_test_registry.py --report
45-
- name: Check that the generated files are up to date
46-
run: |
47-
./scripts/update_api.sh
48-
git diff --exit-code
42+
run: ./scripts/lint.sh
43+
env:
44+
PRE_COMMIT_COLOR: always
4945

5046
build:
5147
name: Build

‎.github/workflows/roll.yml‎

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
# Rolls Playwright to a new microsoft/playwright commit (the tip of main unless
2+
# told otherwise) and opens a pull request. Copilot CLI ports the upstream changes
3+
# with the playwright-roll skill. The pull request comes from the
4+
# microsoft-playwright-automation app, so CI runs on it: this needs the
5+
# PLAYWRIGHT_APP_CLIENT_ID variable and the PLAYWRIGHT_PRIVATE_KEY secret.
6+
# Upstream counterpart: microsoft/playwright.dev .github/workflows/roll-next.yml.
7+
name: Roll to latest Playwright
8+
9+
on:
10+
workflow_dispatch:
11+
inputs:
12+
ref:
13+
description: "microsoft/playwright branch, tag or commit to roll to"
14+
default: "main"
15+
max_ai_credits:
16+
description: "AI credit limit for the Copilot session"
17+
default: "3000"
18+
19+
permissions: {}
20+
21+
concurrency:
22+
group: ${{ github.workflow }}
23+
cancel-in-progress: false
24+
25+
jobs:
26+
versions:
27+
name: Find the commit
28+
runs-on: ubuntu-24.04
29+
if: github.repository == 'microsoft/playwright-python'
30+
permissions:
31+
contents: read
32+
outputs:
33+
old_commit: ${{ steps.versions.outputs.old_commit }}
34+
new_commit: ${{ steps.versions.outputs.new_commit }}
35+
new_short: ${{ steps.versions.outputs.new_short }}
36+
steps:
37+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
38+
- name: Read the pinned and the new commit
39+
id: versions
40+
env:
41+
REF: ${{ inputs.ref }}
42+
run: |
43+
old_commit="$(cat DRIVER_VERSION)"
44+
new_commit="$(python3 scripts/upstream.py resolve "$REF")"
45+
echo "Pinned ${old_commit}, ${REF} is ${new_commit}" >> "$GITHUB_STEP_SUMMARY"
46+
{
47+
echo "old_commit=${old_commit}"
48+
echo "new_commit=${new_commit}"
49+
echo "new_short=${new_commit:0:10}"
50+
} >> "$GITHUB_OUTPUT"
51+
52+
roll:
53+
name: Roll
54+
needs: versions
55+
if: needs.versions.outputs.new_commit != needs.versions.outputs.old_commit
56+
# The runner of CI: the skill runs ./test.sh, both suites on all its cores.
57+
runs-on: playwright-x64-ubuntu24-64-core
58+
timeout-minutes: 240
59+
permissions:
60+
contents: read
61+
copilot-requests: write
62+
outputs:
63+
has_changes: ${{ steps.save.outputs.has_changes }}
64+
version: ${{ steps.save.outputs.version }}
65+
complete: ${{ steps.agent.outcome == 'success' && steps.check.outcome == 'success' }}
66+
env:
67+
OLD_COMMIT: ${{ needs.versions.outputs.old_commit }}
68+
NEW_COMMIT: ${{ needs.versions.outputs.new_commit }}
69+
steps:
70+
# No credentials in the checkout: the agent must not push.
71+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
72+
with:
73+
persist-credentials: false
74+
# The runner context is not available in the env of the job.
75+
- name: Set the output directory
76+
run: echo "OUTPUT_DIR=$RUNNER_TEMP/roll" >> "$GITHUB_ENV"
77+
- name: Set up Python
78+
uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0
79+
with:
80+
python-version: "3.10"
81+
- name: Set up Node.js
82+
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
83+
with:
84+
node-version: '24'
85+
- name: Install dependencies
86+
run: |
87+
python -m pip install --upgrade pip
88+
pip install -r local-requirements.txt
89+
pip install -r requirements.txt
90+
pip install -e .
91+
npm install -g @github/copilot
92+
# Step 5 of the skill runs a failing test against the old driver: build its
93+
# bundle into driver/ before the pin moves .upstream/.
94+
- name: Build the old driver
95+
run: |
96+
python scripts/upstream.py checkout
97+
python scripts/build_driver.py linux
98+
# Steps 1 to 3 of the playwright-roll skill.
99+
- name: Pin
100+
run: |
101+
python scripts/upstream.py pin "$NEW_COMMIT"
102+
python scripts/update_node_version.py
103+
- name: Generate the API, the protocol files and the test registry
104+
run: ./scripts/update_api.sh
105+
- name: Build the driver and the wheel
106+
run: python -m build --wheel
107+
- name: Install browsers
108+
run: python -m playwright install --with-deps chromium firefox webkit
109+
- name: Roll with Copilot CLI
110+
id: agent
111+
# A partial roll still opens a pull request, as a draft.
112+
continue-on-error: true
113+
env:
114+
COPILOT_GITHUB_TOKEN: ${{ github.token }}
115+
MAX_AI_CREDITS: ${{ inputs.max_ai_credits }}
116+
run: |
117+
mkdir -p "$OUTPUT_DIR"
118+
PROMPT=$(cat <<EOF
119+
Roll Playwright Python from microsoft/playwright $OLD_COMMIT to $NEW_COMMIT. Follow the playwright-roll skill.
120+
121+
The workflow already did steps 1 (pin), 2 (generate) and 3 (build) of the skill: DRIVER_VERSION is
122+
$NEW_COMMIT, .upstream/ is checked out at it, NODE_VERSION is the current LTS, ./scripts/update_api.sh has
123+
generated the files from .upstream/, the driver is built, and Chromium, Firefox and WebKit are installed. The
124+
old pin is $OLD_COMMIT (DRIVER_VERSION at HEAD); the bundle of its driver is in driver/, for the comparison of
125+
step 5. Start with mypy playwright and the API comparison of step 2, then step 4.
126+
127+
Do not commit, push or run gh. A later step of the workflow commits your changes and opens the pull request.
128+
129+
When you finish, write the pull request description that step 5 of the skill describes to
130+
$OUTPUT_DIR/pr-body.md, without the list of upstream commits: the workflow sets the title and adds the list.
131+
EOF
132+
)
133+
copilot \
134+
--allow-all-tools \
135+
--allow-all-paths \
136+
--no-ask-user \
137+
--share="$OUTPUT_DIR/copilot-session.md" \
138+
--model gpt-6.1-sol \
139+
--max-ai-credits "$MAX_AI_CREDITS" \
140+
-p "$PROMPT"
141+
# The agent runs scripts/lint.sh last (step 5 of the skill); a roll that does
142+
# not pass it again here opens as a draft.
143+
- name: Check the roll
144+
id: check
145+
continue-on-error: true
146+
run: |
147+
test "$(cat DRIVER_VERSION)" = "$NEW_COMMIT"
148+
./scripts/lint.sh
149+
- name: Save the changes
150+
id: save
151+
run: |
152+
mkdir -p "$OUTPUT_DIR"
153+
git add -A
154+
# Against the checked-out commit, in case the agent committed anyway.
155+
git diff --cached --binary "$GITHUB_SHA" > "$OUTPUT_DIR/roll.patch"
156+
if [[ -s "$OUTPUT_DIR/roll.patch" ]]; then
157+
echo "has_changes=true" >> "$GITHUB_OUTPUT"
158+
else
159+
echo "has_changes=false" >> "$GITHUB_OUTPUT"
160+
fi
161+
echo "version=$(node -p "require('./.upstream/packages/playwright-core/package.json').version")" >> "$GITHUB_OUTPUT"
162+
python scripts/upstream.py log "$OLD_COMMIT" > "$OUTPUT_DIR/commits.md"
163+
{
164+
echo '```'
165+
git diff --cached --stat "$GITHUB_SHA"
166+
echo '```'
167+
} >> "$GITHUB_STEP_SUMMARY"
168+
- name: Upload the roll
169+
if: always()
170+
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
171+
with:
172+
name: roll
173+
path: ${{ env.OUTPUT_DIR }}
174+
if-no-files-found: warn
175+
176+
publish:
177+
name: Open the pull request
178+
needs: [versions, roll]
179+
if: needs.roll.outputs.has_changes == 'true'
180+
runs-on: ubuntu-24.04
181+
permissions:
182+
contents: read
183+
env:
184+
TITLE: "chore: roll Playwright to ${{ needs.roll.outputs.version }} (${{ needs.versions.outputs.new_short }})"
185+
BRANCH: roll/${{ needs.versions.outputs.new_short }}
186+
steps:
187+
- uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
188+
id: app-token
189+
with:
190+
client-id: ${{ vars.PLAYWRIGHT_APP_CLIENT_ID }}
191+
private-key: ${{ secrets.PLAYWRIGHT_PRIVATE_KEY }}
192+
# Pushes as the app, so CI runs on the pull request.
193+
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
194+
with:
195+
token: ${{ steps.app-token.outputs.token }}
196+
- uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
197+
with:
198+
name: roll
199+
path: ${{ runner.temp }}/roll
200+
- name: Push the branch
201+
run: |
202+
git apply --index "$RUNNER_TEMP/roll/roll.patch"
203+
# The app can change workflows; the roll must not.
204+
if git diff --cached --name-only | grep '^\.github/'; then
205+
echo "The roll changes files in .github/" >&2
206+
exit 1
207+
fi
208+
git config user.name "microsoft-playwright-automation[bot]"
209+
git config user.email "203992400+microsoft-playwright-automation[bot]@users.noreply.github.com"
210+
git checkout -b "$BRANCH"
211+
git commit -m "$TITLE"
212+
git push --force origin "$BRANCH"
213+
- name: Create the pull request
214+
uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
215+
env:
216+
BASE: ${{ github.ref_name }}
217+
COMPLETE: ${{ needs.roll.outputs.complete }}
218+
COMPARE_URL: https://github.com/microsoft/playwright/compare/${{ needs.versions.outputs.old_commit }}...${{ needs.versions.outputs.new_commit }}
219+
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
220+
with:
221+
github-token: ${{ steps.app-token.outputs.token }}
222+
script: |
223+
const fs = require('fs');
224+
const { BASE, BRANCH, TITLE, COMPLETE, COMPARE_URL, RUN_URL, RUNNER_TEMP } = process.env;
225+
const read = name => {
226+
const file = `${RUNNER_TEMP}/roll/${name}`;
227+
return fs.existsSync(file) ? fs.readFileSync(file, 'utf8').trim() : '';
228+
};
229+
const parts = [read('pr-body.md') || 'The agent wrote no description.'];
230+
if (COMPLETE !== 'true')
231+
parts.push(`**The roll is not complete:** the agent or the checks failed. See the [roll run](${RUN_URL}).`);
232+
// A description has at most 65536 characters: a long range keeps its first commits.
233+
let commits = read('commits.md').split('\n').filter(Boolean);
234+
const total = commits.length;
235+
while (commits.join('\n').length > 50000)
236+
commits = commits.slice(0, Math.floor(commits.length * 0.9));
237+
if (commits.length < total)
238+
commits.push(`- … and ${total - commits.length} more: ${COMPARE_URL}`);
239+
parts.push(`<details>\n<summary>Upstream commits (${total})</summary>\n\n${commits.join('\n')}\n\n</details>`);
240+
parts.push(`Upstream changes: ${COMPARE_URL}`);
241+
parts.push(`<sub>Rolled by the [roll workflow](${RUN_URL}).</sub>`);
242+
const body = parts.join('\n\n');
243+
const { owner, repo } = context.repo;
244+
const { data: open } = await github.rest.pulls.list({ owner, repo, head: `${owner}:${BRANCH}`, state: 'open' });
245+
if (open.length) {
246+
await github.rest.pulls.update({ owner, repo, pull_number: open[0].number, body });
247+
core.info(`Updated ${open[0].html_url}`);
248+
return;
249+
}
250+
const { data: pr } = await github.rest.pulls.create({
251+
owner,
252+
repo,
253+
head: BRANCH,
254+
base: BASE,
255+
title: TITLE,
256+
body,
257+
draft: COMPLETE !== 'true',
258+
});
259+
core.info(`Opened ${pr.html_url}`);

‎AGENTS.md‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ Run the last two again when `DRIVER_VERSION` changes. If the system lacks `pytho
4343
## Common commands
4444

4545
- Regenerate the generated files: `./scripts/update_api.sh` (runs codegen + pre-commit on the generated files).
46-
- Lint everything: `pre-commit run --all-files` (what the Lint job of CI runs; the checks are not a git hook, do not `pre-commit install`).
46+
- Lint everything: `./scripts/lint.sh`, what the Lint job of CI runs: `.upstream/` at the pin, the generated files up to date, `pre-commit` on every file (the formatters, flake8, mypy, pyright), the test registry. The checks are not a git hook; do not `pre-commit install`.
4747
- Type-check: `mypy playwright`.
4848
- Run tests: `pytest tests/sync tests/async --browser chromium [-k name]`, or `./test.sh [browser]` (both suites, in parallel, under xvfb). Browsers are installed via `playwright install chromium` (do **not** use `--with-deps`, which requires sudo).
4949

@@ -92,7 +92,7 @@ The steps are pin, build, generate, port and check (also in `ROLLING.md`). The p
9292

9393
## Commit Convention
9494

95-
Before committing, run `pre-commit run --all-files` (the Lint job of CI) and fix errors.
95+
Before committing, run `./scripts/lint.sh` (the Lint job of CI) and fix errors.
9696

9797
Semantic commit messages: `label(scope): description`
9898

0 commit comments

Comments
 (0)