After finishing the Jira Clone upgrade to Angular 20, I opened Angular Spotify and saw it was still on Angular 17. Same problem, different repo.
No Angular upgrade happens in this post. Two things came up during planning that are worth writing about. Claude gave confident answers that turned out to be wrong once I pushed back:
If you are using AI for a migration (which is very likely a yes), those two sections are the main point to watch out for.
Angular Spotify has drifted more than Jira Clone had. Current state:
| Package | Current | Notes |
|---|---|---|
@angular/core |
17.3.2 |
Target: 20.x |
@nx/* |
18.2.0 |
Nx already ahead of Angular, fine |
@ngrx/* |
17.0.1 |
Tracks Angular majors |
ng-zorro-antd |
15.1.0 |
Peer deps @angular/core@^15.0.1; highest-risk upgrade |
rxjs |
6.6.6 |
Last release 6.6.7 in 2021; see 3.3 |
zone.js |
0.14.4 |
Fine for 17/18 |
@sentry/angular |
7.49.0 |
Plus @sentry/[email protected], deprecated |
jest |
29.4.3 |
Plan to migrate to Vitest at Angular 20 |
eslint |
8.57.0 |
Flat config + ESLint 9 at Angular 20 |
cypress |
7.6.0 |
Listed as a dep, but no e2e tests exist |
tailwindcss |
3.3.2 |
Stay on v3 (lesson from Jira Clone Part 7) |
On the surface it looks fine: Angular 17, Nx 18, NgRx 17. The issue is [email protected] where the peer deps only support @angular/core@^15.0.1 and we have been running Angular 17. Yarn tolerated the mismatch silently. If we were using npm, we would not have gone this far 😂
[email protected] also looked like a blocker. Claude said it was, it is not hehe. More on that in 3.3.
Going straight to Angular 20 means upgrading seven or eight packages across three Angular majors in one PR which is too risky. I am doing it as a series instead: 17 → 18 → 19 → 20, one major per PR, one post per step.
Before the Angular upgrade, there are four prep PRs to land first. That is what this post is about.
Same opening move as Part 7 of Jira Clone. I started with the plan, not the code. First prompt was roughly: Plan Angular 20 migration. I also want to write a blog post. Read my existing post to match the style.
It was back-and-forth, not a one-shot plan. Claude asked questions, I picked options. A few key decisions:
Then Claude audited the repo and gave the assessment: which packages block the Angular upgrade, and which are just debt to clean up along the way.
Before touching Angular, figure out which other packages are tied to the Angular version and which are not. For example, ng-zorro-antd is tied — it only supports Angular 15. rxjs is not — it works across Angular 17 through 20.
Four packages kept coming up: @sentry/angular, ng-zorro-antd, rxjs, and cypress. For each one, the question was: does this block the Angular upgrade, or is it just debt I want to clean up anyway? One command answers that: npm view <pkg>@<version> peerDependencies.
peerDependenciesis how a package tells you which version of its dependencies it expects you to have. For example,ng-zorro-antd@15expects@angular/core@^15.0.1— meaning it will might work correctly if you run it with Angular 17 or 18.
After two corrections (more on those below), this is Claude’s final classification:
| # | PR | Status | What peer deps say |
|---|---|---|---|
| 2.1 | Sentry: drop @sentry/tracing, bump @sentry/angular to v8 |
Blocker | @sentry/[email protected] caps @angular/core at <= 15.x |
| 2.2 | ng-zorro 15 → 17 | Blocker | ng-zorro-antd@15 caps @angular/core at ^15.0.1 |
| 2.3 | RxJS 6 → 7 | Non-blocker | No peer-dep conflict, see 3.3 |
| 2.4 | Cypress removal | Non-blocker | No peer-dep conflict |
$ npm view @sentry/[email protected] peerDependencies
{
rxjs: '^6.5.5 || ^7.x',
'@angular/core': '>= 10.x <= 15.x', # caps at Angular 15
'@angular/common': '>= 10.x <= 15.x',
'@angular/router': '>= 10.x <= 15.x'
}@sentry/[email protected] caps Angular at 15.x. We have been on Angular 17 for months because yarn allowed the mismatch.
There is also a second issue: @sentry/[email protected] has no peer deps so nothing warned, but it pulls @sentry/*@6.8 internals while @sentry/[email protected] pulls @sentry/*@7.49. We have been shipping two different Sentry versions in one bundle without knowing it.
The Sentry prep PR looked small on paper. Two files: main.ts for the v8 init API, and web-shell.module.ts. I did not expect was a new block in web-shell.module.ts:
{ provide: Sentry.TraceService, deps: [Router] },
{
provide: APP_INITIALIZER,
useFactory: () => (): void => undefined,
deps: [Sentry.TraceService],
multi: true
}I asked Claude why the block was there. The answer was confident and came with a URL:
It is the pattern Sentry’s v8 Angular migration guide recommends. See here: https://docs.sentry.io/platforms/javascript/guides/angular/migration/v7-to-v8/
Sounded ok, then I opened the link. There is nothing on that page about TraceService or APP_INITIALIZER.
So I pushed back: “I do not see Sentry.TraceService mentioned on that page, are you sure?” Claude then ran curl against the actual Sentry docs, grepped for the terms, and came back with the correction.
There is actually a mention of TraceService and the APP_INITIALIZER pattern in the Sentry docs, but it is in the README at sentry-javascript/8.55.1/angular/README.md#tracing
What I want to remember from this one.
$ npm view [email protected] peerDependencies
{
'@angular/core': '^15.0.1', # caps at Angular 15
'@angular/forms': '^15.0.1',
'@angular/common': '^15.0.1',
'@angular/router': '^15.0.1',
'@angular/animations': '^15.0.1',
'@angular/platform-browser': '^15.0.1'
}Same as Sentry, but worse. ng-zorro-antd@15 pins Angular to ^15.0.1 and we are on Angular 17. It has been running on an unsupported Angular version the whole time.
This is the highest-risk PR. ng-zorro touches around several files: player controls, lyrics toggle, modals, toasts. Bumping 15 → 17 means two majors of breaking changes at once: theme tokens, form control API, modal service signatures, and the usual nz-select template quirks.
Why 17 and not 18? To keep the PR manageable. One ng-zorro major per Angular step: 15 → 17 now, then 17 → 18, 18 → 19, 19 → 20 alongside each Angular upgrade.
Claude’s first take was confident:
PR A, RxJS 6 → 7. Angular 18 requires it; doing it on 17 isolates RxJS breakage from Angular breakage.
I almost accepted that. But something feel wrong, and to find out I asked for the actual npm view output. That is when it fell apart:
$ npm view @angular/[email protected] peerDependencies
{ rxjs: '^6.5.3 || ^7.4.0', 'zone.js': '~0.14.0' }
$ npm view @angular/[email protected] peerDependencies
{ rxjs: '^6.5.3 || ^7.4.0', 'zone.js': '~0.14.10' }
$ npm view @angular/[email protected] peerDependencies
{ rxjs: '^6.5.3 || ^7.4.0', 'zone.js': '~0.15.0' }
$ npm view @angular/[email protected] peerDependencies
{ rxjs: '^6.5.3 || ^7.4.0', 'zone.js': '~0.15.0' }Angular 17 through 20 all accept rxjs@^6.5.3 || ^7.4.0. [email protected] is within that range. Claude made up the Angular 18 requirement.
RxJS 6 → 7 is not a blocker. Worth doing anyway since RxJS 6 has not had a release since 2021, .toPromise() is deprecated, and third-party Angular libs are dropping v6 support. But it does not gate the Angular upgrade.
I also grepped apps/ and libs/ for .toPromise( and got zero hits. This PR ends up being just a version upgrade.
What I want to remember from this one. Claude sounded sure. The confidence was not evidence. npm view took ten seconds and settled the question. Without pushing back, I would have shipped this PR based on a made-up requirement.
$ npm view [email protected] peerDependencies
(none)Cypress has no peer deps. No Angular conflict.
But I checked what the repo actually has:
apps/*-e2e foldercypress.config.ts or cypress.json outside node_modules/*.cy.ts spec files anywhere in apps/ or libs/cypress is in package.json, downloaded on every yarn install, and nothing uses itThis is not an upgrade. It is a removal. The plan was updated.
Two blockers, two non-blockers, nothing depends on anything else. Good fit for running all four in parallel.
The approach:
main first. Every subagent’s worktree starts from the same docs.package.json and yarn.lock conflicts are expected, so each PR rebases onto main after the previous one lands.The briefs are at docs/plans/prep/2.1-sentry-v8.md through 2.4-cypress-remove.md. Each one is self-contained with the scope, current state, exact commands, and a validation checklist. A subagent can start cold without reading anything else.
In the Jira Clone series I ran everything sequentially in one Claude Code session. Here, four sessions run on four branches at the same time.
docs/plans/2026-04-18-angular-20-migration-plan.mddocs/plans/prep/Both cases had the same pattern: a confident answer, close to right but not quite, with a plausible-looking source. Both only surfaced because I pushed back.
Three things I am keeping from this:
npm view <pkg>@<version> peerDependencies yourself occasionally. Do not accept “requires X” without verifying.This is not an argument against using Claude Code for migrations. Both mistakes were caught in the back-and-forth. The problem is not that the AI makes things up. It is that you accept them without checking.
Pushing back matters more as AI gets better at sounding confident. I wish AI would push back more often 😂