gitlab-radiator 4.4.5 → 5.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -215,6 +215,11 @@ ol.jobs {
215
215
  background-color: @success-background-color;
216
216
  }
217
217
 
218
+ &.manual {
219
+ color: @manual-text-color;
220
+ background-color: @manual-background-color;
221
+ }
222
+
218
223
  &.running {
219
224
  color: @running-text-color;
220
225
  background: repeating-linear-gradient(
@@ -16,6 +16,8 @@
16
16
  @failed-background-color: rgb(204, 208, 0);
17
17
  @running-text-color: @light-text-color;
18
18
  @running-background-color: @success-background-color;
19
+ @manual-text-color: @light-text-color;
20
+ @manual-background-color: @group-background-color;
19
21
 
20
22
  @error-message-text-color: rgb(255, 0, 0);
21
23
  @error-message-background-color: rgb(139, 0, 0);
package/public/index.html CHANGED
@@ -5,7 +5,6 @@
5
5
  <meta http-equiv="x-ua-compatible" content="IE=edge">
6
6
  <meta name="description" content="GitLab build radiator"/>
7
7
  <title>GitLab build radiator</title>
8
- <script src="/socket.io/socket.io.js"></script>
9
8
  <link rel="stylesheet" type="text/css" href="/client.css"/>
10
9
  </head>
11
10
  <body>
package/src/app.js CHANGED
@@ -1,85 +1,74 @@
1
- import {basicAuth} from './auth.js'
2
- import compression from 'compression'
3
- import {config} from './config.js'
4
- import express from 'express'
5
- import fs from 'fs'
6
- import {fetchOfflineRunners} from './gitlab/runners.js'
7
- import http from 'http'
8
- import {serveLessAsCss} from './less.js'
9
- import {Server} from 'socket.io'
10
- import {update} from './gitlab/index.js'
11
-
12
- const app = express()
13
- const httpServer = http.Server(app)
14
- const socketIoServer = new Server(httpServer)
15
-
16
- if (process.env.NODE_ENV !== 'production' && fs.existsSync('./src/dev-assets.js')) {
17
-
18
- const {bindDevAssets} = await import('./dev-assets.js')
19
- bindDevAssets(app)
1
+ import { basicAuth } from "./auth.js";
2
+ import compression from 'compression';
3
+ import { config } from "./config.js";
4
+ import express from 'express';
5
+ import fs from 'fs';
6
+ import { fetchOfflineRunners } from "./gitlab/runners.js";
7
+ import http from 'http';
8
+ import { serveLessAsCss } from "./less.js";
9
+ import { Server } from 'socket.io';
10
+ import { update } from "./gitlab/index.js";
11
+ const app = express();
12
+ const httpServer = new http.Server(app);
13
+ const socketIoServer = new Server(httpServer);
14
+ if (process.env.NODE_ENV !== 'production' && fs.existsSync('./src/dev-assets.ts')) {
15
+ const { bindDevAssets } = await import("./dev-assets.js");
16
+ bindDevAssets(app);
20
17
  }
21
-
22
- app.disable('x-powered-by')
23
- app.get('/client.css', serveLessAsCss)
24
- app.use(express.static('public'))
25
- app.use(compression())
26
- app.use(basicAuth(config.auth))
27
-
18
+ app.disable('x-powered-by');
19
+ app.get('/client.css', serveLessAsCss);
20
+ app.use(express.static('public'));
21
+ app.use(compression());
22
+ app.use(basicAuth(config.auth));
28
23
  httpServer.listen(config.port, () => {
29
-
30
- console.log(`Listening on port *:${config.port}`)
31
- })
32
-
24
+ console.log(`Listening on port *:${config.port}`);
25
+ });
33
26
  const globalState = {
34
- projects: null,
35
- error: null,
36
- zoom: config.zoom,
37
- projectsOrder: config.projectsOrder,
38
- columns: config.columns,
39
- horizontal: config.horizontal,
40
- groupSuccessfulProjects: config.groupSuccessfulProjects
41
- }
42
-
27
+ projects: null,
28
+ error: null,
29
+ zoom: config.zoom,
30
+ projectsOrder: config.projectsOrder,
31
+ columns: config.columns,
32
+ horizontal: config.horizontal,
33
+ rotateRunningPipelines: config.rotateRunningPipelines,
34
+ groupSuccessfulProjects: config.groupSuccessfulProjects
35
+ };
43
36
  socketIoServer.on('connection', (socket) => {
44
- socket.emit('state', withDate(globalState))
45
- })
46
-
37
+ socket.emit('state', withDate(globalState));
38
+ });
47
39
  async function runUpdate() {
48
- try {
49
- globalState.projects = await update(config)
50
- globalState.error = await errorIfRunnerOffline()
51
- socketIoServer.emit('state', withDate(globalState))
52
- } catch (error) {
53
-
54
- console.error(error.message)
55
- globalState.error = `Failed to communicate with GitLab API: ${error.message}`
56
- socketIoServer.emit('state', withDate(globalState))
57
- }
58
- setTimeout(runUpdate, config.interval)
40
+ try {
41
+ globalState.projects = await update(config.gitlabs, config.rotateRunningPipelines > 0);
42
+ globalState.error = await errorIfRunnerOffline();
43
+ socketIoServer.emit('state', withDate(globalState));
44
+ }
45
+ catch (error) {
46
+ const message = error instanceof Error ? error.message : String(error);
47
+ console.error(message);
48
+ globalState.error = `Failed to communicate with GitLab API: ${message}`;
49
+ socketIoServer.emit('state', withDate(globalState));
50
+ }
51
+ setTimeout(runUpdate, config.interval);
59
52
  }
60
-
61
53
  async function errorIfRunnerOffline() {
62
- const offlineRunnersPerGitlab = await Promise.all(config.gitlabs.map(fetchOfflineRunners))
63
- const {offline, totalCount} = offlineRunnersPerGitlab.reduce((acc, runner) => {
64
- return {
65
- offline: acc.offline.concat(runner.offline),
66
- totalCount: acc.totalCount + runner.totalCount
54
+ const offlineRunnersPerGitlab = await Promise.all(config.gitlabs.map(fetchOfflineRunners));
55
+ const { offline, totalCount } = offlineRunnersPerGitlab.reduce((acc, runner) => {
56
+ return {
57
+ offline: acc.offline.concat(runner.offline),
58
+ totalCount: acc.totalCount + runner.totalCount
59
+ };
60
+ }, { offline: [], totalCount: 0 });
61
+ if (offline.length > 0) {
62
+ const names = offline.map(r => r.name).sort().join(', ');
63
+ const counts = offline.length === totalCount ? 'All' : `${offline.length}/${totalCount}`;
64
+ return `${counts} runners offline: ${names}`;
67
65
  }
68
- }, {offline: [], totalCount: 0})
69
-
70
- if (offline.length > 0) {
71
- const names = offline.map(r => r.name).sort().join(', ')
72
- const counts = offline.length === totalCount ? 'All' : `${offline.length}/${totalCount}`
73
- return `${counts} runners offline: ${names}`
74
- }
75
- return null
66
+ return null;
76
67
  }
77
-
78
- await runUpdate()
79
-
68
+ await runUpdate();
80
69
  function withDate(state) {
81
- return {
82
- ...state,
83
- now: Date.now()
84
- }
70
+ return {
71
+ ...state,
72
+ now: Date.now()
73
+ };
85
74
  }
package/src/auth.js CHANGED
@@ -1,21 +1,18 @@
1
- import authenticate from 'basic-auth'
2
-
1
+ import authenticate from 'basic-auth';
3
2
  export function basicAuth(auth) {
4
- if (!auth || !auth.username || !auth.password) {
5
-
6
- console.log('No authentication configured')
7
- return (req, res, next) => next()
8
- }
9
-
10
-
11
- console.log('HTTP basic auth enabled')
12
- return (req, res, next) => {
13
- const {name, pass} = authenticate(req) || {}
14
- if (auth.username === name && auth.password === pass) {
15
- next()
16
- } else {
17
- res.setHeader('WWW-Authenticate', 'Basic realm="gitlab-radiator"')
18
- res.status(401).end()
3
+ if (!auth) {
4
+ console.log('No authentication configured');
5
+ return (req, res, next) => next();
19
6
  }
20
- }
7
+ console.log('HTTP basic auth enabled');
8
+ return (req, res, next) => {
9
+ const { name, pass } = authenticate(req) || {};
10
+ if (auth.username === name && auth.password === pass) {
11
+ next();
12
+ }
13
+ else {
14
+ res.setHeader('WWW-Authenticate', 'Basic realm="gitlab-radiator"');
15
+ res.status(401).end();
16
+ }
17
+ };
21
18
  }
package/src/config.js CHANGED
@@ -1,44 +1,80 @@
1
- import assert from 'assert'
2
- import fs from 'fs'
3
- import os from 'os'
4
- import yaml from 'js-yaml'
5
-
6
- const configFile = expandTilde(process.env.GITLAB_RADIATOR_CONFIG || '~/.gitlab-radiator.yml')
7
- const yamlContent = fs.readFileSync(configFile, 'utf8')
8
- export const config = validate(yaml.load(yamlContent))
9
-
10
- config.interval = Number(config.interval || 10) * 1000
11
- config.port = Number(config.port || 3000)
12
- config.zoom = Number(config.zoom || 1.0)
13
- config.columns = Number(config.columns || 1)
14
- config.horizontal = config.horizontal || false
15
- config.groupSuccessfulProjects = config.groupSuccessfulProjects || false
16
- config.projectsOrder = config.projectsOrder || ['name']
17
- config.gitlabs = config.gitlabs.map((gitlab) => {
18
- return {
19
- url: gitlab.url,
20
- ignoreArchived: gitlab.ignoreArchived === undefined ? true : gitlab.ignoreArchived,
21
- maxNonFailedJobsVisible: Number(gitlab.maxNonFailedJobsVisible || 999999),
22
- ca: gitlab.caFile && fs.existsSync(gitlab.caFile, 'utf-8') ? fs.readFileSync(gitlab.caFile) : undefined,
23
- 'access-token': gitlab['access-token'] || process.env.GITLAB_ACCESS_TOKEN,
24
- projects: {
25
- excludePipelineStatus: (gitlab.projects || {}).excludePipelineStatus || [],
26
- include: (gitlab.projects || {}).include || '',
27
- exclude: (gitlab.projects || {}).exclude || ''
28
- }
29
- }
30
- })
31
- config.colors = config.colors || {}
32
-
1
+ import fs from 'fs';
2
+ import os from 'os';
3
+ import yaml from 'js-yaml';
4
+ import { z } from 'zod';
33
5
  function expandTilde(path) {
34
- return path.replace(/^~($|\/|\\)/, `${os.homedir()}$1`)
35
- }
36
-
37
- function validate(cfg) {
38
- assert.ok(cfg.gitlabs, 'Mandatory gitlab properties missing from configuration file')
39
- cfg.gitlabs.forEach((gitlab) => {
40
- assert.ok(gitlab.url, 'Mandatory gitlab url missing from configuration file')
41
- assert.ok(gitlab['access-token'] || process.env.GITLAB_ACCESS_TOKEN, 'Mandatory gitlab access token missing from configuration (and none present at GITLAB_ACCESS_TOKEN env variable)')
42
- })
43
- return cfg
6
+ return path.replace(/^~($|\/|\\)/, `${os.homedir()}$1`);
44
7
  }
8
+ const JobStatusSchema = z.literal(['canceled', 'created', 'failed', 'manual', 'pending', 'running', 'skipped', 'success']);
9
+ const GitlabSchema = z.strictObject({
10
+ url: z.string().min(1, 'Mandatory gitlab url missing from configuration file'),
11
+ 'access-token': z.string().min(1).optional(),
12
+ ignoreArchived: z.boolean().default(true),
13
+ maxNonFailedJobsVisible: z.coerce.number().int().default(999999),
14
+ branch: z.string().min(1).optional(),
15
+ caFile: z.string().optional(),
16
+ offlineRunners: z.literal(['all', 'default', 'none']).default('default'),
17
+ projects: z.strictObject({
18
+ excludePipelineStatus: z.array(JobStatusSchema).optional(),
19
+ include: z.string().min(1).optional(),
20
+ exclude: z.string().min(1).optional()
21
+ }).optional()
22
+ }).transform(gitlab => {
23
+ const accessToken = gitlab['access-token'] || process.env.GITLAB_ACCESS_TOKEN;
24
+ if (!accessToken) {
25
+ throw new Error('Mandatory gitlab access token missing from configuration (and none present at GITLAB_ACCESS_TOKEN env variable)');
26
+ }
27
+ const { url, ignoreArchived, maxNonFailedJobsVisible, caFile, branch, offlineRunners, projects } = gitlab;
28
+ const ca = caFile && fs.existsSync(caFile) ? fs.readFileSync(caFile, 'utf-8') : undefined;
29
+ return {
30
+ url,
31
+ ignoreArchived,
32
+ maxNonFailedJobsVisible,
33
+ branch,
34
+ ca,
35
+ offlineRunners,
36
+ 'access-token': accessToken,
37
+ projects
38
+ };
39
+ });
40
+ const OrderSchema = z.literal(['status', 'name', 'id', 'nameWithoutNamespace', 'group']);
41
+ const ConfigSchema = z.strictObject({
42
+ interval: z.coerce.number().default(10).transform(sec => sec * 1000),
43
+ port: z.coerce.number().int().default(3000),
44
+ zoom: z.coerce.number().default(1.0),
45
+ columns: z.coerce.number().int().default(1),
46
+ horizontal: z.boolean().default(false),
47
+ rotateRunningPipelines: z.coerce.number().min(0).default(0).transform(sec => sec * 1000),
48
+ groupSuccessfulProjects: z.boolean().default(false),
49
+ projectsOrder: z.array(OrderSchema).default(['name']),
50
+ gitlabs: z.array(GitlabSchema).min(1, { message: 'Mandatory gitlab properties missing from configuration file' }),
51
+ colors: z.strictObject({
52
+ background: z.string(),
53
+ 'created-background': z.string(),
54
+ 'created-text': z.string(),
55
+ 'dark-text': z.string(),
56
+ 'error-message-background': z.string(),
57
+ 'error-message-text': z.string(),
58
+ 'failed-background': z.string(),
59
+ 'failed-text': z.string(),
60
+ 'group-background': z.string(),
61
+ 'light-text': z.string(),
62
+ 'pending-background': z.string(),
63
+ 'pending-text': z.string(),
64
+ 'project-background': z.string(),
65
+ 'running-background': z.string(),
66
+ 'running-text': z.string(),
67
+ 'skipped-background': z.string(),
68
+ 'skipped-text': z.string(),
69
+ 'success-background': z.string(),
70
+ 'success-text': z.string()
71
+ }).partial().optional(),
72
+ auth: z.strictObject({
73
+ username: z.string(),
74
+ password: z.string()
75
+ }).optional()
76
+ });
77
+ const configFile = expandTilde(process.env.GITLAB_RADIATOR_CONFIG || '~/.gitlab-radiator.yml');
78
+ const yamlContent = fs.readFileSync(configFile, 'utf8');
79
+ const rawConfig = yaml.load(yamlContent);
80
+ export const config = ConfigSchema.parse(rawConfig);
@@ -0,0 +1,11 @@
1
+ import { createRequire } from 'module';
2
+ import webpack from 'webpack';
3
+ import webpackDevMiddleware from 'webpack-dev-middleware';
4
+ import webpackHotMiddleware from 'webpack-hot-middleware';
5
+ const require = createRequire(import.meta.url);
6
+ export function bindDevAssets(app) {
7
+ const config = require('../webpack.dev.cjs');
8
+ const compiler = webpack(config);
9
+ app.use(webpackDevMiddleware(compiler));
10
+ app.use(webpackHotMiddleware(compiler));
11
+ }
@@ -1,27 +1,20 @@
1
- import axios from 'axios'
2
- import https from 'https'
3
- import url from 'url'
4
-
5
- export function gitlabRequest(path, params, gitlab) {
6
- return lazyClient(gitlab).get(path, {params})
1
+ import axios from 'axios';
2
+ import https from 'https';
3
+ import url from 'url';
4
+ export function gitlabRequest(pathStr, params, gitlab) {
5
+ return lazyClient(gitlab).get(pathStr, { params: params || {} });
7
6
  }
8
-
9
- const clients = new Map()
10
-
7
+ const clients = new Map();
11
8
  function lazyClient(gitlab) {
12
- const gitlabUrl = gitlab.url
13
- if (gitlabUrl === undefined) {
14
-
15
- console.log('Got undefined url for ' + JSON.stringify(gitlab))
16
- }
17
- if (!clients.get(gitlabUrl)) {
18
- const client = axios.create({
19
- baseURL: url.resolve(gitlabUrl, '/api/v4/'),
20
- headers: {'PRIVATE-TOKEN': gitlab['access-token']},
21
- httpsAgent: new https.Agent({keepAlive: true, ca: gitlab.ca}),
22
- timeout: 30 * 1000
23
- })
24
- clients.set(gitlabUrl, client)
25
- }
26
- return clients.get(gitlabUrl)
9
+ let client = clients.get(gitlab.url);
10
+ if (!client) {
11
+ client = axios.create({
12
+ baseURL: url.resolve(gitlab.url, '/api/v4/'),
13
+ headers: { 'PRIVATE-TOKEN': gitlab['access-token'] },
14
+ httpsAgent: new https.Agent({ keepAlive: true, ca: gitlab.ca }),
15
+ timeout: 30 * 1000
16
+ });
17
+ clients.set(gitlab.url, client);
18
+ }
19
+ return client;
27
20
  }
@@ -1,55 +1,49 @@
1
- import {fetchLatestPipelines} from './pipelines.js'
2
- import {fetchProjects} from './projects.js'
3
-
4
- export async function update(config) {
5
- const projectsWithPipelines = await loadProjectsWithPipelines(config)
6
- return projectsWithPipelines
7
- .filter(project => project.pipelines.length > 0)
1
+ import { fetchLatestPipelines } from "./pipelines.js";
2
+ import { fetchProjects } from "./projects.js";
3
+ export async function update(gitlabs, prioritizeRunningPipelines) {
4
+ const projectsWithPipelines = await loadProjectsWithPipelines(gitlabs, prioritizeRunningPipelines);
5
+ return projectsWithPipelines
6
+ .filter((project) => project.pipelines.length > 0);
8
7
  }
9
-
10
- async function loadProjectsWithPipelines(config) {
11
- const allProjectsWithPipelines = []
12
- await Promise.all(config.gitlabs.map(async (gitlab) => {
13
- const projects = (await fetchProjects(gitlab))
14
- .map(project => ({
15
- ...project,
16
- maxNonFailedJobsVisible: gitlab.maxNonFailedJobsVisible
17
- }))
18
-
19
- for (const project of projects) {
20
- allProjectsWithPipelines.push(await projectWithPipelines(project, gitlab))
21
- }
22
- }))
23
- return allProjectsWithPipelines
8
+ async function loadProjectsWithPipelines(gitlabs, prioritizeRunningPipelines) {
9
+ const allProjectsWithPipelines = [];
10
+ await Promise.all(gitlabs.map(async (gitlab) => {
11
+ const projects = (await fetchProjects(gitlab))
12
+ .map(project => ({
13
+ ...project,
14
+ maxNonFailedJobsVisible: gitlab.maxNonFailedJobsVisible
15
+ }));
16
+ for (const project of projects) {
17
+ allProjectsWithPipelines.push(await projectWithPipelines(project, gitlab, prioritizeRunningPipelines));
18
+ }
19
+ }));
20
+ return allProjectsWithPipelines;
24
21
  }
25
-
26
- async function projectWithPipelines(project, config) {
27
- const pipelines = filterOutEmpty(await fetchLatestPipelines(project.id, config))
28
- .filter(excludePipelineStatusFilter(config))
29
- const status = defaultBranchStatus(project, pipelines)
30
- return {
31
- ...project,
32
- pipelines,
33
- status
34
- }
22
+ async function projectWithPipelines(project, gitlab, prioritizeRunningPipelines) {
23
+ const pipelines = filterOutEmpty(await fetchLatestPipelines(project.id, gitlab, prioritizeRunningPipelines))
24
+ .filter(excludePipelineStatusFilter(gitlab));
25
+ const status = defaultBranchStatus(project, pipelines);
26
+ return {
27
+ ...project,
28
+ maxNonFailedJobsVisible: gitlab.maxNonFailedJobsVisible,
29
+ pipelines,
30
+ status
31
+ };
35
32
  }
36
-
37
33
  function defaultBranchStatus(project, pipelines) {
38
- const [head] = pipelines
39
- .filter(({ref}) => ref === project.default_branch)
40
- .map(({status}) => status)
41
- return head
34
+ const [head] = pipelines
35
+ .filter(({ ref }) => ref === project.default_branch)
36
+ .map(({ status }) => status);
37
+ return head;
42
38
  }
43
-
44
39
  function filterOutEmpty(pipelines) {
45
- return pipelines.filter(pipeline => pipeline.stages)
40
+ return pipelines.filter(pipeline => pipeline.stages);
46
41
  }
47
-
48
- function excludePipelineStatusFilter(config) {
49
- return pipeline => {
50
- if (config.projects && config.projects.excludePipelineStatus) {
51
- return !config.projects.excludePipelineStatus.includes(pipeline.status)
52
- }
53
- return true
54
- }
42
+ function excludePipelineStatusFilter(gitlab) {
43
+ return (pipeline) => {
44
+ if (gitlab.projects?.excludePipelineStatus) {
45
+ return !gitlab.projects.excludePipelineStatus.includes(pipeline.status);
46
+ }
47
+ return true;
48
+ };
55
49
  }