Versions and Artifacts: Build Once, Promote the Same Bytes
Separate SemVer and CalVer from pipeline run numbers, and carry a uniquely identified artifact through every environment.
The label must identify the actual parcel
If a courier reuses one tracking number for three parcels, the number stops being useful. The same happens when a pipeline publishes every build as latest and a deployment downloads whichever one exists at the time.
A version helps consumers reason about compatibility or release date. A build/run identifier tells you which execution produced an output. A checksum or content digest identifies bytes. These are complementary, not competing concepts.
| Identifier | Answers | Does not prove |
|---|---|---|
SemVer 2.3.1 | Intended compatibility relative to a declared API | Tests or correctness |
CalVer such as 2026.10.1 | Release time under a documented convention | Backward compatibility |
| Pipeline run ID | Which producer execution ran | Which environment received it |
| Git commit | Which source revision was selected | Reproducible dependency resolution |
| Artifact digest | Whether bytes match | Whether those bytes are safe |
CalVer has several valid conventions; define yours rather than assuming every date-like label has the same meaning.
Choose compatibility rules intentionally
Under SemVer, a normal MAJOR.MINOR.PATCH version communicates incompatible API changes, backward-compatible additions, and backward-compatible fixes respectively, within the specificationβs assumptions. Version zero and prereleases have additional caveats. A number is only useful if the publisher follows its contract.
Dependency ranges trade update convenience against control. A lockfile records a resolved graph for supported package managers, while a range expresses permitted versions. Commit the appropriate lockfile and use the package managerβs reproducible installation mode in CI, such as npm ci for a project with a valid npm lockfile.
Locking does not remove the need for updates. Use reviewed dependency updates, compatibility tests, and vulnerability response rather than letting an unreviewed restore silently choose a new production dependency.
Do not assume SemVer build metadata is supported by every registry format. Azure Artifacts Universal Packages, for example, have their own version restrictions. Validate the package format before turning a pipeline string into a publish command.
Carry provenance through a pipeline
For an illustrative Azure Pipelines run, a unique human-readable run name can include date and revision:
name: $(Date:yyyyMMdd).$(Rev:r)
steps:
- script: npm ci
displayName: Restore locked dependencies
- script: npm test
displayName: Test
- script: npm run build
displayName: Build
- publish: dist
artifact: booking-web
This example assumes an existing Node toolchain, lockfile, test/build scripts, and a dist output. It is not a universal build for every repository. Rev uniqueness is tied to the run-number formatβs changing components; the numeric pipeline run ID is still the robust execution reference.
Publish a manifest with the source commit, producer run ID, dependency lock identity, package/container version, and digest where applicable. Keep configuration names, not secret values, in that manifest.
In a dependent job, downloading the current runβs named artifact keeps build and release connected:
steps:
- download: current
artifact: booking-web
For a separate release pipeline, select the exact producer pipeline and run, not simply βlatest successfulβ at deployment time. Otherwise test and production can receive different outputs while displaying the same release name.
Separate environment configuration from binaries
Build once, then apply approved environment-specific configuration at deployment/runtime. Rebuilding for production can change dependencies, timestamps, or generated content and invalidates the claim that production received the tested artifact.
Some applications embed environment configuration during compilation. If so, state that constraint and test the actual resulting production artifact; do not claim byte-for-byte promotion when the process deliberately rebuilds.
Container tags can be mutable. A deployment pinned to a digest has a stronger content identity than a tag that can move. A digest still needs provenance and scanning.
Staging tested artifact from run 410. Production later downloads 'latest successful', now run 412. What is the defect?
Try it: explain an artifact without opening its source
Prerequisites: a small local project with a lockfile and build output, or read access to an existing sandbox pipeline. CI and registry storage may be billed if you run them.
Create a simple release manifest from actual values in your sandbox: commit, run ID if available, version convention, and SHA-256 of one output file. Repeat the hash without rebuilding. Then identify where a deployment would select that exact output. Do not invent a run ID if no pipeline ran.
Expected observation: the hash is stable for unchanged bytes, and the manifest distinguishes source identity, execution identity, compatibility label, and deployment destination.
Cleanup: remove only your local exercise manifest/output if not needed. If you published a test artifact, use the platformβs run-retention process; do not delete a shared release run.
Decision check: can we delete the producing run?
Azure Pipeline artifacts are associated with their run. Deleting the run deletes those artifacts, so preserve required release outputs using an appropriate retention strategy before deleting evidence.