Build, test, and merge a change. Project overview: README. Test lanes in depth: testing reference.
- Install the prerequisites, then
mise install. - Build:
go build ./cmd/openstorage. - Test:
go test ./... -tags=openstorage_viewcheck -count=1 -race -timeout=30mplus the other two lanes. - Lint:
golangci-lint run. - Open a PR with a conventional title; meet the review bar.
| Tool | Why |
|---|---|
| Go ≥ 1.27.3 | go.mod toolchain; CI resolves the version from go.mod |
| git | the server shells out to it; every test uses it |
| git-lfs | LFS e2e tests drive a real git lfs client; CI fails fast without it |
| Docker | default test lane (Postgres + SeaweedFS via testcontainers) and the compose stack |
| gpg / ssh-keygen | only verify-sig ref-policy tests and manual signature dogfooding |
mise.toml pins Go, golangci-lint, and the AWS-reference Terraform.
mise's mise install fetches CI's exact versions;
installing them by hand also works.
The repository and release downloads are internal to the Coder organization, so you need GitHub credentials with repository access.
-
From source, clone with authenticated Git:
gh auth setup-git # or configure another Git credential helper for github.com git clone https://github.com/coder/openstorage.git && cd openstorage go install ./cmd/openstorage
This uses the committed
vendor/tree. Thev0.15.0tag still declares module pathgithub.com/coder/open-storage, notgithub.com/coder/openstorage(GitHub redirects do not change tags). Once a release carries the current path, you can install withGOPRIVATE='github.com/coder/*' go install github.com/coder/openstorage/cmd/openstorage@latest.GOPRIVATEfetches through authenticated Git, bypassing the public module proxy and checksum database. -
With mise, release downloads need a GitHub token: a logged-in
ghCLI (used whengithub.gh_cli_tokensis on, the default) orGITHUB_TOKEN:mise use -g github:coder/openstorage@latest
Older mise without the
github:backend:mise use -g 'ubi:coder/openstorage[exe=openstorage]@latest'.
git clone https://github.com/coder/openstorage.git && cd openstorage
go build ./cmd/openstorage && ./openstorage version
./openstorage keygen --out /tmp/cs-key # org keypair, used by both serve modes belowThe binary lands at ./openstorage, not on PATH (or go install it).
Run it one of three ways:
-
Local mode — SQLite + files; no database or object-storage service:
OPENSTORAGE_ORG=selfhost OPENSTORAGE_ORG_PUBKEYS=/tmp/cs-key.pub.pem \ ./openstorage serve --listen 127.0.0.1:8080 --data-dir /tmp/cs-data
Next: README quickstart (mint tokens, create a repo, push).
-
Production topology (Postgres + SeaweedFS) — compose runs the services;
serveruns on the host:docker compose up -d --wait OPENSTORAGE_ORG=selfhost OPENSTORAGE_ORG_PUBKEYS=/tmp/cs-key.pub.pem \ OPENSTORAGE_PG_URL="postgres://postgres:postgres@127.0.0.1:${PGPORT:-15432}/postgres" \ OPENSTORAGE_S3_URL="s3://codestorage?endpoint=http://127.0.0.1:${S3PORT:-18333}&s3ForcePathStyle=true®ion=us-east-1" \ AWS_ACCESS_KEY_ID=codestorage AWS_SECRET_ACCESS_KEY=codestorage \ ./openstorage serve --listen 127.0.0.1:8080
The opt-in
replicasprofile adds two server containers (compose guide). AllOPENSTORAGE_*variables: configuration reference. -
S3-only storage (experimental) — one SeaweedFS bucket holds everything durable (
s3wal://refs + metadata,s3://blobs). It must exist before boot.docker run -d --name cs-s3 -p 127.0.0.1:8333:8333 -v cs-s3-data:/data chrislusf/seaweedfs:4.51@sha256:f7a49e3f3972ebed5be2ed4befa4a0c68eb7bdddba6e692d42729ceb545134b8 server -s3 -dir=/data -ip.bind=0.0.0.0 until curl -sf http://127.0.0.1:8333/healthz; do sleep 1; done # Cold start: bucket creation and object writes can both precede a writable volume, # so retry create + a real write roundtrip as one unit (create tolerates already-exists). until curl -s -X PUT http://127.0.0.1:8333/openstorage >/dev/null && curl -sf -X PUT -d ready http://127.0.0.1:8333/openstorage/ready && curl -sf -X DELETE http://127.0.0.1:8333/openstorage/ready; do sleep 1; done OPENSTORAGE_ORG=selfhost OPENSTORAGE_ORG_PUBKEYS=/tmp/cs-key.pub.pem \ OPENSTORAGE_PG_URL='s3wal://openstorage?endpoint=http://127.0.0.1:8333®ion=us-east-1' \ OPENSTORAGE_S3_URL='s3://openstorage?endpoint=http://127.0.0.1:8333&s3ForcePathStyle=true®ion=us-east-1' \ AWS_ACCESS_KEY_ID=any AWS_SECRET_ACCESS_KEY=any \ ./openstorage serve --listen 127.0.0.1:8080
This SeaweedFS is unauthenticated, so any credentials work. Next: README quickstart; limits: production guide.
CI gates every PR on these three lanes. The testing reference explains coverage and test conventions.
# 1. Default lane — the full suite on the production backends.
# Needs Docker (testcontainers boots Postgres + SeaweedFS) and git-lfs.
go test ./... -tags=openstorage_viewcheck -count=1 -race -timeout=30m
# 2. Local lane — the git e2e suite on the zero-container backends
# (SQLite + file blobs). No Docker needed.
OPENSTORAGE_TEST_BACKEND=local go test ./internal/githttp -count=1 -race -timeout=15m
# 3. s3wal lane — the same git e2e suite on the zero-infra topology: ONE
# SeaweedFS bucket carrying refs + metadata + blobs, plus lane-only drills
# (restart-from-bucket-only, dual-serve refusal). Needs Docker.
OPENSTORAGE_TEST_BACKEND=s3wal go test ./internal/githttp -tags=openstorage_viewcheck -count=1 -race -timeout=25mThe default lane also fans internal/api, internal/meta, and internal/ref
out into /pg + /sqlite subtests, and runs the s3wal store suites
(conformance on the in-memory CAS fake, qualification on SeaweedFS).
Two flags are required:
-count=1—TestMainreads the lane variable, which the test cache cannot see. Without it, switching lanes can replay the other lane's cached results (details).-race— CI runs every lane with it. Green without-raceis not green.
Iterate on a narrow scope (go test ./internal/api -run TestName -count=1);
run the full matrix at the end.
CI and mise install use the same linter pin from mise.toml:
mise install
golangci-lint runConfig: .golangci.yml (v2 schema) — default linters plus
misspell, unconvert, nolintlint, and gofmt/goimports formatters
(local prefix github.com/coder/openstorage).
Write modern Go, up to the go.mod version. JetBrains'
Modern Go Guidelines ship as
the agent skill .agents/skills/use-modern-go/;
agents load it automatically, humans run the CLI:
sh .agents/skills/use-modern-go/scripts/run-tool.sh list --file-path path/to/file.goNew code should not reintroduce patterns the
modernize
analyzer rewrites:
go run golang.org/x/tools/gopls/internal/analysis/modernize/cmd/modernize@latest -fix -test ./...PRs are squash-merged, and
release-please builds versions and the CHANGELOG from squash titles.
The PR title must be a conventional commit
subject; the pr-title check enforces it, including after title edits:
type(scope)?: summary
- The type prefix is required and must be a type from the table below.
- Package names go in the scope:
fix(githttp): ..., nevergithttp: .... release-please ignores a bare package prefix: the change still lands onmain, but gets no version bump and no CHANGELOG entry. - Only the squash title counts; branch commits can be anything.
| Type | Release effect |
|---|---|
fix |
patch release |
feat |
minor release |
feat!: / BREAKING CHANGE: |
breaking release |
perf |
patch release under "Performance Improvements"; not for refactors that leave performance unchanged |
docs, test, ci, chore, build, refactor |
no release; hidden from the CHANGELOG |
A PR merges when all of these hold:
- CI is green — every job in
ci.yml:lint(golangci-lint + docs checks),terraform(offline Terraform checks),test(default lane),test-local,test-s3wal(S3-only storage lane),image(Docker build + runtime-dependency smoke test), andorphan-sweep-fingerprintsandlane-schedule-fingerprints(the code covered by the verify-orphan-sweep and verify-lane-schedule models is unchanged), plus thepr-titlecheck. Release PRs need their heldciandpr-titleruns approved; see the release runbook. - A clean automated review round — comment
@codex review. Fix genuine errors; for out-of-scope findings, say why and resolve the thread. Every push invalidates the round: re-request on the new head. - A clean automated security round — same protocol with
@codex security review. - No unresolved review threads.
- The PR merges through the merge queue — the queue squashes it onto the
latest
mainand runs the required checks there, so the branch does not need to be up to date. Whenmainconflicts with it, mergemaininto the branch instead of force-pushing.
Fixes land red-first: the PR includes a test that fails on the pre-fix code. Verify docs claims against the code or a live run, not memory.