summaryrefslogtreecommitdiffstats
path: root/cmd/ship/root.go
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2026-01-24 16:55:52 -0800
committerbndw <ben@bdw.to>2026-01-24 16:55:52 -0800
commit8a3cff0dd7eb88cadb73a6df4e14f85450d63317 (patch)
tree461e3b7af3a71f92c8a916f2e4578a63e2d92f13 /cmd/ship/root.go
parent9c222e3fe49c6786c1719b6100564d413d7f8db6 (diff)
Add auto-generated subdomain feature
When a base domain is configured on a host (e.g., apps.example.com), deployments automatically get a subdomain ({name}.apps.example.com). Custom --domain can still be provided to route both domains. - Add BaseDomain field to Host state - Add --base-domain flag to host init - Add 'ship host set-domain' command to update base domain - Update deploy flow to auto-generate subdomains - Fix error display (errors were being silently swallowed) - Remove placeholder email from Caddyfile template
Diffstat (limited to 'cmd/ship/root.go')
-rw-r--r--cmd/ship/root.go63
1 files changed, 48 insertions, 15 deletions
diff --git a/cmd/ship/root.go b/cmd/ship/root.go
index e5d6753..81d33d1 100644
--- a/cmd/ship/root.go
+++ b/cmd/ship/root.go
@@ -43,14 +43,48 @@ func runDeploy(cmd *cobra.Command, args []string) error {
43 return cmd.Help() 43 return cmd.Help()
44 } 44 }
45 45
46 if host == "" || domain == "" { 46 if host == "" {
47 return fmt.Errorf("--host and --domain are required") 47 return fmt.Errorf("--host is required")
48 }
49
50 // Load state to check base domain
51 st, err := state.Load()
52 if err != nil {
53 return fmt.Errorf("error loading state: %w", err)
54 }
55 hostState := st.GetHost(host)
56
57 if domain == "" && hostState.BaseDomain == "" {
58 return fmt.Errorf("--domain required (or configure base domain with 'ship host init --base-domain')")
48 } 59 }
49 60
61 // Infer name early so we can use it for subdomain generation
62 inferredName := name
63 if inferredName == "" {
64 if static {
65 inferredName = domain
66 if inferredName == "" && hostState.BaseDomain != "" {
67 inferredName = filepath.Base(dir)
68 }
69 } else {
70 inferredName = filepath.Base(binary)
71 }
72 }
73
74 // Generate subdomain if base domain configured
75 var domains []string
76 if hostState.BaseDomain != "" {
77 domains = append(domains, inferredName+"."+hostState.BaseDomain)
78 }
79 if domain != "" {
80 domains = append(domains, domain)
81 }
82 combinedDomains := strings.Join(domains, ", ")
83
50 if static { 84 if static {
51 return deployStatic(host, domain, name, dir) 85 return deployStatic(host, combinedDomains, inferredName, dir)
52 } 86 }
53 return deployApp(host, domain, name, binary, port, envVars, envFile, binaryArgs, files) 87 return deployApp(host, combinedDomains, inferredName, binary, port, envVars, envFile, binaryArgs, files)
54} 88}
55 89
56func deployApp(host, domain, name, binaryPath string, portOverride int, envVars []string, envFile, args string, files []string) error { 90func deployApp(host, domain, name, binaryPath string, portOverride int, envVars []string, envFile, args string, files []string) error {
@@ -58,16 +92,12 @@ func deployApp(host, domain, name, binaryPath string, portOverride int, envVars
58 return fmt.Errorf("--binary is required") 92 return fmt.Errorf("--binary is required")
59 } 93 }
60 94
61 if name == "" {
62 name = filepath.Base(binaryPath)
63 }
64
65 if _, err := os.Stat(binaryPath); err != nil { 95 if _, err := os.Stat(binaryPath); err != nil {
66 return fmt.Errorf("binary not found: %s", binaryPath) 96 return fmt.Errorf("binary not found: %s", binaryPath)
67 } 97 }
68 98
69 fmt.Printf("Deploying app: %s\n", name) 99 fmt.Printf("Deploying app: %s\n", name)
70 fmt.Printf(" Domain: %s\n", domain) 100 fmt.Printf(" Domain(s): %s\n", domain)
71 fmt.Printf(" Binary: %s\n", binaryPath) 101 fmt.Printf(" Binary: %s\n", binaryPath)
72 102
73 st, err := state.Load() 103 st, err := state.Load()
@@ -260,21 +290,21 @@ func deployApp(host, domain, name, binaryPath string, portOverride int, envVars
260 } 290 }
261 291
262 fmt.Printf("\n App deployed successfully!\n") 292 fmt.Printf("\n App deployed successfully!\n")
263 fmt.Printf(" https://%s (may take a minute for HTTPS cert)\n", domain) 293 // Show first domain in the URL message
294 primaryDomain := strings.Split(domain, ",")[0]
295 primaryDomain = strings.TrimSpace(primaryDomain)
296 fmt.Printf(" https://%s (may take a minute for HTTPS cert)\n", primaryDomain)
264 return nil 297 return nil
265} 298}
266 299
267func deployStatic(host, domain, name, dir string) error { 300func deployStatic(host, domain, name, dir string) error {
268 if name == "" {
269 name = domain
270 }
271 301
272 if _, err := os.Stat(dir); err != nil { 302 if _, err := os.Stat(dir); err != nil {
273 return fmt.Errorf("directory not found: %s", dir) 303 return fmt.Errorf("directory not found: %s", dir)
274 } 304 }
275 305
276 fmt.Printf("Deploying static site: %s\n", name) 306 fmt.Printf("Deploying static site: %s\n", name)
277 fmt.Printf(" Domain: %s\n", domain) 307 fmt.Printf(" Domain(s): %s\n", domain)
278 fmt.Printf(" Directory: %s\n", dir) 308 fmt.Printf(" Directory: %s\n", dir)
279 309
280 st, err := state.Load() 310 st, err := state.Load()
@@ -348,7 +378,10 @@ func deployStatic(host, domain, name, dir string) error {
348 } 378 }
349 379
350 fmt.Printf("\n Static site deployed successfully!\n") 380 fmt.Printf("\n Static site deployed successfully!\n")
351 fmt.Printf(" https://%s (may take a minute for HTTPS cert)\n", domain) 381 // Show first domain in the URL message
382 primaryDomain := strings.Split(domain, ",")[0]
383 primaryDomain = strings.TrimSpace(primaryDomain)
384 fmt.Printf(" https://%s (may take a minute for HTTPS cert)\n", primaryDomain)
352 return nil 385 return nil
353} 386}
354 387