070959e133
landing-page-staging / Deploy landing page to staging (push) Has been skipped
landing-page-ci / Validate landing page (push) Failing after 4s
visual-baseline / Capture visual baselines (push) Has been cancelled
bake-plugin-previews / Bake plugin previews (push) Has been cancelled
360 lines
16 KiB
YAML
360 lines
16 KiB
YAML
name: report.atom
|
|
|
|
on:
|
|
workflow_run:
|
|
workflows: [ci]
|
|
types: [completed]
|
|
|
|
permissions:
|
|
actions: read
|
|
contents: read
|
|
issues: write
|
|
pull-requests: write
|
|
|
|
concurrency:
|
|
group: report-${{ github.event.workflow_run.id }}
|
|
cancel-in-progress: false
|
|
|
|
jobs:
|
|
report:
|
|
name: Materialize handoff reports
|
|
if: ${{ github.repository == 'nexu-io/open-design' && github.event.workflow_run.event == 'pull_request' }}
|
|
runs-on: ubuntu-24.04
|
|
timeout-minutes: 20
|
|
|
|
steps:
|
|
- name: Checkout trusted workflow code
|
|
uses: actions/checkout@v6.0.2
|
|
with:
|
|
repository: ${{ github.repository }}
|
|
ref: ${{ github.event.repository.default_branch }}
|
|
|
|
- name: Check handoff helper
|
|
run: python3 .github/scripts/handoff.py self-check
|
|
|
|
- name: Resolve report handoff artifacts
|
|
id: artifacts
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
REPO: ${{ github.repository }}
|
|
RUN_ID: ${{ github.event.workflow_run.id }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
pattern="$(python3 .github/scripts/handoff.py artifact-pattern report)"
|
|
count="$(
|
|
gh api "repos/$REPO/actions/runs/$RUN_ID/artifacts" \
|
|
--jq '.artifacts[]? | select(.expired == false and (.name | startswith("handoff-report-"))) | .name' \
|
|
| sed '/^$/d' \
|
|
| wc -l \
|
|
| tr -d ' '
|
|
)"
|
|
if [ "$count" = "0" ]; then
|
|
echo "found=false" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
{
|
|
echo "found=true"
|
|
echo "pattern=$pattern"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Download report handoff artifacts
|
|
if: ${{ steps.artifacts.outputs.found == 'true' }}
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
pattern: ${{ steps.artifacts.outputs.pattern }}
|
|
run-id: ${{ github.event.workflow_run.id }}
|
|
path: ${{ runner.temp }}/handoff-report
|
|
merge-multiple: false
|
|
github-token: ${{ github.token }}
|
|
|
|
- name: Read visual report request
|
|
if: ${{ steps.artifacts.outputs.found == 'true' }}
|
|
id: report_request
|
|
env:
|
|
RUN_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
entries="$RUNNER_TEMP/report-handoffs.jsonl"
|
|
python3 .github/scripts/handoff.py list report "$RUNNER_TEMP/handoff-report" > "$entries"
|
|
visual_entry="$(
|
|
jq -s -c '[.[] | select(.report_type == "visual-pr")] | if length == 0 then empty elif length == 1 then .[0] else error("multiple visual-pr report handoffs") end' "$entries"
|
|
)"
|
|
if [ -z "$visual_entry" ]; then
|
|
echo "present=false" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
head_sha="$(jq -r '.head_sha' <<< "$visual_entry")"
|
|
if [ "$head_sha" != "$RUN_HEAD_SHA" ]; then
|
|
echo "Report handoff head $head_sha does not match workflow_run head $RUN_HEAD_SHA." >&2
|
|
exit 1
|
|
fi
|
|
{
|
|
echo "present=true"
|
|
echo "id=$(jq -r '.id' <<< "$visual_entry")"
|
|
echo "pr_number=$(jq -r '.pr_number' <<< "$visual_entry")"
|
|
echo "head_sha=$head_sha"
|
|
echo "base_sha=$(jq -r '.base_sha' <<< "$visual_entry")"
|
|
echo "run_id=$(jq -r '.run_id' <<< "$visual_entry")"
|
|
echo "artifact_pattern=$(jq -r '.artifact_pattern' <<< "$visual_entry")"
|
|
echo "output_comment_id=$(jq -r '.output_comment_id' <<< "$visual_entry")"
|
|
echo "marker=$(jq -r '.marker' <<< "$visual_entry")"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Resolve visual capture artifacts
|
|
if: ${{ steps.report_request.outputs.present == 'true' }}
|
|
id: capture_artifacts
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
REPO: ${{ github.repository }}
|
|
RUN_ID: ${{ github.event.workflow_run.id }}
|
|
CAPTURE_PATTERN: ${{ steps.report_request.outputs.artifact_pattern }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
prefix="${CAPTURE_PATTERN%\*}"
|
|
count="$(
|
|
gh api "repos/$REPO/actions/runs/$RUN_ID/artifacts" \
|
|
--jq '.artifacts[]? | select(.expired == false) | .name' \
|
|
| awk -v prefix="$prefix" 'index($0, prefix) == 1 { count += 1 } END { print count + 0 }'
|
|
)"
|
|
if [ "$count" = "0" ]; then
|
|
echo "No visual capture artifacts found for pattern $CAPTURE_PATTERN; skipping report."
|
|
echo "found=false" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
echo "found=true" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Download visual capture artifacts
|
|
if: ${{ steps.capture_artifacts.outputs.found == 'true' }}
|
|
uses: actions/download-artifact@v8
|
|
with:
|
|
pattern: ${{ steps.report_request.outputs.artifact_pattern }}
|
|
run-id: ${{ github.event.workflow_run.id }}
|
|
path: ${{ runner.temp }}/visual-artifacts
|
|
merge-multiple: false
|
|
github-token: ${{ github.token }}
|
|
|
|
- name: Read visual capture manifests
|
|
if: ${{ steps.capture_artifacts.outputs.found == 'true' }}
|
|
id: manifest
|
|
env:
|
|
PR_NUMBER: ${{ steps.report_request.outputs.pr_number }}
|
|
HEAD_SHA: ${{ steps.report_request.outputs.head_sha }}
|
|
BASE_SHA: ${{ steps.report_request.outputs.base_sha }}
|
|
RUN_ID: ${{ steps.report_request.outputs.run_id }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
artifacts_dir="$RUNNER_TEMP/visual-artifacts"
|
|
combined_dir="$RUNNER_TEMP/visual-combined"
|
|
screenshots_dir="$combined_dir/visual-screenshots"
|
|
mkdir -p "$screenshots_dir"
|
|
mapfile -t manifests < <(find "$artifacts_dir" -type f -path '*/visual-report/manifest.json' | sort)
|
|
if [ "${#manifests[@]}" -eq 0 ]; then
|
|
mapfile -t manifests < <(find "$artifacts_dir" -type f -name manifest.json | sort)
|
|
fi
|
|
if [ "${#manifests[@]}" -eq 0 ]; then
|
|
echo "Capture manifests not found" >&2
|
|
find "$artifacts_dir" -maxdepth 5 -type f >&2
|
|
exit 1
|
|
fi
|
|
|
|
capture_outcome="success"
|
|
for current_manifest in "${manifests[@]}"; do
|
|
current_pr_number="$(jq -r '.pr_number' "$current_manifest")"
|
|
current_head_sha="$(jq -r '.head_sha' "$current_manifest")"
|
|
current_base_sha="$(jq -r '.base_sha' "$current_manifest")"
|
|
current_run_id="$(jq -r '.run_id' "$current_manifest")"
|
|
current_outcome="$(jq -r '.capture_outcome // "success"' "$current_manifest")"
|
|
if [ "$current_pr_number" != "$PR_NUMBER" ]; then
|
|
echo "Mismatched visual manifest PR number: $current_pr_number vs $PR_NUMBER" >&2
|
|
exit 1
|
|
fi
|
|
if [ "$current_head_sha" != "$HEAD_SHA" ]; then
|
|
echo "Mismatched visual manifest head SHA: $current_head_sha vs $HEAD_SHA" >&2
|
|
exit 1
|
|
fi
|
|
if [ "$current_base_sha" != "$BASE_SHA" ]; then
|
|
echo "Mismatched visual manifest base SHA: $current_base_sha vs $BASE_SHA" >&2
|
|
exit 1
|
|
fi
|
|
if [ "$current_run_id" != "$RUN_ID" ]; then
|
|
echo "Mismatched visual manifest run ID: $current_run_id vs $RUN_ID" >&2
|
|
exit 1
|
|
fi
|
|
if ! [[ "$current_outcome" =~ ^(success|failure|cancelled|skipped)$ ]]; then
|
|
echo "Invalid manifest capture_outcome: $current_outcome" >&2
|
|
exit 1
|
|
fi
|
|
if [ "$current_outcome" != "success" ]; then
|
|
capture_outcome="failure"
|
|
fi
|
|
|
|
manifest_dir="$(dirname "$current_manifest")"
|
|
if [ "$(basename "$manifest_dir")" = "visual-report" ]; then
|
|
artifact_root="$(dirname "$manifest_dir")"
|
|
else
|
|
artifact_root="$manifest_dir"
|
|
fi
|
|
if [ -d "$artifact_root/visual-screenshots" ]; then
|
|
find "$artifact_root/visual-screenshots" -type f -name '*.png' -exec cp '{}' "$screenshots_dir/" \;
|
|
fi
|
|
done
|
|
{
|
|
echo "capture_outcome=$capture_outcome"
|
|
echo "screenshots_dir=$screenshots_dir"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Validate live PR state
|
|
if: ${{ steps.capture_artifacts.outputs.found == 'true' }}
|
|
id: trusted-pr
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
REPO: ${{ github.repository }}
|
|
PR_NUMBER: ${{ steps.report_request.outputs.pr_number }}
|
|
HEAD_SHA: ${{ steps.report_request.outputs.head_sha }}
|
|
BASE_SHA: ${{ steps.report_request.outputs.base_sha }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
pr_json="$(gh api "repos/$REPO/pulls/$PR_NUMBER")"
|
|
pr_state="$(jq -r '.state' <<< "$pr_json")"
|
|
pr_is_draft="$(jq -r '.draft' <<< "$pr_json")"
|
|
current_head="$(jq -r '.head.sha' <<< "$pr_json")"
|
|
current_base="$(jq -r '.base.sha' <<< "$pr_json")"
|
|
if [ "$pr_state" != "open" ]; then
|
|
echo "Skipping visual report for PR $PR_NUMBER because state is $pr_state."
|
|
echo "stale=true" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
if [ "$pr_is_draft" != "false" ]; then
|
|
echo "Skipping visual report for PR $PR_NUMBER because it is draft."
|
|
echo "stale=true" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
if [ "$current_head" != "$HEAD_SHA" ]; then
|
|
echo "Skipping stale visual report for $HEAD_SHA; current PR head is $current_head."
|
|
echo "stale=true" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
if [ "$current_base" != "$BASE_SHA" ]; then
|
|
echo "Skipping stale visual report for base $BASE_SHA; current PR base is $current_base."
|
|
echo "stale=true" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
echo "stale=false" >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Checkout trusted base revision
|
|
if: ${{ steps.capture_artifacts.outputs.found == 'true' && steps.trusted-pr.outputs.stale != 'true' }}
|
|
uses: actions/checkout@v6.0.2
|
|
with:
|
|
repository: ${{ github.repository }}
|
|
ref: ${{ steps.report_request.outputs.base_sha }}
|
|
|
|
- name: Setup workspace
|
|
if: ${{ steps.capture_artifacts.outputs.found == 'true' && steps.trusted-pr.outputs.stale != 'true' }}
|
|
uses: ./.github/actions/setup-workspace
|
|
|
|
- name: Build visual diff report
|
|
if: ${{ steps.capture_artifacts.outputs.found == 'true' && steps.trusted-pr.outputs.stale != 'true' }}
|
|
env:
|
|
R2_ACCOUNT_ID: ${{ secrets.R2_ACCOUNT_ID }}
|
|
R2_ACCESS_KEY_ID: ${{ secrets.R2_ACCESS_KEY_ID }}
|
|
R2_SECRET_ACCESS_KEY: ${{ secrets.R2_SECRET_ACCESS_KEY }}
|
|
R2_BUCKET: ${{ secrets.R2_BUCKET }}
|
|
R2_PUBLIC_ORIGIN: ${{ vars.R2_PUBLIC_ORIGIN }}
|
|
CLOUDFLARE_R2_RELEASES_AK: ${{ secrets.CLOUDFLARE_R2_RELEASES_AK }}
|
|
CLOUDFLARE_R2_RELEASES_SK: ${{ secrets.CLOUDFLARE_R2_RELEASES_SK }}
|
|
CLOUDFLARE_R2_RELEASES_BUCKET: ${{ secrets.CLOUDFLARE_R2_RELEASES_BUCKET }}
|
|
CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN: ${{ vars.CLOUDFLARE_R2_RELEASES_PUBLIC_ORIGIN }}
|
|
CLOUDFLARE_R2_RELEASES_URL: ${{ secrets.CLOUDFLARE_R2_RELEASES_URL }}
|
|
run: |
|
|
pnpm -C e2e exec tsx scripts/visual-report.ts compare-pr \
|
|
--pr-number "${{ steps.report_request.outputs.pr_number }}" \
|
|
--run-id "${{ steps.report_request.outputs.run_id }}" \
|
|
--head-sha "${{ steps.report_request.outputs.head_sha }}" \
|
|
--base-sha "${{ steps.report_request.outputs.base_sha }}" \
|
|
--capture-outcome "${{ steps.manifest.outputs.capture_outcome }}" \
|
|
--screenshots "${{ steps.manifest.outputs.screenshots_dir }}" \
|
|
--comment-out ui/reports/visual-report/comment.md \
|
|
--manifest-out ui/reports/visual-report/report-manifest.json
|
|
|
|
- name: Upsert visual report comment
|
|
if: ${{ steps.capture_artifacts.outputs.found == 'true' && steps.trusted-pr.outputs.stale != 'true' }}
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
REPO: ${{ github.repository }}
|
|
PR_NUMBER: ${{ steps.report_request.outputs.pr_number }}
|
|
HEAD_SHA: ${{ steps.report_request.outputs.head_sha }}
|
|
BASE_SHA: ${{ steps.report_request.outputs.base_sha }}
|
|
MARKER: ${{ steps.report_request.outputs.marker }}
|
|
shell: bash
|
|
run: |
|
|
set -euo pipefail
|
|
pr_json="$(gh api "repos/$REPO/pulls/$PR_NUMBER")"
|
|
pr_state="$(jq -r '.state' <<< "$pr_json")"
|
|
pr_is_draft="$(jq -r '.draft' <<< "$pr_json")"
|
|
current_head="$(jq -r '.head.sha' <<< "$pr_json")"
|
|
current_base="$(jq -r '.base.sha' <<< "$pr_json")"
|
|
if [ "$pr_state" != "open" ]; then
|
|
echo "Skipping visual report comment for PR $PR_NUMBER because state is $pr_state."
|
|
exit 0
|
|
fi
|
|
if [ "$pr_is_draft" != "false" ]; then
|
|
echo "Skipping visual report comment for PR $PR_NUMBER because it is draft."
|
|
exit 0
|
|
fi
|
|
if [ "$current_head" != "$HEAD_SHA" ]; then
|
|
echo "Skipping stale visual report comment for $HEAD_SHA; current PR head is $current_head."
|
|
exit 0
|
|
fi
|
|
if [ "$current_base" != "$BASE_SHA" ]; then
|
|
echo "Skipping stale visual report comment for base $BASE_SHA; current PR base is $current_base."
|
|
exit 0
|
|
fi
|
|
|
|
body_path="e2e/ui/reports/visual-report/comment.md"
|
|
payload_file="$RUNNER_TEMP/visual-report-comment.json"
|
|
jq -n --rawfile body "$body_path" '{body: $body}' > "$payload_file"
|
|
comment_id="$(
|
|
gh api --paginate "repos/$REPO/issues/$PR_NUMBER/comments" \
|
|
| jq -r --arg marker "$MARKER" '.[] | select((.body // "") | contains($marker)) | .id' \
|
|
| tail -n 1 || true
|
|
)"
|
|
if [ -n "$comment_id" ]; then
|
|
if gh api --method PATCH "repos/$REPO/issues/comments/$comment_id" --input "$payload_file" >/dev/null; then
|
|
echo "Updated visual report comment on PR $PR_NUMBER."
|
|
else
|
|
echo "::warning title=Visual report comment update failed::Generated the visual report, but GitHub rejected updating the PR comment."
|
|
{
|
|
echo "### Visual report comment update failed"
|
|
echo
|
|
echo "The visual report was generated, but GitHub rejected updating the PR comment."
|
|
echo "Use the uploaded visual report artifact from this run."
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
fi
|
|
else
|
|
if gh api --method POST "repos/$REPO/issues/$PR_NUMBER/comments" --input "$payload_file" >/dev/null; then
|
|
echo "Created visual report comment on PR $PR_NUMBER."
|
|
else
|
|
echo "::warning title=Visual report comment creation failed::Generated the visual report, but GitHub rejected creating the PR comment."
|
|
{
|
|
echo "### Visual report comment creation failed"
|
|
echo
|
|
echo "The visual report was generated, but GitHub rejected creating the PR comment."
|
|
echo "Use the uploaded visual report artifact from this run."
|
|
} >> "$GITHUB_STEP_SUMMARY"
|
|
fi
|
|
fi
|
|
|
|
- name: Upload visual report artifact
|
|
if: ${{ always() && steps.capture_artifacts.outputs.found == 'true' && steps.trusted-pr.outputs.stale != 'true' }}
|
|
uses: actions/upload-artifact@v7
|
|
with:
|
|
name: visual-pr-report-${{ steps.report_request.outputs.pr_number }}-${{ steps.report_request.outputs.run_id }}
|
|
path: e2e/ui/reports/visual-report
|
|
if-no-files-found: ignore
|
|
retention-days: 7
|