Triggering Jenkins Jobs from a Custom Web UI
As a Quality Engineer (QE), I’ve spent most of my career writing tests, automating workflows, and optimizing pipelines. But recently, I took on a challenge that was a little different—building my first React-based website. The goal? Host a Jenkins runner tool with a frontend UI and a backend service, all bundled and deployed together.
This blog walks you through that journey—what I learned, how I built it, and why stepping beyond test automation can elevate your role as a QE.
A developer on our team was debugging a test suite, which required repeatedly triggering a Jenkins job with various parameters: modules like user removal, environment switches, flags, and emails. The problem? Jenkins’ UI was clunky, inconsistent, and — let’s face it — not built for speed.
“Why do I have to click through four screens and paste a URL just to rerun a test job?” someone muttered across the room.
That’s when it clicked.
What if we could build a simple web app where any dev could select a few options, hit a button, and — BAM! — Jenkins gets to work? and get the actual work done.
The Build
We started with a basic full-stack setup:
Backend: The Bridge
Our /server.js
to handle the application post calls to jenkins :
require('dotenv').config();
const express = require('express');
const cors = require('cors');
const axios = require('axios').default;
const { wrapper } = require('axios-cookiejar-support');
const { CookieJar } = require('tough-cookie');
const app = express();
app.use(cors());
app.use(express.json());
app.post('/trigger-jenkins', async (req, res) => {
const { Module, Environment, Branch, Groups, EmailRecipients } = req.body;
const jenkinsBase = process.env.JENKINS_URL;
const buildUrl = getBuildUrl(Module, jenkinsBase);
console.log(buildUrl)
const crumbUrl = `${jenkinsBase}/crumbIssuer/api/json`;
try {
const jar = new CookieJar();
const client = wrapper(axios.create({ jar }));
const crumbResp = await client.get(crumbUrl, {
auth: {
username: process.env.JENKINS_USER,
password: process.env.JENKINS_TOKEN,
},
});
const crumb = crumbResp.data.crumb;
const crumbField = crumbResp.data.crumbRequestField;
const params = new URLSearchParams({
Module,
Environment,
Branch,
Groups,
EmailRecipients: EmailRecipients || ''
});
// 3. Trigger job using crumb + cookie
const triggerResp = await client.post(buildUrl, params.toString(), {
auth: {
username: process.env.JENKINS_USER,
password: process.env.JENKINS_TOKEN,
},
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
[crumbField]: crumb
},
});
res.send(`✅ Jenkins job triggered. Status: ${triggerResp.status}`);
} catch (err) {
console.error('Trigger error:', err.response?.data || err.message);
res.status(500).json(err.response?.data || { error: err.message });
}
});
function getBuildUrl(module, baseUrl) {
let jobPath;
switch (module) {
case 'Users':
jobPath = process.env.JENKINS_USERS_JOB_PATH;
break;
default:
jobPath = process.env.JENKINS_JOB_PATH;
}
return `${baseUrl}${jobPath}`;
}
const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log(`🚀 Backend running at http://localhost:${PORT}`));
Frontend: A Friendly Face
Our /.pages/UserPage.jsx
featured a form with dropdowns, inputs, and one glorious Trigger button.
import React, { useState } from 'react';
import axios from 'axios';
function UsersPage() {
const [form, setForm] = useState({
Module: '',
Environment: 'dev',
Branch: 'main',
Groups: '',
EmailRecipients: ''
});
const [status, setStatus] = useState('');
const handleChange = (e) => {
setForm({ ...form, [e.target.name]: e.target.value });
};
const handleSubmit = async () => {
setStatus('Triggering Jenkins...');
try {
console.log('Submitting form:', form);
const resp = await axios.post('http://localhost:5000/trigger-jenkins', form);
setStatus(resp.data);
} catch (err) {
setStatus(err.response?.data || 'Error triggering job');
}
};
return (
<div style="{{">
<h2>Trigger Jenkins Job</h2>
{Object.keys(form).map((key) => (
<div style="{{">
<label style="{{"><strong>{key}</strong> : </label>
{/* */}
{
key === 'Groups' ? (
-- Select a Group --
{groupOptions.map((group) => (
{group}
))}
) : key === 'Module' ? (
{moduleOptions.map((module) => (
{module}
))}
):(
)}
<p><strong>{'\u00A0'}{'\u00A0'}{'\u00A0'}{'\u00A0'}{'\u00A0'}{'\u00A0'}{'\u00A0'}{'\u00A0'}{'\u00A0'}
Description : {getDescription(key)} </strong></p>
</div>
))}
{'\u00A0'}{'\u00A0'}{'\u00A0'}{'\u00A0'}<button>Trigger</button>
<p>{status}</p>
</div>
);
}
function getDescription(fieldName) {
switch(fieldName){
case "Module":
return 'Users | User2'
case "Environment":
return 'Dev environment is supported right now'
case "Branch":
return 'Default Branch is main, yet you can send any branch'
case "EmailRecipients":
return 'Enter email separated by commas.'
default:
return 'Nothing defined yet'
}
}
const groupOptions = [
'Select',
'userTest1'
'userTest2'
];
const moduleOptions = [
'Users'
];
export default UsersPage;
/App.js
import React from 'react';
import { BrowserRouter as Router, Routes, Route } from 'react-router-dom';
import UserPage from './pages/UsersPage';
import HomePage from './pages/HomePage';
function App() {
return (
<Route path="/" element={} />
<Route path="/user" element={} />
);
}
export default App;
/.env
to store the job parameters
JENKINS_URL=
JENKINS_USER=
JENKINS_TOKEN=
JENKINS_USERS_JOB_PATH=/job/.../buildWithParameters
JENKINS_JOB_PATH=/job/.../buildWithParameters
Simple, clean, and fast.
No more Jenkins dropdowns from 2010.
The First Trigger
We deployed it locally and let a dev test the waters.
They selected:
- Groups: Test Groups
- Module: Users
- Branch: feature/1234
- Environment: dev
- Email Reciepient: user1@user.com
- Clicked Trigger
Seconds later, Jenkins whirred to life.
No errors. No Jenkins UI. No confusion.
Just:
✅ Jenkins job triggered successfully
Delivering the Results — Email Integration
The Jenkins trigger worked flawlessly.
But developers had a new request:
“Can I get the job results emailed to me? I don’t want to wait on the page or refresh Jenkins every 2 mins.”
Fair ask. Not everyone wants to watch Jenkins logs like it’s a Netflix series.
So we added automated email delivery — once Jenkins finishes a job, users receive a summary and link to the build page using jenkins emailable extension.
Expansion Mode
With the basics in place, we will plan:
- ✅ Added multi-select checkboxes for custom flags
- ✅ Created separate pages like
/jobType2
or/jobType2
- ✅ Dockerized it for consistent deployment
- ✅ Made the backend accept more advanced Jenkins parameters
!!! It became the team’s favourite productivity tool !!!