mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-09-05 09:23:33 +02:00
* fix(ai): let the fix step own config.yaml, and require the patch-counter bump The premise that the fix step cannot touch config.yaml turned out to be wrong, and the real problem was the opposite of what it looked like. config.yaml was already in scope — issue-fix.md lists it among the files the sweep reads and owns, and all three merged ai-fix PRs edited it. What they edited, though, was the one thing hard limit 2 forbade outright: PR #2970 qbittorrent version: "5.2.3.2" -> "5.2.3.3" PR #2912 bazarr version: "1.6.0.1" -> "1.6.0.2" Both bumped only the LOCAL PATCH COUNTER, leaving the upstream X.Y.Z alone — i.e. exactly the right thing, in direct violation of the written rule. Nothing enforces that rule (ai_guard_paths.sh only covers .github/ and .templates/), so it has been quietly contradicted by practice, and it also contradicts CLAUDE.md's own PR requirement to bump version. It matters because Supervisor will not offer a rebuild without the bump: a fix merged without one ships inert while the issue looks closed. That is the worst outcome available — worse than not fixing it. So the carve-out is narrowed to what addons_updater actually owns (the `upstream` field and the upstream X.Y.Z), and bumping the trailing .N is now required rather than forbidden, with the dot-not-hyphen trap called out (X.Y.Z-N reads as a semver pre-release and Supervisor treats it as older). Exotic version shapes — LSIO tags, dates, nightlies — are explicitly left alone rather than guessed at. Applied to all four places the rule is stated so they cannot drift: issue-fix.md, issue-execute-plan.md, CLAUDE.md, and pr-coderabbit.md — the last keeps the restriction, since it amends a PR whose single bump already covers it, but now says why instead of reading as a contradiction. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> * fix(ai): derive the patch counter from updater.json, not from version's shape Six review findings, all reproduced against the repo before accepting. Codex (P1) — the rule "increment the trailing .N" is wrong for most of this repo, because you cannot tell a local counter from an upstream component by looking at `version`. Checked all 134 add-ons: version == upstream_version (no counter, must APPEND .1): 82 version == upstream_version + .N (counter, INCREMENT): 8 version drifted from upstream (LEAVE ALONE): 36 no usable updater.json (LEAVE ALONE): 8 So the previous wording would have mutated updater-owned data on 82 add-ons: sonarr's 4.0.19.3001 IS the upstream version, and incrementing it to 4.0.19.3002 burns the identifier of a future real release; linkwarden's 2.16.0 would have become 2.16.1, indistinguishable from an upstream minor bump. updater.json's upstream_version is now the authority: append .1 when version equals it, increment only the digits that follow it, otherwise leave version alone. Validated by running the rule as written over every add-on — 0 violations of the invariant that a bumped version must still start with upstream_version. Copilot — there is no `upstream:` key in any config.yaml (0 of 134); upstream tracking lives in updater.json as upstream_repo / upstream_version. That was inherited text naming a field that does not exist, in all four places. Replaced with the real constraint: never edit updater.json. Copilot — the "a workflow step enforces them" headers over-claimed. Only limit 1 is machine-enforced (ai_guard_paths.sh); the rest ship silently if broken, which is worth saying plainly given limit 2 has been quietly contradicted by practice for months. Copilot — Outcome B produces a plan and no PR, so "say so in the pull request body" had no place to land. Now covers both. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
204 lines
12 KiB
Markdown
204 lines
12 KiB
Markdown
# CLAUDE.md
|
||
|
||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||
|
||
## Repository Overview
|
||
|
||
This is a Home Assistant add-on repository containing 120+ Docker-based add-ons for the Home Assistant Supervisor. Each add-on is a self-contained directory with a Dockerfile, config schema, and S6-overlay init scripts. The repository uses GitHub Actions for CI/CD, linting, and automated upstream version tracking.
|
||
|
||
## Add-On Directory Structure
|
||
|
||
Most add-ons follow this common layout, though exceptions exist (e.g. some archived add-ons use `config.json` instead of `config.yaml`, some add-ons have `build.yaml` instead of `build.json` or no build file at all, and not every add-on includes a `rootfs/` tree):
|
||
|
||
```
|
||
addon_name/
|
||
├── config.yaml # HA add-on metadata, schema, ports, maps
|
||
├── build.json # Base Docker images per architecture (may be build.yaml, or absent)
|
||
├── Dockerfile # Multi-stage build (always uses shared .templates/ scripts)
|
||
├── updater.json # Upstream release tracking; required to enable automatic updates
|
||
├── CHANGELOG.md # Required; must be updated on every PR
|
||
└── rootfs/ # Optional; absent in some add-ons
|
||
└── etc/
|
||
├── cont-init.d/ # S6-overlay init scripts (numbered, run in order)
|
||
└── services.d/ # S6-overlay supervised services (some add-ons use
|
||
# s6-overlay v3 layout at etc/s6-overlay/s6-rc.d/ instead)
|
||
```
|
||
|
||
## Dockerfile Convention
|
||
|
||
Most Dockerfiles follow this 6-section pattern (some add-ons deviate slightly, e.g. using a pinned upstream image directly instead of `ARG BUILD_FROM`):
|
||
|
||
1. **Build Image** – `ARG BUILD_FROM` + `FROM ${BUILD_FROM}`
|
||
2. **Modify Image** – S6 env vars, LSIO modifications via `ha_lsio.sh`
|
||
3. **Install Apps** – Copy `rootfs/`, download modules, install packages
|
||
4. **Entrypoint** – Set `S6_STAGE2_HOOK=/ha_entrypoint.sh`
|
||
5. **Labels** – Standard OCI + HA labels from build args
|
||
6. **Healthcheck** – curl-based check suppressed from nginx/apache logs
|
||
|
||
Shared build-time scripts are pulled from `.templates/` at build time:
|
||
- `ha_automodules.sh` – Downloads module scripts listed in `ARG MODULES=`
|
||
- `ha_autoapps.sh` – Installs packages listed in `ENV PACKAGES=`
|
||
- `ha_entrypoint.sh` – S6 stage-2 hook; launches the cont-init stack at container start
|
||
- `ha_lsio.sh` – Patches LinuxServer.io base images for HA compatibility
|
||
- `bashio-standalone.sh` – Bashio library for scripts outside Supervisor context
|
||
|
||
The `ARG MODULES=` line lists template scripts to download at build time (e.g., `00-banner.sh 01-custom_script.sh 00-smb_mounts.sh`). Commonly-used modules in `.templates/` (not exhaustive):
|
||
- `00-banner.sh` – Print the add-on startup banner
|
||
- `00-global_var.sh` – Initialize global env vars from HA options
|
||
- `00-local_mounts.sh` – Mount local disks (localdisks option)
|
||
- `00-smb_mounts.sh` – SMB/CIFS network mount support
|
||
- `00-deprecated.sh` – Print a deprecation warning for add-ons superseded by official ones
|
||
- `01-config_yaml.sh` – Map HA options → app's `config.yaml`
|
||
- `01-custom_script.sh` – Run user-provided custom scripts
|
||
- `19-json_repair.sh` – Validate/repair JSON config files
|
||
- `90-disable_ingress.sh` – Allow disabling HA ingress
|
||
- `90-dns_set.sh` – Configure custom DNS
|
||
- `91-silent.sh` – Reduce log verbosity
|
||
- `91-universal_graphic_drivers.sh` – GPU driver detection
|
||
- `99-custom_script.sh` – Run a user `script.sh` from the add-on config dir at startup
|
||
|
||
Other helper scripts in `.templates/` used at build/run time: `ha_automatic_packages.sh` (resolve package names across distros), `ha_entrypoint_modif.sh`, `00-aaa_dockerfile_backup.sh`, plus `config.template`/`script.template`/`show_text_color` (templates/assets copied into add-ons).
|
||
|
||
## config.yaml Schema
|
||
|
||
Key fields in every add-on's `config.yaml`:
|
||
|
||
```yaml
|
||
arch: [aarch64, amd64]
|
||
image: ghcr.io/alexbelgium/{slug}-{arch}
|
||
version: "X.Y.Z" # upstream version (format varies; see Versioning section)
|
||
ingress: true/false
|
||
ingress_port: 8000
|
||
map:
|
||
- addon_config:rw # /addon_configs/<hostname>/
|
||
- share:rw
|
||
- media:rw
|
||
- ssl
|
||
schema:
|
||
env_vars: # Allows arbitrary env var passthrough
|
||
- name: match(^[A-Za-z0-9_]+$)
|
||
value: str?
|
||
PUID: int
|
||
PGID: int
|
||
TZ: str?
|
||
networkdisks: str? # SMB mounts
|
||
localdisks: str? # Local disk mounts
|
||
```
|
||
|
||
The `env_vars` schema key enables the env-var passthrough mechanism. At runtime the `00-global_var.sh` cont-init module reads `/data/options.json` and exports each key as an environment variable (writing to `/.env` and `/etc/environment`). `ha_entrypoint.sh` is the S6 stage-2 hook that launches the cont-init stack but does not itself perform the JSON-to-env conversion.
|
||
|
||
## Versioning
|
||
|
||
Add-on versions in `config.yaml` closely follow the upstream release tag and do not conform to a single fixed format. Common patterns include:
|
||
|
||
- `X.Y.Z` – plain upstream semver (e.g. `0.137.0`)
|
||
- `X.Y.Z.N` – upstream version with a local patch counter (e.g. `0.6.26.2`)
|
||
- LSIO-style tags (e.g. `1.43.1.10611-1e34174b1-ls301`)
|
||
- Date-based versions (e.g. `2026.02.28`)
|
||
- Nightly builds (e.g. `nightly-20260321-397`)
|
||
|
||
For the local patch counter, use a dot (`X.Y.Z.N`), not a hyphen. `X.Y.Z-N` parses as a semver pre-release tag, which Home Assistant Supervisor treats as *older* than plain `X.Y.Z` — it will not offer the update. New and updated add-ons should use `.N`; existing `-N` versions should be migrated to `.N` opportunistically (e.g. when that add-on is next touched), not as a standalone repo-wide sweep.
|
||
|
||
When an upstream version is bumped, update `version` in `config.yaml`. If the add-on's `Dockerfile` contains an `ARG BUILD_UPSTREAM` line, update that value too — it is the canonical place that records the upstream version at build time (it is **not** stored in `build.json`/`build.yaml`). Some add-ons do not use `BUILD_UPSTREAM` at all. The `updater.json` file tracks which upstream source/repo to monitor and records the last seen version.
|
||
|
||
## updater.json Format
|
||
|
||
```json
|
||
{
|
||
"source": "github", // github|dockerhub|pip|gitlab|bitbucket|helm_chart|...
|
||
"upstream_repo": "owner/repo",
|
||
"upstream_version": "1.2.3", // auto-populated by addons_updater
|
||
"slug": "addon_slug",
|
||
"last_update": "2025-01-01",
|
||
"github_beta": false,
|
||
"github_fulltag": false, // true = keep "v3.0.1-ls67", false = strip to "3.0.1"
|
||
"github_tagfilter": "", // require this text in release tag
|
||
"github_exclude": "", // exclude releases containing this text
|
||
"paused": false
|
||
}
|
||
```
|
||
|
||
## CI/CD Workflows
|
||
|
||
**On push to master** (`onpush_builder.yaml`): Detects changed add-ons by watching `config.*` files, then sanitizes text files (Unicode spaces → ASCII, CRLF → LF) and restores shell script permissions. Auto-commits fixes with `[nobuild]` to skip rebuild loop.
|
||
|
||
**On PR** (`onpr_check-pr.yaml`): Validates CHANGELOG.md was updated, runs HA addon-linter, and tests Docker build for all changed add-ons.
|
||
|
||
**Weekly** (`lint.yml`): Runs Super-Linter across the repo, fixes shell formatting with shfmt (4-space indent), opens PRs for automated fixes.
|
||
|
||
**Weekly** (`weekly_addons_updater`): Runs the `addons_updater` container to bump add-on versions to match upstream.
|
||
|
||
Other automation workflows:
|
||
- `daily_README.yaml` – Regenerates the root `README.md` add-on table.
|
||
- `weekly_crlftolf.yaml` – Finds and fixes CRLF line endings repo-wide.
|
||
- `weekly_reduceimagesize.yml` – Compresses images and opens a PR with savings.
|
||
- `weekly_stats.yaml` / `helper_stats_graphs.yaml` – Refresh the `Stats`/`Stats2` files and stat graphs.
|
||
- `daily_stale.yml` – Warns and closes stale issues/PRs.
|
||
- `on_issues.yml` / `on_issues_ping_submitter.yml` – Link issues to add-on READMEs and ping submitters.
|
||
- `generate_stargazer_map.yml` – Regenerates the stargazer map image.
|
||
|
||
Adding `[nobuild]` anywhere in a commit message skips the builder workflow.
|
||
|
||
### AI issue triage
|
||
|
||
A tiered, Claude-powered pipeline triages and fixes add-on issues. It escalates
|
||
from cheap classification to a maintainer-approved automated fix, always leaving
|
||
manual actions with precedence. Prompts live in `.github/prompts/`, shared shell
|
||
in `.github/scripts/`.
|
||
|
||
| Workflow | Model | Trigger | Role |
|
||
|---|---|---|---|
|
||
| `on_issues_ai_triage.yaml` | Sonnet-low | issue opened (+ author reply, daily catch-up) | Tier 1: classify, dedupe, answer, ask for info; label `ai-triage` for real add-on bugs |
|
||
| `daily_ai_fix.yaml` | Opus 5-xhigh | daily 03:00 | Tier 2: diagnose the `ai-triage` batch; small+confident → ready PR (`ai:fixed`); else write a plan (`ai:plan-pending`) |
|
||
| `on_issue_approved.yaml` | Opus 5-high | maintainer adds `ai:approved` | Tier 3: execute the approved plan → ready PR |
|
||
| `on_claude_mention.yml` | Sonnet-low | `@claude` by @alexbelgium | Manual interactive override on any issue/PR |
|
||
| `on_pr_coderabbit.yml` | Sonnet-low | CodeRabbit reviews an `ai-fix/*` PR | Once: fix or reply to review comments |
|
||
|
||
Control labels (`ai:*`) are workflow-owned. Key ones: `ai-triage` (queued for
|
||
the sweep), `ai:plan-pending` (plan posted, awaiting `ai:approved`), `ai:fixed`,
|
||
`ai:upstream`, `ai:needs-info` (a reporter reply re-runs tier 1 once),
|
||
`ai:needs-human`, `ai:blocked` (touched protected paths). `no-ai` opts an issue
|
||
out of the automated tiers but not the manual ones. **Kill switch:** set the
|
||
repo variable `AI_DISABLED=true` to pause every AI workflow with no file edits.
|
||
AI fixes must never touch `.github/` or `.templates/` (enforced by
|
||
`ai_guard_paths.sh`). They may edit `config.yaml` freely except the upstream
|
||
part of `version`, and must never edit `updater.json` — `addons_updater` owns
|
||
both. (There is no `upstream:` key in `config.yaml`; upstream tracking lives in
|
||
`updater.json`.) They must still bump the local patch counter so Supervisor
|
||
offers the rebuild —
|
||
without it the fix ships inert. The counter boundary comes from
|
||
`updater.json`'s `upstream_version`, never from the shape of `version`: append
|
||
`.1` when the two are equal (the common case — upstream versions here run to
|
||
four or five components), increment the trailing digits only when `version` is
|
||
`upstream_version` + `.N`, and otherwise leave `version` alone. This rule is
|
||
prompt-only, not machine-enforced.
|
||
|
||
## Linting Rules
|
||
|
||
| Tool | Config | Key ignores |
|
||
|------|--------|------------|
|
||
| Hadolint | `.hadolint.yaml` | DL3002, DL3006-9, DL3018 (no pinning required) |
|
||
| ShellCheck | `.shellcheckrc` | SC2002 |
|
||
| Markdownlint | `.markdownlint.yaml` | MD013 (line length), MD025, MD033, MD041 |
|
||
| shfmt | (CI enforced) | 4-space indent |
|
||
|
||
## PR Requirements
|
||
|
||
1. Update `CHANGELOG.md` in every changed add-on (CI validates this).
|
||
2. Bump `version` in `config.yaml`.
|
||
3. All linting must pass (Hadolint, ShellCheck, Markdownlint, HA addon-linter).
|
||
4. Docker build must succeed for all declared architectures.
|
||
|
||
## S6-Overlay Init Script Naming
|
||
|
||
Scripts in `rootfs/etc/cont-init.d/` run in lexicographic order. Common numbering conventions (many add-ons use other prefixes too):
|
||
- `20-*` – Directory/folder setup
|
||
- `32-*` – Nginx ingress configuration (e.g. `32-nginx_ingress.sh`)
|
||
- `80-*` – Application configuration
|
||
- `90-*` – Misc pre-startup tasks (ssl, vpn, custom run)
|
||
- `99-*` – Final startup / launch
|
||
|
||
## User Customization
|
||
|
||
Add-ons support end-user customization without rebuilding the image (see the repo wiki). At startup, `99-custom_script.sh` looks in the add-on's config directory for a user-provided `script.sh` (seeded from `.templates/script.template`) and executes it. Combined with the `env_vars` passthrough and the custom-script modules, this lets users inject commands and environment without forking the add-on.
|