Skip to content
This repository was archived by the owner on Oct 14, 2026. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,8 @@ repos:
files: Dockerfile$
- id: json-valid
name: JSON syntax
entry: python3 -m json.tool
# json.tool takes [infile] [outfile]: never pass it several files.
entry: python3 -c 'import json, sys; [json.load(open(f)) for f in sys.argv[1:]]'
language: system
files: \.json$
- id: gitleaks
Expand Down
23 changes: 22 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -72,11 +72,32 @@ resets the variable to its default.
Create it once on each machine you release from (see `vars.example.yaml`):

```yaml
extra_git_hosts: <git-host> # space-separated, added to known_hosts
extra_git_hosts: <git-host> # space-separated, added to known_hosts
onedev_url: https://<onedev-host> # tod server URL (ONEDEV_SERVER_URL)
```

Every template must declare each variable in the file.

## OneDev CLI (tod) and Claude Code skills

Every image has the OneDev CLI `tod` and its 8 agent skills, so Claude Code
can work with OneDev issues, pull requests and builds.

- `tod` is built from a pinned tag (`ARG TOD_VERSION` / `TOD_COMMIT` in each
Dockerfile; the commit is checked). The official installer downloads the
latest binary without verification, so it is not used.
- The skills are installed in `/etc/claude-code/.claude/skills/`: managed,
loaded for every user, always the same version as the binary.
- Server URL: template variable `onedev_url` (`vars.yaml`), exported as
`ONEDEV_SERVER_URL` only when set. Access token: each user's own, set with
`tod config set access-token` (stored in `~/.config/tod/config`, home volume).
No token in the templates or images.
- Claude Code policy: read commands are allowed; commands that change OneDev
ask first; `tod config set` and reading the tod config are denied.
- Version hold: OneDev here is 16.4.1, and tod 4.3.3+ needs 16.5.11. Renovate
keeps tod below 4.3.3 (`renovate.json`). Remove that rule after the server
upgrade.

## Pull request checks

`.github/workflows/checks.yml` runs on each pull request to `main`
Expand Down
11 changes: 8 additions & 3 deletions node-js/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ Docker daemon, so you can run Postgres, Redis and other services with
| Node.js | Node LTS and pnpm through [mise](https://mise.jdx.dev) |
| Docker | Docker Engine, Compose and Buildx **inside** the workspace |
| Editor | VS Code (web) with the Claude Code extension |
| AI | Claude Code CLI (`claude`) |
| Git | `git`, GitHub CLI (`gh`), SSH through Coder |
| AI | Claude Code CLI (`claude`), with skills for OneDev |
| Git | `git`, GitHub CLI (`gh`), OneDev CLI (`tod`), SSH through Coder |
| Other | `build-essential`, `python3` (node-gyp), `ripgrep`, `fd`, `jq`, `tmux`, `htop` |

`NEXT_TELEMETRY_DISABLED=1` is set in the image.
Expand All @@ -45,7 +45,12 @@ in the repo wins over the parameter.
copy private keys into the workspace.
2. **Log in to Claude Code once.** Open a terminal and run `claude`. The login
is kept in `~/.claude`. The VS Code extension uses the same login.
3. **Run the app.** Start the dev server on port 3000 (`pnpm dev`), then open
3. **Connect `tod` to OneDev (optional).** In OneDev, create an access token
(your avatar → **Access Tokens**), then run
`tod config set access-token <token>`. The server URL is already set. Test
it with `tod project list`. Claude Code can then work with OneDev issues,
pull requests and builds through its `tod` skills. It asks before any change.
4. **Run the app.** Start the dev server on port 3000 (`pnpm dev`), then open
the **Preview :3000** app in the Coder dashboard.

## What persists
Expand Down
20 changes: 20 additions & 0 deletions node-js/build/Dockerfile
Original file line number Diff line number Diff line change
@@ -1,6 +1,21 @@
# "node" Coder workspace (Sysbox runtime): Node.js LTS / Next.js.
# Node itself is installed by mise at start (home volume), not here.

# --- OneDev CLI (tod), built from a pinned tag ------------------------------
# The official installer always downloads the latest binary, unverified.
# Here: clone the tag, check its commit, build. Only the binary and the agent
# skills go into the final image.
FROM golang:1.27.0-bookworm AS tod
ARG TOD_VERSION=v4.3.2
ARG TOD_COMMIT=9e280c96bf0f3e541706580c0830e5753dc5d28d
WORKDIR /src
RUN git clone --depth 1 --branch "${TOD_VERSION}" https://code.onedev.io/onedev/tod.git . \
&& test "$(git rev-parse HEAD)" = "${TOD_COMMIT}" \
&& CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o /out/tod . \
&& mkdir -p /out/skills \
&& for d in skills/*/; do cp -r "$d" /out/skills/; done \
&& chmod -R u=rwX,go=rX /out

FROM ubuntu:24.04

SHELL ["/bin/bash", "-o", "pipefail", "-c"]
Expand Down Expand Up @@ -75,6 +90,11 @@ RUN d=/usr/lib/code-server/lib/vscode/node_modules/shell-quote \
RUN install -d -m 0755 /etc/claude-code
COPY --chmod=0644 managed-settings.json /etc/claude-code/managed-settings.json

# --- OneDev CLI (tod) and its Claude Code skills ----------------------------
# Skills in /etc/claude-code/.claude/skills load for every user (managed).
COPY --from=tod /out/tod /usr/local/bin/tod
COPY --from=tod /out/skills /etc/claude-code/.claude/skills

# --- coder user -------------------------------------------
# UID 1000 must stay: existing home volumes are owned by 1000.
RUN userdel -r ubuntu 2>/dev/null || true \
Expand Down
44 changes: 42 additions & 2 deletions node-js/build/managed-settings.json
Original file line number Diff line number Diff line change
@@ -1,9 +1,49 @@
{
"permissions": {
"disableBypassPermissionsMode": "disable",
"allow": [
"Bash(tod version)",
"Bash(tod config get:*)",
"Bash(tod config path)",
"Bash(tod remote)",
"Bash(tod get-login-name:*)",
"Bash(tod get-valid-labels)",
"Bash(tod get-commit-message-requirement:*)",
"Bash(tod project:*)",
"Bash(tod issue list:*)",
"Bash(tod issue get:*)",
"Bash(tod issue current-reference)",
"Bash(tod pr list:*)",
"Bash(tod pr get:*)",
"Bash(tod pr current-reference)",
"Bash(tod build list:*)",
"Bash(tod build get:*)",
"Bash(tod build check-spec)"
],
"ask": [
"Bash(tod issue create:*)",
"Bash(tod issue edit:*)",
"Bash(tod issue change-state:*)",
"Bash(tod issue link:*)",
"Bash(tod issue add-comment:*)",
"Bash(tod issue log-work:*)",
"Bash(tod issue create-branch:*)",
"Bash(tod pr create:*)",
"Bash(tod pr edit:*)",
"Bash(tod pr approve:*)",
"Bash(tod pr request-changes:*)",
"Bash(tod pr merge:*)",
"Bash(tod pr discard:*)",
"Bash(tod pr add-comment:*)",
"Bash(tod pr add-code-comment:*)",
"Bash(tod code-comment:*)",
"Bash(tod build run:*)"
],
"deny": [
"Bash(sudo:*)",
"Read(/etc/claude-code/**)"
"Read(/etc/claude-code/*.json)",
"Bash(tod config set:*)",
"Read(~/.config/tod/**)"
]
}
}
}
18 changes: 15 additions & 3 deletions node-js/main.tf
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,17 @@ variable "extra_git_hosts" {
}
}

variable "onedev_url" {
description = "OneDev server URL for the tod CLI (e.g. https://onedev.example.com). Empty: each user sets it with tod config set."
type = string
default = ""

validation {
condition = var.onedev_url == "" || can(regex("^https://[A-Za-z0-9.-]+(:[0-9]+)?/?$", var.onedev_url))
error_message = "Use an https:// URL without a path, or leave it empty."
}
}

locals {
template_slug = "node"
preview_port = 3000
Expand Down Expand Up @@ -99,11 +110,12 @@ resource "coder_agent" "main" {
arch = "amd64"
os = "linux"

env = {
# ONEDEV_SERVER_URL only when set: tod treats an empty value as an override.
env = merge({
REPO_URL = data.coder_parameter.repo_url.value
GIT_REF = data.coder_parameter.git_ref.value
MISE_TOOLS = "${data.coder_parameter.node_version.value} ${data.coder_parameter.extra_mise_tools.value}"
}
}, var.onedev_url == "" ? {} : { ONEDEV_SERVER_URL = var.onedev_url })

# Git over SSH uses `coder gitssh` (GIT_SSH_COMMAND is set by the agent).
# Add the Coder public key (`coder publickey`) once to GitHub and OneDev.
Expand Down Expand Up @@ -313,7 +325,7 @@ resource "docker_image" "workspace" {
# Built from build/ by GitHub Actions (.github/workflows/images.yml) and
# pushed to GHCR. Not built on x1-hel1. Tags are immutable: a change under
# build/ needs a new tag here.
name = "ghcr.io/moveyourdigital/coder-workspace-node:v0.6.2"
name = "ghcr.io/moveyourdigital/coder-workspace-node:v0.7.0"
keep_locally = true
}

Expand Down
9 changes: 6 additions & 3 deletions ops/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ their own workspace. Credentials stay in that workspace's home volume.
| Network | Tailscale client, `mtr`, `tcpdump`, `nmap`, `iperf3`, `socat`, `whois`, `openssl` | apt |
| Data | `borgbackup`, `postgresql-client`, `redis-tools` | apt |
| Runbooks | `just` | mise |
| Base | Node LTS, `gh`, Claude Code, code-server | mise / apt |
| Base | Node LTS, `gh`, OneDev CLI (`tod`), Claude Code, code-server | mise / apt / image |

mise tool versions are pinned in `main.tf` (`local.ops_tools`).

Expand Down Expand Up @@ -84,12 +84,13 @@ docker --context swarm service ls

Use `--context swarm` per command, or `docker context use swarm` with care.

### 4. Hetzner, Cloudflare, Coder
### 4. Hetzner, Cloudflare, Coder, OneDev

```bash
hcloud context create <project> # paste a project token
wrangler login # or CLOUDFLARE_API_TOKEN in a local .env
coder login https://<coder-url>
tod config set access-token <token> # OneDev: avatar → Access Tokens
```

Use tokens with the minimum scope for the task.
Expand All @@ -103,6 +104,7 @@ Use tokens with the minimum scope for the task.
| Tailscale node | `~/.local/share/tailscale/` |
| SSH key to nodes | `~/.ssh/ops_ed25519` |
| Coder session | `~/.config/coderv2/` |
| OneDev token (`tod`) | `~/.config/tod/config` |

Do **not** store these as Coder user secrets: user secrets go to every
workspace you own, including the coding workspaces.
Expand All @@ -118,7 +120,8 @@ This template has a stricter managed policy than the coding templates:
- **Asks first:** `terraform apply`, all `hcloud` commands, `docker stack deploy`,
`docker service update/scale`, `docker node update`, `docker context use`,
`ansible-playbook`, `wrangler deploy/secret`, `coder templates push`,
`ssh`, `scp`, `rsync`.
`ssh`, `scp`, `rsync`, and every `tod` command that changes OneDev
(same rules as the coding templates).

The policy is a safety net, not a security boundary. Review every action that
changes infrastructure.
Expand Down
20 changes: 20 additions & 0 deletions ops/build/Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,21 @@
# Base = node template. CLI tools (hcloud, terraform, ...) are installed by
# mise at start (pinned in main.tf). System tools are installed here.

# --- OneDev CLI (tod), built from a pinned tag ------------------------------
# The official installer always downloads the latest binary, unverified.
# Here: clone the tag, check its commit, build. Only the binary and the agent
# skills go into the final image.
FROM golang:1.27.0-bookworm AS tod
ARG TOD_VERSION=v4.3.2
ARG TOD_COMMIT=9e280c96bf0f3e541706580c0830e5753dc5d28d
WORKDIR /src
RUN git clone --depth 1 --branch "${TOD_VERSION}" https://code.onedev.io/onedev/tod.git . \
&& test "$(git rev-parse HEAD)" = "${TOD_COMMIT}" \
&& CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -o /out/tod . \
&& mkdir -p /out/skills \
&& for d in skills/*/; do cp -r "$d" /out/skills/; done \
&& chmod -R u=rwX,go=rX /out

FROM ubuntu:24.04

SHELL ["/bin/bash", "-o", "pipefail", "-c"]
Expand Down Expand Up @@ -91,6 +106,11 @@ RUN d=/usr/lib/code-server/lib/vscode/node_modules/shell-quote \
RUN install -d -m 0755 /etc/claude-code
COPY --chmod=0644 managed-settings.json /etc/claude-code/managed-settings.json

# --- OneDev CLI (tod) and its Claude Code skills ----------------------------
# Skills in /etc/claude-code/.claude/skills load for every user (managed).
COPY --from=tod /out/tod /usr/local/bin/tod
COPY --from=tod /out/skills /etc/claude-code/.claude/skills

# --- coder user -------------------------------------------
# UID 1000 must stay: existing home volumes are owned by 1000.
RUN userdel -r ubuntu 2>/dev/null || true \
Expand Down
76 changes: 57 additions & 19 deletions ops/build/managed-settings.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,59 @@
{
"permissions": {
"disableBypassPermissionsMode": "disable",
"allow": [
"Bash(tod version)",
"Bash(tod config get:*)",
"Bash(tod config path)",
"Bash(tod remote)",
"Bash(tod get-login-name:*)",
"Bash(tod get-valid-labels)",
"Bash(tod get-commit-message-requirement:*)",
"Bash(tod project:*)",
"Bash(tod issue list:*)",
"Bash(tod issue get:*)",
"Bash(tod issue current-reference)",
"Bash(tod pr list:*)",
"Bash(tod pr get:*)",
"Bash(tod pr current-reference)",
"Bash(tod build list:*)",
"Bash(tod build get:*)",
"Bash(tod build check-spec)"
],
"ask": [
"Bash(terraform apply:*)",
"Bash(tofu apply:*)",
"Bash(hcloud:*)",
"Bash(docker stack deploy:*)",
"Bash(docker service update:*)",
"Bash(docker service scale:*)",
"Bash(docker node update:*)",
"Bash(docker context use:*)",
"Bash(ansible-playbook:*)",
"Bash(wrangler deploy:*)",
"Bash(wrangler secret:*)",
"Bash(coder templates push:*)",
"Bash(ssh:*)",
"Bash(scp:*)",
"Bash(rsync:*)",
"Bash(tod issue create:*)",
"Bash(tod issue edit:*)",
"Bash(tod issue change-state:*)",
"Bash(tod issue link:*)",
"Bash(tod issue add-comment:*)",
"Bash(tod issue log-work:*)",
"Bash(tod issue create-branch:*)",
"Bash(tod pr create:*)",
"Bash(tod pr edit:*)",
"Bash(tod pr approve:*)",
"Bash(tod pr request-changes:*)",
"Bash(tod pr merge:*)",
"Bash(tod pr discard:*)",
"Bash(tod pr add-comment:*)",
"Bash(tod pr add-code-comment:*)",
"Bash(tod code-comment:*)",
"Bash(tod build run:*)"
],
"deny": [
"Bash(sudo:*)",
"Bash(terraform destroy:*)",
Expand All @@ -24,31 +77,16 @@
"Bash(borg delete:*)",
"Bash(borg prune:*)",
"Bash(tailscale logout:*)",
"Read(/etc/claude-code/**)",
"Read(/etc/claude-code/*.json)",
"Read(~/.ssh/**)",
"Read(~/.config/hcloud/**)",
"Read(~/.local/share/tailscale/**)",
"Read(~/.wrangler/**)",
"Read(~/.config/.wrangler/**)",
"Read(~/.config/coderv2/**)",
"Read(~/.docker/config.json)"
],
"ask": [
"Bash(terraform apply:*)",
"Bash(tofu apply:*)",
"Bash(hcloud:*)",
"Bash(docker stack deploy:*)",
"Bash(docker service update:*)",
"Bash(docker service scale:*)",
"Bash(docker node update:*)",
"Bash(docker context use:*)",
"Bash(ansible-playbook:*)",
"Bash(wrangler deploy:*)",
"Bash(wrangler secret:*)",
"Bash(coder templates push:*)",
"Bash(ssh:*)",
"Bash(scp:*)",
"Bash(rsync:*)"
"Read(~/.docker/config.json)",
"Bash(tod config set:*)",
"Read(~/.config/tod/**)"
]
}
}
Loading
Loading