From 253f8951c053a2d62387eefdee2d03f1dcfcda19 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Fri, 23 Apr 2021 00:06:00 +0200 Subject: [PATCH] Create validateTagName module which validated the tagName before it gets into the Cards Replace throwing with returning an object with an error property for getXTagParts helpers Move Transition into its own module Signed-off-by: Erik Engervall --- .../src/api/PluginApiClient.ts | 19 ++- .../src/cards/Cards.tsx | 16 ++- .../src/cards/CreateRc/CreateRc.tsx | 134 ++++++++++-------- .../src/cards/CreateRc/hooks/useCreateRc.ts | 14 +- .../src/cards/Patch/Patch.tsx | 74 ++++++---- .../ResponseStepDialog/ResponseStepDialog.tsx | 12 +- .../src/components/Transition.tsx | 26 ++++ .../src/helpers/getBumpedTag.test.ts | 24 ++++ .../src/helpers/getBumpedTag.ts | 14 +- .../src/helpers/getRcGitHubInfo.ts | 13 +- .../tagParts/getCalverTagParts.test.ts | 102 ++++++++----- .../src/helpers/tagParts/getCalverTagParts.ts | 25 ++-- .../tagParts/getSemverTagParts.test.ts | 103 +++++++++----- .../src/helpers/tagParts/getSemverTagParts.ts | 40 ++++-- .../src/helpers/tagParts/validateTagName.ts | 47 ++++++ .../helpers/tagParts/validateTagParts.test.ts | 120 ++++++++++++++++ .../useVersioningStrategyMatchesRepoTags.ts | 10 +- .../github-release-manager/src/types/types.ts | 5 + 18 files changed, 592 insertions(+), 206 deletions(-) create mode 100644 plugins/github-release-manager/src/components/Transition.tsx create mode 100644 plugins/github-release-manager/src/helpers/tagParts/validateTagName.ts create mode 100644 plugins/github-release-manager/src/helpers/tagParts/validateTagParts.test.ts diff --git a/plugins/github-release-manager/src/api/PluginApiClient.ts b/plugins/github-release-manager/src/api/PluginApiClient.ts index 8efaacb4b8..e9f8e89f33 100644 --- a/plugins/github-release-manager/src/api/PluginApiClient.ts +++ b/plugins/github-release-manager/src/api/PluginApiClient.ts @@ -20,7 +20,6 @@ import { readGitHubIntegrationConfigs } from '@backstage/integration'; import { CalverTagParts } from '../helpers/tagParts/getCalverTagParts'; import { DISABLE_CACHE } from '../constants/constants'; -import { getRcGitHubInfo } from '../helpers/getRcGitHubInfo'; import { Project } from '../contexts/ProjectContext'; import { SemverTagParts } from '../helpers/tagParts/getSemverTagParts'; @@ -298,19 +297,23 @@ export class PluginApiClient implements IPluginApiClient { createRelease: async ({ owner, repo, - nextGitHubInfo, + rcReleaseTag, + releaseName, + rcBranch, releaseBody, }: { - nextGitHubInfo: ReturnType; + rcReleaseTag: string; + releaseName: string; + rcBranch: string; releaseBody: string; } & OwnerRepo) => { const { octokit } = await this.getOctokit(); const createReleaseResponse = await octokit.repos.createRelease({ owner, repo, - tag_name: nextGitHubInfo.rcReleaseTag, - name: nextGitHubInfo.releaseName, - target_commitish: nextGitHubInfo.rcBranch, + tag_name: rcReleaseTag, + name: releaseName, + target_commitish: rcBranch, body: releaseBody, prerelease: true, }); @@ -697,7 +700,9 @@ export type GetComparisonResult = UnboxReturnedPromise; type CreateRelease = ( args: { - nextGitHubInfo: ReturnType; + rcReleaseTag: string; + releaseName: string; + rcBranch: string; releaseBody: string; } & OwnerRepo, ) => Promise<{ diff --git a/plugins/github-release-manager/src/cards/Cards.tsx b/plugins/github-release-manager/src/cards/Cards.tsx index 0b4149212e..e47181ba4a 100644 --- a/plugins/github-release-manager/src/cards/Cards.tsx +++ b/plugins/github-release-manager/src/cards/Cards.tsx @@ -15,8 +15,8 @@ */ import React, { useState } from 'react'; +import { Alert, AlertTitle } from '@material-ui/lab'; import { ErrorBoundary } from '@backstage/core'; -import { Alert } from '@material-ui/lab'; import { CenteredCircularProgress } from '../components/CenteredCircularProgress'; import { CreateRc } from './CreateRc/CreateRc'; @@ -29,6 +29,7 @@ import { useGetGitHubBatchInfo } from '../hooks/useGetGitHubBatchInfo'; import { usePluginApiClientContext } from '../contexts/PluginApiClientContext'; import { useProjectContext } from '../contexts/ProjectContext'; import { useVersioningStrategyMatchesRepoTags } from '../hooks/useVersioningStrategyMatchesRepoTags'; +import { validateTagName } from '../helpers/tagParts/validateTagName'; export function Cards({ components, @@ -78,6 +79,19 @@ export function Cards({ ); } + const { tagNameError } = validateTagName({ + project, + tagName: gitHubBatchInfo.value.latestRelease?.tagName, + }); + if (tagNameError) { + return ( + + {tagNameError.title && {tagNameError.title}} + {tagNameError.subtitle} + + ); + } + return ( diff --git a/plugins/github-release-manager/src/cards/CreateRc/CreateRc.tsx b/plugins/github-release-manager/src/cards/CreateRc/CreateRc.tsx index 1c9b032b5d..a3c3d33a26 100644 --- a/plugins/github-release-manager/src/cards/CreateRc/CreateRc.tsx +++ b/plugins/github-release-manager/src/cards/CreateRc/CreateRc.tsx @@ -15,7 +15,7 @@ */ import React, { useState, useEffect } from 'react'; -import { Alert } from '@material-ui/lab'; +import { Alert, AlertTitle } from '@material-ui/lab'; import { Button, FormControl, @@ -49,6 +49,18 @@ interface CreateRcProps { successCb?: ComponentConfigCreateRc['successCb']; } +const InfoCardPlusWrapper = ({ children }: { children: React.ReactNode }) => { + const classes = useStyles(); + return ( + + + Create Release Candidate + + {children} + + ); +}; + export const CreateRc = ({ defaultBranch, latestRelease, @@ -90,71 +102,28 @@ export const CreateRc = ({ ); } + if (nextGitHubInfo.error !== undefined) { + return ( + + + {nextGitHubInfo.error.title && ( + {nextGitHubInfo.error.title} + )} + + {nextGitHubInfo.error.subtitle} + + + ); + } + const tagAlreadyExists = latestRelease !== null && latestRelease.tagName === nextGitHubInfo.rcReleaseTag; const conflictingPreRelease = latestRelease !== null && latestRelease.prerelease; - function Description() { - if (conflictingPreRelease) { - return ( - - The most recent release is already a Release Candidate - - ); - } - - if (tagAlreadyExists) { - return ( - - There's already a tag named{' '} - {nextGitHubInfo.rcReleaseTag} - - ); - } - - return ( -
- - - - - - - -
- ); - } - - function CTA() { - return ( - - ); - } - return ( - - - Create Release Candidate - - + {project.versioningStrategy === 'semver' && latestRelease && !conflictingPreRelease && ( @@ -182,9 +151,50 @@ export const CreateRc = ({ )} - + {conflictingPreRelease || tagAlreadyExists ? ( + <> + {conflictingPreRelease && ( + + The most recent release is already a Release Candidate + + )} - - + {tagAlreadyExists && ( + + There's already a tag named{' '} + {nextGitHubInfo.rcReleaseTag} + + )} + + ) : ( +
+ + + + + + + +
+ )} + + + ); }; diff --git a/plugins/github-release-manager/src/cards/CreateRc/hooks/useCreateRc.ts b/plugins/github-release-manager/src/cards/CreateRc/hooks/useCreateRc.ts index b0b0c98dca..2b1b0b688c 100644 --- a/plugins/github-release-manager/src/cards/CreateRc/hooks/useCreateRc.ts +++ b/plugins/github-release-manager/src/cards/CreateRc/hooks/useCreateRc.ts @@ -45,6 +45,16 @@ export function useCreateRc({ project, successCb, }: CreateRC): CardHook { + if (nextGitHubInfo.error) { + throw new GitHubReleaseManagerError( + `Unexpected error: ${ + nextGitHubInfo.error.title + ? `${nextGitHubInfo.error.title} (${nextGitHubInfo.error.subtitle})` + : nextGitHubInfo.error.subtitle + }`, + ); + } + const { responseSteps, addStepToResponseSteps, @@ -162,7 +172,9 @@ export function useCreateRc({ .createRelease({ owner: project.owner, repo: project.repo, - nextGitHubInfo: nextGitHubInfo, + rcReleaseTag: nextGitHubInfo.rcReleaseTag, + releaseName: nextGitHubInfo.releaseName, + rcBranch: nextGitHubInfo.rcBranch, releaseBody: getComparisonRes.value.releaseBody, }) .catch(asyncCatcher); diff --git a/plugins/github-release-manager/src/cards/Patch/Patch.tsx b/plugins/github-release-manager/src/cards/Patch/Patch.tsx index dc3ca1c5b8..d01a366ffb 100644 --- a/plugins/github-release-manager/src/cards/Patch/Patch.tsx +++ b/plugins/github-release-manager/src/cards/Patch/Patch.tsx @@ -16,6 +16,7 @@ import React from 'react'; import { Typography } from '@material-ui/core'; +import { Alert, AlertTitle } from '@material-ui/lab'; import { GetBranchResult, @@ -40,42 +41,59 @@ export const Patch = ({ releaseBranch, successCb, }: PatchProps) => { - const { project } = useProjectContext(); const classes = useStyles(); - function Body() { - if (latestRelease === null) { - return ; - } - - if (releaseBranch === null) { - return ; - } - - const { bumpedTag, tagParts } = getBumpedTag({ - project, - tag: latestRelease.tagName, - bumpLevel: 'patch', - }); - - return ( - - ); - } - return ( Patch Release {latestRelease?.prerelease ? 'Candidate' : 'Version'} - + ); }; + +function BodyWrapper({ latestRelease, releaseBranch, successCb }: PatchProps) { + const { project } = useProjectContext(); + + if (latestRelease === null) { + return ; + } + + if (releaseBranch === null) { + return ; + } + + const bumpedTag = getBumpedTag({ + project, + tag: latestRelease.tagName, + bumpLevel: 'patch', + }); + + if (bumpedTag.error !== undefined) { + return ( + + {bumpedTag.error.title && ( + {bumpedTag.error.title} + )} + + {bumpedTag.error.subtitle} + + ); + } + + return ( + + ); +} diff --git a/plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepDialog.tsx b/plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepDialog.tsx index 89714b7bf0..d504ffec70 100644 --- a/plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepDialog.tsx +++ b/plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepDialog.tsx @@ -14,20 +14,19 @@ * limitations under the License. */ -import React, { forwardRef, Ref } from 'react'; +import React from 'react'; import { Button, Dialog as MaterialDialog, DialogActions, DialogTitle, - Slide, } from '@material-ui/core'; -import { TransitionProps } from '@material-ui/core/transitions'; import RefreshIcon from '@material-ui/icons/Refresh'; import { LinearProgressWithLabel } from './LinearProgressWithLabel'; import { ResponseStep } from '../../types/types'; import { ResponseStepList } from './ResponseStepList'; +import { Transition } from '../Transition'; import { useRefetchContext } from '../../contexts/RefetchContext'; interface DialogProps { @@ -36,13 +35,6 @@ interface DialogProps { title: string; } -const Transition = forwardRef(function Transition( - props: { children?: React.ReactElement } & TransitionProps, - ref: Ref, -) { - return ; -}); - export const ResponseStepDialog = ({ progress, responseSteps, diff --git a/plugins/github-release-manager/src/components/Transition.tsx b/plugins/github-release-manager/src/components/Transition.tsx new file mode 100644 index 0000000000..0c54e0f851 --- /dev/null +++ b/plugins/github-release-manager/src/components/Transition.tsx @@ -0,0 +1,26 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 React, { forwardRef, Ref } from 'react'; +import { Slide } from '@material-ui/core'; +import { TransitionProps } from '@material-ui/core/transitions'; + +export const Transition = forwardRef(function Transition( + props: { children?: React.ReactElement } & TransitionProps, + ref: Ref, +) { + return ; +}); diff --git a/plugins/github-release-manager/src/helpers/getBumpedTag.test.ts b/plugins/github-release-manager/src/helpers/getBumpedTag.test.ts index 1fd7cd2ecf..27023f653f 100644 --- a/plugins/github-release-manager/src/helpers/getBumpedTag.test.ts +++ b/plugins/github-release-manager/src/helpers/getBumpedTag.test.ts @@ -32,6 +32,7 @@ describe('getBumpedTag', () => { expect(result).toMatchInlineSnapshot(` Object { "bumpedTag": "rc-2020.01.01_2", + "error": undefined, "tagParts": Object { "calver": "2020.01.01", "patch": 2, @@ -51,6 +52,7 @@ describe('getBumpedTag', () => { expect(result).toMatchInlineSnapshot(` Object { "bumpedTag": "rc-2020.01.01_2", + "error": undefined, "tagParts": Object { "calver": "2020.01.01", "patch": 2, @@ -72,6 +74,7 @@ describe('getBumpedTag', () => { expect(result).toMatchInlineSnapshot(` Object { "bumpedTag": "rc-1.2.4", + "error": undefined, "tagParts": Object { "major": 1, "minor": 2, @@ -92,6 +95,7 @@ describe('getBumpedTag', () => { expect(result).toMatchInlineSnapshot(` Object { "bumpedTag": "rc-1.3.0", + "error": undefined, "tagParts": Object { "major": 1, "minor": 3, @@ -112,6 +116,7 @@ describe('getBumpedTag', () => { expect(result).toMatchInlineSnapshot(` Object { "bumpedTag": "rc-2.0.0", + "error": undefined, "tagParts": Object { "major": 2, "minor": 0, @@ -122,4 +127,23 @@ describe('getBumpedTag', () => { `); }); }); + + describe('errors', () => { + it('should propagate errors for invalid tags', () => { + const result = getBumpedTag({ + project: mockCalverProject, + tag: '😬', + bumpLevel: 'patch', + }); + + expect(result).toMatchInlineSnapshot(` + Object { + "error": Object { + "subtitle": "Expected calver matching \\"/(rc|version)-([0-9]{4}\\\\.[0-9]{2}\\\\.[0-9]{2})_([0-9]+)/\\", found \\"😬\\"", + "title": "Invalid tag", + }, + } + `); + }); + }); }); diff --git a/plugins/github-release-manager/src/helpers/getBumpedTag.ts b/plugins/github-release-manager/src/helpers/getBumpedTag.ts index 5409f7d2b3..f8b976b01f 100644 --- a/plugins/github-release-manager/src/helpers/getBumpedTag.ts +++ b/plugins/github-release-manager/src/helpers/getBumpedTag.ts @@ -32,11 +32,17 @@ export function getBumpedTag({ }) { const tagParts = getTagParts({ project, tag }); - if (isCalverTagParts(project, tagParts)) { - return getPatchedCalverTag(tagParts); + if (tagParts.error !== undefined) { + return { + error: tagParts.error, + }; } - return getBumpedSemverTag(tagParts, bumpLevel); + if (isCalverTagParts(project, tagParts.tagParts)) { + return getPatchedCalverTag(tagParts.tagParts); + } + + return getBumpedSemverTag(tagParts.tagParts, bumpLevel); } function getPatchedCalverTag(tagParts: CalverTagParts) { @@ -49,6 +55,7 @@ function getPatchedCalverTag(tagParts: CalverTagParts) { return { bumpedTag, tagParts: bumpedTagParts, + error: undefined, }; } @@ -63,6 +70,7 @@ function getBumpedSemverTag( return { bumpedTag, tagParts: bumpedTagParts, + error: undefined, }; } diff --git a/plugins/github-release-manager/src/helpers/getRcGitHubInfo.ts b/plugins/github-release-manager/src/helpers/getRcGitHubInfo.ts index 18043033c9..70935a0dd3 100644 --- a/plugins/github-release-manager/src/helpers/getRcGitHubInfo.ts +++ b/plugins/github-release-manager/src/helpers/getRcGitHubInfo.ts @@ -49,8 +49,17 @@ export const getRcGitHubInfo = ({ }; } - const tagParts = getSemverTagParts(latestRelease.tagName); - const { bumpedTagParts } = getBumpedSemverTagParts(tagParts, semverBumpLevel); + const semverTagParts = getSemverTagParts(latestRelease.tagName); + if (semverTagParts.error !== undefined) { + return { + error: semverTagParts.error, + }; + } + + const { bumpedTagParts } = getBumpedSemverTagParts( + semverTagParts.tagParts, + semverBumpLevel, + ); const bumpedTag = `${bumpedTagParts.major}.${bumpedTagParts.minor}.${bumpedTagParts.patch}`; diff --git a/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.test.ts b/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.test.ts index 238abe720a..c74438b63a 100644 --- a/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.test.ts +++ b/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.test.ts @@ -21,51 +21,87 @@ import { import { getCalverTagParts } from './getCalverTagParts'; describe('getCalverTagParts', () => { - it('should return tagParts for RC tag', () => { - const result = getCalverTagParts(mockReleaseCandidateCalver.tagName); + describe('happy path', () => { + it('should return tagParts for RC tag', () => { + const result = getCalverTagParts(mockReleaseCandidateCalver.tagName); - expect(result).toMatchInlineSnapshot(` + expect(result).toMatchInlineSnapshot(` + Object { + "tagParts": Object { + "calver": "2020.01.01", + "patch": 1, + "prefix": "rc", + }, + } + `); + }); + + it('should return tagParts for Version tag', () => { + const result = getCalverTagParts(mockReleaseVersionCalver.tagName); + + expect(result).toMatchInlineSnapshot(` + Object { + "tagParts": Object { + "calver": "2020.01.01", + "patch": 1, + "prefix": "version", + }, + } + `); + }); + }); + + describe('invalid calver tags', () => { + it('should return error for invalid prefix', () => { + const result = getCalverTagParts('invalid-2020.01.01_1'); + + expect(result).toMatchInlineSnapshot(` Object { - "calver": "2020.01.01", - "patch": 1, - "prefix": "rc", + "error": Object { + "subtitle": "Expected calver matching \\"/(rc|version)-([0-9]{4}\\\\.[0-9]{2}\\\\.[0-9]{2})_([0-9]+)/\\", found \\"invalid-2020.01.01_1\\"", + "title": "Invalid tag", + }, } `); - }); + }); - it('should return tagParts for Version tag', () => { - const result = getCalverTagParts(mockReleaseVersionCalver.tagName); + it('should return error for invalid calver (missing padded zero)', () => { + const result = getCalverTagParts('rc-2020.1.01_1'); - expect(result).toMatchInlineSnapshot(` + expect(result).toMatchInlineSnapshot(` Object { - "calver": "2020.01.01", - "patch": 1, - "prefix": "version", + "error": Object { + "subtitle": "Expected calver matching \\"/(rc|version)-([0-9]{4}\\\\.[0-9]{2}\\\\.[0-9]{2})_([0-9]+)/\\", found \\"rc-2020.1.01_1\\"", + "title": "Invalid tag", + }, } `); - }); + }); - it('should return null for invalid prefix', () => { - expect(() => - getCalverTagParts('invalid-2020.01.01_1'), - ).toThrowErrorMatchingInlineSnapshot(`"Invalid calver tag"`); - }); + it('should return error for invalid calver (missing day)', () => { + const result = getCalverTagParts('rc-2020.01_1'); - it('should return null for invalid calver (missing padded zero)', () => { - expect(() => - getCalverTagParts('rc-2020.1.01_1'), - ).toThrowErrorMatchingInlineSnapshot(`"Invalid calver tag"`); - }); + expect(result).toMatchInlineSnapshot(` + Object { + "error": Object { + "subtitle": "Expected calver matching \\"/(rc|version)-([0-9]{4}\\\\.[0-9]{2}\\\\.[0-9]{2})_([0-9]+)/\\", found \\"rc-2020.01_1\\"", + "title": "Invalid tag", + }, + } + `); + }); - it('should return null for invalid calver (missing day)', () => { - expect(() => - getCalverTagParts('rc-2020.01_1'), - ).toThrowErrorMatchingInlineSnapshot(`"Invalid calver tag"`); - }); + it('should return error for invalid patch (letter instead of number)', () => { + const result = getCalverTagParts('rc-2020.01.01_a'); - it('should return null for invalid patch (letter instead of number)', () => { - expect(() => - getCalverTagParts('rc-2020.01.01_a'), - ).toThrowErrorMatchingInlineSnapshot(`"Invalid calver tag"`); + expect(result).toMatchInlineSnapshot(` + Object { + "error": Object { + "subtitle": "Expected calver matching \\"/(rc|version)-([0-9]{4}\\\\.[0-9]{2}\\\\.[0-9]{2})_([0-9]+)/\\", found \\"rc-2020.01.01_a\\"", + "title": "Invalid tag", + }, + } + `); + }); }); }); diff --git a/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.ts b/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.ts index 0fdd31444e..09f3b3e3d5 100644 --- a/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.ts +++ b/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { GitHubReleaseManagerError } from '../../errors/GitHubReleaseManagerError'; +import { AlertError } from '../../types/types'; export type CalverTagParts = { prefix: string; @@ -25,17 +25,26 @@ export type CalverTagParts = { export const calverRegexp = /(rc|version)-([0-9]{4}\.[0-9]{2}\.[0-9]{2})_([0-9]+)/; export function getCalverTagParts(tag: string) { - const result = tag.match(calverRegexp); + const match = tag.match(calverRegexp); - if (result === null || result.length < 4) { - throw new GitHubReleaseManagerError('Invalid calver tag'); + if (match === null || match.length < 4) { + const error: AlertError = { + title: 'Invalid tag', + subtitle: `Expected calver matching "${calverRegexp}", found "${tag}"`, + }; + + return { + error, + }; } const tagParts: CalverTagParts = { - prefix: result[1], - calver: result[2], - patch: parseInt(result[3], 10), + prefix: match[1], + calver: match[2], + patch: parseInt(match[3], 10), }; - return tagParts; + return { + tagParts, + }; } diff --git a/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.test.ts b/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.test.ts index 9977ab7add..4a5e218eaf 100644 --- a/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.test.ts +++ b/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.test.ts @@ -21,43 +21,80 @@ import { import { getSemverTagParts } from './getSemverTagParts'; describe('getSemverTagParts', () => { - it('should return tagParts for RC tag', () => - expect(getSemverTagParts(mockReleaseCandidateSemver.tagName)) - .toMatchInlineSnapshot(` - Object { - "major": 1, - "minor": 2, - "patch": 3, - "prefix": "rc", - } - `)); + describe('happy path', () => { + it('should return tagParts for RC tag', () => { + const semverTagParts = getSemverTagParts( + mockReleaseCandidateSemver.tagName, + ); - it('should return tagParts for Version tag', () => - expect(getSemverTagParts(mockReleaseVersionSemver.tagName)) - .toMatchInlineSnapshot(` - Object { - "major": 1, - "minor": 2, - "patch": 3, - "prefix": "version", - } - `)); + expect(semverTagParts).toMatchInlineSnapshot(` + Object { + "tagParts": Object { + "major": 1, + "minor": 2, + "patch": 3, + "prefix": "rc", + }, + } + `); + }); - it('should throw for invalid prefix', () => { - expect(() => - getSemverTagParts('invalid-1.2.3'), - ).toThrowErrorMatchingInlineSnapshot(`"Invalid semver tag"`); + it('should return tagParts for Version tag', () => { + const semverTagParts = getSemverTagParts( + mockReleaseVersionSemver.tagName, + ); + + expect(semverTagParts).toMatchInlineSnapshot(` + Object { + "tagParts": Object { + "major": 1, + "minor": 2, + "patch": 3, + "prefix": "version", + }, + } + `); + }); }); - it('should throw for invalid semver (missing patch)', () => { - expect(() => - getSemverTagParts('rc-1.2'), - ).toThrowErrorMatchingInlineSnapshot(`"Invalid semver tag"`); - }); + describe('invalid semver tags', () => { + it('should return error for invalid prefix', () => { + const semverTagParts = getSemverTagParts('invalid-1.2.3'); - it('should throw for invalid semver (founds calver)', () => { - expect(() => - getSemverTagParts('rc-1337.01.01_1'), - ).toThrowErrorMatchingInlineSnapshot(`"Invalid semver tag, found calver"`); + expect(semverTagParts).toMatchInlineSnapshot(` + Object { + "error": Object { + "subtitle": "Expected semver matching \\"/(rc|version)-([0-9]+)\\\\.([0-9]+)\\\\.([0-9]+)/\\", found \\"invalid-1.2.3\\"", + "title": "Invalid tag", + }, + } + `); + }); + + it('should return error for invalid semver (missing patch)', () => { + const semverTagParts = getSemverTagParts('rc-1.2'); + + expect(semverTagParts).toMatchInlineSnapshot(` + Object { + "error": Object { + "subtitle": "Expected semver matching \\"/(rc|version)-([0-9]+)\\\\.([0-9]+)\\\\.([0-9]+)/\\", found \\"rc-1.2\\"", + "title": "Invalid tag", + }, + } + `); + }); + + it('should return error for invalid semver (founds calver)', () => { + const semverTagParts = getSemverTagParts('rc-1337.01.01_1'); + + expect(semverTagParts).toMatchInlineSnapshot(` + Object { + "error": Object { + "subtitle": "Expected semver matching \\"/(rc|version)-([0-9]+)\\\\.([0-9]+)\\\\.([0-9]+)/\\", found calver \\"rc-1337.01.01_1\\"", + "title": "Invalid tag", + }, + } + `); + }); }); }); diff --git a/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.ts b/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.ts index 018dedcee0..8f5d53f488 100644 --- a/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.ts +++ b/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { GitHubReleaseManagerError } from '../../errors/GitHubReleaseManagerError'; +import { AlertError } from '../../types/types'; import { calverRegexp } from './getCalverTagParts'; export type SemverTagParts = { @@ -24,23 +24,41 @@ export type SemverTagParts = { patch: number; }; -export function getSemverTagParts(tag: string) { - const result = tag.match(/(rc|version)-([0-9]+)\.([0-9]+)\.([0-9]+)/); +export const semverRegexp = /(rc|version)-([0-9]+)\.([0-9]+)\.([0-9]+)/; - if (result === null || result.length < 4) { - throw new GitHubReleaseManagerError('Invalid semver tag'); +export function getSemverTagParts(tag: string) { + const match = tag.match(semverRegexp); + + if (match === null || match.length < 4) { + const error: AlertError = { + title: 'Invalid tag', + subtitle: `Expected semver matching "${semverRegexp}", found "${tag}"`, + }; + + return { + error, + }; } if (tag.match(calverRegexp)) { - throw new GitHubReleaseManagerError('Invalid semver tag, found calver'); + const error: AlertError = { + title: 'Invalid tag', + subtitle: `Expected semver matching "${semverRegexp}", found calver "${tag}"`, + }; + + return { + error, + }; } const tagParts: SemverTagParts = { - prefix: result[1], - major: parseInt(result[2], 10), - minor: parseInt(result[3], 10), - patch: parseInt(result[4], 10), + prefix: match[1], + major: parseInt(match[2], 10), + minor: parseInt(match[3], 10), + patch: parseInt(match[4], 10), }; - return tagParts; + return { + tagParts, + }; } diff --git a/plugins/github-release-manager/src/helpers/tagParts/validateTagName.ts b/plugins/github-release-manager/src/helpers/tagParts/validateTagName.ts new file mode 100644 index 0000000000..e7d6124c81 --- /dev/null +++ b/plugins/github-release-manager/src/helpers/tagParts/validateTagName.ts @@ -0,0 +1,47 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { getCalverTagParts } from './getCalverTagParts'; +import { getSemverTagParts } from './getSemverTagParts'; +import { Project } from '../../contexts/ProjectContext'; + +export const validateTagName = ({ + project, + tagName, +}: { + project: Project; + tagName?: string; +}) => { + if (!tagName) { + return { + tagNameError: null, + }; + } + + if (project.versioningStrategy === 'calver') { + const { error } = getCalverTagParts(tagName); + + return { + tagNameError: error, + }; + } + + const { error } = getSemverTagParts(tagName); + + return { + tagNameError: error, + }; +}; diff --git a/plugins/github-release-manager/src/helpers/tagParts/validateTagParts.test.ts b/plugins/github-release-manager/src/helpers/tagParts/validateTagParts.test.ts new file mode 100644 index 0000000000..cf0147db40 --- /dev/null +++ b/plugins/github-release-manager/src/helpers/tagParts/validateTagParts.test.ts @@ -0,0 +1,120 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { + mockCalverProject, + mockReleaseCandidateCalver, + mockReleaseCandidateSemver, + mockSemverProject, +} from '../../test-helpers/test-helpers'; +import { validateTagName } from './validateTagName'; + +describe('validateTagName', () => { + describe('valid tags', () => { + it('should not return any error for valid semver project', () => { + const result = validateTagName({ + project: mockSemverProject, + tagName: mockReleaseCandidateSemver.tagName, + }); + + expect(result).toMatchInlineSnapshot(` + Object { + "tagNameError": undefined, + } + `); + }); + + it('should not return any error for semver project without any releases (i.e. no tagName)', () => { + const result = validateTagName({ + project: mockSemverProject, + }); + + expect(result).toMatchInlineSnapshot(` + Object { + "tagNameError": null, + } + `); + }); + }); + + describe('mismatching tags', () => { + it('should return error for semver project and calver tag', () => { + const result = validateTagName({ + project: mockSemverProject, + tagName: mockReleaseCandidateCalver.tagName, + }); + + expect(result).toMatchInlineSnapshot(` + Object { + "tagNameError": Object { + "subtitle": "Expected semver matching \\"/(rc|version)-([0-9]+)\\\\.([0-9]+)\\\\.([0-9]+)/\\", found calver \\"rc-2020.01.01_1\\"", + "title": "Invalid tag", + }, + } + `); + }); + + it('should return error for calver project and semver tag', () => { + const result = validateTagName({ + project: mockCalverProject, + tagName: mockReleaseCandidateSemver.tagName, + }); + + expect(result).toMatchInlineSnapshot(` + Object { + "tagNameError": Object { + "subtitle": "Expected calver matching \\"/(rc|version)-([0-9]{4}\\\\.[0-9]{2}\\\\.[0-9]{2})_([0-9]+)/\\", found \\"rc-1.2.3\\"", + "title": "Invalid tag", + }, + } + `); + }); + }); + + describe('invalid tags', () => { + it('should return error for semver project and totally invalid tag', () => { + const result = validateTagName({ + project: mockSemverProject, + tagName: 'this-is-so-invalid', + }); + + expect(result).toMatchInlineSnapshot(` + Object { + "tagNameError": Object { + "subtitle": "Expected semver matching \\"/(rc|version)-([0-9]+)\\\\.([0-9]+)\\\\.([0-9]+)/\\", found \\"this-is-so-invalid\\"", + "title": "Invalid tag", + }, + } + `); + }); + + it('should return error for calver project and totally invalid tag', () => { + const result = validateTagName({ + project: mockCalverProject, + tagName: 'this-is-so-invalid', + }); + + expect(result).toMatchInlineSnapshot(` + Object { + "tagNameError": Object { + "subtitle": "Expected calver matching \\"/(rc|version)-([0-9]{4}\\\\.[0-9]{2}\\\\.[0-9]{2})_([0-9]+)/\\", found \\"this-is-so-invalid\\"", + "title": "Invalid tag", + }, + } + `); + }); + }); +}); diff --git a/plugins/github-release-manager/src/hooks/useVersioningStrategyMatchesRepoTags.ts b/plugins/github-release-manager/src/hooks/useVersioningStrategyMatchesRepoTags.ts index 321b5d4b83..032a5280a1 100644 --- a/plugins/github-release-manager/src/hooks/useVersioningStrategyMatchesRepoTags.ts +++ b/plugins/github-release-manager/src/hooks/useVersioningStrategyMatchesRepoTags.ts @@ -35,13 +35,9 @@ export const useVersioningStrategyMatchesRepoTags = ({ setVersioningStrategyMatches(false); if (latestReleaseTagName) { - try { - if (project.repo === repositoryName) { - getTagParts({ project, tag: latestReleaseTagName }); - setVersioningStrategyMatches(true); - } - } catch (error) { - setVersioningStrategyMatches(false); + if (project.repo === repositoryName) { + const { error } = getTagParts({ project, tag: latestReleaseTagName }); + setVersioningStrategyMatches(error === undefined); } } }, [latestReleaseTagName, project, repositoryName]); diff --git a/plugins/github-release-manager/src/types/types.ts b/plugins/github-release-manager/src/types/types.ts index c888c7e539..ece840fa5f 100644 --- a/plugins/github-release-manager/src/types/types.ts +++ b/plugins/github-release-manager/src/types/types.ts @@ -61,3 +61,8 @@ export interface CardHook { run: (args: RunArgs) => Promise; runInvoked: boolean; } + +export interface AlertError { + title?: string; + subtitle: string; +}