UNPKG

autoforce

Version:

Developer Automation tool for Github / Gitlab and Salesforce projects.

585 lines (584 loc) 21 kB
import { execSync } from "child_process"; import context, { ListFilters } from "./context.js"; import { logError, logInfo } from "./color.js"; import metadata from './metadata.js'; import prompts from "prompts"; import templateGenerator from "./template.js"; import { filterBash, getFilesInFolders, getModelFolders, storeConfig, valuesToChoices } from "./util.js"; function generateTemplate(templateFolder, templateExtension, template, context) { if (!template || !templateFolder || !templateExtension) { return; } const templateEngine = templateGenerator(templateFolder, templateExtension); const formulas = { today: Date.now(), }; const view = { ...formulas, ...context }; templateEngine.read(template); templateEngine.render(view); return templateEngine.rendered; } function createTemplate(templateFolders, templateExtension, template, filename, folder, context) { if (!template || !filename || !templateFolders || !templateExtension) { return; } const templateEngine = templateGenerator(templateFolders, templateExtension); const formulas = { today: Date.now(), filename }; const view = { ...formulas, ...context }; templateEngine.read(template); templateEngine.render(view); templateEngine.save(filename, folder); } function convertArgsToString(args) { let argsString = ''; if (Array.isArray(args)) { for (const argName of args) { argsString += context.merge(argName) + ' '; } } else if (typeof args === 'object') { for (const argName in args) { if (!args[argName]) { argsString += argName + ' '; } else { argsString += argName + '=' + context.merge(args[argName]) + ' '; } } } return argsString; } export async function executeCommand(step) { try { context.set('command', step.command + ' ' + convertArgsToString(step.arguments)); execSync(step.command + ' ' + convertArgsToString(step.arguments), { stdio: 'inherit' }); return true; } catch (e) { context.errorMessage = `Se produjo un error al ejecutar el comando: ${e}`; return false; } } export function validateCommand(step) { if (step.command && typeof step.command == 'string') { return true; } return false; } export function validateFunction(step) { if (typeof taskFunctions[step.function] !== 'function') { logError(`No se encontro la funcion ${step.function}`); return false; } if (typeof step.arguments !== 'undefined') { if (typeof step.arguments !== 'object') { logError(`La funcion ${step.function} recibio un argumento de tipo ${typeof step.arguments} y solo soporta object`); return false; } } return true; } function getParams(func) { // String representation of the function code let str = func.toString(); str = str.replace(/\/\*[\s\S]*?\*\//g, '') .replace(/\/\/(.)*/g, '') .replace(/{[\s\S]*}/, '') .replace(/=>/g, '') .trim(); // Start parameter names after first '(' const start = str.indexOf("(") + 1; // End parameter names is just before last ')' const end = str.length - 1; const result = str.substring(start, end).split(", "); const params = []; result.forEach(element => { element = element.replace(/=[\s\S]*/g, '').trim(); if (element.length > 0) { params.push(element); } }); return params; } function createArray(fields, record) { const fieldArray = []; for (const field of fields) { const value = record[field]; fieldArray.push(value); } return fieldArray; } async function askForContinue(message) { const answer = await prompts([ { type: "confirm", name: "continue", initial: true, message } ]); return answer.continue; } async function askForCommitMessage() { const answer = await prompts([ { type: "text", name: "message", initial: "fix", validate: value => value.length > 0 ? true : "El mensaje es requerido", message: "Mensaje del commit" } ]); return answer.message; } export function getCurrentOrganization() { const salidaConfig = executeShell('sf config get target-org --json'); const salidaConfigJson = JSON.parse(salidaConfig); const targetOrg = salidaConfigJson.result[0]; const salidaOrgList = executeShell('sf org list --json'); const salidaOrgListJson = JSON.parse(salidaOrgList); for (const orgType in salidaOrgListJson.result) { for (const orgObject of salidaOrgListJson.result[orgType]) { if (orgObject.alias === targetOrg.value) { if (orgObject?.isExpired === true) { throw new Error(`La scratch ${orgObject.alias} ha expirado!`); } return orgObject; } } } throw new Error(`No se encontro la organizacion ${targetOrg.value} verifique que este activa con: sf org list. `); } // type: 'scratchOrgs', 'sandboxes', others export function getOrganizationObject(alias, type = 'scratchOrgs') { const salida = executeShell('sf org list --json'); const salidaJson = JSON.parse(salida); const orgObject = salidaJson.result[type].filter(scratch => scratch.alias === alias)[0]; if (orgObject?.isExpired === true) { throw new Error(`La scratch ${orgObject.alias} ha expirado!`); } return orgObject; } export function getTargetOrg() { const salida = executeShell('sf force config get target-org --json'); const salidaJson = JSON.parse(salida); return salidaJson.result[0].value; } export function getBranchName() { try { return executeShell("git branch --show-current"); } catch (error) { console.log(error); } return ''; } export async function executeFunction(step) { let returnValue = false; const functionName = step.function; if (typeof taskFunctions[functionName] === 'function') { taskFunctions[functionName].apply(taskFunctions); if (step.arguments && typeof step.arguments === 'object') { let mergedArgs = context.mergeArgs(step.arguments); if (!Array.isArray(mergedArgs)) { const paramNames = getParams(taskFunctions[functionName]); mergedArgs = createArray(paramNames, mergedArgs); } returnValue = await taskFunctions[functionName](...mergedArgs); } else { returnValue = await taskFunctions[functionName](); } } else { throw new Error(`No se encontro la funcion ${functionName}`); } return returnValue; } export function executeShell(command) { try { const buffer = execSync(command); const salida = buffer.toString().trim(); return (salida.endsWith("\n") ? salida.slice(0, -1) : salida); } catch { return ''; } } function getFilesChanged() { const files = []; const salida = executeShell('git diff origin/main --raw'); for (const line of salida.split('\n')) { files.push(line.split(/[ |\t]/)[5]); } return files; } export const taskFunctions = { skip() { logInfo('Error omitido por configuracion del step'); return true; }, storeConfig(variable, value) { storeConfig({ [variable]: value }); context.loadConfig(); return true; }, async docProcess() { if (!context.process) { return false; } const files = getFilesChanged(); if (files.length > 0) { for (const component in metadata) { const helper = metadata[component]; const items = helper.getItems(files); if (items.length > 0) { context.addProcessMetadata(component, items); helper.execute(items, context.process, context.module); } } } return true; }, async retrieveCode() { const tryToRetrieve = await askForContinue("Desea bajar los cambios?"); if (!tryToRetrieve) { return false; } executeShell("sf project retrieve start"); return await taskFunctions.validateScratch(); }, async validateScratch() { const salida = executeShell("sf project retrieve preview"); context.salida = salida; context.noHayCambios = salida.indexOf('No files will be deleted') !== -1 && salida.indexOf('No files will be retrieved') !== -1 && salida.indexOf('No conflicts found') !== -1; // Probar de bajarlos // sf project retrieve start return context.noHayCambios; }, async commitChanges() { const tryToCommit = await askForContinue("Desea commitear los cambios?"); if (!tryToCommit) { return false; } const message = await askForCommitMessage(); executeShell(`git add --all`); executeShell(`git commit -m "${message}"`); return await taskFunctions.checkCommitPending(); }, async publishBranch() { try { const branchName = context.branchName; const salida = executeShell(`git push origin ${branchName}`); return salida ? false : true; } catch (error) { console.log(error); } // mergeBranch return false; }, async createPullRequest() { if (context.gitApi === undefined || context.branchName === undefined || context.issueNumber === undefined) { return false; } try { context.issueFromBranch(context.branchName); const result = await context.gitApi.createPullRequest(context.branchName, `resolves #${context.issueNumber} `, 'AI not implemented yet'); return result; } catch (error) { console.log(error); } // mergeBranch return false; }, cancelIssue() { console.log('Not implemented'); return false; }, deployIssue() { console.log('Not implemented'); return false; }, rollbackIssue() { console.log('Not implemented'); return false; }, async createMilestone(title, state = 'open', description, dueOn) { if (context.projectApi === undefined || context.gitApi === undefined) { return false; } const result = await context.gitApi.createMilestone(title, state, description, dueOn); return result?.id ? true : false; }, async updateMilestone(title, state = 'open', description, dueOn) { if (context.projectApi === undefined || context.gitApi === undefined) { return false; } const result = await context.gitApi.updateMilestone(title, state, description, dueOn); return result?.id ? true : false; }, async createIssue(title, label, body, milestone) { if (context.projectApi === undefined || context.gitApi === undefined) { return false; } if (label === 'new') { const answer = await prompts([ { message: 'Nombre del Label', name: 'nombre', type: 'text' } ]); if (answer.nombre !== undefined) { const result = await context.gitApi.createLabel(answer.nombre); if (result?.id) { label = result.id; } } else { label = ''; } } if (milestone === 'new') { const answer = await prompts([ { message: 'Nombre del Milestone', name: 'nombre', type: 'text' } ]); if (answer.nombre !== undefined) { const result = await context.gitApi.createMilestone(answer.nombre); milestone = result?.id; } else { milestone = ''; } } const issue = await context.projectApi.createIssue(title, context.backlogColumn, label, body, milestone); if (issue) { console.log(`Se creo el issue ${issue.number}`); console.log(`${issue.url}`); // if ( issue.milestone) { // console.log(`Milestone ${issue.milestone.title} expira en ${issue.milestone.dueOn} `); // } return true; } return false; }, async createTemplate(template, folder, name, identifier) { const filename = name.toLocaleLowerCase().replaceAll(' ', '-') + '.md'; createTemplate(getModelFolders('templates'), 'md', template, filename, folder, { name, identifier }); return true; }, async validateIssue(issueNumber, states) { if (context.projectApi === undefined) { context.errorMessage = 'context.projectApi esta undefined'; return false; } const issue = await context.projectApi.getIssue(issueNumber); if (!issue.state) { context.errorMessage = 'El state del issue es undefined'; return false; } const arrayStates = states.toLocaleLowerCase().replace(' ', '').split(','); const validate = arrayStates.includes(issue.state.toLocaleLowerCase().replace(' ', '')); if (!validate) { context.errorMessage = `El state del issue es "${issue.state}", mientras deberia ser ${states}`; return false; } return true; }, async validaNoseaBranchActual(newBranchName) { return getBranchName() !== newBranchName; }, async checkCommitPending() { try { const cambios = executeShell("git status --porcelain=v1"); context.salida = cambios; return cambios == ''; } catch (error) { console.log(error); } return false; }, async createBranch() { try { const newBranchName = context.newBranchName; executeShell(`git checkout -b ${newBranchName} origin/main`); context.set('branchName', getBranchName()); return true; } catch (error) { console.log(error); } // mergeBranch return false; }, async mergeBranch() { try { executeShell(`git fetch`); executeShell(`git merge origin/main`); return true; } catch (error) { console.log(error); } return false; }, async moveIssue(issueNumber, state) { if (context.projectApi === undefined) { return false; } const result = await context.projectApi.moveIssue(issueNumber, state); return result; }, async assignBranchToIssue(issueNumber, newBranchName) { if (context.gitApi === undefined) { return false; } const commitSha = executeShell(`git rev-parse --verify main`); const result = await context.gitApi.assignBranchToIssue(issueNumber, newBranchName, commitSha); return result; }, async assignIssueToMe(issueNumber) { if (!context.projectApi) { return false; } const result = await context.projectApi.assignIssueToMe(issueNumber); return result; }, async viewIssue(issueNumber, template = 'viewIssue') { if (!context.projectApi) { return false; } const result = await context.projectApi.getIssue(issueNumber); const rendered = generateTemplate(getModelFolders('templates'), 'bash', template, { issue: result, ...context }); console.log(rendered); return true; }, async listIssues(listFilter, listTemplate) { let filter = '{states: OPEN}'; const extension = '*'; if (!listFilter) { listFilter = context.options.filter || context.listFilter; } if (!listTemplate) { listTemplate = context.options.template || context.listTemplate; } if (!context.projectApi || !context.gitApi) { return false; } if (!listFilter) { const answer = await prompts([ { message: 'Elija un filtro, o bien lo puede dejar fijo en autoforce como listFilter', name: 'filter', type: 'select', initial: 0, choices: context.listFilters } ]); listFilter = answer.filter; } if (listFilter === ListFilters.PorMilestone) { if (context.options.milestone) { const milestoneFilter = (await context.gitApi.getMilestones()).filter(milestone => milestone.title == context.options.milestone); if (milestoneFilter.length === 0) { return false; } filter = `{ milestoneNumber: "${milestoneFilter[0].number}"}`; } else { const choices = (await context.gitApi.getMilestones()).map(milestone => { return { value: milestone.number, title: milestone.title }; }); choices.push({ value: '', title: 'Issues sin Milestone' }); choices.push({ value: '*', title: 'Issues con Milestone' }); const answer = await prompts([ { message: 'Elija un milestone', name: 'filterValue', type: 'select', initial: 0, choices } ]); filter = `{ milestoneNumber: "${answer.filterValue}"}`; if (answer.filterValue === undefined) return false; } } if (listFilter === ListFilters.PorLabel) { if (context.options.label) { filter = `{labels: "${context.options.label}"}`; } else { const labels = (await context.gitApi.getLabels()).map(label => label.name); const choices = valuesToChoices(labels); const answer = await prompts([ { message: 'Elija un label', name: 'filterValue', type: 'select', initial: 0, choices } ]); if (answer.filterValue === undefined) return false; filter = `{labels: "${answer.filterValue}"}`; } } if (!listTemplate) { const files = getFilesInFolders(getModelFolders('templates'), filterBash).map(filename => filename.split(".")[0]); const templates = valuesToChoices(files); const answer = await prompts([ { message: 'Elija un template, o bien lo puede dejar en autoforce como listTemplate', name: 'template', type: 'select', initial: 0, choices: templates } ]); listTemplate = answer.template; if (listTemplate === undefined) return false; } const result = await context.projectApi.getIssuesWithFilter(filter); console.log(context.version); const rendered = generateTemplate(getModelFolders('templates'), extension, listTemplate, { issues: result, context }); console.log(rendered); return true; }, async checkIssueType(issueNumber) { if (!context.projectApi) { return false; } const issue = await context.projectApi.getIssue(issueNumber); // Setea el issueType segun el issue try { let newIssueType = 'feature'; if (issue.labels && issue.labels?.length > 0) { if (issue.labels.includes('documentation')) { newIssueType = 'doc'; } else if (issue.labels.includes('automation')) { newIssueType = 'automation'; } else if (issue.labels.includes('bug')) { newIssueType = 'fix'; } } context.newIssueType = newIssueType; } catch (error) { console.log(error); } return true; } };