From bea65cef3107a9e5bec30fbb2ef069a099ac7830 Mon Sep 17 00:00:00 2001 From: Mike Landau Date: Mon, 28 Sep 2026 09:16:44 -0700 Subject: [PATCH 1/2] fix(lock): don't rewrite plugin_version on shell/run/install Every command that ran ensureStateIsUpToDate overwrote plugin_version in devbox.lock with the version of the built-in plugin embedded in the running binary, so switching Devbox versions churned the lockfile back and forth. Only fill in missing plugin versions in ensure mode; add/rm/update still overwrite them. Co-Authored-By: Claude Opus 5.5 (1M context) --- internal/devbox/packages.go | 10 +++++--- internal/plugin/plugin.go | 9 ++++++- internal/plugin/plugin_test.go | 47 ++++++++++++++++++++++++++++++++++ 3 files changed, 61 insertions(+), 5 deletions(-) diff --git a/internal/devbox/packages.go b/internal/devbox/packages.go index 7f9805d9fda..6c8d7319e75 100644 --- a/internal/devbox/packages.go +++ b/internal/devbox/packages.go @@ -358,12 +358,12 @@ func (d *Devbox) ensureStateIsUpToDate(ctx context.Context, mode installMode) er ) } - return d.updateLockfile(recomputeState) + return d.updateLockfile(mode, recomputeState) } // updateLockfile will ensure devbox.lock is up to date with the current state of the project.update // If recomputeState is true, then we will also update the state.json file. -func (d *Devbox) updateLockfile(recomputeState bool) error { +func (d *Devbox) updateLockfile(mode installMode, recomputeState bool) error { // Ensure we clean out packages that are no longer needed. d.lockfile.Tidy() @@ -374,9 +374,11 @@ func (d *Devbox) updateLockfile(recomputeState bool) error { } } - // Update plugin versions in lockfile. + // Update plugin versions in lockfile. In ensure mode (shell, run, install) + // only fill in missing versions; changing an existing version is left to + // commands that are expected to modify the lockfile (add, rm, update). for _, pluginConfig := range d.Config().IncludedPluginConfigs() { - if err := d.PluginManager().UpdateLockfileVersion(pluginConfig); err != nil { + if err := d.PluginManager().UpdateLockfileVersion(pluginConfig, mode != ensure); err != nil { return err } } diff --git a/internal/plugin/plugin.go b/internal/plugin/plugin.go index d2dccc59d32..ca2450d1746 100644 --- a/internal/plugin/plugin.go +++ b/internal/plugin/plugin.go @@ -170,7 +170,11 @@ func (m *Manager) CreateFilesForConfig(cfg *Config) error { return nil } -func (m *Manager) UpdateLockfileVersion(cfg *Config) error { +// UpdateLockfileVersion records the plugin's version in the lockfile. An +// existing version is only replaced when overwrite is true, so that commands +// which aren't meant to modify devbox.lock (shell, run, install) don't rewrite +// it just because the running Devbox embeds a newer or older built-in plugin. +func (m *Manager) UpdateLockfileVersion(cfg *Config, overwrite bool) error { pkg := cfg.Source locked := m.lockfile.Packages[pkg.LockfileKey()] // plugins that are not triggered by packages don't have a lockfile entry @@ -178,6 +182,9 @@ func (m *Manager) UpdateLockfileVersion(cfg *Config) error { if locked == nil { return nil } + if locked.PluginVersion != "" && !overwrite { + return nil + } locked.PluginVersion = cfg.Version return m.lockfile.Save() } diff --git a/internal/plugin/plugin_test.go b/internal/plugin/plugin_test.go index 2532bd2c32c..bca2e35e117 100644 --- a/internal/plugin/plugin_test.go +++ b/internal/plugin/plugin_test.go @@ -10,6 +10,7 @@ import ( "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" + "go.jetify.com/devbox/internal/lock" "go.jetify.com/devbox/nix/flake" ) @@ -126,3 +127,49 @@ func TestBuildConfigTemplatePlaceholders(t *testing.T) { "Virtenv should be /.devbox/virtenv/", ) } + +// lockProjectForTest is a minimal lock.File project rooted at dir. +type lockProjectForTest struct{ dir string } + +func (p lockProjectForTest) ConfigHash() (string, error) { return "", nil } +func (p lockProjectForTest) Stdenv() flake.Ref { return flake.Ref{} } +func (p lockProjectForTest) ProjectDir() string { return p.dir } +func (p lockProjectForTest) AllPackageNamesIncludingRemovedTriggerPackages() []string { + return nil +} + +// TestUpdateLockfileVersion verifies that an existing plugin_version is only +// overwritten when requested, so that running a different Devbox version (which +// embeds different built-in plugin versions) doesn't rewrite devbox.lock. +func TestUpdateLockfileVersion(t *testing.T) { + tests := []struct { + name string + locked string + overwrite bool + want string + }{ + {name: "fills missing version", locked: "", overwrite: false, want: "0.0.2"}, + {name: "keeps existing version", locked: "0.0.1", overwrite: false, want: "0.0.1"}, + {name: "overwrites existing version", locked: "0.0.1", overwrite: true, want: "0.0.2"}, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + lockfile, err := lock.GetFile(lockProjectForTest{dir: t.TempDir()}) + require.NoError(t, err) + lockfile.Packages["nodejs@22"] = &lock.Package{ + Resolved: "github:NixOS/nixpkgs/abc#nodejs_22", + PluginVersion: tt.locked, + } + + cfg := &Config{ + PluginOnlyData: PluginOnlyData{ + Source: fakeIncludable{name: "nodejs@22"}, + Version: "0.0.2", + }, + } + m := NewManager(WithLockfile(lockfile)) + require.NoError(t, m.UpdateLockfileVersion(cfg, tt.overwrite)) + assert.Equal(t, tt.want, lockfile.Packages["nodejs@22"].PluginVersion) + }) + } +} From 430b05514a63cf30708895ce0114e01ae428dcb8 Mon Sep 17 00:00:00 2001 From: Mike Landau Date: Mon, 28 Sep 2026 09:26:04 -0700 Subject: [PATCH 2/2] test(plugin): rename table test loop var to satisfy varnamelen Co-Authored-By: Claude Opus 5.5 (1M context) --- internal/plugin/plugin_test.go | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/internal/plugin/plugin_test.go b/internal/plugin/plugin_test.go index bca2e35e117..b6899db3d1f 100644 --- a/internal/plugin/plugin_test.go +++ b/internal/plugin/plugin_test.go @@ -152,13 +152,13 @@ func TestUpdateLockfileVersion(t *testing.T) { {name: "keeps existing version", locked: "0.0.1", overwrite: false, want: "0.0.1"}, {name: "overwrites existing version", locked: "0.0.1", overwrite: true, want: "0.0.2"}, } - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { lockfile, err := lock.GetFile(lockProjectForTest{dir: t.TempDir()}) require.NoError(t, err) lockfile.Packages["nodejs@22"] = &lock.Package{ Resolved: "github:NixOS/nixpkgs/abc#nodejs_22", - PluginVersion: tt.locked, + PluginVersion: test.locked, } cfg := &Config{ @@ -168,8 +168,8 @@ func TestUpdateLockfileVersion(t *testing.T) { }, } m := NewManager(WithLockfile(lockfile)) - require.NoError(t, m.UpdateLockfileVersion(cfg, tt.overwrite)) - assert.Equal(t, tt.want, lockfile.Packages["nodejs@22"].PluginVersion) + require.NoError(t, m.UpdateLockfileVersion(cfg, test.overwrite)) + assert.Equal(t, test.want, lockfile.Packages["nodejs@22"].PluginVersion) }) } }