Freestyle versus Pipeline Jobs

Scenario: A release process lives in UI text boxes, cannot be reviewed in a pull request, and differs after disaster recovery.

Two very different ways to describe a build

Jenkins actually offers two fundamentally different ways to define what a job does, and understanding why one largely replaced the other in modern practice is important context before you touch a real Jenkinsfile.

Analogy: A Freestyle job is like giving someone step-by-step verbal instructions over the phone: "first do this, then click that checkbox, then add this build step." It works, but there's no written record anyone can review beforehand, and if the phone call is forgotten, the instructions are gone. A Pipeline, defined in a Jenkinsfile, is like a written recipe checked into the same cookbook as the dish it makes - it can be reviewed by a second person before anyone cooks with it, it has a version history showing exactly what changed and when, and if the kitchen burns down, the recipe survives because it was never only stored in the kitchen's memory.

This is why the Jenkins community moved so heavily toward Pipeline-as-code: not because Freestyle jobs "don't work," but because a release process typed into UI checkboxes has no code review, no diff, and no guaranteed recovery path if the Jenkins controller itself is lost.

Pipeline example

pipeline { agent any; stages { stage('Package') { steps { sh './package.sh' } } } }

Delivery configuration needs a history you can review

A Freestyle job stores its builders, publishers, wrappers, and triggers as XML inside the Jenkins controller's own configuration. It's approachable for a single simple command or a legacy integration, but as a release process grows more complex, it becomes a pile of UI form fields with weak code review and no real rollback story - you can't diff two versions of a Freestyle job's config the way you diff two commits. Pipeline represents stages, agent selection, conditions, credential usage, parallelism, and post-build processing as actual code, in a text file called a Jenkinsfile that lives beside the application source. That means the Jenkinsfile changes through the exact same pull-request review process as the application code it builds and deploys.

Pipeline as code isn't automatically good code

Switching to Pipeline doesn't automatically create quality - a thousand-line Groovy script with hidden shell logic buried inside script { } blocks can genuinely be worse than a small, simple Freestyle job, because now the complexity is hidden inside a general-purpose scripting language instead of visible UI fields. Keep stage names aligned with delivery states a human would recognize (Build, Test, Deploy - not "Step1", "Step2"), push deterministic build commands into scripts inside the repository (so a developer can run ./build.sh locally, not just inside Jenkins), and make each step restart-aware where practical. Declarative syntax (the pipeline { agent {...} stages {...} } structure used throughout this course) offers validation and a visible, predictable structure. Scripted syntax offers dynamic Groovy control flow at the cost of extra complexity risk, covered in the next module.

Migrating a real job without losing behavior

When converting a Freestyle job to Pipeline, inventory everything it currently does: SCM settings, credentials, environment variable injection, tool versions, triggers, concurrency settings, builders, publishers, retention policy, and permissions. Convert one piece at a time and compare the output of the old and new job for the same commit before cutting over. Publishers like JUnit test reporting and artifact archiving usually belong in a post { always { ... } } block, so a failing shell command in an earlier stage can't accidentally skip publishing the evidence of why it failed. Pin exact tool versions rather than inheriting whatever an administrator has configured as the shifting "latest" - otherwise an unchanged Jenkinsfile can silently produce different build output next month.

[Old Freestyle job #88]  Result: SUCCESS  (used Node.js: "latest" -> resolved to 20.11.0)
[New Pipeline job #1]    Result: SUCCESS  (used Node.js: "20.9.0", pinned in Jenkinsfile)

Even though both show SUCCESS, notice the actual Node.js version differs - this is exactly the kind of silent drift that Pipeline's explicit tools directive (covered later in this track) is designed to eliminate.

A pitfall: running old and new jobs side by side too long, or not long enough

Disable the old Freestyle job's trigger before enabling the new Pipeline's trigger, so both don't end up publishing the same version at the same time under different job names - a real incident pattern is two jobs both deploying to the same environment moments apart, each unaware of the other. But don't delete the legacy job immediately either: keep it around, read-only, just long enough to compare its recorded evidence against the new Pipeline's first few real runs. Migration is genuinely complete only when a reviewer can understand the whole workflow just from reading the source, a freshly rebuilt Jenkins controller could recreate the job from that same source, a failed run still keeps its reports, and any shipped artifact can be traced back to exactly one Jenkinsfile revision and one source commit.

Goal: Practice the concrete behavior in jenkins-declarative-rescue. Learn sections have no Jenkins terminal; the Jenkins lab opens the real shared service in a new full-screen tab inside your private folder.