From 3ac11468a1b40c3dd332f184d52d03beb7b50b4b Mon Sep 17 00:00:00 2001 From: pavel Date: Thu, 14 May 2026 20:21:01 +0200 Subject: [PATCH] fix service templates and paths --- .env.example | 3 +-- README.md | 14 ++++++++++---- internal/app/config.go | 21 +-------------------- internal/app/deploy_service.go | 17 ++++++++++++++++- internal/app/paths.go | 29 +++++++++++++++++++++++++++++ internal/app/systemd_service.go | 15 +++++++++++++-- internal/app/types.go | 1 - 7 files changed, 70 insertions(+), 30 deletions(-) create mode 100644 internal/app/paths.go diff --git a/.env.example b/.env.example index aa11624..740dedf 100644 --- a/.env.example +++ b/.env.example @@ -9,8 +9,7 @@ AUTH_HEADER_EMAIL=X-authentik-email FORGEJO_BASE_URL= FORGEJO_TOKEN= FORGEJO_ORG= -PROJECTS_ROOT=$HOME/projects -DEFAULT_RUN_COMMAND=go run . +PROJECTS_ROOT=$HOME/projects/$app CADDY_ROUTE_ENABLED=false CADDY_ADMIN_URL=http://localhost:2019 CADDY_SERVER_ID=srv0 diff --git a/README.md b/README.md index 37282b4..2444709 100644 --- a/README.md +++ b/README.md @@ -38,6 +38,10 @@ Important: - `X-authentik-email` - `FORGEJO_BASE_URL` + `FORGEJO_TOKEN` enable repository creation. - `FORGEJO_ORG` switches creation from personal repos to org repos. +- `PROJECTS_ROOT` supports templates: + - `$HOME` = workspace unix user home + - `$app` = project slug + - Example: `$HOME/projects/$app` - `UNIX_SOCKET_PATH` enables unix socket listener in addition to `LISTEN_ADDR`. - `CADDY_ROUTE_ENABLED=true` enables route provisioning. - `CADDY_ADMIN_URL` points to the Caddy Admin API (default `http://localhost:2019`). @@ -79,9 +83,10 @@ Route shape: - If auto-provisioning cannot apply (for example external non-Forgejo repo), configure a webhook manually to call `webhook_url`. - Deploy runs only when payload `ref` equals `refs/heads/main`. - Deploy behavior: - 1. Clone repo to `////repo` if missing. + 1. Clone repo to `/repo` if missing. 2. Otherwise `fetch origin main` and `reset --hard origin/main`. - 3. Restart the project user service via `systemctl --user restart`. + 3. Build binary to `/bin/app`. + 4. Restart the project user service via `systemctl --user restart`. Notes: - `WEBHOOK_BASE_URL` should be set to an externally reachable base URL so Forgejo can call webhook endpoints. @@ -100,7 +105,8 @@ Notes: ## Systemd For each project in workspace `` owned by unix user ``, the app writes: -- env file: `////.env` +- env file: `/.env` +- binary path used by systemd: `/bin/app` - unit file: `/.config/systemd/user/projectmgr--.service` Then it attempts as that unix user: @@ -109,6 +115,6 @@ Then it attempts as that unix user: The generated `.env` is synced from the project env-var configuration and always includes: - `LISTEN_NETWORK=unix` -- `LISTEN_ADDRESS=////app.sock` +- `LISTEN_ADDRESS=/app.sock` If user systemd is not available in the runtime environment, project creation still succeeds and unit files are still generated. diff --git a/internal/app/config.go b/internal/app/config.go index 90fdf54..6b22c87 100644 --- a/internal/app/config.go +++ b/internal/app/config.go @@ -3,13 +3,12 @@ package app import ( "errors" "os" - "path/filepath" "strconv" "strings" ) func loadConfig() (Config, error) { - projectRoot := expandPath(getenv("PROJECTS_ROOT", "$HOME/projects")) + projectRoot := strings.TrimSpace(getenv("PROJECTS_ROOT", "$HOME/projects/$app")) cfg := Config{ DatabaseURL: getenv("DATABASE_URL", "postgres://postgres:postgres@localhost:5432/box?sslmode=disable"), ListenAddr: getenv("LISTEN_ADDR", ":8080"), @@ -23,7 +22,6 @@ func loadConfig() (Config, error) { ForgejoToken: getenv("FORGEJO_TOKEN", ""), ForgejoOrg: getenv("FORGEJO_ORG", ""), ProjectRoot: projectRoot, - DefaultRunCommand: getenv("DEFAULT_RUN_COMMAND", "go run ."), CaddyAdminURL: strings.TrimRight(getenv("CADDY_ADMIN_URL", "http://localhost:2019"), "/"), CaddyServerID: getenv("CADDY_SERVER_ID", "srv0"), CaddyDomainSuffix: getenv("CADDY_DOMAIN_SUFFIX", ""), @@ -36,23 +34,6 @@ func loadConfig() (Config, error) { return cfg, nil } -func expandPath(p string) string { - p = strings.TrimSpace(p) - if p == "" { - return filepath.Join(userHomeOrDot(), "projects") - } - if strings.HasPrefix(p, "~/") { - p = filepath.Join(userHomeOrDot(), strings.TrimPrefix(p, "~/")) - } - p = os.ExpandEnv(p) - if !filepath.IsAbs(p) { - if abs, err := filepath.Abs(p); err == nil { - p = abs - } - } - return filepath.Clean(p) -} - func userHomeOrDot() string { h, err := os.UserHomeDir() if err != nil { diff --git a/internal/app/deploy_service.go b/internal/app/deploy_service.go index 11e9fa0..16a5c2a 100644 --- a/internal/app/deploy_service.go +++ b/internal/app/deploy_service.go @@ -5,6 +5,7 @@ import ( "database/sql" "encoding/json" "errors" + "fmt" "log" "net/http" "os" @@ -72,8 +73,15 @@ 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) + projectDir, err := a.projectDirFor(p) + if err != nil { + return err + } repoDir := filepath.Join(projectDir, "repo") + binDir := filepath.Join(projectDir, "bin") + if err := os.MkdirAll(binDir, 0o755); err != nil { + return err + } if _, err := os.Stat(repoDir); os.IsNotExist(err) { if err := runCmd("git", "clone", "--branch", "main", "--single-branch", p.RepoURL, repoDir); err != nil { return err @@ -89,5 +97,12 @@ func (a *App) deployProjectFromMain(_ context.Context, p Project) error { return err } } + binaryPath := filepath.Join(binDir, "app") + if err := runCmd("go", "-C", repoDir, "build", "-o", binaryPath, "."); err != nil { + return fmt.Errorf("build failed: %w", err) + } + if err := ensureOwnedByUnixUser(binaryPath, p.UnixUser, 0o755); err != nil { + return err + } return runSystemctlUser(p.UnixUser, "restart", p.ServiceName) } diff --git a/internal/app/paths.go b/internal/app/paths.go new file mode 100644 index 0000000..8555644 --- /dev/null +++ b/internal/app/paths.go @@ -0,0 +1,29 @@ +package app + +import ( + "path/filepath" + "strings" +) + +func (a *App) projectDirFor(p Project) (string, error) { + homeDir, err := homeForUnixUser(p.UnixUser) + if err != nil { + return "", err + } + tpl := strings.TrimSpace(a.cfg.ProjectRoot) + if tpl == "" { + tpl = "$HOME/projects/$app" + } + path := tpl + path = strings.ReplaceAll(path, "${HOME}", homeDir) + path = strings.ReplaceAll(path, "$HOME", homeDir) + path = strings.ReplaceAll(path, "${app}", p.Slug) + path = strings.ReplaceAll(path, "$app", p.Slug) + if strings.HasPrefix(path, "~/") { + path = filepath.Join(homeDir, strings.TrimPrefix(path, "~/")) + } + if !filepath.IsAbs(path) { + path = filepath.Join(homeDir, path) + } + return filepath.Clean(path), nil +} diff --git a/internal/app/systemd_service.go b/internal/app/systemd_service.go index 8e3bb2f..e6d67e2 100644 --- a/internal/app/systemd_service.go +++ b/internal/app/systemd_service.go @@ -19,7 +19,10 @@ func (a *App) renderSystemdForProject(ctx context.Context, p Project) error { } envVars = []EnvVar{} } - projectDir := filepath.Join(a.cfg.ProjectRoot, slugify(p.UserID), p.Workspace, p.Slug) + projectDir, err := a.projectDirFor(p) + if err != nil { + return err + } if err := os.MkdirAll(projectDir, 0o755); err != nil { return err } @@ -27,6 +30,11 @@ func (a *App) renderSystemdForProject(ctx context.Context, p Project) error { if err := os.MkdirAll(dataDir, 0o755); err != nil { return err } + binDir := filepath.Join(projectDir, "bin") + if err := os.MkdirAll(binDir, 0o755); err != nil { + return err + } + binaryPath := filepath.Join(binDir, "app") socketPath := filepath.Join(projectDir, "app.sock") envPath := filepath.Join(projectDir, ".env") @@ -58,6 +66,9 @@ func (a *App) renderSystemdForProject(ctx context.Context, p Project) error { if err := ensureOwnedByUnixUser(dataDir, p.UnixUser, 0o755); err != nil { return err } + if err := ensureOwnedByUnixUser(binDir, p.UnixUser, 0o755); err != nil { + return err + } if err := ensureOwnedByUnixUser(envPath, p.UnixUser, 0o600); err != nil { return err } @@ -76,7 +87,7 @@ RestartSec=5 [Install] WantedBy=default.target -`, p.Name, dataDir, envPath, a.cfg.DefaultRunCommand) +`, p.Name, dataDir, envPath, binaryPath) homeDir, err := homeForUnixUser(p.UnixUser) if err != nil { diff --git a/internal/app/types.go b/internal/app/types.go index 4d112ce..3cf4c86 100644 --- a/internal/app/types.go +++ b/internal/app/types.go @@ -18,7 +18,6 @@ type Config struct { ForgejoToken string ForgejoOrg string ProjectRoot string - DefaultRunCommand string CaddyAdminURL string CaddyServerID string CaddyDomainSuffix string