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
61 changes: 60 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,65 @@ func (i Info) GetSanitizedVersion() string {
return version
}

const DefaultSemver = "0.0.0-dev"
// devIdentifier is the prerelease identifier marking a build that was not
// produced from a release tag.
const devIdentifier = "dev"

// 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 identifier, 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 = devVersion(nextchanges.Version)

// devVersion returns the development-build version for an upcoming release,
// normally a plain version with a dev prerelease ("1.12.0" -> "1.12.0-dev").
//
// The prerelease branch is for completeness with next_release_version() in
// internal/genkit/tagging.py, which bumps the prerelease instead of the minor
// when .nextchanges/version already carries one; it is not because this repo
// publishes prereleases (it never has). Appending "-dev" to "1.13.0-rc.2" would
// produce the prerelease "rc.2-dev", a single identifier that merely ends in
// "dev", so IsDevelopmentVersion would report false and every dev-build
// exemption would silently switch off. A dot-separated identifier
// ("1.13.0-rc.2.dev") keeps detection working and still sorts after the rc and
// before the final release.
func devVersion(next string) string {
if semver.Prerelease("v"+next) != "" {
return next + "." + devIdentifier
}
return next + "-" + devIdentifier
}

// IsDevelopmentVersion reports whether a version string is a development build's.
// It keys off the trailing dev prerelease identifier 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 {
prerelease := strings.TrimPrefix(semver.Prerelease("v"+strings.TrimPrefix(version, "v")), "-")
if prerelease == "" {
return false
}
identifiers := strings.Split(prerelease, ".")
return identifiers[len(identifiers)-1] == devIdentifier
}

// 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
151 changes: 151 additions & 0 deletions internal/build/info_test.go
Original file line number Diff line number Diff line change
@@ -1,15 +1,166 @@
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},
// A dev build on a prerelease release track; see devVersion.
{"1.13.0-rc.2.dev", true},
{"1.12.0", false},
{"v1.12.0", false},
{"1.12.0-rc.1", false},
// "rc.2-dev" is a single identifier that merely ends in "dev", not a dev
// marker. This is what naively appending "-dev" to a prerelease produced.
{"1.13.0-rc.2-dev", 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",
// A dev build off a prerelease sorts after the rc it follows and before
// the final release; see devVersion for why this shape exists.
"1.12.0-rc.1.dev",
"1.12.0-rc.2",
"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)
}
}
}

// TestDevVersion covers both shapes .nextchanges/version can take. The
// prerelease case is for completeness with next_release_version() in
// internal/genkit/tagging.py, which bumps a prerelease when the file carries
// one; this repo has never published a prerelease.
func TestDevVersion(t *testing.T) {
tests := []struct {
next string
want string
// finalRelease is the release the dev build must sort below: the version
// itself on a stable track, or the release the prerelease leads up to.
finalRelease string
}{
{"1.12.0", "1.12.0-dev", "1.12.0"},
{"2.0.0", "2.0.0-dev", "2.0.0"},
// Appending "-dev" here would yield the prerelease "rc.2-dev", which is
// not a dev marker, so IsDevelopmentVersion would report false and every
// dev-build exemption would silently switch off.
{"1.13.0-rc.2", "1.13.0-rc.2.dev", "1.13.0"},
}

for _, tt := range tests {
t.Run(tt.next, func(t *testing.T) {
got := devVersion(tt.next)
assert.Equal(t, tt.want, got)
require.True(t, semver.IsValid("v"+got), "%q must be valid semver", got)
assert.True(t, IsDevelopmentVersion(got), "%q must be recognized as a dev build", got)
// The dev build sits below the release it will become, and above the
// prerelease it already contains, if any.
assert.Negative(t, semver.Compare("v"+got, "v"+tt.finalRelease), "%q must sort below %q", got, tt.finalRelease)
if tt.next != tt.finalRelease {
assert.Positive(t, semver.Compare("v"+got, "v"+tt.next), "%q must sort above %q", got, tt.next)
}
})
}
}

// 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.True(t, IsDevelopmentVersion(DefaultSemver))
assert.True(t, Info{Version: DefaultSemver}.IsDevelopment())

// The release this dev build will become, e.g. v1.12.0 for 1.12.0-dev.
next := "v" + strings.TrimSuffix(strings.TrimSuffix(DefaultSemver, "-"+devIdentifier), "."+devIdentifier)
require.NotEqual(t, v, next, "DefaultSemver must end with the dev identifier")
assert.Positive(t, semver.Compare(next, v), "the upcoming release must sort above the dev build")
}

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
Loading
Loading