build: add preflight validator family for versions/manifest/changelog drift

Establishes the local pre-push gate. preflight.sh runs four blocks: version
consistency, manifest shape (Icon plus all ImageUrls), changelog sync, plus a
release build as compile-health smoke. setup-hooks.sh wires core.hooksPath to
.githooks. .gitignore opens scripts/ for tracking (setup-dev-env.sh stays
private). Test execution itself lives in a separate local repository and is
not part of this codebase.
This commit is contained in:
2026-05-08 07:23:54 +02:00
parent c64fcfd4d1
commit 0ed88691c2
8 changed files with 179 additions and 1 deletions
+3
View File
@@ -0,0 +1,3 @@
#!/usr/bin/env bash
# .githooks/pre-push — invokes preflight.sh (A/B/C/D=build).
exec scripts/preflight.sh
+1 -1
View File
@@ -9,7 +9,7 @@
.envrc
!.env.example
.vscode/
scripts/
scripts/setup-dev-env.sh
# Local test project (stays out of the published plugin repo;
# pure-function safety net for refactor cycles)
+16
View File
@@ -145,3 +145,19 @@ I respond on weekdays during European business hours and take weekends
and FFXIV patch days off. A pull request that sits for a few days has
not been ignored. Pinging once after a week is fine; please do not
ping daily.
## First-time setup
After cloning, run once:
```bash
./scripts/setup-hooks.sh
```
This wires `core.hooksPath` to `.githooks/`. The pre-push hook runs preflight
(versions/manifest/changelog/build).
### Test suite
The plugin's test suite lives in a separate local repository and is not part of
this codebase. If you need access for development, contact the maintainer.
+22
View File
@@ -0,0 +1,22 @@
#!/usr/bin/env bash
# preflight.sh — pre-push gate. Blocks A/B/C verify config drift; Block D is a
# headless `dotnet build` to catch compile-time API drift. Test execution lives
# in the local Build-Suite repo and is NOT part of this preflight.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
echo "==> preflight: Block A — version consistency"
./scripts/verify-version-consistency.sh
echo "==> preflight: Block B — manifest shape"
./scripts/verify-manifest-shape.sh
echo "==> preflight: Block C — changelog sync"
./scripts/verify-changelog-sync.sh
echo "==> preflight: Block D — plugin compile health"
dotnet build HellionChat/HellionChat.csproj --configuration Release --nologo --verbosity quiet
echo "==> preflight: ALL GREEN"
+13
View File
@@ -0,0 +1,13 @@
#!/usr/bin/env bash
# setup-hooks.sh — installs pre-push hook via core.hooksPath. Idempotent.
# Note: NO pre-commit hook — test execution is local-only in the Build-Suite repo,
# so the plugin repo's pre-commit cannot run tests. Versions/manifest/changelog
# checks happen on pre-push only.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
cd "$ROOT"
git config core.hooksPath .githooks
chmod +x .githooks/pre-push
echo "setup-hooks: core.hooksPath set to .githooks. pre-push live."
+45
View File
@@ -0,0 +1,45 @@
#!/usr/bin/env bash
# verify-changelog-sync.sh — Block C.
# Strips .0 suffix from repo.json AssemblyVersion to derive the 3-digit tag/version.
# yaml.changelog is a single multi-line block with **Hellion Chat X.Y.Z** subblocks.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
YAML="$ROOT/HellionChat/HellionChat.yaml"
REPO_JSON="$ROOT/repo.json"
FORGE_DIR="$ROOT/.github/forge-posts"
fail() { echo "verify-changelog-sync: FAIL — $1" >&2; exit 1; }
ok() { echo "verify-changelog-sync: OK — $1"; }
VER="$(jq -r '.[0].AssemblyVersion' "$REPO_JSON" | sed -E 's/\.0$//')"
TAG="v$VER"
grep -qE "^[[:space:]]*\*\*Hellion Chat ${VER}" "$YAML" \
|| fail "$YAML changelog missing **Hellion Chat ${VER}** subblock. Fix: add the v${VER} block at the top of the changelog field."
jq -r '.[0].Changelog' "$REPO_JSON" | grep -qE "^[[:space:]]*\*\*Hellion Chat ${VER}" \
|| fail "$REPO_JSON Changelog missing **Hellion Chat ${VER}** subblock. Fix: copy the yaml changelog over."
FORGE_FILE="$FORGE_DIR/${TAG}.md"
[ -f "$FORGE_FILE" ] || fail "$FORGE_FILE missing. Fix: create from previous tag's file as template."
SUBTITLE="$(awk '/^---$/{f=!f; next} f && /^subtitle:/' "$FORGE_FILE" | sed -E 's/^subtitle:[[:space:]]*//' | tr -d '\"')"
[ -n "$SUBTITLE" ] || fail "$FORGE_FILE frontmatter missing 'subtitle'."
[ "${#SUBTITLE}" -le 60 ] || fail "$FORGE_FILE subtitle is ${#SUBTITLE} chars (max 60)."
NATUR="$(awk '/^---$/{f=!f; next} f && /^versionsnatur:/' "$FORGE_FILE" | sed -E 's/^versionsnatur:[[:space:]]*//' | tr -d '\"')"
[ -n "$NATUR" ] || fail "$FORGE_FILE frontmatter missing 'versionsnatur'."
[ "${#NATUR}" -le 40 ] || fail "$FORGE_FILE versionsnatur is ${#NATUR} chars (max 40)."
BODY="$(awk '/^---$/{f++; next} f==2' "$FORGE_FILE")"
TITLE_LEN=$((${#VER} + 16 + ${#SUBTITLE}))
FOOTER_LEN=80
TOTAL=$((TITLE_LEN + ${#BODY} + FOOTER_LEN))
[ "$TOTAL" -le 5500 ] || fail "Forge embed total ~${TOTAL} chars > 5500 cap."
YAML_VERSIONS="$(grep -cE '^[[:space:]]*\*\*Hellion Chat' "$YAML" || true)"
[ "$YAML_VERSIONS" -le 4 ] || fail "$YAML changelog has $YAML_VERSIONS version subblocks (max 4). Fix: move oldest to docs/CHANGELOG.md."
ok "yaml/repo.json/forge-post in sync for $TAG, embed-total ~${TOTAL}/5500, $YAML_VERSIONS/4 subblocks"
+43
View File
@@ -0,0 +1,43 @@
#!/usr/bin/env bash
# verify-manifest-shape.sh — Block B. Required fields in lowercase yaml,
# DalamudApiLevel sanity in repo.json, no own DalamudPackager.targets,
# Icon AND every ImageUrl reachable.
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
YAML="$ROOT/HellionChat/HellionChat.yaml"
REPO_JSON="$ROOT/repo.json"
TARGETS_OWN="$ROOT/HellionChat/DalamudPackager.targets"
fail() { echo "verify-manifest-shape: FAIL — $1" >&2; exit 1; }
ok() { echo "verify-manifest-shape: OK — $1"; }
for FIELD in name author punchline description repo_url icon_url image_urls tags changelog; do
grep -qE "^${FIELD}:" "$YAML" || fail "$YAML missing required field: $FIELD"
done
[ -f "$TARGETS_OWN" ] && fail "Own DalamudPackager.targets at $TARGETS_OWN strips Icon/ImageUrls. DELETE it; SDK default works."
API_LEVEL="$(jq -r '.[0].DalamudApiLevel' "$REPO_JSON")"
case "$API_LEVEL" in
''|null) fail "$REPO_JSON missing DalamudApiLevel" ;;
esac
[[ "$API_LEVEL" =~ ^[0-9]+$ ]] || fail "$REPO_JSON DalamudApiLevel must be integer, got: $API_LEVEL"
[ "$API_LEVEL" -ge 12 ] || fail "$REPO_JSON DalamudApiLevel=$API_LEVEL is below SDK 15 floor (12)."
if [ "${HOOKS_OFFLINE:-0}" != "1" ]; then
ICON_URL="$(jq -r '.[0].IconUrl' "$REPO_JSON")"
curl -fsI "$ICON_URL" > /dev/null || fail "IconUrl unreachable: $ICON_URL"
ok "IconUrl reachable: $ICON_URL"
COUNT="$(jq -r '.[0].ImageUrls | length' "$REPO_JSON")"
[ "$COUNT" -ge 1 ] || fail "repo.json ImageUrls is empty"
for i in $(seq 0 $((COUNT - 1))); do
URL="$(jq -r ".[0].ImageUrls[$i]" "$REPO_JSON")"
curl -fsI "$URL" > /dev/null || fail "ImageUrls[$i] unreachable: $URL"
done
ok "$COUNT ImageUrl(s) reachable, DalamudApiLevel=$API_LEVEL"
else
ok "skipped URL reachability (HOOKS_OFFLINE=1), DalamudApiLevel=$API_LEVEL"
fi
+36
View File
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# verify-version-consistency.sh — Block A of preflight.
# csproj <Version> is 3-digit SemVer; repo.json AssemblyVersion is 4-digit (.0 suffix).
set -euo pipefail
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
CSPROJ="$ROOT/HellionChat/HellionChat.csproj"
REPO_JSON="$ROOT/repo.json"
fail() { echo "verify-version-consistency: FAIL — $1" >&2; exit 1; }
ok() { echo "verify-version-consistency: OK — $1"; }
CSPROJ_VER="$(grep -oE '<Version>[^<]+</Version>' "$CSPROJ" | head -1 | sed -E 's/<[^>]+>//g')"
[ -n "$CSPROJ_VER" ] || fail "$CSPROJ has no <Version> element"
EXPECTED_4DIGIT="${CSPROJ_VER}.0"
REPO_VER="$(jq -r '.[0].AssemblyVersion' "$REPO_JSON")"
[ "$REPO_VER" = "$EXPECTED_4DIGIT" ] \
|| fail "csproj=$CSPROJ_VER expects repo.json AssemblyVersion=$EXPECTED_4DIGIT but got $REPO_VER. Fix: align in $REPO_JSON."
TEST_VER="$(jq -r '.[0].TestingAssemblyVersion' "$REPO_JSON")"
[ "$TEST_VER" = "$EXPECTED_4DIGIT" ] \
|| fail "TestingAssemblyVersion=$TEST_VER must match $EXPECTED_4DIGIT. Fix: align in $REPO_JSON."
TAG="v$CSPROJ_VER"
for KEY in DownloadLinkInstall DownloadLinkUpdate DownloadLinkTesting; do
URL="$(jq -r ".[0].$KEY" "$REPO_JSON")"
case "$URL" in
*"/$TAG/"*) ;;
*) fail "$KEY=$URL does not contain tag $TAG. Fix: update $REPO_JSON $KEY to releases/download/$TAG/latest.zip." ;;
esac
done
ok "csproj=$CSPROJ_VER, repo.json=$EXPECTED_4DIGIT, tag $TAG present in DownloadLinks"