gitmote

gitmote  ·  history  ·  branches

chore: bump s3lite to v0.2.0 (RoleOff removed; litestream logging quieted)

20ca686956418e45d732a5a6614817b889226545
Atanas Minev <atmin@protonmail.com> · 2026-07-18 08:57

diff --git a/cmd/gitmote/main.go b/cmd/gitmote/main.go
index 1c4b95c..a43d347 100644
--- a/cmd/gitmote/main.go
+++ b/cmd/gitmote/main.go
@@ -284,7 +284,7 @@ func newHandler(gitHandler http.Handler, ui *webui.Handler, reportAPI *ci.Report
// Exceptions that must stay up on a follower: the liveness probes (Scaleway's
// health check — gating them would deadlock a rolling deploy, since the new
// instance can't promote until the old one drains) and static assets (no
-// metadata). A nil isLeader (unreplicated dev, RoleOff) means always-leader.
+// metadata). A nil isLeader (unreplicated dev, sole writer) means always-leader.
func leaderGate(next http.Handler, isLeader func() bool) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if alwaysServable(r.URL.Path) || isLeader == nil || isLeader() {
@@ -325,8 +325,8 @@ func buildGitHandler(ctx context.Context, logger *slog.Logger) (http.Handler, *w
// RoleAuto: this instance becomes the writer when it can acquire the lease,
// otherwise a read-only follower — so a rolling deploy's brief old+new overlap
// is one writer + one follower, never two writers (safety.md §1). With no
- // replica configured this is RoleOff (always writer): tests and local dev
- // unchanged.
+ // replica configured the role is moot and the instance is the sole writer:
+ // tests and local dev unchanged.
md, err := meta.Open(ctx, metaConfigFromEnv(logger, s3lite.RoleAuto))
if err != nil {
return nil, nil, nil, nil, noop, fmt.Errorf("metadata: %w", err)
@@ -556,7 +556,9 @@ func besideExe(name string) string {
//
// role selects single-writer coordination and is applied only when a replica is
// configured — there is nothing to coordinate on without a shared WAL, so an
-// unreplicated database stays RoleOff (always writer), keeping tests unchanged.
+// unreplicated database is left unset (RoleAuto) and runs as the sole writer,
+// keeping tests unchanged. Leaving it unset also avoids handing s3lite a
+// coordinating role (e.g. bootstrap's RoleWriter) with no s3:// replica to lease.
func metaConfigFromEnv(logger *slog.Logger, role s3lite.Role) meta.Config {
replica := replicaTarget()
cfg := meta.Config{
diff --git a/cmd/gitmote/main_test.go b/cmd/gitmote/main_test.go
index 113c9d1..8947dd5 100644
--- a/cmd/gitmote/main_test.go
+++ b/cmd/gitmote/main_test.go
@@ -273,7 +273,7 @@ func TestRunBootstrapDefaultsHandleAndReissues(t *testing.T) {
func TestMaybeAutoBootstrapCreatesAdminOnceOnFreshLeader(t *testing.T) {
ctx := context.Background()
- md := openMeta(t) // local-only → RoleOff → always leader
+ md := openMeta(t) // local-only → sole writer → always leader
logger := slog.New(slog.DiscardHandler)
// Fresh instance: the default admin is created, no repo.
@@ -354,17 +354,18 @@ func TestMetaConfigDerivesReplicaAndRole(t *testing.T) {
}
}
-func TestMetaConfigNoBucketStaysRoleOff(t *testing.T) {
- // No bucket: nothing to coordinate on, so the database stays local-only and
- // RoleOff (always writer) — tests and ephemeral runs unchanged.
+func TestMetaConfigNoBucketLeavesRoleUnset(t *testing.T) {
+ // No bucket: nothing to coordinate on, so the role is left unset (the zero
+ // value, RoleAuto) even for a coordinating role like RoleWriter — s3lite then
+ // runs the DB local-only as the sole writer, not demanding a lease it can't get.
t.Setenv("GITMOTE_S3_BUCKET", "")
- cfg := metaConfigFromEnv(nil, s3lite.RoleAuto)
+ cfg := metaConfigFromEnv(nil, s3lite.RoleWriter)
if cfg.RestoreFrom != "" || cfg.BackupTo != "" {
t.Errorf("replica = %q / %q, want empty (no bucket)", cfg.RestoreFrom, cfg.BackupTo)
}
- if cfg.Role != s3lite.RoleOff {
- t.Errorf("Role = %v, want RoleOff", cfg.Role)
+ if cfg.Role != s3lite.RoleAuto {
+ t.Errorf("Role = %v, want RoleAuto (unset zero value — role not applied without a replica)", cfg.Role)
}
}
diff --git a/go.mod b/go.mod
index f1cb838..d47997d 100644
--- a/go.mod
+++ b/go.mod
@@ -4,7 +4,7 @@ go 1.26.1
require (
github.com/alecthomas/chroma/v2 v2.27.0
- github.com/atmin/s3lite v0.1.0
+ github.com/atmin/s3lite v0.2.0
github.com/aws/aws-sdk-go-v2 v1.42.1
github.com/aws/aws-sdk-go-v2/config v1.32.27
github.com/aws/aws-sdk-go-v2/feature/s3/transfermanager v0.2.13
diff --git a/go.sum b/go.sum
index ad4bffd..3f80fe2 100644
--- a/go.sum
+++ b/go.sum
@@ -32,8 +32,8 @@ github.com/alecthomas/repr v0.5.2 h1:SU73FTI9D1P5UNtvseffFSGmdNci/O6RsqzeXJtP0Qs
github.com/alecthomas/repr v0.5.2/go.mod h1:Fr0507jx4eOXV7AlPV6AVZLYrLIuIeSOWtW57eE/O/4=
github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.3.0 h1:wQlqotpyjYPjJz+Noh5bRu7Snmydk8SKC5Z6u1CR20Y=
github.com/aliyun/alibabacloud-oss-go-sdk-v2 v1.3.0/go.mod h1:FTzydeQVmR24FI0D6XWUOMKckjXehM/jgMn1xC+DA9M=
-github.com/atmin/s3lite v0.1.0 h1:RNnJWx0D7gSjZfr5OkqosV4wMi68N/bj5PY8IKPEmWg=
-github.com/atmin/s3lite v0.1.0/go.mod h1:2dzs56UfxacqgIFhOnH51vNhBuOYWrXZKd/PW1AIVmQ=
+github.com/atmin/s3lite v0.2.0 h1:p8vgtZ15SjGEC725LQy0JhawILyRx7XHTschXLVt80A=
+github.com/atmin/s3lite v0.2.0/go.mod h1:2dzs56UfxacqgIFhOnH51vNhBuOYWrXZKd/PW1AIVmQ=
github.com/aws/aws-sdk-go-v2 v1.42.1 h1:9eOTgu1z/dVtYpNZ3/8/XbbaX0x/BqE3HUzAzs6K0ek=
github.com/aws/aws-sdk-go-v2 v1.42.1/go.mod h1:5pKeft2eJj+gElQ38Jqg4ibCqh+/AK33/0X3hip7IjM=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.14 h1:3IZY0XAJquT3aHzbkHfPzy4ACPcEjVG0x87KOwtpqGY=
diff --git a/internal/meta/meta.go b/internal/meta/meta.go
index 7f04ef4..e74368f 100644
--- a/internal/meta/meta.go
+++ b/internal/meta/meta.go
@@ -37,12 +37,13 @@ type Config struct {
BackupTo string
// S3 configures s3:// replicas.
S3 s3lite.S3Config
- // Logger receives litestream's log records; nil suppresses INFO.
+ // Logger receives s3lite's lifecycle events; litestream's per-sync chatter is
+ // gated to WARN by s3lite. nil uses a WARN-only stderr logger.
Logger *slog.Logger
- // Role selects single-writer coordination via the s3lite lease. The zero
- // value (RoleOff) keeps the always-writer behaviour used by tests and local
- // unreplicated dev; it only makes sense to set a leased role when BackupTo is
- // a shared replica the instances coordinate on.
+ // Role selects single-writer coordination via the s3lite lease. The zero value
+ // (RoleAuto) coordinates by lease when BackupTo is an s3:// replica and is the
+ // sole writer otherwise (tests, local unreplicated dev). Only set a coordinating
+ // role when BackupTo is an s3:// replica the instances share.
Role s3lite.Role
// LeaseTTL is the writer-lease duration for leased roles; zero uses s3lite's
// default (30s). The holder renews at TTL/3 and stops on any renew failure.
@@ -146,7 +147,7 @@ func (m *Metadata) Close() error { return m.db.Close() }
func (m *Metadata) CloseContext(ctx context.Context) error { return m.db.CloseContext(ctx) }
// IsLeader reports whether this instance currently holds the writer lease and
-// may serve writes. Always true under RoleOff (sole writer). Gate the write
+// may serve writes. Always true for an unleased sole writer. Gate the write
// path on it: a follower must refuse pushes (see docs/architecture/safety.md).
func (m *Metadata) IsLeader() bool { return m.db.IsLeader() }