@cto.ai/ops
Version:
š» CTO.ai Ops - The CLI built for Teams š
385 lines (384 loc) ⢠19.6 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const sdk_1 = require("@cto.ai/sdk");
const fs = tslib_1.__importStar(require("fs-extra"));
const path = tslib_1.__importStar(require("path"));
const base_1 = tslib_1.__importStar(require("../base"));
const env_1 = require("../constants/env");
const opConfig_1 = require("../constants/opConfig");
const CustomErrors_1 = require("../errors/CustomErrors");
const utils_1 = require("../utils");
const get_docker_1 = tslib_1.__importDefault(require("../utils/get-docker"));
const validate_1 = require("../utils/validate");
const ErrorTemplate_1 = require("../errors/ErrorTemplate");
class Publish extends base_1.default {
constructor() {
super(...arguments);
this.resolvePath = async (opPath) => {
return path.resolve(process.cwd(), opPath);
};
this.checkDocker = async (opPath) => {
const docker = await get_docker_1.default(this, 'publish');
if (!docker) {
throw new Error('No docker');
}
return { opPath, docker };
};
this.getOpsAndWorkFlows = async (inputs) => {
const { opPath } = inputs;
const manifest = await fs
.readFile(path.join(opPath, opConfig_1.OP_FILE), 'utf8')
.catch((err) => {
this.debug('%O', err);
throw new CustomErrors_1.FileNotFoundError(err, opPath, opConfig_1.OP_FILE);
});
if (!manifest)
throw new CustomErrors_1.NoLocalOpsFound();
const { ops, version, workflows } = utils_1.parseYaml(manifest);
if (!ops && !workflows) {
throw new CustomErrors_1.NoLocalOpsOrWorkflowsFound();
}
return Object.assign(Object.assign({}, inputs), { opCommands: ops, opWorkflows: workflows, version });
};
this.determineQuestions = async (inputs) => {
const { opCommands, opWorkflows } = inputs;
let opsAndWorkflows;
if (opCommands && opCommands.length && opWorkflows && opWorkflows.length) {
;
({ opsAndWorkflows } = await sdk_1.ux.prompt({
type: 'list',
name: 'opsAndWorkflows',
message: `\n Which would you like to publish ${sdk_1.ux.colors.reset.green('ā')}`,
choices: [
{ name: 'Commands', value: opConfig_1.COMMAND },
{ name: 'Workflows', value: opConfig_1.WORKFLOW },
'Both',
],
afterMessage: `${sdk_1.ux.colors.reset.green('ā')}`,
}));
}
else if (!opCommands || !opCommands.length) {
opsAndWorkflows = opConfig_1.WORKFLOW;
}
else {
opsAndWorkflows = opConfig_1.COMMAND;
}
return Object.assign(Object.assign({}, inputs), { commandsAndWorkflows: opsAndWorkflows });
};
this.selectOpsAndWorkFlows = async (inputs) => {
let { commandsAndWorkflows, opCommands, opWorkflows } = inputs;
switch (commandsAndWorkflows) {
case opConfig_1.COMMAND:
opCommands = await this.selectOps(opCommands);
break;
case opConfig_1.WORKFLOW:
opWorkflows = await this.selectWorkflows(opWorkflows);
break;
default:
opCommands = await this.selectOps(opCommands);
opWorkflows = await this.selectWorkflows(opWorkflows);
}
return Object.assign(Object.assign({}, inputs), { opCommands,
opWorkflows,
commandsAndWorkflows });
};
this.selectOps = async (ops) => {
if (ops.length <= 1) {
return ops;
}
const answers = await sdk_1.ux.prompt({
type: 'checkbox',
name: 'ops',
message: `\n Which ops would you like to publish ${sdk_1.ux.colors.reset.green('ā')}`,
choices: ops.map(op => {
return {
value: op,
name: `${op.name} - ${op.description}`,
};
}),
validate: input => input.length > 0,
});
return answers.ops;
};
this.selectWorkflows = async (workflows) => {
if (workflows.length <= 1) {
return workflows;
}
const answers = await sdk_1.ux.prompt({
type: 'checkbox',
name: 'workflows',
message: `\n Which workflows would you like to publish ${sdk_1.ux.colors.reset.green('ā')}`,
choices: workflows.map(workflow => {
return {
value: workflow,
name: `${workflow.name} - ${workflow.description}`,
};
}),
validate: input => input.length > 0,
});
return answers.workflows;
};
this.findOpsWhereVersionAlreadyExists = async (inputs) => {
const { existingVersions: existingCommandVersions, filteredOps: opCommands, } = await this.filterExistingOps(inputs.opCommands);
const { existingVersions: existingWorkflowVersions, filteredOps: opWorkflows, } = await this.filterExistingOps(inputs.opWorkflows);
return Object.assign(Object.assign({}, inputs), { opCommands,
opWorkflows, existingVersions: [
...existingCommandVersions,
...existingWorkflowVersions,
] });
};
this.filterExistingOps = async (ops) => {
let filteredOps = [];
let existingVersions = [];
for (const op of ops) {
try {
await this.services.api.find(`/private/teams/${this.team.name}/ops/${op.name}/versions/${op.version}`, {
headers: {
Authorization: this.accessToken,
},
});
existingVersions = existingVersions.concat(op);
}
catch (err) {
if (err.error[0].code === 404) {
filteredOps = filteredOps.concat(op);
continue;
}
throw new CustomErrors_1.APIError(err);
}
}
return { existingVersions, filteredOps };
};
this.getNewVersion = async (inputs) => {
if (inputs.existingVersions.length === 0)
return inputs;
let manifest = await fs.readFile(path.join(inputs.opPath, opConfig_1.OP_FILE), 'utf8');
this.log('\n š¤ It seems like the version of the op that you are trying to publish already taken. \n Add a new version indicator in order to publish');
for (let existingOp of inputs.existingVersions) {
this.log(`${this.ux.colors.callOutCyan(`Current version for ${existingOp.name}:`)} ${this.ux.colors.white(existingOp.version)}`);
const { newVersion } = await this.ux.prompt({
type: 'input',
name: 'newVersion',
message: '\nāļø Update version:',
transformer: input => {
return this.ux.colors.white(input);
},
validate: async (input) => {
try {
if (input === '')
return 'Please enter a version';
if (!validate_1.validVersionChars.test(input)) {
return 'ā Sorry, version is required and can only contain letters, digits, underscores, \n periods and dashes and must start and end with a letter or a digit';
}
await this.services.api.find(`/private/teams/${this.team.name}/ops/${existingOp.name}/versions/${input}`, {
headers: {
Authorization: this.accessToken,
},
});
return 'That version is already taken';
}
catch (err) {
if (err.error[0].code === 404) {
return true;
}
throw new CustomErrors_1.APIError(err);
}
},
});
manifest = manifest.replace(`name: ${existingOp.name}:${existingOp.version}`, `name: ${existingOp.name}:${newVersion}`);
existingOp.version = newVersion;
if (existingOp.type === opConfig_1.COMMAND_TYPE) {
inputs.opCommands = inputs.opCommands.concat(existingOp);
const opImageTag = utils_1.getOpImageTag(this.team.name, existingOp.name, existingOp.version, existingOp.isPublic);
const image = utils_1.getOpUrl(env_1.OPS_REGISTRY_HOST, opImageTag);
await this.services.imageService.build(image, path.resolve(process.cwd(), inputs.opPath), existingOp);
}
else if (existingOp.type === opConfig_1.WORKFLOW_TYPE) {
inputs.opWorkflows = inputs.opWorkflows.concat(existingOp);
}
}
fs.writeFileSync(path.join(inputs.opPath, opConfig_1.OP_FILE), manifest);
return Object.assign({}, inputs);
};
this.getRegistryAuth = async (name, version) => {
try {
const registryAuth = await this.services.registryAuthService.create(this.accessToken, this.team.name, name, version, false, true);
return registryAuth;
}
catch (err) {
throw new CustomErrors_1.CouldNotGetRegistryToken(err);
}
};
this.publishOpsAndWorkflows = async (inputs) => {
switch (inputs.commandsAndWorkflows) {
case opConfig_1.COMMAND:
await this.opsPublishLoop(inputs);
break;
case opConfig_1.WORKFLOW:
await this.workflowsPublishLoop(inputs);
break;
default:
await this.opsPublishLoop(inputs);
await this.workflowsPublishLoop(inputs);
}
};
this.opsPublishLoop = async ({ opCommands, version }) => {
try {
for (const op of opCommands) {
if (!validate_1.isValidOpName(op.name)) {
throw new CustomErrors_1.InvalidInputCharacter('Op Name');
}
if (!validate_1.isValidOpVersion(op)) {
throw new CustomErrors_1.InvalidOpVersionFormat();
}
const { publishDescription } = await this.ux.prompt({
type: 'input',
name: 'publishDescription',
message: `\nProvide a changelog of what's new for ${op.name}:${op.version} ${sdk_1.ux.colors.reset.green('ā')}\n\n ${sdk_1.ux.colors.white('āļø Changelog:')}`,
afterMessage: sdk_1.ux.colors.reset.green('ā'),
afterMessageAppend: sdk_1.ux.colors.reset(' added!'),
validate: this._validateDescription,
});
op.publishDescription = publishDescription;
const opName = utils_1.getOpImageTag(this.team.name, op.name, op.version, op.isPublic);
const localImage = await this.services.imageService.checkLocalImage(`${env_1.OPS_REGISTRY_HOST}/${opName}`);
if (!localImage) {
throw new CustomErrors_1.DockerPublishNoImageFound(op.name, this.team.name);
}
if ('run' in op) {
op.type = opConfig_1.COMMAND_TYPE;
const { data: apiOp, } = await this.services.publishService.publishOpToAPI(op, version, this.team.name, this.accessToken, this.services.api);
const registryAuth = await this.getRegistryAuth(op.name, op.version);
await this.services.publishService.publishOpToRegistry(apiOp, registryAuth, this.team.name, this.accessToken, this.services.registryAuthService, this.services.api, version);
this.sendAnalytics('op', apiOp);
}
}
}
catch (err) {
if (err instanceof ErrorTemplate_1.ErrorTemplate) {
throw err;
}
throw new CustomErrors_1.APIError(err);
4;
}
};
this.workflowsPublishLoop = async ({ opWorkflows, version }) => {
try {
for (const workflow of opWorkflows) {
if (!validate_1.isValidOpName(workflow.name)) {
throw new CustomErrors_1.InvalidInputCharacter('Workflow Name');
}
if (!validate_1.isValidOpVersion(workflow)) {
throw new CustomErrors_1.InvalidOpVersionFormat();
}
const { publishDescription } = await this.ux.prompt({
type: 'input',
name: 'publishDescription',
message: `\nProvide a publish description for ${workflow.name}:${workflow.version} ${sdk_1.ux.colors.reset.green('ā')}\n\n ${sdk_1.ux.colors.white('Description:')}`,
afterMessage: sdk_1.ux.colors.reset.green('ā'),
afterMessageAppend: sdk_1.ux.colors.reset(' added!'),
validate: this._validateDescription,
});
workflow.publishDescription = publishDescription;
if ('remote' in workflow && workflow.remote) {
const newSteps = [];
for (const step of workflow.steps) {
let newStep = '';
if (await this.services.buildStepService.isGlueCode(step)) {
const opPath = path.resolve(__dirname, './../templates/workflowsteps/js/');
newStep = await this.services.buildStepService.buildAndPublishGlueCode(step, this.team.id, this.team.name, this.accessToken, opPath, this.user, this.services.publishService, this.services.opService, this.services.api, this.services.registryAuthService, this.state.config, workflow.isPublic, version);
newSteps.push(newStep);
}
else {
if (!this.services.buildStepService.isOpRun(step)) {
this.debug('InvalidStepsFound - Step:', step);
throw new CustomErrors_1.InvalidStepsFound(step);
}
newSteps.push(step);
}
}
workflow.steps = newSteps;
}
try {
const { data: apiWorkflow, } = await this.services.api.create(`/private/teams/${this.team.name}/ops`, Object.assign(Object.assign({}, workflow), { platformVersion: version, type: 'workflow' }), {
headers: {
Authorization: this.accessToken,
},
});
this.log(`\nš ${sdk_1.ux.colors.callOutCyan(apiWorkflow.name)} has been published!`);
this.log(`š„ Visit your Op page here: ${sdk_1.ux.url(`${env_1.OPS_API_HOST}registry/${this.team.name}/${apiWorkflow.name}`, `<${env_1.OPS_API_HOST}${this.team.name}/${apiWorkflow.name}>`)}\n`);
this.sendAnalytics('workflow', apiWorkflow);
}
catch (err) {
this.debug('%O', err);
const InvalidWorkflowStepCodes = [400, 404];
if (err &&
err.error &&
err.error[0] &&
InvalidWorkflowStepCodes.includes(err.error[0].code)) {
if (err.error[0].message === 'version is taken') {
throw new CustomErrors_1.VersionIsTaken();
}
throw new CustomErrors_1.InvalidWorkflowStep(err);
}
throw new CustomErrors_1.CouldNotCreateWorkflow(err.message);
}
}
}
catch (err) {
if (err instanceof ErrorTemplate_1.ErrorTemplate)
throw err;
throw new CustomErrors_1.APIError(err);
}
};
this.sendAnalytics = (publishType, opOrWorkflow) => {
this.services.analytics.track({
userId: this.user.email,
teamId: this.team.id,
cliEvent: 'Ops CLI Publish',
event: 'Ops CLI Publish',
properties: {
name: opOrWorkflow.name,
team: this.team.name,
namespace: `@${this.team.name}/${opOrWorkflow.name}`,
email: this.user.email,
username: this.user.username,
type: publishType,
description: opOrWorkflow.description,
image: `${env_1.OPS_REGISTRY_HOST}/${opOrWorkflow.id.toLowerCase()}:${opOrWorkflow.version}`,
tag: opOrWorkflow.version,
},
});
};
}
_validateDescription(input) {
if (input === '')
return 'You need to provide a publish description of your op before continuing';
return true;
}
async run() {
try {
await this.isLoggedIn();
const { args } = this.parse(Publish);
const publishPipeline = utils_1.asyncPipe(this.resolvePath, this.checkDocker, this.getOpsAndWorkFlows, this.determineQuestions, this.selectOpsAndWorkFlows, this.findOpsWhereVersionAlreadyExists, this.getNewVersion, this.publishOpsAndWorkflows);
await publishPipeline(args.path);
}
catch (err) {
this.debug('%O', err);
this.config.runHook('error', { err, accessToken: this.accessToken });
}
}
}
exports.default = Publish;
Publish.description = 'Publish an Op to your team.';
Publish.flags = {
help: base_1.flags.help({ char: 'h' }),
};
Publish.args = [
{
name: 'path',
description: 'Path to the op you want to publish.',
required: true,
},
];