package webhook import ( "fmt" "log/slog" "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 { slog.Warn("webhook: invalid tag pattern, skipping stage", "project", projectID, "stage", stage.Name, "pattern", pattern, "error", err) 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 registry hostname is matched case-insensitively // (per RFC: registry hostnames are case-insensitive); the path/owner/name are // matched exactly. func imageMatches(projectImage, incomingImage string) bool { if projectImage == incomingImage { return true } pIdx := strings.IndexByte(projectImage, '/') iIdx := strings.IndexByte(incomingImage, '/') if pIdx <= 0 || iIdx <= 0 { return false } pHost, pPath := projectImage[:pIdx], projectImage[pIdx:] iHost, iPath := incomingImage[:iIdx], incomingImage[iIdx:] return strings.EqualFold(pHost, iHost) && pPath == iPath } // 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/ ref whose // branch name equals site.Branch. // For sync_trigger = "tag": the ref must be a tags/ 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 } }