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

👋 Hello, curious human.
Thanks for reading our Tech Blog.

We build things, break things, and occasionally write about both.
Get the best of our tech stories straight to your inbox.

We don’t spam, and your personal email stays private within OLX India.