summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorbndw <ben@bdw.to>2026-02-10 21:29:20 -0800
committerbndw <ben@bdw.to>2026-02-10 21:29:20 -0800
commitc49a067ac84ac5c1691ecf4db6a9bf791246899f (patch)
treeefbb94c0d5a79411e91089922151f1ad424e6645
parent47d4b3b6e4d68660e6e1e05fe2e1c0839f86e40e (diff)
Remove --module flag, add --public, make hooks smarter
Drop git-module type — the post-receive hook now checks for a Dockerfile before building, so repos without one simply skip deploy on push. This covers Go modules and libraries that only need vanity imports. Add --public flag to ship init for per-repo HTTPS clone visibility. Show visibility column in ship list.
-rw-r--r--cmd/ship/init.go113
-rw-r--r--cmd/ship/list.go11
-rw-r--r--internal/templates/templates.go27
3 files changed, 96 insertions, 55 deletions
diff --git a/cmd/ship/init.go b/cmd/ship/init.go
index 167347d..b495702 100644
--- a/cmd/ship/init.go
+++ b/cmd/ship/init.go
@@ -3,6 +3,7 @@ package main
3import ( 3import (
4 "fmt" 4 "fmt"
5 "os" 5 "os"
6 "os/exec"
6 "path/filepath" 7 "path/filepath"
7 "strconv" 8 "strconv"
8 9
@@ -18,7 +19,8 @@ var initCmd = &cobra.Command{
18 Long: `Create a bare git repo on the VPS and generate local .ship/ config files. 19 Long: `Create a bare git repo on the VPS and generate local .ship/ config files.
19 20
20Pushing to the remote triggers an automatic docker build and deploy (for apps) 21Pushing to the remote triggers an automatic docker build and deploy (for apps)
21or a static file checkout (for static sites). 22or a static file checkout (for static sites). If no Dockerfile is present in an
23app repo, pushes are accepted without triggering a deploy.
22 24
23Examples: 25Examples:
24 # Initialize an app (Docker-based) 26 # Initialize an app (Docker-based)
@@ -28,19 +30,27 @@ Examples:
28 ship init myapp --domain custom.example.com 30 ship init myapp --domain custom.example.com
29 31
30 # Initialize a static site 32 # Initialize a static site
31 ship init mysite --static`, 33 ship init mysite --static
34
35 # Initialize a public repo (cloneable via go get / git clone over HTTPS)
36 ship init mylib --public`,
32 Args: cobra.ExactArgs(1), 37 Args: cobra.ExactArgs(1),
33 RunE: runInit, 38 RunE: runInit,
34} 39}
35 40
36func init() { 41func init() {
37 initCmd.Flags().Bool("static", false, "Initialize as static site") 42 initCmd.Flags().Bool("static", false, "Initialize as static site")
43 initCmd.Flags().Bool("public", false, "Make repo publicly cloneable over HTTPS (for go get)")
38 initCmd.Flags().String("domain", "", "Custom domain (default: name.basedomain)") 44 initCmd.Flags().String("domain", "", "Custom domain (default: name.basedomain)")
39} 45}
40 46
41func runInit(cmd *cobra.Command, args []string) error { 47func runInit(cmd *cobra.Command, args []string) error {
42 name := args[0] 48 name := args[0]
49 if err := validateName(name); err != nil {
50 return err
51 }
43 static, _ := cmd.Flags().GetBool("static") 52 static, _ := cmd.Flags().GetBool("static")
53 public, _ := cmd.Flags().GetBool("public")
44 domain, _ := cmd.Flags().GetString("domain") 54 domain, _ := cmd.Flags().GetString("domain")
45 55
46 st, err := state.Load() 56 st, err := state.Load()
@@ -66,6 +76,11 @@ func runInit(cmd *cobra.Command, args []string) error {
66 return fmt.Errorf("app %s already exists", name) 76 return fmt.Errorf("app %s already exists", name)
67 } 77 }
68 78
79 appType := "git-app"
80 if static {
81 appType = "git-static"
82 }
83
69 // Resolve domain 84 // Resolve domain
70 if domain == "" && hostState.BaseDomain != "" { 85 if domain == "" && hostState.BaseDomain != "" {
71 domain = name + "." + hostState.BaseDomain 86 domain = name + "." + hostState.BaseDomain
@@ -74,12 +89,7 @@ func runInit(cmd *cobra.Command, args []string) error {
74 return fmt.Errorf("--domain required (or configure base domain)") 89 return fmt.Errorf("--domain required (or configure base domain)")
75 } 90 }
76 91
77 appType := "git-app" 92 // Allocate port for apps only
78 if static {
79 appType = "git-static"
80 }
81
82 // Allocate port for apps
83 port := 0 93 port := 0
84 if !static { 94 if !static {
85 port = st.AllocatePort(host) 95 port = st.AllocatePort(host)
@@ -96,10 +106,16 @@ func runInit(cmd *cobra.Command, args []string) error {
96 // Create bare repo 106 // Create bare repo
97 fmt.Println("-> Creating bare git repo...") 107 fmt.Println("-> Creating bare git repo...")
98 repo := fmt.Sprintf("/srv/git/%s.git", name) 108 repo := fmt.Sprintf("/srv/git/%s.git", name)
99 if _, err := client.RunSudo(fmt.Sprintf("sudo -u git git init --bare %s", repo)); err != nil { 109 if _, err := client.RunSudo(fmt.Sprintf("sudo -u git git init --bare -b main %s", repo)); err != nil {
100 return fmt.Errorf("error creating bare repo: %w", err) 110 return fmt.Errorf("error creating bare repo: %w", err)
101 } 111 }
102 112
113 if public {
114 if _, err := client.RunSudo(fmt.Sprintf("sudo -u git touch %s/git-daemon-export-ok", repo)); err != nil {
115 return fmt.Errorf("error setting repo public: %w", err)
116 }
117 }
118
103 if static { 119 if static {
104 // Create web root 120 // Create web root
105 fmt.Println("-> Creating web root...") 121 fmt.Println("-> Creating web root...")
@@ -118,32 +134,10 @@ func runInit(cmd *cobra.Command, args []string) error {
118 if err != nil { 134 if err != nil {
119 return fmt.Errorf("error generating hook: %w", err) 135 return fmt.Errorf("error generating hook: %w", err)
120 } 136 }
121 hookPath := fmt.Sprintf("%s/hooks/post-receive", repo) 137 if err := writeHook(client, repo, hookContent); err != nil {
122 if err := client.WriteSudoFile(hookPath, hookContent); err != nil { 138 return err
123 return fmt.Errorf("error writing hook: %w", err)
124 }
125 if _, err := client.RunSudo(fmt.Sprintf("chmod +x %s", hookPath)); err != nil {
126 return fmt.Errorf("error making hook executable: %w", err)
127 }
128 if _, err := client.RunSudo(fmt.Sprintf("chown git:git %s", hookPath)); err != nil {
129 return fmt.Errorf("error setting hook ownership: %w", err)
130 } 139 }
131 } else { 140 } else {
132 // Create directories for app
133 fmt.Println("-> Creating app directories...")
134 dirs := []string{
135 fmt.Sprintf("/var/lib/%s/data", name),
136 fmt.Sprintf("/var/lib/%s/src", name),
137 }
138 for _, dir := range dirs {
139 if _, err := client.RunSudo(fmt.Sprintf("mkdir -p %s", dir)); err != nil {
140 return fmt.Errorf("error creating directory %s: %w", dir, err)
141 }
142 }
143 if _, err := client.RunSudo(fmt.Sprintf("chown -R git:git /var/lib/%s", name)); err != nil {
144 return fmt.Errorf("error setting directory ownership: %w", err)
145 }
146
147 // Create env file 141 // Create env file
148 fmt.Println("-> Creating environment file...") 142 fmt.Println("-> Creating environment file...")
149 envContent := fmt.Sprintf("PORT=%d\nDATA_DIR=/data\n", port) 143 envContent := fmt.Sprintf("PORT=%d\nDATA_DIR=/data\n", port)
@@ -152,7 +146,7 @@ func runInit(cmd *cobra.Command, args []string) error {
152 return fmt.Errorf("error creating env file: %w", err) 146 return fmt.Errorf("error creating env file: %w", err)
153 } 147 }
154 148
155 // Write post-receive hook 149 // Write post-receive hook (handles dir creation on first push)
156 fmt.Println("-> Writing post-receive hook...") 150 fmt.Println("-> Writing post-receive hook...")
157 hookContent, err := templates.PostReceiveHook(map[string]string{ 151 hookContent, err := templates.PostReceiveHook(map[string]string{
158 "Name": name, 152 "Name": name,
@@ -160,15 +154,8 @@ func runInit(cmd *cobra.Command, args []string) error {
160 if err != nil { 154 if err != nil {
161 return fmt.Errorf("error generating hook: %w", err) 155 return fmt.Errorf("error generating hook: %w", err)
162 } 156 }
163 hookPath := fmt.Sprintf("%s/hooks/post-receive", repo) 157 if err := writeHook(client, repo, hookContent); err != nil {
164 if err := client.WriteSudoFile(hookPath, hookContent); err != nil { 158 return err
165 return fmt.Errorf("error writing hook: %w", err)
166 }
167 if _, err := client.RunSudo(fmt.Sprintf("chmod +x %s", hookPath)); err != nil {
168 return fmt.Errorf("error making hook executable: %w", err)
169 }
170 if _, err := client.RunSudo(fmt.Sprintf("chown git:git %s", hookPath)); err != nil {
171 return fmt.Errorf("error setting hook ownership: %w", err)
172 } 159 }
173 } 160 }
174 161
@@ -178,6 +165,7 @@ func runInit(cmd *cobra.Command, args []string) error {
178 Domain: domain, 165 Domain: domain,
179 Port: port, 166 Port: port,
180 Repo: repo, 167 Repo: repo,
168 Public: public,
181 }) 169 })
182 if err := st.Save(); err != nil { 170 if err := st.Save(); err != nil {
183 return fmt.Errorf("error saving state: %w", err) 171 return fmt.Errorf("error saving state: %w", err)
@@ -224,8 +212,25 @@ func runInit(cmd *cobra.Command, args []string) error {
224 } 212 }
225 } 213 }
226 214
227 // Resolve SSH hostname for git remote URL 215 // Initialize local git repo if needed
216 if _, err := os.Stat(".git"); os.IsNotExist(err) {
217 fmt.Println("-> Initializing git repo...")
218 gitInit := exec.Command("git", "init")
219 gitInit.Stdout = os.Stdout
220 gitInit.Stderr = os.Stderr
221 if err := gitInit.Run(); err != nil {
222 return fmt.Errorf("error initializing git repo: %w", err)
223 }
224 }
225
226 // Add origin remote (replace if it already exists)
228 sshHost := host 227 sshHost := host
228 remoteURL := fmt.Sprintf("git@%s:%s", sshHost, repo)
229 exec.Command("git", "remote", "remove", "origin").Run() // ignore error if not exists
230 addRemote := exec.Command("git", "remote", "add", "origin", remoteURL)
231 if err := addRemote.Run(); err != nil {
232 return fmt.Errorf("error adding git remote: %w", err)
233 }
229 234
230 fmt.Printf("\nProject initialized: %s\n", name) 235 fmt.Printf("\nProject initialized: %s\n", name)
231 fmt.Println("\nGenerated:") 236 fmt.Println("\nGenerated:")
@@ -240,8 +245,24 @@ func runInit(cmd *cobra.Command, args []string) error {
240 fmt.Println(" git add .ship/ Dockerfile") 245 fmt.Println(" git add .ship/ Dockerfile")
241 } 246 }
242 fmt.Println(" git commit -m \"initial deploy\"") 247 fmt.Println(" git commit -m \"initial deploy\"")
243 fmt.Printf(" git remote add ship git@%s:%s\n", sshHost, repo) 248 fmt.Println(" git push origin main")
244 fmt.Println(" git push ship main") 249 if !static {
250 fmt.Println("\n (No Dockerfile? Just push — deploy is skipped until one is added.)")
251 }
245 252
246 return nil 253 return nil
247} 254}
255
256func writeHook(client *ssh.Client, repo, content string) error {
257 hookPath := fmt.Sprintf("%s/hooks/post-receive", repo)
258 if err := client.WriteSudoFile(hookPath, content); err != nil {
259 return fmt.Errorf("error writing hook: %w", err)
260 }
261 if _, err := client.RunSudo(fmt.Sprintf("chmod +x %s", hookPath)); err != nil {
262 return fmt.Errorf("error making hook executable: %w", err)
263 }
264 if _, err := client.RunSudo(fmt.Sprintf("chown git:git %s", hookPath)); err != nil {
265 return fmt.Errorf("error setting hook ownership: %w", err)
266 }
267 return nil
268}
diff --git a/cmd/ship/list.go b/cmd/ship/list.go
index a10b2ca..af5baf8 100644
--- a/cmd/ship/list.go
+++ b/cmd/ship/list.go
@@ -37,7 +37,7 @@ func runList(cmd *cobra.Command, args []string) error {
37 } 37 }
38 38
39 w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0) 39 w := tabwriter.NewWriter(os.Stdout, 0, 0, 3, ' ', 0)
40 fmt.Fprintln(w, "NAME\tTYPE\tDOMAIN\tPORT") 40 fmt.Fprintln(w, "NAME\tTYPE\tVISIBILITY\tDOMAIN\tPORT")
41 for name, app := range apps { 41 for name, app := range apps {
42 port := "" 42 port := ""
43 if app.Type == "app" || app.Type == "git-app" { 43 if app.Type == "app" || app.Type == "git-app" {
@@ -47,7 +47,14 @@ func runList(cmd *cobra.Command, args []string) error {
47 if domain == "" { 47 if domain == "" {
48 domain = "-" 48 domain = "-"
49 } 49 }
50 fmt.Fprintf(w, "%s\t%s\t%s\t%s\n", name, app.Type, domain, port) 50 visibility := ""
51 if app.Repo != "" {
52 visibility = "private"
53 if app.Public {
54 visibility = "public"
55 }
56 }
57 fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\n", name, app.Type, visibility, domain, port)
51 } 58 }
52 w.Flush() 59 w.Flush()
53 return nil 60 return nil
diff --git a/internal/templates/templates.go b/internal/templates/templates.go
index 8615117..8f25f8f 100644
--- a/internal/templates/templates.go
+++ b/internal/templates/templates.go
@@ -99,23 +99,37 @@ while read oldrev newrev refname; do
99 [ "$branch" = "main" ] || { echo "Pushed to $branch, skipping deploy."; exit 0; } 99 [ "$branch" = "main" ] || { echo "Pushed to $branch, skipping deploy."; exit 0; }
100done 100done
101 101
102# Ensure checkout directory exists
103sudo /bin/mkdir -p "$SRC"
104sudo /bin/chown -R git:git "/var/lib/${NAME}"
105
102echo "==> Checking out code..." 106echo "==> Checking out code..."
103git --work-tree="$SRC" --git-dir="$REPO" checkout -f main 107git --work-tree="$SRC" --git-dir="$REPO" checkout -f main
104 108
105cd "$SRC" 109cd "$SRC"
106 110
107# Install deployment config from repo 111# If no Dockerfile, nothing to deploy
108if [ -f .ship/service ]; then 112if [ ! -f Dockerfile ]; then
113 echo "No Dockerfile found, skipping deploy."
114 exit 0
115fi
116
117# Install deployment config from repo (using full paths for sudoers)
118if [ -f "$SRC/.ship/service" ]; then
109 echo "==> Installing systemd unit..." 119 echo "==> Installing systemd unit..."
110 sudo cp .ship/service /etc/systemd/system/${NAME}.service 120 sudo /bin/cp "$SRC/.ship/service" "/etc/systemd/system/${NAME}.service"
111 sudo systemctl daemon-reload 121 sudo systemctl daemon-reload
112fi 122fi
113if [ -f .ship/Caddyfile ]; then 123if [ -f "$SRC/.ship/Caddyfile" ]; then
114 echo "==> Installing Caddy config..." 124 echo "==> Installing Caddy config..."
115 sudo cp .ship/Caddyfile /etc/caddy/sites-enabled/${NAME}.caddy 125 sudo /bin/cp "$SRC/.ship/Caddyfile" "/etc/caddy/sites-enabled/${NAME}.caddy"
116 sudo systemctl reload caddy 126 sudo systemctl reload caddy
117fi 127fi
118 128
129# Ensure data directory exists
130sudo /bin/mkdir -p "/var/lib/${NAME}/data"
131sudo /bin/chown -R git:git "/var/lib/${NAME}/data"
132
119echo "==> Building Docker image..." 133echo "==> Building Docker image..."
120docker build -t ${NAME}:latest . 134docker build -t ${NAME}:latest .
121 135
@@ -142,7 +156,7 @@ git --work-tree="$WEBROOT" --git-dir="$REPO" checkout -f main
142 156
143if [ -f "$WEBROOT/.ship/Caddyfile" ]; then 157if [ -f "$WEBROOT/.ship/Caddyfile" ]; then
144 echo "==> Installing Caddy config..." 158 echo "==> Installing Caddy config..."
145 sudo cp "$WEBROOT/.ship/Caddyfile" /etc/caddy/sites-enabled/${NAME}.caddy 159 sudo /bin/cp "$WEBROOT/.ship/Caddyfile" "/etc/caddy/sites-enabled/${NAME}.caddy"
146 sudo systemctl reload caddy 160 sudo systemctl reload caddy
147fi 161fi
148 162
@@ -164,7 +178,6 @@ var codeCaddyTemplate = `{{.BaseDomain}} {
164 transport fastcgi { 178 transport fastcgi {
165 env SCRIPT_FILENAME /usr/lib/git-core/git-http-backend 179 env SCRIPT_FILENAME /usr/lib/git-core/git-http-backend
166 env GIT_PROJECT_ROOT /srv/git 180 env GIT_PROJECT_ROOT /srv/git
167 env GIT_HTTP_EXPORT_ALL 1
168 env REQUEST_METHOD {method} 181 env REQUEST_METHOD {method}
169 env QUERY_STRING {query} 182 env QUERY_STRING {query}
170 env PATH_INFO {path} 183 env PATH_INFO {path}