Compare commits

..

15 Commits

Author SHA1 Message Date
DevCats 11a64e9b08 Merge branch 'main' into feat/nodejs-pre-post-install-scripts 2026-03-20 08:11:52 -05:00
blink-so[bot] 0fd0d08d2f fix: terraform fmt alignment in coder_script.nodejs resource 2026-03-19 18:02:37 +00:00
blink-so[bot] 817238ea64 refactor(nodejs): use base64 encoding and coder exp sync for pre/post install scripts
- Base64 encode pre/post install scripts to safely handle special characters
- Use separate coder_script resources for pre_install, install, and post_install
- Add coder exp sync want/start/complete for execution ordering
- Base64 encode the main install script (run.sh) via templatefile + base64encode
- Revert run.sh to original (no pre/post install handling)
- Add sync name outputs for cross-module dependency coordination
- Update README with cross-module coordination documentation
- Add output assertions to tests
2026-03-19 17:59:22 +00:00
blink-so[bot] d0ef879e5f revert: restore unrelated comment in main.test.ts 2026-03-19 13:24:41 +00:00
blink-so[bot] 872fdbcf35 fix: bump version to 1.1.0 (minor) for new input variables 2026-03-19 13:23:26 +00:00
35C4n0r ce50e52fc5 feat(coder-labs/modules/codex): update default configuration to use model providers instead of profiles (#806)
## Description
- update default configuration to use model providers instead of
profiles

## Type of Change

- [ ] New module
- [ ] New template
- [ ] Bug fix
- [x] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/coder-labs/modules/codex`  
**New version:** `v4.3.1`  
**Breaking change:** [ ] Yes [x] No

## Testing & Validation

- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun fmt`)
- [x] Changes tested locally

## Related Issues

<!-- Link related issues or write "None" if not applicable -->
2026-03-18 11:39:59 +05:30
Hugo Dutka 6940774628 feat: add the portabledesktop module (#805)
## Description
Add a module to install https://github.com/coder/portabledesktop in a
workspace. This will be required for the virtual desktop feature in
Coder Agents.
## Type of Change
- [x] New module
- [ ] New template
- [ ] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other
## Module Information
**Path:** `registry/coder/modules/portabledesktop`
**New version:** `v1.0.0`
**Breaking change:** [ ] Yes [x] No
## Testing & Validation
- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun fmt`)
- [x] Changes tested locally
## Related Issues
None
2026-03-17 10:07:35 +01:00
dependabot[bot] 85c51816f9 chore(deps): bump the github-actions group with 3 updates (#804)
Bumps the github-actions group with 3 updates:
[dorny/paths-filter](https://github.com/dorny/paths-filter),
[coder/coder](https://github.com/coder/coder) and
[oven-sh/setup-bun](https://github.com/oven-sh/setup-bun).

Updates `dorny/paths-filter` from 3.0.2 to 4.0.1
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/dorny/paths-filter/releases">dorny/paths-filter's
releases</a>.</em></p>
<blockquote>
<h2>v4.0.0</h2>
<h2>What's Changed</h2>
<ul>
<li>feat: update action runtime to node24 by <a
href="https://github.com/saschabratton"><code>@​saschabratton</code></a>
in <a
href="https://redirect.github.com/dorny/paths-filter/pull/294">dorny/paths-filter#294</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/saschabratton"><code>@​saschabratton</code></a>
made their first contribution in <a
href="https://redirect.github.com/dorny/paths-filter/pull/294">dorny/paths-filter#294</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/dorny/paths-filter/compare/v3.0.3...v4.0.0">https://github.com/dorny/paths-filter/compare/v3.0.3...v4.0.0</a></p>
<h2>v3.0.3</h2>
<h2>What's Changed</h2>
<ul>
<li>Add missing predicate-quantifier by <a
href="https://github.com/wardpeet"><code>@​wardpeet</code></a> in <a
href="https://redirect.github.com/dorny/paths-filter/pull/279">dorny/paths-filter#279</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/wardpeet"><code>@​wardpeet</code></a>
made their first contribution in <a
href="https://redirect.github.com/dorny/paths-filter/pull/279">dorny/paths-filter#279</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/dorny/paths-filter/compare/v3...v3.0.3">https://github.com/dorny/paths-filter/compare/v3...v3.0.3</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a
href="https://github.com/dorny/paths-filter/blob/master/CHANGELOG.md">dorny/paths-filter's
changelog</a>.</em></p>
<blockquote>
<h1>Changelog</h1>
<h2>v4.0.0</h2>
<ul>
<li><a
href="https://redirect.github.com/dorny/paths-filter/pull/294">Update
action runtime to node24</a></li>
</ul>
<h2>v3.0.3</h2>
<ul>
<li><a
href="https://redirect.github.com/dorny/paths-filter/pull/279">Add
missing predicate-quantifier</a></li>
</ul>
<h2>v3.0.2</h2>
<ul>
<li><a
href="https://redirect.github.com/dorny/paths-filter/pull/224">Add
config parameter for predicate quantifier</a></li>
</ul>
<h2>v3.0.1</h2>
<ul>
<li><a
href="https://redirect.github.com/dorny/paths-filter/pull/133">Compare
base and ref when token is empty</a></li>
</ul>
<h2>v3.0.0</h2>
<ul>
<li><a
href="https://redirect.github.com/dorny/paths-filter/pull/210">Update to
Node.js 20</a></li>
<li><a
href="https://redirect.github.com/dorny/paths-filter/pull/215">Update
all dependencies</a></li>
</ul>
<h2>v2.11.1</h2>
<ul>
<li><a
href="https://redirect.github.com/dorny/paths-filter/pull/167">Update
<code>@​actions/core</code> to v1.10.0 - Fixes warning about deprecated
set-output</a></li>
<li><a
href="https://redirect.github.com/dorny/paths-filter/pull/168">Document
need for pull-requests: read permission</a></li>
<li><a
href="https://redirect.github.com/dorny/paths-filter/pull/164">Updating
to actions/checkout@v3</a></li>
</ul>
<h2>v2.11.0</h2>
<ul>
<li><a
href="https://redirect.github.com/dorny/paths-filter/pull/157">Set
list-files input parameter as not required</a></li>
<li><a
href="https://redirect.github.com/dorny/paths-filter/pull/161">Update
Node.js</a></li>
<li><a
href="https://redirect.github.com/dorny/paths-filter/pull/162">Fix
incorrect handling of Unicode characters in exec()</a></li>
<li><a
href="https://redirect.github.com/dorny/paths-filter/pull/163">Use
Octokit pagination</a></li>
<li><a
href="https://redirect.github.com/dorny/paths-filter/pull/160">Updates
real world links</a></li>
</ul>
<h2>v2.10.2</h2>
<ul>
<li><a href="https://redirect.github.com/dorny/paths-filter/pull/91">Fix
getLocalRef() returns wrong ref</a></li>
</ul>
<h2>v2.10.1</h2>
<ul>
<li><a
href="https://redirect.github.com/dorny/paths-filter/pull/85">Improve
robustness of change detection</a></li>
</ul>
<h2>v2.10.0</h2>
<ul>
<li><a href="https://redirect.github.com/dorny/paths-filter/pull/82">Add
ref input parameter</a></li>
<li><a href="https://redirect.github.com/dorny/paths-filter/pull/83">Fix
change detection in PR when pullRequest.changed_files is
incorrect</a></li>
</ul>
<h2>v2.9.3</h2>
<ul>
<li><a href="https://redirect.github.com/dorny/paths-filter/pull/78">Fix
change detection when base is a tag</a></li>
</ul>
<h2>v2.9.2</h2>
<ul>
<li><a href="https://redirect.github.com/dorny/paths-filter/pull/75">Fix
fetching git history</a></li>
</ul>
<h2>v2.9.1</h2>
<ul>
<li><a href="https://redirect.github.com/dorny/paths-filter/pull/74">Fix
fetching git history + fallback to unshallow repo</a></li>
</ul>
<h2>v2.9.0</h2>
<!-- raw HTML omitted -->
</blockquote>
<p>... (truncated)</p>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/dorny/paths-filter/commit/fbd0ab8f3e69293af611ebaee6363fc25e6d187d"><code>fbd0ab8</code></a>
feat: add merge_group event support</li>
<li><a
href="https://github.com/dorny/paths-filter/commit/efb1da7ce8d89bbc261191e5a2dc1453c3837339"><code>efb1da7</code></a>
feat: add dist/ freshness check to PR workflow</li>
<li><a
href="https://github.com/dorny/paths-filter/commit/d8f7b061b24c30a325ff314b76c37adb05b041ce"><code>d8f7b06</code></a>
Merge pull request <a
href="https://redirect.github.com/dorny/paths-filter/issues/302">#302</a>
from dorny/issue-299</li>
<li><a
href="https://github.com/dorny/paths-filter/commit/addbc147a95845176e1bc013a012fbf1d366389a"><code>addbc14</code></a>
Update README for v4</li>
<li><a
href="https://github.com/dorny/paths-filter/commit/9d7afb8d214ad99e78fbd4247752c4caed2b6e4c"><code>9d7afb8</code></a>
Update CHANGELOG for v4.0.0</li>
<li><a
href="https://github.com/dorny/paths-filter/commit/782470c5d953cae2693d643172b14e01bacb71f3"><code>782470c</code></a>
Merge branch 'releases/v3'</li>
<li><a
href="https://github.com/dorny/paths-filter/commit/d1c1ffe0248fe513906c8e24db8ea791d46f8590"><code>d1c1ffe</code></a>
Update CHANGELOG for v3.0.3</li>
<li><a
href="https://github.com/dorny/paths-filter/commit/ce10459c8b92cd8901166c0a222fbb033ef39365"><code>ce10459</code></a>
Merge pull request <a
href="https://redirect.github.com/dorny/paths-filter/issues/294">#294</a>
from saschabratton/master</li>
<li><a
href="https://github.com/dorny/paths-filter/commit/5f40380c5482e806c81cec080f5192e7234d8fe9"><code>5f40380</code></a>
feat: update action runtime to node24</li>
<li><a
href="https://github.com/dorny/paths-filter/commit/668c092af3649c4b664c54e4b704aa46782f6f7c"><code>668c092</code></a>
Merge pull request <a
href="https://redirect.github.com/dorny/paths-filter/issues/279">#279</a>
from wardpeet/patch-1</li>
<li>Additional commits viewable in <a
href="https://github.com/dorny/paths-filter/compare/de90cc6fb38fc0963ad72b210f1f284cd68cea36...fbd0ab8f3e69293af611ebaee6363fc25e6d187d">compare
view</a></li>
</ul>
</details>
<br />

Updates `coder/coder` from 2.31.3 to 2.31.5
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/coder/coder/releases">coder/coder's
releases</a>.</em></p>
<blockquote>
<h2>v2.31.5</h2>
<h2>Changelog</h2>
<blockquote>
<p>[!NOTE]
This is a mainline Coder release. We advise enterprise customers without
a staging environment to install our <a
href="https://github.com/coder/coder/releases/latest">latest stable
release</a> while we refine this version. Learn more about our <a
href="https://coder.com/docs/install/releases">Release Schedule</a>.</p>
</blockquote>
<h3>Bug fixes</h3>
<ul>
<li>Prevent emitting build duration metric for devcontainer subagents
(<a
href="https://redirect.github.com/coder/coder/issues/22930">#22930</a>,
2cd4e03f1)</li>
<li>Prevent ui error when last org member is removed (<a
href="https://redirect.github.com/coder/coder/issues/23019">#23019</a>,
581e956b4)</li>
<li>Networking: Retry after transport dial timeouts (<a
href="https://redirect.github.com/coder/coder/issues/22977">#22977</a>,
1a774ab7c)</li>
</ul>
<p>Compare: <a
href="https://github.com/coder/coder/compare/v2.31.4...v2.31.5"><code>v2.31.4...v2.31.5</code></a></p>
<h2>Container image</h2>
<ul>
<li><code>docker pull ghcr.io/coder/coder:2.31.5</code></li>
</ul>
<h2>Install/upgrade</h2>
<p>Refer to our docs to <a
href="https://coder.com/docs/install">install</a> or <a
href="https://coder.com/docs/install/upgrade">upgrade</a> Coder, or use
a release asset below.</p>
<h2>v2.31.4</h2>
<h2>Changelog</h2>
<blockquote>
<p>[!NOTE]
This is a mainline Coder release. We advise enterprise customers without
a staging environment to install our <a
href="https://github.com/coder/coder/releases/latest">latest stable
release</a> while we refine this version. Learn more about our <a
href="https://coder.com/docs/install/releases">Release Schedule</a>.</p>
</blockquote>
<h3>Features</h3>
<ul>
<li>Add Prometheus collector for DERP server expvar metrics (<a
href="https://redirect.github.com/coder/coder/issues/22583">#22583</a>,
a3792153d)</li>
</ul>
<h3>Bug fixes</h3>
<ul>
<li>Filter sub-agents from build duration metric (<a
href="https://redirect.github.com/coder/coder/issues/22732">#22732</a>,
757634c72)</li>
<li>Bump aibridge to v1.0.9 to forward Anthropic-Beta header (<a
href="https://redirect.github.com/coder/coder/issues/22842">#22842</a>,
61b513e58)</li>
</ul>
<p>Compare: <a
href="https://github.com/coder/coder/compare/v2.31.3...v2.31.4"><code>v2.31.3...v2.31.4</code></a></p>
<h2>Container image</h2>
<ul>
<li><code>docker pull ghcr.io/coder/coder:2.31.4</code></li>
</ul>
<h2>Install/upgrade</h2>
<p>Refer to our docs to <a
href="https://coder.com/docs/install">install</a> or <a
href="https://coder.com/docs/install/upgrade">upgrade</a> Coder, or use
a release asset below.</p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/coder/coder/commit/1a774ab7ce99063a2e01beb94de3fcbccaf84dbe"><code>1a774ab</code></a>
fix(tailnet): retry after transport dial timeouts (<a
href="https://redirect.github.com/coder/coder/issues/22977">#22977</a>)
(cherry-pick/v2.31...</li>
<li><a
href="https://github.com/coder/coder/commit/581e956b49bf34bc0145188aa7e15f3e7f8e71c4"><code>581e956</code></a>
fix: prevent ui error when last org member is removed (<a
href="https://redirect.github.com/coder/coder/issues/23019">#23019</a>)</li>
<li><a
href="https://github.com/coder/coder/commit/2cd4e03f11dcf732f06af2899c0e896b2c2ee766"><code>2cd4e03</code></a>
fix: prevent emitting build duration metric for devcontainer subagents
(<a
href="https://redirect.github.com/coder/coder/issues/22930">#22930</a>)</li>
<li><a
href="https://github.com/coder/coder/commit/61b513e586d7dd6ded81beaa6766689988427bad"><code>61b513e</code></a>
fix: bump aibridge to v1.0.9 to forward Anthropic-Beta header (<a
href="https://redirect.github.com/coder/coder/issues/22842">#22842</a>)</li>
<li><a
href="https://github.com/coder/coder/commit/757634c720b03eea3c821add9784cb395ae76a9b"><code>757634c</code></a>
fix: filter sub-agents from build duration metric (<a
href="https://redirect.github.com/coder/coder/issues/22732">#22732</a>)
(<a
href="https://redirect.github.com/coder/coder/issues/22919">#22919</a>)</li>
<li><a
href="https://github.com/coder/coder/commit/a3792153dea7efbd6dde31bd41159e4c79b985c7"><code>a379215</code></a>
feat: add Prometheus collector for DERP server expvar metrics (<a
href="https://redirect.github.com/coder/coder/issues/22583">#22583</a>)
(<a
href="https://redirect.github.com/coder/coder/issues/22917">#22917</a>)</li>
<li>See full diff in <a
href="https://github.com/coder/coder/compare/deaacff8437e3f4ee84bc51c4e5162f6dd7d190e...1a774ab7ce99063a2e01beb94de3fcbccaf84dbe">compare
view</a></li>
</ul>
</details>
<br />

Updates `oven-sh/setup-bun` from 2.1.3 to 2.2.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a
href="https://github.com/oven-sh/setup-bun/releases">oven-sh/setup-bun's
releases</a>.</em></p>
<blockquote>
<h2>v2.2.0</h2>
<p><code>oven-sh/setup-bun</code> is the github action for setting up
Bun.</p>
<h2>What's Changed</h2>
<ul>
<li>build: update action runtime to Node.js 24 by <a
href="https://github.com/adam0white"><code>@​adam0white</code></a> in <a
href="https://redirect.github.com/oven-sh/setup-bun/pull/176">oven-sh/setup-bun#176</a></li>
<li>ci: use <code>actions/checkout@v6.0.2</code> in the test workflow by
<a href="https://github.com/tcely"><code>@​tcely</code></a> in <a
href="https://redirect.github.com/oven-sh/setup-bun/pull/173">oven-sh/setup-bun#173</a></li>
<li>ci: update actions for the <code>autofix.ci</code> workflow by <a
href="https://github.com/tcely"><code>@​tcely</code></a> in <a
href="https://redirect.github.com/oven-sh/setup-bun/pull/174">oven-sh/setup-bun#174</a></li>
<li>ci: update actions for the <code>Release new action version</code>
workflow by <a href="https://github.com/tcely"><code>@​tcely</code></a>
in <a
href="https://redirect.github.com/oven-sh/setup-bun/pull/175">oven-sh/setup-bun#175</a></li>
<li>release: v2.2.0 by <a
href="https://github.com/xhyrom"><code>@​xhyrom</code></a> in <a
href="https://redirect.github.com/oven-sh/setup-bun/pull/177">oven-sh/setup-bun#177</a></li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a
href="https://github.com/adam0white"><code>@​adam0white</code></a> made
their first contribution in <a
href="https://redirect.github.com/oven-sh/setup-bun/pull/176">oven-sh/setup-bun#176</a></li>
<li><a href="https://github.com/tcely"><code>@​tcely</code></a> made
their first contribution in <a
href="https://redirect.github.com/oven-sh/setup-bun/pull/173">oven-sh/setup-bun#173</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a
href="https://github.com/oven-sh/setup-bun/compare/v2...v2.2.0">https://github.com/oven-sh/setup-bun/compare/v2...v2.2.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a
href="https://github.com/oven-sh/setup-bun/commit/0c5077e51419868618aeaa5fe8019c62421857d6"><code>0c5077e</code></a>
release: v2.2.0 (<a
href="https://redirect.github.com/oven-sh/setup-bun/issues/177">#177</a>)</li>
<li><a
href="https://github.com/oven-sh/setup-bun/commit/1255e43b02f74b77bb39330ef756405951c3303a"><code>1255e43</code></a>
ci: update actions for the <code>Release new action version</code>
workflow (<a
href="https://redirect.github.com/oven-sh/setup-bun/issues/175">#175</a>)</li>
<li><a
href="https://github.com/oven-sh/setup-bun/commit/61861d1f6a3acf561f12343ea89e2c71ff4af529"><code>61861d1</code></a>
ci: update actions for the <code>autofix.ci</code> workflow (<a
href="https://redirect.github.com/oven-sh/setup-bun/issues/174">#174</a>)</li>
<li><a
href="https://github.com/oven-sh/setup-bun/commit/6f5bd063f58cadd19ae42cca8bb41b191e9949bd"><code>6f5bd06</code></a>
ci: use <code>actions/checkout@v6.0.2</code> in the test workflow (<a
href="https://redirect.github.com/oven-sh/setup-bun/issues/173">#173</a>)</li>
<li><a
href="https://github.com/oven-sh/setup-bun/commit/e3914758a49697077f7bcd190d36582a61667aad"><code>e391475</code></a>
build: update action runtime to Node.js 24 (<a
href="https://redirect.github.com/oven-sh/setup-bun/issues/176">#176</a>)</li>
<li>See full diff in <a
href="https://github.com/oven-sh/setup-bun/compare/ecf28ddc73e819eb6fa29df6b34ef8921c743461...0c5077e51419868618aeaa5fe8019c62421857d6">compare
view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't
alter it yourself. You can also trigger a rebase manually by commenting
`@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits
that have been made to it
- `@dependabot show <dependency name> ignore conditions` will show all
of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's major version (unless you unignore this specific
dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this
group update PR and stop Dependabot creating any more for the specific
dependency's minor version (unless you unignore this specific
dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR
and stop Dependabot creating any more for the specific dependency
(unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore
conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will
remove the ignore condition of the specified dependency and ignore
conditions


</details>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-16 12:54:51 +05:00
35C4n0r 4fdcf0d712 fix(coder/modules/claude-code): update claude session workdir normalization (#803)
## Description

- This lead to a bug where if the folder name is in the form `a.b.c`:
- we check for:
`-home-coder-ai.coder.com/cd32e253-ca16-4fd3-9825-d837e74ae3c2.jsonl`
- But the actual file path for claude-session is:
`-home-coder-ai-coder-com/cd32e253-ca16-4fd3-9825-d837e74ae3c2.jsonl`
- The above bug might also occur in the case of `a_b_c`
- update workdir normalization to handle dot in path

## Type of Change

- [ ] New module
- [ ] New template
- [x] Bug fix
- [ ] Feature/enhancement
- [ ] Documentation
- [ ] Other

## Module Information

<!-- Delete this section if not applicable -->

**Path:** `registry/coder/modules/claude-code`  
**New version:** `v4.8.1`  
**Breaking change:** [ ] Yes [x] No

## Testing & Validation

- [x] Tests pass (`bun test`)
- [x] Code formatted (`bun fmt`)
- [x] Changes tested locally

## Related Issues

<!-- Link related issues or write "None" if not applicable -->
2026-03-13 12:43:31 -05:00
blink-so[bot] acdbd5777f fix: align terraform blocks to pass prettier formatting 2026-03-13 14:24:57 +00:00
blink-so[bot] 8bd24f8b44 simplify: just add pre/post install script variables inline in run.sh
Remove all the coder exp sync machinery, separate script resources,
locals, and outputs. Simply pass the scripts as template variables
into run.sh and eval them before/after the nvm install.
2026-03-13 14:23:14 +00:00
Michael Suchacz 1460293de4 feat(coder/mux): add restart retries for mux exits (#800)
## Summary
- add optional mux auto-restarts with delay, lock cleanup, and
restart-attempt caps
- restart mux after any exit when enabled, including intentional exits
and signals
- require `max_restart_attempts` to be a non-negative whole number and
update docs/tests for the new restart semantics

## Validation
- `bash -n registry/coder/modules/mux/run.sh`
- `cd registry/coder/modules/mux && terraform validate`
- `cd registry/coder/modules/mux && terraform test -verbose`
- `cd registry/coder/modules/mux && bun test main.test.ts`

Generated with OpenAI using Mux
2026-03-13 09:16:38 -05:00
blink-so[bot] 79cef2ecfc 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
2026-03-13 14:07:52 +00:00
blinkagent[bot] 9606297620 feat: pass branch to coder dotfiles (#789)
Supersedes #551 (fork branch couldn't be rebased due to GitHub App
permission limitations).

Original author: @willshu

## Description

Adds support for specifying a git branch when cloning dotfiles
repositories.

### Changes
- Introduces `dotfiles_branch` and `default_dotfiles_branch` Terraform
variables
- Adds a `coder_parameter` for `dotfiles_branch` when not explicitly set
(with `order` matching `dotfiles_uri`)
- Conditionally passes the `--branch` flag to `coder dotfiles` only when
branch is non-empty
- Adds validation to prevent empty string for `dotfiles_branch` (use
`null` to prompt the user)
- Default branch is empty string — defers to the repo's default branch
rather than assuming `main`, matching the behavior of `coder dotfiles
--branch` which states: *"If empty, will default to cloning the default
branch or using the existing branch in the cloned repo on disk."*
- Adds test coverage for custom branch setting and parameter creation

### Review feedback addressed (from Copilot on #551)
- Added `order` field to `dotfiles_branch` parameter for UI consistency
with `dotfiles_uri`
- Conditional echo message — only shows branch info when set
- `--branch` flag only passed when `DOTFILES_BRANCH` is non-empty (both
current-user and sudo paths)
- Added validation block on `var.dotfiles_branch` to reject empty
strings

## Type of Change

- [x] Feature/enhancement

## Module Information

**Path:** `registry/coder/modules/dotfiles`

## Testing & Validation

- [ ] Tests pass (`bun test`)
- [ ] Code formatted (`bun fmt`)
- [ ] Changes tested locally

Co-authored-by: William Shu <william.shu@kkr.com>
Co-authored-by: DevCats <christofer@coder.com>
2026-03-11 16:11:19 -05:00
Shane White a0430e6f83 feat(coder-labs/modules/codex): add boundary support via agentapi module (#795)
## Description
Adds boundary support to the Codex module by passing boundary
variables through to the agentapi module and using
AGENTAPI_BOUNDARY_PREFIX in the start script.

Depends on #780

## Type of Change
- [x] Feature/enhancement

## Module Information
**Path:** `registry/coder-labs/modules/codex`
**Breaking change:** No

---------

Co-authored-by: Shane White <shane.white@cloudsecure.ltd>
Co-authored-by: 35C4n0r <70096901+35C4n0r@users.noreply.github.com>
2026-03-11 23:07:37 +05:30
28 changed files with 1467 additions and 198 deletions
+5 -5
View File
@@ -14,7 +14,7 @@ jobs:
- name: Check out code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Detect changed files
uses: dorny/paths-filter@de90cc6fb38fc0963ad72b210f1f284cd68cea36 # v3
uses: dorny/paths-filter@fbd0ab8f3e69293af611ebaee6363fc25e6d187d # v4.0.1
id: filter
with:
list-files: shell
@@ -37,9 +37,9 @@ jobs:
all:
- '**'
- name: Set up Terraform
uses: coder/coder/.github/actions/setup-tf@deaacff8437e3f4ee84bc51c4e5162f6dd7d190e # v2.31.3
uses: coder/coder/.github/actions/setup-tf@1a774ab7ce99063a2e01beb94de3fcbccaf84dbe # v2.31.5
- name: Set up Bun
uses: oven-sh/setup-bun@ecf28ddc73e819eb6fa29df6b34ef8921c743461 # v2
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
# We're using the latest version of Bun for now, but it might be worth
# reconsidering. They've pushed breaking changes in patch releases
@@ -82,12 +82,12 @@ jobs:
- name: Check out code
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Install Bun
uses: oven-sh/setup-bun@ecf28ddc73e819eb6fa29df6b34ef8921c743461 # v2
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: latest
# Need Terraform for its formatter
- name: Install Terraform
uses: coder/coder/.github/actions/setup-tf@deaacff8437e3f4ee84bc51c4e5162f6dd7d190e # v2.31.3
uses: coder/coder/.github/actions/setup-tf@1a774ab7ce99063a2e01beb94de3fcbccaf84dbe # v2.31.5
- name: Install dependencies
run: bun install
- name: Validate formatting
+2 -2
View File
@@ -26,12 +26,12 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
- name: Set up Bun
uses: oven-sh/setup-bun@ecf28ddc73e819eb6fa29df6b34ef8921c743461 # v2
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
with:
bun-version: latest
- name: Set up Terraform
uses: coder/coder/.github/actions/setup-tf@deaacff8437e3f4ee84bc51c4e5162f6dd7d190e # v2.31.3
uses: coder/coder/.github/actions/setup-tf@1a774ab7ce99063a2e01beb94de3fcbccaf84dbe # v2.31.5
- name: Install dependencies
run: bun install
+27 -12
View File
@@ -13,7 +13,7 @@ Run Codex CLI in your workspace to access OpenAI's models through the Codex inte
```tf
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "4.2.0"
version = "4.3.1"
agent_id = coder_agent.example.id
openai_api_key = var.openai_api_key
workdir = "/home/coder/project"
@@ -32,7 +32,7 @@ module "codex" {
module "codex" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder-labs/codex/coder"
version = "4.2.0"
version = "4.3.1"
agent_id = coder_agent.example.id
openai_api_key = "..."
workdir = "/home/coder/project"
@@ -51,7 +51,7 @@ For tasks integration with AI Bridge, add `enable_aibridge = true` to the [Usage
```tf
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "4.2.0"
version = "4.3.1"
agent_id = coder_agent.example.id
workdir = "/home/coder/project"
enable_aibridge = true
@@ -60,21 +60,16 @@ module "codex" {
When `enable_aibridge = true`, the module:
- Configures Codex to use the AI Bridge profile with `base_url` pointing to `${data.coder_workspace.me.access_url}/api/v2/aibridge/openai/v1` and `env_key` pointing to the workspace owner's session token
- Configures Codex to use the aibridge model_provider with `base_url` pointing to `${data.coder_workspace.me.access_url}/api/v2/aibridge/openai/v1` and `env_key` pointing to the workspace owner's session token
```toml
profile = "aibridge" # sets the default profile to aibridge
model_provider = "aibridge"
[model_providers.aibridge]
name = "AI Bridge"
base_url = "https://example.coder.com/api/v2/aibridge/openai/v1"
env_key = "CODER_AIBRIDGE_SESSION_TOKEN"
wire_api = "responses"
[profiles.aibridge]
model_provider = "aibridge"
model = "<model>" # as configured in the module input
model_reasoning_effort = "<model_reasoning_effort>" # as configured in the module input
```
This allows Codex to route API requests through Coder's AI Bridge instead of directly to OpenAI's API.
@@ -94,7 +89,7 @@ data "coder_task" "me" {}
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "4.2.0"
version = "4.3.1"
agent_id = coder_agent.example.id
openai_api_key = "..."
ai_prompt = data.coder_task.me.prompt
@@ -105,6 +100,26 @@ module "codex" {
}
```
### Usage with Agent Boundaries
This example shows how to configure the Codex module to run the agent behind a process-level boundary that restricts its network access.
By default, when `enable_boundary = true`, the module uses `coder boundary` subcommand (provided by Coder) without requiring any installation.
```tf
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "4.3.1"
agent_id = coder_agent.main.id
openai_api_key = var.openai_api_key
workdir = "/home/coder/project"
enable_boundary = true
}
```
> [!NOTE]
> For developers: The module also supports installing boundary from a release version (`use_boundary_directly = true`) or compiling from source (`compile_boundary_from_source = true`). These are escape hatches for development and testing purposes.
### Advanced Configuration
This example shows additional configuration options for custom models, MCP servers, and base configuration.
@@ -112,7 +127,7 @@ This example shows additional configuration options for custom models, MCP serve
```tf
module "codex" {
source = "registry.coder.com/coder-labs/codex/coder"
version = "4.2.0"
version = "4.3.1"
agent_id = coder_agent.example.id
openai_api_key = "..."
workdir = "/home/coder/project"
+43 -3
View File
@@ -468,9 +468,49 @@ describe("codex", async () => {
id,
"/home/coder/.codex/config.toml",
);
expect(configToml).toContain(
"[profiles.aibridge]\n" + 'model_provider = "aibridge"',
expect(configToml).toContain('model_provider = "aibridge"');
});
test("boundary-enabled", async () => {
const { id } = await setup({
moduleVariables: {
enable_boundary: "true",
boundary_config_path: "/tmp/test-boundary.yaml",
},
});
// Write boundary config
await execContainer(id, [
"bash",
"-c",
`cat > /tmp/test-boundary.yaml <<'EOF'
jail_type: landjail
proxy_port: 8087
log_level: warn
allowlist:
- "domain=api.openai.com"
EOF`,
]);
// Add mock coder binary for boundary setup
await writeExecutable({
containerId: id,
filePath: "/usr/bin/coder",
content: `#!/bin/bash
if [ "$1" = "boundary" ]; then
if [ "$2" = "--help" ]; then
echo "boundary help"
exit 0
fi
shift; shift; exec "$@"
fi
echo "mock coder"`,
});
await execModuleScript(id);
await expectAgentAPIStarted(id);
// Verify boundary wrapper was used in start script
const startLog = await readFileContainer(
id,
"/home/coder/.codex-module/agentapi-start.log",
);
expect(configToml).toContain('profile = "aibridge"');
expect(startLog).toContain("boundary");
});
});
+60 -28
View File
@@ -84,10 +84,10 @@ variable "enable_aibridge" {
variable "model_reasoning_effort" {
type = string
description = "The reasoning effort for the AI Bridge model. One of: none, low, medium, high. https://platform.openai.com/docs/guides/latest-model#lower-reasoning-effort"
default = "medium"
description = "The reasoning effort for the model. One of: none, low, medium, high. https://platform.openai.com/docs/guides/latest-model#lower-reasoning-effort"
default = ""
validation {
condition = contains(["none", "low", "medium", "high"], var.model_reasoning_effort)
condition = contains(["", "none", "minimal", "low", "medium", "high", "xhigh"], var.model_reasoning_effort)
error_message = "model_reasoning_effort must be one of: none, low, medium, high."
}
}
@@ -137,7 +137,7 @@ variable "agentapi_version" {
variable "codex_model" {
type = string
description = "The model for Codex to use. Defaults to gpt-5.3-codex."
default = "gpt-5.3-codex"
default = "gpt-5.4"
}
variable "pre_install_script" {
@@ -176,6 +176,36 @@ variable "codex_system_prompt" {
default = "You are a helpful coding assistant. Start every response with `Codex says:`"
}
variable "enable_boundary" {
type = bool
description = "Enable coder boundary for network filtering."
default = false
}
variable "boundary_config_path" {
type = string
description = "Path to boundary config.yaml inside the workspace. If provided, exposed as BOUNDARY_CONFIG env var."
default = ""
}
variable "boundary_version" {
type = string
description = "Boundary version. When use_boundary_directly is true, a release version should be provided or 'latest' for the latest release."
default = "latest"
}
variable "compile_boundary_from_source" {
type = bool
description = "Whether to compile boundary from source instead of using the official install script."
default = false
}
variable "use_boundary_directly" {
type = bool
description = "Whether to use boundary binary directly instead of coder boundary subcommand."
default = false
}
resource "coder_env" "openai_api_key" {
agent_id = var.agent_id
name = "OPENAI_API_KEY"
@@ -195,7 +225,7 @@ locals {
install_script = file("${path.module}/scripts/install.sh")
start_script = file("${path.module}/scripts/start.sh")
module_dir_name = ".codex-module"
latest_codex_model = "gpt-5.3-codex"
latest_codex_model = "gpt-5.4"
aibridge_config = <<-EOF
[model_providers.aibridge]
name = "AI Bridge"
@@ -203,35 +233,36 @@ locals {
env_key = "CODER_AIBRIDGE_SESSION_TOKEN"
wire_api = "responses"
[profiles.aibridge]
model_provider = "aibridge"
model = "${var.codex_model}"
model_reasoning_effort = "${var.model_reasoning_effort}"
EOF
}
module "agentapi" {
source = "registry.coder.com/coder/agentapi/coder"
version = "2.2.0"
version = "2.3.0"
agent_id = var.agent_id
folder = local.workdir
web_app_slug = local.app_slug
web_app_order = var.order
web_app_group = var.group
web_app_icon = var.icon
web_app_display_name = var.web_app_display_name
cli_app = var.cli_app
cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null
cli_app_display_name = var.cli_app ? var.cli_app_display_name : null
module_dir_name = local.module_dir_name
install_agentapi = var.install_agentapi
agentapi_subdomain = var.subdomain
agentapi_version = var.agentapi_version
enable_state_persistence = var.enable_state_persistence
pre_install_script = var.pre_install_script
post_install_script = var.post_install_script
start_script = <<-EOT
agent_id = var.agent_id
folder = local.workdir
web_app_slug = local.app_slug
web_app_order = var.order
web_app_group = var.group
web_app_icon = var.icon
web_app_display_name = var.web_app_display_name
cli_app = var.cli_app
cli_app_slug = var.cli_app ? "${local.app_slug}-cli" : null
cli_app_display_name = var.cli_app ? var.cli_app_display_name : null
module_dir_name = local.module_dir_name
install_agentapi = var.install_agentapi
agentapi_subdomain = var.subdomain
agentapi_version = var.agentapi_version
enable_state_persistence = var.enable_state_persistence
pre_install_script = var.pre_install_script
post_install_script = var.post_install_script
enable_boundary = var.enable_boundary
boundary_config_path = var.boundary_config_path
boundary_version = var.boundary_version
compile_boundary_from_source = var.compile_boundary_from_source
use_boundary_directly = var.use_boundary_directly
start_script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
@@ -267,6 +298,7 @@ module "agentapi" {
ARG_ADDITIONAL_MCP_SERVERS='${base64encode(var.additional_mcp_servers)}' \
ARG_CODER_MCP_APP_STATUS_SLUG='${local.app_slug}' \
ARG_CODEX_START_DIRECTORY='${local.workdir}' \
ARG_MODEL_REASONING_EFFORT='${var.model_reasoning_effort}' \
ARG_CODEX_INSTRUCTION_PROMPT='${base64encode(var.codex_system_prompt)}' \
/tmp/install.sh
EOT
@@ -93,10 +93,14 @@ function install_codex() {
write_minimal_default_config() {
local config_path="$1"
ARG_DEFAULT_PROFILE=""
ARG_OPTIONAL_TOP_LEVEL_CONFIG=""
if [[ "${ARG_ENABLE_AIBRIDGE}" = "true" ]]; then
ARG_DEFAULT_PROFILE='profile = "aibridge"'
ARG_OPTIONAL_TOP_LEVEL_CONFIG='model_provider = "aibridge"'
fi
if [[ "${ARG_MODEL_REASONING_EFFORT}" != "" ]]; then
ARG_OPTIONAL_TOP_LEVEL_CONFIG+=$'\n'"model_reasoning_effort = \"${ARG_MODEL_REASONING_EFFORT}\""
fi
cat << EOF > "$config_path"
@@ -104,13 +108,17 @@ write_minimal_default_config() {
sandbox_mode = "workspace-write"
approval_policy = "never"
preferred_auth_method = "apikey"
${ARG_DEFAULT_PROFILE}
${ARG_OPTIONAL_TOP_LEVEL_CONFIG}
[sandbox_workspace_write]
network_access = true
[notice.model_migrations]
"${ARG_CODEX_MODEL}" = "${ARG_LATEST_CODEX_MODEL}"
[projects."${ARG_CODEX_START_DIRECTORY}"]
trust_level = "trusted"
EOF
}
@@ -155,7 +155,7 @@ setup_workdir() {
build_codex_args() {
CODEX_ARGS=()
if [[ -n "${ARG_CODEX_MODEL}" ]] && [[ "${ARG_ENABLE_AIBRIDGE}" != "true" ]]; then
if [[ -n "${ARG_CODEX_MODEL}" ]]; then
CODEX_ARGS+=("--model" "${ARG_CODEX_MODEL}")
fi
@@ -210,7 +210,16 @@ capture_session_id() {
start_codex() {
printf "Starting Codex with arguments: %s\n" "${CODEX_ARGS[*]}"
agentapi server --type codex --term-width 67 --term-height 1190 -- codex "${CODEX_ARGS[@]}" &
# AGENTAPI_BOUNDARY_PREFIX is set by the agentapi module's main.sh when
# enable_boundary=true. It points to a wrapper script that runs the command
# through coder boundary, sandboxing only the agent process.
if [ -n "${AGENTAPI_BOUNDARY_PREFIX:-}" ]; then
printf "Starting with coder boundary enabled\n"
agentapi server --type codex --term-width 67 --term-height 1190 -- \
"${AGENTAPI_BOUNDARY_PREFIX}" codex "${CODEX_ARGS[@]}" &
else
agentapi server --type codex --term-width 67 --term-height 1190 -- codex "${CODEX_ARGS[@]}" &
fi
capture_session_id
}
+9 -28
View File
@@ -13,7 +13,7 @@ Run the [Claude Code](https://docs.anthropic.com/en/docs/agents-and-tools/claude
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.8.0"
version = "4.8.1"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
claude_api_key = "xxxx-xxxxx-xxxx"
@@ -60,7 +60,7 @@ By default, when `enable_boundary = true`, the module uses `coder boundary` subc
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.8.0"
version = "4.8.1"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
enable_boundary = true
@@ -81,7 +81,7 @@ For tasks integration with AI Bridge, add `enable_aibridge = true` to the [Usage
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.8.0"
version = "4.8.1"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
enable_aibridge = true
@@ -110,7 +110,7 @@ data "coder_task" "me" {}
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.8.0"
version = "4.8.1"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
ai_prompt = data.coder_task.me.prompt
@@ -133,7 +133,7 @@ This example shows additional configuration options for version pinning, custom
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.8.0"
version = "4.8.1"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
@@ -189,7 +189,7 @@ Run and configure Claude Code as a standalone CLI in your workspace.
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.8.0"
version = "4.8.1"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
install_claude_code = true
@@ -211,32 +211,13 @@ variable "claude_code_oauth_token" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.8.0"
version = "4.8.1"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
claude_code_oauth_token = var.claude_code_oauth_token
}
```
### Usage with Remote Control
[Remote Control](https://code.claude.com/docs/en/remote-control.md) allows you to access your Claude Code session from [claude.ai/code](https://claude.ai/code) or the Claude mobile app. The session runs locally in your workspace while you interact with it from any browser or device.
> [!NOTE]
> Remote Control requires a Claude subscription (Pro, Max, Team, or Enterprise). API keys are not supported. You must authenticate using `claude_code_oauth_token`.
```tf
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.8.0"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
claude_code_oauth_token = var.claude_code_oauth_token
enable_remote_control = true
# remote_control_name = "Custom Name" # Optional: defaults to workspace name
}
```
### Usage with AWS Bedrock
#### Prerequisites
@@ -303,7 +284,7 @@ resource "coder_env" "bedrock_api_key" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.8.0"
version = "4.8.1"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
@@ -360,7 +341,7 @@ resource "coder_env" "google_application_credentials" {
module "claude-code" {
source = "registry.coder.com/coder/claude-code/coder"
version = "4.8.0"
version = "4.8.1"
agent_id = coder_agent.main.id
workdir = "/home/coder/project"
model = "claude-sonnet-4@20250514"
@@ -267,18 +267,6 @@ variable "enable_state_persistence" {
default = true
}
variable "enable_remote_control" {
type = bool
description = "Enable Claude Code Remote Control, allowing the session to be accessed from claude.ai/code or the Claude mobile app. Requires a Claude subscription (Pro, Max, Team, or Enterprise). API keys are not supported. See https://code.claude.com/docs/en/remote-control.md"
default = false
}
variable "remote_control_name" {
type = string
description = "Custom session name for Remote Control, visible in the session list at claude.ai/code. Only used when enable_remote_control is true. Defaults to the workspace name."
default = ""
}
resource "coder_env" "claude_code_md_path" {
count = var.claude_md_path == "" ? 0 : 1
agent_id = var.agent_id
@@ -413,8 +401,6 @@ module "agentapi" {
ARG_USE_BOUNDARY_DIRECTLY='${var.use_boundary_directly}' \
ARG_CODER_HOST='${local.coder_host}' \
ARG_CLAUDE_BINARY_PATH='${var.claude_binary_path}' \
ARG_ENABLE_REMOTE_CONTROL='${var.enable_remote_control}' \
ARG_REMOTE_CONTROL_NAME='${var.remote_control_name != "" ? var.remote_control_name : data.coder_workspace.me.name}' \
/tmp/start.sh
EOT
@@ -417,46 +417,6 @@ run "test_disable_state_persistence" {
}
run "test_enable_remote_control_default" {
command = plan
variables {
agent_id = "test-agent"
workdir = "/home/coder"
}
assert {
condition = var.enable_remote_control == false
error_message = "enable_remote_control should default to false"
}
assert {
condition = var.remote_control_name == ""
error_message = "remote_control_name should default to empty string"
}
}
run "test_enable_remote_control" {
command = plan
variables {
agent_id = "test-agent-rc"
workdir = "/home/coder/project"
enable_remote_control = true
remote_control_name = "My Project"
}
assert {
condition = var.enable_remote_control == true
error_message = "enable_remote_control should be true when explicitly enabled"
}
assert {
condition = var.remote_control_name == "My Project"
error_message = "remote_control_name should be set to 'My Project'"
}
}
run "test_no_api_key_no_env" {
command = plan
@@ -24,8 +24,6 @@ ARG_BOUNDARY_VERSION=${ARG_BOUNDARY_VERSION:-"latest"}
ARG_COMPILE_FROM_SOURCE=${ARG_COMPILE_FROM_SOURCE:-false}
ARG_USE_BOUNDARY_DIRECTLY=${ARG_USE_BOUNDARY_DIRECTLY:-false}
ARG_CODER_HOST=${ARG_CODER_HOST:-}
ARG_ENABLE_REMOTE_CONTROL=${ARG_ENABLE_REMOTE_CONTROL:-false}
ARG_REMOTE_CONTROL_NAME=${ARG_REMOTE_CONTROL_NAME:-}
echo "--------------------------------"
@@ -41,8 +39,6 @@ printf "ARG_BOUNDARY_VERSION: %s\n" "$ARG_BOUNDARY_VERSION"
printf "ARG_COMPILE_FROM_SOURCE: %s\n" "$ARG_COMPILE_FROM_SOURCE"
printf "ARG_USE_BOUNDARY_DIRECTLY: %s\n" "$ARG_USE_BOUNDARY_DIRECTLY"
printf "ARG_CODER_HOST: %s\n" "$ARG_CODER_HOST"
printf "ARG_ENABLE_REMOTE_CONTROL: %s\n" "$ARG_ENABLE_REMOTE_CONTROL"
printf "ARG_REMOTE_CONTROL_NAME: %s\n" "$ARG_REMOTE_CONTROL_NAME"
echo "--------------------------------"
@@ -92,7 +88,7 @@ TASK_SESSION_ID="cd32e253-ca16-4fd3-9825-d837e74ae3c2"
get_project_dir() {
local workdir_normalized
workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/' '-')
workdir_normalized=$(echo "$ARG_WORKDIR" | tr '/._' '-')
echo "$HOME/.claude/projects/${workdir_normalized}"
}
@@ -224,15 +220,6 @@ function start_agentapi() {
[ -n "$ARG_AI_PROMPT" ] && ARGS+=(-- "$ARG_AI_PROMPT")
fi
# Build the claude command - either regular or remote-control mode.
CLAUDE_CMD=("claude")
if [ "$ARG_ENABLE_REMOTE_CONTROL" = "true" ]; then
CLAUDE_CMD+=("remote-control")
if [ -n "$ARG_REMOTE_CONTROL_NAME" ]; then
CLAUDE_CMD+=("--name" "$ARG_REMOTE_CONTROL_NAME")
fi
fi
printf "Running claude code with args: %s\n" "$(printf '%q ' "${ARGS[@]}")"
if [ "$ARG_ENABLE_BOUNDARY" = "true" ]; then
@@ -259,9 +246,9 @@ function start_agentapi() {
agentapi server --type claude --term-width 67 --term-height 1190 -- \
"${BOUNDARY_CMD[@]}" "${BOUNDARY_ARGS[@]}" -- \
"${CLAUDE_CMD[@]}" "${ARGS[@]}"
claude "${ARGS[@]}"
else
agentapi server --type claude --term-width 67 --term-height 1190 -- "${CLAUDE_CMD[@]}" "${ARGS[@]}"
agentapi server --type claude --term-width 67 --term-height 1190 -- claude "${ARGS[@]}"
fi
}
+6 -6
View File
@@ -18,7 +18,7 @@ Under the hood, this module uses the [coder dotfiles](https://coder.com/docs/v2/
module "dotfiles" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder"
version = "1.3.2"
version = "1.4.0"
agent_id = coder_agent.example.id
}
```
@@ -31,7 +31,7 @@ module "dotfiles" {
module "dotfiles" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder"
version = "1.3.2"
version = "1.4.0"
agent_id = coder_agent.example.id
}
```
@@ -42,7 +42,7 @@ module "dotfiles" {
module "dotfiles" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder"
version = "1.3.2"
version = "1.4.0"
agent_id = coder_agent.example.id
user = "root"
}
@@ -54,14 +54,14 @@ module "dotfiles" {
module "dotfiles" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder"
version = "1.3.2"
version = "1.4.0"
agent_id = coder_agent.example.id
}
module "dotfiles-root" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder"
version = "1.3.2"
version = "1.4.0"
agent_id = coder_agent.example.id
user = "root"
dotfiles_uri = module.dotfiles.dotfiles_uri
@@ -90,7 +90,7 @@ You can set a default dotfiles repository for all users by setting the `default_
module "dotfiles" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/dotfiles/coder"
version = "1.3.2"
version = "1.4.0"
agent_id = coder_agent.example.id
default_dotfiles_uri = "https://github.com/coder/dotfiles"
}
+35 -1
View File
@@ -62,7 +62,41 @@ describe("dotfiles", async () => {
agent_id: "foo",
coder_parameter_order: order.toString(),
});
expect(state.resources).toHaveLength(3);
const parameters = state.resources.filter(
(r) => r.type === "coder_parameter",
);
for (const param of parameters) {
expect(param.instances[0].attributes.order).toBe(order);
}
});
it("set custom dotfiles_branch", async () => {
const branch = "develop";
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
dotfiles_branch: branch,
});
expect(state.resources).toHaveLength(2);
expect(state.resources[0].instances[0].attributes.order).toBe(order);
const scriptResource = state.resources.find(
(r) => r.type === "coder_script",
);
expect(scriptResource?.instances[0].attributes.script).toContain(
`DOTFILES_BRANCH="${branch}"`,
);
});
it("default dotfiles_branch creates parameter", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
});
expect(state.resources).toHaveLength(3);
const branchParameter = state.resources.find(
(r) =>
r.type === "coder_parameter" &&
r.instances[0].attributes.name === "dotfiles_branch",
);
expect(branchParameter).toBeDefined();
expect(branchParameter?.instances[0].attributes.default).toBeNull();
});
});
+32
View File
@@ -46,6 +46,12 @@ variable "default_dotfiles_uri" {
}
}
variable "default_dotfiles_branch" {
type = string
description = "The default dotfiles branch if the workspace user does not provide one"
default = ""
}
variable "dotfiles_uri" {
type = string
description = "The URL to a dotfiles repository. (optional, when set, the user isn't prompted for their dotfiles)"
@@ -61,6 +67,17 @@ variable "dotfiles_uri" {
}
}
variable "dotfiles_branch" {
type = string
description = "The branch to use for the dotfiles repository (optional, when set, the user isn't prompted for the branch)"
default = null
validation {
condition = var.dotfiles_branch == null || var.dotfiles_branch != ""
error_message = "dotfiles_branch cannot be an empty string. Use null to prompt the user or provide a valid branch name."
}
}
variable "user" {
type = string
description = "The name of the user to apply the dotfiles to. (optional, applies to the current user by default)"
@@ -107,8 +124,21 @@ data "coder_parameter" "dotfiles_uri" {
}
}
data "coder_parameter" "dotfiles_branch" {
count = var.dotfiles_branch == null ? 1 : 0
type = "string"
name = "dotfiles_branch"
display_name = "Dotfiles Branch"
order = var.coder_parameter_order
default = var.default_dotfiles_branch
description = "The branch to use for the dotfiles repository"
mutable = true
icon = "/icon/dotfiles.svg"
}
locals {
dotfiles_uri = var.dotfiles_uri != null ? var.dotfiles_uri : data.coder_parameter.dotfiles_uri[0].value
dotfiles_branch = var.dotfiles_branch != null ? var.dotfiles_branch : data.coder_parameter.dotfiles_branch[0].value
user = var.user != null ? var.user : ""
encoded_post_clone_script = var.post_clone_script != null ? base64encode(var.post_clone_script) : ""
}
@@ -118,6 +148,7 @@ resource "coder_script" "dotfiles" {
script = templatefile("${path.module}/run.sh", {
DOTFILES_URI : local.dotfiles_uri,
DOTFILES_USER : local.user,
DOTFILES_BRANCH : local.dotfiles_branch,
POST_CLONE_SCRIPT : local.encoded_post_clone_script
})
display_name = "Dotfiles"
@@ -136,6 +167,7 @@ resource "coder_app" "dotfiles" {
command = templatefile("${path.module}/run.sh", {
DOTFILES_URI : local.dotfiles_uri,
DOTFILES_USER : local.user,
DOTFILES_BRANCH : local.dotfiles_branch,
POST_CLONE_SCRIPT : local.encoded_post_clone_script
})
}
+16 -3
View File
@@ -4,6 +4,7 @@ set -euo pipefail
DOTFILES_URI="${DOTFILES_URI}"
DOTFILES_USER="${DOTFILES_USER}"
DOTFILES_BRANCH="${DOTFILES_BRANCH}"
# Validate DOTFILES_URI to prevent command injection (defense in depth)
if [ -n "$DOTFILES_URI" ]; then
@@ -24,10 +25,18 @@ if [ -n "$${DOTFILES_URI// }" ]; then
DOTFILES_USER="$USER"
fi
echo "✨ Applying dotfiles for user $DOTFILES_USER"
if [ -n "$DOTFILES_BRANCH" ]; then
echo "✨ Applying dotfiles for user $DOTFILES_USER from branch $DOTFILES_BRANCH"
else
echo "✨ Applying dotfiles for user $DOTFILES_USER"
fi
if [ "$DOTFILES_USER" = "$USER" ]; then
coder dotfiles "$DOTFILES_URI" -y 2>&1 | tee ~/.dotfiles.log
if [ -n "$DOTFILES_BRANCH" ]; then
coder dotfiles "$DOTFILES_URI" --branch "$DOTFILES_BRANCH" -y 2>&1 | tee ~/.dotfiles.log
else
coder dotfiles "$DOTFILES_URI" -y 2>&1 | tee ~/.dotfiles.log
fi
else
if command -v getent > /dev/null 2>&1; then
DOTFILES_USER_HOME=$(getent passwd "$DOTFILES_USER" | cut -d: -f6)
@@ -40,7 +49,11 @@ if [ -n "$${DOTFILES_URI// }" ]; then
fi
CODER_BIN=$(command -v coder)
sudo -u "$DOTFILES_USER" "$CODER_BIN" dotfiles "$DOTFILES_URI" -y 2>&1 | tee "$DOTFILES_USER_HOME/.dotfiles.log"
if [ -n "$DOTFILES_BRANCH" ]; then
sudo -u "$DOTFILES_USER" "$CODER_BIN" dotfiles "$DOTFILES_URI" --branch "$DOTFILES_BRANCH" -y 2>&1 | tee "$DOTFILES_USER_HOME/.dotfiles.log"
else
sudo -u "$DOTFILES_USER" "$CODER_BIN" dotfiles "$DOTFILES_URI" -y 2>&1 | tee "$DOTFILES_USER_HOME/.dotfiles.log"
fi
fi
fi
+29 -11
View File
@@ -8,13 +8,13 @@ tags: [ai, agents, development, multiplexer]
# Mux
Automatically install and run [Mux](https://github.com/coder/mux) in a Coder workspace. By default, the module auto-detects an available package manager (`npm`, `pnpm`, or `bun`) to install `mux@next` (with a fallback to downloading the npm tarball if none is found). You can also force a specific package manager via `package_manager` and point to a custom registry with `registry_url`. The launcher now keeps watching the mux process after startup and appends signal/exit-code diagnostics to the mux log when the server is killed outside the Node runtime. Mux is a desktop application for parallel agentic development that enables developers to run multiple AI agents simultaneously across isolated workspaces.
Automatically install and run [Mux](https://github.com/coder/mux) in a Coder workspace. By default, the module auto-detects an available package manager (`npm`, `pnpm`, or `bun`) to install `mux@next` (with a fallback to downloading the npm tarball if none is found). You can also force a specific package manager via `package_manager` and point to a custom registry with `registry_url`. The launcher keeps watching the mux process after startup, appends signal/exit-code diagnostics to the mux log when the server is killed outside the Node runtime, and can optionally wait a few seconds, remove the stale server lock, and restart Mux after any exit until an optional restart-attempt cap is reached. Mux is a desktop application for parallel agentic development that enables developers to run multiple AI agents simultaneously across isolated workspaces.
```tf
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
version = "1.4.0"
version = "1.4.3"
agent_id = coder_agent.main.id
}
```
@@ -37,7 +37,7 @@ module "mux" {
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
version = "1.4.0"
version = "1.4.3"
agent_id = coder_agent.main.id
}
```
@@ -48,7 +48,7 @@ module "mux" {
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
version = "1.4.0"
version = "1.4.3"
agent_id = coder_agent.main.id
# Default is "latest"; set to a specific version to pin
install_version = "0.4.0"
@@ -63,7 +63,7 @@ Start Mux with `mux server --add-project /path/to/project`:
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
version = "1.4.0"
version = "1.4.3"
agent_id = coder_agent.main.id
add_project = "/path/to/project"
}
@@ -78,19 +78,35 @@ The module parses quoted values, so grouped arguments remain intact.
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
version = "1.4.0"
version = "1.4.3"
agent_id = coder_agent.main.id
additional_arguments = "--open-mode pinned --add-project '/workspaces/my repo'"
}
```
### Restart After Mux Exits
Enable automatic restarts after Mux exits, including clean exits and intentional shutdown signals such as `SIGTERM`. The launcher waits for `restart_delay_seconds`, removes `~/.mux/server.lock`, and starts Mux again. Set `max_restart_attempts` to a whole number to stop retrying after a fixed number of restarts, or leave it at `0` for unlimited retries.
```tf
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
version = "1.4.3"
agent_id = coder_agent.main.id
restart_on_kill = true
restart_delay_seconds = 3
max_restart_attempts = 5
}
```
### Custom Port
```tf
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
version = "1.4.0"
version = "1.4.3"
agent_id = coder_agent.main.id
port = 8080
}
@@ -104,7 +120,7 @@ Force a specific package manager instead of auto-detection:
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
version = "1.4.0"
version = "1.4.3"
agent_id = coder_agent.main.id
package_manager = "pnpm" # or "npm", "bun"
}
@@ -118,7 +134,7 @@ Use a private or mirrored npm registry:
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
version = "1.4.0"
version = "1.4.3"
agent_id = coder_agent.main.id
registry_url = "https://npm.pkg.github.com"
}
@@ -132,7 +148,7 @@ Run an existing copy of Mux if found, otherwise install from npm:
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
version = "1.4.0"
version = "1.4.3"
agent_id = coder_agent.main.id
use_cached = true
}
@@ -146,7 +162,7 @@ Run without installing from the network (requires Mux to be pre-installed):
module "mux" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/coder/mux/coder"
version = "1.4.0"
version = "1.4.3"
agent_id = coder_agent.main.id
install = false
}
@@ -164,3 +180,5 @@ module "mux" {
- Installs `mux@next` from the npm registry by default; set `registry_url` to use a private or mirrored registry
- Falls back to a direct tarball download when no package manager is found
- Appends best-effort signal and external-kill diagnostics to `log_path` if the mux process dies after startup
- Set `restart_on_kill = true` to wait `restart_delay_seconds`, remove `~/.mux/server.lock`, and restart Mux after it exits
- Set `max_restart_attempts` to a whole-number cap on restart attempts, or leave it at `0` for unlimited retries
+137
View File
@@ -145,6 +145,143 @@ chmod +x /tmp/mux/mux`,
}
}, 60000);
it("restarts after a clean exit when enabled", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
install: false,
log_path: "/tmp/mux.log",
restart_on_kill: true,
restart_delay_seconds: 1,
max_restart_attempts: 1,
});
const instance = findResourceInstance(state, "coder_script");
const id = await runContainer("alpine/curl");
try {
const setup = await execContainer(id, [
"sh",
"-c",
`apk add --no-cache bash >/dev/null
mkdir -p /tmp/mux
cat <<'EOF' > /tmp/mux/mux
#!/usr/bin/env sh
run_count_file="/tmp/mux-run-count"
run_count=0
if [ -f "$run_count_file" ]; then
run_count=$(cat "$run_count_file")
fi
run_count=$((run_count + 1))
printf '%s' "$run_count" > "$run_count_file"
echo "run=$run_count"
if [ "$run_count" -eq 1 ]; then
mkdir -p "$HOME/.mux"
touch "$HOME/.mux/server.lock"
exit 0
fi
if [ -f "$HOME/.mux/server.lock" ]; then
echo "lock=present"
else
echo "lock=cleaned"
fi
exit 0
EOF
chmod +x /tmp/mux/mux`,
]);
expect(setup.exitCode).toBe(0);
const output = await execContainer(id, ["sh", "-c", instance.script]);
if (output.exitCode !== 0) {
console.log("STDOUT:\n" + output.stdout);
console.log("STDERR:\n" + output.stderr);
}
expect(output.exitCode).toBe(0);
await execContainer(id, ["sh", "-c", "sleep 4"]);
const log = await readFileContainer(id, "/tmp/mux.log");
const runCount = await readFileContainer(id, "/tmp/mux-run-count");
expect(log).toContain("run=1");
expect(log).toContain("mux server exited cleanly.");
expect(log).toContain(
"Waiting 1 seconds before restarting mux after it exited.",
);
expect(log).toContain(
"Removing /root/.mux/server.lock before restarting mux.",
);
expect(log).toContain("run=2");
expect(log).toContain("lock=cleaned");
expect(log).toContain(
"Reached the max restart attempts limit (1); not restarting mux again.",
);
expect(runCount.trim()).toBe("2");
} finally {
await removeContainer(id);
}
}, 60000);
it("restarts after SIGTERM when enabled", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
install: false,
log_path: "/tmp/mux.log",
restart_on_kill: true,
restart_delay_seconds: 1,
max_restart_attempts: 1,
});
const instance = findResourceInstance(state, "coder_script");
const id = await runContainer("alpine/curl");
try {
const setup = await execContainer(id, [
"sh",
"-c",
`apk add --no-cache bash >/dev/null
mkdir -p /tmp/mux
cat <<'EOF' > /tmp/mux/mux
#!/usr/bin/env sh
run_count_file="/tmp/mux-run-count"
run_count=0
if [ -f "$run_count_file" ]; then
run_count=$(cat "$run_count_file")
fi
run_count=$((run_count + 1))
printf '%s' "$run_count" > "$run_count_file"
echo "run=$run_count"
if [ "$run_count" -eq 1 ]; then
kill -TERM $$
fi
exit 0
EOF
chmod +x /tmp/mux/mux`,
]);
expect(setup.exitCode).toBe(0);
const output = await execContainer(id, ["sh", "-c", instance.script]);
if (output.exitCode !== 0) {
console.log("STDOUT:\n" + output.stdout);
console.log("STDERR:\n" + output.stderr);
}
expect(output.exitCode).toBe(0);
await execContainer(id, ["sh", "-c", "sleep 4"]);
const log = await readFileContainer(id, "/tmp/mux.log");
const runCount = await readFileContainer(id, "/tmp/mux-run-count");
expect(log).toContain("run=1");
expect(log).toContain("signal TERM (15); shell exit code 143.");
expect(log).toContain(
"Waiting 1 seconds before restarting mux after it exited.",
);
expect(log).toContain("run=2");
expect(log).toContain(
"Reached the max restart attempts limit (1); not restarting mux again.",
);
expect(runCount.trim()).toBe("2");
} finally {
await removeContainer(id);
}
}, 60000);
it("runs with npm present", async () => {
const state = await runTerraformApply(import.meta.dir, {
agent_id: "foo",
+31
View File
@@ -49,6 +49,34 @@ variable "log_path" {
default = "/tmp/mux.log"
}
variable "restart_on_kill" {
type = bool
description = "Restart Mux after it exits by waiting briefly, removing the server lock, and launching it again."
default = false
}
variable "restart_delay_seconds" {
type = number
description = "How long to wait before restarting Mux after it exits when restart_on_kill is enabled."
default = 5
validation {
condition = var.restart_delay_seconds >= 0
error_message = "The 'restart_delay_seconds' variable must be greater than or equal to 0."
}
}
variable "max_restart_attempts" {
type = number
description = "Maximum whole-number restart attempts before giving up. Set to 0 for unlimited restarts when restart_on_kill is enabled."
default = 0
validation {
condition = var.max_restart_attempts >= 0 && floor(var.max_restart_attempts) == var.max_restart_attempts
error_message = "The 'max_restart_attempts' variable must be a whole number greater than or equal to 0."
}
}
variable "add_project" {
type = string
description = "Optional path to add/open as a project in Mux on startup."
@@ -171,6 +199,9 @@ resource "coder_script" "mux" {
OFFLINE : !var.install,
USE_CACHED : var.use_cached,
AUTH_TOKEN : local.mux_auth_token,
RESTART_ON_KILL : var.restart_on_kill,
RESTART_DELAY_SECONDS : var.restart_delay_seconds,
MAX_RESTART_ATTEMPTS : var.max_restart_attempts,
PACKAGE_MANAGER : var.package_manager,
REGISTRY_URL : local.registry_url,
})
+105
View File
@@ -111,6 +111,111 @@ run "launcher_logs_external_kills" {
}
}
run "restart_on_kill_enabled" {
command = plan
variables {
agent_id = "foo"
restart_on_kill = true
restart_delay_seconds = 7
}
assert {
condition = strcontains(resource.coder_script.mux.script, "restart_on_kill_value=\"true\"")
error_message = "mux launcher must receive the restart_on_kill setting"
}
assert {
condition = strcontains(resource.coder_script.mux.script, "restart_delay_seconds_value=\"7\"")
error_message = "mux launcher must receive the configured restart delay"
}
assert {
condition = strcontains(resource.coder_script.mux.script, "Waiting $${RESTART_DELAY_SECONDS_VALUE} seconds before restarting mux after it exited.")
error_message = "mux launcher must log the restart delay before relaunching"
}
assert {
condition = strcontains(resource.coder_script.mux.script, "Removing $HOME/.mux/server.lock before restarting mux.")
error_message = "mux launcher must clean up the server lock before relaunching"
}
assert {
condition = !strcontains(resource.coder_script.mux.script, "\"$exit_code\" -le 128")
error_message = "mux launcher must no longer exclude non-signal exits from restart handling"
}
assert {
condition = !strcontains(resource.coder_script.mux.script, "1|2|15)")
error_message = "mux launcher must no longer exclude intentional signals from restart handling"
}
}
run "restart_on_kill_with_restart_cap" {
command = plan
variables {
agent_id = "foo"
restart_on_kill = true
restart_delay_seconds = 7
max_restart_attempts = 2
}
assert {
condition = strcontains(resource.coder_script.mux.script, "max_restart_attempts_value=\"2\"")
error_message = "mux launcher must receive the configured restart cap"
}
assert {
condition = strcontains(resource.coder_script.mux.script, "Mux will stop restarting after $${max_restart_attempts_value} restart attempts.")
error_message = "mux launcher must describe the configured restart cap"
}
assert {
condition = strcontains(resource.coder_script.mux.script, "Reached the max restart attempts limit ($MAX_RESTART_ATTEMPTS_VALUE); not restarting mux again.")
error_message = "mux launcher must log when it hits the restart cap"
}
}
run "invalid_max_restart_attempts" {
command = plan
variables {
agent_id = "foo"
max_restart_attempts = -1
}
expect_failures = [
var.max_restart_attempts
]
}
run "fractional_max_restart_attempts" {
command = plan
variables {
agent_id = "foo"
max_restart_attempts = 0.5
}
expect_failures = [
var.max_restart_attempts
]
}
run "invalid_restart_delay_seconds" {
command = plan
variables {
agent_id = "foo"
restart_delay_seconds = -1
}
expect_failures = [
var.restart_delay_seconds
]
}
run "custom_version" {
command = plan
+82 -9
View File
@@ -5,17 +5,30 @@ RESET='\033[0m'
MUX_BINARY="${INSTALL_PREFIX}/mux"
function run_mux() {
# Remove stale server lock if present
rm -f "$HOME/.mux/server.lock"
local port_value
local auth_token_value
local restart_on_kill_value
local restart_delay_seconds_value
local max_restart_attempts_value
port_value="${PORT}"
auth_token_value="${AUTH_TOKEN}"
restart_on_kill_value="${RESTART_ON_KILL}"
restart_delay_seconds_value="${RESTART_DELAY_SECONDS}"
max_restart_attempts_value="${MAX_RESTART_ATTEMPTS}"
if [ -z "$port_value" ]; then
port_value="4000"
fi
if [ -z "$restart_delay_seconds_value" ]; then
restart_delay_seconds_value="5"
fi
if [ -z "$max_restart_attempts_value" ]; then
max_restart_attempts_value="0"
fi
mkdir -p "$(dirname "${LOG_PATH}")"
# Build args for mux (POSIX-compatible, avoid bash arrays)
@@ -41,13 +54,24 @@ EOF_ARGS
echo "🚀 Starting mux server on port $port_value..."
echo "Check logs at ${LOG_PATH}!"
echo "Unexpected exits will be appended to ${LOG_PATH} by the launcher."
echo "Mux exit details will be appended to ${LOG_PATH} by the launcher."
if [ "$restart_on_kill_value" = true ]; then
echo "️ Auto-restart after mux exits is enabled with a $${restart_delay_seconds_value}-second delay."
if [ "$max_restart_attempts_value" = "0" ]; then
echo "️ Automatic restarts are unlimited for every mux exit."
else
echo "️ Mux will stop restarting after $${max_restart_attempts_value} restart attempts."
fi
fi
nohup env \
LOG_PATH="${LOG_PATH}" \
MUX_BINARY="$MUX_BINARY" \
AUTH_TOKEN="$auth_token_value" \
PORT_VALUE="$port_value" \
RESTART_ON_KILL_VALUE="$restart_on_kill_value" \
RESTART_DELAY_SECONDS_VALUE="$restart_delay_seconds_value" \
MAX_RESTART_ATTEMPTS_VALUE="$max_restart_attempts_value" \
bash -s -- "$@" > /dev/null 2>&1 << 'EOF_LAUNCHER' &
signal_name() {
local signal_number="$1"
@@ -82,6 +106,14 @@ append_kernel_kill_context() {
fi
}
cleanup_mux_lock() {
rm -f "$HOME/.mux/server.lock"
}
should_restart_mux() {
[ "$RESTART_ON_KILL_VALUE" = "true" ]
}
log_mux_exit() {
local mux_pid="$1"
local exit_code="$2"
@@ -114,11 +146,52 @@ log_mux_exit() {
echo "[$timestamp] Check the earlier mux log lines for any in-process crash breadcrumbs from mux itself."
}
MUX_SERVER_AUTH_TOKEN="$AUTH_TOKEN" PORT="$PORT_VALUE" "$MUX_BINARY" "$@" >> "$LOG_PATH" 2>&1 &
mux_pid=$!
wait "$mux_pid"
exit_code=$?
log_mux_exit "$mux_pid" "$exit_code" >> "$LOG_PATH" 2>&1
log_mux_restart_wait() {
local timestamp
timestamp="$(date -Iseconds 2> /dev/null || date)"
echo "[$timestamp] Waiting $${RESTART_DELAY_SECONDS_VALUE} seconds before restarting mux after it exited."
}
log_mux_restart_cleanup() {
local timestamp
timestamp="$(date -Iseconds 2> /dev/null || date)"
echo "[$timestamp] Removing $HOME/.mux/server.lock before restarting mux."
}
log_mux_restart_cap_reached() {
local timestamp
timestamp="$(date -Iseconds 2> /dev/null || date)"
echo "[$timestamp] Reached the max restart attempts limit ($MAX_RESTART_ATTEMPTS_VALUE); not restarting mux again."
}
restart_attempt_count=0
while true; do
cleanup_mux_lock
MUX_SERVER_AUTH_TOKEN="$AUTH_TOKEN" PORT="$PORT_VALUE" "$MUX_BINARY" "$@" >> "$LOG_PATH" 2>&1 &
mux_pid=$!
wait "$mux_pid"
exit_code=$?
log_mux_exit "$mux_pid" "$exit_code" >> "$LOG_PATH" 2>&1
if should_restart_mux; then
if [ "$MAX_RESTART_ATTEMPTS_VALUE" -gt 0 ] && [ "$restart_attempt_count" -ge "$MAX_RESTART_ATTEMPTS_VALUE" ]; then
log_mux_restart_cap_reached >> "$LOG_PATH" 2>&1
break
fi
restart_attempt_count=$((restart_attempt_count + 1))
log_mux_restart_wait >> "$LOG_PATH" 2>&1
sleep "$RESTART_DELAY_SECONDS_VALUE"
cleanup_mux_lock
log_mux_restart_cleanup >> "$LOG_PATH" 2>&1
continue
fi
break
done
EOF_LAUNCHER
}
# Check if mux is already installed for offline mode
@@ -0,0 +1,46 @@
---
display_name: Portable Desktop
description: Install the portabledesktop binary for lightweight Linux desktop sessions.
icon: ../../../../.icons/desktop.svg
verified: true
tags: [desktop, vnc, ai]
---
# Portable Desktop
Install [portabledesktop](https://github.com/coder/portabledesktop) for lightweight Linux desktop sessions over VNC. The binary is stored in the agent's script data directory and is automatically available on PATH via `CODER_SCRIPT_BIN_DIR`.
```tf
module "portabledesktop" {
source = "registry.coder.com/coder/portabledesktop/coder"
version = "0.1.0"
agent_id = coder_agent.example.id
}
```
## Examples
### Custom download URL with checksum verification
```tf
module "portabledesktop" {
source = "registry.coder.com/coder/portabledesktop/coder"
version = "0.1.0"
agent_id = coder_agent.example.id
url = "https://example.com/portabledesktop-linux-x64"
sha256 = "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
}
```
### Additionally copy to a system path
Use `install_dir` to copy the binary to a system-wide directory in addition to the default script data directory:
```tf
module "portabledesktop" {
source = "registry.coder.com/coder/portabledesktop/coder"
version = "0.1.0"
agent_id = coder_agent.example.id
install_dir = "/usr/local/bin"
}
```
@@ -0,0 +1,242 @@
import { describe, expect, it } from "bun:test";
import {
execContainer,
findResourceInstance,
removeContainer,
runContainer,
runTerraformApply,
runTerraformInit,
testRequiredVariables,
type TerraformState,
} from "~test";
interface TestFixture {
state: TerraformState;
server: ReturnType<typeof Bun.serve>;
[Symbol.asyncDispose](): Promise<void>;
}
interface ContainerHandle {
id: string;
[Symbol.asyncDispose](): Promise<void>;
}
async function setupContainer(image: string): Promise<ContainerHandle> {
const id = await runContainer(image);
return {
id,
[Symbol.asyncDispose]: async () => {
await removeContainer(id);
},
};
}
const ENV_PREFIX =
'export CODER_SCRIPT_DATA_DIR=/tmp/coder-script-data && export CODER_SCRIPT_BIN_DIR=/tmp/coder-script-data/bin && mkdir -p "$CODER_SCRIPT_DATA_DIR" "$CODER_SCRIPT_BIN_DIR" && ';
async function setupFakeBinaryServer(
dir: string,
extraVars?: Record<string, string>,
): Promise<TestFixture> {
const fakeBinary = "#!/bin/sh\necho portabledesktop";
const server = Bun.serve({
port: 0,
fetch() {
return new Response(fakeBinary);
},
});
const state = await runTerraformApply(dir, {
agent_id: "foo",
url: `http://localhost:${server.port}/portabledesktop`,
...extraVars,
});
return {
state,
server,
[Symbol.asyncDispose]: async () => {
server.stop(true);
},
};
}
describe("portabledesktop", async () => {
await runTerraformInit(import.meta.dir);
testRequiredVariables(import.meta.dir, {
agent_id: "foo",
});
it("installs portabledesktop successfully", async () => {
await using fixture = await setupFakeBinaryServer(import.meta.dir);
await using container = await setupContainer("alpine/curl");
const script = findResourceInstance(fixture.state, "coder_script").script;
const resp = await execContainer(container.id, [
"sh",
"-c",
ENV_PREFIX + script,
]);
expect(resp.exitCode).toBe(0);
expect(resp.stdout).toContain("portabledesktop installed successfully");
// Check binary exists at CODER_SCRIPT_DATA_DIR.
const checkBinary = await execContainer(container.id, [
"test",
"-x",
"/tmp/coder-script-data/portabledesktop",
]);
expect(checkBinary.exitCode).toBe(0);
// Check symlink exists at CODER_SCRIPT_BIN_DIR.
const checkSymlink = await execContainer(container.id, [
"test",
"-L",
"/tmp/coder-script-data/bin/portabledesktop",
]);
expect(checkSymlink.exitCode).toBe(0);
}, 30000);
it("verifies checksum when sha256 is provided", async () => {
const fakeBinary = "#!/bin/sh\necho portabledesktop";
const hasher = new Bun.CryptoHasher("sha256");
hasher.update(fakeBinary);
const sha256 = hasher.digest("hex");
await using fixture = await setupFakeBinaryServer(import.meta.dir, {
sha256,
});
await using container = await setupContainer("alpine/curl");
const script = findResourceInstance(fixture.state, "coder_script").script;
const resp = await execContainer(container.id, [
"sh",
"-c",
ENV_PREFIX + script,
]);
expect(resp.exitCode).toBe(0);
expect(resp.stdout).toContain("Checksum verified successfully");
expect(resp.stdout).toContain("portabledesktop installed successfully");
}, 30000);
it("fails when sha256 does not match", async () => {
const wrongSha256 =
"0000000000000000000000000000000000000000000000000000000000000000";
await using fixture = await setupFakeBinaryServer(import.meta.dir, {
sha256: wrongSha256,
});
await using container = await setupContainer("alpine/curl");
const script = findResourceInstance(fixture.state, "coder_script").script;
const resp = await execContainer(container.id, [
"sh",
"-c",
ENV_PREFIX + script,
]);
expect(resp.exitCode).toBe(1);
expect(resp.stdout).toContain("Checksum mismatch");
}, 30000);
it("skips checksum verification when sha256 is not set", async () => {
await using fixture = await setupFakeBinaryServer(import.meta.dir);
await using container = await setupContainer("alpine/curl");
const script = findResourceInstance(fixture.state, "coder_script").script;
const resp = await execContainer(container.id, [
"sh",
"-c",
ENV_PREFIX + script,
]);
expect(resp.exitCode).toBe(0);
expect(resp.stdout).not.toContain("Checksum verified");
expect(resp.stdout).toContain("portabledesktop installed successfully");
}, 30000);
it("falls back to sudo when install_dir is not writable", async () => {
await using fixture = await setupFakeBinaryServer(import.meta.dir, {
install_dir: "/usr/local/bin",
});
await using container = await setupContainer("alpine/curl");
await execContainer(container.id, [
"sh",
"-c",
"apk add sudo && " +
"adduser -D testuser && " +
"echo 'testuser ALL=(ALL) NOPASSWD: ALL' >> /etc/sudoers && " +
"mkdir -p /usr/local/bin",
]);
const script = findResourceInstance(fixture.state, "coder_script").script;
const resp = await execContainer(
container.id,
["sh", "-c", ENV_PREFIX + script],
["--user", "testuser"],
);
expect(resp.exitCode).toBe(0);
expect(resp.stdout).toContain("via sudo");
expect(resp.stdout).toContain("portabledesktop installed successfully");
// Verify the binary was copied to the install_dir.
const check = await execContainer(container.id, [
"test",
"-x",
"/usr/local/bin/portabledesktop",
]);
expect(check.exitCode).toBe(0);
}, 30000);
it("creates install_dir if it does not exist", async () => {
await using fixture = await setupFakeBinaryServer(import.meta.dir, {
install_dir: "/opt/custom/bin",
});
await using container = await setupContainer("alpine/curl");
const script = findResourceInstance(fixture.state, "coder_script").script;
const resp = await execContainer(container.id, [
"sh",
"-c",
ENV_PREFIX + script,
]);
expect(resp.exitCode).toBe(0);
expect(resp.stdout).toContain("portabledesktop installed successfully");
const check = await execContainer(container.id, [
"test",
"-x",
"/opt/custom/bin/portabledesktop",
]);
expect(check.exitCode).toBe(0);
}, 30000);
it("falls back to wget when curl is not available", async () => {
await using fixture = await setupFakeBinaryServer(import.meta.dir);
await using container = await setupContainer("alpine");
// Install wget but ensure curl is not present.
await execContainer(container.id, [
"sh",
"-c",
"apk add wget && ! command -v curl",
]);
const script = findResourceInstance(fixture.state, "coder_script").script;
const resp = await execContainer(container.id, [
"sh",
"-c",
ENV_PREFIX + script,
]);
expect(resp.exitCode).toBe(0);
expect(resp.stdout).toContain("via wget");
expect(resp.stdout).toContain("portabledesktop installed successfully");
}, 30000);
});
@@ -0,0 +1,65 @@
terraform {
required_version = ">= 1.0"
required_providers {
coder = {
source = "coder/coder"
version = ">= 2.5"
}
}
}
variable "agent_id" {
type = string
description = "The ID of a Coder agent."
}
variable "install_dir" {
type = string
description = "Optional directory to copy the binary into (e.g. /usr/local/bin). The binary is always stored in the agent's script data directory and available on PATH via CODER_SCRIPT_BIN_DIR."
default = null
}
variable "url" {
type = string
description = "Custom download URL. Overrides the default GitHub latest release URL when set."
default = null
}
variable "sha256" {
type = string
description = "SHA256 checksum. When set, the downloaded binary is verified against it."
default = null
}
locals {
default_amd64_url = "https://github.com/coder/portabledesktop/releases/latest/download/portabledesktop-linux-x64"
default_arm64_url = "https://github.com/coder/portabledesktop/releases/latest/download/portabledesktop-linux-arm64"
using_custom_url = var.url != null
amd64_url = local.using_custom_url ? var.url : local.default_amd64_url
arm64_url = local.using_custom_url ? var.url : local.default_arm64_url
# Empty string signals "skip verification" to the shell script.
sha256 = var.sha256 != null ? var.sha256 : ""
install_dir = var.install_dir != null ? var.install_dir : ""
}
resource "coder_script" "portabledesktop" {
agent_id = var.agent_id
display_name = "Portable Desktop"
icon = "/icon/desktop.svg"
script = <<-EOT
#!/bin/sh
set -eu
echo -n '${base64encode(file("${path.module}/run.sh"))}' | base64 -d > /tmp/portabledesktop-install.sh
chmod +x /tmp/portabledesktop-install.sh
ARG_AMD64_URL="$(echo -n '${base64encode(local.amd64_url)}' | base64 -d)" \
ARG_ARM64_URL="$(echo -n '${base64encode(local.arm64_url)}' | base64 -d)" \
ARG_SHA256="$(echo -n '${base64encode(local.sha256)}' | base64 -d)" \
ARG_INSTALL_DIR="$(echo -n '${base64encode(local.install_dir)}' | base64 -d)" \
/tmp/portabledesktop-install.sh
EOT
run_on_start = true
}
@@ -0,0 +1,36 @@
run "plan_with_required_vars" {
command = plan
variables {
agent_id = "example-agent-id"
}
}
run "plan_with_custom_install_dir" {
command = plan
variables {
agent_id = "example-agent-id"
install_dir = "/opt/bin"
}
assert {
condition = resource.coder_script.portabledesktop.display_name == "Portable Desktop"
error_message = "Expected coder_script resource to have correct display name"
}
}
run "plan_with_custom_url" {
command = plan
variables {
agent_id = "example-agent-id"
url = "https://example.com/custom-portabledesktop"
sha256 = "abc123"
}
assert {
condition = resource.coder_script.portabledesktop.run_on_start == true
error_message = "Expected coder_script to run on start"
}
}
@@ -0,0 +1,132 @@
#!/usr/bin/env sh
# shellcheck disable=SC2292
# SC2292: We use [ ] instead of [[ ]] for POSIX sh compatibility.
set -eu
error() {
printf "ERROR: %s\n" "$@"
exit 1
}
# Check if portabledesktop is already in PATH.
if command -v portabledesktop > /dev/null 2>&1; then
printf "portabledesktop is already installed and in PATH.\n"
exit 0
fi
# Determine the storage path.
STORAGE_DIR="${CODER_SCRIPT_DATA_DIR}"
BINARY_PATH="${STORAGE_DIR}/portabledesktop"
mkdir -p "${STORAGE_DIR}"
# If the binary already exists and is executable, skip download.
if [ -x "${BINARY_PATH}" ]; then
printf "portabledesktop is already installed at %s, skipping download.\n" "${BINARY_PATH}"
else
# Detect architecture and select the appropriate download URL.
ARCH=$(uname -m)
case "${ARCH}" in
x86_64)
URL="${ARG_AMD64_URL}"
;;
aarch64)
URL="${ARG_ARM64_URL}"
;;
*)
error "Unsupported architecture: ${ARCH}"
;;
esac
# Select download tool.
if command -v curl > /dev/null 2>&1; then
DOWNLOAD_CMD="curl"
elif command -v wget > /dev/null 2>&1; then
DOWNLOAD_CMD="wget"
else
error "No download tool available (curl or wget required)."
fi
# Download with retry loop (3 attempts, 1s sleep between).
TMPFILE=$(mktemp)
MAX_ATTEMPTS=3
DOWNLOAD_SUCCESS=false
ATTEMPT=1
while [ "${ATTEMPT}" -le "${MAX_ATTEMPTS}" ]; do
printf "Downloading portabledesktop (attempt %s/%s) via %s...\n" "${ATTEMPT}" "${MAX_ATTEMPTS}" "${DOWNLOAD_CMD}"
DOWNLOAD_OK=false
if [ "${DOWNLOAD_CMD}" = "curl" ]; then
curl -fsSL "${URL}" -o "${TMPFILE}" && DOWNLOAD_OK=true
else
wget -qO "${TMPFILE}" "${URL}" && DOWNLOAD_OK=true
fi
if [ "${DOWNLOAD_OK}" = "true" ]; then
# Verify checksum when ARG_SHA256 is non-empty.
if [ -n "${ARG_SHA256}" ]; then
CHECKSUM_MATCH=false
if command -v sha256sum > /dev/null 2>&1; then
echo "${ARG_SHA256} ${TMPFILE}" | sha256sum -c - > /dev/null 2>&1 && CHECKSUM_MATCH=true
elif command -v shasum > /dev/null 2>&1; then
echo "${ARG_SHA256} ${TMPFILE}" | shasum -a 256 -c - > /dev/null 2>&1 && CHECKSUM_MATCH=true
else
rm -f "${TMPFILE}"
error "No SHA256 tool available (sha256sum or shasum required)."
fi
if [ "${CHECKSUM_MATCH}" != "true" ]; then
printf "WARNING: Checksum mismatch (attempt %s/%s): expected %s\n" \
"${ATTEMPT}" "${MAX_ATTEMPTS}" "${ARG_SHA256}"
rm -f "${TMPFILE}"
if [ "${ATTEMPT}" -lt "${MAX_ATTEMPTS}" ]; then
sleep 1
fi
ATTEMPT=$((ATTEMPT + 1))
continue
fi
printf "Checksum verified successfully.\n"
fi
DOWNLOAD_SUCCESS=true
break
else
printf "WARNING: Download failed (attempt %s/%s).\n" "${ATTEMPT}" "${MAX_ATTEMPTS}"
if [ "${ATTEMPT}" -lt "${MAX_ATTEMPTS}" ]; then
sleep 1
fi
fi
ATTEMPT=$((ATTEMPT + 1))
done
if [ "${DOWNLOAD_SUCCESS}" != "true" ]; then
rm -f "${TMPFILE}"
error "Failed to download portabledesktop after ${MAX_ATTEMPTS} attempts."
fi
# Make the binary executable and move to storage path.
chmod 755 "${TMPFILE}"
mv "${TMPFILE}" "${BINARY_PATH}"
fi
# Symlink into CODER_SCRIPT_BIN_DIR for PATH access.
if [ -n "${CODER_SCRIPT_BIN_DIR}" ] && [ ! -e "${CODER_SCRIPT_BIN_DIR}/portabledesktop" ]; then
ln -s "${CODER_SCRIPT_DATA_DIR}/portabledesktop" "${CODER_SCRIPT_BIN_DIR}/portabledesktop"
fi
# If ARG_INSTALL_DIR is set, copy the binary there with sudo fallback.
if [ -n "${ARG_INSTALL_DIR}" ]; then
if [ ! -d "${ARG_INSTALL_DIR}" ]; then
mkdir -p "${ARG_INSTALL_DIR}" 2> /dev/null || sudo mkdir -p "${ARG_INSTALL_DIR}" 2> /dev/null || true
fi
if cp "${CODER_SCRIPT_DATA_DIR}/portabledesktop" "${ARG_INSTALL_DIR}/portabledesktop" 2> /dev/null; then
printf "Copied portabledesktop to %s.\n" "${ARG_INSTALL_DIR}/portabledesktop"
elif sudo cp "${CODER_SCRIPT_DATA_DIR}/portabledesktop" "${ARG_INSTALL_DIR}/portabledesktop" 2> /dev/null; then
printf "Copied portabledesktop to %s (via sudo).\n" "${ARG_INSTALL_DIR}/portabledesktop"
else
error "Failed to copy portabledesktop to ${ARG_INSTALL_DIR}/portabledesktop."
fi
fi
printf "portabledesktop installed successfully.\n"
+47 -7
View File
@@ -15,7 +15,7 @@ Automatically installs [Node.js](https://github.com/nodejs/node) via [`nvm`](htt
module "nodejs" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/thezoker/nodejs/coder"
version = "1.0.13"
version = "1.1.0"
agent_id = coder_agent.example.id
}
```
@@ -28,17 +28,55 @@ This installs multiple versions of Node.js:
module "nodejs" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/thezoker/nodejs/coder"
version = "1.0.13"
version = "1.1.0"
agent_id = coder_agent.example.id
node_versions = [
"18",
"20",
"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.
```tf
module "nodejs" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/thezoker/nodejs/coder"
version = "1.1.0"
agent_id = coder_agent.example.id
pre_install_script = "echo 'Setting up prerequisites...'"
post_install_script = "npm install -g yarn pnpm"
}
```
## Cross-Module Dependency Ordering
This module uses `coder exp sync` to coordinate execution ordering with other modules. It exposes the following outputs for use with `coder exp sync want`:
- `install_script_name` — the sync name for the main Node.js installation script
- `pre_install_script_name` — the sync name for the pre-install script
- `post_install_script_name` — the sync name for the post-install script
For example, to ensure another module waits for Node.js to be fully installed:
```tf
module "nodejs" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/thezoker/nodejs/coder"
version = "1.1.0"
agent_id = coder_agent.example.id
}
# In another module's coder_script, wait for Node.js installation:
# coder exp sync want my-script ${module.nodejs[0].install_script_name}
```
## Full example
A example with all available options:
@@ -47,15 +85,17 @@ A example with all available options:
module "nodejs" {
count = data.coder_workspace.me.start_count
source = "registry.coder.com/thezoker/nodejs/coder"
version = "1.0.13"
version = "1.1.0"
agent_id = coder_agent.example.id
nvm_version = "1.0.13"
nvm_version = "v0.39.7"
nvm_install_prefix = "/opt/nvm"
node_versions = [
"16",
"18",
"20",
"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"
}
```
+118 -8
View File
@@ -38,15 +38,125 @@ variable "default_node_version" {
default = "node"
}
resource "coder_script" "nodejs" {
agent_id = var.agent_id
display_name = "Node.js:"
script = 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,
variable "pre_install_script" {
type = string
description = "Custom script to run before installing Node.js."
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) : ""
install_script = 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,
})
encoded_install_script = base64encode(local.install_script)
pre_install_script_name = "nodejs-pre_install_script"
install_script_name = "nodejs-install_script"
post_install_script_name = "nodejs-post_install_script"
module_dir_path = "$HOME/.nodejs-module"
pre_install_path = "${local.module_dir_path}/pre_install.sh"
pre_install_log_path = "${local.module_dir_path}/pre_install.log"
install_path = "${local.module_dir_path}/install.sh"
install_log_path = "${local.module_dir_path}/install.log"
post_install_path = "${local.module_dir_path}/post_install.sh"
post_install_log_path = "${local.module_dir_path}/post_install.log"
}
resource "coder_script" "pre_install_script" {
count = var.pre_install_script == null ? 0 : 1
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.pre_install_path}
chmod +x ${local.pre_install_path}
${local.pre_install_path} 2>&1 | tee ${local.pre_install_log_path}
EOT
}
resource "coder_script" "nodejs" {
agent_id = var.agent_id
display_name = "Node.js: Install"
script = <<-EOT
#!/bin/bash
set -o errexit
set -o pipefail
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 '${local.encoded_install_script}' | base64 -d > ${local.install_path}
chmod +x ${local.install_path}
${local.install_path} 2>&1 | tee ${local.install_log_path}
EOT
run_on_start = true
start_blocks_login = true
}
resource "coder_script" "post_install_script" {
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
mkdir -p ${local.module_dir_path}
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.post_install_path}
chmod +x ${local.post_install_path}
${local.post_install_path} 2>&1 | tee ${local.post_install_log_path}
EOT
}
output "pre_install_script_name" {
description = "The name of the pre-install script for coder exp sync coordination."
value = local.pre_install_script_name
}
output "install_script_name" {
description = "The name of the install script for coder exp sync coordination."
value = local.install_script_name
}
output "post_install_script_name" {
description = "The name of the post-install script for coder exp sync coordination."
value = local.post_install_script_name
}
@@ -0,0 +1,137 @@
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"
}
assert {
condition = output.install_script_name == "nodejs-install_script"
error_message = "install_script_name output should be set"
}
}
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"
}
assert {
condition = output.pre_install_script_name == "nodejs-pre_install_script"
error_message = "pre_install_script_name output should be set"
}
assert {
condition = output.post_install_script_name == "nodejs-post_install_script"
error_message = "post_install_script_name output should be set"
}
}
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"
}
}