DevOps · lesson 1 of 8
Intermediate
7- 2 Choosing a CI/CD Toolchain: The Same Weighted Framework as Choosing a Cloud
- 3 CI/CD Pipeline Patterns: Sequential, Parallel, and Staged
- 4 Deployment Strategies Compared: Rolling, Blue-Green, and Canary
- 5 GitOps in Practice: Git as the Source of Truth
- 6 Computing DORA Metrics From Your Own Git History
- 7 Terraform vs. Ansible: A Weighted Decision Framework
- 8 Observability Synthesis: Logs, Metrics, Traces, and Alerts as One Signal
Also part of DevOps
12- + PR Workflow: Rebasing, Squash Merge, and Merge Commits from Git & GitHub
- + GitHub Actions: Workflows, Jobs, and a Real Failing Step from GitHub Actions
- + Jenkins CI: Pipeline Basics from Jenkins
- + Bash DevOps Scripts from Bash
- + Introduction to Docker from Docker
- + Deployments and Rollouts from Kubernetes
- + Terraform Fundamentals (IaC) from Terraform
- + Terraform CI/CD with GitHub Actions (plan/apply) from Terraform
- + Ansible Idempotency, Verified: Run It Twice and Watch changed=0 from Ansible
- + Prometheus Scrapes You — It's Pull, Not Push from Monitoring & Observability
- + A Firing Alert Doesn't Mean Anyone Was Notified from Monitoring & Observability
- + A CI Security Gate: Gitleaks, Bandit and pip-audit in One Workflow from DevSecOps (AppSec in the pipeline)
What DevOps Actually Is, Measured From a Real Repo
DevOps isn't a tool or a job title — it's practices that show up as a measurable number. Compute deployment frequency from two real git histories and the difference between them is the whole definition.
By SkillByExample · Published
Ask ten people to define DevOps and you’ll get ten answers. Compute one number from two real git histories and the difference explains itself.
A slow-releasing team’s real history
git for-each-ref --sort=creatordate --format="%(creatordate:short) %(refname:short)" refs/tags
2026-07-01 v0.1.0
2026-07-15 v0.2.0
2026-07-29 v0.3.0
2026-08-12 v0.4.0
Four real tags, each created by an actual commit at an actual point in time — not a description of a release cadence, the release cadence itself, as it actually happened.
Measuring deployment frequency
# deploy_frequency.py
import subprocess
from datetime import date
output = subprocess.run(
["git", "for-each-ref", "--sort=creatordate", "--format=%(creatordate:short) %(refname:short)", "refs/tags"],
capture_output=True, text=True, check=True,
).stdout.strip()
dates = []
for line in output.splitlines():
date_str, tag = line.split(maxsplit=1)
y, m, d = map(int, date_str.split("-"))
dates.append((date(y, m, d), tag))
print(f"Releases found: {len(dates)}")
for d, tag in dates:
print(f" {d} {tag}")
gaps = [(dates[i][0] - dates[i - 1][0]).days for i in range(1, len(dates))]
avg_gap = sum(gaps) / len(gaps)
print(f"\nGaps between releases (days): {gaps}")
print(f"Average days between deploys: {avg_gap:.1f}")
print(f"Deploys per week: {7 / avg_gap:.2f}")
python deploy_frequency.py
Releases found: 4
2026-07-01 v0.1.0
2026-07-15 v0.2.0
2026-07-29 v0.3.0
2026-08-12 v0.4.0
Gaps between releases (days): [14, 14, 14]
Average days between deploys: 14.0
Deploys per week: 0.50
Half a deploy per week — a release every two weeks, like clockwork. Not good or bad on its own yet; it’s a baseline to compare against.
The same script, a different real repo
git for-each-ref --sort=creatordate --format="%(creatordate:short) %(refname:short)" refs/tags
2026-08-01 v1
2026-08-02 v2
2026-08-03 v3
2026-08-04 v4
2026-08-04 v5
2026-08-05 v6
python deploy_frequency.py
Releases found: 6
2026-08-01 v1
2026-08-02 v2
2026-08-03 v3
2026-08-04 v4
2026-08-04 v5
2026-08-05 v6
Gaps between releases (days): [1, 1, 1, 0, 1]
Average days between deploys: 0.8
Deploys per week: 8.75
Same script, unmodified, run against a different repo’s real tag history: 8.75 deploys per
week versus 0.50 — a 17x difference, including one day (Aug 4) with two separate
releases. That gap is what “DevOps maturity” actually looks like as a number, not an adjective.
Nothing about job titles changed between these two repos; what changed is how small a unit of
work gets shipped, and how much automation exists to ship it safely at that frequency.
Practice
1. Compute the total time span covered by the slow team's releases.
first, last = dates[0][0], dates[-1][0]
print(f"Span: {(last - first).days} days across {len(dates)} releases")
Span: 42 days across 4 releases
42 days, 4 releases — confirms the same thing the average gap already showed, from a different angle: three intervals of exactly 14 days each, spanning six weeks total.
2. Handle a repo with only one tag, where no gap can be computed.
if len(dates) < 2:
print("Not enough releases to measure a gap yet.")
else:
# existing gap calculation
pass
Not enough releases to measure a gap yet.
A real edge case worth guarding: zip-style gap calculations over a list of length 1 (or 0)
either error or silently produce an empty result — checking len(dates) < 2 first gives a
clear message instead of either failure mode.
3. Compute deploys per week for the fast team using the total span instead of averaging gaps.
first, last = dates[0][0], dates[-1][0]
span_days = (last - first).days
print(f"Alternate calc: {len(dates) - 1} intervals over {span_days} days = {7 * (len(dates) - 1) / span_days:.2f} deploys/week")
Alternate calc: 5 intervals over 4 days = 8.75 deploys/week
Same 8.75 result via a different formula (total intervals over total span, rather than
averaging individual gaps) — a useful sanity check that the average-of-gaps method isn’t
hiding a calculation mistake.
Next: a decision framework for choosing a CI/CD toolchain, the same weighted-scoring approach
cloud/choosing-a-cloud.md used for picking a cloud provider.