diff --git a/.forgejo/workflows/build-blumeops.yaml b/.forgejo/workflows/build-blumeops.yaml new file mode 100644 index 0000000..6b39c3c --- /dev/null +++ b/.forgejo/workflows/build-blumeops.yaml @@ -0,0 +1,164 @@ +# BlumeOps Release Workflow +# +# Creates a versioned release of BlumeOps with all build artifacts. +# Currently includes: +# - Documentation site (Quartz static build) +# +# Future additions may include other release artifacts. +# +# Usage: +# 1. Go to Actions > Build BlumeOps > Run workflow +# 2. Enter a version tag (e.g., v1.2.0) or leave empty to auto-increment patch +# 3. The workflow creates a release with attached artifacts +# +# Documentation asset URL: +# https://forge.ops.eblu.me/eblume/blumeops/releases/download//docs-.tar.gz + +name: Build BlumeOps + +on: + workflow_dispatch: + inputs: + version: + description: 'Version (e.g., v1.2.0) or empty to auto-increment patch (v_._.+1)' + required: false + default: '' + type: string + +jobs: + build: + runs-on: k8s + steps: + - name: Resolve version + id: version + run: | + INPUT_VERSION="${{ inputs.version }}" + + if [ -n "$INPUT_VERSION" ]; then + # User provided a version - validate it + if [[ ! "$INPUT_VERSION" =~ ^v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then + echo "Error: Version must be in format vX.Y.Z (e.g., v1.0.0)" + exit 1 + fi + VERSION="$INPUT_VERSION" + echo "Using provided version: $VERSION" + else + # Auto-increment patch version from latest release + echo "Fetching latest release..." + LATEST=$(curl -sf "https://forge.ops.eblu.me/api/v1/repos/eblume/blumeops/releases/latest" | jq -r '.tag_name // empty') + + if [ -z "$LATEST" ]; then + VERSION="v1.0.0" + echo "No previous releases found, starting at: $VERSION" + else + # Parse vX.Y.Z and increment patch + VERSION=$(echo "$LATEST" | awk -F. '{print $1"."$2"."$3+1}') + echo "Auto-incremented from $LATEST to: $VERSION" + fi + fi + + # Check if this version already exists + if curl -sf "https://forge.ops.eblu.me/api/v1/repos/eblume/blumeops/releases/tags/$VERSION" > /dev/null 2>&1; then + echo "Error: Release $VERSION already exists" + echo "See: https://forge.ops.eblu.me/eblume/blumeops/releases/tag/$VERSION" + exit 1 + fi + + echo "version=$VERSION" >> "$GITHUB_OUTPUT" + echo "Building BlumeOps release: $VERSION" + + - name: Checkout + uses: actions/checkout@v4 + + - name: Build docs + run: | + VERSION="${{ steps.version.outputs.version }}" + echo "Node version: $(node --version)" + echo "NPM version: $(npm --version)" + + # Clone Quartz from local mirror + git clone --depth 1 https://forge.ops.eblu.me/eblume/quartz.git /tmp/quartz + cd /tmp/quartz + + # Install dependencies + npm ci + + # Copy our configuration (lives in docs/ to keep repo root clean) + cp "$GITHUB_WORKSPACE/docs/quartz.config.ts" . + cp "$GITHUB_WORKSPACE/docs/quartz.layout.ts" . + + # Copy docs as content (includes index.md) + rm -rf content + cp -r "$GITHUB_WORKSPACE/docs" content + + # Build + echo "Building static site..." + npx quartz build + + # Create tarball + TARBALL="docs-${VERSION}.tar.gz" + echo "Creating tarball: $TARBALL" + tar -czf "$GITHUB_WORKSPACE/$TARBALL" -C public . + + echo "Build complete!" + ls -lh "$GITHUB_WORKSPACE/$TARBALL" + + - name: Create release + run: | + VERSION="${{ steps.version.outputs.version }}" + TARBALL="docs-${VERSION}.tar.gz" + + echo "Creating release $VERSION..." + + # Use Forgejo API to create release + RELEASE_DATA=$(cat < /etc/nginx/conf.d/default.conf << 'EOF' +server { + listen 80; + server_name _; + root /usr/share/nginx/html; + index index.html; + + # Enable gzip compression + gzip on; + gzip_types text/plain text/css application/json application/javascript text/xml application/xml text/javascript; + + # Cache static assets + location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg|woff|woff2)$ { + expires 1y; + add_header Cache-Control "public, immutable"; + } + + # SPA fallback - serve index.html for client-side routing + location / { + try_files $uri $uri/ $uri.html /index.html; + } + + # Health check endpoint + location /healthz { + access_log off; + return 200 "ok\n"; + add_header Content-Type text/plain; + } +} +EOF + +EXPOSE 80 + +CMD ["/start.sh"] diff --git a/containers/quartz/start.sh b/containers/quartz/start.sh new file mode 100644 index 0000000..778eeb1 --- /dev/null +++ b/containers/quartz/start.sh @@ -0,0 +1,31 @@ +#!/bin/sh +set -e + +HTML_DIR="/usr/share/nginx/html" + +# Check for required environment variable +if [ -z "$DOCS_RELEASE_URL" ]; then + echo "Error: DOCS_RELEASE_URL environment variable is required" + echo "Set it to the URL of the static site tarball to serve" + exit 1 +fi + +echo "Downloading docs from: $DOCS_RELEASE_URL" + +# Download the tarball +if ! curl -fsSL "$DOCS_RELEASE_URL" -o /tmp/docs.tar.gz; then + echo "Error: Failed to download docs from $DOCS_RELEASE_URL" + exit 1 +fi + +# Clear existing content and extract +rm -rf "${HTML_DIR:?}"/* +echo "Extracting docs to $HTML_DIR" +tar -xzf /tmp/docs.tar.gz -C "$HTML_DIR" +rm /tmp/docs.tar.gz + +echo "Docs extracted successfully" +echo "Starting nginx..." + +# Start nginx in foreground +exec nginx -g "daemon off;" diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..01d1b66 --- /dev/null +++ b/docs/README.md @@ -0,0 +1,126 @@ +# BlumeOps Documentation + +> **Note on naming**: The project is properly stylized as **BlumeOps**, though "blumeops" and "Blue Mops" are also commonly used interchangeably. + +This directory contains documentation for BlumeOps, Erich Blume's personal infrastructure GitOps repository. + +## Documentation Restructuring (In Progress) + +The documentation is being restructured to follow the [Diataxis](https://diataxis.fr/) documentation framework while serving multiple audiences. + +### Target Audiences + +1. **Erich (owner)** - A knowledge graph/zettelkasten for quickly recalling important facts about BlumeOps infrastructure and operations. + +2. **Claude/AI agents** - Memory and context enrichment for AI-assisted operations and development. + +3. **New external readers** - People who want to understand "what is BlumeOps?" at a high level. + +4. **Potential operators/contributors** - External readers who want to help operate, modify, or answer questions about BlumeOps, or onboard as a member. + +5. **Replicators** - People who want to duplicate this approach for their own personal infrastructure operations. + +### Requirements + +- **Source format**: Markdown with optional YAML frontmatter +- **Editing**: Compatible with [Obsidian](https://obsidian.md) and [obsidian.nvim](https://github.com/obsidian-nvim/obsidian.nvim) +- **Cross-references**: Wiki-link syntax (`[[link]]`) for internal links +- **Output formats**: HTML (for web hosting) and PDF (for offline reference) +- **Changelog**: Track significant documentation changes + +### Tooling + +**Selected**: [Quartz](https://quartz.jzhao.xyz/) - A TypeScript-based static site generator designed for Obsidian vaults. Features wiki-link support, backlinks, graph view, and excellent Obsidian compatibility. + +**Architecture**: +- **Source**: Markdown files in `docs/` with optional YAML frontmatter +- **Build**: Quartz builds static HTML/CSS/JS via Forgejo workflow +- **Release**: Built assets published as Forgejo release attachments +- **Hosting**: `quartz` container downloads release bundle on startup and serves via nginx +- **URL**: `docs.ops.eblu.me` (planned) + +## Restructuring Phases + +### Phase 1a: Foundation & CI (Current) +- [x] Move existing zk cards to `docs/zk/` +- [x] Update `zk-docs` mise task for new path +- [x] Create this README with restructuring plan +- [x] Select documentation tooling (Quartz) +- [x] Create Quartz configuration (`quartz.config.ts`, `quartz.layout.ts`) +- [x] Create `quartz` container for serving static sites +- [x] Create `build-blumeops` workflow for building releases +- [ ] Test the build workflow and verify release creation +- [ ] Create `CHANGELOG.md` + +### Phase 1b: CD & Hosting +- [ ] Create ArgoCD manifests for `quartz` deployment +- [ ] Add `docs.ops.eblu.me` to Caddy reverse proxy +- [ ] Configure deployment to pull from release URL +- [ ] Test end-to-end: commit -> build -> release -> deploy + +### Phase 2: Tutorials +Learning-oriented content for getting started. + +- [ ] Create `tutorials/` directory +- [ ] "Getting Started with BlumeOps" - What this is and how to explore it +- [ ] "Setting Up a Similar Environment" - For replicators +- [ ] "Your First Contribution" - For potential contributors + +### Phase 3: How-to Guides +Task-oriented instructions for specific operations. + +- [ ] Create `how-to/` directory +- [ ] Migrate operational content from zk cards +- [ ] "How to deploy a new Kubernetes service" +- [ ] "How to add a new Ansible role" +- [ ] "How to update Tailscale ACLs" +- [ ] "How to troubleshoot common issues" + +### Phase 4: Reference +Information-oriented technical descriptions. + +- [ ] Create `reference/` directory +- [ ] Service reference pages (migrate from zk cards) +- [ ] Infrastructure inventory +- [ ] Configuration reference +- [ ] API/CLI reference for mise tasks + +### Phase 5: Explanation +Understanding-oriented discussion of concepts and decisions. + +- [ ] Create `explanation/` directory +- [ ] "Why GitOps?" - Philosophy and approach +- [ ] "Architecture Overview" - How everything fits together +- [ ] "Security Model" - Tailscale, secrets management, etc. +- [ ] "Decision Log" - ADRs (Architecture Decision Records) + +### Phase 6: Integration & Cleanup +- [ ] Migrate remaining useful content from `docs/zk/` +- [ ] Decide fate of zk cards (archive, delete, or keep as separate knowledge base) +- [ ] Update CLAUDE.md to reference new doc structure +- [ ] Mirror docs to GitHub Pages for public access (optional) + +## Current Directory Layout + +``` +docs/ +├── README.md # This file +├── CHANGELOG.md # Documentation changelog (planned) +├── tutorials/ # Learning-oriented (planned) +├── how-to/ # Task-oriented (planned) +├── reference/ # Information-oriented (planned) +├── explanation/ # Understanding-oriented (planned) +└── zk/ # Zettelkasten cards (temporary) + ├── 1767747119-YCPO.md # Main blumeops overview card + └── ... # Service-specific cards and notes +``` + +## Viewing the ZK Cards + +To view all BlumeOps zettelkasten cards: + +```fish +mise run zk-docs +``` + +This displays all cards tagged with `blumeops`, starting with the main overview card. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..409a10e --- /dev/null +++ b/docs/index.md @@ -0,0 +1,7 @@ +--- +title: BlumeOps Documentation +--- + +Welcome to the BlumeOps documentation. + +[[README|Documentation Home]] diff --git a/docs/quartz.config.ts b/docs/quartz.config.ts new file mode 100644 index 0000000..bfdaa22 --- /dev/null +++ b/docs/quartz.config.ts @@ -0,0 +1,91 @@ +import { QuartzConfig } from "./quartz/cfg" +import * as Plugin from "./quartz/plugins" + +/** + * Quartz configuration for BlumeOps documentation + * See https://quartz.jzhao.xyz/configuration + */ +const config: QuartzConfig = { + configuration: { + pageTitle: "BlumeOps Docs", + pageTitleSuffix: "", + enableSPA: true, + enablePopovers: true, + analytics: null, + locale: "en-US", + baseUrl: "docs.ops.eblu.me", + ignorePatterns: ["private", "templates", ".obsidian"], + defaultDateType: "modified", + theme: { + fontOrigin: "googleFonts", + cdnCaching: true, + typography: { + header: "Schibsted Grotesk", + body: "Source Sans Pro", + code: "IBM Plex Mono", + }, + colors: { + lightMode: { + light: "#faf8f8", + lightgray: "#e5e5e5", + gray: "#b8b8b8", + darkgray: "#4e4e4e", + dark: "#2b2b2b", + secondary: "#284b63", + tertiary: "#84a59d", + highlight: "rgba(143, 159, 169, 0.15)", + textHighlight: "#fff23688", + }, + darkMode: { + light: "#161618", + lightgray: "#393639", + gray: "#646464", + darkgray: "#d4d4d4", + dark: "#ebebec", + secondary: "#7b97aa", + tertiary: "#84a59d", + highlight: "rgba(143, 159, 169, 0.15)", + textHighlight: "#fff23688", + }, + }, + }, + }, + plugins: { + transformers: [ + Plugin.FrontMatter(), + Plugin.CreatedModifiedDate({ + priority: ["frontmatter", "git", "filesystem"], + }), + Plugin.SyntaxHighlighting({ + theme: { + light: "github-light", + dark: "github-dark", + }, + keepBackground: false, + }), + Plugin.ObsidianFlavoredMarkdown({ enableInHtmlEmbed: false }), + Plugin.GitHubFlavoredMarkdown(), + Plugin.TableOfContents(), + Plugin.CrawlLinks({ markdownLinkResolution: "shortest" }), + Plugin.Description(), + Plugin.Latex({ renderEngine: "katex" }), + ], + filters: [Plugin.RemoveDrafts()], + emitters: [ + Plugin.AliasRedirects(), + Plugin.ComponentResources(), + Plugin.ContentPage(), + Plugin.FolderPage(), + Plugin.TagPage(), + Plugin.ContentIndex({ + enableSiteMap: true, + enableRSS: true, + }), + Plugin.Assets(), + Plugin.Static(), + Plugin.NotFoundPage(), + ], + }, +} + +export default config diff --git a/docs/quartz.layout.ts b/docs/quartz.layout.ts new file mode 100644 index 0000000..c3f7e6d --- /dev/null +++ b/docs/quartz.layout.ts @@ -0,0 +1,54 @@ +import { PageLayout, SharedLayout } from "./quartz/cfg" +import * as Component from "./quartz/components" + +/** + * Quartz layout configuration for BlumeOps documentation + * See https://quartz.jzhao.xyz/layout + */ + +// Components shared across all pages +export const sharedPageComponents: SharedLayout = { + head: Component.Head(), + header: [], + afterBody: [], + footer: Component.Footer({ + links: { + "GitHub": "https://github.com/eblume/blumeops", + "Forge": "https://forge.ops.eblu.me/eblume/blumeops", + }, + }), +} + +// Components for pages that list posts (folder pages, tag pages) +export const defaultContentPageLayout: PageLayout = { + beforeBody: [ + Component.Breadcrumbs(), + Component.ArticleTitle(), + Component.ContentMeta(), + Component.TagList(), + ], + left: [ + Component.PageTitle(), + Component.MobileOnly(Component.Spacer()), + Component.Search(), + Component.Darkmode(), + Component.DesktopOnly(Component.Explorer()), + ], + right: [ + Component.Graph(), + Component.DesktopOnly(Component.TableOfContents()), + Component.Backlinks(), + ], +} + +export const defaultListPageLayout: PageLayout = { + beforeBody: [Component.Breadcrumbs(), Component.ArticleTitle(), Component.ContentMeta()], + left: [ + Component.PageTitle(), + Component.MobileOnly(Component.Spacer()), + Component.Search(), + Component.Darkmode(), + Component.DesktopOnly(Component.Explorer()), + ], + right: [], +} diff --git a/docs/1767747119-YCPO.md b/docs/zk/1767747119-YCPO.md similarity index 100% rename from docs/1767747119-YCPO.md rename to docs/zk/1767747119-YCPO.md diff --git a/docs/1768246525-RVRY.md b/docs/zk/1768246525-RVRY.md similarity index 100% rename from docs/1768246525-RVRY.md rename to docs/zk/1768246525-RVRY.md diff --git a/docs/1768283761-TRXN.md b/docs/zk/1768283761-TRXN.md similarity index 100% rename from docs/1768283761-TRXN.md rename to docs/zk/1768283761-TRXN.md diff --git a/docs/1768457769-LOCK.md b/docs/zk/1768457769-LOCK.md similarity index 100% rename from docs/1768457769-LOCK.md rename to docs/zk/1768457769-LOCK.md diff --git a/docs/1768506761-GHUW.md b/docs/zk/1768506761-GHUW.md similarity index 100% rename from docs/1768506761-GHUW.md rename to docs/zk/1768506761-GHUW.md diff --git a/docs/1768506761-XGYX.md b/docs/zk/1768506761-XGYX.md similarity index 100% rename from docs/1768506761-XGYX.md rename to docs/zk/1768506761-XGYX.md diff --git a/docs/argocd.md b/docs/zk/argocd.md similarity index 100% rename from docs/argocd.md rename to docs/zk/argocd.md diff --git a/docs/borgmatic.md b/docs/zk/borgmatic.md similarity index 100% rename from docs/borgmatic.md rename to docs/zk/borgmatic.md diff --git a/docs/external-secrets.md b/docs/zk/external-secrets.md similarity index 100% rename from docs/external-secrets.md rename to docs/zk/external-secrets.md diff --git a/docs/grafana.md b/docs/zk/grafana.md similarity index 100% rename from docs/grafana.md rename to docs/zk/grafana.md diff --git a/docs/indri.md b/docs/zk/indri.md similarity index 100% rename from docs/indri.md rename to docs/zk/indri.md diff --git a/docs/jellyfin.md b/docs/zk/jellyfin.md similarity index 100% rename from docs/jellyfin.md rename to docs/zk/jellyfin.md diff --git a/docs/kiwix.md b/docs/zk/kiwix.md similarity index 100% rename from docs/kiwix.md rename to docs/zk/kiwix.md diff --git a/docs/miniflux.md b/docs/zk/miniflux.md similarity index 100% rename from docs/miniflux.md rename to docs/zk/miniflux.md diff --git a/docs/minikube.md b/docs/zk/minikube.md similarity index 100% rename from docs/minikube.md rename to docs/zk/minikube.md diff --git a/docs/navidrome.md b/docs/zk/navidrome.md similarity index 100% rename from docs/navidrome.md rename to docs/zk/navidrome.md diff --git a/docs/postgresql.md b/docs/zk/postgresql.md similarity index 100% rename from docs/postgresql.md rename to docs/zk/postgresql.md diff --git a/docs/pulumi.md b/docs/zk/pulumi.md similarity index 100% rename from docs/pulumi.md rename to docs/zk/pulumi.md diff --git a/docs/teslamate.md b/docs/zk/teslamate.md similarity index 100% rename from docs/teslamate.md rename to docs/zk/teslamate.md diff --git a/docs/transmission.md b/docs/zk/transmission.md similarity index 100% rename from docs/transmission.md rename to docs/zk/transmission.md diff --git a/docs/zot.md b/docs/zk/zot.md similarity index 100% rename from docs/zot.md rename to docs/zk/zot.md diff --git a/mise-tasks/zk-docs b/mise-tasks/zk-docs index 32de565..2ce9bb3 100755 --- a/mise-tasks/zk-docs +++ b/mise-tasks/zk-docs @@ -4,7 +4,7 @@ set -euo pipefail # Blumeops docs now live in the repo itself (symlinked into zk) -DOCS_DIR="$(cd "$(dirname "$0")/.." && pwd)/docs" +DOCS_DIR="$(cd "$(dirname "$0")/.." && pwd)/docs/zk" MAIN_CARD="$DOCS_DIR/1767747119-YCPO.md" # Find all files tagged with blumeops (excluding main card)