Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
node_modules/
dist/
.astro/
graphify-out/
.env
.env.production
.DS_Store
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
/>
<img
src="https://openpreflight.xyz/banner-light.png"
alt="openpreflight — a small CI provider for private repos. One Go binary, one SQLite file, one Check Run per commit."
alt="openpreflight — A small CI provider for private repos. One Go binary, one SQLite file: register a GitHub App, enable your repos, and get one Check Run per commit."
width="880"
/>
</picture>
Expand Down
2 changes: 1 addition & 1 deletion astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export default defineConfig({
starlight({
title: 'openpreflight',
description:
'Self-hosted GitHub Check Runs CI for private repos. One Go binary, one SQLite file.',
'A small CI provider for private repos. One Go binary, one SQLite file: register a GitHub App, enable your repos, and get one Check Run per commit.',
logo: {
light: './src/assets/logo-light.svg',
dark: './src/assets/logo-dark.svg',
Expand Down
4 changes: 3 additions & 1 deletion scripts/check-links.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,13 @@ const root = join(dirname(fileURLToPath(import.meta.url)), '..');
const dist = join(root, 'dist');

const SITE_DEFAULT_DESCRIPTION =
'Self-hosted GitHub Check Runs CI for private repos. One Go binary, one SQLite file.';
'A small CI provider for private repos. One Go binary, one SQLite file: register a GitHub App, enable your repos, and get one Check Run per commit.';

const required = [
'index.html',
'start/quickstart/index.html',
'start/configuration/index.html',
'start/comparison/index.html',
'start/faq/index.html',
'setup/github-app/index.html',
'setup/coolify/index.html',
Expand All @@ -29,6 +30,7 @@ const required = [
'understanding/architecture/index.html',
'understanding/security-model/index.html',
'understanding/deployment/index.html',
'understanding/operations/index.html',
'contributing/development/index.html',
'adr/005-check-suite-gating/index.html',
'favicon.svg',
Expand Down
4 changes: 2 additions & 2 deletions src/content/docs/index.mdx
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
---
title: openpreflight
description: Self-hosted GitHub Check Runs CI for private repos. One Go binary, one SQLite file.
description: "A small CI provider for private repos. One Go binary, one SQLite file: register a GitHub App, enable your repos, and get one Check Run per commit."
template: splash
hero:
tagline: A small CI provider for private repos. One Go binary that configures itself in a web UI and runs your install/test/build on the exact commit, reporting one Check Run with full logs.
tagline: "A small CI provider for private repos. One Go binary, one SQLite file: register a GitHub App, enable your repos, and get one Check Run per commit."
actions:
- text: Quickstart
link: /start/quickstart/
Expand Down
2 changes: 1 addition & 1 deletion src/content/docs/setup/bindings.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
title: "Enable repos"
description: "Enable the private repos that should receive Check Runs; the bindings table is the allow-list."
sidebar:
order: 3
order: 2
---
**Repos** → pick the CI App, optionally pick a Coolify instance as the source of
the repo list, then check the repositories to run checks for. Unchecking a repo
Expand Down
4 changes: 2 additions & 2 deletions src/content/docs/setup/coolify.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
---
title: "Coolify"
title: "Optional: Coolify"
description: "Add Coolify as an optional deployment target and repository picker. Not required for checks to work."
sidebar:
order: 2
order: 3
---
Coolify is a supported deployment target and an optional repo source. **Not
the product, and not required**. Add a team-scoped API token and you get server
Expand Down
112 changes: 112 additions & 0 deletions src/content/docs/start/comparison.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
---
title: "Comparison"
description: "Where openpreflight sits against Woodpecker CI, Drone, self-hosted actions/runner, and Jenkins, and when to pick one of those instead."
sidebar:
order: 3
---
openpreflight is a narrow tool. It reports one Check Run per commit for private
GitHub repos, from one binary and one SQLite file, and it runs `install`,
`test`, and `build`. That is the whole product. Most projects on this page do
more, and for a lot of teams doing more is the point.

Read this before you install, not after.

## The short version

| You want | Pick |
|---|---|
| A check on the commit, on a box you already run, with as little to operate as possible | openpreflight |
| A real pipeline DSL — stages, matrices, caches, artifacts, fan-out | Woodpecker or Drone |
| The same workflows you already have in `.github/workflows/` | self-hosted `actions/runner` |
| Anything that is not GitHub, or plugins for everything | Jenkins |

## Woodpecker CI

The closest comparison, and for most people the better default. Woodpecker is a
container-native pipeline engine with a YAML DSL, matrix builds, plugins,
secrets management, multiple agents, and support for GitHub, GitLab, Gitea,
Bitbucket, and Forgejo.

**Pick Woodpecker if** you want steps that are not install/test/build, matrix
builds, artifacts between steps, more than one machine running jobs, or a forge
that is not GitHub.

**Pick openpreflight if** the server-plus-agent split is more than you want to
run for a handful of private repos, and a Check Run that says pass or fail with
a log behind it is the whole requirement. openpreflight is one container and
one file; there is no agent to register and no broker.

## Drone

The project Woodpecker forked from. Same shape: a server, one or more runners,
a container-per-step pipeline, a mature plugin ecosystem, several forges.
Licensing changed after Harness acquired it, which is worth checking against
your own constraints; Woodpecker is where much of the community went.

The trade-off against openpreflight is the same as Woodpecker's, and if you are
choosing between the two of them rather than against this, that is a decision
openpreflight has no opinion on.

## Self-hosted `actions/runner`

If your workflows are already GitHub Actions, this is usually the right answer
and openpreflight is the wrong one. You keep the YAML, the marketplace, the
matrix syntax, the caches, and the artifacts, and you change where the compute
happens. openpreflight deliberately runs none of that: it does not read
`.github/workflows/`, and it will not.

**Pick `actions/runner` if** you have Actions workflows you want to keep, need
the marketplace, or want GitHub to own the scheduling.

**Pick openpreflight if** you do not want Actions minutes involved at all, do
not want a runner registered against your org, and would rather the whole CI
surface be a thing you can read in an afternoon. The scope note in
[what is not in v1](/start/faq/) is the honest boundary.

## Jenkins

Two decades of plugins, every SCM, every language, every deployment shape.
Nothing here competes with that, and nothing here tries.

**Pick Jenkins if** you need something a plugin already solves, have build
infrastructure that is not just "run three commands in a checkout", or already
run it.

**Pick openpreflight if** you would be installing Jenkins purely to get a
green check on a private repo's pull requests, and the JVM, the plugin
upgrades, and the configuration surface are cost you would rather not carry.

## Where openpreflight is genuinely weaker

Stated plainly, because these are the things that will bite:

- **One machine.** There is no agent protocol. Jobs run in the process or in a
sibling container on the same Docker engine. Every one of the tools above
scales horizontally; this one does not.
- **One job at a time by default.** `max_concurrent_jobs` is 1 and can only be
raised after first boot. See
[Configuration](/start/configuration/).
- **Three steps.** `install`, `test`, `build`, in that order. No stages, no
`needs:`, no fan-out, no conditional steps.
- **No caches and no artifacts.** Every job is a fresh shallow clone. Nothing
is carried between runs or handed to a later step.
- **No matrices.** One pipeline per commit, not one per version combination.
- **GitHub only.** It is built on Check Runs, which no other forge has.
- **One admin user.** No teams, no roles, no SSO.
- **You register the GitHub App.** It is not created for you.

## Where it is genuinely better

- **Operationally small.** One container, one SQLite file, one process. No
broker, no agent registration, no database server. Backups are a file and a
key; see [Operations](/understanding/operations/).
- **Native Check Runs.** Not a status API shim. Runs gate on the check suite
and hold to one live run per commit, which is what makes required checks
behave under force-pushes and rapid pushes. See
[ADR 005](/adr/005-check-suite-gating/).
- **Configured in a UI, not env vars.** Apps and repo bindings are rows you
edit, not a block of environment per installation.
- **Secrets encrypted at rest.** PEMs, webhook secrets, and Coolify tokens are
AES-256-GCM columns. See [Security model](/understanding/security-model/).
- **Small enough to audit.** The whole implementation is `internal/`, with no
plugin surface.
31 changes: 28 additions & 3 deletions src/content/docs/start/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,23 @@ else is a row in SQLite, edited in the UI or over the JSON API. For a
procedural first run, see [Quickstart](/start/quickstart/). Pipeline file
semantics are also covered under [Pipelines](/using/pipelines/).

## One job at a time, by default

`max_concurrent_jobs` defaults to **1**. A fresh install runs one job at a
time; a second commit waits for the first to finish. There is no env var for
it — it is a settings row, so it can only be changed after the process is up,
under **Settings** or `PATCH /api/v1/settings`.

Size for that before you install. One binary on one box with a serial runner
suits a handful of repos that push a few times an hour. It does not suit a
busy monorepo, and it is not a scheduler: raising the number raises how many
jobs this one process runs at once, on this one machine. Jobs never spread
across hosts. Every concurrent job also holds its own checkout under
`WORKSPACE_DIR` and its own log under `DATA_DIR`, so disk scales with the
number too.

The UI caps the field at 32. The API does not.

## Environment

| Variable | Required | Purpose |
Expand All @@ -25,6 +42,12 @@ semantics are also covered under [Pipelines](/using/pipelines/).
There is no `GITHUB_APP_ID` and no `CI_ALLOWED_REPOS`. Those live in
`github_apps` and `repo_bindings`.

`DOCKER_GID` is not in the table because the process never reads it. It is a
Compose variable: it puts uid 10001 in the docker socket's group so `runtime:`
jobs and fork PRs can reach the engine. The default of `998` suits a typical
Linux docker group and does **not** work on Docker Desktop, where the socket is
gid 0 inside the container. See [Deployment](/understanding/deployment/).

Generate a key with:

```bash
Expand All @@ -47,9 +70,8 @@ Single row, `id = 1`. Changed from **Settings** in the UI or
| `default_check_name` | `openpreflight` | Check Run name unless the App or binding overrides. New installs only. An existing database keeps the name it already has, because GitHub matches a required status check by name and renaming one strands its branch protection rule |
| `default_pipeline_file` | `.ci.yml` | Path in the repo |
| `default_timeout_seconds` | `900` | Per-job timeout |
| `max_concurrent_jobs` | `1` | Runner concurrency |
| `max_log_bytes` | 10 MiB | Cap on the on-disk log |
| `max_workspace_bytes` | 1 GiB | Checkout size cap |
| `max_concurrent_jobs` | `1` | Jobs this process runs at once. See above |
| `max_log_bytes` | 10 MiB | The log stops growing at this size; the run continues |
| `log_retention_days` | `14` | Prune old logs and job rows |
| `default_runtime` | empty | Docker image used when a fork job's pipeline has no `runtime:` |
| `skip_fork_prs` | `true` | Fork PRs are ignored. Saving `false` requires Docker plus `default_runtime`. |
Expand Down Expand Up @@ -88,3 +110,6 @@ Resolution order, highest first:
3. Node defaults inferred from `package.json`: `npm ci` / `pnpm` / `yarn` by
lockfile, then `test` and `build` **only if those scripts exist**
4. nothing to run → the check is reported as **skipped**, not failed

Backups, upgrades, and what a restart does to a running job are in
[Operations](/understanding/operations/).
54 changes: 50 additions & 4 deletions src/content/docs/start/faq.md
Original file line number Diff line number Diff line change
@@ -1,12 +1,13 @@
---
title: "FAQ"
description: "Short answers to why a GitHub App is required, why Coolify is optional, why gating is on the check suite, and what v1 deliberately leaves out."
description: "Short answers on GitHub Apps, Coolify, check-suite gating, GitHub Enterprise, monorepos, dogfooding, and what v1 leaves out."
sidebar:
order: 3
order: 4
---

Positioning questions the homepage raises; each answer links the page or ADR
that argues it in full.
Positioning questions the homepage raises, and the ones people ask before
installing. Each answer links the page or ADR that argues it in full. For how
this sits against other CI, see [Comparison](/start/comparison/).

## Why a GitHub App and not OAuth?

Expand Down Expand Up @@ -44,3 +45,48 @@ engine via `CI_DOCKER_HOST`, not Coolify's API as a job runner.

See [Architecture](/understanding/architecture/) and the homepage's "What it
isn't" list.

## Does it work with GitHub Enterprise Server?

The plumbing is there and it has not been tested against a real instance.

Each App row carries an **API URL**, which defaults to
`https://api.github.com` and is the field to change for Enterprise. The git
origin is derived from it, so `https://ghe.example.com/api/v3` clones from
`https://ghe.example.com`. Nothing else in the request path assumes
github.com.

What is untested is everything specific to a GHE deployment: its certificate
chain, its API version skew, and Check Runs behaviour on older releases. If you
try it, an issue saying which version and what broke is useful.

## Does it work for monorepos?

It runs, but it will run everything on every push.

There is no path filter. A binding matches a repo and optionally a branch list
(exact names, or a `release/*` prefix), and that is the whole filter. A commit
touching one directory runs the same `install`, `test`, and `build` as a commit
touching all of them.

A plan is also exactly three steps in a fixed order, so the usual monorepo
answer — one job per affected package, in parallel — has nothing to express
itself with. Combined with `max_concurrent_jobs` defaulting to 1, a busy
monorepo is the case this is worst at. Do the path filtering inside your own
`test` command, or use a tool built for it; see
[Comparison](/start/comparison/).

## Does the project use itself for CI?

No. `openpreflight/openpreflight` runs GitHub Actions — `ci.yml` for vet, test,
and a Docker build, and `release.yml` on a `v*` tag.

Two reasons, both honest. Dogfooding needs a permanently reachable HTTPS
instance and a GitHub App registered against the org, which is infrastructure
the project does not run yet. And releases have to build multi-arch images and
attach binaries, which is not something this tool does at all — it reports a
check, it does not publish artifacts.

Nothing stops it from checking a public repo, incidentally. The repository's
visibility is read from the webhook payload but never gates anything; "private
repos" is what it is aimed at, not a restriction it enforces.
37 changes: 27 additions & 10 deletions src/content/docs/start/quickstart.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,18 +22,35 @@ App. Full environment and settings detail lives in

## Run it

`compose.prod.yaml` is standalone: it pulls the published image, so there is
nothing to clone.

```bash
export CI_SECRET_KEY="$(openssl rand -base64 48)" # required, keep it forever
export CI_PUBLIC_BASE_URL="https://ci.example.com" # optional seed
docker compose up --build
curl -O https://raw.githubusercontent.com/openpreflight/openpreflight/main/compose.prod.yaml
export CI_SECRET_KEY="$(openssl rand -base64 48)"
docker compose -f compose.prod.yaml up -d
```

`CI_SECRET_KEY` is required; the process refuses to start without it. Keep it
forever. Losing it makes stored PEMs and tokens unreadable. See
`CI_SECRET_KEY` is the only variable you must set; the process refuses to start
without it. Keep it forever. Losing it makes stored PEMs and tokens unreadable.
Everything else either has a default or is asked for in the wizard. See
[Configuration](/start/configuration/) for the full env table and key rotation.

Compose, volumes, and reverse-proxy notes are in
[Deployment](/understanding/deployment/).
Pin a version with `OPENPREFLIGHT_VERSION=1.0.0` rather than editing the file.

To build from source instead — this is the contributor path, and `compose.yaml`
is `build: .`, so it needs the checkout:

```bash
git clone https://github.com/openpreflight/openpreflight
cd openpreflight
export CI_SECRET_KEY="$(openssl rand -base64 48)"
docker compose up --build
```

If you use `runtime:` or fork PRs, you also need `DOCKER_GID`. See
[Deployment](/understanding/deployment/), which covers compose, volumes, the
docker socket, and reverse-proxy notes.

## First boot

Expand All @@ -47,10 +64,10 @@ wizard. It is ignored once a user exists.
## Setup order

1. [Register a GitHub App](/setup/github-app/) and paste it under **GitHub Apps**.
2. Optionally [add a Coolify instance](/setup/coolify/) as a repo-picker source
or to install this worker.
3. [Enable repo bindings](/setup/bindings/). The bindings table is the
2. [Enable repo bindings](/setup/bindings/). The bindings table is the
allow-list.
3. Optionally [add a Coolify instance](/setup/coolify/) as a repo-picker source
or to install this worker.

Then commit a [pipeline](/using/pipelines/) (or rely on Node defaults) and push.

Expand Down
Loading
Loading