diff --git a/registry/coder/modules/coder-utils/README.md b/registry/coder/modules/coder-utils/README.md index 25ae28544..07eb223a6 100644 --- a/registry/coder/modules/coder-utils/README.md +++ b/registry/coder/modules/coder-utils/README.md @@ -20,11 +20,11 @@ The Coder Utils module is a building block for modules that need to run multiple ```tf module "coder_utils" { source = "registry.coder.com/coder/coder-utils/coder" - version = "1.0.1" + version = "1.1.0" - agent_id = coder_agent.main.id - agent_name = "myagent" - module_dir_name = ".my-module" + agent_id = coder_agent.main.id + agent_name = "myagent" + module_directory = ".my-module" pre_install_script = <<-EOT #!/bin/bash @@ -56,10 +56,30 @@ module "coder_utils" { The module orchestrates scripts in the following order: -1. **Log File Creation** - Creates module directory and log files -2. **Pre-Install Script** (optional) - Runs before installation -3. **Install Script** - Main installation -4. **Post-Install Script** (optional) - Runs after installation -5. **Start Script** - Starts the application +1. **Pre-Install Script** (optional) - Runs before installation +2. **Install Script** (required) - Main installation +3. **Post-Install Script** (optional) - Runs after installation +4. **Start Script** (optional) - Starts the application Each script waits for its prerequisites to complete before running using `coder exp sync` dependency management. + +## Customizing Script Display + +By default each `coder_script` renders in the Coder UI as plain "Install Script", "Pre-Install Script", etc. Downstream modules can brand them: + +```tf +module "coder_utils" { + source = "registry.coder.com/coder/coder-utils/coder" + version = "1.1.0" + + agent_id = coder_agent.main.id + agent_name = "myagent" + module_directory = ".my-module" + install_script = "echo installing" + + display_name_prefix = "Claude Code" # yields "Claude Code: Install Script", etc. + icon = "/icon/claude.svg" +} +``` + +Both variables are optional. `display_name_prefix` defaults to `""` (no prefix), and `icon` defaults to `null` (use the Coder provider's default). diff --git a/registry/coder/modules/coder-utils/main.test.ts b/registry/coder/modules/coder-utils/main.test.ts index 5987c3288..a7e8e2e9f 100644 --- a/registry/coder/modules/coder-utils/main.test.ts +++ b/registry/coder/modules/coder-utils/main.test.ts @@ -7,7 +7,7 @@ describe("coder-utils", async () => { testRequiredVariables(import.meta.dir, { agent_id: "test-agent-id", agent_name: "test-agent", - module_dir_name: ".test-module", - start_script: "echo 'start'", + module_directory: ".test-module", + install_script: "echo 'install'", }); }); diff --git a/registry/coder/modules/coder-utils/main.tf b/registry/coder/modules/coder-utils/main.tf index cfb8b778a..126496fb6 100644 --- a/registry/coder/modules/coder-utils/main.tf +++ b/registry/coder/modules/coder-utils/main.tf @@ -29,7 +29,6 @@ variable "pre_install_script" { variable "install_script" { type = string description = "Script to install the agent used by AgentAPI." - default = null } variable "post_install_script" { @@ -41,6 +40,7 @@ variable "post_install_script" { variable "start_script" { type = string description = "Script that starts AgentAPI." + default = null } variable "agent_name" { @@ -49,46 +49,67 @@ variable "agent_name" { } -variable "module_dir_name" { +variable "module_directory" { + type = string + description = "The module's working directory for scripts and logs." +} + +variable "display_name_prefix" { type = string - description = "The name of the module directory." + description = "Prefix for each coder_script display_name. Example: setting 'Claude Code' yields 'Claude Code: Install Script', 'Claude Code: Pre-Install Script', etc. When unset, scripts show as plain 'Install Script'." + default = "" +} + +variable "icon" { + type = string + description = "Icon shown in the Coder UI for every coder_script this module creates. Falls back to the Coder provider's default when unset." + default = null } locals { encoded_pre_install_script = var.pre_install_script != null ? base64encode(var.pre_install_script) : "" - encoded_install_script = var.install_script != null ? base64encode(var.install_script) : "" + encoded_install_script = base64encode(var.install_script) encoded_post_install_script = var.post_install_script != null ? base64encode(var.post_install_script) : "" - encoded_start_script = base64encode(var.start_script) + encoded_start_script = var.start_script != null ? base64encode(var.start_script) : "" pre_install_script_name = "${var.agent_name}-pre_install_script" install_script_name = "${var.agent_name}-install_script" post_install_script_name = "${var.agent_name}-post_install_script" start_script_name = "${var.agent_name}-start_script" - module_dir_path = "$HOME/${var.module_dir_name}" + pre_install_path = "${var.module_directory}/pre_install.sh" + install_path = "${var.module_directory}/install.sh" + post_install_path = "${var.module_directory}/post_install.sh" + start_path = "${var.module_directory}/start.sh" - pre_install_path = "${local.module_dir_path}/pre_install.sh" - install_path = "${local.module_dir_path}/install.sh" - post_install_path = "${local.module_dir_path}/post_install.sh" - start_path = "${local.module_dir_path}/start.sh" + pre_install_log_path = "${var.module_directory}/pre_install.log" + install_log_path = "${var.module_directory}/install.log" + post_install_log_path = "${var.module_directory}/post_install.log" + start_log_path = "${var.module_directory}/start.log" - pre_install_log_path = "${local.module_dir_path}/pre_install.log" - install_log_path = "${local.module_dir_path}/install.log" - post_install_log_path = "${local.module_dir_path}/post_install.log" - start_log_path = "${local.module_dir_path}/start.log" + install_sync_deps = var.pre_install_script != null ? local.pre_install_script_name : null + + start_sync_deps = ( + var.post_install_script != null + ? "${local.install_script_name} ${local.post_install_script_name}" + : local.install_script_name + ) + + display_name_prefix = var.display_name_prefix != "" ? "${var.display_name_prefix}: " : "" } resource "coder_script" "pre_install_script" { count = var.pre_install_script == null ? 0 : 1 agent_id = var.agent_id - display_name = "Pre-Install Script" + display_name = "${local.display_name_prefix}Pre-Install Script" + icon = var.icon run_on_start = true script = <<-EOT #!/bin/bash set -o errexit set -o pipefail - mkdir -p ${local.module_dir_path} + mkdir -p ${var.module_directory} trap 'coder exp sync complete ${local.pre_install_script_name}' EXIT coder exp sync start ${local.pre_install_script_name} @@ -96,37 +117,39 @@ resource "coder_script" "pre_install_script" { echo -n '${local.encoded_pre_install_script}' | base64 -d > ${local.pre_install_path} chmod +x ${local.pre_install_path} - ${local.pre_install_path} > ${local.pre_install_log_path} 2>&1 + ${local.pre_install_path} 2>&1 | tee ${local.pre_install_log_path} EOT } resource "coder_script" "install_script" { agent_id = var.agent_id - display_name = "Install Script" + display_name = "${local.display_name_prefix}Install Script" + icon = var.icon run_on_start = true script = <<-EOT #!/bin/bash set -o errexit set -o pipefail - mkdir -p ${local.module_dir_path} + mkdir -p ${var.module_directory} trap 'coder exp sync complete ${local.install_script_name}' EXIT - %{if var.pre_install_script != null~} - coder exp sync want ${local.install_script_name} ${local.pre_install_script_name} + %{if local.install_sync_deps != null~} + coder exp sync want ${local.install_script_name} ${local.install_sync_deps} %{endif~} coder exp sync start ${local.install_script_name} echo -n '${local.encoded_install_script}' | base64 -d > ${local.install_path} chmod +x ${local.install_path} - ${local.install_path} > ${local.install_log_path} 2>&1 + ${local.install_path} 2>&1 | tee ${local.install_log_path} EOT } resource "coder_script" "post_install_script" { count = var.post_install_script != null ? 1 : 0 agent_id = var.agent_id - display_name = "Post-Install Script" + display_name = "${local.display_name_prefix}Post-Install Script" + icon = var.icon run_on_start = true script = <<-EOT #!/bin/bash @@ -140,13 +163,15 @@ resource "coder_script" "post_install_script" { echo -n '${local.encoded_post_install_script}' | base64 -d > ${local.post_install_path} chmod +x ${local.post_install_path} - ${local.post_install_path} > ${local.post_install_log_path} 2>&1 + ${local.post_install_path} 2>&1 | tee ${local.post_install_log_path} EOT } resource "coder_script" "start_script" { + count = var.start_script != null ? 1 : 0 agent_id = var.agent_id - display_name = "Start Script" + display_name = "${local.display_name_prefix}Start Script" + icon = var.icon run_on_start = true script = <<-EOT #!/bin/bash @@ -155,36 +180,28 @@ resource "coder_script" "start_script" { trap 'coder exp sync complete ${local.start_script_name}' EXIT - %{if var.post_install_script != null~} - coder exp sync want ${local.start_script_name} ${local.install_script_name} ${local.post_install_script_name} - %{else~} - coder exp sync want ${local.start_script_name} ${local.install_script_name} - %{endif~} + coder exp sync want ${local.start_script_name} ${local.start_sync_deps} coder exp sync start ${local.start_script_name} echo -n '${local.encoded_start_script}' | base64 -d > ${local.start_path} chmod +x ${local.start_path} - ${local.start_path} > ${local.start_log_path} 2>&1 + ${local.start_path} 2>&1 | tee ${local.start_log_path} EOT } -output "pre_install_script_name" { - description = "The name of the pre-install script for sync." - value = local.pre_install_script_name -} - -output "install_script_name" { - description = "The name of the install script for sync." - value = local.install_script_name -} - -output "post_install_script_name" { - description = "The name of the post-install script for sync." - value = local.post_install_script_name +# Filtered, run-order list of the `coder exp sync` names for every +# coder_script this module actually creates. Absent scripts (pre/post/start +# when their inputs are null) are omitted entirely, not padded with empty +# strings. Downstream modules can use this with +# `coder exp sync want ` to serialize their own +# scripts behind the install pipeline. +output "scripts" { + description = "Ordered list of `coder exp sync` names for the coder_script resources this module creates, in the run order it enforces (pre_install, install, post_install, start). Scripts that were not configured are absent from the list." + value = concat( + var.pre_install_script != null ? [local.pre_install_script_name] : [], + [local.install_script_name], + var.post_install_script != null ? [local.post_install_script_name] : [], + var.start_script != null ? [local.start_script_name] : [], + ) } - -output "start_script_name" { - description = "The name of the start script for sync." - value = local.start_script_name -} \ No newline at end of file diff --git a/registry/coder/modules/coder-utils/main.tftest.hcl b/registry/coder/modules/coder-utils/main.tftest.hcl index d1228a6d4..40aeeb41f 100644 --- a/registry/coder/modules/coder-utils/main.tftest.hcl +++ b/registry/coder/modules/coder-utils/main.tftest.hcl @@ -7,7 +7,7 @@ run "test_with_all_scripts" { variables { agent_id = "test-agent-id" agent_name = "test-agent" - module_dir_name = ".test-module" + module_directory = ".test-module" pre_install_script = "echo 'pre-install'" install_script = "echo 'install'" post_install_script = "echo 'post-install'" @@ -35,7 +35,7 @@ run "test_with_all_scripts" { error_message = "Pre-install script should run on start" } - # Verify install_script is created + # Verify install_script is always created assert { condition = coder_script.install_script.agent_id == "test-agent-id" error_message = "Install script agent ID should match input" @@ -51,6 +51,12 @@ run "test_with_all_scripts" { error_message = "Install script should run on start" } + # install should sync-want pre_install + assert { + condition = can(regex("sync want test-agent-install_script test-agent-pre_install_script", coder_script.install_script.script)) + error_message = "Install script should sync-want pre_install_script when pre_install is provided" + } + # Verify post_install_script is created when provided assert { condition = length(coder_script.post_install_script) == 1 @@ -72,98 +78,103 @@ run "test_with_all_scripts" { error_message = "Post-install script should run on start" } - # Verify start_script is created + # Verify start_script is created when provided + assert { + condition = length(coder_script.start_script) == 1 + error_message = "Start script should be created when start_script is provided" + } + assert { - condition = coder_script.start_script.agent_id == "test-agent-id" + condition = coder_script.start_script[0].agent_id == "test-agent-id" error_message = "Start script agent ID should match input" } assert { - condition = coder_script.start_script.display_name == "Start Script" + condition = coder_script.start_script[0].display_name == "Start Script" error_message = "Start script should have correct display name" } assert { - condition = coder_script.start_script.run_on_start == true + condition = coder_script.start_script[0].run_on_start == true error_message = "Start script should run on start" } +} + +# Test with only install_script (minimum required input) +run "test_install_only" { + command = plan - # Verify outputs for script names + variables { + agent_id = "test-agent-id" + agent_name = "test-agent" + module_directory = ".test-module" + install_script = "echo 'install'" + } + + # Verify optional scripts are NOT created assert { - condition = output.pre_install_script_name == "test-agent-pre_install_script" - error_message = "Pre-install script name output should be correctly formatted" + condition = length(coder_script.pre_install_script) == 0 + error_message = "Pre-install script should not be created when not provided" } assert { - condition = output.install_script_name == "test-agent-install_script" - error_message = "Install script name output should be correctly formatted" + condition = length(coder_script.post_install_script) == 0 + error_message = "Post-install script should not be created when not provided" } assert { - condition = output.post_install_script_name == "test-agent-post_install_script" - error_message = "Post-install script name output should be correctly formatted" + condition = length(coder_script.start_script) == 0 + error_message = "Start script should not be created when not provided" } + # Verify install_script is created assert { - condition = output.start_script_name == "test-agent-start_script" - error_message = "Start script name output should be correctly formatted" + condition = coder_script.install_script.agent_id == "test-agent-id" + error_message = "Install script should be created" } } -# Test with only required scripts (no pre/post install) -run "test_without_optional_scripts" { +# Test with install and start scripts (no pre/post install) +run "test_install_and_start" { command = plan variables { - agent_id = "test-agent-id" - agent_name = "test-agent" - module_dir_name = ".test-module" - install_script = "echo 'install'" - start_script = "echo 'start'" + agent_id = "test-agent-id" + agent_name = "test-agent" + module_directory = ".test-module" + install_script = "echo 'install'" + start_script = "echo 'start'" } - # Verify pre_install_script is NOT created when not provided assert { condition = length(coder_script.pre_install_script) == 0 - error_message = "Pre-install script should not be created when pre_install_script is null" + error_message = "Pre-install script should not be created when not provided" } - # Verify post_install_script is NOT created when not provided assert { condition = length(coder_script.post_install_script) == 0 - error_message = "Post-install script should not be created when post_install_script is null" + error_message = "Post-install script should not be created when not provided" } - # Verify required scripts are still created assert { condition = coder_script.install_script.agent_id == "test-agent-id" error_message = "Install script should be created" } assert { - condition = coder_script.start_script.agent_id == "test-agent-id" + condition = length(coder_script.start_script) == 1 error_message = "Start script should be created" } - # Verify outputs assert { - condition = output.pre_install_script_name == "test-agent-pre_install_script" - error_message = "Pre-install script name output should be generated even when script is not created" - } - - assert { - condition = output.install_script_name == "test-agent-install_script" - error_message = "Install script name output should be correctly formatted" - } - - assert { - condition = output.post_install_script_name == "test-agent-post_install_script" - error_message = "Post-install script name output should be generated even when script is not created" + condition = coder_script.start_script[0].agent_id == "test-agent-id" + error_message = "Start script agent ID should match input" } + # start should sync-want install (no post_install) assert { - condition = output.start_script_name == "test-agent-start_script" - error_message = "Start script name output should be correctly formatted" + condition = can(regex("sync want test-agent-start_script test-agent-install_script", coder_script.start_script[0].script)) + error_message = "Start script should sync-want install_script" } } @@ -172,14 +183,13 @@ run "test_with_mock_data" { command = plan variables { - agent_id = "mock-agent" - agent_name = "mock-agent" - module_dir_name = ".mock-module" - install_script = "echo 'install'" - start_script = "echo 'start'" + agent_id = "mock-agent" + agent_name = "mock-agent" + module_directory = ".mock-module" + install_script = "echo 'install'" + start_script = "echo 'start'" } - # Mock the data sources for testing override_data { target = data.coder_workspace.me values = { @@ -212,14 +222,13 @@ run "test_with_mock_data" { } } - # Verify scripts are created with mocked data assert { condition = coder_script.install_script.agent_id == "mock-agent" error_message = "Install script should use the mocked agent ID" } assert { - condition = coder_script.start_script.agent_id == "mock-agent" + condition = coder_script.start_script[0].agent_id == "mock-agent" error_message = "Start script should use the mocked agent ID" } } @@ -229,43 +238,294 @@ run "test_script_naming" { command = plan variables { - agent_id = "test-agent" - agent_name = "custom-name" - module_dir_name = ".test-module" - install_script = "echo 'install'" - start_script = "echo 'start'" + agent_id = "test-agent" + agent_name = "custom-name" + module_directory = ".test-module" + install_script = "echo 'install'" + start_script = "echo 'start'" } - # Verify script names are constructed correctly - # The script should contain references to custom-name-* in the sync commands assert { condition = can(regex("custom-name-install_script", coder_script.install_script.script)) error_message = "Install script should use custom agent_name in sync commands" } assert { - condition = can(regex("custom-name-start_script", coder_script.start_script.script)) + condition = can(regex("custom-name-start_script", coder_script.start_script[0].script)) error_message = "Start script should use custom agent_name in sync commands" } +} + +# Test install syncs with pre_install when provided +run "test_install_syncs_with_pre_install" { + command = plan + + variables { + agent_id = "test-agent-id" + agent_name = "test-agent" + module_directory = ".test-module" + pre_install_script = "echo 'pre-install'" + install_script = "echo 'install'" + } + + assert { + condition = length(coder_script.pre_install_script) == 1 + error_message = "Pre-install script should be created" + } + + assert { + condition = can(regex("sync want test-agent-install_script test-agent-pre_install_script", coder_script.install_script.script)) + error_message = "Install script should sync-want pre_install_script" + } +} + +# Test start script sync deps with post_install present +run "test_start_syncs_with_post_install" { + command = plan + + variables { + agent_id = "test-agent-id" + agent_name = "test-agent" + module_directory = ".test-module" + install_script = "echo 'install'" + post_install_script = "echo 'post-install'" + start_script = "echo 'start'" + } + + # start should sync-want both install and post_install + assert { + condition = can(regex("sync want test-agent-start_script test-agent-install_script test-agent-post_install_script", coder_script.start_script[0].script)) + error_message = "Start script should sync-want both install_script and post_install_script" + } + + # post_install should sync-want install + assert { + condition = can(regex("sync want test-agent-post_install_script test-agent-install_script", coder_script.post_install_script[0].script)) + error_message = "Post-install script should sync-want install_script" + } +} + +# Verify display_name_prefix is prepended to every script's display_name +run "test_display_name_prefix_applied" { + command = plan + + variables { + agent_id = "test-agent-id" + agent_name = "test-agent" + module_directory = ".test-module" + display_name_prefix = "Claude Code" + pre_install_script = "echo 'pre-install'" + install_script = "echo 'install'" + post_install_script = "echo 'post-install'" + start_script = "echo 'start'" + } + + assert { + condition = coder_script.pre_install_script[0].display_name == "Claude Code: Pre-Install Script" + error_message = "Pre-install script display_name should be prefixed" + } + + assert { + condition = coder_script.install_script.display_name == "Claude Code: Install Script" + error_message = "Install script display_name should be prefixed" + } + + assert { + condition = coder_script.post_install_script[0].display_name == "Claude Code: Post-Install Script" + error_message = "Post-install script display_name should be prefixed" + } + + assert { + condition = coder_script.start_script[0].display_name == "Claude Code: Start Script" + error_message = "Start script display_name should be prefixed" + } +} + +# Verify icon is propagated to every coder_script +run "test_icon_applied" { + command = plan + + variables { + agent_id = "test-agent-id" + agent_name = "test-agent" + module_directory = ".test-module" + icon = "/icon/claude.svg" + pre_install_script = "echo 'pre-install'" + install_script = "echo 'install'" + post_install_script = "echo 'post-install'" + start_script = "echo 'start'" + } + + assert { + condition = coder_script.pre_install_script[0].icon == "/icon/claude.svg" + error_message = "Pre-install script icon should match input" + } + + assert { + condition = coder_script.install_script.icon == "/icon/claude.svg" + error_message = "Install script icon should match input" + } + + assert { + condition = coder_script.post_install_script[0].icon == "/icon/claude.svg" + error_message = "Post-install script icon should match input" + } + + assert { + condition = coder_script.start_script[0].icon == "/icon/claude.svg" + error_message = "Start script icon should match input" + } +} + +# Verify optional scripts are not created when their variables are unset +run "test_optional_scripts_absent_by_default" { + command = plan + + variables { + agent_id = "test-agent-id" + agent_name = "test-agent" + module_directory = ".test-module" + install_script = "echo install" + } + + assert { + condition = length(coder_script.pre_install_script) == 0 + error_message = "Pre-install coder_script should not be created when pre_install_script is unset" + } + + assert { + condition = length(coder_script.post_install_script) == 0 + error_message = "Post-install coder_script should not be created when post_install_script is unset" + } + + assert { + condition = length(coder_script.start_script) == 0 + error_message = "Start coder_script should not be created when start_script is unset" + } +} + +# Verify `scripts` output is a filtered, run-order list +run "test_scripts_output_with_all" { + command = plan + + variables { + agent_id = "test-agent-id" + agent_name = "test-agent" + module_directory = ".test-module" + pre_install_script = "echo pre" + install_script = "echo install" + post_install_script = "echo post" + start_script = "echo start" + } + + assert { + condition = length(output.scripts) == 4 + error_message = "scripts should have 4 entries when every script is set" + } + + assert { + condition = output.scripts[0] == "test-agent-pre_install_script" + error_message = "scripts[0] must be the pre-install name" + } + + assert { + condition = output.scripts[1] == "test-agent-install_script" + error_message = "scripts[1] must be the install name" + } + + assert { + condition = output.scripts[2] == "test-agent-post_install_script" + error_message = "scripts[2] must be the post-install name" + } + + assert { + condition = output.scripts[3] == "test-agent-start_script" + error_message = "scripts[3] must be the start name" + } +} + +run "test_scripts_output_with_install_only" { + command = plan + + variables { + agent_id = "test-agent-id" + agent_name = "test-agent" + module_directory = ".test-module" + install_script = "echo install" + } + + assert { + condition = length(output.scripts) == 1 + error_message = "scripts should have exactly 1 entry (install) when pre/post/start are unset" + } + + assert { + condition = output.scripts[0] == "test-agent-install_script" + error_message = "scripts[0] must be the install name" + } +} + +run "test_scripts_output_with_install_and_post" { + command = plan + + variables { + agent_id = "test-agent-id" + agent_name = "test-agent" + module_directory = ".test-module" + install_script = "echo install" + post_install_script = "echo post" + } + + assert { + condition = length(output.scripts) == 2 + error_message = "scripts should have 2 entries (install, post)" + } + + assert { + condition = output.scripts[0] == "test-agent-install_script" + error_message = "scripts[0] must be the install name" + } + + assert { + condition = output.scripts[1] == "test-agent-post_install_script" + error_message = "scripts[1] must be the post-install name" + } +} + +# Every script must stream combined stdout+stderr to both the agent log +# (via stdout) and the on-disk log file (via tee), so workspace users +# watching `coder_script` output in the UI see progress live and can +# read the same content from the log file after the fact. +run "test_scripts_tee_stdout_and_log_file" { + command = plan + + variables { + agent_id = "test-agent-id" + agent_name = "test-agent" + module_directory = ".test-module" + pre_install_script = "echo pre" + install_script = "echo install" + post_install_script = "echo post" + start_script = "echo start" + } - # Verify outputs use custom agent_name assert { - condition = output.pre_install_script_name == "custom-name-pre_install_script" - error_message = "Pre-install script name output should use custom agent_name" + condition = can(regex("pre_install.sh 2>&1 \\| tee .*pre_install.log", coder_script.pre_install_script[0].script)) + error_message = "pre_install wrapper must tee combined output to the log file and stdout" } assert { - condition = output.install_script_name == "custom-name-install_script" - error_message = "Install script name output should use custom agent_name" + condition = can(regex("install.sh 2>&1 \\| tee .*install.log", coder_script.install_script.script)) + error_message = "install wrapper must tee combined output to the log file and stdout" } assert { - condition = output.post_install_script_name == "custom-name-post_install_script" - error_message = "Post-install script name output should use custom agent_name" + condition = can(regex("post_install.sh 2>&1 \\| tee .*post_install.log", coder_script.post_install_script[0].script)) + error_message = "post_install wrapper must tee combined output to the log file and stdout" } assert { - condition = output.start_script_name == "custom-name-start_script" - error_message = "Start script name output should use custom agent_name" + condition = can(regex("start.sh 2>&1 \\| tee .*start.log", coder_script.start_script[0].script)) + error_message = "start wrapper must tee combined output to the log file and stdout" } }