UNPKG

autoforce

Version:

Developer Automation tool for Github / Gitlab and Salesforce projects.

683 lines (682 loc) 26.6 kB
import { executeShell, getOrganizationObject, getCurrentOrganization, getBranchName, getTargetOrg } from "./taskFunctions.js"; import { convertNameToKey, convertKeyToName, getFiles, filterDirectory, addNewItems, CONFIG_FILE, createConfigurationFile } from "./util.js"; import { GitHubApi } from "./github-graphql.js"; import { GitHubProjectApi } from "./github-project-graphql.js"; import { GitLabApi } from "./gitlab-graphql.js"; import prompts from "prompts"; import matter from 'gray-matter'; import fs from "fs"; import { logError, logWarning } from "./color.js"; export var GitServices; (function (GitServices) { GitServices["GitHub"] = "github"; GitServices["GitLab"] = "gitlab"; GitServices["None"] = "none"; })(GitServices || (GitServices = {})); export var ListFilters; (function (ListFilters) { ListFilters["Mios"] = "mios"; ListFilters["PorMilestone"] = "milestone"; ListFilters["PorLabel"] = "label"; })(ListFilters || (ListFilters = {})); export var ProjectServices; (function (ProjectServices) { ProjectServices["GitHub"] = "github"; ProjectServices["GitLab"] = "gitlab"; ProjectServices["Jira"] = "Jira"; ProjectServices["None"] = "none"; })(ProjectServices || (ProjectServices = {})); const filterProcesses = (fullPath) => fullPath.endsWith(".md"); // && !fullPath.endsWith("intro.md") const ISSUES_TYPES = [{ value: 'feature', title: 'feature' }, { value: 'bug', title: 'bug' }, { value: 'documentation', title: 'documentation' }, { value: 'automation', title: 'automation' }]; function searchInFolderHierarchy(element, parentFolder) { if (fs.existsSync(`${parentFolder}/${element}`)) { return parentFolder; } else { const lastIndex = parentFolder.lastIndexOf('/'); if (lastIndex !== -1) { const newParentFolder = parentFolder.substring(0, lastIndex); if (newParentFolder !== '') { return searchInFolderHierarchy(element, newParentFolder); } } } return ''; } function getProjectPath() { return searchInFolderHierarchy('package.json', process.cwd() || '.'); } function getDataFromPackage() { const data = {}; try { const projectPath = getProjectPath(); const filename = `${projectPath}/package.json`; if (!fs.existsSync(filename)) { throw new Error("No se encontro el package.json en " + projectPath); } const content = fs.readFileSync(filename, "utf8"); const packageJson = JSON.parse(content); if (packageJson.repository) { if (packageJson.repository.url) { data.repositoryUrl = packageJson.repository.url; data.repositoryType = packageJson.repository.type; // Ver de sacar repo y owner if (data.repositoryUrl) { if (data.repositoryUrl.includes("github.com")) { const repositoryArray = data.repositoryUrl.split('github.com/'); [data.repositoryOwner, data.repositoryRepo] = repositoryArray[1].split('/'); } if (data.repositoryUrl.includes("gitlab.com")) { const repositoryArray = data.repositoryUrl.split('gitlab.com/'); [data.repositoryOwner, data.repositoryRepo] = repositoryArray[1].split('/'); } } } else if (typeof packageJson.repository === 'string') { data.repositoryUrl = packageJson.repository; const repositoryArray = data.repositoryUrl.split(':'); data.repositoryType = repositoryArray[0]; [data.repositoryOwner, data.repositoryRepo] = repositoryArray[1].split('/'); } if (data.repositoryRepo && data.repositoryRepo.endsWith('.git')) { data.repositoryRepo = data.repositoryRepo.replace('.git', ''); } } } catch (error) { console.log(error); throw new Error(`Verifique que exista y sea valido el package.json`); } return data; } class Context { devModel; // Default Model de commands gitModel; docModel; errorMessage = ''; projectPath = getProjectPath(); projectModel; gitServices = GitServices.None; isGitApi = false; gitApi; version; options = {}; projectServices = ProjectServices.None; isProjectApi = false; projectApi; sfInstalled = true; sfToken = true; branchName; issueNumber; issueType; _dictionaryFolder; _process; _processesHeader; _newIssueNumber; _newIssueType; newBranchName; defaultDias = 7; permissionSet; issueTitle; isVerbose = false; _scratch; _branchScratch; existNewBranch = false; _targetOrg; // Documentacion processes; // Ultima salida del shell salida = ''; // Git Repository repositoryUrl; repositoryType; repositoryOwner; repositoryRepo; // Project Reference projectId; backlogColumn = 'Todo'; //Templates especiales listFilter = ListFilters.Mios; listTemplate = 'openIssues'; constructor() { this.loadConfig(); this.loadPackage(); } async labels() { const choices = [{ value: '', title: 'Ninguno' }, { value: 'new', title: 'Nuevo' }]; const labels = await this.gitApi?.getLabels(); if (labels) { labels.forEach(label => choices.push({ value: label.name, title: label.name })); } return choices; } listFilters() { const filters = [{ title: 'Solo mios abiertos', value: ListFilters.Mios, description: 'Busca los issues donde este asignado y esten en state Open' }, { title: 'Por Milestone', value: ListFilters.PorMilestone, description: 'Busca los issues de un deterinado milestone' }, { title: 'Por Label', value: ListFilters.PorLabel, description: 'Busca los issues de un deterinado label' }]; return filters; } async milestoneNumbers() { const choices = [{ value: '', title: 'Ninguno' }]; const milestones = await this.gitApi?.getMilestones(); if (milestones) { milestones.forEach(milestone => choices.push({ value: milestone.number, title: milestone.title })); } return choices; } async milestones() { const choices = [{ value: '', title: 'Ninguno' }, { value: 'new', title: 'Nuevo' }]; const milestones = await this.gitApi?.getMilestones(); if (milestones) { milestones.forEach(milestone => choices.push({ value: milestone.id, title: milestone.title })); } return choices; } loadProjectApi() { if (!this.isProjectApi) { if (this.projectServices == ProjectServices.GitHub && process.env.GITHUB_TOKEN) { if (this.repositoryOwner && this.repositoryRepo) { const token = process.env.GITHUB_TOKEN; const projectId = this.projectId && !Number.isNaN(this.projectId) ? Number.parseInt(this.projectId) : undefined; if (!projectId) { throw Error('Para gestionar proyectos en Github se necesita el projectId. npx autoforce config'); } this.gitApi = this.projectApi = new GitHubProjectApi(token, this.repositoryOwner, this.repositoryRepo, projectId); this.isProjectApi = true; if (this, this.gitServices == GitServices.GitHub) { this.isGitApi = true; } } else { logWarning(`No se pudo inicializar el conector a GitHub, Verifique repositoryOwner: ${this.repositoryOwner} o repositoryRepo: ${this.repositoryRepo} o projectId: ${this.projectId}`); } } if (this.projectServices == ProjectServices.GitLab && process.env.GITLAB_TOKEN) { if (this.repositoryOwner && this.repositoryRepo) { const token = process.env.GITLAB_TOKEN; const projectId = this.projectId && Number.isInteger(this.projectId) ? Number.parseInt(this.projectId) : undefined; this.gitApi = this.projectApi = new GitLabApi(token, this.repositoryOwner, this.repositoryRepo, projectId); this.isProjectApi = true; } else { logWarning(`No se pudo inicializar el conector a GitLab, Verifique repositoryOwner: ${this.repositoryOwner} o repositoryRepo: ${this.repositoryRepo} o projectId: ${this.projectId}`); } } } } loadGitApi() { if (!this.isGitApi) { if (!this.gitServices && this.repositoryUrl) { if (this.repositoryUrl.indexOf('github') > 0) { this.gitServices = GitServices.GitHub; } else if (this.repositoryUrl.indexOf('gitlab') > 0) { this.gitServices = GitServices.GitLab; } } if (this.gitServices == GitServices.GitHub && process.env.GITHUB_TOKEN) { if (this.repositoryOwner && this.repositoryRepo) { const token = process.env.GITHUB_TOKEN; this.gitApi = new GitHubApi(token, this.repositoryOwner, this.repositoryRepo); this.isGitApi = true; } else { logWarning(`No se pudo inicializar el conector a GitHub, Verifique repositoryOwner: ${this.repositoryOwner} o repositoryRepo: ${this.repositoryRepo} o projectId: ${this.projectId}`); } } if (this.gitServices == GitServices.GitLab && process.env.GITLAB_TOKEN) { if (this.repositoryOwner && this.repositoryRepo) { const token = process.env.GITLAB_TOKEN; this.gitApi = new GitLabApi(token, this.repositoryOwner, this.repositoryRepo); this.isGitApi = true; } else { logWarning(`No se pudo inicializar el conector a GitLab, Verifique repositoryOwner: ${this.repositoryOwner} o repositoryRepo: ${this.repositoryRepo} o projectId: ${this.projectId}`); } } } } loadPackage() { const data = getDataFromPackage(); this.repositoryUrl = data.repositoryUrl; this.repositoryOwner = data.repositoryOwner; this.repositoryRepo = data.repositoryRepo; } async createConfig() { if (!fs.existsSync(CONFIG_FILE)) { logWarning('Bienvenido! La herramienta Autoforce necesita un primer paso de configuracion antes de usarla.'); logWarning('- Podes usar el asistente ejecutando npx autoforce config'); logWarning('- Podes hacerlo manualmente leyendo la documentacion y creando .autoforce.json manualmente en el root del proyecto (https://sebastianclaros.github.io/autoforce/docs/configuracion)'); const answer = await prompts([ { type: "confirm", name: "asistente", message: "Queres ejecutar el asistente ahora?" } ]); if (answer.asistente) { await createConfigurationFile(); return true; } return false; } } loadConfig() { if (fs.existsSync(CONFIG_FILE)) { const content = fs.readFileSync(CONFIG_FILE, "utf8"); try { const config = JSON.parse(content); for (const key in config) { this.set(key, config[key]); } } catch { throw new Error(`Verifique que el ${CONFIG_FILE} sea json valido`); } return true; } } init() { this.createConfig(); this.loadProjectApi(); this.loadGitApi(); // this.branchName = getBranchName(); if (typeof this.branchName === 'string') { this.issueFromBranch(this.branchName); } } get targetOrg() { if (!this._targetOrg) { this._targetOrg = getTargetOrg(); } return this._targetOrg; } get existBranchScratch() { return typeof this._branchScratch !== 'undefined'; } get branchScratch() { if (!this._branchScratch && this.branchName) { this._branchScratch = getOrganizationObject(this.branchName); } return this._branchScratch; } getProcessHeader(fullpath) { const fileContents = fs.readFileSync(fullpath, 'utf8'); const { data } = matter(fileContents); return data; } addProcessMetadata(component, items) { if (!this.process) { throw new Error(`No hay proceso configurado`); } const content = fs.readFileSync(CONFIG_FILE, "utf8"); try { const config = JSON.parse(content); const processes = config.processes || {}; if (!processes[this.process]) { processes[this.process] = {}; } if (!processes[this.process][component]) { processes[this.process][component] = []; } addNewItems(processes[this.process][component], items); config.processes = processes; fs.writeFileSync(CONFIG_FILE, JSON.stringify(config, null, 2)); } catch { throw new Error(`No se pudo guardar la metadata`); } } set dictionaryFolder(value) { this._dictionaryFolder = value; } get dictionaryFolder() { return this._dictionaryFolder ? this._dictionaryFolder : getProjectPath() + '/docs'; } get processesHeader() { if (!this._processesHeader) { this._processesHeader = {}; const folders = getFiles(this.dictionaryFolder, filterDirectory, true, ['diccionarios']); for (const folder of folders) { const fullpath = `${this.dictionaryFolder}/${folder}`; const filenames = getFiles(fullpath, filterProcesses); for (const filename of filenames) { const header = this.getProcessHeader(fullpath + "/" + filename); if (header.process) { this._processesHeader[header.process] = { ...header, folder: fullpath, filename }; } } } } return this._processesHeader; } // TODO: merge con getProcessFromDocs getProcessMetadata() { const folders = getFiles(this.dictionaryFolder, filterDirectory, true, ['diccionarios']); const retArray = []; for (const folder of folders) { const fullpath = `${process.cwd()}/docs/${folder}`; const processes = getFiles(fullpath, filterProcesses); for (const process of processes) { const header = this.getProcessHeader(fullpath + "/" + process); const processKey = convertNameToKey(header.slug || header.title || process); if (this.processes && this.processes[processKey]) { retArray.push({ folder, name: convertKeyToName(processKey), ...this.processes[processKey] }); } } } return retArray; } modulesFolders() { const folders = getFiles(this.dictionaryFolder, filterDirectory, true, ['diccionarios', 'src', '.docusaurus', 'node_modules']).sort(); return folders.map(module => { return { value: this.dictionaryFolder + '/' + module, title: module.replaceAll('/', ' > ') }; }); } get existScratch() { return typeof this.scratch !== 'undefined'; } get scratch() { if (!this._scratch) { this._scratch = getCurrentOrganization(); } return this._scratch; } async validate(guards) { for (const guard of guards) { // Chequeo de variables de entorno if (guard[0] === '$') { if (!process.env[guard.substring(1)]) { throw new Error(`La variable de entorno ${guard} no esta configurada.`); } } else { const value = await this.get(guard); if (!value) { throw new Error(`No se encontro la variable ${guard} en el contexto. Ejecute yarn auto config o lea el index.md para mas informacion.`); } } } } issueFromBranch(branchName) { const branchSplit = branchName.split("/"); if (branchSplit.length > 1) { this.issueType = branchSplit[0]; if (!Number.isNaN(Number(branchSplit[1]))) { this.issueNumber = branchSplit[1]; } else { // [this.issueNumber, this.issueTitle] = branchSplit[1].split() // /^([^ -]+)[ -](.*)$/.exec( branchSplit[1]).slice(1); } } } branchNameFromIssue(issueType, issueNumber, title) { let baseName = issueType + '/' + issueNumber; if (title) { baseName += ' - ' + title.replaceAll(' ', '-'); } return baseName; } get isDevelopment() { return this.issueType === 'feature' || this.issueType === 'fix'; } get isNewDevelopment() { return this.newIssueType === 'feature' || this.newIssueType === 'fix'; } get newIssueNumber() { return this._newIssueNumber; } set newIssueNumber(value) { this._newIssueNumber = value; if (this.newIssueType) { this.setNewBranchName(); } } get newIssueType() { return this._newIssueType; } set newIssueType(value) { this._newIssueType = value; if (this.newIssueNumber) { this.setNewBranchName(); } } setNewBranchName() { if (this.newIssueType && this.newIssueNumber) { this.newBranchName = this.branchNameFromIssue(this.newIssueType, this.newIssueNumber); const salida = executeShell(`git show-ref refs/heads/${this.newBranchName}`); this.existNewBranch = typeof salida === 'string' && (salida.includes(this.newBranchName)); } } async askFornewBranchName() { if (!this.newBranchName) { if (!this.newIssueType) { this.newIssueType = await this.askFornewIssueType(); } if (!this.newIssueNumber) { this.newIssueNumber = await this.askFornewIssueNumber(); } this.setNewBranchName(); } return this.newBranchName; } async askFornewIssueNumber() { if (this.options.issue && this.projectApi) { const issues = await this.projectApi.searchIssues(this.options.issue); if (issues.length > 0) { return `${issues[0].number}`; } } const answer = await prompts([ { type: "text", name: "newIssueNumber", message: "Por favor ingrese el nuevo issueNumber?" } ]); return answer.newIssueNumber; } set process(value) { this._process = value; } getProcessFromTitle(title) { const desde = title.indexOf('['); const hasta = title.indexOf(']', desde); if (desde !== -1 && hasta !== -1) { return title.substring(desde + 1, hasta); } return; } get process() { if (!this._process && this.issueTitle) { const process = this.getProcessFromTitle(this.issueTitle); if (process) { this._process = process; } } return this._process; } async askForprocess() { if (this.projectApi && !this.issueTitle && this.issueNumber) { const issue = await this.projectApi.getIssue(this.issueNumber); this.issueTitle = issue.title; } if (this.issueTitle) { const process = this.getProcessFromTitle(this.issueTitle); if (process && this.processesHeader[process]) { return process; } } const choices = Object.values(this.processesHeader).map(header => { return { value: header.process, title: header.title }; }); const answer = await prompts([{ type: "select", name: "process", message: "Por favor seleccione el proceso", choices }]); return answer.process; } async askFornewIssueType() { const answer = await prompts([ { type: "list", name: "newIssueType", initial: "feature", message: "Por favor ingrese el type del issue?", choices: ISSUES_TYPES } ]); return answer.newIssueType; } async convertToArrayOfInputs(inputs) { let inputsArray = []; if (Array.isArray(inputs)) { // Si viene los args como ['name1', 'names] lo convierte a [{name: 'name1'}, {name: 'name2'}] inputsArray = inputs.map(input => { return { name: input, type: 'text', message: `Por favor ingrese ${input}?` }; }); } else { // Si viene args como objeto { name1: {...}, name2: {...}} lo convierte a [{name: name1...}, {name: name2...}] for (const key in inputs) { if (typeof inputs[key].default === 'string') { inputs[key].initial = this.merge(inputs[key].default); } if (typeof inputs[key].values === 'string') { const methodName = inputs[key].values; if (typeof this[methodName] === 'function') { const choices = await this[methodName](); if (Array.isArray(choices)) { inputs[key].choices = choices; } } } inputsArray.push({ ...{ name: key, type: 'text', message: `Por favor ingrese ${key}?` }, ...inputs[key] }); } } return inputsArray; } async askForExit( /* prompt: PromptObject */) { const answer = await prompts([ { type: "confirm", name: "exit", initial: true, message: "Desea salir?" } ]); if (answer.exit) { process.exit(-1); } } mergeArgs(args) { if (Array.isArray(args)) { const argsArray = []; for (const argName of args) { if (typeof argName === 'string') { argsArray.push(this.merge(argName)); } } return argsArray; } else if (typeof args === 'object') { const argsObject = {}; for (const argName in args) { argsObject[argName] = this.merge(args[argName]); } return argsObject; } throw new Error(`Los argumentos ${args} son incompatibles para el merge`); } async askForArguments(inputs) { // unifica los dos tipos de inputs (array y objeto) en un array de inputs try { const inputsArray = await this.convertToArrayOfInputs(inputs); for (const input of inputsArray) { const hasValue = await this.get(input.name); if (!hasValue) { const answer = await prompts([input], { onCancel: this.askForExit }); this[input.name] = answer[input.name]; } } } catch { throw new Error(`No se pudo obtener los argumentos para ${inputs}`); } } setObject(obj) { for (const field in obj) { Object.defineProperty(this, field, { value: obj[field], writable: true }); } } set(key, value) { try { this[key] = value; } catch { throw new Error(`No se puede setear el ${key} con el valor ${value} en context`); } } // Devuelve el valor o hace un askFor si esta vacio async get(key) { try { const value = this[key]; if (!value) { const askForMethod = 'askFor' + key; if (this[askForMethod] && typeof this[askForMethod] == 'function') { this[key] = await this[askForMethod](); } } return this[key]; } catch { throw new Error(`No se puedo obtener la propiedad ${key} en context`); } } merge(text) { if (typeof text != 'string' || text.indexOf('${') === -1) { return text || ''; } const matches = text.matchAll(/\$\{([^}]+)}/g); // si no tiene para merge if (matches === null) { return text; } // si es un texto con merges for (const match of matches) { const mergedValue = this[match[1]]; // si es una sola variable if (match.index == 0 && text === match[0]) { return mergedValue; } if (typeof mergedValue === 'string') { text = text.replace(match[0], mergedValue); } else if (typeof mergedValue === 'number' || typeof mergedValue === 'boolean') { text = text.replace(match[0], mergedValue.toString()); } else { throw new Error(`La propiedad '${match[1]}' del objeto context no es mergeable`); } } return text; } } const context = new Context(); let initialized = false; export function initializeContext() { try { if (initialized === false) { context.init(); initialized = true; } } catch (error) { if (error instanceof Error) { logError(error.message); } process.exit(1); } } export default context;