learning_ai_common_plat/scripts/lint-infra.sh

349 lines
12 KiB
Bash
Executable File

#!/usr/bin/env bash
# ═══════════════════════════════════════════════════════════════════════
# lint-infra.sh — Infrastructure validation for the ByteLyst ecosystem
# ═══════════════════════════════════════════════════════════════════════
# Validates all Dockerfiles (hadolint) and Helm charts (helm lint + template)
# across the 11 ByteLyst repos.
#
# Usage:
# ./scripts/lint-infra.sh # Lint everything (Dockerfiles + Helm charts)
# ./scripts/lint-infra.sh --docker # Dockerfiles only
# ./scripts/lint-infra.sh --helm # Helm charts only
# ./scripts/lint-infra.sh --fix # Show suggested fixes alongside errors
# ./scripts/lint-infra.sh path/to/Dockerfile [path/to/Chart-dir] ...
#
# Prerequisites:
# brew install hadolint helm
#
# Exit codes:
# 0 All checks passed
# 1 One or more checks failed
# ═══════════════════════════════════════════════════════════════════════
set -euo pipefail
# ── Colors ─────────────────────────────────────────────────────────────
RED='\033[0;31m'; GREEN='\033[0;32m'; YELLOW='\033[1;33m'
CYAN='\033[0;36m'; BOLD='\033[1m'; DIM='\033[2m'; NC='\033[0m'
# ── Globals ────────────────────────────────────────────────────────────
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
WORKSPACE_ROOT="$(cd "$REPO_ROOT/.." && pwd)"
LINT_DOCKER=true
LINT_HELM=true
SHOW_FIX=false
EXPLICIT_PATHS=()
PASS=0
FAIL=0
SKIP=0
FAILURES=()
# ── Known Hadolint false-positive suppressions ─────────────────────────
# DL3045: COPY to relative path — all our Dockerfiles set WORKDIR first
# DL3018: Pin versions in apk add — we use node:22-alpine base images
# DL3008: Pin versions in apt-get — impractical for Python sidecar base images
# DL3059: Multiple consecutive RUN — intentional for layer caching
# SC2155: Declare and assign separately — false positive for BuildKit secret mount pattern
# (export VAR="$(cat /run/secrets/...)" is the canonical Docker secret idiom)
HADOLINT_IGNORE="DL3045,DL3018,DL3008,DL3059,SC2155"
# ── ByteLyst ecosystem repos (relative to workspace root) ─────────────
ECOSYSTEM_REPOS=(
learning_ai_common_plat
learning_voice_ai_agent
learning_multimodal_memory_agents
learning_ai_clock
learning_ai_jarvis_jr
learning_ai_fastgap
learning_ai_peakpulse
learning_ai_flowmonk
learning_ai_notes
learning_ai_trails
learning_ai_local_memory_gpt
)
# ── Helpers ────────────────────────────────────────────────────────────
log() { echo -e "${CYAN}[lint-infra]${NC} $*"; }
ok() { echo -e " ${GREEN}${NC} $*"; PASS=$((PASS + 1)); }
fail() { echo -e " ${RED}${NC} $*"; FAIL=$((FAIL + 1)); FAILURES+=("$1"); }
skip() { echo -e " ${DIM}$*${NC}"; SKIP=$((SKIP + 1)); }
warn() { echo -e " ${YELLOW}${NC} $*"; }
usage() {
echo "Usage: $(basename "$0") [OPTIONS] [PATH ...]"
echo ""
echo "Options:"
echo " --docker Lint Dockerfiles only"
echo " --helm Lint Helm charts only"
echo " --fix Show suggested fixes alongside errors"
echo " -h, --help Show this help"
echo ""
echo "If PATHs are given, only those files/dirs are checked."
echo "Otherwise, auto-discovers across all 11 ByteLyst repos."
echo ""
echo "Prerequisites: brew install hadolint helm"
}
# Check if a tool is available, print install instructions if not
require_tool() {
local tool="$1" install_cmd="$2"
if ! command -v "$tool" &>/dev/null; then
echo -e "${RED}Error:${NC} '${tool}' is not installed."
echo -e " Install: ${BOLD}${install_cmd}${NC}"
echo ""
return 1
fi
return 0
}
# Return a short relative path for display
short_path() {
local full="$1"
# Try to make path relative to workspace root
if [[ "$full" == "$WORKSPACE_ROOT"/* ]]; then
echo "${full#$WORKSPACE_ROOT/}"
else
echo "$full"
fi
}
# ── Auto-discovery ─────────────────────────────────────────────────────
discover_dockerfiles() {
local files=()
for repo in "${ECOSYSTEM_REPOS[@]}"; do
local repo_dir="${WORKSPACE_ROOT}/${repo}"
[ -d "$repo_dir" ] || continue
while IFS= read -r -d '' f; do
files+=("$f")
done < <(find "$repo_dir" -maxdepth 4 -name 'Dockerfile*' \
-not -path '*/node_modules/*' \
-not -path '*/.git/*' \
-not -path '*/_deferred*' \
-print0 2>/dev/null | sort -z)
done
echo "${files[@]}"
}
discover_helm_charts() {
local dirs=()
for repo in "${ECOSYSTEM_REPOS[@]}"; do
local repo_dir="${WORKSPACE_ROOT}/${repo}"
[ -d "$repo_dir" ] || continue
while IFS= read -r -d '' f; do
dirs+=("$(dirname "$f")")
done < <(find "$repo_dir" -maxdepth 4 -name 'Chart.yaml' \
-not -path '*/node_modules/*' \
-not -path '*/.git/*' \
-print0 2>/dev/null | sort -z)
done
echo "${dirs[@]+"${dirs[@]}"}"
}
# ── Lint functions ─────────────────────────────────────────────────────
lint_dockerfile() {
local file="$1"
local display
display=$(short_path "$file")
if [ ! -f "$file" ]; then
skip "${display} (not found)"
return
fi
local output exit_code=0
# Build hadolint args
local hadolint_args=(--no-color)
IFS=',' read -ra ignore_rules <<< "$HADOLINT_IGNORE"
for rule in "${ignore_rules[@]}"; do
hadolint_args+=(--ignore "$rule")
done
output=$(hadolint "${hadolint_args[@]}" "$file" 2>&1) || exit_code=$?
if [ $exit_code -eq 0 ]; then
ok "${display}"
else
fail "${display}"
# Print each warning/error indented
while IFS= read -r line; do
if [[ "$line" == *"error"* ]]; then
echo -e " ${RED}${line}${NC}"
elif [[ "$line" == *"warning"* ]]; then
echo -e " ${YELLOW}${line}${NC}"
else
echo -e " ${DIM}${line}${NC}"
fi
done <<< "$output"
if $SHOW_FIX; then
echo -e " ${CYAN}Fix: review hadolint wiki — https://github.com/hadolint/hadolint/wiki${NC}"
fi
fi
}
lint_helm_chart() {
local chart_dir="$1"
local display
display=$(short_path "$chart_dir")
if [ ! -f "${chart_dir}/Chart.yaml" ]; then
skip "${display} (no Chart.yaml)"
return
fi
local output exit_code
# 1. helm lint (syntax + structure)
output=$(helm lint "$chart_dir" 2>&1) || exit_code=$?
if [ "${exit_code:-0}" -ne 0 ]; then
fail "${display} (helm lint)"
echo "$output" | while IFS= read -r line; do
echo -e " ${DIM}${line}${NC}"
done
return
fi
# 2. helm template (renders without errors)
output=$(helm template test-release "$chart_dir" 2>&1) || exit_code=$?
if [ "${exit_code:-0}" -ne 0 ]; then
fail "${display} (helm template)"
echo "$output" | tail -10 | while IFS= read -r line; do
echo -e " ${DIM}${line}${NC}"
done
return
fi
ok "${display}"
}
# ── Parse CLI ──────────────────────────────────────────────────────────
parse_args() {
while [ $# -gt 0 ]; do
case "$1" in
--docker)
LINT_DOCKER=true; LINT_HELM=false ;;
--helm)
LINT_DOCKER=false; LINT_HELM=true ;;
--fix)
SHOW_FIX=true ;;
-h|--help)
usage; exit 0 ;;
-*)
echo "Unknown option: $1"; usage; exit 1 ;;
*)
EXPLICIT_PATHS+=("$1") ;;
esac
shift
done
}
# ── Main ───────────────────────────────────────────────────────────────
main() {
parse_args "$@"
echo ""
echo -e "${BOLD}╔═══════════════════════════════════════════════════════════╗${NC}"
echo -e "${BOLD}║ ByteLyst Infrastructure Lint ║${NC}"
echo -e "${BOLD}╚═══════════════════════════════════════════════════════════╝${NC}"
echo ""
# ── Tool checks ────────────────────────────────────────────────────
local missing=false
if $LINT_DOCKER; then
require_tool hadolint "brew install hadolint" || missing=true
fi
if $LINT_HELM; then
require_tool helm "brew install helm" || missing=true
fi
if $missing; then
echo -e "${RED}Missing required tools. Install them and re-run.${NC}"
exit 1
fi
# ── Explicit paths mode ────────────────────────────────────────────
if [ ${#EXPLICIT_PATHS[@]} -gt 0 ]; then
log "Linting ${#EXPLICIT_PATHS[@]} explicit path(s)..."
echo ""
for p in "${EXPLICIT_PATHS[@]}"; do
if [[ "$(basename "$p")" == Dockerfile* ]]; then
lint_dockerfile "$p"
elif [ -f "${p}/Chart.yaml" ]; then
lint_helm_chart "$p"
else
warn "Unknown target: $p (expected Dockerfile* or dir with Chart.yaml)"
fi
done
else
# ── Auto-discover mode ─────────────────────────────────────────
if $LINT_DOCKER; then
log "Discovering Dockerfiles across ${#ECOSYSTEM_REPOS[@]} repos..."
echo ""
local dockerfiles
IFS=' ' read -ra dockerfiles <<< "$(discover_dockerfiles)"
if [ ${#dockerfiles[@]} -eq 0 ]; then
warn "No Dockerfiles found."
else
log "Found ${#dockerfiles[@]} Dockerfile(s)"
echo ""
for df in "${dockerfiles[@]}"; do
lint_dockerfile "$df"
done
fi
echo ""
fi
if $LINT_HELM; then
log "Discovering Helm charts across ${#ECOSYSTEM_REPOS[@]} repos..."
echo ""
local charts
IFS=' ' read -ra charts <<< "$(discover_helm_charts)"
if [ ${#charts[@]} -eq 0 ] || [ -z "${charts[0]:-}" ]; then
log "No Helm charts found (none in this workspace)."
else
log "Found ${#charts[@]} Helm chart(s)"
echo ""
for chart in "${charts[@]}"; do
lint_helm_chart "$chart"
done
fi
echo ""
fi
fi
# ── Summary ──────────────────────────────────────────────────────
echo ""
echo -e "${BOLD}═══ Summary ═══${NC}"
echo -e " ${GREEN}Passed:${NC} ${PASS}"
echo -e " ${RED}Failed:${NC} ${FAIL}"
if [ $SKIP -gt 0 ]; then
echo -e " ${DIM}Skipped: ${SKIP}${NC}"
fi
echo ""
if [ $FAIL -gt 0 ]; then
echo -e "${RED}${BOLD}FAILED${NC}${FAIL} issue(s) found:"
for f in "${FAILURES[@]}"; do
echo -e " ${RED}${NC} ${f}"
done
echo ""
echo -e "${DIM}Suppressed rules: ${HADOLINT_IGNORE}${NC}"
echo -e "${DIM}To see fix suggestions, re-run with --fix${NC}"
exit 1
else
echo -e "${GREEN}${BOLD}ALL PASSED${NC}${PASS} check(s) clean."
if [ $SKIP -gt 0 ]; then
echo -e "${DIM}(${SKIP} skipped)${NC}"
fi
exit 0
fi
}
main "$@"