This commit is contained in:
pavel 2026-05-14 19:21:15 +02:00
commit b38c39030b
22 changed files with 1925 additions and 0 deletions

View file

@ -0,0 +1,93 @@
package app
import (
"context"
"database/sql"
"encoding/json"
"errors"
"log"
"net/http"
"os"
"path/filepath"
"strconv"
"strings"
)
func (a *App) handleDeployWebhook(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return
}
path := strings.TrimPrefix(r.URL.Path, "/api/webhooks/deploy/")
parts := strings.Split(path, "/")
if len(parts) != 2 || parts[0] == "" || parts[1] == "" {
http.Error(w, "bad path", http.StatusBadRequest)
return
}
projectID, err := strconv.ParseInt(parts[0], 10, 64)
if err != nil {
http.Error(w, "invalid project id", http.StatusBadRequest)
return
}
p, err := a.getProjectForWebhook(r.Context(), projectID, parts[1])
if err != nil {
http.Error(w, "unauthorized", http.StatusUnauthorized)
return
}
var payload struct {
Ref string `json:"ref"`
}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
http.Error(w, "invalid json", http.StatusBadRequest)
return
}
if payload.Ref != "refs/heads/main" {
writeJSON(w, http.StatusAccepted, map[string]string{"status": "ignored", "reason": "not main branch"})
return
}
if err := a.deployProjectFromMain(r.Context(), p); err != nil {
log.Printf("deploy failed project_id=%d err=%v", p.ID, err)
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
writeJSON(w, http.StatusOK, map[string]string{"status": "deployed"})
}
func (a *App) getProjectForWebhook(ctx context.Context, projectID int64, token string) (Project, error) {
var p Project
err := a.db.QueryRowContext(ctx, `SELECT p.id, p.user_id, p.name, p.slug, p.description, p.repo_url, p.service_name, p.route_host, p.target_port, p.workspace_id, w.name, w.unix_user, p.deploy_token, p.created_at
FROM projects p JOIN workspaces w ON w.id=p.workspace_id WHERE p.id=$1 AND p.deploy_token=$2`, projectID, token).
Scan(&p.ID, &p.UserID, &p.Name, &p.Slug, &p.Description, &p.RepoURL, &p.ServiceName, &p.RouteHost, &p.TargetPort, &p.WorkspaceID, &p.Workspace, &p.UnixUser, &p.DeployToken, &p.CreatedAt)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return Project{}, errors.New("project not found")
}
return Project{}, err
}
p.WebhookURL = a.deployWebhookURL(p.ID, p.DeployToken)
return p, nil
}
func (a *App) deployProjectFromMain(_ context.Context, p Project) error {
if p.RepoURL == "" {
return errors.New("project has no repo_url to deploy from")
}
projectDir := filepath.Join(a.cfg.ProjectRoot, slugify(p.UserID), p.Workspace, p.Slug)
repoDir := filepath.Join(projectDir, "repo")
if _, err := os.Stat(repoDir); os.IsNotExist(err) {
if err := runCmd("git", "clone", "--branch", "main", "--single-branch", p.RepoURL, repoDir); err != nil {
return err
}
} else {
if err := runCmd("git", "-C", repoDir, "remote", "set-url", "origin", p.RepoURL); err != nil {
return err
}
if err := runCmd("git", "-C", repoDir, "fetch", "origin", "main", "--prune"); err != nil {
return err
}
if err := runCmd("git", "-C", repoDir, "reset", "--hard", "origin/main"); err != nil {
return err
}
}
return runCmd("sudo", "-n", "-u", p.UnixUser, "systemctl", "--user", "restart", p.ServiceName)
}