mirror of
https://github.com/alexbelgium/hassio-addons.git
synced 2026-08-23 13:23:32 +02:00
* chore: add hassio-addon-workflow skill for Claude Code Checks in the repo-specific skill so future Claude Code sessions get the tiered scope->measure->plan->implement->verify workflow, repo traps, and helper scripts (preflight/measure/env_trace/validate/pr_review) without depending on a local machine's ~/.claude config. * fix(skill): address PR review feedback from Copilot and Codex - SKILL.md: repo-relative script invocation (skill is now checked in); correct the CI-gates list — the PR add-on linter is blocking, only the weekly Super-Linter is non-blocking - preflight.sh: git-aware repo detection (worktrees have a .git file) - pr_review.sh: header now documents resolve's actual --all behavior - measure.sh: CPU% uses getconf CLK_TCK; sample all processes, not the top-24 by RSS - env_trace.sh: validate VAR as a strict env-var name before regex use - validate.sh: shellcheck also covers extensionless run/finish; --vs-master skips visibly when a linter is missing instead of reporting a false clean * fix(skill): address CodeRabbit review feedback - pr_review.sh: resolve exits nonzero unless every thread actually resolved; watch exits nonzero and says so when checks settle with failures instead of reporting bare "settled" - validate.sh: pass the config.yaml path to Python as argv instead of interpolating $ADDON into the source (CWE-94)
129 lines
6.1 KiB
Bash
Executable File
129 lines
6.1 KiB
Bash
Executable File
#!/usr/bin/env bash
|
|
# Run every linter that CI will run and that works locally. The Docker build is deliberately not
|
|
# attempted: dockerd does not start in this environment, so CI is the only gate for it — say that
|
|
# rather than implying the build was checked.
|
|
#
|
|
# --vs-master re-lints each changed file at origin/master and prints only findings your diff
|
|
# ADDED. Without it you will chase warnings that were already in the file.
|
|
#
|
|
# Usage: validate.sh [addon-dir] [--vs-master]
|
|
set -uo pipefail
|
|
|
|
ADDON="${1:-}"
|
|
[ "${ADDON:-}" = "--vs-master" ] && { ADDON=""; set -- --vs-master; }
|
|
VS_MASTER=false
|
|
for a in "$@"; do [ "$a" = "--vs-master" ] && VS_MASTER=true; done
|
|
|
|
if [ -z "$ADDON" ]; then
|
|
mapfile -t _dirs < <(git diff --name-only origin/master...HEAD 2> /dev/null |
|
|
cut -d/ -f1 | sort -u | grep -vE '^\.' )
|
|
if [ "${#_dirs[@]}" -gt 1 ]; then
|
|
echo "several changed dirs: ${_dirs[*]}"
|
|
echo "pass one explicitly: validate.sh <addon-dir>"; exit 1
|
|
fi
|
|
ADDON="${_dirs[0]:-}"
|
|
fi
|
|
[ -z "$ADDON" ] && { echo "usage: validate.sh <addon-dir> [--vs-master]"; exit 1; }
|
|
git rev-parse --verify origin/master > /dev/null 2>&1 || {
|
|
echo "origin/master missing — run: git fetch origin master"; exit 1; }
|
|
export PYTHONDONTWRITEBYTECODE=1
|
|
echo "== validating $ADDON =="
|
|
|
|
fail=0
|
|
note() { printf ' %-13s %s\n' "$1" "$2"; }
|
|
|
|
# Shell: bash -n then shellcheck -x (follows sourced files, as CI does).
|
|
while IFS= read -r f; do
|
|
[ -f "$f" ] || continue
|
|
if ! out=$(bash -n "$f" 2>&1); then note "bash -n" "FAIL $f"; echo "$out" | sed 's/^/ /'; fail=1; fi
|
|
done < <(find "$ADDON" -type f \( -name '*.sh' -o -name 'run' -o -name 'finish' -o -name 'autostart' \) 2> /dev/null)
|
|
[ "$fail" -eq 0 ] && note "bash -n" "ok"
|
|
|
|
if command -v shellcheck > /dev/null 2>&1; then
|
|
sc=$(find "$ADDON" -type f \( -name '*.sh' -o -name 'autostart' -o -name 'run' -o -name 'finish' \) -print0 2> /dev/null |
|
|
xargs -0 -r shellcheck -x -f gcc 2>&1)
|
|
if [ -n "$sc" ]; then
|
|
note "shellcheck" "$(printf '%s\n' "$sc" | grep -c .) finding(s)"
|
|
printf '%s\n' "$sc" | sed 's/^/ /' | head -20
|
|
else note "shellcheck" "clean"; fi
|
|
fi
|
|
|
|
command -v hadolint > /dev/null 2>&1 && [ -f "$ADDON/Dockerfile" ] && {
|
|
hl=$(hadolint "$ADDON/Dockerfile" 2>&1)
|
|
[ -n "$hl" ] && { note "hadolint" "$(printf '%s\n' "$hl" | grep -c .) finding(s)"; printf '%s\n' "$hl" | sed 's/^/ /' | head -10; } || note "hadolint" "clean"
|
|
}
|
|
|
|
if [ -f "$ADDON/config.yaml" ]; then
|
|
# path passed as argv, never interpolated into Python source
|
|
python3 - "$ADDON/config.yaml" <<'PY' || { note "config.yaml" "FAIL parse"; fail=1; }
|
|
import yaml,sys
|
|
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 {})))
|
|
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)
|
|
PY
|
|
command -v yamllint > /dev/null 2>&1 && {
|
|
yl=$(yamllint -f parsable "$ADDON/config.yaml" 2>&1 | grep -c .)
|
|
note "yamllint" "$yl finding(s) (compare with --vs-master)"
|
|
}
|
|
fi
|
|
|
|
while IFS= read -r f; do
|
|
python3 -m py_compile "$f" 2> /dev/null || { note "py_compile" "FAIL $f"; fail=1; }
|
|
done < <(find "$ADDON" -type f -name '*.py' 2> /dev/null)
|
|
|
|
$VS_MASTER && command -v npx > /dev/null 2>&1 && [ -f "$ADDON/CHANGELOG.md" ] && {
|
|
md=$(npx --yes markdownlint-cli2 "$ADDON/CHANGELOG.md" 2>&1 | grep -cE "CHANGELOG.md:[0-9]+")
|
|
note "markdownlint" "$md finding(s) in CHANGELOG (mostly pre-existing; lint is continue-on-error in CI)"
|
|
}
|
|
|
|
echo
|
|
echo "== CI requirements =="
|
|
if git diff --name-only origin/master...HEAD 2> /dev/null | grep -q "$ADDON/CHANGELOG.md"; then
|
|
note "CHANGELOG" "updated"
|
|
else
|
|
# This one IS gated: onpr_check-pr.yaml exits 1 without it.
|
|
note "CHANGELOG" "NOT UPDATED — this is the one CI hard-gate"; fail=1
|
|
fi
|
|
if git diff origin/master...HEAD -- "$ADDON/config.yaml" 2> /dev/null | grep -q '^+version:'; then
|
|
note "version" "bumped"
|
|
else
|
|
# Repo convention and required for the rebuild to be offered — but no workflow gates it,
|
|
# so this is a warning, not a failure.
|
|
note "version" "NOT bumped (convention; no rebuild will be offered) — not a CI gate"
|
|
fi
|
|
note "docker build" "NOT tested locally (dockerd unavailable) — CI is the only gate"
|
|
|
|
if $VS_MASTER; then
|
|
echo
|
|
echo "== findings ADDED by this diff (pre-existing ones filtered out) =="
|
|
tmp=$(mktemp -d); trap 'rm -rf "$tmp"' EXIT
|
|
git diff --name-only origin/master...HEAD -- "$ADDON" 2> /dev/null | while IFS= read -r f; do
|
|
git show "origin/master:$f" > "$tmp/base" 2> /dev/null || continue
|
|
# A missing linter must be a visible skip, not a silent "no new findings":
|
|
# its "command not found" error is identical for base and head, so comm would
|
|
# cancel it out and report a false clean.
|
|
case "$f" in
|
|
*.sh | *autostart | */run | */finish)
|
|
command -v shellcheck > /dev/null 2>&1 || { echo " $f: SKIPPED (shellcheck not installed)"; continue; }
|
|
cmd() { shellcheck -x -f gcc "$1" 2>&1 | sed 's/^[^:]*:[0-9]*:[0-9]*://'; } ;;
|
|
*.yaml | *.yml)
|
|
command -v yamllint > /dev/null 2>&1 || { echo " $f: SKIPPED (yamllint not installed)"; continue; }
|
|
cmd() { yamllint -f parsable "$1" 2>&1 | sed 's/^[^:]*//; s/^:[0-9]*:[0-9]*//'; } ;;
|
|
*Dockerfile)
|
|
command -v hadolint > /dev/null 2>&1 || { echo " $f: SKIPPED (hadolint not installed)"; continue; }
|
|
cmd() { hadolint "$1" 2>&1 | sed 's/^[^:]*//; s/^:[0-9]*//'; } ;;
|
|
*) continue ;;
|
|
esac
|
|
cp "$tmp/base" "$tmp/base_f"; b=$(cmd "$tmp/base_f" | sort)
|
|
a=$(cmd "$f" | sort)
|
|
new=$(comm -13 <(printf '%s\n' "$b") <(printf '%s\n' "$a") | grep -c .)
|
|
[ "$new" -gt 0 ] && { echo " $f: $new NEW finding(s)"; comm -13 <(printf '%s\n' "$b") <(printf '%s\n' "$a") | sed 's/^/ /' | head -5; }
|
|
done
|
|
echo " (nothing listed above = your diff introduced no new lint findings)"
|
|
fi
|
|
|
|
echo
|
|
[ "$fail" -eq 0 ] && echo "== local validation passed ==" || echo "== local validation FAILED =="
|
|
exit "$fail"
|