UNPKG

abstruse

Version:
669 lines (588 loc) 18.9 kB
import { spawn } from 'child_process'; import { readdir, readFile } from 'fs'; import * as yaml from 'js-yaml'; import * as temp from 'temp'; import { getHttpJsonResponse } from './utils'; export enum CommandType { git = 'git', before_install = 'before_install', install = 'install', before_script = 'before_script', script = 'script', before_cache = 'before_cache', restore_cache = 'restore_cache', store_cache = 'store_cache', after_success = 'after_success', after_failure = 'after_failure', before_deploy = 'before_deploy', deploy = 'deploy', after_deploy = 'after_deploy', after_script = 'after_script' } export enum CommandTypePriority { git = 1, before_install = 2, install = 3, before_script = 4, script = 5, before_cache = 6, restore_cache = 7, store_cache = 8, after_success = 9, after_failure = 10, after_script = 11, before_deploy = 12, deploy = 13, after_deploy = 14 } export enum JobStage { test = 'test', deploy = 'deploy' } export interface Build { image?: string; env?: string; } export interface Matrix { include: Build[]; exclude: Build[]; allow_failures: Build[]; } export interface Command { command: string; type: CommandType; env?: string[]; } export interface JobsAndEnv { commands: Command[]; env: string[]; stage: JobStage; image: string; version?: string; cache?: string[]; } export interface Repository { clone_url: string; branch: string; clone_depth?: number; pr?: number; sha?: string; file_tree?: string[]; access_token?: string; type?: 'github' | 'bitbucket' | 'gogs' | 'gitlab'; } export interface Config { image: string; os?: string; stage?: JobStage; cache?: string[] | null; branches?: { test: string[], ignore: string[] }; env?: { global: string[], matrix: string[] }; before_install?: { command: string, type: CommandType }[]; install: { command: string, type: CommandType }[]; before_script?: { command: string, type: CommandType }[]; script: { command: string, type: CommandType }[]; before_cache?: { command: string, type: CommandType }[]; after_success?: { command: string, type: CommandType }[]; after_failure?: { command: string, type: CommandType }[]; before_deploy?: { command: string, type: CommandType }[]; deploy?: { command: string, type: CommandType }[]; after_deploy?: { command: string, type: CommandType }[]; after_script?: { command: string, type: CommandType }[]; jobs?: { include?: Config[], exclude?: Config[] }; matrix?: Matrix; } export function getRemoteParsedConfig(repository: Repository): Promise<JobsAndEnv[]> { return new Promise((resolve, reject) => { let cloneUrl = repository.clone_url; let branch = repository.branch || 'master'; let sha = repository.sha || null; let pr = repository.pr || null; let cloneDir = null; if (repository.access_token) { cloneUrl = cloneUrl.replace('//', `//${repository.access_token}@`); } createGitTmpDir() .then(dir => cloneDir = dir) .then(() => spawnGit(['clone', cloneUrl, '-b', branch, '--depth', '1', cloneDir])) .then(() => { if (sha || pr) { return checkoutShaOrPr(sha, pr, cloneDir, repository.type, repository.branch); } else { return Promise.resolve(); } }) .then(() => readGitDir(cloneDir)) .then(files => repository.file_tree = files) .then(() => { if (repository.file_tree.indexOf('.abstruse.yml') === -1) { let err = new Error(`Repository doesn't contains '.abstruse.yml' configuration file.`); return Promise.reject(err); } else { return Promise.resolve(); } }) .then(() => readAbstruseConfigFile(cloneDir)) .then(config => yaml.load(config)) .then(json => parseConfig(json)) .then(parsed => generateJobsAndEnv(repository, parsed)) .then(result => resolve(result)) .catch(err => reject(err)); }); } export function parseConfig(data: any): Config { let main = parseJob(data); main.matrix = parseMatrix(data.matrix || null); if (data.jobs) { main.jobs.include = data.jobs.include ? data.jobs.include.map(job => parseJob(job)) : []; main.jobs.exclude = data.jobs.exclude ? data.jobs.exclude.map(job => parseJob(job)) : []; } return main; } export function parseConfigFromRaw(repository: Repository, raw: string): Promise<JobsAndEnv[]> { return new Promise((resolve, reject) => { return Promise.resolve() .then(() => yaml.load(raw)) .then(json => parseConfig(json)) .then(parsed => generateJobsAndEnv(repository, parsed)) .then(result => resolve(result)) .catch(err => reject(err)); }); } export function checkRepositoryAccess(repository: Repository): Promise<boolean> { return new Promise((resolve, reject) => { let cloneUrl = repository.clone_url; let branch = repository.branch || 'master'; let cloneDir = null; if (repository.access_token) { cloneUrl = cloneUrl.replace('//', `//${repository.access_token}@`); } createGitTmpDir() .then(dir => cloneDir = dir) .then(() => spawnGit(['clone', cloneUrl, '-b', branch, '--depth', '1', cloneDir])) .then(() => resolve(true)) .catch(err => resolve(false)); }); } export function checkConfigPresence(repository: Repository): Promise<boolean> { return new Promise((resolve, reject) => { let cloneUrl = repository.clone_url; let branch = repository.branch || 'master'; let cloneDir = null; if (repository.access_token) { cloneUrl = cloneUrl.replace('//', `//${repository.access_token}@`); } createGitTmpDir() .then(dir => cloneDir = dir) .then(() => spawnGit(['clone', cloneUrl, '-b', branch, '--depth', '1', cloneDir])) .then(() => readGitDir(cloneDir)) .then(files => repository.file_tree = files) .then(() => { if (repository.file_tree.indexOf('.abstruse.yml') === -1) { resolve(false); } else { resolve(true); } }) .catch(err => resolve(false)); }); } export function getConfigRawFile(repository: Repository): Promise<any> { return new Promise((resolve, reject) => { let cloneUrl = repository.clone_url; let branch = repository.branch || 'master'; let cloneDir = null; if (repository.access_token) { cloneUrl = cloneUrl.replace('//', `//${repository.access_token}@`); } createGitTmpDir() .then(dir => cloneDir = dir) .then(() => spawnGit(['clone', cloneUrl, '-b', branch, '--depth', '1', cloneDir])) .then(() => readGitDir(cloneDir)) .then(files => repository.file_tree = files) .then(() => { if (repository.file_tree.indexOf('.abstruse.yml') === -1) { resolve(false); } else { return Promise.resolve(); } }) .then(() => readAbstruseConfigFile(cloneDir)) .then(rawFile => resolve(rawFile)) .catch(err => resolve(false)); }); } function parseJob(data: any): Config { let config: Config = { image: null, os: null, stage: data.stage || 'test', cache: null, branches: null, env: null, before_install: null, install: null, before_script: null, script: null, before_cache: null, after_success: null, after_failure: null, before_deploy: null, deploy: null, after_deploy: null, after_script: null, jobs: { include: [], exclude: [] }, matrix: null }; config.image = data.image || null; config.os = parseOS(data.os || null); config.cache = parseCache(data.cache || null); config.branches = parseBranches(data.branches || null); config.env = parseEnv(data.env || null); config.before_install = parseCommands(data.before_install || null, CommandType.before_install); config.install = parseCommands(data.install || null, CommandType.install); config.before_script = parseCommands(data.before_script || null, CommandType.before_script); config.script = parseCommands(data.script || null, CommandType.script); config.before_cache = parseCommands(data.before_cache || null, CommandType.before_cache); config.after_success = parseCommands(data.after_success || null, CommandType.after_success); config.after_failure = parseCommands(data.after_failure || null, CommandType.after_failure); config.before_deploy = parseCommands(data.before_deploy || null, CommandType.before_deploy); config.deploy = parseCommands(data.deploy || null, CommandType.deploy); config.after_deploy = parseCommands(data.after_deploy || null, CommandType.after_deploy); config.after_script = parseCommands(data.after_script || null, CommandType.after_script); return config; } function parseOS(os: string | null): string { return 'linux'; // since we are compatible with travis configs, hardcode this to Linux } function parseCache(cache: any | null): string[] | null { if (!cache) { return null; } else { if (Array.isArray(cache)) { return cache; } else { return []; } } } function parseBranches(branches: any | null): { test: string[], ignore: string[] } | null { if (!branches) { return null; } else { if (Array.isArray(branches)) { return { test: branches, ignore: [] }; } else if (typeof branches === 'object') { let only = [], except = []; if (branches && branches.only) { only = branches.only; } if (branches && branches.except) { except = branches.except; } return { test: only, ignore: except }; } else { throw new Error(`Unknown format for property branches.`); } } } function parseEnv(env: any | null): { global: string[], matrix: string[] } { if (!env) { return null; } else { if (typeof env === 'object') { let global = []; let matrix = []; if (env.global) { if (Array.isArray(env.global)) { global = env.global; } else { throw new Error(`Unknown format for property env.global.`); } } if (env.matrix) { if (Array.isArray(env.matrix)) { matrix = env.matrix; } else { throw new Error(`Unknown format for property env.matrix.`); } } return { global, matrix }; } else if (typeof env === 'string') { return { global: [env], matrix: [] }; } else { throw new Error(`Unknown format for property env.`); } } } function parseCommands( commands: any | null, type: CommandType ): { command: string, type: CommandType }[] { if (!(type in CommandType)) { throw new Error(`Command type must enum of CommandType.`); } if (!commands) { return []; } else { if (typeof commands === 'string') { return [{ command: commands, type: type }]; } else if (Array.isArray(commands)) { return commands.map(cmd => ({ command: cmd, type: type })); } else { throw new Error(`Unknown or invalid type of commands specified.`); } } } function parseMatrix(matrix: any | null): Matrix { if (!matrix) { return { include: [], exclude: [], allow_failures: [] }; } else { if (Array.isArray(matrix)) { let include = matrix.map((m: Build) => m); return { include: include, exclude: [], allow_failures: [] }; } else if (typeof matrix === 'object') { let include = []; let exclude = []; let allowFailures = []; if (matrix.include && Array.isArray(matrix.include)) { include = matrix.include; } if (matrix.exclude && Array.isArray(matrix.exclude)) { exclude = matrix.exclude; } if (matrix.allow_failures && Array.isArray(matrix.allow_failures)) { allowFailures = matrix.allow_failures; } return { include: include, exclude: exclude, allow_failures: allowFailures }; } else { throw new Error(`Unknown or invalid matrix format.`); } } } export function generateJobsAndEnv(repo: Repository, config: Config): JobsAndEnv[] { let data: JobsAndEnv[] = []; // check if it's branch that we want to build if (!checkBranches(repo.branch, config.branches)) { return []; } // global and matrix environment variables let globalEnv = config.env && config.env.global || []; let matrixEnv = config.env && config.env.matrix || []; // 1. clone repository let splitted = repo.clone_url.split('/'); let name = splitted[splitted.length - 1].replace(/\.git/, ''); // TODO: update to ${ABSTRUSE_BUILD_DIR} if (repo.access_token) { repo.clone_url = repo.clone_url.replace('//', `//${repo.access_token}@`); } let cloneBranch = repo.type === 'bitbucket' ? 'master' : repo.branch; let clone = `git clone -q ${repo.clone_url} -b ${cloneBranch} .`; // 2. fetch & checkout let fetch = null; let checkout = null; if (repo.pr) { if (repo.type === 'github') { fetch = `git fetch origin pull/${repo.pr}/head:pr${repo.pr}`; checkout = `git checkout pr${repo.pr}`; } else if (repo.type === 'bitbucket') { fetch = `git fetch origin ${repo.branch}`; checkout = `git checkout FETCH_HEAD`; } } else if (repo.sha) { fetch = `git fetch origin ${repo.branch}`; checkout = `git checkout ${repo.sha} .`; } // 3. generate commands let installCommands = [] .concat(config.before_install || []) .concat(config.install || []); let testCommands = [] .concat(config.before_script || []) .concat(config.script || []) .concat(config.before_cache || []) .concat(config.after_success || []) .concat(config.after_failure || []) .concat(config.after_script || []); let deployCommands = [] .concat(config.before_deploy || []) .concat(config.deploy || []) .concat(config.after_deploy || []); // 4. generate jobs outta commands data = data.concat(matrixEnv.map(menv => { let env = globalEnv.concat(menv); return { commands: installCommands.concat(testCommands), env: env, stage: JobStage.test, image: config.image }; })).concat(config.matrix.include.map(i => { let env = globalEnv.concat(i.env || []); return { commands: installCommands.concat(testCommands), env: env, stage: JobStage.test, image: i.image || config.image }; })).concat(config.jobs.include.map((job, i) => { let env = globalEnv.concat(job.env && job.env.global || []); let jobInstallCommands = [] .concat(job.before_install || []) .concat(job.install || []); let jobTestCommands = [] .concat(job.before_script || []) .concat(job.script || []) .concat(job.before_cache || []) .concat(job.after_success || []) .concat(job.after_failure || []) .concat(job.after_script || []); let jobDeployCommands = [] .concat(job.before_deploy || []) .concat(job.deploy || []) .concat(job.after_deploy || []); return { commands: jobInstallCommands.concat(jobTestCommands).concat(jobDeployCommands), env: env, stage: job.stage, image: config.image }; })); data = data.map(d => { // add git commands in front d.commands.unshift(...[ { command: clone, type: CommandType.git }, { command: fetch, type: CommandType.git }, { command: checkout, type: CommandType.git } ]); // apply cache to each job d.cache = config.cache; d.commands = d.commands.filter(cmd => !!cmd.command); return d; }); if (deployCommands.length) { let gitCommands = [ { command: clone, type: CommandType.git }, { command: fetch, type: CommandType.git }, { command: checkout, type: CommandType.git } ]; data.push({ commands: gitCommands.concat(deployCommands), env: globalEnv.concat('DEPLOY'), stage: JobStage.deploy, image: config.image }); } return data; } function checkBranches(branch: string, branches: { test: string[], ignore: string[] }): boolean { if (!branches) { return true; } else { let ignore = false; let test = false; branches.ignore.forEach(ignored => { let regex: RegExp = new RegExp(ignored); if (regex.test(branch)) { if (!ignore) { ignore = true; } } }); if (ignore) { return false; } branches.test.forEach(tested => { let regex: RegExp = new RegExp(tested); if (regex.test(branch)) { if (!test) { test = true; } } }); return test; } } function spawnGit(args: string[]): Promise<void> { return new Promise((resolve, reject) => { let git = spawn('git', args, { detached: true }); git.stdout.on('data', data => { data = data.toString(); if (/Username/.test(data)) { reject('Not authorized'); } }); git.on('close', code => { if (code === 0) { resolve(); } else { reject(code); } }); }); } function checkoutShaOrPr( sha: string, pr: number, dir: string, type: string, branch: string ): Promise<void> { return new Promise((resolve, reject) => { let fetch = null; let checkout = null; let gitDir = `--git-dir ${dir}/.git`; if (pr) { if (type === 'github') { fetch = `${gitDir} fetch origin pull/${pr}/head:pr${pr}`; checkout = `${gitDir} --work-tree ${dir} checkout pr${pr}`; } else if (type === 'bitbucket') { fetch = `${gitDir} fetch origin ${branch}`; checkout = `${gitDir} --work-tree ${dir} checkout FETCH_HEAD`; } } else if (sha) { fetch = `${gitDir} fetch origin ${branch}`; if (type === 'bitbucket') { checkout = `${gitDir} --work-tree ${dir} checkout FETCH_HEAD`; } else { checkout = `${gitDir} --work-tree ${dir} checkout ${sha}`; } } if (fetch && checkout) { spawnGit(fetch.split(' ')) .then(() => spawnGit(checkout.split(' ')) .then(() => resolve()) .catch(err => reject(err))); } else { resolve(); } }); } function readAbstruseConfigFile(dirPath: string): Promise<any> { return new Promise((resolve, reject) => { readFile(dirPath + '/.abstruse.yml', (err, contents) => { if (err) { reject(err); } else { resolve(contents.toString()); } }); }); } function readGitDir(dirPath: string): Promise<string[]> { return new Promise((resolve, reject) => { readdir(dirPath, (err, files) => { if (err) { reject(err); } else { resolve(files); } }); }); } function createGitTmpDir(): Promise<string> { return new Promise((resolve, reject) => { temp.mkdir('abstruse-git', (err, dirPath) => { if (err) { reject(err); } else { resolve(dirPath); } }); }); }