Git, CI/CD & GitOpsCXVI · Governance Without BureaucracyShiftLeft
Shifted-left controls — catching at PR time, not deploy time
What you'll learn
- Define shifted-left controls and the cost difference between PR-time and deploy-time failures
- Identify the classes of check that belong at PR time (lint, policy, secrets, signatures) and the classes that belong later (load test, chaos, soak)
- Wire a shifted-left control into the pull-request pipeline so that the merge button is the structural gate
- Keep the shifted-left check list from overwhelming the PR
Prerequisites
Verified against Git 2.55.x teaching target; 2.40+ minimum · GitHub Actions continuous service; Aug 2026 documentation baseline · Argo CD v3.5.x teaching target; v3.0+ minimum · Flux v2.9.x · Sigstore Cosign v3.1.x · SLSA v1.2 · OCI Distribution Specification v1.1 · Git LFS v3.7.1 · Kubernetes (cross-course target) 1.36.x
A shifted-left control is one that runs at PR time, not deploy time. The cost difference is the central reason: a failure caught at PR time costs the engineer a comment and a commit; a failure caught at deploy time costs the team an incident, a rollback, and a postmortem. This lesson teaches the wire-up, the classes of check that belong at PR time, and the discipline of keeping the PR fast.
The cost of where a check lives
A control that runs in the PR pipeline catches the failure before the change reaches the default branch. A control that runs at deploy time catches the failure after the change has merged, after the GitOps controller has reconciled, and possibly after the service has served traffic. The two failure modes have different costs.
flowchart LR
subgraph LEFT["PR time (shifted left)"]
A1["Author learns\nin seconds"] --> A2["Commit fix:\nminutes"]
A2 --> A3["Merge:\nminutes"]
end
subgraph RIGHT["Deploy time"]
B1["Cluster rejects\nor service fails"] --> B2["Incident opens"]
B2 --> B3["Rollback / hotfix"]
B3 --> B4["Postmortem:\nhours to days"]
end
The dollar-and-time cost:
- PR-time failure. Engineer sees a red check. The engineer reads the message, fixes the commit, pushes again. Total cost: minutes of one engineer.
- Deploy-time failure. Cluster rejects the manifest, or the service serves a 5xx. The on-call wakes. The rollback runs. The postmortem writes a timeline. Total cost: hours of one engineer plus on-call plus incident commander plus the downstream cost of the failed service.
The shifted-left control is cheaper because the cost is paid by the engineer who introduced the failure, at the moment the engineer is already at the keyboard. The deploy-time failure is more expensive because the cost is paid by everyone downstream, at a moment when nobody was planning to debug.
The classes of check that belong at PR time
Not every check belongs at PR time. The rule is: a check belongs at PR time if it can produce a verdict in seconds, and the verdict is mechanical (machine- detectable). A check belongs later if the verdict requires time, infrastructure, or judgement.
At PR time:
- Lint. Terraform
fmt, Kubernetes YAML lint, shellcheck, actionlint. Seconds. Mechanical. - Policy as code. Conftest against manifests and Terraform plans. Seconds. Mechanical.
- Secret scan. gitleaks, trufflehog. Seconds. Mechanical.
- Commit signature. git verify-commit, branch protection. Seconds. Mechanical.
- Vulnerability scan on the built image. Trivy, Grype. Tens of seconds to minutes depending on the image size. Mechanical, but slow - some teams promote this to the merge-to-default-branch step rather than every PR.
Later:
- Load test. Requires deployed infrastructure and minutes to hours of traffic. Belongs in a nightly job or a release-candidate pipeline.
- Chaos test. Requires the running system and an orchestrator. Belongs in a pre-prod environment.
- Soak test. Requires hours of traffic. Belongs in a release-candidate pipeline.
- Compliance audit. Requires the full system and a reports database. Belongs in continuous-compliance (the next lesson).
flowchart TB
PR["PR pipeline:\nshifted left"] --> L["Lint"]
PR --> PC["Policy as code"]
PR --> SC["Secret scan"]
PR --> SIG["Commit signature"]
L --> BP["Build green:\nmerge allowed"]
PC --> BP
SC --> BP
SIG --> BP
NIGHT["Nightly / RC"] --> LT["Load test"]
NIGHT --> SOAK["Soak test"]
NIGHT --> CHAOS["Chaos test"]
The shift-left discipline is to push every mechanical check into the PR pipeline and leave only the time-consuming or judgement-heavy checks for later stages. The shift is structural: the PR pipeline catches the cheap failures; the deploy pipeline handles the expensive failures only.
The wire-up
A shifted-left control is structural only when it is wired into the merge gate. The wire-up is the same as the policy-as-code wire-up: the check runs as a CI step; the step’s verdict is a required status check in branch protection; the merge button is disabled until the step passes.
# Conftest at PR time: policy against the diff
conftest test --policy policy/ manifests.yaml
# OPA at PR time: general-purpose evaluation
opa eval -d policy.rego -i input.json "data.policy.allow"
# actionlint at PR time: GitHub Actions YAML check
actionlint .github/workflows/
flowchart LR
P["Pull request opened"] --> CI["CI pipeline:\nlint, policy, secrets, sig"]
CI -->|"all green"| BP["Branch protection:\nrequired checks pass"]
BP -->|"approval + check"| M["Merge allowed"]
CI -->|"any red"| R["Author fixes,\npushes again"]
R --> CI
The branch-protection rule is the structural gate. Without the rule, the engineer can merge with a red check. With the rule, the merge button is disabled. The CI step is the mechanic; the branch-protection rule is the structure.
Production discipline
- Push every mechanical, fast check to PR time. Lint, policy, secrets, signatures. Seconds.
- Keep load, soak, chaos out of the PR pipeline. These checks belong in nightly or RC. The PR is fast or it is bypassed.
- Wire the PR check to branch protection. The structural gate is the required-checks list, not the CI step alone.
- Track the PR’s median duration. A PR median above 20 minutes is the leading indicator that the team is batching changes.
Cross-course references
- This course, Part CI (CI Performance) covers the caching, sharding, and parallelism that keep the shifted-left check list fast.
- This course, Part CIX-02 covers the Terraform
fmtandvalidatechecks that belong at PR time. - This course, Part CV-02 covers Kubernetes unsafe-prune rules that belong at PR time, not at reconcile time.
Quiz
Knowledge check · 4 questions
Q1. A team runs a 90-minute load test on every pull request. The team is annoyed: PRs sit for hours, engineers batch changes into weekly dumps, and the load test catches only configuration regressions that could be caught by a unit test. What is the structural fix?
Q2. A CI step that runs on every pull request but is not in the branch-protection required-checks list is not a structural control.
Q3. List four classes of check that belong in the PR pipeline (shifted left) and one class that does not.
Q4. Redesign the PR pipeline so the shifted-left discipline is followed and the PR remains fast.
Team T's current PR pipeline runs the following steps in order: (1) actionlint on GitHub Actions YAML, (2) terraform fmt -check, (3) Conftest policy check, (4) gitleaks secret scan, (5) Trivy vulnerability scan on the built image, (6) unit tests, (7) integration tests against a staging cluster, (8) load test with 50,000 virtual users, (9) chaos test that randomly kills a pod during the integration tests. The median PR duration is 75 minutes. Engineers batch changes weekly. The team has had two production incidents in three months that the PR pipeline could have caught.
Passing score: 75%. Answers are checked in this browser.