From 4a5709f2885f3e9688f5a21450972870f028494a Mon Sep 17 00:00:00 2001 From: admin Date: Tue, 20 Oct 2026 08:58:41 -0400 Subject: [PATCH 1/4] fix(docker): rebuild plugin images on startup (D-1449) --- internal/docker/compose.go | 6 ++-- internal/docker/compose_test.go | 2 +- internal/docker/compose_upgrade_test.go | 46 +++++++++++++++++++++++++ 3 files changed, 51 insertions(+), 3 deletions(-) create mode 100644 internal/docker/compose_upgrade_test.go diff --git a/internal/docker/compose.go b/internal/docker/compose.go index 72f801f06..f076b6a49 100644 --- a/internal/docker/compose.go +++ b/internal/docker/compose.go @@ -65,10 +65,12 @@ func withServices(args []string, services ...string) []string { return append(append(args, "--"), services...) } -// ComposeUp runs `docker compose up -d [services...]` in the given workdir. +// ComposeUp runs `docker compose up -d --build [services...]` in the given workdir. +// Rebuild services with a build context even when their image tag exists: a +// plugin upgrade can change the source without changing its local image tag. func (c *Compose) ComposeUp(ctx context.Context, workdir string, services ...string) error { args := c.buildBaseArgs() - args = withServices(append(args, "up", "-d"), services...) + args = withServices(append(args, "up", "-d", "--build"), services...) return c.Run(ctx, workdir, args...) } diff --git a/internal/docker/compose_test.go b/internal/docker/compose_test.go index 04b2c9bc8..29ee46658 100644 --- a/internal/docker/compose_test.go +++ b/internal/docker/compose_test.go @@ -43,7 +43,7 @@ func TestComposeServicesAfterDoubleDash(t *testing.T) { tail func(svc string) []string // argv after "compose -f /m/base.yml" }{ {"up", func(c *Compose, s string) error { return c.ComposeUp(ctx, ".", s) }, - func(s string) []string { return []string{"up", "-d", "--", s} }}, + func(s string) []string { return []string{"up", "-d", "--build", "--", s} }}, {"up --no-deps", func(c *Compose, s string) error { return c.ComposeUpNoDeps(ctx, ".", s) }, func(s string) []string { return []string{"up", "-d", "--no-deps", "--", s} }}, {"restart", func(c *Compose, s string) error { return c.ComposeRestart(ctx, ".", s) }, diff --git a/internal/docker/compose_upgrade_test.go b/internal/docker/compose_upgrade_test.go new file mode 100644 index 000000000..dc70d8fd5 --- /dev/null +++ b/internal/docker/compose_upgrade_test.go @@ -0,0 +1,46 @@ +package docker + +import ( + "context" + "os" + "path/filepath" + "strings" + "testing" +) + +// Model Compose's image reuse contract: a tagged image survives an up unless +// --build is supplied. The installed plugin context has already been upgraded. +func TestComposeUpRebuildsInstalledPlugin(t *testing.T) { + for _, services := range [][]string{nil, {"plugin"}} { + name := "all services" + if len(services) > 0 { + name = "named service" + } + t.Run(name, func(t *testing.T) { + dir := t.TempDir() + writeFile(t, filepath.Join(dir, "project-plugin-latest"), "1.0.0") + writeFile(t, filepath.Join(dir, "plugin-version"), "2.0.0") + bin := filepath.Join(dir, "docker") + writeExec(t, bin, `#!/bin/sh +for arg do + if [ "$arg" = "--build" ]; then + cp plugin-version project-plugin-latest || exit 1 + fi +done +cp project-plugin-latest served-version +`) + c := NewCompose("docker-compose.yml") + c.DockerPath = bin + if err := c.ComposeUp(context.Background(), dir, services...); err != nil { + t.Fatal(err) + } + got, err := os.ReadFile(filepath.Join(dir, "served-version")) + if err != nil { + t.Fatal(err) + } + if strings.TrimSpace(string(got)) != "2.0.0" { + t.Fatalf("served plugin version %s, want upgraded 2.0.0", got) + } + }) + } +} From 17ad8981dd3c8219afc53d354ba80fea4cdfc808 Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Tue, 20 Oct 2026 09:44:55 -0400 Subject: [PATCH 2/4] test(ci): isolate canon and entitlement fixtures from host state --- cmd/commands/canon_engine_test.go | 3 ++- internal/build/plugins_entitlement_mount_test.go | 8 ++++++++ internal/license/entitlement/write_test.go | 7 +++++++ 3 files changed, 17 insertions(+), 1 deletion(-) diff --git a/cmd/commands/canon_engine_test.go b/cmd/commands/canon_engine_test.go index 0ad0b8a0b..7d5da8b7e 100644 --- a/cmd/commands/canon_engine_test.go +++ b/cmd/commands/canon_engine_test.go @@ -776,7 +776,8 @@ func TestCanonEnginePrepare(t *testing.T) { t.Helper() compattest.Set(t, v15) savedArgs, savedMount := os.Args, mountInstalledPlugins - t.Cleanup(func() { os.Args, mountInstalledPlugins = savedArgs, savedMount }) + defer func() { os.Args, mountInstalledPlugins = savedArgs, savedMount }() + mountInstalledPlugins = func(*cobra.Command) {} if mount { mountInstalledPlugins = func(r *cobra.Command) { ci := fxCmd("ci", nil, fxCmd("nodes", fxRun)) diff --git a/internal/build/plugins_entitlement_mount_test.go b/internal/build/plugins_entitlement_mount_test.go index 1bd6a9156..f64ab6fb5 100644 --- a/internal/build/plugins_entitlement_mount_test.go +++ b/internal/build/plugins_entitlement_mount_test.go @@ -8,6 +8,7 @@ import ( "os" "path/filepath" "regexp" + "strconv" "strings" "testing" @@ -91,6 +92,13 @@ func TestMountIgnoresOverride(t *testing.T) { func TestMountEmptyFileE125(t *testing.T) { compattest.Set(t, true) t.Setenv("LICENSE_CACHE_PATH", filepath.Join(t.TempDir(), "missing")) + // Missing-token tests must not renew a licence configured on the CI host. + t.Setenv("HOME", t.TempDir()) + t.Setenv("USERPROFILE", t.TempDir()) + t.Setenv("NSELF_PLUGIN_LICENSE_KEY", "") + for i := 1; i <= 10; i++ { + t.Setenv("NSELF_LICENSE_KEY_"+strconv.Itoa(i), "") + } t.Setenv("LICENSE_PUBLIC_KEY_OVERRIDE", "") dir, in := entitlementFixture(t, "licensed") path := filepath.Join(dir, "fixture", pluginComposeFilename) diff --git a/internal/license/entitlement/write_test.go b/internal/license/entitlement/write_test.go index 009a311f1..52c43e14c 100644 --- a/internal/license/entitlement/write_test.go +++ b/internal/license/entitlement/write_test.go @@ -104,6 +104,13 @@ func TestEntitlementRefreshWrites(t *testing.T) { func TestBuildE125UnderV15(t *testing.T) { compattest.Both(t, func(t *testing.T) { t.Setenv("LICENSE_CACHE_PATH", filepath.Join(t.TempDir(), "missing.json")) + // Missing-token tests must not renew a licence configured on the CI host. + t.Setenv("HOME", t.TempDir()) + t.Setenv("USERPROFILE", t.TempDir()) + t.Setenv("NSELF_PLUGIN_LICENSE_KEY", "") + for i := 1; i <= 10; i++ { + t.Setenv("NSELF_LICENSE_KEY_"+strconv.Itoa(i), "") + } dir, plugins := t.TempDir(), t.TempDir() installed := filepath.Join(plugins, "fixture-licensed") if err := os.MkdirAll(installed, 0700); err != nil { From 80479ad640cff8164ffbcfc0f8055e546f4ba004 Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Tue, 20 Oct 2026 09:45:26 -0400 Subject: [PATCH 3/4] test(compose): expect image rebuilds in lifecycle calls --- cmd/commands/dev_test.go | 2 +- internal/service/compose_manifest_test.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cmd/commands/dev_test.go b/cmd/commands/dev_test.go index ddc55eb1c..37b988264 100644 --- a/cmd/commands/dev_test.go +++ b/cmd/commands/dev_test.go @@ -78,7 +78,7 @@ func TestDevComposeCallsCarryEnvFiles(t *testing.T) { t.Fatal(err) } flags := "compose --env-file " + filepath.Join(dir, ".env") + " --env-file " + filepath.Join(dir, ".nself", "compose.env") - want := []string{flags + " up -d", flags + " up -d --no-deps --build -- notify"} + want := []string{flags + " up -d --build", flags + " up -d --no-deps --build -- notify"} if got := strings.Split(strings.TrimRight(string(b), "\n"), "\n"); !reflect.DeepEqual(got, want) { t.Errorf("compose calls =\n%q\nwant\n%q", got, want) } diff --git a/internal/service/compose_manifest_test.go b/internal/service/compose_manifest_test.go index f6a7c99c5..9b2793890 100644 --- a/internal/service/compose_manifest_test.go +++ b/internal/service/compose_manifest_test.go @@ -149,7 +149,7 @@ func TestComposeForFollowsBuildManifests(t *testing.T) { t.Fatal(err) } got := strings.Join(strings.Fields(string(raw)), " ") - want := strings.Join([]string{"compose", "-f", base, "-f", frag, "--env-file", dotenv, "--env-file", computed, "up", "-d", "--", "minio"}, " ") + want := strings.Join([]string{"compose", "-f", base, "-f", frag, "--env-file", dotenv, "--env-file", computed, "up", "-d", "--build", "--", "minio"}, " ") if got != want { t.Fatalf("docker argv\n got: %s\nwant: %s", got, want) } From f26dc25dfa53c4ff78749561e1c59b152a034eda Mon Sep 17 00:00:00 2001 From: Aric Camarata Date: Tue, 20 Oct 2026 10:02:32 -0400 Subject: [PATCH 4/4] fix(ci): explicitly discard best-effort notice write errors --- cmd/commands/ci_bootstrap.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/commands/ci_bootstrap.go b/cmd/commands/ci_bootstrap.go index 34eb2b9f2..7aa57e27f 100644 --- a/cmd/commands/ci_bootstrap.go +++ b/cmd/commands/ci_bootstrap.go @@ -141,7 +141,7 @@ func retireLegacyCICache() { _ = os.Remove(legacy) } } - fmt.Fprintln(ciNoticeOut, "notice: ~/.nself/bin/nself-ci is no longer used (unverified); nself now installs the signed ci plugin") + _, _ = fmt.Fprintln(ciNoticeOut, "notice: ~/.nself/bin/nself-ci is no longer used (unverified); nself now installs the signed ci plugin") }) }