Remove iCloud Photos from borgmatic backup (#100)

## Summary
- Remove ~/Pictures from borgmatic source directories
- Update borgmatic and backup policy documentation
- Add Sifaka-Native Data section to clarify that photos (via Immich), music (via Navidrome), and video (via Jellyfin) are stored directly on Sifaka

## Deployment and Testing
- [ ] Run `mise run provision-indri -- --tags borgmatic --check --diff` to preview changes
- [ ] Run `mise run provision-indri -- --tags borgmatic` to apply
- [ ] Verify borgmatic config no longer includes ~/Pictures

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Reviewed-on: https://forge.ops.eblu.me/eblume/blumeops/pulls/100
This commit is contained in:
Erich Blume 2026-02-04 07:09:28 -08:00
commit 72f9f21d46
9 changed files with 78 additions and 148 deletions

194
CLAUDE.md
View file

@ -1,181 +1,95 @@
# CLAUDE.md # CLAUDE.md
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. Guidance for Claude Code working in this repository. See also [[ai-assistance-guide]].
## Project Overview ## Overview
blumeops is Erich Blume's GitOps repository for personal infrastructure management, orchestrated via tailnet `tail8d86e.ts.net`. blumeops is Erich Blume's GitOps repository for personal infrastructure, orchestrated via tailnet `tail8d86e.ts.net`.
**Critical: This repository is published publicly at https://github.com/eblume/blumeops, so never include any secrets!** **CRITICAL: Public repo at github.com/eblume/blumeops - never commit secrets!**
## Rules ## Rules
1. **CRITICAL: Always use `--context=minikube-indri` with kubectl commands.** The user has work contexts configured that must never be touched. Every kubectl command must explicitly specify the context to prevent accidental operations against the wrong cluster. 1. **Always use `--context=minikube-indri` with kubectl** - work contexts must never be touched
2. **Run `mise run zk-docs -- --style=header --color=never --decorations=always` at session start**
2. At the start of every session, even if the user asked to do something else, run `mise run zk-docs -- --style=header --color=never --decorations=always` to prime your context with key BlumeOps documentation. The docs are hosted at https://docs.ops.eblu.me and source lives in `docs/`. 3. **Feature branches only** - checkout main, pull, create branch, commit often
4. **Create PRs via `tea pr create`** - user reviews before deploy, merges after
3. When making any changes, start by making sure you're on the `main` git branch and up-to-date, and then create a feature branch. Commit often while working, and create a PR using: 5. **Check PR comments with `mise run pr-comments <pr_number>`** before proceeding
```fish 6. **Add changelog fragments** - `docs/changelog.d/<branch>.<type>.md`
tea pr create --title "Description of change" --description "$(cat <<'EOF' Types: `feature`, `bugfix`, `infra`, `doc`, `ai`, `misc`
## Summary 7. **Test before applying** - dry runs (`--check --diff`), syntax checks, `ssh indri '...'`
- First change 8. **Wait for user review before deploying**
- Second change 9. **Never merge PRs or push to main without explicit request**
10. **Verify deployments** - `mise run indri-services-check`
## Deployment and Testing
- [x] Done thing one
- [ ] Needed thing two
🤖 Generated with [Claude Code](https://claude.com/claude-code)
EOF
)"
```
The user will review your work as you go, and will merge the PR as the last step in the process, even after deploying. After the user reviews the PR and leaves comments, check for unresolved comments with:
```fish
mise run pr-comments <pr_number>
```
Address each unresolved comment before proceeding. The user will resolve comments on the Forge UI as they are addressed.
4. When making changes, add a towncrier changelog fragment. Use the branch name as the identifier when possible, or use orphan (`+`) otherwise:
```bash
# Using branch name (preferred)
echo "Add new feature X" > docs/changelog.d/new-feature.feature.md
# Orphan fragment (when no branch name fits)
echo "Fix bug Y" > docs/changelog.d/+fix-bug-y.bugfix.md
```
Fragment types: `feature`, `bugfix`, `infra`, `doc`, `misc`. Fragments are collected into CHANGELOG.md during releases.
5. Use `Brewfile` and `mise.toml` to install tools needed on the development workstation (typically hostnamed "gilbert", username "eblume").
6. Services are hosted either on indri directly (via ansible) or in Kubernetes (via ArgoCD). See the "Service Deployment" section below for details.
7. Try to always test changes before applying them. Use syntax checkers, do dry runs (`--check --diff`), run commands manually via `ssh indri 'some command'`, etc.
8. **Wait for user review before deploying.** After creating a PR, do not run deployment commands until the user has had a chance to review the changes. The user will indicate when they're ready to deploy.
9. After deploying changes, try to verify the result. Use `mise run indri-services-check` to do a general service health check.
10. **Never merge PRs or push to main without explicit user request.** The user will merge PRs themselves after review. Only merge a PR or push directly to main if the user explicitly asks you to.
## Project Structure ## Project Structure
``` ```
./docs/ # blumeops documentation (Diataxis structure, built with Quartz) ./docs/ # documentation (Diataxis, Quartz)
./docs/changelog.d/ # towncrier changelog fragments ./docs/changelog.d/ # towncrier fragments
./mise-tasks/ # management and utility scripts run via `mise run` ./mise-tasks/ # scripts via `mise run`
./ansible/playbooks/ # ansible playbooks (indri.yml is primary) ./ansible/playbooks/ # ansible (indri.yml primary)
./ansible/roles/ # ansible roles for indri-hosted services ./ansible/roles/ # indri service roles
./argocd/apps/ # ArgoCD Application definitions (app-of-apps pattern) ./argocd/apps/ # ArgoCD Application definitions
./argocd/manifests/ # Kubernetes manifests for each service ./argocd/manifests/ # k8s manifests per service
./pulumi/ # Pulumi IaC for tailnet ACLs and cloud resources ./pulumi/ # Pulumi IaC (tailnet ACLs, cloud)
~/code/personal/ # projects managed by the user ~/code/personal/ # user's projects
~/code/3rd/ # external projects, mirrored or downloaded ~/code/3rd/ # mirrored external projects
~/code/work # FORBIDDEN, never go here, avoid searching it ~/code/work # FORBIDDEN
``` ```
## Service Deployment ## Service Deployment
### Kubernetes Services (via ArgoCD) ### Kubernetes (ArgoCD)
Most services run on `k8s.tail8d86e.ts.net`, via minikube on indri. They are managed via ArgoCD using the app-of-apps pattern: Most services run in minikube on indri via ArgoCD (app-of-apps, manual sync).
- **Application definitions**: `argocd/apps/<service>.yaml` **PR workflow:**
- **Manifests**: `argocd/manifests/<service>/` 1. Create branch, modify `argocd/manifests/<service>/`
- **Sync policy**: Manual sync (no auto-sync on git push) 2. Push, then `argocd app sync apps`
3. Test on branch: `argocd app set <service> --revision <branch> && argocd app sync <service>`
4. After merge: `argocd app set <service> --revision main && argocd app sync <service>`
**PR workflow for k8s services:** **Commands:** `argocd app list|get|diff|sync <app>`
1. Create feature branch and add/modify manifests **Login:** `argocd login argocd.ops.eblu.me --username admin --password "$(op --vault vg6xf6vvfmoh5hqjjhlhbeoaie item get srogeebssulhtb6tnqd7ls6qey --fields password --reveal)"`
2. Push branch to forge
3. Sync the `apps` application to pick up new Application definitions: ### Indri (Ansible)
```fish
argocd app sync apps Native services: Forgejo, Zot, Caddy, Borgmatic, Alloy
```
4. Point the service app at the feature branch for testing:
```fish
argocd app set <service> --revision feature/branch-name
argocd app sync <service>
```
5. Test the deployment
6. After PR merge, reset to main and resync:
```fish
argocd app set <service> --revision main
argocd app sync <service>
```
**Useful commands:**
```fish ```fish
argocd app list # List all apps mise run provision-indri # full
argocd app get <app> # Get app details mise run provision-indri -- --tags <role> # specific
argocd app diff <app> # Preview changes before sync mise run provision-indri -- --check --diff # dry run
argocd app sync <app> # Sync an app
kubectl --context=minikube-indri get pods -n <namespace> # Check pods
kubectl --context=minikube-indri logs -n <namespace> <pod> # View logs
``` ```
Note: The user has fish abbreviations `ki` for `kubectl --context=minikube-indri` and `k9i` for `k9s --context=minikube-indri`, but these only work in interactive shells. ### Routing
**ArgoCD login (when token expires):** | Domain | Mechanism | Reachable from |
```fish |--------|-----------|----------------|
argocd login argocd.ops.eblu.me --username admin --password "$(op --vault vg6xf6vvfmoh5hqjjhlhbeoaie item get srogeebssulhtb6tnqd7ls6qey --fields password --reveal)" | `*.ops.eblu.me` | Caddy on indri (100.98.163.89) | everywhere incl. k8s pods |
``` | `*.tail8d86e.ts.net` | Tailscale MagicDNS | tailnet clients only |
### Indri Services (via Ansible) Check tailscale serve: `ssh indri 'tailscale serve status --json'`
Some services run directly on indri outside of Kubernetes: ## Container Releases
- **Forgejo** - Git forge at `forge.ops.eblu.me` (HTTPS: 443, SSH: 2222)
- **Zot Registry** - Container registry at `registry.ops.eblu.me` (k8s depends on it)
- **Caddy** - Reverse proxy for `*.ops.eblu.me` with TLS via ACME DNS-01
- **Borgmatic** - Backup system
- **Grafana Alloy** - Metrics/logs collector
**Deployment:**
```fish
mise run provision-indri # Full playbook
mise run provision-indri -- --tags <role> # Specific role
mise run provision-indri -- --check --diff # Dry run
```
### Service Routing
**External DNS (`*.ops.eblu.me`)** - Services accessible from anywhere on the tailnet, including k8s pods and docker containers:
- Managed via Caddy reverse proxy on indri
- DNS points to indri's Tailscale IP (100.98.163.89)
- TLS certificates via Let's Encrypt (ACME DNS-01 with Gandi)
- Config: `ansible/roles/caddy/`
**Tailscale MagicDNS (`*.tail8d86e.ts.net`)** - Services only accessible from Tailscale clients:
- K8s services use Tailscale Ingress (via tailscale-operator)
- Some legacy services still use `tailscale serve`
- Cannot be reached from k8s pods or docker containers (they're not Tailscale clients)
Use `ssh indri 'tailscale serve status --json'` to check current tailscale serve entries.
## Container Image Releases
```fish ```fish
mise run container-list # Show containers and recent tags mise run container-list # show images/tags
mise run container-release runner v1.0.0 # Tag and trigger build workflow mise run container-release <name> <version> # tag and build
``` ```
## Third-Party Projects ## Third-Party Projects
When a task requires cloning or using a third-party git repository (e.g., for building from source), **ask the user to mirror it on forge first**, then clone from the mirror: Ask user to mirror on forge first, then clone to `~/code/3rd/<project>/`.
- Mirror location: `https://forge.ops.eblu.me/eblume/<project>.git`
- Clone to: `~/code/3rd/<project>/`
This avoids external dependencies and ensures the project is available even if the upstream is unreachable.
## Task Discovery ## Task Discovery
To discover pending blumeops tasks, run:
```fish ```fish
mise run blumeops-tasks mise run blumeops-tasks # fetch from Todoist, sorted by priority
``` ```
This fetches tasks from the "Blumeops" project in Todoist (via 1Password for API credentials) and displays them sorted by priority: p1 (urgent), p2 (high), p4 (normal/default), p3 (backlog). The typical workflow is to pick a task from this list at the start of a session, then dive in with planning.
## Credentials ## Credentials
The root store for credentials is 1password, which can be accessed via `op --vault <vaultid> item get <itemid> --field fieldname --reveal`, which will prompt the user for their assent and biometrics or password. Typically, use scripts to defer this action - try not to ever grab credentials directly. For instance, the indri.yml playbook starts with `pre_tasks` to gather the relevant secrets needed to provision its services. Some services have their credentials exported to files `chmod 0600` on indri, but they still start out in 1password. In some cases you can test services with a command that grabs the credential, but try to use environment variables or other arrangements to avoid learning the credential yourself, and warn the user first. Root store is 1Password. Never grab directly - use existing patterns (ansible pre_tasks, external-secrets, scripts with `op` CLI). Warn user before any credential access.

View file

@ -16,7 +16,6 @@ borgmatic_source_directories:
- /opt/homebrew/var/forgejo - /opt/homebrew/var/forgejo
- /Users/erichblume/.config/borgmatic - /Users/erichblume/.config/borgmatic
- /Users/erichblume/Documents - /Users/erichblume/Documents
- /Users/erichblume/Pictures
# Backup repository # Backup repository
borgmatic_repositories: borgmatic_repositories:

View file

@ -0,0 +1 @@
Add wiki-link formatting convention to AI assistance guide

View file

@ -0,0 +1 @@
Add 'ai' changelog fragment type for AI assistance changes

View file

@ -0,0 +1 @@
Remove iCloud Photos from borgmatic backup (photos now managed via Immich)

View file

@ -25,9 +25,6 @@ Daily backup system using Borg backup, running on indri.
- `/opt/homebrew/var/forgejo` - Git forge data - `/opt/homebrew/var/forgejo` - Git forge data
- `~/.config/borgmatic` - Borgmatic config - `~/.config/borgmatic` - Borgmatic config
- `~/Documents` - Personal documents - `~/Documents` - Personal documents
- `~/Pictures` - Photos (see note below)
**iCloud Photos note:** macOS Photos.app defaults to "Optimize Mac Storage" which keeps only thumbnails locally. Borgmatic only backs up what's on disk, so iCloud-only photos are NOT backed up via this method.
**Databases:** **Databases:**
- `miniflux` on [[postgresql]] - `miniflux` on [[postgresql]]

View file

@ -25,7 +25,6 @@ Daily automated backups from [[indri]] to [[sifaka | Sifaka]] NAS.
| `/opt/homebrew/var/forgejo` | Git repositories | Critical | | `/opt/homebrew/var/forgejo` | Git repositories | Critical |
| `~/.config/borgmatic` | Backup config | High | | `~/.config/borgmatic` | Backup config | High |
| `~/Documents` | Personal documents | High | | `~/Documents` | Personal documents | High |
| `~/Pictures` | Photos | Medium |
### Databases ### Databases
@ -34,6 +33,10 @@ Daily automated backups from [[indri]] to [[sifaka | Sifaka]] NAS.
| miniflux | [[postgresql | pg.ops.eblu.me]] | pg_dump stream | | miniflux | [[postgresql | pg.ops.eblu.me]] | pg_dump stream |
| teslamate | [[postgresql | pg.ops.eblu.me]] | pg_dump stream | | teslamate | [[postgresql | pg.ops.eblu.me]] | pg_dump stream |
## Sifaka-Native Data
Some data lives directly on [[sifaka]] rather than being backed up to it (photos via [[immich]], music via [[navidrome]], video via [[jellyfin]]). See [[sifaka]] for data protection details.
## What Is NOT Backed Up ## What Is NOT Backed Up
| Data | Reason | | Data | Reason |

View file

@ -57,7 +57,16 @@ Add a fragment for user-visible changes:
echo "Description" > docs/changelog.d/branch-name.feature.md echo "Description" > docs/changelog.d/branch-name.feature.md
``` ```
Types (file suffix): `.feature`, `.bugfix`, `.infra`, `.doc`, `.misc` Types (file suffix): `.feature`, `.bugfix`, `.infra`, `.doc`, `.ai`, `.misc`
### Wiki-Link Formatting
Use simple wiki-links without alternate text or extra spaces:
- Prefer `[[borgmatic]]` over `[[borgmatic | Borgmatic]]`
- Only use alternate text when grammatically warranted (e.g., `[[cluster|Kubernetes]]` reads better than `[[cluster]]`)
- No spaces around the pipe: `[[path|Text]]` not `[[ path | Text ]]`
When editing documentation, rewrite links to follow this convention as you encounter them.
## Service Locations ## Service Locations
@ -66,7 +75,7 @@ Understanding where services run helps target changes correctly:
| Location | Services | Management | | Location | Services | Management |
|----------|----------|------------| |----------|----------|------------|
| [[indri]] (native) | Forgejo, Zot, Jellyfin, Caddy | Ansible | | [[indri]] (native) | Forgejo, Zot, Jellyfin, Caddy | Ansible |
| [[cluster | Kubernetes]] | Everything else | ArgoCD | | [[cluster|Kubernetes]] | Everything else | ArgoCD |
## Mise Tasks ## Mise Tasks
@ -108,7 +117,7 @@ For AI agents building context:
Credentials live in 1Password. Never retrieve them directly - use existing patterns: Credentials live in 1Password. Never retrieve them directly - use existing patterns:
- Ansible `pre_tasks` gather secrets at playbook start - Ansible `pre_tasks` gather secrets at playbook start
- [[external-secrets|External Secrets]] syncs to Kubernetes - [[external-secrets]] syncs to Kubernetes
- Scripts use `op` CLI with user biometric prompts - Scripts use `op` CLI with user biometric prompts
## Common Pitfalls ## Common Pitfalls

View file

@ -29,6 +29,11 @@ directory = "doc"
name = "Documentation" name = "Documentation"
showcontent = true showcontent = true
[[tool.towncrier.type]]
directory = "ai"
name = "AI Assistance"
showcontent = true
[[tool.towncrier.type]] [[tool.towncrier.type]]
directory = "misc" directory = "misc"
name = "Miscellaneous" name = "Miscellaneous"