Case study · PT HM Sampoerna Tbk.
Jenkins to CloudBees Pipeline Migration
Every pipeline was a hand-written Jenkinsfile, and no two looked alike. Migrating to CloudBees was the deadline; standardizing what got migrated was the actual work.
Context
At PT HM Sampoerna, part of Philip Morris International, the application portfolio ran its CI/CD on legacy Jenkins, and the organization was moving to CloudBees for enterprise support and governance. Pipelines had accumulated organically over the years, each application carrying its own Jenkinsfile, written by whoever set that application up, in whatever style they preferred at the time.
The move to CloudBees was the deadline. Everything was going to be touched anyway, which made it the right, and cheapest, moment to standardize what got carried across.
None of this happened on a paused estate, either. Application teams kept shipping features and change requests against Jenkins the entire time the migration was underway. That left no room for the move itself to cause any impact: a pipeline in flight couldn't miss a build, and a team mid-release couldn't be blocked, just because the platform underneath it was changing.
Challenge
Two problems stacked on top of each other, and solving only the first would have wasted the migration. The first was the migration itself: pipelines had to move from legacy Jenkins to CloudBees across dev, staging, and production without stalling delivery.
The second was the inconsistency underneath. Jenkinsfiles had no shared structure: stage names, credential handling, and notification logic all differed per application. Lifting that as-is would have carried years of drift onto the new platform.
Part of why it drifted that far: applications didn't share a vendor stack either. Different teams built against different CI plugins, different deployment targets, different credential stores, whatever their application's own tooling called for. A Jenkinsfile written against one vendor's requirements wasn't just styled differently from another, it was structurally incompatible with it. Standardizing meant reconciling that vendor and tooling spread too, not just tidying up formatting.
Every application had built its own Jenkinsfile around its own vendor stack, and none of that development would pause for the migration. Carrying that spread onto CloudBees as-is wouldn't have modernized anything, it would have rebuilt the exact same drift on newer infrastructure.
Action
The initial plan was to move applications one by one: port the Jenkinsfile, test it on CloudBees, fix whatever broke, check it off, move to the next. At the size of this estate that cycle repeated for every single application, with no part of the work from one carrying over to the next, and it was on track to take months. Templating collapsed that repetition: rather than porting each Jenkinsfile by hand, the pipeline definition became a generated artifact, one canonical template, rendered per application from its own config, so fixing it once fixed it everywhere instead of once per app.
Audit the existing pipelines
Inventory every application's Jenkinsfile and separate the variation that mattered, build tooling, deploy targets, approval gates, from the incidental differences that were just personal style.
Extract the canonical shape
Identify the stages every pipeline genuinely shares, and separate them from the per-application values that only look like structure.
Template it with Jinja2
One Jenkinsfile template, rendered per application from declarative config, the same reasoning behind the YAML-driven Terraform work: keep one reviewed definition, let configuration carry the differences.
Migrate environment by environment
Roll out dev first, then staging, then production, so each environment validated the template before the next inherited it, with the previous platform still available as a fallback until each cutover was confirmed.
app: payment-api
stages: ["build", "test", "deploy"]
deploy_target: ecs
credentials_id: payment-api-creds
pipeline {
agent any
environment {
CREDENTIALS_ID = '{{ credentials_id }}'
}
stages {
{% for stage in stages %}
stage('{{ stage }}') {
steps {
sh './ci/{{ stage }}.sh --target={{ deploy_target }}'
}
}
{% endfor %}
}
}
What that template actually renders: a small per-application config feeding one shared Jenkinsfile.j2, instead of a Groovy file copy-pasted and hand-edited for each application. The {% for stage in stages %} loop generates one stage block per entry in config.yaml, so adding or dropping a stage is a one-line config change instead of a Groovy edit. Credentials and the deploy target follow the same pattern: values the template reads from config, not values hardcoded per application. Change the config, re-render, and the Jenkinsfile that comes out is correct by construction, the same template every application shares.
Tech Stack
Running the four migration steps by hand, per application, was still the same repetition templating was supposed to remove. So I wrote a Python migration script to carry it out in one pass: it works through each environment in order, dev, then staging, then production, and within each environment, one pipeline at a time. For every pipeline it pulls the existing Jenkinsfile, renders it against the new Jinja2 template, pushes the build to CloudBees, and runs the test suite. If that passes, it moves to the next pipeline. If it doesn't, the script stops there, and the fix goes into that specific pipeline's config rather than into the shared template, so one application's edge case never leaks into every other application's build.
Results
The estate landed on CloudBees across all three environments, standardized rather than merely relocated. Structurally, a pipeline change is now a config change reviewed against one template, instead of an edit to one of many independently-written Groovy files, so onboarding a new application means filling in config rather than authoring a pipeline from scratch. The time saved came directly from replacing the port-test-fix-checkmark cycle with the migration script: four weeks against an original 12-week manual plan, across 100+ pipelines. Human error dropped for the same reason. A hand-edited Groovy file can typo a stage name or drop a credential reference and nobody notices until it fails in production. A rendered template can't drift that way, since the same reviewed logic generates every pipeline, so a mistake in config surfaces as an immediate test failure instead of a silent inconsistency somebody finds later.
The template made the pipelines consistent. The migration script is what made 100+ of them achievable in four weeks instead of twelve.