feat(proxy): enhance Traefik version notifications to show patch and minor upgrades

- Store both patch update and newer minor version information simultaneously
- Display patch update availability alongside minor version upgrades in notifications
- Add newer_branch_target and newer_branch_latest fields to traefik_outdated_info
- Update all notification channels (Discord, Telegram, Slack, Pushover, Email, Webhook)
- Show minor version in format (e.g., v3.6) for upgrade targets instead of patch version
- Enhance UI callouts with clearer messaging about available upgrades
- Remove verbose logging in favor of cleaner code structure
- Handle edge case where SSH command returns empty response

🤖 Generated with Claude Code

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Andras Bacsai
2025-11-18 14:53:49 +01:00
co-authored by Claude
parent c77eaddede
commit 6dbe58f22b
15 changed files with 618 additions and 241 deletions
+78 -71
View File
@@ -8,7 +8,6 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class CheckTraefikVersionForServerJob implements ShouldQueue
{
@@ -33,80 +32,78 @@ class CheckTraefikVersionForServerJob implements ShouldQueue
*/
public function handle(): void
{
try {
Log::debug("CheckTraefikVersionForServerJob: Processing server '{$this->server->name}' (ID: {$this->server->id})");
// Detect current version (makes SSH call)
$currentVersion = getTraefikVersionFromDockerCompose($this->server);
// Detect current version (makes SSH call)
$currentVersion = getTraefikVersionFromDockerCompose($this->server);
// Update detected version in database
$this->server->update(['detected_traefik_version' => $currentVersion]);
Log::info("CheckTraefikVersionForServerJob: Server '{$this->server->name}' - Detected version: ".($currentVersion ?? 'unable to detect'));
if (! $currentVersion) {
return;
}
// Update detected version in database
$this->server->update(['detected_traefik_version' => $currentVersion]);
// Check if image tag is 'latest' by inspecting the image (makes SSH call)
$imageTag = instant_remote_process([
"docker inspect coolify-proxy --format '{{.Config.Image}}' 2>/dev/null",
], $this->server, false);
if (! $currentVersion) {
Log::warning("CheckTraefikVersionForServerJob: Server '{$this->server->name}' - Unable to detect version, skipping");
// Handle empty/null response from SSH command
if (empty(trim($imageTag))) {
return;
}
return;
}
if (str_contains(strtolower(trim($imageTag)), ':latest')) {
return;
}
// Check if image tag is 'latest' by inspecting the image (makes SSH call)
$imageTag = instant_remote_process([
"docker inspect coolify-proxy --format '{{.Config.Image}}' 2>/dev/null",
], $this->server, false);
// Parse current version to extract major.minor.patch
$current = ltrim($currentVersion, 'v');
if (! preg_match('/^(\d+\.\d+)\.(\d+)$/', $current, $matches)) {
return;
}
if (str_contains(strtolower(trim($imageTag)), ':latest')) {
Log::info("CheckTraefikVersionForServerJob: Server '{$this->server->name}' uses 'latest' tag, skipping notification (UI warning only)");
$currentBranch = $matches[1]; // e.g., "3.6"
$currentPatch = $matches[2]; // e.g., "0"
return;
}
// Find the latest version for this branch
$latestForBranch = $this->traefikVersions["v{$currentBranch}"] ?? null;
// Parse current version to extract major.minor.patch
$current = ltrim($currentVersion, 'v');
if (! preg_match('/^(\d+\.\d+)\.(\d+)$/', $current, $matches)) {
Log::warning("CheckTraefikVersionForServerJob: Server '{$this->server->name}' - Invalid version format '{$current}', skipping");
if (! $latestForBranch) {
// User is on a branch we don't track - check if newer branches exist
$newerBranchInfo = $this->getNewerBranchInfo($current, $currentBranch);
return;
}
$currentBranch = $matches[1]; // e.g., "3.6"
$currentPatch = $matches[2]; // e.g., "0"
Log::debug("CheckTraefikVersionForServerJob: Server '{$this->server->name}' - Parsed branch: {$currentBranch}, patch: {$currentPatch}");
// Find the latest version for this branch
$latestForBranch = $this->traefikVersions["v{$currentBranch}"] ?? null;
if (! $latestForBranch) {
// User is on a branch we don't track - check if newer branches exist
$this->checkForNewerBranch($current, $currentBranch);
return;
}
// Compare patch version within the same branch
$latest = ltrim($latestForBranch, 'v');
if (version_compare($current, $latest, '<')) {
Log::info("CheckTraefikVersionForServerJob: Server '{$this->server->name}' is outdated - current: {$current}, latest for branch: {$latest}");
$this->storeOutdatedInfo($current, $latest, 'patch_update');
if ($newerBranchInfo) {
$this->storeOutdatedInfo($current, $newerBranchInfo['latest'], 'minor_upgrade', $newerBranchInfo['target']);
} else {
// Check if newer branches exist
$this->checkForNewerBranch($current, $currentBranch);
// No newer branch found, clear outdated info
$this->server->update(['traefik_outdated_info' => null]);
}
} catch (\Throwable $e) {
Log::error("CheckTraefikVersionForServerJob: Error checking server '{$this->server->name}': ".$e->getMessage(), [
'server_id' => $this->server->id,
'exception' => $e,
]);
throw $e;
return;
}
// Compare patch version within the same branch
$latest = ltrim($latestForBranch, 'v');
// Always check for newer branches first
$newerBranchInfo = $this->getNewerBranchInfo($current, $currentBranch);
if (version_compare($current, $latest, '<')) {
// Patch update available
$this->storeOutdatedInfo($current, $latest, 'patch_update', null, $newerBranchInfo);
} elseif ($newerBranchInfo) {
// Only newer branch available (no patch update)
$this->storeOutdatedInfo($current, $newerBranchInfo['latest'], 'minor_upgrade', $newerBranchInfo['target']);
} else {
// Fully up to date
$this->server->update(['traefik_outdated_info' => null]);
}
}
/**
* Check if there are newer branches available.
* Get information about newer branches if available.
*/
private function checkForNewerBranch(string $current, string $currentBranch): void
private function getNewerBranchInfo(string $current, string $currentBranch): ?array
{
$newestBranch = null;
$newestVersion = null;
@@ -122,28 +119,38 @@ class CheckTraefikVersionForServerJob implements ShouldQueue
}
if ($newestVersion) {
Log::info("CheckTraefikVersionForServerJob: Server '{$this->server->name}' - newer branch {$newestBranch} available ({$newestVersion})");
$this->storeOutdatedInfo($current, $newestVersion, 'minor_upgrade');
} else {
Log::info("CheckTraefikVersionForServerJob: Server '{$this->server->name}' is fully up to date - version: {$current}");
// Clear any outdated info using schemaless attributes
$this->server->extra_attributes->forget('traefik_outdated_info');
$this->server->save();
return [
'target' => "v{$newestBranch}",
'latest' => ltrim($newestVersion, 'v'),
];
}
return null;
}
/**
* Store outdated information using schemaless attributes.
* Store outdated information in database.
*/
private function storeOutdatedInfo(string $current, string $latest, string $type): void
private function storeOutdatedInfo(string $current, string $latest, string $type, ?string $upgradeTarget = null, ?array $newerBranchInfo = null): void
{
// Store in schemaless attributes for persistence
$this->server->extra_attributes->set('traefik_outdated_info', [
$outdatedInfo = [
'current' => $current,
'latest' => $latest,
'type' => $type,
'checked_at' => now()->toIso8601String(),
]);
$this->server->save();
];
// For minor upgrades, add the upgrade_target field (e.g., "v3.6")
if ($type === 'minor_upgrade' && $upgradeTarget) {
$outdatedInfo['upgrade_target'] = $upgradeTarget;
}
// If there's a newer branch available (even for patch updates), include that info
if ($newerBranchInfo) {
$outdatedInfo['newer_branch_target'] = $newerBranchInfo['target'];
$outdatedInfo['newer_branch_latest'] = $newerBranchInfo['latest'];
}
$this->server->update(['traefik_outdated_info' => $outdatedInfo]);
}
}
+79 -64
View File
@@ -10,7 +10,6 @@ use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\File;
use Illuminate\Support\Facades\Log;
class CheckTraefikVersionJob implements ShouldQueue
{
@@ -20,69 +19,85 @@ class CheckTraefikVersionJob implements ShouldQueue
public function handle(): void
{
try {
Log::info('CheckTraefikVersionJob: Starting Traefik version check with parallel processing');
// Load versions from versions.json
$versionsPath = base_path('versions.json');
if (! File::exists($versionsPath)) {
Log::warning('CheckTraefikVersionJob: versions.json not found, skipping check');
return;
}
$allVersions = json_decode(File::get($versionsPath), true);
$traefikVersions = data_get($allVersions, 'traefik');
if (empty($traefikVersions) || ! is_array($traefikVersions)) {
Log::warning('CheckTraefikVersionJob: Traefik versions not found or invalid in versions.json');
return;
}
$branches = array_keys($traefikVersions);
Log::info('CheckTraefikVersionJob: Loaded Traefik version branches', ['branches' => $branches]);
// Query all servers with Traefik proxy that are reachable
$servers = Server::whereNotNull('proxy')
->whereProxyType(ProxyTypes::TRAEFIK->value)
->whereRelation('settings', 'is_reachable', true)
->whereRelation('settings', 'is_usable', true)
->get();
$serverCount = $servers->count();
Log::info("CheckTraefikVersionJob: Found {$serverCount} server(s) with Traefik proxy");
if ($serverCount === 0) {
Log::info('CheckTraefikVersionJob: No Traefik servers found, job completed');
return;
}
// Dispatch individual server check jobs in parallel
Log::info('CheckTraefikVersionJob: Dispatching parallel server check jobs');
foreach ($servers as $server) {
CheckTraefikVersionForServerJob::dispatch($server, $traefikVersions);
}
Log::info("CheckTraefikVersionJob: Dispatched {$serverCount} parallel server check jobs");
// Dispatch notification job with delay to allow server checks to complete
// For 1000 servers with 60s timeout each, we need at least 60s delay
// But jobs run in parallel via queue workers, so we only need enough time
// for the slowest server to complete
$delaySeconds = min(300, max(60, (int) ($serverCount / 10))); // 60s minimum, 300s maximum, 0.1s per server
NotifyOutdatedTraefikServersJob::dispatch()->delay(now()->addSeconds($delaySeconds));
Log::info("CheckTraefikVersionJob: Scheduled notification job with {$delaySeconds}s delay");
Log::info('CheckTraefikVersionJob: Job completed successfully - parallel processing initiated');
} catch (\Throwable $e) {
Log::error('CheckTraefikVersionJob: Error checking Traefik versions: '.$e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
]);
throw $e;
// Load versions from versions.json
$versionsPath = base_path('versions.json');
if (! File::exists($versionsPath)) {
return;
}
$allVersions = json_decode(File::get($versionsPath), true);
$traefikVersions = data_get($allVersions, 'traefik');
if (empty($traefikVersions) || ! is_array($traefikVersions)) {
return;
}
// Query all servers with Traefik proxy that are reachable
$servers = Server::whereNotNull('proxy')
->whereProxyType(ProxyTypes::TRAEFIK->value)
->whereRelation('settings', 'is_reachable', true)
->whereRelation('settings', 'is_usable', true)
->get();
$serverCount = $servers->count();
if ($serverCount === 0) {
return;
}
// Dispatch individual server check jobs in parallel
foreach ($servers as $server) {
CheckTraefikVersionForServerJob::dispatch($server, $traefikVersions);
}
// Dispatch notification job with delay to allow server checks to complete
// Jobs run in parallel via queue workers, but we need to account for:
// - Queue worker capacity (workers process jobs concurrently)
// - Job timeout (60s per server check)
// - Retry attempts (3 retries with exponential backoff)
// - Network latency and SSH connection overhead
//
// Calculation strategy:
// - Assume ~10-20 workers processing the high queue
// - Each server check takes up to 60s (timeout)
// - With retries, worst case is ~180s per job
// - More conservative: 0.2s per server (instead of 0.1s)
// - Higher minimum: 120s (instead of 60s) to account for retries
// - Keep 300s maximum to avoid excessive delays
$delaySeconds = $this->calculateNotificationDelay($serverCount);
if (isDev()) {
$delaySeconds = 1;
}
NotifyOutdatedTraefikServersJob::dispatch()->delay(now()->addSeconds($delaySeconds));
}
/**
* Calculate the delay in seconds before sending notifications.
*
* This method calculates an appropriate delay to allow all parallel
* CheckTraefikVersionForServerJob instances to complete before sending
* notifications to teams.
*
* The calculation considers:
* - Server count (more servers = longer delay)
* - Queue worker capacity
* - Job timeout (60s) and retry attempts (3x)
* - Network latency and SSH connection overhead
*
* @param int $serverCount Number of servers being checked
* @return int Delay in seconds
*/
protected function calculateNotificationDelay(int $serverCount): int
{
$minDelay = config('constants.server_checks.notification_delay_min');
$maxDelay = config('constants.server_checks.notification_delay_max');
$scalingFactor = config('constants.server_checks.notification_delay_scaling');
// Calculate delay based on server count
// More conservative approach: 0.2s per server
$calculatedDelay = (int) ($serverCount * $scalingFactor);
// Apply min/max boundaries
return min($maxDelay, max($minDelay, $calculatedDelay));
}
}
+26 -56
View File
@@ -11,7 +11,6 @@ use Illuminate\Contracts\Queue\ShouldQueue;
use Illuminate\Foundation\Bus\Dispatchable;
use Illuminate\Queue\InteractsWithQueue;
use Illuminate\Queue\SerializesModels;
use Illuminate\Support\Facades\Log;
class NotifyOutdatedTraefikServersJob implements ShouldQueue
{
@@ -32,67 +31,38 @@ class NotifyOutdatedTraefikServersJob implements ShouldQueue
*/
public function handle(): void
{
try {
Log::info('NotifyOutdatedTraefikServersJob: Starting notification aggregation');
// Query servers that have outdated info stored
$servers = Server::whereNotNull('proxy')
->whereProxyType(ProxyTypes::TRAEFIK->value)
->whereRelation('settings', 'is_reachable', true)
->whereRelation('settings', 'is_usable', true)
->get();
// Query servers that have outdated info stored
$servers = Server::whereNotNull('proxy')
->whereProxyType(ProxyTypes::TRAEFIK->value)
->whereRelation('settings', 'is_reachable', true)
->whereRelation('settings', 'is_usable', true)
->get();
$outdatedServers = collect();
$outdatedServers = collect();
foreach ($servers as $server) {
if ($server->traefik_outdated_info) {
// Attach the outdated info as a dynamic property for the notification
$server->outdatedInfo = $server->traefik_outdated_info;
$outdatedServers->push($server);
}
}
foreach ($servers as $server) {
$outdatedInfo = $server->extra_attributes->get('traefik_outdated_info');
if ($outdatedServers->isEmpty()) {
return;
}
if ($outdatedInfo) {
// Attach the outdated info as a dynamic property for the notification
$server->outdatedInfo = $outdatedInfo;
$outdatedServers->push($server);
}
// Group by team and send notifications
$serversByTeam = $outdatedServers->groupBy('team_id');
foreach ($serversByTeam as $teamId => $teamServers) {
$team = Team::find($teamId);
if (! $team) {
continue;
}
$outdatedCount = $outdatedServers->count();
Log::info("NotifyOutdatedTraefikServersJob: Found {$outdatedCount} outdated server(s)");
if ($outdatedCount === 0) {
Log::info('NotifyOutdatedTraefikServersJob: No outdated servers found, no notifications to send');
return;
}
// Group by team and send notifications
$serversByTeam = $outdatedServers->groupBy('team_id');
$teamCount = $serversByTeam->count();
Log::info("NotifyOutdatedTraefikServersJob: Grouped outdated servers into {$teamCount} team(s)");
foreach ($serversByTeam as $teamId => $teamServers) {
$team = Team::find($teamId);
if (! $team) {
Log::warning("NotifyOutdatedTraefikServersJob: Team ID {$teamId} not found, skipping");
continue;
}
$serverNames = $teamServers->pluck('name')->join(', ');
Log::info("NotifyOutdatedTraefikServersJob: Sending notification to team '{$team->name}' for {$teamServers->count()} server(s): {$serverNames}");
// Send one notification per team with all outdated servers
$team->notify(new TraefikVersionOutdated($teamServers));
Log::info("NotifyOutdatedTraefikServersJob: Notification sent to team '{$team->name}'");
}
Log::info('NotifyOutdatedTraefikServersJob: Job completed successfully');
} catch (\Throwable $e) {
Log::error('NotifyOutdatedTraefikServersJob: Error sending notifications: '.$e->getMessage(), [
'exception' => $e,
'trace' => $e->getTraceAsString(),
]);
throw $e;
// Send one notification per team with all outdated servers
$team->notify(new TraefikVersionOutdated($teamServers));
}
}
}
+15 -5
View File
@@ -230,6 +230,17 @@ class Proxy extends Component
return null;
}
// Check if we have outdated info stored
$outdatedInfo = $this->server->traefik_outdated_info;
if ($outdatedInfo && isset($outdatedInfo['type']) && $outdatedInfo['type'] === 'minor_upgrade') {
// Use the upgrade_target field if available (e.g., "v3.6")
if (isset($outdatedInfo['upgrade_target'])) {
return str_starts_with($outdatedInfo['upgrade_target'], 'v')
? $outdatedInfo['upgrade_target']
: "v{$outdatedInfo['upgrade_target']}";
}
}
$versionsPath = base_path('versions.json');
if (! File::exists($versionsPath)) {
return null;
@@ -251,18 +262,17 @@ class Proxy extends Component
$currentBranch = $matches[1];
// Find the newest branch that's greater than current
$newestVersion = null;
$newestBranch = null;
foreach ($traefikVersions as $branch => $version) {
$branchNum = ltrim($branch, 'v');
if (version_compare($branchNum, $currentBranch, '>')) {
$cleanVersion = ltrim($version, 'v');
if (! $newestVersion || version_compare($cleanVersion, $newestVersion, '>')) {
$newestVersion = $cleanVersion;
if (! $newestBranch || version_compare($branchNum, $newestBranch, '>')) {
$newestBranch = $branchNum;
}
}
}
return $newestVersion ? "v{$newestVersion}" : null;
return $newestBranch ? "v{$newestBranch}" : null;
} catch (\Throwable $e) {
return null;
}
+2
View File
@@ -142,6 +142,7 @@ class Server extends BaseModel
protected $casts = [
'proxy' => SchemalessAttributes::class,
'traefik_outdated_info' => 'array',
'logdrain_axiom_api_key' => 'encrypted',
'logdrain_newrelic_license_key' => 'encrypted',
'delete_unused_volumes' => 'boolean',
@@ -168,6 +169,7 @@ class Server extends BaseModel
'hetzner_server_status',
'is_validating',
'detected_traefik_version',
'traefik_outdated_info',
];
protected $guarded = [];
@@ -27,6 +27,17 @@ class TraefikVersionOutdated extends CustomEmailNotification
return str_starts_with($version, 'v') ? $version : "v{$version}";
}
private function getUpgradeTarget(array $info): string
{
// For minor upgrades, use the upgrade_target field (e.g., "v3.6")
if (($info['type'] ?? 'patch_update') === 'minor_upgrade' && isset($info['upgrade_target'])) {
return $this->formatVersion($info['upgrade_target']);
}
// For patch updates, show the full version
return $this->formatVersion($info['latest'] ?? 'unknown');
}
public function toMail($notifiable = null): MailMessage
{
$mail = new MailMessage;
@@ -44,24 +55,37 @@ class TraefikVersionOutdated extends CustomEmailNotification
public function toDiscord(): DiscordMessage
{
$count = $this->servers->count();
$hasUpgrades = $this->servers->contains(fn ($s) => ($s->outdatedInfo['type'] ?? 'patch_update') === 'minor_upgrade');
$hasUpgrades = $this->servers->contains(fn ($s) => ($s->outdatedInfo['type'] ?? 'patch_update') === 'minor_upgrade' ||
isset($s->outdatedInfo['newer_branch_target'])
);
$description = "**{$count} server(s)** running outdated Traefik proxy. Update recommended for security and features.\n\n";
$description .= "*Based on actual running container version*\n\n";
$description .= "**Affected servers:**\n";
foreach ($this->servers as $server) {
$info = $server->outdatedInfo ?? [];
$current = $this->formatVersion($info['current'] ?? 'unknown');
$latest = $this->formatVersion($info['latest'] ?? 'unknown');
$type = ($info['type'] ?? 'patch_update') === 'patch_update' ? '(patch)' : '(upgrade)';
$description .= "• {$server->name}: {$current} → {$latest} {$type}\n";
$upgradeTarget = $this->getUpgradeTarget($info);
$isPatch = ($info['type'] ?? 'patch_update') === 'patch_update';
$hasNewerBranch = isset($info['newer_branch_target']);
if ($isPatch && $hasNewerBranch) {
$newerBranchTarget = $info['newer_branch_target'];
$newerBranchLatest = $this->formatVersion($info['newer_branch_latest']);
$description .= "• {$server->name}: {$current} → {$upgradeTarget} (patch update available)\n";
$description .= " ↳ Also available: {$newerBranchTarget} (latest patch: {$newerBranchLatest}) - new minor version\n";
} elseif ($isPatch) {
$description .= "• {$server->name}: {$current} → {$upgradeTarget} (patch update available)\n";
} else {
$description .= "• {$server->name}: {$current} (latest patch: {$latest}) → {$upgradeTarget} (new minor version available)\n";
}
}
$description .= "\n⚠️ It is recommended to test before switching the production version.";
if ($hasUpgrades) {
$description .= "\n\n📖 **For major/minor upgrades**: Read the Traefik changelog before upgrading to understand breaking changes.";
$description .= "\n\n📖 **For minor version upgrades**: Read the Traefik changelog before upgrading to understand breaking changes and new features.";
}
return new DiscordMessage(
@@ -74,25 +98,38 @@ class TraefikVersionOutdated extends CustomEmailNotification
public function toTelegram(): array
{
$count = $this->servers->count();
$hasUpgrades = $this->servers->contains(fn ($s) => ($s->outdatedInfo['type'] ?? 'patch_update') === 'minor_upgrade');
$hasUpgrades = $this->servers->contains(fn ($s) => ($s->outdatedInfo['type'] ?? 'patch_update') === 'minor_upgrade' ||
isset($s->outdatedInfo['newer_branch_target'])
);
$message = "⚠️ Coolify: Traefik proxy outdated on {$count} server(s)!\n\n";
$message .= "Update recommended for security and features.\n";
$message .= "ℹ️ Based on actual running container version\n\n";
$message .= "📊 Affected servers:\n";
foreach ($this->servers as $server) {
$info = $server->outdatedInfo ?? [];
$current = $this->formatVersion($info['current'] ?? 'unknown');
$latest = $this->formatVersion($info['latest'] ?? 'unknown');
$type = ($info['type'] ?? 'patch_update') === 'patch_update' ? '(patch)' : '(upgrade)';
$message .= "• {$server->name}: {$current} → {$latest} {$type}\n";
$upgradeTarget = $this->getUpgradeTarget($info);
$isPatch = ($info['type'] ?? 'patch_update') === 'patch_update';
$hasNewerBranch = isset($info['newer_branch_target']);
if ($isPatch && $hasNewerBranch) {
$newerBranchTarget = $info['newer_branch_target'];
$newerBranchLatest = $this->formatVersion($info['newer_branch_latest']);
$message .= "• {$server->name}: {$current} → {$upgradeTarget} (patch update available)\n";
$message .= " ↳ Also available: {$newerBranchTarget} (latest patch: {$newerBranchLatest}) - new minor version\n";
} elseif ($isPatch) {
$message .= "• {$server->name}: {$current} → {$upgradeTarget} (patch update available)\n";
} else {
$message .= "• {$server->name}: {$current} (latest patch: {$latest}) → {$upgradeTarget} (new minor version available)\n";
}
}
$message .= "\n⚠️ It is recommended to test before switching the production version.";
if ($hasUpgrades) {
$message .= "\n\n📖 For major/minor upgrades: Read the Traefik changelog before upgrading to understand breaking changes.";
$message .= "\n\n📖 For minor version upgrades: Read the Traefik changelog before upgrading to understand breaking changes and new features.";
}
return [
@@ -104,24 +141,37 @@ class TraefikVersionOutdated extends CustomEmailNotification
public function toPushover(): PushoverMessage
{
$count = $this->servers->count();
$hasUpgrades = $this->servers->contains(fn ($s) => ($s->outdatedInfo['type'] ?? 'patch_update') === 'minor_upgrade');
$hasUpgrades = $this->servers->contains(fn ($s) => ($s->outdatedInfo['type'] ?? 'patch_update') === 'minor_upgrade' ||
isset($s->outdatedInfo['newer_branch_target'])
);
$message = "Traefik proxy outdated on {$count} server(s)!\n";
$message .= "Based on actual running container version\n\n";
$message .= "Affected servers:\n";
foreach ($this->servers as $server) {
$info = $server->outdatedInfo ?? [];
$current = $this->formatVersion($info['current'] ?? 'unknown');
$latest = $this->formatVersion($info['latest'] ?? 'unknown');
$type = ($info['type'] ?? 'patch_update') === 'patch_update' ? '(patch)' : '(upgrade)';
$message .= "• {$server->name}: {$current} → {$latest} {$type}\n";
$upgradeTarget = $this->getUpgradeTarget($info);
$isPatch = ($info['type'] ?? 'patch_update') === 'patch_update';
$hasNewerBranch = isset($info['newer_branch_target']);
if ($isPatch && $hasNewerBranch) {
$newerBranchTarget = $info['newer_branch_target'];
$newerBranchLatest = $this->formatVersion($info['newer_branch_latest']);
$message .= "• {$server->name}: {$current} → {$upgradeTarget} (patch update available)\n";
$message .= " Also: {$newerBranchTarget} (latest: {$newerBranchLatest}) - new minor version\n";
} elseif ($isPatch) {
$message .= "• {$server->name}: {$current} → {$upgradeTarget} (patch update available)\n";
} else {
$message .= "• {$server->name}: {$current} (latest patch: {$latest}) → {$upgradeTarget} (new minor version available)\n";
}
}
$message .= "\nIt is recommended to test before switching the production version.";
if ($hasUpgrades) {
$message .= "\n\nFor major/minor upgrades: Read the Traefik changelog before upgrading.";
$message .= "\n\nFor minor version upgrades: Read the Traefik changelog before upgrading.";
}
return new PushoverMessage(
@@ -134,24 +184,37 @@ class TraefikVersionOutdated extends CustomEmailNotification
public function toSlack(): SlackMessage
{
$count = $this->servers->count();
$hasUpgrades = $this->servers->contains(fn ($s) => ($s->outdatedInfo['type'] ?? 'patch_update') === 'minor_upgrade');
$hasUpgrades = $this->servers->contains(fn ($s) => ($s->outdatedInfo['type'] ?? 'patch_update') === 'minor_upgrade' ||
isset($s->outdatedInfo['newer_branch_target'])
);
$description = "Traefik proxy outdated on {$count} server(s)!\n";
$description .= "_Based on actual running container version_\n\n";
$description .= "*Affected servers:*\n";
foreach ($this->servers as $server) {
$info = $server->outdatedInfo ?? [];
$current = $this->formatVersion($info['current'] ?? 'unknown');
$latest = $this->formatVersion($info['latest'] ?? 'unknown');
$type = ($info['type'] ?? 'patch_update') === 'patch_update' ? '(patch)' : '(upgrade)';
$description .= "• `{$server->name}`: {$current} → {$latest} {$type}\n";
$upgradeTarget = $this->getUpgradeTarget($info);
$isPatch = ($info['type'] ?? 'patch_update') === 'patch_update';
$hasNewerBranch = isset($info['newer_branch_target']);
if ($isPatch && $hasNewerBranch) {
$newerBranchTarget = $info['newer_branch_target'];
$newerBranchLatest = $this->formatVersion($info['newer_branch_latest']);
$description .= "• `{$server->name}`: {$current} → {$upgradeTarget} (patch update available)\n";
$description .= " ↳ Also available: {$newerBranchTarget} (latest patch: {$newerBranchLatest}) - new minor version\n";
} elseif ($isPatch) {
$description .= "• `{$server->name}`: {$current} → {$upgradeTarget} (patch update available)\n";
} else {
$description .= "• `{$server->name}`: {$current} (latest patch: {$latest}) → {$upgradeTarget} (new minor version available)\n";
}
}
$description .= "\n:warning: It is recommended to test before switching the production version.";
if ($hasUpgrades) {
$description .= "\n\n:book: For major/minor upgrades: Read the Traefik changelog before upgrading to understand breaking changes.";
$description .= "\n\n:book: For minor version upgrades: Read the Traefik changelog before upgrading to understand breaking changes and new features.";
}
return new SlackMessage(
@@ -166,13 +229,26 @@ class TraefikVersionOutdated extends CustomEmailNotification
$servers = $this->servers->map(function ($server) {
$info = $server->outdatedInfo ?? [];
return [
$webhookData = [
'name' => $server->name,
'uuid' => $server->uuid,
'current_version' => $info['current'] ?? 'unknown',
'latest_version' => $info['latest'] ?? 'unknown',
'update_type' => $info['type'] ?? 'patch_update',
];
// For minor upgrades, include the upgrade target (e.g., "v3.6")
if (($info['type'] ?? 'patch_update') === 'minor_upgrade' && isset($info['upgrade_target'])) {
$webhookData['upgrade_target'] = $info['upgrade_target'];
}
// Include newer branch info if available
if (isset($info['newer_branch_target'])) {
$webhookData['newer_branch_target'] = $info['newer_branch_target'];
$webhookData['newer_branch_latest'] = $info['newer_branch_latest'];
}
return $webhookData;
})->toArray();
return [