Artifact a3899bd89d71a4bd093db3d8309ab1bb9edcd67cd69feb09833f24b8e08ed94f:
- File
docs/site/content/docs/production.md
— part of check-in
[612f2e7acc]
at
2026-10-23 20:05:29
on branch main
— feat(signing): sign check-ins and horizon seeds with gpg or ssh
go-libfossil signed nothing and verified nothing, so a structural-horizon clone could not tell a seed its origin minted from one substituted on disk (draft-fossil-horizon-00 §5.2.4, HZ-5.2-5 and HZ-5.2-6), and a repository with fossil's clearsign setting on stored its check-ins unsigned.
Signing follows fossil's contract, the same one zit follows. With clearsign on (and the legacy omitsign off), every check-in a commit or branch creation stores is clear-signed by pgp-command: unset, gpg --clearsign -o, run as COMMAND OUTFILE INFILE; a command named ssh* runs as COMMAND INFILE and its detached signature is wrapped in fossil's SSH envelope. A signer that fails, or returns text that does not strip back to the manifest, abandons the commit. NoSign and --nosign store one check-in unsigned.
An origin with clearsign on signs each seed it mints. It signs only after the request is authorized, outside the request's transaction, keeps one signature per seed and command so the seed's name is stable, and re-signs a kept signature its own horizon-seed-key no longer verifies. Keeping is best effort, so a read-only mirror still serves.
A clone given its origin's key (CloneOpts.SeedKey, repo clone --seed-key, kept as horizon-seed-key) verifies every seed on clone, pull and rebuild, and falls back to the content horizon from one unsigned or signed by another key. An SSH key is verified in process: namespace fossilscm, the configured key, no SHA-1 ssh-rsa, and exactly fossil's envelope. An OpenPGP fingerprint is verified by gpg, which must report one good signature by that key over text byte-equal to what the parser reads, with nothing after the signature. A verifier that cannot run keeps the held seed and fails the sync instead of discarding it.
fossil 2.28 and zit read the signed check-ins; a check-in zit signs with the same SSH key verifies here; zit clones a signing origin and holds its seed. (user: [email protected], size: 4965) [annotate] [blame] [check-ins using] [more...]
title: Production patterns
weight: 60
The Quickstart uses log.Fatal to keep the snippets short. This page shows the patterns you'll actually want when libfossil is embedded in a service.
Error handling
Don't log.Fatal from a long-running process — return errors up to a caller that can log, retry, or fail the request.
func openRepo(path string) (*libfossil.Repo, error) {
repo, err := libfossil.Open(path)
if err != nil {
return nil, fmt.Errorf("open %s: %w", path, err)
}
return repo, nil
}
The errors you get back are wrapped Go errors; use errors.Is and errors.As against the sentinel errors exported from the libfossil package when you need to branch on a specific failure (e.g., "repo not found" vs. "schema mismatch").
Context cancellation
Sync, Pull, and Clone accept a context.Context. Pass a request-scoped context with a deadline so a stalled remote doesn't tie up your goroutine forever.
ctx, cancel := context.WithTimeout(req.Context(), 30*time.Second)
defer cancel()
if _, err := repo.Sync(ctx, transport, libfossil.SyncOpts{Pull: true}); err != nil {
return fmt.Errorf("sync: %w", err)
}
The HTTP transport honors the deadline at the network boundary; the rest of the sync state machine checks ctx.Done() between rounds.
Telemetry
Wire an observer to your Repo's operations to surface sync rounds, checkout events, and per-error events to your existing logging or metrics stack. See Extension Points for the full interface, and observer/otel for an OpenTelemetry-ready implementation that emits spans + counters.
import (
libfossil "github.com/danmestas/go-libfossil"
otelobs "github.com/danmestas/go-libfossil/observer/otel"
)
obs := otelobs.NewSyncObserver()
_, err := repo.Sync(ctx, transport, libfossil.SyncOpts{
Pull: true,
Observer: obs,
})
observer/otel is a separate Go module so the OpenTelemetry SDK doesn't leak into consumers that don't want it.
Repo handle lifetime
A *Repo wraps a SQLite database connection pool. Open it once at service startup, keep it for the lifetime of the process, and Close() it during shutdown — don't open and close per request.
type service struct {
repo *libfossil.Repo
}
func newService(path string) (*service, error) {
repo, err := libfossil.Open(path)
if err != nil {
return nil, err
}
return &service{repo: repo}, nil
}
func (s *service) Shutdown() error {
return s.repo.Close()
}
Concurrent reads through one Repo are safe — SQLite handles the locking. Writes serialize at the database level; if your service has heavy write traffic, queue write operations through a single goroutine rather than relying on lock contention to back-pressure.
Driver selection
The shipped cmd/libfossil CLI uses modernc by default; go build -tags test_ncruces ./cmd/libfossil selects ncruces.
For library embedders targeting Linux/macOS/Windows, the default modernc driver is the right choice — pure Go, static binaries, no surprises. Switch to ncruces only when you're shipping a WASI/browser binary or when you need ncruces-specific behavior. Blank-import exactly one driver:
import _ "github.com/danmestas/go-libfossil/db/driver/ncruces"
// Do not also import:
// _ "github.com/danmestas/go-libfossil/db/driver/modernc"
Drivers are mutually exclusive — a process can register exactly one. See db for the registry contract.
Signing check-ins
With the clearsign setting on, every check-in Commit, Checkout.Checkin or repo branch new stores is signed, as fossil and zit sign them. The signer is the pgp-command setting, fossil's contract:
- PGP: unset, it is
gpg --clearsign -o, run asCOMMAND OUTFILE INFILE. - SSH: a command whose name starts with
ssh, such asssh-keygen -q -Y sign -n fossilscm -f ~/.ssh/id_ed25519, is run asCOMMAND INFILEand its detached signature is wrapped in fossil's SSH envelope.
libfossil -R repo.fossil repo config set clearsign on
libfossil -R repo.fossil repo config set pgp-command "ssh-keygen -q -Y sign -n fossilscm -f ~/.ssh/id_ed25519"
The signed text is the artifact, so a signed check-in is named by the hash of the signed bytes; fossil 2.26 and later, and zit, read it. A signer that fails abandons the commit. CommitOpts.NoSign, CheckoutCommitOpts.NoSign and --nosign store one check-in unsigned. The command runs through the shell, so ~ and environment variables expand, and it runs inside the commit's transaction: a signer that waits on a passphrase prompt holds the repository's write lock until it returns or two minutes pass.
The same settings sign the seeds a structural-horizon origin mints; see Signed seeds.