feat(engine,deploy): seed-drift guard + track current dictionary release
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 54s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m12s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 10s
CI / integration (pull_request) Successful in 14s
CI / ui (pull_request) Successful in 54s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m12s
The dictionary release moved to v1.2.1 while DICT_VERSION stayed pinned at v1.0.0 in CI and the image/compose seed defaults. Two problems: 1. CI validated against a stale dictionary. 2. The contour seed could be bumped on a live volume, which silently relabels the already-seeded bytes — voiding games pinned to the prior label and serving the wrong dictionary for new ones. The flat DAWGs carry no embedded version, so this drift was undetectable. Changes: - Seed-drift guard: OpenWithVersions records the flat dir's version in a .seed_version marker on first boot and refuses to start when a later BACKEND_DICT_VERSION disagrees. DICT_VERSION is now the seed for a *fresh* volume only; a live contour migrates through the admin console (old versions stay resident, in-progress games keep replaying). - Track the current release: CI's DICT_VERSION centralised to one workflow-level env (v1.2.1); image/compose/.env seed defaults bumped to v1.2.1. The deploy job keeps reading the per-contour vars.TEST_DICT_VERSION. - Docs: ARCHITECTURE §5 (decision record), backend/deploy READMEs, PRERELEASE tracker (DV row). Verified locally against the v1.2.1 artifact: gofmt, build, vet, unit and integration (-tags=integration) all green.
This commit is contained in:
@@ -0,0 +1,52 @@
|
||||
package engine
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// seedMarkerFile names the file, in the flat dictionary directory, that records the
|
||||
// version the directory was first seeded as. It is dot-prefixed so OpenWithVersions'
|
||||
// version scan skips it (like the .staging upload area).
|
||||
const seedMarkerFile = ".seed_version"
|
||||
|
||||
// checkSeedMarker reconciles the flat dictionary directory's recorded seed version
|
||||
// with bootVersion, guarding the seed-drift footgun.
|
||||
//
|
||||
// The contour's dictionary lives on a named volume seeded from the image once and
|
||||
// never re-seeded (deploy/docker-compose.yml). The flat directory's DAWG files carry
|
||||
// no embedded version, so their version is only the label BACKEND_DICT_VERSION gives
|
||||
// them. Bumping the build seed on a live volume would therefore relabel the already
|
||||
// seeded bytes: games that pinned the old label become unreplayable (voided) and new
|
||||
// games would silently use the wrong dictionary. checkSeedMarker records the seed
|
||||
// version on a fresh directory and, on every later boot, returns an error when
|
||||
// bootVersion no longer matches the recorded seed — so an operator changes a live
|
||||
// dictionary by uploading the new release through the admin console, or wipes the
|
||||
// volume to re-seed, never by bumping the build seed (docs/ARCHITECTURE.md §5).
|
||||
//
|
||||
// A directory that cannot be written makes the first record fail; that already breaks
|
||||
// the admin console (which writes version subdirectories here), so the error is
|
||||
// returned rather than swallowed, matching the package's fail-loud dictionary setup.
|
||||
func checkSeedMarker(dir, bootVersion string) error {
|
||||
path := filepath.Join(dir, seedMarkerFile)
|
||||
data, err := os.ReadFile(path)
|
||||
switch {
|
||||
case err == nil:
|
||||
if recorded := strings.TrimSpace(string(data)); recorded != bootVersion {
|
||||
return fmt.Errorf("engine: dictionary volume was seeded as %q but BACKEND_DICT_VERSION is %q; "+
|
||||
"change a live dictionary by uploading the release through the admin console, or wipe the "+
|
||||
"volume to re-seed — do not bump the build seed (docs/ARCHITECTURE.md §5)", recorded, bootVersion)
|
||||
}
|
||||
return nil
|
||||
case errors.Is(err, os.ErrNotExist):
|
||||
if err := os.WriteFile(path, []byte(bootVersion+"\n"), 0o644); err != nil {
|
||||
return fmt.Errorf("engine: record dictionary seed marker %s: %w", path, err)
|
||||
}
|
||||
return nil
|
||||
default:
|
||||
return fmt.Errorf("engine: read dictionary seed marker %s: %w", path, err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user