From e2a4bce63e5d20e7ca7b12fe1ae1b097625d6858 Mon Sep 17 00:00:00 2001 From: Henning Dieterichs Date: Tue, 7 Apr 2026 12:57:16 +0200 Subject: [PATCH] Fail screenshot CI if fixtures with tag blocks-ci change --- .github/workflows/screenshot-test.yml | 54 +++++++++- build/lib/screenshotBlocksCi.ts | 99 +++++++++++++++++++ .../blocks-ci-screenshots.md | 31 ++++++ 3 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 build/lib/screenshotBlocksCi.ts create mode 100644 test/componentFixtures/blocks-ci-screenshots.md diff --git a/.github/workflows/screenshot-test.yml b/.github/workflows/screenshot-test.yml index 11ef3ec9f1d..bf52531b1d0 100644 --- a/.github/workflows/screenshot-test.yml +++ b/.github/workflows/screenshot-test.yml @@ -55,6 +55,52 @@ jobs: - name: Capture screenshots run: ./node_modules/.bin/component-explorer render --project ./test/componentFixtures/component-explorer.json + - name: Check blocks-ci screenshots + id: blocks-ci + run: | + node build/lib/screenshotBlocksCi.ts \ + test/componentFixtures/.screenshots/current/manifest.json \ + test/componentFixtures/blocks-ci-screenshots.md \ + https://hediet-screenshots.azurewebsites.net \ + --json \ + > /tmp/blocks-ci-diff.json 2>/tmp/blocks-ci-stderr.txt \ + && echo "match=true" >> "$GITHUB_OUTPUT" \ + || { + echo "match=false" >> "$GITHUB_OUTPUT" + cat /tmp/blocks-ci-stderr.txt >&2 + DIFF=$(cat /tmp/blocks-ci-diff.json) + echo "diff<> "$GITHUB_OUTPUT" + echo "$DIFF" >> "$GITHUB_OUTPUT" + echo "BLOCKS_CI_EOF" >> "$GITHUB_OUTPUT" + } + + - name: Suggest blocks-ci screenshot update + if: github.event_name == 'pull_request' && steps.blocks-ci.outputs.match == 'false' + uses: actions/github-script@v7 + with: + script: | + const diff = JSON.parse(process.env.DIFF_JSON); + const filePath = 'test/componentFixtures/blocks-ci-screenshots.md'; + const marker = ''; + + await github.rest.pulls.createReview({ + owner: context.repo.owner, + repo: context.repo.repo, + pull_number: context.issue.number, + commit_id: context.payload.pull_request.head.sha, + event: 'COMMENT', + body: `${marker}\n### blocks-ci screenshots changed\n\nApply the suggestion below to update the committed screenshots.`, + comments: [{ + path: filePath, + start_line: diff.startLine < diff.endLine ? diff.startLine : undefined, + line: diff.endLine, + side: 'RIGHT', + body: `${marker}\nScreenshots for \`blocks-ci\` fixtures have changed. Apply this suggestion to accept:\n\n\`\`\`suggestion\n${diff.replacement}\n\`\`\``, + }], + }); + env: + DIFF_JSON: ${{ steps.blocks-ci.outputs.diff }} + - name: Upload screenshots uses: actions/upload-artifact@v7 with: @@ -70,7 +116,7 @@ jobs: echo "::add-mask::$TOKEN" echo "token=$TOKEN" >> "$GITHUB_OUTPUT" - - name: Upload screenshots + - name: Upload screenshots to service run: | cd test/componentFixtures/.screenshots/current zip -qr "$GITHUB_WORKSPACE/screenshots.zip" . @@ -135,6 +181,12 @@ jobs: env: COMMENT_BODY: ${{ steps.diff.outputs.body }} + - name: Fail if blocks-ci hashes changed + if: steps.blocks-ci.outputs.match == 'false' + run: | + echo "::error::blocks-ci screenshot hashes do not match committed file. See PR review suggestion to update." + exit 1 + # - name: Compare screenshots # id: compare # run: | diff --git a/build/lib/screenshotBlocksCi.ts b/build/lib/screenshotBlocksCi.ts new file mode 100644 index 00000000000..7fdbf5b371a --- /dev/null +++ b/build/lib/screenshotBlocksCi.ts @@ -0,0 +1,99 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +// Compares blocks-ci fixture screenshots from a rendered manifest against the committed markdown. +// Usage: node build/lib/screenshotBlocksCi.ts [--json] +// +// Exit codes: +// 0 — all screenshots match (or no blocks-ci fixtures) +// 1 — mismatches found +// +// Without --json: prints the full updated markdown to stdout. +// With --json: prints a JSON object with { startLine, endLine, replacement } for a GitHub suggestion. + +import * as fs from 'fs'; + +const HEADER = ''; + +interface ManifestFixture { + readonly fixtureId: string; + readonly imageHash: string; + readonly labels?: readonly string[]; +} + +interface Manifest { + readonly fixtures: readonly ManifestFixture[]; +} + +function generateMarkdown(fixtures: readonly ManifestFixture[], serviceUrl: string): string { + const lines: string[] = [HEADER, '']; + const seen = new Set(); + const sorted = [...fixtures].sort((a, b) => a.fixtureId.localeCompare(b.fixtureId)); + for (const f of sorted) { + if (f.labels?.includes('blocks-ci') && !seen.has(f.fixtureId)) { + seen.add(f.fixtureId); + lines.push(`#### ${f.fixtureId}`); + lines.push(`![screenshot](${serviceUrl}/images/${f.imageHash})`); + lines.push(''); + } + } + return lines.join('\n'); +} + +function computeDiff(committedLines: string[], updatedLines: string[]): { startLine: number; endLine: number; replacement: string } { + let firstDiff = 0; + while (firstDiff < committedLines.length && firstDiff < updatedLines.length && committedLines[firstDiff] === updatedLines[firstDiff]) { + firstDiff++; + } + + let committedEnd = committedLines.length - 1; + let updatedEnd = updatedLines.length - 1; + while (committedEnd > firstDiff && updatedEnd > firstDiff && committedLines[committedEnd] === updatedLines[updatedEnd]) { + committedEnd--; + updatedEnd--; + } + + return { + startLine: firstDiff + 1, // 1-indexed + endLine: committedEnd + 1, // 1-indexed + replacement: updatedLines.slice(firstDiff, updatedEnd + 1).join('\n'), + }; +} + +function main(): void { + const args = process.argv.slice(2); + const jsonMode = args.includes('--json'); + const positional = args.filter(a => a !== '--json'); + const [manifestPath, markdownPath, serviceUrl] = positional; + + if (!manifestPath || !markdownPath || !serviceUrl) { + console.error('Usage: node build/lib/screenshotBlocksCi.ts [--json]'); + process.exit(1); + } + + const manifest: Manifest = JSON.parse(fs.readFileSync(manifestPath, 'utf8')); + const actual = generateMarkdown(manifest.fixtures, serviceUrl); + + let committed = ''; + if (fs.existsSync(markdownPath)) { + committed = fs.readFileSync(markdownPath, 'utf8'); + } + + if (actual === committed) { + process.exit(0); + } + + console.error('blocks-ci screenshots have changed.'); + + if (jsonMode) { + const diff = computeDiff(committed.split('\n'), actual.split('\n')); + process.stdout.write(JSON.stringify(diff) + '\n'); + } else { + process.stdout.write(actual); + } + process.exit(1); +} + +main(); diff --git a/test/componentFixtures/blocks-ci-screenshots.md b/test/componentFixtures/blocks-ci-screenshots.md new file mode 100644 index 00000000000..f8bd9432b36 --- /dev/null +++ b/test/componentFixtures/blocks-ci-screenshots.md @@ -0,0 +1,31 @@ + + +#### chat/aiCustomizations/aiCustomizationManagementEditor/AgentsTabNarrow/Dark +![screenshot](https://hediet-screenshots.azurewebsites.net/images/5dde8b97d2124e8277c145e6fe118fd99dc3d82715311f968afae09d6682856a) + +#### chat/aiCustomizations/aiCustomizationManagementEditor/AgentsTabNarrow/Light +![screenshot](https://hediet-screenshots.azurewebsites.net/images/5dde8b97d2124e8277c145e6fe118fd99dc3d82715311f968afae09d6682856a) + +#### chat/aiCustomizations/aiCustomizationManagementEditor/LocalHarness/Dark +![screenshot](https://hediet-screenshots.azurewebsites.net/images/89c6f26c1eae112fda0cc2a6acd5fde5960779ff9000d84049674e5cb4991073) + +#### chat/aiCustomizations/aiCustomizationManagementEditor/LocalHarness/Light +![screenshot](https://hediet-screenshots.azurewebsites.net/images/89c6f26c1eae112fda0cc2a6acd5fde5960779ff9000d84049674e5cb4991073) + +#### chat/aiCustomizations/aiCustomizationManagementEditor/McpServersTab/Dark +![screenshot](https://hediet-screenshots.azurewebsites.net/images/89c6f26c1eae112fda0cc2a6acd5fde5960779ff9000d84049674e5cb4991073) + +#### chat/aiCustomizations/aiCustomizationManagementEditor/McpServersTab/Light +![screenshot](https://hediet-screenshots.azurewebsites.net/images/89c6f26c1eae112fda0cc2a6acd5fde5960779ff9000d84049674e5cb4991073) + +#### chat/aiCustomizations/aiCustomizationManagementEditor/McpServersTabNarrow/Dark +![screenshot](https://hediet-screenshots.azurewebsites.net/images/5dde8b97d2124e8277c145e6fe118fd99dc3d82715311f968afae09d6682856a) + +#### chat/aiCustomizations/aiCustomizationManagementEditor/McpServersTabNarrow/Light +![screenshot](https://hediet-screenshots.azurewebsites.net/images/5dde8b97d2124e8277c145e6fe118fd99dc3d82715311f968afae09d6682856a) + +#### editor/codeEditor/CodeEditor/Dark +![screenshot](https://hediet-screenshots.azurewebsites.net/images/0df99bccb101cf98033b2ee803213bcf385195eccac8cf151f0806f51daf77a2) + +#### editor/codeEditor/CodeEditor/Light +![screenshot](https://hediet-screenshots.azurewebsites.net/images/0f270976bd9a821ecf24858003089206f5aed1dd5465fe0c545d81fa8f8b4cbe)