0632f512e6
Build / build (push) Successful in 10m25s
Replace the single global webhook secret with entity-scoped secrets stored
on each project and static site. Webhook-driven project autocreate is
removed — projects must exist before their URL can trigger deploys.
Also wires static-site webhooks (sync_trigger=push|tag), turning the
previously inert "push" trigger into a functional one: POST the site's
webhook URL from a Git provider and Tinyforge re-syncs on matching refs.
- Adds webhook_secret columns + unique indexes to projects and static_sites
- Per-entity GET/regenerate endpoints under /api/projects/{id}/webhook
and /api/sites/{id}/webhook (admin-only)
- Removes /api/settings/webhook-url and the global webhook panel
- Reusable WebhookPanel Svelte component on both detail pages, i18n in en/ru
- Tests for matcher (siteRefMatches, ParseImageRef) and handler (project
match/mismatch/404 and site push/manual/branch-skip)
81 lines
2.0 KiB
Go
81 lines
2.0 KiB
Go
package webhook
|
|
|
|
import (
|
|
"fmt"
|
|
"path"
|
|
"strings"
|
|
|
|
"github.com/alexei/tinyforge/internal/store"
|
|
)
|
|
|
|
// matchStage finds the first stage of a project whose tag pattern matches the
|
|
// given tag. Uses path.Match for glob-style matching (same as the registry poller).
|
|
func matchStage(st *store.Store, projectID, tag string) (store.Stage, bool, error) {
|
|
stages, err := st.GetStagesByProjectID(projectID)
|
|
if err != nil {
|
|
return store.Stage{}, false, fmt.Errorf("get stages: %w", err)
|
|
}
|
|
|
|
for _, stage := range stages {
|
|
pattern := stage.TagPattern
|
|
if pattern == "" {
|
|
pattern = "*"
|
|
}
|
|
|
|
matched, err := path.Match(pattern, tag)
|
|
if err != nil {
|
|
// Invalid pattern — skip this stage.
|
|
continue
|
|
}
|
|
if matched {
|
|
return stage, true, nil
|
|
}
|
|
}
|
|
|
|
return store.Stage{}, false, nil
|
|
}
|
|
|
|
// imageMatches reports whether an incoming image reference matches the
|
|
// project's stored image. The comparison is case-sensitive and exact.
|
|
func imageMatches(projectImage, incomingImage string) bool {
|
|
return projectImage == incomingImage
|
|
}
|
|
|
|
// siteRefMatches reports whether a Git ref (e.g. "refs/heads/main" or
|
|
// "refs/tags/v1.2.3") targets the site's configured branch or tag pattern.
|
|
//
|
|
// For sync_trigger = "push": the ref must be a heads/<branch> ref whose
|
|
// branch name equals site.Branch.
|
|
// For sync_trigger = "tag": the ref must be a tags/<tag> ref whose tag name
|
|
// matches site.TagPattern via glob semantics.
|
|
// Unknown triggers return false (caller should have filtered these out).
|
|
func siteRefMatches(site store.StaticSite, ref string) bool {
|
|
switch site.SyncTrigger {
|
|
case "push":
|
|
branch, ok := strings.CutPrefix(ref, "refs/heads/")
|
|
if !ok {
|
|
return false
|
|
}
|
|
if site.Branch == "" {
|
|
return true
|
|
}
|
|
return branch == site.Branch
|
|
case "tag":
|
|
tag, ok := strings.CutPrefix(ref, "refs/tags/")
|
|
if !ok {
|
|
return false
|
|
}
|
|
pattern := site.TagPattern
|
|
if pattern == "" {
|
|
pattern = "*"
|
|
}
|
|
matched, err := path.Match(pattern, tag)
|
|
if err != nil {
|
|
return false
|
|
}
|
|
return matched
|
|
default:
|
|
return false
|
|
}
|
|
}
|