Retire plans directory, convert migrate-forgejo-from-brew to mikado card
The plans/ directory predated the mikado method approach. Deleted all completed and abandoned plans, converted the still-relevant migrate-forgejo-from-brew into a lean mikado chain root card under how-to/forgejo/, cleaned up dangling wiki-links across docs, and fixed a stale "pre-commit" reference to "prek". Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
e90c287504
commit
55a846eb25
18 changed files with 56 additions and 1693 deletions
1
docs/changelog.d/+retire-plans-directory.doc.md
Normal file
1
docs/changelog.d/+retire-plans-directory.doc.md
Normal file
|
|
@ -0,0 +1 @@
|
|||
Retire docs plans directory: deleted completed/abandoned plans, converted migrate-forgejo-from-brew to a mikado chain root card, removed plans references from tutorials and how-to index.
|
||||
|
|
@ -51,6 +51,5 @@ Replace Dex with [Authentik](https://goauthentik.io/) as the SSO identity provid
|
|||
|
||||
- [[authentik]] — OIDC identity provider
|
||||
- [[federated-login]] — How authentication works across BlumeOps
|
||||
- [[adopt-oidc-provider]] — Dex deployment plan (completed)
|
||||
- [[ringtail]] — Target cluster
|
||||
- [[agent-change-process]] — C2 methodology used for this change
|
||||
|
|
|
|||
|
|
@ -144,5 +144,4 @@ Trigger a manual sync on one mirror to confirm the new PAT works:
|
|||
## Related
|
||||
|
||||
- [[forgejo]] — Forgejo service reference
|
||||
- [[upstream-fork-strategy]] — Stacked-branch forking for repos with local modifications
|
||||
- [[gandi-operations]] — Similar PAT rotation workflow for Gandi DNS
|
||||
|
|
|
|||
46
docs/how-to/forgejo/migrate-forgejo-from-brew.md
Normal file
46
docs/how-to/forgejo/migrate-forgejo-from-brew.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
---
|
||||
title: Migrate Forgejo from Brew to Source Build
|
||||
status: active
|
||||
modified: 2026-03-04
|
||||
tags:
|
||||
- how-to
|
||||
- forgejo
|
||||
---
|
||||
|
||||
# Migrate Forgejo from Brew to Source Build
|
||||
|
||||
Transition Forgejo on indri from Homebrew to a source-built binary with LaunchAgent, matching the pattern used by [[zot]], [[caddy]], and [[alloy]].
|
||||
|
||||
## Motivation
|
||||
|
||||
Forgejo was force-upgraded from v13 to v14 by `brew upgrade`, breaking version control. A source build pins versions and aligns with the established native service pattern.
|
||||
|
||||
## Architecture Decisions
|
||||
|
||||
| Decision | Choice | Rationale |
|
||||
|----------|--------|-----------|
|
||||
| **Source remote** | Codeberg upstream | Avoids circular dependency (Forgejo hosting its own source) |
|
||||
| **Secondary remote** | `forge.eblu.me/mirrors/forgejo` | Convenience and backup |
|
||||
| **Version tracking** | `indri-deployment` branch on tag | Rebase to upgrade; explicit version pinning |
|
||||
| **Build deps** | Go 1.24+, Node 20+ via mise | Consistent with other mise-managed tooling |
|
||||
| **Process manager** | LaunchAgent plist | Matches zot, caddy, alloy |
|
||||
| **Data location** | `~/forgejo` | Migrated from `/opt/homebrew/var/forgejo` |
|
||||
| **Run user** | `erichblume` | LaunchAgent session user (SSH git user stays `forgejo`) |
|
||||
|
||||
## Key Steps
|
||||
|
||||
1. Clone from Codeberg, add forge mirror remote
|
||||
2. Check out target tag, create `indri-deployment` branch
|
||||
3. Build with `TAGS="bindata timedzdata sqlite sqlite_unlock_notify" mise x -- make build`
|
||||
4. Stop brew service, copy data to `~/forgejo`, fix ownership
|
||||
5. Run Ansible (`--tags forgejo`) to deploy updated role with LaunchAgent
|
||||
6. Verify (API version, SSH clone, push, Actions runners, services-check)
|
||||
7. `brew uninstall forgejo`
|
||||
|
||||
## Reference Patterns
|
||||
|
||||
- `ansible/roles/zot/` — primary pattern for source-built binary roles (tasks, defaults, handlers, plist template)
|
||||
|
||||
## Related
|
||||
|
||||
- [[forgejo]] — Service reference
|
||||
|
|
@ -48,22 +48,11 @@ Task-oriented instructions for common BlumeOps operations. These guides assume y
|
|||
| [[restore-1password-backup]] | Recover 1Password credentials from borgmatic backup |
|
||||
| [[troubleshooting]] | Diagnose and fix common issues |
|
||||
|
||||
## Plans
|
||||
## Forgejo
|
||||
|
||||
Migration and transition plans for upcoming infrastructure changes.
|
||||
Mikado chain for migrating Forgejo from Homebrew to source-built binary. Track progress with `mise run docs-mikado migrate-forgejo-from-brew`.
|
||||
|
||||
| Plan | Description |
|
||||
|------|-------------|
|
||||
| [[plans]] | Index of all plans |
|
||||
| [[completed]] | Completed plans archive |
|
||||
| [[migrate-forgejo-from-brew]] | Transition Forgejo from Homebrew to source-built binary |
|
||||
| [[add-unifi-pulumi-stack]] | Add Pulumi IaC for UniFi Express 7 (abandoned) |
|
||||
| [[segment-home-network]] | Manual three-network segmentation for UniFi Express 7 |
|
||||
| [[adopt-dagger-ci]] | Adopt Dagger as CI/CD build engine |
|
||||
| [[upstream-fork-strategy]] | Stacked-branch forking strategy for upstream projects |
|
||||
| [[adopt-oidc-provider]] | Deploy OIDC identity provider for SSO across services |
|
||||
| [[upgrade-grafana]] | Upgrade Grafana to 12.x with kustomize and home-built container |
|
||||
| [[operationalize-reolink-camera]] | Cloud-free NVR with Frigate and ring buffer recording |
|
||||
- [[migrate-forgejo-from-brew]]
|
||||
|
||||
## Ringtail
|
||||
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
---
|
||||
title: "Plan: Add UniFi Pulumi Stack"
|
||||
modified: 2026-02-14
|
||||
tags:
|
||||
- how-to
|
||||
- plans
|
||||
- networking
|
||||
- pulumi
|
||||
---
|
||||
|
||||
# Plan: Add UniFi Pulumi Stack
|
||||
|
||||
> **Status:** Abandoned
|
||||
> **Superseded by:** [[segment-home-network]]
|
||||
|
||||
## Why Abandoned
|
||||
|
||||
Attempted Feb 2026 with the `ubiquiti-community/unifi` Terraform provider via `pulumi package add`. Two issues made the approach unviable:
|
||||
|
||||
1. **API key auth skips UniFi OS auto-detection** ([provider bug #74](https://github.com/ubiquiti-community/terraform-provider-unifi/issues/74)) — requires username/password instead, which is unsuitable for IaC
|
||||
2. **"No-op" update on the default LAN network reset undeclared properties** — bricked the network, requiring a factory reset and backup restore
|
||||
|
||||
The provider ecosystem (ubiquiti-community, filipowm, pulumiverse) is too immature for critical single-device infrastructure like the home router. A provider bug that causes a network outage on a no-op update is an unacceptable risk.
|
||||
|
||||
## What Survives
|
||||
|
||||
The network segmentation goals from this plan remain valid and are carried forward in [[segment-home-network]], which describes how to configure three-network segmentation manually through the UX7 web UI.
|
||||
|
||||
## Related
|
||||
|
||||
- [[segment-home-network]] — Manual segmentation plan (replacement)
|
||||
- [[unifi]] — Reference card
|
||||
|
|
@ -1,540 +0,0 @@
|
|||
---
|
||||
title: "Plan: Adopt Dagger as CI/CD Build Engine"
|
||||
modified: 2026-02-11
|
||||
tags:
|
||||
- how-to
|
||||
- plans
|
||||
- ci-cd
|
||||
- dagger
|
||||
---
|
||||
|
||||
# Plan: Adopt Dagger as CI/CD Build Engine
|
||||
|
||||
> **Status:** Phases 1–3 complete
|
||||
|
||||
## Background
|
||||
|
||||
BlumeOps CI/CD currently runs on Forgejo Actions (GitHub Actions-compatible). While functional, the system has pain points that are inherent to the GHA ecosystem:
|
||||
|
||||
- **Hard to debug** — logs are buried in a web UI, no way to SSH into a running job, no interactive debugging
|
||||
- **No local iteration** — the only way to test a workflow change is to push and wait for CI
|
||||
- **Supply chain risk** — community actions are opaque third-party code running in your infrastructure
|
||||
- **Runner complexity** — the k8s-runner image must bundle every tool any workflow might need (Docker CLI, buildx, skopeo, Node.js, etc.)
|
||||
- **YAML as programming language** — complex workflows become unreadable
|
||||
|
||||
### Why Dagger?
|
||||
|
||||
[Dagger](https://dagger.io/) is an open-source (Apache-2.0) build engine built on BuildKit. It addresses every pain point above:
|
||||
|
||||
| Pain point | Dagger solution |
|
||||
|------------|-----------------|
|
||||
| Can't debug builds | `--interactive` drops you into a shell at the failure point; `.terminal()` adds breakpoints |
|
||||
| Can't run locally | `dagger call` runs identically on your laptop and in CI — same code path |
|
||||
| Supply chain risk | Build logic is your own Python code, not third-party actions |
|
||||
| Runner bloat | Runner only needs Docker + `dagger` CLI; all tools live inside Dagger containers |
|
||||
| YAML complexity | Pipelines are real Python (classes, decorators, async/await) — not templated YAML |
|
||||
|
||||
### What Dagger is NOT
|
||||
|
||||
Dagger is a **build engine**, not a CI scheduler. It does not handle triggers, scheduling, or webhooks. We keep Forgejo Actions as a thin trigger layer — its YAML becomes trivially simple (install dagger, run `dagger call`). All actual build logic moves to Python.
|
||||
|
||||
### Alternatives Considered
|
||||
|
||||
| System | Verdict | Reason |
|
||||
|--------|---------|--------|
|
||||
| **BuildKite** | Rejected | No fully self-hosted option (cloud control plane required); no native Forgejo integration; adds external dependency for a homelab |
|
||||
| **Concourse CI** | Rejected | Fully self-hosted and great debugging (`fly intercept`), but verbose YAML with no built-in templating; small community; 2-4GB RAM overhead for the scheduler; doesn't solve local iteration as cleanly |
|
||||
| **Earthly** | Not viable | Project discontinued April 2025, all cloud services shut down July 2025 |
|
||||
|
||||
Dagger was chosen because it delivers the best local iteration story, supports Python natively, and requires zero infrastructure beyond what we already have (Docker on the runner).
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────┐ ┌──────────────────┐
|
||||
│ Forgejo Actions │ │ Your terminal │
|
||||
│ (trigger layer) │ │ (local dev) │
|
||||
│ │ │ │
|
||||
│ on: push tags │ │ mise run ... │
|
||||
│ → dagger call ... │ │ → dagger call .. │
|
||||
└──────────┬───────────┘ └────────┬──────────┘
|
||||
│ │
|
||||
▼ ▼
|
||||
┌──────────────────────────────────────┐
|
||||
│ Dagger Engine (BuildKit) │
|
||||
│ │
|
||||
│ blumeops-ci Python module │
|
||||
│ ├── build(container_name) │
|
||||
│ ├── publish(container_name, version)│
|
||||
│ ├── build_docs(version) │
|
||||
│ ├── release_docs(version, tokens) │
|
||||
│ └── validate() │
|
||||
└──────────────┬───────────────────────┘
|
||||
│
|
||||
┌────────┼────────┐
|
||||
▼ ▼ ▼
|
||||
┌─────┐ ┌──────┐ ┌───────┐
|
||||
│ Zot │ │Forgejo│ │ArgoCD │
|
||||
│ │ │Pkgs │ │ │
|
||||
└─────┘ └──────┘ └───────┘
|
||||
```
|
||||
|
||||
**Key principle:** The same `dagger call` command runs on your Mac during development and in the Forgejo runner during CI. The Forgejo Actions YAML is a thin shim that parses the trigger event and calls Dagger.
|
||||
|
||||
## Dagger Module Structure
|
||||
|
||||
```
|
||||
dagger/
|
||||
├── dagger.json # Module metadata, SDK selection
|
||||
├── pyproject.toml # Python deps (httpx, etc.)
|
||||
├── uv.lock # Locked dependencies
|
||||
└── src/blumeops_ci/
|
||||
└── __init__.py # All build functions
|
||||
```
|
||||
|
||||
## Secrets Handling
|
||||
|
||||
Dagger has a first-class `Secret` type — values are never logged, cached, or visible in traces.
|
||||
|
||||
**From CLI:**
|
||||
```bash
|
||||
dagger call release-docs \
|
||||
--src=. --version=v1.6.0 \
|
||||
--forgejo-token=env:FORGEJO_TOKEN \
|
||||
--argocd-token=env:ARGOCD_TOKEN
|
||||
```
|
||||
|
||||
The `env:VARIABLE` syntax reads from environment variables. In Forgejo Actions, secrets are injected as env vars. Locally, a mise task calls `op read` to populate them.
|
||||
|
||||
**In Python code:**
|
||||
```python
|
||||
@function
|
||||
async def release_docs(
|
||||
self,
|
||||
src: dagger.Directory,
|
||||
version: str,
|
||||
forgejo_token: dagger.Secret,
|
||||
argocd_token: dagger.Secret,
|
||||
) -> str:
|
||||
# Token is mounted securely, never exposed in logs
|
||||
token = await forgejo_token.plaintext()
|
||||
```
|
||||
|
||||
**Rule of thumb:** Simple API calls (Forgejo package upload) use Python `httpx` directly in the module runtime. CLI tools without good Python libraries (ArgoCD) run in container steps with secrets mounted as env vars via `.with_secret_variable()`.
|
||||
|
||||
## Phase 1: Container Builds
|
||||
|
||||
Migrate `build-container.yaml` to use Dagger for the build/push logic.
|
||||
|
||||
### Dagger Functions
|
||||
|
||||
```python
|
||||
@function
|
||||
def build(self, src: dagger.Directory, container_name: str) -> dagger.Container:
|
||||
"""Build a container from containers/<name>/Dockerfile."""
|
||||
context = src.directory(f"containers/{container_name}")
|
||||
return dag.container().build(context)
|
||||
|
||||
@function
|
||||
async def publish(
|
||||
self,
|
||||
src: dagger.Directory,
|
||||
container_name: str,
|
||||
version: str,
|
||||
registry: str = "registry.ops.eblu.me",
|
||||
) -> str:
|
||||
"""Build and push to zot registry."""
|
||||
ctr = self.build(src, container_name)
|
||||
ref = f"{registry}/blumeops/{container_name}:{version}"
|
||||
return await ctr.publish(ref)
|
||||
```
|
||||
|
||||
### Local Iteration Workflow
|
||||
|
||||
```bash
|
||||
# Build — validates Dockerfile, fast cached feedback
|
||||
dagger call build --src=. --container-name=devpi
|
||||
|
||||
# Build and drop into a shell to inspect the container
|
||||
dagger call build --src=. --container-name=devpi terminal
|
||||
|
||||
# Debug a failure interactively
|
||||
dagger call --interactive build --src=. --container-name=devpi
|
||||
|
||||
# Push a dev tag for testing in k8s (ArgoCD ignores it)
|
||||
dagger call publish --src=. --container-name=devpi --version=dev
|
||||
|
||||
# Publish the real version
|
||||
dagger call publish --src=. --container-name=devpi --version=v1.1.0
|
||||
```
|
||||
|
||||
### Forgejo Actions Integration
|
||||
|
||||
The existing tag-based trigger model is preserved. The workflow becomes a thin Dagger invocation:
|
||||
|
||||
```yaml
|
||||
name: Build Container
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '*-v[0-9]*'
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: k8s
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Parse tag
|
||||
id: parse
|
||||
run: |
|
||||
TAG="${GITHUB_REF_NAME}"
|
||||
CONTAINER="${TAG%-v[0-9]*}"
|
||||
VERSION="${TAG#"${CONTAINER}"-}"
|
||||
echo "container=$CONTAINER" >> "$GITHUB_OUTPUT"
|
||||
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
|
||||
- name: Publish
|
||||
run: |
|
||||
curl -fsSL https://dl.dagger.io/dagger/install.sh | sh
|
||||
./bin/dagger call publish \
|
||||
--src=. \
|
||||
--container-name=${{ steps.parse.outputs.container }} \
|
||||
--version=${{ steps.parse.outputs.version }}
|
||||
```
|
||||
|
||||
The composite action (`.forgejo/actions/build-push-image/`), skopeo workaround, and docker save/load dance are all eliminated — that logic lives in the Dagger module.
|
||||
|
||||
### Zot Manifest Compatibility
|
||||
|
||||
The current workflow uses skopeo because Docker 27's manifest format has issues with zot. Dagger's `.publish()` uses BuildKit's push mechanism, which is different. This **must be tested** during implementation. If BuildKit's push also has zot compatibility issues, the Dagger function can shell out to skopeo inside a container step as a fallback.
|
||||
|
||||
### Release Flow (Unchanged)
|
||||
|
||||
```bash
|
||||
mise run container-tag-and-release <container> <version>
|
||||
# → creates git tag → pushes → Forgejo Action triggers → dagger call publish
|
||||
```
|
||||
|
||||
## Phase 2: Docs Build + Forgejo Packages Migration
|
||||
|
||||
Migrate `build-blumeops.yaml` to use Dagger for the build logic and switch from Forgejo releases to Forgejo generic packages for the docs tarball.
|
||||
|
||||
### Artifact Migration: Forgejo Releases → Forgejo Packages
|
||||
|
||||
**Current:** Docs tarball uploaded as a Forgejo release asset.
|
||||
```
|
||||
https://forge.eblu.me/eblume/blumeops/releases/download/v1.5.2/docs-v1.5.2.tar.gz
|
||||
```
|
||||
|
||||
**New:** Docs tarball uploaded to Forgejo generic packages registry.
|
||||
```
|
||||
https://forge.eblu.me/api/packages/eblume/generic/blumeops-docs/v1.6.0/docs-v1.6.0.tar.gz
|
||||
```
|
||||
|
||||
This decouples the docs artifact from git releases while keeping the versioned URL pattern. Forgejo releases can still be created for changelog/announcement purposes without carrying the tarball.
|
||||
|
||||
### Dagger Functions
|
||||
|
||||
```python
|
||||
@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")
|
||||
.with_exec(["pip", "install", "towncrier"])
|
||||
.with_directory("/workspace", src)
|
||||
.with_workdir("/workspace")
|
||||
.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 build Quartz site, return 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")
|
||||
)
|
||||
|
||||
@function
|
||||
async def upload_docs(
|
||||
self,
|
||||
tarball: dagger.File,
|
||||
version: str,
|
||||
forgejo_token: dagger.Secret,
|
||||
) -> str:
|
||||
"""Upload docs tarball to Forgejo generic packages."""
|
||||
import httpx
|
||||
|
||||
token = await forgejo_token.plaintext()
|
||||
await tarball.export(f"/tmp/docs-{version}.tar.gz")
|
||||
|
||||
async with httpx.AsyncClient() as client:
|
||||
with open(f"/tmp/docs-{version}.tar.gz", "rb") as f:
|
||||
resp = await client.put(
|
||||
f"https://forge.eblu.me/api/packages/eblume/generic/"
|
||||
f"blumeops-docs/{version}/docs-{version}.tar.gz",
|
||||
headers={"Authorization": f"token {token}"},
|
||||
content=f.read(),
|
||||
)
|
||||
resp.raise_for_status()
|
||||
return f"https://forge.eblu.me/api/packages/eblume/generic/blumeops-docs/{version}/docs-{version}.tar.gz"
|
||||
|
||||
@function
|
||||
async def release_docs(
|
||||
self,
|
||||
src: dagger.Directory,
|
||||
version: str,
|
||||
forgejo_token: dagger.Secret,
|
||||
argocd_token: dagger.Secret,
|
||||
) -> str:
|
||||
"""Full docs release: build, upload to Forgejo packages, sync ArgoCD."""
|
||||
tarball = await self.build_docs(src, version)
|
||||
pkg_url = await self.upload_docs(tarball, version, forgejo_token)
|
||||
|
||||
# Sync ArgoCD
|
||||
await (
|
||||
dag.container()
|
||||
.from_("alpine:3.21")
|
||||
.with_exec(["apk", "add", "--no-cache", "curl"])
|
||||
.with_secret_variable("ARGOCD_AUTH_TOKEN", argocd_token)
|
||||
.with_exec(["sh", "-c",
|
||||
"curl -fSs -X POST "
|
||||
"-H \"Authorization: Bearer $ARGOCD_AUTH_TOKEN\" "
|
||||
"\"https://argocd.ops.eblu.me/api/v1/applications/docs/sync\" "
|
||||
"-d '{}'"])
|
||||
.sync()
|
||||
)
|
||||
|
||||
return pkg_url
|
||||
```
|
||||
|
||||
### Local Iteration Workflow
|
||||
|
||||
```bash
|
||||
# Test the full docs build locally — identical to CI
|
||||
dagger call build-docs --src=. --version=dev export --path=./docs-dev.tar.gz
|
||||
|
||||
# Inspect the output
|
||||
tar tf docs-dev.tar.gz | head -20
|
||||
|
||||
# Debug a Quartz build failure interactively
|
||||
dagger call --interactive build-docs --src=. --version=dev
|
||||
|
||||
# Test just the changelog build
|
||||
dagger call build-changelog --src=. --version=dev export --path=./updated-src/
|
||||
```
|
||||
|
||||
This is particularly valuable for debugging Quartz build issues and for iterating on a personal quartz fork.
|
||||
|
||||
### Forgejo Actions Integration
|
||||
|
||||
The workflow remains manually triggered (workflow_dispatch) to preserve centralized version sequencing. Dagger handles the build/upload/deploy; the workflow handles version resolution and git commit:
|
||||
|
||||
```yaml
|
||||
name: Build BlumeOps
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
version_type: { type: choice, options: [BUMP_PATCH, BUMP_MINOR, BUMP_MAJOR] }
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: k8s
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with: { fetch-depth: 0 }
|
||||
|
||||
- name: Resolve version
|
||||
id: version
|
||||
run: |
|
||||
# ... existing version bump logic (query Forgejo API, bump semver) ...
|
||||
|
||||
- name: Build, upload, and deploy
|
||||
env:
|
||||
FORGEJO_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
ARGOCD_TOKEN: ${{ secrets.ARGOCD_AUTH_TOKEN }}
|
||||
run: |
|
||||
curl -fsSL https://dl.dagger.io/dagger/install.sh | sh
|
||||
./bin/dagger call release-docs \
|
||||
--src=. --version=${{ steps.version.outputs.version }} \
|
||||
--forgejo-token=env:FORGEJO_TOKEN \
|
||||
--argocd-token=env:ARGOCD_TOKEN
|
||||
|
||||
- name: Export changelog changes
|
||||
run: |
|
||||
./bin/dagger call build-changelog \
|
||||
--src=. --version=${{ steps.version.outputs.version }} \
|
||||
export --path=.
|
||||
|
||||
- name: Update manifest and commit
|
||||
run: |
|
||||
VERSION="${{ steps.version.outputs.version }}"
|
||||
URL="https://forge.eblu.me/api/packages/eblume/generic/blumeops-docs/${VERSION}/docs-${VERSION}.tar.gz"
|
||||
sed -i "s|value: \"https://.*\"|value: \"${URL}\"|" \
|
||||
argocd/manifests/docs/deployment.yaml
|
||||
git config user.name "Forgejo Actions"
|
||||
git config user.email "actions@forge.ops.eblu.me"
|
||||
git add CHANGELOG.md docs/changelog.d/ argocd/manifests/docs/deployment.yaml
|
||||
git commit -m "Release docs $VERSION [skip ci]"
|
||||
git push origin HEAD:main
|
||||
```
|
||||
|
||||
### Manifest Update
|
||||
|
||||
The quartz container's `DOCS_RELEASE_URL` env var in `argocd/manifests/docs/deployment.yaml` must be updated to use the Forgejo packages URL format:
|
||||
|
||||
```yaml
|
||||
# Before (Forgejo releases):
|
||||
- name: DOCS_RELEASE_URL
|
||||
value: "https://forge.eblu.me/eblume/blumeops/releases/download/v1.5.2/docs-v1.5.2.tar.gz"
|
||||
|
||||
# After (Forgejo generic packages):
|
||||
- name: DOCS_RELEASE_URL
|
||||
value: "https://forge.eblu.me/api/packages/eblume/generic/blumeops-docs/v1.6.0/docs-v1.6.0.tar.gz"
|
||||
```
|
||||
|
||||
The quartz container's `start.sh` already downloads from `DOCS_RELEASE_URL` via curl — no container changes needed, just the URL format changes.
|
||||
|
||||
## Phase 3: Runner Simplification
|
||||
|
||||
Once container builds and docs builds both use Dagger, the k8s-runner image can be simplified.
|
||||
|
||||
### Current Runner Requirements
|
||||
|
||||
The `forgejo-runner` container (at `containers/forgejo-runner/`) bundles:
|
||||
- Docker CLI + buildx plugin (for container builds)
|
||||
- Skopeo (for zot push compatibility)
|
||||
- Node.js (for Quartz docs builds)
|
||||
- ArgoCD CLI (for deployment sync)
|
||||
- Various other tools
|
||||
|
||||
### Simplified Runner
|
||||
|
||||
With Dagger, the runner needs:
|
||||
- Docker CLI (Dagger shells out to `docker` to provision its BuildKit engine)
|
||||
- The `dagger` CLI binary
|
||||
- Node.js (required by `actions/checkout@v4` and other JavaScript Actions)
|
||||
- Git (for checkout)
|
||||
- ArgoCD CLI, uv, flyctl (used directly in workflow steps)
|
||||
- Basic shell utilities and tzdata
|
||||
|
||||
Removed from the pre-Dagger image: Docker buildx plugin, skopeo, lsb-release, xz-utils. These tools now live inside Dagger containers.
|
||||
|
||||
### Implementation
|
||||
|
||||
Update `containers/forgejo-runner/Dockerfile` to remove tool-specific dependencies. Install the `dagger` CLI instead. The DinD sidecar in the Forgejo runner pod (`argocd/manifests/forgejo-runner/`) stays unchanged — Dagger's engine runs inside Docker, which the sidecar provides.
|
||||
|
||||
## Phase 4: Future Workflows
|
||||
|
||||
These are natural extensions once the Dagger module is established:
|
||||
|
||||
### Forked Project Builds
|
||||
|
||||
Once the [[upstream-fork-strategy]] is in place, forked projects (e.g., a personal quartz fork) can use the same Dagger patterns for building. The docs build function could accept a quartz source directory parameter instead of cloning upstream, enabling builds against the fork.
|
||||
|
||||
### Python Package Builds
|
||||
|
||||
If private Python packages are built for [[devpi]], Dagger is a natural fit:
|
||||
|
||||
```bash
|
||||
dagger call build-package --src=. --version=v1.0.0
|
||||
# → builds wheel/sdist → uploads to devpi
|
||||
```
|
||||
|
||||
### Pre-Merge Validation
|
||||
|
||||
A `validate` function that runs linting, doc link checks, and other pre-merge checks:
|
||||
|
||||
```bash
|
||||
dagger call validate --src=.
|
||||
# → runs docs-check-links, docs-check-index, docs-check-filenames, etc.
|
||||
```
|
||||
|
||||
Same checks run locally and in CI. Could be triggered by Forgejo Actions on PR creation.
|
||||
|
||||
## Caveats and Risks
|
||||
|
||||
### Dagger Is Pre-1.0
|
||||
|
||||
Current version is v0.19.x. API breakage between versions is possible. Mitigations:
|
||||
- Pin the Dagger CLI version in the runner image and local install
|
||||
- Test upgrades on a branch before adopting
|
||||
- The module is small enough to update quickly if APIs change
|
||||
|
||||
### Privileged Container Requirement
|
||||
|
||||
The Dagger engine requires privileged container access. The current Forgejo runner already uses DinD (privileged), so this should work. Must be verified during implementation.
|
||||
|
||||
### BuildKit Cache Persistence
|
||||
|
||||
BuildKit caches aggressively, making repeated builds fast. Since the Forgejo runner pod is persistent (not ephemeral), the cache persists between CI runs. Locally, the Dagger engine maintains its own cache. No special cache configuration should be needed.
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
### Phase 1 (Containers)
|
||||
- [x] `dagger call build --src=. --container-name=nettest` succeeds locally
|
||||
- [ ] `dagger call build --src=. --container-name=nettest terminal` drops into container shell
|
||||
- [x] `dagger call publish --src=. --container-name=nettest --version=test` pushes to zot
|
||||
- [x] Zot manifest compatibility confirmed (no skopeo needed) or fallback implemented
|
||||
- [x] Tag-triggered Forgejo Action successfully calls `dagger call publish`
|
||||
- [x] Existing `mise run container-tag-and-release` workflow still works end-to-end
|
||||
|
||||
### Phase 2 (Docs)
|
||||
- [x] `dagger call build-docs --src=. --version=dev` produces valid tarball locally
|
||||
- [x] Tarball contents match current Quartz build output
|
||||
- [ ] ~~`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~~ (deferred)
|
||||
- [ ] ~~ArgoCD sync works from within Dagger~~ (deferred)
|
||||
- [x] Forgejo Actions workflow_dispatch completes full release cycle
|
||||
- [x] CHANGELOG.md and fragment cleanup committed correctly
|
||||
|
||||
### Phase 3 (Runner)
|
||||
- [x] Simplified runner image builds and runs (forgejo-runner v3.0.2)
|
||||
- [x] Dagger engine starts inside the runner's DinD environment
|
||||
- [x] All existing workflows pass with the simplified runner
|
||||
- [x] TZ=America/Los_Angeles works in job containers (tzdata installed)
|
||||
|
||||
### Known Issues
|
||||
|
||||
- **Changelog dates show UTC date:** Towncrier uses `datetime.date.today()` which respects `TZ`, and `tzdata` is installed in the runner image, but the changelog still renders tomorrow's date (UTC). The runner pod and job containers both have `TZ=America/Los_Angeles` set. Root cause is unresolved — may require passing an explicit `--date` flag to towncrier or patching the Dagger `build_changelog` container.
|
||||
|
||||
## How-To Articles
|
||||
|
||||
Standalone how-to articles for Dagger were deemed unnecessary — the existing [[update-documentation]] how-to was updated to reflect the Dagger build process, and a [[dagger]] reference card covers CLI usage and function signatures.
|
||||
|
||||
## Reference
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `.forgejo/workflows/build-container.yaml` | Current container build workflow (to be migrated) |
|
||||
| `.forgejo/workflows/build-blumeops.yaml` | Current docs build workflow (to be migrated) |
|
||||
| `.forgejo/actions/build-push-image/action.yaml` | Current composite action (to be removed) |
|
||||
| `containers/forgejo-runner/Dockerfile` | Runner image (to be simplified) |
|
||||
| `argocd/manifests/forgejo-runner/` | Runner k8s manifests |
|
||||
| `argocd/manifests/docs/deployment.yaml` | Docs deployment (DOCS_RELEASE_URL to update) |
|
||||
|
||||
## Related
|
||||
|
||||
- [[upstream-fork-strategy]] — Forking strategy plan (future Dagger integration)
|
||||
- [[forgejo]] — CI/CD infrastructure
|
||||
- [[zot]] — Container registry
|
||||
- [[apps]] — ArgoCD application registry
|
||||
|
|
@ -1,105 +0,0 @@
|
|||
---
|
||||
title: "Plan: Adopt OIDC Identity Provider"
|
||||
modified: 2026-02-19
|
||||
tags:
|
||||
- how-to
|
||||
- plans
|
||||
- security
|
||||
- oidc
|
||||
---
|
||||
|
||||
# Plan: Adopt OIDC Identity Provider
|
||||
|
||||
> **Status:** Completed (2026-02-19) — Phase 1 (Dex + Grafana). Dex was subsequently replaced by [[authentik]] (see [[deploy-authentik]]).
|
||||
> **PR:** #222
|
||||
|
||||
## Background
|
||||
|
||||
BlumeOps services currently handle authentication independently — ArgoCD has its own admin password, Grafana has its own login, Forgejo has local accounts, and zot has no auth at all. There is no single sign-on, no centralized user management, and no way to issue scoped API keys or service tokens from a shared identity.
|
||||
|
||||
Adding an OpenID Connect (OIDC) identity provider gives BlumeOps a central authentication layer. Services delegate login to the IdP, and the IdP issues tokens that carry identity claims.
|
||||
|
||||
## Final Design
|
||||
|
||||
### Provider: Dex
|
||||
|
||||
Dex was chosen for its lightweight footprint (single Go binary, ~50MB RAM), config-driven operation (no web UI needed), and native Gitea/Forgejo connector support.
|
||||
|
||||
### Architecture
|
||||
|
||||
```
|
||||
User Browser
|
||||
|
|
||||
v
|
||||
Grafana (indri/minikube) --OIDC--> Dex (ringtail/k3s) --OAuth2--> Forgejo (indri/native)
|
||||
^ |
|
||||
| |
|
||||
+---------------------- redirect back with token -------------------+
|
||||
```
|
||||
|
||||
Key design decisions:
|
||||
|
||||
- **Dex runs on ringtail's k3s cluster** — isolates the IdP from indri's minikube. If minikube goes down, Dex stays up. Recovery path: SSH → indri → ArgoCD local admin → fix.
|
||||
- **Forgejo is the upstream identity source** — not static passwords. Users authenticate with their Forgejo account. Adding a user to SSO = creating a Forgejo account.
|
||||
- **SQLite3 storage with emptyDir** — avoids a Kubernetes CRD storage bug (Go URL parsing issue with in-cluster API address). Pod restart invalidates sessions (users re-login), acceptable for a homelab.
|
||||
- **NixOS-built container** — `containers/dex/default.nix` using `pkgs.dex-oidc`, consistent with the ntfy pattern.
|
||||
- **Full config templated via ExternalSecret** — the entire `config.yaml` lives in the ExternalSecret template with secrets injected from 1Password. Nothing sensitive in git.
|
||||
- **Cross-cluster communication** — Grafana reaches Dex via `https://dex.ops.eblu.me` (Caddy → Tailscale → ringtail), not k8s-internal DNS.
|
||||
|
||||
### Resolved Open Questions
|
||||
|
||||
- **Service dependency and recovery:** Dex on ringtail is independent of minikube. All services keep local admin logins as break-glass. If Dex goes down, users log in locally.
|
||||
- **Dex vs Authentik:** Dex confirmed as the right choice. Config-driven, minimal resource usage, native Forgejo connector.
|
||||
- **Storage backend:** SQLite3 (not Kubernetes CRDs). The CRD backend crashes due to a Go URL parsing bug with k3s's in-cluster API address. SQLite3 with emptyDir is simpler and avoids the issue.
|
||||
- **User management scaling:** Forgejo connector solves this. Users are managed in Forgejo, not in Dex config files. Future option to add Google/GitHub connectors alongside Forgejo.
|
||||
- **Tailscale ACL interaction:** Dex is tailnet-only via Caddy. Public access is a future consideration tied to exposing Forgejo publicly.
|
||||
|
||||
## Execution (as completed)
|
||||
|
||||
1. Created `containers/dex/default.nix` and built `dex:v1.0.0-nix`
|
||||
2. Created 1Password item "Dex (blumeops)" with Forgejo OAuth2 credentials and Grafana client secret
|
||||
3. Created OAuth2 application in Forgejo (Site Administration → Applications, confidential client, redirect URI `https://dex.ops.eblu.me/callback`)
|
||||
4. Created ArgoCD app (`argocd/apps/dex.yaml`) targeting ringtail
|
||||
5. Created k8s manifests: ExternalSecret, Deployment, Service, Ingress (5 files in `argocd/manifests/dex/`)
|
||||
6. Added `dex.ops.eblu.me` to Caddy reverse proxy config
|
||||
7. Created `grafana-dex-oauth` ExternalSecret for Grafana's OIDC client secret
|
||||
8. Added `auth.generic_oauth` to Grafana's `values.yaml` with Dex endpoints
|
||||
9. Fixed Grafana `root_url` from `grafana.tail8d86e.ts.net` to `grafana.ops.eblu.me` (OAuth state cookie mismatch)
|
||||
10. Deployed and verified end-to-end SSO flow
|
||||
|
||||
## Verification (completed)
|
||||
|
||||
- [x] Container image exists: `dex:v1.0.0-nix` in registry
|
||||
- [x] OIDC discovery endpoint returns valid configuration
|
||||
- [x] Health check passes (`/healthz`)
|
||||
- [x] Grafana login page shows "Sign in with Dex" button
|
||||
- [x] OIDC flow: click Dex → Forgejo login → redirect back → logged in as Admin
|
||||
- [x] Break-glass: local admin login still works
|
||||
- [x] `mise run services-check` passes
|
||||
- [x] ArgoCD shows dex app healthy and synced
|
||||
|
||||
## Key Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `containers/dex/default.nix` | NixOS container build |
|
||||
| `argocd/apps/dex.yaml` | ArgoCD app (ringtail target) |
|
||||
| `argocd/manifests/dex/` | K8s manifests (ExternalSecret, Deployment, Service, Ingress) |
|
||||
| `argocd/manifests/grafana-config/external-secret-dex-oauth.yaml` | Grafana OIDC client secret |
|
||||
| `argocd/manifests/grafana/values.yaml` | Grafana OIDC config (`auth.generic_oauth`) |
|
||||
| `ansible/roles/caddy/defaults/main.yml` | Caddy reverse proxy entry |
|
||||
|
||||
## Future Phases
|
||||
|
||||
- **Phase 2:** ArgoCD OIDC (keep local admin, RBAC: `g, blume.erich@gmail.com, role:admin`)
|
||||
- **Phase 3:** Forgejo OAuth2 provider integration (keep local accounts)
|
||||
- **Phase 4:** Miniflux, Immich, other services
|
||||
- **Phase 5:** Zot OIDC + hardening (per [[harden-zot-registry]])
|
||||
|
||||
## Related
|
||||
|
||||
- [[authentik]] - Current OIDC identity provider (replaced Dex)
|
||||
- [[federated-login]] - How authentication works across BlumeOps
|
||||
- [[harden-zot-registry]] - Future OIDC client
|
||||
- [[forgejo]] - Upstream OAuth2 provider
|
||||
- [[grafana]] - First OIDC client
|
||||
|
|
@ -1,19 +0,0 @@
|
|||
---
|
||||
title: Completed Plans
|
||||
modified: 2026-02-23
|
||||
tags:
|
||||
- how-to
|
||||
- plans
|
||||
---
|
||||
|
||||
# Completed Plans
|
||||
|
||||
Plans that have been fully implemented and verified. Kept for historical reference and as templates for future plans.
|
||||
|
||||
| Plan | Completed | Description |
|
||||
|------|-----------|-------------|
|
||||
| [[adopt-dagger-ci]] | 2026-02-11 | Adopt Dagger as CI/CD build engine (Phases 1–3) |
|
||||
| [[segment-home-network]] | 2026-02-14 | Manual three-network segmentation for UniFi Express 7 |
|
||||
| [[operationalize-reolink-camera]] | 2026-02-15 | Deploy Frigate NVR stack with Mosquitto, Ntfy, and frigate-notify |
|
||||
| [[adopt-oidc-provider]] | 2026-02-19 | Deploy OIDC identity provider with Grafana SSO (initially Dex, replaced by Authentik) |
|
||||
| [[deploy-authentik]] | 2026-02-20 | Deploy Authentik IdP with Nix container, Blueprints, and OIDC client migration |
|
||||
|
|
@ -1,289 +0,0 @@
|
|||
---
|
||||
title: "Plan: Operationalize ReoLink Camera"
|
||||
modified: 2026-02-19
|
||||
tags:
|
||||
- how-to
|
||||
- plans
|
||||
- security
|
||||
- surveillance
|
||||
- frigate
|
||||
---
|
||||
|
||||
# Plan: Operationalize ReoLink Camera
|
||||
|
||||
> **Status:** Completed (2026-02-15)
|
||||
> **Depends on:** [[add-unifi-pulumi-stack]] — the camera must be on the IoT VLAN, isolated from the rest of the network.
|
||||
> **PR:** #190
|
||||
|
||||
## Background
|
||||
|
||||
A ReoLink Elite Floodlight WiFi outdoor camera has been purchased. The goal is to operate it in a fully **cloud-free, privacy-first** configuration — no ReoLink cloud account, no Ring-style surveillance state participation. All video stays on local infrastructure.
|
||||
|
||||
### Goals
|
||||
|
||||
- **NVR recording to sifaka** — continuous and event-based recording stored on the Synology NAS via NFS, not on any cloud service
|
||||
- **No SD card** — the camera does not need one when recording externally; avoid relying on on-device storage
|
||||
- **Cloud-free** — disable UID/P2P, block internet access at the firewall, operate as a pure LAN device
|
||||
- **Object detection and alerting** — detect people, vehicles, animals and send notifications without relying on ReoLink's cloud AI features
|
||||
- **Ring buffer retention** — automatic storage management so recordings don't fill the NAS
|
||||
- **IoT VLAN isolation** — camera lives on the isolated IoT/Appliances network with only the required ports open to the services subnet
|
||||
|
||||
## ReoLink Elite Floodlight WiFi
|
||||
|
||||
### Capabilities
|
||||
|
||||
| Feature | Details |
|
||||
|---------|---------|
|
||||
| Resolution | 4K/8MP (5120x1552 stitched dual-lens panoramic, 180°) |
|
||||
| Codec | H.265 (HEVC) main stream, H.264 sub stream |
|
||||
| Connectivity | WiFi 6 (802.11ax) dual-band |
|
||||
| RTSP | Yes (disabled by default, enable in settings) — `rtsp://admin:<pw>@<IP>:554/Preview_01_main` |
|
||||
| ONVIF | Yes (port 8000, disabled by default) |
|
||||
| HTTP API | Yes — `https://<IP>/cgi-bin/api.cgi?cmd=<Cmd>&user=admin&password=<pw>` |
|
||||
| Floodlight control | Via HTTP API (`SetWhiteLed`) — brightness, mode (off/smart/always/timer) |
|
||||
| On-device AI | Person/vehicle/pet detection (runs locally on camera, fires ONVIF events) |
|
||||
|
||||
### Cloud-Free Operation
|
||||
|
||||
The camera operates fully without internet:
|
||||
|
||||
1. **Disable UID (P2P):** Settings > Network > Advanced > Enable UID → Off
|
||||
2. **Block internet at firewall:** IoT VLAN rule denies all outbound to WAN
|
||||
3. **No ReoLink cloud account needed** — initial setup via app on local network, skip account prompts
|
||||
|
||||
What works without internet: RTSP, ONVIF, HTTP API, on-device AI detection, floodlight control, live view.
|
||||
|
||||
What is lost: remote app access (use VPN/Tailscale instead), push notifications (use Frigate alerting), OTA firmware updates (manual firmware files instead).
|
||||
|
||||
### SD Card: Not Required
|
||||
|
||||
Confirmed: the camera streams RTSP and fires ONVIF events without an SD card. On-device recording/playback and local AI video search require an SD card, but both are unnecessary when using an external NVR.
|
||||
|
||||
### Required Network Ports
|
||||
|
||||
| Port | Protocol | Purpose | Who connects |
|
||||
|------|----------|---------|-------------|
|
||||
| 554 | TCP (RTSP) | Video streaming | Frigate (services subnet) |
|
||||
| 443 | TCP (HTTPS) | API control | Home Assistant / scripts (services subnet) |
|
||||
| 8000 | TCP (ONVIF) | Event subscriptions | Home Assistant (services subnet) |
|
||||
|
||||
These ports need to be allowed from the BlumeOps Services subnet (`192.168.10.0/24`) to the camera's IP on the IoT VLAN (`192.168.3.0/24`). All other traffic to/from the camera is blocked.
|
||||
|
||||
## Frigate NVR
|
||||
|
||||
Frigate is the clear choice for homelab NVR — open-source, container-native, sophisticated retention, native Prometheus metrics, and first-class ReoLink support.
|
||||
|
||||
### Architecture
|
||||
|
||||
Frigate runs as a container in the k8s cluster on indri. It consumes the camera's RTSP streams via go2rtc (an embedded RTSP restreaming proxy that handles connection reliability), performs object detection on the sub stream, and writes recordings to sifaka via NFS.
|
||||
|
||||
```
|
||||
ReoLink Camera (IoT VLAN)
|
||||
│
|
||||
│ RTSP (port 554)
|
||||
▼
|
||||
Frigate (k8s pod on indri)
|
||||
├── go2rtc — RTSP restream proxy
|
||||
├── FFmpeg — stream decoding
|
||||
├── ONNX detector — object detection (CPU)
|
||||
├── /media/frigate — NFS mount to sifaka
|
||||
└── /db — local SQLite (emptyDir or PV)
|
||||
│
|
||||
├──→ Prometheus (/api/metrics endpoint)
|
||||
└──→ MQTT (detection events)
|
||||
```
|
||||
|
||||
### Object Detection on M1
|
||||
|
||||
Indri is an Apple M1 Mac mini. Inside minikube's Linux VM, the Apple Neural Engine is not accessible. Detection options:
|
||||
|
||||
- **ONNX (CPU):** Works on ARM64. For a single camera, M1's performance cores handle detection comfortably. This is the starting point.
|
||||
- **Hailo-8L (future):** If more cameras are added, a USB-attached Hailo-8L accelerator (~$30) could be passed through to the VM. Evaluate only if CPU detection proves insufficient.
|
||||
|
||||
### Recording and Retention (Ring Buffer)
|
||||
|
||||
Frigate's retention system is the most sophisticated of any homelab NVR:
|
||||
|
||||
```yaml
|
||||
record:
|
||||
enabled: true
|
||||
retain:
|
||||
days: 3 # Keep ALL continuous recordings for 3 days
|
||||
mode: all
|
||||
alerts:
|
||||
retain:
|
||||
days: 30 # Keep alert clips (person/vehicle) for 30 days
|
||||
mode: active_objects
|
||||
detections:
|
||||
retain:
|
||||
days: 14 # Keep all detection clips for 14 days
|
||||
mode: motion
|
||||
```
|
||||
|
||||
**Safety mechanism:** When less than 1 hour of storage remains, the oldest 2 hours of recordings are deleted automatically (checked every 5 minutes).
|
||||
|
||||
**Recordings are written directly from the camera stream — no re-encoding.** This means zero CPU cost for recording; CPU is only used for detection on the sub stream.
|
||||
|
||||
### Storage Estimates
|
||||
|
||||
For a single 4K H.265 camera at moderate quality:
|
||||
|
||||
| Strategy | Per Day | 30 Days | Notes |
|
||||
|----------|---------|---------|-------|
|
||||
| 24/7 continuous | ~80-130 GB | 2.4-3.9 TB | Upper bound |
|
||||
| Motion-only | ~8-26 GB | 240-780 GB | Depends on scene activity |
|
||||
| Detection-only (active objects) | ~2-13 GB | 60-390 GB | Most efficient |
|
||||
| Hybrid: 3d continuous + 30d events | — | ~600 GB-1 TB | Recommended starting point |
|
||||
|
||||
A dedicated 2 TB NFS share on sifaka gives comfortable headroom for the hybrid approach with one camera.
|
||||
|
||||
### NFS Storage Setup
|
||||
|
||||
Mount an NFS share from sifaka into the Frigate pod:
|
||||
|
||||
- **Recordings:** NFS PersistentVolume (e.g., `sifaka:/volume1/frigate`) mounted at `/media/frigate`
|
||||
- **Database:** Local storage (emptyDir or a hostPath PV) mounted at `/db` — SQLite performs poorly over NFS
|
||||
|
||||
This follows the same pattern as Navidrome (`argocd/manifests/navidrome/pv-nfs.yaml`) and Immich (`argocd/manifests/immich/pv-nfs.yaml`).
|
||||
|
||||
**NFS export on sifaka:** Add `/volume1/frigate` with access restricted to the BlumeOps Services subnet (`192.168.10.0/24`) and Docker NAT range (`100.64.0.0/10`).
|
||||
|
||||
### Prometheus Metrics
|
||||
|
||||
Frigate exposes a native `/api/metrics` Prometheus endpoint with:
|
||||
|
||||
- `frigate_cpu_usage_percent`, `frigate_mem_usage_percent`
|
||||
- `frigate_camera_fps`, `frigate_detection_fps`, `frigate_process_fps`
|
||||
- `frigate_skipped_fps`, `frigate_detection_enabled`
|
||||
|
||||
A pre-built [Grafana dashboard](https://grafana.com/grafana/dashboards/18226-frigate/) exists. Add a Prometheus scrape target and a Grafana dashboard ConfigMap.
|
||||
|
||||
### Alerting
|
||||
|
||||
Options for detection-based notifications (no Home Assistant required):
|
||||
|
||||
- **[frigate-notify](https://github.com/0x2142/frigate-notify):** Standalone notification service supporting Telegram, Ntfy, Pushover, Discord, webhooks, and more. Runs as a separate container, subscribes to Frigate's MQTT events.
|
||||
- **MQTT events:** Frigate publishes to MQTT on every detection — can be consumed by any MQTT subscriber.
|
||||
- **Home Assistant automations:** If HA is added later, full integration with notification channels.
|
||||
|
||||
### ReoLink-Specific Configuration
|
||||
|
||||
ReoLink cameras need go2rtc as an intermediary (direct RTSP from Frigate can drop connections). Frigate config sketch:
|
||||
|
||||
```yaml
|
||||
go2rtc:
|
||||
streams:
|
||||
front_floodlight:
|
||||
- "ffmpeg:http://admin:<your-password>@192.168.3.X/flv?port=1935&app=bcs&stream=channel0_main.bcs#video=copy#audio=copy#audio=opus"
|
||||
front_floodlight_sub:
|
||||
- "ffmpeg:http://admin:<your-password>@192.168.3.X/flv?port=1935&app=bcs&stream=channel0_sub.bcs"
|
||||
|
||||
cameras:
|
||||
front_floodlight:
|
||||
enabled: true
|
||||
ffmpeg:
|
||||
inputs:
|
||||
- path: rtsp://127.0.0.1:8554/front_floodlight
|
||||
input_args: preset-rtsp-restream
|
||||
roles: [record]
|
||||
- path: rtsp://127.0.0.1:8554/front_floodlight_sub
|
||||
input_args: preset-rtsp-restream
|
||||
roles: [detect]
|
||||
detect:
|
||||
enabled: true
|
||||
width: 640
|
||||
height: 480
|
||||
objects:
|
||||
track: [person, car, dog, cat]
|
||||
```
|
||||
|
||||
Camera settings to apply: enable RTSP and ONVIF, set "fluency first" encoding mode, set interframe space to 1x.
|
||||
|
||||
## Execution Steps
|
||||
|
||||
1. **Prerequisite: Network segmentation** (see [[add-unifi-pulumi-stack]])
|
||||
- Camera on IoT VLAN (`192.168.3.0/24`)
|
||||
- Firewall rules allowing ports 554, 443, 8000 from services subnet
|
||||
|
||||
2. **Camera initial setup**
|
||||
- Connect to WiFi (IoT SSID)
|
||||
- Set static IP or DHCP reservation
|
||||
- Enable RTSP, ONVIF in camera settings
|
||||
- Disable UID/P2P
|
||||
- Set admin password, store in 1Password
|
||||
- Block internet access at firewall
|
||||
|
||||
3. **Create NFS share on sifaka**
|
||||
- Create `/volume1/frigate` shared folder in Synology DSM
|
||||
- Set NFS permissions: `192.168.10.0/24` and `100.64.0.0/10`
|
||||
|
||||
4. **Deploy Frigate to k8s**
|
||||
- Create `argocd/manifests/frigate/` with Deployment, Service, ConfigMap, PV/PVC
|
||||
- NFS PV for recordings, local storage for database
|
||||
- Configure go2rtc + camera streams
|
||||
- Start with CPU detection (ONNX)
|
||||
|
||||
5. **Deploy MQTT broker** (if not already present)
|
||||
- Frigate needs MQTT for event publishing
|
||||
- Evaluate lightweight options: Mosquitto as a k8s pod
|
||||
|
||||
6. **Set up alerting**
|
||||
- Deploy frigate-notify (or equivalent) as a sidecar or separate pod
|
||||
- Configure notification channel (Ntfy, Telegram, or similar)
|
||||
|
||||
7. **Add Prometheus scrape target and Grafana dashboard**
|
||||
- Add Frigate to `argocd/manifests/prometheus/configmap.yaml`
|
||||
- Add `configmap-frigate.yaml` dashboard to `argocd/manifests/grafana-config/dashboards/`
|
||||
|
||||
8. **Update documentation**
|
||||
- Create reference card for the camera and Frigate
|
||||
- Add changelog fragment
|
||||
- Update sifaka NFS export documentation
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
- [x] Camera streams accessible via RTSP from services subnet
|
||||
- [ ] Camera has no internet access (blocked at firewall) — pending IoT VLAN segmentation
|
||||
- [x] Frigate pod is running and showing live camera feed in web UI
|
||||
- [x] Recordings appearing in NFS share on sifaka
|
||||
- [x] Object detection working (person/vehicle detected in Frigate UI)
|
||||
- [x] Retention policy active (old recordings cleaned up automatically)
|
||||
- [x] Alerts firing on detection events (ntfy push notifications with ~6s delivery)
|
||||
- [x] Prometheus metrics visible in Grafana dashboard
|
||||
- [x] `mise run services-check` passes
|
||||
|
||||
## Open Questions (Resolved)
|
||||
|
||||
- **MQTT broker:** Deployed Mosquitto (eclipse-mosquitto:2) in the `mqtt` namespace. Lightweight, anonymous access, cluster-internal only (no Caddy/ingress needed since MQTT is TCP, not HTTP).
|
||||
- **Home Assistant:** Deferred. Frigate + frigate-notify + ntfy provides a complete pipeline without HA.
|
||||
- **Sifaka NFS share sizing:** Allocated 2 TB. Hybrid retention (3d continuous, 30d alerts, 14d detections) keeps usage well within bounds.
|
||||
- **Additional cameras:** Using ONNX/YOLO-NAS-s on CPU at ~535ms/frame, ~2 FPS detection. Adequate for single camera. Apple Silicon Detector (ASD) via ZMQ is the next upgrade path for better performance (~15ms via Neural Engine). Requires Frigate 0.17+.
|
||||
- **Floodlight automation:** Deferred to future Home Assistant evaluation.
|
||||
|
||||
## Future Considerations
|
||||
|
||||
- **Home Assistant** — adds powerful automation for camera + floodlight + notifications
|
||||
- **License plate recognition** — Frigate supports LPR with appropriate models
|
||||
- **Multiple cameras** — the pattern scales; add more cameras to the same Frigate instance
|
||||
- **Frigate+** ($50/yr) — improved detection models trained on community data, fewer false positives
|
||||
|
||||
## Reference Pattern Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `argocd/manifests/navidrome/pv-nfs.yaml` | NFS PersistentVolume pattern |
|
||||
| `argocd/manifests/immich/pv-nfs.yaml` | NFS PV with ReadWriteMany |
|
||||
| `argocd/manifests/grafana-config/dashboards/configmap-zot.yaml` | Grafana dashboard ConfigMap pattern |
|
||||
| `argocd/manifests/prometheus/configmap.yaml` | Prometheus scrape target config |
|
||||
| `docs/reference/storage/sifaka.md` | NFS export documentation |
|
||||
|
||||
## Post-Completion Update
|
||||
|
||||
Frigate, Mosquitto, and ntfy were migrated from indri's minikube to [[ringtail]]'s k3s cluster with RTX 4080 GPU acceleration (PRs #216, #217). The ZMQ Apple Silicon Detector has been retired in favour of ONNX with CUDA execution provider. Object detection now runs on the GPU rather than CPU.
|
||||
|
||||
## Related
|
||||
|
||||
- [[add-unifi-pulumi-stack]] — network segmentation (IoT VLAN for camera)
|
||||
- [[sifaka]] — NAS storage for recordings
|
||||
- [[cluster]] — k8s cluster hosting Frigate
|
||||
- [[grafana]] — monitoring dashboards
|
||||
|
|
@ -1,124 +0,0 @@
|
|||
---
|
||||
title: "Plan: Segment Home Network"
|
||||
modified: 2026-02-24
|
||||
tags:
|
||||
- how-to
|
||||
- plans
|
||||
- networking
|
||||
---
|
||||
|
||||
# Plan: Segment Home Network
|
||||
|
||||
> **Status:** Completed (2026-02-14)
|
||||
> **Replaces:** [[add-unifi-pulumi-stack]] (abandoned — provider bugs)
|
||||
|
||||
## Background
|
||||
|
||||
All devices currently share a single flat `192.168.1.0/24` network. This means IoT appliances (Frame TV, dishwasher) and guest devices can reach NFS shares, management interfaces, and all other services on the LAN.
|
||||
|
||||
This plan segments the home network into three zones using the UX7 web UI. The IaC approach was abandoned after the `ubiquiti-community/unifi` Terraform provider bricked the network on a no-op update — see [[add-unifi-pulumi-stack]] for details.
|
||||
|
||||
### Security Driver: NFS Exposure
|
||||
|
||||
Sifaka's NFS exports (`/volume1/torrents`, `/volume1/music`, `/volume1/photos`) whitelist `192.168.1.0/24`. Today, **any device on the WiFi** — including IoT appliances or guest devices — can mount and write to these shares. After segmentation, only Main network devices (192.168.1.0/24) have NFS access. IoT (192.168.3.0/24) and Guest (192.168.2.0/24) are on different subnets and cannot reach NFS even without firewall rules.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- [ ] **Back up the UX7 configuration** via `https://192.168.1.1` → Settings → System → Backup. Download the `.unf` backup file before making any changes.
|
||||
- [ ] Verify all wired devices (indri, sifaka, gilbert) have connectivity
|
||||
- [ ] Know which devices should go on each network
|
||||
|
||||
## Three Networks
|
||||
|
||||
| Network | SSID | VLAN | Subnet | Bands | Purpose |
|
||||
|---------|------|------|--------|-------|---------|
|
||||
| Main | Radio New Vegas | 1 (default) | 192.168.1.0/24 | All | Trusted devices (indri, sifaka, gilbert, mouse) |
|
||||
| IoT | (TBD by user) | 3 | 192.168.3.0/24 | 2.4GHz only | Smart devices (Frame TV, appliances) |
|
||||
| Guest | (TBD by user) | 2 | 192.168.2.0/24 | All | Visitors, internet-only |
|
||||
|
||||
## UX7 Configuration Steps
|
||||
|
||||
All configuration is done through the UX7 web UI at `https://192.168.1.1`.
|
||||
|
||||
### 1. Create IoT Network
|
||||
|
||||
Settings → Networks → Create New:
|
||||
|
||||
- **Name:** IoT
|
||||
- **VLAN ID:** 3
|
||||
- **Gateway/Subnet:** 192.168.3.1/24
|
||||
- **DHCP:** Enabled, range 192.168.3.6–192.168.3.254
|
||||
|
||||
### 2. Create Guest Network
|
||||
|
||||
Settings → Networks → Create New:
|
||||
|
||||
- **Name:** Guest
|
||||
- **VLAN ID:** 2
|
||||
- **Gateway/Subnet:** 192.168.2.1/24
|
||||
- **DHCP:** Enabled, range 192.168.2.6–192.168.2.254
|
||||
|
||||
### 3. Create IoT WLAN
|
||||
|
||||
Settings → WiFi → Create New:
|
||||
|
||||
- **SSID:** (user's choice)
|
||||
- **Network:** IoT
|
||||
- **Band:** 2.4GHz only
|
||||
- **Security:** WPA2/WPA3
|
||||
|
||||
### 4. Create Guest WLAN
|
||||
|
||||
Settings → WiFi → Create New:
|
||||
|
||||
- **SSID:** (user's choice)
|
||||
- **Network:** Guest
|
||||
- **Security:** WPA2/WPA3
|
||||
- **Guest policies:** Enabled (client isolation)
|
||||
|
||||
### 5. Enable mDNS Reflector
|
||||
|
||||
Settings → Networks → Global Network Settings:
|
||||
|
||||
- Enable **Multicast DNS** — this allows AirPlay/casting discovery across VLANs (Main ↔ IoT)
|
||||
|
||||
## Firewall Rules (Zone-Based)
|
||||
|
||||
Configured at Settings → Policy Engine → Traffic & Firewall Rules, using Zone-Based Firewall.
|
||||
|
||||
All three networks (Default, IoT, Guest) are in the **Internal** zone. Default inter-VLAN policy is **allow**, so we add **block** rules. **Rule ordering matters** — allow rules must come before matching block rules. Rules are combined where the UI supports multiple destinations.
|
||||
|
||||
**Reordering rules:** The default Traffic & Firewall Rules view may grey out the Reorder button. Use the **Policy Engine → zone matrix view** (grid icon in the left sidebar under Policy Engine) instead — this view allows reordering.
|
||||
|
||||
| # | Name | Action | Source | Destination | Protocol/Port | Notes |
|
||||
|---|------|--------|--------|-------------|---------------|-------|
|
||||
| 1 | Allow established/related | Allow | Any | Any | All (Return Traffic only) | Allows return traffic for initiated connections; must be first |
|
||||
| 2 | IoT → Main streaming allow | Allow | IoT | 192.168.1.99 (indri) | TCP 443, 8096 | Jellyfin direct (8096) and Caddy (443) |
|
||||
| 3 | Main → IoT AirPlay | Allow | Default | 192.168.3.62 (Frame TV) | TCP+UDP 80,443,554,3689,5000-5001,7000-7001,7100,5353,6001-6002,7010-7011 | AirPlay control and streaming; add more IoT IPs as needed |
|
||||
| 4 | IoT AirPlay → Main reverse | Allow | 192.168.3.62 (Frame TV) | Default | TCP+UDP 49152-65535 | AirPlay dynamic reverse connections; scoped to TV IP only. May be unnecessary — see note below |
|
||||
| 5 | Guest → Main,IoT block | Block | Guest | Default + IoT | All | Internet-only isolation, combined into one rule |
|
||||
| 6 | IoT → Main block | Block | IoT | Default | All | Protect NFS and trusted devices |
|
||||
|
||||
### Notes on Firewall Rules
|
||||
|
||||
**Rule ordering is critical.** The zone-based policy engine evaluates rules by their index (display order). Allow rules placed after block rules are never reached. When creating new rules, they are appended at the end — use the zone matrix view to reorder them above the block rules.
|
||||
|
||||
**AirPlay across VLANs** requires: (1) mDNS reflector enabled on both networks for device discovery, (2) allow rules for AirPlay control ports from Main → TV, and (3) the established/related rule (rule 1) to allow return traffic. Rule 4 (dynamic reverse ports) was added during troubleshooting but may not be necessary — the original failure was caused by rule ordering (allow rules placed after block rules), not missing port rules. If tightening the firewall in the future, try disabling rule 4 and testing whether AirPlay still works with just the established/related rule. The TV IP (192.168.3.62) has a fixed DHCP reservation.
|
||||
|
||||
**IoT streaming:** Jellyfin listens on indri:8096 (HTTP). IoT devices (Frame TV) connect directly to `http://192.168.1.99:8096`. Rule 2 allows this specific port; all other Main network access from IoT is blocked by rule 6. The `*.ops.eblu.me` domain resolves to indri's Tailscale IP (100.x.x.x), which is unreachable from non-Tailscale devices, so IoT devices must use the LAN IP directly.
|
||||
|
||||
**DHCP reservations:** Indri at 192.168.1.99 and Frame TV at 192.168.3.62 — both have fixed IPs to ensure firewall rules remain valid.
|
||||
|
||||
**NFS exports:** No changes needed to sifaka's NFS configuration. The exports whitelist `192.168.1.0/24` — after segmentation, only Main network devices are on that subnet. IoT (192.168.3.0/24) and Guest (192.168.2.0/24) can't reach NFS because they're on different subnets. The firewall rules provide defense-in-depth.
|
||||
|
||||
## Future Considerations
|
||||
|
||||
- **UnPoller** — add Prometheus metrics exporter for UniFi gear, integrates with existing Grafana stack
|
||||
- **IaC revisit** — if the ubiquiti-community provider matures and fixes the destructive-update bug, IaC could be reconsidered
|
||||
|
||||
## Related
|
||||
|
||||
- [[add-unifi-pulumi-stack]] — Previous IaC approach (abandoned)
|
||||
- [[unifi]] — Reference card
|
||||
- [[hosts]] — Device inventory
|
||||
- [[power]] — UPS power chain
|
||||
|
|
@ -1,299 +0,0 @@
|
|||
---
|
||||
title: "Plan: Migrate Forgejo from Brew to Source Build"
|
||||
modified: 2026-02-10
|
||||
tags:
|
||||
- how-to
|
||||
- plans
|
||||
- forgejo
|
||||
---
|
||||
|
||||
# Plan: Migrate Forgejo from Brew to Source Build
|
||||
|
||||
> **Status:** Planned (not yet executed)
|
||||
|
||||
## Background
|
||||
|
||||
Forgejo was force-upgraded from v13 to v14 by `brew upgrade`, breaking version control. To prevent uncontrolled upgrades and align with the established pattern for other native services (zot, caddy, alloy), we are transitioning Forgejo from Homebrew to a source-built binary managed by a LaunchAgent.
|
||||
|
||||
### Why Source Build?
|
||||
|
||||
- **Version pinning** — upgrade on our schedule by checking out specific tags
|
||||
- **Consistency** — matches [[zot]], [[caddy]], and [[alloy]] deployment patterns
|
||||
- **Control** — build flags, patches, and dependencies are explicit
|
||||
|
||||
## Source Remote
|
||||
|
||||
Use **Codeberg upstream** as the primary clone source to avoid a circular dependency (Forgejo hosting its own source):
|
||||
|
||||
```
|
||||
https://codeberg.org/forgejo/forgejo.git
|
||||
```
|
||||
|
||||
Add the forge mirror as a secondary remote for convenience and backup:
|
||||
|
||||
```
|
||||
https://forge.eblu.me/mirrors/forgejo.git
|
||||
```
|
||||
|
||||
## One-Time Migration Steps
|
||||
|
||||
These steps are performed manually on indri **before** running Ansible.
|
||||
|
||||
### 1. Clone Forgejo from Codeberg
|
||||
|
||||
```fish
|
||||
ssh indri 'git clone https://codeberg.org/forgejo/forgejo.git ~/code/3rd/forgejo'
|
||||
```
|
||||
|
||||
### 2. Add Forge Mirror as Secondary Remote
|
||||
|
||||
```fish
|
||||
ssh indri 'cd ~/code/3rd/forgejo && git remote add forge https://forge.eblu.me/mirrors/forgejo.git'
|
||||
```
|
||||
|
||||
### 3. Check Out the Desired Version Tag
|
||||
|
||||
```fish
|
||||
ssh indri 'cd ~/code/3rd/forgejo && git checkout v14.0.1'
|
||||
```
|
||||
|
||||
### 4. Create a Local Deployment Branch
|
||||
|
||||
Create a local-only `indri-deployment` branch to track the deployed version. Rebase this branch when upgrading to new tags:
|
||||
|
||||
```fish
|
||||
ssh indri 'cd ~/code/3rd/forgejo && git checkout -b indri-deployment'
|
||||
```
|
||||
|
||||
### 5. Set Up Build Dependencies via Mise
|
||||
|
||||
Forgejo requires Go 1.24+ and Node 20+:
|
||||
|
||||
```fish
|
||||
ssh indri 'cd ~/code/3rd/forgejo && mise use go@1.24 node@20'
|
||||
```
|
||||
|
||||
### 6. Build the Binary
|
||||
|
||||
```fish
|
||||
ssh indri 'cd ~/code/3rd/forgejo && TAGS="bindata timedzdata sqlite sqlite_unlock_notify" mise x -- make build'
|
||||
```
|
||||
|
||||
This produces `./forgejo` in the repo root.
|
||||
|
||||
### 7. Stop Brew Forgejo
|
||||
|
||||
```fish
|
||||
ssh indri 'brew services stop forgejo'
|
||||
```
|
||||
|
||||
### 8. Copy Data to New Location
|
||||
|
||||
```fish
|
||||
ssh indri 'sudo cp -a /opt/homebrew/var/forgejo ~/forgejo'
|
||||
```
|
||||
|
||||
### 9. Fix Ownership
|
||||
|
||||
```fish
|
||||
ssh indri 'sudo chown -R erichblume:staff ~/forgejo'
|
||||
```
|
||||
|
||||
### 10. Run Ansible to Deploy New Config + LaunchAgent
|
||||
|
||||
```fish
|
||||
mise run provision-indri -- --tags forgejo
|
||||
```
|
||||
|
||||
### 11. Verify Service Health
|
||||
|
||||
See the verification checklist below.
|
||||
|
||||
### 12. Uninstall Brew Forgejo
|
||||
|
||||
Only after verifying everything works:
|
||||
|
||||
```fish
|
||||
ssh indri 'brew uninstall forgejo'
|
||||
```
|
||||
|
||||
## Ansible Role Changes
|
||||
|
||||
The following changes to `ansible/roles/forgejo/` should be made in the execution session.
|
||||
|
||||
### `defaults/main.yml`
|
||||
|
||||
Update paths and add new variables to match the zot pattern (`ansible/roles/zot/defaults/main.yml`):
|
||||
|
||||
```yaml
|
||||
# Source build paths
|
||||
forgejo_repo_dir: /Users/erichblume/code/3rd/forgejo
|
||||
forgejo_binary: "{{ forgejo_repo_dir }}/forgejo"
|
||||
|
||||
# Data paths (migrated from brew)
|
||||
forgejo_work_path: /Users/erichblume/forgejo
|
||||
forgejo_config_path: "{{ forgejo_work_path }}/custom/conf/app.ini"
|
||||
forgejo_data_path: "{{ forgejo_work_path }}/data"
|
||||
forgejo_log_path: "{{ forgejo_work_path }}/log"
|
||||
forgejo_log_dir: /Users/erichblume/Library/Logs
|
||||
|
||||
# RUN_USER changes from 'forgejo' to 'erichblume' (LaunchAgent user)
|
||||
forgejo_run_user: erichblume
|
||||
```
|
||||
|
||||
### `tasks/main.yml`
|
||||
|
||||
Replace brew install/start with binary-check + LaunchAgent pattern (matching `ansible/roles/zot/tasks/main.yml`):
|
||||
|
||||
```yaml
|
||||
---
|
||||
# Forgejo role — source-built binary with LaunchAgent
|
||||
#
|
||||
# ONE-TIME SETUP (before running ansible):
|
||||
#
|
||||
# 1. Clone forgejo from codeberg (avoid circular dependency):
|
||||
# ssh indri 'git clone https://codeberg.org/forgejo/forgejo.git ~/code/3rd/forgejo'
|
||||
#
|
||||
# 2. Add forge mirror as secondary remote:
|
||||
# ssh indri 'cd ~/code/3rd/forgejo && git remote add forge https://forge.eblu.me/mirrors/forgejo.git'
|
||||
#
|
||||
# 3. Set up Go and Node via mise:
|
||||
# ssh indri 'cd ~/code/3rd/forgejo && mise use go@1.24 node@20'
|
||||
#
|
||||
# 4. Build:
|
||||
# ssh indri 'cd ~/code/3rd/forgejo && TAGS="bindata timedzdata sqlite sqlite_unlock_notify" mise x -- make build'
|
||||
#
|
||||
# 5. Run ansible to deploy config and LaunchAgent
|
||||
|
||||
- name: Verify forgejo binary exists
|
||||
ansible.builtin.stat:
|
||||
path: "{{ forgejo_binary }}"
|
||||
register: forgejo_binary_stat
|
||||
|
||||
- name: Fail if forgejo binary not found
|
||||
ansible.builtin.fail:
|
||||
msg: |
|
||||
Forgejo binary not found at {{ forgejo_binary }}.
|
||||
Please build from source first:
|
||||
ssh indri 'cd ~/code/3rd/forgejo && TAGS="bindata timedzdata sqlite sqlite_unlock_notify" mise x -- make build'
|
||||
when: not forgejo_binary_stat.stat.exists
|
||||
|
||||
- name: Ensure forgejo config directory exists
|
||||
ansible.builtin.file:
|
||||
path: "{{ forgejo_work_path }}/custom/conf"
|
||||
state: directory
|
||||
mode: '0755'
|
||||
|
||||
- name: Deploy forgejo config
|
||||
ansible.builtin.template:
|
||||
src: app.ini.j2
|
||||
dest: "{{ forgejo_config_path }}"
|
||||
mode: '0600'
|
||||
notify: Restart forgejo
|
||||
|
||||
- name: Deploy forgejo LaunchAgent plist
|
||||
ansible.builtin.template:
|
||||
src: forgejo.plist.j2
|
||||
dest: ~/Library/LaunchAgents/mcquack.eblume.forgejo.plist
|
||||
mode: '0644'
|
||||
notify: Restart forgejo
|
||||
|
||||
- name: Check if forgejo LaunchAgent is loaded
|
||||
ansible.builtin.command: launchctl list mcquack.eblume.forgejo
|
||||
register: forgejo_launchctl_check
|
||||
changed_when: false
|
||||
failed_when: false
|
||||
|
||||
- name: Load forgejo LaunchAgent if not loaded
|
||||
ansible.builtin.command: launchctl load ~/Library/LaunchAgents/mcquack.eblume.forgejo.plist
|
||||
when: forgejo_launchctl_check.rc != 0
|
||||
changed_when: true
|
||||
failed_when: false
|
||||
```
|
||||
|
||||
### `handlers/main.yml`
|
||||
|
||||
Replace `brew services restart` with `launchctl unload/load` (matching `ansible/roles/zot/handlers/main.yml`):
|
||||
|
||||
```yaml
|
||||
---
|
||||
- name: Restart forgejo
|
||||
ansible.builtin.shell: |
|
||||
launchctl unload ~/Library/LaunchAgents/mcquack.eblume.forgejo.plist 2>/dev/null || true
|
||||
launchctl load ~/Library/LaunchAgents/mcquack.eblume.forgejo.plist
|
||||
changed_when: true
|
||||
```
|
||||
|
||||
### New Template: `forgejo.plist.j2`
|
||||
|
||||
LaunchAgent plist (matching `ansible/roles/zot/templates/zot.plist.j2`):
|
||||
|
||||
```xml
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!-- {{ ansible_managed }} -->
|
||||
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
||||
<plist version="1.0">
|
||||
<dict>
|
||||
<key>Label</key>
|
||||
<string>mcquack.eblume.forgejo</string>
|
||||
<key>ProgramArguments</key>
|
||||
<array>
|
||||
<string>{{ forgejo_binary }}</string>
|
||||
<string>-w</string>
|
||||
<string>{{ forgejo_work_path }}</string>
|
||||
<string>-c</string>
|
||||
<string>{{ forgejo_config_path }}</string>
|
||||
<string>web</string>
|
||||
</array>
|
||||
<key>RunAtLoad</key>
|
||||
<true/>
|
||||
<key>KeepAlive</key>
|
||||
<true/>
|
||||
<key>StandardOutPath</key>
|
||||
<string>{{ forgejo_log_dir }}/mcquack.forgejo.out.log</string>
|
||||
<key>StandardErrorPath</key>
|
||||
<string>{{ forgejo_log_dir }}/mcquack.forgejo.err.log</string>
|
||||
</dict>
|
||||
</plist>
|
||||
```
|
||||
|
||||
### `app.ini.j2`
|
||||
|
||||
No changes needed — paths already flow through variables in `defaults/main.yml`. The only change is that `RUN_USER` will pick up `erichblume` from the updated default.
|
||||
|
||||
## What Stays the Same
|
||||
|
||||
- **1Password secret fetching** — playbook `pre_tasks` are unchanged
|
||||
- **`forgejo_actions_secrets` role** — API-based secret sync is unaffected
|
||||
- **SSH clone URLs** — `BUILTIN_SSH_SERVER_USER` stays `forgejo` (this is the git SSH user, not the OS user)
|
||||
- **Caddy routing** — still proxies to `localhost:3001`
|
||||
- **SQLite database** — copied as-is to new location
|
||||
- **All `app.ini` settings** — template is unchanged, just re-rendered with new paths
|
||||
|
||||
## Verification Checklist
|
||||
|
||||
After running the migration and Ansible:
|
||||
|
||||
- [ ] `ssh indri 'launchctl list mcquack.eblume.forgejo'` — shows running
|
||||
- [ ] `curl https://forge.eblu.me/api/v1/version` — returns JSON with version
|
||||
- [ ] Git clone over SSH: `git clone ssh://forgejo@forge.ops.eblu.me:2222/eblume/blumeops.git /tmp/test-clone`
|
||||
- [ ] Git push works on an existing clone
|
||||
- [ ] Ansible dry-run is clean: `mise run provision-indri -- --tags forgejo --check --diff`
|
||||
- [ ] `mise run services-check` — all green
|
||||
- [ ] Forgejo Actions runners reconnect and jobs succeed
|
||||
|
||||
## Future Considerations
|
||||
|
||||
- **CI-built binaries** — build on gilbert or in Forgejo Actions, deploy as artifact
|
||||
- **Artifact release system** — tag-triggered binary builds, similar to container releases (`mise run container-release`)
|
||||
- **Automated upgrades** — Renovate or similar watching Codeberg tags, opening PRs with version bumps
|
||||
- **Indri user management** — run each service as its own macOS user for isolation (a `forgejo` user exists but LaunchAgent session management under non-login users is tricky on macOS)
|
||||
|
||||
## Reference Pattern Files
|
||||
|
||||
| File | Purpose |
|
||||
|------|---------|
|
||||
| `ansible/roles/zot/tasks/main.yml` | Primary pattern for source-built binary tasks |
|
||||
| `ansible/roles/zot/defaults/main.yml` | Variable naming conventions |
|
||||
| `ansible/roles/zot/templates/zot.plist.j2` | LaunchAgent plist template |
|
||||
| `ansible/roles/zot/handlers/main.yml` | Handler pattern (launchctl unload/load) |
|
||||
|
|
@ -1,22 +0,0 @@
|
|||
---
|
||||
title: Plans
|
||||
modified: 2026-02-20
|
||||
tags:
|
||||
- how-to
|
||||
- plans
|
||||
---
|
||||
|
||||
# Plans
|
||||
|
||||
Migration and transition plans for upcoming infrastructure changes. Each plan is a how-to document that captures the full context, steps, and verification criteria for a future execution session.
|
||||
|
||||
Plans differ from regular how-to guides in that they describe work that has been designed but not yet executed. Once a plan is completed, it moves to [[completed]].
|
||||
|
||||
| Plan | Status | Description |
|
||||
|------|--------|-------------|
|
||||
| [[migrate-forgejo-from-brew]] | Planned | Transition Forgejo from Homebrew to source-built binary with LaunchAgent |
|
||||
| [[add-unifi-pulumi-stack]] | Abandoned | Add Pulumi IaC for UniFi Express 7 (provider bugs — see doc) |
|
||||
| [[upstream-fork-strategy]] | Planned | Stacked-branch forking strategy for tracking upstream projects |
|
||||
| [[adopt-oidc-provider]] | Completed | Deploy OIDC identity provider for SSO across services |
|
||||
| [[deploy-authentik]] | Completed | Deploy Authentik IdP — Mikado chain tracked in `how-to/authentik/` |
|
||||
| [[operationalize-reolink-camera]] | Planned | Cloud-free NVR with Frigate, object detection, and ring buffer recording to sifaka |
|
||||
|
|
@ -1,234 +0,0 @@
|
|||
---
|
||||
title: "Plan: Upstream Fork Strategy"
|
||||
modified: 2026-02-11
|
||||
tags:
|
||||
- how-to
|
||||
- plans
|
||||
- forgejo
|
||||
- git
|
||||
---
|
||||
|
||||
# Plan: Upstream Fork Strategy
|
||||
|
||||
> **Status:** Planned (design sketch — not yet executed)
|
||||
|
||||
## Background
|
||||
|
||||
Several BlumeOps projects need to track upstream repositories while maintaining local modifications. Examples include a personal Quartz fork (for docs site customization) and potentially other tools where upstream changes need to flow in continuously.
|
||||
|
||||
The current approach — Forgejo auto-tracking mirrors — works for read-only copies but breaks down when we need to:
|
||||
|
||||
1. Add BlumeOps-specific changes (delete upstream workflows, add `mise.toml`, custom config)
|
||||
2. Develop features that might eventually be upstreamed
|
||||
3. Keep all of this synchronized as upstream evolves
|
||||
|
||||
### Goals
|
||||
|
||||
- Upstream changes flow in automatically (daily rebase)
|
||||
- Rebase conflicts are detected and reported, not silently ignored
|
||||
- BlumeOps-specific patches are cleanly separated from upstream-candidate work
|
||||
- Feature branches that could become upstream PRs are maintained independently
|
||||
|
||||
## Branch Model
|
||||
|
||||
The strategy uses stacked branches — each layer builds on the one below:
|
||||
|
||||
```
|
||||
upstream/main (read-only tracking branch)
|
||||
│
|
||||
▼
|
||||
blumeops (primary branch — blumeops-specific patches)
|
||||
│ e.g., delete .github/, add mise.toml,
|
||||
│ configure for tailnet, etc.
|
||||
│
|
||||
├──▶ feature/foo (feature branch — developed on top of blumeops)
|
||||
│ │ intended for local use, may never go upstream
|
||||
│ │
|
||||
│ └──▶ feature/foo-upstream (optional — same changes rebased onto main)
|
||||
│ for submitting as an upstream PR
|
||||
│
|
||||
└──▶ feature/bar (another feature branch)
|
||||
```
|
||||
|
||||
### Branch Purposes
|
||||
|
||||
| Branch | Base | Purpose | Rebased onto |
|
||||
|--------|------|---------|--------------|
|
||||
| `upstream/main` | — | Tracks upstream's `main` (or `master`) via `git fetch` | Never rebased |
|
||||
| `blumeops` | `upstream/main` | Primary branch; BlumeOps-specific patches only | `upstream/main` (daily) |
|
||||
| `feature/*` | `blumeops` | Feature development | `blumeops` (after successful rebase) |
|
||||
| `feature/*-upstream` | `upstream/main` | Cherry-picked/rebased feature for upstream PR | `upstream/main` (on demand) |
|
||||
|
||||
### What Goes in `blumeops` vs `feature/*`
|
||||
|
||||
**`blumeops` branch** — infrastructure-level changes that are permanent and BlumeOps-specific:
|
||||
- Delete upstream CI workflows (`.github/workflows/`)
|
||||
- Add `mise.toml` for local tooling
|
||||
- Add or modify configuration for the BlumeOps environment
|
||||
- Patch version pins or dependency overrides
|
||||
|
||||
**`feature/*` branches** — functional changes to the project itself:
|
||||
- Bug fixes you want to contribute upstream
|
||||
- New features or customizations
|
||||
- Anything that could theoretically stand on its own as a PR to the upstream project
|
||||
|
||||
This separation ensures the `blumeops` branch stays small and conflict-resistant (infrastructure changes rarely conflict with upstream code changes), while feature branches carry the substantive modifications.
|
||||
|
||||
## Daily Rebase Workflow
|
||||
|
||||
A Forgejo Actions workflow runs on a schedule to keep `blumeops` rebased onto the latest upstream:
|
||||
|
||||
### Workflow Outline
|
||||
|
||||
```
|
||||
trigger: cron (daily) or manual dispatch
|
||||
|
||||
1. Fetch upstream remote
|
||||
2. Check if upstream/main has new commits since last rebase
|
||||
3. If no new commits → exit early
|
||||
4. Attempt: git rebase blumeops --onto upstream/main
|
||||
5. If rebase succeeds:
|
||||
a. Force-push blumeops
|
||||
b. For each feature/* branch:
|
||||
- Attempt rebase onto updated blumeops
|
||||
- If success → force-push
|
||||
- If conflict → skip, record failure
|
||||
6. If rebase fails (conflict):
|
||||
a. Abort rebase
|
||||
b. Create or update a Forgejo issue with conflict details
|
||||
c. Label the issue for visibility
|
||||
```
|
||||
|
||||
### Conflict Reporting
|
||||
|
||||
When a rebase fails, the workflow creates (or updates) a Forgejo issue via the API:
|
||||
|
||||
- **Title:** `Rebase conflict: blumeops onto upstream/main` (or `feature/foo onto blumeops`)
|
||||
- **Body:** Include the conflicting files, the upstream commit range, and the git output
|
||||
- **Labels:** `rebase-conflict`, `automated`
|
||||
- **Assignee:** `eblume`
|
||||
|
||||
The issue serves as a task to manually resolve the conflict. Once resolved and force-pushed, the next daily run succeeds and the issue can be closed.
|
||||
|
||||
### Safety Guards
|
||||
|
||||
- **Never force-push `upstream/main`** — this is a read-only tracking branch, only updated via `git fetch`
|
||||
- **Abort on any rebase ambiguity** — if the rebase produces unexpected state, abort and report rather than pushing garbage
|
||||
- **Dry-run mode** — the workflow should support a manual dispatch input to run in dry-run mode (rebase but don't push, just report what would happen)
|
||||
- **Lock file** — prevent concurrent rebase runs from colliding (Forgejo Actions concurrency groups)
|
||||
|
||||
## One-Time Setup Per Fork
|
||||
|
||||
### Step 1: Create the Mirror
|
||||
|
||||
Set up a Forgejo auto-tracking mirror of the upstream project:
|
||||
```
|
||||
Forgejo → New Migration → Git → URL: https://github.com/org/project.git
|
||||
```
|
||||
|
||||
### Step 2: Disable Mirroring
|
||||
|
||||
Once mirrored, disable the auto-sync in Forgejo repository settings. The repository is now a regular Forgejo repo with the upstream history.
|
||||
|
||||
### Step 3: Set Up Remotes
|
||||
|
||||
```bash
|
||||
cd ~/code/3rd/<project>
|
||||
git remote rename origin forge
|
||||
git remote add upstream https://github.com/org/project.git
|
||||
git fetch upstream
|
||||
```
|
||||
|
||||
### Step 4: Create the `blumeops` Branch
|
||||
|
||||
```bash
|
||||
git checkout upstream/main
|
||||
git checkout -b blumeops
|
||||
# Apply blumeops-specific patches
|
||||
git commit -m "BlumeOps: remove upstream workflows, add mise.toml"
|
||||
git push forge blumeops
|
||||
```
|
||||
|
||||
Set `blumeops` as the default branch in Forgejo repository settings.
|
||||
|
||||
### Step 5: Add the Rebase Workflow
|
||||
|
||||
Add `.forgejo/workflows/rebase-upstream.yaml` to the `blumeops` branch. This workflow is itself a blumeops-specific patch — upstream doesn't have it.
|
||||
|
||||
### Step 6: Protect Branches
|
||||
|
||||
Configure Forgejo branch protection:
|
||||
- `blumeops`: only the rebase workflow (and manual push) can force-push
|
||||
- `upstream/main`: read-only (only updated by the rebase workflow's `git fetch`)
|
||||
|
||||
## The Upstream PR Path
|
||||
|
||||
When a feature is ready to be proposed upstream:
|
||||
|
||||
1. Create `feature/foo-upstream` from `upstream/main`
|
||||
2. Cherry-pick or rebase `feature/foo` commits onto it (excluding any `blumeops`-specific commits)
|
||||
3. Push to the fork on the upstream platform (e.g., GitHub)
|
||||
4. Open PR from the fork to upstream
|
||||
|
||||
This branch is maintained independently — it does not participate in the daily rebase. It's a point-in-time snapshot for the PR. If the PR needs updates, rebase it manually.
|
||||
|
||||
## First Instance: Quartz Fork
|
||||
|
||||
Quartz (the documentation site generator) is the planned first fork and the primary motivation for this strategy.
|
||||
|
||||
- **Upstream:** `https://github.com/jackyzha0/quartz.git`
|
||||
- **Forge repo:** `forge.ops.eblu.me/mirrors/quartz`
|
||||
- **Primary branch:** `blumeops`
|
||||
|
||||
### BlumeOps-Specific Patches (`blumeops` branch)
|
||||
|
||||
Changes that are permanently BlumeOps-specific and would never go upstream:
|
||||
|
||||
- Remove `.github/` workflows
|
||||
- Add `mise.toml` with pinned Node version
|
||||
- Configure Quartz defaults for BlumeOps site metadata
|
||||
|
||||
### Feature Work (`feature/*` branches)
|
||||
|
||||
The key feature motivating this fork is **`last-reviewed` frontmatter support**. BlumeOps documentation uses a `last-reviewed` date in frontmatter to track documentation staleness (see `mise run docs-review`). Upstream Quartz has no awareness of this field. The fork enables:
|
||||
|
||||
- **Rendering `last-reviewed` in article headers** — display when a doc was last reviewed, making staleness visible to readers without running CLI tools
|
||||
- **Staleness indicators** — visual styling (e.g., a warning banner) for docs where `last-reviewed` exceeds a threshold
|
||||
- **Sorting/filtering by review date** — Quartz explorer or listing pages that surface docs needing attention
|
||||
|
||||
This is a strong upstream PR candidate — other Quartz users maintaining knowledge bases would benefit from custom frontmatter rendering. The `feature/last-reviewed` branch would be developed on the `blumeops` branch (for local use) with a parallel `feature/last-reviewed-upstream` branch rebased onto `upstream/main` for the PR.
|
||||
|
||||
### Integration with Dagger Docs Build
|
||||
|
||||
This fork directly supports the [[adopt-dagger-ci]] plan. Once the fork exists, the Dagger `build_docs` function switches from cloning upstream Quartz to using the fork:
|
||||
|
||||
```python
|
||||
# Before (cloning upstream):
|
||||
.with_exec(["git", "clone", "--depth=1",
|
||||
"https://github.com/jackyzha0/quartz.git", "/tmp/quartz"])
|
||||
|
||||
# After (using the BlumeOps fork):
|
||||
.with_exec(["git", "clone", "--depth=1", "--branch=blumeops",
|
||||
"https://forge.eblu.me/mirrors/quartz.git", "/tmp/quartz"])
|
||||
```
|
||||
|
||||
This means the `build-blumeops.yaml` workflow automatically picks up fork customizations (like `last-reviewed` rendering) when building docs — no separate integration step needed. Local iteration via `dagger call build-docs` also uses the fork, so you can test Quartz customizations against actual BlumeOps content before pushing.
|
||||
|
||||
## Open Questions
|
||||
|
||||
- **Rebase vs merge:** This plan uses rebase for a clean linear history. Merge commits would avoid force-pushes but create a messier history. Rebase is preferred for small forks; revisit if the commit volume grows.
|
||||
- **Notification mechanism:** Forgejo issues are proposed for conflict reporting. Alternatives: email, Slack webhook, Todoist task via API. Issues are preferred because they're visible in the forge and can carry discussion.
|
||||
- **Feature branch automation:** The daily rebase of feature branches onto `blumeops` is aggressive — it means feature branches are force-pushed daily. An alternative is to only rebase feature branches on demand (manually or via workflow dispatch). Start with manual and automate later based on experience.
|
||||
- **Multiple upstreams:** Some projects track multiple remotes (e.g., a CNCF project with a GitHub mirror and a self-hosted primary). The workflow should support configurable upstream remote URLs.
|
||||
|
||||
## Future Considerations
|
||||
|
||||
- **Renovate integration** — Renovate could watch upstream tags and open PRs to the `blumeops` branch when new releases are available, complementing the daily rebase with release-aware updates
|
||||
- **Dagger integration** — forked projects that produce build artifacts can use the BlumeOps Dagger module for builds, sharing the same local iteration and CI patterns
|
||||
- **Template repository** — once the pattern is proven with quartz, create a template repo or mise task that scaffolds the branch structure and rebase workflow for new forks
|
||||
|
||||
## Related
|
||||
|
||||
- [[adopt-dagger-ci]] — CI/CD build engine (consumes fork artifacts)
|
||||
- [[forgejo]] — Git forge hosting the forks
|
||||
- [[docs]] — Documentation site (first fork consumer)
|
||||
|
|
@ -17,7 +17,7 @@ Home WiFi router and network controller, managed via the UX7 web UI.
|
|||
| **Model** | UniFi Express 7 (UX7) |
|
||||
| **LAN IP** | `192.168.1.1` |
|
||||
| **Management URL** | `https://192.168.1.1` |
|
||||
| **Management** | Web UI only (no IaC — see [[add-unifi-pulumi-stack]]) |
|
||||
| **Management** | Web UI only (no IaC) |
|
||||
| **Power** | Battery-backed via UPS (see [[power]]) |
|
||||
|
||||
## What It Does
|
||||
|
|
@ -37,7 +37,7 @@ The UX7 is the home WiFi access point and network gateway. It provides:
|
|||
| Guest | 2 | 192.168.2.0/24 | Visitors, internet-only |
|
||||
| IoT | 3 | 192.168.3.0/24 | Smart devices (Frame TV, appliances) |
|
||||
|
||||
See [[segment-home-network]] for the full segmentation plan and firewall rules.
|
||||
Three-network segmentation configured manually via UX7 web UI (Feb 2026).
|
||||
|
||||
## Network Topology
|
||||
|
||||
|
|
@ -67,12 +67,10 @@ Local admin account on the UX7. Credentials stored in 1Password (vault `blumeops
|
|||
|
||||
## Why Not IaC?
|
||||
|
||||
Attempted Feb 2026 with the `ubiquiti-community/unifi` Terraform provider via Pulumi. A "no-op" update on the default LAN network reset undeclared properties, bricking the network and requiring a factory reset. The provider ecosystem is too immature for single-device infrastructure. See [[add-unifi-pulumi-stack]] for details.
|
||||
Attempted Feb 2026 with the `ubiquiti-community/unifi` Terraform provider via Pulumi. A "no-op" update on the default LAN network reset undeclared properties, bricking the network and requiring a factory reset. The provider ecosystem is too immature for single-device infrastructure.
|
||||
|
||||
## Related
|
||||
|
||||
- [[segment-home-network]] — Network segmentation plan
|
||||
- [[add-unifi-pulumi-stack]] — Previous IaC approach (abandoned)
|
||||
- [[hosts]] — Device inventory
|
||||
- [[power]] — UPS power chain
|
||||
- [[indri]] — Primary server (wired connection)
|
||||
|
|
|
|||
|
|
@ -77,4 +77,3 @@ A separate **frigate-notify** pod (`ghcr.io/0x2142/frigate-notify:v0.3.5`) subsc
|
|||
- [[ntfy]] - Push notification delivery
|
||||
- [[sifaka]] - NAS storage for recordings
|
||||
- [[observability]] - Prometheus metrics at `/api/metrics`
|
||||
- [[operationalize-reolink-camera]] - Original deployment plan
|
||||
|
|
|
|||
|
|
@ -88,5 +88,4 @@ In [[forgejo]] Actions, secrets are injected as env vars. Locally, mise tasks ca
|
|||
- [[forgejo]] — CI/CD trigger layer
|
||||
- [[zot]] — Container registry (publish target)
|
||||
- [[docs]] — Documentation site (build target)
|
||||
- [[adopt-dagger-ci]] — Adoption plan (phases 1–3 complete)
|
||||
- [[manage-lockfile]] — Ringtail flake lockfile management
|
||||
|
|
|
|||
|
|
@ -21,7 +21,6 @@ The docs follow the [Diataxis](https://diataxis.fr/) framework:
|
|||
| **[[tutorials|Tutorials]]** | Learning-oriented | "I'm new and want to understand" |
|
||||
| **[[reference|Reference]]** | Information-oriented | "I need specific technical details" |
|
||||
| **[[how-to|How-to]]** | Task-oriented | "I need to do X" |
|
||||
| **[[plans|Plans]]** | Future work | "What's planned next?" |
|
||||
| **[[explanation|Explanation]]** | Understanding-oriented | "I want to understand why" |
|
||||
|
||||
## Quick Paths by Audience
|
||||
|
|
@ -30,7 +29,6 @@ The docs follow the [Diataxis](https://diataxis.fr/) framework:
|
|||
|
||||
You probably want quick access to operational details:
|
||||
- [[how-to]] guides for common operations (deploy, troubleshoot, update ACLs)
|
||||
- [[plans]] captures migration and transition plans for future execution
|
||||
- [[reference]] has service URLs, commands, and config locations
|
||||
- [[ai-assistance-guide]] explains how to work effectively with Claude
|
||||
- Run `mise run ai-docs` to prime AI context with key documentation
|
||||
|
|
@ -39,7 +37,6 @@ You probably want quick access to operational details:
|
|||
|
||||
Context for effective assistance:
|
||||
- Read [[ai-assistance-guide]] for operational conventions
|
||||
- [[plans]] has migration plans designed for AI-executed sessions
|
||||
- [[reference]] has the technical specifics you'll need
|
||||
- The repo's `CLAUDE.md` has critical rules (especially the kubectl context requirement)
|
||||
|
||||
|
|
@ -73,7 +70,7 @@ Documentation uses `[[wiki-links]]` for cross-references:
|
|||
|
||||
When reading on the web (docs.eblu.me), these render as clickable links. The backlinks panel shows what references each page.
|
||||
|
||||
Pre-commit hooks automatically validate that all wiki-links point to existing files and that link targets are unambiguous.
|
||||
Prek hooks automatically validate that all wiki-links point to existing files and that link targets are unambiguous.
|
||||
|
||||
## AI Context Priming
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue