diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2659dd0..a7eb4fb 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -88,3 +88,29 @@ jobs: - uses: actions/upload-artifact@v6 with: path: results + + cache-baseline: + name: Cache Baseline + if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }} + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v6 + - name: Cache Screenshots + id: screenshot-cache + uses: actions/cache@v5 + with: + path: build + key: screenshots-${{ github.sha }} + - uses: pnpm/action-setup@v4 + - uses: actions/setup-node@v6 + with: + node-version: 24 + cache: pnpm + - name: Install Dependencies + if: steps.screenshot-cache.outputs.cache-hit != 'true' + run: | + pnpm install + pnpm exec playwright install --with-deps + - name: Capture Screenshots + if: steps.screenshot-cache.outputs.cache-hit != 'true' + run: pnpm run test:screenshots diff --git a/.github/workflows/visual-diff.yml b/.github/workflows/visual-diff.yml index f788828..f85fa5f 100644 --- a/.github/workflows/visual-diff.yml +++ b/.github/workflows/visual-diff.yml @@ -1,7 +1,8 @@ name: Visual Diff on: - pull_request: {} + pull_request: + types: [labeled, unlabeled, opened, synchronize, reopened] concurrency: group: visual-diff-${{ github.head_ref || github.ref }} @@ -11,20 +12,36 @@ jobs: baseline: name: Baseline runs-on: ubuntu-latest + outputs: + ref: ${{ steps.ref.outputs.ref }} steps: - uses: actions/checkout@v6 with: ref: ${{ github.base_ref }} + - id: ref + run: | + echo "$(git rev-parse HEAD)" + echo "ref=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + - name: Cache Screenshots + id: screenshot-cache + uses: actions/cache@v5 + with: + path: build + key: screenshots-${{ steps.ref.outputs.ref }} - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v6 with: node-version: 24 cache: pnpm - - run: pnpm install - - run: pnpm exec playwright install --with-deps + - name: Install Dependencies + if: steps.screenshot-cache.outputs.cache-hit != 'true' + run: | + pnpm install + pnpm exec playwright install --with-deps - name: Capture Screenshots + if: steps.screenshot-cache.outputs.cache-hit != 'true' run: pnpm run test:screenshots - - uses: actions/upload-artifact@v6 + - uses: actions/upload-artifact@v7 with: name: baseline path: build @@ -32,18 +49,34 @@ jobs: candidate: name: Candidate runs-on: ubuntu-latest + outputs: + ref: ${{ steps.ref.outputs.ref }} steps: - uses: actions/checkout@v6 + - id: ref + run: | + echo "$(git rev-parse HEAD)" + echo "ref=$(git rev-parse HEAD)" >> "$GITHUB_OUTPUT" + - name: Cache Screenshots + id: screenshot-cache + uses: actions/cache@v5 + with: + path: build + key: screenshots-${{ steps.ref.outputs.ref }}-${{ matrix.name }} - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v6 with: node-version: 24 cache: pnpm - - run: pnpm install - - run: pnpm exec playwright install --with-deps + - name: Install Dependencies + if: steps.screenshot-cache.outputs.cache-hit != 'true' + run: | + pnpm install + pnpm exec playwright install --with-deps - name: Capture Screenshots - run: pnpm run test:screenshots - - uses: actions/upload-artifact@v6 + if: steps.screenshot-cache.outputs.cache-hit != 'true' + run: pnpm test:screenshots + - uses: actions/upload-artifact@v7 with: name: candidate path: build @@ -54,35 +87,68 @@ jobs: runs-on: ubuntu-latest env: OUTPUT_DIR: visual-diff-${{ github.event.pull_request.number }} + BASELINE_REF: ${{needs.baseline.outputs.ref}} + CANDIDATE_REF: ${{needs.candidate.outputs.ref}} steps: - uses: actions/checkout@v6 - - uses: actions/download-artifact@v7 - - run: ls -lh baseline candidate - - uses: pnpm/action-setup@v4 - uses: actions/setup-node@v6 with: node-version: 24 - - run: pnpm install - - run: pnpm build + - uses: pnpm/action-setup@v4 + - name: Restore Comparison Cache + id: comparison-cache + uses: actions/cache/restore@v5 + with: + path: | + ${{ env.OUTPUT_DIR }} + results + key: comparison-${{ env.BASELINE_REF }}-${{ env.CANDIDATE_REF }} + - uses: actions/download-artifact@v8 + if: steps.comparison-cache.outputs.cache-hit != 'true' + - run: ls -lh baseline candidate 2>/dev/null || true - run: | mkdir -p ./results echo ${{ github.event.pull_request.number }} > ./results/pr_number + - run: pnpm install + if: steps.comparison-cache.outputs.cache-hit != 'true' + - run: pnpm build + if: steps.comparison-cache.outputs.cache-hit != 'true' - name: Create Visual Diff - run: node ./dist/bin/visual-differ.js baseline candidate ${{ env.OUTPUT_DIR }} > results/visual-diff.txt - - if: always() - run: cp ${{ env.OUTPUT_DIR }}/report.md results/ - - if: always() - run: cat results/report.md results/visual-diff.txt - - uses: actions/upload-artifact@v6 + if: steps.comparison-cache.outputs.cache-hit != 'true' + run: | + set +e + node ./dist/bin/visual-differ.js --threshold=0.1 baseline candidate ${{ env.OUTPUT_DIR }} > results/visual-diff.txt + exit_code=$? + echo $exit_code > ./results/exit_code + cp ${{ env.OUTPUT_DIR }}/report.md results/ + cat results/report.md results/visual-diff.txt + - uses: actions/upload-artifact@v7 id: upload-output - if: always() with: name: ${{ env.OUTPUT_DIR }} path: ${{ env.OUTPUT_DIR }} - - if: always() - run: echo ${{ steps.upload-output.outputs.artifact-url }} > ./results/artifact_url - - uses: actions/upload-artifact@v6 - if: always() + - run: | + echo ${{ steps.upload-output.outputs.artifact-url }} > ./results/artifact_url + echo ${{ contains(github.event.pull_request.labels.*.name, 'approve visual diff') }} > ./results/approved + - name: Save Comparison Cache + if: steps.comparison-cache.outputs.cache-hit != 'true' + uses: actions/cache/save@v5 + with: + path: | + ${{ env.OUTPUT_DIR }} + results + key: ${{ steps.comparison-cache.outputs.cache-primary-key }} + - uses: actions/upload-artifact@v7 with: name: results path: results + - name: Check Status + run: | + exitCode=$(cat ./results/exit_code) + approved=$(cat ./results/approved) + echo "visual-differ exit code: $exitCode" + echo "approve visual diff label present: $approved" + if [ "$exitCode" != "0" ] && [ "$approved" != "true" ]; then + echo "Visual diff detected and 'approve visual diff' label not present." + exit 1 + fi diff --git a/.github/workflows/visual_diff_results.yml b/.github/workflows/visual_diff_results.yml index 922384c..3182f93 100644 --- a/.github/workflows/visual_diff_results.yml +++ b/.github/workflows/visual_diff_results.yml @@ -7,8 +7,39 @@ on: - completed jobs: + check-artifact: + name: Check for results artifact + runs-on: ubuntu-latest + permissions: + actions: read + outputs: + found: ${{ steps.check.outputs.found }} + steps: + - name: Check for "results" artifact + id: check + uses: actions/github-script@v8 + with: + github-token: ${{ secrets.GITHUB_TOKEN }} + script: | + const allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ + owner: context.repo.owner, + repo: context.repo.repo, + run_id: context.payload.workflow_run.id, + }); + + const match = allArtifacts.data.artifacts.find(a => a.name === 'results'); + + if (match) { + core.setOutput('found', 'true'); + } else { + core.setOutput('found', 'false'); + console.log('No artifact uploaded.'); + } + comment: name: Comment on Pull Request + needs: check-artifact + if: ${{ needs.check-artifact.outputs.found == 'true' }} runs-on: ubuntu-latest permissions: actions: read @@ -23,27 +54,33 @@ jobs: script: | const fs = require('fs'); const path = require('path'); + let allArtifacts = await github.rest.actions.listWorkflowRunArtifacts({ owner: context.repo.owner, repo: context.repo.repo, run_id: context.payload.workflow_run.id, }); + let matchArtifact = allArtifacts.data.artifacts.find((artifact) => { return artifact.name === 'results'; }); + let download = await github.rest.actions.downloadArtifact({ owner: context.repo.owner, repo: context.repo.repo, artifact_id: matchArtifact.id, archive_format: 'zip', }); + const temp = '${{ runner.temp }}/artifacts'; if (!fs.existsSync(temp)){ fs.mkdirSync(temp); } fs.writeFileSync(path.join(temp, 'results.zip'), Buffer.from(download.data)); + - name: Unzip artifact run: unzip "${{ runner.temp }}/artifacts/results.zip" -d "${{ runner.temp }}/artifacts" + - name: Extract Details uses: actions/github-script@v8 id: data @@ -52,49 +89,83 @@ jobs: const fs = require('fs'); const path = require('path'); const temp = '${{ runner.temp }}/artifacts'; + const report = fs.readFileSync(path.join(temp, 'report.md'), "utf8"); const issue_number = Number(fs.readFileSync(path.join(temp, 'pr_number'))); + const exit_code = Number(fs.readFileSync(path.join(temp, 'exit_code'))); + const approved = fs.readFileSync(path.join(temp, 'approved'), "utf8"); const url = fs.readFileSync(path.join(temp, 'artifact_url'), "utf8"); - return { report, issue_number, url }; + const rhett = { report, issue_number, exit_code, approved, url }; + console.log(rhett); + return rhett; + - name: Decode Output id: decoded run: | JSON='${{ steps.data.outputs.result }}' ISSUE_NUMBER="$(echo "$JSON" | jq -r '.issue_number')" + EXIT_CODE="$(echo "$JSON" | jq -r '.exit_code')" + APPROVED="$(echo "$JSON" | jq -r '.approved')" URL="$(echo "$JSON" | jq -r '.url')" REPORT="$(echo "$JSON" | jq -r '.report')" { echo "issue_number=$ISSUE_NUMBER" + echo "exit_code=$EXIT_CODE" + echo "approved=$APPROVED" echo "url=$URL" echo "report<> "$GITHUB_OUTPUT" + - name: Show Output run: | echo "issue_number:" echo "${{ steps.decoded.outputs.issue_number }}" + echo "exit_code:" + echo "${{ steps.decoded.outputs.exit_code }}" + + echo "approved:" + echo "${{ steps.decoded.outputs.approved }}" + echo "url:" echo "${{ steps.decoded.outputs.url }}" echo "report:" echo "${{ steps.decoded.outputs.report }}" + + - name: Approval Results + id: approval-results + run: | + exitCode="${{ steps.decoded.outputs.exit_code }}" + approved="${{ steps.decoded.outputs.approved }}" + echo "visual-differ exit code: $exitCode" + echo "approve visual diff label present: $approved" + if [ "$exitCode" != "0" ] && [ "$approved" == "true" ]; then + APPROVAL_RESULTS="### ✅ Visual Diff Approved" + else + APPROVAL_RESULTS="" + fi + echo "results=$APPROVAL_RESULTS" >> "$GITHUB_OUTPUT" + - name: Find Previous Comment uses: peter-evans/find-comment@v4 id: find with: issue-number: ${{ steps.decoded.outputs.issue_number }} body-includes: ${{ env.MARKER }} + - name: Create or Update Comment uses: peter-evans/create-or-update-comment@v5 with: comment-id: ${{ steps.find.outputs.comment-id }} issue-number: ${{ steps.decoded.outputs.issue_number }} body: | + ${{ steps.approval-results.outputs.results }} ${{ steps.decoded.outputs.report }} Download the [results](${{ steps.decoded.outputs.url }}).