From f682a1c61a98b311644558e847ea55aeee35c1e4 Mon Sep 17 00:00:00 2001 From: kube89 <34094131+kube89@users.noreply.github.com> Date: Fri, 23 Jan 2026 23:05:10 +1100 Subject: [PATCH 1/4] add Octopus.Action.Script.PreloadScriptModules for selective Bash module preloading --- .../Scripting/Bash/BashScriptBootstrapper.cs | 24 ++++++++++++++++++- .../Scripting/Bash/BashScriptVariables.cs | 7 ++++++ 2 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 source/Calamari.Common/Features/Scripting/Bash/BashScriptVariables.cs diff --git a/source/Calamari.Common/Features/Scripting/Bash/BashScriptBootstrapper.cs b/source/Calamari.Common/Features/Scripting/Bash/BashScriptBootstrapper.cs index 5822ec1792..db7c33d4e6 100644 --- a/source/Calamari.Common/Features/Scripting/Bash/BashScriptBootstrapper.cs +++ b/source/Calamari.Common/Features/Scripting/Bash/BashScriptBootstrapper.cs @@ -148,7 +148,12 @@ public static (string bootstrapFile, string[] temporaryFiles) PrepareBootstrapFi writer.NewLine = LinuxNewLine; writer.WriteLine("#!/bin/bash"); writer.WriteLine("source \"$(pwd)/" + Path.GetFileName(configurationFile) + "\""); - writer.WriteLine("shift"); // Shift the variable decryption key out of scope of the user script (see: https://github.com/OctopusDeploy/Calamari/pull/773) + writer.WriteLine("shift"); // Shift the variable decryption key out of scope of the user script and script modules (see: https://github.com/OctopusDeploy/Calamari/pull/773) + + var preloadModules = variables.Get(BashScriptVariables.PreloadScriptModules); + if (!string.IsNullOrWhiteSpace(preloadModules)) + PreloadScriptModules(writer, preloadModules, scriptModulePaths); + writer.WriteLine("source \"$(pwd)/" + Path.GetFileName(script.File) + "\" " + script.Parameters); writer.Flush(); } @@ -158,6 +163,23 @@ public static (string bootstrapFile, string[] temporaryFiles) PrepareBootstrapFi return (bootstrapFile, scriptModulePaths); } + + static void PreloadScriptModules(StreamWriter writer, string preloadModules, string[] scriptModulePaths) + { + var modules = preloadModules.Split(new[] { ',', ';' }, StringSplitOptions.RemoveEmptyEntries); + foreach (var module in modules) + { + var sanitizedName = ScriptVariables.FormatScriptName(module.Trim()); + var fileName = $"{sanitizedName}.sh"; + var scriptModule = scriptModulePaths.FirstOrDefault(p => string.Equals(Path.GetFileName(p), fileName, StringComparison.OrdinalIgnoreCase)); + if (scriptModule != null) + { + Log.VerboseFormat("Preloading script module '{0}' by sourcing {1}.", module.Trim(), fileName); + writer.WriteLine("source \"$(pwd)/" + fileName + "\""); + } + } + } + static IEnumerable PrepareScriptModules(IVariables variables, string workingDirectory) { foreach (var variableName in variables.GetNames().Where(ScriptVariables.IsLibraryScriptModule)) diff --git a/source/Calamari.Common/Features/Scripting/Bash/BashScriptVariables.cs b/source/Calamari.Common/Features/Scripting/Bash/BashScriptVariables.cs new file mode 100644 index 0000000000..fead467ec7 --- /dev/null +++ b/source/Calamari.Common/Features/Scripting/Bash/BashScriptVariables.cs @@ -0,0 +1,7 @@ +namespace Calamari.Common.Features.Scripting.Bash +{ + public static class BashScriptVariables + { + public static readonly string PreloadScriptModules = "Octopus.Action.Script.PreloadScriptModules"; + } +} From 5f835873c2281a66d7b37c742cc00afefe780720 Mon Sep 17 00:00:00 2001 From: kube89 <34094131+kube89@users.noreply.github.com> Date: Sat, 24 Jan 2026 00:35:39 +1100 Subject: [PATCH 2/4] add test coverage for the Bash script module preloading functionality --- .../Fixtures/Bash/BashFixture.cs | 105 ++++++++++++++++++ .../Fixtures/Bash/Scripts/preload-modules.sh | 8 ++ 2 files changed, 113 insertions(+) create mode 100644 source/Calamari.Tests/Fixtures/Bash/Scripts/preload-modules.sh diff --git a/source/Calamari.Tests/Fixtures/Bash/BashFixture.cs b/source/Calamari.Tests/Fixtures/Bash/BashFixture.cs index 52bbb0a127..b16ead1839 100644 --- a/source/Calamari.Tests/Fixtures/Bash/BashFixture.cs +++ b/source/Calamari.Tests/Fixtures/Bash/BashFixture.cs @@ -352,6 +352,111 @@ public void ShouldBeAbleToEnumerateVariableValues(FeatureToggle? featureToggle) output.AssertOutput("Key: VariableName`prop`anotherprop` 13, Value: Value`prop`13"); output.AssertOutput($"Key: {specialCharacters}, Value: {specialCharacters}"); } + + [RequiresBashDotExeIfOnWindows] + public void ShouldPreloadScriptModules() + { + var (output, _) = RunScript("preload-modules.sh", + new Dictionary() + { + ["Octopus.Script.Module[test_module]"] = @"function test_function { + echo ""Function from preloaded module"" +} + +export PRELOADED_VAR=""module_value""", + ["Octopus.Script.Module.Language[test_module]"] = "Bash", + ["Octopus.Action.Script.PreloadScriptModules"] = "test_module" + }); + + Assert.Multiple(() => + { + output.AssertSuccess(); + output.AssertOutput("Calling test_function from preloaded module..."); + output.AssertOutput("Function from preloaded module"); + output.AssertOutput("Checking preloaded variable..."); + output.AssertOutput("PRELOADED_VAR=module_value"); + }); + } + + [RequiresBashDotExeIfOnWindows] + public void ShouldPreloadMultipleScriptModulesInOrder() + { + var (output, _) = RunScript("preload-modules.sh", + new Dictionary() + { + ["Octopus.Script.Module[module1]"] = @"function test_function { + echo ""Module 1"" +} + +export PRELOADED_VAR=""value1""", + ["Octopus.Script.Module.Language[module1]"] = "Bash", + ["Octopus.Script.Module[module2]"] = @"function test_function { + echo ""Module 2"" +} + +export PRELOADED_VAR=""value2""", + ["Octopus.Script.Module.Language[module2]"] = "Bash", + // module2 sourced last, so it wins + ["Octopus.Action.Script.PreloadScriptModules"] = "module1,module2" + }); + + Assert.Multiple(() => + { + output.AssertSuccess(); + // Should use module2's function (last wins) + output.AssertOutput("Module 2"); + // Should use module2's variable (last wins) + output.AssertOutput("PRELOADED_VAR=value2"); + }); + } + + [RequiresBashDotExeIfOnWindows] + [TestCase("module1,module2", TestName = "Comma without spaces")] + [TestCase("module1, module2", TestName = "Comma with space")] + [TestCase("module1 , module2", TestName = "Comma with spaces around")] + [TestCase("module1;module2", TestName = "Semicolon without spaces")] + [TestCase("module1; module2", TestName = "Semicolon with space")] + [TestCase("module1 ; module2", TestName = "Semicolon with spaces around")] + [TestCase("module1,module2;module3", TestName = "Mixed delimiters")] + [TestCase("module1, module2; module3", TestName = "Mixed delimiters with spaces")] + [TestCase("module1,,module2", TestName = "Multiple commas (empty entries)")] + [TestCase(" module1 , module2 ", TestName = "Leading and trailing whitespace")] + [TestCase("module1, ,module2", TestName = "Whitespace-only entry")] + public void ShouldHandleVariousDelimiterCombinations(string moduleList) + { + var (output, _) = RunScript("preload-modules.sh", + new Dictionary() + { + ["Octopus.Script.Module[module1]"] = @"function test_function { + echo ""Module 1"" +} + +export PRELOADED_VAR=""from_module1""", + ["Octopus.Script.Module.Language[module1]"] = "Bash", + ["Octopus.Script.Module[module2]"] = @"function test_function { + echo ""Module 2"" +} + +export PRELOADED_VAR=""from_module2""", + ["Octopus.Script.Module.Language[module2]"] = "Bash", + ["Octopus.Script.Module[module3]"] = @"function test_function { + echo ""Module 3"" +} + +export PRELOADED_VAR=""from_module3""", + ["Octopus.Script.Module.Language[module3]"] = "Bash", + ["Octopus.Action.Script.PreloadScriptModules"] = moduleList + }); + + Assert.Multiple(() => + { + output.AssertSuccess(); + // All test cases should successfully parse the module names + // The exact output depends on which modules are specified and in what order + // But all variations should parse correctly without errors + }); + } + } } public static class AdditionalVariablesExtensions diff --git a/source/Calamari.Tests/Fixtures/Bash/Scripts/preload-modules.sh b/source/Calamari.Tests/Fixtures/Bash/Scripts/preload-modules.sh new file mode 100644 index 0000000000..bfe95cae1b --- /dev/null +++ b/source/Calamari.Tests/Fixtures/Bash/Scripts/preload-modules.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +# Test that preloaded modules are available +echo "Calling test_function from preloaded module..." +test_function + +echo "Checking preloaded variable..." +echo "PRELOADED_VAR=$PRELOADED_VAR" From ee6b75b87862216a09ce89d60add8a4135e11b82 Mon Sep 17 00:00:00 2001 From: kube89 <34094131+kube89@users.noreply.github.com> Date: Sat, 24 Jan 2026 10:10:45 +1100 Subject: [PATCH 3/4] improve tests --- .../Fixtures/Bash/BashFixture.cs | 53 ++++++++++++------- 1 file changed, 34 insertions(+), 19 deletions(-) diff --git a/source/Calamari.Tests/Fixtures/Bash/BashFixture.cs b/source/Calamari.Tests/Fixtures/Bash/BashFixture.cs index b16ead1839..070828a62c 100644 --- a/source/Calamari.Tests/Fixtures/Bash/BashFixture.cs +++ b/source/Calamari.Tests/Fixtures/Bash/BashFixture.cs @@ -353,6 +353,7 @@ public void ShouldBeAbleToEnumerateVariableValues(FeatureToggle? featureToggle) output.AssertOutput($"Key: {specialCharacters}, Value: {specialCharacters}"); } + [Test] [RequiresBashDotExeIfOnWindows] public void ShouldPreloadScriptModules() { @@ -378,6 +379,7 @@ public void ShouldPreloadScriptModules() }); } + [Test] [RequiresBashDotExeIfOnWindows] public void ShouldPreloadMultipleScriptModulesInOrder() { @@ -410,19 +412,21 @@ public void ShouldPreloadMultipleScriptModulesInOrder() }); } + [Test] [RequiresBashDotExeIfOnWindows] - [TestCase("module1,module2", TestName = "Comma without spaces")] - [TestCase("module1, module2", TestName = "Comma with space")] - [TestCase("module1 , module2", TestName = "Comma with spaces around")] - [TestCase("module1;module2", TestName = "Semicolon without spaces")] - [TestCase("module1; module2", TestName = "Semicolon with space")] - [TestCase("module1 ; module2", TestName = "Semicolon with spaces around")] - [TestCase("module1,module2;module3", TestName = "Mixed delimiters")] - [TestCase("module1, module2; module3", TestName = "Mixed delimiters with spaces")] - [TestCase("module1,,module2", TestName = "Multiple commas (empty entries)")] - [TestCase(" module1 , module2 ", TestName = "Leading and trailing whitespace")] - [TestCase("module1, ,module2", TestName = "Whitespace-only entry")] - public void ShouldHandleVariousDelimiterCombinations(string moduleList) + [TestCase("module1,module2", "module1,module2")] + [TestCase("module1, module2", "module1,module2")] + [TestCase("module1 , module2", "module1,module2")] + [TestCase("module1;module2", "module1,module2")] + [TestCase("module1; module2", "module1,module2")] + [TestCase("module1 ; module2", "module1,module2")] + [TestCase("module1,module2;module3", "module1,module2,module3")] + [TestCase("module1, module2; module3", "module1,module2,module3")] + [TestCase("module1,,module2", "module1,module2")] + [TestCase(" module1 , module2 ", "module1,module2")] + [TestCase("module1, ,module2", "module1,module2")] + [TestCase("module1,missing_module", "module1")] + public void PreloadModulesShouldHandleVariousDelimiterCombinations(string moduleList, string expectedModules) { var (output, _) = RunScript("preload-modules.sh", new Dictionary() @@ -431,19 +435,31 @@ public void ShouldHandleVariousDelimiterCombinations(string moduleList) echo ""Module 1"" } -export PRELOADED_VAR=""from_module1""", +if [ -z ""$PRELOADED_VAR"" ]; then + export PRELOADED_VAR=""module1"" +else + export PRELOADED_VAR=""$PRELOADED_VAR,module1"" +fi", ["Octopus.Script.Module.Language[module1]"] = "Bash", ["Octopus.Script.Module[module2]"] = @"function test_function { echo ""Module 2"" } -export PRELOADED_VAR=""from_module2""", +if [ -z ""$PRELOADED_VAR"" ]; then + export PRELOADED_VAR=""module2"" +else + export PRELOADED_VAR=""$PRELOADED_VAR,module2"" +fi", ["Octopus.Script.Module.Language[module2]"] = "Bash", ["Octopus.Script.Module[module3]"] = @"function test_function { echo ""Module 3"" } -export PRELOADED_VAR=""from_module3""", +if [ -z ""$PRELOADED_VAR"" ]; then + export PRELOADED_VAR=""module3"" +else + export PRELOADED_VAR=""$PRELOADED_VAR,module3"" +fi", ["Octopus.Script.Module.Language[module3]"] = "Bash", ["Octopus.Action.Script.PreloadScriptModules"] = moduleList }); @@ -451,12 +467,11 @@ public void ShouldHandleVariousDelimiterCombinations(string moduleList) Assert.Multiple(() => { output.AssertSuccess(); - // All test cases should successfully parse the module names - // The exact output depends on which modules are specified and in what order - // But all variations should parse correctly without errors + + // Verify expected modules were loaded in the correct order + output.AssertOutput($"PRELOADED_VAR={expectedModules}"); }); } - } } public static class AdditionalVariablesExtensions From e6859b2b149fe57fba9fb0fb50e612f37e6d6653 Mon Sep 17 00:00:00 2001 From: kube89 <34094131+kube89@users.noreply.github.com> Date: Sat, 24 Jan 2026 19:44:51 +1100 Subject: [PATCH 4/4] Change log message. Log prior already specifies filename. Just added noise to logs. --- .../Features/Scripting/Bash/BashScriptBootstrapper.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/source/Calamari.Common/Features/Scripting/Bash/BashScriptBootstrapper.cs b/source/Calamari.Common/Features/Scripting/Bash/BashScriptBootstrapper.cs index db7c33d4e6..f97b2dd043 100644 --- a/source/Calamari.Common/Features/Scripting/Bash/BashScriptBootstrapper.cs +++ b/source/Calamari.Common/Features/Scripting/Bash/BashScriptBootstrapper.cs @@ -174,7 +174,7 @@ static void PreloadScriptModules(StreamWriter writer, string preloadModules, str var scriptModule = scriptModulePaths.FirstOrDefault(p => string.Equals(Path.GetFileName(p), fileName, StringComparison.OrdinalIgnoreCase)); if (scriptModule != null) { - Log.VerboseFormat("Preloading script module '{0}' by sourcing {1}.", module.Trim(), fileName); + Log.VerboseFormat("Preloading script module '{0}'.", module.Trim()); writer.WriteLine("source \"$(pwd)/" + fileName + "\""); } }