C1: migrate cv + docs from minikube to indri-native #342

Merged
eblume merged 3 commits from migrate-cv-docs-to-indri into main 2026-04-29 14:55:12 -07:00
16 changed files with 415 additions and 136 deletions

View file

@ -178,10 +178,11 @@ jobs:
echo "## Documentation" echo "## Documentation"
echo "" echo ""
echo "Download \`$TARBALL\` and configure the quartz container with:" echo "Download \`$TARBALL\` directly, or bump \`docs_version\`"
echo "in \`ansible/roles/docs/defaults/main.yml\` and run:"
echo "" echo ""
echo "\`\`\`" echo "\`\`\`"
echo "DOCS_RELEASE_URL=https://forge.eblu.me/eblume/blumeops/releases/download/$VERSION/$TARBALL" echo "mise run provision-indri -- --tags docs"
echo "\`\`\`" echo "\`\`\`"
} > /tmp/release_body.txt } > /tmp/release_body.txt
@ -223,18 +224,16 @@ jobs:
echo "" echo ""
echo "Release created successfully!" echo "Release created successfully!"
- name: Update docs deployment - name: Bump docs_version in ansible role
run: | run: |
VERSION="${{ steps.version.outputs.version }}" VERSION="${{ steps.version.outputs.version }}"
TARBALL="docs-${VERSION}.tar.gz" DEFAULTS_FILE="ansible/roles/docs/defaults/main.yml"
DEPLOYMENT_FILE="argocd/manifests/docs/deployment.yaml"
RELEASE_URL="https://forge.eblu.me/eblume/blumeops/releases/download/${VERSION}/${TARBALL}"
echo "Updating $DEPLOYMENT_FILE with new release URL..." echo "Bumping docs_version in $DEFAULTS_FILE to ${VERSION}..."
yq -i "(.spec.template.spec.containers[0].env[] | select(.name == \"DOCS_RELEASE_URL\")).value = \"${RELEASE_URL}\"" "$DEPLOYMENT_FILE" yq -i ".docs_version = \"${VERSION}\"" "$DEFAULTS_FILE"
echo "Updated deployment:" echo "Updated defaults:"
grep -A1 "DOCS_RELEASE_URL" "$DEPLOYMENT_FILE" grep -E "^docs_version:" "$DEFAULTS_FILE"
- name: Commit release changes - name: Commit release changes
env: env:
@ -248,7 +247,7 @@ jobs:
git config user.email "actions@forge.ops.eblu.me" git config user.email "actions@forge.ops.eblu.me"
# Stage deployment changes # Stage deployment changes
git add argocd/manifests/docs/deployment.yaml git add ansible/roles/docs/defaults/main.yml
# Stage changelog changes if updated # Stage changelog changes if updated
if [ "$CHANGELOG_UPDATED" = "true" ]; then if [ "$CHANGELOG_UPDATED" = "true" ]; then
@ -270,34 +269,6 @@ jobs:
echo "Changes committed and pushed" echo "Changes committed and pushed"
fi fi
- name: Deploy docs
env:
ARGOCD_AUTH_TOKEN: ${{ secrets.ARGOCD_AUTH_TOKEN }}
run: |
echo "Syncing docs app via ArgoCD..."
# Sync docs app (uses ARGOCD_AUTH_TOKEN env var for auth)
argocd app sync docs \
--server argocd.ops.eblu.me \
--grpc-web \
--prune
# Wait for sync to complete
argocd app wait docs \
--server argocd.ops.eblu.me \
--grpc-web \
--timeout 120
echo "Docs app synced successfully!"
- name: Purge Fly.io proxy cache
env:
FLY_API_TOKEN: ${{ secrets.FLY_DEPLOY_TOKEN }}
run: |
echo "Purging nginx cache on Fly.io proxy..."
fly ssh console -a blumeops-proxy -C "sh -c 'rm -rf /tmp/cache && nginx -s reload'"
echo "Cache purged"
- name: Summary - name: Summary
run: | run: |
VERSION="${{ steps.version.outputs.version }}" VERSION="${{ steps.version.outputs.version }}"
@ -309,5 +280,12 @@ jobs:
echo "Release URL:" echo "Release URL:"
echo " https://forge.eblu.me/eblume/blumeops/releases/tag/$VERSION" echo " https://forge.eblu.me/eblume/blumeops/releases/tag/$VERSION"
echo "" echo ""
echo "Asset URL (for DOCS_RELEASE_URL ConfigMap):" echo "Asset URL:"
echo " https://forge.eblu.me/eblume/blumeops/releases/download/$VERSION/$TARBALL" echo " https://forge.eblu.me/eblume/blumeops/releases/download/$VERSION/$TARBALL"
echo ""
echo "To deploy on indri, run from gilbert:"
echo " mise run provision-indri -- --tags docs"
echo ""
echo "Then purge the Fly.io proxy cache:"
echo " fly ssh console -a blumeops-proxy -C \\"
echo " \"sh -c 'rm -rf /tmp/cache && nginx -s reload'\""

View file

@ -1,12 +1,14 @@
# CV Deploy Workflow # CV Deploy Workflow
# #
# Updates the CV deployment to a specific package version, commits # Bumps cv_version in ansible/roles/cv/defaults/main.yml and pushes the change.
# the change, and syncs via ArgoCD. # Deployment to indri is manual (runner has no SSH access to indri):
# mise run provision-indri -- --tags cv
# #
# Usage: # Usage:
# 1. Release a new CV package from the cv repo first # 1. Release a new CV package from the cv repo first
# 2. Go to Actions > Deploy CV > Run workflow # 2. Go to Actions > Deploy CV > Run workflow
# 3. Enter the version to deploy, or leave as "latest" # 3. Enter the version to deploy, or leave as "latest"
# 4. Run the command above on gilbert to apply
name: Deploy CV name: Deploy CV
@ -60,18 +62,16 @@ jobs:
- name: Checkout - name: Checkout
uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- name: Update CV deployment - name: Bump cv_version in ansible role
run: | run: |
VERSION="${{ steps.version.outputs.version }}" VERSION="${{ steps.version.outputs.version }}"
TARBALL="cv-${VERSION}.tar.gz" DEFAULTS_FILE="ansible/roles/cv/defaults/main.yml"
DEPLOYMENT_FILE="argocd/manifests/cv/deployment.yaml"
RELEASE_URL="https://forge.eblu.me/api/packages/eblume/generic/cv/${VERSION}/${TARBALL}"
echo "Updating $DEPLOYMENT_FILE with CV_RELEASE_URL..." echo "Bumping cv_version in $DEFAULTS_FILE to ${VERSION}..."
yq -i "(.spec.template.spec.containers[0].env[] | select(.name == \"CV_RELEASE_URL\")).value = \"${RELEASE_URL}\"" "$DEPLOYMENT_FILE" yq -i ".cv_version = \"${VERSION}\"" "$DEFAULTS_FILE"
echo "Updated deployment:" echo "Updated defaults:"
grep -A1 "CV_RELEASE_URL" "$DEPLOYMENT_FILE" grep -E "^cv_version:" "$DEFAULTS_FILE"
- name: Commit release changes - name: Commit release changes
env: env:
@ -82,7 +82,7 @@ jobs:
git config user.name "Forgejo Actions" git config user.name "Forgejo Actions"
git config user.email "actions@forge.ops.eblu.me" git config user.email "actions@forge.ops.eblu.me"
git add argocd/manifests/cv/deployment.yaml git add ansible/roles/cv/defaults/main.yml
if git diff --cached --quiet; then if git diff --cached --quiet; then
echo "No changes to commit (already at $VERSION)" echo "No changes to commit (already at $VERSION)"
@ -94,38 +94,16 @@ jobs:
echo "Changes committed and pushed" echo "Changes committed and pushed"
fi fi
- name: Deploy CV
env:
ARGOCD_AUTH_TOKEN: ${{ secrets.ARGOCD_AUTH_TOKEN }}
run: |
echo "Syncing CV app via ArgoCD..."
argocd app sync cv \
--server argocd.ops.eblu.me \
--grpc-web \
--prune
argocd app wait cv \
--server argocd.ops.eblu.me \
--grpc-web \
--timeout 120
echo "CV app synced successfully!"
- name: Purge Fly.io proxy cache
env:
FLY_API_TOKEN: ${{ secrets.FLY_DEPLOY_TOKEN }}
run: |
echo "Purging nginx cache on Fly.io proxy..."
fly ssh console -a blumeops-proxy -C "sh -c 'rm -rf /tmp/cache && nginx -s reload'"
echo "Cache purged"
- name: Summary - name: Summary
run: | run: |
VERSION="${{ steps.version.outputs.version }}" VERSION="${{ steps.version.outputs.version }}"
echo "================================================" echo "================================================"
echo "CV Deployed: $VERSION" echo "CV version bumped: $VERSION"
echo "================================================" echo "================================================"
echo "" echo ""
echo "CV should now be live at:" echo "To deploy on indri, run from gilbert:"
echo " https://cv.ops.eblu.me/" echo " mise run provision-indri -- --tags cv"
echo ""
echo "Then purge the Fly.io proxy cache:"
echo " fly ssh console -a blumeops-proxy -C \\"
echo " \"sh -c 'rm -rf /tmp/cache && nginx -s reload'\""

View file

@ -256,5 +256,9 @@
tags: jellyfin_metrics tags: jellyfin_metrics
- role: forgejo_metrics - role: forgejo_metrics
tags: forgejo_metrics tags: forgejo_metrics
- role: cv
tags: cv
- role: docs
tags: docs
- role: caddy - role: caddy
tags: caddy tags: caddy

View file

@ -72,10 +72,16 @@ caddy_services:
backend: "https://go.tail8d86e.ts.net" backend: "https://go.tail8d86e.ts.net"
- name: docs - name: docs
host: "docs.{{ caddy_domain }}" host: "docs.{{ caddy_domain }}"
backend: "https://docs.tail8d86e.ts.net" kind: static
root: "{{ docs_content_dir }}"
try_html: true # Quartz: path → path/ → path.html → 404.html
- name: cv - name: cv
host: "cv.{{ caddy_domain }}" host: "cv.{{ caddy_domain }}"
backend: "https://cv.tail8d86e.ts.net" kind: static
root: "{{ cv_content_dir }}"
download_paths:
- path: /resume.pdf
filename: erich-blume-resume.pdf
- name: nvr - name: nvr
host: "nvr.{{ caddy_domain }}" host: "nvr.{{ caddy_domain }}"
backend: "https://nvr.tail8d86e.ts.net" backend: "https://nvr.tail8d86e.ts.net"

View file

@ -31,6 +31,25 @@
{% for service in caddy_services %} {% for service in caddy_services %}
@{{ service.name }} host {{ service.host }} @{{ service.name }} host {{ service.host }}
handle @{{ service.name }} { handle @{{ service.name }} {
{% if service.kind | default('proxy') == 'static' %}
root * {{ service.root }}
encode gzip
# Long-cache fingerprinted assets; everything else stays default.
@{{ service.name }}_assets path_regexp \.(css|js|png|jpg|jpeg|gif|ico|svg|woff|woff2)$
header @{{ service.name }}_assets Cache-Control "public, max-age=31536000, immutable"
{% for dl in service.download_paths | default([]) %}
@{{ service.name }}_dl{{ loop.index }} path {{ dl.path }}
header @{{ service.name }}_dl{{ loop.index }} Content-Disposition `attachment; filename="{{ dl.filename }}"`
{% endfor %}
{% if service.try_html | default(false) %}
try_files {path} {path}/ {path}.html
handle_errors 404 {
rewrite * /404.html
file_server
}
{% endif %}
file_server
{% else %}
{% if service.cache_policy | default('') == 'spa' %} {% if service.cache_policy | default('') == 'spa' %}
# SPA cache policy: hashed static assets are immutable, HTML must revalidate. # SPA cache policy: hashed static assets are immutable, HTML must revalidate.
# Prevents stale HTML from referencing chunk hashes that no longer exist. # Prevents stale HTML from referencing chunk hashes that no longer exist.
@ -47,6 +66,7 @@
} }
{% else %} {% else %}
reverse_proxy {{ service.backend }} reverse_proxy {{ service.backend }}
{% endif %}
{% endif %} {% endif %}
} }

View file

@ -0,0 +1,10 @@
---
# CV / resume static site (native, replaces minikube Deployment)
# Caddy serves cv_content_dir directly via the static-kind service block.
cv_version: "v1.0.3"
cv_release_url: "https://forge.eblu.me/api/packages/eblume/generic/cv/{{ cv_version }}/cv-{{ cv_version }}.tar.gz"
cv_home: /Users/erichblume/blumeops/cv
cv_content_dir: "{{ cv_home }}/content"
cv_version_sentinel: "{{ cv_home }}/.installed-version"

View file

@ -0,0 +1,57 @@
---
# cv role — download and extract the CV release tarball into cv_content_dir.
# Caddy serves the directory directly; there is no daemon to manage.
#
# Idempotency: a sentinel file records the installed cv_version. The
# download/extract steps only run when the sentinel doesn't match cv_version.
#
# We use curl rather than ansible.builtin.get_url because the forge generic-
# packages endpoint returns 405 on HEAD requests, which get_url issues before
# downloading.
- name: Ensure cv home exists
ansible.builtin.file:
path: "{{ cv_home }}"
state: directory
mode: '0755'
- name: Read installed cv version sentinel
ansible.builtin.slurp:
src: "{{ cv_version_sentinel }}"
register: cv_installed_raw
failed_when: false
changed_when: false
- name: Set installed cv version fact
ansible.builtin.set_fact:
cv_installed_version: >-
{{ (cv_installed_raw.content | b64decode).strip()
if (cv_installed_raw.content is defined) else '' }}
- name: Recreate cv content dir
ansible.builtin.file:
path: "{{ cv_content_dir }}"
state: "{{ item }}"
mode: '0755'
loop:
- absent
- directory
when: cv_installed_version != cv_version
- name: Download and extract cv release tarball
ansible.builtin.shell:
cmd: >-
set -euo pipefail;
curl -fsSL {{ cv_release_url | quote }} -o {{ cv_home }}/cv.tar.gz &&
tar -xzf {{ cv_home }}/cv.tar.gz -C {{ cv_content_dir }} &&
rm -f {{ cv_home }}/cv.tar.gz
executable: /bin/bash
when: cv_installed_version != cv_version
changed_when: true
- name: Write cv version sentinel
ansible.builtin.copy:
content: "{{ cv_version }}\n"
dest: "{{ cv_version_sentinel }}"
mode: '0644'
when: cv_installed_version != cv_version

View file

@ -0,0 +1,11 @@
---
# Docs (Quartz-built static site) — replaces minikube Deployment.
# Caddy serves docs_content_dir directly via the static-kind service block,
# with Quartz-style try_files (path → path/ → path.html → 404).
docs_version: "v1.16.0"
docs_release_url: "https://forge.eblu.me/eblume/blumeops/releases/download/{{ docs_version }}/docs-{{ docs_version }}.tar.gz"
docs_home: /Users/erichblume/blumeops/docs
docs_content_dir: "{{ docs_home }}/content"
docs_version_sentinel: "{{ docs_home }}/.installed-version"

View file

@ -0,0 +1,57 @@
---
# docs role — download and extract the Quartz-built docs tarball into
# docs_content_dir. Caddy serves the directory directly with Quartz-style
# try_files; there is no daemon to manage.
#
# Idempotency: a sentinel file records the installed docs_version. The
# download/extract steps only run when the sentinel doesn't match docs_version.
#
# Mirrors the cv role's curl-based download for consistency, even though the
# forge releases endpoint here does support HEAD.
- name: Ensure docs home exists
ansible.builtin.file:
path: "{{ docs_home }}"
state: directory
mode: '0755'
- name: Read installed docs version sentinel
ansible.builtin.slurp:
src: "{{ docs_version_sentinel }}"
register: docs_installed_raw
failed_when: false
changed_when: false
- name: Set installed docs version fact
ansible.builtin.set_fact:
docs_installed_version: >-
{{ (docs_installed_raw.content | b64decode).strip()
if (docs_installed_raw.content is defined) else '' }}
- name: Recreate docs content dir
ansible.builtin.file:
path: "{{ docs_content_dir }}"
state: "{{ item }}"
mode: '0755'
loop:
- absent
- directory
when: docs_installed_version != docs_version
- name: Download and extract docs release tarball
ansible.builtin.shell:
cmd: >-
set -euo pipefail;
curl -fsSL {{ docs_release_url | quote }} -o {{ docs_home }}/docs.tar.gz &&
tar -xzf {{ docs_home }}/docs.tar.gz -C {{ docs_content_dir }} &&
rm -f {{ docs_home }}/docs.tar.gz
executable: /bin/bash
when: docs_installed_version != docs_version
changed_when: true
- name: Write docs version sentinel
ansible.builtin.copy:
content: "{{ docs_version }}\n"
dest: "{{ docs_version_sentinel }}"
mode: '0644'
when: docs_installed_version != docs_version

View file

@ -12,6 +12,10 @@
href: https://registry.ops.eblu.me href: https://registry.ops.eblu.me
icon: zot-registry icon: zot-registry
description: Container registry description: Container registry
- Devpi:
href: https://pypi.ops.eblu.me
icon: mdi-language-python
description: PyPI caching mirror
- Sifaka NAS: - Sifaka NAS:
href: https://nas.ops.eblu.me href: https://nas.ops.eblu.me
icon: synology icon: synology
@ -77,3 +81,15 @@
href: https://ntfy.ops.eblu.me href: https://ntfy.ops.eblu.me
icon: ntfy.png icon: ntfy.png
description: Push notifications description: Push notifications
- Services:
# CV and Docs were previously auto-discovered from k8s Ingresses; after
# the indri-native migration ([[cv-on-indri]], [[docs-on-indri]]) there
# is no Ingress to discover, so they live here as static entries.
- CV:
href: https://cv.eblu.me
icon: mdi-file-document
description: Resume / CV
- Docs:
href: https://docs.eblu.me
icon: mdi-book-open-page-variant
description: BlumeOps Documentation

View file

@ -0,0 +1 @@
Migrated CV (`cv.eblu.me`) and Docs (`docs.eblu.me`) from minikube Deployments to indri-native ansible roles. Caddy now serves the extracted release tarballs directly via a new `kind: static` service-block in the Caddy template — no daemon, no container — replacing the prior nginx-in-a-pod layer. Removes a network hop on every request and shrinks minikube's footprint. See [[cv-on-indri]] and [[docs-on-indri]]. Part of the broader minikube wind-down.

View file

@ -0,0 +1,72 @@
---
title: CV on Indri
modified: 2026-04-29
last-reviewed: 2026-04-29
tags:
- how-to
- operations
---
# CV on Indri
How the CV/resume static site (`cv.eblu.me`) is deployed on indri natively. Replaces the prior minikube Deployment; mirrors the rationale of [[devpi-on-indri]].
## Why native, not Kubernetes
CV is a tiny static site (HTML + CSS + PDF). It needs no daemon, no database, no auth. Caddy on indri can serve the extracted tarball directly via `file_server`. Removing the minikube Deployment shrinks the cluster's footprint and removes a network hop (Fly → indri Caddy → ProxyGroup ingress → minikube pod becomes Fly → indri Caddy → local files).
## Layout
| Concern | Path / detail |
|---|---|
| Content dir | `/Users/erichblume/blumeops/cv/content/` |
| Version sentinel | `/Users/erichblume/blumeops/cv/.installed-version` |
| Caddy entry | `cv` service in `ansible/roles/caddy/defaults/main.yml` (`kind: static`) |
| Public URL | `https://cv.eblu.me` (via [[flyio-proxy]]) |
| Private URL | `https://cv.ops.eblu.me` (Caddy on indri) |
| Tarball source | Forgejo generic package `cv` (`forge.eblu.me/eblume/-/packages`) |
The role is driven by `cv_version` in `ansible/roles/cv/defaults/main.yml`. The download and extract steps only fire when the on-disk sentinel doesn't match `cv_version` — i.e. after a version bump.
## Deploy
Two paths:
**From a release workflow** (most common):
1. Run the `Release CV` workflow in the cv repo → produces a new generic package
2. Run the blumeops `Deploy CV` workflow → bumps `cv_version` in `ansible/roles/cv/defaults/main.yml` and pushes to main
3. From gilbert: `mise run provision-indri -- --tags cv`
4. From gilbert: `fly ssh console -a blumeops-proxy -C "sh -c 'rm -rf /tmp/cache && nginx -s reload'"` to purge the public-edge cache
**Manual** (e.g., reverting): edit `cv_version` in the role defaults yourself, then steps 34.
## Verify
```fish
ssh indri 'cat ~/blumeops/cv/.installed-version'
ssh indri 'ls -la ~/blumeops/cv/content/'
curl -fsSI https://cv.ops.eblu.me/ # private
curl -fsSI https://cv.eblu.me/ # public
curl -fsSI https://cv.eblu.me/resume.pdf | grep -i disposition
```
The PDF response should include `content-disposition: attachment; filename="erich-blume-resume.pdf"`.
## Bumping the cv version
Edit `cv_version` in `ansible/roles/cv/defaults/main.yml` and re-run `mise run provision-indri -- --tags cv`. The role recreates the content dir from the new tarball; the sentinel update triggers the next idempotent skip.
## Backup
The content dir is **not** in `borgmatic_source_directories`. The tarball is re-downloadable from the Forgejo generic package store on every deploy, and the source is in the cv repo — recovery is just re-running the role.
## Rollback
If a bad version is published, set `cv_version` back to the previous tag in `ansible/roles/cv/defaults/main.yml` and re-run the role. The full minikube manifest set is preserved in git history (commits prior to the migration cleanup) for the worst case.
## Related
- [[devpi-on-indri]] — same shape, different upstream
- [[restart-indri]] — graceful indri restart procedure
- [[cv]] — service reference

View file

@ -0,0 +1,66 @@
---
title: Docs on Indri
modified: 2026-04-29
last-reviewed: 2026-04-29
tags:
- how-to
- operations
---
# Docs on Indri
How the Quartz documentation site (`docs.eblu.me`) is deployed on indri natively. Replaces the prior minikube Deployment; same shape as [[cv-on-indri]] with one extra wrinkle for Quartz's clean URLs.
## Why native, not Kubernetes
The docs site is fully static HTML produced by Quartz. Caddy can serve the extracted tarball directly. The Quartz-specific behavior the previous nginx container provided (`try_files $uri $uri/ $uri.html =404` and a custom `/404.html`) maps cleanly to Caddy's `try_files` and `handle_errors`.
## Layout
| Concern | Path / detail |
|---|---|
| Content dir | `/Users/erichblume/blumeops/docs/content/` |
| Version sentinel | `/Users/erichblume/blumeops/docs/.installed-version` |
| Caddy entry | `docs` service in `ansible/roles/caddy/defaults/main.yml` (`kind: static`, `try_html: true`) |
| Public URL | `https://docs.eblu.me` (via [[flyio-proxy]]) |
| Private URL | `https://docs.ops.eblu.me` (Caddy on indri) |
| Tarball source | Forgejo release asset on the blumeops repo (`docs-<version>.tar.gz`) |
`docs_version` in `ansible/roles/docs/defaults/main.yml` is the blumeops release tag (e.g. `v1.16.0`). The role's download/extract is gated by an on-disk sentinel.
## Deploy
1. Run the `Build BlumeOps` Forgejo workflow → builds the tarball, creates a release, bumps `docs_version` in the ansible role, pushes to main
2. From gilbert: `mise run provision-indri -- --tags docs`
3. From gilbert: `fly ssh console -a blumeops-proxy -C "sh -c 'rm -rf /tmp/cache && nginx -s reload'"`
The Caddy block uses `try_files {path} {path}/ {path}.html` and a `handle_errors 404 → /404.html` rewrite, matching the original nginx behavior so Quartz's clean URLs continue to work.
## Verify
```fish
ssh indri 'cat ~/blumeops/docs/.installed-version'
ssh indri 'ls ~/blumeops/docs/content/'
curl -fsSI https://docs.ops.eblu.me/ # private
curl -fsSI https://docs.eblu.me/ # public
curl -fsSI https://docs.eblu.me/explanation/agent-change-process # clean URL → .html fallback
curl -fsSI https://docs.eblu.me/no-such-path-exists/ # → /404.html
```
## Bumping the docs version
Normally driven by the workflow. If you need to pin manually, edit `docs_version` in `ansible/roles/docs/defaults/main.yml` and re-run `mise run provision-indri -- --tags docs`.
## Backup
Content dir is not borgmatic-backed. Source is in this repo; release tarballs are on the forge.
## Rollback
Set `docs_version` back to the previous release tag in the role defaults and re-run. Older release tarballs remain available as Forgejo release assets.
## Related
- [[cv-on-indri]] — sibling service, simpler (no `try_html`)
- [[devpi-on-indri]] — pattern reference for indri-native services
- [[docs]] — service reference

View file

@ -1,7 +1,7 @@
--- ---
title: CV title: CV
modified: 2026-03-27 modified: 2026-04-29
last-reviewed: 2026-03-27 last-reviewed: 2026-04-29
tags: tags:
- service - service
- resume - resume
@ -15,37 +15,36 @@ Personal resume/CV served as a static HTML page with PDF download, built from YA
| Property | Value | | Property | Value |
|----------|-------| |----------|-------|
| **URL** | `cv.eblu.me` (public, via [[flyio-proxy]]) | | **Public URL** | `cv.eblu.me` (via [[flyio-proxy]]) |
| **Namespace** | `cv` | | **Private URL** | `cv.ops.eblu.me` (Caddy on indri) |
| **Container** | `registry.ops.eblu.me/blumeops/cv` ([kustomization](https://forge.eblu.me/eblume/blumeops/src/branch/main/argocd/manifests/cv/kustomization.yaml)) | | **Deployment** | Ansible role `cv` on indri (no daemon — Caddy serves files directly) |
| **Content dir** | `~/blumeops/cv/content/` on indri |
| **Source repo** | `forge.eblu.me/eblume/cv` (private, not mirrored to GitHub) | | **Source repo** | `forge.eblu.me/eblume/cv` (private, not mirrored to GitHub) |
| **Content packages** | `forge.eblu.me/eblume/-/packages` (generic package `cv`) | | **Content packages** | `forge.eblu.me/eblume/-/packages` (generic package `cv`) |
| **ArgoCD App** | `cv` |
Migrated from minikube to indri-native on 2026-04-29 (see [[cv-on-indri]]).
## Architecture ## Architecture
1. **Source**: `resume.yaml` (content) + `template.html` (Jinja2) + `style.css` in the cv repo 1. **Source**: `resume.yaml` (content) + `template.html` (Jinja2) + `style.css` in the cv repo
2. **Build**: `render.py` (uv script runner) generates `index.html`; WeasyPrint generates `resume.pdf` 2. **Build**: `render.py` (uv script runner) generates `index.html`; WeasyPrint generates `resume.pdf`
3. **Release**: Dagger `build` function packages `index.html`, `style.css`, `resume.pdf` into a tarball, uploaded to Forgejo generic packages 3. **Release**: Dagger `build` function packages `index.html`, `style.css`, `resume.pdf` into a tarball, uploaded to Forgejo generic packages
4. **Deploy**: nginx container downloads the tarball at startup via `CV_RELEASE_URL` env var 4. **Deploy**: ansible role downloads the tarball into `~/blumeops/cv/content/` on indri; Caddy serves the directory directly
## Endpoints ## Endpoints
| Path | Description | | Path | Description |
|------|-------------| |------|-------------|
| `/` | Resume HTML page | | `/` | Resume HTML page |
| `/resume.pdf` | PDF download (Content-Disposition: attachment) | | `/resume.pdf` | PDF download (Caddy adds `Content-Disposition: attachment`) |
| `/healthz` | Health check (200 OK) |
## Configuration ## Configuration
**Key files (blumeops):** **Key files (blumeops):**
- `containers/cv/Dockerfile` — nginx:alpine container - `ansible/roles/cv/defaults/main.yml` — pinned `cv_version` and tarball URL
- `containers/cv/start.sh` — tarball download + extraction - `ansible/roles/cv/tasks/main.yml` — sentinel-gated download + extract
- `containers/cv/default.conf` — nginx config (gzip, caching, PDF headers) - `ansible/roles/caddy/defaults/main.yml``cv` service entry (`kind: static`, `download_paths` for the PDF)
- `argocd/manifests/cv/deployment.yaml``CV_RELEASE_URL` env var
- `argocd/apps/cv.yaml` — ArgoCD Application
**Key files (cv repo):** **Key files (cv repo):**
@ -56,17 +55,15 @@ Personal resume/CV served as a static HTML page with PDF download, built from YA
- `src/cv_ci/main.py` — Dagger pipeline (alpine + uv + WeasyPrint) - `src/cv_ci/main.py` — Dagger pipeline (alpine + uv + WeasyPrint)
- `.forgejo/workflows/cv-release.yaml` — Release workflow - `.forgejo/workflows/cv-release.yaml` — Release workflow
## Secrets ## Release flow
| Secret | Repo | Source | Description | 1. Release a new package from the cv repo (`Release CV` workflow)
|--------|------|--------|-------------| 2. Run the blumeops `Deploy CV` workflow → bumps `cv_version` in the ansible role and pushes
| `FORGE_TOKEN` | cv | 1Password (via Ansible) | Forgejo API token for package uploads | 3. Run `mise run provision-indri -- --tags cv` from gilbert
4. Purge the Fly.io proxy cache so the new content is fetched
Provisioned via `forgejo_actions_secrets` Ansible role. See [[create-release-artifact-workflow]].
## Related ## Related
- [[docs]] — Similar architecture (nginx container + content tarball) - [[cv-on-indri]] — Operations how-to
- [[docs]] — Similar architecture (Caddy serving a tarball-extracted dir)
- [[flyio-proxy]] — Exposes `cv.eblu.me` publicly via Tailscale tunnel - [[flyio-proxy]] — Exposes `cv.eblu.me` publicly via Tailscale tunnel
- [[create-release-artifact-workflow]] — How to set up release artifact workflows
- [[deploy-k8s-service]] — General k8s deployment guide

View file

@ -1,7 +1,7 @@
--- ---
title: Docs title: Docs
modified: 2026-03-23 modified: 2026-04-29
last-reviewed: 2026-03-23 last-reviewed: 2026-04-29
tags: tags:
- service - service
- documentation - documentation
@ -9,44 +9,42 @@ tags:
# Docs (Quartz) # Docs (Quartz)
Documentation site built with [Quartz](https://quartz.jzhao.xyz/) and served via nginx. Documentation site built with [Quartz](https://quartz.jzhao.xyz/).
## Quick Reference ## Quick Reference
| Property | Value | | Property | Value |
|----------|-------| |----------|-------|
| **Public URL** | https://docs.eblu.me | | **Public URL** | https://docs.eblu.me (via [[flyio-proxy]]) |
| **Private URL** | `docs.ops.eblu.me` (tailnet only, via [[caddy]]) | | **Private URL** | `docs.ops.eblu.me` (Caddy on indri) |
| **Namespace** | `docs` | | **Deployment** | Ansible role `docs` on indri (no daemon — Caddy serves files directly) |
| **Image** | `registry.ops.eblu.me/blumeops/quartz` (see `argocd/manifests/docs/kustomization.yaml` for current tag) | | **Content dir** | `~/blumeops/docs/content/` on indri |
| **Source** | `docs/` directory in blumeops repo | | **Source** | `docs/` directory in blumeops repo |
| **Build** | Forgejo workflow `build-blumeops.yaml` | | **Build** | Forgejo workflow `build-blumeops.yaml` |
| **Public proxy** | [[flyio-proxy]] (Fly.io → Tailscale tunnel) |
Migrated from minikube to indri-native on 2026-04-29 (see [[docs-on-indri]]).
## Architecture ## Architecture
1. **Source**: Markdown files in `docs/` with Obsidian-compatible wiki-links 1. **Source**: Markdown files in `docs/` with Obsidian-compatible wiki-links
2. **Build**: Forgejo workflow builds Quartz static site on push to main 2. **Build**: `Build BlumeOps` Forgejo workflow runs towncrier + Quartz, uploads tarball as a release asset, and bumps `docs_version` in the ansible role
3. **Release**: Built assets published as Forgejo release attachments 3. **Deploy**: ansible role downloads the tarball into `~/blumeops/docs/content/` on indri; Caddy serves the directory directly with Quartz-style `try_files` (path → path/ → path.html → 404.html)
4. **Deploy**: Container downloads release bundle on startup, serves via nginx
## Release Process
Documentation is built and released via the `build-blumeops` Forgejo workflow (manual dispatch):
1. Quartz builds static HTML/CSS/JS
2. Assets uploaded as Forgejo release attachment
3. Workflow updates `DOCS_RELEASE_URL` in `argocd/manifests/docs/deployment.yaml` and commits to main
4. ArgoCD syncs the updated deployment; new pod downloads the release bundle at startup
## Configuration ## Configuration
- **Quartz config**: `quartz.config.ts` - **Quartz config**: `quartz.config.ts`
- **Layout**: `quartz.layout.ts` - **Layout**: `quartz.layout.ts`
- **ArgoCD app**: `argocd/apps/docs.yaml` - **Ansible role**: `ansible/roles/docs/`
- **Manifests**: `argocd/manifests/docs/` - **Caddy entry**: `ansible/roles/caddy/defaults/main.yml` (`kind: static`, `try_html: true`)
## Release flow
1. Run the `Build BlumeOps` workflow → builds tarball, creates release, bumps `docs_version` in the ansible role and pushes
2. Run `mise run provision-indri -- --tags docs` from gilbert
3. Purge the Fly.io proxy cache so the new content is fetched
## Related ## Related
- [[argocd]] - Deployment management - [[docs-on-indri]] — Operations how-to
- [[forgejo]] - Build workflows - [[cv]] — Similar architecture
- [[forgejo]] — Build workflows

View file

@ -221,18 +221,26 @@ services:
notes: Installed via uv into a venv on indri; version pinned in ansible/roles/devpi/defaults/main.yml notes: Installed via uv into a venv on indri; version pinned in ansible/roles/devpi/defaults/main.yml
- name: cv - name: cv
type: argocd type: ansible
last-reviewed: 2026-04-27 last-reviewed: 2026-04-29
current-version: "1.0.3" current-version: "1.0.3"
upstream-source: https://forge.eblu.me/eblume/cv upstream-source: https://forge.eblu.me/eblume/cv
notes: Personal static site; review build deps (WeasyPrint, Jinja2) in source repo notes: >-
Static tarball downloaded by ansible/roles/cv into ~/blumeops/cv/content on indri;
served directly by Caddy (kind=static). Migrated from minikube 2026-04-29.
Review build deps (WeasyPrint, Jinja2) in source repo on upstream review.
- name: docs - name: docs
type: argocd type: ansible
last-reviewed: 2026-03-07 last-reviewed: 2026-04-29
current-version: "1.28.2" current-version: "1.28.2"
upstream-source: https://github.com/jackyzha0/quartz/releases upstream-source: https://forge.eblu.me/eblume/blumeops/releases
notes: Quartz static site generator; container version tracks nginx base notes: >-
Quartz-built tarball downloaded by ansible/roles/docs into ~/blumeops/docs/content
on indri; served directly by Caddy (kind=static, try_html). Migrated from
minikube 2026-04-29. current-version still tracks the legacy quartz/nginx
base; will switch to the docs release tag (e.g. v1.16.0) once the dead
containers/quartz Dockerfile is removed in the cleanup commit.
- name: forgejo-runner - name: forgejo-runner
type: argocd type: argocd