Skip to content

feat(config): add devbox_version to require a devbox version - #2990

Merged
mikeland73 merged 4 commits into
mainfrom
mikeland73/devbox-version-constraint
Sep 28, 2026
Merged

mikeland73 merged 4 commits into
mainfrom
mikeland73/devbox-version-constraint

Conversation

@mikeland73

Copy link
Copy Markdown
Collaborator

Summary

Adds a top-level devbox_version field to devbox.json that declares which devbox versions a project supports. Addresses #1371.

// Shorthand: a semver constraint that warns on mismatch
"devbox_version": "^0.18.0"

// Object form with an explicit policy
"devbox_version": {"version": "0.18.4", "on_mismatch": "error"}
  • Constraints use npm-style syntax via Masterminds/semver/v3: 0.18.4, ^0.18.0, ~0.18.1, >=0.17.0 <0.19.0, 0.18.x, ||. The library was already an indirect dependency and is now direct; vendor-hash is unchanged. An invalid constraint or policy is a config load error.
  • warn (default) prints the warning once per project per process tree, so nested devbox commands don't repeat it, then continues.
  • error fails before the command does any work.
  • The message names the required and running versions, and suggests DEVBOX_USE_VERSION=<version> for exact pins or devbox version update for ranges.
  • DEVBOX_VERSION_POLICY=off|warn|error overrides the policy in devbox.json, as an escape hatch for CI and emergencies.
  • Where the check runs: in devbox.Open, right after the config loads and before the lockfile is read. That covers every command that opens a project, including devbox global.
  • Commands that don't open a project (init, search, version, …) and dev builds skip it. Only the root config's field is used; it is ignored in plugins and includes.
  • devbox init doesn't add the field.
  • The JSON schema is updated, since it sets additionalProperties: false.

Devbox releases from before this change ignore the field, so enforcement starts with the release that ships it. The follow-up PR adds an auto policy that switches to the pinned version.

How was it tested?

  • Unit tests: internal/devconfig/configfile/devbox_version_test.go covers parsing, round-trips and validation. internal/vercheck/project_test.go covers the policies, warning dedupe, the env override, and dev builds.
  • testscripts/version/devbox_version.test.txt covers warn, error, the off override, a satisfied constraint, and an invalid constraint. It sets DEVBOX_PROD=1 so the check isn't skipped for dev builds.
  • go test ./internal/... ./pkg/... and devbox run lint pass.

Community Contribution License

All community contributions in this pull request are licensed to the project
maintainers under the terms of the
Apache 2 License.

By creating this pull request, I represent that I have the right to license the
contributions to the project maintainers under the Apache 2 License as stated in
the
Community Contribution License.

🤖 Generated with Claude Code

@mikeland73

Copy link
Copy Markdown
Collaborator Author

@claude ptal

@mikeland73

Copy link
Copy Markdown
Collaborator Author

@claude review

@claude

claude Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Claude finished @mikeland73's task in 5m 25s —— View job


Code review: devbox_version support

Todo list

  • Gather context (read diff, changed files, CLAUDE.md — no CLAUDE.md in this repo)
  • Review internal/devconfig/configfile/devbox_version.go
  • Review internal/vercheck/project.go
  • Review integration point in internal/devbox/devbox.go
  • Review schema, env, tests
  • Post final review feedback

Overall this is a clean, well-scoped implementation. Test coverage for parsing, round-tripping, policy resolution, and the env override is thorough, and the design decisions (checking only the root config, skipping dev builds, DEVBOX_VERSION_POLICY as an escape hatch) all match the PR description.

Finding: warn-dedupe only remembers the last project, not all of them

internal/vercheck/project.go:64-68:

if os.Getenv(warnedEnvName) == configPath {
    return nil
}
ux.Fwarningf(w, "%s\n", msg)
return os.Setenv(warnedEnvName, configPath)

This stores the value of a single fixed-name env var (__DEVBOX_VERSION_MISMATCH_WARNED) to remember which project already warned. If a devbox process's exec chain opens two different projects (e.g. a nested devbox invocation for project B spawned from within project A's shell/init hook), returning to project A later in the same process tree will warn again — the env var no longer holds A's path, it holds B's.

The codebase already has an established pattern for exactly this kind of per-project "have I done this already" marker: suffix the env var name with a hash of the project dir, so each project gets its own independent slot — see SkipInitHookEnvName() in internal/devbox/envvars.go:221-222 ("__DEVBOX_SKIP_INIT_HOOK_" + d.ProjectDirHash()) and shellEnvHashKey in internal/devbox/devbox.go:433, both built on cachehash.Bytes(...). Using the same approach here (e.g. warnedEnvName + "_" + cachehash.Bytes([]byte(configPath)) set to a sentinel value) would avoid the collision without changing the observable behavior for the common single-project case.

Fix this →

Minor notes (not blocking)

  • internal/devconfig/configfile/devbox_version.go:80-86 (ExactVersion): an explicit-equals constraint like "=0.18.4" is semantically an exact pin, but ExactVersion() doesn't strip the leading = before calling semver.StrictNewVersion, so it falls through to the generic "range" mismatch message instead of the more precise DEVBOX_USE_VERSION=0.18.4 suggestion. Covered by TestDevboxVersionExactVersion's existing expectations, so this is a known/tested gap rather than a bug — just a small UX polish opportunity if you want exact-equals constraints to get the sharper message.
  • os.Setenv/os.Getenv in CheckProjectVersion are unsynchronized global state. Fine today since devbox.Open runs once per process, but worth keeping in mind if project-opening is ever parallelized.

Testing

I wasn't able to run go build/go test/devbox run lint in this sandbox (tool permissions block go invocations here), so I reviewed by reading the code and tests directly rather than executing them. The unit tests and testscript in the diff look correct and exercise the warn/error/off/satisfied/invalid paths well; the schema update in .schema/devbox.schema.json matches the Go-side validation (oneOf string/object, on_mismatch enum of warn/error only, additionalProperties: false).

@claude

claude Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Claude finished @mikeland73's task in 4m 12s —— View job


Claude's review

Todo list

  • Gather context (read diff, changed files, CLAUDE.md)
  • Review internal/devconfig/configfile/devbox_version.go + tests
  • Review internal/vercheck/project.go + tests
  • Review internal/devbox/devbox.go integration point
  • Review schema, env, testscript changes
  • Post final review feedback

Overall this is a clean, well-tested implementation — the shorthand/object JSON handling, the root-config-only scoping, the per-process warning dedupe, and the config validation are all solid. A few things worth a look before merging:

1. The mismatch message's suggested fix can be wrong for range constraints (internal/vercheck/project.go:74-84)

if exact, ok := required.ExactVersion(); ok {
    fmt.Fprintf(&msg, "Set %s=%s to run the required version.", envir.DevboxUseVersion, exact)
} else {
    fmt.Fprintf(
        &msg,
        "Run `devbox version update`, or set %s to a version that satisfies %q.",
        envir.DevboxUseVersion, required.Version,
    )
}

For a range like ">=0.17.0 <0.19.0", this always leads with devbox version update — but that command always jumps to the latest release. If the running version is above the upper bound (e.g. user is on 0.20.0), running devbox version update won't fix anything (it can't go backwards), yet it's presented as the primary fix. The DEVBOX_USE_VERSION alternative is the only one that actually works in that case. Tests only cover the "running version is too old" direction (0.17.2 vs ^0.18.0), so this asymmetry isn't caught. Consider only suggesting devbox version update when the current version is below the constraint's lower bound.

Fix this →

2. DEVBOX_USE_VERSION isn't consumed anywhere in this repo

envir.DevboxUseVersion (internal/envir/env.go:21-22) is new, and a repo-wide search shows it's only ever referenced in the mismatch message text and in tests asserting that text — nothing in this codebase reads it to actually select/launch a pinned devbox binary. The comment says "tells the launcher which devbox version to run," implying this is meant to be picked up by the external launcher script (launch.sh, referenced in internal/vercheck/vercheck.go:29-31), which lives outside this repo. Worth confirming that launcher support for DEVBOX_USE_VERSION is already shipped (or shipping alongside this), otherwise the error/warning message tells users to set a variable that currently has no effect.

3. Possible prerelease-matching gotcha with Masterminds/semver

Masterminds/semver/v3's documented behavior is that a pre-release version (e.g. 0.19.0-rc1) does not satisfy a plain range constraint (e.g. ^0.18.0) unless the constraint itself specifies a pre-release for the same major.minor.patch. If devbox ever ships a pre-release/rc binary, CheckProjectVersion (internal/vercheck/project.go:44) could report a false mismatch even though the version is numerically in range. The satisfied.json testscript case only exercises an exact-match constraint ("0.0.0-dev" against "0.0.0-dev"), which sidesteps this because the constraint and version share the same pre-release tag — it doesn't exercise a range constraint against a pre-release running version. Worth a quick check if this matters for your release process.

Minor notes

  • internal/vercheck/project.go:36-37: the dedupe env var stores only the last checked config path, so if a process alternates between two projects (not likely in practice, but possible with tooling that opens multiple projects) both will re-warn each time it's not the most recent one. Fine given the stated goal ("dedupe within a nested command tree"), just noting the boundary.
  • Nice touch keeping off rejectable in devbox.json but valid only via DEVBOX_VERSION_POLICY (internal/devconfig/configfile/devbox_version.go:98-101) — that's a good, deliberate constraint.

Testing

I wasn't able to run go test ./... or devbox run lint in this sandboxed review environment (build/network commands require interactive approval I don't have here), so this is a static review only. The included unit tests and testscript look thorough for the cases they cover.

@mikeland73
mikeland73 added this pull request to stack #2992 September 28, 2026 18:00
@mikeland73

Copy link
Copy Markdown
Collaborator Author

@claude review

@claude

claude Bot commented Sep 28, 2026 •

Copy link
Copy Markdown

Claude finished @mikeland73's task in 17s —— View job


Review of devbox_version support

I read the code statically and did not run the build or tests. The implementation is clean and well scoped. Two of the earlier automated review points are already addressed by the latest commits, and one is still open.

Already addressed

  • The devbox version update suggestion is now only shown when the latest release satisfies the constraint (latestSatisfies, internal/vercheck/project.go). This was commit 749561b.
  • Incidental opens no longer trigger the check. Telemetry and shell completion use SkipVersionCheck, and run --list now returns the error instead of swallowing it. This was commit 50c2729.
  • DEVBOX_USE_VERSION is consumed by the external launcher. It is also referenced in the Docker image templates, so the message points at a real knob.

Still open

  1. The warn dedupe only remembers the last project (internal/vercheck/project.go:64-68).

    • __DEVBOX_VERSION_MISMATCH_WARNED holds a single path. If project A's process tree opens B and then returns to A, A warns again.
    • The repo's usual pattern is a per-project env var name, as in SkipInitHookEnvName() ("__DEVBOX_SKIP_INIT_HOOK_" + hash). Something like warnedEnvName + "_" + cachehash.Bytes([]byte(configPath)) set to "1" would fix it.
    • This is low severity. It only affects trees that alternate between projects, and the cost is a repeated warning.
    • The comment on warnedEnvName documents the "last project" behavior, so it would need updating if you change this.
  2. Pre-release running versions may not satisfy range constraints.

    • Masterminds/semver does not match a pre-release like 0.19.0-rc1 against ^0.18.0 or >=0.18.0, even when the numbers are in range.
    • That would give a false warning, or a false error under the error policy, if devbox ever ships rc builds.
    • The testscript only covers an exact pin against 0.0.0-dev. If rc builds are possible, consider stripping the pre-release before checking (current.SetPrerelease("")) or adding a test for this case.

Minor

  • ExactVersion() doesn't treat =0.18.4 as an exact pin, so it gets the range-style message. This is cosmetic.
  • os.Setenv in CheckProjectVersion is process-global state. That's fine today.

Neither open item blocks the merge.

mikeland73 and others added 3 commits September 28, 2026 12:00
Add a top-level `devbox_version` field to devbox.json that declares which
devbox versions a project supports (#1371). It accepts a semver constraint
string, or an object with an explicit mismatch policy:

    "devbox_version": "^0.18.0"
    "devbox_version": {"version": "0.18.4", "on_mismatch": "error"}

When the running devbox doesn't satisfy the constraint, "warn" (the default)
prints a warning once per project per process tree and "error" fails the
command. DEVBOX_VERSION_POLICY=off|warn|error overrides the policy.

The check runs in devbox.Open right after the config loads, before the
lockfile is read, so it covers every command that opens a project
(including `devbox global`). Development builds skip it. Constraints use
npm-style syntax via Masterminds/semver, which moves from an indirect to a
direct dependency.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
For range constraints, the mismatch message always suggested
`devbox version update`, but that only moves to the latest release. If the
running version is already past the constraint's upper bound, or the latest
release doesn't satisfy it, updating can't fix the mismatch. Now we only
suggest it when the latest version (DEVBOX_LATEST_VERSION) satisfies the
constraint, and otherwise point at DEVBOX_USE_VERSION alone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
…ntally

Two places opened the current directory's project on every devbox
invocation, so the devbox_version check fired for commands that don't use
the project (e.g. `devbox version` printed the mismatch warning), and a
swallowed error let `devbox run --list` ignore the "error" policy:

- `devbox run` computed ValidArgs eagerly while building the command tree.
  It's now a lazy ValidArgsFunction that only runs during shell completion,
  which also lets cobra parse --config for us. listScripts now returns its
  error, so `run --list` and bare `run` report it.
- The telemetry middleware opens the project after every command to record
  package names.

Add devopt.Opts.SkipVersionCheck for these incidental opens (telemetry and
completion), and cover `devbox version` and `run --list` in the testscript.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mikeland73
mikeland73 force-pushed the mikeland73/devbox-version-constraint branch from 50c2729 to 4331a49 Compare September 28, 2026 19:00
…lepp/lapp stacks

The lepp-stack run_test flaked in CI with "the database system is starting
up" because it assumed postgres was ready after a fixed 2s sleep. Poll with
pg_isready (up to 30s) instead.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mikeland73
mikeland73 merged commit 87b2e72 into main Sep 28, 2026
28 checks passed
@mikeland73
mikeland73 deleted the mikeland73/devbox-version-constraint branch September 28, 2026 22:23
mikeland73 added a commit that referenced this pull request Sep 28, 2026
…sion-auto

main has the squash-merged devbox_version change (#2990). Conflicts in the
devbox_version files are add/add conflicts where this branch's version is
main's plus the "auto" policy, so this branch's side is kept.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
mikeland73 added a commit that referenced this pull request Sep 28, 2026
)

## Summary

Stacked on #2990, which adds `devbox_version`. Merge that first.

Adds an `auto` policy that switches to the pinned devbox version:

```json
"devbox_version": {"version": "0.18.4", "on_mismatch": "auto"}
```

- **How switching works:** when the running devbox doesn't match, it
re-runs the same command through the devbox launcher (`$LAUNCHER_PATH`)
with `DEVBOX_USE_VERSION` set to the pinned version.
- The existing launcher already downloads, checksum-verifies and runs
any release named by `DEVBOX_USE_VERSION`, so the launcher needs no
changes.
- Download progress goes to stderr, so `shellenv` and direnv output
stays clean.
- **Exact versions only:** `auto` requires an exact version; a range
with `auto` is a config error. `DEVBOX_VERSION_POLICY=auto` with a range
fails like `error`.
- **A `DEVBOX_USE_VERSION` set by the user wins:** devbox warns instead
of switching.
- **Loop guard:** `__DEVBOX_AUTO_VERSION` marks a version that `auto`
chose. That lets a nested shell switch again for a different project,
and makes devbox fail instead of looping if the launcher doesn't switch.
- **Without a launcher** (for example Nix or Homebrew installs), `auto`
fails with install instructions.
- **Inheritance:** `DEVBOX_USE_VERSION` carries into `devbox shell`, so
nested devbox commands stay on the pinned version.

## How was it tested?

- Unit tests with a mocked `exec` cover: switching, replacing existing
env values, switching again for a different project, no launcher, the
loop guard, a user-set version winning, and the env override with a
range.
- `testscripts/version/devbox_version_auto.test.txt` uses a fake
launcher script to check that the same command is re-run with
`DEVBOX_USE_VERSION` set. It also covers the no-launcher error, a
user-set version winning, and rejecting ranges.
- Manual end-to-end run with the real launcher (v0.2.2): a binary built
as 0.17.1, run in a project pinned to `0.18.4` with `auto`, ran `devbox
run` under 0.18.4. Inside the script, `devbox version` printed 0.18.4
and `DEVBOX_USE_VERSION=0.18.4`.
- `go test ./internal/...` and `devbox run lint` pass.

## Community Contribution License

All community contributions in this pull request are licensed to the
project
maintainers under the terms of the
[Apache 2 License](https://www.apache.org/licenses/LICENSE-2.0).

By creating this pull request, I represent that I have the right to
license the
contributions to the project maintainers under the Apache 2 License as
stated in
the
[Community Contribution
License](https://github.com/jetify-com/opensource/blob/main/CONTRIBUTING.md#community-contribution-license).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
@mikeland73 mikeland73 linked an issue Sep 28, 2026 that may be closed by this pull request
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: add devbox required version to devbox.json

1 participant