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
Comments
This website uses MailComments: you can send your comments to this post by email. Read more about MailComments, including suggestions for writing your comments (in HTML or Markdown).
LeMike

I give you a tool that "determines" your next version:

https://github.com/sourcere...

It thinks for you and makes suggestions while you can focus on writing code ;)

Cheers!

If the library and the bundle are close together, i agree with you Marc. But other things, for instance Elastica and FOSElasticaBundle are not maintained by the same person at all and it would be strange to try to follow the same release cycle.

@matthias what do you think about the comment from Iltar? Imho its very convenient to have the bundle depend on the library so that the end user can just require the bundle and get everything (otherwise he would need to figure out which version is compatible, which is stupid too).

Matthias Noback

See my responses below :) Yes, a bundle should always depend on the library, and the user should not need to worry about adding the library specifically to the list of project dependencies. They just add the bundle as a dependency, and it pulls in the best version of the library for them.

Marc Morera Merino

Hmmm interesting.

Maybe it is not necessary to maintain both component and bundle in the same version, but if you think as a user (all type of users, beginners, advanced and proficiency), if you maintain same version always between them, you are providing maybe a little bit more of mental consistency.

Another thing to consider is that when you work with a single repository with components and bundles (like Symfony does), it is very normal (and IMHO logical) to add same new tag to all sub-repositories than the big one. If symfony/symfony is tagged as 2.5.6, all read-only sub-packages should be tagged as well with the same tag, even if last added tag is currently in HEAD (current component has no changes between versions).

Matthias Noback

Yes, Marc, I agree with that. If developing bundle and library goes in hand in hand, like many projects do, then it should be like you describe. Chances are that BC breaking changes will occur in both of them at the same time anyway.

(deleted)

Matthias Noback

Thanks for your remark Iltar. I'm slowly wrapping my head around this ;) I think the situation you suggest actually calls for an addition to the list of BC breaking changes: between minor versions of a bundle, the bundle should not change major versions of its dependencies. Would that suffice, you think?

I would be explicit about the opposite too: Changing a major version of a dependency should be a major change of the version - at least if that dependency is expected to be used directly in a project using the bundle.

If possible, the bundle should be as open as possible though. If it works fine with 2 major versions of a library, no reason to restrict to the latest major version - leave that choice to the project if possible.