init
This commit is contained in:
commit
b38c39030b
22 changed files with 1925 additions and 0 deletions
192
internal/app/integrations.go
Normal file
192
internal/app/integrations.go
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func (a *App) createForgejoRepo(ctx context.Context, name, description string, private bool) (string, error) {
|
||||
if a.cfg.ForgejoBaseURL == "" || a.cfg.ForgejoToken == "" {
|
||||
return "", nil
|
||||
}
|
||||
payload := map[string]any{"name": name, "description": description, "private": private, "auto_init": true}
|
||||
body, _ := json.Marshal(payload)
|
||||
endpoint := a.cfg.ForgejoBaseURL + "/api/v1/user/repos"
|
||||
if a.cfg.ForgejoOrg != "" {
|
||||
endpoint = fmt.Sprintf("%s/api/v1/orgs/%s/repos", a.cfg.ForgejoBaseURL, a.cfg.ForgejoOrg)
|
||||
}
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodPost, endpoint, strings.NewReader(string(body)))
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
req.Header.Set("Authorization", "token "+a.cfg.ForgejoToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("forgejo request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode >= 300 {
|
||||
return "", fmt.Errorf("forgejo error: status=%d body=%s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
var parsed struct {
|
||||
HTMLURL string `json:"html_url"`
|
||||
}
|
||||
if err := json.Unmarshal(respBody, &parsed); err != nil {
|
||||
return "", nil
|
||||
}
|
||||
return parsed.HTMLURL, nil
|
||||
}
|
||||
|
||||
func (a *App) buildRouteHost(slug, username string) string {
|
||||
if a.cfg.CaddyDomainSuffix == "" {
|
||||
return fmt.Sprintf("%s-%s.local", slugify(username), slug)
|
||||
}
|
||||
return fmt.Sprintf("%s.%s", slug, strings.TrimPrefix(a.cfg.CaddyDomainSuffix, "."))
|
||||
}
|
||||
|
||||
func (a *App) ensureCaddyRoute(ctx context.Context, p Project) error {
|
||||
if !a.cfg.CaddyRouteEnabled {
|
||||
return nil
|
||||
}
|
||||
route := map[string]any{
|
||||
"@id": "project-route-" + p.UserID + "-" + p.Slug,
|
||||
"match": []any{map[string]any{"host": []string{p.RouteHost}}},
|
||||
"handle": []any{map[string]any{"handler": "reverse_proxy", "upstreams": []any{map[string]any{"dial": fmt.Sprintf("127.0.0.1:%d", p.TargetPort)}}}},
|
||||
}
|
||||
body, _ := json.Marshal(route)
|
||||
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) ensureForgejoDeployWebhook(ctx context.Context, p Project) error {
|
||||
if a.cfg.ForgejoBaseURL == "" || a.cfg.ForgejoToken == "" || p.RepoURL == "" {
|
||||
return nil
|
||||
}
|
||||
if a.cfg.WebhookBaseURL == "" {
|
||||
return errors.New("WEBHOOK_BASE_URL is required for automatic Forgejo webhook provisioning")
|
||||
}
|
||||
|
||||
owner, repo, err := parseForgejoRepoOwnerRepo(a.cfg.ForgejoBaseURL, p.RepoURL)
|
||||
if err != nil {
|
||||
// Existing repo is not on configured Forgejo host; skip auto-provision.
|
||||
return nil
|
||||
}
|
||||
|
||||
hooksURL := fmt.Sprintf("%s/api/v1/repos/%s/%s/hooks", a.cfg.ForgejoBaseURL, owner, repo)
|
||||
req, err := http.NewRequestWithContext(ctx, http.MethodGet, hooksURL, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
req.Header.Set("Authorization", "token "+a.cfg.ForgejoToken)
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
return fmt.Errorf("forgejo hooks list failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
if resp.StatusCode >= 300 {
|
||||
return fmt.Errorf("forgejo hooks list error: status=%d body=%s", resp.StatusCode, string(body))
|
||||
}
|
||||
var hooks []struct {
|
||||
Config struct {
|
||||
URL string `json:"url"`
|
||||
} `json:"config"`
|
||||
}
|
||||
_ = json.Unmarshal(body, &hooks)
|
||||
for _, h := range hooks {
|
||||
if strings.TrimSpace(h.Config.URL) == strings.TrimSpace(p.WebhookURL) {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
payload := map[string]any{
|
||||
"type": "gitea",
|
||||
"active": true,
|
||||
"events": []string{"push"},
|
||||
"config": map[string]any{
|
||||
"url": p.WebhookURL,
|
||||
"content_type": "json",
|
||||
},
|
||||
}
|
||||
bodyBytes, _ := json.Marshal(payload)
|
||||
createReq, err := http.NewRequestWithContext(ctx, http.MethodPost, hooksURL, strings.NewReader(string(bodyBytes)))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
createReq.Header.Set("Authorization", "token "+a.cfg.ForgejoToken)
|
||||
createReq.Header.Set("Content-Type", "application/json")
|
||||
createResp, err := http.DefaultClient.Do(createReq)
|
||||
if err != nil {
|
||||
return fmt.Errorf("forgejo hook create failed: %w", err)
|
||||
}
|
||||
defer createResp.Body.Close()
|
||||
createBody, _ := io.ReadAll(createResp.Body)
|
||||
if createResp.StatusCode >= 300 {
|
||||
return fmt.Errorf("forgejo hook create error: status=%d body=%s", createResp.StatusCode, string(createBody))
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func parseForgejoRepoOwnerRepo(forgejoBaseURL, repoURL string) (string, string, error) {
|
||||
fBase, err := url.Parse(forgejoBaseURL)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
normalized := strings.TrimSpace(repoURL)
|
||||
if strings.HasPrefix(normalized, "git@") {
|
||||
// git@host:owner/repo(.git)
|
||||
at := strings.Index(normalized, "@")
|
||||
colon := strings.Index(normalized, ":")
|
||||
if at < 0 || colon < 0 || colon <= at+1 {
|
||||
return "", "", errors.New("invalid ssh repo url")
|
||||
}
|
||||
host := normalized[at+1 : colon]
|
||||
if !strings.EqualFold(host, fBase.Hostname()) {
|
||||
return "", "", errors.New("repo host does not match configured forgejo host")
|
||||
}
|
||||
path := strings.TrimPrefix(normalized[colon+1:], "/")
|
||||
path = strings.TrimSuffix(path, ".git")
|
||||
parts := strings.Split(path, "/")
|
||||
if len(parts) < 2 {
|
||||
return "", "", errors.New("repo path does not include owner/repo")
|
||||
}
|
||||
return parts[0], parts[1], nil
|
||||
}
|
||||
|
||||
u, err := url.Parse(normalized)
|
||||
if err != nil {
|
||||
return "", "", err
|
||||
}
|
||||
if !strings.EqualFold(u.Hostname(), fBase.Hostname()) {
|
||||
return "", "", errors.New("repo host does not match configured forgejo host")
|
||||
}
|
||||
path := strings.TrimPrefix(u.Path, "/")
|
||||
path = strings.TrimSuffix(path, ".git")
|
||||
parts := strings.Split(path, "/")
|
||||
if len(parts) < 2 {
|
||||
return "", "", errors.New("repo path does not include owner/repo")
|
||||
}
|
||||
return parts[0], parts[1], nil
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue