Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions .github/workflows/_visual-regression.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
name: Visual regression

on:
workflow_call:

permissions:
contents: read

jobs:
visual-regression:
name: Playwright visual snapshots
runs-on: ubuntu-latest
timeout-minutes: 15
# Pinned to the exact @playwright/test version in package.json β€” the
# image and npm package must match exactly, or browser launch breaks.
# Screenshot comparison is sensitive to font rendering and software
# rasterization, which differ by host OS/GPU β€” running inside this
# exact image keeps this job and the /update-snapshots regeneration
# job (update-visual-baselines.yaml, same digest) pixel-consistent
# with each other regardless of which runner picks up the job.
container:
image: mcr.microsoft.com/playwright@sha256:eff16c30e6f3f4af0a03fa4b706120d5e9b0891c344a27d64559aff5900a4a27 # v1.63.0-noble
options: --ipc=host
env:
# Keep in sync with the image tag in the comment above β€” the
# "Verify Playwright version" step below checks the installed
# @playwright/test against this, so a dependency bump without a
# matching image bump fails loud and named instead of as a
# confusing browser-launch error on an unrelated PR.
PLAYWRIGHT_IMAGE_VERSION: '1.63.0'
# Keep the build-time MCP metadata format stable. The regular site
# build still validates the current ToolHive release independently.
TOOLHIVE_VERSION: '0.49.0'
steps:
- name: Checkout repository
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

# actions/checkout marks the repo safe in the RUNNER's git config,
# but a run: step inside `container:` executes via `docker exec`
# into a separate environment that never got that exception β€”
# without this, Docusaurus's last-update-date git log call (and
# anything else here shelling out to git) fails with "detected
# dubious ownership in repository".
- name: Mark workspace as a safe git directory
run: git config --global --add safe.directory "$GITHUB_WORKSPACE"

# The Playwright image is minimal β€” it ships neither jq, which
# scripts/install-thv.sh requires, nor a Node version manager, since
# it already bundles the Node build this image's Chromium was
# tested against. A pinned static binary (checksum-verified) avoids
# `apt-get update`, which syncs the full Ubuntu archive index just
# to install one small tool.
- name: Install jq
run: |
curl -sSL -o /usr/local/bin/jq https://github.com/jqlang/jq/releases/download/jq-1.8.2/jq-linux-amd64
echo "b1c22172dd303f3be49e935aa56aa48a8b7a46e0bc838b4997d3bb451495870f /usr/local/bin/jq" | sha256sum -c -
chmod +x /usr/local/bin/jq

- name: Cache dependencies
id: cache
uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0
with:
path: ./node_modules
key: modules-${{ hashFiles('package-lock.json') }}

- name: Install dependencies
if: steps.cache.outputs.cache-hit != 'true'
run: npm ci

- name: Verify Playwright version matches pinned image
run: |
installed="$(npx playwright --version | sed -n 's/^Version \([0-9.]*\)$/\1/p')"
if [ "$installed" != "$PLAYWRIGHT_IMAGE_VERSION" ]; then
echo "::error::@playwright/test resolved to $installed but the pinned image is for $PLAYWRIGHT_IMAGE_VERSION β€” bump the image digest (see container.image comment) to match."
exit 1
fi

- name: Install ToolHive CLI
run: ./scripts/install-thv.sh
Comment thread
danbarr marked this conversation as resolved.

- name: Build site
run: npm run build

- name: Run visual regression tests
run: npm run test:visual
Comment thread
danbarr marked this conversation as resolved.

- name: Upload test artifacts
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
if: failure()
with:
name: playwright-report
path: |
test-results/
playwright-report/
retention-days: 7
4 changes: 4 additions & 0 deletions .github/workflows/on-pr.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -21,3 +21,7 @@ jobs:
static-checks:
name: Static checks
uses: $/.github/workflows/_static-checks.yaml

visual-regression:
name: Visual regression
uses: $/.github/workflows/_visual-regression.yaml
82 changes: 82 additions & 0 deletions .github/workflows/pr-screenshot-summary.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
name: PR Screenshot Summary

# workflow_run always loads this workflow from the default branch. That makes
# it a trusted follow-up to the read-only PR workflow even though this job can
# update pull request descriptions.
# The privileged job checks out only the API-resolved base SHA and treats the
# PR head as inert Git data. Zizmor cannot infer that cross-step trust boundary.
on: # zizmor: ignore[dangerous-triggers]
workflow_run:
workflows: ['On PR']
types: [completed]

concurrency:
group: ${{ github.workflow }}-${{ github.event.workflow_run.pull_requests[0].number }}
cancel-in-progress: true

permissions: {}

jobs:
update-description:
name: Update PR description with screenshot summary
if: |
github.event.workflow_run.event == 'pull_request' &&
github.event.workflow_run.pull_requests[0] != null
runs-on: ubuntu-latest
permissions:
contents: read
pull-requests: write
steps:
- name: Resolve current PR context
id: pr
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number }}
REPO: ${{ github.repository }}
RUN_HEAD_SHA: ${{ github.event.workflow_run.head_sha }}
run: |
PR_JSON="$(gh api "repos/$REPO/pulls/$PR_NUMBER")"
BASE_SHA="$(echo "$PR_JSON" | jq -r '.base.sha')"
HEAD_SHA="$(echo "$PR_JSON" | jq -r '.head.sha')"
if [ "$HEAD_SHA" != "$RUN_HEAD_SHA" ]; then
echo "The PR advanced after this run; its newer run will update the summary."
echo "current=false" >> "$GITHUB_OUTPUT"
exit 0
fi
{
echo "current=true"
echo "base_sha=$BASE_SHA"
echo "head_sha=$HEAD_SHA"
} >> "$GITHUB_OUTPUT"

- name: Checkout trusted base revision
if: steps.pr.outputs.current == 'true'
uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
ref: ${{ steps.pr.outputs.base_sha }}
fetch-depth: 0
persist-credentials: false

- name: Fetch PR head for read-only inspection
if: steps.pr.outputs.current == 'true'
env:
EXPECTED_HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
PR_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number }}
run: |
git fetch --no-tags origin \
"+refs/pull/$PR_NUMBER/head:refs/remotes/origin/pr/$PR_NUMBER/head"
ACTUAL_HEAD_SHA="$(git rev-parse "refs/remotes/origin/pr/$PR_NUMBER/head")"
if [ "$ACTUAL_HEAD_SHA" != "$EXPECTED_HEAD_SHA" ]; then
echo "::error::PR head changed while preparing the summary; retry against the current head."
exit 1
fi

- name: Update PR description with screenshot summary
if: steps.pr.outputs.current == 'true'
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
PR_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number }}
BASE_SHA: ${{ steps.pr.outputs.base_sha }}
HEAD_SHA: ${{ steps.pr.outputs.head_sha }}
# This script comes from the checked-out base revision, never the PR.
run: node scripts/update-pr-screenshot-summary.mjs
Comment thread
danbarr marked this conversation as resolved.
Loading
Loading