Contributing¶
A merge to main is a deployment: ArgoCD applies payload/ to real hardware,
and there is no staging cluster in between. The checks below exist to make that
routine.
Setup¶
uv sync # virtualenv and dependencies, including dev tools
uv run pre-commit install # the same linters CI runs
Checks¶
The same checks, one at a time:
uv run yamllint -f github .
uv run zensical build --clean --strict # docs site
markdownlint-cli2 '**/*.md' '!**/.venv' '!.agent'
uv run ruff check . && uv run ruff format --check . # python only
uv run pytest -q # python only
cd ansible && uv run ansible-lint # ansible only
Before touching payload/, render what Helm will actually apply; the
kubectl line needs a kubeconfig, since even a client-side dry run asks the
API server for its resource types.
helm template <name> <repo>/<chart> --version <targetRevision> -f <values>
kubectl apply --dry-run=client -f payload/platform/<component>/
--strict is what CI uses. A page not registered in nav in zensical.toml
fails the build, deliberately. On a pull request, the argo-diff-preview
workflow comments the rendered ArgoCD manifest diff between main and your
branch; read it, since a three-line values change can render as two hundred
lines of different objects. The full list of workflows is in
Maintenance.
Make targets¶
Every target in the Makefile, and the page that explains it. The ones that
touch the cluster read output/kubeconfig (KUBECONFIG=<file> picks
another).
| Target | Does | Page |
|---|---|---|
setup |
Creates the virtualenv with every tool | Setup |
download |
Fetches and verifies the Flatcar image, syslinux and the sysexts | Quickstart |
config |
Renders the Ignition configs, PXE menus and credentials from the inventory | Quickstart, Ansible |
artifacts |
download then config |
Quickstart |
serve |
Runs the DHCP-proxy, TFTP and HTTP boot server | Boot server |
reinstall |
Arms a node (LIMIT=<node>) or, after a typed confirmation, every node to wipe and install on its next network boot |
Node Lifecycle |
reinstall-cancel |
Disarms them again | Switching back to local boot |
kubeconfig |
Copies the admin kubeconfig to output/kubeconfig |
Quickstart |
check-context |
Guard the cluster targets depend on: fails without a kubeconfig | |
bootstrap |
install-cilium, install-argo, bootstrap-apps in one go |
Quickstart |
install-cilium |
Gateway API CRDs and Cilium, at the pinned versions | Quickstart |
install-argo |
The argo-cd chart, at the pinned version | Quickstart |
bootstrap-apps |
The AppProjects and the argocd Application; ArgoCD owns everything after |
Quickstart |
storage-check |
Walks the Rook chain and binds a test PVC | Is storage ready? |
bao-init |
Initialises, unseals and configures OpenBao once | OpenBao |
bao-unseal |
Unseals every sealed replica from the key file (KEYFILE=<file> picks another) |
OpenBao |
bao-policies |
Rewrites ESO's policies and Kubernetes auth roles | OpenBao |
bao-secrets |
Prompts for the typed secrets and writes them | OpenBao |
bao-rotate-root |
Replaces a kept root token with the operator login | OpenBao |
claude-session-pat |
Stores (SESSION=<name>) or removes (DELETE=1) one agent session's GitHub token |
Claude agents |
untaint, taint |
Removes and restores the control-plane taint | Single-node clusters |
fonts, fonts-check |
Downloads the pinned webfonts, or diffs them against what is committed | Fonts |
clean-artifacts, clean |
Deletes the generated files, or everything under output/ after asking |
Repository layout |
Branches and commits¶
Branch names are prefixed by area: docs/, feat/, fix/, chore/. Commit
messages follow Conventional Commits:
docs: explain the stack choices and collect the known limitations
fix(bootstrap): repoint the install targets at the files that exist
chore(deps): update helm release kube-prometheus-stack to v89
The body explains why; the diff already says what.
Do not hand-edit version numbers
targetRevision in the manifests and the versions in ansible/inventory.yaml are owned by Renovate — see Maintenance. A hand bump conflicts with, or is reverted by, the next Renovate PR. The bootstrap targets in the Makefile read the same targetRevision values rather than pinning their own — see Version pins.
Review¶
.github/CODEOWNERS assigns every path to the repository owner, which requests
that review; it is not required, because an author cannot approve their own
pull request. What main requires is the ci-ok check; which Renovate PRs
merge on it alone is in Automerge policy.
.agent/rules/general-rules.md holds the standing rules for AI coding agents
working here: GitOps only, docs in docs/, no hand-edited versions. Keep it in
sync when those conventions change.
Documentation¶
The site is built with Zensical from docs/,
configured in zensical.toml, and published to GitHub Pages by
.github/workflows/docs.yaml at https://homelab.wlkr.ch/. It is
deliberately not hosted on the cluster, so it stays readable when the cluster
is not. The build also writes llms.txt, llms-full.txt and a .md twin of
every page for LLM clients (llmstxt.org); the sections of
llms.txt are listed in zensical.toml under plugins.llmstxt, so a new
section in nav needs an entry there too.
| Stage | What happens |
|---|---|
| Build | On push to main touching docs/**, overrides/**, zensical.toml, pyproject.toml or uv.lock, docs.yaml runs zensical build --clean --strict |
| Deploy | The workflow drops a .nojekyll marker into site/ (otherwise Pages runs Jekyll, which drops paths it considers private) and pushes to gh-pages, leaving pr-preview/ untouched |
| Previews | preview.yaml builds every pull request matching the same paths, forks included, and publishes it under pr-preview/ on gh-pages; closing the PR removes it. Its path filter must match docs.yaml, because this is the only --strict build a PR gets. Only the publish step is skipped for fork PRs: they have no write access |
Locally:
uv run zensical serve # http://localhost:8000, rebuilds on save
uv run zensical build # one-off build into site/ (gitignored)
Conventions¶
- Every page is registered in
nav; cross-references are relative Markdown paths (platform/openbao.md,../quickstart.md) so they resolve on the site and on GitHub. - Diagrams use Mermaid fences; Zensical loads the runtime on pages that contain one.
- Markdown is linted by
lint-markdown.yamland themarkdownlint-cli2pre-commit hook.MD046: a blank line followed by an indented line reads as a code block, so keep!!!admonition bodies to a single paragraph.MD007: nested lists indent by four spaces. - Inline HTML is limited to
<div>(MD033in.markdownlint-cli2.yaml), which the card grids on hub pages need. - Theme templates are overridden by a same-named file under
overrides/(theme.custom_dir). Currently onlypartials/source.html, which drops the repository-facts call that 404s because this repository publishes no releases.
Fonts¶
Fonts are self-hosted so the site makes no third-party requests:
theme.font = false in zensical.toml, docs/stylesheets/fonts.css declares
Inter and JetBrains Mono from the woff2 files in docs/assets/fonts/, and
each SIL OFL licence sits next to them. scripts/update-fonts.sh pins both
upstream releases (Inter,
JetBrains Mono) and
Renovate tracks the pins.
A font-pin PR needs a second commit
Renovate can move the pin but cannot write the binaries, so its PR changes one line and the woff2 files are still the old release. Check out the branch, run make fonts to download what the pin now names, and commit the result. make fonts-check (run by fonts-check.yaml on every such PR) diffs the downloads against what is committed and fails the branch until that commit arrives, which is what holds the automerge.