Jenkins, for the jobs that are not builds

Keep Jenkins for builds.
Move the 3 a.m. jobs.

This is not a pitch to rip out Jenkins. Jenkins is a build system and it is good at that. But most long-lived Jenkins installs have grown a second population of jobs that were never builds: a freestyle job holding a shell script on a timer. Database backups. Certificate rotation. Log pruning. The monthly report. They ended up in Jenkins because Jenkins was already there and already had cron. SwitchTender takes that half, with one command, and gives it the approvals, host history, and provable audit trail an operational change needs. Your pipelines stay where they are.

Why a backup script on a timer is a bad fit for a CI server

Not because Jenkins is bad. Because a build and a production change are different things, and only one of them is safe to re-run.

Nobody approved it and nothing recorded it

A build is meant to run on every commit without asking. A script that touches production is not. In Jenkins, a freestyle job runs when its timer fires, and what you keep afterward is console output on a retention policy. SwitchTender holds a run for approval where a policy covers it, records who launched it and what they changed, and writes the result into a hash-chained audit trail you can hand to an auditor and they can verify without trusting you or the server.

A shell script inherits the whole plugin tree

The job that runs backup.sh lives inside a JVM with a plugin set that has to be kept current for the builds around it. Every upgrade is a change to the thing your backup runs on. SwitchTender is one static Go binary and one SQLite file, so the schedule that matters at 3 a.m. is not downstream of a plugin bump made for a pipeline.

No memory of the machines

Jenkins remembers builds. Operational work needs the other axis: which host has failed the last four times, what changed on it, when it last came clean. SwitchTender keeps per-host history across every run, spots flaky hosts, and detects drift from a dry run, because the question is not "did the job pass" but "is that machine right."

One command reads your jobs directory

Jenkins has no single export file. Point the importer at the tree, or upload a zip of it, and it reads every job in place. Nothing is created until you ask.

switchtender import jenkins /var/jenkins_home --inventory prod

Folders are followed and each job keeps its full name. Shell steps become one Bash template in order. Parameters become a survey. Build triggers become schedules. Leave off --apply and it only reports what it would create.

The four things the importer refuses to guess

An importer that half-translates a job is worse than one that says it cannot. Each of these is named in the report and left out.

Pipeline jobs

A Jenkinsfile is a Groovy program. There is no honest mechanical translation from one into a template, so Pipeline, multibranch, matrix, and Maven jobs are skipped by name rather than imported as a shell that would not do what the job did. Those are usually your actual builds, and they should stay in Jenkins.

Poll SCM triggers

A poll trigger looks like a schedule and is not one: it asks the repository whether anything changed and builds only if it did, so a job polling every five minutes usually does nothing. Imported as a plain schedule it would run for real every five minutes. For a deploy job that is an incident, not a migration artifact.

Password parameters and trigger tokens

Jenkins stores a password parameter encrypted. A survey answer here is stored in plain text on the run, so importing one would quietly downgrade a secret. It is refused and named, and so is a job's remote build token, which is a shared secret that would hand a launch to anybody holding it.

Jenkins build variables

A step reading $WORKSPACE or $BUILD_NUMBER gets an empty string here, and rm -rf $WORKSPACE/ with WORKSPACE unset is the worst version of that. Every occurrence is found and reported so you supply it or rewrite the step, rather than discovering it at 3 a.m.

Your H schedules survive the trip

Jenkins writes H where a number would go and hashes the job name to spread load, so H 2 * * * means "some minute past two, the same one every time." No cron parser accepts the letter, so a naive import drops every one of those jobs as invalid, and the job silently never runs again. Here they are resolved rather than approximated: the cadence and the window are kept, @daily and its family are expanded the way Jenkins expands them, a spec holding several lines becomes several schedules, and Sunday written as 7 is renumbered to 0 instead of being rejected. The report names the expression each one became.

Where each tool wins

Rows where Jenkins is the right answer are in this table too. It is a build server, and this is not one.

CapabilitySwitchTenderJenkins
Build on commit, per-branch pipelinesNot its job
Test reports and build artifactsNot its job
Plugin ecosystemDeliberately smallVery large
Runs without a JVM
One binary, no plugins to run a job
Approval gates enforced by policyPlugin
Tamper-evident, signed audit trail
Verifiable offline by a third party
Per-host fleet memory and flaky-host detection
Ansible, Terraform, and OpenTofu as first-class toolsVia plugins
Drift detection from a dry run
Imports the other tool's jobsAWX, Semaphore, Rundeck, Jenkins, crontab

Honesty first

Jenkins has two decades of production history, an ecosystem no new tool will match, and an enormous amount of institutional knowledge inside your company. If your freestyle jobs are working and nobody is asking who approved them, there is no reason to move them. This is worth your time when somebody starts asking that question: an audit, a compliance program, a change-control review, or a 3 a.m. job that did something nobody can now reconstruct. Move that subset, keep the pipelines, and run both. Nothing here asks you to choose.

See what it would import, in five minutes

The dry run creates nothing. It reads your jobs and prints exactly what it would make and what it refused.

  1. 1

    Install it

    brew install kordloom/tap/switchtender
  2. 2

    See what it would import

    switchtender import jenkins /var/jenkins_home --inventory prod
  3. 3

    Create the objects

    switchtender import jenkins /var/jenkins_home --inventory prod --apply

The migration guide covers every mapping and every refusal, and the get started guide has prebuilt binaries for Linux, macOS, and Windows with published checksums.

Common questions

The three asked most often when somebody is comparing this against what they already run.

Does it need an agent on each host?

No. It reaches the machines it manages over SSH, the same way Ansible does, and installs nothing on them. There is no per-host daemon to deploy, patch, or account for. You run the one server binary, and optionally a few more worker processes against the same store for throughput, which are pool members rather than agents belonging to a host.

Can it read secrets from AWS Secrets Manager, Azure Key Vault, or Vault?

All three, resolved at launch rather than copied into this database. Vault dynamic secrets go further: a short-lived credential is minted for each run and revoked when the run ends. AWS and Azure both authenticate from an instance role or managed identity with no stored key, and anything else resolves through a command whose output is the secret.

Can I run a Terraform plan, hold it for approval, then run Ansible?

Yes, and it is what pipelines are for here. Steps mix tools freely on a dependency graph with parallel branches, built on a drag-and-drop canvas. The approval is not a convention somebody can skip: a policy decides which runs are held, the core enforces the hold, and the approval binds to the exact plan reviewed, so a run cannot be approved as one thing and executed as another.

Let Jenkins build. Let this one answer for production.