diff --git a/AGENTS.md b/AGENTS.md index 0fc96eda..5bf73e57 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -344,6 +344,17 @@ npx tsx scripts/encrypt-migrate.ts --product chronomind # Live encrypt ./scripts/prep-consumer.sh /path/to/consumer-dir # pack + rewrite ./scripts/prep-consumer.sh /path/to/consumer-dir --restore # undo +# ── Infrastructure lint (Dockerfiles + Helm charts) ─ +./scripts/lint-infra.sh # Lint all 25 Dockerfiles + any Helm charts +./scripts/lint-infra.sh --docker # Dockerfiles only +./scripts/lint-infra.sh --helm # Helm charts only +./scripts/lint-infra.sh path/to/Dockerfile # Explicit path(s) +# Requires: brew install hadolint helm + +# ── Cross-repo typecheck + test ────────────────────── +./scripts/typecheck-all.sh # pnpm typecheck across all 11 repos +./scripts/test-all.sh # pnpm test --run across all 11 repos + # ── Health check all services ────────────────────── pnpm --filter @lysnrai/monitoring check ``` diff --git a/README.md b/README.md index 69783ea2..bacb9147 100644 --- a/README.md +++ b/README.md @@ -159,6 +159,27 @@ Each consumer repo has a convenience wrapper: `scripts/docker-prep.sh` (or `scri > **Dashboards inside this repo** (`dashboards/admin-web`, `dashboards/tracker-web`) use `workspace:*` refs and do NOT need this workflow — pnpm resolves them automatically. +## Infrastructure Lint + +Validates all 25 Dockerfiles across the 11 ByteLyst repos using [hadolint](https://github.com/hadolint/hadolint), and any Helm charts using `helm lint` + `helm template`. + +```bash +# Prerequisites +brew install hadolint helm + +# Lint everything +./scripts/lint-infra.sh + +# Dockerfiles only / Helm charts only +./scripts/lint-infra.sh --docker +./scripts/lint-infra.sh --helm + +# Explicit paths +./scripts/lint-infra.sh path/to/Dockerfile path/to/chart-dir +``` + +Suppressed rules (false positives for this codebase): `DL3045`, `DL3018`, `DL3008`, `DL3059`, `SC2155`. + ## Design Tokens Generate platform-specific token files from the canonical JSON: diff --git a/scripts/lint-infra.sh b/scripts/lint-infra.sh new file mode 100755 index 00000000..c23fbade --- /dev/null +++ b/scripts/lint-infra.sh @@ -0,0 +1,348 @@ +#!/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 "$@" diff --git a/scripts/test-all.sh b/scripts/test-all.sh new file mode 100755 index 00000000..4cb0f1c9 --- /dev/null +++ b/scripts/test-all.sh @@ -0,0 +1,95 @@ +#!/usr/bin/env bash +# ═══════════════════════════════════════════════════════════════════════ +# test-all.sh — Run Vitest across all ByteLyst repos +# ═══════════════════════════════════════════════════════════════════════ +set -euo pipefail + +RED='\033[0;31m'; GREEN='\033[0;32m'; CYAN='\033[0;36m' +BOLD='\033[1m'; DIM='\033[2m'; NC='\033[0m' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORKSPACE_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +PASS=0 +FAIL=0 +SKIP=0 +FAILURES=() + +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 +) + +SUBS=(backend web user-dashboard-web mindlyst-native/web) + +echo "" +echo -e "${BOLD}╔═══════════════════════════════════════════════════════════╗${NC}" +echo -e "${BOLD}║ ByteLyst Cross-Repo Test Runner ║${NC}" +echo -e "${BOLD}╚═══════════════════════════════════════════════════════════╝${NC}" +echo "" + +for repo in "${REPOS[@]}"; do + repo_dir="${WORKSPACE_ROOT}/${repo}" + [ -d "$repo_dir" ] || continue + + for sub in "${SUBS[@]}"; do + pkg="${repo_dir}/${sub}/package.json" + [ -f "$pkg" ] || continue + + has_test=$(jq -r '.scripts.test // empty' "$pkg" 2>/dev/null) + [ -n "$has_test" ] || continue + + # Check if there are actually test files + test_count=$(find "${repo_dir}/${sub}/src" -name '*.test.ts' -o -name '*.test.tsx' 2>/dev/null | head -1) + if [ -z "$test_count" ]; then + SKIP=$((SKIP + 1)) + continue + fi + + label="${repo}/${sub}" + echo -ne " ${CYAN}▶${NC} ${label}..." + + if (cd "${repo_dir}/${sub}" && pnpm run test --run 2>&1) > /tmp/test-output-$$.txt 2>&1; then + # Extract test count from vitest output + summary=$(grep -E 'Tests\s+[0-9]' /tmp/test-output-$$.txt 2>/dev/null | tail -1 || echo "") + echo -e "\r ${GREEN}✓${NC} ${label} ${DIM}${summary}${NC}" + PASS=$((PASS + 1)) + else + echo -e "\r ${RED}✗${NC} ${label}" + grep -E 'FAIL|Error|failed' /tmp/test-output-$$.txt 2>/dev/null | tail -5 | while IFS= read -r line; do echo " ${line}"; done + FAIL=$((FAIL + 1)) + FAILURES+=("$label") + fi + rm -f /tmp/test-output-$$.txt + done +done + +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} (no test files)${NC}" +fi + +if [ $FAIL -gt 0 ]; then + echo "" + echo -e "${RED}${BOLD}FAILED${NC} — ${FAIL} test suite(s) failed:" + for f in "${FAILURES[@]}"; do + echo -e " ${RED}✗${NC} ${f}" + done + exit 1 +else + echo "" + echo -e "${GREEN}${BOLD}ALL PASSED${NC} — ${PASS} suite(s) green." + exit 0 +fi diff --git a/scripts/typecheck-all.sh b/scripts/typecheck-all.sh new file mode 100755 index 00000000..258ad511 --- /dev/null +++ b/scripts/typecheck-all.sh @@ -0,0 +1,83 @@ +#!/usr/bin/env bash +# ═══════════════════════════════════════════════════════════════════════ +# typecheck-all.sh — Run TypeScript typecheck across all ByteLyst repos +# ═══════════════════════════════════════════════════════════════════════ +set -euo pipefail + +RED='\033[0;31m'; GREEN='\033[0;32m'; CYAN='\033[0;36m' +BOLD='\033[1m'; DIM='\033[2m'; NC='\033[0m' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +WORKSPACE_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" + +PASS=0 +FAIL=0 +FAILURES=() + +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 +) + +# Subdirectories that may have their own typecheck script +SUBS=(backend web user-dashboard-web mindlyst-native/web) + +echo "" +echo -e "${BOLD}╔═══════════════════════════════════════════════════════════╗${NC}" +echo -e "${BOLD}║ ByteLyst Cross-Repo Typecheck ║${NC}" +echo -e "${BOLD}╚═══════════════════════════════════════════════════════════╝${NC}" +echo "" + +for repo in "${REPOS[@]}"; do + repo_dir="${WORKSPACE_ROOT}/${repo}" + [ -d "$repo_dir" ] || continue + + for sub in "${SUBS[@]}"; do + pkg="${repo_dir}/${sub}/package.json" + [ -f "$pkg" ] || continue + + has_tc=$(jq -r '.scripts.typecheck // empty' "$pkg" 2>/dev/null) + [ -n "$has_tc" ] || continue + + label="${repo}/${sub}" + echo -ne " ${CYAN}▶${NC} ${label}..." + + if (cd "${repo_dir}/${sub}" && pnpm run typecheck 2>&1) > /tmp/tc-output-$$.txt 2>&1; then + echo -e "\r ${GREEN}✓${NC} ${label}" + PASS=$((PASS + 1)) + else + echo -e "\r ${RED}✗${NC} ${label}" + tail -5 /tmp/tc-output-$$.txt | while IFS= read -r line; do echo " ${line}"; done + FAIL=$((FAIL + 1)) + FAILURES+=("$label") + fi + rm -f /tmp/tc-output-$$.txt + done +done + +echo "" +echo -e "${BOLD}═══ Summary ═══${NC}" +echo -e " ${GREEN}Passed:${NC} ${PASS}" +echo -e " ${RED}Failed:${NC} ${FAIL}" + +if [ $FAIL -gt 0 ]; then + echo "" + echo -e "${RED}${BOLD}FAILED${NC} — ${FAIL} typecheck(s) failed:" + for f in "${FAILURES[@]}"; do + echo -e " ${RED}✗${NC} ${f}" + done + exit 1 +else + echo "" + echo -e "${GREEN}${BOLD}ALL PASSED${NC} — ${PASS} typecheck(s) clean." + exit 0 +fi