Skip to content

docs: add DevTrail documentation for release infrastructure session (… #7

docs: add DevTrail documentation for release infrastructure session (…

docs: add DevTrail documentation for release infrastructure session (… #7

# =============================================================================
# DevTrail - Documentation Validation Workflow
# =============================================================================
#
# This workflow validates documentation on each Pull Request and push to main.
# https://strangedays.tech
#
# Executes:
# 1. File naming convention validation
# 2. Metadata (front-matter) validation
# 3. Sensitive information detection
# 4. Markdown linting
# 5. Internal link verification
#
# =============================================================================
name: Documentation Validation
on:
push:
branches: [main, develop]
paths:
- '.devtrail/**'
- '.github/workflows/docs-validation.yml'
pull_request:
branches: [main, develop]
paths:
- '.devtrail/**'
jobs:
validate-docs:
name: Validate Documentation
runs-on: ubuntu-latest
steps:
# =========================================================================
# Checkout
# =========================================================================
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0 # Required to compare with base branch
# =========================================================================
# Setup Node.js (for markdownlint)
# =========================================================================
- name: Setup Node.js
uses: actions/setup-node@v5
with:
node-version: '20'
- name: Install markdownlint-cli
run: npm install -g markdownlint-cli
# =========================================================================
# Get changed files
# =========================================================================
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v44
with:
files: |
.devtrail/**/*.md
# =========================================================================
# Validate file naming convention
# =========================================================================
- name: Validate file naming convention
if: steps.changed-files.outputs.any_changed == 'true'
run: |
echo "📋 Validating file naming convention..."
ERRORS=0
# Canonical source of valid types: cli/src/document.rs::DocType::ALL_PREFIXES
VALID_PATTERN="^(ADR|REQ|TES|OPS|INC|TDE|AILOG|AIDEC|ETH|DOC|SEC|MCARD|SBOM|DPIA)-[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{3}-[a-z0-9-]+\.md$"
EXCLUDED="PRINCIPLES.md|DOCUMENTATION-POLICY.md|AGENT-RULES.md|TEMPLATE-.*\.md|README.md|QUICK-REFERENCE.md|INDEX.md|GIT-BRANCHING-STRATEGY.md"
for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
filename=$(basename "$file")
# Skip excluded files
if echo "$filename" | grep -qE "$EXCLUDED"; then
echo " ⊘ Excluded: $filename"
continue
fi
# Validate naming convention
if ! echo "$filename" | grep -qE "$VALID_PATTERN"; then
echo " ✗ Invalid naming: $filename"
echo " Expected: [TYPE]-[YYYY-MM-DD]-[NNN]-[description].md"
ERRORS=$((ERRORS + 1))
else
echo " ✓ $filename"
fi
done
if [ $ERRORS -gt 0 ]; then
echo "::error::Found $ERRORS naming convention errors"
exit 1
fi
echo "✅ Naming convention valid"
# =========================================================================
# Validate front-matter
# =========================================================================
- name: Validate front-matter metadata
if: steps.changed-files.outputs.any_changed == 'true'
run: |
echo "📋 Validating metadata..."
ERRORS=0
EXCLUDED="PRINCIPLES.md|DOCUMENTATION-POLICY.md|AGENT-RULES.md|TEMPLATE-.*\.md|README.md|QUICK-REFERENCE.md|INDEX.md|GIT-BRANCHING-STRATEGY.md"
REQUIRED_FIELDS="id title status created"
for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
filename=$(basename "$file")
# Skip excluded files
if echo "$filename" | grep -qE "$EXCLUDED"; then
continue
fi
# Verify front-matter exists
if ! head -1 "$file" | grep -q "^---"; then
echo " ✗ Missing front-matter: $filename"
ERRORS=$((ERRORS + 1))
continue
fi
# Verify required fields
for field in $REQUIRED_FIELDS; do
if ! grep -q "^$field:" "$file"; then
echo " ✗ Missing field '$field' in: $filename"
ERRORS=$((ERRORS + 1))
fi
done
done
if [ $ERRORS -gt 0 ]; then
echo "::error::Found $ERRORS metadata errors"
exit 1
fi
echo "✅ Metadata valid"
# =========================================================================
# Validate risk_level / review_required cross-check
# =========================================================================
- name: Validate risk_level requires review_required
if: steps.changed-files.outputs.any_changed == 'true'
run: |
echo "📋 Validating risk_level and review_required..."
ERRORS=0
EXCLUDED="PRINCIPLES.md|DOCUMENTATION-POLICY.md|AGENT-RULES.md|TEMPLATE-.*\.md|README.md|QUICK-REFERENCE.md|INDEX.md|GIT-BRANCHING-STRATEGY.md"
for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
filename=$(basename "$file")
# Skip excluded files
if echo "$filename" | grep -qE "$EXCLUDED"; then
continue
fi
# Extract front-matter
FRONTMATTER=$(sed -n '/^---$/,/^---$/p' "$file" | sed '1d;$d')
if [ -z "$FRONTMATTER" ]; then
continue
fi
RISK_LEVEL=$(echo "$FRONTMATTER" | grep "^risk_level:" | head -1 | sed 's/risk_level: *//' | tr -d '\r' || true)
REVIEW_REQUIRED=$(echo "$FRONTMATTER" | grep "^review_required:" | head -1 | sed 's/review_required: *//' | tr -d '\r' || true)
if [ "$RISK_LEVEL" = "high" ] || [ "$RISK_LEVEL" = "critical" ]; then
if [ "$REVIEW_REQUIRED" != "true" ]; then
echo "::error file=$file::risk_level is '$RISK_LEVEL' but review_required is not true"
ERRORS=$((ERRORS + 1))
fi
fi
done
if [ $ERRORS -gt 0 ]; then
echo "::error::Found $ERRORS risk_level/review_required errors"
exit 1
fi
echo "✅ Risk level validation passed"
# =========================================================================
# Detect sensitive information
# =========================================================================
- name: Check for sensitive information
if: steps.changed-files.outputs.any_changed == 'true'
run: |
echo "🔒 Checking for sensitive information..."
WARNINGS=0
PATTERNS="password|api_key|apikey|secret|token|private_key|credentials"
for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
MATCHES=$(grep -inE "$PATTERNS" "$file" 2>/dev/null | head -5 || true)
if [ -n "$MATCHES" ]; then
echo "::warning file=$file::Possible sensitive information detected"
echo "$MATCHES"
WARNINGS=$((WARNINGS + 1))
fi
done
if [ $WARNINGS -gt 0 ]; then
echo "⚠️ Detected $WARNINGS files with possible sensitive information"
else
echo "✅ No sensitive information detected"
fi
# =========================================================================
# Markdown Lint
# =========================================================================
- name: Run markdownlint
if: steps.changed-files.outputs.any_changed == 'true'
run: |
echo "📝 Running markdownlint..."
# Create temporary configuration
cat > .markdownlint.json << 'EOF'
{
"default": true,
"MD013": false,
"MD033": false,
"MD041": false,
"MD024": { "siblings_only": true }
}
EOF
markdownlint ${{ steps.changed-files.outputs.all_changed_files }} || {
echo "::warning::markdownlint found formatting issues"
}
echo "✅ Linting completed"
# =========================================================================
# Verify internal links
# =========================================================================
- name: Check internal links
if: steps.changed-files.outputs.any_changed == 'true'
run: |
echo "🔗 Verifying internal links..."
ERRORS=0
for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
# Extract internal markdown links: [text](path)
LINKS=$(grep -oE '\[.+\]\([^http][^)]+\)' "$file" 2>/dev/null || true)
for link in $LINKS; do
# Extract only the path
path=$(echo "$link" | sed 's/.*](//' | sed 's/)//' | sed 's/#.*//')
if [ -n "$path" ]; then
# Resolve relative path
dir=$(dirname "$file")
fullpath="$dir/$path"
if [ ! -f "$fullpath" ] && [ ! -d "$fullpath" ]; then
echo " ✗ Broken link in $file: $path"
ERRORS=$((ERRORS + 1))
fi
fi
done
done
if [ $ERRORS -gt 0 ]; then
echo "::warning::Found $ERRORS broken links"
else
echo "✅ All internal links are valid"
fi
# =========================================================================
# Summary
# =========================================================================
- name: Summary
if: always()
run: |
echo "═══════════════════════════════════════════════════════════════"
echo "📊 Documentation validation completed"
echo "═══════════════════════════════════════════════════════════════"
echo ""
echo "Files validated: ${{ steps.changed-files.outputs.all_changed_files_count }}"
# ===========================================================================
# Job: Compliance check
# ===========================================================================
compliance-check:
name: Compliance Check
runs-on: ubuntu-latest
needs: validate-docs
steps:
- name: Checkout repository
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Get changed files
id: changed-files
uses: tj-actions/changed-files@v44
with:
files: |
.devtrail/**/*.md
- name: Verify high-risk documents have ETH reference
if: steps.changed-files.outputs.any_changed == 'true'
run: |
echo "📋 Checking compliance: high-risk documents must reference an ETH..."
ERRORS=0
EXCLUDED="PRINCIPLES.md|DOCUMENTATION-POLICY.md|AGENT-RULES.md|TEMPLATE-.*\.md|README.md|QUICK-REFERENCE.md|INDEX.md|GIT-BRANCHING-STRATEGY.md"
for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
filename=$(basename "$file")
if echo "$filename" | grep -qE "$EXCLUDED"; then
continue
fi
FRONTMATTER=$(sed -n '/^---$/,/^---$/p' "$file" | sed '1d;$d')
if [ -z "$FRONTMATTER" ]; then
continue
fi
RISK_LEVEL=$(echo "$FRONTMATTER" | grep "^risk_level:" | head -1 | sed 's/risk_level: *//' | tr -d '\r' || true)
if [ "$RISK_LEVEL" = "high" ] || [ "$RISK_LEVEL" = "critical" ]; then
RELATED=$(echo "$FRONTMATTER" | grep -A20 "^related:" | grep "^ *-" || true)
HAS_ETH=$(echo "$RELATED" | grep -i "ETH-" || true)
if [ -z "$HAS_ETH" ]; then
echo "::warning file=$file::High-risk document ($RISK_LEVEL) has no ETH reference in 'related:'"
fi
fi
done
echo "✅ Compliance check completed"
- name: Verify EU AI Act high-risk documents have required section
if: steps.changed-files.outputs.any_changed == 'true'
run: |
echo "📋 Checking EU AI Act compliance..."
EXCLUDED="PRINCIPLES.md|DOCUMENTATION-POLICY.md|AGENT-RULES.md|TEMPLATE-.*\.md|README.md|QUICK-REFERENCE.md|INDEX.md|GIT-BRANCHING-STRATEGY.md"
for file in ${{ steps.changed-files.outputs.all_changed_files }}; do
filename=$(basename "$file")
if echo "$filename" | grep -qE "$EXCLUDED"; then
continue
fi
FRONTMATTER=$(sed -n '/^---$/,/^---$/p' "$file" | sed '1d;$d')
if [ -z "$FRONTMATTER" ]; then
continue
fi
EU_RISK=$(echo "$FRONTMATTER" | grep "^eu_ai_act_risk:" | head -1 | sed 's/eu_ai_act_risk: *//' | tr -d '\r' || true)
if [ "$EU_RISK" = "high" ]; then
BODY=$(sed -n '/^---$/,/^---$/!p' "$file" | tail -n +2)
if ! echo "$BODY" | grep -qi "EU AI Act"; then
echo "::warning file=$file::eu_ai_act_risk is 'high' but document lacks 'EU AI Act Considerations' section"
fi
fi
done
echo "✅ EU AI Act compliance check completed"
# ===========================================================================
# Job: Governance metrics (only on push to main)
# ===========================================================================
governance-metrics:
name: Governance Metrics
runs-on: ubuntu-latest
needs: validate-docs
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Generate governance metrics report
run: |
echo "📊 Generating governance metrics..."
DEVTRAIL_DIR=".devtrail"
TYPES=("AILOG" "AIDEC" "ADR" "ETH" "REQ" "TES" "INC" "TDE" "SEC" "MCARD" "SBOM" "DPIA")
# Header
echo "## DevTrail Governance Metrics" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "Generated: $(date -u +"%Y-%m-%d %H:%M UTC")" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
# Document counts by type
echo "### Documents by Type" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "| Type | Count |" >> "$GITHUB_STEP_SUMMARY"
echo "|------|------:|" >> "$GITHUB_STEP_SUMMARY"
TOTAL=0
for TYPE in "${TYPES[@]}"; do
COUNT=$(find "$DEVTRAIL_DIR" -name "${TYPE}-*.md" -not -path "*/templates/*" 2>/dev/null | wc -l)
TOTAL=$((TOTAL + COUNT))
echo "| $TYPE | $COUNT |" >> "$GITHUB_STEP_SUMMARY"
done
echo "| **TOTAL** | **$TOTAL** |" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
# Documents from the last 7 days
echo "### Recent Documents (last 7 days)" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
WEEK_AGO=$(date -u -d "7 days ago" +%Y-%m-%d 2>/dev/null || date -u -v-7d +%Y-%m-%d 2>/dev/null || echo "0000-00-00")
RECENT=$(find "$DEVTRAIL_DIR" -name "*.md" -not -path "*/templates/*" -newer <(date -d "$WEEK_AGO" +%s 2>/dev/null || echo /dev/null) 2>/dev/null | wc -l || echo 0)
echo "Documents created/modified in the last 7 days: **$RECENT**" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
# Risk level distribution
echo "### Risk Level Distribution" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "| Risk Level | Count |" >> "$GITHUB_STEP_SUMMARY"
echo "|------------|------:|" >> "$GITHUB_STEP_SUMMARY"
for RISK in low medium high critical; do
RISK_COUNT=$(grep -rl "^risk_level: *$RISK" "$DEVTRAIL_DIR" --include="*.md" 2>/dev/null | grep -v templates | wc -l)
echo "| $RISK | $RISK_COUNT |" >> "$GITHUB_STEP_SUMMARY"
done
echo "" >> "$GITHUB_STEP_SUMMARY"
# Review compliance rate
echo "### Review Compliance" >> "$GITHUB_STEP_SUMMARY"
echo "" >> "$GITHUB_STEP_SUMMARY"
HIGH_RISK_DOCS=$(grep -rl "^risk_level: *\(high\|critical\)" "$DEVTRAIL_DIR" --include="*.md" 2>/dev/null | grep -v templates || true)
HIGH_COUNT=$(echo "$HIGH_RISK_DOCS" | grep -c . 2>/dev/null || echo 0)
if [ "$HIGH_COUNT" -gt 0 ]; then
REVIEWED=0
for doc in $HIGH_RISK_DOCS; do
if grep -q "^review_required: *true" "$doc" 2>/dev/null; then
REVIEWED=$((REVIEWED + 1))
fi
done
RATE=$((REVIEWED * 100 / HIGH_COUNT))
echo "High/critical risk documents with review_required: true: **$REVIEWED / $HIGH_COUNT ($RATE%)**" >> "$GITHUB_STEP_SUMMARY"
else
echo "No high/critical risk documents found." >> "$GITHUB_STEP_SUMMARY"
fi
echo "" >> "$GITHUB_STEP_SUMMARY"
echo "✅ Governance metrics generated"
# ===========================================================================
# Job: Generate documentation index (only on main)
# ===========================================================================
generate-index:
name: Generate Documentation Index
runs-on: ubuntu-latest
needs: validate-docs
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- name: Checkout repository
uses: actions/checkout@v5
- name: Generate documentation index
run: |
echo "📚 Generating documentation index..."
cat > .devtrail/INDEX.md << 'EOF'
# Documentation Index
*Automatically generated on $(date -u +"%Y-%m-%d %H:%M UTC")*
## Governance
EOF
# List documents by folder
for folder in .devtrail/*/; do
folder_name=$(basename "$folder")
echo "" >> .devtrail/INDEX.md
echo "## ${folder_name}" >> .devtrail/INDEX.md
echo "" >> .devtrail/INDEX.md
find "$folder" -name "*.md" -type f | sort | while read file; do
filename=$(basename "$file")
# Extract title from front-matter or use filename
title=$(grep "^title:" "$file" 2>/dev/null | sed 's/title: *//' | head -1 || echo "$filename")
echo "- [$title]($file)" >> .devtrail/INDEX.md
done
done
echo "✅ Index generated: .devtrail/INDEX.md"
- name: Commit index if changed
run: |
git config --local user.email "github-actions[bot]@users.noreply.github.com"
git config --local user.name "github-actions[bot]"
if git diff --quiet .devtrail/INDEX.md; then
echo "No changes to index"
else
git add .devtrail/INDEX.md
git commit -m "docs: update documentation index [skip ci]"
git push
fi