Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -198,3 +198,20 @@ jobs:
env:
BASE_BRANCH: origin/${{ github.base_ref }}
run: mise vale:version-urls

frontmatter:
name: Validate frontmatter
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
with:
fetch-depth: 0
- uses: jdx/mise-action@be3be2260bc02bc3fbf94c5e2fed8b7964baf074 # v3.4.0
env:
GITHUB_TOKEN: ${{ github.token }}
- name: Fetch base branch
run: git fetch origin ${{ github.base_ref }}:${{ github.base_ref }}
- name: Validate frontmatter in new docs
env:
BASE_BRANCH: origin/${{ github.base_ref }}
run: mise frontmatter:validate
18 changes: 18 additions & 0 deletions .github/workflows/test-tools.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
name: Test tools
on:
push:
branches: [master]
paths: [tools/**]
pull_request:
paths: [tools/**]
jobs:
test-tools:
name: Test tool scripts
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
- uses: jdx/mise-action@be3be2260bc02bc3fbf94c5e2fed8b7964baf074 # v3.4.0
env:
GITHUB_TOKEN: ${{ github.token }}
- name: Run tool tests
run: mise test:tools
12 changes: 10 additions & 2 deletions mise.toml
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,14 @@ git diff --name-only --diff-filter=d origin/master...HEAD | \
description = "Check for hardcoded version URLs in changed files"
run = "tools/check-version-urls.sh"

[tasks."frontmatter:validate"]
description = "Validate frontmatter in new docs files"
run = "tools/validate-frontmatter.sh"

[tasks."test:tools"]
description = "Run all tool tests (tools/test-*.sh)"
run = "for f in tools/test-*.sh; do [ -x \"$f\" ] && \"$f\"; done"

[tasks.check]
description = "Run vale and link checker (requires dev server running at localhost:7777)"
depends = ["vale:branch", "vale:version-urls", "links:check"]
description = "Run necessary checks"
depends = ["vale:branch", "vale:version-urls", "frontmatter:validate"]
219 changes: 219 additions & 0 deletions tools/test-validate-frontmatter.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
#!/usr/bin/env bash
set -euo pipefail

# Tests for validate-frontmatter.sh
# Runs validation logic against fixture files without git

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
FIXTURES_DIR="$SCRIPT_DIR/test-fixtures/frontmatter"
PASSED=0
FAILED=0

# Create fixtures directory
mkdir -p "$FIXTURES_DIR"

# Helper to run validation on a single file
validate_file() {
local file="$1"
local ERRORS=0

FRONTMATTER=$(awk '/^---$/{if(++c==2)exit; next}c==1' "$file")

if [ -z "$FRONTMATTER" ]; then
echo "no-frontmatter"
return
fi

if ! echo "$FRONTMATTER" | grep -qE '^title:'; then
ERRORS=$((ERRORS + 1))
fi

if ! echo "$FRONTMATTER" | grep -qE '^description: *\S'; then
ERRORS=$((ERRORS + 1))
fi

if ! echo "$FRONTMATTER" | grep -qE '^keywords:'; then
ERRORS=$((ERRORS + 1))
else
KEYWORDS_COUNT=$(echo "$FRONTMATTER" | awk '/^keywords:/{f=1;next} f && /^[a-zA-Z_-]+:/{exit} f && /^ *-/' | wc -l | tr -d ' ')
if [ "$KEYWORDS_COUNT" -lt 1 ]; then
ERRORS=$((ERRORS + 1))
elif [ "$KEYWORDS_COUNT" -gt 3 ]; then
ERRORS=$((ERRORS + 1))
fi
fi

echo "$ERRORS"
}

# Test helper
test_case() {
local name="$1"
local expected="$2"
local file="$3"

result=$(validate_file "$file")
if [ "$result" = "$expected" ]; then
echo "✓ $name"
PASSED=$((PASSED + 1))
else
echo "✗ $name (expected: $expected, got: $result)"
FAILED=$((FAILED + 1))
fi
}

echo "Creating test fixtures..."

# Valid file with all fields
cat > "$FIXTURES_DIR/valid.md" << 'EOF'
---
title: Valid Page
description: This is a valid page with all required fields
keywords:
- test
- valid
---

# Content
EOF

# Valid with 1 keyword
cat > "$FIXTURES_DIR/valid-1-keyword.md" << 'EOF'
---
title: Valid Page
description: This is valid
keywords:
- single
---

# Content
EOF

# Valid with 3 keywords
cat > "$FIXTURES_DIR/valid-3-keywords.md" << 'EOF'
---
title: Valid Page
description: This is valid
keywords:
- one
- two
- three
---

# Content
EOF

# Missing description
cat > "$FIXTURES_DIR/missing-description.md" << 'EOF'
---
title: Missing Description
keywords:
- test
---

# Content
EOF

# Missing keywords
cat > "$FIXTURES_DIR/missing-keywords.md" << 'EOF'
---
title: Missing Keywords
description: No keywords here
---

# Content
EOF

# Missing title
cat > "$FIXTURES_DIR/missing-title.md" << 'EOF'
---
description: No title here
keywords:
- test
---

# Content
EOF

# Too many keywords (4)
cat > "$FIXTURES_DIR/too-many-keywords.md" << 'EOF'
---
title: Too Many Keywords
description: Has 4 keywords
keywords:
- one
- two
- three
- four
---

# Content
EOF

# Empty keywords array
cat > "$FIXTURES_DIR/empty-keywords.md" << 'EOF'
---
title: Empty Keywords
description: Keywords field exists but empty
keywords:
---

# Content
EOF

# No frontmatter
cat > "$FIXTURES_DIR/no-frontmatter.md" << 'EOF'
# Just Content

No frontmatter here
EOF

# All fields missing
cat > "$FIXTURES_DIR/all-missing.md" << 'EOF'
---
layout: page
---

# Content
EOF

# Keywords followed by another field
cat > "$FIXTURES_DIR/keywords-with-next-field.md" << 'EOF'
---
title: Test
description: Test description
keywords:
- one
- two
author: Someone
---

# Content
EOF

echo ""
echo "Running tests..."
echo ""

# Run tests
test_case "valid file with all fields" "0" "$FIXTURES_DIR/valid.md"
test_case "valid with 1 keyword" "0" "$FIXTURES_DIR/valid-1-keyword.md"
test_case "valid with 3 keywords" "0" "$FIXTURES_DIR/valid-3-keywords.md"
test_case "missing description" "1" "$FIXTURES_DIR/missing-description.md"
test_case "missing keywords" "1" "$FIXTURES_DIR/missing-keywords.md"
test_case "missing title" "1" "$FIXTURES_DIR/missing-title.md"
test_case "too many keywords (4)" "1" "$FIXTURES_DIR/too-many-keywords.md"
test_case "empty keywords array" "1" "$FIXTURES_DIR/empty-keywords.md"
test_case "no frontmatter" "no-frontmatter" "$FIXTURES_DIR/no-frontmatter.md"
test_case "all fields missing" "3" "$FIXTURES_DIR/all-missing.md"
test_case "keywords followed by another field" "0" "$FIXTURES_DIR/keywords-with-next-field.md"

echo ""
echo "Results: $PASSED passed, $FAILED failed"

# Cleanup
rm -rf "$FIXTURES_DIR"

if [ $FAILED -gt 0 ]; then
exit 1
fi
90 changes: 90 additions & 0 deletions tools/validate-frontmatter.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
#!/usr/bin/env bash
set -euo pipefail

# Validate frontmatter in new documentation files
# Checks for required fields: title, description, keywords (1-3 items)
# Usage: ./validate-frontmatter.sh [base-branch]

BASE_BRANCH="${1:-${BASE_BRANCH:-origin/master}}"

echo "Validating frontmatter in new docs files..."

HAS_FILES=0
ERRORS=0

# Get only NEW (added) markdown files in app/_src/
while IFS= read -r file; do
# Skip if no files
[ -z "$file" ] && continue

HAS_FILES=1
FILE_ERRORS=0

# Extract frontmatter (between first two ---)
FRONTMATTER=$(awk '/^---$/{if(++c==2)exit; next}c==1' "$file")

if [ -z "$FRONTMATTER" ]; then
echo "ERROR: $file - no frontmatter found"
ERRORS=$((ERRORS + 1))
continue
fi

# Check for title
if ! echo "$FRONTMATTER" | grep -qE '^title:'; then
echo "ERROR: $file - missing 'title' field"
FILE_ERRORS=$((FILE_ERRORS + 1))
fi

# Check for description (must have non-empty value)
if ! echo "$FRONTMATTER" | grep -qE '^description: *\S'; then
echo "ERROR: $file - missing or empty 'description' field"
FILE_ERRORS=$((FILE_ERRORS + 1))
fi

# Check for keywords
if ! echo "$FRONTMATTER" | grep -qE '^keywords:'; then
echo "ERROR: $file - missing 'keywords' field"
FILE_ERRORS=$((FILE_ERRORS + 1))
else
# Count keywords (lines starting with spaces and '-' after 'keywords:')
KEYWORDS_SECTION=$(echo "$FRONTMATTER" | awk '/^keywords:/{f=1;next} f && /^[a-zA-Z_-]+:/{exit} f && /^ *-/' | wc -l | tr -d ' ')

if [ "$KEYWORDS_SECTION" -lt 1 ]; then
echo "ERROR: $file - keywords must have at least 1 item"
FILE_ERRORS=$((FILE_ERRORS + 1))
elif [ "$KEYWORDS_SECTION" -gt 3 ]; then
echo "ERROR: $file - keywords must have at most 3 items (found $KEYWORDS_SECTION)"
FILE_ERRORS=$((FILE_ERRORS + 1))
fi
fi

if [ $FILE_ERRORS -gt 0 ]; then
ERRORS=$((ERRORS + FILE_ERRORS))
fi

done < <(git diff --name-only --diff-filter=A "${BASE_BRANCH}...HEAD" 2>/dev/null | \
grep -E '^app/_src/.*\.(md|markdown)$' | \
grep -v '/generated/' | \
grep -v '/raw/' || true)

if [ $HAS_FILES -eq 0 ]; then
echo "No new docs files found"
exit 0
fi

if [ $ERRORS -gt 0 ]; then
echo ""
echo "Found $ERRORS frontmatter error(s)"
echo ""
echo "Required frontmatter format:"
echo "---"
echo "title: Page Title"
echo "description: 1-2 sentence description for SEO"
echo "keywords:"
echo " - keyword1"
echo " - keyword2"
echo "---"
exit 1
fi

echo "All frontmatter valid"