Jenkins CLI + Emacs = Butler

Published 2026-09-01

tag(s): #programming #overblown-minor-annoyances #emacs

This is yet another gripping tale of "do everything in Emacs". In this case running Jenkins jobs.
It is also a good overview of my current approach to building Emacs packages. Which is by half-assing them.

The scenario

We run deployments at work with an in-house CLI tool.
Why not use Jenkins/Octopus/Bamboo/Whatever directly to move code around? I have no idea.
Actually, a better way to put it is: I think we could do it, maybe even should. But I am not idealistic enough to have volunteered to, you know, actually make it happen.
I accepted the CLI as part of our workflow: it is mature, stable, and at this point I can modify to support whatever we require.
Just accept that part. Like I do. :)

Also, as I said, it is a CLI. I always have a shell buffer open in the server. Time to run a deploy? Easy peasy.
Except that the powers that be decided we need to click around a web page instead of an easy to use CLI. Why, oh why, misguided management...
... well, actually, there is a couple very good reasons. By running the tool via Jenkins we get traceability, limit the users that can run the tool, and get log output archived automatically. Hard to argue with that many benefits. :)
But! I still wanted to run deployments without leaving Emacs. What to do?

Minimum viable product

If you peek around the emacs-utils repository, you will find a bunch of half-baked packages. Most of them consume a CLI tool, which tends to be simpler than calling an API yourself.[1]
And they are half-baked for a reason. My days of trying to build an all-encompassing tool are behind me. Sharper, the dotnet wrapper, was probably the last attempt.

Nowadays, I am fluent enough in elisp that I can put together a small set of commands to accomplish what I need now, and then I can add features as time goes by. Focus only on the low hanging fruit each time. I call these "proto-packages".

As an example, I wrote commands to upload/download from Azure blob storage asynchronously using AZ CLI in like ten minutes, the day I needed them. A couple weeks later, since I was still using blob storage regularly, I turned that starting point into a basic "directory" navigation.
How basic? Enough for I needed regularly, and no more: list files, move up/down the tree, upload something at current level.

Butler is born

Putting together the last two paragraphs - is there a CLI that I can use to run deployments in Jenkins directly, without opening the browser first?
Yes, there is a Jenkins CLI.
What if I write a small interaction library for said tool, so I can then add a command to run deploys from Emacs?
Sure thing, here is butler.
As it stands now, it offers a grand total of TWO commands, one to read the console output of the last job execution, and another to launch one. The package innards are modified versions of the ones I used in the GH CLI and AZ CLI proto-packages.

Can I use butler-run-job to start a deployment directly from Emacs?
Indeed:


(require 'butler)
(setopt butler-url "https://employer.jenkins.url:9999"
        ;; it's important to use `expand-file-name', I run this in Windows and Linux :)
        butler-jar-path (expand-file-name "~/.local/bin/jenkins-cli.jar"))

(defun hoagie-colo-deploy ()
  "Interactively run a colo deploy.
Prompts for a command and environment."
  (interactive)
  (let ((commands '(deployproject1 deployproject2  ... deployprojectN))
        (environments '(dev uat prod etc etc1 etc2))
        (tickets (read-string "Tickets (csv): ")))
    (butler-run-job "Job Name For Internal  Deployments"
                    (list (concat "COMMAND="
                                  (completing-read "Command: " commands nil t))
                          (concat "ENVIRONMENT="
                                  (completing-read "Environment: " environments nil t))
                          (concat "TICKET="
                                  tickets)))))
    

Mission accomplished!

What's next for Butler? And everything else in that repository

Nothing.
Well, nothing planned.

After the first version of Butler, I added a function to update a job definition. We changed the name of an agent, and since a lot of jobs were impacted, I scripted a mass update using butler. Yak shaving, baby.

But I don't expect to publicize it[2]. Or to try to cover 100% of the features offered by the Jenkins CLI, unless I happen to need exactly that.
Like with everything else in that repo, people are free to copy pieces, fork, submit patches, whatever.

For reasons even I don't understand, I don't feel compelled to write The One Solution For az|gh|jenkins-cli|etc. That Everyone Can Rely On.
I used to, which is why I submitted things to MELPA in the past. But these days? I don't see the point.
Does that make me a "bad community member"? I don't know. Maybe. 🤷

Footnotes
  1. That was the approach I used eons ago, when I wrote Panda.
  2. Beyond this post, I guess.

Share your thoughts (via email)

Back to top

Back to homepage