Compare commits

..

1 Commits

Author SHA1 Message Date
alexbelgium
d9b92f4e70 docs(skill): simplify again after the code review, and demand a trigger for defensive branches
The full loop ran simplify (step 5) before Codex's code review (step 6) and never
again, so nothing walked back what the review added. Adversarial review is asked to
find what could go wrong, so its output is a list of arguments for more code and it
is never asked whether the branch it wants is reachable — accepting objections only
ratchets the diff upward. Step 6 now ends by re-running step 5's checks over the
hunks the review touched.

The other half was earlier than the review. The standing rule already said complexity
is bought only by a measurement, but it said it about performance, so a branch added
for robustness did not visibly fall under it. It now covers hypothetical hosts as
well as hypothetical performance: name the input that reaches a defensive branch and
the image it happens on, or delete it and let the case fail visibly. Step 3's
attack-your-own-plan list asks the same question before any code exists, which is
where it is cheapest to answer.

The case study in references/simplify.md is #3013: 25 lines of code at review, 10
merged. A pure-bash fallback written at implement time for images shipping
with-contenv but not s6-dumpenv — reasoned from the two binaries living in different
s6 packages, never demonstrated on a real image, and defending a case that would have
degraded to the pre-fix behaviour anyway — plus the helper function and second reset
that existed only to serve it. Deleting the fallback deleted all of it. The review's
own objections were correct and cost two tokens on an existing line.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-08-24 14:11:35 +02:00
35 changed files with 101 additions and 380 deletions

View File

@@ -116,10 +116,9 @@ Attack your own plan before implementing:
- What is the **blast radius** if the assumption underneath it is wrong? - What is the **blast radius** if the assumption underneath it is wrong?
- What am I **inferring** that I could instead **detect at runtime** or **record explicitly**? - 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. 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 - For every branch that exists **only to survive something going wrong**: what input reaches it,
where that input actually arrives, and go and look. Naming is the bar, not reproducing it here — on which image? Go and look. A fallback for a configuration you cannot find an instance of is
`references/simplify.md` works the `/dev/shm` guard and the `s6-dumpenv` fallback through that not robustness, it is a second code path nobody will ever exercise or notice rotting.
distinction.
Full loop only, before writing code: get Codex's independent read on the plan. Spawn a subagent 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 whose prompt includes the path `references/codex-review.md` and tells it to follow that file's
@@ -159,19 +158,13 @@ defends one you have now demonstrated, or one you have merely been told about? T
correctness objection often deletes the code that made it necessary, and a fix that collapses back 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. to fewer lines than you started the review with is the normal outcome, not a suspicious one.
These edits land after step 4's checks already ran, so re-run them: `scripts/validate.sh <addon>
--vs-master` plus the behavioural tests, over the final diff. Deleting a branch is exactly the
kind of edit that leaves a stray `fi` behind.
## 7. Open the PR ## 7. Open the PR
CI gates on a PR: **`CHANGELOG.md` updated** (hard fail), the **HA add-on linter** 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 (`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 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 `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>)`, `README.md` if you added options; match the CHANGELOG heading format `## X.Y (DD-MM-YYYY)`.
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, 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. **what is not verified**, and how to roll back the riskiest hunk alone.
@@ -192,12 +185,7 @@ 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 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 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` motivated the work once the rebuilt add-on is running. Real "merged and inert" examples, and what
(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`. to do when a fix can't be self-verified: `references/evidence.md`.
## 10. Calibrate and report ## 10. Calibrate and report
@@ -216,9 +204,5 @@ Risk + rollback — the riskiest hunk and how to revert it alone
Lead with anything that did not work — a merged PR that achieved nothing is the single most Lead with anything that did not work — a merged PR that achieved nothing is the single most
important sentence in the report. Give confidence per claim, not one blanket number. important sentence in the report. Give confidence per claim, not one blanket number.
**Feed the skill.** When a shipped fix needed a follow-up PR, or a reviewer caught something this
skill should have, add the distilled lesson to the matching `references/` file in that follow-up
PR — one entry, with the PR numbers. That loop is what keeps this file short and the traps real.
Scripts are meant to be **run, not read** — each is cited at its point of use above; read one Scripts are meant to be **run, not read** — each is cited at its point of use above; read one
only if its output surprises you. only if its output surprises you.

View File

@@ -1,7 +1,7 @@
# Simplify — case studies # Simplify — case studies
Evidence for why the mechanism ladder in SKILL.md step 3 exists, and why levels 4-6 need a reason Evidence for why the mechanism ladder in SKILL.md step 3 exists, and why levels 4-6 need a reason
that survives being said out loud. In each case the simpler option existed and was skipped. that survives being said out loud. In all three cases the simpler option existed and was skipped.
Being able to build the complicated thing is not a reason to. Being able to build the complicated thing is not a reason to.
- A rejected PR spent a **388-line TCP proxy plus a 142-line monkeypatch of a private upstream - A rejected PR spent a **388-line TCP proxy plus a 142-line monkeypatch of a private upstream
@@ -13,13 +13,6 @@ Being able to build the complicated thing is not a reason to.
- A resolution cap shipped as a **new init script writing an s6 envdir** — the wrong mechanism - A resolution cap shipped as a **new init script writing an s6 envdir** — the wrong mechanism
entirely (ladder level 4). Renaming the option to the env var the service already reads entirely (ladder level 4). Renaming the option to the env var the service already reads
(level 1) would have worked, and the new script did not. (level 1) would have worked, and the new script did not.
- A calibre-web trusted-ips fix injected the add-on's **per-boot IP**, which forced a
rewrite-every-boot design that erased user entries; preserving them then needed merge logic
plus a state file recording what was injected (+34 lines, PR #3009 — closed unmerged). Asking
"is there a constant that makes the rewrite unnecessary?" gave the shipped fix: trust the
static supervisor range `172.30.32.0/23`, one idempotent statement, **net −6 lines**
(PR #3010). Complexity spent working around a changing value is a sign to hunt for the
constant instead.
- A `.templates/ha_entrypoint.sh` fix went to review at 25 lines of code and merged at 10. Two - A `.templates/ha_entrypoint.sh` fix went to review at 25 lines of code and merged at 10. Two
sources of the excess, and neither was caught by the loop: a **pure-bash fallback** written at sources of the excess, and neither was caught by the loop: a **pure-bash fallback** written at
implement time for images shipping `with-contenv` but not `s6-dumpenv` — reasoned from the two implement time for images shipping `with-contenv` but not `s6-dumpenv` — reasoned from the two
@@ -42,16 +35,10 @@ Being able to build the complicated thing is not a reason to.
- **Is the fix bigger than the thing it fixes?** That is a smell, not a rule — but it usually - **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. 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, - **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 the way you would check a measurement. If you cannot produce the case, delete the branch — the
reproduce it here: Docker's 64 MB `/dev/shm` default is documented behaviour that HA does not situation then fails the way it already fails today, visibly, instead of through a second path
override, so the bullet above keeps that guard even though this host measured 7.7 GB. Nobody that is never exercised and silently rots as the base images move. Write down in the PR body
could name a single image shipping `with-contenv` without `s6-dumpenv`, so that fallback went. what you cut and why, so the next person does not re-add it from the same reasoning.
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? - **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 Code that reads a documented knob keeps working. Code that reaches into private internals
does not. does not.

View File

@@ -11,7 +11,6 @@ workflows and lint rules — that is not repeated here.
- [Environment and workspace](#environment-and-workspace) - [Environment and workspace](#environment-and-workspace)
- [Measurement](#measurement) - [Measurement](#measurement)
- [Passing values into base-image services](#passing-values-into-base-image-services) - [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) - [Shell and bashio](#shell-and-bashio)
- [Dockerfile and architecture](#dockerfile-and-architecture) - [Dockerfile and architecture](#dockerfile-and-architecture)
- [Versioning](#versioning) - [Versioning](#versioning)
@@ -117,26 +116,6 @@ place for filesystem and permission setup.
the openbox autostart. (ANGLE's OpenGL backend, for instance, fails with "Could not open the the openbox autostart. (ANGLE's OpenGL backend, for instance, fails with "Could not open the
default X display".) default X display".)
## Writing into an app's own config
**A field your cont-init script writes may also be user-editable in the app's UI.** An
unconditional `UPDATE`/overwrite on every boot silently erases whatever the user added there,
and containers are recreated on restart so it re-erases forever (calibre-web
`config_reverse_proxy_trusted_ips`, #3004 — flagged by two review bots, fixed in #3010).
Prepend/merge with an idempotence guard instead of assigning.
**Prefer values that are constant across boots.** The add-on's own IP changes every restart,
so injecting it forces a rewrite-every-boot design plus stale-entry cleanup (a stale trusted IP
can be handed to a *different* add-on later). Trusting the whole supervisor range
`172.30.32.0/23` is constant, written once. For dual-stack listeners the IPv4 form never
matches IPv4-mapped addresses — also list the mapped form (`::ffff:172.30.32.0/119`).
Constant is not free when the value gates **authentication**: trusting the whole range means any
add-on on the supervisor network can send the auth header and impersonate a user. #3010 shipped
that as an explicit, stated trade-off with the maintainer's sign-off. State the blast radius in
the PR body and get the maintainer's call before widening trust — never present it as a neutral
simplification.
## Shell and bashio ## Shell and bashio
**`bashio::config` for lists**: `while read ... < <(bashio::config ...)` silently yields an empty **`bashio::config` for lists**: `while read ... < <(bashio::config ...)` silently yields an empty
@@ -216,17 +195,6 @@ All three hard gates below are matrixed over `check-addon-changes.outputs.change
- **Version bump** — no workflow checks it. It is repo convention, and required for Supervisor to - **Version bump** — no workflow checks it. It is repo convention, and required for Supervisor to
offer the rebuild, but it will not fail CI. offer the rebuild, but it will not fail CI.
**"Merged" is not "on master".** The push builder's revert-on-failure job reverts the merge
commit when its prebuild step fails — including failures unrelated to your diff. A seerr fix
merged at 05:15 and was reverted one minute later because `EndBug/add-and-commit`'s floating
`v11` tag had moved to a broken release (#2993, reapplied verbatim in #2997). After merge,
`git fetch origin master` first — the remote-tracking ref is stale otherwise and would "confirm"
against pre-merge state — then check that `git diff origin/master -- <the paths you touched>` is
empty before declaring done. Scope it to your paths: master moves under you, so whole-tree
equality fails on unrelated commits. Ancestry is not the check either — this repo squash-merges,
so a merged PR head is never an ancestor of `master` (verified on #3010, whose fix is live), and
a revert leaves the original commit an ancestor anyway.
**CI rewrites your shell scripts.** `lint.yml` runs `shfmt -w -i 4 -ci -bn -sr` over every `*.sh` **CI rewrites your shell scripts.** `lint.yml` runs `shfmt -w -i 4 -ci -bn -sr` over every `*.sh`
and `run`, plus a `chmod +x` pass, on schedule. Repo-wide reformatting commits land on master and `run`, plus a `chmod +x` pass, on schedule. Repo-wide reformatting commits land on master
without your involvement — another reason a shared checkout goes stale mid-task. without your involvement — another reason a shared checkout goes stale mid-task.
@@ -241,18 +209,6 @@ account. Note it and move on rather than guessing.
**Resolving a review thread requires GraphQL** (`resolveReviewThread`); the REST API cannot do it. **Resolving a review thread requires GraphQL** (`resolveReviewThread`); the REST API cannot do it.
`scripts/pr_review.sh` wraps fetch / reply / resolve. `scripts/pr_review.sh` wraps fetch / reply / resolve.
**Most CHANGELOG heading dates are ISO, whatever the bots' defaults say.** Match the format
already in the add-on's file — a `DD-MM-YYYY` file stays `DD-MM-YYYY`. Where you have no
precedent, ISO is the house style: as of 2026-08-25, `## <version> (YYYY-MM-DD)` accounts for
7705 dated headings against 363 in `DD-MM-YYYY`, and the newest entry is ISO in 125 of 135
add-ons. Copilot flags an ISO file that gets a `DD-MM-YYYY` entry (#3019). `DD-MM-YYYY` is not
invented — it is what `onpush_builder.yaml` writes with `date '+%d-%m-%Y'` when it has to insert
a heading you forgot, and what the addons_updater bot writes when its `date_iso8601` option is
off (`99-run.sh`; it is on in production here) — but neither is a reason to write it yourself.
The builder's duplicate check is `grep -q "^## ${version} ("` — an unescaped BRE, so the dots in
a version match any character, and it does not look at the date at all. Either way an ISO heading
you wrote yourself still suppresses the bot's insertion.
**The repo's `.markdownlint.yaml` does not disable MD022/MD032**, so a CHANGELOG will show **The repo's `.markdownlint.yaml` does not disable MD022/MD032**, so a CHANGELOG will show
dozens of pre-existing heading/list findings. They are noise because lint is `continue-on-error`, dozens of pre-existing heading/list findings. They are noise because lint is `continue-on-error`,
not because the config exempts them — don't cite the config as a reason to ignore a finding. not because the config exempts them — don't cite the config as a reason to ignore a finding.

View File

@@ -90,7 +90,6 @@ resolve)
;; ;;
watch) watch)
MINS="${3:-180}" # the addon build alone has taken ~3h; 20 was far too short MINS="${3:-180}" # the addon build alone has taken ~3h; 20 was far too short
wfail=2 # not 0: running out of minutes with checks still pending is not a pass
for i in $(seq 1 "$MINS"); do for i in $(seq 1 "$MINS"); do
c=$(gh pr checks "$PR" 2> /dev/null | awk '{print $1"="$2}' | tr '\n' ' ') c=$(gh pr checks "$PR" 2> /dev/null | awk '{print $1"="$2}' | tr '\n' ' ')
if [ -z "$c" ]; then if [ -z "$c" ]; then
@@ -103,14 +102,11 @@ watch)
case "$c" in case "$c" in
*pending*) sleep 60 ;; *pending*) sleep 60 ;;
*fail* | *error* | *cancel*) echo "settled — with FAILURES (see above)"; wfail=1; break ;; *fail* | *error* | *cancel*) echo "settled — with FAILURES (see above)"; wfail=1; break ;;
*) echo "settled — all passing"; wfail=0; break ;; *) echo "settled — all passing"; break ;;
esac esac
done done
[ "$wfail" -eq 2 ] && echo "gave up after ${MINS}m, checks still unsettled — NOT a pass"
# A PR touching no */config.* skips the CHANGELOG, linter and build jobs outright (#3018).
case "${c:-}" in *skipping*) echo " ...of which some were SKIPPED — a skipped job tested nothing" ;; esac
echo "note: long queues here are usually account runner contention, not your diff." echo "note: long queues here are usually account runner contention, not your diff."
exit "$wfail" exit "${wfail:-0}"
;; ;;
*) echo "unknown: $CMD"; exit 1 ;; *) echo "unknown: $CMD"; exit 1 ;;
esac esac

View File

@@ -64,8 +64,7 @@ if [ -n "${BUILD_VERSION:-}" ]; then
echo " $SLUG/config.yaml version = $here" echo " $SLUG/config.yaml version = $here"
echo " running image BUILD_VERSION = $BUILD_VERSION" echo " running image BUILD_VERSION = $BUILD_VERSION"
if [ "$here" = "$BUILD_VERSION" ]; then if [ "$here" = "$BUILD_VERSION" ]; then
# version is bumped once per PR, so a later commit or a dirty tree matches here. echo " MATCH — this checkout corresponds to the running image."
echo " VERSION MATCH — source revision itself is not verified."
else else
echo " MISMATCH — this branch is NOT what is running." echo " MISMATCH — this branch is NOT what is running."
git fetch origin master --quiet 2> /dev/null git fetch origin master --quiet 2> /dev/null

View File

@@ -4,13 +4,10 @@
# rather than implying the build was checked. # rather than implying the build was checked.
# #
# --vs-master re-lints each changed file at origin/master and prints only findings your diff # --vs-master re-lints each changed file at origin/master and prints only findings your diff
# ADDED, and fails if there are any. Without it you will chase warnings that were already there. # ADDED. Without it you will chase warnings that were already in the file.
# #
# Usage: validate.sh [addon-dir] [--vs-master] # Usage: validate.sh [addon-dir] [--vs-master]
set -uo pipefail set -uo pipefail
# git diff prints repo-root-relative paths and $ADDON is a top-level directory name: neither
# resolves from a subdirectory, where the -f guard below would skip every file and report clean.
if root=$(git rev-parse --show-toplevel 2> /dev/null); then cd "$root" || exit 1; fi
ADDON="${1:-}" ADDON="${1:-}"
[ "${ADDON:-}" = "--vs-master" ] && { ADDON=""; set -- --vs-master; } [ "${ADDON:-}" = "--vs-master" ] && { ADDON=""; set -- --vs-master; }
@@ -34,21 +31,17 @@ echo "== validating $ADDON =="
fail=0 fail=0
note() { printf ' %-13s %s\n' "$1" "$2"; } note() { printf ' %-13s %s\n' "$1" "$2"; }
# execline `run`/`finish` files are not shell (25 of them here, across 21 add-ons). Neither
# linter below can read one, so anything either says about it is noise.
is_execline() { local l; IFS= read -r l < "$1" 2> /dev/null; [[ $l == '#!'*execlineb* ]]; }
# Shell: bash -n then shellcheck -x (follows sourced files, as CI does). One list for both. # Shell: bash -n then shellcheck -x (follows sourced files, as CI does).
files=() while IFS= read -r f; do
while IFS= read -r f; do is_execline "$f" || files+=("$f"); done \ [ -f "$f" ] || continue
< <(find "$ADDON" -type f \( -name '*.sh' -o -name 'run' -o -name 'finish' -o -name 'autostart' \) 2> /dev/null)
for f in "${files[@]}"; do
if ! out=$(bash -n "$f" 2>&1); then note "bash -n" "FAIL $f"; echo "$out" | sed 's/^/ /'; fail=1; fi if ! out=$(bash -n "$f" 2>&1); then note "bash -n" "FAIL $f"; echo "$out" | sed 's/^/ /'; fail=1; fi
done done < <(find "$ADDON" -type f \( -name '*.sh' -o -name 'run' -o -name 'finish' -o -name 'autostart' \) 2> /dev/null)
[ "$fail" -eq 0 ] && note "bash -n" "${#files[@]} file(s) checked" [ "$fail" -eq 0 ] && note "bash -n" "ok"
if [ "${#files[@]}" -gt 0 ] && command -v shellcheck > /dev/null 2>&1; then if command -v shellcheck > /dev/null 2>&1; then
sc=$(shellcheck -x -f gcc "${files[@]}" 2>&1) sc=$(find "$ADDON" -type f \( -name '*.sh' -o -name 'autostart' -o -name 'run' -o -name 'finish' \) -print0 2> /dev/null |
xargs -0 -r shellcheck -x -f gcc 2>&1)
if [ -n "$sc" ]; then if [ -n "$sc" ]; then
note "shellcheck" "$(printf '%s\n' "$sc" | grep -c .) finding(s)" note "shellcheck" "$(printf '%s\n' "$sc" | grep -c .) finding(s)"
printf '%s\n' "$sc" | sed 's/^/ /' | head -20 printf '%s\n' "$sc" | sed 's/^/ /' | head -20
@@ -57,10 +50,7 @@ fi
command -v hadolint > /dev/null 2>&1 && [ -f "$ADDON/Dockerfile" ] && { command -v hadolint > /dev/null 2>&1 && [ -f "$ADDON/Dockerfile" ] && {
hl=$(hadolint "$ADDON/Dockerfile" 2>&1) hl=$(hadolint "$ADDON/Dockerfile" 2>&1)
if [ -n "$hl" ]; then [ -n "$hl" ] && { note "hadolint" "$(printf '%s\n' "$hl" | grep -c .) finding(s)"; printf '%s\n' "$hl" | sed 's/^/ /' | head -10; } || note "hadolint" "clean"
note "hadolint" "$(printf '%s\n' "$hl" | grep -c .) finding(s)"
printf '%s\n' "$hl" | sed 's/^/ /' | head -10
else note "hadolint" "clean"; fi
} }
if [ -f "$ADDON/config.yaml" ]; then if [ -f "$ADDON/config.yaml" ]; then
@@ -89,14 +79,11 @@ $VS_MASTER && command -v npx > /dev/null 2>&1 && [ -f "$ADDON/CHANGELOG.md" ] &&
echo echo
echo "== CI requirements ==" echo "== CI requirements =="
# -Fxq, not -q: unanchored, seerr's is matched by zzz_archived_overseerr's, and . is a wildcard. if git diff --name-only origin/master...HEAD 2> /dev/null | grep -q "$ADDON/CHANGELOG.md"; then
# Stricter than the gate itself, whose quoted =~ accepts that same collision
# (onpr_check-pr.yaml:75), so this can fail where CI passes — the wrong add-on's is still wrong.
if git diff --name-only origin/master...HEAD 2> /dev/null | grep -Fxq "$ADDON/CHANGELOG.md"; then
note "CHANGELOG" "updated" note "CHANGELOG" "updated"
else else
# This one IS gated: onpr_check-pr.yaml exits 1 without it. # This one IS gated: onpr_check-pr.yaml exits 1 without it.
note "CHANGELOG" "NOT UPDATED for $ADDON — CI hard-gates this"; fail=1 note "CHANGELOG" "NOT UPDATED — this is the one CI hard-gate"; fail=1
fi fi
if git diff origin/master...HEAD -- "$ADDON/config.yaml" 2> /dev/null | grep -q '^+version:'; then if git diff origin/master...HEAD -- "$ADDON/config.yaml" 2> /dev/null | grep -q '^+version:'; then
note "version" "bumped" note "version" "bumped"
@@ -111,15 +98,8 @@ if $VS_MASTER; then
echo echo
echo "== findings ADDED by this diff (pre-existing ones filtered out) ==" echo "== findings ADDED by this diff (pre-existing ones filtered out) =="
tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
added=0 git diff --name-only origin/master...HEAD -- "$ADDON" 2> /dev/null | while IFS= read -r f; do
# Fed by process substitution, not a pipe: a pipeline runs this in a subshell, where the git show "origin/master:$f" > "$tmp/base" 2> /dev/null || continue
# findings below could never reach $fail and the verdict would contradict the list.
while IFS= read -r f; do
# Deleted: linting the path that is gone invents a finding. Added: no base, and an
# empty one says the right thing — every finding in it is one this diff added.
[ -f "$f" ] || continue
is_execline "$f" && continue
git show "origin/master:$f" > "$tmp/base" 2> /dev/null || : > "$tmp/base"
# A missing linter must be a visible skip, not a silent "no new findings": # A missing linter must be a visible skip, not a silent "no new findings":
# its "command not found" error is identical for base and head, so comm would # its "command not found" error is identical for base and head, so comm would
# cancel it out and report a false clean. # cancel it out and report a false clean.
@@ -135,16 +115,12 @@ if $VS_MASTER; then
cmd() { hadolint "$1" 2>&1 | sed 's/^[^:]*//; s/^:[0-9]*//'; } ;; cmd() { hadolint "$1" 2>&1 | sed 's/^[^:]*//; s/^:[0-9]*//'; } ;;
*) continue ;; *) continue ;;
esac esac
b=$(cmd "$tmp/base" | sort) cp "$tmp/base" "$tmp/base_f"; b=$(cmd "$tmp/base_f" | sort)
a=$(cmd "$f" | sort) a=$(cmd "$f" | sort)
new=$(comm -13 <(printf '%s\n' "$b") <(printf '%s\n' "$a")) new=$(comm -13 <(printf '%s\n' "$b") <(printf '%s\n' "$a") | grep -c .)
[ -n "$new" ] && { [ "$new" -gt 0 ] && { echo " $f: $new NEW finding(s)"; comm -13 <(printf '%s\n' "$b") <(printf '%s\n' "$a") | sed 's/^/ /' | head -5; }
echo " $f: $(printf '%s\n' "$new" | grep -c .) NEW finding(s)" done
printf '%s\n' "$new" | sed 's/^/ /' | head -5 echo " (nothing listed above = your diff introduced no new lint findings)"
added=1; fail=1
}
done < <(git diff --name-only origin/master...HEAD -- "$ADDON" 2> /dev/null)
[ "$added" -eq 0 ] && echo " (none — this diff introduced no new lint findings)"
fi fi
echo echo

View File

@@ -228,7 +228,7 @@ jobs:
echo "... done" echo "... done"
- name: Commit if needed - name: Commit if needed
uses: EndBug/add-and-commit@v11.1.1 uses: EndBug/add-and-commit@v11.0.0
with: with:
message: "GitHub bot : README updated" message: "GitHub bot : README updated"
default_author: github_actions default_author: github_actions

View File

@@ -125,7 +125,7 @@ jobs:
- name: Analyse and fix - name: Analyse and fix
if: steps.batch.outputs.count != '0' if: steps.batch.outputs.count != '0'
uses: anthropics/claude-code-action@dcb57747bfceeaa1fa72638cae52295d1d853d4a # v1 uses: anthropics/claude-code-action@9d7150bc8a3dae8149739a88019d192b579ad90c # v1
with: with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Skip the OIDC -> Claude App token exchange. The scheduled path # Skip the OIDC -> Claude App token exchange. The scheduled path

View File

@@ -237,7 +237,7 @@ jobs:
# Get stars evolution # Get stars evolution
wget -S -O .github/starsevol.svg "https://api.star-history.com/svg?repos=alexbelgium/hassio-addons&type=Date" || true wget -S -O .github/starsevol.svg "https://api.star-history.com/svg?repos=alexbelgium/hassio-addons&type=Date" || true
- name: Commit if needed - name: Commit if needed
uses: EndBug/add-and-commit@v11.1.1 uses: EndBug/add-and-commit@v11.0.0
with: with:
message: "GitHub bot : graphs updated" message: "GitHub bot : graphs updated"
default_author: github_actions default_author: github_actions

View File

@@ -64,7 +64,7 @@ jobs:
fetch-depth: 1 fetch-depth: 1
- name: Run Claude Code - name: Run Claude Code
uses: anthropics/claude-code-action@dcb57747bfceeaa1fa72638cae52295d1d853d4a # v1 uses: anthropics/claude-code-action@9d7150bc8a3dae8149739a88019d192b579ad90c # v1
with: with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# AI_PR_TOKEN, not GITHUB_TOKEN, so a PR Claude opens triggers CI. # AI_PR_TOKEN, not GITHUB_TOKEN, so a PR Claude opens triggers CI.

View File

@@ -135,7 +135,7 @@ jobs:
- name: Execute the plan - name: Execute the plan
if: steps.bundle.outputs.has_plan == 'true' if: steps.bundle.outputs.has_plan == 'true'
uses: anthropics/claude-code-action@dcb57747bfceeaa1fa72638cae52295d1d853d4a # v1 uses: anthropics/claude-code-action@9d7150bc8a3dae8149739a88019d192b579ad90c # v1
with: with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Skip the OIDC -> Claude App token exchange, which 401s whenever # Skip the OIDC -> Claude App token exchange, which 401s whenever

View File

@@ -59,7 +59,7 @@ jobs:
# Remove issues list # Remove issues list
rm issueslist rm issueslist
- name: Commit if needed - name: Commit if needed
uses: EndBug/add-and-commit@v11.1.1 uses: EndBug/add-and-commit@v11.0.0
with: with:
message: "Github bot : issues linked to readme" message: "Github bot : issues linked to readme"
default_author: github_actions default_author: github_actions

View File

@@ -166,7 +166,7 @@ jobs:
id: classify id: classify
if: github.event_name != 'issue_comment' || steps.claim.outputs.go == 'true' if: github.event_name != 'issue_comment' || steps.claim.outputs.go == 'true'
continue-on-error: true continue-on-error: true
uses: anthropics/claude-code-action@dcb57747bfceeaa1fa72638cae52295d1d853d4a # v1 uses: anthropics/claude-code-action@9d7150bc8a3dae8149739a88019d192b579ad90c # v1
with: with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Without this the action falls back to the OIDC -> Claude App token # Without this the action falls back to the OIDC -> Claude App token

View File

@@ -79,7 +79,7 @@ jobs:
- name: Address CodeRabbit comments - name: Address CodeRabbit comments
if: steps.claim.outputs.go == 'true' if: steps.claim.outputs.go == 'true'
uses: anthropics/claude-code-action@dcb57747bfceeaa1fa72638cae52295d1d853d4a # v1 uses: anthropics/claude-code-action@9d7150bc8a3dae8149739a88019d192b579ad90c # v1
with: with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Skip the OIDC -> Claude App token exchange, which 401s whenever # Skip the OIDC -> Claude App token exchange, which 401s whenever

View File

@@ -95,7 +95,7 @@ jobs:
- name: Commit sanitize changes - name: Commit sanitize changes
id: sanitize_commit id: sanitize_commit
if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }} if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' }}
uses: EndBug/add-and-commit@v11.1.1 uses: EndBug/add-and-commit@v11.0.0
with: with:
commit: -u commit: -u
message: "GitHub bot: sanitize (spaces + LF endings) & chmod [nobuild]" message: "GitHub bot: sanitize (spaces + LF endings) & chmod [nobuild]"
@@ -410,7 +410,7 @@ jobs:
done done
- name: Commit changelog changes - name: Commit changelog changes
uses: EndBug/add-and-commit@v11.1.1 uses: EndBug/add-and-commit@v11.0.0
with: with:
commit: -u commit: -u
message: "GitHub bot: changelog [nobuild]" message: "GitHub bot: changelog [nobuild]"

View File

@@ -18,7 +18,7 @@ jobs:
uses: erclu/check-crlf@v1 uses: erclu/check-crlf@v1
- name: Commit if needed - name: Commit if needed
uses: EndBug/add-and-commit@v11.1.1 uses: EndBug/add-and-commit@v11.0.0
with: with:
message: "Github bot : CRLF corrected" message: "Github bot : CRLF corrected"
default_author: github_actions default_author: github_actions
@@ -50,7 +50,7 @@ jobs:
dos2unix -k "$f" dos2unix -k "$f"
done done
- name: Commit if needed - name: Commit if needed
uses: EndBug/add-and-commit@v11.1.1 uses: EndBug/add-and-commit@v11.0.0
with: with:
message: "Github bot : CRLF corrected" message: "Github bot : CRLF corrected"
default_author: github_actions default_author: github_actions

View File

@@ -31,7 +31,7 @@ jobs:
- name: Commit if needed - name: Commit if needed
if: steps.calibre.outputs.markdown != '' if: steps.calibre.outputs.markdown != ''
uses: EndBug/add-and-commit@v11.1.1 uses: EndBug/add-and-commit@v11.0.0
with: with:
message: "Github bot : image compressed" message: "Github bot : image compressed"
default_author: github_actions default_author: github_actions

View File

@@ -109,7 +109,7 @@ jobs:
#TOTAL3="$(awk '{SUM+=$2}END{print SUM}' Stats)" #TOTAL3="$(awk '{SUM+=$2}END{print SUM}' Stats)"
- name: Commit if needed - name: Commit if needed
uses: EndBug/add-and-commit@v11.1.1 uses: EndBug/add-and-commit@v11.0.0
with: with:
default_author: github_actions default_author: github_actions
message : "Github bot : stats updated" message : "Github bot : stats updated"

View File

@@ -168,59 +168,6 @@ if [ -z "$shebang" ]; then
exit 1 exit 1
fi fi
#####################################
# Seed the s6 container environment #
#####################################
# s6-overlay's stage 1 dumps the container's environment into /run/s6/container_environment, and
# `with-contenv` reads it back (emptyenv -p; s6-envdir). Add-ons that override the base image's
# ENTRYPOINT ["/init"] with ENTRYPOINT ["/usr/bin/env"] plus CMD ["/ha_entrypoint.sh"] never run
# stage 1, so nothing creates that directory and every #!/usr/bin/with-contenv script outside the
# three globs whose shebang is rewritten below either exits non-zero before its first line
# (directory missing: s6-envdir errors) or runs against whatever a cont-init script happened to
# leave there -- measured at 16 variables instead of 110, SUPERVISOR_TOKEN among the missing.
# Neither says anything, so all that surfaces is what the caller makes of it: a HEALTHCHECK
# reporting "unhealthy", a cron job doing nothing. So dump the environment here instead.
#
# Deliberately after the shebang probe above, not next to the other PID 1 setup: the probe's first
# candidate is "/command/with-contenv bashio" and it fails today in exactly these add-ons, so the
# probe falls through to "/usr/bin/env bashio". Seeding earlier would make that candidate start
# succeeding and flip the shebang of every cont-init and service script here, so scripts launched
# directly would lose what an earlier sourced script exported -- a far larger change than this.
#
# It does switch on two dormant writes: 00-global_var.sh and 01-config_yaml.sh push their values
# into the envdir, but only `if [ -d ]`. That is what those lines are for, and it means out-of-glob
# scripts now see the user's configured options too.
S6_CONTAINER_ENV="/run/s6/container_environment"
# Only when this script is PID 1 -- under /init it is the stage-2 hook and stage 1 has already
# written the directory -- and only where with-contenv exists to care.
if $PID1 && { [ -x /command/with-contenv ] || [ -x /usr/bin/with-contenv ]; }; then
# Filled in a sibling and renamed into place, never written to live. A half-populated envdir is
# worse than an absent one: s6-envdir accepts it, so a with-contenv script starts and runs
# against an environment quietly missing SUPERVISOR_TOKEN, where an absent one stops it at its
# shebang. rename(2) means a concurrent reader -- a HEALTHCHECK can run alongside PID 1 -- sees
# the directory either absent or complete, never mid-dump.
#
# Cleared rather than written over: /run is not a tmpfs here, so an image layer can persist
# entries, and writing name by name would merge into them and leave variables PID 1 does not
# have, a stale SUPERVISOR_TOKEN among them. A failed rm has to abort the chain, because mkdir -p
# accepts a surviving symlink-to-directory and would let the dump follow it. rm does not traverse
# a symlink, but it would empty anything bind-mounted at this exact path -- not a configuration
# any add-on uses, and not one s6 would tolerate either.
if rm -rf "$S6_CONTAINER_ENV" "$S6_CONTAINER_ENV.tmp" && mkdir -p "$S6_CONTAINER_ENV.tmp" &&
s6-dumpenv -- "$S6_CONTAINER_ENV.tmp" && mv "$S6_CONTAINER_ENV.tmp" "$S6_CONTAINER_ENV"; then
echo "Populated $S6_CONTAINER_ENV for with-contenv"
else
# Leaves the directory absent, which is exactly how this fails today -- so the failure mode is
# unchanged, not newly degraded. Never fatal, a read-only /run must still let the add-on boot,
# but never silent either, since the shebang failure it leaves behind says nothing on its own.
rm -rf "$S6_CONTAINER_ENV" "$S6_CONTAINER_ENV.tmp" 2>/dev/null || true
echo -e "\e[38;5;214m$(date) WARNING: could not populate $S6_CONTAINER_ENV; scripts with a with-contenv shebang will fail at their shebang, as they did before this was attempted\e[0m"
fi
fi
#################################### ####################################
# Bashio library for source fallback # Bashio library for source fallback
#################################### ####################################

View File

@@ -427,7 +427,7 @@ If you want to do add the repository manually, please follow the procedure highl
![aarch64][aarch64-badge] ![aarch64][aarch64-badge]
![amd64][amd64-badge] ![amd64][amd64-badge]
&#10003; [Free Games Claimer](free_games_claimer/) : Claims free games from Epic, Prime, GOG, Steam, Ubisoft and more &#10003; [Free Games Claimer](free_games_claimer/) : Automatically claims free games from Epic Games Store, Amazon Prime Gaming, GOG, Steam, and optional GamerPower-supported stores
&emsp;&emsp;![Version](https://img.shields.io/badge/dynamic/yaml?label=Version&query=%24.version&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Ffree_games_claimer%2Fconfig.yaml) &emsp;&emsp;![Version](https://img.shields.io/badge/dynamic/yaml?label=Version&query=%24.version&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Ffree_games_claimer%2Fconfig.yaml)
![Update](https://img.shields.io/badge/dynamic/json?label=Updated&query=%24.last_update&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Ffree_games_claimer%2Fupdater.json) ![Update](https://img.shields.io/badge/dynamic/json?label=Updated&query=%24.last_update&url=https%3A%2F%2Fraw.githubusercontent.com%2Falexbelgium%2Fhassio-addons%2Fmaster%2Ffree_games_claimer%2Fupdater.json)

View File

@@ -1,28 +1,3 @@
## 2.1.0 (2026-08-24)
- Updated the pinned upstream from Free Games Claimer Remaster 1.1 to 1.6,
which adds the Ubisoft giveaway, Fab, AliExpress and Epic mobile stores,
fixed daily scheduler times, the `VNC_URL` notification link override, and a
fix for the `--accept-lang` flag that made every store's browser detectable
as automated.
- Mirrored upstream's Chromium hardening: `xdg-open` is neutralised and an
`AutoLaunchProtocolsFromOrigins` policy is installed, so app-scheme links
cannot open a blocking dialog in the VNC session.
- Disabled upstream's release-update notification by default. It tells the user
to run `docker compose pull`, while the add-on is updated through the Home
Assistant add-on store. Set `NOTIFY_UPDATES=true` in `config.env` to receive
it anyway.
- Reworded the add-on description so the store list reads as partial rather
than exhaustive; the full list is in the README.
- The default store selection is unchanged: existing installations keep
claiming from Epic, Prime Gaming and GOG until `STORES` is edited.
- Replaced the pinned upstream commit with `ARG BUILD_UPSTREAM`, which names an
upstream release tag, and re-enabled the repository updater for this add-on.
Upstream releases are now picked up automatically instead of requiring a
manual commit pin. Upstream's development tags are excluded: `lastversion`
reports the `v1.7d` development tag as release `1.7`, for which no source
archive exists, so an unfiltered update would have broken the build.
## 2.0.1 (2026-07-17) ## 2.0.1 (2026-07-17)
- Aligned the pull-request build context with the production builder by copying - Aligned the pull-request build context with the production builder by copying

View File

@@ -17,7 +17,7 @@ ARG BUILD_REF
ARG BUILD_REPOSITORY ARG BUILD_REPOSITORY
ARG BUILD_VERSION ARG BUILD_VERSION
ARG UPSTREAM_REPOSITORY="P-Adamiec/Free-Games-Claimer-Remaster" ARG UPSTREAM_REPOSITORY="P-Adamiec/Free-Games-Claimer-Remaster"
ARG BUILD_UPSTREAM="1.6" ARG UPSTREAM_REF="cca4992354215cef8807b748575af797606627f4"
ENV S6_CMD_WAIT_FOR_SERVICES="1" \ ENV S6_CMD_WAIT_FOR_SERVICES="1" \
S6_CMD_WAIT_FOR_SERVICES_MAXTIME="0" \ S6_CMD_WAIT_FOR_SERVICES_MAXTIME="0" \
@@ -27,7 +27,10 @@ ENV S6_CMD_WAIT_FOR_SERVICES="1" \
WIDTH="1280" \ WIDTH="1280" \
HEIGHT="720" \ HEIGHT="720" \
DEPTH="24" \ DEPTH="24" \
SHOW="1" SHOW="1" \
COMMIT="${UPSTREAM_REF}" \
BRANCH="main" \
NOW="${BUILD_DATE}"
# Install the dependencies used by Free Games Claimer Remaster. The upstream # Install the dependencies used by Free Games Claimer Remaster. The upstream
# Dockerfile supports both amd64 (Google Chrome) and arm64 (Chromium), so the # Dockerfile supports both amd64 (Google Chrome) and arm64 (Chromium), so the
@@ -84,11 +87,6 @@ RUN apt-get update \
else \ else \
apt-get install -y --no-install-recommends chromium; \ apt-get install -y --no-install-recommends chromium; \
fi \ fi \
&& printf '#!/bin/sh\nexit 0\n' > /usr/bin/xdg-open \
&& chmod +x /usr/bin/xdg-open \
&& mkdir -p /etc/opt/chrome/policies/managed /etc/chromium/policies/managed \
&& printf '%s\n' '{"AutoLaunchProtocolsFromOrigins":[{"protocol":"aliexpress","allowed_origins":["*"]},{"protocol":"aliexpresshd","allowed_origins":["*"]},{"protocol":"aecmd","allowed_origins":["*"]},{"protocol":"alibaba","allowed_origins":["*"]},{"protocol":"alipay","allowed_origins":["*"]},{"protocol":"alipays","allowed_origins":["*"]},{"protocol":"tmall","allowed_origins":["*"]},{"protocol":"taobao","allowed_origins":["*"]},{"protocol":"market","allowed_origins":["*"]},{"protocol":"intent","allowed_origins":["*"]}]}' \
| tee /etc/opt/chrome/policies/managed/fgc-autolaunch.json /etc/chromium/policies/managed/fgc-autolaunch.json > /dev/null \
&& ln -sf /usr/share/novnc/vnc_auto.html /usr/share/novnc/index.html \ && ln -sf /usr/share/novnc/vnc_auto.html /usr/share/novnc/index.html \
&& ln -sf /usr/bin/python3 /usr/bin/python \ && ln -sf /usr/bin/python3 /usr/bin/python \
&& apt-get purge -y gnupg \ && apt-get purge -y gnupg \
@@ -96,20 +94,13 @@ RUN apt-get update \
&& apt-get clean \ && apt-get clean \
&& rm -rf /var/lib/apt/lists/* /var/cache/* /var/tmp/* /tmp/* /usr/share/doc/* && rm -rf /var/lib/apt/lists/* /var/cache/* /var/tmp/* /tmp/* /usr/share/doc/*
# Install the upstream release named by BUILD_UPSTREAM. The repository updater # Install a deterministic snapshot of the replacement upstream. Updating the
# bumps that value together with the add-on version. A release tag is a mutable # upstream reference is intentionally explicit so image contents cannot change
# reference: rebuilding the same BUILD_UPSTREAM installs whatever the tag points # without an add-on version bump.
# at today, which is the accepted cost of tracking upstream automatically.
# Upstream tags its releases "v1.6", but the tag name is downloaded without the
# prefix as well so that an unattended update does not fail the build if
# upstream ever drops it.
WORKDIR /fgc WORKDIR /fgc
RUN (curl --proto "=https" --tlsv1.2 -fsSL \ RUN curl --proto "=https" --tlsv1.2 -fsSL \
"https://github.com/${UPSTREAM_REPOSITORY}/archive/v${BUILD_UPSTREAM}.tar.gz" \ "https://github.com/${UPSTREAM_REPOSITORY}/archive/${UPSTREAM_REF}.tar.gz" \
-o /tmp/free-games-claimer-remaster.tar.gz \ -o /tmp/free-games-claimer-remaster.tar.gz \
|| curl --proto "=https" --tlsv1.2 -fsSL \
"https://github.com/${UPSTREAM_REPOSITORY}/archive/${BUILD_UPSTREAM}.tar.gz" \
-o /tmp/free-games-claimer-remaster.tar.gz) \
&& tar -xzf /tmp/free-games-claimer-remaster.tar.gz --strip-components=1 -C /fgc \ && tar -xzf /tmp/free-games-claimer-remaster.tar.gz --strip-components=1 -C /fgc \
&& python3 -m pip install --no-cache-dir --break-system-packages -r requirements.txt \ && python3 -m pip install --no-cache-dir --break-system-packages -r requirements.txt \
&& dos2unix ./*.sh \ && dos2unix ./*.sh \
@@ -164,4 +155,4 @@ LABEL \
org.opencontainers.image.created="${BUILD_DATE}" \ org.opencontainers.image.created="${BUILD_DATE}" \
org.opencontainers.image.revision="${BUILD_REF}" \ org.opencontainers.image.revision="${BUILD_REF}" \
org.opencontainers.image.version="${BUILD_VERSION}" \ org.opencontainers.image.version="${BUILD_VERSION}" \
io.hass.upstream="https://github.com/${UPSTREAM_REPOSITORY}/releases" io.hass.upstream="https://github.com/${UPSTREAM_REPOSITORY}/tree/${UPSTREAM_REF}"

View File

@@ -24,19 +24,14 @@ This add-on is based on
[Free Games Claimer Remaster](https://github.com/P-Adamiec/Free-Games-Claimer-Remaster). [Free Games Claimer Remaster](https://github.com/P-Adamiec/Free-Games-Claimer-Remaster).
It can claim free games from: It can claim free games from:
- Epic Games Store, including its weekly free mobile game - Epic Games Store
- Fab, Epic's asset marketplace (`fab`)
- Amazon Prime Gaming - Amazon Prime Gaming
- GOG - GOG
- Steam - Steam
- Ubisoft giveaways (`ubisoft`)
- AliExpress daily coin check-in (`aliexpress`)
- GamerPower-supported stores, when explicitly enabled - GamerPower-supported stores, when explicitly enabled
For compatibility with previous add-on releases, the default store selection For compatibility with previous add-on releases, the default store selection
remains Epic Games, Prime Gaming, and GOG. The other stores are enabled by remains Epic Games, Prime Gaming, and GOG.
adding them to `STORES`, for example `epic,prime,gog,fab,ubisoft`, and each
needs its own credentials in `config.env`.
## Web interface ## Web interface
@@ -103,11 +98,6 @@ NOTIFY=tgram://bot-token/chat-id
# DISCORD_WEBHOOK=https://discord.com/api/webhooks/... # DISCORD_WEBHOOK=https://discord.com/api/webhooks/...
``` ```
Upstream's release-update notification (`NOTIFY_UPDATES`) is disabled by the
add-on, because it advises running `docker compose pull` while the add-on is
actually updated through the Home Assistant add-on store. Setting
`NOTIFY_UPDATES=true` in `config.env` re-enables it.
Existing variables such as `EG_EMAIL`, `EG_PASSWORD`, `PG_EMAIL`, Existing variables such as `EG_EMAIL`, `EG_PASSWORD`, `PG_EMAIL`,
`PG_PASSWORD`, `PG_OTPKEY`, `GOG_EMAIL`, `GOG_PASSWORD`, `SHOW`, `WIDTH`, `PG_PASSWORD`, `PG_OTPKEY`, `GOG_EMAIL`, `GOG_PASSWORD`, `SHOW`, `WIDTH`,
`HEIGHT`, `TIMEOUT`, `LOGIN_TIMEOUT`, `DRYRUN`, and `NOTIFY` remain compatible. `HEIGHT`, `TIMEOUT`, `LOGIN_TIMEOUT`, `DRYRUN`, and `NOTIFY` remain compatible.
@@ -142,31 +132,16 @@ normally uses port `7080`.
## Upstream update policy ## Upstream update policy
The image is built from the upstream release named by `ARG BUILD_UPSTREAM` in The image is built from an explicit upstream commit in the Dockerfile. This
the Dockerfile, downloaded as the matching `v<version>` source tarball. The keeps amd64 and aarch64 images reproducible and prevents an upstream branch or
repository updater tracks upstream releases and bumps that value, the add-on container tag from changing without an add-on review and version bump.
version and `CHANGELOG.md` together, so a new upstream release reaches the
add-on without a manual edit.
A release tag is a mutable reference. Rebuilding the same `BUILD_UPSTREAM` The repository updater is intentionally paused for this add-on because the
installs whatever that tag points at, so an upstream tag that is force-moved or add-on uses its own `2.x` version series while the replacement upstream uses a
deleted would change or fail the build without an add-on change. That is the `1.x` version series. An automatic replacement would risk a Home Assistant
accepted cost of automatic tracking, and it is the same trade-off every other version regression and would not safely update the pinned commit. A maintainer
automatically updated add-on in this repository makes; the previous commit pin upstream update must therefore update `UPSTREAM_REF`, `upstream_version`, the
was immutable but could only be advanced by hand. add-on version, and `CHANGELOG.md` together.
Upstream's development tags (`v1.7d` and similar) are filtered out through
`"github_exclude": "d"` in `updater.json`. Without it the updater reports the
`v1.7d` tag as release `1.7`, for which GitHub serves no source archive, and
the build would fail.
The add-on version does not track the upstream version. The add-on uses a `2.x`
series while upstream is on `1.x`, and Home Assistant only offers an update
when the new version sorts strictly higher, so the updater increments the
add-on version (`2.1.0` to `2.1.1`) instead of publishing a lower-sorting
upstream number. The upstream release actually installed is recorded in
`upstream_version` in `updater.json`, in `CHANGELOG.md`, and in the add-on's
startup banner.
## Installation ## Installation

View File

@@ -2,7 +2,7 @@
arch: arch:
- aarch64 - aarch64
- amd64 - amd64
description: "Claims free games from Epic, Prime, GOG, Steam, Ubisoft and more" description: "Automatically claims free games from Epic Games Store, Amazon Prime Gaming, GOG, Steam, and optional GamerPower-supported stores"
devices: devices:
- /dev/dri - /dev/dri
- /dev/dri/card0 - /dev/dri/card0
@@ -96,5 +96,5 @@ schema:
slug: free_games_claimer slug: free_games_claimer
udev: true udev: true
url: https://github.com/alexbelgium/hassio-addons url: https://github.com/alexbelgium/hassio-addons
version: "2.1.0" version: "2.0.1"
webui: "[PROTO:ssl]://[HOST]:[PORT:6080]" webui: "[PROTO:ssl]://[HOST]:[PORT:6080]"

View File

@@ -36,12 +36,6 @@ set -a
source "${RUNTIME_CONFIG}" source "${RUNTIME_CONFIG}"
set +a set +a
# Upstream checks GitHub for newer releases and then tells the user to run
# "docker compose pull", which is wrong here: the add-on is updated through the
# Home Assistant add-on store. Default the check off, but honour an explicit
# NOTIFY_UPDATES from config.env.
export NOTIFY_UPDATES="${NOTIFY_UPDATES:-false}"
# The Home Assistant port mapping is intentionally kept at 6080 for a seamless # The Home Assistant port mapping is intentionally kept at 6080 for a seamless
# upgrade from the previous add-on, even though the new upstream defaults to 7080. # upgrade from the previous add-on, even though the new upstream defaults to 7080.
if [ -n "${NOVNC_PORT:-}" ] && [ "${NOVNC_PORT}" != "6080" ]; then if [ -n "${NOVNC_PORT:-}" ] && [ "${NOVNC_PORT}" != "6080" ]; then

View File

@@ -10,17 +10,12 @@ TIMEOUT=60
LOGIN_TIMEOUT=180 LOGIN_TIMEOUT=180
VNC_LOGIN_TIMEOUT=180 VNC_LOGIN_TIMEOUT=180
NOVNC_PORT=6080 NOVNC_PORT=6080
# Public address used in notification links when behind a reverse proxy.
# VNC_URL=https://fgc.example.tld
# Keep the previous add-on's default stores. The upstream also supports # Keep the previous add-on's default stores. Add steam or gamerpower if wanted.
# steam, fab, ubisoft, aliexpress and gamerpower; add them here if wanted.
STORES=epic,prime,gog STORES=epic,prime,gog
# Used only when RUN_ONCE is disabled in the add-on options. # Used only when RUN_ONCE is disabled in the add-on options.
SCHEDULER_HOURS=12 SCHEDULER_HOURS=12
# SCHEDULER_TIMEZONE=UTC
# SCHEDULER_FIXED_TIMES=17:00,21:30
# Common credentials can be used as fallbacks for all stores. # Common credentials can be used as fallbacks for all stores.
# EMAIL= # EMAIL=
@@ -38,27 +33,7 @@ SCHEDULER_HOURS=12
# GOG_PASSWORD= # GOG_PASSWORD=
# STEAM_USERNAME= # STEAM_USERNAME=
# STEAM_PASSWORD= # STEAM_PASSWORD=
# UBI_EMAIL=
# UBI_PASSWORD=
# UBI_OTPKEY=
# AE_EMAIL=
# AE_PASSWORD=
# Epic's weekly free mobile game, claimed with the epic store.
# EG_MOBILE=true
# EG_MOBILE_PLATFORMS=android,ios
# Fab assets sign in with the Epic account. Accepting the licence agreement is
# required to claim; set to false to be notified instead of accepting.
# FAB_ACCEPT_EULA=true
# Notifications (Apprise or Discord) # Notifications (Apprise or Discord)
# NOTIFY= # NOTIFY=
# DISCORD_WEBHOOK= # DISCORD_WEBHOOK=
# NOTIFY_ALREADY_CLAIMED=false
# NOTIFY_SKIP_STORES=
# Upstream release notifications are disabled by the add-on: updates are
# delivered through the Home Assistant add-on store, not by docker compose.
# Set to true to receive them anyway.
# NOTIFY_UPDATES=false

View File

@@ -1,13 +1,11 @@
{ {
"dockerhub_by_date": false, "dockerhub_by_date": false,
"dockerhub_list_size": 2, "dockerhub_list_size": 2,
"github_exclude": "d", "last_update": "17-07-2026",
"github_fulltag": false, "paused": true,
"last_update": "24-08-2026",
"paused": false,
"repository": "alexbelgium/hassio-addons", "repository": "alexbelgium/hassio-addons",
"slug": "free_games_claimer", "slug": "free_games_claimer",
"source": "github", "source": "github",
"upstream_repo": "P-Adamiec/Free-Games-Claimer-Remaster", "upstream_repo": "P-Adamiec/Free-Games-Claimer-Remaster",
"upstream_version": "1.6" "upstream_version": "1.1"
} }

View File

@@ -1,7 +1,3 @@
## 2.44.0.1 (2026-08-24)
- Rebuild to pick up a fix in the shared entrypoint: when `ha_entrypoint.sh` runs as PID 1, as it does in this add-on, it now populates `/run/s6/container_environment`, so a script carrying a `#!/usr/bin/with-contenv` shebang gets a populated environment including `SUPERVISOR_TOKEN` instead of failing at its shebang. Nothing shipped in this add-on still uses that shebang (the healthcheck moved to plain bash in 2.44.0), so this changes nothing about the agent itself; it matters for a `script.sh` added by the user, and it is the add-on that build-tests the shared change
## 2.44.0 (2026-08-23) ## 2.44.0 (2026-08-23)
- Fix: Use `portainer/agent:alpine-sts` to match the STS release channel configured in `updater.json` - Fix: Use `portainer/agent:alpine-sts` to match the STS release channel configured in `updater.json`

View File

@@ -41,4 +41,4 @@ schema:
slug: portainer_agent slug: portainer_agent
udev: true udev: true
url: https://github.com/alexbelgium/hassio-addons url: https://github.com/alexbelgium/hassio-addons
version: "2.44.0.1" version: "2.44.0"

View File

@@ -1,8 +1,4 @@
## 5.1.1.2 (2026-08-25)
- Ingress is now enabled: the WebUI opens directly in the Home Assistant sidebar, and the "Open Web UI" button now goes there. Access by ip:port is unchanged, but has to be typed rather than clicked, as Home Assistant does not allow an add-on to offer both.
- Note for users who set a "Host verification" whitelist in SABnzbd: ingress sends `Host: 127.0.0.1:8080` upstream, because SABnzbd rejects any Host that is not an IP literal. That whitelist therefore no longer filters the ingress route, which is gated by Home Assistant authentication instead. Direct ip:port access is unchanged and still filtered.
## 5.1.1 (2026-08-22) ## 5.1.1 (2026-08-22)
- Update to latest version from linuxserver/docker-sabnzbd (changelog : https://github.com/linuxserver/docker-sabnzbd/releases) - Update to latest version from linuxserver/docker-sabnzbd (changelog : https://github.com/linuxserver/docker-sabnzbd/releases)

View File

@@ -70,7 +70,7 @@ environment:
PGID: "0" PGID: "0"
PUID: "0" PUID: "0"
image: ghcr.io/alexbelgium/sabnzbd-{arch} image: ghcr.io/alexbelgium/sabnzbd-{arch}
ingress: true ingress_entry: sabnzbd
init: false init: false
map: map:
- addon_config:rw - addon_config:rw
@@ -106,4 +106,5 @@ schema:
slug: sabnzbd slug: sabnzbd
udev: true udev: true
url: https://github.com/alexbelgium/hassio-addons url: https://github.com/alexbelgium/hassio-addons
version: "5.1.1.2" version: "5.1.1"
webui: http://[HOST]:[PORT:8080]

View File

@@ -1,14 +1,21 @@
#!/usr/bin/with-contenv bashio #!/usr/bin/with-contenv bashio
# shellcheck shell=bash # shellcheck shell=bash
# shellcheck disable=SC2317
set -e set -e
################# #################
# NGINX SETTING # # NGINX SETTING #
################# #################
exit 0
ingress_port=$(bashio::addon.ingress_port) ingress_port=$(bashio::addon.ingress_port)
ingress_interface=$(bashio::addon.ip_address) ingress_interface=$(bashio::addon.ip_address)
ingress_entry=$(bashio::addon.ingress_entry)
sed -i "s/%%port%%/${ingress_port}/g" /etc/nginx/servers/ingress.conf sed -i "s/%%port%%/${ingress_port}/g" /etc/nginx/servers/ingress.conf
sed -i "s/%%interface%%/${ingress_interface}/g" /etc/nginx/servers/ingress.conf sed -i "s/%%interface%%/${ingress_interface}/g" /etc/nginx/servers/ingress.conf
sed -i "s|%%ingress_entry%%|${ingress_entry}|g" /etc/nginx/servers/ingress.conf
# Allows serving js
sed -i 's/<!-- %if-not-debug% -->/<!-- %if-not-debug% /g' /app/sabnzbd/webui/index.html
sed -i 's/<!-- %end% -->/ %end% -->/g' /app/sabnzbd/webui/index.html
sed -i 's/<!-- %if-debug%/<!-- %if-debug% -->/g' /app/sabnzbd/webui/index.html
sed -i 's/ %end% -->/<!-- %end% -->/g' /app/sabnzbd/webui/index.html

View File

@@ -2,34 +2,22 @@ server {
listen %%interface%%:%%port%% default_server; listen %%interface%%:%%port%% default_server;
include /etc/nginx/includes/server_params.conf; include /etc/nginx/includes/server_params.conf;
include /etc/nginx/includes/proxy_params.conf;
client_max_body_size 0; client_max_body_size 0;
location / { location / {
proxy_pass http://127.0.0.1:8080; add_header Access-Control-Allow-Origin *;
proxy_connect_timeout 30m;
proxy_send_timeout 30m;
proxy_read_timeout 30m;
proxy_pass http://127.0.0.1:8080;
# SABnzbd refuses any request whose Host is not an IP literal proxy_set_header Accept-Encoding "";
# ("Access denied - Hostname verification failed"), so send the # Correct url without port when using https
# upstream socket rather than the browser's host. X-Forwarded-For is sub_filter_once off;
# the only other header it reads (for its verify_xff_header option). sub_filter_types *;
proxy_set_header Host $proxy_host; sub_filter /sabnzbd %%ingress_entry%%/sabnzbd;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; }
# The interface itself only emits relative links, so no body
# rewriting is needed. Redirects are the exception: Raiser() emits
# a path under url_base, so prefix them with the ingress entry.
# absolute_redirect must stay off, or nginx expands the rewritten
# Location into http://<browser host>:<ingress port>/..., a port the
# browser cannot reach.
proxy_redirect / %%ingress_entry%%/;
absolute_redirect off;
# The login cookie is hardcoded to Path=/, which on the ingress
# origin would send it to every other add-on's ingress path too.
proxy_cookie_path / %%ingress_entry%%/;
proxy_http_version 1.1;
proxy_read_timeout 86400s;
proxy_send_timeout 86400s;
}
} }

View File

@@ -1,9 +0,0 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
# ==============================================================================
# Stop the container when Nginx fails, so ingress does not silently go dead
# ==============================================================================
if [[ "$1" -ne 0 && "$1" -ne 256 ]]; then
bashio::log.error "Nginx exited with code $1"
kill -15 1
fi

View File

@@ -1,11 +0,0 @@
#!/usr/bin/with-contenv bashio
# shellcheck shell=bash
set -e
# ==============================================================================
# Wait for sabnzbd to become available
bashio::net.wait_for 8080 localhost 900
bashio::log.info "Starting NGinx..."
exec nginx