diff --git a/README.md b/README.md index 01c1c9e..2cf4e2c 100644 --- a/README.md +++ b/README.md @@ -241,7 +241,8 @@ banner, your verdict, crops embedded) to attach to a ticket or publish as proof: [docs/output.md](https://github.com/tmashara/renderdiff/blob/main/docs/output.md#evidencehtml). For an intended change, run with `--threshold 0 --no-recheck`, then `renderdiff report --markdown pr.md` writes a before/after table per route for the PR -description ([docs/output.md](https://github.com/tmashara/renderdiff/blob/main/docs/output.md#markdown)). +description, with `--base-url ` for absolute image links once the out dir is uploaded +([docs/output.md](https://github.com/tmashara/renderdiff/blob/main/docs/output.md#markdown)). `renderdiff merge --out ` writes the full run's results to a new out dir with the shots a partial run also took (say, a few routes rerun under a new mask) diff --git a/docs/output.md b/docs/output.md index 10fde1a..4afe69a 100644 --- a/docs/output.md +++ b/docs/output.md @@ -348,7 +348,35 @@ Build it after a fresh full run. intended change: a `## ` section per route with the before (right) and after (left) URLs, and a `| Width | Before | After |` table linking each width's full-page shots, with `(h±Npx)` when the heights differ and the error in place of an errored shot. Links are -relative to the Markdown file; uploading the images to GitHub stays manual. +relative to the Markdown file, or `/` with `--base-url `: upload the out dir +anywhere, then pass its URL so the images render in a PR description. `evidence.html` is +self-contained and can go anywhere on its own. + +Cloudflare Pages gives `report.html` a per-PR URL: + +```bash +npx wrangler pages project create

--production-branch main # once +npx wrangler pages deploy renderdiff-out --project-name

--branch pr-123 +renderdiff report renderdiff-out --markdown pr.md --base-url https://pr-123.

.pages.dev +``` + +In CI, wrangler needs `CLOUDFLARE_API_TOKEN` and `CLOUDFLARE_ACCOUNT_ID`. Keep branch names +simple like `pr-123`: the alias is lowercased, sanitized and truncated. Cloudflare Access can +keep `report.html` private, but then the images won't render in the PR: GitHub fetches +images through its anonymous camo proxy. For private inline images, use a reports repo: + +```bash +git clone --depth 1 https://github.com// reports +mkdir -p reports/pr-123 && cp -R renderdiff-out/. reports/pr-123/ +git -C reports add . && git -C reports commit -m pr-123 && git -C reports push +renderdiff report renderdiff-out --markdown pr.md \ + --base-url https://github.com///raw/main/pr-123 +``` + +Those links render for viewers signed in with read access to the reports repo. Don't copy +`raw.githubusercontent.com/...?token=` URLs instead: the token expires. Use a separate repo, +not a branch of the main one: every clone of the main repo would download the screenshots, and +its history keeps them after the branch is deleted. ## merge diff --git a/skills/renderdiff/SKILL.md b/skills/renderdiff/SKILL.md index aad7637..3e71e83 100644 --- a/skills/renderdiff/SKILL.md +++ b/skills/renderdiff/SKILL.md @@ -86,4 +86,5 @@ attachment is the archive copy. Accepted entries are OS-specific: build it on th recorded them. For an intended change (a redesign), run with `--threshold 0 --no-recheck` and -`report --markdown pr.md` for the PR description's before/after tables. +`report --markdown pr.md` for the PR description's before/after tables; add +`--base-url ` once the out dir is uploaded, so the images render in the PR. diff --git a/src/cli.ts b/src/cli.ts index 3ff8cd9..a0ceb15 100644 --- a/src/cli.ts +++ b/src/cli.ts @@ -186,7 +186,7 @@ const HELP = ` renderdiff http://localhost:3000 https://example.com / /about renderdiff http://localhost:3000 https://example.com /pricing::/plans - renderdiff report [--verdict ] [--title ] [--markdown ] + renderdiff report [--verdict ] [--title ] [--markdown [--base-url ]] renderdiff merge --out

renderdiff probe [--width ] [--loads ] @@ -214,7 +214,8 @@ const HELP = ` evidence.html beside it: one self-contained page (crops embedded) with a parity banner and the --verdict file's text. --markdown also writes a before/after table per route (right before, left after, links to the shots) for a PR description; for an intended change, - run with --threshold 0 --no-recheck first. + run with --threshold 0 --no-recheck first. --base-url is a URL the shot names are + appended to in those links, for an out dir uploaded there. renderdiff merge writes 's results to --out with each shot also has replaced by the partial run's, and its other shots added. Both must compare the same origins. @@ -366,10 +367,15 @@ async function sitemapRoutes(spec: string | true, routes: Route[]): Promise { try { let dir: string | undefined; - const opt: { "--verdict"?: string; "--title"?: string; "--markdown"?: string } = {}; + const opt: { + "--verdict"?: string; + "--title"?: string; + "--markdown"?: string; + "--base-url"?: string; + } = {}; for (let i = 0; i < argv.length; i++) { const v = argv[i]!; - if (v === "--verdict" || v === "--title" || v === "--markdown") { + if (v === "--verdict" || v === "--title" || v === "--markdown" || v === "--base-url") { const value = argv[++i]; if (value === undefined) throw new Error(`${v} needs a value`); opt[v] = value; @@ -379,8 +385,10 @@ async function report(argv: string[]): Promise { } if (!dir) throw new Error( - "usage: renderdiff report [--verdict ] [--title ] [--markdown ]", + "usage: renderdiff report [--verdict ] [--title ] [--markdown [--base-url ]]", ); + if (opt["--base-url"] !== undefined && !opt["--markdown"]) + throw new Error("--base-url needs --markdown"); const file = path.join(dir, "results.json"); if (!fs.existsSync(file)) throw new Error(`no ${file}; run renderdiff with --out ${dir} first`); const verdict = @@ -390,7 +398,8 @@ async function report(argv: string[]): Promise { console.log(await writeReport(data.results, data.meta, dir, data.summary.stale)); const out = await writeEvidence(data, dir, verdict, opt["--title"]); console.log(`${out} (${Math.ceil(fs.statSync(out).size / 1024)} KB)`); - if (opt["--markdown"]) console.log(writeMarkdown(data, dir, opt["--markdown"])); + if (opt["--markdown"]) + console.log(writeMarkdown(data, dir, opt["--markdown"], opt["--base-url"])); process.exit(0); } catch (e) { console.error(`✗ ${e instanceof Error ? e.message : String(e)}`); diff --git a/src/report.ts b/src/report.ts index 76e101c..3cacd21 100644 --- a/src/report.ts +++ b/src/report.ts @@ -808,9 +808,18 @@ ${neutralized(meta, results)} } /** Before/after tables for a PR description: one section per route, right side as before. */ -export function writeMarkdown(data: Results, outDir: string, file: string): string { +export function writeMarkdown( + data: Results, + outDir: string, + file: string, + baseUrl?: string, +): string { const link = (img: string) => - encodeURI(path.relative(path.dirname(file), path.join(outDir, img)).split(path.sep).join("/")); + baseUrl + ? `${baseUrl.replace(/\/+$/, "")}/${encodeURI(img)}` + : encodeURI( + path.relative(path.dirname(file), path.join(outDir, img)).split(path.sep).join("/"), + ); const sections = [...Map.groupBy(data.results, name)].map(([rt, shots]) => { const r0 = shots[0]!; const rows = shots.map((r) => diff --git a/tests/unit/cli-args.test.ts b/tests/unit/cli-args.test.ts index b5c617f..635fe0b 100644 --- a/tests/unit/cli-args.test.ts +++ b/tests/unit/cli-args.test.ts @@ -75,7 +75,14 @@ describe("help and version", () => { if (!options.includes(f)) problems.push(`the README table lists ${f}, --help's Options don't`); - const foreign = new Set(["--immutable"]); // yarn install, in docs/case-study.md + // yarn install in docs/case-study.md; wrangler and git in docs/output.md + const foreign = new Set([ + "--immutable", + "--production-branch", + "--project-name", + "--branch", + "--depth", + ]); for (const f of ["README.md", ...md("docs"), ...md("skills")]) for (const flag of flags(read(f))) if (!known.has(flag) && !foreign.has(flag)) diff --git a/tests/unit/report.test.ts b/tests/unit/report.test.ts index 49f02c9..b3704c7 100644 --- a/tests/unit/report.test.ts +++ b/tests/unit/report.test.ts @@ -180,6 +180,28 @@ describe("renderdiff report --markdown", () => { "| 1440 | error: boom | |", ]); }); + + test("--base-url links the shots under that URL, not relative to the file", async () => { + const out = path.join(tempDir(), "shot out"); + fs.mkdirSync(out); + const images = { left: "index.1440.left.png", right: "index.1440.right.png", diff: "x.png" }; + writeResults([shot("/", { images })], meta(), out, []); + const md = path.join(out, "..", "pr", "pr.md"); + const args = ["report", out, "--markdown", md, "--base-url", "https://r.example/pr-1/"]; + const r = await runCli(args, { noOut: true }); + assert.equal(r.code, 0, r.stderr); + assert.deepEqual(fs.readFileSync(md, "utf8").match(/^\| \d+.*/gm), [ + "| 1440 | ![before](https://r.example/pr-1/index.1440.right.png) | ![after](https://r.example/pr-1/index.1440.left.png) |", + ]); + }); + + test("--base-url without --markdown is a usage error", async () => { + const r = await runCli(["report", tempDir(), "--base-url", "https://r.example"], { + noOut: true, + }); + assert.equal(r.code, 2); + assert.match(r.stderr, /--base-url needs --markdown/); + }); }); describe("verdict", () => {