Skip to main content
Version: 0.4.x

Service Templates

wxops-templates is the seed repository the scaffold wizard reads from. Each template directory is a complete, production-ready microservice starter. The portal reads template.yaml to render the wizard card; on scaffold, it copies the remaining files into a fresh Gitea repo and substitutes template variables throughout.

Template Catalog

TemplateLanguageFrameworkDockerfile baseDefault port
golang-serviceGo 1.26stdlib net/httpdistroless/static-debian12:nonroot8080
nodejs-serviceNode.js 24Express + TypeScriptalpine (appuser 1001)8080
python-servicePython 3.14FastAPI + uvicornalpine (appuser 1001)8080

All three templates expose the same four HTTP endpoints, ship the same CI pipeline structure, and are functionally equivalent from the platform's perspective.

template.yaml Format

template.yaml lives at the root of each template directory. It is portal metadata only — it is not copied into the scaffolded project.

name: golang-service # stable identifier; portal falls back to dir name
title: Go Microservice # card heading in the wizard picker
description: > # shown on the wizard card
Production-ready Go service with structured logging,
health checks, and Prometheus metrics.
tags:
- backend
- go
- microservice

runtime:
language: go # go | python | node
version:
default: "1.26"
options:
- "1.26"
- "1.25"
- "1.24"
- "1.23"
# packageManager: only for python and node

defaults: # pre-fills wizard form fields
port: 8080
replicas: 1
cpuRequest: 100m
memoryRequest: 128Mi
cpuLimit: 200m
memoryLimit: 256Mi
healthPath: /healthz
metricsPath: /metrics

recommends: # shown as badges on the card; never auto-enabled
vault: true
database: false
api: false
apiType: openapi
ingress: true

Template Variable Substitution

The portal substitutes these variables throughout all copied files:

VariableDescriptionExample
{{ .ProjectName }}New service namepayment-api
{{ .ProjectOwner }}Gitea org/userrocket-team
{{ .TenantNamespace }}Kubernetes namespacetenant-rocket-team
{{ .RuntimeVersion }}Language version chosen in wizard1.26, 3.14, 26
{{ .PackageManager }}Package manager (Node/Python only)npm, poetry, uv

Variables appear in Dockerfiles (ARG GO_VERSION={{ .RuntimeVersion }}), Makefiles, source code service name strings, cliff.toml URLs, tunnel configs, and release-notes templates.

What Every Template Generates

Endpoint Contract

All templates must expose these four endpoints — the platform depends on them:

EndpointResponseUsed by
GET /healthz{"status":"ok"}Kubernetes liveness probe
GET /readyz{"status":"ok"}Kubernetes readiness probe
GET /metricsPrometheus text formatkube-prometheus-stack scrape
GET /{"service":"<name>","status":"running"}Smoke tests

Graceful Shutdown

  • Go: http.Server.Shutdown() with 15-second context timeout on SIGTERM/SIGINT
  • Node.js: server.close() with 15-second forced exit
  • Python: uvicorn handles natively

Multi-Stage Dockerfile

Two-stage build: builder stage compiles/installs → production stage runs as non-root. ARG RUNTIME_VERSION={{ .RuntimeVersion }} makes the version configurable at build time.

Pre-commit Hooks

Every template ships a .pre-commit-config.yaml that runs on git commit and enforces consistent quality gates before code ever leaves the developer's machine. Install once per machine with pip install pre-commit && pre-commit install.

Common hooks (all three templates)

HookWhat it checksFail condition
trailing-whitespaceTrailing spaces on any lineAny found (.md files excluded)
end-of-file-fixerFiles end with a newlineMissing newline
check-yamlYAML syntaxParse error
check-jsonJSON syntaxParse error
detect-private-keyPrivate key material in any filePattern match
check-added-large-filesFile sizes>500 KB
gitleaks (v8.22.1)Secret patterns in diffsAny detected secret
hadolint-docker (v2.13.1)Dockerfile best practicesLint error
conventional-pre-commitCommit message formatDoes not match Conventional Commits
check-docs (local script)docs/ structure completenessMissing required sections

Language-specific linter hooks

TemplateHook(s)What they fix/flag
golang-servicego-fmt, go-vet, go-mod-tidyFormatting, static analysis, module graph
nodejs-serviceeslint (v9)TypeScript/JS lint errors and rule violations
python-serviceruff --fix, ruff-formatPEP 8, import sort, auto-fixable issues

Conventional commit enforcement

conventional-pre-commit runs at the commit-msg stage (not the pre-commit stage), so it only validates the message, never the diff. The allowed types mirror the commit_parsers in cliff.toml — adding a type to one requires updating the other:

Commit typeChangelog group
featFeatures
fixBug Fixes
perfPerformance
refactorRefactor
docsDocumentation
testTesting
chore(ci)CI/CD
chore(deps)Dependencies
chore(release): …skipped (release machinery)
choreChores
revertReverts
feat(…)! / fix(…)!Breaking Changes

Doc check hook

scripts/check-docs.sh runs only when files under docs/ change. It validates the required structure: GOVERNANCE.md, at least one ADR under adrs/, and at least one runbook under runbooks/. Bypass with SKIP_DOC_CHECK=1 git commit or include [skip-docs] in the commit message.

Changelog & Release

Every template ships cliff.toml and three Makefile targets that tie local development to the CI release pipeline.

How git-cliff works

git-cliff reads the full git log, groups commits by type using cliff.toml commit_parsers, and renders CHANGELOG.md from a Tera template. Two important behaviors:

  • RC tags are skipped. skip_tags = ".*-rc.*" means staging RC tags (v1.2.0-rc1) never appear in the changelog — only stable vX.Y.Z tags produce a new section.
  • Breaking changes are protected. protect_breaking_commits = true prevents a squash or rebase from hiding a commit marked with ! (e.g. feat(api)!: …).

Commit links and diff URLs in the changelog body point to the scaffolded project's Gitea repo — the {{ .ProjectOwner }} and {{ .ProjectName }} template variables are substituted at scaffold time so all links are correct from day one.

Makefile targets

TargetWhat it does
make changelogRegenerate CHANGELOG.md from full git history via git-cliff -o CHANGELOG.md
make changelog-previewPrint the unreleased section to stdout without writing any file
make releaseAuto-bump version via git-cliff --bumped-version, tag HEAD as vX.Y.Z, push the tag; blocks -rc tags
make release VERSION=v1.2.0Same as above but with a manual version override

The make release target refuses to create an -rc tag — RC versions are assigned automatically by CI on every merge to staging. You only run make release to cut a production tag on main.

CI release job

When a v* tag (not -rc) is pushed, CI runs a dedicated release job after build-and-push succeeds:

1. Checkout with full git history (fetch-depth: 0)
2. Install git-cliff
3. Regenerate CHANGELOG.md → commit back to main as
"chore(release): update changelog for vX.Y.Z [skip ci]"
4. Build release body from release-notes/vX.Y.Z.md (if it exists) or
release-notes/template.md, with {{CLIFF_NOTES}} replaced by git-cliff output
5. Create a Gitea release via softprops/action-gh-release

The [skip ci] suffix on the changelog commit prevents a recursive pipeline trigger.

Optional hand-written release notes

If release-notes/vX.Y.Z.md exists when the tag is pushed, CI uses it as the release body template and splices in the auto-generated git-cliff output at the {{CLIFF_NOTES}} placeholder. If neither file exists, git-cliff output is used directly. This lets platform teams write highlights above the raw commit list.

Documentation Structure

docs/
├── GOVERNANCE.md # RFC → ADR decision flow, bypass rules
├── adrs/
│ └── adr-001-initial-architecture.md
└── runbooks/
└── runbook-deployment.md

Doc validation runs as a pre-commit hook. Bypass with SKIP_DOC_CHECK=1 or [skip-docs] in the commit message.

Developer Tunnel Configs

Both mirrord and Telepresence configs ship in every template:

.mirrord/mirrord.json # mirrord exec target: deployment/{{ .ProjectName }}-darlane
.telepresence/config.yml # intercept: {{ .ProjectName }} in {{ .TenantNamespace }}

See Debug with Darlane for when to use each.

CI Pipeline

The pipeline in .gitea/workflows/ci.yaml has three jobs:

JobRuns whenWhat it does
testPush to develop/staging, all PRsLanguage-specific unit tests
securityEvery pushTrivy filesystem + image scan (warnings only, never blocks)
build-and-pushAll pushes except PRsBuilds and tags image (see table below)

Image Tag Strategy

Git refTag producedHow
Push to developdev-{YYYY-MM-DD_HH-MM-SS}-{sha7}Build + push
Merge to stagingvX.Y.Z-rcNcrane cp dev-tag → rc-tag (no rebuild)
Merge to staging (hotfix)continues RC seriesRebuild if source changed
v* tag on mainvX.Y.Zcrane cp rc-tag → stable-tag (no rebuild)

The crane cp approach (copy-then-retag) guarantees the RC and production images are byte-for-byte identical — no rebuild, no drift. See Git Flow for the full branch model.

Adding a New Template

Checklist for a new template directory in wxops-templates:

  • template.yaml with all required fields (name, title, description, tags, runtime, defaults, recommends)
  • Four required HTTP endpoints (healthz, readyz, metrics, /)
  • Graceful shutdown on SIGTERM/SIGINT with ≤15s timeout
  • Multi-stage Dockerfile with non-root user, ARG for runtime version
  • .gitea/workflows/ci.yaml matching the three-job structure
  • Makefile with all required targets (dev, build, test, lint, docker-build, docker-run, release, changelog, etc.)
  • docs/ structure (GOVERNANCE.md, adrs/adr-001-*.md, runbooks/runbook-deployment.md)
  • .mirrord/mirrord.json + .telepresence/config.yml with template variables
  • Pre-commit hooks config (.pre-commit-config.yaml)
  • cliff.toml with {{ .ProjectOwner }} / {{ .ProjectName }} Gitea URLs