Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
microsoft
GitHub Repository: microsoft/vscode
Path: blob/main/build/lib/screenshotDiffReport.ts
13379 views
1
/*---------------------------------------------------------------------------------------------
2
* Copyright (c) Microsoft Corporation. All rights reserved.
3
* Licensed under the MIT License. See License.txt in the project root for license information.
4
*--------------------------------------------------------------------------------------------*/
5
6
// Fetches a screenshot diff from the service and prints the PR comment markdown to stdout.
7
// Usage: node build/lib/screenshotDiffReport.ts <service-url> <owner> <repo> <base-sha> <current-sha>
8
// Outputs nothing (exit 0) when there are no visual changes.
9
10
import * as fs from 'fs';
11
import * as path from 'path';
12
import { fileURLToPath } from 'url';
13
14
const __dirname = path.dirname(fileURLToPath(import.meta.url));
15
16
const COMMENT_MARKER = '<!-- screenshot-diff-report -->';
17
const EXPAND_FIRST_N = 5;
18
const EXCLUDED_LABELS = new Set(['animated', 'flaky']);
19
20
interface CompareEntry {
21
readonly fixtureId: string;
22
readonly imageUrl: string;
23
readonly labels?: readonly string[];
24
readonly changeCount?: number;
25
}
26
27
interface CompareChangedEntry {
28
readonly fixtureId: string;
29
readonly beforeImageUrl: string;
30
readonly afterImageUrl: string;
31
readonly labels?: readonly string[];
32
readonly changeCount?: number;
33
}
34
35
interface CompareResult {
36
readonly baseCommitSha: string;
37
readonly added: readonly CompareEntry[];
38
readonly removed: readonly CompareEntry[];
39
readonly changed: readonly CompareChangedEntry[];
40
readonly unchanged: readonly CompareEntry[];
41
}
42
43
function shouldIncludeInReport(labels: readonly string[] | undefined): boolean {
44
return !labels?.some(l => EXCLUDED_LABELS.has(l));
45
}
46
47
function generateMarkdown(result: CompareResult, baseSha: string, currentSha: string): string {
48
const changed = result.changed.filter(e => shouldIncludeInReport(e.labels));
49
const added = result.added.filter(e => shouldIncludeInReport(e.labels));
50
const removed = result.removed.filter(e => shouldIncludeInReport(e.labels));
51
52
if (changed.length === 0 && added.length === 0 && removed.length === 0) {
53
return '';
54
}
55
56
const lines: string[] = [];
57
58
lines.push('## Screenshot Changes');
59
lines.push('');
60
lines.push(`**Base:** \`${baseSha.slice(0, 8)}\` **Current:** \`${currentSha.slice(0, 8)}\``);
61
lines.push('');
62
63
if (changed.length > 0) {
64
lines.push(`### Changed (${changed.length})`);
65
lines.push('');
66
for (let i = 0; i < changed.length; i++) {
67
const entry = changed[i];
68
const open = i < EXPAND_FIRST_N ? ' open' : '';
69
lines.push(`<details${open}><summary><code>${entry.fixtureId}</code></summary>`);
70
lines.push('');
71
lines.push('| Before | After |');
72
lines.push('|--------|-------|');
73
lines.push(`| ![before](${entry.beforeImageUrl}) | ![after](${entry.afterImageUrl}) |`);
74
lines.push('');
75
lines.push('</details>');
76
lines.push('');
77
}
78
}
79
80
if (added.length > 0) {
81
lines.push(`### Added (${added.length})`);
82
lines.push('');
83
for (let i = 0; i < added.length; i++) {
84
const entry = added[i];
85
const open = i < EXPAND_FIRST_N ? ' open' : '';
86
lines.push(`<details${open}><summary><code>${entry.fixtureId}</code></summary>`);
87
lines.push('');
88
lines.push(`![current](${entry.imageUrl})`);
89
lines.push('');
90
lines.push('</details>');
91
lines.push('');
92
}
93
}
94
95
if (removed.length > 0) {
96
lines.push(`### Removed (${removed.length})`);
97
lines.push('');
98
for (let i = 0; i < removed.length; i++) {
99
const entry = removed[i];
100
const open = i < EXPAND_FIRST_N ? ' open' : '';
101
lines.push(`<details${open}><summary><code>${entry.fixtureId}</code></summary>`);
102
lines.push('');
103
lines.push(`![baseline](${entry.imageUrl})`);
104
lines.push('');
105
lines.push('</details>');
106
lines.push('');
107
}
108
}
109
110
return lines.join('\n');
111
}
112
113
async function fetchCompare(serviceUrl: string, owner: string, repo: string, baseSha: string, currentSha: string): Promise<CompareResult> {
114
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
115
const token = process.env.SCREENSHOT_SERVICE_TOKEN;
116
if (token) {
117
headers['Authorization'] = `Bearer ${token}`;
118
}
119
const response = await fetch(`${serviceUrl}/compare`, {
120
method: 'POST',
121
headers,
122
body: JSON.stringify({ owner, repo, baseCommitSha: baseSha, currentCommitSha: currentSha }),
123
});
124
125
if (!response.ok) {
126
const body = await response.json().catch(() => ({})) as { error?: string };
127
throw new Error(body.error ?? `Service returned ${response.status}`);
128
}
129
130
const result = await response.json() as CompareResult;
131
132
// Write result to .tmp for debugging
133
const tmpDir = path.join(__dirname, '../../.tmp');
134
fs.mkdirSync(tmpDir, { recursive: true });
135
fs.writeFileSync(path.join(tmpDir, 'screenshotDiffReport.json'), JSON.stringify(result, null, 2));
136
137
return result;
138
}
139
140
async function main(): Promise<void> {
141
const [serviceUrl, owner, repo, baseSha, currentSha] = process.argv.slice(2);
142
if (!serviceUrl || !owner || !repo || !baseSha || !currentSha) {
143
console.error('Usage: node build/lib/screenshotDiffReport.ts <service-url> <owner> <repo> <base-sha> <current-sha>');
144
process.exit(1);
145
}
146
147
const result = await fetchCompare(serviceUrl, owner, repo, baseSha, currentSha);
148
149
console.error(`Compare result: ${result.changed.length} changed, ${result.added.length} added, ${result.removed.length} removed, ${result.unchanged.length} unchanged`);
150
151
const markdown = generateMarkdown(result, baseSha, currentSha);
152
153
if (!markdown) {
154
console.error('No reportable changes (all entries may be excluded by labels).');
155
process.exit(0);
156
}
157
158
process.stdout.write(`${COMMENT_MARKER}\n${markdown}`);
159
}
160
161
main().catch(err => {
162
console.error(err instanceof Error ? err.message : err);
163
process.exit(1);
164
});
165
166