From 930f99d70687172dd440f82fb0e5c6a51bca8374 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Wed, 29 Apr 2026 14:33:57 -0700 Subject: [PATCH 1/3] C1: migrate cv + docs from minikube to indri-native (deploy artifacts) Replaces the cv and docs minikube Deployments with ansible roles that download release tarballs into ~/cv/content and ~/docs/content on indri. Caddy now serves those directories directly via a new kind=static service-block in the Caddy template; no daemon, no nginx pod, no ProxyGroup ingress on the request path. This commit adds the deploy-side artifacts only. Live cutover (delete argocd apps, run ansible, verify) is staged manually after PR review; the dead containers/{cv,quartz} and argocd manifests are removed in a follow-up commit so each commit is internally consistent. Workflows are simplified: the deploy step now bumps the role's pinned version and pushes; running ansible + purging the Fly cache is manual from gilbert (matches the devpi pattern). service-versions.yaml: cv and docs are type=ansible. docs current-version remains 1.28.2 for now to keep container-version-check passing while containers/quartz still exists; will move to the docs release tag in the cleanup commit. Co-Authored-By: Claude Opus 4.7 (1M context) --- .forgejo/workflows/build-blumeops.yaml | 58 +++++---------- .forgejo/workflows/cv-deploy.yaml | 58 +++++---------- ansible/playbooks/indri.yml | 4 ++ ansible/roles/caddy/defaults/main.yml | 10 ++- ansible/roles/caddy/templates/Caddyfile.j2 | 20 ++++++ ansible/roles/cv/defaults/main.yml | 10 +++ ansible/roles/cv/tasks/main.yml | 57 +++++++++++++++ ansible/roles/docs/defaults/main.yml | 11 +++ ansible/roles/docs/tasks/main.yml | 57 +++++++++++++++ .../migrate-cv-docs-to-indri.infra.md | 1 + docs/how-to/operations/cv-on-indri.md | 72 +++++++++++++++++++ docs/how-to/operations/docs-on-indri.md | 66 +++++++++++++++++ docs/reference/services/cv.md | 43 ++++++----- docs/reference/services/docs.md | 46 ++++++------ service-versions.yaml | 22 ++++-- 15 files changed, 399 insertions(+), 136 deletions(-) create mode 100644 ansible/roles/cv/defaults/main.yml create mode 100644 ansible/roles/cv/tasks/main.yml create mode 100644 ansible/roles/docs/defaults/main.yml create mode 100644 ansible/roles/docs/tasks/main.yml create mode 100644 docs/changelog.d/migrate-cv-docs-to-indri.infra.md create mode 100644 docs/how-to/operations/cv-on-indri.md create mode 100644 docs/how-to/operations/docs-on-indri.md diff --git a/.forgejo/workflows/build-blumeops.yaml b/.forgejo/workflows/build-blumeops.yaml index 383542f..c6e6c3c 100644 --- a/.forgejo/workflows/build-blumeops.yaml +++ b/.forgejo/workflows/build-blumeops.yaml @@ -178,10 +178,11 @@ jobs: echo "## Documentation" 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 "DOCS_RELEASE_URL=https://forge.eblu.me/eblume/blumeops/releases/download/$VERSION/$TARBALL" + echo "mise run provision-indri -- --tags docs" echo "\`\`\`" } > /tmp/release_body.txt @@ -223,18 +224,16 @@ jobs: echo "" echo "Release created successfully!" - - name: Update docs deployment + - name: Bump docs_version in ansible role run: | VERSION="${{ steps.version.outputs.version }}" - TARBALL="docs-${VERSION}.tar.gz" - DEPLOYMENT_FILE="argocd/manifests/docs/deployment.yaml" - RELEASE_URL="https://forge.eblu.me/eblume/blumeops/releases/download/${VERSION}/${TARBALL}" + DEFAULTS_FILE="ansible/roles/docs/defaults/main.yml" - echo "Updating $DEPLOYMENT_FILE with new release URL..." - yq -i "(.spec.template.spec.containers[0].env[] | select(.name == \"DOCS_RELEASE_URL\")).value = \"${RELEASE_URL}\"" "$DEPLOYMENT_FILE" + echo "Bumping docs_version in $DEFAULTS_FILE to ${VERSION}..." + yq -i ".docs_version = \"${VERSION}\"" "$DEFAULTS_FILE" - echo "Updated deployment:" - grep -A1 "DOCS_RELEASE_URL" "$DEPLOYMENT_FILE" + echo "Updated defaults:" + grep -E "^docs_version:" "$DEFAULTS_FILE" - name: Commit release changes env: @@ -248,7 +247,7 @@ jobs: git config user.email "actions@forge.ops.eblu.me" # Stage deployment changes - git add argocd/manifests/docs/deployment.yaml + git add ansible/roles/docs/defaults/main.yml # Stage changelog changes if updated if [ "$CHANGELOG_UPDATED" = "true" ]; then @@ -270,34 +269,6 @@ jobs: echo "Changes committed and pushed" 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 run: | VERSION="${{ steps.version.outputs.version }}" @@ -309,5 +280,12 @@ jobs: echo "Release URL:" echo " https://forge.eblu.me/eblume/blumeops/releases/tag/$VERSION" echo "" - echo "Asset URL (for DOCS_RELEASE_URL ConfigMap):" + echo "Asset URL:" 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'\"" diff --git a/.forgejo/workflows/cv-deploy.yaml b/.forgejo/workflows/cv-deploy.yaml index f99352d..001aa36 100644 --- a/.forgejo/workflows/cv-deploy.yaml +++ b/.forgejo/workflows/cv-deploy.yaml @@ -1,12 +1,14 @@ # CV Deploy Workflow # -# Updates the CV deployment to a specific package version, commits -# the change, and syncs via ArgoCD. +# Bumps cv_version in ansible/roles/cv/defaults/main.yml and pushes the change. +# Deployment to indri is manual (runner has no SSH access to indri): +# mise run provision-indri -- --tags cv # # Usage: # 1. Release a new CV package from the cv repo first # 2. Go to Actions > Deploy CV > Run workflow # 3. Enter the version to deploy, or leave as "latest" +# 4. Run the command above on gilbert to apply name: Deploy CV @@ -60,18 +62,16 @@ jobs: - name: Checkout uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 - - name: Update CV deployment + - name: Bump cv_version in ansible role run: | VERSION="${{ steps.version.outputs.version }}" - TARBALL="cv-${VERSION}.tar.gz" - DEPLOYMENT_FILE="argocd/manifests/cv/deployment.yaml" - RELEASE_URL="https://forge.eblu.me/api/packages/eblume/generic/cv/${VERSION}/${TARBALL}" + DEFAULTS_FILE="ansible/roles/cv/defaults/main.yml" - echo "Updating $DEPLOYMENT_FILE with CV_RELEASE_URL..." - yq -i "(.spec.template.spec.containers[0].env[] | select(.name == \"CV_RELEASE_URL\")).value = \"${RELEASE_URL}\"" "$DEPLOYMENT_FILE" + echo "Bumping cv_version in $DEFAULTS_FILE to ${VERSION}..." + yq -i ".cv_version = \"${VERSION}\"" "$DEFAULTS_FILE" - echo "Updated deployment:" - grep -A1 "CV_RELEASE_URL" "$DEPLOYMENT_FILE" + echo "Updated defaults:" + grep -E "^cv_version:" "$DEFAULTS_FILE" - name: Commit release changes env: @@ -82,7 +82,7 @@ jobs: git config user.name "Forgejo Actions" 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 echo "No changes to commit (already at $VERSION)" @@ -94,38 +94,16 @@ jobs: echo "Changes committed and pushed" 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 run: | VERSION="${{ steps.version.outputs.version }}" echo "================================================" - echo "CV Deployed: $VERSION" + echo "CV version bumped: $VERSION" echo "================================================" echo "" - echo "CV should now be live at:" - echo " https://cv.ops.eblu.me/" + echo "To deploy on indri, run from gilbert:" + 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'\"" diff --git a/ansible/playbooks/indri.yml b/ansible/playbooks/indri.yml index fa87b36..ddb57f8 100644 --- a/ansible/playbooks/indri.yml +++ b/ansible/playbooks/indri.yml @@ -256,5 +256,9 @@ tags: jellyfin_metrics - role: forgejo_metrics tags: forgejo_metrics + - role: cv + tags: cv + - role: docs + tags: docs - role: caddy tags: caddy diff --git a/ansible/roles/caddy/defaults/main.yml b/ansible/roles/caddy/defaults/main.yml index 80993ee..6eada76 100644 --- a/ansible/roles/caddy/defaults/main.yml +++ b/ansible/roles/caddy/defaults/main.yml @@ -72,10 +72,16 @@ caddy_services: backend: "https://go.tail8d86e.ts.net" - name: docs 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 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 host: "nvr.{{ caddy_domain }}" backend: "https://nvr.tail8d86e.ts.net" diff --git a/ansible/roles/caddy/templates/Caddyfile.j2 b/ansible/roles/caddy/templates/Caddyfile.j2 index 4f103f1..b08f16a 100644 --- a/ansible/roles/caddy/templates/Caddyfile.j2 +++ b/ansible/roles/caddy/templates/Caddyfile.j2 @@ -31,6 +31,25 @@ {% for service in caddy_services %} @{{ service.name }} host {{ service.host }} 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' %} # SPA cache policy: hashed static assets are immutable, HTML must revalidate. # Prevents stale HTML from referencing chunk hashes that no longer exist. @@ -47,6 +66,7 @@ } {% else %} reverse_proxy {{ service.backend }} +{% endif %} {% endif %} } diff --git a/ansible/roles/cv/defaults/main.yml b/ansible/roles/cv/defaults/main.yml new file mode 100644 index 0000000..6a00eb5 --- /dev/null +++ b/ansible/roles/cv/defaults/main.yml @@ -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/cv +cv_content_dir: "{{ cv_home }}/content" +cv_version_sentinel: "{{ cv_home }}/.installed-version" diff --git a/ansible/roles/cv/tasks/main.yml b/ansible/roles/cv/tasks/main.yml new file mode 100644 index 0000000..c254325 --- /dev/null +++ b/ansible/roles/cv/tasks/main.yml @@ -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 diff --git a/ansible/roles/docs/defaults/main.yml b/ansible/roles/docs/defaults/main.yml new file mode 100644 index 0000000..cbddd1c --- /dev/null +++ b/ansible/roles/docs/defaults/main.yml @@ -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/docs +docs_content_dir: "{{ docs_home }}/content" +docs_version_sentinel: "{{ docs_home }}/.installed-version" diff --git a/ansible/roles/docs/tasks/main.yml b/ansible/roles/docs/tasks/main.yml new file mode 100644 index 0000000..dec775e --- /dev/null +++ b/ansible/roles/docs/tasks/main.yml @@ -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 diff --git a/docs/changelog.d/migrate-cv-docs-to-indri.infra.md b/docs/changelog.d/migrate-cv-docs-to-indri.infra.md new file mode 100644 index 0000000..608a6b9 --- /dev/null +++ b/docs/changelog.d/migrate-cv-docs-to-indri.infra.md @@ -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. diff --git a/docs/how-to/operations/cv-on-indri.md b/docs/how-to/operations/cv-on-indri.md new file mode 100644 index 0000000..cb40eea --- /dev/null +++ b/docs/how-to/operations/cv-on-indri.md @@ -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/cv/content/` | +| Version sentinel | `/Users/erichblume/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 3–4. + +## Verify + +```fish +ssh indri 'cat ~/cv/.installed-version' +ssh indri 'ls -la ~/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 diff --git a/docs/how-to/operations/docs-on-indri.md b/docs/how-to/operations/docs-on-indri.md new file mode 100644 index 0000000..3351e2e --- /dev/null +++ b/docs/how-to/operations/docs-on-indri.md @@ -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/docs/content/` | +| Version sentinel | `/Users/erichblume/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-.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 ~/docs/.installed-version' +ssh indri 'ls ~/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 diff --git a/docs/reference/services/cv.md b/docs/reference/services/cv.md index 55805d6..adbaeaa 100644 --- a/docs/reference/services/cv.md +++ b/docs/reference/services/cv.md @@ -1,7 +1,7 @@ --- title: CV -modified: 2026-03-27 -last-reviewed: 2026-03-27 +modified: 2026-04-29 +last-reviewed: 2026-04-29 tags: - service - resume @@ -15,37 +15,36 @@ Personal resume/CV served as a static HTML page with PDF download, built from YA | Property | Value | |----------|-------| -| **URL** | `cv.eblu.me` (public, via [[flyio-proxy]]) | -| **Namespace** | `cv` | -| **Container** | `registry.ops.eblu.me/blumeops/cv` ([kustomization](https://forge.eblu.me/eblume/blumeops/src/branch/main/argocd/manifests/cv/kustomization.yaml)) | +| **Public URL** | `cv.eblu.me` (via [[flyio-proxy]]) | +| **Private URL** | `cv.ops.eblu.me` (Caddy on indri) | +| **Deployment** | Ansible role `cv` on indri (no daemon — Caddy serves files directly) | +| **Content dir** | `~/cv/content/` on indri | | **Source repo** | `forge.eblu.me/eblume/cv` (private, not mirrored to GitHub) | | **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 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` 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 `~/cv/content/` on indri; Caddy serves the directory directly ## Endpoints | Path | Description | |------|-------------| | `/` | Resume HTML page | -| `/resume.pdf` | PDF download (Content-Disposition: attachment) | -| `/healthz` | Health check (200 OK) | +| `/resume.pdf` | PDF download (Caddy adds `Content-Disposition: attachment`) | ## Configuration **Key files (blumeops):** -- `containers/cv/Dockerfile` — nginx:alpine container -- `containers/cv/start.sh` — tarball download + extraction -- `containers/cv/default.conf` — nginx config (gzip, caching, PDF headers) -- `argocd/manifests/cv/deployment.yaml` — `CV_RELEASE_URL` env var -- `argocd/apps/cv.yaml` — ArgoCD Application +- `ansible/roles/cv/defaults/main.yml` — pinned `cv_version` and tarball URL +- `ansible/roles/cv/tasks/main.yml` — sentinel-gated download + extract +- `ansible/roles/caddy/defaults/main.yml` — `cv` service entry (`kind: static`, `download_paths` for the PDF) **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) - `.forgejo/workflows/cv-release.yaml` — Release workflow -## Secrets +## Release flow -| Secret | Repo | Source | Description | -|--------|------|--------|-------------| -| `FORGE_TOKEN` | cv | 1Password (via Ansible) | Forgejo API token for package uploads | - -Provisioned via `forgejo_actions_secrets` Ansible role. See [[create-release-artifact-workflow]]. +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 +3. Run `mise run provision-indri -- --tags cv` from gilbert +4. Purge the Fly.io proxy cache so the new content is fetched ## 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 -- [[create-release-artifact-workflow]] — How to set up release artifact workflows -- [[deploy-k8s-service]] — General k8s deployment guide diff --git a/docs/reference/services/docs.md b/docs/reference/services/docs.md index 1361d02..1274b0f 100644 --- a/docs/reference/services/docs.md +++ b/docs/reference/services/docs.md @@ -1,7 +1,7 @@ --- title: Docs -modified: 2026-03-23 -last-reviewed: 2026-03-23 +modified: 2026-04-29 +last-reviewed: 2026-04-29 tags: - service - documentation @@ -9,44 +9,42 @@ tags: # 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 | Property | Value | |----------|-------| -| **Public URL** | https://docs.eblu.me | -| **Private URL** | `docs.ops.eblu.me` (tailnet only, via [[caddy]]) | -| **Namespace** | `docs` | -| **Image** | `registry.ops.eblu.me/blumeops/quartz` (see `argocd/manifests/docs/kustomization.yaml` for current tag) | +| **Public URL** | https://docs.eblu.me (via [[flyio-proxy]]) | +| **Private URL** | `docs.ops.eblu.me` (Caddy on indri) | +| **Deployment** | Ansible role `docs` on indri (no daemon — Caddy serves files directly) | +| **Content dir** | `~/docs/content/` on indri | | **Source** | `docs/` directory in blumeops repo | | **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 1. **Source**: Markdown files in `docs/` with Obsidian-compatible wiki-links -2. **Build**: Forgejo workflow builds Quartz static site on push to main -3. **Release**: Built assets published as Forgejo release attachments -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 +2. **Build**: `Build BlumeOps` Forgejo workflow runs towncrier + Quartz, uploads tarball as a release asset, and bumps `docs_version` in the ansible role +3. **Deploy**: ansible role downloads the tarball into `~/docs/content/` on indri; Caddy serves the directory directly with Quartz-style `try_files` (path → path/ → path.html → 404.html) ## Configuration - **Quartz config**: `quartz.config.ts` - **Layout**: `quartz.layout.ts` -- **ArgoCD app**: `argocd/apps/docs.yaml` -- **Manifests**: `argocd/manifests/docs/` +- **Ansible role**: `ansible/roles/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 -- [[argocd]] - Deployment management -- [[forgejo]] - Build workflows +- [[docs-on-indri]] — Operations how-to +- [[cv]] — Similar architecture +- [[forgejo]] — Build workflows diff --git a/service-versions.yaml b/service-versions.yaml index e819c6c..fd0416f 100644 --- a/service-versions.yaml +++ b/service-versions.yaml @@ -221,18 +221,26 @@ services: notes: Installed via uv into a venv on indri; version pinned in ansible/roles/devpi/defaults/main.yml - name: cv - type: argocd - last-reviewed: 2026-04-27 + type: ansible + last-reviewed: 2026-04-29 current-version: "1.0.3" 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 ~/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 - type: argocd - last-reviewed: 2026-03-07 + type: ansible + last-reviewed: 2026-04-29 current-version: "1.28.2" - upstream-source: https://github.com/jackyzha0/quartz/releases - notes: Quartz static site generator; container version tracks nginx base + upstream-source: https://forge.eblu.me/eblume/blumeops/releases + notes: >- + Quartz-built tarball downloaded by ansible/roles/docs into ~/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 type: argocd -- 2.50.1 (Apple Git-155) From 9a81bc4633d88a35b337ce8efac8f3107df646ec Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Wed, 29 Apr 2026 14:42:48 -0700 Subject: [PATCH 2/3] homepage: add cv, docs, devpi as static entries cv and docs were auto-discovered by homepage's k8s integration via gethomepage.dev/* annotations on their Ingresses. After the indri-native migration the Ingresses are gone, so add static entries that link to the public URLs. devpi was never on the homepage; adding it under Host Services next to the other indri-native infra (Forgejo, Registry). The static entries lose pod-status indicators (no pod to watch) but stay clickable. During the migration window the auto-discovered entries and the static entries will both render briefly; once the cv/docs Ingresses are deleted, only the static entries remain. Co-Authored-By: Claude Opus 4.7 (1M context) --- argocd/manifests/homepage/services.yaml | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/argocd/manifests/homepage/services.yaml b/argocd/manifests/homepage/services.yaml index 58b8bb7..211e043 100644 --- a/argocd/manifests/homepage/services.yaml +++ b/argocd/manifests/homepage/services.yaml @@ -12,6 +12,10 @@ href: https://registry.ops.eblu.me icon: zot-registry description: Container registry + - Devpi: + href: https://pypi.ops.eblu.me + icon: mdi-language-python + description: PyPI caching mirror - Sifaka NAS: href: https://nas.ops.eblu.me icon: synology @@ -77,3 +81,15 @@ href: https://ntfy.ops.eblu.me icon: ntfy.png 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 -- 2.50.1 (Apple Git-155) From 5b52f18356cb0b7aa03846c0b5bb16d74c3c9e30 Mon Sep 17 00:00:00 2001 From: Erich Blume Date: Wed, 29 Apr 2026 14:54:01 -0700 Subject: [PATCH 3/3] cv+docs: relocate content under ~/blumeops/{cv,docs} Keeps blumeops-managed state grouped under a single namespace in the home dir rather than scattered top-level dirs. Caddy block paths are derived from cv_content_dir / docs_content_dir, so the role-defaults edit propagates automatically. Validated end-to-end on indri: tarballs extracted to the new paths, sentinels written, second run is idempotent. Old ~/cv and ~/docs from the earlier validation run were removed. Co-Authored-By: Claude Opus 4.7 (1M context) --- ansible/roles/cv/defaults/main.yml | 2 +- ansible/roles/docs/defaults/main.yml | 2 +- docs/how-to/operations/cv-on-indri.md | 8 ++++---- docs/how-to/operations/docs-on-indri.md | 8 ++++---- docs/reference/services/cv.md | 4 ++-- docs/reference/services/docs.md | 4 ++-- service-versions.yaml | 4 ++-- 7 files changed, 16 insertions(+), 16 deletions(-) diff --git a/ansible/roles/cv/defaults/main.yml b/ansible/roles/cv/defaults/main.yml index 6a00eb5..734e52b 100644 --- a/ansible/roles/cv/defaults/main.yml +++ b/ansible/roles/cv/defaults/main.yml @@ -5,6 +5,6 @@ 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/cv +cv_home: /Users/erichblume/blumeops/cv cv_content_dir: "{{ cv_home }}/content" cv_version_sentinel: "{{ cv_home }}/.installed-version" diff --git a/ansible/roles/docs/defaults/main.yml b/ansible/roles/docs/defaults/main.yml index cbddd1c..f09221b 100644 --- a/ansible/roles/docs/defaults/main.yml +++ b/ansible/roles/docs/defaults/main.yml @@ -6,6 +6,6 @@ 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/docs +docs_home: /Users/erichblume/blumeops/docs docs_content_dir: "{{ docs_home }}/content" docs_version_sentinel: "{{ docs_home }}/.installed-version" diff --git a/docs/how-to/operations/cv-on-indri.md b/docs/how-to/operations/cv-on-indri.md index cb40eea..432acab 100644 --- a/docs/how-to/operations/cv-on-indri.md +++ b/docs/how-to/operations/cv-on-indri.md @@ -19,8 +19,8 @@ CV is a tiny static site (HTML + CSS + PDF). It needs no daemon, no database, no | Concern | Path / detail | |---|---| -| Content dir | `/Users/erichblume/cv/content/` | -| Version sentinel | `/Users/erichblume/cv/.installed-version` | +| 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) | @@ -44,8 +44,8 @@ Two paths: ## Verify ```fish -ssh indri 'cat ~/cv/.installed-version' -ssh indri 'ls -la ~/cv/content/' +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 diff --git a/docs/how-to/operations/docs-on-indri.md b/docs/how-to/operations/docs-on-indri.md index 3351e2e..e683db5 100644 --- a/docs/how-to/operations/docs-on-indri.md +++ b/docs/how-to/operations/docs-on-indri.md @@ -19,8 +19,8 @@ The docs site is fully static HTML produced by Quartz. Caddy can serve the extra | Concern | Path / detail | |---|---| -| Content dir | `/Users/erichblume/docs/content/` | -| Version sentinel | `/Users/erichblume/docs/.installed-version` | +| 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) | @@ -39,8 +39,8 @@ The Caddy block uses `try_files {path} {path}/ {path}.html` and a `handle_errors ## Verify ```fish -ssh indri 'cat ~/docs/.installed-version' -ssh indri 'ls ~/docs/content/' +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 diff --git a/docs/reference/services/cv.md b/docs/reference/services/cv.md index adbaeaa..1bc5f15 100644 --- a/docs/reference/services/cv.md +++ b/docs/reference/services/cv.md @@ -18,7 +18,7 @@ Personal resume/CV served as a static HTML page with PDF download, built from YA | **Public URL** | `cv.eblu.me` (via [[flyio-proxy]]) | | **Private URL** | `cv.ops.eblu.me` (Caddy on indri) | | **Deployment** | Ansible role `cv` on indri (no daemon — Caddy serves files directly) | -| **Content dir** | `~/cv/content/` on indri | +| **Content dir** | `~/blumeops/cv/content/` on indri | | **Source repo** | `forge.eblu.me/eblume/cv` (private, not mirrored to GitHub) | | **Content packages** | `forge.eblu.me/eblume/-/packages` (generic package `cv`) | @@ -29,7 +29,7 @@ Migrated from minikube to indri-native on 2026-04-29 (see [[cv-on-indri]]). 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` 3. **Release**: Dagger `build` function packages `index.html`, `style.css`, `resume.pdf` into a tarball, uploaded to Forgejo generic packages -4. **Deploy**: ansible role downloads the tarball into `~/cv/content/` on indri; Caddy serves the directory directly +4. **Deploy**: ansible role downloads the tarball into `~/blumeops/cv/content/` on indri; Caddy serves the directory directly ## Endpoints diff --git a/docs/reference/services/docs.md b/docs/reference/services/docs.md index 1274b0f..8ca8310 100644 --- a/docs/reference/services/docs.md +++ b/docs/reference/services/docs.md @@ -18,7 +18,7 @@ Documentation site built with [Quartz](https://quartz.jzhao.xyz/). | **Public URL** | https://docs.eblu.me (via [[flyio-proxy]]) | | **Private URL** | `docs.ops.eblu.me` (Caddy on indri) | | **Deployment** | Ansible role `docs` on indri (no daemon — Caddy serves files directly) | -| **Content dir** | `~/docs/content/` on indri | +| **Content dir** | `~/blumeops/docs/content/` on indri | | **Source** | `docs/` directory in blumeops repo | | **Build** | Forgejo workflow `build-blumeops.yaml` | @@ -28,7 +28,7 @@ Migrated from minikube to indri-native on 2026-04-29 (see [[docs-on-indri]]). 1. **Source**: Markdown files in `docs/` with Obsidian-compatible wiki-links 2. **Build**: `Build BlumeOps` Forgejo workflow runs towncrier + Quartz, uploads tarball as a release asset, and bumps `docs_version` in the ansible role -3. **Deploy**: ansible role downloads the tarball into `~/docs/content/` on indri; Caddy serves the directory directly with Quartz-style `try_files` (path → path/ → path.html → 404.html) +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) ## Configuration diff --git a/service-versions.yaml b/service-versions.yaml index fd0416f..d77fa13 100644 --- a/service-versions.yaml +++ b/service-versions.yaml @@ -226,7 +226,7 @@ services: current-version: "1.0.3" upstream-source: https://forge.eblu.me/eblume/cv notes: >- - Static tarball downloaded by ansible/roles/cv into ~/cv/content on indri; + 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. @@ -236,7 +236,7 @@ services: current-version: "1.28.2" upstream-source: https://forge.eblu.me/eblume/blumeops/releases notes: >- - Quartz-built tarball downloaded by ansible/roles/docs into ~/docs/content + 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 -- 2.50.1 (Apple Git-155)