Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
4 changes: 4 additions & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/`:
Expand Down
6 changes: 5 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down
54 changes: 54 additions & 0 deletions _scripts/test_validate_images.sh
Original file line number Diff line number Diff line change
@@ -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"
41 changes: 41 additions & 0 deletions _scripts/validate_images.sh
Original file line number Diff line number Diff line change
@@ -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."
Loading