Skip to content

Feat/pin devbox version #2565

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions devbox.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
{
"name": "devbox",
"description": "Instant, easy, and predictable development environments",
"devbox_version": "0.0.0-dev",
"packages": {
"fd": "latest",
"git": "latest",
Expand Down
6 changes: 6 additions & 0 deletions docs/app/docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ Your devbox configuration is stored in a `devbox.json` file, located in your pro

```json
{
"devbox_version": "",
"packages": [] | {},
"env": {},
"shell": {
Expand All @@ -17,6 +18,10 @@ Your devbox configuration is stored in a `devbox.json` file, located in your pro
}
```

## Devbox Version

The devbox_version field locks your project to a specific Devbox version, safeguarding against unexpected changes when collaborators update their environments.

### Packages

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).
Expand Down Expand Up @@ -297,6 +302,7 @@ An example of a devbox configuration for a Rust project called `hello_world` mig

```json
{
"devbox_version": "v1.0.0",
"packages": [
"rustup@latest",
"libiconv@latest"
Expand Down
2 changes: 1 addition & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ require (
github.com/google/go-cmp v0.7.0
github.com/google/uuid v1.6.0
github.com/hashicorp/go-envparse v0.1.0
github.com/hashicorp/go-version v1.7.0
github.com/joho/godotenv v1.5.1
github.com/mattn/go-isatty v0.0.20
github.com/mholt/archives v0.1.0
Expand Down Expand Up @@ -167,7 +168,6 @@ require (
github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-immutable-radix/v2 v2.1.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-version v1.7.0 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/hashicorp/hcl v1.0.0 // indirect
github.com/hexops/gotextdiff v1.0.3 // indirect
Expand Down
1 change: 1 addition & 0 deletions internal/devconfig/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ const defaultInitHook = "echo 'Welcome to devbox!' > /dev/null"
func DefaultConfig() *Config {
cfg, err := loadBytes([]byte(fmt.Sprintf(`{
"$schema": "https://raw.githubusercontent.com/jetify-com/devbox/%s/.schema/devbox.schema.json",
"devbox_version": "%s",
"packages": [],
"shell": {
"init_hook": [
Expand Down
44 changes: 43 additions & 1 deletion internal/devconfig/configfile/file.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ import (
"go.jetify.com/devbox/internal/boxcli/usererr"
"go.jetify.com/devbox/internal/cachehash"
"go.jetify.com/devbox/internal/devbox/shellcmd"
"go.jetify.com/devbox/internal/build"
"github.com/hashicorp/go-version"
)

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

// Let's users specify the version of devbox.
DevboxVersion string `json:"devbox_version,omitempty"`

// PackagesMutator is the slice of Nix packages that devbox makes available in
// its environment. Deliberately do not omitempty.
PackagesMutator PackagesMutator `json:"packages"`
Expand Down Expand Up @@ -109,7 +114,13 @@ func (c *ConfigFile) InitHook() *shellcmd.Commands {

// SaveTo writes the config to a file.
func (c *ConfigFile) SaveTo(path string) error {
return os.WriteFile(filepath.Join(path, DefaultName), c.Bytes(), 0o644)
finalPath := path
if filepath.Base(path) != DefaultName {
finalPath = filepath.Join(path, DefaultName)
}

//return os.WriteFile(filepath.Join(path, DefaultName), c.Bytes(), 0o644)
return os.WriteFile(filepath.Join(finalPath), c.Bytes(), 0o644)
}

// TODO: Can we remove SaveTo and just use Save()?
Expand Down Expand Up @@ -157,6 +168,7 @@ func validateConfig(cfg *ConfigFile) error {
fns := []func(cfg *ConfigFile) error{
ValidateNixpkg,
validateScripts,
ValidateDevboxVersion,
}

for _, fn := range fns {
Expand Down Expand Up @@ -203,3 +215,33 @@ func ValidateNixpkg(cfg *ConfigFile) error {
}
return nil
}

func ValidateDevboxVersion(cfg *ConfigFile) error {
if cfg.DevboxVersion == "" {
return usererr.New("Missing devbox_version field in config, suggested value: \"~%s\",", build.Version)
}

// Use hashicorp/go-version for version constraint checking
constraints, err := version.NewConstraint(cfg.DevboxVersion)
if err != nil {
return usererr.New("Invalid devbox_version constraint in config: %s", cfg.DevboxVersion)
}

currentVersion, err := version.NewVersion(build.Version)
if err != nil {
return usererr.New("Invalid current devbox version: %s", build.Version)
}

if !constraints.Check(currentVersion) {
return usererr.New("Devbox version mismatch: project requires version %s but your running version is %s",
cfg.DevboxVersion, build.Version)
}

return nil
}

// SetDevboxVersion sets the devbox_version field in the config
func (c *ConfigFile) SetDevboxVersion(version string) {
c.DevboxVersion = version
c.SetStringField("DevboxVersion", version)
}
107 changes: 74 additions & 33 deletions internal/templates/template.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ import (

"go.jetify.com/devbox/internal/boxcli/usererr"
"go.jetify.com/devbox/internal/build"
"go.jetify.com/devbox/internal/devconfig"

"github.com/hashicorp/go-version"
)

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

func InitFromRepo(w io.Writer, repo, subdir, target string) error {
if err := createDirAndEnsureEmpty(target); err != nil {
return err
}
parsedRepoURL, err := ParseRepoURL(repo)
if err != nil {
return errors.WithStack(err)
}
if err := createDirAndEnsureEmpty(target); err != nil {
return err
}
parsedRepoURL, err := ParseRepoURL(repo)
if err != nil {
return errors.WithStack(err)
}

tmp, err := os.MkdirTemp("", "devbox-template")
if err != nil {
return errors.WithStack(err)
}
cmd := exec.Command(
"git", "clone", parsedRepoURL,
// Clone and checkout a specific ref
"-b", lo.Ternary(build.IsDev, "main", build.Version),
// Create shallow clone with depth of 1
"--depth", "1",
tmp,
)
fmt.Fprintf(w, "%s\n", cmd)
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
if err = cmd.Run(); err != nil {
return errors.WithStack(err)
}
tmp, err := os.MkdirTemp("", "devbox-template")
if err != nil {
return errors.WithStack(err)
}
cmd := exec.Command(
"git", "clone", parsedRepoURL,
// Clone and checkout a specific ref
"-b", lo.Ternary(build.IsDev, "main", build.Version),
// Create shallow clone with depth of 1
"--depth", "1",
tmp,
)
fmt.Fprintf(w, "%s\n", cmd)
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
if err = cmd.Run(); err != nil {
return errors.WithStack(err)
}

cmd = exec.Command(
"sh", "-c",
fmt.Sprintf("cp -r %s %s", filepath.Join(tmp, subdir, "*"), target),
)
fmt.Fprintf(w, "%s\n", cmd)
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
if err = cmd.Run(); err != nil {
return errors.WithStack(err)
}

cmd = exec.Command(
"sh", "-c",
fmt.Sprintf("cp -r %s %s", filepath.Join(tmp, subdir, "*"), target),
)
fmt.Fprintf(w, "%s\n", cmd)
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
return errors.WithStack(cmd.Run())
// Set the devbox version after initializing the template
return SetCurrentDevboxVersion(w, target)
}

func List(w io.Writer, showAll bool) {
Expand Down Expand Up @@ -105,3 +113,36 @@ func ParseRepoURL(repo string) (string, error) {
// like: https://github.com/jetify-com/devbox.git
return strings.TrimSuffix(repo, ".git"), nil
}

// SetCurrentDevboxVersion sets the current version as the required version in the config
func SetCurrentDevboxVersion(w io.Writer, projectDir string) error {
if strings.HasSuffix(projectDir, "devbox.json") {
projectDir = filepath.Dir(projectDir)
}

fmt.Println(projectDir)

cfg, err := devconfig.Open(projectDir)
if err != nil {
return errors.WithStack(err)
}
fmt.Printf("%v", cfg)

// Create a constraint like "~1.2.0" (compatible with 1.2.x)
currentVersion, err := version.NewVersion(build.Version)
if err != nil {
return errors.WithStack(err)
}

segments := currentVersion.Segments()
if len(segments) < 2 {
return errors.New("invalid version format")
}

// Create a constraint for the current major.minor version
versionConstraint := fmt.Sprintf("~%d.%d.0", segments[0], segments[1])

fmt.Fprintf(w, "Setting project devbox version constraint: %s\n", versionConstraint)
cfg.Root.SetDevboxVersion(versionConstraint)
return cfg.Root.SaveTo(cfg.Root.AbsRootPath)
}
Loading