Skip to content

Commit e0ed2c6

Browse files
JackNDwyerclaude
andcommitted
feat(agent): richer deploy output with deployment name and links
After a successful `lk agent deploy`, report the agent name, target deployment, and live version, plus links to analytics and the agent console (deep-linked to the deployment). Resolves name/version with a single existing ListAgents call; falls back to a minimal status line if that lookup fails so a successful deploy is never reported as an error. Console deep-link now carries the deployment as a query param. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 17a3715 commit e0ed2c6

3 files changed

Lines changed: 118 additions & 10 deletions

File tree

cmd/lk/agent.go

Lines changed: 67 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -879,10 +879,76 @@ func deployAgent(ctx context.Context, cmd *cli.Command) error {
879879
return fmt.Errorf("unable to deploy agent: %w", err)
880880
}
881881

882-
out.Status("Deployed agent")
882+
reportDeployment(ctx, agentId, agentDeployment)
883883
return nil
884884
}
885885

886+
// reportDeployment prints a summary of a completed deployment — the agent name,
887+
// the target deployment, the live version, and links to the agent details page
888+
// and the agent console for the deployment. It resolves the name/version with a
889+
// single ListAgents call; on any failure it falls back to the minimal status
890+
// line so a successful deploy is never reported as a failure.
891+
func reportDeployment(ctx context.Context, agentID, deployment string) {
892+
targetDeployment := deployment
893+
if targetDeployment == "" {
894+
targetDeployment = "production"
895+
}
896+
897+
agentName := ""
898+
version := ""
899+
if res, err := agentsClient.ListAgents(ctx, &lkproto.ListAgentsRequest{AgentId: agentID}); err == nil {
900+
for _, agent := range res.Agents {
901+
for _, regionalAgent := range agent.AgentDeployments {
902+
regionDeployment := regionalAgent.Deployment
903+
if regionDeployment == "" {
904+
regionDeployment = "production"
905+
}
906+
if regionDeployment != targetDeployment {
907+
continue
908+
}
909+
if regionalAgent.AgentName != "" {
910+
agentName = regionalAgent.AgentName
911+
}
912+
if regionalAgent.Version != "" {
913+
version = regionalAgent.Version
914+
} else if agent.Version != "" {
915+
version = agent.Version
916+
}
917+
}
918+
}
919+
}
920+
921+
summary := "Completed deployment of agent"
922+
if agentName != "" {
923+
summary += fmt.Sprintf(" %s", util.Accented(agentName))
924+
}
925+
summary += fmt.Sprintf(" to %s", util.Accented(targetDeployment))
926+
if version != "" {
927+
summary += fmt.Sprintf(" (%s)", version)
928+
}
929+
out.Status(summary)
930+
931+
// Links use the same ws URL the deploy ran against; both are "" for
932+
// non-cloud projects, in which case nothing is printed.
933+
wsURL := project.URL
934+
if link := cloudAgentURL(wsURL, agentID); link != "" {
935+
out.Statusf("Agent details: %s", consoleLinkLabel(link))
936+
}
937+
if link := cloudConsoleURL(wsURL, agentName, deployment); link != "" {
938+
out.Statusf("Test in Agent Console: %s", consoleLinkLabel(link))
939+
}
940+
}
941+
942+
// consoleLinkLabel accents a URL and makes it a clickable OSC 8 hyperlink on
943+
// interactive terminals, matching how `lk agent start` renders console links.
944+
func consoleLinkLabel(link string) string {
945+
label := util.Accented(link)
946+
if out.Interactive() {
947+
label = util.Hyperlink(link, label)
948+
}
949+
return label
950+
}
951+
886952
func promoteAgent(ctx context.Context, cmd *cli.Command) error {
887953
agentID, err := getAgentID(ctx, cmd, workingDir, tomlFilename, false)
888954
if err != nil {

cmd/lk/agent_run.go

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -34,16 +34,34 @@ import (
3434
// cloudConsoleURL returns the LiveKit Cloud agents-console URL for a worker that
3535
// registered against wsURL with the given agent name, or "" when wsURL is not a
3636
// LiveKit Cloud project (e.g. a self-hosted or localhost server), in which case
37-
// no console link is shown.
38-
func cloudConsoleURL(wsURL, agentName string) string {
37+
// no console link is shown. A non-empty deployment is deep-linked so the console
38+
// targets that deployment; an empty deployment targets production.
39+
func cloudConsoleURL(wsURL, agentName, deployment string) string {
3940
consoleHost, sub := cloudProject(wsURL)
4041
if consoleHost == "" {
4142
return ""
4243
}
43-
return fmt.Sprintf(
44+
link := fmt.Sprintf(
4445
"https://%s/projects/d_%s/agents/console?agentName=%s&autoStart=false",
4546
consoleHost, sub, url.QueryEscape(agentName),
4647
)
48+
if deployment != "" {
49+
link += "&deployment=" + url.QueryEscape(deployment)
50+
}
51+
return link
52+
}
53+
54+
// cloudAgentURL returns the LiveKit Cloud analytics URL for a specific agent, or
55+
// "" when wsURL is not a recognized LiveKit Cloud project.
56+
func cloudAgentURL(wsURL, agentID string) string {
57+
consoleHost, sub := cloudProject(wsURL)
58+
if consoleHost == "" {
59+
return ""
60+
}
61+
return fmt.Sprintf(
62+
"https://%s/projects/d_%s/agents/%s",
63+
consoleHost, sub, url.QueryEscape(agentID),
64+
)
4765
}
4866

4967
// cloudProject maps a LiveKit Cloud project URL to its console host and project
@@ -306,7 +324,7 @@ func runAgentDev(ctx context.Context, cmd *cli.Command) error {
306324
// agent in the browser. Printed once, even across hot reloads (link stays valid).
307325
var consoleLinkOnce sync.Once
308326
cfg.OnServerInfo = func(agentName, wsURL string) {
309-
if link := cloudConsoleURL(wsURL, agentName); link != "" {
327+
if link := cloudConsoleURL(wsURL, agentName, ""); link != "" {
310328
consoleLinkOnce.Do(func() {
311329
// Delay briefly so the link prints after the agent's own startup
312330
// logs rather than getting buried in them.

cmd/lk/agent_run_test.go

Lines changed: 29 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -398,23 +398,47 @@ func TestCloudProject(t *testing.T) {
398398
func TestCloudConsoleURL(t *testing.T) {
399399
assert.Equal(t,
400400
"https://cloud.livekit.io/projects/d_dztest2/agents/console?agentName=my-agent&autoStart=false",
401-
cloudConsoleURL("wss://dztest2.livekit.cloud", "my-agent"),
401+
cloudConsoleURL("wss://dztest2.livekit.cloud", "my-agent", ""),
402402
)
403403
// staging projects point at the staging console host
404404
assert.Equal(t,
405405
"https://cloud.staging.livekit.io/projects/d_dztest2/agents/console?agentName=my-agent&autoStart=false",
406-
cloudConsoleURL("wss://dztest2.staging.livekit.cloud", "my-agent"),
406+
cloudConsoleURL("wss://dztest2.staging.livekit.cloud", "my-agent", ""),
407407
)
408408
// empty agent name (the common dev default) still yields a usable link
409409
assert.Equal(t,
410410
"https://cloud.livekit.io/projects/d_dztest2/agents/console?agentName=&autoStart=false",
411-
cloudConsoleURL("wss://dztest2.livekit.cloud", ""),
411+
cloudConsoleURL("wss://dztest2.livekit.cloud", "", ""),
412412
)
413413
// agent names are query-escaped
414414
assert.Equal(t,
415415
"https://cloud.livekit.io/projects/d_dztest2/agents/console?agentName=my+agent%2F1&autoStart=false",
416-
cloudConsoleURL("wss://dztest2.livekit.cloud", "my agent/1"),
416+
cloudConsoleURL("wss://dztest2.livekit.cloud", "my agent/1", ""),
417+
)
418+
// a non-production deployment is deep-linked via the deployment param
419+
assert.Equal(t,
420+
"https://cloud.livekit.io/projects/d_dztest2/agents/console?agentName=my-agent&autoStart=false&deployment=staging",
421+
cloudConsoleURL("wss://dztest2.livekit.cloud", "my-agent", "staging"),
422+
)
423+
// deployment values are query-escaped
424+
assert.Equal(t,
425+
"https://cloud.livekit.io/projects/d_dztest2/agents/console?agentName=my-agent&autoStart=false&deployment=pre%2Fprod",
426+
cloudConsoleURL("wss://dztest2.livekit.cloud", "my-agent", "pre/prod"),
427+
)
428+
// non-cloud URLs produce no link
429+
assert.Empty(t, cloudConsoleURL("http://localhost:7880", "my-agent", ""))
430+
}
431+
432+
func TestCloudAgentURL(t *testing.T) {
433+
assert.Equal(t,
434+
"https://cloud.livekit.io/projects/d_dztest2/agents/CA_abc123",
435+
cloudAgentURL("wss://dztest2.livekit.cloud", "CA_abc123"),
436+
)
437+
// staging projects point at the staging console host
438+
assert.Equal(t,
439+
"https://cloud.staging.livekit.io/projects/d_dztest2/agents/CA_abc123",
440+
cloudAgentURL("wss://dztest2.staging.livekit.cloud", "CA_abc123"),
417441
)
418442
// non-cloud URLs produce no link
419-
assert.Empty(t, cloudConsoleURL("http://localhost:7880", "my-agent"))
443+
assert.Empty(t, cloudAgentURL("http://localhost:7880", "CA_abc123"))
420444
}

0 commit comments

Comments
 (0)