I merged two PRs to fix my dependency alerts on GitHub. Both had empty diffs.
GitHub kept showing 11 open Dependabot alerts (10 high, 1 moderate). It was right, and I was wrong.
When I dug into my terminal, I saw this:
text
- pypdf==6.21.4
- pypdf==6.21.1 My security patch was silently downgraded. Here is exactly why it happened and how to fix it.
Why did uv add downgrade my package?
I was working on CivicAI, an open-source PII redaction pipeline. I ran uv add presidio-analyzer presidio-anonymizer to add a new feature.
However, my local main branch was 5 commits behind origin/main.
pyproject.toml said "pypdf>=4.0".
My local uv.lock was stale and had pypdf 6.21.1 cached.
The resolver saw that 6.21.1 satisfied >=4.0, so it silently kept the vulnerable version.
I pushed the code. The tests passed. But the lockfile still had vulnerabilities.
The fix that actually sticks
You cannot just fix the lockfile; you have to fix the rules so it can't happen again.
Step 1: Sync your local repo
bash
git fetch origin
git rebase origin/main
Step 2: Force the upgrade
bash
uv lock --upgrade-package pypdf
Step 3: Raise the floor in pyproject.toml
Change "pypdf>=4.0" to "pypdf>=6.21.4".
This is the most important step. By raising the floor, even if a developer runs uv add on a stale lockfile, the resolver is forced to install 6.21.4 or higher.
Step 4: Declare your test dependencies
I also noticed my Presidio integration test was silently skipping on a fresh clone because presidio-analyzer wasn't in pyproject.toml.
toml
pyproject.toml
dependencies = [
"pypdf>=6.21.4",
"presidio-analyzer>=2.2.0",
"presidio-anonymizer>=2.2.0",
]
Result:
bash
$ gh api repos/sitara2007/CivicAI/dependabot/alerts --jq '[.[]|select(.state=="open")]|length'
0
Tests: 28 passed, 1 skipped (tested against real Postgres).
The Takeaway for Engineers
A merged PR isn’t a fix until the lockfile diff proves it. Always run git diff uv.lock before merging.
A skipped test is a signal, not a pass. If a test skips because a dependency isn't declared, your CI is lying to you.
Passing tests, dependency scanning, and code scanning answer different questions. You need all three.
I'm building CivicAI, an open-source PII redaction pipeline with a tamper-evident audit log. If your team sends sensitive text to LLMs, what's the hardest part of redaction today?
Top comments (2)
check this🤔 github.com/sitara2007/CivicAI
"A skipped test is a signal, not a pass" is the line I'd pin above the rest of it. A skip and a pass are different claims that render identically in the one place anyone looks, which is the same reason a scanner can be sitting on eleven alerts behind two merged PRs with empty diffs. The check wasn't wrong. Nothing distinguished "this ran and was fine" from "this never ran".
On your closing question: the hardest part of redaction I've hit is that the check usually shares an assumption with the thing it checks. I worked on a PDF redactor (pdfairlock.com) that verified its own output by re-opening the file and confirming the sensitive string was gone. It passed every time, and it was right to pass, because the text really was destroyed. The black box was in the wrong place, since the drawing and the verifying both computed position the same wrong way. A human looking at a picture caught it; no assertion written in that style could have.
The text equivalent would be scoring the redacted output with the same entity recogniser that produced the spans. If the detector misses an entity type, the verifier misses it identically, and the tamper-evident audit log then faithfully records a clean run. Which is worse than no audit log, because now you have a receipt.