fix: lesson frontmatter cleanup + CI gates documentation #351
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
| name: Misaka Network Agent Auditor | |
| on: | |
| pull_request: | |
| types: [opened, synchronize, reopened] | |
| workflow_dispatch: | |
| inputs: | |
| pr_number: | |
| description: "PR number to audit" | |
| required: false | |
| type: string | |
| jobs: | |
| audit: | |
| permissions: | |
| contents: read | |
| issues: write | |
| pull-requests: write | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout Code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| # ── Scope Detection (Issue #230) ── | |
| - name: Detect Change Scope | |
| id: scope | |
| run: | | |
| BASE_SHA="${{ github.event.pull_request.base.sha }}" | |
| HEAD_SHA="${{ github.event.pull_request.head.sha }}" | |
| # workflow_dispatch has no PR context — always full | |
| if [ -z "$BASE_SHA" ] || [ -z "$HEAD_SHA" ]; then | |
| echo "scope=full" >> "$GITHUB_OUTPUT" | |
| echo "Scope: full (workflow_dispatch)" | |
| exit 0 | |
| fi | |
| CHANGED=$(git diff --name-only "$BASE_SHA" "$HEAD_SHA") | |
| NON_LESSON=$(echo "$CHANGED" | grep -Ev '^(lessons/|\.github/workflows/(lesson-quality|pr-checks)\.yml$)' || true) | |
| if [ -n "$NON_LESSON" ]; then | |
| echo "scope=full" >> "$GITHUB_OUTPUT" | |
| echo "Scope: full (changes outside lessons/)" | |
| else | |
| echo "scope=lessons-only" >> "$GITHUB_OUTPUT" | |
| echo "Scope: lessons-only" | |
| fi | |
| - name: Setup Python | |
| uses: actions/setup-python@v5 | |
| with: | |
| python-version: "3.10" | |
| cache: pip | |
| - name: Install Dependencies | |
| if: steps.scope.outputs.scope == 'full' | |
| run: | | |
| echo "=== Installing core dependencies ===" | |
| pip install -r requirements.txt || { echo "::error::Core dependency install failed"; exit 1; } | |
| echo "=== Installing optional dependencies ===" | |
| find . -path ./venv -prune -o -name "requirements.txt" -print | while IFS= read -r req; do | |
| dir=$(dirname "$req") | |
| if [ "$dir" != "." ]; then | |
| echo " Installing $req ..." | |
| pip install -r "$req" 2>&1 || echo " ⚠️ Optional install failed: $req" | |
| fi | |
| done | |
| echo "=== Test dependencies ===" | |
| pip install pytest pytest-cov | |
| echo "PYTHONPATH=$(pwd):$PYTHONPATH" >> "$GITHUB_ENV" | |
| echo "=== Dependency install complete ===" | |
| - name: Install Schema Dependencies | |
| if: steps.scope.outputs.scope == 'lessons-only' | |
| run: | | |
| pip install pyyaml jsonschema | |
| echo "PYTHONPATH=$(pwd):$PYTHONPATH" >> "$GITHUB_ENV" | |
| # ── Quality Score Gate (full scope only) ── | |
| - name: Agent Quality Score | |
| if: steps.scope.outputs.scope == 'full' | |
| id: score | |
| continue-on-error: true | |
| uses: ./.github/actions/score-agent | |
| with: | |
| pr-number: ${{ github.event.pull_request.number || github.event.inputs.pr_number }} | |
| repo: "Ikalus1988/MisakaNet" | |
| threshold: "50" | |
| gh-token: ${{ secrets.SHELDON_PAT || secrets.GITHUB_TOKEN }} | |
| - name: Reject Low-Quality PR | |
| if: steps.scope.outputs.scope == 'full' && steps.score.outcome == 'success' && steps.score.outputs.verdict == 'fail' | |
| env: | |
| GH_TOKEN: ${{ secrets.SHELDON_PAT || secrets.GITHUB_TOKEN }} | |
| run: | | |
| PR_NUM="${{ github.event.pull_request.number }}" | |
| echo "=== Low-Quality PR #$PR_NUM Rejected ===" | |
| echo "Score: ${{ steps.score.outputs.score }}/100" | |
| gh pr close "$PR_NUM" --repo Ikalus1988/MisakaNet \ | |
| --comment "### Quality Score Failed | |
| Your PR scored ${{ steps.score.outputs.score }}/100 (minimum: 50). | |
| Issues detected: | |
| ${{ steps.score.outputs.reasons }} | |
| Please clean up formatting noise and resubmit." | |
| exit 1 | |
| # ── DCO Audit Gate (all PRs) ── | |
| - name: DCO Audit | |
| id: dco | |
| uses: Ikalus1988/dco-audit@v1 | |
| with: | |
| base-sha: ${{ github.event.pull_request.base.sha }} | |
| head-sha: ${{ github.event.pull_request.head.sha }} | |
| token: ${{ secrets.SHELDON_PAT || secrets.GITHUB_TOKEN }} | |
| repo: "Ikalus1988/MisakaNet" | |
| pr-number: ${{ github.event.pull_request.number || '' }} | |
| - name: Fail on DCO Violation | |
| if: steps.dco.outputs.dco-passed == 'false' | |
| run: | | |
| echo "DCO check failed: ${{ steps.dco.outputs.failed-count }} commit(s) without sign-off." | |
| exit 1 | |
| # ── PR Size Check (full scope only) ── | |
| - name: Check PR Size | |
| if: steps.scope.outputs.scope == 'full' | |
| id: prsize | |
| run: | | |
| CHANGED=${{ github.event.pull_request.changed_files || 0 }} | |
| ADDITIONS=${{ github.event.pull_request.additions || 0 }} | |
| SUSPICIOUS=false | |
| NOTES="" | |
| if [ "$CHANGED" -gt 10 ]; then | |
| SUSPICIOUS=true | |
| NOTES="Warn ${CHANGED} files changed (threshold: 10)" | |
| fi | |
| if [ "$ADDITIONS" -gt 500 ]; then | |
| SUSPICIOUS=true | |
| NOTES="${NOTES}; ${ADDITIONS} lines added (threshold: 500)" | |
| fi | |
| echo "suspicious=${SUSPICIOUS}" >> "$GITHUB_OUTPUT" | |
| echo "notes=${NOTES}" >> "$GITHUB_OUTPUT" | |
| printf '## PR Size Check\n\n| Metric | Value |\n|--------|-------|\n| Files Changed | %s |\n| Lines Added | %s |\n| Suspicious | %s |\n\n' "${CHANGED}" "${ADDITIONS}" "${SUSPICIOUS}" >> "$GITHUB_STEP_SUMMARY" | |
| # ── Secret Scanning (full scope only) ── | |
| - name: Secret Scan | |
| id: secrets | |
| if: steps.scope.outputs.scope == 'full' | |
| run: | | |
| echo "=== Scanning for hardcoded secrets ===" | |
| if python3 scripts/check_worker_secrets.py; then | |
| echo "secrets_result=pass" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "secrets_result=fail" >> "$GITHUB_OUTPUT" | |
| echo "::error::Secret scan found hardcoded secrets — blocking merge" | |
| exit 1 | |
| fi | |
| # ── Dependency Audit (full scope only) ── | |
| - name: Dependency Audit | |
| id: depaudit | |
| if: steps.scope.outputs.scope == 'full' | |
| run: | | |
| # Only hard-fail if this PR actually changes dependency files | |
| BASE_SHA="${{ github.event.pull_request.base.sha }}" | |
| HEAD_SHA="${{ github.event.pull_request.head.sha }}" | |
| DEPS_CHANGED=false | |
| if git diff --name-only "$BASE_SHA" "$HEAD_SHA" | grep -qE "requirements\.txt|hub/requirements\.txt|package\.json|pyproject\.toml"; then | |
| DEPS_CHANGED=true | |
| echo "Dependency files changed — audit is blocking" | |
| else | |
| echo "No dependency files changed — audit is advisory only" | |
| fi | |
| echo "=== Python dependency audit ===" | |
| pip install pip-audit 2>/dev/null || true | |
| AUDIT_FAIL=false | |
| if ! pip-audit --requirement requirements.txt 2>&1; then | |
| AUDIT_FAIL=true | |
| echo "::error::pip-audit found vulnerabilities" | |
| fi | |
| echo "=== JS dependency audit ===" | |
| if [ -f package.json ]; then | |
| if ! npm audit --audit-level=high 2>&1; then | |
| AUDIT_FAIL=true | |
| echo "::error::npm audit found high-severity issues" | |
| fi | |
| fi | |
| if [ "$AUDIT_FAIL" = "true" ]; then | |
| echo "depaudit_result=fail" >> "$GITHUB_OUTPUT" | |
| if [ "$DEPS_CHANGED" = "true" ]; then | |
| exit 1 | |
| else | |
| echo "::warning::Dependency vulnerabilities found but no dependency files changed — not blocking" | |
| fi | |
| else | |
| echo "depaudit_result=pass" >> "$GITHUB_OUTPUT" | |
| fi | |
| # ── Test Suite (full scope only) ── | |
| - name: Run Test Suite | |
| if: steps.scope.outputs.scope == 'full' | |
| id: pytest | |
| run: | | |
| pytest --cov=scripts --cov=misakanet --cov-report=term --cov-fail-under=20 tests/ > pytest_report.txt 2>&1 | |
| cat pytest_report.txt | |
| continue-on-error: true | |
| - name: Parse Coverage | |
| if: steps.scope.outputs.scope == 'full' | |
| id: coverage | |
| run: | | |
| coverage=$(grep -oP 'TOTAL\s+\d+\s+\d+\s+\K\d+(?=%)' pytest_report.txt || echo "0") | |
| echo "rate=$coverage" >> "$GITHUB_OUTPUT" | |
| echo "Coverage: ${coverage}%" | |
| OUTCOME="${{ steps.pytest.outcome }}" | |
| if [ "$OUTCOME" = "success" ]; then | |
| RESULT="PASS" | |
| else | |
| RESULT="FAIL" | |
| fi | |
| printf '## Test Suite\n\n| Metric | Value |\n|--------|-------|\n| Outcome | %s |\n| Coverage | %s%% |\n\n' "${RESULT}" "${coverage}" >> "$GITHUB_STEP_SUMMARY" | |
| # ── Lesson Schema Validation (all PRs) ── | |
| - name: Validate Lesson Schema | |
| id: schema | |
| continue-on-error: true | |
| run: | | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| BASE_SHA="${{ github.event.pull_request.base.sha }}" | |
| HEAD_SHA="${{ github.event.pull_request.head.sha }}" | |
| mapfile -t TARGETS < <(git diff --name-only "$BASE_SHA" "$HEAD_SHA" -- 'lessons/**/*.md' 'lessons/*.md') | |
| else | |
| mapfile -t TARGETS < <(find lessons -name '*.md' 2>/dev/null | sort) | |
| fi | |
| RESULT=pass | |
| if [ "${#TARGETS[@]}" -eq 0 ]; then | |
| echo "No changed lesson markdown files." | |
| else | |
| for target in "${TARGETS[@]}"; do | |
| [ -f "$target" ] || continue | |
| if ! python3 scripts/validate_lessons.py "$target"; then | |
| RESULT=fail | |
| fi | |
| done | |
| fi | |
| if [ "$RESULT" = "pass" ]; then | |
| echo "schema_result=pass" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "schema_result=fail" >> "$GITHUB_OUTPUT" | |
| echo "⚠️ Schema validation produced warnings or errors" >> "$GITHUB_STEP_SUMMARY" | |
| fi | |
| # ── Audit Report (all PRs) ── | |
| - name: Post Audit Report | |
| if: always() | |
| env: | |
| GH_TOKEN: ${{ secrets.SHELDON_PAT || secrets.GITHUB_TOKEN }} | |
| run: | | |
| SCOPE="${{ steps.scope.outputs.scope }}" | |
| OUTCOME="${{ steps.pytest.outcome }}" | |
| COVERAGE="${{ steps.coverage.outputs.rate }}" | |
| SUSPICIOUS="${{ steps.prsize.outputs.suspicious }}" | |
| SIZE_NOTES="${{ steps.prsize.outputs.notes }}" | |
| SCORE="${{ steps.score.outputs.score }}" | |
| REASONS="${{ steps.score.outputs.reasons }}" | |
| DCO_RESULT="${{ steps.dco.outputs.dco-passed }}" | |
| DCO_FAILED="${{ steps.dco.outputs.failed-count }}" | |
| SCHEMA="${{ steps.schema.outputs.schema_result }}" | |
| SECRETS="${{ steps.secrets.outputs.secrets_result }}" | |
| DEPAUDIT="${{ steps.depaudit.outputs.depaudit_result }}" | |
| PR_NUM="${{ github.event.inputs.pr_number || github.event.pull_request.number }}" | |
| SHA="${{ github.event.pull_request.head.sha || 'manual' }}" | |
| SHA_SHORT=$(echo "$SHA" | cut -c1-7) | |
| # ── Build report ── | |
| REPORT="## 🧾 Audit Report — PR #${PR_NUM} (${SHA_SHORT})" | |
| if [ "$SCOPE" = "lessons-only" ]; then | |
| REPORT+=$'\n\n' | |
| REPORT+="> 📚 **Lessons-only PR** — skipped: quality score, PR size, test suite" | |
| fi | |
| REPORT+=$'\n\n' | |
| # 1. Quality Score (full scope only) | |
| if [ "$SCOPE" = "full" ]; then | |
| REPORT+="### 📊 Quality Score" | |
| REPORT+=$'\n\n' | |
| if [ -z "$SCORE" ]; then | |
| REPORT+="⚠️ Quality score unavailable; continuing with hard gates." | |
| elif [ -n "$REASONS" ]; then | |
| REPORT+="Score: ${SCORE}/100" | |
| REPORT+=$'\n\n' | |
| REPORT+="**Deductions:**" | |
| REPORT+=$'\n' | |
| echo "$REASONS" | while IFS= read -r line; do | |
| [ -n "$line" ] && REPORT+="- ${line}"$'\n' | |
| done | |
| else | |
| REPORT+="Score: ${SCORE}/100 — no deductions." | |
| fi | |
| REPORT+=$'\n\n' | |
| fi | |
| # 2. DCO Audit | |
| REPORT+="### 🔏 DCO Audit" | |
| REPORT+=$'\n\n' | |
| if [ "$DCO_RESULT" = "true" ]; then | |
| REPORT+="✅ All commits signed-off." | |
| else | |
| REPORT+="❌ **${DCO_FAILED} commit(s)** missing Signed-off-by." | |
| fi | |
| REPORT+=$'\n\n' | |
| # 3. PR Size (full scope only) | |
| if [ "$SCOPE" = "full" ]; then | |
| REPORT+="### 📏 PR Size" | |
| REPORT+=$'\n\n' | |
| REPORT+="| Metric | Value |" | |
| REPORT+=$'\n' | |
| REPORT+="|--------|-------|" | |
| REPORT+=$'\n' | |
| REPORT+="| Files Changed | ${{ github.event.pull_request.changed_files }} |" | |
| REPORT+=$'\n' | |
| REPORT+="| Lines Added | ${{ github.event.pull_request.additions }} |" | |
| REPORT+=$'\n' | |
| if [ "$SUSPICIOUS" = "true" ]; then | |
| REPORT+="| ⚠️ Warning | ${SIZE_NOTES} |" | |
| fi | |
| REPORT+=$'\n\n' | |
| fi | |
| # 4. Secret Scan (full scope only) | |
| if [ "$SCOPE" = "full" ]; then | |
| REPORT+="### 🔐 Secret Scan" | |
| REPORT+=$'\n\n' | |
| if [ "$SECRETS" = "pass" ]; then | |
| REPORT+="✅ No hardcoded secrets detected." | |
| elif [ "$SECRETS" = "fail" ]; then | |
| REPORT+="❌ **Hardcoded secrets found** — merge blocked." | |
| else | |
| REPORT+="⏭️ Skipped." | |
| fi | |
| REPORT+=$'\n\n' | |
| fi | |
| # 5. Dependency Audit (full scope only) | |
| if [ "$SCOPE" = "full" ]; then | |
| REPORT+="### 📦 Dependency Audit" | |
| REPORT+=$'\n\n' | |
| if [ "$DEPAUDIT" = "pass" ]; then | |
| REPORT+="✅ No known vulnerabilities." | |
| elif [ "$DEPAUDIT" = "fail" ]; then | |
| REPORT+="❌ **Vulnerabilities found** — merge blocked." | |
| else | |
| REPORT+="⏭️ Skipped." | |
| fi | |
| REPORT+=$'\n\n' | |
| fi | |
| # 6. Test Suite (full scope only) | |
| if [ "$SCOPE" = "full" ]; then | |
| REPORT+="### 🧪 Test Suite" | |
| REPORT+=$'\n\n' | |
| if [ "$OUTCOME" = "success" ]; then | |
| REPORT+="✅ **PASS** — ${COVERAGE}% coverage" | |
| else | |
| REPORT+="❌ **FAIL** — tests have failures" | |
| if [ -n "$COVERAGE" ]; then | |
| REPORT+=" (${COVERAGE}% coverage)" | |
| fi | |
| fi | |
| REPORT+=$'\n\n' | |
| fi | |
| # 7. Schema Validation | |
| REPORT+="### 📋 Lesson Schema" | |
| REPORT+=$'\n\n' | |
| if [ "$SCHEMA" = "pass" ]; then | |
| REPORT+="✅ All lessons valid." | |
| elif [ "$SCHEMA" = "fail" ]; then | |
| REPORT+="⚠️ Schema validation produced warnings." | |
| else | |
| REPORT+="⏭️ Skipped (no lessons changed)." | |
| fi | |
| REPORT+=$'\n\n' | |
| # 8. Verdict | |
| REPORT+="### ⚖️ Verdict" | |
| REPORT+=$'\n\n' | |
| VERDICT_PASS=true | |
| if [ "$DCO_RESULT" != "true" ]; then | |
| REPORT+="❌ DCO audit failed."$'\n' | |
| VERDICT_PASS=false | |
| fi | |
| if [ "$SCOPE" = "full" ]; then | |
| if [ -n "$SCORE" ] && [ "$SCORE" -lt 40 ] 2>/dev/null; then | |
| REPORT+="❌ Quality Score ${SCORE}/40 below threshold."$'\n' | |
| VERDICT_PASS=false | |
| fi | |
| if [ "$SECRETS" = "fail" ]; then | |
| REPORT+="❌ Secret scan found hardcoded secrets."$'\n' | |
| VERDICT_PASS=false | |
| fi | |
| if [ "$DEPAUDIT" = "fail" ]; then | |
| REPORT+="❌ Dependency audit found vulnerabilities."$'\n' | |
| VERDICT_PASS=false | |
| fi | |
| if [ "$OUTCOME" != "success" ]; then | |
| REPORT+="❌ Test suite failed."$'\n' | |
| VERDICT_PASS=false | |
| fi | |
| fi | |
| if [ "$SCHEMA" = "fail" ]; then | |
| REPORT+="❌ Lesson schema validation failed."$'\n' | |
| VERDICT_PASS=false | |
| fi | |
| if [ "$VERDICT_PASS" = "true" ]; then | |
| if [ "$SCOPE" = "lessons-only" ]; then | |
| REPORT+="✅ DCO + schema passed. Ready for merge." | |
| else | |
| REPORT+="✅ All gates passed. Ready for merge." | |
| fi | |
| fi | |
| REPORT+=$'\n\n---\n' | |
| REPORT+="_Scope: \`${SCOPE}\` | Triggered by \`${SHA_SHORT}\` | [View run](https://github.com/Ikalus1988/MisakaNet/actions/runs/${{ github.run_id }})_" | |
| # ── Post comment ── | |
| if [ -n "$PR_NUM" ]; then | |
| gh pr comment "$PR_NUM" --repo Ikalus1988/MisakaNet --body "$REPORT" 2>/dev/null || \ | |
| gh issue comment "$PR_NUM" --repo Ikalus1988/MisakaNet --body "$REPORT" 2>/dev/null || \ | |
| echo "Could not post comment to PR #$PR_NUM" | |
| fi | |
| # ── Exit with failure if any hard gate fails ── | |
| if [ "$SCOPE" = "full" ] && [ "$SECRETS" = "fail" ]; then | |
| echo "Audit failed: hardcoded secrets detected." | |
| exit 1 | |
| fi | |
| # Note: dep audit blocking is handled in the dep audit step itself | |
| # (only blocks when dependency files actually changed) | |
| if [ "$SCOPE" = "full" ] && [ "$OUTCOME" != "success" ]; then | |
| echo "Audit failed: test suite has issues." | |
| exit 1 | |
| fi | |
| if [ "$DCO_RESULT" != "true" ]; then | |
| echo "Audit failed: DCO violations." | |
| exit 1 | |
| fi | |
| if [ "$SCHEMA" = "fail" ]; then | |
| echo "Audit failed: lesson schema validation failed." | |
| exit 1 | |
| fi | |
| # ── Auto-Merge Gate (all PRs) ── | |
| - name: Auto-Merge Gate | |
| if: success() && github.event_name == 'pull_request' | |
| env: | |
| GH_TOKEN: ${{ secrets.SHELDON_PAT || secrets.GITHUB_TOKEN }} | |
| run: | | |
| PR_NUM="${{ github.event.pull_request.number }}" | |
| echo "=== Auto-Merge Gate for PR #$PR_NUM ===" | |
| if [ -z "$GH_TOKEN" ] || [ "$GH_TOKEN" = "" ]; then | |
| echo " SKIP: GH_TOKEN not available for fork PR" | |
| exit 0 | |
| fi | |
| MERGEABLE=$(gh api repos/Ikalus1988/MisakaNet/pulls/$PR_NUM --jq '.mergeable') | |
| echo "Mergeable: $MERGEABLE" | |
| [ "$MERGEABLE" != "MERGEABLE" ] && { echo "Not mergeable. Skipping."; exit 0; } | |
| BODY=$(gh api repos/Ikalus1988/MisakaNet/pulls/$PR_NUM --jq '.body') | |
| UNCHECKED=$(echo "$BODY" | grep -c "\[ \]" || true) | |
| echo "Unchecked AC items: $UNCHECKED" | |
| [ "$UNCHECKED" -gt 0 ] && { echo "AC not all checked. Skipping."; exit 0; } | |
| gh pr merge "$PR_NUM" --repo Ikalus1988/MisakaNet --merge --auto \ | |
| --subject "Auto-merge #$PR_NUM: ${{ github.event.pull_request.title }}" | |
| echo "Auto-merge enabled for PR #$PR_NUM" |