From 4c7bd7db202d54d0119d0e818e7f66d42024f7c7 Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Mon, 7 Sep 2026 10:49:43 +0200 Subject: [PATCH 1/3] fix(updater): retain retirement output and discover custom service --- .changeset/retirement-command-output.md | 5 +++++ go/cmd/ftw-updater/main.go | 3 +++ go/cmd/ftw-updater/retire_python.go | 16 +++++++++++++++- go/cmd/ftw-updater/retire_python_test.go | 7 +++++++ 4 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 .changeset/retirement-command-output.md diff --git a/.changeset/retirement-command-output.md b/.changeset/retirement-command-output.md new file mode 100644 index 00000000..4e08aeb8 --- /dev/null +++ b/.changeset/retirement-command-output.md @@ -0,0 +1,5 @@ +--- +"ftw": patch +--- + +Keep complete errors and backup paths visible when retiring Python, and find the installed updater even when its Compose service has a custom name. diff --git a/go/cmd/ftw-updater/main.go b/go/cmd/ftw-updater/main.go index a9fa96fb..209973c2 100644 --- a/go/cmd/ftw-updater/main.go +++ b/go/cmd/ftw-updater/main.go @@ -278,6 +278,9 @@ func main() { srv.mainServiceName = selectedService srv.imageID = srv.currentServiceImageID if *retirePython { + // This is an interactive command. Preserve the helper's complete output, + // including the final error or backup path after its startup messages. + srv.runner = dockerStreaming ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) defer cancel() retire := srv.retirePythonViaHelper diff --git a/go/cmd/ftw-updater/retire_python.go b/go/cmd/ftw-updater/retire_python.go index ad8cec66..3f8943b8 100644 --- a/go/cmd/ftw-updater/retire_python.go +++ b/go/cmd/ftw-updater/retire_python.go @@ -171,7 +171,11 @@ func retiredPythonCompose(data []byte) ([]byte, bool, error) { // The running updater mounts Compose read-only. Use its exact local image in // a short-lived helper with a writable project mount, as self-replacement does. func (s *server) retirePythonViaHelper(ctx context.Context) error { - image, err := s.imageID(ctx, "ftw-updater") + service, err := s.updaterServiceName() + if err != nil { + return err + } + image, err := s.imageID(ctx, service) if err != nil { return fmt.Errorf("current updater image: %w", err) } @@ -188,6 +192,16 @@ func (s *server) retirePythonViaHelper(ctx context.Context) error { return s.runner(ctx, nil, args...) } +func dockerStreaming(ctx context.Context, extraEnv []string, args ...string) error { + cmd := exec.CommandContext(ctx, "docker", args...) + if len(extraEnv) > 0 { + cmd.Env = append(os.Environ(), extraEnv...) + } + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd.Run() +} + func (s *server) retiredPythonContainers(ctx context.Context) ([]string, error) { coreID, err := s.serviceContainerID(ctx, s.mainServiceName) if err != nil { diff --git a/go/cmd/ftw-updater/retire_python_test.go b/go/cmd/ftw-updater/retire_python_test.go index bc4d3dce..1e4426f8 100644 --- a/go/cmd/ftw-updater/retire_python_test.go +++ b/go/cmd/ftw-updater/retire_python_test.go @@ -72,6 +72,13 @@ func TestUpdaterRejectsRetiredOptimizer(t *testing.T) { func TestRetirePythonHelperUsesLocalImageAndWritableProject(t *testing.T) { s, runner := newTestServer(t) + writeCompose(t, s.composeFile, "services:\n renamed-updater:\n image: mirror.example/team/ftw-updater:v2.16.0-beta.1\n") + s.imageID = func(_ context.Context, service string) (string, error) { + if service != "renamed-updater" { + t.Fatalf("looked up %q instead of the installed updater", service) + } + return "sha256:current", nil + } t.Setenv("COMPOSE_PROJECT_NAME", "existing-site") if err := s.retirePythonViaHelper(context.Background()); err != nil { t.Fatal(err) From f22d04ecd3b0e942671039a95b6ed3b45f044005 Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Mon, 7 Sep 2026 10:50:51 +0200 Subject: [PATCH 2/3] fix(updater): report retirement backups only when created --- go/cmd/ftw-updater/retire_python.go | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/go/cmd/ftw-updater/retire_python.go b/go/cmd/ftw-updater/retire_python.go index 3f8943b8..e045d4df 100644 --- a/go/cmd/ftw-updater/retire_python.go +++ b/go/cmd/ftw-updater/retire_python.go @@ -273,6 +273,10 @@ func (s *server) retirePythonOptimizer(ctx context.Context) error { if err != nil { return err } + if len(changes) == 0 && len(ids) == 0 { + fmt.Println("Compose has no retired Python wiring and no Python service remains.") + return nil + } suffix := ".before-python-removal-" + time.Now().UTC().Format("20060102T150405.000000000") for _, c := range changes { if err := os.WriteFile(c.path+suffix, c.before, c.mode); err != nil { @@ -300,7 +304,11 @@ func (s *server) retirePythonOptimizer(ctx context.Context) error { return restore(fmt.Errorf("remove retired container: %w", err)) } } - fmt.Println("Python optimizer removed. Compose backups:", suffix, "Recreate Core at its pinned version to release the old IPC mount.") + fmt.Println("Python optimizer removed.") + if len(changes) > 0 { + fmt.Println("Compose backups:", suffix) + } + fmt.Println("Recreate Core at its pinned version to release the old IPC mount.") return nil } From 5fdf4979128f9bf699629b1e6b808cfabd1e49fd Mon Sep 17 00:00:00 2001 From: Fredrik Ahlgren Date: Mon, 7 Sep 2026 10:54:23 +0200 Subject: [PATCH 3/3] fix(updater): recognize the published compatibility image --- .github/brand/compatibility-allowlist.txt | 2 ++ go/cmd/ftw-updater/self_replace.go | 3 ++- go/cmd/ftw-updater/self_replace_test.go | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) diff --git a/.github/brand/compatibility-allowlist.txt b/.github/brand/compatibility-allowlist.txt index 9bce438c..ead3ac07 100644 --- a/.github/brand/compatibility-allowlist.txt +++ b/.github/brand/compatibility-allowlist.txt @@ -8,6 +8,8 @@ ^docs/ha-integration\.md:> \*\*Compatibility contract:\*\* the MQTT prefix `forty-two-watts`, discovery$ ^docs/ha-integration\.md:> identifier `forty_two_watts`, client ID `forty-two-watts-ha`, and existing$ ^go/cmd/ftw-updater/main\.go:.*legacyMainServiceName.*= "forty-two-watts"$ +# Published compatibility updater image remains supported during migration. +^go/cmd/ftw-updater/self_replace\.go:.*legacyUpdaterImage.*= "ghcr.io/frahlg/forty-two-watts-updater"$ ^go/cmd/ftw/main\.go:// caldavUsername resolves the CalDAV username \(default fortytwowatts\)\.$ ^go/internal/ha/bridge\.go:.*topicPrefix string // stable wire ID, e\.g\. "forty-two-watts"$ ^scripts/deploy-go\.sh:.*ln -sf ftw forty-two-watts$ diff --git a/go/cmd/ftw-updater/self_replace.go b/go/cmd/ftw-updater/self_replace.go index 2e7dbbe1..604efd43 100644 --- a/go/cmd/ftw-updater/self_replace.go +++ b/go/cmd/ftw-updater/self_replace.go @@ -35,6 +35,7 @@ import ( const ( canonicalUpdaterImage = "ghcr.io/srcfl/ftw-updater" + legacyUpdaterImage = "ghcr.io/frahlg/forty-two-watts-updater" // updaterTagEnv pins the sidecar image the same way mainTagEnv pins Core. // These must match the tagEnv values in componentSpec. updaterTagEnv = "FTW_UPDATER_IMAGE_TAG" @@ -215,7 +216,7 @@ func isUpdaterImage(image string) bool { if idx := strings.Index(repo[slash+1:], ":"); idx >= 0 { repo = repo[:slash+1+idx] } - if repo == canonicalUpdaterImage { + if repo == canonicalUpdaterImage || repo == legacyUpdaterImage { return true } return strings.HasSuffix(repo, "/ftw-updater") diff --git a/go/cmd/ftw-updater/self_replace_test.go b/go/cmd/ftw-updater/self_replace_test.go index c133149e..1abcdaa9 100644 --- a/go/cmd/ftw-updater/self_replace_test.go +++ b/go/cmd/ftw-updater/self_replace_test.go @@ -253,6 +253,9 @@ func TestIsUpdaterImage(t *testing.T) { }{ {"ghcr.io/srcfl/ftw-updater:latest", true}, {"ghcr.io/srcfl/ftw-updater", true}, + {legacyUpdaterImage + ":v2.16.0-beta.1", true}, + {legacyUpdaterImage + ":${FTW_UPDATER_IMAGE_TAG:-latest}", true}, + {legacyUpdaterImage + "@sha256:abc", true}, // Compose images reach us unexpanded; the default's own colon must not // be mistaken for the tag separator. {"ghcr.io/srcfl/ftw-updater:${FTW_UPDATER_IMAGE_TAG:-latest}", true},