gitlab-radiator 4.4.5 → 5.0.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.
- package/README.md +11 -6
- package/bin/gitlab-radiator.js +1 -1
- package/package.json +27 -19
- package/public/client.js +1 -1
- package/public/client.less +5 -0
- package/public/colors.less +2 -0
- package/public/index.html +0 -1
- package/src/{app.js → app.ts} +17 -17
- package/src/auth.ts +20 -0
- package/src/common/gitlab-types.d.ts +57 -0
- package/src/config.ts +95 -0
- package/src/gitlab/client.ts +34 -0
- package/src/gitlab/index.ts +59 -0
- package/src/gitlab/pipelines.ts +178 -0
- package/src/gitlab/projects.ts +87 -0
- package/src/gitlab/runners.ts +35 -0
- package/src/{less.js → less.ts} +12 -5
- package/src/auth.js +0 -21
- package/src/config.js +0 -44
- package/src/gitlab/client.js +0 -27
- package/src/gitlab/index.js +0 -55
- package/src/gitlab/pipelines.js +0 -119
- package/src/gitlab/projects.js +0 -73
- package/src/gitlab/runners.js +0 -18
package/src/{less.js → less.ts}
RENAMED
|
@@ -1,11 +1,13 @@
|
|
|
1
|
-
import {config} from './config.js'
|
|
2
1
|
import fs from 'fs'
|
|
3
2
|
import less from 'less'
|
|
4
3
|
import path from 'path'
|
|
4
|
+
import {config} from './config.ts'
|
|
5
|
+
import type {Config} from './config.ts'
|
|
6
|
+
import type {Request, Response} from 'express'
|
|
5
7
|
|
|
6
8
|
const filename = path.join('public', 'client.less')
|
|
7
9
|
|
|
8
|
-
export async function serveLessAsCss(
|
|
10
|
+
export async function serveLessAsCss(_req: Request, res: Response) {
|
|
9
11
|
try {
|
|
10
12
|
const source = await fs.promises.readFile(filename, 'utf-8')
|
|
11
13
|
const {css} = await less.render(withColorOverrides(source), {filename})
|
|
@@ -17,10 +19,15 @@ export async function serveLessAsCss(req, res) {
|
|
|
17
19
|
}
|
|
18
20
|
}
|
|
19
21
|
|
|
20
|
-
function withColorOverrides(source) {
|
|
22
|
+
function withColorOverrides(source: string) {
|
|
23
|
+
const {colors} = config
|
|
24
|
+
if (!colors) {
|
|
25
|
+
return source
|
|
26
|
+
}
|
|
27
|
+
|
|
21
28
|
let colorLess = ''
|
|
22
|
-
Object.keys(
|
|
23
|
-
colorLess += `@${stateName}-color:${
|
|
29
|
+
Object.keys(colors).forEach((stateName) => {
|
|
30
|
+
colorLess += `@${stateName}-color:${colors[stateName as keyof Config["colors"]]};`
|
|
24
31
|
})
|
|
25
32
|
return source + colorLess
|
|
26
33
|
}
|
package/src/auth.js
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import authenticate from 'basic-auth'
|
|
2
|
-
|
|
3
|
-
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()
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
}
|
package/src/config.js
DELETED
|
@@ -1,44 +0,0 @@
|
|
|
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
|
-
|
|
33
|
-
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
|
|
44
|
-
}
|
package/src/gitlab/client.js
DELETED
|
@@ -1,27 +0,0 @@
|
|
|
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})
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
const clients = new Map()
|
|
10
|
-
|
|
11
|
-
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)
|
|
27
|
-
}
|
package/src/gitlab/index.js
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
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)
|
|
8
|
-
}
|
|
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
|
|
24
|
-
}
|
|
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
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
function defaultBranchStatus(project, pipelines) {
|
|
38
|
-
const [head] = pipelines
|
|
39
|
-
.filter(({ref}) => ref === project.default_branch)
|
|
40
|
-
.map(({status}) => status)
|
|
41
|
-
return head
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function filterOutEmpty(pipelines) {
|
|
45
|
-
return pipelines.filter(pipeline => pipeline.stages)
|
|
46
|
-
}
|
|
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
|
-
}
|
|
55
|
-
}
|
package/src/gitlab/pipelines.js
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
import _ from 'lodash'
|
|
2
|
-
import {gitlabRequest} from './client.js'
|
|
3
|
-
|
|
4
|
-
export async function fetchLatestPipelines(projectId, gitlab) {
|
|
5
|
-
const pipelines = await fetchLatestAndMasterPipeline(projectId, gitlab)
|
|
6
|
-
|
|
7
|
-
const pipelinesWithStages = []
|
|
8
|
-
for (const {id, ref, status} of pipelines) {
|
|
9
|
-
const {commit, stages} = await fetchJobs(projectId, id, gitlab)
|
|
10
|
-
const downstreamStages = await fetchDownstreamJobs(projectId, id, gitlab)
|
|
11
|
-
pipelinesWithStages.push({
|
|
12
|
-
id,
|
|
13
|
-
ref,
|
|
14
|
-
status,
|
|
15
|
-
commit,
|
|
16
|
-
stages: stages.concat(downstreamStages)
|
|
17
|
-
})
|
|
18
|
-
}
|
|
19
|
-
return pipelinesWithStages
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
async function fetchLatestAndMasterPipeline(projectId, config) {
|
|
24
|
-
const pipelines = await fetchPipelines(projectId, config, {per_page: 100})
|
|
25
|
-
if (pipelines.length === 0) {
|
|
26
|
-
return []
|
|
27
|
-
}
|
|
28
|
-
const latestPipeline = _.take(pipelines, 1)
|
|
29
|
-
if (latestPipeline[0].ref === 'master') {
|
|
30
|
-
return latestPipeline
|
|
31
|
-
}
|
|
32
|
-
const latestMasterPipeline = _(pipelines).filter({ref: 'master'}).take(1).value()
|
|
33
|
-
if (latestMasterPipeline.length > 0) {
|
|
34
|
-
return latestPipeline.concat(latestMasterPipeline)
|
|
35
|
-
}
|
|
36
|
-
const masterPipelines = await fetchPipelines(projectId, config, {per_page: 50, ref: 'master'})
|
|
37
|
-
return latestPipeline.concat(_.take(masterPipelines, 1))
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function fetchPipelines(projectId, config, options) {
|
|
41
|
-
const {data: pipelines} = await gitlabRequest(`/projects/${projectId}/pipelines`, options, config)
|
|
42
|
-
return pipelines.filter(pipeline => pipeline.status !== 'skipped')
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
async function fetchDownstreamJobs(projectId, pipelineId, config) {
|
|
46
|
-
const {data: gitlabBridgeJobs} = await gitlabRequest(`/projects/${projectId}/pipelines/${pipelineId}/bridges`, {per_page: 100}, config)
|
|
47
|
-
const childPipelines = gitlabBridgeJobs.filter(bridge => bridge.downstream_pipeline !== null && bridge.downstream_pipeline.status !== 'skipped')
|
|
48
|
-
|
|
49
|
-
const downstreamStages = []
|
|
50
|
-
for(const childPipeline of childPipelines) {
|
|
51
|
-
const {stages} = await fetchJobs(childPipeline.downstream_pipeline.project_id, childPipeline.downstream_pipeline.id, config)
|
|
52
|
-
downstreamStages.push(stages.map(stage => ({
|
|
53
|
-
...stage,
|
|
54
|
-
name: `${childPipeline.stage}:${stage.name}`
|
|
55
|
-
})))
|
|
56
|
-
}
|
|
57
|
-
return downstreamStages.flat()
|
|
58
|
-
}
|
|
59
|
-
|
|
60
|
-
async function fetchJobs(projectId, pipelineId, config) {
|
|
61
|
-
const {data: gitlabJobs} = await gitlabRequest(`/projects/${projectId}/pipelines/${pipelineId}/jobs?include_retried=true`, {per_page: 100}, config)
|
|
62
|
-
if (gitlabJobs.length === 0) {
|
|
63
|
-
return {commit: undefined, stages: []}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
const commit = findCommit(gitlabJobs)
|
|
67
|
-
const stages = _(gitlabJobs)
|
|
68
|
-
.map(job => ({
|
|
69
|
-
id: job.id,
|
|
70
|
-
status: job.status,
|
|
71
|
-
stage: job.stage,
|
|
72
|
-
name: job.name,
|
|
73
|
-
startedAt: job.started_at,
|
|
74
|
-
finishedAt: job.finished_at,
|
|
75
|
-
url: job.web_url
|
|
76
|
-
}))
|
|
77
|
-
.orderBy('id')
|
|
78
|
-
.groupBy('stage')
|
|
79
|
-
.mapValues(mergeRetriedJobs)
|
|
80
|
-
.mapValues(cleanup)
|
|
81
|
-
.toPairs()
|
|
82
|
-
.map(([name, jobs]) => ({name, jobs: _.sortBy(jobs, 'name')}))
|
|
83
|
-
.value()
|
|
84
|
-
|
|
85
|
-
return {
|
|
86
|
-
commit,
|
|
87
|
-
stages
|
|
88
|
-
}
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
function findCommit(jobs) {
|
|
92
|
-
const [job] = jobs.filter(j => j.commit)
|
|
93
|
-
if (!job) {
|
|
94
|
-
return null
|
|
95
|
-
}
|
|
96
|
-
return {
|
|
97
|
-
title: job.commit.title,
|
|
98
|
-
author: job.commit.author_name
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
function mergeRetriedJobs(jobs) {
|
|
103
|
-
return jobs.reduce((mergedJobs, job) => {
|
|
104
|
-
const index = mergedJobs.findIndex(mergedJob => mergedJob.name === job.name)
|
|
105
|
-
if (index >= 0) {
|
|
106
|
-
mergedJobs[index] = job
|
|
107
|
-
} else {
|
|
108
|
-
mergedJobs.push(job)
|
|
109
|
-
}
|
|
110
|
-
return mergedJobs
|
|
111
|
-
}, [])
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
function cleanup(jobs) {
|
|
115
|
-
return _(jobs)
|
|
116
|
-
.map(job => _.omitBy(job, _.isNull))
|
|
117
|
-
.map(job => _.omit(job, 'stage'))
|
|
118
|
-
.value()
|
|
119
|
-
}
|
package/src/gitlab/projects.js
DELETED
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
import {gitlabRequest} from './client.js'
|
|
2
|
-
|
|
3
|
-
export async function fetchProjects(gitlab) {
|
|
4
|
-
const projects = await fetchOwnProjects(gitlab)
|
|
5
|
-
return projects
|
|
6
|
-
// Ignore projects for which CI/CD is not enabled
|
|
7
|
-
.filter(project => project.jobs_enabled)
|
|
8
|
-
.map(projectMapper)
|
|
9
|
-
.filter(includeRegexFilter(gitlab))
|
|
10
|
-
.filter(excludeRegexFilter(gitlab))
|
|
11
|
-
.filter(archivedFilter(gitlab))
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
async function fetchOwnProjects(gitlab) {
|
|
15
|
-
const projects = []
|
|
16
|
-
const SAFETY_MAX_PAGE = 10
|
|
17
|
-
for (let page = 1; page <= SAFETY_MAX_PAGE; page += 1) {
|
|
18
|
-
|
|
19
|
-
const {data, headers} = await gitlabRequest('/projects', {page, per_page: 100, membership: true}, gitlab)
|
|
20
|
-
projects.push(data)
|
|
21
|
-
if (data.length === 0 || !headers['x-next-page']) {
|
|
22
|
-
break
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
return projects.flat()
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
function projectMapper(project) {
|
|
29
|
-
return {
|
|
30
|
-
id: project.id,
|
|
31
|
-
name: project.path_with_namespace,
|
|
32
|
-
nameWithoutNamespace: project.path,
|
|
33
|
-
group: getGroupName(project),
|
|
34
|
-
archived: project.archived,
|
|
35
|
-
default_branch: project.default_branch || 'master',
|
|
36
|
-
url: project.web_url,
|
|
37
|
-
tags: (project.tag_list || []).map(t => t.toLowerCase())
|
|
38
|
-
}
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
function getGroupName(project) {
|
|
42
|
-
const pathWithNameSpace = project.path_with_namespace
|
|
43
|
-
return pathWithNameSpace.split('/')[0]
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function includeRegexFilter(config) {
|
|
47
|
-
return project => {
|
|
48
|
-
if (config.projects && config.projects.include) {
|
|
49
|
-
const includeRegex = new RegExp(config.projects.include, "i")
|
|
50
|
-
return includeRegex.test(project.name)
|
|
51
|
-
}
|
|
52
|
-
return true
|
|
53
|
-
}
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
function excludeRegexFilter(config) {
|
|
57
|
-
return project => {
|
|
58
|
-
if (config.projects && config.projects.exclude) {
|
|
59
|
-
const excludeRegex = new RegExp(config.projects.exclude, "i")
|
|
60
|
-
return !excludeRegex.test(project.name)
|
|
61
|
-
}
|
|
62
|
-
return true
|
|
63
|
-
}
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
function archivedFilter(config) {
|
|
67
|
-
return project => {
|
|
68
|
-
if (config.ignoreArchived) {
|
|
69
|
-
return !project.archived
|
|
70
|
-
}
|
|
71
|
-
return true
|
|
72
|
-
}
|
|
73
|
-
}
|
package/src/gitlab/runners.js
DELETED
|
@@ -1,18 +0,0 @@
|
|
|
1
|
-
import {gitlabRequest} from './client.js'
|
|
2
|
-
|
|
3
|
-
export async function fetchOfflineRunners(gitlab) {
|
|
4
|
-
const runners = await fetchRunners(gitlab)
|
|
5
|
-
const offline = runners.filter(r => r.status === 'offline')
|
|
6
|
-
return {
|
|
7
|
-
offline,
|
|
8
|
-
totalCount: runners.length
|
|
9
|
-
}
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
async function fetchRunners(gitlab) {
|
|
13
|
-
const {data: runners} = await gitlabRequest('/runners', {}, gitlab)
|
|
14
|
-
return runners.map(r => ({
|
|
15
|
-
name: r.description || r.id,
|
|
16
|
-
status: r.status
|
|
17
|
-
}))
|
|
18
|
-
}
|