Compare commits
18 Commits
create-pul
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
15a2abe451 | ||
|
|
4c1038916d | ||
|
|
71bbdb0929 | ||
|
|
41f0863702 | ||
|
|
8fec8556e8 | ||
|
|
3ec897fa38 | ||
|
|
5a4d7b2f8c | ||
|
|
b519002cea | ||
|
|
bad72b9451 | ||
|
|
4028564991 | ||
|
|
096db771fc | ||
|
|
58e98fb63c | ||
|
|
40d0401ccd | ||
|
|
883b38cacc | ||
|
|
80821a7d9a | ||
|
|
e7f8c37695 | ||
|
|
88faf503fb | ||
|
|
b88c5c5826 |
@@ -30,14 +30,8 @@ VAR="${1:?usage: env_trace.sh <VAR> [process-name-or-pid]}"
|
|||||||
TARGET="${2:-}"
|
TARGET="${2:-}"
|
||||||
# VAR is interpolated into grep/sed patterns below — restrict it to a valid env var name
|
# VAR is interpolated into grep/sed patterns below — restrict it to a valid env var name
|
||||||
case "$VAR" in
|
case "$VAR" in
|
||||||
[A-Za-z_]*) [ -z "${VAR//[A-Za-z0-9_]/}" ] || {
|
[A-Za-z_]*) [ -z "${VAR//[A-Za-z0-9_]/}" ] || { echo "invalid env var name: $VAR" >&2; exit 1; } ;;
|
||||||
echo "invalid env var name: $VAR" >&2
|
*) echo "invalid env var name: $VAR" >&2; exit 1 ;;
|
||||||
exit 1
|
|
||||||
} ;;
|
|
||||||
*)
|
|
||||||
echo "invalid env var name: $VAR" >&2
|
|
||||||
exit 1
|
|
||||||
;;
|
|
||||||
esac
|
esac
|
||||||
|
|
||||||
echo "== tracing ${VAR} =="
|
echo "== tracing ${VAR} =="
|
||||||
@@ -45,7 +39,7 @@ echo
|
|||||||
|
|
||||||
echo "1. /data/options.json (the user's saved options)"
|
echo "1. /data/options.json (the user's saved options)"
|
||||||
if [ -f /data/options.json ]; then
|
if [ -f /data/options.json ]; then
|
||||||
python3 - "$VAR" << 'PY'
|
python3 - "$VAR" <<'PY'
|
||||||
import json, sys
|
import json, sys
|
||||||
var = sys.argv[1]
|
var = sys.argv[1]
|
||||||
try:
|
try:
|
||||||
@@ -86,8 +80,7 @@ echo "3. s6 container_environment (only read by services using #!/usr/bin/with-c
|
|||||||
seen3=0
|
seen3=0
|
||||||
for d in /var/run/s6/container_environment /run/s6/container_environment; do
|
for d in /var/run/s6/container_environment /run/s6/container_environment; do
|
||||||
if [ -f "$d/$VAR" ]; then
|
if [ -f "$d/$VAR" ]; then
|
||||||
echo " $d/$VAR = [$(cat "$d/$VAR")]"
|
echo " $d/$VAR = [$(cat "$d/$VAR")]"; seen3=1
|
||||||
seen3=1
|
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
[ "$seen3" -eq 0 ] && echo " not present in either envdir"
|
[ "$seen3" -eq 0 ] && echo " not present in either envdir"
|
||||||
@@ -129,8 +122,8 @@ else
|
|||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
# What it was actually launched with beats any theory about its environment.
|
# What it was actually launched with beats any theory about its environment.
|
||||||
tr '\0' '\n' < "/proc/$pid/cmdline" 2> /dev/null | tail -n +2 \
|
tr '\0' '\n' < "/proc/$pid/cmdline" 2> /dev/null | tail -n +2 |
|
||||||
| grep -iE "res|screen|${VAR}" | head -3 | sed 's/^/ argv: /'
|
grep -iE "res|screen|${VAR}" | head -3 | sed 's/^/ argv: /'
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ awk '{t+=$2} END{printf " threads=%d\n", t}' <(ps -eo pid,nlwp --no-headers 2>
|
|||||||
echo
|
echo
|
||||||
echo "== per-process memory (top 20 by PSS) =="
|
echo "== per-process memory (top 20 by PSS) =="
|
||||||
printf ' %-28s %8s %8s %8s\n' COMMAND RSS PSS PRIVATE
|
printf ' %-28s %8s %8s %8s\n' COMMAND RSS PSS PRIVATE
|
||||||
python3 - "$OUT" << 'PY'
|
python3 - "$OUT" <<'PY'
|
||||||
import os, sys
|
import os, sys
|
||||||
rows = []
|
rows = []
|
||||||
for pid in filter(str.isdigit, os.listdir('/proc')):
|
for pid in filter(str.isdigit, os.listdir('/proc')):
|
||||||
@@ -62,17 +62,17 @@ PY
|
|||||||
|
|
||||||
echo
|
echo
|
||||||
echo "== reserved-but-not-resident (lazy allocations, NOT leaks) =="
|
echo "== reserved-but-not-resident (lazy allocations, NOT leaks) =="
|
||||||
ipcs -m 2> /dev/null | awk 'NR>3 && $5 ~ /^[0-9]+$/ && $5 > 50000000 {printf " SysV shm %.0f MB (owner %s) — check Rss in /proc/<pid>/smaps before calling it used\n", $5/1048576, $3}'
|
ipcs -m 2>/dev/null | awk 'NR>3 && $5 ~ /^[0-9]+$/ && $5 > 50000000 {printf " SysV shm %.0f MB (owner %s) — check Rss in /proc/<pid>/smaps before calling it used\n", $5/1048576, $3}'
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "== CPU over ${SAMPLE}s (idle unless you are driving the UI) =="
|
echo "== CPU over ${SAMPLE}s (idle unless you are driving the UI) =="
|
||||||
# utime+stime. Parsed after the LAST ')' because field 2 is (comm) and may contain spaces —
|
# utime+stime. Parsed after the LAST ')' because field 2 is (comm) and may contain spaces —
|
||||||
# a plain $14+$15 is wrong for anything like 'npm exec @foo' and silently reports a fabricated
|
# a plain $14+$15 is wrong for anything like 'npm exec @foo' and silently reports a fabricated
|
||||||
# number rather than failing.
|
# number rather than failing.
|
||||||
jiffies() { awk -F') ' '{n=split($NF,a," "); print a[12]+a[13]}' "/proc/$1/stat" 2> /dev/null; }
|
jiffies() { awk -F') ' '{n=split($NF,a," "); print a[12]+a[13]}' "/proc/$1/stat" 2>/dev/null; }
|
||||||
|
|
||||||
# jiffies are USER_HZ units — almost always 100, but read it rather than assume it
|
# jiffies are USER_HZ units — almost always 100, but read it rather than assume it
|
||||||
HZ=$(getconf CLK_TCK 2> /dev/null) && [ "$HZ" -gt 0 ] 2> /dev/null || HZ=100
|
HZ=$(getconf CLK_TCK 2>/dev/null) && [ "$HZ" -gt 0 ] 2>/dev/null || HZ=100
|
||||||
|
|
||||||
# Sample EVERY readable process, not the top-N of ps.txt: that list is sorted by RSS,
|
# Sample EVERY readable process, not the top-N of ps.txt: that list is sorted by RSS,
|
||||||
# and the busiest process is not necessarily a big one.
|
# and the busiest process is not necessarily a big one.
|
||||||
@@ -86,14 +86,14 @@ for pid in "${!t0[@]}"; do
|
|||||||
[ -r "/proc/$pid/stat" ] || continue
|
[ -r "/proc/$pid/stat" ] || continue
|
||||||
t1=$(jiffies "$pid") || continue
|
t1=$(jiffies "$pid") || continue
|
||||||
[ -n "$t1" ] && [ -n "${t0[$pid]}" ] || continue
|
[ -n "$t1" ] && [ -n "${t0[$pid]}" ] || continue
|
||||||
delta=$((t1 - ${t0[$pid]}))
|
delta=$(( t1 - ${t0[$pid]} ))
|
||||||
[ "$delta" -gt 0 ] || continue
|
[ "$delta" -gt 0 ] || continue
|
||||||
pct=$(awk -v d="$delta" -v s="$SAMPLE" -v hz="$HZ" 'BEGIN{printf "%.2f", d*100/(hz*s)}')
|
pct=$(awk -v d="$delta" -v s="$SAMPLE" -v hz="$HZ" 'BEGIN{printf "%.2f", d*100/(hz*s)}')
|
||||||
comm=$(tr -d '\0' < "/proc/$pid/comm" 2> /dev/null)
|
comm=$(tr -d '\0' < "/proc/$pid/comm" 2>/dev/null)
|
||||||
echo "$pct $pid $comm"
|
echo "$pct $pid $comm"
|
||||||
done | sort -rn | head -12 | awk '{printf " %6s%% %-8s %s\n", $1, $2, $3}'
|
done | sort -rn | head -12 | awk '{printf " %6s%% %-8s %s\n", $1, $2, $3}'
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo " established conns on :8082/:3000/:3001 = $(ss -tn 2> /dev/null | grep -cE 'ESTAB.*:(8082|3000|3001)')"
|
echo " established conns on :8082/:3000/:3001 = $(ss -tn 2>/dev/null | grep -cE 'ESTAB.*:(8082|3000|3001)')"
|
||||||
echo " (those are claude_desktop/webtop viewer ports; 0 here means CPU above is idle burn)"
|
echo " (those are claude_desktop/webtop viewer ports; 0 here means CPU above is idle burn)"
|
||||||
echo " raw ps: $OUT/ps.txt"
|
echo " raw ps: $OUT/ps.txt"
|
||||||
|
|||||||
@@ -22,173 +22,146 @@ set -uo pipefail
|
|||||||
|
|
||||||
REPO="${HASSIO_REPO:-}"
|
REPO="${HASSIO_REPO:-}"
|
||||||
[ -z "$REPO" ] && REPO=$(gh repo view --json nameWithOwner --jq .nameWithOwner 2> /dev/null)
|
[ -z "$REPO" ] && REPO=$(gh repo view --json nameWithOwner --jq .nameWithOwner 2> /dev/null)
|
||||||
[ -z "$REPO" ] && {
|
[ -z "$REPO" ] && { echo "cannot determine repo; set HASSIO_REPO=owner/name" >&2; exit 1; }
|
||||||
echo "cannot determine repo; set HASSIO_REPO=owner/name" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
echo "repo: $REPO" >&2
|
echo "repo: $REPO" >&2
|
||||||
CMD="${1:-}"
|
CMD="${1:-}"; PR="${2:-}"
|
||||||
PR="${2:-}"
|
[ -z "$CMD" ] || [ -z "$PR" ] && { sed -n '2,20p' "$0" | sed 's/^# \?//'; exit 1; }
|
||||||
[ -z "$CMD" ] || [ -z "$PR" ] && {
|
|
||||||
sed -n '2,20p' "$0" | sed 's/^# \?//'
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
case "$CMD" in
|
case "$CMD" in
|
||||||
list)
|
list)
|
||||||
echo "== inline comments on #$PR =="
|
echo "== inline comments on #$PR =="
|
||||||
gh api "repos/$REPO/pulls/$PR/comments" --paginate \
|
gh api "repos/$REPO/pulls/$PR/comments" --paginate \
|
||||||
--jq 'sort_by(.created_at)[] | "=== [\(.id)] \(.user.login) | \(.path):\(.line // .original_line) ===\n\(.body)\n"'
|
--jq 'sort_by(.created_at)[] | "=== [\(.id)] \(.user.login) | \(.path):\(.line // .original_line) ===\n\(.body)\n"'
|
||||||
echo "== review bodies =="
|
echo "== review bodies =="
|
||||||
gh api "repos/$REPO/pulls/$PR/reviews" \
|
gh api "repos/$REPO/pulls/$PR/reviews" \
|
||||||
--jq '.[] | select(.body != "") | "--- \(.user.login) (\(.state)) ---\n\(.body[0:4000])\n"'
|
--jq '.[] | select(.body != "") | "--- \(.user.login) (\(.state)) ---\n\(.body[0:4000])\n"'
|
||||||
;;
|
;;
|
||||||
status)
|
status)
|
||||||
gh pr checks "$PR" 2>&1 | head -15
|
gh pr checks "$PR" 2>&1 | head -15
|
||||||
echo
|
echo
|
||||||
gh api graphql -f query="{repository(owner:\"${REPO%%/*}\",name:\"${REPO##*/}\"){pullRequest(number:$PR){reviewThreads(first:50){nodes{id isResolved path comments(first:1){nodes{author{login}}}}}}}}" \
|
gh api graphql -f query="{repository(owner:\"${REPO%%/*}\",name:\"${REPO##*/}\"){pullRequest(number:$PR){reviewThreads(first:50){nodes{id isResolved path comments(first:1){nodes{author{login}}}}}}}}" \
|
||||||
--jq '.data.repository.pullRequest.reviewThreads.nodes[] | "\(if .isResolved then "resolved" else "OPEN " end) \(.id) \(.comments.nodes[0].author.login) \(.path)"'
|
--jq '.data.repository.pullRequest.reviewThreads.nodes[] | "\(if .isResolved then "resolved" else "OPEN " end) \(.id) \(.comments.nodes[0].author.login) \(.path)"'
|
||||||
;;
|
;;
|
||||||
reply)
|
reply)
|
||||||
ID="${3:?comment id}"
|
ID="${3:?comment id}"; BODY="${4:?text or @file}"
|
||||||
BODY="${4:?text or @file}"
|
if [ "${BODY#@}" != "$BODY" ]; then
|
||||||
if [ "${BODY#@}" != "$BODY" ]; then
|
out=$(gh api "repos/$REPO/pulls/$PR/comments/$ID/replies" -F body=@"${BODY#@}" --jq '.id' 2>&1)
|
||||||
out=$(gh api "repos/$REPO/pulls/$PR/comments/$ID/replies" -F body=@"${BODY#@}" --jq '.id' 2>&1)
|
rc=$?
|
||||||
rc=$?
|
else
|
||||||
else
|
out=$(gh api "repos/$REPO/pulls/$PR/comments/$ID/replies" -f body="$BODY" --jq '.id' 2>&1)
|
||||||
out=$(gh api "repos/$REPO/pulls/$PR/comments/$ID/replies" -f body="$BODY" --jq '.id' 2>&1)
|
rc=$?
|
||||||
rc=$?
|
fi
|
||||||
fi
|
# Silently "succeeding" here is worse than failing: a later session reads the transcript and
|
||||||
# Silently "succeeding" here is worse than failing: a later session reads the transcript and
|
# believes a reviewer was answered when they were not.
|
||||||
# believes a reviewer was answered when they were not.
|
if [ "$rc" -eq 0 ] && [ -n "$out" ]; then
|
||||||
if [ "$rc" -eq 0 ] && [ -n "$out" ]; then
|
echo "replied to $ID (comment $out)"
|
||||||
echo "replied to $ID (comment $out)"
|
else
|
||||||
else
|
echo "FAILED to reply to $ID: $out" >&2
|
||||||
echo "FAILED to reply to $ID: $out" >&2
|
echo " (top-level review bodies have different ids and cannot take replies here)" >&2
|
||||||
echo " (top-level review bodies have different ids and cannot take replies here)" >&2
|
exit 1
|
||||||
exit 1
|
fi
|
||||||
fi
|
;;
|
||||||
;;
|
resolve)
|
||||||
resolve)
|
shift 2
|
||||||
shift 2
|
ids="$*"
|
||||||
ids="$*"
|
if [ "${ids:-}" = "--all" ]; then
|
||||||
if [ "${ids:-}" = "--all" ]; then
|
echo "About to resolve EVERY unresolved thread. Only do this if you have read and"
|
||||||
echo "About to resolve EVERY unresolved thread. Only do this if you have read and"
|
echo "answered each one — a resolved-but-wrong thread is worse than an open one."
|
||||||
echo "answered each one — a resolved-but-wrong thread is worse than an open one."
|
gh api graphql -f query="{repository(owner:\"${REPO%%/*}\",name:\"${REPO##*/}\"){pullRequest(number:$PR){reviewThreads(first:100){nodes{isResolved path comments(first:1){nodes{author{login} body}}}}}}}" \
|
||||||
gh api graphql -f query="{repository(owner:\"${REPO%%/*}\",name:\"${REPO##*/}\"){pullRequest(number:$PR){reviewThreads(first:100){nodes{isResolved path comments(first:1){nodes{author{login} body}}}}}}}" \
|
--jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved==false) | " - \(.comments.nodes[0].author.login) \(.path): \(.comments.nodes[0].body[0:90])"'
|
||||||
--jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved==false) | " - \(.comments.nodes[0].author.login) \(.path): \(.comments.nodes[0].body[0:90])"'
|
printf 'Type "yes" to resolve all: '; read -r ok
|
||||||
printf 'Type "yes" to resolve all: '
|
[ "$ok" = "yes" ] || { echo "aborted"; exit 1; }
|
||||||
read -r ok
|
ids=""
|
||||||
[ "$ok" = "yes" ] || {
|
elif [ -z "$ids" ]; then
|
||||||
echo "aborted"
|
echo "usage: pr_review.sh resolve <PR> <THREAD_ID...> (or --all, with confirmation)" >&2
|
||||||
exit 1
|
echo "resolve each thread as you answer it; get ids from: pr_review.sh status $PR" >&2
|
||||||
}
|
exit 1
|
||||||
ids=""
|
fi
|
||||||
elif [ -z "$ids" ]; then
|
if [ -z "$ids" ]; then
|
||||||
echo "usage: pr_review.sh resolve <PR> <THREAD_ID...> (or --all, with confirmation)" >&2
|
ids=$(gh api graphql -f query="{repository(owner:\"${REPO%%/*}\",name:\"${REPO##*/}\"){pullRequest(number:$PR){reviewThreads(first:50){nodes{id isResolved}}}}}" \
|
||||||
echo "resolve each thread as you answer it; get ids from: pr_review.sh status $PR" >&2
|
--jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved==false) | .id')
|
||||||
exit 1
|
fi
|
||||||
fi
|
[ -z "$ids" ] && { echo "nothing unresolved"; exit 0; }
|
||||||
if [ -z "$ids" ]; then
|
rfail=0
|
||||||
ids=$(gh api graphql -f query="{repository(owner:\"${REPO%%/*}\",name:\"${REPO##*/}\"){pullRequest(number:$PR){reviewThreads(first:50){nodes{id isResolved}}}}}" \
|
for id in $ids; do
|
||||||
--jq '.data.repository.pullRequest.reviewThreads.nodes[] | select(.isResolved==false) | .id')
|
r=$(gh api graphql -f query="mutation{resolveReviewThread(input:{threadId:\"$id\"}){thread{isResolved}}}" \
|
||||||
fi
|
--jq '.data.resolveReviewThread.thread.isResolved' 2>&1)
|
||||||
[ -z "$ids" ] && {
|
echo " $id -> $r"
|
||||||
echo "nothing unresolved"
|
[ "$r" = "true" ] || rfail=1
|
||||||
exit 0
|
done
|
||||||
}
|
# exiting 0 on a failed mutation would let a session believe threads were resolved
|
||||||
rfail=0
|
exit "$rfail"
|
||||||
for id in $ids; do
|
;;
|
||||||
r=$(gh api graphql -f query="mutation{resolveReviewThread(input:{threadId:\"$id\"}){thread{isResolved}}}" \
|
watch)
|
||||||
--jq '.data.resolveReviewThread.thread.isResolved' 2>&1)
|
MINS="${3:-180}" # the addon build alone has taken ~3h; 20 was far too short
|
||||||
echo " $id -> $r"
|
# Checks that are red on essentially every add-on PR here and gate nothing: master carries no
|
||||||
[ "$r" = "true" ] || rfail=1
|
# branch protection, and #3019, #3044 and #3050 all merged with Codacy failing. They are kept
|
||||||
done
|
# out of the verdict but always printed, so the reader still sees them and can judge. This is
|
||||||
# exiting 0 on a failed mutation would let a session believe threads were resolved
|
# deliberately a denylist of known noise, not an allowlist of blocking checks — a job added to
|
||||||
exit "$rfail"
|
# CI later counts as blocking until someone puts it here on purpose.
|
||||||
;;
|
ADVISORY_CHECKS="Codacy Static Code Analysis" # one per line if more are ever added
|
||||||
watch)
|
wfail=2 # not 0: running out of minutes with checks still pending is not a pass
|
||||||
MINS="${3:-180}" # the addon build alone has taken ~3h; 20 was far too short
|
c=""; bstates=""; adv=""
|
||||||
# Checks that are red on essentially every add-on PR here and gate nothing: master carries no
|
for i in $(seq 1 "$MINS"); do
|
||||||
# branch protection, and #3019, #3044 and #3050 all merged with Codacy failing. They are kept
|
# gh pr checks emits TAB-separated columns with no header when its output is not a TTY,
|
||||||
# out of the verdict but always printed, so the reader still sees them and can judge. This is
|
# which inside this $(... | awk) it never is. (Attached to a terminal it prints a wholly
|
||||||
# deliberately a denylist of known noise, not an allowlist of blocking checks — a job added to
|
# different ANSI table; --json would be sturdier still but does not exist before gh 2.36,
|
||||||
# CI later counts as blocking until someone puts it here on purpose.
|
# and 2.23 ships here.) Every blocking gate has spaces in its name — "Addon linting
|
||||||
ADVISORY_CHECKS="Codacy Static Code Analysis" # one per line if more are ever added
|
# (wger)", "Test addon build (wger)" — so awk's default separator split them on
|
||||||
wfail=2 # not 0: running out of minutes with checks still pending is not a pass
|
# whitespace: "Codacy Static Code Analysis<TAB>fail" became "Codacy=Static" and the state
|
||||||
c=""
|
# column was never read at all. watch printed "all passing" on a red #3044 and on #3042
|
||||||
bstates=""
|
# with the linter failing, and could not see a pending build either.
|
||||||
adv=""
|
# If that format ever does change, the allowlist below fails safe rather than passing: a
|
||||||
for i in $(seq 1 "$MINS"); do
|
# header row lands in the failure branch, and a space-aligned table parses to no rows,
|
||||||
# gh pr checks emits TAB-separated columns with no header when its output is not a TTY,
|
# which keeps watch waiting instead of returning 0.
|
||||||
# which inside this $(... | awk) it never is. (Attached to a terminal it prints a wholly
|
rows=$(gh pr checks "$PR" 2> /dev/null | awk -F'\t' -v ADV="$ADVISORY_CHECKS" '
|
||||||
# different ANSI table; --json would be sturdier still but does not exist before gh 2.36,
|
|
||||||
# and 2.23 ships here.) Every blocking gate has spaces in its name — "Addon linting
|
|
||||||
# (wger)", "Test addon build (wger)" — so awk's default separator split them on
|
|
||||||
# whitespace: "Codacy Static Code Analysis<TAB>fail" became "Codacy=Static" and the state
|
|
||||||
# column was never read at all. watch printed "all passing" on a red #3044 and on #3042
|
|
||||||
# with the linter failing, and could not see a pending build either.
|
|
||||||
# If that format ever does change, the allowlist below fails safe rather than passing: a
|
|
||||||
# header row lands in the failure branch, and a space-aligned table parses to no rows,
|
|
||||||
# which keeps watch waiting instead of returning 0.
|
|
||||||
rows=$(gh pr checks "$PR" 2> /dev/null | awk -F'\t' -v ADV="$ADVISORY_CHECKS" '
|
|
||||||
BEGIN { n = split(ADV, a, "\n"); for (j = 1; j <= n; j++) adv[a[j]] = 1 }
|
BEGIN { n = split(ADV, a, "\n"); for (j = 1; j <= n; j++) adv[a[j]] = 1 }
|
||||||
NF >= 2 { print (($1 in adv) ? "A" : "B") "\t" $1 "=" $2 "\t" $2 }')
|
NF >= 2 { print (($1 in adv) ? "A" : "B") "\t" $1 "=" $2 "\t" $2 }')
|
||||||
if [ -z "$rows" ]; then
|
if [ -z "$rows" ]; then
|
||||||
# Normal in the first minutes after `gh pr create`, and also whenever gh errors.
|
# Normal in the first minutes after `gh pr create`, and also whenever gh errors.
|
||||||
# Calling that "settled" would report success for checks that never ran.
|
# Calling that "settled" would report success for checks that never ran.
|
||||||
echo "[$i] no checks reported yet (gh returned nothing) — still waiting"
|
echo "[$i] no checks reported yet (gh returned nothing) — still waiting"
|
||||||
sleep 60
|
sleep 60; continue
|
||||||
continue
|
fi
|
||||||
fi
|
c=$(printf '%s\n' "$rows" | cut -f2 | tr '\n' ' ')
|
||||||
c=$(printf '%s\n' "$rows" | cut -f2 | tr '\n' ' ')
|
echo "[$i] $c"
|
||||||
echo "[$i] $c"
|
# Judge the state column only, never the joined name=state line: a check whose NAME
|
||||||
# Judge the state column only, never the joined name=state line: a check whose NAME
|
# contains "fail" must not read as a failure.
|
||||||
# contains "fail" must not read as a failure.
|
bstates=$(printf '%s\n' "$rows" | awk -F'\t' '$1 == "B" { print $3 }' | tr '\n' ' ')
|
||||||
bstates=$(printf '%s\n' "$rows" | awk -F'\t' '$1 == "B" { print $3 }' | tr '\n' ' ')
|
adv=$(printf '%s\n' "$rows" | awk -F'\t' '$1 == "A" { print $2 }' | tr '\n' ' ')
|
||||||
adv=$(printf '%s\n' "$rows" | awk -F'\t' '$1 == "A" { print $2 }' | tr '\n' ' ')
|
if [ -z "$bstates" ]; then
|
||||||
if [ -z "$bstates" ]; then
|
echo " only advisory checks have reported — no blocking check has run yet"
|
||||||
echo " only advisory checks have reported — no blocking check has run yet"
|
sleep 60; continue
|
||||||
sleep 60
|
fi
|
||||||
continue
|
# Allowlist the good states rather than denylisting the bad ones: an unrecognised state
|
||||||
fi
|
# must land in the failure branch, because falling through to "passing" is this command's
|
||||||
# Allowlist the good states rather than denylisting the bad ones: an unrecognised state
|
# worst outcome.
|
||||||
# must land in the failure branch, because falling through to "passing" is this command's
|
nbad=0; npend=0
|
||||||
# worst outcome.
|
for s in $bstates; do
|
||||||
nbad=0
|
case "$s" in
|
||||||
npend=0
|
pass | skipping) ;;
|
||||||
for s in $bstates; do
|
pending) npend=$((npend + 1)) ;;
|
||||||
case "$s" in
|
*) nbad=$((nbad + 1)) ;;
|
||||||
pass | skipping) ;;
|
esac
|
||||||
pending) npend=$((npend + 1)) ;;
|
|
||||||
*) nbad=$((nbad + 1)) ;;
|
|
||||||
esac
|
|
||||||
done
|
|
||||||
if [ "$nbad" -gt 0 ]; then
|
|
||||||
echo "settled — blocking checks FAILED:"
|
|
||||||
printf '%s\n' "$rows" \
|
|
||||||
| awk -F'\t' '$1 == "B" && $3 != "pass" && $3 != "skipping" && $3 != "pending" { print " " $2 }'
|
|
||||||
wfail=1
|
|
||||||
break
|
|
||||||
elif [ "$npend" -gt 0 ]; then
|
|
||||||
sleep 60
|
|
||||||
continue
|
|
||||||
else
|
|
||||||
echo "settled — blocking checks passing"
|
|
||||||
wfail=0
|
|
||||||
break
|
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
[ "$wfail" -eq 2 ] && echo "gave up after ${MINS}m, checks still unsettled — NOT a pass"
|
if [ "$nbad" -gt 0 ]; then
|
||||||
# Printed on pass and on failure alike: it is excluded from the verdict, not hidden.
|
echo "settled — blocking checks FAILED:"
|
||||||
[ -n "$adv" ] && echo " advisory (non-blocking, not counted in the verdict): $adv"
|
printf '%s\n' "$rows" |
|
||||||
# A PR touching no */config.* skips the CHANGELOG, linter and build jobs outright (#3018).
|
awk -F'\t' '$1 == "B" && $3 != "pass" && $3 != "skipping" && $3 != "pending" { print " " $2 }'
|
||||||
case " $bstates " in *" skipping "*) echo " ...of which some were SKIPPED — a skipped job tested nothing" ;; esac
|
wfail=1; break
|
||||||
echo "note: long queues here are usually account runner contention, not your diff."
|
elif [ "$npend" -gt 0 ]; then
|
||||||
exit "$wfail"
|
sleep 60; continue
|
||||||
;;
|
else
|
||||||
*)
|
echo "settled — blocking checks passing"; wfail=0; break
|
||||||
echo "unknown: $CMD"
|
fi
|
||||||
exit 1
|
done
|
||||||
;;
|
[ "$wfail" -eq 2 ] && echo "gave up after ${MINS}m, checks still unsettled — NOT a pass"
|
||||||
|
# Printed on pass and on failure alike: it is excluded from the verdict, not hidden.
|
||||||
|
[ -n "$adv" ] && echo " advisory (non-blocking, not counted in the verdict): $adv"
|
||||||
|
# A PR touching no */config.* skips the CHANGELOG, linter and build jobs outright (#3018).
|
||||||
|
case " $bstates " 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."
|
||||||
|
exit "$wfail"
|
||||||
|
;;
|
||||||
|
*) echo "unknown: $CMD"; exit 1 ;;
|
||||||
esac
|
esac
|
||||||
|
|||||||
@@ -59,10 +59,7 @@ if [ -n "${BUILD_VERSION:-}" ]; then
|
|||||||
if [ -z "$SLUG" ] && [ -n "${HOSTNAME:-}" ]; then
|
if [ -z "$SLUG" ] && [ -n "${HOSTNAME:-}" ]; then
|
||||||
base=$(printf '%s' "$HOSTNAME" | sed 's/^[0-9a-f]\{8\}-//')
|
base=$(printf '%s' "$HOSTNAME" | sed 's/^[0-9a-f]\{8\}-//')
|
||||||
for cand in "$(printf '%s' "$base" | tr '-' '_')" "$base"; do
|
for cand in "$(printf '%s' "$base" | tr '-' '_')" "$base"; do
|
||||||
[ -f "$REPO/$cand/config.yaml" ] && {
|
[ -f "$REPO/$cand/config.yaml" ] && { SLUG="$cand"; break; }
|
||||||
SLUG="$cand"
|
|
||||||
break
|
|
||||||
}
|
|
||||||
done
|
done
|
||||||
[ -z "$SLUG" ] && SLUG="$base"
|
[ -z "$SLUG" ] && SLUG="$base"
|
||||||
fi
|
fi
|
||||||
@@ -84,8 +81,8 @@ if [ -n "${BUILD_VERSION:-}" ]; then
|
|||||||
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
|
||||||
master=$(git show origin/master:"$SLUG/config.yaml" 2> /dev/null \
|
master=$(git show origin/master:"$SLUG/config.yaml" 2> /dev/null |
|
||||||
| grep -E '^version:' | head -1 | tr -d "\"'" | awk '{print $2}')
|
grep -E '^version:' | head -1 | tr -d "\"'" | awk '{print $2}')
|
||||||
echo " origin/master version = ${master:-unknown}"
|
echo " origin/master version = ${master:-unknown}"
|
||||||
echo " -> work from origin/master; analysing this branch will mislead you."
|
echo " -> work from origin/master; analysing this branch will mislead you."
|
||||||
echo
|
echo
|
||||||
|
|||||||
@@ -12,10 +12,7 @@
|
|||||||
set -uo pipefail
|
set -uo pipefail
|
||||||
|
|
||||||
TRAPS="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/references/traps.md"
|
TRAPS="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)/references/traps.md"
|
||||||
[ -f "$TRAPS" ] || {
|
[ -f "$TRAPS" ] || { echo "not found: $TRAPS" >&2; exit 1; }
|
||||||
echo "not found: $TRAPS" >&2
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# The Contents list duplicates the headings; grep the headings themselves so the list cannot drift.
|
# The Contents list duplicates the headings; grep the headings themselves so the list cannot drift.
|
||||||
list() {
|
list() {
|
||||||
@@ -23,16 +20,13 @@ list() {
|
|||||||
grep '^## ' "$TRAPS" | grep -v '^## Contents' | sed 's/^## / /'
|
grep '^## ' "$TRAPS" | grep -v '^## Contents' | sed 's/^## / /'
|
||||||
}
|
}
|
||||||
|
|
||||||
[ $# -eq 0 ] && {
|
[ $# -eq 0 ] && { list; exit 0; }
|
||||||
list
|
|
||||||
exit 0
|
|
||||||
}
|
|
||||||
|
|
||||||
# awk over exact heading text: section names contain '/' and other characters that would need
|
# 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
|
# escaping in a sed address, and a keyword matching several headings should be an error, not a
|
||||||
# silent pick of the first.
|
# silent pick of the first.
|
||||||
mapfile -t matches < <(grep '^## ' "$TRAPS" | grep -v '^## Contents' \
|
mapfile -t matches < <(grep '^## ' "$TRAPS" | grep -v '^## Contents' |
|
||||||
| grep -iF -- "$1" | sed 's/^## //')
|
grep -iF -- "$1" | sed 's/^## //')
|
||||||
|
|
||||||
case "${#matches[@]}" in
|
case "${#matches[@]}" in
|
||||||
0)
|
0)
|
||||||
|
|||||||
@@ -13,31 +13,22 @@ set -uo pipefail
|
|||||||
if root=$(git rev-parse --show-toplevel 2> /dev/null); then cd "$root" || exit 1; fi
|
if root=$(git rev-parse --show-toplevel 2> /dev/null); then cd "$root" || exit 1; fi
|
||||||
|
|
||||||
ADDON="${1:-}"
|
ADDON="${1:-}"
|
||||||
[ "${ADDON:-}" = "--vs-master" ] && {
|
[ "${ADDON:-}" = "--vs-master" ] && { ADDON=""; set -- --vs-master; }
|
||||||
ADDON=""
|
|
||||||
set -- --vs-master
|
|
||||||
}
|
|
||||||
VS_MASTER=false
|
VS_MASTER=false
|
||||||
for a in "$@"; do [ "$a" = "--vs-master" ] && VS_MASTER=true; done
|
for a in "$@"; do [ "$a" = "--vs-master" ] && VS_MASTER=true; done
|
||||||
|
|
||||||
if [ -z "$ADDON" ]; then
|
if [ -z "$ADDON" ]; then
|
||||||
mapfile -t _dirs < <(git diff --name-only origin/master...HEAD 2> /dev/null \
|
mapfile -t _dirs < <(git diff --name-only origin/master...HEAD 2> /dev/null |
|
||||||
| cut -d/ -f1 | sort -u | grep -vE '^\.')
|
cut -d/ -f1 | sort -u | grep -vE '^\.' )
|
||||||
if [ "${#_dirs[@]}" -gt 1 ]; then
|
if [ "${#_dirs[@]}" -gt 1 ]; then
|
||||||
echo "several changed dirs: ${_dirs[*]}"
|
echo "several changed dirs: ${_dirs[*]}"
|
||||||
echo "pass one explicitly: validate.sh <addon-dir>"
|
echo "pass one explicitly: validate.sh <addon-dir>"; exit 1
|
||||||
exit 1
|
|
||||||
fi
|
fi
|
||||||
ADDON="${_dirs[0]:-}"
|
ADDON="${_dirs[0]:-}"
|
||||||
fi
|
fi
|
||||||
[ -z "$ADDON" ] && {
|
[ -z "$ADDON" ] && { echo "usage: validate.sh <addon-dir> [--vs-master]"; exit 1; }
|
||||||
echo "usage: validate.sh <addon-dir> [--vs-master]"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
git rev-parse --verify origin/master > /dev/null 2>&1 || {
|
git rev-parse --verify origin/master > /dev/null 2>&1 || {
|
||||||
echo "origin/master missing — run: git fetch origin master"
|
echo "origin/master missing — run: git fetch origin master"; exit 1; }
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
export PYTHONDONTWRITEBYTECODE=1
|
export PYTHONDONTWRITEBYTECODE=1
|
||||||
echo "== validating $ADDON =="
|
echo "== validating $ADDON =="
|
||||||
|
|
||||||
@@ -45,22 +36,14 @@ 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
|
# 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.
|
# linter below can read one, so anything either says about it is noise.
|
||||||
is_execline() {
|
is_execline() { local l; IFS= read -r l < "$1" 2> /dev/null; [[ $l == '#!'*execlineb* ]]; }
|
||||||
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). One list for both.
|
||||||
files=()
|
files=()
|
||||||
while IFS= read -r f; do is_execline "$f" || files+=("$f"); done \
|
while IFS= read -r f; do is_execline "$f" || files+=("$f"); done \
|
||||||
< <(find "$ADDON" -type f \( -name '*.sh' -o -name 'run' -o -name 'finish' -o -name 'autostart' \) 2> /dev/null)
|
< <(find "$ADDON" -type f \( -name '*.sh' -o -name 'run' -o -name 'finish' -o -name 'autostart' \) 2> /dev/null)
|
||||||
for f in "${files[@]}"; do
|
for f in "${files[@]}"; do
|
||||||
if ! out=$(bash -n "$f" 2>&1); then
|
if ! out=$(bash -n "$f" 2>&1); then note "bash -n" "FAIL $f"; echo "$out" | sed 's/^/ /'; fail=1; fi
|
||||||
note "bash -n" "FAIL $f"
|
|
||||||
echo "$out" | sed 's/^/ /'
|
|
||||||
fail=1
|
|
||||||
fi
|
|
||||||
done
|
done
|
||||||
[ "$fail" -eq 0 ] && note "bash -n" "${#files[@]} file(s) checked"
|
[ "$fail" -eq 0 ] && note "bash -n" "${#files[@]} file(s) checked"
|
||||||
|
|
||||||
@@ -82,16 +65,13 @@ command -v hadolint > /dev/null 2>&1 && [ -f "$ADDON/Dockerfile" ] && {
|
|||||||
|
|
||||||
if [ -f "$ADDON/config.yaml" ]; then
|
if [ -f "$ADDON/config.yaml" ]; then
|
||||||
# path passed as argv, never interpolated into Python source
|
# path passed as argv, never interpolated into Python source
|
||||||
python3 - "$ADDON/config.yaml" << 'PY' || {
|
python3 - "$ADDON/config.yaml" <<'PY' || { note "config.yaml" "FAIL parse"; fail=1; }
|
||||||
import yaml,sys
|
import yaml,sys
|
||||||
d=yaml.safe_load(open(sys.argv[1]))
|
d=yaml.safe_load(open(sys.argv[1]))
|
||||||
print(' %-13s ok (version=%s, %d options)' % ('config.yaml', d.get('version'), len(d.get('options') or {})))
|
print(' %-13s ok (version=%s, %d options)' % ('config.yaml', d.get('version'), len(d.get('options') or {})))
|
||||||
missing=[k for k in (d.get('options') or {}) if k not in (d.get('schema') or {})]
|
missing=[k for k in (d.get('options') or {}) if k not in (d.get('schema') or {})]
|
||||||
if missing: print(' %-13s options with no schema entry: %s' % ('WARN', missing)); sys.exit(0)
|
if missing: print(' %-13s options with no schema entry: %s' % ('WARN', missing)); sys.exit(0)
|
||||||
PY
|
PY
|
||||||
note "config.yaml" "FAIL parse"
|
|
||||||
fail=1
|
|
||||||
}
|
|
||||||
command -v yamllint > /dev/null 2>&1 && {
|
command -v yamllint > /dev/null 2>&1 && {
|
||||||
yl=$(yamllint -f parsable "$ADDON/config.yaml" 2>&1 | grep -c .)
|
yl=$(yamllint -f parsable "$ADDON/config.yaml" 2>&1 | grep -c .)
|
||||||
note "yamllint" "$yl finding(s) (compare with --vs-master)"
|
note "yamllint" "$yl finding(s) (compare with --vs-master)"
|
||||||
@@ -99,10 +79,7 @@ PY
|
|||||||
fi
|
fi
|
||||||
|
|
||||||
while IFS= read -r f; do
|
while IFS= read -r f; do
|
||||||
python3 -m py_compile "$f" 2> /dev/null || {
|
python3 -m py_compile "$f" 2> /dev/null || { note "py_compile" "FAIL $f"; fail=1; }
|
||||||
note "py_compile" "FAIL $f"
|
|
||||||
fail=1
|
|
||||||
}
|
|
||||||
done < <(find "$ADDON" -type f -name '*.py' 2> /dev/null)
|
done < <(find "$ADDON" -type f -name '*.py' 2> /dev/null)
|
||||||
|
|
||||||
$VS_MASTER && command -v npx > /dev/null 2>&1 && [ -f "$ADDON/CHANGELOG.md" ] && {
|
$VS_MASTER && command -v npx > /dev/null 2>&1 && [ -f "$ADDON/CHANGELOG.md" ] && {
|
||||||
@@ -119,8 +96,7 @@ if git diff --name-only origin/master...HEAD 2> /dev/null | grep -Fxq "$ADDON/CH
|
|||||||
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"
|
note "CHANGELOG" "NOT UPDATED for $ADDON — CI hard-gates this"; fail=1
|
||||||
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"
|
||||||
@@ -134,8 +110,7 @@ note "docker build" "NOT tested locally (dockerd unavailable) — CI is the only
|
|||||||
if $VS_MASTER; then
|
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)
|
tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
|
||||||
trap 'rm -rf "$tmp"' EXIT
|
|
||||||
added=0
|
added=0
|
||||||
# Fed by process substitution, not a pipe: a pipeline runs this in a subshell, where the
|
# Fed by process substitution, not a pipe: a pipeline runs this in a subshell, where the
|
||||||
# findings below could never reach $fail and the verdict would contradict the list.
|
# findings below could never reach $fail and the verdict would contradict the list.
|
||||||
@@ -150,26 +125,14 @@ if $VS_MASTER; then
|
|||||||
# cancel it out and report a false clean.
|
# cancel it out and report a false clean.
|
||||||
case "$f" in
|
case "$f" in
|
||||||
*.sh | *autostart | */run | */finish)
|
*.sh | *autostart | */run | */finish)
|
||||||
command -v shellcheck > /dev/null 2>&1 || {
|
command -v shellcheck > /dev/null 2>&1 || { echo " $f: SKIPPED (shellcheck not installed)"; continue; }
|
||||||
echo " $f: SKIPPED (shellcheck not installed)"
|
cmd() { shellcheck -x -f gcc "$1" 2>&1 | sed 's/^[^:]*:[0-9]*:[0-9]*://'; } ;;
|
||||||
continue
|
|
||||||
}
|
|
||||||
cmd() { shellcheck -x -f gcc "$1" 2>&1 | sed 's/^[^:]*:[0-9]*:[0-9]*://'; }
|
|
||||||
;;
|
|
||||||
*.yaml | *.yml)
|
*.yaml | *.yml)
|
||||||
command -v yamllint > /dev/null 2>&1 || {
|
command -v yamllint > /dev/null 2>&1 || { echo " $f: SKIPPED (yamllint not installed)"; continue; }
|
||||||
echo " $f: SKIPPED (yamllint not installed)"
|
cmd() { yamllint -f parsable "$1" 2>&1 | sed 's/^[^:]*//; s/^:[0-9]*:[0-9]*//'; } ;;
|
||||||
continue
|
|
||||||
}
|
|
||||||
cmd() { yamllint -f parsable "$1" 2>&1 | sed 's/^[^:]*//; s/^:[0-9]*:[0-9]*//'; }
|
|
||||||
;;
|
|
||||||
*Dockerfile)
|
*Dockerfile)
|
||||||
command -v hadolint > /dev/null 2>&1 || {
|
command -v hadolint > /dev/null 2>&1 || { echo " $f: SKIPPED (hadolint not installed)"; continue; }
|
||||||
echo " $f: SKIPPED (hadolint not installed)"
|
cmd() { hadolint "$1" 2>&1 | sed 's/^[^:]*//; s/^:[0-9]*//'; } ;;
|
||||||
continue
|
|
||||||
}
|
|
||||||
cmd() { hadolint "$1" 2>&1 | sed 's/^[^:]*//; s/^:[0-9]*//'; }
|
|
||||||
;;
|
|
||||||
*) continue ;;
|
*) continue ;;
|
||||||
esac
|
esac
|
||||||
b=$(cmd "$tmp/base" | sort)
|
b=$(cmd "$tmp/base" | sort)
|
||||||
@@ -178,8 +141,7 @@ if $VS_MASTER; then
|
|||||||
[ -n "$new" ] && {
|
[ -n "$new" ] && {
|
||||||
echo " $f: $(printf '%s\n' "$new" | grep -c .) NEW finding(s)"
|
echo " $f: $(printf '%s\n' "$new" | grep -c .) NEW finding(s)"
|
||||||
printf '%s\n' "$new" | sed 's/^/ /' | head -5
|
printf '%s\n' "$new" | sed 's/^/ /' | head -5
|
||||||
added=1
|
added=1; fail=1
|
||||||
fail=1
|
|
||||||
}
|
}
|
||||||
done < <(git diff --name-only origin/master...HEAD -- "$ADDON" 2> /dev/null)
|
done < <(git diff --name-only origin/master...HEAD -- "$ADDON" 2> /dev/null)
|
||||||
[ "$added" -eq 0 ] && echo " (none — this diff introduced no new lint findings)"
|
[ "$added" -eq 0 ] && echo " (none — this diff introduced no new lint findings)"
|
||||||
|
|||||||
2
.github/prompts/issue-classify.md
vendored
@@ -40,7 +40,7 @@ the check is a guard, not a common case.)
|
|||||||
## Rule 1 — pick exactly one verdict
|
## Rule 1 — pick exactly one verdict
|
||||||
|
|
||||||
| verdict | when |
|
| verdict | when |
|
||||||
| --- | --- |
|
|---|---|
|
||||||
| `duplicate` | An existing open or closed issue reports the same thing. Set `duplicate_of`. |
|
| `duplicate` | An existing open or closed issue reports the same thing. Set `duplicate_of`. |
|
||||||
| `needs-info` | You cannot tell what is wrong without the add-on version, HA version, architecture, config, or the actual log output. |
|
| `needs-info` | You cannot tell what is wrong without the add-on version, HA version, architecture, config, or the actual log output. |
|
||||||
| `question` | A usage question answerable from `DOCS.md`, the wiki, or the add-on config. Not a defect. |
|
| `question` | A usage question answerable from `DOCS.md`, the wiki, or the add-on config. Not a defect. |
|
||||||
|
|||||||
2
.github/prompts/issue-fix.md
vendored
@@ -39,7 +39,7 @@ rest are on you: nothing checks them, so breaking one ships silently.
|
|||||||
that value, and compare:
|
that value, and compare:
|
||||||
|
|
||||||
| `version` vs `U` | what to do | example |
|
| `version` vs `U` | what to do | example |
|
||||||
| --- | --- | --- |
|
|---|---|---|
|
||||||
| identical | **append** `.1` | sonarr `4.0.19.3001` -> `4.0.19.3001.1` |
|
| identical | **append** `.1` | sonarr `4.0.19.3001` -> `4.0.19.3001.1` |
|
||||||
| `U` + `.` + digits | **increment** those digits | radarr `6.3.0.10514.1` -> `6.3.0.10514.2` |
|
| `U` + `.` + digits | **increment** those digits | radarr `6.3.0.10514.1` -> `6.3.0.10514.2` |
|
||||||
| anything else | **leave it alone** | plex, readarr, joal |
|
| anything else | **leave it alone** | plex, readarr, joal |
|
||||||
|
|||||||
2
.github/scripts/ai_guard_paths.sh
vendored
@@ -44,7 +44,7 @@ for pr in $PRS; do
|
|||||||
echo "$BAD"
|
echo "$BAD"
|
||||||
# Ensure the label exists before adding it — with set -e a missing
|
# Ensure the label exists before adding it — with set -e a missing
|
||||||
# label would abort the whole loop and skip any PRs behind this one.
|
# label would abort the whole loop and skip any PRs behind this one.
|
||||||
gh label create "ai:blocked" --repo "$REPO" --color ededed > /dev/null 2>&1 || true
|
gh label create "ai:blocked" --repo "$REPO" --color ededed >/dev/null 2>&1 || true
|
||||||
gh pr ready "$pr" --repo "$REPO" --undo || true
|
gh pr ready "$pr" --repo "$REPO" --undo || true
|
||||||
gh pr edit "$pr" --repo "$REPO" --add-label "ai:blocked"
|
gh pr edit "$pr" --repo "$REPO" --add-label "ai:blocked"
|
||||||
gh pr comment "$pr" --repo "$REPO" --body \
|
gh pr comment "$pr" --repo "$REPO" --body \
|
||||||
|
|||||||
172
.github/scripts/ai_triage_context.sh
vendored
@@ -15,116 +15,110 @@ CTX="$OUT/context.md"
|
|||||||
: > "$CTX"
|
: > "$CTX"
|
||||||
|
|
||||||
gh issue view "$ISSUE_NUMBER" --repo "$REPO" \
|
gh issue view "$ISSUE_NUMBER" --repo "$REPO" \
|
||||||
--json number,title,body,author,labels,createdAt,comments > "$OUT/issue.json"
|
--json number,title,body,author,labels,createdAt,comments > "$OUT/issue.json"
|
||||||
|
|
||||||
TITLE=$(jq -r '.title' "$OUT/issue.json")
|
TITLE=$(jq -r '.title' "$OUT/issue.json")
|
||||||
|
|
||||||
# ---------------------------------------------------------------- addon slug
|
# ---------------------------------------------------------------- addon slug
|
||||||
# Titles follow "🐛 [Immich Frame] ENV_VARS arent being picked up".
|
# Titles follow "🐛 [Immich Frame] ENV_VARS arent being picked up".
|
||||||
RAW=$(sed -n 's/.*\[\([^]]*\)\].*/\1/p' <<< "$TITLE" | head -n1)
|
RAW=$(sed -n 's/.*\[\([^]]*\)\].*/\1/p' <<<"$TITLE" | head -n1)
|
||||||
ADDON=""
|
ADDON=""
|
||||||
if [ -n "$RAW" ]; then
|
if [ -n "$RAW" ]; then
|
||||||
CAND=$(tr '[:upper:] ' '[:lower:]_' <<< "$RAW")
|
CAND=$(tr '[:upper:] ' '[:lower:]_' <<<"$RAW")
|
||||||
# Directory list without checking out any of them.
|
# Directory list without checking out any of them.
|
||||||
git ls-tree -d --name-only HEAD > "$OUT/dirs.txt"
|
git ls-tree -d --name-only HEAD > "$OUT/dirs.txt"
|
||||||
for guess in "$CAND" "${CAND//_/-}" "${CAND//_/.}"; do
|
for guess in "$CAND" "${CAND//_/-}" "${CAND//_/.}"; do
|
||||||
if grep -qxF "$guess" "$OUT/dirs.txt"; then
|
if grep -qxF "$guess" "$OUT/dirs.txt"; then ADDON="$guess"; break; fi
|
||||||
ADDON="$guess"
|
done
|
||||||
break
|
# Separator-insensitive exact match: a title like "[Calibre-web]" (hyphen)
|
||||||
fi
|
# against a directory named calibre_web (underscore) matches neither exact
|
||||||
done
|
# guess above, and would otherwise fall through to the substring fallback
|
||||||
# Separator-insensitive exact match: a title like "[Calibre-web]" (hyphen)
|
# below, which picks the shorter "calibre" instead — the wrong add-on.
|
||||||
# against a directory named calibre_web (underscore) matches neither exact
|
# Stripping -, _, . from both sides before comparing catches this case.
|
||||||
# guess above, and would otherwise fall through to the substring fallback
|
if [ -z "$ADDON" ]; then
|
||||||
# below, which picks the shorter "calibre" instead — the wrong add-on.
|
CAND_STRIPPED=$(tr -d '_.-' <<<"$CAND")
|
||||||
# Stripping -, _, . from both sides before comparing catches this case.
|
while IFS= read -r dir; do
|
||||||
if [ -z "$ADDON" ]; then
|
if [ "$(tr -d '_.-' <<<"$dir")" = "$CAND_STRIPPED" ]; then ADDON="$dir"; break; fi
|
||||||
CAND_STRIPPED=$(tr -d '_.-' <<< "$CAND")
|
done < "$OUT/dirs.txt"
|
||||||
while IFS= read -r dir; do
|
fi
|
||||||
if [ "$(tr -d '_.-' <<< "$dir")" = "$CAND_STRIPPED" ]; then
|
# Last resort: longest directory name contained in the candidate.
|
||||||
ADDON="$dir"
|
if [ -z "$ADDON" ]; then
|
||||||
break
|
ADDON=$(awk -v c="$CAND" 'length($0)>2 && index(c,$0){print length($0)"\t"$0}' \
|
||||||
fi
|
|
||||||
done < "$OUT/dirs.txt"
|
|
||||||
fi
|
|
||||||
# Last resort: longest directory name contained in the candidate.
|
|
||||||
if [ -z "$ADDON" ]; then
|
|
||||||
ADDON=$(awk -v c="$CAND" 'length($0)>2 && index(c,$0){print length($0)"\t"$0}' \
|
|
||||||
"$OUT/dirs.txt" | sort -rn | head -n1 | cut -f2)
|
"$OUT/dirs.txt" | sort -rn | head -n1 | cut -f2)
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
{
|
{
|
||||||
echo "# Issue #${ISSUE_NUMBER}"
|
echo "# Issue #${ISSUE_NUMBER}"
|
||||||
echo
|
echo
|
||||||
echo "Repo: ${REPO}"
|
echo "Repo: ${REPO}"
|
||||||
echo "Addon resolved from title: ${ADDON:-UNRESOLVED}"
|
echo "Addon resolved from title: ${ADDON:-UNRESOLVED}"
|
||||||
echo
|
echo
|
||||||
echo "## Title"
|
echo "## Title"
|
||||||
echo "$TITLE"
|
echo "$TITLE"
|
||||||
echo
|
echo
|
||||||
echo "## Author"
|
echo "## Author"
|
||||||
jq -r '.author.login' "$OUT/issue.json"
|
jq -r '.author.login' "$OUT/issue.json"
|
||||||
echo
|
echo
|
||||||
echo "## Body"
|
echo "## Body"
|
||||||
echo '```'
|
echo '```'
|
||||||
jq -r '.body // "(empty)"' "$OUT/issue.json"
|
jq -r '.body // "(empty)"' "$OUT/issue.json"
|
||||||
echo '```'
|
echo '```'
|
||||||
echo
|
echo
|
||||||
echo "## Existing comments (in order)"
|
echo "## Existing comments (in order)"
|
||||||
jq -r '.comments[]? | "### @\(.author.login)\n\(.body)\n"' "$OUT/issue.json"
|
jq -r '.comments[]? | "### @\(.author.login)\n\(.body)\n"' "$OUT/issue.json"
|
||||||
echo
|
echo
|
||||||
echo "## Existing labels"
|
echo "## Existing labels"
|
||||||
jq -r '[.labels[]?.name] | join(", ")' "$OUT/issue.json"
|
jq -r '[.labels[]?.name] | join(", ")' "$OUT/issue.json"
|
||||||
} >> "$CTX"
|
} >> "$CTX"
|
||||||
|
|
||||||
# ------------------------------------------------------------- addon sources
|
# ------------------------------------------------------------- addon sources
|
||||||
if [ -n "$ADDON" ]; then
|
if [ -n "$ADDON" ]; then
|
||||||
{
|
{
|
||||||
|
echo
|
||||||
|
echo "## Addon files: ${ADDON}/"
|
||||||
|
# `set` REPLACES the checkout list, so .templates has to be repeated here
|
||||||
|
# or the workflow's sparse-checkout of it is silently undone at this point
|
||||||
|
# — which is exactly the state that starved #2949 of its turn budget.
|
||||||
|
if ! git sparse-checkout set --no-cone .github/prompts .github/scripts .templates "$ADDON" 2>&1; then
|
||||||
|
# Swallowing this used to leave ADDON resolved with no files behind it,
|
||||||
|
# so the classifier could still reach high confidence off the addon
|
||||||
|
# name alone. Say so explicitly, in the same word Rule 2 already keys
|
||||||
|
# its low-confidence check on.
|
||||||
|
echo
|
||||||
|
echo "**Could not check out this add-on's source. Treat as UNRESOLVED for confidence purposes.**"
|
||||||
|
else
|
||||||
|
for f in config.yaml config.json Dockerfile CHANGELOG.md DOCS.md README.md; do
|
||||||
|
[ -f "$ADDON/$f" ] || continue
|
||||||
echo
|
echo
|
||||||
echo "## Addon files: ${ADDON}/"
|
echo "### ${ADDON}/${f}"
|
||||||
# `set` REPLACES the checkout list, so .templates has to be repeated here
|
echo '```'
|
||||||
# or the workflow's sparse-checkout of it is silently undone at this point
|
head -c 8000 "$ADDON/$f"
|
||||||
# — which is exactly the state that starved #2949 of its turn budget.
|
echo '```'
|
||||||
if ! git sparse-checkout set --no-cone .github/prompts .github/scripts .templates "$ADDON" 2>&1; then
|
done
|
||||||
# Swallowing this used to leave ADDON resolved with no files behind it,
|
|
||||||
# so the classifier could still reach high confidence off the addon
|
|
||||||
# name alone. Say so explicitly, in the same word Rule 2 already keys
|
|
||||||
# its low-confidence check on.
|
|
||||||
echo
|
|
||||||
echo "**Could not check out this add-on's source. Treat as UNRESOLVED for confidence purposes.**"
|
|
||||||
else
|
|
||||||
for f in config.yaml config.json Dockerfile CHANGELOG.md DOCS.md README.md; do
|
|
||||||
[ -f "$ADDON/$f" ] || continue
|
|
||||||
echo
|
|
||||||
echo "### ${ADDON}/${f}"
|
|
||||||
echo '```'
|
|
||||||
head -c 8000 "$ADDON/$f"
|
|
||||||
echo '```'
|
|
||||||
done
|
|
||||||
|
|
||||||
echo
|
echo
|
||||||
echo "## Recent commits touching ${ADDON}/"
|
echo "## Recent commits touching ${ADDON}/"
|
||||||
git log -n 15 --date=short --pretty='- %ad %h %s' -- "$ADDON" 2> /dev/null || true
|
git log -n 15 --date=short --pretty='- %ad %h %s' -- "$ADDON" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
} >> "$CTX"
|
} >> "$CTX"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# -------------------------------------------------------- possible duplicates
|
# -------------------------------------------------------- possible duplicates
|
||||||
{
|
{
|
||||||
echo
|
echo
|
||||||
echo "## Similar existing issues (candidate duplicates)"
|
echo "## Similar existing issues (candidate duplicates)"
|
||||||
KEYWORDS=$(tr -cs '[:alnum:]' ' ' <<< "$TITLE" \
|
KEYWORDS=$(tr -cs '[:alnum:]' ' ' <<<"$TITLE" \
|
||||||
| tr '[:upper:]' '[:lower:]' \
|
| tr '[:upper:]' '[:lower:]' \
|
||||||
| tr ' ' '\n' | awk 'length($0)>3' | head -n6 | paste -sd' ')
|
| tr ' ' '\n' | awk 'length($0)>3' | head -n6 | paste -sd' ')
|
||||||
# Excludes the issue being triaged: if it's already indexed by GitHub search
|
# Excludes the issue being triaged: if it's already indexed by GitHub search
|
||||||
# by the time this runs, keyword overlap with its own title would otherwise
|
# by the time this runs, keyword overlap with its own title would otherwise
|
||||||
# list it as a "candidate duplicate" of itself.
|
# list it as a "candidate duplicate" of itself.
|
||||||
gh search issues --repo "$REPO" --limit 15 \
|
gh search issues --repo "$REPO" --limit 15 \
|
||||||
--json number,title,state,url -- "$KEYWORDS" 2> /dev/null \
|
--json number,title,state,url -- "$KEYWORDS" 2>/dev/null \
|
||||||
| jq -r --argjson self "$ISSUE_NUMBER" \
|
| jq -r --argjson self "$ISSUE_NUMBER" \
|
||||||
'.[] | select(.number != $self) | "- #\(.number) [\(.state)] \(.title)"' \
|
'.[] | select(.number != $self) | "- #\(.number) [\(.state)] \(.title)"' \
|
||||||
|| echo "(search unavailable)"
|
|| echo "(search unavailable)"
|
||||||
} >> "$CTX"
|
} >> "$CTX"
|
||||||
|
|
||||||
echo "context bundle: $(wc -c < "$CTX") bytes, addon=${ADDON:-none}"
|
echo "context bundle: $(wc -c < "$CTX") bytes, addon=${ADDON:-none}"
|
||||||
|
|||||||
10
.github/stargazer_countries.csv
vendored
@@ -250,6 +250,7 @@ F4bsi,Germany,
|
|||||||
Fabaderheld,Austria,
|
Fabaderheld,Austria,
|
||||||
Fabeuss,France,
|
Fabeuss,France,
|
||||||
Fabian-Eigenherd,Germany,
|
Fabian-Eigenherd,Germany,
|
||||||
|
Fabiot9,,2026-09-13
|
||||||
Faithfinder,Canada,
|
Faithfinder,Canada,
|
||||||
FallsSteven,,2026-08-10
|
FallsSteven,,2026-08-10
|
||||||
Felitendo,Germany,
|
Felitendo,Germany,
|
||||||
@@ -471,6 +472,7 @@ MNDani,,2026-08-10
|
|||||||
MTHarden,United States,
|
MTHarden,United States,
|
||||||
MYLE-01,New Zealand,
|
MYLE-01,New Zealand,
|
||||||
MaBeniu,,2026-08-10
|
MaBeniu,,2026-08-10
|
||||||
|
Maboroshi0327,,2026-09-13
|
||||||
MachinaDev,,2026-08-10
|
MachinaDev,,2026-08-10
|
||||||
Madelena,United States,
|
Madelena,United States,
|
||||||
MagicMau,,2026-08-10
|
MagicMau,,2026-08-10
|
||||||
@@ -515,6 +517,7 @@ Mofx-01,,2026-08-10
|
|||||||
Mokdeze,,2026-08-10
|
Mokdeze,,2026-08-10
|
||||||
MondoGao,United States,
|
MondoGao,United States,
|
||||||
Moosh-be,Belgium,
|
Moosh-be,Belgium,
|
||||||
|
Moozzuzz,,2026-09-13
|
||||||
MrAct1on,United States,
|
MrAct1on,United States,
|
||||||
MrCraigen,,2026-08-10
|
MrCraigen,,2026-08-10
|
||||||
MrHacky,,2026-08-10
|
MrHacky,,2026-08-10
|
||||||
@@ -952,6 +955,7 @@ austinmccalley,,2026-08-10
|
|||||||
autodeck,United Kingdom,
|
autodeck,United Kingdom,
|
||||||
aviadlevy,,2026-08-10
|
aviadlevy,,2026-08-10
|
||||||
aviv926,,2026-08-10
|
aviv926,,2026-08-10
|
||||||
|
axelburks,United States,2026-09-13
|
||||||
axute,Germany,
|
axute,Germany,
|
||||||
azerioxal,United States,
|
azerioxal,United States,
|
||||||
azure1922,,2026-08-10
|
azure1922,,2026-08-10
|
||||||
@@ -1043,6 +1047,7 @@ brettmiller,United States,
|
|||||||
brewston,,2026-08-10
|
brewston,,2026-08-10
|
||||||
brianalexander,,2026-08-10
|
brianalexander,,2026-08-10
|
||||||
brianm71,,2026-08-10
|
brianm71,,2026-08-10
|
||||||
|
brietman,,2026-09-13
|
||||||
brittek,Australia,
|
brittek,Australia,
|
||||||
brokec,,2026-08-10
|
brokec,,2026-08-10
|
||||||
brunerm99,,2026-08-10
|
brunerm99,,2026-08-10
|
||||||
@@ -1451,6 +1456,7 @@ gh-residante,,2026-08-10
|
|||||||
ghisch,,2026-08-10
|
ghisch,,2026-08-10
|
||||||
gieljnssns,Belgium,
|
gieljnssns,Belgium,
|
||||||
gigatexel,,2026-08-10
|
gigatexel,,2026-08-10
|
||||||
|
gilberth,Peru,2026-09-13
|
||||||
gileswells,United States,
|
gileswells,United States,
|
||||||
giotec,,2026-08-10
|
giotec,,2026-08-10
|
||||||
git-mdumont,,
|
git-mdumont,,
|
||||||
@@ -1771,6 +1777,7 @@ ldec,,2026-08-10
|
|||||||
ldkdev,Viet Nam,
|
ldkdev,Viet Nam,
|
||||||
le-pet,,2026-08-10
|
le-pet,,2026-08-10
|
||||||
lead0r,,2026-08-10
|
lead0r,,2026-08-10
|
||||||
|
leaskovski,,2026-09-13
|
||||||
legovaer,Belgium,
|
legovaer,Belgium,
|
||||||
legusha,,2026-08-10
|
legusha,,2026-08-10
|
||||||
lehnerpat,,2026-08-10
|
lehnerpat,,2026-08-10
|
||||||
@@ -2001,6 +2008,8 @@ najdias,Portugal,
|
|||||||
nalditopr,,2026-08-10
|
nalditopr,,2026-08-10
|
||||||
nana4rider,Japan,
|
nana4rider,Japan,
|
||||||
naor1,Israel,
|
naor1,Israel,
|
||||||
|
narmod,,2026-09-13
|
||||||
|
nascentt,,2026-09-13
|
||||||
natbrood,,2026-08-10
|
natbrood,,2026-08-10
|
||||||
nate-marshall,,2026-08-10
|
nate-marshall,,2026-08-10
|
||||||
nduest,Germany,
|
nduest,Germany,
|
||||||
@@ -2583,6 +2592,7 @@ w10devheh,,2026-08-10
|
|||||||
waako,,2026-08-10
|
waako,,2026-08-10
|
||||||
walduino,,2026-08-10
|
walduino,,2026-08-10
|
||||||
wangjiezhe,China,
|
wangjiezhe,China,
|
||||||
|
wannes-wagemans,Belgium,2026-09-13
|
||||||
warbringer,,2026-08-10
|
warbringer,,2026-08-10
|
||||||
wardbeyens,Belgium,
|
wardbeyens,Belgium,
|
||||||
warkurre86,,2026-08-10
|
warkurre86,,2026-08-10
|
||||||
|
|||||||
|
BIN
.github/stargazer_map.png
vendored
|
Before Width: | Height: | Size: 66 KiB After Width: | Height: | Size: 67 KiB |
BIN
.github/stats.png
vendored
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 1.9 KiB |
BIN
.github/stats_addons.png
vendored
|
Before Width: | Height: | Size: 10 KiB After Width: | Height: | Size: 4.4 KiB |
2
.github/workflows/daily_ai_fix.yaml
vendored
@@ -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@d75b94d5ad426cb8546e6628b6f5f19b84e5cce1 # v1
|
uses: anthropics/claude-code-action@56cf60fde42f7b19c3abfd5c9c48b69a1288461f # 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
|
||||||
|
|||||||
2
.github/workflows/lint.yml
vendored
@@ -7,8 +7,6 @@ on:
|
|||||||
- "**/config.*"
|
- "**/config.*"
|
||||||
pull_request:
|
pull_request:
|
||||||
branches: [ master ]
|
branches: [ master ]
|
||||||
schedule:
|
|
||||||
- cron: "0 0 * * 0"
|
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
2
.github/workflows/on_claude_mention.yml
vendored
@@ -64,7 +64,7 @@ jobs:
|
|||||||
fetch-depth: 1
|
fetch-depth: 1
|
||||||
|
|
||||||
- name: Run Claude Code
|
- name: Run Claude Code
|
||||||
uses: anthropics/claude-code-action@d75b94d5ad426cb8546e6628b6f5f19b84e5cce1 # v1
|
uses: anthropics/claude-code-action@56cf60fde42f7b19c3abfd5c9c48b69a1288461f # 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.
|
||||||
|
|||||||
2
.github/workflows/on_issue_approved.yaml
vendored
@@ -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@d75b94d5ad426cb8546e6628b6f5f19b84e5cce1 # v1
|
uses: anthropics/claude-code-action@56cf60fde42f7b19c3abfd5c9c48b69a1288461f # 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
|
||||||
|
|||||||
2
.github/workflows/on_issues_ai_triage.yaml
vendored
@@ -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@d75b94d5ad426cb8546e6628b6f5f19b84e5cce1 # v1
|
uses: anthropics/claude-code-action@56cf60fde42f7b19c3abfd5c9c48b69a1288461f # 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
|
||||||
|
|||||||
2
.github/workflows/on_pr_coderabbit.yml
vendored
@@ -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@d75b94d5ad426cb8546e6628b6f5f19b84e5cce1 # v1
|
uses: anthropics/claude-code-action@56cf60fde42f7b19c3abfd5c9c48b69a1288461f # 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
|
||||||
|
|||||||
4
.github/workflows/onpush_builder.yaml
vendored
@@ -306,7 +306,7 @@ jobs:
|
|||||||
id: build
|
id: build
|
||||||
if: steps.info.outputs.build_arch == 'true' && steps.info.outputs.has_dockerfile == 'true'
|
if: steps.info.outputs.build_arch == 'true' && steps.info.outputs.has_dockerfile == 'true'
|
||||||
continue-on-error: true
|
continue-on-error: true
|
||||||
uses: home-assistant/builder/actions/build-image@2026.06.0
|
uses: home-assistant/builder/actions/build-image@2026.09.0
|
||||||
with:
|
with:
|
||||||
arch: ${{ matrix.arch }}
|
arch: ${{ matrix.arch }}
|
||||||
cache-gha: "false"
|
cache-gha: "false"
|
||||||
@@ -331,7 +331,7 @@ jobs:
|
|||||||
# else for the retry to be skipped from.
|
# else for the retry to be skipped from.
|
||||||
- name: Build ${{ matrix.addon }} add-on (retry after failed attempt)
|
- name: Build ${{ matrix.addon }} add-on (retry after failed attempt)
|
||||||
if: steps.build.outcome == 'failure'
|
if: steps.build.outcome == 'failure'
|
||||||
uses: home-assistant/builder/actions/build-image@2026.06.0
|
uses: home-assistant/builder/actions/build-image@2026.09.0
|
||||||
with:
|
with:
|
||||||
arch: ${{ matrix.arch }}
|
arch: ${{ matrix.arch }}
|
||||||
cache-gha: "false"
|
cache-gha: "false"
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ set -e
|
|||||||
# Banner
|
# Banner
|
||||||
# ======================================================================
|
# ======================================================================
|
||||||
|
|
||||||
if ! bashio::supervisor.ping 2> /dev/null; then
|
if ! bashio::supervisor.ping 2>/dev/null; then
|
||||||
# Source standalone bashio first to provide function definitions
|
# Source standalone bashio first to provide function definitions
|
||||||
if [ -f /usr/local/lib/bashio-standalone.sh ]; then
|
if [ -f /usr/local/lib/bashio-standalone.sh ]; then
|
||||||
source /usr/local/lib/bashio-standalone.sh
|
source /usr/local/lib/bashio-standalone.sh
|
||||||
@@ -19,11 +19,11 @@ if ! bashio::supervisor.ping 2> /dev/null; then
|
|||||||
if [ ! -f /usr/bin/bashio ]; then
|
if [ ! -f /usr/bin/bashio ]; then
|
||||||
cp -rf /usr/local/lib/bashio-standalone.sh /usr/bin/bashio
|
cp -rf /usr/local/lib/bashio-standalone.sh /usr/bin/bashio
|
||||||
fi
|
fi
|
||||||
grep -rl "^#!.*bashio" /etc \
|
grep -rl "^#!.*bashio" /etc |
|
||||||
| while IFS= read -r f; do
|
while IFS= read -r f; do
|
||||||
grep -qF "source /usr/local/lib/bashio-standalone.sh" "$f" && continue
|
grep -qF "source /usr/local/lib/bashio-standalone.sh" "$f" && continue
|
||||||
sed -i '1a source /usr/local/lib/bashio-standalone.sh' "$f"
|
sed -i '1a source /usr/local/lib/bashio-standalone.sh' "$f"
|
||||||
done
|
done
|
||||||
else
|
else
|
||||||
bashio::log.blue '-----------------------------------------------------------'
|
bashio::log.blue '-----------------------------------------------------------'
|
||||||
bashio::log.blue " Add-on: $(bashio::addon.name)"
|
bashio::log.blue " Add-on: $(bashio::addon.name)"
|
||||||
@@ -49,7 +49,7 @@ bashio::log.green ' Provided by: https://github.com/alexbelgium/hassio-addons '
|
|||||||
bashio::log.blue '-----------------------------------------------------------'
|
bashio::log.blue '-----------------------------------------------------------'
|
||||||
|
|
||||||
# Adapt user abc
|
# Adapt user abc
|
||||||
if command -v id &> /dev/null && id abc &> /dev/null; then
|
if command -v id &>/dev/null && id abc &>/dev/null; then
|
||||||
if bashio::config.has_value "PUID" && bashio::config.has_value "PGID"; then
|
if bashio::config.has_value "PUID" && bashio::config.has_value "PGID"; then
|
||||||
PUID="$(bashio::config "PUID")"
|
PUID="$(bashio::config "PUID")"
|
||||||
PGID="$(bashio::config "PGID")"
|
PGID="$(bashio::config "PGID")"
|
||||||
|
|||||||
@@ -152,15 +152,15 @@ if [[ "${1:-}" == "--self-test" ]]; then
|
|||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[[ "$self_test_rc" -eq 0 ]] \
|
[[ "$self_test_rc" -eq 0 ]] &&
|
||||||
&& echo "${#self_test_values[@]} values round-tripped unchanged (export block + dotenv)"
|
echo "${#self_test_values[@]} values round-tripped unchanged (export block + dotenv)"
|
||||||
exit "$self_test_rc"
|
exit "$self_test_rc"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
################################################################################
|
################################################################################
|
||||||
# Guard: only run inside Supervisor-managed add-ons
|
# Guard: only run inside Supervisor-managed add-ons
|
||||||
################################################################################
|
################################################################################
|
||||||
if ! bashio::supervisor.ping 2> /dev/null; then
|
if ! bashio::supervisor.ping 2>/dev/null; then
|
||||||
echo "..."
|
echo "..."
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
@@ -179,7 +179,7 @@ ENV_FILE="/.env"
|
|||||||
ETC_ENV_FILE="/etc/environment"
|
ETC_ENV_FILE="/etc/environment"
|
||||||
|
|
||||||
[[ -f "$JSONSOURCE" ]] || bashio::exit.nok "Missing $JSONSOURCE"
|
[[ -f "$JSONSOURCE" ]] || bashio::exit.nok "Missing $JSONSOURCE"
|
||||||
command -v jq > /dev/null || bashio::exit.nok "jq is required"
|
command -v jq >/dev/null || bashio::exit.nok "jq is required"
|
||||||
|
|
||||||
mkdir -p /etc
|
mkdir -p /etc
|
||||||
touch "$ETC_ENV_FILE"
|
touch "$ETC_ENV_FILE"
|
||||||
@@ -193,10 +193,7 @@ SECRETSOURCE=""
|
|||||||
|
|
||||||
resolve_secret() {
|
resolve_secret() {
|
||||||
local v="$1" name line
|
local v="$1" name line
|
||||||
[[ "$v" =~ ^[[:space:]]*\!secret[[:space:]]+(.+)$ ]] || {
|
[[ "$v" =~ ^[[:space:]]*\!secret[[:space:]]+(.+)$ ]] || { printf '%s' "$v"; return; }
|
||||||
printf '%s' "$v"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
name="${BASH_REMATCH[1]}"
|
name="${BASH_REMATCH[1]}"
|
||||||
[[ -n "$SECRETSOURCE" ]] || bashio::exit.nok "Secrets not mounted"
|
[[ -n "$SECRETSOURCE" ]] || bashio::exit.nok "Secrets not mounted"
|
||||||
|
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
|
|
||||||
set -e
|
set -e
|
||||||
|
|
||||||
if ! bashio::supervisor.ping 2> /dev/null; then
|
if ! bashio::supervisor.ping 2>/dev/null; then
|
||||||
bashio::log.blue "Disabled : please use another method"
|
bashio::log.blue "Disabled : please use another method"
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
bashio::log.notice "This script is used to mount remote smb/cifs/nfs shares. Instructions here : https://github.com/alexbelgium/hassio-addons/wiki/Mounting-remote-shares-in-Addons"
|
bashio::log.notice "This script is used to mount remote smb/cifs/nfs shares. Instructions here : https://github.com/alexbelgium/hassio-addons/wiki/Mounting-remote-shares-in-Addons"
|
||||||
@@ -16,167 +16,167 @@ bashio::log.notice "This script is used to mount remote smb/cifs/nfs shares. Ins
|
|||||||
####################
|
####################
|
||||||
|
|
||||||
cleanup_cred() {
|
cleanup_cred() {
|
||||||
if [[ -n "${CRED_FILE:-}" && -f "${CRED_FILE:-}" ]]; then
|
if [[ -n "${CRED_FILE:-}" && -f "${CRED_FILE:-}" ]]; then
|
||||||
rm -f "$CRED_FILE" || true
|
rm -f "$CRED_FILE" || true
|
||||||
fi
|
fi
|
||||||
CRED_FILE=""
|
CRED_FILE=""
|
||||||
}
|
}
|
||||||
|
|
||||||
test_mount() {
|
test_mount() {
|
||||||
MOUNTED=false
|
MOUNTED=false
|
||||||
ERROR_MOUNT=false
|
ERROR_MOUNT=false
|
||||||
mountpoint="/mnt/$diskname"
|
mountpoint="/mnt/$diskname"
|
||||||
|
|
||||||
if ! mountpoint -q "$mountpoint"; then
|
if ! mountpoint -q "$mountpoint"; then
|
||||||
return 0
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
_test_write() {
|
||||||
|
local testfile="$mountpoint/.writetest_$$"
|
||||||
|
if : >"$testfile" 2>/dev/null; then
|
||||||
|
rm -f "$testfile" 2>/dev/null || true
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
rm -f "$testfile" 2>/dev/null || true
|
||||||
|
return 1
|
||||||
fi
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
_test_write() {
|
if _test_write 2>/dev/null; then
|
||||||
local testfile="$mountpoint/.writetest_$$"
|
MOUNTED=true
|
||||||
if : > "$testfile" 2> /dev/null; then
|
return 0
|
||||||
rm -f "$testfile" 2> /dev/null || true
|
fi
|
||||||
return 0
|
|
||||||
else
|
|
||||||
rm -f "$testfile" 2> /dev/null || true
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
}
|
|
||||||
|
|
||||||
if _test_write 2> /dev/null; then
|
local orig_mountoptions="$MOUNTOPTIONS"
|
||||||
|
|
||||||
|
if [[ "$FSTYPE" == "cifs" && "$orig_mountoptions" != *"noserverino"* ]]; then
|
||||||
|
echo "... retrying mount with noserverino"
|
||||||
|
MOUNTOPTIONS="${orig_mountoptions},noserverino"
|
||||||
|
|
||||||
|
umount "$mountpoint" 2>/dev/null || true
|
||||||
|
if mount_drive "$MOUNTOPTIONS"; then
|
||||||
|
if _test_write 2>/dev/null; then
|
||||||
MOUNTED=true
|
MOUNTED=true
|
||||||
return 0
|
return 0
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
local orig_mountoptions="$MOUNTOPTIONS"
|
if [[ "$FSTYPE" == "cifs" && "$orig_mountoptions" != *"noperm"* ]]; then
|
||||||
|
echo "... retrying mount with noperm"
|
||||||
|
MOUNTOPTIONS="${orig_mountoptions},noperm"
|
||||||
|
|
||||||
if [[ "$FSTYPE" == "cifs" && "$orig_mountoptions" != *"noserverino"* ]]; then
|
umount "$mountpoint" 2>/dev/null || true
|
||||||
echo "... retrying mount with noserverino"
|
if mount_drive "$MOUNTOPTIONS"; then
|
||||||
MOUNTOPTIONS="${orig_mountoptions},noserverino"
|
if _test_write 2>/dev/null; then
|
||||||
|
MOUNTED=true
|
||||||
umount "$mountpoint" 2> /dev/null || true
|
return 0
|
||||||
if mount_drive "$MOUNTOPTIONS"; then
|
fi
|
||||||
if _test_write 2> /dev/null; then
|
|
||||||
MOUNTED=true
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "$FSTYPE" == "cifs" && "$orig_mountoptions" != *"noperm"* ]]; then
|
if mountpoint -q "$mountpoint"; then
|
||||||
echo "... retrying mount with noperm"
|
MOUNTED="readonly"
|
||||||
MOUNTOPTIONS="${orig_mountoptions},noperm"
|
else
|
||||||
|
MOUNTED=false
|
||||||
umount "$mountpoint" 2> /dev/null || true
|
fi
|
||||||
if mount_drive "$MOUNTOPTIONS"; then
|
return 0
|
||||||
if _test_write 2> /dev/null; then
|
|
||||||
MOUNTED=true
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
if mountpoint -q "$mountpoint"; then
|
|
||||||
MOUNTED="readonly"
|
|
||||||
else
|
|
||||||
MOUNTED=false
|
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
}
|
}
|
||||||
|
|
||||||
mount_drive() {
|
mount_drive() {
|
||||||
MOUNTED=true
|
MOUNTED=true
|
||||||
MOUNTOPTIONS="$1"
|
MOUNTOPTIONS="$1"
|
||||||
|
|
||||||
if [[ "$FSTYPE" == "cifs" ]]; then
|
if [[ "$FSTYPE" == "cifs" ]]; then
|
||||||
mount -t cifs -o "$MOUNTOPTIONS" "$disk" "/mnt/$diskname" 2> "$ERRORCODE_FILE" || MOUNTED=false
|
mount -t cifs -o "$MOUNTOPTIONS" "$disk" "/mnt/$diskname" 2>"$ERRORCODE_FILE" || MOUNTED=false
|
||||||
elif [[ "$FSTYPE" == "nfs" ]]; then
|
elif [[ "$FSTYPE" == "nfs" ]]; then
|
||||||
mount -t nfs -o "$MOUNTOPTIONS" "$disk" "/mnt/$diskname" 2> "$ERRORCODE_FILE" || MOUNTED=false
|
mount -t nfs -o "$MOUNTOPTIONS" "$disk" "/mnt/$diskname" 2>"$ERRORCODE_FILE" || MOUNTED=false
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$MOUNTED" == "true" ]]; then
|
if [[ "$MOUNTED" == "true" ]]; then
|
||||||
test_mount
|
test_mount
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Retry ladder: SMB3 -> SMB2 when mount fails due to dialect/negotiation issues
|
# Retry ladder: SMB3 -> SMB2 when mount fails due to dialect/negotiation issues
|
||||||
retry_cifs_with_vers_ladder_on_dialect_failure() {
|
retry_cifs_with_vers_ladder_on_dialect_failure() {
|
||||||
[[ "${FSTYPE:-}" == "cifs" ]] || return 0
|
[[ "${FSTYPE:-}" == "cifs" ]] || return 0
|
||||||
[[ "${MOUNTED:-false}" == "false" ]] || return 0
|
[[ "${MOUNTED:-false}" == "false" ]] || return 0
|
||||||
[[ "${CIFS_LADDER_ATTEMPTED:-false}" == "false" ]] || return 0
|
[[ "${CIFS_LADDER_ATTEMPTED:-false}" == "false" ]] || return 0
|
||||||
|
|
||||||
local err mountpoint
|
local err mountpoint
|
||||||
mountpoint="/mnt/$diskname"
|
mountpoint="/mnt/$diskname"
|
||||||
err="$(cat "$ERRORCODE_FILE" 2> /dev/null || true)"
|
err="$(cat "$ERRORCODE_FILE" 2>/dev/null || true)"
|
||||||
|
|
||||||
if echo "$err" | grep -Eq 'mount error\(13\)|Permission denied|NT_STATUS_(LOGON_FAILURE|ACCESS_DENIED)|STATUS_(LOGON_FAILURE|ACCESS_DENIED)'; then
|
if echo "$err" | grep -Eq 'mount error\(13\)|Permission denied|NT_STATUS_(LOGON_FAILURE|ACCESS_DENIED)|STATUS_(LOGON_FAILURE|ACCESS_DENIED)'; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! echo "$err" | grep -Eq 'mount error\(22\)|mount error\(95\)|mount error\(112\)|Server abruptly closed the connection|does not support the SMB version|Protocol negotiation|NT_STATUS_CONNECTION_DISCONNECTED'; then
|
if ! echo "$err" | grep -Eq 'mount error\(22\)|mount error\(95\)|mount error\(112\)|Server abruptly closed the connection|does not support the SMB version|Protocol negotiation|NT_STATUS_CONNECTION_DISCONNECTED'; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
CIFS_LADDER_ATTEMPTED=true
|
CIFS_LADDER_ATTEMPTED=true
|
||||||
bashio::log.warning "...... CIFS negotiation/dialect failure: trying SMB dialect ladder (3.x -> 2.x -> 1.0)."
|
bashio::log.warning "...... CIFS negotiation/dialect failure: trying SMB dialect ladder (3.x -> 2.x -> 1.0)."
|
||||||
|
|
||||||
local base_opts try_opts vers vopt sectry
|
local base_opts try_opts vers vopt sectry
|
||||||
|
|
||||||
|
base_opts="$MOUNTOPTIONS"
|
||||||
|
base_opts="$(echo "$base_opts" | sed -E 's/,vers=[^,]+//g; s/,sec=[^,]+//g')"
|
||||||
|
|
||||||
|
local -a opt_variants=("" ",nounix" ",noserverino" ",nounix,noserverino")
|
||||||
|
local -a sec_variants=("" ",sec=ntlmssp" ",sec=ntlmv2" ",sec=ntlm")
|
||||||
|
local -a vers_variants=("3.1.1" "3.02" "3.0" "2.1" "2.0" "1.0")
|
||||||
|
|
||||||
|
for vopt in "${opt_variants[@]}"; do
|
||||||
|
for vers in "${vers_variants[@]}"; do
|
||||||
|
for sectry in "${sec_variants[@]}"; do
|
||||||
|
[[ "$MOUNTED" == "false" ]] || break
|
||||||
|
umount "$mountpoint" 2>/dev/null || true
|
||||||
|
try_opts="${base_opts}${vopt},vers=${vers}${sectry}"
|
||||||
|
mount_drive "$try_opts"
|
||||||
|
done
|
||||||
|
[[ "$MOUNTED" == "false" ]] || break
|
||||||
|
done
|
||||||
|
[[ "$MOUNTED" == "false" ]] || break
|
||||||
|
done
|
||||||
|
|
||||||
|
if [[ "$MOUNTED" == "false" ]]; then
|
||||||
|
bashio::log.warning "...... still failing after vers ladder; retrying with reduced CIFS options."
|
||||||
base_opts="$MOUNTOPTIONS"
|
base_opts="$MOUNTOPTIONS"
|
||||||
base_opts="$(echo "$base_opts" | sed -E 's/,vers=[^,]+//g; s/,sec=[^,]+//g')"
|
base_opts="$(echo "$base_opts" | sed -E 's/,vers=[^,]+//g; s/,sec=[^,]+//g')"
|
||||||
|
base_opts="${base_opts//,mfsymlinks/}"
|
||||||
|
base_opts="${base_opts//,nobrl/}"
|
||||||
|
base_opts="$(echo "$base_opts" | sed -E 's/,iocharset=[^,]+//g')"
|
||||||
|
|
||||||
local -a opt_variants=("" ",nounix" ",noserverino" ",nounix,noserverino")
|
local -a vers_variants2=("2.1" "2.0" "1.0")
|
||||||
local -a sec_variants=("" ",sec=ntlmssp" ",sec=ntlmv2" ",sec=ntlm")
|
|
||||||
local -a vers_variants=("3.1.1" "3.02" "3.0" "2.1" "2.0" "1.0")
|
|
||||||
|
|
||||||
for vopt in "${opt_variants[@]}"; do
|
for vopt in "${opt_variants[@]}"; do
|
||||||
for vers in "${vers_variants[@]}"; do
|
for vers in "${vers_variants2[@]}"; do
|
||||||
for sectry in "${sec_variants[@]}"; do
|
for sectry in "${sec_variants[@]}"; do
|
||||||
[[ "$MOUNTED" == "false" ]] || break
|
[[ "$MOUNTED" == "false" ]] || break
|
||||||
umount "$mountpoint" 2> /dev/null || true
|
umount "$mountpoint" 2>/dev/null || true
|
||||||
try_opts="${base_opts}${vopt},vers=${vers}${sectry}"
|
try_opts="${base_opts}${vopt},vers=${vers}${sectry}"
|
||||||
mount_drive "$try_opts"
|
mount_drive "$try_opts"
|
||||||
done
|
|
||||||
[[ "$MOUNTED" == "false" ]] || break
|
|
||||||
done
|
done
|
||||||
[[ "$MOUNTED" == "false" ]] || break
|
[[ "$MOUNTED" == "false" ]] || break
|
||||||
|
done
|
||||||
|
[[ "$MOUNTED" == "false" ]] || break
|
||||||
done
|
done
|
||||||
|
|
||||||
if [[ "$MOUNTED" == "false" ]]; then
|
if [[ "$MOUNTED" == "false" ]]; then
|
||||||
bashio::log.warning "...... still failing after vers ladder; retrying with reduced CIFS options."
|
for vopt in "${opt_variants[@]}"; do
|
||||||
base_opts="$MOUNTOPTIONS"
|
for vers in "${vers_variants2[@]}"; do
|
||||||
base_opts="$(echo "$base_opts" | sed -E 's/,vers=[^,]+//g; s/,sec=[^,]+//g')"
|
[[ "$MOUNTED" == "false" ]] || break
|
||||||
base_opts="${base_opts//,mfsymlinks/}"
|
umount "$mountpoint" 2>/dev/null || true
|
||||||
base_opts="${base_opts//,nobrl/}"
|
try_opts="${base_opts}${vopt},vers=${vers},sec=ntlmssp"
|
||||||
base_opts="$(echo "$base_opts" | sed -E 's/,iocharset=[^,]+//g')"
|
mount_drive "$try_opts"
|
||||||
|
|
||||||
local -a vers_variants2=("2.1" "2.0" "1.0")
|
|
||||||
for vopt in "${opt_variants[@]}"; do
|
|
||||||
for vers in "${vers_variants2[@]}"; do
|
|
||||||
for sectry in "${sec_variants[@]}"; do
|
|
||||||
[[ "$MOUNTED" == "false" ]] || break
|
|
||||||
umount "$mountpoint" 2> /dev/null || true
|
|
||||||
try_opts="${base_opts}${vopt},vers=${vers}${sectry}"
|
|
||||||
mount_drive "$try_opts"
|
|
||||||
done
|
|
||||||
[[ "$MOUNTED" == "false" ]] || break
|
|
||||||
done
|
|
||||||
[[ "$MOUNTED" == "false" ]] || break
|
|
||||||
done
|
done
|
||||||
|
[[ "$MOUNTED" == "false" ]] || break
|
||||||
if [[ "$MOUNTED" == "false" ]]; then
|
done
|
||||||
for vopt in "${opt_variants[@]}"; do
|
|
||||||
for vers in "${vers_variants2[@]}"; do
|
|
||||||
[[ "$MOUNTED" == "false" ]] || break
|
|
||||||
umount "$mountpoint" 2> /dev/null || true
|
|
||||||
try_opts="${base_opts}${vopt},vers=${vers},sec=ntlmssp"
|
|
||||||
mount_drive "$try_opts"
|
|
||||||
done
|
|
||||||
[[ "$MOUNTED" == "false" ]] || break
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
########################
|
########################
|
||||||
@@ -185,289 +185,289 @@ retry_cifs_with_vers_ladder_on_dialect_failure() {
|
|||||||
|
|
||||||
if bashio::config.has_value 'networkdisks'; then
|
if bashio::config.has_value 'networkdisks'; then
|
||||||
|
|
||||||
echo "Mounting network share(s)..."
|
echo "Mounting network share(s)..."
|
||||||
|
|
||||||
MOREDISKS="$(bashio::config 'networkdisks')"
|
MOREDISKS="$(bashio::config 'networkdisks')"
|
||||||
USERNAME="$(bashio::config 'cifsusername')"
|
USERNAME="$(bashio::config 'cifsusername')"
|
||||||
PASSWORD="$(bashio::config 'cifspassword')"
|
PASSWORD="$(bashio::config 'cifspassword')"
|
||||||
|
|
||||||
|
SMBVERS=""
|
||||||
|
SECVERS=""
|
||||||
|
CHARSET=",iocharset=utf8"
|
||||||
|
|
||||||
|
MOREDISKS="${MOREDISKS//$'\r'/}"
|
||||||
|
MOREDISKS="${MOREDISKS//$'\n'/,}"
|
||||||
|
MOREDISKS="$(echo "$MOREDISKS" | sed -E 's/[[:space:]]*,[[:space:]]*/,/g; s/^[[:space:]]+//; s/[[:space:]]+$//')"
|
||||||
|
|
||||||
|
DOMAINCLIENT=""
|
||||||
|
CIFSDOMAIN=""
|
||||||
|
if bashio::config.has_value 'cifsdomain'; then
|
||||||
|
CIFSDOMAIN="$(bashio::config 'cifsdomain')"
|
||||||
|
echo "... using domain $CIFSDOMAIN"
|
||||||
|
DOMAINCLIENT="--workgroup=$CIFSDOMAIN"
|
||||||
|
fi
|
||||||
|
|
||||||
|
PUID=",uid=$(id -u)"
|
||||||
|
PGID=",gid=$(id -g)"
|
||||||
|
if bashio::config.has_value 'PUID' && bashio::config.has_value 'PGID'; then
|
||||||
|
echo "... using PUID $(bashio::config 'PUID') and PGID $(bashio::config 'PGID')"
|
||||||
|
PUID=",uid=$(bashio::config 'PUID')"
|
||||||
|
PGID=",gid=$(bashio::config 'PGID')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
IFS=',' read -r -a DISK_LIST <<< "$MOREDISKS"
|
||||||
|
|
||||||
|
for disk in "${DISK_LIST[@]}"; do
|
||||||
|
CRED_FILE=""
|
||||||
|
cleanup_cred
|
||||||
|
|
||||||
|
disk="${disk//$'\r'/}"
|
||||||
|
disk="$(echo "$disk" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"
|
||||||
|
[[ -z "$disk" ]] && continue
|
||||||
|
|
||||||
|
disk="$(echo "$disk" | sed 's,/$,,')"
|
||||||
|
disk="${disk//"\040"/ }"
|
||||||
|
|
||||||
|
FSTYPE="cifs"
|
||||||
|
if [[ "$disk" =~ ^nfs:// ]]; then
|
||||||
|
FSTYPE="nfs"
|
||||||
|
disk="${disk#nfs://}"
|
||||||
|
elif [[ "$disk" =~ ^[^/:]+:/.+ ]]; then
|
||||||
|
FSTYPE="nfs"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$FSTYPE" == "cifs" ]]; then
|
||||||
|
server="$(echo "$disk" | grep -E -o "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" | head -n 1)"
|
||||||
|
else
|
||||||
|
server="${disk%%:*}"
|
||||||
|
fi
|
||||||
|
|
||||||
|
diskname="$disk"
|
||||||
|
diskname="${diskname//\\//}"
|
||||||
|
diskname="${diskname##*/}"
|
||||||
|
|
||||||
|
if mountpoint -q "/mnt/$diskname" 2>/dev/null; then
|
||||||
|
bashio::log.warning "...... mount point /mnt/$diskname already in use (name collision for $disk). Skipping this share."
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
|
||||||
|
ERRORCODE_FILE="/tmp/mount_error_${diskname//[^a-zA-Z0-9._-]/_}.log"
|
||||||
|
: >"$ERRORCODE_FILE" || true
|
||||||
|
|
||||||
|
MOUNTED=false
|
||||||
|
CIFS_LADDER_ATTEMPTED=false
|
||||||
|
SMBVERS_FORCE=""
|
||||||
|
SECVERS_FORCE=""
|
||||||
SMBVERS=""
|
SMBVERS=""
|
||||||
SECVERS=""
|
SECVERS=""
|
||||||
CHARSET=",iocharset=utf8"
|
|
||||||
|
|
||||||
MOREDISKS="${MOREDISKS//$'\r'/}"
|
echo "... mounting ($FSTYPE) $disk"
|
||||||
MOREDISKS="${MOREDISKS//$'\n'/,}"
|
|
||||||
MOREDISKS="$(echo "$MOREDISKS" | sed -E 's/[[:space:]]*,[[:space:]]*/,/g; s/^[[:space:]]+//; s/[[:space:]]+$//')"
|
|
||||||
|
|
||||||
DOMAINCLIENT=""
|
if [[ "$FSTYPE" == "cifs" ]]; then
|
||||||
CIFSDOMAIN=""
|
if [[ ! "$disk" =~ ^//[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/.+ ]]; then
|
||||||
if bashio::config.has_value 'cifsdomain'; then
|
bashio::log.fatal "...... invalid CIFS path \"$disk\". Use //123.12.12.12/sharedfolder,//123.12.12.12/sharedfolder2"
|
||||||
CIFSDOMAIN="$(bashio::config 'cifsdomain')"
|
echo "Invalid CIFS path structure: $disk" >"$ERRORCODE_FILE" || true
|
||||||
echo "... using domain $CIFSDOMAIN"
|
continue
|
||||||
DOMAINCLIENT="--workgroup=$CIFSDOMAIN"
|
fi
|
||||||
|
else
|
||||||
|
if [[ ! "$disk" =~ ^[^/:]+:/.+ ]]; then
|
||||||
|
bashio::log.fatal "...... invalid NFS path \"$disk\". Use server:/export/path or 123.12.12.12:/export/path"
|
||||||
|
echo "Invalid NFS path structure: $disk" >"$ERRORCODE_FILE" || true
|
||||||
|
continue
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
PUID=",uid=$(id -u)"
|
mkdir -p "/mnt/$diskname"
|
||||||
PGID=",gid=$(id -g)"
|
chown root:root "/mnt/$diskname"
|
||||||
if bashio::config.has_value 'PUID' && bashio::config.has_value 'PGID'; then
|
|
||||||
echo "... using PUID $(bashio::config 'PUID') and PGID $(bashio::config 'PGID')"
|
if [[ "$FSTYPE" == "cifs" ]]; then
|
||||||
PUID=",uid=$(bashio::config 'PUID')"
|
CRED_FILE="$(mktemp /tmp/cifs-cred.XXXXXX)"
|
||||||
PGID=",gid=$(bashio::config 'PGID')"
|
chmod 600 "$CRED_FILE"
|
||||||
|
{
|
||||||
|
printf 'username=%s\n' "$USERNAME"
|
||||||
|
printf 'password=%s\n' "$PASSWORD"
|
||||||
|
if [[ -n "${CIFSDOMAIN:-}" ]]; then
|
||||||
|
printf 'domain=%s\n' "$CIFSDOMAIN"
|
||||||
|
fi
|
||||||
|
} >"$CRED_FILE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
IFS=',' read -r -a DISK_LIST <<< "$MOREDISKS"
|
if [[ "$FSTYPE" == "cifs" ]]; then
|
||||||
|
mount_drive "rw,file_mode=0775,dir_mode=0775,credentials=${CRED_FILE},nobrl,mfsymlinks${SMBVERS}${SECVERS}${PUID}${PGID}${CHARSET}"
|
||||||
|
if [[ "$MOUNTED" == "false" ]]; then
|
||||||
|
retry_cifs_with_vers_ladder_on_dialect_failure
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
mount_drive "rw,nfsvers=4.2,proto=tcp,hard,timeo=600,retrans=2"
|
||||||
|
fi
|
||||||
|
|
||||||
for disk in "${DISK_LIST[@]}"; do
|
if [[ "$MOUNTED" == "false" ]]; then
|
||||||
CRED_FILE=""
|
if [[ "$FSTYPE" == "cifs" ]]; then
|
||||||
cleanup_cred
|
if command -v nmap >/dev/null 2>&1; then
|
||||||
|
output="$(nmap -F "$server" -T5 -oG - 2>/dev/null || true)"
|
||||||
disk="${disk//$'\r'/}"
|
if ! echo "$output" | grep -q "445/open"; then
|
||||||
disk="$(echo "$disk" | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')"
|
if echo "$output" | grep -q "/open"; then
|
||||||
[[ -z "$disk" ]] && continue
|
bashio::log.fatal "...... $server is reachable but SMB port not opened, stopping script"
|
||||||
|
else
|
||||||
disk="$(echo "$disk" | sed 's,/$,,')"
|
bashio::log.fatal "...... fatal : $server not reachable, is it correct"
|
||||||
disk="${disk//"\040"/ }"
|
fi
|
||||||
|
cleanup_cred
|
||||||
FSTYPE="cifs"
|
rm -f "$ERRORCODE_FILE" 2>/dev/null || true
|
||||||
if [[ "$disk" =~ ^nfs:// ]]; then
|
|
||||||
FSTYPE="nfs"
|
|
||||||
disk="${disk#nfs://}"
|
|
||||||
elif [[ "$disk" =~ ^[^/:]+:/.+ ]]; then
|
|
||||||
FSTYPE="nfs"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$FSTYPE" == "cifs" ]]; then
|
|
||||||
server="$(echo "$disk" | grep -E -o "[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+" | head -n 1)"
|
|
||||||
else
|
|
||||||
server="${disk%%:*}"
|
|
||||||
fi
|
|
||||||
|
|
||||||
diskname="$disk"
|
|
||||||
diskname="${diskname//\\//}"
|
|
||||||
diskname="${diskname##*/}"
|
|
||||||
|
|
||||||
if mountpoint -q "/mnt/$diskname" 2> /dev/null; then
|
|
||||||
bashio::log.warning "...... mount point /mnt/$diskname already in use (name collision for $disk). Skipping this share."
|
|
||||||
continue
|
continue
|
||||||
|
fi
|
||||||
|
echo "...... $server is confirmed reachable"
|
||||||
|
else
|
||||||
|
bashio::log.warning "...... nmap not available; skipping SMB port reachability test"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
ERRORCODE_FILE="/tmp/mount_error_${diskname//[^a-zA-Z0-9._-]/_}.log"
|
if command -v smbclient >/dev/null 2>&1; then
|
||||||
: > "$ERRORCODE_FILE" || true
|
OUTPUT="$(smbclient -t 2 -L "$server" -U "$USERNAME%$PASSWORD" -c "exit" $DOMAINCLIENT 2>&1 || true)"
|
||||||
|
|
||||||
|
if echo "$OUTPUT" | grep -q "LOGON_FAILURE"; then
|
||||||
|
bashio::log.fatal "...... incorrect Username, Password, or Domain! Script will stop."
|
||||||
|
cleanup_cred
|
||||||
|
rm -f "$ERRORCODE_FILE" 2>/dev/null || true
|
||||||
|
bashio::addon.stop
|
||||||
|
elif echo "$OUTPUT" | grep -q "tree connect failed" || echo "$OUTPUT" | grep -q "NT_STATUS_CONNECTION_DISCONNECTED"; then
|
||||||
|
echo "... using SMBv1"
|
||||||
|
bashio::log.warning "...... share reachable only with legacy SMBv1 (NT1) negotiation. Forcing SMBv1 options."
|
||||||
|
SMBVERS_FORCE=",vers=1.0"
|
||||||
|
SECVERS_FORCE=",sec=ntlm"
|
||||||
|
elif ! echo "$OUTPUT" | grep -q "Disk"; then
|
||||||
|
bashio::log.fatal "...... no shares found. Invalid or inaccessible SMB path?"
|
||||||
|
else
|
||||||
|
echo "...... credentials are valid"
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
bashio::log.warning "...... smbclient not available; skipping SMB credential test"
|
||||||
|
fi
|
||||||
|
|
||||||
|
SMBRAW=""
|
||||||
|
SMB1_DETECTED=false
|
||||||
|
if command -v nmap >/dev/null 2>&1; then
|
||||||
|
NMAP_OUTPUT="$(nmap --script smb-protocols -p 445 "$server" 2>/dev/null || true)"
|
||||||
|
SMBRAW="$(
|
||||||
|
echo "$NMAP_OUTPUT" \
|
||||||
|
| awk '/SMB2_DIALECT_/ {print $NF}' \
|
||||||
|
| sed 's/SMB2_DIALECT_//' \
|
||||||
|
| tr -d '_' \
|
||||||
|
| sort -V | tail -n 1 || true
|
||||||
|
)"
|
||||||
|
if [[ -z "$SMBRAW" ]] && echo "$NMAP_OUTPUT" | grep -Eiq 'NT LM 0\.12|SMBv1|NT1'; then
|
||||||
|
SMB1_DETECTED=true
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
MOUNTED=false
|
|
||||||
CIFS_LADDER_ATTEMPTED=false
|
|
||||||
SMBVERS_FORCE=""
|
|
||||||
SECVERS_FORCE=""
|
|
||||||
SMBVERS=""
|
SMBVERS=""
|
||||||
SECVERS=""
|
case "$SMBRAW" in
|
||||||
|
311) SMBVERS=",vers=3.1.1" ;;
|
||||||
|
302) SMBVERS=",vers=3.02" ;;
|
||||||
|
300) SMBVERS=",vers=3.0" ;;
|
||||||
|
210) SMBVERS=",vers=2.1" ;;
|
||||||
|
202|200) SMBVERS=",vers=2.0" ;;
|
||||||
|
*) SMBVERS="" ;;
|
||||||
|
esac
|
||||||
|
|
||||||
echo "... mounting ($FSTYPE) $disk"
|
if [[ -z "$SMBVERS" && "$SMB1_DETECTED" == "true" ]]; then
|
||||||
|
echo "...... SMB version detected via nmap : SMBv1 (NT LM 0.12)"
|
||||||
|
SMBVERS=",vers=1.0"
|
||||||
|
SECVERS=",sec=ntlm"
|
||||||
|
fi
|
||||||
|
|
||||||
if [[ "$FSTYPE" == "cifs" ]]; then
|
if [[ -n "$SMBVERS" ]]; then
|
||||||
if [[ ! "$disk" =~ ^//[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+/.+ ]]; then
|
echo "...... SMB version detected : ${SMBVERS#,vers=}"
|
||||||
bashio::log.fatal "...... invalid CIFS path \"$disk\". Use //123.12.12.12/sharedfolder,//123.12.12.12/sharedfolder2"
|
elif command -v smbclient >/dev/null 2>&1 && smbclient -t 2 -L "$server" -m NT1 -U "$USERNAME%$PASSWORD" $DOMAINCLIENT -c "exit" &>/dev/null; then
|
||||||
echo "Invalid CIFS path structure: $disk" > "$ERRORCODE_FILE" || true
|
echo "...... SMB version : only SMBv1 is supported, this can lead to issues"
|
||||||
continue
|
SECVERS=",sec=ntlm"
|
||||||
fi
|
SMBVERS=",vers=1.0"
|
||||||
else
|
else
|
||||||
if [[ ! "$disk" =~ ^[^/:]+:/.+ ]]; then
|
echo "...... SMB version : couldn't detect, falling back to SMB3->SMB2->SMB1 ladder on negotiation/dialect failure"
|
||||||
bashio::log.fatal "...... invalid NFS path \"$disk\". Use server:/export/path or 123.12.12.12:/export/path"
|
SMBVERS=",vers=3.1.1"
|
||||||
echo "Invalid NFS path structure: $disk" > "$ERRORCODE_FILE" || true
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p "/mnt/$diskname"
|
if [[ -n "$SMBVERS_FORCE" ]]; then
|
||||||
chown root:root "/mnt/$diskname"
|
if [[ -n "$SMBVERS" && "$SMBVERS" != "$SMBVERS_FORCE" ]]; then
|
||||||
|
bashio::log.warning "...... overriding detected SMB version ${SMBVERS#,vers=} with forced ${SMBVERS_FORCE#,vers=} (server requires legacy protocol)"
|
||||||
if [[ "$FSTYPE" == "cifs" ]]; then
|
fi
|
||||||
CRED_FILE="$(mktemp /tmp/cifs-cred.XXXXXX)"
|
SMBVERS="$SMBVERS_FORCE"
|
||||||
chmod 600 "$CRED_FILE"
|
[[ -z "$SECVERS" ]] && SECVERS="$SECVERS_FORCE"
|
||||||
{
|
|
||||||
printf 'username=%s\n' "$USERNAME"
|
|
||||||
printf 'password=%s\n' "$PASSWORD"
|
|
||||||
if [[ -n "${CIFSDOMAIN:-}" ]]; then
|
|
||||||
printf 'domain=%s\n' "$CIFSDOMAIN"
|
|
||||||
fi
|
|
||||||
} > "$CRED_FILE"
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$FSTYPE" == "cifs" ]]; then
|
SECVERS_BASE="$SECVERS"
|
||||||
mount_drive "rw,file_mode=0775,dir_mode=0775,credentials=${CRED_FILE},nobrl,mfsymlinks${SMBVERS}${SECVERS}${PUID}${PGID}${CHARSET}"
|
for SECTRY in "$SECVERS_BASE" ",sec=ntlmv2" ",sec=ntlmssp" ",sec=ntlmsspi" ",sec=krb5i" ",sec=krb5" ",sec=ntlm" ",sec=ntlmv2i"; do
|
||||||
if [[ "$MOUNTED" == "false" ]]; then
|
if [[ "$MOUNTED" == "false" ]]; then
|
||||||
retry_cifs_with_vers_ladder_on_dialect_failure
|
mount_drive "rw,file_mode=0775,dir_mode=0775,credentials=${CRED_FILE},nobrl,mfsymlinks${SMBVERS}${SECTRY}${PUID}${PGID}${CHARSET}"
|
||||||
fi
|
fi
|
||||||
else
|
done
|
||||||
mount_drive "rw,nfsvers=4.2,proto=tcp,hard,timeo=600,retrans=2"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ "$MOUNTED" == "false" ]]; then
|
if [[ "$MOUNTED" == "false" ]]; then
|
||||||
if [[ "$FSTYPE" == "cifs" ]]; then
|
retry_cifs_with_vers_ladder_on_dialect_failure
|
||||||
if command -v nmap > /dev/null 2>&1; then
|
|
||||||
output="$(nmap -F "$server" -T5 -oG - 2> /dev/null || true)"
|
|
||||||
if ! echo "$output" | grep -q "445/open"; then
|
|
||||||
if echo "$output" | grep -q "/open"; then
|
|
||||||
bashio::log.fatal "...... $server is reachable but SMB port not opened, stopping script"
|
|
||||||
else
|
|
||||||
bashio::log.fatal "...... fatal : $server not reachable, is it correct"
|
|
||||||
fi
|
|
||||||
cleanup_cred
|
|
||||||
rm -f "$ERRORCODE_FILE" 2> /dev/null || true
|
|
||||||
continue
|
|
||||||
fi
|
|
||||||
echo "...... $server is confirmed reachable"
|
|
||||||
else
|
|
||||||
bashio::log.warning "...... nmap not available; skipping SMB port reachability test"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if command -v smbclient > /dev/null 2>&1; then
|
|
||||||
OUTPUT="$(smbclient -t 2 -L "$server" -U "$USERNAME%$PASSWORD" -c "exit" $DOMAINCLIENT 2>&1 || true)"
|
|
||||||
|
|
||||||
if echo "$OUTPUT" | grep -q "LOGON_FAILURE"; then
|
|
||||||
bashio::log.fatal "...... incorrect Username, Password, or Domain! Script will stop."
|
|
||||||
cleanup_cred
|
|
||||||
rm -f "$ERRORCODE_FILE" 2> /dev/null || true
|
|
||||||
bashio::addon.stop
|
|
||||||
elif echo "$OUTPUT" | grep -q "tree connect failed" || echo "$OUTPUT" | grep -q "NT_STATUS_CONNECTION_DISCONNECTED"; then
|
|
||||||
echo "... using SMBv1"
|
|
||||||
bashio::log.warning "...... share reachable only with legacy SMBv1 (NT1) negotiation. Forcing SMBv1 options."
|
|
||||||
SMBVERS_FORCE=",vers=1.0"
|
|
||||||
SECVERS_FORCE=",sec=ntlm"
|
|
||||||
elif ! echo "$OUTPUT" | grep -q "Disk"; then
|
|
||||||
bashio::log.fatal "...... no shares found. Invalid or inaccessible SMB path?"
|
|
||||||
else
|
|
||||||
echo "...... credentials are valid"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
bashio::log.warning "...... smbclient not available; skipping SMB credential test"
|
|
||||||
fi
|
|
||||||
|
|
||||||
SMBRAW=""
|
|
||||||
SMB1_DETECTED=false
|
|
||||||
if command -v nmap > /dev/null 2>&1; then
|
|
||||||
NMAP_OUTPUT="$(nmap --script smb-protocols -p 445 "$server" 2> /dev/null || true)"
|
|
||||||
SMBRAW="$(
|
|
||||||
echo "$NMAP_OUTPUT" \
|
|
||||||
| awk '/SMB2_DIALECT_/ {print $NF}' \
|
|
||||||
| sed 's/SMB2_DIALECT_//' \
|
|
||||||
| tr -d '_' \
|
|
||||||
| sort -V | tail -n 1 || true
|
|
||||||
)"
|
|
||||||
if [[ -z "$SMBRAW" ]] && echo "$NMAP_OUTPUT" | grep -Eiq 'NT LM 0\.12|SMBv1|NT1'; then
|
|
||||||
SMB1_DETECTED=true
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
|
|
||||||
SMBVERS=""
|
|
||||||
case "$SMBRAW" in
|
|
||||||
311) SMBVERS=",vers=3.1.1" ;;
|
|
||||||
302) SMBVERS=",vers=3.02" ;;
|
|
||||||
300) SMBVERS=",vers=3.0" ;;
|
|
||||||
210) SMBVERS=",vers=2.1" ;;
|
|
||||||
202 | 200) SMBVERS=",vers=2.0" ;;
|
|
||||||
*) SMBVERS="" ;;
|
|
||||||
esac
|
|
||||||
|
|
||||||
if [[ -z "$SMBVERS" && "$SMB1_DETECTED" == "true" ]]; then
|
|
||||||
echo "...... SMB version detected via nmap : SMBv1 (NT LM 0.12)"
|
|
||||||
SMBVERS=",vers=1.0"
|
|
||||||
SECVERS=",sec=ntlm"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "$SMBVERS" ]]; then
|
|
||||||
echo "...... SMB version detected : ${SMBVERS#,vers=}"
|
|
||||||
elif command -v smbclient > /dev/null 2>&1 && smbclient -t 2 -L "$server" -m NT1 -U "$USERNAME%$PASSWORD" $DOMAINCLIENT -c "exit" &> /dev/null; then
|
|
||||||
echo "...... SMB version : only SMBv1 is supported, this can lead to issues"
|
|
||||||
SECVERS=",sec=ntlm"
|
|
||||||
SMBVERS=",vers=1.0"
|
|
||||||
else
|
|
||||||
echo "...... SMB version : couldn't detect, falling back to SMB3->SMB2->SMB1 ladder on negotiation/dialect failure"
|
|
||||||
SMBVERS=",vers=3.1.1"
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [[ -n "$SMBVERS_FORCE" ]]; then
|
|
||||||
if [[ -n "$SMBVERS" && "$SMBVERS" != "$SMBVERS_FORCE" ]]; then
|
|
||||||
bashio::log.warning "...... overriding detected SMB version ${SMBVERS#,vers=} with forced ${SMBVERS_FORCE#,vers=} (server requires legacy protocol)"
|
|
||||||
fi
|
|
||||||
SMBVERS="$SMBVERS_FORCE"
|
|
||||||
[[ -z "$SECVERS" ]] && SECVERS="$SECVERS_FORCE"
|
|
||||||
fi
|
|
||||||
|
|
||||||
SECVERS_BASE="$SECVERS"
|
|
||||||
for SECTRY in "$SECVERS_BASE" ",sec=ntlmv2" ",sec=ntlmssp" ",sec=ntlmsspi" ",sec=krb5i" ",sec=krb5" ",sec=ntlm" ",sec=ntlmv2i"; do
|
|
||||||
if [[ "$MOUNTED" == "false" ]]; then
|
|
||||||
mount_drive "rw,file_mode=0775,dir_mode=0775,credentials=${CRED_FILE},nobrl,mfsymlinks${SMBVERS}${SECTRY}${PUID}${PGID}${CHARSET}"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [[ "$MOUNTED" == "false" ]]; then
|
|
||||||
retry_cifs_with_vers_ladder_on_dialect_failure
|
|
||||||
fi
|
|
||||||
|
|
||||||
else
|
|
||||||
if command -v nmap > /dev/null 2>&1; then
|
|
||||||
output="$(nmap -F "$server" -T5 -oG - 2> /dev/null || true)"
|
|
||||||
if ! echo "$output" | grep -Eq '(2049|111)/open'; then
|
|
||||||
bashio::log.fatal "...... $server is reachable but NFS ports not open"
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
bashio::log.warning "...... nmap not available; skipping NFS port reachability test"
|
|
||||||
fi
|
|
||||||
|
|
||||||
for NFVER in 4.2 4.1 4 3; do
|
|
||||||
if [[ "$MOUNTED" == "false" ]]; then
|
|
||||||
mount_drive "rw,nfsvers=${NFVER},proto=tcp"
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [[ "$MOUNTED" == "true" ]]; then
|
else
|
||||||
bashio::log.info "...... $disk successfully mounted to /mnt/$diskname with options ${MOUNTOPTIONS/$PASSWORD/XXXXXXXXXX}"
|
if command -v nmap >/dev/null 2>&1; then
|
||||||
rm -f "$ERRORCODE_FILE" 2> /dev/null || true
|
output="$(nmap -F "$server" -T5 -oG - 2>/dev/null || true)"
|
||||||
|
if ! echo "$output" | grep -Eq '(2049|111)/open'; then
|
||||||
if [[ "$FSTYPE" == "cifs" && "$MOUNTOPTIONS" == *"vers=1.0"* ]]; then
|
bashio::log.fatal "...... $server is reachable but NFS ports not open"
|
||||||
bashio::log.warning ""
|
fi
|
||||||
bashio::log.warning "Your SMB system requires SMBv1. This is an obsolete protocol. Please correct this to prevent issues."
|
|
||||||
bashio::log.warning ""
|
|
||||||
fi
|
|
||||||
|
|
||||||
cleanup_cred
|
|
||||||
|
|
||||||
elif [[ "$MOUNTED" == "readonly" ]]; then
|
|
||||||
bashio::log.warning "...... $disk mounted to /mnt/$diskname but is READ-ONLY or not writable by UID/GID ${PUID#,uid=}:${PGID#,gid=}."
|
|
||||||
bashio::log.warning "...... Check Samba share permissions, or try setting PUID/PGID to 0/0 (root), or adjust server ACLs."
|
|
||||||
rm -f "$ERRORCODE_FILE" 2> /dev/null || true
|
|
||||||
cleanup_cred
|
|
||||||
|
|
||||||
else
|
else
|
||||||
if [[ "$FSTYPE" == "cifs" ]]; then
|
bashio::log.warning "...... nmap not available; skipping NFS port reachability test"
|
||||||
bashio::log.fatal "Error, unable to mount $disk to /mnt/$diskname with username $USERNAME. Please check share path, username/password/domain; try UID/GID 0."
|
|
||||||
bashio::log.fatal "Here is some debugging info :"
|
|
||||||
if command -v smbclient > /dev/null 2>&1; then
|
|
||||||
smbclient -t 2 -L "$server" -U "$USERNAME%$PASSWORD" -c "exit" $DOMAINCLIENT || true
|
|
||||||
else
|
|
||||||
bashio::log.warning "smbclient not available; cannot print SMB debugging info"
|
|
||||||
fi
|
|
||||||
|
|
||||||
mount_drive "rw,credentials=${CRED_FILE}${PUID}${PGID}"
|
|
||||||
if [[ "$MOUNTED" == "false" ]]; then
|
|
||||||
retry_cifs_with_vers_ladder_on_dialect_failure
|
|
||||||
fi
|
|
||||||
else
|
|
||||||
bashio::log.fatal "Error, unable to mount NFS share $disk to /mnt/$diskname. Please check export path and allowlist for this client."
|
|
||||||
mount_drive "rw"
|
|
||||||
fi
|
|
||||||
|
|
||||||
ERR_READ="$(cat "$ERRORCODE_FILE" 2> /dev/null || true)"
|
|
||||||
bashio::log.fatal "Error read : ${ERR_READ:-unknown error}, addon will stop in 1 min"
|
|
||||||
|
|
||||||
umount "/mnt/$diskname" 2> /dev/null || true
|
|
||||||
rmdir "/mnt/$diskname" 2> /dev/null || true
|
|
||||||
cleanup_cred
|
|
||||||
rm -f "$ERRORCODE_FILE" 2> /dev/null || true
|
|
||||||
|
|
||||||
bashio::addon.stop
|
|
||||||
fi
|
fi
|
||||||
done
|
|
||||||
|
for NFVER in 4.2 4.1 4 3; do
|
||||||
|
if [[ "$MOUNTED" == "false" ]]; then
|
||||||
|
mount_drive "rw,nfsvers=${NFVER},proto=tcp"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ "$MOUNTED" == "true" ]]; then
|
||||||
|
bashio::log.info "...... $disk successfully mounted to /mnt/$diskname with options ${MOUNTOPTIONS/$PASSWORD/XXXXXXXXXX}"
|
||||||
|
rm -f "$ERRORCODE_FILE" 2>/dev/null || true
|
||||||
|
|
||||||
|
if [[ "$FSTYPE" == "cifs" && "$MOUNTOPTIONS" == *"vers=1.0"* ]]; then
|
||||||
|
bashio::log.warning ""
|
||||||
|
bashio::log.warning "Your SMB system requires SMBv1. This is an obsolete protocol. Please correct this to prevent issues."
|
||||||
|
bashio::log.warning ""
|
||||||
|
fi
|
||||||
|
|
||||||
|
cleanup_cred
|
||||||
|
|
||||||
|
elif [[ "$MOUNTED" == "readonly" ]]; then
|
||||||
|
bashio::log.warning "...... $disk mounted to /mnt/$diskname but is READ-ONLY or not writable by UID/GID ${PUID#,uid=}:${PGID#,gid=}."
|
||||||
|
bashio::log.warning "...... Check Samba share permissions, or try setting PUID/PGID to 0/0 (root), or adjust server ACLs."
|
||||||
|
rm -f "$ERRORCODE_FILE" 2>/dev/null || true
|
||||||
|
cleanup_cred
|
||||||
|
|
||||||
|
else
|
||||||
|
if [[ "$FSTYPE" == "cifs" ]]; then
|
||||||
|
bashio::log.fatal "Error, unable to mount $disk to /mnt/$diskname with username $USERNAME. Please check share path, username/password/domain; try UID/GID 0."
|
||||||
|
bashio::log.fatal "Here is some debugging info :"
|
||||||
|
if command -v smbclient >/dev/null 2>&1; then
|
||||||
|
smbclient -t 2 -L "$server" -U "$USERNAME%$PASSWORD" -c "exit" $DOMAINCLIENT || true
|
||||||
|
else
|
||||||
|
bashio::log.warning "smbclient not available; cannot print SMB debugging info"
|
||||||
|
fi
|
||||||
|
|
||||||
|
mount_drive "rw,credentials=${CRED_FILE}${PUID}${PGID}"
|
||||||
|
if [[ "$MOUNTED" == "false" ]]; then
|
||||||
|
retry_cifs_with_vers_ladder_on_dialect_failure
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
bashio::log.fatal "Error, unable to mount NFS share $disk to /mnt/$diskname. Please check export path and allowlist for this client."
|
||||||
|
mount_drive "rw"
|
||||||
|
fi
|
||||||
|
|
||||||
|
ERR_READ="$(cat "$ERRORCODE_FILE" 2>/dev/null || true)"
|
||||||
|
bashio::log.fatal "Error read : ${ERR_READ:-unknown error}, addon will stop in 1 min"
|
||||||
|
|
||||||
|
umount "/mnt/$diskname" 2>/dev/null || true
|
||||||
|
rmdir "/mnt/$diskname" 2>/dev/null || true
|
||||||
|
cleanup_cred
|
||||||
|
rm -f "$ERRORCODE_FILE" 2>/dev/null || true
|
||||||
|
|
||||||
|
bashio::addon.stop
|
||||||
|
fi
|
||||||
|
done
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -138,9 +138,9 @@ append_unique_line() {
|
|||||||
# $1=file, $2=line
|
# $1=file, $2=line
|
||||||
local _file="$1"
|
local _file="$1"
|
||||||
local _line="$2"
|
local _line="$2"
|
||||||
mkdir -p "$(dirname "$_file")" 2> /dev/null || true
|
mkdir -p "$(dirname "$_file")" 2>/dev/null || true
|
||||||
touch "$_file" 2> /dev/null || true
|
touch "$_file" 2>/dev/null || true
|
||||||
grep -qxF -- "$_line" "$_file" 2> /dev/null || echo "$_line" >> "$_file"
|
grep -qxF -- "$_line" "$_file" 2>/dev/null || echo "$_line" >> "$_file"
|
||||||
}
|
}
|
||||||
|
|
||||||
while IFS= read -r line; do
|
while IFS= read -r line; do
|
||||||
|
|||||||
@@ -7,33 +7,27 @@ set -e
|
|||||||
##########################################
|
##########################################
|
||||||
|
|
||||||
pick_exec_dir() {
|
pick_exec_dir() {
|
||||||
local d
|
local d
|
||||||
for d in /dev/shm /run /var/run /mnt /root /; do
|
for d in /dev/shm /run /var/run /mnt /root /; do
|
||||||
if [ -d "$d" ] && [ -w "$d" ]; then
|
if [ -d "$d" ] && [ -w "$d" ]; then
|
||||||
local t="${d%/}/.exec_test_$$"
|
local t="${d%/}/.exec_test_$$"
|
||||||
printf '#!/bin/sh\necho ok\n' > "$t" 2> /dev/null || {
|
printf '#!/bin/sh\necho ok\n' >"$t" 2>/dev/null || { rm -f "$t" 2>/dev/null || true; continue; }
|
||||||
rm -f "$t" 2> /dev/null || true
|
chmod 700 "$t" 2>/dev/null || { rm -f "$t" 2>/dev/null || true; continue; }
|
||||||
continue
|
if "$t" >/dev/null 2>&1; then
|
||||||
}
|
rm -f "$t" 2>/dev/null || true
|
||||||
chmod 700 "$t" 2> /dev/null || {
|
echo "$d"
|
||||||
rm -f "$t" 2> /dev/null || true
|
return 0
|
||||||
continue
|
fi
|
||||||
}
|
rm -f "$t" 2>/dev/null || true
|
||||||
if "$t" > /dev/null 2>&1; then
|
fi
|
||||||
rm -f "$t" 2> /dev/null || true
|
done
|
||||||
echo "$d"
|
return 1
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
rm -f "$t" 2> /dev/null || true
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
return 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
EXEC_DIR="$(pick_exec_dir || true)"
|
EXEC_DIR="$(pick_exec_dir || true)"
|
||||||
if [ -z "${EXEC_DIR:-}" ]; then
|
if [ -z "${EXEC_DIR:-}" ]; then
|
||||||
echo "ERROR: Could not find an exec-capable writable directory."
|
echo "ERROR: Could not find an exec-capable writable directory."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
######################
|
######################
|
||||||
@@ -41,14 +35,14 @@ fi
|
|||||||
######################
|
######################
|
||||||
|
|
||||||
candidate_shebangs=(
|
candidate_shebangs=(
|
||||||
"/command/with-contenv bashio"
|
"/command/with-contenv bashio"
|
||||||
"/usr/bin/with-contenv bashio"
|
"/usr/bin/with-contenv bashio"
|
||||||
"/usr/bin/env bashio"
|
"/usr/bin/env bashio"
|
||||||
"/usr/bin/bashio"
|
"/usr/bin/bashio"
|
||||||
"/usr/bin/bash"
|
"/usr/bin/bash"
|
||||||
"/bin/bash"
|
"/bin/bash"
|
||||||
"/usr/bin/sh"
|
"/usr/bin/sh"
|
||||||
"/bin/sh"
|
"/bin/sh"
|
||||||
)
|
)
|
||||||
|
|
||||||
SHEBANG_ERRORS=()
|
SHEBANG_ERRORS=()
|
||||||
@@ -91,76 +85,76 @@ echo "${_bv:-PROBE_OK}"
|
|||||||
'
|
'
|
||||||
|
|
||||||
validate_shebang() {
|
validate_shebang() {
|
||||||
local candidate="$1"
|
local candidate="$1"
|
||||||
local tmp out rc
|
local tmp out rc
|
||||||
local errfile msg
|
local errfile msg
|
||||||
|
|
||||||
# shellcheck disable=SC2206
|
# shellcheck disable=SC2206
|
||||||
local cmd=($candidate)
|
local cmd=( $candidate )
|
||||||
local exe="${cmd[0]}"
|
local exe="${cmd[0]}"
|
||||||
|
|
||||||
if [ ! -x "$exe" ]; then
|
if [ ! -x "$exe" ]; then
|
||||||
SHEBANG_ERRORS+=(" - FAIL (not executable): #!$candidate")
|
SHEBANG_ERRORS+=(" - FAIL (not executable): #!$candidate")
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
tmp="${EXEC_DIR%/}/shebang_test.$$.$RANDOM"
|
|
||||||
errfile="${EXEC_DIR%/}/shebang_probe_err.$$"
|
|
||||||
{
|
|
||||||
printf '#!%s\n' "$candidate"
|
|
||||||
printf '%s\n' "$probe_script_content"
|
|
||||||
} > "$tmp"
|
|
||||||
chmod 700 "$tmp" 2> /dev/null || true
|
|
||||||
|
|
||||||
set +e
|
|
||||||
out="$("$tmp" 2> "$errfile")"
|
|
||||||
rc=$?
|
|
||||||
set -e
|
|
||||||
|
|
||||||
rm -f "$tmp" 2> /dev/null || true
|
|
||||||
|
|
||||||
if [ "$rc" -eq 0 ] && [ -n "${out:-}" ] && [ "$out" != "null" ]; then
|
|
||||||
rm -f "$errfile" 2> /dev/null || true
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
msg=$' - FAIL: #!'"$candidate"$'\n'" rc=$rc, stdout='${out:-}'"$'\n'
|
|
||||||
if [ -s "$errfile" ]; then
|
|
||||||
msg+=$' stderr:\n'
|
|
||||||
msg+="$(sed -n '1,8p' "$errfile")"$'\n'
|
|
||||||
else
|
|
||||||
msg+=$' stderr: <empty>\n'
|
|
||||||
fi
|
|
||||||
SHEBANG_ERRORS+=("$msg")
|
|
||||||
rm -f "$errfile" 2> /dev/null || true
|
|
||||||
return 1
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
tmp="${EXEC_DIR%/}/shebang_test.$$.$RANDOM"
|
||||||
|
errfile="${EXEC_DIR%/}/shebang_probe_err.$$"
|
||||||
|
{
|
||||||
|
printf '#!%s\n' "$candidate"
|
||||||
|
printf '%s\n' "$probe_script_content"
|
||||||
|
} >"$tmp"
|
||||||
|
chmod 700 "$tmp" 2>/dev/null || true
|
||||||
|
|
||||||
|
set +e
|
||||||
|
out="$("$tmp" 2>"$errfile")"
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
rm -f "$tmp" 2>/dev/null || true
|
||||||
|
|
||||||
|
if [ "$rc" -eq 0 ] && [ -n "${out:-}" ] && [ "$out" != "null" ]; then
|
||||||
|
rm -f "$errfile" 2>/dev/null || true
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
msg=$' - FAIL: #!'"$candidate"$'\n'" rc=$rc, stdout='${out:-}'"$'\n'
|
||||||
|
if [ -s "$errfile" ]; then
|
||||||
|
msg+=$' stderr:\n'
|
||||||
|
msg+="$(sed -n '1,8p' "$errfile")"$'\n'
|
||||||
|
else
|
||||||
|
msg+=$' stderr: <empty>\n'
|
||||||
|
fi
|
||||||
|
SHEBANG_ERRORS+=("$msg")
|
||||||
|
rm -f "$errfile" 2>/dev/null || true
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
shebang=""
|
shebang=""
|
||||||
for candidate in "${candidate_shebangs[@]}"; do
|
for candidate in "${candidate_shebangs[@]}"; do
|
||||||
if validate_shebang "$candidate"; then
|
if validate_shebang "$candidate"; then
|
||||||
shebang="$candidate"
|
shebang="$candidate"
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ -z "$shebang" ]; then
|
if [ -z "$shebang" ]; then
|
||||||
echo "ERROR: No valid shebang found." >&2
|
echo "ERROR: No valid shebang found." >&2
|
||||||
printf ' - %s\n' "${candidate_shebangs[@]}" >&2
|
printf ' - %s\n' "${candidate_shebangs[@]}" >&2
|
||||||
if [ "${#SHEBANG_ERRORS[@]}" -gt 0 ]; then
|
if [ "${#SHEBANG_ERRORS[@]}" -gt 0 ]; then
|
||||||
printf '%s\n' "${SHEBANG_ERRORS[@]}" >&2
|
printf '%s\n' "${SHEBANG_ERRORS[@]}" >&2
|
||||||
fi
|
fi
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if ! command -v bashio::addon.version > /dev/null 2>&1; then
|
if ! command -v bashio::addon.version >/dev/null 2>&1; then
|
||||||
for f in /usr/lib/bashio/bashio.sh /usr/lib/bashio/lib.sh /usr/src/bashio/bashio.sh /usr/local/lib/bashio/bashio.sh /usr/local/lib/bashio-standalone.sh; do
|
for f in /usr/lib/bashio/bashio.sh /usr/lib/bashio/lib.sh /usr/src/bashio/bashio.sh /usr/local/lib/bashio/bashio.sh /usr/local/lib/bashio-standalone.sh; do
|
||||||
if [ -f "$f" ]; then
|
if [ -f "$f" ]; then
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
. "$f"
|
. "$f"
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
##################
|
##################
|
||||||
@@ -168,9 +162,9 @@ fi
|
|||||||
##################
|
##################
|
||||||
|
|
||||||
# Exit if /config is not mounted or HA not used
|
# Exit if /config is not mounted or HA not used
|
||||||
if [ ! -d /config ] || ! bashio::supervisor.ping 2> /dev/null; then
|
if [ ! -d /config ] || ! bashio::supervisor.ping 2>/dev/null; then
|
||||||
echo "..."
|
echo "..."
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Define slug
|
# Define slug
|
||||||
@@ -179,11 +173,11 @@ slug="${slug#*_}"
|
|||||||
|
|
||||||
# Check type of config folder
|
# Check type of config folder
|
||||||
if [ ! -f /config/configuration.yaml ] && [ ! -f /config/configuration.json ]; then
|
if [ ! -f /config/configuration.yaml ] && [ ! -f /config/configuration.json ]; then
|
||||||
CONFIGLOCATION="/config"
|
CONFIGLOCATION="/config"
|
||||||
CONFIGFILEBROWSER="/app_configs/${HOSTNAME/-/_}/$slug.sh"
|
CONFIGFILEBROWSER="/app_configs/${HOSTNAME/-/_}/$slug.sh"
|
||||||
else
|
else
|
||||||
CONFIGLOCATION="/config/addons_autoscripts"
|
CONFIGLOCATION="/config/addons_autoscripts"
|
||||||
CONFIGFILEBROWSER="/homeassistant/addons_autoscripts/$slug.sh"
|
CONFIGFILEBROWSER="/homeassistant/addons_autoscripts/$slug.sh"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Default location
|
# Default location
|
||||||
@@ -195,28 +189,28 @@ bashio::log.green "Execute $CONFIGFILEBROWSER if existing"
|
|||||||
|
|
||||||
# Download template if no script found and exit
|
# Download template if no script found and exit
|
||||||
if [ ! -f "$CONFIGSOURCE" ]; then
|
if [ ! -f "$CONFIGSOURCE" ]; then
|
||||||
TEMPLATESOURCE="https://raw.githubusercontent.com/alexbelgium/hassio-addons/master/.templates/script.template"
|
TEMPLATESOURCE="https://raw.githubusercontent.com/alexbelgium/hassio-addons/master/.templates/script.template"
|
||||||
curl -f -L -s -S "$TEMPLATESOURCE" --output "$CONFIGSOURCE" || true
|
curl -f -L -s -S "$TEMPLATESOURCE" --output "$CONFIGSOURCE" || true
|
||||||
exit 0
|
exit 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Convert scripts to linux
|
# Convert scripts to linux
|
||||||
dos2unix "$CONFIGSOURCE" > /dev/null 2>&1 || true
|
dos2unix "$CONFIGSOURCE" >/dev/null 2>&1 || true
|
||||||
chmod +x "$CONFIGSOURCE"
|
chmod +x "$CONFIGSOURCE"
|
||||||
|
|
||||||
if grep -q 'bashio' "$CONFIGSOURCE"; then
|
if grep -q 'bashio' "$CONFIGSOURCE"; then
|
||||||
sed -i "1s|^.*|#!$shebang|" "$CONFIGSOURCE"
|
sed -i "1s|^.*|#!$shebang|" "$CONFIGSOURCE"
|
||||||
else
|
else
|
||||||
sed -i '1s|^.*|#!/bin/bash|' "$CONFIGSOURCE"
|
sed -i '1s|^.*|#!/bin/bash|' "$CONFIGSOURCE"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Check if there is actual commands
|
# Check if there is actual commands
|
||||||
while IFS= read -r line; do
|
while IFS= read -r line; do
|
||||||
line="$(echo "$line" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
line="$(echo "$line" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//')"
|
||||||
|
|
||||||
if [[ -n "$line" ]] && [[ ! "$line" =~ ^# ]]; then
|
if [[ -n "$line" ]] && [[ ! "$line" =~ ^# ]]; then
|
||||||
bashio::log.green "... script found, executing"
|
bashio::log.green "... script found, executing"
|
||||||
"$CONFIGSOURCE"
|
"$CONFIGSOURCE"
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
done < "$CONFIGSOURCE"
|
done < "$CONFIGSOURCE"
|
||||||
|
|||||||
@@ -9,12 +9,12 @@ set -e
|
|||||||
# Avoid usage of local dns such as adguard home or pihole\n"
|
# Avoid usage of local dns such as adguard home or pihole\n"
|
||||||
|
|
||||||
if bashio::config.has_value 'DNS_server'; then
|
if bashio::config.has_value 'DNS_server'; then
|
||||||
if ! bashio::config.true "openvpn_enabled" && ! bashio::config.true "wireguard_enabled"; then
|
if ! bashio::config.true "openvpn_enabled" && ! bashio::config.true "wireguard_enabled"; then
|
||||||
# Define variables
|
# Define variables
|
||||||
DNSSERVER=$(bashio::config 'DNS_server')
|
DNSSERVER=$(bashio::config 'DNS_server')
|
||||||
DNS=""
|
DNS=""
|
||||||
DNSLIST=""
|
DNSLIST=""
|
||||||
|
|
||||||
# Get DNS servers
|
# Get DNS servers
|
||||||
# shellcheck disable=SC2086
|
# shellcheck disable=SC2086
|
||||||
for server in ${DNSSERVER//,/ }; do # Separate comma separated values
|
for server in ${DNSSERVER//,/ }; do # Separate comma separated values
|
||||||
@@ -26,7 +26,7 @@ if bashio::config.has_value 'DNS_server'; then
|
|||||||
bashio::log.warning "DNS $server was requested but can't be pinged. It won't be used"
|
bashio::log.warning "DNS $server was requested but can't be pinged. It won't be used"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# Only add DNS if there are DNS set
|
# Only add DNS if there are DNS set
|
||||||
# shellcheck disable=SC2236
|
# shellcheck disable=SC2236
|
||||||
if [[ -n "${DNS:-}" ]]; then
|
if [[ -n "${DNS:-}" ]]; then
|
||||||
|
|||||||
@@ -33,297 +33,293 @@ _BASHIO_COLOR=1
|
|||||||
[ "${TERM:-}" = "dumb" ] && _BASHIO_COLOR=0
|
[ "${TERM:-}" = "dumb" ] && _BASHIO_COLOR=0
|
||||||
|
|
||||||
_bashio_color() {
|
_bashio_color() {
|
||||||
[ "$_BASHIO_COLOR" = "1" ] || return 0
|
[ "$_BASHIO_COLOR" = "1" ] || return 0
|
||||||
case "${1:-}" in
|
case "${1:-}" in
|
||||||
blue) printf '\033[34m' ;;
|
blue) printf '\033[34m' ;;
|
||||||
green) printf '\033[32m' ;;
|
green) printf '\033[32m' ;;
|
||||||
yellow) printf '\033[33m' ;;
|
yellow) printf '\033[33m' ;;
|
||||||
red) printf '\033[31m' ;;
|
red) printf '\033[31m' ;;
|
||||||
magenta) printf '\033[35m' ;;
|
magenta) printf '\033[35m' ;;
|
||||||
reset) printf '\033[0m' ;;
|
reset) printf '\033[0m' ;;
|
||||||
*) printf '' ;;
|
*) printf '' ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
_bashio_log() {
|
_bashio_log() {
|
||||||
local c="${1:-}"
|
local c="${1:-}"; shift || true
|
||||||
shift || true
|
printf '%s%s%s\n' "$(_bashio_color "$c")" "$*" "$(_bashio_color reset)"
|
||||||
printf '%s%s%s\n' "$(_bashio_color "$c")" "$*" "$(_bashio_color reset)"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Helpers
|
# Helpers
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
_bashio_is_true() {
|
_bashio_is_true() {
|
||||||
case "${1:-}" in
|
case "${1:-}" in
|
||||||
1 | true | TRUE | True | yes | YES | Yes | on | ON | On) return 0 ;;
|
1|true|TRUE|True|yes|YES|Yes|on|ON|On) return 0 ;;
|
||||||
*) return 1 ;;
|
*) return 1 ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
|
|
||||||
# ENV mapping helper:
|
# ENV mapping helper:
|
||||||
# tries variants + prefixes and prints the value if env var is defined (even empty),
|
# tries variants + prefixes and prints the value if env var is defined (even empty),
|
||||||
# returning 0 when found, 1 when not found.
|
# returning 0 when found, 1 when not found.
|
||||||
_bashio_env_get() {
|
_bashio_env_get() {
|
||||||
local key="${1:-}"
|
local key="${1:-}"
|
||||||
[ -n "$key" ] || return 1
|
[ -n "$key" ] || return 1
|
||||||
|
|
||||||
local norm norm_uc raw_uc
|
local norm norm_uc raw_uc
|
||||||
norm="$(printf '%s' "$key" | tr '.-' '__')"
|
norm="$(printf '%s' "$key" | tr '.-' '__')"
|
||||||
norm_uc="$(printf '%s' "$norm" | tr '[:lower:]' '[:upper:]')"
|
norm_uc="$(printf '%s' "$norm" | tr '[:lower:]' '[:upper:]')"
|
||||||
raw_uc="$(printf '%s' "$key" | tr '[:lower:]' '[:upper:]')"
|
raw_uc="$(printf '%s' "$key" | tr '[:lower:]' '[:upper:]')"
|
||||||
|
|
||||||
local variants=(
|
local variants=(
|
||||||
"$key"
|
"$key"
|
||||||
"$raw_uc"
|
"$raw_uc"
|
||||||
"$norm"
|
"$norm"
|
||||||
"$norm_uc"
|
"$norm_uc"
|
||||||
)
|
)
|
||||||
|
|
||||||
local prefixes=("" "CFG_" "CONFIG_" "ADDON_" "OPTION_" "OPT_")
|
local prefixes=("" "CFG_" "CONFIG_" "ADDON_" "OPTION_" "OPT_")
|
||||||
|
|
||||||
local v p name
|
local v p name
|
||||||
for v in "${variants[@]}"; do
|
for v in "${variants[@]}"; do
|
||||||
for p in "${prefixes[@]}"; do
|
for p in "${prefixes[@]}"; do
|
||||||
name="${p}${v}"
|
name="${p}${v}"
|
||||||
if [ -n "${!name+x}" ]; then
|
if [ -n "${!name+x}" ]; then
|
||||||
printf '%s' "${!name}"
|
printf '%s' "${!name}"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
done
|
|
||||||
done
|
done
|
||||||
|
done
|
||||||
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# env presence (even if empty) used by config.exists
|
# env presence (even if empty) used by config.exists
|
||||||
_bashio_env_has() {
|
_bashio_env_has() {
|
||||||
local key="${1:-}"
|
local key="${1:-}"
|
||||||
[ -n "$key" ] || return 1
|
[ -n "$key" ] || return 1
|
||||||
_bashio_env_get "$key" > /dev/null 2>&1
|
_bashio_env_get "$key" >/dev/null 2>&1
|
||||||
}
|
}
|
||||||
|
|
||||||
# JSON options source (jq required). Prints value or empty; returns 0 always.
|
# JSON options source (jq required). Prints value or empty; returns 0 always.
|
||||||
_bashio_json_get() {
|
_bashio_json_get() {
|
||||||
local key="${1:-}"
|
local key="${1:-}"
|
||||||
local file="${STANDALONE_OPTIONS_JSON:-}"
|
local file="${STANDALONE_OPTIONS_JSON:-}"
|
||||||
|
|
||||||
[ -n "$key" ] || return 0
|
[ -n "$key" ] || return 0
|
||||||
[ -n "$file" ] || return 0
|
[ -n "$file" ] || return 0
|
||||||
[ -f "$file" ] || return 0
|
[ -f "$file" ] || return 0
|
||||||
command -v jq > /dev/null 2>&1 || return 0
|
command -v jq >/dev/null 2>&1 || return 0
|
||||||
|
|
||||||
# getpath(split(".")) supports nested access; missing => empty
|
# getpath(split(".")) supports nested access; missing => empty
|
||||||
jq -er --arg k "$key" 'getpath(($k|split("."))) // empty' "$file" 2> /dev/null || true
|
jq -er --arg k "$key" 'getpath(($k|split("."))) // empty' "$file" 2>/dev/null || true
|
||||||
}
|
}
|
||||||
|
|
||||||
# Net wait using /dev/tcp with a timeout
|
# Net wait using /dev/tcp with a timeout
|
||||||
_bashio_tcp_wait() {
|
_bashio_tcp_wait() {
|
||||||
local host="${1:-}" port="${2:-}" to="${3:-30}"
|
local host="${1:-}" port="${2:-}" to="${3:-30}"
|
||||||
[ -n "$host" ] && [ -n "$port" ] || return 1
|
[ -n "$host" ] && [ -n "$port" ] || return 1
|
||||||
|
|
||||||
local start now
|
local start now
|
||||||
start="$(date +%s)"
|
start="$(date +%s)"
|
||||||
while :; do
|
while :; do
|
||||||
if exec 3<> "/dev/tcp/${host}/${port}" 2> /dev/null; then
|
if exec 3<>"/dev/tcp/${host}/${port}" 2>/dev/null; then
|
||||||
exec 3>&- 3<&-
|
exec 3>&- 3<&-
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
now="$(date +%s)"
|
now="$(date +%s)"
|
||||||
if [ $((now - start)) -ge "$to" ]; then
|
if [ $((now - start)) -ge "$to" ]; then
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
sleep 1
|
sleep 1
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
# Prefer nc if present, fallback to /dev/tcp
|
# Prefer nc if present, fallback to /dev/tcp
|
||||||
_bashio_tcp_wait_nc() {
|
_bashio_tcp_wait_nc() {
|
||||||
command -v nc > /dev/null 2>&1 || return 1
|
command -v nc >/dev/null 2>&1 || return 1
|
||||||
local host="${1:-}" port="${2:-}" to="${3:-30}"
|
local host="${1:-}" port="${2:-}" to="${3:-30}"
|
||||||
# BusyBox and OpenBSD nc differ; cover both styles
|
# BusyBox and OpenBSD nc differ; cover both styles
|
||||||
nc -z -w "$to" "$host" "$port" 2> /dev/null || nc -z "$host" "$port" 2> /dev/null
|
nc -z -w "$to" "$host" "$port" 2>/dev/null || nc -z "$host" "$port" 2>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Logging API
|
# Logging API
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
bashio::log.blue() { _bashio_log blue "$*"; }
|
bashio::log.blue() { _bashio_log blue "$*"; }
|
||||||
bashio::log.green() { _bashio_log green "$*"; }
|
bashio::log.green() { _bashio_log green "$*"; }
|
||||||
bashio::log.yellow() { _bashio_log yellow "$*"; }
|
bashio::log.yellow() { _bashio_log yellow "$*"; }
|
||||||
bashio::log.red() { _bashio_log red "$*"; }
|
bashio::log.red() { _bashio_log red "$*"; }
|
||||||
bashio::log.magenta() { _bashio_log magenta "$*"; }
|
bashio::log.magenta() { _bashio_log magenta "$*"; }
|
||||||
|
|
||||||
# Common aliases
|
# Common aliases
|
||||||
bashio::log.info() { bashio::log.blue "$@"; }
|
bashio::log.info() { bashio::log.blue "$@"; }
|
||||||
bashio::log.warning() { bashio::log.yellow "$@"; }
|
bashio::log.warning() { bashio::log.yellow "$@"; }
|
||||||
bashio::log.error() { bashio::log.red "$@"; }
|
bashio::log.error() { bashio::log.red "$@"; }
|
||||||
bashio::log.debug() { printf '%s\n' "$*"; }
|
bashio::log.debug() { printf '%s\n' "$*"; }
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Supervisor shim
|
# Supervisor shim
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
bashio::supervisor.ping() {
|
bashio::supervisor.ping() {
|
||||||
_bashio_is_true "${STANDALONE_FORCE_SUPERVISOR_PING:-}" && return 0
|
_bashio_is_true "${STANDALONE_FORCE_SUPERVISOR_PING:-}" && return 0
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Add-on metadata
|
# Add-on metadata
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
bashio::addon.name() { printf '%s' "${ADDON_NAME:-Standalone container}"; }
|
bashio::addon.name() { printf '%s' "${ADDON_NAME:-Standalone container}"; }
|
||||||
bashio::addon.description() { printf '%s' "${ADDON_DESCRIPTION:-Running without Home Assistant Supervisor}"; }
|
bashio::addon.description() { printf '%s' "${ADDON_DESCRIPTION:-Running without Home Assistant Supervisor}"; }
|
||||||
bashio::addon.version() { printf '%s' "${BUILD_VERSION:-1.0}"; }
|
bashio::addon.version() { printf '%s' "${BUILD_VERSION:-1.0}"; }
|
||||||
bashio::addon.version_latest() { printf '%s' "${ADDON_VERSION_LATEST:-${BUILD_VERSION:-1.0}}"; }
|
bashio::addon.version_latest(){ printf '%s' "${ADDON_VERSION_LATEST:-${BUILD_VERSION:-1.0}}"; }
|
||||||
|
|
||||||
bashio::addon.update_available() {
|
bashio::addon.update_available() {
|
||||||
if [ -n "${ADDON_VERSION_LATEST:-}" ] && [ "${ADDON_VERSION_LATEST:-}" != "${BUILD_VERSION:-}" ]; then
|
if [ -n "${ADDON_VERSION_LATEST:-}" ] && [ "${ADDON_VERSION_LATEST:-}" != "${BUILD_VERSION:-}" ]; then
|
||||||
printf '%s' "true"
|
printf '%s' "true"
|
||||||
else
|
else
|
||||||
printf '%s' "false"
|
printf '%s' "false"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
bashio::addon.ingress_port() { printf '%s' "${ADDON_INGRESS_PORT:-}"; }
|
bashio::addon.ingress_port() { printf '%s' "${ADDON_INGRESS_PORT:-}"; }
|
||||||
bashio::addon.ingress_entry() { printf '%s' "${ADDON_INGRESS_ENTRY:-}"; }
|
bashio::addon.ingress_entry() { printf '%s' "${ADDON_INGRESS_ENTRY:-}"; }
|
||||||
bashio::addon.ip_address() { printf '%s' "${ADDON_IP_ADDRESS:-}"; }
|
bashio::addon.ip_address() { printf '%s' "${ADDON_IP_ADDRESS:-}"; }
|
||||||
|
|
||||||
# Ports:
|
# Ports:
|
||||||
# - numeric arg "8080" -> env PORT_8080 or ADDON_PORT_8080, fallback to the number
|
# - numeric arg "8080" -> env PORT_8080 or ADDON_PORT_8080, fallback to the number
|
||||||
# - non-numeric "WEB_PORT" -> resolve as config/env key
|
# - non-numeric "WEB_PORT" -> resolve as config/env key
|
||||||
bashio::addon.port() {
|
bashio::addon.port() {
|
||||||
local arg="${1:-}"
|
local arg="${1:-}"
|
||||||
if [[ "$arg" =~ ^[0-9]+$ ]]; then
|
if [[ "$arg" =~ ^[0-9]+$ ]]; then
|
||||||
local v=""
|
local v=""
|
||||||
v="$(_bashio_env_get "PORT_${arg}" 2> /dev/null || true)"
|
v="$(_bashio_env_get "PORT_${arg}" 2>/dev/null || true)"
|
||||||
[ -z "$v" ] && v="$(_bashio_env_get "ADDON_PORT_${arg}" 2> /dev/null || true)"
|
[ -z "$v" ] && v="$(_bashio_env_get "ADDON_PORT_${arg}" 2>/dev/null || true)"
|
||||||
printf '%s' "${v:-$arg}"
|
printf '%s' "${v:-$arg}"
|
||||||
else
|
else
|
||||||
printf '%s' "$(_bashio_env_get "$arg" 2> /dev/null || true)"
|
printf '%s' "$(_bashio_env_get "$arg" 2>/dev/null || true)"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# addon.option : write/delete option in JSON when possible; fallback export env
|
# addon.option : write/delete option in JSON when possible; fallback export env
|
||||||
bashio::addon.option() {
|
bashio::addon.option() {
|
||||||
local key="${1:-}" value="${2-__BASHIO_UNSET__}" file="${STANDALONE_OPTIONS_JSON:-}"
|
local key="${1:-}" value="${2-__BASHIO_UNSET__}" file="${STANDALONE_OPTIONS_JSON:-}"
|
||||||
[ -n "$key" ] || return 0
|
[ -n "$key" ] || return 0
|
||||||
|
|
||||||
if [ -n "$file" ] && [ -f "$file" ] && command -v jq > /dev/null 2>&1; then
|
if [ -n "$file" ] && [ -f "$file" ] && command -v jq >/dev/null 2>&1; then
|
||||||
local tmp
|
local tmp
|
||||||
tmp="$(mktemp)"
|
tmp="$(mktemp)"
|
||||||
if [ "$value" = "__BASHIO_UNSET__" ]; then
|
if [ "$value" = "__BASHIO_UNSET__" ]; then
|
||||||
jq --arg k "$key" 'delpath(($k|split(".")))' "$file" > "$tmp" && mv "$tmp" "$file"
|
jq --arg k "$key" 'delpath(($k|split(".")))' "$file" >"$tmp" && mv "$tmp" "$file"
|
||||||
else
|
else
|
||||||
jq --arg k "$key" --arg v "$value" 'setpath(($k|split(".")); $v)' "$file" > "$tmp" && mv "$tmp" "$file"
|
jq --arg k "$key" --arg v "$value" 'setpath(($k|split(".")); $v)' "$file" >"$tmp" && mv "$tmp" "$file"
|
||||||
fi
|
|
||||||
return 0
|
|
||||||
fi
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
# Fallback: export as env (dot/dash -> underscore). Delete becomes no-op.
|
# Fallback: export as env (dot/dash -> underscore). Delete becomes no-op.
|
||||||
if [ "$value" != "__BASHIO_UNSET__" ]; then
|
if [ "$value" != "__BASHIO_UNSET__" ]; then
|
||||||
export "$(printf '%s' "$key" | tr '.-' '__')"="$value"
|
export "$(printf '%s' "$key" | tr '.-' '__')"="$value"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# System info
|
# System info
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
bashio::info.operating_system() {
|
bashio::info.operating_system() {
|
||||||
if [ -r /etc/os-release ]; then
|
if [ -r /etc/os-release ]; then
|
||||||
# shellcheck disable=SC1091
|
# shellcheck disable=SC1091
|
||||||
. /etc/os-release
|
. /etc/os-release
|
||||||
printf '%s' "${PRETTY_NAME:-${NAME:-Linux}}"
|
printf '%s' "${PRETTY_NAME:-${NAME:-Linux}}"
|
||||||
else
|
else
|
||||||
printf '%s' "Linux"
|
printf '%s' "Linux"
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
bashio::info.arch() { uname -m; }
|
bashio::info.arch() { uname -m; }
|
||||||
bashio::info.machine() { uname -m; }
|
bashio::info.machine() { uname -m; }
|
||||||
bashio::info.homeassistant() { printf '%s' "standalone"; }
|
bashio::info.homeassistant(){ printf '%s' "standalone"; }
|
||||||
bashio::info.supervisor() { printf '%s' "standalone"; }
|
bashio::info.supervisor() { printf '%s' "standalone"; }
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Config API
|
# Config API
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
bashio::config() {
|
bashio::config() {
|
||||||
local key="${1:-}"
|
local key="${1:-}"
|
||||||
[ -n "$key" ] || {
|
[ -n "$key" ] || { printf '%s' ""; return 0; }
|
||||||
printf '%s' ""
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
local v=""
|
local v=""
|
||||||
if _bashio_env_get "$key" > /dev/null 2>&1; then
|
if _bashio_env_get "$key" >/dev/null 2>&1; then
|
||||||
v="$(_bashio_env_get "$key" 2> /dev/null || true)"
|
v="$(_bashio_env_get "$key" 2>/dev/null || true)"
|
||||||
fi
|
fi
|
||||||
[ -z "$v" ] && v="$(_bashio_json_get "$key")"
|
[ -z "$v" ] && v="$(_bashio_json_get "$key")"
|
||||||
printf '%s' "${v:-}"
|
printf '%s' "${v:-}"
|
||||||
}
|
}
|
||||||
|
|
||||||
bashio::config.has_value() { [ -n "$(bashio::config "$1")" ]; }
|
bashio::config.has_value() { [ -n "$(bashio::config "$1")" ]; }
|
||||||
|
|
||||||
bashio::config.true() {
|
bashio::config.true() {
|
||||||
_bashio_is_true "$(bashio::config "$1")"
|
_bashio_is_true "$(bashio::config "$1")"
|
||||||
}
|
}
|
||||||
|
|
||||||
# config.exists : key is present (env or JSON), even if value is empty
|
# config.exists : key is present (env or JSON), even if value is empty
|
||||||
bashio::config.exists() {
|
bashio::config.exists() {
|
||||||
local key="${1:-}" file="${STANDALONE_OPTIONS_JSON:-}"
|
local key="${1:-}" file="${STANDALONE_OPTIONS_JSON:-}"
|
||||||
[ -n "$key" ] || return 1
|
[ -n "$key" ] || return 1
|
||||||
|
|
||||||
if _bashio_env_has "$key"; then
|
if _bashio_env_has "$key"; then
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "$file" ] && [ -f "$file" ] && command -v jq > /dev/null 2>&1; then
|
if [ -n "$file" ] && [ -f "$file" ] && command -v jq >/dev/null 2>&1; then
|
||||||
jq -e --arg k "$key" 'haspath(($k|split(".")))' "$file" > /dev/null 2>&1
|
jq -e --arg k "$key" 'haspath(($k|split(".")))' "$file" >/dev/null 2>&1
|
||||||
return $?
|
return $?
|
||||||
fi
|
fi
|
||||||
|
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
# Common "require.*" shims (advisory/no-op in standalone)
|
# Common "require.*" shims (advisory/no-op in standalone)
|
||||||
bashio::config.require.ssl() { printf '%s' "${REQUIRE_SSL:-true}"; }
|
bashio::config.require.ssl() { printf '%s' "${REQUIRE_SSL:-true}"; }
|
||||||
bashio::config.require.username() { :; }
|
bashio::config.require.username() { :; }
|
||||||
bashio::config.require.password() { :; }
|
bashio::config.require.password() { :; }
|
||||||
bashio::config.require.port() { :; }
|
bashio::config.require.port() { :; }
|
||||||
|
|
||||||
# config.array:
|
# config.array:
|
||||||
# Accepts CSV ("a,b,c"), space/newline-separated text, or JSON array ["a","b"].
|
# Accepts CSV ("a,b,c"), space/newline-separated text, or JSON array ["a","b"].
|
||||||
# Prints one item per line.
|
# Prints one item per line.
|
||||||
bashio::config.array() {
|
bashio::config.array() {
|
||||||
local key="${1:-}" raw
|
local key="${1:-}" raw
|
||||||
raw="$(bashio::config "$key")"
|
raw="$(bashio::config "$key")"
|
||||||
[ -n "$raw" ] || return 0
|
[ -n "$raw" ] || return 0
|
||||||
|
|
||||||
if command -v jq > /dev/null 2>&1 && printf '%s' "$raw" | jq -e . > /dev/null 2>&1; then
|
if command -v jq >/dev/null 2>&1 && printf '%s' "$raw" | jq -e . >/dev/null 2>&1; then
|
||||||
printf '%s' "$raw" | jq -r '.[]' 2> /dev/null && return 0
|
printf '%s' "$raw" | jq -r '.[]' 2>/dev/null && return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if printf '%s' "$raw" | grep -q ','; then
|
if printf '%s' "$raw" | grep -q ','; then
|
||||||
printf '%s' "$raw" | tr ',' '\n'
|
printf '%s' "$raw" | tr ',' '\n'
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
printf '%s\n' "$raw"
|
printf '%s\n' "$raw"
|
||||||
}
|
}
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# var helpers
|
# var helpers
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
bashio::var.true() { _bashio_is_true "${1:-}"; }
|
bashio::var.true() { _bashio_is_true "${1:-}"; }
|
||||||
bashio::var.false() { ! _bashio_is_true "${1:-}"; }
|
bashio::var.false() { ! _bashio_is_true "${1:-}"; }
|
||||||
bashio::var.has_value() { [ -n "${1:-}" ]; }
|
bashio::var.has_value() { [ -n "${1:-}" ]; }
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Filesystem helpers
|
# Filesystem helpers
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
bashio::fs.file_exists() { [ -f "${1:-}" ]; }
|
bashio::fs.file_exists() { [ -f "${1:-}" ]; }
|
||||||
bashio::fs.directory_exists() { [ -d "${1:-}" ]; }
|
bashio::fs.directory_exists() { [ -d "${1:-}" ]; }
|
||||||
bashio::fs.file_contains() {
|
bashio::fs.file_contains() {
|
||||||
local f="${1:-}" p="${2:-}"
|
local f="${1:-}" p="${2:-}"
|
||||||
[ -f "$f" ] && grep -q -- "$p" "$f" 2> /dev/null
|
[ -f "$f" ] && grep -q -- "$p" "$f" 2>/dev/null
|
||||||
}
|
}
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -331,25 +327,25 @@ bashio::fs.file_contains() {
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Wait for TCP service: bashio::net.wait_for host port [timeout]
|
# Wait for TCP service: bashio::net.wait_for host port [timeout]
|
||||||
bashio::net.wait_for() {
|
bashio::net.wait_for() {
|
||||||
local host="${1:-}" port="${2:-}" to="${3:-30}"
|
local host="${1:-}" port="${2:-}" to="${3:-30}"
|
||||||
_bashio_tcp_wait_nc "$host" "$port" "$to" && return 0
|
_bashio_tcp_wait_nc "$host" "$port" "$to" && return 0
|
||||||
_bashio_tcp_wait "$host" "$port" "$to"
|
_bashio_tcp_wait "$host" "$port" "$to"
|
||||||
}
|
}
|
||||||
|
|
||||||
# DNS helper: bashio::dns.host <hostname> -> prints an IP (or empty)
|
# DNS helper: bashio::dns.host <hostname> -> prints an IP (or empty)
|
||||||
bashio::dns.host() {
|
bashio::dns.host() {
|
||||||
local h="${1:-}"
|
local h="${1:-}"
|
||||||
[ -n "$h" ] || return 1
|
[ -n "$h" ] || return 1
|
||||||
if command -v getent > /dev/null 2>&1; then
|
if command -v getent >/dev/null 2>&1; then
|
||||||
getent ahostsv4 "$h" | awk '{print $1; exit}'
|
getent ahostsv4 "$h" | awk '{print $1; exit}'
|
||||||
else
|
else
|
||||||
nslookup "$h" 2> /dev/null | awk '/^Address: /{print $2; exit}'
|
nslookup "$h" 2>/dev/null | awk '/^Address: /{print $2; exit}'
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
# Hostname
|
# Hostname
|
||||||
bashio::host.hostname() {
|
bashio::host.hostname() {
|
||||||
command -v hostname > /dev/null 2>&1 && hostname || printf '%s' "${HOSTNAME:-unknown}"
|
command -v hostname >/dev/null 2>&1 && hostname || printf '%s' "${HOSTNAME:-unknown}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -359,43 +355,40 @@ bashio::host.hostname() {
|
|||||||
# bashio::services "mqtt" "host"
|
# bashio::services "mqtt" "host"
|
||||||
# bashio::services.available "mqtt"
|
# bashio::services.available "mqtt"
|
||||||
bashio::services() {
|
bashio::services() {
|
||||||
local svc="${1:-}" key="${2:-}"
|
local svc="${1:-}" key="${2:-}"
|
||||||
[ -n "$svc" ] && [ -n "$key" ] || {
|
[ -n "$svc" ] && [ -n "$key" ] || { printf '%s' ""; return 0; }
|
||||||
printf '%s' ""
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
local upper svc_upper var v=""
|
local upper svc_upper var v=""
|
||||||
upper="$(printf '%s' "$key" | tr '[:lower:]' '[:upper:]')"
|
upper="$(printf '%s' "$key" | tr '[:lower:]' '[:upper:]')"
|
||||||
svc_upper="$(printf '%s' "$svc" | tr '[:lower:]' '[:upper:]')"
|
svc_upper="$(printf '%s' "$svc" | tr '[:lower:]' '[:upper:]')"
|
||||||
|
|
||||||
# Common mappings
|
# Common mappings
|
||||||
case "$svc_upper:$upper" in
|
case "$svc_upper:$upper" in
|
||||||
MQTT:HOST) var="MQTT_HOST" ;;
|
MQTT:HOST) var="MQTT_HOST" ;;
|
||||||
MQTT:PORT) var="MQTT_PORT" ;;
|
MQTT:PORT) var="MQTT_PORT" ;;
|
||||||
MQTT:USERNAME) var="MQTT_USER" ;;
|
MQTT:USERNAME) var="MQTT_USER" ;;
|
||||||
MQTT:PASSWORD) var="MQTT_PASSWORD" ;;
|
MQTT:PASSWORD) var="MQTT_PASSWORD" ;;
|
||||||
MQTT:TLS) var="MQTT_TLS" ;;
|
MQTT:TLS) var="MQTT_TLS" ;;
|
||||||
MYSQL:HOST | MARIADB:HOST) var="DB_HOST" ;;
|
MYSQL:HOST|MARIADB:HOST) var="DB_HOST" ;;
|
||||||
MYSQL:PORT | MARIADB:PORT) var="DB_PORT" ;;
|
MYSQL:PORT|MARIADB:PORT) var="DB_PORT" ;;
|
||||||
MYSQL:USERNAME | MARIADB:USERNAME) var="DB_USER" ;;
|
MYSQL:USERNAME|MARIADB:USERNAME) var="DB_USER" ;;
|
||||||
MYSQL:PASSWORD | MARIADB:PASSWORD) var="DB_PASSWORD" ;;
|
MYSQL:PASSWORD|MARIADB:PASSWORD) var="DB_PASSWORD" ;;
|
||||||
MYSQL:DATABASE | MARIADB:DATABASE) var="DB_NAME" ;;
|
MYSQL:DATABASE|MARIADB:DATABASE) var="DB_NAME" ;;
|
||||||
*) var="${svc_upper}_${upper}" ;;
|
*) var="${svc_upper}_${upper}" ;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
v="$(_bashio_env_get "$var" 2> /dev/null || true)"
|
v="$(_bashio_env_get "$var" 2>/dev/null || true)"
|
||||||
if [ -z "$v" ]; then
|
if [ -z "$v" ]; then
|
||||||
v="$(_bashio_json_get "services.${svc}.${key}")"
|
v="$(_bashio_json_get "services.${svc}.${key}")"
|
||||||
[ -z "$v" ] && v="$(_bashio_json_get "${svc}.${key}")"
|
[ -z "$v" ] && v="$(_bashio_json_get "${svc}.${key}")"
|
||||||
fi
|
fi
|
||||||
printf '%s' "${v:-}"
|
printf '%s' "${v:-}"
|
||||||
}
|
}
|
||||||
|
|
||||||
bashio::services.available() {
|
bashio::services.available() {
|
||||||
local svc="${1:-}" host
|
local svc="${1:-}" host
|
||||||
host="$(bashio::services "$svc" "host")"
|
host="$(bashio::services "$svc" "host")"
|
||||||
[ -n "$host" ]
|
[ -n "$host" ]
|
||||||
}
|
}
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
@@ -403,43 +396,39 @@ bashio::services.available() {
|
|||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
mkdir -p "$BASHIO_CACHE_DIR"
|
mkdir -p "$BASHIO_CACHE_DIR"
|
||||||
bashio::cache.exists() { [ -f "$BASHIO_CACHE_DIR/${1}.cache" ]; }
|
bashio::cache.exists() { [ -f "$BASHIO_CACHE_DIR/${1}.cache" ]; }
|
||||||
bashio::cache.get() { [ -f "$BASHIO_CACHE_DIR/${1}.cache" ] && cat "$BASHIO_CACHE_DIR/${1}.cache"; }
|
bashio::cache.get() { [ -f "$BASHIO_CACHE_DIR/${1}.cache" ] && cat "$BASHIO_CACHE_DIR/${1}.cache"; }
|
||||||
bashio::cache.set() { printf '%s' "${2:-}" > "$BASHIO_CACHE_DIR/${1}.cache"; }
|
bashio::cache.set() { printf '%s' "${2:-}" > "$BASHIO_CACHE_DIR/${1}.cache"; }
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# jq wrapper (some add-ons call bashio::jq)
|
# jq wrapper (some add-ons call bashio::jq)
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
bashio::jq() { command -v jq > /dev/null 2>&1 && jq "$@"; }
|
bashio::jq() { command -v jq >/dev/null 2>&1 && jq "$@"; }
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Home Assistant token
|
# Home Assistant token
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
bashio::homeassistant.token() {
|
bashio::homeassistant.token() {
|
||||||
local t="${HOMEASSISTANT_TOKEN:-${HASS_TOKEN:-}}"
|
local t="${HOMEASSISTANT_TOKEN:-${HASS_TOKEN:-}}"
|
||||||
if [ -z "$t" ] && [ -n "${STANDALONE_OPTIONS_JSON:-}" ] && [ -f "${STANDALONE_OPTIONS_JSON:-}" ] && command -v jq > /dev/null 2>&1; then
|
if [ -z "$t" ] && [ -n "${STANDALONE_OPTIONS_JSON:-}" ] && [ -f "${STANDALONE_OPTIONS_JSON:-}" ] && command -v jq >/dev/null 2>&1; then
|
||||||
t="$(jq -er '.homeassistant.token // empty' "$STANDALONE_OPTIONS_JSON" 2> /dev/null || true)"
|
t="$(jq -er '.homeassistant.token // empty' "$STANDALONE_OPTIONS_JSON" 2>/dev/null || true)"
|
||||||
fi
|
fi
|
||||||
printf '%s' "${t:-}"
|
printf '%s' "${t:-}"
|
||||||
}
|
}
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Exit helpers
|
# Exit helpers
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
bashio::exit.ok() { exit 0; }
|
bashio::exit.ok() { exit 0; }
|
||||||
bashio::exit.nok() {
|
bashio::exit.nok() { local m="${1:-}"; [ -n "$m" ] && bashio::log.red "$m"; exit 1; }
|
||||||
local m="${1:-}"
|
|
||||||
[ -n "$m" ] && bashio::log.red "$m"
|
|
||||||
exit 1
|
|
||||||
}
|
|
||||||
|
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Core config check shim
|
# Core config check shim
|
||||||
# -----------------------------------------------------------------------------
|
# -----------------------------------------------------------------------------
|
||||||
# Set STANDALONE_CORE_CHECK_CMD="hass --script check_config -c /config" to enable
|
# Set STANDALONE_CORE_CHECK_CMD="hass --script check_config -c /config" to enable
|
||||||
bashio::core.check() {
|
bashio::core.check() {
|
||||||
if [ -n "${STANDALONE_CORE_CHECK_CMD:-}" ]; then
|
if [ -n "${STANDALONE_CORE_CHECK_CMD:-}" ]; then
|
||||||
eval "$STANDALONE_CORE_CHECK_CMD"
|
eval "$STANDALONE_CORE_CHECK_CMD"
|
||||||
else
|
else
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -41,14 +41,14 @@ restore_apt_sources_if_missing() {
|
|||||||
mirror="http://ports.ubuntu.com/ubuntu-ports"
|
mirror="http://ports.ubuntu.com/ubuntu-ports"
|
||||||
security_mirror="$mirror"
|
security_mirror="$mirror"
|
||||||
fi
|
fi
|
||||||
cat > /etc/apt/sources.list << EOF2
|
cat > /etc/apt/sources.list <<EOF2
|
||||||
deb ${mirror} ${codename} main restricted universe multiverse
|
deb ${mirror} ${codename} main restricted universe multiverse
|
||||||
deb ${mirror} ${codename}-updates main restricted universe multiverse
|
deb ${mirror} ${codename}-updates main restricted universe multiverse
|
||||||
deb ${security_mirror} ${codename}-security main restricted universe multiverse
|
deb ${security_mirror} ${codename}-security main restricted universe multiverse
|
||||||
EOF2
|
EOF2
|
||||||
;;
|
;;
|
||||||
debian)
|
debian)
|
||||||
cat > /etc/apt/sources.list << EOF2
|
cat > /etc/apt/sources.list <<EOF2
|
||||||
deb http://deb.debian.org/debian ${codename} main contrib non-free non-free-firmware
|
deb http://deb.debian.org/debian ${codename} main contrib non-free non-free-firmware
|
||||||
deb http://deb.debian.org/debian ${codename}-updates main contrib non-free non-free-firmware
|
deb http://deb.debian.org/debian ${codename}-updates main contrib non-free non-free-firmware
|
||||||
deb http://deb.debian.org/debian-security ${codename}-security main contrib non-free non-free-firmware
|
deb http://deb.debian.org/debian-security ${codename}-security main contrib non-free non-free-firmware
|
||||||
|
|||||||
@@ -184,7 +184,7 @@ done
|
|||||||
if [ "$PACKMANAGER" = "apt" ]; then apt-get update > /dev/null; fi
|
if [ "$PACKMANAGER" = "apt" ]; then apt-get update > /dev/null; fi
|
||||||
if [ "$PACKMANAGER" = "pacman" ]; then pacman -Sy > /dev/null; fi
|
if [ "$PACKMANAGER" = "pacman" ]; then pacman -Sy > /dev/null; fi
|
||||||
if [ "$PACKMANAGER" = "apk" ] && [ -f /etc/apk/repositories ] && ! grep -q "community" /etc/apk/repositories; then
|
if [ "$PACKMANAGER" = "apk" ] && [ -f /etc/apk/repositories ] && ! grep -q "community" /etc/apk/repositories; then
|
||||||
ALPINE_VER=$(cat /etc/alpine-release 2> /dev/null | cut -d. -f1,2)
|
ALPINE_VER=$(cat /etc/alpine-release 2>/dev/null | cut -d. -f1,2)
|
||||||
if [ -n "$ALPINE_VER" ]; then
|
if [ -n "$ALPINE_VER" ]; then
|
||||||
echo "https://dl-cdn.alpinelinux.org/alpine/v${ALPINE_VER}/community" >> /etc/apk/repositories
|
echo "https://dl-cdn.alpinelinux.org/alpine/v${ALPINE_VER}/community" >> /etc/apk/repositories
|
||||||
apk update > /dev/null
|
apk update > /dev/null
|
||||||
@@ -228,7 +228,7 @@ for files in "/etc/services.d" "/etc/cont-init.d"; do
|
|||||||
if ! ls $files 1> /dev/null 2>&1; then continue; fi
|
if ! ls $files 1> /dev/null 2>&1; then continue; fi
|
||||||
|
|
||||||
# Bashio
|
# Bashio
|
||||||
if grep -q -rnw "${files}" -e 'bashio' 2> /dev/null && [ ! -f "/usr/bin/bashio" ]; then
|
if grep -q -rnw "${files}" -e 'bashio' 2>/dev/null && [ ! -f "/usr/bin/bashio" ]; then
|
||||||
[ "$VERBOSE" = true ] && echo "install bashio"
|
[ "$VERBOSE" = true ] && echo "install bashio"
|
||||||
BASHIO_VERSION="v0.17.5"
|
BASHIO_VERSION="v0.17.5"
|
||||||
mkdir -p /tmp/bashio
|
mkdir -p /tmp/bashio
|
||||||
|
|||||||
@@ -7,13 +7,13 @@
|
|||||||
|
|
||||||
PID1=false
|
PID1=false
|
||||||
if [ "$$" -eq 1 ]; then
|
if [ "$$" -eq 1 ]; then
|
||||||
PID1=true
|
PID1=true
|
||||||
echo "Starting as entrypoint"
|
echo "Starting as entrypoint"
|
||||||
if [ -d /command ]; then
|
if [ -d /command ]; then
|
||||||
ln -sf /command/* /usr/bin/ 2> /dev/null || true
|
ln -sf /command/* /usr/bin/ 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
echo "Starting custom scripts"
|
echo "Starting custom scripts"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
##########################################
|
##########################################
|
||||||
@@ -28,40 +28,40 @@ fi
|
|||||||
# mid-write by the move -- only PID 1 is signalled, and bash runs a trap at a command
|
# 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.
|
# boundary, so whatever external command is in flight reaps first.
|
||||||
terminate() {
|
terminate() {
|
||||||
local local_pid
|
local local_pid
|
||||||
# Best-effort, so errexit must not apply: this runs with `set -e` in force (validate_shebang
|
# 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
|
# 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
|
# 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
|
# 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
|
# 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.
|
# fails whenever the stop arrives before the database is up.
|
||||||
set +e
|
set +e
|
||||||
echo "Termination signal received, forwarding to subprocesses..."
|
echo "Termination signal received, forwarding to subprocesses..."
|
||||||
if command -v pgrep > /dev/null 2>&1; then
|
if command -v pgrep >/dev/null 2>&1; then
|
||||||
while read -r pid; do
|
while read -r pid; do
|
||||||
[ -n "$pid" ] || continue
|
[ -n "$pid" ] || continue
|
||||||
echo "Terminating child PID $pid"
|
echo "Terminating child PID $pid"
|
||||||
kill -TERM "$pid" 2> /dev/null || echo "Failed to terminate PID $pid"
|
kill -TERM "$pid" 2>/dev/null || echo "Failed to terminate PID $pid"
|
||||||
done < <(pgrep -P "$$" || true)
|
done < <(pgrep -P "$$" || true)
|
||||||
else
|
else
|
||||||
for p in /proc/[0-9]*/; do
|
for p in /proc/[0-9]*/; do
|
||||||
local_pid="${p#/proc/}"
|
local_pid="${p#/proc/}"
|
||||||
local_pid="${local_pid%/}"
|
local_pid="${local_pid%/}"
|
||||||
if [ "$local_pid" -ne 1 ] && grep -q "^PPid:[[:space:]]*$$" "/proc/$local_pid/status" 2> /dev/null; then
|
if [ "$local_pid" -ne 1 ] && grep -q "^PPid:[[:space:]]*$$" "/proc/$local_pid/status" 2>/dev/null; then
|
||||||
echo "Terminating child PID $local_pid"
|
echo "Terminating child PID $local_pid"
|
||||||
kill -TERM "$local_pid" 2> /dev/null || echo "Failed to terminate PID $local_pid"
|
kill -TERM "$local_pid" 2>/dev/null || echo "Failed to terminate PID $local_pid"
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
wait || true
|
wait || true
|
||||||
echo "All subprocesses terminated. Exiting."
|
echo "All subprocesses terminated. Exiting."
|
||||||
exit 0
|
exit 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# Only when this script is PID 1. Under Docker's own init the entrypoint is an ordinary
|
# 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.
|
# child and the signal handling above belongs to that init, not to us.
|
||||||
if $PID1; then
|
if $PID1; then
|
||||||
trap terminate SIGTERM SIGINT
|
trap terminate SIGTERM SIGINT
|
||||||
fi
|
fi
|
||||||
|
|
||||||
##########################################
|
##########################################
|
||||||
@@ -69,37 +69,31 @@ fi
|
|||||||
##########################################
|
##########################################
|
||||||
|
|
||||||
pick_exec_dir() {
|
pick_exec_dir() {
|
||||||
# Prefer locations that are commonly exec-capable in containers
|
# Prefer locations that are commonly exec-capable in containers
|
||||||
# and writable. Avoid /tmp because it may be mounted noexec.
|
# and writable. Avoid /tmp because it may be mounted noexec.
|
||||||
local d
|
local d
|
||||||
for d in /dev/shm /run /var/run /mnt /root /; do
|
for d in /dev/shm /run /var/run /mnt /root /; do
|
||||||
if [ -d "$d" ] && [ -w "$d" ]; then
|
if [ -d "$d" ] && [ -w "$d" ]; then
|
||||||
# Create a tiny test executable to confirm "exec" works
|
# Create a tiny test executable to confirm "exec" works
|
||||||
local t="${d%/}/.exec_test_$$"
|
local t="${d%/}/.exec_test_$$"
|
||||||
printf '#!/bin/sh\necho ok\n' > "$t" 2> /dev/null || {
|
printf '#!/bin/sh\necho ok\n' >"$t" 2>/dev/null || { rm -f "$t" 2>/dev/null || true; continue; }
|
||||||
rm -f "$t" 2> /dev/null || true
|
chmod 700 "$t" 2>/dev/null || { rm -f "$t" 2>/dev/null || true; continue; }
|
||||||
continue
|
if "$t" >/dev/null 2>&1; then
|
||||||
}
|
rm -f "$t" 2>/dev/null || true
|
||||||
chmod 700 "$t" 2> /dev/null || {
|
echo "$d"
|
||||||
rm -f "$t" 2> /dev/null || true
|
return 0
|
||||||
continue
|
fi
|
||||||
}
|
rm -f "$t" 2>/dev/null || true
|
||||||
if "$t" > /dev/null 2>&1; then
|
fi
|
||||||
rm -f "$t" 2> /dev/null || true
|
done
|
||||||
echo "$d"
|
return 1
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
rm -f "$t" 2> /dev/null || true
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
return 1
|
|
||||||
}
|
}
|
||||||
|
|
||||||
EXEC_DIR="$(pick_exec_dir || true)"
|
EXEC_DIR="$(pick_exec_dir || true)"
|
||||||
if [ -z "${EXEC_DIR:-}" ]; then
|
if [ -z "${EXEC_DIR:-}" ]; then
|
||||||
echo "ERROR: Could not find an exec-capable writable directory (e.g., /dev/shm,/run)."
|
echo "ERROR: Could not find an exec-capable writable directory (e.g., /dev/shm,/run)."
|
||||||
echo "Your environment likely mounts all writable dirs as noexec; shebang validation cannot run safely."
|
echo "Your environment likely mounts all writable dirs as noexec; shebang validation cannot run safely."
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
######################
|
######################
|
||||||
@@ -107,14 +101,14 @@ fi
|
|||||||
######################
|
######################
|
||||||
|
|
||||||
candidate_shebangs=(
|
candidate_shebangs=(
|
||||||
"/command/with-contenv bashio"
|
"/command/with-contenv bashio"
|
||||||
"/usr/bin/with-contenv bashio"
|
"/usr/bin/with-contenv bashio"
|
||||||
"/usr/bin/env bashio"
|
"/usr/bin/env bashio"
|
||||||
"/usr/bin/bashio"
|
"/usr/bin/bashio"
|
||||||
"/usr/bin/bash"
|
"/usr/bin/bash"
|
||||||
"/bin/bash"
|
"/bin/bash"
|
||||||
"/usr/bin/sh"
|
"/usr/bin/sh"
|
||||||
"/bin/sh"
|
"/bin/sh"
|
||||||
)
|
)
|
||||||
|
|
||||||
SHEBANG_ERRORS=()
|
SHEBANG_ERRORS=()
|
||||||
@@ -158,68 +152,68 @@ echo "${_bv:-PROBE_OK}"
|
|||||||
'
|
'
|
||||||
|
|
||||||
validate_shebang() {
|
validate_shebang() {
|
||||||
local candidate="$1"
|
local candidate="$1"
|
||||||
local tmp out rc
|
local tmp out rc
|
||||||
local errfile msg
|
local errfile msg
|
||||||
|
|
||||||
# shellcheck disable=SC2206
|
# shellcheck disable=SC2206
|
||||||
local cmd=($candidate)
|
local cmd=( $candidate )
|
||||||
local exe="${cmd[0]}"
|
local exe="${cmd[0]}"
|
||||||
|
|
||||||
if [ ! -x "$exe" ]; then
|
if [ ! -x "$exe" ]; then
|
||||||
SHEBANG_ERRORS+=(" - FAIL (not executable): #!$candidate")
|
SHEBANG_ERRORS+=(" - FAIL (not executable): #!$candidate")
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
tmp="${EXEC_DIR%/}/shebang_test.$$.$RANDOM"
|
|
||||||
errfile="${EXEC_DIR%/}/shebang_probe_err.$$"
|
|
||||||
{
|
|
||||||
printf '#!%s\n' "$candidate"
|
|
||||||
printf '%s\n' "$probe_script_content"
|
|
||||||
} > "$tmp"
|
|
||||||
chmod 700 "$tmp" 2> /dev/null || true
|
|
||||||
|
|
||||||
set +e
|
|
||||||
out="$("$tmp" 2> "$errfile")"
|
|
||||||
rc=$?
|
|
||||||
set -e
|
|
||||||
|
|
||||||
rm -f "$tmp" 2> /dev/null || true
|
|
||||||
|
|
||||||
if [ "$rc" -eq 0 ] && [ -n "${out:-}" ] && [ "$out" != "null" ]; then
|
|
||||||
rm -f "$errfile" 2> /dev/null || true
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
msg=$' - FAIL: #!'"$candidate"$'\n'" rc=$rc, stdout='${out:-}'"$'\n'
|
|
||||||
if [ -s "$errfile" ]; then
|
|
||||||
msg+=$' stderr:\n'
|
|
||||||
msg+="$(sed -n '1,8p' "$errfile")"$'\n'
|
|
||||||
else
|
|
||||||
msg+=$' stderr: <empty>\n'
|
|
||||||
fi
|
|
||||||
SHEBANG_ERRORS+=("$msg")
|
|
||||||
rm -f "$errfile" 2> /dev/null || true
|
|
||||||
return 1
|
return 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
tmp="${EXEC_DIR%/}/shebang_test.$$.$RANDOM"
|
||||||
|
errfile="${EXEC_DIR%/}/shebang_probe_err.$$"
|
||||||
|
{
|
||||||
|
printf '#!%s\n' "$candidate"
|
||||||
|
printf '%s\n' "$probe_script_content"
|
||||||
|
} >"$tmp"
|
||||||
|
chmod 700 "$tmp" 2>/dev/null || true
|
||||||
|
|
||||||
|
set +e
|
||||||
|
out="$("$tmp" 2>"$errfile")"
|
||||||
|
rc=$?
|
||||||
|
set -e
|
||||||
|
|
||||||
|
rm -f "$tmp" 2>/dev/null || true
|
||||||
|
|
||||||
|
if [ "$rc" -eq 0 ] && [ -n "${out:-}" ] && [ "$out" != "null" ]; then
|
||||||
|
rm -f "$errfile" 2>/dev/null || true
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
msg=$' - FAIL: #!'"$candidate"$'\n'" rc=$rc, stdout='${out:-}'"$'\n'
|
||||||
|
if [ -s "$errfile" ]; then
|
||||||
|
msg+=$' stderr:\n'
|
||||||
|
msg+="$(sed -n '1,8p' "$errfile")"$'\n'
|
||||||
|
else
|
||||||
|
msg+=$' stderr: <empty>\n'
|
||||||
|
fi
|
||||||
|
SHEBANG_ERRORS+=("$msg")
|
||||||
|
rm -f "$errfile" 2>/dev/null || true
|
||||||
|
return 1
|
||||||
}
|
}
|
||||||
|
|
||||||
shebang=""
|
shebang=""
|
||||||
for candidate in "${candidate_shebangs[@]}"; do
|
for candidate in "${candidate_shebangs[@]}"; do
|
||||||
if validate_shebang "$candidate"; then
|
if validate_shebang "$candidate"; then
|
||||||
shebang="$candidate"
|
shebang="$candidate"
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
if [ -z "$shebang" ]; then
|
if [ -z "$shebang" ]; then
|
||||||
echo "ERROR: No valid shebang found (unable to execute bashio::addon.version via candidates)." >&2
|
echo "ERROR: No valid shebang found (unable to execute bashio::addon.version via candidates)." >&2
|
||||||
echo "Tried:" >&2
|
echo "Tried:" >&2
|
||||||
printf ' - %s\n' "${candidate_shebangs[@]}" >&2
|
printf ' - %s\n' "${candidate_shebangs[@]}" >&2
|
||||||
if [ "${#SHEBANG_ERRORS[@]}" -gt 0 ]; then
|
if [ "${#SHEBANG_ERRORS[@]}" -gt 0 ]; then
|
||||||
echo "Probe failures:" >&2
|
echo "Probe failures:" >&2
|
||||||
printf '%s\n' "${SHEBANG_ERRORS[@]}" >&2
|
printf '%s\n' "${SHEBANG_ERRORS[@]}" >&2
|
||||||
fi
|
fi
|
||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
#####################################
|
#####################################
|
||||||
@@ -251,28 +245,28 @@ 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
|
# 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.
|
# written the directory -- and only where with-contenv exists to care.
|
||||||
if $PID1 && { [ -x /command/with-contenv ] || [ -x /usr/bin/with-contenv ]; }; then
|
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
|
# 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
|
# 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
|
# 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
|
# shebang. rename(2) means a concurrent reader -- a HEALTHCHECK can run alongside PID 1 -- sees
|
||||||
# the directory either absent or complete, never mid-dump.
|
# 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
|
# 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
|
# 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
|
# 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
|
# 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
|
# 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.
|
# 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" \
|
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
|
s6-dumpenv -- "$S6_CONTAINER_ENV.tmp" && mv "$S6_CONTAINER_ENV.tmp" "$S6_CONTAINER_ENV"; then
|
||||||
echo "Populated $S6_CONTAINER_ENV for with-contenv"
|
echo "Populated $S6_CONTAINER_ENV for with-contenv"
|
||||||
else
|
else
|
||||||
# Leaves the directory absent, which is exactly how this fails today -- so the failure mode is
|
# 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,
|
# 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.
|
# 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
|
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"
|
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
|
||||||
fi
|
fi
|
||||||
|
|
||||||
####################################
|
####################################
|
||||||
@@ -282,21 +276,21 @@ fi
|
|||||||
BASHIO_LIB=""
|
BASHIO_LIB=""
|
||||||
BASHIO_LIB_FULL=false
|
BASHIO_LIB_FULL=false
|
||||||
for f in /usr/lib/bashio/bashio.sh /usr/lib/bashio/lib.sh /usr/src/bashio/bashio.sh /usr/local/lib/bashio/bashio.sh; do
|
for f in /usr/lib/bashio/bashio.sh /usr/lib/bashio/lib.sh /usr/src/bashio/bashio.sh /usr/local/lib/bashio/bashio.sh; do
|
||||||
if [ -f "$f" ]; then
|
if [ -f "$f" ]; then
|
||||||
BASHIO_LIB="$f"
|
BASHIO_LIB="$f"
|
||||||
# The real library, which talks to the Supervisor. The standalone shim below only reads
|
# The real library, which talks to the Supervisor. The standalone shim below only reads
|
||||||
# environment variables, which matters to wait_for_supervisor().
|
# environment variables, which matters to wait_for_supervisor().
|
||||||
BASHIO_LIB_FULL=true
|
BASHIO_LIB_FULL=true
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
if [ -z "$BASHIO_LIB" ]; then
|
if [ -z "$BASHIO_LIB" ]; then
|
||||||
for f in /usr/local/lib/bashio-standalone.sh /.bashio-standalone.sh; do
|
for f in /usr/local/lib/bashio-standalone.sh /.bashio-standalone.sh; do
|
||||||
if [ -f "$f" ]; then
|
if [ -f "$f" ]; then
|
||||||
BASHIO_LIB="$f"
|
BASHIO_LIB="$f"
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
##############################
|
##############################
|
||||||
@@ -322,56 +316,56 @@ fi
|
|||||||
# skips the wait. When the Supervisor is already up -- the normal case -- this costs one request.
|
# skips the wait. When the Supervisor is already up -- the normal case -- this costs one request.
|
||||||
|
|
||||||
wait_for_supervisor() {
|
wait_for_supervisor() {
|
||||||
local max="${HA_SUPERVISOR_WAIT:-30}"
|
local max="${HA_SUPERVISOR_WAIT:-30}"
|
||||||
local started deadline remaining attempt announced=0
|
local started deadline remaining attempt announced=0
|
||||||
|
|
||||||
# Nothing to wait for without a token. The standalone shim is excluded too: it answers these
|
# Nothing to wait for without a token. The standalone shim is excluded too: it answers these
|
||||||
# calls from environment variables and never contacts the Supervisor, so it can never satisfy
|
# calls from environment variables and never contacts the Supervisor, so it can never satisfy
|
||||||
# the probe and would burn the whole ceiling on every boot.
|
# the probe and would burn the whole ceiling on every boot.
|
||||||
[ -n "${SUPERVISOR_TOKEN:-}" ] || return 0
|
[ -n "${SUPERVISOR_TOKEN:-}" ] || return 0
|
||||||
[ "${BASHIO_LIB_FULL:-false}" = "true" ] || return 0
|
[ "${BASHIO_LIB_FULL:-false}" = "true" ] || return 0
|
||||||
# bashio's own curl carries no --max-time, so each attempt is bounded from the outside.
|
# bashio's own curl carries no --max-time, so each attempt is bounded from the outside.
|
||||||
command -v timeout > /dev/null 2>&1 || return 0
|
command -v timeout >/dev/null 2>&1 || return 0
|
||||||
# Digits only, then forced to base 10: `test -gt` accepts a zero-padded override like 08, but
|
# Digits only, then forced to base 10: `test -gt` accepts a zero-padded override like 08, but
|
||||||
# arithmetic expansion reads it as octal and fails, which would leave the deadline empty and
|
# arithmetic expansion reads it as octal and fails, which would leave the deadline empty and
|
||||||
# spin the loop below forever.
|
# spin the loop below forever.
|
||||||
case "$max" in '' | *[!0-9]*) return 0 ;; esac
|
case "$max" in '' | *[!0-9]*) return 0 ;; esac
|
||||||
max=$((10#$max))
|
max=$((10#$max))
|
||||||
[ "$max" -gt 0 ] || return 0
|
[ "$max" -gt 0 ] || return 0
|
||||||
|
|
||||||
started=$SECONDS
|
started=$SECONDS
|
||||||
deadline=$((started + max))
|
deadline=$((started + max))
|
||||||
|
|
||||||
while :; do
|
while :; do
|
||||||
remaining=$((deadline - SECONDS))
|
remaining=$((deadline - SECONDS))
|
||||||
if [ "$remaining" -le 0 ]; then
|
if [ "$remaining" -le 0 ]; then
|
||||||
echo -e "\e[38;5;214m$(date) WARNING: Supervisor API did not report this add-on's network details within ${max}s, continuing anyway\e[0m"
|
echo -e "\e[38;5;214m$(date) WARNING: Supervisor API did not report this add-on's network details within ${max}s, continuing anyway\e[0m"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# No single attempt may outlive the ceiling it is bounded by.
|
# No single attempt may outlive the ceiling it is bounded by.
|
||||||
attempt=5
|
attempt=5
|
||||||
[ "$remaining" -lt "$attempt" ] && attempt="$remaining"
|
[ "$remaining" -lt "$attempt" ] && attempt="$remaining"
|
||||||
|
|
||||||
# One call is enough to settle all of them: bashio fetches the whole /addons/self/info object
|
# One call is enough to settle all of them: bashio fetches the whole /addons/self/info object
|
||||||
# and caches it, so a populated ip_address means ingress_port and the rest are cached too.
|
# and caches it, so a populated ip_address means ingress_port and the rest are cached too.
|
||||||
# Run in a child shell so bashio's globals and traps stay out of the entrypoint; its own error
|
# Run in a child shell so bashio's globals and traps stay out of the entrypoint; its own error
|
||||||
# logging is dropped because a failed attempt here is expected, not news.
|
# logging is dropped because a failed attempt here is expected, not news.
|
||||||
# shellcheck disable=SC2016
|
# shellcheck disable=SC2016
|
||||||
if timeout "$attempt" bash -c '. "$1" && [ -n "$(bashio::addon.ip_address)" ]' \
|
if timeout "$attempt" bash -c '. "$1" && [ -n "$(bashio::addon.ip_address)" ]' \
|
||||||
_ "$BASHIO_LIB" > /dev/null 2>&1; then
|
_ "$BASHIO_LIB" >/dev/null 2>&1; then
|
||||||
[ "$announced" -eq 0 ] || echo "Supervisor API ready after $((SECONDS - started))s"
|
[ "$announced" -eq 0 ] || echo "Supervisor API ready after $((SECONDS - started))s"
|
||||||
return 0
|
return 0
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ "$announced" -eq 0 ]; then
|
if [ "$announced" -eq 0 ]; then
|
||||||
echo "Waiting for the Supervisor API to report this add-on's network details..."
|
echo "Waiting for the Supervisor API to report this add-on's network details..."
|
||||||
announced=1
|
announced=1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Skipped when the attempt already consumed what was left, so the sleep cannot overshoot.
|
# Skipped when the attempt already consumed what was left, so the sleep cannot overshoot.
|
||||||
[ "$((deadline - SECONDS))" -gt 0 ] && sleep 1
|
[ "$((deadline - SECONDS))" -gt 0 ] && sleep 1
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
|
|
||||||
wait_for_supervisor
|
wait_for_supervisor
|
||||||
@@ -381,97 +375,97 @@ wait_for_supervisor
|
|||||||
####################
|
####################
|
||||||
|
|
||||||
run_one_script() {
|
run_one_script() {
|
||||||
local script="$1"
|
local script="$1"
|
||||||
|
|
||||||
echo "$script: executing"
|
echo "$script: executing"
|
||||||
|
|
||||||
if [ "$(id -u)" -eq 0 ]; then
|
if [ "$(id -u)" -eq 0 ]; then
|
||||||
chown "$(id -u)":"$(id -g)" "$script" || true
|
chown "$(id -u)":"$(id -g)" "$script" || true
|
||||||
chmod a+x "$script" || true
|
chmod a+x "$script" || true
|
||||||
else
|
else
|
||||||
echo -e "\e[38;5;214m$(date) WARNING: Script executed with user $(id -u):$(id -g), things can break and chown won't work\e[0m"
|
echo -e "\e[38;5;214m$(date) WARNING: Script executed with user $(id -u):$(id -g), things can break and chown won't work\e[0m"
|
||||||
sed -i "s/^[[:space:]]*chown /true # chown /g" "$script"
|
sed -i "s/^[[:space:]]*chown /true # chown /g" "$script"
|
||||||
sed -i "s/^[[:space:]]*chmod /true # chmod /g" "$script"
|
sed -i "s/^[[:space:]]*chmod /true # chmod /g" "$script"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
sed -i "1s|^.*|#!$shebang|" "$script"
|
sed -i "1s|^.*|#!$shebang|" "$script"
|
||||||
chmod +x "$script"
|
chmod +x "$script"
|
||||||
|
|
||||||
if [ "${ha_entry_source:-null}" = "true" ]; then
|
if [ "${ha_entry_source:-null}" = "true" ]; then
|
||||||
sed -i -E 's/^[[:space:]]*exit ([0-9]+)/return \1 \|\| exit \1/g' "$script"
|
sed -i -E 's/^[[:space:]]*exit ([0-9]+)/return \1 \|\| exit \1/g' "$script"
|
||||||
sed -i 's/bashio::exit\.nok/return 1/g' "$script"
|
sed -i 's/bashio::exit\.nok/return 1/g' "$script"
|
||||||
sed -i 's/bashio::exit\.ok/return 0/g' "$script"
|
sed -i 's/bashio::exit\.ok/return 0/g' "$script"
|
||||||
|
# shellcheck disable=SC1090
|
||||||
|
source "$script" || echo -e "\033[0;31mError\033[0m : $script exiting $?"
|
||||||
|
else
|
||||||
|
_run_rc=0
|
||||||
|
"$script" || _run_rc=$?
|
||||||
|
if [ "$_run_rc" -eq 126 ] && [ -n "${BASHIO_LIB:-}" ]; then
|
||||||
|
echo "Direct exec failed (rc=126, likely E2BIG), retrying via source in subshell..."
|
||||||
|
_run_rc=0
|
||||||
|
(
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
source "$script" || echo -e "\033[0;31mError\033[0m : $script exiting $?"
|
. "$BASHIO_LIB" 2>/dev/null || true
|
||||||
else
|
# shellcheck disable=SC1090
|
||||||
_run_rc=0
|
. "$script"
|
||||||
"$script" || _run_rc=$?
|
) || _run_rc=$?
|
||||||
if [ "$_run_rc" -eq 126 ] && [ -n "${BASHIO_LIB:-}" ]; then
|
if [ "$_run_rc" -ne 0 ]; then
|
||||||
echo "Direct exec failed (rc=126, likely E2BIG), retrying via source in subshell..."
|
echo -e "\033[0;31mError\033[0m : $script exiting $_run_rc"
|
||||||
_run_rc=0
|
fi
|
||||||
(
|
elif [ "$_run_rc" -ne 0 ]; then
|
||||||
# shellcheck disable=SC1090
|
echo -e "\033[0;31mError\033[0m : $script exiting $_run_rc"
|
||||||
. "$BASHIO_LIB" 2> /dev/null || true
|
|
||||||
# shellcheck disable=SC1090
|
|
||||||
. "$script"
|
|
||||||
) || _run_rc=$?
|
|
||||||
if [ "$_run_rc" -ne 0 ]; then
|
|
||||||
echo -e "\033[0;31mError\033[0m : $script exiting $_run_rc"
|
|
||||||
fi
|
|
||||||
elif [ "$_run_rc" -ne 0 ]; then
|
|
||||||
echo -e "\033[0;31mError\033[0m : $script exiting $_run_rc"
|
|
||||||
fi
|
|
||||||
fi
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
sed -i '1a exit 0' "$script"
|
sed -i '1a exit 0' "$script"
|
||||||
}
|
}
|
||||||
|
|
||||||
if [ -d /etc/cont-init.d ]; then
|
if [ -d /etc/cont-init.d ]; then
|
||||||
for SCRIPTS in /etc/cont-init.d/*; do
|
for SCRIPTS in /etc/cont-init.d/*; do
|
||||||
[ -e "$SCRIPTS" ] || continue
|
[ -e "$SCRIPTS" ] || continue
|
||||||
run_one_script "$SCRIPTS"
|
run_one_script "$SCRIPTS"
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if $PID1; then
|
if $PID1; then
|
||||||
shopt -s nullglob
|
shopt -s nullglob
|
||||||
for runfile in /etc/services.d/*/run /etc/s6-overlay/s6-rc.d/*/run; do
|
for runfile in /etc/services.d/*/run /etc/s6-overlay/s6-rc.d/*/run; do
|
||||||
[ -f "$runfile" ] || continue
|
[ -f "$runfile" ] || continue
|
||||||
echo "Starting: $runfile"
|
echo "Starting: $runfile"
|
||||||
sed -i "1s|^.*|#!$shebang|" "$runfile"
|
sed -i "1s|^.*|#!$shebang|" "$runfile"
|
||||||
chmod +x "$runfile"
|
chmod +x "$runfile"
|
||||||
(
|
(
|
||||||
restart_count=0
|
restart_count=0
|
||||||
max_restarts=5
|
max_restarts=5
|
||||||
while true; do
|
while true; do
|
||||||
_svc_rc=0
|
_svc_rc=0
|
||||||
"$runfile" || _svc_rc=$?
|
"$runfile" || _svc_rc=$?
|
||||||
if [ "$_svc_rc" -eq 126 ] && [ -n "${BASHIO_LIB:-}" ]; then
|
if [ "$_svc_rc" -eq 126 ] && [ -n "${BASHIO_LIB:-}" ]; then
|
||||||
echo "Direct exec of $runfile failed (rc=126, likely E2BIG), retrying via source..."
|
echo "Direct exec of $runfile failed (rc=126, likely E2BIG), retrying via source..."
|
||||||
_svc_rc=0
|
_svc_rc=0
|
||||||
(
|
(
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
. "$BASHIO_LIB" 2> /dev/null || true
|
. "$BASHIO_LIB" 2>/dev/null || true
|
||||||
# shellcheck disable=SC1090
|
# shellcheck disable=SC1090
|
||||||
. "$runfile"
|
. "$runfile"
|
||||||
) || _svc_rc=$?
|
) || _svc_rc=$?
|
||||||
fi
|
fi
|
||||||
rc=$_svc_rc
|
rc=$_svc_rc
|
||||||
if [ "$rc" -eq 0 ]; then
|
if [ "$rc" -eq 0 ]; then
|
||||||
echo "$runfile exited cleanly (exit 0), not restarting."
|
echo "$runfile exited cleanly (exit 0), not restarting."
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
restart_count=$((restart_count + 1))
|
restart_count=$((restart_count + 1))
|
||||||
if [ "$restart_count" -ge "$max_restarts" ]; then
|
if [ "$restart_count" -ge "$max_restarts" ]; then
|
||||||
echo -e "\033[0;31mERROR: $runfile has crashed $restart_count times (last exit code: $rc), giving up.\033[0m"
|
echo -e "\033[0;31mERROR: $runfile has crashed $restart_count times (last exit code: $rc), giving up.\033[0m"
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
echo -e "\e[38;5;214m$(date) WARNING: $runfile exited (code $rc), restarting (#${restart_count}/${max_restarts}) in 5s...\e[0m"
|
echo -e "\e[38;5;214m$(date) WARNING: $runfile exited (code $rc), restarting (#${restart_count}/${max_restarts}) in 5s...\e[0m"
|
||||||
sleep 5
|
sleep 5
|
||||||
done
|
done
|
||||||
) &
|
) &
|
||||||
done
|
done
|
||||||
shopt -u nullglob
|
shopt -u nullglob
|
||||||
fi
|
fi
|
||||||
|
|
||||||
######################
|
######################
|
||||||
@@ -479,18 +473,18 @@ fi
|
|||||||
######################
|
######################
|
||||||
|
|
||||||
if $PID1; then
|
if $PID1; then
|
||||||
echo " "
|
echo " "
|
||||||
echo -e "\033[0;32mEverything started!\033[0m"
|
echo -e "\033[0;32mEverything started!\033[0m"
|
||||||
|
|
||||||
while :; do
|
while :; do
|
||||||
sleep infinity &
|
sleep infinity &
|
||||||
wait $!
|
wait $!
|
||||||
done
|
done
|
||||||
else
|
else
|
||||||
echo " "
|
echo " "
|
||||||
echo -e "\033[0;32mStarting the upstream container\033[0m"
|
echo -e "\033[0;32mStarting the upstream container\033[0m"
|
||||||
echo " "
|
echo " "
|
||||||
if [ -f /docker-mods ]; then
|
if [ -f /docker-mods ]; then
|
||||||
exec /docker-mods
|
exec /docker-mods
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ manual actions with precedence. Prompts live in `.github/prompts/`, shared shell
|
|||||||
in `.github/scripts/`.
|
in `.github/scripts/`.
|
||||||
|
|
||||||
| Workflow | Model | Trigger | Role |
|
| Workflow | Model | Trigger | Role |
|
||||||
| --- | --- | --- | --- |
|
|---|---|---|---|
|
||||||
| `on_issues_ai_triage.yaml` | Sonnet-low | issue opened (+ author reply, daily catch-up) | Tier 1: classify, dedupe, answer, ask for info; label `ai-triage` for real add-on bugs |
|
| `on_issues_ai_triage.yaml` | Sonnet-low | issue opened (+ author reply, daily catch-up) | Tier 1: classify, dedupe, answer, ask for info; label `ai-triage` for real add-on bugs |
|
||||||
| `daily_ai_fix.yaml` | Opus 5-xhigh | daily 03:00 | Tier 2: diagnose the `ai-triage` batch; small+confident → ready PR (`ai:fixed`); else write a plan (`ai:plan-pending`) |
|
| `daily_ai_fix.yaml` | Opus 5-xhigh | daily 03:00 | Tier 2: diagnose the `ai-triage` batch; small+confident → ready PR (`ai:fixed`); else write a plan (`ai:plan-pending`) |
|
||||||
| `on_issue_approved.yaml` | Opus 5-high | maintainer adds `ai:approved` | Tier 3: execute the approved plan → ready PR |
|
| `on_issue_approved.yaml` | Opus 5-high | maintainer adds `ai:approved` | Tier 3: execute the approved plan → ready PR |
|
||||||
@@ -176,7 +176,7 @@ prompt-only, not machine-enforced.
|
|||||||
## Linting Rules
|
## Linting Rules
|
||||||
|
|
||||||
| Tool | Config | Key ignores |
|
| Tool | Config | Key ignores |
|
||||||
| ------ | -------- | ------------ |
|
|------|--------|------------|
|
||||||
| Hadolint | `.hadolint.yaml` | DL3002, DL3006-9, DL3018 (no pinning required) |
|
| Hadolint | `.hadolint.yaml` | DL3002, DL3006-9, DL3018 (no pinning required) |
|
||||||
| ShellCheck | `.shellcheckrc` | SC2002 |
|
| ShellCheck | `.shellcheckrc` | SC2002 |
|
||||||
| Markdownlint | `.markdownlint.yaml` | MD013 (line length), MD025, MD033, MD041 |
|
| Markdownlint | `.markdownlint.yaml` | MD013 (line length), MD025, MD033, MD041 |
|
||||||
|
|||||||
@@ -109,7 +109,7 @@ never a default.
|
|||||||
|
|
||||||
### Addon version numbering
|
### Addon version numbering
|
||||||
|
|
||||||
The `version` written in the addon `config.yaml` is the one Home Assistant compares to decide whether an update is available. Home Assistant hides the update when it can order both versions and the new one is not strictly newer (`1.2.3` -> `1.2.3-2` is a semver pre-release, so it is _older_), and it cannot order tags such as `version-bf9e0b4f` or `ubuntu-2026-06-01` at all.
|
The `version` written in the addon `config.yaml` is the one Home Assistant compares to decide whether an update is available. Home Assistant hides the update when it can order both versions and the new one is not strictly newer (`1.2.3` -> `1.2.3-2` is a semver pre-release, so it is *older*), and it cannot order tags such as `version-bf9e0b4f` or `ubuntu-2026-06-01` at all.
|
||||||
|
|
||||||
The addon version is therefore derived from the upstream tag:
|
The addon version is therefore derived from the upstream tag:
|
||||||
|
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 1.5 KiB |
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -1,32 +1,32 @@
|
|||||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||||
|
|
||||||
## 2.8.0 (2026-09-05)
|
## 2.8.0 (2026-09-05)
|
||||||
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
||||||
|
|
||||||
## 2.7.0 (2026-08-29)
|
## 2.7.0 (2026-08-29)
|
||||||
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
||||||
|
|
||||||
## 2.5.1 (2026-08-22)
|
## 2.5.1 (2026-08-22)
|
||||||
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
||||||
|
|
||||||
## 2.4.0 (2026-08-13)
|
## 2.4.0 (2026-08-13)
|
||||||
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
||||||
|
|
||||||
## 2.1.0 (2026-08-08)
|
## 2.1.0 (2026-08-08)
|
||||||
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
||||||
|
|
||||||
## 2.0.3 (2026-08-01)
|
## 2.0.3 (2026-08-01)
|
||||||
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
||||||
|
|
||||||
## 2.0.2 (2026-08-01)
|
## 2.0.2 (2026-08-01)
|
||||||
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
||||||
|
|
||||||
## 2.0.0 (2026-07-22)
|
## 2.0.0 (2026-07-22)
|
||||||
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
||||||
|
|
||||||
## 1.76.52 (2026-07-11)
|
## 1.76.52 (2026-07-11)
|
||||||
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
||||||
|
|
||||||
## 1.76.51 (2026-06-17)
|
## 1.76.51 (2026-06-17)
|
||||||
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
- Update to latest version from lklynet/aurral (changelog : https://github.com/lklynet/aurral/releases)
|
||||||
## 1.76.49 (2026-06-05)
|
## 1.76.49 (2026-06-05)
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ This addon is based on the docker image <https://github.com/lklynet/aurral>
|
|||||||
## Configuration
|
## Configuration
|
||||||
|
|
||||||
| Option | Default | Description |
|
| Option | Default | Description |
|
||||||
| --- | --- | --- |
|
|---|---|---|
|
||||||
| `download_folder` | `/share/aurral/downloads` | Path where Aurral writes flow downloads. Must be under `/share`. |
|
| `download_folder` | `/share/aurral/downloads` | Path where Aurral writes flow downloads. Must be under `/share`. |
|
||||||
| `weekly_flow_folder` | `weekly-flow` | Subfolder name appended to `download_folder` for weekly flow files. The full path will be `download_folder/weekly_flow_folder`. |
|
| `weekly_flow_folder` | `weekly-flow` | Subfolder name appended to `download_folder` for weekly flow files. The full path will be `download_folder/weekly_flow_folder`. |
|
||||||
|
|
||||||
|
|||||||
BIN
aurral/stats.png
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -1,19 +1,19 @@
|
|||||||
|
|
||||||
## 1.86.0 (2026-09-12)
|
## 1.86.0 (2026-09-12)
|
||||||
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
|
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
|
||||||
|
|
||||||
## 1.85.0 (2026-08-29)
|
## 1.85.0 (2026-08-29)
|
||||||
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
|
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
|
||||||
|
|
||||||
## 1.84.0 (2026-08-15)
|
## 1.84.0 (2026-08-15)
|
||||||
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
|
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
|
||||||
|
|
||||||
## 1.83.0 (2026-08-01)
|
## 1.83.0 (2026-08-01)
|
||||||
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
|
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
|
||||||
|
|
||||||
## 1.82.1 (2026-07-16)
|
## 1.82.1 (2026-07-16)
|
||||||
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
|
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
|
||||||
|
|
||||||
## 1.81.0 (2026-07-04)
|
## 1.81.0 (2026-07-04)
|
||||||
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
|
- Update to latest version from autobrr/autobrr (changelog : https://github.com/autobrr/autobrr/releases)
|
||||||
## 1.80.0 (2026-06-05)
|
## 1.80.0 (2026-06-05)
|
||||||
|
|||||||
@@ -67,7 +67,7 @@ Default credentials: `admin` / `password` (change after first login).
|
|||||||
### Options
|
### Options
|
||||||
|
|
||||||
| Option | Type | Default | Description |
|
| Option | Type | Default | Description |
|
||||||
| -------- | ------ | --------- | ------------- |
|
|--------|------|---------|-------------|
|
||||||
| `PGID` | int | `0` | Group ID for file permissions |
|
| `PGID` | int | `0` | Group ID for file permissions |
|
||||||
| `PUID` | int | `0` | User ID for file permissions |
|
| `PUID` | int | `0` | User ID for file permissions |
|
||||||
| `TZ` | str | | Timezone (e.g., `Europe/London`) |
|
| `TZ` | str | | Timezone (e.g., `Europe/London`) |
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.4 KiB |
BIN
baikal/stats.png
|
Before Width: | Height: | Size: 3.9 KiB After Width: | Height: | Size: 1.7 KiB |
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -44,7 +44,7 @@ Configurations can be done through the app webUI, except for the following optio
|
|||||||
### Options
|
### Options
|
||||||
|
|
||||||
| Option | Type | Default | Description |
|
| Option | Type | Default | Description |
|
||||||
| -------- | ------ | --------- | ------------- |
|
|--------|------|---------|-------------|
|
||||||
| `PGID` | int | `0` | Group ID for file permissions |
|
| `PGID` | int | `0` | Group ID for file permissions |
|
||||||
| `PUID` | int | `0` | User ID for file permissions |
|
| `PUID` | int | `0` | User ID for file permissions |
|
||||||
| `TZ` | str | | Timezone (e.g., `Europe/London`) |
|
| `TZ` | str | | Timezone (e.g., `Europe/London`) |
|
||||||
|
|||||||
0
bazarr/rootfs/etc/services.d/nginx/run
Executable file → Normal file
BIN
bazarr/stats.png
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -17,7 +17,7 @@ A privacy-first PDF toolkit running entirely in your browser — no uploads, no
|
|||||||
### Organize & Edit
|
### Organize & Edit
|
||||||
|
|
||||||
| Tool | Tool | Tool |
|
| Tool | Tool | Tool |
|
||||||
| ------ | ------ | ------ |
|
|------|------|------|
|
||||||
| Merge PDF | Split PDF | Organize PDF |
|
| Merge PDF | Split PDF | Organize PDF |
|
||||||
| Delete Pages | Extract Pages | Reverse Pages |
|
| Delete Pages | Extract Pages | Reverse Pages |
|
||||||
| Rotate PDF | Rotate Custom | Crop PDF |
|
| Rotate PDF | Rotate Custom | Crop PDF |
|
||||||
@@ -28,7 +28,7 @@ A privacy-first PDF toolkit running entirely in your browser — no uploads, no
|
|||||||
### Convert TO PDF
|
### Convert TO PDF
|
||||||
|
|
||||||
| Tool | Tool | Tool |
|
| Tool | Tool | Tool |
|
||||||
| ------ | ------ | ------ |
|
|------|------|------|
|
||||||
| Word to PDF | Excel to PDF | PowerPoint to PDF |
|
| Word to PDF | Excel to PDF | PowerPoint to PDF |
|
||||||
| Image to PDF | JPG to PDF | PNG to PDF |
|
| Image to PDF | JPG to PDF | PNG to PDF |
|
||||||
| BMP to PDF | TIFF to PDF | WEBP to PDF |
|
| BMP to PDF | TIFF to PDF | WEBP to PDF |
|
||||||
@@ -44,7 +44,7 @@ A privacy-first PDF toolkit running entirely in your browser — no uploads, no
|
|||||||
### Convert FROM PDF
|
### Convert FROM PDF
|
||||||
|
|
||||||
| Tool | Tool | Tool |
|
| Tool | Tool | Tool |
|
||||||
| ------ | ------ | ------ |
|
|------|------|------|
|
||||||
| PDF to DOCX | PDF to Excel | PDF to JPG |
|
| PDF to DOCX | PDF to Excel | PDF to JPG |
|
||||||
| PDF to PNG | PDF to BMP | PDF to TIFF |
|
| PDF to PNG | PDF to BMP | PDF to TIFF |
|
||||||
| PDF to WEBP | PDF to SVG | PDF to Text |
|
| PDF to WEBP | PDF to SVG | PDF to Text |
|
||||||
@@ -54,7 +54,7 @@ A privacy-first PDF toolkit running entirely in your browser — no uploads, no
|
|||||||
### Security & Metadata
|
### Security & Metadata
|
||||||
|
|
||||||
| Tool | Tool | Tool |
|
| Tool | Tool | Tool |
|
||||||
| ------ | ------ | ------ |
|
|------|------|------|
|
||||||
| Encrypt PDF | Decrypt PDF | Change Permissions |
|
| Encrypt PDF | Decrypt PDF | Change Permissions |
|
||||||
| Remove Restrictions | Sign PDF | Digital Sign PDF |
|
| Remove Restrictions | Sign PDF | Digital Sign PDF |
|
||||||
| Validate Signature | Edit Metadata | View Metadata |
|
| Validate Signature | Edit Metadata | View Metadata |
|
||||||
@@ -64,7 +64,7 @@ A privacy-first PDF toolkit running entirely in your browser — no uploads, no
|
|||||||
### Enhance & Process
|
### Enhance & Process
|
||||||
|
|
||||||
| Tool | Tool | Tool |
|
| Tool | Tool | Tool |
|
||||||
| ------ | ------ | ------ |
|
|------|------|------|
|
||||||
| Compress PDF | OCR PDF | Deskew PDF |
|
| Compress PDF | OCR PDF | Deskew PDF |
|
||||||
| Rasterize PDF | Linearize PDF | PDF to PDF/A |
|
| Rasterize PDF | Linearize PDF | PDF to PDF/A |
|
||||||
| Adjust Colors | Invert Colors | Text Color |
|
| Adjust Colors | Invert Colors | Text Color |
|
||||||
@@ -76,7 +76,7 @@ A privacy-first PDF toolkit running entirely in your browser — no uploads, no
|
|||||||
### Forms & More
|
### Forms & More
|
||||||
|
|
||||||
| Tool | Tool | Tool |
|
| Tool | Tool | Tool |
|
||||||
| ------ | ------ | ------ |
|
|------|------|------|
|
||||||
| Form Creator | Form Filler | Table of Contents |
|
| Form Creator | Form Filler | Table of Contents |
|
||||||
| Bookmark | PDF Editor | Extract Images |
|
| Bookmark | PDF Editor | Extract Images |
|
||||||
| Extract Tables | Extract Attachments | Edit Attachments |
|
| Extract Tables | Extract Attachments | Edit Attachments |
|
||||||
|
|||||||
0
bentopdf/rootfs/etc/s6-overlay/s6-rc.d/bentopdf/run
Executable file → Normal file
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.4 KiB |
|
Before Width: | Height: | Size: 4.0 KiB After Width: | Height: | Size: 1.8 KiB |
@@ -1,3 +1,11 @@
|
|||||||
|
## 20260916.2 (16-09-2026)
|
||||||
|
- Minor bugs fixed
|
||||||
|
## 20260916.1 (16-09-2026)
|
||||||
|
- Minor bugs fixed
|
||||||
|
## 20260916 (16-09-2026)
|
||||||
|
- Minor bugs fixed
|
||||||
|
## 20260915 (15-09-2026)
|
||||||
|
- Minor bugs fixed
|
||||||
## 20260912.2 (12-09-2026)
|
## 20260912.2 (12-09-2026)
|
||||||
- Minor bugs fixed
|
- Minor bugs fixed
|
||||||
## 20260912 (12-09-2026)
|
## 20260912 (12-09-2026)
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ Requires a second model to confirm the **first** detection of each bird species
|
|||||||
|
|
||||||
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.
|
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`:
|
**Off by default.** Turn it on in the web UI under *Settings → Filters → First Daily Detection Consensus*, or in `config.yaml`:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
realtime:
|
realtime:
|
||||||
@@ -89,7 +89,7 @@ It deliberately does **nothing** in these cases, all of which keep today's singl
|
|||||||
|
|
||||||
- you run only one bird model (the default) — a second opinion does not exist, so the rule can never trigger
|
- 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 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
|
- 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
|
- 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
|
- the taxonomy or the database cannot be consulted — it fails open and accepts the detection
|
||||||
|
|
||||||
|
|||||||
@@ -127,5 +127,5 @@ slug: birdnet-go-dev
|
|||||||
udev: true
|
udev: true
|
||||||
url: https://github.com/alexbelgium/hassio-addons
|
url: https://github.com/alexbelgium/hassio-addons
|
||||||
usb: true
|
usb: true
|
||||||
version: "20260912.3"
|
version: "20260916.2"
|
||||||
video: true
|
video: true
|
||||||
|
|||||||
@@ -33,10 +33,7 @@ TARGET_DIR=""
|
|||||||
while [ "$#" -gt 0 ]; do
|
while [ "$#" -gt 0 ]; do
|
||||||
case "$1" in
|
case "$1" in
|
||||||
--check) CHECK_ONLY=1 ;;
|
--check) CHECK_ONLY=1 ;;
|
||||||
-*)
|
-*) echo "unknown option: $1" >&2; exit 64 ;;
|
||||||
echo "unknown option: $1" >&2
|
|
||||||
exit 64
|
|
||||||
;;
|
|
||||||
*)
|
*)
|
||||||
# Last-one-wins would silently clone into the wrong directory if a caller ever
|
# 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
|
# appended an argument; the pre-flag script used "${1}", so refuse rather than
|
||||||
@@ -187,7 +184,7 @@ if [ "${CHECK_ONLY}" = "1" ]; then
|
|||||||
fi
|
fi
|
||||||
echo "!!! CHECK FAILED: ${#conflicting[@]} PR(s) would break the add-on build" >&2
|
echo "!!! CHECK FAILED: ${#conflicting[@]} PR(s) would break the add-on build" >&2
|
||||||
for entry in "${conflicting[@]}"; do
|
for entry in "${conflicting[@]}"; do
|
||||||
IFS='|' read -r number scope files title <<< "${entry}"
|
IFS='|' read -r number scope files title <<<"${entry}"
|
||||||
echo "!!! CONFLICT pr=#${number} conflicts-with=${scope} files=${files} title=${title}" >&2
|
echo "!!! CONFLICT pr=#${number} conflicts-with=${scope} files=${files} title=${title}" >&2
|
||||||
done
|
done
|
||||||
echo "!!! conflicts-with=main -> the PR is stale against main; merge main into its branch and resolve there." >&2
|
echo "!!! conflicts-with=main -> the PR is stale against main; merge main into its branch and resolve there." >&2
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ if [ ! -f "$CONFIG_LOCATION" ]; then
|
|||||||
# config. (We can't remove the file and continue — subsequent yq calls
|
# config. (We can't remove the file and continue — subsequent yq calls
|
||||||
# under set -e would abort the init script.)
|
# under set -e would abort the init script.)
|
||||||
if ! curl -fL -s -S \
|
if ! curl -fL -s -S \
|
||||||
https://raw.githubusercontent.com/tphakala/birdnet-go/refs/heads/main/internal/conf/config.yaml \
|
https://raw.githubusercontent.com/tphakala/birdnet-go/refs/heads/main/internal/conf/config.yaml \
|
||||||
-o "$CONFIG_LOCATION"; then
|
-o "$CONFIG_LOCATION"; then
|
||||||
bashio::log.warning "Could not download default config.yaml; seeding an empty document so addon defaults can populate it"
|
bashio::log.warning "Could not download default config.yaml; seeding an empty document so addon defaults can populate it"
|
||||||
echo '{}' > "$CONFIG_LOCATION"
|
echo '{}' > "$CONFIG_LOCATION"
|
||||||
fi
|
fi
|
||||||
@@ -190,5 +190,5 @@ LOG_DIR="/config/logs"
|
|||||||
if [ -d "$LOG_DIR" ]; then
|
if [ -d "$LOG_DIR" ]; then
|
||||||
bashio::log.info "Trimming log files older than ${LOG_MAX_AGE_DAYS} days in ${LOG_DIR}"
|
bashio::log.info "Trimming log files older than ${LOG_MAX_AGE_DAYS} days in ${LOG_DIR}"
|
||||||
ln -sf "$LOG_DIR" /logs
|
ln -sf "$LOG_DIR" /logs
|
||||||
find "$LOG_DIR" -type f -name "*.log*" -mtime +"$LOG_MAX_AGE_DAYS" -delete 2> /dev/null || true
|
find "$LOG_DIR" -type f -name "*.log*" -mtime +"$LOG_MAX_AGE_DAYS" -delete 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -44,8 +44,8 @@ if ! bashio::config.true 'mariadb_auto_config'; then
|
|||||||
# Only revert if config.yaml points at the HA MariaDB host we would have
|
# Only revert if config.yaml points at the HA MariaDB host we would have
|
||||||
# written — a mysql block pointing at a different host was set manually.
|
# written — a mysql block pointing at a different host was set manually.
|
||||||
# shellcheck disable=SC2016
|
# shellcheck disable=SC2016
|
||||||
CURRENT_MYSQL_HOST="$(yq -r '.output.mysql.host // empty' "$CONFIG_LOCATION" 2> /dev/null || true)"
|
CURRENT_MYSQL_HOST="$(yq -r '.output.mysql.host // empty' "$CONFIG_LOCATION" 2>/dev/null || true)"
|
||||||
if yq -e '.output.mysql.enabled == true' "$CONFIG_LOCATION" > /dev/null 2>&1 \
|
if yq -e '.output.mysql.enabled == true' "$CONFIG_LOCATION" >/dev/null 2>&1 \
|
||||||
&& [ "${CURRENT_MYSQL_HOST}" = "${MYSQL_HOST}" ]; then
|
&& [ "${CURRENT_MYSQL_HOST}" = "${MYSQL_HOST}" ]; then
|
||||||
yq -i -y \
|
yq -i -y \
|
||||||
'.output.mysql.enabled = false
|
'.output.mysql.enabled = false
|
||||||
@@ -71,7 +71,7 @@ bashio::log.green "---"
|
|||||||
# Resolve MariaDB hostname to IPv4: on HAOS >=17.3 the Supervisor network
|
# Resolve MariaDB hostname to IPv4: on HAOS >=17.3 the Supervisor network
|
||||||
# gained IPv6, but the MariaDB addon only grants its user from the IPv4
|
# gained IPv6, but the MariaDB addon only grants its user from the IPv4
|
||||||
# subnet. Fall back to the raw hostname if resolution fails.
|
# subnet. Fall back to the raw hostname if resolution fails.
|
||||||
MYSQL_HOST_RESOLVED="$(getent ahostsv4 "${MYSQL_HOST}" 2> /dev/null | awk '{print $1; exit}')"
|
MYSQL_HOST_RESOLVED="$(getent ahostsv4 "${MYSQL_HOST}" 2>/dev/null | awk '{print $1; exit}')"
|
||||||
MYSQL_HOST_RESOLVED="${MYSQL_HOST_RESOLVED:-${MYSQL_HOST}}"
|
MYSQL_HOST_RESOLVED="${MYSQL_HOST_RESOLVED:-${MYSQL_HOST}}"
|
||||||
if [ "${MYSQL_HOST_RESOLVED}" != "${MYSQL_HOST}" ]; then
|
if [ "${MYSQL_HOST_RESOLVED}" != "${MYSQL_HOST}" ]; then
|
||||||
bashio::log.blue "Resolved ${MYSQL_HOST} -> ${MYSQL_HOST_RESOLVED} (forcing IPv4)"
|
bashio::log.blue "Resolved ${MYSQL_HOST} -> ${MYSQL_HOST_RESOLVED} (forcing IPv4)"
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 1.2 KiB |
@@ -1,5 +1,5 @@
|
|||||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||||
|
|
||||||
## 20260827 (2026-08-29)
|
## 20260827 (2026-08-29)
|
||||||
- Update to latest version from tphakala/birdnet-go (changelog : https://github.com/tphakala/birdnet-go/releases)
|
- Update to latest version from tphakala/birdnet-go (changelog : https://github.com/tphakala/birdnet-go/releases)
|
||||||
- Upstream tag : 20260823
|
- Upstream tag : 20260823
|
||||||
@@ -7,13 +7,13 @@
|
|||||||
- Minor bugs fixed
|
- Minor bugs fixed
|
||||||
## 20260716.1 (26-08-2026)
|
## 20260716.1 (26-08-2026)
|
||||||
- Minor bugs fixed
|
- Minor bugs fixed
|
||||||
|
|
||||||
## 20260716.1 (2026-08-26)
|
## 20260716.1 (2026-08-26)
|
||||||
- Rebuild after syncing the alexbelgium/birdnet-go fork with upstream tphakala/birdnet-go and resolving conflicts on all open pull requests
|
- Rebuild after syncing the alexbelgium/birdnet-go fork with upstream tphakala/birdnet-go and resolving conflicts on all open pull requests
|
||||||
|
|
||||||
## 20260716 (2026-07-16)
|
## 20260716 (2026-07-16)
|
||||||
- Update to latest version from tphakala/birdnet-go (changelog : https://github.com/tphakala/birdnet-go/releases)
|
- Update to latest version from tphakala/birdnet-go (changelog : https://github.com/tphakala/birdnet-go/releases)
|
||||||
|
|
||||||
## 20260712 (2026-07-13)
|
## 20260712 (2026-07-13)
|
||||||
- Update to latest version from tphakala/birdnet-go (changelog : https://github.com/tphakala/birdnet-go/releases)
|
- Update to latest version from tphakala/birdnet-go (changelog : https://github.com/tphakala/birdnet-go/releases)
|
||||||
## nightly-20260615-4 (09-07-2026)
|
## nightly-20260615-4 (09-07-2026)
|
||||||
@@ -23,8 +23,8 @@
|
|||||||
|
|
||||||
## nightly-20260615-2 (05-07-2026)
|
## nightly-20260615-2 (05-07-2026)
|
||||||
- Minor bugs fixed
|
- Minor bugs fixed
|
||||||
- Fix detections/database not persisting across restarts on a fresh install: upstream's default `config.yaml` ships `output.sqlite.path: birdnet.db` (relative) explicitly, so the missing-only (`//=`) seeding introduced previously never rewrote it to an absolute path. A relative path resolves against the app's ephemeral working directory, so the database was silently recreated empty on every restart. Any relative `output.sqlite.path` is now rewritten to live under the persistent `/config` on startup; values already set to an absolute path are left untouched. (https://github.com/tphakala/birdnet-go/discussions/3774)
|
- Fix detections/database not persisting across restarts on a fresh install: upstream's default `config.yaml` ships `output.sqlite.path: birdnet.db` (relative) explicitly, so the missing-only (`//=`) seeding introduced previously never rewrote it to an absolute path. A relative path resolves against the app's ephemeral working directory, so the database was silently recreated empty on every restart. Any relative `output.sqlite.path` is now rewritten to live under the persistent `/config` on startup; values already set to an absolute path are left untouched. (https://github.com/tphakala/birdnet-go/discussions/3774)
|
||||||
- MQTT auto-config now also enables BirdNET-Go's native Home Assistant MQTT auto-discovery: detection sensors appear in Home Assistant automatically with no manual YAML (existing UI/config.yaml edits are preserved)
|
- MQTT auto-config now also enables BirdNET-Go's native Home Assistant MQTT auto-discovery: detection sensors appear in Home Assistant automatically with no manual YAML (existing UI/config.yaml edits are preserved)
|
||||||
- MQTT auto-config seeds `realtime.mqtt.retain: true` (only when unset) so sensor states survive Home Assistant restarts
|
- MQTT auto-config seeds `realtime.mqtt.retain: true` (only when unset) so sensor states survive Home Assistant restarts
|
||||||
- Added supervisor watchdog (tcp://[HOST]:[PORT:8080]) so the add-on is automatically restarted if BirdNET-Go stops responding
|
- Added supervisor watchdog (tcp://[HOST]:[PORT:8080]) so the add-on is automatically restarted if BirdNET-Go stops responding
|
||||||
- Added backup_exclude for rotated logs, making Home Assistant backups smaller (SQLite journals are kept so hot backups stay consistent)
|
- Added backup_exclude for rotated logs, making Home Assistant backups smaller (SQLite journals are kept so hot backups stay consistent)
|
||||||
|
|||||||
@@ -43,8 +43,8 @@ if [ ! -f "$CONFIG_LOCATION" ]; then
|
|||||||
# config. (We can't remove the file and continue — subsequent yq calls
|
# config. (We can't remove the file and continue — subsequent yq calls
|
||||||
# under set -e would abort the init script.)
|
# under set -e would abort the init script.)
|
||||||
if ! curl -fL -s -S \
|
if ! curl -fL -s -S \
|
||||||
https://raw.githubusercontent.com/tphakala/birdnet-go/refs/heads/main/internal/conf/config.yaml \
|
https://raw.githubusercontent.com/tphakala/birdnet-go/refs/heads/main/internal/conf/config.yaml \
|
||||||
-o "$CONFIG_LOCATION"; then
|
-o "$CONFIG_LOCATION"; then
|
||||||
bashio::log.warning "Could not download default config.yaml; seeding an empty document so addon defaults can populate it"
|
bashio::log.warning "Could not download default config.yaml; seeding an empty document so addon defaults can populate it"
|
||||||
echo '{}' > "$CONFIG_LOCATION"
|
echo '{}' > "$CONFIG_LOCATION"
|
||||||
fi
|
fi
|
||||||
@@ -186,5 +186,5 @@ LOG_DIR="/config/logs"
|
|||||||
if [ -d "$LOG_DIR" ]; then
|
if [ -d "$LOG_DIR" ]; then
|
||||||
bashio::log.info "Trimming log files older than ${LOG_MAX_AGE_DAYS} days in ${LOG_DIR}"
|
bashio::log.info "Trimming log files older than ${LOG_MAX_AGE_DAYS} days in ${LOG_DIR}"
|
||||||
ln -sf "$LOG_DIR" /logs
|
ln -sf "$LOG_DIR" /logs
|
||||||
find "$LOG_DIR" -type f -name "*.log*" -mtime +"$LOG_MAX_AGE_DAYS" -delete 2> /dev/null || true
|
find "$LOG_DIR" -type f -name "*.log*" -mtime +"$LOG_MAX_AGE_DAYS" -delete 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.0 KiB After Width: | Height: | Size: 1.5 KiB |
@@ -9,9 +9,9 @@ Here are some example tests I did (the whole threads are really interesting also
|
|||||||
|
|
||||||
**My recommendation:**
|
**My recommendation:**
|
||||||
|
|
||||||
- **Best entry system (< 50 €):** Boya By-lm40 (30 €) + deadcat (10 €)
|
- **Best entry system (< 50 €):** Boya By-lm40 (30 €) + deadcat (10 €)
|
||||||
- **Best middle end system (< 150 €):** Clippy EM272 TRS/TRRS (55 €) + Rode AI micro trs/trrs to usb (70 €) + Rycote deadcat (27 €)
|
- **Best middle end system (< 150 €):** Clippy EM272 TRS/TRRS (55 €) + Rode AI micro trs/trrs to usb (70 €) + Rycote deadcat (27 €)
|
||||||
- **Best high end system (<400 €):** Clippy EM272 XLR (85 €) or LOM Ucho Pro (75 €) + Focusrite Scarlet 2i2 4th Gen (200 €) + Bubblebee Pro Extreme deadcat (45 €)
|
- **Best high end system (<400 €):** Clippy EM272 XLR (85 €) or LOM Ucho Pro (75 €) + Focusrite Scarlet 2i2 4th Gen (200 €) + Bubblebee Pro Extreme deadcat (45 €)
|
||||||
|
|
||||||
**Sources for high end microphones in Europe:**
|
**Sources for high end microphones in Europe:**
|
||||||
|
|
||||||
|
|||||||
@@ -112,7 +112,6 @@ def automatic_mqtt_publish(file, detection, path):
|
|||||||
mqttc.publish(mqtt_topic, json_bird, 1)
|
mqttc.publish(mqtt_topic, json_bird, 1)
|
||||||
log.info("Posted to MQTT: ok")
|
log.info("Posted to MQTT: ok")
|
||||||
|
|
||||||
|
|
||||||
# Create MQTT client using legacy callback API when available for
|
# Create MQTT client using legacy callback API when available for
|
||||||
# compatibility with paho-mqtt >= 2.0
|
# compatibility with paho-mqtt >= 2.0
|
||||||
callback_api = getattr(mqtt, "CallbackAPIVersion", None)
|
callback_api = getattr(mqtt, "CallbackAPIVersion", None)
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.5 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -9,9 +9,9 @@ Here are some example tests I did (the whole threads are really interesting also
|
|||||||
|
|
||||||
**My recommendation:**
|
**My recommendation:**
|
||||||
|
|
||||||
- **Best entry system (< 50 €):** Boya By-lm40 (30 €) + deadcat (10 €)
|
- **Best entry system (< 50 €):** Boya By-lm40 (30 €) + deadcat (10 €)
|
||||||
- **Best middle end system (< 150 €):** Clippy EM272 TRS/TRRS (55 €) + Rode AI micro trs/trrs to usb (70 €) + Rycote deadcat (27 €)
|
- **Best middle end system (< 150 €):** Clippy EM272 TRS/TRRS (55 €) + Rode AI micro trs/trrs to usb (70 €) + Rycote deadcat (27 €)
|
||||||
- **Best high end system (<400 €):** Clippy EM272 XLR (85 €) or LOM Ucho Pro (75 €) + Focusrite Scarlet 2i2 4th Gen (200 €) + Bubblebee Pro Extreme deadcat (45 €)
|
- **Best high end system (<400 €):** Clippy EM272 XLR (85 €) or LOM Ucho Pro (75 €) + Focusrite Scarlet 2i2 4th Gen (200 €) + Bubblebee Pro Extreme deadcat (45 €)
|
||||||
|
|
||||||
**Sources for high end microphones in Europe:**
|
**Sources for high end microphones in Europe:**
|
||||||
|
|
||||||
|
|||||||
@@ -112,7 +112,6 @@ def automatic_mqtt_publish(file, detection, path):
|
|||||||
mqttc.publish(mqtt_topic, json_bird, 1)
|
mqttc.publish(mqtt_topic, json_bird, 1)
|
||||||
log.info("Posted to MQTT: ok")
|
log.info("Posted to MQTT: ok")
|
||||||
|
|
||||||
|
|
||||||
# Create MQTT client using legacy callback API when available for
|
# Create MQTT client using legacy callback API when available for
|
||||||
# compatibility with paho-mqtt >= 2.0
|
# compatibility with paho-mqtt >= 2.0
|
||||||
callback_api = getattr(mqtt, "CallbackAPIVersion", None)
|
callback_api = getattr(mqtt, "CallbackAPIVersion", None)
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 4.7 KiB After Width: | Height: | Size: 1.9 KiB |
@@ -1,20 +1,20 @@
|
|||||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||||
|
|
||||||
## 0.8.8 (2026-08-22)
|
## 0.8.8 (2026-08-22)
|
||||||
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
|
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
|
||||||
|
|
||||||
## 0.8.6 (2026-08-13)
|
## 0.8.6 (2026-08-13)
|
||||||
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
|
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
|
||||||
|
|
||||||
## 0.8.5 (2026-08-01)
|
## 0.8.5 (2026-08-01)
|
||||||
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
|
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
|
||||||
|
|
||||||
## 0.8.4 (2026-07-13)
|
## 0.8.4 (2026-07-13)
|
||||||
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
|
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
|
||||||
|
|
||||||
## 0.8.3 (2026-07-11)
|
## 0.8.3 (2026-07-11)
|
||||||
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
|
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
|
||||||
|
|
||||||
## 0.8.2.1 (2026-07-05)
|
## 0.8.2.1 (2026-07-05)
|
||||||
- Re-tag of 0.8.2-1 with no content change. Home Assistant compares add-on versions with semver semantics, where a `-N` suffix counts as a *pre-release* and sorts **below** the base version — so users already on 0.8.2 saw the 0.8.2-1 nginx fix as "Up-to-date" with the Update button disabled. Four-segment `0.8.2.1` sorts above both `0.8.2` and `0.8.2-1` (and below the next upstream `0.8.3`), so the update becomes installable everywhere.
|
- Re-tag of 0.8.2-1 with no content change. Home Assistant compares add-on versions with semver semantics, where a `-N` suffix counts as a *pre-release* and sorts **below** the base version — so users already on 0.8.2 saw the 0.8.2-1 nginx fix as "Up-to-date" with the Update button disabled. Four-segment `0.8.2.1` sorts above both `0.8.2` and `0.8.2-1` (and below the next upstream `0.8.3`), so the update becomes installable everywhere.
|
||||||
|
|
||||||
@@ -24,7 +24,7 @@
|
|||||||
|
|
||||||
## 0.8.2 (2026-07-04)
|
## 0.8.2 (2026-07-04)
|
||||||
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
|
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
|
||||||
|
|
||||||
## 0.7.5 (2026-06-23)
|
## 0.7.5 (2026-06-23)
|
||||||
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
|
- Update to latest version from Suncuss/BirdNET-PiPy (changelog : https://github.com/Suncuss/BirdNET-PiPy/releases)
|
||||||
## 0.7.4 (2026-06-01)
|
## 0.7.4 (2026-06-01)
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ map:
|
|||||||
name: BirdNET-PiPy
|
name: BirdNET-PiPy
|
||||||
options:
|
options:
|
||||||
env_vars: []
|
env_vars: []
|
||||||
ICECAST_PASSWORD: ""
|
ICECAST_PASSWORD: ''
|
||||||
data_location: /config/data
|
data_location: /config/data
|
||||||
panel_icon: mdi:bird
|
panel_icon: mdi:bird
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
@@ -7,11 +7,11 @@ DATA_LOCATION="$(bashio::config 'data_location' || true)"
|
|||||||
DATA_LOCATION="${DATA_LOCATION:-$DEFAULT_LOCATION}"
|
DATA_LOCATION="${DATA_LOCATION:-$DEFAULT_LOCATION}"
|
||||||
|
|
||||||
case "${DATA_LOCATION}" in
|
case "${DATA_LOCATION}" in
|
||||||
/config/* | /share/* | /data/*) ;;
|
/config/*|/share/*|/data/*) ;;
|
||||||
*)
|
*)
|
||||||
bashio::log.warning "Invalid data_location '${DATA_LOCATION}', falling back to ${DEFAULT_LOCATION}"
|
bashio::log.warning "Invalid data_location '${DATA_LOCATION}', falling back to ${DEFAULT_LOCATION}"
|
||||||
DATA_LOCATION="${DEFAULT_LOCATION}"
|
DATA_LOCATION="${DEFAULT_LOCATION}"
|
||||||
;;
|
;;
|
||||||
esac
|
esac
|
||||||
|
|
||||||
LEGACY1="/config/birdnet-pipy/data"
|
LEGACY1="/config/birdnet-pipy/data"
|
||||||
@@ -19,14 +19,14 @@ LEGACY2="/data"
|
|||||||
|
|
||||||
mkdir -p "${DATA_LOCATION}"
|
mkdir -p "${DATA_LOCATION}"
|
||||||
|
|
||||||
if [ -z "$(ls -A "${DATA_LOCATION}" 2> /dev/null || true)" ]; then
|
if [ -z "$(ls -A "${DATA_LOCATION}" 2>/dev/null || true)" ]; then
|
||||||
if [ -d "${LEGACY1}" ] && [ -n "$(ls -A "${LEGACY1}" 2> /dev/null || true)" ]; then
|
if [ -d "${LEGACY1}" ] && [ -n "$(ls -A "${LEGACY1}" 2>/dev/null || true)" ]; then
|
||||||
bashio::log.notice "Migrating legacy data from ${LEGACY1} to ${DATA_LOCATION}"
|
bashio::log.notice "Migrating legacy data from ${LEGACY1} to ${DATA_LOCATION}"
|
||||||
cp -a "${LEGACY1}/." "${DATA_LOCATION}/" || true
|
cp -a "${LEGACY1}/." "${DATA_LOCATION}/" || true
|
||||||
elif [ -d "${LEGACY2}" ] && [ "${LEGACY2}" != "${DATA_LOCATION}" ] && [ -n "$(ls -A "${LEGACY2}" 2> /dev/null || true)" ]; then
|
elif [ -d "${LEGACY2}" ] && [ "${LEGACY2}" != "${DATA_LOCATION}" ] && [ -n "$(ls -A "${LEGACY2}" 2>/dev/null || true)" ]; then
|
||||||
bashio::log.notice "Migrating legacy data from ${LEGACY2} to ${DATA_LOCATION}"
|
bashio::log.notice "Migrating legacy data from ${LEGACY2} to ${DATA_LOCATION}"
|
||||||
cp -a "${LEGACY2}/." "${DATA_LOCATION}/" || true
|
cp -a "${LEGACY2}/." "${DATA_LOCATION}/" || true
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mkdir -p "${DATA_LOCATION}/config" "${DATA_LOCATION}/clips" "${DATA_LOCATION}/logs" "${DATA_LOCATION}/cache" || true
|
mkdir -p "${DATA_LOCATION}/config" "${DATA_LOCATION}/clips" "${DATA_LOCATION}/logs" "${DATA_LOCATION}/cache" || true
|
||||||
|
|||||||
@@ -7,10 +7,10 @@ CFG_DIR="${DATA_DIR}/config"
|
|||||||
SETTINGS="${CFG_DIR}/user_settings.json"
|
SETTINGS="${CFG_DIR}/user_settings.json"
|
||||||
|
|
||||||
if [ -f "$SETTINGS" ]; then
|
if [ -f "$SETTINGS" ]; then
|
||||||
if ! grep -q 'detection' "$SETTINGS"; then
|
if ! grep -q 'detection' "$SETTINGS"; then
|
||||||
bak="${SETTINGS}.bak"
|
bak="${SETTINGS}.bak"
|
||||||
[ -e "$bak" ] && bak="${SETTINGS}.bak.$(date -u +%Y%m%dT%H%M%SZ)"
|
[ -e "$bak" ] && bak="${SETTINGS}.bak.$(date -u +%Y%m%dT%H%M%SZ)"
|
||||||
mv -f -- "$SETTINGS" "$bak"
|
mv -f -- "$SETTINGS" "$bak"
|
||||||
echo "WARNING: Erroneous file detected: '$SETTINGS' did not contain 'detection' and was renamed to '$bak'." >&2
|
echo "WARNING: Erroneous file detected: '$SETTINGS' did not contain 'detection' and was renamed to '$bak'." >&2
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
@@ -3,21 +3,21 @@
|
|||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
SND_GID=""
|
SND_GID=""
|
||||||
if [ -e /dev/snd ] && command -v stat > /dev/null 2>&1; then
|
if [ -e /dev/snd ] && command -v stat >/dev/null 2>&1; then
|
||||||
SND_GID="$(stat -c '%g' /dev/snd 2> /dev/null || true)"
|
SND_GID="$(stat -c '%g' /dev/snd 2>/dev/null || true)"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
if [ -n "${SND_GID}" ] && getent group audio > /dev/null 2>&1; then
|
if [ -n "${SND_GID}" ] && getent group audio >/dev/null 2>&1; then
|
||||||
current_gid="$(getent group audio | cut -d: -f3 || true)"
|
current_gid="$(getent group audio | cut -d: -f3 || true)"
|
||||||
if [ -n "${current_gid}" ] && [ "${current_gid}" != "${SND_GID}" ]; then
|
if [ -n "${current_gid}" ] && [ "${current_gid}" != "${SND_GID}" ]; then
|
||||||
groupmod -g "${SND_GID}" audio 2> /dev/null || true
|
groupmod -g "${SND_GID}" audio 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
for u in root nginx www-data icecast2; do
|
for u in root nginx www-data icecast2; do
|
||||||
if id "${u}" > /dev/null 2>&1; then
|
if id "${u}" >/dev/null 2>&1; then
|
||||||
usermod -aG audio "${u}" 2> /dev/null || true
|
usermod -aG audio "${u}" 2>/dev/null || true
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
|
|
||||||
# Create /run/pulse/native symlink if the PulseAudio socket is elsewhere
|
# Create /run/pulse/native symlink if the PulseAudio socket is elsewhere
|
||||||
@@ -25,21 +25,21 @@ done
|
|||||||
PULSE_SOCK="${PULSE_SERVER:-}"
|
PULSE_SOCK="${PULSE_SERVER:-}"
|
||||||
PULSE_SOCK="${PULSE_SOCK#unix:}"
|
PULSE_SOCK="${PULSE_SOCK#unix:}"
|
||||||
if [ -n "${PULSE_SOCK}" ] && [ -S "${PULSE_SOCK}" ] && [ ! -S /run/pulse/native ]; then
|
if [ -n "${PULSE_SOCK}" ] && [ -S "${PULSE_SOCK}" ] && [ ! -S /run/pulse/native ]; then
|
||||||
mkdir -p /run/pulse
|
mkdir -p /run/pulse
|
||||||
ln -sf "${PULSE_SOCK}" /run/pulse/native
|
ln -sf "${PULSE_SOCK}" /run/pulse/native
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Copy PulseAudio cookie for the icecast2 user so it can authenticate
|
# Copy PulseAudio cookie for the icecast2 user so it can authenticate
|
||||||
if id icecast2 > /dev/null 2>&1; then
|
if id icecast2 >/dev/null 2>&1; then
|
||||||
ICECAST_HOME="$(getent passwd icecast2 | cut -d: -f6)"
|
ICECAST_HOME="$(getent passwd icecast2 | cut -d: -f6)"
|
||||||
if [ -n "${ICECAST_HOME}" ]; then
|
if [ -n "${ICECAST_HOME}" ]; then
|
||||||
for cookie in /config/.config/pulse/cookie /root/.config/pulse/cookie; do
|
for cookie in /config/.config/pulse/cookie /root/.config/pulse/cookie; do
|
||||||
if [ -f "${cookie}" ]; then
|
if [ -f "${cookie}" ]; then
|
||||||
mkdir -p "${ICECAST_HOME}/.config/pulse"
|
mkdir -p "${ICECAST_HOME}/.config/pulse"
|
||||||
cp "${cookie}" "${ICECAST_HOME}/.config/pulse/cookie"
|
cp "${cookie}" "${ICECAST_HOME}/.config/pulse/cookie"
|
||||||
chown icecast2 "${ICECAST_HOME}/.config/pulse" "${ICECAST_HOME}/.config/pulse/cookie"
|
chown icecast2 "${ICECAST_HOME}/.config/pulse" "${ICECAST_HOME}/.config/pulse/cookie"
|
||||||
break
|
break
|
||||||
fi
|
fi
|
||||||
done
|
done
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|||||||
14
birdnet-pipy/rootfs/etc/services.d/api/run
Executable file → Normal file
@@ -10,10 +10,10 @@ cd /app
|
|||||||
# and core.main reach the API locally, satisfying the wait_for in nginx/run +
|
# and core.main reach the API locally, satisfying the wait_for in nginx/run +
|
||||||
# main/run). Heavy admin jobs cooperatively yield to keep this worker live.
|
# main/run). Heavy admin jobs cooperatively yield to keep this worker live.
|
||||||
exec gunicorn wsgi:application \
|
exec gunicorn wsgi:application \
|
||||||
--worker-class geventwebsocket.gunicorn.workers.GeventWebSocketWorker \
|
--worker-class geventwebsocket.gunicorn.workers.GeventWebSocketWorker \
|
||||||
--workers 1 \
|
--workers 1 \
|
||||||
--bind 127.0.0.1:5002 \
|
--bind 127.0.0.1:5002 \
|
||||||
--timeout 120 \
|
--timeout 120 \
|
||||||
--graceful-timeout 30 \
|
--graceful-timeout 30 \
|
||||||
--keep-alive 65 \
|
--keep-alive 65 \
|
||||||
--error-logfile -
|
--error-logfile -
|
||||||
|
|||||||
0
birdnet-pipy/rootfs/etc/services.d/icecast/run
Executable file → Normal file
0
birdnet-pipy/rootfs/etc/services.d/main/run
Executable file → Normal file
0
birdnet-pipy/rootfs/etc/services.d/model/run
Executable file → Normal file
0
birdnet-pipy/rootfs/etc/services.d/nginx/run
Executable file → Normal file
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -1,10 +1,10 @@
|
|||||||
|
|
||||||
## 1.37.2 (2026-08-29)
|
## 1.37.2 (2026-08-29)
|
||||||
- Update to latest version from dani-garcia/bitwarden_rs (changelog : https://github.com/dani-garcia/bitwarden_rs/releases)
|
- Update to latest version from dani-garcia/bitwarden_rs (changelog : https://github.com/dani-garcia/bitwarden_rs/releases)
|
||||||
|
|
||||||
## 1.37.1 (2026-08-01)
|
## 1.37.1 (2026-08-01)
|
||||||
- Update to latest version from dani-garcia/bitwarden_rs (changelog : https://github.com/dani-garcia/bitwarden_rs/releases)
|
- Update to latest version from dani-garcia/bitwarden_rs (changelog : https://github.com/dani-garcia/bitwarden_rs/releases)
|
||||||
|
|
||||||
## 1.37.0 (2026-07-25)
|
## 1.37.0 (2026-07-25)
|
||||||
- Update to latest version from dani-garcia/bitwarden_rs (changelog : https://github.com/dani-garcia/bitwarden_rs/releases)
|
- Update to latest version from dani-garcia/bitwarden_rs (changelog : https://github.com/dani-garcia/bitwarden_rs/releases)
|
||||||
|
|
||||||
|
|||||||
0
bitwarden/rootfs/etc/s6-overlay/s6-rc.d/init-nginx/run
Executable file → Normal file
0
bitwarden/rootfs/etc/s6-overlay/s6-rc.d/nginx/run
Executable file → Normal file
6
bitwarden/rootfs/etc/s6-overlay/s6-rc.d/vaultwarden/run
Executable file → Normal file
@@ -21,19 +21,19 @@ export ROCKET_SECRET_KEY="${secret_key}"
|
|||||||
# Find the matching log level
|
# Find the matching log level
|
||||||
if bashio::config.has_value 'log_level'; then
|
if bashio::config.has_value 'log_level'; then
|
||||||
case "$(bashio::string.lower "$(bashio::config 'log_level')")" in
|
case "$(bashio::string.lower "$(bashio::config 'log_level')")" in
|
||||||
all | trace)
|
all|trace)
|
||||||
log_level="trace"
|
log_level="trace"
|
||||||
;;
|
;;
|
||||||
debug)
|
debug)
|
||||||
log_level="debug"
|
log_level="debug"
|
||||||
;;
|
;;
|
||||||
info | notice)
|
info|notice)
|
||||||
log_level="info"
|
log_level="info"
|
||||||
;;
|
;;
|
||||||
warning)
|
warning)
|
||||||
log_level="warn"
|
log_level="warn"
|
||||||
;;
|
;;
|
||||||
error | fatal)
|
error|fatal)
|
||||||
log_level="error"
|
log_level="error"
|
||||||
;;
|
;;
|
||||||
off)
|
off)
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -64,7 +64,7 @@ Default credentials are shown in the startup logs.
|
|||||||
### Options
|
### Options
|
||||||
|
|
||||||
| Option | Type | Default | Description |
|
| Option | Type | Default | Description |
|
||||||
| -------- | ------ | --------- | ------------- |
|
|--------|------|---------|-------------|
|
||||||
| `PGID` | int | `0` | Group ID for file permissions |
|
| `PGID` | int | `0` | Group ID for file permissions |
|
||||||
| `PUID` | int | `0` | User ID for file permissions |
|
| `PUID` | int | `0` | User ID for file permissions |
|
||||||
| `TZ` | str | | Timezone (e.g., `Europe/London`) |
|
| `TZ` | str | | Timezone (e.g., `Europe/London`) |
|
||||||
|
|||||||
@@ -1,50 +1,50 @@
|
|||||||
|
|
||||||
## 1.95.101-ls128 (2026-09-12)
|
## 1.95.101-ls128 (2026-09-12)
|
||||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||||
|
|
||||||
## 1.94.121-ls127 (2026-09-05)
|
## 1.94.121-ls127 (2026-09-05)
|
||||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||||
|
|
||||||
## 1.94.117-ls125 (2026-08-29)
|
## 1.94.117-ls125 (2026-08-29)
|
||||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||||
|
|
||||||
## 1.93.138-ls124 (2026-08-22)
|
## 1.93.138-ls124 (2026-08-22)
|
||||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||||
|
|
||||||
## 1.93.136-ls122 (2026-08-13)
|
## 1.93.136-ls122 (2026-08-13)
|
||||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||||
|
|
||||||
## 1.93.134-ls121 (2026-08-08)
|
## 1.93.134-ls121 (2026-08-08)
|
||||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||||
|
|
||||||
## 1.93.129-ls119 (2026-08-01)
|
## 1.93.129-ls119 (2026-08-01)
|
||||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||||
|
|
||||||
## 1.92.144-ls117 (2026-07-25)
|
## 1.92.144-ls117 (2026-07-25)
|
||||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||||
|
|
||||||
## 1.92.141-ls115 (2026-07-18)
|
## 1.92.141-ls115 (2026-07-18)
|
||||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||||
|
|
||||||
## 1.92.140-ls113 (2026-07-16)
|
## 1.92.140-ls113 (2026-07-16)
|
||||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||||
|
|
||||||
## 1.92.139-ls112 (2026-07-11)
|
## 1.92.139-ls112 (2026-07-11)
|
||||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||||
|
|
||||||
## 1.92.134-ls109 (2026-07-04)
|
## 1.92.134-ls109 (2026-07-04)
|
||||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||||
|
|
||||||
## 1.91.180-ls108 (2026-06-27)
|
## 1.91.180-ls108 (2026-06-27)
|
||||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||||
|
|
||||||
## 1.91.175-ls106 (2026-06-20)
|
## 1.91.175-ls106 (2026-06-20)
|
||||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||||
|
|
||||||
## 1.91.172-ls105 (2026-06-13)
|
## 1.91.172-ls105 (2026-06-13)
|
||||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||||
|
|
||||||
## 1.91.171-ls104 (2026-06-11)
|
## 1.91.171-ls104 (2026-06-11)
|
||||||
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
- Update to latest version from linuxserver/docker-brave (changelog : https://github.com/linuxserver/docker-brave/releases)
|
||||||
## 1.91.168-ls103 (2026-06-05)
|
## 1.91.168-ls103 (2026-06-05)
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,13 +1,13 @@
|
|||||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||||
|
|
||||||
## 2026.09.05 (2026-09-05)
|
## 2026.09.05 (2026-09-05)
|
||||||
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
||||||
- Upstream tag : version-d39975b5
|
- Upstream tag : version-d39975b5
|
||||||
|
|
||||||
## 2026.08.13 (2026-08-13)
|
## 2026.08.13 (2026-08-13)
|
||||||
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
||||||
- Upstream tag : version-e8713bf7
|
- Upstream tag : version-e8713bf7
|
||||||
|
|
||||||
## 2026.08.01.1 (2026-08-01)
|
## 2026.08.01.1 (2026-08-01)
|
||||||
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
||||||
- Upstream tag : version-e2e1ec9c
|
- Upstream tag : version-e2e1ec9c
|
||||||
@@ -15,25 +15,25 @@
|
|||||||
|
|
||||||
- Version renamed from `version-bf9e0b4f`, which Home Assistant could not order and therefore could not reliably offer as an update: the previous version held no number that could order it, so the date of this change is used. The addon itself and the upstream version it tracks are unchanged
|
- Version renamed from `version-bf9e0b4f`, which Home Assistant could not order and therefore could not reliably offer as an update: the previous version held no number that could order it, so the date of this change is used. The addon itself and the upstream version it tracks are unchanged
|
||||||
|
|
||||||
|
|
||||||
## version-bf9e0b4f (2026-07-25)
|
## version-bf9e0b4f (2026-07-25)
|
||||||
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
||||||
|
|
||||||
## version-ae9a7fca (2026-07-16)
|
## version-ae9a7fca (2026-07-16)
|
||||||
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
||||||
|
|
||||||
## version-30a7c401 (2026-07-11)
|
## version-30a7c401 (2026-07-11)
|
||||||
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
||||||
|
|
||||||
## version-7148c2a3 (2026-07-04)
|
## version-7148c2a3 (2026-07-04)
|
||||||
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
||||||
|
|
||||||
## version-ec868334 (2026-06-27)
|
## version-ec868334 (2026-06-27)
|
||||||
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
||||||
|
|
||||||
## version-b9b82d3e (2026-06-20)
|
## version-b9b82d3e (2026-06-20)
|
||||||
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
||||||
|
|
||||||
## version-cca5154d (2026-06-11)
|
## version-cca5154d (2026-06-11)
|
||||||
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
- Update to latest version from linuxserver/docker-chromium (changelog : https://github.com/linuxserver/docker-chromium/releases)
|
||||||
## version-89f88890 (2026-06-05)
|
## version-89f88890 (2026-06-05)
|
||||||
|
|||||||
1
browser_chromium/rootfs/etc/cont-init.d/20-folders.sh
Normal file → Executable file
@@ -7,6 +7,7 @@ set -e
|
|||||||
PUID=$(bashio::config "PUID")
|
PUID=$(bashio::config "PUID")
|
||||||
PGID=$(bashio::config "PGID")
|
PGID=$(bashio::config "PGID")
|
||||||
|
|
||||||
|
|
||||||
# Create cache
|
# Create cache
|
||||||
#mkdir -p /.cache
|
#mkdir -p /.cache
|
||||||
#chmod 755 /.cache
|
#chmod 755 /.cache
|
||||||
|
|||||||
@@ -5,12 +5,12 @@ set -Eeuo pipefail
|
|||||||
|
|
||||||
# Resolve chromium binary (LSIO uses this path)
|
# Resolve chromium binary (LSIO uses this path)
|
||||||
if [[ -x /usr/lib/chromium/chromium ]]; then
|
if [[ -x /usr/lib/chromium/chromium ]]; then
|
||||||
BIN=/usr/lib/chromium/chromium
|
BIN=/usr/lib/chromium/chromium
|
||||||
elif command -v chromium >/dev/null 2>&1; then
|
elif command -v chromium >/dev/null 2>&1; then
|
||||||
BIN="$(command -v chromium)"
|
BIN="$(command -v chromium)"
|
||||||
else
|
else
|
||||||
echo "ERROR: chromium binary not found" >&2
|
echo "ERROR: chromium binary not found" >&2
|
||||||
exit 127
|
exit 127
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Fixed profile dir as requested
|
# Fixed profile dir as requested
|
||||||
@@ -19,7 +19,7 @@ mkdir -p "${USER_DATA_DIR}"
|
|||||||
|
|
||||||
# Cleanup stale locks if chromium is not already running
|
# Cleanup stale locks if chromium is not already running
|
||||||
if ! pgrep -f 'chromium' >/dev/null 2>&1; then
|
if ! pgrep -f 'chromium' >/dev/null 2>&1; then
|
||||||
rm -f "${USER_DATA_DIR}/Singleton"* || true
|
rm -f "${USER_DATA_DIR}/Singleton"* || true
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# Detect seccomp mode (0 = disabled)
|
# Detect seccomp mode (0 = disabled)
|
||||||
@@ -27,22 +27,22 @@ seccomp_mode="$(awk -F':' '/^Seccomp:/{gsub(/[[:space:]]/,"",$2); print $2}' /pr
|
|||||||
|
|
||||||
# Base arguments (exactly what you requested + required safety flags)
|
# Base arguments (exactly what you requested + required safety flags)
|
||||||
args=(
|
args=(
|
||||||
--remote-debugging-address=0.0.0.0
|
--remote-debugging-address=0.0.0.0
|
||||||
--remote-debugging-port=9221
|
--remote-debugging-port=9221
|
||||||
--user-data-dir="${USER_DATA_DIR}"
|
--user-data-dir="${USER_DATA_DIR}"
|
||||||
--disable-dev-shm-usage
|
--disable-dev-shm-usage
|
||||||
--no-first-run
|
--no-first-run
|
||||||
--no-default-browser-check
|
--no-default-browser-check
|
||||||
--disable-background-networking
|
--disable-background-networking
|
||||||
--disable-sync
|
--disable-sync
|
||||||
--password-store=basic
|
--password-store=basic
|
||||||
"--simulate-outdated-no-au=Tue, 31 Dec 2099 23:59:59 GMT"
|
"--simulate-outdated-no-au=Tue, 31 Dec 2099 23:59:59 GMT"
|
||||||
--start-maximized
|
--start-maximized
|
||||||
)
|
)
|
||||||
|
|
||||||
# Container reality: sandbox usually breaks unless seccomp is fully disabled
|
# Container reality: sandbox usually breaks unless seccomp is fully disabled
|
||||||
if [[ "${seccomp_mode}" != "0" ]]; then
|
if [[ "${seccomp_mode}" != "0" ]]; then
|
||||||
args+=(--no-sandbox --test-type)
|
args+=( --no-sandbox --test-type )
|
||||||
fi
|
fi
|
||||||
|
|
||||||
# IMPORTANT:
|
# IMPORTANT:
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 1.3 KiB |
@@ -1,41 +1,41 @@
|
|||||||
|
|
||||||
## 2.56.7 (2026-09-12)
|
## 2.56.7 (2026-09-12)
|
||||||
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
||||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||||
|
|
||||||
## 2.56.3 (2026-09-05)
|
## 2.56.3 (2026-09-05)
|
||||||
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
||||||
|
|
||||||
## 2.56.0 (2026-08-22)
|
## 2.56.0 (2026-08-22)
|
||||||
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
||||||
|
|
||||||
## 2.55.4 (2026-08-13)
|
## 2.55.4 (2026-08-13)
|
||||||
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
||||||
|
|
||||||
## 2.55.3 (2026-08-08)
|
## 2.55.3 (2026-08-08)
|
||||||
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
||||||
|
|
||||||
## 2.55.2 (2026-08-01)
|
## 2.55.2 (2026-08-01)
|
||||||
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
||||||
|
|
||||||
## 2.55.0 (2026-07-16)
|
## 2.55.0 (2026-07-16)
|
||||||
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
||||||
|
|
||||||
## 2.54.2 (2026-07-04)
|
## 2.54.2 (2026-07-04)
|
||||||
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
||||||
|
|
||||||
## 2.54.1 (2026-06-23)
|
## 2.54.1 (2026-06-23)
|
||||||
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
||||||
|
|
||||||
## 2.54.0 (2026-06-20)
|
## 2.54.0 (2026-06-20)
|
||||||
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
||||||
|
|
||||||
## 2.52.2 (2026-06-17)
|
## 2.52.2 (2026-06-17)
|
||||||
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
||||||
|
|
||||||
## 2.52.1 (2026-06-13)
|
## 2.52.1 (2026-06-13)
|
||||||
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
||||||
|
|
||||||
## 2.52.0 (2026-06-11)
|
## 2.52.0 (2026-06-11)
|
||||||
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
- Update to latest version from browserless/chrome (changelog : https://github.com/browserless/chrome/releases)
|
||||||
## 2.51.0 (2026-06-05)
|
## 2.51.0 (2026-06-05)
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.3 KiB After Width: | Height: | Size: 1.6 KiB |
@@ -1,17 +1,17 @@
|
|||||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||||
|
|
||||||
## 9.14.0 (2026-08-29)
|
## 9.14.0 (2026-08-29)
|
||||||
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)
|
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)
|
||||||
|
|
||||||
## 9.13.0 (2026-08-08)
|
## 9.13.0 (2026-08-08)
|
||||||
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)
|
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)
|
||||||
|
|
||||||
## 9.12.0 (2026-08-01)
|
## 9.12.0 (2026-08-01)
|
||||||
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)
|
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)
|
||||||
|
|
||||||
## 9.11.0 (2026-07-04)
|
## 9.11.0 (2026-07-04)
|
||||||
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)
|
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)
|
||||||
|
|
||||||
## 9.10.0 (2026-06-27)
|
## 9.10.0 (2026-06-27)
|
||||||
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)
|
- Update to latest version from linuxserver/docker-calibre (changelog : https://github.com/linuxserver/docker-calibre/releases)
|
||||||
|
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ Please read the upstream container documentation for further info: https://githu
|
|||||||
### Options
|
### Options
|
||||||
|
|
||||||
| Option | Type | Default | Description |
|
| Option | Type | Default | Description |
|
||||||
| -------- | ------ | --------- | ------------- |
|
|--------|------|---------|-------------|
|
||||||
| `PGID` | int | `0` | Group ID for file permissions |
|
| `PGID` | int | `0` | Group ID for file permissions |
|
||||||
| `PUID` | int | `0` | User ID for file permissions |
|
| `PUID` | int | `0` | User ID for file permissions |
|
||||||
| `TZ` | str | | Timezone (e.g., `Europe/London`) |
|
| `TZ` | str | | Timezone (e.g., `Europe/London`) |
|
||||||
|
|||||||
|
Before Width: | Height: | Size: 3.1 KiB After Width: | Height: | Size: 1.4 KiB |
@@ -1,5 +1,5 @@
|
|||||||
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
- Migrate legacy add-on configuration map names to current app configuration terminology.
|
||||||
|
|
||||||
## 0.6.27.4 (2026-09-04)
|
## 0.6.27.4 (2026-09-04)
|
||||||
- Fix: Kobo sync could not be enabled, failing with "Kepubify binary not found" even when the path was set by hand. The LinuxServer base image installs the converter as `/usr/bin/kepubify` with `curl -o`, which leaves it mode 0644 and gives it a name calibre-web does not accept : `binary_helper.py` only takes `kepubify-linux-64bit` or `kepubify-linux-32bit`, and only when `os.access(X_OK)` passes. The addon now makes the binary executable and publishes it as `/opt/kepubify/kepubify-linux-64bit`, the directory calibre-web's own autodetection already probes, so the path is filled in without any manual step (https://github.com/alexbelgium/hassio-addons/issues/3040)
|
- Fix: Kobo sync could not be enabled, failing with "Kepubify binary not found" even when the path was set by hand. The LinuxServer base image installs the converter as `/usr/bin/kepubify` with `curl -o`, which leaves it mode 0644 and gives it a name calibre-web does not accept : `binary_helper.py` only takes `kepubify-linux-64bit` or `kepubify-linux-32bit`, and only when `os.access(X_OK)` passes. The addon now makes the binary executable and publishes it as `/opt/kepubify/kepubify-linux-64bit`, the directory calibre-web's own autodetection already probes, so the path is filled in without any manual step (https://github.com/alexbelgium/hassio-addons/issues/3040)
|
||||||
- Fix: on installs created before that change, calibre-web had already run its autodetection once, found nothing usable and stored an empty path, and it never retries. An empty path is now reset so calibre-web detects the converter itself at the next start. A path set by hand is left alone
|
- Fix: on installs created before that change, calibre-web had already run its autodetection once, found nothing usable and stored an empty path, and it never retries. An empty path is now reset so calibre-web detects the converter itself at the next start. A path set by hand is left alone
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ Default password: admin123
|
|||||||
### Options
|
### Options
|
||||||
|
|
||||||
| Option | Type | Default | Description |
|
| Option | Type | Default | Description |
|
||||||
| -------- | ------ | --------- | ------------- |
|
|--------|------|---------|-------------|
|
||||||
| `PGID` | int | `0` | Group ID for file permissions |
|
| `PGID` | int | `0` | Group ID for file permissions |
|
||||||
| `PUID` | int | `0` | User ID for file permissions |
|
| `PUID` | int | `0` | User ID for file permissions |
|
||||||
| `TZ` | str | | Timezone (e.g., `Europe/London`) |
|
| `TZ` | str | | Timezone (e.g., `Europe/London`) |
|
||||||
@@ -99,7 +99,7 @@ This addon supports mounting both local drives and remote SMB shares:
|
|||||||
|
|
||||||
### Optional Calibre-Web features
|
### Optional Calibre-Web features
|
||||||
|
|
||||||
Calibre-Web documents optional extras that a manual installation adds with `pip install calibreweb[metadata]` and similar. **You do not need to install anything here**: the LinuxServer base image this add-on builds on installs Calibre-Web's `requirements.txt` _and_ its full `optional-requirements.txt` into the application's virtualenv, so the gdrive, gmail, goodreads, ldap, oauth, metadata, comics and kobo dependencies are all present already. Running `pip install calibreweb[...]` inside the container is not a supported way to enable them: it installs the PyPI distribution of Calibre-Web over an installation that already has those dependencies, and it can disturb the versions the base image pinned. It is also thrown away, because the Supervisor recreates the add-on container on restart.
|
Calibre-Web documents optional extras that a manual installation adds with `pip install calibreweb[metadata]` and similar. **You do not need to install anything here**: the LinuxServer base image this add-on builds on installs Calibre-Web's `requirements.txt` *and* its full `optional-requirements.txt` into the application's virtualenv, so the gdrive, gmail, goodreads, ldap, oauth, metadata, comics and kobo dependencies are all present already. Running `pip install calibreweb[...]` inside the container is not a supported way to enable them: it installs the PyPI distribution of Calibre-Web over an installation that already has those dependencies, and it can disturb the versions the base image pinned. It is also thrown away, because the Supervisor recreates the add-on container on restart.
|
||||||
|
|
||||||
Optional features are switched on in the Calibre-Web web interface, not in the add-on options, under `Admin` -> `Basic Configuration` -> `Feature Configuration` (for example `Enable Uploads`, `Enable Kobo sync`, `Use Goodreads`).
|
Optional features are switched on in the Calibre-Web web interface, not in the add-on options, under `Admin` -> `Basic Configuration` -> `Feature Configuration` (for example `Enable Uploads`, `Enable Kobo sync`, `Use Goodreads`).
|
||||||
|
|
||||||
|
|||||||
@@ -30,16 +30,16 @@ else
|
|||||||
# duplicates it can leave behind are entries calibre-web skips or already trusts.
|
# duplicates it can leave behind are entries calibre-web skips or already trusts.
|
||||||
# The column only exists once calibre-web 0.6.27+ has migrated app.db and cont-init runs
|
# The column only exists once calibre-web 0.6.27+ has migrated app.db and cont-init runs
|
||||||
# before calibre-web, so a failure here is not fatal : the next start applies it.
|
# before calibre-web, so a failure here is not fatal : the next start applies it.
|
||||||
trusted_ips_error=$(sqlite3 /config/app.db "update settings set config_reverse_proxy_trusted_ips='127.0.0.1,::1,::ffff:127.0.0.1,172.30.32.0/23,::ffff:172.30.32.0/119,'||coalesce(config_reverse_proxy_trusted_ips,'') where coalesce(config_reverse_proxy_trusted_ips,'') not like '%::ffff:172.30.32.0/119%'" 2>&1) \
|
trusted_ips_error=$(sqlite3 /config/app.db "update settings set config_reverse_proxy_trusted_ips='127.0.0.1,::1,::ffff:127.0.0.1,172.30.32.0/23,::ffff:172.30.32.0/119,'||coalesce(config_reverse_proxy_trusted_ips,'') where coalesce(config_reverse_proxy_trusted_ips,'') not like '%::ffff:172.30.32.0/119%'" 2>&1) ||
|
||||||
|| bashio::log.warning "Could not set the ingress trusted ip list, it will be applied at next start (${trusted_ips_error})"
|
bashio::log.warning "Could not set the ingress trusted ip list, it will be applied at next start (${trusted_ips_error})"
|
||||||
|
|
||||||
# Calibre-web autodetects kepubify only while this setting is still NULL. On every install
|
# Calibre-web autodetects kepubify only while this setting is still NULL. On every install
|
||||||
# that predates the Dockerfile fix above, that detection already ran, found nothing usable and
|
# that predates the Dockerfile fix above, that detection already ran, found nothing usable and
|
||||||
# stored an empty string, so it is never retried. Put an empty value back to NULL and
|
# stored an empty string, so it is never retried. Put an empty value back to NULL and
|
||||||
# calibre-web detects /opt/kepubify itself when it starts, a few seconds after this runs.
|
# calibre-web detects /opt/kepubify itself when it starts, a few seconds after this runs.
|
||||||
# A path the user set by hand is not empty and is left alone.
|
# A path the user set by hand is not empty and is left alone.
|
||||||
kepubify_error=$(sqlite3 /config/app.db "update settings set config_kepubifypath = NULL where config_kepubifypath = ''" 2>&1) \
|
kepubify_error=$(sqlite3 /config/app.db "update settings set config_kepubifypath = NULL where config_kepubifypath = ''" 2>&1) ||
|
||||||
|| bashio::log.warning "Could not reset the kepubify path, it will be applied at next start (${kepubify_error})"
|
bashio::log.warning "Could not reset the kepubify path, it will be applied at next start (${kepubify_error})"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
bashio::log.info "Default username:password is admin:admin123"
|
bashio::log.info "Default username:password is admin:admin123"
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
|
|
||||||
## 0.60.4 (2026-09-12)
|
## 0.60.4 (2026-09-12)
|
||||||
- Update to latest version from linuxserver/docker-changedetection.io (changelog : https://github.com/linuxserver/docker-changedetection.io/releases)
|
- Update to latest version from linuxserver/docker-changedetection.io (changelog : https://github.com/linuxserver/docker-changedetection.io/releases)
|
||||||
|
|
||||||
## 0.60.3 (2026-09-05)
|
## 0.60.3 (2026-09-05)
|
||||||
- Update to latest version from linuxserver/docker-changedetection.io (changelog : https://github.com/linuxserver/docker-changedetection.io/releases)
|
- Update to latest version from linuxserver/docker-changedetection.io (changelog : https://github.com/linuxserver/docker-changedetection.io/releases)
|
||||||
|
|
||||||
## 0.55.8 (2026-07-16)
|
## 0.55.8 (2026-07-16)
|
||||||
- Update to latest version from linuxserver/docker-changedetection.io (changelog : https://github.com/linuxserver/docker-changedetection.io/releases)
|
- Update to latest version from linuxserver/docker-changedetection.io (changelog : https://github.com/linuxserver/docker-changedetection.io/releases)
|
||||||
|
|
||||||
|
|||||||
@@ -52,7 +52,7 @@ You can add a shortcut pointing to your Changedetection.io instance with the fol
|
|||||||
### Options
|
### Options
|
||||||
|
|
||||||
| Option | Type | Default | Description |
|
| Option | Type | Default | Description |
|
||||||
| -------- | ------ | --------- | ------------- |
|
|--------|------|---------|-------------|
|
||||||
| `PGID` | int | `0` | Group ID for file permissions |
|
| `PGID` | int | `0` | Group ID for file permissions |
|
||||||
| `PUID` | int | `0` | User ID for file permissions |
|
| `PUID` | int | `0` | User ID for file permissions |
|
||||||
| `TZ` | str | | Timezone (e.g., `Europe/London`) |
|
| `TZ` | str | | Timezone (e.g., `Europe/London`) |
|
||||||
|
|||||||