diff --git a/.github/workflows/badges-documentation-update.yml b/.github/workflows/badges-documentation-update.yml new file mode 100644 index 0000000000..ce74f205e8 --- /dev/null +++ b/.github/workflows/badges-documentation-update.yml @@ -0,0 +1,163 @@ +name: "Badges: Documentation Update" + +on: + push: + branches: [develop] + paths: + - "docs/**/*.md" + - "**.md" + - ".github/automation/badges.schema.yml" + workflow_dispatch: + inputs: + target_files: + description: "Glob pattern for files to update (default: all)" + required: false + default: "" + dry_run: + description: "Dry run mode (no commits)" + required: false + type: boolean + default: false + +jobs: + update-badges: + name: "Update Documentation Badges" + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + issues: write + actions: read + + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + steps: + - name: "Checkout repository" + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: "Setup Node.js" + uses: actions/setup-node@v4 + with: + node-version: "18" + cache: "npm" + + - name: "Install dependencies" + run: npm ci + + - name: "Detect changed files" + id: changed + run: | + if [[ "${{ github.event_name }}" == "push" ]]; then + # Get files changed in this push + git diff-tree --no-commit-id --name-only -r "${{ github.event.before }}" "${{ github.event.after }}" > changed_files.txt + elif [[ -n "${{ inputs.target_files }}" ]]; then + # Use provided glob pattern with proper escaping + target_pattern="${{ inputs.target_files }}" + find . -path "${target_pattern}" -type f > changed_files.txt + else + # Find all markdown files with frontmatter + find docs instructions . -name "*.md" -type f | head -50 > changed_files.txt + fi + + # Filter to only markdown files + grep '\.md$' changed_files.txt || true + + count=$(grep -c '\.md$' changed_files.txt || echo 0) + echo "count=$count" >> $GITHUB_OUTPUT + echo "files=$(cat changed_files.txt | tr '\n' ',' | sed 's/,$//')" >> $GITHUB_OUTPUT + + - name: "Validate badge schema" + id: validate + run: | + if [[ ! -f ".github/automation/badges.schema.yml" ]]; then + echo "::warning::Badge schema not found - skipping badge generation" + exit 0 + fi + + # Basic YAML validation + npm run validate:badges-schema 2>/dev/null || { + echo "::warning::Badge schema validation failed - will attempt generation anyway" + } + + - name: "Generate and update badges" + id: generate + run: | + # Check if we have files to process + if [[ ! -f changed_files.txt ]]; then + echo "No files to process" + echo "updated_count=0" >> $GITHUB_OUTPUT + exit 0 + fi + + updated_count=0 + failed_count=0 + + while IFS= read -r file; do + [[ -z "$file" ]] && continue + [[ ! -f "$file" ]] && continue + + echo "Processing: $file" + + # Check if file has badge markers + if grep -q "" "$file" 2>/dev/null; then + # Use node script to generate and insert badges with schema + npm run generate:badges -- "$file" 2>/dev/null && { + echo " βœ“ Badges generated" + ((updated_count++)) + } || { + echo " βœ— Badge generation failed" + ((failed_count++)) + } + else + # Skip files without markers + echo " β—‹ No badge markers - skipping" + fi + done < changed_files.txt + + echo "updated_count=$updated_count" >> $GITHUB_OUTPUT + echo "failed_count=$failed_count" >> $GITHUB_OUTPUT + + - name: "Commit badge updates" + if: steps.generate.outputs.updated_count > 0 && !inputs.dry_run + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + git add docs/**/*.md *.md 2>/dev/null || true + + if git diff --cached --quiet; then + echo "No changes to commit" + exit 0 + fi + + git commit -m "docs: Update badges in documentation [skip ci]" || { + echo "::warning::No changes to commit" + exit 0 + } + + git push origin HEAD:"${{ github.ref_name }}" + + - name: "Report results" + if: always() + run: | + echo "## πŸ“Š Badge Update Results" + echo "" + echo "- **Files Scanned:** ${{ steps.changed.outputs.count }}" + echo "- **Files Updated:** ${{ steps.generate.outputs.updated_count }}" + echo "- **Failed Updates:** ${{ steps.generate.outputs.failed_count }}" + echo "- **Dry Run:** ${{ inputs.dry_run }}" + echo "" + echo "### Files Processed" + echo "\`\`\`" + cat changed_files.txt 2>/dev/null || echo "No files processed" + echo "\`\`\`" + + - name: "Log errors" + if: failure() + run: | + echo "::error::Badge update workflow failed" + echo "Check logs above for details" diff --git a/.github/workflows/badges-health-check.yml b/.github/workflows/badges-health-check.yml new file mode 100644 index 0000000000..7873894dfd --- /dev/null +++ b/.github/workflows/badges-health-check.yml @@ -0,0 +1,227 @@ +name: "Badges: Health Check" + +on: + schedule: + # Run weekly on Tuesday at 02:00 UTC + - cron: "0 2 * * 2" + workflow_dispatch: + inputs: + report_only: + description: "Report findings without creating issues" + required: false + type: boolean + default: false + +jobs: + health-check: + name: "Validate All Badge Links" + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + steps: + - name: "Checkout repository" + uses: actions/checkout@v4 + with: + fetch-depth: 1 + + - name: "Setup Node.js" + uses: actions/setup-node@v4 + with: + node-version: "18" + cache: "npm" + + - name: "Install dependencies" + run: npm ci + + - name: "Find markdown files with badges" + id: find + run: | + echo "Finding markdown files with badge markers..." + + find . -type f -name "*.md" \ + ! -path "*/node_modules/*" \ + ! -path "*/.git/*" \ + ! -path "*/.github/*" \ + -exec grep -l "" {} \; > /tmp/files_with_badges.txt + + count=$(grep -c . /tmp/files_with_badges.txt || echo 0) + echo "files_count=$count" >> $GITHUB_OUTPUT + + echo "Found $count files with badges" + + - name: "Extract badge URLs" + id: extract + run: | + echo "Extracting badge URLs from markdown files..." + + total_badges=0 + total_files=$(grep -c . /tmp/files_with_badges.txt || echo 0) + + > /tmp/all_badges.txt + + while IFS= read -r file; do + [[ -z "$file" ]] && continue + + echo "Processing: $file" + + # Extract badge URLs only (img.shields.io and github.com/...badge.svg) + grep -oP '(?:https?://img\.shields\.io/|https?://github\.com/)[^)]+' "$file" | sort -u >> /tmp/all_badges.txt + + done < /tmp/files_with_badges.txt + + # Remove duplicates + sort -u /tmp/all_badges.txt > /tmp/unique_badges.txt + mv /tmp/unique_badges.txt /tmp/all_badges.txt + + total_badges=$(grep -c . /tmp/all_badges.txt || echo 0) + echo "badges_count=$total_badges" >> $GITHUB_OUTPUT + + echo "Found $total_badges unique badge URLs" + + - name: "Validate badge URLs" + id: validate + run: | + echo "Validating badge URLs..." + + broken_count=0 + timeout_count=0 + working_count=0 + + > /tmp/broken_links.txt + > /tmp/timeout_links.txt + + while IFS= read -r url; do + [[ -z "$url" ]] && continue + + echo -n " Testing: $url ... " + + # Test URL with 10 second timeout + response=$(timeout 10 curl -s -o /dev/null -w "%{http_code}" "$url" 2>/dev/null || echo "timeout") + + case "$response" in + 200|301|302|304) + echo "βœ“ OK" + ((++working_count)) + ;; + 404) + echo "βœ— BROKEN (404)" + echo "$url" >> /tmp/broken_links.txt + ((++broken_count)) + ;; + 403|410) + echo "βœ— BROKEN ($response)" + echo "$url" >> /tmp/broken_links.txt + ((++broken_count)) + ;; + timeout) + echo "⚠ TIMEOUT" + echo "$url" >> /tmp/timeout_links.txt + ((++timeout_count)) + ;; + *) + echo "? UNKNOWN ($response)" + ((++working_count)) + ;; + esac + done < /tmp/all_badges.txt + + echo "broken_count=$broken_count" >> $GITHUB_OUTPUT + echo "timeout_count=$timeout_count" >> $GITHUB_OUTPUT + echo "working_count=$working_count" >> $GITHUB_OUTPUT + + - name: "Generate health report" + id: report + run: | + echo "# Badge Health Check Report" > /tmp/health_report.md + echo "" >> /tmp/health_report.md + echo "**Scan Date:** $(date -u '+%Y-%m-%d %H:%M:%S UTC')" >> /tmp/health_report.md + echo "" >> /tmp/health_report.md + echo "## Summary" >> /tmp/health_report.md + echo "" >> /tmp/health_report.md + echo "| Metric | Count |" >> /tmp/health_report.md + echo "|--------|-------|" >> /tmp/health_report.md + echo "| Files Scanned | ${{ steps.find.outputs.files_count }} |" >> /tmp/health_report.md + echo "| Badge URLs | ${{ steps.extract.outputs.badges_count }} |" >> /tmp/health_report.md + echo "| Working | ${{ steps.validate.outputs.working_count }} |" >> /tmp/health_report.md + echo "| Broken | ${{ steps.validate.outputs.broken_count }} |" >> /tmp/health_report.md + echo "| Timeouts | ${{ steps.validate.outputs.timeout_count }} |" >> /tmp/health_report.md + + if [[ ${{ steps.validate.outputs.broken_count }} -gt 0 ]]; then + echo "" >> /tmp/health_report.md + echo "## Broken Links" >> /tmp/health_report.md + cat /tmp/broken_links.txt >> /tmp/health_report.md + fi + + if [[ ${{ steps.validate.outputs.timeout_count }} -gt 0 ]]; then + echo "" >> /tmp/health_report.md + echo "## Timeout Links" >> /tmp/health_report.md + cat /tmp/timeout_links.txt >> /tmp/health_report.md + fi + + cat /tmp/health_report.md + echo "report_generated=true" >> $GITHUB_OUTPUT + + - name: "Create health check issue" + if: steps.validate.outputs.broken_count > 0 && !inputs.report_only + continue-on-error: true + run: | + # Prepend DoR/DoD to health report + { + echo "## Definition of Ready (DoR)" + echo "" + echo "- [x] Badge validation workflow completed" + echo "- [x] Broken links identified" + echo "- [x] Health report generated" + echo "" + cat /tmp/health_report.md + echo "" + echo "## Definition of Done (DoD)" + echo "" + echo "- [ ] Broken links investigated" + echo "- [ ] Schema corrected or links validated" + echo "- [ ] Documentation updated if needed" + echo "- [ ] Health check rerun confirms resolution" + } > /tmp/issue_body.txt + + gh issue create \ + --repo "${{ github.repository }}" \ + --title "πŸ₯ Badge Health Check - ${{ steps.validate.outputs.broken_count }} Broken Links" \ + --body-file /tmp/issue_body.txt \ + --label "type:task,area:automation,priority:important" || { + echo "::warning::Could not create issue" + } + + - name: "Post workflow summary" + if: always() + run: | + echo "## πŸ₯ Badge Health Check Results" + echo "" + echo "**Scan Results:**" + echo "- Files Checked: ${{ steps.find.outputs.files_count }}" + echo "- Badge URLs: ${{ steps.extract.outputs.badges_count }}" + echo "- Working: ${{ steps.validate.outputs.working_count }} βœ“" + echo "- Broken: ${{ steps.validate.outputs.broken_count }} βœ—" + echo "- Timeouts: ${{ steps.validate.outputs.timeout_count }} ⚠" + echo "" + + if [[ ${{ steps.validate.outputs.broken_count }} -eq 0 && ${{ steps.validate.outputs.timeout_count }} -eq 0 ]]; then + echo "βœ… **All badges are healthy!**" + else + echo "❌ **Issues detected - see report above**" + fi + + - name: "Log detailed results" + if: failure() + run: | + echo "Health check failed with errors." + echo "" + echo "Broken links:" + cat /tmp/broken_links.txt || echo "None" + echo "" + echo "Timeout links:" + cat /tmp/timeout_links.txt || echo "None" diff --git a/.github/workflows/badges-readme-status.yml b/.github/workflows/badges-readme-status.yml new file mode 100644 index 0000000000..c78775586d --- /dev/null +++ b/.github/workflows/badges-readme-status.yml @@ -0,0 +1,272 @@ +name: "Badges: README Status Maintenance" + +on: + schedule: + # Run daily at 00:00 UTC + - cron: "0 0 * * *" + workflow_dispatch: + inputs: + force_update: + description: "Force update regardless of changes" + required: false + type: boolean + default: false + +jobs: + update-readme-badges: + name: "Update README Status Badges" + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + checks: read + issues: write + + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + steps: + - name: "Checkout repository" + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: "Setup Node.js" + uses: actions/setup-node@v4 + with: + node-version: "18" + cache: "npm" + + - name: "Install dependencies" + run: npm ci + + - name: "Get repository metadata" + id: repo + run: | + # Get repository name and owner + REPO="${{ github.repository }}" + OWNER="${REPO%/*}" + REPO_NAME="${REPO#*/}" + + echo "owner=${OWNER}" >> $GITHUB_OUTPUT + echo "name=${REPO_NAME}" >> $GITHUB_OUTPUT + echo "full_name=${REPO}" >> $GITHUB_OUTPUT + + - name: "Query workflow status" + id: workflows + run: | + # Get list of critical workflows + WORKFLOWS=( + "checks.yml" + "docs-validation.yml" + "gitleaks.yml" + "main-branch-guard.yml" + "labeling-governance.yml" + "metadata-governance.yml" + "release.yml" + "template-enforcement.yml" + "validate-pr-template.yml" + ) + + # Initialize badges list + BADGES="" + + for workflow in "${WORKFLOWS[@]}"; do + workflow_name="${workflow%.yml}" + + # Get latest run status from GitHub API + status=$(gh run list \ + --repo "${{ steps.repo.outputs.full_name }}" \ + --workflow "$workflow" \ + --limit 1 \ + --json conclusion \ + --jq '.[0].conclusion // "unknown"' 2>/dev/null || echo "unknown") + + # Convert status to badge color and text + case "$status" in + success) + badge_color="success" + badge_text="passing" + ;; + failure) + badge_color="failure" + badge_text="failing" + ;; + cancelled) + badge_color="inactive" + badge_text="cancelled" + ;; + *) + badge_color="lightgrey" + badge_text="unknown" + ;; + esac + + # Create badge markdown + badge_url="https://img.shields.io/badge/${workflow_name}-${badge_text}-${badge_color}.svg" + badge_link="https://github.com/${{ steps.repo.outputs.full_name }}/actions/workflows/${workflow}" + badge_md="[![${workflow_name}](${badge_url})](${badge_link})" + + BADGES="${BADGES}${badge_md}"$'\n' + + echo " βœ“ $workflow_name: $badge_text" + done + + # Save badges to file + echo "$BADGES" > /tmp/badges.txt + echo "badge_count=$(echo "$BADGES" | grep -c "^\[" || echo 0)" >> $GITHUB_OUTPUT + + - name: "Update README.md" + id: update + run: | + README_FILE="./README.md" + + if [[ ! -f "$README_FILE" ]]; then + echo "::warning::README.md not found - skipping update" + exit 0 + fi + + # Check if README has badge markers + if ! grep -q "" "$README_FILE"; then + echo "::warning::Badge markers not found in README.md - skipping update" + exit 0 + fi + + # Read new badges + NEW_BADGES=$(cat /tmp/badges.txt) + + # Replace badges between markers + TEMP_FILE="${README_FILE}.tmp" + awk ' + BEGIN { in_badges = 0 } + // { + print + in_badges = 1 + while ((getline < "/tmp/badges.txt") > 0) { + print + } + next + } + // { + in_badges = 0 + print + next + } + !in_badges || /