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
10 changes: 5 additions & 5 deletions .github/workflows/check.yml
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,11 @@ jobs:
- name: Fail if go.mod/go.sum changed
run: git diff --exit-code

- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
version: "0.8.9"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

We now run uv find python from the Taskfile instead of assuming the right version exists.

This means we need to make sure uv is available before using the Taskfile.

- name: Run Go lint checks (does not include formatting checks)
run: go tool -modfile=tools/task/go.mod task lint

Expand All @@ -44,11 +49,6 @@ jobs:
version: "0.9.1"
args: "format --check"

- name: Install uv
uses: astral-sh/setup-uv@08807647e7069bb48b6ef5acd8ec9567f424441b # v8.1.0
with:
version: "0.8.9"

- name: "task fmt: Python and Go formatting"
# Python formatting is already checked above, but this also checks Go and YAML formatting
run: |
Expand Down
6 changes: 4 additions & 2 deletions Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ vars:
# a separate static `**/testdata/**` glob, not this script.
# Limitation: git grep only scans tracked files; new //go:embed directives in
# untracked files are missed until the file is staged or committed.
# Run via uv with a pinned interpreter floor so the helper works on hosts whose
# default python3 is older than the 3.11 this repo's scripts target.
EMBED_SOURCES:
sh: 'python3 tools/list_embeds.py'
sh: 'uv run -p ">=3.11" --no-project python tools/list_embeds.py'

# pydabs-* tasks live in python/Taskfile.yml so `task pydabs-foo` works when
# run from python/. Flattened so they keep their `pydabs-` names at the root.
Expand Down Expand Up @@ -948,7 +950,7 @@ tasks:
generates:
- bundle/direct/dresources/apitypes.generated.yml
cmds:
- "sh -c 'python3 bundle/direct/tools/generate_apitypes.py .codegen/cli.json acceptance/bundle/refschema/out.fields.txt > bundle/direct/dresources/apitypes.generated.yml'"
- "sh -c 'uv run -p \">=3.11\" --no-project python bundle/direct/tools/generate_apitypes.py .codegen/cli.json acceptance/bundle/refschema/out.fields.txt > bundle/direct/dresources/apitypes.generated.yml'"

generate-direct-resources:
desc: Generate direct engine resources YAML
Expand Down
23 changes: 23 additions & 0 deletions acceptance/acceptance_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,14 @@ func testAccept(t *testing.T, inprocessMode bool, singleTest string) int {
os.Unsetenv(v) //nolint:usetesting // t.Setenv cannot unset
}

// Verify external tool prerequisites before doing any work, so a stale
// toolchain fails fast with an actionable message instead of producing
// confusing diffs deep into the run.
internal.RequireModernJq(t)
internal.RequireModernUv(t)
internal.RequireModernRuff(t)
internal.EnsureModernPython(t)

buildDir := getBuildDir(t, cwd, runtime.GOOS, runtime.GOARCH)

// Set up terraform for tests. Skip on DBR - tests with RunsOnDbr only use direct deployment.
Expand Down Expand Up @@ -759,6 +767,21 @@ func runTest(t *testing.T,
// into compared output. Tests can override this via [Env] in test.toml.
cmd.Env = append(cmd.Env, "DATABRICKS_CLI_DISABLE_UPDATE_CHECK=true")

// Neutralize Databricks-internal development-environment interference so
// acceptance tests behave the same as on CI (which has none of this). Two
// sources both reach the blocking proxy on every git invocation:
//
// 1. A command-timing shim that wraps git (ahead of the real binary on
// PATH) and POSTs per-command metrics over the network.
// COMMAND_TIMER_DISABLE=1 makes it pass through without the beacon.
// 2. A managed global git config installs a core.hooksPath whose hooks
// (secret scanning, etc.) also beacon metrics. Ignoring the global and
// system git config disables those hooks and keeps tests hermetic; tests
// configure the repos they create via git-repo-init locally.
cmd.Env = append(cmd.Env, "COMMAND_TIMER_DISABLE=1")
cmd.Env = append(cmd.Env, "GIT_CONFIG_GLOBAL="+os.DevNull)
cmd.Env = append(cmd.Env, "GIT_CONFIG_SYSTEM="+os.DevNull)

for _, kv := range testEnv {
key, value, _ := strings.Cut(kv, "=")
// Only add replacement by default if value is part of EnvMatrix with more than 1 option and length is 4 or more chars
Expand Down
32 changes: 32 additions & 0 deletions acceptance/internal/jq.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package internal

import (
"fmt"
"os/exec"
"strings"
"testing"
)

// RequireModernJq fails the run if jq is missing or older than 1.7. Acceptance
// scripts use jq 1.7 features (the pick/1 builtin and the `.foo.[]` iteration
// syntax); an older jq compiles them as errors and produces spurious diffs
// across many tests rather than one clear failure.
func RequireModernJq(t *testing.T) {
out, err := exec.Command("jq", "--version").Output()
if err != nil {
t.Fatalf("jq not found on PATH (acceptance tests require jq >= 1.7): %v", err)
}
version := strings.TrimSpace(string(out))
if !jqVersionOK(version) {
t.Fatalf("acceptance tests require jq >= 1.7 (found %q); install a newer jq", version)
}
}

// jqVersionOK reports whether `jq --version` output (e.g. "jq-1.7.1") is >= 1.7.
func jqVersionOK(version string) bool {
var major, minor int
if _, err := fmt.Sscanf(version, "jq-%d.%d", &major, &minor); err != nil {
return false
}
return major > 1 || (major == 1 && minor >= 7)
}
17 changes: 17 additions & 0 deletions acceptance/internal/jq_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package internal

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestJqVersionOK(t *testing.T) {
assert.True(t, jqVersionOK("jq-1.7"))
assert.True(t, jqVersionOK("jq-1.7.1"))
assert.True(t, jqVersionOK("jq-1.8.1"))
assert.True(t, jqVersionOK("jq-2.0"))
assert.False(t, jqVersionOK("jq-1.6"))
assert.False(t, jqVersionOK("jq version 1.7"))
assert.False(t, jqVersionOK(""))
}
42 changes: 42 additions & 0 deletions acceptance/internal/python.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
package internal

import (
"os"
"os/exec"
"path/filepath"
"runtime"
"strings"
"testing"
)

// EnsureModernPython makes `python3` on PATH resolve to a Python >= 3.11, the
// version this repo's scripts target. Acceptance scripts invoke `python3`
// directly and some import stdlib modules added in 3.11 (e.g. tomllib in
// acceptance/bundle/resources/permissions/analyze_requests.py), but a host's
// default python3 may be older. uv (already required for building the
// databricks-bundles wheel) discovers or provisions a suitable interpreter; we
// symlink it as python3/python into a temp dir prepended to PATH so every
// script and build step resolves it. Fails hard if uv is missing or has no
// suitable Python.
func EnsureModernPython(t *testing.T) {
// Windows runners already ship a python3 >= 3.11, and os.Symlink needs extra
// privileges there, so don't provision: use the interpreter already on PATH.
if runtime.GOOS == "windows" {
return
}

out, err := exec.Command("uv", "python", "find", ">=3.11").Output()
if err != nil {
t.Fatalf("uv could not find python >= 3.11: %v", err)
}
python := strings.TrimSpace(string(out))

binDir := t.TempDir()
for _, link := range []string{"python3", "python"} {
if err := os.Symlink(python, filepath.Join(binDir, link)); err != nil {
t.Fatalf("failed to symlink %s as %s: %v", python, link, err)
}
}
t.Setenv("PATH", binDir+string(os.PathListSeparator)+os.Getenv("PATH")) //nolint:forbidigo // acceptance test harness; no ctx for libs/env
t.Logf("acceptance tests: using %s (via uv) as python3", python)
}
23 changes: 23 additions & 0 deletions acceptance/internal/python_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package internal

import (
"os/exec"
"strings"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)

func TestEnsureModernPython(t *testing.T) {
if _, err := exec.LookPath("uv"); err != nil {
t.Skip("uv not installed")
}

EnsureModernPython(t)

// After setup, the python3 resolved from PATH must satisfy the floor.
out, err := exec.Command("python3", "-c", "import sys; print(sys.version_info >= (3, 11))").Output()
require.NoError(t, err)
assert.Equal(t, "True", strings.TrimSpace(string(out)))
}
8 changes: 8 additions & 0 deletions acceptance/internal/rejecting_proxy.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import (
"net/http"
"strings"
"testing"

"github.com/databricks/cli/libs/testserver"
)

// StartRejectingProxy starts an HTTP proxy server bound to a loopback port and
Expand Down Expand Up @@ -93,6 +95,12 @@ func handleBlockedConnection(t *testing.T, conn net.Conn, hint string) {
if isLoopback || isReserved {
// Expected unreachable fixture or local test server — log only, don't fail.
t.Logf("blocking proxy: blocked loopback/reserved host: %s", detail)
} else if testserver.IsLocalhostProbe(req) {
// Some Databricks-internal development environments run a port watcher
// that auto-forwards every new localhost listener and probes it with
// `HEAD / Host: localhost`. This is not the CLI-under-test reaching the
// internet, so log it instead of failing the test.
t.Logf("blocking proxy: ignored localhost port-classification probe: %s", detail)
} else {
t.Errorf("internet access blocked by proxy: %s%s", detail, hint)
}
Expand Down
38 changes: 38 additions & 0 deletions acceptance/internal/ruff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package internal

import (
"fmt"
"os/exec"
"strings"
"testing"
)

// RequireModernRuff fails the run if ruff is missing or older than 0.9.1, the
// version pinned across the repo (python/pyproject.toml, Taskfile.yml). The
// pydabs check-formatting acceptance test runs `ruff format` and its golden
// output assumes that formatter behavior.
func RequireModernRuff(t *testing.T) {
out, err := exec.Command("ruff", "--version").Output()
if err != nil {
t.Fatalf("ruff not found on PATH (acceptance tests require ruff >= 0.9.1): %v", err)
}
version := strings.TrimSpace(string(out))
if !ruffVersionOK(version) {
t.Fatalf("acceptance tests require ruff >= 0.9.1 (found %q); install a newer ruff", version)
}
}

// ruffVersionOK reports whether `ruff --version` output (e.g. "ruff 0.9.1") is >= 0.9.1.
func ruffVersionOK(version string) bool {
var major, minor, patch int
if _, err := fmt.Sscanf(version, "ruff %d.%d.%d", &major, &minor, &patch); err != nil {
return false
}
if major != 0 {
return major > 0
}
if minor != 9 {
return minor > 9
}
return patch >= 1
}
17 changes: 17 additions & 0 deletions acceptance/internal/ruff_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package internal

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestRuffVersionOK(t *testing.T) {
assert.True(t, ruffVersionOK("ruff 0.9.1"))
assert.True(t, ruffVersionOK("ruff 0.9.2"))
assert.True(t, ruffVersionOK("ruff 0.11.0"))
assert.True(t, ruffVersionOK("ruff 1.0.0"))
assert.False(t, ruffVersionOK("ruff 0.9.0"))
assert.False(t, ruffVersionOK("ruff 0.8.5"))
assert.False(t, ruffVersionOK(""))
}
32 changes: 32 additions & 0 deletions acceptance/internal/uv.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package internal

import (
"fmt"
"os/exec"
"strings"
"testing"
)

// RequireModernUv fails the run if uv is missing or older than 0.4. uv builds
// the databricks-bundles wheel and provides the test interpreter via
// `uv python find` (see EnsureModernPython), which landed in the 0.3 line; 0.4
// is a small margin above that.
func RequireModernUv(t *testing.T) {
out, err := exec.Command("uv", "--version").Output()
if err != nil {
t.Fatalf("uv not found on PATH (acceptance tests require uv >= 0.4): %v", err)
}
version := strings.TrimSpace(string(out))
if !uvVersionOK(version) {
t.Fatalf("acceptance tests require uv >= 0.4 (found %q); install a newer uv", version)
}
}

// uvVersionOK reports whether `uv --version` output (e.g. "uv 0.11.22 (abc)") is >= 0.4.
func uvVersionOK(version string) bool {
var major, minor int
if _, err := fmt.Sscanf(version, "uv %d.%d", &major, &minor); err != nil {
return false
}
return major > 0 || minor >= 4
}
16 changes: 16 additions & 0 deletions acceptance/internal/uv_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package internal

import (
"testing"

"github.com/stretchr/testify/assert"
)

func TestUvVersionOK(t *testing.T) {
assert.True(t, uvVersionOK("uv 0.4.0"))
assert.True(t, uvVersionOK("uv 0.11.22 (abcdef 2025-01-01)"))
assert.True(t, uvVersionOK("uv 1.0.0"))
assert.False(t, uvVersionOK("uv 0.3.5"))
assert.False(t, uvVersionOK("0.4.0"))
assert.False(t, uvVersionOK(""))
}
26 changes: 26 additions & 0 deletions libs/testserver/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"fmt"
"io"
"maps"
"net"
"net/http"
"net/http/httptest"
"net/url"
Expand All @@ -24,6 +25,24 @@ const testPidKey = "test-pid"

var testPidRegex = regexp.MustCompile(testPidKey + `/(\d+)`)

// IsLocalhostProbe reports whether r is an external port-classification probe
// rather than traffic from the CLI-under-test or its helper scripts.
//
// Some Databricks-internal development environments run a port watcher that
// auto-forwards every new localhost listener and probes it to decide whether it
// speaks HTTP or HTTPS, connecting back and sending `HEAD / HTTP/1.0` with
// `Host: localhost`. All legitimate test traffic is configured against
// 127.0.0.1:PORT, so the Host is the reliable discriminator: a request whose
// host is bare "localhost" never originates from the test. The method and path
// checks keep the match tight so a genuinely misdirected request still surfaces.
func IsLocalhostProbe(r *http.Request) bool {
host := r.Host
if h, _, err := net.SplitHostPort(host); err == nil {
host = h
}
return host == "localhost" && r.Method == http.MethodHead && r.URL.Path == "/"
}

func ExtractPidFromHeaders(headers http.Header) int {
ua := headers.Get("User-Agent")
matches := testPidRegex.FindStringSubmatch(ua)
Expand Down Expand Up @@ -243,6 +262,13 @@ func New(t testutil.TestingT) *Server {

// Set up the not found handler as fallback
notFoundFunc := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Answer external port-classification probes benignly instead of failing
// the test with a spurious "No handler" error. See IsLocalhostProbe.
if IsLocalhostProbe(r) {
w.WriteHeader(http.StatusOK)
return
}

pattern := r.Method + " " + r.URL.Path
bodyBytes, err := io.ReadAll(r.Body)
var body string
Expand Down
Loading
Loading