package registry import ( "context" "fmt" "path" "sort" "strings" ) // RegistryImage represents a container image discovered from a registry. type RegistryImage struct { Name string `json:"name"` Owner string `json:"owner"` FullRef string `json:"full_ref"` // e.g., "git.example.com/owner/my-app" } // Client defines the interface for interacting with a container image registry. type Client interface { // ListTags returns all available tags for the given image. ListTags(ctx context.Context, image string) ([]string, error) // GetLatestTag returns the most recently created tag that matches the given // glob pattern. Returns an empty string and no error if no tags match. GetLatestTag(ctx context.Context, image string, pattern string) (string, error) // ListImages returns all container images available in the registry for the // given owner. Returns an error if the registry does not support image listing. ListImages(ctx context.Context, owner string) ([]RegistryImage, error) } // DeployTriggerer is called by the poller when a new tag is detected for a // stage with auto_deploy enabled. This decouples the registry package from the // deployer implementation. type DeployTriggerer interface { TriggerDeploy(ctx context.Context, projectID, stageID, imageTag string) error } // MatchTags filters a list of tags, returning only those that match the given // glob pattern. Pattern matching uses path.Match semantics (*, ?, []). // Returns an error if the pattern is malformed. func MatchTags(tags []string, pattern string) ([]string, error) { if pattern == "" || pattern == "*" { result := make([]string, len(tags)) copy(result, tags) return result, nil } // Validate pattern once before iterating. if _, err := path.Match(pattern, ""); err != nil { return nil, fmt.Errorf("invalid tag pattern %q: %w", pattern, err) } var matched []string for _, tag := range tags { ok, _ := path.Match(pattern, tag) if ok { matched = append(matched, tag) } } return matched, nil } // LatestTag returns the last element of a sorted tag list that matches the // pattern. Tags are sorted lexicographically; the "latest" is the last in sort // order. Returns empty string if no tags match. Returns an error if the pattern // is malformed. func LatestTag(tags []string, pattern string) (string, error) { matched, err := MatchTags(tags, pattern) if err != nil { return "", err } if len(matched) == 0 { return "", nil } sort.Strings(matched) return matched[len(matched)-1], nil } // NewClient creates a registry Client based on the registry type string. // Supported types: "gitea". Future: "github", "dockerhub". func NewClient(registryType, baseURL, token string) (Client, error) { switch strings.ToLower(registryType) { case "gitea": return NewGiteaClient(baseURL, token), nil default: return nil, fmt.Errorf("unsupported registry type: %s", registryType) } }