docs: add DevTrail documentation for release infrastructure session (… #7
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # ============================================================================= | |
| # 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 |