From d6b76425964ce1f6b1e0ebca05a73d70c4dcfc6a Mon Sep 17 00:00:00 2001 From: Vitor Carvalho Date: Wed, 14 Oct 2026 15:28:27 +0100 Subject: [PATCH] tod: template-wide default server URL; users only need ONEDEV_TOKEN - Template variable onedev_url -> agent env ONEDEV_DEFAULT_SERVER_URL (own name: no conflict with a user secret). - tod wrapper: server URL = ONEDEV_SERVER_URL, else the tod config file, else the template default. - Smoke test checks the default and that a user value wins. READMEs: only the ONEDEV_TOKEN secret is needed. Image tags: node/php v0.9.0, ops v0.8.0. Co-Authored-By: Claude Opus 5.5 --- README.md | 18 +++++++++++------- node-js/README.md | 12 ++++++------ node-js/build/tod-env.sh | 22 +++++++++++++++++----- node-js/main.tf | 15 ++++++++++++++- ops/README.md | 6 ++++-- ops/build/tod-env.sh | 22 +++++++++++++++++----- ops/main.tf | 15 ++++++++++++++- php-wordpress/README.md | 12 ++++++------ php-wordpress/build/tod-env.sh | 22 +++++++++++++++++----- php-wordpress/main.tf | 15 ++++++++++++++- tests/smoke.sh | 3 +++ vars.example.yaml | 1 + 12 files changed, 124 insertions(+), 39 deletions(-) diff --git a/README.md b/README.md index f2b926e..5beee3a 100644 --- a/README.md +++ b/README.md @@ -72,7 +72,8 @@ resets the variable to its default. Create it once on each machine you release from (see `vars.example.yaml`): ```yaml -extra_git_hosts: # space-separated, added to known_hosts +extra_git_hosts: # space-separated, added to known_hosts +onedev_url: https:// # default tod server URL ``` Every template must declare each variable in the file. @@ -87,12 +88,15 @@ can work with OneDev issues, pull requests and builds. 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 and token come from each user's Coder user secrets - `ONEDEV_SERVER_URL` and `ONEDEV_TOKEN`. tod reads `ONEDEV_ACCESS_TOKEN`, so - `/usr/local/bin/tod` is a wrapper (`build/tod-env.sh`) that uses - `ONEDEV_TOKEN` when `ONEDEV_ACCESS_TOKEN` is not set, then starts the real - binary (`/usr/local/lib/tod/tod`). Without secrets: `tod config set` - (stored in `~/.config/tod/config`). No token in the templates or images. +- Each user only provides a token: the Coder user secret `ONEDEV_TOKEN`. + `/usr/local/bin/tod` is a wrapper (`build/tod-env.sh`) around the real + binary (`/usr/local/lib/tod/tod`): + - token: `ONEDEV_ACCESS_TOKEN`, else `ONEDEV_TOKEN`, else the tod config file + - server URL: `ONEDEV_SERVER_URL` (a user secret, if any), else the tod + config file, else the template default `ONEDEV_DEFAULT_SERVER_URL` + (template variable `onedev_url`, from `vars.yaml`) + + No token in the templates or images. - The token is an environment variable, so any process in the workspace (Claude Code included) can read it. Use a token with the minimum scope. - Claude Code policy: read commands are allowed; commands that change OneDev diff --git a/node-js/README.md b/node-js/README.md index b4246ce..72b57d3 100644 --- a/node-js/README.md +++ b/node-js/README.md @@ -46,12 +46,12 @@ in the repo wins over the parameter. 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. **Connect `tod` to OneDev (optional).** In OneDev, create an access token - (your avatar → **Access Tokens**). In Coder, add two user secrets (**Account → - Secrets**) as environment variables: `ONEDEV_SERVER_URL` (your OneDev URL) - and `ONEDEV_TOKEN` (the token). Restart the workspace, then test with - `tod project list`. Without secrets, use `tod config set` instead. Claude - Code can then work with OneDev issues, pull requests and builds through its - `tod` skills. It asks before any change. + (your avatar → **Access Tokens**). Store it as a Coder user secret with the + environment variable `ONEDEV_TOKEN`: + `printf %s "" | coder secret create onedev-token --env ONEDEV_TOKEN`. + Restart the workspace, then test with `tod project list`. The OneDev URL is + preset by the template. 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. diff --git a/node-js/build/tod-env.sh b/node-js/build/tod-env.sh index fcc90d0..7ca345a 100644 --- a/node-js/build/tod-env.sh +++ b/node-js/build/tod-env.sh @@ -1,10 +1,22 @@ #!/bin/sh -# tod wrapper: tod reads ONEDEV_ACCESS_TOKEN. Coder user secrets may provide -# the token as ONEDEV_TOKEN instead; use it when ONEDEV_ACCESS_TOKEN is unset. -# ONEDEV_SERVER_URL is read by tod directly. Without both, tod uses its config -# file (tod config set). +# tod wrapper for Coder workspaces. Starts the real binary with: +# +# token: ONEDEV_ACCESS_TOKEN, else the Coder user secret ONEDEV_TOKEN, +# else the tod config file (tod config set) +# server URL: ONEDEV_SERVER_URL (user secret), else the tod config file, +# else the template default ONEDEV_DEFAULT_SERVER_URL +# (template variable onedev_url) +tod=/usr/local/lib/tod/tod + if [ -z "${ONEDEV_ACCESS_TOKEN+set}" ] && [ -n "${ONEDEV_TOKEN:-}" ]; then ONEDEV_ACCESS_TOKEN="$ONEDEV_TOKEN" export ONEDEV_ACCESS_TOKEN fi -exec /usr/local/lib/tod/tod "$@" + +if [ -z "${ONEDEV_SERVER_URL+set}" ] && [ -n "${ONEDEV_DEFAULT_SERVER_URL:-}" ] \ + && [ -z "$("$tod" config get server-url 2>/dev/null)" ]; then + ONEDEV_SERVER_URL="$ONEDEV_DEFAULT_SERVER_URL" + export ONEDEV_SERVER_URL +fi + +exec "$tod" "$@" diff --git a/node-js/main.tf b/node-js/main.tf index 9c97ed1..2156c01 100644 --- a/node-js/main.tf +++ b/node-js/main.tf @@ -29,6 +29,17 @@ variable "extra_git_hosts" { } } +variable "onedev_url" { + description = "Default OneDev server URL for the tod CLI (e.g. https://onedev.example.com). A user secret ONEDEV_SERVER_URL or the tod config file wins." + 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 @@ -103,6 +114,8 @@ resource "coder_agent" "main" { 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}" + # Default for tod; the tod wrapper uses it only as the last fallback. + ONEDEV_DEFAULT_SERVER_URL = var.onedev_url } # Git over SSH uses `coder gitssh` (GIT_SSH_COMMAND is set by the agent). @@ -313,7 +326,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.8.0" + name = "ghcr.io/moveyourdigital/coder-workspace-node:v0.9.0" keep_locally = true } diff --git a/ops/README.md b/ops/README.md index 88036f8..8aa4a59 100644 --- a/ops/README.md +++ b/ops/README.md @@ -92,8 +92,10 @@ wrangler login # or CLOUDFLARE_API_TOKEN in a local coder login https:// ``` -OneDev (`tod`): add the Coder user secrets `ONEDEV_SERVER_URL` and -`ONEDEV_TOKEN` (OneDev: avatar → Access Tokens), or run `tod config set`. +OneDev (`tod`): store your OneDev access token (avatar → Access Tokens) as the +Coder user secret `ONEDEV_TOKEN` +(`printf %s "" | coder secret create onedev-token --env ONEDEV_TOKEN`), +then restart the workspace. The OneDev URL is preset by the template. Use tokens with the minimum scope for the task. diff --git a/ops/build/tod-env.sh b/ops/build/tod-env.sh index fcc90d0..7ca345a 100644 --- a/ops/build/tod-env.sh +++ b/ops/build/tod-env.sh @@ -1,10 +1,22 @@ #!/bin/sh -# tod wrapper: tod reads ONEDEV_ACCESS_TOKEN. Coder user secrets may provide -# the token as ONEDEV_TOKEN instead; use it when ONEDEV_ACCESS_TOKEN is unset. -# ONEDEV_SERVER_URL is read by tod directly. Without both, tod uses its config -# file (tod config set). +# tod wrapper for Coder workspaces. Starts the real binary with: +# +# token: ONEDEV_ACCESS_TOKEN, else the Coder user secret ONEDEV_TOKEN, +# else the tod config file (tod config set) +# server URL: ONEDEV_SERVER_URL (user secret), else the tod config file, +# else the template default ONEDEV_DEFAULT_SERVER_URL +# (template variable onedev_url) +tod=/usr/local/lib/tod/tod + if [ -z "${ONEDEV_ACCESS_TOKEN+set}" ] && [ -n "${ONEDEV_TOKEN:-}" ]; then ONEDEV_ACCESS_TOKEN="$ONEDEV_TOKEN" export ONEDEV_ACCESS_TOKEN fi -exec /usr/local/lib/tod/tod "$@" + +if [ -z "${ONEDEV_SERVER_URL+set}" ] && [ -n "${ONEDEV_DEFAULT_SERVER_URL:-}" ] \ + && [ -z "$("$tod" config get server-url 2>/dev/null)" ]; then + ONEDEV_SERVER_URL="$ONEDEV_DEFAULT_SERVER_URL" + export ONEDEV_SERVER_URL +fi + +exec "$tod" "$@" diff --git a/ops/main.tf b/ops/main.tf index b0968c4..9f51526 100644 --- a/ops/main.tf +++ b/ops/main.tf @@ -29,6 +29,17 @@ variable "extra_git_hosts" { } } +variable "onedev_url" { + description = "Default OneDev server URL for the tod CLI (e.g. https://onedev.example.com). A user secret ONEDEV_SERVER_URL or the tod config file wins." + 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 = "ops" @@ -121,6 +132,8 @@ resource "coder_agent" "main" { REPO_URL = data.coder_parameter.repo_url.value GIT_REF = data.coder_parameter.git_ref.value MISE_TOOLS = "${data.coder_parameter.node_version.value} ${local.ops_tools} ${data.coder_parameter.extra_mise_tools.value}" + # Default for tod; the tod wrapper uses it only as the last fallback. + ONEDEV_DEFAULT_SERVER_URL = var.onedev_url } # Git over SSH uses `coder gitssh` (GIT_SSH_COMMAND is set by the agent). @@ -349,7 +362,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-ops:v0.7.0" + name = "ghcr.io/moveyourdigital/coder-workspace-ops:v0.8.0" keep_locally = true } diff --git a/php-wordpress/README.md b/php-wordpress/README.md index 5a17ab3..548ebbd 100644 --- a/php-wordpress/README.md +++ b/php-wordpress/README.md @@ -45,12 +45,12 @@ On each start, the workspace runs `mise install`, `npm`/`pnpm`/`yarn`/`bun insta 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. **Connect `tod` to OneDev (optional).** In OneDev, create an access token - (your avatar → **Access Tokens**). In Coder, add two user secrets (**Account → - Secrets**) as environment variables: `ONEDEV_SERVER_URL` (your OneDev URL) - and `ONEDEV_TOKEN` (the token). Restart the workspace, then test with - `tod project list`. Without secrets, use `tod config set` instead. Claude - Code can then work with OneDev issues, pull requests and builds through its - `tod` skills. It asks before any change. + (your avatar → **Access Tokens**). Store it as a Coder user secret with the + environment variable `ONEDEV_TOKEN`: + `printf %s "" | coder secret create onedev-token --env ONEDEV_TOKEN`. + Restart the workspace, then test with `tod project list`. The OneDev URL is + preset by the template. Claude Code can then work with OneDev issues, pull + requests and builds through its `tod` skills. It asks before any change. 4. **Start WordPress locally.** Use a `compose.yaml` in your project, then open the **Preview :8080** app in the Coder dashboard. diff --git a/php-wordpress/build/tod-env.sh b/php-wordpress/build/tod-env.sh index fcc90d0..7ca345a 100644 --- a/php-wordpress/build/tod-env.sh +++ b/php-wordpress/build/tod-env.sh @@ -1,10 +1,22 @@ #!/bin/sh -# tod wrapper: tod reads ONEDEV_ACCESS_TOKEN. Coder user secrets may provide -# the token as ONEDEV_TOKEN instead; use it when ONEDEV_ACCESS_TOKEN is unset. -# ONEDEV_SERVER_URL is read by tod directly. Without both, tod uses its config -# file (tod config set). +# tod wrapper for Coder workspaces. Starts the real binary with: +# +# token: ONEDEV_ACCESS_TOKEN, else the Coder user secret ONEDEV_TOKEN, +# else the tod config file (tod config set) +# server URL: ONEDEV_SERVER_URL (user secret), else the tod config file, +# else the template default ONEDEV_DEFAULT_SERVER_URL +# (template variable onedev_url) +tod=/usr/local/lib/tod/tod + if [ -z "${ONEDEV_ACCESS_TOKEN+set}" ] && [ -n "${ONEDEV_TOKEN:-}" ]; then ONEDEV_ACCESS_TOKEN="$ONEDEV_TOKEN" export ONEDEV_ACCESS_TOKEN fi -exec /usr/local/lib/tod/tod "$@" + +if [ -z "${ONEDEV_SERVER_URL+set}" ] && [ -n "${ONEDEV_DEFAULT_SERVER_URL:-}" ] \ + && [ -z "$("$tod" config get server-url 2>/dev/null)" ]; then + ONEDEV_SERVER_URL="$ONEDEV_DEFAULT_SERVER_URL" + export ONEDEV_SERVER_URL +fi + +exec "$tod" "$@" diff --git a/php-wordpress/main.tf b/php-wordpress/main.tf index 95a0d14..ea3a062 100644 --- a/php-wordpress/main.tf +++ b/php-wordpress/main.tf @@ -29,6 +29,17 @@ variable "extra_git_hosts" { } } +variable "onedev_url" { + description = "Default OneDev server URL for the tod CLI (e.g. https://onedev.example.com). A user secret ONEDEV_SERVER_URL or the tod config file wins." + 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 = "php-wordpress" preview_port = 8080 @@ -103,6 +114,8 @@ resource "coder_agent" "main" { 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}" + # Default for tod; the tod wrapper uses it only as the last fallback. + ONEDEV_DEFAULT_SERVER_URL = var.onedev_url } # Git over SSH uses `coder gitssh` (GIT_SSH_COMMAND is set by the agent). @@ -316,7 +329,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-php-wordpress:v0.8.0" + name = "ghcr.io/moveyourdigital/coder-workspace-php-wordpress:v0.9.0" keep_locally = true } diff --git a/tests/smoke.sh b/tests/smoke.sh index a702c56..7b6e298 100755 --- a/tests/smoke.sh +++ b/tests/smoke.sh @@ -25,6 +25,9 @@ common=' # The wrapper maps the Coder user secret ONEDEV_TOKEN to ONEDEV_ACCESS_TOKEN test -z "$(env -u ONEDEV_ACCESS_TOKEN -u ONEDEV_TOKEN tod config get access-token)" test -n "$(env -u ONEDEV_ACCESS_TOKEN ONEDEV_TOKEN=x tod config get access-token)" + # ...and uses the template default URL when nothing else sets one + test "$(env -u ONEDEV_SERVER_URL ONEDEV_DEFAULT_SERVER_URL=https://d.example tod config get server-url)" = https://d.example + test "$(ONEDEV_SERVER_URL=https://u.example ONEDEV_DEFAULT_SERVER_URL=https://d.example tod config get server-url)" = https://u.example n=0 for f in /etc/claude-code/.claude/skills/*/SKILL.md; do test -r "$f" && n=$((n + 1)); done test "$n" -ge 8 || { echo "tod skills: $n readable, want 8+" >&2; exit 1; } diff --git a/vars.example.yaml b/vars.example.yaml index b8e66ea..c6bbf95 100644 --- a/vars.example.yaml +++ b/vars.example.yaml @@ -1,3 +1,4 @@ # Copy to vars.yaml (not in Git) and set the values. # `just push` passes vars.yaml to `coder templates push --variables-file`. extra_git_hosts: "" # space-separated host names, e.g. "git.example.com" +onedev_url: "" # e.g. "https://onedev.example.com" (default tod server)