Create additional stats for Stats component

Add button for fetching and calculating average release times

Signed-off-by: Erik Engervall <erik.engervall@gmail.com>
This commit is contained in:
Erik Engervall
2021-04-25 17:59:01 +02:00
parent e3c318ded1
commit c5bd6be684
32 changed files with 1319 additions and 601 deletions
@@ -23,6 +23,7 @@
"@backstage/core": "^0.7.5",
"@backstage/integration": "^0.5.1",
"@backstage/theme": "^0.2.5",
"recharts": "^1.8.5",
"@material-ui/core": "^4.11.0",
"@material-ui/icons": "^4.9.1",
"@material-ui/lab": "4.0.0-alpha.45",
@@ -39,6 +40,7 @@
"@backstage/dev-utils": "^0.1.13",
"@backstage/test-utils": "^0.1.10",
"@testing-library/jest-dom": "^5.10.1",
"@types/recharts": "^1.8.15",
"@testing-library/react-hooks": "^3.4.2",
"@testing-library/react": "^11.2.5",
"@testing-library/user-event": "^12.0.7",
@@ -591,7 +591,6 @@ ${selectedPatchCommit.commit.message}`,
});
return releases.map(release => ({
release,
id: release.id,
name: release.name,
tagName: release.tag_name,
@@ -19,7 +19,6 @@ import { Alert } from '@material-ui/lab';
import {
Box,
makeStyles,
Paper,
Table,
TableBody,
TableCell,
@@ -29,11 +28,11 @@ import {
} from '@material-ui/core';
import { CenteredCircularProgress } from '../../components/CenteredCircularProgress';
import { getMappedReleases } from './helpers/mapReleases';
import { getReleasesWithTags } from './helpers/getReleasesWithTags';
import { getSummary } from './helpers/getSummary';
import { getMappedReleases } from './helpers/getMappedReleases';
import { getReleaseStats } from './helpers/getReleaseStats';
import { Info } from './Info/Info';
import { ReleaseStatsContext } from './contexts/ReleaseStatsContext';
import { Row } from './Row/Row';
import { Summary } from './Summary';
import { useGetStats } from './hooks/useGetStats';
import { useProjectContext } from '../../contexts/ProjectContext';
import { Warn } from './Warn';
@@ -65,31 +64,31 @@ export function DialogBody() {
const { allReleases, allTags } = stats.value;
const { mappedReleases } = getMappedReleases({ allReleases, project });
const { releasesWithTags } = getReleasesWithTags({
const { releaseStats } = getReleaseStats({
mappedReleases,
allTags,
project,
});
const summary = getSummary({ releasesWithTags });
const shouldWarn =
releasesWithTags.unmappableTags.length > 0 ||
releasesWithTags.unmatchedTags.length > 0 ||
releasesWithTags.unmatched.length > 0;
releaseStats.unmappableTags.length > 0 ||
releaseStats.unmatchedTags.length > 0 ||
releaseStats.unmatchedReleases.length > 0;
if (shouldWarn) {
// eslint-disable-next-line no-console
console.log("⚠️ Here's a summary of unmapped/unmatched tags/releases", {
unmappableTags: releasesWithTags.unmappableTags,
unmatchedTags: releasesWithTags.unmatchedTags,
unmatchedReleases: releasesWithTags.unmatched,
unmatchedReleases: releaseStats.unmatchedReleases,
unmatchedTags: releaseStats.unmatchedTags,
unmappableTags: releaseStats.unmappableTags,
});
}
return (
<>
<Summary summary={summary} />
<ReleaseStatsContext.Provider value={{ releaseStats }}>
<Info />
<TableContainer component={Paper}>
<TableContainer>
<Table className={classes.table} size="small">
<TableHead>
<TableRow>
@@ -102,26 +101,22 @@ export function DialogBody() {
</TableHead>
<TableBody>
{Object.entries(releasesWithTags.releases).map(
([baseVersion, releaseWithTags], index) => {
{Object.entries(releaseStats.releases).map(
([baseVersion, releaseStat], index) => {
return (
<Row
key={`row-${index}`}
baseVersion={baseVersion}
releaseWithTags={releaseWithTags}
releaseStat={releaseStat}
/>
);
},
)}
</TableBody>
</Table>
</TableContainer>
<Box marginTop={2}>
{shouldWarn && (
<Warn releasesWithTags={releasesWithTags} project={project} />
)}
</Box>
</>
<Box marginTop={2}>{shouldWarn && <Warn />}</Box>
</TableContainer>
</ReleaseStatsContext.Provider>
);
}
@@ -0,0 +1,49 @@
/*
* 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 from 'react';
import { getDecimalNumber } from '../../helpers/getDecimalNumber';
import { useGetReleaseTimes } from '../hooks/useGetReleaseTimes';
export function AverageReleaseTime({
averageReleaseTime,
}: {
averageReleaseTime: ReturnType<
typeof useGetReleaseTimes
>['averageReleaseTime'];
}) {
if (averageReleaseTime.length === 0) {
return <>-</>;
}
const average = averageReleaseTime.reduce(
(acc, { daysWithHours }) => {
acc.daysWithHours += daysWithHours / averageReleaseTime.length;
return acc;
},
{ daysWithHours: 0 },
);
const days = Math.floor(average.daysWithHours);
const hours = getDecimalNumber((average.daysWithHours - days) * 24, 1);
return (
<>
{days} days {hours} hours
</>
);
}
@@ -0,0 +1,132 @@
/*
* 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 from 'react';
import {
Box,
Button,
Tooltip as MaterialTooltip,
Typography,
} from '@material-ui/core';
import { BarChart, Bar, XAxis, YAxis, Legend, Tooltip } from 'recharts';
import { AverageReleaseTime } from './AverageReleaseTime';
import { LinearProgressWithLabel } from '../../../../components/ResponseStepDialog/LinearProgressWithLabel';
import { LongestReleaseTime } from './LongestReleaseTime';
import { useGetReleaseTimes } from '../hooks/useGetReleaseTimes';
import { useReleaseStatsContext } from '../../contexts/ReleaseStatsContext';
export function InDepth() {
const { releaseStats } = useReleaseStatsContext();
const {
averageReleaseTime,
progress,
releaseCommitPairs,
run,
} = useGetReleaseTimes();
const skipped =
Object.keys(releaseStats.releases).length - releaseCommitPairs.length;
return (
<Box style={{ flex: 1 }}>
<Box margin={1}>
<Typography variant="h4">In-depth</Typography>
</Box>
<Box style={{ display: 'flex' }}>
<Box margin={1} style={{ display: 'flex', flex: 1 }}>
<Box>
<Typography variant="h6">Release time</Typography>
<Typography variant="body2">
<strong>Release time</strong> is derived by comparing{' '}
<i>createdAt</i> of the commits belonging to the first and last
tag of each release. Releases without patches will have tags
pointing towards the same commit and will thus be omitted. This
project will omit {skipped} out of the total{' '}
{Object.keys(releaseStats.releases).length} releases.
</Typography>
</Box>
</Box>
<Box
margin={1}
style={{ display: 'flex', flex: 1, flexDirection: 'column' }}
>
<Box>
<Typography variant="h6">In numbers</Typography>
<Typography variant="body2" color="textSecondary">
<strong>Average release time</strong>:{' '}
<AverageReleaseTime averageReleaseTime={averageReleaseTime} />
</Typography>
<Typography variant="body2" color="textSecondary">
<strong>Longest release</strong>:{' '}
<LongestReleaseTime averageReleaseTime={averageReleaseTime} />
</Typography>
</Box>
<Box marginTop={1}>
{progress === 0 && (
<MaterialTooltip
title={`This action will send ~${
releaseCommitPairs.length * 2
} requests`}
>
<Button
variant="contained"
color="secondary"
onClick={() => run()}
size="small"
>
Crunch the numbers
</Button>
</MaterialTooltip>
)}
</Box>
</Box>
</Box>
<Box marginTop={4}>
<BarChart
width={700}
height={70 + averageReleaseTime.length * 22}
data={
averageReleaseTime.length > 0
? averageReleaseTime
: [{ version: 'x.y.z', days: 0 }]
}
margin={{ top: 5, right: 30, left: 20, bottom: 5 }}
layout="vertical"
>
<XAxis type="number" />
<YAxis dataKey="version" type="category" />
<Tooltip labelStyle={{ color: '#000', fontWeight: 'bold' }} />
<Legend />
<Bar dataKey="days" fill="#82ca9d" />
</BarChart>
{progress > 0 && progress < 100 && (
<Box marginTop={1}>
<LinearProgressWithLabel progress={progress} responseSteps={[]} />
</Box>
)}
</Box>
</Box>
);
}
@@ -0,0 +1,43 @@
/*
* 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 from 'react';
import { getDecimalNumber } from '../../helpers/getDecimalNumber';
import { useGetReleaseTimes } from '../hooks/useGetReleaseTimes';
export function LongestReleaseTime({
averageReleaseTime,
}: {
averageReleaseTime: ReturnType<
typeof useGetReleaseTimes
>['averageReleaseTime'];
}) {
if (averageReleaseTime.length === 0) {
return <>-</>;
}
const longestRelease = [...averageReleaseTime].sort(
(a, b) => b.daysWithHours - a.daysWithHours,
)[0];
return (
<>
{longestRelease.version} ({longestRelease.days} days{' '}
{getDecimalNumber(longestRelease.hours, 1)} hours )
</>
);
}
@@ -0,0 +1,41 @@
/*
* 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 from 'react';
import { Paper } from '@material-ui/core';
import { InDepth } from './InDepth/InDepth';
import { Summary } from './Summary';
export function Info() {
return (
<Paper
variant="outlined"
style={{
padding: 20,
marginLeft: 25,
marginRight: 25,
marginBottom: 25,
display: 'flex',
flexDirection: 'column',
}}
>
<Summary />
<InDepth />
</Paper>
);
}
@@ -0,0 +1,108 @@
/*
* 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 from 'react';
import {
Box,
makeStyles,
Table,
TableBody,
TableCell,
TableContainer,
TableHead,
TableRow,
Typography,
} from '@material-ui/core';
import { getDecimalNumber } from '../helpers/getDecimalNumber';
import { getSummary } from '../helpers/getSummary';
import { useReleaseStatsContext } from '../contexts/ReleaseStatsContext';
const useStyles = makeStyles({
table: {
minWidth: 650,
},
});
export function Summary() {
const { releaseStats } = useReleaseStatsContext();
const { summary } = getSummary({ releaseStats });
const classes = useStyles();
return (
<Box margin={1}>
<Typography variant="h4">Summary</Typography>
<Typography variant="body2">
<strong>Total releases</strong>: {summary.totalReleases}
</Typography>
<TableContainer>
<Table size="small" className={classes.table}>
<TableHead>
<TableRow>
<TableCell />
<TableCell>Patches</TableCell>
<TableCell>Patches per release</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell component="th" scope="row">
Release Candidate
</TableCell>
<TableCell>{summary.totalCandidatePatches}</TableCell>
<TableCell>
{getDecimalNumber(
summary.totalCandidatePatches / summary.totalReleases,
)}
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
Release Version
</TableCell>
<TableCell>{summary.totalVersionPatches}</TableCell>
<TableCell>
{getDecimalNumber(
summary.totalVersionPatches / summary.totalReleases,
)}
</TableCell>
</TableRow>
<TableRow>
<TableCell component="th" scope="row">
Total
</TableCell>
<TableCell>
{summary.totalCandidatePatches + summary.totalVersionPatches}
</TableCell>
<TableCell>
{getDecimalNumber(
(summary.totalCandidatePatches +
summary.totalVersionPatches) /
summary.totalReleases,
)}
</TableCell>
</TableRow>
</TableBody>
</Table>
</TableContainer>
</Box>
);
}
@@ -0,0 +1,109 @@
/*
* 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 { getReleaseCommitPairs } from './getReleaseCommitPairs';
describe('getReleaseCommitPairs', () => {
it('should work', () => {
const nonPublishedRelease = {
baseVersion: '1.0',
createdAt: '2021-01-01T10:11:12Z',
htmlUrl: 'html_url',
candidates: [
{
tagName: 'rc-1.0.0',
sha: 'sha-1.0.0',
},
{
tagName: 'rc-1.0.1',
sha: 'sha-1.0.1',
},
],
versions: [],
};
const releaseWithoutPatches = {
baseVersion: '2.0',
createdAt: '2021-01-01T10:11:12Z',
htmlUrl: 'html_url',
candidates: [
{
tagName: 'rc-2.0.0',
sha: 'sha-2.0.0',
},
],
versions: [
{
tagName: 'version-2.0.0',
sha: 'sha-2.0.0',
},
],
};
const releaseWithPatches = {
baseVersion: '3.0',
createdAt: '2021-01-01T10:11:12Z',
htmlUrl: 'html_url',
candidates: [
{
tagName: 'rc-3.0.1',
sha: 'sha-3.0.1',
},
{
tagName: 'rc-3.0.0',
sha: 'sha-3.0.0',
},
],
versions: [
{
tagName: 'version-3.0.1',
sha: 'sha-3.0.1',
},
],
};
const result = getReleaseCommitPairs({
releaseStats: {
releases: {
nonPublishedRelease, // Should be omitted
releaseWithoutPatches, // Should be omitted
releaseWithPatches,
},
unmatchedReleases: [],
unmappableTags: [],
unmatchedTags: [],
},
});
expect(result).toMatchInlineSnapshot(`
Object {
"releaseCommitPairs": Array [
Object {
"baseVersion": "3.0",
"endCommit": Object {
"sha": "sha-3.0.1",
"tagName": "version-3.0.1",
},
"startCommit": Object {
"sha": "sha-3.0.0",
"tagName": "rc-3.0.0",
},
},
],
}
`);
});
});
@@ -0,0 +1,65 @@
/*
* 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 { ReleaseCommitPairs } from '../hooks/useGetReleaseTimes';
import { ReleaseStats } from '../../contexts/ReleaseStatsContext';
export function getReleaseCommitPairs({
releaseStats,
}: {
releaseStats: ReleaseStats;
}) {
const releaseCommitPairs = Object.values(releaseStats.releases).reduce(
(acc: ReleaseCommitPairs, release) => {
const startTag = [...release.candidates].reverse()[0];
const endTag = release.versions[0];
// Missing Release Candidate for unknown reason
if (!startTag?.sha) {
return acc;
}
// Missing Release Version (likely prerelease)
if (!endTag?.sha) {
return acc;
}
// First RC tag is pointing towards the same commit as the most
// recent Version tag, meaning there haven't been any patches
// and we thus cannot determine and difference in time
if (startTag?.sha === endTag?.sha) {
return acc;
}
return acc.concat({
baseVersion: release.baseVersion,
startCommit: {
tagName: startTag.tagName,
sha: startTag.sha,
},
endCommit: {
tagName: endTag.tagName,
sha: endTag.sha,
},
});
},
[],
);
return {
releaseCommitPairs,
};
}
@@ -0,0 +1,127 @@
/*
* 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 { useEffect, useState } from 'react';
import { useAsync, useAsyncFn } from 'react-use';
import { DateTime } from 'luxon';
import { getReleaseCommitPairs } from '../helpers/getReleaseCommitPairs';
import { usePluginApiClientContext } from '../../../../contexts/PluginApiClientContext';
import { useProjectContext } from '../../../../contexts/ProjectContext';
import { useReleaseStatsContext } from '../../contexts/ReleaseStatsContext';
export type ReleaseCommitPairs = Array<{
baseVersion: string;
startCommit: {
tagName: string;
sha: string;
};
endCommit: {
tagName: string;
sha: string;
};
}>;
type ReleaseTime = {
version: string;
daysWithHours: number;
days: number;
hours: number;
startCommitCreatedAt?: string;
endCommitCreatedAt?: string;
};
export function useGetReleaseTimes() {
const { pluginApiClient } = usePluginApiClientContext();
const { project } = useProjectContext();
const { releaseStats } = useReleaseStatsContext();
const [averageReleaseTime, setAverageReleaseTime] = useState<ReleaseTime[]>(
[],
);
const [progress, setProgress] = useState(0);
const { releaseCommitPairs } = getReleaseCommitPairs({ releaseStats });
const [fnRes, run] = useAsyncFn(() => {
setProgress(0);
return getAndSetReleaseTime(0);
});
useAsync(async () => {
if (averageReleaseTime.length === 0) return;
if (releaseCommitPairs.length === averageReleaseTime.length) return;
await getAndSetReleaseTime(averageReleaseTime.length);
}, [fnRes.value, averageReleaseTime]);
useEffect(() => {
const unboundedProgress = Math.round(
(averageReleaseTime.length / releaseCommitPairs.length) * 100,
);
const boundedProgress = unboundedProgress > 100 ? 100 : unboundedProgress;
setProgress(boundedProgress);
}, [averageReleaseTime.length, releaseCommitPairs.length]);
async function getAndSetReleaseTime(index: number) {
const { baseVersion, startCommit, endCommit } = releaseCommitPairs[index];
const [
{ createdAt: startCommitCreatedAt },
{ createdAt: endCommitCreatedAt },
] = await Promise.all([
pluginApiClient.stats.getCommit({
owner: project.owner,
repo: project.repo,
ref: startCommit.sha,
}),
pluginApiClient.stats.getCommit({
owner: project.owner,
repo: project.repo,
ref: endCommit.sha,
}),
]);
const releaseTime: ReleaseTime = {
version: baseVersion,
daysWithHours: 0,
days: 0,
hours: 0,
startCommitCreatedAt,
endCommitCreatedAt,
};
if (startCommitCreatedAt && endCommitCreatedAt) {
const { days: luxDays = 0, hours: luxHours = 0 } = DateTime.fromISO(
endCommitCreatedAt,
)
.diff(DateTime.fromISO(startCommitCreatedAt), ['days', 'hours'])
.toObject();
releaseTime.daysWithHours = luxDays + luxHours / 24;
releaseTime.days = luxDays;
releaseTime.hours = luxHours;
}
setAverageReleaseTime([...averageReleaseTime, releaseTime]);
}
return {
releaseCommitPairs,
averageReleaseTime,
progress,
run,
};
}
@@ -27,7 +27,7 @@ import {
import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown';
import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp';
import { getReleasesWithTags } from '../helpers/getReleasesWithTags';
import { ReleaseStats } from '../contexts/ReleaseStatsContext';
import { RowCollapsed } from './RowCollapsed/RowCollapsed';
const useRowStyles = makeStyles({
@@ -40,17 +40,15 @@ const useRowStyles = makeStyles({
interface RowProps {
baseVersion: string;
releaseWithTags: ReturnType<
typeof getReleasesWithTags
>['releasesWithTags']['releases']['0'];
releaseStat: ReleaseStats['releases']['0'];
}
export function Row({ baseVersion, releaseWithTags }: RowProps) {
export function Row({ baseVersion, releaseStat }: RowProps) {
const [open, setOpen] = useState(false);
const classes = useRowStyles();
return (
<React.Fragment>
<>
<TableRow className={classes.root}>
<TableCell>
<IconButton
@@ -63,34 +61,32 @@ export function Row({ baseVersion, releaseWithTags }: RowProps) {
</TableCell>
<TableCell component="th" scope="row">
<Link href={releaseWithTags.htmlUrl} target="_blank">
<Link href={releaseStat.htmlUrl} target="_blank">
{baseVersion}
{releaseWithTags.versions.length === 0 ? ' (prerelease)' : ''}
{releaseStat.versions.length === 0 ? ' (prerelease)' : ''}
</Link>
</TableCell>
<TableCell>
{releaseWithTags.createdAt
? DateTime.fromISO(releaseWithTags.createdAt)
{releaseStat.createdAt
? DateTime.fromISO(releaseStat.createdAt)
.setLocale('sv-SE')
.toFormat('yyyy-MM-dd')
: '-'}
</TableCell>
<TableCell>{releaseWithTags.candidates.length}</TableCell>
<TableCell>{releaseStat.candidates.length}</TableCell>
<TableCell>
{Math.max(0, releaseWithTags.versions.length - 1)}
</TableCell>
<TableCell>{Math.max(0, releaseStat.versions.length - 1)}</TableCell>
</TableRow>
<TableRow>
<TableCell style={{ paddingBottom: 0, paddingTop: 0 }} colSpan={6}>
<Collapse in={open} timeout="auto" unmountOnExit>
<RowCollapsed releaseWithTags={releaseWithTags} />
<RowCollapsed releaseStat={releaseStat} />
</Collapse>
</TableCell>
</TableRow>
</React.Fragment>
</>
);
}
@@ -17,14 +17,12 @@
import React from 'react';
import { Box, Typography } from '@material-ui/core';
import { getReleasesWithTags } from '../../helpers/getReleasesWithTags';
import { ReleaseStats } from '../../contexts/ReleaseStatsContext';
export function ReleaseTagList({
releaseWithTags,
releaseStat,
}: {
releaseWithTags: ReturnType<
typeof getReleasesWithTags
>['releasesWithTags']['releases']['0'];
releaseStat: ReleaseStats['releases']['0'];
}) {
return (
<Box
@@ -36,9 +34,9 @@ export function ReleaseTagList({
justifyContent: 'center',
}}
>
{releaseWithTags.versions.length > 0 && (
{releaseStat.versions.length > 0 && (
<Box style={{ position: 'relative' }}>
{releaseWithTags.versions.map(version => (
{releaseStat.versions.map(version => (
<Typography variant="body1" key={version.tagName}>
{version.tagName}
</Typography>
@@ -46,7 +44,7 @@ export function ReleaseTagList({
</Box>
)}
{releaseWithTags.versions.length > 0 && (
{releaseStat.versions.length > 0 && (
<Box
margin={1}
style={{
@@ -60,7 +58,7 @@ export function ReleaseTagList({
)}
<Box style={{ position: 'relative' }}>
{releaseWithTags.candidates.map(candidate => (
{releaseStat.candidates.map(candidate => (
<Typography variant="body1" key={candidate.tagName}>
{candidate.tagName}
</Typography>
@@ -15,27 +15,23 @@
*/
import React from 'react';
import { Box, Typography } from '@material-ui/core';
import { DateTime } from 'luxon';
import { CenteredCircularProgress } from '../../../../components/CenteredCircularProgress';
import { getReleasesWithTags } from '../../helpers/getReleasesWithTags';
import { useGetCommit } from '../../hooks/useGetCommit';
import { Box, Typography } from '@material-ui/core';
import { Alert } from '@material-ui/lab';
import { CenteredCircularProgress } from '../../../../components/CenteredCircularProgress';
import { ReleaseStats } from '../../contexts/ReleaseStatsContext';
import { useGetCommit } from '../../hooks/useGetCommit';
interface ReleaseTimeProps {
releaseWithTags: ReturnType<
typeof getReleasesWithTags
>['releasesWithTags']['releases']['0'];
releaseStat: ReleaseStats['releases']['0'];
}
export function ReleaseTime({ releaseWithTags }: ReleaseTimeProps) {
const reversedCandidates = [...releaseWithTags.candidates].reverse();
const firstCandidateSha = reversedCandidates[0]?.sha;
export function ReleaseTime({ releaseStat }: ReleaseTimeProps) {
const firstCandidateSha = [...releaseStat.candidates].reverse()[0]?.sha;
const { commit: releaseCut } = useGetCommit({ ref: firstCandidateSha });
const mostRecentVersionSha = releaseWithTags.versions[0]?.sha;
const mostRecentVersionSha = releaseStat.versions[0]?.sha;
const { commit: releaseComplete } = useGetCommit({
ref: mostRecentVersionSha,
});
@@ -57,15 +53,6 @@ export function ReleaseTime({ releaseWithTags }: ReleaseTimeProps) {
);
}
if (releaseComplete.error) {
return (
<Alert severity="error">
Failed to fetch the final Release Version Commit (
{releaseComplete.error.message})
</Alert>
);
}
const diff =
releaseCut.value?.createdAt && releaseComplete.value?.createdAt
? DateTime.fromISO(releaseComplete.value.createdAt)
@@ -83,7 +70,7 @@ export function ReleaseTime({ releaseWithTags }: ReleaseTimeProps) {
}}
>
<Typography variant="body1">
Release completed{' '}
{releaseStat.versions.length === 0 ? '-' : 'Release completed '}
{releaseComplete.value?.createdAt &&
DateTime.fromISO(releaseComplete.value.createdAt)
.setLocale('sv-SE')
@@ -99,7 +86,11 @@ export function ReleaseTime({ releaseWithTags }: ReleaseTimeProps) {
}}
>
<Typography variant="h5" color="secondary">
Release time: {diff.days} days
{diff.days === -1 ? (
<>Ongoing: {diff.days} days</>
) : (
<>Completed in: {diff.days} days</>
)}
</Typography>
</Box>
@@ -16,17 +16,15 @@
import React from 'react';
import { Box } from '@material-ui/core';
import { getReleasesWithTags } from '../../helpers/getReleasesWithTags';
import { ReleaseTime } from './ReleaseTime';
import { ReleaseStats } from '../../contexts/ReleaseStatsContext';
import { ReleaseTagList } from './ReleaseTagList';
import { ReleaseTime } from './ReleaseTime';
interface RowCollapsedProps {
releaseWithTags: ReturnType<
typeof getReleasesWithTags
>['releasesWithTags']['releases']['0'];
releaseStat: ReleaseStats['releases']['0'];
}
export function RowCollapsed({ releaseWithTags }: RowCollapsedProps) {
export function RowCollapsed({ releaseStat }: RowCollapsedProps) {
return (
<Box
margin={1}
@@ -37,9 +35,9 @@ export function RowCollapsed({ releaseWithTags }: RowCollapsedProps) {
paddingRight: '10%',
}}
>
<ReleaseTagList releaseWithTags={releaseWithTags} />
<ReleaseTagList releaseStat={releaseStat} />
<ReleaseTime releaseWithTags={releaseWithTags} />
<ReleaseTime releaseStat={releaseStat} />
</Box>
);
}
@@ -1,89 +0,0 @@
/*
* 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 from 'react';
import { Box, Paper, Typography } from '@material-ui/core';
import { getDecimalNumber } from './helpers/getDecimalNumber';
import { getSummary } from './helpers/getSummary';
interface SummaryProps {
summary: ReturnType<typeof getSummary>;
}
export function Summary({ summary }: SummaryProps) {
return (
<Paper
variant="outlined"
style={{
padding: 20,
marginLeft: 25,
marginRight: 25,
marginBottom: 25,
}}
>
<Box margin={1}>
<Typography variant="h4">Summary</Typography>
<Typography variant="body2">
Total releases: {summary.totalReleases}
</Typography>
</Box>
<Box margin={1}>
<Typography variant="h6">Release Candidate</Typography>
<Typography variant="body2">
Release Candidate patches: {summary.totalCandidatePatches}
</Typography>
<Typography variant="body2">
Release Candidate patches per release:{' '}
{getDecimalNumber(
summary.totalCandidatePatches / summary.totalReleases,
)}
</Typography>
</Box>
<Box margin={1}>
<Typography variant="h6">Release Version</Typography>
<Typography variant="body2">
Release Version patches: {summary.totalVersionPatches}
</Typography>
<Typography variant="body2">
Release Version patches per release:{' '}
{getDecimalNumber(
summary.totalVersionPatches / summary.totalReleases,
)}
</Typography>
</Box>
<Box margin={1}>
<Typography variant="h6">Total</Typography>
<Typography variant="body2">
Patches: {summary.totalCandidatePatches + summary.totalVersionPatches}
</Typography>
<Typography variant="body2">
Patches per release:{' '}
{getDecimalNumber(
(summary.totalCandidatePatches + summary.totalVersionPatches) /
summary.totalReleases,
)}
</Typography>
</Box>
</Paper>
);
}
@@ -17,39 +17,37 @@
import React from 'react';
import { Alert } from '@material-ui/lab';
import { getReleasesWithTags } from './helpers/getReleasesWithTags';
import { Project } from '../../contexts/ProjectContext';
import { useProjectContext } from '../../contexts/ProjectContext';
import { useReleaseStatsContext } from './contexts/ReleaseStatsContext';
interface WarnProps {
releasesWithTags: ReturnType<typeof getReleasesWithTags>['releasesWithTags'];
project: Project;
}
export const Warn = () => {
const { releaseStats } = useReleaseStatsContext();
const { project } = useProjectContext();
export const Warn = ({ releasesWithTags, project }: WarnProps) => {
return (
<Alert severity="warning" style={{ marginBottom: 10 }}>
{releasesWithTags.unmappableTags.length > 0 && (
{releaseStats.unmappableTags.length > 0 && (
<div>
Failed to map{' '}
<strong>{releasesWithTags.unmappableTags.length}</strong> tags to
releases
Failed to map <strong>{releaseStats.unmappableTags.length}</strong>{' '}
tags to releases
</div>
)}
{releasesWithTags.unmatchedTags.length > 0 && (
{releaseStats.unmatchedTags.length > 0 && (
<div>
Failed to match <strong>{releaseStats.unmatchedTags.length}</strong>{' '}
tags to {project.versioningStrategy}
</div>
)}
{releaseStats.unmatchedReleases.length > 0 && (
<div>
Failed to match{' '}
<strong>{releasesWithTags.unmatchedTags.length}</strong> tags to{' '}
<strong>{releaseStats.unmatchedReleases.length}</strong> releases to{' '}
{project.versioningStrategy}
</div>
)}
{releasesWithTags.unmatched.length > 0 && (
<div>
Failed to match <strong>{releasesWithTags.unmatched.length}</strong>{' '}
releases to {project.versioningStrategy}
</div>
)}
<div>See full output in the console</div>
</Alert>
);
@@ -0,0 +1,64 @@
/*
* 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 { createContext, useContext } from 'react';
import { GitHubReleaseManagerError } from '../../../errors/GitHubReleaseManagerError';
export interface ReleaseStats {
unmappableTags: string[];
unmatchedTags: string[];
unmatchedReleases: string[];
releases: {
[baseVersion: string]: {
baseVersion: string;
createdAt: string | null;
htmlUrl: string;
/**
* Ordered from new to old
*/
candidates: {
tagName: string;
sha: string;
}[];
/**
* Ordered from new to old
*/
versions: {
tagName: string;
sha: string;
}[];
};
};
}
export const ReleaseStatsContext = createContext<
{ releaseStats: ReleaseStats } | undefined
>(undefined);
export const useReleaseStatsContext = () => {
const { releaseStats } = useContext(ReleaseStatsContext) ?? {};
if (!releaseStats) {
throw new GitHubReleaseManagerError('releaseStats not found');
}
return {
releaseStats,
};
};
@@ -0,0 +1,43 @@
/*
* 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 { getDecimalNumber } from './getDecimalNumber';
describe('getDecimalNumber', () => {
it('should handle NaN', () => {
const result = getDecimalNumber(NaN);
expect(result).toEqual(0);
});
it('should only handle decimals', () => {
const result = getDecimalNumber(1);
expect(result).toEqual(1);
});
it('should get decimal number with default decimals = 2', () => {
const result = getDecimalNumber(1 / 3);
expect(result).toMatchInlineSnapshot(`0.33`);
});
it('should get decimal number for decimals = 1', () => {
const result = getDecimalNumber(1 / 3, 1);
expect(result).toMatchInlineSnapshot(`0.3`);
});
});
@@ -14,13 +14,13 @@
* limitations under the License.
*/
export function getDecimalNumber(n: number) {
export function getDecimalNumber(n: number, decimals = 2) {
if (isNaN(n)) {
return 0;
}
if (n.toString().includes('.')) {
return n.toFixed(2);
return parseFloat(n.toFixed(decimals));
}
return n;
@@ -0,0 +1,71 @@
/*
* 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 { getMappedReleases } from './getMappedReleases';
import { mockSemverProject } from '../../../test-helpers/test-helpers';
describe('getMappedReleases', () => {
it('should get mapped releases', () => {
const createRelease = (tagName: string) => ({
createdAt: '2021-01-01T10:11:12Z',
htmlUrl: 'html_url',
id: 1,
name: 'name',
tagName,
});
const result = getMappedReleases({
project: mockSemverProject,
allReleases: [createRelease('rc-1.0.0'), createRelease('rc-1.1.0')],
});
expect(result).toMatchInlineSnapshot(`
Object {
"mappedReleases": Object {
"releases": Object {
"1.0": Object {
"baseVersion": "1.0",
"candidates": Array [
Object {
"sha": "",
"tagName": "rc-1.0.0",
},
],
"createdAt": "2021-01-01T10:11:12Z",
"htmlUrl": "html_url",
"versions": Array [],
},
"1.1": Object {
"baseVersion": "1.1",
"candidates": Array [
Object {
"sha": "",
"tagName": "rc-1.1.0",
},
],
"createdAt": "2021-01-01T10:11:12Z",
"htmlUrl": "html_url",
"versions": Array [],
},
},
"unmappableTags": Array [],
"unmatchedReleases": Array [],
"unmatchedTags": Array [],
},
}
`);
});
});
@@ -17,6 +17,7 @@
import { calverRegexp } from '../../../helpers/tagParts/getCalverTagParts';
import { GetAllReleasesResult } from '../../../api/PluginApiClient';
import { Project } from '../../../contexts/ProjectContext';
import { ReleaseStats } from '../contexts/ReleaseStatsContext';
import { semverRegexp } from '../../../helpers/tagParts/getSemverTagParts';
export function getMappedReleases({
@@ -28,61 +29,47 @@ export function getMappedReleases({
}) {
return {
mappedReleases: allReleases.reduce(
(
acc: {
unmatched: string[];
releases: {
[baseVersion: string]: {
createdAt: string | null;
candidates: {
tagName: string;
sha: string;
}[];
versions: {
tagName: string;
sha: string;
}[];
htmlUrl: string;
};
};
},
release,
) => {
(acc: ReleaseStats, release) => {
const match =
project.versioningStrategy === 'semver'
? release.tagName.match(semverRegexp)
: release.tagName.match(calverRegexp);
if (!match) {
acc.unmatched.push(release.tagName);
acc.unmatchedReleases.push(release.tagName);
return acc;
}
const prefix = match[1];
const prefix = match[1] as 'rc' | 'version';
const baseVersion =
project.versioningStrategy === 'semver'
? `${match[2]}.${match[3]}`
: match[2];
if (!acc.releases[baseVersion]) {
acc.releases[baseVersion] = {
createdAt: release.createdAt,
candidates:
prefix === 'rc' ? [{ tagName: release.tagName, sha: '' }] : [],
versions:
prefix === 'version'
? [{ tagName: release.tagName, sha: '' }]
: [],
htmlUrl: release.htmlUrl,
const releaseEntry = {
tagName: release.tagName,
sha: '',
};
acc.releases[baseVersion] = {
baseVersion,
createdAt: release.createdAt,
htmlUrl: release.htmlUrl,
candidates: prefix === 'rc' ? [releaseEntry] : [],
versions: prefix === 'version' ? [releaseEntry] : [],
};
return acc;
}
return acc;
},
{
unmatched: [],
releases: {},
unmappableTags: [],
unmatchedReleases: [],
unmatchedTags: [],
},
),
};
@@ -0,0 +1,124 @@
/*
* 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 { getReleaseStats } from './getReleaseStats';
import { mockSemverProject } from '../../../test-helpers/test-helpers';
describe('getReleaseStats', () => {
it('should get releases with tags', () => {
const result = getReleaseStats({
project: mockSemverProject,
mappedReleases: {
releases: {
'1.0': {
baseVersion: '1.0',
createdAt: '2021-01-01T10:11:12Z',
htmlUrl: 'html_url',
candidates: [
{
tagName: 'rc-1.0.0',
sha: '',
},
],
versions: [],
},
'1.1': {
baseVersion: '1.1',
createdAt: '2021-01-01T10:11:12Z',
htmlUrl: 'html_url',
candidates: [
{
tagName: 'rc-1.1.0',
sha: '',
},
],
versions: [],
},
},
unmappableTags: [],
unmatchedReleases: [],
unmatchedTags: [],
},
allTags: [
{ sha: 'sha', tagName: 'rc-1.0.0' },
{ sha: 'sha', tagName: 'rc-1.0.1' },
{ sha: 'sha', tagName: 'rc-1.0.2' },
{ sha: 'sha', tagName: 'version-1.0.2' },
{ sha: 'sha', tagName: 'rc-1.1.1' },
{ sha: 'unmatchable', tagName: 'rc-1/2/3' },
{ sha: 'unmappable', tagName: 'rc-123.123.123' },
],
});
expect(result).toMatchInlineSnapshot(`
Object {
"releaseStats": Object {
"releases": Object {
"1.0": Object {
"baseVersion": "1.0",
"candidates": Array [
Object {
"sha": "sha",
"tagName": "rc-1.0.0",
},
Object {
"sha": "sha",
"tagName": "rc-1.0.1",
},
Object {
"sha": "sha",
"tagName": "rc-1.0.2",
},
],
"createdAt": "2021-01-01T10:11:12Z",
"htmlUrl": "html_url",
"versions": Array [
Object {
"sha": "sha",
"tagName": "version-1.0.2",
},
],
},
"1.1": Object {
"baseVersion": "1.1",
"candidates": Array [
Object {
"sha": "",
"tagName": "rc-1.1.0",
},
Object {
"sha": "sha",
"tagName": "rc-1.1.1",
},
],
"createdAt": "2021-01-01T10:11:12Z",
"htmlUrl": "html_url",
"versions": Array [],
},
},
"unmappableTags": Array [
"rc-123.123.123",
],
"unmatchedReleases": Array [],
"unmatchedTags": Array [
"rc-1/2/3",
],
},
}
`);
});
});
@@ -0,0 +1,81 @@
/*
* 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 { calverRegexp } from '../../../helpers/tagParts/getCalverTagParts';
import { GetAllTagsResult } from '../../../api/PluginApiClient';
import { Project } from '../../../contexts/ProjectContext';
import { ReleaseStats } from '../contexts/ReleaseStatsContext';
import { semverRegexp } from '../../../helpers/tagParts/getSemverTagParts';
export function getReleaseStats({
allTags,
project,
mappedReleases,
}: {
allTags: GetAllTagsResult;
project: Project;
mappedReleases: ReleaseStats;
}) {
const releaseStats = allTags.reduce(
(acc: ReleaseStats, tag) => {
const match =
project.versioningStrategy === 'semver'
? tag.tagName.match(semverRegexp)
: tag.tagName.match(calverRegexp);
if (!match) {
acc.unmatchedTags.push(tag.tagName);
return acc;
}
const prefix = match[1] as 'rc' | 'version';
const baseVersion =
project.versioningStrategy === 'semver'
? `${match[2]}.${match[3]}` // major.minor
: match[2]; // yyyy.MM.dd
const release = acc.releases[baseVersion];
if (!release) {
acc.unmappableTags.push(tag.tagName);
return acc;
}
const dest = release[prefix === 'rc' ? 'candidates' : 'versions'];
const releaseToEnrich = dest.find(
({ tagName }) => tagName === tag.tagName,
);
if (releaseToEnrich) {
releaseToEnrich.sha = tag.sha;
} else {
dest.push({
tagName: tag.tagName,
sha: tag.sha,
});
}
return acc;
},
{
...mappedReleases,
},
);
return {
releaseStats,
};
}
@@ -1,101 +0,0 @@
/*
* 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 { calverRegexp } from '../../../helpers/tagParts/getCalverTagParts';
import { GetAllTagsResult } from '../../../api/PluginApiClient';
import { getMappedReleases } from './mapReleases';
import { Project } from '../../../contexts/ProjectContext';
import { semverRegexp } from '../../../helpers/tagParts/getSemverTagParts';
export function getReleasesWithTags({
allTags,
project,
mappedReleases,
}: {
allTags: GetAllTagsResult;
project: Project;
mappedReleases: ReturnType<typeof getMappedReleases>['mappedReleases'];
}) {
return {
releasesWithTags: allTags.reduce(
(
acc: ReturnType<typeof getMappedReleases>['mappedReleases'] & {
unmatchedTags: string[];
unmappableTags: string[];
},
tag,
) => {
const match =
project.versioningStrategy === 'semver'
? tag.tagName.match(semverRegexp)
: tag.tagName.match(calverRegexp);
if (!match) {
acc.unmatchedTags.push(tag.tagName);
return acc;
}
const prefix = match[1];
const baseVersion =
project.versioningStrategy === 'semver'
? `${match[2]}.${match[3]}`
: match[2];
if (!acc.releases[baseVersion]) {
acc.unmappableTags.push(tag.tagName);
return acc;
}
if (prefix === 'rc') {
const existingEntry = acc.releases[baseVersion].candidates.find(
({ tagName }) => tagName === tag.tagName,
);
if (existingEntry) {
existingEntry.sha = tag.sha;
} else {
acc.releases[baseVersion].candidates.push({
tagName: tag.tagName,
sha: tag.sha,
});
}
}
if (prefix === 'version') {
const existingEntry = acc.releases[baseVersion].versions.find(
({ tagName }) => tagName === tag.tagName,
);
if (existingEntry) {
existingEntry.sha = tag.sha;
} else {
acc.releases[baseVersion].versions.push({
tagName: tag.tagName,
sha: tag.sha,
});
}
}
return acc;
},
{
...mappedReleases,
unmatchedTags: [],
unmappableTags: [],
},
),
};
}
@@ -0,0 +1,34 @@
/*
* 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 { getSummary } from './getSummary';
import { mockReleaseStats } from '../../../test-helpers/stats';
describe('getSummary', () => {
it('should get summary', () => {
const result = getSummary({ releaseStats: mockReleaseStats });
expect(result).toMatchInlineSnapshot(`
Object {
"summary": Object {
"totalCandidatePatches": 3,
"totalReleases": 2,
"totalVersionPatches": 1,
},
}
`);
});
});
@@ -14,32 +14,35 @@
* limitations under the License.
*/
import { getReleasesWithTags } from './getReleasesWithTags';
import { ReleaseStats } from '../contexts/ReleaseStatsContext';
export function getSummary({
releasesWithTags,
}: {
releasesWithTags: ReturnType<typeof getReleasesWithTags>['releasesWithTags'];
}) {
return Object.entries(releasesWithTags.releases).reduce(
(
acc: {
totalReleases: number;
totalCandidatePatches: number;
totalVersionPatches: number;
export function getSummary({ releaseStats }: { releaseStats: ReleaseStats }) {
return {
summary: Object.entries(releaseStats.releases).reduce(
(
acc: {
totalReleases: number;
totalCandidatePatches: number;
totalVersionPatches: number;
},
[_baseVersion, mappedRelease],
) => {
const candidatePatches =
Object.keys(mappedRelease.candidates).length - 1;
const versionPatches = Object.keys(mappedRelease.versions).length - 1;
acc.totalReleases += 1;
acc.totalCandidatePatches +=
candidatePatches >= 0 ? candidatePatches : 0;
acc.totalVersionPatches += versionPatches >= 0 ? versionPatches : 0;
return acc;
},
[_baseVersion, mappedRelease],
) => {
acc.totalReleases += 1;
acc.totalCandidatePatches += mappedRelease.candidates.length - 1;
acc.totalVersionPatches += mappedRelease.versions.length - 1;
return acc;
},
{
totalReleases: 0,
totalCandidatePatches: 0,
totalVersionPatches: 0,
},
);
{
totalReleases: 0,
totalCandidatePatches: 0,
totalVersionPatches: 0,
},
),
};
}
@@ -16,20 +16,25 @@
import { useAsync } from 'react-use';
import { GitHubReleaseManagerError } from '../../../errors/GitHubReleaseManagerError';
import { usePluginApiClientContext } from '../../../contexts/PluginApiClientContext';
import { useProjectContext } from '../../../contexts/ProjectContext';
export const useGetCommit = ({ ref }: { ref: string }) => {
export const useGetCommit = ({ ref }: { ref?: string }) => {
const { pluginApiClient } = usePluginApiClientContext();
const { project } = useProjectContext();
const commit = useAsync(() =>
pluginApiClient.stats.getCommit({
const commit = useAsync(async () => {
if (!ref) {
throw new GitHubReleaseManagerError('Missing ref to get commit');
}
return pluginApiClient.stats.getCommit({
owner: project.owner,
repo: project.repo,
ref,
}),
);
});
});
return {
commit,
@@ -0,0 +1,70 @@
/*
* 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 { ReleaseStats } from '../features/Stats/contexts/ReleaseStatsContext';
export const mockReleaseStats: ReleaseStats = {
releases: {
'1.0': {
baseVersion: '1.0',
createdAt: '2021-01-01T10:11:12Z',
htmlUrl: 'html_url',
candidates: [
{
tagName: 'rc-1.0.1',
sha: 'sha-1.0.1',
},
{
tagName: 'rc-1.0.0',
sha: 'sha-1.0.0',
},
],
versions: [],
},
'1.1': {
baseVersion: '1.1',
createdAt: '2021-01-01T10:11:12Z',
htmlUrl: 'html_url',
candidates: [
{
tagName: 'rc-1.1.2',
sha: 'sha-1.1.2',
},
{
tagName: 'rc-1.1.1',
sha: 'sha-1.1.1',
},
{
tagName: 'rc-1.1.0',
sha: 'sha-1.1.0',
},
],
versions: [
{
tagName: 'version-1.1.3',
sha: 'sha-1.1.3',
},
{
tagName: 'version-1.1.2',
sha: 'sha-1.1.2',
},
],
},
},
unmappableTags: [],
unmatchedReleases: [],
unmatchedTags: [],
};
@@ -1,146 +0,0 @@
/*
* 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 * as testHelpers from './test-helpers';
describe('testHelpers', () => {
it('should export the correct things', () => {
expect(testHelpers).toMatchInlineSnapshot(`
Object {
"mockApiClient": Object {
"createRc": Object {
"createRef": [MockFunction],
"createRelease": [MockFunction],
"getComparison": [MockFunction],
},
"getBranch": [MockFunction],
"getHost": [MockFunction],
"getLatestCommit": [MockFunction],
"getLatestRelease": [MockFunction],
"getOwners": [MockFunction],
"getRecentCommits": [MockFunction],
"getRepoPath": [MockFunction],
"getRepositories": [MockFunction],
"getRepository": [MockFunction],
"getUsername": [MockFunction],
"patch": Object {
"createCherryPickCommit": [MockFunction],
"createReference": [MockFunction],
"createTagObject": [MockFunction],
"createTempCommit": [MockFunction],
"forceBranchHeadToTempCommit": [MockFunction],
"merge": [MockFunction],
"replaceTempCommit": [MockFunction],
"updateRelease": [MockFunction],
},
"promoteRc": Object {
"promoteRelease": [MockFunction],
},
"stats": Object {
"getAllReleases": [MockFunction],
"getAllTags": [MockFunction],
"getCommit": [MockFunction],
},
},
"mockBumpedTag": "rc-2020.01.01_1337",
"mockCalverProject": Object {
"isProvidedViaProps": false,
"owner": "mock_owner",
"repo": "mock_repo",
"versioningStrategy": "calver",
},
"mockDefaultBranch": "mock_defaultBranch",
"mockNextGitHubInfoCalver": Object {
"rcBranch": "rc/2020.01.01_1",
"rcReleaseTag": "rc-2020.01.01_1",
"releaseName": "Version 2020.01.01_1",
},
"mockNextGitHubInfoSemver": Object {
"rcBranch": "rc/1.2.3",
"rcReleaseTag": "rc-1.2.3",
"releaseName": "Version 1.2.3",
},
"mockReleaseBranch": Object {
"commit": Object {
"commit": Object {
"tree": Object {
"sha": "mock_branch_commit_commit_tree_sha",
},
},
"sha": "mock_branch_commit_sha",
},
"links": Object {
"html": "mock_branch_links_html",
},
"name": "rc/1.2.3",
},
"mockReleaseCandidateCalver": Object {
"htmlUrl": "mock_release_html_url",
"id": 1,
"prerelease": true,
"tagName": "rc-2020.01.01_1",
"targetCommitish": "rc/2020.01.01_1",
},
"mockReleaseCandidateSemver": Object {
"htmlUrl": "mock_release_html_url",
"id": 1,
"prerelease": true,
"tagName": "rc-1.2.3",
"targetCommitish": "rc/1.2.3",
},
"mockReleaseVersionCalver": Object {
"htmlUrl": "mock_release_html_url",
"id": 1,
"prerelease": false,
"tagName": "version-2020.01.01_1",
"targetCommitish": "rc/2020.01.01_1",
},
"mockReleaseVersionSemver": Object {
"htmlUrl": "mock_release_html_url",
"id": 1,
"prerelease": false,
"tagName": "version-1.2.3",
"targetCommitish": "rc/1.2.3",
},
"mockSearchCalver": "?versioningStrategy=calver&owner=mock_owner&repo=mock_repo",
"mockSearchSemver": "?versioningStrategy=semver&owner=mock_owner&repo=mock_repo",
"mockSelectedPatchCommit": Object {
"author": Object {
"htmlUrl": "author_html_url",
"login": "author_login",
},
"commit": Object {
"message": "commit_message",
},
"firstParentSha": "mock_first_parent_sha",
"htmlUrl": "mock_htmlUrl",
"sha": "mock_sha_selected_patch_commit",
},
"mockSemverProject": Object {
"isProvidedViaProps": false,
"owner": "mock_owner",
"repo": "mock_repo",
"versioningStrategy": "semver",
},
"mockTagParts": Object {
"calver": "2020.01.01",
"patch": 1,
"prefix": "rc",
},
}
`);
});
});
@@ -280,16 +280,25 @@ export const mockApiClient: IPluginApiClient = {
},
stats: {
getAllTags: jest.fn(async () => {
throw new Error('Not implemented');
}),
getAllTags: jest.fn(async () => [
{
tagName: MOCK_RELEASE_CANDIDATE_TAG_NAME_CALVER,
sha: 'mock_sha',
},
]),
getAllReleases: jest.fn(async () => {
throw new Error('Not implemented');
}),
getAllReleases: jest.fn(async () => [
{
id: 1,
name: 'mock_release_name',
tagName: 'mock_release_tag_name',
createdAt: 'mock_release_published_at',
htmlUrl: 'mock_release_html_url',
},
]),
getCommit: jest.fn(async () => {
throw new Error('Not implemented');
}),
getCommit: jest.fn(async () => ({
createdAt: '2021-01-01T10:11:12Z',
})),
},
};
@@ -1,88 +0,0 @@
/*
* 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 * as testIds from './test-ids';
describe('test-ids', () => {
it('should export the correct things', () => {
expect(testIds).toMatchInlineSnapshot(`
Object {
"TEST_IDS": Object {
"components": Object {
"circularProgress": "grm--circular-progress",
"differ": Object {
"current": "grm--differ-current",
"icons": Object {
"branch": "grm--differ--icons--branch",
"github": "grm--differ--icons--github",
"slack": "grm--differ--icons--slack",
"tag": "grm--differ--icons--tag",
"versioning": "grm--differ--icons--versioning",
},
"next": "grm--differ-next",
},
"divider": "grm--divider",
"linearProgressWithLabel": "grm--linear-progress-with-label",
"noLatestRelease": "grm--no-latest-release",
"responseStepListDialogContent": "grm--response-step-list--dialog-content",
"responseStepListItem": "grm--response-step-list-item",
"responseStepListItemIconDefault": "grm--response-step-list-item--item-icon--default",
"responseStepListItemIconFailure": "grm--response-step-list-item--item-icon--failure",
"responseStepListItemIconLink": "grm--response-step-list-item--item-icon--link",
"responseStepListItemIconSuccess": "grm--response-step-list-item--item-icon--success",
},
"createRc": Object {
"cta": "grm--create-rc--cta",
"semverSelect": "grm--create-rc--semver-select",
},
"form": Object {
"owner": Object {
"empty": "grm--form--owner--empty",
"error": "grm--form--owner--error",
"loading": "grm--form--owner--loading",
"select": "grm--form--owner--select",
},
"repo": Object {
"empty": "grm--form--repo--empty",
"error": "grm--form--repo--error",
"loading": "grm--form--repo--loading",
"select": "grm--form--repo--select",
},
"versioningStrategy": Object {
"radioGroup": "grm--form--versioning-strategy--radio-group",
},
},
"info": Object {
"info": "grm--info",
"infoFeaturePlus": "grm--info-feature-plus",
},
"patch": Object {
"body": "grm--patch-body",
"error": "grm--patch-body--error",
"loading": "grm--patch-body--loading",
"notPrerelease": "grm--patch-body--not-prerelease--info",
},
"promoteRc": Object {
"cta": "grm--promote-rc-body--cta",
"mockedPromoteRcBody": "grm-mocked-promote-rc-body",
"notRcWarning": "grm--promote-rc--not-rc-warning",
"promoteRc": "grm--promote-rc",
},
},
}
`);
});
});