Semantic versioning

In the previous article, we talked about how we can publish a swift package, and how we version it using semantic git tags. But what does semantic mean? In this article, we’ll explore this subject in more detail.

The basics

A library’s public API is all the symbols (e.g: types, variables, and functions) used by its clients via import. When we depend on a library, we also depend on its public API to accomplish a task. This dependency makes part of our code coupled to it.

Imagine we’re using a library from a popular Swift package. What would happen if the maintainers released a new version with huge changes to its public API? Our code would break. To avoid this issue, we could rely on a single version. But this means we would be locked in it, incapable of updating the package without updating our code. To make things worse, imagine each package owner has a different way of handling versions. These scenarios are what we call dependency hell.

Semantic versioning is a specification aimed at putting an end to dependency hell. It divides versions into three numbers: major, minor, and patch (e.g. 1.5.8). Here’s what each number mean:

  • Major means the public API has changed, requiring our code to be updated
  • Minor means new code was added to the public API, the additions don’t break existing integrations (backward compatible)
  • Patch means bugs were fixed without changing the public API

Selecting package versions

When adding a Swift package, the recommended approach is to select a version using the up to next major dependency rule:

Use the Up to Next Major version.

It means our project we’ll receive updates that don’t break our code. Select the Up to next minor rule if you only wish to receive bug fixes.

When declaring dependencies on the package, we can also specify those same version rules:

// ...
dependencies: [
    .package(
        url: "https://github.com/apple/swift-algorithms.git",
        .upToNextMajor(from: "1.0.0")
    )
],
// ...

Guidelines

Besides defining how to organize our versions, the specification also comes with some important guidelines:

Development versions (below 1.0.0)

Every version below 1.0.0 is considered unstable and under development. The public API isn’t defined yet and can change drastically.

The initial version (1.0.0)

Once the public API is defined, it’s time to release the initial version of the package. The public API is considered stable and set in stone.

Never modify a released version

If you wish to correct something, release a new version on top of the one with errors.

Use a development branch

You don’t need to release every single change you make. We can release multiple changes using a single version. Having a development branch allows developers to decide when to release a batch of changes as a single major, minor, or patch.

Pre-releases

We can also pre-release versions. This is useful for testing features as they get added. They follow the same versioning rules but have a specific format in the end:

1.2.5-beta1
1.2.5-1
1.2.5-alpha-633

A pre-release doesn’t need to be stable. Use it only to test a set of features while they are being developed. Pre-releases have less precedence over official versions:

1.2.5 > 1.2.5-beta1

Usage with SPM

On the package side, to specify a pre-release, we use the Version type:

// ...
dependencies: [
    .package(
        url: "https://github.com/apple/swift-algorithms.git",
        from: Version(1, 2, 5, prereleaseIdentifiers: ["alpha", "1"]))
],
// ...

The above code specifies a range of versions: 1.2.5-alpha-1 ..< 2.0.0.

On the project side (using Xcode 13.2.1), we can include the pre-release identifiers directly:

1.2.0-beta.1 ..< 2.0.0

If we include the pre-release identifiers, SPM will fetch the versions automatically when we update our packages.

Keep a changelog

It’s important to document every version being added. The documentation is added to a file called CHANGELOG.md. It contains the history of your project, with relevant information on what was added, removed, or changed. Here’s an example from the Swift-Algorithms package.

For more information, visit this link.

Closing thoughts

If you wish to correctly use dependency managers, make sure to understand what semantic versioning is. We’ve explored what problems it solves, and also some of its main guidelines. We also understood what a CHANGELOG file is, and how we can use it.

In the next article, we’ll talk about building executables with SPM. We’ll also take a look at its command-line interface.

One thought on “Semantic versioning

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s