diff --git a/.changeset/sour-terms-end.md b/.changeset/sour-terms-end.md
new file mode 100644
index 0000000000..fd20fa13c8
--- /dev/null
+++ b/.changeset/sour-terms-end.md
@@ -0,0 +1,7 @@
+---
+'@backstage/plugin-git-release-manager': minor
+---
+
+Errors caused while patching can leave the release branch in a broken state. Most commonly caused due to merge errors.
+
+This has been solved by introducing a dry run prior to patching the release branch. The dry run will attempt to cherry pick the selected patch commit onto a temporary branch created off of the release branch. If it succeeds, the temporary branch is deleted and the patch is applied on the release branch
diff --git a/plugins/git-release-manager/src/api/GitReleaseClient.test.ts b/plugins/git-release-manager/src/api/GitReleaseClient.test.ts
index 9125cb6110..418da485a2 100644
--- a/plugins/git-release-manager/src/api/GitReleaseClient.test.ts
+++ b/plugins/git-release-manager/src/api/GitReleaseClient.test.ts
@@ -37,6 +37,7 @@ describe('GitReleaseClient', () => {
"createRef": [Function],
"createRelease": [Function],
"createTagObject": [Function],
+ "deleteRef": [Function],
"getAllReleases": [Function],
"getAllTags": [Function],
"getBranch": [Function],
diff --git a/plugins/git-release-manager/src/api/GitReleaseClient.ts b/plugins/git-release-manager/src/api/GitReleaseClient.ts
index 948c77b47a..996b8bfdd3 100644
--- a/plugins/git-release-manager/src/api/GitReleaseClient.ts
+++ b/plugins/git-release-manager/src/api/GitReleaseClient.ts
@@ -293,6 +293,19 @@ export class GitReleaseClient implements GitReleaseApi {
};
};
+ deleteRef: GitReleaseApi['deleteRef'] = async ({ owner, repo, ref }) => {
+ const { octokit } = await this.getOctokit();
+ const createRefResponse = await octokit.git.deleteRef({
+ owner,
+ repo,
+ ref,
+ });
+
+ return {
+ success: createRefResponse.status === 204,
+ };
+ };
+
getComparison: GitReleaseApi['getComparison'] = async ({
owner,
repo,
@@ -651,6 +664,14 @@ export interface GitReleaseApi {
};
}>;
+ deleteRef: (
+ args: {
+ ref: string;
+ } & OwnerRepo,
+ ) => Promise<{
+ success: boolean;
+ }>;
+
getComparison: (
args: {
base: string;
diff --git a/plugins/git-release-manager/src/features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate.ts b/plugins/git-release-manager/src/features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate.ts
index 1173d6fbbe..4030d9370f 100644
--- a/plugins/git-release-manager/src/features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate.ts
+++ b/plugins/git-release-manager/src/features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate.ts
@@ -279,7 +279,7 @@ export function useCreateReleaseCandidate({
previousTag: latestRelease?.tagName,
});
} catch (error) {
- asyncCatcher(error);
+ asyncCatcher(error as Error);
}
addStepToResponseSteps({
diff --git a/plugins/git-release-manager/src/features/Patch/PatchBody.tsx b/plugins/git-release-manager/src/features/Patch/PatchBody.tsx
index 272363380f..2a2692bb22 100644
--- a/plugins/git-release-manager/src/features/Patch/PatchBody.tsx
+++ b/plugins/git-release-manager/src/features/Patch/PatchBody.tsx
@@ -141,6 +141,15 @@ export const PatchBody = ({
)}
+
+
+ Patches the release branch, creates a new tag and updates the Git
+ release. A dry run on a temporary branch will run prior to patching
+ the release branch to ensure there's no merge conflicts. Manual
+ patching is recommended should the dry run fail.
+
+
+
({
describe('patch', () => {
beforeEach(jest.clearAllMocks);
- it('should return the expected responseSteps and progress', async () => {
+ it('should return the expected responseSteps (including patch dry run) and progress', async () => {
const { result } = renderHook(() =>
usePatch({
bumpedTag: mockBumpedTag,
@@ -54,10 +54,10 @@ describe('patch', () => {
});
expect(result.error).toEqual(undefined);
- expect(result.current.responseSteps).toHaveLength(9);
+ expect(result.current.responseSteps).toHaveLength(18);
});
- it('should return the expected responseSteps and progress (with onSuccess)', async () => {
+ it('should return the expected responseSteps (including patch dry run) and progress (with onSuccess)', async () => {
const { result } = renderHook(() =>
usePatch({
bumpedTag: mockBumpedTag,
@@ -73,11 +73,56 @@ describe('patch', () => {
});
expect(result.error).toEqual(undefined);
- expect(result.current.responseSteps).toHaveLength(10);
+ expect(result.current.responseSteps).toHaveLength(19);
expect(result.current).toMatchInlineSnapshot(`
Object {
"progress": 100,
"responseSteps": Array [
+ Object {
+ "message": ,
+ },
+ Object {
+ "message": ,
+ },
+ Object {
+ "message": ,
+ },
+ Object {
+ "message": ,
+ },
+ Object {
+ "message": ,
+ },
+ Object {
+ "message": ,
+ },
+ Object {
+ "message": ,
+ },
+ Object {
+ "message": ,
+ },
+ Object {
+ "message": ,
+ },
Object {
"link": "https://mock_branch_links_html",
"message": "Fetched release branch \\"rc/1.2.3\\"",
diff --git a/plugins/git-release-manager/src/features/Patch/hooks/usePatch.ts b/plugins/git-release-manager/src/features/Patch/hooks/usePatch.ts
index 338fa2a2d1..188ffbca2e 100644
--- a/plugins/git-release-manager/src/features/Patch/hooks/usePatch.ts
+++ b/plugins/git-release-manager/src/features/Patch/hooks/usePatch.ts
@@ -14,27 +14,27 @@
* limitations under the License.
*/
+import { useAsync } from 'react-use';
import { useEffect, useState } from 'react';
-import { useAsync, useAsyncFn } from 'react-use';
-import {
- GetLatestReleaseResult,
- GetRecentCommitsResultSingle,
-} from '../../../api/GitReleaseClient';
-import { CalverTagParts } from '../../../helpers/tagParts/getCalverTagParts';
import {
CardHook,
ComponentConfig,
PatchOnSuccessArgs,
} from '../../../types/types';
+import {
+ GetLatestReleaseResult,
+ GetRecentCommitsResultSingle,
+} from '../../../api/GitReleaseClient';
+import { CalverTagParts } from '../../../helpers/tagParts/getCalverTagParts';
import { getPatchCommitSuffix } from '../helpers/getPatchCommitSuffix';
import { gitReleaseManagerApiRef } from '../../../api/serviceApiRef';
import { Project } from '../../../contexts/ProjectContext';
import { SemverTagParts } from '../../../helpers/tagParts/getSemverTagParts';
import { TAG_OBJECT_MESSAGE } from '../../../constants/constants';
-import { useResponseSteps } from '../../../hooks/useResponseSteps';
-import { useUserContext } from '../../../contexts/UserContext';
import { useApi } from '@backstage/core-plugin-api';
+import { usePatchDryRun } from './usePatchDryRun';
+import { useUserContext } from '../../../contexts/UserContext';
export interface UsePatch {
bumpedTag: string;
@@ -54,38 +54,53 @@ export function usePatch({
}: UsePatch): CardHook {
const pluginApiClient = useApi(gitReleaseManagerApiRef);
const { user } = useUserContext();
- const { responseSteps, addStepToResponseSteps, asyncCatcher, abortIfError } =
- useResponseSteps();
const releaseBranchName = latestRelease.targetCommitish;
+ const {
+ run,
+ runInvoked,
+ lastCallRes,
+ abortIfError,
+ addStepToResponseSteps,
+ asyncCatcher,
+ responseSteps,
+ TOTAL_PATCH_PREP_STEPS,
+ selectedPatchCommit,
+ } = usePatchDryRun({
+ bumpedTag,
+ releaseBranchName,
+ project,
+ tagParts,
+ });
+
/**
* (1) Here is the branch we want to cherry-pick to:
* > branch = GET /repos/$owner/$repo/branches/$branchName
* > branchSha = branch.commit.sha
* > branchTree = branch.commit.commit.tree.sha
*/
- const [releaseBranchRes, run] = useAsyncFn(
- async (selectedPatchCommit: GetRecentCommitsResultSingle) => {
- const { branch: releaseBranch } = await pluginApiClient
- .getBranch({
- owner: project.owner,
- repo: project.repo,
- branch: releaseBranchName,
- })
- .catch(asyncCatcher);
+ const releaseBranchRes = useAsync(async () => {
+ abortIfError(lastCallRes.error);
+ if (!lastCallRes.value) return undefined;
- addStepToResponseSteps({
- message: `Fetched release branch "${releaseBranch.name}"`,
- link: releaseBranch.links.html,
- });
+ const { branch: releaseBranch } = await pluginApiClient
+ .getBranch({
+ owner: project.owner,
+ repo: project.repo,
+ branch: releaseBranchName,
+ })
+ .catch(asyncCatcher);
- return {
- releaseBranch,
- selectedPatchCommit,
- };
- },
- );
+ addStepToResponseSteps({
+ message: `Fetched release branch "${releaseBranch.name}"`,
+ link: releaseBranch.links.html,
+ });
+
+ return {
+ releaseBranch,
+ };
+ }, [lastCallRes.value, lastCallRes.error]);
/**
* (2) Create a temporary commit on the branch, which extends as a sibling of
@@ -102,9 +117,7 @@ export function usePatch({
owner: project.owner,
repo: project.repo,
message: `Temporary commit for patch ${tagParts.patch}`,
- parents: [
- releaseBranchRes.value.selectedPatchCommit.firstParentSha ?? '',
- ],
+ parents: [selectedPatchCommit.firstParentSha ?? ''],
tree: releaseBranchRes.value.releaseBranch.commit.commit.tree.sha,
})
.catch(asyncCatcher);
@@ -159,7 +172,7 @@ export function usePatch({
owner: project.owner,
repo: project.repo,
base: releaseBranchName,
- head: releaseBranchRes.value.selectedPatchCommit.sha,
+ head: selectedPatchCommit.sha,
})
.catch(asyncCatcher);
@@ -184,7 +197,6 @@ export function usePatch({
if (!mergeRes.value || !releaseBranchRes.value) return undefined;
const releaseBranchSha = releaseBranchRes.value.releaseBranch.commit.sha;
- const selectedPatchCommit = releaseBranchRes.value.selectedPatchCommit;
const { commit: cherryPickCommit } = await pluginApiClient.createCommit({
owner: project.owner,
@@ -299,8 +311,6 @@ export function usePatch({
abortIfError(createdReferenceRes.error);
if (!createdReferenceRes.value || !releaseBranchRes.value) return undefined;
- const selectedPatchCommit = releaseBranchRes.value.selectedPatchCommit;
-
const { release } = await pluginApiClient
.updateRelease({
owner: project.owner,
@@ -343,16 +353,15 @@ ${selectedPatchCommit.commit.message}`,
project,
tagParts,
},
- patchCommitMessage:
- releaseBranchRes.value.selectedPatchCommit.commit.message,
- patchCommitUrl: releaseBranchRes.value.selectedPatchCommit.htmlUrl,
+ patchCommitMessage: selectedPatchCommit.commit.message,
+ patchCommitUrl: selectedPatchCommit.htmlUrl,
patchedTag: updatedReleaseRes.value.tagName,
previousTag: latestRelease.tagName,
updatedReleaseName: updatedReleaseRes.value.name,
updatedReleaseUrl: updatedReleaseRes.value.htmlUrl,
});
} catch (error) {
- asyncCatcher(error);
+ asyncCatcher(error as Error);
}
addStepToResponseSteps({
@@ -361,7 +370,7 @@ ${selectedPatchCommit.commit.message}`,
});
}, [updatedReleaseRes.value, updatedReleaseRes.error]);
- const TOTAL_STEPS = 9 + (!!onSuccess ? 1 : 0);
+ const TOTAL_STEPS = 9 + (!!onSuccess ? 1 : 0) + TOTAL_PATCH_PREP_STEPS;
const [progress, setProgress] = useState(0);
useEffect(() => {
setProgress((responseSteps.length / TOTAL_STEPS) * 100);
@@ -371,10 +380,6 @@ ${selectedPatchCommit.commit.message}`,
progress,
responseSteps,
run,
- runInvoked: Boolean(
- releaseBranchRes.loading ||
- releaseBranchRes.value ||
- releaseBranchRes.error,
- ),
+ runInvoked,
};
}
diff --git a/plugins/git-release-manager/src/features/Patch/hooks/usePatchDryRun.tsx b/plugins/git-release-manager/src/features/Patch/hooks/usePatchDryRun.tsx
new file mode 100644
index 0000000000..de3a67e874
--- /dev/null
+++ b/plugins/git-release-manager/src/features/Patch/hooks/usePatchDryRun.tsx
@@ -0,0 +1,396 @@
+/*
+ * Copyright 2021 The Backstage Authors
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+import { useApi } from '@backstage/core-plugin-api';
+import { useAsync, useAsyncFn } from 'react-use';
+import React from 'react';
+
+import { CalverTagParts } from '../../../helpers/tagParts/getCalverTagParts';
+import { getPatchCommitSuffix } from '../helpers/getPatchCommitSuffix';
+import { GetRecentCommitsResultSingle } from '../../../api/GitReleaseClient';
+import { gitReleaseManagerApiRef } from '../../../api/serviceApiRef';
+import { GitReleaseManagerError } from '../../../errors/GitReleaseManagerError';
+import { Project } from '../../../contexts/ProjectContext';
+import { SemverTagParts } from '../../../helpers/tagParts/getSemverTagParts';
+import { useResponseSteps } from '../../../hooks/useResponseSteps';
+
+export interface UsePatchDryRun {
+ bumpedTag: string;
+ releaseBranchName: string;
+ project: Project;
+ tagParts: NonNullable;
+}
+
+const PatchDryRunMessage = ({ message }: { message: string }) => (
+ <>
+ [Patch dry run] {message}
+ >
+);
+
+// Inspiration: https://stackoverflow.com/questions/53859199/how-to-cherry-pick-through-githubs-api
+export function usePatchDryRun({
+ bumpedTag,
+ releaseBranchName,
+ project,
+ tagParts,
+}: UsePatchDryRun) {
+ const pluginApiClient = useApi(gitReleaseManagerApiRef);
+ const { responseSteps, addStepToResponseSteps, asyncCatcher, abortIfError } =
+ useResponseSteps();
+
+ const tempPatchBranchName = `${releaseBranchName}-backstage-grm-patch-dry-run`;
+
+ /**
+ * (1) Get the release branch's most recent commit
+ */
+ const [latestCommitOnReleaseBranchRes, run] = useAsyncFn(
+ async (selectedPatchCommit: GetRecentCommitsResultSingle) => {
+ try {
+ await pluginApiClient.deleteRef({
+ owner: project.owner,
+ repo: project.repo,
+ ref: `heads/${tempPatchBranchName}`,
+ });
+ } catch (error: any) {
+ if (error.message !== 'Reference does not exist') {
+ throw error;
+ }
+ }
+
+ const { commit: latestCommit } = await pluginApiClient
+ .getCommit({
+ owner: project.owner,
+ repo: project.repo,
+ ref: releaseBranchName,
+ })
+ .catch(asyncCatcher);
+
+ addStepToResponseSteps({
+ message: (
+
+ ),
+ });
+
+ return {
+ latestCommit,
+ selectedPatchCommit,
+ };
+ },
+ );
+
+ /**
+ * (2) Create temporary patch branch based on release branch's most recent sha
+ * to test if programmatic cherry pick patching is possible
+ */
+ const createTempPatchBranchRes = useAsync(async () => {
+ abortIfError(latestCommitOnReleaseBranchRes.error);
+ if (!latestCommitOnReleaseBranchRes.value) return undefined;
+
+ const { reference: createdReleaseBranch } = await pluginApiClient
+ .createRef({
+ owner: project.owner,
+ repo: project.repo,
+ sha: latestCommitOnReleaseBranchRes.value.latestCommit.sha,
+ ref: `refs/heads/${tempPatchBranchName}`,
+ })
+ .catch(error => {
+ if (error?.body?.message === 'Reference already exists') {
+ throw new GitReleaseManagerError(
+ `Branch "${tempPatchBranchName}" already exists: .../tree/${tempPatchBranchName}`,
+ );
+ }
+ throw error;
+ })
+ .catch(asyncCatcher);
+
+ addStepToResponseSteps({
+ message: (
+
+ ),
+ });
+
+ return {
+ ...createdReleaseBranch,
+ selectedPatchCommit:
+ latestCommitOnReleaseBranchRes.value.selectedPatchCommit,
+ };
+ }, [
+ latestCommitOnReleaseBranchRes.value,
+ latestCommitOnReleaseBranchRes.error,
+ ]);
+
+ /**
+ * (3) Here is the branch we want to cherry-pick to:
+ * > branch = GET /repos/$owner/$repo/branches/$branchName
+ * > branchSha = branch.commit.sha
+ * > branchTree = branch.commit.commit.tree.sha
+ */
+ const tempPatchBranchRes = useAsync(async () => {
+ abortIfError(createTempPatchBranchRes.error);
+ if (!createTempPatchBranchRes.value) return undefined;
+
+ const { branch: releaseBranch } = await pluginApiClient
+ .getBranch({
+ owner: project.owner,
+ repo: project.repo,
+ branch: tempPatchBranchName,
+ })
+ .catch(asyncCatcher);
+
+ addStepToResponseSteps({
+ message: (
+
+ ),
+ });
+
+ return {
+ releaseBranch,
+ selectedPatchCommit: createTempPatchBranchRes.value.selectedPatchCommit,
+ };
+ }, [createTempPatchBranchRes.value, createTempPatchBranchRes.error]);
+
+ /**
+ * (4) Create a temporary commit on the branch, which extends as a sibling of
+ * the commit we want but contains the current tree of the target branch:
+ * > parentSha = commit.parents.head // first parent -- there should only be one
+ * > tempCommit = POST /repos/$owner/$repo/git/commits { "message": "temp", "tree": branchTree, "parents": [parentSha] }
+ */
+ const tempCommitRes = useAsync(async () => {
+ abortIfError(tempPatchBranchRes.error);
+ if (!tempPatchBranchRes.value) return undefined;
+
+ const { commit: tempCommit } = await pluginApiClient
+ .createCommit({
+ owner: project.owner,
+ repo: project.repo,
+ message: `Temporary commit for patch ${tagParts.patch}`,
+ parents: [
+ tempPatchBranchRes.value.selectedPatchCommit.firstParentSha ?? '',
+ ],
+ tree: tempPatchBranchRes.value.releaseBranch.commit.commit.tree.sha,
+ })
+ .catch(asyncCatcher);
+
+ addStepToResponseSteps({
+ message: ,
+ });
+
+ return {
+ ...tempCommit,
+ };
+ }, [tempPatchBranchRes.value, tempPatchBranchRes.error]);
+
+ /**
+ * (5) Now temporarily force the branch over to that commit:
+ * > PATCH /repos/$owner/$repo/git/refs/heads/$refName { sha = tempCommit.sha, force = true }
+ */
+ const forceBranchRes = useAsync(async () => {
+ abortIfError(tempCommitRes.error);
+ if (!tempCommitRes.value) return undefined;
+
+ await pluginApiClient
+ .updateRef({
+ owner: project.owner,
+ repo: project.repo,
+ sha: tempCommitRes.value.sha,
+ ref: `heads/${tempPatchBranchName}`,
+ force: true,
+ })
+ .catch(asyncCatcher);
+
+ addStepToResponseSteps({
+ message: (
+
+ ),
+ });
+
+ return {
+ trigger: 'next step 🚀 ',
+ };
+ }, [tempCommitRes.value, tempCommitRes.error]);
+
+ /**
+ * (6) Merge the commit we want into this mess:
+ * > merge = POST /repos/$owner/$repo/merges { "base": branchName, "head": commit.sha }
+ */
+ const mergeRes = useAsync(async () => {
+ abortIfError(forceBranchRes.error);
+ if (!forceBranchRes.value || !tempPatchBranchRes.value) return undefined;
+
+ const { merge } = await pluginApiClient
+ .merge({
+ owner: project.owner,
+ repo: project.repo,
+ base: tempPatchBranchName,
+ head: tempPatchBranchRes.value.selectedPatchCommit.sha,
+ })
+ .catch(async error => {
+ if (error?.message === 'Merge conflict') {
+ try {
+ await pluginApiClient.deleteRef({
+ owner: project.owner,
+ repo: project.repo,
+ ref: `heads/${tempPatchBranchName}`,
+ });
+ } catch (_error) {
+ // swallow
+ }
+
+ throw new GitReleaseManagerError(
+ 'Patching failed due to merge conflict. Will attempt to delete temporary patch dry run branch. Manual patching is recommended.',
+ );
+ }
+
+ throw error;
+ })
+ .catch(asyncCatcher);
+
+ addStepToResponseSteps({
+ message: (
+
+ ),
+ });
+
+ return {
+ ...merge,
+ };
+ }, [forceBranchRes.value, forceBranchRes.error]);
+
+ /**
+ * (7) Now that we know what the tree should be, create the cherry-pick commit.
+ * Note that branchSha is the original from up at the top.
+ * > cherry = POST /repos/$owner/$repo/git/commits { "message": "looks good!", "tree": mergeTree, "parents": [branchSha] }
+ */
+ const cherryPickRes = useAsync(async () => {
+ abortIfError(mergeRes.error);
+ if (!mergeRes.value || !tempPatchBranchRes.value) return undefined;
+
+ const releaseBranchSha = tempPatchBranchRes.value.releaseBranch.commit.sha;
+ const {
+ commit: { message },
+ sha: commitSha,
+ } = tempPatchBranchRes.value.selectedPatchCommit;
+
+ const { commit: cherryPickCommit } = await pluginApiClient.createCommit({
+ owner: project.owner,
+ repo: project.repo,
+ message: `[patch (dry run) ${bumpedTag}] ${message}
+
+ ${getPatchCommitSuffix({ commitSha })}`,
+ parents: [releaseBranchSha],
+ tree: mergeRes.value.commit.tree.sha,
+ });
+
+ addStepToResponseSteps({
+ message: (
+
+ ),
+ });
+
+ return {
+ ...cherryPickCommit,
+ };
+ }, [mergeRes.value, mergeRes.error]);
+
+ /**
+ * (8) Replace the temp commit with the real commit:
+ * > PATCH /repos/$owner/$repo/git/refs/heads/$refName { sha = cherry.sha, force = true }
+ */
+ const updatedRefRes = useAsync(async () => {
+ abortIfError(cherryPickRes.error);
+ if (!cherryPickRes.value) return undefined;
+
+ const { reference: updatedReference } = await pluginApiClient
+ .updateRef({
+ owner: project.owner,
+ repo: project.repo,
+ ref: `heads/${tempPatchBranchName}`,
+ sha: cherryPickRes.value.sha,
+ force: true,
+ })
+ .catch(asyncCatcher);
+
+ addStepToResponseSteps({
+ message: (
+
+ ),
+ });
+
+ return {
+ ...updatedReference,
+ };
+ }, [cherryPickRes.value, cherryPickRes.error]);
+
+ /**
+ * (9) Delete temp patch branch
+ */
+ const deleteTempPatchBranchRes = useAsync(async () => {
+ abortIfError(updatedRefRes.error);
+ if (!updatedRefRes.value) return undefined;
+
+ const { success: deletedReferenceSuccess } =
+ await pluginApiClient.deleteRef({
+ owner: project.owner,
+ repo: project.repo,
+ ref: `heads/${tempPatchBranchName}`,
+ });
+
+ addStepToResponseSteps({
+ message: (
+
+ ),
+ });
+
+ return {
+ deletedReferenceSuccess,
+ };
+ }, [updatedRefRes.value, updatedRefRes.error]);
+
+ const TOTAL_PATCH_PREP_STEPS = 9;
+
+ return {
+ TOTAL_PATCH_PREP_STEPS,
+ run,
+ runInvoked: Boolean(
+ deleteTempPatchBranchRes.loading ||
+ deleteTempPatchBranchRes.value ||
+ deleteTempPatchBranchRes.error,
+ ),
+ lastCallRes: deleteTempPatchBranchRes,
+ responseSteps,
+ addStepToResponseSteps,
+ asyncCatcher,
+ abortIfError,
+ selectedPatchCommit: latestCommitOnReleaseBranchRes.value
+ ?.selectedPatchCommit as any,
+ };
+}
diff --git a/plugins/git-release-manager/src/features/PromoteRc/hooks/usePromoteRc.ts b/plugins/git-release-manager/src/features/PromoteRc/hooks/usePromoteRc.ts
index 232bc0b744..7ebc4fed54 100644
--- a/plugins/git-release-manager/src/features/PromoteRc/hooks/usePromoteRc.ts
+++ b/plugins/git-release-manager/src/features/PromoteRc/hooks/usePromoteRc.ts
@@ -182,7 +182,7 @@ export function usePromoteRc({
updatedTagUrl: promotedReleaseRes.value.htmlUrl,
});
} catch (error) {
- asyncCatcher(error);
+ asyncCatcher(error as Error);
}
addStepToResponseSteps({
diff --git a/plugins/git-release-manager/src/hooks/useResponseSteps.test.ts b/plugins/git-release-manager/src/hooks/useResponseSteps.test.ts
index 29ef7b7aae..5daa8b47ad 100644
--- a/plugins/git-release-manager/src/hooks/useResponseSteps.test.ts
+++ b/plugins/git-release-manager/src/hooks/useResponseSteps.test.ts
@@ -72,7 +72,16 @@ describe('useResponseSteps', () => {
Array [
Object {
"icon": "failure",
- "message": "Something went wrong 🔥",
+ "message":
+ Something went wrong
+
+
+ 🔥
+
+ ,
"secondaryMessage": "Error message: :(",
},
]
@@ -96,7 +105,16 @@ describe('useResponseSteps', () => {
Array [
Object {
"icon": "failure",
- "message": "Something went wrong 🔥",
+ "message":
+ Something went wrong
+
+
+ 🔥
+
+ ,
"secondaryMessage": "Error message: unknown",
},
]
@@ -105,29 +123,6 @@ describe('useResponseSteps', () => {
});
describe('abortIfError', () => {
- it('should throw if Error and add a failure step', async () => {
- const { result } = renderHook(() => useResponseSteps());
-
- expect(result.current.responseSteps).toMatchInlineSnapshot(`Array []`);
-
- act(() => {
- try {
- result.current.abortIfError(new Error('Das kaboom'));
- } catch (error) {
- //
- }
- });
-
- expect(result.current.responseSteps).toMatchInlineSnapshot(`
- Array [
- Object {
- "icon": "failure",
- "message": "Skipped due to error in previous step",
- },
- ]
- `);
- });
-
it('should do nothing if not Error', async () => {
const { result } = renderHook(() => useResponseSteps());
diff --git a/plugins/git-release-manager/src/hooks/useResponseSteps.ts b/plugins/git-release-manager/src/hooks/useResponseSteps.tsx
similarity index 84%
rename from plugins/git-release-manager/src/hooks/useResponseSteps.ts
rename to plugins/git-release-manager/src/hooks/useResponseSteps.tsx
index d39996e2f3..3c44a34a0d 100644
--- a/plugins/git-release-manager/src/hooks/useResponseSteps.ts
+++ b/plugins/git-release-manager/src/hooks/useResponseSteps.tsx
@@ -14,15 +14,10 @@
* limitations under the License.
*/
-import { useState } from 'react';
+import React, { useState } from 'react';
import { ResponseStep } from '../types/types';
-const RESPONSE_STEP_FAILURE_ABORT: ResponseStep = {
- message: 'Skipped due to error in previous step',
- icon: 'failure',
-};
-
export function useResponseSteps() {
const [responseSteps, setResponseSteps] = useState([]);
@@ -32,7 +27,14 @@ export function useResponseSteps() {
const asyncCatcher = (error: Error): never => {
const responseStepError: ResponseStep = {
- message: 'Something went wrong 🔥',
+ message: (
+
+ Something went wrong{' '}
+
+ 🔥
+
+
+ ),
secondaryMessage: `Error message: ${
error?.message ? error.message : 'unknown'
}`,
@@ -45,7 +47,6 @@ export function useResponseSteps() {
const abortIfError = (error?: Error) => {
if (error) {
- addStepToResponseSteps(RESPONSE_STEP_FAILURE_ABORT);
throw error;
}
};
diff --git a/plugins/git-release-manager/src/test-helpers/mock-api-client.ts b/plugins/git-release-manager/src/test-helpers/mock-api-client.ts
index b2e39d6f7c..bbfe8f7636 100644
--- a/plugins/git-release-manager/src/test-helpers/mock-api-client.ts
+++ b/plugins/git-release-manager/src/test-helpers/mock-api-client.ts
@@ -87,6 +87,10 @@ export const mockApiClient: GitReleaseApi = {
},
})),
+ deleteRef: jest.fn(async () => ({
+ success: true,
+ })),
+
createRelease: jest.fn(async () => ({
release: {
name: 'mock_createRelease_name',
diff --git a/plugins/git-release-manager/src/types/types.ts b/plugins/git-release-manager/src/types/types.ts
index 39418fa9d3..600a293deb 100644
--- a/plugins/git-release-manager/src/types/types.ts
+++ b/plugins/git-release-manager/src/types/types.ts
@@ -53,7 +53,7 @@ export interface PatchOnSuccessArgs {
}
export interface ResponseStep {
- message: string | React.ReactNode;
+ message: React.ReactNode;
secondaryMessage?: string | React.ReactNode;
link?: string;
icon?: 'success' | 'failure';