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.

58 linesCodeBlameRaw
1// Exercise the built Worker in workerd before it can be deployed. Storage is
2// temporary and local; no service bindings or Cloudflare credentials are used.
3import assert from "node:assert/strict";
4import { readFileSync, readdirSync } from "node:fs";
5import { join, resolve } from "node:path";
6import { Miniflare, convertV4MiniflareOptions } from "miniflare";
7
8const root = resolve("build/server");
9const config = JSON.parse(readFileSync(join(root, "wrangler.json"), "utf8"));
10const source = readFileSync(join(root, "index.js"), "utf8");
11const build = source.match(/searchParams\.set\("_g1t_build", "([^"]+)"\)/)?.[1];
12assert.ok(build, "The page cache must be separated by build");
13const css = "body{color:red}";
14// Seed R2 and the page cache through a test-only entry, keeping all R2 I/O
15// inside workerd. The application entry itself is the unmodified build.
16const seed = `import app from './index.js';
17export default { async fetch(request, env, ctx) {
18 if (new URL(request.url).pathname === '/__seed') {
19 await env.DOWNLOADS.put('web-assets/assets/app-old12345.css', ${JSON.stringify(css)}, {httpMetadata:{contentType:'text/css'}});
20 const cache = new URL('/pricing.data', request.url);
21 cache.searchParams.set('_g1t_build', ${JSON.stringify(build)});
22 await caches.default.put(new Request(cache), new Response('wrong-build-cache', {headers:{'cache-control':'public, max-age=300','x-g1t-kept-at':String(Date.now())}}));
23 return new Response('seeded');
24 }
25 return app.fetch(request, env, ctx);
26}};`;
27const files = [join(root, "index.js"), ...readdirSync(join(root, "assets")).filter((name) => name.endsWith(".js")).map((name) => join(root, "assets", name))];
28const runtime = new Miniflare(convertV4MiniflareOptions({
29 name: "web-deploy-check", cf: false, modulesRoot: root,
30 modules: [{ type: "ESModule", path: join(root, "deploy-check.mjs"), contents: seed }, ...files.map((path) => ({ type: "ESModule", path }))],
31 compatibilityDate: config.compatibility_date,
32 compatibilityFlags: config.compatibility_flags,
33 r2Buckets: ["DOWNLOADS"],
34}));
35try {
36 const origin = await runtime.ready;
37 const request = (path, init) => fetch(new URL(path, origin), { ...init, signal: AbortSignal.timeout(15_000) });
38 assert.equal((await request("/__seed")).status, 200);
39 const old = await request("/assets/app-old12345.css");
40 assert.equal(old.status, 200);
41 assert.equal(old.headers.get("content-type"), "text/css");
42 assert.equal(await old.text(), css);
43 const missing = await request("/assets/app-missing0.css");
44 assert.equal(missing.status, 404);
45 assert.equal(missing.headers.get("cache-control"), "no-store");
46 assert.doesNotMatch(missing.headers.get("content-type") ?? "", /html/);
47 for (const path of ["/pricing.data", "/flagon-io/-/home.data?tab=one&_routes=root"]) {
48 const answer = await request(path, { headers: { "x-g1t-build": "previous-build" } });
49 assert.equal(answer.status, 202);
50 const body = await answer.text();
51 assert.match(body, /reload/);
52 assert.ok(body.includes(path.split(".data")[0]));
53 assert.doesNotMatch(body, /_routes|wrong-build-cache/);
54 }
55 console.log("Deployment safety: old CSS survives, missing assets stay isolated, old navigations bypass cache and loaders.");
56} finally {
57 await runtime.dispose();
58}