A Pub Workspace Can Make an Unpublished Version Look Perfectly Healthy

I had a federated Flutter plugin whose tests passed, analysis passed, native builds passed, and dependency constraints looked correct.
Then pana ran dart pub downgrade and awarded three packages 50 points out of 160.
The workflow required 120. The previous score had been 140.
Nothing had suddenly become fifty-points bad. The local workspace had been hiding a release that did not exist yet.
A workspace resolves siblings by path
Dart's pub workspace gives all member packages one shared dependency resolution. Each member uses:
resolution: workspace
That is excellent for monorepo development. Change the platform interface, update an iOS implementation, and test the facade without publishing intermediate packages.
It also creates a blind spot (the convenience was real, so the confidence felt earned). A workspace member resolves its sibling from the local checkout. The version constraint can name a release that is absent from pub.dev, or allow an older version that lacks the API being called, while local tests keep using today's source.
The code and the constraint are now answering different questions:
local workspace: Does today's sibling source work?
registry consumer: Can this constraint resolve a published compatible version?
Both questions matter. Only one was being asked locally.
Why downgrade found the problem
pana does more than run the happy-path resolution. Its downgrade analysis tries the lowest versions allowed by the package constraints.
If a platform package calls an API introduced in a local platform_interface 0.1.1 but still allows ^0.1.0, the workspace can use the local 0.1.1 source and pass. Downgrade can select the hosted 0.1.0 release and expose the missing API.
In the recorded warm_alarm failure, the three platform packages failed together. The platform interface passed because it had no sibling dependency beneath it.
That pattern was the clue. One broken platform implementation would fail alone. Three siblings failing at the same dependency level pointed at the shared interface constraint.
A federated plugin is a release graph
The packages could not be published in one simultaneous batch. The registry had to learn about each dependency level before the next one could resolve.
The required order was:
platform_interface
-> android, ios, macos
-> facade
Android, iOS, and macOS could publish independently after the interface existed. The facade had to wait until every platform package version in its constraints was available.
This is not ceremony around tags. It is topological sorting with a package registry in the middle.
One earlier release tried to move the platform packages and facade together. The platform packages passed pana, while the facade failed because the new platform releases were not available from pub.dev yet. The repository split the feature across three pull requests because the dependency graph required three observable publication stages.
Local green needs a registry-shaped check
The fix was not to abandon workspaces. Their shared resolution is useful, and Dart's documentation explicitly describes the consistency benefit.
The fix was to add checks that remove the workspace's local advantage.
Before releasing a federated package level:
- Inspect every sibling constraint against the API actually used.
- Run package analysis and tests inside the workspace.
- Run downgrade or an equivalent registry-backed compatibility check.
- Publish only the current dependency level.
- Confirm that the registry serves that release before moving upward.
The first two checks protect source compatibility. The last three protect release compatibility.
The lesson is narrower than “monorepos are dangerous”
The workspace did exactly what it promised. It provided a shared local resolution for packages developed together. The mistake was treating that promise as evidence about the public registry.
This distinction applies beyond Dart. Any workspace, link, editable install, or path dependency can make unreleased sibling code appear available. The exact commands change, but the question stays the same: did the test resolve what an external consumer can resolve?
If you release a federated Flutter plugin, draw the package graph before creating tags. Then make each registry edge real before testing the next level.
Your local checkout already knows the future. Pub.dev does not.
Get the next post.
If you made it to the end, meet the next post in your inbox or RSS reader.