Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
17 commits
Select commit Hold shift + click to select a range
3e49ed9
fix(devcontainer): persist profile selection
skevetter Sep 24, 2026
348048c
fix(devcontainer): normalize legacy subpath
skevetter Sep 25, 2026
d5a409b
fix(devcontainer): normalize persisted subpath
skevetter Sep 25, 2026
0b91ef8
fix(devcontainer): restore embedded config priority and pin path sema…
skevetter Sep 25, 2026
0af0e67
refactor(devcontainer): extract last-path fallback and wrap long lines
skevetter Sep 25, 2026
85b766d
fix(devcontainer): normalize leading slash in stored git subpath
skevetter Sep 25, 2026
72c082d
fix(devcontainer): skip stale last-path fallback after content reset
skevetter Sep 25, 2026
c690070
refactor(devcontainer): trim redundant comments
skevetter Sep 25, 2026
847f906
test(e2e): assert the retained profile after selector-free restart
skevetter Sep 25, 2026
4dab18b
refactor(e2e): trim profile assertion comment
skevetter Sep 25, 2026
cb2dbdc
fix: embedded devcontainer config outranks persisted path/id selectors
skevetter Sep 25, 2026
10a76f9
fix: honor the effective devcontainer selector on the crane path
skevetter Sep 25, 2026
ac08840
test: hoist the repeated embedded image marker into a constant
skevetter Sep 25, 2026
4c7c615
fix: treat only not-exist as absent in the last-path staleness check
skevetter Sep 25, 2026
e52154f
refactor(devcontainer): trim redundant comments
skevetter Sep 25, 2026
a11a6b9
test(e2e): tolerate transient rootful Podman SSH stalls
skevetter Sep 25, 2026
3ca4ed8
test(e2e): read lifecycle markers from workspace
skevetter Sep 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions cmd/ci/ci.go
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,8 @@ func (cmd *CICmd) resolveWorkspace(
ProviderUserOptions: cmd.ProviderOptions,
DevContainerImage: cmd.DevContainerImage,
DevContainerPath: cmd.DevContainerPath,
DevContainerID: cmd.DevContainerID,
DevContainerSource: cmd.DevContainerSource,
SSHConfigPath: sshConfigPath,
UID: cmd.UID,
Owner: cmd.Owner,
Expand Down
5 changes: 4 additions & 1 deletion cmd/workspace/up/agent.go
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,10 @@ func (cmd *UpCmd) devsyUpDaemon(
func (cmd *UpCmd) buildWorkspaceOptions(workspace *provider2.Workspace) provider2.CLIOptions {
baseOptions := cmd.CLIOptions
baseOptions.ID = workspace.ID
baseOptions.DevContainerPath = workspace.DevContainerPath
if workspace.DevContainerConfig == nil {
baseOptions.DevContainerPath = workspace.DevContainerPath
baseOptions.DevContainerID = workspace.DevContainerID
}
baseOptions.DevContainerImage = workspace.DevContainerImage
baseOptions.DevContainerSource = workspace.DevContainerSource
baseOptions.IDE = workspace.IDE.Name
Expand Down
1 change: 1 addition & 0 deletions cmd/workspace/up/up_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,6 +217,7 @@ func (cmd *UpCmd) resolveParams(
ReconfigureProvider: cmd.Reconfigure,
DevContainerImage: cmd.DevContainerImage,
DevContainerPath: cmd.DevContainerPath,
DevContainerID: cmd.DevContainerID,
DevContainerSource: cmd.DevContainerSource,
SSHConfigPath: cmd.SSHConfigPath,
SSHConfigIncludePath: devsyConfig.ContextOption(
Expand Down
38 changes: 38 additions & 0 deletions e2e/tests/up-docker-compose/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import (
pkgconfig "github.com/devsy-org/devsy/pkg/config"
"github.com/devsy-org/devsy/pkg/devcontainer/config"
docker "github.com/devsy-org/devsy/pkg/docker"
"github.com/devsy-org/devsy/pkg/flags/names"
provider2 "github.com/devsy-org/devsy/pkg/provider"
"github.com/devsy-org/devsy/pkg/status"
"github.com/docker/docker/api/types/container"
Expand Down Expand Up @@ -142,6 +143,43 @@ var _ = ginkgo.Describe(
gomega.Expect(restartIds).To(gomega.HaveLen(1), "1 compose container after restart")
}, ginkgo.SpecTimeout(framework.TimeoutLong()))

ginkgo.It("multi-profile lifecycle retains selected profile", func(ctx context.Context) {
tempDir, err := setupWorkspace(
"tests/up-docker-compose/testdata/docker-compose-multi-profile",
tc.initialDir,
tc.f,
)
framework.ExpectNoError(err)

err = tc.f.DevsyUp(ctx, names.Flag(names.DevContainer), "id:max", tempDir)
framework.ExpectNoError(err)

err = tc.f.DevsyWorkspaceStop(ctx, tempDir)
framework.ExpectNoError(err)

err = tc.f.DevsyUp(ctx, tempDir)
framework.ExpectNoError(err)
Comment thread
greptile-apps[bot] marked this conversation as resolved.

workspace, err := tc.f.FindWorkspace(ctx, tempDir)
framework.ExpectNoError(err)

// Distinct markers verify the persisted selector survived restart.
err = tc.f.ExecCommand(
ctx,
true,
true,
"[max]",
[]string{
cmdWorkspace, cmdSSH, flagCommand,
"echo \"[$SELECTED_PROFILE]\"", workspace.ID,
},
)
Comment thread
coderabbitai[bot] marked this conversation as resolved.
framework.ExpectNoError(err)

err = tc.f.DevsyWorkspaceDelete(ctx, tempDir)
framework.ExpectNoError(err)
}, ginkgo.SpecTimeout(framework.TimeoutLong()))

ginkgo.It("environment variables", func(ctx context.Context) {
_, workspace, err := tc.setupAndStartWorkspace(
ctx,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
services:
app:
image: ghcr.io/devsy-org/test-images/go:1
command: sleep infinity
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "max-nvidia",
"dockerComposeFile": "../compose.yaml",
"service": "app",
"workspaceFolder": "/workspaces",
"containerEnv": {
"SELECTED_PROFILE": "max-nvidia"
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
"name": "max",
"dockerComposeFile": "../compose.yaml",
"service": "app",
"workspaceFolder": "/workspaces",
"containerEnv": {
"SELECTED_PROFILE": "max"
}
}
9 changes: 9 additions & 0 deletions e2e/tests/up/helper.go
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,15 @@ func probeSSH(
return out, err
}

func readLifecycleFile(workspaceDir, name string) (string, error) {
//nolint:gosec // G304: test-controlled path inside workspace
data, err := os.ReadFile(filepath.Join(workspaceDir, name))
if err != nil {
return "", err
}
return strings.TrimSpace(string(data)), nil
}

// lifecycleMarkerCount reads a marker file in workspaceDir and returns the count
// of non-empty lines. If the file does not exist, it returns 0, nil.
//
Expand Down
36 changes: 10 additions & 26 deletions e2e/tests/up/provider_docker.go
Original file line number Diff line number Diff line change
Expand Up @@ -394,47 +394,31 @@ var _ = ginkgo.Describe(
err = dtc.f.DevsyUp(ctx, tempDir)
framework.ExpectNoError(err)

// onCreateCommand and updateContentCommand should have run (foreground).
out, err := dtc.execSSH(ctx, tempDir, "cat $HOME/on-create.out")
out, err := readLifecycleFile(tempDir, "on-create.out")
framework.ExpectNoError(err)
gomega.Expect(strings.TrimSpace(out)).To(gomega.Equal("onCreateDone"))
gomega.Expect(out).To(gomega.Equal("onCreateDone"))

out, err = dtc.execSSH(ctx, tempDir, "cat $HOME/update-content.out")
out, err = readLifecycleFile(tempDir, "update-content.out")
framework.ExpectNoError(err)
gomega.Expect(strings.TrimSpace(out)).To(gomega.Equal("updateContentDone"))
gomega.Expect(out).To(gomega.Equal("updateContentDone"))

// postCreateCommand runs as a deferred hook in the background.
// Wait for it to complete and verify the marker file + env substitution.
gomega.Eventually(func() string {
out, err := dtc.execSSH(ctx, tempDir, "cat $HOME/deferred.marker 2>/dev/null")
if err != nil {
return ""
}
return strings.TrimSpace(out)
// Deferred hooks run in the background; verify completion and env substitution.
gomega.Eventually(func() (string, error) {
return readLifecycleFile(tempDir, "deferred.marker")
}).WithTimeout(30*time.Second).WithPolling(2*time.Second).Should(
gomega.Equal("postCreateDone"),
"deferred postCreateCommand should eventually complete in background",
)

// Verify the deferred hook received substituted env vars, not literals.
envPath, err := dtc.execSSH(ctx, tempDir, "cat $HOME/deferred-env-path.out")
envPath, err := readLifecycleFile(tempDir, "deferred-env-path.out")
framework.ExpectNoError(err)
gomega.Expect(envPath).To(gomega.ContainSubstring("/usr/local/bin"),
"deferred hook should receive resolved PATH, not ${containerEnv:PATH}")
gomega.Expect(envPath).NotTo(gomega.ContainSubstring("${containerEnv:"),
"deferred hook should not contain literal variable references")

// postStartCommand also deferred — verify it ran.
gomega.Eventually(func() string {
out, err := dtc.execSSH(
ctx,
tempDir,
"cat $HOME/post-start-deferred.out 2>/dev/null",
)
if err != nil {
return ""
}
return strings.TrimSpace(out)
gomega.Eventually(func() (string, error) {
return readLifecycleFile(tempDir, "post-start-deferred.out")
}).WithTimeout(30*time.Second).WithPolling(2*time.Second).Should(
gomega.Equal("postStartDone"),
"deferred postStartCommand should eventually complete in background",
Expand Down
34 changes: 9 additions & 25 deletions e2e/tests/up/provider_podman_rootful_lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,45 +108,29 @@ var _ = ginkgo.Describe(
err = f.DevsyUp(ctx, tempDir)
framework.ExpectNoError(err)

out, err := f.DevsySSH(ctx, tempDir, "cat $HOME/on-create.out")
out, err := readLifecycleFile(tempDir, "on-create.out")
framework.ExpectNoError(err)
gomega.Expect(strings.TrimSpace(out)).To(gomega.Equal("onCreateDone"))
gomega.Expect(out).To(gomega.Equal("onCreateDone"))

out, err = f.DevsySSH(ctx, tempDir, "cat $HOME/update-content.out")
out, err = readLifecycleFile(tempDir, "update-content.out")
framework.ExpectNoError(err)
gomega.Expect(strings.TrimSpace(out)).To(gomega.Equal("updateContentDone"))
gomega.Expect(out).To(gomega.Equal("updateContentDone"))

gomega.Eventually(func() string {
out, err := probeSSH(f,
ctx, tempDir, "cat $HOME/deferred.marker 2>/dev/null",
)
if err != nil {
return ""
}
return strings.TrimSpace(out)
gomega.Eventually(func() (string, error) {
return readLifecycleFile(tempDir, "deferred.marker")
}).WithTimeout(30 * time.Second).WithPolling(2 * time.Second).Should(
gomega.Equal("postCreateDone"),
)

envPath, err := f.DevsySSH(
ctx, tempDir, "cat $HOME/deferred-env-path.out",
)
envPath, err := readLifecycleFile(tempDir, "deferred-env-path.out")
framework.ExpectNoError(err)
gomega.Expect(envPath).To(
gomega.ContainSubstring("/usr/local/bin"),
)
gomega.Expect(envPath).NotTo(gomega.ContainSubstring("${containerEnv:"))

gomega.Eventually(func() string {
out, err := probeSSH(f,
ctx,
tempDir,
"cat $HOME/post-start-deferred.out 2>/dev/null",
)
if err != nil {
return ""
}
return strings.TrimSpace(out)
gomega.Eventually(func() (string, error) {
return readLifecycleFile(tempDir, "post-start-deferred.out")
}).WithTimeout(30 * time.Second).WithPolling(2 * time.Second).Should(
gomega.Equal("postStartDone"),
)
Expand Down
34 changes: 9 additions & 25 deletions e2e/tests/up/provider_podman_rootless_lifecycle.go
Original file line number Diff line number Diff line change
Expand Up @@ -101,45 +101,29 @@ var _ = ginkgo.Describe(
err = f.DevsyUp(ctx, tempDir)
framework.ExpectNoError(err)

out, err := f.DevsySSH(ctx, tempDir, "cat $HOME/on-create.out")
out, err := readLifecycleFile(tempDir, "on-create.out")
framework.ExpectNoError(err)
gomega.Expect(strings.TrimSpace(out)).To(gomega.Equal("onCreateDone"))
gomega.Expect(out).To(gomega.Equal("onCreateDone"))

out, err = f.DevsySSH(ctx, tempDir, "cat $HOME/update-content.out")
out, err = readLifecycleFile(tempDir, "update-content.out")
framework.ExpectNoError(err)
gomega.Expect(strings.TrimSpace(out)).To(gomega.Equal("updateContentDone"))
gomega.Expect(out).To(gomega.Equal("updateContentDone"))

gomega.Eventually(func() string {
out, err := probeSSH(f,
ctx, tempDir, "cat $HOME/deferred.marker 2>/dev/null",
)
if err != nil {
return ""
}
return strings.TrimSpace(out)
gomega.Eventually(func() (string, error) {
return readLifecycleFile(tempDir, "deferred.marker")
}).WithTimeout(30 * time.Second).WithPolling(2 * time.Second).Should(
gomega.Equal("postCreateDone"),
)

envPath, err := f.DevsySSH(
ctx, tempDir, "cat $HOME/deferred-env-path.out",
)
envPath, err := readLifecycleFile(tempDir, "deferred-env-path.out")
framework.ExpectNoError(err)
gomega.Expect(envPath).To(
gomega.ContainSubstring("/usr/local/bin"),
)
gomega.Expect(envPath).NotTo(gomega.ContainSubstring("${containerEnv:"))

gomega.Eventually(func() string {
out, err := probeSSH(f,
ctx,
tempDir,
"cat $HOME/post-start-deferred.out 2>/dev/null",
)
if err != nil {
return ""
}
return strings.TrimSpace(out)
gomega.Eventually(func() (string, error) {
return readLifecycleFile(tempDir, "post-start-deferred.out")
}).WithTimeout(30 * time.Second).WithPolling(2 * time.Second).Should(
gomega.Equal("postStartDone"),
)
Expand Down
8 changes: 4 additions & 4 deletions e2e/tests/up/testdata/docker-waitfor/.devcontainer.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,8 @@
"remoteEnv": {
"CONTAINER_ENV_PATH": "${containerEnv:PATH}"
},
"onCreateCommand": "echo onCreateDone > $HOME/on-create.out",
"updateContentCommand": "echo updateContentDone > $HOME/update-content.out",
"postCreateCommand": "echo -n ${CONTAINER_ENV_PATH} > $HOME/deferred-env-path.out && echo postCreateDone > $HOME/deferred.marker",
"postStartCommand": "echo postStartDone > $HOME/post-start-deferred.out"
"onCreateCommand": "echo onCreateDone > on-create.out",
"updateContentCommand": "echo updateContentDone > update-content.out",
"postCreateCommand": "echo -n ${CONTAINER_ENV_PATH} > deferred-env-path.out && echo postCreateDone > deferred.marker",
"postStartCommand": "echo postStartDone > post-start-deferred.out"
}
Loading
Loading