RFC #5 — Conflict resolution semantics between plugins
Self-RFC. Pins what happens when two plugins step on each other.
Context
In a healthy ecosystem, two plugins eventually do conflicting things — register an IHost for the same CI environment, add middleware at the same (phase, priority) slot, contribute a tool wrapper with the same name. The host needs one consistent answer per conflict shape. The worst answer is silent winner-takes-all — it produces "why doesn't my plugin work" tickets with no useful error.
Proposed default: throw with a clear message
For every extension point, the default on conflict is to throw FalloutPluginConflictException at startup, naming both plugins and the colliding resource, with exact resolution steps.
Example:
Plugin conflict: two plugins registered IHost for environment 'GitHubActions':
- Fallout.Plugin.GitHubActions (1.2.0, from your PackageReference)
- Fallout.Plugin.CICustom (0.4.1, from your PackageReference)
Resolve by:
(a) Removing one of the PackageReferences from _build.csproj, or
(b) In _build.csproj, set <FalloutPluginPrecedence>Fallout.Plugin.GitHubActions</FalloutPluginPrecedence>
to declare which plugin wins, or
(c) File a bug against one of the plugins if they should coexist.
Per-extension-point semantics
Not every extension point conflicts the same way:
| Extension point |
Conflict semantics |
IBuildMiddleware |
No conflict. Multiple middleware at a phase coexist, run in priority order. Same priority → alphabetical-by-plugin-id breaks the tie. |
IHost + IHostDetector |
Conflict if two detectors match the current env. Throws. |
IParameterSource |
No conflict. Sources run in order; first non-null wins. Order via [Order]. |
IToolWrapperContribution |
Conflict if two plugins contribute the same *Tasks type name. Throws. |
ITargetLifecycleListener |
No conflict. All listeners run on every event. |
IOutputSink |
No conflict. All sinks receive every event. |
Pattern: "broadcast" points (IBuildMiddleware, ITargetLifecycleListener, IOutputSink, IParameterSource) can't conflict by design. "Single-winner" points (IHost, IToolWrapperContribution) throw.
User-facing precedence escape hatch
For the throwing cases, declare precedence in _build.csproj:
<ItemGroup>
<FalloutPluginPrecedence Include="Fallout.Plugin.GitHubActions" />
</ItemGroup>
Deliberate escape — for when two plugins should both be installed but one wins on conflict. Not for accidental conflicts; resolve those by removing the unused plugin.
Why not "highest version" or "newest install" wins?
- Highest version: version numbers carry no cross-plugin meaning. A 2.0 may predate another's 1.0.
- Newest install: depends on
PackageReference order, unstable across restores.
- First alphabetically: arbitrary; no useful signal.
Throwing is the only behaviour where the user is guaranteed to know there's a conflict.
Questions for discussion
- Ship
dotnet fallout plugins conflicts to preview conflicts for the current PackageReference set without a build? Default: yes. Disagree?
- Precedence escape hatch in
_build.csproj or a separate fallout-plugins.json? Default: csproj — keeps build config in one place. Disagree?
- Anything in the "broadcast" column that should be single-winner, or vice versa? It's a judgment call.
Out of scope (other RFCs)
How to engage
Comment if you've maintained a plugin ecosystem that got conflict resolution wrong (Jenkins, VS extensions, Cake addins, MSBuild SDKs). Lessons from those are the most useful input.
Decision lands in
When this RFC locks (2026-08-31), its shape becomes the spec for:
RFC #5 — Conflict resolution semantics between plugins
Self-RFC. Pins what happens when two plugins step on each other.
Context
In a healthy ecosystem, two plugins eventually do conflicting things — register an
IHostfor the same CI environment, add middleware at the same(phase, priority)slot, contribute a tool wrapper with the same name. The host needs one consistent answer per conflict shape. The worst answer is silent winner-takes-all — it produces "why doesn't my plugin work" tickets with no useful error.Proposed default: throw with a clear message
For every extension point, the default on conflict is to throw
FalloutPluginConflictExceptionat startup, naming both plugins and the colliding resource, with exact resolution steps.Example:
Per-extension-point semantics
Not every extension point conflicts the same way:
IBuildMiddlewareIHost+IHostDetectorIParameterSource[Order].IToolWrapperContribution*Taskstype name. Throws.ITargetLifecycleListenerIOutputSinkPattern: "broadcast" points (
IBuildMiddleware,ITargetLifecycleListener,IOutputSink,IParameterSource) can't conflict by design. "Single-winner" points (IHost,IToolWrapperContribution) throw.User-facing precedence escape hatch
For the throwing cases, declare precedence in
_build.csproj:Deliberate escape — for when two plugins should both be installed but one wins on conflict. Not for accidental conflicts; resolve those by removing the unused plugin.
Why not "highest version" or "newest install" wins?
PackageReferenceorder, unstable across restores.Throwing is the only behaviour where the user is guaranteed to know there's a conflict.
Questions for discussion
dotnet fallout plugins conflictsto preview conflicts for the currentPackageReferenceset without a build? Default: yes. Disagree?_build.csprojor a separatefallout-plugins.json? Default: csproj — keeps build config in one place. Disagree?Out of scope (other RFCs)
How to engage
Comment if you've maintained a plugin ecosystem that got conflict resolution wrong (Jenkins, VS extensions, Cake addins, MSBuild SDKs). Lessons from those are the most useful input.
Decision lands in
When this RFC locks (2026-08-31), its shape becomes the spec for: