From 4028d0a901580f98046511ca77286e0691adc516 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Sun, 10 May 2026 19:59:58 +0200 Subject: [PATCH 01/11] demo: change plain fill to red to trigger visual regression --- apps/planner/app/lib/elevation-chart-draw.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/planner/app/lib/elevation-chart-draw.ts b/apps/planner/app/lib/elevation-chart-draw.ts index e8c8867..2c61307 100644 --- a/apps/planner/app/lib/elevation-chart-draw.ts +++ b/apps/planner/app/lib/elevation-chart-draw.ts @@ -179,7 +179,7 @@ export function drawElevationChart( } ctx.lineTo(PADDING.left + chartW, PADDING.top + chartH); ctx.closePath(); - ctx.fillStyle = "rgba(37, 99, 235, 0.15)"; + ctx.fillStyle = "rgba(220, 38, 38, 0.15)"; // demo: red fill to trigger visual regression ctx.fill(); ctx.beginPath(); From 15c4e0f8e0def9a91c67d7c8de0354fce54d540e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Sun, 10 May 2026 20:03:55 +0200 Subject: [PATCH 02/11] demo: increase opacity to 0.8 so visual diff is detectable --- apps/planner/app/lib/elevation-chart-draw.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/planner/app/lib/elevation-chart-draw.ts b/apps/planner/app/lib/elevation-chart-draw.ts index 2c61307..bb5d411 100644 --- a/apps/planner/app/lib/elevation-chart-draw.ts +++ b/apps/planner/app/lib/elevation-chart-draw.ts @@ -179,7 +179,7 @@ export function drawElevationChart( } ctx.lineTo(PADDING.left + chartW, PADDING.top + chartH); ctx.closePath(); - ctx.fillStyle = "rgba(220, 38, 38, 0.15)"; // demo: red fill to trigger visual regression + ctx.fillStyle = "rgba(220, 38, 38, 0.8)"; // demo: vivid red fill to trigger visual regression ctx.fill(); ctx.beginPath(); From bceae8366e3b6af50351a8e7fc81ab9344b5b1d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Sun, 10 May 2026 20:10:03 +0200 Subject: [PATCH 03/11] Post visual diff images as PR comment on failure When visual tests fail on a PR, upload each diff PNG to GitHub's CDN via the issue assets endpoint and post a comment with the images embedded inline. Also fixes the artifact upload path to point at .vitest-attachments/ where the actual/diff files live. Co-Authored-By: Claude Sonnet 4.6 --- .github/workflows/ci.yml | 36 +++++++++++++++++++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 61fa59e..545de4e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -138,14 +138,48 @@ jobs: run: pnpm exec playwright install-deps chromium - name: Run visual regression tests + id: visual-tests run: pnpm --filter @trails-cool/planner test:visual + - name: Post diff comment on PR + if: failure() && steps.visual-tests.outcome == 'failure' && github.event_name == 'pull_request' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + diffs=$(find apps/planner/.vitest-attachments -name "*-diff-*.png" 2>/dev/null | sort) + if [ -z "$diffs" ]; then exit 0; fi + + body="## Visual regression failures"$'\n\n' + body+="| Test | Diff (reference → actual) |"$'\n' + body+="|------|--------------------------|"$'\n' + + for diff in $diffs; do + # Upload image to GitHub's CDN via the issue assets endpoint + response=$(curl -sf -X POST \ + -H "Authorization: Bearer $GH_TOKEN" \ + -H "Content-Type: image/png" \ + -H "Accept: application/json" \ + --data-binary @"$diff" \ + "https://uploads.github.com/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/assets?name=$(basename $diff)") + url=$(echo "$response" | jq -r '.url // empty') + name=$(basename "$diff" | sed 's/-diff-chromium-linux\.png//' | sed 's/-/ /g') + if [ -n "$url" ]; then + body+="| \`$name\` | ![$name]($url) |"$'\n' + else + body+="| \`$name\` | *(upload failed)* |"$'\n' + fi + done + + body+=$'\n'"Run \`pnpm --filter @trails-cool/planner test:visual:update\` to regenerate snapshots if the change is intentional." + + gh pr comment ${{ github.event.pull_request.number }} --body "$body" + - name: Upload screenshots on failure if: failure() uses: actions/upload-artifact@v7 with: name: visual-snapshots-diff - path: apps/planner/app/**/__screenshots__/ + path: apps/planner/.vitest-attachments/ retention-days: 7 e2e: From bc39d73c1d3bebb4842e052ac1a13883f5588c17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Sun, 10 May 2026 20:21:27 +0200 Subject: [PATCH 04/11] Grant pull-requests: write to visual-tests job for diff comment --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 545de4e..a3dfa25 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -113,6 +113,9 @@ jobs: visual-tests: name: Visual Tests runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write steps: - uses: actions/checkout@v6 - uses: pnpm/action-setup@v6 From 9a6a4bae40482356fc5a865a4ff15e423d86c5f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Sun, 10 May 2026 20:25:06 +0200 Subject: [PATCH 05/11] Fix visual diff comment: drop CDN upload, post artifact link instead --- .github/workflows/ci.yml | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a3dfa25..0a6019b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -153,27 +153,16 @@ jobs: if [ -z "$diffs" ]; then exit 0; fi body="## Visual regression failures"$'\n\n' - body+="| Test | Diff (reference → actual) |"$'\n' - body+="|------|--------------------------|"$'\n' - + body+="The following tests produced screenshot diffs:"$'\n\n' for diff in $diffs; do - # Upload image to GitHub's CDN via the issue assets endpoint - response=$(curl -sf -X POST \ - -H "Authorization: Bearer $GH_TOKEN" \ - -H "Content-Type: image/png" \ - -H "Accept: application/json" \ - --data-binary @"$diff" \ - "https://uploads.github.com/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/assets?name=$(basename $diff)") - url=$(echo "$response" | jq -r '.url // empty') name=$(basename "$diff" | sed 's/-diff-chromium-linux\.png//' | sed 's/-/ /g') - if [ -n "$url" ]; then - body+="| \`$name\` | ![$name]($url) |"$'\n' - else - body+="| \`$name\` | *(upload failed)* |"$'\n' - fi + body+="- \`$name\`"$'\n' done - - body+=$'\n'"Run \`pnpm --filter @trails-cool/planner test:visual:update\` to regenerate snapshots if the change is intentional." + body+=$'\n'"**[Download diff screenshots artifact](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})** (expand the Visual Tests job → Artifacts)"$'\n\n' + body+="To update snapshots if the change is intentional:"$'\n' + body+="\`\`\`"$'\n' + body+="pnpm --filter @trails-cool/planner test:visual:update"$'\n' + body+="\`\`\`" gh pr comment ${{ github.event.pull_request.number }} --body "$body" From bde195faf7e4d92588dad9be5daa244cf4eba14d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Sun, 10 May 2026 20:30:35 +0200 Subject: [PATCH 06/11] Fix sed pattern to strip platform suffix in diff comment --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0a6019b..0be8980 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -155,7 +155,7 @@ jobs: body="## Visual regression failures"$'\n\n' body+="The following tests produced screenshot diffs:"$'\n\n' for diff in $diffs; do - name=$(basename "$diff" | sed 's/-diff-chromium-linux\.png//' | sed 's/-/ /g') + name=$(basename "$diff" | sed 's/-diff-chromium-[a-z]*\.png//' | sed 's/-/ /g') body+="- \`$name\`"$'\n' done body+=$'\n'"**[Download diff screenshots artifact](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})** (expand the Visual Tests job → Artifacts)"$'\n\n' From c04b7d8abafb8534ca15c78e3a8d94d6a7cfdcec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Sun, 10 May 2026 20:35:25 +0200 Subject: [PATCH 07/11] Fix artifact upload: include-hidden-files for .vitest-attachments --- .github/workflows/ci.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0be8980..665cea9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -172,6 +172,7 @@ jobs: with: name: visual-snapshots-diff path: apps/planner/.vitest-attachments/ + include-hidden-files: true retention-days: 7 e2e: From 8e2929c914c4a152b2d33cbae7da4b2620df86a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Sun, 10 May 2026 20:59:40 +0200 Subject: [PATCH 08/11] Restore inline diff images using VISUAL_DIFF_TOKEN for asset upload --- .github/workflows/ci.yml | 22 ++++++++++++++++++---- 1 file changed, 18 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 665cea9..c8c3f7c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -148,18 +148,32 @@ jobs: if: failure() && steps.visual-tests.outcome == 'failure' && github.event_name == 'pull_request' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + VISUAL_DIFF_TOKEN: ${{ secrets.VISUAL_DIFF_TOKEN }} run: | diffs=$(find apps/planner/.vitest-attachments -name "*-diff-*.png" 2>/dev/null | sort) if [ -z "$diffs" ]; then exit 0; fi body="## Visual regression failures"$'\n\n' - body+="The following tests produced screenshot diffs:"$'\n\n' + body+="| Test | Diff (reference → actual) |"$'\n' + body+="|------|--------------------------|"$'\n' + for diff in $diffs; do name=$(basename "$diff" | sed 's/-diff-chromium-[a-z]*\.png//' | sed 's/-/ /g') - body+="- \`$name\`"$'\n' + response=$(curl -sf -X POST \ + -H "Authorization: Bearer $VISUAL_DIFF_TOKEN" \ + -H "Content-Type: image/png" \ + -H "Accept: application/json" \ + --data-binary @"$diff" \ + "https://uploads.github.com/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/assets?name=$(basename $diff)") + url=$(echo "$response" | jq -r '.url // empty') + if [ -n "$url" ]; then + body+="| \`$name\` | ![$name]($url) |"$'\n' + else + body+="| \`$name\` | *(upload failed)* |"$'\n' + fi done - body+=$'\n'"**[Download diff screenshots artifact](https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }})** (expand the Visual Tests job → Artifacts)"$'\n\n' - body+="To update snapshots if the change is intentional:"$'\n' + + body+=$'\n'"To update snapshots if the change is intentional:"$'\n' body+="\`\`\`"$'\n' body+="pnpm --filter @trails-cool/planner test:visual:update"$'\n' body+="\`\`\`" From a852a0acfb90c2889ca5e4c7f06df1602008296e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Sun, 10 May 2026 21:06:43 +0200 Subject: [PATCH 09/11] Debug: log HTTP status from asset upload --- .github/workflows/ci.yml | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c8c3f7c..a5a90fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -159,13 +159,16 @@ jobs: for diff in $diffs; do name=$(basename "$diff" | sed 's/-diff-chromium-[a-z]*\.png//' | sed 's/-/ /g') - response=$(curl -sf -X POST \ + response=$(curl -s -w "\n%{http_code}" -X POST \ -H "Authorization: Bearer $VISUAL_DIFF_TOKEN" \ -H "Content-Type: image/png" \ -H "Accept: application/json" \ --data-binary @"$diff" \ "https://uploads.github.com/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/assets?name=$(basename $diff)") - url=$(echo "$response" | jq -r '.url // empty') + http_code=$(echo "$response" | tail -1) + body_resp=$(echo "$response" | head -1) + echo "Upload HTTP $http_code: $body_resp" + url=$(echo "$body_resp" | jq -r '.url // empty') if [ -n "$url" ]; then body+="| \`$name\` | ![$name]($url) |"$'\n' else From de21ca47795482e394806d8df49e5e6478eaa3d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Sun, 10 May 2026 21:11:48 +0200 Subject: [PATCH 10/11] Fix asset upload: use multipart/form-data (-F) not raw binary --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a5a90fb..6032a77 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -161,9 +161,8 @@ jobs: name=$(basename "$diff" | sed 's/-diff-chromium-[a-z]*\.png//' | sed 's/-/ /g') response=$(curl -s -w "\n%{http_code}" -X POST \ -H "Authorization: Bearer $VISUAL_DIFF_TOKEN" \ - -H "Content-Type: image/png" \ -H "Accept: application/json" \ - --data-binary @"$diff" \ + -F "file=@$diff;type=image/png" \ "https://uploads.github.com/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/assets?name=$(basename $diff)") http_code=$(echo "$response" | tail -1) body_resp=$(echo "$response" | head -1) From 53663ff59fc2d1e0f0d01724a4f9320f4c2757fc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ullrich=20Sch=C3=A4fer?= Date: Sun, 10 May 2026 21:47:13 +0200 Subject: [PATCH 11/11] Switch to job summary for diff images (base64 data URIs) --- .github/workflows/ci.yml | 53 ++++++++++++++++++++++------------------ 1 file changed, 29 insertions(+), 24 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 6032a77..3fabf23 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -144,38 +144,43 @@ jobs: id: visual-tests run: pnpm --filter @trails-cool/planner test:visual - - name: Post diff comment on PR - if: failure() && steps.visual-tests.outcome == 'failure' && github.event_name == 'pull_request' - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - VISUAL_DIFF_TOKEN: ${{ secrets.VISUAL_DIFF_TOKEN }} + - name: Write diff images to job summary + if: failure() && steps.visual-tests.outcome == 'failure' run: | diffs=$(find apps/planner/.vitest-attachments -name "*-diff-*.png" 2>/dev/null | sort) if [ -z "$diffs" ]; then exit 0; fi - body="## Visual regression failures"$'\n\n' - body+="| Test | Diff (reference → actual) |"$'\n' - body+="|------|--------------------------|"$'\n' - + echo "## Visual regression failures" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY for diff in $diffs; do name=$(basename "$diff" | sed 's/-diff-chromium-[a-z]*\.png//' | sed 's/-/ /g') - response=$(curl -s -w "\n%{http_code}" -X POST \ - -H "Authorization: Bearer $VISUAL_DIFF_TOKEN" \ - -H "Accept: application/json" \ - -F "file=@$diff;type=image/png" \ - "https://uploads.github.com/repos/${{ github.repository }}/issues/${{ github.event.pull_request.number }}/assets?name=$(basename $diff)") - http_code=$(echo "$response" | tail -1) - body_resp=$(echo "$response" | head -1) - echo "Upload HTTP $http_code: $body_resp" - url=$(echo "$body_resp" | jq -r '.url // empty') - if [ -n "$url" ]; then - body+="| \`$name\` | ![$name]($url) |"$'\n' - else - body+="| \`$name\` | *(upload failed)* |"$'\n' - fi + b64=$(base64 -w 0 "$diff") + echo "### \`$name\`" >> $GITHUB_STEP_SUMMARY + echo "\"$name" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY done + echo "To update snapshots if the change is intentional:" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY + echo "pnpm --filter @trails-cool/planner test:visual:update" >> $GITHUB_STEP_SUMMARY + echo '```' >> $GITHUB_STEP_SUMMARY - body+=$'\n'"To update snapshots if the change is intentional:"$'\n' + - name: Post diff comment on PR + if: failure() && steps.visual-tests.outcome == 'failure' && github.event_name == 'pull_request' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + diffs=$(find apps/planner/.vitest-attachments -name "*-diff-*.png" 2>/dev/null | sort) + if [ -z "$diffs" ]; then exit 0; fi + + run_url="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" + body="## Visual regression failures"$'\n\n' + body+="The following tests produced screenshot diffs:"$'\n\n' + for diff in $diffs; do + name=$(basename "$diff" | sed 's/-diff-chromium-[a-z]*\.png//' | sed 's/-/ /g') + body+="- \`$name\`"$'\n' + done + body+=$'\n'"**[View diff images in the job summary]($run_url)**"$'\n\n' + body+="To update snapshots if the change is intentional:"$'\n' body+="\`\`\`"$'\n' body+="pnpm --filter @trails-cool/planner test:visual:update"$'\n' body+="\`\`\`"