From a8ad8e934d15d2bf84f942414a89af1d2691adbc Mon Sep 17 00:00:00 2001 From: bndw Date: Sun, 8 Feb 2026 12:32:59 -0800 Subject: Add git-centric deployment with Docker builds and vanity imports New deployment model where projects start with a git remote on the VPS. Pushing to the remote triggers automatic docker build and deploy via post-receive hooks. The base domain serves Go vanity imports and git HTTPS cloning via Caddy + fcgiwrap. - Add `ship init ` command to create bare repos and .ship/ config - Add `ship deploy ` command for manual rebuilds - Extend `ship host init --base-domain` to set up Docker, git user, fcgiwrap, sudoers, and vanity import infrastructure - Add git-app and git-static types alongside existing app and static - Update remove, status, logs, restart, list, and config-update to handle new types --- cmd/ship/deploy_cmd.go | 138 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 138 insertions(+) create mode 100644 cmd/ship/deploy_cmd.go (limited to 'cmd/ship/deploy_cmd.go') diff --git a/cmd/ship/deploy_cmd.go b/cmd/ship/deploy_cmd.go new file mode 100644 index 0000000..ca5c54d --- /dev/null +++ b/cmd/ship/deploy_cmd.go @@ -0,0 +1,138 @@ +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] + + 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 +} -- cgit v1.2.3