Compare commits
2 Commits
main
...
claude-sel
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db433e4d34 | ||
|
|
99f3524160 |
6
.github/workflows/ci.yaml
vendored
6
.github/workflows/ci.yaml
vendored
@ -37,7 +37,7 @@ jobs:
|
||||
all:
|
||||
- '**'
|
||||
- name: Set up Terraform
|
||||
uses: coder/coder/.github/actions/setup-tf@2b778f292c2ddf8ac261683d0d5d8a18da1512f6 # v2.33.3
|
||||
uses: coder/coder/.github/actions/setup-tf@34584e909bbe6f501fb2cbdc994325b4d3f9e2ef # v2.32.0
|
||||
- name: Set up Bun
|
||||
uses: oven-sh/setup-bun@0c5077e51419868618aeaa5fe8019c62421857d6 # v2
|
||||
with:
|
||||
@ -87,13 +87,13 @@ jobs:
|
||||
bun-version: latest
|
||||
# Need Terraform for its formatter
|
||||
- name: Install Terraform
|
||||
uses: coder/coder/.github/actions/setup-tf@2b778f292c2ddf8ac261683d0d5d8a18da1512f6 # v2.33.3
|
||||
uses: coder/coder/.github/actions/setup-tf@34584e909bbe6f501fb2cbdc994325b4d3f9e2ef # v2.32.0
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
- name: Validate formatting
|
||||
run: bun fmt:ci
|
||||
- name: Check for typos
|
||||
uses: crate-ci/typos@aca895bf05aec0cb7dffa6f94495e923224d9f17 # v1.46.2
|
||||
uses: crate-ci/typos@cf5f1c29a8ac336af8568821ec41919923b05a83 # v1.45.1
|
||||
with:
|
||||
config: .github/typos.toml
|
||||
validate-readme-files:
|
||||
|
||||
2
.github/workflows/version-bump.yaml
vendored
2
.github/workflows/version-bump.yaml
vendored
@ -31,7 +31,7 @@ jobs:
|
||||
bun-version: latest
|
||||
|
||||
- name: Set up Terraform
|
||||
uses: coder/coder/.github/actions/setup-tf@2b778f292c2ddf8ac261683d0d5d8a18da1512f6 # v2.33.3
|
||||
uses: coder/coder/.github/actions/setup-tf@34584e909bbe6f501fb2cbdc994325b4d3f9e2ef # v2.32.0
|
||||
|
||||
- name: Install dependencies
|
||||
run: bun install
|
||||
|
||||
4
.github/workflows/zizmor.yaml
vendored
4
.github/workflows/zizmor.yaml
vendored
@ -27,7 +27,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor (blocking, HIGH only)
|
||||
uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
|
||||
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
|
||||
with:
|
||||
advanced-security: false
|
||||
annotations: true
|
||||
@ -49,7 +49,7 @@ jobs:
|
||||
persist-credentials: false
|
||||
|
||||
- name: Run zizmor (SARIF)
|
||||
uses: zizmorcore/zizmor-action@5f14fd08f7cf1cb1609c1e344975f152c7ee938d # v0.5.6
|
||||
uses: zizmorcore/zizmor-action@b1d7e1fb5de872772f31590499237e7cce841e8e # v0.5.3
|
||||
with:
|
||||
inputs: |
|
||||
.github/workflows
|
||||
|
||||
@ -1,4 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect width="7" height="7" x="14" y="3" rx="1"/>
|
||||
<path d="M10 21V8a1 1 0 0 0-1-1H4a1 1 0 0 0-1 1v12a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-5a1 1 0 0 0-1-1H3"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 339 B |
@ -1,5 +0,0 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="white" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<rect width="18" height="7" x="3" y="3" rx="1"/>
|
||||
<rect width="9" height="7" x="3" y="14" rx="1"/>
|
||||
<rect width="5" height="7" x="16" y="14" rx="1"/>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 336 B |
@ -0,0 +1,73 @@
|
||||
---
|
||||
display_name: Claude Code self-hosted runner
|
||||
description: Run Anthropic's Claude Code self-hosted runner as a long-lived process inside a Coder workspace, with per-workspace scoped self-eviction so the prebuild reconciler keeps the pool warm.
|
||||
icon: ../../../../.icons/claude.svg
|
||||
verified: false
|
||||
tags: [ai, claude, claude-code, anthropic, runner]
|
||||
---
|
||||
|
||||
# Claude Code self-hosted runner
|
||||
|
||||
Drops Anthropic's [Claude Code self-hosted runner](https://docs.anthropic.com/en/docs/claude-code/self-hosted-runners) into any Coder template that has a `coder_agent` and a workspace image with the runner binary installed (`/usr/local/bin/claude self-hosted-runner` by default).
|
||||
|
||||
The module owns the runner script (writes a per-session wrapper that forces `--permission-mode bypassPermissions`, then spawns a detached supervisor that runs the runner in the foreground and POSTs a delete build to self-evict on drain), the agent environment variables it needs, an optional bot-git askpass setup, and a host Docker socket gid fixup. Agent metadata items (lock status, active sessions, runner ID, last poll) are emitted via the `agent_metadata` output for the parent to splat into a `dynamic "metadata"` block.
|
||||
|
||||
The parent template still owns the `coder_agent` itself, the per-workspace scope-restricted self-evict token (minted via the `Mastercard/restapi` provider against an admin bootstrap token), the prebuild preset, and the infra block (`docker_container`, `kubernetes_pod`, etc.).
|
||||
|
||||
> [!IMPORTANT]
|
||||
> This module is part of the [Claude Code self-hosted runners on Coder](https://coder.com/docs/ai-coder/claude-code-self-hosted-runners) recipe, which currently targets Anthropic's EAP build of the runner. Both the runner binary and the wire contract are still evolving; expect API drift until Anthropic ships GA.
|
||||
|
||||
## Usage
|
||||
|
||||
```tf
|
||||
module "claude_self_hosted_runner" {
|
||||
source = "registry.coder.com/coder-labs/claude-self-hosted-runner/coder"
|
||||
version = "1.0.0"
|
||||
|
||||
agent_id = coder_agent.main.id
|
||||
workspace_id = data.coder_workspace.me.id
|
||||
pool_secret = var.pool_secret
|
||||
self_evict_token = jsondecode(restapi_object.self_evict_token.api_response).key
|
||||
git_bot_token = var.git_bot_token
|
||||
capacity = tonumber(data.coder_parameter.capacity.value)
|
||||
}
|
||||
|
||||
resource "coder_agent" "main" {
|
||||
# ... arch, os, dir, startup_script_behavior, etc.
|
||||
|
||||
# Static metadata blocks coexist with the dynamic block below;
|
||||
# Terraform concatenates them on the same coder_agent.
|
||||
metadata {
|
||||
display_name = "CPU"
|
||||
key = "cpu"
|
||||
script = "top -bn1 | awk '/Cpu/ {print $2 \"%\"}'"
|
||||
interval = 10
|
||||
timeout = 5
|
||||
}
|
||||
|
||||
dynamic "metadata" {
|
||||
for_each = module.claude_self_hosted_runner.agent_metadata
|
||||
content {
|
||||
display_name = metadata.value.display_name
|
||||
key = metadata.value.key
|
||||
interval = metadata.value.interval
|
||||
timeout = metadata.value.timeout
|
||||
script = metadata.value.script
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## What the module does
|
||||
|
||||
- Writes `$HOME/.claude/wrapper.sh` at agent start. The wrapper appends `--permission-mode bypassPermissions` after `"$@"` so unattended sessions never stall on a tool-approval prompt; Claude Code's flag parser is last-occurrence-wins, so this overrides the server-supplied permission mode.
|
||||
- Sets up the runner's required environment (`CLAUDE_POOL_SECRET`, `CLAUDE_CAPACITY`, `GIT_BOT_TOKEN`, `CODER_SELF_TOKEN`, `CODER_WORKSPACE_ID`) via `coder_env` resources on the agent.
|
||||
- Spawns a `setsid nohup` supervisor that runs the runner in the foreground. When the runner exits on drain, the supervisor POSTs `/api/v2/workspaces/{id}/builds` with `{"transition":"delete"}` to self-evict, so Coder's prebuild reconciler can queue a replacement.
|
||||
- Wires up `GIT_ASKPASS` if `git_bot_token` is supplied so the runner's child claude can `git push` without baking credentials into the image.
|
||||
- If the parent template mounts the host Docker socket at `/var/run/docker.sock` and the gid does not match the in-container `docker` group, chgrps the socket so the workspace user can use it without sudo.
|
||||
|
||||
## Self-eviction security model
|
||||
|
||||
The `self_evict_token` input is minted by the parent template via the `Mastercard/restapi` provider at template build time, against an admin bootstrap token that lives in Terraform state and is never injected into the workspace. The minted token is scoped to `workspace:delete + workspace:read + template:read + user:read` and allow-listed to this single workspace's UUID. A leaked copy can do exactly one thing: delete this one workspace. No read of peer prebuilds, no SSH, no external auth, no git creds.
|
||||
|
||||
The supervisor uses raw `curl` against `/api/v2/workspaces/{id}/builds`, not the `coder delete` CLI. The CLI fetches workspace resources first, which fails against the scoped token whose allow-list intersection excludes peer workspaces.
|
||||
185
registry/coder-labs/modules/claude-self-hosted-runner/main.tf
Normal file
185
registry/coder-labs/modules/claude-self-hosted-runner/main.tf
Normal file
@ -0,0 +1,185 @@
|
||||
terraform {
|
||||
required_version = ">= 1.5"
|
||||
|
||||
required_providers {
|
||||
coder = {
|
||||
source = "coder/coder"
|
||||
version = ">= 2.13"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
variable "agent_id" {
|
||||
type = string
|
||||
description = "The ID of a Coder agent."
|
||||
}
|
||||
|
||||
variable "workspace_id" {
|
||||
type = string
|
||||
description = "data.coder_workspace.me.id from the parent template. Used by the supervisor to self-evict via the workspace builds endpoint."
|
||||
}
|
||||
|
||||
variable "pool_secret" {
|
||||
type = string
|
||||
description = "Claude Code self-hosted runner pool secret (from claude.ai)."
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "self_evict_token" {
|
||||
type = string
|
||||
description = "Per-workspace, scope-restricted Coder API token. Scope = workspace:delete + workspace:read + template:read + user:read, allow_list = this workspace's UUID. A leaked copy can only delete this one workspace. The parent template mints it via the Mastercard/restapi provider at build time."
|
||||
sensitive = true
|
||||
}
|
||||
|
||||
variable "git_bot_token" {
|
||||
type = string
|
||||
description = "Optional git PAT for the bot identity. Wired through GIT_ASKPASS so the runner's child claude can push without baking credentials into the image."
|
||||
sensitive = true
|
||||
default = ""
|
||||
}
|
||||
|
||||
variable "capacity" {
|
||||
type = number
|
||||
description = "Maximum sessions the runner serves at once. The runner locks to one Anthropic user; this caps parallelism within that user's queue."
|
||||
default = 4
|
||||
validation {
|
||||
condition = var.capacity >= 1 && var.capacity <= 16
|
||||
error_message = "capacity must be between 1 and 16."
|
||||
}
|
||||
}
|
||||
|
||||
variable "runner_binary_path" {
|
||||
type = string
|
||||
description = "Path to the `claude self-hosted-runner` binary inside the workspace."
|
||||
default = "/usr/local/bin/claude"
|
||||
}
|
||||
|
||||
variable "claude_binary_path" {
|
||||
type = string
|
||||
description = "Path to the Claude Code binary the wrapper execs for each session."
|
||||
default = "/opt/claude/claude"
|
||||
}
|
||||
|
||||
variable "order" {
|
||||
type = number
|
||||
description = "Order of the runner script in the agent UI."
|
||||
default = null
|
||||
}
|
||||
|
||||
resource "coder_env" "pool_secret" {
|
||||
agent_id = var.agent_id
|
||||
name = "CLAUDE_POOL_SECRET"
|
||||
value = var.pool_secret
|
||||
}
|
||||
|
||||
resource "coder_env" "capacity" {
|
||||
agent_id = var.agent_id
|
||||
name = "CLAUDE_CAPACITY"
|
||||
value = tostring(var.capacity)
|
||||
}
|
||||
|
||||
resource "coder_env" "git_bot_token" {
|
||||
agent_id = var.agent_id
|
||||
name = "GIT_BOT_TOKEN"
|
||||
value = var.git_bot_token
|
||||
}
|
||||
|
||||
resource "coder_env" "self_token" {
|
||||
agent_id = var.agent_id
|
||||
name = "CODER_SELF_TOKEN"
|
||||
value = var.self_evict_token
|
||||
}
|
||||
|
||||
resource "coder_env" "workspace_id" {
|
||||
agent_id = var.agent_id
|
||||
name = "CODER_WORKSPACE_ID"
|
||||
value = var.workspace_id
|
||||
}
|
||||
|
||||
resource "coder_script" "claude_runner" {
|
||||
agent_id = var.agent_id
|
||||
display_name = "Claude self-hosted runner"
|
||||
icon = "/icon/code.svg"
|
||||
run_on_start = true
|
||||
start_blocks_login = false
|
||||
script = templatefile("${path.module}/scripts/run.sh", {
|
||||
CLAUDE_BINARY_PATH = var.claude_binary_path
|
||||
RUNNER_BINARY_PATH = var.runner_binary_path
|
||||
})
|
||||
}
|
||||
|
||||
# Agent metadata items. The parent splats this list into a
|
||||
# `dynamic "metadata"` block on its own `coder_agent` because nested
|
||||
# blocks cannot be injected from a module. Scraped from the runner's
|
||||
# local /healthz and /metrics endpoints; this is the only window a
|
||||
# Coder admin has into who the Anthropic pool has bound this workspace
|
||||
# to (the runner does not expose the locked user's email over its
|
||||
# local endpoints; that lives in claude.ai > Self-hosted runner pools).
|
||||
output "agent_metadata" {
|
||||
description = "List of agent metadata items the parent template should splat into a `dynamic \"metadata\"` block on its coder_agent."
|
||||
value = [
|
||||
{
|
||||
display_name = "Lock status"
|
||||
key = "0_lock_status"
|
||||
interval = 10
|
||||
timeout = 5
|
||||
# The runner does not expose its locked state via /metrics or
|
||||
# /healthz in the current BYOC build, so we infer it from
|
||||
# active_sessions and latch a sticky flag on disk: once a
|
||||
# session has landed, the runner is locked to that Anthropic
|
||||
# user for its entire lifetime per Anthropic's spec, even when
|
||||
# the active count drops back to zero between sessions.
|
||||
script = <<-EOT
|
||||
flag="$HOME/.claude/locked"
|
||||
active=$(curl -fsS http://127.0.0.1:8080/healthz 2>/dev/null \
|
||||
| jq -r '.active_sessions // 0')
|
||||
if [ "$${active:-0}" -gt 0 ] && [ ! -f "$flag" ]; then
|
||||
touch "$flag" 2>/dev/null || true
|
||||
fi
|
||||
if [ -f "$flag" ]; then
|
||||
printf 'locked'
|
||||
else
|
||||
printf 'unlocked'
|
||||
fi
|
||||
EOT
|
||||
},
|
||||
{
|
||||
display_name = "Active sessions"
|
||||
key = "1_active_sessions"
|
||||
interval = 5
|
||||
timeout = 5
|
||||
script = <<-EOT
|
||||
active=$(curl -fsS http://127.0.0.1:8080/healthz 2>/dev/null \
|
||||
| jq -r '.active_sessions // empty')
|
||||
if [ -z "$active" ]; then echo '?'; exit 0; fi
|
||||
printf '%s / %s' "$active" "$${CLAUDE_CAPACITY:-1}"
|
||||
EOT
|
||||
},
|
||||
{
|
||||
display_name = "Runner ID"
|
||||
key = "2_runner_id"
|
||||
interval = 30
|
||||
timeout = 5
|
||||
script = <<-EOT
|
||||
curl -fsS http://127.0.0.1:8080/healthz 2>/dev/null \
|
||||
| jq -r '.runner_id // "(starting)"'
|
||||
EOT
|
||||
},
|
||||
{
|
||||
display_name = "Last Anthropic poll"
|
||||
key = "3_last_poll"
|
||||
interval = 15
|
||||
timeout = 5
|
||||
script = <<-EOT
|
||||
age=$(curl -fsS http://127.0.0.1:8080/healthz 2>/dev/null \
|
||||
| jq -r '.last_poll_age_ms // empty')
|
||||
if [ -z "$age" ]; then echo '?'; exit 0; fi
|
||||
if [ "$age" -lt 30000 ]; then
|
||||
printf 'ok (%sms ago)' "$age"
|
||||
else
|
||||
printf 'stale (%ss ago)' $((age/1000))
|
||||
fi
|
||||
EOT
|
||||
},
|
||||
]
|
||||
}
|
||||
@ -0,0 +1,123 @@
|
||||
run "plan_with_required_vars" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workspace_id = "test-workspace"
|
||||
pool_secret = "test-pool-secret"
|
||||
self_evict_token = "test-self-token"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(resource.coder_env.pool_secret.value) > 0
|
||||
error_message = "pool_secret env should be set"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.capacity.value == "4"
|
||||
error_message = "default capacity should be 4"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_script.claude_runner.display_name == "Claude self-hosted runner"
|
||||
error_message = "expected the runner coder_script display_name"
|
||||
}
|
||||
}
|
||||
|
||||
run "custom_capacity_and_binary_paths" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workspace_id = "test-workspace"
|
||||
pool_secret = "test-pool-secret"
|
||||
self_evict_token = "test-self-token"
|
||||
capacity = 8
|
||||
claude_binary_path = "/custom/claude"
|
||||
runner_binary_path = "/custom/runner"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.capacity.value == "8"
|
||||
error_message = "capacity input should flow into CLAUDE_CAPACITY env"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.claude_runner.script, "/custom/claude")
|
||||
error_message = "claude_binary_path should appear in the rendered script"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(resource.coder_script.claude_runner.script, "/custom/runner")
|
||||
error_message = "runner_binary_path should appear in the rendered script"
|
||||
}
|
||||
}
|
||||
|
||||
run "git_bot_token_optional" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workspace_id = "test-workspace"
|
||||
pool_secret = "test-pool-secret"
|
||||
self_evict_token = "test-self-token"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = resource.coder_env.git_bot_token.value == ""
|
||||
error_message = "git_bot_token should default to empty string"
|
||||
}
|
||||
}
|
||||
|
||||
run "capacity_validation_rejects_zero" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workspace_id = "test-workspace"
|
||||
pool_secret = "test-pool-secret"
|
||||
self_evict_token = "test-self-token"
|
||||
capacity = 0
|
||||
}
|
||||
|
||||
expect_failures = [
|
||||
var.capacity,
|
||||
]
|
||||
}
|
||||
|
||||
run "capacity_validation_rejects_high" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workspace_id = "test-workspace"
|
||||
pool_secret = "test-pool-secret"
|
||||
self_evict_token = "test-self-token"
|
||||
capacity = 17
|
||||
}
|
||||
|
||||
expect_failures = [
|
||||
var.capacity,
|
||||
]
|
||||
}
|
||||
|
||||
run "agent_metadata_output_has_four_items" {
|
||||
command = apply
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent"
|
||||
workspace_id = "test-workspace"
|
||||
pool_secret = "test-pool-secret"
|
||||
self_evict_token = "test-self-token"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = length(output.agent_metadata) == 4
|
||||
error_message = "agent_metadata should expose four scraping items"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = output.agent_metadata[0].key == "0_lock_status"
|
||||
error_message = "first metadata item should be lock_status"
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,114 @@
|
||||
#!/usr/bin/env bash
|
||||
# Wires up everything the Claude Code self-hosted runner needs at agent
|
||||
# start, then spawns a detached supervisor that keeps the runner alive
|
||||
# and self-evicts on drain.
|
||||
#
|
||||
# Runtime env (set by coder_env in main.tf):
|
||||
# CLAUDE_POOL_SECRET Anthropic pool secret (mandatory).
|
||||
# CLAUDE_CAPACITY Max parallel sessions per runner (default 1).
|
||||
# GIT_BOT_TOKEN Optional bot PAT for GIT_ASKPASS.
|
||||
# CODER_SELF_TOKEN Per-workspace scope-restricted Coder API token.
|
||||
# CODER_WORKSPACE_ID This workspace's UUID, used by self-eviction.
|
||||
# CODER_AGENT_URL Set by the Coder agent itself.
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
CLAUDE_BINARY_PATH='${CLAUDE_BINARY_PATH}'
|
||||
RUNNER_BINARY_PATH='${RUNNER_BINARY_PATH}'
|
||||
|
||||
if [ -z "$${CLAUDE_POOL_SECRET:-}" ]; then
|
||||
echo "CLAUDE_POOL_SECRET is empty. Set the pool_secret input on the module."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
install -d -m 0700 "$HOME/.claude"
|
||||
|
||||
# --- Bot git askpass ----------------------------------------------------
|
||||
if [ -n "$${GIT_BOT_TOKEN:-}" ]; then
|
||||
install -d -m 0700 "$HOME/.git-creds"
|
||||
cat > "$HOME/.git-creds/askpass.sh" << 'ASK'
|
||||
#!/bin/sh
|
||||
printf '%s' "$GIT_BOT_TOKEN"
|
||||
ASK
|
||||
chmod 0500 "$HOME/.git-creds/askpass.sh"
|
||||
git config --global core.askPass "$HOME/.git-creds/askpass.sh"
|
||||
git config --global credential.helper ''
|
||||
fi
|
||||
|
||||
# --- Host Docker socket gid fixup --------------------------------------
|
||||
if [ -S /var/run/docker.sock ]; then
|
||||
sock_gid=$(stat -c %g /var/run/docker.sock)
|
||||
docker_gid=$(getent group docker | cut -d: -f3 || true)
|
||||
if [ -n "$${docker_gid:-}" ] && [ "$${sock_gid}" != "$${docker_gid}" ]; then
|
||||
sudo chgrp "$${docker_gid}" /var/run/docker.sock 2> /dev/null || true
|
||||
fi
|
||||
fi
|
||||
|
||||
# --- Pool secret on disk -----------------------------------------------
|
||||
POOL_SECRET_FILE="$HOME/.claude/pool-secret"
|
||||
rm -f "$POOL_SECRET_FILE"
|
||||
umask 077
|
||||
printf '%s' "$${CLAUDE_POOL_SECRET}" > "$POOL_SECRET_FILE"
|
||||
chmod 0400 "$POOL_SECRET_FILE"
|
||||
|
||||
# --- Wrapper script -----------------------------------------------------
|
||||
# Runner execs this once per session, appending its server-computed
|
||||
# flags. Claude Code's flag parser is last-occurrence-wins, so flags
|
||||
# after "$@" win. Force --permission-mode bypassPermissions so
|
||||
# unattended sessions never stall on a tool-approval prompt.
|
||||
WRAPPER="$HOME/.claude/wrapper.sh"
|
||||
{
|
||||
echo '#!/bin/bash'
|
||||
echo "exec $${CLAUDE_BINARY_PATH} \"\$@\" --permission-mode bypassPermissions"
|
||||
} > "$WRAPPER"
|
||||
chmod 0755 "$WRAPPER"
|
||||
|
||||
# --- Supervisor --------------------------------------------------------
|
||||
# Runs the runner in the foreground; on runner exit POSTs a delete
|
||||
# build to self-evict. Raw curl, not `coder delete`: the CLI fetches
|
||||
# workspace resources first, which fails with the per-workspace
|
||||
# scoped token whose allow-list excludes peer prebuilds.
|
||||
#
|
||||
# Single-quoted heredoc, so nothing is expanded by the outer shell.
|
||||
# The supervisor reads its env vars (CODER_SELF_TOKEN, CODER_AGENT_URL,
|
||||
# etc.) at runtime, when it's invoked under setsid.
|
||||
SUPERVISOR="$HOME/.claude/supervisor.sh"
|
||||
cat > "$SUPERVISOR" << SUP
|
||||
#!/usr/bin/env bash
|
||||
set -uo pipefail
|
||||
exec >>"\$HOME/.claude/supervisor.log" 2>&1
|
||||
echo "[supervisor] start \$(date -Is)"
|
||||
|
||||
$${RUNNER_BINARY_PATH} self-hosted-runner \\
|
||||
--pool-secret-file "\$HOME/.claude/pool-secret" \\
|
||||
--capacity "\$${CLAUDE_CAPACITY:-1}" \\
|
||||
--log-file "\$HOME/.claude/runner.log" \\
|
||||
--exec-path "\$HOME/.claude/wrapper.sh"
|
||||
echo "[supervisor] runner exited rc=\$? \$(date -Is)"
|
||||
|
||||
if [ -z "\$${CODER_SELF_TOKEN:-}" ]; then
|
||||
echo "[supervisor] CODER_SELF_TOKEN is empty; skipping self-eviction."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
http_code=\$(curl -s -o /tmp/evict.out -w "%%{http_code}" \\
|
||||
-X POST \\
|
||||
-H "Coder-Session-Token: \$CODER_SELF_TOKEN" \\
|
||||
-H "Content-Type: application/json" \\
|
||||
-d '{"transition":"delete"}' \\
|
||||
"\$CODER_AGENT_URL/api/v2/workspaces/\$CODER_WORKSPACE_ID/builds")
|
||||
if [ "\$http_code" = "201" ]; then
|
||||
echo "[supervisor] self-eviction queued (HTTP 201)."
|
||||
else
|
||||
echo "[supervisor] self-eviction failed (HTTP \$http_code): \$(head -c 300 /tmp/evict.out)"
|
||||
fi
|
||||
SUP
|
||||
chmod 0700 "$SUPERVISOR"
|
||||
|
||||
# Detach with setsid + nohup. The supervisor reopens stdout/stderr to
|
||||
# its own logfile; redirect all standard fds here to /dev/null so this
|
||||
# script's exit doesn't drag the supervisor with it.
|
||||
setsid nohup "$SUPERVISOR" < /dev/null > /dev/null 2>&1 &
|
||||
disown
|
||||
|
||||
echo "Runner spawned as detached supervisor (pid=$!). See ~/.claude/supervisor.log."
|
||||
@ -13,7 +13,7 @@ Install and configure the [Claude Code](https://docs.anthropic.com/en/docs/agent
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "5.2.0"
|
||||
version = "5.1.0"
|
||||
agent_id = coder_agent.main.id
|
||||
anthropic_api_key = "xxxx-xxxxx-xxxx"
|
||||
}
|
||||
@ -47,7 +47,7 @@ locals {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "5.2.0"
|
||||
version = "5.1.0"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = local.claude_workdir
|
||||
anthropic_api_key = "xxxx-xxxxx-xxxx"
|
||||
@ -78,7 +78,7 @@ resource "coder_app" "claude" {
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "5.2.0"
|
||||
version = "5.1.0"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
enable_ai_gateway = true
|
||||
@ -95,33 +95,6 @@ Claude Code then routes API requests through Coder's AI Gateway instead of direc
|
||||
> [!CAUTION]
|
||||
> `enable_ai_gateway = true` is mutually exclusive with `anthropic_api_key` and `claude_code_oauth_token`. Setting any of them together fails at plan time.
|
||||
|
||||
### Enterprise policy via managed settings
|
||||
|
||||
The `managed_settings` input writes a policy file to `/etc/claude-code/managed-settings.d/10-coder.json` inside the workspace. Claude Code reads this directory at startup with the highest configuration precedence, so users cannot override these values in their own `~/.claude/settings.json`. This is a local file mechanism and works with any inference backend (Anthropic API, AWS Bedrock, Google Vertex AI, or AI Gateway).
|
||||
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "5.2.0"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
anthropic_api_key = "xxxx-xxxxx-xxxx"
|
||||
|
||||
managed_settings = {
|
||||
permissions = {
|
||||
defaultMode = "acceptEdits"
|
||||
disableBypassPermissionsMode = "disable"
|
||||
deny = ["Bash(curl:*)", "Bash(wget:*)", "WebFetch"]
|
||||
}
|
||||
env = {
|
||||
DISABLE_TELEMETRY = "0"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
See the [Claude Code settings reference](https://docs.anthropic.com/en/docs/claude-code/settings) for the full schema. Common keys: `permissions` (`defaultMode`, `allow`, `deny`, `disableBypassPermissionsMode`, `additionalDirectories`), `env`, `model`, `apiKeyHelper`, `hooks`, `cleanupPeriodDays`.
|
||||
|
||||
### Advanced Configuration
|
||||
|
||||
This example shows version pinning, a pre-installed binary path, a custom model, and MCP servers.
|
||||
@ -129,7 +102,7 @@ This example shows version pinning, a pre-installed binary path, a custom model,
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "5.2.0"
|
||||
version = "5.1.0"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
|
||||
@ -193,7 +166,7 @@ Downstream `coder_script` resources can wait for this module's install pipeline
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "5.2.0"
|
||||
version = "5.1.0"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
anthropic_api_key = "xxxx-xxxxx-xxxx"
|
||||
@ -279,7 +252,7 @@ resource "coder_env" "bedrock_api_key" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "5.2.0"
|
||||
version = "5.1.0"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
model = "global.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
||||
@ -336,7 +309,7 @@ resource "coder_env" "google_application_credentials" {
|
||||
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "5.2.0"
|
||||
version = "5.1.0"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
model = "claude-sonnet-4@20250514"
|
||||
@ -377,7 +350,7 @@ The module automatically tags every span and metric with `coder.workspace_id`, `
|
||||
```tf
|
||||
module "claude-code" {
|
||||
source = "registry.coder.com/coder/claude-code/coder"
|
||||
version = "5.2.0"
|
||||
version = "5.1.0"
|
||||
agent_id = coder_agent.main.id
|
||||
workdir = "/home/coder/project"
|
||||
anthropic_api_key = "xxxx-xxxxx-xxxx"
|
||||
|
||||
@ -382,13 +382,10 @@ describe("claude-code", async () => {
|
||||
const parsed = JSON.parse(claudeConfig);
|
||||
expect(parsed.autoUpdaterStatus).toBe("disabled");
|
||||
expect(parsed.hasCompletedOnboarding).toBe(true);
|
||||
expect(parsed.bypassPermissionsModeAccepted).toBe(true);
|
||||
expect(parsed.hasAcknowledgedCostThreshold).toBe(true);
|
||||
expect(parsed.projects[workdir].hasCompletedProjectOnboarding).toBe(true);
|
||||
expect(parsed.projects[workdir].hasTrustDialogAccepted).toBe(true);
|
||||
// Permission posture is delivered via /etc/claude-code/managed-settings.d/,
|
||||
// not user-writable ~/.claude.json acceptance flags.
|
||||
expect(parsed.bypassPermissionsModeAccepted).toBeUndefined();
|
||||
expect(parsed.autoModeAccepted).toBeUndefined();
|
||||
});
|
||||
|
||||
test("standalone-mode-with-oauth-token", async () => {
|
||||
@ -416,7 +413,7 @@ describe("claude-code", async () => {
|
||||
);
|
||||
const parsed = JSON.parse(claudeConfig);
|
||||
expect(parsed.hasCompletedOnboarding).toBe(true);
|
||||
expect(parsed.bypassPermissionsModeAccepted).toBeUndefined();
|
||||
expect(parsed.bypassPermissionsModeAccepted).toBe(true);
|
||||
});
|
||||
|
||||
test("standalone-mode-no-auth", async () => {
|
||||
@ -439,49 +436,6 @@ describe("claude-code", async () => {
|
||||
expect(resp.stdout.trim()).toBe("ABSENT");
|
||||
});
|
||||
|
||||
test("claude-managed-settings-written", async () => {
|
||||
const { id, scripts } = await setup({
|
||||
moduleVariables: {
|
||||
managed_settings: JSON.stringify({
|
||||
permissions: {
|
||||
defaultMode: "acceptEdits",
|
||||
disableBypassPermissionsMode: "disable",
|
||||
deny: ["Bash(rm -rf*)"],
|
||||
},
|
||||
}),
|
||||
},
|
||||
});
|
||||
await runScripts(id, scripts);
|
||||
|
||||
const policy = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"cat /etc/claude-code/managed-settings.d/10-coder.json",
|
||||
]);
|
||||
expect(policy.exitCode).toBe(0);
|
||||
expect(policy.stdout).toContain('"defaultMode":"acceptEdits"');
|
||||
expect(policy.stdout).toContain('"disableBypassPermissionsMode":"disable"');
|
||||
expect(policy.stdout).toContain('"deny":["Bash(rm -rf*)"]');
|
||||
|
||||
const installLog = await readFileContainer(
|
||||
id,
|
||||
"/home/coder/.coder-modules/coder/claude-code/logs/install.log",
|
||||
);
|
||||
expect(installLog).toContain("Wrote Claude Code managed settings");
|
||||
});
|
||||
|
||||
test("claude-managed-settings-not-set", async () => {
|
||||
const { id, scripts } = await setup();
|
||||
await runScripts(id, scripts);
|
||||
|
||||
const resp = await execContainer(id, [
|
||||
"bash",
|
||||
"-c",
|
||||
"test -e /etc/claude-code/managed-settings.d/10-coder.json && echo EXISTS || echo ABSENT",
|
||||
]);
|
||||
expect(resp.stdout.trim()).toBe("ABSENT");
|
||||
});
|
||||
|
||||
test("telemetry-otel", async () => {
|
||||
const { coderEnvVars } = await setup({
|
||||
moduleVariables: {
|
||||
|
||||
@ -102,12 +102,6 @@ variable "claude_binary_path" {
|
||||
}
|
||||
}
|
||||
|
||||
variable "managed_settings" {
|
||||
type = any
|
||||
description = "Policy settings written to /etc/claude-code/managed-settings.d/10-coder.json. Highest-precedence client config; works with any inference backend (Anthropic API, Bedrock, Vertex, AI Gateway). See https://docs.anthropic.com/en/docs/claude-code/settings for the schema."
|
||||
default = null
|
||||
}
|
||||
|
||||
variable "enable_ai_gateway" {
|
||||
type = bool
|
||||
description = "Use AI Gateway for Claude Code. https://coder.com/docs/ai-coder/ai-gateway"
|
||||
@ -243,7 +237,6 @@ locals {
|
||||
ARG_MCP = var.mcp != "" ? base64encode(var.mcp) : ""
|
||||
ARG_MCP_CONFIG_REMOTE_PATH = base64encode(jsonencode(var.mcp_config_remote_path))
|
||||
ARG_ENABLE_AI_GATEWAY = tostring(var.enable_ai_gateway)
|
||||
ARG_MANAGED_SETTINGS_JSON = var.managed_settings != null ? base64encode(jsonencode(var.managed_settings)) : ""
|
||||
})
|
||||
module_dir_name = ".coder-modules/coder/claude-code"
|
||||
}
|
||||
|
||||
@ -283,47 +283,3 @@ run "test_workdir_optional" {
|
||||
error_message = "workdir should default to null when omitted"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_managed_settings" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-managed-settings"
|
||||
workdir = "/home/coder/project"
|
||||
managed_settings = {
|
||||
permissions = {
|
||||
defaultMode = "acceptEdits"
|
||||
disableBypassPermissionsMode = "disable"
|
||||
deny = ["Bash(rm -rf*)"]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.managed_settings.permissions.defaultMode == "acceptEdits"
|
||||
error_message = "managed_settings should accept the permissions object"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(local.install_script, "/etc/claude-code/managed-settings.d")
|
||||
error_message = "install script should reference the managed-settings.d drop-in directory"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = strcontains(local.install_script, base64encode(jsonencode(var.managed_settings)))
|
||||
error_message = "install script should embed the base64-encoded managed_settings JSON"
|
||||
}
|
||||
}
|
||||
|
||||
run "test_managed_settings_default_null" {
|
||||
command = plan
|
||||
|
||||
variables {
|
||||
agent_id = "test-agent-managed-settings-default"
|
||||
}
|
||||
|
||||
assert {
|
||||
condition = var.managed_settings == null
|
||||
error_message = "managed_settings should default to null when omitted"
|
||||
}
|
||||
}
|
||||
|
||||
@ -17,7 +17,6 @@ ARG_CLAUDE_BINARY_PATH="$${ARG_CLAUDE_BINARY_PATH//\$HOME/$HOME}"
|
||||
ARG_MCP=$(echo -n '${ARG_MCP}' | base64 -d)
|
||||
ARG_MCP_CONFIG_REMOTE_PATH=$(echo -n '${ARG_MCP_CONFIG_REMOTE_PATH}' | base64 -d)
|
||||
ARG_ENABLE_AI_GATEWAY='${ARG_ENABLE_AI_GATEWAY}'
|
||||
ARG_MANAGED_SETTINGS_JSON=$(echo -n '${ARG_MANAGED_SETTINGS_JSON}' | base64 -d)
|
||||
|
||||
export PATH="$${ARG_CLAUDE_BINARY_PATH}:$PATH"
|
||||
|
||||
@ -30,7 +29,6 @@ printf "ARG_CLAUDE_BINARY_PATH: %s\n" "$${ARG_CLAUDE_BINARY_PATH}"
|
||||
printf "ARG_MCP: %s\n" "$${ARG_MCP}"
|
||||
printf "ARG_MCP_CONFIG_REMOTE_PATH: %s\n" "$${ARG_MCP_CONFIG_REMOTE_PATH}"
|
||||
printf "ARG_ENABLE_AI_GATEWAY: %s\n" "$${ARG_ENABLE_AI_GATEWAY}"
|
||||
printf "ARG_MANAGED_SETTINGS_JSON: %s\n" "$${ARG_MANAGED_SETTINGS_JSON}"
|
||||
|
||||
echo "--------------------------------"
|
||||
|
||||
@ -146,32 +144,6 @@ function setup_claude_configurations() {
|
||||
|
||||
}
|
||||
|
||||
function write_managed_settings() {
|
||||
if [ -z "$${ARG_MANAGED_SETTINGS_JSON}" ]; then
|
||||
return
|
||||
fi
|
||||
|
||||
local dropin_dir="/etc/claude-code/managed-settings.d"
|
||||
local target="$${dropin_dir}/10-coder.json"
|
||||
|
||||
if ! echo "$${ARG_MANAGED_SETTINGS_JSON}" | jq empty 2> /dev/null; then
|
||||
echo "Warning: managed_settings is not valid JSON, skipping policy write"
|
||||
return
|
||||
fi
|
||||
|
||||
if command_exists sudo; then
|
||||
sudo mkdir -p "$${dropin_dir}"
|
||||
echo "$${ARG_MANAGED_SETTINGS_JSON}" | sudo tee "$${target}" > /dev/null
|
||||
sudo chmod 0644 "$${target}"
|
||||
else
|
||||
mkdir -p "$${dropin_dir}"
|
||||
echo "$${ARG_MANAGED_SETTINGS_JSON}" > "$${target}"
|
||||
chmod 0644 "$${target}"
|
||||
fi
|
||||
|
||||
echo "Wrote Claude Code managed settings to $${target}"
|
||||
}
|
||||
|
||||
function configure_standalone_mode() {
|
||||
echo "Configuring Claude Code for standalone mode..."
|
||||
|
||||
@ -186,6 +158,8 @@ function configure_standalone_mode() {
|
||||
echo "Updating existing Claude configuration at $${claude_config}"
|
||||
|
||||
jq '.autoUpdaterStatus = "disabled" |
|
||||
.autoModeAccepted = true |
|
||||
.bypassPermissionsModeAccepted = true |
|
||||
.hasAcknowledgedCostThreshold = true |
|
||||
.hasCompletedOnboarding = true' \
|
||||
"$${claude_config}" > "$${claude_config}.tmp" && mv "$${claude_config}.tmp" "$${claude_config}"
|
||||
@ -194,6 +168,8 @@ function configure_standalone_mode() {
|
||||
cat > "$${claude_config}" << EOF
|
||||
{
|
||||
"autoUpdaterStatus": "disabled",
|
||||
"autoModeAccepted": true,
|
||||
"bypassPermissionsModeAccepted": true,
|
||||
"hasAcknowledgedCostThreshold": true,
|
||||
"hasCompletedOnboarding": true
|
||||
}
|
||||
@ -213,5 +189,4 @@ EOF
|
||||
|
||||
install_claude_code_cli
|
||||
setup_claude_configurations
|
||||
write_managed_settings
|
||||
configure_standalone_mode
|
||||
|
||||
@ -14,7 +14,7 @@ This module allows you to automatically clone a repository by URL and skip if it
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.3.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
}
|
||||
@ -28,7 +28,7 @@ module "git-clone" {
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.3.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
base_dir = "~/projects/coder"
|
||||
@ -43,7 +43,7 @@ To use with [Git Authentication](https://coder.com/docs/v2/latest/admin/git-prov
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.3.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
}
|
||||
@ -70,7 +70,7 @@ data "coder_parameter" "git_repo" {
|
||||
module "git_clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.3.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
url = data.coder_parameter.git_repo.value
|
||||
}
|
||||
@ -105,7 +105,7 @@ Configuring `git-clone` for a self-hosted GitHub Enterprise Server running at `g
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.3.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.example.com/coder/coder/tree/feat/example"
|
||||
git_providers = {
|
||||
@ -125,7 +125,7 @@ To GitLab clone with a specific branch like `feat/example`
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.3.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://gitlab.com/coder/coder/-/tree/feat/example"
|
||||
}
|
||||
@ -137,7 +137,7 @@ Configuring `git-clone` for a self-hosted GitLab running at `gitlab.example.com`
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.3.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://gitlab.example.com/coder/coder/-/tree/feat/example"
|
||||
git_providers = {
|
||||
@ -159,7 +159,7 @@ For example, to clone the `feat/example` branch:
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.3.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
branch_name = "feat/example"
|
||||
@ -177,7 +177,7 @@ For example, this will clone into the `~/projects/coder/coder-dev` folder:
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.3.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
folder_name = "coder-dev"
|
||||
@ -196,7 +196,7 @@ If not defined, the default, `0`, performs a full clone.
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.3.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
depth = 1
|
||||
@ -212,7 +212,7 @@ This is useful for preparing the environment or validating prerequisites before
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.3.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
pre_clone_script = <<-EOT
|
||||
@ -235,7 +235,7 @@ This is useful for running initialization tasks like installing dependencies or
|
||||
module "git-clone" {
|
||||
count = data.coder_workspace.me.start_count
|
||||
source = "registry.coder.com/coder/git-clone/coder"
|
||||
version = "1.3.1"
|
||||
version = "1.3.0"
|
||||
agent_id = coder_agent.example.id
|
||||
url = "https://github.com/coder/coder"
|
||||
post_clone_script = <<-EOT
|
||||
|
||||
@ -250,14 +250,13 @@ describe("git-clone", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
url: "fake-url",
|
||||
base_dir: "/tmp",
|
||||
post_clone_script: "echo 'Post-clone script executed'",
|
||||
});
|
||||
const output = await executeScriptInContainer(
|
||||
state,
|
||||
"alpine/git",
|
||||
"sh",
|
||||
"mkdir -p /tmp/fake-url && echo 'existing' > /tmp/fake-url/file.txt",
|
||||
"mkdir -p ~/fake-url && echo 'existing' > ~/fake-url/file.txt",
|
||||
);
|
||||
expect(output.stdout).toContain("Running post-clone script...");
|
||||
expect(output.stdout).toContain("Post-clone script executed");
|
||||
@ -274,35 +273,4 @@ describe("git-clone", async () => {
|
||||
expect(output.stdout).toContain("Pre-clone script executed");
|
||||
expect(output.stdout).toContain("Cloning fake-url to ~/fake-url...");
|
||||
});
|
||||
|
||||
it("fails when pre-clone script fails", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
url: "fake-url",
|
||||
pre_clone_script: "echo 'Pre-clone script failed'; exit 42",
|
||||
});
|
||||
const output = await executeScriptInContainer(state, "alpine/git");
|
||||
expect(output.exitCode).toBe(42);
|
||||
expect(output.stdout).toContain("Running pre-clone script...");
|
||||
expect(output.stdout).toContain("Pre-clone script failed");
|
||||
expect(output.stdout).not.toContain("Cloning fake-url to ~/fake-url...");
|
||||
});
|
||||
|
||||
it("fails when post-clone script fails", async () => {
|
||||
const state = await runTerraformApply(import.meta.dir, {
|
||||
agent_id: "foo",
|
||||
url: "fake-url",
|
||||
base_dir: "/tmp",
|
||||
post_clone_script: "echo 'Post-clone script failed'; exit 43",
|
||||
});
|
||||
const output = await executeScriptInContainer(
|
||||
state,
|
||||
"alpine/git",
|
||||
"sh",
|
||||
"mkdir -p /tmp/fake-url && echo 'existing' > /tmp/fake-url/file.txt",
|
||||
);
|
||||
expect(output.exitCode).toBe(43);
|
||||
expect(output.stdout).toContain("Running post-clone script...");
|
||||
expect(output.stdout).toContain("Post-clone script failed");
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,7 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
REPO_URL="${REPO_URL}"
|
||||
CLONE_PATH="${CLONE_PATH}"
|
||||
BRANCH_NAME="${BRANCH_NAME}"
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user