feat(nodejs): add pre and post install scripts with coder exp sync support

Add pre_install_script and post_install_script variables to the nodejs
module following the pattern used by other registry modules (agent-helper,
claude-code, aider, etc.). Scripts use coder exp sync for reliable
execution ordering, enabling dependency coordination between modules.

Changes:
- Add pre_install_script and post_install_script optional variables
- Wrap install script with coder exp sync want/start/complete
- Add conditional pre/post install coder_script resources
- Export sync script names as outputs for cross-module coordination
- Add nodejs.tftest.hcl with 5 test cases
- Update README with pre/post install documentation and examples
- Bump version references to 1.0.14
This commit is contained in:
blink-so[bot] 2026-03-13 14:07:52 +00:00
parent 9606297620
commit 79cef2ecfc
4 changed files with 273 additions and 18 deletions

View File

@ -15,7 +15,7 @@ Automatically installs [Node.js](https://github.com/nodejs/node) via [`nvm`](htt
module "nodejs" { module "nodejs" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/thezoker/nodejs/coder" source = "registry.coder.com/thezoker/nodejs/coder"
version = "1.0.13" version = "1.0.14"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
} }
``` ```
@ -28,17 +28,35 @@ This installs multiple versions of Node.js:
module "nodejs" { module "nodejs" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/thezoker/nodejs/coder" source = "registry.coder.com/thezoker/nodejs/coder"
version = "1.0.13" version = "1.0.14"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
node_versions = [ node_versions = [
"18", "18",
"20", "20",
"node" "node"
] ]
default_node_version = "1.0.13" default_node_version = "20"
} }
``` ```
## Pre and Post Install Scripts
Use `pre_install_script` and `post_install_script` to run custom scripts before and after Node.js installation. These use `coder exp sync` for reliable script ordering, making them useful for dependency coordination between modules.
```tf
module "nodejs" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/thezoker/nodejs/coder"
version = "1.0.14"
agent_id = coder_agent.example.id
pre_install_script = "echo 'Setting up prerequisites...'"
post_install_script = "npm install -g yarn pnpm"
}
```
The module exports sync script names (`pre_install_script_name`, `install_script_name`, `post_install_script_name`) that other modules can use with `coder exp sync want` to coordinate execution order.
## Full example ## Full example
A example with all available options: A example with all available options:
@ -47,15 +65,17 @@ A example with all available options:
module "nodejs" { module "nodejs" {
count = data.coder_workspace.me.start_count count = data.coder_workspace.me.start_count
source = "registry.coder.com/thezoker/nodejs/coder" source = "registry.coder.com/thezoker/nodejs/coder"
version = "1.0.13" version = "1.0.14"
agent_id = coder_agent.example.id agent_id = coder_agent.example.id
nvm_version = "1.0.13" nvm_version = "v0.39.7"
nvm_install_prefix = "/opt/nvm" nvm_install_prefix = "/opt/nvm"
node_versions = [ node_versions = [
"16",
"18", "18",
"20",
"node" "node"
] ]
default_node_version = "1.0.13" default_node_version = "20"
pre_install_script = "echo 'Pre-install setup'"
post_install_script = "npm install -g typescript"
} }
``` ```

View File

@ -1,5 +1,5 @@
import { describe } from "bun:test"; import { describe, expect, it } from "bun:test";
import { runTerraformInit, testRequiredVariables } from "~test"; import { runTerraformInit, testRequiredVariables, runTerraformApply } from "~test";
describe("nodejs", async () => { describe("nodejs", async () => {
await runTerraformInit(import.meta.dir); await runTerraformInit(import.meta.dir);
@ -8,5 +8,19 @@ describe("nodejs", async () => {
agent_id: "foo", agent_id: "foo",
}); });
// More tests depend on shebang refactors it("accepts pre_install_script and post_install_script", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
pre_install_script: "echo pre",
post_install_script: "echo post",
});
expect(state).toBeDefined();
});
it("works without pre/post install scripts", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
});
expect(state).toBeDefined();
});
}); });

View File

@ -38,15 +38,114 @@ variable "default_node_version" {
default = "node" default = "node"
} }
variable "pre_install_script" {
type = string
description = "Custom script to run before installing Node.js. Can be used for dependency ordering between modules."
default = null
}
variable "post_install_script" {
type = string
description = "Custom script to run after installing Node.js."
default = null
}
locals {
encoded_pre_install_script = var.pre_install_script != null ? base64encode(var.pre_install_script) : ""
encoded_post_install_script = var.post_install_script != null ? base64encode(var.post_install_script) : ""
module_dir_path = "$HOME/.nodejs-module"
pre_install_script_name = "nodejs-pre_install_script"
install_script_name = "nodejs-install_script"
post_install_script_name = "nodejs-post_install_script"
}
resource "coder_script" "nodejs_pre_install" {
count = var.pre_install_script != null ? 1 : 0
agent_id = var.agent_id
display_name = "Node.js: Pre-Install"
run_on_start = true
script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
mkdir -p ${local.module_dir_path}
trap 'coder exp sync complete ${local.pre_install_script_name}' EXIT
coder exp sync start ${local.pre_install_script_name}
echo -n '${local.encoded_pre_install_script}' | base64 -d > ${local.module_dir_path}/pre_install.sh
chmod +x ${local.module_dir_path}/pre_install.sh
${local.module_dir_path}/pre_install.sh 2>&1
EOT
}
resource "coder_script" "nodejs" { resource "coder_script" "nodejs" {
agent_id = var.agent_id agent_id = var.agent_id
display_name = "Node.js:" display_name = "Node.js: Install"
script = templatefile("${path.module}/run.sh", { run_on_start = true
NVM_VERSION : var.nvm_version, script = <<-EOT
INSTALL_PREFIX : var.nvm_install_prefix, #!/bin/bash
NODE_VERSIONS : join(",", var.node_versions), set -o errexit
DEFAULT : var.default_node_version, set -o pipefail
})
run_on_start = true mkdir -p ${local.module_dir_path}
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}
%{endif~}
coder exp sync start ${local.install_script_name}
echo -n '${base64encode(templatefile("${path.module}/run.sh", {
NVM_VERSION = var.nvm_version,
INSTALL_PREFIX = var.nvm_install_prefix,
NODE_VERSIONS = join(",", var.node_versions),
DEFAULT = var.default_node_version,
}))}' | base64 -d > ${local.module_dir_path}/install.sh
chmod +x ${local.module_dir_path}/install.sh
${local.module_dir_path}/install.sh 2>&1
EOT
start_blocks_login = true start_blocks_login = true
} }
resource "coder_script" "nodejs_post_install" {
count = var.post_install_script != null ? 1 : 0
agent_id = var.agent_id
display_name = "Node.js: Post-Install"
run_on_start = true
script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
trap 'coder exp sync complete ${local.post_install_script_name}' EXIT
coder exp sync want ${local.post_install_script_name} ${local.install_script_name}
coder exp sync start ${local.post_install_script_name}
echo -n '${local.encoded_post_install_script}' | base64 -d > ${local.module_dir_path}/post_install.sh
chmod +x ${local.module_dir_path}/post_install.sh
${local.module_dir_path}/post_install.sh 2>&1
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
}

View File

@ -0,0 +1,122 @@
run "test_nodejs_basic" {
command = plan
variables {
agent_id = "test-agent-123"
}
assert {
condition = var.agent_id == "test-agent-123"
error_message = "Agent ID variable should be set correctly"
}
assert {
condition = var.nvm_version == "master"
error_message = "nvm_version should default to master"
}
assert {
condition = var.default_node_version == "node"
error_message = "default_node_version should default to node"
}
assert {
condition = var.pre_install_script == null
error_message = "pre_install_script should default to null"
}
assert {
condition = var.post_install_script == null
error_message = "post_install_script should default to null"
}
}
run "test_with_scripts" {
command = plan
variables {
agent_id = "test-agent-scripts"
pre_install_script = "echo 'Pre-install script'"
post_install_script = "echo 'Post-install script'"
}
assert {
condition = var.pre_install_script == "echo 'Pre-install script'"
error_message = "Pre-install script should be set correctly"
}
assert {
condition = var.post_install_script == "echo 'Post-install script'"
error_message = "Post-install script should be set correctly"
}
}
run "test_custom_options" {
command = plan
variables {
agent_id = "test-agent-custom"
nvm_version = "v0.39.7"
nvm_install_prefix = ".custom-nvm"
node_versions = ["18", "20", "node"]
default_node_version = "20"
}
assert {
condition = var.nvm_version == "v0.39.7"
error_message = "nvm_version should be set to v0.39.7"
}
assert {
condition = var.nvm_install_prefix == ".custom-nvm"
error_message = "nvm_install_prefix should be set correctly"
}
assert {
condition = length(var.node_versions) == 3
error_message = "node_versions should have 3 entries"
}
assert {
condition = var.default_node_version == "20"
error_message = "default_node_version should be set to 20"
}
}
run "test_with_pre_install_only" {
command = plan
variables {
agent_id = "test-agent-pre"
pre_install_script = "echo 'pre-install'"
}
assert {
condition = var.pre_install_script != null
error_message = "Pre-install script should be set"
}
assert {
condition = var.post_install_script == null
error_message = "Post-install script should default to null"
}
}
run "test_with_post_install_only" {
command = plan
variables {
agent_id = "test-agent-post"
post_install_script = "echo 'post-install'"
}
assert {
condition = var.pre_install_script == null
error_message = "Pre-install script should default to null"
}
assert {
condition = var.post_install_script != null
error_message = "Post-install script should be set"
}
}