diff --git a/NEXT_CHANGELOG.md b/NEXT_CHANGELOG.md index 6a15893628e..9710ffc13f4 100644 --- a/NEXT_CHANGELOG.md +++ b/NEXT_CHANGELOG.md @@ -9,7 +9,7 @@ * Increase the SSH server startup timeout from 10 to 45 minutes when a GPU accelerator is requested via `databricks ssh connect --accelerator` ([#5569](https://github.com/databricks/cli/pull/5569)). * Fix authentication falling back to the default profile in `.databrickscfg` when a host is already configured via the environment (e.g. `DATABRICKS_HOST` with `DATABRICKS_TOKEN`) ([#5616](https://github.com/databricks/cli/pull/5616)). * ssh: fix opening remote environment in Cursor, which previously hung on default-extension install and never opened the editor ([#5619](https://github.com/databricks/cli/pull/5619)). - +* `databricks labs list` now only shows projects that can be installed ([#5560](https://github.com/databricks/cli/pull/5560)). ### Bundles * Remove API enum values and types that are still in development from the `databricks-bundles` Python package; these were never accepted by the backend ([#5484](https://github.com/databricks/cli/pull/5484)). diff --git a/cmd/labs/list.go b/cmd/labs/list.go index 171f956a1e7..2e709b7b385 100644 --- a/cmd/labs/list.go +++ b/cmd/labs/list.go @@ -2,6 +2,7 @@ package labs import ( "context" + "slices" "github.com/databricks/cli/cmd/labs/github" "github.com/databricks/cli/cmd/labs/project" @@ -9,6 +10,15 @@ import ( "github.com/spf13/cobra" ) +const ( + labsOrg = "databrickslabs" + + // installableTopic is the GitHub repository topic that labs maintainers add to + // projects installable via `databricks labs install`. The repositories API + // returns topics inline, so filtering on it costs no extra requests. + installableTopic = "databricks-cli-installable" +) + type labsMeta struct { Name string `json:"name"` Description string `json:"description"` @@ -20,14 +30,35 @@ func allRepos(ctx context.Context) (github.Repositories, error) { if err != nil { return nil, err } - cache := github.NewRepositoryCache("databrickslabs", cacheDir) + cache := github.NewRepositoryCache(labsOrg, cacheDir) return cache.Load(ctx) } +// installableRepos returns the org repositories that `databricks labs install` can +// install. Most repositories don't ship a labs.yml manifest (e.g. libraries +// published to package indexes); maintainers tag the installable ones with +// installableTopic so the listing doesn't advertise projects that fail to install. +func installableRepos(ctx context.Context) (github.Repositories, error) { + repos, err := allRepos(ctx) + if err != nil { + return nil, err + } + var out github.Repositories + for _, repo := range repos { + if repo.IsArchived || repo.IsFork { + continue + } + if slices.Contains(repo.Topics, installableTopic) { + out = append(out, repo) + } + } + return out, nil +} + func newListCommand() *cobra.Command { return &cobra.Command{ Use: "list", - Short: "List all labs", + Short: "List labs that can be installed", Annotations: map[string]string{ "template": cmdio.Heredoc(` Name Description @@ -37,18 +68,12 @@ func newListCommand() *cobra.Command { }, RunE: func(cmd *cobra.Command, args []string) error { ctx := cmd.Context() - repositories, err := allRepos(ctx) + repositories, err := installableRepos(ctx) if err != nil { return err } var info []labsMeta for _, v := range repositories { - if v.IsArchived { - continue - } - if v.IsFork { - continue - } description := v.Description if len(description) > 50 { description = description[:50] + "..." diff --git a/cmd/labs/list_test.go b/cmd/labs/list_test.go index d1f763a7f33..f3908b896e1 100644 --- a/cmd/labs/list_test.go +++ b/cmd/labs/list_test.go @@ -1,10 +1,14 @@ package labs_test import ( + "net/http" + "net/http/httptest" "testing" + "github.com/databricks/cli/cmd/labs/github" "github.com/databricks/cli/internal/testcli" "github.com/databricks/cli/libs/env" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -15,4 +19,28 @@ func TestListingWorks(t *testing.T) { stdout, _, err := c.Run() require.NoError(t, err) require.Contains(t, stdout.String(), "ucx") + // blueprint is in the repositories cache fixture but lacks the + // databricks-cli-installable topic, proving the topic filter is applied. + require.NotContains(t, stdout.String(), "blueprint") +} + +func TestListingFiltersReposWithoutTopic(t *testing.T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + assert.Equal(t, "/users/databrickslabs/repos", r.URL.Path) + _, err := w.Write([]byte(`[ + {"name": "ucx", "description": "Unity Catalog Migrations", "topics": ["databricks-cli-installable"]}, + {"name": "brickster", "description": "R interface to Databricks", "topics": []} + ]`)) + assert.NoError(t, err) + })) + defer server.Close() + ctx := t.Context() + ctx = github.WithApiOverride(ctx, server.URL) + ctx = env.WithUserHomeDir(ctx, t.TempDir()) + + c := testcli.NewRunner(t, ctx, "labs", "list") + stdout, _, err := c.Run() + require.NoError(t, err) + require.Contains(t, stdout.String(), "ucx") + require.NotContains(t, stdout.String(), "brickster") } diff --git a/cmd/labs/project/testdata/installed-in-home/.databricks/labs/databrickslabs-repositories.json b/cmd/labs/project/testdata/installed-in-home/.databricks/labs/databrickslabs-repositories.json index 896ebecc59e..11a96ac42a7 100644 --- a/cmd/labs/project/testdata/installed-in-home/.databricks/labs/databrickslabs-repositories.json +++ b/cmd/labs/project/testdata/installed-in-home/.databricks/labs/databrickslabs-repositories.json @@ -25,7 +25,9 @@ "stargazers_count": 100500, "fork": false, "archived": false, - "topics": [], + "topics": [ + "databricks-cli-installable" + ], "html_url": "https://github.com/databrickslabs/ucx", "clone_url": "https://github.com/databrickslabs/ucx.git", "ssh_url": "git@github.com:databrickslabs/ucx.git",