evOo-local/.github/workflows/publish-playwright-report.yml
cynarAI 634b3f823b
ci: stop image publish legs from cancelling each other (#191)
Every main push since the concurrency change cancelled two of the three
image builds: the build job's concurrency group named only the ref, so
the app, updater and computer legs of one run shared it and cancelled
each other, and the publish job never ran. Main has not published an
edge image since. The build and publish groups now name the matrix leg.

The Playwright report publisher also read the jobs of a run's latest
attempt, so a re-run already in progress made it fail on an empty
conclusion. It now reads the attempt that finished.


Claude-Session: https://claude.ai/code/session_01R8L4XgqjAemFR2remB5hVY

Co-authored-by: Claude <noreply@anthropic.com>
2026-10-10 06:50:07 +02:00

188 lines
9.1 KiB
YAML

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: [self-hosted, evoo-ci, Linux]
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 }}
RUN_ATTEMPT: ${{ github.event.workflow_run.run_attempt }}
shell: bash
run: |
# Read the attempt that finished: a re-run already in progress has no conclusion yet.
result="$(gh api "repos/${GITHUB_REPOSITORY}/actions/runs/${RUN_ID}/attempts/${RUN_ATTEMPT}/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"
echo "id=$(jq -r --arg name "$ARTIFACT_NAME" '.artifacts[] | select(.name == $name and .expired == false) | .id' <<<"$artifacts")" >> "$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'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ARTIFACT_ID: ${{ steps.artifact.outputs.id }}
shell: bash
run: |
gh api "repos/${GITHUB_REPOSITORY}/actions/artifacts/${ARTIFACT_ID}/zip" > "$RUNNER_TEMP/playwright-artifacts.zip"
# Keep untrusted files outside the checkout, dependencies, configuration,
# and generated dashboard consumed by the comment step.
python3 scripts/extract-playwright-artifacts.py \
"$RUNNER_TEMP/playwright-artifacts.zip" "$RUNNER_TEMP/playwright-artifacts"
- 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' }}
PLAYWRIGHT_REPORT_DIR: ${{ runner.temp }}/playwright-artifacts/playwright-report
PLAYWRIGHT_TEST_RESULTS_DIR: ${{ runner.temp }}/playwright-artifacts/apps/web/test-results
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="<!-- rakazo-playwright-screenshots -->"
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