Skip to content
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 .nextchanges/cli/dev-build-version.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
A locally built CLI (`go build`, without release flags) now reports the next release version with a `-dev` prerelease, e.g. `1.12.0-dev+abcdef123456`, instead of `0.0.0-dev+abcdef123456`. The old string sorted below every published release even though a local build is newer than the latest release; the new one sorts above the latest release and below the release it will become, matching what goreleaser already produces for snapshot builds.
21 changes: 21 additions & 0 deletions .nextchanges/nextversion.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
// Package nextchanges exposes the next release version to the build.
//
// It lives in this directory because go:embed cannot reach a parent directory:
// embedding the version file from internal/build would require a second copy of
// the value, which could then drift from this one.
package nextchanges

import (
_ "embed"
"strings"
)

// versionFile is the raw contents of the version file. It has a trailing newline
// (the whitespace linter requires one), so it is not exported directly.
//
//go:embed version
var versionFile string

// Version is the next release version, e.g. "1.12.0". The release tooling bumps
// the embedded file after each release; see README.md.
var Version = strings.TrimSpace(versionFile)
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ Deployment complete!
>>> print_state.py
{
"state_version": 2,
"cli_version": "[CLI_VERSION]",
"cli_version": "0.0.0-test",
"lineage": "test-lineage",
"serial": 2,
"state": {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"state_version": 1,
"cli_version": "0.0.0-dev",
"cli_version": "0.0.0-test",
"lineage": "test-lineage",
"serial": 1,
"state": {
Expand Down
2 changes: 1 addition & 1 deletion acceptance/bundle/telemetry/deploy/script
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ trace cat telemetry.json | jq ' .entry.databricks_cli_log.bundle_deploy_event.ex
# omit it from the engine-agnostic out.telemetry.txt below. Sizes are deterministic
# for a fixed config and asserted exactly. state_file_size_bytes is dropped because
# it is os.Stat of resources.json, whose header embeds the CLI version string
# (0.0.0-dev+<sha> on linux/macos vs 0.0.0-dev on windows).
# (<version>-dev+<sha> on linux/macos vs <version>-dev on windows).
cat telemetry.json | jq '.entry.databricks_cli_log.bundle_deploy_event.resources_metadata | if . then del(.state_file_size_bytes) else . end' > out.resources_metadata.$DATABRICKS_BUNDLE_ENGINE.txt

# The dry-run migration to the direct engine runs only after a terraform deploy,
Expand Down
4 changes: 2 additions & 2 deletions acceptance/cmd/bundle/dms-read-only/output.txt
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
>>> [CLI] bundle-deployments list-versions deployments/abc
[
{
"cli_version": "[CLI_VERSION]",
"cli_version": "0.0.0-test",
"name": "deployments/abc/versions/v1",
"status": "VERSION_STATUS_COMPLETED",
"version_type": ""
Expand All @@ -41,7 +41,7 @@

>>> [CLI] bundle-deployments get-version deployments/abc/versions/v1
{
"cli_version": "[CLI_VERSION]",
"cli_version": "0.0.0-test",
"git_info": {
"branch": "main",
"commit": "[COMMIT_SHA]",
Expand Down
4 changes: 2 additions & 2 deletions acceptance/cmd/bundle/dms-read-only/test.toml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ Response.Body = '''
"versions": [
{
"name": "deployments/abc/versions/v1",
"cli_version": "0.0.0-dev",
"cli_version": "0.0.0-test",
"status": "VERSION_STATUS_COMPLETED"
}
]
Expand All @@ -77,7 +77,7 @@ Pattern = "GET /api/2.0/bundle/deployments/abc/versions/v1"
Response.Body = '''
{
"name": "deployments/abc/versions/v1",
"cli_version": "0.0.0-dev",
"cli_version": "0.0.0-test",
"status": "VERSION_STATUS_COMPLETED",
"git_info": {
"branch": "main",
Expand Down
5 changes: 4 additions & 1 deletion bundle/config/mutator/verify_cli_version.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,10 @@ func (v *verifyCliVersion) Apply(ctx context.Context, b *bundle.Bundle) diag.Dia
}

if !c.Check(version) {
if version.Prerelease() == "dev" && version.Major() == 0 {
// Development builds are built from main and may already carry the change
// the constraint requires, so the constraint is a warning rather than an
// error. Masterminds reports the prerelease without its leading dash.
if version.Prerelease() == "dev" {
return diag.Warningf("Ignoring Databricks CLI version constraint for development build. Required: %s, current: %s", constraint, currentVersion)
}

Expand Down
4 changes: 3 additions & 1 deletion bundle/deploy/terraform/init_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import (
"github.com/databricks/cli/bundle"
"github.com/databricks/cli/bundle/config"
"github.com/databricks/cli/bundle/internal/tf/schema"
"github.com/databricks/cli/internal/build"
"github.com/databricks/cli/internal/testutil"
"github.com/databricks/cli/libs/env"
"github.com/hashicorp/go-version"
Expand Down Expand Up @@ -237,8 +238,9 @@ func TestSetUserAgentExtra_Python(t *testing.T) {
env := make(map[string]string, 0)
err := setUserAgentExtraEnvVar(env, b)
require.NoError(t, err)
// The CLI version is the test binary's own, which is the not-injected default.
assert.Equal(t, map[string]string{
"DATABRICKS_USER_AGENT_EXTRA": "cli/0.0.0-dev databricks-pydabs/0.7.0",
"DATABRICKS_USER_AGENT_EXTRA": "cli/" + build.DefaultSemver + " databricks-pydabs/0.7.0",
}, env)
}

Expand Down
36 changes: 35 additions & 1 deletion internal/build/info.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (
"sync"
"time"

nextchanges "github.com/databricks/cli/.nextchanges"
"golang.org/x/mod/semver"
)

Expand Down Expand Up @@ -42,7 +43,40 @@ func (i Info) GetSanitizedVersion() string {
return version
}

const DefaultSemver = "0.0.0-dev"
// devPrerelease marks a build that was not produced from a release tag.
const devPrerelease = "-dev"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can we export this and use in the other places?


// DefaultSemver is the version reported when buildVersion was not injected,
// i.e. a plain "go build" rather than a goreleaser build. It is the next release
// version with a -dev prerelease, so it sorts above the latest release and below
// the release it will become:
//
// Compare(v1.11.0, v1.12.0-dev+sha) = -1
// Compare(v1.12.0, v1.12.0-dev+sha) = +1
//
// A bare "0.0.0-dev" would instead sort below every published release, even
// though a local build is built from main and is therefore newer than the
// latest release. This matches what goreleaser produces for snapshot builds
// (see snapshot.version_template in .goreleaser.yaml).
var DefaultSemver = nextchanges.Version + devPrerelease

// IsDevelopmentVersion reports whether a version string is a development build's.
// It keys off the -dev prerelease rather than a specific version number, so it
// keeps working as the next release version changes. The version may be given
// with or without a leading "v".
//
// Prefer Info.IsDevelopment when you have the running build's Info; this is for
// callers holding only a version string (e.g. one read from a file or a
// parameter), so the definition of "development build" lives in one place.
func IsDevelopmentVersion(version string) bool {
return semver.Prerelease("v"+strings.TrimPrefix(version, "v")) == devPrerelease
}

// IsDevelopment reports whether this binary was built from a development or
// snapshot build rather than a release tag.
func (i Info) IsDevelopment() bool {
return i.IsSnapshot || IsDevelopmentVersion(i.Version)
}

// getDefaultBuildVersion uses build information stored by Go itself
// to synthesize a build version if one wasn't set.
Expand Down
105 changes: 105 additions & 0 deletions internal/build/info_test.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,120 @@
package build

import (
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/mod/semver"
)

func TestGetDetails(t *testing.T) {
GetInfo()
}

func TestIsDevelopment(t *testing.T) {
tests := []struct {
name string
info Info
want bool
}{
{"dev build with commit metadata", Info{Version: "1.12.0-dev+abc123"}, true},
{"dev build without metadata", Info{Version: "1.12.0-dev"}, true},
{"released version", Info{Version: "1.12.0"}, false},
// A release candidate is not a dev build: it is built from a tag, so the
// version constraints and update checks that dev builds bypass still apply.
{"release candidate", Info{Version: "1.12.0-rc.1"}, false},
// goreleaser marks snapshots explicitly, independent of the version string.
{"snapshot of a release version", Info{Version: "1.12.0", IsSnapshot: true}, true},
{"malformed version", Info{Version: "not-a-version"}, false},
}

for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
assert.Equal(t, tt.want, tt.info.IsDevelopment())
})
}
}

func TestIsDevelopmentVersion(t *testing.T) {
tests := []struct {
version string
want bool
}{
{"1.12.0-dev+abc123", true},
{"1.12.0-dev", true},
// Accepted with or without a leading "v", since callers hold versions in
// both forms (the schema's minimum version is v-prefixed).
{"v1.12.0-dev", true},
{"1.12.0", false},
{"v1.12.0", false},
{"1.12.0-rc.1", false},
{"not-a-version", false},
{"", false},
}

for _, tt := range tests {
t.Run(tt.version, func(t *testing.T) {
assert.Equal(t, tt.want, IsDevelopmentVersion(tt.version))
})
}
}

// TestVersionOrdering pins the full ordering model this version scheme exists
// for. A dev build is built from main, so it must sort ABOVE the release it
// followed and BELOW the release it will become. The old "0.0.0-dev" scheme
// violated this: it sorted below every release, including bare "0.0.0".
//
// The list is in strictly ascending order; the test asserts every pair, so it
// covers both the neighbouring steps and the transitive relationships.
func TestVersionOrdering(t *testing.T) {
ascending := []string{
"0.0.0-dev",
"0.0.0",
"1.11.0-dev",
"1.11.0",
// Build metadata is not part of precedence, so this is EQUAL to the
// bare 1.12.0-dev below and must sit between 1.11.0 and 1.12.0.
"1.12.0-dev+abc123",
"1.12.0-rc.1",
"1.12.0",
"1.12.1-dev",
"1.12.1",
"2.0.0-dev",
"2.0.0",
}

// Build metadata is ignored for precedence, so this pair compares equal.
require.Zero(t, semver.Compare("v1.12.0-dev+abc123", "v1.12.0-dev"))

for i, lower := range ascending {
require.True(t, semver.IsValid("v"+lower), "%q must be valid semver", lower)
assert.Zero(t, semver.Compare("v"+lower, "v"+lower), "%q must equal itself", lower)

for _, higher := range ascending[i+1:] {
// 1.12.0-dev+abc123 and 1.12.0-rc.1 are adjacent in the list but
// -dev sorts below -rc alphabetically, so they are still ordered.
assert.Negative(t, semver.Compare("v"+lower, "v"+higher), "%q must sort below %q", lower, higher)
assert.Positive(t, semver.Compare("v"+higher, "v"+lower), "%q must sort above %q", higher, lower)
}
}
}

// TestDefaultSemverSortsAboveLastRelease applies the ordering above to the
// version an actual local build reports, which TestVersionOrdering cannot do
// because DefaultSemver tracks .nextchanges/version.
func TestDefaultSemverSortsAboveLastRelease(t *testing.T) {
v := "v" + DefaultSemver
require.True(t, semver.IsValid(v), "DefaultSemver %q must be valid semver", DefaultSemver)
require.Equal(t, devPrerelease, semver.Prerelease(v))

// The release this dev build will become, e.g. v1.12.0 for 1.12.0-dev.
next := strings.TrimSuffix(v, devPrerelease)
assert.Positive(t, semver.Compare(next, v), "the upcoming release must sort above the dev build")
assert.True(t, Info{Version: DefaultSemver}.IsDevelopment())
}

func TestGetSanitizedVersion(t *testing.T) {
tests := []struct {
name string
Expand Down
2 changes: 1 addition & 1 deletion libs/aitools/installer/installer.go
Original file line number Diff line number Diff line change
Expand Up @@ -463,7 +463,7 @@ func incompatibleAgentNames(targetAgents []*agents.Agent) []string {
func resolveSkills(ctx context.Context, skills map[string]SkillMeta, opts InstallOptions) (map[string]SkillMeta, error) {
isSpecific := len(opts.SpecificSkills) > 0
cliVersion := build.GetInfo().Version
isDev := strings.HasPrefix(cliVersion, build.DefaultSemver)
isDev := build.GetInfo().IsDevelopment()

// Start with all skills or only the requested ones.
var candidates map[string]SkillMeta
Expand Down
2 changes: 1 addition & 1 deletion libs/aitools/installer/update.go
Original file line number Diff line number Diff line change
Expand Up @@ -114,7 +114,7 @@ func UpdateSkills(ctx context.Context, src ManifestSource, targetAgents []*agent
result := &UpdateResult{}

cliVersion := build.GetInfo().Version
isDev := strings.HasPrefix(cliVersion, build.DefaultSemver)
isDev := build.GetInfo().IsDevelopment()

// Sort skill names for deterministic output.
names := slices.Sorted(maps.Keys(skillSet))
Expand Down
16 changes: 6 additions & 10 deletions libs/clicompat/clicompat.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,11 +47,6 @@ const (
// localManifestFile is the filename for the locally cached manifest.
localManifestFile = "compat-manifest.json"

// devVersionPrefix identifies dev builds whose semver (0.0.0) is lower than
// all real CLI versions. These are treated as bleeding-edge and resolve to
// the highest versioned entry.
devVersionPrefix = "0.0.0-dev"

maxFetchAttempts = 3
fetchRetryBackoff = 300 * time.Millisecond
)
Expand Down Expand Up @@ -208,7 +203,7 @@ func IsNotFoundError(err error) bool {
// and all versions above it, up to (but not including) the next entry.
//
// Resolution order:
// 1. Dev builds (version starts with "0.0.0-dev") use the highest versioned entry.
// 1. Dev builds (a -dev prerelease) use the highest versioned entry.
// 2. Exact match on CLI version.
// 3. Nearest lower version (semver-sorted). This also handles CLI versions
// newer than all entries, returning the highest known entry.
Expand All @@ -224,10 +219,11 @@ func Resolve(m Manifest, cliVersion string) (Entry, error) {
return Entry{}, errors.New("compatibility manifest has no versioned entries")
}

// Dev builds (0.0.0-dev*) have semver lower than all real CLI versions,
// so they would incorrectly resolve to the lowest entry. Use the highest
// versioned entry instead, since dev builds represent the bleeding edge.
if strings.HasPrefix(cliVersion, devVersionPrefix) {
// Dev builds are built from main and may carry changes that no released
// version has, so resolving them by semver would pick the entry for the
// previous release. Use the highest versioned entry instead, since dev
// builds represent the bleeding edge.
if build.IsDevelopmentVersion(cliVersion) {
return m[versions[0]], nil
}

Expand Down
5 changes: 3 additions & 2 deletions libs/jsonschema/schema.go
Original file line number Diff line number Diff line change
Expand Up @@ -270,8 +270,9 @@ func (s *Schema) validateSchemaMinimumCliVersion(currentVersion string) func() e
return nil
}

// Ignore this validation rule for local builds.
if semver.Compare("v"+build.DefaultSemver, currentVersion) == 0 {
// Ignore this validation rule for development builds, which are built from
// main and may already carry the change the schema requires.
if build.IsDevelopmentVersion(currentVersion) {
return nil
}

Expand Down
2 changes: 1 addition & 1 deletion libs/versioncheck/notice.go
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ func gatherConditions(ctx context.Context, cmd *cobra.Command) notifyConditions
nonInteractive := !cmdio.HasIO(ctx) || cmdio.GetInteractiveMode(ctx) == cmdio.InteractiveModeNone
onRuntime := dbr.HasDetection(ctx) && dbr.RunsOnRuntime(ctx)
return notifyConditions{
developmentBuild: isDevelopmentBuild(build.GetInfo()),
developmentBuild: build.GetInfo().IsDevelopment(),
cacheDisabled: cacheDisabled,
optedOut: optedOut,
onRuntime: onRuntime,
Expand Down
9 changes: 1 addition & 8 deletions libs/versioncheck/versioncheck.go
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ type Result struct {
// compare against, so they short-circuit without a network call.
func Check(ctx context.Context) *Result {
info := build.GetInfo()
if isDevelopmentBuild(info) {
if info.IsDevelopment() {
return &Result{
CurrentVersion: info.Version,
DevelopmentBuild: true,
Expand Down Expand Up @@ -110,13 +110,6 @@ func Check(ctx context.Context) *Result {
}
}

// isDevelopmentBuild reports whether the binary was not built from a tagged
// release. Snapshot builds (goreleaser --snapshot) and local `go build`
// binaries (version 0.0.0-dev+<sha>) fall into this category.
func isDevelopmentBuild(info build.Info) bool {
return info.IsSnapshot || strings.HasPrefix(info.Version, "0.0.0")
}

// isNewer reports whether latest is a higher semver than current. Both are
// bare versions without a leading "v".
func isNewer(current, latest string) bool {
Expand Down
Loading
Loading