mirror of
https://github.com/strapi/strapi.git
synced 2025-12-16 09:45:08 +00:00
Add <latest> Upgrade Command (#21754)
This commit is contained in:
parent
f52f276833
commit
e8787bcc0d
@ -2,6 +2,7 @@ import prompts from 'prompts';
|
|||||||
import { InvalidArgumentError, Option } from 'commander';
|
import { InvalidArgumentError, Option } from 'commander';
|
||||||
|
|
||||||
import type { Command } from 'commander';
|
import type { Command } from 'commander';
|
||||||
|
|
||||||
import { loggerFactory } from '../../modules/logger';
|
import { loggerFactory } from '../../modules/logger';
|
||||||
import { Version, isLiteralSemVer, isValidSemVer, semVerFactory } from '../../modules/version';
|
import { Version, isLiteralSemVer, isValidSemVer, semVerFactory } from '../../modules/version';
|
||||||
import { handleError } from '../errors';
|
import { handleError } from '../errors';
|
||||||
@ -71,6 +72,12 @@ export const register = (program: Command) => {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// upgrade latest
|
||||||
|
addReleaseUpgradeCommand(
|
||||||
|
Version.ReleaseType.Latest,
|
||||||
|
'Upgrade to the latest available version of Strapi'
|
||||||
|
);
|
||||||
|
|
||||||
// upgrade major
|
// upgrade major
|
||||||
addReleaseUpgradeCommand(
|
addReleaseUpgradeCommand(
|
||||||
Version.ReleaseType.Major,
|
Version.ReleaseType.Major,
|
||||||
|
|||||||
@ -1,6 +1,13 @@
|
|||||||
import chalk from 'chalk';
|
import chalk from 'chalk';
|
||||||
|
|
||||||
|
import { AbortedError } from '../modules/error';
|
||||||
|
|
||||||
export const handleError = (err: unknown, isSilent: boolean) => {
|
export const handleError = (err: unknown, isSilent: boolean) => {
|
||||||
|
// If the upgrade process has been aborted, exit silently
|
||||||
|
if (err instanceof AbortedError) {
|
||||||
|
process.exit(0);
|
||||||
|
}
|
||||||
|
|
||||||
if (!isSilent) {
|
if (!isSilent) {
|
||||||
console.error(
|
console.error(
|
||||||
chalk.red(`[ERROR]\t[${new Date().toISOString()}]`),
|
chalk.red(`[ERROR]\t[${new Date().toISOString()}]`),
|
||||||
|
|||||||
@ -19,6 +19,12 @@ export class NPMCandidateNotFoundError extends Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export class AbortedError extends Error {
|
||||||
|
constructor(message: string = 'Upgrade aborted') {
|
||||||
|
super(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export const unknownToError = (e: unknown): Error => {
|
export const unknownToError = (e: unknown): Error => {
|
||||||
if (e instanceof Error) {
|
if (e instanceof Error) {
|
||||||
return e;
|
return e;
|
||||||
|
|||||||
@ -1,9 +1,15 @@
|
|||||||
|
import type { NPM } from '../npm';
|
||||||
|
import type { AppProject } from '../project';
|
||||||
import type { Version } from '../version';
|
import type { Version } from '../version';
|
||||||
import type { Requirement } from '../requirement';
|
import type { Requirement } from '../requirement';
|
||||||
import type { Logger } from '../logger';
|
import type { Logger } from '../logger';
|
||||||
import type { ConfirmationCallback } from '../common/types';
|
import type { ConfirmationCallback } from '../common/types';
|
||||||
|
|
||||||
export interface Upgrader {
|
export interface Upgrader {
|
||||||
|
getNPMPackage(): NPM.Package;
|
||||||
|
getProject(): AppProject;
|
||||||
|
getTarget(): Version.SemVer;
|
||||||
|
|
||||||
setTarget(target: Version.SemVer): this;
|
setTarget(target: Version.SemVer): this;
|
||||||
setRequirements(requirements: Requirement.Requirement[]): this;
|
setRequirements(requirements: Requirement.Requirement[]): this;
|
||||||
setLogger(logger: Logger): this;
|
setLogger(logger: Logger): this;
|
||||||
@ -14,6 +20,8 @@ export interface Upgrader {
|
|||||||
dry(enabled?: boolean): this;
|
dry(enabled?: boolean): this;
|
||||||
onConfirm(callback: ConfirmationCallback | null): this;
|
onConfirm(callback: ConfirmationCallback | null): this;
|
||||||
|
|
||||||
|
confirm(message: string): Promise<boolean>;
|
||||||
|
|
||||||
addRequirement(requirement: Requirement.Requirement): this;
|
addRequirement(requirement: Requirement.Requirement): this;
|
||||||
|
|
||||||
upgrade(): Promise<UpgradeReport>;
|
upgrade(): Promise<UpgradeReport>;
|
||||||
|
|||||||
@ -57,6 +57,18 @@ export class Upgrader implements UpgraderInterface {
|
|||||||
this.confirmationCallback = null;
|
this.confirmationCallback = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getNPMPackage(): NPM.Package {
|
||||||
|
return this.npmPackage;
|
||||||
|
}
|
||||||
|
|
||||||
|
getProject(): AppProject {
|
||||||
|
return this.project;
|
||||||
|
}
|
||||||
|
|
||||||
|
getTarget(): Version.SemVer {
|
||||||
|
return semVerFactory(this.target.raw);
|
||||||
|
}
|
||||||
|
|
||||||
setRequirements(requirements: Requirement.Requirement[]) {
|
setRequirements(requirements: Requirement.Requirement[]) {
|
||||||
this.requirements = requirements;
|
this.requirements = requirements;
|
||||||
return this;
|
return this;
|
||||||
@ -172,6 +184,14 @@ export class Upgrader implements UpgraderInterface {
|
|||||||
return successReport();
|
return successReport();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async confirm(message: string): Promise<boolean> {
|
||||||
|
if (typeof this.confirmationCallback !== 'function') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.confirmationCallback(message);
|
||||||
|
}
|
||||||
|
|
||||||
private async checkRequirements(
|
private async checkRequirements(
|
||||||
requirements: Requirement.Requirement[],
|
requirements: Requirement.Requirement[],
|
||||||
context: Requirement.TestContext
|
context: Requirement.TestContext
|
||||||
@ -292,10 +312,13 @@ export class Upgrader implements UpgraderInterface {
|
|||||||
|
|
||||||
private async runCodemods(range: Version.Range): Promise<void> {
|
private async runCodemods(range: Version.Range): Promise<void> {
|
||||||
const codemodRunner = codemodRunnerFactory(this.project, range);
|
const codemodRunner = codemodRunnerFactory(this.project, range);
|
||||||
|
|
||||||
codemodRunner.dry(this.isDry);
|
codemodRunner.dry(this.isDry);
|
||||||
|
|
||||||
if (this.logger) {
|
if (this.logger) {
|
||||||
codemodRunner.setLogger(this.logger);
|
codemodRunner.setLogger(this.logger);
|
||||||
}
|
}
|
||||||
|
|
||||||
await codemodRunner.run();
|
await codemodRunner.run();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -22,6 +22,30 @@ describe('Version Utilities', () => {
|
|||||||
expect(range.raw).toBe('>1.0.0 <=2');
|
expect(range.raw).toBe('>1.0.0 <=2');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should create a range for Minor release type', () => {
|
||||||
|
const currentVersion = new semver.SemVer('1.0.0');
|
||||||
|
|
||||||
|
const range = rangeFromReleaseType(currentVersion, Version.ReleaseType.Minor);
|
||||||
|
expect(range).toBeInstanceOf(semver.Range);
|
||||||
|
expect(range.raw).toBe('>1.0.0 <2.0.0');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a range for Patch release type', () => {
|
||||||
|
const currentVersion = new semver.SemVer('1.0.0');
|
||||||
|
|
||||||
|
const range = rangeFromReleaseType(currentVersion, Version.ReleaseType.Patch);
|
||||||
|
expect(range).toBeInstanceOf(semver.Range);
|
||||||
|
expect(range.raw).toBe('>1.0.0 <1.1.0');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create a range for Latest release type', () => {
|
||||||
|
const currentVersion = new semver.SemVer('1.0.0');
|
||||||
|
|
||||||
|
const range = rangeFromReleaseType(currentVersion, Version.ReleaseType.Latest);
|
||||||
|
expect(range).toBeInstanceOf(semver.Range);
|
||||||
|
expect(range.raw).toBe('>1.0.0');
|
||||||
|
});
|
||||||
|
|
||||||
it('should throw for unsupported release types', () => {
|
it('should throw for unsupported release types', () => {
|
||||||
const currentVersion = new semver.SemVer('1.0.0');
|
const currentVersion = new semver.SemVer('1.0.0');
|
||||||
expect(() =>
|
expect(() =>
|
||||||
|
|||||||
@ -43,6 +43,7 @@ describe('Version Utilities', () => {
|
|||||||
expect(isSemVerReleaseType(Version.ReleaseType.Major)).toBe(true);
|
expect(isSemVerReleaseType(Version.ReleaseType.Major)).toBe(true);
|
||||||
expect(isSemVerReleaseType(Version.ReleaseType.Minor)).toBe(true);
|
expect(isSemVerReleaseType(Version.ReleaseType.Minor)).toBe(true);
|
||||||
expect(isSemVerReleaseType(Version.ReleaseType.Patch)).toBe(true);
|
expect(isSemVerReleaseType(Version.ReleaseType.Patch)).toBe(true);
|
||||||
|
expect(isSemVerReleaseType(Version.ReleaseType.Latest)).toBe(true);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return false for invalid release types', () => {
|
it('should return false for invalid release types', () => {
|
||||||
|
|||||||
@ -9,12 +9,16 @@ export const rangeFactory = (range: string): Version.Range => {
|
|||||||
|
|
||||||
export const rangeFromReleaseType = (current: Version.SemVer, identifier: Version.ReleaseType) => {
|
export const rangeFromReleaseType = (current: Version.SemVer, identifier: Version.ReleaseType) => {
|
||||||
switch (identifier) {
|
switch (identifier) {
|
||||||
|
case Version.ReleaseType.Latest: {
|
||||||
|
// Match anything greater than the current version
|
||||||
|
return rangeFactory(`>${current.raw}`);
|
||||||
|
}
|
||||||
case Version.ReleaseType.Major: {
|
case Version.ReleaseType.Major: {
|
||||||
// For example, 4.15.4 returns 5.0.0
|
// For example, 4.15.4 returns 5.0.0
|
||||||
const nextMajor = semVerFactory(current.raw).inc('major');
|
const nextMajor = semVerFactory(current.raw).inc('major');
|
||||||
|
|
||||||
// Using only the major version as upper limit allows any minor,
|
// Using only the major version as the upper limit allows any minor,
|
||||||
// patch or build version to be taken in the range.
|
// patch, or build version to be taken in the range.
|
||||||
//
|
//
|
||||||
// For example, if the current version is "4.15.4", incrementing the
|
// For example, if the current version is "4.15.4", incrementing the
|
||||||
// major version would result in "5.0.0".
|
// major version would result in "5.0.0".
|
||||||
|
|||||||
@ -10,7 +10,10 @@ export type LiteralSemVer = `${Version}.${Version}.${Version}`;
|
|||||||
export type { SemVer, Range } from 'semver';
|
export type { SemVer, Range } from 'semver';
|
||||||
|
|
||||||
export enum ReleaseType {
|
export enum ReleaseType {
|
||||||
|
// Classic
|
||||||
Major = 'major',
|
Major = 'major',
|
||||||
Minor = 'minor',
|
Minor = 'minor',
|
||||||
Patch = 'patch',
|
Patch = 'patch',
|
||||||
|
// Other
|
||||||
|
Latest = 'latest',
|
||||||
}
|
}
|
||||||
|
|||||||
@ -30,7 +30,7 @@ describe('codemods task', () => {
|
|||||||
target: Version.ReleaseType.Major,
|
target: Version.ReleaseType.Major,
|
||||||
logger,
|
logger,
|
||||||
dry: false,
|
dry: false,
|
||||||
selectCodemods: (options) => Promise.resolve(options),
|
selectCodemods: <T>(options: T) => Promise.resolve(options),
|
||||||
};
|
};
|
||||||
|
|
||||||
(codemodRunnerFactory as jest.Mock).mockReturnValue({
|
(codemodRunnerFactory as jest.Mock).mockReturnValue({
|
||||||
|
|||||||
@ -18,6 +18,8 @@ export const getRangeFromTarget = (
|
|||||||
const { major, minor, patch } = currentVersion;
|
const { major, minor, patch } = currentVersion;
|
||||||
|
|
||||||
switch (target) {
|
switch (target) {
|
||||||
|
case Version.ReleaseType.Latest:
|
||||||
|
throw new Error("Can't use <latest> to create a codemods range: not implemented");
|
||||||
case Version.ReleaseType.Major:
|
case Version.ReleaseType.Major:
|
||||||
return rangeFactory(`${major}`);
|
return rangeFactory(`${major}`);
|
||||||
case Version.ReleaseType.Minor:
|
case Version.ReleaseType.Minor:
|
||||||
|
|||||||
@ -0,0 +1 @@
|
|||||||
|
export * from './latest';
|
||||||
62
packages/utils/upgrade/src/tasks/upgrade/prompts/latest.ts
Normal file
62
packages/utils/upgrade/src/tasks/upgrade/prompts/latest.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import { AbortedError } from '../../../modules/error';
|
||||||
|
import * as f from '../../../modules/format';
|
||||||
|
|
||||||
|
import { rangeFactory, semVerFactory, Version } from '../../../modules/version';
|
||||||
|
|
||||||
|
import type { Upgrader } from '../../../modules/upgrader';
|
||||||
|
import type { UpgradeOptions } from '../types';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the upgrade prompts when using the latest tag.
|
||||||
|
*
|
||||||
|
* - checks if an upgrade involves a major bump, warning and asking for user confirmation before proceeding
|
||||||
|
*/
|
||||||
|
export const latest = async (upgrader: Upgrader, options: UpgradeOptions) => {
|
||||||
|
// Exit if the upgrade target isn't the latest tag
|
||||||
|
if (options.target !== Version.ReleaseType.Latest) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Retrieve utilities from the upgrader instance
|
||||||
|
const npmPackage = upgrader.getNPMPackage();
|
||||||
|
const target = upgrader.getTarget();
|
||||||
|
const project = upgrader.getProject();
|
||||||
|
|
||||||
|
const { strapiVersion: current } = project;
|
||||||
|
|
||||||
|
// Pre-formatted strings used in logs
|
||||||
|
const fTargetMajor = f.highlight(`v${target.major}`);
|
||||||
|
const fCurrentMajor = f.highlight(`v${current.major}`);
|
||||||
|
|
||||||
|
const fTarget = f.version(target);
|
||||||
|
const fCurrent = f.version(current);
|
||||||
|
|
||||||
|
// Flags
|
||||||
|
const isMajorUpgrade = target.major > current.major;
|
||||||
|
|
||||||
|
// Handle potential major upgrade, warns, and asks for confirmation to proceed
|
||||||
|
if (isMajorUpgrade) {
|
||||||
|
options.logger.warn(
|
||||||
|
`Detected a major upgrade for the "${f.highlight(Version.ReleaseType.Latest)}" tag: ${fCurrent} > ${fTarget}`
|
||||||
|
);
|
||||||
|
|
||||||
|
// Find the latest release in between the current one and the next major
|
||||||
|
const newerPackageRelease = npmPackage
|
||||||
|
.findVersionsInRange(rangeFactory(`>${current.raw} <${target.major}`))
|
||||||
|
.at(-1);
|
||||||
|
|
||||||
|
// If the project isn't on the latest version for the current major, emit a warning
|
||||||
|
if (newerPackageRelease) {
|
||||||
|
const fLatest = f.version(semVerFactory(newerPackageRelease.version));
|
||||||
|
options.logger.warn(
|
||||||
|
`It's recommended to first upgrade to the latest version of ${fCurrentMajor} (${fLatest}) before upgrading to ${fTargetMajor}.`
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
const proceedAnyway = await upgrader.confirm(`I know what I'm doing. Proceed anyway!`);
|
||||||
|
|
||||||
|
if (!proceedAnyway) {
|
||||||
|
throw new AbortedError();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@ -8,14 +8,16 @@ import { constants as upgraderConstants, upgraderFactory } from '../../modules/u
|
|||||||
import { Version } from '../../modules/version';
|
import { Version } from '../../modules/version';
|
||||||
|
|
||||||
import * as requirements from './requirements';
|
import * as requirements from './requirements';
|
||||||
|
import * as prompts from './prompts';
|
||||||
|
|
||||||
import type { UpgradeOptions } from './types';
|
import type { UpgradeOptions } from './types';
|
||||||
|
import type { Upgrader } from '../../modules/upgrader';
|
||||||
|
|
||||||
export const upgrade = async (options: UpgradeOptions) => {
|
export const upgrade = async (options: UpgradeOptions) => {
|
||||||
const timer = timerFactory();
|
const timer = timerFactory();
|
||||||
const { logger, codemodsTarget } = options;
|
const { logger, codemodsTarget } = options;
|
||||||
|
|
||||||
// Make sure we're resolving the correct working directory based on the given input
|
// Resolves the correct working directory based on the given input
|
||||||
const cwd = path.resolve(options.cwd ?? process.cwd());
|
const cwd = path.resolve(options.cwd ?? process.cwd());
|
||||||
|
|
||||||
const project = projectFactory(cwd);
|
const project = projectFactory(cwd);
|
||||||
@ -49,17 +51,11 @@ export const upgrade = async (options: UpgradeOptions) => {
|
|||||||
upgrader.overrideCodemodsTarget(codemodsTarget);
|
upgrader.overrideCodemodsTarget(codemodsTarget);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't add the same requirements when manually targeting a major upgrade
|
// Prompt user for confirmation details before upgrading
|
||||||
// using a semver as it's implied that the users know what they're doing
|
await runUpgradePrompts(upgrader, options);
|
||||||
if (options.target === Version.ReleaseType.Major) {
|
|
||||||
upgrader
|
|
||||||
.addRequirement(requirements.major.REQUIRE_AVAILABLE_NEXT_MAJOR)
|
|
||||||
.addRequirement(requirements.major.REQUIRE_LATEST_FOR_CURRENT_MAJOR);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Make sure the git repository is in an optimal state before running the upgrade
|
// Add specific requirements before upgrading
|
||||||
// Mainly used to ease rollbacks in case the upgrade is corrupted
|
addUpgradeRequirements(upgrader, options);
|
||||||
upgrader.addRequirement(requirements.common.REQUIRE_GIT.asOptional());
|
|
||||||
|
|
||||||
// Actually run the upgrade process once configured,
|
// Actually run the upgrade process once configured,
|
||||||
// The response contains information about the final status: success/error
|
// The response contains information about the final status: success/error
|
||||||
@ -73,3 +69,23 @@ export const upgrade = async (options: UpgradeOptions) => {
|
|||||||
|
|
||||||
logger.info(`Completed in ${f.durationMs(timer.elapsedMs)}ms`);
|
logger.info(`Completed in ${f.durationMs(timer.elapsedMs)}ms`);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const runUpgradePrompts = async (upgrader: Upgrader, options: UpgradeOptions) => {
|
||||||
|
if (options.target === Version.ReleaseType.Latest) {
|
||||||
|
await prompts.latest(upgrader, options);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addUpgradeRequirements = (upgrader: Upgrader, options: UpgradeOptions): void => {
|
||||||
|
// Don't add the same requirements when manually targeting a major upgrade
|
||||||
|
// using a semver as it's implied that the users know what they're doing
|
||||||
|
if (options.target === Version.ReleaseType.Major) {
|
||||||
|
upgrader
|
||||||
|
.addRequirement(requirements.major.REQUIRE_AVAILABLE_NEXT_MAJOR)
|
||||||
|
.addRequirement(requirements.major.REQUIRE_LATEST_FOR_CURRENT_MAJOR);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the git repository is in an optimal state before running the upgrade
|
||||||
|
// Mainly used to ease rollbacks in case the upgrade is corrupted
|
||||||
|
upgrader.addRequirement(requirements.common.REQUIRE_GIT.asOptional());
|
||||||
|
};
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user