Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 9 additions & 3 deletions .github/scripts/lib/gh-pages-publisher.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down Expand Up @@ -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({
Expand All @@ -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})) {
Expand Down
41 changes: 41 additions & 0 deletions .github/scripts/lib/gh-pages-publisher.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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'),
Expand Down Expand Up @@ -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'),
Expand Down Expand Up @@ -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', () =>
Expand Down
Loading