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 <erik.engervall@gmail.com>
This commit is contained in:
@@ -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<typeof getRcGitHubInfo>;
|
||||
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<GetComparison>;
|
||||
|
||||
type CreateRelease = (
|
||||
args: {
|
||||
nextGitHubInfo: ReturnType<typeof getRcGitHubInfo>;
|
||||
rcReleaseTag: string;
|
||||
releaseName: string;
|
||||
rcBranch: string;
|
||||
releaseBody: string;
|
||||
} & OwnerRepo,
|
||||
) => Promise<{
|
||||
|
||||
@@ -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 (
|
||||
<Alert severity="error">
|
||||
{tagNameError.title && <AlertTitle>{tagNameError.title}</AlertTitle>}
|
||||
{tagNameError.subtitle}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<RefetchContext.Provider value={{ refetchTrigger, setRefetchTrigger }}>
|
||||
<ErrorBoundary>
|
||||
|
||||
@@ -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 (
|
||||
<InfoCardPlus>
|
||||
<Typography variant="h4" className={classes.paragraph}>
|
||||
Create Release Candidate
|
||||
</Typography>
|
||||
{children}
|
||||
</InfoCardPlus>
|
||||
);
|
||||
};
|
||||
|
||||
export const CreateRc = ({
|
||||
defaultBranch,
|
||||
latestRelease,
|
||||
@@ -90,71 +102,28 @@ export const CreateRc = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (nextGitHubInfo.error !== undefined) {
|
||||
return (
|
||||
<InfoCardPlusWrapper>
|
||||
<Alert severity="error">
|
||||
{nextGitHubInfo.error.title && (
|
||||
<AlertTitle>{nextGitHubInfo.error.title}</AlertTitle>
|
||||
)}
|
||||
|
||||
{nextGitHubInfo.error.subtitle}
|
||||
</Alert>
|
||||
</InfoCardPlusWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const tagAlreadyExists =
|
||||
latestRelease !== null &&
|
||||
latestRelease.tagName === nextGitHubInfo.rcReleaseTag;
|
||||
const conflictingPreRelease =
|
||||
latestRelease !== null && latestRelease.prerelease;
|
||||
|
||||
function Description() {
|
||||
if (conflictingPreRelease) {
|
||||
return (
|
||||
<Alert className={classes.paragraph} severity="warning">
|
||||
The most recent release is already a Release Candidate
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
if (tagAlreadyExists) {
|
||||
return (
|
||||
<Alert className={classes.paragraph} severity="warning">
|
||||
There's already a tag named{' '}
|
||||
<strong>{nextGitHubInfo.rcReleaseTag}</strong>
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={classes.paragraph}>
|
||||
<Typography>
|
||||
<Differ
|
||||
icon="branch"
|
||||
current={releaseBranch?.name}
|
||||
next={nextGitHubInfo.rcBranch}
|
||||
/>
|
||||
</Typography>
|
||||
|
||||
<Typography>
|
||||
<Differ
|
||||
icon="tag"
|
||||
current={latestRelease?.tagName}
|
||||
next={nextGitHubInfo.rcReleaseTag}
|
||||
/>
|
||||
</Typography>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function CTA() {
|
||||
return (
|
||||
<Button
|
||||
data-testid={TEST_IDS.createRc.cta}
|
||||
disabled={conflictingPreRelease || tagAlreadyExists || runInvoked}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => run()}
|
||||
>
|
||||
Create Release Candidate
|
||||
</Button>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<InfoCardPlus>
|
||||
<Typography variant="h4" className={classes.paragraph}>
|
||||
Create Release Candidate
|
||||
</Typography>
|
||||
|
||||
<InfoCardPlusWrapper>
|
||||
{project.versioningStrategy === 'semver' &&
|
||||
latestRelease &&
|
||||
!conflictingPreRelease && (
|
||||
@@ -182,9 +151,50 @@ export const CreateRc = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Description />
|
||||
{conflictingPreRelease || tagAlreadyExists ? (
|
||||
<>
|
||||
{conflictingPreRelease && (
|
||||
<Alert className={classes.paragraph} severity="warning">
|
||||
The most recent release is already a Release Candidate
|
||||
</Alert>
|
||||
)}
|
||||
|
||||
<CTA />
|
||||
</InfoCardPlus>
|
||||
{tagAlreadyExists && (
|
||||
<Alert className={classes.paragraph} severity="warning">
|
||||
There's already a tag named{' '}
|
||||
<strong>{nextGitHubInfo.rcReleaseTag}</strong>
|
||||
</Alert>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<div className={classes.paragraph}>
|
||||
<Typography>
|
||||
<Differ
|
||||
icon="branch"
|
||||
current={releaseBranch?.name}
|
||||
next={nextGitHubInfo.rcBranch}
|
||||
/>
|
||||
</Typography>
|
||||
|
||||
<Typography>
|
||||
<Differ
|
||||
icon="tag"
|
||||
current={latestRelease?.tagName}
|
||||
next={nextGitHubInfo.rcReleaseTag}
|
||||
/>
|
||||
</Typography>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<Button
|
||||
data-testid={TEST_IDS.createRc.cta}
|
||||
disabled={conflictingPreRelease || tagAlreadyExists || runInvoked}
|
||||
variant="contained"
|
||||
color="primary"
|
||||
onClick={() => run()}
|
||||
>
|
||||
Create Release Candidate
|
||||
</Button>
|
||||
</InfoCardPlusWrapper>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -45,6 +45,16 @@ export function useCreateRc({
|
||||
project,
|
||||
successCb,
|
||||
}: CreateRC): CardHook<void> {
|
||||
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);
|
||||
|
||||
@@ -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 <NoLatestRelease />;
|
||||
}
|
||||
|
||||
if (releaseBranch === null) {
|
||||
return <NoLatestRelease />;
|
||||
}
|
||||
|
||||
const { bumpedTag, tagParts } = getBumpedTag({
|
||||
project,
|
||||
tag: latestRelease.tagName,
|
||||
bumpLevel: 'patch',
|
||||
});
|
||||
|
||||
return (
|
||||
<PatchBody
|
||||
bumpedTag={bumpedTag}
|
||||
latestRelease={latestRelease}
|
||||
releaseBranch={releaseBranch}
|
||||
successCb={successCb}
|
||||
tagParts={tagParts}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<InfoCardPlus>
|
||||
<Typography variant="h4" className={classes.paragraph}>
|
||||
Patch Release {latestRelease?.prerelease ? 'Candidate' : 'Version'}
|
||||
</Typography>
|
||||
|
||||
<Body />
|
||||
<BodyWrapper
|
||||
latestRelease={latestRelease}
|
||||
releaseBranch={releaseBranch}
|
||||
successCb={successCb}
|
||||
/>
|
||||
</InfoCardPlus>
|
||||
);
|
||||
};
|
||||
|
||||
function BodyWrapper({ latestRelease, releaseBranch, successCb }: PatchProps) {
|
||||
const { project } = useProjectContext();
|
||||
|
||||
if (latestRelease === null) {
|
||||
return <NoLatestRelease />;
|
||||
}
|
||||
|
||||
if (releaseBranch === null) {
|
||||
return <NoLatestRelease />;
|
||||
}
|
||||
|
||||
const bumpedTag = getBumpedTag({
|
||||
project,
|
||||
tag: latestRelease.tagName,
|
||||
bumpLevel: 'patch',
|
||||
});
|
||||
|
||||
if (bumpedTag.error !== undefined) {
|
||||
return (
|
||||
<Alert severity="error">
|
||||
{bumpedTag.error.title && (
|
||||
<AlertTitle>{bumpedTag.error.title}</AlertTitle>
|
||||
)}
|
||||
|
||||
{bumpedTag.error.subtitle}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<PatchBody
|
||||
bumpedTag={bumpedTag.bumpedTag}
|
||||
latestRelease={latestRelease}
|
||||
releaseBranch={releaseBranch}
|
||||
successCb={successCb}
|
||||
tagParts={bumpedTag.tagParts}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
+2
-10
@@ -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<any, any> } & TransitionProps,
|
||||
ref: Ref<unknown>,
|
||||
) {
|
||||
return <Slide direction="up" ref={ref} {...props} />;
|
||||
});
|
||||
|
||||
export const ResponseStepDialog = ({
|
||||
progress,
|
||||
responseSteps,
|
||||
|
||||
@@ -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<any, any> } & TransitionProps,
|
||||
ref: Ref<unknown>,
|
||||
) {
|
||||
return <Slide direction="up" ref={ref} {...props} />;
|
||||
});
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -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}`;
|
||||
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
};
|
||||
@@ -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",
|
||||
},
|
||||
}
|
||||
`);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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]);
|
||||
|
||||
@@ -61,3 +61,8 @@ export interface CardHook<RunArgs> {
|
||||
run: (args: RunArgs) => Promise<any>;
|
||||
runInvoked: boolean;
|
||||
}
|
||||
|
||||
export interface AlertError {
|
||||
title?: string;
|
||||
subtitle: string;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user