diff --git a/config.json b/config.json index 4665715..570b3e8 100644 --- a/config.json +++ b/config.json @@ -17,7 +17,8 @@ "urlClient": "https://dev.tombutcher.work", "urlElectronClient": "http://localhost:5780", "urlApi": "https://dev.tombutcher.work/api", - "devAuthClient": "http://localhost:3500" + "devAuthClient": "http://localhost:3500", + "jenkinsProject": "https://ci.tombutcher.work/job/farmcontrol/job/farmcontrol-ui" }, "database": { "mongo": { @@ -77,7 +78,8 @@ "urlClient": "http://localhost:3000", "urlElectronClient": "http://localhost:5780", "urlApi": "http://localhost:8788/api", - "devAuthClient": "http://localhost:3500" + "devAuthClient": "http://localhost:3500", + "jenkinsProject": "https://ci.tombutcher.work/job/farmcontrol/job/farmcontrol-ui" }, "database": { "mongo": { @@ -135,7 +137,8 @@ "urlClient": "https://web.farmcontrol.app", "urlElectronClient": "http://localhost:3000", "urlApi": "https://api.farmcontrol.app", - "devAuthClient": "http://localhost:3500" + "devAuthClient": "http://localhost:3500", + "jenkinsProject": "https://ci.tombutcher.work/job/farmcontrol/job/farmcontrol-ui" }, "database": { "mongo": { diff --git a/src/index.js b/src/index.js index 941c267..79b634b 100644 --- a/src/index.js +++ b/src/index.js @@ -59,6 +59,7 @@ import { excelRoutes, csvRoutes, appLaunchRoutes, + appUpdateRoutes, } from './routes/index.js'; import path from 'path'; import * as fs from 'fs'; @@ -185,6 +186,7 @@ app.use('/rss', rssRoutes); app.use('/excel', excelRoutes); app.use('/csv', csvRoutes); app.use('/applaunch', appLaunchRoutes); +app.use('/appupdate', appUpdateRoutes); // Start the application if (process.env.NODE_ENV !== 'test') { diff --git a/src/routes/index.js b/src/routes/index.js index a14411b..de0d626 100644 --- a/src/routes/index.js +++ b/src/routes/index.js @@ -52,6 +52,7 @@ import rssRoutes from './misc/rss.js'; import excelRoutes from './misc/excel.js'; import csvRoutes from './misc/csv.js'; import appLaunchRoutes from './misc/applaunch.js'; +import appUpdateRoutes from './misc/appupdate.js'; export { userRoutes, @@ -108,4 +109,5 @@ export { excelRoutes, csvRoutes, appLaunchRoutes, + appUpdateRoutes, }; diff --git a/src/routes/misc/appupdate.js b/src/routes/misc/appupdate.js new file mode 100644 index 0000000..21a66cc --- /dev/null +++ b/src/routes/misc/appupdate.js @@ -0,0 +1,13 @@ +import express from 'express'; +import { isAuthenticated } from '../../keycloak.js'; +import { + appUpdateBranchesRouteHandler, + appUpdateCurrentRouteHandler, +} from '../../services/misc/appupdate.js'; + +const router = express.Router(); + +router.get('/branches', isAuthenticated, appUpdateBranchesRouteHandler); +router.get('/current', isAuthenticated, appUpdateCurrentRouteHandler); + +export default router; diff --git a/src/services/misc/appupdate.js b/src/services/misc/appupdate.js new file mode 100644 index 0000000..d28383e --- /dev/null +++ b/src/services/misc/appupdate.js @@ -0,0 +1,122 @@ +import axios from 'axios'; +import config from '../../config.js'; +import log4js from 'log4js'; + +const logger = log4js.getLogger('AppUpdate'); +logger.level = config.server.logLevel; + +const normalizeProjectUrl = (projectUrl) => { + if (typeof projectUrl !== 'string') return ''; + return projectUrl.replace(/\/+$/, ''); +}; + +const getProjectUrl = () => normalizeProjectUrl(config.app?.jenkinsProject); + +const buildApiUrl = (baseUrl, query = '') => { + const cleanBaseUrl = normalizeProjectUrl(baseUrl); + if (!cleanBaseUrl) return ''; + return `${cleanBaseUrl}/api/json${query}`; +}; + +const getBranchBuildApiUrl = (branchUrl, buildType = 'lastSuccessfulBuild') => + `${normalizeProjectUrl(branchUrl)}/${buildType}/api/json?tree=number,url,result,timestamp,artifacts[fileName,relativePath]`; + +const mapArtifacts = (build) => { + const buildUrl = normalizeProjectUrl(build?.url); + const artifacts = Array.isArray(build?.artifacts) ? build.artifacts : []; + console.log(artifacts); + return artifacts.map((artifact) => ({ + fileName: artifact.fileName, + relativePath: artifact.relativePath, + url: `${buildUrl}/artifact/${artifact.relativePath}`, + })); +}; + +const getBranchesFromJenkins = async () => { + const projectUrl = getProjectUrl(); + if (!projectUrl) { + throw new Error('Missing config.app.jenkinsProject'); + } + + const jenkinsUrl = buildApiUrl(projectUrl, '?tree=jobs[name,url,color]'); + const response = await axios.get(jenkinsUrl, { timeout: 10000 }); + const jobs = Array.isArray(response.data?.jobs) ? response.data.jobs : []; + + return jobs + .map((job) => ({ + name: job.name, + url: job.url, + color: job.color, + })) + .filter((job) => typeof job.name === 'string' && typeof job.url === 'string'); +}; + +const getLatestBuildForBranch = async (branchUrl) => { + const buildTypes = ['lastSuccessfulBuild', 'lastBuild']; + + for (const buildType of buildTypes) { + try { + const response = await axios.get(getBranchBuildApiUrl(branchUrl, buildType), { + timeout: 10000, + }); + if (response?.data?.number) { + return { build: response.data, source: buildType }; + } + } catch (error) { + logger.debug(`Failed ${buildType} lookup for ${branchUrl}: ${error.message}`); + } + } + + return { build: null, source: null }; +}; + +export const appUpdateBranchesRouteHandler = async (req, res) => { + try { + const branches = await getBranchesFromJenkins(); + return res.send({ + branches: branches.map((branch) => branch.name), + }); + } catch (error) { + logger.error('Failed to fetch Jenkins branches:', error); + return res.status(500).send({ + error: 'Failed to fetch app update branches', + }); + } +}; + +export const appUpdateCurrentRouteHandler = async (req, res) => { + const requestedBranch = typeof req.query.branch === 'string' ? req.query.branch.trim() : ''; + + if (!requestedBranch) { + return res.status(400).send({ error: 'branch query parameter is required' }); + } + + try { + const branches = await getBranchesFromJenkins(); + const branch = branches.find((item) => item.name === requestedBranch); + + if (!branch) { + return res.status(404).send({ error: `Branch "${requestedBranch}" not found` }); + } + + const { build, source } = await getLatestBuildForBranch(branch.url); + if (!build) { + return res.status(404).send({ error: `No build found for branch "${requestedBranch}"` }); + } + + return res.send({ + branch: requestedBranch, + buildNumber: build.number, + buildUrl: build.url, + buildResult: build.result, + buildTimestamp: build.timestamp, + buildSource: source, + artifacts: mapArtifacts(build), + }); + } catch (error) { + logger.error('Failed to fetch Jenkins build info:', error); + return res.status(500).send({ + error: 'Failed to fetch current app update details', + }); + } +};