v0.276.0: a restore and a drive move keep the app's records (R-697, R-700)
gates / gates (push) Successful in 26s

A drive move persisted through the restore's fresh app.yaml write and dropped the pin: the syncer
then copied the catalog verbatim and the next start jumped the app past its ladder (R-700).
persistDriveFlip now changes HDD_PATH and nothing else. The restore's write carries the life
records (conversion copies, desired_state, update history) from the app.yaml it replaces, and a
second conversion no longer overwrites the first kept copy's record (R-697).

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
2026-09-27 17:59:04 +02:00
parent 54bb4da343
commit 820e8efde1
7 changed files with 331 additions and 42 deletions
+36 -11
View File
@@ -742,11 +742,13 @@ func (m *Manager) doFlipRedeploy(name, target string) error {
if m.testSeams != nil && m.testSeams.flipRedeploy != nil {
return m.testSeams.flipRedeploy(name, target)
}
cfg := m.LoadAppConfigByName(name)
if cfg == nil {
return fmt.Errorf("app config not found")
// R-700 (v0.276.0): the move changes WHERE the data lives and nothing else — persistDriveFlip keeps
// the pin and every record. This used to be RedeployFromEnv, whose fresh app.yaml dropped the pin: the
// syncer then copied the catalog verbatim and the next start jumped the app past its ladder.
if err := m.persistDriveFlip(name, target); err != nil {
return err
}
if err := m.RedeployFromEnv(name, flipEnv(cfg.Env, target)); err != nil {
if err := m.upFromAppConfig(name); err != nil {
return err
}
if !m.waitHealthy(name) {
@@ -755,14 +757,37 @@ func (m *Manager) doFlipRedeploy(name, target string) error {
return nil
}
// flipEnv returns a copy of env with HDD_PATH set to target.
func flipEnv(env map[string]string, target string) map[string]string {
out := make(map[string]string, len(env)+1)
for k, v := range env {
out[k] = v
// persistDriveFlip points app.yaml's HDD_PATH at target and changes NOTHING else (R-700, v0.276.0):
// load-then-save, the SaveAppConfig rule — the pin, the intent, the installed images, the update records
// and the kept conversion copies all stay. Starts nothing.
func (m *Manager) persistDriveFlip(name, target string) error {
stack, ok := m.GetStack(name)
if !ok {
return fmt.Errorf("stack %q not found", name)
}
out["HDD_PATH"] = target
return out
dir := filepath.Dir(stack.ComposePath)
cfg := LoadAppConfig(dir)
if cfg == nil {
return fmt.Errorf("app config not found")
}
if cfg.Env == nil {
cfg.Env = map[string]string{}
}
from := cfg.Env["HDD_PATH"]
cfg.Env["HDD_PATH"] = target
cfg.Deployed = true
meta := LoadMetadata(dir)
if err := SaveAppConfig(dir, cfg, m.encKey, SensitiveEnvVars(&meta)); err != nil {
return fmt.Errorf("saving app config: %w", err)
}
m.mu.Lock()
if s, ok := m.stacks[name]; ok {
s.Deployed = true
s.AppConfig = cfg
}
m.mu.Unlock()
m.logger.Printf("[INFO] [stacks] %s: data moved %s -> %s — app.yaml keeps its pin (%d service(s)) and records", name, from, target, len(cfg.PinnedImages))
return nil
}
// waitHealthy polls until the stack is up (running/unhealthy) or times out.