Project Conventions#

This project is a showcase for a language-agnostic development template that combines devcontainer isolation with Claude Code integration. The conventions documented here are intended to be shared across projects regardless of language stack — TypeScript, Python, Go, or others — via a copier template (extending python-copier-template). The goal is a consistent developer experience: anyone who has worked on one project can pick up another without re-learning the tooling.

See Claude Code Integration for the AI-assisted development pattern that sits on top of these conventions.

Task runner: just#

All projects use just as the task runner. just is language-agnostic, has no runtime dependencies beyond a single binary, and uses a simple recipe syntax.

Every project must expose at least these recipes:

Recipe

Purpose

just check

Run lint, test, and docs in parallel — the local CI equivalent

just lint

Language-specific linting and type checking

just test

Run tests with coverage

just docs

Build Sphinx documentation

just dev

Start the development server (where applicable)

just build

Production build (where applicable)

just check is the single command a developer runs before committing. CI runs the same checks, so a passing just check should predict a passing CI build.

just check vs pre-commit hooks#

Both just check and the pre-commit hooks run linting and type checking, but they serve different purposes and the overlap is intentional:

  • Pre-commit hooks gate each commit. They run only on staged files, apply auto-fixes where possible, and include housekeeping checks (large files, merge conflicts, end-of-file fixing, gitleaks, conventional commit validation).

  • just check is a full-project validation. It runs lint, tests, and the docs build in parallel — read-only, no auto-fix — intended for manual use before committing or as a CI gate.

Removing either would leave a gap: pre-commit catches issues at commit time on changed files, while just check validates the entire project.

Commit messages: Conventional Commits#

All projects use Conventional Commits, enforced by a commit-msg pre-commit hook. The prefix communicates intent and enables automated changelogs:

Prefix

When to use

feat:

New feature

fix:

Bug fix

docs:

Documentation only

chore:

Maintenance (deps, config, CI)

refactor:

Code change that neither fixes a bug nor adds a feature

test:

Adding or updating tests

security:

Security hardening

Keep the summary line under 70 characters. Use the body for detail.

Documentation: Sphinx + MyST + Diataxis#

All projects build documentation with Sphinx using MyST for Markdown support and the PyData theme.

Documentation follows the Diataxis framework with four sections:

  • Tutorials (docs/tutorials/) — learning-oriented, get to a working result

  • How-to guides (docs/how-to/) — task-oriented, practical steps for experienced users

  • Explanations (docs/explanations/) — understanding-oriented, how and why things work

  • Reference (docs/reference/) — information-oriented, precise technical specifications

CI treats Sphinx warnings as errors (--fail-on-warning). New pages must be added to the appropriate toctree or the build will fail.

Pre-commit hooks#

All projects use pre-commit to catch issues at commit time. The standard hooks across all languages:

  • Large file detection — prevents accidental binary commits

  • YAML validation — catches syntax errors in config files

  • Merge conflict markers — blocks commits with unresolved conflicts

  • End-of-file fixing — ensures consistent file endings

  • Gitleaks — scans for hardcoded secrets

  • Conventional Commits — validates commit message format

Language-specific hooks are added per project (e.g. ESLint for TypeScript, Ruff for Python, golangci-lint for Go).

For projects with Helm charts, the helm-values-schema-json hook regenerates values.schema.json from annotated values.yaml on every commit.

Helm chart conventions#

Projects that deploy to Kubernetes include a Helm chart with a generated JSON Schema for values.yaml. The schema provides IDE autocompletion and catches misconfiguration early.

The schema is generated from @schema annotations in values.yaml:

# @schema description: Kubernetes Service type
# @schema enum: [ClusterIP, LoadBalancer, NodePort]
type: LoadBalancer

A .schema.config.yaml in the chart directory configures the generator. The generated values.schema.json is committed to the repo and regenerated automatically by the pre-commit hook.

Chart versioning is handled by CI — the version is derived from the git tag (e.g. tag v1.2.3 → chart version 1.2.3). Do not manually bump version in Chart.yaml.

CI: GitHub Actions#

All projects use a consistent GitHub Actions CI structure with reusable workflow files:

.github/workflows/
├── ci.yml              # Main orchestrator
├── _check.yml          # Lint, type check, and test
├── _container.yml      # Docker image build and publish
├── _docs.yml           # Sphinx docs build and GitHub Pages deploy
├── _helm.yml           # Helm chart packaging (if applicable)
└── _release.yml        # GitHub Release creation

The ci.yml workflow orchestrates: lint → test → container → docs → release. Container images and Helm charts are published only on tagged releases.

Releases: GitHub Releases#

Releases are triggered by pushing a git tag (e.g. git tag v1.2.3 && git push origin v1.2.3). CI publishes artifacts (container images, Helm charts) and the _release.yml workflow creates a GitHub Release with auto-generated notes. Tags containing a, b, or rc are marked as pre-releases.

Dependency updates: Renovate#

All projects use Renovate for automated dependency updates. Minor and patch updates are auto-merged when tests pass. Major updates create PRs for manual review.

Git workflow#

  • Never push directly to main — all changes go through pull requests

  • PRs are the unit of review and the trigger for CI

Devcontainer#

Every project runs in a Dev Container with rootless Podman (or rootless Docker) as the intended runtime. The security assumptions below rely on unprivileged container execution. The setup is derived from the python-copier-template pattern used across Diamond Light Source projects.

Base image and setup scripts#

The devcontainer Dockerfile uses a base image with common development tools pre-installed. System-level tooling is baked into the image for faster starts, while tools that change frequently are installed by scripts:

  • postCreate.sh runs once on first container creation — installs Claude Code CLI, language dependencies, and pre-commit hooks

  • postStart.sh runs on every container start (including restarts). This is necessary because VS Code copies the host gitconfig into the container after postCreateCommand, which can re-inject credential helpers. The script resets the credential helper each time to maintain isolation.

Credential isolation#

The devcontainer isolates credentials from the host to limit the blast radius of prompt injection attacks:

  • SSH_AUTH_SOCK="" disables SSH agent forwarding, preventing access to host SSH keys

  • postStart.sh blanks the git credential helper and removes any url.ssh://git@github.com/.insteadOf rewrite on every start. Without this, the SSH rewrite would bypass HTTPS authentication entirely

  • Scoped GitHub PAT — authentication uses a fine-grained token limited to specific repositories, persisted in a per-repo container volume (gh-auth-${localWorkspaceFolderBasename}). Set up via just gh-auth

Persistent caches#

The devcontainer uses container volumes for persistence across rebuilds:

Volume

Mount

Purpose

devcontainer-shared-cache

/cache

uv, pre-commit, Python venvs

gh-auth-${workspaceFolderBase}

~/.config/gh

Per-repo GitHub CLI auth

The gh-auth volume stores a fine-grained PAT. This is safe under rootless Podman: no daemon socket to compromise, user-owned storage with standard file permissions, user namespace isolation, and scoped tokens that limit blast radius.

Workspace mounting#

The parent directory is mounted as /workspaces rather than just the project directory:

"workspaceMount": "source=${localWorkspaceFolder}/..,target=/workspaces,type=bind"

This allows pip install -e ../sibling-project for developing against peer projects. The host’s ~/.claude directory is bind-mounted so Claude Code configuration and memory persist across container rebuilds.

What varies per language#

The conventions above are universal. The following are language-specific choices that each project makes independently:

Concern

TypeScript

Python

Go

Linter

ESLint + tsc

Ruff + Pyright

golangci-lint

Test runner

vitest

pytest

go test

Package manager

npm

uv

go modules

Formatter

ESLint

Ruff

gofmt

Devcontainer base

ubuntu-devcontainer + Node.js

ubuntu-devcontainer

mcr.microsoft.com/devcontainers/go

The justfile recipes wrap these language-specific tools so that the top-level commands (just check, just test, etc.) remain the same everywhere.