Skip to content

Commit 2be38fc

Browse files
authored
Merge branch 'main' into kacpersaw/fix-flaky-workspace-cancel
2 parents 78eecfb + 33509f2 commit 2be38fc

File tree

104 files changed

+4856
-3134
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

104 files changed

+4856
-3134
lines changed

.devcontainer/scripts/post_create.sh

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
#!/bin/sh
22

33
install_devcontainer_cli() {
4-
npm install -g @devcontainers/cli@0.80.0 --integrity=sha512-w2EaxgjyeVGyzfA/KUEZBhyXqu/5PyWNXcnrXsZOBrt3aN2zyGiHrXoG54TF6K0b5DSCF01Rt5fnIyrCeFzFKw==
4+
set -e
5+
echo "🔧 Installing DevContainer CLI..."
6+
cd "$(dirname "$0")/../tools/devcontainer-cli"
7+
npm ci --omit=dev
8+
ln -sf "$(pwd)/node_modules/.bin/devcontainer" "$(npm config get prefix)/bin/devcontainer"
59
}
610

711
install_ssh_config() {

.devcontainer/tools/devcontainer-cli/package-lock.json

Lines changed: 26 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
"name": "devcontainer-cli",
3+
"private": true,
4+
"version": "1.0.0",
5+
"dependencies": {
6+
"@devcontainers/cli": "^0.80.0"
7+
}
8+
}

.github/actions/setup-tf/action.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,5 +7,5 @@ runs:
77
- name: Install Terraform
88
uses: hashicorp/setup-terraform@b9cd54a3c349d3f38e8881555d616ced269862dd # v3.1.2
99
with:
10-
terraform_version: 1.12.2
10+
terraform_version: 1.13.0
1111
terraform_wrapper: false

.github/dependabot.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ updates:
3333
- dependency-name: "*"
3434
update-types:
3535
- version-update:semver-patch
36+
- dependency-name: "github.com/mark3labs/mcp-go"
3637

3738
# Update our Dockerfile.
3839
- package-ecosystem: "docker"

.github/workflows/release.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ jobs:
3737
runs-on: ${{ github.repository_owner == 'coder' && 'depot-ubuntu-22.04-8' || 'ubuntu-latest' }}
3838
steps:
3939
- name: Allow only maintainers/admins
40-
uses: actions/github-script@v7.0.1
40+
uses: actions/github-script@60a0d83039c74a4aee543508d2ffcb1c3799cdea # v7.0.1
4141
with:
4242
github-token: ${{ secrets.GITHUB_TOKEN }}
4343
script: |

cli/exp.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ func (r *RootCmd) expCmd() *serpent.Command {
1616
r.mcpCommand(),
1717
r.promptExample(),
1818
r.rptyCommand(),
19+
r.tasksCommand(),
1920
},
2021
}
2122
return cmd

cli/exp_task.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package cli
2+
3+
import (
4+
"github.com/coder/serpent"
5+
)
6+
7+
func (r *RootCmd) tasksCommand() *serpent.Command {
8+
cmd := &serpent.Command{
9+
Use: "task",
10+
Aliases: []string{"tasks"},
11+
Short: "Experimental task commands.",
12+
Handler: func(i *serpent.Invocation) error {
13+
return i.Command.HelpHandler(i)
14+
},
15+
Children: []*serpent.Command{
16+
r.taskList(),
17+
r.taskCreate(),
18+
r.taskStatus(),
19+
},
20+
}
21+
return cmd
22+
}

cli/exp_task_status.go

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,171 @@
1+
package cli
2+
3+
import (
4+
"fmt"
5+
"strings"
6+
"time"
7+
8+
"github.com/google/uuid"
9+
"golang.org/x/xerrors"
10+
11+
"github.com/coder/coder/v2/cli/cliui"
12+
"github.com/coder/coder/v2/codersdk"
13+
"github.com/coder/serpent"
14+
)
15+
16+
func (r *RootCmd) taskStatus() *serpent.Command {
17+
var (
18+
client = new(codersdk.Client)
19+
formatter = cliui.NewOutputFormatter(
20+
cliui.TableFormat(
21+
[]taskStatusRow{},
22+
[]string{
23+
"state changed",
24+
"status",
25+
"state",
26+
"message",
27+
},
28+
),
29+
cliui.ChangeFormatterData(
30+
cliui.JSONFormat(),
31+
func(data any) (any, error) {
32+
rows, ok := data.([]taskStatusRow)
33+
if !ok {
34+
return nil, xerrors.Errorf("expected []taskStatusRow, got %T", data)
35+
}
36+
if len(rows) != 1 {
37+
return nil, xerrors.Errorf("expected exactly 1 row, got %d", len(rows))
38+
}
39+
return rows[0], nil
40+
},
41+
),
42+
)
43+
watchArg bool
44+
watchIntervalArg time.Duration
45+
)
46+
cmd := &serpent.Command{
47+
Short: "Show the status of a task.",
48+
Use: "status",
49+
Aliases: []string{"stat"},
50+
Options: serpent.OptionSet{
51+
{
52+
Default: "false",
53+
Description: "Watch the task status output. This will stream updates to the terminal until the underlying workspace is stopped.",
54+
Flag: "watch",
55+
Name: "watch",
56+
Value: serpent.BoolOf(&watchArg),
57+
},
58+
{
59+
Default: "1s",
60+
Description: "Interval to poll the task for updates. Only used in tests.",
61+
Hidden: true,
62+
Flag: "watch-interval",
63+
Name: "watch-interval",
64+
Value: serpent.DurationOf(&watchIntervalArg),
65+
},
66+
},
67+
Middleware: serpent.Chain(
68+
serpent.RequireNArgs(1),
69+
r.InitClient(client),
70+
),
71+
Handler: func(i *serpent.Invocation) error {
72+
ctx := i.Context()
73+
ec := codersdk.NewExperimentalClient(client)
74+
identifier := i.Args[0]
75+
76+
taskID, err := uuid.Parse(identifier)
77+
if err != nil {
78+
// Try to resolve the task as a named workspace
79+
// TODO: right now tasks are still "workspaces" under the hood.
80+
// We should update this once we have a proper task model.
81+
ws, err := namedWorkspace(ctx, client, identifier)
82+
if err != nil {
83+
return err
84+
}
85+
taskID = ws.ID
86+
}
87+
task, err := ec.TaskByID(ctx, taskID)
88+
if err != nil {
89+
return err
90+
}
91+
92+
out, err := formatter.Format(ctx, toStatusRow(task))
93+
if err != nil {
94+
return xerrors.Errorf("format task status: %w", err)
95+
}
96+
_, _ = fmt.Fprintln(i.Stdout, out)
97+
98+
if !watchArg {
99+
return nil
100+
}
101+
102+
lastStatus := task.Status
103+
lastState := task.CurrentState
104+
t := time.NewTicker(watchIntervalArg)
105+
defer t.Stop()
106+
// TODO: implement streaming updates instead of polling
107+
for range t.C {
108+
task, err := ec.TaskByID(ctx, taskID)
109+
if err != nil {
110+
return err
111+
}
112+
if lastStatus == task.Status && taskStatusEqual(lastState, task.CurrentState) {
113+
continue
114+
}
115+
out, err := formatter.Format(ctx, toStatusRow(task))
116+
if err != nil {
117+
return xerrors.Errorf("format task status: %w", err)
118+
}
119+
// hack: skip the extra column header from formatter
120+
if formatter.FormatID() != cliui.JSONFormat().ID() {
121+
out = strings.SplitN(out, "\n", 2)[1]
122+
}
123+
_, _ = fmt.Fprintln(i.Stdout, out)
124+
125+
if task.Status == codersdk.WorkspaceStatusStopped {
126+
return nil
127+
}
128+
lastStatus = task.Status
129+
lastState = task.CurrentState
130+
}
131+
return nil
132+
},
133+
}
134+
formatter.AttachOptions(&cmd.Options)
135+
return cmd
136+
}
137+
138+
func taskStatusEqual(s1, s2 *codersdk.TaskStateEntry) bool {
139+
if s1 == nil && s2 == nil {
140+
return true
141+
}
142+
if s1 == nil || s2 == nil {
143+
return false
144+
}
145+
return s1.State == s2.State
146+
}
147+
148+
type taskStatusRow struct {
149+
codersdk.Task `table:"-"`
150+
ChangedAgo string `json:"-" table:"state changed,default_sort"`
151+
Timestamp time.Time `json:"-" table:"-"`
152+
TaskStatus string `json:"-" table:"status"`
153+
TaskState string `json:"-" table:"state"`
154+
Message string `json:"-" table:"message"`
155+
}
156+
157+
func toStatusRow(task codersdk.Task) []taskStatusRow {
158+
tsr := taskStatusRow{
159+
Task: task,
160+
ChangedAgo: time.Since(task.UpdatedAt).Truncate(time.Second).String() + " ago",
161+
Timestamp: task.UpdatedAt,
162+
TaskStatus: string(task.Status),
163+
}
164+
if task.CurrentState != nil {
165+
tsr.ChangedAgo = time.Since(task.CurrentState.Timestamp).Truncate(time.Second).String() + " ago"
166+
tsr.Timestamp = task.CurrentState.Timestamp
167+
tsr.TaskState = string(task.CurrentState.State)
168+
tsr.Message = task.CurrentState.Message
169+
}
170+
return []taskStatusRow{tsr}
171+
}

0 commit comments

Comments
 (0)