Skip to content

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

go build -o eventboat ./cmd/eventboat     # the single binary
./eventboat                               # bare invocation prints help, exit 0
./eventboat verify --config examples/linear/pipeline.yaml
./eventboat test examples
./eventboat run --config examples/linear/pipeline.yaml
./eventboat run --config my.yaml --ephemeral   # in-memory store, no ./data

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:

  1. Build: golang:1.25-alpine, CGO_ENABLED=0 go build -trimpath — the pure-Go dependency set is what makes the static build possible.
  2. Runtime: gcr.io/distroless/static-debian12:nonroot, binary at /usr/local/bin/eventboat, running as nonroot (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.

docker build -t eventboat:dev .
docker run --rm -v "$PWD/examples/linear:/work" -w /work eventboat:dev \
  run --config /work/pipeline.yaml

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):

PathWritten byContents
data/stores/<name>.dbevery entry point via store.Ownerone SQLite store per pipeline: spool, checkpoint, source states, dead letters, job history
data/pipelines/<name>.yamlops.Deploythe 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:

TriggerTags
push to main:main and :sha-<short>
tag v1.2.3:1.2.3 (semver {{version}})
pull requestbuild 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 ## Unreleased in CHANGELOG.md at 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 Unreleased into a dated version section, tag v<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-rc1 in 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/eventboat on 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.