package webhook import ( "context" "fmt" "path" "github.com/alexei/docker-watcher/internal/store" ) // FindProjectAndStage searches for a project whose image matches the parsed // image reference, then finds the stage whose tag pattern matches the incoming // tag. Returns (project, stage, found, error). // // Matching logic: // 1. Iterate all projects. // 2. Compare the project's Image field against the parsed image's FullName(). // 3. For the matched project, iterate its stages and find one whose TagPattern // matches the incoming tag using path.Match (glob semantics). // 4. If multiple stages match, the first match wins (stages are ordered by name). func FindProjectAndStage(ctx context.Context, st *store.Store, parsed ParsedImage) (store.Project, store.Stage, bool, error) { projects, err := st.GetAllProjects() if err != nil { return store.Project{}, store.Stage{}, false, fmt.Errorf("get projects: %w", err) } imageName := parsed.FullName() for _, project := range projects { if !imageMatches(project.Image, imageName) { continue } stage, found, err := matchStage(st, project.ID, parsed.Tag) if err != nil { return store.Project{}, store.Stage{}, false, fmt.Errorf("match stage for project %s: %w", project.Name, err) } if found { return project, stage, true, nil } // Project matches but no stage pattern matches this tag. // Return project with empty stage — caller can decide what to do. // For now, we treat it as "not found" so auto-create doesn't fire // for known projects with no matching stage. return store.Project{}, store.Stage{}, false, nil } return store.Project{}, store.Stage{}, false, nil } // imageMatches checks if a project's stored image name matches the parsed // image name. The comparison is case-sensitive and supports the project image // being stored as either "owner/name" or just "name". func imageMatches(projectImage, incomingImage string) bool { if projectImage == incomingImage { return true } // Also match if the incoming image has an owner prefix but the project // only stores the bare name (or vice versa). This handles registries // that include or omit the owner segment. return false } // 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 }