chore(straymark): update framework fw-4.27.0 → fw-4.32.0 #58
Workflow file for this run
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
| # ============================================================================= | |
| # StrayMark - 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: | |
| - '.straymark/**' | |
| - '.github/workflows/docs-validation.yml' | |
| pull_request: | |
| branches: [main, develop] | |
| paths: | |
| - '.straymark/**' | |
| # Single source of truth for the DocType prefixes the workflow recognizes. | |
| # Adding a new DocType requires updating BOTH this list and | |
| # `cli/src/document.rs::DocType::ALL_PREFIXES`. Keep them in sync. | |
| # Files whose basename does not start with one of these prefixes followed by | |
| # `-` are treated as framework / governance / template files and are silently | |
| # skipped from the per-document checks (no manual exclude list needed). | |
| env: | |
| DOC_TYPE_PREFIXES: "AILOG|AIDEC|ADR|ETH|REQ|TES|INC|TDE|SEC|MCARD|SBOM|DPIA|PIPIA|CACFILE|TC260RA|AILABEL" | |
| 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: | | |
| .straymark/**/*.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 | |
| # Whitelist approach: only files whose basename starts with one of | |
| # the known DocType prefixes are validated. Framework / governance / | |
| # template files (e.g., AGENT-RULES.md, AI-KPIS.md, NIST-AI-RMF-*) | |
| # don't match any prefix and are silently skipped — no manual | |
| # exclude list to maintain. | |
| PREFIX_RE="^(${DOC_TYPE_PREFIXES})-" | |
| # Optional single-letter suffix on the sequence number lets adopters | |
| # resolve same-day same-sequence filename collisions without | |
| # renumbering downstream entries (e.g. `-028b-` when `-028-` is | |
| # already taken by an earlier-committed file on the same date). | |
| VALID_PATTERN="^(${DOC_TYPE_PREFIXES})-[0-9]{4}-[0-9]{2}-[0-9]{2}-[0-9]{3}[a-z]?-[a-z0-9-]+\.md$" | |
| for file in ${{ steps.changed-files.outputs.all_changed_files }}; do | |
| filename=$(basename "$file") | |
| # Skip files that are not adopter-created typed documents. | |
| if ! echo "$filename" | grep -qE "$PREFIX_RE"; then | |
| echo " ⊘ Skipped (framework / non-typed): $filename" | |
| continue | |
| fi | |
| # File starts with a DocType prefix — validate the full pattern. | |
| if ! echo "$filename" | grep -qE "$VALID_PATTERN"; then | |
| echo " ✗ Invalid naming: $filename" | |
| echo " Expected: [TYPE]-[YYYY-MM-DD]-[NNN]<a>-[description].md" | |
| echo " Where <a> is an optional single lowercase letter for same-day" | |
| echo " same-sequence collisions (e.g. -028b- when -028- is taken)." | |
| echo " Valid TYPEs: ${DOC_TYPE_PREFIXES//|/, }" | |
| 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 | |
| PREFIX_RE="^(${DOC_TYPE_PREFIXES})-" | |
| REQUIRED_FIELDS="id title status created" | |
| for file in ${{ steps.changed-files.outputs.all_changed_files }}; do | |
| filename=$(basename "$file") | |
| # Only validate adopter-created typed documents. | |
| if ! echo "$filename" | grep -qE "$PREFIX_RE"; 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 | |
| PREFIX_RE="^(${DOC_TYPE_PREFIXES})-" | |
| for file in ${{ steps.changed-files.outputs.all_changed_files }}; do | |
| filename=$(basename "$file") | |
| # Only validate adopter-created typed documents. | |
| if ! echo "$filename" | grep -qE "$PREFIX_RE"; 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: | | |
| .straymark/**/*.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..." | |
| # NOTE: Missing-ETH is intentionally a warning (not an error) — adopters | |
| # may file the ETH later in a follow-up document. No ERRORS counter is | |
| # kept because there is no exit-1 path; the warnings ride through the | |
| # normal CI run and surface in the GitHub Actions UI. | |
| PREFIX_RE="^(${DOC_TYPE_PREFIXES})-" | |
| for file in ${{ steps.changed-files.outputs.all_changed_files }}; do | |
| filename=$(basename "$file") | |
| if ! echo "$filename" | grep -qE "$PREFIX_RE"; 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..." | |
| PREFIX_RE="^(${DOC_TYPE_PREFIXES})-" | |
| for file in ${{ steps.changed-files.outputs.all_changed_files }}; do | |
| filename=$(basename "$file") | |
| if ! echo "$filename" | grep -qE "$PREFIX_RE"; 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..." | |
| STRAYMARK_DIR=".straymark" | |
| # Derive the types array from the workflow-level env var so we never | |
| # have to maintain it in two places. | |
| IFS='|' read -ra TYPES <<< "$DOC_TYPE_PREFIXES" | |
| # Header | |
| { | |
| echo "## StrayMark Governance Metrics" | |
| echo "" | |
| echo "Generated: $(date -u +"%Y-%m-%d %H:%M UTC")" | |
| echo "" | |
| echo "### Documents by Type" | |
| echo "" | |
| echo "| Type | Count |" | |
| echo "|------|------:|" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| TOTAL=0 | |
| for TYPE in "${TYPES[@]}"; do | |
| COUNT=$(find "$STRAYMARK_DIR" -name "${TYPE}-*.md" -not -path "*/templates/*" 2>/dev/null | wc -l) | |
| TOTAL=$((TOTAL + COUNT)) | |
| echo "| $TYPE | $COUNT |" >> "$GITHUB_STEP_SUMMARY" | |
| done | |
| 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 "$STRAYMARK_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 "| **TOTAL** | **$TOTAL** |" | |
| echo "" | |
| echo "### Recent Documents (last 7 days)" | |
| echo "" | |
| echo "Documents created/modified in the last 7 days: **$RECENT**" | |
| echo "" | |
| echo "### Risk Level Distribution" | |
| echo "" | |
| echo "| Risk Level | Count |" | |
| echo "|------------|------:|" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| for RISK in low medium high critical; do | |
| RISK_COUNT=$(grep -rl "^risk_level: *$RISK" "$STRAYMARK_DIR" --include="*.md" 2>/dev/null | grep -vc templates || echo 0) | |
| echo "| $RISK | $RISK_COUNT |" >> "$GITHUB_STEP_SUMMARY" | |
| done | |
| { | |
| echo "" | |
| echo "### Review Compliance" | |
| echo "" | |
| } >> "$GITHUB_STEP_SUMMARY" | |
| HIGH_RISK_DOCS=$(grep -rl "^risk_level: *\(high\|critical\)" "$STRAYMARK_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 > .straymark/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 .straymark/*/; do | |
| folder_name=$(basename "$folder") | |
| { | |
| echo "" | |
| echo "## ${folder_name}" | |
| echo "" | |
| } >> .straymark/INDEX.md | |
| find "$folder" -name "*.md" -type f | sort | while IFS= read -r 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)" >> .straymark/INDEX.md | |
| done | |
| done | |
| echo "✅ Index generated: .straymark/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 .straymark/INDEX.md; then | |
| echo "No changes to index" | |
| else | |
| git add .straymark/INDEX.md | |
| git commit -m "docs: update documentation index [skip ci]" | |
| git push | |
| fi |