package main import ( "fmt" "github.com/bdw/ship/internal/ssh" "github.com/bdw/ship/internal/state" "github.com/spf13/cobra" ) var deployGitCmd = &cobra.Command{ Use: "deploy ", Short: "Manually rebuild and deploy a git-deployed app", Long: `Trigger a manual rebuild from the latest code in the git repo. This runs the same steps as the post-receive hook: checkout code, install .ship/ configs, docker build (for apps), and restart. Examples: ship deploy myapp`, Args: cobra.ExactArgs(1), RunE: runDeployGit, } func runDeployGit(cmd *cobra.Command, args []string) error { name := args[0] if err := validateName(name); err != nil { return err } st, err := state.Load() if err != nil { return fmt.Errorf("error loading state: %w", err) } host := hostFlag if host == "" { host = st.GetDefaultHost() } if host == "" { return fmt.Errorf("--host is required") } app, err := st.GetApp(host, name) if err != nil { return err } if app.Type != "git-app" && app.Type != "git-static" { return fmt.Errorf("%s is not a git-deployed app (type: %s)", name, app.Type) } fmt.Printf("Deploying %s...\n", name) client, err := ssh.Connect(host) if err != nil { return fmt.Errorf("error connecting to VPS: %w", err) } defer client.Close() if app.Type == "git-app" { if err := deployGitApp(client, name); err != nil { return err } } else { if err := deployGitStatic(client, name); err != nil { return err } } fmt.Println("\nDeploy complete!") return nil } func deployGitApp(client *ssh.Client, name string) error { repo := fmt.Sprintf("/srv/git/%s.git", name) src := fmt.Sprintf("/var/lib/%s/src", name) fmt.Println("-> Checking out code...") if _, err := client.RunSudo(fmt.Sprintf("git --work-tree=%s --git-dir=%s checkout -f main", src, repo)); err != nil { return fmt.Errorf("error checking out code: %w", err) } // Install deployment config from repo serviceSrc := fmt.Sprintf("%s/.ship/service", src) serviceDst := fmt.Sprintf("/etc/systemd/system/%s.service", name) fmt.Println("-> Installing systemd unit...") if _, err := client.RunSudo(fmt.Sprintf("cp %s %s", serviceSrc, serviceDst)); err != nil { fmt.Printf(" Warning: no .ship/service found, skipping\n") } else { if _, err := client.RunSudo("systemctl daemon-reload"); err != nil { return fmt.Errorf("error reloading systemd: %w", err) } } caddySrc := fmt.Sprintf("%s/.ship/Caddyfile", src) caddyDst := fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", name) fmt.Println("-> Installing Caddy config...") if _, err := client.RunSudo(fmt.Sprintf("cp %s %s", caddySrc, caddyDst)); err != nil { fmt.Printf(" Warning: no .ship/Caddyfile found, skipping\n") } else { if _, err := client.RunSudo("systemctl reload caddy"); err != nil { return fmt.Errorf("error reloading Caddy: %w", err) } } fmt.Println("-> Building Docker image...") if err := client.RunSudoStream(fmt.Sprintf("docker build -t %s:latest %s", name, src)); err != nil { return fmt.Errorf("error building Docker image: %w", err) } fmt.Println("-> Restarting service...") if _, err := client.RunSudo(fmt.Sprintf("systemctl restart %s", name)); err != nil { return fmt.Errorf("error restarting service: %w", err) } return nil } func deployGitStatic(client *ssh.Client, name string) error { repo := fmt.Sprintf("/srv/git/%s.git", name) webroot := fmt.Sprintf("/var/www/%s", name) fmt.Println("-> Deploying static site...") if _, err := client.RunSudo(fmt.Sprintf("git --work-tree=%s --git-dir=%s checkout -f main", webroot, repo)); err != nil { return fmt.Errorf("error checking out code: %w", err) } caddySrc := fmt.Sprintf("%s/.ship/Caddyfile", webroot) caddyDst := fmt.Sprintf("/etc/caddy/sites-enabled/%s.caddy", name) fmt.Println("-> Installing Caddy config...") if _, err := client.RunSudo(fmt.Sprintf("cp %s %s", caddySrc, caddyDst)); err != nil { fmt.Printf(" Warning: no .ship/Caddyfile found, skipping\n") } else { if _, err := client.RunSudo("systemctl reload caddy"); err != nil { return fmt.Errorf("error reloading Caddy: %w", err) } } return nil }