Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit b8fe8c5

Browse files
Merge branch 'main' into dm-devcontainer-retry
2 parents 14bbb1d + 670fa4a commit b8fe8c5

File tree

176 files changed

+8490
-1890
lines changed

Some content is hidden

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

176 files changed

+8490
-1890
lines changed

.github/.linkspector.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,5 +24,6 @@ ignorePatterns:
2424
- pattern: "mutagen.io"
2525
- pattern: "docs.github.com"
2626
- pattern: "claude.ai"
27+
- pattern: "splunk.com"
2728
aliveStatusCodes:
2829
- 200

.github/workflows/ci.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -902,7 +902,7 @@ jobs:
902902
# the check to pass. This is desired in PRs, but not in mainline.
903903
- name: Publish to Chromatic (non-mainline)
904904
if: github.ref != 'refs/heads/main' && github.repository_owner == 'coder'
905-
uses: chromaui/action@c50adf8eaa8c2878af3263499a73077854de39d4 # v12.2.0
905+
uses: chromaui/action@b5848056bb67ce5f1cccca8e62a37cbd9dd42871 # v13.0.1
906906
env:
907907
NODE_OPTIONS: "--max_old_space_size=4096"
908908
STORYBOOK: true
@@ -934,7 +934,7 @@ jobs:
934934
# infinitely "in progress" in mainline unless we re-review each build.
935935
- name: Publish to Chromatic (mainline)
936936
if: github.ref == 'refs/heads/main' && github.repository_owner == 'coder'
937-
uses: chromaui/action@c50adf8eaa8c2878af3263499a73077854de39d4 # v12.2.0
937+
uses: chromaui/action@b5848056bb67ce5f1cccca8e62a37cbd9dd42871 # v13.0.1
938938
env:
939939
NODE_OPTIONS: "--max_old_space_size=4096"
940940
STORYBOOK: true

.github/workflows/docker-base.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ jobs:
6060

6161
# This uses OIDC authentication, so no auth variables are required.
6262
- name: Build base Docker image via depot.dev
63-
uses: depot/build-push-action@636daae76684e38c301daa0c5eca1c095b24e780 # v1.14.0
63+
uses: depot/build-push-action@2583627a84956d07561420dcc1d0eb1f2af3fac0 # v1.15.0
6464
with:
6565
project: wl5hnrrkns
6666
context: base-build-context

.github/workflows/docs-ci.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ jobs:
2828
- name: Setup Node
2929
uses: ./.github/actions/setup-node
3030

31-
- uses: tj-actions/changed-files@d52d20fa3f981cb852b861fd8f55308b5fe29637 # v45.0.7
31+
- uses: tj-actions/changed-files@666c9d29007687c52e3c7aa2aac6c0ffcadeadc3 # v45.0.7
3232
id: changed-files
3333
with:
3434
files: |

.github/workflows/dogfood.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ jobs:
3535
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
3636

3737
- name: Setup Nix
38-
uses: nixbuild/nix-quick-install-action@5bb6a3b3abe66fd09bbf250dce8ada94f856a703 # v30
38+
uses: nixbuild/nix-quick-install-action@889f3180bb5f064ee9e3201428d04ae9e41d54ad # v31
3939

4040
- uses: nix-community/cache-nix-action@135667ec418502fa5a3598af6fb9eb733888ce6a # v6.1.3
4141
with:
@@ -72,7 +72,7 @@ jobs:
7272
uses: depot/setup-action@b0b1ea4f69e92ebf5dea3f8713a1b0c37b2126a5 # v1.6.0
7373

7474
- name: Set up Docker Buildx
75-
uses: docker/setup-buildx-action@b5ca514318bd6ebac0fb2aedd5d36ec1b5c232a2 # v3.10.0
75+
uses: docker/setup-buildx-action@e468171a9de216ec08956ac3ada2f0791b6bd435 # v3.11.1
7676

7777
- name: Login to DockerHub
7878
if: github.ref == 'refs/heads/main'
@@ -82,7 +82,7 @@ jobs:
8282
password: ${{ secrets.DOCKERHUB_PASSWORD }}
8383

8484
- name: Build and push Non-Nix image
85-
uses: depot/build-push-action@636daae76684e38c301daa0c5eca1c095b24e780 # v1.14.0
85+
uses: depot/build-push-action@2583627a84956d07561420dcc1d0eb1f2af3fac0 # v1.15.0
8686
with:
8787
project: b4q6ltmpzh
8888
token: ${{ secrets.DEPOT_TOKEN }}

.github/workflows/release.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -364,7 +364,7 @@ jobs:
364364
# This uses OIDC authentication, so no auth variables are required.
365365
- name: Build base Docker image via depot.dev
366366
if: steps.image-base-tag.outputs.tag != ''
367-
uses: depot/build-push-action@636daae76684e38c301daa0c5eca1c095b24e780 # v1.14.0
367+
uses: depot/build-push-action@2583627a84956d07561420dcc1d0eb1f2af3fac0 # v1.15.0
368368
with:
369369
project: wl5hnrrkns
370370
context: base-build-context

CLAUDE.md

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -103,5 +103,4 @@ Read [cursor rules](.cursorrules).
103103

104104
The frontend is contained in the site folder.
105105

106-
For building Frontend refer to [this document](docs/contributing/frontend.md)
107106
For building Frontend refer to [this document](docs/about/contributing/frontend.md)

agent/agentcontainers/api.go

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ import (
2424
"cdr.dev/slog"
2525
"github.com/coder/coder/v2/agent/agentcontainers/watcher"
2626
"github.com/coder/coder/v2/agent/agentexec"
27+
"github.com/coder/coder/v2/agent/usershell"
2728
"github.com/coder/coder/v2/coderd/httpapi"
2829
"github.com/coder/coder/v2/codersdk"
2930
"github.com/coder/coder/v2/codersdk/agentsdk"
@@ -58,6 +59,7 @@ type API struct {
5859
logger slog.Logger
5960
watcher watcher.Watcher
6061
execer agentexec.Execer
62+
commandEnv CommandEnv
6163
ccli ContainerCLI
6264
containerLabelIncludeFilter map[string]string // Labels to filter containers by.
6365
dccli DevcontainerCLI
@@ -111,6 +113,29 @@ func WithExecer(execer agentexec.Execer) Option {
111113
}
112114
}
113115

116+
// WithCommandEnv sets the CommandEnv implementation to use.
117+
func WithCommandEnv(ce CommandEnv) Option {
118+
return func(api *API) {
119+
api.commandEnv = func(ei usershell.EnvInfoer, preEnv []string) (string, string, []string, error) {
120+
shell, dir, env, err := ce(ei, preEnv)
121+
if err != nil {
122+
return shell, dir, env, err
123+
}
124+
env = slices.DeleteFunc(env, func(s string) bool {
125+
// Ensure we filter out environment variables that come
126+
// from the parent agent and are incorrect or not
127+
// relevant for the devcontainer.
128+
return strings.HasPrefix(s, "CODER_WORKSPACE_AGENT_NAME=") ||
129+
strings.HasPrefix(s, "CODER_WORKSPACE_AGENT_URL=") ||
130+
strings.HasPrefix(s, "CODER_AGENT_TOKEN=") ||
131+
strings.HasPrefix(s, "CODER_AGENT_AUTH=") ||
132+
strings.HasPrefix(s, "CODER_AGENT_DEVCONTAINERS_ENABLE=")
133+
})
134+
return shell, dir, env, nil
135+
}
136+
}
137+
}
138+
114139
// WithContainerCLI sets the agentcontainers.ContainerCLI implementation
115140
// to use. The default implementation uses the Docker CLI.
116141
func WithContainerCLI(ccli ContainerCLI) Option {
@@ -153,7 +178,7 @@ func WithSubAgentURL(url string) Option {
153178
}
154179
}
155180

156-
// WithSubAgent sets the environment variables for the sub-agent.
181+
// WithSubAgentEnv sets the environment variables for the sub-agent.
157182
func WithSubAgentEnv(env ...string) Option {
158183
return func(api *API) {
159184
api.subAgentEnv = env
@@ -262,6 +287,13 @@ func NewAPI(logger slog.Logger, options ...Option) *API {
262287
for _, opt := range options {
263288
opt(api)
264289
}
290+
if api.commandEnv != nil {
291+
api.execer = newCommandEnvExecer(
292+
api.logger,
293+
api.commandEnv,
294+
api.execer,
295+
)
296+
}
265297
if api.ccli == nil {
266298
api.ccli = NewDockerCLI(api.execer)
267299
}

agent/agentcontainers/api_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"net/http"
99
"net/http/httptest"
1010
"os"
11+
"os/exec"
1112
"runtime"
1213
"slices"
1314
"strings"
@@ -30,7 +31,9 @@ import (
3031
"github.com/coder/coder/v2/agent/agentcontainers/acmock"
3132
"github.com/coder/coder/v2/agent/agentcontainers/watcher"
3233
"github.com/coder/coder/v2/agent/agentexec"
34+
"github.com/coder/coder/v2/agent/usershell"
3335
"github.com/coder/coder/v2/codersdk"
36+
"github.com/coder/coder/v2/pty"
3437
"github.com/coder/coder/v2/testutil"
3538
"github.com/coder/quartz"
3639
)
@@ -305,6 +308,38 @@ func (m *fakeSubAgentClient) Delete(ctx context.Context, id uuid.UUID) error {
305308
return nil
306309
}
307310

311+
// fakeExecer implements agentexec.Execer for testing and tracks execution details.
312+
type fakeExecer struct {
313+
commands [][]string
314+
createdCommands []*exec.Cmd
315+
}
316+
317+
func (f *fakeExecer) CommandContext(ctx context.Context, cmd string, args ...string) *exec.Cmd {
318+
f.commands = append(f.commands, append([]string{cmd}, args...))
319+
// Create a command that returns empty JSON for docker commands.
320+
c := exec.CommandContext(ctx, "echo", "[]")
321+
f.createdCommands = append(f.createdCommands, c)
322+
return c
323+
}
324+
325+
func (f *fakeExecer) PTYCommandContext(ctx context.Context, cmd string, args ...string) *pty.Cmd {
326+
f.commands = append(f.commands, append([]string{cmd}, args...))
327+
return &pty.Cmd{
328+
Context: ctx,
329+
Path: cmd,
330+
Args: append([]string{cmd}, args...),
331+
Env: []string{},
332+
Dir: "",
333+
}
334+
}
335+
336+
func (f *fakeExecer) getLastCommand() *exec.Cmd {
337+
if len(f.createdCommands) == 0 {
338+
return nil
339+
}
340+
return f.createdCommands[len(f.createdCommands)-1]
341+
}
342+
308343
func TestAPI(t *testing.T) {
309344
t.Parallel()
310345

@@ -1984,6 +2019,57 @@ func TestAPI(t *testing.T) {
19842019
// Then: We expected it to succeed
19852020
require.Len(t, fSAC.created, 1)
19862021
})
2022+
2023+
t.Run("CommandEnv", func(t *testing.T) {
2024+
t.Parallel()
2025+
2026+
ctx := testutil.Context(t, testutil.WaitShort)
2027+
logger := slogtest.Make(t, &slogtest.Options{IgnoreErrors: true}).Leveled(slog.LevelDebug)
2028+
2029+
// Create fake execer to track execution details.
2030+
fakeExec := &fakeExecer{}
2031+
2032+
// Custom CommandEnv that returns specific values.
2033+
testShell := "/bin/custom-shell"
2034+
testDir := t.TempDir()
2035+
testEnv := []string{"CUSTOM_VAR=test_value", "PATH=/custom/path"}
2036+
2037+
commandEnv := func(ei usershell.EnvInfoer, addEnv []string) (shell, dir string, env []string, err error) {
2038+
return testShell, testDir, testEnv, nil
2039+
}
2040+
2041+
mClock := quartz.NewMock(t) // Stop time.
2042+
2043+
// Create API with CommandEnv.
2044+
api := agentcontainers.NewAPI(logger,
2045+
agentcontainers.WithClock(mClock),
2046+
agentcontainers.WithExecer(fakeExec),
2047+
agentcontainers.WithCommandEnv(commandEnv),
2048+
)
2049+
defer api.Close()
2050+
2051+
// Call RefreshContainers directly to trigger CommandEnv usage.
2052+
_ = api.RefreshContainers(ctx) // Ignore error since docker commands will fail.
2053+
2054+
// Verify commands were executed through the custom shell and environment.
2055+
require.NotEmpty(t, fakeExec.commands, "commands should be executed")
2056+
2057+
// Want: /bin/custom-shell -c "docker ps --all --quiet --no-trunc"
2058+
require.Equal(t, testShell, fakeExec.commands[0][0], "custom shell should be used")
2059+
if runtime.GOOS == "windows" {
2060+
require.Equal(t, "/c", fakeExec.commands[0][1], "shell should be called with /c on Windows")
2061+
} else {
2062+
require.Equal(t, "-c", fakeExec.commands[0][1], "shell should be called with -c")
2063+
}
2064+
require.Len(t, fakeExec.commands[0], 3, "command should have 3 arguments")
2065+
require.GreaterOrEqual(t, strings.Count(fakeExec.commands[0][2], " "), 2, "command/script should have multiple arguments")
2066+
2067+
// Verify the environment was set on the command.
2068+
lastCmd := fakeExec.getLastCommand()
2069+
require.NotNil(t, lastCmd, "command should be created")
2070+
require.Equal(t, testDir, lastCmd.Dir, "custom directory should be used")
2071+
require.Equal(t, testEnv, lastCmd.Env, "custom environment should be used")
2072+
})
19872073
}
19882074

19892075
// mustFindDevcontainerByPath returns the devcontainer with the given workspace

agent/agentcontainers/devcontainercli.go

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import (
77
"encoding/json"
88
"errors"
99
"io"
10-
"os"
1110

1211
"golang.org/x/xerrors"
1312

@@ -280,7 +279,6 @@ func (d *devcontainerCLI) ReadConfig(ctx context.Context, workspaceFolder, confi
280279
}
281280

282281
c := d.execer.CommandContext(ctx, "devcontainer", args...)
283-
c.Env = append(c.Env, "PATH="+os.Getenv("PATH"))
284282
c.Env = append(c.Env, env...)
285283

286284
var stdoutBuf bytes.Buffer

agent/agentcontainers/execer.go

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
package agentcontainers
2+
3+
import (
4+
"context"
5+
"os/exec"
6+
"runtime"
7+
8+
"github.com/kballard/go-shellquote"
9+
10+
"cdr.dev/slog"
11+
"github.com/coder/coder/v2/agent/agentexec"
12+
"github.com/coder/coder/v2/agent/usershell"
13+
"github.com/coder/coder/v2/pty"
14+
)
15+
16+
// CommandEnv is a function that returns the shell, working directory,
17+
// and environment variables to use when executing a command. It takes
18+
// an EnvInfoer and a pre-existing environment slice as arguments.
19+
// This signature matches agentssh.Server.CommandEnv.
20+
type CommandEnv func(ei usershell.EnvInfoer, addEnv []string) (shell, dir string, env []string, err error)
21+
22+
// commandEnvExecer is an agentexec.Execer that uses a CommandEnv to
23+
// determine the shell, working directory, and environment variables
24+
// for commands. It wraps another agentexec.Execer to provide the
25+
// necessary context.
26+
type commandEnvExecer struct {
27+
logger slog.Logger
28+
commandEnv CommandEnv
29+
execer agentexec.Execer
30+
}
31+
32+
func newCommandEnvExecer(
33+
logger slog.Logger,
34+
commandEnv CommandEnv,
35+
execer agentexec.Execer,
36+
) *commandEnvExecer {
37+
return &commandEnvExecer{
38+
logger: logger,
39+
commandEnv: commandEnv,
40+
execer: execer,
41+
}
42+
}
43+
44+
// Ensure commandEnvExecer implements agentexec.Execer.
45+
var _ agentexec.Execer = (*commandEnvExecer)(nil)
46+
47+
func (e *commandEnvExecer) prepare(ctx context.Context, inName string, inArgs ...string) (name string, args []string, dir string, env []string) {
48+
shell, dir, env, err := e.commandEnv(nil, nil)
49+
if err != nil {
50+
e.logger.Error(ctx, "get command environment failed", slog.Error(err))
51+
return inName, inArgs, "", nil
52+
}
53+
54+
caller := "-c"
55+
if runtime.GOOS == "windows" {
56+
caller = "/c"
57+
}
58+
name = shell
59+
args = []string{caller, shellquote.Join(append([]string{inName}, inArgs...)...)}
60+
return name, args, dir, env
61+
}
62+
63+
func (e *commandEnvExecer) CommandContext(ctx context.Context, cmd string, args ...string) *exec.Cmd {
64+
name, args, dir, env := e.prepare(ctx, cmd, args...)
65+
c := e.execer.CommandContext(ctx, name, args...)
66+
c.Dir = dir
67+
c.Env = env
68+
return c
69+
}
70+
71+
func (e *commandEnvExecer) PTYCommandContext(ctx context.Context, cmd string, args ...string) *pty.Cmd {
72+
name, args, dir, env := e.prepare(ctx, cmd, args...)
73+
c := e.execer.PTYCommandContext(ctx, name, args...)
74+
c.Dir = dir
75+
c.Env = env
76+
return c
77+
}

0 commit comments

Comments
 (0)