This commit is contained in:
parent
6841623e23
commit
38688a68f5
3 changed files with 99 additions and 25 deletions
|
|
@ -109,16 +109,22 @@ jobs:
|
||||||
# Writes straight into the mount so the copies persist across deploys —
|
# Writes straight into the mount so the copies persist across deploys —
|
||||||
# they are NOT always reproducible, because a source instance deleting a
|
# they are NOT always reproducible, because a source instance deleting a
|
||||||
# file leaves ours as the only one.
|
# file leaves ours as the only one.
|
||||||
|
id: media
|
||||||
run: |
|
run: |
|
||||||
set -eu
|
set -eu
|
||||||
if [ -d /deploy-app ]; then
|
if [ -d /deploy-app ]; then
|
||||||
mkdir -p /deploy-app/media
|
mkdir -p /deploy-app/media
|
||||||
tools/fetch-media.sh /deploy-app/media
|
dir=/deploy-app/media
|
||||||
else
|
else
|
||||||
echo "WARNING: /deploy-app not mounted; mirroring to a throwaway dir." >&2
|
echo "WARNING: /deploy-app not mounted; mirroring to a throwaway dir." >&2
|
||||||
echo "Media will be re-downloaded on every build until the mount exists." >&2
|
echo "Media will be re-downloaded on every build until the mount exists." >&2
|
||||||
tools/fetch-media.sh media
|
dir=media
|
||||||
fi
|
fi
|
||||||
|
tools/fetch-media.sh "$dir"
|
||||||
|
# Handed to the e2e step so it checks the files where they actually
|
||||||
|
# are. It used to assume ./media and reported every referenced file
|
||||||
|
# as missing here, which reads as a broken site and means a wrong path.
|
||||||
|
echo "dir=$dir" >> "$GITHUB_OUTPUT"
|
||||||
|
|
||||||
- name: Build and test the backend
|
- name: Build and test the backend
|
||||||
id: srv
|
id: srv
|
||||||
|
|
@ -216,6 +222,7 @@ jobs:
|
||||||
# A gate, not a report: a failure here stops the deploy.
|
# A gate, not a report: a failure here stops the deploy.
|
||||||
env:
|
env:
|
||||||
SRV: ${{ steps.srv.outputs.srv }}
|
SRV: ${{ steps.srv.outputs.srv }}
|
||||||
|
E2E_MEDIA_DIR: ${{ steps.media.outputs.dir }}
|
||||||
run: tools/e2e.sh "$SRV/catcrafts-server"
|
run: tools/e2e.sh "$SRV/catcrafts-server"
|
||||||
|
|
||||||
- name: Pre-compress static assets
|
- name: Pre-compress static assets
|
||||||
|
|
|
||||||
34
tools/e2e.sh
34
tools/e2e.sh
|
|
@ -28,6 +28,22 @@ fi
|
||||||
|
|
||||||
PORT="${E2E_PORT:-8199}"
|
PORT="${E2E_PORT:-8199}"
|
||||||
BASE="http://127.0.0.1:$PORT"
|
BASE="http://127.0.0.1:$PORT"
|
||||||
|
|
||||||
|
# Where tools/fetch-media.sh put the mirrored files.
|
||||||
|
#
|
||||||
|
# NOT hardcoded to ./media: in the repo that is where they land, but CI points
|
||||||
|
# the mirror at the persistent mount instead (/deploy-app/media) so the copies
|
||||||
|
# survive a deploy. A check that assumed the dev layout reported every single
|
||||||
|
# referenced file as missing on CI while the files were perfectly fine on the
|
||||||
|
# mount — a failure that said "40 files missing" and meant "wrong directory".
|
||||||
|
# Override with E2E_MEDIA_DIR; the workflow passes the same path it gave the
|
||||||
|
# mirror.
|
||||||
|
MEDIA_DIR="${E2E_MEDIA_DIR:-}"
|
||||||
|
if [ -z "$MEDIA_DIR" ]; then
|
||||||
|
for d in media /deploy-app/media; do
|
||||||
|
if [ -d "$d" ]; then MEDIA_DIR="$d"; break; fi
|
||||||
|
done
|
||||||
|
fi
|
||||||
WORK="$(mktemp -d)"
|
WORK="$(mktemp -d)"
|
||||||
ORDERS="$WORK/orders.jsonl"
|
ORDERS="$WORK/orders.jsonl"
|
||||||
|
|
||||||
|
|
@ -505,18 +521,28 @@ else
|
||||||
fi
|
fi
|
||||||
# Every tier has to be a file that exists, or the ladder serves a 404 to
|
# Every tier has to be a file that exists, or the ladder serves a 404 to
|
||||||
# whichever browsers pick that rung — which is precisely the set of
|
# whichever browsers pick that rung — which is precisely the set of
|
||||||
# browsers nobody testing this site is using.
|
# browsers nobody testing this site is using. The files are served by
|
||||||
|
# Caddy rather than by this server, so they are checked on disk.
|
||||||
|
if [ -z "$MEDIA_DIR" ]; then
|
||||||
|
skip "media files exist" "no media directory found; set E2E_MEDIA_DIR"
|
||||||
|
else
|
||||||
missing=0
|
missing=0
|
||||||
for f in $(curl -s "$BASE/sitemap.xml" | grep -oE '/posts/[a-z0-9-]+' | head -n 20 \
|
for f in $(curl -s "$BASE/sitemap.xml" | grep -oE '/posts/[a-z0-9-]+' | head -n 20 \
|
||||||
| while read -r pg; do curl -s "$BASE$pg"; done \
|
| while read -r pg; do curl -s "$BASE$pg"; done \
|
||||||
| grep -oE '(src|srcset)="/media/[^"]+"' \
|
| grep -oE '(src|srcset)="/media/[^"]+"' \
|
||||||
| sed 's/.*="//; s/"$//' | sort -u); do
|
| sed 's/.*="//; s/"$//' | sort -u); do
|
||||||
[ -f "media${f#/media}" ] || { missing=$((missing + 1)); echo " missing: $f" >&2; }
|
if [ ! -f "$MEDIA_DIR/${f#/media/}" ]; then
|
||||||
|
missing=$((missing + 1))
|
||||||
|
# Bounded: a wrong directory makes EVERY file missing, and a
|
||||||
|
# hundred identical lines buries the one fact that matters.
|
||||||
|
[ "$missing" -le 5 ] && echo " missing: $f" >&2
|
||||||
|
fi
|
||||||
done
|
done
|
||||||
if [ "$missing" -eq 0 ]; then
|
if [ "$missing" -eq 0 ]; then
|
||||||
ok "every referenced media file is on the mount"
|
ok "every referenced media file is in $MEDIA_DIR"
|
||||||
else
|
else
|
||||||
bad "media files" "$missing referenced file(s) not on the mount"
|
bad "media files" "$missing referenced file(s) not in $MEDIA_DIR"
|
||||||
|
fi
|
||||||
fi
|
fi
|
||||||
else
|
else
|
||||||
skip "image format ladder" "no <picture> on this page — ffmpeg absent at mirror time?"
|
skip "image format ladder" "no <picture> on this page — ffmpeg absent at mirror time?"
|
||||||
|
|
|
||||||
|
|
@ -110,12 +110,12 @@ probe_dims() {
|
||||||
w=0; h=0
|
w=0; h=0
|
||||||
[ "$HAVE_FFPROBE" = 1 ] || return 0
|
[ "$HAVE_FFPROBE" = 1 ] || return 0
|
||||||
pw=$(ffprobe -v error -select_streams v:0 -show_entries stream=width \
|
pw=$(ffprobe -v error -select_streams v:0 -show_entries stream=width \
|
||||||
-of default=nw=1:nk=1 "$1" 2>/dev/null | head -n1 || true)
|
-of default=nw=1:nk=1 "$1" </dev/null 2>/dev/null | head -n1 || true)
|
||||||
ph=$(ffprobe -v error -select_streams v:0 -show_entries stream=height \
|
ph=$(ffprobe -v error -select_streams v:0 -show_entries stream=height \
|
||||||
-of default=nw=1:nk=1 "$1" 2>/dev/null | head -n1 || true)
|
-of default=nw=1:nk=1 "$1" </dev/null 2>/dev/null | head -n1 || true)
|
||||||
rot=$(ffprobe -v error -select_streams v:0 \
|
rot=$(ffprobe -v error -select_streams v:0 \
|
||||||
-show_entries stream_side_data=rotation \
|
-show_entries stream_side_data=rotation \
|
||||||
-of default=nw=1:nk=1 "$1" 2>/dev/null | head -n1 || true)
|
-of default=nw=1:nk=1 "$1" </dev/null 2>/dev/null | head -n1 || true)
|
||||||
case "$pw" in ''|*[!0-9]*) pw=0 ;; esac
|
case "$pw" in ''|*[!0-9]*) pw=0 ;; esac
|
||||||
case "$ph" in ''|*[!0-9]*) ph=0 ;; esac
|
case "$ph" in ''|*[!0-9]*) ph=0 ;; esac
|
||||||
# ffprobe reports this as a signed number that some builds print with a
|
# ffprobe reports this as a signed number that some builds print with a
|
||||||
|
|
@ -174,7 +174,7 @@ transcode_image() {
|
||||||
# to actually be counted — ~75 ms on a 3 MP image, once per new file.
|
# to actually be counted — ~75 ms on a 3 MP image, once per new file.
|
||||||
_frames=$(ffprobe -v error -select_streams v:0 -count_frames \
|
_frames=$(ffprobe -v error -select_streams v:0 -count_frames \
|
||||||
-show_entries stream=nb_read_frames \
|
-show_entries stream=nb_read_frames \
|
||||||
-of default=nw=1:nk=1 "$_file" 2>/dev/null | head -n1)
|
-of default=nw=1:nk=1 "$_file" </dev/null 2>/dev/null | head -n1)
|
||||||
case "$_frames" in
|
case "$_frames" in
|
||||||
''|*[!0-9]*|1) ;; # unknown or a single frame: a still
|
''|*[!0-9]*|1) ;; # unknown or a single frame: a still
|
||||||
*) echo "fetch-media: $_name is animated, serving it as one file" >&2
|
*) echo "fetch-media: $_name is animated, serving it as one file" >&2
|
||||||
|
|
@ -184,7 +184,7 @@ transcode_image() {
|
||||||
# Alpha has to survive the transcode: an image with a transparent corner
|
# Alpha has to survive the transcode: an image with a transparent corner
|
||||||
# encoded into a format with no alpha plane gains an opaque black one.
|
# encoded into a format with no alpha plane gains an opaque black one.
|
||||||
_pixfmt=$(ffprobe -v error -select_streams v:0 -show_entries stream=pix_fmt \
|
_pixfmt=$(ffprobe -v error -select_streams v:0 -show_entries stream=pix_fmt \
|
||||||
-of default=nw=1:nk=1 "$_file" 2>/dev/null | head -n1)
|
-of default=nw=1:nk=1 "$_file" </dev/null 2>/dev/null | head -n1)
|
||||||
case "$_pixfmt" in
|
case "$_pixfmt" in
|
||||||
yuva*|rgba*|bgra*|argb*|abgr*|gbrap*|ya8|ya16*|pal8) _avif_pix=yuva444p ;;
|
yuva*|rgba*|bgra*|argb*|abgr*|gbrap*|ya8|ya16*|pal8) _avif_pix=yuva444p ;;
|
||||||
*) _avif_pix=yuv444p ;;
|
*) _avif_pix=yuv444p ;;
|
||||||
|
|
@ -194,7 +194,7 @@ transcode_image() {
|
||||||
# wrong-format encode cannot leave a file the next run adopts as finished.
|
# wrong-format encode cannot leave a file the next run adopts as finished.
|
||||||
if [ -z "$avif_name" ]; then
|
if [ -z "$avif_name" ]; then
|
||||||
_cand="$_base.avif"
|
_cand="$_base.avif"
|
||||||
if [ -f "$MEDIA_DIR/$_cand" ]; then
|
if rendition_ok "$_cand" av1; then
|
||||||
avif_name="$_cand"
|
avif_name="$_cand"
|
||||||
elif encode_rendition "$_file" "$_cand" av1 \
|
elif encode_rendition "$_file" "$_cand" av1 \
|
||||||
-c:v libaom-av1 -still-picture 1 -crf 26 -cpu-used 6 \
|
-c:v libaom-av1 -still-picture 1 -crf 26 -cpu-used 6 \
|
||||||
|
|
@ -205,7 +205,7 @@ transcode_image() {
|
||||||
|
|
||||||
if [ -z "$png_name" ]; then
|
if [ -z "$png_name" ]; then
|
||||||
_cand="$_base.png"
|
_cand="$_base.png"
|
||||||
if [ -f "$MEDIA_DIR/$_cand" ]; then
|
if rendition_ok "$_cand" png; then
|
||||||
png_name="$_cand"
|
png_name="$_cand"
|
||||||
elif encode_rendition "$_file" "$_cand" png -c:v png -f image2; then
|
elif encode_rendition "$_file" "$_cand" png -c:v png -f image2; then
|
||||||
png_name="$_cand"
|
png_name="$_cand"
|
||||||
|
|
@ -213,6 +213,31 @@ transcode_image() {
|
||||||
fi
|
fi
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# The codec ffprobe reports for a file, or empty when it cannot say.
|
||||||
|
codec_of() {
|
||||||
|
[ "$HAVE_FFPROBE" = 1 ] || return 0
|
||||||
|
ffprobe -v error -select_streams v:0 -show_entries stream=codec_name \
|
||||||
|
-of default=nw=1:nk=1 "$1" </dev/null 2>/dev/null | head -n1
|
||||||
|
}
|
||||||
|
|
||||||
|
# rendition_ok NAME EXPECTED_CODEC — true when the file is already on the mount
|
||||||
|
# AND really is that codec.
|
||||||
|
#
|
||||||
|
# The second half is what makes the mount self-healing. Renditions are adopted
|
||||||
|
# by name and never re-derived, so anything wrong that once landed there would
|
||||||
|
# be trusted forever — which is exactly what a run of MJPEG files under .png
|
||||||
|
# names would have been. A file that fails re-encodes over the top instead.
|
||||||
|
rendition_ok() {
|
||||||
|
[ -f "$MEDIA_DIR/$1" ] || return 1
|
||||||
|
_have=$(codec_of "$MEDIA_DIR/$1")
|
||||||
|
# No ffprobe to ask: trust what is there rather than re-encoding every
|
||||||
|
# image on every build.
|
||||||
|
[ -n "$_have" ] || return 0
|
||||||
|
[ "$_have" = "$2" ] && return 0
|
||||||
|
echo "fetch-media: $1 on the mount is '$_have', not '$2' — re-encoding it" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
|
||||||
# encode_rendition SRC OUTNAME EXPECTED_CODEC ffmpeg-args...
|
# encode_rendition SRC OUTNAME EXPECTED_CODEC ffmpeg-args...
|
||||||
#
|
#
|
||||||
# Runs the encode into a .part, verifies the result really is the codec asked
|
# Runs the encode into a .part, verifies the result really is the codec asked
|
||||||
|
|
@ -224,17 +249,26 @@ transcode_image() {
|
||||||
# out.png` silently produced a run of lossy JPEGs sitting under .png names, which
|
# out.png` silently produced a run of lossy JPEGs sitting under .png names, which
|
||||||
# the page then advertised to browsers as image/png. The codec is pinned by the
|
# the page then advertised to browsers as image/png. The codec is pinned by the
|
||||||
# callers above; this is the check that the pin held.
|
# callers above; this is the check that the pin held.
|
||||||
|
#
|
||||||
|
# -nostdin AND </dev/null, both deliberately. ffmpeg reads standard input for
|
||||||
|
# interactive keystrokes, and this runs inside `while read src; do ... done
|
||||||
|
# <<EOF` — so it happily ate the front of the NEXT url off the here-document.
|
||||||
|
# The symptom was a handful of downloads failing per run with mangled hosts in
|
||||||
|
# the log ("ttps://", "s://", "tps://" — a different number of bytes swallowed
|
||||||
|
# each time), which then left those posts pointing at third-party media and
|
||||||
|
# failed the origin check in CI. It only bit when there was something to encode,
|
||||||
|
# so a rerun always "fixed" it.
|
||||||
encode_rendition() {
|
encode_rendition() {
|
||||||
_src="$1"; _out="$2"; _want="$3"
|
_src="$1"; _out="$2"; _want="$3"
|
||||||
shift 3
|
shift 3
|
||||||
if ! ffmpeg -y -v error -i "$_src" -frames:v 1 "$@" \
|
if ! ffmpeg -nostdin -y -v error -i "$_src" -frames:v 1 "$@" \
|
||||||
"$MEDIA_DIR/$_out.part" 2>/dev/null; then
|
"$MEDIA_DIR/$_out.part" </dev/null 2>/dev/null; then
|
||||||
rm -f "$MEDIA_DIR/$_out.part"
|
rm -f "$MEDIA_DIR/$_out.part"
|
||||||
echo "fetch-media: could not encode $_out, serving without that tier" >&2
|
echo "fetch-media: could not encode $_out, serving without that tier" >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
_got=$(ffprobe -v error -select_streams v:0 -show_entries stream=codec_name \
|
_got=$(ffprobe -v error -select_streams v:0 -show_entries stream=codec_name \
|
||||||
-of default=nw=1:nk=1 "$MEDIA_DIR/$_out.part" 2>/dev/null | head -n1)
|
-of default=nw=1:nk=1 "$MEDIA_DIR/$_out.part" </dev/null 2>/dev/null | head -n1)
|
||||||
if [ "$_got" != "$_want" ]; then
|
if [ "$_got" != "$_want" ]; then
|
||||||
rm -f "$MEDIA_DIR/$_out.part"
|
rm -f "$MEDIA_DIR/$_out.part"
|
||||||
echo "fetch-media: $_out came out as '$_got', expected '$_want' — discarding it" >&2
|
echo "fetch-media: $_out came out as '$_got', expected '$_want' — discarding it" >&2
|
||||||
|
|
@ -288,7 +322,14 @@ encoded=0
|
||||||
#
|
#
|
||||||
# Fed by a here-document rather than a pipe so the counters below survive — in
|
# Fed by a here-document rather than a pipe so the counters below survive — in
|
||||||
# `jq | while`, the loop runs in a subshell and every increment is discarded.
|
# `jq | while`, the loop runs in a subshell and every increment is discarded.
|
||||||
while IFS= read -r src; do
|
#
|
||||||
|
# The list arrives on fd 3, not stdin, and the loop reads it from there. That is
|
||||||
|
# not decoration: ffmpeg below reads standard input for interactive keystrokes
|
||||||
|
# and swallowed the front of the next URL straight off the here-document, which
|
||||||
|
# cost a few posts their mirrored media on every run that had something to
|
||||||
|
# encode. ffmpeg is told -nostdin as well, but keeping the list off stdin
|
||||||
|
# entirely is what stops the next tool added to this loop from doing it again.
|
||||||
|
while IFS= read -r src <&3; do
|
||||||
[ -n "$src" ] || continue
|
[ -n "$src" ] || continue
|
||||||
|
|
||||||
# Ours already — adopt the file on the mount and do no network at all.
|
# Ours already — adopt the file on the mount and do no network at all.
|
||||||
|
|
@ -445,7 +486,7 @@ while IFS= read -r src; do
|
||||||
--argjson w "${w:-0}" --argjson h "${h:-0}" \
|
--argjson w "${w:-0}" --argjson h "${h:-0}" \
|
||||||
'. + [{src: $src, path: $path, w: $w, h: $h}]' "$MAP" > "$MAP.new" \
|
'. + [{src: $src, path: $path, w: $w, h: $h}]' "$MAP" > "$MAP.new" \
|
||||||
&& mv "$MAP.new" "$MAP"
|
&& mv "$MAP.new" "$MAP"
|
||||||
done <<EOF
|
done 3<<EOF
|
||||||
$(jq -r --arg re "$MEDIA_REF_RE" \
|
$(jq -r --arg re "$MEDIA_REF_RE" \
|
||||||
'[ (.[].media[]? | .src, (.poster // empty)),
|
'[ (.[].media[]? | .src, (.poster // empty)),
|
||||||
(.[] | .body // "" | scan($re)) ]
|
(.[] | .body // "" | scan($re)) ]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue