diff --git a/AGENTS.md b/AGENTS.md index 6c64732..419c1c5 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2075,6 +2075,15 @@ Rules: and enables them. Logs: `journalctl --user -u skatespots-archive`. - Commits touching only `scripts/`, `*.md`, `cvm-server/`, `skills/`, `.agents/` are **skipped by the deploy timer** → no Netlify credits spent. +- **Dependency updates + vulnerability scan (2026-10-12):** `scripts/update-deps.sh` + runs `npm outdated` + `npm audit` (dated report in `~/.local/state/skatespots-deps/`), + applies **non-breaking** updates only (`npm update` + `npm audit fix`, never + `--force`), then gates on `npm test` + `smoke-test.mjs`: **green → commit + push** + (the deploy timer ships it); **red → restores the previous lockfile**. Self-gates + to one real run per **20 days** (`DEPS_INTERVAL_DAYS`), skips on a dirty tree, and + bounds the push (`timeout 180`) since the git remote signs via the remote signer. + Timer: `scripts/install-deps-timer.sh` → `skatespots-deps.timer` (daily tick). + Flags: `--scan-only`, `--no-push`, `--force`. Logs: `journalctl --user -u skatespots-deps`. ### Community relay — self-hosted Newlay (2026-10-12) diff --git a/scripts/install-deps-timer.sh b/scripts/install-deps-timer.sh new file mode 100755 index 0000000..4437d54 --- /dev/null +++ b/scripts/install-deps-timer.sh @@ -0,0 +1,16 @@ +#!/usr/bin/env bash +# +# Install the dependency-update + vulnerability-scan systemd user timer. +# scripts/install-deps-timer.sh +set -euo pipefail + +SRC="$(cd "$(dirname "$0")/systemd" && pwd)" +DEST="$HOME/.config/systemd/user" +mkdir -p "$DEST" + +cp "$SRC/skatespots-deps.service" "$DEST/" +cp "$SRC/skatespots-deps.timer" "$DEST/" + +systemctl --user daemon-reload +systemctl --user enable --now skatespots-deps.timer +systemctl --user list-timers skatespots-deps.timer --no-pager diff --git a/scripts/systemd/skatespots-deps.service b/scripts/systemd/skatespots-deps.service new file mode 100644 index 0000000..88c7513 --- /dev/null +++ b/scripts/systemd/skatespots-deps.service @@ -0,0 +1,6 @@ +[Unit] +Description=SkateSpots scheduled dependency update + vulnerability scan + +[Service] +Type=oneshot +ExecStart=%h/SkateSpots/scripts/update-deps.sh diff --git a/scripts/systemd/skatespots-deps.timer b/scripts/systemd/skatespots-deps.timer new file mode 100644 index 0000000..7b1e37d --- /dev/null +++ b/scripts/systemd/skatespots-deps.timer @@ -0,0 +1,10 @@ +[Unit] +Description=Daily tick for the SkateSpots dependency job (self-gated to ~20 days) + +[Timer] +OnCalendar=daily +Persistent=true +RandomizedDelaySec=30min + +[Install] +WantedBy=timers.target diff --git a/scripts/update-deps.sh b/scripts/update-deps.sh new file mode 100755 index 0000000..2948a76 --- /dev/null +++ b/scripts/update-deps.sh @@ -0,0 +1,124 @@ +#!/usr/bin/env bash +# +# Scheduled dependency update + vulnerability scan (every ~20 days). +# +# Driven by the systemd user timer `skatespots-deps.timer` (a daily tick; this +# script self-gates to one real run per INTERVAL_DAYS). What it does: +# 1. bails if the working tree is dirty (never mixes with your own changes); +# 2. scans `npm outdated` + `npm audit` into a dated report; +# 3. applies NON-breaking updates (`npm update` + `npm audit fix` — never --force); +# 4. runs the full test suite (`npm test`) AND the boot smoke test; +# 5. on success commits + pushes the lockfile/package.json (the deploy timer then +# ships it); on failure it RESTORES the previous lockfile and reports. +# +# Flags: +# --scan-only report only (no apply, no commit) +# --no-push apply + test + commit locally, but don't push +# --force ignore the interval gate +# +# Env: DEPS_INTERVAL_DAYS (default 20), DEPS_STATE_DIR (default ~/.local/state/skatespots-deps) +set -uo pipefail + +REPO="/home/sovereignsk8/SkateSpots" +export PATH="/home/sovereignsk8/.nvm/versions/node/v22.23.1/bin:$PATH" +cd "$REPO" + +INTERVAL_DAYS="${DEPS_INTERVAL_DAYS:-20}" +STATE_DIR="${DEPS_STATE_DIR:-$HOME/.local/state/skatespots-deps}" +mkdir -p "$STATE_DIR" +LAST="$STATE_DIR/last-run" +REPORT="$STATE_DIR/report-$(LC_ALL=C date +%F).txt" + +SCAN_ONLY=0; NO_PUSH=0; FORCE=0 +for a in "$@"; do + case "$a" in + --scan-only) SCAN_ONLY=1 ;; + --no-push) NO_PUSH=1 ;; + --force) FORCE=1 ;; + esac +done + +log() { echo "[$(date -Is)] $*"; } +notify() { + command -v notify-send >/dev/null 2>&1 && notify-send -a SkateSpots "Dependency job" "$1" 2>/dev/null || true +} +finish_timer() { date +%s > "$LAST"; } + +# ── interval gate ─────────────────────────────────────────────────────────── +if [ "$FORCE" -eq 0 ] && [ "$SCAN_ONLY" -eq 0 ] && [ -f "$LAST" ]; then + last=$(cat "$LAST" 2>/dev/null || echo 0) + now=$(date +%s) + elapsed=$(( now - last )) + if [ "$elapsed" -lt $(( INTERVAL_DAYS * 86400 )) ]; then + log "not due yet ($(( (INTERVAL_DAYS * 86400 - elapsed) / 86400 ))d left)" + exit 0 + fi +fi + +# ── scan ──────────────────────────────────────────────────────────────────── +{ + echo "=== SkateSpots dependency scan — $(date -Is) ===" + echo + echo "=== npm outdated ===" + npm outdated 2>&1 || true + echo + echo "=== npm audit ===" + npm audit 2>&1 || true +} | tee "$REPORT" + +if [ "$SCAN_ONLY" -eq 1 ]; then + log "scan-only; report: $REPORT" + exit 0 +fi + +# ── never mix with the user's uncommitted work ────────────────────────────── +if [ -n "$(git status --porcelain)" ]; then + log "working tree is dirty — skipping (commit or stash first)" + exit 0 +fi + +cp package.json "$STATE_DIR/package.json.bak" 2>/dev/null || true +cp package-lock.json "$STATE_DIR/package-lock.json.bak" 2>/dev/null || true + +# ── apply non-breaking updates ────────────────────────────────────────────── +log "applying non-breaking updates (npm update + npm audit fix)…" +npm update 2>&1 | tail -3 +npm audit fix 2>&1 | tail -3 +{ + echo; echo "=== post-update npm audit ==="; npm audit 2>&1 || true +} | tee -a "$REPORT" + +# ── gate: full tests + boot smoke ─────────────────────────────────────────── +log "running tests + smoke…" +if npm test >"$STATE_DIR/test.log" 2>&1 && node scripts/smoke-test.mjs >"$STATE_DIR/smoke.log" 2>&1; then + if [ -n "$(git status --porcelain package.json package-lock.json)" ]; then + git add package.json package-lock.json + git commit -m "chore(deps): scheduled dependency update + audit ($(LC_ALL=C date +%F))" >/dev/null 2>&1 || true + if [ "$NO_PUSH" -eq 0 ]; then + # Bound the push: the git remote signs via the remote signer and can stall. + if timeout 180 git push origin main >"$STATE_DIR/push.log" 2>&1; then + log "pushed dependency update" + notify "Dependency update applied, tests pass, pushed." + else + log "push failed/skipped (see $STATE_DIR/push.log)" + notify "Dependency update committed locally but the push failed." + fi + else + log "committed locally (--no-push)" + notify "Dependency update applied and committed locally." + fi + else + log "no dependency changes to commit" + notify "Dependency scan complete: nothing to update." + fi + finish_timer + log "done" +else + log "tests FAILED — restoring the previous lockfile" + cp "$STATE_DIR/package.json.bak" package.json 2>/dev/null || true + cp "$STATE_DIR/package-lock.json.bak" package-lock.json 2>/dev/null || true + npm i --silent >/dev/null 2>&1 || true + notify "Dependency update REVERTED — tests failed (see $STATE_DIR/test.log)." + finish_timer + exit 1 +fi