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; +}