Table of Contents

Contributing

Contributions are very welcome! Whether it is a bug fix, a new feature, improved documentation, or just a question — please feel free to open an issue or pull request.

Getting the Code

git clone https://github.com/psachmann/fetchling.git
cd fetchling

Setting Up Your Dev Environment

You need:

Tool Version
.NET SDK 10.0 (see global.json)
CSharpier 1.3.0 (installed as a local dotnet tool)

After cloning, restore the local tools and packages:

dotnet tool restore
dotnet restore

Git Hooks

The repo ships husky hook definitions in .husky/, but no project wires them up automatically — install them once per clone:

dotnet husky install

This adds a pre-commit hook that runs dotnet csharpier format on staged files, and a pre-push hook that runs the full test suite (dotnet test -c Release). Both mirror checks CI also runs, so the hooks just let you catch a formatting or test failure before pushing instead of after.

Common Commands

# Build
dotnet build

# Run all tests
dotnet test

# Run tests with coverage
dotnet test --configuration Release -- --coverage --coverage-output-format cobertura

# Check formatting
dotnet csharpier check .

# Fix formatting
dotnet csharpier format .

# Build the documentation site
dotnet docfx docs/docfx.json

Code Style

The project uses CSharpier for formatting, enforced as a CI gate and as a husky pre-commit hook (see Git Hooks below). Run dotnet csharpier format . locally before pushing to avoid the CI failing on formatting.

There is no extensive style guide beyond what CSharpier enforces — just try to follow the patterns already present in the codebase.

Build Gates

Four gates guard the shipping surface under src/ (tests and samples are unaffected). Any of them can turn a change that builds locally into a Release-build failure, so it's worth knowing what each one is before your PR trips it:

  • Public API tracking — every src/ project (except DevTools) lists its entire public surface in PublicAPI.Shipped.txt / PublicAPI.Unshipped.txt. Adding, removing, or changing a public member fails the Release build (RS0016 / RS0017) until you add or remove the line. Apply the IDE's "Add to public API" code fix, or run:

    dotnet format analyzers Fetchling.slnx --diagnostics RS0016 --severity warn
    

    This can't touch .razor files — <Suspense>-style component members go into src/Fetchling.Blazor/PublicAPI.Unshipped.txt by hand, using the exact line the RS0016 message gives you.

  • Package validation — dotnet pack diffs each assembly against the last stable release and fails on an unintentional breaking change. It only runs on pack, not build, so run dotnet pack -c Release locally if your change touches a public member. An intentional break needs a suppression file, not a weakened gate:

    dotnet pack -c Release -p:ApiCompatGenerateSuppressionFile=true
    
  • Banned symbols (src/BannedSymbols.txt) — bans ambient time (DateTime.Now/.UtcNow, etc.) and blocking waits (Thread.Sleep, Task.Delay) as RS0030. All time in src/ must flow through the injected IScheduler so tests can drive it with TestScheduler — this gate is what catches a stray DateTime.UtcNow before it ships. Test code is exempt.

  • Trim / AOT analyzers — every src/ project except DevTools claims trim and AOT compatibility. A construct that can't be statically analyzed for trimming fails the Release build rather than surfacing later as a runtime failure in a consumer's trimmed or AOT-published app.

Making Changes

  1. Fork the repository and create a branch from main.
  2. Write tests for your change. All new behavior should be covered by tests.
  3. Run the full test suite to make sure nothing is broken: dotnet test.
  4. Check formatting: dotnet csharpier check .
  5. Open a pull request against main with a clear description of what you changed and why.

Adding a New Feature

If you are thinking about a larger feature or a change to the public API, please open an issue first to discuss it. That saves everyone time and avoids the frustration of a PR being declined after significant work.

Fixing a Bug

  1. Open an issue describing the bug (if one does not exist already).
  2. Add a failing test that reproduces the bug.
  3. Fix the bug so the test passes.
  4. Open a pull request linking to the issue.

Making a Breaking Change

Breaking changes only ship in a major release, so please open an issue first. Once one is agreed, document it in the migration guide in the same pull request — you know the why and the fix better than anyone will at release time.

The guide is docs/doc/migrating-to-v<N>.md, where <N> is the upcoming major (currently migrating-to-v3.md). Your PR owes it an entry if it:

  • adds a suppression to a CompatibilitySuppressions.xml or a *REMOVED* line to a PublicAPI.Unshipped.txt (see Build Gates), or
  • changes behaviour or telemetry (names, units, tags) in a way existing code still compiles against. No gate catches these, which is exactly why they need writing down.

Each entry is a row in the "At a Glance" table plus a section under "Breaking Changes" with what changed, the error or symptom a consumer sees, a Why:, and a How to migrate: — with before/after code where it applies. migrating-to-v2.md shows the tone and depth to aim for.

If yours is the first break of a new major, create the file and add it to docs/doc/toc.yml above the previous guide.

There is no changelog file to update: release notes are generated by GitHub from pull request titles, so a clear PR title is all they need.

Which section of the notes a PR lands in is decided by its label, configured in .github/release.yml: breaking-change, enhancement and bug get their own sections, anything else falls under "Other Changes", and Dependabot PRs (label dependencies) are left out. Labels are read when the release is published, so they can still be added to merged PRs afterwards.

Improving Documentation

Documentation lives in the docs/doc/ directory as Markdown files. Changes to documentation are just as valuable as code changes — please feel free to open PRs for typos, unclear explanations, missing examples, or anything else.

Running the Documentation Site Locally

DocFX is already restored as a local tool by dotnet tool restore (see Setting Up Your Dev Environment).

# Build and serve locally
dotnet docfx docs/docfx.json --serve

Then open http://localhost:8080 in your browser.

CI / CD

The build pipeline runs on every push and pull request to main:

  1. Restore tools and packages
  2. CSharpier format check
  3. Release build (warnings are treated as errors in Release mode)
  4. Tests with code coverage (Cobertura format)
  5. Coverage upload to Codecov

Pull requests must pass all CI checks before merging.

Release Process (Maintainers)

Cutting a stable release

  • Public API files: move the accumulated PublicAPI.Unshipped.txt additions into PublicAPI.Shipped.txt, delete each *REMOVED* line together with its Shipped counterpart, and leave Unshipped with only its #nullable enable header. Prereleases leave both files alone. Members whose only change since 2.0.0 is nullability annotation (!, ?, ~) already appear in Shipped in their current form: the analyzer matches symbols ignoring annotations and would reject a *REMOVED*/re-add pair (RS0025, RS0050, RS0036).
  • Package validation: set PackageValidationBaselineVersion to the new stable version (currently unset, because no stable Fetchling.* package exists; the 2.x DotNetQuery.* assemblies share no names). The baseline is deliberately the last stable release, not the latest prerelease. For an intentional break, generate a suppression file instead of weakening the gate: mise run suppressions.
  • Docs: remove the "not released yet" note from migrating-to-v3.md, drop the "the upcoming N.0" wording from other docs, and link the guide from README.md, docs/index.md and the upgrade paragraph of docs/llms.txt.
  • Context7: add the new tag to previousVersions in context7.json (keep the previous one) and update the versioned ID in README.md and docs/llms.txt. A push to main touching indexed paths re-indexes via .github/workflows/context7.yaml (needs CONTEXT7_API_KEY).

Migration guide

docs/doc/migrating-to-v<N>.md is written in the PR that makes the break. Each entry is a row in the "At a Glance" table (change / affects you if… / what to do) plus a "Breaking Changes" section with what changed, the symptom a consumer sees, a Why: and a How to migrate: with before/after code. When the first break of a new major lands, create the file and add it to docs/doc/toc.yml above the previous guide. The docs site deploys from main, so open the guide with a "not released yet" note.

Release notes come from PR titles (generate_release_notes: true); grouping is in .github/release.yml (labels breaking-change / enhancement / bug; dependencies and Dependabot PRs are excluded).

Other gate details

  • TrackPublicApi and IsAotCompatible have no default; RequireExplicitProjectProperties fails the build if a src/ project omits either, so an opt-out is always visible.
  • Fetchling.Blazor.DevTools sets TrackPublicApi=false (a drop-in debugging component, not an API to program against) and IsAotCompatible=false (its inspector reflection-serializes arbitrary consumer data, tripping IL2026/IL3050; under trimming JsonSerializer emits silently-empty JSON).
  • RS0041 is suppressed in Fetchling.Blazor (Razor-generated BuildRenderTree overrides use oblivious reference types).
  • To ban another API, add a documentation-comment-ID line (P: / M: with full parameter list / T:), then ; and the message, to src/BannedSymbols.txt.

Reporting Issues

Found a bug? Have a question? Please open an issue with:

  • a clear description of the problem or question,
  • a minimal reproduction (if it is a bug),
  • the .NET version and platform you are using.

License

By contributing to Fetchling, you agree that your contributions will be licensed under the MIT License.