Skip to content

refactor: extract cmd/up.go into focused pkg/ packages#719

Merged
skevetter merged 8 commits intomainfrom
plan/refactor-workspace-up
Apr 12, 2026
Merged

refactor: extract cmd/up.go into focused pkg/ packages#719
skevetter merged 8 commits intomainfrom
plan/refactor-workspace-up

Conversation

@skevetter
Copy link
Copy Markdown
Owner

@skevetter skevetter commented Apr 12, 2026

Summary

  • Extract ~950 lines from cmd/up.go (1839 → 898 lines) into 5 focused packages:
    • pkg/dotfiles — dotfiles setup logic (14 tests)
    • pkg/ide/opener — IDE launch dispatch for 20+ IDE types (7 tests)
    • pkg/gpg — GPG agent forwarding (1 test)
    • pkg/tunnel — browser tunnel, backhaul, SSH command creation (4 tests)
    • pkg/workspace — provider version update checking (11 tests)
  • Pure behavioral preservation — no feature changes
  • Import cycle between clientimplementation and tunnel resolved via DaemonStartFunc callback pattern

Summary by CodeRabbit

Release Notes

  • New Features

    • Added GPG agent forwarding support for secure signing operations within containers.
    • Enhanced browser-based IDE support with improved tunneling capabilities.
  • Refactor

    • Reorganized IDE opening logic for improved modularity and maintainability.
    • Extracted dotfiles installation into a dedicated system component.
    • Moved provider update checking into a centralized package.

Move IDE opening logic (VSCode flavors, JetBrains, browser-based IDEs,
Fleet) from cmd/up.go into a dedicated pkg/ide/opener package with an
exported Open() function and Params struct. Helper functions
(performGpgForwarding, startBrowserTunnel, setupBackhaul, createSSHCommand)
are temporarily duplicated — they will move to pkg/gpg/ and pkg/tunnel/
in subsequent tasks.
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Apr 12, 2026

Warning

Rate limit exceeded

@skevetter has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 4 minutes and 50 seconds before requesting another review.

Your organization is not enrolled in usage-based pricing. Contact your admin to enable usage-based pricing to continue reviews beyond the rate limit, or try again in 4 minutes and 50 seconds.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 73ad4660-4683-40a7-89bb-67a80ee15ea5

📥 Commits

Reviewing files that changed from the base of the PR and between 09a82a1 and c1290eb.

📒 Files selected for processing (2)
  • pkg/gpg/forward.go
  • pkg/gpg/forward_test.go
📝 Walkthrough

Walkthrough

This PR refactors cmd/up.go by extracting key functionality into dedicated packages: dotfiles setup moves to pkg/dotfiles, IDE opening logic moves to pkg/ide/opener, GPG forwarding moves to pkg/gpg, browser tunneling moves to pkg/tunnel, and provider update checking moves to pkg/workspace. The main command file is significantly simplified by delegating to these new modules.

Changes

Cohort / File(s) Summary
Dotfiles Setup
pkg/dotfiles/dotfiles.go, pkg/dotfiles/dotfiles_test.go
New dotfiles package with Setup function that resolves dotfiles repository/script from DevPodConfig, constructs SSH commands with environment variables via buildDotCmd, and executes installation. Tests cover env variable extraction and command argument construction.
IDE Opening & Dispatch
pkg/ide/opener/opener.go, pkg/ide/opener/opener_test.go
New opener package extracting IDE dispatch logic from cmd/up.go. Implements Open to route to browser variants (VS Code, Jupyter, RStudio) or desktop handlers (VS Code desktop, JetBrains, Fleet). Adds ParseAddressAndPort for binding address/port selection. Tests verify address/port parsing with empty, explicit, and invalid inputs.
Browser Tunnel Infrastructure
pkg/tunnel/browser.go, pkg/tunnel/browser_test.go
New browser tunnel startup with optional GPG/auth forwarding via StartBrowserTunnel. Includes SSH command construction, backhaul setup, and service execution. Tests validate SSH argument building with debug and extra argument combinations.
GPG Agent Forwarding
pkg/gpg/forward.go, pkg/gpg/forward_test.go
New gpg package for agent forwarding with ForwardAgent spawning background SSH process. Constructs forwarding arguments including GPG flags and remote user configuration. Tests verify correct SSH argument composition.
Provider Update Checking
pkg/workspace/provider_update.go, pkg/workspace/provider_update_test.go
New provider update logic with CheckProviderUpdate and GetProInstance functions. Handles version comparison, semantic version parsing, and conditional update skipping for dev/internal providers. Tests verify skip conditions and version comparison logic.
Command Refactoring
cmd/up.go
Removed 964 lines of local helper implementations (dotfiles, IDE opening, provider checking) and replaced with calls to new package APIs. Net change of +23 lines after delegating to extracted modules.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~50 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 27.91% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'refactor: extract cmd/up.go into focused pkg/ packages' accurately and concisely summarizes the primary change: extracting ~950 lines from cmd/up.go into five new focused packages.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch plan/refactor-workspace-up

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@skevetter skevetter marked this pull request as draft April 12, 2026 04:44
@skevetter skevetter force-pushed the plan/refactor-workspace-up branch from 0cb3678 to 1026036 Compare April 12, 2026 04:45
- Add 7 table-driven tests for providerVersionNeedsUpdate covering
  same version, upgrades, downgrades, mixed prefixes, and invalid input
- Add buildSSHCommandArgs test case for debug + extra args combined
- Rename dotCmd to backhaulCmd in SetupBackhaul for clarity
@skevetter skevetter force-pushed the plan/refactor-workspace-up branch from 1026036 to 09a82a1 Compare April 12, 2026 04:57
@skevetter skevetter marked this pull request as ready for review April 12, 2026 05:22
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
pkg/workspace/provider_update.go (1)

116-122: Consider logging the error before returning nil.

GetProInstance silently swallows the error from ListProInstances. While returning nil is acceptable for "not found" scenarios, an actual listing error (e.g., filesystem issue) may warrant at least a debug log so users can diagnose unexpected behavior.

Proposed improvement
 func GetProInstance(
 	devPodConfig *config.Config,
 	providerName string,
 	log log.Logger,
 ) *provider2.ProInstance {
 	proInstances, err := ListProInstances(devPodConfig, log)
 	if err != nil {
+		log.Debugf("failed to list pro instances: %v", err)
 		return nil
 	} else if len(proInstances) == 0 {
 		return nil
 	}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/workspace/provider_update.go` around lines 116 - 122, GetProInstance
currently calls ListProInstances(devPodConfig, log) and returns nil on any error
without logging; update GetProInstance to log the returned err (using the
provided log instance) before returning nil so listing failures are observable;
reference the call to ListProInstances and the variables proInstances and err
inside GetProInstance when adding a log statement (e.g., debug/error level) to
record context about devPodConfig and the error.
pkg/gpg/forward_test.go (1)

9-21: Consider using assert.Equal for stricter argument ordering verification.

Using assert.Contains verifies presence but not the correct ordering of flag-value pairs (e.g., that "root" follows "--user"). Since CLI argument order typically matters, consider testing the exact slice to catch ordering regressions.

Stricter assertion example
 func TestBuildForwardArgs(t *testing.T) {
 	args := buildForwardArgs("root", "test-context", "test-workspace")
-	assert.Contains(t, args, "ssh")
-	assert.Contains(t, args, "--gpg-agent-forwarding=true")
-	assert.Contains(t, args, "--agent-forwarding=true")
-	assert.Contains(t, args, "--user")
-	assert.Contains(t, args, "root")
-	assert.Contains(t, args, "--context")
-	assert.Contains(t, args, "test-context")
-	assert.Contains(t, args, "test-workspace")
-	assert.Contains(t, args, "--command")
-	assert.Contains(t, args, "sleep infinity")
+	expected := []string{
+		"ssh",
+		"--gpg-agent-forwarding=true",
+		"--agent-forwarding=true",
+		"--start-services=true",
+		"--user",
+		"root",
+		"--context",
+		"test-context",
+		"test-workspace",
+		"--log-output=raw",
+		"--command", "sleep infinity",
+	}
+	assert.Equal(t, expected, args)
 }
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/gpg/forward_test.go` around lines 9 - 21, Update TestBuildForwardArgs to
assert the exact argument ordering by replacing the multiple assert.Contains
checks with a single assert.Equal that compares the returned slice from
buildForwardArgs("root", "test-context", "test-workspace") to the expected exact
[]string (including "ssh", "--gpg-agent-forwarding=true",
"--agent-forwarding=true", "--user", "root", "--context", "test-context",
"test-workspace", "--command", "sleep infinity") so the test fails on any
ordering or missing/extra argument regressions.
pkg/gpg/forward.go (1)

34-40: Improve error reporting and avoid variable shadowing.

The goroutine captures and reassigns the outer err variable, which is confusing (though safe since ForwardAgent returns before the goroutine completes). Additionally, the error message doesn't include the actual error value, making debugging difficult.

Proposed improvement
 	go func() {
-		//nolint:gosec // execPath comes from os.Executable()
-		err = exec.Command(execPath, args...).Run()
-		if err != nil {
-			logger.Error("failure in forwarding gpg-agent")
+		//nolint:gosec // execPath comes from os.Executable()
+		if runErr := exec.Command(execPath, args...).Run(); runErr != nil {
+			logger.Errorf("failure in forwarding gpg-agent: %v", runErr)
 		}
 	}()
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/gpg/forward.go` around lines 34 - 40, The goroutine in ForwardAgent
currently reassigns the outer err and logs a generic message; change it to
capture the exec.Command(...).Run() result into a local variable (e.g., localErr
:= exec.Command(execPath, args...).Run()) to avoid shadowing, and include the
actual error when logging (use logger.Error with the error value or
logger.WithError(localErr).Error) so the log shows the underlying failure from
exec.Command(execPath, args...).Run() instead of just "failure in forwarding
gpg-agent".
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@pkg/ide/opener/opener.go`:
- Around line 109-140: ParseAddressAndPort currently returns a single "address"
string (sometimes the full host:port) which hides the configured bind host;
change it so ParseAddressAndPort (and helpers parseDefaultPort and
parseExplicitAddress) return the host separately (host string, port int, error)
where parseExplicitAddress uses net.SplitHostPort to extract host and port,
preserves any non-wildcard host verbatim (including IPv6) and only normalizes
wildcard binds ("0.0.0.0", "::", empty or "*") to "localhost"; parseDefaultPort
should return the chosen port and a sensible default host (e.g. "localhost" or
the configured default) rather than a combined address string; update callers of
ParseAddressAndPort to use the returned host and port when constructing/logging
the browser URL.
- Around line 219-247: The StartServicesDaemon call from makeDaemonStartFunc is
missing propagation of the Git SSH signing key, causing daemon-based browser IDE
sessions to drop the --git-ssh-signing-key flag; add a GitSSHSigningKey field to
the StartServicesDaemonOptions struct and set it when calling
StartServicesDaemon in makeDaemonStartFunc (pass params.GitSSHSigningKey),
mirroring how RunServicesOptions is populated in the SSH path (see cmd/ssh.go)
so the daemon path receives the same GitSSHSigningKey value.

In `@pkg/tunnel/browser.go`:
- Around line 37-43: StartBrowserTunnel spawns a fire-and-forget backhaul but
doesn't pass the caller context to SetupBackhaul, so the child SSH process can
outlive the browser session; update SetupBackhaul to accept a context.Context
(or add a SetupBackhaulContext wrapper) and use exec.CommandContext inside it,
then call it from StartBrowserTunnel with p.Ctx (or context.Background fallback)
inside the goroutine; update BrowserTunnelParams usage and any callers of
SetupBackhaul accordingly so the backhaul process is canceled/teardown when
p.Ctx is done.

In `@pkg/workspace/provider_update.go`:
- Around line 94-98: The check using strings.Split(providerSource, "@") is
unreachable because Split always returns >=1 item; change the validation to
verify an '@' exists by checking len(splitted) < 2 (or providerSource contains
"@") before proceeding. Update the error path to return a clear message (e.g.,
"no provider version delimiter '@' in providerSource") and keep the existing
assignment providerSource = splitted[0] + "@" + newVersion after the validation;
reference symbols: strings.Split, splitted, providerSource, newVersion.

---

Nitpick comments:
In `@pkg/gpg/forward_test.go`:
- Around line 9-21: Update TestBuildForwardArgs to assert the exact argument
ordering by replacing the multiple assert.Contains checks with a single
assert.Equal that compares the returned slice from buildForwardArgs("root",
"test-context", "test-workspace") to the expected exact []string (including
"ssh", "--gpg-agent-forwarding=true", "--agent-forwarding=true", "--user",
"root", "--context", "test-context", "test-workspace", "--command", "sleep
infinity") so the test fails on any ordering or missing/extra argument
regressions.

In `@pkg/gpg/forward.go`:
- Around line 34-40: The goroutine in ForwardAgent currently reassigns the outer
err and logs a generic message; change it to capture the exec.Command(...).Run()
result into a local variable (e.g., localErr := exec.Command(execPath,
args...).Run()) to avoid shadowing, and include the actual error when logging
(use logger.Error with the error value or logger.WithError(localErr).Error) so
the log shows the underlying failure from exec.Command(execPath, args...).Run()
instead of just "failure in forwarding gpg-agent".

In `@pkg/workspace/provider_update.go`:
- Around line 116-122: GetProInstance currently calls
ListProInstances(devPodConfig, log) and returns nil on any error without
logging; update GetProInstance to log the returned err (using the provided log
instance) before returning nil so listing failures are observable; reference the
call to ListProInstances and the variables proInstances and err inside
GetProInstance when adding a log statement (e.g., debug/error level) to record
context about devPodConfig and the error.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f2642ec9-81e1-4f3d-80e5-524138ddd891

📥 Commits

Reviewing files that changed from the base of the PR and between 696b944 and 09a82a1.

📒 Files selected for processing (11)
  • cmd/up.go
  • pkg/dotfiles/dotfiles.go
  • pkg/dotfiles/dotfiles_test.go
  • pkg/gpg/forward.go
  • pkg/gpg/forward_test.go
  • pkg/ide/opener/opener.go
  • pkg/ide/opener/opener_test.go
  • pkg/tunnel/browser.go
  • pkg/tunnel/browser_test.go
  • pkg/workspace/provider_update.go
  • pkg/workspace/provider_update_test.go

Comment thread pkg/ide/opener/opener.go
Comment on lines +109 to +140
func ParseAddressAndPort(bindAddressOption string, defaultPort int) (string, int, error) {
if bindAddressOption == "" {
return parseDefaultPort(defaultPort)
}

return parseExplicitAddress(bindAddressOption)
}

func parseDefaultPort(defaultPort int) (string, int, error) {
portName, err := port.FindAvailablePort(defaultPort)
if err != nil {
return "", 0, err
}

return fmt.Sprintf("%d", portName), portName, nil
}

func parseExplicitAddress(address string) (string, int, error) {
_, p, err := net.SplitHostPort(address)
if err != nil {
return "", 0, fmt.Errorf("parse host:port: %w", err)
}
if p == "" {
return "", 0, fmt.Errorf("parse ADDRESS: expected host:port, got %s", address)
}

portName, err := strconv.Atoi(p)
if err != nil {
return "", 0, fmt.Errorf("parse host:port: %w", err)
}

return address, portName, nil
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Respect the configured bind host in the browser URL.

ParseAddressAndPort accepts an explicit host:port, but the Jupyter, RStudio, and OpenVSCode flows always open/log localhost. A non-loopback or IPv6-only bind address will forward successfully yet still send the user to the wrong URL. Return the host separately and only normalize wildcard binds (0.0.0.0 / ::) to localhost.

Also applies to: 276-276, 326-326, 375-375

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/ide/opener/opener.go` around lines 109 - 140, ParseAddressAndPort
currently returns a single "address" string (sometimes the full host:port) which
hides the configured bind host; change it so ParseAddressAndPort (and helpers
parseDefaultPort and parseExplicitAddress) return the host separately (host
string, port int, error) where parseExplicitAddress uses net.SplitHostPort to
extract host and port, preserves any non-wildcard host verbatim (including IPv6)
and only normalizes wildcard binds ("0.0.0.0", "::", empty or "*") to
"localhost"; parseDefaultPort should return the chosen port and a sensible
default host (e.g. "localhost" or the configured default) rather than a combined
address string; update callers of ParseAddressAndPort to use the returned host
and port when constructing/logging the browser URL.

Comment thread pkg/ide/opener/opener.go
Comment on lines +219 to +247
func makeDaemonStartFunc(
params Params,
forwardPorts bool,
extraPorts []string,
) func(ctx context.Context) error {
daemonClient, ok := params.Client.(client2.DaemonClient)
if !ok {
return nil
}

return func(ctx context.Context) error {
toolClient, _, err := daemonClient.SSHClients(ctx, params.User)
if err != nil {
return err
}
defer func() { _ = toolClient.Close() }()

err = clientimplementation.StartServicesDaemon(
ctx,
clientimplementation.StartServicesDaemonOptions{
DevPodConfig: params.DevPodConfig,
Client: daemonClient,
SSHClient: toolClient,
User: params.User,
Log: params.Log,
ForwardPorts: forwardPorts,
ExtraPorts: extraPorts,
},
)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "=== StartServicesDaemonOptions / StartServicesDaemon ==="
rg -n -C4 '\btype StartServicesDaemonOptions struct\b|\bfunc StartServicesDaemon\b|\bGitSSHSigningKey\b|\bConfigureGitSSHSignatureHelper\b' --type=go

echo
echo "=== Daemon browser path ==="
rg -n -C4 '\bmakeDaemonStartFunc\b|\bStartBrowserTunnel\b|\bGitSSHSigningKey\b' --type=go

Repository: skevetter/devpod

Length of output: 23992


Pass GitSSHSigningKey through StartServicesDaemonOptions to fix daemon browser path.

The daemon path in makeDaemonStartFunc calls StartServicesDaemon but StartServicesDaemonOptions lacks a GitSSHSigningKey field. This means daemon-based browser IDE sessions lose --git-ssh-signing-key support, while the SSH tunnel path correctly forwards it via RunServicesOptions. The SSH command path (cmd/ssh.go) explicitly passes GitSSHSigningKey to show this is required. Either add GitSSHSigningKey to StartServicesDaemonOptions and propagate it from makeDaemonStartFunc (via params.GitSSHSigningKey), or extract it from the workspace config inside StartServicesDaemon.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/ide/opener/opener.go` around lines 219 - 247, The StartServicesDaemon
call from makeDaemonStartFunc is missing propagation of the Git SSH signing key,
causing daemon-based browser IDE sessions to drop the --git-ssh-signing-key
flag; add a GitSSHSigningKey field to the StartServicesDaemonOptions struct and
set it when calling StartServicesDaemon in makeDaemonStartFunc (pass
params.GitSSHSigningKey), mirroring how RunServicesOptions is populated in the
SSH path (see cmd/ssh.go) so the daemon path receives the same GitSSHSigningKey
value.

Comment thread pkg/tunnel/browser.go
Comment on lines +37 to +43
func StartBrowserTunnel(p BrowserTunnelParams) error {
if p.AuthSockID != "" {
go func() {
if err := SetupBackhaul(p.Client, p.AuthSockID, p.Logger); err != nil {
p.Logger.Error("Failed to setup backhaul SSH connection: ", err)
}
}()
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Bind the backhaul process to the caller context.

StartBrowserTunnel fire-and-forgets a long-lived devpod ssh backhaul, but SetupBackhaul uses exec.Command and never sees p.Ctx. When the browser session ends, that child can keep the SSH session and forwarded agent alive until it exits on its own. Please thread the context through so teardown matches the tunnel lifetime.

Suggested fix
 func StartBrowserTunnel(p BrowserTunnelParams) error {
 	if p.AuthSockID != "" {
 		go func() {
-			if err := SetupBackhaul(p.Client, p.AuthSockID, p.Logger); err != nil {
+			if err := SetupBackhaul(p.Ctx, p.Client, p.AuthSockID, p.Logger); err != nil {
 				p.Logger.Error("Failed to setup backhaul SSH connection: ", err)
 			}
 		}()
 	}
@@
-func SetupBackhaul(client client2.BaseWorkspaceClient, authSockID string, logger log.Logger) error {
+func SetupBackhaul(ctx context.Context, client client2.BaseWorkspaceClient, authSockID string, logger log.Logger) error {
 	execPath, err := os.Executable()
 	if err != nil {
 		return err
 	}
@@
-	backhaulCmd := exec.Command(
+	backhaulCmd := exec.CommandContext(
+		ctx,
 		execPath,
 		"ssh",
@@
 	logger.Info("Setting up backhaul SSH connection")
 
 	writer := logger.Writer(logrus.InfoLevel, false)
+	defer func() { _ = writer.Close() }()
 
 	backhaulCmd.Stdout = writer
 	backhaulCmd.Stderr = writer

Also applies to: 123-166

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/tunnel/browser.go` around lines 37 - 43, StartBrowserTunnel spawns a
fire-and-forget backhaul but doesn't pass the caller context to SetupBackhaul,
so the child SSH process can outlive the browser session; update SetupBackhaul
to accept a context.Context (or add a SetupBackhaulContext wrapper) and use
exec.CommandContext inside it, then call it from StartBrowserTunnel with p.Ctx
(or context.Background fallback) inside the goroutine; update
BrowserTunnelParams usage and any callers of SetupBackhaul accordingly so the
backhaul process is canceled/teardown when p.Ctx is done.

Comment on lines +94 to +98
splitted := strings.Split(providerSource, "@")
if len(splitted) == 0 {
return fmt.Errorf("no provider source found %s", providerSource)
}
providerSource = splitted[0] + "@" + newVersion
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Unreachable condition: strings.Split always returns at least one element.

strings.Split(providerSource, "@") will always return a slice with at least one element (the original string if no @ is found). The check len(splitted) == 0 is never true. Consider checking len(splitted) < 2 if you want to validate that an @ delimiter exists.

Proposed fix
 	splitted := strings.Split(providerSource, "@")
-	if len(splitted) == 0 {
-		return fmt.Errorf("no provider source found %s", providerSource)
+	if len(splitted) < 2 {
+		return fmt.Errorf("provider source %s missing version suffix (`@version`)", providerSource)
 	}
 	providerSource = splitted[0] + "@" + newVersion
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
splitted := strings.Split(providerSource, "@")
if len(splitted) == 0 {
return fmt.Errorf("no provider source found %s", providerSource)
}
providerSource = splitted[0] + "@" + newVersion
splitted := strings.Split(providerSource, "@")
if len(splitted) < 2 {
return fmt.Errorf("provider source %s missing version suffix (`@version`)", providerSource)
}
providerSource = splitted[0] + "@" + newVersion
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@pkg/workspace/provider_update.go` around lines 94 - 98, The check using
strings.Split(providerSource, "@") is unreachable because Split always returns
>=1 item; change the validation to verify an '@' exists by checking
len(splitted) < 2 (or providerSource contains "@") before proceeding. Update the
error path to return a clear message (e.g., "no provider version delimiter '@'
in providerSource") and keep the existing assignment providerSource =
splitted[0] + "@" + newVersion after the validation; reference symbols:
strings.Split, splitted, providerSource, newVersion.

- Fix error variable shadowing in ForwardAgent goroutine
- Include actual error in gpg-agent forwarding failure log
- Use assert.Equal for strict argument ordering in GPG test
@skevetter skevetter merged commit b5d20ba into main Apr 12, 2026
41 checks passed
@skevetter skevetter deleted the plan/refactor-workspace-up branch April 12, 2026 06:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant