Skip to content

Where people and agents ship software together. The open-source git platform for the whole job: issues, agents, checks and deploys to the edge.

Commit

Show production screenshots to public project readers

syntaqxcommitted Parentd55bdf5Browse files
5 files+64−140/5 viewed
+10−0
1818 pageRange,
1919 parseFilter,
2020 productionEnvironment,
21+ nativeProduction,
2122 shortSha,
2223 withFilter,
2324 } from "./deployments.ts";
212213 assert.equal(isNewestStatus(tied, 1), true);
213214 assert.equal(isNewestStatus([], 0), false);
214215 });
216+
217+test("native screenshots use the serving app, not a newer attempt or custom domain", () => {
218+ const current = { source: "g1t_page", state: "success", environment_url: "https://app-acme.g1t.page", sha: "abcdef1", updated_at: "2026-10-15T01:00:00Z" };
219+ const production = { ...env("production", { production_environment: true, latest: deployed }), current, url: "https://custom.example" };
220+ assert.deepEqual(nativeProduction([production as never]), { url: current.environment_url, commit: current.sha, deployedAt: current.updated_at });
221+ assert.equal(nativeProduction([{ ...production, current: null } as never]), null);
222+ assert.equal(nativeProduction([{ ...production, current: { ...current, state: "inactive" } } as never]), null);
223+ assert.equal(nativeProduction([{ ...production, current: { ...current, source: "actions" } } as never]), null);
224+});
+7−0
140140 return orderEnvironments(environments).find((env) => env.production_environment && env.latest != null) ?? null;
141141 }
142142
143+/** The serving native app, from deployment metadata available to every repository reader. */
144+export function nativeProduction(environments: readonly DeploymentEnvironment[]): { url: string; commit: string; deployedAt: string } | null {
145+ const current = productionEnvironment(environments)?.current;
146+ if (current?.source !== "g1t_page" || current.state !== "success" || !current.environment_url) return null;
147+ return { url: current.environment_url, commit: current.sha, deployedAt: current.updated_at };
148+}
149+
143150 /**
144151 * Production deployed somewhere other than g1t.page (by g1t Actions, or
145152 * reported through the API), as an app that deploys itself shows it: the
+6−8
5656 import { accessTo, countsFor, projectFor, refusal, repoFor } from "../../lib/access.server";
5757 import { shotVersion } from "./production-screenshot";
5858 import { DeploymentStateIcon, DeploymentsPanel } from "../../components/deployments-panel";
59−import { STATE_WORD, environmentUrl, productionEnvironment, reportedProduction } from "../../lib/deployments";
59+import { STATE_WORD, nativeProduction, productionEnvironment, reportedProduction } from "../../lib/deployments";
6060 import { cardSources, externalTarget, externalVersion, productionImageUrl } from "../../lib/production-image";
6161 import { useAddresses } from "../../lib/addresses";
6262 import { commitChecksFor } from "../../lib/commit-checks.server";
267267 // anyone else from the production environment, which they may read.
268268 const liveProduction = live.find((app) => app.kind === "production") ?? null;
269269 const current = productionEnv?.current ?? null;
270− const production = liveProduction
270+ const production = nativeProduction(environmentList?.environments ?? []) ?? (liveProduction
271271 ? { url: liveProduction.url, commit: liveProduction.commit, deployedAt: liveProduction.deployedAt }
272− : productionEnv && current?.source === "g1t_page" && environmentUrl(productionEnv)
273− ? { url: environmentUrl(productionEnv)!, commit: current.sha, deployedAt: current.updated_at }
274− : null;
272+ : null);
275273 const telemetryData = observed?.overview && (observed.overview.totals.requests > 0 || (observed.errors?.length ?? 0) > 0) ? observed : null;
276274
277275 return {
285283 previews: live.filter((app) => app.kind === "preview"),
286284 production,
287285 serving: current ? { id: current.id, sha: current.sha, ref: current.ref, at: current.updated_at, creator: current.creator, runUrl: current.run_url } : null,
288− // The screenshot of production on g1t.page, for members (like its builds).
289− productionShot: member && liveProduction ? shotVersion(liveProduction) : null,
286+ // Every reader can see production; administrative builds and settings remain member-only.
287+ productionShot: production ? shotVersion(production) : null,
290288 latestProductionBuild: latestProduction ? { status: latestProduction.status } : null,
291289 productionLatest: productionLatest
292290 ? { id: productionLatest.id, sha: productionLatest.sha, state: productionLatest.state, at: productionLatest.updated_at, environment: productionEnv!.name }
791789 </p>
792790 )}
793791 {productionPlan && <div className="flex flex-wrap items-center justify-between gap-3 border-t border-line px-5 py-3 sm:px-6">
794− <span className="text-xs text-muted">{plan === "elsewhere" ? "Production is deployed by its own pipeline." : !loaderData.settings?.enabled ? "Deployments are not enabled for this project." : <>Push to <span className="font-mono text-fg-soft">{defaultBranch ?? "the default branch"}</span> to update production.</>}</span>
792+ <span className="text-xs text-muted">{plan === "elsewhere" ? "Production is deployed by its own pipeline." : loaderData.settings?.enabled === false ? "Deployments are not enabled for this project." : <>Push to <span className="font-mono text-fg-soft">{defaultBranch ?? "the default branch"}</span> to update production.</>}</span>
795793 <div className="flex flex-wrap items-center gap-2">
796794 {member && loaderData.can.manage_integrations && <ButtonLink to={`${base}/settings/deployments`} variant="ghost" size="sm"><Settings2 size={14} />Deployment settings</ButtonLink>}
797795 <ButtonLink to={`${base}/deployments`} variant="outline" size="sm"><Rocket size={14} />Deployments{loaderData.environments && <span className="text-muted">{loaderData.environments.total_count}</span>}</ButtonLink>
+10−5
11 /**
2− * A screenshot of a project's production, for its overview. Members only,
3− * like the deployments it shows. The overview asks with `?v=` naming the
2+ * A screenshot of a project's production, for anyone allowed to read it.
3+ * The overview asks with `?v=` naming the
44 * deploy (its commit and when it went live), so the browser keeps a
55 * screenshot of that deploy for good, and a resumed or moved app, deployed
66 * again at the same commit, is asked for anew. One taken before the
1111 import type { Route } from "./+types/production-screenshot";
1212 import { deployments } from "../../lib/services.server";
1313 import { getViewer } from "../../lib/session.server";
14+import { projectFor } from "../../lib/access.server";
15+import { nativeProduction } from "../../lib/deployments";
1416
1517 const LONG = "private, max-age=31536000, immutable";
1618
2325 export async function loader({ params, context, request }: Route.LoaderArgs) {
2426 const viewer = getViewer(context);
2527 const none = (status: number) => new Response(null, { status, headers: { "cache-control": BRIEF } });
26− if (!viewer || !env.SCREENSHOTS) return none(404);
27− const list = await deployments.list({ workspace: params.owner, slug: params.repo }, viewer).catch(() => null);
28− const production = list?.ok ? list.value.live.find((app) => app.kind === "production") : undefined;
28+ if (!env.SCREENSHOTS) return none(404);
29+ const project = await projectFor(context, params).catch(() => null);
30+ if (!project?.ok) return none(404);
31+ const repo = project.value.source.kind === "hosted" ? project.value.source.repo : { namespace: params.owner, name: params.repo };
32+ const environments = await deployments.environments(repo, viewer).catch(() => null);
33+ const production = nativeProduction(environments?.ok ? environments.value.environments : []);
2934 if (!production) return none(404);
3035 const shot = await env.SCREENSHOTS.image({
3136 host: new URL(production.url).host,
+31−1
1414 // Seed R2 and the page cache through a test-only entry, keeping all R2 I/O
1515 // inside workerd. The application entry itself is the unmodified build.
1616 const seed = `import app from './index.js';
17+import { WorkerEntrypoint } from 'cloudflare:workers';
18+export class ScreenshotStub extends WorkerEntrypoint {
19+ async image(input) {
20+ if (input.host !== 'public-acme.g1t.page' || input.commit !== 'abcdef1234567') throw new Error('Unexpected screenshot target');
21+ return {body:new TextEncoder().encode('production-preview').buffer,contentType:'image/jpeg',commit:input.commit,capturedAt:'2026-10-15T02:00:00Z'};
22+ }
23+}
1724 export default { async fetch(request, env, ctx) {
1825 if (new URL(request.url).pathname === '/__seed') {
1926 await env.DOWNLOADS.put('web-assets/assets/app-old12345.css', ${JSON.stringify(css)}, {httpMetadata:{contentType:'text/css'}});
3441 r2Buckets: ["DOWNLOADS"],
3542 serviceBindings: {
3643 IDENTITY: async () => Response.json(null),
37− PROJECTS: async () => Response.json({ ok: true, value: [] }),
44+ PROJECTS: async (request) => {
45+ const method = new URL(request.url).pathname.split("/").pop();
46+ const args = await request.json();
47+ if (method !== "get") return Response.json({ ok: true, value: [] });
48+ if (args.slug !== "public") return Response.json({ ok: false, error: { code: "not_found", message: "Not found" } });
49+ return Response.json({ ok: true, value: { source: { kind: "hosted", repo: { namespace: "acme", name: "public" } } } });
50+ },
51+ DEPLOYMENTS: async (request) => {
52+ const method = new URL(request.url).pathname.split("/").pop();
53+ const args = await request.json();
54+ assert.equal(method, "list_environments", "Public previews must not read member-only deployment lists");
55+ assert.equal(args.viewer, null);
56+ assert.equal(args.repo.name, "public");
57+ const current = { id: "dpl_live", source: "g1t_page", state: "success", sha: "abcdef1234567", environment_url: "https://public-acme.g1t.page", updated_at: "2026-10-15T01:00:00Z" };
58+ return Response.json({ ok: true, value: { environments: [{ name: "production", production_environment: true, updated_at: current.updated_at, current, latest: { ...current, id: "dpl_next", sha: "bbbbbbb", state: "in_progress" } }] } });
59+ },
60+ SCREENSHOTS: { name: "web-deploy-check", entrypoint: "ScreenshotStub" },
3861 },
3962 }));
4063 try {
4164 const origin = await runtime.ready;
4265 const request = (path, init) => fetch(new URL(path, origin), { ...init, signal: AbortSignal.timeout(15_000) });
4366 assert.equal((await request("/__seed")).status, 200);
67+ const screenshot = await request("/acme/public/production.jpg?v=abcdef1234567-" + Date.parse("2026-10-15T01:00:00Z"));
68+ assert.equal(screenshot.status, 200, "Signed-out readers can see public production screenshots");
69+ assert.equal(screenshot.headers.get("content-type"), "image/jpeg");
70+ assert.equal(screenshot.headers.get("cache-control"), "private, max-age=31536000, immutable");
71+ assert.equal(await screenshot.text(), "production-preview");
72+ assert.equal((await request("/acme/private/production.jpg")).status, 404, "Unreadable projects never expose screenshots");
73+ console.log("Public production screenshots: anonymous access, private-project denial, and serving commit selection verified.");
4474 for (const [path, destination] of [["/developers", "https://docs.g1t.sh/reference/api/"], ["/openapi.json", "https://api.g1t.sh/openapi.json"], ["/mcp", "https://mcp.g1t.sh"]]) {
4575 const answer = await request(path, { redirect: "manual" });
4676 assert.equal(answer.status, 308);

Comments

Sign in to comment.