Skip to content

Contributing to CanKit.Pro

Thanks for helping. This page covers the three things that are specific to this repository: how branches and commits work (they decide releases), how to run the tests, and where a change belongs — here or upstream in CanKit.

Does your change belong here?

CanKit.Pro sits on top of CanKit, which it consumes as a NuGet package. It does not fork it.

  • Adapters, ICanBus, CanFrame, timing, the registry → upstream, at pkuyo/CanKit. We cannot fix those here, and a workaround layered on top usually makes the eventual upstream fix harder.
  • Demultiplexing, actors and scheduling, deadlines and bus state, CAN-ID arithmetic, and the protocol stacks above them → here.

Moving to a new CanKit version is a single change: bump CanKitVersion in eng/Dependencies.props and let CI tell you whether anything broke.

Getting set up

git clone https://github.com/dborgards/CanKit.Pro.git
cd CanKit.Pro
dotnet build CanKit.Pro.sln -c Release
dotnet test  CanKit.Pro.sln -c Release

You need the .NET 10 SDK or newer (global.json pins 10.0.100 and rolls forward to a later major if that is what you have installed). No CAN hardware is needed — the whole suite runs on CanKit's virtual:// loopback adapter and on in-repo test doubles.

A local build produces version 0.0.0, deliberately: nothing in the build computes a version, it only receives one from the pipeline or from semantic-release. To see what GitVersion makes of your working copy, ask it directly:

dotnet tool restore
dotnet gitversion

Optional but useful before pushing:

dotnet format CanKit.Pro.sln    # applies .editorconfig

Branching

Trunk-based, one long-lived branch:

Branch Purpose
main Always releasable. Every merge is analysed by semantic-release and may publish a release.
feat/…, fix/…, docs/… Short-lived. Branch from main, open a pull request into main.

There is no develop and no release branch: the version is computed from the commits, so a staging branch would only add a place for the two to disagree.

Commits and pull-request titles decide the release

Pull requests are merged with a merge commit, and every commit on the branch is retained and analysed by semantic-release — not only the pull-request title. git log --first-parent main shows Merge pull request … throughout; the merge commit's own subject is not a Conventional Commit and contributes nothing, so the branch commits are what decide the release.

Write every commit as a Conventional Commit, and keep the pull-request title one as well — it is what reviewers read, and it is what would decide the release if the repository ever switches to squash merging:

feat(rawcan): expose per-subscription drop counters      -> minor release
fix(actor): stop Dispose deadlocking on the loop thread  -> patch release
feat(addressing)!: rename ComposePgn parameters          -> major release
docs: explain the deadline rearm semantics               -> no release

Scopes match the packages: rawcan, actor, addressing, reliability, plus docs, ci, build, deps.

A breaking change needs both the ! marker and a footer that says what to do about it:

feat(rawcan)!: ISubscription.Frames requires a CancellationToken

BREAKING CHANGE: `Frames` is now `Frames(CancellationToken)`. Callers using
`await foreach (var f in sub.Frames.WithCancellation(token))` become
`await foreach (var f in sub.Frames(token))`.

Types that never release on their own: docs, test, chore, ci, style, refactor. Use them honestly — labelling a behaviour change as refactor means it ships with no changelog entry and no version bump, which is worse than a noisy changelog.

Tests

Every behavioural change needs a test, and tests here are expected to be deterministic:

  • No hardware, no timing luck. Use the virtual:// loopback adapter for anything about real bus behaviour, and ControllableBus (tests/CanKit.Pro.Tests/Infrastructure/) when the test needs to control what the bus does — echo frames, bus state, whether a transmit is accepted. ControllableBus.DeferredEchoCapable(...) parks each TX echo in a DeferredEchoQueue instead of raising it inside Transmit, which is the only way to have two sends pending at once: a synchronous echo re-enters CanBusService's pending-send lock on the transmitting thread, so the pending list never holds more than that thread's own entry. Reach for it whenever the behaviour under test is about how several in-flight sends relate to each other.
  • Do not test through an adapter's internals. If a test needs reflection into another package's private state, it is testing that package, not ours; drive the scenario through the double instead.
  • Cite the requirement. Tests reference FR-RAW-* IDs from the SRS so a reader can tell intended behaviour from incidental behaviour. Keep that up.

Run a single class while iterating:

dotnet test CanKit.Pro.sln --filter "FullyQualifiedName~TxConfirmTests"

The net48 leg

The libraries ship netstandard2.0 as well as net10.0, so the suite multi-targets net10.0;net48net48 consumes the netstandard2.0 assets, which is the only way that build is ever executed rather than merely compiled (SRS NFR-004). The extra target framework is added only when the build runs on Windows, because nothing else can host it; on Linux and macOS dotnet test runs net10.0 alone and the Windows CI job covers the rest.

A test that genuinely cannot run on .NET Framework belongs behind #if NET, not outside the suite. To compile-check the net48 leg without a Windows machine:

dotnet build tests/CanKit.Pro.Tests -f net48 -p:CanKitProTestNetFrameworkLeg=true

Public API

These are published libraries, so the public surface is a promise:

  • XML documentation on every public type and member. The existing code documents in English with a Chinese translation, inherited from CanKit's own style — English alone is fine for new code. Those comments are published verbatim: eng/build-api-docs.sh turns them into the API reference on every website build, so what you write there is what readers see, and the FR-…/ADR-… ids you cite become links into the SRS and the arc42 document.
  • Prefer adding an overload to changing a signature. If a break is genuinely right, mark it ! and write the migration into the footer.
  • New public types need a matching section in the package's README.md, which ships inside the .nupkg and is what people actually read on nuget.org.

Reporting bugs

Use the issue templates. A reproduction on virtual:// endpoints is worth a great deal: it can go into the test suite as-is, which usually means the fix ships in the next release rather than the one after.

License

Contributions are accepted under the MIT License. There is no CLA — opening a pull request licenses your contribution under the repository's license. See docs/licensing.md.