diff --git a/.githooks/pre-push b/.githooks/pre-push new file mode 100755 index 0000000..cc14b17 --- /dev/null +++ b/.githooks/pre-push @@ -0,0 +1,3 @@ +#!/usr/bin/env bash +# .githooks/pre-push — invokes preflight.sh (A/B/C/D=build). +exec scripts/preflight.sh diff --git a/.gitignore b/.gitignore index 26fb2a6..abb8b73 100644 --- a/.gitignore +++ b/.gitignore @@ -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) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dd39b67..7de7a9e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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. diff --git a/scripts/preflight.sh b/scripts/preflight.sh new file mode 100755 index 0000000..adc53bf --- /dev/null +++ b/scripts/preflight.sh @@ -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" diff --git a/scripts/setup-hooks.sh b/scripts/setup-hooks.sh new file mode 100755 index 0000000..1f79667 --- /dev/null +++ b/scripts/setup-hooks.sh @@ -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." diff --git a/scripts/verify-changelog-sync.sh b/scripts/verify-changelog-sync.sh new file mode 100755 index 0000000..e8fadd8 --- /dev/null +++ b/scripts/verify-changelog-sync.sh @@ -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" diff --git a/scripts/verify-manifest-shape.sh b/scripts/verify-manifest-shape.sh new file mode 100755 index 0000000..49545bb --- /dev/null +++ b/scripts/verify-manifest-shape.sh @@ -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 diff --git a/scripts/verify-version-consistency.sh b/scripts/verify-version-consistency.sh new file mode 100755 index 0000000..c0b2ae7 --- /dev/null +++ b/scripts/verify-version-consistency.sh @@ -0,0 +1,36 @@ +#!/usr/bin/env bash +# verify-version-consistency.sh — Block A of preflight. +# csproj 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 '[^<]+' "$CSPROJ" | head -1 | sed -E 's/<[^>]+>//g')" +[ -n "$CSPROJ_VER" ] || fail "$CSPROJ has no 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"