diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0696c098e..77f3b17aa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,9 @@ jobs: - name: Generate tag and category data run: make generate + - name: Validate meetup image references + run: make validate + - name: Build Jekyll site run: bundle exec jekyll build diff --git a/CLAUDE.md b/CLAUDE.md index b620b26dd..9474dd37a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -60,6 +60,10 @@ Tags and categories are not manually maintained — they're generated from colle The CI build (`.github/workflows/pages.yml`) always runs `make generate` before `jekyll build`. Do not manually edit files in `tags/`, `categories/`, or `_data/tags.yml`/`_data/categories.yml` — they will be overwritten. +### Image Validation + +`_scripts/validate_images.sh` (exposed as `make validate`) checks that every meetup's `image:` front matter resolves to a file in `archives/images/events/`, skipping external `http(s)` URLs. It exits non-zero on a missing source and runs on pull requests via `ci.yml`. It is intentionally **not** part of `make generate` or `pages.yml`, so a missing image never blocks the production deploy. Run `bash _scripts/test_validate_images.sh` to exercise the validator against fixtures. + ### Navigation and Site Data Site-wide data is in `_data/`: diff --git a/Makefile b/Makefile index c11ce8e11..707da7722 100644 --- a/Makefile +++ b/Makefile @@ -40,6 +40,10 @@ generate-pages: # Generate all content generate: generate-data generate-pages +# Validate that meetup image references resolve to source files +validate: + $(SCRIPTS_DIR)/validate_images.sh + # Update Git submodules update: git submodule update --remote --merge @@ -48,4 +52,4 @@ update: clean: rm -rf _site .jekyll-metadata -.PHONY: all bundle serve serve-incremental generate-data generate-pages generate update-submodules clean +.PHONY: all bundle serve serve-incremental generate-data generate-pages generate validate update-submodules clean diff --git a/README.md b/README.md index bc1400b23..0a56fabbb 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,9 @@ Ensure your Ruby environment matches the `Gemfile` requirements. Older gem versi **Jekyll-GitHub Pages Compatibility** GitHub Pages restricts Jekyll plugins for security. See [supported versions](https://docs.github.com/en/pages/setting-up-a-github-pages-site-with-jekyll/about-github-pages-and-jekyll#plugins) for details. +**Validating Meetup Images** +Run `make validate` to check that every meetup's `image:` front matter points at a real file in `archives/images/events/`. CI runs this on pull requests to `main` (including the daily submodule-update PR) and fails if a referenced image is missing, so broken references are caught before they reach the site. + **Image Derivative Caching** CI caches generated image thumbnails (`_site/assets/thumbs`) keyed on the contents of `archives/images/`, so unchanged images are not regenerated. Locally, `_site` persists between builds, so repeat builds are already incremental — running `make clean` wipes `_site` and forces a full regeneration on the next build. diff --git a/_scripts/test_validate_images.sh b/_scripts/test_validate_images.sh new file mode 100755 index 000000000..f6d3da389 --- /dev/null +++ b/_scripts/test_validate_images.sh @@ -0,0 +1,54 @@ +#!/bin/bash +# Fixture-based tests for validate_images.sh. Each case builds a temp ARCHIVES_DIR. +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" +VALIDATOR="$SCRIPT_DIR/validate_images.sh" +fails=0 + +setup_fixture() { + mkdir -p "$1/_meetups" "$1/images/events" +} + +# Case 1: source present -> exit 0 +t1=$(mktemp -d); setup_fixture "$t1" +printf 'image: present.jpg\n' > "$t1/_meetups/ok.md" +: > "$t1/images/events/present.jpg" +if ARCHIVES_DIR="$t1" "$VALIDATOR" >/dev/null 2>&1; then + echo "✅ case 1 (present) exits 0" +else + echo "❌ case 1 (present) should exit 0"; fails=$((fails + 1)) +fi + +# Case 2: source missing -> exit 1, names the file and image +t2=$(mktemp -d); setup_fixture "$t2" +printf 'image: missing.avif\n' > "$t2/_meetups/bad.md" +out=$(ARCHIVES_DIR="$t2" "$VALIDATOR" 2>&1) && rc=0 || rc=$? +if [ "${rc:-0}" -eq 1 ] && printf '%s' "$out" | grep -q "bad.md" && printf '%s' "$out" | grep -q "missing.avif"; then + echo "✅ case 2 (missing) exits 1 and names bad.md/missing.avif" +else + echo "❌ case 2 (missing) should exit 1 naming bad.md/missing.avif (rc=${rc:-0})"; fails=$((fails + 1)) +fi + +# Case 3: external URL -> not flagged, exit 0 +t3=$(mktemp -d); setup_fixture "$t3" +printf 'image: https://example.com/x.jpg\n' > "$t3/_meetups/url.md" +if ARCHIVES_DIR="$t3" "$VALIDATOR" >/dev/null 2>&1; then + echo "✅ case 3 (external url) exits 0" +else + echo "❌ case 3 (external url) should exit 0"; fails=$((fails + 1)) +fi + +# Case 4: CRLF line endings on the image value -> still resolves -> exit 0 +t4=$(mktemp -d); setup_fixture "$t4" +printf 'image: present.jpg\r\n' > "$t4/_meetups/crlf.md" +: > "$t4/images/events/present.jpg" +if ARCHIVES_DIR="$t4" "$VALIDATOR" >/dev/null 2>&1; then + echo "✅ case 4 (CRLF) exits 0" +else + echo "❌ case 4 (CRLF) should exit 0"; fails=$((fails + 1)) +fi + +rm -rf "$t1" "$t2" "$t3" "$t4" +if [ "$fails" -gt 0 ]; then echo "❌ $fails test(s) failed"; exit 1; fi +echo "✅ all validate_images tests passed" diff --git a/_scripts/validate_images.sh b/_scripts/validate_images.sh new file mode 100755 index 000000000..166acc1f5 --- /dev/null +++ b/_scripts/validate_images.sh @@ -0,0 +1,41 @@ +#!/bin/bash +set -euo pipefail + +ARCHIVES_DIR="${ARCHIVES_DIR:-archives}" +MEETUPS_DIR="$ARCHIVES_DIR/_meetups" +EVENTS_DIR="$ARCHIVES_DIR/images/events" + +echo "🔍 Validating meetup image references in $MEETUPS_DIR..." + +if [ ! -d "$MEETUPS_DIR" ]; then + echo "❌ Meetups directory not found: $MEETUPS_DIR" + exit 1 +fi + +missing=0 +checked=0 + +shopt -s nullglob +for file in "$MEETUPS_DIR"/*.md; do + # First `image:` value, with surrounding whitespace and any quotes stripped. + img=$(grep -m1 '^image:' "$file" 2>/dev/null | sed "s/^image:[[:space:]]*//; s/[\"']//g; s/[[:space:]]*\$//" || true) + + # Skip entries with no image or an external URL. + [ -z "$img" ] && continue + case "$img" in + http://*|https://*) continue ;; + esac + + checked=$((checked + 1)) + if [ ! -f "$EVENTS_DIR/$img" ]; then + echo "❌ $(basename "$file") → image '$img' not found in $EVENTS_DIR/" + missing=$((missing + 1)) + fi +done + +if [ "$missing" -gt 0 ]; then + echo "❌ $missing meetup image reference(s) missing." + exit 1 +fi + +echo "✅ All $checked meetup image references resolve."