diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index 2a47c02..d5b6528 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -12,7 +12,6 @@ on: - "static/**" - "README.md" - "CONTRIBUTING.md" - - "docs/RELEASE.md" pull_request: branches: - main @@ -24,7 +23,6 @@ on: - "static/**" - "README.md" - "CONTRIBUTING.md" - - "docs/RELEASE.md" workflow_dispatch: permissions: diff --git a/.goreleaser.yml b/.goreleaser.yml index bc1b799..fe3b226 100644 --- a/.goreleaser.yml +++ b/.goreleaser.yml @@ -55,10 +55,15 @@ checksum: algorithm: sha256 name_template: 'CHECKSUMS' -# Publishes Formula/kvs.rb to skyoo2003/homebrew-tap. The token comes from a +# Publishes Casks/kvs.rb to skyoo2003/homebrew-tap. The token comes from a # GitHub App installed only on that repository: the job's own GITHUB_TOKEN # cannot write to another repo. -brews: +# +# A cask rather than a formula because GoReleaser deprecated `brews`: a formula that installs a +# pre-built binary is what a cask is for. `brew install skyoo2003/tap/kvs` keeps working, and the +# tap needs a tap_migrations.json entry so anyone holding the old formula moves over on upgrade +# instead of being told it is gone. +homebrew_casks: - ids: - kvs repository: @@ -68,10 +73,15 @@ brews: homepage: "https://github.com/skyoo2003/kvs" description: "A key-value store you can run as a server or import as a Go module" license: "MIT" - install: | - bin.install "kvs" - test: | - assert_match version.to_s, shell_output("#{bin}/kvs -v") + # Nothing signs or notarizes these binaries, so Gatekeeper quarantines what the cask staged and + # the first run dies on a dialog rather than an error. Stripping the attribute is what makes an + # unsigned cask runnable; it is not an optional nicety. + hooks: + post: + install: | + if OS.mac? + system_command "/usr/bin/xattr", args: ["-dr", "com.apple.quarantine", "#{staged_path}/kvs"] + end dockers_v2: - images: diff --git a/CHANGELOG.md b/CHANGELOG.md index 168c28e..cca06a9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,10 +1,27 @@ -# Changelog -All notable changes to this project will be documented in this file. - -The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), -adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html), -and is generated by [Changie](https://github.com/miniscruff/changie). - +## [v1.0.0](https://github.com/skyoo2003/kvs/releases/tag/v1.0.0) - 2026-08-11 +### Added +* Redis/Valkey compatible RESP2 server on 127.0.0.1:6379, sharing one keyspace with the HTTP and gRPC APIs; it steps aside if the port is taken, holds at most 10000 connections, and bounds one transaction's queue at 64MiB so a single client cannot exhaust memory ([#232](https://github.com/skyoo2003/kvs/issues/232)) +* Lua scripting with EVAL, EVALSHA, and SCRIPT LOAD/EXISTS/FLUSH; a script runs sandboxed under one write lock, so its redis.call sequence is atomic, and gives up after 5 seconds rather than hold the store ([#232](https://github.com/skyoo2003/kvs/issues/232)) +* Keep the keyspace across restarts with `kvs serve --data-dir`, which appends every change to a log and replays it at startup ([#236](https://github.com/skyoo2003/kvs/issues/236)) +* The cjson library inside a script, so cjson.encode and cjson.decode work under EVAL instead of failing on a nil global; a JSON null decodes to cjson.null rather than nil, so it does not end the array it sits in ([#232](https://github.com/skyoo2003/kvs/issues/232)) +* Run a Raft cluster with `kvs serve --raft-addr` and `--join`; writes pass consensus, and losing the leader triggers an election instead of needing a person ([#236](https://github.com/skyoo2003/kvs/issues/236)) +* A compatibility page states what v1 promises not to break, what it leaves out, and the trust boundary kvs assumes; the exported Go surface is pinned in testdata/api-surface.txt and checked by a test, so it cannot widen or narrow unnoticed ([#244](https://github.com/skyoo2003/kvs/issues/244)) +* A data directory now carries a format version, and kvs refuses to start on one it does not recognise instead of replaying bytes another version laid out; the refusal names both versions and what to do about it, and it covers the Raft store as well as the append log ([#245](https://github.com/skyoo2003/kvs/issues/245)) +### Changed +* Container images now run 'serve' by default as UID 65534 and no longer declare a volume ([#228](https://github.com/skyoo2003/kvs/issues/228)) +* RESP SCAN, HSCAN, SSCAN, and ZSCAN now page their walk, WATCH tracks only the keys it was given, lists cost O(1) at both ends, and expired keys are reclaimed by a sampling sweep ([#232](https://github.com/skyoo2003/kvs/issues/232)) +* INFO and HELLO now report what a clustered node actually is — a follower answers role:slave and names the leader in master_host and master_port, a leader counts the others in connected_slaves, and cluster_enabled:0 says kvs is not Redis Cluster ([#236](https://github.com/skyoo2003/kvs/issues/236)) +* Homebrew installs a cask instead of a formula; `brew install skyoo2003/tap/kvs` is unchanged, and the tap carries a tap_migrations.json entry so an existing formula install moves across on upgrade. GoReleaser deprecated the formula path for pre-built binaries, and the release would have broken on a floating version of it ([#248](https://github.com/skyoo2003/kvs/issues/248)) +### Removed +* Drop the unused data structure packages pkg/bitset, pkg/cuckoofilter, pkg/lsm, and pkg/rbt; the storage engine went to an append log and Raft instead, and nothing in the server or the library imported them. pkg/resp stays, since the RESP server is built on it ([#236](https://github.com/skyoo2003/kvs/issues/236)) +### Fixed +* Fix multi-arch (linux/arm64) release images and expose the gRPC port 3457 in container images ([#228](https://github.com/skyoo2003/kvs/issues/228)) +* An HTTP 405 now carries the same JSON error body as every other HTTP error, instead of an empty response the documented contract did not allow ([#244](https://github.com/skyoo2003/kvs/issues/244)) +### Documentation +* The durability and clustering page now carries measured numbers from a four hour run under load with a node stopped every thirty seconds and kept down for ten - 329,631 acknowledged writes, 111,516 of them taken while a node was gone, and none of them lost across 479 restarts - along with 51 bytes of append log per write, the Raft log a node too busy restarting never truncates, and a make soak target that reproduces all of it ([#246](https://github.com/skyoo2003/kvs/issues/246)) +* The release page now covers what one tag actually produces, the checks worth running before pushing it, and an upgrade section measured by running v0.1.1 and v1 side by side - the library, the CLI, and gRPC unchanged, an HTTP 405 now carrying a JSON body, a RESP listener appearing on loopback, and no data directory to migrate because v0.1.1 never wrote one ([#248](https://github.com/skyoo2003/kvs/issues/248)) +### Misc +* Migrate golangci-lint config to v2 and pin the linter to v2.12.2 in CI ([#229](https://github.com/skyoo2003/kvs/issues/229)) ## [v0.1.1](https://github.com/skyoo2003/kvs/releases/tag/v0.1.1) - 2026-04-20 ### Changed @@ -15,7 +32,6 @@ and is generated by [Changie](https://github.com/miniscruff/changie). ### Fixed * Fix goreleaser flag: --rm-dist → --clean for v2 compatibility - ## [v0.1.0](https://github.com/skyoo2003/kvs/releases/tag/v0.1.0) - 2026-03-14 ### Added * Implement a usable kvs module and release path ([#184](https://github.com/skyoo2003/kvs/issues/184)) diff --git a/GOVERNANCE.md b/GOVERNANCE.md index 66d3cbd..8857323 100644 --- a/GOVERNANCE.md +++ b/GOVERNANCE.md @@ -17,7 +17,7 @@ KVS follows a **BDFL (Benevolent Dictator for Life)** model: - Releases follow [Semantic Versioning](https://semver.org) - The project lead determines release scope and timing - Changelog is managed with [Changie](https://github.com/miniscruff/changie) -- See [RELEASE.md](docs/RELEASE.md) for the full release workflow +- See [the release process](https://skyoo2003.github.io/kvs/docs/release/) for the full release workflow ## Contributing diff --git a/changes/unreleased/Added-20260730-210000.yaml b/changes/unreleased/Added-20260730-210000.yaml deleted file mode 100644 index 8f37e46..0000000 --- a/changes/unreleased/Added-20260730-210000.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Added -body: Redis/Valkey compatible RESP2 server on 127.0.0.1:6379, sharing one keyspace with the HTTP and gRPC APIs; it steps aside if the port is taken, holds at most 10000 connections, and bounds one transaction's queue at 64MiB so a single client cannot exhaust memory -time: 2026-07-30T21:00:00.000000+09:00 -custom: - Issue: "232" diff --git a/changes/unreleased/Added-20260801-120000.yaml b/changes/unreleased/Added-20260801-120000.yaml deleted file mode 100644 index 7e1d747..0000000 --- a/changes/unreleased/Added-20260801-120000.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Added -body: Lua scripting with EVAL, EVALSHA, and SCRIPT LOAD/EXISTS/FLUSH; a script runs sandboxed under one write lock, so its redis.call sequence is atomic, and gives up after 5 seconds rather than hold the store -time: 2026-08-01T12:00:00.000000+09:00 -custom: - Issue: "232" diff --git a/changes/unreleased/Added-20260801-152145.yaml b/changes/unreleased/Added-20260801-152145.yaml deleted file mode 100644 index 97f3382..0000000 --- a/changes/unreleased/Added-20260801-152145.yaml +++ /dev/null @@ -1,3 +0,0 @@ -kind: Added -body: Keep the keyspace across restarts with `kvs serve --data-dir`, which appends every change to a log and replays it at startup -time: 2026-08-01T15:21:50.419127+09:00 diff --git a/changes/unreleased/Added-20260801-160000.yaml b/changes/unreleased/Added-20260801-160000.yaml deleted file mode 100644 index b65cdf4..0000000 --- a/changes/unreleased/Added-20260801-160000.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Added -body: The cjson library inside a script, so cjson.encode and cjson.decode work under EVAL instead of failing on a nil global; a JSON null decodes to cjson.null rather than nil, so it does not end the array it sits in -time: 2026-08-01T16:00:00.000000+09:00 -custom: - Issue: "232" diff --git a/changes/unreleased/Added-20260801-161504.yaml b/changes/unreleased/Added-20260801-161504.yaml deleted file mode 100644 index 2e288b8..0000000 --- a/changes/unreleased/Added-20260801-161504.yaml +++ /dev/null @@ -1,3 +0,0 @@ -kind: Added -body: Run a Raft cluster with `kvs serve --raft-addr` and `--join`; writes pass consensus, and losing the leader triggers an election instead of needing a person -time: 2026-08-01T16:15:04.621770+09:00 diff --git a/changes/unreleased/Added-20260809-000000.yaml b/changes/unreleased/Added-20260809-000000.yaml deleted file mode 100644 index 13e6eba..0000000 --- a/changes/unreleased/Added-20260809-000000.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Added -body: A compatibility page states what v1 promises not to break, what it leaves out, and the trust boundary kvs assumes; the exported Go surface is pinned in testdata/api-surface.txt and checked by a test, so it cannot widen or narrow unnoticed -time: 2026-08-09T00:00:00.000000+09:00 -custom: - Issue: "244" diff --git a/changes/unreleased/Added-20260809-120000.yaml b/changes/unreleased/Added-20260809-120000.yaml deleted file mode 100644 index ba9c112..0000000 --- a/changes/unreleased/Added-20260809-120000.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Added -body: A data directory now carries a format version, and kvs refuses to start on one it does not recognise instead of replaying bytes another version laid out; the refusal names both versions and what to do about it, and it covers the Raft store as well as the append log -time: 2026-08-09T12:00:00.000000+09:00 -custom: - Issue: "245" diff --git a/changes/unreleased/Changed-20260730-073249.yaml b/changes/unreleased/Changed-20260730-073249.yaml deleted file mode 100644 index 8132e6e..0000000 --- a/changes/unreleased/Changed-20260730-073249.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Changed -body: Container images now run 'serve' by default as UID 65534 and no longer declare a volume -time: 2026-07-30T07:32:49.636506+09:00 -custom: - Issue: "228" diff --git a/changes/unreleased/Changed-20260730-210001.yaml b/changes/unreleased/Changed-20260730-210001.yaml deleted file mode 100644 index c03a371..0000000 --- a/changes/unreleased/Changed-20260730-210001.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Changed -body: "server.RunListeners now takes the store before a Listeners struct instead of positional net.Listener arguments" -time: 2026-07-30T21:00:01.000000+09:00 -custom: - Issue: "232" diff --git a/changes/unreleased/Changed-20260730-233000.yaml b/changes/unreleased/Changed-20260730-233000.yaml deleted file mode 100644 index 2fbe700..0000000 --- a/changes/unreleased/Changed-20260730-233000.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Changed -body: RESP SCAN, HSCAN, SSCAN, and ZSCAN now page their walk, WATCH tracks only the keys it was given, lists cost O(1) at both ends, and expired keys are reclaimed by a sampling sweep -time: 2026-07-30T23:30:00.000000+09:00 -custom: - Issue: "232" diff --git a/changes/unreleased/Changed-20260802-000000.yaml b/changes/unreleased/Changed-20260802-000000.yaml deleted file mode 100644 index 9b6987f..0000000 --- a/changes/unreleased/Changed-20260802-000000.yaml +++ /dev/null @@ -1,3 +0,0 @@ -kind: Changed -body: INFO and HELLO now report what a clustered node actually is — a follower answers role:slave and names the leader in master_host and master_port, a leader counts the others in connected_slaves, and cluster_enabled:0 says kvs is not Redis Cluster -time: 2026-08-02T00:00:00.000000+09:00 diff --git a/changes/unreleased/Documentation-20260809-210000.yaml b/changes/unreleased/Documentation-20260809-210000.yaml deleted file mode 100644 index c615385..0000000 --- a/changes/unreleased/Documentation-20260809-210000.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Documentation -body: The durability and clustering page now carries measured numbers from a four hour run under load with a node stopped every thirty seconds and kept down for ten - 329,631 acknowledged writes, 111,516 of them taken while a node was gone, and none of them lost across 479 restarts - along with 51 bytes of append log per write, the Raft log a node too busy restarting never truncates, and a make soak target that reproduces all of it -time: 2026-08-09T21:00:00.000000+09:00 -custom: - Issue: "246" diff --git a/changes/unreleased/Fixed-20260730-073249.yaml b/changes/unreleased/Fixed-20260730-073249.yaml deleted file mode 100644 index f27e02c..0000000 --- a/changes/unreleased/Fixed-20260730-073249.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Fixed -body: Fix multi-arch (linux/arm64) release images and expose the gRPC port 3457 in container images -time: 2026-07-30T07:32:49.630294+09:00 -custom: - Issue: "228" diff --git a/changes/unreleased/Fixed-20260731-001220.yaml b/changes/unreleased/Fixed-20260731-001220.yaml deleted file mode 100644 index 2627bee..0000000 --- a/changes/unreleased/Fixed-20260731-001220.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Fixed -body: 'Homebrew formula updates again on release: tap publishing was dropped along with GH_PAT and now uses a short-lived GitHub App token' -time: 2026-07-31T00:12:20.85594+09:00 -custom: - Issue: "233" diff --git a/changes/unreleased/Fixed-20260803-090000.yaml b/changes/unreleased/Fixed-20260803-090000.yaml deleted file mode 100644 index 5355e03..0000000 --- a/changes/unreleased/Fixed-20260803-090000.yaml +++ /dev/null @@ -1,3 +0,0 @@ -kind: Fixed -body: Store.Snapshot and Store.Speculate now return ErrNoCodec instead of panicking on a nil dereference when no Codec has been set; an empty keyspace has nothing to encode and still succeeds, which is why the crash only showed up once a key existed -time: 2026-08-03T09:00:00.000000+09:00 diff --git a/changes/unreleased/Fixed-20260810-100000.yaml b/changes/unreleased/Fixed-20260810-100000.yaml deleted file mode 100644 index 389cef8..0000000 --- a/changes/unreleased/Fixed-20260810-100000.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Fixed -body: An HTTP 405 now carries the same JSON error body as every other HTTP error, instead of an empty response the documented contract did not allow -time: 2026-08-10T10:00:00.000000+09:00 -custom: - Issue: "244" diff --git a/changes/unreleased/Misc-20260730-080930.yaml b/changes/unreleased/Misc-20260730-080930.yaml deleted file mode 100644 index eab2cce..0000000 --- a/changes/unreleased/Misc-20260730-080930.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Misc -body: Migrate golangci-lint config to v2 and pin the linter to v2.12.2 in CI -time: 2026-07-30T08:09:30.503608+09:00 -custom: - Issue: "229" diff --git a/changes/unreleased/Removed-20260730-073249.yaml b/changes/unreleased/Removed-20260730-073249.yaml deleted file mode 100644 index c9d61ce..0000000 --- a/changes/unreleased/Removed-20260730-073249.yaml +++ /dev/null @@ -1,5 +0,0 @@ -kind: Removed -body: Drop Homebrew formula releases; the skyoo2003/tap/kvs formula is no longer updated -time: 2026-07-30T07:32:49.622709+09:00 -custom: - Issue: "228" diff --git a/changes/unreleased/Removed-20260802-001500.yaml b/changes/unreleased/Removed-20260802-001500.yaml deleted file mode 100644 index 4187330..0000000 --- a/changes/unreleased/Removed-20260802-001500.yaml +++ /dev/null @@ -1,3 +0,0 @@ -kind: Removed -body: Drop the unused data structure packages pkg/bitset, pkg/cuckoofilter, pkg/lsm, and pkg/rbt; the storage engine went to an append log and Raft instead, and nothing in the server or the library imported them. pkg/resp stays, since the RESP server is built on it -time: 2026-08-02T00:15:00.000000+09:00 diff --git a/changes/v1.0.0.md b/changes/v1.0.0.md new file mode 100644 index 0000000..fb76dd6 --- /dev/null +++ b/changes/v1.0.0.md @@ -0,0 +1,24 @@ +## [v1.0.0](https://github.com/skyoo2003/kvs/releases/tag/v1.0.0) - 2026-08-11 +### Added +* Redis/Valkey compatible RESP2 server on 127.0.0.1:6379, sharing one keyspace with the HTTP and gRPC APIs; it steps aside if the port is taken, holds at most 10000 connections, and bounds one transaction's queue at 64MiB so a single client cannot exhaust memory ([#232](https://github.com/skyoo2003/kvs/issues/232)) +* Lua scripting with EVAL, EVALSHA, and SCRIPT LOAD/EXISTS/FLUSH; a script runs sandboxed under one write lock, so its redis.call sequence is atomic, and gives up after 5 seconds rather than hold the store ([#232](https://github.com/skyoo2003/kvs/issues/232)) +* Keep the keyspace across restarts with `kvs serve --data-dir`, which appends every change to a log and replays it at startup ([#236](https://github.com/skyoo2003/kvs/issues/236)) +* The cjson library inside a script, so cjson.encode and cjson.decode work under EVAL instead of failing on a nil global; a JSON null decodes to cjson.null rather than nil, so it does not end the array it sits in ([#232](https://github.com/skyoo2003/kvs/issues/232)) +* Run a Raft cluster with `kvs serve --raft-addr` and `--join`; writes pass consensus, and losing the leader triggers an election instead of needing a person ([#236](https://github.com/skyoo2003/kvs/issues/236)) +* A compatibility page states what v1 promises not to break, what it leaves out, and the trust boundary kvs assumes; the exported Go surface is pinned in testdata/api-surface.txt and checked by a test, so it cannot widen or narrow unnoticed ([#244](https://github.com/skyoo2003/kvs/issues/244)) +* A data directory now carries a format version, and kvs refuses to start on one it does not recognise instead of replaying bytes another version laid out; the refusal names both versions and what to do about it, and it covers the Raft store as well as the append log ([#245](https://github.com/skyoo2003/kvs/issues/245)) +### Changed +* Container images now run 'serve' by default as UID 65534 and no longer declare a volume ([#228](https://github.com/skyoo2003/kvs/issues/228)) +* RESP SCAN, HSCAN, SSCAN, and ZSCAN now page their walk, WATCH tracks only the keys it was given, lists cost O(1) at both ends, and expired keys are reclaimed by a sampling sweep ([#232](https://github.com/skyoo2003/kvs/issues/232)) +* INFO and HELLO now report what a clustered node actually is — a follower answers role:slave and names the leader in master_host and master_port, a leader counts the others in connected_slaves, and cluster_enabled:0 says kvs is not Redis Cluster ([#236](https://github.com/skyoo2003/kvs/issues/236)) +* Homebrew installs a cask instead of a formula; `brew install skyoo2003/tap/kvs` is unchanged, and the tap carries a tap_migrations.json entry so an existing formula install moves across on upgrade. GoReleaser deprecated the formula path for pre-built binaries, and the release would have broken on a floating version of it ([#248](https://github.com/skyoo2003/kvs/issues/248)) +### Removed +* Drop the unused data structure packages pkg/bitset, pkg/cuckoofilter, pkg/lsm, and pkg/rbt; the storage engine went to an append log and Raft instead, and nothing in the server or the library imported them. pkg/resp stays, since the RESP server is built on it ([#236](https://github.com/skyoo2003/kvs/issues/236)) +### Fixed +* Fix multi-arch (linux/arm64) release images and expose the gRPC port 3457 in container images ([#228](https://github.com/skyoo2003/kvs/issues/228)) +* An HTTP 405 now carries the same JSON error body as every other HTTP error, instead of an empty response the documented contract did not allow ([#244](https://github.com/skyoo2003/kvs/issues/244)) +### Documentation +* The durability and clustering page now carries measured numbers from a four hour run under load with a node stopped every thirty seconds and kept down for ten - 329,631 acknowledged writes, 111,516 of them taken while a node was gone, and none of them lost across 479 restarts - along with 51 bytes of append log per write, the Raft log a node too busy restarting never truncates, and a make soak target that reproduces all of it ([#246](https://github.com/skyoo2003/kvs/issues/246)) +* The release page now covers what one tag actually produces, the checks worth running before pushing it, and an upgrade section measured by running v0.1.1 and v1 side by side - the library, the CLI, and gRPC unchanged, an HTTP 405 now carrying a JSON body, a RESP listener appearing on loopback, and no data directory to migrate because v0.1.1 never wrote one ([#248](https://github.com/skyoo2003/kvs/issues/248)) +### Misc +* Migrate golangci-lint config to v2 and pin the linter to v2.12.2 in CI ([#229](https://github.com/skyoo2003/kvs/issues/229)) diff --git a/cmd/kvs/serve_test.go b/cmd/kvs/serve_test.go index 6236b62..d32256d 100644 --- a/cmd/kvs/serve_test.go +++ b/cmd/kvs/serve_test.go @@ -1,10 +1,14 @@ package main import ( + "path/filepath" + "strconv" + "strings" "testing" "github.com/spf13/viper" + "github.com/skyoo2003/kvs/internal/datadir" "github.com/skyoo2003/kvs/internal/server" ) @@ -163,3 +167,30 @@ func TestResolveServeConfigFlagsOverrideViper(t *testing.T) { t.Fatalf("resolveServeConfig() = %+v, want flag values", got) } } + +// Upgrading to a build that reads a different on-disk format is the one upgrade path kvs +// promises anything about, and the promise is a refusal that says what to do. The datadir +// package tests the refusal itself; this tests that it reaches whoever ran the command, which is +// where the sentence is actually read. Nothing binds a port: serve.go opens the store before it +// listens, so the command is over before the addresses matter. +func TestServeRefusesADataDirItCannotRead(t *testing.T) { + dir := t.TempDir() + + const foreign = "999" + if err := osWriteFile(filepath.Join(dir, datadir.FormatName), []byte(foreign+"\n")); err != nil { + t.Fatalf("write %s error = %v", datadir.FormatName, err) + } + + _, _, err := runCLI(t, "serve", "--data-dir", dir) + if err == nil { + t.Fatal("serve --data-dir on a foreign format = nil, want a refusal") + } + + // The operator has to be able to tell which directory, what it holds, and what this build + // would have read, or the message is not an upgrade instruction. + for _, want := range []string{dir, foreign, strconv.Itoa(datadir.Version)} { + if !strings.Contains(err.Error(), want) { + t.Fatalf("serve --data-dir error = %q, want it to mention %q", err, want) + } + } +} diff --git a/content/docs/release.md b/content/docs/release.md index af64dbf..9bbab2e 100644 --- a/content/docs/release.md +++ b/content/docs/release.md @@ -3,29 +3,157 @@ title: "Release Process" weight: 8 --- -The project uses [Changie](https://github.com/miniscruff/changie) for changelog management and [GoReleaser](https://goreleaser.com/) for automated releases. +A release is one tag push. Everything else — binaries, checksums, the container image, the +Homebrew cask, the GitHub release and its notes — comes out of that. The workflow that does it is +[`.github/workflows/release.yaml`][release-workflow], driven by +[Changie](https://github.com/miniscruff/changie) for the notes and +[GoReleaser](https://goreleaser.com/) for the artifacts. -## Commands +[release-workflow]: https://github.com/skyoo2003/kvs/blob/main/.github/workflows/release.yaml + +## Cutting a release + +Notes come first, because the workflow refuses to run without them. + +```sh +changie new # one fragment per user-visible change, as the change is made +changie batch v1.2.3 # fold every fragment into changes/v1.2.3.md +changie merge # prepend that file to CHANGELOG.md +``` + +`changie batch` empties `changes/unreleased/`, so **read what it produced before merging.** Two +things are worth looking for, both of which have happened here: + +- A fragment with no `Issue` renders as `[#]` — a dead link in the notes. `changie batch + v1.2.3 --dry-run` shows this without consuming anything. +- Changes that cancel out. Something added and removed again between two tags is not news to + anyone upgrading, and a release note saying both is worse than saying neither. The test is + whether someone on the previous release can observe the difference. +- The previous version's heading glued to the last bullet of the new one. `changie batch` writes + the version file without a trailing newline and `changie merge` concatenates verbatim, which + costs the release below its heading. Give the file its newline and merge again. + +Commit the batched file and the changelog, merge to `main`, then: + +```sh +git tag v1.2.3 && git push origin v1.2.3 +``` + +### Before pushing the tag + +| Check | Why | +|---|---| +| `changes/v1.2.3.md` exists on `main` | The workflow fails in two seconds without it, after the tag is already pushed | +| `make all` passes | Nothing downstream runs the tests | +| `go test -run TestPublicAPISurface .` passes | An unintended change to the exported Go surface is a broken promise — see [Compatibility](../compatibility/) | +| `goreleaser check` passes | Catches deprecated configuration before a floating GoReleaser version turns it into a failed release | +| `gh release list` has no stale draft | Release Drafter keeps a rolling draft; it does not collide with a real tag, but it lingers next to the release just cut | +| The tap still holds its formula | Deleting it before the cask exists leaves `brew install skyoo2003/tap/kvs` with nothing to resolve. The swap belongs after the release job, not before the tag — see below | + +A local rehearsal that does everything except publish: + +```sh +goreleaser release --snapshot --clean # add --skip=docker without a docker daemon +``` + +The `goreleaser` job in `.github/workflows/cd.yml` runs the same command on every pull request +touching the build, so the release path is exercised before merge rather than for the first time on +a tag. + +### Retiring the formula, once + +kvs published a Homebrew formula until v1. GoReleaser writes `Casks/kvs.rb` from the first v1 tag +onward, and the tap has to be finished by hand — GoReleaser has no migration support, and neither +its cask options nor `conflicts:` move an install that already exists. + +Do this **after** the release job has pushed the cask, in one commit, or the tap is briefly a tap +with no kvs in it: ```sh -changie new # create a new changelog fragment -changie batch {version} # batch fragments into a version file -changie merge # merge into CHANGELOG.md +gh api repos/skyoo2003/homebrew-tap/contents/tap_migrations.json -X PUT \ + -f message='Migrate kvs from formula to cask' \ + -f content="$(printf '{\n "kvs": "skyoo2003/tap"\n}\n' | base64)" +# then delete Formula/kvs.rb and the copy at the tap root ``` -## Application Release +The value is the tap, not a path to anything: Homebrew re-resolves the name there and finds the +cask. It only consults the file when the name resolves to nothing, which is why the formula has to +go in the same change — while it is there, it wins and the migration never fires. + +## What comes out + +| Artifact | Where | +|---|---| +| Archives — darwin `amd64` `arm64`, linux `386` `amd64` `arm64` `armv7`, windows `386` `amd64` `arm64` (nine) | GitHub release | +| `CHECKSUMS` — sha256 for every archive | GitHub release | +| Container image | `ghcr.io/skyoo2003/kvs`, tagged `v1.2.3-alpine`, `v1.2-alpine`, `v1-alpine`, `latest-alpine` | +| Homebrew cask | `skyoo2003/homebrew-tap`, installed with `brew install skyoo2003/tap/kvs` | +| Release notes | The GitHub release body, taken from `changes/v1.2.3.md` | + +Each archive carries the binary plus `LICENSE`, `README.md`, `CHANGELOG.md`, and +`CODE_OF_CONDUCT.md`. The binary reports the tag through `kvs version`. + +**Every container tag but the first moves.** `latest-alpine`, `v1-alpine`, and `v1.2-alpine` point +at whatever was released most recently, which is why there are no pre-release tags: publishing +`v1.0.0-rc.1` would move `latest-alpine` to a release candidate. Rehearse with `--snapshot` +instead. + +## Upgrading from v0.1.x + +Measured by running v0.1.1 and this version side by side, not inferred from the diff. + +**The Go library is unchanged.** `NewStore`, `Get`, `Put`, `Delete`, and `ErrKeyNotFound` keep the +signatures they had in v0.1.1 — the example in that release's README compiles and runs against v1 +untouched. Everything else on the package is new. [Compatibility](../compatibility/) is what +promises to keep it that way. + +**The command line is unchanged.** `kvs serve --http-addr … --grpc-addr …`, `--config`, `version`, +and `-v` all still mean what they meant. The flags added since are additions. + +**One new listener appears.** v1 serves RESP on `127.0.0.1:6379` unless told otherwise, which +v0.1.1 did not have. It is loopback-only, so nothing new is reachable from off the machine, and a +port already in use is logged and skipped rather than being fatal: + +``` +kvs: listen resp: listen tcp 127.0.0.1:6379: bind: address already in use; +RESP is off (set --resp-addr to move it, or "none" to silence this) +``` + +Pass `--resp-addr none` to not have it at all. + +**gRPC is unchanged.** `api/kvsv1/kvs.proto` has not been touched since v0.1.1. + +**HTTP changes in one place.** A `405 Method Not Allowed` now carries the same JSON error body as +every other HTTP error instead of an empty response. Status codes, paths, and the bodies of `PUT`, +`GET`, `DELETE`, and `/healthz` — including their 404s — are byte-identical to v0.1.1. + +**There is no data to migrate.** v0.1.1 had no `--data-dir`: the keyspace lived in memory and went +away with the process. Persistence arrived after it, so no released version of kvs ever wrote a +data directory. Starting v1 with `--data-dir` on an empty directory is the whole upgrade. + +Point v1 at a directory written in a format it does not know and it refuses to start, saying so +rather than replaying bytes it does not understand: + +``` +open data dir /var/lib/kvs: /var/lib/kvs is format 999 and this build understands format 1. +kvs does not convert between them: run the version that wrote it, or move the directory aside +and load the data again. +``` -1. Prepare changelog: `changie new` → `changie batch v0.x.0` → `changie merge` -2. Commit and push to `main` -3. Create and push a tag: `git tag v0.x.0 && git push origin v0.x.0` -4. GitHub Actions runs `.github/workflows/release.yaml` -5. GoReleaser builds binaries and Docker images +**Homebrew installs a cask now, not a formula.** `brew install skyoo2003/tap/kvs` is the same +command; GoReleaser deprecated the formula path for pre-built binaries. Moving an install that +already exists is the tap's job, not this repository's — see the tap row in the pre-tag checks +above. -### Artifacts +## Documentation site -- **Binaries**: darwin/linux/windows (amd64, arm64) via GitHub Releases -- **Docker**: `ghcr.io/skyoo2003/kvs:{tag}-alpine` (linux/amd64, linux/arm64) +The Hugo site at [skyoo2003.github.io/kvs](https://skyoo2003.github.io/kvs/) is published by +`.github/workflows/docs.yaml` on every push to `main` that touches `hugo.toml`, `content/`, +`layouts/`, `static/`, `README.md`, or `CONTRIBUTING.md`. Hugo builds into `public/`, which +`actions/upload-pages-artifact` and `actions/deploy-pages` publish. It needs the repository's Pages +setting to deploy from GitHub Actions. -## Documentation Site +## Further Reading -The Hugo documentation site is deployed automatically to [skyoo2003.github.io/kvs](https://skyoo2003.github.io/kvs/) on every push to `main` that touches `content/`, `layouts/`, `static/`, or documentation-related files. +- [Compatibility](../compatibility/) — what v1 promises not to break +- [Contributing](../contributing/) — development commands and the pull request flow diff --git a/docs/RELEASE.md b/docs/RELEASE.md deleted file mode 100644 index 3c77f20..0000000 --- a/docs/RELEASE.md +++ /dev/null @@ -1,68 +0,0 @@ -# Release - -This repository has two release paths: - -- tagged application releases through `.github/workflows/release.yaml` -- static documentation site releases through `.github/workflows/docs.yaml` - -## Commands - -```sh -$ changie new -$ changie batch {version} -$ changie merge -``` - -## Project Release - -Project releases are published from `.github/workflows/release.yaml` when a tag matching `v*` is pushed. - -### Preparation - -1. Create changelog entries with `changie new` -2. Batch the release notes with `changie batch {version}` -3. Merge the changelog with `changie merge` -4. Commit the release notes and create the release tag - -### Publish Flow - -1. Push a tag such as `v0.2.3` -2. GitHub Actions runs `.github/workflows/release.yaml` -3. GoReleaser builds the `kvs` binaries and archives defined in `.goreleaser.yml` -4. The workflow publishes the GitHub release artifacts and related package outputs - -### Verification - -After pushing the tag, confirm that the `Release` workflow succeeds and that the expected release artifacts are attached to the GitHub release. - -## Documentation Site Release - -The static documentation site is built with Hugo and published through GitHub Pages Actions. - -### Trigger - -The docs workflow runs on pushes to `main` when one of these paths changes: - -- `hugo.toml` -- `content/**` -- `layouts/**` -- `static/**` -- `README.md` -- `CONTRIBUTING.md` -- `docs/RELEASE.md` -- `.github/workflows/docs.yaml` - -### Publish Flow - -1. GitHub Actions runs `.github/workflows/docs.yaml` -2. Hugo builds the site into `public/` -3. `actions/upload-pages-artifact` uploads `public/` as the Pages artifact -4. `actions/deploy-pages` publishes that artifact to GitHub Pages - -### Repository Setting - -GitHub Pages must be configured to deploy from GitHub Actions. - -### Verification - -After merging a docs change to `main`, confirm that the `Docs` workflow succeeds and that the site is available at `https://skyoo2003.github.io/kvs/`