#!/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 "$@"