From f3c018663e41cf56f9521cf9a64d1676d5ee7dfd Mon Sep 17 00:00:00 2001 From: Dominic Griesel Date: Fri, 11 Mar 2022 22:36:49 +0100 Subject: [PATCH] feat: implement rollback on failure --- packages/core/package.json | 1 + packages/core/src/index.ts | 3 ++- packages/core/src/lib/context.ts | 4 ++++ packages/core/src/lib/planner.test.ts | 1 + packages/core/src/lib/planner.ts | 13 +++++++++++++ packages/core/src/lib/plugin.ts | 3 +++ packages/core/src/lib/shared.ts | 18 ++++++++++++++++++ packages/plugin-changelog/src/index.ts | 18 ++++++++++++++++++ packages/plugin-iobroker/src/index.ts | 19 +++++++++++++++++-- packages/plugin-package/src/index.ts | 15 +++++++++++++-- packages/release-script/src/index.ts | 4 ++++ packages/testing/src/lib/context.ts | 1 + yarn.lock | 1 + 13 files changed, 96 insertions(+), 5 deletions(-) diff --git a/packages/core/package.json b/packages/core/package.json index a2a4c0d..56c85bd 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -37,6 +37,7 @@ "node": ">=12.20" }, "dependencies": { + "alcalzone-shared": "^4.0.1", "execa": "^5.1.1" }, "devDependencies": { diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index ad32de5..c45d31d 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -1,6 +1,7 @@ export { stripColors } from "./lib/cli"; export * from "./lib/error"; export * from "./lib/exec"; -export { execute, resolvePlugins } from "./lib/planner"; +export { execute, resolvePlugins, rollback } from "./lib/planner"; +export { cloneDeep } from "./lib/shared"; export { DefaultStages } from "./lib/stage"; export * from "./types"; diff --git a/packages/core/src/lib/context.ts b/packages/core/src/lib/context.ts index 5205d96..569e9ec 100644 --- a/packages/core/src/lib/context.ts +++ b/packages/core/src/lib/context.ts @@ -1,5 +1,6 @@ import type { CLI } from "./cli"; import type { Plugin } from "./plugin"; +import type { Stage } from "./stage"; import type { System } from "./system"; export interface Context { @@ -43,4 +44,7 @@ export interface Context { getData(key: string): T; hasData(key: string): boolean; setData(key: string, value: any): void; + + /** Keeps track of which stages have been fully or partially executed */ + executedStages: Stage[]; } diff --git a/packages/core/src/lib/planner.test.ts b/packages/core/src/lib/planner.test.ts index d273912..e7cb0d7 100644 --- a/packages/core/src/lib/planner.test.ts +++ b/packages/core/src/lib/planner.test.ts @@ -347,6 +347,7 @@ describe("execute", () => { }, argv: {}, errors: [], + executedStages: [], } as unknown as Context; await execute(context); diff --git a/packages/core/src/lib/planner.ts b/packages/core/src/lib/planner.ts index f2f5063..02cc831 100644 --- a/packages/core/src/lib/planner.ts +++ b/packages/core/src/lib/planner.ts @@ -167,6 +167,8 @@ export async function execute(context: Context): Promise { } for (const stage of stages) { context.cli.prefix = `${stage.id}`; + context.executedStages.push(stage); + const plugins = await planStage(context, stage); if (context.argv.verbose) { context.cli.log( @@ -200,3 +202,14 @@ export async function execute(context: Context): Promise { if (!isTest) console.log(); } } + +/** Undo all the changes that have already been made to the file system */ +export async function rollback(context: Context): Promise { + for (const plugin of context.plugins) { + try { + await plugin.rollback?.(context); + } catch { + // ignore + } + } +} diff --git a/packages/core/src/lib/plugin.ts b/packages/core/src/lib/plugin.ts index bd7eed9..fba2481 100644 --- a/packages/core/src/lib/plugin.ts +++ b/packages/core/src/lib/plugin.ts @@ -35,4 +35,7 @@ export interface Plugin { /** Execute the plugin for the given stage */ executeStage(context: Context, stage: Stage): Promise; + + /** Reset the previous state in case of an execution error */ + rollback?: (context: Context) => Promise | void; } diff --git a/packages/core/src/lib/shared.ts b/packages/core/src/lib/shared.ts index 306fa2b..970d5e3 100644 --- a/packages/core/src/lib/shared.ts +++ b/packages/core/src/lib/shared.ts @@ -1,3 +1,21 @@ +import { isArray, isObject } from "alcalzone-shared/typeguards"; import type { Context } from "./context"; export type ConstOrDynamic = T | ((context: Context) => T | Promise); + +/** + * Creates a deep copy of the given object + */ +export function cloneDeep(source: T): T { + if (isArray(source)) { + return source.map((i) => cloneDeep(i)) as any; + } else if (isObject(source)) { + const target: any = {}; + for (const [key, value] of Object.entries(source)) { + target[key] = cloneDeep(value); + } + return target; + } else { + return source; + } +} diff --git a/packages/plugin-changelog/src/index.ts b/packages/plugin-changelog/src/index.ts index aa7b277..30cea4a 100644 --- a/packages/plugin-changelog/src/index.ts +++ b/packages/plugin-changelog/src/index.ts @@ -157,10 +157,12 @@ class ChangelogPlugin implements Plugin { let parsedOld: typeof parsed | undefined; if (changelogOld) { parsedOld = parseChangelogFile(changelogOld, changelogPlaceholderPrefix.substr(1)); + context.setData("changelog_old_raw", changelogOld); } const entries = [...parsed.entries, ...(parsedOld?.entries ?? [])]; + context.setData("changelog_raw", changelog); context.setData("changelog_filename", changelogFilename); context.setData("changelog_before", parsed.before); context.setData("changelog_after", parsed.after); @@ -314,6 +316,22 @@ ${fileContent.slice(changelogBefore.length)}`; // The part after the new placeho } } } + + async rollback(context: Context): Promise { + if (!context.executedStages.some((s) => s.id === "edit")) { + // Nothing has been changed yet + return; + } + + const changelog_raw = context.getData("changelog_raw"); + const changelogFilename = context.getData("changelog_filename"); + await fs.writeFile(path.join(context.cwd, changelogFilename), changelog_raw); + + if (context.hasData("changelog_old_raw")) { + const changelog_old_raw = context.getData("changelog_old_raw"); + await fs.writeFile(path.join(context.cwd, "CHANGELOG_OLD.md"), changelog_old_raw); + } + } } export default ChangelogPlugin; diff --git a/packages/plugin-iobroker/src/index.ts b/packages/plugin-iobroker/src/index.ts index ce07a86..8e7c453 100644 --- a/packages/plugin-iobroker/src/index.ts +++ b/packages/plugin-iobroker/src/index.ts @@ -1,4 +1,4 @@ -import { DefaultStages } from "@alcalzone/release-script-core"; +import { cloneDeep, DefaultStages } from "@alcalzone/release-script-core"; import type { Context, Plugin, Stage } from "@alcalzone/release-script-core/types"; import { isObject } from "alcalzone-shared/typeguards"; import fs from "fs-extra"; @@ -127,7 +127,7 @@ You can suppress this check with the ${colors.bold("--no-workflow-check")} flag. private async executeEditStage(context: Context): Promise { const newVersion = context.getData("version_new"); - const ioPack = context.getData("io-package.json"); + const ioPack = cloneDeep(context.getData("io-package.json")); if (context.argv.dryRun) { context.cli.log( @@ -205,6 +205,21 @@ You can suppress this check with the ${colors.bold("--no-workflow-check")} flag. await this.executeEditStage(context); } } + + async rollback(context: Context): Promise { + if (!context.executedStages.some((s) => s.id === "edit")) { + // Nothing has been changed yet + return; + } + + const ioPack = context.getData>("io-package.json"); + let ioPackDirectory = context.cwd; + if (context.argv.ioPackage) { + ioPackDirectory = path.join(ioPackDirectory, context.argv.ioPackage as string); + } + const ioPackPath = path.join(ioPackDirectory, "io-package.json"); + await fs.writeJson(ioPackPath, ioPack, { spaces: 2 }); + } } export default IoBrokerPlugin; diff --git a/packages/plugin-package/src/index.ts b/packages/plugin-package/src/index.ts index c2b17ff..11a853e 100644 --- a/packages/plugin-package/src/index.ts +++ b/packages/plugin-package/src/index.ts @@ -1,5 +1,5 @@ import { detectPackageManager } from "@alcalzone/pak"; -import { DefaultStages } from "@alcalzone/release-script-core"; +import { cloneDeep, DefaultStages } from "@alcalzone/release-script-core"; import type { Context, Plugin, Stage } from "@alcalzone/release-script-core/types"; import { isArray, isObject } from "alcalzone-shared/typeguards"; import fs from "fs-extra"; @@ -197,7 +197,7 @@ Alternatively, you can use ${context.cli.colors.blue("lerna")} to manage the mon private async executeEditStage(context: Context): Promise { const newVersion = context.getData("version_new"); - const pack = context.getData("package.json"); + const pack = cloneDeep(context.getData("package.json")); if (context.argv.dryRun) { context.cli.log( @@ -333,6 +333,17 @@ Alternatively, you can use ${context.cli.colors.blue("lerna")} to manage the mon } } } + + async rollback(context: Context): Promise { + if (!context.executedStages.some((s) => s.id === "edit")) { + // Nothing has been changed yet + return; + } + + const pack = context.getData>("package.json"); + const packPath = path.join(context.cwd, "package.json"); + await fs.writeJson(packPath, pack, { spaces: 2 }); + } } export default PackagePlugin; diff --git a/packages/release-script/src/index.ts b/packages/release-script/src/index.ts index e2e2ac6..bd1e658 100644 --- a/packages/release-script/src/index.ts +++ b/packages/release-script/src/index.ts @@ -8,6 +8,7 @@ import { Plugin, ReleaseError, resolvePlugins, + rollback, SelectOption, stripColors, } from "@alcalzone/release-script-core"; @@ -268,6 +269,7 @@ export async function main(): Promise { setData: (key: string, value: any) => { data.set(key, value); }, + executedStages: [], }; context.cli = new CLI(context); @@ -295,6 +297,7 @@ export async function main(): Promise { message += "!"; console.error(); console.error(message); + await rollback(context); process.exit(1); } } catch (e: any) { @@ -318,6 +321,7 @@ export async function main(): Promise { ), ); } + await rollback(context); process.exit((e as any).code ?? 1); } } diff --git a/packages/testing/src/lib/context.ts b/packages/testing/src/lib/context.ts index 93daaea..7f7abc3 100644 --- a/packages/testing/src/lib/context.ts +++ b/packages/testing/src/lib/context.ts @@ -75,6 +75,7 @@ export const defaultContextOptions: Omit< }, plugins: [], sys: new MockSystem(), + executedStages: [], }; export function createMockContext( diff --git a/yarn.lock b/yarn.lock index 93559d4..1178bd8 100644 --- a/yarn.lock +++ b/yarn.lock @@ -21,6 +21,7 @@ __metadata: resolution: "@alcalzone/release-script-core@workspace:packages/core" dependencies: "@types/yargs": ^17.0.9 + alcalzone-shared: ^4.0.1 execa: ^5.1.1 picocolors: 1.0.0 typescript: ~4.6.2