docs: migrate workflow to DooPlex-local execution
This commit is contained in:
@@ -3,7 +3,9 @@
|
|||||||
> Read automatically when Claude Code works in this repo. Stable orientation only — **current state
|
> Read automatically when Claude Code works in this repo. Stable orientation only — **current state
|
||||||
> lives in `CONTEXT.md` and the tops of `hub/CHANGELOG.md` / `scripts/CHANGELOG.md` /
|
> lives in `CONTEXT.md` and the tops of `hub/CHANGELOG.md` / `scripts/CHANGELOG.md` /
|
||||||
> `website/CHANGELOG.md`**, never here. Cross-repo orientation (the felhom system, artifact
|
> `website/CHANGELOG.md`**, never here. Cross-repo orientation (the felhom system, artifact
|
||||||
> taxonomy, access): workspace-root `e:\git\CLAUDE.md`; this file is `felhom.eu`-specific.
|
> taxonomy, access): workspace-root `/mnt/5_hdd/felhom.eu/git/CLAUDE.md`; this file is
|
||||||
|
> `felhom.eu`-specific. A versioned copy of that workspace file lives at
|
||||||
|
> `documentation/runbooks/workspace-CLAUDE.md`.
|
||||||
|
|
||||||
## Project overview
|
## Project overview
|
||||||
|
|
||||||
@@ -15,8 +17,9 @@ This repo contains:
|
|||||||
system**: `architecture/01..05-*.md`, `proxmox-platform.md`, `tests/phase*-findings.md`,
|
system**: `architecture/01..05-*.md`, `proxmox-platform.md`, `tests/phase*-findings.md`,
|
||||||
runbooks, audits. Read these before designing.
|
runbooks, audits. Read these before designing.
|
||||||
- **Skills** (`skills/`) — the versioned source of the Claude Code skills
|
- **Skills** (`skills/`) — the versioned source of the Claude Code skills
|
||||||
(`felhom-build-deploy`, `felhom-ui-design`, `felhom-testing`); install/update with
|
(`felhom-build-deploy`, `felhom-ui-design`, `felhom-testing`, `felhom-app-catalog`);
|
||||||
`python scripts/install_skills.py` (junction into `~/.claude/skills/`).
|
install/update with `python3 scripts/install_skills.py` (symlink into `~/.claude/skills/` on
|
||||||
|
POSIX, junction on Windows — either way repo edits are live immediately).
|
||||||
|
|
||||||
See `README.md` for full architecture/DNS/email/SEO docs. See `TASK.md` for the current task (if any).
|
See `README.md` for full architecture/DNS/email/SEO docs. See `TASK.md` for the current task (if any).
|
||||||
See `REUSE.md` before writing new code.
|
See `REUSE.md` before writing new code.
|
||||||
@@ -65,10 +68,11 @@ pushes; **you (Claude Code) implement**. A file being open in the editor is NOT
|
|||||||
SSH + the felhom-agent token). Mark a step HUMAN only when it genuinely needs physical presence, a
|
SSH + the felhom-agent token). Mark a step HUMAN only when it genuinely needs physical presence, a
|
||||||
real-world decision, or credentials CC truly lacks.
|
real-world decision, or credentials CC truly lacks.
|
||||||
- Validation of a push against a spec's criteria is project Claude's job, not yours, unless asked.
|
- Validation of a push against a spec's criteria is project Claude's job, not yours, unless asked.
|
||||||
- **Browser automation available** (`claude-in-chrome`): CC can drive `hub.felhom.eu`, the website,
|
- **Browser automation is NOT available** in the DooPlex environment (`claude-in-chrome` was a
|
||||||
or any `*.demo-felhom.eu` UI for live validation + screenshots. Caveat: tools attach only to a
|
Windows-workstation capability). Validate at the endpoint level — invoke the exact endpoint the UI
|
||||||
session STARTED AFTER the bridge connected. Note: the hub UI is operator-password-gated — CC
|
invokes — and via render tests; say which method was used. The hub UI is operator-password-gated
|
||||||
cannot log in; verify UI changes via render tests instead.
|
anyway, so render tests were already the method for UI changes. Strict end-to-end UI coverage is a
|
||||||
|
manual click-through by the operator.
|
||||||
|
|
||||||
> **In every repository where you make a change, update both files in that repo:**
|
> **In every repository where you make a change, update both files in that repo:**
|
||||||
> - **`CHANGELOG.md`** — cumulative log, newest on top (here: per-area `hub/`, `scripts/`, `website/`).
|
> - **`CHANGELOG.md`** — cumulative log, newest on top (here: per-area `hub/`, `scripts/`, `website/`).
|
||||||
@@ -93,14 +97,20 @@ pushes; **you (Claude Code) implement**. A file being open in the editor is NOT
|
|||||||
- **Config:** YAML via ConfigMap at `/etc/felhom-hub/hub.yaml`. Secrets via out-of-band
|
- **Config:** YAML via ConfigMap at `/etc/felhom-hub/hub.yaml`. Secrets via out-of-band
|
||||||
`secretKeyRef` (never inline stringData — REUSE.md §3).
|
`secretKeyRef` (never inline stringData — REUSE.md §3).
|
||||||
|
|
||||||
## SSH access
|
## Environment & access
|
||||||
|
|
||||||
Use the Windows OpenSSH binary (Git Bash's ssh fails silently): `SSH=/c/Windows/System32/OpenSSH/ssh.exe`.
|
Claude Code runs **on DooPlex (192.168.0.180, Debian 13, user `kisfenyo`)** — the k3s node itself.
|
||||||
|
Repos in `/mnt/5_hdd/felhom.eu/git/`, build dirs in `/mnt/5_hdd/felhom.eu/build/`. `kubectl` and the
|
||||||
|
image build/push are local commands; felhom-pve is one SSH hop.
|
||||||
|
|
||||||
| Host | IP | User | Role |
|
| Host | Access | Role |
|
||||||
|------|----|------|------|
|
|------|--------|------|
|
||||||
| Build server (k3s node) | 192.168.0.180 | kisfenyo | Build + push images, `sudo kubectl` |
|
| **DooPlex (this host)** | local — `/mnt/5_hdd/felhom.eu/{git,build}/` | Build + push images, `sudo kubectl` |
|
||||||
| Demo Proxmox host | 192.168.0.162 | root@pam (SSH alias `felhom-pve`) | pveum/pct + live Proxmox validation |
|
| Demo Proxmox host | `ssh felhom-pve` (root@192.168.0.162) | pveum/pct + live Proxmox validation |
|
||||||
|
|
||||||
|
> **Legacy: Windows workstation.** Until 2026-07-19 CC ran on Windows 11 with repos in `E:\git\`,
|
||||||
|
> and every remote command needed `SSH=/c/Windows/System32/OpenSSH/ssh.exe` (Git Bash's ssh fails
|
||||||
|
> silently). Retained in case that environment is revived.
|
||||||
|
|
||||||
## Build & deploy — Hub (GitOps via ArgoCD)
|
## Build & deploy — Hub (GitOps via ArgoCD)
|
||||||
|
|
||||||
@@ -118,14 +128,20 @@ truth is the **manifest**:
|
|||||||
happened — reconcile via the manifest, not the changelog.
|
happened — reconcile via the manifest, not the changelog.
|
||||||
- Green gate before any hub commit: `go build ./... && go vet ./... && go test ./...` in `hub/`.
|
- Green gate before any hub commit: `go build ./... && go vet ./... && go test ./...` in `hub/`.
|
||||||
|
|
||||||
Steps: commit+push code → `./build.sh <VER> --push` on 180 (`/mnt/5_hdd/felhom.eu/build/felhom-hub` — all felhom build dirs/repos moved off the SSD to `/mnt/5_hdd/felhom.eu/` on 2026-07-18) → bump
|
> **Clean-tree gate before any build:** `git status --porcelain` must be empty and
|
||||||
`manifests/hub.yaml` tag + push → ArgoCD hard-refresh + sync (kubectl-patch method in the skill) →
|
> `git rev-parse HEAD` must equal `git rev-parse origin/main` in the repo being built. An unpushed
|
||||||
verify Synced/Healthy + rollout + image + startup log.
|
> change does not exist — never build a dirty or unpushed tree. The `git pull` in the build step
|
||||||
|
> stays (it is a no-op when you work in this tree, and load-bearing if anything was pushed from
|
||||||
|
> elsewhere).
|
||||||
|
|
||||||
|
Steps: commit+push code → `cd /mnt/5_hdd/felhom.eu/build/felhom-hub && ./build.sh <VER> --push`
|
||||||
|
(local) → bump `manifests/hub.yaml` tag + push → ArgoCD hard-refresh + sync (kubectl-patch method in
|
||||||
|
the skill, now local `sudo kubectl`) → verify Synced/Healthy + rollout + image + startup log.
|
||||||
|
|
||||||
## Build & deploy — Website / Manifests
|
## Build & deploy — Website / Manifests
|
||||||
|
|
||||||
- **Website** auto-deploys via git-sync; just push to `main` (live in 1–2 min). **Run
|
- **Website** auto-deploys via git-sync; just push to `main` (live in 1–2 min). **Run
|
||||||
`python scripts/site_gates.py` after ANY website change**; new pages go into its `PAGES` list.
|
`python3 scripts/site_gates.py` after ANY website change**; new pages go into its `PAGES` list.
|
||||||
Emergency edits: https://files.felhom.eu. All `website/` HTML is **UTF-8 with BOM** — preserve it.
|
Emergency edits: https://files.felhom.eu. All `website/` HTML is **UTF-8 with BOM** — preserve it.
|
||||||
- **Manifests** are GitOps via the `felhom` app — commit to `main`, then deliberate sync.
|
- **Manifests** are GitOps via the `felhom` app — commit to `main`, then deliberate sync.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,118 @@
|
|||||||
|
<!-- VERSIONED COPY. The LIVE file is /mnt/5_hdd/felhom.eu/git/CLAUDE.md on DooPlex (it is not
|
||||||
|
inside any repo, so it is mirrored here to be versioned and restorable). Keep the two in
|
||||||
|
sync when either changes. -->
|
||||||
|
|
||||||
|
# CLAUDE.md — `/mnt/5_hdd/felhom.eu/git` workspace root (DooPlex)
|
||||||
|
|
||||||
|
## What this workspace is
|
||||||
|
|
||||||
|
`/mnt/5_hdd/felhom.eu/git` is a parent folder holding the felhom sibling repos. Most are one logical
|
||||||
|
product — **Felhom**, a managed home-server service for Hungarian households — spread across several
|
||||||
|
repos. (Any non-felhom repo is unrelated; ignore unless asked.)
|
||||||
|
|
||||||
|
**Claude Code runs HERE, on DooPlex (192.168.0.180), as `kisfenyo`.** Builds are local commands; the
|
||||||
|
Proxmox host is one SSH hop (`ssh felhom-pve`). The Windows workstation is no longer the
|
||||||
|
orchestration point and its trees are stale — see "Legacy: Windows workstation" at the bottom.
|
||||||
|
|
||||||
|
Run CC inside tmux so sessions survive SSH drops: **`tmux new -A -s cc`**.
|
||||||
|
|
||||||
|
## This host is production infrastructure
|
||||||
|
|
||||||
|
DooPlex runs Gitea, the container registry, k3s + Longhorn, PBS, and the hub. Treat it accordingly:
|
||||||
|
|
||||||
|
- NEVER run `docker system prune`, `docker image prune -a`, or any global Docker cleanup here.
|
||||||
|
- NEVER touch k3s data dirs, Longhorn mounts, PBS datastores, or Gitea storage. Workspace is
|
||||||
|
`/mnt/5_hdd/felhom.eu/` — stay inside it plus `~/build` symlinks/dirs.
|
||||||
|
- Destructive disk/guest operations belong to felhom-pve via the agent — never on this host.
|
||||||
|
- Do not run Claude Code with permission prompts disabled on this host.
|
||||||
|
- Watch disk headroom before large builds: `df -h /mnt/5_hdd /` — abort if either is >90%.
|
||||||
|
|
||||||
|
## The Felhom system (three-component model, Proxmox-based)
|
||||||
|
|
||||||
|
- **Hub** — operator backend on k3s (`hub.felhom.eu`). Lives in `felhom.eu/hub/`.
|
||||||
|
- **Host agent** — one per Proxmox host, operator-tier, owns all Proxmox interaction. Repo `felhom-agent/`.
|
||||||
|
- **In-guest controller** — one per customer LXC, Docker-only. Repo `felhom-controller/`.
|
||||||
|
|
||||||
|
Other felhom repos: `app-catalog-felhom.eu/` (app templates), `homelab-manifests/` (DooPlex k3s).
|
||||||
|
|
||||||
|
**Authoritative design docs (read these before designing anything):** `felhom.eu/documentation/architecture/01..05-*.md`, `felhom.eu/documentation/proxmox-platform.md`, `felhom.eu/documentation/tests/phase{0,1-2,3,4}-findings.md`.
|
||||||
|
|
||||||
|
## Per-repo guidance
|
||||||
|
|
||||||
|
When you work in a repo, read its `CLAUDE.md` (it loads on-demand the moment you touch a file there):
|
||||||
|
- `felhom-agent/CLAUDE.md` — the Go host agent.
|
||||||
|
- `felhom.eu/CLAUDE.md` — hub + website + manifests + the architecture docs.
|
||||||
|
- `felhom-controller/CLAUDE.md` — the in-guest controller.
|
||||||
|
|
||||||
|
## Skills
|
||||||
|
|
||||||
|
Four Felhom skills exist (personal scope, `~/.claude/skills/`): **`felhom-build-deploy`** (all
|
||||||
|
build/deploy/publish runbooks), **`felhom-ui-design`** (design-system v2 tokens/rules/gates),
|
||||||
|
**`felhom-testing`** (non-hollow tests + red-proofs + seams), **`felhom-app-catalog`** (catalog
|
||||||
|
authoring workflow). Source of truth: `felhom.eu/skills/`; install/update with
|
||||||
|
`python3 felhom.eu/scripts/install_skills.py` (symlink — repo edits are live immediately).
|
||||||
|
|
||||||
|
## Artifact taxonomy (READ THIS — it prevents the "what do I do?" stall)
|
||||||
|
|
||||||
|
The planning/architecture assistant (in claude.ai, "project Claude") produces files with distinct roles. **A file being open in the editor is NOT an instruction. If no task is stated, ask.**
|
||||||
|
|
||||||
|
- **`TASK.md` / `TASK-*.md`** — a spec for **you (Claude Code) to implement**. Implement it when it is placed as `TASK.md` at a repo root, or when explicitly told "implement <file>". Then push, update `CHANGELOG.md`, and write the repo's `REPORT.md`.
|
||||||
|
- **`RUNBOOK-*.md`** — an operational procedure. CC executes the steps it has access and capability for, including live validation on the demo nodes and the demo Proxmox host (CC has root@felhom-pve SSH + the felhom-agent token). A step is human-only only when it genuinely needs physical presence, a real-world decision, or credentials CC truly lacks — mark those steps HUMAN. Do not decline a whole procedure because it touches a live host or a privileged token. (Judgment still applies: confirm before irreversible ops on real customer data — but demo scratch guests are fair game.)
|
||||||
|
- **Validation/review** — checking a push against a spec's criteria is **project Claude's** job, not yours, unless asked.
|
||||||
|
|
||||||
|
## Shared conventions
|
||||||
|
|
||||||
|
- **Push to `main` directly** — no feature branches.
|
||||||
|
|
||||||
|
> **Clean-tree gate before any build:** `git status --porcelain` must be empty and
|
||||||
|
> `git rev-parse HEAD` must equal `git rev-parse origin/main` in the repo being built. An unpushed
|
||||||
|
> change does not exist — never build a dirty or unpushed tree. The `git pull` in the build step
|
||||||
|
> stays (it is a no-op when you work in this tree, and load-bearing if anything was pushed from
|
||||||
|
> elsewhere).
|
||||||
|
|
||||||
|
> **In every repository where you make a change, update both files in that repo:**
|
||||||
|
> - **`CHANGELOG.md`** — a cumulative log of **all** changes; newest entry on top.
|
||||||
|
> - **`REPORT.md`** — **overwrite** with a summary of the **most recent** implementation (or significant validation/operational run) only; not cumulative.
|
||||||
|
>
|
||||||
|
> **Never write secrets** — tokens, passwords, private keys, API keys — into `CHANGELOG.md`, `REPORT.md`, or any committed file. Reference them as "stored out-of-band" instead.
|
||||||
|
|
||||||
|
- **Versioning** is via build-time ldflags (`-X main.version`/`-X main.Version`); bump on meaningful changes + add a CHANGELOG entry.
|
||||||
|
- Code quality: double-check for bugs/edge cases; add debug logging; **ask rather than guess** when you'd otherwise need to invent input or output.
|
||||||
|
|
||||||
|
## Live validation — no browser here
|
||||||
|
|
||||||
|
**`claude-in-chrome` is NOT available on DooPlex.** The standard method is endpoint-level: invoke the
|
||||||
|
exact endpoint the UI invokes (no server logic is skipped, only rendering) and say which method was
|
||||||
|
used. Strict end-to-end UI coverage is a manual click-through by the operator.
|
||||||
|
|
||||||
|
## Access
|
||||||
|
|
||||||
|
Local (this host): repos `/mnt/5_hdd/felhom.eu/git/<repo>`, build dirs
|
||||||
|
`/mnt/5_hdd/felhom.eu/build/felhom-{controller,hub,agent}`, `sudo kubectl`, Go toolchain, Docker
|
||||||
|
build+push to `gitea.dooplex.hu/admin/`.
|
||||||
|
|
||||||
|
| Host | Access | Use |
|
||||||
|
|---|---|---|
|
||||||
|
| **DooPlex (this host)** | local — Debian 13, `kisfenyo`, `/mnt/5_hdd/felhom.eu/` | build/push images, `sudo kubectl`, build+run the agent for tests |
|
||||||
|
| Demo Proxmox host `demo-felhom` | `ssh felhom-pve` (root@192.168.0.162, no sudo) | pveum/pct + live Proxmox validation |
|
||||||
|
| Demo guest 9201 | `ssh felhom-pve "pct exec 9201 -- ..."` | the live demo controller |
|
||||||
|
| felhotest (legacy) | `ssh -p 33022 kisfenyo@router.abonet.hu` | OLD /opt/docker compose mechanism |
|
||||||
|
|
||||||
|
The demo Proxmox host key changes on reprovision (N100) → refresh with
|
||||||
|
`ssh-keygen -R 192.168.0.162` then connect with `-o StrictHostKeyChecking=accept-new`
|
||||||
|
(`ssh-keyscan` hangs — avoid it).
|
||||||
|
|
||||||
|
## Legacy: Windows workstation
|
||||||
|
|
||||||
|
Kept so the old environment can be revived; **not the current setup**.
|
||||||
|
|
||||||
|
- Repos were in `E:\git\` (`/e/git/` in Git Bash); this file lived at `E:\git\CLAUDE.md`.
|
||||||
|
- **SSH binary had to be** `SSH=/c/Windows/System32/OpenSSH/ssh.exe` — Git Bash's `/usr/bin/ssh`
|
||||||
|
lacks access to the Windows SSH Agent and fails silently. Every remote command was
|
||||||
|
`$SSH kisfenyo@192.168.0.180 "..."`; details in `felhom-controller/docs/vscode-ssh-fix.md`.
|
||||||
|
- `pct exec` over SSH needed `export MSYS_NO_PATHCONV=1` (MSYS mangled `/`-paths).
|
||||||
|
- Agent deploy was a two-hop copy: build on 180 → `scp` to the Windows box (local path needed
|
||||||
|
`cygpath -w`) → `scp` on to felhom-pve. Beware CRLF when scp-ing config files through Windows.
|
||||||
|
- Skills were installed as Windows junctions (`mklink /J`) rather than POSIX symlinks.
|
||||||
|
- `claude-in-chrome` browser automation WAS available there (attaching only to sessions started
|
||||||
|
after the bridge connected).
|
||||||
@@ -1,5 +1,21 @@
|
|||||||
# Felhom scripts — Changelog
|
# Felhom scripts — Changelog
|
||||||
|
|
||||||
|
## install_skills.py — cross-platform (POSIX symlink / Windows junction) (2026-07-19)
|
||||||
|
|
||||||
|
Claude Code now runs on DooPlex (Debian 13), where `mklink /J` does not exist — the script would
|
||||||
|
have fallen through to COPY mode on every run, silently breaking the "repo edits are live
|
||||||
|
immediately" property that makes `felhom.eu/skills/` the source of truth.
|
||||||
|
|
||||||
|
- Link creation is now platform-dispatched behind `os.name == "nt"`: `os.symlink(...,
|
||||||
|
target_is_directory=True)` on POSIX, the existing `mklink /J` on Windows. Copy-mode fallback,
|
||||||
|
idempotency, and the re-run warning are unchanged.
|
||||||
|
- **`remove_existing()` had a real hazard on POSIX**: `os.rmdir()` fails on a symlink-to-directory,
|
||||||
|
which would have fallen through to `shutil.rmtree()` — and rmtree following a symlink into
|
||||||
|
`skills/` would have deleted the repo's own skill sources. It now unlinks symlinks explicitly
|
||||||
|
before any rmdir/rmtree path is reached. Existence checks use `os.path.lexists()` so a broken
|
||||||
|
link is seen and replaced rather than ignored.
|
||||||
|
- Human-facing output says "symlink" or "junction" per platform.
|
||||||
|
|
||||||
## build-felhom-iso.sh v1.22.0 — the boot screen is ours, and it offers exactly one thing (R-38) (2026-07-19)
|
## build-felhom-iso.sh v1.22.0 — the boot screen is ours, and it offers exactly one thing (R-38) (2026-07-19)
|
||||||
|
|
||||||
**A boot menu is a product surface, and ours was Proxmox's.** Every ISO is now repacked after
|
**A boot menu is a product surface, and ours was Proxmox's.** Every ISO is now repacked after
|
||||||
|
|||||||
+43
-17
@@ -1,12 +1,13 @@
|
|||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
"""Install the Felhom Claude Code skills from <repo>/skills/ into ~/.claude/skills/.
|
"""Install the Felhom Claude Code skills from <repo>/skills/ into ~/.claude/skills/.
|
||||||
|
|
||||||
Usage: python scripts/install_skills.py (run from the felhom.eu repo root or anywhere)
|
Usage: python3 scripts/install_skills.py (run from the felhom.eu repo root or anywhere)
|
||||||
|
|
||||||
Personal skills (~/.claude/skills/) apply across all projects — the right scope for the 4-repo
|
Personal skills (~/.claude/skills/) apply across all projects — the right scope for the 4-repo
|
||||||
E:\\git workspace. Preferred install is a Windows junction (mklink /J) so edits in the repo are live
|
felhom workspace. Preferred install is a LINK so edits in the repo are live immediately: a POSIX
|
||||||
immediately; if junction creation fails or isn't followed, falls back to a full COPY — in copy mode
|
symlink on Linux/macOS (the DooPlex environment), a junction (mklink /J) on Windows. If the link
|
||||||
you must RE-RUN this script after editing skills/. Idempotent: safe to re-run any time.
|
cannot be created or isn't followed, falls back to a full COPY — in copy mode you must RE-RUN this
|
||||||
|
script after editing skills/. Idempotent: safe to re-run any time.
|
||||||
"""
|
"""
|
||||||
import os, shutil, subprocess, sys
|
import os, shutil, subprocess, sys
|
||||||
|
|
||||||
@@ -18,7 +19,11 @@ fails = 0
|
|||||||
copy_mode_used = False
|
copy_mode_used = False
|
||||||
|
|
||||||
|
|
||||||
def is_working_junction(target, src):
|
IS_WINDOWS = os.name == "nt"
|
||||||
|
LINK_KIND = "junction" if IS_WINDOWS else "symlink"
|
||||||
|
|
||||||
|
|
||||||
|
def is_working_link(target, src):
|
||||||
"""True if target resolves into src (junction/symlink already correct)."""
|
"""True if target resolves into src (junction/symlink already correct)."""
|
||||||
try:
|
try:
|
||||||
return os.path.isfile(os.path.join(target, "SKILL.md")) and \
|
return os.path.isfile(os.path.join(target, "SKILL.md")) and \
|
||||||
@@ -28,35 +33,56 @@ def is_working_junction(target, src):
|
|||||||
|
|
||||||
|
|
||||||
def remove_existing(target):
|
def remove_existing(target):
|
||||||
# a junction must be removed with rmdir semantics (never recurse INTO it), a copy with rmtree
|
# a link must be removed WITHOUT recursing INTO it (that would delete the repo's skills);
|
||||||
|
# a real copied directory needs rmtree
|
||||||
|
if os.path.islink(target):
|
||||||
|
os.unlink(target) # POSIX symlink (also a Windows symlink-to-dir)
|
||||||
|
return
|
||||||
try:
|
try:
|
||||||
os.rmdir(target) # works for junctions and empty dirs
|
os.rmdir(target) # works for Windows junctions and empty dirs
|
||||||
except OSError:
|
except OSError:
|
||||||
shutil.rmtree(target, ignore_errors=True)
|
shutil.rmtree(target, ignore_errors=True)
|
||||||
|
|
||||||
|
|
||||||
|
def make_link(target, src):
|
||||||
|
"""Create the platform's live link. Returns (ok, detail-for-humans)."""
|
||||||
|
if IS_WINDOWS:
|
||||||
|
r = subprocess.run(["cmd", "/c", "mklink", "/J", target, src],
|
||||||
|
capture_output=True, text=True)
|
||||||
|
if r.returncode != 0:
|
||||||
|
return False, (r.stderr or r.stdout).strip() or "mklink failed"
|
||||||
|
return True, ""
|
||||||
|
try:
|
||||||
|
os.symlink(src, target, target_is_directory=True)
|
||||||
|
return True, ""
|
||||||
|
except OSError as e:
|
||||||
|
return False, str(e)
|
||||||
|
|
||||||
|
|
||||||
def install(name):
|
def install(name):
|
||||||
global fails, copy_mode_used
|
global fails, copy_mode_used
|
||||||
src = os.path.join(SRC, name)
|
src = os.path.join(SRC, name)
|
||||||
target = os.path.join(DST, name)
|
target = os.path.join(DST, name)
|
||||||
if os.path.exists(target):
|
if os.path.lexists(target):
|
||||||
if is_working_junction(target, src):
|
if is_working_link(target, src):
|
||||||
print("OK %-22s junction (already installed, live-linked to repo)" % name)
|
print("OK %-22s %s (already installed, live-linked to repo)" % (name, LINK_KIND))
|
||||||
return
|
return
|
||||||
remove_existing(target)
|
remove_existing(target)
|
||||||
# try junction first
|
ok, detail = make_link(target, src)
|
||||||
r = subprocess.run(["cmd", "/c", "mklink", "/J", target, src],
|
if ok and is_working_link(target, src):
|
||||||
capture_output=True, text=True)
|
print("OK %-22s %s -> %s" % (name, LINK_KIND, src))
|
||||||
if r.returncode == 0 and is_working_junction(target, src):
|
|
||||||
print("OK %-22s junction -> %s" % (name, src))
|
|
||||||
return
|
return
|
||||||
|
if not ok:
|
||||||
|
reason = detail
|
||||||
|
else:
|
||||||
|
reason = "not followed"
|
||||||
# fall back to copy
|
# fall back to copy
|
||||||
if os.path.exists(target):
|
if os.path.lexists(target):
|
||||||
remove_existing(target)
|
remove_existing(target)
|
||||||
try:
|
try:
|
||||||
shutil.copytree(src, target)
|
shutil.copytree(src, target)
|
||||||
copy_mode_used = True
|
copy_mode_used = True
|
||||||
print("OK %-22s COPY (junction failed: %s)" % (name, (r.stderr or r.stdout).strip() or "not followed"))
|
print("OK %-22s COPY (%s failed: %s)" % (name, LINK_KIND, reason))
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
fails += 1
|
fails += 1
|
||||||
print("FAIL %-22s %s" % (name, e))
|
print("FAIL %-22s %s" % (name, e))
|
||||||
|
|||||||
@@ -36,9 +36,8 @@ The core rule. A guessed `wget` probe ENOENTs at runtime and the container flaps
|
|||||||
inspection). On the demo guest:
|
inspection). On the demo guest:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
SSH=/c/Windows/System32/OpenSSH/ssh.exe; export MSYS_NO_PATHCONV=1
|
|
||||||
for t in wget curl node python3; do
|
for t in wget curl node python3; do
|
||||||
echo "$t: $($SSH felhom-pve "pct exec 9201 -- docker run --rm --entrypoint sh <image>:<tag> -c 'command -v $t'" 2>/dev/null | tr -d '\r')"
|
echo "$t: $(ssh felhom-pve "pct exec 9201 -- docker run --rm --entrypoint sh <image>:<tag> -c 'command -v $t'" 2>/dev/null | tr -d '\r')"
|
||||||
done
|
done
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@@ -5,24 +5,32 @@ description: Build, deploy, publish, or verify ANY Felhom artifact — felhom-co
|
|||||||
|
|
||||||
# Felhom build & deploy runbooks
|
# Felhom build & deploy runbooks
|
||||||
|
|
||||||
All repos live in `E:\git\` (Git Bash: `/e/git/`). Trunk-based: commit+push to `main` first, always.
|
**Claude Code runs on DooPlex (192.168.0.180, Debian 13, `kisfenyo`) — builds are LOCAL commands.**
|
||||||
|
All repos live under `$FELHOM_ROOT/git/`. Trunk-based: commit+push to `main` first, always.
|
||||||
Update the repo's `CHANGELOG.md` (+ `REUSE.md` if a shared helper changed) in the same commit.
|
Update the repo's `CHANGELOG.md` (+ `REUSE.md` if a shared helper changed) in the same commit.
|
||||||
|
|
||||||
## Session invariants (set once, every session)
|
## Session invariants (set once, every session)
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
SSH=/c/Windows/System32/OpenSSH/ssh.exe # Git Bash's /usr/bin/ssh can't reach the Windows agent — fails silently
|
FELHOM_ROOT=/mnt/5_hdd/felhom.eu # working root — ALL felhom repos/build/drill/iso live HERE
|
||||||
export MSYS_NO_PATHCONV=1 # before any ssh felhom-pve command with absolute paths (pct etc.)
|
# repos $FELHOM_ROOT/git/felhom-*, build dirs $FELHOM_ROOT/build/felhom-*
|
||||||
FELHOM_ROOT=/mnt/5_hdd/felhom.eu # build-server working root — ALL felhom repos/build/drill/iso live HERE
|
|
||||||
# (moved off the SSD 2026-07-18; expands locally into the "…" SSH strings below)
|
|
||||||
```
|
```
|
||||||
|
|
||||||
|
> **Clean-tree gate before any build:** `git status --porcelain` must be empty and
|
||||||
|
> `git rev-parse HEAD` must equal `git rev-parse origin/main` in the repo being built. An unpushed
|
||||||
|
> change does not exist — never build a dirty or unpushed tree. The `git pull` in the build step
|
||||||
|
> stays (it is a no-op when you work in this tree, and load-bearing if anything was pushed from
|
||||||
|
> elsewhere).
|
||||||
|
|
||||||
| Host | Access | Role |
|
| Host | Access | Role |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Build server (k3s) | `$SSH kisfenyo@192.168.0.180` | build+push images/binaries (`$FELHOM_ROOT/build/felhom-{controller,hub,agent}`, repos under `$FELHOM_ROOT/git/`), `sudo kubectl` |
|
| **DooPlex (this host, k3s)** | local | build+push images/binaries (`$FELHOM_ROOT/build/felhom-{controller,hub,agent}`, repos under `$FELHOM_ROOT/git/`), `sudo kubectl` |
|
||||||
| Demo Proxmox host | `$SSH felhom-pve` (root@192.168.0.162) | agent deploy, `pct` into guests |
|
| Demo Proxmox host | `ssh felhom-pve` (root@192.168.0.162) | agent deploy, `pct` into guests |
|
||||||
| Demo guest 9201 | via `pct exec 9201 -- bash -c '...'` on felhom-pve | the live controller |
|
| Demo guest 9201 | via `ssh felhom-pve "pct exec 9201 -- bash -c '...'"` | the live controller |
|
||||||
| felhotest (legacy) | `$SSH -p 33022 kisfenyo@router.abonet.hu` | OLD /opt/docker compose mechanism — not the 9201 flow |
|
| felhotest (legacy) | `ssh -p 33022 kisfenyo@router.abonet.hu` | OLD /opt/docker compose mechanism — not the 9201 flow |
|
||||||
|
|
||||||
|
**No browser here:** `claude-in-chrome` is not available on DooPlex. Validate at the endpoint level
|
||||||
|
(invoke the exact endpoint the UI invokes) and say which method was used.
|
||||||
|
|
||||||
Version bumps are ldflags-only (`-X main.version` / `-X main.Version`) — build scripts inject them; no source edit.
|
Version bumps are ldflags-only (`-X main.version` / `-X main.Version`) — build scripts inject them; no source edit.
|
||||||
|
|
||||||
@@ -32,13 +40,13 @@ Version bumps are ldflags-only (`-X main.version` / `-X main.Version`) — build
|
|||||||
the tag written in `/etc/felhom-controller-image` (anonymous Gitea pull). Data volume + encryption key persist.
|
the tag written in `/etc/felhom-controller-image` (anonymous Gitea pull). Data volume + encryption key persist.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. commit+push the repo
|
# 1. commit+push the repo (clean-tree gate above)
|
||||||
# 2. build+push image (build.sh does NOT git-pull — the explicit pull is load-bearing)
|
# 2. build+push image, LOCAL (build.sh does NOT git-pull — the explicit pull is load-bearing)
|
||||||
$SSH kisfenyo@192.168.0.180 "cd $FELHOM_ROOT/build/felhom-controller && git -C $FELHOM_ROOT/git/felhom-controller pull && ./build.sh <VER> --push"
|
cd $FELHOM_ROOT/build/felhom-controller && git -C $FELHOM_ROOT/git/felhom-controller pull && ./build.sh <VER> --push
|
||||||
# 3. deploy in the guest
|
# 3. deploy in the guest
|
||||||
$SSH felhom-pve "pct exec 9201 -- bash -c 'docker pull gitea.dooplex.hu/admin/felhom-controller:<VER> && echo gitea.dooplex.hu/admin/felhom-controller:<VER> > /etc/felhom-controller-image && systemctl restart felhom-controller-bootstrap.service'"
|
ssh felhom-pve "pct exec 9201 -- bash -c 'docker pull gitea.dooplex.hu/admin/felhom-controller:<VER> && echo gitea.dooplex.hu/admin/felhom-controller:<VER> > /etc/felhom-controller-image && systemctl restart felhom-controller-bootstrap.service'"
|
||||||
# 4. verify
|
# 4. verify
|
||||||
$SSH felhom-pve "pct exec 9201 -- docker ps --filter name=felhom-controller --format '{{.Image}} {{.Status}}'"
|
ssh felhom-pve "pct exec 9201 -- docker ps --filter name=felhom-controller --format '{{.Image}} {{.Status}}'"
|
||||||
```
|
```
|
||||||
|
|
||||||
Check current live version first: same `docker ps` command, or `cat /etc/felhom-controller-image`.
|
Check current live version first: same `docker ps` command, or `cat /etc/felhom-controller-image`.
|
||||||
@@ -49,19 +57,19 @@ Runs as the NON-ROOT `felhom-agent` user: `/usr/local/bin/felhom-agent --config
|
|||||||
(systemd `felhom-agent.service`). Sudoers allowlist at `/etc/sudoers.d/felhom-agent`.
|
(systemd `felhom-agent.service`). Sudoers allowlist at `/etc/sudoers.d/felhom-agent`.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# build on 180 (pull first!)
|
# build LOCALLY (pull first!)
|
||||||
$SSH kisfenyo@192.168.0.180 "cd $FELHOM_ROOT/git/felhom-agent && git pull && go build -ldflags '-X main.version=<VER>' -o /tmp/felhom-agent-<VER> ./cmd/felhom-agent"
|
cd $FELHOM_ROOT/git/felhom-agent && git pull && go build -ldflags '-X main.version=<VER>' -o /tmp/felhom-agent-<VER> ./cmd/felhom-agent
|
||||||
# fetch to local, then push to the PVE host (Windows scp needs cygpath -w for the LOCAL path)
|
# copy to the PVE host — ONE hop, no intermediate workstation
|
||||||
scp kisfenyo@192.168.0.180:/tmp/felhom-agent-<VER> "$(cygpath -w /tmp/felhom-agent-<VER>)"
|
scp /tmp/felhom-agent-<VER> felhom-pve:/tmp/
|
||||||
scp "$(cygpath -w /tmp/felhom-agent-<VER>)" felhom-pve:/tmp/
|
|
||||||
# install with backup + restart
|
# install with backup + restart
|
||||||
$SSH felhom-pve "cp /usr/local/bin/felhom-agent /usr/local/bin/felhom-agent.bak-\$(/usr/local/bin/felhom-agent --version | awk '{print \$2}') && install -m0755 /tmp/felhom-agent-<VER> /usr/local/bin/felhom-agent && systemctl restart felhom-agent && sleep 3 && /usr/local/bin/felhom-agent --version && journalctl -u felhom-agent -n 20 --no-pager"
|
ssh felhom-pve "cp /usr/local/bin/felhom-agent /usr/local/bin/felhom-agent.bak-\$(/usr/local/bin/felhom-agent --version | awk '{print \$2}') && install -m0755 /tmp/felhom-agent-<VER> /usr/local/bin/felhom-agent && systemctl restart felhom-agent && sleep 3 && /usr/local/bin/felhom-agent --version && journalctl -u felhom-agent -n 20 --no-pager"
|
||||||
```
|
```
|
||||||
|
|
||||||
**Ship the sudoers + guarded-mkfs wrapper WITH the binary whenever `configs/` changed** — several Go
|
**Ship the sudoers + guarded-mkfs wrapper WITH the binary whenever `configs/` changed** — several Go
|
||||||
guards exist only if the deployed sudoers/wrapper match the binary (globs must match `stageTemp`
|
guards exist only if the deployed sudoers/wrapper match the binary (globs must match `stageTemp`
|
||||||
patterns). Beware CRLF when scp-ing config files through Windows. After restart, check the journal
|
patterns). After restart, check the journal for a clean `ReassertGuestBinds` and no capability-probe
|
||||||
for a clean `ReassertGuestBinds` and no capability-probe degradations.
|
degradations. (Legacy Windows workstation: the copy was two hops via the Windows box, needing
|
||||||
|
`cygpath -w` for the local scp path — and config files scp'd through Windows risked CRLF.)
|
||||||
|
|
||||||
Publish to Gitea (so Day-0 self-install can fetch it): `scripts/publish-agent.sh <ver> <binary>` with
|
Publish to Gitea (so Day-0 self-install can fetch it): `scripts/publish-agent.sh <ver> <binary>` with
|
||||||
`REGISTRY_*` creds. The hub's Day-0 artifact manifest must then vouch the new version — that UI is
|
`REGISTRY_*` creds. The hub's Day-0 artifact manifest must then vouch the new version — that UI is
|
||||||
@@ -74,21 +82,21 @@ operator-password-gated (CC cannot); flag it as an operator follow-up.
|
|||||||
(reverted on next sync), never `:latest`. The live image can lag the CHANGELOG — reconcile via the manifest.
|
(reverted on next sync), never `:latest`. The live image can lag the CHANGELOG — reconcile via the manifest.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# 1. commit+push code 2. build+push image
|
# 1. commit+push code 2. build+push image (LOCAL)
|
||||||
$SSH kisfenyo@192.168.0.180 "cd $FELHOM_ROOT/build/felhom-hub && ./build.sh <VER> --push"
|
cd $FELHOM_ROOT/build/felhom-hub && ./build.sh <VER> --push
|
||||||
# 3. bump manifests/hub.yaml image tag → <VER>, commit, push
|
# 3. bump manifests/hub.yaml image tag → <VER>, commit, push
|
||||||
# 4. hard-refresh + sync (argocd CLI on 180 is not logged in — drive the Application CR)
|
# 4. hard-refresh + sync (argocd CLI is not logged in — drive the Application CR)
|
||||||
$SSH kisfenyo@192.168.0.180 "sudo kubectl -n argocd annotate application felhom argocd.argoproj.io/refresh=hard --overwrite; sleep 8; sudo kubectl -n argocd get application felhom -o jsonpath='{.status.sync.status} {.status.sync.revision}{\"\n\"}'"
|
sudo kubectl -n argocd annotate application felhom argocd.argoproj.io/refresh=hard --overwrite; sleep 8; sudo kubectl -n argocd get application felhom -o jsonpath='{.status.sync.status} {.status.sync.revision}{"\n"}'
|
||||||
$SSH kisfenyo@192.168.0.180 "sudo kubectl -n argocd patch application felhom --type merge -p '{\"operation\":{\"initiatedBy\":{\"username\":\"cc\"},\"sync\":{\"syncStrategy\":{\"apply\":{}}}}}'"
|
sudo kubectl -n argocd patch application felhom --type merge -p '{"operation":{"initiatedBy":{"username":"cc"},"sync":{"syncStrategy":{"apply":{}}}}}'
|
||||||
# 5. verify: Synced/Healthy + rollout + image tag + startup log
|
# 5. verify: Synced/Healthy + rollout + image tag + startup log
|
||||||
$SSH kisfenyo@192.168.0.180 "sudo kubectl -n argocd get application felhom -o jsonpath='sync={.status.sync.status} health={.status.health.status}{\"\n\"}'; sudo kubectl -n felhom-system rollout status deploy/hub --timeout=90s; sudo kubectl -n felhom-system get deploy hub -o jsonpath='{.spec.template.spec.containers[0].image}'; echo; sudo kubectl -n felhom-system logs -l app=hub --tail 10"
|
sudo kubectl -n argocd get application felhom -o jsonpath='sync={.status.sync.status} health={.status.health.status}{"\n"}'; sudo kubectl -n felhom-system rollout status deploy/hub --timeout=90s; sudo kubectl -n felhom-system get deploy hub -o jsonpath='{.spec.template.spec.containers[0].image}'; echo; sudo kubectl -n felhom-system logs -l app=hub --tail 10
|
||||||
```
|
```
|
||||||
|
|
||||||
Green gate before any hub commit: `go build ./... && go vet ./... && go test ./...` in `hub/`.
|
Green gate before any hub commit: `go build ./... && go vet ./... && go test ./...` in `hub/`.
|
||||||
|
|
||||||
## Website (felhom.eu/website)
|
## Website (felhom.eu/website)
|
||||||
|
|
||||||
Push to `main` = deployed (git-sync sidecar, live in ~1–2 min). **Run `python scripts/site_gates.py`
|
Push to `main` = deployed (git-sync sidecar, live in ~1–2 min). **Run `python3 scripts/site_gates.py`
|
||||||
first, after ANY website change** (BOM, emoji, nav parity, cache-bust `?v=N` — bump it when css/svg
|
first, after ANY website change** (BOM, emoji, nav parity, cache-bust `?v=N` — bump it when css/svg
|
||||||
change). New pages must be added to the script's `PAGES` list. Emergency edits: https://files.felhom.eu.
|
change). New pages must be added to the script's `PAGES` list. Emergency edits: https://files.felhom.eu.
|
||||||
|
|
||||||
|
|||||||
@@ -34,8 +34,8 @@ Files: `website/assets/site.css`, `hub/internal/web/templates/style.css`,
|
|||||||
website sections are boxless (rules + spacing, not cards).
|
website sections are boxless (rules + spacing, not cards).
|
||||||
- **Two-tone H1:** last word wrapped in `<span>` (renders `--blue-bright`). H1 only — never H2+.
|
- **Two-tone H1:** last word wrapped in `<span>` (renders `--blue-bright`). H1 only — never H2+.
|
||||||
- **Icons:** monochrome sprite (`icons.svg`, `<use href="...#name">`) / Lucide-style. **No emoji
|
- **Icons:** monochrome sprite (`icons.svg`, `<use href="...#name">`) / Lucide-style. **No emoji
|
||||||
anywhere** — enforced by gates; never hunt emoji with grep (Windows grep false-negatives multibyte;
|
anywhere** — enforced by gates; never hunt emoji with grep (proven to false-negative multibyte on
|
||||||
Python gates only).
|
the Windows workstation, and grep was never the sanctioned check regardless — Python gates only).
|
||||||
- **Fonts:** vendored woff2 (latin-ext for Hungarian) — **no CDN fonts** (gate-enforced on the website).
|
- **Fonts:** vendored woff2 (latin-ext for Hungarian) — **no CDN fonts** (gate-enforced on the website).
|
||||||
- **Language:** Hungarian for everything customer-facing (controller UI, customer emails); English for
|
- **Language:** Hungarian for everything customer-facing (controller UI, customer emails); English for
|
||||||
operator surfaces (hub UI, operator alerts). Hungarian text lives in maps like `severityLabels` /
|
operator surfaces (hub UI, operator alerts). Hungarian text lives in maps like `severityLabels` /
|
||||||
|
|||||||
Reference in New Issue
Block a user