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) =&gt; (
        <div style="{{">
          <label style="{{"><strong>{key}</strong> : </label>
          {/*  */}
          {
          key === 'Groups' ? (
            
            -- Select a Group -- 
                {groupOptions.map((group) =&gt; (
                  
                    {group}
                  
                ))}
          
          ) : key === 'Module' ? (
            
              {moduleOptions.map((module) =&gt; (
                {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 (
    
      
        &lt;Route path=&quot;/&quot; element={} /&gt;
        &lt;Route path=&quot;/user&quot; element={} /&gt;
      
    
  );
}

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 !!!

Author