singularci
Version:
SingularCI is a DSL transpiler used to generate CI/CD configuration files for existing CI platforms
231 lines (190 loc) • 6.88 kB
text/typescript
import YAML from 'yaml';
import { TargetPlatformGenerator } from '../interfaces/TargetPlatformGenerator';
import fs from 'fs';
import path from 'path';
import { dockerSetup, generateBuildDockerImageTask, generateCheckoutTask, generateRunTask } from './tasks';
import { GitLabJobObject } from './types';
import { Inject, Service } from 'typedi';
import DSLParser from './../../Parser/DSLParser';
import IPipeline from './../../SemanticModel/interfaces/IPipeline';
import IStage from '../../SemanticModel/interfaces/IStage';
import { TaskType } from '../../SemanticModel/Tasks/TaskEnum';
export class GitLabConfigGenerator implements TargetPlatformGenerator {
private pipeline: IPipeline;
private parser: DSLParser;
private configObject: any;
constructor( parser: DSLParser) {
this.parser = parser;
this.pipeline = this.parser.parse();
this.configObject = {};
}
public generateConfig = () => {
if (!this.shouldGenerate()) return;
this.pipeline = this.parser.parse();
this.buildSecrets();
this.buildTriggers();
this.buildStages();
this.buildJobs();
this.writeToFile();
}
private writeToFile = () => {
fs.writeFileSync(
path.join(
process.cwd(),
".gitlab-ci.yml"
),
YAML.stringify(this.configObject),
"utf-8"
);
}
private shouldGenerate(): boolean {
return this.pipeline != undefined && this.pipeline.getPlatformTargets().getTargets().includes('GitLab');
}
private buildStages() {
const stagesArray: string[] = [];
for (let i = 0; i < this.pipeline.getStages().length; i++) {
const stage: IStage = this.pipeline.getStages()[i];
stagesArray.push(stage.getName());
}
this.configObject.stages = stagesArray;
}
private buildTriggers() {
const isPushSet = this.pipeline.getTrigger().getTypes().includes('push');
const isPullRequestSet = this.pipeline.getTrigger().getTypes().includes('pull_request');
const rulesObject: [{
if: string,
when: string
}?] = [];
const triggerObject = {
workflow: {
rules: rulesObject
}
};
if (isPushSet) {
triggerObject.workflow.rules.push(
{
if: '$CI_PIPELINE_SOURCE == "push"',
when: 'always'
}
);
}
if (isPullRequestSet) {
triggerObject.workflow.rules.push(
{
if: '$CI_PIPELINE_SOURCE == "merge_request_event"',
when: 'always'
}
)
}
Object.assign(this.configObject, triggerObject);
}
private buildSecrets() {
this.pipeline = this.changeSecretsSyntax(this.pipeline);
}
private changeSecretsSyntax = (obj: any) => {
if (typeof obj === 'object') {
// iterating over the object using for..in
for (const key in obj) {
//checking if the current value is an object itself
if (typeof obj[key] === 'object') {
// if so then again calling the same function
this.changeSecretsSyntax(obj[key])
} else {
// else getting the value and replacing braces with empty spaces
if (obj[key] !== undefined && isNaN(obj[key])) {
const secrets: string[] = obj[key].match(/\$\{(secrets\.)[a-zA-Z][^{}]+\}/gm);
if (secrets) {
for (let i = 0; i < secrets.length; i++) {
const newValue = obj[key].replace(secrets[i], "$" + secrets[i].replace("${secrets.", "").replace("}", "") + "");
obj[key] = newValue;
}
}
}
}
}
}
return obj;
}
private buildJobs() {
const stages = this.pipeline.getStages();
for (let i = 0; i < stages.length; i++) {
const jobs = stages[i].getJobs();
for (let j = 0; j < jobs.length; j++) {
const tasks = jobs[j].getTasks();
const needs = stages[i].getNeeds();
const stage = stages[i];
const stageKey = `${stage.getName()}-${this.sanitizeJobName(jobs[j].getName())}`;
const tasksArray: any[] = [];
const needsArray: string[] = [];
const beforeScriptArray: string[] = [];
const jobObject:GitLabJobObject = {
[stageKey]: {
image: this.getSelectedImage(this.pipeline.getStages()[i]),
stage: this.pipeline.getStages()[i].getName(),
needs: needsArray,
before_script: beforeScriptArray,
script: tasksArray,
}
};
beforeScriptArray.push(...this.buildBeforeScript(tasks));
tasksArray.push(...this.buildTasks(stageKey, jobObject, tasks));
needsArray.push(...this.buildNeeds(needs));
Object.assign(this.configObject, jobObject);
}
}
}
private buildNeeds = (needs:any): string[] => {
const needsArray: string[] = [];
for (let k = 0; k < needs.length; k++) {
const needsName: string = needs[k];
const neededStage = this.pipeline.getStages().find(stage => stage.getName() === needsName);
if (neededStage) {
const jobs = neededStage.getJobs();
const jobAmount = jobs.length;
for (let l = 0; l < jobAmount; l++) {
const jobName = `${neededStage.getName()}-${this.sanitizeJobName(jobs[l].getName())}`;
needsArray.push(jobName);
}
}
}
return needsArray;
}
private buildBeforeScript = (tasks: any) => {
const beforeScriptArray: string[] = [];
for (const task of tasks) {
if (task.getType() === TaskType.Checkout) {
beforeScriptArray.push(...generateCheckoutTask(task));
}
}
return beforeScriptArray;
}
private buildTasks = (stageKey: string, jobObject:GitLabJobObject, tasks:any):any[] => {
const tasksArray: any[] = [];
for (const task of tasks) {
if (task.getType() === TaskType.BuildDockerImage) {
jobObject[stageKey].image = "docker:latest";
jobObject[stageKey].services = dockerSetup()
tasksArray.push(...generateBuildDockerImageTask(task));
}
if (task.getType() === TaskType.Run) {
tasksArray.push(...generateRunTask(task));
}
}
return tasksArray;
}
private sanitizeJobName = (name:string):string => {
return name.replaceAll(' ','_')
}
private getSelectedImage(stage: IStage): string {
const runsOn = stage.getRunsOn();
switch (runsOn) {
case "ubuntu-latest":
return "ubuntu:latest";
case "windows-latest":
return "mcr.microsoft.com/windows:20H2";
default:
return runsOn;
}
}
}