name: publish Playwright report on: workflow_run: workflows: [ci] types: [completed] permissions: actions: read contents: read issues: write pull-requests: write concurrency: group: playwright-publication cancel-in-progress: false jobs: publish: if: >- (github.event.workflow_run.conclusion == 'success' || github.event.workflow_run.conclusion == 'failure' || github.event.workflow_run.conclusion == 'timed_out') && (github.event.workflow_run.event == 'pull_request' || (github.event.workflow_run.event == 'push' && github.event.workflow_run.head_branch == 'main')) runs-on: ubuntu-latest timeout-minutes: 10 steps: # workflow_run has repository secrets. Always use trusted default-branch code here; # never check out or execute the contributor's pull-request revision. - uses: actions/checkout@v5 with: ref: ${{ github.event.repository.default_branch }} persist-credentials: false - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v4 with: node-version: 22 cache: pnpm - run: pnpm install --frozen-lockfile - name: Read Playwright job result id: playwright env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} RUN_ID: ${{ github.event.workflow_run.id }} shell: bash run: | result="$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${RUN_ID}/jobs?per_page=100" \ --jq '[.jobs[] | select(.name == "Web E2E / Web E2E") | .conclusion] | unique | if length == 1 then .[0] else error("expected exactly one Playwright job") end')" case "$result" in success|failure) ;; timed_out) ;; *) echo "Unexpected Playwright job conclusion: $result" >&2; exit 1 ;; esac echo "job_result=$result" >> "$GITHUB_OUTPUT" if [[ "$result" == "timed_out" ]]; then result="failure"; fi echo "result=$result" >> "$GITHUB_OUTPUT" - name: Resolve pull request if: github.event.workflow_run.event == 'pull_request' id: pull_request env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} HEAD_OWNER: ${{ github.event.workflow_run.head_repository.owner.login }} HEAD_SHA: ${{ github.event.workflow_run.head_sha }} shell: bash run: | pulls="$(gh api --method GET \ "repos/${GITHUB_REPOSITORY}/pulls?state=all&head=${HEAD_OWNER}:${HEAD_BRANCH}&per_page=100")" number="$(jq -r --arg sha "$HEAD_SHA" \ '[.[] | select(.head.sha == $sha)] | if length == 1 then .[0].number else empty end' \ <<<"$pulls")" if [[ -z "$number" ]]; then echo "::warning::Could not resolve a pull request for ${HEAD_OWNER}:${HEAD_BRANCH} at ${HEAD_SHA}." fi echo "number=$number" >> "$GITHUB_OUTPUT" - name: Check for Playwright artifact id: artifact env: ARTIFACT_NAME: playwright-artifacts-${{ github.event.workflow_run.id }}-${{ github.event.workflow_run.run_attempt }} GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PLAYWRIGHT_JOB_RESULT: ${{ steps.playwright.outputs.job_result }} RUN_ID: ${{ github.event.workflow_run.id }} shell: bash run: | artifacts="$(gh api --method GET \ "repos/${GITHUB_REPOSITORY}/actions/runs/${RUN_ID}/artifacts?name=${ARTIFACT_NAME}")" count="$(jq --arg name "$ARTIFACT_NAME" \ '[.artifacts[] | select(.name == $name and .expired == false)] | length' \ <<<"$artifacts")" if [[ "$count" == "1" ]]; then echo "exists=true" >> "$GITHUB_OUTPUT" elif [[ "$count" == "0" && "$PLAYWRIGHT_JOB_RESULT" == "timed_out" ]]; then echo "::warning::The timed-out Playwright job ended before it uploaded an artifact. Publishing an empty gallery." echo "exists=false" >> "$GITHUB_OUTPUT" else echo "Expected exactly one unexpired Playwright artifact; found $count." >&2 exit 1 fi - name: Download Playwright artifacts if: steps.artifact.outputs.exists == 'true' uses: actions/download-artifact@v4 with: name: playwright-artifacts-${{ github.event.workflow_run.id }}-${{ github.event.workflow_run.run_attempt }} path: . github-token: ${{ secrets.GITHUB_TOKEN }} run-id: ${{ github.event.workflow_run.id }} - name: Publish trusted Playwright screenshot gallery id: publish env: AWS_ACCESS_KEY_ID: ${{ secrets.S3_ACCESS_KEY_ID }} AWS_SECRET_ACCESS_KEY: ${{ secrets.S3_SECRET_ACCESS_KEY }} AWS_DEFAULT_REGION: ${{ vars.S3_REGION }} AWS_EC2_METADATA_DISABLED: "true" S3_BUCKET: ${{ vars.S3_BUCKET }} S3_ENDPOINT: ${{ vars.S3_ENDPOINT }} PLAYWRIGHT_PUBLIC_BASE_URL: ${{ vars.PLAYWRIGHT_PUBLIC_BASE_URL }} PLAYWRIGHT_RESULT: ${{ steps.playwright.outputs.result }} PLAYWRIGHT_RUN_ATTEMPT: ${{ github.event.workflow_run.run_attempt }} PLAYWRIGHT_RUN_ID: ${{ github.event.workflow_run.id }} PLAYWRIGHT_RUN_NUMBER: ${{ github.event.workflow_run.run_number }} PLAYWRIGHT_RUN_URL: ${{ github.event.workflow_run.html_url }} PLAYWRIGHT_SHA: ${{ github.event.workflow_run.head_sha }} PLAYWRIGHT_EVENT: ${{ github.event.workflow_run.event }} PLAYWRIGHT_BRANCH: ${{ github.event.workflow_run.head_branch }} PLAYWRIGHT_PR_NUMBER: ${{ steps.pull_request.outputs.number }} PLAYWRIGHT_REPOSITORY_URL: ${{ github.server_url }}/${{ github.repository }} PLAYWRIGHT_PUBLISH_REPORT: ${{ github.event.workflow_run.event == 'push' }} run: bash scripts/publish-playwright-report.sh - name: Link screenshot gallery from pull request if: >- github.event.workflow_run.event == 'pull_request' && steps.pull_request.outputs.number != '' && steps.publish.outputs.latest_pr_run == 'true' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PLAYWRIGHT_PUBLIC_BASE_URL: ${{ vars.PLAYWRIGHT_PUBLIC_BASE_URL }} PR_NUMBER: ${{ steps.pull_request.outputs.number }} RUN_URL: ${{ github.event.workflow_run.html_url }} SHA: ${{ github.event.workflow_run.head_sha }} shell: bash run: | if [[ ! "$PR_NUMBER" =~ ^[1-9][0-9]*$ ]]; then echo "PR_NUMBER must be a positive integer." >&2 exit 1 fi public_base_url="${PLAYWRIGHT_PUBLIC_BASE_URL%/}" gallery_url="$public_base_url/prs/$PR_NUMBER/index.html" review_path="$GITHUB_WORKSPACE/.tmp/playwright-dashboard/screenshots/review.json" if [[ ! -f "$review_path" ]]; then echo "Missing Playwright review manifest at $review_path." >&2 exit 1 fi changed_paths_file="$(mktemp)" gh api --paginate \ "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files?per_page=100" \ --jq '.[].filename' > "$changed_paths_file" body="$( PLAYWRIGHT_GALLERY_URL="$gallery_url" \ PLAYWRIGHT_DASHBOARD_URL="$public_base_url/index.html" \ PLAYWRIGHT_RUN_URL="$RUN_URL" \ PLAYWRIGHT_SHA="$SHA" \ pnpm exec tsx packages/testkit/src/cli/build-playwright-pr-screenshot-comment.ts \ "$review_path" \ "$changed_paths_file" )" marker="" comment_id="$(gh api --paginate --slurp \ "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments?per_page=100" | \ jq -r --arg marker "$marker" \ '[.[][] | select(.user.login == "github-actions[bot]" and (.body | contains($marker)))] | last | .id // empty')" payload="$(jq -n --arg body "$body" '{body: $body}')" if [[ -n "$comment_id" ]]; then gh api --method PATCH "repos/${GITHUB_REPOSITORY}/issues/comments/${comment_id}" --input - <<<"$payload" else gh api --method POST "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" --input - <<<"$payload" fi