Started app update implementation.
All checks were successful
farmcontrol/farmcontrol-api/pipeline/head This commit looks good

This commit is contained in:
Tom Butcher 2026-06-21 01:59:07 +01:00
parent cc1c827c0b
commit de40e0c3ff
5 changed files with 145 additions and 3 deletions

View File

@ -17,7 +17,8 @@
"urlClient": "https://dev.tombutcher.work", "urlClient": "https://dev.tombutcher.work",
"urlElectronClient": "http://localhost:5780", "urlElectronClient": "http://localhost:5780",
"urlApi": "https://dev.tombutcher.work/api", "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": { "database": {
"mongo": { "mongo": {
@ -77,7 +78,8 @@
"urlClient": "http://localhost:3000", "urlClient": "http://localhost:3000",
"urlElectronClient": "http://localhost:5780", "urlElectronClient": "http://localhost:5780",
"urlApi": "http://localhost:8788/api", "urlApi": "http://localhost:8788/api",
"devAuthClient": "http://localhost:3500" "devAuthClient": "http://localhost:3500",
"jenkinsProject": "https://ci.tombutcher.work/job/farmcontrol/job/farmcontrol-ui"
}, },
"database": { "database": {
"mongo": { "mongo": {
@ -135,7 +137,8 @@
"urlClient": "https://web.farmcontrol.app", "urlClient": "https://web.farmcontrol.app",
"urlElectronClient": "http://localhost:3000", "urlElectronClient": "http://localhost:3000",
"urlApi": "https://api.farmcontrol.app", "urlApi": "https://api.farmcontrol.app",
"devAuthClient": "http://localhost:3500" "devAuthClient": "http://localhost:3500",
"jenkinsProject": "https://ci.tombutcher.work/job/farmcontrol/job/farmcontrol-ui"
}, },
"database": { "database": {
"mongo": { "mongo": {

View File

@ -59,6 +59,7 @@ import {
excelRoutes, excelRoutes,
csvRoutes, csvRoutes,
appLaunchRoutes, appLaunchRoutes,
appUpdateRoutes,
} from './routes/index.js'; } from './routes/index.js';
import path from 'path'; import path from 'path';
import * as fs from 'fs'; import * as fs from 'fs';
@ -185,6 +186,7 @@ app.use('/rss', rssRoutes);
app.use('/excel', excelRoutes); app.use('/excel', excelRoutes);
app.use('/csv', csvRoutes); app.use('/csv', csvRoutes);
app.use('/applaunch', appLaunchRoutes); app.use('/applaunch', appLaunchRoutes);
app.use('/appupdate', appUpdateRoutes);
// Start the application // Start the application
if (process.env.NODE_ENV !== 'test') { if (process.env.NODE_ENV !== 'test') {

View File

@ -52,6 +52,7 @@ import rssRoutes from './misc/rss.js';
import excelRoutes from './misc/excel.js'; import excelRoutes from './misc/excel.js';
import csvRoutes from './misc/csv.js'; import csvRoutes from './misc/csv.js';
import appLaunchRoutes from './misc/applaunch.js'; import appLaunchRoutes from './misc/applaunch.js';
import appUpdateRoutes from './misc/appupdate.js';
export { export {
userRoutes, userRoutes,
@ -108,4 +109,5 @@ export {
excelRoutes, excelRoutes,
csvRoutes, csvRoutes,
appLaunchRoutes, appLaunchRoutes,
appUpdateRoutes,
}; };

View File

@ -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;

View File

@ -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',
});
}
};