Semantic versioning for bundles

Posted on by Matthias Noback

A short introduction to semantic versioning

Semantic versioning is an agreement between the user of a package and its maintainer. The maintainer should be able to fix bugs, add new features or completely change the API of the software they provide. At the same time, the user of the package should not be forced to make changes to their own project whenever a package maintainer decides to release a new version.

The most extreme solution to this problem is: the package should never change, the user should never need to upgrade. But this is totally useless, since the user wants new features too, they just shouldn't jeopardize their existing, functioning code. At the same time the package maintainer wants to release new features too, and they may even want to redo things completely every once in a while.

Semantic versioning assumes a package to have a version number that consists of three numbers, separated by dots, like 2.5.1. The first number is called the "major version", the second number is called the "minor version", the last number is called the "patch version". The semantic versioning agreement in short tells the package maintainer to increment the:

MAJOR version when you make incompatible API changes,

MINOR version when you add functionality in a backwards-compatible manner, and

PATCH version when you make backwards-compatible bug fixes.

You can read the full explanation of the concept and what you are agreeing upon if you say that your package "follows semantic versioning" on semver.org.

Symfony and semver

As of version 2.3 the Symfony framework officially uses semver. They also apply some extra rules for parts of the code (classes, interfaces, methods) which they label as being part of the official Symfony API by adding an @api annotation to the respective doc comments. Semantic versioning and public API labeling together this consistutes Symfony's backwards compatibility promise.

In short, Semantic Versioning means that only major releases (such as 2.0, 3.0 etc.) are allowed to break backwards compatibility. Minor releases (such as 2.5, 2.6 etc.) may introduce new features, but must do so without breaking the existing API of that release branch (2.x in the previous example).

Bundles and semver

I was asked by Paul Rogers from the Symfony Live London crew:

How should you version a bundle? Should it be related to the library version, like ElasticBundle does?

As a matter of fact, I had already spent some toughts on this issue. The answer to the first question is: apply semver, just like any package should do. And the answer to the second is: no. It should not per se be related to the library version for which the bundle provides a framework-specific integration layer. I think this second answer requires some more detailed reasoning from my side.

Bundles expose an API themselves

The code in a library package exposes an API. People are going to use that API in a particular way. They are going to instantiate some of the classes from the package with a particular set of constructor arguments. They are going to call some of its methods with a particular set of arguments. This is the reason why semantic versioning should be applied to such packages in the first place: the maintainer should not be allowed to change any of the things the user relies on, like class names, method names, required parameters, etc.

A bundle mostly doesn't expose an API consisting of classes and methods. The API of a bundle consists of *services, or service definitions, and parameters. These are different types of entities. Yet they share some characteristics: service definitions often provide constructor arguments in a particular order, they sometimes contain method calls used for setter injection, they are public or private, abstract or concrete, have a certain name, etc.

Some changes, like making a private service public won't make a difference for an existing user.

services:
    formerly_a_private_service:
        public: true

If a service was formerly a private service, the user could not have relied on it in any way that a public service doesn't support. So that kind of a change should not to be considered a backward compatibility (BC) break.

The other way around - making a public service private - on the contrary should be considered a BC break.

services:
    formerly_a_public_service:
        public: false

Some users may rely on it being public. For example they may call

$container->get('formerly_a_public_service');

somewhere and all of a sudden they would get an exception for that.

The API of a bundle leads a life on its own

The fact that bundles have their own API, which changes in a way that is not related to the changes in the underlying library per se, means that bundles should have their own versioning. When the bundle maintainer introduces a BC break in the bundle's service definitions, they should increment its major version. If the bundle maintainer keeps BC by adding a service alias, or wrapping a service in a way that is transparent to the user, they are allowed to increment just the minor version. And if the maintainer fixes a bug, they can just increment the patch version.

A bundle may reach maturity at a later time than the library

Another reason why bundle versions are not necessarily the same as the version of the library it serves to integrate, is that a bundle may be created much later than the library. In such a situation, the library may be at 2.5.1, while the bundle is only available as a pre-stable development release, e.g. at version 0.3.4. When the bundle is finally stable, it wouldn't make sense to skip some versions and release it as 2.5.1 too.

A library may contain bugs that are totally unrelated to the bundle

Whenever a bug is fixed in the library, its patch version will be incremented. When the library is at version 2.5.1 and a bug gets fixed, the new version will be 2.5.2. Now if there is a bundle at 2.5.1, it should increment to 2.5.2 just to keep up, even though the bundle didn't change in any respect. That doesn't make sense. And even though this may seem a bit weak as an argument, this is what my reasoning really boils down to:

a change in the library doesn't necessarily require a change in the bundle, so it also doesn't make sense to make the bundle strictly follow the library version.

A library may contain features that are not implemented by the bundle

One last argument for separate bundle versioning is that a bundle might not provide integration for all the features that a library offers. Then if the library stays the same, while the bundle starts to expose existing library features, the bundle should increment its minor version, while the library keeps the same version number. This is obviously an unwanted situation.

Conclusion

I think my point is clear and I hope this article can serve as a definite answer to the question: "should a bundle (module, plugin, etc.) follow the versions of the corresponding library?" No, it shouldn't.

PHP Symfony2 bundle semver package design