fix(installer): clear legacy IMAGE_TAG so upgraders aren't pinned to v1.0.0
The v1.0.0 .env.production.example template hard-coded IMAGE_TAG=v1.0.0. Merge() preserved any non-empty existing value, so re-running the installer after the template was changed to IMAGE_TAG= still left the stale pin in .env, silently locking docker compose to the old release. Introduce a small templateAuthoritativeKeys set: when the template defines such a key (even as empty), the template wins. Existing non-empty user values are still preserved if a future template drops the key entirely. IMAGE_TAG is the only entry today. Adds two regression tests covering both directions.
This commit is contained in:
@@ -19,12 +19,32 @@ type Options struct {
|
||||
CORSAllowedOrigin string
|
||||
}
|
||||
|
||||
// templateAuthoritativeKeys are keys whose template definition always wins,
|
||||
// even when the user's existing .env has a non-empty value. This exists so
|
||||
// that intentional template changes (e.g. clearing IMAGE_TAG so compose can
|
||||
// resolve the default `latest` tag) propagate to upgraders. Without this,
|
||||
// stale values written by older template versions become permanently sticky
|
||||
// because Merge otherwise prefers any non-empty existing value.
|
||||
//
|
||||
// Only add a key here when the template is the source of truth for it AND
|
||||
// shipping a stale legacy value would break upgrades. IMAGE_TAG is the
|
||||
// canonical case: v1.0.0's template hard-coded IMAGE_TAG=v1.0.0, which
|
||||
// pinned every upgrader to that release until manually edited.
|
||||
var templateAuthoritativeKeys = map[string]struct{}{
|
||||
"IMAGE_TAG": {},
|
||||
}
|
||||
|
||||
func Merge(templateVals, existingVals map[string]string, opts Options) map[string]string {
|
||||
out := make(map[string]string, len(templateVals)+len(existingVals)+8)
|
||||
for k, v := range templateVals {
|
||||
out[k] = v
|
||||
}
|
||||
for k, v := range existingVals {
|
||||
if _, authoritative := templateAuthoritativeKeys[k]; authoritative {
|
||||
if _, definedInTemplate := templateVals[k]; definedInTemplate {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if strings.TrimSpace(v) != "" {
|
||||
out[k] = v
|
||||
}
|
||||
|
||||
@@ -127,6 +127,45 @@ func TestEnsureRequired_PreservesManualDatabaseURL(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestMerge_ClearsLegacyImageTagFromExistingEnv(t *testing.T) {
|
||||
// Regression: v1.0.0's template hard-coded IMAGE_TAG=v1.0.0. The current
|
||||
// template ships IMAGE_TAG= (empty) so docker-compose.yaml's `:-latest`
|
||||
// fallback wins. Without IMAGE_TAG being template-authoritative the old
|
||||
// value stays sticky and pins upgraders to v1.0.0 forever.
|
||||
template := map[string]string{
|
||||
"IMAGE_TAG": "",
|
||||
"POSTGRES_PASSWORD": "change-me-strong-db-password",
|
||||
}
|
||||
existing := map[string]string{
|
||||
"IMAGE_TAG": "v1.0.0",
|
||||
"POSTGRES_PASSWORD": "user-set-password",
|
||||
}
|
||||
|
||||
out := Merge(template, existing, Options{})
|
||||
if got := out["IMAGE_TAG"]; got != "" {
|
||||
t.Errorf("IMAGE_TAG = %q, want empty (template-authoritative)", got)
|
||||
}
|
||||
if got := out["POSTGRES_PASSWORD"]; got != "user-set-password" {
|
||||
t.Errorf("POSTGRES_PASSWORD = %q, want user-set value preserved", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestMerge_PreservesImageTagWhenTemplateOmits(t *testing.T) {
|
||||
// If a future template drops IMAGE_TAG entirely, an explicit pin in the
|
||||
// user's .env should still survive (no template intent to clear).
|
||||
template := map[string]string{
|
||||
"POSTGRES_PASSWORD": "change-me",
|
||||
}
|
||||
existing := map[string]string{
|
||||
"IMAGE_TAG": "v1.2.3",
|
||||
}
|
||||
|
||||
out := Merge(template, existing, Options{})
|
||||
if got := out["IMAGE_TAG"]; got != "v1.2.3" {
|
||||
t.Errorf("IMAGE_TAG = %q, want v1.2.3 preserved", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEnsureRequired_SetsInternalAPIKeyFileDefault(t *testing.T) {
|
||||
values := map[string]string{
|
||||
"POSTGRES_PASSWORD": "abcdef1234567890abcdef1234567890",
|
||||
|
||||
Reference in New Issue
Block a user