7 Commits

Author SHA1 Message Date
victor.kolomin 8665ea22e1 remove
ci/woodpecker/tag/woodpecker Pipeline was successful
2025-11-30 16:34:14 +01:00
victor.kolomin 93b35eb1d9 add woodpecker CI
ci/woodpecker/tag/woodpecker Pipeline failed
2025-11-30 16:31:45 +01:00
victor.kolomin d0a8c5b3a8 change logic for prepull and file modification
continuous-integration/drone/tag Build is passing
2025-09-19 15:39:50 +02:00
victor.kolomin d3530314c2 add prepull before commit
continuous-integration/drone/tag Build is passing
2025-09-19 15:30:48 +02:00
victor.kolomin 64a96fa4cd migrate from aws to docker.io
continuous-integration/drone/tag Build is passing
2025-09-19 14:19:38 +02:00
victor.kolomin e7e52ca3b5 add WEB API check if Docker API check failed
continuous-integration/drone/tag Build is failing
2025-09-19 14:10:41 +02:00
victor.kolomin 157f79240b TryNormalizeTagForSemVer example, "1.2.3.4" might become "1.2.3-rev.4"
continuous-integration/drone/tag Build is passing
2025-09-19 12:28:34 +02:00
8 changed files with 262 additions and 69 deletions
+21
View File
@@ -0,0 +1,21 @@
steps:
- name: build and push docker image
image: woodpeckerci/plugin-docker-buildx
settings:
repo: git.kolspace.cc/victor.kolomin/controlla
registry: git.kolspace.cc
platforms: linux/amd64
username:
from_secret: gitea_registry_user
password:
from_secret: gitea_registry_pass
tags:
- ${CI_COMMIT_TAG}
- latest
cache_from:
- git.kolspace.cc/victor.kolomin/controlla:latest
cache_to: type=inline
when:
event:
- tag
+2 -2
View File
@@ -1,4 +1,4 @@
FROM public.ecr.aws/docker/library/golang:1.25.1-alpine3.22 AS builder
FROM docker.io/library/golang:1.25.1-alpine3.22 AS builder
WORKDIR /app
COPY . .
@@ -7,7 +7,7 @@ COPY . .
RUN go build -o controlla .
# Final minimal image
FROM public.ecr.aws/docker/library/alpine:3.22.1
FROM docker.io/library/alpine:3.22.1
WORKDIR /app
# Copy the binary and config file from the builder stage
+65 -4
View File
@@ -5,6 +5,9 @@ import (
"fmt"
"io"
"net/http"
"time"
"github.com/sirupsen/logrus"
)
type CommonRegistryClient struct {
@@ -15,13 +18,71 @@ type CommonRegistryClient struct {
// NewCommonRegistryClient creates a new CommonRegistryClient for the specified registry host.
func NewCommonRegistryClient(host string) *CommonRegistryClient {
return &CommonRegistryClient{
Client: &http.Client{},
Host: host,
Client: &http.Client{
Timeout: 15 * time.Second,
},
Host: host,
}
}
// GetTags fetches the list of tags for a given image from the registry.
// GetTags retrieves a list of tags for a given image from the registry.
// It first tries the standard Docker Registry API, and if that fails,
// it falls back to a Gitea-like Web API.
func (c *CommonRegistryClient) GetTags(image string) ([]string, error) {
tags, err := c.getTagsFromDockerAPI(image)
if err != nil {
logrus.Warnf("Standard Docker API failed for %s (error: %v). Trying Web API fallback...", c.Host, err)
// Try the Web API fallback
fallbackTags, fallbackErr := c.getTagsFromWebAPI(image)
if fallbackErr != nil {
logrus.Errorf("Web API fallback also failed for %s (error: %v).", c.Host, fallbackErr)
// Возвращаем оригинальную, более информативную ошибку от Docker API.
return nil, err
}
logrus.Infof("Successfully fetched tags for %s using Web API fallback.", c.Host)
return fallbackTags, nil
}
return tags, nil
}
// getTagsFromWebAPI fetches tags using a Gitea-like Web API endpoint.
func (c *CommonRegistryClient) getTagsFromWebAPI(image string) ([]string, error) {
url := fmt.Sprintf("https://%s/%s/tags/list", c.Host, image)
req, err := http.NewRequest("GET", url, nil)
if err != nil {
return nil, err
}
resp, err := c.Client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("web api request for %s failed with status %d: %s", c.Host, resp.StatusCode, string(body))
}
// Gitea uses "results", some others might use "tags". Let's try to be flexible.
var result struct {
Tags []string `json:"tags"`
Results []string `json:"results"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
if len(result.Results) > 0 {
return result.Results, nil
}
return result.Tags, nil
}
// getTagsFromDockerAPI fetches tags using the standard Docker Registry v2 API.
func (c *CommonRegistryClient) getTagsFromDockerAPI(image string) ([]string, error) {
url := fmt.Sprintf("https://%s/v2/%s/tags/list", c.Host, image)
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
@@ -34,7 +95,7 @@ func (c *CommonRegistryClient) GetTags(image string) ([]string, error) {
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
body, _ := io.ReadAll(resp.Body)
return nil, fmt.Errorf("registry %s request failed: %s", c.Host, string(body))
return nil, fmt.Errorf("registry %s request failed with status %d: %s", c.Host, resp.StatusCode, string(body))
}
var result struct {
Name string `json:"name"`
+34 -22
View File
@@ -14,12 +14,38 @@ import (
"gopkg.in/yaml.v3"
)
// UpdateAndPushService updates the image tag of a specified service in a docker-compose.yaml file,
// commits the change to a Git repository, and pushes the commit to the remote repository.
func UpdateAndPushService(repoPath, filePath, serviceName, newTag, SSHKeyPath, authorName, authorEmail string) error {
// UpdateAndPushService updates a service's image tag in its docker-compose file and pushes the change to the Git repository.
func UpdateAndPushService(repoPath, filePath, serviceName, newTag, sshKeyPath, authorName, authorEmail string) error {
logrus.Infof("Starting update for service '%s' in file '%s' to tag '%s'", serviceName, filePath, newTag)
// Reading and updating the docker-compose.yaml file
// --- CORRECT LOGIC: Open repo and pull first to ensure we are up-to-date ---
repo, err := git.PlainOpen(repoPath)
if err != nil {
return fmt.Errorf("could not open repository: %w", err)
}
worktree, err := repo.Worktree()
if err != nil {
return fmt.Errorf("failed to get worktree: %w", err)
}
auth, err := ssh.NewPublicKeysFromFile("git", sshKeyPath, "")
if err != nil {
return fmt.Errorf("cannot generate public keys from file: %w", err)
}
logrus.Info("Pulling latest changes before modifying files...")
pullOpts := &git.PullOptions{
RemoteName: "origin",
Auth: auth,
}
err = worktree.Pull(pullOpts)
if err != nil && err != git.NoErrAlreadyUpToDate {
return fmt.Errorf("could not pull changes: %w", err)
}
// --- END CORRECT LOGIC ---
// 1. Reading and updating the docker-compose.yaml file
yamlFile, err := os.ReadFile(filePath)
if err != nil {
return fmt.Errorf("could not read file: %w", err)
@@ -30,7 +56,7 @@ func UpdateAndPushService(repoPath, filePath, serviceName, newTag, SSHKeyPath, a
return fmt.Errorf("could not unmarshal YAML: %w", err)
}
// Finding and updating the service image tag
// 2. Finding and updating the service image tag
services, ok := data["services"].(map[string]interface{})
if !ok {
return fmt.Errorf("invalid services structure in YAML")
@@ -55,16 +81,6 @@ func UpdateAndPushService(repoPath, filePath, serviceName, newTag, SSHKeyPath, a
logrus.Infof("docker-compose.yaml file updated successfully.")
// 4. Committing and pushing the changes to the Git repository
repo, err := git.PlainOpen(repoPath)
if err != nil {
return fmt.Errorf("failed to open Git repository: %w", err)
}
worktree, err := repo.Worktree()
if err != nil {
return fmt.Errorf("failed to get worktree: %w", err)
}
relativePath, err := filepath.Rel(repoPath, filePath)
if err != nil {
return fmt.Errorf("could not get relative path: %w", err)
@@ -83,22 +99,18 @@ func UpdateAndPushService(repoPath, filePath, serviceName, newTag, SSHKeyPath, a
},
})
if err != nil {
return fmt.Errorf("could not commit changes: %w", err)
return fmt.Errorf("could not create commit: %w", err)
}
logrus.Infof("New commit created: %s", commit)
publicKeys, err := ssh.NewPublicKeysFromFile("git", SSHKeyPath, "")
if err != nil {
return fmt.Errorf("failed to read SSH key: %w", err)
}
auth := publicKeys
// Push the changes to the remote repository
logrus.Info("Pushing changes to remote repository...")
err = repo.Push(&git.PushOptions{
Auth: auth,
})
if err != nil {
return fmt.Errorf("could not push changes: %w", err)
}
logrus.Infof("Changes pushed to remote repository successfully.")
return nil
}
+6 -1
View File
@@ -29,8 +29,13 @@ func SortAndFilterTags(tags []string, n int, ignoreKeywords []string) []string {
if isWeirdTag(tag) {
continue
}
cleanTag := strings.TrimPrefix(tag, "v")
// --- MODIFICATION: Try to normalize tag before parsing as SemVer ---
normalizedTag := TryNormalizeTagForSemVer(tag)
cleanTag := strings.TrimPrefix(normalizedTag, "v")
v, err := semver.NewVersion(cleanTag)
// --- END MODIFICATION ---
if err == nil {
uniqueVersions[v.String()] = v
tagMap[v.String()] = tag // сохраняем оригинальный тег!
+29
View File
@@ -1,7 +1,9 @@
package versionutils
import (
"fmt"
"sort"
"strconv"
"strings"
"github.com/Masterminds/semver/v3"
@@ -77,3 +79,30 @@ func GetLatestTags(tags []string, count int) []string {
func NormalizeTag(tag string) string {
return strings.TrimPrefix(tag, "v")
}
// TryNormalizeTagForSemVer attempts to normalize a non-standard tag into a SemVer-compatible format.
// For example, "1.2.3.4" might become "1.2.3-rev.4".
// This is a heuristic approach for specific non-SemVer patterns.
func TryNormalizeTagForSemVer(tag string) string {
// If it already parses as SemVer, no need to normalize
if _, err := semver.NewVersion(strings.TrimPrefix(tag, "v")); err == nil {
return tag
}
// Pattern: X.Y.Z.A (e.g., 1.2.3.4)
parts := strings.Split(tag, ".")
if len(parts) == 4 {
// Try to form X.Y.Z-rev.A
if _, err := strconv.Atoi(parts[3]); err == nil { // Check if the last part is a number
normalized := fmt.Sprintf("%s.%s.%s-rev.%s", parts[0], parts[1], parts[2], parts[3])
if _, err := semver.NewVersion(normalized); err == nil {
logrus.Debugf("Normalized tag '%s' to '%s' for SemVer parsing.", tag, normalized)
return normalized
}
}
}
// Add other normalization patterns here if needed
return tag // Return original tag if no normalization helps
}
+105 -39
View File
@@ -36,11 +36,6 @@ type UpdateInfo struct {
NewTag string
}
var commonClients = map[string]*registry.CommonRegistryClient{
"docker.gitea.com": registry.NewCommonRegistryClient("docker.gitea.com"),
"quay.io": registry.NewCommonRegistryClient("quay.io"),
}
// updateCache stores information about found updates.
var updateCache = make(map[string]UpdateInfo)
var cacheMutex sync.Mutex
@@ -90,11 +85,10 @@ func handleCallbackQuery(bot *tgbotapi.BotAPI, callback *tgbotapi.CallbackQuery,
logrus.Infof("Received update request for image: %s, tag: %s", fullImageName, newTag)
// --- NEW: Remove from sentNotifications when acted upon ---
// Remove from sentNotifications when acted upon ---
sentNotificationsMutex.Lock()
delete(sentNotifications, fullImageName+":"+newTag)
sentNotificationsMutex.Unlock()
// --- End NEW ---
// Looking for servies to update
repoPath := appConfig.Git.LocalRepoPath
@@ -155,11 +149,10 @@ func handleCallbackQuery(bot *tgbotapi.BotAPI, callback *tgbotapi.CallbackQuery,
logrus.Infof("Received ignore request for image: %s, version: %s", fullImageName, tagToIgnore)
// --- NEW: Remove from sentNotifications when acted upon ---
// Remove from sentNotifications when acted upon ---
sentNotificationsMutex.Lock()
delete(sentNotifications, fullImageName+":"+tagToIgnore)
sentNotificationsMutex.Unlock()
// --- End NEW ---
err := sl.Add(fullImageName, tagToIgnore)
if err != nil {
@@ -168,6 +161,25 @@ func handleCallbackQuery(bot *tgbotapi.BotAPI, callback *tgbotapi.CallbackQuery,
logrus.Infof("Update for %s with tag %s has been added to the skip list.", fullImageName, tagToIgnore)
}
}
// Clear ALL related notifications/warnings for this image ---
var fullImageName string
if strings.HasPrefix(callbackData, "update_to:") {
fullImageName, _, _ = decodeCallbackData(callbackData, "update_to")
} else if strings.HasPrefix(callbackData, "ignore:") {
fullImageName, _, _ = decodeCallbackData(callbackData, "ignore")
}
if fullImageName != "" {
sentNotificationsMutex.Lock()
for key := range sentNotifications {
if strings.HasPrefix(key, fullImageName+":") {
delete(sentNotifications, key)
}
}
sentNotificationsMutex.Unlock()
logrus.Debugf("Cleared all pending notifications/warnings for %s after user action.", fullImageName)
}
// Send confirmation to user and delete original message
deleteConfig := tgbotapi.NewDeleteMessage(callback.Message.Chat.ID, callback.Message.MessageID)
_, err := bot.Request(deleteConfig)
@@ -265,23 +277,23 @@ func main() {
// Initizle registry clients
dockerHubClient := registry.NewDockerHubClient(appConfig.DockerUser, appConfig.DockerPass)
ghcrClient := registry.NewGhcrClient()
commonClients := make(map[string]*registry.CommonRegistryClient) // Created on-the-fly
runCheck(ctx, appConfig, bot, sl, dockerHubClient, ghcrClient)
runCheck(ctx, appConfig, bot, sl, dockerHubClient, ghcrClient, commonClients)
// Main loop: periodic checks
for {
select {
case <-ctx.Done():
logrus.Infof("Shutting down...")
return
case <-ticker.C:
runCheck(ctx, appConfig, bot, sl, dockerHubClient, ghcrClient)
runCheck(ctx, appConfig, bot, sl, dockerHubClient, ghcrClient, commonClients)
}
}
}
// runCheck checks all container images for updates and sends notifications if updates are available.
func runCheck(ctx context.Context, appConfig *config.AppConfig, bot *tgbotapi.BotAPI, sl *skiplist.SkipList, dockerHubClient *registry.DockerHubClient, ghcrClient *registry.GhcrClient) {
func runCheck(ctx context.Context, appConfig *config.AppConfig, bot *tgbotapi.BotAPI, sl *skiplist.SkipList, dockerHubClient *registry.DockerHubClient, ghcrClient *registry.GhcrClient, commonClients map[string]*registry.CommonRegistryClient) {
logrus.Infof("Starting image update check...")
repoPath := appConfig.Git.LocalRepoPath
@@ -303,12 +315,12 @@ func runCheck(ctx context.Context, appConfig *config.AppConfig, bot *tgbotapi.Bo
logrus.Infof("Repository cloned or pulled successfully.")
// Get list of relevant files in the repository
relevantFiles, err := checker.FindRelevantFiles(repoPath, appConfig.Checker.WatchFolder) // <-- Передаем watchFolder
relevantFiles, err := checker.FindRelevantFiles(repoPath, appConfig.Checker.WatchFolder)
if err != nil {
logrus.Errorf("Error finding relevant files: %v", err)
return
}
logrus.Infof("Found %d relevant files in watch folder '%s'.", len(relevantFiles), appConfig.Checker.WatchFolder) // <-- Обновляем лог
logrus.Infof("Found %d relevant files in watch folder '%s'.", len(relevantFiles), appConfig.Checker.WatchFolder) // Debug log
// Parse relevant files to extract services and their images
services, err := checker.ParseRelevantFiles(relevantFiles)
@@ -332,40 +344,62 @@ func runCheck(ctx context.Context, appConfig *config.AppConfig, bot *tgbotapi.Bo
case <-ctx.Done():
return
default:
logrus.Infof("Parsing image: Registry=%s, Image=%s, Tag=%s", services[0].ImageInfo.Registry, services[0].ImageInfo.Image, services[0].ImageInfo.Tag)
currentImageTag := services[0].ImageInfo.Tag
logrus.Infof("Parsing image: Registry=%s, Image=%s, Tag=%s", services[0].ImageInfo.Registry, services[0].ImageInfo.Image, currentImageTag)
if sl.IsSkipped(fullImageName, services[0].ImageInfo.Tag) {
logrus.Infof("Skipping update for %s. It's in the skip list.", fullImageName)
continue
}
if sl.IsImageIgnored(fullImageName) {
logrus.Infof("Image %s is globally ignored (skipped.yaml), skipping.", fullImageName)
// Clear notifications if image is skipped/ignored ---
if sl.IsSkipped(fullImageName, currentImageTag) || sl.IsImageIgnored(fullImageName) {
if sl.IsSkipped(fullImageName, currentImageTag) {
logrus.Infof("Skipping update for %s. It's in the skip list.", fullImageName)
} else {
logrus.Infof("Image %s is globally ignored (skipped.yaml), skipping.", fullImageName)
}
sentNotificationsMutex.Lock()
for key := range sentNotifications {
if strings.HasPrefix(key, fullImageName+":") {
delete(sentNotifications, key)
}
}
sentNotificationsMutex.Unlock()
continue
}
var tags []string
var err error
registryName := services[0].ImageInfo.Registry
switch services[0].ImageInfo.Registry {
switch registryName {
case "docker.io":
tags, err = dockerHubClient.GetTags(services[0].ImageInfo.Image)
case "ghcr.io":
tags, err = ghcrClient.GetTags(services[0].ImageInfo.Image)
default:
client, ok := commonClients[services[0].ImageInfo.Registry]
if ok {
tags, err = client.GetTags(services[0].ImageInfo.Image)
} else {
logrus.Infof("Skipping unsupported registry: %s", services[0].ImageInfo.Registry)
continue
// Create client on-the-fly ---
client, ok := commonClients[registryName]
if !ok {
logrus.Debugf("Creating new common client for registry: %s", registryName)
client = registry.NewCommonRegistryClient(registryName)
commonClients[registryName] = client
}
tags, err = client.GetTags(services[0].ImageInfo.Image)
}
logrus.Debugf("Avaliable tags for %s: %v", fullImageName, tags)
if err != nil {
logrus.Errorf("Error getting tags for %s: %v", fullImageName, err)
// --- MODIFICATION: Suppress duplicate error messages ---
notificationKey := fullImageName + ":error_getting_tags"
sentNotificationsMutex.Lock()
if sentNotifications[notificationKey] {
sentNotificationsMutex.Unlock()
continue
}
sentNotifications[notificationKey] = true
sentNotificationsMutex.Unlock()
chatID, _ := strconv.ParseInt(appConfig.TelegramChatID, 10, 64)
notifier.SendTelegramMessage(bot, chatID, fmt.Sprintf("❌ Error getting tags for %s: %v", fullImageName, err))
// --- END MODIFICATION ---
continue
}
@@ -373,7 +407,7 @@ func runCheck(ctx context.Context, appConfig *config.AppConfig, bot *tgbotapi.Bo
latestTags := versionutils.SortAndFilterTags(tags, appConfig.Checker.ProposedTagsCount, appConfig.Checker.IgnoreKeywords)
if len(latestTags) == 0 {
logrus.Warnf("⚠️ Can't check updates for %s. Current tag: %s, it is not use SemVer template. Available tags: %v",
fullImageName, services[0].ImageInfo.Tag, tags)
fullImageName, currentImageTag, tags)
currentDigest, err1 := getDigestForImage(
services[0].ImageInfo.Registry,
@@ -381,6 +415,7 @@ func runCheck(ctx context.Context, appConfig *config.AppConfig, bot *tgbotapi.Bo
services[0].ImageInfo.Tag,
dockerHubClient,
ghcrClient,
commonClients, // Pass it here
)
latestDigest, err2 := getDigestForImage(
services[0].ImageInfo.Registry,
@@ -388,6 +423,7 @@ func runCheck(ctx context.Context, appConfig *config.AppConfig, bot *tgbotapi.Bo
"latest",
dockerHubClient,
ghcrClient,
commonClients, // And here
)
logrus.Debugf("Digest fallback for %s: currentDigest=%s (err=%v), latestDigest=%s (err=%v)",
@@ -395,9 +431,21 @@ func runCheck(ctx context.Context, appConfig *config.AppConfig, bot *tgbotapi.Bo
if err1 == nil && err2 == nil && currentDigest != "" && latestDigest != "" {
if currentDigest != latestDigest {
logrus.Warnf("🚨 Digest update for %s. Current tag: %s, 'latest' tag digest differs!", fullImageName, services[0].ImageInfo.Tag)
logrus.Warnf("🚨 Digest update for %s. Current tag: %s, 'latest' tag digest differs!", fullImageName, currentImageTag)
// --- MODIFICATION: Suppress duplicate digest update messages ---
notificationKey := fullImageName + ":digest_update"
sentNotificationsMutex.Lock()
if sentNotifications[notificationKey] {
sentNotificationsMutex.Unlock()
continue
}
sentNotifications[notificationKey] = true
sentNotificationsMutex.Unlock()
chatID, _ := strconv.ParseInt(appConfig.TelegramChatID, 10, 64)
notifier.SendTelegramMessage(bot, chatID, fmt.Sprintf("🚨 Digest update for %s. Current tag: %s, 'latest' tag digest differs!", fullImageName, currentImageTag))
// --- END MODIFICATION ---
} else {
logrus.Infof("%s is actual by digest. Current tag: %s", fullImageName, services[0].ImageInfo.Tag)
logrus.Infof("%s is actual by digest. Current tag: %s", fullImageName, currentImageTag)
}
} else {
logrus.Errorf("Can't check updates for %s. No SemVer tags and no digest fallback.", fullImageName)
@@ -407,15 +455,28 @@ func runCheck(ctx context.Context, appConfig *config.AppConfig, bot *tgbotapi.Bo
// Check the latest tag is newer than the current tag
latestAvailable := latestTags[0]
currentVersion, err := semver.NewVersion(services[0].ImageInfo.Tag)
// --- MODIFICATION: Try to normalize current tag before parsing ---
normalizedCurrentTag := versionutils.TryNormalizeTagForSemVer(currentImageTag)
currentVersion, err := semver.NewVersion(strings.TrimPrefix(normalizedCurrentTag, "v"))
// --- END MODIFICATION ---
if err != nil {
logrus.Warnf("⚠️ Can't check updates for %s. Current tag: %s, it is not use SemVer template. Available tags: %v",
fullImageName, services[0].ImageInfo.Tag, tags)
fullImageName, currentImageTag, tags)
// --- MODIFICATION: Suppress duplicate warning messages ---
notificationKey := fullImageName + ":" + currentImageTag + ":non_semver_warning"
sentNotificationsMutex.Lock()
if sentNotifications[notificationKey] {
sentNotificationsMutex.Unlock()
continue
}
sentNotifications[notificationKey] = true
sentNotificationsMutex.Unlock()
chatID, _ := strconv.ParseInt(appConfig.TelegramChatID, 10, 64)
notifier.SendTelegramMessage(bot, chatID, fmt.Sprintf("⚠️ Can't check updates for %s. Current tag: %s, it is not use SemVer template.", fullImageName, services[0].ImageInfo.Tag))
notifier.SendTelegramMessage(bot, chatID, fmt.Sprintf("⚠️ Can't check updates for %s. Current tag: %s, it is not use SemVer template.", fullImageName, currentImageTag))
// --- END MODIFICATION ---
continue
}
latestVersion, _ := semver.NewVersion(latestAvailable)
latestVersion, _ := semver.NewVersion(strings.TrimPrefix(latestAvailable, "v"))
if latestVersion.GreaterThan(currentVersion) {
for _, tag := range latestTags {
@@ -473,8 +534,13 @@ func runCheck(ctx context.Context, appConfig *config.AppConfig, bot *tgbotapi.Bo
}
notifier.SendInteractiveMessage(bot, appConfig.TelegramChatID, message, []string{tag}, callbackData)
}
// --- MODIFICATION: Clear warnings if an update is found ---
sentNotificationsMutex.Lock()
delete(sentNotifications, fullImageName+":"+currentImageTag+":non_semver_warning")
sentNotificationsMutex.Unlock()
// --- END MODIFICATION ---
} else {
logrus.Infof("✅ %s is actual. Current tag: %s", fullImageName, services[0].ImageInfo.Tag)
logrus.Infof("✅ %s is actual. Current tag: %s", fullImageName, currentImageTag)
// --- NEW LOGIC: Clear any pending notifications for this image if it's now actual ---
sentNotificationsMutex.Lock()
for key := range sentNotifications {
@@ -505,7 +571,7 @@ func encodeCallbackData(prefix, fullImageName, tag string) string {
}
// Get image digest for given registry, image and tag
func getDigestForImage(registryName, image, tag string, dockerHubClient *registry.DockerHubClient, ghcrClient *registry.GhcrClient) (string, error) {
func getDigestForImage(registryName, image, tag string, dockerHubClient *registry.DockerHubClient, ghcrClient *registry.GhcrClient, commonClients map[string]*registry.CommonRegistryClient) (string, error) {
switch registryName {
case "docker.io":
return dockerHubClient.GetDigest(image, tag)
Submodule repos/myrepo deleted from 27608766af