@tempo-ai/visual-review 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +35 -0
- package/dist/bin.js +31 -0
- package/dist/bin.js.map +1 -0
- package/dist/chunk-4QAZFBC3.js +13586 -0
- package/dist/chunk-4QAZFBC3.js.map +1 -0
- package/dist/chunk-DJGBIRN5.js +146 -0
- package/dist/chunk-DJGBIRN5.js.map +1 -0
- package/dist/chunk-EYRAT56C.js +42 -0
- package/dist/chunk-EYRAT56C.js.map +1 -0
- package/dist/chunk-HMPQK5ZG.js +288 -0
- package/dist/chunk-HMPQK5ZG.js.map +1 -0
- package/dist/chunk-QLAJNWA2.js +9 -0
- package/dist/chunk-QLAJNWA2.js.map +1 -0
- package/dist/chunk-W7PBID4V.js +38 -0
- package/dist/chunk-W7PBID4V.js.map +1 -0
- package/dist/claim-WI2ZVIDS.js +14 -0
- package/dist/claim-WI2ZVIDS.js.map +1 -0
- package/dist/cli-3HH2BY7Y.js +16 -0
- package/dist/cli-3HH2BY7Y.js.map +1 -0
- package/dist/index.d.ts +245 -0
- package/dist/index.js +69 -0
- package/dist/index.js.map +1 -0
- package/dist/install-browsers-5RCJN3AK.js +8 -0
- package/dist/install-browsers-5RCJN3AK.js.map +1 -0
- package/dist/report-RCKB2CVS.js +9 -0
- package/dist/report-RCKB2CVS.js.map +1 -0
- package/package.json +52 -0
- package/templates/visual-review.yml +169 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,245 @@
|
|
|
1
|
+
export { CaptureSession, looksBlank, sha256Hex, startCaptureSession, storyboardRenderId } from './src/capture';
|
|
2
|
+
export { CHROMIUM_LAUNCH_ARGS, FREEZE_MOTION_CSS, FROZEN_EPOCH_MS, buildFreezeInitScript, settlePage } from './src/determinism';
|
|
3
|
+
export { DevserverSession, startDevserverSession } from './src/devserver';
|
|
4
|
+
export { DEVICE_SCALE_FACTOR, FONTS_BUNDLE_VERSION, KIT_VERSION, buildEnvFingerprint } from './src/fingerprint';
|
|
5
|
+
export { TimingReport, percentile, summarize } from './src/timing';
|
|
6
|
+
import { CaptureStatus } from './src/types';
|
|
7
|
+
export { CaptureStatus, EnvFingerprint } from './src/types';
|
|
8
|
+
|
|
9
|
+
declare function main$3(argv?: string[]): Promise<number>;
|
|
10
|
+
|
|
11
|
+
type VrtIgnoreRegion = {
|
|
12
|
+
x: number;
|
|
13
|
+
y: number;
|
|
14
|
+
w: number;
|
|
15
|
+
h: number;
|
|
16
|
+
};
|
|
17
|
+
/** One tracked storyboard, as enumerated from the canvas AST. */
|
|
18
|
+
type TrackedStoryboard = {
|
|
19
|
+
/** Canvas page folder path relative to the tempo dir — the store's
|
|
20
|
+
* `canvasPath` and the Design tab's comment `contentPath`, e.g.
|
|
21
|
+
* "designs/canvases/buttons". */
|
|
22
|
+
canvasPath: string;
|
|
23
|
+
/** The canvas file path relative to the PROJECT root (the canvas-host
|
|
24
|
+
* module key), e.g. "tempo/designs/canvases/buttons/index.canvas.tsx" —
|
|
25
|
+
* what the direct-render URL's `canvasPath` param takes. */
|
|
26
|
+
hostCanvasPath: string;
|
|
27
|
+
storyboardId: string;
|
|
28
|
+
name?: string;
|
|
29
|
+
layout: {
|
|
30
|
+
x: number;
|
|
31
|
+
y: number;
|
|
32
|
+
width: number;
|
|
33
|
+
height: number;
|
|
34
|
+
};
|
|
35
|
+
threshold?: number;
|
|
36
|
+
ignore?: VrtIgnoreRegion[];
|
|
37
|
+
};
|
|
38
|
+
type EnumerationResult = {
|
|
39
|
+
tracked: TrackedStoryboard[];
|
|
40
|
+
/** Tracked but `vrt={{ skip: true }}` — paused: rides the manifest with
|
|
41
|
+
* `skip: true` (so pausing is not misread as deletion) but is neither
|
|
42
|
+
* captured nor gated. */
|
|
43
|
+
paused: TrackedStoryboard[];
|
|
44
|
+
/** Total storyboards seen (tracked or not) — for the log line. */
|
|
45
|
+
totalStoryboards: number;
|
|
46
|
+
totalCanvases: number;
|
|
47
|
+
/** Structural parse errors — enumeration is AST truth, so these fail the
|
|
48
|
+
* run loudly instead of silently shrinking the manifest. */
|
|
49
|
+
errors: Array<{
|
|
50
|
+
filePath: string;
|
|
51
|
+
message: string;
|
|
52
|
+
}>;
|
|
53
|
+
};
|
|
54
|
+
type RunManifest = {
|
|
55
|
+
repoKey: string;
|
|
56
|
+
githubRepo?: string;
|
|
57
|
+
headSha: string;
|
|
58
|
+
branch: string;
|
|
59
|
+
trigger: "pr" | "main" | "manual";
|
|
60
|
+
prNumber?: number;
|
|
61
|
+
baseCandidates: string[];
|
|
62
|
+
/** Opaque lease ownership proof when this run is publishing a baseline
|
|
63
|
+
* claimed before rendering began. */
|
|
64
|
+
baselineClaimToken?: string;
|
|
65
|
+
envFingerprint: string;
|
|
66
|
+
expected: Array<TrackedStoryboard["layout"] & {
|
|
67
|
+
canvasPath: string;
|
|
68
|
+
storyboardId: string;
|
|
69
|
+
name?: string;
|
|
70
|
+
threshold?: number;
|
|
71
|
+
ignore?: VrtIgnoreRegion[];
|
|
72
|
+
/** Paused (`vrt={{ skip: true }}`) — in the manifest, not gated. */
|
|
73
|
+
skip?: boolean;
|
|
74
|
+
}>;
|
|
75
|
+
snapshots: Array<{
|
|
76
|
+
canvasPath: string;
|
|
77
|
+
storyboardId: string;
|
|
78
|
+
pngHash?: string;
|
|
79
|
+
htmlHash?: string;
|
|
80
|
+
status: CaptureStatus;
|
|
81
|
+
error?: string;
|
|
82
|
+
}>;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* The producer credential. In production (and any customer repo) this is a
|
|
87
|
+
* GitHub Actions OIDC token minted with `audience: tempo-visual-review`; the
|
|
88
|
+
* global shared secret is accepted only by non-prod deployments (previews,
|
|
89
|
+
* e2e, local capture). See the Convex `visualReviewAuthenticate` gate.
|
|
90
|
+
*/
|
|
91
|
+
type IngestCredential = {
|
|
92
|
+
kind: "oidc";
|
|
93
|
+
value: string;
|
|
94
|
+
} | {
|
|
95
|
+
kind: "secret";
|
|
96
|
+
value: string;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* A credential source. GitHub OIDC id-tokens expire 5 minutes after minting,
|
|
100
|
+
* and a capture run spans many minutes between its first authenticated call
|
|
101
|
+
* (the mid-capture baseline fetch) and its last (finalize) — so the client
|
|
102
|
+
* resolves the credential PER REQUEST from a provider that can re-mint,
|
|
103
|
+
* instead of pinning one token at construction.
|
|
104
|
+
*/
|
|
105
|
+
type IngestCredentialProvider = () => Promise<IngestCredential>;
|
|
106
|
+
type BaselineClaimResult = {
|
|
107
|
+
status: "unsupported";
|
|
108
|
+
reason?: string;
|
|
109
|
+
} | {
|
|
110
|
+
status: "ready";
|
|
111
|
+
commitSha: string;
|
|
112
|
+
} | {
|
|
113
|
+
status: "capturing";
|
|
114
|
+
commitSha: string;
|
|
115
|
+
leaseExpiresAt: number;
|
|
116
|
+
} | {
|
|
117
|
+
status: "claimed";
|
|
118
|
+
commitSha: string;
|
|
119
|
+
leaseExpiresAt: number;
|
|
120
|
+
};
|
|
121
|
+
type BaselineClaimIdentity = {
|
|
122
|
+
repoKey: string;
|
|
123
|
+
commitSha: string;
|
|
124
|
+
envFingerprint: string;
|
|
125
|
+
ownerToken: string;
|
|
126
|
+
};
|
|
127
|
+
declare class VisualReviewHttpError extends Error {
|
|
128
|
+
readonly status: number;
|
|
129
|
+
constructor(message: string, status: number);
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Client for the Convex `/visual-review/*` ingest routes (TEM-1775 §4.3):
|
|
133
|
+
* hash-negotiated, idempotent, resumable. A retried CI job re-uploads
|
|
134
|
+
* nothing (negotiate reports every hash present) and re-finalizes safely
|
|
135
|
+
* (the mutation dedups on repo+sha+env+trigger+branch).
|
|
136
|
+
*/
|
|
137
|
+
declare class VisualReviewIngestClient {
|
|
138
|
+
/** Convex site origin, e.g. https://dapper-heron-945.convex.site */
|
|
139
|
+
private readonly baseUrl;
|
|
140
|
+
private readonly credential;
|
|
141
|
+
private lastCredentialKind;
|
|
142
|
+
constructor(baseUrl: string, credential: IngestCredential | IngestCredentialProvider);
|
|
143
|
+
private authHeader;
|
|
144
|
+
private request;
|
|
145
|
+
/** Best-effort read of the typed `reason`/`error` from an error body. */
|
|
146
|
+
private errorReason;
|
|
147
|
+
/** → the subset of `hashes` the store lacks. */
|
|
148
|
+
negotiate(hashes: string[]): Promise<string[]>;
|
|
149
|
+
uploadBlob(hash: string, bytes: Uint8Array, size: {
|
|
150
|
+
width: number;
|
|
151
|
+
height: number;
|
|
152
|
+
}): Promise<void>;
|
|
153
|
+
claimBaseline(input: {
|
|
154
|
+
repoKey: string;
|
|
155
|
+
commitSha: string;
|
|
156
|
+
envFingerprint: string;
|
|
157
|
+
ownerToken: string;
|
|
158
|
+
}): Promise<BaselineClaimResult>;
|
|
159
|
+
renewBaselineClaim(input: BaselineClaimIdentity): Promise<{
|
|
160
|
+
renewed: true;
|
|
161
|
+
leaseExpiresAt: number;
|
|
162
|
+
}>;
|
|
163
|
+
failBaselineClaim(input: Omit<BaselineClaimIdentity, "envFingerprint"> & {
|
|
164
|
+
envFingerprint?: string;
|
|
165
|
+
error: string;
|
|
166
|
+
}): Promise<{
|
|
167
|
+
released: boolean;
|
|
168
|
+
}>;
|
|
169
|
+
baselineManifest(repoKey: string, commitSha: string, envFingerprint: string): Promise<Pick<RunManifest, "expected" | "snapshots">>;
|
|
170
|
+
finalize(manifest: RunManifest): Promise<{
|
|
171
|
+
runId: string;
|
|
172
|
+
reportKey: string;
|
|
173
|
+
state: string;
|
|
174
|
+
deduped: boolean;
|
|
175
|
+
reportUrl: string;
|
|
176
|
+
statusUrl: string;
|
|
177
|
+
}>;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
declare function resolveOrClaimBaseline(options: {
|
|
181
|
+
ingest: VisualReviewIngestClient;
|
|
182
|
+
repoKey: string;
|
|
183
|
+
commitSha: string;
|
|
184
|
+
envFingerprint: string;
|
|
185
|
+
ownerToken: string;
|
|
186
|
+
log: (line: string) => void;
|
|
187
|
+
waitTimeoutMs?: number;
|
|
188
|
+
pollIntervalMs?: number;
|
|
189
|
+
now?: () => number;
|
|
190
|
+
sleep?: (ms: number) => Promise<void>;
|
|
191
|
+
}): Promise<BaselineClaimResult>;
|
|
192
|
+
declare function main$2(argv?: string[]): Promise<number>;
|
|
193
|
+
|
|
194
|
+
/** The production Convex HTTP origin customers ingest visual-review runs to. */
|
|
195
|
+
declare const DEFAULT_VISUAL_REVIEW_INGEST_URL = "https://greedy-jackal-526.convex.site";
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Enumerates the tracked set (TEM-1775 §3.2): every `<Storyboard>` under the
|
|
199
|
+
* canvases root carrying a literal `vrt` prop, minus `skip: true`. Route and
|
|
200
|
+
* html storyboards are out of scope in v1 — the parser only attaches `vrt`
|
|
201
|
+
* to component storyboards, so they can never enter the manifest.
|
|
202
|
+
*
|
|
203
|
+
* The generic AST walk lives in `@tempo-modules/canvas-capture-headless`;
|
|
204
|
+
* this layers the vrt POLICY on top:
|
|
205
|
+
* - Structural parse errors FAIL enumeration for that file (its storyboards
|
|
206
|
+
* can't be trusted); they are returned for the caller to report loudly
|
|
207
|
+
* rather than silently shrinking the tracked set.
|
|
208
|
+
* - A malformed `vrt` declaration (dynamic expression, bad threshold,
|
|
209
|
+
* broken ignore region) is DEGRADED to the editor but FATAL here: the
|
|
210
|
+
* author expressed tracking intent, and silently proceeding would either
|
|
211
|
+
* drop the board from the gate or gate it with different settings than
|
|
212
|
+
* written.
|
|
213
|
+
* - An ignore configuration masking ≥90% of the board is fatal — "tracked"
|
|
214
|
+
* must not be a lie.
|
|
215
|
+
*/
|
|
216
|
+
declare function enumerateTrackedStoryboards(options: {
|
|
217
|
+
/** The tempo dir (holding tempo.config.json), e.g. `<repo>/tempo`. */
|
|
218
|
+
tempoDir: string;
|
|
219
|
+
/** The canvases root, e.g. `<repo>/tempo/designs`. */
|
|
220
|
+
pagesDir: string;
|
|
221
|
+
}): Promise<EnumerationResult>;
|
|
222
|
+
|
|
223
|
+
/**
|
|
224
|
+
* Authors declare `vrt.ignore` regions in storyboard-local CSS px; the
|
|
225
|
+
* compare engine masks the DEVICE-pixel snapshot. The run manifest carries
|
|
226
|
+
* device px — this is the single conversion point.
|
|
227
|
+
*/
|
|
228
|
+
declare function scaleIgnoreRegionsToDevicePx(regions: VrtIgnoreRegion[] | undefined): VrtIgnoreRegion[] | undefined;
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* `tempo-visual-review install-browsers` — install the Chromium build that
|
|
232
|
+
* matches the playwright THIS package depends on.
|
|
233
|
+
*
|
|
234
|
+
* A bare `npx playwright install` resolves registry-latest playwright and can
|
|
235
|
+
* fetch a browser revision the pinned driver cannot launch; resolving the
|
|
236
|
+
* installer from our own dependency tree removes that drift for every
|
|
237
|
+
* consumer of the generated workflow. Linux adds `--with-deps` (Chromium's
|
|
238
|
+
* shared libraries via apt) because CI VMs are ephemeral; other platforms
|
|
239
|
+
* already carry those libraries.
|
|
240
|
+
*/
|
|
241
|
+
declare function main$1(argv?: string[]): Promise<number>;
|
|
242
|
+
|
|
243
|
+
declare function main(argv?: string[]): Promise<number>;
|
|
244
|
+
|
|
245
|
+
export { type BaselineClaimIdentity, type BaselineClaimResult, DEFAULT_VISUAL_REVIEW_INGEST_URL, type EnumerationResult, type RunManifest, type TrackedStoryboard, VisualReviewHttpError, VisualReviewIngestClient, type VrtIgnoreRegion, enumerateTrackedStoryboards, resolveOrClaimBaseline, main$2 as runCaptureCli, main$3 as runClaimCli, main$1 as runInstallBrowsersCli, main as runReportCli, scaleIgnoreRegionsToDevicePx };
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createRequire as __tempoCreateRequire } from 'node:module'; const require = __tempoCreateRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
main
|
|
4
|
+
} from "./chunk-DJGBIRN5.js";
|
|
5
|
+
import {
|
|
6
|
+
CHROMIUM_LAUNCH_ARGS,
|
|
7
|
+
DEVICE_SCALE_FACTOR,
|
|
8
|
+
FONTS_BUNDLE_VERSION,
|
|
9
|
+
FREEZE_MOTION_CSS,
|
|
10
|
+
FROZEN_EPOCH_MS,
|
|
11
|
+
KIT_VERSION,
|
|
12
|
+
TimingReport,
|
|
13
|
+
VisualReviewHttpError,
|
|
14
|
+
VisualReviewIngestClient,
|
|
15
|
+
buildEnvFingerprint,
|
|
16
|
+
buildFreezeInitScript,
|
|
17
|
+
enumerateTrackedStoryboards,
|
|
18
|
+
looksBlank,
|
|
19
|
+
main as main2,
|
|
20
|
+
percentile,
|
|
21
|
+
resolveOrClaimBaseline,
|
|
22
|
+
scaleIgnoreRegionsToDevicePx,
|
|
23
|
+
settlePage,
|
|
24
|
+
sha256Hex,
|
|
25
|
+
startCaptureSession,
|
|
26
|
+
startDevserverSession,
|
|
27
|
+
storyboardRenderId,
|
|
28
|
+
summarize
|
|
29
|
+
} from "./chunk-4QAZFBC3.js";
|
|
30
|
+
import {
|
|
31
|
+
DEFAULT_VISUAL_REVIEW_INGEST_URL
|
|
32
|
+
} from "./chunk-QLAJNWA2.js";
|
|
33
|
+
import {
|
|
34
|
+
main as main3
|
|
35
|
+
} from "./chunk-EYRAT56C.js";
|
|
36
|
+
import {
|
|
37
|
+
main as main4
|
|
38
|
+
} from "./chunk-HMPQK5ZG.js";
|
|
39
|
+
import "./chunk-W7PBID4V.js";
|
|
40
|
+
export {
|
|
41
|
+
CHROMIUM_LAUNCH_ARGS,
|
|
42
|
+
DEFAULT_VISUAL_REVIEW_INGEST_URL,
|
|
43
|
+
DEVICE_SCALE_FACTOR,
|
|
44
|
+
FONTS_BUNDLE_VERSION,
|
|
45
|
+
FREEZE_MOTION_CSS,
|
|
46
|
+
FROZEN_EPOCH_MS,
|
|
47
|
+
KIT_VERSION,
|
|
48
|
+
TimingReport,
|
|
49
|
+
VisualReviewHttpError,
|
|
50
|
+
VisualReviewIngestClient,
|
|
51
|
+
buildEnvFingerprint,
|
|
52
|
+
buildFreezeInitScript,
|
|
53
|
+
enumerateTrackedStoryboards,
|
|
54
|
+
looksBlank,
|
|
55
|
+
percentile,
|
|
56
|
+
resolveOrClaimBaseline,
|
|
57
|
+
main2 as runCaptureCli,
|
|
58
|
+
main as runClaimCli,
|
|
59
|
+
main3 as runInstallBrowsersCli,
|
|
60
|
+
main4 as runReportCli,
|
|
61
|
+
scaleIgnoreRegionsToDevicePx,
|
|
62
|
+
settlePage,
|
|
63
|
+
sha256Hex,
|
|
64
|
+
startCaptureSession,
|
|
65
|
+
startDevserverSession,
|
|
66
|
+
storyboardRenderId,
|
|
67
|
+
summarize
|
|
68
|
+
};
|
|
69
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { createRequire as __tempoCreateRequire } from 'node:module'; const require = __tempoCreateRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
main
|
|
4
|
+
} from "./chunk-HMPQK5ZG.js";
|
|
5
|
+
import "./chunk-W7PBID4V.js";
|
|
6
|
+
export {
|
|
7
|
+
main
|
|
8
|
+
};
|
|
9
|
+
//# sourceMappingURL=report-RCKB2CVS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":[],"sourcesContent":[],"mappings":"","names":[]}
|
package/package.json
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@tempo-ai/visual-review",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"bin": {
|
|
6
|
+
"tempo-visual-review": "./dist/bin.js"
|
|
7
|
+
},
|
|
8
|
+
"files": [
|
|
9
|
+
"dist/",
|
|
10
|
+
"templates/",
|
|
11
|
+
"README.md"
|
|
12
|
+
],
|
|
13
|
+
"exports": {
|
|
14
|
+
".": {
|
|
15
|
+
"import": "./dist/index.js",
|
|
16
|
+
"types": "./dist/index.d.ts"
|
|
17
|
+
}
|
|
18
|
+
},
|
|
19
|
+
"publishConfig": {
|
|
20
|
+
"access": "public"
|
|
21
|
+
},
|
|
22
|
+
"repository": {
|
|
23
|
+
"type": "git",
|
|
24
|
+
"url": "https://github.com/TempoLabsAI/tempo-monorepo.git",
|
|
25
|
+
"directory": "tempo-modules/visual-review"
|
|
26
|
+
},
|
|
27
|
+
"engines": {
|
|
28
|
+
"node": ">=20"
|
|
29
|
+
},
|
|
30
|
+
"dependencies": {
|
|
31
|
+
"@parcel/watcher": "^2.5.1",
|
|
32
|
+
"oxc-parser": "^0.110.0",
|
|
33
|
+
"picomatch": "^4.0.5",
|
|
34
|
+
"playwright": "1.58.2",
|
|
35
|
+
"pngjs": "^7.0.0"
|
|
36
|
+
},
|
|
37
|
+
"devDependencies": {
|
|
38
|
+
"@tempo-modules/canvas-capture-headless": "workspace:*",
|
|
39
|
+
"@types/node": "^25.0.8",
|
|
40
|
+
"@types/pngjs": "^6.0.5",
|
|
41
|
+
"tsup": "^8.5.1",
|
|
42
|
+
"vitest": "^3.1.1"
|
|
43
|
+
},
|
|
44
|
+
"scripts": {
|
|
45
|
+
"build": "tsup",
|
|
46
|
+
"capture": "bun src/cli.ts",
|
|
47
|
+
"prepare": "tsup",
|
|
48
|
+
"prepublishOnly": "pnpm build && pnpm test",
|
|
49
|
+
"test": "vitest run",
|
|
50
|
+
"test:watch": "vitest"
|
|
51
|
+
}
|
|
52
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
# Written by Tempo visual-review onboarding (visual_review_init). The pnpm
|
|
2
|
+
# setup and the two install lines are the package-manager-specific part;
|
|
3
|
+
# init rewrites them for npm / yarn / bun. Keep every
|
|
4
|
+
# `npx --yes @tempo-ai/visual-review@latest …` line as it is: Tempo's own
|
|
5
|
+
# repository runs these exact commands, so each release is exercised there
|
|
6
|
+
# before it reaches you. Nothing from your checkout executes — the checkout
|
|
7
|
+
# is the subject of the capture.
|
|
8
|
+
name: Visual Review
|
|
9
|
+
|
|
10
|
+
on:
|
|
11
|
+
pull_request:
|
|
12
|
+
# labeled/unlabeled: the bypass label takes effect (and is revocable)
|
|
13
|
+
# without pushing a new commit.
|
|
14
|
+
types: [opened, synchronize, reopened, edited, labeled, unlabeled]
|
|
15
|
+
push:
|
|
16
|
+
# Your default branch: pushes here publish the baseline PRs compare against.
|
|
17
|
+
branches: [main]
|
|
18
|
+
|
|
19
|
+
permissions:
|
|
20
|
+
contents: read
|
|
21
|
+
statuses: write
|
|
22
|
+
pull-requests: write
|
|
23
|
+
# GitHub OIDC is the only credential Tempo accepts. No secret is stored.
|
|
24
|
+
id-token: write
|
|
25
|
+
|
|
26
|
+
concurrency:
|
|
27
|
+
# A new push never queues behind a stale run. Label events get their own
|
|
28
|
+
# group so an unrelated label added mid-capture cannot cancel it.
|
|
29
|
+
group: visual-review-${{ github.event.pull_request.number || github.sha }}-${{ (github.event.action == 'labeled' || github.event.action == 'unlabeled') && 'label' || 'code' }}
|
|
30
|
+
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
|
31
|
+
|
|
32
|
+
jobs:
|
|
33
|
+
# `visual-review:bypass` label — the incident escape hatch. Posts success
|
|
34
|
+
# and skips capture entirely.
|
|
35
|
+
bypass:
|
|
36
|
+
if: >-
|
|
37
|
+
github.event_name == 'pull_request'
|
|
38
|
+
&& contains(github.event.pull_request.labels.*.name, 'visual-review:bypass')
|
|
39
|
+
runs-on: ubuntu-latest
|
|
40
|
+
timeout-minutes: 5
|
|
41
|
+
steps:
|
|
42
|
+
- run: npx --yes @tempo-ai/visual-review@latest report --bypass --sha ${{ github.event.pull_request.head.sha }}
|
|
43
|
+
env:
|
|
44
|
+
GH_TOKEN: ${{ github.token }}
|
|
45
|
+
|
|
46
|
+
# `<!-- tempo-no-visual-change -->` in the PR body — a 100%-certainty claim
|
|
47
|
+
# that the PR changes no UI and no tested storyboard. Posts success and
|
|
48
|
+
# skips capture.
|
|
49
|
+
no-visual-change:
|
|
50
|
+
if: >-
|
|
51
|
+
github.event_name == 'pull_request'
|
|
52
|
+
&& !contains(github.event.pull_request.labels.*.name, 'visual-review:bypass')
|
|
53
|
+
&& contains(github.event.pull_request.body, '<!-- tempo-no-visual-change -->')
|
|
54
|
+
runs-on: ubuntu-latest
|
|
55
|
+
timeout-minutes: 5
|
|
56
|
+
steps:
|
|
57
|
+
- run: npx --yes @tempo-ai/visual-review@latest report --no-visual-change --pr ${{ github.event.pull_request.number }} --sha ${{ github.event.pull_request.head.sha }}
|
|
58
|
+
env:
|
|
59
|
+
GH_TOKEN: ${{ github.token }}
|
|
60
|
+
|
|
61
|
+
capture:
|
|
62
|
+
# Pushes to the default branch, real code events without either skip
|
|
63
|
+
# declaration, and bypass-label removal (to restore the real gate).
|
|
64
|
+
if: >-
|
|
65
|
+
!cancelled()
|
|
66
|
+
&& (
|
|
67
|
+
github.event_name == 'push'
|
|
68
|
+
|| (
|
|
69
|
+
contains(fromJSON('["opened","synchronize","reopened","edited"]'), github.event.action)
|
|
70
|
+
&& !contains(github.event.pull_request.labels.*.name, 'visual-review:bypass')
|
|
71
|
+
&& !contains(github.event.pull_request.body, '<!-- tempo-no-visual-change -->')
|
|
72
|
+
)
|
|
73
|
+
|| (
|
|
74
|
+
github.event.action == 'unlabeled'
|
|
75
|
+
&& github.event.label.name == 'visual-review:bypass'
|
|
76
|
+
&& !contains(github.event.pull_request.body, '<!-- tempo-no-visual-change -->')
|
|
77
|
+
)
|
|
78
|
+
)
|
|
79
|
+
runs-on: ubuntu-latest
|
|
80
|
+
# A PR may first wait for another run's baseline and then render both the
|
|
81
|
+
# merge-base and its own head.
|
|
82
|
+
timeout-minutes: 90
|
|
83
|
+
outputs:
|
|
84
|
+
status_url: ${{ steps.capture.outputs.status_url }}
|
|
85
|
+
steps:
|
|
86
|
+
- uses: actions/checkout@v4
|
|
87
|
+
with:
|
|
88
|
+
# Full history: the merge-base ancestor chain is the baseline key.
|
|
89
|
+
fetch-depth: 0
|
|
90
|
+
# Capture the PR head, not GitHub's synthetic merge commit.
|
|
91
|
+
ref: ${{ github.event.pull_request.head.sha || github.sha }}
|
|
92
|
+
|
|
93
|
+
# Announces intent before dependency/browser setup so stacked PRs wait
|
|
94
|
+
# on the baseline lease instead of duplicating a capture. Purely an
|
|
95
|
+
# optimization, so a registry hiccup must not fail the run.
|
|
96
|
+
- run: npx --yes @tempo-ai/visual-review@latest claim || echo "::warning::capture claim announcement unavailable; capture claims after setup"
|
|
97
|
+
|
|
98
|
+
- if: github.event_name == 'pull_request'
|
|
99
|
+
run: |
|
|
100
|
+
gh api "repos/${{ github.repository }}/statuses/${{ github.event.pull_request.head.sha }}" \
|
|
101
|
+
-f state=pending -f context=visual-review \
|
|
102
|
+
-f "description=capturing storyboards…"
|
|
103
|
+
env:
|
|
104
|
+
GH_TOKEN: ${{ github.token }}
|
|
105
|
+
|
|
106
|
+
- uses: pnpm/action-setup@v4
|
|
107
|
+
- uses: actions/setup-node@v4
|
|
108
|
+
with:
|
|
109
|
+
node-version: 20
|
|
110
|
+
cache: pnpm
|
|
111
|
+
- run: pnpm install --frozen-lockfile
|
|
112
|
+
# The canvas host under `tempo/` is its own package. Capture never
|
|
113
|
+
# mutates this checkout (TEMPO_DEVSERVER_PROTECTED_ROOTS below), so the
|
|
114
|
+
# host's dependencies are installed here, explicitly.
|
|
115
|
+
- run: cd tempo && pnpm install --no-frozen-lockfile
|
|
116
|
+
|
|
117
|
+
# Chromium matching the CLI's own pinned playwright, plus its shared
|
|
118
|
+
# libraries on Linux.
|
|
119
|
+
- run: npx --yes @tempo-ai/visual-review@latest install-browsers
|
|
120
|
+
|
|
121
|
+
- uses: actions/cache@v4
|
|
122
|
+
with:
|
|
123
|
+
path: tempo/node_modules/.vite
|
|
124
|
+
key: visual-review-vite-${{ runner.os }}-${{ hashFiles('**/pnpm-lock.yaml') }}
|
|
125
|
+
|
|
126
|
+
# `capture` infers PR-vs-main, head SHA, branch, merge-base and
|
|
127
|
+
# repository from GitHub's event payload and the standard GITHUB_*
|
|
128
|
+
# environment, and authenticates with short-lived OIDC tokens it mints
|
|
129
|
+
# itself.
|
|
130
|
+
- id: capture
|
|
131
|
+
run: npx --yes @tempo-ai/visual-review@latest capture --parallel 2
|
|
132
|
+
env:
|
|
133
|
+
# Forbid the capture's canvas-host supervisor from running package
|
|
134
|
+
# manager installs or updates inside this checkout; those would
|
|
135
|
+
# churn the Vite cache mid-run and rewrite lockfiles.
|
|
136
|
+
TEMPO_DEVSERVER_PROTECTED_ROOTS: ${{ github.workspace }}
|
|
137
|
+
|
|
138
|
+
# A finished capture has already consumed its claim; this only releases
|
|
139
|
+
# a lease left behind by a setup failure or cancellation.
|
|
140
|
+
- if: always()
|
|
141
|
+
run: npx --yes @tempo-ai/visual-review@latest claim --release || echo "::warning::capture claim release unavailable; the lease expires on its own"
|
|
142
|
+
|
|
143
|
+
- if: failure() && github.event_name == 'pull_request'
|
|
144
|
+
run: |
|
|
145
|
+
gh api "repos/${{ github.repository }}/statuses/${{ github.event.pull_request.head.sha }}" \
|
|
146
|
+
-f state=failure -f context=visual-review \
|
|
147
|
+
-f "description=capture failed — see workflow logs"
|
|
148
|
+
env:
|
|
149
|
+
GH_TOKEN: ${{ github.token }}
|
|
150
|
+
|
|
151
|
+
# Polls the run until Tempo has compared it, then posts the verdict and the
|
|
152
|
+
# sticky PR comment. Approvals in the Tempo report flip the check afterwards
|
|
153
|
+
# without a CI re-run.
|
|
154
|
+
report:
|
|
155
|
+
needs: capture
|
|
156
|
+
if: github.event_name == 'pull_request'
|
|
157
|
+
runs-on: ubuntu-latest
|
|
158
|
+
timeout-minutes: 20
|
|
159
|
+
steps:
|
|
160
|
+
- run: npx --yes @tempo-ai/visual-review@latest report --status-url "${{ needs.capture.outputs.status_url }}" --pr ${{ github.event.pull_request.number }} --sha ${{ github.event.pull_request.head.sha }}
|
|
161
|
+
env:
|
|
162
|
+
GH_TOKEN: ${{ github.token }}
|
|
163
|
+
|
|
164
|
+
# A crashed report must not strand the check at pending. Only stomps
|
|
165
|
+
# pending/absent — a verdict Tempo already delivered stays.
|
|
166
|
+
- if: failure()
|
|
167
|
+
run: npx --yes @tempo-ai/visual-review@latest report --fallback-failure --sha ${{ github.event.pull_request.head.sha }}
|
|
168
|
+
env:
|
|
169
|
+
GH_TOKEN: ${{ github.token }}
|