2867db22af
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.
218 lines
10 KiB
YAML
218 lines
10 KiB
YAML
name: Provision Coder Templates
|
|
|
|
# Keeps Coder templates in sync with templates/*/ in this repo:
|
|
# - every push to main pushes a new version of each templates/<env>/ dir
|
|
# whose VERSION file names a version not already pushed (coder templates
|
|
# push creates the template if it doesn't exist yet, so adding a new
|
|
# templates/<env>/ directory - with a VERSION file - is enough to
|
|
# provision a new one). This workflow triggers on any change under
|
|
# templates/**, not just a specific template's own directory, so VERSION
|
|
# is what keeps an edit to one template from generating a no-op new
|
|
# version for every other, unchanged template.
|
|
# - if a templates/<env>/ directory is removed on main, its template is
|
|
# deleted from Coder. `coder templates delete` refuses to delete a
|
|
# template that still has active workspaces, so this can't silently
|
|
# orphan running workspaces - it just fails loudly and needs a human.
|
|
#
|
|
# Requires three repo/org secrets (Settings > Actions > Secrets):
|
|
# CODER_URL e.g. https://code.octoturge.com
|
|
# CODER_SESSION_TOKEN a token from `coder tokens create`, ideally under a
|
|
# dedicated service account rather than a personal one
|
|
# PACKAGE_REGISTRY_TOKEN a Gitea access token (user Settings > Applications)
|
|
# with write:package scope, for pushing each
|
|
# Dockerfile-having template's image to this
|
|
# instance's container registry.
|
|
#
|
|
# Not secrets.GITEA_TOKEN (Gitea Actions' built-in token): as of this
|
|
# writing it cannot authenticate to the container registry in any shipped
|
|
# Gitea version - `permissions: packages: write` is a no-op because the
|
|
# Actions token's package scope isn't wired up server-side yet (open since
|
|
# Gitea 1.19: https://github.com/go-gitea/gitea/issues/23642; fix in
|
|
# https://github.com/go-gitea/gitea/pull/39070, not yet merged). Every
|
|
# attempt fails at docker login with a plain "unauthorized", regardless of
|
|
# the permissions: block or which account triggered the workflow. A
|
|
# manually-created PAT is the only thing that currently works. Named
|
|
# without a GITEA_ prefix because Gitea Actions reserves that prefix for
|
|
# its own built-in secrets and rejects creating one with that name.
|
|
#
|
|
# Any templates/<env>/ that has its own Dockerfile gets its image built and
|
|
# pushed here (build-images, on the dedicated "docker-build" runner - see
|
|
# templates/web/main.tf for why: that runner is scoped to this repo only and
|
|
# has host Docker socket access that the shared runner-1 deliberately
|
|
# doesn't). provision then just pulls the tag build-images produced, instead
|
|
# of building it itself at `terraform apply` time - keeps a slow toolchain
|
|
# compile off of "someone is waiting to create a workspace".
|
|
#
|
|
# Each template's image is built independently - one Dockerfile failing to
|
|
# build doesn't stop the others from building, and provision skips pushing
|
|
# only the specific template(s) whose image build failed this run (leaving
|
|
# their previous, already-working Coder template version in place) rather
|
|
# than skipping every template or pushing one with no matching image.
|
|
|
|
on:
|
|
push:
|
|
branches: [main]
|
|
paths:
|
|
- "templates/**"
|
|
- ".gitea/workflows/coder-templates.yml"
|
|
workflow_dispatch: {}
|
|
|
|
jobs:
|
|
build-images:
|
|
# The docker-build runner's docker_host: "" setting already auto-injects
|
|
# /var/run/docker.sock into job containers - an explicit
|
|
# container.volumes mount for the same path here fails at container
|
|
# creation with "Duplicate mount point: /var/run/docker.sock".
|
|
runs-on: docker-build
|
|
# Currently a no-op (see the PACKAGE_REGISTRY_TOKEN note above) but
|
|
# harmless to declare now - once go-gitea/gitea#39070 ships, GITEA_TOKEN
|
|
# will need this to get package write access, so this is one less thing
|
|
# to remember when PACKAGE_REGISTRY_TOKEN can eventually be retired.
|
|
permissions:
|
|
packages: write
|
|
outputs:
|
|
failed_templates: ${{ steps.build.outputs.failed_templates }}
|
|
# docker:27-cli (Alpine) has no bash - only the POSIX /bin/sh (busybox
|
|
# ash) - but run: steps default to bash, which fails with "exec: bash:
|
|
# executable file not found in $PATH". Both run: steps below are plain
|
|
# POSIX shell already, so just run them under sh.
|
|
defaults:
|
|
run:
|
|
shell: sh
|
|
steps:
|
|
# actions/checkout is a JS action and needs Node in the job container -
|
|
# this job runs in docker:27-cli (Alpine, just the Docker CLI) so it has
|
|
# no Node, and actions/checkout fails immediately with "exec: node:
|
|
# executable file not found in $PATH". Alpine does have apk/git though,
|
|
# so clone directly instead.
|
|
- name: Checkout
|
|
run: |
|
|
set -e
|
|
apk add --no-cache git
|
|
git clone --depth 1 --branch "${{ github.ref_name }}" "${{ github.server_url }}/${{ github.repository }}.git" .
|
|
|
|
- name: Build and push every template's image (skips a tag that's already in the registry)
|
|
id: build
|
|
run: |
|
|
set -e
|
|
echo "${{ secrets.PACKAGE_REGISTRY_TOKEN }}" | docker login git.octoturge.com -u octoturge --password-stdin
|
|
|
|
FAILED=""
|
|
for dockerfile in templates/*/Dockerfile; do
|
|
[ -e "$dockerfile" ] || continue
|
|
dir="$(dirname "$dockerfile")"
|
|
name="$(basename "$dir")"
|
|
TAG="$(sha1sum "$dockerfile" | cut -d' ' -f1)"
|
|
IMAGE="git.octoturge.com/octo-tech/profiles-${name}:${TAG}"
|
|
|
|
echo "::group::${name}"
|
|
if docker manifest inspect "$IMAGE" >/dev/null 2>&1; then
|
|
echo "$IMAGE already in the registry (Dockerfile unchanged), skipping build."
|
|
elif docker build -t "$IMAGE" "$dir" && docker push "$IMAGE"; then
|
|
echo "Built and pushed $IMAGE"
|
|
else
|
|
echo "::warning::Failed to build/push $IMAGE - templates/$name will be skipped this run."
|
|
FAILED="$FAILED $name"
|
|
fi
|
|
echo "::endgroup::"
|
|
done
|
|
|
|
echo "failed_templates=${FAILED# }" >> "$GITHUB_OUTPUT"
|
|
|
|
provision:
|
|
# needs: build-images orders this after the image builds (so a fresh
|
|
# template push never points at a tag that isn't in the registry yet)
|
|
# without making every template's reprovisioning depend on ALL builds
|
|
# succeeding - if:always() overrides the default "skip if a dependency
|
|
# failed" behavior, since build-images only fails outright on an
|
|
# infra-level problem (e.g. registry login); a single template's build
|
|
# failure is reported via failed_templates instead and only skips that
|
|
# one template below.
|
|
needs: build-images
|
|
if: always()
|
|
runs-on: ubuntu-latest
|
|
env:
|
|
CODER_URL: ${{ secrets.CODER_URL }}
|
|
CODER_SESSION_TOKEN: ${{ secrets.CODER_SESSION_TOKEN }}
|
|
FAILED_TEMPLATES: ${{ needs.build-images.outputs.failed_templates }}
|
|
steps:
|
|
- name: Checkout (full history, needed to detect removed templates)
|
|
uses: actions/checkout@v4
|
|
with:
|
|
fetch-depth: 0
|
|
|
|
- name: Install coder CLI
|
|
run: |
|
|
set -e
|
|
curl -fsSL https://coder.com/install.sh | sh
|
|
coder version
|
|
|
|
- name: Push (create or update) every template
|
|
run: |
|
|
set -e
|
|
for dir in templates/*/; do
|
|
name="$(basename "$dir")"
|
|
full="profiles-$name"
|
|
case " $FAILED_TEMPLATES " in
|
|
*" $name "*)
|
|
echo "::warning::Skipping $full - its Docker image failed to build this run (see build-images), leaving the previous template version in place."
|
|
continue
|
|
;;
|
|
esac
|
|
|
|
# templates/<name>/VERSION lets a template opt out of being
|
|
# reprovisioned on every push: bump it and coder templates push
|
|
# names the new version "v<N>"; leave it as-is and this looks up
|
|
# whether that version name is already pushed and skips if so.
|
|
# This is a manual contract, not a content hash - editing a
|
|
# template without bumping its VERSION means the change won't
|
|
# go out until someone does. paths: on this workflow's trigger
|
|
# is templates/** as a whole, so without this every template
|
|
# gets a new (identical) version on any push under templates/,
|
|
# even ones whose own directory didn't change.
|
|
version=""
|
|
if [ -f "$dir/VERSION" ]; then
|
|
version="$(tr -d '[:space:]' < "$dir/VERSION")"
|
|
fi
|
|
if [ -n "$version" ]; then
|
|
existing="$(coder templates versions list "$full" -o json 2>/dev/null || true)"
|
|
if [ -n "$existing" ] && echo "$existing" | jq -e --arg v "v$version" 'any(.[]; .name == $v)' >/dev/null 2>&1; then
|
|
echo "Skipping $full - version v$version (templates/$name/VERSION) is already pushed. Bump the VERSION file to push a new one."
|
|
continue
|
|
fi
|
|
fi
|
|
|
|
args=(-d "$dir" --yes -m "auto-provisioned from ${GITHUB_SHA:0:12}")
|
|
[ -n "$version" ] && args+=(--name "v$version")
|
|
|
|
echo "::group::Pushing $full from $dir"
|
|
coder templates push "$full" "${args[@]}"
|
|
echo "::endgroup::"
|
|
done
|
|
|
|
- name: Delete templates whose directory was removed
|
|
if: github.event_name == 'push'
|
|
run: |
|
|
set -e
|
|
PREV_SHA="$(git rev-parse HEAD~1 2>/dev/null || true)"
|
|
if [ -z "$PREV_SHA" ]; then
|
|
echo "No previous commit on this branch (first push), nothing to diff. Skipping."
|
|
exit 0
|
|
fi
|
|
|
|
OLD_DIRS="$(git ls-tree -d --name-only "$PREV_SHA" -- 'templates/*' 2>/dev/null | xargs -n1 basename 2>/dev/null || true)"
|
|
if [ -z "$OLD_DIRS" ]; then
|
|
echo "No templates/ directory at $PREV_SHA, nothing to diff. Skipping."
|
|
exit 0
|
|
fi
|
|
|
|
for old in $OLD_DIRS; do
|
|
if [ ! -d "templates/$old" ]; then
|
|
name="profiles-$old"
|
|
echo "::group::Deleting $name (templates/$old was removed)"
|
|
coder templates delete "$name" --yes \
|
|
|| echo "::warning::Failed to delete $name - check for active workspaces still using it."
|
|
echo "::endgroup::"
|
|
fi
|
|
done
|