This commit is contained in:
Shreyas Goenka 2025-01-27 12:59:32 +01:00
commit bd231012d1
No known key found for this signature in database
GPG Key ID: 92A07DF49CCB0622
19 changed files with 409 additions and 28 deletions

View File

@ -10,19 +10,65 @@ on:
jobs:
publish-to-winget-pkgs:
runs-on:
group: databricks-protected-runner-group
labels: windows-server-latest
group: databricks-deco-testing-runner-group
labels: ubuntu-latest-deco
environment: release
steps:
- uses: vedantmgoyal2009/winget-releaser@93fd8b606a1672ec3e5c6c3bb19426be68d1a8b0 # v2
with:
identifier: Databricks.DatabricksCLI
installers-regex: 'windows_.*-signed\.zip$' # Only signed Windows releases
token: ${{ secrets.ENG_DEV_ECOSYSTEM_BOT_TOKEN }}
fork-user: eng-dev-ecosystem-bot
- name: Checkout repository and submodules
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
# Use the tag from the input, or the ref name if the input is not provided.
# The ref name is equal to the tag name when this workflow is triggered by the "sign-cli" command.
release-tag: ${{ inputs.tag || github.ref_name }}
# When updating the version of komac, make sure to update the checksum in the next step.
# Find both at https://github.com/russellbanks/Komac/releases.
- name: Download komac binary
run: |
curl -s -L -o $RUNNER_TEMP/komac-2.9.0-x86_64-unknown-linux-gnu.tar.gz https://github.com/russellbanks/Komac/releases/download/v2.9.0/komac-2.9.0-x86_64-unknown-linux-gnu.tar.gz
- name: Verify komac binary
run: |
echo "d07a12831ad5418fee715488542a98ce3c0e591d05c850dd149fe78432be8c4c $RUNNER_TEMP/komac-2.9.0-x86_64-unknown-linux-gnu.tar.gz" | sha256sum -c -
- name: Untar komac binary to temporary path
run: |
mkdir -p $RUNNER_TEMP/komac
tar -xzf $RUNNER_TEMP/komac-2.9.0-x86_64-unknown-linux-gnu.tar.gz -C $RUNNER_TEMP/komac
- name: Add komac to PATH
run: echo "$RUNNER_TEMP/komac" >> $GITHUB_PATH
- name: Confirm komac version
run: komac --version
# Use the tag from the input, or the ref name if the input is not provided.
# The ref name is equal to the tag name when this workflow is triggered by the "sign-cli" command.
- name: Strip "v" prefix from version
id: strip_version
run: echo "version=$(echo ${{ inputs.tag || github.ref_name }} | sed 's/^v//')" >> "$GITHUB_OUTPUT"
- name: Get URLs of signed Windows binaries
id: get_windows_urls
run: |
urls=$(
gh api https://api.github.com/repos/databricks/cli/releases/tags/${{ inputs.tag || github.ref_name }} | \
jq -r .assets[].browser_download_url | \
grep -E '_windows_.*-signed\.zip$' | \
tr '\n' ' '
)
if [ -z "$urls" ]; then
echo "No signed Windows binaries found" >&2
exit 1
fi
echo "urls=$urls" >> "$GITHUB_OUTPUT"
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Publish to Winget
run: |
komac update Databricks.DatabricksCLI \
--version ${{ steps.strip_version.outputs.version }} \
--submit \
--urls ${{ steps.get_windows_urls.outputs.urls }} \
env:
KOMAC_FORK_OWNER: eng-dev-ecosystem-bot
GITHUB_TOKEN: ${{ secrets.ENG_DEV_ECOSYSTEM_BOT_TOKEN }}

View File

@ -60,12 +60,6 @@ jobs:
- name: Install uv
uses: astral-sh/setup-uv@887a942a15af3a7626099df99e897a18d9e5ab3a # v5.1.0
- name: Run ruff
uses: astral-sh/ruff-action@31a518504640beb4897d0b9f9e50a2a9196e75ba # v3.0.1
with:
version: "0.9.1"
args: "format --check"
- name: Set go env
run: |
echo "GOPATH=$(go env GOPATH)" >> $GITHUB_ENV
@ -80,7 +74,7 @@ jobs:
- name: Run tests with coverage
run: make cover
golangci:
linters:
needs: cleanups
name: lint
runs-on: ubuntu-latest
@ -105,6 +99,11 @@ jobs:
with:
version: v1.63.4
args: --timeout=15m
- name: Run ruff
uses: astral-sh/ruff-action@31a518504640beb4897d0b9f9e50a2a9196e75ba # v3.0.1
with:
version: "0.9.1"
args: "format --check"
validate-bundle-schema:
needs: cleanups

4
NOTICE
View File

@ -105,3 +105,7 @@ License - https://github.com/wI2L/jsondiff/blob/master/LICENSE
https://github.com/hexops/gotextdiff
Copyright (c) 2009 The Go Authors. All rights reserved.
License - https://github.com/hexops/gotextdiff/blob/main/LICENSE
https://github.com/BurntSushi/toml
Copyright (c) 2013 TOML authors
https://github.com/BurntSushi/toml/blob/master/COPYING

View File

@ -17,3 +17,5 @@ For more complex tests one can also use:
- `errcode` helper: if the command fails with non-zero code, it appends `Exit code: N` to the output but returns success to caller (bash), allowing continuation of script.
- `trace` helper: prints the arguments before executing the command.
- custom output files: redirect output to custom file (it must start with `out`), e.g. `$CLI bundle validate > out.txt 2> out.error.txt`.
See [selftest](./selftest) for a toy test.

View File

@ -66,12 +66,7 @@ func TestInprocessMode(t *testing.T) {
if InprocessMode {
t.Skip("Already tested by TestAccept")
}
if runtime.GOOS == "windows" {
// - catalogs A catalog is the first layer of Unity Catalogs three-level namespace.
// + catalogs A catalog is the first layer of Unity Catalog<6F>s three-level namespace.
t.Skip("Fails on CI on unicode characters")
}
require.NotZero(t, testAccept(t, true, "help"))
require.Equal(t, 1, testAccept(t, true, "selftest"))
}
func testAccept(t *testing.T, InprocessMode bool, singleTest string) int {
@ -187,6 +182,13 @@ func getTests(t *testing.T) []string {
}
func runTest(t *testing.T, dir, coverDir string, repls testdiff.ReplacementsContext) {
config, configPath := LoadConfig(t, dir)
isEnabled, isPresent := config.GOOS[runtime.GOOS]
if isPresent && !isEnabled {
t.Skipf("Disabled via GOOS.%s setting in %s", runtime.GOOS, configPath)
}
var tmpDir string
var err error
if KeepTmp {
@ -200,6 +202,7 @@ func runTest(t *testing.T, dir, coverDir string, repls testdiff.ReplacementsCont
}
repls.SetPathWithParents(tmpDir, "$TMPDIR")
repls.Repls = append(repls.Repls, config.Repls...)
scriptContents := readMergedScriptContents(t, dir)
testutil.WriteFile(t, filepath.Join(tmpDir, EntryPointScript), scriptContents)
@ -250,9 +253,11 @@ func runTest(t *testing.T, dir, coverDir string, repls testdiff.ReplacementsCont
server.WriteRequestsToDisk(outRequestsPath)
}
printedRepls := false
// Compare expected outputs
for relPath := range outputs {
doComparison(t, repls, dir, tmpDir, relPath)
doComparison(t, repls, dir, tmpDir, relPath, &printedRepls)
}
// Make sure there are not unaccounted for new files
@ -267,12 +272,12 @@ func runTest(t *testing.T, dir, coverDir string, repls testdiff.ReplacementsCont
if strings.HasPrefix(relPath, "out") {
// We have a new file starting with "out"
// Show the contents & support overwrite mode for it:
doComparison(t, repls, dir, tmpDir, relPath)
doComparison(t, repls, dir, tmpDir, relPath, &printedRepls)
}
}
}
func doComparison(t *testing.T, repls testdiff.ReplacementsContext, dirRef, dirNew, relPath string) {
func doComparison(t *testing.T, repls testdiff.ReplacementsContext, dirRef, dirNew, relPath string, printedRepls *bool) {
pathRef := filepath.Join(dirRef, relPath)
pathNew := filepath.Join(dirNew, relPath)
bufRef, okRef := readIfExists(t, pathRef)
@ -317,6 +322,15 @@ func doComparison(t *testing.T, repls testdiff.ReplacementsContext, dirRef, dirN
t.Logf("Overwriting existing output file: %s", relPath)
testutil.WriteFile(t, pathRef, valueNew)
}
if !equal && printedRepls != nil && !*printedRepls {
*printedRepls = true
var items []string
for _, item := range repls.Repls {
items = append(items, fmt.Sprintf("REPL %s => %s", item.Old, item.New))
}
t.Log("Available replacements:\n" + strings.Join(items, "\n"))
}
}
// Returns combined script.prepare (root) + script.prepare (parent) + ... + script + ... + script.cleanup (parent) + ...

View File

@ -0,0 +1,2 @@
bundle:
name: git-permerror

View File

@ -0,0 +1,78 @@
=== No permission to access .git. Badness: inferred flag is set to true even though we did not infer branch. bundle_root_path is not correct in subdir case.
>>> chmod 000 .git
>>> $CLI bundle validate
Error: unable to load repository specific gitconfig: open config: permission denied
Name: git-permerror
Target: default
Workspace:
User: $USERNAME
Path: /Workspace/Users/$USERNAME/.bundle/git-permerror/default
Found 1 error
Exit code: 1
>>> $CLI bundle validate -o json
Error: unable to load repository specific gitconfig: open config: permission denied
Exit code: 1
{
"bundle_root_path": ".",
"inferred": true
}
>>> withdir subdir/a/b $CLI bundle validate -o json
Error: unable to load repository specific gitconfig: open config: permission denied
Exit code: 1
{
"bundle_root_path": ".",
"inferred": true
}
=== No permissions to read .git/HEAD. Badness: warning is not shown. inferred is incorrectly set to true. bundle_root_path is not correct in subdir case.
>>> chmod 000 .git/HEAD
>>> $CLI bundle validate -o json
{
"bundle_root_path": ".",
"inferred": true
}
>>> withdir subdir/a/b $CLI bundle validate -o json
{
"bundle_root_path": ".",
"inferred": true
}
=== No permissions to read .git/config. Badness: inferred is incorretly set to true. bundle_root_path is not correct is subdir case.
>>> chmod 000 .git/config
>>> $CLI bundle validate -o json
Error: unable to load repository specific gitconfig: open config: permission denied
Exit code: 1
{
"bundle_root_path": ".",
"inferred": true
}
>>> withdir subdir/a/b $CLI bundle validate -o json
Error: unable to load repository specific gitconfig: open config: permission denied
Exit code: 1
{
"bundle_root_path": ".",
"inferred": true
}

View File

@ -0,0 +1,25 @@
mkdir myrepo
cd myrepo
cp ../databricks.yml .
git-repo-init
mkdir -p subdir/a/b
printf "=== No permission to access .git. Badness: inferred flag is set to true even though we did not infer branch. bundle_root_path is not correct in subdir case.\n"
trace chmod 000 .git
errcode trace $CLI bundle validate
errcode trace $CLI bundle validate -o json | jq .bundle.git
errcode trace withdir subdir/a/b $CLI bundle validate -o json | jq .bundle.git
printf "\n\n=== No permissions to read .git/HEAD. Badness: warning is not shown. inferred is incorrectly set to true. bundle_root_path is not correct in subdir case.\n"
chmod 700 .git
trace chmod 000 .git/HEAD
errcode trace $CLI bundle validate -o json | jq .bundle.git
errcode trace withdir subdir/a/b $CLI bundle validate -o json | jq .bundle.git
printf "\n\n=== No permissions to read .git/config. Badness: inferred is incorretly set to true. bundle_root_path is not correct is subdir case.\n"
chmod 666 .git/HEAD
trace chmod 000 .git/config
errcode trace $CLI bundle validate -o json | jq .bundle.git
errcode trace withdir subdir/a/b $CLI bundle validate -o json | jq .bundle.git
rm -fr .git

View File

@ -0,0 +1,5 @@
Badness = "Warning logs not shown; inferred flag is set to true incorrect; bundle_root_path is not correct"
[GOOS]
# This test relies on chmod which does not work on Windows
windows = false

104
acceptance/config_test.go Normal file
View File

@ -0,0 +1,104 @@
package acceptance_test
import (
"os"
"path/filepath"
"sync"
"testing"
"github.com/BurntSushi/toml"
"github.com/databricks/cli/libs/testdiff"
"github.com/stretchr/testify/require"
)
const configFilename = "test.toml"
var (
configCache map[string]TestConfig
configMutex sync.Mutex
)
type TestConfig struct {
// Place to describe what's wrong with this test. Does not affect how the test is run.
Badness string
// Which OSes the test is enabled on. Each string is compared against runtime.GOOS.
// If absent, default to true.
GOOS map[string]bool
// List of additional replacements to apply on this test.
// Old is a regexp, New is a replacement expression.
Repls []testdiff.Replacement
}
// FindConfig finds the closest config file.
func FindConfig(t *testing.T, dir string) (string, bool) {
shared := false
for {
path := filepath.Join(dir, configFilename)
_, err := os.Stat(path)
if err == nil {
return path, shared
}
shared = true
if dir == "" || dir == "." {
break
}
if os.IsNotExist(err) {
dir = filepath.Dir(dir)
continue
}
t.Fatalf("Error while reading %s: %s", path, err)
}
t.Fatal("Config not found: " + configFilename)
return "", shared
}
// LoadConfig loads the config file. Non-leaf configs are cached.
func LoadConfig(t *testing.T, dir string) (TestConfig, string) {
path, leafConfig := FindConfig(t, dir)
if leafConfig {
return DoLoadConfig(t, path), path
}
configMutex.Lock()
defer configMutex.Unlock()
if configCache == nil {
configCache = make(map[string]TestConfig)
}
result, ok := configCache[path]
if ok {
return result, path
}
result = DoLoadConfig(t, path)
configCache[path] = result
return result, path
}
func DoLoadConfig(t *testing.T, path string) TestConfig {
bytes, err := os.ReadFile(path)
if err != nil {
t.Fatalf("failed to read config: %s", err)
}
var config TestConfig
meta, err := toml.Decode(string(bytes), &config)
require.NoError(t, err)
keys := meta.Undecoded()
if len(keys) > 0 {
t.Fatalf("Undecoded keys in %s: %#v", path, keys)
}
return config
}

View File

@ -47,3 +47,14 @@ title() {
local label="$1"
printf "\n=== %s" "$label"
}
withdir() {
local dir="$1"
shift
local orig_dir="$(pwd)"
cd "$dir" || return $?
"$@"
local exit_code=$?
cd "$orig_dir" || return $?
return $exit_code
}

View File

@ -0,0 +1 @@
HELLO

View File

@ -0,0 +1,35 @@
=== Capturing STDERR
>>> python3 -c import sys; sys.stderr.write("STDERR\n")
STDERR
=== Capturing STDOUT
>>> python3 -c import sys; sys.stderr.write("STDOUT\n")
STDOUT
=== Capturing exit code
>>> errcode python3 -c raise SystemExit(5)
Exit code: 5
=== Capturing exit code (alt)
>>> python3 -c raise SystemExit(7)
Exit code: 7
=== Capturing pwd
>>> python3 -c import os; print(os.getcwd())
$TMPDIR
=== Capturing subdir
>>> mkdir -p subdir/a/b/c
>>> withdir subdir/a/b/c python3 -c import os; print(os.getcwd())
$TMPDIR/subdir/a/b/c
=== Custom output files - everything starting with out is captured and compared
>>> echo HELLO
=== Custom regex can be specified in [[Repl]] section
1234
CUSTOM_NUMBER_REGEX
123456

View File

@ -0,0 +1,26 @@
printf "=== Capturing STDERR"
trace python3 -c 'import sys; sys.stderr.write("STDERR\n")'
printf "\n=== Capturing STDOUT"
trace python3 -c 'import sys; sys.stderr.write("STDOUT\n")'
printf "\n=== Capturing exit code"
trace errcode python3 -c 'raise SystemExit(5)'
printf "\n=== Capturing exit code (alt)"
errcode trace python3 -c 'raise SystemExit(7)'
printf "\n=== Capturing pwd"
trace python3 -c 'import os; print(os.getcwd())'
printf "\n=== Capturing subdir"
trace mkdir -p subdir/a/b/c
trace withdir subdir/a/b/c python3 -c 'import os; print(os.getcwd())'
printf "\n=== Custom output files - everything starting with out is captured and compared"
trace echo HELLO > out.hello.txt
printf "\n=== Custom regex can be specified in [[Repl]] section\n"
echo 1234
echo 12345
echo 123456

View File

@ -0,0 +1,20 @@
# Badness = "Brief description of what's wrong with the test output, if anything"
#[GOOS]
# Disable on Windows
#windows = false
# Disable on Mac
#mac = false
# Disable on Linux
#linux = false
[[Repls]]
Old = '\b[0-9]{5}\b'
New = "CUSTOM_NUMBER_REGEX"
[[Repls]]
# Fix path with reverse slashes in the output for Windows.
Old = '\$TMPDIR\\subdir\\a\\b\\c'
New = '$$TMPDIR/subdir/a/b/c'

2
acceptance/test.toml Normal file
View File

@ -0,0 +1,2 @@
# If test directory nor any of its parents do not have test.toml then this file serves as fallback configuration.
# The configurations are not merged across parents; the closest one is used fully.

1
go.mod
View File

@ -5,6 +5,7 @@ go 1.23
toolchain go1.23.4
require (
github.com/BurntSushi/toml v1.4.0 // MIT
github.com/Masterminds/semver/v3 v3.3.1 // MIT
github.com/briandowns/spinner v1.23.1 // Apache 2.0
github.com/databricks/databricks-sdk-go v0.55.0 // Apache 2.0

2
go.sum generated
View File

@ -8,6 +8,8 @@ cloud.google.com/go/compute/metadata v0.3.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1h
dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk=
dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/Masterminds/semver/v3 v3.3.1 h1:QtNSWtVZ3nBfk8mAOu/B6v7FMJ+NHTIgUPi7rj+4nv4=
github.com/Masterminds/semver/v3 v3.3.1/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM=
github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow=

View File

@ -79,7 +79,11 @@ func (r *ReplacementsContext) Set(old, new string) {
if err == nil {
encodedOld, err := json.Marshal(old)
if err == nil {
r.appendLiteral(trimQuotes(string(encodedOld)), trimQuotes(string(encodedNew)))
encodedStrNew := trimQuotes(string(encodedNew))
encodedStrOld := trimQuotes(string(encodedOld))
if encodedStrNew != new || encodedStrOld != old {
r.appendLiteral(encodedStrOld, encodedStrNew)
}
}
}