| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160 |
- #!/usr/bin/env bash
- # Staleness verifier for ffmpeg-ops docs — offline structural + live build-drift.
- #
- # --offline (default): structural integrity, NO ffmpeg needed. Assets parse as
- # JSON, every reference/script/asset on disk is cited from SKILL.md, and every
- # relative link in SKILL.md resolves. Runs in PR CI; may block.
- # --live: does the documentation still match an actual ffmpeg? Extracts the
- # encoders/filters the docs rely on and checks them against the INSTALLED
- # build (`-encoders`/`-filters`/`-h full`). Core items missing = drift
- # (exit 10); build-optional items (libx265, libvmaf, ...) only warn.
- # Runs in the scheduled freshness workflow; never blocks a PR.
- #
- # Usage: verify-commands.sh [--offline | --live] [-q]
- # Input: none (inspects the skill's own files; --live also the ffmpeg on PATH)
- # Output: stdout = findings (one per line, "DRIFT:" / "STRUCT:" prefixed)
- # Stderr: progress, warnings
- # Exit: 0 clean, 2 usage, 7 ffmpeg unavailable (--live only; advisory),
- # 10 drift/structural finding
- #
- # Examples:
- # verify-commands.sh --offline
- # verify-commands.sh --live
- # verify-commands.sh --live -q; echo "exit=$?"
- set -uo pipefail
- EXIT_OK=0; EXIT_USAGE=2; EXIT_UNAVAILABLE=7; EXIT_DRIFT=10
- MODE="offline"; QUIET=0
- while [[ $# -gt 0 ]]; do
- case "$1" in
- --offline) MODE="offline" ;;
- --live) MODE="live" ;;
- -q|--quiet) QUIET=1 ;;
- -h|--help) sed -n '2,26p' "$0" | sed 's/^# \{0,1\}//'; exit "$EXIT_OK" ;;
- *) echo "ERROR: unknown argument: $1 (try --help)" >&2; exit "$EXIT_USAGE" ;;
- esac
- shift
- done
- SKILL_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
- SKILL_MD="$SKILL_DIR/SKILL.md"
- findings=0
- emit() { [[ "$QUIET" -eq 1 ]] || printf '%s\n' "$1" >&2; }
- finding() { printf '%s\n' "$1"; findings=$((findings + 1)); }
- # Pick a working python for JSON validation (Windows Store stub exits non-zero).
- PYTHON=""
- for c in python3 python py; do
- if command -v "$c" >/dev/null 2>&1 && "$c" -c "" >/dev/null 2>&1; then PYTHON="$c"; break; fi
- done
- # ── offline: structural ──────────────────────────────────────────────────────
- offline_checks() {
- emit "== verify-commands --offline (structural)"
- [[ -f "$SKILL_MD" ]] || { finding "STRUCT: SKILL.md missing"; return; }
- # 1. assets parse as JSON
- for a in "$SKILL_DIR"/assets/*.json; do
- [[ -e "$a" ]] || continue
- if [[ -n "$PYTHON" ]]; then
- "$PYTHON" -c "import json,sys; json.load(open(sys.argv[1], encoding='utf-8'))" "$a" \
- >/dev/null 2>&1 || finding "STRUCT: asset not valid JSON: $(basename "$a")"
- elif command -v jq >/dev/null 2>&1; then
- jq empty "$a" >/dev/null 2>&1 || finding "STRUCT: asset not valid JSON: $(basename "$a")"
- fi
- done
- # 2. every shipped resource is cited from SKILL.md (dead weight check)
- for d in references scripts assets; do
- for f in "$SKILL_DIR/$d"/*; do
- [[ -f "$f" ]] || continue
- base="$(basename "$f")"
- [[ "$base" == ".gitkeep" ]] && continue
- grep -q "$base" "$SKILL_MD" \
- || finding "STRUCT: $d/$base exists on disk but is never cited from SKILL.md"
- done
- done
- # 3. every relative resource link in SKILL.md resolves
- while IFS= read -r path; do
- [[ -e "$SKILL_DIR/$path" ]] \
- || finding "STRUCT: SKILL.md links to missing file: $path"
- done < <(grep -oE '\]\((references|assets|scripts|tests)/[^)#]+\)' "$SKILL_MD" \
- | sed -E 's/^\]\(//; s/\)$//' | sort -u)
- }
- # ── live: docs vs the installed build ────────────────────────────────────────
- live_checks() {
- emit "== verify-commands --live (installed-build drift)"
- if ! command -v ffmpeg >/dev/null 2>&1; then
- echo "ffmpeg not on PATH — live check unavailable (advisory, not a failure)" >&2
- exit "$EXIT_UNAVAILABLE"
- fi
- local encoders filters hfull docs
- encoders="$(ffmpeg -hide_banner -encoders 2>/dev/null)"
- filters="$(ffmpeg -hide_banner -filters 2>/dev/null)"
- hfull="$(ffmpeg -hide_banner -h full 2>/dev/null)"
- docs="$(cat "$SKILL_MD" "$SKILL_DIR"/references/*.md 2>/dev/null)"
- # Filters that exist in EVERY ffmpeg build — absence means the filter was
- # renamed/removed upstream, i.e. our docs drifted.
- local core_filters=(scale crop pad fps overlay concat setpts atempo amix
- silencedetect silenceremove loudnorm palettegen paletteuse
- select tile transpose trim atrim split format)
- # Build-optional (external libs / hw): warn only.
- local optional_tokens=(libx264 libx265 libsvtav1 libaom-av1 libvpx-vp9 libopus
- libmp3lame drawtext subtitles lut3d zscale tonemap
- libvmaf minterpolate vidstabdetect vidstabtransform
- bwdif hqdn3d nlmeans xstack showwaves showspectrum
- colorbalance colortemperature colorchannelmixer
- colorhold vibrance haldclut chromashift)
- # CLI options the cookbook depends on; renamed/removed = drift (-vsync class).
- local core_options=(fps_mode movflags avoid_negative_ts map_metadata
- filter_complex frames pix_fmt)
- # NOTE: the flags column width varies across ffmpeg majors (3 chars <=7.x,
- # 2 chars in 8.x) — match any flag run, then the exact filter name token.
- for f in "${core_filters[@]}"; do
- grep -qE "^ +[A-Z.|]+ +$f +" <<<"$filters" \
- || finding "DRIFT: core filter '$f' not in installed ffmpeg (renamed/removed upstream?)"
- done
- for opt in "${core_options[@]}"; do
- grep -q -- "-$opt" <<<"$hfull" \
- || finding "DRIFT: documented option '-$opt' unknown to installed ffmpeg"
- done
- # Every software encoder the docs name must at least be a known encoder name
- # in this build — missing here is a warning (build config), not drift, EXCEPT
- # the universal natives (aac, ffv1) which every build ships.
- for enc in aac ffv1; do
- grep -qE "^ [A-Z.]{6} +$enc " <<<"$encoders" \
- || finding "DRIFT: native encoder '$enc' not in installed ffmpeg"
- done
- for tok in "${optional_tokens[@]}"; do
- if grep -qF "$tok" <<<"$docs"; then
- grep -qE "(^ [A-Z.]{6} +$tok )|(^ +[A-Z.|]+ +$tok +)" <<<"$encoders"$'\n'"$filters" \
- || emit " warn: '$tok' documented but absent from this build (build-optional — not drift)"
- fi
- done
- # Deprecated-flag tripwire: docs must not RECOMMEND -vsync (mentioning it as
- # deprecated in footgun tables is fine; a code fence using it is not).
- if grep -E '^\s*ffmpeg .*-vsync ' <<<"$docs" | grep -vq 'fps_mode'; then
- finding "DRIFT: a documented command still uses deprecated -vsync (use -fps_mode)"
- fi
- }
- case "$MODE" in
- offline) offline_checks ;;
- live) live_checks ;;
- esac
- if [[ "$findings" -eq 0 ]]; then
- emit "verify-commands ($MODE): clean"
- exit "$EXIT_OK"
- fi
- emit "verify-commands ($MODE): $findings finding(s)"
- exit "$EXIT_DRIFT"
|