Files
octoturge 013567f02a
Provision Coder Templates / provision (push) Successful in 2m11s
feat: base git/gnupg install + auto SSH/GPG key setup for external git
- Every template's coder_agent startup script now unconditionally
  installs git and gnupg as base packages (guarded on `command -v`, so
  it's a fast no-op where already present, e.g. templates/web's baked
  image). Not every base image ships gnupg by default.

- cli-setup-wizard.sh (all 6 templates) now tracks whether the user
  actually ended up authenticated against GitHub and/or Gitea via the
  existing gh/tea install-and-login prompts. If at least one succeeded,
  it asks once more whether to auto-generate an SSH key (ed25519) and a
  GPG signing key (ed25519, quick-gen) and register them with whichever
  host(s) are in play - stays completely silent for "local git only"
  (neither gh nor tea set up).

  - GitHub: `gh ssh-key add` / `gh gpg-key add` (official gh CLI
    subcommands).
  - Gitea: direct calls against `/api/v1/user/keys` and
    `/api/v1/user/gpg_keys`, reusing the token `tea login add` already
    stored in tea's config.yml (parsed with a small awk extractor).
  - Either upload failing (already added, API shape mismatch, etc.)
    just prints the manual command/URL and moves on - never blocks the
    rest of the wizard, consistent with every other step's style.
  - git is configured to sign commits with the new key
    (user.signingkey + commit.gpgsign) once a GPG key exists, whether
    freshly generated or already present from a prior run.

README updated to document both additions.
2026-08-26 23:16:25 +02:00

267 lines
14 KiB
Markdown

# 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:
```sh
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](https://bun.sh) 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`)
`git` and `gnupg` themselves aren't part of this opt-in flow - every
template's `coder_agent` startup script installs them unconditionally as
base packages (a no-op where they're already present, e.g. baked into
`templates/web`'s image). If the wizard just authenticated GitHub and/or
Gitea above (skipped entirely for "local git only" - neither set up), it
asks once more whether to auto-generate an ed25519 SSH key and an ed25519
GPG signing key and register them with whichever host(s) got set up: `gh
ssh-key add` / `gh gpg-key add` for GitHub, a direct call against Gitea's
`/api/v1/user/keys` and `/api/v1/user/gpg_keys` (using the token `tea
login add` already stored) for Gitea. Either upload failing just prints
the manual command/URL to finish it yourself - never blocks the rest of
the wizard.
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:
```sh
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](https://codeberg.org/forgejo/forgejo/issues/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):
```sh
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.