Files
app-catalog-felhom.eu/CLAUDE.md
T
admin 459766cb16
gates / gates (push) Successful in 0s
docs: R-168 is CLOSED — the "CI is still owed" sentence was stale (R-229 part 2)
Corrected in all four instruction files across all four repos. Found while confirming this
session own push by run ID, which is precisely the check that catches it.

In felhom-agent/CLAUDE.md the sentence contradicted the same file release section, which
already said R-168 mails the failure -- a contradiction inside one instruction file, the exact
class the R-229 work exists to find.

REPORT.md deliberately NOT overwritten in the sibling repos: a one-line docs correction must not
destroy the record of their last real implementation.
2026-08-06 11:03:02 +02:00

81 lines
6.2 KiB
Markdown

# CLAUDE.md — `app-catalog-felhom.eu`
> Loads when Claude Code touches this repo. Current state: `CONTEXT.md` + `CHANGELOG.md` top.
> Cross-repo orientation: workspace-root `/mnt/5_hdd/felhom.eu/git/CLAUDE.md`.
## What this repo is
The Felhom **app catalog**: one directory per app under `templates/<app>/`, each holding exactly
`docker-compose.yml` + `.felhom.yml` (deploy fields, resources, healthcheck probe, app_info — all
customer-facing text in Hungarian). The felhom-controller git-syncs these to every customer box;
`.felhom.yml` drives the deploy wizard. `templates.json` + `scripts/generate-customer.sh` are LEGACY
(Portainer-era) — new apps don't touch them.
## Deploy contract
**Push to `main` = deploy.** The controller's sync picks changes up within 15 minutes (or trigger via
the dashboard "Sablonok frissítése" button / `POST /api/sync`). Only the two template files sync;
deployed `app.yaml` (customer secrets) is never overwritten. Full deploy details: the
`felhom-build-deploy` skill.
## Conventions
- **See `REUSE.md` before adding or editing an app** — canonical example app (paperless-ngx),
required `.felhom.yml` fields, healthcheck family per image type, memory-limit rules, traps.
- Update `REUSE.md` in the same commit that changes a catalog-wide convention.
- `README.md` is the format spec — update its app tables when adding an app.
- Update `CHANGELOG.md` (newest on top) and overwrite `REPORT.md` with every pushed change.
- No secrets in any committed file; secrets are generated at deploy time via `deploy_fields`
`generate:` specs.
- **Run `python3 scripts/catalog_gates.py <app>` after ANY template change** — it is the ONE entry
point and runs all three gates below, exiting non-zero if any fails. Name the app(s) you touched
and it scopes the two gates that accept scoping, which is fast; with no names the runtime gate
deploys **every** template, so that form belongs **on a scratch host, never a customer box**.
Exit: 0 all clean · 1 convicted · 2 UNDETERMINED, which is never a pass.
**Why a runner and not four separate invocations** (operator ruling 2026-08-02, R-161): of this
project's gates, the only ones that ever get run are the ones with a single entry point named in a
CLAUDE.md — `felhom.eu/scripts/site_gates.py` is run, and R-29's three orphans are named nowhere and
have stopped nothing. Controller-side enforcement was rejected because a check at template load can
only read the file, and a static audit of all 53 templates reports the catalog clean **including
papra** — it would pass on the exact defect it exists to catch. CI was rejected for now: neither
repo has any, and there are no users yet. **R-161 stays open at reduced scope** — this is
convention, run by a person; real automatic enforcement is owed when a second person touches
templates. **Update 2026-08-02:** `.githooks/pre-push` now runs `catalog_gates.py --fast` on every
push, which is gate 1 (`check-image-pins.py`) only — the other two need network and a container
runtime and take minutes per app, and a push that pulls images and starts containers gets bypassed
within a week, after which the bypass is the habit. They stay deliberate periodic runs. The hook is
per-clone (`git config core.hooksPath .githooks`) and `git push --no-verify` bypasses it, which is
why gate 1 alone cannot be the whole story — CI re-runs the entry point on every push and **emails
on failure** (`felhom.eu` `OPEN-ITEMS.md` R-168, CLOSED 2026-08-02), which is what notices a bypass.
- **Never `:latest` or untagged images in templates** — pin a concrete version tag; an app deployed
anywhere in the fleet is pinned to the digest it is currently running (a pin must never cause a
version jump). Digest pins (`@sha256:`) also count. Gate: `python scripts/check-image-pins.py`
(run after any compose change; exit 1 on any floating/missing tag).
- **A pinned tag can still rot away upstream** — the pin gate is syntactic and cannot see that.
Second gate: `python3 scripts/check-image-resolvable.py` (exit 0 resolve / 1 GONE / 2 inconclusive),
run at the start of every catalog campaign and before any publish train that vouches the catalog.
Needs network + `docker`; unauthenticated Docker Hub throttles a full sweep, so `docker login`
first or expect exit 2. It reports a throttle as INCONCLUSIVE, never as a dead image.
- **A well-formed template can still preserve the wrong folder** — and no static check can see it.
Third gate, the only RUNTIME one: `python3 scripts/check-volume-persistence.py`
(0 all clean / **1 REFUSED** / 2 undecided). It deploys each template, exercises it into writing
data, and compares where the data landed against what the compose mounts. Needs Docker + network
and minutes per app, so it is periodic like the resolvability gate — run it whenever a template's
`volumes:` block or image tag changes, and at the start of every catalog campaign, **on a scratch
host, never a customer box**. It refuses to report at all unless it has just re-proven itself in
both directions against two canary templates. Fixture tests (no Docker):
`python3 scripts/test_check_volume_persistence.py`.
**UNDETERMINED is exit 2 and is never a pass** — an app that wrote nothing has not been shown to
be correct. Why it exists: papra mounted `papra_data:/app/data` while the app wrote its database
to `/app/app-data/db/`, so its backup completed, verified, and contained an empty directory
(R-156, Campaign 10).
- **Taking an app out of circulation — use `lifecycle:`, never a directory move.** `.felhom.yml`
gains an optional `lifecycle:` field: `available` (default; absent/empty means this), `hidden`
(not offered for new installs, no explanation owed), `abandoned` (upstream stopped developing it —
not offered for new installs, and every box already running it shows a permanent "Nem karbantartott"
notice). **Deployed instances keep working in full either way** — the state affects what is
OFFERED, never what already runs, and the controller REFUSES a deploy of a non-available template
server-side. An unknown value degrades to `available` with one WARN, so a typo can never brick a
template. This supersedes the short-lived `retired/` directory move, which was wrong: removing a
template orphans every customer already running it.