Create separate shared libraries for your different platforms (desktop and web). Whatever both of these libraries will need should go in a separate shared library. Thus, you could split these libraries up as follows:
Company.Core
: Used by all libraries. Contains generic code, helper classes, etc. that are not specific to any platform. References no other shared libraries.
Company.Core.Desktop
: Used by desktop applications. References Company.Core
library.
Company.Core.Web
: Used by web applications. References Company.Core
library.
This allows you to change the web technology being used without affecting the desktop technology being used. This also allows you to add more platforms in the future (e.g. Company.Core.Mobile
).
Ensure these libraries are unit tested and never again copy code between projects.
Based on your description, the common library appears to be a collection of business functionality and utilities common to many of your projects. Regressions caused by one product's modifications may not be immediately visible in the other products.
Some core recommendations are:
Versioning
As you mention, the library is pretty stable. Versioning this library, if you haven't already, is the first step to preventing side-effects. Once released, versioned artifacts are assumed to be immutable.
Each consuming project should lock to a known-good version of the library. QA only has to worry about breaking changes when they choose to update to a newer library version. At this point, they should be running full regression testing anyway.
Semantic versioning (major-minor-patch) is a compatibility-based versioning scheme that might apply in your case. Library versions can be easily managed through artifact-management tools such as NuGet. Source code can be maintained using branches, tags or similar mechanisms.
Release Notes: With versioning, you have the additional advantage of release notes. Each version can describe the bug fixes and enhancements that went into that release. This makes it more visible to product owners, developers and QA, so they can make informed decisions.
Decomposition
Based on historical data, you might have a good idea about which areas of the library have the most changes or churn. You might also have fairly independent areas of functionality within the same library.
It might be a good idea to separate these areas into independent libraries and modules, each with their own versions and locked dependency versions. This helps to reduce the impact of a version upgrade and isolate ripple effects to a more defined area.
Product owners can now track which sub-modules need the most attention, and would be able to create a dedicated team to manage or QA those specific areas, if necessary.
Unit tests and e2e tests
Define your "units" of functionality. In most utilities, the unit is a single class or a single function. In some cases, it might be a module.
Design unit tests to cover individual functionality. Use the 80-20 rule to decide which units need the most tests. In addition, design end-to-end tests as necessary, mocking out sample library consumers and testing the expected behavior.
This will be the first line of defense, and should alert developers that they are making incompatible changes and need to update the version.
Best Answer
You create a public API and version it. The consumers ( the other projects ) get to follow the API and you get to unit test it thoroughly.
Also it is a good practice to do code reviews. They should be done by you and the other core developer whenever something is about to change in your common library code.
Before any changes are to be included either you or the other core developer must sign off the code. Then and only then should the change be merged into the trunk.
Be careful of changes that break unit tests on the public side of the api, in that case you should change the api's version. A good idea is to follow semantic versioning like it is suggested in the comments.
Be strict about this.