Skip to content

Commit 1dedd8f

Browse files
committed
feat: Ability to lock devbox version per project
Let's users pin the devbox version so there are no issues with people running various devbox versions while working in team setting. * Implements #1371
1 parent 8057f1b commit 1dedd8f

File tree

6 files changed

+119
-35
lines changed

6 files changed

+119
-35
lines changed

devbox.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
{
22
"name": "devbox",
33
"description": "Instant, easy, and predictable development environments",
4+
"devbox_version": "0.0.0-dev",
45
"packages": {
56
"fd": "latest",
67
"git": "latest",

docs/app/docs/configuration.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ Your devbox configuration is stored in a `devbox.json` file, located in your pro
77

88
```json
99
{
10+
"devbox_version": "",
1011
"packages": [] | {},
1112
"env": {},
1213
"shell": {
@@ -17,6 +18,10 @@ Your devbox configuration is stored in a `devbox.json` file, located in your pro
1718
}
1819
```
1920

21+
## Devbox Version
22+
23+
The devbox_version field locks your project to a specific Devbox version, safeguarding against unexpected changes when collaborators update their environments.
24+
2025
### Packages
2126

2227
This is a list or map of Nix packages that should be installed in your Devbox shell and containers. These packages will only be installed and available within your shell, and will have precedence over any packages installed in your local machine. You can search for Nix packages using [Nix Package Search](https://search.nixos.org/packages).
@@ -297,6 +302,7 @@ An example of a devbox configuration for a Rust project called `hello_world` mig
297302

298303
```json
299304
{
305+
"devbox_version": "v1.0.0",
300306
"packages": [
301307
"rustup@latest",
302308
"libiconv@latest"

go.mod

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ require (
2323
github.com/google/go-cmp v0.7.0
2424
github.com/google/uuid v1.6.0
2525
github.com/hashicorp/go-envparse v0.1.0
26+
github.com/hashicorp/go-version v1.7.0
2627
github.com/joho/godotenv v1.5.1
2728
github.com/mattn/go-isatty v0.0.20
2829
github.com/mholt/archives v0.1.0
@@ -167,7 +168,6 @@ require (
167168
github.com/hashicorp/errwrap v1.1.0 // indirect
168169
github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect
169170
github.com/hashicorp/go-multierror v1.1.1 // indirect
170-
github.com/hashicorp/go-version v1.7.0 // indirect
171171
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
172172
github.com/hashicorp/hcl v1.0.0 // indirect
173173
github.com/hexops/gotextdiff v1.0.3 // indirect

internal/devconfig/config.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ const defaultInitHook = "echo 'Welcome to devbox!' > /dev/null"
4949
func DefaultConfig() *Config {
5050
cfg, err := loadBytes([]byte(fmt.Sprintf(`{
5151
"$schema": "https://raw.githubusercontent.com/jetify-com/devbox/%s/.schema/devbox.schema.json",
52+
"devbox_version": "%s",
5253
"packages": [],
5354
"shell": {
5455
"init_hook": [

internal/devconfig/configfile/file.go

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,8 @@ import (
1717
"go.jetify.com/devbox/internal/boxcli/usererr"
1818
"go.jetify.com/devbox/internal/cachehash"
1919
"go.jetify.com/devbox/internal/devbox/shellcmd"
20+
"go.jetify.com/devbox/internal/build"
21+
"github.com/hashicorp/go-version"
2022
)
2123

2224
const (
@@ -32,6 +34,9 @@ type ConfigFile struct {
3234
Name string `json:"name,omitempty"`
3335
Description string `json:"description,omitempty"`
3436

37+
// Let's users specify the version of devbox.
38+
DevboxVersion string `json:"devbox_version,omitempty"`
39+
3540
// PackagesMutator is the slice of Nix packages that devbox makes available in
3641
// its environment. Deliberately do not omitempty.
3742
PackagesMutator PackagesMutator `json:"packages"`
@@ -109,7 +114,6 @@ func (c *ConfigFile) InitHook() *shellcmd.Commands {
109114

110115
// SaveTo writes the config to a file.
111116
func (c *ConfigFile) SaveTo(path string) error {
112-
return os.WriteFile(filepath.Join(path, DefaultName), c.Bytes(), 0o644)
113117
finalPath := path
114118
if filepath.Base(path) != DefaultName {
115119
finalPath = filepath.Join(path, DefaultName)
@@ -164,6 +168,7 @@ func validateConfig(cfg *ConfigFile) error {
164168
fns := []func(cfg *ConfigFile) error{
165169
ValidateNixpkg,
166170
validateScripts,
171+
ValidateDevboxVersion,
167172
}
168173

169174
for _, fn := range fns {
@@ -210,3 +215,33 @@ func ValidateNixpkg(cfg *ConfigFile) error {
210215
}
211216
return nil
212217
}
218+
219+
func ValidateDevboxVersion(cfg *ConfigFile) error {
220+
if cfg.DevboxVersion == "" {
221+
return usererr.New("Missing devbox_version field in config, suggested value: \"~%s\",", build.Version)
222+
}
223+
224+
// Use hashicorp/go-version for version constraint checking
225+
constraints, err := version.NewConstraint(cfg.DevboxVersion)
226+
if err != nil {
227+
return usererr.New("Invalid devbox_version constraint in config: %s", cfg.DevboxVersion)
228+
}
229+
230+
currentVersion, err := version.NewVersion(build.Version)
231+
if err != nil {
232+
return usererr.New("Invalid current devbox version: %s", build.Version)
233+
}
234+
235+
if !constraints.Check(currentVersion) {
236+
return usererr.New("Devbox version mismatch: project requires version %s but your running version is %s",
237+
cfg.DevboxVersion, build.Version)
238+
}
239+
240+
return nil
241+
}
242+
243+
// SetDevboxVersion sets the devbox_version field in the config
244+
func (c *ConfigFile) SetDevboxVersion(version string) {
245+
c.DevboxVersion = version
246+
c.SetStringField("DevboxVersion", version)
247+
}

internal/templates/template.go

Lines changed: 74 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,9 @@ import (
1818

1919
"go.jetify.com/devbox/internal/boxcli/usererr"
2020
"go.jetify.com/devbox/internal/build"
21+
"go.jetify.com/devbox/internal/devconfig"
22+
23+
"github.com/hashicorp/go-version"
2124
)
2225

2326
func InitFromName(w io.Writer, template, target string) error {
@@ -29,41 +32,46 @@ func InitFromName(w io.Writer, template, target string) error {
2932
}
3033

3134
func InitFromRepo(w io.Writer, repo, subdir, target string) error {
32-
if err := createDirAndEnsureEmpty(target); err != nil {
33-
return err
34-
}
35-
parsedRepoURL, err := ParseRepoURL(repo)
36-
if err != nil {
37-
return errors.WithStack(err)
38-
}
35+
if err := createDirAndEnsureEmpty(target); err != nil {
36+
return err
37+
}
38+
parsedRepoURL, err := ParseRepoURL(repo)
39+
if err != nil {
40+
return errors.WithStack(err)
41+
}
3942

40-
tmp, err := os.MkdirTemp("", "devbox-template")
41-
if err != nil {
42-
return errors.WithStack(err)
43-
}
44-
cmd := exec.Command(
45-
"git", "clone", parsedRepoURL,
46-
// Clone and checkout a specific ref
47-
"-b", lo.Ternary(build.IsDev, "main", build.Version),
48-
// Create shallow clone with depth of 1
49-
"--depth", "1",
50-
tmp,
51-
)
52-
fmt.Fprintf(w, "%s\n", cmd)
53-
cmd.Stderr = os.Stderr
54-
cmd.Stdout = os.Stdout
55-
if err = cmd.Run(); err != nil {
56-
return errors.WithStack(err)
57-
}
43+
tmp, err := os.MkdirTemp("", "devbox-template")
44+
if err != nil {
45+
return errors.WithStack(err)
46+
}
47+
cmd := exec.Command(
48+
"git", "clone", parsedRepoURL,
49+
// Clone and checkout a specific ref
50+
"-b", lo.Ternary(build.IsDev, "main", build.Version),
51+
// Create shallow clone with depth of 1
52+
"--depth", "1",
53+
tmp,
54+
)
55+
fmt.Fprintf(w, "%s\n", cmd)
56+
cmd.Stderr = os.Stderr
57+
cmd.Stdout = os.Stdout
58+
if err = cmd.Run(); err != nil {
59+
return errors.WithStack(err)
60+
}
61+
62+
cmd = exec.Command(
63+
"sh", "-c",
64+
fmt.Sprintf("cp -r %s %s", filepath.Join(tmp, subdir, "*"), target),
65+
)
66+
fmt.Fprintf(w, "%s\n", cmd)
67+
cmd.Stderr = os.Stderr
68+
cmd.Stdout = os.Stdout
69+
if err = cmd.Run(); err != nil {
70+
return errors.WithStack(err)
71+
}
5872

59-
cmd = exec.Command(
60-
"sh", "-c",
61-
fmt.Sprintf("cp -r %s %s", filepath.Join(tmp, subdir, "*"), target),
62-
)
63-
fmt.Fprintf(w, "%s\n", cmd)
64-
cmd.Stderr = os.Stderr
65-
cmd.Stdout = os.Stdout
66-
return errors.WithStack(cmd.Run())
73+
// Set the devbox version after initializing the template
74+
return SetCurrentDevboxVersion(w, target)
6775
}
6876

6977
func List(w io.Writer, showAll bool) {
@@ -105,3 +113,36 @@ func ParseRepoURL(repo string) (string, error) {
105113
// like: https://github.com/jetify-com/devbox.git
106114
return strings.TrimSuffix(repo, ".git"), nil
107115
}
116+
117+
// SetCurrentDevboxVersion sets the current version as the required version in the config
118+
func SetCurrentDevboxVersion(w io.Writer, projectDir string) error {
119+
if strings.HasSuffix(projectDir, "devbox.json") {
120+
projectDir = filepath.Dir(projectDir)
121+
}
122+
123+
fmt.Println(projectDir)
124+
125+
cfg, err := devconfig.Open(projectDir)
126+
if err != nil {
127+
return errors.WithStack(err)
128+
}
129+
fmt.Printf("%v", cfg)
130+
131+
// Create a constraint like "~1.2.0" (compatible with 1.2.x)
132+
currentVersion, err := version.NewVersion(build.Version)
133+
if err != nil {
134+
return errors.WithStack(err)
135+
}
136+
137+
segments := currentVersion.Segments()
138+
if len(segments) < 2 {
139+
return errors.New("invalid version format")
140+
}
141+
142+
// Create a constraint for the current major.minor version
143+
versionConstraint := fmt.Sprintf("~%d.%d.0", segments[0], segments[1])
144+
145+
fmt.Fprintf(w, "Setting project devbox version constraint: %s\n", versionConstraint)
146+
cfg.Root.SetDevboxVersion(versionConstraint)
147+
return cfg.Root.SaveTo(cfg.Root.AbsRootPath)
148+
}

0 commit comments

Comments
 (0)