Skip to content

fix: lesson frontmatter cleanup + CI gates documentation #351

fix: lesson frontmatter cleanup + CI gates documentation

fix: lesson frontmatter cleanup + CI gates documentation #351

Workflow file for this run

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"