From 7e1a44c2bb2db71958f3c18dbae865b08cd08dda Mon Sep 17 00:00:00 2001 From: mstrhakr Date: Thu, 3 Sep 2026 21:48:43 -0400 Subject: [PATCH 01/24] fix: quiet routine dockerload reconnect warnings (#133) --- .../javascript/composeManagerMain.js | 32 ++++++++++++++++--- 1 file changed, 28 insertions(+), 4 deletions(-) diff --git a/source/compose.manager/javascript/composeManagerMain.js b/source/compose.manager/javascript/composeManagerMain.js index 78cd1544..e86efb89 100644 --- a/source/compose.manager/javascript/composeManagerMain.js +++ b/source/compose.manager/javascript/composeManagerMain.js @@ -3240,10 +3240,34 @@ $(function() { if (!isCurrentComposeDockerLoad(composeDockerLoad, newGeneration)) { return; } - composeLogger('WebSocket error', { - code: code, - desc: desc - }, 'user', 'warn', 'dockerload'); + + var socketError = {}; + if (code && typeof code === 'object') { + socketError.type = code.type || null; + socketError.message = code.message || null; + socketError.readyState = (code.target && code.target.readyState !== undefined) ? code.target.readyState : null; + if (code.code !== undefined && code.code !== null) { + socketError.code = code.code; + } + } else if (code !== undefined && code !== null) { + socketError.code = code; + } + + if (desc && typeof desc === 'object') { + if (desc.type) { + socketError.descType = desc.type; + } + if (desc.readyState !== undefined && desc.readyState !== null) { + socketError.descReadyState = desc.readyState; + } + if (desc.message) { + socketError.descMessage = desc.message; + } + } else if (desc !== undefined && desc !== null) { + socketError.desc = desc; + } + + composeLogger('WebSocket reconnect/error', socketError, 'user', 'debug', 'dockerload'); }); // If dockerload pauses/stalls, drop stale values on a timer so the UI From 838964f9d53f6efd184b967fd1cfe7e28191433f Mon Sep 17 00:00:00 2001 From: mstrhakr Date: Fri, 4 Sep 2026 06:07:01 -0400 Subject: [PATCH 02/24] feat: add option to not close editor modal on outside click (#126) --- source/compose.manager/compose.manager.settings.page | 10 ++++++++++ source/compose.manager/default.cfg | 1 + .../compose.manager/javascript/composeManagerMain.js | 10 ++++++++-- 3 files changed, 19 insertions(+), 2 deletions(-) diff --git a/source/compose.manager/compose.manager.settings.page b/source/compose.manager/compose.manager.settings.page index 61db6b87..07bc7c73 100755 --- a/source/compose.manager/compose.manager.settings.page +++ b/source/compose.manager/compose.manager.settings.page @@ -2905,6 +2905,16 @@ $acePath = file_exists('/usr/local/emhttp/plugins/dynamix/javascript/ace/ace.js'
_(Advanced Options)_

Performance tuning and troubleshooting options for advanced users.

+
+
_(Don't close editor modal on outside click)_:
+
+ > +
+ When enabled, the Compose editor stays open when you click or drag on the backdrop outside the modal. This keeps editing from being interrupted while selecting or moving large blocks of text. +
+
+
+
_(Debug Logging)_:
diff --git a/source/compose.manager/default.cfg b/source/compose.manager/default.cfg index 681f1391..a8de5c3f 100755 --- a/source/compose.manager/default.cfg +++ b/source/compose.manager/default.cfg @@ -30,3 +30,4 @@ STACKS_DEFAULT_EXPANDED="false" ONLY_EXPAND_RUNNING_STACKS="false" COMPOSE_STATS_RATE_MODE="live" COMPOSE_STATS_CUSTOM_INTERVAL_MS="1000" +DONT_CLOSE_EDITOR_MODAL_ON_OUTSIDE_CLICK="false" diff --git a/source/compose.manager/javascript/composeManagerMain.js b/source/compose.manager/javascript/composeManagerMain.js index e86efb89..914e740e 100644 --- a/source/compose.manager/javascript/composeManagerMain.js +++ b/source/compose.manager/javascript/composeManagerMain.js @@ -1460,9 +1460,15 @@ function initEditorModal() { // Close modal when clicking on the overlay background (not the inner modal content) $('#editor-modal-overlay').off('click.editorModal').on('click.editorModal', function(e) { - if (e.target === this) { - closeEditorModal(); + if (e.target !== this) { + return; } + getConfig().then(function(cfg) { + if (cfg.DONT_CLOSE_EDITOR_MODAL_ON_OUTSIDE_CLICK === 'true') { + return; + } + closeEditorModal(); + }); }); } From 9b98f66b489b55c66e08bb0ae1410de19709710a Mon Sep 17 00:00:00 2001 From: mstrhakr Date: Fri, 4 Sep 2026 17:40:26 -0400 Subject: [PATCH 03/24] fix: support docker-compose stacks that resolve COMPOSE_FILE from .env in default discovery mode (#149) --- source/compose.manager/include/Util.php | 83 ++++++++++++++++++++++- source/compose.manager/scripts/compose.sh | 18 +++-- tests/unit/StackInfoTest.php | 15 ++++ 3 files changed, 111 insertions(+), 5 deletions(-) diff --git a/source/compose.manager/include/Util.php b/source/compose.manager/include/Util.php index 8ee8147d..36948b4d 100644 --- a/source/compose.manager/include/Util.php +++ b/source/compose.manager/include/Util.php @@ -2282,7 +2282,88 @@ private static function getComposeFilePath($path): string|null break; } } - return $composeFilePath; + if ($composeFilePath !== null) { + return $composeFilePath; + } + + $envFilePath = self::resolveProjectEnvFilePath($path); + if ($envFilePath === null) { + return null; + } + + return self::resolveComposeFileFromEnvFile($envFilePath); + } + + /** + * Resolve the active env file for a project root, respecting explicit + * envpath metadata before falling back to the local .env file. + * + * @param string $path Project directory path + * @return string|null Resolved env file path or null if none is usable + */ + private static function resolveProjectEnvFilePath(string $path): ?string + { + $stackDir = rtrim($path, '/'); + $envPathMetadata = $stackDir . '/envpath'; + if (is_file($envPathMetadata)) { + $raw = @file_get_contents($envPathMetadata); + $candidate = $raw === false ? '' : trim($raw); + if ($candidate !== '') { + $resolved = $candidate; + if (!Path::isAbsolutePath($resolved)) { + $resolved = $stackDir . '/' . $resolved; + } + if (is_file($resolved)) { + return realpath($resolved) ?: $resolved; + } + } + } + + $defaultEnvPath = $stackDir . '/.env'; + return is_file($defaultEnvPath) ? $defaultEnvPath : null; + } + + /** + * Resolve the first valid compose file declared by COMPOSE_FILE in an env file. + * + * @param string $envFilePath + * @return string|null + */ + private static function resolveComposeFileFromEnvFile(string $envFilePath): ?string + { + $content = @file_get_contents($envFilePath); + if ($content === false) { + return null; + } + + $envDir = dirname($envFilePath); + foreach (preg_split('/\R/', $content) as $line) { + $line = trim($line); + if ($line === '' || str_starts_with($line, '#') || str_starts_with($line, ';')) { + continue; + } + if (!str_contains($line, '=')) { + continue; + } + [$key, $value] = explode('=', $line, 2); + if (trim($key) !== 'COMPOSE_FILE') { + continue; + } + + foreach (self::splitComposeFileValue(trim($value)) as $entry) { + $entry = Strings::stripQuotes(trim($entry)); + if ($entry === '') { + continue; + } + $candidate = Path::isAbsolutePath($entry) ? $entry : $envDir . '/' . $entry; + if (is_file($candidate) && preg_match('/\.ya?ml$/i', basename($candidate)) === 1) { + return $candidate; + } + } + break; + } + + return null; } diff --git a/source/compose.manager/scripts/compose.sh b/source/compose.manager/scripts/compose.sh index 5f4f3cb8..f2295531 100755 --- a/source/compose.manager/scripts/compose.sh +++ b/source/compose.manager/scripts/compose.sh @@ -22,7 +22,7 @@ env_args=() file_args=() profile_names=() profile_args=() -project_dir_args=() +project_directory="" cmd_args=() stack_path="" debug=false @@ -169,7 +169,7 @@ do ;; -w | --workdir ) if [ -d "$2" ]; then - project_dir_args=("--project-directory" "$2") + project_directory="$2" else log_msg "ERROR" "Project directory does not exist: $2" exit 1 @@ -220,8 +220,18 @@ for profile_name in "${profile_names[@]}"; do profile_args+=("--profile" "$profile_name") done -# Build the compose base command as an array (no eval needed) -compose_base=(docker compose "${project_dir_args[@]}" "${env_args[@]}" "${file_args[@]}" "${profile_args[@]}") +if [ -n "$project_directory" ]; then + if ! cd "$project_directory" 2>/dev/null; then + log_msg "ERROR" "Failed to cd into project directory: $project_directory" + exit 1 + fi +fi + +# Build the compose base command as an array (no eval needed). +# When we need Docker Compose default discovery we intentionally run from the +# project directory itself so it matches plain `cd && docker compose ...`, +# which is the behavior the project was validated against. +compose_base=(docker compose "${env_args[@]}" "${file_args[@]}" "${profile_args[@]}") # Canonicalize project name through shared PHP sanitizer. if ! name=$(canonicalize_project_name "$name"); then diff --git a/tests/unit/StackInfoTest.php b/tests/unit/StackInfoTest.php index d6b768a1..e90a9739 100644 --- a/tests/unit/StackInfoTest.php +++ b/tests/unit/StackInfoTest.php @@ -1113,6 +1113,21 @@ public function testBuildComposeArgsWithComposeFileInEnv(): void $this->assertContains($stackDir . '/compose.debug.yaml', $args['filePaths']); } + public function testProjectLoadsWhenComposeFileIsOnlyDeclaredInDotEnv(): void + { + $stack = 'env-compose-file-only'; + $stackDir = $this->tempRoot . '/' . $stack; + mkdir($stackDir . '/docker', 0755, true); + file_put_contents("$stackDir/.env", "COMPOSE_FILE=docker/docker-compose.yml\n"); + file_put_contents("$stackDir/docker/docker-compose.yml", "services:\n web:\n image: nginx\n"); + + $info = \StackInfo::fromProject($this->tempRoot, $stack); + + $this->assertSame($stackDir . '/docker/docker-compose.yml', $info->composeFilePath); + $this->assertContains($stackDir . '/docker/docker-compose.yml', $info->getEditableComposeFiles()); + $this->assertStringContainsString('docker-compose.yml', $info->buildComposeArgs()['files']); + } + public function testBuildComposeArgsWithQuotedComposeFileInEnv(): void { $stack = 'env-compose-file-quoted'; From 2d0e9e1f85783d594e81f24ac42118758e633411 Mon Sep 17 00:00:00 2001 From: mstrhakr Date: Sat, 5 Sep 2026 20:40:06 -0400 Subject: [PATCH 04/24] Fix: Pass stack directory to resolveProjectEnvFilePath for indirect stack envpath metadata resolution For indirect stacks, metadata files (including envpath) live in the stack directory ($this->path), but getComposeFilePath() was called with the indirect path ($this->composeSource). This prevented envpath discovery for indirect stacks. Changes: - Add optional $stackPath parameter to getComposeFilePath() - Update resolveProjectEnvFilePath() to accept both compose path and stack path - Pass $this->path as stack path when resolving compose file - Look for envpath metadata in stack directory regardless of indirect path This ensures indirect stacks with envpath metadata can properly discover compose files via COMPOSE_FILE variable, matching the behavior of direct stacks. --- source/compose.manager/include/Util.php | 27 ++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/source/compose.manager/include/Util.php b/source/compose.manager/include/Util.php index 36948b4d..c3a3e2f3 100644 --- a/source/compose.manager/include/Util.php +++ b/source/compose.manager/include/Util.php @@ -2189,13 +2189,12 @@ private function __construct(string $composeRoot, string $projectFolder) $this->composeSource = dirname($this->indirectPath); } else { $this->composeSource = $this->indirectPath; - $this->composeFilePath = self::getComposeFilePath($this->composeSource); + $this->composeFilePath = self::getComposeFilePath($this->composeSource, $this->path); } } else { $this->composeSource = $this->path; - $this->composeFilePath = self::getComposeFilePath($this->composeSource); + $this->composeFilePath = self::getComposeFilePath($this->composeSource, $this->path); } - if ($this->invalidIndirectPath === null && $this->indirectPath !== null && $this->indirectPath !== '' && $this->composeFilePath === null && $this->isIndirect) { // Preserve the broken indirect target for repair flows. $this->invalidIndirectPath = $this->indirectPath; @@ -2267,10 +2266,15 @@ public static function clearCache(?string $key = null): void * Checks for compose.yaml, compose.yml, docker-compose.yaml, docker-compose.yml * in that order and returns the first one found. * + * @param string $path The compose source directory (where to look for compose files and .env) + * @param string|null $stackPath Optional stack metadata directory (where to look for envpath metadata); + * defaults to $path if not provided * @return string|null Full path to the compose file if found, or null if none found */ - private static function getComposeFilePath($path): string|null + private static function getComposeFilePath($path, ?string $stackPath = null): string|null { + $stackPath = $stackPath ?? $path; + if (is_string($path) && is_file($path)) { return preg_match('/\.ya?ml$/i', basename($path)) === 1 ? $path : null; } @@ -2286,7 +2290,7 @@ private static function getComposeFilePath($path): string|null return $composeFilePath; } - $envFilePath = self::resolveProjectEnvFilePath($path); + $envFilePath = self::resolveProjectEnvFilePath($path, $stackPath); if ($envFilePath === null) { return null; } @@ -2298,12 +2302,16 @@ private static function getComposeFilePath($path): string|null * Resolve the active env file for a project root, respecting explicit * envpath metadata before falling back to the local .env file. * - * @param string $path Project directory path + * @param string $path Compose source directory (where to look for .env) + * @param string $stackPath Stack directory (where to look for envpath metadata) * @return string|null Resolved env file path or null if none is usable */ - private static function resolveProjectEnvFilePath(string $path): ?string + private static function resolveProjectEnvFilePath(string $path, string $stackPath): ?string { - $stackDir = rtrim($path, '/'); + $stackDir = rtrim($stackPath, '/'); + $composeDir = rtrim($path, '/'); + + // Check for explicit envpath metadata in stack directory $envPathMetadata = $stackDir . '/envpath'; if (is_file($envPathMetadata)) { $raw = @file_get_contents($envPathMetadata); @@ -2319,7 +2327,8 @@ private static function resolveProjectEnvFilePath(string $path): ?string } } - $defaultEnvPath = $stackDir . '/.env'; + // Fall back to .env in compose directory + $defaultEnvPath = $composeDir . '/.env'; return is_file($defaultEnvPath) ? $defaultEnvPath : null; } From 32db3290994f3762b9bc74edef8bf9516e44a25e Mon Sep 17 00:00:00 2001 From: mstrhakr Date: Sat, 5 Sep 2026 20:55:00 -0400 Subject: [PATCH 05/24] Fix: Support relative envpath values in getExplicitEnvFilePath Update getExplicitEnvFilePath() to handle relative paths in envpath metadata, matching the behavior of resolveProjectEnvFilePath(). Previously, relative envpath values (e.g., 'config/.env') would not be resolved correctly when used in compose commands. Now both absolute and relative paths are supported, with relative paths resolved against the stack directory. This ensures consistency between compose file discovery and env-file argument construction for stacks with relative envpath metadata. --- source/compose.manager/include/Util.php | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/source/compose.manager/include/Util.php b/source/compose.manager/include/Util.php index c3a3e2f3..89a6909e 100644 --- a/source/compose.manager/include/Util.php +++ b/source/compose.manager/include/Util.php @@ -2615,6 +2615,9 @@ public function getEffectiveEnvFilePath(): ?string /** * Resolve a valid explicit envpath configured in stack metadata. * + * Supports both absolute and relative paths. Relative paths are resolved + * relative to the stack directory ($this->path). + * * @return string|null */ private function getExplicitEnvFilePath(): ?string @@ -2629,10 +2632,19 @@ private function getExplicitEnvFilePath(): ?string return null; } + // Try as absolute path first if (is_file($rawEnvPath)) { return realpath($rawEnvPath) ?: $rawEnvPath; } + // Try as relative to stack directory + if (!Path::isAbsolutePath($rawEnvPath)) { + $relativePath = $this->path . '/' . $rawEnvPath; + if (is_file($relativePath)) { + return realpath($relativePath) ?: $relativePath; + } + } + composeLogger('Explicit envpath is set but not resolvable to a file; falling back to default env resolution', [ 'project' => $this->projectFolder, 'envpath' => $rawEnvPath, From 9e190d77c927d5e54ff684eb572a50d9b570c1cf Mon Sep 17 00:00:00 2001 From: mstrhakr Date: Sat, 5 Sep 2026 21:00:28 -0400 Subject: [PATCH 06/24] Fix #3: Add tests for indirect stack envpath metadata discovery Add two new test cases to verify indirect stacks can properly discover compose files via envpath metadata: 1. testIndirectStackLoadsWhenComposeFileViaEnvpath: Tests indirect stack with absolute envpath pointing to a .env file that declares COMPOSE_FILE 2. testIndirectStackLoadsWhenComposeFileViaRelativeEnvpath: Tests indirect stack with relative envpath (resolved relative to stack directory) pointing to a .env file that declares COMPOSE_FILE Also normalize returned paths in resolveComposeFileFromEnvFile() using realpath() to ensure consistent path representation across different resolution paths. These tests verify the fixes in the previous commits work correctly for indirect stacks with custom envpath metadata. --- source/compose.manager/include/Util.php | 2 +- tests/unit/StackInfoTest.php | 49 +++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 1 deletion(-) diff --git a/source/compose.manager/include/Util.php b/source/compose.manager/include/Util.php index 89a6909e..e218dc23 100644 --- a/source/compose.manager/include/Util.php +++ b/source/compose.manager/include/Util.php @@ -2366,7 +2366,7 @@ private static function resolveComposeFileFromEnvFile(string $envFilePath): ?str } $candidate = Path::isAbsolutePath($entry) ? $entry : $envDir . '/' . $entry; if (is_file($candidate) && preg_match('/\.ya?ml$/i', basename($candidate)) === 1) { - return $candidate; + return realpath($candidate) ?: $candidate; } } break; diff --git a/tests/unit/StackInfoTest.php b/tests/unit/StackInfoTest.php index e90a9739..d75ca56c 100644 --- a/tests/unit/StackInfoTest.php +++ b/tests/unit/StackInfoTest.php @@ -1128,6 +1128,55 @@ public function testProjectLoadsWhenComposeFileIsOnlyDeclaredInDotEnv(): void $this->assertStringContainsString('docker-compose.yml', $info->buildComposeArgs()['files']); } + public function testIndirectStackLoadsWhenComposeFileViaEnvpath(): void + { + $stack = 'indirect-env-compose'; + $stackDir = $this->tempRoot . '/' . $stack; + $indirectDir = $this->tempRoot . '/indirect_source'; + mkdir($stackDir); + mkdir($indirectDir . '/config', 0755, true); + + // Create indirect stack with envpath metadata + file_put_contents("$stackDir/indirect", $indirectDir); + file_put_contents("$stackDir/envpath", "$indirectDir/config/.env"); + + // Create .env file at indirect location with COMPOSE_FILE declaration + file_put_contents("$indirectDir/config/.env", "COMPOSE_FILE=../docker-compose.yml\n"); + file_put_contents("$indirectDir/docker-compose.yml", "services:\n web:\n image: nginx\n"); + + $info = \StackInfo::fromProject($this->tempRoot, $stack); + + $this->assertTrue($info->isIndirect); + $this->assertSame($indirectDir, $info->composeSource); + $this->assertSame($indirectDir . '/docker-compose.yml', $info->composeFilePath); + $this->assertContains($indirectDir . '/docker-compose.yml', $info->getEditableComposeFiles()); + } + + public function testIndirectStackLoadsWhenComposeFileViaRelativeEnvpath(): void + { + $stack = 'indirect-relative-env'; + $stackDir = $this->tempRoot . '/' . $stack; + $indirectDir = $this->tempRoot . '/another_indirect'; + mkdir($stackDir); + mkdir($indirectDir); + + // Create indirect stack with relative envpath metadata + file_put_contents("$stackDir/indirect", $indirectDir); + file_put_contents("$stackDir/envpath", "config/.env"); // relative path in stack dir + + // Create the referenced .env file in stack directory + mkdir("$stackDir/config", 0755, true); + file_put_contents("$stackDir/config/.env", "COMPOSE_FILE=../docker-compose.yml\n"); + file_put_contents("$stackDir/docker-compose.yml", "services:\n web:\n image: nginx\n"); + + $info = \StackInfo::fromProject($this->tempRoot, $stack); + + $this->assertTrue($info->isIndirect); + $this->assertSame($indirectDir, $info->composeSource); + // The compose file is discovered via COMPOSE_FILE in the referenced envpath + $this->assertSame($stackDir . '/docker-compose.yml', $info->composeFilePath); + } + public function testBuildComposeArgsWithQuotedComposeFileInEnv(): void { $stack = 'env-compose-file-quoted'; From 1be1c0eb9ef61d01b852a0d315b8ebc5c937ac29 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 6 Sep 2026 01:04:08 +0000 Subject: [PATCH 07/24] chore: update changelog for v2026.09.05.2104 [skip ci] --- compose.manager.plg | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/compose.manager.plg b/compose.manager.plg index 66df4cc2..62c73fa5 100644 --- a/compose.manager.plg +++ b/compose.manager.plg @@ -36,13 +36,15 @@ > -###2026.09.03.2109 -- Bug Fixes (test): update follow-logs test to check for correct option syntax -- Harden follow-session suspension and add regression checks -- Escape dashboard stack folder attributes -- Fix follow-logs CLI parsing and cleanup trap -- [PR #147](https://github.com/mstrhakr/compose_plugin/pull/147) -- [beta release diff](https://github.com/mstrhakr/compose_plugin/compare/v2026.08.31...v2026.09.03.2109) +###2026.09.05.2104 +- Features: add option to not close editor modal on outside click (#126) +- Bug Fixes: support docker-compose stacks that resolve COMPOSE_FILE from .env in default discovery mode (#149) +- Bug Fixes: quiet routine dockerload reconnect warnings (#133) +- Fix #3: Add tests for indirect stack envpath metadata discovery +- Fix: Support relative envpath values in getExplicitEnvFilePath +- Fix: Pass stack directory to resolveProjectEnvFilePath for indirect stack envpath metadata resolution +- [PR #150](https://github.com/mstrhakr/compose_plugin/pull/150) +- [beta release diff](https://github.com/mstrhakr/compose_plugin/compare/v2026.09.03...v2026.09.05.2104) From ab4b71c1d2473bdf86dcc1c743715aa119c9f192 Mon Sep 17 00:00:00 2001 From: mstrhakr Date: Sun, 6 Sep 2026 17:05:58 -0400 Subject: [PATCH 08/24] fix: enforce string type for path parameter in getComposeFilePath method --- source/compose.manager/include/Util.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/source/compose.manager/include/Util.php b/source/compose.manager/include/Util.php index e218dc23..cd2fd5b7 100644 --- a/source/compose.manager/include/Util.php +++ b/source/compose.manager/include/Util.php @@ -2271,11 +2271,11 @@ public static function clearCache(?string $key = null): void * defaults to $path if not provided * @return string|null Full path to the compose file if found, or null if none found */ - private static function getComposeFilePath($path, ?string $stackPath = null): string|null + private static function getComposeFilePath(string $path, ?string $stackPath = null): string|null { $stackPath = $stackPath ?? $path; - if (is_string($path) && is_file($path)) { + if (is_file($path)) { return preg_match('/\.ya?ml$/i', basename($path)) === 1 ? $path : null; } From c2f1c16a0bfefdd30e473e0caba4c3917f7dabd3 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 6 Sep 2026 21:06:32 +0000 Subject: [PATCH 09/24] chore: update changelog for v2026.09.06.1706 [skip ci] --- compose.manager.plg | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/compose.manager.plg b/compose.manager.plg index 62c73fa5..265bdaa3 100644 --- a/compose.manager.plg +++ b/compose.manager.plg @@ -36,15 +36,10 @@ > -###2026.09.05.2104 -- Features: add option to not close editor modal on outside click (#126) -- Bug Fixes: support docker-compose stacks that resolve COMPOSE_FILE from .env in default discovery mode (#149) -- Bug Fixes: quiet routine dockerload reconnect warnings (#133) -- Fix #3: Add tests for indirect stack envpath metadata discovery -- Fix: Support relative envpath values in getExplicitEnvFilePath -- Fix: Pass stack directory to resolveProjectEnvFilePath for indirect stack envpath metadata resolution +###2026.09.06.1706 +- Minor updates and improvements - [PR #150](https://github.com/mstrhakr/compose_plugin/pull/150) -- [beta release diff](https://github.com/mstrhakr/compose_plugin/compare/v2026.09.03...v2026.09.05.2104) +- [beta release diff](https://github.com/mstrhakr/compose_plugin/compare/v2026.09.03...v2026.09.06.1706) From 026da4584889f4fea1723d8b18a041cc0a772750 Mon Sep 17 00:00:00 2001 From: mstrhakr Date: Sun, 6 Sep 2026 21:13:20 -0400 Subject: [PATCH 10/24] fix: dynamically resolve latest infozip package from Slackware repository --- source/pkg_build.sh | 34 ++++++++++++++++++++++++++++------ 1 file changed, 28 insertions(+), 6 deletions(-) diff --git a/source/pkg_build.sh b/source/pkg_build.sh index 905d1d6e..9f35f8ee 100755 --- a/source/pkg_build.sh +++ b/source/pkg_build.sh @@ -130,13 +130,35 @@ wget_args() { echo "${args[@]}" } +resolve_latest_slackware_package() { + local package_prefix="$1" + local package_index + local package_name="" + + package_index="$(mktemp)" + if ! wget $(wget_args) -q -O "$package_index" "https://slackware.osuosl.org/slackware64-current/slackware64/a/"; then + echo "Failed to fetch Slackware package index while resolving ${package_prefix}." | tee -a "$LOG_FILE" + rm -f "$package_index" + exit 9 + fi + + package_name="$(grep -Eo "${package_prefix}[^\"'[:space:]]+\\.txz" "$package_index" | sort -V | tail -n 1 || true)" + rm -f "$package_index" + + if [[ -z "$package_name" ]]; then + echo "No ${package_prefix} package found in the Slackware current package index." | tee -a "$LOG_FILE" + exit 10 + fi + + echo "$package_name" +} + echo "Installing unzip dependency..." -INFOZIP_PKG="infozip-6.0-x86_64-8.txz" -download_with_sha_cache \ - "https://slackware.osuosl.org/slackware64-current/slackware64/a/${INFOZIP_PKG}" \ - "" \ - "$INFOZIP_PKG" \ - "2df6d72a3662be939fb533564b1b1e6d4fedd1e2cbddaa8d39627509a397d4d3" +INFOZIP_PKG="$(resolve_latest_slackware_package "infozip")" +INFOZIP_URL="https://slackware.osuosl.org/slackware64-current/slackware64/a/${INFOZIP_PKG}" + +echo "Resolved latest infozip package: ${INFOZIP_PKG}" | tee -a "$LOG_FILE" +download_file_quiet "$INFOZIP_URL" "$INFOZIP_PKG" "infozip package" run_quiet upgradepkg --install-new "${INFOZIP_PKG}" echo "Creating temporary package structure at $tmpdir..." From 2a672e8ba1ca2710b337340277d73159381c9d5d Mon Sep 17 00:00:00 2001 From: mstrhakr Date: Sun, 6 Sep 2026 21:15:53 -0400 Subject: [PATCH 11/24] fix: enhance environment variable handling in build scripts and workflows --- .github/workflows/build.yml | 4 ++ .gitignore | 1 + build.sh | 103 ++++++++++++++++++++++++++---------- build_in_docker.sh | 2 +- deploy.sh | 29 ++++++++-- 5 files changed, 107 insertions(+), 32 deletions(-) mode change 100644 => 100755 build.sh diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 8a947e00..af6ea999 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -45,6 +45,10 @@ concurrency: env: FORCE_JAVASCRIPT_ACTIONS_TO_NODE24: 'true' PLUGIN_NAME: compose.manager + HOST_WORKSPACE_ROOT: ${{ github.workspace }} + HOST_SOURCE_PATH: ${{ github.workspace }}/source + HOST_ARCHIVE_PATH: ${{ github.workspace }}/archive + HOST_CACHE_PATH: ${{ github.workspace }}/archive/.build-cache jobs: build: diff --git a/.gitignore b/.gitignore index 8a1b5ca9..8963672b 100644 --- a/.gitignore +++ b/.gitignore @@ -27,3 +27,4 @@ Thumbs.db # Runtime config files (created at runtime, not source) autoupdate.json +.env diff --git a/build.sh b/build.sh old mode 100644 new mode 100755 index 3e840cf3..377c8aaf --- a/build.sh +++ b/build.sh @@ -12,6 +12,26 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" OUTPUT_PATH="$SCRIPT_DIR/archive" PLG_FILE="$SCRIPT_DIR/compose.manager.plg" VERSIONS_FILE="$SCRIPT_DIR/versions.env" +BUILD_ENV_FILE="${BUILD_ENV_FILE:-$SCRIPT_DIR/.env}" + +load_env_file() { + local env_file="$1" + if [[ -n "$env_file" && -f "$env_file" ]]; then + # shellcheck disable=SC1090 + set -a + source "$env_file" + set +a + fi +} + +load_env_file "$VERSIONS_FILE" +load_env_file "$BUILD_ENV_FILE" + +# Environment override knobs for nested Docker / Unraid host setups. +HOST_WORKSPACE_ROOT="${HOST_WORKSPACE_ROOT:-}" +HOST_SOURCE_PATH="${HOST_SOURCE_PATH:-}" +HOST_ARCHIVE_PATH_OVERRIDE="${HOST_ARCHIVE_PATH:-}" +HOST_CACHE_PATH_OVERRIDE="${HOST_CACHE_PATH:-}" # Argument parsing while [[ $# -gt 0 ]]; do @@ -44,7 +64,7 @@ if [[ -f "$VERSIONS_FILE" ]]; then [[ "$line" =~ ^RESVG_SHA256=(.+)$ ]] && [[ -z "$RESVG_SHA256" ]] && RESVG_SHA256="${BASH_REMATCH[1]}" done < "$VERSIONS_FILE" fi -: "${COMPOSE_VERSION:=5.1.2}" +: "${COMPOSE_VERSION:=5.5.0}" : "${RESVG_VERSION:=0.48.1}" # Generate dev version with timestamp if requested @@ -114,7 +134,13 @@ mkdir -p "$ARCHIVE_PATH" # Host path for docker socket operations should be the actual unRAID path. HOST_ARCHIVE_PATH="$ARCHIVE_PATH" -if [[ "$in_container" == true && -d "/code" ]]; then +if [[ -n "$HOST_ARCHIVE_PATH_OVERRIDE" ]]; then + HOST_ARCHIVE_PATH="$HOST_ARCHIVE_PATH_OVERRIDE" +elif [[ -n "$HOST_WORKSPACE_ROOT" ]]; then + HOST_ARCHIVE_PATH="${HOST_WORKSPACE_ROOT%/}/compose_plugin/archive" +elif [[ "$in_container" == true && -d "/mnt/user/code" ]]; then + HOST_ARCHIVE_PATH="/mnt/user/code/compose_plugin/archive" +elif [[ "$in_container" == true && -d "/code" ]]; then # Map /code inside container to host path for Docker bind mounts (normally /mnt/user/code). read -r host_root host_source < <(awk '$5=="/code" {for(i=1;i<=NF;i++){if($i=="-"){print $4, $(i+2); exit}}}' /proc/self/mountinfo 2>/dev/null || true) if [[ -n "$host_root" && -n "$host_source" ]]; then @@ -158,8 +184,18 @@ mkdir -p "$HOST_ARCHIVE_PATH" 2>/dev/null || true CACHE_PATH="$ARCHIVE_PATH/.build-cache" HOST_CACHE_PATH="$HOST_ARCHIVE_PATH/.build-cache" +if [[ -n "$HOST_CACHE_PATH_OVERRIDE" ]]; then + HOST_CACHE_PATH="$HOST_CACHE_PATH_OVERRIDE" +fi SOURCE_PATH="$SCRIPT_DIR/source" +if [[ -n "$HOST_SOURCE_PATH" ]]; then + SOURCE_PATH="$HOST_SOURCE_PATH" +elif [[ -n "$HOST_WORKSPACE_ROOT" ]]; then + SOURCE_PATH="${HOST_WORKSPACE_ROOT%/}/compose_plugin/source" +elif [[ "$in_container" == true && -d "/mnt/user/code/compose_plugin/source" ]]; then + SOURCE_PATH="/mnt/user/code/compose_plugin/source" +fi mkdir -p "$ARCHIVE_PATH" mkdir -p "$HOST_ARCHIVE_PATH" @@ -248,28 +284,15 @@ SOURCE_PATH="$TMP_SOURCE_PATH" echo "Docker will mount SOURCE_PATH=$SOURCE_PATH" # Determine a strategy to provide SOURCE_PATH to the container. -# First attempt direct bind mount; if that fails we fallback to tar stream. - -build_cmd_direct=(docker run --rm --tmpfs /tmp \ - -v "$HOST_ARCHIVE_PATH:/mnt/output:rw" \ - -v "$HOST_CACHE_PATH:/mnt/cache:rw" \ - -v "$SOURCE_PATH:/mnt/source:ro" \ - -v "$HOST_CA_CERT:$CONTAINER_CA_CERT:ro" \ - -e TZ=America/New_York \ - -e COMPOSE_VERSION="$COMPOSE_VERSION" \ - -e RESVG_VERSION="$RESVG_VERSION" \ - -e RESVG_SHA256="$RESVG_SHA256" \ - -e OUTPUT_FOLDER=/mnt/output \ - -e DOWNLOAD_CACHE_DIR=/mnt/cache \ - -e PKG_VERSION="$VERSION" \ - -e PKG_BUILD="$BUILD_NUM" \ - -e CA_CERT="$CONTAINER_CA_CERT" \ - vbatts/slackware:latest \ - sh -c 'test -f /mnt/source/pkg_build.sh') - -if "${build_cmd_direct[@]}"; then - echo "Direct source mount works. Running build via direct mount..." - if ! docker run --rm --tmpfs /tmp \ +# In nested Docker and CI, direct bind mounts are usually not visible to the daemon, +# so prefer the tar-stream path unless we are on a native host with a visible workspace. +USE_DIRECT_BIND=false +if [[ "$in_container" != true && -z "${CI:-}" && -d "$SOURCE_PATH" ]]; then + USE_DIRECT_BIND=true +fi + +if [[ "$USE_DIRECT_BIND" == true ]]; then + build_cmd_direct=(docker run --rm --tmpfs /tmp \ -v "$HOST_ARCHIVE_PATH:/mnt/output:rw" \ -v "$HOST_CACHE_PATH:/mnt/cache:rw" \ -v "$SOURCE_PATH:/mnt/source:ro" \ @@ -284,11 +307,35 @@ if "${build_cmd_direct[@]}"; then -e PKG_BUILD="$BUILD_NUM" \ -e CA_CERT="$CONTAINER_CA_CERT" \ vbatts/slackware:latest \ - /mnt/source/pkg_build.sh; then - echo "Docker build failed."; exit 1 + sh -c 'test -f /mnt/source/pkg_build.sh') + + if "${build_cmd_direct[@]}"; then + echo "Direct source mount works. Running build via direct mount..." + if ! docker run --rm --tmpfs /tmp \ + -v "$HOST_ARCHIVE_PATH:/mnt/output:rw" \ + -v "$HOST_CACHE_PATH:/mnt/cache:rw" \ + -v "$SOURCE_PATH:/mnt/source:ro" \ + -v "$HOST_CA_CERT:$CONTAINER_CA_CERT:ro" \ + -e TZ=America/New_York \ + -e COMPOSE_VERSION="$COMPOSE_VERSION" \ + -e RESVG_VERSION="$RESVG_VERSION" \ + -e RESVG_SHA256="$RESVG_SHA256" \ + -e OUTPUT_FOLDER=/mnt/output \ + -e DOWNLOAD_CACHE_DIR=/mnt/cache \ + -e PKG_VERSION="$VERSION" \ + -e PKG_BUILD="$BUILD_NUM" \ + -e CA_CERT="$CONTAINER_CA_CERT" \ + vbatts/slackware:latest \ + /mnt/source/pkg_build.sh; then + echo "Docker build failed."; exit 1 + fi + else + echo "Direct bind-mount probe failed. Docker is remote or nested; falling back to tar-stream upload." fi -else - echo "Direct mount failed, using tar stream fallback." +fi + +if [[ "$USE_DIRECT_BIND" != true || ! "${build_cmd_direct[@]}" ]]; then + echo "Using tar-stream upload for Docker build (safe for nested Docker and CI)." if ! tar -C "$SOURCE_PATH" -cf - . | docker run --rm --tmpfs /tmp -i \ -v "$HOST_ARCHIVE_PATH:/mnt/output:rw" \ -v "$HOST_CACHE_PATH:/mnt/cache:rw" \ diff --git a/build_in_docker.sh b/build_in_docker.sh index a4a1baa4..a77669ec 100755 --- a/build_in_docker.sh +++ b/build_in_docker.sh @@ -2,7 +2,7 @@ # shellcheck disable=SC1091 SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" [ -f "${SCRIPT_DIR}/versions.env" ] && source "${SCRIPT_DIR}/versions.env" -[ -z "$COMPOSE_VERSION" ] && COMPOSE_VERSION=5.1.2 +[ -z "$COMPOSE_VERSION" ] && COMPOSE_VERSION=5.5.0 [ -z "$PKG_VERSION" ] && PKG_VERSION="$(date +%Y.%m.%d)" [ -z "$PKG_BUILD" ] && PKG_BUILD="$(date +%H%M)" mkdir -p "$PWD/archive/.build-cache" diff --git a/deploy.sh b/deploy.sh index 5e12a25f..86e8fa83 100755 --- a/deploy.sh +++ b/deploy.sh @@ -6,15 +6,38 @@ trap 'echo "ERROR: command failed on line $LINENO" >&2' ERR VERSION="" DEV=false REMOTE_HOSTS=() -USER_NAME="root" -REMOTE_DIR="/tmp" +USER_NAME="${USER_NAME:-root}" +REMOTE_DIR="${REMOTE_DIR:-/tmp}" PACKAGE_PATH="" SKIP_BUILD=false -COMPOSE_VERSION="5.1.2" +COMPOSE_VERSION="" QUICK=false +if [[ -n "${REMOTE_HOSTS:-}" ]]; then + IFS=',' read -r -a REMOTE_HOSTS <<< "${REMOTE_HOSTS}" +fi + SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ARCHIVE_DIR="$SCRIPT_DIR/archive" +ENV_FILE="$SCRIPT_DIR/.env" + +if [[ -f "$SCRIPT_DIR/versions.env" ]]; then + # shellcheck disable=SC1091 + set -a + source "$SCRIPT_DIR/versions.env" + set +a +fi + +if [[ -f "$ENV_FILE" ]]; then + # shellcheck disable=SC1090 + set -a + source "$ENV_FILE" + set +a +fi + +if [[ -z "${COMPOSE_VERSION:-}" ]]; then + COMPOSE_VERSION="5.1.2" +fi in_container=false if [[ -f "/.dockerenv" ]] || grep -qE '/docker|/lxc|/kubepods' /proc/1/cgroup 2>/dev/null; then From fef5e810134977ec705e3e412def88e116957d7c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 7 Sep 2026 01:16:23 +0000 Subject: [PATCH 12/24] chore: update changelog for v2026.09.06.2116 [skip ci] --- compose.manager.plg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compose.manager.plg b/compose.manager.plg index 265bdaa3..97d5962d 100644 --- a/compose.manager.plg +++ b/compose.manager.plg @@ -36,10 +36,10 @@ > -###2026.09.06.1706 -- Minor updates and improvements +###2026.09.06.2116 +- Bug Fixes: enhance environment variable handling in build scripts and workflows - [PR #150](https://github.com/mstrhakr/compose_plugin/pull/150) -- [beta release diff](https://github.com/mstrhakr/compose_plugin/compare/v2026.09.03...v2026.09.06.1706) +- [beta release diff](https://github.com/mstrhakr/compose_plugin/compare/v2026.09.03...v2026.09.06.2116) From c475d9df9c62d58d70eb48848b17693e3ba58272 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 7 Sep 2026 01:17:11 +0000 Subject: [PATCH 13/24] Release v2026.09.06.2116 [skip ci] --- compose.manager.plg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compose.manager.plg b/compose.manager.plg index 97d5962d..861aa69b 100644 --- a/compose.manager.plg +++ b/compose.manager.plg @@ -2,11 +2,11 @@ - + - - + + From be17bf6b879662d6a66bd3047860685d1ca05cd8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 8 Sep 2026 00:56:05 +0000 Subject: [PATCH 14/24] chore: sync pluginURL+README for dev branch [skip ci] --- compose.manager.plg | 2 +- source/compose.manager/README.md | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/compose.manager.plg b/compose.manager.plg index 2fe69f72..861aa69b 100644 --- a/compose.manager.plg +++ b/compose.manager.plg @@ -10,7 +10,7 @@ - + diff --git a/source/compose.manager/README.md b/source/compose.manager/README.md index 42be301a..8baaf817 100644 --- a/source/compose.manager/README.md +++ b/source/compose.manager/README.md @@ -1,3 +1,3 @@ -**Compose Manager Plus** +**Compose Manager Plus (Beta)** A plugin for unRAID that installs Docker Compose and adds a management interface to the web UI. From 1e3fc5b476dcdcbc454105be6e0392eb55b93227 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 8 Sep 2026 00:56:27 +0000 Subject: [PATCH 15/24] chore: update changelog for v2026.09.07.2056 [skip ci] --- compose.manager.plg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compose.manager.plg b/compose.manager.plg index 861aa69b..de5844af 100644 --- a/compose.manager.plg +++ b/compose.manager.plg @@ -36,10 +36,10 @@ > -###2026.09.06.2116 -- Bug Fixes: enhance environment variable handling in build scripts and workflows +###2026.09.07.2056 +- Chores: update changelog for v2026.09.03 [skip ci] - [PR #150](https://github.com/mstrhakr/compose_plugin/pull/150) -- [beta release diff](https://github.com/mstrhakr/compose_plugin/compare/v2026.09.03...v2026.09.06.2116) +- [beta release diff](https://github.com/mstrhakr/compose_plugin/compare/v2026.09.03...v2026.09.07.2056) From 8681080eb5a95f0ca74f68b1981410f0aef99aab Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 8 Sep 2026 00:57:19 +0000 Subject: [PATCH 16/24] Release v2026.09.07.2056 [skip ci] --- compose.manager.plg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compose.manager.plg b/compose.manager.plg index de5844af..d94653d1 100644 --- a/compose.manager.plg +++ b/compose.manager.plg @@ -2,11 +2,11 @@ - + - - + + From 7c76f36d083cef5d3fe204c330be17cb55536edd Mon Sep 17 00:00:00 2001 From: mstrhakr Date: Mon, 7 Sep 2026 21:14:15 -0400 Subject: [PATCH 17/24] fix: enhance logging and background processing in docker_started script --- source/compose.manager/event/docker_started | 61 ++++++++++++--------- tests/unit/compose.bats | 10 ++++ 2 files changed, 46 insertions(+), 25 deletions(-) diff --git a/source/compose.manager/event/docker_started b/source/compose.manager/event/docker_started index 0596b126..bfdbdacd 100755 --- a/source/compose.manager/event/docker_started +++ b/source/compose.manager/event/docker_started @@ -23,29 +23,40 @@ wait_for_docker_daemon() { while [ $waited -lt $DOCKER_WAIT_TIMEOUT ]; do if docker info >/dev/null 2>&1; then - log "Docker daemon is ready after ${waited}s" + log "Docker daemon is ready after ${waited}s" debug return 0 fi if [ $waited -eq 0 ]; then - log "Waiting for Docker daemon to become ready..." + log "Waiting for Docker daemon to become ready..." debug fi sleep $check_interval waited=$((waited + check_interval)) done - log "ERROR: Docker daemon was not ready after ${DOCKER_WAIT_TIMEOUT}s" + log "ERROR: Docker daemon was not ready after ${DOCKER_WAIT_TIMEOUT}s" error return 1 } -# Logging helper — writes to log file and delegates to shared composeLogger for syslog +# Logging helper — writes to log file and delegates to shared composeLogger for syslog. +# Accepts an optional log level so debug output can be enabled without changing callers. log() { - local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $1" - echo "$msg" >> "$LOG_FILE" - composeLogger "$1" info autostart + local msg="$1" + local level="${2:-info}" + local timestamp="[$(date '+%Y-%m-%d %H:%M:%S')] $msg" + echo "$timestamp" >> "$LOG_FILE" + composeLogger "$msg" "$level" autostart } +# Unraid invokes this event hook synchronously. Spawn a background worker so the array +# can finish transitioning to Started while Compose autostart continues in the background. +if [ "${COMPOSE_MANAGER_AUTOSTART_CHILD:-0}" != "1" ]; then + composeLogger "Autostart event received; detaching background worker to avoid blocking array startup" debug autostart + nohup env COMPOSE_MANAGER_AUTOSTART_CHILD=1 "$0" >/dev/null 2>&1 & + exit 0 +fi + # Load the saved UI stack order for the current compose root. # Prints "projectposition" lines when a saved order exists. load_saved_stack_order() { @@ -90,7 +101,7 @@ foreach (array_values($order) as $index => $project) { # Wait for Docker autostart containers to finish starting wait_for_docker_autostart() { - log "Waiting for Docker autostart containers to stabilize..." + log "Waiting for Docker autostart containers to stabilize..." debug local waited=0 local check_interval=5 @@ -111,13 +122,13 @@ wait_for_docker_autostart() { if [ $total_starting -eq 0 ]; then stable_count=$((stable_count + 1)) if [ $stable_count -ge $required_stable ]; then - log "Docker containers stabilized after ${waited}s" + log "Docker containers stabilized after ${waited}s" debug return 0 fi else stable_count=0 if [ $total_starting -ne $last_starting ]; then - log "Waiting for Docker: $total_starting containers still starting..." + log "Waiting for Docker: $total_starting containers still starting..." debug last_starting=$total_starting fi fi @@ -126,7 +137,7 @@ wait_for_docker_autostart() { waited=$((waited + check_interval)) done - log "WARNING: Docker wait timeout after ${DOCKER_WAIT_TIMEOUT}s, proceeding anyway" + log "WARNING: Docker wait timeout after ${DOCKER_WAIT_TIMEOUT}s, proceeding anyway" warning return 1 } @@ -170,7 +181,7 @@ start_stack() { local stack_name="$2" local start_time=$(date +%s) - log "Starting stack: $stack_name" + log "Starting stack: $stack_name" debug local project project=$(basename "$dir") @@ -179,7 +190,7 @@ start_stack() { local -a cmd_args=("$COMPOSE_WRAPPER" -c up) if ! load_compose_action_spec "$COMPOSE_ROOT" "$project" up "$dir"; then local reason="${COMPOSE_SPEC_ERROR_MESSAGE:-compose args provider failed}" - log "ERROR: Failed to resolve compose args for stack '$stack_name': $reason; skipping startup" + log "ERROR: Failed to resolve compose args for stack '$stack_name': $reason; skipping startup" error return 1 fi @@ -227,32 +238,32 @@ start_stack() { local duration=$(($(date +%s) - start_time)) if [ $exit_code -eq 0 ]; then - log "Stack $stack_name started successfully in ${duration}s" + log "Stack $stack_name started successfully in ${duration}s" debug return 0 elif [ $exit_code -eq 124 ]; then - log "ERROR: Stack $stack_name startup timed out after ${STARTUP_TIMEOUT}s" + log "ERROR: Stack $stack_name startup timed out after ${STARTUP_TIMEOUT}s" error return 124 else - log "ERROR: Stack $stack_name failed to start (exit code: $exit_code)" + log "ERROR: Stack $stack_name failed to start (exit code: $exit_code)" error # Log last few lines of output for debugging echo "$output" | tail -5 | while read line; do - [ -n "$line" ] && log " $stack_name: $line" + [ -n "$line" ] && log " $stack_name: $line" debug done return $exit_code fi } # Main execution -log "=== Compose Manager Autostart Begin ===" +log "=== Compose Manager Autostart Begin ===" debug if [ -z "$COMPOSE_ROOT" ] || [ ! -d "$COMPOSE_ROOT" ]; then - log "ERROR: Invalid compose root path: '$COMPOSE_ROOT'" + log "ERROR: Invalid compose root path: '$COMPOSE_ROOT'" error exit 1 fi # Always wait for Docker daemon readiness to avoid early-boot startup races. if ! wait_for_docker_daemon; then - log "=== Compose Manager Autostart Aborted (docker unavailable) ===" + log "=== Compose Manager Autostart Aborted (docker unavailable) ===" error exit 1 fi @@ -299,10 +310,10 @@ done | sort -t$'\t' -k1,1n -k2,2f | cut -f2-)) unset IFS total_stacks=${#sorted_stacks[@]} -log "Found $total_stacks stacks with autostart enabled" +log "Found $total_stacks stacks with autostart enabled" debug if [ $total_stacks -eq 0 ]; then - log "=== Compose Manager Autostart Complete (no stacks) ===" + log "=== Compose Manager Autostart Complete (no stacks) ===" debug exit 0 fi @@ -317,7 +328,7 @@ for stack_entry in "${sorted_stacks[@]}"; do stack_name="${stack_entry#*|}" current=$((current + 1)) - log "[$current/$total_stacks] Processing: $stack_name" + log "[$current/$total_stacks] Processing: $stack_name" debug # Start stack and wait for it to complete before moving to next start_stack "$dir" "$stack_name" @@ -330,8 +341,8 @@ for stack_entry in "${sorted_stacks[@]}"; do esac done -log "=== Compose Manager Autostart Complete ===" -log "Results: $succeeded succeeded, $failed failed, $timedout timed out (of $total_stacks total)" +log "=== Compose Manager Autostart Complete ===" debug +log "Results: $succeeded succeeded, $failed failed, $timedout timed out (of $total_stacks total)" debug # Exit with error if any failed (for logging purposes, doesn't affect Unraid) if [ $failed -gt 0 ] || [ $timedout -gt 0 ]; then diff --git a/tests/unit/compose.bats b/tests/unit/compose.bats index eb9ef3dd..d4fd18e8 100644 --- a/tests/unit/compose.bats +++ b/tests/unit/compose.bats @@ -150,6 +150,16 @@ test_setup() { assert_success } +@test "autostart script detaches from array-start and exits immediately" { + local autostart_script="$BATS_TEST_DIRNAME/../../source/compose.manager/event/docker_started" + run grep -F 'nohup env COMPOSE_MANAGER_AUTOSTART_CHILD=1' "$autostart_script" + assert_success + run grep -F 'COMPOSE_MANAGER_AUTOSTART_CHILD' "$autostart_script" + assert_success + run grep -F 'Autostart event received; detaching background worker' "$autostart_script" + assert_success +} + @test "compose.sh update action pull step uses --ignore-buildable" { # The update action pulls before 'up -d --build'; buildable services are handled by --build run grep -E 'pull --ignore-buildable' "$COMPOSE_SCRIPT" From 77e248215368f9933fd66a327150235ac9801627 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 8 Sep 2026 01:21:36 +0000 Subject: [PATCH 18/24] chore: update changelog for v2026.09.07.2121 [skip ci] --- compose.manager.plg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compose.manager.plg b/compose.manager.plg index d94653d1..0ebda49f 100644 --- a/compose.manager.plg +++ b/compose.manager.plg @@ -36,10 +36,10 @@ > -###2026.09.07.2056 -- Chores: update changelog for v2026.09.03 [skip ci] +###2026.09.07.2121 +- Bug Fixes: enhance logging and background processing in docker_started script - [PR #150](https://github.com/mstrhakr/compose_plugin/pull/150) -- [beta release diff](https://github.com/mstrhakr/compose_plugin/compare/v2026.09.03...v2026.09.07.2056) +- [beta release diff](https://github.com/mstrhakr/compose_plugin/compare/v2026.09.03...v2026.09.07.2121) From f800f17981d1c291460c8d986f07939a64b0a657 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Tue, 8 Sep 2026 01:22:30 +0000 Subject: [PATCH 19/24] Release v2026.09.07.2121 [skip ci] --- compose.manager.plg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compose.manager.plg b/compose.manager.plg index 0ebda49f..6db7fa01 100644 --- a/compose.manager.plg +++ b/compose.manager.plg @@ -2,11 +2,11 @@ - + - - + + From dcf735346e472f1ed3a9eba5595de320a0d3b19f Mon Sep 17 00:00:00 2001 From: mstrhakr Date: Tue, 8 Sep 2026 09:20:59 -0400 Subject: [PATCH 20/24] fix: adjust css so effective command is visible in all themes --- .../compose.manager.settings.page | 19 ++++++++++++++++++- source/compose.manager/sheets/EditorModal.css | 11 +++++++---- 2 files changed, 25 insertions(+), 5 deletions(-) diff --git a/source/compose.manager/compose.manager.settings.page b/source/compose.manager/compose.manager.settings.page index 07bc7c73..8e9b0477 100755 --- a/source/compose.manager/compose.manager.settings.page +++ b/source/compose.manager/compose.manager.settings.page @@ -530,7 +530,24 @@ $acePath = file_exists('/usr/local/emhttp/plugins/dynamix/javascript/ace/ace.js' } .compose-status-info { - color: var(--dynamix-sb-title-text-color); + color: var(--text-color, var(--dynamix-sb-title-text-color, #f2f2f2)); + background: var(--dynamix-sb-body-bg-color, var(--background-color, #111)); + } + + #settings-discovery-mode-badge, + #compose-stack-discovery-mode-badge { + display: inline-block; + color: var(--text-color, var(--dynamix-sb-title-text-color, #f2f2f2)); + background: var(--dynamix-sb-body-bg-color, var(--background-color, #111)); + border: 1px solid var(--border-color, var(--dynamix-box-inner-div-border-color, #3b3b3b)); + } + + #settings-discovery-mode-toggle, + #compose-stack-discovery-mode-toggle { + color: var(--black, #111); + background: var(--brand-orange, #f15a24); + border: 1px solid var(--brand-orange, #f15a24); + box-shadow: none; } #aggressive-mode-wrapper { diff --git a/source/compose.manager/sheets/EditorModal.css b/source/compose.manager/sheets/EditorModal.css index eb158eff..1984db17 100644 --- a/source/compose.manager/sheets/EditorModal.css +++ b/source/compose.manager/sheets/EditorModal.css @@ -306,10 +306,10 @@ } .settings-effective-command-wrap { - border: 1px solid var(--dynamix-box-inner-div-border-color, var(--border-color)); + border: 1px solid var(--border-color, var(--dynamix-box-inner-div-border-color)); border-radius: 6px; padding: 14px 16px; - background-color: var(--dynamix-sb-body-bg-color, var(--background-color)); + background-color: var(--background-color, var(--dynamix-sb-body-bg-color)); box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.04); } @@ -323,8 +323,11 @@ line-height: 1.5; white-space: pre-wrap; word-break: break-all; - color: var(--text-color); - background: transparent; + color: var(--text-color, var(--dynamix-sb-body-text-color, #111)); + background-color: var(--background-color, var(--dynamix-sb-body-bg-color, #fff)); + border: 1px solid var(--border-color, var(--dynamix-box-inner-div-border-color)); + border-radius: 4px; + padding: 8px 10px; text-shadow: none; } From 3c14e08083d3b8db49e1d26e409e146d33eb14d1 Mon Sep 17 00:00:00 2001 From: mstrhakr Date: Wed, 9 Sep 2026 11:02:18 -0400 Subject: [PATCH 21/24] fix: stop forced rebuilds on compose update Issue #149: update path always added --build, which breaks stacks like akvorado that publish an image and also define a build: section. - default update now pulls images normally instead of forcing a local rebuild - add per-stack + global BUILD_ON_UPDATE_DEFAULT toggle for explicit rebuilds - keep rebuilds opt-in for real local-build stacks - add regression coverage for compose file discovery and update build behavior --- .../compose.manager.settings.page | 12 ++ source/compose.manager/default.cfg | 1 + .../include/ComposeManager.php | 9 ++ source/compose.manager/include/Exec.php | 16 +++ source/compose.manager/include/Helpers.php | 28 ++++ .../javascript/composeManagerMain.js | 12 +- source/compose.manager/scripts/compose.sh | 25 +++- .../unit/ComposeProjectDirectoryArgsTest.php | 121 ++++++++++++++++++ tests/unit/compose.bats | 26 +++- 9 files changed, 236 insertions(+), 14 deletions(-) create mode 100644 tests/unit/ComposeProjectDirectoryArgsTest.php diff --git a/source/compose.manager/compose.manager.settings.page b/source/compose.manager/compose.manager.settings.page index 8e9b0477..1fbb14aa 100755 --- a/source/compose.manager/compose.manager.settings.page +++ b/source/compose.manager/compose.manager.settings.page @@ -2763,6 +2763,18 @@ $acePath = file_exists('/usr/local/emhttp/plugins/dynamix/javascript/ace/ace.js'
+
+
_(Rebuild Images on Update by Default)_:
+
+ > +
+ Default behavior for stack Update actions: when enabled, --build is added so services with a build: section are rebuilt from source. + Leave disabled for stacks that publish an image alongside a build: section — those should be pulled, not rebuilt. + This is a global default and can still be overridden per stack. +
+
+
+
_(New Stack Configuration)_

Default settings applied automatically when creating new compose stacks.

diff --git a/source/compose.manager/default.cfg b/source/compose.manager/default.cfg index a8de5c3f..55725a3c 100755 --- a/source/compose.manager/default.cfg +++ b/source/compose.manager/default.cfg @@ -16,6 +16,7 @@ RUN_IN_BACKGROUND_DEFAULT="false" REMOVE_ORPHANS_DEFAULT="false" WAIT_FOR_HEALTHY_DEFAULT="false" WAIT_FOR_HEALTHY_TIMEOUT_DEFAULT="300" +BUILD_ON_UPDATE_DEFAULT="false" DISABLE_ACTION_WARNINGS="false" NEW_STACK_USE_DEFAULT_COMPOSE_FILES="false" NEW_STACK_OVERRIDE_MANAGEMENT_AUTOMATIC="true" diff --git a/source/compose.manager/include/ComposeManager.php b/source/compose.manager/include/ComposeManager.php index fa45e1b2..5f6f8306 100755 --- a/source/compose.manager/include/ComposeManager.php +++ b/source/compose.manager/include/ComposeManager.php @@ -781,6 +781,15 @@ function compose_manager_cpu_spec_count($cpuSpec)
Seconds to wait for the stack to become healthy. Leave empty to use the global default timeout.
+ +
+ + +
Enable only for stacks whose images are built locally. Leave unchecked for stacks that publish an image alongside a build: section — those are pulled instead, matching plain docker compose up.
+
diff --git a/source/compose.manager/include/Exec.php b/source/compose.manager/include/Exec.php index b11ec6d6..afc91688 100644 --- a/source/compose.manager/include/Exec.php +++ b/source/compose.manager/include/Exec.php @@ -1015,6 +1015,10 @@ function composeResolveContainerIcon(string $containerName, string $service, arr $waitTimeoutFile = "$compose_root/$script/wait_timeout"; $waitTimeout = is_file($waitTimeoutFile) ? trim(file_get_contents($waitTimeoutFile)) : ""; + // Get rebuild-on-update override + $buildOnUpdateFile = "$compose_root/$script/build_on_update"; + $buildOnUpdate = is_file($buildOnUpdateFile) ? trim(file_get_contents($buildOnUpdateFile)) : ""; + // Get additional compose files (one path per line) $extraComposeFilesFile = "$compose_root/$script/extra_compose_files"; $extraComposeFiles = is_file($extraComposeFilesFile) ? trim(file_get_contents($extraComposeFilesFile)) : ""; @@ -1104,6 +1108,7 @@ function composeResolveContainerIcon(string $containerName, string $service, arr 'defaultProfile' => $defaultProfile, 'waitForHealthy' => ($waitForHealthy === 'true' || $waitForHealthy === '1'), 'waitTimeout' => $waitTimeout, + 'buildOnUpdate' => ($buildOnUpdate === 'true' || $buildOnUpdate === '1'), 'extraComposeFiles' => $extraComposeFiles, 'composeFileCandidates' => $composeFileCandidates, 'editableComposeFiles' => $stackInfo->getEditableComposeFiles(), @@ -1217,6 +1222,7 @@ function composeResolveContainerIcon(string $containerName, string $service, arr $defaultProfile = isset($_POST['defaultProfile']) ? trim($_POST['defaultProfile']) : ""; $waitForHealthy = isset($_POST['waitForHealthy']) ? strtolower(trim((string) $_POST['waitForHealthy'])) : "false"; $waitTimeout = isset($_POST['waitTimeout']) ? trim((string) $_POST['waitTimeout']) : ""; + $buildOnUpdate = isset($_POST['buildOnUpdate']) ? strtolower(trim((string) $_POST['buildOnUpdate'])) : "false"; $useDefaultComposeFiles = isset($_POST['useDefaultComposeFiles']) && strtolower(trim((string) $_POST['useDefaultComposeFiles'])) === 'true'; @@ -1374,6 +1380,16 @@ function composeResolveContainerIcon(string $containerName, string $service, arr } } + // Set stack rebuild-on-update override + $buildOnUpdateFile = "$compose_root/$script/build_on_update"; + if ($buildOnUpdate === 'true' || $buildOnUpdate === '1') { + file_put_contents($buildOnUpdateFile, 'true'); + } elseif ($buildOnUpdate === 'false' || $buildOnUpdate === '0' || $buildOnUpdate === '') { + if (is_file($buildOnUpdateFile)) { + @unlink($buildOnUpdateFile); + } + } + // Set additional compose files (skipped entirely when the field was not sent) if ($extraComposeFilesProvided) { $extraComposeFilesFile = "$compose_root/$script/extra_compose_files"; diff --git a/source/compose.manager/include/Helpers.php b/source/compose.manager/include/Helpers.php index d16a49ee..94ad947a 100644 --- a/source/compose.manager/include/Helpers.php +++ b/source/compose.manager/include/Helpers.php @@ -207,6 +207,29 @@ function resolveStackWaitSettings(string $stackPath, array $cfg): array return ['enabled' => $enabled, 'timeout' => $timeout, 'stackName' => $stackName]; } +/** + * Resolve whether `update` should rebuild buildable services for a stack. + * + * Compose only builds automatically when an image is missing, so forcing + * `--build` breaks stacks that publish an image alongside an unbuildable + * `build:` section (see issue #149). Opt-in per stack, global default otherwise. + * + * @param array $cfg + */ +function resolveStackBuildOnUpdate(string $stackPath, array $cfg): bool +{ + $buildFile = rtrim($stackPath, '/') . '/build_on_update'; + + if (is_file($buildFile)) { + $raw = trim((string) file_get_contents($buildFile)); + if ($raw !== '') { + return ($raw === 'true' || $raw === '1'); + } + } + + return (($cfg['BUILD_ON_UPDATE_DEFAULT'] ?? 'false') === 'true'); +} + /** * Build and echo a compose command for a single stack. * @@ -238,6 +261,7 @@ function echoComposeCommand($action, array $options = []) $followLogs = !empty($options['followLogs']); $waitForHealthy = false; $waitTimeout = (string) ($cfg['WAIT_FOR_HEALTHY_TIMEOUT_DEFAULT'] ?? '300'); + $buildOnUpdate = ($action === 'update') && resolveStackBuildOnUpdate($path, $cfg); if ($action === 'up') { $resolvedWait = resolveStackWaitSettings($path, $cfg); $waitForHealthy = !empty($resolvedWait['enabled']); @@ -326,6 +350,10 @@ function echoComposeCommand($action, array $options = []) $composeCommand[] = "--follow-logs"; } + if ($buildOnUpdate) { + $composeCommand[] = "--build"; + } + if ($action === 'up' && $waitForHealthy) { if ($followLogs) { composeLogger("Blocked wait-for-healthy with follow logs enabled", ['action' => $action, 'path' => $path], 'user', 'warning', 'compose'); diff --git a/source/compose.manager/javascript/composeManagerMain.js b/source/compose.manager/javascript/composeManagerMain.js index 914e740e..d4b8abde 100644 --- a/source/compose.manager/javascript/composeManagerMain.js +++ b/source/compose.manager/javascript/composeManagerMain.js @@ -1277,7 +1277,7 @@ function initEditorModal() { editorModal.editors['override'] = overrideEditor; // Initialize settings field change tracking - $('#settings-name, #settings-description, #settings-icon-url, #settings-webui-url, #settings-env-path, #settings-default-profile, #settings-wait-for-healthy, #settings-wait-timeout, #settings-external-compose-path, #settings-external-compose-file, #settings-use-default-compose-files').on('input change', function() { + $('#settings-name, #settings-description, #settings-icon-url, #settings-webui-url, #settings-env-path, #settings-default-profile, #settings-wait-for-healthy, #settings-wait-timeout, #settings-build-on-update, #settings-external-compose-path, #settings-external-compose-file, #settings-use-default-compose-files').on('input change', function() { var fieldId = this.id.replace('settings-', ''); var isCheckbox = this.type === 'checkbox'; var currentValue = isCheckbox ? ($(this).is(':checked') ? 'true' : 'false') : $(this).val(); @@ -6294,6 +6294,11 @@ function loadSettingsData(project, projectName) { $('#settings-wait-timeout').val(waitTimeout); editorModal.originalSettings['wait-timeout'] = waitTimeout; + // Rebuild-on-update setting + var buildOnUpdate = response.buildOnUpdate === true || response.buildOnUpdate === 'true' || response.buildOnUpdate === '1'; + $('#settings-build-on-update').prop('checked', buildOnUpdate); + editorModal.originalSettings['build-on-update'] = buildOnUpdate ? 'true' : 'false'; + // Compose file discovery mode var useDefaultComposeFiles = response.useDefaultComposeFiles === true; $('#settings-use-default-compose-files').prop('checked', useDefaultComposeFiles); @@ -6341,6 +6346,7 @@ function loadSettingsData(project, projectName) { $('#settings-default-profile').val(''); $('#settings-wait-for-healthy').prop('checked', false); $('#settings-wait-timeout').val(''); + $('#settings-build-on-update').prop('checked', false); $('#settings-external-compose-path').val(''); $('#settings-external-compose-file').val(''); $('#settings-use-default-compose-files').prop('checked', false); @@ -7293,6 +7299,7 @@ function saveSettings(saveErrors) { var defaultProfile = $('#settings-default-profile').val(); var waitForHealthy = $('#settings-wait-for-healthy').is(':checked') ? 'true' : 'false'; var waitTimeout = $('#settings-wait-timeout').val(); + var buildOnUpdate = $('#settings-build-on-update').is(':checked') ? 'true' : 'false'; var externalComposePath = $('#settings-external-compose-path').val(); var externalComposeFilePath = $('#settings-external-compose-file').val(); var useDefaultComposeFiles = $('#settings-use-default-compose-files').is(':checked') ? 'true' : 'false'; @@ -7307,6 +7314,7 @@ function saveSettings(saveErrors) { defaultProfile: defaultProfile, waitForHealthy: waitForHealthy, waitTimeout: waitTimeout, + buildOnUpdate: buildOnUpdate, externalComposePath: externalComposePath, externalComposeFilePath: externalComposeFilePath, useDefaultComposeFiles: useDefaultComposeFiles @@ -7326,6 +7334,7 @@ function saveSettings(saveErrors) { editorModal.originalSettings['default-profile'] = defaultProfile; editorModal.originalSettings['wait-for-healthy'] = waitForHealthy; editorModal.originalSettings['wait-timeout'] = waitTimeout; + editorModal.originalSettings['build-on-update'] = buildOnUpdate; editorModal.originalSettings['external-compose-path'] = externalComposePath; editorModal.originalSettings['external-compose-file'] = externalComposeFilePath; editorModal.originalSettings['use-default-compose-files'] = useDefaultComposeFiles; @@ -7336,6 +7345,7 @@ function saveSettings(saveErrors) { editorModal.modifiedSettings.delete('default-profile'); editorModal.modifiedSettings.delete('wait-for-healthy'); editorModal.modifiedSettings.delete('wait-timeout'); + editorModal.modifiedSettings.delete('build-on-update'); editorModal.modifiedSettings.delete('external-compose-path'); editorModal.modifiedSettings.delete('external-compose-file'); editorModal.modifiedSettings.delete('use-default-compose-files'); diff --git a/source/compose.manager/scripts/compose.sh b/source/compose.manager/scripts/compose.sh index f2295531..c4e40a5e 100755 --- a/source/compose.manager/scripts/compose.sh +++ b/source/compose.manager/scripts/compose.sh @@ -12,7 +12,7 @@ LOCK_TIMEOUT=${COMPOSE_LOCK_TIMEOUT:-30} LOCK_DIR="/var/run/compose.manager" SHORT=e:,c:,f:,p:,d:,o:,g:,s:,w: -LONG=env,command:,file:,project_name:,project_dir:,override:,profile:,debug,recreate,remove-orphans,stack-path:,workdir:,follow-logs,wait,wait-timeout: +LONG=env,command:,file:,project_name:,project_dir:,override:,profile:,debug,recreate,remove-orphans,stack-path:,workdir:,follow-logs,wait,wait-timeout:,build OPTS=$(getopt -a -n compose --options $SHORT --longoptions $LONG -- "$@") eval set -- "$OPTS" @@ -29,6 +29,7 @@ debug=false follow_logs=false wait_for_healthy=false wait_timeout="" +build_on_update=false lock_fd="" operation_exit_code=0 @@ -204,6 +205,10 @@ do wait_timeout="$2" shift 2 ;; + --build ) + build_on_update=true + shift; + ;; --) shift; break @@ -345,10 +350,18 @@ case $command in ;; update) + up_args=("-d") + pull_args=() + if [ "$build_on_update" = true ]; then + # --ignore-buildable only makes sense when we rebuild those services ourselves. + pull_args+=("--ignore-buildable") + up_args+=("--build") + fi + if [ "$debug" = true ]; then log_msg "DEBUG" "${compose_base[*]} -p $name images -q" - log_msg "DEBUG" "${compose_base[*]} -p $name pull --ignore-buildable" - log_msg "DEBUG" "${compose_base[*]} -p $name up -d --build" + log_msg "DEBUG" "${compose_base[*]} -p $name pull ${pull_args[*]}" + log_msg "DEBUG" "${compose_base[*]} -p $name up ${up_args[*]}" fi # Capture current images for cleanup later @@ -374,9 +387,9 @@ case $command in images=( "${images[@]##sha256:}" ) fi - # Pull latest images (--ignore-buildable: skip services with build sections, they are rebuilt by up --build) + # Pull latest images. Buildable services are only skipped when we rebuild them below. echo "Pulling latest images..." - "${compose_base[@]}" -p "$name" pull --ignore-buildable + "${compose_base[@]}" -p "$name" pull "${pull_args[@]}" pull_exit=$? if [ $pull_exit -ne 0 ]; then @@ -391,7 +404,7 @@ case $command in # Recreate containers with new images echo "" echo "Recreating containers..." - "${compose_base[@]}" -p "$name" up -d --build + "${compose_base[@]}" -p "$name" up "${up_args[@]}" up_exit=$? if [ $up_exit -eq 0 ]; then diff --git a/tests/unit/ComposeProjectDirectoryArgsTest.php b/tests/unit/ComposeProjectDirectoryArgsTest.php new file mode 100644 index 00000000..d8d1d362 --- /dev/null +++ b/tests/unit/ComposeProjectDirectoryArgsTest.php @@ -0,0 +1,121 @@ +stackRoot = sys_get_temp_dir() . '/compose_projdir_test_' . getmypid(); + @mkdir($this->stackRoot . '/docker', 0755, true); + file_put_contents($this->stackRoot . '/docker/docker-compose.yml', "services: {}\n"); + file_put_contents($this->stackRoot . '/docker/docker-compose-ipinfo.yml', "services: {}\n"); + } + + protected function tearDown(): void + { + @unlink($this->stackRoot . '/build_on_update'); + @unlink($this->stackRoot . '/docker/docker-compose.yml'); + @unlink($this->stackRoot . '/docker/docker-compose-ipinfo.yml'); + @rmdir($this->stackRoot . '/docker'); + @rmdir($this->stackRoot); + + parent::tearDown(); + } + + public function testExplicitComposeFilesDoNotEmitProjectDirectory(): void + { + $composeCommand = []; + \appendComposeFileArgs($composeCommand, [ + 'projectDirectory' => $this->stackRoot, + 'useDefaultFileDiscovery' => false, + 'filePaths' => [ + $this->stackRoot . '/docker/docker-compose.yml', + $this->stackRoot . '/docker/docker-compose-ipinfo.yml', + ], + ]); + + $this->assertSame([ + '-f' . $this->stackRoot . '/docker/docker-compose.yml', + '-f' . $this->stackRoot . '/docker/docker-compose-ipinfo.yml', + ], $composeCommand); + } + + public function testDefaultDiscoveryEmitsOnlyProjectDirectory(): void + { + $composeCommand = []; + \appendComposeFileArgs($composeCommand, [ + 'projectDirectory' => $this->stackRoot, + 'useDefaultFileDiscovery' => true, + 'filePaths' => [$this->stackRoot . '/docker/docker-compose.yml'], + ]); + + $this->assertSame(['-w' . $this->stackRoot], $composeCommand); + } + + public function testDefaultDiscoveryWithoutProjectDirectoryEmitsNothing(): void + { + $composeCommand = []; + \appendComposeFileArgs($composeCommand, [ + 'projectDirectory' => '', + 'useDefaultFileDiscovery' => true, + 'filePaths' => [$this->stackRoot . '/docker/docker-compose.yml'], + ]); + + $this->assertSame([], $composeCommand); + } + + public function testNonexistentComposeFilesAreSkipped(): void + { + $composeCommand = []; + \appendComposeFileArgs($composeCommand, [ + 'projectDirectory' => $this->stackRoot, + 'useDefaultFileDiscovery' => false, + 'filePaths' => [$this->stackRoot . '/docker/missing.yml'], + ]); + + $this->assertSame([], $composeCommand); + } + + public function testBuildOnUpdateDefaultsToGlobalSetting(): void + { + $this->assertFalse(\resolveStackBuildOnUpdate($this->stackRoot, [])); + $this->assertFalse(\resolveStackBuildOnUpdate($this->stackRoot, ['BUILD_ON_UPDATE_DEFAULT' => 'false'])); + $this->assertTrue(\resolveStackBuildOnUpdate($this->stackRoot, ['BUILD_ON_UPDATE_DEFAULT' => 'true'])); + } + + public function testBuildOnUpdateStackOverrideWinsOverGlobal(): void + { + $overrideFile = $this->stackRoot . '/build_on_update'; + + file_put_contents($overrideFile, "true\n"); + $this->assertTrue(\resolveStackBuildOnUpdate($this->stackRoot, ['BUILD_ON_UPDATE_DEFAULT' => 'false'])); + + file_put_contents($overrideFile, "false\n"); + $this->assertFalse(\resolveStackBuildOnUpdate($this->stackRoot, ['BUILD_ON_UPDATE_DEFAULT' => 'true'])); + + file_put_contents($overrideFile, ""); + $this->assertTrue(\resolveStackBuildOnUpdate($this->stackRoot, ['BUILD_ON_UPDATE_DEFAULT' => 'true'])); + + @unlink($overrideFile); + } +} diff --git a/tests/unit/compose.bats b/tests/unit/compose.bats index d4fd18e8..5c27c28f 100644 --- a/tests/unit/compose.bats +++ b/tests/unit/compose.bats @@ -160,14 +160,26 @@ test_setup() { assert_success } -@test "compose.sh update action pull step uses --ignore-buildable" { - # The update action pulls before 'up -d --build'; buildable services are handled by --build - run grep -E 'pull --ignore-buildable' "$COMPOSE_SCRIPT" +@test "compose.sh update action only rebuilds when --build is requested" { + # Issue #149: forcing --build breaks stacks that publish an image alongside + # an unbuildable build: section, so the rebuild must be opt-in. + run grep -F 'up -d --build' "$COMPOSE_SCRIPT" + assert_failure + + run grep -F -- '--build )' "$COMPOSE_SCRIPT" + assert_success + + run grep -F 'build_on_update=true' "$COMPOSE_SCRIPT" + assert_success +} + +@test "compose.sh update action pairs --ignore-buildable with the rebuild flag" { + # --ignore-buildable is only correct when we rebuild those services ourselves. + run grep -F 'pull_args+=("--ignore-buildable")' "$COMPOSE_SCRIPT" + assert_success + + run grep -F 'up_args+=("--build")' "$COMPOSE_SCRIPT" assert_success - # Should appear at least twice (pull action + update action) - local count - count=$(grep -cE 'pull --ignore-buildable' "$COMPOSE_SCRIPT") - [ "$count" -ge 2 ] } @test "compose.sh mutating commands have explicit final exit propagation" { From 028a12237112c37a830c7714bc152aff106aeb29 Mon Sep 17 00:00:00 2001 From: mstrhakr Date: Sun, 13 Sep 2026 12:30:17 -0400 Subject: [PATCH 22/24] fix: set remove-orphans checkbox checked state at render time instead of via setTimeout --- .../javascript/composeManagerMain.js | 20 +++++++------------ 1 file changed, 7 insertions(+), 13 deletions(-) diff --git a/source/compose.manager/javascript/composeManagerMain.js b/source/compose.manager/javascript/composeManagerMain.js index d4b8abde..27cc246d 100644 --- a/source/compose.manager/javascript/composeManagerMain.js +++ b/source/compose.manager/javascript/composeManagerMain.js @@ -3884,9 +3884,10 @@ function isStackRunning(project) { return $stackRow.length > 0 && $stackRow.data('isup') == '1'; } -function buildRemoveOrphansCheckboxHtml(checkboxId) { +function buildRemoveOrphansCheckboxHtml(checkboxId, checked) { + var checkedAttr = checked ? ' checked' : ''; return '
' + - '' + + '' + '' + '
'; } @@ -4900,12 +4901,11 @@ function startAllStacks() { '' + '' + ''; - var removeOrphansHtml = buildRemoveOrphansCheckboxHtml('swal-remove-orphans-startall'); - getConfig().then(function(pluginCfg) { var bgDefault = pluginCfg && pluginCfg.RUN_IN_BACKGROUND_DEFAULT === 'true'; var removeOrphansDefault = pluginCfg && pluginCfg.REMOVE_ORPHANS_DEFAULT === 'true'; var disableWarnings = pluginCfg && pluginCfg.DISABLE_ACTION_WARNINGS === 'true'; + var removeOrphansHtml = buildRemoveOrphansCheckboxHtml('swal-remove-orphans-startall', removeOrphansDefault); if (disableWarnings) { executeStartAllStacks({ @@ -4941,8 +4941,6 @@ function startAllStacks() { setTimeout(function() { var $cb = $('#swal-run-bg-startall'); if ($cb.length) $cb.prop('checked', bgDefault); - var $removeCb = $('#swal-remove-orphans-startall'); - if ($removeCb.length) $removeCb.prop('checked', removeOrphansDefault); }, 50); }); } @@ -5038,12 +5036,11 @@ function stopAllStacks() { '' + '' + ''; - var removeOrphansHtml = buildRemoveOrphansCheckboxHtml('swal-remove-orphans-stopall'); - getConfig().then(function(pluginCfg) { var bgDefault = pluginCfg && pluginCfg.RUN_IN_BACKGROUND_DEFAULT === 'true'; var removeOrphansDefault = pluginCfg && pluginCfg.REMOVE_ORPHANS_DEFAULT === 'true'; var disableWarnings = pluginCfg && pluginCfg.DISABLE_ACTION_WARNINGS === 'true'; + var removeOrphansHtml = buildRemoveOrphansCheckboxHtml('swal-remove-orphans-stopall', removeOrphansDefault); if (disableWarnings) { executeStopAllStacks({ @@ -5079,8 +5076,6 @@ function stopAllStacks() { setTimeout(function() { var $cb = $('#swal-run-bg-stopall'); if ($cb.length) $cb.prop('checked', bgDefault); - var $removeCb = $('#swal-remove-orphans-stopall'); - if ($removeCb.length) $removeCb.prop('checked', removeOrphansDefault); }, 50); }); } @@ -5493,7 +5488,6 @@ function renderStackActionDialog(action, displayName, path, profile, containers, var bgDefault = pluginCfg && pluginCfg.RUN_IN_BACKGROUND_DEFAULT === 'true'; removeOrphansDefault = pluginCfg && pluginCfg.REMOVE_ORPHANS_DEFAULT === 'true'; var disableWarnings = pluginCfg && pluginCfg.DISABLE_ACTION_WARNINGS === 'true'; - var stackMismatchDetected = !!showRemoveOrphans; if (disableWarnings) { // In default background mode (warnings disabled and background enabled), don't show toast if background is used @@ -5506,8 +5500,8 @@ function renderStackActionDialog(action, displayName, path, profile, containers, return; } - var removeOrphansChecked = removeOrphansDefault || stackMismatchDetected; - var showRemoveOrphansOption = !!cfg.showRemoveOrphans || stackMismatchDetected; + var removeOrphansChecked = removeOrphansDefault; + var showRemoveOrphansOption = !!cfg.showRemoveOrphans; // Use native swal (SweetAlert 1.x) with callback style swal({ From 324023134934aa231a9b7a5547dd793d28ea7e86 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 13 Sep 2026 20:36:52 +0000 Subject: [PATCH 23/24] chore: update changelog for v2026.09.13.1636 [skip ci] --- compose.manager.plg | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/compose.manager.plg b/compose.manager.plg index 6db7fa01..40635f14 100644 --- a/compose.manager.plg +++ b/compose.manager.plg @@ -36,10 +36,12 @@ > -###2026.09.07.2121 -- Bug Fixes: enhance logging and background processing in docker_started script +###2026.09.13.1636 +- Bug Fixes: set remove-orphans checkbox checked state at render time instead of via setTimeout +- Bug Fixes: stop forced rebuilds on compose update +- Bug Fixes: adjust css so effective command is visible in all themes - [PR #150](https://github.com/mstrhakr/compose_plugin/pull/150) -- [beta release diff](https://github.com/mstrhakr/compose_plugin/compare/v2026.09.03...v2026.09.07.2121) +- [beta release diff](https://github.com/mstrhakr/compose_plugin/compare/v2026.09.03...v2026.09.13.1636) From 3d5ec4cfa26a512e100bf457c7485d8d6ecd0af8 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Sun, 13 Sep 2026 20:37:46 +0000 Subject: [PATCH 24/24] Release v2026.09.13.1636 [skip ci] --- compose.manager.plg | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/compose.manager.plg b/compose.manager.plg index 40635f14..f07f7e1c 100644 --- a/compose.manager.plg +++ b/compose.manager.plg @@ -2,11 +2,11 @@ - + - - + +