## 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
3.3 KiB
| title | date-modified | tags | |||
|---|---|---|---|---|---|
| Forgejo | 2026-02-08 |
|
Forgejo
Git forge and CI/CD platform. Primary source of truth for blumeops (mirrored to GitHub).
Quick Reference
| Property | Value |
|---|---|
| URL | https://forge.ops.eblu.me |
| SSH | ssh://forgejo@forge.ops.eblu.me:2222 |
| Local Ports | 3001 (HTTP), 2200 (SSH) |
| Config | ansible/roles/forgejo/templates/app.ini.j2 |
Repositories
| Repo | Description |
|---|---|
eblume/blumeops |
Infrastructure as code (primary) |
eblume/alloy |
Grafana Alloy fork (CGO build) |
eblume/tesla_auth |
Tesla OAuth helper |
| Helm chart mirrors | cloudnative-pg-charts, grafana-helm-charts |
CI/CD (Forgejo Actions)
Runner: Kubernetes pod with Docker-in-Docker sidecar
- Namespace:
forgejo-runner - Labels:
k8s - ArgoCD app:
forgejo-runner
Workflows: .forgejo/workflows/
build-container.yaml- Container image builds on tagbuild-blumeops.yaml- Documentation builds and releases
Secrets (Forgejo Config)
Server configuration secrets managed via 1Password → Ansible:
lfs-jwt-secret,internal-token,oauth2-jwt-secret- Forgejo server tokensrunner_reg- Runner registration token (also in k8s via external-secrets)
Forgejo Actions Secrets
Repository-level secrets for CI/CD workflows, synced from 1Password via Ansible.
| Secret | 1Password Field | Used By | Purpose |
|---|---|---|---|
ARGOCD_AUTH_TOKEN |
argocd_token |
build-blumeops.yaml |
Sync docs app after release |
These secrets are injected as ${{ secrets.SECRET_NAME }} in workflow files.
IaC: The forgejo_actions_secrets Ansible role syncs these secrets from 1Password to Forgejo via the Forgejo API. Run with:
mise run provision-indri -- --tags forgejo_actions_secrets
API Token Setup (Manual, One-Time)
The Ansible role authenticates to the Forgejo API using a Personal Access Token (PAT). This PAT must be created manually:
- Go to https://forge.ops.eblu.me/user/settings/applications
- Create a new token with
write:repositoryscope - Store it in 1Password → "Forgejo Secrets" item →
api-tokenfield
This is a bootstrapping requirement - the PAT enables IaC for all other secrets.
Future: Public Access
Forgejo can be exposed publicly at forge.eblu.me via flyio-proxy. Since Forgejo runs natively on indri (not in k8s), the pattern is:
- Create a k8s ExternalName Service pointing to indri's Tailscale IP
- Create a Tailscale Ingress with
tailscale.com/tags: "tag:k8s,tag:flyio-target" - Add the nginx server block and DNS CNAME
Exposing a dynamic, authenticated service like Forgejo requires a full security review before going live:
- Disable open user registration (require invites or admin approval)
- Configure fail2ban on indri with a filter for Forgejo's log format
- Ensure Forgejo logs the forwarded client IP (
X-Real-IP) rather than the proxy's Tailscale IP - Audit repository visibility defaults and permissions
- Rehearse the break-glass shutoff (
mise run fly-shutoff)
See expose-service-publicly for the full howto and dynamic service checklist.