Skip to main content

Conventions

Three groups: conventions to respect, things never to do, and the tool-wrapper recipe.

Conventions worth respecting

  • Centralized package versions — add new packages to Directory.Packages.props, never inline. Adding a meaningful library (not a tiny transitive helper)? Add a row to docs/dependencies.md in the same PR — reviewers will ask.
  • Test naming follows AV1600 behavior-focused style. Write test names as short, present-tense descriptions of observable behavior, not method calls. Example good: Missing_changelog_does_not_add_a_full_changelog_link_to_release_notes. Example bad: GetNuGetReleaseNotes_WithMissingChangelog_AndGitHubRepository_DoesNotThrow. Focus on "what happens" not "what method is called" (see AV1600).
  • Test files and classes use Specs suffix. Name test projects Foo.Specs, test files FooSpecs.cs, and test classes FooSpecs. Use Specs not Test or Tests — it clarifies that the class describes the expected behavior (specification) of the subject under test.
  • Tool wrappers: copy/paste from neighbours; cover full commands; use <c>, <a>, <ul>/<ol>, <em>, <para/> in help; don't write secret: false or default: xxx. See Tool wrapper recipe below.
  • Tests next to code, separate folder: every Foo project under src/ has a sibling Foo.Tests project under tests/. Mirror the namespace.
  • No IDE-specific style files committed. .editorconfig and *.DotSettings were removed during the takeover — relying on dotnet format defaults and review.
  • Telemetry opt-out is set in test runs (FALLOUT_TELEMETRY_OPTOUT=true). Keep it that way.
  • No per-file license headers. The MIT notice lives in LICENSE at the repo root, and NuGet packages declare MIT via PackageLicenseExpression. Per-file headers were stripped in v11 (one source of truth + the header URL would have rotted on the repo-org transfer). Vendored third-party code keeps its own copyright headers — don't touch those (e.g. files under src/Persistence/Fallout.Persistence.Solution/ retain Microsoft's MIT notice).
  • [Experimental] for opt-in unstable public APIs. Not-yet-stable public surface is marked with [Experimental("FALLOUT0xx")] rather than held back or shipped silently. See the [Experimental] convention below and the diagnostic-ID registry.
  • [Obsolete] with a DiagnosticId for deprecations. Deprecated public surface carries [Obsolete(..., DiagnosticId = "FALLOUTOBS0xx")] so TreatWarningsAsErrors consumers can suppress a single deprecation. See the [Obsolete] convention below and the diagnostic-ID registry.

Writing style for issues, PRs, and commits

Applies to AI tools and humans. Many readers are non-native English speakers — keep it readable.

  • Be short and precise. Lead with the point.
  • Prefer bullet points over paragraphs.
  • Use plain, simple English. Short sentences, common words.
  • Cut filler: no preamble, no hedging, no AI-flavored padding.
  • Say what changed and why. Drop the rest.

Covers GitHub issues, PR titles/bodies/comments, and commit messages.

[Experimental] for opt-in unstable APIs

Per ADR-0004 §5 (channel ladder amended by ADR-0008), public APIs that aren't ready to commit to a stability guarantee are marked with System.Diagnostics.CodeAnalysis.ExperimentalAttribute instead of being held back or shipped silently. The attribute ships in the .NET 8+ BCL — no package reference needed (the repo targets .NET 10).

using System.Diagnostics.CodeAnalysis;

[Experimental("FALLOUT001")]
public sealed class NewPluginHost
{
// ...
}

Rules:

  • Diagnostic-ID scheme: FALLOUT0xx. Each experimental surface gets its own ID (e.g. FALLOUT001), allocated sequentially and never reused — a retired ID stays retired. Register every allocation in the diagnostic-ID registry in the same PR that introduces it.
  • Consumers must explicitly opt in. ExperimentalAttribute is an error-by-default diagnostic: code that touches the API fails to compile until the consumer suppresses the exact ID — #pragma warning disable FALLOUT001 around the call site, or <NoWarn>$(NoWarn);FALLOUT001</NoWarn> in their project. Opting into instability is therefore a conscious, per-API choice — which is right for a framework (a product devs build on), not an app.
  • Promoting to stable = removing the attribute. Because the feature already rode the main test lane, deleting the [Experimental] line is the whole promotion — no special cross-branch dance. This is what lets stabilised work feed into the production line without a divergent fork. Adding or removing [Experimental] is not a breaking change.
  • Channel discipline differs. On the main (preview) test lane, churn is expected and the attribute is a courtesy. On a release/YYYY production line, any risky-but-shipped public surface must wear [Experimental] — that contract is what keeps the stable line trustworthy while still carrying new work. With the experimental branch retired (ADR-0008), [Experimental] is now the primary mechanism for isolating unstable surface on main — including breaking changes batched toward the yearly major.
  • Don't apply it speculatively. Because the diagnostic is error-by-default, marking an API that's already used internally breaks the build everywhere it's referenced. Only add [Experimental] to a genuinely not-yet-stable API, and suppress every internal usage in the same change so the build stays green.

[Obsolete] for deprecating public APIs

When a public API is on its way out, mark it with System.ObsoleteAttribute and give it a DiagnosticId. This is the sanctioned deprecation path under AGENTS.md rule #2 — keep the old surface working (usually bridging to the replacement) while steering consumers to the new one. DiagnosticId/UrlFormat ship in the .NET 5+ BCL — no package reference needed (the repo targets .NET 10).

using System;

[Obsolete(
"Use [GitHubActionsInputAttribute] instead. Removed in 2027.x.x.",
DiagnosticId = "FALLOUTOBS001",
UrlFormat = "https://github.com/Fallout-build/Fallout/blob/main/docs/obsolete_apis.md")]
public string[] OnWorkflowDispatchOptionalInputs { get; set; } = new string[0];

Rules:

  • Adding [Obsolete] is not a breaking change. It's warning-level by default, so existing code keeps compiling — this is why it's preferred over a hard break. The removal is the break, and it's batched to the next yearly major. State the removal target in the message (e.g. Removed in 2027.x.x.).
  • Always set a DiagnosticId. Without one the compiler reports the generic CS0618, so a TreatWarningsAsErrors consumer can only fix every usage at once or blanket-NoWarn all deprecations. A per-deprecation FALLOUTOBS0xx ID lets them suppress just this one while they migrate.
  • Diagnostic-ID scheme: FALLOUTOBS0xx. Allocated sequentially and never reused, from a sequence separate from the FALLOUT0xx used by [Experimental]. Register every allocation in the diagnostic-ID registry in the same PR that introduces the attribute.
  • Keep the deprecated surface functional. Prefer bridging the old member to the new one (e.g. fold legacy arrays into the typed replacement) over leaving it inert, and suppress the internal bridge usage with #pragma warning disable scoped to the exact ID.

CI pipeline & triggers

Shaped by milestone #18 and the ADR-0004 ladder (amended by ADR-0008, which collapsed experimental into main). Invariants:

  • Feature branches run zero CI until a PR is opened. Push triggers list only long-lived branches; nothing fires on feature/*, bugfix/*, etc. until they're PR'd against main/release/*/support/*. Do not add a working-branch pattern to any OnPush*/branches: trigger.
  • The Linux PR gate (ubuntu-latest) is the only required check — runs on PRs to the long-lived branches.
  • main (push) → -preview to GitHub Packages (preview.yml) — the sole continuous publisher now that experimental.yml is deleted.
  • Cross-platform windows/macos are gated to release intent — PR-to-release/*/support/* or a v* tag push only. They do not run on main pushes. ("On main we've got our edge.")
  • concurrency: cancel-in-progress on every build workflow except release.yml — never cancel a publish mid-flight.
  • Canonical CI-ignore paths: docs/**, .assets/**, **/*.md — applied to every PR/push trigger.
  • The ubuntu-latest / windows-latest / macos-latest workflows are generated from build/Build.CI.GitHubActions.cs — edit the attributes + constants there and regenerate (./build.sh), never hand-edit the .yml. preview.yml / release.yml are hand-written.
  • Every publishing lane runs Test before it publishes (#324). preview.yml and release.yml both run a single dotnet fallout Test Pack invocation — NUKE executes it as discrete internal stages (Restore → Compile → Test → Pack) and fails at the breaking stage, so a test failure stops the job before the push step. Don't split a lane into separate dotnet fallout Compile/Test/Pack steps — each invocation re-runs the dependency graph (double-compile); the single invocation is the staged build.
  • Caching (#328): every workflow caches ~/.nuget/packages + .fallout/temp, keyed on global.json + **/*.csproj + Directory.Packages.props (the dependency-affecting set), with a restore-keys: prefix fallback for partial restores. There is no packages.lock.json to add to the key, and build outputs (bin/obj) are deliberately not cached (stale-artifact correctness risk).

What not to do

  • Don't reintroduce source/ — production code lives under src/, tests under tests/. Same for images/ (now .assets/).
  • Don't add main (or any working-branch pattern) to the push triggers of the cross-platform workflows — they're release-intent-gated on purpose (milestone #18 / #318 / #326).
  • Don't add submodules: recursive to checkouts — there are no submodules (no .gitmodules); it's a dead init step.
  • Don't add secret/default defaults to tool JSON files (see CONTRIBUTING.md).
  • Don't introduce a new test framework or assertion library — stay on xUnit + FluentAssertions + Verify.
  • Don't commit output/ or any bin//obj/ directory.
  • Don't commit fallout-global.sln or other fallout-global.* files — they're generated by GenerateGlobalSolution.
  • Don't bypass Directory.Packages.props or Directory.Build.targets.
  • Don't reintroduce .editorconfig or *.DotSettings without a maintainer-level decision — they were intentionally removed.

Tool wrapper recipe

When asked to add or extend a tool wrapper:

  1. Find the closest existing tool under src/Fallout.Common/Tools/<Tool>/<Tool>.json.
  2. Copy its shape; cover a full command with all arguments.
  3. Run ./build.ps1 GenerateTools to verify it generates cleanly.
  4. Do not commit the generated .cs output.