Contributing to Homerun
This is the "I want to run this from source and change code" guide. If you just
want to run Homerun, you don't need any of this: use the installer one-liner
or compose.prod.yaml instead, see
docs/getting-started.md. Nothing here (git, Bun, a
dev server) is needed for either of those paths.
Prerequisites
- mise, then
mise installin the repo: it installs the Bun, Go and prek versions pinned inmise.toml. Without mise, install those three yourself at the same versions (Go is only needed forcmd/andinternal/, the worker, its agent mode, the CLI and the installer). golangci-lint is pinned separately, see below, mise doesn't install it. - Docker (for Traefik + Postgres, and for the containers the app itself will manage once it's running)
Toolchain (mise)
mise.toml pins the tools a checkout needs outside node_modules: Bun, Go and
prek. mise install installs them, and mise's shell activation
(eval "$(mise activate zsh)", see
mise's docs) puts those versions on
PATH inside the repo. mise ls shows what's active.
golangci-lint is pinned separately, in its own Go module (tools/go/go.mod,
kept out of the root go.mod deliberately, so its ~210 indirect dependencies
don't feed minimum-version-selection into the binaries this repo ships), and run
via go tool, not installed as a standalone binary:
go tool -modfile=tools/go/go.mod golangci-lint run ./cmd/... ./internal/... ./tests/unit/go/...
(part of bun run check and a pre-commit hook, below). Bump it with
cd tools/go && go get -tool github.com/golangci/golangci-lint/v2/cmd/golangci-lint@vX.
mise can't install Docker itself, only check it: mise run docker (also run
after every mise install) fails if no daemon is reachable or docker compose
v2 is missing, and creates the homerun network if it doesn't exist yet.
CI doesn't use mise, so the same versions are also pinned elsewhere, and a bump
has to touch every copy: Bun in package.json's packageManager and the
Dockerfile's oven/bun tags, Go in go.mod and the Dockerfile's golang
tag, prek via j178/prek-action in code_quality.yaml. Renovate updates
mise.toml along with the rest.
Setup
git clone https://github.com/orochibraru/homerun.git && cd homerun
mise install
bun install
docker network create homerun # already done by mise install
docker compose up -d # Traefik + Postgres, see compose.yaml
cp .env.example .env # set AUTH_SECRET, and ORIGIN=http://localhost:5173 for bun run dev
bun run devOpen http://localhost:5173. Migrations in drizzle/ apply themselves at boot;
bun run db:generate is only for when you change src/lib/server/db/schema.ts.
The app runs directly on the host (not in a container) so it can reach the
Docker socket without any socket-forwarding; compose.yaml only
runs Traefik and Postgres. The first account you create becomes admin
automatically; signing in for the first time drops you into the onboarding
wizard (base domain / Docker / Traefik / email).
bun run dev also runs the job worker (cmd/worker, Go), with its output
prefixed [worker]. Deploys, builds, scans, backups, cron jobs and Docker
cleanups only run while it's up. It rebuilds and restarts on every change under
cmd/ or internal/; a change that doesn't compile prints the error and leaves
the previous worker running. bun run dev --only=app is vite alone,
bun run dev --only=worker the worker alone. Without Go installed, run the
worker in Docker instead: docker compose --profile worker up -d --build worker
(rebuild it after a Go change).
bun run build && bun run start runs the built app instead of the Vite dev
server, closer to how the production Docker image runs it, still directly on the
host, still against the same compose.yaml Postgres/Traefik.
cmd/cli/ and cmd/installer/ are both Go programs (one go.mod at the repo
root, no bun install needed for either of them):
go run ./cmd/cli services list, go run ./cmd/installer --dry-run, etc.
cmd/worker is the third: go run ./cmd/worker next to a running Postgres runs
the local job worker (what bun run dev already starts for you); with no
DATABASE_URL set, the same command instead runs it in agent mode, the
standalone build-server binary that used to be cmd/agent/ before it was merged
into the worker, see cmd/worker/README.md.
Before every change: the hard gates
These are enforced by git hooks, not just CI. The hooks are run by
prek from .pre-commit-config.yaml; prek comes
from mise install, then bun install wires them up for you (prepare runs
prek install, which installs the pre-commit, commit-msg and pre-push hooks). A
commit only runs the fast, per-file hooks (oxlint, Biome format and import
sorting, Prettier and markdownlint, gofmt, Tailwind, typos, secret scanning), a
few seconds. A push runs the whole-repo gates: the type check, unit tests (80%
coverage gate), golangci-lint and the Go tests. Hooks autofix in place, so a
commit that gets rejected for "files were modified by this hook" just needs
git add and a re-commit. After pulling this change, run prek install once so
the pre-push hook exists.
bun run check # svelte-check --fail-on-warnings over src/ and tests/, tsc over scripts/, go vet and golangci-lint over every package under cmd/ and internal/, zero errors AND zero warnings
bun run lint # markdownlint-cli2, lint-tailwind.ts and oxlint --type-aware --deny-warnings, whole repoRun both after every change, not just once at the end. bun run check's scope
is already the whole repo regardless of which files you touched, so a red result
elsewhere is still your problem to look at, not something to wave off as
unrelated without actually checking. bun run check includes go vet and
golangci-lint (the same pair CI's Go job runs, golangci-lint via
go tool -modfile=tools/go/go.mod, see Toolchain above) over cmd/cli/,
cmd/installer/, cmd/worker/ and every shared internal/ library; scope
either one to a single sub-project by narrowing the path yourself (e.g.
go vet ./cmd/cli/... ./internal/cli/... ./tests/unit/go/internal/cli/...). If
you changed a REST API route or config.ts, also run bun run gen and commit
the regenerated openapi.json, homerun.schema.json and
tests/integration/support/openapi-types.ts: CI fails when they're stale.
bun run test is the fast suite (seconds, no Postgres or Docker needed): Go
tests plus the bun:test unit suite. bun run test:integration needs a real
Postgres and Docker daemon, see CLAUDE.md's "Commands" section for the full
breakdown of test/test:* scripts and .agents/notes/testing.md for what
integration and E2E need.
Two real-infrastructure suites live outside that (Multipass + Docker locally,
never in CI): bun scripts/e2e-multipass.ts drives the installer/worker/CLI
built from your working tree, and bun scripts/e2e-multipass-release.ts drives
the published release using the commands the docs themselves print. If you
touched an install instruction,
bun scripts/e2e-multipass-release.ts --only=docs is the seconds-long, VM-free
half of the latter.
Conventions
The full, detailed set of architectural and style conventions this codebase
holds itself to lives in CLAUDE.md: route-file typing rules, the
DTO layer, the OOP-vs-static-class conventions, and a long list of "real,
tested" findings from past work worth not re-discovering the hard way. Read it
before a non-trivial change; it's written for exactly this purpose (it's also
what Claude Code reads when working in this repo).
Commits
Commit messages follow
Conventional Commits (feat:, fix:,
chore:, etc.). orochibraru/releaser drives version bumps and changelog
generation from them on every push to main (.github/workflows/publish.yaml),
so a misformatted subject line isn't just a style nit, it changes what actually
ships.
PRs are squash-merged with the PR title as the commit message, so the PR
title is what counts, and CI fails a PR whose title isn't a conventional
commit. Only feat, fix, perf, refactor, docs and breaking changes
(feat!:) cut a release. Changes that only touch docs/, markdown files,
.agents/ or .claude/ don't trigger a release or image build on their own.
Releases
There's no local release command; releasing is CI-only, triggered on push to
main by orochibraru/releaser. See the "Release automation" section of
.agents/notes/packages-and-release.md
for what it does (binaries for cmd/cli/cmd/installer/cmd/worker, the
Docker image, the GitHub release).
This guide lives in the project repo: edit it there, and this page follows within a day.