Adopt Dagger CI for docs build (Phase 2) (#157)

## Summary

Migrates the docs build pipeline to Dagger (Phase 2 of the Dagger CI adoption plan).

- **Backfill `date-modified` frontmatter** on all 80 docs — Dagger's `--src=.` excludes `.git`, so Quartz can't use git history for page dates. Frontmatter dates work with or without git.
- **New `docs-check-frontmatter` mise task + pre-commit hook** — validates all docs have `title`, `tags`, and `date-modified`
- **New Dagger functions** — `build_changelog` (towncrier in Python container) and `build_docs` (chains changelog → Quartz build in Node container, returns tarball)
- **Simplified CI workflow** — the ~44-line inline Quartz build (clone, npm ci, build, tar, cleanup) is replaced by `dagger call build-docs`. Changelog step remains local on the runner since towncrier needs to modify the host working tree for the git commit.

### Design decisions

- **Towncrier runs twice in CI**: once inside Dagger (for the docs tarball) and once on the runner (for the git commit). This is intentional — Dagger's directory export is additive and can't delete the consumed changelog fragments from the host.
- **Artifact hosting stays on Forgejo Releases** (not migrated to Forgejo Packages as the plan doc originally suggested). That migration can happen independently.
- **`date-modified` frontmatter** preserved even though `build_changelog` installs git — the git there is only for towncrier's `git add` call, not for history. The local iteration story (`dagger call build-docs --src=. --version=dev` with uncommitted changes) depends on frontmatter dates.

### Local iteration

```bash
dagger call build-docs --src=. --version=dev export --path=./docs-dev.tar.gz
tar tf docs-dev.tar.gz | head -20
```

## Deployment and Testing

- [x] `dagger call build-docs --src=. --version=dev` produces valid 1.1MB tarball (149 HTML pages)
- [x] Pre-commit hooks pass (including new `docs-check-frontmatter`)
- [ ] Full `workflow_dispatch` run after merge

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/157
This commit is contained in:
Erich Blume 2026-02-11 16:33:16 -08:00
commit b197bd5f58
85 changed files with 272 additions and 55 deletions

View file

@ -1,5 +1,5 @@
import dagger import dagger
from dagger import function, object_type from dagger import dag, function, object_type
@object_type @object_type
@ -22,3 +22,65 @@ class BlumeopsCi:
ctr = self.build(src, container_name) ctr = self.build(src, container_name)
ref = f"{registry}/blumeops/{container_name}:{version}" ref = f"{registry}/blumeops/{container_name}:{version}"
return await ctr.publish(ref) return await ctr.publish(ref)
@function
async def build_changelog(
self, src: dagger.Directory, version: str
) -> dagger.Directory:
"""Run towncrier to build changelog, return modified source tree."""
return await (
dag.container()
.from_("python:3.12-slim")
# git is required because towncrier stages CHANGELOG.md via git add
.with_exec(["apt-get", "update", "-qq"])
.with_exec(["apt-get", "install", "-y", "-qq", "git"])
.with_exec(["pip", "install", "towncrier"])
.with_directory("/workspace", src)
.with_workdir("/workspace")
.with_exec(["git", "init"])
.with_exec(["towncrier", "build", "--version", version, "--yes"])
.directory("/workspace")
)
@function
async def build_docs(self, src: dagger.Directory, version: str) -> dagger.File:
"""Build changelog then Quartz site. Returns docs tarball."""
updated_src = await self.build_changelog(src, version)
return await (
dag.container()
.from_("node:20-slim")
.with_exec(["apt-get", "update", "-qq"])
.with_exec(["apt-get", "install", "-y", "-qq", "git"])
.with_directory("/workspace", updated_src)
.with_workdir("/workspace")
.with_exec(
[
"git",
"clone",
"--depth=1",
"https://github.com/jackyzha0/quartz.git",
"/tmp/quartz",
]
)
.with_exec(
[
"sh",
"-c",
"cp -r /tmp/quartz/quartz /tmp/quartz/package*.json "
"/tmp/quartz/tsconfig.json .",
]
)
.with_exec(["npm", "ci"])
.with_exec(["cp", "docs/quartz.config.ts", "."])
.with_exec(["cp", "docs/quartz.layout.ts", "."])
.with_exec(["cp", "CHANGELOG.md", "docs/"])
.with_exec(["npx", "quartz", "build", "-d", "docs"])
.with_exec(
[
"sh",
"-c",
f"tar -czf /docs-{version}.tar.gz -C public .",
]
)
.file(f"/docs-{version}.tar.gz")
)

View file

@ -106,14 +106,43 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
with: with:
# Need full history for git operations
fetch-depth: 0 fetch-depth: 0
- name: Ensure Dagger CLI
run: |
# Bootstrap: install dagger if not already in the runner image.
# Remove once all runners include dagger (Phase 3).
if ! command -v dagger &>/dev/null; then
echo "Dagger not found, installing..."
curl -fsSL https://dl.dagger.io/dagger/install.sh | DAGGER_VERSION=0.19.11 sh
mv ./bin/dagger /usr/local/bin/dagger && rmdir ./bin
fi
dagger version
- name: Build docs
run: |
VERSION="${{ steps.version.outputs.version }}"
TARBALL="docs-${VERSION}.tar.gz"
echo "Building docs via Dagger..."
# build-docs calls build_changelog internally (towncrier runs inside
# the Dagger container). The host working tree is not modified — only
# the tarball is exported. Towncrier runs a second time on the runner
# in the next step so that CHANGELOG.md and fragment deletion are
# captured in the git commit.
dagger call build-docs --src=. --version="$VERSION" \
export --path="./$TARBALL"
echo "Build complete!"
ls -lh "$TARBALL"
- name: Build changelog - name: Build changelog
id: changelog id: changelog
run: | run: |
VERSION="${{ steps.version.outputs.version }}" VERSION="${{ steps.version.outputs.version }}"
# Run towncrier on the runner (not in Dagger) so that CHANGELOG.md
# updates and fragment deletions appear in the working tree for the
# git commit step. This is intentionally a second towncrier run —
# the first happened inside the Dagger build-docs container above.
# Check if there are any changelog fragments # Check if there are any changelog fragments
FRAGMENTS=$(find docs/changelog.d -name "*.md" -not -name ".gitkeep" 2>/dev/null | wc -l) FRAGMENTS=$(find docs/changelog.d -name "*.md" -not -name ".gitkeep" 2>/dev/null | wc -l)
@ -123,7 +152,6 @@ jobs:
echo "changelog_updated=true" >> "$GITHUB_OUTPUT" echo "changelog_updated=true" >> "$GITHUB_OUTPUT"
# Extract the changelog section for this release to include in release body # Extract the changelog section for this release to include in release body
# The section starts with "## [$VERSION]" and ends before the next "## [" or EOF
RELEASE_NOTES=$(awk -v ver="$VERSION" ' RELEASE_NOTES=$(awk -v ver="$VERSION" '
/^## \[/ { /^## \[/ {
if (found) exit if (found) exit
@ -132,7 +160,6 @@ jobs:
found {print} found {print}
' CHANGELOG.md | tail -n +2) ' CHANGELOG.md | tail -n +2)
# Save release notes to a file for later use (handles multiline content)
echo "$RELEASE_NOTES" > /tmp/release_notes.md echo "$RELEASE_NOTES" > /tmp/release_notes.md
echo "Release notes extracted for $VERSION" echo "Release notes extracted for $VERSION"
else else
@ -141,51 +168,6 @@ jobs:
echo "" > /tmp/release_notes.md echo "" > /tmp/release_notes.md
fi fi
- name: Build docs
run: |
VERSION="${{ steps.version.outputs.version }}"
echo "Node version: $(node --version)"
echo "NPM version: $(npm --version)"
# Clone Quartz to temp location
git clone --depth 1 https://github.com/jackyzha0/quartz.git /tmp/quartz
# Copy Quartz build system into blumeops workspace
# This allows building from within the repo so git can find file history
cp -r /tmp/quartz/quartz "$GITHUB_WORKSPACE/"
cp /tmp/quartz/package.json "$GITHUB_WORKSPACE/"
cp /tmp/quartz/package-lock.json "$GITHUB_WORKSPACE/"
cp /tmp/quartz/tsconfig.json "$GITHUB_WORKSPACE/"
cd "$GITHUB_WORKSPACE"
# Install dependencies
npm ci
# Copy our configuration to workspace root
cp docs/quartz.config.ts .
cp docs/quartz.layout.ts .
# Copy CHANGELOG.md into docs so it's part of the content
cp CHANGELOG.md docs/
# Build using -d docs so git can find file history at correct paths
echo "Building static site..."
npx quartz build -d docs
# Create tarball
TARBALL="docs-${VERSION}.tar.gz"
echo "Creating tarball: $TARBALL"
tar -czf "$TARBALL" -C public .
echo "Build complete!"
ls -lh "$TARBALL"
# Clean up Quartz build artifacts (keep tarball)
rm -rf quartz public node_modules
rm -f package.json package-lock.json tsconfig.json quartz.config.ts quartz.layout.ts
rm -f docs/CHANGELOG.md # Remove copied file
- name: Create release - name: Create release
env: env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

View file

@ -110,3 +110,9 @@ repos:
language: system language: system
files: ^docs/.*\.md$ files: ^docs/.*\.md$
pass_filenames: false pass_filenames: false
- id: docs-check-frontmatter
name: docs-check-frontmatter
entry: mise run docs-check-frontmatter
language: system
files: ^docs/.*\.md$
pass_filenames: false

View file

@ -0,0 +1 @@
Migrate docs build pipeline to Dagger (Phase 2): `dagger call build-docs --src=. --version=dev` now runs the full Quartz build locally, identically to CI. Adds `date-modified` frontmatter to all docs and a `docs-check-frontmatter` pre-commit hook.

View file

@ -1,5 +1,6 @@
--- ---
title: Architecture title: Architecture
date-modified: 2026-02-09
last-reviewed: 2026-02-09 last-reviewed: 2026-02-09
tags: tags:
- explanation - explanation

View file

@ -1,5 +1,6 @@
--- ---
title: Explanation title: Explanation
date-modified: 2026-02-10
last-reviewed: 2026-02-10 last-reviewed: 2026-02-10
tags: tags:
- explanation - explanation

View file

@ -1,5 +1,6 @@
--- ---
title: Security Model title: Security Model
date-modified: 2026-02-11
last-reviewed: 2026-02-11 last-reviewed: 2026-02-11
tags: tags:
- explanation - explanation

View file

@ -1,5 +1,6 @@
--- ---
title: Why GitOps title: Why GitOps
date-modified: 2026-02-07
tags: tags:
- explanation - explanation
- philosophy - philosophy

View file

@ -1,5 +1,6 @@
--- ---
title: Add Ansible Role title: Add Ansible Role
date-modified: 2026-02-07
tags: tags:
- how-to - how-to
- ansible - ansible

View file

@ -1,5 +1,6 @@
--- ---
title: Deploy K8s Service title: Deploy K8s Service
date-modified: 2026-02-07
tags: tags:
- how-to - how-to
- kubernetes - kubernetes

View file

@ -1,5 +1,6 @@
--- ---
title: Expose a Service Publicly title: Expose a Service Publicly
date-modified: 2026-02-08
tags: tags:
- how-to - how-to
- fly-io - fly-io

View file

@ -1,5 +1,6 @@
--- ---
title: Gandi Operations title: Gandi Operations
date-modified: 2026-02-08
tags: tags:
- how-to - how-to
- dns - dns

View file

@ -1,5 +1,6 @@
--- ---
title: How-To title: How-To
date-modified: 2026-02-11
tags: tags:
- how-to - how-to
--- ---

View file

@ -1,5 +1,6 @@
--- ---
title: Review Documentation title: Review Documentation
date-modified: 2026-02-09
tags: tags:
- how-to - how-to
- documentation - documentation

View file

@ -1,5 +1,6 @@
--- ---
title: Manage Fly.io Proxy title: Manage Fly.io Proxy
date-modified: 2026-02-08
tags: tags:
- how-to - how-to
- fly-io - fly-io

View file

@ -1,5 +1,6 @@
--- ---
title: "Plan: Add UniFi Pulumi Stack" title: "Plan: Add UniFi Pulumi Stack"
date-modified: 2026-02-11
tags: tags:
- how-to - how-to
- plans - plans

View file

@ -1,5 +1,6 @@
--- ---
title: "Plan: Adopt Dagger as CI/CD Build Engine" title: "Plan: Adopt Dagger as CI/CD Build Engine"
date-modified: 2026-02-11
tags: tags:
- how-to - how-to
- plans - plans
@ -9,7 +10,7 @@ tags:
# Plan: Adopt Dagger as CI/CD Build Engine # Plan: Adopt Dagger as CI/CD Build Engine
> **Status:** Phase 1 implemented > **Status:** Phase 2 implemented
## Background ## Background
@ -496,11 +497,11 @@ BuildKit caches aggressively, making repeated builds fast. Since the Forgejo run
- [x] Existing `mise run container-tag-and-release` workflow still works end-to-end - [x] Existing `mise run container-tag-and-release` workflow still works end-to-end
### Phase 2 (Docs) ### Phase 2 (Docs)
- [ ] `dagger call build-docs --src=. --version=dev` produces valid tarball locally - [x] `dagger call build-docs --src=. --version=dev` produces valid tarball locally
- [ ] Tarball contents match current Quartz build output - [x] Tarball contents match current Quartz build output
- [ ] `dagger call release-docs` uploads to Forgejo packages successfully - [ ] ~~`dagger call release-docs` uploads to Forgejo packages successfully~~ (deferred — artifact hosting stays on Forgejo Releases)
- [ ] Quartz container starts and serves docs from Forgejo packages URL - [ ] ~~Quartz container starts and serves docs from Forgejo packages URL~~ (deferred)
- [ ] ArgoCD sync works from within Dagger - [ ] ~~ArgoCD sync works from within Dagger~~ (deferred)
- [ ] Forgejo Actions workflow_dispatch completes full release cycle - [ ] Forgejo Actions workflow_dispatch completes full release cycle
- [ ] CHANGELOG.md and fragment cleanup committed correctly - [ ] CHANGELOG.md and fragment cleanup committed correctly

View file

@ -1,5 +1,6 @@
--- ---
title: "Plan: Adopt OIDC Identity Provider" title: "Plan: Adopt OIDC Identity Provider"
date-modified: 2026-02-11
tags: tags:
- how-to - how-to
- plans - plans

View file

@ -1,5 +1,6 @@
--- ---
title: "Plan: Forgejo Actions Dashboard" title: "Plan: Forgejo Actions Dashboard"
date-modified: 2026-02-11
tags: tags:
- how-to - how-to
- plans - plans

View file

@ -1,5 +1,6 @@
--- ---
title: "Plan: Harden Zot Registry" title: "Plan: Harden Zot Registry"
date-modified: 2026-02-11
tags: tags:
- how-to - how-to
- plans - plans

View file

@ -1,5 +1,6 @@
--- ---
title: "Plan: Migrate Forgejo from Brew to Source Build" title: "Plan: Migrate Forgejo from Brew to Source Build"
date-modified: 2026-02-10
tags: tags:
- how-to - how-to
- plans - plans

View file

@ -1,5 +1,6 @@
--- ---
title: "Plan: Operationalize ReoLink Camera" title: "Plan: Operationalize ReoLink Camera"
date-modified: 2026-02-11
tags: tags:
- how-to - how-to
- plans - plans

View file

@ -1,5 +1,6 @@
--- ---
title: Plans title: Plans
date-modified: 2026-02-11
tags: tags:
- how-to - how-to
- plans - plans

View file

@ -1,5 +1,6 @@
--- ---
title: "Plan: Upstream Fork Strategy" title: "Plan: Upstream Fork Strategy"
date-modified: 2026-02-11
tags: tags:
- how-to - how-to
- plans - plans

View file

@ -1,5 +1,6 @@
--- ---
title: Restart Indri title: Restart Indri
date-modified: 2026-02-10
tags: tags:
- how-to - how-to
- operations - operations

View file

@ -1,5 +1,6 @@
--- ---
title: Restore 1Password Backup title: Restore 1Password Backup
date-modified: 2026-02-10
tags: tags:
- how-to - how-to
- operations - operations

View file

@ -1,5 +1,6 @@
--- ---
title: Troubleshooting title: Troubleshooting
date-modified: 2026-02-07
tags: tags:
- how-to - how-to
- operations - operations

View file

@ -1,5 +1,6 @@
--- ---
title: Update Documentation title: Update Documentation
date-modified: 2026-02-08
tags: tags:
- how-to - how-to
- documentation - documentation

View file

@ -1,5 +1,6 @@
--- ---
title: Update Tailscale ACLs title: Update Tailscale ACLs
date-modified: 2026-02-07
tags: tags:
- how-to - how-to
- tailscale - tailscale

View file

@ -1,5 +1,6 @@
--- ---
title: Use PyPI Proxy title: Use PyPI Proxy
date-modified: 2026-02-07
tags: tags:
- how-to - how-to
- python - python

View file

@ -1,5 +1,6 @@
--- ---
title: BlumeOps title: BlumeOps
date-modified: 2026-02-08
aliases: [] aliases: []
id: index id: index
tags: [] tags: []

View file

@ -1,5 +1,6 @@
--- ---
title: Roles title: Roles
date-modified: 2026-02-07
tags: tags:
- ansible - ansible
- reference - reference

View file

@ -1,5 +1,6 @@
--- ---
title: Gandi title: Gandi
date-modified: 2026-02-08
tags: tags:
- infrastructure - infrastructure
- networking - networking

View file

@ -1,5 +1,6 @@
--- ---
title: Gilbert title: Gilbert
date-modified: 2026-02-07
tags: tags:
- infrastructure - infrastructure
- host - host

View file

@ -1,5 +1,6 @@
--- ---
title: Hosts title: Hosts
date-modified: 2026-02-10
tags: tags:
- infrastructure - infrastructure
--- ---

View file

@ -1,5 +1,6 @@
--- ---
title: Indri title: Indri
date-modified: 2026-02-09
tags: tags:
- infrastructure - infrastructure
- host - host

View file

@ -1,5 +1,6 @@
--- ---
title: Power title: Power
date-modified: 2026-02-09
tags: tags:
- infrastructure - infrastructure
--- ---

View file

@ -1,5 +1,6 @@
--- ---
title: Routing title: Routing
date-modified: 2026-02-09
tags: tags:
- infrastructure - infrastructure
- networking - networking

View file

@ -1,5 +1,6 @@
--- ---
title: Tailscale title: Tailscale
date-modified: 2026-02-08
tags: tags:
- infrastructure - infrastructure
- networking - networking

View file

@ -1,5 +1,6 @@
--- ---
title: UniFi title: UniFi
date-modified: 2026-02-10
tags: tags:
- infrastructure - infrastructure
- networking - networking

View file

@ -1,5 +1,6 @@
--- ---
title: Apps title: Apps
date-modified: 2026-02-07
tags: tags:
- kubernetes - kubernetes
- argocd - argocd

View file

@ -1,5 +1,6 @@
--- ---
title: Cluster title: Cluster
date-modified: 2026-02-07
tags: tags:
- kubernetes - kubernetes
--- ---

View file

@ -1,5 +1,6 @@
--- ---
title: External Secrets title: External Secrets
date-modified: 2026-02-07
tags: tags:
- kubernetes - kubernetes
- secrets - secrets

View file

@ -1,5 +1,6 @@
--- ---
title: Tailscale Operator title: Tailscale Operator
date-modified: 2026-02-08
tags: tags:
- kubernetes - kubernetes
- tailscale - tailscale

View file

@ -1,5 +1,6 @@
--- ---
title: Backup title: Backup
date-modified: 2026-02-07
tags: tags:
- operations - operations
--- ---

View file

@ -1,5 +1,6 @@
--- ---
title: Disaster Recovery title: Disaster Recovery
date-modified: 2026-02-10
tags: tags:
- operations - operations
--- ---

View file

@ -1,5 +1,6 @@
--- ---
title: Observability title: Observability
date-modified: 2026-02-07
tags: tags:
- operations - operations
--- ---

View file

@ -1,5 +1,6 @@
--- ---
title: Reference title: Reference
date-modified: 2026-02-10
tags: tags:
- reference - reference
--- ---

View file

@ -1,5 +1,6 @@
--- ---
title: 1Password title: 1Password
date-modified: 2026-02-10
tags: tags:
- service - service
- secrets - secrets

View file

@ -1,5 +1,6 @@
--- ---
title: Alloy title: Alloy
date-modified: 2026-02-08
tags: tags:
- service - service
- observability - observability

View file

@ -1,5 +1,6 @@
--- ---
title: ArgoCD title: ArgoCD
date-modified: 2026-02-07
tags: tags:
- service - service
- gitops - gitops

View file

@ -1,5 +1,6 @@
--- ---
title: Automounter title: Automounter
date-modified: 2026-02-07
tags: tags:
- services - services
- macos - macos

View file

@ -1,5 +1,6 @@
--- ---
title: Borgmatic title: Borgmatic
date-modified: 2026-02-10
tags: tags:
- service - service
- backup - backup

View file

@ -1,5 +1,6 @@
--- ---
title: Caddy title: Caddy
date-modified: 2026-02-08
tags: tags:
- service - service
- networking - networking

View file

@ -1,5 +1,6 @@
--- ---
title: Devpi title: Devpi
date-modified: 2026-02-07
tags: tags:
- service - service
- python - python

View file

@ -1,5 +1,6 @@
--- ---
title: Docs title: Docs
date-modified: 2026-02-08
tags: tags:
- service - service
- documentation - documentation

View file

@ -1,5 +1,6 @@
--- ---
title: Fly.io Proxy title: Fly.io Proxy
date-modified: 2026-02-08
tags: tags:
- service - service
- networking - networking

View file

@ -1,5 +1,6 @@
--- ---
title: Forgejo title: Forgejo
date-modified: 2026-02-08
tags: tags:
- service - service
- git - git

View file

@ -1,5 +1,6 @@
--- ---
title: Grafana title: Grafana
date-modified: 2026-02-08
tags: tags:
- service - service
- observability - observability

View file

@ -1,5 +1,6 @@
--- ---
title: Immich title: Immich
date-modified: 2026-02-07
tags: tags:
- service - service
- media - media

View file

@ -1,5 +1,6 @@
--- ---
title: Jellyfin title: Jellyfin
date-modified: 2026-02-07
tags: tags:
- service - service
- media - media

View file

@ -1,5 +1,6 @@
--- ---
title: Kiwix title: Kiwix
date-modified: 2026-02-07
tags: tags:
- service - service
- knowledge - knowledge

View file

@ -1,5 +1,6 @@
--- ---
title: Loki title: Loki
date-modified: 2026-02-08
tags: tags:
- service - service
- observability - observability

View file

@ -1,5 +1,6 @@
--- ---
title: Miniflux title: Miniflux
date-modified: 2026-02-07
tags: tags:
- service - service
- rss - rss

View file

@ -1,5 +1,6 @@
--- ---
title: Navidrome title: Navidrome
date-modified: 2026-02-07
tags: tags:
- service - service
- media - media

View file

@ -1,5 +1,6 @@
--- ---
title: PostgreSQL title: PostgreSQL
date-modified: 2026-02-07
tags: tags:
- service - service
- database - database

View file

@ -1,5 +1,6 @@
--- ---
title: Prometheus title: Prometheus
date-modified: 2026-02-08
tags: tags:
- service - service
- observability - observability

View file

@ -1,5 +1,6 @@
--- ---
title: TeslaMate title: TeslaMate
date-modified: 2026-02-07
tags: tags:
- service - service
- vehicle - vehicle

View file

@ -1,5 +1,6 @@
--- ---
title: Transmission title: Transmission
date-modified: 2026-02-07
tags: tags:
- service - service
- torrent - torrent

View file

@ -1,5 +1,6 @@
--- ---
title: Zot title: Zot
date-modified: 2026-02-07
tags: tags:
- service - service
- registry - registry

View file

@ -1,5 +1,6 @@
--- ---
title: Backups title: Backups
date-modified: 2026-02-10
tags: tags:
- storage - storage
- backup - backup

View file

@ -1,5 +1,6 @@
--- ---
title: PostgreSQL Storage title: PostgreSQL Storage
date-modified: 2026-02-07
tags: tags:
- storage - storage
- database - database

View file

@ -1,5 +1,6 @@
--- ---
title: Sifaka title: Sifaka
date-modified: 2026-02-09
tags: tags:
- storage - storage
--- ---

View file

@ -1,5 +1,6 @@
--- ---
title: Adding a Service title: Adding a Service
date-modified: 2026-02-07
tags: tags:
- tutorials - tutorials
- argocd - argocd

View file

@ -1,5 +1,6 @@
--- ---
title: AI Assistance Guide title: AI Assistance Guide
date-modified: 2026-02-09
tags: tags:
- tutorials - tutorials
- ai - ai

View file

@ -1,5 +1,6 @@
--- ---
title: Contributing title: Contributing
date-modified: 2026-02-07
tags: tags:
- tutorials - tutorials
- contributing - contributing

View file

@ -1,5 +1,6 @@
--- ---
title: Exploring the Docs title: Exploring the Docs
date-modified: 2026-02-10
tags: tags:
- tutorials - tutorials
- getting-started - getting-started

View file

@ -1,5 +1,6 @@
--- ---
title: Replicating BlumeOps title: Replicating BlumeOps
date-modified: 2026-02-07
tags: tags:
- tutorials - tutorials
- replication - replication

View file

@ -1,5 +1,6 @@
--- ---
title: ArgoCD Config title: ArgoCD Config
date-modified: 2026-02-07
tags: tags:
- tutorials - tutorials
- replication - replication

View file

@ -1,5 +1,6 @@
--- ---
title: Core Services title: Core Services
date-modified: 2026-02-07
tags: tags:
- tutorials - tutorials
- replication - replication

View file

@ -1,5 +1,6 @@
--- ---
title: Kubernetes Bootstrap title: Kubernetes Bootstrap
date-modified: 2026-02-07
tags: tags:
- tutorials - tutorials
- replication - replication

View file

@ -1,5 +1,6 @@
--- ---
title: Observability Stack title: Observability Stack
date-modified: 2026-02-07
tags: tags:
- tutorials - tutorials
- replication - replication

View file

@ -1,5 +1,6 @@
--- ---
title: Tailscale Setup title: Tailscale Setup
date-modified: 2026-02-07
tags: tags:
- tutorials - tutorials
- replication - replication

View file

@ -1,5 +1,6 @@
--- ---
title: Tutorials title: Tutorials
date-modified: 2026-02-07
tags: tags:
- tutorials - tutorials
--- ---

View file

@ -0,0 +1,86 @@
#!/usr/bin/env -S uv run --script
# /// script
# requires-python = ">=3.12"
# dependencies = ["rich>=13.0.0"]
# ///
#MISE description="Check that all docs have required frontmatter fields"
"""Validate that all documentation files have required YAML frontmatter.
Required fields: title, tags, date-modified
Scans all markdown files in docs/ (excluding changelog.d/) and checks
that each file has YAML frontmatter containing the required fields.
Usage: mise run docs-check-frontmatter
"""
import re
import sys
from pathlib import Path
from rich.console import Console
from rich.table import Table
DOCS_DIR = Path(__file__).parent.parent / "docs"
REQUIRED_FIELDS = {"title", "tags", "date-modified"}
# Match YAML frontmatter block
FRONTMATTER_PATTERN = re.compile(r"^---\n(.*?)\n---\n", re.DOTALL)
# Match top-level YAML keys (not indented)
KEY_PATTERN = re.compile(r"^([a-zA-Z][a-zA-Z0-9_-]*):", re.MULTILINE)
def extract_frontmatter_keys(file_path: Path) -> set[str] | None:
"""Extract top-level keys from YAML frontmatter. Returns None if no frontmatter."""
content = file_path.read_text()
match = FRONTMATTER_PATTERN.match(content)
if not match:
return None
frontmatter = match.group(1)
return set(KEY_PATTERN.findall(frontmatter))
def main() -> int:
console = Console()
console.print("[bold]Frontmatter Validation[/bold]")
console.print(f"Required fields: {', '.join(sorted(REQUIRED_FIELDS))}")
console.print()
issues: list[tuple[str, set[str]]] = []
for md_file in sorted(DOCS_DIR.rglob("*.md")):
if "changelog.d" in md_file.parts:
continue
rel_path = str(md_file.relative_to(DOCS_DIR))
keys = extract_frontmatter_keys(md_file)
if keys is None:
issues.append((rel_path, REQUIRED_FIELDS))
continue
missing = REQUIRED_FIELDS - keys
if missing:
issues.append((rel_path, missing))
if issues:
console.print("[bold red]Missing Required Frontmatter[/bold red]")
console.print()
table = Table(show_header=True, header_style="bold")
table.add_column("File")
table.add_column("Missing Fields")
for rel_path, missing in issues:
table.add_row(rel_path, ", ".join(sorted(missing)))
console.print(table)
console.print()
return 1
console.print("[bold green]All docs have required frontmatter![/bold green]")
return 0
if __name__ == "__main__":
sys.exit(main())