mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-09-18 15:47:37 +02:00
Compare commits
21 Commits
create-pul
...
fc07a2d1b4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fc07a2d1b4 | ||
|
|
a3bba58229 | ||
|
|
af2736a170 | ||
|
|
d31ccfa21c | ||
|
|
9c277876ff | ||
|
|
7e59aa8803 | ||
|
|
360fd669fa | ||
|
|
7bf77c8558 | ||
|
|
a195d3ac39 | ||
|
|
14834d4063 | ||
|
|
f5caafd62c | ||
|
|
fd61631ccd | ||
|
|
578a7f8368 | ||
|
|
5115dcb93a | ||
|
|
fa5e890c4c | ||
|
|
addb0ca66f | ||
|
|
16e112b21f | ||
|
|
040a4e0f2f | ||
|
|
f72f7bef0f | ||
|
|
c18f45d822 | ||
|
|
abf3d76873 |
@@ -4,29 +4,30 @@ description: >-
|
||||
Workflow for alexbelgium/hassio-addons Home Assistant add-on work: diagnose with real
|
||||
measurements, independent Codex review, implement, open a PR, resolve CodeRabbit / Copilot /
|
||||
Codex bot review comments, verify in production. Use for any task touching an add-on in this
|
||||
repo — bugs, RAM/CPU/performance tuning, Dockerfile, config.yaml, cont-init.d or s6 changes,
|
||||
version bumps, opening or iterating PRs — and when asked to "check with codex", "verify with
|
||||
chatgpt", or resolve bot comments. Cheap for small asks: a light path skips the heavy steps.
|
||||
repo — bugs, RAM/CPU/performance tuning, Dockerfile, config.yaml, build.json,
|
||||
updater.json, cont-init.d, services.d or s6 changes, version and CHANGELOG bumps, a failing
|
||||
add-on CI check, opening or iterating PRs — and, on an add-on task, when asked to "check with
|
||||
codex", "verify with chatgpt", or resolve bot comments. Cheap for small asks: a light path skips
|
||||
the heavy steps.
|
||||
---
|
||||
|
||||
# Home Assistant add-on workflow
|
||||
|
||||
**Answer style.** Chat replies are terse: no pleasantries, no tool-call narration, no decorative
|
||||
tables or emoji, no dumped logs — quote the shortest decisive line, and don't re-read or re-print
|
||||
what is already in context. Fragments and dropped articles are fine. Never compressed: uncertainty
|
||||
markers ("likely", "assumed", "not verified"), negations (`not`/`never`/`no`/`only`), numbers,
|
||||
units, technical terms, code blocks, error strings — step 9's Verified/Checked/Assumed distinction
|
||||
outranks brevity every time. Write in full prose, not fragments, for security warnings,
|
||||
irreversible-action confirmations, and any multi-step sequence a fragment could make ambiguous.
|
||||
Persisted text is prose too: commits, CHANGELOG entries, PR bodies, review-thread replies, the
|
||||
step 10 report.
|
||||
**Answer style.** Chat replies are terse — no pleasantries, tool-call narration, decorative tables,
|
||||
emoji or dumped logs; quote the shortest decisive line and don't re-print what is already in
|
||||
context. Telegraphic fragments are fine *there*. Two things outrank brevity, because dropping a
|
||||
word from either changes the meaning rather than shortening it: never compress uncertainty markers
|
||||
("likely", "assumed", "not verified"), negations, numbers, units, technical terms, code or error
|
||||
strings — step 9's Verified/Checked/Assumed distinction wins every time; and write full prose
|
||||
wherever a fragment could be read two ways — security warnings, irreversible-action confirmations,
|
||||
multi-step sequences, and everything persisted (commits, CHANGELOG entries, PR bodies, review-thread
|
||||
replies, the step 10 report).
|
||||
|
||||
Triage first, then one of two paths:
|
||||
|
||||
- **Light** — typo/doc fixes, CHANGELOG edits, version bumps, one-file edits at ladder levels
|
||||
1-3 (below), simple questions: scope → implement → validate (`$SKILL/scripts/validate.sh
|
||||
<addon> --vs-master`; `$SKILL` defined below) → PR (version bump + CHANGELOG still required) →
|
||||
resolve bot comments.
|
||||
1-3 (below), simple questions: scope → implement → validate (step 4) → PR (version bump +
|
||||
CHANGELOG still required) → resolve bot comments.
|
||||
- **Full loop** — performance/RAM/CPU work, diagnosis, anything changing a shipped default,
|
||||
ladder levels 4-6, or an explicit Codex-check request: scope → measure → plan → Codex reviews
|
||||
the plan → implement → simplify → Codex reviews the code → **simplify again** → PR → resolve
|
||||
@@ -47,9 +48,8 @@ reasoning about a hypothetical *host* either. A defensive branch is complexity l
|
||||
the input that reaches it and the image or host where that happens, or delete it and let the case
|
||||
fail visibly instead.
|
||||
|
||||
**Repo layout.** `alexbelgium/hassio-addons`; each add-on is a top-level directory. This skill is
|
||||
checked in at `.claude/skills/hassio-addon-workflow/` (canonical copy). Set the skill root once,
|
||||
then every `scripts/…` and `references/…` path below is relative to it:
|
||||
**Skill root.** The canonical copy lives at `.claude/skills/hassio-addon-workflow/`. Set it once;
|
||||
every `scripts/…` and `references/…` path below is relative to it:
|
||||
|
||||
```bash
|
||||
SKILL="$(git rev-parse --show-toplevel)/.claude/skills/hassio-addon-workflow"
|
||||
@@ -63,9 +63,10 @@ bash "$SKILL/scripts/preflight.sh" # and likewise for the other scripts
|
||||
|
||||
**Delegate heavy output to a subagent.** Codex reviews and multi-thread PR triage produce output
|
||||
you don't need verbatim in your own context. For Codex's plan review (step 3), Codex's code
|
||||
review (step 6), and PR-comment listing when there are more than ~5 threads (step 8): launch a
|
||||
subagent to run the command and report back only the objections/findings and your assessment of
|
||||
each, not the raw transcript.
|
||||
review (step 6), and PR-comment listing when there are more than ~5 threads (step 8): if subagents
|
||||
are available, launch one to run the command and report back only the objections/findings and your
|
||||
assessment of each, not the raw transcript. Otherwise redirect the output to a file and read the
|
||||
parts you need.
|
||||
|
||||
---
|
||||
|
||||
@@ -85,9 +86,11 @@ Measure the running add-on rather than reasoning from source (`$BUILD_VERSION` s
|
||||
- Is this flag/driver/package actually present? → inspect the artifact: `/proc/<pid>/cmdline`,
|
||||
`command -v`, `/var/log/apt/history.log`
|
||||
|
||||
Verify you're reading the right revision first — `scripts/preflight.sh` catches a stale branch
|
||||
before it costs a full analysis pass. Measurement methodology, gotchas, and real failure examples:
|
||||
`references/evidence.md`.
|
||||
Verify you're reading the right revision first — `scripts/preflight.sh` compares the checkout
|
||||
against the running `$BUILD_VERSION`, which catches the usual stale branch before it costs a full
|
||||
analysis pass (a version match does not prove the source is identical). Read
|
||||
`references/evidence.md` before interpreting any number you did not get straight from
|
||||
`measure.sh`, and for the failure modes this step exists to prevent.
|
||||
|
||||
## 3. Plan — choose the mechanism level, then Codex reviews it (full loop)
|
||||
|
||||
@@ -117,45 +120,60 @@ Attack your own plan before implementing:
|
||||
- What am I **inferring** that I could instead **detect at runtime** or **record explicitly**?
|
||||
Highest-yield question here — see `references/evidence.md`'s failure-mode section.
|
||||
- For every branch that exists **only to survive something going wrong**: name the image or host
|
||||
where that input actually arrives, and go and look. Naming is the bar, not reproducing it here —
|
||||
`references/simplify.md` works the `/dev/shm` guard and the `s6-dumpenv` fallback through that
|
||||
distinction.
|
||||
where that input arrives (the standing rule above) and go and look, before you write it.
|
||||
|
||||
Full loop only, before writing code: get Codex's independent read on the plan. Spawn a subagent
|
||||
whose prompt includes the path `references/codex-review.md` and tells it to follow that file's
|
||||
invocation, then report back only Codex's objections and an assessment of each — not the raw
|
||||
transcript.
|
||||
Full loop only, before writing code: get Codex's independent read on the plan, delegated as above —
|
||||
`references/codex-review.md` has the invocation and how to write the prompt.
|
||||
|
||||
## 4. Implement
|
||||
|
||||
Touching a shell script, Dockerfile, or env option? Read `references/traps.md` first — skim the
|
||||
headings, read the sections you're about to touch; the bashio, s6-env, arch-guard and versioning
|
||||
traps are all live. (The light-path facts it holds — versioning format, CHANGELOG heading — are
|
||||
already inline in step 7.) Validate with `scripts/validate.sh <addon> --vs-master`. Write
|
||||
behavioural tests for anything with branches, targeting **the regression a reviewer described**,
|
||||
not just the happy path.
|
||||
Read the `references/traps.md` section matching what you're about to touch. It is ~18 KB and all
|
||||
but one section is irrelevant to any given edit, so print the one you need rather than reading the
|
||||
file — run it with no argument to list the sections:
|
||||
|
||||
| Touching | Run |
|
||||
| --- | --- |
|
||||
| an option or anything a base-image service reads | `bash "$SKILL/scripts/traps.sh" passing` |
|
||||
| a file the app also writes itself | `bash "$SKILL/scripts/traps.sh" "app's own"` |
|
||||
| shell, bashio, a symlinked script | `bash "$SKILL/scripts/traps.sh" bashio` |
|
||||
| `Dockerfile`, `build.json`, an arch guard | `bash "$SKILL/scripts/traps.sh" dockerfile` |
|
||||
| Chromium, Electron, Xvfb | `bash "$SKILL/scripts/traps.sh" chromium` |
|
||||
|
||||
Then validate with `scripts/validate.sh <addon> --vs-master`, and write behavioural tests for
|
||||
anything with branches, targeting **the regression a reviewer described**, not just the happy
|
||||
path.
|
||||
|
||||
## 5. Simplify
|
||||
|
||||
Before requesting review, check: did the diff stay at the ladder level chosen in step 3? Can this
|
||||
be solved by deleting instead of adding? Is the fix bigger than what it fixes? How does it fail in
|
||||
three years? And on reuse: does any hunk reimplement something `.templates/`, another script in
|
||||
this add-on, or a sibling add-on already does — and if a future add-on hits this same problem,
|
||||
will it find one way to solve it or two? Fold a near-duplicate into the existing mechanism, or
|
||||
justify the divergence in the PR body — but never at the cost of an isolation rule
|
||||
`references/traps.md` documents: scripts shared by symlink with the webtop add-ons take a new
|
||||
numbered script, not an edit. Case studies of what happens when this check is skipped:
|
||||
`references/simplify.md`.
|
||||
Six questions over your own diff, before anyone else reads it:
|
||||
|
||||
- **Level** — did the diff stay at the ladder level chosen in step 3, or creep up one?
|
||||
- **Deletion** — can this be solved by deleting instead of adding?
|
||||
- **Size** — is the fix bigger than the thing it fixes?
|
||||
- **Reuse** — does any hunk reimplement what `.templates/`, another script in this add-on, or a
|
||||
sibling add-on already does? If a future add-on hits this problem, will it find one way to solve
|
||||
it or two? Fold near-duplicates in, or justify the divergence in the PR body.
|
||||
- **Depth** — is this a special case bolted onto shared infrastructure? Fix the shared mechanism
|
||||
instead once more than one add-on hits it; generalising from a single case is how bespoke
|
||||
designs get built, so below that bar the special case is the right call.
|
||||
- **Longevity** — how does this fail in three years, when the base image or upstream has moved?
|
||||
|
||||
The standing exception to Reuse and Depth: scripts shared by symlink with the webtop add-ons take a
|
||||
new numbered script, never an edit (`references/traps.md#shell-and-bashio`). Case studies for the rest, including
|
||||
what shipped when this pass was skipped: `references/simplify.md`.
|
||||
|
||||
## 6. Codex attacks the code, then simplify what the review added (full loop only)
|
||||
|
||||
Same delegated invocation, pointed at `git diff origin/master...HEAD` plus your reasoning per
|
||||
hunk. Details in `references/codex-review.md`.
|
||||
|
||||
Then run step 5's checks again over the hunks the review changed. Adversarial review only ever
|
||||
argues *for* another branch — that is its job — so accepting objections ratchets the diff upward,
|
||||
and nothing else in the loop walks it back down. For each accepted objection: is the case it
|
||||
defends one you have now demonstrated, or one you have merely been told about? Taking a
|
||||
Then run step 5's checks again over the hunks the review changed. Adversarial review mostly argues
|
||||
*for* another branch — that is what it is asked to do — so accepting objections tends to ratchet
|
||||
the diff upward, and nothing else in the loop walks it back down. Sort each objection before you
|
||||
write anything:
|
||||
**"this is wrong"** is a bug and you fix it; **"this is undefended"** is a claim about some host,
|
||||
and it needs the same demonstration you would demand of a measurement — is the case it defends one
|
||||
you have now demonstrated, or one you have merely been told about? Taking a
|
||||
correctness objection often deletes the code that made it necessary, and a fix that collapses back
|
||||
to fewer lines than you started the review with is the normal outcome, not a suspicious one.
|
||||
|
||||
@@ -165,20 +183,23 @@ kind of edit that leaves a stray `fi` behind.
|
||||
|
||||
## 7. Open the PR
|
||||
|
||||
CI gates on a PR: **`CHANGELOG.md` updated** (hard fail), the **HA add-on linter**
|
||||
(`frenck/action-addon-linter`, blocking — not the weekly Super-Linter, which is non-blocking), and
|
||||
the **add-on image build**. Bump `version` anyway (`X.Y.Z.N`, never `X.Y.Z-N`, see
|
||||
`references/traps.md#versioning`) — Supervisor won't offer a rebuild without it. Update
|
||||
`README.md` if you added options; write the CHANGELOG heading as `## <version> (<date>)`,
|
||||
matching the date format already in that file — almost always ISO `YYYY-MM-DD`, see
|
||||
`references/traps.md#ci-and-review-bots`.
|
||||
Three hard gates — **`CHANGELOG.md` updated**, the **HA add-on linter**
|
||||
(`frenck/action-addon-linter`), and the **add-on image build** — but only on a PR that changes a
|
||||
top-level `config.*`. On a PR that doesn't (docs, `.github/`, `.claude/`) they *skip*, which is not
|
||||
the same as passing. Super-Linter runs on every PR and is `continue-on-error`, so it never blocks;
|
||||
fix its real findings anyway. Nothing checks the version bump, so bump it yourself — Supervisor
|
||||
won't offer a rebuild without one, and `CLAUDE.md` has the format. Update `README.md` if you added
|
||||
options; write the CHANGELOG heading as `## <version> (<date>)`, matching the date format already
|
||||
in that file — almost always ISO `YYYY-MM-DD`, see `references/traps.md#ci-and-review-bots`.
|
||||
|
||||
Write the body to a file, `gh pr create --body-file`: state what was measured, what changed,
|
||||
**what is not verified**, and how to roll back the riskiest hunk alone.
|
||||
|
||||
## 8. Resolve review comments
|
||||
|
||||
`scripts/pr_review.sh list|reply|resolve|status|watch <PR>`. For every comment, **reproduce the
|
||||
`scripts/pr_review.sh list|status|watch <PR>` to read, `reply <PR> <COMMENT_ID> <text|@file>` and
|
||||
`resolve <PR> <THREAD_ID…|--all>` to answer; run it with no arguments for the full usage. For every
|
||||
comment, **reproduce the
|
||||
claim before agreeing or disagreeing** — reviewers are frequently right and occasionally
|
||||
confidently wrong; a reproduction takes a minute and decides it either way. Reply with the
|
||||
evidence, then resolve. **Push back when you're right**, on the thread — a resolved-but-wrong
|
||||
@@ -192,13 +213,12 @@ work" — either it was exercised, or say plainly it wasn't.
|
||||
|
||||
Light path: verification is `validate.sh` plus CI; anything beyond that is Assumed. Full loop: CI
|
||||
passing proves the build works, not that the change does anything — re-run the measurement that
|
||||
motivated the work once the rebuilt add-on is running. After merge, `git fetch origin master`
|
||||
(the tracking ref is stale otherwise), then confirm the *changes* survived — `git diff
|
||||
origin/master -- <the paths you touched>` comes back empty. Ancestry is not the check: a revert
|
||||
leaves your commit in history and undoes its tree, so `--contains` reports success either way. The
|
||||
builder's revert-on-failure job can revert a merge for reasons unrelated to your diff (see
|
||||
`references/traps.md#ci-and-review-bots`). Real "merged and inert" examples, and what
|
||||
to do when a fix can't be self-verified: `references/evidence.md`.
|
||||
motivated the work once the rebuilt add-on is running. Real "merged and inert" examples, and what
|
||||
to do when a fix cannot be self-verified: `references/evidence.md`. Then confirm the change
|
||||
survived the merge: `git fetch origin master` first (the tracking ref is stale otherwise), then
|
||||
`git diff origin/master -- <the paths you touched>` must come back empty. Ancestry is not the
|
||||
check, and the builder reverts merges for reasons unrelated to your diff — both explained in
|
||||
`references/traps.md#ci-and-review-bots`.
|
||||
|
||||
## 10. Calibrate and report
|
||||
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
|
||||
Used for step 3 (plan review) and step 6 (code review), full loop only. Codex is a genuinely
|
||||
different model reading the files itself; on this workload it has repeatedly been worth the
|
||||
minutes. Delegate the invocation to a subagent (see SKILL.md's subagent-delegation note) so its
|
||||
output doesn't land verbatim in your context — have the subagent return only Codex's objections
|
||||
and your assessment of each.
|
||||
minutes. Run it through a subagent, per SKILL.md's delegation note.
|
||||
|
||||
## Invocation
|
||||
|
||||
@@ -12,11 +10,12 @@ and your assessment of each.
|
||||
on ~4 KB prompts (2026-08-03); the CLI with the same content succeeded. The MCP tool is still fine
|
||||
for short questions.
|
||||
|
||||
`--sandbox read-only` lets Codex read files but blocks writes and command execution, and
|
||||
`approval_policy=never` means it will not be prompted for permission to run anything either — so
|
||||
paste every number into the prompt rather than expecting Codex to gather it. `- <` feeds the
|
||||
prompt file on stdin. Run it in the background so you are not blocked for the several minutes it
|
||||
takes (`&` here, or your harness's background-task mechanism):
|
||||
`--sandbox read-only` blocks writes, not reads, and `approval_policy=never` stops it asking for
|
||||
permission rather than stopping it acting — so it can still run read-only commands and often
|
||||
falls back to fetching the repo from GitHub instead of reading your worktree. Paste every number
|
||||
into the prompt rather than expecting it to gather them, and treat what it reports about *local*
|
||||
state as unverified. `- <` feeds the prompt file on stdin. Run it in the background so you are not
|
||||
blocked for the several minutes it takes (`&` here, or your harness's background-task mechanism):
|
||||
|
||||
```bash
|
||||
codex exec --model gpt-5.6-sol --sandbox read-only --skip-git-repo-check \
|
||||
@@ -40,9 +39,5 @@ codex exec --model gpt-5.6-sol --sandbox read-only --skip-git-repo-check \
|
||||
confidently, and separately caught a genuine methodology error in the same review. Treat its
|
||||
confirmations with the same scepticism as its objections — especially about the build.
|
||||
|
||||
**Its objections ratchet complexity upward.** An adversarial reviewer is asked to find what could
|
||||
go wrong, so its output is a list of arguments for more code; it is never asked whether the branch
|
||||
it wants is reachable. Separate "this is wrong" from "this is undefended" before you write
|
||||
anything: the first is a bug and you fix it, the second is a claim about some host, and it needs
|
||||
the same demonstration you would demand of a measurement. That is what step 6's second simplify
|
||||
pass is for.
|
||||
**Its objections only ever argue for more code** — it is asked what could go wrong, never whether
|
||||
the branch it wants is reachable. Sort them before writing anything; SKILL.md step 6 is that pass.
|
||||
|
||||
@@ -1,16 +1,24 @@
|
||||
# Evidence — measurement methodology and case studies
|
||||
|
||||
## Why summed RSS and reserved-vs-resident both matter
|
||||
## How to read the numbers
|
||||
|
||||
- **Summed RSS double-counts shared pages.** Removing a duplicate process frees its *private*
|
||||
memory, not its RSS. `scripts/measure.sh` reports PSS and private alongside RSS — quote
|
||||
**private** when arguing "removing this saves N MB".
|
||||
- **A big mapping is not necessarily resident.** Large SysV/tmpfs segments are lazily populated;
|
||||
reserved size is reported separately from resident for this reason.
|
||||
- `/proc/meminfo` and `free` show **host** figures (no memory cgroup namespace here) — never
|
||||
attribute those to the add-on.
|
||||
- Sample duration matters: a 3 s CPU sample measured 2.3% where a 20 s sample measured 21.6% for
|
||||
the same process. Use ≥20 s for anything you report.
|
||||
**Summed RSS overstates savings.** Shared library pages are counted once per process, so removing
|
||||
a duplicate frees its *private* memory, not its RSS. Measured example: four MCP shims summed to
|
||||
882 MB RSS but 643 MB PSS / 564 MB private, and per-process private ranged 54 MB down to 2 MB —
|
||||
which completely changes which duplicate is worth removing. Quote private when arguing "removing
|
||||
this saves N MB".
|
||||
|
||||
**A large mapping is often not resident.** SysV/tmpfs segments are lazily populated. Xvfb's
|
||||
506 MB framebuffer shows `Rss: 0` in `/proc/<pid>/smaps`. Check before calling anything a leak.
|
||||
|
||||
**`/proc/meminfo` and `free` show host figures** — there is no memory cgroup namespace here.
|
||||
Never attribute those totals to the add-on.
|
||||
|
||||
**A short CPU sample is not a CPU measurement.** A 3 s sample measured 2.3% where a 20 s sample
|
||||
measured 21.6% for the same process. Use >= 20 s for anything you report.
|
||||
|
||||
`scripts/measure.sh` already reports PSS and private alongside RSS, and resident separately from
|
||||
reserved, so these three only bite when you compute a figure yourself or quote one from `ps`.
|
||||
|
||||
## Before asserting anything, ask what would show it false
|
||||
|
||||
|
||||
@@ -30,28 +30,24 @@ Being able to build the complicated thing is not a reason to.
|
||||
It took the maintainer asking "is this the simplest way possible?" to run the pass that step 6
|
||||
now requires.
|
||||
|
||||
## Checks worth running against your own diff
|
||||
## Where SKILL.md step 5's questions get hard
|
||||
|
||||
- **Did the diff stay at the ladder level chosen in step 3?** If it crept up a level, either
|
||||
justify that out loud or redo it at the level you chose.
|
||||
- **Can this be solved by deleting instead of adding?** A flag that shouldn't be passed, a
|
||||
process that shouldn't start, a registration that shouldn't be duplicated. Deleting usually
|
||||
shrinks the regression surface — but not always: the `/dev/shm` case in
|
||||
`references/evidence.md` is a removal that reintroduced a crash loop on hosts unlike this one.
|
||||
A removal that depends on a host default still needs the same verification as an addition.
|
||||
- **Is the fix bigger than the thing it fixes?** That is a smell, not a rule — but it usually
|
||||
means the problem was framed one level too deep.
|
||||
- **For each defensive branch: what input reaches it, on which image or host?** Go and check,
|
||||
the way you would check a measurement. The bar is being able to **name** the case, not to
|
||||
reproduce it here: Docker's 64 MB `/dev/shm` default is documented behaviour that HA does not
|
||||
override, so the bullet above keeps that guard even though this host measured 7.7 GB. Nobody
|
||||
could name a single image shipping `with-contenv` without `s6-dumpenv`, so that fallback went.
|
||||
If you cannot name the case, delete the branch — the situation then fails the way it already
|
||||
fails today, visibly, instead of through a second path that is never exercised and silently
|
||||
rots as the base images move. Weigh the cost too: a one-flag guard against a crash you cannot
|
||||
rule out is cheap, a second code path that degrades to the pre-fix behaviour anyway is not.
|
||||
Write down in the PR body what you cut and why, so the next person does not re-add it from the
|
||||
same reasoning.
|
||||
- **How does this fail in three years**, when the base image, Electron, or upstream has moved?
|
||||
Code that reads a documented knob keeps working. Code that reaches into private internals
|
||||
does not.
|
||||
**Deletion is not automatically the safe direction.** The `/dev/shm` case in `evidence.md` is a
|
||||
removal that reintroduced a crash loop on hosts unlike this one. A removal that depends on a host
|
||||
default needs the same verification as an addition.
|
||||
|
||||
**Naming the case is the bar for a defensive branch, not reproducing it.** Docker's 64 MB
|
||||
`/dev/shm` default is documented behaviour that Home Assistant does not override, so that guard
|
||||
stays even though this host measured 7.7 GB. Nobody could name a single image shipping
|
||||
`with-contenv` without `s6-dumpenv`, so that fallback went. If you cannot name the case, delete the
|
||||
branch: the situation then fails the way it already fails today, visibly, instead of through a
|
||||
second path that is never exercised and silently rots as the base images move. Weigh the cost both
|
||||
ways — a one-flag guard against a crash you cannot rule out is cheap; a second code path that
|
||||
degrades to the pre-fix behaviour anyway is not. Write in the PR body what you cut and why, so the
|
||||
next person does not re-add it from the same reasoning.
|
||||
|
||||
**"Bigger than the thing it fixes" is a smell, not a rule** — but it usually means the problem was
|
||||
framed one level too deep.
|
||||
|
||||
**Three-year failure** favours code that reads a documented knob. Code that reaches into private
|
||||
internals does not survive the base image moving.
|
||||
|
||||
@@ -9,7 +9,6 @@ workflows and lint rules — that is not repeated here.
|
||||
## Contents
|
||||
|
||||
- [Environment and workspace](#environment-and-workspace)
|
||||
- [Measurement](#measurement)
|
||||
- [Passing values into base-image services](#passing-values-into-base-image-services)
|
||||
- [Writing into an app's own config](#writing-into-an-apps-own-config)
|
||||
- [Shell and bashio](#shell-and-bashio)
|
||||
@@ -46,20 +45,6 @@ gate. One observed run took ~3 hours, with 20+ runs queued against 2 executing
|
||||
runner contention, not the diff. Check `gh run list` before concluding your PR is stuck. Poll in
|
||||
a background task, and never claim the build is verified when it hasn't run.
|
||||
|
||||
## Measurement
|
||||
|
||||
**Summed RSS overstates savings.** Shared library pages are counted once per process, so removing
|
||||
a duplicate frees its *private* memory, not its RSS. Measured example: four MCP shims summed to
|
||||
882 MB RSS but 643 MB PSS / 564 MB private, and per-process private ranged 54 MB down to 2 MB —
|
||||
which completely changes which duplicate is worth removing. Quote private when arguing "removing
|
||||
this saves N MB".
|
||||
|
||||
**A large mapping is often not resident.** SysV/tmpfs segments are lazily populated. Xvfb's
|
||||
506 MB framebuffer shows `Rss: 0` in `/proc/<pid>/smaps`. Check before calling anything a leak.
|
||||
|
||||
**`/proc/meminfo` and `free` show host figures** — there is no memory cgroup namespace here.
|
||||
Never attribute those totals to the add-on.
|
||||
|
||||
**`rtk` filters some command output.** For a complete listing, redirect to a file and read that
|
||||
(`ps ... > $SP/ps.txt`), or use `rtk proxy <cmd>`.
|
||||
|
||||
@@ -190,11 +175,9 @@ build.
|
||||
|
||||
## Versioning
|
||||
|
||||
**`X.Y.Z.N`, never `X.Y.Z-N`.** A hyphen parses as a semver pre-release, which Supervisor treats
|
||||
as *older* than `X.Y.Z` — the update is never offered.
|
||||
|
||||
Date-based versions (`2026.08.03`) are common here. Check whether master has already moved to the
|
||||
version you were about to use.
|
||||
`CLAUDE.md` owns the format (`X.Y.Z.N`, never `X.Y.Z-N`, and why). The one thing it does not say:
|
||||
date-based versions (`2026.08.03`) are common here, so check whether master has already moved to
|
||||
the version you were about to use before you pick it.
|
||||
|
||||
## Chromium / Electron under Xvfb
|
||||
|
||||
|
||||
@@ -5,7 +5,14 @@
|
||||
# Usage: preflight.sh [repo-path] [addon-slug]
|
||||
set -uo pipefail
|
||||
|
||||
REPO="${1:-/data/claude/hassio-addons}"
|
||||
# Default to the checkout this is run from, never a fixed path: a fixed path silently inspected
|
||||
# the main checkout while the caller worked in a worktree, reporting a branch nobody was editing —
|
||||
# the exact stale-checkout trap this script exists to catch. Falling back to one when git cannot
|
||||
# answer would recreate it, so refuse instead and make the caller say which repo they mean.
|
||||
REPO="${1:-}"
|
||||
if [ -z "$REPO" ]; then
|
||||
REPO=$(git rev-parse --show-toplevel 2> /dev/null) || REPO=""
|
||||
fi
|
||||
SLUG="${2:-}"
|
||||
|
||||
echo "== tools =="
|
||||
@@ -26,9 +33,14 @@ fi
|
||||
echo
|
||||
echo "== repo =="
|
||||
# git-aware check: in a worktree .git is a file, not a directory
|
||||
if [ -z "$REPO" ]; then
|
||||
echo " not inside a git checkout, and no repo path given"
|
||||
echo " -> pass one explicitly: preflight.sh <repo-path> [addon-slug]"
|
||||
exit 1
|
||||
fi
|
||||
if ! git -C "$REPO" rev-parse --git-dir > /dev/null 2>&1; then
|
||||
echo " no git repo at $REPO"
|
||||
exit 0
|
||||
exit 1
|
||||
fi
|
||||
cd "$REPO" || exit 0
|
||||
branch=$(git branch --show-current 2> /dev/null || echo "(detached)")
|
||||
|
||||
49
.claude/skills/hassio-addon-workflow/scripts/traps.sh
Executable file
49
.claude/skills/hassio-addon-workflow/scripts/traps.sh
Executable file
@@ -0,0 +1,49 @@
|
||||
#!/usr/bin/env bash
|
||||
# Print one section of references/traps.md.
|
||||
#
|
||||
# traps.md is ~18 KB and every section but one is irrelevant to any given edit: a shell fix needs
|
||||
# 642 bytes of it, a Dockerfile fix 2.6 KB, and "CI and review bots" — 35% of the file — is needed
|
||||
# at steps 7-8 and never at step 4. A markdown anchor cannot be loaded on its own, so reading the
|
||||
# file to reach one section pays for all of them. This prints just the section, so the routing
|
||||
# table in SKILL.md step 4 costs what it claims to.
|
||||
#
|
||||
# Usage: traps.sh <keyword> # substring of a section heading, case-insensitive
|
||||
# traps.sh # list the sections
|
||||
set -uo pipefail
|
||||
|
||||
TRAPS="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/references/traps.md"
|
||||
[ -f "$TRAPS" ] || { echo "not found: $TRAPS" >&2; exit 1; }
|
||||
|
||||
# The Contents list duplicates the headings; grep the headings themselves so the list cannot drift.
|
||||
list() {
|
||||
echo "sections (pass any substring):"
|
||||
grep '^## ' "$TRAPS" | grep -v '^## Contents' | sed 's/^## / /'
|
||||
}
|
||||
|
||||
[ $# -eq 0 ] && { list; exit 0; }
|
||||
|
||||
# awk over exact heading text: section names contain '/' and other characters that would need
|
||||
# escaping in a sed address, and a keyword matching several headings should be an error, not a
|
||||
# silent pick of the first.
|
||||
mapfile -t matches < <(grep '^## ' "$TRAPS" | grep -v '^## Contents' |
|
||||
grep -iF -- "$1" | sed 's/^## //')
|
||||
|
||||
case "${#matches[@]}" in
|
||||
0)
|
||||
echo "no section matching '$1'" >&2
|
||||
list >&2
|
||||
exit 1
|
||||
;;
|
||||
1) ;;
|
||||
*)
|
||||
echo "'$1' matches ${#matches[@]} sections — be more specific:" >&2
|
||||
printf ' %s\n' "${matches[@]}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
awk -v want="## ${matches[0]}" '
|
||||
$0 == want { inside = 1; print; next }
|
||||
inside && /^## / { exit }
|
||||
inside { print }
|
||||
' "$TRAPS"
|
||||
2
.github/workflows/daily_ai_fix.yaml
vendored
2
.github/workflows/daily_ai_fix.yaml
vendored
@@ -125,7 +125,7 @@ jobs:
|
||||
|
||||
- name: Analyse and fix
|
||||
if: steps.batch.outputs.count != '0'
|
||||
uses: anthropics/claude-code-action@a874e9ecd7bb36efdad65429c6b35815f5a08f10 # v1
|
||||
uses: anthropics/claude-code-action@d75b94d5ad426cb8546e6628b6f5f19b84e5cce1 # v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
# Skip the OIDC -> Claude App token exchange. The scheduled path
|
||||
|
||||
2
.github/workflows/on_claude_mention.yml
vendored
2
.github/workflows/on_claude_mention.yml
vendored
@@ -64,7 +64,7 @@ jobs:
|
||||
fetch-depth: 1
|
||||
|
||||
- name: Run Claude Code
|
||||
uses: anthropics/claude-code-action@a874e9ecd7bb36efdad65429c6b35815f5a08f10 # v1
|
||||
uses: anthropics/claude-code-action@d75b94d5ad426cb8546e6628b6f5f19b84e5cce1 # v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
# AI_PR_TOKEN, not GITHUB_TOKEN, so a PR Claude opens triggers CI.
|
||||
|
||||
2
.github/workflows/on_issue_approved.yaml
vendored
2
.github/workflows/on_issue_approved.yaml
vendored
@@ -135,7 +135,7 @@ jobs:
|
||||
|
||||
- name: Execute the plan
|
||||
if: steps.bundle.outputs.has_plan == 'true'
|
||||
uses: anthropics/claude-code-action@a874e9ecd7bb36efdad65429c6b35815f5a08f10 # v1
|
||||
uses: anthropics/claude-code-action@d75b94d5ad426cb8546e6628b6f5f19b84e5cce1 # v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
# Skip the OIDC -> Claude App token exchange, which 401s whenever
|
||||
|
||||
2
.github/workflows/on_issues_ai_triage.yaml
vendored
2
.github/workflows/on_issues_ai_triage.yaml
vendored
@@ -166,7 +166,7 @@ jobs:
|
||||
id: classify
|
||||
if: github.event_name != 'issue_comment' || steps.claim.outputs.go == 'true'
|
||||
continue-on-error: true
|
||||
uses: anthropics/claude-code-action@a874e9ecd7bb36efdad65429c6b35815f5a08f10 # v1
|
||||
uses: anthropics/claude-code-action@d75b94d5ad426cb8546e6628b6f5f19b84e5cce1 # v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
# Without this the action falls back to the OIDC -> Claude App token
|
||||
|
||||
2
.github/workflows/on_pr_coderabbit.yml
vendored
2
.github/workflows/on_pr_coderabbit.yml
vendored
@@ -79,7 +79,7 @@ jobs:
|
||||
|
||||
- name: Address CodeRabbit comments
|
||||
if: steps.claim.outputs.go == 'true'
|
||||
uses: anthropics/claude-code-action@a874e9ecd7bb36efdad65429c6b35815f5a08f10 # v1
|
||||
uses: anthropics/claude-code-action@d75b94d5ad426cb8546e6628b6f5f19b84e5cce1 # v1
|
||||
with:
|
||||
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
|
||||
# Skip the OIDC -> Claude App token exchange, which 401s whenever
|
||||
|
||||
@@ -16,6 +16,54 @@ else
|
||||
echo "Starting custom scripts"
|
||||
fi
|
||||
|
||||
##########################################
|
||||
# Install the stop handler #
|
||||
##########################################
|
||||
|
||||
# As namespace PID 1 -- which is what "init: false" makes this script -- the kernel
|
||||
# discards any signal whose handler is still SIG_DFL. Installed at the end of startup,
|
||||
# as it used to be, the handler missed every stop that arrived while the Supervisor
|
||||
# probe or the cont-init chain was still running: Home Assistant waited out the grace
|
||||
# period for a SIGKILL and reported the add-on as failed. Nothing here is interrupted
|
||||
# mid-write by the move -- only PID 1 is signalled, and bash runs a trap at a command
|
||||
# boundary, so whatever external command is in flight reaps first.
|
||||
terminate() {
|
||||
local local_pid
|
||||
# Best-effort, so errexit must not apply: this runs with `set -e` in force (validate_shebang
|
||||
# leaves it on), and under errexit the first failing command aborts the handler and exits with
|
||||
# its status, skipping the child-kill loop and the `exit 0` below. Every command in the body
|
||||
# here is already guarded, but add-ons patch this function at build time -- postgres_15 and
|
||||
# postgres_17 sed an unguarded `pg_ctl ... stop` in right after the echo -- and that command
|
||||
# fails whenever the stop arrives before the database is up.
|
||||
set +e
|
||||
echo "Termination signal received, forwarding to subprocesses..."
|
||||
if command -v pgrep >/dev/null 2>&1; then
|
||||
while read -r pid; do
|
||||
[ -n "$pid" ] || continue
|
||||
echo "Terminating child PID $pid"
|
||||
kill -TERM "$pid" 2>/dev/null || echo "Failed to terminate PID $pid"
|
||||
done < <(pgrep -P "$$" || true)
|
||||
else
|
||||
for p in /proc/[0-9]*/; do
|
||||
local_pid="${p#/proc/}"
|
||||
local_pid="${local_pid%/}"
|
||||
if [ "$local_pid" -ne 1 ] && grep -q "^PPid:[[:space:]]*$$" "/proc/$local_pid/status" 2>/dev/null; then
|
||||
echo "Terminating child PID $local_pid"
|
||||
kill -TERM "$local_pid" 2>/dev/null || echo "Failed to terminate PID $local_pid"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
wait || true
|
||||
echo "All subprocesses terminated. Exiting."
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Only when this script is PID 1. Under Docker's own init the entrypoint is an ordinary
|
||||
# child and the signal handling above belongs to that init, not to us.
|
||||
if $PID1; then
|
||||
trap terminate SIGTERM SIGINT
|
||||
fi
|
||||
|
||||
##########################################
|
||||
# Pick an exec-capable directory #
|
||||
##########################################
|
||||
@@ -428,31 +476,6 @@ if $PID1; then
|
||||
echo " "
|
||||
echo -e "\033[0;32mEverything started!\033[0m"
|
||||
|
||||
terminate() {
|
||||
local local_pid
|
||||
echo "Termination signal received, forwarding to subprocesses..."
|
||||
if command -v pgrep >/dev/null 2>&1; then
|
||||
while read -r pid; do
|
||||
[ -n "$pid" ] || continue
|
||||
echo "Terminating child PID $pid"
|
||||
kill -TERM "$pid" 2>/dev/null || echo "Failed to terminate PID $pid"
|
||||
done < <(pgrep -P "$$" || true)
|
||||
else
|
||||
for p in /proc/[0-9]*/; do
|
||||
local_pid="${p#/proc/}"
|
||||
local_pid="${local_pid%/}"
|
||||
if [ "$local_pid" -ne 1 ] && grep -q "^PPid:[[:space:]]*$$" "/proc/$local_pid/status" 2>/dev/null; then
|
||||
echo "Terminating child PID $local_pid"
|
||||
kill -TERM "$local_pid" 2>/dev/null || echo "Failed to terminate PID $local_pid"
|
||||
fi
|
||||
done
|
||||
fi
|
||||
wait || true
|
||||
echo "All subprocesses terminated. Exiting."
|
||||
exit 0
|
||||
}
|
||||
|
||||
trap terminate SIGTERM SIGINT
|
||||
while :; do
|
||||
sleep infinity &
|
||||
wait $!
|
||||
|
||||
@@ -1,3 +1,25 @@
|
||||
## 20260909.7 (10-09-2026)
|
||||
- Correct the "first daily detection consensus" README section (review feedback on PR #3056): a merged-upstream setting stays in the build rather than disappearing, every attempt for a species is held back until one is accepted rather than only the first, and the "known to every active bird model" exemption is scoped per audio source, matching the implementation.
|
||||
## 20260909.6 (10-09-2026)
|
||||
- Document the fork-only "first daily detection consensus" setting (alexbelgium/birdnet-go#63): requires a second model to confirm each bird species' first detection of the day. Off by default; no behaviour change unless enabled.
|
||||
## 20260909.5 (09-09-2026)
|
||||
- Minor bugs fixed
|
||||
## 20260909.4 (09-09-2026)
|
||||
- Minor bugs fixed
|
||||
## 20260909.3 (09-09-2026)
|
||||
- Minor bugs fixed
|
||||
## 20260909.2 (09-09-2026)
|
||||
- Minor bugs fixed
|
||||
## 20260909 (09-09-2026)
|
||||
- Minor bugs fixed
|
||||
## 20260908.2 (08-09-2026)
|
||||
- Minor bugs fixed
|
||||
## 20260908.1 (08-09-2026)
|
||||
- Synced with upstream birdnet-go (4 commits); re-merges the open fork PRs, adding fork PR #62 (reanalyze a clip with every loaded model + one-click correction)
|
||||
## 20260908 (08-09-2026)
|
||||
- Synced with upstream birdnet-go (7 commits); re-merges the open fork PRs
|
||||
## 20260907 (07-09-2026)
|
||||
- Rebuild: re-merges the open fork PRs, picking up the updated fork PR #57
|
||||
## 20260901.4 (01-09-2026)
|
||||
- Minor bugs fixed
|
||||
## 20260901.3 (01-09-2026)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Home assistant add-on: Birdnet-Go (from source)
|
||||
|
||||
> **⚠️ Test build.** This is a special variant of the [standard birdnet-go add-on](https://github.com/alexbelgium/hassio-addons/tree/master/birdnet-go). Instead of pulling the prebuilt `ghcr.io/tphakala/birdnet-go` image, it **compiles BirdNET-Go from the [`alexbelgium/birdnet-go`](https://github.com/alexbelgium/birdnet-go) fork**. At build time it syncs the fork's `main` with the `tphakala/birdnet-go` upstream and **merges every open non-draft ("in review") pull request on the fly** (see [`merge-prs.sh`](./merge-prs.sh)), so the binary reflects upstream main plus all work currently under review. Everything below is identical to the standard add-on.
|
||||
> **⚠️ Test build.** This is a special variant of the [standard birdnet-go add-on](https://github.com/alexbelgium/hassio-addons/tree/master/birdnet-go). Instead of pulling the prebuilt `ghcr.io/tphakala/birdnet-go` image, it **compiles BirdNET-Go from the [`alexbelgium/birdnet-go`](https://github.com/alexbelgium/birdnet-go) fork**. At build time it syncs the fork's `main` with the `tphakala/birdnet-go` upstream and **merges every open non-draft ("in review") pull request on the fly** (see [`merge-prs.sh`](./merge-prs.sh)), so the binary reflects upstream main plus all work currently under review. Everything below is identical to the standard add-on, except the fork-only settings listed under [Fork-only settings](#fork-only-settings).
|
||||
|
||||
|
||||
|
||||
@@ -65,6 +65,38 @@ Additional variables can be configured using the config.yaml file found in /conf
|
||||
- Config_env.yaml
|
||||
Additional environment variables can be configured there
|
||||
|
||||
### Fork-only settings
|
||||
|
||||
These are currently fork-only settings from the [`alexbelgium/birdnet-go`](https://github.com/alexbelgium/birdnet-go) fork and are **not** in the standard add-on. [`merge-prs.sh`](./merge-prs.sh) syncs the fork's `main` with upstream before applying open PRs, so once a PR merges upstream the setting stays in this build — it just arrives via that sync instead of the PR-merge step, and stops being fork-only. Only a PR that is **closed without merging** drops its setting from later builds.
|
||||
|
||||
#### First daily detection consensus
|
||||
|
||||
Requires a second model to confirm the **first** detection of each bird species each day. Until one is accepted, every attempt for that species is held to the same two-model bar; only once a detection clears it does every later detection that day behave exactly as it does today, on a single model.
|
||||
|
||||
The first detection of a species in a day is the weakest evidence the pipeline produces, and it is the one that creates a "new species today" entry. Asking two models to agree on just that one detection removes most spurious new-species entries without slowing anything else down.
|
||||
|
||||
**Off by default.** Turn it on in the web UI under *Settings → Filters → First Daily Detection Consensus*, or in `config.yaml`:
|
||||
|
||||
```yaml
|
||||
realtime:
|
||||
firstdailyconsensus:
|
||||
enabled: true
|
||||
```
|
||||
|
||||
The setting is re-read on each detection cycle, so it takes effect without restarting the add-on.
|
||||
|
||||
It deliberately does **nothing** in these cases, all of which keep today's single-model behaviour:
|
||||
|
||||
- you run only one bird model (the default) — a second opinion does not exist, so the rule can never trigger
|
||||
- the species is not a bird — bats and the non-bird sound classes Perch reports (insects, amphibians, mammals, `power_tool`, and so on)
|
||||
- the species is not known to *every* active bird model analyzing that audio source — a species only one of them can name could never reach two confirmations. With several sources running different model combinations, this is decided per source, not add-on-wide
|
||||
- a dynamic threshold has actually lowered the bar for that species, meaning you asked for a more permissive gate
|
||||
- the taxonomy or the database cannot be consulted — it fails open and accepts the detection
|
||||
|
||||
In practice it only bites when a single audio source has two or more bird models (for example BirdNET plus Perch) analyzing it, on species all of them can identify. The trade is fewer false new-species entries, at the cost of occasionally delaying a genuine first sighting until a second model agrees.
|
||||
|
||||
Requires [alexbelgium/birdnet-go#63](https://github.com/alexbelgium/birdnet-go/pull/63).
|
||||
|
||||
### MQTT and MariaDB auto-configuration (opt-in)
|
||||
|
||||
If the Home Assistant **MQTT** addon is installed and running and you set `mqtt_auto_config: true` in the addon options, the addon writes the HA Mosquitto credentials directly into BirdNET-Go's `config.yaml` on every startup: `realtime.mqtt.enabled`, `broker`, `username`, and `password` are populated, and the topic defaults to `birdnet`. In addition, it enables BirdNET-Go's **native Home Assistant MQTT auto-discovery** (`realtime.mqtt.homeassistant.enabled`), so the detection sensors show up in Home Assistant automatically — **no manual MQTT sensor YAML required** (the hand-written sensors in [HAINTEGRATION.md](./HAINTEGRATION.md) remain available if you prefer to build your own). Messages are also retained (`realtime.mqtt.retain: true`) so sensor states survive Home Assistant restarts. When the option is `false` (the default), the addon still logs the broker details and reminds you about the option whenever Mosquitto is detected — nothing is written.
|
||||
|
||||
@@ -127,5 +127,5 @@ slug: birdnet-go-dev
|
||||
udev: true
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
usb: true
|
||||
version: "20260901.4"
|
||||
version: "20260909.7"
|
||||
video: true
|
||||
|
||||
@@ -11,6 +11,15 @@
|
||||
# stamping) is written to the directory given as $1 so the Docker build can
|
||||
# compile it.
|
||||
#
|
||||
# With --check the script does not build anything: it performs the exact same
|
||||
# merge sequence, skips (instead of failing on) every conflicting PR, and prints
|
||||
# one "!!! CONFLICT pr=#N conflicts-with=... files=... " line per offender before
|
||||
# exiting 2. Use it to find conflicts *before* a build burns on them. This is a
|
||||
# different question from GitHub's `mergeable` field, which compares a PR against
|
||||
# its own base ref - for a stacked PR that base is another feature branch (often
|
||||
# stale, sometimes belonging to a closed PR), so GitHub can report CLEAN for a PR
|
||||
# that does not merge onto main at all.
|
||||
#
|
||||
# Environment:
|
||||
# BIRDNET_FORK owner/repo of the fork (default alexbelgium/birdnet-go)
|
||||
# BIRDNET_UPSTREAM owner/repo of the upstream (default tphakala/birdnet-go)
|
||||
@@ -19,7 +28,26 @@
|
||||
#
|
||||
set -euo pipefail
|
||||
|
||||
TARGET_DIR="${1:?usage: merge-prs.sh <target-dir>}"
|
||||
CHECK_ONLY="${MERGE_PRS_CHECK:-0}"
|
||||
TARGET_DIR=""
|
||||
while [ "$#" -gt 0 ]; do
|
||||
case "$1" in
|
||||
--check) CHECK_ONLY=1 ;;
|
||||
-*) echo "unknown option: $1" >&2; exit 64 ;;
|
||||
*)
|
||||
# Last-one-wins would silently clone into the wrong directory if a caller ever
|
||||
# appended an argument; the pre-flag script used "${1}", so refuse rather than
|
||||
# quietly change which operand counts.
|
||||
if [ -n "${TARGET_DIR}" ]; then
|
||||
echo "usage: merge-prs.sh [--check] <target-dir>" >&2
|
||||
exit 64
|
||||
fi
|
||||
TARGET_DIR="$1"
|
||||
;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
: "${TARGET_DIR:?usage: merge-prs.sh [--check] <target-dir>}"
|
||||
|
||||
FORK="${BIRDNET_FORK:-alexbelgium/birdnet-go}"
|
||||
UPSTREAM="${BIRDNET_UPSTREAM:-tphakala/birdnet-go}"
|
||||
@@ -33,6 +61,52 @@ GH_TOKEN="${GH_TOKEN:-${GITHUB_TOKEN:-}}"
|
||||
|
||||
log() { echo ">>> $*"; }
|
||||
|
||||
# Conflicting PRs collected in --check mode: "number|scope|files|title".
|
||||
conflicting=()
|
||||
|
||||
LOCKFILE="frontend/package-lock.json"
|
||||
|
||||
# The single place that decides whether a conflicted merge is still acceptable.
|
||||
# package-lock.json is generated content and stacked PRs can carry an older copy even when
|
||||
# their source changes merge cleanly, so keep the lockfile already assembled on the base side
|
||||
# — but only when it is the sole conflict. Any source conflict stays fatal.
|
||||
# Returns 0 when it resolved and committed such a merge, 1 when the conflict is real.
|
||||
# BOTH the real merge and the --check probe must go through here: when only the real merge
|
||||
# applied the policy, the probe called a PR "conflicts-with=main" that the build would have
|
||||
# merged fine, and printed the opposite remediation to the true one.
|
||||
resolve_sole_lockfile() {
|
||||
local dir="$1"
|
||||
local -a conflicted
|
||||
mapfile -t conflicted < <(git -C "${dir}" diff --name-only --diff-filter=U)
|
||||
if [ "${#conflicted[@]}" -ne 1 ] || [ "${conflicted[0]}" != "${LOCKFILE}" ]; then
|
||||
return 1
|
||||
fi
|
||||
log "Resolving generated ${LOCKFILE} conflict using the base tree"
|
||||
git -C "${dir}" checkout --ours -- "${LOCKFILE}" || return 1
|
||||
git -C "${dir}" add "${LOCKFILE}" || return 1
|
||||
git -C "${dir}" commit --no-edit > /dev/null || return 1
|
||||
}
|
||||
|
||||
# Does ${1} merge cleanly onto the pristine upstream-synced main? Probed in a
|
||||
# throwaway worktree so the accumulated tree is left untouched. Tells apart a PR
|
||||
# that is simply stale against main (fixable inside that PR's own branch) from
|
||||
# one that only clashes with another open PR (needs a cross-PR decision).
|
||||
merges_onto_main() {
|
||||
local sha="$1" tmpdir probe rc=0
|
||||
tmpdir="$(mktemp -d)"
|
||||
probe="${tmpdir}/probe"
|
||||
git worktree add --quiet --detach "${probe}" "${MAIN_SYNCED}"
|
||||
if ! git -C "${probe}" merge --no-edit --no-ff -m probe "${sha}" > /dev/null 2>&1; then
|
||||
# Same policy as the real merge, or this misclassifies a lockfile-only clash.
|
||||
resolve_sole_lockfile "${probe}" > /dev/null 2>&1 || rc=1
|
||||
fi
|
||||
git worktree remove --force "${probe}" > /dev/null 2>&1 || true
|
||||
# worktree remove only takes the child back; without this the mktemp parent is left behind
|
||||
# on every checked conflict.
|
||||
rmdir "${tmpdir}" > /dev/null 2>&1 || true
|
||||
return "${rc}"
|
||||
}
|
||||
|
||||
git config --global user.email "addon-builder@users.noreply.github.com"
|
||||
git config --global user.name "BirdNET-Go Addon Builder"
|
||||
git config --global advice.detachedHead false
|
||||
@@ -47,6 +121,7 @@ git remote add upstream "${UPSTREAM_URL}"
|
||||
git fetch --no-tags upstream main
|
||||
# --no-ff keeps an explicit sync commit; a no-op when main is already current.
|
||||
git merge --no-edit --no-ff upstream/main
|
||||
MAIN_SYNCED="$(git rev-parse HEAD)"
|
||||
|
||||
log "Querying open non-draft PRs from ${FORK}"
|
||||
auth_header=()
|
||||
@@ -79,27 +154,43 @@ for entry in "${prs[@]}"; do
|
||||
if ! git merge --no-edit --no-ff -m "Merge PR #${number}: ${title}" "${sha}"; then
|
||||
mapfile -t conflicted_files < <(git diff --name-only --diff-filter=U)
|
||||
|
||||
# package-lock.json is generated content and stacked PRs can carry an
|
||||
# older copy even when their source changes merge cleanly. Keep the
|
||||
# lockfile already assembled from upstream and earlier PRs, but only
|
||||
# when it is the sole conflict. Any source conflict remains fatal.
|
||||
if [ "${#conflicted_files[@]}" -eq 1 ] \
|
||||
&& [ "${conflicted_files[0]}" = "frontend/package-lock.json" ]; then
|
||||
log "Resolving generated frontend/package-lock.json conflict using the accumulated tree"
|
||||
git checkout --ours -- frontend/package-lock.json
|
||||
git add frontend/package-lock.json
|
||||
git commit --no-edit
|
||||
if resolve_sole_lockfile .; then
|
||||
: # generated lockfile only - the merge is committed and the build continues
|
||||
else
|
||||
echo "!!! Merge conflict while merging PR #${number} (${title})." >&2
|
||||
if [ "${#conflicted_files[@]}" -gt 0 ]; then
|
||||
printf '!!! Conflicting file: %s\n' "${conflicted_files[@]}" >&2
|
||||
fi
|
||||
echo "!!! Resolve the conflict in the fork or pause this PR, then rebuild." >&2
|
||||
git merge --abort || true
|
||||
|
||||
if [ "${CHECK_ONLY}" = "1" ]; then
|
||||
scope="accumulated"
|
||||
merges_onto_main "${sha}" || scope="main"
|
||||
conflicting+=("${number}|${scope}|${conflicted_files[*]:-}|${title}")
|
||||
log "check mode: skipping PR #${number}, continuing with the rest"
|
||||
continue
|
||||
fi
|
||||
|
||||
echo "!!! Resolve the conflict in the fork or pause this PR, then rebuild." >&2
|
||||
exit 1
|
||||
fi
|
||||
fi
|
||||
done
|
||||
|
||||
if [ "${CHECK_ONLY}" = "1" ]; then
|
||||
if [ "${#conflicting[@]}" -eq 0 ]; then
|
||||
log "CHECK OK: every open non-draft PR merges into the combined build tree"
|
||||
exit 0
|
||||
fi
|
||||
echo "!!! CHECK FAILED: ${#conflicting[@]} PR(s) would break the add-on build" >&2
|
||||
for entry in "${conflicting[@]}"; do
|
||||
IFS='|' read -r number scope files title <<<"${entry}"
|
||||
echo "!!! CONFLICT pr=#${number} conflicts-with=${scope} files=${files} title=${title}" >&2
|
||||
done
|
||||
echo "!!! conflicts-with=main -> the PR is stale against main; merge main into its branch and resolve there." >&2
|
||||
echo "!!! conflicts-with=accumulated -> the PR only clashes with another open PR; decide which one owns the hunk." >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
log "Merged HEAD: $(git rev-parse --short HEAD)"
|
||||
log "Source tree ready at ${TARGET_DIR}"
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
## 0.6.1.2 (2026-09-07)
|
||||
- Rebuild to pick up a shared `ha_entrypoint.sh` fix: the SIGTERM/SIGINT handler is now installed at the top of the entrypoint instead of at the end of startup. With `init: false` the entrypoint is namespace PID 1, and the kernel discards a signal that PID 1 has no handler for, so a stop arriving while the Supervisor probe or the `cont-init.d` chain was still running was lost entirely and the add-on died only when the grace period expired into SIGKILL. Stops are now honoured from the first moment of startup. No change to add-on behaviour otherwise.
|
||||
|
||||
## 0.6.1.1 (2026-09-07)
|
||||
- Fix the add-on refusing to stop: Home Assistant reported an Error status after a few seconds and the container kept running and serving the web UI. `cont-init.d/99-run.sh` started nginx in the foreground, and `ha_entrypoint.sh` runs every cont-init script in the foreground, so the entrypoint never reached the point where it installs the `terminate()` handler that forwards SIGTERM to the application on shutdown. The application is now started in the background, and `init: false` makes the entrypoint run as PID 1 so the orphaned process is reparented to it and receives that signal. Closes #3049.
|
||||
|
||||
|
||||
@@ -21,5 +21,5 @@ schema:
|
||||
value: str?
|
||||
slug: omni-tools
|
||||
url: https://github.com/alexbelgium/hassio-addons
|
||||
version: 0.6.1.1
|
||||
version: 0.6.1.2
|
||||
webui: "[PROTO:ssl]://[HOST]:[PORT:80]"
|
||||
|
||||
Reference in New Issue
Block a user