Cluster and pipeline hardening
A Neos cluster ships with several code-quality checks off, or too lenient, by default. Some shape output a developer already sees day to day — GenerationLogLevels codes decide which messages even show up in a neos generate run. Others live in commands a developer doesn't typically reach for while coding — check-metadata flags, dependencies unused — so they only catch anything once the pipeline is set up to run them on every change.
Either way, the lenient default exists so shipping a new check never breaks every existing cluster at once, not because the check is unimportant. A cluster left running with the defaults accumulates technical debt silently: warnings nobody reads, boundaries nobody enforces, and — since Neos Copilot validates its own work through the same CLI output a developer would read — more noise for it to work through too, at a direct cost in time, quality, and tokens. This article lists what to turn on, how to disable a specific occurrence without weakening the check for everyone else, and how to bring an already-noisy cluster back to clean.
Cluster configuration
Settings that live in the cluster itself, independent of how CI invokes it.
GenerationLogLevels: raise what's off or too quiet by default
GenerationLogLevels:
M0004: Warning # UnconfiguredRoute
M0063: Warning # UICSharpWarning
M0077: Warning # UIViewPropertySortableRequiresFilterableEntityViewProperty
M0004 and M0063 default to None (fully silent); M0077 defaults to Information (easy to miss in a large log). Raising all three to Warning turns them into real, visible findings. See GenerationLogLevels for every code and its default.
Module-boundary drift: resolve it, don't carry a permanent exemption
By default, check-metadata enforces module boundaries — error N102 for an undeclared cross-module reference, N108 for an Internal element accessed without a grant (see Module accessibility). A cluster configuration carrying YamlIgnoredErrors: [102] (or 108) has turned this off entirely. Resolve the underlying coupling instead — declaring a missing dependency, relocating a misplaced element, or rebalancing which module a shared reference belongs to; see the patterns in Analyzers — then remove the entry. See Migrating an unclean cluster below if the cluster isn't there yet.
Pipeline actions
In addition to the cluster configuration above, the CI pipeline needs to actually enforce it. Recommended order:
neos restoreneos check-metadata -cc -cls --format --warnaserror— the command's own default is already strict on some axes (--warnaserrordefaults totrue) but not others (-cc/-clsdefault tofalseoutside single-file mode); pin every flag explicitly rather than relying on implicit defaults.-imd/--ignore-module-dependenciesis a different, legitimate lever: it scopes the check away from modules undermodules/dependencies— modules you consume but don't own — so it's a reasonable permanent setting when you don't want formatting or other findings reported against code you can't fix. It doesn't affect module-boundary enforcement (N102/N108) on the modules you do own.neos generate --rebuild --warnaserror—--warnaserrordefaults tofalseat the CLI level; without it, generation warnings just print and the command still exits0.- Build and test the cluster server.
- Sonar analyze/publish, followed by an explicit quality-gate wait step that fails the build. Publishing to Sonar alone doesn't fail anything when the gate fails — that needs its own step, or a failing gate only ever shows up as a dashboard color, days later.
- Client install, build, and
npm run typecheck. neos dependencies unused --fail-on-found, run last so every other step still gets a chance to report first.neos dependencies unusedalone only reports; nothing fails without the flag. Unused metadata accumulates silently as a cluster evolves — a screen gets removed but its images don't, a feature is reworked but the old string resources stay. Some findings will be false positives (usage the tool can't trace: an external API, a runtime-only reference, a UI package resolved outside the metadata graph) — mark those explicitly rather than leaving the gate permanently red or turning it off; see the CLI reference for the full command group.
Disabling a diagnostic case by case, not globally
Nothing here justifies accepting a warning or error as-is: every one of these checks flags a real problem, never a false positive to shrug off by default. When a specific occurrence is genuinely a false positive, or a real problem you've deliberately chosen to accept for now, disable that occurrence — never the check as a whole. How you do that depends on which kind of diagnostic it is:
- Roslyn diagnostics (
NEOS000x) support per-occurrence suppression:[SuppressMessage(...)]on a member in server code, or#pragma warning disable/restorearound a span in either server or transpiled UI code — always with a real justification and a linked work item. See Analyzers for the full guidance. neos generatemessages (likeM0004) support a genuine per-element override, with a required justification: inNeos Studio, open the properties of the module containing the element and use theGeneration message level configurationtab to set the level for that message code and element. See Generation message level configuration.check-metadataerrors (likeN102,N108) have no per-occurrence mechanism.YamlIgnoredErrorsonly takes an error number, and applies to the whole cluster — there is no way to ignore a single instance ofN102while still enforcing it everywhere else. Keep this limitation in mind when deciding how to approach an unclean cluster (see below): the error either gets fixed, or the whole cluster goes without that check until it does.
Migrating an unclean cluster
Turning these checks on for the first time on an established cluster will surface violations that were always there — that's the check doing its job, not a sign anything just broke. It doesn't have to happen in one sitting, and it doesn't have to block anyone else's work while it's in progress. The plan is always the same shape: flip the setting today, then work down a visible, shrinking backlog — never wait for the backlog to clear before turning the check on, and never leave it off "for now."
Roslyn diagnostics and neos generate messages make this easy, since both support suppressing one occurrence at a time:
Flip the setting today —
GenerationLogLevels: Warning,generate --warnaserror, whichever applies.Suppress each pre-existing violation with a real justification, and open a work item for the actual fix. For a Roslyn diagnostic:
#pragma warning disable NEOS0001 // Genuinely circular with Pricing - work item #12345, tracked for removal. var discount = Pricing.Application.DiscountService.GetActiveDiscount(); #pragma warning restore NEOS0001For a
neos generatemessage, theGeneration message level configurationtab inNeos Studio(see above) persists to the module's own metadata, one entry per element:# MessageLevelConfigurations/Invoicing.yml - ElementIdentifier: DataTable.InvoicingStagingTable MessageCode: 85 Justification: Staging table used internally by the invoicing batch job, intentionally not referenced by any entity - work item #12345, tracked for removal. MessageLevel: NoneChip away at the backlog whenever there's time. Each fix removes its suppression — the pipeline is green from day one, and the shrinking suppression list is your visible progress.
check-metadata errors need one extra step first, since there's no way to suppress a single N102/N108 occurrence — the whole cluster goes without the check until every occurrence is fixed:
Keep the existing
YamlIgnoredErrorsentry for now, so the pipeline doesn't go red on the whole backlog at once — but say why, and by when:# Temporary, tracked by work item #12345 - remove once every N102 violation is fixed. YamlIgnoredErrors: [102]Run
neos check-metadata --fix --formatand commit the result before triaging anything by hand.--fixrewrites and cleans upYAMLfiles, and--format(with--fix) reformats the embedded XML templates and embedded C# code inside them to their canonical form — a real chunk of a typical backlog is exactly this kind of formatting drift, and this step clears it in one pass, for free.Run
neos check-metadataagain to get the real, remaining list, and turn it into one work item per problem.Work through them in batches.
Once the list is empty, remove
YamlIgnoredErrorsin the same change that closes the last work item — make that removal its own tracked goal from day one, so it's the thing that actually happens, not just the plan.
A UI template referencing an Internal element without a grant has no suppression mechanism at all — unlike every other check above, it isn't a neos generate message with a code (so GenerationLogLevels and the per-element override both have nothing to target), and it isn't a check-metadata error either (so YamlIgnoredErrors doesn't apply). See UI template references. If turning on generate --warnaserror surfaces one of these, the only path is to fix it immediately: correct the reference, or grant InternalAccessGrantedTo on the owning module. In practice this is rare compared to the other checks above, since it only fires on an already-Internal element being referenced from a template — fix these first, before flipping the setting on everything else.
dependencies unused findings only ever need one of two outcomes, not a suppress-then-fix-later cycle — but on an established cluster with years of accumulated debt, that list can still be long. False positives are rare for StringResource, and for UIView, Image, Report, and Theme they almost always come down to the same root cause: a hard-coded string, or a name built dynamically at runtime, instead of the generated typed ID — exactly the kind of reference the tool can't reliably trace back to its target. Always prefer fixing the code to make the dependency detectable over marking the finding as a false positive:
Run
neos dependencies unusedto get the current, full list.For each finding, decide which it is:
Genuinely dead — delete it. This is the fix; there's nothing to track afterward. For a
StringResource, this is low-risk even at volume, since false positives are rare there in the first place: deleting one still actually referenced almost always fails generation, so a mistaken deletion surfaces immediately rather than as a silent runtime break. ForUIView,Image,Report, andTheme, that guarantee only holds for a traceable reference (a typed ID, a declared metadata association) — a hard-coded string the tool couldn't trace in the first place won't fail generation either, and only breaks at runtime; when in doubt, search the codebase for the element's name as plain text before deleting one of these.Actually used, but through an untraceable reference — replace the hard-coded string or
nameof()with the corresponding typed ID (see the link above); the element then stops appearing in the list on its own, with no manual marking to maintain.Genuinely unreachable from anywhere in the cluster's own metadata or C# — an external API consumer, a runtime-only reference, a UI package resolved outside the metadata graph. Only here, as a last resort, mark it with the element's
UsedByproperty, so the tool honors it as used from then on:UsedBy: - External API consumer (Contoso integration) - see work item #12345
Turn on
--fail-on-foundonce the list is empty.
Either way, a suppression or an ignored error code is a promise to fix something, not a decision to stop caring about it — give it an owner and a work item, and let the shrinking list speak for itself.
See also
- Analyzers — the
NEOS000xdiagnostics, how to resolve or suppress one. - Module accessibility — the
N108/Internal-element check. - Metadata check — the full
check-metadatacommand reference. - Generation message level configuration — every
GenerationLogLevelscode, its default, and the per-element override.