Skip to content

Commit 8f2ef4e

Browse files
anton-107Isaac
andcommitted
aitools: do not block plugin installs on the latest-release lookup
Plugin installs no longer fail when the lookup of the latest skills release on api.github.com fails (for example HTTP 403 when rate-limited). The agent's own CLI fetches the plugin from its marketplace, so the skills ref only labels the recorded plugin version. On lookup failure the CLI now records the plugin version as unknown and keeps the scope's existing `state.Release`. ```diff executePlan (cmd/aitools/install.go) if plugin items ref = GetSkillsRef(ctx) - lookup error -> install fails + lookup error -> debug log, ref = "" (plugin version unknown) install plugins RecordPluginInstalls (libs/aitools/installer/plugin.go) - state.Release = ref + if ref != "": state.Release = ref ``` `DATABRICKS_SKILLS_REF` and a pinned compat version still take precedence. Skills installs are unchanged. Changelog fragment: `.nextchanges/cli/aitools-plugin-ref-fallback.md`. Co-authored-by: Isaac <no-reply@databricks.com>
1 parent 20e3ef4 commit 8f2ef4e

5 files changed

Lines changed: 81 additions & 6 deletions

File tree

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
* `databricks aitools install` no longer fails a plugin-only install when the lookup of the latest skills release on GitHub fails or is rate-limited. The plugin is installed and its version is recorded as unknown. ([#7474](https://github.com/databricks/cli/pull/7474))

‎cmd/aitools/install.go‎

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -591,9 +591,12 @@ func executePlan(ctx context.Context, src installer.ManifestSource, plan []agent
591591

592592
pluginCount := 0
593593
if len(pluginItems) > 0 {
594+
// Plugins install from the agent's marketplace, so the skills ref only
595+
// labels the recorded version. Record it as unknown when unresolved.
594596
ref, _, err := installer.GetSkillsRef(ctx)
595597
if err != nil {
596-
return outcomes, err
598+
log.Debugf(ctx, "Recording plugin version as unknown: %v", err)
599+
ref = ""
597600
}
598601
records := map[string]installer.PluginRecord{}
599602
for _, it := range pluginItems {
@@ -703,9 +706,9 @@ func buildInstallOutput(scope string, outcomes []agentOutcome, runErr error) ins
703706
}
704707
out.Agents = append(out.Agents, entry)
705708
}
706-
// A top-level failure (skills-group install, ref lookup, plugin recording)
707-
// has no per-agent entry, so surface it here too; otherwise the consumer sees
708-
// a non-zero exit with an empty agents array and no reason. Per-agent failures
709+
// A top-level failure (skills-group install, plugin recording) has no
710+
// per-agent entry, so surface it here too; otherwise the consumer sees a
711+
// non-zero exit with an empty agents array and no reason. Per-agent failures
709712
// stay in the agents entries above and are not repeated here.
710713
if e := topLevelFailure(runErr); e != nil {
711714
out.Error = e.Error()

‎cmd/aitools/install_test.go‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,8 @@ import (
66
"context"
77
"encoding/json"
88
"errors"
9+
"net/http"
10+
"net/http/httptest"
911
"os"
1012
"path/filepath"
1113
"runtime"
@@ -269,6 +271,52 @@ func TestExecutePlanSkipBlockedPluginExit0(t *testing.T) {
269271
require.Error(t, err)
270272
}
271273

274+
func TestExecutePlanPluginWithUnresolvedSkillsRelease(t *testing.T) {
275+
tmp := t.TempDir()
276+
t.Setenv("DATABRICKS_SKILLS_REF", "")
277+
t.Setenv("DATABRICKS_FORCE_EMBEDDED_COMPAT", "false")
278+
t.Setenv("DATABRICKS_CACHE_ENABLED", "true")
279+
t.Setenv("DATABRICKS_CACHE_DIR", tmp)
280+
require.NoError(t, os.WriteFile(filepath.Join(tmp, "compat-manifest.json"), []byte(`{"0.0.0":{"appkit":"0.84.0","skills":"latest"}}`), 0o600))
281+
lookups := 0
282+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
283+
lookups++
284+
w.WriteHeader(http.StatusForbidden)
285+
}))
286+
t.Cleanup(srv.Close)
287+
t.Setenv("DATABRICKS_CLI_GITHUB_API_URL", srv.URL)
288+
289+
installRef, recordRef := "unset", "unset"
290+
orig := installPluginForAgentFn
291+
origRec := recordPluginInstallsFn
292+
origCleanup := cleanupLegacyFn
293+
t.Cleanup(func() {
294+
installPluginForAgentFn = orig
295+
recordPluginInstallsFn = origRec
296+
cleanupLegacyFn = origCleanup
297+
})
298+
cleanupLegacyFn = func(context.Context, *agents.Agent, string) error { return nil }
299+
installPluginForAgentFn = func(_ context.Context, _ *agents.Agent, scope, ref string) (installer.PluginRecord, error) {
300+
installRef = ref
301+
return installer.PluginRecord{Plugin: "databricks", Scope: scope, Version: installer.DisplaySkillsVersion(ref)}, nil
302+
}
303+
recordPluginInstallsFn = func(_ context.Context, _ string, _ map[string]installer.PluginRecord, ref string) error {
304+
recordRef = ref
305+
return nil
306+
}
307+
308+
claude := testPluginAgent(agents.NameClaudeCode, "Claude Code", "claude")
309+
plan := buildPlan([]*agents.Agent{claude}, installer.ScopeGlobal, false, true)
310+
outcomes, err := executePlan(cmdio.MockDiscard(t.Context()), nil, plan, installer.InstallOptions{Scope: installer.ScopeGlobal}, false)
311+
require.NoError(t, err)
312+
require.Len(t, outcomes, 1)
313+
assert.Equal(t, outcomeInstalled, outcomes[0].status)
314+
assert.Equal(t, 1, lookups)
315+
// The marketplace picks the plugin version, so a failed lookup records it as unknown.
316+
assert.Empty(t, installRef)
317+
assert.Empty(t, recordRef)
318+
}
319+
272320
// --- RunE: skills-only path (config-dir detection, no plugin) ---
273321

274322
func TestInstallSkillsOnlyAllAgents(t *testing.T) {

‎libs/aitools/installer/plugin.go‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -391,7 +391,8 @@ func claudeAlreadyDisabled(err error) bool {
391391

392392
// RecordPluginInstalls persists plugin install records into the state file for
393393
// the given CLI scope (global or project), creating state if none exists. ref
394-
// is the resolved skills release the install corresponds to.
394+
// is the resolved skills release the install corresponds to. An empty ref
395+
// (release unresolved) keeps the existing state.Release.
395396
func RecordPluginInstalls(ctx context.Context, cliScope string, records map[string]PluginRecord, ref string) error {
396397
dir, err := skillsDir(ctx, cliScope)
397398
if err != nil {
@@ -415,7 +416,10 @@ func RecordPluginInstalls(ctx context.Context, cliScope string, records map[stri
415416
state.Plugins = make(map[string]PluginRecord, len(records))
416417
}
417418
maps.Copy(state.Plugins, records)
418-
state.Release = ref
419+
// An unresolved release keeps the one recorded by earlier installs.
420+
if ref != "" {
421+
state.Release = ref
422+
}
419423
state.LastUpdated = time.Now()
420424
state.Scope = cliScope
421425
return SaveState(dir, state)

‎libs/aitools/installer/plugin_test.go‎

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -787,6 +787,25 @@ func TestUninstallPluginKeepMarketplaceFlag(t *testing.T) {
787787
}
788788
}
789789

790+
func TestRecordPluginInstallsUnresolvedRefKeepsRelease(t *testing.T) {
791+
setupTestHome(t)
792+
ctx := cmdio.MockDiscard(t.Context())
793+
dir, err := GlobalSkillsDir(ctx)
794+
require.NoError(t, err)
795+
796+
require.NoError(t, RecordPluginInstalls(ctx, ScopeGlobal, map[string]PluginRecord{
797+
agents.NameClaudeCode: {Marketplace: "databricks-agent-skills", Plugin: "databricks", Version: "0.2.6"},
798+
}, "v0.2.6"))
799+
require.NoError(t, RecordPluginInstalls(ctx, ScopeGlobal, map[string]PluginRecord{
800+
agents.NameCodex: {Marketplace: "databricks-agent-skills", Plugin: "databricks"},
801+
}, ""))
802+
803+
st, err := LoadState(dir)
804+
require.NoError(t, err)
805+
assert.Equal(t, "v0.2.6", st.Release)
806+
assert.Contains(t, st.Plugins, agents.NameCodex)
807+
}
808+
790809
func TestRecordPluginInstallsThenSkillsInstallNoPanic(t *testing.T) {
791810
tmp := setupTestHome(t)
792811
ctx := cmdio.MockDiscard(t.Context())

0 commit comments

Comments
 (0)