<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: xzawed</title>
    <description>The latest articles on DEV Community by xzawed (@xzawed).</description>
    <link>https://dev.to/xzawed</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3896824%2F58a4e9be-19f4-4cae-ae5a-23efa15ec65a.png</url>
      <title>DEV Community: xzawed</title>
      <link>https://dev.to/xzawed</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/xzawed"/>
    <language>en</language>
    <item>
      <title>I built a tool that runs static analysis + Claude AI review on every GitHub Push/PR — SCAManager</title>
      <dc:creator>xzawed</dc:creator>
      <pubDate>Sat, 25 Apr 2026 13:44:00 +0000</pubDate>
      <link>https://dev.to/xzawed/i-built-a-tool-that-runs-static-analysis-claude-ai-review-on-every-github-pushpr-scamanager-252m</link>
      <guid>https://dev.to/xzawed/i-built-a-tool-that-runs-static-analysis-claude-ai-review-on-every-github-pushpr-scamanager-252m</guid>
      <description>&lt;p&gt;It started as a small annoyance&lt;br&gt;
PR reviews are always a chore. On a small team — or a side project I run alone — the "someone has to look at this" person is always me. And if you're pushing straight to main, code review effectively disappears.&lt;br&gt;
I started by stacking pylint and flake8 on top of GitHub Actions. But those don't answer the questions that actually matter: did this change do what I meant it to? Or does the commit message actually describe what changed? Static analysis catches grammar and style. It can't read intent.&lt;br&gt;
So I asked Claude to review the same diffs, fused both signals together, scored them out of 100, and pushed the result to Telegram. That became SCAManager.&lt;br&gt;
GitHub: &lt;a href="https://github.com/xzawed/SCAManager" rel="noopener noreferrer"&gt;https://github.com/xzawed/SCAManager&lt;/a&gt;&lt;/p&gt;

&lt;p&gt;What it does&lt;br&gt;
When a GitHub Webhook fires for a Push or PR event, the following runs in parallel:&lt;/p&gt;

&lt;p&gt;Static analysis — pylint, flake8, bandit&lt;br&gt;
AI code review — Claude Haiku 4.5&lt;br&gt;
Commit message evaluation — Claude AI&lt;/p&gt;

&lt;p&gt;Results map to a 100-point score and an A–F grade, then ship to whichever of the nine channels you've configured: Telegram, GitHub PR Comment, GitHub Commit Comment, GitHub Issue, Discord, Slack, Email, Generic Webhook, n8n.&lt;br&gt;
For PRs, the score drives the gate automatically:&lt;/p&gt;

&lt;p&gt;Auto mode — Above threshold → GitHub APPROVE. Below → REQUEST_CHANGES.&lt;br&gt;
Semi-auto mode — Inline buttons in Telegram for manual approval.&lt;br&gt;
Auto-merge — Above a separate threshold → squash merge.&lt;/p&gt;

&lt;p&gt;The scoring system — why these weights&lt;br&gt;
ItemPointsEvaluatorCode quality25pylint + flake8Security20banditCommit message15Claude AIImplementation direction25Claude AITest coverage15Claude AITotal100&lt;br&gt;
Things machines see well go to machines (pylint, bandit). Things that need human judgment go to AI. AI evaluations come back on a 0–10 or 0–20 scale, then get re-weighted into the final score.&lt;br&gt;
If ANTHROPIC_API_KEY isn't set, the AI items default to a neutral middle, and static analysis alone can still hit 89 points (B grade) at most. The tool isn't useless without API spend.&lt;/p&gt;

&lt;p&gt;Architecture — the parts that were interesting to build&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;asyncio.gather() for parallelism
Running static analysis and AI review serially makes per-PR analysis time miserable. Wrapping them in asyncio.gather() collapses total wall-clock to whatever the slowest task is.
I use asyncio.gather(return_exceptions=True) for the nine notification channels too — but here the goal is isolation, not speed. If Telegram is down, that shouldn't block Slack.&lt;/li&gt;
&lt;li&gt;Idempotency — same SHA, no double work
GitHub Webhooks get retransmitted (response timeouts, retries, etc.). Running the same commit SHA twice costs money and produces no new information, so I dedupe by SHA at the DB layer.
GitHub Push/PR
└─ POST /webhooks/github  (HMAC-SHA256 verification)
   └─ BackgroundTask: run_analysis_pipeline()
        ├─ Repo register · SHA dedup (idempotency)
        ├─ asyncio.gather() ── parallel
        │    ├─ analyze_file() × N  (pylint · flake8 · bandit)
        │    └─ review_code()       (Claude AI)
        ├─ calculate_score() → grade
        ├─ run_gate_check()  [PR only]
        └─ asyncio.gather(return_exceptions=True) → notification channels&lt;/li&gt;
&lt;li&gt;Two ways to use the AI
Same review, two call paths:&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Server mode — Anthropic API. Needs ANTHROPIC_API_KEY. Costs money.&lt;br&gt;
Local hook mode — Claude Code CLI (claude -p). Runs locally, no API key needed.&lt;/p&gt;

&lt;p&gt;Local hook mode runs as a pre-push git hook. Output goes to terminal and to the dashboard. Environments without the CLI (Codespaces, mobile) silently skip the hook — exit 0 always, never blocks the push.&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;DB Failover
I built a FailoverSessionFactory that switches over to a fallback PostgreSQL when primary dies. /health reports which DB is currently active.
Honestly, this is probably over-engineered. Whether a small side project actually needs failover is a separate question — building it was largely a learning exercise.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;Limits and trade-offs&lt;br&gt;
This tool isn't going to fit every team. Being honest about it:&lt;/p&gt;

&lt;p&gt;Python-only — Static analysis is pylint/flake8/bandit. For non-Python repos, only the AI review piece gives you value.&lt;br&gt;
AI score consistency — LLM output isn't 100% deterministic. The score is for spotting trends, not as a hard, trustworthy number.&lt;br&gt;
API cost — Teams shipping big PRs frequently can rack up Claude API spend fast. File filters and thresholds give you some control, but it's a real cost line.&lt;br&gt;
Auto-merge risk — Score-driven squash merge is convenient and dangerous. Validate your threshold settings before turning it on. Start in semi-auto mode.&lt;/p&gt;

&lt;p&gt;If you want to try it&lt;/p&gt;

&lt;p&gt;Repo: &lt;a href="https://github.com/xzawed/SCAManager" rel="noopener noreferrer"&gt;https://github.com/xzawed/SCAManager&lt;/a&gt;&lt;br&gt;
License: MIT&lt;br&gt;
Required: Python 3.13 · PostgreSQL · GitHub OAuth App&lt;br&gt;
Optional: ANTHROPIC_API_KEY · Telegram Bot Token · SMTP&lt;/p&gt;

&lt;p&gt;Easiest deploy: Railway with the PostgreSQL plugin and your env vars filled in. For on-prem, uvicorn + nginx + systemd works fine.&lt;br&gt;
Feedback, issues, and "wait, is this actually how it should behave?" reports are all welcome.&lt;/p&gt;

</description>
      <category>ai</category>
      <category>programming</category>
    </item>
  </channel>
</rss>
