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
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
"build": "astro build",
"preview": "astro preview",
"check-links": "node scripts/check-links.mjs",
"refresh-demo-runs": "node scripts/refresh-demo-runs.mjs",
"astro": "astro"
},
"dependencies": {
Expand Down
8 changes: 8 additions & 0 deletions public/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ Configuration happens in the web UI: register GitHub Apps, bind repos, and mint
2. The worker mints an installation token, opens a Check Run, fetches the exact commit, detaches the checkout, and strips the remote before any step runs.
3. Pipeline steps run in-process, or via `docker run` when `runtime:` is set. The Check Run carries a truncated log tail, and the full log stays on the details page.

## Live demo

Six pull requests on [openpreflight/demo](https://github.com/openpreflight/demo) produce real Check Runs on a self-hosted instance (not GitHub Actions). The log pages are the same `/runs/{id}` pages you get behind auth; shareable logs are on for that binding only.

Outcomes: passing, failing test, failing build, timeout, skipped, container runtime. Links to individual run pages can 404 after log retention prunes a job — the site falls back to the pull request.

See https://openpreflight.xyz/#demo

## Not in v1

- GitHub Actions YAML
Expand Down
21 changes: 21 additions & 0 deletions scripts/check-links.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,27 @@ const allowedExternalOrigins = [
'https://www.apache.org/licenses/LICENSE-2.0',
];

// Shareable demo log pages live on whatever host public_base_url is. Allow
// those origins from demo-runs.json so a refresh cannot break link CI.
try {
const demoRuns = JSON.parse(
readFileSync(join(root, 'src', 'data', 'demo-runs.json'), 'utf8'),
);
for (const run of demoRuns.runs ?? []) {
if (typeof run.runUrl !== 'string' || !run.runUrl) continue;
try {
const origin = new URL(run.runUrl).origin;
if (origin.startsWith('https://') && !allowedExternalOrigins.includes(origin)) {
allowedExternalOrigins.push(origin);
}
} catch {
/* ignore unparseable runUrl; the HTML check will still flag it */
}
}
} catch {
/* file missing until the demo section lands */
}

const missing = required.filter((p) => !existsSync(join(dist, p)));
if (missing.length) {
console.error('Missing required dist paths:');
Expand Down
144 changes: 144 additions & 0 deletions scripts/refresh-demo-runs.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
#!/usr/bin/env node
/**
* Rewrite src/data/demo-runs.json from public GitHub Checks on openpreflight/demo.
*
* Never calls the demo instance. UI uses prUrl always; runUrl is optional.
* If a Check Run cannot be resolved, prUrl stays and runUrl is cleared — never
* invent a /runs/ URL that would 404.
*
* Usage (from website/):
* npm run refresh-demo-runs
*
* Optional: GITHUB_TOKEN for a higher API rate limit. Unauthenticated works
* on a public repo.
*/
import { readFileSync, writeFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

const root = join(dirname(fileURLToPath(import.meta.url)), "..");
const dataPath = join(root, "src", "data", "demo-runs.json");
const OWNER = "openpreflight";
const REPO = "demo";
const CHECK_NAME = /openpreflight/i;
const RUN_PATH = /\/runs\/[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;

const token = process.env.GITHUB_TOKEN || process.env.GH_TOKEN || "";

/** @param {string} path */
async function gh(path) {
const headers = {
Accept: "application/vnd.github+json",
"User-Agent": "openpreflight-refresh-demo-runs",
"X-GitHub-Api-Version": "2022-11-28",
};
if (token) headers.Authorization = `Bearer ${token}`;
const res = await fetch(`https://api.github.com${path}`, { headers });
if (!res.ok) {
const body = await res.text();
throw new Error(`GitHub ${res.status} ${path}: ${body.slice(0, 400)}`);
}
return res.json();
}

/** @param {unknown} url */
function shareableRunUrl(url) {
if (typeof url !== "string" || !url) return null;
try {
const parsed = new URL(url);
if (parsed.protocol !== "https:") return null;
if (!RUN_PATH.test(parsed.pathname)) return null;
return parsed.href;
} catch {
return null;
}
}

/**
* @param {Array<{ name?: string, details_url?: string, completed_at?: string | null, started_at?: string | null }>} checks
*/
function pickCheck(checks) {
const named = checks.filter((c) => CHECK_NAME.test(c.name ?? ""));
const pool = named.length ? named : checks;
return [...pool].sort((a, b) => {
const ta = Date.parse(a.completed_at || a.started_at || 0);
const tb = Date.parse(b.completed_at || b.started_at || 0);
return tb - ta;
})[0];
}

const data = JSON.parse(readFileSync(dataPath, "utf8"));
if (!data?.runs?.length) {
console.error("demo-runs.json has no runs[]");
process.exit(1);
}

let pulls;
try {
pulls = await gh(`/repos/${OWNER}/${REPO}/pulls?state=open&per_page=100`);
} catch (err) {
console.error(
"Could not list PRs; leaving demo-runs.json unchanged.\n",
err instanceof Error ? err.message : err,
);
process.exit(1);
}

if (!Array.isArray(pulls)) {
console.error("Unexpected pulls payload; leaving demo-runs.json unchanged.");
process.exit(1);
}

/** @type {Map<string, { html_url: string, sha: string }>} */
const byBranch = new Map();
for (const pr of pulls) {
const ref = pr.head?.ref;
const sha = pr.head?.sha;
if (typeof ref === "string" && typeof sha === "string" && pr.html_url) {
byBranch.set(ref, { html_url: pr.html_url, sha });
}
}

let resolved = 0;
let missingRun = 0;

for (const run of data.runs) {
const pr = byBranch.get(run.branch);
if (pr) {
run.prUrl = pr.html_url;
}
// Always keep a GitHub link even if the PR is gone.
if (!run.prUrl) {
run.prUrl = `https://github.com/${OWNER}/${REPO}`;
}

if (!pr) {
run.runUrl = null;
missingRun += 1;
continue;
}

try {
const payload = await gh(
`/repos/${OWNER}/${REPO}/commits/${pr.sha}/check-runs`,
);
const check = pickCheck(payload.check_runs ?? []);
const url = shareableRunUrl(check?.details_url);
run.runUrl = url;
if (url) resolved += 1;
else missingRun += 1;
} catch (err) {
console.error(
`check-runs failed for ${run.branch}; clearing runUrl.\n`,
err instanceof Error ? err.message : err,
);
run.runUrl = null;
missingRun += 1;
}
}

data.updatedAt = new Date().toISOString();
writeFileSync(dataPath, `${JSON.stringify(data, null, 2)}\n`);
console.log(
`wrote ${dataPath}: ${resolved} runUrl(s), ${missingRun} without a shareable log (prUrl kept).`,
);
1 change: 1 addition & 0 deletions src/components/blocks/site-header-01.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ const defaultLinks: SiteHeaderLink[] = [
{ label: "Product", href: "#product" },
{ label: "How it runs", href: "#how" },
{ label: "Pipeline", href: "#pipeline" },
{ label: "Demo", href: "#demo" },
{ label: "Docs", href: "https://docs.openpreflight.xyz" },
];

Expand Down
97 changes: 97 additions & 0 deletions src/components/templates/saas-landing-01.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,40 @@ import { SiteHeader01 } from "@/components/blocks/site-header-01";
import { Badge } from "@/components/ui/badge";
import { Button } from "@/components/ui/button";
import { cn } from "@/lib/utils";
import demoRuns from "@/data/demo-runs.json";

const DOCS = "https://docs.openpreflight.xyz";
const REPO = "https://github.com/openpreflight/openpreflight";
const DEMO_REPO = "https://github.com/openpreflight/demo";

function outcomeLabel(conclusion: string) {
switch (conclusion) {
case "success":
return "passed";
case "failure":
return "failed";
case "timed_out":
return "timed out";
case "skipped":
return "skipped";
default:
return conclusion;
}
}

function outcomeClass(conclusion: string) {
switch (conclusion) {
case "success":
return "text-[var(--pass,#2f6f4f)]";
case "failure":
case "timed_out":
return "text-red-700 dark:text-red-400";
case "skipped":
return "text-muted-foreground";
default:
return "text-muted-foreground";
}
}
const checkSteps = [
{ name: "install", command: "npm ci", duration: "8s", width: "19%" },
{ name: "test", command: "go test ./...", duration: "21s", width: "50%" },
Expand Down Expand Up @@ -90,6 +121,7 @@ function SaasLanding01({ className, ...props }: React.ComponentProps<"div">) {
{ label: "Product", href: "#product" },
{ label: "How it runs", href: "#how" },
{ label: "Pipeline", href: "#pipeline" },
{ label: "Demo", href: "#demo" },
{ label: "Docs", href: DOCS },
]}
ctaLabel="Quickstart"
Expand Down Expand Up @@ -364,6 +396,71 @@ timeout: 15m`}</code>
</div>
</section>

<section className="px-5 py-24 sm:px-8 sm:py-32" id="demo">
<div className="mx-auto max-w-7xl">
<Badge variant="secondary">
<Check className="size-3.5" /> Live demo
</Badge>
<h2 className="mt-6 max-w-3xl text-balance text-4xl font-semibold tracking-[-.05em] sm:text-5xl">
Real Check Runs, public log pages
</h2>
<p className="mt-5 max-w-2xl text-base leading-relaxed text-muted-foreground">
These are real Check Runs on{" "}
<a
className="font-mono text-sm text-foreground underline-offset-4 hover:underline"
href={DEMO_REPO}
>
openpreflight/demo
</a>
, produced by a self-hosted instance — not GitHub Actions. The
log pages are the same pages you get behind auth; shareable logs
are on for that one binding. A run URL can 404 after retention
prunes the job — the pull request stays.
</p>
<ul className="mt-12 grid gap-8 sm:grid-cols-2 lg:grid-cols-3">
{demoRuns.runs.map((entry) => (
<li key={entry.branch}>
<p
className={cn(
"font-mono text-xs font-medium uppercase tracking-wide",
outcomeClass(entry.conclusion),
)}
>
{outcomeLabel(entry.conclusion)}
</p>
<h3 className="mt-2 font-mono text-lg font-semibold tracking-tight">
{entry.title}
</h3>
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
{entry.outcome}
</p>
<div className="mt-4 flex flex-wrap gap-x-4 gap-y-2 text-sm">
{entry.runUrl ? (
<a
className="text-foreground underline-offset-4 hover:underline"
href={entry.runUrl}
>
run log
</a>
) : null}
<a
className="text-muted-foreground underline-offset-4 hover:underline"
href={entry.prUrl}
>
pull request
</a>
</div>
</li>
))}
</ul>
<p className="mt-10 max-w-2xl text-sm text-muted-foreground">
The demo instance is a live box and may occasionally be down. If a
run link is missing, use the pull request — the Check Run on
GitHub still points at the details URL when the instance is up.
</p>
</div>
</section>

<section className="px-5 pb-8 sm:px-8" id="run">
<div className="mx-auto max-w-7xl overflow-hidden rounded-[2rem] border border-foreground/10 bg-background px-6 py-16 sm:px-10">
<div className="mx-auto max-w-2xl text-center">
Expand Down
60 changes: 60 additions & 0 deletions src/data/demo-runs.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
{
"repo": "openpreflight/demo",
"updatedAt": "2026-08-29T02:25:16.474Z",
"runs": [
{
"id": "passing",
"title": "Passing",
"outcome": "three steps, all green",
"branch": "demo/passing",
"conclusion": "success",
"prUrl": "https://github.com/openpreflight/demo/pull/7",
"runUrl": null
},
{
"id": "failing-test",
"title": "Failing test",
"outcome": "real node:test assertion failure",
"branch": "demo/failing-test",
"conclusion": "failure",
"prUrl": "https://github.com/openpreflight/demo/pull/8",
"runUrl": null
},
{
"id": "failing-build",
"title": "Failing build",
"outcome": "real build / syntax error",
"branch": "demo/failing-build",
"conclusion": "failure",
"prUrl": "https://github.com/openpreflight/demo/pull/9",
"runUrl": null
},
{
"id": "timeout",
"title": "Timeout",
"outcome": "pipeline deadline (timeout: 5s, sleep 60)",
"branch": "demo/timeout",
"conclusion": "timed_out",
"prUrl": "https://github.com/openpreflight/demo/pull/10",
"runUrl": null
},
{
"id": "skipped",
"title": "Skipped",
"outcome": "nothing to run → Check Run skipped",
"branch": "demo/skipped",
"conclusion": "skipped",
"prUrl": "https://github.com/openpreflight/demo/pull/11",
"runUrl": null
},
{
"id": "container",
"title": "Container",
"outcome": "runtime: node:24 via Docker",
"branch": "demo/container",
"conclusion": "success",
"prUrl": "https://github.com/openpreflight/demo/pull/12",
"runUrl": null
}
]
}
1 change: 1 addition & 0 deletions tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
],
"compilerOptions": {
"baseUrl": ".",
"resolveJsonModule": true,
"paths": {
"@/*": [
"./src/*"
Expand Down
Loading