Skip to content

Commit a35982e

Browse files
authored
Merge pull request #116 from thetic/docker-versions-changelog
feat: show docker.versions changelogs in the update dialog
2 parents 34009ee + e246ff8 commit a35982e

4 files changed

Lines changed: 144 additions & 3 deletions

File tree

docs/user-guide.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,16 @@ Each stack supports the following actions:
4141
| **Edit Stack** | Open the stack editor |
4242
| **Remove Stack** | Delete the stack configuration |
4343

44+
### Checking for Updates
45+
46+
Use **Check Updates** on a stack to query the registry for newer image versions. Results are cached until the next manual or scheduled check (see [Update Checking settings](configuration.md#update-checking)).
47+
48+
When updates are available, clicking **Update Stack** opens a confirmation dialog listing each container alongside its current and incoming image digest. Containers that are already up to date are dimmed.
49+
50+
#### Changelogs
51+
52+
If the [docker.versions](https://github.com/phyzical/docker.versions) Unraid plugin is installed, a **Changelog** link appears below the digest for each container with a pending update. Clicking it opens the release notes for that image in a modal. No configuration is required — Compose Manager detects docker.versions automatically.
53+
4454
## Autostart
4555

4656
Enable autostart to have stacks start automatically when the Unraid array starts.

source/compose.manager/include/Exec.php

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1504,15 +1504,16 @@ function rejectStaleClientPath(?string $actualPath, string $label): bool
15041504
case 'getSavedUpdateStatus':
15051505
// Load saved update status from file
15061506
$composeUpdateStatusFile = COMPOSE_UPDATE_STATUS_FILE;
1507+
$dockerVersionsInstalled = is_dir('/usr/local/emhttp/plugins/docker.versions');
15071508
if (is_file($composeUpdateStatusFile)) {
15081509
$savedStatus = json_decode(file_get_contents($composeUpdateStatusFile), true);
15091510
if ($savedStatus) {
1510-
echo json_encode(['result' => 'success', 'stacks' => $savedStatus]);
1511+
echo json_encode(['result' => 'success', 'stacks' => $savedStatus, 'dockerVersionsInstalled' => $dockerVersionsInstalled]);
15111512
} else {
1512-
echo json_encode(['result' => 'success', 'stacks' => []]);
1513+
echo json_encode(['result' => 'success', 'stacks' => [], 'dockerVersionsInstalled' => $dockerVersionsInstalled]);
15131514
}
15141515
} else {
1515-
echo json_encode(['result' => 'success', 'stacks' => []]);
1516+
echo json_encode(['result' => 'success', 'stacks' => [], 'dockerVersionsInstalled' => $dockerVersionsInstalled]);
15161517
}
15171518
break;
15181519
case 'getLogs':

source/compose.manager/javascript/composeManagerMain.js

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -966,6 +966,9 @@ function updateTabModifiedState() {
966966
// Update status cache per stack
967967
var stackUpdateStatus = {};
968968

969+
// Set to true when docker.versions plugin is detected (populated from getSavedUpdateStatus response)
970+
var dockerVersionsInstalled = false;
971+
969972
// Load saved update status from server (called on page load)
970973
// If auto-check is enabled and interval has elapsed, trigger a fresh check
971974
// Also checks for pending rechecks from recent update operations
@@ -978,6 +981,7 @@ function loadSavedUpdateStatus() {
978981
var response = JSON.parse(data);
979982
if (response.result === 'success' && response.stacks) {
980983
stackUpdateStatus = response.stacks;
984+
if (response.dockerVersionsInstalled) dockerVersionsInstalled = true;
981985

982986
// Update the UI for each stack with saved status
983987
for (var stackName in response.stacks) {
@@ -3501,6 +3505,42 @@ function mergeUpdateStatus(containers, project) {
35013505
return containers;
35023506
}
35033507

3508+
// Show docker.versions changelog for a single container.
3509+
// Delegates entirely to docker.versions' own showChangeLog() so that display
3510+
// logic, Nchan subscription management, and message routing stay in one place.
3511+
//
3512+
// Coupling point: showChangeLog(containerName) — global function from
3513+
// docker.versions/scripts/changelog.js. If that function is renamed or
3514+
// removed, this feature silently does nothing.
3515+
function showComposeChangelog(containerName, path, profile) {
3516+
if (typeof showChangeLog !== 'function') return;
3517+
showChangeLog(containerName);
3518+
// docker.versions' OK button calls updateContainer(), which bypasses
3519+
// compose_plugin's update mechanism. Hide it so the only exit is Cancel.
3520+
setTimeout(function() { $('.sweet-alert .confirm').hide(); }, 0);
3521+
if (!path) return;
3522+
// Reopen the Update Stack dialog when the changelog dialog closes.
3523+
// SweetAlert 1.x has no close event; poll the showSweetAlert class instead.
3524+
// Cap at 300 ticks (30 s) so the interval self-cleans if the dialog never opens.
3525+
var appeared = false;
3526+
var ticks = 0;
3527+
var poll = setInterval(function() {
3528+
if (++ticks > 300) { clearInterval(poll); return; }
3529+
var open = $('.sweet-alert').hasClass('showSweetAlert');
3530+
if (!appeared) { if (open) appeared = true; return; }
3531+
if (!open) {
3532+
clearInterval(poll);
3533+
// Skip reopening when DISABLE_ACTION_WARNINGS is true: renderStackActionDialog
3534+
// has a fast-path that calls UpdateStackConfirmed directly in that mode,
3535+
// so reopening would trigger an immediate update rather than a dialog.
3536+
getConfig().then(function(cfg) {
3537+
if (cfg && cfg.DISABLE_ACTION_WARNINGS === 'true') return;
3538+
showStackActionDialog('update', path, profile || '');
3539+
});
3540+
}
3541+
}, 100);
3542+
}
3543+
35043544
// Unified stack action dialog - handles up, down, and update actions
35053545
function showStackActionDialog(action, path, profile) {
35063546
var stackName = basename(path);
@@ -3753,6 +3793,9 @@ function renderStackActionDialog(action, displayName, path, profile, containers,
37533793
html += ' <i class="fa fa-arrow-right compose-status-success" style="margin:0 4px;"></i> ';
37543794
html += '<span class="compose-status-success" title="' + composeEscapeAttr(remoteSha) + '">' + composeEscapeHtml(remoteSha.substring(0, 8)) + '</span>';
37553795
html += '</div>';
3796+
if (dockerVersionsInstalled) {
3797+
html += '<div style="margin-top:4px;"><a href="#" data-changelog-container="' + composeEscapeAttr(containerName) + '" data-changelog-path="' + composeEscapeAttr(path) + '" data-changelog-profile="' + composeEscapeAttr(profile || '') + '" onclick="showComposeChangelog(this.dataset.changelogContainer,this.dataset.changelogPath,this.dataset.changelogProfile);return false;" style="font-size:0.85em;"><i class="fa fa-list" style="margin-right:3px;"></i>Changelog</a></div>';
3798+
}
37563799
} else if (localSha) {
37573800
// No update - just show current SHA (greyed)
37583801
html += '<div style="font-family:var(--font-bitstream);font-size:0.9em;margin-top:2px;" title="' + composeEscapeAttr(localSha) + '"><span class="compose-text-muted">' + composeEscapeHtml(localSha.substring(0, 8)) + '</span></div>';

tests/unit/ExecActionsTest.php

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
use PluginTests\TestCase;
1818
use PluginTests\Mocks\FunctionMocks;
19+
use PluginTests\StreamWrapper\UnraidStreamWrapper;
1920

2021

2122
require_once '/usr/local/emhttp/plugins/compose.manager/include/Util.php';
@@ -1057,6 +1058,92 @@ public function testSetStackSettingsRejectsUnsupportedIconUrlTypes(): void
10571058
}
10581059
}
10591060

1061+
// ===========================================
1062+
// getSavedUpdateStatus Action Tests
1063+
// ===========================================
1064+
1065+
/**
1066+
* Returns dockerVersionsInstalled=false when the plugin directory is absent.
1067+
*/
1068+
public function testGetSavedUpdateStatusDockerVersionsNotInstalled(): void
1069+
{
1070+
@unlink(COMPOSE_UPDATE_STATUS_FILE);
1071+
1072+
// Make the test deterministic even when running on a host that has docker.versions installed.
1073+
$fakeDir = sys_get_temp_dir() . '/fake_docker_versions_absent_' . getmypid();
1074+
UnraidStreamWrapper::addMapping('/usr/local/emhttp/plugins/docker.versions', $fakeDir);
1075+
1076+
$output = $this->executeAction('getSavedUpdateStatus');
1077+
$result = json_decode($output, true);
1078+
1079+
$this->assertIsArray($result);
1080+
$this->assertEquals('success', $result['result']);
1081+
$this->assertSame([], $result['stacks']);
1082+
$this->assertFalse($result['dockerVersionsInstalled']);
1083+
}
1084+
1085+
/**
1086+
* Returns dockerVersionsInstalled=true when the plugin directory exists.
1087+
*/
1088+
public function testGetSavedUpdateStatusDockerVersionsInstalled(): void
1089+
{
1090+
@unlink(COMPOSE_UPDATE_STATUS_FILE);
1091+
1092+
$fakeDir = sys_get_temp_dir() . '/fake_docker_versions_' . getmypid();
1093+
mkdir($fakeDir, 0755, true);
1094+
$this->externalCleanupPaths[] = $fakeDir;
1095+
UnraidStreamWrapper::addMapping('/usr/local/emhttp/plugins/docker.versions', $fakeDir);
1096+
1097+
$output = $this->executeAction('getSavedUpdateStatus');
1098+
$result = json_decode($output, true);
1099+
1100+
$this->assertIsArray($result);
1101+
$this->assertEquals('success', $result['result']);
1102+
$this->assertSame([], $result['stacks']);
1103+
$this->assertTrue($result['dockerVersionsInstalled']);
1104+
}
1105+
1106+
/**
1107+
* Returns saved stacks alongside dockerVersionsInstalled when a status file exists.
1108+
*/
1109+
public function testGetSavedUpdateStatusIncludesSavedStacks(): void
1110+
{
1111+
$savedStatus = ['my-stack' => ['hasUpdate' => true, 'containers' => []]];
1112+
file_put_contents(COMPOSE_UPDATE_STATUS_FILE, json_encode($savedStatus));
1113+
1114+
$fakeDir = sys_get_temp_dir() . '/fake_docker_versions_' . getmypid();
1115+
mkdir($fakeDir, 0755, true);
1116+
$this->externalCleanupPaths[] = $fakeDir;
1117+
UnraidStreamWrapper::addMapping('/usr/local/emhttp/plugins/docker.versions', $fakeDir);
1118+
1119+
$output = $this->executeAction('getSavedUpdateStatus');
1120+
$result = json_decode($output, true);
1121+
1122+
$this->assertIsArray($result);
1123+
$this->assertEquals('success', $result['result']);
1124+
$this->assertEquals($savedStatus, $result['stacks']);
1125+
$this->assertTrue($result['dockerVersionsInstalled']);
1126+
1127+
@unlink(COMPOSE_UPDATE_STATUS_FILE);
1128+
}
1129+
1130+
/**
1131+
* Falls back to empty stacks when the status file contains invalid JSON.
1132+
*/
1133+
public function testGetSavedUpdateStatusHandlesInvalidStatusFile(): void
1134+
{
1135+
file_put_contents(COMPOSE_UPDATE_STATUS_FILE, 'not-valid-json');
1136+
1137+
$output = $this->executeAction('getSavedUpdateStatus');
1138+
$result = json_decode($output, true);
1139+
1140+
$this->assertIsArray($result);
1141+
$this->assertEquals('success', $result['result']);
1142+
$this->assertSame([], $result['stacks']);
1143+
1144+
@unlink(COMPOSE_UPDATE_STATUS_FILE);
1145+
}
1146+
10601147
// ===========================================
10611148
// checkStackLock Action Tests
10621149
// ===========================================

0 commit comments

Comments
 (0)