Frontend Development 40 min read

Implementing Real-Time Jenkins Build Log Retrieval with WebSocket in a Frontend Deployment Platform

This tutorial walks through building a front‑end publishing platform that initiates Jenkins builds, uses dynamic routing, and leverages Socket.io to stream real‑time build logs to the browser, covering both front‑end Vue components and back‑end Node integration.

Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Rare Earth Juejin Tech Community
Implementing Real-Time Jenkins Build Log Retrieval with WebSocket in a Frontend Deployment Platform
Statement: This article is the first exclusive signed article on the Rare Earth Juejin Tech Community, prohibited from reposting within 14 days, and after 14 days reposting is also prohibited without authorization; infringement will be prosecuted!

Hello everyone! This article reaches the final stage of the practical front‑end publishing platform, obtaining Jenkins build logs. Here you will learn how to work with full‑stack WebSocket, mastering its practical application scenarios and skills! Without further ado, read on.

Series articles:

Overview of front‑end automated deployment process, how to achieve a front‑end publishing platform? Article link

Front‑end publishing platform node server practice! Article link

Front‑end publishing platform jenkins practice! How to achieve front‑end automated deployment? Article link

Front‑end publishing platform full‑stack practice (complete front‑ and back‑end development)! Develop a front‑end publishing platform article link

websocket full‑stack practice, implement unique build instance + log synchronization

Reviewing the previous implementation, we have already run the entire automated deployment pipeline of the publishing platform; we are just a step away from completing the whole platform. This article continues the previous hands‑on work. The main goals are:

Front‑end initiates jenkins build

Hands‑on websocket to fetch Jenkins build logs

Quick Look at Source Code

Front‑end project repository

Back‑end project repository

1. Front‑end Build Page

Review of the current front‑end UI implementation:

From the previous article, the CRUD for build configuration information has been completed. The next step is to trigger a Jenkins build from the front‑end. The idea is that the user clicks the configuration row in the table view, navigates to the configuration detail page, and performs the build there.

1. Front‑end Dynamic Route Configuration

The entry to the detail page is actually the table . By using each configuration's unique identifier (e.g., id) together with Vue's dynamic routing, we can render the configuration detail page.

First we modify the table column to make the project name clickable:

<
el-table-column
label="项目名称" prop="projectName">
<!-- Add @click, pass rowData, navigate to detail by id -->
<template #default="scope">
<el-button type="primary" link @click="handleToDetail(scope.row)">{{ scope.row.projectName }}</el-button>
  </template>
</el-table-column>

The UI after the change looks like:

Next we need to add a dynamic route entry and create a ConfigDetail folder under the pages directory.

Configure dynamic route: ConfigDetail { path: '/configDetail/:id', component: () => import('@/pages/ConfigDetail/index.vue'), name: 'ConfigDetail' },

Add ConfigDetail component:

Implement navigation function handleToDetail : const handleToDetail = (rowData) => { // rowData is the parameter passed from the table router.push({ name: "ConfigDetail", params: { id: rowData._id } }) }

After these steps, clicking a row in the table opens the configuration detail page:

2. Configuration Detail Page

The requirements for the detail page are:

Display configuration information

Trigger a build

Show build progress (print build logs)

Edit build information

In this article we focus on the first three points.

To display configuration info we obtain the configuration id from the URL and query the back‑end for details (the back‑end previously only provided pagination).

Adding the back‑end API:

Add a query interface for configuration details: router.get('/jobDetail', controller.getConfigDetail)

Implement getConfigDetail : export async function getConfigDetail(ctx, next) { try { const { id } = ctx.request.query; const data = await services.findJobDetail(id); ctx.state.apiResponse = { code: RESPONSE_CODE.SUC, data }; } catch (e) { ctx.state.apiResponse = { code: RESPONSE_CODE.ERR, msg: 'Configuration detail query failed' }; } next(); } The service simply calls Mongoose: export function findJobDetail(id) { return JobModel.findById(id); }

After the back‑end API is ready, test it with Postman:

In the front‑end component, fetch the data in the onMounted hook and store it in a reactive object (implementation omitted for brevity). The UI finally looks like:

The page is divided into three sections: configuration info area, operation area, and log area. At this point the front‑end is ready to trigger a build, and we move on to the WebSocket implementation.

2. WebSocket Practice

The main purpose of this stage is to display the current Jenkins build log in real time on the front‑end.

1. Initialize Socket

Install the socket.io package for both front‑end and back‑end (v4):

Front‑end project: pnpm install socket.io-client

Back‑end project: pnpm install socket.io

After installation, initialize the socket and test connectivity.

Front‑end initialization (placed in the configuration detail page's onMounted hook): const initSocket = () => { const { id } = route.params; const ioInstance = io({ // The back‑end will also expose a /jenkins/build route path: '/jenkins/build', query: { id } }); // After initialization, listen for events emitted by the back‑end ioInstance.on('', function() {}); };

Back‑end initialization (create the /jenkins/build route): export default function initBuildSocket(httpServer) { // Implement /jenkins/build route const io = new Socket(httpServer, { path: '/jenkins/build' }); // When a connection event occurs, execute controller.socketConnect io.on('connection', controller.socketConnect); } In controller.socketConnect we obtain the socket instance and can use on / emit to communicate with the front‑end. export function socketConnect(socket) { console.log('connection suc'); socket.on('', function() {}); }

Because the front‑end and back‑end run on different ports, we need to configure CORS / devServer proxy for the /jenkins path. The Vite config proxy entry looks like:

'/jenkins'
: {
  target: 'http://localhost:3200',
  changeOrigin: true
}

After all preparations, start the front‑end project and observe the network panel:

Multiple HTTP polling requests appear under the Network > fetch/xhr section.

WebSocket connection to /jenkins/build is established.

Node debugging console prints "connection suc".

2. Socket Synchronize Build Logs

The core of this stage is to emit build logs from the back‑end to all connected front‑end sockets.

Front‑end triggers a build by emitting build:start :

const handleBuild = () => {
  // Emit 'build:start' when the build button is clicked
  ioInstance.value.emit('build:start');
};

socket.on('build:start', async function() {
  console.log('build start');
  const jobName = 'test-config-job';
  const config = await jobConfig.findJobById(id);
  await jenkins.configJob(jobName, config);
  const { buildNumber, logStream } = await jenkins.build(jobName);
  console.log('buildNumber', buildNumber);
});

The back‑end forwards the log stream to the front‑end:

logStream.on('data', function(text) {
  console.log(text);
  socket.emit('build:data', text);
});
logStream.on('error', function(err) {
  console.log('error', err);
  socket.emit('build:error', err);
});
logStream.on('end', function() {
  console.log('end');
  socket.emit('build:end');
});

Front‑end receives the log events and displays them in a <pre> block:

const stream = ref('');
const initLogStream = () => {
  ioInstance.value.on('build:data', function(data) {
    stream.value = data;
  });
  ioInstance.value.on('build:error', function(err) {});
  ioInstance.value.on('build:end', function() {});
};
<pre>{{ stream }}</pre>

After clicking the build button, the front‑end shows the real‑time log as demonstrated in the screenshot below:

3. Global Unique Build Instance

To ensure that all users viewing the same configuration see the same build status, we maintain a global map of Build instances keyed by configuration id.

export default class Build {
  constructor(id, delBuilderFn) {
    this.id = id;               // configuration id
    this.isBuilding = false;    // build status
    this.logStream = null;      // log stream instance
    this.logStreamText = '';    // accumulated log text
    this.buildNumber = '';
    this.delBuilderFn = delBuilderFn; // function to delete from map
  }

  async build(socket) {
    this.isBuilding = true;
    const jobName = 'test-config-job';
    const config = await jobConfig.findJobById(this.id);
    await jenkins.configJob(jobName, config);
    const { buildNumber, logStream } = await jenkins.build(jobName);
    this.buildNumber = buildNumber;
    this.logStream = logStream;
    this.logStream.on('data', (text) => {
      this.logStreamText += text; // accumulate log
    });
    this.initLogStream(socket);
  }

  initLogStream(socket) {
    if (!this.logStream) return;
    this.logStream.on('data', () => {
      socket.emit('build:data', this.logStreamText);
    });
    this.logStream.on('error', (err) => {
      socket.emit('build:error', err);
      this.isBuilding = false;
      this.delBuilderFn(this.id);
    });
    this.logStream.on('end', () => {
      socket.emit('build:end');
      this.isBuilding = false;
      this.delBuilderFn(this.id);
    });
  }

  destroy() {
    this.id = null;
    this.isBuilding = null;
    this.logStream = null;
    this.logStreamText = null;
    this.buildNumber = null;
  }
}

The Admin class manages the global map:

import Build from './index';

class Admin {
  constructor() {
    this.map = {};
  }

  getBuilder(id, socket) {
    if (Reflect.has(this.map, id)) {
      this.map[id].initLogStream(socket);
      return this.map[id];
    }
    return this.createBuilder(id);
  }

  createBuilder(id) {
    const builder = new Build(id, this.delBuilder.bind(this));
    this.map[id] = builder;
    return builder;
  }

  delBuilder(id) {
    this.map[id] && this.map[id].destroy();
    Reflect.deleteProperty(this.map, id);
  }
}

export default new Admin();

The socket connection handler now obtains the builder from adminInstance and triggers the build:

export function socketConnect(socket) {
  console.log('connection suc');
  const { id } = socket.handshake.query;
  const builder = adminInstance.getBuilder(id, socket);
  socket.on('build:start', async function() {
    console.log('build start');
    await builder.build(socket);
  });
}

With this design, opening two browser tabs for the same configuration shows synchronized logs, as shown below:

Conclusion

The article demonstrates a full‑stack implementation of a front‑end publishing platform, covering configuration management, Jenkins build triggering, and real‑time log streaming via WebSocket. While the demo code is functional, production‑grade systems would require additional design, memory‑leak prevention, and more robust error handling.

frontendVueWebSocketJenkinsSocket.ionodeBuild Log
Rare Earth Juejin Tech Community
Written by

Rare Earth Juejin Tech Community

Juejin, a tech community that helps developers grow.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.