Jenkins vs CircleCI: The Landscape in 2026
The CI/CD ecosystem has evolved dramatically. Jenkins, the 20-year-old automation veteran, and CircleCI, the cloud-native contender, now represent two fundamentally different philosophies of how software delivery pipelines should be built. By 2026, the conversation has shifted from "which tool is better" to "which tool aligns with your team's operational model." This tutorial walks you through every facet of the comparison, with practical code examples you can run today.
What You'll Actually Build While Reading
By the end of this guide, you'll have constructed identical pipelines on both platforms — a multi-service application with testing, security scanning, and deployment stages — so you can feel the ergonomic differences firsthand.
Part 1: Understanding the Core Philosophies
🚀 Deploy your AI agent in 10 minutes
Managed Hermes hosting. Zero DevOps. 100M tokens/mo included.
Try it free →Jenkins: The Extensible Automation Server
Jenkins is not merely a CI tool. It's a general-purpose automation engine running on the Java Virtual Machine, with a plugin ecosystem exceeding 1,800 extensions. In 2026, Jenkins remains the dominant choice for organizations with complex, heterogeneous pipelines that span on-premise hardware, legacy systems, and custom security requirements. Its controller-agent architecture allows you to distribute workloads across any compute resource you own — bare metal, VMs, Kubernetes pods, or even mainframes.
The key mental model: Jenkins gives you absolute control at the cost of configuration complexity. Every behavior — from how artifacts are stored to how secrets are injected — is something you explicitly define.
CircleCI: The Managed Execution Platform
CircleCI takes the opposite approach. It provides opinionated, hardened execution environments where your pipeline runs in disposable containers or VMs managed entirely by the platform. By 2026, CircleCI has doubled down on its "serverless CI" vision: you supply the configuration, and CircleCI handles scaling, secret management, caching, and flaky-test detection automatically. There is no controller to maintain, no plugin compatibility matrix to wrestle with, and no Java heap to tune.
The key mental model: CircleCI optimizes for developer velocity by removing infrastructure decisions from your critical path. You trade control for operational simplicity.
Part 2: Why the Distinction Matters in 2026
The stakes have changed. Modern pipelines must handle:
- Supply chain attestation — generating SLSA provenance metadata that proves what went into a build
- Policy-as-code — enforcing deployment gates via OPA or Kyverno rules before any release reaches production
- Fleet-wide caching — sharing build caches across hundreds of microservices without poisoning
- Ephemeral compute — running sensitive workloads in short-lived, sandboxed environments that disappear after execution
Your CI platform either accelerates or obstructs these requirements. Choosing incorrectly means retrofitting capabilities the platform was never designed to support.
Part 3: Hands-On Pipeline Construction
We'll build a realistic pipeline for a polyglot application consisting of a Go API service and a Node.js frontend, both residing in a monorepo. The pipeline must: run tests in parallel, perform a Trivy vulnerability scan, build Docker images, push to a registry, and deploy to a Kubernetes cluster with an approval gate.
3.1 The Jenkins Pipeline (Declarative Syntax)
Jenkins pipelines are defined in a Jenkinsfile at the repository root. The declarative syntax, extended with Shared Libraries for reusable logic, is the modern standard for 2026.
// Jenkinsfile
pipeline {
agent none
environment {
// Injected from Jenkins credential store via withCredentials
REGISTRY = 'ghcr.io'
DOCKER_HOST = 'unix:///var/run/docker.sock'
}
parameters {
choice(name: 'ENVIRONMENT', choices: ['staging', 'production'], description: 'Deployment target')
booleanParam(name: 'SKIP_SCANS', defaultValue: false, description: 'Skip vulnerability scanning')
}
stages {
stage('Parallel Testing') {
parallel {
stage('Go API Tests') {
agent {
label 'go-runner'
}
steps {
sh '''
cd api
go test -v -coverprofile=coverage.out ./...
go tool cover -func=coverage.out
'''
publishHTML(target: [
allowMissing: false,
always: true,
reportDir: 'api',
reportFiles: 'coverage.html',
reportName: 'Go Coverage Report'
])
}
}
stage('Frontend Tests') {
agent {
label 'node-runner'
}
steps {
sh '''
cd frontend
npm ci
npm run test -- --coverage
npm run lint
'''
junit 'frontend/junit.xml'
}
}
}
}
stage('Security Scanning') {
when {
expression { !params.SKIP_SCANS }
}
agent {
label 'security-scanner'
}
steps {
script {
def imageTag = "${env.BUILD_TAG ?: 'latest'}"
sh """
trivy image --severity HIGH,CRITICAL --ignore-unfixed \
--format sarif --output trivy-results.sarif \
${REGISTRY}/myorg/api:${imageTag}
"""
// Upload SARIF to GitHub Security tab
uploadSarif 'trivy-results.sarif'
}
}
}
stage('Build & Push Images') {
agent {
label 'docker-builder'
}
steps {
script {
def version = sh(
script: 'git describe --tags --always --dirty',
returnStdout: true
).trim()
parallel([
api: {
sh """
cd api
docker build -t ${REGISTRY}/myorg/api:${version} .
docker push ${REGISTRY}/myorg/api:${version}
"""
},
frontend: {
sh """
cd frontend
docker build -t ${REGISTRY}/myorg/web:${version} .
docker push ${REGISTRY}/myorg/web:${version}
"""
}
])
// Store version as a build artifact
writeFile file: 'version.txt', text: version
archiveArtifacts artifacts: 'version.txt'
}
}
}
stage('Approval Gate') {
when {
expression { params.ENVIRONMENT == 'production' }
}
steps {
input message: 'Deploy to production?',
ok: 'Proceed',
submitter: 'ops-team,jane-doe'
}
}
stage('Deploy to Kubernetes') {
agent {
label 'k8s-deployer'
}
steps {
script {
def version = readFile('version.txt').trim()
def namespace = params.ENVIRONMENT
sh """
kubectl set image deployment/api \
api=${REGISTRY}/myorg/api:${version} \
-n ${namespace} --record
kubectl set image deployment/web \
web=${REGISTRY}/myorg/web:${version} \
-n ${namespace} --record
kubectl rollout status deployment/api -n ${namespace}
kubectl rollout status deployment/web -n ${namespace}
"""
}
}
post {
failure {
script {
sh 'kubectl rollout undo deployment/api -n ${params.ENVIRONMENT}'
sh 'kubectl rollout undo deployment/web -n ${params.ENVIRONMENT}'
}
}
}
}
}
post {
always {
// Collect pipeline telemetry
script {
def duration = currentBuild.duration / 1000
sh "echo Pipeline duration: ${duration}s"
}
cleanWs()
}
success {
// SLSA provenance generation
script {
writeFile file: 'provenance.json', text: """
{
"buildConfig": "${currentBuild.buildId}",
"commit": "${GIT_COMMIT}",
"timestamp": "${new Date().toISOString()}",
"builder": "Jenkins-${env.JENKINS_VERSION}"
}
"""
archiveArtifacts artifacts: 'provenance.json'
}
}
}
}
3.2 The CircleCI Configuration
CircleCI configuration lives in .circleci/config.yml. The platform uses "executors" (reusable execution environment definitions) and "orbs" (packaged, shareable configuration bundles) to reduce repetition.
# .circleci/config.yml
version: 2.1
# Orbs abstract complex logic into parameterized commands
orbs:
trivy: aqua/trivy@3.2.1
kubernetes: circleci/kubernetes@2.1.0
snyk: snyk/snyk@1.9.1
# Executors define reusable environments
executors:
go-executor:
docker:
- image: cimg/go:1.23
resource_class: medium
environment:
GOFLAGS: "-mod=mod"
node-executor:
docker:
- image: cimg/node:22.0
resource_class: medium
docker-executor:
docker:
- image: cimg/base:current
resource_class: large
security-executor:
docker:
- image: aquasec/trivy:0.58.0
resource_class: medium
# Commands bundle steps into reusable units
commands:
generate-provenance:
steps:
- run:
name: Generate SLSA provenance
command: |
mkdir -p /tmp/provenance
cat > /tmp/provenance/slsa.json << 'EOF'
{
"buildType": "CircleCI-${CIRCLE_BUILD_NUM}",
"commit": "${CIRCLE_SHA1}",
"timestamp": "$(date -Iseconds)",
"pipelineId": "${CIRCLE_WORKFLOW_ID}",
"builderVersion": "${CIRCLE_BUILD_NUM}"
}
EOF
- persist_to_workspace:
root: /tmp/provenance
paths:
- slsa.json
jobs:
go-tests:
executor: go-executor
steps:
- checkout
- restore_cache:
keys:
- go-mod-{{ checksum "api/go.sum" }}
- run:
name: Run Go Tests with Coverage
command: |
cd api
go test -v -coverprofile=coverage.out -covermode=atomic ./...
go tool cover -html=coverage.out -o coverage.html
go tool cover -func=coverage.out | tee coverage-summary.txt
- store_test_results:
path: api/test-results
- store_artifacts:
path: api/coverage.html
destination: coverage/go-coverage
- save_cache:
key: go-mod-{{ checksum "api/go.sum" }}
paths:
- ~/go/pkg/mod
- persist_to_workspace:
root: .
paths:
- api/coverage.html
frontend-tests:
executor: node-executor
steps:
- checkout
- restore_cache:
keys:
- npm-{{ checksum "frontend/package-lock.json" }}
- run:
name: Install & Test Frontend
command: |
cd frontend
npm ci
npm run test -- --coverage --ci --reporters=jest-junit
npm run lint -- --format junit -o lint-results.xml
- store_test_results:
path: frontend/junit.xml
- store_artifacts:
path: frontend/coverage
destination: coverage/frontend-coverage
- save_cache:
key: npm-{{ checksum "frontend/package-lock.json" }}
paths:
- ~/.npm
- persist_to_workspace:
root: frontend
paths:
- junit.xml
security-scan:
executor: security-executor
steps:
- checkout
- attach_workspace:
at: /tmp/workspace
- run:
name: Trivy Vulnerability Scan
command: |
export VERSION=$(git describe --tags --always --dirty)
trivy image \
--severity HIGH,CRITICAL \
--ignore-unfixed \
--format sarif \
--output trivy-report.sarif \
ghcr.io/myorg/api:${VERSION}
- store_artifacts:
path: trivy-report.sarif
destination: security-reports
build-and-push:
executor: docker-executor
parameters:
component:
type: string
docker-context:
type: string
default: "."
steps:
- checkout
- setup_remote_docker:
version: 20.10.24
docker_layer_caching: true
- run:
name: Build & Push Image
command: |
VERSION=$(git describe --tags --always --dirty)
COMPONENT=<< parameters.component >>
CONTEXT=<< parameters.docker-context >>
echo "Building ghcr.io/myorg/${COMPONENT}:${VERSION}"
docker build \
--cache-from=ghcr.io/myorg/${COMPONENT}:latest \
--build-arg BUILDKIT_INLINE_CACHE=1 \
-t ghcr.io/myorg/${COMPONENT}:${VERSION} \
-t ghcr.io/myorg/${COMPONENT}:latest \
${CONTEXT}
# Push with retry logic
for i in 1 2 3; do
docker push ghcr.io/myorg/${COMPONENT}:${VERSION} && break
echo "Push failed, retrying in 10s..."
sleep 10
done
docker push ghcr.io/myorg/${COMPONENT}:latest
- generate-provenance
deploy-to-k8s:
executor: kubernetes/default
parameters:
environment:
type: string
steps:
- checkout
- kubernetes/install-kubectl:
kubectl-version: "1.30"
- run:
name: Deploy to Kubernetes
command: |
VERSION=$(git describe --tags --always --dirty)
NAMESPACE=<< parameters.environment >>
kubectl set image deployment/api \
api=ghcr.io/myorg/api:${VERSION} \
-n ${NAMESPACE} --record
kubectl set image deployment/web \
web=ghcr.io/myorg/web:${VERSION} \
-n ${NAMESPACE} --record
kubectl rollout status deployment/api -n ${NAMESPACE} --timeout=5m
kubectl rollout status deployment/web -n ${NAMESPACE} --timeout=5m
# Verify deployment health
kubectl wait --for=condition=available \
deployment/api deployment/web \
-n ${NAMESPACE} --timeout=60s
workflows:
version: 2
build-deploy:
jobs:
# Parallel test execution
- go-tests:
name: go-tests-v<< pipeline.number >>
context:
- build-secrets
- frontend-tests:
name: frontend-tests-v<< pipeline.number >>
context:
- build-secrets
# Security gate runs after tests pass
- security-scan:
name: security-scan-api
requires:
- go-tests-v<< pipeline.number >>
context:
- security-context
# Parallel image builds
- build-and-push:
name: build-api-image
component: api
docker-context: api
requires:
- security-scan-api
- frontend-tests-v<< pipeline.number >>
context:
- registry-credentials
- build-and-push:
name: build-web-image
component: web
docker-context: frontend
requires:
- security-scan-api
- frontend-tests-v<< pipeline.number >>
context:
- registry-credentials
# Staging deployment (automatic)
- deploy-to-k8s:
name: deploy-staging
environment: staging
requires:
- build-api-image
- build-web-image
context:
- staging-k8s-cluster
# Production requires manual approval
- hold-production:
type: approval
requires:
- deploy-staging
filters:
branches:
only: main
- deploy-to-k8s:
name: deploy-production
environment: production
requires:
- hold-production
context:
- production-k8s-cluster
filters:
branches:
only: main
# Post-deployment verification
- run:
name: smoke-tests
command: |
curl -f ${PRODUCTION_ENDPOINT}/health || exit 1
requires:
- deploy-production
Part 4: Deep Dive: Where Each Platform Excels
4.1 Jenkins Strengths in 2026
Custom Execution Topology
Jenkins lets you define exactly where each stage runs. Need a stage on a GPU-equipped node with CUDA 12? Define a label and Jenkins routes it there. Need to run tests on an air-gapped network segment with no internet access? Configure a static agent in that segment. This granularity is unmatched by any managed platform.
// Example: GPU-specific stage in Jenkins
stage('ML Model Validation') {
agent {
label 'gpu-cuda12-ubuntu24'
}
tools {
// Custom tool installation via tool plugin
conda 'miniconda3-latest'
}
steps {
sh '''
conda activate ml-pipeline
nvidia-smi
python validate_model.py --batch-size 128
'''
}
}
Plugin-Driven Ecosystem
By 2026, Jenkins plugins cover niche use cases that CircleCI orbs simply don't address: mainframe deployment (IBM z/OS plugin), hardware-in-the-loop testing, SAP system integration, and compliance frameworks like FedRAMP that require specific audit trail formats. The Shared Library pattern lets you codify organizational conventions into reusable Groovy code.
// vars/securityGate.groovy - Shared Library
def call(String environment, Map thresholds) {
def criticalThreshold = thresholds.critical ?: 0
def highThreshold = thresholds.high ?: 0
echo "Running security gate for ${environment}"
def trivyOutput = sh(
script: "trivy image --severity HIGH,CRITICAL --format json ${imageRef}",
returnStdout: true
)
def json = readJSON text: trivyOutput
def criticalCount = json.Results?.Vulnerabilities?.count { it.Severity == 'CRITICAL' } ?: 0
def highCount = json.Results?.Vulnerabilities?.count { it.Severity == 'HIGH' } ?: 0
if (criticalCount > criticalThreshold || highCount > highThreshold) {
error "Security gate failed: ${criticalCount} critical, ${highCount} high vulnerabilities"
}
// Generate compliance artifact
writeFile file: "security-gate-${environment}.json", text: groovy.json.JsonOutput.toJson([
timestamp: new Date().format('yyyy-MM-dd HH:mm:ss'),
critical: criticalCount,
high: highCount,
passed: true,
signedBy: env.BUILD_USER
])
}
4.2 CircleCI Strengths in 2026
Zero-Infrastructure Operations
CircleCI's most compelling advantage is the absence of operational overhead. There's no controller to patch, no agent VMs to rotate, no plugin dependency hell. The platform automatically scales execution capacity based on your organization's usage patterns. For teams shipping 50+ microservices, this translates to measurable reduction in toil.
First-Class Caching Architecture
CircleCI's caching uses content-addressable storage with automatic restoration. The restore_cache step implements partial key matching, falling back to the most recent cache when an exact checksum isn't found. This is particularly powerful for monorepo setups where dependencies change infrequently.
# Advanced CircleCI caching strategy
- restore_cache:
keys:
# Most specific first
- deps-{{ .Branch }}-{{ checksum "api/go.sum" }}-{{ checksum "api/go.mod" }}
- deps-{{ .Branch }}-{{ checksum "api/go.sum" }}
- deps-{{ .Branch }}-
- deps-main-
- run:
name: Download dependencies
command: |
cd api
go mod download
- save_cache:
key: deps-{{ .Branch }}-{{ checksum "api/go.sum" }}-{{ checksum "api/go.mod" }}
paths:
- ~/go/pkg/mod
- ~/.cache/go-build
Flaky Test Detection (CircleCI Feature)
By 2026, CircleCI's built-in test analytics automatically identifies and quarantines flaky tests. The platform tracks test outcomes across builds and flags tests that exhibit non-deterministic behavior, surfacing them in the UI and optionally excluding them from the pass/fail calculation.
# CircleCI test splitting with flaky detection
- run:
name: Run tests with automatic splitting
command: |
cd frontend
# CircleCI's test splitting distributes tests across parallel runs
TESTFILES=$(circleci tests glob "src/**/*.test.ts" | \
circleci tests split --split-by=timings --timings-type=filename)
npm run test -- --testPathPattern "$TESTFILES" --ci
# In the CircleCI web UI, flaky tests are automatically labeled
# with detection confidence scores after 5+ runs
Part 5: Security Comparison
Secret Management
Jenkins stores secrets in its internal credential store (backed by AES encryption) or via external providers like HashiCorp Vault through the Vault plugin. Secrets are injected at runtime using the withCredentials block, which masks values in logs automatically.
// Jenkins: Masked secret injection
stage('Database Migration') {
steps {
withCredentials([
string(credentialsId: 'db-migration-password', variable: 'DB_PASS'),
file(credentialsId: 'ca-bundle', variable: 'CA_BUNDLE')
]) {
sh '''
# DB_PASS is automatically masked in console output
export DB_PASSWORD="$DB_PASS"
migrate --ca-bundle "$CA_BUNDLE" apply
'''
}
}
}
CircleCI uses "contexts" — named collections of environment variables scoped to specific projects or organization-wide. Contexts can be restricted by branch, making it impossible for a PR from a fork to access production credentials. Environment variable values are encrypted at rest and never appear in raw logs.
# CircleCI: Context-driven secrets
workflows:
deploy:
jobs:
- deploy-to-k8s:
context:
- restricted-prod-context # Only available on main branch
requires:
- approval-gate
# Context definition (configured in CircleCI web UI, not in YAML):
# Name: restricted-prod-context
# Variables: K8S_API_TOKEN, TLS_CERT, DB_CONNECTION_STRING
# Branch restrictions: main, release/*
Supply Chain Security
Both platforms now support generating SLSA Level 3 provenance. Jenkins achieves this through the provenance plugin combined with signed build records. CircleCI bakes attestation generation into its workflow engine, automatically creating in-toto attestations that can be verified via the Sigstore ecosystem.
Part 6: Cost Dynamics in 2026
The cost models diverge sharply. Jenkins, being self-hosted, incurs infrastructure costs: controller instance (typically $200–800/month for a production-grade setup with HA), agent VMs or Kubernetes nodes, and storage for artifacts. You pay for what you provision, regardless of utilization. For organizations already operating substantial compute infrastructure, this marginal cost is often negligible.
CircleCI charges per execution minute with tiered plans. By 2026, the pricing has evolved to include "credits" that map to compute-seconds across different resource classes. Heavy users negotiate volume discounts. The key variable: idle time costs nothing on CircleCI (you only pay when pipelines run), whereas Jenkins infrastructure costs are constant. For teams with bursty CI patterns, CircleCI's model is often cheaper; for teams running continuous pipelines 24/7, Jenkins can be more economical.
Part 7: Best Practices for 2026
Jenkins Best Practices
- Treat Jenkins as Cattle, Not a Pet — Use Configuration-as-Code (JCasC) to define your entire controller configuration in YAML. Never manually configure a production Jenkins instance through the UI.
- Externalize State — Store artifacts in S3/GCS compatible storage via the artifact manager plugin, not on the controller filesystem.
- Pipeline Library Versioning — Tag your Shared Library repository with semantic versions and reference specific versions in pipelines to prevent breaking changes.
- Agent Ephemerality — Use Kubernetes-backed agents that are created per-job and destroyed after execution. Avoid long-lived "pet" agents accumulating state.
# jenkins-casc.yaml snippet for Configuration-as-Code
jenkins:
systemMessage: "Production Jenkins Controller - Managed via CasC"
securityRealm:
ldap:
server: "ldap.internal.company.com"
rootDN: "dc=company,dc=com"
authorizationStrategy:
roleBased:
roles:
- name: "pipeline-developers"
pattern: ".*"
permissions:
- "Job/Build"
- "Job/Read"
- "Job/Configure"
clouds:
- kubernetes:
name: "k8s-agents"
serverUrl: "https://k8s-api.internal.company.com"
templates:
- name: "go-runner"
image: "golang:1.23"
ephemeral: true
idleMinutes: 0 # Immediate teardown
retentionPolicy: "never"
CircleCI Best Practices
- Orb Pinning — Always pin orb versions to a specific semver range. Floating major versions can introduce breaking changes silently.
- Workspace Hygiene — Use
persist_to_workspaceonly for files genuinely needed by downstream jobs. Workspace transfer adds latency. - Dynamic Configuration — For monorepos, use CircleCI's dynamic config feature to generate per-service pipeline configurations at runtime based on what actually changed.
- Resource Class Right-Sizing — Profile your jobs and select appropriate resource classes. The default "medium" often leaves performance on the table for compute-heavy workloads.
# Dynamic configuration for monorepo efficiency
# File: .circleci/config.yml
version: 2.1
setup: true
jobs:
generate-config:
docker:
- image: cimg/base:current
steps:
- checkout
- run:
name: Generate per-service config
command: |
# Determine which services changed
CHANGED=$(git diff --name-only origin/main...HEAD | \
awk -F'/' '{print $1}' | sort -u)
# Generate continuation config
python3 .circleci/generate-config.py $CHANGED > /tmp/continue-config.yml
- continuation:
configuration_path: /tmp/continue-config.yml
workflows:
setup-workflow:
jobs:
- generate-config:
filters:
branches:
ignore: main
When to Choose Jenkins (Decision Matrix)
- You require on-premise execution on specific hardware profiles
- Your organization has regulatory requirements demanding complete infrastructure control
- You're running 1000+ builds daily with stable compute utilization
- You need integration with legacy systems (mainframes, SAP, proprietary hardware)
- Your team has dedicated DevOps engineers comfortable managing infrastructure
When to Choose CircleCI (Decision Matrix)
- Your team wants to eliminate CI infrastructure management entirely
- You're shipping multiple services with bursty build patterns
- Flaky test detection and automatic parallelization are high-value features
- Your organization operates primarily in cloud-native ecosystems (Kubernetes, serverless)
- Developer experience velocity matters more than absolute customization
Part 8: The Hybrid Approach
A growing pattern in 2026 is the hybrid model: use CircleCI for routine application CI (PR validation, unit tests, image builds) while maintaining a Jenkins instance for specialized workflows — compliance attestation, legacy deployment, hardware-in-the-loop testing, or mainframe integration. This lets each team choose the platform that fits their specific workflow without forcing a one-size-fits-all decision.
The integration point is typically the artifact repository. Both platforms publish to the same container registry or artifact store, and deployment pipelines consume from that shared source of truth regardless of which CI system produced the artifact.
# Example: Artifact handoff between platforms
# Jenkins publishes signed artifacts with metadata
stage('Publish Attested Artifact') {
steps {
sh '''
# Generate cosign signature
cosign sign \
--key cosign.key \
ghcr.io/myorg/api:${VERSION}
# Push metadata to shared artifact catalog
oras push \
ghcr.io/myorg/artifact-catalog:api-${VERSION} \
sbom.spdx:application/spdx+json \
provenance.json:application/vnd.in-toto+json
'''
}
}
# CircleCI consumes the same artifact for deployment
# The platform doesn't care which CI built it
- run:
name: Deploy attested artifact
command: |
cosign verify \
--key cosign.pub \
ghcr.io/myorg/api:${VERSION}
kubectl set image deployment/api \
api=ghcr.io/myorg/api:${VERSION}
Conclusion
The Jenkins vs CircleCI comparison in 2026 isn't about picking a winner — it's about matching platform philosophy to team reality. Jenkins rewards organizations that value control, customization, and have the operational maturity to manage infrastructure. CircleCI rewards teams that prioritize velocity, simplicity, and want their CI platform to fade into the background. Both are mature, production-tested systems capable of shipping software at scale. The practical pipelines in this tutorial demonstrate that both platforms solve the same problems, but the ergonomics of how you express that solution — Groovy declarative syntax with Shared Libraries vs YAML with Orbs — will feel dramatically different day to day. Choose based on which friction you prefer to live with: infrastructure maintenance or platform constraints.