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
30 changes: 29 additions & 1 deletion .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,13 @@ name: docs
#
# It runs on pull_request as well as main because deploy.yml only runs on push to
# main, so a PR-time signal needs its own workflow. It is cheap: no Docker, no
# Cloudflare credentials, no deploy — typedoc over one entry point.
# Cloudflare credentials — typedoc over one entry point.
#
# `site` builds the documentation site (apps/docs) on every run — the build fails
# on a broken internal link — and on main deploys it to its Worker, which serves
# flare-dispatch.fractalbox.dev. Its pages are symbolic links to Markdown across
# the repo (guides, run catalog, action READMEs, specs), so any path can change
# the site and the workflow carries no path filter.
on:
pull_request:
push:
Expand Down Expand Up @@ -44,3 +50,25 @@ jobs:
git diff --cached -- apps/docs/reference
exit 1
fi

site:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
- uses: actions/setup-node@v4
with:
node-version: 22
cache: pnpm
- run: pnpm install --frozen-lockfile
- run: pnpm --filter @fractalboxdev/flare-dispatch-docs run build
# Static assets only, so the deploy token needs Workers Scripts:Edit and no
# zone grant: the custom domain is already attached, and wrangler only
# re-asserts it.
- name: Deploy
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
working-directory: apps/docs
run: pnpm exec wrangler deploy
env:
CLOUDFLARE_API_TOKEN: ${{ secrets.CLOUDFLARE_API_TOKEN }}
CLOUDFLARE_ACCOUNT_ID: ${{ secrets.CLOUDFLARE_ACCOUNT_ID }}
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ A run can be started from three sources:

## Deployment model

Single-tenant BYOC — no multi-tenant SaaS. Deploy with `wrangler deploy` into your own Cloudflare account. Default deploy domain: `flare-dispatch.fractalbox.dev`.
Single-tenant BYOC — no multi-tenant SaaS. Deploy with `wrangler deploy` into your own Cloudflare account. The dispatcher serves `flare-dispatch-app.fractalbox.dev` (API, GitHub App webhook, dashboard); the documentation site (`apps/docs`) serves `flare-dispatch.fractalbox.dev`.

## Conventions

Expand Down
26 changes: 14 additions & 12 deletions apps/dispatcher/src/routes/github.ts
Original file line number Diff line number Diff line change
Expand Up @@ -177,10 +177,12 @@ const jsonError = (error: string, message: string, status: number): Response =>
// there's zero JS to ship for theming. If the design system on the docs site
// evolves materially, mirror the token block below.

/** Canonical docs/landing origin — the pages link back here for the full story. */
/** The documentation site (apps/docs) — the pages link back here for the full story. */
const DOCS_ORIGIN = "https://flare-dispatch.fractalbox.dev";
/** The public repo — `#quickstart` is the deploy-from-zero entry point. */
const REPO_URL = "https://github.com/fractalbox/flare-dispatch";
/** Deploying a Dispatcher and wiring its GitHub App, end to end. */
const DEPLOY_GUIDE = `${DOCS_ORIGIN}/actions/deploy-dispatcher-action/`;
/** The public repo. */
const REPO_URL = "https://github.com/fractalboxdev/flare-dispatch";

/**
* The shared stylesheet. Inlined into every install page's `<head>`. A trimmed
Expand Down Expand Up @@ -287,10 +289,10 @@ const brandPage = (opts: {
<footer class="colophon container">
BYOC · runs in your own Cloudflare account
<nav>
<a href="${DOCS_ORIGIN}/docs/prd">PRD</a>
<a href="${DOCS_ORIGIN}/docs/05-byoc">BYOC setup</a>
<a href="${DOCS_ORIGIN}/recipes">Recipes</a>
<a href="${REPO_URL}#quickstart">Quickstart</a>
<a href="${DOCS_ORIGIN}/">Docs</a>
<a href="${DEPLOY_GUIDE}">Deploy guide</a>
<a href="${DOCS_ORIGIN}/runs/">Run catalog</a>
<a href="${REPO_URL}">Source</a>
</nav>
</footer>
${opts.tail ?? ""}
Expand Down Expand Up @@ -619,7 +621,7 @@ ${clientSecret}</pre>
<p class="cta"><a class="btn" href="${installUrl}" rel="noreferrer noopener">Install ${name}</a></p>

<h2>3. Verify</h2>
<p>After installing, dispatch a run from a workflow on the installed repo — the Dispatcher will create a check-run on the commit. See the <a href="${DOCS_ORIGIN}/docs/05-byoc">BYOC setup spec</a> for the end-to-end walkthrough.</p>`,
<p>After installing, dispatch a run from a workflow on the installed repo — the Dispatcher will create a check-run on the commit. See the <a href="${DEPLOY_GUIDE}">deploy guide</a> for the end-to-end walkthrough.</p>`,
});
};

Expand Down Expand Up @@ -740,7 +742,7 @@ const renderInstallLlms = (origin: string): string => `# Install FlareDispatch

- Dispatcher origin: ${origin}
- Full docs: ${DOCS_ORIGIN}
- Source + quickstart: ${REPO_URL}#quickstart
- Source: ${REPO_URL}

## Prerequisites (verify before starting)

Expand Down Expand Up @@ -793,15 +795,15 @@ choice of scope — surface the link, don't guess.)

Open a pull request (or dispatch a run) on an installed repo. Within a few
seconds a FlareDispatch Check Run should appear on the commit. If it does, the
install is complete. If not, see the BYOC setup spec:
${DOCS_ORIGIN}/docs/05-byoc
install is complete. If not, see the deploy guide:
${DEPLOY_GUIDE}

## Notes

- Idempotent: re-running \`wrangler secret put\` overwrites; re-running
\`wrangler deploy\` is safe.
- Pure-webhook mode means no \`.github/workflows\` file is required — installing
the App is the trigger. Details: ${DOCS_ORIGIN}/docs/05-byoc
the App is the trigger. Details: ${DOCS_ORIGIN}/runs/
`;

/**
Expand Down
3 changes: 3 additions & 0 deletions apps/docs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
/dist/
/.astro/
/.wrangler/
22 changes: 19 additions & 3 deletions apps/docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,22 @@ committed output differs from what the current source produces. A hand-maintaine
drifts from the types consumers actually pin, and the drift stays invisible until someone follows it
into a compile error.

No site build exists in this repo yet. These files are plain markdown a static generator can consume
unchanged, and they read correctly on GitHub in the meantime; [`llms.txt`](llms.txt) indexes them for
agents that fetch raw markdown.
The site at <https://flare-dispatch.fractalbox.dev> is an Astro Starlight build of this directory.
Every page under [`src/content/docs/`](src/content/docs/) except the landing page is a relative
symbolic link to Markdown elsewhere in the repo — these guides, the run catalog, the action READMEs,
the specs and ADRs — so each file has one copy and reads the same on GitHub and on the site.
[`src/lib/pages.mjs`](src/lib/pages.mjs) derives each page's title and description from its first
heading and paragraph, and rewrites relative links to site URLs (or to GitHub, for files that are not
pages). The site also serves `/llms.txt` and `/llms-full.txt`, generated from the same pages.

To publish another file, add a symbolic link under `src/content/docs/` and a sidebar entry in
[`astro.config.mjs`](astro.config.mjs). The build fails on a broken internal link.

```sh
pnpm --filter @fractalboxdev/flare-dispatch-docs run dev # local preview
pnpm --filter @fractalboxdev/flare-dispatch-docs run build # what CI runs
```

`.github/workflows/docs.yml` builds the site on every pull request and deploys it on `main`, to a
static-assets Worker (`wrangler.jsonc`). The dispatcher Worker — API, webhooks, dashboard, log
viewer — serves `flare-dispatch-app.fractalbox.dev`.
70 changes: 70 additions & 0 deletions apps/docs/astro.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
// @ts-check
import { defineConfig } from "astro/config";
import { satteri } from "@astrojs/markdown-satteri";
import starlight from "@astrojs/starlight";
import starlightLinksValidator from "starlight-links-validator";
import { README_PAGES, repoLinks } from "./src/lib/pages.mjs";

const DESCRIPTION =
"FlareDispatch offloads the expensive half of GitHub Actions — agentic review, Playwright e2e, acceptance suites, matrix fan-outs — onto a Cloudflare stack you own.";

export default defineConfig({
site: "https://flare-dispatch.fractalbox.dev",
trailingSlash: "always",
markdown: {
// Pages are repository Markdown whose relative links point at sibling files, as on
// GitHub; on the site they point at pages (src/lib/pages.mjs).
processor: satteri({ hastPlugins: [repoLinks] }),
},
integrations: [
starlight({
title: "FlareDispatch",
description: DESCRIPTION,
social: [{ icon: "github", label: "GitHub", href: "https://github.com/fractalboxdev/flare-dispatch" }],
lastUpdated: false,
customCss: ["./src/styles/brand.css"],
head: [{ tag: "link", attrs: { rel: "alternate", type: "text/plain", href: "/llms.txt", title: "llms.txt" } }],
plugins: [
starlightLinksValidator({
errorOnLocalLinks: true,
exclude: ({ link }) =>
// Pages outside the docs collection.
["/llms.txt", "/llms-full.txt"].includes(link) ||
// README.md pages served at their directory (src/lib/pages.mjs).
README_PAGES.has(link.replace(/#.*$/, "")),
}),
],
sidebar: [
{ label: "Overview", link: "/" },
{
label: "GitHub Actions",
items: ["actions", "actions/flare-dispatch-action", "actions/deploy-dispatcher-action"],
},
{ label: "Run catalog", slug: "runs" },
{
label: "Substrate",
items: [
"substrate",
"substrate/facade",
"substrate/grant-profiles",
"substrate/byoc-upgrade",
"substrate/contract-versioning",
],
},
{ label: "API reference", items: [{ label: "Substrate facade", slug: "reference/substrate-contract" }] },
{
label: "Design records",
collapsed: true,
items: [
{ label: "Dispatcher ADRs", collapsed: true, items: [{ autogenerate: { directory: "design/adr" } }] },
{ label: "Dispatcher specs", collapsed: true, items: [{ autogenerate: { directory: "design/dispatcher" } }] },
{ label: "Substrate specs", collapsed: true, items: [{ autogenerate: { directory: "design/substrate" } }] },
],
},
],
}),
],
vite: {
server: { allowedHosts: [".ts.net"] },
},
});
40 changes: 0 additions & 40 deletions apps/docs/llms.txt

This file was deleted.

22 changes: 22 additions & 0 deletions apps/docs/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"name": "@fractalboxdev/flare-dispatch-docs",
"private": true,
"type": "module",
"scripts": {
"dev": "astro dev",
"build": "astro build",
"preview": "astro preview",
"check": "astro check",
"deploy": "astro build && wrangler deploy"
},
"devDependencies": {
"@astrojs/check": "0.9.10",
"@astrojs/markdown-satteri": "0.4.1",
"@astrojs/starlight": "0.42.2",
"astro": "7.3.3",
"cookie": "2.0.1",
"starlight-links-validator": "0.26.0",
"typescript": "^5.6.3",
"wrangler": "^4.50.0"
}
}
3 changes: 3 additions & 0 deletions apps/docs/public/_headers
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
# Cloudflare Workers static assets: response headers by path.
/_astro/*
Cache-Control: public, max-age=31536000, immutable
24 changes: 24 additions & 0 deletions apps/docs/src/content.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { defineCollection } from "astro:content";
import { docsLoader } from "@astrojs/starlight/loaders";
import { docsSchema } from "@astrojs/starlight/schema";
import { pageFrontmatter, pageId } from "./lib/pages.mjs";

// The docs collection is Starlight's glob over src/content/docs/, whose entries are
// relative symbolic links into the repository. Repository Markdown carries no front
// matter: its title comes from the first `# heading` and its description from the
// first paragraph, and README.md becomes the index of its directory.
const loader = docsLoader({ generateId: pageId });

const docs = defineCollection({
loader: {
name: "flare-dispatch-docs-loader",
load: (context) =>
loader.load({
...context,
parseData: (props) => context.parseData({ ...props, data: pageFrontmatter(props.data, props.filePath) }),
}),
},
schema: docsSchema(),
});

export const collections = { docs };
1 change: 1 addition & 0 deletions apps/docs/src/content/docs/actions/index.md
1 change: 1 addition & 0 deletions apps/docs/src/content/docs/design/adr
1 change: 1 addition & 0 deletions apps/docs/src/content/docs/design/dispatcher
1 change: 1 addition & 0 deletions apps/docs/src/content/docs/design/substrate
55 changes: 55 additions & 0 deletions apps/docs/src/content/docs/index.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
---
title: BYOC CI/CD on Cloudflare
description: FlareDispatch offloads the expensive half of GitHub Actions — agentic review, Playwright e2e, acceptance suites, matrix fan-outs — onto a Cloudflare stack you own.
template: splash
hero:
title: FlareDispatch
tagline: Offload the expensive half of GitHub Actions onto a Cloudflare stack you own. Runs are typed Effect-TS programs, not YAML.
actions:
- text: Offload a job
link: /actions/flare-dispatch-action/
icon: right-arrow
- text: Run catalog
link: /runs/
variant: minimal
- text: GitHub
link: https://github.com/fractalboxdev/flare-dispatch
icon: external
variant: minimal
---

import { Card, CardGrid, LinkCard } from "@astrojs/starlight/components";

FlareDispatch is BYOC CI/CD. Heavy jobs — agentic code review, Playwright e2e, acceptance suites, matrix fan-outs, security scans — run on Cloudflare instead of GitHub-hosted runners. Each organization deploys one Dispatcher Worker and one GitHub App into its own Cloudflare account; there is no multi-tenant service.

<CardGrid>
<Card title="Workflows" icon="random">
Orchestration. Every run is a durable Workflow instance with a step timeline.
</Card>
<Card title="Containers" icon="laptop">
Execution. Jobs run in sandboxed containers under deny-all egress.
</Card>
<Card title="Browser Rendering" icon="seti:html">
Playwright e2e and recorded product demos over CDP.
</Card>
<Card title="R2" icon="document">
Cache and artifacts, with tokened log links on every check-run.
</Card>
</CardGrid>

## Triggers

A run starts from a **GitHub Actions** step (`flare-dispatch-action` HMAC-signs and POSTs the dispatch), a **GitHub App webhook** (repo events fire runs directly), or a **cron schedule**. The verdict lands on the pull request as a `flare-dispatch/<run>` check-run; gate branch protection on that name.

## Runs are programs

Runs are written against a layered Effect-TS DSL: **capabilities** wrap Cloudflare primitives, **primitives** compose them into build, test and cache steps, and **recipes** are the named runs a Dispatcher registers. Steps are typed, errors are tagged, and matches are exhaustive.

## Start here

<CardGrid>
<LinkCard title="Offload a job" href="/actions/flare-dispatch-action/" description="Dispatch a run from a GitHub Actions step and gate on its check-run." />
<LinkCard title="Deploy a Dispatcher" href="/actions/deploy-dispatcher-action/" description="Ship the Worker into your own account from an operator overlay." />
<LinkCard title="Run catalog" href="/runs/" description="The runs a Dispatcher registers, their inputs, and where their logs live." />
<LinkCard title="Substrate" href="/substrate/" description="The execution environment for agentic work, consumed through a service-binding facade." />
</CardGrid>
1 change: 1 addition & 0 deletions apps/docs/src/content/docs/reference
1 change: 1 addition & 0 deletions apps/docs/src/content/docs/runs.md
1 change: 1 addition & 0 deletions apps/docs/src/content/docs/substrate
Loading
Loading