diff --git a/api/jenkins.ts b/api/jenkins.ts new file mode 100644 index 0000000..cbcf5de --- /dev/null +++ b/api/jenkins.ts @@ -0,0 +1,108 @@ +import got from "../libs/got"; +import { createBadgenHandler, PathArgs } from "../libs/create-badgen-handler"; +import humanizeDuration from "humanize-duration"; + +export default createBadgenHandler({ + title: "Jenkins", + examples: { + "/jenkins/last-build/jenkins.mono-project.com/job/test-mono-mainline/": + "Last build status", + "/jenkins/fix-time/jenkins.mono-project.com/job/test-mono-mainline/": + "Time taken to fix a broken build", + "/jenkins/broken-build/jenkins.mono-project.com/job/test-mono-mainline/": + "# of broken builds", + }, + handlers: { + "/jenkins/last-build/:hostname/:job*": lastJobStatusHandler, + "/jenkins/fix-time/:hostname/:job*": buildFixTimeHandler, + "/jenkins/broken-build/:hostname/:job*": brokenBuildsHandler, + }, +}); + +const shortEnglishHumanizer = humanizeDuration.humanizer({ + language: "shortEn", + units: ["d", "h"], + round: true, + languages: { + shortEn: { + y: () => "y", + mo: () => "mo", + w: () => "w", + d: () => "d", + h: () => "h", + m: () => "m", + s: () => "s", + ms: () => "ms", + }, + }, +}); + +const statusToColor = (status: string) => { + return status.toUpperCase() === "SUCCESS" ? "green" : "red"; +}; + +const brokenBuildsToColor = (count: number) => { + return count < 10 ? "green" : count < 20 ? "orange" : "red"; +}; +const buildFixTimeToColor = (hours: number) => { + return hours < 2 ? "green" : hours < 6 ? "orange" : "red"; +}; + +async function jenkinsLastBuild({ hostname, job }: PathArgs) { + const endpoint = `https://${hostname}/${job}/lastBuild/api/json?tree=result,timestamp,estimatedDuration`; + return await got(endpoint).json(); +} + +async function jenkinsBuilds({ hostname, job }: PathArgs) { + const endpoint = `https://${hostname}/${job}/api/json?tree=builds[number,status,timestamp,id,result]`; + return await got(endpoint).json(); +} + +async function lastJobStatusHandler({ hostname, job }: PathArgs) { + const response = await jenkinsLastBuild({ hostname, job }); + return { + subject: "Last Build", + status: response.result, + color: statusToColor(response.result), + }; +} + +async function brokenBuildsHandler({ hostname, job }: PathArgs) { + const response = await jenkinsBuilds({ hostname, job }); + const brokenBuilds = response.builds.filter(function (build) { + return build.result.toUpperCase() !== "SUCCESS"; + }); + return { + subject: "Broken Builds", + status: brokenBuilds.length, + color: brokenBuildsToColor(brokenBuilds.length), + }; +} + +async function buildFixTimeHandler({ hostname, job }: PathArgs) { + const response = await jenkinsBuilds({ hostname, job }); + + var lastSuccessTime = 0; + var lastFailTime = 0; + + for (let index = 0; index < response.builds.length; index++) { + const element = response.builds[index]; + if (element.result.toUpperCase() == "SUCCESS") { + lastSuccessTime = element.timestamp; + lastFailTime = lastSuccessTime; + } else { + lastFailTime = element.timestamp; + break; + } + } + if (lastSuccessTime == 0) lastSuccessTime = new Date().getTime(); + if (lastFailTime == 0) lastFailTime = lastSuccessTime; + + return { + subject: "Fix Time", + status: `${shortEnglishHumanizer((lastSuccessTime - lastFailTime) | 0)}`, + color: buildFixTimeToColor( + ((lastSuccessTime - lastFailTime) / 3600000) | 0 + ), + }; +} diff --git a/libs/badge-list.ts b/libs/badge-list.ts index 70c8b51..c1feecb 100644 --- a/libs/badge-list.ts +++ b/libs/badge-list.ts @@ -58,6 +58,7 @@ export const liveBadgeList = [ 'tidelift', 'runkit', 'https', + 'jenkins', ] export async function loadBadgeMeta () { diff --git a/package-lock.json b/package-lock.json index 536b5ab..8a5cea8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4861,9 +4861,9 @@ "dev": true }, "elliptic": { - "version": "6.5.2", - "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.2.tgz", - "integrity": "sha512-f4x70okzZbIQl/NSRLkI/+tteV/9WqL98zx+SQ69KbXxmVrmjwsNUPn/gYJJ0sHvEak24cZgHIPegRePAtA/xw==", + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.5.3.tgz", + "integrity": "sha512-IMqzv5wNQf+E6aHeIqATs0tOLeOTwj1QKbRcS3jBbYkl5oLAserA8yJTT7/VyHUYG91PRmPyeQDObKLPpeS4dw==", "dev": true, "requires": { "bn.js": "^4.4.0", @@ -6114,6 +6114,11 @@ } } }, + "humanize-duration": { + "version": "3.23.1", + "resolved": "https://registry.npmjs.org/humanize-duration/-/humanize-duration-3.23.1.tgz", + "integrity": "sha512-aoOEkomAETmVuQyBx4E7/LfPlC9s8pAA/USl7vFRQpDjepo3aiyvFfOhtXSDqPowdBVPFUZ7onG/KyuolX0qPg==" + }, "iconv-lite": { "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", @@ -7114,9 +7119,9 @@ } }, "minimist": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", - "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.5.tgz", + "integrity": "sha512-FM9nNUYrRBAELZQT3xeZQ7fmMOBg6nWNmJKTcgsJeaLstP/UODVpGsr5OhXhhXg6f+qtJ8uiZ+PUxkDWcgIXLw==", "dev": true }, "minipass": { @@ -7203,12 +7208,12 @@ } }, "mkdirp": { - "version": "0.5.1", - "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", - "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.5.tgz", + "integrity": "sha512-NKmAlESf6jMGym1++R0Ra7wvhV+wFW63FaSOFPwRahvea0gMUcGUhVeAg/0BC0wiv9ih5NYPB1Wn1UEI1/L+xQ==", "dev": true, "requires": { - "minimist": "0.0.8" + "minimist": "^1.2.5" } }, "move-concurrently": { diff --git a/package.json b/package.json index 9e2ab70..3343184 100644 --- a/package.json +++ b/package.json @@ -32,6 +32,7 @@ "date-fns": "^2.15.0", "got": "^10.7.0", "haxe-rpc-client": "^1.0.0", + "humanize-duration": "^3.23.1", "lodash.debounce": "^4.0.8", "measurement-protocol": "^0.1.1", "micro": "^9.3.4",