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
18 changes: 18 additions & 0 deletions .github/workflows/cloudflare-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,24 @@ jobs:
OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
run: pnpm alchemy deploy --stage prod --yes

- name: Smoke Cloudflare production llms.txt
run: |
node --input-type=module <<'NODE'
import assert from "node:assert/strict";

const url = "https://schematics.run/llms.txt";
const response = await fetch(url, {
headers: { Accept: "text/plain", "Cache-Control": "no-cache" },
signal: AbortSignal.timeout(15_000),
});
const contentType = response.headers.get("content-type") ?? "";
console.log(`${url}: ${response.status}, ${contentType}, cf-cache-status=${response.headers.get("cf-cache-status")}`);
assert.equal(response.status, 200, "/llms.txt must return 200");
assert.match(contentType, /^text\/plain(?:;|$)/i, "/llms.txt must be text/plain");
assert((await response.text()).startsWith("# Schematics"), "/llms.txt must start with '# Schematics'");
console.log("Production llms.txt smoke passed.");
NODE

- name: Resolve Cloudflare production API URL
id: production-api
env:
Expand Down
15 changes: 2 additions & 13 deletions alchemy.run.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { Stage } from "alchemy";
import * as Effect from "effect/Effect";
import * as Layer from "effect/Layer";
import ApiWorker from "./alchemy/schematics-api-worker.ts";
import { playgroundMemo } from "./alchemy/playground-memo.ts";
import {
PROD_API_BASE_URL,
PROD_PLAYGROUND_HOSTNAME,
Expand Down Expand Up @@ -62,19 +63,7 @@ export default Alchemy.Stack(
htmlHandling: "auto-trailing-slash",
notFoundHandling: "single-page-application",
},
memo: {
include: [
"apps/playground/**",
"alchemy/**",
"packages/*/src/**",
"packages/*/package.json",
"package.json",
"pnpm-lock.yaml",
"pnpm-workspace.yaml",
"tsconfig.base.json",
"vitest.aliases.ts",
],
},
memo: playgroundMemo,
});

if (shouldCommentOnPullRequest) {
Expand Down
17 changes: 17 additions & 0 deletions alchemy/playground-memo.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { MemoOptions } from "alchemy/Command/Memo";

// Alchemy evaluates these globs from the Vite root (apps/playground), not
// the repository root. Include public assets so edits trigger an upload.
export const playgroundMemo = {
lockfile: true,
include: [
"**/*",
"../../alchemy/**",
"../../packages/*/src/**",
"../../packages/*/package.json",
"../../package.json",
"../../pnpm-workspace.yaml",
"../../tsconfig.base.json",
"../../vitest.aliases.ts",
],
} satisfies MemoOptions;
86 changes: 86 additions & 0 deletions packages/server/test/playground-deploy.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { describe, expect, it } from "@effect/vitest";
import { NodeFileSystem, NodePath } from "@effect/platform-node";
import { hashDirectory } from "alchemy/Command/Memo";
import { readAssets } from "alchemy/Cloudflare";
import { Effect } from "effect";
import { mkdir, mkdtemp, readFile, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { playgroundMemo } from "../../../alchemy/playground-memo";

describe("playground deployment", () => {
it("invalidates Alchemy's memo when public assets or workspace inputs change", async () => {
const root = await mkdtemp(join(tmpdir(), "schematics-playground-memo-"));
const playground = join(root, "apps/playground");
const inputs = [
"apps/playground/public/llms.txt",
"apps/playground/public/_headers",
"apps/playground/src/main.tsx",
"alchemy/playground-memo.ts",
"packages/core/src/index.ts",
"packages/core/package.json",
"package.json",
"pnpm-lock.yaml",
"pnpm-workspace.yaml",
"tsconfig.base.json",
"vitest.aliases.ts",
];
const hash = () =>
Effect.runPromise(
hashDirectory({ cwd: playground, memo: playgroundMemo }).pipe(
Effect.provide([NodeFileSystem.layer, NodePath.layer]),
),
);

try {
await mkdir(join(root, ".git"));
await writeFile(join(root, ".gitignore"), "dist/\nnode_modules/\n");
for (const input of inputs) {
await mkdir(dirname(join(root, input)), { recursive: true });
await writeFile(join(root, input), "original");
}
let previous = await hash();
for (const input of inputs) {
await writeFile(join(root, input), "changed");
const current = await hash();
expect(current, input).not.toBe(previous);
previous = current;
}
await rm(join(playground, "public/llms.txt"));
expect(await hash(), "removing llms.txt must also invalidate the memo").not.toBe(previous);
previous = await hash();
await writeFile(join(playground, "public/llms.txt"), "new asset");
expect(await hash(), "adding llms.txt must invalidate the memo").not.toBe(previous);
previous = await hash();
await mkdir(join(playground, "dist"));
await writeFile(join(playground, "dist/llms.txt"), "build output");
expect(await hash(), "ignored build output must not trigger a rebuild").toBe(previous);
} finally {
await rm(root, { recursive: true, force: true });
}
});

it("includes llms.txt and text headers in Alchemy's assets with SPA fallback enabled", async () => {
const directory = await mkdtemp(join(tmpdir(), "schematics-playground-assets-"));
const publicDir = new URL("../../../apps/playground/public/", import.meta.url);
const source = await readFile(new URL("llms.txt", publicDir), "utf8");
const headers = await readFile(new URL("_headers", publicDir), "utf8");

try {
await writeFile(join(directory, "llms.txt"), source);
await writeFile(join(directory, "_headers"), headers);
await writeFile(join(directory, "index.html"), "<!doctype html><html></html>");
const assets = await Effect.runPromise(
readAssets({ directory, notFoundHandling: "single-page-application" }).pipe(
Effect.provide([NodeFileSystem.layer, NodePath.layer]),
),
);
expect(assets.manifest["/llms.txt"]?.size).toBe(Buffer.byteLength(source));
expect(assets.manifest["/index.html"]).toBeDefined();
expect(assets.manifest["/_headers"]).toBeUndefined();
expect(assets.config?.headers).toBe(headers);
} finally {
await rm(directory, { recursive: true, force: true });
}
});
});
9 changes: 9 additions & 0 deletions turbo.json
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@
"dependsOn": ["build"],
"outputs": []
},
"@schematics/server#test": {
"dependsOn": ["build"],
"inputs": [
"$TURBO_DEFAULT$",
"$TURBO_ROOT$/alchemy/playground-memo.ts",
"$TURBO_ROOT$/apps/playground/public/**"
],
"outputs": []
},
"typecheck": {
"dependsOn": ["build"],
"outputs": []
Expand Down
Loading