static
This commit is contained in:
parent
9d5a33e743
commit
53a5e2e45a
3 changed files with 67 additions and 92 deletions
|
|
@ -85,7 +85,8 @@ Route shape:
|
||||||
1. Clone repo to `~/projects/<app>/repo` if missing.
|
1. Clone repo to `~/projects/<app>/repo` if missing.
|
||||||
2. Otherwise `fetch origin main` and `reset --hard origin/main`.
|
2. Otherwise `fetch origin main` and `reset --hard origin/main`.
|
||||||
3. Build binary to `~/projects/<app>/bin/app`.
|
3. Build binary to `~/projects/<app>/bin/app`.
|
||||||
4. Restart the project user service via `systemctl --user restart`.
|
4. Sync static assets from `~/projects/<app>/repo/static` to `~/projects/<app>/data/static`.
|
||||||
|
5. Restart the project user service via `systemctl --user restart`.
|
||||||
|
|
||||||
Notes:
|
Notes:
|
||||||
- `WEBHOOK_BASE_URL` should be set to an externally reachable base URL so Forgejo can call webhook endpoints.
|
- `WEBHOOK_BASE_URL` should be set to an externally reachable base URL so Forgejo can call webhook endpoints.
|
||||||
|
|
|
||||||
|
|
@ -113,6 +113,9 @@ func (a *App) buildProjectBinary(p Project) error {
|
||||||
if err := ensureOwnedByUnixUser(binaryPath, p.UnixUser, 0o755); err != nil {
|
if err := ensureOwnedByUnixUser(binaryPath, p.UnixUser, 0o755); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
if err := a.syncStaticAssetsForProject(p); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -186,3 +189,38 @@ func runGoBuildForUser(unixUser, repoDir, binaryPath string) error {
|
||||||
"go", "-C", repoDir, "build", "-buildvcs=false", "-o", binaryPath, ".",
|
"go", "-C", repoDir, "build", "-buildvcs=false", "-o", binaryPath, ".",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (a *App) syncStaticAssetsForProject(p Project) error {
|
||||||
|
projectDir, err := a.projectDirFor(p)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
repoStatic := filepath.Join(projectDir, "repo", "static")
|
||||||
|
dataStatic := filepath.Join(projectDir, "data", "static")
|
||||||
|
|
||||||
|
if _, err := os.Stat(repoStatic); err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := os.MkdirAll(dataStatic, 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := ensureOwnedByUnixUser(dataStatic, p.UnixUser, 0o755); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Preferred path: fast deterministic sync with deletes.
|
||||||
|
if err := runCmd("sudo", "-n", "-u", p.UnixUser, "rsync", "-a", "--delete", repoStatic+"/", dataStatic+"/"); err == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Fallback if rsync is unavailable: recreate target and copy.
|
||||||
|
if err := runCmd("sudo", "-n", "-u", p.UnixUser, "rm", "-rf", dataStatic); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := runCmd("sudo", "-n", "-u", p.UnixUser, "mkdir", "-p", dataStatic); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return runCmd("sudo", "-n", "-u", p.UnixUser, "cp", "-a", repoStatic+"/.", dataStatic+"/")
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -72,70 +72,12 @@ func (a *App) ensureCaddyRoute(ctx context.Context, p Project) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
indexes := findCaddyRouteIndexesByID(cfg, a.cfg.CaddyServerID, route["@id"].(string))
|
newRoutes, err := mergedDedupedRoutes(cfg, a.cfg.CaddyServerID, route)
|
||||||
if len(indexes) > 0 {
|
if err != nil {
|
||||||
// Update first matching route in place.
|
|
||||||
if err := a.putCaddyRouteAtIndex(ctx, indexes[0], route); err != nil {
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
// Remove stale duplicates from highest index to lowest.
|
body, _ := json.Marshal(newRoutes)
|
||||||
for i := len(indexes) - 1; i >= 1; i-- {
|
|
||||||
if err := a.deleteCaddyRouteAtIndex(ctx, indexes[i]); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := a.appendCaddyRoute(ctx, route); err != nil {
|
|
||||||
// If duplicate race happened, retry once through dedupe/update path.
|
|
||||||
if strings.Contains(err.Error(), "duplicate ID") {
|
|
||||||
cfg2, ferr := a.fetchCaddyConfig(ctx)
|
|
||||||
if ferr != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
indexes2 := findCaddyRouteIndexesByID(cfg2, a.cfg.CaddyServerID, route["@id"].(string))
|
|
||||||
if len(indexes2) == 0 {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if uerr := a.putCaddyRouteAtIndex(ctx, indexes2[0], route); uerr != nil {
|
|
||||||
return uerr
|
|
||||||
}
|
|
||||||
for i := len(indexes2) - 1; i >= 1; i-- {
|
|
||||||
if derr := a.deleteCaddyRouteAtIndex(ctx, indexes2[i]); derr != nil {
|
|
||||||
return derr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) appendCaddyRoute(ctx context.Context, route map[string]any) error {
|
|
||||||
body, _ := json.Marshal(route)
|
|
||||||
url := fmt.Sprintf("%s/config/apps/http/servers/%s/routes", a.cfg.CaddyAdminURL, a.cfg.CaddyServerID)
|
url := fmt.Sprintf("%s/config/apps/http/servers/%s/routes", a.cfg.CaddyAdminURL, a.cfg.CaddyServerID)
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, strings.NewReader(string(body)))
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
req.Header.Set("Content-Type", "application/json")
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("caddy request failed: %w", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
if resp.StatusCode >= 300 {
|
|
||||||
respBody, _ := io.ReadAll(resp.Body)
|
|
||||||
return fmt.Errorf("caddy error: status=%d body=%s", resp.StatusCode, string(respBody))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *App) putCaddyRouteAtIndex(ctx context.Context, idx int, route map[string]any) error {
|
|
||||||
body, _ := json.Marshal(route)
|
|
||||||
url := fmt.Sprintf("%s/config/apps/http/servers/%s/routes/%d", a.cfg.CaddyAdminURL, a.cfg.CaddyServerID, idx)
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, strings.NewReader(string(body)))
|
req, err := http.NewRequestWithContext(ctx, http.MethodPut, url, strings.NewReader(string(body)))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|
@ -143,66 +85,60 @@ func (a *App) putCaddyRouteAtIndex(ctx context.Context, idx int, route map[strin
|
||||||
req.Header.Set("Content-Type", "application/json")
|
req.Header.Set("Content-Type", "application/json")
|
||||||
resp, err := http.DefaultClient.Do(req)
|
resp, err := http.DefaultClient.Do(req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("caddy update request failed: %w", err)
|
return fmt.Errorf("caddy routes rewrite failed: %w", err)
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
if resp.StatusCode >= 300 {
|
if resp.StatusCode >= 300 {
|
||||||
respBody, _ := io.ReadAll(resp.Body)
|
respBody, _ := io.ReadAll(resp.Body)
|
||||||
return fmt.Errorf("caddy update error: status=%d body=%s", resp.StatusCode, string(respBody))
|
return fmt.Errorf("caddy routes rewrite error: status=%d body=%s", resp.StatusCode, string(respBody))
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) deleteCaddyRouteAtIndex(ctx context.Context, idx int) error {
|
func mergedDedupedRoutes(cfg map[string]any, serverID string, desired map[string]any) ([]any, error) {
|
||||||
url := fmt.Sprintf("%s/config/apps/http/servers/%s/routes/%d", a.cfg.CaddyAdminURL, a.cfg.CaddyServerID, idx)
|
|
||||||
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, url, nil)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
resp, err := http.DefaultClient.Do(req)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("caddy delete request failed: %w", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
if resp.StatusCode >= 300 {
|
|
||||||
respBody, _ := io.ReadAll(resp.Body)
|
|
||||||
return fmt.Errorf("caddy delete error: status=%d body=%s", resp.StatusCode, string(respBody))
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func findCaddyRouteIndexesByID(cfg map[string]any, serverID, routeID string) []int {
|
|
||||||
var out []int
|
|
||||||
apps, ok := cfg["apps"].(map[string]any)
|
apps, ok := cfg["apps"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
return out
|
return nil, errors.New("caddy config missing apps")
|
||||||
}
|
}
|
||||||
httpApp, ok := apps["http"].(map[string]any)
|
httpApp, ok := apps["http"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
return out
|
return nil, errors.New("caddy config missing apps.http")
|
||||||
}
|
}
|
||||||
servers, ok := httpApp["servers"].(map[string]any)
|
servers, ok := httpApp["servers"].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
return out
|
return nil, errors.New("caddy config missing apps.http.servers")
|
||||||
}
|
}
|
||||||
server, ok := servers[serverID].(map[string]any)
|
server, ok := servers[serverID].(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
return out
|
return nil, fmt.Errorf("caddy config missing server %q", serverID)
|
||||||
}
|
}
|
||||||
routes, ok := server["routes"].([]any)
|
routes, ok := server["routes"].([]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
return out
|
routes = []any{}
|
||||||
}
|
}
|
||||||
|
desiredID, _ := desired["@id"].(string)
|
||||||
|
var out []any
|
||||||
|
seenDesired := false
|
||||||
for i, r := range routes {
|
for i, r := range routes {
|
||||||
|
_ = i
|
||||||
rm, ok := r.(map[string]any)
|
rm, ok := r.(map[string]any)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
out = append(out, r)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if rid, ok := rm["@id"].(string); ok && rid == routeID {
|
if rid, ok := rm["@id"].(string); ok && rid == desiredID {
|
||||||
out = append(out, i)
|
if !seenDesired {
|
||||||
|
out = append(out, desired)
|
||||||
|
seenDesired = true
|
||||||
}
|
}
|
||||||
|
continue
|
||||||
}
|
}
|
||||||
return out
|
out = append(out, r)
|
||||||
|
}
|
||||||
|
if !seenDesired {
|
||||||
|
out = append(out, desired)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *App) fetchCaddyConfig(ctx context.Context) (map[string]any, error) {
|
func (a *App) fetchCaddyConfig(ctx context.Context) (map[string]any, error) {
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue