Public: anyone can read this; the crew can change it.

A preview shows production's data and never says whose it is #244

Merged opened this

Check · Review · Land · Live done — Landed and live in production.

Merged

scout/forge:scout/222-a-preview-shows-production-s-data-and-ne → main 2 commits fork

  • Merged. scout/222-a-preview-shows-production-s-data-and-ne was merged into main as 5f7323b.

Review

Sign in to review this change.
4 files changed +72 −0
src/web/pages/apps.ts +3
@@ -67,6 +67,7 @@ import {
6767 envStateBadge,
6868 icon,
6969 listCard,
70 previewDataNote,
7071 row,
7172 seg,
7273 selectField,
@@ -801,8 +802,10 @@ export function previewCard(ctx: Ctx, repo: Repo, pull: Pull): Html {
801802 // A preview's terminal is a different login from production's — its own
802803 // command, shown only to a writer, same as the app page's.
803804 const writer = canWrite(ctx.app.db, repo, ctx.auth.user);
805 const dataNote = previewDataNote(view.value.app.previewData, pull.createdAt);
804806 return card(
805807 html`${envFacts(base, env)}
808 ${dataNote === null ? null : html`<p class="muted small">${dataNote}</p>`}
806809 ${when(writer, () =>
807810 terminalLine(
808811 terminalFor(ctx.app, view.value.app, env.env, env),
src/web/studio/previews.ts +5
@@ -31,6 +31,7 @@ import {
3131 envPhrase,
3232 icon,
3333 listCard,
34 previewDataNote,
3435 row,
3536 terminalLine,
3637 time,
@@ -104,6 +105,10 @@ function previewRow(
104105 if (env?.since) bits.push(html`up ${time(env.since)}`);
105106 const say = env?.error ?? env?.failedDeploy?.error ?? env?.note;
106107 if (say) bits.push(html`${say}`);
108 if (env !== undefined) {
109 const dataNote = previewDataNote(record.previewData, pull.createdAt);
110 if (dataNote !== null) bits.push(dataNote);
111 }
107112 return row({
108113 ...(href === undefined ? {} : { href, nav: true }),
109114 lead: icon("rocket"),
src/web/ui.ts +21
@@ -11,6 +11,7 @@
1111 */
1212
1313 import type { EnvState, EnvStatus } from "../apps/runtime.ts";
14 import type { PreviewData } from "../apps/data.ts";
1415 import type { RunKind, RunState } from "../core/runs.ts";
1516 import { icon, type IconName } from "./icons.ts";
1617 import { Html, html, raw, when } from "./html.ts";
@@ -390,6 +391,26 @@ export function envFacts(
390391 )}`;
391392 }
392393
394 /**
395 * The one fact a preview says about whose data it has: its own copy of
396 * production's, taken the moment the change that opened it did — the only
397 * moment `ensureEnvData` (`apps/data.ts`) ever takes one, since a redeploy
398 * of the same preview keeps what is already there. That instant is a
399 * pull's own `createdAt`, already known wherever a preview is drawn, so
400 * this never stats the data directory to answer. An app whose preview
401 * policy is `empty` has no production data to name, so there is nothing to
402 * say — and production's own card never calls this at all: there is
403 * nothing to warn about data it simply *is*. Bare text, not a card or a
404 * paragraph: the caller decides how it sits beside the rest of the facts.
405 */
406 export function previewDataNote(
407 policy: PreviewData,
408 openedAt: string,
409 ): Html | null {
410 if (policy !== "copy") return null;
411 return html`its own copy of production's data, taken ${time(openedAt)}`;
412 }
413
393414 /**
394415 * An environment's terminal, as copyable monospace text with the host key
395416 * fingerprint beside it — so a first connection can be trusted deliberately
test/studio.test.ts +43
@@ -1738,6 +1738,49 @@ describe("every open pull's preview on the studio page", () => {
17381738 expect(after).not.toContain("<h2>Previews</h2>");
17391739 expect(after).not.toContain(`pull-${pull.number}/open`);
17401740 }, 30_000);
1741
1742 test("a change's page names its preview's data as its own copy and gives the moment it was taken; production's card does not", async () => {
1743 const alice = getUserByName(app.db, "alice")!;
1744 const branch = await commitToBranch(app, alice, repo, {
1745 branch: "feature/y",
1746 base: repo.defaultBranch,
1747 message: "Add Y",
1748 files: [{ path: "Y.md", content: "y\n" }],
1749 });
1750 if (!branch.ok) throw new Error(JSON.stringify(branch.error));
1751 const opened = await openPull(app, alice, repo, {
1752 title: "Add Y",
1753 body: "",
1754 baseBranch: repo.defaultBranch,
1755 headBranch: "feature/y",
1756 });
1757 if (!opened.ok) throw new Error(String(opened.error));
1758 const pull = opened.value;
1759 const record = getAppByRepo(app.db, repo.id)!;
1760 const env = `pull-${pull.number}` as const;
1761
1762 const deadline = Date.now() + 30_000;
1763 while (
1764 Date.now() < deadline &&
1765 runtime.envStatus(record, env)?.state !== "running"
1766 )
1767 await Bun.sleep(100);
1768 expect(runtime.envStatus(record, env)?.state).toBe("running");
1769
1770 const note = "its own copy of production's data, taken";
1771
1772 const appPage = await (await get("/alice/widget")).text();
1773 expect(appPage).toContain(note);
1774 // Production's own card, above the previews list, never says it.
1775 const production = appPage.slice(0, appPage.indexOf("<h2>Previews</h2>"));
1776 expect(production).not.toContain(note);
1777
1778 const changePage = await (
1779 await get(`/alice/widget/changes/${pull.number}`)
1780 ).text();
1781 expect(changePage).toContain("<h2>Preview</h2>");
1782 expect(changePage).toContain(note);
1783 }, 30_000);
17411784 });
17421785
17431786 describe("the studio home does not wait on the decider", () => {

↑↓ navigate ↵ open ⌘↵ its app esc close

Keyboard shortcuts

Docs