diff --git a/AGENTS.md b/AGENTS.md index 7419eec..c533b33 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -2340,7 +2340,7 @@ Rules: apply). `zapstore.yaml` license was `custom` (the listing showed "custom"); now **`AGPL-3.0-only`** (re-published in 1.6.8, verified on the 32267 tag). On **failure** the job restores the lockfile and schedules a retry in **~3 days** - (a failed run no longer burns the whole 20-day window); every notification now + (a failed run no longer burns the whole 16-day window); every notification now includes the **OSV total** ("N packages affected by M known vulnerabilities"). **Final security scan (2026-10-26):** researched the app-relevant advisories and fixed them — **zod 3.25 → 4.6.7** (only `AppProvider` imports zod; no @@ -2363,6 +2363,23 @@ Rules: `whitespace-nowrap` keeps the trailing **More** reachable, the mobile bottom nav + burger append the full catalog, dropdowns have max-height + internal scroll, and interactive targets meet the 44px rule. No other menu bugs found. + **Automation hardening (2026-10-27):** the dependency interval is now + **16 days** (`DEPS_INTERVAL_DAYS=16` in + `scripts/systemd/skatespots-deps.service` and the script default), plus a + separate **weekly scan-only** timer (`skatespots-deps-scan.timer`, Mon 06:00) + that reports npm/OSV findings without touching the lockfile. The deps service + has `TimeoutStartSec=30min` (the 90s default can kill a legitimate + update/test run) and both maintenance scripts resolve the newest nvm Node + instead of hard-coding `v22.23.1`. `@tanstack/query-core` is pinned with an + npm **override** to `$@tanstack/react-query`, so `npm update` cannot create a + second copy — the Oct 26 failure was duplicate `query-core` breaking tsc. + Failed runs now keep `test.log`/`smoke.log` + the attempted package files in + `~/.local/state/skatespots-deps/failures/` (8 kept, rotated) and a + `last-failure` marker; reports keep 12. Releases are one command via + **`scripts/release.sh`** (notes check → `npm test` → bump → Android build → + `zsp publish` → relay verification → commit/push + tag). The deploy timer has + `TimeoutStartSec=20min`, and all deploy/deps units are tracked in + `scripts/systemd/`. ### Community relay — self-hosted Newlay (2026-10-12) diff --git a/package.json b/package.json index bb51b9e..2107e76 100644 --- a/package.json +++ b/package.json @@ -175,5 +175,8 @@ "vite": "^6.4.5", "vitest": "^5.0.5", "web-push": "^3.6.7" + }, + "overrides": { + "@tanstack/query-core": "$@tanstack/react-query" } } diff --git a/scripts/auto-deploy.sh b/scripts/auto-deploy.sh index faaecf8..6ea90d6 100755 --- a/scripts/auto-deploy.sh +++ b/scripts/auto-deploy.sh @@ -17,8 +17,13 @@ set -euo pipefail REPO="/home/sovereignsk8/SkateSpots" -# systemd runs with a minimal PATH; node/npm live under nvm here. -export PATH="/home/sovereignsk8/.nvm/versions/node/v22.23.1/bin:$PATH" +# systemd runs with a minimal PATH; resolve the newest nvm Node so a Node +# upgrade doesn't silently break deploys. +if [ -d "$HOME/.nvm/versions/node" ]; then + NODE_BIN="$(ls -d "$HOME"/.nvm/versions/node/*/bin 2>/dev/null | sort -V | tail -1)" + [ -n "$NODE_BIN" ] && export PATH="$NODE_BIN:$PATH" +fi +export PATH="$HOME/go/bin:$PATH" STATE_DIR="$HOME/.local/state/skatespots-deploy" SHA_FILE="$STATE_DIR/last-deployed-sha" diff --git a/scripts/install-deps-timer.sh b/scripts/install-deps-timer.sh index 4437d54..6fc0b6d 100755 --- a/scripts/install-deps-timer.sh +++ b/scripts/install-deps-timer.sh @@ -1,6 +1,6 @@ #!/usr/bin/env bash # -# Install the dependency-update + vulnerability-scan systemd user timer. +# Install the dependency-update + vulnerability-scan systemd user timers. # scripts/install-deps-timer.sh set -euo pipefail @@ -10,7 +10,9 @@ mkdir -p "$DEST" cp "$SRC/skatespots-deps.service" "$DEST/" cp "$SRC/skatespots-deps.timer" "$DEST/" +cp "$SRC/skatespots-deps-scan.service" "$DEST/" +cp "$SRC/skatespots-deps-scan.timer" "$DEST/" systemctl --user daemon-reload -systemctl --user enable --now skatespots-deps.timer -systemctl --user list-timers skatespots-deps.timer --no-pager +systemctl --user enable --now skatespots-deps.timer skatespots-deps-scan.timer +systemctl --user list-timers skatespots-deps.timer skatespots-deps-scan.timer --no-pager diff --git a/scripts/release.sh b/scripts/release.sh new file mode 100755 index 0000000..64698cc --- /dev/null +++ b/scripts/release.sh @@ -0,0 +1,129 @@ +#!/usr/bin/env bash +# +# One-command SkateSpots release: bump → test → build APK → publish to +# Zapstore → verify → commit/push (+ optional git tag). +# +# Usage: +# scripts/release.sh patch|minor|major [--yes] +# scripts/release.sh 1.7.1 [--yes] +# +# Before running: +# 1. write the new section at the TOP of release-notes.txt (do not commit it — +# this script commits it with the version bump); +# 2. provide the release signer WITHOUT putting it in shell history: +# export SIGN_WITH=nsec1... # current shell only +# or point at a 0600 file: +# export SKATES_RELEASE_ENV=$HOME/.config/skatespots/release.env +# # file contains: SIGN_WITH=nsec1... +# +# What it does NOT do: deploy the web (the skatespots-deploy.timer does that +# within ~3 min, 24h throttle) or touch the relay. +set -euo pipefail + +REPO="/home/sovereignsk8/SkateSpots" +ZSP="${ZSP:-$HOME/go/bin/zsp}" +export ANDROID_HOME="${ANDROID_HOME:-$HOME/Android/Sdk}" +SPEC="${1:-}" +YES=0 +shift || true +for a in "$@"; do [ "$a" = "--yes" ] && YES=1; done + +die() { echo "✗ $*" >&2; exit 1; } +log() { echo "• $*"; } + +[ -n "$SPEC" ] || die "usage: scripts/release.sh patch|minor|major| [--yes]" +[ -x "$ZSP" ] || die "zsp not found/executable at $ZSP (set ZSP=...)" +[ -d "$ANDROID_HOME" ] || die "Android SDK not found at $ANDROID_HOME (set ANDROID_HOME=...)" + +# Optional protected env file (never commit it). +if [ -z "${SIGN_WITH:-}" ] && [ -n "${SKATES_RELEASE_ENV:-}" ]; then + [ -f "$SKATES_RELEASE_ENV" ] || die "SKATES_RELEASE_ENV does not exist: $SKATES_RELEASE_ENV" + set -a + # shellcheck disable=SC1090 + . "$SKATES_RELEASE_ENV" + set +a +fi +[ -n "${SIGN_WITH:-}" ] || die "SIGN_WITH is not set (export it or use SKATES_RELEASE_ENV)" + +# Resolve the newest nvm Node (the old hard-coded path breaks after upgrades). +if [ -d "$HOME/.nvm/versions/node" ]; then + NODE_BIN="$(ls -d "$HOME"/.nvm/versions/node/*/bin 2>/dev/null | sort -V | tail -1)" + [ -n "$NODE_BIN" ] && export PATH="$NODE_BIN:$PATH" +fi +export PATH="$HOME/go/bin:$PATH" + +cd "$REPO" + +CURRENT="$(node -p "require('./package.json').version")" +log "current version: $CURRENT" + +# The notes must be the only uncommitted change, so the release commit is clean. +if git diff --quiet -- release-notes.txt; then + die "release-notes.txt is unchanged — write the new section first (and don't commit it)" +fi +DIRTY="$(git status --porcelain | grep -vE '^ M release-notes\.txt$' || true)" +[ -z "$DIRTY" ] || die "working tree has other changes: +$DIRTY" + +git fetch --quiet origin main +[ "$(git rev-parse HEAD)" = "$(git rev-parse origin/main)" ] || \ + die "local main is not in sync with origin/main — push or pull first" + +if [ "$YES" -eq 0 ]; then + [ -t 0 ] || die "non-interactive: pass --yes to confirm" + printf 'Release %s from %s? [y/N] ' "$SPEC" "$CURRENT" + read -r ans + case "$ans" in y|Y|yes|YES) ;; *) die "aborted" ;; esac +fi + +log "running the full test gate (this also builds the web bundle)…" +npm test + +log "bumping version ($SPEC)…" +npm version "$SPEC" --no-git-tag-version >/dev/null +VERSION="$(node -p "require('./package.json').version")" +[ "$VERSION" != "$CURRENT" ] || die "version did not change" +log "version: $VERSION" + +log "building the signed release APK…" +npm run android:build + +APK="android/app/build/outputs/apk/release/app-release.apk" +[ -f "$APK" ] || die "APK not found at $APK" +SHA="$(sha256sum "$APK" | awk '{print $1}')" +log "APK: $APK (sha256 ${SHA:0:16}…)" + +log "publishing to Zapstore…" +# shellcheck disable=SC2086 +"$ZSP" publish --quiet zapstore.yaml + +log "verifying the Zapstore release event…" +FOUND=0 +for _ in 1 2 3 4 5 6; do + if command -v nak >/dev/null 2>&1; then + OUT="$(timeout 15 nak req -k 30063 -d "org.skatespots.app@$VERSION" wss://relay.zapstore.dev 2>/dev/null || true)" + if printf '%s' "$OUT" | grep -q "\[\"version\",\"$VERSION\"\]"; then FOUND=1; break; fi + else + log "nak not installed — skipping relay verification" + FOUND=1 + break + fi + sleep 4 +done +[ "$FOUND" -eq 1 ] || log "warning: the release event did not appear on the relay yet (it usually propagates shortly)" + +log "committing the release…" +git add package.json package-lock.json release-notes.txt +git commit -m "release: $VERSION" >/dev/null +git tag -a "v$VERSION" -m "SkateSpots $VERSION" +git push origin main +if ! git push origin "v$VERSION" 2>/dev/null; then + log "warning: tag v$VERSION was created locally but the push failed" +fi + +echo +echo "✓ SkateSpots $VERSION published" +echo " APK: $APK" +echo " SHA256: $SHA" +echo " Web: the deploy timer picks up the commit within ~3 min (24h throttle)" +echo " Zapstore: https://zapstore.dev/apps/org.skatespots.app" diff --git a/scripts/systemd/skatespots-deploy.service b/scripts/systemd/skatespots-deploy.service new file mode 100644 index 0000000..cd38306 --- /dev/null +++ b/scripts/systemd/skatespots-deploy.service @@ -0,0 +1,10 @@ +[Unit] +Description=Auto-deploy SkateSpots web app to Netlify when origin/main changes +Documentation=file:///home/sovereignsk8/SkateSpots/scripts/auto-deploy.sh + +[Service] +Type=oneshot +WorkingDirectory=/home/sovereignsk8/SkateSpots +# npm ci + vite build + smoke test can exceed systemd's 90s default. +TimeoutStartSec=20min +ExecStart=/home/sovereignsk8/SkateSpots/scripts/auto-deploy.sh diff --git a/scripts/systemd/skatespots-deploy.timer b/scripts/systemd/skatespots-deploy.timer new file mode 100644 index 0000000..771304f --- /dev/null +++ b/scripts/systemd/skatespots-deploy.timer @@ -0,0 +1,11 @@ +[Unit] +Description=Poll SkateSpots origin/main and deploy to Netlify on change + +[Timer] +OnBootSec=2min +OnUnitActiveSec=3min +AccuracySec=30s +Persistent=true + +[Install] +WantedBy=timers.target diff --git a/scripts/systemd/skatespots-deps-scan.service b/scripts/systemd/skatespots-deps-scan.service new file mode 100644 index 0000000..68628ea --- /dev/null +++ b/scripts/systemd/skatespots-deps-scan.service @@ -0,0 +1,7 @@ +[Unit] +Description=SkateSpots weekly vulnerability scan (report only, no lockfile changes) + +[Service] +Type=oneshot +TimeoutStartSec=15min +ExecStart=%h/SkateSpots/scripts/update-deps.sh --scan-only diff --git a/scripts/systemd/skatespots-deps-scan.timer b/scripts/systemd/skatespots-deps-scan.timer new file mode 100644 index 0000000..1831a82 --- /dev/null +++ b/scripts/systemd/skatespots-deps-scan.timer @@ -0,0 +1,10 @@ +[Unit] +Description=Weekly SkateSpots npm/OSV vulnerability scan + +[Timer] +OnCalendar=Mon 06:00 +Persistent=true +RandomizedDelaySec=30min + +[Install] +WantedBy=timers.target diff --git a/scripts/systemd/skatespots-deps.service b/scripts/systemd/skatespots-deps.service index 88c7513..0643689 100644 --- a/scripts/systemd/skatespots-deps.service +++ b/scripts/systemd/skatespots-deps.service @@ -3,4 +3,8 @@ Description=SkateSpots scheduled dependency update + vulnerability scan [Service] Type=oneshot +Environment=DEPS_INTERVAL_DAYS=16 +# npm update + typecheck + tests + smoke can take several minutes; the default +# 90s systemd start timeout would kill a legitimate run. +TimeoutStartSec=30min ExecStart=%h/SkateSpots/scripts/update-deps.sh diff --git a/scripts/systemd/skatespots-deps.timer b/scripts/systemd/skatespots-deps.timer index 7b1e37d..bd26e26 100644 --- a/scripts/systemd/skatespots-deps.timer +++ b/scripts/systemd/skatespots-deps.timer @@ -1,5 +1,5 @@ [Unit] -Description=Daily tick for the SkateSpots dependency job (self-gated to ~20 days) +Description=Daily tick for the SkateSpots dependency job (self-gated to ~16 days) [Timer] OnCalendar=daily diff --git a/scripts/update-deps.sh b/scripts/update-deps.sh index b4707a5..ebfff8e 100755 --- a/scripts/update-deps.sh +++ b/scripts/update-deps.sh @@ -1,9 +1,11 @@ #!/usr/bin/env bash # -# Scheduled dependency update + vulnerability scan (every ~20 days). +# Scheduled dependency update + vulnerability scan (every ~16 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: +# script self-gates to one real run per INTERVAL_DAYS). A second weekly timer +# (`skatespots-deps-scan.timer`) runs this with --scan-only for earlier CVE +# visibility without touching the lockfile. 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); @@ -20,14 +22,22 @@ set -uo pipefail REPO="/home/sovereignsk8/SkateSpots" -export PATH="/home/sovereignsk8/.nvm/versions/node/v22.23.1/bin:$PATH" +# Resolve the newest nvm Node so a Node upgrade doesn't silently break the +# timer (the old hard-coded v22.23.1 path did). +if [ -d "$HOME/.nvm/versions/node" ]; then + NODE_BIN="$(ls -d "$HOME"/.nvm/versions/node/*/bin 2>/dev/null | sort -V | tail -1)" + [ -n "$NODE_BIN" ] && export PATH="$NODE_BIN:$PATH" +fi +export PATH="$HOME/go/bin:$PATH" cd "$REPO" -INTERVAL_DAYS="${DEPS_INTERVAL_DAYS:-20}" +INTERVAL_DAYS="${DEPS_INTERVAL_DAYS:-16}" 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" +FAILURE_FILE="$STATE_DIR/last-failure" +FAILURE_DIR_BASE="$STATE_DIR/failures" # OSV-Scanner (broader than npm audit: queries the OSV database for the lockfile). # Installed with: go install github.com/google/osv-scanner/v2/cmd/osv-scanner@latest OSV_BIN="${OSV_BIN:-$HOME/go/bin/osv-scanner}" @@ -81,9 +91,14 @@ fi OSV_SUMMARY="$(grep -E '^Total [0-9]+ packages affected' "$REPORT" | tail -1)" [ -n "$OSV_SUMMARY" ] || OSV_SUMMARY="OSV scan: no summary (scanner missing?)" +# Rotate diagnostics so the state dir cannot grow forever. +ls -1t "$STATE_DIR"/report-*.txt 2>/dev/null | tail -n +13 | xargs -r rm -f +ls -1dt "$FAILURE_DIR_BASE"/* 2>/dev/null | tail -n +9 | xargs -r rm -rf + if [ "$SCAN_ONLY" -eq 1 ]; then log "scan-only; report: $REPORT" echo "$OSV_SUMMARY" + notify "Vulnerability scan: $OSV_SUMMARY (report: $REPORT)" exit 0 fi @@ -133,15 +148,22 @@ if npm test >"$STATE_DIR/test.log" 2>&1 && node scripts/smoke-test.mjs >"$STATE_ notify "Dependency scan complete: nothing to update. ${OSV_SUMMARY}" fi finish_timer + rm -f "$FAILURE_FILE" log "done" else - log "tests FAILED — restoring the previous lockfile" + log "tests FAILED — saving diagnostics and restoring the previous lockfile" + FAIL_DIR="$FAILURE_DIR_BASE/$(date +%F-%H%M)" + mkdir -p "$FAIL_DIR" + cp "$STATE_DIR/test.log" "$STATE_DIR/smoke.log" "$FAIL_DIR"/ 2>/dev/null || true + cp package.json package-lock.json "$FAIL_DIR"/ 2>/dev/null || true + printf '%s\n' "tests failed at $(date -Is)" "see $FAIL_DIR/test.log" > "$FAIL_DIR/summary.txt" + echo "$(date -Is) tests failed — $FAIL_DIR" > "$FAILURE_FILE" 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 # A failed run must NOT consume the whole interval: retry in ~3 days instead. RETRY_DAYS=3; [ "$INTERVAL_DAYS" -gt 4 ] || RETRY_DAYS=1 echo $(( $(date +%s) - (INTERVAL_DAYS - RETRY_DAYS) * 86400 )) > "$LAST" - notify "Dependency update REVERTED — tests failed (see $STATE_DIR/test.log). ${OSV_SUMMARY}" + notify "Dependency update REVERTED — tests failed (see $FAIL_DIR). ${OSV_SUMMARY}" exit 1 fi