Skip to content

Source and Artifact Management

With a fresh cup of coffee to eradicate the morning fog, a user visits our download page to install the latest version of our product. Using their browser's meta information we serve the version of our software we're confident the user is looking for. They download the installer, run it, and launch the application. Half asleep, our user made a mistake and clicks a "Help" button which opens a section of our documentation in their browser.

What seems like a straightforward process requires some management on our side. When building software, we consume and create a cornucopia of interdependent artifacts. Depending on our product distribution, a single release may include executable binaries for multiple operating systems, static or dynamic libraries, container images, infrastructure deployment manifests, documentation, test fixtures for integration and end-to-end tests, licenses, code signatures, and compliance certifications.

The previous chapter covered how to trigger a release. Tagging a commit launches our build pipeline and produces our release artifacts. Building a release often takes a longer time than building a development version, as we validate the generated outputs with more scrutiny and notarize and sign the binaries. Once every step completes, we store the resulting artifacts and associate them with the release version.

Every release artifact represents a specific state of the source code. When customers report defects, we must be able to identify the exact revision from which the release was built to reproduce the issue. Likewise, security audits, compliance reviews, and forensic investigations require access to the precise source code associated with a released version.

The complexity of our systems and the tools already in use shape how we store our artifacts. We can manage them using our existing source control tools, e.g. in GitHub or GitLab we attach dedicated release assets to a release, where as teams using Perforce may store generated artifacts dedicated depots or as ordinary file revisions. Larger organizations default to dedicated artifact repositories such as Artifactory to manage access and meta data.

After creating the artifacts, we make them available to customers. Our release process moves the artifacts from internal storage to public storage and replaces all public download links and documentation with the latest content. Simultaneously, we provide long-term access to previous versions. Each historical release requires a permanent reference to its complete set of artifacts, e.g. the appropriate documentation, licenses, signatures, and other aggregate assets.

In addition to official releases, we need a way to share work-in-progress with selected customers. These preview builds pass through the same rigorous build pipeline but are not published for general availability. Instead, we distribute them through semi-public download links so customers can verify bug fixes, evaluate new features, or confirm that an issue has been resolved before the next official release.

Products often rely on third-party distribution channels. Our release pipeline updates these alongside our own download page. Whether we publish through package managers such as Homebrew, Chocolatey, or APT, or distribute container images through Docker Hub or language packages through the respective package managers, every distribution channel should be updated in lockstep with the official release.

Semantic Versioning

A release binary carries no inherent record of what changed since the previous version. We attach metadata to every release, the most common being a version number.

In the previous chapter we built a branching strategy to support multiple releases in parallel. We tagged every release via a pattern akin to <type><year>.<increment>, e.g. v2026.4. While this pattern helps users map the releases to their specific product lines, it gives little insight to our product changes other than that it's the fourth release of 2026.

To provide insights about behavioral changes of a release, our industry converged towards Semantic Versioning (SemVer). This versioning practice reflects the impact of the changes for backwards compatibility. A SemVer standard consists of increments across three levels of severity, major.minor.patch (e.g. v1.23.45). This pattern allows our users to infer the type of changes since they last updated the product.

We increment the major version for breaking changes. If we modify an API signature, edit the serialization schemas, or remove a command flag, we bump the first number in the release pattern. Separating change impact from change volume seems irregular for first-time users of SemVer. Movie and video game sequels have taught us that an increment of the "big" number indicates a major content release. Such is not the case for SemVer. Renaming a flag from --max to --limit in a CLI command is a narrow scoped change, which breaks every script that uses the flag. Our users need to be aware that they may need to update their code when bumping this dependency.

When we add new functionality or extend existing functionality without changing existing behavior, we increment the minor version. This indicates to our users that the product is generally safe to upgrade without worrying about their existing infrastructure. Users can make use of the latest changes and security upgrades. An important distinction to make for SemVer is that it's standardized across backwards compatibility, not forwards compatibility.

This means, that we guarantee a user can upgrade from v1.2.3 to v1.5.0 without requiring any changes on their side. We do not guarantee that data written by v1.5.0 can be read by v1.2.3. While this seems clear in isolation, it becomes important when considering any kind of migration between minor versions. If we migrate our schemas for the additional functionality of the new version, we either test against forwards compatibility before releasing our update or make the user aware of omnidirectional nature of the update.

Incrementing the patch number communicates that the only changed functionality was an unwanted one. We fixed a bug. No other change made it into the release. Not only are patches safe to update, we encourage the updates for security and reliability.

When we update the major or minor version, we reset the right numbers to zero. E.g. given the base version v1.2.3 incrementing the major number results in version v2.0.0, whereas a minor bump results in the version v1.3.0.

Besides the triad of SemVer numbers, we append the commit hash or the changelist number of the version we built the release with. The pattern v1.23.45.a6fh79 stamps the state of the source code directly into the version number, rather than relying on any mapping stored in a database.

Changelogs

Knowing that a release contains breaking changes is a good start. Knowing what changed is better. When describing changes, we distinguish between two artifacts: changelogs and release notes.

A changelog is an engineering artifact. It lists every change included in a release, typically by collecting all commits since the previous version together with their commit messages. We often enrich these entries with internal metadata such as Jira issues, pull requests, bug reports, and other references that help future engineers understand the reasoning and origin of a code change.

Generating changelogs becomes almost effortless when developing trunk-based. At release, we collect the titles and descriptions of all merged integrations and assemble the complete history of the changes.

Internally, teams often accumulate many builds that differ only by their modification timestamp, making it difficult to determine which binary contains a particular feature or bug fix. A changelog provides an authoritative mapping between a build and its contents, making verification and troubleshooting easier.

Release notes serve a different audience. They are customer-facing documents that undergo copy editing, become part of the product documentation, and may even be repurposed by marketing. Engineering-focused products sometimes publish their changelog as their release notes, but consumer products often prioritize readability and personality over completeness.

Slack is a well-known example:

Slack 4.49.89 — April 28, 2026

Bug Fixes

Everything is an itty-bitty bit better than it was before. Trust us.

These copy-written notes can be charming when our shipped product has little flaws and its updates introduce next to none-. Mature products with stable platforms, dedicated support organizations, and enterprise customers can afford playful release notes as long as anyone who needs technical detail has another place to find it.

New products should be more cautious. Early adopters evaluate a software and generally value clarity over wit. Until well established, we prefer to appear competent than quirky.


  • Good read? Unlock the rest of the chapter!

    Engineering Collaboration is currently available as an Advanced Reading Copy for select readers.

    Get in touch with the author