DevOps · 05

CI/CD Pipelines

A pipeline is the road every commit travels to production. This page explains the ideas first (integration, delivery, deployment, trunk-based development, build once and deploy many), then builds a pipeline for a Spring and Maven service, adds quality gates, scanning and safe secrets with OIDC, shows the same pipeline in Jenkins, GitLab CI and GitHub Actions, and finishes with environments, GitOps, rollbacks and supply-chain security.

12
Chapters
3
CI tools
OIDC
No stored keys
GitOps
Pull deploys
01
Chapter One - The Idea

Continuous Integration, Delivery and Deployment

Before pipelines, releasing meant a branch that lived for weeks, a merge day nobody enjoyed, a build made on somebody's laptop and a manual checklist. The cost of that process is not the hours it takes. It is that feedback arrives late: you learn that two changes do not work together long after the people who wrote them have forgotten the details.

CI/CD attacks that delay. The idea is to make the path from a commit to production automatic, repeatable and boring, so that releasing becomes a non-event and a failure is found minutes after it was introduced.

One pipeline, left to right: every commit travels the same road
commit build unit tests SAST / static scan package integration tests Continuous integration: every push, minutes, no humans publish artifact deploy staging smoke tests approve / promote deploy prod Continuous delivery: the same artifact moves through environments artifact is built once Delivery: a human (or a policy) presses the button for production. Deployment: the button is removed; a green pipeline goes to production by itself. Both are "CD". Be explicit about which one you mean.
TermWhat it meansHuman in the loop?Typical trigger
Continuous Integration (CI)Everyone merges to a shared branch often; each change is built and tested automaticallyNoEvery push / merge request
Continuous DeliveryEvery change that passes CI is kept in a releasable state and can be deployed at any timeYes, a manual approval for productionGreen pipeline on the main branch
Continuous DeploymentEvery change that passes all automated checks goes to production without anyone pressing a buttonNoGreen pipeline on the main branch
Key insight: A pipeline is an assembly line with quality inspectors. The car (your artifact) is built once and then passes inspection stations. A defective car is stopped at the first station that notices, not at the customer.
What a pipeline is made of
  • Trigger - what starts it: a push, a merge request, a tag, a schedule, a manual click, another pipeline.
  • Stage - a group of jobs that run at the same point in the flow (build, test, package, deploy). Stages usually run in order.
  • Job - one unit of work with a script, running on a runner / agent / executor (a VM or container).
  • Artifact - a file a job produces and later jobs or humans consume (a JAR, a test report, a container image).
  • Gate - a condition that can stop the flow: failing tests, a quality gate, a security scan, a manual approval.
  • Environment - a place the artifact is deployed to (dev, staging, production), often with its own secrets and approvals.
Where this page fits

Page 02 and page 03 cover what you deploy into (containers, Kubernetes). This page is the machinery that gets a commit there. Page 06 applies the same pipeline ideas to infrastructure code, and page 08 covers the rollout strategies a deploy job can use.

02
Chapter Two - Branching

Trunk-Based Development and Other Strategies

The branching model decides how often code is integrated, so it decides how useful your CI is. A pipeline that runs on a branch that lives for three weeks does not give continuous integration. It gives continuous isolation.

Trunk-based development vs long-lived branches
Trunk-based: short-lived branches, merged within a day or two feature feature feature Branches live hours or a day or two. main is always releasable. Unfinished work hides behind a feature flag. Long-lived branches (Gitflow style): develop, release, hotfix main develop release/1.4 Merges are big and rare. Integration happens late, which is exactly what CI tries to avoid.
StrategyHow it worksStrengthsCosts
Trunk-based developmentOne main branch. Developers commit to it directly or through branches that live hours to a couple of days. Releases are cut from main (often by tag).Small merges, fast feedback, no merge day, works naturally with CDNeeds good tests, feature flags for unfinished work, and discipline about keeping main green
GitHub flow / GitLab flow (simple)Short feature branches, pull request or merge request, review, merge to main, deploy from mainSimple, review built inDrifts into long branches if reviews are slow
GitflowLong-lived develop and main, plus feature/*, release/*, hotfix/*Explicit release stabilisation, suits versioned products with several supported releasesHeavy ceremony, late integration, two sources of truth; poor fit for continuous deployment of a web service
Release branches from trunkTrunk-based, plus a release/x.y branch cut for a version that must be patched laterSupports maintenance releases without Gitflow overheadFixes must be applied to trunk first, then cherry-picked
What trunk-based development needs to work
  • Fast, reliable CI. If the pipeline takes 40 minutes and fails randomly, nobody will merge small and often.
  • Feature flags (also called toggles) so that incomplete features can be merged but stay dark. The flag is removed when the feature is finished; a flag that lives forever is a hidden branch.
  • Branch by abstraction for large refactors: introduce an interface, switch callers one at a time, delete the old path.
  • Pre-merge checks: the merge request pipeline runs build and tests, and the target branch refuses red merges.
  • Small changes. Large changes are the real reason branches live long.
⚠ Calling it CI while branches live for weeks
  • If feature branches stay open for a sprint, integration still happens at the end, only now it is hidden behind a green pipeline on each branch. The signal that you are really doing CI: how old is the oldest open branch, and how long does a merge request wait for review?
Practical default

For a typical Spring service deployed several times a week: short-lived branches, merge request with a mandatory pipeline and one review, merge to main, and the pipeline on main builds the artifact that goes to staging and production. Use a release branch only if you must patch old versions.

03
Chapter Three - A Real Pipeline

Pipeline Stages for a Java, Spring and Maven Service

Here is the shape of a pipeline for a typical Spring Boot service built with Maven. The commands are the same whichever CI tool runs them: the tool only decides where and when.

StageCommand (typical)What it catchesNotes
Compilemvn -B -ntp compileSyntax and type errorsCheapest feedback; fail here before anything expensive
Unit testsmvn -B -ntp testLogic errors in isolationRun by the Surefire plugin; fast (no network, no containers)
Static analysisCheckstyle, SpotBugs, SonarQube scanner, SASTStyle, bug patterns, vulnerable code patternsRun in parallel with tests
Packagemvn -B -ntp package -DskipTestsPackaging problems (repackaged Spring Boot JAR)Skip tests only because a previous stage already ran them
Integration testsmvn -B -ntp verifyWiring, SQL, messaging, HTTP contractsRun by the Failsafe plugin in the verify phase; use Testcontainers or CI service containers
Image builddocker build, Buildpacks, or JibContainer-level problemsTag with an immutable identifier (chapter 04)
Image scanTrivy, Grype or a registry scannerKnown CVEs in OS packages and librariesChapter 05
PublishPush the JAR and/or image to a repository-Only on the main branch
Deploy and verifyDeploy job, then smoke testsConfiguration and environment problemsChapter 09
Maven specifics that matter in CI
  • -B (batch mode) removes interactive prompts and colour. -ntp (--no-transfer-progress) removes the download progress noise that floods logs.
  • Use the Maven Wrapper (./mvnw) so the pipeline and developers use the same Maven version, and pin the JDK in the image or the setup step, not "whatever the agent has".
  • Surefire runs unit tests during test (default patterns include *Test, Test*, *Tests, *TestCase); Failsafe runs integration tests during integration-test and checks the result in verify (default patterns include *IT, IT*, *ITCase). Naming decides which plugin picks a test up.
  • Make the build reproducible: no SNAPSHOT dependencies in release builds, and set project.build.outputTimestamp so the JAR does not change byte-for-byte on every build.
  • Produce test reports as XML (Surefire and Failsafe write JUnit-format files under target/surefire-reports and target/failsafe-reports) and have the CI tool publish them.
Shell - the whole pipeline as a developer would run it locally
$ ./mvnw -B -ntp clean verify # compile, unit tests, package, integration tests $ docker build -t orders-service:local . $ docker run --rm -p 8080:8080 orders-service:local
Reproduce CI locally first

If a pipeline step is a command you cannot run on your laptop, debugging it means committing and waiting. Keep the logic in scripts and Maven goals, and keep the CI file as a thin list of "run this on that runner". This also makes moving between Jenkins, GitLab and GitHub Actions a small job instead of a rewrite.

Caching dependencies

Without a cache every job downloads the entire Maven dependency tree from the repository again. A cache stores ~/.m2/repository (or a project-local directory) between runs. The principle is the same in every tool:

  • Key the cache on a hash of the files that define the dependencies (pom.xml). When the POM changes the key changes and a fresh cache is built; otherwise it is reused.
  • Provide a fallback key (a prefix match) so that a changed POM still starts from the previous cache and only downloads what is new.
  • Do not cache build output (target/) as if it were a dependency. Pass build output between jobs as an artifact: a cache is a best-effort speed-up that may be missing, an artifact is a guaranteed hand-off.
  • Treat the cache as untrusted input for release builds: a poisoned or corrupted cache should not be able to change what you ship. Many teams build releases with a clean or read-only cache.
CacheArtifact
PurposeSpeed: avoid re-downloadingCorrectness: hand a result to a later job or a human
Guaranteed to exist?No (miss, eviction, different runner)Yes, until it expires
Typical contentMaven repository, npm cacheJAR, test report, coverage report, image digest file
Keyed byHash of pom.xmlPipeline / job id
⚠ Caching target/ to skip rebuilding
  • A cache that contains compiled classes can make a build pass with stale output ("works on the runner that had the cache"). Cache dependencies only; let the build produce its own output every time.
04
Chapter Four - Artifacts

Versioning, Immutability and Build Once, Deploy Many

The most important rule in delivery pipelines is build once, deploy many: produce the artifact one time, store it, and promote exactly that artifact through every environment. If production runs a rebuild of the code that staging ran, then what you tested is not what you shipped, even if the source commit is identical. Different dependency resolution, a different base image or a different build date can change the result.

Build once, deploy many: the same bytes in every environment
commit a1b2c3d build + test artifact registry orders:1.4.2 / sha256:9f3c... dev config + secrets for dev staging config + secrets for staging production config + secrets for production same image same image Only configuration differs between environments. The artifact is never rebuilt for a promotion.
Pipeline builds orders from commit a1b2c3d
▶
It produces one JAR and one container image, tagged with an immutable identifier and pushed to the registry
Tests run against that artifact
▶
Integration and smoke tests pull the image by tag or digest. They do not rebuild it
Staging deploy
▶
The deploy job references the image identifier and injects the staging configuration at deploy time
Production deploy
▶
Same identifier, production configuration. If staging passed, production runs the same bytes
Rollback
▶
Deploy the previous identifier. It is still in the registry, because artifacts are never overwritten
Versioning and tagging
SchemeExampleGood forWatch out for
Commit SHAorders:a1b2c3dEvery build, perfect traceability to sourceNot human-friendly; needs a map to release numbers
Semantic versionorders:1.4.2Libraries and anything customers or other teams consumeSomeone must decide the number; automate it from tags or conventional commits
Version + SHAorders:1.4.2-a1b2c3dReadable and traceableLonger tags
Build numberorders:1287Quick to implementResets or collides when you change CI system
**latest**orders:latestAlmost nothing in a pipelineIt changes under you. A deployment that says latest is not reproducible and cannot be rolled back by name
  • Immutable tags: configure the registry so a pushed tag cannot be overwritten (Amazon ECR calls this tag immutability; other registries have an equivalent). Without this, "the same tag" can silently mean different bytes.
  • Deploy by digest (image@sha256:...) when you need an absolute guarantee. A tag is a label that points to a digest; the digest identifies the content.
  • Snapshots stay out of production. A Maven -SNAPSHOT version is mutable by design. Release builds use fixed versions.
  • Record provenance: the commit SHA, pipeline URL and build time belong in the artifact's metadata (an image label, META-INF/MANIFEST.MF, or Spring Boot's build-info), so a running instance can answer "what exactly am I?".
Java - Spring Boot can expose build metadata at /actuator/info
// pom.xml: add the goal "build-info" to spring-boot-maven-plugin, and in application.yml: // management.info.build.enabled: true // management.endpoints.web.exposure.include: health,info // // Then GET /actuator/info returns something like (illustrative): // {"build":{"artifact":"orders","version":"1.4.2","time":"2026-10-31T09:12:44Z"}}
⚠ Rebuilding for each environment
  • A pipeline with a "build-prod" job that compiles the code again after staging passed breaks the guarantee that production runs what was tested. Promote the artifact; vary only configuration.
⚠ Baking environment configuration into the artifact
  • If the JAR or image contains staging URLs and credentials, you must rebuild to deploy elsewhere. Externalise configuration (environment variables, Spring profiles, config maps, a secrets manager) and keep the artifact environment-neutral.
05
Chapter Five - Quality

Tests, Flaky Tests, Quality Gates and Scanning

A pipeline is only as trustworthy as its tests. The goal is fast, reliable feedback: the quickest tests first, the slowest later, and a failure that always means "something is really wrong".

The testing pyramid, and what each layer costs in a pipeline
End-to-end / UI few, slow, fragile Integration / contract / API some, seconds to minutes Unit tests many, milliseconds each Where they run in the pipeline merge request: unit + a few integration, static analysis main: full integration + image scan after deploy: smoke + narrow E2E in staging Cheap tests run first and run often. Expensive tests run later, on fewer commits, against a deployed artifact.
LayerSpring/Java exampleSpeedRun when
UnitJUnit 5 + Mockito on a service classMillisecondsEvery push
Slice / integration@DataJpaTest, @WebMvcTest, Testcontainers PostgreSQL, @SpringBootTestSecondsEvery merge request (or main)
ContractConsumer-driven contracts between services (for example Spring Cloud Contract or Pact)SecondsEvery merge request of provider and consumer
End-to-endA browser or HTTP script against the deployed staging systemMinutesAfter deploy to staging
SmokeThree to ten calls proving the deployment is alive (health, one read, one write)SecondsAfter every deploy, including production
Flaky tests are a pipeline bug

A flaky test passes and fails on the same code. Each one teaches the team that red does not mean broken, and people start clicking "re-run" instead of reading failures. That destroys the value of the whole pipeline.

Typical causeWhat you seeFix
Sleeps and timing assumptions (Thread.sleep)Fails only on a loaded runnerWait for a condition (Awaitility) with a timeout, not a fixed delay
Shared mutable state between testsPasses alone, fails in the suite, order-dependentIsolate data; clean up; random order locally to expose it
Shared external service or databaseFails when two pipelines run at oncePer-test containers or schemas; do not share a test database
Dependence on the clock or time zoneFails at midnight, month end, or on a different runnerInject a Clock; fix the zone
Network calls to the internetIntermittent DNS or rate-limit errorsStub the call; use WireMock or a local container
Port collisions"Address already in use"Use random ports (server.port=0) and Testcontainers' mapped ports
  • Quarantine, do not ignore. Move a known-flaky test to a separate, non-blocking job with a ticket and a deadline, so it stops blocking everyone but is not forgotten.
  • Retry is a smell, not a cure. A bounded retry on infrastructure steps (network downloads) is reasonable; retrying a failing assertion hides real race conditions.
Quality gates

A quality gate is an automated pass/fail decision based on measurements. If the gate fails, the pipeline stops and the change does not merge or ship. Typical conditions:

  • Tests pass and the number of tests did not silently drop.
  • Coverage on new code does not fall below an agreed level. Measure it with a coverage tool such as JaCoCo. Treat coverage as a smoke alarm, not a goal: a high number with weak assertions proves little.
  • No new serious issues from static analysis (bugs, vulnerabilities, code smells) in the code that was just changed.
  • No known critical vulnerabilities in dependencies or the container image (below).
  • License checks if your organisation requires them.

SonarQube is a common tool for the analysis part. Its quality gate is a set of conditions evaluated on new code or on overall code during each analysis; the default built-in gate is called Sonar way, and you can define your own. The "new code" focus matters in older codebases: you do not have to fix ten years of history to stop things getting worse. The exact default thresholds change between versions, so read them in your own SonarQube instance instead of quoting a number.

Gate on the delta

Applying a gate to new code makes the rule enforceable on a legacy codebase and keeps pressure on the code being touched today. A gate on the whole codebase that starts out red gets switched off.

Dependency and container scanning
ScanLooks atExamples of toolsWhen
SAST (static application security testing)Your source code for insecure patternsSonarQube, CodeQL, SemgrepEvery merge request, parallel to tests
SCA (software composition analysis)Your dependency tree (pom.xml) against known-vulnerability databasesOWASP Dependency-Check, Dependabot/Renovate alerts, GitLab dependency scanningEvery merge request and on a schedule (new CVEs appear after you ship)
Container / image scanOS packages and libraries inside the imageTrivy, Grype, registry scannersAfter image build, before publish; again on a schedule
Secret scanCommitted passwords, tokens, keysgitleaks, GitHub secret scanning, GitLab secret detectionPre-commit and on every push
IaC scanTerraform and Kubernetes manifests for misconfigurationCheckov, tflint, Trivy configEvery merge request touching infra (page 06)
Shell - fail a pipeline when the image has fixable serious vulnerabilities
$ trivy image --severity HIGH,CRITICAL --ignore-unfixed --exit-code 1 registry.example.com/orders:1.4.2

The three flags used above exist in the Trivy CLI: --severity restricts what is reported, --ignore-unfixed hides vulnerabilities with no available fix, and --exit-code sets the process exit code when issues are found (so the job fails). Checked against trivy image --help, Trivy 0.76.1, 2026-10-31.

⚠ A scan that nobody can act on
  • A gate that fails on every CVE, including those with no fix and those in test-scope libraries, trains people to bypass it. Start by failing on fixable HIGH/CRITICAL findings in the runtime image, track the rest, and keep an explicit, time-limited suppression list with owners.
06
Chapter Six - Secrets

Secrets in Pipelines and OIDC Instead of Long-Lived Keys

A pipeline needs to do privileged things: push images, deploy, read a registry. That makes it one of the most valuable targets in your company. The rule is to give it as few secrets as possible, for as short a time as possible.

OIDC: the pipeline proves who it is, AWS hands back short-lived credentials
CI job (GitHub / GitLab) CI platform OIDC provider (issues JWT) AWS STS (+ IAM role trust) AWS resources (S3, ECR, EKS ...) 1. request a token 2. signed JWT 3. AssumeRoleWithWebIdentity(JWT) 4. temporary credentials (expire) 5. call AWS with them AWS checks the token signature against the CI platform's public keys, then the role trust policy: audience (aud) and subject (sub), e.g. only repo acme/orders-service on branch main may assume this role.
RuleWhyHow
Never echo a secretLogs are stored, shared and read by many peopleDo not echo variables, do not run env or set -x in jobs that hold secrets. CI tools mask known secrets in logs, but masking is a safety net, and it cannot catch a transformed value (base64, split, URL-encoded)
Use the platform's secret storeSecrets in the repository are leaked forever through git historyMasked / protected variables (GitLab), credentials store (Jenkins), repository or environment secrets (GitHub)
Scope themA feature-branch pipeline should not be able to deploy to productionProtected branches/environments, environment-level secrets, separate roles for plan and apply
Prefer short-lived credentialsA stolen key that expires in an hour is a small incidentOIDC federation (below), instead of an IAM user access key stored in CI
Do not give untrusted code your secretsPipelines from forks or untrusted merge requests can run attacker-written scriptsDo not expose secrets to fork pipelines; be careful with workflow triggers that run with secrets on external code
Rotate and auditCredentials leak; you need to replace them quickly and know who used themAutomate rotation; use CloudTrail to see which role session did what
OIDC to AWS: how it works

The old way is to create an IAM user, generate an access key and store AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY in the CI system. Those keys do not expire, live in a third-party system, and must be rotated by hand. OpenID Connect (OIDC) federation removes the stored key altogether.

  • The CI platform acts as an identity provider. For each job it can issue a signed JWT that states who the job is (repository, branch or environment, pipeline) and for whom it is meant (the audience).
  • In AWS you register that platform as an IAM OIDC identity provider, and create an IAM role whose trust policy says: "principals authenticated by this provider, with this audience and this subject, may call sts:AssumeRoleWithWebIdentity".
  • During the job, the token is exchanged with AWS STS for temporary credentials. Nothing long-lived is stored in the CI system. Only the role ARN, which is not a secret, is in the pipeline file.
JSON - IAM role trust policy for GitHub Actions (restrict to one repo and branch)
{ "Version": "2012-10-17", "Statement": [ { "Effect": "Allow", "Principal": { "Federated": "arn:aws:iam::111122223333:oidc-provider/token.actions.githubusercontent.com" }, "Action": "sts:AssumeRoleWithWebIdentity", "Condition": { "StringEquals": { "token.actions.githubusercontent.com:aud": "sts.amazonaws.com", "token.actions.githubusercontent.com:sub": "repo:acme/orders-service:ref:refs/heads/main" } } } ] }

The provider URL for GitHub is https://token.actions.githubusercontent.com, and for the official AWS action the audience is sts.amazonaws.com. The **sub** (subject) condition is the part that matters for security: without it, any repository on GitHub that obtains a token for that provider could try to assume the role. GitHub subjects look like repo:ORG/REPO:ref:refs/heads/main for a branch and repo:ORG/REPO:environment:NAME when the job uses a GitHub environment. For GitHub, the job must also have the permission id-token: write before it can request the token. Checked against docs.github.com (OIDC in AWS and OIDC reference), 2026-10-31.

YAML - GitLab CI: request an ID token with the audience AWS expects
deploy: id_tokens: AWS_TOKEN: aud: sts.amazonaws.com script: # exchange $AWS_TOKEN with aws sts assume-role-with-web-identity (abridged) - aws sts get-caller-identity

On the GitLab side the id_tokens keyword creates a JWT in the variable you name, and aud sets its audience claim; GitLab's AWS guide uses aws sts assume-role-with-web-identity with that token and scopes the trust policy with claims such as sub (and, on GitLab.com, namespace_id and project_id). Checked against the GitLab CI/CD YAML reference and the "Configure OpenID Connect in AWS" page, 2026-10-31.

⚠ An OIDC trust policy without a subject condition
  • If the trust policy only checks the audience, any project that can get a token from that provider may be able to assume the role. Always pin sub (and prefer a branch, tag or environment form) and, on GitLab.com, the stable project_id / namespace_id. Use a separate role for pull request pipelines (read-only, plan) and for the main branch (deploy).
⚠ Printing secrets while debugging
  • A quick echo $DB_PASSWORD or env | sort to debug a failing job writes the secret into logs that dozens of people can read and that are archived for months. Rotate any secret that appeared in a log, even if "masked".
07
Chapter Seven - Jenkins

A Declarative Jenkinsfile

Jenkins is the long-standing self-hosted automation server. A pipeline is defined in a **Jenkinsfile kept in the repository (pipeline as code). Jenkins supports two syntaxes. Declarative** (pipeline { ... }) has a fixed, validated structure and is the recommended starting point; scripted is plain Groovy and more flexible but easier to turn into spaghetti. A multibranch pipeline job scans the repository, creates a job per branch and pull request, and runs the Jenkinsfile found on each.

Jenkinsfile - build, check in parallel, publish, approve, deploy
pipeline { agent none options { timeout(time: 30, unit: 'MINUTES') disableConcurrentBuilds() buildDiscarder(logRotator(numToKeepStr: '30')) } environment { IMAGE = 'registry.example.com/orders-service' } stages { stage('Build and test') { agent { docker { image 'maven:3.9-eclipse-temurin-21' } } steps { sh 'mvn -B clean verify' } post { always { junit 'target/surefire-reports/*.xml' } success { archiveArtifacts artifacts: 'target/*.jar', fingerprint: true stash name: 'app-jar', includes: 'target/*.jar' } } } stage('Checks') { failFast true parallel { stage('Static analysis') { agent { label 'linux' } steps { sh './scripts/static-analysis.sh' } } stage('Dependency scan') { agent { label 'linux' } steps { sh './scripts/dependency-scan.sh' } } } } stage('Publish image') { when { branch 'main' } agent { label 'docker' } steps { unstash 'app-jar' withCredentials([usernamePassword(credentialsId: 'registry-creds', usernameVariable: 'REG_USER', passwordVariable: 'REG_PASS')]) { sh ''' echo "$REG_PASS" | docker login -u "$REG_USER" --password-stdin registry.example.com docker build -t "$IMAGE:$GIT_COMMIT" . docker push "$IMAGE:$GIT_COMMIT" ''' } } } stage('Approve production') { when { branch 'main' } steps { timeout(time: 1, unit: 'HOURS') { input message: 'Deploy this build to production?' } } } stage('Deploy production') { when { branch 'main' } agent { label 'deployer' } steps { sh './deploy.sh production "$GIT_COMMIT"' } } } post { failure { echo 'Build failed. Notify the team here (mail, Slack plugin, ...).' } } }

Everything above uses directives and steps from the Jenkins Pipeline syntax reference and the plugin step references (Credentials Binding, Pipeline: Basic Steps, JUnit, core archiveArtifacts). The file was read against the documentation; it was not executed on a Jenkins instance for this page, so run it through your controller's declarative linter before relying on it. The docker agent needs the Docker Pipeline plugin and Docker on the agent; the when { branch ... } condition works in multibranch pipelines.

PieceWhat it doesNotes
agent none at top levelNo executor is held for the whole run; each stage declares its own agentLets a stage such as the approval wait without occupying a build node
agent { docker { image ... } } / agent { label ... }Where the stage runs: a container, or any node with that labelPin the image tag; use labels to target nodes that have the tools you need
options { ... }Pipeline-wide settingstimeout, disableConcurrentBuilds, buildDiscarder are documented options; a pipeline without a timeout can hang forever
environment { ... }Environment variables for the pipeline or a stageA credentials(...) helper in this block binds a stored credential to a variable
stages / stage / stepsThe sequence of workSteps are things like sh, junit, archiveArtifacts, stash
parallel { ... }Run nested stages at the same timefailFast true aborts the others when one fails
when { ... }Run a stage only if a condition holdsbranch, changeRequest, expression, ...; beforeAgent true evaluates the condition before allocating an agent
inputPause for human approvalWrap it in timeout so abandoned approvals do not block forever
post { always / success / failure / ... }Run after a stage or the whole pipeline depending on the resultUse always for publishing test results, failure for notifications, cleanup for tidy-up
withCredentials([...])Expose credentials to a block of steps and mask them in the logProvided by the Credentials Binding plugin: usernamePassword, string, file, ...
stash / unstashPass files between stages that run on different agentsGood for a JAR; for large files use a repository, not stash
⚠ Secrets via string interpolation in Groovy
  • In sh "docker login -p ${PASSWORD}" Groovy interpolates the secret before the shell sees it, and the value can leak into process listings and logs. Use single-quoted shell strings and let the shell read the environment variable (sh 'echo "$REG_PASS" | docker login --password-stdin ...'), exactly as the Jenkinsfile above does.
⚠ Business logic in the Jenkinsfile
  • Large Groovy blocks are hard to test and run only on Jenkins. Keep the Jenkinsfile as orchestration (which stage, which agent, which gate) and put build logic in Maven goals and shell scripts that a developer can run locally. If many repositories share the same pipeline, move it into a shared library.
Operating Jenkins

Keep the controller light: do not build on it, run work on agents (preferably ephemeral containers or VMs), pin plugin versions, back up the configuration, and keep Jenkins and its plugins patched, because a plugin vulnerability on a server that holds deploy credentials is a direct route to production.

08
Chapter Eight - GitLab CI and GitHub Actions

The Same Pipeline in Two More Dialects

GitLab CI and GitHub Actions are YAML-based and hosted with the code. The concepts match everything above; only the keywords differ. Both example files below were parsed with a YAML parser, and the GitLab keywords were checked against the GitLab CI/CD YAML reference. They were not run against a live GitLab or GitHub project.

.gitlab-ci.yml
YAML - .gitlab-ci.yml: build, test in parallel, image, staging, manual production
stages: [build, test, package, deploy] default: image: maven:3.9-eclipse-temurin-21 interruptible: true variables: MAVEN_OPTS: "-Dmaven.repo.local=$CI_PROJECT_DIR/.m2/repository" workflow: rules: - if: $CI_PIPELINE_SOURCE == "merge_request_event" - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH .maven-cache: cache: key: files: [pom.xml] paths: [.m2/repository] policy: pull compile: stage: build extends: .maven-cache cache: policy: pull-push script: - mvn -B -DskipTests package artifacts: paths: [target/*.jar] expire_in: 1 day unit-test: stage: test extends: .maven-cache needs: [compile] script: - mvn -B test artifacts: when: always reports: junit: target/surefire-reports/TEST-*.xml integration-test: stage: test extends: .maven-cache needs: [compile] services: - postgres:16 variables: POSTGRES_PASSWORD: ci-only-password script: - mvn -B verify -Pintegration-tests artifacts: when: always reports: junit: target/failsafe-reports/TEST-*.xml docker-image: stage: package image: docker:27 services: [docker:27-dind] needs: [unit-test, integration-test] rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH script: - echo "$CI_REGISTRY_PASSWORD" | docker login -u "$CI_REGISTRY_USER" --password-stdin "$CI_REGISTRY" - docker build -t "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" . - docker push "$CI_REGISTRY_IMAGE:$CI_COMMIT_SHORT_SHA" deploy-staging: stage: deploy needs: [docker-image] rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH environment: name: staging url: https://staging.example.com script: - ./deploy.sh staging "$CI_COMMIT_SHORT_SHA" deploy-prod: stage: deploy needs: [deploy-staging] rules: - if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH when: manual environment: name: production url: https://www.example.com resource_group: production id_tokens: AWS_TOKEN: aud: sts.amazonaws.com script: - ./deploy.sh production "$CI_COMMIT_SHORT_SHA"
KeywordWhat it does in the file above
stagesDefines the order: build, test, package, deploy. Jobs in the same stage run in parallel
defaultSettings applied to every job: the Maven image, and interruptible: true so a newer pipeline on the same ref can cancel this one
workflow:rulesDecides whether the pipeline is created at all: here only for merge requests and for the default branch, avoiding duplicate branch and merge request pipelines
.maven-cache + extendsA hidden job (name starts with a dot) used as a template. cache:key:files derives the cache key from pom.xml; the pull policy downloads without uploading, and only compile uses pull-push
needsTurns the pipeline into a graph: unit-test starts as soon as compile finishes, without waiting for other jobs of the previous stage
artifacts + expire_inPasses the JAR to later jobs and deletes it after a day. artifacts:reports:junit lets GitLab show test results in the merge request
rules with when: manualThe production deploy waits for a person to start it
environmentRecords deployments to named environments with a URL, giving a deployment history and rollback entry points in the UI
resource_groupEnsures only one production deploy runs at a time across pipelines
id_tokensRequests a JWT with the given audience for OIDC to AWS (chapter 06)

Two details are worth knowing. The cache is best-effort and keyed, while artifacts are guaranteed hand-offs (chapter 03). And rules replaced the older only/except, which should not be used in new pipelines.

GitHub Actions workflow
YAML - .github/workflows/ci.yml: build with a Maven cache, then deploy to staging via OIDC
name: ci on: pull_request: push: branches: [main] permissions: contents: read concurrency: group: ci-${{ github.ref }} cancel-in-progress: true jobs: build: runs-on: ubuntu-latest timeout-minutes: 20 steps: - uses: actions/checkout@v6 - uses: actions/setup-java@v5 with: distribution: temurin java-version: '21' cache: maven - run: mvn -B verify - uses: actions/upload-artifact@v7 with: name: app-jar path: target/*.jar retention-days: 7 deploy-staging: needs: build if: github.ref == 'refs/heads/main' runs-on: ubuntu-latest environment: staging permissions: id-token: write contents: read steps: - uses: actions/download-artifact@v7 with: name: app-jar - uses: aws-actions/configure-aws-credentials@v6 with: role-to-assume: arn:aws:iam::111122223333:role/orders-deploy-staging aws-region: eu-west-1 - run: aws sts get-caller-identity
  • Trigger: on lists events (pull_request, push to main). Jobs run on fresh runners; needs orders them.
  • **permissions:** the file starts with contents: read for the whole workflow (least privilege) and the deploy job additionally requests id-token: write, required for OIDC.
  • **cache: maven** on actions/setup-java caches the Maven repository, keyed on the POM files (documented in the setup-java README). No separate cache step is needed.
  • **environment: staging** links the job to a GitHub environment, where you can configure required reviewers, wait timers and environment-scoped secrets.
  • **concurrency** with cancel-in-progress cancels superseded runs on the same ref and saves runner minutes.
  • **timeout-minutes** stops a stuck job from burning minutes.
Action versions

The versions in this file (actions/checkout@v6, actions/setup-java@v5, actions/upload-artifact@v7, actions/download-artifact@v7, aws-actions/configure-aws-credentials@v6) are major-version tags that exist as of 2026-10-31, verified through the GitHub API. Newer majors may already exist, and the examples in GitHub's own docs use different ones. For real pipelines pin to a full commit SHA (chapter 10), and let Dependabot or Renovate propose updates.

ConceptJenkinsGitLab CIGitHub Actions
Pipeline fileJenkinsfile.gitlab-ci.yml.github/workflows/*.yml
Where code runsAgents / nodes (labels)Runners (tags)Runners (runs-on)
Group of workstagestages + jobsjobs
Run in parallelparallel { }Same stage, or needsJobs without needs
Conditionswhen { }rules:if: expressions, on: filters
Pass files between jobsstash/unstash, archiveArtifactsartifactsupload-artifact / download-artifact
Dependency cachePlugin or persistent workspacecacheactions/cache or setup-java cache
Manual approvalinputwhen: manual, protected environmentsEnvironment required reviewers
SecretsCredentials store, withCredentialsCI/CD variables (masked, protected)Secrets (repo, org, environment)
OIDC tokenPlugin-dependentid_tokenspermissions: id-token: write
Reusable piecesShared librariesinclude, extends, CI/CD componentsReusable workflows, composite actions
Choosing

The right tool is usually the one next to your code. Use GitLab CI when the code is in GitLab, GitHub Actions on GitHub. Jenkins earns its place when you need very custom or on-premises workflows, or you already operate it. What matters is that the pipeline logic lives in scripts, so a tool change is a translation of orchestration, not a rewrite.

09
Chapter Nine - Environments and GitOps

Promotion, Approvals, Rollbacks and Pull-Based Deployment

Environments, promotion and approvals

An environment is a named target with its own configuration, secrets, access rules and (often) its own cluster or account. Promotion means moving the same artifact from one environment to the next after checks pass.

EnvironmentPurposeDeploy triggerChecks before the next step
dev / previewFast feedback on every merge or merge request; may be ephemeralAutomaticSmoke tests
stagingProduction-like system to verify the exact artifactAutomatic after main is greenSmoke + end-to-end, performance checks as needed
productionReal usersAutomatic (deployment) or after approval (delivery)Post-deploy smoke tests, health and error-rate monitoring (page 07)
  • Approvals are gates for people: a required reviewer on a GitHub environment, a when: manual job or protected environment in GitLab, an input step in Jenkins. Use them where the business or regulation requires a human decision, not as a substitute for tests.
  • Separation of duties: the person who wrote the change should not be the only approver of its production deploy, if your compliance rules say so. Most CI systems can enforce who may approve or deploy.
  • Deployment windows and freezes can be enforced in the pipeline (a schedule or a manual job) as well as by policy.
  • Serialise deployments to one environment (a GitLab resource_group, a GitHub concurrency group, Jenkins disableConcurrentBuilds) so that two pipelines cannot deploy to production at once.
Rollbacks

A deployment is only safe if you can undo it quickly. The common approaches are:

ApproachHowCaveat
Redeploy the previous artifactRun the deploy job again with the previous image tag. Works because artifacts are immutable and keptNeeds the old tag to still exist; retention rules must not delete it
Revert in Git (GitOps)git revert the change in the config repo; the controller applies the old stateSlower than a button, but fully audited and consistent with Git
Platform rollbackkubectl rollout undo, Argo CD rollback to a previous synced revisionFast, but Git and cluster now disagree until you reconcile
Roll forwardShip a fix quicklyRight when the bug is small and the pipeline is fast; wrong when data was corrupted
Feature flag offDisable the feature without redeployingNeeds the flag to exist and to default safe
⚠ Rolling back code across a database migration
  • Rolling the application back does not roll the schema back. Make migrations backward compatible (expand, migrate, contract): add the new column first, deploy code that works with both, and remove the old column in a later release. Otherwise the "previous version" you roll back to cannot run against the new schema.
GitOps with Argo CD and Flux

GitOps applies the pipeline idea to deployment: the desired state of the environment (Kubernetes manifests, Helm values, Kustomize overlays) lives in a Git repository, and an agent running inside the cluster continuously compares it with the live state and makes them match. The diagram at the top of this chapter shows the difference from a classic push deploy.

Push-based deploy vs pull-based GitOps
Push: CI holds cluster credentials and pushes CI pipeline kubectl / helm upgrade Cluster cluster creds in CI CI needs inbound access to the cluster API. Pull (GitOps): an agent inside the cluster reconciles toward Git CI pipeline Config repo (desired state) Cluster Argo CD / Flux (controller) workloads image tag 1. fetch 2. compare Git vs live, 3. apply the diff The agent runs inside the cluster and reaches out to Git. CI never gets cluster credentials. If someone edits the cluster by hand, the agent sees the difference (drift) and reports or reverts it.
Push (CI deploys)Pull (GitOps controller)
Who has cluster credentialsThe CI systemOnly the controller inside the cluster
Network directionCI must reach the cluster APIThe cluster reaches out to Git (and a registry)
Source of truthThe last pipeline runThe Git repository
Drift (someone changes the cluster by hand)Invisible until the next deploy overwrites itDetected continuously; reported, or reverted if self-healing is on
Audit trailPipeline logsGit history: who changed what, when, and why (a pull request)
RollbackRe-run a jobgit revert and let the controller apply it
YAML - Argo CD Application: keep namespace orders in sync with a Git path
apiVersion: argoproj.io/v1alpha1 kind: Application metadata: name: orders-service namespace: argocd spec: project: default source: repoURL: https://git.example.com/platform/orders-config.git targetRevision: main path: overlays/prod destination: server: https://kubernetes.default.svc namespace: orders syncPolicy: automated: prune: true selfHeal: true syncOptions: - CreateNamespace=true
  • source is the Git repository, revision and path that hold the manifests; destination is the cluster and namespace. The apiVersion: argoproj.io/v1alpha1 / kind: Application form is the one in the Argo CD documentation.
  • syncPolicy.automated makes Argo CD apply changes by itself. By default automated sync does not delete resources removed from Git; prune: true enables deleting them, and selfHeal: true makes it revert manual changes to the cluster (documented in the Argo CD automated sync page).
  • Without automated, Argo CD shows the application as OutOfSync and waits for someone to press Sync. That is the GitOps version of a manual approval.
  • Flux takes the same approach with separate controllers: a source controller fetches Git or OCI sources, and Kustomize and Helm controllers apply them (a notification controller sends events). Pick one and learn its model; the concepts transfer.

A typical split is two repositories: the application repo (code, tests, Dockerfile) and a config repo (manifests or Helm values per environment). The application pipeline builds the image, pushes it, and commits the new image tag to the config repo (or opens a pull request there). The GitOps controller does the actual deployment. This also gives promotion a natural form: a promotion from staging to production is a pull request that changes the tag in overlays/prod.

⚠ Putting secrets in the GitOps repo in clear text
  • Git is forever and widely readable. Keep secrets out of manifests: use a secrets manager with an in-cluster operator (for example External Secrets) or encrypt them before committing (for example Sealed Secrets or SOPS), and only commit the reference or the ciphertext.
10
Chapter Ten - Hardening

Pipeline Speed, Cost, Supply-Chain Security and Common Mistakes

Speed and cost

A slow pipeline is a tax on every developer and a reason to batch changes, which makes failures harder to diagnose. A pipeline that uses too many minutes is a bill. The levers are similar in every tool:

LeverWhat to doTrade-off
Run lessPath filters (skip the Java pipeline when only docs changed), skip stages with nothing to doFilters must be right or you skip needed checks
Run in parallelSplit independent work (unit tests, static analysis, scans); shard slow test suitesMore runners at once; parallel jobs need isolated resources
Fail fastCheap checks first (format, compile, unit); stop on the first failureYou may fix one error at a time instead of seeing all
CacheDependencies keyed on pom.xml; Docker layer cache; build cacheStale or poisoned cache risk (chapter 03)
Cancel redundant runsGitLab interruptible, GitHub concurrency with cancel-in-progressNever cancel a deploy or release run in progress
Right-size runnersMeasure before buying bigger machines; builds are often I/O or network boundCost per minute vs minutes saved
Optimise the DockerfileCopy dependency files first so layers are reused; multi-stage builds; small base imageMore Dockerfile complexity
Set timeouts and retentionJob timeouts, artifact expiry, log and cache retentionDo not expire artifacts you still need for rollback
Measure first

Look at the pipeline duration split by stage over a week. The slowest stage and the most frequently run pipeline are where optimisation pays. A common target: the merge request pipeline gives a result in the time it takes to get a coffee, not to eat lunch.

Pipeline security and the software supply chain

Your pipeline compiles other people's code (dependencies), runs scripts and holds deploy credentials. An attacker who compromises any of these ends up in production. Defences are mostly about knowing and pinning what you run:

  • Pin versions. Do not use floating references such as latest for base images, build tools or CI actions. Pin images to a tag or digest, and pin third-party actions to a full-length commit SHA, which GitHub's security guidance recommends because a tag can be moved to a different commit.
  • Automate updates with Dependabot or Renovate so pinned versions do not rot, and review the pull requests.
  • Lock dependencies. Use a lockfile or a controlled Maven repository manager (Nexus, Artifactory) as a proxy so builds do not pull directly from arbitrary sources.
  • Least privilege. Default workflow permissions to read-only; give each job the minimum; separate roles for plan and apply, for build and deploy.
  • Isolate runners. Prefer ephemeral runners (fresh VM or container per job). A long-lived shared runner can leak files and credentials between jobs.
  • Do not run untrusted code with secrets (forks, untrusted merge requests), and avoid interpolating untrusted input (branch names, PR titles) into shell commands in workflow files.
  • Produce an SBOM and sign artifacts where your organisation requires it. The SLSA framework (Supply-chain Levels for Software Artifacts, slsa.dev) is a specification that defines progressively stronger levels of build integrity and provenance; use it as a vocabulary for how far along you are.
Common mistakes
⚠ Common pipeline mistakes
  • Rebuilding for production instead of promoting the tested artifact.
  • Long-lived cloud keys stored in CI when OIDC is available.
  • **latest tags** and mutable artifacts: no reproducibility, no rollback.
  • Ignoring flaky tests until nobody trusts red builds.
  • A pipeline only the CI tool can run: logic hidden in UI configuration or giant YAML, impossible to reproduce locally.
  • No timeouts on jobs and approvals, so stuck runs block the queue.
  • Shared mutable test environments that make results depend on who else is running.
  • Skipping the gate "just this once" until the gate means nothing.
  • Treating the pipeline as a snowflake: no review, no versioning of the pipeline definition. It is production code.
11
Chapter Eleven - Interview

Questions You Should Be Able to Answer

Crisp answers for the questions that come up in interviews on this topic.

  • Q: What is the difference between continuous integration, continuous delivery and continuous deployment?
    CI: developers merge often and every change is built and tested automatically. Continuous delivery: every change that passes the pipeline is releasable, and production deploys need a manual decision. Continuous deployment: no manual step, a green pipeline goes to production by itself.
  • Q: What does "build once, deploy many" mean and why does it matter?
    The artifact is built a single time, stored under an immutable identifier, and the same bytes are promoted through every environment with only configuration changing. It guarantees that production runs what was tested, and it makes rollbacks a redeploy of an earlier artifact.
  • Q: Why is trunk-based development a good match for CI/CD?
    Short-lived branches mean small merges, early integration and fast feedback. Unfinished work is hidden behind feature flags, so main stays releasable. Long-lived branches delay integration, which is the problem CI is meant to solve.
  • Q: How do you speed up a slow Maven pipeline?
    Cache ~/.m2 keyed on pom.xml, run independent stages in parallel, fail fast with cheap checks first, avoid rebuilding, split slow test suites, skip pipelines when only docs changed, cancel superseded runs, and measure per-stage time before optimising.
  • Q: What is the difference between a cache and an artifact?
    A cache is a best-effort speed-up (dependencies) that may be missing; an artifact is a guaranteed output handed to later jobs or users (a JAR, a report). Do not rely on a cache for correctness and do not cache build output.
  • Q: How should secrets be handled in a pipeline?
    Store them in the platform's secret store, scope them to protected branches and environments, never print them, do not expose them to untrusted code, rotate them, and prefer short-lived credentials via OIDC instead of long-lived keys.
  • Q: How does OIDC federation from GitHub Actions or GitLab to AWS work?
    The CI platform issues a signed JWT for each job. AWS is configured with the platform as an IAM OIDC identity provider and a role whose trust policy allows sts:AssumeRoleWithWebIdentity for a specific audience and subject. The job exchanges the token for temporary credentials. No long-lived key is stored, and the subject condition limits which repository, branch or environment can assume the role.
  • Q: What is a flaky test and what do you do about it?
    A test that sometimes fails on unchanged code, usually through timing, shared state, external dependencies or clocks. Fix the cause, quarantine it into a non-blocking job with an owner and deadline meanwhile, and avoid masking it with blanket retries.
  • Q: What is a quality gate?
    An automated pass/fail rule based on measurements, such as tests passing, coverage and no new serious issues on new code, and no critical vulnerabilities. A failing gate stops the merge or the release. Applying it to new code makes it workable on legacy codebases.
  • Q: Declarative vs scripted Jenkins pipelines?
    Declarative has a structured, validated pipeline { } syntax with stages, agents, post and when, and is the recommended default. Scripted is free-form Groovy: more flexible, easier to make unmaintainable. Complex logic should live in scripts or a shared library either way.
  • Q: How do rules, needs and workflow:rules differ in GitLab CI?
    rules decides whether a job is added to a pipeline, needs lets a job start as soon as the jobs it depends on finish regardless of stage, and workflow:rules decides whether the whole pipeline is created.
  • Q: Push-based deployment vs GitOps?
    In push, CI holds cluster credentials and applies changes. In GitOps an in-cluster controller (Argo CD, Flux) pulls the desired state from Git and reconciles. GitOps removes cluster credentials from CI, gives a Git audit trail, and detects drift.
  • Q: How do you roll back a bad release?
    Redeploy the previous immutable artifact, or revert the change in the config repo for GitOps, or turn off a feature flag. Make database migrations backward compatible so the old code can still run. Decide beforehand when to roll forward instead.
  • Q: How would you secure a CI pipeline against supply-chain attacks?
    Pin base images and actions (commit SHA), use lockfiles and a proxy repository, scan dependencies and images, give jobs least privilege, use ephemeral isolated runners, do not run untrusted code with secrets, sign and record provenance of artifacts.
  • Q: Why not tag images latest?
    It is mutable, so a deployment cannot be reproduced or rolled back by name, and different nodes may run different content under the same tag. Use immutable tags (commit SHA or version) or digests.
12
Chapter Twelve - Summary

Cheat Sheet and Recap

Cheat sheet
TopicRememberTool / keyword
CI vs CD vs CDIntegrate often; always releasable; no human gateDelivery needs approval, deployment does not
BranchingShort-lived branches into main, flags for unfinished workTrunk-based development
Java buildUnit tests in test, integration tests in verifySurefire, Failsafe, mvn -B -ntp verify
CacheDependencies only, keyed on pom.xmlGitLab cache:key:files, setup-java cache: maven
ArtifactBuild once, immutable tag or digest, promoteNever latest; same image in every environment
GatesTests, new-code coverage, static analysis, scansSonarQube quality gate, Trivy --exit-code
SecretsMasked, scoped, never printed; OIDC over keysGitHub id-token: write, GitLab id_tokens
JenkinsDeclarative, agents per stage, post, withCredentialspipeline { agent stages post }
GitLabStages, needs graph, rules, environment.gitlab-ci.yml
GitHub ActionsLeast-privilege permissions, pin actions by SHA.github/workflows/*.yml
ApprovalsHumans for decisions, tests for correctnessinput, when: manual, environment reviewers
GitOpsGit is the desired state; controller pulls and reconcilesArgo CD syncPolicy.automated, Flux
RollbackRedeploy previous artifact or revert in GitBackward-compatible migrations
Supply chainPin, lock, scan, least privilege, ephemeral runnersDependabot/Renovate, SLSA vocabulary
∑ CI/CD pipelines - what to remember
  • A pipeline makes the path from commit to production automatic, repeatable and boring, so that failures are found in minutes.
  • Integrate often. Without short-lived branches, CI is only a build server.
  • Build once, deploy many. Immutable artifact, environment-specific configuration injected at deploy time.
  • Fast checks first, expensive ones later, gates that always mean something. Treat flaky tests as pipeline bugs.
  • No long-lived cloud keys in CI. Use OIDC with a tight sub condition, and keep secrets out of logs and out of untrusted code.
  • The tool is a dialect. Jenkins, GitLab CI and GitHub Actions share the same concepts. Keep logic in scripts and Maven goals.
  • GitOps pulls instead of pushes, which removes cluster credentials from CI and makes drift visible.
  • Pin and least-privilege everything the pipeline touches. The pipeline is production code and a prime target.
Version note

Checked on 2026-10-31: Jenkins directives and steps against jenkins.io (Pipeline syntax and plugin step references); GitLab keywords (rules, needs, cache:key:files, cache:policy, artifacts:expire_in, environment, resource_group, interruptible, id_tokens) against the GitLab CI/CD YAML reference and the GitLab AWS OIDC page; GitHub OIDC requirements (id-token: write, provider URL, aud and sub conditions) against docs.github.com; action major versions against the GitHub API; Argo CD Application and automated sync (prune, selfHeal) against the Argo CD docs; Trivy flags against trivy image --help (0.76.1); SonarQube quality gate concepts against the SonarQube docs. The YAML and JSON files were parsed; the Jenkinsfile, GitLab and GitHub files were not executed on real servers. Tool versions move quickly: re-check keywords against your version.

Next in this section