@cto.ai/ops
Version:
š» CTO.ai - The CLI built for Teams š
655 lines (654 loc) ⢠33.4 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
const tslib_1 = require("tslib");
const fs = tslib_1.__importStar(require("fs-extra"));
const path = tslib_1.__importStar(require("path"));
const semver_1 = tslib_1.__importDefault(require("semver"));
const cli_sdk_1 = require("@cto.ai/cli-sdk");
const strings_1 = require("../constants/strings");
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");
const OpsYml_1 = require("./../types/OpsYml");
const Docker_1 = require("../types/Docker");
class Publish extends base_1.default {
constructor() {
super(...arguments);
this.resolvePath = (opPath) => {
return path.resolve(process.cwd(), opPath);
};
this.checkDocker = async () => {
const docker = await (0, get_docker_1.default)(this, 'publish');
if (!docker) {
throw new Error('No docker');
}
};
this.getOpsAndWorkflows = async (opPath) => {
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.NoLocalOpsOrPipelinesFound();
const parsedManifest = (0, utils_1.parseYaml)(manifest);
const totalItems = parsedManifest.ops.length +
parsedManifest.workflows.length +
parsedManifest.pipelines.length +
parsedManifest.services.length;
if (totalItems === 0) {
//TODO change error
throw new CustomErrors_1.NoLocalOpsOrWorkflowsFound();
}
return parsedManifest;
};
this.convertPipelinesToWorkflows = (pipelines, opPath) => {
const pipelineCommands = [];
const pipelineWorkflows = pipelines.map(pipeline => {
const runSteps = pipeline.jobs.map(({ name, linked, params }) => {
if (linked) {
if (pipeline.linkedCommandsNames === null ||
pipeline.linkedCommandsNames === undefined) {
pipeline.linkedCommandsNames = [];
}
pipeline.linkedCommandsNames.push(name);
if (params !== undefined &&
params.length > 0 &&
(pipeline.env !== undefined || pipeline.env)) {
if (pipeline.env.static === undefined ||
pipeline.env.static === null) {
pipeline.env.static = [];
}
pipeline.env.static.push(...params);
}
}
return linked
? 'ops run ' + name
: 'ops run ' + name + ':' + pipeline.version;
});
const newWorkflow = {
name: pipeline.name,
version: pipeline.version,
description: pipeline.description,
type: opConfig_1.WORKFLOW_TYPE,
remote: pipeline.remote,
isPublic: pipeline.isPublic || false,
steps: runSteps,
env: pipeline.env,
events: pipeline.events,
trigger: pipeline.trigger,
bind: pipeline.bind,
linkedCommandsNames: pipeline.linkedCommandsNames,
};
//convert each pipeline job to a OpCommand
pipelineCommands.push(...pipeline.jobs
.filter(e => !e.linked)
.map(({ name }) => {
let { ops } = (0, utils_1.parseYaml)(fs.readFileSync(`${opPath}/.ops/jobs/${name}/ops.yml`, 'utf8'));
if (ops.length > 0) {
ops[0].isPublic = pipeline.isPublic || false;
ops[0].version = pipeline.version;
}
return ops[0];
}));
return newWorkflow;
});
return { pipelineWorkflows, pipelineCommands };
};
this.choosePublishKind = async (commands, pipelines) => {
if (commands.length !== 0 && pipelines.length !== 0) {
const { commandsOrPipelines } = await cli_sdk_1.ux.prompt({
type: 'list',
name: 'commandsOrPipelines',
message: `\n Which would you like to publish ${cli_sdk_1.ux.colors.reset.green('ā')}`,
choices: [
{ name: 'Commands', value: opConfig_1.COMMAND },
{ name: 'Pipelines', value: opConfig_1.PIPELINE },
],
afterMessage: `${cli_sdk_1.ux.colors.reset.green('ā')}`,
});
return commandsOrPipelines;
}
else if (commands && commands.length) {
return opConfig_1.COMMAND;
}
else if (pipelines && pipelines.length) {
return opConfig_1.PIPELINE;
}
else {
this.debug('found neither valid commands or pipelines');
process.exit(1);
}
};
this.selectOpsAndWorkflows = async (commandsOrPipelines, commands, workflows) => {
switch (commandsOrPipelines) {
case opConfig_1.COMMAND:
const selectedCommands = await this.selectOps(commands);
return { selectedCommands, selectedWorkflows: [] };
case opConfig_1.PIPELINE:
const selectedWorkflows = await this.selectWorkflows(workflows);
return { selectedCommands: [], selectedWorkflows };
default:
this.debug('found neither valid commands or pipelines');
process.exit(1);
}
};
this.selectOps = async (ops) => {
if (ops.length <= 1) {
return ops;
}
const answers = await cli_sdk_1.ux.prompt({
type: 'checkbox',
name: 'ops',
message: `\n Which workflows would you like to publish ${cli_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 cli_sdk_1.ux.prompt({
type: 'checkbox',
name: 'workflows',
message: `\n Which workflows would you like to publish ${cli_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.checkForExistingVersion = async (name, version) => {
try {
await this.services.api.find(`/private/teams/${this.team.name}/ops/${name}/versions/${version}`, {
headers: {
Authorization: this.accessToken,
},
});
return true;
}
catch (err) {
if (err.error[0].code === 404) {
return false;
}
throw new CustomErrors_1.APIError(err);
}
};
this.ensureAvailableVersion = async (opPath, op, options) => {
const versionAlreadyPublished = await this.checkForExistingVersion(op.name, op.version);
// We want to always rebuild the pipeline images
if (!versionAlreadyPublished && op.type != opConfig_1.PIPELINE_TYPE) {
return op;
}
if (versionAlreadyPublished) {
let manifest = await fs.readFile(path.join(opPath, opConfig_1.OP_FILE), 'utf8');
this.log(`${this.ux.colors.callOutCyan(`Current version for ${op.name}:`)} ${this.ux.colors.white(op.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) => {
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';
}
if (await this.checkForExistingVersion(op.name, input)) {
return 'That version is already taken';
}
return true;
},
});
manifest = manifest.replace(`name: ${op.name}:${op.version}`, `name: ${op.name}:${newVersion}`);
await fs.writeFile(path.join(opPath, opConfig_1.OP_FILE), manifest);
op.version = newVersion;
}
switch (op.type) {
case opConfig_1.COMMAND_TYPE:
case opConfig_1.SERVICE_TYPE:
const opImageTag = (0, utils_1.getOpImageTag)(this.team.name, op.name, op.version, op.isPublic);
const image = (0, utils_1.getOpUrl)(env_1.OPS_REGISTRY_HOST, opImageTag);
await this.services.imageService.build(image, path.resolve(process.cwd(), opPath), op, options);
break;
case opConfig_1.PIPELINE_TYPE:
const commands = await this.services.opService.convertOpsToCommands([op], this.state.config, opPath);
await this.services.opService.opsBuildLoop(commands, opPath, this.state.config, options);
break;
}
return op;
};
this.getMaxSemver = async (versions) => {
try {
return versions
.filter(version => {
return semver_1.default.valid(version, true);
})
.map(function (version) {
return {
original: version,
clean: semver_1.default.clean(version),
};
})
.reduce(function (v1, v2) {
return semver_1.default['gt'](v2.clean, v1.clean) ? v2 : v1;
}).original;
}
catch (err) {
this.debug('%O', err);
// return empty semver when no workflow versions exist using semver
return '0.0.0';
}
};
this.bumpUpVersion = async (teamName, opName, version) => {
try {
const { data } = await this.services.api.find(`/private/teams/${teamName}/ops/${opName}/versions`, {
headers: {
Authorization: this.accessToken,
},
});
const versions = data.map(op => {
return op.version;
});
version = await this.getMaxSemver(versions);
const [major, minor, patch] = version.split('.');
const updatedMinor = Number(minor) + 1;
const updatedVersion = `${major}.${updatedMinor}.${patch}`;
return updatedVersion;
}
catch (err) {
this.debug('%O', err);
return '0.1.0';
}
};
this.filterOps = (ops, names) => {
return ops.filter(item => {
for (let op of names) {
if (item.name === op) {
return true;
}
}
});
};
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.promptForChangelog = async (opName, opVersion) => {
const { publishChangelog } = await this.ux.prompt({
type: 'input',
name: 'publishChangelog',
message: `\nProvide a publish changelog for ${opName}:${opVersion} ${cli_sdk_1.ux.colors.reset.green('ā')}\n\n ${cli_sdk_1.ux.colors.white('Changelog:')}`,
afterMessage: cli_sdk_1.ux.colors.reset.green('ā'),
afterMessageAppend: cli_sdk_1.ux.colors.reset(' added!'),
validate: this._validateChangelog,
});
return publishChangelog;
};
this.opsPublishLoop = async (opCommands, version, config, changelog) => {
try {
for (const op of opCommands) {
if (!(0, validate_1.isValidOpName)(op.name)) {
throw new CustomErrors_1.InvalidInputCharacter('Workflow Name');
}
if (!(0, validate_1.isValidOpVersion)(op.version)) {
throw new CustomErrors_1.InvalidOpVersionFormat();
}
if (!changelog) {
changelog = await this.promptForChangelog(op.name, op.version);
}
op.publishDescription = changelog;
const opName = (0, 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);
}
// TODO: What do we do if this isn't true
if ('run' in op) {
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.api);
if (apiOp.events) {
const builder = (apiOp && apiOp.trigger && apiOp.trigger.length > 0) || false;
await this.services.subscriptionService.sendSubscriptions(apiOp.name, apiOp.id, op.type, apiOp.events, config, builder);
}
this.sendAnalytics('op', apiOp, config);
}
}
}
catch (err) {
this.debug('%O', err);
if (err instanceof ErrorTemplate_1.ErrorTemplate) {
throw err;
}
throw new CustomErrors_1.APIError(err);
}
};
// Publish every single Pipeline Command (AKA Pipeline job) image
this.pipelineCommandsPublishLoop = async (pipelineCommands, version, config) => {
for (const op of pipelineCommands) {
if (!(0, validate_1.isValidOpName)(op.name)) {
throw new CustomErrors_1.InvalidInputCharacter('Workflow Name');
}
if (!(0, validate_1.isValidOpVersion)(op.version)) {
throw new CustomErrors_1.InvalidOpVersionFormat();
}
op.publishDescription = strings_1.PIPELINE_PUBLISH_DESCRIPTION + op.name;
try {
const opName = (0, 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);
}
op.type = opConfig_1.JOB_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.api);
this.sendAnalytics('op', apiOp, config);
}
catch (err) {
if (err instanceof ErrorTemplate_1.ErrorTemplate) {
throw err;
}
throw new CustomErrors_1.APIError(err);
}
}
};
this.sleep = async (ms) => {
return new Promise(resolve => setTimeout(resolve, ms));
};
// Publish the entity that groups all the "Pipeline jobs" together
this.workflowsPublishLoop = async (opWorkflows, version, config, changelog) => {
try {
for (const workflow of opWorkflows) {
if (!(0, validate_1.isValidOpName)(workflow.name)) {
throw new CustomErrors_1.InvalidInputCharacter('Workflow Name');
}
if (!(0, validate_1.isValidOpVersion)(workflow.version)) {
throw new CustomErrors_1.InvalidOpVersionFormat();
}
if (!changelog) {
changelog = await this.promptForChangelog(workflow.name, workflow.version);
}
workflow.publishDescription = changelog;
if ('remote' in workflow && workflow.remote) {
for (const step of workflow.steps) {
if (!step.includes('ops run')) {
this.debug('InvalidStepsFound - Step:', step);
throw new CustomErrors_1.InvalidStepsFound(step);
}
}
}
// Added sleep to fix the timing issue
// the api call below fails some times when it takes longer for registry and db insertions on the api side
// TODO: fix the timing issue and remove sleep
await this.sleep(2000);
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š ${cli_sdk_1.ux.colors.callOutCyan(apiWorkflow.name)} has been published!`);
this.log(`š„ View in our registry here: ${cli_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`);
if (workflow.events) {
await this.services.subscriptionService.sendSubscriptions(apiWorkflow.name, apiWorkflow.id, 'pipeline', workflow.events, config, false);
}
this.sendAnalytics('workflow', apiWorkflow, config);
}
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.eventsWarning = (opsToBuild, teamName) => {
for (let i = 0; i < opsToBuild.length; i++) {
if (opsToBuild[i].events) {
const opName = cli_sdk_1.ux.colors.callOutCyan(opsToBuild[i].name);
teamName = cli_sdk_1.ux.colors.callOutCyan(teamName);
this.log(cli_sdk_1.ux.colors.actionBlue(`\nThe selected workflow ${opName} has events in the ops.yml\nbut your active team ${teamName} does not have our Github App Installed.`));
this.log(cli_sdk_1.ux.colors.actionBlue(`Visit ${env_1.WWW_HOST}/home to install our Github App`));
process.exit();
}
}
};
this.sendAnalytics = (publishType, opOrWorkflow, config) => {
this.services.analytics.track('Ops CLI Publish', {
name: opOrWorkflow.name,
team: config.team.name,
namespace: `@${config.team.name}/${opOrWorkflow.name}`,
username: config.user.username,
type: publishType,
description: opOrWorkflow.description,
image: `${env_1.OPS_REGISTRY_HOST}/${opOrWorkflow.id.toLowerCase()}:${opOrWorkflow.version}`,
tag: opOrWorkflow.version,
}, config);
};
this.validateBilling = async (config, op) => {
const billing = await this.services.billingService.validateBilling(config.team.name, op.type, op.name, config.user.email, config.user.username, config.tokens.accessToken);
if (billing.quantity < billing.maxFreeUnits) {
console.log(`${cli_sdk_1.ux.colors.primary('Good news! You have')} ${cli_sdk_1.ux.colors.successGreen(`${billing.freeUnitsAvailable} free`)} ${cli_sdk_1.ux.colors.primary(`${billing.workflowType} left on your team's free plan.`)}`);
}
else if (billing.isNewWorkflow) {
// TODO: make this dynamic later
const prices = {
Services: '$19',
Pipelines: '$14',
Commands: '$7',
};
console.log(`\nš¦ ${cli_sdk_1.ux.colors.primary('You currently have')} ${cli_sdk_1.ux.colors.successGreen(`${billing.quantity} ${billing.workflowType}`)} ${cli_sdk_1.ux.colors.primary('published to')} ${cli_sdk_1.ux.colors.callOutCyan(`${config.team.name}`)}`);
console.log(`š³ ${cli_sdk_1.ux.colors.primary('This will add')} ${cli_sdk_1.ux.colors.green(prices[billing.workflowType])} ${cli_sdk_1.ux.colors.primary('to your monthly team subscription.')} \n`);
const { billingConfirm } = await cli_sdk_1.ux.prompt({
type: 'confirm',
name: 'billingConfirm',
suffix: false,
message: `Are you sure you want to publish @${config.team.name}/${op.name}:${op.version}?`,
});
if (!billingConfirm) {
throw new Error('š« Terminating publish workflow. No further action taken.');
}
}
};
this._selectOption = async (ops, name) => {
if (ops.length <= 1) {
return ops;
}
let anwsers = await cli_sdk_1.ux.prompt({
type: 'checkbox',
name,
message: `\n Which workflows would you like to publish ${cli_sdk_1.ux.colors.reset.green('ā')}`,
choices: ops.map(op => {
return {
value: op,
name: `${op.name} - ${op.description}`,
};
}),
validate: input => input.length > 0,
});
return anwsers.ops;
};
}
_validateChangelog(input) {
if (input === '')
return 'You need to provide a publish changelog of your workflow before continuing';
return true;
}
async run() {
const config = await this.isLoggedIn();
try {
const { flags, args } = this.parse(Publish);
const buildOptions = Object.assign({ nocache: flags.nocache }, Docker_1.PREFER_AMD64);
await this.checkDocker();
const teamInstallationExists = await this.services.subscriptionService.getTeamInstallation(config);
const opPath = this.resolvePath(args.path);
let manifest = await this.getOpsAndWorkflows(opPath);
// If op is a pipeline, check job name(s)
// to ensure no namespece conflict
if (manifest.pipelines) {
// check pipeline name against its job names
for (let pipeline of manifest.pipelines) {
for (let job of pipeline.jobs) {
if (job.name === pipeline.name) {
throw new CustomErrors_1.InvalidPipelineJobNameOpsYml(job.name);
}
}
}
}
manifest.ops = (0, OpsYml_1.checkAndApplyLatestSDKVersion)(manifest.ops);
if (flags.ops) {
const opsToBuild = this.filterOps([...manifest.ops, ...manifest.pipelines, ...manifest.services], flags.ops);
if (!teamInstallationExists) {
this.eventsWarning(opsToBuild, config.team.name);
}
let ops = [];
for (let op of opsToBuild) {
if (await this.checkForExistingVersion(op.name, op.version)) {
const updatedVersion = await this.bumpUpVersion(config.team.name, op.name, op.version);
op.version = updatedVersion;
let manifest = await fs.readFile(path.join(opPath, opConfig_1.OP_FILE), 'utf8');
manifest = manifest.replace(`name: ${op.name}:${op.version}`, `name: ${op.name}:${updatedVersion}`);
if (op.type === opConfig_1.PIPELINE_TYPE) {
// pipelines need to be converted to ops in order to be build
const convertedPipelines = await this.services.opService.convertPipelinesToOps([op], config, opPath);
ops.push(...convertedPipelines);
}
else if (op.type === opConfig_1.SERVICE_TYPE) {
// services need to be converted to ops in order to be build
const convertedServices = (0, OpsYml_1.convertServicesToOps)([
op,
]);
ops.push(...convertedServices);
}
else {
ops.push(op);
}
}
}
await this.services.opService.opsBuildLoop(ops, opPath, config, buildOptions);
const opsToPublish = this.filterOps([...manifest.ops, ...manifest.pipelines, ...manifest.services], flags.ops);
const commandsToPublish = opsToPublish.filter(op => op.type === opConfig_1.COMMAND_TYPE);
const pipelinesToPublish = opsToPublish.filter(op => op.type === opConfig_1.PIPELINE_TYPE);
const servicesToPublish = opsToPublish.filter(op => op.type === opConfig_1.SERVICE_TYPE);
const { pipelineWorkflows, pipelineCommands, } = this.convertPipelinesToWorkflows(pipelinesToPublish, opPath);
const serviceOps = (0, OpsYml_1.convertServicesToOps)(servicesToPublish);
const commands = commandsToPublish.concat(serviceOps);
await this.opsPublishLoop(commands, manifest.version, config, flags.changelog);
await this.pipelineCommandsPublishLoop(pipelineCommands, manifest.version, config);
await this.workflowsPublishLoop(pipelineWorkflows, manifest.version, config, flags.changelog);
}
else {
const serviceOps = (0, OpsYml_1.convertServicesToOps)(manifest.services);
const allOps = [...serviceOps, ...manifest.pipelines, ...manifest.ops];
const ops = await this._selectOption(allOps, 'ops');
for (let op of ops) {
const versionedOp = await this.ensureAvailableVersion(opPath, op, buildOptions);
if (!teamInstallationExists) {
this.eventsWarning([versionedOp], config.team.name);
}
switch (op.type) {
case opConfig_1.SERVICE_TYPE:
case opConfig_1.COMMAND:
// await this.validateBilling(config, finalCommands[0])
await this.opsPublishLoop([versionedOp], manifest.version, config, flags.changelog);
break;
case opConfig_1.PIPELINE:
const {
// Parent Pipeline
pipelineWorkflows,
// Pipeline Job
pipelineCommands, } = this.convertPipelinesToWorkflows([versionedOp], opPath);
// Publish Pipeline Jobs
await this.pipelineCommandsPublishLoop(pipelineCommands, manifest.version, config);
// Publish Parnet Pipeline
await this.workflowsPublishLoop(pipelineWorkflows, manifest.version, config, flags.changelog);
break;
default:
this.debug('found neither valid commands or pipelines');
process.exit(1);
}
}
}
// await this.services.billingService.updateBilling(
// config.team.name,
// config.user.username,
// config.user.email,
// config.tokens.accessToken,
// )
}
catch (err) {
this.debug('%O', err);
this.config.runHook('error', {
err,
accessToken: config.tokens.accessToken,
});
}
}
}
exports.default = Publish;
Publish.description = 'Publish a workflow to your team.';
Publish.flags = {
help: base_1.flags.help({ char: 'h' }),
ops: base_1.flags.string({
char: 'o',
multiple: true,
description: 'Provide the list of workflows that you want to publish.',
helpValue: 'workflows',
}),
changelog: base_1.flags.string({
char: 'c',
description: 'Provide a publish changelog',
default: '',
}),
nocache: base_1.flags.boolean({
default: false,
description: 'Do not use cache when building the image',
}),
};
Publish.args = [
{
name: 'path',
description: 'Path to the workflow you want to publish.',
required: true,
},
];