From e8d792963cb182623e5cf7ff78401cbda2c56080 Mon Sep 17 00:00:00 2001 From: trivedi-vatsal Date: Sat, 29 Aug 2026 10:09:21 +0530 Subject: [PATCH 1/4] Redesign the subpages to match the landing page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every inner page was the same narrow prose column while the homepage was a fully designed landing. This gives the subpages the landing page's visual language without changing what they claim. - Add src/components/marketing/: PageHero (breadcrumbs, atmosphere, optional product visual), Section (default/muted/inverted bands), Card/Grid, Steps, MarkList, CodePanel, CheckRunPanel, Callout, Prose, CtaLink/Actions and NextSteps. - Rewrite all 14 subpages plus the 404 with them: facts that were buried in paragraphs are now field tables (/pipeline), a comparison table (/compare/github-actions), grouped glossary cards (/concepts), topic cards (/security) and numbered flows (/product, /integrations/github-app). Each page ends with cross-links instead of a bare CTA row. - Replace the two ASCII diagrams on /product and /self-hosted with wire diagrams that wrap on a phone. - Fix an unlayered `a { color: inherit }` reset that beat every Tailwind colour and underline utility on anchors, so inline links were rendering as plain body text everywhere. - Drop the stale Header/CheckRunPanel/RunFlow components: nothing imported them and they referenced CSS variables the theme no longer defines. Copy is restructured, not extended — claims still track the docs and the code repo. `pnpm run build && pnpm run check-links` clean. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 9 +- src/components/CheckRunPanel.astro | 38 --- src/components/Header.astro | 31 -- src/components/RunFlow.astro | 36 --- src/components/marketing/Actions.astro | 8 + src/components/marketing/Callout.astro | 26 ++ src/components/marketing/Card.astro | 60 ++++ src/components/marketing/CheckRunPanel.astro | 142 +++++++++ src/components/marketing/CodePanel.astro | 25 ++ src/components/marketing/CtaLink.astro | 53 ++++ src/components/marketing/Grid.astro | 16 + src/components/marketing/MarkList.astro | 34 ++ src/components/marketing/NextSteps.astro | 42 +++ src/components/marketing/PageHero.astro | 78 +++++ src/components/marketing/Prose.astro | 8 + src/components/marketing/Section.astro | 73 +++++ src/components/marketing/Steps.astro | 46 +++ src/layouts/MarketingPage.astro | 6 +- src/pages/404.astro | 42 ++- src/pages/compare/github-actions.astro | 209 +++++++++++-- src/pages/concepts.astro | 223 +++++++++---- src/pages/integrations/github-app.astro | 148 +++++++-- src/pages/integrations/index.astro | 162 ++++++++-- src/pages/open-source.astro | 195 +++++++++--- src/pages/pipeline.astro | 218 ++++++++++--- src/pages/product/index.astro | 310 ++++++++++++++----- src/pages/security.astro | 200 ++++++++---- src/pages/self-hosted.astro | 231 +++++++++++--- src/pages/use-cases/index.astro | 141 +++++++-- src/pages/use-cases/open-source.astro | 168 ++++++++-- src/pages/use-cases/private-repos.astro | 141 +++++++-- src/pages/use-cases/self-hosted-teams.astro | 127 ++++++-- src/pages/why.astro | 231 +++++++++++--- src/styles/global.css | 184 +++++------ 34 files changed, 2890 insertions(+), 771 deletions(-) delete mode 100644 src/components/CheckRunPanel.astro delete mode 100644 src/components/Header.astro delete mode 100644 src/components/RunFlow.astro create mode 100644 src/components/marketing/Actions.astro create mode 100644 src/components/marketing/Callout.astro create mode 100644 src/components/marketing/Card.astro create mode 100644 src/components/marketing/CheckRunPanel.astro create mode 100644 src/components/marketing/CodePanel.astro create mode 100644 src/components/marketing/CtaLink.astro create mode 100644 src/components/marketing/Grid.astro create mode 100644 src/components/marketing/MarkList.astro create mode 100644 src/components/marketing/NextSteps.astro create mode 100644 src/components/marketing/PageHero.astro create mode 100644 src/components/marketing/Prose.astro create mode 100644 src/components/marketing/Section.astro create mode 100644 src/components/marketing/Steps.astro diff --git a/README.md b/README.md index 387e85b..71fe576 100644 --- a/README.md +++ b/README.md @@ -38,9 +38,14 @@ npm run dev - `src/pages/index.astro`: the marketing homepage - `src/pages/`: Product, Why, Self-hosted, Security, and the other hub routes - `src/pages/404.astro`: branded not-found -- `src/layouts/MarketingPage.astro`: shared header/footer chrome for inner pages +- `src/layouts/MarketingPage.astro`: header/footer chrome for inner pages; the + pages themselves compose full-bleed sections - `src/lib/site.ts`: nav, footer groups, CTAs -- `src/components/`: Header, CheckRunPanel, RunFlow, Rivelle blocks +- `src/components/marketing/`: the subpage system — `PageHero`, `Section`, + `Card`/`Grid`, `Steps`, `MarkList`, `CodePanel`, `CheckRunPanel`, `Callout`, + `Prose`, `CtaLink`/`Actions`, `NextSteps` +- `src/components/blocks/`, `src/components/ui/`: Rivelle blocks and primitives + used by the homepage template - `src/layouts/Layout.astro`: head, meta, OG tags, header/footer chrome - `src/styles/global.css`: product-green palette, JetBrains Mono, light/dark via `prefers-color-scheme` diff --git a/src/components/CheckRunPanel.astro b/src/components/CheckRunPanel.astro deleted file mode 100644 index 91190e8..0000000 --- a/src/components/CheckRunPanel.astro +++ /dev/null @@ -1,38 +0,0 @@ ---- -interface Props { - lines: string[]; -} - -const { lines } = Astro.props; ---- - -
-
- - Check run -
-
{
-      lines.map((line) => {
-        const isPass = line.trimStart().startsWith('✓');
-        const isSep = line.includes('──');
-        const isTitle = line.trim() === 'openpreflight';
-        let cls = 'block text-[var(--ink)]';
-        if (isPass) cls = 'block text-[var(--pass)]';
-        else if (isSep || isTitle) cls = 'block text-[var(--ink-muted)]';
-        if (isSep) {
-          return (
-            
-          );
-        }
-        return {line || '\u00a0'};
-      })
-    }
-
diff --git a/src/components/Header.astro b/src/components/Header.astro deleted file mode 100644 index 1aed4a9..0000000 --- a/src/components/Header.astro +++ /dev/null @@ -1,31 +0,0 @@ ---- -const DOCS = 'https://docs.openpreflight.xyz'; -const REPO = 'https://github.com/openpreflight/openpreflight'; ---- - -
- -
diff --git a/src/components/RunFlow.astro b/src/components/RunFlow.astro deleted file mode 100644 index 8b64f2c..0000000 --- a/src/components/RunFlow.astro +++ /dev/null @@ -1,36 +0,0 @@ ---- -interface Step { - title: string; - detail: string; -} - -interface Props { - steps: Step[]; -} - -const { steps } = Astro.props; ---- - -
    - { - steps.map((step, i) => ( -
  1. - {i < steps.length - 1 && ( -
  2. - )) - } -
diff --git a/src/components/marketing/Actions.astro b/src/components/marketing/Actions.astro new file mode 100644 index 0000000..4cb83a0 --- /dev/null +++ b/src/components/marketing/Actions.astro @@ -0,0 +1,8 @@ +--- +interface Props { + class?: string; +} +const { class: className = '' } = Astro.props; +--- + +
diff --git a/src/components/marketing/Callout.astro b/src/components/marketing/Callout.astro new file mode 100644 index 0000000..7110dda --- /dev/null +++ b/src/components/marketing/Callout.astro @@ -0,0 +1,26 @@ +--- +/** Accent-rail note. Used for the "source of truth is the docs" reminders. */ +interface Props { + title?: string; + tone?: 'accent' | 'muted'; + class?: string; +} + +const { title, tone = 'accent', class: className = '' } = Astro.props; + +const toneClass = + tone === 'accent' + ? 'border-primary/25 bg-primary/[.06]' + : 'border-foreground/10 bg-muted/40'; +--- + + diff --git a/src/components/marketing/Card.astro b/src/components/marketing/Card.astro new file mode 100644 index 0000000..9327bee --- /dev/null +++ b/src/components/marketing/Card.astro @@ -0,0 +1,60 @@ +--- +/** + * Bordered card, matching the landing page's pillar cards. Becomes a link when + * `href` is set. `tone="dark"` for use inside a Section tone="dark". + */ +interface Props { + title?: string; + href?: string; + eyebrow?: string; + index?: number; + tone?: 'default' | 'dark'; + class?: string; +} + +const { title, href, eyebrow, index, tone = 'default', class: className = '' } = Astro.props; + +const Tag = href ? 'a' : 'article'; + +const base = + 'group flex flex-col rounded-[1.5rem] border p-6 transition-[border-color,background-color] duration-300 sm:p-7'; +const toneClass = + tone === 'dark' + ? 'border-background/12 bg-background/[.055]' + : 'border-foreground/10 bg-background'; +const hoverClass = href + ? tone === 'dark' + ? 'hover:border-primary/50' + : 'hover:border-primary/45 hover:bg-primary/[.03]' + : ''; + +const eyebrowClass = tone === 'dark' ? 'text-primary' : 'text-muted-foreground'; +const bodyClass = tone === 'dark' ? 'text-background/60' : 'text-muted-foreground'; +--- + + + { + (eyebrow || index !== undefined) && ( +

+ {index !== undefined ? String(index).padStart(2, '0') : eyebrow} +

+ ) + } + { + title && ( +

+ {title} + {href && ( + + )} +

+ ) + } +
+ +
+
diff --git a/src/components/marketing/CheckRunPanel.astro b/src/components/marketing/CheckRunPanel.astro new file mode 100644 index 0000000..b0ad0e2 --- /dev/null +++ b/src/components/marketing/CheckRunPanel.astro @@ -0,0 +1,142 @@ +--- +/** + * The product's hero artifact (design.md): a Check Run as GitHub renders it, + * in the product's own mono voice. Illustrative values only — pass real ones + * where the page has them. + */ +interface Step { + name: string; + command: string; + duration: string; + width: string; + state?: 'pass' | 'fail' | 'skip'; +} + +interface Props { + label?: string; + repo: string; + meta?: string[]; + conclusion?: 'passed' | 'failed' | 'skipped'; + duration?: string; + steps: Step[]; + footer?: string; + class?: string; +} + +const { + label = 'Check Run · pull request', + repo, + meta = [], + conclusion = 'passed', + duration, + steps, + footer = 'View full logs', + class: className = '', +} = Astro.props; + +const summary = { + passed: { color: '#7cc79c', glyph: '✓', text: 'Passed' }, + failed: { color: '#e0857b', glyph: '✕', text: 'Failed' }, + skipped: { color: '#9aa39a', glyph: '–', text: 'Skipped' }, +}[conclusion]; + +const stepColor = { + pass: '#7cc79c', + fail: '#e0857b', + skip: '#9aa39a', +}; +const stepGlyph = { pass: '✓', fail: '✕', skip: '–' }; +--- + +
+
+ + {label} +
+ +
+ Example Check Run on {repo}: {steps.length} steps, {summary.text.toLowerCase()}. +
+ +
+
+ {repo} + { + meta.map((item) => ( + <> + · + {item} + + )) + } +
+
+
+ + + {summary.text} + +
+ {duration && {duration}} +
+
+ +
    + { + steps.map((step) => { + const state = step.state ?? 'pass'; + const color = stepColor[state]; + return ( +
  • + +
    +
    + {step.name} + {step.duration} +
    +
    + + {step.command} + +
    +
    +
  • + ); + }) + } +
+ + { + footer && ( +
+ + {footer} → + +
+ ) + } +
diff --git a/src/components/marketing/CodePanel.astro b/src/components/marketing/CodePanel.astro new file mode 100644 index 0000000..3d34eef --- /dev/null +++ b/src/components/marketing/CodePanel.astro @@ -0,0 +1,25 @@ +--- +/** Dark code panel with a filename bar — same chrome as the hero panel. */ +interface Props { + label?: string; + code: string; + class?: string; +} + +const { label, code, class: className = '' } = Astro.props; +--- + +
+ { + label && ( +
+
+ ) + } +
{code}
+
diff --git a/src/components/marketing/CtaLink.astro b/src/components/marketing/CtaLink.astro new file mode 100644 index 0000000..668c09e --- /dev/null +++ b/src/components/marketing/CtaLink.astro @@ -0,0 +1,53 @@ +--- +/** Static equivalent of the landing page's Button variants, as an anchor. */ +interface Props { + href: string; + variant?: 'primary' | 'outline' | 'ghost' | 'onDark'; + size?: 'default' | 'lg'; + arrow?: boolean; + class?: string; +} + +const { + href, + variant = 'outline', + size = 'default', + arrow = true, + class: className = '', +} = Astro.props; + +const base = + 'group inline-flex shrink-0 items-center justify-center gap-2 whitespace-nowrap rounded-xl text-sm font-semibold tracking-[-0.012em] transition-[color,background-color,border-color,box-shadow,transform] duration-300 ease-[cubic-bezier(.16,1,.3,1)] hover:-translate-y-0.5 focus-visible:ring-2 focus-visible:ring-ring/55 focus-visible:ring-offset-2 focus-visible:ring-offset-background motion-reduce:transform-none'; + +const variantClass = { + primary: + 'rounded-[0.9rem_0.9rem_0.9rem_0.35rem] bg-primary text-primary-foreground shadow-[0_10px_28px_-16px_var(--primary)] hover:bg-primary/90', + outline: + 'border border-foreground/14 bg-background/65 text-foreground shadow-[inset_0_1px_0_color-mix(in_oklch,var(--foreground)_5%,transparent)] hover:border-foreground/28 hover:bg-foreground/[.045]', + ghost: 'text-primary hover:bg-primary/10', + onDark: + 'border border-background/20 bg-transparent text-background hover:bg-background/10', +}[variant]; + +const sizeClass = size === 'lg' ? 'h-12 px-6 text-[0.95rem]' : 'h-10.5 px-4.5 py-2'; +--- + + + + { + arrow && ( + + ) + } + diff --git a/src/components/marketing/Grid.astro b/src/components/marketing/Grid.astro new file mode 100644 index 0000000..19bfc73 --- /dev/null +++ b/src/components/marketing/Grid.astro @@ -0,0 +1,16 @@ +--- +interface Props { + cols?: 2 | 3 | 4; + class?: string; +} + +const { cols = 3, class: className = '' } = Astro.props; + +const colsClass = { + 2: 'sm:grid-cols-2', + 3: 'sm:grid-cols-2 lg:grid-cols-3', + 4: 'sm:grid-cols-2 lg:grid-cols-4', +}[cols]; +--- + +
diff --git a/src/components/marketing/MarkList.astro b/src/components/marketing/MarkList.astro new file mode 100644 index 0000000..230ec57 --- /dev/null +++ b/src/components/marketing/MarkList.astro @@ -0,0 +1,34 @@ +--- +/** Chip list with a leading mark. `cross` is the "not in v1" pattern. */ +interface Props { + items: string[]; + mark?: 'check' | 'cross' | 'dot'; + cols?: 1 | 2 | 3; + tone?: 'default' | 'dark'; + class?: string; +} + +const { items, mark = 'check', cols = 2, tone = 'default', class: className = '' } = Astro.props; + +const colsClass = { 1: '', 2: 'sm:grid-cols-2', 3: 'sm:grid-cols-2 lg:grid-cols-3' }[cols]; +const itemClass = + tone === 'dark' + ? 'border-background/12 text-background/70' + : 'border-foreground/10 text-muted-foreground'; +const glyph = { check: '✓', cross: '✕', dot: '·' }[mark]; +const glyphClass = + mark === 'check' ? 'text-primary' : 'text-muted-foreground/70'; +--- + + diff --git a/src/components/marketing/NextSteps.astro b/src/components/marketing/NextSteps.astro new file mode 100644 index 0000000..2bbcc97 --- /dev/null +++ b/src/components/marketing/NextSteps.astro @@ -0,0 +1,42 @@ +--- +/** Closing cross-link band. Every page ends with somewhere useful to go. */ +interface Link { + label: string; + href: string; + description: string; +} + +interface Props { + title?: string; + links: Link[]; +} + +const { title = 'Keep reading', links } = Astro.props; +--- + +
+
+

+ {title} +

+ +
+
diff --git a/src/components/marketing/PageHero.astro b/src/components/marketing/PageHero.astro new file mode 100644 index 0000000..ea2a956 --- /dev/null +++ b/src/components/marketing/PageHero.astro @@ -0,0 +1,78 @@ +--- +/** + * Inner-page hero. Same atmosphere and type scale as the landing hero, one + * step down. Pass a `visual` slot for the product panel or a diagram. + */ +interface Crumb { + label: string; + href?: string; +} + +interface Props { + kicker: string; + title: string; + lead?: string; + crumbs?: Crumb[]; +} + +const { kicker, title, lead, crumbs = [] } = Astro.props; +const hasVisual = Astro.slots.has('visual'); +--- + +
+ +
+
+ { + crumbs.length > 0 && ( + + ) + } +

+ {kicker} +

+

+ {title} +

+ { + lead && ( +

+ {lead} +

+ ) + } + +
+ { + hasVisual && ( +
+ + ) + } +
+
diff --git a/src/components/marketing/Prose.astro b/src/components/marketing/Prose.astro new file mode 100644 index 0000000..c0d9c12 --- /dev/null +++ b/src/components/marketing/Prose.astro @@ -0,0 +1,8 @@ +--- +interface Props { + class?: string; +} +const { class: className = '' } = Astro.props; +--- + +
diff --git a/src/components/marketing/Section.astro b/src/components/marketing/Section.astro new file mode 100644 index 0000000..b36acfa --- /dev/null +++ b/src/components/marketing/Section.astro @@ -0,0 +1,73 @@ +--- +/** + * A full-width page section. `tone="dark"` is the inverted band the landing + * page uses for "How a run happens"; keep it to one per page. + */ +interface Props { + id?: string; + tone?: 'default' | 'muted' | 'dark'; + badge?: string; + title?: string; + description?: string; + align?: 'start' | 'center'; + class?: string; +} + +const { + id, + tone = 'default', + badge, + title, + description, + align = 'start', + class: className = '', +} = Astro.props; + +const toneClass = { + default: '', + muted: 'bg-muted/35', + dark: 'on-dark bg-foreground text-background', +}[tone]; + +const badgeClass = + tone === 'dark' + ? 'border-background/20 bg-background/10 text-background' + : 'border-primary/12 bg-primary/10 text-primary'; + +const descriptionClass = + tone === 'dark' ? 'text-background/60' : 'text-muted-foreground'; +--- + +
+
+ { + (badge || title || description) && ( +
+ {badge && ( + + {badge} + + )} + {title && ( +

+ {title} +

+ )} + {description && ( +

+ {description} +

+ )} +
+ ) + } + +
+
diff --git a/src/components/marketing/Steps.astro b/src/components/marketing/Steps.astro new file mode 100644 index 0000000..ea90584 --- /dev/null +++ b/src/components/marketing/Steps.astro @@ -0,0 +1,46 @@ +--- +/** Numbered rail. Vertical on every breakpoint; the connector is decorative. */ +interface Step { + title: string; + detail: string; +} + +interface Props { + steps: Step[]; + tone?: 'default' | 'dark'; + class?: string; +} + +const { steps, tone = 'default', class: className = '' } = Astro.props; + +const lineClass = tone === 'dark' ? 'bg-background/15' : 'bg-foreground/10'; +const dotClass = + tone === 'dark' + ? 'border-background/20 bg-foreground text-primary' + : 'border-foreground/12 bg-background text-primary'; +const detailClass = tone === 'dark' ? 'text-background/60' : 'text-muted-foreground'; +--- + +
    + { + steps.map((step, i) => ( +
  1. + {i < steps.length - 1 && ( +
  2. + )) + } +
diff --git a/src/layouts/MarketingPage.astro b/src/layouts/MarketingPage.astro index fd402a9..9dc19b8 100644 --- a/src/layouts/MarketingPage.astro +++ b/src/layouts/MarketingPage.astro @@ -23,11 +23,7 @@ const { title, description } = Astro.props; ctaHref={CTA.quickstart} showProfile={false} /> -
-
- -
-
+
-

404

-

Page not found

-

That URL is not on this site. The links below cover everything that is.

- + + + Home + Documentation + + + + diff --git a/src/pages/compare/github-actions.astro b/src/pages/compare/github-actions.astro index 5242809..6592196 100644 --- a/src/pages/compare/github-actions.astro +++ b/src/pages/compare/github-actions.astro @@ -1,46 +1,189 @@ --- import MarketingPage from '../../layouts/MarketingPage.astro'; +import PageHero from '../../components/marketing/PageHero.astro'; +import Section from '../../components/marketing/Section.astro'; +import Grid from '../../components/marketing/Grid.astro'; +import Card from '../../components/marketing/Card.astro'; +import Actions from '../../components/marketing/Actions.astro'; +import CtaLink from '../../components/marketing/CtaLink.astro'; +import Callout from '../../components/marketing/Callout.astro'; +import NextSteps from '../../components/marketing/NextSteps.astro'; import { CTA, DOCS, REPO } from '../../lib/site'; + +const rows = [ + { + label: 'What it is', + actions: 'An orchestration layer for workflows.', + op: 'A Check Runs worker for one commit at a time.', + }, + { + label: 'Where jobs run', + actions: 'Hosted runners, or a self-hosted actions/runner.', + op: 'The server you installed the binary on.', + }, + { + label: 'Configuration', + actions: 'Workflow YAML in .github/workflows.', + op: '.ci.yml, binding overrides, or package.json scripts.', + }, + { + label: 'Reusable steps', + actions: 'Marketplace actions.', + op: 'Shell commands you write. There is no registry.', + }, + { + label: 'Matrices, caches, artifacts', + actions: 'Yes.', + op: 'Not in v1.', + }, + { + label: 'Where logs live', + actions: 'On GitHub.', + op: 'On your disk, behind GET /runs/{id}.', + }, + { + label: 'What you operate', + actions: 'Nothing, or a runner fleet.', + op: 'One binary, one SQLite file, one GitHub App.', + }, +]; --- -

Compare

-

Choose the right layer

-

- GitHub Actions is orchestration: matrices, caches, artifacts, marketplace - actions, hosted or self-hosted actions/runner. openpreflight is - a Check Runs worker you host. Use them together when that matches the setup. - Do not treat this product as a replacement for Actions YAML. -

+ + + What openpreflight does + + Full docs comparison + + + + +
+
+ + { + rows.map((row, i) => ( +
0 ? 'border-t border-foreground/10' : ''}> +

+ {row.label} +

+
+
+

+ GitHub Actions +

+

+

+
+

+ openpreflight +

+

+

+
+
+ )) + } +
+
-

What openpreflight is

-
    -
  • One binary, one SQLite file, on your server
  • -
  • A GitHub App you register
  • -
  • install / test / build from .ci.yml
  • -
  • One Check Run per commit, logs on your disk
  • -
+
+ + + Release pipelines, matrices across versions, artifact publishing, + scheduled jobs, anything that needs the marketplace. + + + Install, test, and build on a private repo, reported as a Check Run, + with the logs on hardware you already operate. + + +

+ Both can run on the same commit. GitHub shows every check that reports on + it, whoever produced them. +

+
-

What it is not

-
    -
  • GitHub Actions YAML or actions/runner
  • -
  • Matrices, caches, or artifacts
  • -
  • A hosted runner fleet
  • -
  • Creating GitHub Apps for you
  • -
-

- Those sit on - Not in v1. - Neighbors (Woodpecker, Drone, Jenkins) are compared in the - docs, not as fake “we replace - Actions” copy. -

+
+
+ + GitHub Actions YAML, actions/runner, matrices, caches, + artifacts, a hosted runner fleet, or creating GitHub Apps for you. The + list stays on Not in v1. + + + Woodpecker, Drone, and Jenkins all do more than this. They are compared + in the docs, + not with fake “we replace Actions” copy. + +
+ +

+ If you need orchestration, use Actions or one of its neighbours. If you + need a private commit checked by a worker you own, this is the smaller + answer. +

+
+
- +
diff --git a/src/pages/concepts.astro b/src/pages/concepts.astro index 2909377..5194c62 100644 --- a/src/pages/concepts.astro +++ b/src/pages/concepts.astro @@ -1,72 +1,177 @@ --- import MarketingPage from '../layouts/MarketingPage.astro'; +import PageHero from '../components/marketing/PageHero.astro'; +import Section from '../components/marketing/Section.astro'; +import Actions from '../components/marketing/Actions.astro'; +import CtaLink from '../components/marketing/CtaLink.astro'; +import Callout from '../components/marketing/Callout.astro'; +import NextSteps from '../components/marketing/NextSteps.astro'; import { CTA, DOCS } from '../lib/site'; + +const groups = [ + { + heading: 'On GitHub', + terms: [ + { + id: 'check-run', + term: 'Check Run', + body: 'GitHub’s status object on a commit. openpreflight creates one per job and writes the conclusion plus a log tail. It is the product’s hero artifact.', + links: [{ label: 'ADR 005', href: `${DOCS}/adr/005-check-suite-gating/` }], + }, + { + id: 'github-app', + term: 'GitHub App', + body: 'An App you register. Webhooks, installation tokens, Check Runs. Not OAuth, and not Coolify’s GitHub connector.', + links: [ + { label: 'Setup', href: `${DOCS}/setup/github-app/` }, + { label: 'ADR 003', href: `${DOCS}/adr/003-github-app/` }, + ], + }, + ], + }, + { + heading: 'What you configure', + terms: [ + { + id: 'binding', + term: 'Binding', + body: 'A row that says: this App, this repo, these branches, these optional command overrides. No enabled binding, no job.', + links: [{ label: 'Bindings', href: `${DOCS}/setup/bindings/` }], + }, + { + id: 'pipeline', + term: 'Pipeline (.ci.yml)', + body: 'Repo file with install, test, build, and optional runtime and timeout. Commands you write, not a marketplace of named checks.', + links: [ + { label: 'Pipeline', href: '/pipeline/' }, + { label: 'Docs', href: `${DOCS}/using/pipelines/` }, + ], + }, + ], + }, + { + heading: 'What happens at run time', + terms: [ + { + id: 'job', + term: 'Job', + body: 'One queued or running attempt for an (app, repo, sha). Logs are files under DATA_DIR.', + links: [{ label: 'Logs', href: `${DOCS}/using/logs/` }], + }, + { + id: 'executor', + term: 'Executor', + body: 'Process (default), or docker run when runtime: is set or the job is a fork PR.', + links: [{ label: 'ADR 004', href: `${DOCS}/adr/004-docker-executor/` }], + }, + { + id: 'shareable-log', + term: 'Shareable log', + body: 'Per-binding opt-in so GET /runs/{id} works without a session. Treat the URL as a secret.', + links: [], + }, + ], + }, +]; --- -

Concepts

-

Words the binary uses

-

- A glossary that matches the docs. Each entry is the thing in v1, not a - metaphor for a future platform. -

- -

Check Run

-

- GitHub’s status object on a commit. openpreflight creates one per job and - writes conclusion plus a log tail. It is the product’s hero artifact. - ADR 005. -

- -

Binding

-

- A row that says this App, this repo, these branches, these optional command - overrides. No enabled binding, no job. - Bindings. -

- -

Pipeline (.ci.yml)

-

- Repo file with install, test, build, - optional runtime and timeout. Commands you write, - not a marketplace of named checks. - Pipeline, - docs. -

- -

GitHub App

-

- An App you register. Webhooks, installation tokens, Check Runs. Not OAuth, - not Coolify’s GitHub connector. - Setup, - ADR 003. -

- -

Job

-

- One queued or running attempt for an (app, repo, sha). Logs - are files under DATA_DIR. - Logs. -

+ + + + Architecture + + Product + + -

Executor

-

- Process (default) or docker run when runtime: is - set or the job is a fork PR. - ADR 004. -

+
+ +

+ A binding says which repo a GitHub App checks; + a matching push creates a job; the executor runs + the pipeline; the result is a Check Run whose + details URL is a log page. +

+
-

Shareable log

-

- Per-binding opt-in so GET /runs/{id} works without a - session. Treat the URL as a secret. -

+
+ { + groups.map((group) => ( +
+

+ {group.heading} +

+
+ {group.terms.map((entry) => ( +
+
+ +
+
+ + {entry.links.length > 0 && ( + + {entry.links.map((link) => ( + + {link.label} + + ))} + + )} +
+
+ ))} +
+
+ )) + } +
+
- +
diff --git a/src/pages/integrations/github-app.astro b/src/pages/integrations/github-app.astro index ab81c89..377bcbd 100644 --- a/src/pages/integrations/github-app.astro +++ b/src/pages/integrations/github-app.astro @@ -1,32 +1,136 @@ --- import MarketingPage from '../../layouts/MarketingPage.astro'; +import PageHero from '../../components/marketing/PageHero.astro'; +import Section from '../../components/marketing/Section.astro'; +import Steps from '../../components/marketing/Steps.astro'; +import Grid from '../../components/marketing/Grid.astro'; +import Card from '../../components/marketing/Card.astro'; +import CheckRunPanel from '../../components/marketing/CheckRunPanel.astro'; +import Actions from '../../components/marketing/Actions.astro'; +import CtaLink from '../../components/marketing/CtaLink.astro'; +import Callout from '../../components/marketing/Callout.astro'; +import NextSteps from '../../components/marketing/NextSteps.astro'; import { CTA, DOCS } from '../../lib/site'; + +const steps = [ + { + title: 'Register the App', + detail: + 'In your org or user settings, with the permissions and events listed in the docs. You own it — openpreflight never creates one for you.', + }, + { + title: 'Point the webhook at your instance', + detail: 'The URL is https://your-host/webhook/{slug}.', + }, + { + title: 'Paste the PEM and webhook secret into the UI', + detail: + 'They are stored as AES-256-GCM encrypted columns, and GET responses give back a redacted marker.', + }, + { + title: 'Install it on the repos you care about', + detail: 'Org-wide or a hand-picked set. Installation tokens are minted per job.', + }, + { + title: 'Enable a binding and push', + detail: + 'The binding names the repo and branches. The next commit gets the first Check Run.', + }, +]; + +const panelSteps = [ + { name: 'install', command: 'npm ci', duration: '6s', width: '22%' }, + { name: 'test', command: 'npm test', duration: '14s', width: '48%' }, +]; --- -

Integrations / GitHub App

-

Install, bind, first Check Run

-

- openpreflight does not create the App for you. You register it, paste the - PEM and webhook secret into the UI, enable a binding, and push. -

-
    -
  1. Register the App with the permissions and events in the docs.
  2. -
  3. Point the webhook at https://your-host/webhook/{slug}.
  4. -
  5. Install the App on the org or repos you care about.
  6. -
  7. Enable a binding. Push a commit (or open a PR).
  8. -
-

- The full checklist, permission table, and event list: - Register a GitHub App. - Bindings: - enable repos. -

- + + + Setup docs + Bindings + + + + +
+ + +

+ The exact permission table and event list are in{' '} + Register a GitHub App. They change + with the code, so this page does not copy them. +

+
+
+ +
+ + + Check suite webhooks arrive at your slug URL and are verified by HMAC + before anything is enqueued. + + + A short-lived installation token clones the exact SHA. It never lands in + the remote URL, and the remote is stripped before steps run. + + + The same App reports the conclusion and a log tail back onto the commit. + + +
+ +
diff --git a/src/pages/integrations/index.astro b/src/pages/integrations/index.astro index b8964c1..5eea45d 100644 --- a/src/pages/integrations/index.astro +++ b/src/pages/integrations/index.astro @@ -1,40 +1,148 @@ --- import MarketingPage from '../../layouts/MarketingPage.astro'; +import PageHero from '../../components/marketing/PageHero.astro'; +import Section from '../../components/marketing/Section.astro'; +import Actions from '../../components/marketing/Actions.astro'; +import CtaLink from '../../components/marketing/CtaLink.astro'; +import MarkList from '../../components/marketing/MarkList.astro'; +import Callout from '../../components/marketing/Callout.astro'; +import NextSteps from '../../components/marketing/NextSteps.astro'; import { CTA, DOCS } from '../../lib/site'; + +const surfaces = [ + { + status: 'Required', + title: 'GitHub App', + body: 'Webhooks in, installation tokens out, Check Runs on the commit. You register it; the worker never creates one for you.', + href: '/integrations/github-app/', + cta: 'How to set it up', + primary: true, + }, + { + status: 'Default', + title: 'Process executor', + body: 'Used whenever runtime is empty. Steps run in the worker process, on the host — no extra daemon.', + href: null, + cta: null, + primary: false, + }, + { + status: 'Optional', + title: 'Docker', + body: 'A reachable engine turns runtime: into docker run. Fork pull requests always use it.', + href: `${DOCS}/adr/004-docker-executor/`, + cta: 'ADR 004', + primary: false, + }, + { + status: 'Optional', + title: 'Coolify', + body: 'Server inventory, a repo picker, and an install-worker API. It is not a job runner and is not required.', + href: `${DOCS}/setup/coolify/`, + cta: 'Coolify docs', + primary: false, + }, +]; + +const notAdapters = ['GitLab CI', 'Jenkins', 'CircleCI', 'Buildkite', 'Azure DevOps']; --- -

Integrations

-

Surfaces that exist in v1

-

- This is not a grid of CI vendors. GitLab CI, Jenkins, CircleCI, Buildkite, - and Azure DevOps are not adapters here. -

+ + + Start with the App + Quickstart + + -
- -

GitHub App

-

Required. Webhooks, installation tokens, Check Runs.

-
- -

Coolify

-

Optional API: inventory, repo picker, install-worker. Not a job runner.

-
- -

Docker

-

runtime: and fork jobs via docker run.

-
-
-

Process executor

-

Default when runtime is empty. Steps run in the worker.

+
+
+ { + surfaces.map((surface) => { + const Tag = surface.href ? 'a' : 'div'; + return ( + + + {surface.status} + +

{surface.title}

+

+ {surface.cta && ( + + {surface.cta} + + + )} + + ); + }) + }

-
+ + +
+ + +

+ “Portable” here means the same .ci.yml and the same worker + on your own host — not one config that targets every CI vendor. See{' '} + the product page for what that does and does not buy you. +

+
+
- + diff --git a/src/pages/open-source.astro b/src/pages/open-source.astro index 490c779..e35ac35 100644 --- a/src/pages/open-source.astro +++ b/src/pages/open-source.astro @@ -1,54 +1,169 @@ --- import MarketingPage from '../layouts/MarketingPage.astro'; +import PageHero from '../components/marketing/PageHero.astro'; +import Section from '../components/marketing/Section.astro'; +import Grid from '../components/marketing/Grid.astro'; +import Card from '../components/marketing/Card.astro'; +import Actions from '../components/marketing/Actions.astro'; +import CtaLink from '../components/marketing/CtaLink.astro'; +import Callout from '../components/marketing/Callout.astro'; +import NextSteps from '../components/marketing/NextSteps.astro'; import { CTA, DOCS, DOCS_REPO, REPO, WEBSITE_REPO, RELEASE } from '../lib/site'; + +const repos = [ + { + name: 'openpreflight/openpreflight', + href: REPO, + detail: 'The Go binary — API, UI, webhook receiver, runner.', + licence: 'Apache-2.0', + }, + { + name: 'openpreflight/docs', + href: DOCS_REPO, + detail: 'Reference documentation at docs.openpreflight.xyz.', + licence: 'MIT', + }, + { + name: 'openpreflight/website', + href: WEBSITE_REPO, + detail: 'This site, plus the shared brand images.', + licence: 'MIT', + }, + { + name: 'openpreflight/.github', + href: 'https://github.com/openpreflight/.github', + detail: 'The organisation profile.', + licence: '—', + }, +]; --- -

Open source

-

Inspectable worker, public repos

-

- The worker holds GitHub App PEMs. Open source is how you can read what it - does with them. License is Apache-2.0 on the binary; the two sites are MIT. -

- -

Repositories

- + + + View source + Apache-2.0 + + -

Contribute

-

- Issues and pull requests on the code repo. Development loop: - development. - Website and docs have their own CONTRIBUTING files. There is no foundation, - board, or RFC process to join. -

+
+ +
-

Releases

-

- v1.0.0 is tagged (29 August 2026). The changelog lives in - the code repo. The GitHub Release has linux amd64 and - arm64 binaries. Pin the published image with{" "} - OPENPREFLIGHT_VERSION=1.0.0. -

+
+ + + Bug fixes land fastest when they arrive with a test that fails without + the patch. go test ./... runs offline — GitHub and Coolify + are faked. + + + Small things the scope already covers but the code does not do yet. Not + features from the Not-in-v1 list. + + + Corrections where the documentation drifted from behaviour. The website + and docs repos have their own CONTRIBUTING files. + + +

+ There is no foundation, board, or RFC process to join. Issues and pull + requests on the code repository are the whole process. +

+ + CONTRIBUTING.md + Development loop + Open an issue + +
-

Security reports

-

- SECURITY.md - — not the public issue tracker for vulnerabilities. -

+
+
+ + v1.0.0 is tagged (29 August 2026). The changelog lives + in the code repo. The GitHub Release has + linux amd64 and arm64 binaries. Pin the published image with{' '} + OPENPREFLIGHT_VERSION=1.0.0. + + + Vulnerabilities go through SECURITY.md, + never the public issue tracker. + +
+ +

+ A CI worker with your App’s private key is a high-trust component. The{' '} + security page lists what it is allowed to do; the + source is how you check that the list is true. +

+
+
- +
diff --git a/src/pages/pipeline.astro b/src/pages/pipeline.astro index cb93c5a..2064f47 100644 --- a/src/pages/pipeline.astro +++ b/src/pages/pipeline.astro @@ -1,58 +1,192 @@ --- import MarketingPage from '../layouts/MarketingPage.astro'; +import PageHero from '../components/marketing/PageHero.astro'; +import Section from '../components/marketing/Section.astro'; +import Grid from '../components/marketing/Grid.astro'; +import Card from '../components/marketing/Card.astro'; +import CodePanel from '../components/marketing/CodePanel.astro'; +import Actions from '../components/marketing/Actions.astro'; +import CtaLink from '../components/marketing/CtaLink.astro'; +import Callout from '../components/marketing/Callout.astro'; +import NextSteps from '../components/marketing/NextSteps.astro'; import { CTA, DOCS, REPO } from '../lib/site'; + +const fields = [ + { + name: 'runtime', + required: 'optional', + detail: + 'Container image for the steps. Omit it and they run in the worker process.', + }, + { + name: 'install', + required: 'optional', + detail: 'Shell command. Usually the lockfile install for your language.', + }, + { name: 'test', required: 'optional', detail: 'Shell command. Its exit code gates the commit.' }, + { name: 'build', required: 'optional', detail: 'Shell command, run after test.' }, + { + name: 'timeout', + required: 'optional', + detail: 'How long the run may take before it is cut off.', + }, +]; + +const resolution = [ + { + order: '01', + title: 'The repo’s pipeline file', + detail: '.ci.yml at the root of the commit being checked.', + }, + { + order: '02', + title: 'The binding’s command overrides', + detail: 'Set in the UI, for repos you do not want to add a file to.', + }, + { + order: '03', + title: 'Node defaults from package.json', + detail: + 'Lockfile install, then test and build only if those scripts exist.', + }, + { + order: '04', + title: 'Nothing to run', + detail: 'The check reports skipped — not failed.', + }, +]; --- -

Pipeline

-

What runs before the Check Run completes

-

- There is no check registry. Steps are shell commands in - .ci.yml — illustrations like go test or - npm test are commands you author, not first-class products. -

- -

The file

-
{`runtime: node:24
+  
+    
+      Pipeline docs
+      
+        Example file
+      
+    
+    
-  

- Sample in the code repo: - examples/.ci.yml. - Default filename is .ci.yml, not openpreflight.yaml. -

+timeout: 15m`} + /> +
+ +
+
+ { + fields.map((field) => ( +
+
+ {field.name} + + {field.required} + +
+
+ +
+
+ )) + } +
+
-

Resolution order

-

Highest first:

-
    -
  1. the repo’s pipeline file
  2. -
  3. the binding’s command overrides
  4. -
  5. - Node defaults from package.json (lockfile install, then - test / build only if those scripts exist) -
  6. -
  7. nothing to run → the check is skipped, not failed
  8. -
-

A failing step stops the run; later steps are reported skipped.

+
+
    + { + resolution.map((step) => ( +
  1. + {step.order} +

    +

    +

  2. + )) + } +
+

+ A failing step stops the run; later steps are reported skipped. Nothing to + run at all is a skipped check, which is a different signal from a failure. +

+
-

Runtime

-

- Omit runtime to run in-process. A non-empty image uses - docker run --rm. If the engine is unreachable, the job fails - instead of falling back. Fork jobs always use Docker. -

-

- The contract, not this page, is source of truth: - pipelines. -

+
+ + + Omit runtime and steps run as local processes on the host + that runs the binary. Nothing else to install. + + + A non-empty image uses docker run --rm. If the engine is + unreachable the job fails instead of falling back. Fork jobs always use + Docker. + + + +

+ This page is positioning. The contract — full field list, precedence + rules, and what each executor guarantees — is{' '} + the pipeline reference. +

+
+
- + diff --git a/src/pages/product/index.astro b/src/pages/product/index.astro index dcecda0..19e097d 100644 --- a/src/pages/product/index.astro +++ b/src/pages/product/index.astro @@ -1,89 +1,253 @@ --- import MarketingPage from '../../layouts/MarketingPage.astro'; +import PageHero from '../../components/marketing/PageHero.astro'; +import Section from '../../components/marketing/Section.astro'; +import Grid from '../../components/marketing/Grid.astro'; +import Card from '../../components/marketing/Card.astro'; +import Steps from '../../components/marketing/Steps.astro'; +import MarkList from '../../components/marketing/MarkList.astro'; +import CheckRunPanel from '../../components/marketing/CheckRunPanel.astro'; +import CodePanel from '../../components/marketing/CodePanel.astro'; +import Actions from '../../components/marketing/Actions.astro'; +import CtaLink from '../../components/marketing/CtaLink.astro'; +import Prose from '../../components/marketing/Prose.astro'; +import Callout from '../../components/marketing/Callout.astro'; +import NextSteps from '../../components/marketing/NextSteps.astro'; import { CTA, DOCS, REPO, RELEASE } from '../../lib/site'; + +const panelSteps = [ + { name: 'install', command: 'npm ci', duration: '8s', width: '19%' }, + { name: 'test', command: 'npm test', duration: '21s', width: '50%' }, + { name: 'build', command: 'npm run build', duration: '13s', width: '31%' }, +]; + +const fit = [ + 'You want CI on private GitHub repositories', + 'You do not want GitHub Actions runners', + 'You do not want to learn a pipeline DSL', + 'You already have a machine to run it on', +]; + +const notInV1 = [ + 'GitHub Actions YAML', + 'actions/runner', + 'Matrices, caches, artifacts', + 'Creating GitHub Apps for you', +]; + +const runSteps = [ + { + title: 'GitHub POSTs the webhook', + detail: + 'The check suite event arrives at /webhook/{slug}. The API validates it, confirms the binding is enabled, and enqueues.', + }, + { + title: 'The runner takes the job', + detail: + 'One live run per commit. The worker checks out the immutable SHA into a workspace, then reads the pipeline.', + }, + { + title: 'install / test / build', + detail: + 'Steps run through the executor — the worker process by default, or docker run when runtime: is set.', + }, + { + title: 'One Check Run comes back', + detail: + 'Conclusion plus a log tail land on the commit. The details URL points at your instance, where the full log lives.', + }, +]; + +const inbound = [ + { label: 'GitHub', detail: 'POST /webhook/{slug}' }, + { label: 'Browser / CLI', detail: 'session or Bearer' }, +]; + +const core = [ + { label: 'api', detail: 'validate · enqueue · serve UI' }, + { label: 'queue.Runner', detail: 'one live run per commit' }, + { label: 'SQLite', detail: 'apps · bindings · jobs · secrets' }, +]; + +const outbound = [ + { label: 'GitHub App', detail: 'creates the Check Run' }, + { label: 'workspace', detail: 'checkout of the exact SHA' }, + { label: 'pipeline', detail: 'reads .ci.yml' }, + { label: 'executor', detail: 'process or docker run' }, +]; --- -

Product

-

Self-hosted Check Runs for private repos

-

- One Go binary and one SQLite file. You register a GitHub App, bind repos in - the UI, and every commit gets a Check Run with logs that live on your server. -

-

- v1.0.0 is the tagged v1 release.{" "} - GitHub Release ·{" "} - Changelog. -

- -

Fit

-

- You want CI on private GitHub repositories without GitHub Actions runners - and without a pipeline DSL. You already have a machine. The worker is the - product, not a mode. -

-

- Full platforms, hosted control planes, and Kubernetes-oriented runners - already fill a different slot. This one does not replace them. See - why it is this shape and - the FAQ. + + + Quickstart + See a live run + + + +

+ v1.0.0 is the tagged v1 release.{' '} + GitHub Release + {' · '} + Changelog.

-

What it is not

-

v1 does not include GitHub Actions YAML, actions/runner, matrices, caches, artifacts, or creating GitHub Apps for you. Those stay on Not in v1.

+
+
+
+

Use it when

+ +

+ Three concrete versions of that are written up as{' '} + use cases. +

+
+
+

Not in v1

+ +

+ The full list stays on Not in v1. + If one of those is a requirement, the docs comparison names a better tool. +

+
+
+
-

How a run happens

-

The same loop as the docs, in marketing nouns:

-
{`GitHub ──POST /webhook/{slug}──► api ──enqueue──► queue.Runner
-                                      │                │
-Browser / CLI ──session/Bearer──► api │                ├── GitHub App (Check Run)
-                                      │                ├── workspace (exact SHA)
-                                      └── SQLite       ├── pipeline (.ci.yml)
-                                                       └── executor (process or docker run)`}
-

- Webhook → queue → checkout of the immutable SHA → install/test/build → one - Check Run. One live run per commit. Details: - architecture - and - ADR 005. -

+
+
+ { + [ + { heading: 'In', nodes: inbound }, + { heading: 'One process', nodes: core }, + { heading: 'Out', nodes: outbound }, + ].map((group) => ( +
+

{group.heading}

+
    + {group.nodes.map((node) => ( +
  • +

    {node.label}

    +

    {node.detail}

    +
  • + ))} +
+
+ )) + } +
+ + + Architecture + ADR 005 + +
-

Portable in the real sense

-

- The same .ci.yml and the same worker run on your host. - That is not “any CI vendor.” There are no GitLab, Jenkins, or CircleCI - adapters. -

+
+
+ +

+ Bindings and Apps are rows in SQLite, edited in the web UI — not a + block of environment variables per installation. An optional + .ci.yml in the repo supplies the commands, with binding + overrides and package.json scripts filling the gaps. +

+

+ Resolution order and the full field list live in{' '} + the pipeline docs; the short + version is on /pipeline. +

+

+ Portable in the real sense: the same + .ci.yml and the same worker run on your host. + That is not “any CI vendor” — there are no GitLab, Jenkins, or + CircleCI adapters. +

+
+ +
+
-

Configuration

-

- Bindings and Apps are rows in SQLite, edited in the web UI. Optional - .ci.yml in the repo supplies commands; binding overrides and - package.json scripts fill gaps. Resolution order lives in - pipelines. Marketing summary: - /pipeline. -

- -

Results

-

- GitHub shows the Check Run. The details URL is GET /runs/{id} - on your instance — session by default, or a shareable log if that binding - opted in. -

- -

Security

-

- Claims match the docs only: - security model. - Marketing page: /security. -

+
+ + + GitHub shows conclusion and a log tail on the commit, the same way it + shows any other check. + + + GET /runs/{id} on your instance — session by default, + or a shareable log if that binding opted in. + + + + Claims here match the published{' '} + security model and + nothing more. The page-length version is /security. + +
- +
diff --git a/src/pages/security.astro b/src/pages/security.astro index 1071dbf..7295636 100644 --- a/src/pages/security.astro +++ b/src/pages/security.astro @@ -1,79 +1,149 @@ --- import MarketingPage from '../layouts/MarketingPage.astro'; +import PageHero from '../components/marketing/PageHero.astro'; +import Section from '../components/marketing/Section.astro'; +import Actions from '../components/marketing/Actions.astro'; +import CtaLink from '../components/marketing/CtaLink.astro'; +import Callout from '../components/marketing/Callout.astro'; +import NextSteps from '../components/marketing/NextSteps.astro'; import { CTA, DOCS, REPO } from '../lib/site'; + +const topics = [ + { + title: 'Execution', + body: 'Steps run as a local process, or as docker run --rm when runtime: is set. Job containers drop capabilities, set no-new-privileges, and do not get the engine socket. Image names are allow-listed.', + }, + { + title: 'Network', + body: 'GitHub POSTs webhooks to your public HTTPS URL. The worker clones with an installation token via GIT_CONFIG_* Basic auth — never in the remote URL — then strips the remote before pipeline steps run.', + }, + { + title: 'Secrets at rest', + body: 'App PEM, webhook secret, and Coolify token columns are AES-256-GCM. GET responses return a redacted marker. The key is CI_SECRET_KEY; rotation uses CI_SECRET_KEY_OLD on boot.', + }, + { + title: 'Job environment', + body: 'Job env is built from scratch: no CI_SECRET_KEY, no PEMs, no webhook secrets, no Coolify tokens, no installation token.', + }, + { + title: 'Fork pull requests', + body: 'Fork PRs are skipped by default. Opting in requires a reachable Docker engine and a default_runtime. Fork jobs always run in Docker.', + }, + { + title: 'Sessions and CSRF', + body: 'Session cookies are HttpOnly, and Secure behind HTTPS. Browser writes need a CSRF token; Bearer callers skip CSRF.', + links: [{ label: 'ADR 002', href: `${DOCS}/adr/002-authentication/` }], + }, + { + title: 'Shareable logs', + body: 'A binding can opt into unauthenticated GET /runs/{id}. Job ids are random UUIDs — treat the link as a secret.', + }, +]; --- -

Security

-

What the worker is allowed to do

-

- This page restates the published security model. It does not add claims. - Source: - security model - and - SECURITY.md. -

- -

Execution

-

- Steps run as a local process, or as docker run --rm when - runtime: is set. Job containers drop capabilities, set - no-new-privileges, and do not get the engine socket. Image - names are allow-listed. -

- -

Network

-

- GitHub POSTs webhooks to your public HTTPS URL. The worker clones with an - installation token via GIT_CONFIG_* Basic auth — never in the - remote URL — then strips the remote before pipeline steps run. -

- -

Secrets at rest

-

- App PEM, webhook secret, and Coolify token columns are AES-256-GCM. - GET responses return a redacted marker. The key is CI_SECRET_KEY. - Rotation uses CI_SECRET_KEY_OLD on boot. -

- -

Job environment

-

- Job env is built from scratch: no CI_SECRET_KEY, no PEMs, no - webhook secrets, no Coolify tokens, no installation token. -

- -

Fork pull requests

-

- Fork PRs are skipped by default. Opt-in requires a reachable Docker engine - and default_runtime. Fork jobs always run in Docker. -

+ + + + Security model + + + SECURITY.md + + + -

Sessions and CSRF

-

- Session cookies are HttpOnly, Secure behind HTTPS. Browser writes need a - CSRF token. Bearer callers skip CSRF. See - ADR 002. -

+
+
+ { + topics.map((topic) => ( +
+

{topic.title}

+

+ {topic.links && ( +

+ {topic.links.map((link) => ( + + {link.label} + + ))} +

+ )} +
+ )) + } +
+
-

Shareable logs

-

- A binding can opt into unauthenticated GET /runs/{id}. - Job ids are random UUIDs; treat the link as a secret. -

+
+
+

+ Report vulnerabilities the way SECURITY.md describes — private reporting + on the GitHub repository, or email. Not the public issue tracker. +

+
+ + SECURITY.md + + security@openpreflight.xyz + + +
-

Reporting

-

- Report vulnerabilities as described in - SECURITY.md - (private reporting on the GitHub repo, and - security@openpreflight.xyz). -

+
+ +

+ The worker holds GitHub App private keys, which is exactly why the code + is public. Read what it does with them in{' '} + the repository, and see{' '} + open source for how the repos are laid out. +

+
+
- +
diff --git a/src/pages/self-hosted.astro b/src/pages/self-hosted.astro index 0cd922f..c6413c4 100644 --- a/src/pages/self-hosted.astro +++ b/src/pages/self-hosted.astro @@ -1,57 +1,194 @@ --- import MarketingPage from '../layouts/MarketingPage.astro'; +import PageHero from '../components/marketing/PageHero.astro'; +import Section from '../components/marketing/Section.astro'; +import Grid from '../components/marketing/Grid.astro'; +import Card from '../components/marketing/Card.astro'; +import CodePanel from '../components/marketing/CodePanel.astro'; +import Actions from '../components/marketing/Actions.astro'; +import CtaLink from '../components/marketing/CtaLink.astro'; +import Callout from '../components/marketing/Callout.astro'; +import NextSteps from '../components/marketing/NextSteps.astro'; import { CTA, DOCS, REPO } from '../lib/site'; + +const yours = [ + { label: 'compose / binary', detail: 'UI · API · webhook · runner' }, + { label: 'SQLite in DATA_DIR', detail: 'apps · bindings · jobs' }, + { label: 'logs on disk', detail: 'served by /runs/{id}' }, +]; + +const theirs = [ + { label: 'HTTPS webhooks', detail: 'GitHub → your public URL' }, + { label: 'Check Runs', detail: 'your worker → the commit' }, + { label: 'details_url', detail: 'points back at your host' }, +]; + +const operate = [ + { + title: 'A public HTTPS URL', + body: 'Something GitHub can reach, with a reverse proxy in front of port 8080.', + }, + { + title: 'CI_SECRET_KEY', + body: 'The only required env var. It decrypts your secret columns — keep it forever.', + }, + { + title: 'A volume that persists', + body: 'SQLite and job logs live under DATA_DIR. Lose it and you lose history.', + }, + { + title: 'A GitHub App you register', + body: 'The worker does not create Apps for you. You own it, in your org.', + }, +]; --- -

Self-hosted

-

You run the worker. GitHub shows the Check Run.

-

- There is no hosted openpreflight control plane. Compose or the binary on - your server is the whole deployment. -

- -

The diagram

-
{`Your infra                         GitHub
-─────────                         ──────
-compose / binary
-  UI + API + webhook + runner  ◄── HTTPS webhooks
-  SQLite in DATA_DIR           ──► Check Runs
-  logs on disk                 ──► details_url (your host)`}
- -

What you operate

-
    -
  • A public HTTPS URL GitHub can reach (reverse proxy in front of port 8080).
  • -
  • CI_SECRET_KEY — the only required env var. Keep it forever.
  • -
  • SQLite and logs under DATA_DIR. That volume must persist.
  • -
  • A GitHub App you register. The worker does not create Apps for you.
  • -
-

- Walkthrough: - quickstart - and - deployment. -

- -

Docker and Coolify are optional

-

- Process executor is the default. runtime: and fork PRs need a - Docker engine (CI_DOCKER_HOST or a mounted socket). Coolify is - optional inventory, a repo picker, and an install-worker API — not required - to run CI, and not a job runner. -

-

- There is no first-class Kubernetes operator and no air-gap product. If you - put the binary on an isolated network, that is ordinary self-hosting, not a - documented mode. -

- - + + + Quickstart + + Deployment + + + + +
+
+ { + [ + { heading: 'Your infrastructure', nodes: yours, accent: true }, + { heading: 'GitHub', nodes: theirs, accent: false }, + ].map((column) => ( +
+

+ {column.heading} +

+
    + {column.nodes.map((node) => ( +
  • +

    {node.label}

    +

    {node.detail}

    +
  • + ))} +
+
+ )) + } +
+
+ +
+ + { + operate.map((item, index) => ( + +

+ +

+

+ +

+
+ )) + } +
+
+ +
+
+ +
+

+ Nothing to clone — the file pulls the published image. Then open the + UI, run the first-boot wizard, register your GitHub App, and enable + the repos you want checks on. +

+ + Full quickstart + + Day-two operations + + +
+
+
+ +
+ + + Steps run in the worker. This is what you get with no extra + infrastructure at all. + + + Needed for runtime: and for fork PRs, via + CI_DOCKER_HOST or a mounted socket. + + + Inventory, a repo picker, and an install-worker API. It never executes + jobs. + + + +

+ There is no first-class Kubernetes operator and no air-gap product. If + you put the binary on an isolated network, that is ordinary + self-hosting — not a documented mode with support behind it. +

+
+
+ +
diff --git a/src/pages/use-cases/index.astro b/src/pages/use-cases/index.astro index d4ba1fe..e37b2bf 100644 --- a/src/pages/use-cases/index.astro +++ b/src/pages/use-cases/index.astro @@ -1,36 +1,129 @@ --- import MarketingPage from '../../layouts/MarketingPage.astro'; +import PageHero from '../../components/marketing/PageHero.astro'; +import Section from '../../components/marketing/Section.astro'; +import Actions from '../../components/marketing/Actions.astro'; +import CtaLink from '../../components/marketing/CtaLink.astro'; +import Callout from '../../components/marketing/Callout.astro'; +import NextSteps from '../../components/marketing/NextSteps.astro'; import { CTA } from '../../lib/site'; + +const cases = [ + { + href: '/use-cases/private-repos/', + eyebrow: 'The common one', + title: 'Private GitHub repos', + body: 'You want a status on the commit without buying hosted Actions minutes or writing workflow YAML for install / test / build.', + points: ['A GitHub App you register', 'Clone via installation token', 'One Check Run per commit'], + }, + { + href: '/use-cases/self-hosted-teams/', + eyebrow: 'Already have the box', + title: 'Self-hosted teams', + body: 'A VPS or home-lab machine is already paid for. openpreflight is a Compose stack on it: UI, webhook, runner, SQLite, logs.', + points: ['Runner on your hardware', 'Logs never leave the host', 'Shareable logs are opt-in'], + }, + { + href: '/use-cases/open-source/', + eyebrow: 'Proof you can click', + title: 'Open-source contributors', + body: 'The public demo repo carries pull requests that produce real Check Runs from a self-hosted instance — pass, fail, timeout, skip.', + points: ['Six demo pull requests', 'Public run log pages', 'Same pages you get behind auth'], + }, +]; --- -

Use cases

-

Who this is for

-

- Three honest stories. Not a policy engine, and not one YAML across GitLab - and Jenkins. -

+ + + Quickstart + How it works + + - +
+ + +

+ Hosted CI for arbitrary public repos, cross-vendor pipeline portability, + and policy engines are all out of scope. See{' '} + the comparison if orchestration is + what you are shopping for. +

+
+
- +
diff --git a/src/pages/use-cases/open-source.astro b/src/pages/use-cases/open-source.astro index 5333d22..dd90f81 100644 --- a/src/pages/use-cases/open-source.astro +++ b/src/pages/use-cases/open-source.astro @@ -1,33 +1,153 @@ --- import MarketingPage from '../../layouts/MarketingPage.astro'; -import { CTA, DEMO_REPO } from '../../lib/site'; +import PageHero from '../../components/marketing/PageHero.astro'; +import Section from '../../components/marketing/Section.astro'; +import Grid from '../../components/marketing/Grid.astro'; +import Card from '../../components/marketing/Card.astro'; +import Actions from '../../components/marketing/Actions.astro'; +import CtaLink from '../../components/marketing/CtaLink.astro'; +import Callout from '../../components/marketing/Callout.astro'; +import NextSteps from '../../components/marketing/NextSteps.astro'; +import CheckRunPanel from '../../components/marketing/CheckRunPanel.astro'; +import { CTA, DEMO_REPO, REPO } from '../../lib/site'; +import demoRuns from '../../data/demo-runs.json'; + +const outcomes = { + success: { label: 'passed', tone: 'text-primary' }, + failure: { label: 'failed', tone: 'text-destructive' }, + timed_out: { label: 'timed out', tone: 'text-destructive' }, + skipped: { label: 'skipped', tone: 'text-muted-foreground' }, +}; + +const panelSteps = [ + { name: 'install', command: 'npm ci', duration: '7s', width: '24%' }, + { name: 'test', command: 'npm test', duration: '11s', width: '38%', state: 'fail' as const }, + { name: 'build', command: 'npm run build', duration: '—', width: '0%', state: 'skip' as const }, +]; --- -

Use cases / Open source

-

Public PRs, real Check Runs

-

- openpreflight/demo - is a small Node utility with six pull requests. Each is meant to produce a - Check Run on a self-hosted instance — passing, failing test, failing build, - timeout, skipped, container runtime. -

-

- The log pages are the same /runs/{id} pages you get - behind auth. Shareable logs are on for that binding only. Until the demo - App is bound, the site links the pull requests and leaves run URLs empty - rather than inventing them. -

-

- This is contributor-facing proof, not a hosted CI service for arbitrary - public repos. You still run your own worker. -

- + + + See the live runs + Demo repo + + + + +
+
    + { + demoRuns.runs.map((entry) => { + const outcome = outcomes[entry.conclusion] ?? outcomes.skipped; + return ( +
  • +

    + {outcome.label} +

    +

    {entry.title}

    +

    {entry.outcome}

    +
    + {entry.runUrl && ( + + run log + + )} + + pull request + +
    +
  • + ); + }) + } +
+
+ +
+ + + These checks come from a self-hosted openpreflight instance reporting + through its own GitHub App. + + + /runs/{'{id}'} is the page you get behind auth. Shareable + logs are on for that one binding. + + + This is proof you can inspect, not a hosted CI service for arbitrary + public repos. + + +
+ +
+ +

+ The demo instance is a live box and can be down. A run URL may also 404 + once retention prunes the job — the pull request always stays. Until the + demo App is bound, run URLs are left empty rather than invented. +

+
+
+ +
diff --git a/src/pages/use-cases/private-repos.astro b/src/pages/use-cases/private-repos.astro index 3553a62..0ddc6f7 100644 --- a/src/pages/use-cases/private-repos.astro +++ b/src/pages/use-cases/private-repos.astro @@ -1,32 +1,129 @@ --- import MarketingPage from '../../layouts/MarketingPage.astro'; +import PageHero from '../../components/marketing/PageHero.astro'; +import Section from '../../components/marketing/Section.astro'; +import Grid from '../../components/marketing/Grid.astro'; +import Card from '../../components/marketing/Card.astro'; +import Steps from '../../components/marketing/Steps.astro'; +import CheckRunPanel from '../../components/marketing/CheckRunPanel.astro'; +import Actions from '../../components/marketing/Actions.astro'; +import CtaLink from '../../components/marketing/CtaLink.astro'; +import NextSteps from '../../components/marketing/NextSteps.astro'; import { CTA, DOCS } from '../../lib/site'; + +const panelSteps = [ + { name: 'install', command: 'go mod download', duration: '5s', width: '16%' }, + { name: 'test', command: 'go test ./...', duration: '24s', width: '62%' }, + { name: 'build', command: 'go build ./...', duration: '8s', width: '22%' }, +]; + +const steps = [ + { + title: 'Register a GitHub App against those repos', + detail: 'It lives in your org, with the permissions the docs list — nothing broader.', + }, + { + title: 'Bind the repos in the UI', + detail: 'A binding names the App, the repo, the branches, and any command overrides.', + }, + { + title: 'Push', + detail: + 'The worker clones with an installation token that never lands in the remote URL, and strips the remote before your steps run.', + }, +]; --- -

Use cases / Private repos

-

Check Runs on private code, without Actions

-

- GitHub already stores the repo. You want a status on the commit. You do not - want to put that work on hosted Actions minutes or write workflow YAML for - install/test/build. -

-

- Register a GitHub App against those private repos, bind them in the UI, and - the worker clones with an installation token that never lands in the remote - URL. The Check Run is the artifact GitHub already knows how to display. -

-

- Setup: - GitHub App - and - bindings. -

- + + + Quickstart + How it works + + + + +
+ +
+ +
+ + + GitHub already knows how to display it — on the commit, in the pull + request, and in branch protection. + + + The details URL is GET /runs/{'{id}'} on your instance, behind + a session unless that binding opted into shareable logs. + + + .ci.yml holds the same commands you already run locally. + + +
+ +
+
+ + Permissions, events, and the webhook URL to use. + + + Bindings, branch filters, and command overrides. + +
+
+ +
diff --git a/src/pages/use-cases/self-hosted-teams.astro b/src/pages/use-cases/self-hosted-teams.astro index 3c7d1b7..24a8730 100644 --- a/src/pages/use-cases/self-hosted-teams.astro +++ b/src/pages/use-cases/self-hosted-teams.astro @@ -1,30 +1,117 @@ --- import MarketingPage from '../../layouts/MarketingPage.astro'; +import PageHero from '../../components/marketing/PageHero.astro'; +import Section from '../../components/marketing/Section.astro'; +import Grid from '../../components/marketing/Grid.astro'; +import Card from '../../components/marketing/Card.astro'; +import Actions from '../../components/marketing/Actions.astro'; +import CtaLink from '../../components/marketing/CtaLink.astro'; +import Callout from '../../components/marketing/Callout.astro'; +import MarkList from '../../components/marketing/MarkList.astro'; +import NextSteps from '../../components/marketing/NextSteps.astro'; import { CTA, DOCS } from '../../lib/site'; + +const stack = [ + 'UI and JSON API', + 'Webhook receiver', + 'Job runner', + 'SQLite in DATA_DIR', + 'Job logs on disk', + 'Nothing else to schedule', +]; --- -

Use cases / Self-hosted teams

-

Runner and logs stay on your box

-

- The team already pays for a VPS or a home-lab machine. openpreflight is a - Compose stack on that machine: UI, webhook, runner, SQLite, logs. -

-

- GitHub only receives Check Run payloads and a details URL that points at - you. Shareable logs are opt-in per binding. Operations (backups, upgrades, - what a restart does to an in-flight job) are in - operations. -

-

- Coolify can inventory servers and install the worker. It is not required, - and it does not execute jobs. -

- + + + Self-hosting details + Quickstart + + + +
+ +
+ +
+ + + Conclusion plus a truncated log tail, written onto the commit through + your own GitHub App. + + + It points back at your host. The full log is served by your instance, + not copied anywhere. + + + Shareable logs are opt-in per binding. There is no vendor telemetry + endpoint and no hosted control plane. + + +
+ +
+
+ + Backups, upgrades, and what a restart does to an in-flight job. + + + Reverse proxy, volumes, and the environment reference. + +
+ +

+ Coolify can inventory servers and install the worker for you. It is + optional, and it does not execute jobs — see{' '} + integrations for where it actually sits. +

+
+
+ +
diff --git a/src/pages/why.astro b/src/pages/why.astro index 6f2da0a..d2d8dbd 100644 --- a/src/pages/why.astro +++ b/src/pages/why.astro @@ -1,66 +1,195 @@ --- import MarketingPage from '../layouts/MarketingPage.astro'; +import PageHero from '../components/marketing/PageHero.astro'; +import Section from '../components/marketing/Section.astro'; +import Grid from '../components/marketing/Grid.astro'; +import Card from '../components/marketing/Card.astro'; +import MarkList from '../components/marketing/MarkList.astro'; +import Actions from '../components/marketing/Actions.astro'; +import CtaLink from '../components/marketing/CtaLink.astro'; +import Prose from '../components/marketing/Prose.astro'; +import Callout from '../components/marketing/Callout.astro'; +import NextSteps from '../components/marketing/NextSteps.astro'; import { CTA, DOCS, REPO, RELEASE } from '../lib/site'; + +const positions = [ + { + eyebrow: 'One end', + title: 'Hosted Actions', + body: 'The default path. Workflow YAML, hosted minutes, and a lot of surface area.', + }, + { + eyebrow: 'Other end', + title: 'A full platform', + body: 'Right when you need matrices, caches, and artifacts, with a control plane to operate.', + }, + { + eyebrow: 'This', + title: 'In between', + body: 'The commit is gated, the logs stay here, and the operator is a binary.', + }, +]; + +const shape = [ + { + title: 'One process', + body: 'Configurator and worker are the same program. Nothing else to schedule.', + }, + { + title: 'One SQLite file', + body: 'Apps, bindings, jobs, and encrypted secrets are rows in one file.', + }, + { + title: 'An App you own', + body: 'You register the GitHub App. It is not brokered through anyone else.', + }, + { + title: 'No new DSL', + body: 'Pipelines are install / test / build in .ci.yml.', + }, +]; + +const ceiling = [ + 'GitHub Actions YAML', + 'actions/runner', + 'Creating GitHub Apps for you', + 'Matrices', + 'Caches', + 'Artifacts', +]; --- -

Why

-

CI for private repos, small enough to host

-

- The problem is not “we need another CI platform.” It is: we want a Check - Run on our private code, on our machine, without learning a second - workflow language. -

+ + + See the product + View source + + -

The problem

-

- GitHub already knows how to show a Check Run. Hosted Actions is the default - path, and it brings YAML, hosted minutes, and a lot of surface area. Full - self-hosted platforms exist for teams that need matrices, caches, and - artifacts. Plenty of people want something in between: the commit is gated, - the logs stay here, and the operator is a binary. -

-

- v1.0.0 is out. Linux binaries are on the{" "} - GitHub Release. The ceiling below is still the - product boundary, not a backlog. -

+
+ + {positions.map((position) => ( + {position.body} + ))} + + +

+ Both ends are the right answer for someone. Plenty of people want + neither: no hosted minutes to buy, no orchestration layer to run — just + the commit checked, on hardware that is already theirs. +

+

+ v1.0.0 is out. Linux binaries are on the{' '} + GitHub Release. The ceiling below is still the + product boundary, not a backlog. +

+
+
-

The shape

-

- One process is configurator and worker. One SQLite file holds Apps, - bindings, jobs, and encrypted secrets. You register a GitHub App you own. - Pipelines are install/test/build in .ci.yml, not a new DSL. - Runs are gated on the commit the way Zuul does it — trigger on the check - suite, build the immutable SHA, one live run per commit. The ceiling of - that model is in - ADR 005. -

+
+ + {shape.map((item) => ( + + + + ))} + + +

+ Runs are gated on the commit the way Zuul does it — trigger on the + check suite, build the immutable SHA, one live run per commit. The + ceiling of that model is written down in ADR 005 rather than discovered + later. +

+
+ + Read ADR 005 + +
-

Contrast

-

- Hosted runners and full platforms solve orchestration. This product does - not. It reports Check Runs from a worker you host. GitHub Actions can still - orchestrate everything else. They can coexist; this does not replace - workflow YAML. See - openpreflight and GitHub Actions. -

+
+
+ + Matrices, caches, artifacts, a marketplace, and a scheduler. openpreflight + does none of that, and GitHub Actions can keep doing all of it. + + + Clone the SHA, run install / test / build, report one Check Run. The two + can coexist on the same repository. + +
+ + + openpreflight and GitHub Actions + + +
-

The ceiling

-

- Not in v1: Actions YAML, actions/runner, creating Apps for - you, matrices, caches, artifacts. Jobs on another machine use a Docker - engine (CI_DOCKER_HOST), not Coolify as a job runner. If those - are requirements, pick a different tool — the - docs comparison is honest about - that. -

+
+ + +

+ Jobs on another machine use a Docker engine (CI_DOCKER_HOST), + not Coolify as a job runner. The docs comparison is + honest about which neighbour to reach for instead. +

+
+
- +
diff --git a/src/styles/global.css b/src/styles/global.css index 09cce4c..f06da8b 100644 --- a/src/styles/global.css +++ b/src/styles/global.css @@ -351,145 +351,129 @@ body { font-family: var(--font-family-sans); } -a { - color: inherit; - text-decoration: none; -} - -.marketing-article h1 { - font-size: 2.25rem; - font-weight: 600; - letter-spacing: -0.04em; - line-height: 1.1; +/* In @layer base so utilities (text-primary, underline, ...) still win: an + unlayered rule here would override every Tailwind utility on an anchor. */ +@layer base { + a { + color: inherit; + text-decoration: none; + } } -.marketing-article .lead { - margin-top: 1.25rem; - font-size: 1.125rem; - line-height: 1.6; - color: var(--muted-foreground); -} +/* rivelle:theme:end */ -.marketing-article h2 { - margin-top: 2.75rem; - font-size: 1.35rem; - font-weight: 600; - letter-spacing: -0.03em; -} +/* ------------------------------------------------------------------------ + Marketing subpage system + Sections, cards, and prose that carry the landing page's visual language + into /product, /why, /security and the rest. Tokens only — no second accent. + ------------------------------------------------------------------------ */ -.marketing-article h3 { - margin-top: 1.75rem; - font-size: 1.05rem; - font-weight: 600; +/* Softer relative of .hero-atmosphere for inner-page heroes. */ +.page-atmosphere { + background-image: + radial-gradient( + ellipse 70% 60% at 50% 0%, + color-mix(in srgb, var(--primary) 16%, transparent), + transparent 62% + ), + linear-gradient( + to right, + color-mix(in srgb, var(--foreground) 4%, transparent) 1px, + transparent 1px + ), + linear-gradient( + to bottom, + color-mix(in srgb, var(--foreground) 4%, transparent) 1px, + transparent 1px + ); + background-size: auto, 48px 48px, 48px 48px; + mask-image: linear-gradient(to bottom, black 40%, transparent); } -.marketing-article p, -.marketing-article ul, -.marketing-article ol { - margin-top: 0.85rem; - line-height: 1.65; +/* Reading-width body copy inside a section. design.md: reading-max 42rem. */ +.prose-block { + max-width: 42rem; + font-size: 1rem; + line-height: 1.7; color: var(--muted-foreground); } -.marketing-article ul, -.marketing-article ol { - padding-left: 1.2rem; +.prose-block > * + * { + margin-top: 1rem; } -.marketing-article li + li { - margin-top: 0.4rem; +.prose-block strong { + color: var(--foreground); + font-weight: 600; } -.marketing-article a { +.prose-block a { color: var(--primary); + text-decoration: underline; + text-decoration-color: color-mix(in srgb, var(--primary) 35%, transparent); text-underline-offset: 4px; } -.marketing-article a:hover { - text-decoration: underline; +.prose-block a:hover { + text-decoration-color: currentColor; } -.marketing-article pre, -.marketing-article .diagram { - margin-top: 1.25rem; - overflow-x: auto; - border: 1px solid var(--border); - border-radius: 0.75rem; - padding: 1rem 1.1rem; +.prose-block code, +.inline-code { font-family: var(--font-mono); - font-size: 0.8rem; - line-height: 1.55; + font-size: 0.85em; + border-radius: var(--radius-sm); + padding: 0.1em 0.35em; + background: color-mix(in srgb, var(--foreground) 6%, transparent); color: var(--foreground); } -.marketing-article .kicker { - font-family: var(--font-mono); - font-size: 0.875rem; - font-weight: 500; - letter-spacing: 0.04em; - color: var(--primary); +.prose-block ul, +.prose-block ol { + padding-left: 1.15rem; + list-style-position: outside; } -.marketing-article .cta-row { - display: flex; - flex-wrap: wrap; - gap: 0.75rem; - margin-top: 2.5rem; +.prose-block ul { + list-style-type: disc; } -.marketing-article .cta-row a { - display: inline-flex; - align-items: center; - border-radius: 0.375rem; - padding: 0.5rem 1rem; - font-size: 0.875rem; - font-weight: 500; - text-decoration: none; -} - -.marketing-article .cta-row a.primary { - background: var(--primary); - color: var(--primary-foreground); +.prose-block ol { + list-style-type: decimal; } -.marketing-article .cta-row a.secondary { - border: 1px solid var(--border); - color: var(--foreground); +.prose-block li + li { + margin-top: 0.45rem; } -.marketing-article .card-grid { - display: grid; - gap: 0.85rem; - margin-top: 1.5rem; +/* Copy that sits on the inverted (bg-foreground) band. */ +.on-dark .prose-block { + color: color-mix(in srgb, var(--background) 62%, transparent); } -@media (min-width: 640px) { - .marketing-article .card-grid.cols-2, - .marketing-article .card-grid.cols-3 { - grid-template-columns: repeat(2, minmax(0, 1fr)); - } +.on-dark .prose-block strong, +.on-dark .prose-block code { + color: var(--background); } -.marketing-article .card-grid a, -.marketing-article .card-grid div { - border: 1px solid var(--border); - border-radius: 1rem; - padding: 1.1rem 1.2rem; - color: inherit; - text-decoration: none; +.on-dark .prose-block code { + background: color-mix(in srgb, var(--background) 12%, transparent); } -.marketing-article .card-grid a:hover { - border-color: var(--primary); - text-decoration: none; +.on-dark .prose-block a { + color: var(--primary); } -.marketing-article .card-grid h3 { - margin-top: 0; +/* Wire diagrams: mono labels in boxes, wrapping instead of overflowing the + way the old ASCII art did on a phone. */ +.wire-node { + border: 1px solid color-mix(in srgb, var(--foreground) 12%, transparent); + border-radius: var(--radius-lg); + background: var(--card); + font-family: var(--font-mono); } -.marketing-article .card-grid p { - margin-top: 0.4rem; - font-size: 0.9rem; +.on-dark .wire-node { + border-color: color-mix(in srgb, var(--background) 14%, transparent); + background: color-mix(in srgb, var(--background) 6%, transparent); } -/* rivelle:theme:end */ From ce1e87a373e3ffe1c10cb65811115e88b1517c4b Mon Sep 17 00:00:00 2001 From: trivedi-vatsal Date: Sat, 29 Aug 2026 10:57:45 +0530 Subject: [PATCH 2/4] Build the subpages out to the homepage's depth MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The redesign fixed the layout but left every inner page thinner than the landing page — 354-650 words against its 893. This adds the substance, sourced from the docs and code repos rather than invented. Each page now carries the material a reader needs before installing: - /why: Zuul as named prior art (what is borrowed, what is rejected), the five things this is genuinely better at, the eight it is genuinely worse at, and the five questions people ask first. 486 -> 1152 words. - /product: one-live-run-per-commit semantics, the four ways into the process, binding -> App -> settings precedence, and an honest section on concurrency, restarts, backups and monorepos. 650 -> 1225. - /pipeline: the five keys, Node defaults by lockfile, binding overrides, what a pipeline cannot express, and what stops a run. 394 -> 875. - /self-hosted: requirements, what has to survive a redeploy, and day-two reality — restarts, forward-only migrations, sizing, pruning. 374 -> 886. - /security: the bindings allow-list as the trust boundary, key rotation, and a table of what is reachable without a session. 354 -> 809. - /compare: the pick-by-what-you-want table plus Woodpecker, Drone, actions/runner and Jenkins with real choose-this-instead guidance. 397 -> 823. - The hub and leaf pages get the same treatment; all are now 700-1225 words against the homepage's 893. Add a Faq component for the positioning questions, and fix Steps so a step title renders markup instead of escaping it. Every claim traces to openpreflight/docs or the code repo README, and how-to still links out rather than being duplicated here. Co-Authored-By: Claude Opus 5 (1M context) --- src/components/marketing/Faq.astro | 47 +++++ src/components/marketing/Steps.astro | 2 +- src/pages/compare/github-actions.astro | 117 +++++++++++++ src/pages/concepts.astro | 161 +++++++++++++++++ src/pages/integrations/github-app.astro | 105 +++++++++++ src/pages/integrations/index.astro | 109 ++++++++++++ src/pages/open-source.astro | 102 +++++++++++ src/pages/pipeline.astro | 118 +++++++++++++ src/pages/product/index.astro | 157 +++++++++++++++-- src/pages/security.astro | 112 +++++++++++- src/pages/self-hosted.astro | 131 ++++++++++++++ src/pages/use-cases/index.astro | 149 +++++++++++++++- src/pages/use-cases/open-source.astro | 107 ++++++++++++ src/pages/use-cases/private-repos.astro | 110 ++++++++++++ src/pages/use-cases/self-hosted-teams.astro | 109 ++++++++++++ src/pages/why.astro | 184 +++++++++++++++++++- 16 files changed, 1791 insertions(+), 29 deletions(-) create mode 100644 src/components/marketing/Faq.astro diff --git a/src/components/marketing/Faq.astro b/src/components/marketing/Faq.astro new file mode 100644 index 0000000..c6516bb --- /dev/null +++ b/src/components/marketing/Faq.astro @@ -0,0 +1,47 @@ +--- +/** + * Positioning questions, answered inline. Each answer links the doc or ADR + * that argues it in full — this page never becomes the source of truth. + */ +interface Item { + question: string; + answer: string; + href?: string; + linkLabel?: string; +} + +interface Props { + items: Item[]; + tone?: 'default' | 'dark'; + class?: string; +} + +const { items, tone = 'default', class: className = '' } = Astro.props; + +const dividerClass = tone === 'dark' ? 'divide-background/12' : 'divide-foreground/10'; +const borderClass = tone === 'dark' ? 'border-background/12' : 'border-foreground/10'; +const answerClass = tone === 'dark' ? 'text-background/60' : 'text-muted-foreground'; +--- + +
+ { + items.map((item) => ( +
+
{item.question}
+
+ + {item.href && ( + + {item.linkLabel ?? 'Read more'} → + + )} +
+
+ )) + } +
diff --git a/src/components/marketing/Steps.astro b/src/components/marketing/Steps.astro index ea90584..c339435 100644 --- a/src/components/marketing/Steps.astro +++ b/src/components/marketing/Steps.astro @@ -34,7 +34,7 @@ const detailClass = tone === 'dark' ? 'text-background/60' : 'text-muted-foregro {String(i + 1).padStart(2, '0')}
-

{step.title}

+

+

+
+ { + shortVersion.map((row, i) => ( +
0 ? 'border-t border-foreground/10' : ''} ${row.self ? 'bg-primary/[.04]' : ''}`} + > +

{row.want}

+ + {row.pick} + +
+ )) + } +
+
+
+
+
+ { + neighbours.map((n) => ( +
+

{n.name}

+

{n.summary}

+
+
+

+ Choose it +

+

{n.them}

+
+
+

+ Choose openpreflight +

+

{n.us}

+
+
+
+ )) + } +
+ +

+ The full write-up, including where this is genuinely weaker than every + one of them, is in the{' '} + docs comparison. The short list + of limits lives on why this shape. +

+
+
+
diff --git a/src/pages/concepts.astro b/src/pages/concepts.astro index 5194c62..808f728 100644 --- a/src/pages/concepts.astro +++ b/src/pages/concepts.astro @@ -6,6 +6,7 @@ import Actions from '../components/marketing/Actions.astro'; import CtaLink from '../components/marketing/CtaLink.astro'; import Callout from '../components/marketing/Callout.astro'; import NextSteps from '../components/marketing/NextSteps.astro'; +import Steps from '../components/marketing/Steps.astro'; import { CTA, DOCS } from '../lib/site'; const groups = [ @@ -18,6 +19,12 @@ const groups = [ body: 'GitHub’s status object on a commit. openpreflight creates one per job and writes the conclusion plus a log tail. It is the product’s hero artifact.', links: [{ label: 'ADR 005', href: `${DOCS}/adr/005-check-suite-gating/` }], }, + { + id: 'check-suite', + term: 'Check suite', + body: 'GitHub’s container for the runs on one commit. It is the only thing that triggers work here — never push, never pull_request. A suite is already scoped to one commit and one App.', + links: [{ label: 'ADR 005', href: `${DOCS}/adr/005-check-suite-gating/` }], + }, { id: 'github-app', term: 'GitHub App', @@ -38,6 +45,12 @@ const groups = [ body: 'A row that says: this App, this repo, these branches, these optional command overrides. No enabled binding, no job.', links: [{ label: 'Bindings', href: `${DOCS}/setup/bindings/` }], }, + { + id: 'settings', + term: 'Settings', + body: 'One row holding the instance-wide defaults: check name, pipeline file path, timeout, how many jobs run at once, log size and retention. A binding or an App can override most of them.', + links: [{ label: 'Configuration', href: `${DOCS}/start/configuration/` }], + }, { id: 'pipeline', term: 'Pipeline (.ci.yml)', @@ -58,6 +71,18 @@ const groups = [ body: 'One queued or running attempt for an (app, repo, sha). Logs are files under DATA_DIR.', links: [{ label: 'Logs', href: `${DOCS}/using/logs/` }], }, + { + id: 'workspace', + term: 'Workspace', + body: 'The per-job checkout of the exact SHA. It is detached, the remote is stripped before any step runs, and the whole directory is deleted when the job ends.', + links: [], + }, + { + id: 'installation-token', + term: 'Installation token', + body: 'A short-lived credential minted per job from the webhook’s installation id. It clones the commit and writes the Check Run, and it is never placed in the job’s environment.', + links: [{ label: 'Security model', href: `${DOCS}/understanding/security-model/` }], + }, { id: 'executor', term: 'Executor', @@ -140,6 +165,142 @@ const groups = [
+
+ check suite is created for a commit', + detail: + 'GitHub creates it and delivers it to the webhook URL of the GitHub App you registered.', + }, + { + title: 'The binding decides whether anything happens', + detail: + 'No enabled binding for that repo, and the delivery is acknowledged and dropped — however valid its signature.', + }, + { + title: 'A job is queued for that (app, repo, sha)', + detail: + 'One live run per commit. A second delivery for the same commit is answered already queued, or supersedes the run in flight if a human pressed Re-run.', + }, + { + title: 'An installation token fills a workspace', + detail: + 'The exact SHA is fetched, detached, and the remote stripped — before any of your commands run.', + }, + { + title: 'The executor runs the pipeline', + detail: + 'In this process, or in a container when runtime: is set. A failing step stops the run and later steps report skipped.', + }, + { + title: 'A Check Run is completed', + detail: + 'Conclusion plus a log tail on the commit, with a details URL pointing at the full log — behind a session, unless that binding opted into a shareable log.', + }, + ]} + /> + + + The same loop, at reference depth + + +
+ +
+
+ { + [ + { + word: 'Workflow', + why: 'There is no workflow language. A pipeline is three shell commands in a fixed order.', + }, + { + word: 'Stage', + why: 'No stages and no needs:. install, then test, then build, and that is the plan format.', + }, + { + word: 'Agent', + why: 'There is no agent protocol to register. Jobs run in this process or in a sibling container.', + }, + { + word: 'Matrix', + why: 'One pipeline per commit, never one per version combination.', + }, + { + word: 'Artifact', + why: 'Nothing is handed from one step to the next, or published anywhere.', + }, + { + word: 'Cache', + why: 'Every job is a fresh shallow clone. Nothing is carried between runs.', + }, + { + word: 'Plugin', + why: 'No marketplace and no extension API. Steps are commands you already run locally.', + }, + { + word: 'Control plane', + why: 'There is no hosted anything. The binary on your server is the whole deployment.', + }, + ].map((item) => ( +
+

+ {item.word} +

+

+

+ )) + } +
+
+ +
+
+ { + [ + { + n: '01', + title: 'Binding', + body: 'The per-repo row. Branches, check name, pipeline file, timeout, commands, shareable logs.', + }, + { + n: '02', + title: 'App', + body: 'Defaults for every repo bound to that GitHub App.', + }, + { + n: '03', + title: 'Settings', + body: 'The instance-wide row, used when neither of the above says otherwise.', + }, + ].map((item) => ( +
+ {item.n} +

{item.title}

+

{item.body}

+
+ )) + } +
+ +

+ The install / test / build commands have their own order — repo file, + then binding overrides, then Node defaults from package.json, + then nothing to run, which reports skipped. That ladder is on{' '} + /pipeline. +

+
+
+ +
+
+ { + [ + { name: 'Checks', level: 'Read and write', why: 'Creating and completing the Check Run — the entire point.' }, + { name: 'Contents', level: 'Read-only', why: 'Cloning the commit being checked. Read-only: it never writes to your repo.' }, + { name: 'Metadata', level: 'Read-only', why: 'The mandatory baseline GitHub requires alongside the others.' }, + { name: 'Check suite', level: 'Event', why: 'The trigger. GitHub creates a suite per commit and delivers it here.' }, + { name: 'Check run', level: 'Event', why: 'Re-run requests, honoured only for this App’s own checks.' }, + ].map((item) => ( +
+
+ {item.name} + + {item.level} + +
+

{item.why}

+
+ )) + } +
+

+ The authoritative table — exact values, the webhook URL format, and the + order to do things in — is in the setup docs. It changes with the code, so + this page does not try to be the checklist. +

+ + The full checklist + +
+
@@ -99,6 +138,72 @@ const panelSteps = [
+
+
+ + One job produces one Check Run, so there is a single entry to tick in + branch protection. Pick the check name before you rely on it — renaming + one strands the rule that referenced it. + + + The same App can cover everything it is installed on. Each repo still + needs its own enabled binding, which is what keeps the allow-list + meaningful. + + + A binding can change the branch list, the commands, the timeout, the + check name, and whether logs are shareable — useful for the one repo + that does not fit the default. + +
+ +

+ After the binding exists, the loop is automatic: a commit lands, a + check appears. The next thing worth reading is{' '} + what runs inside it. +

+
+
+ +
+ enabled binding is acknowledged and dropped, however valid its signature. That is the allow-list working, not a failure — check the binding first.', + href: `${DOCS}/setup/bindings/`, + linkLabel: 'Bindings', + }, + { + question: 'Where did my PEM go?', + answer: + 'It is encrypted at rest and never shown again. Reads return a redacted marker rather than the value, so keep your own copy when you generate it on GitHub.', + href: `${DOCS}/understanding/security-model/`, + linkLabel: 'Security model', + }, + { + question: 'Does the App need to be public?', + answer: + 'No. It lives in your account or organisation and is installed only where you install it. Nothing about it is published.', + }, + { + question: 'What about GitHub Enterprise Server?', + answer: + 'Each App row carries an API URL, and the git origin is derived from it. The plumbing is there; it has not been tested against a real instance, and an issue saying which version broke would be useful.', + href: `${DOCS}/start/faq/`, + linkLabel: 'FAQ', + }, + ]} + /> +
+ +
+ runtime:, steps run in the worker process. Most installs never need anything past this point.', + }, + { + title: 'Add Docker only if you need it', + detail: + 'Set CI_DOCKER_HOST — or mount a socket — when you want container runtimes or fork pull requests.', + }, + ]} + /> +
+ +
+
+ +

+ User tokens and OAuth tokens are refused by GitHub for this — the + Check Runs API is App-only. That single fact is why the integration + list starts and ends where it does. +

+

+ A GitHub App also has exactly one webhook URL, which + is why Coolify’s own GitHub connector cannot stand in: its webhook + already belongs to Coolify’s deploy pipeline, and its manifest carries + no checks permission. +

+
+ + + Receives check-suite webhooks, mints a short-lived installation token + per job, and writes the Check Run back onto the commit. + + + Get created for you, act on repos you have not bound, or hand its + private key to a job. + + +
+ + ADR 003 + Set one up + +
+ +
+ runtime:, and for every fork pull request. The engine comes from CI_DOCKER_HOST, else DOCKER_HOST, else the default socket — and it can be a different machine, over Docker’s own remote API.', + href: `${DOCS}/adr/004-docker-executor/`, + linkLabel: 'ADR 004', + }, + { + question: 'What happens if the engine is unreachable?', + answer: + 'The job fails rather than quietly falling back to running your commands as a process on the host. The service itself boots and reports checks fine without an engine — only runtime: jobs and fork PRs need it.', + }, + { + question: 'Do job containers get the Docker socket?', + answer: + 'No. Job containers drop capabilities, set no-new-privileges, and never receive the engine socket. Image names are allow-listed.', + href: `${DOCS}/understanding/security-model/`, + linkLabel: 'Security model', + }, + { + question: 'What is Coolify used for?', + answer: + 'Server inventory, a repository picker for the bindings screen, and an install-worker call that creates the compose application for you. That is the whole surface.', + href: `${DOCS}/setup/coolify/`, + linkLabel: 'Coolify', + }, + { + question: 'Can Coolify run the jobs?', + answer: + 'No. A Coolify token cannot start a docker run. To put jobs on another machine you point CI_DOCKER_HOST at that machine’s Docker engine — that is Docker’s remote API, not Coolify’s.', + href: `${DOCS}/understanding/deployment/`, + linkLabel: 'Deployment', + }, + ]} + /> +
+
+
+
+ +

+ The whole implementation lives under internal/. There is + no pkg/, nothing importable, and no extension API — which + means there is also no plugin boundary where behaviour can hide. +

+

+ go test ./... needs no network and no credentials: the + GitHub and Coolify APIs are faked, and the clone and pipeline tests run + against a real git-http-backend server over a fixture + repository. +

+

+ That is the practical reason to open source a worker that holds App + private keys — a reviewer can read every path a key takes in an + afternoon, and run the suite without asking anyone for access. +

+
+ +
+
+
@@ -132,6 +167,73 @@ const repos = [
+
+
+
+

+ Apache-2.0 +

+

The binary

+

+ A permissive licence with an explicit patent grant, for the thing you + will actually run on your own infrastructure. Fork it, run it + internally, or ship a modified build. +

+ + Read the licence → + +
+
+

+ MIT +

+

The two sites

+

+ The documentation and this marketing site. Lighter terms for prose and + markup that nobody deploys as infrastructure — take a page as a + starting point if it is useful. +

+
+
+
+ +
+ +
+ needs:', + 'Fan-out and parallel steps', + 'Conditional steps', + 'Caches between runs', + 'Artifacts between steps', + 'Build matrices', + 'Path filters for monorepos', + 'Steps that are not install / test / build', +]; + +const overrides = [ + 'the branch list', + 'the check name', + 'the pipeline file path', + 'the timeout', + 'the install / test / build commands', + 'whether logs are shareable', +]; + const resolution = [ { order: '01', @@ -106,6 +129,14 @@ timeout: 15m`} )) } + +

+ A file that only sets runtime: or timeout: + still applies those, while the commands come from the binding or from + package.json. Image names are allow-listed — no shell + metacharacters and no leading -. +

+
+
+
+
+

Node defaults

+ +

+ With no pipeline file and no overrides, a Node repo is inferred from + package.json: the install command follows the lockfile + — npm ci, pnpm, or yarn — and + test and build run only if those + scripts exist. +

+

+ That is the whole inference. No other language is guessed, and + nothing is invented for a repo that has neither a file nor scripts — + it reports skipped. +

+
+
+
+

Binding overrides

+

+ For a repo you would rather not add a file to, a binding can override: +

+ +

+ At run time precedence is binding → App → settings. +

+
+
+
+
@@ -155,6 +218,61 @@ timeout: 15m`}
+
+ +

+ Every job is a fresh shallow clone, so nothing is carried between runs or + handed to a later step. If your build needs any of the above, a pipeline + engine like Woodpecker or Drone is the right tool — the{' '} + docs comparison{' '} + makes that call explicitly. +

+
+ +
+ skipped, not failed. That is a different signal on purpose — a repo with no pipeline is not a broken build.', + }, + { + question: 'How long can a run take?', + answer: + 'Until the timeout. timeout in the file overrides the binding, which overrides the settings default. A job that exceeds it is cut off and reported.', + href: `${DOCS}/start/configuration/`, + linkLabel: 'Configuration', + }, + { + question: 'How big can a log get?', + answer: + 'There is a byte cap; the log stops growing at it while the run continues. Logs and their job rows are pruned on a retention window, so history is finite by design.', + href: `${DOCS}/using/logs/`, + linkLabel: 'Logs', + }, + { + question: 'Can a fork PR run my pipeline?', + answer: + 'Not by default — fork pull requests are skipped. Opting in requires a reachable Docker engine and a default_runtime, and fork jobs then always run in Docker, never as a process on the host.', + href: `${DOCS}/adr/004-docker-executor/`, + linkLabel: 'ADR 004', + }, + ]} + /> +
+ requested delivery for a commit already in flight is answered already queued. Delivery ids still in flight are deduped.', + }, + { + title: 'A human presses Re-run', + detail: + 'A rerequested delivery supersedes the run in flight — the old one is cancelled first, then the new one is queued.', + }, + { + title: 'A newer commit lands', + detail: + 'An older in-flight job for the same repo and ref on a different SHA is cancelled, and the new SHA is enqueued in its place.', + }, +]; + +const surfaces = [ + { + title: 'The web UI', + body: 'Server-rendered pages for Apps, bindings, settings, jobs, and log pages. This is where a first install gets configured.', + }, + { + title: 'The JSON API', + body: 'The same handlers answer JSON. POST /api/v1/login returns a bearer token; jobs can be listed, re-run, and cancelled from a script.', + }, + { + title: 'The webhook', + body: 'POST /webhook/{slug} is the only route GitHub touches, and it is HMAC-verified against that App\u2019s secret.', + }, + { + title: 'A health probe', + body: 'GET /health is liveness. It answers 503 when the process cannot read SQLite, which is what a reverse proxy should watch.', + }, +]; + const inbound = [ { label: 'GitHub', detail: 'POST /webhook/{slug}' }, { label: 'Browser / CLI', detail: 'session or Bearer' }, @@ -166,26 +204,53 @@ const outbound = [ +
+ + {dedup.map((item, index) => ( + + + + ))} + + +

+ Branch protection reads whichever check finished last. Holding to one + live run per (app, repo, sha) is what makes a required check + behave under force-pushes and rapid pushes — the argument is in{' '} + ADR 005. +

+
+
+

Bindings and Apps are rows in SQLite, edited in the web UI — not a - block of environment variables per installation. An optional - .ci.yml in the repo supplies the commands, with binding - overrides and package.json scripts filling the gaps. + block of environment variables per installation. The process reads a + handful of env vars so it can start; everything after that is a row. +

+

+ A binding can override the branch list, the check name, the pipeline + file path, the timeout, the install / test / build commands, and + whether logs are shareable. At run time precedence is + binding → App → settings. +

+

+ The bindings table is the allow-list. A webhook for a + repo with no enabled binding is acknowledged and dropped, however + valid its signature. Only enable repos you trust — a pipeline runs + that repo’s own commands.

Resolution order and the full field list live in{' '} the pipeline docs; the short version is on /pipeline.

-

- Portable in the real sense: the same - .ci.yml and the same worker run on your host. - That is not “any CI vendor” — there are no GitLab, Jenkins, or - CircleCI adapters. -

+
+ + {surfaces.map((item) => ( + + + + ))} + +

+ The HTML UI and the JSON API come out of the same handlers, so anything + you can click you can also script. There is no separate frontend to + deploy and no second service to keep in sync. +

+ + API reference + +
+
- + - GitHub shows conclusion and a log tail on the commit, the same way it - shows any other check. + GitHub shows the conclusion and a truncated log tail on the commit, the + same way it shows any other check. Steps are rendered as a table in that + run’s summary rather than as separate runs. - GET /runs/{id} on your instance — session by default, - or a shareable log if that binding opted in. + GET /runs/{id} on your instance. GitHub never fetches + it — the reader’s browser does, so it needs a session unless that + binding opted into shareable logs. + + + Written to a file per job on your disk, capped so one runaway build + cannot fill the volume, and pruned on a retention window you set. @@ -216,6 +305,46 @@ timeout: 15m`}
+
+ max_concurrent_jobs is a settings row that starts at 1 and can only be raised after first boot — and raising it adds concurrency on this one machine. Jobs never spread across hosts.', + href: `${DOCS}/start/configuration/`, + linkLabel: 'Configuration', + }, + { + question: 'What happens if you restart mid-job?', + answer: + 'Nothing in progress survives. On a clean signal the job writes itself cancelled and is not retried; on a kill it is requeued from the beginning on the next boot, which opens a second Check Run on that commit.', + href: `${DOCS}/understanding/operations/`, + linkLabel: 'Operations', + }, + { + question: 'What has to be backed up?', + answer: + 'DATA_DIR plus CI_SECRET_KEY, and neither is much use without the other. A backup without the key restores your bindings and job history but no credentials.', + href: `${DOCS}/understanding/operations/`, + linkLabel: 'Operations', + }, + { + question: 'Does it work for monorepos?', + answer: + 'It runs, but it runs everything on every push. There is no path filter, and a plan is three steps in a fixed order — so the usual monorepo answer, one job per affected package, has nothing to express itself with.', + href: `${DOCS}/start/faq/`, + linkLabel: 'FAQ', + }, + ]} + /> +
+ GIT_CONFIG_* Basic auth — never in the remote URL — then strips the remote before pipeline steps run.', + body: 'GitHub POSTs webhooks to your public HTTPS URL, HMAC-verified against that App\u2019s secret. The worker clones with a short-lived installation token passed through GIT_CONFIG_* as Basic x-access-token — GitHub\u2019s git endpoint wants Basic, not the REST API\u2019s Bearer. It never enters the remote URL, .git/config, or a command line, and the remote is removed before any pipeline step runs.', }, { title: 'Secrets at rest', @@ -86,6 +90,112 @@ const topics = [

+
+
+ +

+ A signed webhook for a repo with no enabled binding is acknowledged + and dropped, however valid its signature. Fork pull requests are + dropped on top of that unless you have explicitly opted in, and that + opt-in requires Docker. +

+

+ Enable only repos you trust. A pipeline runs that + repository’s own commands — in this process by default, or in a + container when runtime: is set. That is the trust boundary + worth thinking about before you tick a box in the picker. +

+

+ Bindings and their overrides are documented in{' '} + enable repos. +

+
+
+

What a job container gets

+ no-new-privileges', + 'An allow-listed image name', + 'A fresh environment with none of your secrets in it', + 'No access to the Docker engine socket', + ]} + mark="check" + /> +
+
+
+ +
+ + + App PEMs, webhook secrets, and Coolify tokens are AES-256-GCM columns in + SQLite. Reads give back a redacted marker, never the value. + + + Set the new key, keep the old one alongside it for one start, and the + secret columns are re-sealed under the new key. A row that opens with + neither key fails startup rather than serving broken. + + + A backup without the key still restores bindings and job history — but + the App has to be pasted in again. Store the key where your other + secrets live, not beside the backup. + + +
+ +
+
+ { + [ + { + route: 'POST /webhook/{slug}', + who: 'GitHub only', + note: 'HMAC-verified against that App’s webhook secret. An unsigned or wrongly-signed delivery goes nowhere.', + }, + { + route: 'GET /health', + who: 'Anyone', + note: 'Liveness for your proxy. 503 when SQLite cannot be read. It carries no data about your repos.', + }, + { + route: 'GET /runs/{id}', + who: 'Session — or anyone, if opted in', + note: 'A binding can make its log pages readable by link holders. Job ids are random UUIDs; treat such a link as a secret.', + }, + { + route: 'Everything else', + who: 'Session or bearer token', + note: 'Browser writes additionally need a CSRF token. Bearer callers carry no ambient cookie and so skip CSRF.', + }, + ].map((row, i) => ( +
0 ? 'border-t border-foreground/10' : ''}`}> + {row.route} + {row.who} +

{row.note}

+
+ )) + } +
+ +

+ There are no teams, no roles, and no SSO. That is a real limit, listed + with the others on why this shape — not something to + discover after you have onboarded a team. +

+
+
+

diff --git a/src/pages/self-hosted.astro b/src/pages/self-hosted.astro index c6413c4..ea4c1cc 100644 --- a/src/pages/self-hosted.astro +++ b/src/pages/self-hosted.astro @@ -8,7 +8,9 @@ import CodePanel from '../components/marketing/CodePanel.astro'; import Actions from '../components/marketing/Actions.astro'; import CtaLink from '../components/marketing/CtaLink.astro'; import Callout from '../components/marketing/Callout.astro'; +import MarkList from '../components/marketing/MarkList.astro'; import NextSteps from '../components/marketing/NextSteps.astro'; +import Faq from '../components/marketing/Faq.astro'; import { CTA, DOCS, REPO } from '../lib/site'; const yours = [ @@ -23,6 +25,33 @@ const theirs = [ { label: 'details_url', detail: 'points back at your host' }, ]; +const state = [ + { + path: 'DATA_DIR/ci.db', + holds: 'Settings, users, GitHub Apps, bindings, job rows — with the secret columns encrypted inside it.', + keep: 'Yes', + tone: true, + }, + { + path: 'DATA_DIR/logs/', + holds: 'One file per job, the full build log.', + keep: 'If you want history', + tone: true, + }, + { + path: 'CI_SECRET_KEY', + holds: 'The AES-256-GCM key for those encrypted columns. Store it wherever your other secrets live — not next to the backup.', + keep: 'Yes, separately', + tone: true, + }, + { + path: 'WORKSPACE_DIR', + holds: 'Per-job checkouts, deleted when the job ends.', + keep: 'No', + tone: false, + }, +]; + const operate = [ { title: 'A public HTTPS URL', @@ -109,6 +138,64 @@ const operate = [

+
+
+ CI_SECRET_KEY, generated once and kept forever', + ]} + mark="check" + /> +
+

What ships in the image

+

+ A static Go binary plus git, Node, and the + Docker CLI. It runs as an unprivileged user, and pipeline shells are + reaped by the init process rather than left behind. +

+

+ Node matters only when a job has no runtime: and + runs as a process. A Docker engine matters only for + runtime: jobs and fork pull requests — the + service boots and reports checks without one. +

+
+
+
+ +
+
+ { + state.map((row, i) => ( +
0 ? 'border-t border-foreground/10' : ''}`}> + {row.path} +

{row.holds}

+ + {row.keep} + +
+ )) + } +
+ +

+ It restores your bindings and job history, but no credentials — the App + PEM and webhook secrets stay unreadable and have to be pasted in again. + Back both up, and back them up separately. +

+
+
+
+
+ cancelled, and is not retried on its own. On a kill it stays in progress and the next boot runs it again from the beginning, which opens a second Check Run on that commit.', + href: `${DOCS}/understanding/operations/`, + linkLabel: 'Operations', + }, + { + question: 'How do upgrades work?', + answer: + 'Pull the new image and bring it up. Migrations run on boot and are forward-only — there are no down migrations, so rolling a release back is not supported and the way out of a bad upgrade is the backup you took before it.', + href: `${DOCS}/understanding/operations/`, + linkLabel: 'Operations', + }, + { + question: 'How much machine does it need?', + answer: + 'Less than you would expect for a handful of repos, and it will not stretch further than one box. Every concurrent job holds its own checkout and its own log, so disk scales with concurrency, and there is no cap on checkout size — size the workspace for your largest repo.', + href: `${DOCS}/start/configuration/`, + linkLabel: 'Configuration', + }, + { + question: 'What cleans up after itself?', + answer: + 'An hourly pass prunes expired sessions, then old job rows and their log files past the retention window. Queued and running jobs are never pruned. Nothing else is cleaned up automatically.', + href: `${DOCS}/understanding/operations/`, + linkLabel: 'Operations', + }, + { + question: 'What should the proxy watch?', + answer: + 'GET /health. A 503 means the process cannot read SQLite. Honour X-Forwarded-Proto too — session and CSRF cookies set Secure when that header says https.', + href: `${DOCS}/understanding/deployment/`, + linkLabel: 'Deployment', + }, + ]} + /> +
+
diff --git a/src/pages/use-cases/index.astro b/src/pages/use-cases/index.astro index e37b2bf..c0cb889 100644 --- a/src/pages/use-cases/index.astro +++ b/src/pages/use-cases/index.astro @@ -6,7 +6,10 @@ import Actions from '../../components/marketing/Actions.astro'; import CtaLink from '../../components/marketing/CtaLink.astro'; import Callout from '../../components/marketing/Callout.astro'; import NextSteps from '../../components/marketing/NextSteps.astro'; -import { CTA } from '../../lib/site'; +import Grid from '../../components/marketing/Grid.astro'; +import Card from '../../components/marketing/Card.astro'; +import Faq from '../../components/marketing/Faq.astro'; +import { CTA, DOCS } from '../../lib/site'; const cases = [ { @@ -82,14 +85,142 @@ const cases = [ )) }
- -

- Hosted CI for arbitrary public repos, cross-vendor pipeline portability, - and policy engines are all out of scope. See{' '} - the comparison if orchestration is - what you are shopping for. -

-
+ + +
+ + + Gated on the check suite and the immutable SHA, with one live run per + commit — so a required check behaves under force-pushes and rapid + pushes. + + + GitHub gets a conclusion and a tail. The full log is a file on your + disk, served by your instance at a details URL. + + + Three shell commands in .ci.yml — or nothing at all, if a + Node repo’s package.json already says enough. + + +
+ +
+
+ { + [ + { + who: 'Private GitHub repos', + href: '/use-cases/private-repos/', + body: 'The repository is already on GitHub and the team wants a status on the commit. Hosted Actions would do it, but it means buying minutes for work that a machine they already pay for could do, and writing workflow YAML for what is really three commands. Registering an App and ticking a repo is a smaller ask than adopting a second configuration language.', + }, + { + who: 'Self-hosted teams', + href: '/use-cases/self-hosted-teams/', + body: 'The constraint is not cost, it is custody: build logs should stay on infrastructure the team operates. A full platform would satisfy that and bring a control plane, agents, and a database server with it. One container and one file satisfies it too, and there is nothing else to keep patched.', + }, + { + who: 'Open-source contributors', + href: '/use-cases/open-source/', + body: 'Nobody should have to install a CI tool to find out how it reports a failure. The demo repository carries pull requests that land on each conclusion, from an ordinary self-hosted instance, with log pages anyone can open — proof you can click before you commit an afternoon.', + }, + ].map((item) => ( + +

+ {item.who} + +

+

{item.body}

+
+ )) + } +
+
+ +
+
+ { + [ + { + case: 'Hosted CI for arbitrary public repos', + why: 'There is no hosted openpreflight. Every instance is one someone runs, bound to repos they chose.', + go: 'Read the product page', + href: '/product/', + }, + { + case: 'One config across GitLab, Jenkins, and CircleCI', + why: '“Portable” here means the same file and the same worker on your own host — there are no vendor adapters.', + go: 'See integrations', + href: '/integrations/', + }, + { + case: 'A policy or orchestration engine', + why: 'No stages, no fan-out, no cross-repo pipelines, no approvals. Actions and its neighbours own that layer.', + go: 'Compare the layers', + href: '/compare/github-actions/', + }, + ].map((item) => ( + +

+ Not this +

+

{item.case}

+

{item.why}

+ + {item.go} + + +
+ )) + } +
+
+ +
+
+
+
+ { + [ + { + n: '01', + title: 'A self-hosted instance', + body: 'An ordinary openpreflight box with a GitHub App registered against the demo repository. Nothing special is running.', + }, + { + n: '02', + title: 'One binding, logs shareable', + body: 'Shareable logs are a per-binding opt-in, switched on for the demo repository only. Every other binding stays behind a session.', + }, + { + n: '03', + title: 'Six branches, six conclusions', + body: 'The pull requests are shaped so that pass, failure, timeout, skip, and a container runtime each show up as themselves.', + }, + ].map((item) => ( +
+ {item.n} +

{item.title}

+

{item.body}

+
+ )) + } +
+ +

+ Nothing about that setup is privileged. Point your own instance at a + public repository, opt that binding into shareable logs, and you have the + same thing — visibility is read from the payload but never gates + anything. +

+
+
+ +
+
+ + A conclusion and a step table in the run’s summary — install, test, + build, with the one that failed marked and the rest reported skipped. + + + Enough output attached to the Check Run to see what broke without + leaving GitHub. + + + The details URL, served by the instance that ran the job. For this + binding it opens without an account; everywhere else it needs a session. + +
+ +

+ Two of the six pull requests fail on purpose and one times out. That is + the part worth clicking — a green check tells you very little about + whether a CI tool reports failures usefully. +

+
+
+ +
+
+ +

+ go test ./... needs no network and no credentials. The + GitHub and Coolify APIs are faked, and the clone and pipeline tests + run against a real git-http-backend server over a fixture + repository — so the interesting paths are actually exercised, not + stubbed away. +

+

+ That is what makes a bug report with a failing test the most useful + thing you can send. The whole implementation is one + internal/ tree with no plugin surface, so there is no + extension API to learn before you can read it. +

+
+ +
+ + + CONTRIBUTING.md + + Repos and licences + +
+

@@ -113,6 +212,14 @@ const panelSteps = [ once retention prunes the job — the pull request always stays. Until the demo App is bound, run URLs are left empty rather than invented.

+

+ For the same reason the project is honest about not dogfooding: + openpreflight/openpreflight runs GitHub Actions. Dogfooding + needs a permanently reachable HTTPS instance and an App registered + against the org, and releases have to publish multi-arch images — which + this tool does not do at all. It reports a check; it does not ship + artifacts. +

diff --git a/src/pages/use-cases/private-repos.astro b/src/pages/use-cases/private-repos.astro index 0ddc6f7..b54afcb 100644 --- a/src/pages/use-cases/private-repos.astro +++ b/src/pages/use-cases/private-repos.astro @@ -8,7 +8,10 @@ import Steps from '../../components/marketing/Steps.astro'; import CheckRunPanel from '../../components/marketing/CheckRunPanel.astro'; import Actions from '../../components/marketing/Actions.astro'; import CtaLink from '../../components/marketing/CtaLink.astro'; +import Callout from '../../components/marketing/Callout.astro'; import NextSteps from '../../components/marketing/NextSteps.astro'; +import Faq from '../../components/marketing/Faq.astro'; +import MarkList from '../../components/marketing/MarkList.astro'; import { CTA, DOCS } from '../../lib/site'; const panelSteps = [ @@ -81,6 +84,113 @@ const steps = [ +
+
+
+

It can

+ +
+
+

It cannot

+ +
+
+ +

+ A short-lived installation token is handed to git through + GIT_CONFIG_*, never in the remote URL, .git/config, + or a command line — and the remote is removed before your first command + runs. Details on /security. +

+
+
+ +
+
+
+

+ One job produces one Check Run, and its steps are rendered as a table + inside that run rather than as separate checks. That is what makes it + usable as a required status: there is exactly one entry to tick in + branch protection, and adding a step to your pipeline later never + breaks the rule. +

+

+ Holding to one live run per commit matters here too. Without it, a + force-push or a fast second commit can leave two runs of the same name + racing, and branch protection reads whichever finished last. +

+
+ +

+ Pick the check name before you rely on it. GitHub + matches a required status check by name, so renaming one strands the + branch protection rule that referenced the old name. New installs + choose it once; an existing instance keeps the name it already has for + exactly this reason. +

+
+
+ + ADR 005 + Check name settings + +
+ +
+ release/* prefix. Do path filtering inside your own test command.', + href: `${DOCS}/start/faq/`, + linkLabel: 'FAQ', + }, + { + question: 'What about pull requests from forks?', + answer: + 'Skipped by default. Opting in requires a reachable Docker engine and a default runtime, and those jobs then always run in a container rather than as a process on your host.', + href: `${DOCS}/adr/004-docker-executor/`, + linkLabel: 'ADR 004', + }, + { + question: 'Who can read the logs?', + answer: + 'Anyone with a session on your instance. A single binding can opt into shareable log pages if you want to hand a link to someone without an account — treat that link as a secret.', + href: `${DOCS}/using/logs/`, + linkLabel: 'Logs', + }, + ]} + /> +
+
diff --git a/src/pages/use-cases/self-hosted-teams.astro b/src/pages/use-cases/self-hosted-teams.astro index 24a8730..568697c 100644 --- a/src/pages/use-cases/self-hosted-teams.astro +++ b/src/pages/use-cases/self-hosted-teams.astro @@ -8,7 +8,9 @@ import Actions from '../../components/marketing/Actions.astro'; import CtaLink from '../../components/marketing/CtaLink.astro'; import Callout from '../../components/marketing/Callout.astro'; import MarkList from '../../components/marketing/MarkList.astro'; +import Steps from '../../components/marketing/Steps.astro'; import NextSteps from '../../components/marketing/NextSteps.astro'; +import Faq from '../../components/marketing/Faq.astro'; import { CTA, DOCS } from '../../lib/site'; const stack = [ @@ -62,6 +64,113 @@ const stack = [
+
+ CI_SECRET_KEY, and a volume that persists. The image pulls; there is nothing to clone.', + }, + { + title: 'Put it behind your proxy', + detail: + 'GitHub has to reach POST /webhook/{slug} over HTTPS. Point the domain at port 8080 and let the proxy forward X-Forwarded-Proto so cookies come back Secure.', + }, + { + title: 'Register a GitHub App and paste it in', + detail: + 'Name, slug, App ID, webhook secret, PEM. The secret columns are encrypted on the way in, and reads give back a redacted marker.', + }, + { + title: 'Tick the repos', + detail: + 'The bindings screen is the allow-list. Nothing runs for a repo you did not enable, however valid the webhook signature.', + }, + { + title: 'Push', + detail: + 'The commit collects a Check Run whose details URL points back at your box. From here it is ordinary infrastructure.', + }, + ]} + /> + +

+ Exact commands, mounts, and the reverse-proxy notes are in the{' '} + quickstart and{' '} + deployment pages, which + track the code. This page is only the shape of it. +

+
+
+ +
+ test and build need, plus a small idle Go process. The worker itself is not the expensive part — your pipeline is.', + }, + { + question: 'How much disk?', + answer: + 'Every concurrent job holds its own checkout and its own log, so disk scales with concurrency. There is no cap on checkout size: size the workspace volume for your largest repository times however many jobs you allow at once.', + href: `${DOCS}/start/configuration/`, + linkLabel: 'Configuration', + }, + { + question: 'How much history does it keep?', + answer: + 'A retention window you set. An hourly pass deletes job rows and their log files past it — queued and running jobs are never pruned. History is finite by design.', + href: `${DOCS}/understanding/operations/`, + linkLabel: 'Operations', + }, + { + question: 'What does a deploy cost?', + answer: + 'A brief outage and any job that was mid-run. On a clean stop that job records itself cancelled and is not retried; deploy when the queue is empty if you care.', + href: `${DOCS}/understanding/operations/`, + linkLabel: 'Operations', + }, + { + question: 'Can jobs run on a different machine?', + answer: + 'Point CI_DOCKER_HOST at another Docker engine and runtime: jobs execute there. That is Docker’s remote API — it is still one worker deciding what runs, not a fleet.', + href: `${DOCS}/understanding/deployment/`, + linkLabel: 'Deployment', + }, + ]} + /> +
+ +
+
+ + There are no teams, no roles, and no SSO. One account configures Apps, + bindings, and settings — so decide who holds it the way you would decide + who holds a server credential. + + + The JSON API takes a token from a login call, so a deploy script or a + chat bot can list, re-run, and cancel jobs without a browser session. + + + Reading a run needs a session — unless that binding opted into shareable + logs, which turns its log pages into anyone-with-the-link URLs. + +
+ +

+ This is a genuine limit, not an oversight, and it is listed with the + others on why this shape. If your team needs + per-person access to CI configuration, that is a reason to pick + something larger. +

+
+
+
diff --git a/src/pages/why.astro b/src/pages/why.astro index d2d8dbd..63b4247 100644 --- a/src/pages/why.astro +++ b/src/pages/why.astro @@ -9,6 +9,7 @@ import Actions from '../components/marketing/Actions.astro'; import CtaLink from '../components/marketing/CtaLink.astro'; import Prose from '../components/marketing/Prose.astro'; import Callout from '../components/marketing/Callout.astro'; +import Faq from '../components/marketing/Faq.astro'; import NextSteps from '../components/marketing/NextSteps.astro'; import { CTA, DOCS, REPO, RELEASE } from '../lib/site'; @@ -57,6 +58,78 @@ const ceiling = [ 'Caches', 'Artifacts', ]; + +const borrowed = [ + 'Gate on the commit, not the push', + 'Queue work against an immutable SHA', + 'Attach logs to the run', + 'Write the result back to the forge', +]; + +const rejected = [ + 'ZooKeeper for coordination', + 'Nodepool for node lifecycle', + 'Ansible as the execution layer', + 'A scheduler separate from its executors', +]; + +const strengths = [ + { + title: 'Operationally small', + body: 'One container, one SQLite file, one process. No broker, no agent registration, no database server.', + }, + { + title: 'Native Check Runs', + body: 'Not a status API shim. Gating on the check suite is what makes required checks behave under force-pushes and rapid pushes.', + }, + { + title: 'Configured in a UI', + body: 'Apps and repo bindings are rows you edit, not a block of environment variables per installation.', + }, + { + title: 'Secrets encrypted at rest', + body: 'PEMs, webhook secrets, and Coolify tokens are AES-256-GCM columns.', + }, + { + title: 'Small enough to audit', + body: 'The whole implementation is one internal/ tree, with no plugin surface.', + }, +]; + +const weaknesses = [ + { + title: 'One machine', + body: 'There is no agent protocol. Jobs run in the process, or in a sibling container on the same Docker engine. This does not scale horizontally.', + }, + { + title: 'One job at a time, by default', + body: 'max_concurrent_jobs is 1 and can only be raised after first boot. Raising it adds concurrency on this one machine, not across hosts.', + }, + { + title: 'Three steps, fixed order', + body: 'install, test, build. No stages, no needs:, no fan-out, no conditional steps.', + }, + { + title: 'No caches, no artifacts', + body: 'Every job is a fresh shallow clone. Nothing carries between runs or moves to a later step.', + }, + { + title: 'No matrices', + body: 'One pipeline per commit, not one per version combination.', + }, + { + title: 'GitHub only', + body: 'It is built on Check Runs, which no other forge has.', + }, + { + title: 'One admin user', + body: 'No teams, no roles, no SSO.', + }, + { + title: 'Monorepos are the worst case', + body: 'There is no path filter. A commit touching one directory runs the same three commands as a commit touching all of them.', + }, +]; ---
+
+
+
+

Borrowed

+ +
+
+

Not adopted

+ +
+
+ +

+ Trigger on check_suite and check_run, never on + push or pull_request. A suite is already scoped + to one commit and one App; push would fire for refs nobody + is reviewing. One Check Run per job means one required-status entry in + branch protection, so adding a step never breaks it. +

+

+ ADR 005 records what + is borrowed, what is rejected, and where the ceiling is. +

+
+
+
@@ -143,21 +246,94 @@ const ceiling = [
+
+ + {strengths.map((item) => ( + + + + ))} + +
+
- +
+ {weaknesses.map((item) => ( +
+

{item.title}

+

+

+ ))} +
+

+ Not in v1 at all +

+

Jobs on another machine use a Docker engine (CI_DOCKER_HOST), - not Coolify as a job runner. The docs comparison is - honest about which neighbour to reach for instead. + not Coolify as a job runner. The docs comparison says + which neighbour to reach for instead — Woodpecker and Drone for a real + pipeline DSL, a self-hosted actions/runner to keep your + existing workflows, Jenkins for everything else.

+
+ push case would reverse that decision rather than extend it.', + href: `${DOCS}/adr/005-check-suite-gating/`, + linkLabel: 'ADR 005', + }, + { + question: 'Why is Coolify optional?', + answer: + 'It is a deployment target and an optional repository source, not the product. Skip it and everything works the same: checks still come from an App you register, and jobs still run here or on any Docker engine you point CI_DOCKER_HOST at.', + href: `${DOCS}/setup/coolify/`, + linkLabel: 'Coolify', + }, + { + question: 'Does it work with GitHub Enterprise Server?', + answer: + 'The plumbing is there and it has not been tested against a real instance. Each App row carries an API URL and the git origin is derived from it. What is untested is everything specific to a GHE deployment.', + href: `${DOCS}/start/faq/`, + linkLabel: 'FAQ', + }, + { + question: 'Does the project use itself for CI?', + answer: + 'No — openpreflight/openpreflight runs GitHub Actions. Dogfooding needs a permanently reachable HTTPS instance and an App registered against the org, and releases have to publish multi-arch images, which this tool does not do at all.', + href: `${DOCS}/start/faq/`, + linkLabel: 'FAQ', + }, + ]} + /> +
+ Date: Sat, 29 Aug 2026 11:57:33 +0530 Subject: [PATCH 3/4] One site map for nav and footer; stop repeating sections across pages Two problems: no page listed the whole site, and the same facts were being restated on up to eight pages. Navigation - `siteSections` in src/lib/site.ts is now the single source of truth. The header nav, the footer, and the 404 all render it, so a route cannot exist without being reachable from every page. - The header gains grouped menus (Product, Why, Trust) with a description per link, and the mobile sheet lists every group rather than four flat links. - The footer carries all 14 routes plus the reference links, in four columns. It is static HTML, which is what makes it the fallback for the JS-driven menus. De-duplication - Removed the "Keep reading" block from all 15 pages, and deleted the component. It was the same section fifteen times, doing a job the nav and footer now do properly. - Gave every repeated fact one owner and left a link everywhere else: concurrency and sizing, restarts, backups -> /self-hosted; the bindings allow-list, clone credentials, fork PRs, shareable logs, container hardening -> /security; the ceiling and the not-in-v1 list -> /why; dedup semantics -> /product; the neighbours -> /compare; branch-protection naming -> /use-cases/private-repos; the offline test suite -> /open-source. - An audit of 16 previously-repeated claims now shows none on more than two pages, and the pairs are the homepage summary plus its owner. Verified over CDP against the built output: the menus open on click and on Enter, Escape closes them, the mobile sheet carries all 14 routes, and every page's footer does too. Co-Authored-By: Claude Opus 5 (1M context) --- README.md | 6 +- src/components/blocks/footer-01.tsx | 4 +- src/components/blocks/site-header-01.tsx | 78 ++++++++--- src/components/marketing/NextSteps.astro | 42 ------ src/components/templates/saas-landing-01.tsx | 3 +- src/layouts/MarketingPage.astro | 3 +- src/lib/site.ts | 131 +++++++++++++++---- src/pages/404.astro | 48 ++++--- src/pages/compare/github-actions.astro | 60 ++------- src/pages/concepts.astro | 37 +----- src/pages/integrations/github-app.astro | 53 ++------ src/pages/integrations/index.astro | 41 +----- src/pages/open-source.astro | 35 ----- src/pages/pipeline.astro | 50 +------ src/pages/product/index.astro | 99 ++++---------- src/pages/security.astro | 44 +------ src/pages/self-hosted.astro | 35 ----- src/pages/use-cases/index.astro | 57 +------- src/pages/use-cases/open-source.astro | 99 +++++--------- src/pages/use-cases/private-repos.astro | 79 +++-------- src/pages/use-cases/self-hosted-teams.astro | 57 ++------ src/pages/why.astro | 50 +------ 22 files changed, 333 insertions(+), 778 deletions(-) delete mode 100644 src/components/marketing/NextSteps.astro diff --git a/README.md b/README.md index 71fe576..8a96760 100644 --- a/README.md +++ b/README.md @@ -40,10 +40,12 @@ npm run dev - `src/pages/404.astro`: branded not-found - `src/layouts/MarketingPage.astro`: header/footer chrome for inner pages; the pages themselves compose full-bleed sections -- `src/lib/site.ts`: nav, footer groups, CTAs +- `src/lib/site.ts`: `siteSections` is the canonical site map — the header nav, + the footer, and the 404 all render it, so a route cannot exist without being + reachable from every page - `src/components/marketing/`: the subpage system — `PageHero`, `Section`, `Card`/`Grid`, `Steps`, `MarkList`, `CodePanel`, `CheckRunPanel`, `Callout`, - `Prose`, `CtaLink`/`Actions`, `NextSteps` + `Faq`, `Prose`, `CtaLink`/`Actions` - `src/components/blocks/`, `src/components/ui/`: Rivelle blocks and primitives used by the homepage template - `src/layouts/Layout.astro`: head, meta, OG tags, header/footer chrome diff --git a/src/components/blocks/footer-01.tsx b/src/components/blocks/footer-01.tsx index 697fb09..3ae1c81 100644 --- a/src/components/blocks/footer-01.tsx +++ b/src/components/blocks/footer-01.tsx @@ -54,7 +54,7 @@ function Footer01({ {...props} >
-
+
- +

- A short-lived installation token is handed to git through - GIT_CONFIG_*, never in the remote URL, .git/config, - or a command line — and the remote is removed before your first command - runs. Details on /security. + With a short-lived installation token that never reaches your pipeline + steps. The mechanics — and everything else the worker is allowed to do — + are on security.

@@ -155,37 +153,28 @@ const steps = [ -
+
release/* style prefix, and an empty list means every branch the App sees.', + href: `${DOCS}/setup/bindings/`, + linkLabel: 'Bindings', }, { - question: 'Can I filter which paths trigger a run?', + question: 'Can two repos run different commands?', answer: - 'No. A binding matches a repo and optionally a branch list — exact names, or a release/* prefix. Do path filtering inside your own test command.', - href: `${DOCS}/start/faq/`, - linkLabel: 'FAQ', + 'Yes. Each repo can carry its own .ci.yml, or the binding can override install, test, and build for a repo you would rather not add a file to.', + href: '/pipeline/', + linkLabel: 'Pipeline', }, { - question: 'What about pull requests from forks?', + question: 'How do I try it on one repo first?', answer: - 'Skipped by default. Opting in requires a reachable Docker engine and a default runtime, and those jobs then always run in a container rather than as a process on your host.', - href: `${DOCS}/adr/004-docker-executor/`, - linkLabel: 'ADR 004', - }, - { - question: 'Who can read the logs?', - answer: - 'Anyone with a session on your instance. A single binding can opt into shareable log pages if you want to hand a link to someone without an account — treat that link as a secret.', - href: `${DOCS}/using/logs/`, - linkLabel: 'Logs', + 'Enable exactly one binding. Nothing else the App is installed on will run, because a repo with no enabled binding is ignored — so a trial stays a trial.', }, ]} /> @@ -202,38 +191,4 @@ const steps = [
- diff --git a/src/pages/use-cases/self-hosted-teams.astro b/src/pages/use-cases/self-hosted-teams.astro index 568697c..c62f557 100644 --- a/src/pages/use-cases/self-hosted-teams.astro +++ b/src/pages/use-cases/self-hosted-teams.astro @@ -9,7 +9,6 @@ import CtaLink from '../../components/marketing/CtaLink.astro'; import Callout from '../../components/marketing/Callout.astro'; import MarkList from '../../components/marketing/MarkList.astro'; import Steps from '../../components/marketing/Steps.astro'; -import NextSteps from '../../components/marketing/NextSteps.astro'; import Faq from '../../components/marketing/Faq.astro'; import { CTA, DOCS } from '../../lib/site'; @@ -85,7 +84,7 @@ const stack = [ { title: 'Tick the repos', detail: - 'The bindings screen is the allow-list. Nothing runs for a repo you did not enable, however valid the webhook signature.', + 'Nothing runs for a repo you did not enable — see security for why that is the trust boundary.', }, { title: 'Push', @@ -116,9 +115,7 @@ const stack = [ { question: 'How much disk?', answer: - 'Every concurrent job holds its own checkout and its own log, so disk scales with concurrency. There is no cap on checkout size: size the workspace volume for your largest repository times however many jobs you allow at once.', - href: `${DOCS}/start/configuration/`, - linkLabel: 'Configuration', + 'Enough for your largest checkout, times however many jobs you allow at once, plus the logs you keep. Self-hosted has the sizing rule.', }, { question: 'How much history does it keep?', @@ -130,9 +127,7 @@ const stack = [ { question: 'What does a deploy cost?', answer: - 'A brief outage and any job that was mid-run. On a clean stop that job records itself cancelled and is not retried; deploy when the queue is empty if you care.', - href: `${DOCS}/understanding/operations/`, - linkLabel: 'Operations', + 'A brief outage and whatever was mid-run. Self-hosted covers exactly how an interrupted job is recorded.', }, { question: 'Can jobs run on a different machine?', @@ -157,8 +152,8 @@ const stack = [ chat bot can list, re-run, and cancel jobs without a browser session. - Reading a run needs a session — unless that binding opted into shareable - logs, which turns its log pages into anyone-with-the-link URLs. + Reading a run needs an account on your instance, with one per-repo + exception you can switch on. Security covers it.
@@ -180,47 +175,13 @@ const stack = [ Reverse proxy, volumes, and the environment reference. - +

- Coolify can inventory servers and install the worker for you. It is - optional, and it does not execute jobs — see{' '} - integrations for where it actually sits. + It can inventory your servers and install the worker for you.{' '} + Integrations covers what it does and does + not do here.

-
diff --git a/src/pages/why.astro b/src/pages/why.astro index 63b4247..b0437fb 100644 --- a/src/pages/why.astro +++ b/src/pages/why.astro @@ -10,7 +10,6 @@ import CtaLink from '../components/marketing/CtaLink.astro'; import Prose from '../components/marketing/Prose.astro'; import Callout from '../components/marketing/Callout.astro'; import Faq from '../components/marketing/Faq.astro'; -import NextSteps from '../components/marketing/NextSteps.astro'; import { CTA, DOCS, REPO, RELEASE } from '../lib/site'; const positions = [ @@ -282,11 +281,9 @@ const weaknesses = [

- Jobs on another machine use a Docker engine (CI_DOCKER_HOST), - not Coolify as a job runner. The docs comparison says - which neighbour to reach for instead — Woodpecker and Drone for a real - pipeline DSL, a self-hosted actions/runner to keep your - existing workflows, Jenkins for everything else. + If one of these is a dealbreaker, that is useful information rather than + a dead end — the comparison works + through which tool to reach for instead.

@@ -295,13 +292,6 @@ const weaknesses = [ - From 5c137b01c34bc7df643efc268b0d330ad522fe8e Mon Sep 17 00:00:00 2001 From: trivedi-vatsal Date: Sat, 29 Aug 2026 14:05:07 +0530 Subject: [PATCH 4/4] Finish the subpage redesign on top of the v1.0.0 copy. The branch is rebased onto main so the tagged release is stated, and the dogfooding FAQs match the live demo instance. Co-authored-by: Cursor --- src/pages/open-source.astro | 4 ++-- src/pages/product/index.astro | 16 +++++++++------- src/pages/why.astro | 11 +++++++++-- 3 files changed, 20 insertions(+), 11 deletions(-) diff --git a/src/pages/open-source.astro b/src/pages/open-source.astro index 0da4f8b..4cb2440 100644 --- a/src/pages/open-source.astro +++ b/src/pages/open-source.astro @@ -10,7 +10,7 @@ import Callout from '../components/marketing/Callout.astro'; import CodePanel from '../components/marketing/CodePanel.astro'; import Prose from '../components/marketing/Prose.astro'; import Faq from '../components/marketing/Faq.astro'; -import { CTA, DOCS, DOCS_REPO, REPO, WEBSITE_REPO, RELEASE } from '../lib/site'; +import { DOCS, DOCS_REPO, REPO, WEBSITE_REPO, RELEASE } from '../lib/site'; const repos = [ { @@ -220,7 +220,7 @@ examples/ a sample .ci.yml { question: 'Does the project use itself for CI?', answer: - 'No — it runs GitHub Actions, and says so. Dogfooding needs a permanently reachable HTTPS instance and an App registered against the org, and releases publish multi-arch images, which this tool does not do at all.', + 'No — it runs GitHub Actions, and says so. A public demo instance at ci.openpreflight.xyz runs Check Runs for openpreflight/demo; that is not CI for this repository. Releases publish multi-arch images, which this tool does not do.', href: `${DOCS}/start/faq/`, linkLabel: 'FAQ', }, diff --git a/src/pages/product/index.astro b/src/pages/product/index.astro index b8f5382..09df711 100644 --- a/src/pages/product/index.astro +++ b/src/pages/product/index.astro @@ -136,19 +136,21 @@ const outbound = [ steps={panelSteps} /> -

- v1.0.0 is the tagged v1 release.{' '} - GitHub Release - {' · '} - Changelog. -

-
+ +

+ Tagged 29 August 2026.{' '} + GitHub Release + {' · '} + Changelog. +

+
+

Use it when

diff --git a/src/pages/why.astro b/src/pages/why.astro index b0437fb..763fa82 100644 --- a/src/pages/why.astro +++ b/src/pages/why.astro @@ -10,7 +10,7 @@ import CtaLink from '../components/marketing/CtaLink.astro'; import Prose from '../components/marketing/Prose.astro'; import Callout from '../components/marketing/Callout.astro'; import Faq from '../components/marketing/Faq.astro'; -import { CTA, DOCS, REPO, RELEASE } from '../lib/site'; +import { DOCS, REPO, RELEASE } from '../lib/site'; const positions = [ { @@ -292,6 +292,13 @@ const weaknesses = [ v1.0.0 was tagged 29 August 2026. Linux binaries are on the GitHub Release. What v1 still does not include is listed below — those things are out of scope, not unfinished.', + href: RELEASE, + linkLabel: 'GitHub Release', + }, { question: 'Why gate on the check suite instead of push?', answer: @@ -316,7 +323,7 @@ const weaknesses = [ { question: 'Does the project use itself for CI?', answer: - 'No — openpreflight/openpreflight runs GitHub Actions. Dogfooding needs a permanently reachable HTTPS instance and an App registered against the org, and releases have to publish multi-arch images, which this tool does not do at all.', + 'No — openpreflight/openpreflight runs GitHub Actions. A public demo instance runs Check Runs for openpreflight/demo; that is not CI for the product repo. Releases have to publish multi-arch images, which this tool does not do.', href: `${DOCS}/start/faq/`, linkLabel: 'FAQ', },