bananareporter
Version:
Easily generate a report from multiple sources
256 lines (255 loc) • 11.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.GitlabIntegration = exports.GitlabConfig = void 0;
const base_1 = require("./base");
// import {Axios} from 'axios'
const common_1 = require("../util/common");
const zod_1 = require("zod");
const axios_1 = require("axios");
const dayjs = require("dayjs");
const logger_1 = require("../util/logger");
const objectPath = require("object-path");
exports.GitlabConfig = zod_1.z.object({
committerUsername: zod_1.z.string().min(1),
userId: zod_1.z.number().optional(),
token: zod_1.z.string().min(1),
filters: zod_1.z.array(zod_1.z.object({
on: zod_1.z.string().min(1),
regex: zod_1.z.string().min(1),
})).nonempty().optional(),
domain: zod_1.z.string().optional().default('gitlab.com'),
apiVersion: zod_1.z.string().startsWith('v').optional().default('v4'),
protocol: zod_1.z.union([zod_1.z.literal('http'), zod_1.z.literal('https')]).optional().default('https'),
apiBasePath: zod_1.z.string().optional().default('api'),
// TODO should be extended from a base integration
from: common_1.zIsoString.optional(),
to: common_1.zIsoString.optional(),
delay: zod_1.z.number().min(0).optional(),
});
class GitlabIntegration extends base_1.IntegrationBase {
constructor(_rawConfig, bananaReporterConfig) {
var _a, _b, _c;
super(_rawConfig, bananaReporterConfig);
this.projectIdToDetails = {};
logger_1.logger.debug('gitlab integration');
this.config = exports.GitlabConfig.parse(_rawConfig);
logger_1.logger.debug('gitlab integration config', this.config);
this.bananaReporterConfig = bananaReporterConfig;
this.delayToUse = (_a = this.config.delay) !== null && _a !== void 0 ? _a : this.bananaReporterConfig.delay;
logger_1.logger.debug(`gitlab integration delayToUse ${this.delayToUse}`);
this.dateRange = {
from: (_b = this.config.from) !== null && _b !== void 0 ? _b : this.bananaReporterConfig.from,
to: (_c = this.config.to) !== null && _c !== void 0 ? _c : this.bananaReporterConfig.to,
};
logger_1.logger.debug('gitlab integration dateRange', this.dateRange);
const baseURL = `${this.config.protocol}://${this.config.domain}/${this.config.apiBasePath}/${this.config.apiVersion}`;
logger_1.logger.debug(`gitlab integration baseURL ${baseURL}`);
this.httpClient = axios_1.default.create({
baseURL,
headers: {
Authorization: `Bearer ${this.config.token}`,
},
timeout: 35000,
});
}
setup(options) {
for (const p of options.projectDetails) {
if (!(p.id in this.projectIdToDetails)) {
this.projectIdToDetails[p.id] = p;
}
}
}
async httpRequest(path, config) {
logger_1.logger.debug(`gitlab integration http request ${path}`, config);
const res = await this.httpClient(path, {
...config,
});
return res.data;
}
/**
* events API needs the user id, retrieve it
* from the username provided
* @returns GitLab user id
*/
async getUserId() {
if (typeof this.config.userId !== 'undefined') {
return this.config.userId;
}
const username = this.config.committerUsername;
const data = await this.httpRequest(`/users?username=${username}`);
if (data.length === 0) {
throw new Error(`unable to find the GitLab username ${username}`);
}
return data[0].id;
}
async fetchData() {
const userId = await this.getUserId();
logger_1.logger.debug(`gitlab integration userId ${userId}`);
let page = 1;
let eventList = [];
const projectIds = new Set();
while (page > 0) {
logger_1.logger.debug(`gitlab integration working on ${page}`);
/* eslint-disable no-await-in-loop */
const events = await this.getUserEvents(userId, {
from: this.dateRange.from,
to: this.dateRange.to,
page,
});
// no more data, break loop
if (events.length === 0) {
logger_1.logger.debug('gitlab integration no more events to process');
break;
}
// eslint-disable-next-line unicorn/prefer-spread
eventList = eventList.concat(events);
for (const e of events) {
projectIds.add(e.project_id);
}
logger_1.logger.debug(`gitlab integration eventList ${eventList.length} (added ${events.length} events)`);
page += 1;
await (0, common_1.delay)(this.delayToUse);
}
// get all project details
const projectDetails = await this.getProjects(userId, projectIds);
logger_1.logger.debug('gitlab integration projectDetails', projectDetails);
for (const p of projectDetails) {
this.projectIdToDetails[p.id] = p;
}
this.setup({
projectDetails,
});
// format data to common obj
let filteredData = eventList
.filter(e => {
var _a;
return (e.action_name
.startsWith('pushed') &&
((_a = e === null || e === void 0 ? void 0 : e.push_data) === null || _a === void 0 ? void 0 : _a.commit_title) &&
!e.push_data.commit_title.startsWith('Merge branch'));
});
const projectsToRemove = new Set();
for (const filter of this.config.filters || []) {
const targetKey = filter.on;
if (targetKey.startsWith('$project.')) {
// custom logic for project matching
for (const projectId of Object.keys(this.projectIdToDetails)) {
const project = this.projectIdToDetails[Number(projectId)];
const targetProjectKey = targetKey.split('.').pop();
if (!(targetProjectKey in project)) {
const errorMsg = `while filtering the project ${project.name} (id ${project.id}) unable to find the key ${targetProjectKey} in the project's object (available keys: ${Object.keys(project).join(',')})`;
logger_1.logger.error(errorMsg);
throw new Error(errorMsg);
}
const regex = new RegExp(filter.regex);
if (!regex.test(project[targetProjectKey])) {
// false, remove
projectsToRemove.add(Number(projectId));
}
}
}
filteredData = filteredData.filter(e => {
const targetKey = filter.on;
// custom project is filtered later on
if (targetKey.startsWith('$project.')) {
return true;
}
const regex = new RegExp(filter.regex);
const value = objectPath.get(e, targetKey);
if (typeof value === 'undefined') {
const errorMsg = `while filtering event id ${e.id} unable to find the key ${targetKey} in the event's object (available keys: ${Object.keys(e).join(',')})`;
logger_1.logger.error(errorMsg);
throw new Error(errorMsg);
}
return regex.test(value);
});
}
if (projectsToRemove.size > 0) {
filteredData = filteredData.filter(e => {
return !projectsToRemove.has(e.project_id);
});
}
const formattedData = filteredData.map(e => this.toBananaReporterObj(e));
return formattedData;
}
/**
* GitLab doesn't return project information in the events call,
* need to fetch all projects to enrich the events data
* @param userId identifier of gitlab user
* @param projectIds set of project ids found in events call
* @returns list of projects found
*/
async getProjects(userId, projectIds) {
// get all user projects, if some projects are missing
// try to get the project via a direct GET call
const remainingProjectIdsToFetch = projectIds;
const projects = await this.httpRequest(`/users/${userId}/projects`);
for (const p of projects) {
remainingProjectIdsToFetch.delete(p.id);
}
const projectsLoaded = projects;
if (remainingProjectIdsToFetch.size > 0) {
// delay before starting to fetch other data
await (0, common_1.delay)(this.delayToUse);
// eslint-disable-next-line unicorn/prefer-spread
for (const projectId of Array.from(remainingProjectIdsToFetch)) {
try {
const project = await this.httpRequest(`/projects/${projectId}`);
if (project) {
remainingProjectIdsToFetch.delete(projectId);
projectsLoaded.push(project);
}
}
catch (error) {
logger_1.logger.debug(`gitlab integration unable to fetch details for project ID ${projectId}`, error);
}
await (0, common_1.delay)(this.delayToUse);
}
}
if (remainingProjectIdsToFetch.size > 0) {
logger_1.logger.warn(`unable to retrieve details for the following project ids: ${[...remainingProjectIdsToFetch].join(',')} make sure you have still access to these projects, the report won't include the project names`);
}
return projectsLoaded;
}
async getUserEvents(userId, options) {
var _a;
const fromDate = dayjs(options.from);
const toDate = dayjs(options.to);
// recalculate from - to according on GitLab API behaviour
const from = fromDate.subtract(1, 'day').format('YYYY-MM-DD');
const to = toDate.add(1, 'day').format('YYYY-MM-DD');
logger_1.logger.debug(`gitlab integration getUserEvents from ${from} to ${to}`);
const page = (_a = options.page) !== null && _a !== void 0 ? _a : 1;
const url = `/users/${userId}/events`;
const events = await this.httpRequest(url, {
params: {
after: from,
before: to,
sort: 'asc',
// eslint-disable-next-line camelcase
per_page: 100,
action: 'pushed',
page,
},
});
return events;
}
toBananaReporterObj(rawData) {
const projectId = rawData.project_id;
const projectDetails = this.projectIdToDetails[projectId];
const projectName = projectDetails === null || projectDetails === void 0 ? void 0 : projectDetails.path;
const description = `${rawData.push_data.commit_title} branch:${rawData.push_data.ref} git:${rawData.push_data.commit_to.slice(0, 7)}`;
return {
id: `${String(rawData.push_data.commit_to)}`,
date: rawData.created_at,
username: rawData.author_username,
description,
projectId: String(projectId),
projectName,
type: GitlabIntegration.type,
__raw: this.bananaReporterConfig.includeRawObject ? rawData : undefined,
};
}
}
exports.GitlabIntegration = GitlabIntegration;
GitlabIntegration.type = base_1.SourceType.Enum.gitlab;