1. 30
  1. 13

    This means the difference between 2022.08.1 and 2022.09.2 is impossible to tell. Was 2022.08.1 the second release in August, or a hotfix for a break in 2022.08.0? Which release is more stable? Should I expect them to be compatible? There just isn’t any information here to make a decision.

    As I understand it, the pro-CalVer argument is that SemVer and other “compatibility semantics as part of the version number” approaches are basically wishful thinking – that there’s no way to actually do it and satisfy all or even most consumers, since consumers will have varying ideas and expectations about those semantics. So, the argument goes, better to use something that explicitly cannot contain compatibility semantics; this way everyone knows they have to go to the changelog every time, rather than make potentially unfounded assumptions based solely on the version number.

    So the CalVer supporter’s reply to the above quote would be “yes, that’s deliberate and is in fact the whole point of using a version scheme like CalVer”.

    1. 9

      consumers will have varying ideas and expectations about those semantics

      I don’t buy this. It is true as an academic argument, but on a practical level SemVer works very well. It doesn’t have to be perfect, it has to be good enough.

      1. 7

        It is true as an academic argument, but on a practical level SemVer works very well.

        The “scandal” of the Python cryptography package rewriting its compiled extension from C to Rust is my go-to example of SemVer expectation problems. The package wasn’t even using or advertising SemVer, and the rewrite of its compiled extension did not break or change any of the package’s documented public API (so would not have been a SemVer violation even if the package were trying to follow SemVer), but it was still widely condemned as an unacceptable compatibility break and a violation of consumer expectations.

        So this is very much a practical and not an “academic” thing.

        1. 10

          Was it widely condemned?

          I got the impression that it was condemned by a very small but very loud minority - people who cared about alpha mainframe architectures possibly?

          I didn’t follow the situation super closely though, so maybe there was more of a universal condemnation than I picked up.

          1. 2

            I remember far fewer voices of reason in that whole situation. Some of it was, I think, driven by the choice of Rust specifically (for some people that’s practically a culture-war target nowadays), but I would not have wanted tobe a cryptography maintainer as that was going on.

            1. 3

              I would not have wanted to be a cryptography maintainer as that was going on

              Me neither, but perhaps this is more reflective of the community than of the versioning scheme used?

              Edit: I should be clear that I’m not part of that community.

          2. 3

            Works very well does not mean perfect. I don’t see any incompatibility here.

            1. 2

              I’m not asking for “perfect”, though. And I don’t think “works very well” is how I’d describe the situation I mentioned.

              1. 3

                I’m not applying it to that situation. That seems like one place it may have broken down.

                When I say works very well, I’m talking about my experience of how it has made it possible to keep the many dependencies of many applications up to date with very little breakage. I think of it as a hint, or a strong signal, not an absolute guarantee. The alternative is having no signal and adding a larger burden on every user of the dependencies.

          3. 3

            My problem with SemVer is that, due to the “spacebar heating” effect, pretty much anything can be a breaking change, so what the author considers an actually breaking change is highly subjective.

            1. 2

              This may be true for applications, where the set of interfaces has to be clearly defined to be able to define breakage, but for libraries, breaking changes are really easy to identify. You bump the patch version if the change did not affect the ABI, the minor version if you only added to the interface (not affecting all applications linked against a smaller minor version) and the major version if you changed the interface (i.e. changed existing methods or data structure).

              Regarding applications, I like the approach to build even those as libraries and the GUI as a thin layer on top. This makes it easy to write different interfaces to one single program (e.g. a GUI, a TUI, a web interface and maybe even a local REST-API or other form of IPC at the same time). Then you can apply the library rules and resolve this conflict. :)

              1. 2

                The library rules seem too simple. What about backwards-incompatible semantic changes that don’t touch the interface?

                1. 0

                  This naturally also constitutes a major version bump, which can be trivially deduced from the rules I gave.

                  1. 2

                    Would you consider the removal of spacebar heating a breaking change?

                    1. 1

                      No, because it wasn’t specified in the documentation.

              2. 2

                There’s a weird corollary of this where sometimes libraries that are actually good at maintaining backwards compatibility in practice end up with really large major version numbers. Their authors care a lot about backwards compatibility, so they bump major any time they change anything that could possibly be depended on. This includes stuff that almost certainly no real API consumer cares about, like the joke about spacebar heating. As a result they end up on version “27.0.0” after a few years. Meanwhile consumers originally written against their “0.1.0” prerelease have yet to get broken.

                1. 1

                  SemVer also signals intent as to the scope of the change. What else, besides always reading the change notes (if there are any) or just trying it out, do you suggest?

                  1. 1

                    Assume all changes are breaking and invest in good test automation.

            2. 3

              My favorite variation on CalVer has a leading digit.

              So if you adopted CalVer at version 3.42, on today Jan 17th 2023, the new version would be 3.20230117.0

              1. 3

                My big problem with SemVer is that it’s a property of interfaces not implementations and so using SemVer for package versions is inappropriate. I’d be very happy with a package using calendar versions for its releases and advertising the set of interfaces that is supports with semantic versions.

                COM handles interface versioning in the simplest way possible: any change is considered a breaking change and needs a new version. Languages that support adding APIs (new types in a namespace, new methods in a class, and so on) in a non-breaking way[1] can support additions in a minor revision and removals only in a major revision.

                Good software engineering with graceful deprecation periods is impossible with the semver-for-implementations model. Imagine three versions:

                • A includes a feature.
                • B deprecates the feature and adds a replacement.
                • C removes the deprecated feature.

                This is the flow that I want from libraries that I consume. I move from A to B, test, get some warnings, eventually migrate to the new API, then migrate from B to C. At any given point, my code either works with A and B, or with B and C. With semantic versioning for interfaces this works fine:

                • A implements 1.0
                • B implements 1.1 (or 1.0 if the replacement feature is not exposed on the old interface) and 2.0
                • C implements 2.0 (or 2.1 if it also adds some new things)

                With implementation SemVer, you end up with C being 2.0, indicating that it’s the one that’s a breaking change, but it isn’t a breaking change for anything that heeded the deprecation warnings and moved to the new APIs when using B.

                [1] This is tricky in the presence of patten matching on types and other forms of introspection, because some code may depend on the absence of a method with a particular name on a type.

                1. 1

                  Can’t this be approximated by creating a differently-named package for 2.0? As in, you start with libfoo 1.0, then release libfoo 1.1 with the deprecation warnings along with libfoo2 2.0 which implements the new API. I suppose there won’t be a version that implements both APIs at the same time but maybe that’s a good thing (KISS and all)?

                  1. 1

                    That would work as long as you have a single user of the package. If not then you will end up linking libfoo and libfoo2 in the same program, which will often cause problems. With versioned interfaces, any library that depends on libfoo can move to the 2.0 API and expose compat interfaces that the the 1.0 interfaces from libfoo, allowing you to upgrade each library independently and then move to the C version once everything has moved. With the libfoo and libfoo2 distinction you end up with a flag day when you must bump all dependencies to a new version at the same time. In my experience, this kind of flag day is one of the worst things to hit in software engineering because something will invariably break and it takes ages to find the root cause because you’ve changed too many things at once.

                2. 2

                  One’s final product is another’s component.

                  My team had a similar discussion several months ago and agreed on the same heuristic:

                  • CalVer for our applications
                  • SemVer for our libraries

                  That said, it seems like web APIs break the mold. AWS, Stripe, Square, and Twilio all use CalVer.

                  1. 2

                    Code nowadays has become more and more short-lived, especially in the web framework space, where people seemingly rewrite their codebases every 6 months with $NEWEST_WEB_FRAMEWORK.

                    I think that the only motivation behind calendar versioning is to show off how “new” a release is, as not to be consumed in the maelstrom that this runaway diesel engine has become.

                    Semantic versioning is a great scheme to build upon, especially for libraries, and I see that the author agrees with this sentiment. But maybe it makes sense to reflect upon the original premise of trying to show off code recenticity due to the fast code rot of some parts of the industry. Then semantic versioning will remain truly victorious.

                    1. 2

                      I’m here for Dask 3000.0.0

                      1. 1

                        we use calver in pytype, and it has proven to be a really good fit for us. for the most part we don’t have “versions” so much as a series of continuous improvements; the main breaking change is dropping support for an old python version, and occasionally adding a feature that while technically desirable nonetheless breaks older code that was relying on a more permissive view of types.

                        for us, the key to making calver work well is to be meticulous about documentation. for instance:

                        • a supported versions table listing the last pytype release that supported historical python versions
                        • a detailed changelog entry every time we bump the version number

                        we also have breaking changes guarded behind feature flags so that people can try out the effect of the new checks on their code before we make them the default behaviour.