Hacker News new | past | comments | ask | show | jobs | submit login

Here’s the post’s core thesis:

> At this point you could even get rid of the version altogether and just use the commit hash on the rare occasions when we need a version identifier. We're all on the internet, and we're all constantly running npm install or equivalent. Just say "Leftpad-17", it's cleaner.

> And that's it. Package managers should provide no options for version pinning.

> A package manager that followed such a proposal would foster an eco-system with greater care for introducing incompatibility. Packages that wantonly broke their consumers would "gain a reputation" and get out-competed by packages that didn't, rather than gaining a "temporary" pinning that serves only to perpetuate them.

Pinning is absolutely crucial when building software from some sources out of your control, especially with larger teams or larger software. You cannot rely on the responsibility of others, be they 100s of upstream library authors, or 100s of other peer developers on your product. One among those people may cause a dependency to break your build, either by releasing a bad version, or foolishly adding a dependency without carefully checking its “reputation” for frequent breaking changes.

In either case, without pinning, your build is non-deterministic and sometimes starts failing it’s tests without any diff. You can’t bisect that. Your only remediation is manual debugging and analysis. Work stops for N engineers because everyone’s branches are broken.

I don’t think any level of community fostering is worth that kind of risk.




> just use the commit hash

Does the author honestly fail to realize that commit hashes are incomprehensible to most humans and look like noise, it takes extraordinary mental effort to compare them? While difference between 1.2.3 and 1.4.5 is instantly apparent to any human?

> Pinning is absolutely crucial when building software from some sources out of your control

Amen. I am surprised how the author does not recognize it.

In fact, the only idea that does not come as outright false immediately after reading is "if you change behavior, rename". But thinking about it for a bit, it is wrong too. First, naming is hard. Finding good recognizable name for a package is hard enough, if each BC break would require a new one, we'd drown in names. Second, behavior breaks are not total. If RoR or ElasticSearch releases a new version with BC break, they do not stop being essentially the same package just with somewhat different behavior. Most of the knowledge you had about it is still relevant. Some pieces are broken, but not the whole concept. New name requires throwing out the whole concept and building a new one, essentially. It is not good for incremental gradual change.


My reading of that was that "rename" in that context meant "rename your package from Foo-1 to Foo-2", not come up with a completely brand-new name.


Mine too, eventually. I am surprised that the writer was unable to make even that very simple point clear.


I thought the author did a decent job - he had two examples (Rails-5 and Leftpad-17) that made the point pretty clearly.


it takes extraordinary mental effort to compare them

Worse is that you can't just sort on hashes alphabetically and recover the correct version order. That alone is reason enough to use some kind of sortable versioning scheme.


> While difference between 1.2.3 and 1.4.5 is instantly apparent to any human?

The author's point is that the difference between 1.2.3 and 1.4.5 can be anything from a bugfix to the software going from self-hosted photo software to now it's driving a car. The assumption that users have the same perception of a major and minor version as the developer responsible for the versioning, is assuming that all humans understand versioning 100% identically.


You're overthinking this. The only significant difference between the versions 1.2.3 and 1.4.5 is that 1.4.5 is instantly recognized as the newer version. Can you say the same about two arbitrary commit hashes?


But what is the value in knowing that it is newer, if you know nothing else than that it is newer?


Preach it brother. I work primarily in the enterprise Java world and I'd never just blindly compile in a dependency of LATEST and keep my fingers crossed. Any version bumps in my POM dependencies block are intentional. The transitive dependencies are hard enough to deal with already without having to worry if my dependent binary footprint changed significantly from the last build.


Within a given team new dev always breeds bugs. Yet some persume that's somehow not true for upstream dependencies? I can't see making that assumption.

I would think the best approch is "trust but verify" ANY update to a dependency. A dependency might save you some time but it's not a free pass to be irresponsible. There's not such thing as FOSS.


> Pinning is absolutely crucial when building software from some sources out of your control, especially with larger teams or larger software. You cannot rely on the responsibility of others, be they 100s of upstream library authors, or 100s of other peer developers on your product.

In fact that's the entire reason why "semantics versioning" doesn't really work:

* you can never be certain that the maintainer actually follows it

* one user's breaking change is one maintainer's bugfix

* while some packaging systems attempt to automate and enforce it (e.g. Elm's) even that only goes so far due to type system limitations (e.g. changing acceptable values from [0, 4] to [0, 4[ as a side-effect of some change deep in the bowels of the library's logic is pretty much guaranteed not to surface if you don't have a dependent type system, and may not do so even then)


> you can never be certain that the maintainer actually follows it

I write a bunch of Rust. The Cargo package manager has several layers of "defense":

1. The "Cargo.lock" contains exact versions of all transitive dependencies. (And Cargo doesn't allow overwriting a version once published, and lock files even override normal package "yanks", so these version numbers are very stable identifiers.)

2. The "Cargo.toml" file contains manually-specified semver information. I can run "cargo update" to get the latest, semver-compatible versions of all my dependencies.

3. In the rare case that somebody messes up semver, I can just blacklist certain versions in "Cargo.toml", or add more specific constraints. I think I've done this maybe two or three times in thousands of dependency updates.

4. My own projects have unit tests, and of course, Rust does extensive compile-time checks. So I'm likely to catch any breakage anyway.

So the absolute worse-case scenario here is that somebody messes up (which is very rare). At this point, I can just file a bug upstream, lock a specific version down in "Cargo.toml", and wait for things to get sorted out.

I have zero need to "be certain". I'm quite happy with "It works nicely 99.9% of time, and it has an easy manual fallback when the inevitable problems occur."


It only "doesn't really work" because this is ascribing too much responsibility to it. Semver is a social protocol designed to assist in solving--but by no means completely solve--the technical problem of how to resolve dependency versioning. It only gives us the barest protocol to allow us to begin describing the problem, and permits us to attempt to build technical solutions on top of it. Nobody's pretending it's perfect, but if anyone has a better solution, feel free to propose it (and neither the OP's, nor "romantic versioning", are any better at solving this problem than semver).


Package registries should implement some sort of required automated integration testing. If a package owner wants to update their package with what they claim is a non-breaking change according to semver, then the new version must pass all integration test cases of the previous. This could also include crowdsourced testing of that package, that allows users of that package to add tests over time to that particular version, so as to mitigate owners who don't test their packages well enough.


Then you’re just defining “breaking change” as “change that breaks the tests.” Anyone with any programming experience knows that “but the tests passed” is never a valid excuse for a bug.


Some verification would be better than no verification. You still need to verify and test package upgrades and pin versions; but it would make the semver contract at least specified to some degree.


I think this is an excellent idea, particularly for typed languages. For a typed language package manager, you can run the following verifications:

- for a patch version, all exported (name, type) pairs of A.B.C are identical to those of A.B.(C-1)

- for a minor version, enforce first rule, but allow new types for new names.

I think the Elm package manager does this, based on other comments here in this thread.

Either way, that sort of automatic “exports” verification could be a base layer in a semver verification package manager, with integration tests layered on top.


There's a lot more to in than that in practice. For example, in a language with function overloading and implicit conversions, adding an overload may silently change the meaning of existing code.

Sometimes, this can get very gnarly. For example, C# has overloading, and it also has type inference for lambdas; and it tries to make these two work seamlessly. So, consider something like this:

   int F(Func<int, int> g) => ...

   string F(Func<string, string> g) => ...
This is perfectly legal - just a couple of overloads. Now, suppose we call it like so:

   F(x => x.ToUpper());
There's no type for x here, so compiler has to infer it; but it also has two possible overloads to deal with. The way it goes about it is by considering both, and seeing if the lambda "makes sense" - i.e. if the body compiles, and its result type (accounting for possible implicit conversions) matches that specified in the function type being considered.

Now, when it tries Func<int, int>, it doesn't work, because if x is int, it doesn't have a method ToUpper, so the body of the lambda is ill-formed. On the other hand, given Func<string, string>, x is string, ToUpper() is well-formed, and returns a string, matching the function type. So that's the overload that gets chosen.

Now consider what happens if someone adds a method called ToUpper to int: now the code that was compiling before, and that was calling ToUpper on string, will suddenly fail because of ambiguity.

In more complicated scenarios (e.g. involving inheritance), it is even possible to have code that silently changes its meaning in such circumstances, e.g. when overloading methods across several classes in a hierarchy. It'd be rather convoluted code, but not out of the realm of possibility.


For C ABI, this exists since quite some time: https://abi-laboratory.pro/tracker/timeline/libevent/ (though, there is not really a way to enforce).


It's a wetware protocol, not formal, mathematically certain concept.

You also can never be certain that the car is not going to run through pedestrian crossing on red light when you step in - but it doesn't mean that traffic lights are useless.

Semver is like traffic lights. For reproducible builds/increased certainty harden it with some kind of pinning or local copy.


> * you can never be certain that the maintainer actually follows it

That's a bad argument. You can also never be certain any third-party code works at all, or did not change without changing versions, or does not have subtle bugs that surface only in your particular use case, etc. If you dismissed the whole concept on basis that somebody could fail to follow it you wouldn't be able to use any third-party dependencies at all. At which point the question of versioning is kinda moot.

> one user's breaking change is one maintainer's bugfix

This is a "grey zone" fallacy. From the fact that there might be disagreements on the margins, does not follow there is not a huge majority of clear-cut cases where everybody agrees that something is a huge or tiny change, and in this huge majority of cases it is useful. Even if there's no definition that would work in absolute 100% of cases, it works just fine in 99% of them.


> That's a bad argument. You can also never be certain any third-party code works at all

Perhaps in the philosophical sense that you can never be certain of anything, this is true. But that only place that thinking leads is to staying under the covers all day.

You actually can achieve a high degree of certainty that third-party code works to some reasonable standard by testing it. Which is the whole point - once you have tested a version, and are content that it works, you wouldn't want to blindly switch to another version without again performing a similar set of tests.


If you are pulling in 200 dependencies you're going to be in the grey zone fairly frequently.


>* you can never be certain that the maintainer actually follows it

Elm enforces semantic versioning with its type system, and one can also print the diff of values/functions between separate package versions. In principle, this is doable.


API compatibility (showing that a package contains the same functions, which accept the same kinds of inputs and produce the same kinds of outputs) is only part of the problem-- it's harder to show that the functions actually do the same thing, but that's arguably more important than API compatibility. Typically, a compiler will catch a breaking API change; a behavior change will cause problems at runtime if a function suddenly starts doing something completely different.


> it's harder to show that the functions actually do the same thing

Effectively impossible, with a Turing complete programming environment.

(“Effectively,” because technically our physical computers have a bounded number of states and inputs, thus aren’t technically Turing complete, thus Rice’s theorem technically doesn’t apply.)


There are however tools like QuickCheck (https://hackage.haskell.org/package/QuickCheck) that can bombard the function with large amounts of random (but legal) input data and makes sure it's behaving.


Yes, true. But still, having the compiler reject breaking changes when there should be none already goes a long way


> you can never be certain that the maintainer actually follows it

The same happens for the proposed model ¯\_(ツ)_/¯


I think you and many other people misunderstood my criticism, it wasn't a criticism of semantic versioning per-se, but was rather than explanation of the issues with semver, which the proposed "scheme" indeed does not fix in any way, shape or form.

The proposed author claims that "You cannot rely on the responsibility of others" but their scheme fails in the exact same way.


Yep, I've always felt versioning is more or less a form of marketing for your software. It's handy that people are kind of using convention to say things like "this is a HUGE change" vs "oops, bugfix". But it's really just conventions of communication, which, in a nutshell, comes with zero guarantee .

Semantic versioning and automatic "transitive" dependencies sure are handy, especially in that "naive" phase of development. But man, once you start having to start tracking performance and security, it's time to turn off any magic update.

Or, in other words, when you've got people depending on you, you need to start putting your security hat on and assume that there can be a breach with every change, or your performance hat and assume the system will crash due to a performance problem. No way would I want any rebuild OF THE SAME SOURCE subject to change.


I have a crazy theory about versioning, except it's not the idea itself that's crazy, but what you have to do to get it.

Software versioning is a case of the XY Problem. We don't know what we want, but we know something that might help us get it (and we are wrong).

We don't care if you add behavior to your library (unless it fulfills a need we had or didn't know we had). What we really care about is if you take behavior away. And in some cases it's not even all the behavior. If I'm only using 20% of your library and you haven't changed it, I can keep upgrading whenever.

What semantic versioning is trying and failing to achieve is to make libraries adhere to the Liskov Substitution Principle (LSP). If you had a library that had sufficient tests, then any change you make that causes you to add tests but not change an existing test means that it should satisfy the LSP (tests that correct ambiguities in the spec notwithstanding). People can feel pretty safe upgrading if all of the tests they depend on are intact.

The difficult part of this 'solution' is that reusable code would have to have pretty exhaustive test coverage. I think we are already bifurcating into a world of library authors and library consumers, so I'm okay with this. We should feel comfortable demanding more than what we currently get from our library writers. In this world, what you version is your test harness, and not your code.


> Pinning is absolutely crucial

Precisely this.

The problem isn't with versioning, it's with non-deterministic builds. Once you have a large enough project that you can no longer keep manual track of version changes, you build tooling which checks for the latest version and opens a pull request (and subsequent CI build on the pre-merge commit) with the latest version. This keeps your project up to date and prevents the woes of non-deterministic building.


> Pinning is absolutely crucial when building software from some sources out of your control.

IMHO it's a bad solution to a real problem.

Too often, I've seen software with out of date libraries or frameworks, with versions set a the start of a project and never touched once since. In some cases it even lead to security flaws not being patched.

Another side effect is that hard pinning complete versions makes for somewhat hard packaging (if you try to follow some distro guidelines (like the Debian ones), package libraries separately and not bundle everything together).

In some extreme cases, avoiding the small and incremental breaks & fixes in API can lead to code bases so far from the modern APIs of their dependencies that throwing away the code and re-implementing from scratch is actually a better solution.

I feel that was is missing in most languages is a way to properly maintain APIs (like tools native in the language that help ensure no breaks in the API contract) and language that have concepts of API versioning and API negotiation (kind of like rest API or protocol version negotiation) native to it. If this was also combined with a backward compatibility policy of "at least maintain version N + version (N-1)" and clear deprecation notifications, it would make for smoother updates and makes most systems more reliable.

I would love to see a language with these items as core features.

But we are far from this situation. What I generally do instead is to carefully chose my dependencies in projects that have a good track record of not breaking their APIs too often while being correctly maintained and also I tend to try to keep the number of dependencies in my projects to a minimum. I almost never hard pin dependency versions.

It's no wonder that the MIL-STD-498 has a large focus on interfaces (Interface Requirements Specification (IRS) and Interface Design Description (IDD) specification documents). It's actually one of the harder parts to get right when designing complex systems (including complex pieces of software).


Pinning isn't enough. People push history changes, rename things and just delete stuff all the time. I've had this happen to be more than once and would now never consider a application production ready until all dependencies are either vendored or forked.


There are plenty of strategies for making your dependencies deterministic. At Airbnb, we use caching proxies in front of all third-party language package managers, so the first time someone in the org pulls “foo 1.2.3”, the source tarball is frozen forever. The package can’t be yanked upstream, force-pushed over, etc.


This is complex vendoring, nothing more.


Yeap


why not just check in your dependencies like Firefox or chromium or WebKit. with your proxy only people using your proxy get those benefits. with checkin everyone gets those benefits.


pinning while more reliable is not actually fully reliable. If you want reliable you should be checking in copies of your dependencies either directly or as sub modules. then if you want to upgrade you check in the latest. Pinning still allows the person in control of the 3rdparty repo to mess you up. Your reproducibility is still at their mercy.


That depends on the repo. Some do not allow changes (short of the repo itself failing). Some allow only withdrawing versions, but no changes.

But yes, you should either commit deps, or have your own repo/caching-proxy which will neither change nor drop old versions.


I just use the date. Period.


In the same vein, I do not understand the popularity of semantic versioning. Why would I trust hundreds of people that their non-breaking changes are really non-breaking to me?

And suddenly, the auto-scaling fails in production because something has updated to include a new bug.


As with all things in life, semantic versioning is a heuristic for a largely stochastic process. You and only you are responsible for whether your code is broken or not. Semantic versioning is just this one standard most of us have agreed to follow as a community to make that responsibility a little easier to bear.


I think I was being unclear.

I didn't mean that semantic versioning is bad, I meant that specifying your dependencies in terms of semantic versions is bad.




Join us for AI Startup School this June 16-17 in San Francisco!

Guidelines | FAQ | Lists | API | Security | Legal | Apply to YC | Contact

Search: