1 | "use strict";
|
2 | Object.defineProperty(exports, "__esModule", { value: true });
|
3 | const tslib_1 = require("tslib");
|
4 | const sdk_1 = require("@cto.ai/sdk");
|
5 | const fs = tslib_1.__importStar(require("fs-extra"));
|
6 | const path = tslib_1.__importStar(require("path"));
|
7 | const base_1 = tslib_1.__importStar(require("../base"));
|
8 | const env_1 = require("../constants/env");
|
9 | const opConfig_1 = require("../constants/opConfig");
|
10 | const CustomErrors_1 = require("../errors/CustomErrors");
|
11 | const utils_1 = require("../utils");
|
12 | const get_docker_1 = tslib_1.__importDefault(require("../utils/get-docker"));
|
13 | const validate_1 = require("../utils/validate");
|
14 | const ErrorTemplate_1 = require("../errors/ErrorTemplate");
|
15 | class Publish extends base_1.default {
|
16 | constructor() {
|
17 | super(...arguments);
|
18 | this.resolvePath = async (opPath) => {
|
19 | return path.resolve(process.cwd(), opPath);
|
20 | };
|
21 | this.checkDocker = async (opPath) => {
|
22 | const docker = await get_docker_1.default(this, 'publish');
|
23 | if (!docker) {
|
24 | throw new Error('No docker');
|
25 | }
|
26 | return { opPath, docker };
|
27 | };
|
28 | this.getOpsAndWorkFlows = async (inputs) => {
|
29 | const { opPath } = inputs;
|
30 | const manifest = await fs
|
31 | .readFile(path.join(opPath, opConfig_1.OP_FILE), 'utf8')
|
32 | .catch((err) => {
|
33 | this.debug('%O', err);
|
34 | throw new CustomErrors_1.FileNotFoundError(err, opPath, opConfig_1.OP_FILE);
|
35 | });
|
36 | if (!manifest)
|
37 | throw new CustomErrors_1.NoLocalOpsFound();
|
38 | const { ops, version, workflows } = utils_1.parseYaml(manifest);
|
39 | if (!ops && !workflows) {
|
40 | throw new CustomErrors_1.NoLocalOpsOrWorkflowsFound();
|
41 | }
|
42 | return Object.assign(Object.assign({}, inputs), { opCommands: ops, opWorkflows: workflows, version });
|
43 | };
|
44 | this.determineQuestions = async (inputs) => {
|
45 | const { opCommands, opWorkflows } = inputs;
|
46 | let opsAndWorkflows;
|
47 | if (opCommands && opCommands.length && opWorkflows && opWorkflows.length) {
|
48 | ;
|
49 | ({ opsAndWorkflows } = await sdk_1.ux.prompt({
|
50 | type: 'list',
|
51 | name: 'opsAndWorkflows',
|
52 | message: `\n Which would you like to publish ${sdk_1.ux.colors.reset.green('ā')}`,
|
53 | choices: [
|
54 | { name: 'Commands', value: opConfig_1.COMMAND },
|
55 | { name: 'Workflows', value: opConfig_1.WORKFLOW },
|
56 | 'Both',
|
57 | ],
|
58 | afterMessage: `${sdk_1.ux.colors.reset.green('ā')}`,
|
59 | }));
|
60 | }
|
61 | else if (!opCommands || !opCommands.length) {
|
62 | opsAndWorkflows = opConfig_1.WORKFLOW;
|
63 | }
|
64 | else {
|
65 | opsAndWorkflows = opConfig_1.COMMAND;
|
66 | }
|
67 | return Object.assign(Object.assign({}, inputs), { commandsAndWorkflows: opsAndWorkflows });
|
68 | };
|
69 | this.selectOpsAndWorkFlows = async (inputs) => {
|
70 | let { commandsAndWorkflows, opCommands, opWorkflows } = inputs;
|
71 | switch (commandsAndWorkflows) {
|
72 | case opConfig_1.COMMAND:
|
73 | opCommands = await this.selectOps(opCommands);
|
74 | break;
|
75 | case opConfig_1.WORKFLOW:
|
76 | opWorkflows = await this.selectWorkflows(opWorkflows);
|
77 | break;
|
78 | default:
|
79 | opCommands = await this.selectOps(opCommands);
|
80 | opWorkflows = await this.selectWorkflows(opWorkflows);
|
81 | }
|
82 | return Object.assign(Object.assign({}, inputs), { opCommands,
|
83 | opWorkflows,
|
84 | commandsAndWorkflows });
|
85 | };
|
86 | this.selectOps = async (ops) => {
|
87 | if (ops.length <= 1) {
|
88 | return ops;
|
89 | }
|
90 | const answers = await sdk_1.ux.prompt({
|
91 | type: 'checkbox',
|
92 | name: 'ops',
|
93 | message: `\n Which ops would you like to publish ${sdk_1.ux.colors.reset.green('ā')}`,
|
94 | choices: ops.map(op => {
|
95 | return {
|
96 | value: op,
|
97 | name: `${op.name} - ${op.description}`,
|
98 | };
|
99 | }),
|
100 | validate: input => input.length > 0,
|
101 | });
|
102 | return answers.ops;
|
103 | };
|
104 | this.selectWorkflows = async (workflows) => {
|
105 | if (workflows.length <= 1) {
|
106 | return workflows;
|
107 | }
|
108 | const answers = await sdk_1.ux.prompt({
|
109 | type: 'checkbox',
|
110 | name: 'workflows',
|
111 | message: `\n Which workflows would you like to publish ${sdk_1.ux.colors.reset.green('ā')}`,
|
112 | choices: workflows.map(workflow => {
|
113 | return {
|
114 | value: workflow,
|
115 | name: `${workflow.name} - ${workflow.description}`,
|
116 | };
|
117 | }),
|
118 | validate: input => input.length > 0,
|
119 | });
|
120 | return answers.workflows;
|
121 | };
|
122 | this.findOpsWhereVersionAlreadyExists = async (inputs) => {
|
123 | const { existingVersions: existingCommandVersions, filteredOps: opCommands, } = await this.filterExistingOps(inputs.opCommands);
|
124 | const { existingVersions: existingWorkflowVersions, filteredOps: opWorkflows, } = await this.filterExistingOps(inputs.opWorkflows);
|
125 | return Object.assign(Object.assign({}, inputs), { opCommands,
|
126 | opWorkflows, existingVersions: [
|
127 | ...existingCommandVersions,
|
128 | ...existingWorkflowVersions,
|
129 | ] });
|
130 | };
|
131 | this.filterExistingOps = async (ops) => {
|
132 | let filteredOps = [];
|
133 | let existingVersions = [];
|
134 | for (const op of ops) {
|
135 | try {
|
136 | await this.services.api.find(`/private/teams/${this.team.name}/ops/${op.name}/versions/${op.version}`, {
|
137 | headers: {
|
138 | Authorization: this.accessToken,
|
139 | },
|
140 | });
|
141 | existingVersions = existingVersions.concat(op);
|
142 | }
|
143 | catch (err) {
|
144 | if (err.error[0].code === 404) {
|
145 | filteredOps = filteredOps.concat(op);
|
146 | continue;
|
147 | }
|
148 | throw new CustomErrors_1.APIError(err);
|
149 | }
|
150 | }
|
151 | return { existingVersions, filteredOps };
|
152 | };
|
153 | this.getNewVersion = async (inputs) => {
|
154 | if (inputs.existingVersions.length === 0)
|
155 | return inputs;
|
156 | let manifest = await fs.readFile(path.join(inputs.opPath, opConfig_1.OP_FILE), 'utf8');
|
157 | 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');
|
158 | for (let existingOp of inputs.existingVersions) {
|
159 | this.log(`${this.ux.colors.callOutCyan(`Current version for ${existingOp.name}:`)} ${this.ux.colors.white(existingOp.version)}`);
|
160 | const { newVersion } = await this.ux.prompt({
|
161 | type: 'input',
|
162 | name: 'newVersion',
|
163 | message: '\nāļø Update version:',
|
164 | transformer: input => {
|
165 | return this.ux.colors.white(input);
|
166 | },
|
167 | validate: async (input) => {
|
168 | try {
|
169 | if (input === '')
|
170 | return 'Please enter a version';
|
171 | if (!validate_1.validVersionChars.test(input)) {
|
172 | 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';
|
173 | }
|
174 | await this.services.api.find(`/private/teams/${this.team.name}/ops/${existingOp.name}/versions/${input}`, {
|
175 | headers: {
|
176 | Authorization: this.accessToken,
|
177 | },
|
178 | });
|
179 | return 'That version is already taken';
|
180 | }
|
181 | catch (err) {
|
182 | if (err.error[0].code === 404) {
|
183 | return true;
|
184 | }
|
185 | throw new CustomErrors_1.APIError(err);
|
186 | }
|
187 | },
|
188 | });
|
189 | manifest = manifest.replace(`name: ${existingOp.name}:${existingOp.version}`, `name: ${existingOp.name}:${newVersion}`);
|
190 | existingOp.version = newVersion;
|
191 | if (existingOp.type === opConfig_1.COMMAND_TYPE) {
|
192 | inputs.opCommands = inputs.opCommands.concat(existingOp);
|
193 | const opImageTag = utils_1.getOpImageTag(this.team.name, existingOp.name, existingOp.version, existingOp.isPublic);
|
194 | const image = utils_1.getOpUrl(env_1.OPS_REGISTRY_HOST, opImageTag);
|
195 | await this.services.imageService.build(image, path.resolve(process.cwd(), inputs.opPath), existingOp);
|
196 | }
|
197 | else if (existingOp.type === opConfig_1.WORKFLOW_TYPE) {
|
198 | inputs.opWorkflows = inputs.opWorkflows.concat(existingOp);
|
199 | }
|
200 | }
|
201 | fs.writeFileSync(path.join(inputs.opPath, opConfig_1.OP_FILE), manifest);
|
202 | return Object.assign({}, inputs);
|
203 | };
|
204 | this.getRegistryAuth = async (name, version) => {
|
205 | try {
|
206 | const registryAuth = await this.services.registryAuthService.create(this.accessToken, this.team.name, name, version, false, true);
|
207 | return registryAuth;
|
208 | }
|
209 | catch (err) {
|
210 | throw new CustomErrors_1.CouldNotGetRegistryToken(err);
|
211 | }
|
212 | };
|
213 | this.publishOpsAndWorkflows = async (inputs) => {
|
214 | switch (inputs.commandsAndWorkflows) {
|
215 | case opConfig_1.COMMAND:
|
216 | await this.opsPublishLoop(inputs);
|
217 | break;
|
218 | case opConfig_1.WORKFLOW:
|
219 | await this.workflowsPublishLoop(inputs);
|
220 | break;
|
221 | default:
|
222 | await this.opsPublishLoop(inputs);
|
223 | await this.workflowsPublishLoop(inputs);
|
224 | }
|
225 | };
|
226 | this.opsPublishLoop = async ({ opCommands, version }) => {
|
227 | try {
|
228 | for (const op of opCommands) {
|
229 | if (!validate_1.isValidOpName(op.name)) {
|
230 | throw new CustomErrors_1.InvalidInputCharacter('Op Name');
|
231 | }
|
232 | if (!validate_1.isValidOpVersion(op)) {
|
233 | throw new CustomErrors_1.InvalidOpVersionFormat();
|
234 | }
|
235 | const { publishDescription } = await this.ux.prompt({
|
236 | type: 'input',
|
237 | name: 'publishDescription',
|
238 | 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:')}`,
|
239 | afterMessage: sdk_1.ux.colors.reset.green('ā'),
|
240 | afterMessageAppend: sdk_1.ux.colors.reset(' added!'),
|
241 | validate: this._validateDescription,
|
242 | });
|
243 | op.publishDescription = publishDescription;
|
244 | const opName = utils_1.getOpImageTag(this.team.name, op.name, op.version, op.isPublic);
|
245 | const localImage = await this.services.imageService.checkLocalImage(`${env_1.OPS_REGISTRY_HOST}/${opName}`);
|
246 | if (!localImage) {
|
247 | throw new CustomErrors_1.DockerPublishNoImageFound(op.name, this.team.name);
|
248 | }
|
249 | if ('run' in op) {
|
250 | op.type = opConfig_1.COMMAND_TYPE;
|
251 | const { data: apiOp, } = await this.services.publishService.publishOpToAPI(op, version, this.team.name, this.accessToken, this.services.api);
|
252 | const registryAuth = await this.getRegistryAuth(op.name, op.version);
|
253 | await this.services.publishService.publishOpToRegistry(apiOp, registryAuth, this.team.name, this.accessToken, this.services.registryAuthService, this.services.api, version);
|
254 | this.sendAnalytics('op', apiOp);
|
255 | }
|
256 | }
|
257 | }
|
258 | catch (err) {
|
259 | if (err instanceof ErrorTemplate_1.ErrorTemplate) {
|
260 | throw err;
|
261 | }
|
262 | throw new CustomErrors_1.APIError(err);
|
263 | 4;
|
264 | }
|
265 | };
|
266 | this.workflowsPublishLoop = async ({ opWorkflows, version }) => {
|
267 | try {
|
268 | for (const workflow of opWorkflows) {
|
269 | if (!validate_1.isValidOpName(workflow.name)) {
|
270 | throw new CustomErrors_1.InvalidInputCharacter('Workflow Name');
|
271 | }
|
272 | if (!validate_1.isValidOpVersion(workflow)) {
|
273 | throw new CustomErrors_1.InvalidOpVersionFormat();
|
274 | }
|
275 | const { publishDescription } = await this.ux.prompt({
|
276 | type: 'input',
|
277 | name: 'publishDescription',
|
278 | message: `\nProvide a publish description for ${workflow.name}:${workflow.version} ${sdk_1.ux.colors.reset.green('ā')}\n\n ${sdk_1.ux.colors.white('Description:')}`,
|
279 | afterMessage: sdk_1.ux.colors.reset.green('ā'),
|
280 | afterMessageAppend: sdk_1.ux.colors.reset(' added!'),
|
281 | validate: this._validateDescription,
|
282 | });
|
283 | workflow.publishDescription = publishDescription;
|
284 | if ('remote' in workflow && workflow.remote) {
|
285 | const newSteps = [];
|
286 | for (const step of workflow.steps) {
|
287 | let newStep = '';
|
288 | if (await this.services.buildStepService.isGlueCode(step)) {
|
289 | const opPath = path.resolve(__dirname, './../templates/workflowsteps/js/');
|
290 | 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);
|
291 | newSteps.push(newStep);
|
292 | }
|
293 | else {
|
294 | if (!this.services.buildStepService.isOpRun(step)) {
|
295 | this.debug('InvalidStepsFound - Step:', step);
|
296 | throw new CustomErrors_1.InvalidStepsFound(step);
|
297 | }
|
298 | newSteps.push(step);
|
299 | }
|
300 | }
|
301 | workflow.steps = newSteps;
|
302 | }
|
303 | try {
|
304 | const { data: apiWorkflow, } = await this.services.api.create(`/private/teams/${this.team.name}/ops`, Object.assign(Object.assign({}, workflow), { platformVersion: version, type: 'workflow' }), {
|
305 | headers: {
|
306 | Authorization: this.accessToken,
|
307 | },
|
308 | });
|
309 | this.log(`\nš ${sdk_1.ux.colors.callOutCyan(apiWorkflow.name)} has been published!`);
|
310 | 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`);
|
311 | this.sendAnalytics('workflow', apiWorkflow);
|
312 | }
|
313 | catch (err) {
|
314 | this.debug('%O', err);
|
315 | const InvalidWorkflowStepCodes = [400, 404];
|
316 | if (err &&
|
317 | err.error &&
|
318 | err.error[0] &&
|
319 | InvalidWorkflowStepCodes.includes(err.error[0].code)) {
|
320 | if (err.error[0].message === 'version is taken') {
|
321 | throw new CustomErrors_1.VersionIsTaken();
|
322 | }
|
323 | throw new CustomErrors_1.InvalidWorkflowStep(err);
|
324 | }
|
325 | throw new CustomErrors_1.CouldNotCreateWorkflow(err.message);
|
326 | }
|
327 | }
|
328 | }
|
329 | catch (err) {
|
330 | if (err instanceof ErrorTemplate_1.ErrorTemplate)
|
331 | throw err;
|
332 | throw new CustomErrors_1.APIError(err);
|
333 | }
|
334 | };
|
335 | this.sendAnalytics = (publishType, opOrWorkflow) => {
|
336 | this.services.analytics.track({
|
337 | userId: this.user.email,
|
338 | teamId: this.team.id,
|
339 | cliEvent: 'Ops CLI Publish',
|
340 | event: 'Ops CLI Publish',
|
341 | properties: {
|
342 | name: opOrWorkflow.name,
|
343 | team: this.team.name,
|
344 | namespace: `@${this.team.name}/${opOrWorkflow.name}`,
|
345 | email: this.user.email,
|
346 | username: this.user.username,
|
347 | type: publishType,
|
348 | description: opOrWorkflow.description,
|
349 | image: `${env_1.OPS_REGISTRY_HOST}/${opOrWorkflow.id.toLowerCase()}:${opOrWorkflow.version}`,
|
350 | tag: opOrWorkflow.version,
|
351 | },
|
352 | });
|
353 | };
|
354 | }
|
355 | _validateDescription(input) {
|
356 | if (input === '')
|
357 | return 'You need to provide a publish description of your op before continuing';
|
358 | return true;
|
359 | }
|
360 | async run() {
|
361 | try {
|
362 | await this.isLoggedIn();
|
363 | const { args } = this.parse(Publish);
|
364 | const publishPipeline = utils_1.asyncPipe(this.resolvePath, this.checkDocker, this.getOpsAndWorkFlows, this.determineQuestions, this.selectOpsAndWorkFlows, this.findOpsWhereVersionAlreadyExists, this.getNewVersion, this.publishOpsAndWorkflows);
|
365 | await publishPipeline(args.path);
|
366 | }
|
367 | catch (err) {
|
368 | this.debug('%O', err);
|
369 | this.config.runHook('error', { err, accessToken: this.accessToken });
|
370 | }
|
371 | }
|
372 | }
|
373 | exports.default = Publish;
|
374 | Publish.description = 'Publish an Op to your team.';
|
375 | Publish.flags = {
|
376 | help: base_1.flags.help({ char: 'h' }),
|
377 | };
|
378 | Publish.args = [
|
379 | {
|
380 | name: 'path',
|
381 | description: 'Path to the op you want to publish.',
|
382 | required: true,
|
383 | },
|
384 | ];
|