Compare commits

...

7 Commits

Author SHA1 Message Date
Alexandre
278eeb931b Fix Bazarr ingress: keep redirects relative so they aren't blocked as mixed content (#2910)
* Fix Bazarr ingress: keep redirects relative so they aren't blocked as mixed content

Opening the Bazarr panel over HTTPS failed with:

  Mixed Content: ... requested an insecure frame
  'http://<ha_host>:8099/bazarr/'. This request has been blocked

Bazarr is Flask-based and answers /bazarr (the ingress entry, which has no
trailing slash) with a redirect to /bazarr/, made absolute against the Host
nginx sends upstream -- http://127.0.0.1:6767/bazarr/. proxy_redirect's
implicit "default" rule strips that prefix, which makes nginx treat the
Location as its own; the header filter then re-absolutises it as
$scheme://$host:$server_port/... Since $host is the browser's host forwarded
by the Supervisor and $server_port is the ingress port (8099, the Supervisor
default as no ingress_port is declared), the result is a plain-http URL on a
port the browser refuses to frame from an https page.

absolute_redirect off keeps the Location relative, and the proxy_redirect
rules re-prefix it with the ingress entry so it resolves under
/api/hassio_ingress/<token>/. The second rule also covers backends that emit
an already-relative Location; external absolute redirects match neither rule
and pass through untouched.

Verified against a local nginx with a stand-in backend: the pre-fix config
reproduces http://<host>:<ingress_port>/bazarr/ exactly, and the fixed config
returns /api/hassio_ingress/<token>/bazarr/ for both absolute and relative
upstream Locations while leaving an external redirect alone.

Also fixes the fallback base_url in services.d/nginx/run, which wrote it
without the leading / and so reintroduced the startup crash fixed in 1.5.6-4.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* Tighten base_url guard in nginx run script to require the leading slash

CodeRabbit review on #2910: the guard `grep -q "base_url.*$slug"` matches
both "base_url: bazarr" and "base_url: /bazarr" -- the .* swallows the slash
-- so it treated the malformed no-slash form as already correct and never
triggered the repair. Require the literal "base_url: /$slug" instead, so a
config missing the slash is actually detected and fixed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-27 15:52:00 +02:00
Alexandre
83aa854206 fix(ci): drop track_progress where the event has no issue/PR context (#2909)
The AI fix sweep has never completed a non-empty batch. claude-code-action
rejects track_progress unless the triggering event is pull_request, issues,
issue_comment, pull_request_review_comment or pull_request_review, and
daily_ai_fix.yaml only ever runs on schedule or workflow_dispatch. The step
died in input validation after 0.3s:

  Action failed with error: track_progress is only supported for events:
  pull_request, issues, issue_comment, pull_request_review_comment,
  pull_request_review. Current event: workflow_dispatch

This went unnoticed because the step is gated on `count != '0'`, so every
run with an empty batch skipped it and reported green. Every run that
actually had issues to work through failed identically (runs 30265277395,
30011599875).

daily_ai_fix.yaml: remove it — no trigger of this workflow can ever satisfy
the constraint. Claude still comments per issue via gh, as issue-fix.md
instructs; only the run-level sticky comment is lost.

on_issue_approved.yaml: same latent failure on its workflow_dispatch path,
but the `issues` path is valid, so gate it on the event instead of dropping
it. The action defaults this input to the string "false", so the expression
result is a shape it already handles.

Co-authored-by: Claude <noreply@anthropic.com>
2026-07-27 14:43:32 +02:00
Alexandre
885b055768 fix(ci): pass github_token to claude-code-action so AI workflows can auth (#2908)
Every claude-code-action step except on_claude_mention.yml left the github_token
input unset, so the action fell back to the OIDC -> Claude App token exchange.
That exchange requires github.actor to have write access on the repo; on an
issues.opened event the actor is the outside reporter, so it always 401'd.
Classify is continue-on-error, so the job went green while doing nothing.

Setting the input short-circuits the exchange (action.yml maps it to
OVERRIDE_GITHUB_TOKEN; token.ts returns it before requesting OIDC).

GITHUB_TOKEN for the read-only classifier; AI_PR_TOKEN for the three that push
branches or open PRs, so the resulting PR triggers CI.

Also dropped the workflow-level id-token: write grant, which is unreachable once
github_token is set (CodeRabbit).
2026-07-27 12:41:16 +02:00
Alexandre
79fb5a93ef Merge pull request #2907 from alexbelgium/claude/elegant-nobel-8ca7b6
fix(templates): stop corrupting option values in shell_quote / dotenv_quote
2026-07-27 12:21:25 +02:00
alexbelgium
b1f238a024 fix(templates): don't swallow bash -n's parse error in --self-test
Keep stderr from the dotenv env-file validation so a failure shows which line
broke instead of just "not valid shell".

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-27 11:24:05 +02:00
alexbelgium
041356e68b fix(templates): escape $ and backtick when writing /.env and /etc/environment
dotenv_quote emits a double-quoted value, and both files are read back by
sourcing them from a shell: browserless_chrome does "set -a; . /.env" from its
Dockerfile, wger copies /.env into /data/env.sh as export lines, and
fireflyiii_data_importer relies on /etc/environment for cron. Inside double
quotes $ and ` are still special, and neither was escaped, so the value was
expanded rather than read literally:

    pa$$w0rd    came back as pa904869w0rd   (the shell PID)
    ${HOME}     came back as /root
    back`tick`  ran tick as a command and kept only "back"

Escape both, after the existing backslash doubling so the added backslashes are
not doubled in turn.

Extend --self-test to cover this path as well: it now writes an env file, checks
it parses, sources it and compares. An unescaped backtick makes the file
unparseable, which would take the sourcing shell down with it, so that case is
reported rather than left to abort the run.

Values containing a real newline remain out of scope: dotenv_quote writes them
as a literal \n, which a dotenv parser unescapes but a shell does not.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-26 18:24:21 +02:00
alexbelgium
85bc9c723d fix(templates): stop corrupting option values containing a single quote
00-global_var.sh turns every addon option into an "export KEY='value'" block
that is injected at the top of cont-init scripts, service run scripts and the
shells, so the quoting has to survive an eval byte for byte.

Two defects sat on that path and hid each other:

- shell_quote replaced ' with '"'"' followed by a stray space, so a value like
  "O'Brien pass" reached the application as "O' Brien pass". Passwords and any
  option holding an apostrophe were silently wrong.
- shell_quote also doubled every backslash, and append_export then passed the
  result through "awk -v", which runs its own escape processing and halved it
  again. Backslash values (regexes, Windows and UNC paths) therefore survived
  by accident, and fixing either half alone breaks them: dropping the doubling
  leaves awk eating \t, \b and \\, while keeping it doubles the value for real
  once awk is gone.

shell_quote now applies the POSIX rule (only ' needs escaping, as '\'') and
append_export appends the line directly instead of going through awk, which
also drops a full rewrite of the block per option.

Add a --self-test that builds a real export block and sources it, so the check
covers the whole path rather than either helper in isolation -- testing them
separately is exactly what let this pair stay wrong:

    bash .templates/00-global_var.sh --self-test

Reported in #2768. dotenv_quote is left alone: its output is double-quoted, so
the doubling it does is correct there.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-26 17:45:39 +02:00
9 changed files with 209 additions and 63 deletions

View File

@@ -50,7 +50,6 @@ permissions:
contents: write
issues: write
pull-requests: write
id-token: write # claude-code-action fetches a GitHub OIDC token to auth the OAuth flow
concurrency:
group: ai-fix-sweep
@@ -129,9 +128,16 @@ jobs:
uses: anthropics/claude-code-action@44423bdec74b97d67543eb16c110546762c110b2 # v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# One sticky, auto-updating status comment per run instead of the
# model narrating its own progress in scattered comments.
track_progress: true
# Skip the OIDC -> Claude App token exchange. The scheduled path
# happens to pass it (github.actor is the maintainer), but
# workflow_dispatch by anyone else would 401. AI_PR_TOKEN, not
# GITHUB_TOKEN, so a PR Claude opens triggers CI.
github_token: ${{ secrets.AI_PR_TOKEN }}
# No track_progress here. It needs an issue or PR to hang its sticky
# comment on, and the action hard-fails validation without one; this
# workflow only ever runs on schedule/workflow_dispatch. Per-issue
# progress still gets reported — issue-fix.md has Claude comment on
# each issue directly via gh.
prompt: |
The batch of issues to work through is /tmp/ai-fix/batch.json.
Follow .github/prompts/issue-fix.md exactly. Do not deviate from

View File

@@ -32,7 +32,6 @@ permissions:
contents: write
issues: write
pull-requests: write
id-token: write # claude-code-action fetches a GitHub OIDC token to auth the OAuth flow
concurrency:
group: ai-approve-${{ github.event.issue.number || inputs.issue }}
@@ -139,7 +138,14 @@ jobs:
uses: anthropics/claude-code-action@44423bdec74b97d67543eb16c110546762c110b2 # v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
track_progress: true
# Skip the OIDC -> Claude App token exchange, which 401s whenever
# github.actor lacks write access. AI_PR_TOKEN, not GITHUB_TOKEN, so
# a PR Claude opens triggers CI.
github_token: ${{ secrets.AI_PR_TOKEN }}
# Only the `issues` path has a comment thread to track progress in.
# On workflow_dispatch there is none, and passing true there fails
# the action's input validation outright.
track_progress: ${{ github.event_name == 'issues' }}
prompt: |
The approved plan is /tmp/ai-exec/plan.md and the issue it belongs
to is /tmp/ai-exec/issue.json. Follow .github/prompts/issue-execute-plan.md

View File

@@ -41,7 +41,6 @@ on:
permissions:
contents: read
issues: write
id-token: write # claude-code-action fetches a GitHub OIDC token to auth the OAuth flow
concurrency:
group: ai-triage-${{ github.event.issue.number || inputs.issue || github.run_id }}
@@ -148,6 +147,12 @@ jobs:
uses: anthropics/claude-code-action@44423bdec74b97d67543eb16c110546762c110b2 # v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Without this the action falls back to the OIDC -> Claude App token
# exchange, which 401s ("User does not have write access on this
# repository") whenever github.actor is the outside reporter who
# opened the issue or replied to a needs-info request. Same token the
# step already exports as GH_TOKEN; classify only reads.
github_token: ${{ secrets.GITHUB_TOKEN }}
show_full_output: true
prompt: |
Read /tmp/ai-triage/context.md, then follow the instructions in

View File

@@ -25,7 +25,6 @@ permissions:
contents: write
pull-requests: write
issues: write
id-token: write # claude-code-action fetches a GitHub OIDC token to auth the OAuth flow
concurrency:
group: ai-coderabbit-${{ github.event.pull_request.number }}
@@ -83,6 +82,11 @@ jobs:
uses: anthropics/claude-code-action@44423bdec74b97d67543eb16c110546762c110b2 # v1
with:
claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }}
# Skip the OIDC -> Claude App token exchange, which 401s whenever
# github.actor lacks write access — here github.actor is
# coderabbitai[bot], the review submitter. AI_PR_TOKEN, not
# GITHUB_TOKEN, so the pushed fixes re-trigger CI on the PR.
github_token: ${{ secrets.AI_PR_TOKEN }}
prompt: |
CodeRabbit has reviewed pull request #${{ github.event.pull_request.number }}
on ${{ github.repository }}. You are on that PR's branch. Follow

View File

@@ -3,6 +3,160 @@
set -e
################################################################################
# Block markers, temp helper, quoting and export-block builders
#
# Everything here is free of side effects and defined before the Supervisor
# guard, so that the self-test below can exercise the whole value path outside a
# container, where bashio is not available.
################################################################################
BLOCK_BEGIN="# --- BEGIN ADDON ENV (generated) ---"
BLOCK_END="# --- END ADDON ENV (generated) ---"
mktemp_safe() {
local tmpdir="${TMPDIR:-/tmp}"
mkdir -p "$tmpdir"
mktemp "$tmpdir/tmp.XXXXXXXXXX"
}
dotenv_quote() {
# For /.env and /etc/environment: double quotes + minimal escaping.
#
# These files are read back by sourcing them from a shell, so every
# character that is still special inside double quotes has to be escaped.
# $ and ` used to be left alone, which meant a value was expanded instead of
# being read literally: a password like pa$$w0rd came back with the shell
# PID spliced into it, and a value containing backticks ran as a command.
#
# Backslash must be doubled first, so that the backslashes added below are
# not doubled in turn.
local v="$1"
v="${v//\\/\\\\}"
v="${v//\"/\\\"}"
v="${v//\$/\\\$}"
v="${v//\`/\\\`}"
v="${v//$'\n'/\\n}"
v="${v//$'\r'/\\r}"
printf '"%s"' "$v"
}
shell_quote() {
# Single-quote for safe injection into shell code.
#
# Inside single quotes every character is literal, so the only thing a value
# needs escaping for is the quote character itself: close the quote, emit an
# escaped quote, reopen it. Backslashes must be left untouched.
#
# This used to double every backslash and to replace ' with '"'"' followed by
# a stray space. The stray space corrupted every value containing a quote
# (O'Brien pass arrived as O' Brien pass); the doubling was undone further
# down the path by the "awk -v" in append_export, so backslashes survived by
# accident. Both halves are fixed together -- see the note in append_export.
local s="$1"
printf "'%s'" "${s//\'/\'\\\'\'}"
}
append_export() {
# Plain append, deliberately not awk: "awk -v q=$value" runs the value
# through awk's escape processing, which turns \t into a tab, \b into a
# backspace and \\ into a single backslash. That used to be cancelled out by
# shell_quote doubling every backslash, so the two bugs hid each other --
# fixing only one of them corrupts the value.
printf 'export %s=%s\n' "$1" "$(shell_quote "$2")" >> "$EXPORT_BODY"
}
compose_export_block() {
{
echo "$BLOCK_BEGIN"
echo "# Generated from $JSONSOURCE"
cat "$EXPORT_BODY"
echo "$BLOCK_END"
} > "$EXPORT_BLOCK"
}
################################################################################
# Self-test: bash .templates/00-global_var.sh --self-test
#
# Builds a real export block and sources it, which is exactly what happens once
# the block is injected at the top of a service run script, then checks that
# every value came back byte for byte. Testing the whole path matters: the two
# defects this guards against (shell_quote doubling backslashes and append_export
# passing values through "awk -v") cancelled each other out, so a test of either
# helper alone reported success while the pair was wrong.
#
# Runs before the Supervisor guard and exits, so it never affects startup.
################################################################################
if [[ "${1:-}" == "--self-test" ]]; then
# Literal test data: the single quotes and metacharacters are the point.
# shellcheck disable=SC2016
self_test_values=(
'plain.host'
'next\.duckdns\.org' # regex, dots escaped once
'next\\.duckdns\\.org' # regex, dots escaped twice by the user
'C:\Users\bob\share' # windows path, \U and \b are awk escapes
'\\server\share' # UNC path
'col\tsep' # \t is an awk escape
"O'Brien pass" # embedded quote
"it's a 'quoted' word" # several embedded quotes
"'leading"
"trailing'"
'a$b`c"d' # shell metacharacters
'*.example.com|^foo\d+$'
$'sp ace\ttab'
''
)
JSONSOURCE="self-test"
EXPORT_BODY="$(mktemp_safe)"
EXPORT_BLOCK="$(mktemp_safe)"
self_test_env="$(mktemp_safe)"
trap 'rm -f "$EXPORT_BODY" "$EXPORT_BLOCK" "$self_test_env"' EXIT
self_test_rc=0
self_test_check() {
# $1 name of the variable that was read back, $2 expected value, $3 how
local self_test_got="${!1}"
[[ "$self_test_got" == "$2" ]] && return 0
printf 'FAIL (%s): <%s> came back as <%s>\n' "$3" "$2" "$self_test_got"
self_test_rc=1
}
# 1. The export block, sourced the way an injected run script would
for self_test_i in "${!self_test_values[@]}"; do
append_export "SELFTEST_${self_test_i}" "${self_test_values[$self_test_i]}"
done
compose_export_block
# shellcheck source=/dev/null
. "$EXPORT_BLOCK"
for self_test_i in "${!self_test_values[@]}"; do
self_test_check "SELFTEST_${self_test_i}" "${self_test_values[$self_test_i]}" "export block"
done
# 2. /.env, sourced the way browserless_chrome and wger read it back.
# Values holding a newline are out of scope: dotenv_quote writes them as a
# literal \n, which a dotenv parser unescapes but a shell does not.
for self_test_i in "${!self_test_values[@]}"; do
printf 'DOTENVTEST_%s=%s\n' \
"$self_test_i" "$(dotenv_quote "${self_test_values[$self_test_i]}")"
done > "$self_test_env"
if ! bash -n "$self_test_env"; then
# An unescaped backtick or quote leaves the file unparseable, which would
# abort the sourcing shell instead of just yielding a wrong value.
echo "FAIL (dotenv): generated env file is not valid shell"
self_test_rc=1
else
# shellcheck source=/dev/null
. "$self_test_env"
for self_test_i in "${!self_test_values[@]}"; do
self_test_check "DOTENVTEST_${self_test_i}" "${self_test_values[$self_test_i]}" "dotenv"
done
fi
[[ "$self_test_rc" -eq 0 ]] &&
echo "${#self_test_values[@]} values round-tripped unchanged (export block + dotenv)"
exit "$self_test_rc"
fi
################################################################################
# Guard: only run inside Supervisor-managed add-ons
################################################################################
@@ -30,15 +184,6 @@ command -v jq >/dev/null || bashio::exit.nok "jq is required"
mkdir -p /etc
touch "$ETC_ENV_FILE"
################################################################################
# Temp helper
################################################################################
mktemp_safe() {
local tmpdir="${TMPDIR:-/tmp}"
mkdir -p "$tmpdir"
mktemp "$tmpdir/tmp.XXXXXXXXXX"
}
################################################################################
# Secrets support
################################################################################
@@ -68,54 +213,13 @@ resolve_secret() {
printf '%s' "$line"
}
################################################################################
# Quoting
################################################################################
dotenv_quote() {
# For /.env and /etc/environment: double quotes + minimal escaping
local v="$1"
v="${v//\\/\\\\}"
v="${v//\"/\\\"}"
v="${v//$'\n'/\\n}"
v="${v//$'\r'/\\r}"
printf '"%s"' "$v"
}
shell_quote() {
# Single-quote for safe injection in shell code
local s="$1"
s="${s//\\/\\\\}"
s="${s//\'/\'\"\'\"\' }"
s="${s% }"
printf "'%s'" "$s"
}
################################################################################
# S6 + script injection block
################################################################################
BLOCK_BEGIN="# --- BEGIN ADDON ENV (generated) ---"
BLOCK_END="# --- END ADDON ENV (generated) ---"
EXPORT_BLOCK="$(mktemp_safe)"
EXPORT_BODY="$(mktemp_safe)"
KV_FILE="$(mktemp_safe)"
trap 'rm -f "$EXPORT_BLOCK" "$KV_FILE"' EXIT
{
echo "$BLOCK_BEGIN"
echo "# Generated from $JSONSOURCE"
echo "$BLOCK_END"
} > "$EXPORT_BLOCK"
append_export() {
local k="$1" v="$2" q
q="$(shell_quote "$v")"
awk -v k="$k" -v q="$q" -v e="$BLOCK_END" '
$0==e { print "export " k "=" q }
{ print }
' "$EXPORT_BLOCK" > "$EXPORT_BLOCK.tmp"
mv "$EXPORT_BLOCK.tmp" "$EXPORT_BLOCK"
}
trap 'rm -f "$EXPORT_BLOCK" "$EXPORT_BODY" "$KV_FILE"' EXIT
inject_block() {
local f="$1" tmp
@@ -235,6 +339,8 @@ cp "$ENV_FILE" "$ETC_ENV_FILE"
################################################################################
# Inject into scripts and shells (best-effort)
################################################################################
compose_export_block
for f in /etc/services.d/*/run /etc/s6-overlay/s6-rc.d/*/run /etc/cont-init.d/*.sh /entrypoint.sh /etc/bash.bashrc "${GLOBAL_VAR_FILES:-}"; do
[[ -f "$f" ]] && inject_block "$f"
done

View File

@@ -1,4 +1,9 @@
## 1.6.0.1 (2026-07-27)
- Fix ingress: nginx rewrote Bazarr's redirects into an absolute `http://<host>:8099/...` URL, which the browser blocked as mixed content when Home Assistant is served over HTTPS. Redirects now stay relative and point at the ingress path
- Fix fallback base_url in the nginx service script missing its leading `/`, which crashed Bazarr on startup
## 1.6.0 (2026-07-08)
- Update to latest version from linuxserver/docker-bazarr (changelog : https://github.com/linuxserver/docker-bazarr/releases)

View File

@@ -112,4 +112,4 @@ schema:
slug: bazarr_nas
udev: true
url: https://github.com/alexbelgium/hassio-addons/tree/master/bazarr
version: "1.6.0"
version: "1.6.0.1"

View File

@@ -19,6 +19,19 @@ server {
proxy_set_header Connection $http_connection;
#auth_basic off;
# Adjust Location headers in backend redirects
# Bazarr is Flask-based and answers /bazarr with a redirect to /bazarr/,
# made absolute against the Host nginx sends upstream, so it reads
# http://127.0.0.1:6767/bazarr/. proxy_redirect strips that prefix, and
# nginx then re-absolutises the result as $scheme://$host:$server_port/...
# i.e. http://<ha_host>:8099/bazarr/ -- blocked by the browser as mixed
# content inside the ingress iframe. absolute_redirect off keeps it
# relative; the proxy_redirect rules re-prefix it with the ingress entry
# (the second rule also covers a redirect that was relative already).
absolute_redirect off; # Do not add port to redirect
proxy_redirect http://127.0.0.1:6767/ %%ingress_entry%%/;
proxy_redirect / %%ingress_entry%%/;
# Correct base_url
proxy_set_header Accept-Encoding "";
sub_filter_once off;

View File

@@ -15,9 +15,10 @@ bashio::net.wait_for "$port" localhost 900
if [ -f "$CONFIG_LOCATION" ]; then
if ! bashio::config.true "ingress_disabled"; then
if ! bashio::config.has_value "connection_mode" || [ "$(bashio::config 'connection_mode')" != "noingress_auth" ]; then
if ! grep -q "base_url.*$slug" "$CONFIG_LOCATION"; then
if ! grep -q "base_url: /$slug" "$CONFIG_LOCATION"; then
bashio::log.warning "BaseUrl not set properly, restarting"
sed -i "s/ base_url:.*/ base_url: $slug/" "$CONFIG_LOCATION"
# Must start with / for Flask blueprint registration
sed -i "s| base_url:.*| base_url: /$slug|" "$CONFIG_LOCATION"
bashio::addon.restart
fi
fi