From 3207ac9a4ecca1ebc16d9f74859d53aa94890e86 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Fri, 7 Aug 2026 23:35:36 +0000 Subject: [PATCH 1/2] fix(workspace): forward gpg on browser open Signed-off-by: GitHub --- cmd/workspace/up/configure.go | 21 ++++++-- cmd/workspace/up/configure_test.go | 84 ++++++++++++++++++++++++++++++ e2e/tests/ide/browser_returns.go | 78 +++++++++++++++++++++++++++ 3 files changed, 180 insertions(+), 3 deletions(-) create mode 100644 cmd/workspace/up/configure_test.go diff --git a/cmd/workspace/up/configure.go b/cmd/workspace/up/configure.go index 2f7bec4367..6d3182c577 100644 --- a/cmd/workspace/up/configure.go +++ b/cmd/workspace/up/configure.go @@ -77,8 +77,23 @@ func (cmd *UpCmd) openIDE( } ideConfig := client.WorkspaceConfig().IDE - return opener.Open(ctx, ideConfig.Name, ideConfig.Options, opener.IDEParams{ - GPGAgentForwarding: cmd.GPGAgentForwarding, + return opener.Open(ctx, ideConfig.Name, ideConfig.Options, cmd.buildIDEParams( + devsyConfig, + client, + wctx, + )) +} + +func (cmd *UpCmd) buildIDEParams( + devsyConfig *config.Config, + client client2.BaseWorkspaceClient, + wctx *workspaceContext, +) opener.IDEParams { + setupGPGAgentForwarding := cmd.GPGAgentForwarding || + devsyConfig.ContextOptionBool(config.ContextOptionGPGAgentForwarding) + + return opener.IDEParams{ + GPGAgentForwarding: setupGPGAgentForwarding, SSHAuthSockID: cmd.SSHAuthSockID, GitSSHSigningKey: cmd.GitSSHSigningKey, DevsyConfig: devsyConfig, @@ -87,7 +102,7 @@ func (cmd *UpCmd) openIDE( Result: wctx.result, TunnelMode: wctx.tunnelPort > 0, Launch: cmd.IDELaunch, - }) + } } func (cmd *UpCmd) reconfigureSSHWithTunnel( diff --git a/cmd/workspace/up/configure_test.go b/cmd/workspace/up/configure_test.go new file mode 100644 index 0000000000..c09bef9d95 --- /dev/null +++ b/cmd/workspace/up/configure_test.go @@ -0,0 +1,84 @@ +package up + +import ( + "testing" + + "github.com/devsy-org/devsy/cmd/flags" + "github.com/devsy-org/devsy/pkg/config" + config2 "github.com/devsy-org/devsy/pkg/devcontainer/config" + "github.com/devsy-org/devsy/pkg/ide/opener" + provider2 "github.com/devsy-org/devsy/pkg/provider" + "github.com/stretchr/testify/assert" +) + +func TestBuildIDEParams_GPGAgentForwardingParity(t *testing.T) { + tests := []struct { + name string + cmdFlag bool + contextOption string + expected bool + }{ + { + name: "enabled by context option only", + cmdFlag: false, + contextOption: config.BoolTrue, + expected: true, + }, + { + name: "enabled by cli flag only", + cmdFlag: true, + contextOption: config.BoolFalse, + expected: true, + }, + { + name: "disabled when both are off", + cmdFlag: false, + contextOption: config.BoolFalse, + expected: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + cmd := &UpCmd{ + GlobalFlags: &flags.GlobalFlags{}, + CLIOptions: provider2.CLIOptions{ + SSHAuthSockID: "sock-id", + GitSSHSigningKey: "ssh-sign-key", + }, + GPGAgentForwarding: tt.cmdFlag, + IDELaunch: opener.LaunchAuto, + } + + devsyConfig := testConfigWithGPGForwardingOption(tt.contextOption) + wctx := &workspaceContext{ + user: "vscode", + result: &config2.Result{}, + tunnelPort: 10800, + } + + params := cmd.buildIDEParams(devsyConfig, nil, wctx) + + assert.Equal(t, tt.expected, params.GPGAgentForwarding) + assert.Equal(t, "sock-id", params.SSHAuthSockID) + assert.Equal(t, "ssh-sign-key", params.GitSSHSigningKey) + assert.Equal(t, "vscode", params.User) + assert.Equal(t, wctx.result, params.Result) + assert.True(t, params.TunnelMode) + assert.Equal(t, opener.LaunchAuto, params.Launch) + }) + } +} + +func testConfigWithGPGForwardingOption(value string) *config.Config { + return &config.Config{ + DefaultContext: config.DefaultContext, + Contexts: map[string]*config.ContextConfig{ + config.DefaultContext: { + Options: map[string]config.OptionValue{ + config.ContextOptionGPGAgentForwarding: {Value: value}, + }, + }, + }, + } +} diff --git a/e2e/tests/ide/browser_returns.go b/e2e/tests/ide/browser_returns.go index 8b1f094d11..026cd74b7c 100644 --- a/e2e/tests/ide/browser_returns.go +++ b/e2e/tests/ide/browser_returns.go @@ -16,6 +16,7 @@ import ( "time" "github.com/devsy-org/devsy/e2e/framework" + "github.com/devsy-org/devsy/pkg/config" "github.com/devsy-org/devsy/pkg/flags/names" "github.com/devsy-org/devsy/pkg/ide/opener" "github.com/onsi/ginkgo/v2" @@ -365,6 +366,83 @@ var _ = ginkgo.Describe( }, ) + ginkgo.It( + "forwards GPG in browser IDE when enabled only via context option", + ginkgo.Label("gpg"), + ginkgo.SpecTimeout(framework.TimeoutLong()), + func(ctx context.Context) { + f := framework.NewDefaultFramework(initialDir + "/bin") + tempDir, err := framework.CopyToTempDir("tests/ide/testdata-gpg-nonroot") + framework.ExpectNoError(err) + ginkgo.DeferCleanup(framework.CleanupTempDir, initialDir, tempDir) + + contextName := fmt.Sprintf("gpg-ctx-%d", time.Now().UnixNano()) + err = f.DevsyContextCreate(ctx, contextName) + framework.ExpectNoError(err) + ginkgo.DeferCleanup(func(cleanupCtx context.Context) { + _ = f.DevsyContextUse(cleanupCtx, config.DefaultContext) + _ = f.DevsyContextDelete(cleanupCtx, contextName) + }) + + err = f.DevsyContextUse(ctx, contextName) + framework.ExpectNoError(err) + err = f.ExecCommand(ctx, false, true, "", []string{ + "context", "set", + names.Flag(names.Option), config.ContextOptionGPGAgentForwarding + "=" + config.BoolTrue, + }) + framework.ExpectNoError(err) + + err = f.DevsyProviderAdd(ctx, "docker") + framework.ExpectNoError(err) + err = f.DevsyProviderUse(ctx, "docker") + framework.ExpectNoError(err) + ginkgo.DeferCleanup(func(cleanupCtx context.Context) { + _ = f.DevsyWorkspaceDelete(cleanupCtx, tempDir) + }) + + ginkgo.GinkgoT().Setenv("GNUPGHOME", ginkgo.GinkgoT().TempDir()) + framework.ExpectNoError( + framework.ImportGpgKey( + filepath.Join( + initialDir, + "tests/ssh/testdata/gpg-forwarding/gpg-private.key", + ), + ), + ) + + stdout, stderr, err := f.DevsyUpStreamsRaw(ctx, tempDir, + "--ide=openvscode", "--ide-launch=headless", "--debug") + framework.ExpectNoError(err) + combined := stdout + stderr + gomega.Expect(combined).To(gomega.ContainSubstring("Starting vscode in browser mode at"), + "expected browser IDE opener path to execute; got:\n%s", combined) + gomega.Expect(combined).To(gomega.ContainSubstring("forwarding gpg-agent"), + "expected browser IDE GPG forward bootstrap to run; got:\n%s", combined) + + gomega.Expect(combined).NotTo(gomega.ContainSubstring("continuing without it"), + "GPG agent forwarding must succeed when enabled from context option only; got:\n%s", combined) + gomega.Expect(combined).NotTo(gomega.ContainSubstring( + "timed out waiting for gpg-agent forward to become ready", + ), "GPG forward should be ready before handing off browser IDE session; got:\n%s", combined) + + ws, err := f.FindWorkspace(ctx, tempDir) + framework.ExpectNoError(err) + state, err := opener.ReadTunnelState(ws.Context, ws.ID) + framework.ExpectNoError(err) + gomega.Expect(state).NotTo(gomega.BeNil(), + "expected browser tunnel state to exist for IDE session") + gomega.Expect(state.PID).To(gomega.BeNumerically(">", 0), + "expected detached browser tunnel process to be running") + + sshCtx, cancelSSH := context.WithDeadline(ctx, time.Now().Add(30*time.Second)) + defer cancelSSH() + err = f.DevsySSHGpgSecretKeyForwarded(sshCtx, tempDir, gpgTestKeyFingerprint) + framework.ExpectNoError(err) + + framework.ExpectNoError(f.DevsyStop(ctx, tempDir)) + }, + ) + ginkgo.It( "does not log 'setup KubeConfig' on a workspace without kubeconfig forwarding", ginkgo.SpecTimeout(framework.TimeoutLong()), From 80358a019c05a5cfc942fa7848b61c5d20c5b662 Mon Sep 17 00:00:00 2001 From: Samuel K Date: Fri, 7 Aug 2026 23:49:09 +0000 Subject: [PATCH 2/2] fix: lint errors Signed-off-by: GitHub --- .github/workflows/workflow-approval.yml | 4 ++-- cmd/workspace/up/configure_test.go | 6 ++++-- e2e/tests/ide/browser_returns.go | 21 +++++++++++++++------ 3 files changed, 21 insertions(+), 10 deletions(-) diff --git a/.github/workflows/workflow-approval.yml b/.github/workflows/workflow-approval.yml index ed20463937..e1e442769e 100644 --- a/.github/workflows/workflow-approval.yml +++ b/.github/workflows/workflow-approval.yml @@ -13,10 +13,10 @@ jobs: - uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3 id: app-token with: - app-id: ${{ secrets.DEVSY_GITHUB_APP_ID }} + client-id: ${{ secrets.DEVSY_GITHUB_APP_ID }} private-key: ${{ secrets.DEVSY_GITHUB_APP_PRIVATE_KEY }} - uses: skevetter/automatic-approve-action@80f16f6e6bea1dfc0f613df222390ee143a4d8e9 # v2 with: token: ${{ steps.app-token.outputs.token }} - workflows: "commit.yml,lint.yml,pr-ci.yml,pre-commit.yml,release-please.yml,promote-release.yml" + workflows: "commit.yml,pr-ci.yml,pre-commit.yml" diff --git a/cmd/workspace/up/configure_test.go b/cmd/workspace/up/configure_test.go index c09bef9d95..4b83948b1b 100644 --- a/cmd/workspace/up/configure_test.go +++ b/cmd/workspace/up/configure_test.go @@ -11,6 +11,8 @@ import ( "github.com/stretchr/testify/assert" ) +const vscodeUser = "vscode" + func TestBuildIDEParams_GPGAgentForwardingParity(t *testing.T) { tests := []struct { name string @@ -52,7 +54,7 @@ func TestBuildIDEParams_GPGAgentForwardingParity(t *testing.T) { devsyConfig := testConfigWithGPGForwardingOption(tt.contextOption) wctx := &workspaceContext{ - user: "vscode", + user: vscodeUser, result: &config2.Result{}, tunnelPort: 10800, } @@ -62,7 +64,7 @@ func TestBuildIDEParams_GPGAgentForwardingParity(t *testing.T) { assert.Equal(t, tt.expected, params.GPGAgentForwarding) assert.Equal(t, "sock-id", params.SSHAuthSockID) assert.Equal(t, "ssh-sign-key", params.GitSSHSigningKey) - assert.Equal(t, "vscode", params.User) + assert.Equal(t, vscodeUser, params.User) assert.Equal(t, wctx.result, params.Result) assert.True(t, params.TunnelMode) assert.Equal(t, opener.LaunchAuto, params.Launch) diff --git a/e2e/tests/ide/browser_returns.go b/e2e/tests/ide/browser_returns.go index 026cd74b7c..5c05ef7096 100644 --- a/e2e/tests/ide/browser_returns.go +++ b/e2e/tests/ide/browser_returns.go @@ -386,10 +386,18 @@ var _ = ginkgo.Describe( err = f.DevsyContextUse(ctx, contextName) framework.ExpectNoError(err) - err = f.ExecCommand(ctx, false, true, "", []string{ - "context", "set", - names.Flag(names.Option), config.ContextOptionGPGAgentForwarding + "=" + config.BoolTrue, - }) + err = f.ExecCommand( + ctx, + false, + true, + "", + []string{ + "context", + "set", + names.Flag(names.Option), + config.ContextOptionGPGAgentForwarding + "=" + config.BoolTrue, + }, + ) framework.ExpectNoError(err) err = f.DevsyProviderAdd(ctx, "docker") @@ -414,8 +422,9 @@ var _ = ginkgo.Describe( "--ide=openvscode", "--ide-launch=headless", "--debug") framework.ExpectNoError(err) combined := stdout + stderr - gomega.Expect(combined).To(gomega.ContainSubstring("Starting vscode in browser mode at"), - "expected browser IDE opener path to execute; got:\n%s", combined) + gomega.Expect(combined). + To(gomega.ContainSubstring("Starting vscode in browser mode at"), + "expected browser IDE opener path to execute; got:\n%s", combined) gomega.Expect(combined).To(gomega.ContainSubstring("forwarding gpg-agent"), "expected browser IDE GPG forward bootstrap to run; got:\n%s", combined)