This harness verifies that @limitless-angular/sanity can be built once,
packed once, and installed into fresh Angular consumer applications for every
supported Angular version set.
The compatibility promise is about the released package shape:
build library -> pack .tgz -> install same .tgz in generated Angular appsIt intentionally does not test the monorepo source package directly. The generated consumers install the packed tarball, so the checks catch missing exports, bad declaration files, local dependency leaks, Angular compiler incompatibilities, and runtime boot failures that a real application would see after installation from npm.
Run the full pipeline:
pnpm run compatRun individual steps:
pnpm run compat:affected --base-ref origin/main
pnpm run compat:affected:test
pnpm turbo run test --filter=@limitless-angular/angular-compat
pnpm run compat:assert
pnpm run compat:matrix
pnpm run compat:matrix --canary
pnpm run compat:pack
pnpm run compat:artifact
pnpm run compat:test
pnpm run compat:test --set angular-20-floor
pnpm run compat:test --set angular-22-latest --skip-runtime
pnpm run compat:canary-report --status-dir .compat/canary-status
pnpm run compat:release-parityThe compatibility tooling is a private workspace package named
@limitless-angular/angular-compat, so Turbo can run its package-level unit
tests with pnpm turbo run test --filter=@limitless-angular/angular-compat.
Root compat:* scripts route lifecycle commands through Turbo and filter to the
compat package. Package-local command names use the same compat:* namespace,
with tools/angular-compat/cli.mjs owning argument parsing.
tools/angular-compat/turbo.json scopes the compat-only Turbo task settings to
this package; the root turbo.json keeps shared monorepo tasks. The matrix
command is the one intentional direct package call because CI captures its
stdout as JSON. Playwright browser installation is also direct package setup,
not a Turbo task. The compatibility behavior lives in reusable functions in the
other .mjs files.
The package unit tests include an orchestration contract test that protects
these routing decisions without running the slow pack or generated-consumer
checks. Run them with
pnpm turbo run test --filter=@limitless-angular/angular-compat.
tools/angular-compat/config.json is the source of truth.
Stable required consumers live in consumerVersionSets:
{
"id": "angular-20-floor",
"angularMajor": 20,
"mode": "floor"
}Supported modes:
floor: latest patch in the first minor for that major, such as>=19.0.0 <19.1.0. This protects consumers on the lowest version promised by the peer range.latest: latest published release for that Angular major.dist-tag: npm dist-tag such asnext. Use this only incanaryVersionSets, because canaries are advisory.
Canary toolchains resolve Angular framework packages from @angular/core and
resolve CLI-owned packages, such as @angular/build and @angular/cli, from
their own npm dist-tags. This keeps the advisory matrix resilient when the
Angular framework and Angular CLI release trains publish prereleases at
different times.
buildAngularMajor is the stable major used to build the library artifact. It
must be the newest stable Angular major represented by consumerVersionSets.
The test matrix then installs the same tarball into every configured consumer.
compat:pack creates a temporary workspace, installs the selected build
toolchain, builds the Angular package, and writes one tarball under
.compat/artifacts.
compat:prepare-publish removes source-only metadata, such as private: true,
from a built package manifest before preview or release publishing. It can also
write an explicit semver version into that manifest.
compat:artifact validates the tarball:
- package name and expected version match
- package is not private
- no scripts or dev dependencies are published
- no
workspace:,file:,link:, orportal:dependency references leak - expected package exports exist
- exported declaration files and FESM bundles exist
- no source
.tsfiles ornode_modulesare included
compat:test creates generated Angular applications and installs the packed
tarball into them. Each generated app:
- imports every configured public entrypoint
- compiles public API type smoke checks
- builds in production mode
- boots in Chromium with Playwright
- verifies Portable Text rendering and Sanity image URL generation
- fails on browser page errors or console errors
CI first runs pnpm turbo run compat:affected --filter=@limitless-angular/angular-compat to decide whether the Sanity
compatibility jobs are relevant. The decision asks Turbo whether either
@limitless-angular/sanity or @limitless-angular/angular-compat has affected
package tasks. A small explicit contract list covers files that are outside
package ownership but still define the compatibility workflow, such as
CI/release workflows, .nvmrc, turbo.json, and the release tooling package.
workflow_dispatch always runs the compatibility jobs.
When eligible, CI runs stable consumers as required jobs and angular-next as
an advisory canary job. Stable consumer failures block the PR. Canary failures
keep the CI run green, emit GitHub warnings, and publish a single managed PR
comment so reviewers can see future-version drift without opening the Actions
log. When all canary rows pass again, CI removes the managed comment. Canary
status artifacts include the exact Angular, Angular CLI, and TypeScript
versions that were tested so the advisory comment stays useful as npm dist-tags
move. The report formatting and sticky-comment behavior live in
tools/angular-compat/canary-report.mjs; the workflow only downloads the
status artifacts and calls that reporter.
To add support for another Angular major:
- Update the Angular peer dependency range in
packages/sanity/package.json. - Add stable version sets to
consumerVersionSetsintools/angular-compat/config.json. - Set
buildAngularMajorto the newest stable major inconsumerVersionSets. - Optionally add or update a canary set in
canaryVersionSets. - Run:
pnpm run compat:assert
pnpm run compat:matrix
pnpm run compat:pack
pnpm run compat:artifact
pnpm run compat:testAdding a new stable major usually means adding both a floor and a latest row.
Replace <major> with the Angular major being added:
{
"id": "angular-<major>-floor",
"angularMajor": <major>,
"mode": "floor"
},
{
"id": "angular-<major>-latest",
"angularMajor": <major>,
"mode": "latest"
}The exact major in the id is not special to the tooling, but keeping it explicit makes CI output easy to scan.