feat: add Phase 5 deployment commands#8
Conversation
Add deployment commands for environment lifecycle management: - environment:branch - create branch environments - environment:activate - activate inactive environments - environment:deactivate - deactivate active environments - environment:delete - delete environments - redeploy - trigger redeployment (post_deploy hook only) - push - push code to Upsun git remote Features: - --wait flag for polling activity until completion - Fixed API response parsing for _embedded.activities wrapper - Smoke tests for all deployment commands (23 tests passing) - .env file support for test configuration Co-Authored-By: Claude <noreply@anthropic.com>
Code Review: PR #8 - Phase 5 Deployment CommandsSummaryAdds 6 deployment-related commands:
Plus Stats: +1054/-7 lines, 9 files changed Verdict:
|
| File | Purpose |
|---|---|
cmd/deploy.go |
6 commands + waitForActivity |
cmd/deploy_test.go |
Unit tests |
cmd/cli.go |
Register commands |
internal/api/environments.go |
API methods |
internal/api/interfaces.go |
Interface updates |
internal/api/mock.go |
Mock implementations |
scripts/smoke-test.sh |
Extended tests |
README.md |
Documentation |
Recommended Action
This works as-is, but consider addressing architecture points before adding more commands. The repeated patterns will compound as the codebase grows.
Minimal fix: Rename deploy.go → environment_actions.go
Better fix: Extract helpers + split file by concern
- Add 'failure' state handling in WaitForActivity (#4) - Add RequireProjectID() and ResolveEnvironmentID() context helpers (#2) - Move WaitForActivity to Context method (#3) - Remove unused --yes flag from environment:delete (#5) - Split deploy.go by concern: RedeployCmd stays, PushCmd moves to git.go, environment lifecycle commands move to environment.go (#1) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Code Review: PR #8 - Phase 5 Deployment Commands (Updated)SummaryAdds 6 deployment commands with proper architecture:
Stats: +1054 lines, 11 files changed Verdict:
|
| File | Purpose |
|---|---|
cmd/environment.go |
All environment commands (list, info, branch, activate, deactivate, delete) |
cmd/deploy.go |
Just RedeployCmd (44 lines) |
cmd/git.go |
Just PushCmd (68 lines) |
cmd/context.go |
Shared helpers |
This is a clean separation by resource/concern.
Helpers Extracted to Context: ✅
// cmd/context.go
func (c *Context) RequireProjectID() (string, error)
func (c *Context) ResolveEnvironmentID(explicit string) (string, error)
func (c *Context) WaitForActivity(client, projectID, activityID) (*Activity, error)These eliminate the repeated boilerplate. Commands are now concise:
func (c *RedeployCmd) Run(ctx *Context) error {
projectID, err := ctx.RequireProjectID() // One line instead of 5
envID, err := ctx.ResolveEnvironmentID(c.EnvironmentID) // One line instead of 7
...
}WaitForActivity: ✅ Handles Edge Cases
switch activity.State {
case "complete":
if activity.Result == "failure" { // Complete but failed
return activity, errors.NewInternalError("activity failed")
}
return activity, nil
case "cancelled":
return activity, errors.NewValidationError("activity was cancelled")
case "failure": // Direct failure state
return activity, errors.NewInternalError("activity failed")
}Handles: complete+success, complete+failure, cancelled, failure state, timeout, context cancellation.
Issues Found
🔴 Bug: PushCmd.Wait flag defined but never used (cmd/git.go:14,56)
type PushCmd struct {
Target string `help:"Target branch" short:"t"`
Force bool `help:"Force push" short:"f"`
Wait bool `help:"Wait for the activity to complete" short:"w"` // DEFINED
}
func (c *PushCmd) Run(ctx *Context) error {
// ...
if err := execGit(ctx, args...); err != nil { ... }
return ctx.Output(...) // c.Wait NEVER CHECKED
}Fix: Either implement waiting (requires API call to find triggered activity) or remove the flag.
🟡 Minor: execGit doesn't verify git is installed (cmd/git.go:101-106)
func execGit(ctx *Context, args ...string) error {
cmd := exec.CommandContext(ctx, "git", args...) // Will fail cryptically if git not in PATH
...
}Suggestion: Add exec.LookPath("git") check with clear error:
gitPath, err := exec.LookPath("git")
if err != nil {
return errors.NewValidationError("git not found").WithHint("Install git to use push command")
}🟡 Minor: API errors use fmt.Errorf instead of structured errors
In internal/api/environments.go (lines 941, 959, 975, 1001):
if len(resp.Embedded.Activities) == 0 {
return nil, fmt.Errorf("no activity returned") // Generic error
}Should use structured error for consistency:
return nil, &APIError{Message: "no activity returned from API", StatusCode: 0}🟡 Minor: Missing tests for --wait behavior and failure states
Tests cover success paths but not:
--waitflag actually waiting- Activity failure during wait
- Context cancellation during wait
Edge Cases Handled: ✅
| Edge Case | Handled |
|---|---|
| No project specified | ✅ RequireProjectID() returns validation error |
| No environment specified | ✅ ResolveEnvironmentID() returns validation error |
| Activity completes with failure | ✅ Checks activity.Result == "failure" |
| Activity cancelled | ✅ Returns error |
| Activity state "failure" | ✅ Returns error |
| Wait timeout (30 min) | ✅ Returns timeout error |
| Context cancelled during wait | ✅ Returns ctx.Err() |
| Project has no repository URL | ✅ Returns validation error |
| Parent environment doesn't exist | ✅ API returns 404, handled by handleAPIError |
| Delete active environment | ✅ API returns 400, handled by handleAPIError |
Security: ✅
url.PathEscape()on all path parametersexec.CommandContextrespects context cancellation- No shell injection (args passed directly, not through shell)
Checklist
- Proper file organization (environment.go, deploy.go, git.go)
- Helpers extracted to Context
- WaitForActivity handles all activity states
- Security (path escaping, no shell injection)
- Unit tests for happy paths
-
--waitflag implemented in PushCmd -
exec.LookPathcheck for git - Tests for wait/failure edge cases
Recommended Actions
Must fix before merge:
- Either implement
--waitin PushCmd or remove the flag (misleading API)
Nice to have:
2. Add exec.LookPath("git") check
3. Use structured errors in API layer
- Remove unused --wait flag from PushCmd (bug fix) - Add exec.LookPath check for clearer error when git not installed - Use structured APIError instead of fmt.Errorf in API layer Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Tests cover: - WaitForActivity success, failure states, cancellation, API errors - Context cancellation during polling - Polling until completion - RequireProjectID from flag, env, and missing - ResolveEnvironmentID from explicit, flag, env, and missing Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Code Review: PR #8 - Phase 5 Deployment Commands (Re-review)SummaryAdds 6 deployment commands with Stats: +1399/-24 lines, 13 files changed, 4 commits Verdict: ✅ Ready to MergeAll previous review issues addressed. Architecture is sound. Issues Fixed Since Last Review
Architecture Review: ✅ ExcellentFile OrganizationClean separation by resource/concern. Context HelpersCommands are concise thanks to extracted helpers: func (c *RedeployCmd) Run(ctx *Context) error {
projectID, err := ctx.RequireProjectID() // Validates project
envID, err := ctx.ResolveEnvironmentID(...) // Resolves from arg/flag/env
activity, err := ctx.WaitForActivity(...) // Polls until complete
}WaitForActivity - All States Handledswitch activity.State {
case "complete":
if activity.Result == "failure" { return error } // Complete but failed
return activity, nil // Success
case "cancelled": return error
case "failure": return error
}
// + timeout handling
// + context cancellationError Handling: ✅ Comprehensive
Test Coverage: ✅ Good
|
| Edge Case | Handled |
|---|---|
| Activity state "complete" + result "failure" | ✅ Returns error |
| Activity state "failure" | ✅ Returns error |
| Activity state "cancelled" | ✅ Returns error |
| Polling timeout (30 min) | ✅ Returns error with state |
| Context cancelled mid-poll | ✅ Returns ctx.Err() |
| API error during poll | ✅ Propagates error |
| Git not in PATH | ✅ Clear error message |
| Empty repository URL | ✅ Validation error |
| Delete active environment | ✅ API returns 400, handled |
Checklist
- Clean file organization (environment.go, deploy.go, git.go)
- Helpers extracted to Context (DRY)
- WaitForActivity handles all activity states
- Structured errors with hints throughout
- Security (path escaping, exec.LookPath, no shell injection)
- Comprehensive unit tests (722 new lines)
- Smoke tests updated
- Documentation updated
Ship it! 🚀
- Add 'failure' state handling in WaitForActivity (#4) - Add RequireProjectID() and ResolveEnvironmentID() context helpers (#2) - Move WaitForActivity to Context method (#3) - Remove unused --yes flag from environment:delete (#5) - Split deploy.go by concern: RedeployCmd stays, PushCmd moves to git.go, environment lifecycle commands move to environment.go (#1) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
- Remove unused --wait flag from PushCmd (bug fix) - Add exec.LookPath check for clearer error when git not installed - Use structured APIError instead of fmt.Errorf in API layer Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Summary
--waitflag for activity polling until completion_embedded.activitieswrapperNew Commands
environment:branchenvironment:activateenvironment:deactivateenvironment:deleteredeploypushSmoke Test Results
Test plan
redeploy --waitpolls until activity completesenvironment:branch --waitcreates and waits for branch🤖 Generated with Claude Code