Skip to content

An enterprise upgrade with the team's own libraries, and its video - #13

Merged
hazeliscoding merged 69 commits into
mainfrom
feat/m7-enterprise
Nov 9, 2026
Merged

hazeliscoding merged 69 commits into
mainfrom
feat/m7-enterprise

Conversation

@hazeliscoding

Copy link
Copy Markdown
Owner

Summary

M7 takes an app built like a company's from Angular 21 to 22. It's an Angular 21 portal on pnpm 12, set up for a dev container. Its internal UI libraries live in their own repo, reach it through a private registry, and accept only Angular 21. Claude Code with ng-hop did the upgrade from the Windows host in six turns. It stopped and offered to upgrade the team's libraries first, then published them. It got past a release-age rule inside ng update and upgraded the app. A review of the whole milestone then found real problems, and they're fixed here.

  • The fixture. examples/enterprise/ holds acme-ui, acme-portal and a local Verdaccio, as templates that setup.mjs copies out. Its README walks through the run, and the setup stops on a registry that isn't the example's or that an earlier run already moved to 22.
  • The team's own blockers. When a blocking library's scope has a registry of its own, the plan names that registry and the order to upgrade the libraries in, and names whole groups that need each other. The skill confirms who owns them before offering to upgrade them first. The plan now reads a pnpm workspace's root .npmrc. It didn't before, so a blocked app looked reachable.
  • Node pins. The plan names the repo's Node pins below a major's range, from .nvmrc, .node-version, .tool-versions, a Dockerfile, the dev container, engines and volta (in the app's folder and at the repo's root), and from GitHub workflows. The run missed the dev container's pin, and the container then refused Angular 22:
    Node pins below what Angular 22 needs (^22.22.3 || ^24.15.0 || >=26.0.0), in the repo: .devcontainer/Dockerfile (node:22.12.0-bookworm)
    
  • A Node in a gap. A Node in a gap of Angular's range, such as 24.14 against Angular 22's ^22.22.3 || ^24.15.0 || >=26.0.0, now counts as too old, since ng won't start on it. The plan called it unsupported before, which the skill reads as carry on.
  • Release age. From Angular CLI 22.1, ng update skips a release younger than the package manager's minimum release age, and reads no exemption list. update.mjs --install-first installs such a library first, and the troubleshooting guide gives the cause and both ways on.
  • The video. docs/video/enterprise/ holds the source of a 7:52 tutorial made from the real run with HyperFrames. The video file isn't in the repo.
  • The roadmap. M7 is met. The review's decisions are recorded, and what it left for later is M8.

Checked

  • npm test passes on Windows: 296 tests, 287 pass, 9 skipped (the POSIX-only ones). The plan, update and skill tests also passed on Node 18.20 and 20.20 before the last plan fix.
  • Every behaviour fix from the review had a failing test first, and reverting each fix makes its test fail again.
  • The example's setup, against fake registries: one that serves react, one that wants a login, a ping that answers 500, and nothing listening. It stops before copying anything each time, and it accepts the example's own registry.
  • Real tools: pnpm 12.16.1 and yarn 4.18.1 for --install-first; the Angular CLI's source for the age filter; Docker Desktop and @devcontainers/cli for the example's dev container; Verdaccio 7.0.2 for the setup.
  • The video: hyperframes check reports no findings, and the render is 7:52 at 1080p30, -16 LUFS.
  • Not run: the evals, or the whole walkthrough again after the review's fixes.

…h to upgrade first

A scope with a registry of its own, apart from the default one, is how a company publishes its libraries, so a blocker from one is most likely the team's, and waiting for it would wait on themselves. The plan says so on the blocker, and when several are, it names the order to upgrade them in, the one another needs first. A company-wide mirror set as the default registry says nothing about owners, so it doesn't count.
When the plan says a blocker comes from the person's own registry, the choices gain one: upgrade it first, in its own repository, in the plan's order, and publish once they say so. A new reference covers a library workspace: no app to run, each library's peer ranges and version to move, building in dependency order, and publishing only with their go-ahead.
A dev container, .nvmrc, engines field or CI workflow pinned below what the new Angular needs fails for the team while this machine passes. The skill raises such a pin, keeping its form, and says so under hand fixes, and the report names it. It still never switches this machine's Node.
…and a dev container

Acme's portal is on Angular 21, and its UI comes from Acme's own libraries, published to a private registry, whose peer ranges stop at 21. So the libraries have to move first. The example holds both repositories as templates from Angular CLI 21 and pnpm 12, a Verdaccio config standing in for the company registry, and a setup script that publishes the 21.4.0 libraries and makes each a git repository. The portal's dev container pins Node 22.12, which Angular 21 accepts and 22 doesn't, keeps node_modules in a volume of its own, and reaches the host's registry through a small forwarder.
…ecisions

The enterprise example and the plan's own-registry blockers are done. The decisions record what marks a library as the team's, why the skill raises the repo's own Node pins, and why the example is templates a setup script copies out.
Angular drops the whitespace between elements, so the two ran together.
In the enterprise run, ng update quoted @acme/ui-core 21.4.0's Angular 21 peers right after 22.0.0 was published, because pnpm 12's minimumReleaseAge kept the new release out. The guide now says how to recognise it, that pnpm add exempts a version it's given by name, and that exempting relaxes a safeguard, so it's the person's call.
…ions

Angular CLI 22's update resolver drops versions younger than pnpm's minimum-release-age, 24 hours by default from pnpm 11, or yarn 4's npmMinimalAgeGate, and never reads minimumReleaseAgeExclude. So a just-published internal release fails ng update's peer check for a day even once exempted, as the enterprise run found and update-resolver.js in @angular/cli 22.2.6 confirms. The guide gives both ways on: wait, or install the exempted releases first and update with --allow-dirty, with the person's go-ahead.
In the enterprise run the agent searched JSON and YAML files for Node pins, reported none, and missed the dev container's Dockerfile, which pinned node:22.12.0. The dev container then failed: Angular 22's CLI needs 22.22.3. The plan now reads .nvmrc, .node-version, .tool-versions, engines and volta in package.json, FROM lines in a dev container or root Dockerfile, the dev container's image and node feature, and node-version in GitHub workflows, and names each pin below a major's Node range under that major, or under the current one when the app is already there. A bare major such as 22 floats to its newest release, so it counts as fitting.
…ran into

Claude Code took the libraries and then the portal to Angular 22 in six turns. The roadmap records the two release-age snags and the missed dev container pin, and where the skill now covers each. The dev container, rebuilt afterwards, builds and serves the app on Node 22.23.4.
The narration, scenes and build scripts for the HyperFrames video, which go from the plan to the rebuilt dev container. The voice, the timings and the render are made again from them, so they stay out. The example's README points here, and fonts are marked binary.
… line

The render's text runs a little wider than the snapshots', so @acme/ui-forms broke at its hyphen in the intro, the date split in option 1, and a title bar wrapped.
Verdaccio 7 refuses any Node below 24, so a reader on Node 22 stopped at
step 1, and Angular CLI 22 refuses Node 24 below 24.15 later in the hop.
Setup also runs git init, which the list left out.
…its Node

corepack@latest is 0.36.0, which declares Node 22.12 unsupported, so the
image warned EBADENGINE and changed with each corepack release. 0.34.7 is
the last corepack whose engines include Node 22.12, and it still starts
pnpm 12 from package.json's packageManager.
…lume

A Docker volume name is shared by the whole machine, and setup makes a new
copy each run, so every copy's container installed into the same
acme-portal-node-modules. ${devcontainerId} differs between folders and
stays the same when the container is rebuilt.
…the app's checks in it

A plain devcontainers up reuses a container made before the upgrade, on
its old Node, and reports success once pnpm install is done, before
anything runs Angular. So the README now rebuilds it and runs the build
and tests through exec, with --watch=false so the tests don't wait for
changes in a terminal. VS Code's local window offers Rebuild and Reopen
in Container; Rebuild Container shows only inside the container.
The container reaches the registry through host.docker.internal, which
Docker Desktop leads to the host's 127.0.0.1, where Verdaccio listens.
Docker Engine on Linux has no such name by default, and a 127.0.0.1
listener wouldn't answer the bridge address anyway, so there the
container's install can't fetch the @acme packages.
… release is minutes old

pnpm holds a release back for its first day, so every reader who goes on
straight after publishing the libraries' 22.0.0 meets this stop, and Angular
CLI 22's ng update applies the same rule itself, ignoring the exemption. The
recorded run took two extra turns on it, and step 3 said nothing about it.
…to 22

The registry keeps what each run publishes. Rerun from the same clone after
a walkthrough, setup only checked that 21.4.0 was there, so the new portal's
plan said Angular 22 was reachable and the walkthrough's first step didn't
happen. Setup now stops before copying anything, and both it and the README
say how to start over.
The search for the repo's root walks up from the project to the first .git.
The test project sat in a plain temp folder, so a git repo anywhere above it,
such as a dotfiles repo in the home folder, became the root, and the test's
own workflow stopped being read.
At warn, Verdaccio 7 prints nothing at all once it's listening, so the
terminal step 1 asks the reader to leave running stays blank and looks
stuck. At info it prints its address and "server started", then a line
per request, and only @acme traffic reaches it.
From Angular CLI 22.1, ng update skips a release younger than the package manager's minimum release age, a day by default on pnpm 11 and newer and on yarn 4, and reads no exemption list. So a library the team published that morning fails the peer check. The guide said to install it first and run the plan's update with --allow-dirty, which update.mjs rejects, along with any dirty tree.

update.mjs now takes --install-first <library>. From a clean tree it adds each one in the form package.json gives it now (^, ~ or exact, as ng update writes it), checks that only package.json and the lockfile changed, and then updates the rest with --allow-dirty. An exemption pnpm writes on its own stops it before the update, because that's the person's call. yarn is found the way pnpm is, through its shim and with no shell.

Checked with real pnpm 12.16.1, yarn 4.18.1 and git against a release ten minutes old: the caret and a tilde survive, a devDependency stays one, and strict pnpm fails before anything changes. Typed into cmd.exe, `pnpm add pkg@^2.0.0` saved "2.0.0", since ^ is cmd's escape character, so the script does the add itself.
…t must be named

Ten one-line changes to plan.mjs left every test passing: dropping any of
.nvmrc, .tool-versions, the dev container's image or node feature, engines or
volta, the walk to the repo's root, the pins on an app already there, making
engines always fit, or counting a scope pointed at the default registry as the
team's. The pins test only had values that fit for most of them, and the
mirror half of the own-registry test passed even with nothing to block.

Now one run holds a value below the range in every place the plan reads, an
app already on the target names its pin, an app below the repo root gets the
root's workflow, and a scope set to the default registry blocks without a word
about owners. Each of those changes now fails its test.
A dev container tag such as typescript-node:5.2-24-bookworm starts with the
image's own version, and the plan read 5.2 as the Node, so the pin was below
for every Angular and no Node the agent picked could clear it. The Node is now
the two-digit major after that prefix, and a tag that is only the image's
version, such as 5-trixie, names none. Read against all 1056 typescript-node
tags on MCR, none gives a wrong major.

engines ">=24" passed for Angular 22, whose CLI stops on Node 24.0 to 24.14.
An engines range now fits only when the lowest Node it allows is in Angular's
range.

An engines range nothing meets, such as ">=22 <20", or a number past 2^53 in
.nvmrc threw from semver and ended the plan with a stack trace. A value semver
can't hold now counts as no pin, as one it can't read already did.
devcontainer.json allows comments, and so does a workflow's YAML. A
commented-out image or node-version was listed as a pin to raise, and a
commented-out image above the one in use hid the real one, since only the
first image line is read. Whole-line comments are now dropped first.
…npm and yarn

The section said pnpm won't resolve a release younger than its minimum release age. pnpm 12.16.1 installed one ten minutes old as soon as it was named, and wrote the exemption itself. What skips it is ng update, from Angular CLI 22.1 (22.0.9 has no such filter), which reads no exemption list. The check it gave, `pnpm config get minimumReleaseAge`, prints undefined for the one-day default, so the release's age from npm view is the check now. yarn 4 gates the same day by default and refuses the release itself with YN0016 until .yarnrc.yml preapproves it, and nothing covered it.

The way on was to pass --allow-dirty to an update.mjs that rejects it. Now it's to commit the approved exemption by itself, which a frozen install still accepts, and run the plan's command with --install-first, except for a library that ships migrations, which ng update would skip. A release-age exemption joins the list of things that wait for the person's approval.
…s warning

Passing an args array with shell: true makes Node 24 print DEP0190, a
security deprecation warning, during every Windows setup. Node joins the
arguments with spaces anyway, and setup's pnpm arguments are fixed, with
none from input and none with a space, so they now go as one command line.
git still runs without a shell.
Step 4e installs what ng update didn't move, which on pnpm is a pnpm add of the plan's newest fitting release. When that release is under a day old, pnpm 12.16.1 writes it into minimumReleaseAgeExclude in pnpm-workspace.yaml, creating the file if it has to, and says only "Added 1 entry". That's a safeguard relaxed without the person, so the step now says to ask before committing it, and to undo the whole add on a no, since a frozen install with only the entry removed still installed the release from the lockfile.
…dy there

Already on Angular 22 with a dev container on node:22.12.0, the plan printed "Nothing to upgrade" and then the Node pins below what Angular 22 needs. The skill's already-there path ended with "That's the whole job", and step 4e only knew the per-major label, so a pin an earlier upgrade left behind would stay failing on the team's machines. The path now raises them, in the patch commit or one of their own, and 4e names both labels.
When no library was ready, every one left went in by name, so a library that
only needed two that need each other could come before what it needs, and the
first library's needs were never shown, so a cycle read like a clean order.
An optional peer also counted as a need, which could make a cycle out of a
plain dependency and print the order backwards.

Optional peers no longer count. When nothing is ready, one library whose
every need leads back to it starts, so the ones that only need it come after.
Every library with needs says so, and the note names the libraries that need
each other, to upgrade and publish together.
The release-age section said yarn 4. yarn's own releases say otherwise: 4.9.4 has no npmMinimalAgeGate, 4.10.1 added it with a default of 0, and 4.15.0 made the default a day, which 4.18.1 prints as 1440. Angular's CLI reads whatever yarn prints, so an app on yarn 4.0 to 4.14 isn't held back unless someone set the gate. The section, its pointer and libraries.md now say yarn 4.15 or newer.
… is the team's

A licensed vendor serves its packages from a registry set for its scope, as
Infragistics does for @Infragistics, the same way a team serves its own. The
plan called any such registry "your own", stated as fact, so a vendor's
blocker read as the team's library to upgrade first. The blocker now says the
npm config sets a registry for its scope, with the URL, and the note keeps
"If they are your team's" before offering to upgrade first.

With only one library, the note no longer gives an order of one.
…ames

The check that a key from the app's own .npmrc is in npm's listing used `in`,
which also matches names every object has, such as constructor. Object.hasOwn
matches only what npm listed.
javascript-node has no 5.2.5-20-bookworm tag on MCR. 4.0.10-20-bookworm is
published, and it still starts with the image's own dotted version.
engines "*" was listed as a Node pin below the range, since its lowest
Node is 0.0.0, so the agent was told to raise a value that keeps the team
on no Node in particular and can't be raised and keep its form. semver
also makes "*" of ">=0" and "x". A range with an upper bound, such as
"<20", still starts at 0.0.0 and is still named.
…erves this app

The plan reads Node pins in the app's folder and at the repo's root, and
now the root's package.json too. In a monorepo the root's Dockerfile,
workflows and engines can belong to another service, and step 4e said to
raise every pin listed, inside the Angular upgrade's commit, where this
app's checks can't catch what the change does there. A pin whose path
starts with ../ is now raised only when it builds, tests or runs this app,
and the report says which were left.
The plan no longer calls a scope's registry "your own", since a licensed
vendor sets one up the same way. It says the registry comes from the npm
config, and offers the order only if the libraries are the team's, so the
walkthrough's first step says the same.
… as the skill does

Step 3 said pnpm holds a release back for its first day. pnpm 12.16.1
installs a release it's asked for by name and writes the exemption itself.
What skips it is ng update, from Angular CLI 22.1, which reads no
exemption list, and that's what the troubleshooting guide and update.mjs
--install-first now work from. The step now gives the same cause and the
same two ways on.
A list written one version a line stopped at the first comment, whether
after an item (- 22.x # current) or on a line between items, since the
comment line was left blank and the value stopped at the #. The versions
after it were never checked. Comment lines now go with their line break,
and a comment after a value goes too.
… it left

The review found that a scope's registry doesn't show who owns a library, that the plan missed a pnpm workspace's root .npmrc, and that the release-age way through needed the runner. M7's account of the run also blamed pnpm for what ng update did.
The video keeps the run as it happened, so the README names the plan wording and the release-age story that have changed since.
Setup pinged localhost:4873 without reading the answer, then published Acme's libraries to whatever registry was there. 4873 is Verdaccio's default port, so that could be a company Verdaccio. Setup now stops before copying anything if the ping fails, or if the registry serves react: the example's has only @acme packages and answers 401 for react.
…pace's root

pnpm 12.16.1 refuses to add to a workspace root whose pnpm-workspace.yaml lists two or more package patterns (ERR_PNPM_ADDING_TO_ROOT) unless -w says the root is meant, so --install-first stopped there before the update. It now passes -w when the app's folder holds a pnpm-workspace.yaml that lists packages, and never below the root, where -w would add to the root instead of the app.
…om the example's

Such a registry answers 401 for react, as the example's does, so it passes the new check, and a real Verdaccio set up that way did. Setup would then publish to it only if npm has a token for it. The comment names the check that would stop it, for when that matters.
…an install

pnpm writes an app's lockfile and exemptions at the workspace root, and git restore . run in the app's folder left those changed, so the next update refused a tree that wasn't clean. git restore :/ covers the whole repository. pnpm makes an untracked pnpm-workspace.yaml only beside package.json when there was none, so that advice stays.
… ng won't start on it

Node 24.14 is in a gap of Angular 22's range, ^22.22.3 || ^24.15.0 || >=26.0.0. It isn't below every version in the range, so the plan said unsupported, which the skill takes as fine to carry on, and Angular 22's ng exits with an error on it. Now only a Node above every version in the range is unsupported, and one in a gap is too old, since a newer Node fits.
…ly two of them

In a ring of three, where a needs b, b needs c and c needs a, the note said only that a and b need each other and go out together, though c has to go with them. It now names each group of libraries that need each other, directly or through others, however many are in it. Pairs, two separate pairs and chains read as before.
…er the repo keeps them

The plan reads a fixed set of files, and the skill searches the repo for the rest, so the old title promised more than the test checks.
…at fits, as its control

It showed a pin below the range gets named on an app already on the target, and a plan that named every pin passed it too. A dev container on node:24-bookworm, inside Angular 22's range, now has to name none.
…npm workspace root

Up to Angular CLI 22.0, ng update fetches every dependency's metadata with its own client, which reads only the app folder's .npmrc and the user's and global ones. Run below a pnpm workspace root whose .npmrc gives the team's scope a registry, CLI 21.2.29 stopped with a 404 from the public registry, while pnpm and the plan found the package. CLI 22.1 and newer ask pnpm instead.
…other together

The plan now names every library in a cycle and says no order works for them, while blocked.md only said to follow the plan's order.
pnpm 12.16.1 ran a dry-run publish on main and on master, and refused one on upgrade/angular-22 with ERR_PNPM_GIT_NOT_CORRECT_BRANCH: Branch is not on 'master|main'.
…ase on its own line

The plan now calls Node 24.14 too old for Angular 22, whose range is ^22.22.3 || ^24.15.0 || >=26.0.0, and CLI 22.2.6 exits on it. A newer 24 such as 24.15 is enough there, so asking only to switch Node sent people looking for a new major.
… too old

The plan now calls a Node in a gap of Angular's range too-old, and Node 23 and 25 sit in gaps of Angular 22's ^22.22.3 || ^24.15.0 || >=26.0.0. CLI 22.2.6's bin/ng.js only warns on those two and runs, and CLI 21.2.29 warns on every odd major, so the skill's reason, that ng will refuse to run, was wrong there. Angular still doesn't support them, so the advice to stop and switch stays.
…, not a comment

Its control matched /need each other/ anywhere in plan.mjs, and two comments there say it too, so the test still passed on a plan.mjs whose note no longer did.
When b needs x, and x and y need each other, the order read x, b, y, though the note says x and y go out together. Each group of libraries that need each other now goes into the order whole.
 packages

Such a registry answers 401 for react, as the example's does, so it got through, and setup would publish to it if npm had a token for it. The example's lets anyone read @acme packages.
…und deferred

A Node in a gap of Angular's range now counts as too old. Node 27 on Angular 22, and ng update's .npmrc lookup below other workspaces, go to M8.
@hazeliscoding
hazeliscoding merged commit 7d8631b into main Nov 9, 2026
14 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant