Patchright Release Build and Publish #38
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Patchright Release Build and Publish | |
| on: | |
| # enabling manual trigger | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: 'Playwright Version (e.g. v1.58.0 or 1.58.0)' | |
| default: '' | |
| patchright_release: | |
| description: 'Patchright Release Version (e.g. v1.58.0 or 1.58.0) on npm' | |
| default: '' | |
| # running every hour | |
| schedule: | |
| - cron: '0 * * * *' | |
| env: | |
| REPO: ${{ github.repository }} | |
| GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| patchright_release: ${{ github.event.inputs.patchright_release || '' }} | |
| jobs: | |
| check-release-version: | |
| name: Check Release Version | |
| runs-on: ubuntu-latest | |
| outputs: | |
| proceed: ${{ steps.version_check.outputs.proceed == 'true' && (github.event_name != 'schedule' || steps.existing_failure_issue.outputs.skip_due_to_existing_issue != 'true') }} | |
| playwright_version: ${{ steps.version_check.outputs.playwright_version }} | |
| previous_playwright_version: ${{ steps.version_check.outputs.previous_playwright_version }} | |
| steps: | |
| - name: Checkout Repository | |
| uses: actions/checkout@v6 | |
| - name: Check Release Version | |
| id: version_check | |
| run: | | |
| if [ -n "${{ github.event.inputs.version }}" ]; then | |
| raw_version="${{ github.event.inputs.version }}" | |
| normalized_version="v${raw_version#v}" | |
| response=$(curl --silent "https://api.github.com/repos/${REPO}/releases/latest") | |
| previous_playwright_version=$(echo "$response" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/') | |
| echo "proceed=true" >>$GITHUB_OUTPUT | |
| echo "playwright_version=${normalized_version}" >>$GITHUB_OUTPUT | |
| echo "previous_playwright_version=${previous_playwright_version:-v0.0.0}" >>$GITHUB_OUTPUT | |
| echo "playwright_version=${normalized_version}" >> $GITHUB_ENV | |
| elif [ -n "${{ github.event.inputs.patchright_release }}" ]; then | |
| raw_patchright_release="${{ github.event.inputs.patchright_release }}" | |
| normalized_patchright_release="${raw_patchright_release#v}" | |
| response=$(curl --silent "https://api.github.com/repos/microsoft/playwright/releases/latest") | |
| playwright_version=$(echo "$response" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/') | |
| response=$(curl --silent "https://api.github.com/repos/${REPO}/releases/latest") | |
| previous_playwright_version=$(echo "$response" | grep '"tag_name"' | sed -E 's/.*"([^"]+)".*/\1/') | |
| echo "proceed=true" >>$GITHUB_OUTPUT | |
| echo "playwright_version=${playwright_version}" >>$GITHUB_OUTPUT | |
| echo "previous_playwright_version=${previous_playwright_version:-v0.0.0}" >>$GITHUB_OUTPUT | |
| echo "playwright_version=${playwright_version}" >> $GITHUB_ENV | |
| echo "patchright_release=${normalized_patchright_release}" >> $GITHUB_ENV | |
| else | |
| chmod +x utils/release_version_check.sh | |
| utils/release_version_check.sh | |
| fi | |
| - name: Check Existing Failure Issue For Scheduled Run | |
| id: existing_failure_issue | |
| if: github.event_name == 'schedule' && steps.version_check.outputs.proceed == 'true' | |
| uses: actions/github-script@v8 | |
| env: | |
| TARGET_VERSION: ${{ steps.version_check.outputs.playwright_version }} | |
| with: | |
| script: | | |
| const rawTargetVersion = process.env.TARGET_VERSION || 'unknown'; | |
| const normalizedTargetVersion = rawTargetVersion.startsWith('v') ? rawTargetVersion.slice(1) : rawTargetVersion; | |
| const targetTitle = `Release Failed: v${normalizedTargetVersion} - Tests Failed`; | |
| const issues = await github.paginate(github.rest.issues.listForRepo, { | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| state: 'open', | |
| labels: 'release-failed,upstream-break', | |
| per_page: 100, | |
| }); | |
| const existingIssue = issues.find((issue) => | |
| !issue.pull_request && issue.title === targetTitle && issue.user?.login === 'github-actions[bot]' | |
| ); | |
| core.setOutput('skip_due_to_existing_issue', existingIssue ? 'true' : 'false'); | |
| check-patch-impact: | |
| name: Check Patch Impact | |
| needs: [check-release-version, run-patchright-tests] | |
| if: always() && needs.check-release-version.outputs.proceed == 'true' | |
| permissions: | |
| contents: read | |
| issues: write | |
| uses: ./.github/workflows/check_patch_impact.yml | |
| with: | |
| old_version: ${{ needs.check-release-version.outputs.previous_playwright_version }} | |
| new_version: ${{ needs.check-release-version.outputs.playwright_version }} | |
| create_issue: false | |
| run-patchright-tests: | |
| name: Run Patchright Tests | |
| needs: check-release-version | |
| if: needs.check-release-version.outputs.proceed == 'true' | |
| uses: ./.github/workflows/patchright_tests.yml | |
| with: | |
| playwright_version: ${{ needs.check-release-version.outputs.playwright_version }} | |
| notify-test-failure: | |
| name: Open Issue On Test Failure | |
| needs: [check-release-version, run-patchright-tests, check-patch-impact] | |
| if: always() && needs.check-release-version.outputs.proceed == 'true' && (needs.run-patchright-tests.outputs.test_outcome == 'failure' || needs.run-patchright-tests.result == 'failure') | |
| runs-on: ubuntu-latest | |
| permissions: | |
| actions: read | |
| contents: write | |
| issues: write | |
| pull-requests: write | |
| steps: | |
| - name: Download Patch Impact Report Artifact | |
| continue-on-error: true | |
| uses: actions/download-artifact@v6 | |
| with: | |
| name: report.json | |
| path: artifacts | |
| - name: Open Test Failure Issue | |
| uses: actions/github-script@v8 | |
| env: | |
| OLD_VERSION: ${{ needs.check-release-version.outputs.previous_playwright_version }} | |
| NEW_VERSION: ${{ needs.check-release-version.outputs.playwright_version }} | |
| with: | |
| script: | | |
| const fs = require('node:fs'); | |
| const owner = context.repo.owner; | |
| const repo = context.repo.repo; | |
| const runUrl = `${context.serverUrl}/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}`; | |
| const commitSha = context.sha; | |
| const oldVersion = process.env.OLD_VERSION || 'unknown'; | |
| const rawNewVersion = process.env.NEW_VERSION || 'unknown'; | |
| const normalizedNewVersion = rawNewVersion.startsWith('v') ? rawNewVersion.slice(1) : rawNewVersion; | |
| const issueTitle = `Release Failed: v${normalizedNewVersion} - Tests Failed`; | |
| let reportJsonText = 'Report artifact could not be read.'; | |
| let affectedTable = 'No affected symbols were found in the patch impact report.'; | |
| try { | |
| const reportPath = 'artifacts/report.json'; | |
| const reportRaw = fs.readFileSync(reportPath, 'utf8'); | |
| reportJsonText = reportRaw; | |
| const report = JSON.parse(reportRaw); | |
| const affected = Array.isArray(report.affected) ? report.affected : []; | |
| if (affected.length > 0) { | |
| const header = '| Symbol | Kind | Change Type | Playwright File | Patch File |'; | |
| const divider = '|--------|------|-------------|-----------------|------------|'; | |
| const rows = affected.map((row) => { | |
| const symbol = row.symbol ?? ''; | |
| const kind = row.kind ?? ''; | |
| const changeType = row.changeType ?? ''; | |
| const playwrightFile = row.playwrightFile ?? ''; | |
| const patchFile = row.patchFile ?? ''; | |
| return `| ${symbol} | ${kind} | ${changeType} | ${playwrightFile} | ${patchFile} |`; | |
| }); | |
| affectedTable = [header, divider, ...rows].join('\n'); | |
| } | |
| } catch (error) { | |
| affectedTable = 'Patch impact report artifact could not be read.'; | |
| } | |
| let pageTestsStepLink = 'Unavailable'; | |
| let libraryTestsStepLink = 'Unavailable'; | |
| let pageTestsStepUrl = null; | |
| let libraryTestsStepUrl = null; | |
| try { | |
| const jobsResponse = await github.rest.actions.listJobsForWorkflowRun({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| run_id: context.runId, | |
| per_page: 100, | |
| }); | |
| const testsJob = jobsResponse.data.jobs.find((job) => | |
| job.name.includes('Run Patchright Tests') || | |
| job.name.toLowerCase().includes('run-patchright-tests') | |
| ); | |
| if (testsJob?.html_url) { | |
| const pageStep = (testsJob.steps || []).find((step) => step.name === 'Run Page Tests'); | |
| const libraryStep = (testsJob.steps || []).find((step) => step.name === 'Run Library Tests'); | |
| if (pageStep?.number) { | |
| pageTestsStepUrl = `${testsJob.html_url}#step:${pageStep.number}:1`; | |
| pageTestsStepLink = `[Run Page Tests](${pageTestsStepUrl})`; | |
| } | |
| if (libraryStep?.number) { | |
| libraryTestsStepUrl = `${testsJob.html_url}#step:${libraryStep.number}:1`; | |
| libraryTestsStepLink = `[Run Library Tests](${libraryTestsStepUrl})`; | |
| } | |
| } | |
| } catch (error) { | |
| // Keep defaults when job/step links are unavailable. | |
| } | |
| let reportObj = null; | |
| try { | |
| reportObj = JSON.parse(reportJsonText); | |
| } catch { | |
| // leave null; summary will reflect that the report was unreadable | |
| } | |
| let reportArtifact = null; | |
| try { | |
| const artifactsResponse = await github.rest.actions.listWorkflowRunArtifacts({ | |
| owner, | |
| repo, | |
| run_id: context.runId, | |
| }); | |
| reportArtifact = artifactsResponse.data.artifacts.find((a) => a.name === 'report.json') || null; | |
| } catch (error) { | |
| reportArtifact = { error: String(error.message) }; | |
| } | |
| const summaryPayload = { | |
| old_version: oldVersion, | |
| new_version: rawNewVersion, | |
| summary: reportObj?.summary || { total: 0, affected: 0, unaffected: 0 }, | |
| workflow_run_url: runUrl, | |
| commit: commitSha, | |
| test_links: { | |
| page_tests: pageTestsStepUrl, | |
| library_tests: libraryTestsStepUrl, | |
| }, | |
| affected_symbols: (reportObj?.affected || []).map((row) => ({ | |
| symbol: row.symbol, | |
| kind: row.kind, | |
| change_type: row.changeType, | |
| playwright_file: row.playwrightFile, | |
| patch_file: row.patchFile, | |
| })), | |
| report_artifact: reportArtifact && reportArtifact.id ? { | |
| artifact_id: reportArtifact.id, | |
| name: reportArtifact.name, | |
| archive_download_url: reportArtifact.archive_download_url, | |
| } : (reportArtifact && reportArtifact.error ? { error: reportArtifact.error } : null), | |
| }; | |
| const issueBody = [ | |
| `## Release Failed: Playwright ${oldVersion} → ${rawNewVersion}`, | |
| '', | |
| 'The automated release workflow failed at the test step.', | |
| '', | |
| 'Failure context:', | |
| `- Workflow run: ${runUrl}`, | |
| `- Commit: ${commitSha}`, | |
| '', | |
| 'Test step logs:', | |
| `- ${pageTestsStepLink}`, | |
| `- ${libraryTestsStepLink}`, | |
| '', | |
| 'The following patched APIs changed between these versions and may be the cause:', | |
| '', | |
| affectedTable, | |
| '', | |
| '### Machine-readable summary', | |
| '', | |
| '```json', | |
| JSON.stringify(summaryPayload, null, 2), | |
| '```', | |
| '', | |
| 'The full `report.json` artifact is linked in `report_artifact.archive_download_url` above.', | |
| '', | |
| 'cc @Vinyzu', | |
| ].join('\n'); | |
| const existingIssues = await github.rest.issues.listForRepo({ | |
| owner, | |
| repo, | |
| state: 'open', | |
| labels: 'release-failed,upstream-break', | |
| per_page: 100, | |
| }); | |
| const existingIssue = existingIssues.data.find((issue) => | |
| !issue.pull_request && issue.title === issueTitle && issue.user?.login === 'github-actions[bot]' | |
| ); | |
| if (existingIssue) { | |
| await github.rest.issues.update({ | |
| owner, | |
| repo, | |
| issue_number: existingIssue.number, | |
| title: issueTitle, | |
| body: issueBody, | |
| labels: ['release-failed', 'upstream-break'], | |
| }); | |
| await github.rest.issues.createComment({ | |
| owner, | |
| repo, | |
| issue_number: existingIssue.number, | |
| body: `Failure reproduced in workflow run ${runUrl} on commit ${commitSha}.`, | |
| }); | |
| } else { | |
| const { data: createdIssue } = await github.rest.issues.create({ | |
| owner, | |
| repo, | |
| title: issueTitle, | |
| body: issueBody, | |
| labels: ['release-failed', 'upstream-break'], | |
| }); | |
| const branchName = `fix-v${normalizedNewVersion}-${context.runId}`; | |
| const { data: baseBranch } = await github.rest.repos.getBranch({ | |
| owner, | |
| repo, | |
| branch: 'main', | |
| }); | |
| const baseSha = baseBranch.commit.sha; | |
| const baseTreeSha = baseBranch.commit.commit.tree.sha; | |
| await github.rest.git.createRef({ | |
| owner, | |
| repo, | |
| ref: `refs/heads/${branchName}`, | |
| sha: baseSha, | |
| }); | |
| const { data: scaffoldCommit } = await github.rest.git.createCommit({ | |
| owner, | |
| repo, | |
| message: `chore: scaffold ${branchName}`, | |
| tree: baseTreeSha, | |
| parents: [baseSha], | |
| }); | |
| await github.rest.git.updateRef({ | |
| owner, | |
| repo, | |
| ref: `heads/${branchName}`, | |
| sha: scaffoldCommit.sha, | |
| }); | |
| const { data: pullRequest } = await github.rest.pulls.create({ | |
| owner, | |
| repo, | |
| title: `fix: investigate Playwright ${rawNewVersion} release failure`, | |
| head: branchName, | |
| base: 'main', | |
| draft: true, | |
| body: [ | |
| `Closes #${createdIssue.number}`, | |
| '', | |
| `Auto-created from failed release workflow run: ${runUrl}`, | |
| ].join('\n'), | |
| }); | |
| console.log(`Draft fix PR created: ${pullRequest.html_url}`); | |
| } | |
| patchright-workflow: | |
| name: "Patchright Release: Install, Patch, Build and Publish Patchright Driver" | |
| needs: [check-release-version, check-patch-impact, run-patchright-tests] | |
| # check-patch-impact is an informational notifier (opens an issue on upstream | |
| # breakage), NOT a release gate - the conditions below never check its result. | |
| # always() lets the release proceed even when it fails, e.g. the first-build | |
| # bootstrap case where there is no previous release to diff against (v0.0.0 404). | |
| if: always() && needs.check-release-version.outputs.proceed == 'true' && needs.run-patchright-tests.result == 'success' && needs.run-patchright-tests.outputs.test_outcome == 'success' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| id-token: write | |
| steps: | |
| - name: Checkout Repository | |
| uses: actions/checkout@v6 | |
| - name: Configure Fast APT Mirror | |
| uses: vegardit/fast-apt-mirror.sh@v1 | |
| - name: Setup Node v24 | |
| uses: actions/setup-node@v6 | |
| with: | |
| node-version: 24 | |
| registry-url: 'https://registry.npmjs.org' | |
| - name: Install NPM Dependencies | |
| run: npm install | |
| - name: Install Playwright Driver | |
| run: | | |
| git clone https://github.com/microsoft/playwright --branch ${{ needs.check-release-version.outputs.playwright_version }} | |
| cd playwright | |
| npm ci | |
| - name: Patch Playwright Driver | |
| run: | | |
| cd playwright | |
| git -C .. submodule update --init --recursive --remote patchright-nodejs | |
| cd .. && npm run patch | |
| - name: Generate Playwright Channels | |
| # Ignore the error exit code, as the script exits 1 when a file is modified. | |
| continue-on-error: true | |
| run: | | |
| cd playwright | |
| node utils/generate_channels.js | |
| - name: Build Patchright Driver | |
| run: | | |
| cd playwright | |
| npm run build | |
| npx playwright install-deps | |
| chmod +x utils/build/build-playwright-driver.sh | |
| utils/build/build-playwright-driver.sh | |
| - name: Rebrand Patchright Package | |
| run: | | |
| cd playwright | |
| npx tsx ../patchright-nodejs/patchright_rebranding.ts | |
| - name: Publish Patchright-Core Package | |
| run: | | |
| cd playwright/packages/patchright-core/ | |
| package_name=$(node -p "require('./package.json').name") | |
| package_version=$(node -p "require('./package.json').version") | |
| if npm view "${package_name}@${package_version}" version >/dev/null 2>&1; then | |
| echo "Skipping ${package_name}@${package_version}: already published." | |
| else | |
| npm publish --access=public --provenance | |
| fi | |
| - name: Publish Patchright Package | |
| run: | | |
| cd playwright/packages/patchright/ | |
| package_name=$(node -p "require('./package.json').name") | |
| package_version=$(node -p "require('./package.json').version") | |
| if npm view "${package_name}@${package_version}" version >/dev/null 2>&1; then | |
| echo "Skipping ${package_name}@${package_version}: already published." | |
| else | |
| npm publish --access=public --provenance | |
| fi | |
| - name: Publish Patchright Driver | |
| if: env.patchright_release == '' || needs.check-release-version.outputs.previous_playwright_version != needs.check-release-version.outputs.playwright_version | |
| env: | |
| PLAYWRIGHT_VERSION: ${{ needs.check-release-version.outputs.playwright_version }} | |
| run: | | |
| chmod +x utils/release_driver.sh | |
| utils/release_driver.sh |