Skip to content

Commit 66e9cee

Browse files
feat: add Supabase data layer for shared storage and team baselines (Phase 4)
Adds optional Supabase integration for team-wide visual regression: - scripts/supabase.ts: Upload images to Storage, insert run/diff/baseline rows, generate signed URLs for PR comment thumbnails, remote baseline management (fetch, promote, list) - migrations/001_initial_schema.sql: vr_runs, vr_diffs, vr_baselines tables with indexes and RLS policies - --upload flag on ci.ts: pushes results to Supabase when configured - Diff thumbnails in PR comments via signed URLs - Optional: supabase config field — local file storage still works without it Supabase eliminates git bloat from baseline PNGs and enables shared baselines across the 11-person team and multiple SBUs. 44 tests passing. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 70f70b8 commit 66e9cee

12 files changed

Lines changed: 922 additions & 14 deletions

File tree

.claude-plugin/plugin.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dojowatch",
3-
"version": "0.3.0",
3+
"version": "0.4.0",
44
"description": "AI-native visual regression testing — capture, diff, and analyze UI changes with LLM vision",
55
"author": {
66
"name": "Dojo Coding LLC",

CHANGELOG.md

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,20 @@
11
# Changelog
22

3-
## [0.3.0] — Unreleased
3+
## [0.4.0] — Unreleased
4+
5+
### Added
6+
- **Supabase data layer** (`scripts/supabase.ts`): Full Supabase integration for shared storage
7+
- Upload runs, diffs, and baselines to Supabase Storage (private buckets)
8+
- Insert `vr_runs`, `vr_diffs`, `vr_baselines` rows with structured metadata
9+
- Signed URL generation for embedding diff thumbnails in PR comments
10+
- Remote baseline management: fetch, promote, list across team
11+
- `uploadCheckRun()` orchestrates the full upload in one call
12+
- **Database migration** (`migrations/001_initial_schema.sql`): Tables, indexes, RLS policies
13+
- **`--upload` flag** on CI orchestrator: pushes results to Supabase when configured
14+
- **Diff thumbnails in PR comments**: When Supabase is configured, regression rows include clickable diff image links
15+
- **Optional Supabase config**: `supabase` field in config.json is optional — local file storage works without it
16+
17+
## [0.3.0]
418

519
### Added
620
- **Gemini analysis module** (`scripts/analyze-gemini.ts`): Batch multimodal analysis using Google GenAI SDK

migrations/001_initial_schema.sql

Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
-- DojoWatch Database Schema
2+
-- Run this against your Supabase project via the SQL editor or CLI.
3+
4+
-- ─── Tables ──────────────────────────────────────────────────────
5+
6+
-- One row per CI or local check execution
7+
CREATE TABLE IF NOT EXISTS vr_runs (
8+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
9+
project TEXT NOT NULL,
10+
pr_number INTEGER,
11+
branch TEXT NOT NULL,
12+
commit_sha TEXT NOT NULL,
13+
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'pass', 'fail')),
14+
total_diffs INTEGER NOT NULL DEFAULT 0,
15+
regressions_count INTEGER NOT NULL DEFAULT 0,
16+
engine TEXT NOT NULL CHECK (engine IN ('claude', 'gemini')),
17+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
18+
);
19+
20+
-- One row per changed screenshot in a run
21+
CREATE TABLE IF NOT EXISTS vr_diffs (
22+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
23+
run_id UUID NOT NULL REFERENCES vr_runs(id) ON DELETE CASCADE,
24+
name TEXT NOT NULL,
25+
viewport TEXT NOT NULL,
26+
baseline_storage_path TEXT,
27+
current_storage_path TEXT,
28+
diff_storage_path TEXT,
29+
pixel_diff_percent REAL NOT NULL DEFAULT 0,
30+
tier TEXT NOT NULL CHECK (tier IN ('SKIP', 'FAST_CHECK', 'FULL_ANALYSIS')),
31+
analysis JSONB,
32+
severity TEXT CHECK (severity IN ('high', 'medium', 'low')),
33+
review_status TEXT NOT NULL DEFAULT 'pending' CHECK (review_status IN ('pending', 'approved', 'rejected')),
34+
reviewed_by TEXT,
35+
reviewed_at TIMESTAMPTZ,
36+
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
37+
);
38+
39+
-- Source of truth for approved baseline screenshots
40+
CREATE TABLE IF NOT EXISTS vr_baselines (
41+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
42+
project TEXT NOT NULL,
43+
name TEXT NOT NULL,
44+
viewport TEXT NOT NULL,
45+
storage_path TEXT NOT NULL,
46+
hash TEXT NOT NULL,
47+
approved_at TIMESTAMPTZ NOT NULL DEFAULT now(),
48+
approved_by TEXT,
49+
UNIQUE (project, name, viewport)
50+
);
51+
52+
-- ─── Indexes ─────────────────────────────────────────────────────
53+
54+
CREATE INDEX IF NOT EXISTS idx_vr_runs_project ON vr_runs(project);
55+
CREATE INDEX IF NOT EXISTS idx_vr_runs_branch ON vr_runs(branch);
56+
CREATE INDEX IF NOT EXISTS idx_vr_runs_pr ON vr_runs(pr_number);
57+
CREATE INDEX IF NOT EXISTS idx_vr_diffs_run_id ON vr_diffs(run_id);
58+
CREATE INDEX IF NOT EXISTS idx_vr_diffs_review ON vr_diffs(review_status);
59+
CREATE INDEX IF NOT EXISTS idx_vr_baselines_project ON vr_baselines(project);
60+
CREATE INDEX IF NOT EXISTS idx_vr_baselines_lookup ON vr_baselines(project, name, viewport);
61+
62+
-- ─── Row Level Security ──────────────────────────────────────────
63+
64+
ALTER TABLE vr_runs ENABLE ROW LEVEL SECURITY;
65+
ALTER TABLE vr_diffs ENABLE ROW LEVEL SECURITY;
66+
ALTER TABLE vr_baselines ENABLE ROW LEVEL SECURITY;
67+
68+
-- Service role (CI uploads) has full access
69+
CREATE POLICY "Service role full access on vr_runs"
70+
ON vr_runs FOR ALL
71+
USING (true)
72+
WITH CHECK (true);
73+
74+
CREATE POLICY "Service role full access on vr_diffs"
75+
ON vr_diffs FOR ALL
76+
USING (true)
77+
WITH CHECK (true);
78+
79+
CREATE POLICY "Service role full access on vr_baselines"
80+
ON vr_baselines FOR ALL
81+
USING (true)
82+
WITH CHECK (true);
83+
84+
-- Anon key can read (for dashboard, when it's built)
85+
CREATE POLICY "Anon read on vr_runs"
86+
ON vr_runs FOR SELECT
87+
USING (true);
88+
89+
CREATE POLICY "Anon read on vr_diffs"
90+
ON vr_diffs FOR SELECT
91+
USING (true);
92+
93+
CREATE POLICY "Anon read on vr_baselines"
94+
ON vr_baselines FOR SELECT
95+
USING (true);
96+
97+
-- ─── Storage Buckets ─────────────────────────────────────────────
98+
-- Create these via Supabase dashboard or CLI:
99+
-- supabase storage create baselines --public false
100+
-- supabase storage create captures --public false
101+
-- supabase storage create diffs --public false
102+
--
103+
-- All buckets are private. Dashboard serves images via signed URLs.

package-lock.json

Lines changed: 113 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "dojowatch",
3-
"version": "0.3.0",
3+
"version": "0.4.0",
44
"private": true,
55
"type": "module",
66
"scripts": {
@@ -11,6 +11,7 @@
1111
},
1212
"dependencies": {
1313
"@google/genai": "^1.46.0",
14+
"@supabase/supabase-js": "^2.100.1",
1415
"picocolors": "^1.1.1",
1516
"pixelmatch": "^6.0.0",
1617
"playwright": "^1.50.0",

scripts/ci.ts

Lines changed: 35 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ import { captureRoutes, captureStorybook } from "./capture.js";
1616
import { prefilterAll } from "./prefilter.js";
1717
import { analyzeWithGemini } from "./analyze-gemini.js";
1818
import { generateCommentMarkdown, postComment } from "./comment.js";
19+
import { createServiceClient, uploadCheckRun, getSignedDiffUrls } from "./supabase.js";
1920
import type { CheckRun } from "./types.js";
2021

2122
async function main(): Promise<void> {
2223
const args = process.argv.slice(2);
2324
const prArg = args.find((a) => a.startsWith("--pr="))?.split("=")[1]
2425
?? args[args.indexOf("--pr") + 1];
2526
const prNumber = prArg ? parseInt(prArg, 10) : undefined;
27+
const uploadFlag = args.includes("--upload");
2628

2729
const projectRoot = findProjectRoot();
2830
if (!projectRoot) {
@@ -124,14 +126,43 @@ async function main(): Promise<void> {
124126
},
125127
};
126128

127-
// Save check run
129+
// Save check run locally
128130
const checkRunPath = join(dojowatchDir, "last-check.json");
129131
writeFileSync(checkRunPath, JSON.stringify(checkRun, null, 2));
130132

131-
// ── Step 4: PR Comment ─────────────────────────────────────────
133+
// ── Step 4: Upload to Supabase ─────────────────────────────────
134+
let runId: string | undefined;
135+
if (uploadFlag && config.supabase) {
136+
console.log(pc.bold("\nStep 4: Uploading to Supabase..."));
137+
const supabaseClient = createServiceClient(config);
138+
runId = await uploadCheckRun(supabaseClient, checkRun, config, {
139+
prNumber,
140+
engine: "gemini",
141+
capturesDir: join(dojowatchDir, "captures"),
142+
diffsDir: join(dojowatchDir, "diffs"),
143+
});
144+
} else if (uploadFlag && !config.supabase) {
145+
console.log(pc.yellow("\n --upload flag set but no supabase config. Skipping."));
146+
}
147+
148+
// ── Step 5: PR Comment ─────────────────────────────────────────
132149
if (prNumber) {
133-
console.log(pc.bold(`\nStep 4: Posting comment to PR #${prNumber}...`));
134-
const markdown = generateCommentMarkdown(checkRun);
150+
console.log(pc.bold(`\nStep 5: Posting comment to PR #${prNumber}...`));
151+
152+
// If we uploaded to Supabase, enrich comment with signed diff URLs
153+
let diffUrls: Map<string, string> | undefined;
154+
if (runId && config.supabase) {
155+
const supabaseClient = createServiceClient(config);
156+
diffUrls = await getSignedDiffUrls(
157+
supabaseClient,
158+
runId,
159+
config.project,
160+
prefilterResults,
161+
config.supabase.signedUrlExpiry
162+
);
163+
}
164+
165+
const markdown = generateCommentMarkdown(checkRun, diffUrls);
135166
postComment(prNumber, markdown);
136167
} else {
137168
console.log(pc.dim("\nNo --pr flag. Skipping PR comment."));

0 commit comments

Comments
 (0)