Skip to content
This repository has been archived by the owner on Dec 4, 2020. It is now read-only.

[core] Upgrade the Semantic Versioning whitepaper: breaking change introduces new contract #2958

Open
bjhargrave opened this issue Sep 26, 2018 · 7 comments

Comments

@bjhargrave
Copy link
Member

Original bug ID: BZ#3090
From: @rinswind
Reported version: R7

@bjhargrave
Copy link
Member Author

Comment author: @rinswind

Bug BZ#3081 lead to a discussion about how to introduce breaking changes to an API so that they cause least downstream breakage (do not fork the universe), but allow gradual adoption even within one bundle.

The discussion converted on the conclusion that in a way a package name is bound immutably to a meaning that can not be changed if the above properties are required. A breaking change introduces a brand new thing with a new meaning and therefore a new package name has to be bound to that new meaning.

In short: breaking changes should be introduced with new packages, not by bumps to the major version of the existing packages. Logically every package stays forever at major version 1.

The Semantic Versioning whitepaper should be upgraded to reflect this new thinking.

A number of details need to be hashed out.
Here are some examples:

  • Is the full semantic version still used anywhere?
  • Should there be recommended naming conventions? E.g. "org.example.foo.v2"
  • How does this reflect the version import ranges? E.g. the consumer can have [1.0.0, infinity), but the provider still has to have [1.0.0, 1.1.0).
  • What to do when there are already released multiple major versions?
  • ...

@bjhargrave
Copy link
Member Author

Comment author: @rinswind

As an initial iteration I propose a convention one can use when starting from a green field:

  1. org.example.foo; version=0.0.1
    The prototype period when the meaning is first being defined. Breakage is allowed now and only now.

  2. org.example.foo; version=1.0.0
    The meaning of the package name becomes locked down.

  3. org.example.foo; version=1.0.1
    The meaning is unchanged.

  4. org.example.foo; version=1.1.0
    The meaning enriched, but not invalidated.

  5. org.example.foo; version=2.0.0
    ILLEGAL: the meaning is invalidated.

  6. org.example.foo.v2; version=1.0.0
    New package name bound to the new meaning.

The goal is not only to make it mechanically possible to have gradual migration, but also to communicate clearly

  • The succession of releases of each package: no skipping numbers in any position in the version to make people look for the missing releases.
  • That the new package replaces the old package: hence it incorporates it's name.

The trade-off is that everything being forever at "1.x.y" looks odd.

@bjhargrave
Copy link
Member Author

Comment author: @tverbele

I still disagree though (or I am missing something). The meaning can still remain the same although a version 2.0.0 seems more appropriate.

For example org.example.foo; version=1.0.0 provides an API to do task Foo. Now I discover Promises and PushStreams, and decide to totally rewrite my API using Promise and PushStream return types. The new API breaks everything out there, but the meaning is still the same, it still does task Foo. To me it seems legit to have this new API as package org.example.foo; version=2.0.0. Why is it better to find out a new name, although the meaning stays the same? Why is it better to name the package org.example.foo.v2?

@bjhargrave
Copy link
Member Author

Comment author: @rinswind

(In reply to Tim Verbelen from comment BZ#2)

I still disagree though (or I am missing something). The meaning can still
remain the same although a version 2.0.0 seems more appropriate.

For example org.example.foo; version=1.0.0 provides an API to do task Foo.
Now I discover Promises and PushStreams, and decide to totally rewrite my
API using Promise and PushStream return types. The new API breaks everything
out there, but the meaning is still the same, it still does task Foo. To me
it seems legit to have this new API as package org.example.foo;
version=2.0.0. Why is it better to find out a new name, although the meaning
stays the same? Why is it better to name the package org.example.foo.v2?

Then we must refine the terms. Perhaps just "breaking contract" - in the sense we use it in OSGi. This includes binary compatibility and behavioral compatibility.

The goal is to provide a smooth migration path to the new contract.
This means one where the same piece of code can use both contracts at the same time. In OSGi we share at the package granularity. Therefore the same piece of code must be able to load both 1.0 and 2.0. Therefore the only way is to introduce a new package.

@bjhargrave
Copy link
Member Author

Comment author: @tverbele

Ok I see, you want a bundle to be able to import both v1.0.0 and v2.0.0 hence v2.0.0 should be a different package name. Still I find a version inside the package name like org.example.foo.v2 a bit silly and would not make that the recommended naming convention.

Also, I wonder whether is this an often required use case. I mean, if my bundle contains org.example.impl.foo; version=1.0.0 which implements org.example.foo; version=1.0.0, and then org.example.foo; version=2.0.0 is released, then I'd have to totally rewrite my bundle anyway. Why not just release org.example.impl.foo; version=2.0.0 that implements the new API? People wanting the old API can still use my v1.0.0 bundle?

@bjhargrave
Copy link
Member Author

Comment author: @rinswind

(In reply to Tim Verbelen from comment BZ#4)

Ok I see, you want a bundle to be able to import both v1.0.0 and v2.0.0
hence v2.0.0 should be a different package name. Still I find a version
inside the package name like org.example.foo.v2 a bit silly and would not
make that the recommended naming convention.

Also, I wonder whether is this an often required use case. I mean, if my
bundle contains org.example.impl.foo; version=1.0.0 which implements
org.example.foo; version=1.0.0, and then org.example.foo; version=2.0.0 is
released, then I'd have to totally rewrite my bundle anyway. Why not just
release org.example.impl.foo; version=2.0.0 that implements the new API?
People wanting the old API can still use my v1.0.0 bundle?

I think because in practice someone downstream can break with a uses constraints violation out of which there is no way out unless an entire transitive dependency chain migrates.

If we come to disagree this is a good idea - so be it. It's still valuable to have the argumentation against that.

@bjhargrave
Copy link
Member Author

Comment author: @rinswind

Here is a further refinement of the proposition of this bug:

A better term than "meaning" for OSGi is "contract".

We version contracts, not artifacts.

Sometimes a contract has only one provider, sometimes a contract ships with that provider. The rest of the world tends oversimplify versions to only this last case.

As we know there are two parties collaborating around the contract:

  • consumers
  • providers

The goal of the contract is fulfilled when it is used. This is why the consumers are more important than the providers. This is reflected in the semantic version, where the most significant number (the major version) describes compatibility for the consumers.

A critical trait of the contract is that it allows consumers and providers to evolve at different pace:

  • The micro version breaks no one.
  • The minor version breaks only the provider.
  • The major version breaks both the consumer and the provider.

The proposition of this bug is that a major version change should be considered a new contract, rather than an evolution of the old one. This then will allow both consumers and providers to migrate from the old contract to the new gradually, rather than to split the universe.

Splitting the universe is bad because it means the new contract can fulfill it's goal only after everyone migrates in reverse dependency order. I.e. the trait of a contract to allow independent evolution is broken - consumers and providers become locked. The consumers are not in a position to compensate by consuming both contracts, nor are providers by implementing both contracts.

Another way to look at it is that a major version change is equivalent to the state when the contract was first introduced and there were no providers. Again the contract has to be adopted in reverse dependency order. So it looks more like the major version change is a new contract for all practical purposes.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant