@swell/cli
Version:
Swell's command line interface/utility
208 lines (204 loc) • 7.75 kB
JavaScript
import { confirm, input } from '@inquirer/prompts';
import git from '@npmcli/git';
import { Args, Flags } from '@oclif/core';
import { CLIError } from '@oclif/errors';
import ora from 'ora';
import * as semver from 'semver';
import style from '../../lib/style.js';
import { PushAppCommand } from '../../push-app-command.js';
const RELEASE_TYPES = [
'major',
'premajor',
'minor',
'preminor',
'patch',
'prepatch',
'prerelease',
];
export default class AppVersion extends PushAppCommand {
static args = {
nextVersion: Args.string({
description: `semver version or release type: ${[
'<next.version>',
...RELEASE_TYPES,
].join('|')}`,
async parse(input) {
if (RELEASE_TYPES.includes(input)) {
return input;
}
const parsedInput = semver.valid(input);
if (parsedInput) {
return parsedInput;
}
throw new CLIError(`Invalid version: ${input}`);
},
}),
};
static description = `When executed without specifying NEXTVERSION, the command will display the current
version of your app.
If a NEXTVERSION is provided, the command will validate and compute the version
according to Semver rules by only allowing incremental updates.
The version change will be committed to the Git repository using the provided
message or a default similar to the tag, e.g. "v1.0.1".
By default, it creates a Git tag for the specified version. You can disable this
behavior by using the --no-git-tag option.`;
static examples = [
'swell app version',
'swell app version minor',
'swell app version 1.0.0 -m "First release"',
'swell app version patch -y',
];
static flags = {
amend: Flags.boolean({
description: 'amend the description an existing version',
}),
'force-create-files': Flags.boolean({
description: 'do not use files from previous versions',
}),
message: Flags.string({
char: 'm',
description: 'description of the changes in this version for internal use',
}),
'no-git-tag': Flags.boolean({
description: 'do not create a git tag for this version',
}),
yes: Flags.boolean({
char: 'y',
description: 'skip confirmation prompt',
}),
};
static orientation = {
env: 'test',
};
static summary = 'Increment the version of your Swell app.';
async run() {
const { args, flags } = await this.parse(AppVersion);
const { amend, 'no-git-tag': noGitTag, yes, 'force-create-files': forceCreateFiles = false, } = flags;
let { message } = flags;
let { nextVersion } = args;
if (!(await this.ensureAppExists(undefined, false))) {
return;
}
const localVersion = this.app.version;
if (amend) {
const amendVersion = nextVersion || localVersion;
const appVersion = await this.getVersion(amendVersion);
if (appVersion) {
await this.updateVersion(amendVersion, {
description: message,
});
}
else {
this.error(`Version ${amendVersion} does not exist yet.`);
}
return;
}
const latestVersion = await this.getVersion();
const remoteVersion = latestVersion?.version;
// Suggest the next version if not provided
if (!nextVersion &&
remoteVersion &&
semver.gt(localVersion, remoteVersion)) {
nextVersion = localVersion;
}
// If the user didn't specify a version, show the latest one
if (!nextVersion) {
if (remoteVersion) {
this.log(`${style.appConfigValue(this.app.name)} latest remote version is ${style.appConfigValue(remoteVersion)}.`);
}
else {
nextVersion = localVersion;
if (!nextVersion) {
this.error(`${style.appConfigValue(this.app.name)} has not been versioned yet.`);
}
}
return;
}
nextVersion = this.validateAndComputeVersion(nextVersion, localVersion);
await this.pushAppConfigs();
await this.deployAppFrontend(false, false);
if (!message) {
message = await input({
message: `Describe the changes in version ${nextVersion}:`,
});
}
if (!yes) {
const continueVersion = await confirm({
message: `Create ${style.appConfigValue(this.app.name)} version ${nextVersion}?`,
});
if (!continueVersion) {
return;
}
}
if (!message) {
message = nextVersion;
}
this.log();
const spinner = ora();
spinner.start('Creating version');
try {
await this.createRemoteVersion(nextVersion, message, spinner, forceCreateFiles);
await this.gitCommit(message);
if (!noGitTag)
await this.gitTag(nextVersion);
}
catch (error) {
spinner.fail(error.stderr || error.message);
return;
}
spinner.succeed(`${style.appConfigValue(this.app.name)} version ${nextVersion} created.`);
}
async createRemoteVersion(nextVersion, message, spinner, forceCreateFiles = false) {
const body = {
description: message,
version: nextVersion,
};
if (forceCreateFiles) {
body.$force_create_files = true;
}
// update the version on the remote
const versionResponse = await this.api.post({ adminPath: `/apps/${this.app.id}/versions` }, {
body,
onAsyncGetPath: (response) => ({
adminPath: `/apps/${this.app.id}/versions/${response.id}`,
}),
spinner,
});
if (!versionResponse || !versionResponse.version) {
spinner.fail('Something went wrong.');
}
// update the local app config file since remote was successful
this.swellConfig.set('version', nextVersion);
}
async gitCommit(message) {
// check if there are changes in the app config file
// if there are no changes, there is nothing to commit
const configStatus = await git.spawn(['status', this.swellConfig.path], {
cwd: this.swellConfig.cwd,
});
if (configStatus.stdout.includes('nothing to commit')) {
return;
}
// add the app config file to commit - this will contain the new version
await git.spawn(['add', this.swellConfig.path]);
// commit the version change to git. It skips the git hooks with -n
await git.spawn(['commit', '-n', '-m', message]);
}
async gitTag(nextVersion) {
const tag = nextVersion;
// tag the version in git
await git.spawn(['tag', tag], { cwd: process.cwd() });
}
validateAndComputeVersion(nextVersion, localVersion) {
let version = nextVersion;
// compute the value of the next version and check if it's valid
if (RELEASE_TYPES.includes(nextVersion)) {
// increment the version
version = semver.inc(localVersion, version);
}
if (semver.lte(version, localVersion)) {
this.error(`Version ${version} must be greater than the current version ${localVersion}.`);
}
return version;
}
}