|
| 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}`); |
0 commit comments