From 1660f8bef9c97a6acef7f9b04a35b54fbe10181b Mon Sep 17 00:00:00 2001 From: Cindy Zhang Date: Sun, 30 Aug 2026 08:44:35 -0700 Subject: [PATCH] fix(ci): preserve visual reports during preview deploy --- .github/scripts/lib/gh-pages-publisher.mjs | 12 ++++-- .../scripts/lib/gh-pages-publisher.test.mjs | 41 +++++++++++++++++++ 2 files changed, 50 insertions(+), 3 deletions(-) diff --git a/.github/scripts/lib/gh-pages-publisher.mjs b/.github/scripts/lib/gh-pages-publisher.mjs index cadc20998e339..8f7055b712a30 100755 --- a/.github/scripts/lib/gh-pages-publisher.mjs +++ b/.github/scripts/lib/gh-pages-publisher.mjs @@ -1541,9 +1541,12 @@ export async function publishManualVisualBaseline({ refuse(`could not push the updated baseline after ${maxAttempts} attempts`); } -function removeContents(destination) { - fs.rmSync(destination, {recursive: true, force: true}); +function replacePreviewContents(destination) { fs.mkdirSync(destination, {recursive: true}); + for (const entry of fs.readdirSync(destination)) { + if (entry === 'visual') continue; + fs.rmSync(path.join(destination, entry), {recursive: true, force: true}); + } } export async function publishPrPreview({ @@ -1571,6 +1574,9 @@ export async function publishPrPreview({ refuse(`${name} must be a directory`); } } + if (fs.existsSync(path.join(storybookDir, 'visual'))) { + refuse('the Storybook artifact contains the reserved visual path'); + } const destinationRel = `pr/${prNumber}`; for (let attempt = 1; attempt <= maxAttempts; attempt += 1) { const checkout = checkoutPages({ @@ -1583,7 +1589,7 @@ export async function publishPrPreview({ }); try { const destination = path.join(checkout, destinationRel); - removeContents(destination); + replacePreviewContents(destination); copyContents(storybookDir, destination); fs.mkdirSync(path.join(destination, 'sandbox'), {recursive: true}); for (const entry of fs.readdirSync(sandboxDir, {withFileTypes: true})) { diff --git a/.github/scripts/lib/gh-pages-publisher.test.mjs b/.github/scripts/lib/gh-pages-publisher.test.mjs index ae88f595bd2db..8f7de2af034ea 100644 --- a/.github/scripts/lib/gh-pages-publisher.test.mjs +++ b/.github/scripts/lib/gh-pages-publisher.test.mjs @@ -144,6 +144,10 @@ function fixture() { writeFile(path.join(seed, 'sandbox', 'old.html'), 'old sandbox'); writeFile(path.join(seed, 'assets', 'old.css'), 'old asset'); writeFile(path.join(seed, 'pr', '123', 'index.html'), 'preview'); + writeFile( + path.join(seed, 'pr', '123', 'visual', 'evidence.json'), + 'visual evidence', + ); writeFile(path.join(seed, 'pr', '124', 'index.html'), 'closed preview'); writeFile( path.join(seed, 'pr', '123', 'sandbox', 'template-assets', 'old.txt'), @@ -1082,6 +1086,12 @@ describe('gh-pages publisher', () => { path.join(final, 'pr', '123', 'sandbox', 'template-assets'), ), ).toBe(false); + expect( + fs.readFileSync( + path.join(final, 'pr', '123', 'visual', 'evidence.json'), + 'utf8', + ), + ).toBe('visual evidence'); expect( fs.readFileSync( path.join(final, 'reports', 'vibe', 'index.html'), @@ -1109,6 +1119,37 @@ describe('gh-pages publisher', () => { ).toBe(true); }); + it('rejects Storybook artifacts that collide with trusted visual evidence', async () => { + const fx = fixture(); + const storybook = path.join(fx.root, 'colliding-storybook'); + const sandbox = path.join(fx.root, 'colliding-sandbox'); + writeFile(path.join(storybook, 'index.html'), 'new preview'); + writeFile( + path.join(storybook, 'visual', 'evidence.json'), + 'untrusted evidence', + ); + writeFile(path.join(sandbox, 'index.html'), 'new sandbox'); + + await expect( + queuedPublish(fx, 940, 'pr-preview/123', () => + publishPrPreview({ + ...context(fx, 940, 'pr-preview/123'), + pr: 123, + storybook, + sandbox, + }), + ), + ).rejects.toThrow(/reserved visual path/); + + const final = cloneRemote(fx.remote, fx.root); + expect( + fs.readFileSync( + path.join(final, 'pr', '123', 'visual', 'evidence.json'), + 'utf8', + ), + ).toBe('visual evidence'); + }); + it('cleans stale previews without deleting visual evidence or live previews', async () => { const fx = fixture(); await queuedPublish(fx, 941, 'cleanup/previews', () =>