Fallout-build / Fallout-build/Fallout
[RFC #5] Conflict resolution semantics between plugins
- Dominant language
- C#
- Stars
- 154
- Forks
- 19
- Avg merge
- 1d 22h
- Merged PRs (30d)
- 15
Description
**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 Fallout.Plugin.GitHubActions
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`:
```xml
```
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
1. Ship `dotnet fallout plugins conflicts` to preview conflicts for the current `PackageReference` set without a build? Default: yes. Disagree?
2. Precedence escape hatch in `_build.csproj` or a separate `fallout-plugins.json`? Default: csproj — keeps build config in one place. Disagree?
3. Anything in the "broadcast" column that should be single-winner, or vice versa? It's a judgment call.
## Out of scope (other RFCs)
- Plugin contract — RFC #97.
- Extension points — RFC #98.
- SDK versioning — RFC #99.
- Discovery and load — RFC #100.
## 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:
- #149 — [SDK-4] Conflict resolution runtime — throw-by-default
Contributor guide
Assessment
This issue has not been assessed yet.