Run 109 failed: profiles-3d-printing (and the same would've hit every
other already-versioned template) already had a "v1" version from the
previous run, and the pre-check meant to detect that and skip
(coder templates versions list -o json piped through jq) apparently
doesn't match the CLI's actual JSON shape - it never found the
existing version, so the push was attempted anyway and coder rejected
the duplicate name, failing the whole job.
Simpler and more robust: just attempt the push and treat its specific
"A template version with name ... already exists" failure as the skip
signal, instead of trying to predict it from a separate list call.
The registry sits behind a reverse proxy that 413s large blob pushes,
so templates/web/Dockerfile's oversized RUN blocks (apt installs,
rustup targets, cargo installs) are broken up so no single layer is
too big to push.
Also adds templates/<name>/VERSION (starting at "1" for all six
templates) and has the provision job's push step look up whether that
version is already pushed before running coder templates push, since
the workflow triggers on any change under templates/** and previously
reprovisioned every template on every push, not just the one that
changed.
Currently a no-op given GITEA_TOKEN can't reach the registry at all yet,
but harmless to have in place now so nothing needs to change here once
that fix ships and PACKAGE_REGISTRY_TOKEN can eventually be retired.
secrets.GITEA_TOKEN cannot authenticate to Gitea's container registry in
any currently-shipped version, regardless of the permissions: block or
which account triggers the workflow - confirmed via
go-gitea/gitea#23642 (open since 1.19) and its fix, PR #39070, which is
dated 2026-08-24 and not yet merged. Every run since switching to it has
failed at docker login with a plain "unauthorized". Reverting to a
manually-created PACKAGE_REGISTRY_TOKEN secret (the GITEA_PACKAGE_TOKEN
attempt from before this branch used a reserved-prefix name Gitea
silently refuses to create).
Documents what we just found the hard way: build-images's docker push
runs as whichever Gitea account authenticated the triggering push
(github.actor), which needs its own package-registry write access -
separate from that account having git push rights to the repo at all.
GITEA_PACKAGE_TOKEN was never a creatable secret name (GITEA_ prefix is
reserved), and Gitea Actions' built-in token can be granted registry
write access directly via `permissions: packages: write` (per
https://docs.gitea.com/usage/actions/token-permissions/) - the
"unauthorized: reqPackageAccess" issue noted earlier looks to have been
exactly this: the default restricted token mode denying package write
unless a job explicitly requests it, not an unfixable bug in the token
itself. Drops the manual-PAT requirement entirely - no secret to create
or maintain.
GITEA_PACKAGE_TOKEN was never actually creatable - Gitea Actions reserves
the GITEA_ prefix for its own built-in secrets, so Settings > Actions >
Secrets rejects a repo secret by that name. The workflow referenced a
secret that could never exist, so docker login always got an empty
password and failed with a confusing "interactive login from a non TTY
device" error. Renamed to PACKAGE_REGISTRY_TOKEN, and added an explicit
empty-secret check so a future misconfiguration fails with a clear
message instead of that confusing docker error.
docker:27-cli (Alpine) has no bash, only the POSIX /bin/sh (busybox
ash). run: steps default to bash, so both steps in build-images failed
immediately with "exec: bash: executable file not found in $PATH" -
confirmed via the run's log right after the previous checkout fix
landed. Both steps are already plain POSIX shell, so declare shell: sh
as the job default instead of switching interpreters.
actions/checkout is a JS action and needs Node to run inside the job
container. The docker-build runner's job image (docker:27-cli, Alpine,
just the Docker CLI) has no Node, so the step failed immediately with
"exec: node: executable file not found in $PATH" - confirmed via the
run's actual log (build-images failed after 16s on every push since the
image-in-CI switch, including the just-merged duplicate-mount and
if:always() fixes). Alpine has apk/git, so clone directly instead.
Fixes a regression where the whole provision job (all 6 templates) was
skipped whenever build-web-image failed, since Actions skips a job whose
needs: dependency failed by default. Generalizes the single web-image
build step into a loop over every templates/*/Dockerfile, building and
pushing each independently so one failure doesn't block the rest, and
reports failures via a job output. provision now runs unconditionally
(if: always()) and skips pushing only the specific template(s) whose
image build failed this run, leaving their previous working version in
place instead of pushing one with no matching registry tag.
First real run of the new job failed at container creation:
"Error response from daemon: Duplicate mount point: /var/run/docker.sock".
The docker-build runner's config.yaml has docker_host: "" (not "-"),
which means act_runner already auto-injects the host socket into job
containers on its own - the workflow's explicit
container.volumes mount for the same path was a second, conflicting
request for it. Just dropping the explicit mount; the runner already
provides it.
templates/web's docker_image resource used a `build` block, so every
first-use of a new Dockerfile hash triggered a from-scratch build
(including the ~15-20min Rust toolchain compile) right at `terraform
apply` time - i.e. while someone was waiting to create a workspace.
Adds a build-web-image job to coder-templates.yml that builds and pushes
git.octoturge.com/octo-tech/profiles-web:<dockerfile-sha1> to this
instance's container registry, tagged identically to what
docker_image.web now computes and pulls (no build block). provision now
depends on build-web-image so a template never gets pushed pointing at
an image that isn't there yet. Skips the build entirely if that tag's
already in the registry, so an unrelated templates/* change doesn't
pay any cost.
Runs on a new dedicated "docker-build" runner (profiles-web-build),
scoped to just this repo via a repo-level registration token, with
host Docker socket access - deliberately not added to the existing
shared runner-1, which has no such access and stays untouched. Repo is
public, so the pulled image needs no registry auth; the push does, via
a new GITEA_PACKAGE_TOKEN repo secret (write:package scope).
Since CI and this Coder deployment share the same Docker daemon, the
"pull" is normally a same-host cache hit, not a real network pull.
Verified: `terraform validate` passes against the updated
templates/web/main.tf (run directly inside the coder-server container,
which has terraform embedded).
git ls-tree -d --name-only \$SHA -- templates matched the templates/
directory entry itself (basename "templates"), so every run tried and
harmlessly failed to delete a nonexistent profiles-templates template.
Use 'templates/*' as the pathspec to list only the child directories.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
coder templates push -d templates/<env> only uploads that directory to the
Coder server, so main.tf's file() references reaching outside it via
../../profile-templates and ../../scripts failed at push/apply time
("Invalid function argument ... this function works only with files that
are distributed as part of the configuration source code"). Confirmed via
an actual failed run of coder-templates.yml once the runner network fix let
it get that far.
Fix: duplicate profile.code-profile, cli-setup-wizard.sh, and
install-skills.sh into each templates/<env>/ directory and drop the old
shared scripts/ and profile-templates/ directories.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This Coder deployment caps API token lifetime at 168h (7 days), so
rather than raising that cap deployment-wide, add
.gitea/workflows/rotate-coder-token.yml: runs daily, mints a new 168h
coder token, PUTs it into this repo's CODER_SESSION_TOKEN secret via
Gitea's actions/secrets API (confirmed against the live instance's
swagger.v1.json - PUT /repos/{owner}/{repo}/actions/secrets/{name} with
{"data": "..."}), then deletes the token(s) it replaced. Old token isn't
touched until the new one is confirmed live, so a failed run fails safe.
Needs a one-time GITEA_ROTATION_TOKEN secret (a Gitea PAT with
write:repository scope, no expiration) so the workflow can write to its
own repo's secrets going forward - documented in README. After that,
CODER_SESSION_TOKEN (used by coder-templates.yml) never needs manual
attention again.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
.gitea/workflows/coder-templates.yml pushes every templates/<env>/ dir
to Coder as profiles-<env> on every push to main that touches
templates/**, scripts/**, or profile-templates/** - coder templates push
creates the template on first run and updates it thereafter, so adding a
new templates/<env>/ directory is enough to provision it, no workflow
edits needed.
It also diffs templates/ against the previous commit and runs
`coder templates delete profiles-<env>` for any directory that was
removed. Deletion fails (loudly, as a job warning, not a hard failure)
rather than succeeding if the template still has active workspaces,
since coder templates delete already refuses that server-side.
Runs on the ubuntu-latest self-hosted Gitea runner already registered on
this instance (confirmed via gitea-runner-1's /data/.runner labels) and
installs the coder CLI itself. Needs CODER_URL and CODER_SESSION_TOKEN
as repo/org Actions secrets - documented in README, left for the user to
set up since token creation needs their own Coder login.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>