Skip to content

Commit

Permalink
Merge pull request #103 from INSO-TUWien/feature/100
Browse files Browse the repository at this point in the history
Feature/100 Github Actions CI Indexer
  • Loading branch information
nuberion authored May 4, 2023
2 parents 463b28d + 0e95af8 commit adb9ea0
Show file tree
Hide file tree
Showing 16 changed files with 280 additions and 63 deletions.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
Binocular
=====

[![Build Status](https://travis-ci.org/INSO-TUWien/Binocular.svg?branch=development)](https://travis-ci.org/INSO-TUWien/Binocular)
[![Build Binocular](https://github.com/INSO-TUWien/Binocular/actions/workflows/build-bincoular.yml/badge.svg?branch=develop)](https://github.com/INSO-TUWien/Binocular/actions/workflows/build-bincoular.yml)

Binocular is a tool for visualizing data from various software-engineering
tools. It works as a command-line tool run from a git-repository. When
Expand Down Expand Up @@ -63,7 +63,7 @@ json.
Should only be used if the repository could not detect the indexers automatically.
- `its`: Holds the name of the issue tracking system indexer,for instance, gitlab or github
- `ci`: Since the CI indexer importer is searching for the corresponding file in the repository, it can be necessary to specify the
correct indexer like, for example, travis.
correct indexer like, for example, gitlab, github or travis.

A sample configuration file looks like this:

Expand All @@ -88,7 +88,7 @@ A sample configuration file looks like this:
},
"indexers": {
"its": "github",
"ci": "travis"
"ci": "github"
}
}
```
Expand Down
13 changes: 4 additions & 9 deletions foxx/schema.js
Original file line number Diff line number Diff line change
Expand Up @@ -230,7 +230,6 @@ const queryType = new gql.GraphQLObjectType({
// //TODO: RETURN MERGE(build, { stats: MERGE(countsByStatus) })`;

// return q;

if (args.since && args.until) {
return aql`
FOR build IN ${builds}
Expand All @@ -239,10 +238,8 @@ const queryType = new gql.GraphQLObjectType({
FILTER DATE_TIMESTAMP(build.createdAt) >= DATE_TIMESTAMP(${args.since})
FILTER DATE_TIMESTAMP(build.createdAt) <= DATE_TIMESTAMP(${args.until})
LET countsByStatus = (
FOR other IN ${builds}
FILTER other.finishedAt <= build.createdAt
COLLECT status = other.status WITH COUNT INTO statusCount
RETURN { [status]: statusCount }
COLLECT status = build.status WITH COUNT INTO statusCount
RETURN { [status]: statusCount }
)
RETURN MERGE(build, { stats: MERGE(countsByStatus) })`;
}
Expand All @@ -251,10 +248,8 @@ const queryType = new gql.GraphQLObjectType({
SORT build.createdAt ASC
${limit}
LET countsByStatus = (
FOR other IN ${builds}
FILTER other.finishedAt <= build.createdAt
COLLECT status = other.status WITH COUNT INTO statusCount
RETURN { [status]: statusCount }
COLLECT status = build.status WITH COUNT INTO statusCount
RETURN { [status]: statusCount }
)
RETURN MERGE(build, { stats: MERGE(countsByStatus) })`;
},
Expand Down
4 changes: 2 additions & 2 deletions foxx/types/build.js
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ const BuildStatus = new gql.GraphQLEnumType({
success: {},
pending: {},
running: {},
canceled: {},
cancelled: {},
skipped: {},
created: {},
started: {},
Expand Down Expand Up @@ -138,7 +138,7 @@ module.exports = new gql.GraphQLObjectType({
pending: {
type: gql.GraphQLInt,
},
canceled: {
cancelled: {
type: gql.GraphQLInt,
},
},
Expand Down
90 changes: 90 additions & 0 deletions lib/core/provider/github.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
'use strict';

const _ = require('lodash');
const urlJoin = require('url-join');
const log = require('debug')('github');
const Paginator = require('../../paginator.js');
const { response } = require('express');
const { Octokit } = require('@octokit/rest');

class GitHub {
constructor(options) {
this.baseUrl = options.baseUrl;
this.privateToken = options.privateToken;
this.requestTimeout = options.requestTimeout;
this.count = 0;
this.stopping = false;
this.github = new Octokit({
baseUrl: 'https://api.github.com',
auth: this.privateToken,
});
}

getPipelines(projectId) {
log('getPipelines(%o)', projectId);
return this.paginatedRequest((page) => {
return this.github.rest.actions.listWorkflowRunsForRepo({
owner: projectId.split('/')[0],
repo: projectId.split('/')[1],
page: page,
});
});
}

getPipeline(projectId, pipelineId) {
log('getPipeline(%o, %o)', projectId, pipelineId);
return this.github.rest.actions
.getWorkflowRun({
owner: projectId.split('/')[0],
repo: projectId.split('/')[1],
run_id: pipelineId,
})
.then((workflowRun) => {
return workflowRun.data;
});
}

getPipelineJobs(projectId, pipelineId) {
log('getPipelineJobs(%o,%o)', projectId, pipelineId);
return this.github.rest.actions
.listJobsForWorkflowRun({
owner: projectId.split('/')[0],
repo: projectId.split('/')[1],
run_id: pipelineId,
})
.then((jobs) => {
return jobs.data.jobs;
});
}

isStopping() {
return this.stopping;
}

stop() {
this.stopping = true;
}

paginatedRequest(rq) {
return new Paginator(
(page, per_page) => {
if (this.stopping) {
return Promise.resolve({
data: {
workflow_runs: [],
},
});
}
return rq(page);
},
(resp) => {
return resp.data.workflow_runs || [];
},
(resp) => {
return (this.count = parseInt(resp.data.total_count, 10));
}
);
}
}

module.exports = GitHub;
20 changes: 0 additions & 20 deletions lib/importer/GenericImporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,6 @@ module.exports = async (repo, componentType, componentFactory, reporter, context
const typePath = componentType !== 'CI' ? 'indexers.its' : 'indexers.ci';
let type = _.toLower(config.get(typePath, 'auto'));

if (type === 'auto' && componentType === 'CI') {
type = await detectCIProvider(repo);
}

if (type === 'auto') {
type = await detectRepoProvider(repo, componentType);
}
Expand Down Expand Up @@ -72,19 +68,3 @@ async function detectRepoProvider(repo, componentType) {
throw new IllegalArgumentError(`Unable to auto-detect ${componentType}. Please configure it manually`);
}
}

/**
* analyse the repository to check the corresponding remote provider
*
* @param repo contains the information of the repository
* @returns {Promise<string>}
*/
// eslint-disable-next-line no-unused-vars
async function detectCIProvider(repo) {
log('Detecting CI from files in the repo...');
if (await FindFiles(await repo.getRoot(), /.travis\.ya?ml$/)) {
return 'travis';
}

return 'auto';
}
8 changes: 4 additions & 4 deletions lib/indexers/ci/CIIndexer.js
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,11 @@ class CIIndexer {
(!existingBuild || new Date(existingBuild.updatedAt).getTime() < new Date(pipeline.updatedAt).getTime())
) {
log(`Processing build #${pipeline.id} [${persistCount + omitCount}]`);
return Promise.join(
return Promise.all([
this.controller.getPipeline(projectId, pipeline.id),
this.controller.getPipelineJobs(projectId, pipeline.id)
)
.spread(this.buildMapper)
this.controller.getPipelineJobs(projectId, pipeline.id),
])
.then((results) => this.buildMapper(results[0], results[1]))
.then(() => persistCount++);
} else {
log(`Skipping build #${pipeline.id} [${persistCount + omitCount}]`);
Expand Down
133 changes: 133 additions & 0 deletions lib/indexers/ci/GitHubCIIndexer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
'use strict';

const Build = require('../../models/Build.js');
const FindFiles = require('file-regex');
const UrlProvider = require('../../url-providers');
const log = require('debug')('importer:github-ci-indexer');
const ConfigurationError = require('../../errors/ConfigurationError.js');
const CIIndexer = require('./CIIndexer');
const moment = require('moment');
const { Octokit } = require('@octokit/rest');
const GitHub = require('../../core/provider/github');

class GitHubCIIndexer {
constructor(repository, progressReporter) {
this.repo = repository;
this.reporter = progressReporter;
this.stopping = false;
}

async configure(config) {
this.urlProvider = await UrlProvider.getCiUrlProvider(this.repo, this.reporter);

this.github = new Octokit({
baseUrl: 'https://api.github.com',
auth: config?.auth?.token,
});

//prerequisites to permit the use of travis
if (!this.urlProvider || !config) {
throw new ConfigurationError('GitHub/Octokit cannot be configured!');
}

const currentBranch = await this.repo.getCurrentBranch();
const originUrl = await this.repo.getOriginUrl();

let repoName = originUrl.substring('https://github.com'.length + 1);
if (repoName.endsWith('.git')) {
repoName = repoName.slice(0, -'.git'.length);
}

log(`fetching branch data from ${currentBranch}[${currentBranch}]`);
this.controller = new GitHub({
baseUrl: 'https://api.github.com',
privateToken: config?.auth?.token,
requestTimeout: config.timeout,
});

this.indexer = new CIIndexer(this.reporter, this.controller, repoName, async (pipeline, jobs) => {
jobs = jobs || [];
log(
`create build ${JSON.stringify({
id: pipeline.id,
number: pipeline.run_number,
sha: pipeline.head_commit.sha,
status: convertState(pipeline.status),
updatedAt: moment(pipeline.updated_at).toISOString(),
startedAt: moment(pipeline.run_started_at).toISOString(),
finishedAt: moment(pipeline.updated_at).toISOString(),
committedAt: moment(pipeline.head_commit.timestamp).toISOString(),
})}`
);
const username = (pipeline.actor || {}).login;
const userFullName = username !== undefined ? (await this.github.users.getByUsername({ username: username })).data.name : '';
let status = 'canceled';
let lastStartedAt = pipeline.run_started_at;
let lastFinishedAt = pipeline.updated_at;
if (jobs.length > 0) {
status = convertState(jobs[jobs.length - 1].conclusion);
lastStartedAt = jobs[jobs.length - 1].created_at;
lastFinishedAt = jobs[jobs.length - 1].completed_at;
}
return Build.persist({
id: pipeline.id,
sha: pipeline.head_sha,
ref: pipeline.head_commit.id,
status: status,
tag: pipeline.display_title,
user: username,
userFullName: userFullName !== null ? userFullName : username,
createdAt: moment(pipeline.created_at || (jobs.length > 0 ? jobs[0].created_at : pipeline.started_at)).toISOString(),
updatedAt: moment(pipeline.updated_at).toISOString(),
startedAt: moment(pipeline.run_started_at).toISOString(),
finishedAt: moment(lastFinishedAt).toISOString(),
committedAt: moment(pipeline.head_commit.committed_at).toISOString(),
duration: moment(lastFinishedAt).unix() - moment(lastStartedAt).unix(),
jobs: jobs.map((job) => ({
id: job.id,
name: username,
status: job.conclusion,
stage: job.conclusion,
createdAt: moment(job.created_at).toISOString(),
finishedAt: moment(job.completed_at).toISOString(),
webUrl: job.html_url,
})),
webUrl: pipeline.html_url,
});
});
}

async index() {
try {
return await this.indexer.index();
} catch (e) {
console.log('GitHubCI Indexer Failed! - ' + e.message);
}
}

isStopping() {
return this.stopping;
}

stop() {
if (this.indexer && !this.indexer.isStopping()) {
this.indexer.stop();
}

if (this.controller && !this.controller.isStopping()) {
this.controller.stop();
}
this.stopping = true;
}
}

const convertState = (state) => {
switch (state) {
case 'failure':
return 'failed';
default:
return state;
}
};

module.exports = GitHubCIIndexer;
4 changes: 2 additions & 2 deletions lib/indexers/ci/TravisCIIndexer.js
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ class TravisCIIndexer {
id: pipeline.id,
sha: pipeline.commit.sha,
ref: pipeline.commit.ref,
status: convertState(pipeline.state),
status: pipeline.state,
tag: pipeline.tag,
user: username,
userFullName: userFullName,
Expand All @@ -75,7 +75,7 @@ class TravisCIIndexer {
jobs: jobs.map((job) => ({
id: job.id,
name: username,
status: convertState(job.state),
status: job.state,
stage: job.stage,
createdAt: moment(job.created_at).toISOString(),
finishedAt: moment(job.finished_at).toISOString(),
Expand Down
1 change: 1 addition & 0 deletions lib/indexers/ci/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ let provider;
const components = {
travis: require('./TravisCIIndexer'),
gitlab: require('./GitLabCIIndexer'),
github: require('./GitHubCIIndexer'),
};

/**
Expand Down
Loading

0 comments on commit adb9ea0

Please sign in to comment.