Skip to content

[ENHANCEMENT] Add a CI check for image files whose contents do not match their extension #16158

Description

@mattstratton

Implements the PR check requested in #15957 ("can we do a PR check to validate that the extension matches the actual file content?"). #15957 covers cleaning up the images that are already wrong; this one is about stopping new ones getting in, and can land independently of that cleanup.

The problem

An image whose contents do not match its extension (a WebP saved as .png is the common one) builds fine on our pinned Hugo 0.152.2, because Hugo sniffs the content rather than trusting the name. Newer Hugo does not, and fails the build. So these land in the repo invisibly and only surface later, at the worst possible moment — during a version bump, on someone else's PR.

It is nobody's fault: saving an image from a browser frequently produces a WebP, and utilities/add_sponsors.sh hardcodes a .png destination regardless of what you feed it (#16150).

The constraint that shapes the design

A check over the whole repo is not viable. Counting current tracked image files under assets/ and static/:

total tracked image files: 17543
violations:                  857

Broken down by what they actually are:

Real format Count
JPEG 623
PNG 172
WebP 57
GIF 5

(Note it goes both ways — 172 are real PNGs carrying a .jpg name.)

A repo-wide check would therefore fail every pull request from the day it merged, until all 857 were fixed. That would be miserable for organizers who have nothing to do with it.

So the check must only look at files changed in the pull request. New mismatches are blocked immediately; the backlog gets cleaned up separately under #15957 at whatever pace suits. Most of the 857 are harmless today (a JPEG named .png works on both old and new Hugo); only the 57 WebP and 5 GIF actually break newer Hugo.

Proposed implementation

A step in the existing lint job in .github/workflows/hugo.yml. No new action or dependency — file is already on ubuntu-latest.

      - name: Lint image formats
        env:
          PR_NUMBER: ${{ github.event.pull_request.number }}
          REPO: ${{ github.repository }}
          GH_TOKEN: ${{ github.token }}
        run: |
          # Image files whose contents do not match their extension build fine on
          # the pinned Hugo but break on newer versions. Only files changed in this
          # PR are checked - the repo has a backlog of pre-existing mismatches
          # (see #15957).
          CHANGED=$(gh pr diff "$PR_NUMBER" --repo "$REPO" --name-only \
            | grep -iE '\.(png|jpg|jpeg|gif|webp)$' || true)

          [ -z "$CHANGED" ] && { echo "No image files changed."; exit 0; }

          fail=0
          while IFS= read -r f; do
            [ -f "$f" ] || continue          # deleted in this PR
            ext=$(printf '%s' "${f##*.}" | tr '[:upper:]' '[:lower:]')
            mime=$(file --mime-type -b "$f")
            case "$ext:$mime" in
              png:image/png|jpg:image/jpeg|jpeg:image/jpeg|gif:image/gif|webp:image/webp) ;;
              *)
                case "$mime" in
                  image/png)  want=png ;;  image/jpeg) want=jpg ;;
                  image/gif)  want=gif ;;  image/webp) want=webp ;;
                  *)          want="" ;;
                esac
                if [ -n "$want" ]; then
                  echo "::error file=$f::$f is actually $mime. Convert it to a real .$ext, or rename it to .$want and update the reference."
                else
                  echo "::error file=$f::$f is not a recognised image ($mime)."
                fi
                fail=1 ;;
            esac
          done <<< "$CHANGED"

          if [ "$fail" -eq 1 ]; then
            echo ""
            echo "Saving an image from a browser often produces a WebP with a .png name."
            echo "Convert with:  magick broken.png png:fixed.png"
            exit 1
          fi
          echo "All changed image files match their extensions."

Using ::error file=...:: means GitHub annotates the offending file directly in the PR's Files Changed tab, which is the bit that actually helps a non-technical organizer.

I tested the detection logic locally against real files in this repo: it correctly flags assets/sponsors/c/clio.png (WebP) and assets/sponsors/o/oracle-before-20251111.png (GIF), and correctly passes assets/sponsors/d/datadog.png (a real PNG) and assets/events/2026-boston/speakers/dan-morgan.webp (a genuine .webp).

Things worth deciding before implementing

  • Hard fail or warning? I have written it as a hard fail, matching the filename lint. The counter-argument is that it blocks a sponsor addition over something Hugo currently tolerates. A warning would not stop the bleeding though, and this is trivially fixable once you know.
  • Renaming vs converting. The error offers both. Renaming is wrong for sponsor logos, because the filename must keep matching data/sponsors/<id>.yml — converting is nearly always the right fix there. The message could be smarter about that for paths under assets/sponsors/.
  • SVG is deliberately excluded. file reports SVGs inconsistently (text/xml, text/plain, image/svg+xml) and would produce false positives.
  • Once the cleanup in Many images lie about their formats. #15957 is done, this could be widened from "changed files" to the whole repo, which would also catch anything that slipped in before the check existed.

Related

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions