Please be mindful of version bounds and semantic versioning when tagging your packages

In order for packages to be automatically merged into the general registry, it is required that the package’s Project.toml contains a compat section giving upper version bounds on all direct dependencies.

I wanted to bring a few concerns to the forefront of the minds of package developers, so that we don’t inadvertently get burned by this practice:

Don’t tag your new version as containing breaking changes unless it really does

For versions ≥ v"1.0", any time there are version differences of at least v"1.0" it implies there are breaking changes. For versions < v"1.0", any time there are version differences of at least “v"0.1” it implies there are breaking changes. In the Julia ecosystem, the overwhelming majority of packages have versions < v"1.0". I know it is tempting to bump the middle version number if there is some significant internal change. It seems to me that this indeed happens sometimes, and I’m probalby guilty of this myself. If you do this, every package that depends on yours will stop using your latest tags by default, so you should only bump this number if it really is necessary because of some breaking change to the external API of your package.

When incrementing the version number, consider testing packages that use yours as a dependency

It is frequently the case that an author makes breaking changes, but that these are minimal and there’s a good chance that some packages won’t break. Again, if you change the version to indicate a breaking change, initially not much will actually use it by default. If you want people to actually use your new tag, it might require you to test some packages you may be aware of that use your package. Authors of other packages probably aren’t going to be overly aware of your version history, so you’ll be helping them out if you can check whether your package still works. Of course, I’m not suggesting that packages that depend on one’s package are the responsibility of that package’s author, but there will be many cases in which that author is aware of some of the major use cases. This sort of practice is a good complement to internal unit tests anyway.

Consider having a clear separation between unit tests of the external API and those of package internals

The former can be used to assess breakage.

check your package’s compat section (at least) every time you tag a release

This is probably the most important point. If you forget about this, your package will be stuck downgrading everything to old packages forever. When developing your code, I strongly recommend commenting out the compat section entirely so that you are running on all the latest releases. Then, when it comes time to tag a release, you can check what versions of everything is getting used and put them in.

I think everything I said here is pretty uncontroversial, I’m sure I’ll hear about it if I’m wrong.

25 Likes

Is there a standard/efficient way to discover what registered packages depend on a given package?

4 Likes

Is there a way to specify all versions in a range, like 0.2 <= v < 1.0? I find the docs here baffling (is "0.2" different from "^0.2"?) but I begin to think they are telling me I must write them all out, "0.2, 0.3, 0.4, ..." by hand?
https://julialang.github.io/Pkg.jl/v1/compatibility/index.html

2 Likes

This whole auto-merging idea seems like a not very well thought out process yet at the moment.

For example, it is incorrect to specify upper bounds for dependencies until it is known at what version number there will actually be a compatibility issue. This cannot be predicted until it happens.

That’s why I think it is a terrible idea to require upper bounds on all dependencies.

For example, I will not be using the auto-merging on any of my packages, because I refuse to specify any upper bounds for dependencies unless I actually anticipate an incompatibility.

I have tagged a minor release on Thursday and it still hasn’t been merged 4 days later, I understand the maintainers there are very busy, but I simply can’t rely on the automatic merging with these policies.

4 Likes

That’s only true if people do not strictly adhere to semantic versioning. Indeed, though most people are pretty diligent, this still doesn’t always happen, so in practice I don’t completely disagree with you.

It’s also been pointed out that it’s hard to come up with a strict definition of “breaking”, and I don’t disagree with that either.

1 Like

No, you are wrong about your statement. I am strictly adhering to the semantic versioning in fact.

Just because there is a breaking change in a new release doesn’t mean that my package, which depends on that new release, will be affected. So it could be that you made a breaking change, but my package is not at all affected by it. This is why it is incorrect to require upper bounds in all cases.

So, somebody could be strictly adhering to semantic versioning, and also not be affected by a specific breaking change, so no new upper bound is needed in that case. Only packages which are affected by the actual breaking change need the new bounds.

I will only change the bounds if I am actually affected by those breaking changes… that’s why in general it is correct to not have any upper bounds at all, unless you have a specific incompatibility.

1 Like

I think the reasoning is that if there are breaking changes anywhere in the API, it’s better to declare the new version incompatible than risk it, especially since in general something could fail silently or give unexpected behavior.

Admittedly I’m playing devil’s advocate here: I tend to agree with your thinking on this, I don’t really like putting upper bounds in unless they are absolutely necessary. To be honest, I’m a little worried about what things will be like a year from now when everyone is doing this, neglecting to update things and there are inreconcilable dependencies all over the place.

However I started this post with the intent of giving helpful, uncontroversial advice about what people can do given the status quo, so I don’t want to completely derail myself from that goal.

1 Like

It’s going to be a mess, that’s my prediction. This is going to make [compat] more complicated.

I foresee that this will create more problems than it solves, but that’s just my humble opinion.

1 Like

MyPkg = 0.2 is not different than MyPkg = ^0.2 and both mean any release with version >= 0.2 and < 0.3, since that this the set of packages fully compatible with 0.2 according to (Julia’s interpretation of) semvar, meaning if MyPkg is your dependency and the resolver upgrades it to any version in that range, your package should still work. In the same way, MyPkg = 1.2 (which is the same as MyPkg = ^1.2) means any version >= 1.2 and < 2.0, since those are the versions fully compatible with 1.2, and any package that works when its dependency MyPkg has version 1.2 should continue working when it gets upgraded to anything in that range.

If your package is compatible with multiple breaking releases, then I believe you do have to write them out. Note 0.2 and 0.3 could have 100% different APIs, so there isn’t a way to know (edit: from the version number alone) that your package is compatible with both (unlike 1.2 and 1.3 for which if it works with 1.2 it should work with 1.3 too).

Then why do both exist, why does the documentation contain both?

(Edit: I see now that it does say they are equal. But why not delete every single ^?)

Not an expert but imposing these upper bounds on a world where almost every package is 0.X seems at best premature. I just spent an hour trying to figure out what’s blocking some upgrade, and I can’t.

1 Like

^ is just the default; something that can be a bit confusing is that MyPkg = "1.0" is different from MyPkg = "=1.0" which means precisely version 1.0.

I think it’s good that we push people to adding compat bounds, because without it semvar is kind of useless. If I want to make a breaking change to my package and not break downstream packages, I can only safely do so if the downstream packages are using compat bounds.

By the way, I agree the registration process and it’s bounds requirements are a bit confusing (edit: actually re-reading what you wrote, you didn’t say the bounds were confusing, just hard to find what is blocking something-- didn’t mean to misquote you in my rush to plug my PR :wink: ). I hope my PR here will eventually help make the process less confusing: https://github.com/JuliaRegistries/General/pull/3567.

1 Like

Since we (Pkg) consider minor versions breaking pre 1.0 there is not really a point in beeing able to specify 0.2 <= v < 1.0.

It is correct by definition; we have to assume that the next breaking (according to semver) release is actually breaking.

Exactly. So why YOLO it? It’s like not putting on your safety belt when driving because “How can I know I will crash and die otherwise on this new route?”. Thats not what you do; you put on your safety belt, and then if it turns out that specific route is safe, then you can relax. But there is NO way you can know in advance.

… So you are telling me you will go back and edit all your previous releases?

If you are, then whats the problem of adding the bounds?

It is not incorrect. At the time of the release it is the most correct thing to do, you can not verify that the next release will not break. The only logical assumption, however, is that it indeed will break things.


The only logical thing here is to assume that the next breaking (according to the semver version) release is actually breaking. If it turns out that it isn’t, then we can relax the bounds. This way we don’t need to deal with things like this: Put an upper bound on HTTP version in Coverage <= v0.8.0 by martinholters · Pull Request #21711 · JuliaLang/METADATA.jl · GitHub, add upper bound to widgets dependency by piever · Pull Request #21178 · JuliaLang/METADATA.jl · GitHub, Upper bound NNlib for CuArrays by MikeInnes · Pull Request #20147 · JuliaLang/METADATA.jl · GitHub, Upper bound MatrixDepot in LightGraphsExtras by andreasnoack · Pull Request #19874 · JuliaLang/METADATA.jl · GitHub, BlockBandedMatrices upper bound by dlfivefifty · Pull Request #19766 · JuliaLang/METADATA.jl · GitHub, Upper-bound StaticArrays for BEAST by timholy · Pull Request #19688 · JuliaLang/METADATA.jl · GitHub (and Pull requests · JuliaLang/METADATA.jl · GitHub for more).

16 Likes

They just happen to mean the same thing pre-1.0.

2 Likes

They mean the same thing always, right? (edit: it’s ~ and ^ that are the same pre-1.0, not nothing vs caret, right?)

Yea, sorry, thought we discussed tilde vs caret.

1 Like

I am not YOLO’ing or whatever that means… instead of blindly putting bounds on packages (which i consider reckless and careless), I am actually paying attention to my compatibility issues.

Exactly correct, once you specify an upper bound (and it was previously unbounded) then it retroactively bounds the previous versions as well. This is my exact reasoning, you are correct.

This doesn’t require extra work, as it is automatic already.

The problem is that it is incorrect to specify a bound unless there is an actual incompatibility.

It is incorrect, especially if I specifically checked it myself and know it better than a automated bot. The automated bot doesn’t have more knowledge than I do in that case.

I hope that you will not refuse to merge my release just because of this.

OK I find the docs pretty confusing, and will try to improve them when I get a minute.

But when stuck in a corner you can’t reverse out of, is there some way to turn down strictness? Force pin some packages & ignore their constraints? Short of dev-ing them & deleting the [compat] by hand. If only to try to discover what works & what needs to be fixed.

Thats exactly what you are not doing, but OK.

This is not how it works though. What will happen is that the resolver will chose an old version (where you did not have the bound) and downgrade to that one.

Tell me, how can you know there won’t be an incompatibility?

Checked what? There is nothing to check, the next release is not released yet, you can not check against it.

Not sure what you are implying here.

2 Likes

You are wrong, I have previously tagged releases in the new registry and have seen what happens.

What happens is that in the registry all previous version get the bounds added. Try it, you’ll see.

Since the registry entries get updated, when somebody updates their registry, they get the bounds.

By doing tests and taking a look at the breaking changes to see what changed.

That’s the whole point… you are just guessing, which is reckless. You literally want people to guess when there might be an incompatibility. This is to me the definition of reckless and careless.

I’m not wrong.

But there is nothing to test, there is no new release to test against.

I mean, it’s a qualified guess. The only logical guess. We have to assume that if the package author releases a breaking (according to semver version) release it will break our package.

Whats so terrible with actually waiting and testing against the new release when it is released?

7 Likes