Skip to content

feat(cmd): add run-user-commands lifecycle command - #203

Merged
skevetter merged 2 commits into
mainfrom
devsy-027-run-user-commands
May 4, 2026
Merged

feat(cmd): add run-user-commands lifecycle command#203
skevetter merged 2 commits into
mainfrom
devsy-027-run-user-commands

Conversation

@skevetter

@skevetterskevetter commented May 4, 2026

Copy link
Copy Markdown
Contributor

Summary

Adds a new run-user-commands CLI command that connects to an already-running devcontainer and executes lifecycle commands (postCreateCommand, postStartCommand, postAttachCommand) in order. This unblocks CI/CD pipelines that need to trigger lifecycle hooks on existing containers without a full up cycle.

Summary by CodeRabbit

  • New Features

    • Added run-user-commands CLI command to execute Dev Container lifecycle commands (post-create, post-start, post-attach) within running workspace containers, with flexible container targeting through label-based identification.
  • Tests

    • Added comprehensive test coverage for the new command, validating proper registration, required flags, command options, and environment variable construction.

Add a new CLI command that connects to an already-running devcontainer
and executes lifecycle commands (postCreateCommand, postStartCommand,
postAttachCommand) in order. This enables CI/CD pipelines to trigger
lifecycle hooks on existing containers without a full `up` cycle.
Includes a hidden `runUserCommands` camelCase alias for devcontainer
CLI compatibility and outputs the JSON result envelope on stdout.
@netlify

netlifyBot commented May 4, 2026

Copy link
Copy Markdown

Deploy Preview for devsydev canceled.

NameLink
🔨 Latest commitd11536a
🔍 Latest deploy loghttps://app.netlify.com/projects/devsydev/deploys/69f8754ee74daf0008fa023c

@coderabbitai

coderabbitaiBot commented May 4, 2026

Copy link
Copy Markdown
📝 Walkthrough

Walkthrough

A new run-user-commands CLI command is added to execute Dev Container lifecycle hooks (postCreateCommand, postStartCommand, postAttachCommand) inside a running workspace container. The command validates ID labels, resolves the target container, loads execution parameters, runs each lifecycle hook in sequence via Docker exec, and returns a result JSON to stderr.

Changes

New run-user-commands CLI Feature

Layer / File(s)Summary
Command Definition & Wiring
cmd/root.go, cmd/runusercommands.go (lines 1–66)
NewRunUserCommandsCmd and NewRunUserCommandsCmdAlias constructors define the run-user-commands command and hidden runUserCommands alias with --workspace-folder (required) and --id-label (repeated) flags. Both commands are registered in BuildRoot.
Container Resolution & Hook Execution
cmd/runusercommands.go (lines 76–187)
Run method validates ID labels, calls resolveContainer to load dev config and locate the target container, then runLifecycleHooks to execute postCreateCommand, postStartCommand, and postAttachCommand in sequence via execLifecycleHook; writes result JSON to stderr.
Helper Implementations
cmd/runusercommands.go (lines 97–143, 145–187, 189–212)
resolveContainer loads merged workspace config and builds lifecycle exec parameters; execLifecycleHook skips empty hooks and runs non-empty commands via docker exec with stdio passthrough; buildLifecycleEnvArgs derives sorted -e KEY=VALUE docker environment arguments from RemoteEnv, skipping nil values.
Tests & Validation
cmd/runusercommands_test.go
11 tests verify command naming, alias visibility, root registration, required flags, flag types, and buildLifecycleEnvArgs behavior across nil inputs, empty configs, and populated environment maps.

Sequence Diagram

sequenceDiagram
actor User
participant CLI as run-user-commands
participant Config as Dev Config Loader
participant Container as Docker Container
User->>CLI: Execute with workspace-folder & id-labels
CLI->>Config: Load dev config & resolve workspace
Config-->>CLI: Workspace & container ID
CLI->>Container: Run postCreateCommand (docker exec)
Container-->>CLI: ✓ or error
CLI->>Container: Run postStartCommand (docker exec)
Container-->>CLI: ✓ or error
CLI->>Container: Run postAttachCommand (docker exec)
Container-->>CLI: ✓ or error
CLI-->>User: Write result JSON to stderr
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~22 minutes

Possibly related PRs

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 18.75% which is insufficient. The required threshold is 80.00%.Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check nameStatusExplanation
Description Check✅ PassedCheck skipped - CodeRabbit’s high-level summary is enabled.
Title check✅ PassedThe title clearly and specifically summarizes the main change: adding a new run-user-commands CLI command that executes Dev Container lifecycle commands, which aligns with all the file changes (runusercommands.go, its tests, and root.go registration).
Linked Issues check✅ PassedCheck skipped because no linked issues were found for this pull request.
Out of Scope Changes check✅ PassedCheck skipped because no linked issues were found for this pull request.

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


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
Review rate limit: 0/1 reviews remaining, refill in 60 minutes.

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

- Write JSON envelope to stderr (not stdout) so subprocess output
doesn't corrupt the parseable envelope
- Register camelCase alias as a separate hidden command instead of
using Cobra's visible Aliases field
- Remove unnecessary devcontainerGetRunnerID wrapper function
- Sort env map keys for deterministic arg ordering
@skevetter
skevetter marked this pull request as ready for review May 4, 2026 10:35
@skevetter
skevetter enabled auto-merge (squash) May 4, 2026 10:35

@coderabbitaicoderabbitaiBot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@cmd/runusercommands.go`:
- Around line 181-184: The hook's stderr is currently forwarded to os.Stderr
inside execLifecycleHook which mixes diagnostics with the JSON envelope written
later by params.helper.Run; change execLifecycleHook (and where it's invoked) to
capture hook stderr into a separate io.Writer (e.g., a bytes.Buffer or a
dedicated params.hookStderr) instead of passing os.Stderr, pass that buffer into
params.helper.Run invocations (the call site using params.helper.Run(params.ctx,
args, os.Stdin, os.Stdout, os.Stderr)), and ensure the JSON envelope is written
only to os.Stdout; emit any captured hook stderr to os.Stderr (or into a
separate diagnostics channel/field) outside or before/after the envelope so it
cannot corrupt the JSON output.
- Around line 91-94: The call to devcconfig.WriteResultJSON in the end of the
command currently discards its error; change the code in the function containing
user := devcconfig.GetRemoteUser(result) so that the returned error from
devcconfig.WriteResultJSON(os.Stderr, params.containerID, user, params.workdir)
is checked and propagated (or logged and returned) instead of being
ignored—i.e., capture the error, log a clear message with params.containerID
using log.Infof/log.Errorf as appropriate, and return the error (or wrap it) so
callers receive a failure when writing the result envelope fails.
🪄 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: bf44724d-13f4-47a6-8020-1897798b8840

📥 Commits

Reviewing files that changed from the base of the PR and between 7ddcfca and d11536a.

📒 Files selected for processing (3)
  • cmd/root.go
  • cmd/runusercommands.go
  • cmd/runusercommands_test.go

Comment on lines +91 to +94
user := devcconfig.GetRemoteUser(result)
log.Infof("lifecycle commands completed for container %s", params.containerID)
_ = devcconfig.WriteResultJSON(os.Stderr, params.containerID, user, params.workdir)
return 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 | ⚡ Quick win

Handle WriteResultJSON failures instead of discarding them.

If writing the result envelope fails, the command still returns success, which leaves callers with no machine-readable result and masks the real failure.

💡 Suggested fix
-	user := devcconfig.GetRemoteUser(result)-	log.Infof("lifecycle commands completed for container %s", params.containerID)-	_ = devcconfig.WriteResultJSON(os.Stderr, params.containerID, user, params.workdir)-	return nil+	user := devcconfig.GetRemoteUser(result)+	log.Infof("lifecycle commands completed for container %s", params.containerID)+	if err := devcconfig.WriteResultJSON(os.Stderr, params.containerID, user, params.workdir); err != nil {+ return err+	}+	return nil
📝 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
user:=devcconfig.GetRemoteUser(result)
log.Infof("lifecycle commands completed for container %s", params.containerID)
_=devcconfig.WriteResultJSON(os.Stderr, params.containerID, user, params.workdir)
returnnil
user:=devcconfig.GetRemoteUser(result)
log.Infof("lifecycle commands completed for container %s", params.containerID)
iferr:=devcconfig.WriteResultJSON(os.Stderr, params.containerID, user, params.workdir); err!=nil {
returnerr
}
returnnil
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@cmd/runusercommands.go` around lines 91 - 94, The call to
devcconfig.WriteResultJSON in the end of the command currently discards its
error; change the code in the function containing user :=
devcconfig.GetRemoteUser(result) so that the returned error from
devcconfig.WriteResultJSON(os.Stderr, params.containerID, user, params.workdir)
is checked and propagated (or logged and returned) instead of being
ignored—i.e., capture the error, log a clear message with params.containerID
using log.Infof/log.Errorf as appropriate, and return the error (or wrap it) so
callers receive a failure when writing the result envelope fails.

Comment on lines +181 to +184
if err := params.helper.Run(params.ctx, args, os.Stdin, os.Stdout, os.Stderr); err != nil {
return fmt.Errorf("command %q failed: %w", key, 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 | 🏗️ Heavy lift

Keep hook stderr separate from the JSON envelope.

execLifecycleHook() forwards each lifecycle command's stderr to os.Stderr, and Run() later writes the result envelope to the same stream. Any hook that emits diagnostics will corrupt the envelope and break parsers downstream.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@cmd/runusercommands.go` around lines 181 - 184, The hook's stderr is
currently forwarded to os.Stderr inside execLifecycleHook which mixes
diagnostics with the JSON envelope written later by params.helper.Run; change
execLifecycleHook (and where it's invoked) to capture hook stderr into a
separate io.Writer (e.g., a bytes.Buffer or a dedicated params.hookStderr)
instead of passing os.Stderr, pass that buffer into params.helper.Run
invocations (the call site using params.helper.Run(params.ctx, args, os.Stdin,
os.Stdout, os.Stderr)), and ensure the JSON envelope is written only to
os.Stdout; emit any captured hook stderr to os.Stderr (or into a separate
diagnostics channel/field) outside or before/after the envelope so it cannot
corrupt the JSON output.

Sign up for freeto 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

@skevetter