Artifacts, versions and caching
Build once and promote the same artifact everywhere, version it traceably, keep builds reproducible, and speed them up with caches.
- Explain “build once, deploy many” and why artifacts are promoted, not rebuilt
- Version artifacts so every running build traces back to a commit
- Make builds reproducible with lockfiles, and fast with cache keys
At Release Night, the operations team rebuilt the app from source on the production server. Twice, it pulled a newer library version than the one tested, and the drones started delivering to the wrong hemisphere.
Build once, deploy many. The pipeline builds an artifact - a wheel, a JAR, a container image - exactly once, stores it in an artifact repository (a package or container registry such as Artifactory, Nexus, GitHub Packages or a cloud registry), and then promotes the very same artifact through dev, staging and production. What you tested is, byte for byte, what you ship. Only configuration differs between environments.
Versions you can trace
Every artifact needs a unique, immutable version that leads back to its source:
- Semantic versioning (
MAJOR.MINOR.PATCH) for things others depend on: libraries and APIs. - Add the commit SHA as build metadata -
1.4.0+3f2a9c1- or use the SHA as the image tag, so “what’s running?” has an exact answer. - Never overwrite a published version. If 1.4.0 was wrong, publish 1.4.1.
Reproducible builds
A build is reproducible if the same source always produces the same result. Pin dependency versions with a lockfile (poetry.lock, package-lock.json, requirements.txt with exact versions and hashes), pin tool and base-image versions, and avoid downloading “latest” anything during the build.
Caching
Installing dependencies from scratch on every run is slow. A CI cache saves a directory (like pip’s download cache) under a key and restores it on later runs. The key should change exactly when the cached content would: hash the lockfile.
- Exact hit: same lockfile, everything restored - the install takes seconds.
- Partial hit: the lockfile changed; restore the newest cache with the same prefix (a restore key) so most packages are already there, then save a new cache.
- Miss: start from scratch.
1import hashlib
2
3def cache_key(os_name, lockfile_text):
4 digest = hashlib.sha256(lockfile_text.encode()).hexdigest()[:12]
5 return f"deps-{os_name}-{digest}"
6
7print(cache_key("linux", "flask==3.0.3\nrequests==2.32.3\n"))
8print(cache_key("linux", "flask==3.0.3\nrequests==2.32.3\n"))
9print(cache_key("linux", "flask==3.1.0\nrequests==2.32.3\n"))deps-linux-bec5158eea64 deps-linux-bec5158eea64 deps-linux-0b76931b637a
Try it
Cache or artifact?
Caches are disposable speed-ups; artifacts are the outputs you keep, test and ship. Sort each item.
“pip’s download directory”
“The container image pushed to the registry and deployed to production”
“node_modules from the last run”
“The JUnit test report from the run”
“The built wheel `byte_bakery-1.4.0-py3-none-any.whl`”
“Docker layer cache from the previous build”
Key takeaways
Build an artifact once, store it in a repository, and promote the same artifact through every environment.
Give every artifact an immutable version that traces to a commit, like
1.4.0+3f2a9c1.Pin dependencies with lockfiles for reproducible builds.
Key caches on a hash of the lockfile, with a prefix fallback for partial hits.
Lesson quiz
7 questions · pass with 5 correct · up to 50 XP
Passing this quiz completes the lesson and keeps your streak going. Questions you miss come back in review sessions later.
Practice: automate DevOps chores in Python
Write the small Python tools DevOps teams really build - pipeline runners, plan checkers, metric calculators, scanners - and run them against sample inputs. They run locally in your browser; no servers or cloud accounts needed.
Simulate a CI cache
The first input line is the cache key prefix, like deps-linux-. Each following line is a CI run: run_id lockfile_hash. A run’s key is the prefix plus its lockfile hash.
For each run: if a cache with exactly that key exists, print run 1: hit. Otherwise, if any cache exists, restore the most recently saved one (run 2: partial (restored deps-linux-ab12)), else run 1: miss. After a partial hit or a miss, the run saves a cache under its own key. Finish with exact hit rate: 40%.
- A week of runs
- Lockfile churn
Python runs in a sandboxed browser worker with a 60 second time limit. Its runtime loads from the Pyodide CDN; your code stays in this browser.
Promote artifacts safely
Environments go dev, staging, prod. Each input line is an event:
build VERSION- builds the artifact and deploys it to dev: printbuild 1.4.0: deployed to dev.test VERSION ENV pass|fail- records a test result, only if VERSION is currently deployed in ENV: printtest 1.4.0 dev: pass, ortest 1.4.0 staging: ignored (not deployed there).promote VERSION ENV- deploys VERSION to ENV if it was built and its latest test result in the previous environment is a pass. Printpromote 1.4.0 staging: ok, orrejected (unknown artifact),rejected (not tested in dev)orrejected (tests failed in dev).
Finish with dev: 1.5.0, staging: 1.4.0, prod: none.
- Two releases
Python runs in a sandboxed browser worker with a 60 second time limit. Its runtime loads from the Pyodide CDN; your code stays in this browser.
Questions about this lesson
Stuck? Ask. Figured something out? Share it. Explaining is one of the best ways to learn.
Loading posts…