From ba54f0a7f4529229f508801a0cc51a5313a24613 Mon Sep 17 00:00:00 2001 From: Alex Olson Date: Sun, 28 Jun 2026 11:11:49 -0600 Subject: [PATCH 1/8] Add design spec for meetup image source validation (#142) Co-Authored-By: Claude Opus 4.8 --- ...026-06-28-validate-image-sources-design.md | 140 ++++++++++++++++++ 1 file changed, 140 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-28-validate-image-sources-design.md diff --git a/docs/superpowers/specs/2026-06-28-validate-image-sources-design.md b/docs/superpowers/specs/2026-06-28-validate-image-sources-design.md new file mode 100644 index 000000000..5c1c7db2a --- /dev/null +++ b/docs/superpowers/specs/2026-06-28-validate-image-sources-design.md @@ -0,0 +1,140 @@ +# Validate meetup image sources at build time (issue #142) + +## Problem + +Meetup #549's front matter referenced `hacknight_549.avif` while the source +file was `hacknight_549.png`. Nothing validated the reference, so it surfaced +only as a runtime `jekyll_picture_tag` warning and a broken image in production: + +``` +Jekyll Picture Tag Warning: JPT Could not find .../archives/images/events/hacknight_549.avif. Your site will have broken images. Continuing. +``` + +Because content is hand-authored in the auto-updated `archives/` submodule, the +next such mismatch would ship the same silent way. + +## Goal + +Catch a meetup whose `image:` source file is missing **before** it merges, +with a clear error naming the file — rather than as a buried build warning. + +## Scope + +**Meetups only.** Investigation of the collections that carry an `image:` field: + +- **Meetups** (`archives/_meetups/`): `image:` is a bare filename rendered via + `{% picture events /events/{{ page.image }} %}` (`_layouts/meetup.html:92`), + resolving to `archives/images/events/`. This is the only collection + JPT processes and the only source of the "Could not find" warning. 452 of 549 + meetups use it. +- **Projects** (`archives/_projects/`): `image:` holds **external URLs** + (`https://…`), rendered as a plain ``/og:image. Not a local file — cannot + be validated by file existence. +- **Organizations** (`archives/_organizations/`): have `image:` front matter, + but `organization.html` does not render it, and the referenced files live in + the **main repo** (`assets/images/organizations/`), not the archives. + +Validating projects (URLs) or organizations (unrendered, different repo) would +add path-mapping complexity for fields that produce no broken JPT image. The +validator therefore targets meetups; values beginning with `http` are skipped. + +## Failure mode + +The script **exits non-zero** on any missing source. It is wired into +`ci.yml`, which runs on pull requests to `main` — including the daily +auto-update-archives PR — so a bad reference fails the PR check before merge. +It is **not** added to `pages.yml` (the push-to-`main` deploy) or to +`make generate`, so a single missing image can never block deploying the whole +site. Developers can also run it locally via `make validate`. + +## Design + +### `_scripts/validate_images.sh` (new) + +Follows the existing `_scripts/*.sh` conventions (`#!/bin/bash`, +`set -euo pipefail`, `find`, emoji progress output). + +- `ARCHIVES_DIR="${ARCHIVES_DIR:-archives}"` — overridable so tests can point at + a fixture directory. +- `EVENTS_DIR="$ARCHIVES_DIR/images/events"`. +- For each `"$ARCHIVES_DIR"/_meetups/*.md`: + - Extract the first `image:` value; strip surrounding quotes and whitespace. + - Skip if empty. + - Skip if it begins with `http` (external URL — out of scope). + - If `"$EVENTS_DIR/"` does not exist, record it as missing. +- Print each miss as: + `❌ 549.md → image 'hacknight_549.avif' not found in archives/images/events/` +- Print a final summary: `✅ All meetup image references resolve` when clean, + or `❌ meetup image reference(s) missing` when not. +- **Exit 1** if any are missing; **exit 0** otherwise. + +### `Makefile` (modify) + +Add a `validate` target and declare it `.PHONY`: + +```make +validate: + ./_scripts/validate_images.sh +``` + +Kept separate from `generate` — validation is a check, not data generation, and +keeping it out of `generate` keeps it off the deploy path (`pages.yml` runs +`make generate`). + +### `.github/workflows/ci.yml` (modify) + +Add a step to the `build` job, immediately **before** `Build Jekyll site`: + +```yaml + - name: Validate meetup image references + run: make validate +``` + +`ci.yml` triggers on `pull_request` to `main` and checks out submodules +recursively, so the archives images are present and a broken reference fails the +PR. + +## Data flow + +``` +PR to main + -> ci.yml build job + checkout (submodules: recursive) + make validate <- fails fast on a missing meetup image + make generate + bundle exec jekyll build + Pa11y +``` + +## Testing + +A test harness points `ARCHIVES_DIR` at a temporary fixture tree: + +- **Pass case:** `_meetups/ok.md` with `image: present.jpg` and + `images/events/present.jpg` on disk → script exits 0, prints the ✅ summary. +- **Fail case:** `_meetups/bad.md` with `image: missing.avif` and no matching + source → script exits 1, output names `bad.md` and `missing.avif`. +- **External-URL case:** `_meetups/url.md` with `image: https://example.com/x.jpg` + → not reported as missing (script exits 0 for that fixture). +- **Real-tree smoke check:** run against the working tree. With the #549 fix not + yet merged locally it exits 1 naming `549.md`, demonstrating it catches the + real case; after the archives fix lands it exits 0. + +## Scope boundaries + +- Does not validate projects (external URLs) or organizations (unrendered, + main-repo assets). +- Does not touch `pages.yml` or `make generate`; production deploys are not + gated by this check. +- Out of scope: the pre-existing issue that meetup `image:` bare filenames are + also used verbatim as `og:image` URLs in `_layouts/base.html` (a separate + social-card concern, not a JPT broken image). + +## Acceptance criteria + +- [ ] A meetup referencing a non-existent image source is named and fails the + check before/at build time. +- [ ] External-URL `image:` values are not flagged as missing. +- [ ] `make validate` runs the check locally; `ci.yml` runs it on PRs to `main`. +- [ ] `pages.yml` and `make generate` are unchanged (deploy not gated). +- [ ] Dev workflow docs mention `make validate`. From 2852709226b51893f2abd33b392f1b037d7064a8 Mon Sep 17 00:00:00 2001 From: Alex Olson Date: Sun, 28 Jun 2026 11:15:30 -0600 Subject: [PATCH 2/8] Add implementation plan for meetup image source validation (#142) Co-Authored-By: Claude Opus 4.8 --- .../2026-06-28-validate-image-sources.md | 334 ++++++++++++++++++ 1 file changed, 334 insertions(+) create mode 100644 docs/superpowers/plans/2026-06-28-validate-image-sources.md diff --git a/docs/superpowers/plans/2026-06-28-validate-image-sources.md b/docs/superpowers/plans/2026-06-28-validate-image-sources.md new file mode 100644 index 000000000..c36291437 --- /dev/null +++ b/docs/superpowers/plans/2026-06-28-validate-image-sources.md @@ -0,0 +1,334 @@ +# Meetup Image Source Validation Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. + +**Goal:** Add a build-time check that fails when a meetup's `image:` front matter points at a source file missing from `archives/images/events/`. + +**Architecture:** A standalone bash script (`_scripts/validate_images.sh`) following the existing `_scripts/*.sh` conventions, with `ARCHIVES_DIR` overridable for fixture-based tests. It is exposed locally via a `make validate` target and run on pull requests via a step in `ci.yml`. The push-to-`main` deploy (`pages.yml`) and `make generate` are deliberately left untouched so production deploys are never gated by this check. + +**Tech Stack:** Bash, GNU/BSD coreutils (`grep`, `sed`), GNU Make, GitHub Actions. + +## Global Constraints + +- Scope is **meetups only** (`archives/_meetups/*.md`); source dir is `archives/images/events/`. +- Skip `image:` values that are empty or begin with `http://` / `https://` (external URLs). +- Script **exits 1** if any source is missing, **exits 0** otherwise. +- `ARCHIVES_DIR` env var overrides the archives root (default `archives`); source dir is `$ARCHIVES_DIR/images/events`. +- Do **not** modify `pages.yml` or the `make generate` target — deploy must not be gated. +- Follow existing script style: `#!/bin/bash`, `set -euo pipefail`, emoji progress output (🔍 / ❌ / ✅). +- Branch: `validate-image-sources`. +- **Sequencing:** this check fails on the pre-existing #549 (`hacknight_549.avif`) until issue #141 (archives PR CivicTechTO/archives#26) is merged and the submodule pointer is updated. That is correct behaviour, not a bug in this work — see Task 5. + +--- + +### Task 1: Create the validator script with its test (TDD) + +**Files:** +- Create: `_scripts/validate_images.sh` +- Create: `_scripts/test_validate_images.sh` + +**Interfaces:** +- Produces: an executable `_scripts/validate_images.sh` that reads `ARCHIVES_DIR` (default `archives`), scans `$ARCHIVES_DIR/_meetups/*.md`, and exits 1 if any non-URL `image:` value is missing from `$ARCHIVES_DIR/images/events/`, else 0. Consumed by Task 2 (`make validate`, `ci.yml`). + +- [ ] **Step 1: Write the failing test** + +Create `_scripts/test_validate_images.sh`: + +```bash +#!/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 + +rm -rf "$t1" "$t2" "$t3" +if [ "$fails" -gt 0 ]; then echo "❌ $fails test(s) failed"; exit 1; fi +echo "✅ all validate_images tests passed" +``` + +- [ ] **Step 2: Run the test to verify it fails** + +Run: `chmod +x _scripts/test_validate_images.sh && bash _scripts/test_validate_images.sh` +Expected: FAIL — the validator does not exist yet, so case 1 and case 3 report failure (and the script ultimately exits non-zero). Output includes `❌ case 1` because `"$VALIDATOR"` is not found. + +- [ ] **Step 3: Write the validator** + +Create `_scripts/validate_images.sh`: + +```bash +#!/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..." + +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." +``` + +- [ ] **Step 4: Make it executable and run the test to verify it passes** + +Run: `chmod +x _scripts/validate_images.sh && bash _scripts/test_validate_images.sh` +Expected: PASS — three ✅ lines then `✅ all validate_images tests passed`, exit 0. + +- [ ] **Step 5: Commit** + +```bash +git add _scripts/validate_images.sh _scripts/test_validate_images.sh +git commit -m "feat: add meetup image source validator with tests (#142) + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +### Task 2: Wire the validator into Make and CI + +**Files:** +- Modify: `Makefile` (add `validate` target; extend `.PHONY`) +- Modify: `.github/workflows/ci.yml` (add a step in the `build` job before `Build Jekyll site`) + +**Interfaces:** +- Consumes: `_scripts/validate_images.sh` from Task 1. +- Produces: `make validate` and a CI step both invoking the validator. + +- [ ] **Step 1: Add the `validate` Make target** + +In `Makefile`, immediately after the `generate: generate-data generate-pages` block, add: + +```make +# Validate that meetup image references resolve to source files +validate: + $(SCRIPTS_DIR)/validate_images.sh +``` + +- [ ] **Step 2: Add `validate` to `.PHONY`** + +In `Makefile`, change the final line from: + +```make +.PHONY: all bundle serve serve-incremental generate-data generate-pages generate update-submodules clean +``` + +to: + +```make +.PHONY: all bundle serve serve-incremental generate-data generate-pages generate validate update-submodules clean +``` + +- [ ] **Step 3: Verify `make validate` runs the script** + +Run: `make validate; echo "exit=$?"` +Expected: it prints the 🔍 line and either a ✅ summary (exit=0) or ❌ lines (exit=1). On this branch, before the #141 submodule bump, expect `❌ 549.md → image 'hacknight_549.avif' not found ...` and `exit=1`. This confirms the target is wired and the check is live. + +- [ ] **Step 4: Add the CI step** + +In `.github/workflows/ci.yml`, locate this part of the `build` job: + +```yaml + - name: Generate tag and category data + run: make generate + + - name: Build Jekyll site + run: bundle exec jekyll build +``` + +Insert a step between them so it reads: + +```yaml + - 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 +``` + +- [ ] **Step 5: Verify the workflow YAML parses** + +Run: `ruby -ryaml -e "YAML.load_file('.github/workflows/ci.yml'); puts 'YAML OK'"` +Expected: `YAML OK` + +- [ ] **Step 6: Verify step ordering** + +Run: `grep -n "Validate meetup image references\|Build Jekyll site\|Generate tag and category data" .github/workflows/ci.yml` +Expected: `Validate meetup image references` appears at a line number greater than `Generate tag and category data` and less than `Build Jekyll site`. + +- [ ] **Step 7: Commit** + +```bash +git add Makefile .github/workflows/ci.yml +git commit -m "ci: run meetup image validation on PRs and via make validate (#142) + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +### Task 3: Document the validation + +**Files:** +- Modify: `README.md` (add a note under `### Common Issues`, before `## Development Scripts`) +- Modify: `CLAUDE.md` (extend the Data Generation / scripts guidance) + +**Interfaces:** none consumed by later tasks. + +- [ ] **Step 1: Add a README note** + +In `README.md`, immediately before the line `## Development Scripts`, insert: + +```markdown +**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. + +``` + +- [ ] **Step 2: Add a CLAUDE.md note** + +In `CLAUDE.md`, find the `### Data Generation Pipeline` section. Immediately before the `### Navigation and Site Data` heading that follows it, insert: + +```markdown +### 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. + +``` + +- [ ] **Step 3: Verify both edits landed** + +Run: `grep -n "Validating Meetup Images" README.md && grep -n "### Image Validation" CLAUDE.md` +Expected: one match in each file. + +- [ ] **Step 4: Commit** + +```bash +git add README.md CLAUDE.md +git commit -m "docs: document make validate image check (#142) + +Co-Authored-By: Claude Opus 4.8 " +``` + +--- + +### Task 4: Push the branch and open the PR + +**Files:** none. + +**Interfaces:** none. + +- [ ] **Step 1: Re-run the test suite as a final gate** + +Run: `bash _scripts/test_validate_images.sh; echo "exit=$?"` +Expected: `✅ all validate_images tests passed` and `exit=0`. + +- [ ] **Step 2: Push the branch** + +```bash +git push -u origin validate-image-sources +``` + +- [ ] **Step 3: Open the PR** + +```bash +gh pr create --title "Validate meetup image sources at build time (#142)" --body "$(cat <<'EOF' +Closes #142. + +Adds `_scripts/validate_images.sh` (with `_scripts/test_validate_images.sh`), exposed as `make validate` and run on pull requests via `ci.yml`. It checks that every meetup's `image:` front matter resolves to a file in `archives/images/events/`, skipping external `http(s)` URLs, and exits non-zero on a missing source. This is the guard that would have caught #549 before it shipped. + +Scope is meetups only — projects use external image URLs and organizations' `image:` is unrendered and lives in the main repo (see the design doc). `pages.yml` and `make generate` are untouched, so production deploys are never gated by this check. + +## Dependency / sequencing +This check correctly fails on the pre-existing #549 (`hacknight_549.avif`) until #141 (CivicTechTO/archives#26) is merged and the `archives` submodule pointer is updated. CI on this PR will go green once that fix is in the pinned submodule commit. + +🤖 Generated with [Claude Code](https://claude.com/claude-code) +EOF +)" +``` + +--- + +### Task 5: Confirm sequencing with #141 (no code) + +**Files:** none. This task records the cross-issue dependency for whoever merges. + +- [ ] **Step 1: Note the merge order** + +For #142's own CI to pass, the `archives` submodule pinned by this branch must already contain the #549 fix: + +1. Merge CivicTechTO/archives#26 (the `.avif → .png` fix). +2. Update the civictech.ca `archives` submodule pointer to that commit — either via the daily `update-submodule.yml` PR, or by merging it into this branch. +3. Re-run CI on the #142 PR; `make validate` now exits 0 and the check passes. + +Until then, a red `Validate meetup image references` check on this PR is the **expected, correct** signal that the guard works — not a defect to "fix" by weakening the check. + +--- + +## Notes for the implementer + +- The validator and its test are pure bash; no Ruby/Jekyll build is required to develop or test them. Run `bash _scripts/test_validate_images.sh` for fast feedback. +- Do not add the check to `make generate` or `pages.yml`, and do not relax the non-zero exit to make this branch's CI green — the correct path to green is landing #141 (Task 5). From 11bc48699e54dd1788a81d164f28e2a6159195c6 Mon Sep 17 00:00:00 2001 From: Alex Olson Date: Sun, 28 Jun 2026 11:18:57 -0600 Subject: [PATCH 3/8] feat: add meetup image source validator with tests (#142) Co-Authored-By: Claude Opus 4.8 --- _scripts/test_validate_images.sh | 44 ++++++++++++++++++++++++++++++++ _scripts/validate_images.sh | 36 ++++++++++++++++++++++++++ 2 files changed, 80 insertions(+) create mode 100755 _scripts/test_validate_images.sh create mode 100755 _scripts/validate_images.sh diff --git a/_scripts/test_validate_images.sh b/_scripts/test_validate_images.sh new file mode 100755 index 000000000..9eb28e131 --- /dev/null +++ b/_scripts/test_validate_images.sh @@ -0,0 +1,44 @@ +#!/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 + +rm -rf "$t1" "$t2" "$t3" +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..513373f8a --- /dev/null +++ b/_scripts/validate_images.sh @@ -0,0 +1,36 @@ +#!/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..." + +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." From 62b987ea5eae53011551fcd4c0b875e3168fab1d Mon Sep 17 00:00:00 2001 From: Alex Olson Date: Sun, 28 Jun 2026 11:22:25 -0600 Subject: [PATCH 4/8] ci: run meetup image validation on PRs and via make validate (#142) Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 3 +++ Makefile | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) 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/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 From 17cd6aa04cce44bc4aac51ace2c3a5f4c8ba9a04 Mon Sep 17 00:00:00 2001 From: Alex Olson Date: Sun, 28 Jun 2026 11:24:59 -0600 Subject: [PATCH 5/8] docs: document make validate image check (#142) Co-Authored-By: Claude Opus 4.8 --- CLAUDE.md | 4 ++++ README.md | 3 +++ 2 files changed, 7 insertions(+) diff --git a/CLAUDE.md b/CLAUDE.md index f79546558..d4ce200dd 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/README.md b/README.md index 60d860a40..617f6a4d7 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. + ## Development Scripts ### Update Submodule From 86050cf81b41c6e8dd659e077b61c179a145bd14 Mon Sep 17 00:00:00 2001 From: Alex Olson Date: Sun, 28 Jun 2026 11:29:46 -0600 Subject: [PATCH 6/8] harden: guard missing meetups dir + CRLF test case (#142) Co-Authored-By: Claude Opus 4.8 --- _scripts/test_validate_images.sh | 12 +++++++++++- _scripts/validate_images.sh | 5 +++++ 2 files changed, 16 insertions(+), 1 deletion(-) diff --git a/_scripts/test_validate_images.sh b/_scripts/test_validate_images.sh index 9eb28e131..f6d3da389 100755 --- a/_scripts/test_validate_images.sh +++ b/_scripts/test_validate_images.sh @@ -39,6 +39,16 @@ else echo "❌ case 3 (external url) should exit 0"; fails=$((fails + 1)) fi -rm -rf "$t1" "$t2" "$t3" +# 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 index 513373f8a..166acc1f5 100755 --- a/_scripts/validate_images.sh +++ b/_scripts/validate_images.sh @@ -7,6 +7,11 @@ 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 From 0572242615200b720229ccd6980e801c1487db7e Mon Sep 17 00:00:00 2001 From: Alex Olson Date: Sun, 28 Jun 2026 11:31:44 -0600 Subject: [PATCH 7/8] Delete docs/superpowers/plans/2026-06-28-validate-image-sources.md --- .../2026-06-28-validate-image-sources.md | 334 ------------------ 1 file changed, 334 deletions(-) delete mode 100644 docs/superpowers/plans/2026-06-28-validate-image-sources.md diff --git a/docs/superpowers/plans/2026-06-28-validate-image-sources.md b/docs/superpowers/plans/2026-06-28-validate-image-sources.md deleted file mode 100644 index c36291437..000000000 --- a/docs/superpowers/plans/2026-06-28-validate-image-sources.md +++ /dev/null @@ -1,334 +0,0 @@ -# Meetup Image Source Validation Implementation Plan - -> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. - -**Goal:** Add a build-time check that fails when a meetup's `image:` front matter points at a source file missing from `archives/images/events/`. - -**Architecture:** A standalone bash script (`_scripts/validate_images.sh`) following the existing `_scripts/*.sh` conventions, with `ARCHIVES_DIR` overridable for fixture-based tests. It is exposed locally via a `make validate` target and run on pull requests via a step in `ci.yml`. The push-to-`main` deploy (`pages.yml`) and `make generate` are deliberately left untouched so production deploys are never gated by this check. - -**Tech Stack:** Bash, GNU/BSD coreutils (`grep`, `sed`), GNU Make, GitHub Actions. - -## Global Constraints - -- Scope is **meetups only** (`archives/_meetups/*.md`); source dir is `archives/images/events/`. -- Skip `image:` values that are empty or begin with `http://` / `https://` (external URLs). -- Script **exits 1** if any source is missing, **exits 0** otherwise. -- `ARCHIVES_DIR` env var overrides the archives root (default `archives`); source dir is `$ARCHIVES_DIR/images/events`. -- Do **not** modify `pages.yml` or the `make generate` target — deploy must not be gated. -- Follow existing script style: `#!/bin/bash`, `set -euo pipefail`, emoji progress output (🔍 / ❌ / ✅). -- Branch: `validate-image-sources`. -- **Sequencing:** this check fails on the pre-existing #549 (`hacknight_549.avif`) until issue #141 (archives PR CivicTechTO/archives#26) is merged and the submodule pointer is updated. That is correct behaviour, not a bug in this work — see Task 5. - ---- - -### Task 1: Create the validator script with its test (TDD) - -**Files:** -- Create: `_scripts/validate_images.sh` -- Create: `_scripts/test_validate_images.sh` - -**Interfaces:** -- Produces: an executable `_scripts/validate_images.sh` that reads `ARCHIVES_DIR` (default `archives`), scans `$ARCHIVES_DIR/_meetups/*.md`, and exits 1 if any non-URL `image:` value is missing from `$ARCHIVES_DIR/images/events/`, else 0. Consumed by Task 2 (`make validate`, `ci.yml`). - -- [ ] **Step 1: Write the failing test** - -Create `_scripts/test_validate_images.sh`: - -```bash -#!/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 - -rm -rf "$t1" "$t2" "$t3" -if [ "$fails" -gt 0 ]; then echo "❌ $fails test(s) failed"; exit 1; fi -echo "✅ all validate_images tests passed" -``` - -- [ ] **Step 2: Run the test to verify it fails** - -Run: `chmod +x _scripts/test_validate_images.sh && bash _scripts/test_validate_images.sh` -Expected: FAIL — the validator does not exist yet, so case 1 and case 3 report failure (and the script ultimately exits non-zero). Output includes `❌ case 1` because `"$VALIDATOR"` is not found. - -- [ ] **Step 3: Write the validator** - -Create `_scripts/validate_images.sh`: - -```bash -#!/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..." - -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." -``` - -- [ ] **Step 4: Make it executable and run the test to verify it passes** - -Run: `chmod +x _scripts/validate_images.sh && bash _scripts/test_validate_images.sh` -Expected: PASS — three ✅ lines then `✅ all validate_images tests passed`, exit 0. - -- [ ] **Step 5: Commit** - -```bash -git add _scripts/validate_images.sh _scripts/test_validate_images.sh -git commit -m "feat: add meetup image source validator with tests (#142) - -Co-Authored-By: Claude Opus 4.8 " -``` - ---- - -### Task 2: Wire the validator into Make and CI - -**Files:** -- Modify: `Makefile` (add `validate` target; extend `.PHONY`) -- Modify: `.github/workflows/ci.yml` (add a step in the `build` job before `Build Jekyll site`) - -**Interfaces:** -- Consumes: `_scripts/validate_images.sh` from Task 1. -- Produces: `make validate` and a CI step both invoking the validator. - -- [ ] **Step 1: Add the `validate` Make target** - -In `Makefile`, immediately after the `generate: generate-data generate-pages` block, add: - -```make -# Validate that meetup image references resolve to source files -validate: - $(SCRIPTS_DIR)/validate_images.sh -``` - -- [ ] **Step 2: Add `validate` to `.PHONY`** - -In `Makefile`, change the final line from: - -```make -.PHONY: all bundle serve serve-incremental generate-data generate-pages generate update-submodules clean -``` - -to: - -```make -.PHONY: all bundle serve serve-incremental generate-data generate-pages generate validate update-submodules clean -``` - -- [ ] **Step 3: Verify `make validate` runs the script** - -Run: `make validate; echo "exit=$?"` -Expected: it prints the 🔍 line and either a ✅ summary (exit=0) or ❌ lines (exit=1). On this branch, before the #141 submodule bump, expect `❌ 549.md → image 'hacknight_549.avif' not found ...` and `exit=1`. This confirms the target is wired and the check is live. - -- [ ] **Step 4: Add the CI step** - -In `.github/workflows/ci.yml`, locate this part of the `build` job: - -```yaml - - name: Generate tag and category data - run: make generate - - - name: Build Jekyll site - run: bundle exec jekyll build -``` - -Insert a step between them so it reads: - -```yaml - - 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 -``` - -- [ ] **Step 5: Verify the workflow YAML parses** - -Run: `ruby -ryaml -e "YAML.load_file('.github/workflows/ci.yml'); puts 'YAML OK'"` -Expected: `YAML OK` - -- [ ] **Step 6: Verify step ordering** - -Run: `grep -n "Validate meetup image references\|Build Jekyll site\|Generate tag and category data" .github/workflows/ci.yml` -Expected: `Validate meetup image references` appears at a line number greater than `Generate tag and category data` and less than `Build Jekyll site`. - -- [ ] **Step 7: Commit** - -```bash -git add Makefile .github/workflows/ci.yml -git commit -m "ci: run meetup image validation on PRs and via make validate (#142) - -Co-Authored-By: Claude Opus 4.8 " -``` - ---- - -### Task 3: Document the validation - -**Files:** -- Modify: `README.md` (add a note under `### Common Issues`, before `## Development Scripts`) -- Modify: `CLAUDE.md` (extend the Data Generation / scripts guidance) - -**Interfaces:** none consumed by later tasks. - -- [ ] **Step 1: Add a README note** - -In `README.md`, immediately before the line `## Development Scripts`, insert: - -```markdown -**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. - -``` - -- [ ] **Step 2: Add a CLAUDE.md note** - -In `CLAUDE.md`, find the `### Data Generation Pipeline` section. Immediately before the `### Navigation and Site Data` heading that follows it, insert: - -```markdown -### 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. - -``` - -- [ ] **Step 3: Verify both edits landed** - -Run: `grep -n "Validating Meetup Images" README.md && grep -n "### Image Validation" CLAUDE.md` -Expected: one match in each file. - -- [ ] **Step 4: Commit** - -```bash -git add README.md CLAUDE.md -git commit -m "docs: document make validate image check (#142) - -Co-Authored-By: Claude Opus 4.8 " -``` - ---- - -### Task 4: Push the branch and open the PR - -**Files:** none. - -**Interfaces:** none. - -- [ ] **Step 1: Re-run the test suite as a final gate** - -Run: `bash _scripts/test_validate_images.sh; echo "exit=$?"` -Expected: `✅ all validate_images tests passed` and `exit=0`. - -- [ ] **Step 2: Push the branch** - -```bash -git push -u origin validate-image-sources -``` - -- [ ] **Step 3: Open the PR** - -```bash -gh pr create --title "Validate meetup image sources at build time (#142)" --body "$(cat <<'EOF' -Closes #142. - -Adds `_scripts/validate_images.sh` (with `_scripts/test_validate_images.sh`), exposed as `make validate` and run on pull requests via `ci.yml`. It checks that every meetup's `image:` front matter resolves to a file in `archives/images/events/`, skipping external `http(s)` URLs, and exits non-zero on a missing source. This is the guard that would have caught #549 before it shipped. - -Scope is meetups only — projects use external image URLs and organizations' `image:` is unrendered and lives in the main repo (see the design doc). `pages.yml` and `make generate` are untouched, so production deploys are never gated by this check. - -## Dependency / sequencing -This check correctly fails on the pre-existing #549 (`hacknight_549.avif`) until #141 (CivicTechTO/archives#26) is merged and the `archives` submodule pointer is updated. CI on this PR will go green once that fix is in the pinned submodule commit. - -🤖 Generated with [Claude Code](https://claude.com/claude-code) -EOF -)" -``` - ---- - -### Task 5: Confirm sequencing with #141 (no code) - -**Files:** none. This task records the cross-issue dependency for whoever merges. - -- [ ] **Step 1: Note the merge order** - -For #142's own CI to pass, the `archives` submodule pinned by this branch must already contain the #549 fix: - -1. Merge CivicTechTO/archives#26 (the `.avif → .png` fix). -2. Update the civictech.ca `archives` submodule pointer to that commit — either via the daily `update-submodule.yml` PR, or by merging it into this branch. -3. Re-run CI on the #142 PR; `make validate` now exits 0 and the check passes. - -Until then, a red `Validate meetup image references` check on this PR is the **expected, correct** signal that the guard works — not a defect to "fix" by weakening the check. - ---- - -## Notes for the implementer - -- The validator and its test are pure bash; no Ruby/Jekyll build is required to develop or test them. Run `bash _scripts/test_validate_images.sh` for fast feedback. -- Do not add the check to `make generate` or `pages.yml`, and do not relax the non-zero exit to make this branch's CI green — the correct path to green is landing #141 (Task 5). From 385e99c61bef10994a0d0df5779a2ed33d712c7a Mon Sep 17 00:00:00 2001 From: Alex Olson Date: Sun, 28 Jun 2026 11:32:00 -0600 Subject: [PATCH 8/8] Delete docs/superpowers/specs/2026-06-28-validate-image-sources-design.md --- ...026-06-28-validate-image-sources-design.md | 140 ------------------ 1 file changed, 140 deletions(-) delete mode 100644 docs/superpowers/specs/2026-06-28-validate-image-sources-design.md diff --git a/docs/superpowers/specs/2026-06-28-validate-image-sources-design.md b/docs/superpowers/specs/2026-06-28-validate-image-sources-design.md deleted file mode 100644 index 5c1c7db2a..000000000 --- a/docs/superpowers/specs/2026-06-28-validate-image-sources-design.md +++ /dev/null @@ -1,140 +0,0 @@ -# Validate meetup image sources at build time (issue #142) - -## Problem - -Meetup #549's front matter referenced `hacknight_549.avif` while the source -file was `hacknight_549.png`. Nothing validated the reference, so it surfaced -only as a runtime `jekyll_picture_tag` warning and a broken image in production: - -``` -Jekyll Picture Tag Warning: JPT Could not find .../archives/images/events/hacknight_549.avif. Your site will have broken images. Continuing. -``` - -Because content is hand-authored in the auto-updated `archives/` submodule, the -next such mismatch would ship the same silent way. - -## Goal - -Catch a meetup whose `image:` source file is missing **before** it merges, -with a clear error naming the file — rather than as a buried build warning. - -## Scope - -**Meetups only.** Investigation of the collections that carry an `image:` field: - -- **Meetups** (`archives/_meetups/`): `image:` is a bare filename rendered via - `{% picture events /events/{{ page.image }} %}` (`_layouts/meetup.html:92`), - resolving to `archives/images/events/`. This is the only collection - JPT processes and the only source of the "Could not find" warning. 452 of 549 - meetups use it. -- **Projects** (`archives/_projects/`): `image:` holds **external URLs** - (`https://…`), rendered as a plain ``/og:image. Not a local file — cannot - be validated by file existence. -- **Organizations** (`archives/_organizations/`): have `image:` front matter, - but `organization.html` does not render it, and the referenced files live in - the **main repo** (`assets/images/organizations/`), not the archives. - -Validating projects (URLs) or organizations (unrendered, different repo) would -add path-mapping complexity for fields that produce no broken JPT image. The -validator therefore targets meetups; values beginning with `http` are skipped. - -## Failure mode - -The script **exits non-zero** on any missing source. It is wired into -`ci.yml`, which runs on pull requests to `main` — including the daily -auto-update-archives PR — so a bad reference fails the PR check before merge. -It is **not** added to `pages.yml` (the push-to-`main` deploy) or to -`make generate`, so a single missing image can never block deploying the whole -site. Developers can also run it locally via `make validate`. - -## Design - -### `_scripts/validate_images.sh` (new) - -Follows the existing `_scripts/*.sh` conventions (`#!/bin/bash`, -`set -euo pipefail`, `find`, emoji progress output). - -- `ARCHIVES_DIR="${ARCHIVES_DIR:-archives}"` — overridable so tests can point at - a fixture directory. -- `EVENTS_DIR="$ARCHIVES_DIR/images/events"`. -- For each `"$ARCHIVES_DIR"/_meetups/*.md`: - - Extract the first `image:` value; strip surrounding quotes and whitespace. - - Skip if empty. - - Skip if it begins with `http` (external URL — out of scope). - - If `"$EVENTS_DIR/"` does not exist, record it as missing. -- Print each miss as: - `❌ 549.md → image 'hacknight_549.avif' not found in archives/images/events/` -- Print a final summary: `✅ All meetup image references resolve` when clean, - or `❌ meetup image reference(s) missing` when not. -- **Exit 1** if any are missing; **exit 0** otherwise. - -### `Makefile` (modify) - -Add a `validate` target and declare it `.PHONY`: - -```make -validate: - ./_scripts/validate_images.sh -``` - -Kept separate from `generate` — validation is a check, not data generation, and -keeping it out of `generate` keeps it off the deploy path (`pages.yml` runs -`make generate`). - -### `.github/workflows/ci.yml` (modify) - -Add a step to the `build` job, immediately **before** `Build Jekyll site`: - -```yaml - - name: Validate meetup image references - run: make validate -``` - -`ci.yml` triggers on `pull_request` to `main` and checks out submodules -recursively, so the archives images are present and a broken reference fails the -PR. - -## Data flow - -``` -PR to main - -> ci.yml build job - checkout (submodules: recursive) - make validate <- fails fast on a missing meetup image - make generate - bundle exec jekyll build - Pa11y -``` - -## Testing - -A test harness points `ARCHIVES_DIR` at a temporary fixture tree: - -- **Pass case:** `_meetups/ok.md` with `image: present.jpg` and - `images/events/present.jpg` on disk → script exits 0, prints the ✅ summary. -- **Fail case:** `_meetups/bad.md` with `image: missing.avif` and no matching - source → script exits 1, output names `bad.md` and `missing.avif`. -- **External-URL case:** `_meetups/url.md` with `image: https://example.com/x.jpg` - → not reported as missing (script exits 0 for that fixture). -- **Real-tree smoke check:** run against the working tree. With the #549 fix not - yet merged locally it exits 1 naming `549.md`, demonstrating it catches the - real case; after the archives fix lands it exits 0. - -## Scope boundaries - -- Does not validate projects (external URLs) or organizations (unrendered, - main-repo assets). -- Does not touch `pages.yml` or `make generate`; production deploys are not - gated by this check. -- Out of scope: the pre-existing issue that meetup `image:` bare filenames are - also used verbatim as `og:image` URLs in `_layouts/base.html` (a separate - social-card concern, not a JPT broken image). - -## Acceptance criteria - -- [ ] A meetup referencing a non-existent image source is named and fails the - check before/at build time. -- [ ] External-URL `image:` values are not flagged as missing. -- [ ] `make validate` runs the check locally; `ci.yml` runs it on PRs to `main`. -- [ ] `pages.yml` and `make generate` are unchanged (deploy not gated). -- [ ] Dev workflow docs mention `make validate`.