Building & release
Eventboat builds with the standard Go toolchain and no code generation step
in the normal path: go build ./... is the whole story for contributors.
This page covers the local build, the container image, the CI workflows,
and how releases are cut.
Prerequisites
- Go 1.25 (matches
go.mod; the CI jobs pin the same version). - Docker — only for the env-gated integration suites (the Kafka testcontainers test) and for building/running the container image.
- Nothing else: all drivers are pure Go (SQLite via
modernc.org/sqlite, Postgres via pgx, Kafka via kafka-go), so CGO is never required.
Local build and run
The example job pipeline’s sqlite source database regenerates with
go run ./examples/job-sync/seed. The WASM test guest is committed as a
built artifact (internal/wasmhost/testdata/aggregate.wasm), so plain
go test ./... needs no wasm toolchain; rebuild it with
GOOS=wasip1 GOARCH=wasm go build -buildmode=c-shared -o ../aggregate.wasm .
from internal/wasmhost/testdata/guest (CI does exactly this).
The container image
The Dockerfile is two stages:
- Build:
golang:1.25-alpine,CGO_ENABLED=0 go build -trimpath— the pure-Go dependency set is what makes the static build possible. - Runtime:
gcr.io/distroless/static-debian12:nonroot, binary at/usr/local/bin/eventboat, running asnonroot(uid 65532).
/pipelines and /data are the conventional mount points (config dir and
SQLite volume — examples/k8s/deployment.yaml mounts exactly these; .keep
placeholders carry the nonroot-owned directories through COPY and keep
unmounted smoke runs working). No CMD: a bare docker run prints the help
screen and exits 0.
Development data layout
Where the entry points put things on disk (all under --data-dir, default
data; --ephemeral replaces the SQLite store with a cached in-memory one):
| Path | Written by | Contents |
|---|---|---|
data/stores/<name>.db | every entry point via store.Owner | one SQLite store per pipeline: spool, checkpoint, source states, dead letters, job history |
data/pipelines/<name>.yaml | ops.Deploy | the deployed config — persisted verbatim, reloaded on restart and per job run |
The canonical layout is data/stores/<sanitized pipeline>.db (candidate 04):
the one-shot verbs (trigger, replay, jobs, run --config) and the
daemon (run --config-dir, mcp --http) all open the SAME per-pipeline
file, so a run history written by trigger is the one jobs list and the
daemon’s Status read. The name is sanitized to a conservative charset and
checked against the Windows reserved device names (internal/fsname, the
same rules the loader applies). The pre-candidate-04 layouts —
data/eventboat.db and data/stores/pipeline.db — are retired without
migration (beta ruling): the old files are simply no longer read.
The per-pipeline name is the store file namespace, which is why
metadata.name is validated so strictly (cfg_name_invalid).
Regenerating protocol code
pkg/pluginproto is generated from proto/eventboat/plugin/v1/plugin.proto.
Regenerate it only with the pinned protoc-gen-go toolchain the existing
stubs were built with (see the git history for the toolchain version), and
expect a matching ABI bump ripple: external plugins import the generated
stubs, so a regeneration that changes the surface means external plugins
must be rebuilt — record that in the CHANGELOG entry.
GHCR publishing
.github/workflows/docker.yml builds linux/amd64 + linux/arm64 (QEMU +
buildx; CGO off makes cross-builds plain Go cross-compilation) and pushes
ghcr.io/eventboat/eventboat:
| Trigger | Tags |
|---|---|
push to main | :main and :sha-<short> |
tag v1.2.3 | :1.2.3 (semver {{version}}) |
| pull request | build validation only, nothing pushed |
There is deliberately no moving 0.x tag while pre-1.0: a floating
0.2 must not silently point at an rc of a later patch line.
CI workflows
.github/workflows/ci.yml runs on every push to main and every PR:
- test — build, vet, rebuild the WASM guest from source, then
go test -race ./...(includes the eight invariants, the examples verify/test gate, the gRPC plugin acceptance, the CESQL TCK, the wasm tests; the env-gated integration packages skip without their env). - lint — golangci-lint v2 (v2.13.2) with the repo config
(
.golangci.yml): standard set + misspell + gofmt; exclusions are justified in the config. - kafka-integration — the real-broker suite
(
EVENTBOAT_KAFKA_TEST=1, testcontainers). - bench —
scripts/bench-gate.sh(the loose threshold gate) plus the informational Starlark-vs-WASM benchmark.
.github/workflows/soak.yml — nightly (02:30 UTC) plus manual dispatch:
the soak suite under mixed load with injected faults, duration controlled by
the workflow input (default 25m).
The docs site is published by a GitHub Pages workflow (.github/workflows/pages.yml):
it builds and deploys the site from docs/ changes — content pages and the
generator — so docs edits go live without a release. The rendered result
lives at eventboat.dev/eventboat/ (the project-pages subpath under the
organization domain).
The public docs site (eventboat.dev, the eventboat/eventboat.github.io
Hugo repo) consumes this repo’s developer guides at ITS build time: its
Pages workflow checks out eventboat/eventboat and runs
tools/sitegen -hugo-out to render docs/developer/*.md into Hugo content
under content/docs/developer/, then builds Hugo as usual — plus a daily
scheduled rebuild to pick up upstream doc changes. Nothing is copied into
that repo; this repo stays the single source of truth, so a guide edit here
is live there after the next org-site build (push-triggered or daily).
The editor story
The LSP (eventboat lsp) works with any editor; a minimal VS Code launcher
lives in examples/editors/vscode — it spawns the binary over stdio and
wires diagnostics, completion and hover through. There is no marketplace
extension; the example is the integration.
Release process
- CHANGELOG discipline: every user-visible change lands under
## UnreleasedinCHANGELOG.mdat the time of the commit (Keep a Changelog format; the repo’s entries carry the rationale, not just the diff summary). Look at the git log — commits like the Settled→Commit rename ship with their full old→new mapping in the entry. - Cutting a release: consolidate
Unreleasedinto a dated version section, tagv<semver>, push the tag. The tag triggers docker.yml, which publishes:<version>to GHCR. Pre-releases use rc suffixes following semver (v0.1.0-beta,v0.2.0-rc1in the tag history); dependency bumps or contract changes that would break plugins ride an rc first (the v0.2.0-rc1 release bumped lynx-go/commands for a contract fix). - The binary is the release artifact for non-container users:
CGO_ENABLED=0 go build -trimpath -o eventboat ./cmd/eventboaton the supported targets (linux, macOS, the BSDs, Windows); the only platform-specific code paths are the store lease’s file locks (flock/LockFileEx), and a target without a lock implementation refuses to run a pipeline loudly rather than skipping the single-writer guarantee.
tools/sitegen — the docs site generator
tools/sitegen (in progress) is the documentation site generator: a
separate Go module under tools/, so the main module’s dependency tree
stays untouched. Run it with go run ./tools/sitegen from within tools/.
It renders the Markdown under docs/ into a static site with no CDN
dependencies — no external JS or fonts — because the site must render
anywhere, including air-gapped environments. Frontmatter (title, order)
drives page titles and ordering; relative links within docs/ resolve as
site links; code blocks are plain ASCII-safe fences.