Astro 5 to 7 on a Live Site: The Peer-Locked Dependabot Pair and Three Silent Breakages
Two Dependabot major bumps that could not install alone, a revert, and the one PR that moved Astro and MDX together. What broke without a failing build, and what I checked.
The site you are reading ran on Astro 5 until late September. Moving it to Astro 7 took a revert and about a month, and the build failed loudly for only some of the problems. The rest rendered fine, passed a visual glance and would have shipped. This is the log of what happened, using the commit and PR text that is still in the repo.
Attempt one: two green auto-merges that were not supposed to merge
On 2026-08-31 two Dependabot PRs merged: astro 5.18.2 to 7.2.9 (#168) and @astrojs/mdx 4.3.14 to 7.0.8 (#169). Both are semver-major. The repo has a dependabot-auto-merge.yml that is supposed to auto-merge only patch and minor updates, so neither should have gone through.
The merged state had a lockfile out of sync with package.json. npm ci failed with Missing: @astrojs/[email protected] from lock file. That failure surfaced on the first real run of the new Cloudflare Pages deploy workflow, which stopped before publishing. Production stayed on the last manual deploy, so no visitor saw anything. I reverted both in #174 and wrote down the one thing I could not explain: why a major bump passed a gate that checks for patch and minor. The PR says “root cause not fully diagnosed”, and that is still true. My leading suspicion is that a recreated Dependabot PR carries an earlier auto-merge flag forward, but I have not confirmed it.
The revert was the right call. A two-major jump needs an audit of breaking changes, and doing that while a deploy pipeline is red means doing it badly.
Attempt two: the pair that could not install
Dependabot opened the upgrade again as two separate PRs, #192 for astro 5.18.2 to 7.3.2 and #189 for @astrojs/mdx 4.3.14 to 8.0.1. Neither could ever pass CI. @astrojs/mdx@8 requires astro ^7.2.6, and @astrojs/mdx@4 requires astro ^5. Each PR changes one side of a peer constraint that the other side still violates, so npm ci has no valid tree to build. On Astro 5 main, npm ci could not resolve either split PR.
Dependabot’s default behavior opens one PR per package. For independent packages that is a feature. For a peer-locked pair it produces two PRs that fail forever, and the failure looks like a flaky dependency problem rather than a grouping problem. Dependabot supports grouped updates in its config, and my dependabot.yml at the time was the minimal weekly default with no groups.
The fix was one PR, #198, that moved both packages together, superseded both Dependabot PRs and listed the migration changes explicitly.
What broke without a failing build
Four things broke once the lockfile was right. Three of them did not fail the build.
Legacy content collections. Astro 6 removed them. The build does fail here, with LegacyContentConfigError, so this one is loud. The fix is moving src/content/config.ts to src/content.config.ts, adding a glob loader and importing z from astro/zod. Routes and post.id values stayed the same because the filenames were already slugs.
Whitespace between inline elements. Astro 7 changed the compressHTML default to 'jsx', which strips whitespace between inline elements. The 404 page rendered “Page notfound”. The header rendered “Oleh Veheria–Valencia, Spain, CET” with the separators glued together. No test failed, because no test asserted on that exact string. Setting compressHTML: true restored the old behavior.
CSS target. Vite 8 defaults to a Safari 16.4 baseline. That emitted media-query range syntax such as width>=48rem and dropped -webkit-backdrop-filter from the Tailwind backdrop-blur-* utilities. The consequence is lost breakpoints on iOS Safari before 16.4 and no header blur before Safari 18. I pinned vite.build.cssTarget to the Vite 6 default, chrome87 / edge88 / firefox78 / safari14. I did not check what share of traffic that protects. The site is small enough that the number would be noise, so I chose the conservative target.
A second Vite. The upgrade left [email protected] at the top level, so @tailwindcss/vite imported Vite 6 helpers while Astro’s nested Vite 8 ran the build. After npm dedupe there is a single [email protected] and the build output did not change. This one is easy to miss because everything works until the two copies disagree.
The agent detection that killed the test run
This is the one I did not expect. When Astro 7 detects that it is running under an AI agent, it detaches astro dev and astro preview into the background. The command then exits immediately. Playwright’s webServer reported Process from config.webServer exited early, and an orphaned server kept port 4321 occupied.
Most of my local work on this repo goes through Claude Code, so the local green gate and the autopilot’s end-to-end runs both hit it. GitHub Actions is not detected as an agent, so CI was fine, which made the failure look local and intermittent. Adding --ignore-lock to the webServer command in playwright.config.ts made the server stay in the foreground again, and the config now carries a comment saying why. I did not read Astro’s source for the mechanism, I only confirmed the behavior.
How I checked that nothing else changed
I built main and the branch and compared the output page by page. Both had the same file list, 52 pages, except for one renamed CSS chunk. Visible text, href, src, id, meta and hreflang attributes, JSON-LD and the Shiki code-block markup all matched. The CSS differed only in equivalent lowering, for example clamp() becoming max(min()) and inset becoming explicit sides. Inline-script differences were minifier output.
Then the normal gate: npm run verify, which is the build, 77 end-to-end tests in dev mode, 77 in preview mode and 40 hook tests. All passed.
That diff is the step I would repeat on any framework major. A passing test suite tells you the things you thought to assert still hold. A rendered-output diff tells you what changed, including the things nobody asserted on, and the “Page notfound” bug would have shipped without it.
Rules that came out of it
- Peer-locked packages go in one PR. My
dependabot.ymlstill has no group for this pair, so the next split is possible. - A major bump that auto-merged is a gate bug. I still owe that one a root cause.
- Compare rendered output between the old and new build before merging a framework major.
- Pin the CSS target explicitly when a build tool changes its browser baseline.
- Test configs that start servers should not depend on whether the tool thinks a human is watching.
What I did not measure
Whether any visitor ever saw one of the regressions, since attempt one never published. How much traffic comes from Safari below 16.4. How often Dependabot splits a peer-locked pair across my other repos. The cause of the auto-merge gate failure.
If you have run a framework major through Dependabot, did the split PRs fail the same way for you, and did grouping fix it?