The symptom
I’d tagged v2.0.8 of mcp-scan and pushed the release. Logged out, in a private window, the release page rendered fine: title, notes, assets, nothing unusual. Logged in as the account that owns the repo, the same path returned a 500 with no body worth reading, just GitHub’s generic error page.
That split is the whole story. It meant the failure wasn’t in anything an anonymous visitor could trigger. It was in something that only renders for someone with write access to the repo, which narrows the search space by a lot if you notice it early. I didn’t notice it early. I spent the first hour chasing the release itself.
Wrong turn one: the orphaned draft
The repo had an old draft release sitting around from an earlier, abandoned tag. Dangling tag, no published notes, the kind of thing you forget to clean up. It looked like exactly the sort of orphaned row that would confuse a release-listing query, so I deleted it. The 500 didn’t move. Same trace, same page, still owner-only.
Wrong turn two: recreating the release
Next guess: something was wrong with the v2.0.8 release row itself, maybe a malformed asset reference or a tag mismatch GitHub’s renderer choked on. I deleted the release and recreated it from the same tag, clean. Still 500, still only when authenticated. Two fixes in, both plausible, both wrong, and both cost real time because each one looked like it should have worked.
That’s usually the tell that you’re fixing the wrong layer: the fix addresses something real and broken-looking, but the error doesn’t care. If the error persists unchanged across two structurally different fixes to the same object, the object isn’t the problem.
Reproducing it properly
I stopped guessing at the release data and reproduced the exact failure in the owner’s own authenticated session instead of inferring it from behavior. Loaded the page, watched what actually renders differently when GitHub knows you’re the repo owner versus when it doesn’t.
The difference is a banner. If GitHub thinks your repo could be a GitHub Action and you own it, the releases page renders a “Publish this Action to Marketplace” prompt above the release notes. Anonymous visitors never see it, because it’s not their action to publish. It’s conditional, server-rendered, and it was the thing 500ing. The release itself, the part I’d been rebuilding, was fine the whole time.
The actual cause
mcp-scan ships as a GitHub Action, declared in action.yml, with the bare name mcp-scan. GitHub Marketplace assigns each published action a slug derived from its name, and that slug is supposed to be unique. At some point, a Marketplace listing using that exact slug had existed and been delisted. Delisting doesn’t free the slug. It stays reserved, permanently, with no visible record anywhere in the repo, the Action, or the release that it was ever taken.
So the Marketplace-publish component, on every render, tried to check slug availability for mcp-scan, hit a reserved-but-invisible collision, and threw. Server-side. Every time. Only for the owner, because only the owner gets that component at all.
The fix was one line. I renamed the action in action.yml from mcp-scan to mcp-scan Action. The derived slug changed, the collision went away, the Marketplace-publish check succeeded, and the 500 was gone on the next load. Nothing about the release, the tag, or the draft had ever been broken.
What I took from it
Seeing a page work anonymous and break authenticated means a different render path is failing underneath, not a flaky error, so chase what the authenticated view adds before touching anything both views share. I didn’t, and spent an hour rebuilding the release itself. The 500 on the releases page had nothing to do with releases: it came from a Marketplace-publish banner that only owners get. Two structurally different fixes to the release left the error unchanged, which should have told me the object I kept fixing wasn’t the object that was broken. And a delisted Marketplace slug stays reserved forever with zero trace of it anywhere in the repo or the UI, no error, no hint, just a silent collision waiting for the next name that happens to match.