Files
Profiles-for-Coder/README.md
T
octoturge 0c53bb6554
Provision Coder Templates / provision (push) Successful in 2m7s
Add gh/tea to the CLI wizard, drop the JetBrains module from all templates
- cli-setup-wizard.sh (identical across all 6 templates): add GitHub CLI
  (gh, installed via the official apt repo) and Gitea CLI (tea, official
  binary release to ~/.local/bin) as two more opt-in install-then-login
  prompts, alongside the existing Copilot/Antigravity/Claude Code CLI
  entries. Same pattern: skip if already installed, ask before installing,
  attempt login, never hard-fail the wizard.
- main.tf (all 6 templates): remove the `module "jetbrains"` block - not
  used, dropped per request.
- README.md: update the "Layout" blurb and CLI setup wizard tool list to
  match.
2026-08-26 14:58:51 +02:00

13 KiB

Profiles-for-Coder

Coder templates for per-discipline dev environments (Default, 3D Printing, COBOL, Python, TTRPG, Web). Each environment is its own Coder template - not a dropdown inside one shared container, which is what this repo used to do and which didn't actually work.

Layout

templates/
  default/
    main.tf                  # Standard Default Dev
    profile.code-profile     # VS Code .code-profile export for this env
    cli-setup-wizard.sh      # first-run wizard, see below
    install-skills.sh        # Agent Skills installer, see below
  3d-printing/  (same 4 files)  # 3D Printing & Engineering
  cobol/        (same 4 files)  # COBOL Modern Mainframe
  python/       (same 4 files)  # Python Engineering
  ttrpg/        (same 4 files)  # TTRPG & Lore Building
  web/          (same 4 files, plus Dockerfile)  # Web Applications
extensions/                 # Agent Skills bundles, installed per env (see below)

Web Applications: baked-in toolchain image

Every other template pulls codercom/enterprise-base:ubuntu straight from Docker Hub and installs what little it needs (just Bun) via a coder_script at workspace start. templates/web/ doesn't do that: it needs a large, slow-to-install native toolchain (Rust/rustup with cross targets, the Tauri 2 / WebKit GUI dev libraries, Node + Bun + pnpm/yarn, Python with OpenCV/ONNX/ CPU-torch, Postgres/Redis/SQLite CLI clients, protoc, clang/llvm) that would make every workspace start take many minutes if installed on the fly.

Instead templates/web/main.tf builds templates/web/Dockerfile at apply/push time via the docker provider's docker_image resource (build { context = path.module }, same directory as main.tf so no file() reaches outside the template per the constraint above) and runs the container from that image instead of the enterprise-base one. The image tag embeds filesha1(Dockerfile), so editing the Dockerfile forces a rebuild on the next apply/push while an unchanged Dockerfile reuses Docker's build cache. Because the toolchain lives under /home/coder (rustup, cargo, bun), and that path is a fresh named Docker volume on a workspace's first boot, Docker's own "populate an empty volume from the image's directory contents" behavior copies all of it into the persistent volume automatically - no extra coder_script needed, matching how the /etc/skel copy in every template's startup_script already relies on that same mechanism.

Each templates/<env>/ is a complete, independent Coder template (agent, docker container, code-server) with its own copy of everything main.tf needs. They're deliberately not built from a shared Terraform module or shared files elsewhere in the repo: coder templates push -d templates/<env> only uploads that one directory to the Coder server, so any file() reference reaching outside it (e.g. the old shared scripts//profile-templates/ layout) fails at push/apply time with Invalid function argument: ... this function works only with files that are distributed as part of the configuration source code - discovered the hard way once coder-templates.yml actually ran end-to-end. main.tf, profile.code-profile, cli-setup-wizard.sh, and install-skills.sh are duplicated per template rather than shared for this reason; only their locals block and profile file differ meaningfully. The naming convention is profiles-<dir> (matches what .gitea/workflows/coder-templates.yml does automatically - see below). To push by hand:

coder templates push profiles-default      -d templates/default
coder templates push profiles-3d-printing  -d templates/3d-printing
coder templates push profiles-cobol        -d templates/cobol
coder templates push profiles-python       -d templates/python
coder templates push profiles-ttrpg        -d templates/ttrpg
coder templates push profiles-web          -d templates/web

How the VS Code profile gets applied

templates/<env>/profile.code-profile is a real VS Code Profile export: a JSON file whose settings and extensions fields are themselves JSON-encoded strings (double/triple-nested). Each template's main.tf reads and decodes its matching file at terraform apply/push time (via file() + jsondecode()), then:

  • passes the extension ID list straight into the code-server module's extensions input, so code-server installs them on first boot - no interactive prompt needed, Terraform handles it declaratively;
  • writes the raw settings.json text (comments and all - VS Code tolerates JSONC) to ~/.local/share/code-server/User/settings.json via a coder_script.

This replaces the old approach, which downloaded a zip of this repo from Gitea inside the running container and tried to apply settings from ~/.local/share/profiles-cache/<profile>.json - a path that never matched the actual .code-profile file extension, so settings never applied. That bug (plus the single shared container) is why "one container, many envs" never really worked.

Bun

Every template except web installs Bun via a coder_script (curl -fsSL https://bun.sh/install | bash) and hooks ~/.bun/bin onto PATH in ~/.bashrc (the installer doesn't reliably do this itself in a non-interactive/scripted shell). web instead bakes Bun into its Dockerfile with BUN_INSTALL/PATH set as image ENV - see "Web Applications: baked-in toolchain image" above. Either way, the CLI setup wizard below uses bun install -g <pkg> instead of npm install -g <pkg> for everything it installs.

CLI setup wizard

templates/<env>/cli-setup-wizard.sh (identical across envs) is dropped onto every workspace and hooked into ~/.bashrc. It runs in every new interactive terminal - until the user finishes it - and offers to install + log into:

  • GitHub Copilot CLI (bun install -g @github/copilot, then copilot login)
  • Google Antigravity CLI (curl -fsSL https://antigravity.google/cli/install.sh | bash, binary agy)
  • Claude Code CLI (bun install -g @anthropic-ai/claude-code, then claude)
  • GitHub CLI (gh, via the official apt repo, then gh auth login)
  • Gitea CLI (tea, official binary release downloaded to ~/.local/bin, then tea login add)

It does not ask about VS Code extensions, since those are handled by Terraform (see above). Once the user confirms completion it writes a sentinel file (~/.cache/coder-cli-wizard/done) and stops prompting. It can always be re-run manually: bash /opt/coder/cli-setup-wizard.sh --force.

extensions/ directory -> Agent Skills

extensions/awesome-skills-plugin and extensions/custom-specialty-plugin are Agent Skills bundles (plugin.json + SKILL.md files) - the old root main.tf copied this folder straight into code-server's VS Code extensions directory, which never worked since these aren't VS Code extension packages.

Each template now runs its own copy of install-skills.sh (via a coder_script, pulling a fresh zip of this repo from Gitea rather than embedding ~2.5MB into Terraform state) to install skills into all three AI CLIs' personal skills directories:

CLI Skills directory
Claude Code ~/.claude/skills/<name>/
GitHub Copilot CLI ~/.copilot/skills/<name>/
Antigravity CLI ~/.gemini/config/skills/<name>/ (per antigravity.google/docs/skills - some third-party docs disagree on this path, worth a spot-check on a live workspace)

Every environment gets the full extensions/awesome-skills-plugin/skills/* bundle. On top of that, whichever env has a matching entry in extensions/custom-specialty-plugin/skills/ gets it installed too, set via the SPECIALTY_SKILLS env var passed to the script from each template's install_skills coder_script:

  • COBOL -> cobol-teacher
  • 3D Printing -> openscad-parametric
  • TTRPG -> foundryvtt-modding and ttrpg-lore-weaver
  • Default / Python / Web -> none (no matching specialty skill exists yet)

Auto-provisioning (Gitea Actions)

.gitea/workflows/coder-templates.yml keeps Coder in sync with this repo on every push to main that touches templates/**:

  • Add a new templates/<env>/ directory -> next push creates a new Coder template profiles-<env> automatically. No workflow edits needed.
  • Edit an existing templates/<env>/main.tf (or its profile.code-profile, cli-setup-wizard.sh, or install-skills.sh) -> next push updates that template with a new version.
  • Remove a templates/<env>/ directory -> next push deletes profiles-<env> from Coder. coder templates delete refuses if the template still has active workspaces, so this fails loudly instead of silently orphaning anyone's running workspace - that failure only shows up as a ::warning:: in the job log, it doesn't fail the whole run.

It runs on the ubuntu-latest self-hosted runner already registered on this Gitea instance and installs the coder CLI itself via coder.com/install.sh.

One-time setup required (not something this workflow can do for itself - needs a human with Coder access). This deployment caps API token lifetime at 168h (7 days), so rather than raising that cap deployment-wide, .gitea/workflows/rotate-coder-token.yml (see below) keeps a fresh token flowing into the secret automatically:

  1. Create a Coder API token - ideally under a dedicated service account rather than a personal login, since this token can create/delete templates:
    coder login https://code.octoturge.com
    coder tokens create --name gitea-ci --lifetime 168h
    
  2. In Gitea, go to this repo's Settings -> Actions -> Secrets (or the org-level equivalent to share across repos) and add:
    • CODER_URL = https://code.octoturge.com
    • CODER_SESSION_TOKEN = the token printed by step 1

Until those secrets exist, coder-templates.yml will run and fail cleanly at the coder templates push step rather than doing anything destructive.

If the Secrets tab doesn't show up (some Gitea/Forgejo versions drop the Secrets nav link from Settings -> Actions while Runners and Variables still show, even though the page and API underneath both still work - see forgejo#938), try either:

  1. Go straight to the URL the nav link would normally point at: https://<gitea-host>/<owner>/<repo>/settings/actions/secrets. If a working "Add Secret" form loads there, it's just a missing nav link - add the secrets on that page as normal.
  2. If that also won't load, set the secrets via the Actions Secrets API instead, using a Gitea personal access token (write:repository scope, Settings -> Applications -> Generate New Token):
    GITEA_PAT="<your Gitea PAT>"
    OWNER=octoturge
    REPO=Profiles-for-Coder
    
    curl -s -X PUT "https://<gitea-host>/api/v1/repos/$OWNER/$REPO/actions/secrets/CODER_URL" \
      -H "Authorization: token $GITEA_PAT" -H "Content-Type: application/json" \
      -d '{"data":"https://code.octoturge.com"}'
    
    curl -s -X PUT "https://<gitea-host>/api/v1/repos/$OWNER/$REPO/actions/secrets/CODER_SESSION_TOKEN" \
      -H "Authorization: token $GITEA_PAT" -H "Content-Type: application/json" \
      -d "{\"data\":\"$(coder tokens create --name gitea-ci --lifetime 168h)\"}"
    
    A 201/204 response means the secret was saved. This is the exact same endpoint rotate-coder-token.yml uses at runtime, so if it works here it confirms the workflow itself will be able to update the secret later too. Never paste a PAT or Coder token into a chat/ticket - run these commands from a trusted shell only.

Token rotation (Gitea Actions)

.gitea/workflows/rotate-coder-token.yml runs daily and keeps CODER_SESSION_TOKEN alive forever without anyone needing to remember to refresh it: it mints a new 168h Coder token, writes it into the CODER_SESSION_TOKEN secret via the Gitea API, then deletes the token(s) it just replaced. If a run ever fails, the previous token is still untouched and still valid (nothing gets deleted until the new one is confirmed live), so it fails safe rather than locking you out.

One-time bootstrap (also needs a human - this is what lets the rotation workflow write to its own repo's secrets):

  1. Create a Gitea personal access token with write:repository scope and no expiration (Settings -> Applications -> Generate New Token). This one doesn't rotate itself, so give it a long life up front.
  2. Add it as a repo/org Actions secret named ROTATION_PAT (same Settings -> Actions -> Secrets page as above - if that tab is missing, see the nav-link workaround / API fallback in the "Auto-provisioning" section above, same curl -X PUT .../actions/secrets/<name> pattern, just with ROTATION_PAT as the secret name and the PAT itself as the value). Not named GITEA_ROTATION_TOKEN - Gitea reserves the GITEA_ prefix for its own automatic tokens/variables and rejects secrets with that prefix (Error: invalid variable or secret name).

After that, CODER_SESSION_TOKEN never needs manual attention again - you can also trigger a rotation on demand from Gitea's Actions tab (workflow_dispatch) instead of waiting for the daily schedule.