diff --git a/plugins/github-release-manager/README.md b/plugins/github-release-manager/README.md index 2a99912576..86efc0c069 100644 --- a/plugins/github-release-manager/README.md +++ b/plugins/github-release-manager/README.md @@ -54,6 +54,6 @@ The plugin exports a single full-page extension `GitHubReleaseManagerPage`, whic The plugin is configurable either via props or the select elements on the page. -If project configuration is provided via props, the select elements are disabled. It is also possible to omit components from the page via props, as well as attaching callbacks for successful executions. +If project configuration is provided via props, the select elements are disabled. It is also possible to omit features from the page via props, as well as attaching callbacks for successful executions. See the plugin's dev folder (`dev/index.tsx`) to see some examples. diff --git a/plugins/github-release-manager/src/GitHubReleaseManager.tsx b/plugins/github-release-manager/src/GitHubReleaseManager.tsx index 348d4a0578..f0156af06c 100644 --- a/plugins/github-release-manager/src/GitHubReleaseManager.tsx +++ b/plugins/github-release-manager/src/GitHubReleaseManager.tsx @@ -38,8 +38,9 @@ import { useStyles } from './styles/styles'; export interface GitHubReleaseManagerProps { project?: Omit; - components?: { + features?: { info?: Pick, 'omit'>; + stats?: Pick, 'omit'>; createRc?: ComponentConfigCreateRc; promoteRc?: ComponentConfigPromoteRc; patch?: ComponentConfigPatch; @@ -90,9 +91,7 @@ export function GitHubReleaseManager(props: GitHubReleaseManagerProps) { - {isProjectValid(project) && ( - - )} + {isProjectValid(project) && } diff --git a/plugins/github-release-manager/src/api/PluginApiClient.test.ts b/plugins/github-release-manager/src/api/PluginApiClient.test.ts index e9db190eef..998e8bc4e8 100644 --- a/plugins/github-release-manager/src/api/PluginApiClient.test.ts +++ b/plugins/github-release-manager/src/api/PluginApiClient.test.ts @@ -57,6 +57,10 @@ describe('PluginApiClient', () => { "promoteRc": Object { "promoteRelease": [Function], }, + "stats": Object { + "getAllReleases": [Function], + "getAllTags": [Function], + }, } `); }); diff --git a/plugins/github-release-manager/src/api/PluginApiClient.ts b/plugins/github-release-manager/src/api/PluginApiClient.ts index e9f8e89f33..31996f8882 100644 --- a/plugins/github-release-manager/src/api/PluginApiClient.ts +++ b/plugins/github-release-manager/src/api/PluginApiClient.ts @@ -562,6 +562,43 @@ ${selectedPatchCommit.commit.message}`, }; }, }; + + stats = { + getAllTags: async ({ owner, repo }: OwnerRepo) => { + const { octokit } = await this.getOctokit(); + + const tags = await octokit.paginate(octokit.repos.listTags, { + owner, + repo, + per_page: 100, + ...DISABLE_CACHE, + }); + + return tags.map(tag => ({ + tagName: tag.name, + })); + }, + + getAllReleases: async ({ owner, repo }: OwnerRepo) => { + const { octokit } = await this.getOctokit(); + + const releases = await octokit.paginate(octokit.repos.listReleases, { + owner, + repo, + per_page: 100, + ...DISABLE_CACHE, + }); + + return releases.map(release => ({ + release, + id: release.id, + name: release.name, + tagName: release.tag_name, + createdAt: release.published_at, + htmlUrl: release.html_url, + })); + }, + }; } type UnboxPromise> = T extends Promise @@ -623,6 +660,28 @@ type GetRecentCommits = ( export type GetRecentCommitsResult = UnboxReturnedPromise; export type GetRecentCommitsResultSingle = UnboxArray; +type GetAllTags = ( + args: OwnerRepo, +) => Promise< + Array<{ + tagName: string; + }> +>; +export type GetAllTagsResult = UnboxReturnedPromise; + +type GetAllReleases = ( + args: OwnerRepo, +) => Promise< + Array<{ + id: number; + name: string | null; + tagName: string; + createdAt: string | null; + htmlUrl: string; + }> +>; +export type GetAllReleasesResult = UnboxReturnedPromise; + type GetLatestRelease = ( args: OwnerRepo, ) => Promise<{ @@ -858,4 +917,8 @@ export interface IPluginApiClient { promoteRc: { promoteRelease: PromoteRelease; }; + stats: { + getAllTags: GetAllTags; + getAllReleases: GetAllReleases; + }; } diff --git a/plugins/github-release-manager/src/features/Features.test.tsx b/plugins/github-release-manager/src/features/Features.test.tsx index fd81b276cf..bccca0e283 100644 --- a/plugins/github-release-manager/src/features/Features.test.tsx +++ b/plugins/github-release-manager/src/features/Features.test.tsx @@ -52,8 +52,8 @@ describe('Features', () => { expect(getByTestId(TEST_IDS.info.info)).toMatchInlineSnapshot(`
{ : A GitHub release intended for end users

-
`); }); diff --git a/plugins/github-release-manager/src/features/Features.tsx b/plugins/github-release-manager/src/features/Features.tsx index 673c9f5696..5d3e2a7151 100644 --- a/plugins/github-release-manager/src/features/Features.tsx +++ b/plugins/github-release-manager/src/features/Features.tsx @@ -32,9 +32,9 @@ import { useVersioningStrategyMatchesRepoTags } from '../hooks/useVersioningStra import { validateTagName } from '../helpers/tagParts/validateTagName'; export function Features({ - components, + features, }: { - components: GitHubReleaseManagerProps['components']; + features: GitHubReleaseManagerProps['features']; }) { const { pluginApiClient } = usePluginApiClientContext(); const { project } = useProjectContext(); @@ -114,34 +114,35 @@ export function Features({ )} - {!components?.info?.omit && ( + {!features?.info?.omit && ( )} - {!components?.createRc?.omit && ( + {!features?.createRc?.omit && ( )} - {!components?.promoteRc?.omit && ( + {!features?.promoteRc?.omit && ( )} - {!components?.patch?.omit && ( + {!features?.patch?.omit && ( )} diff --git a/plugins/github-release-manager/src/features/Info/Info.tsx b/plugins/github-release-manager/src/features/Info/Info.tsx index ff431930d8..063c66adee 100644 --- a/plugins/github-release-manager/src/features/Info/Info.tsx +++ b/plugins/github-release-manager/src/features/Info/Info.tsx @@ -15,7 +15,8 @@ */ import React, { useState } from 'react'; -import { Link, Typography, Button } from '@material-ui/core'; +import { Link, Typography, Button, Box } from '@material-ui/core'; +import BarChartIcon from '@material-ui/icons/BarChart'; import { GetBranchResult, @@ -23,7 +24,7 @@ import { } from '../../api/PluginApiClient'; import { Differ } from '../../components/Differ'; import { InfoCardPlus } from '../../components/InfoCardPlus'; -import { Stats } from '../../components/Stats/Stats'; +import { Stats } from '../Stats/Stats'; import { TEST_IDS } from '../../test-helpers/test-ids'; import { useProjectContext } from '../../contexts/ProjectContext'; import { useStyles } from '../../styles/styles'; @@ -32,16 +33,21 @@ import flowImage from './flow.png'; interface InfoCardProps { releaseBranch: GetBranchResult | null; latestRelease: GetLatestReleaseResult; + statsEnabled: boolean; } -export const Info = ({ releaseBranch, latestRelease }: InfoCardProps) => { +export const Info = ({ + releaseBranch, + latestRelease, + statsEnabled, +}: InfoCardProps) => { const { project } = useProjectContext(); const classes = useStyles(); const [showStats, setShowStats] = useState(false); return ( -
+ Terminology @@ -65,18 +71,9 @@ export const Info = ({ releaseBranch, latestRelease }: InfoCardProps) => { Release Version: A GitHub release intended for end users + - - {showStats && } -
- -
+ Flow @@ -90,9 +87,9 @@ export const Info = ({ releaseBranch, latestRelease }: InfoCardProps) => { flow -
+ -
+ Details @@ -113,7 +110,23 @@ export const Info = ({ releaseBranch, latestRelease }: InfoCardProps) => { Latest release: -
+ + + {statsEnabled && ( + + + + {showStats && } + + )}
); }; diff --git a/plugins/github-release-manager/src/features/Stats/DialogBody.tsx b/plugins/github-release-manager/src/features/Stats/DialogBody.tsx new file mode 100644 index 0000000000..3c2f884417 --- /dev/null +++ b/plugins/github-release-manager/src/features/Stats/DialogBody.tsx @@ -0,0 +1,194 @@ +/* + * 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 { Alert } from '@material-ui/lab'; +import { + Box, + makeStyles, + Paper, + Table, + TableBody, + TableCell, + TableContainer, + TableHead, + TableRow, + Typography, +} from '@material-ui/core'; + +import { CenteredCircularProgress } from '../../components/CenteredCircularProgress'; +import { getMappedReleases } from './getMappedReleases'; +import { getSummary } from './getSummary'; +import { getTags } from './getTags'; +import { Row } from './Row'; +import { useGetStats } from './hooks/useGetStats'; +import { useProjectContext } from '../../contexts/ProjectContext'; +import { Warn } from './Warn'; + +const useStyles = makeStyles({ + table: { + minWidth: 650, + }, +}); + +export function DialogBody() { + const classes = useStyles(); + const { stats } = useGetStats(); + const { project } = useProjectContext(); + + if (stats.error) { + return ( + Unexpected error: {stats.error.message} + ); + } + + if (stats.loading) { + return ; + } + + if (!stats.value) { + return Couldn't find any stats :(; + } + + const { allReleases, allTags } = stats.value; + const mappedReleases = getMappedReleases({ allReleases, project }); + const tags = getTags({ allTags, project, mappedReleases }); + const summary = getSummary({ mappedReleases }); + const shouldWarn = + tags.unmappable.length > 0 || + tags.unmatched.length > 0 || + mappedReleases.unmatched.length > 0; + + if (shouldWarn) { + // eslint-disable-next-line no-console + console.log("⚠️ Here's a summary of unmapped/unmatched tags/releases", { + unmappableTags: tags.unmappable, + unmatchableTags: tags.unmatched, + unmatchableReleases: mappedReleases.unmatched, + }); + } + + const getDecimalNumber = (n: number) => { + if (isNaN(n)) { + return 0; + } + + if (n.toString().includes('.')) { + return n.toFixed(2); + } + + return n; + }; + + return ( + <> + + + Summary + + Total releases: {summary.totalReleases} + + + + + Release Candidate + + Release Candidate patches: {summary.totalCandidatePatches} + + + + Release Candidate patches per release:{' '} + {getDecimalNumber( + summary.totalCandidatePatches / summary.totalReleases, + )} + + + + + Release Version + + Release Version patches: {summary.totalVersionPatches} + + + + Release Version patches per release:{' '} + {getDecimalNumber( + summary.totalVersionPatches / summary.totalReleases, + )} + + + + + Total + + Patches:{' '} + {summary.totalCandidatePatches + summary.totalVersionPatches} + + + + Patches per release:{' '} + {getDecimalNumber( + (summary.totalCandidatePatches + summary.totalVersionPatches) / + summary.totalReleases, + )} + + + + + + + + + + Release + Created at + # candidate patches + # release patches + + + + + {Object.entries(mappedReleases.releases).map( + ([baseVersion, mappedRelease], index) => { + return ( + + ); + }, + )} + +
+
+ + + {shouldWarn && ( + + )} + + + ); +} diff --git a/plugins/github-release-manager/src/features/Stats/DialogTitle.tsx b/plugins/github-release-manager/src/features/Stats/DialogTitle.tsx new file mode 100644 index 0000000000..92e8d0d588 --- /dev/null +++ b/plugins/github-release-manager/src/features/Stats/DialogTitle.tsx @@ -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 React from 'react'; +import { + createStyles, + IconButton, + Theme, + Typography, + withStyles, + WithStyles, +} from '@material-ui/core'; +import CloseIcon from '@material-ui/icons/Close'; +import MuiDialogTitle from '@material-ui/core/DialogTitle'; + +import { Stats } from './Stats'; + +interface DialogTitleProps extends WithStyles { + children: React.ReactNode; + setShowStats: React.ComponentProps['setShowStats']; +} + +const styles = (theme: Theme) => + createStyles({ + root: { + margin: 0, + padding: theme.spacing(2), + }, + closeButton: { + position: 'absolute', + right: theme.spacing(1), + top: theme.spacing(1), + color: theme.palette.grey[500], + }, + }); + +export const DialogTitle = withStyles(styles)((props: DialogTitleProps) => { + const { children, classes, setShowStats, ...other } = props; + + return ( + + {children} + setShowStats(false)} + > + + + + ); +}); diff --git a/plugins/github-release-manager/src/features/Stats/Row.tsx b/plugins/github-release-manager/src/features/Stats/Row.tsx new file mode 100644 index 0000000000..ea95f8e5c2 --- /dev/null +++ b/plugins/github-release-manager/src/features/Stats/Row.tsx @@ -0,0 +1,131 @@ +/* + * 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, { useState } from 'react'; +import { DateTime } from 'luxon'; +import { + Box, + Collapse, + IconButton, + Link, + makeStyles, + TableCell, + TableRow, + Typography, +} from '@material-ui/core'; +import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown'; +import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp'; + +import { getMappedReleases } from './getMappedReleases'; + +const useRowStyles = makeStyles({ + root: { + '& > *': { + borderBottom: 'unset', + }, + }, +}); + +interface RowProps { + baseVersion: string; + mappedRelease: ReturnType['releases']['0']; +} + +export function Row({ baseVersion, mappedRelease }: RowProps) { + const [open, setOpen] = useState(false); + const classes = useRowStyles(); + const versions = mappedRelease.versions.reverse(); + const candidates = mappedRelease.candidates.reverse(); + const isPrerelease = versions.length === 0; + + return ( + + + + setOpen(!open)} + > + {open ? : } + + + + + + {baseVersion} + {isPrerelease ? ' (prerelease)' : ''} + + + + + {mappedRelease.createdAt + ? DateTime.fromISO(mappedRelease.createdAt) + .setLocale('sv-SE') + .toFormat('yyyy-MM-dd') + : '-'} + + + {candidates.length} + + {Math.max(0, versions.length - 1)} + + + + + + +
+ {!isPrerelease && ( + + {versions.map(version => ( + + {version} + + ))} + + )} + + {!isPrerelease && ( + + {' 🚀 '} + + )} + + + {candidates.map(candidate => ( + + {candidate} + + ))} + +
+
+
+
+
+
+ ); +} diff --git a/plugins/github-release-manager/src/features/Stats/Stats.tsx b/plugins/github-release-manager/src/features/Stats/Stats.tsx new file mode 100644 index 0000000000..e590c0a320 --- /dev/null +++ b/plugins/github-release-manager/src/features/Stats/Stats.tsx @@ -0,0 +1,66 @@ +/* + * 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 { Button, Dialog, Theme, withStyles } from '@material-ui/core'; +import CloseIcon from '@material-ui/icons/Close'; +import MuiDialogActions from '@material-ui/core/DialogActions'; +import MuiDialogContent from '@material-ui/core/DialogContent'; + +import { DialogBody } from './DialogBody'; +import { DialogTitle } from './DialogTitle'; +import { Transition } from '../../components/Transition'; + +const DialogContent = withStyles((theme: Theme) => ({ + root: { + padding: theme.spacing(2), + }, +}))(MuiDialogContent); + +const DialogActions = withStyles((theme: Theme) => ({ + root: { + margin: 0, + padding: theme.spacing(1), + }, +}))(MuiDialogActions); + +interface StatsProps { + setShowStats: React.Dispatch>; +} + +export function Stats({ setShowStats }: StatsProps) { + return ( + + Stats + + + + + + + + + + ); +} diff --git a/plugins/github-release-manager/src/features/Stats/Warn.tsx b/plugins/github-release-manager/src/features/Stats/Warn.tsx new file mode 100644 index 0000000000..ecae90426d --- /dev/null +++ b/plugins/github-release-manager/src/features/Stats/Warn.tsx @@ -0,0 +1,56 @@ +/* + * 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 { Alert } from '@material-ui/lab'; + +import { getMappedReleases } from './getMappedReleases'; +import { getTags } from './getTags'; +import { Project } from '../../contexts/ProjectContext'; + +interface WarnProps { + tags: ReturnType; + mappedReleases: ReturnType; + project: Project; +} + +export const Warn = ({ tags, mappedReleases, project }: WarnProps) => { + return ( + + {tags.unmappable.length > 0 && ( +
+ Failed to map {tags.unmappable.length} tags to + releases +
+ )} + + {tags.unmatched.length > 0 && ( +
+ Failed to match {tags.unmatched.length} tags to{' '} + {project.versioningStrategy} +
+ )} + + {mappedReleases.unmatched.length > 0 && ( +
+ Failed to match {mappedReleases.unmatched.length}{' '} + releases to {project.versioningStrategy} +
+ )} +
See full output in the console
+
+ ); +}; diff --git a/plugins/github-release-manager/src/features/Stats/getMappedReleases.tsx b/plugins/github-release-manager/src/features/Stats/getMappedReleases.tsx new file mode 100644 index 0000000000..52a6204589 --- /dev/null +++ b/plugins/github-release-manager/src/features/Stats/getMappedReleases.tsx @@ -0,0 +1,74 @@ +/* + * 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 { GetAllReleasesResult } from '../../api/PluginApiClient'; +import { Project } from '../../contexts/ProjectContext'; +import { semverRegexp } from '../../helpers/tagParts/getSemverTagParts'; + +export function getMappedReleases({ + allReleases, + project, +}: { + allReleases: GetAllReleasesResult; + project: Project; +}) { + return allReleases.reduce( + ( + acc: { + unmatched: string[]; + releases: { + [baseVersion: string]: { + createdAt: string | null; + candidates: string[]; + versions: string[]; + htmlUrl: string; + }; + }; + }, + release, + ) => { + const match = + project.versioningStrategy === 'semver' + ? release.tagName.match(semverRegexp) + : release.tagName.match(calverRegexp); + + if (!match) { + acc.unmatched.push(release.tagName); + return acc; + } + + const prefix = match[1]; + const baseVersion = + project.versioningStrategy === 'semver' + ? `${match[2]}.${match[3]}` + : match[2]; + + if (!acc.releases[baseVersion]) { + acc.releases[baseVersion] = { + createdAt: release.createdAt, + candidates: prefix === 'rc' ? [release.tagName] : [], + versions: prefix === 'version' ? [release.tagName] : [], + htmlUrl: release.htmlUrl, + }; + return acc; + } + + return acc; + }, + { unmatched: [], releases: {} }, + ); +} diff --git a/plugins/github-release-manager/src/features/Stats/getSummary.tsx b/plugins/github-release-manager/src/features/Stats/getSummary.tsx new file mode 100644 index 0000000000..f90758987b --- /dev/null +++ b/plugins/github-release-manager/src/features/Stats/getSummary.tsx @@ -0,0 +1,45 @@ +/* + * 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'; + +export function getSummary({ + mappedReleases, +}: { + mappedReleases: ReturnType; +}) { + return Object.entries(mappedReleases.releases).reduce( + ( + acc: { + totalReleases: number; + totalCandidatePatches: number; + totalVersionPatches: number; + }, + [_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, + }, + ); +} diff --git a/plugins/github-release-manager/src/features/Stats/getTags.tsx b/plugins/github-release-manager/src/features/Stats/getTags.tsx new file mode 100644 index 0000000000..4ad38cec17 --- /dev/null +++ b/plugins/github-release-manager/src/features/Stats/getTags.tsx @@ -0,0 +1,75 @@ +/* + * 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 './getMappedReleases'; +import { Project } from '../../contexts/ProjectContext'; +import { semverRegexp } from '../../helpers/tagParts/getSemverTagParts'; + +export function getTags({ + allTags, + project, + mappedReleases, +}: { + allTags: GetAllTagsResult; + project: Project; + mappedReleases: ReturnType; +}) { + return allTags.reduce( + (acc: { unmatched: string[]; unmappable: string[] }, tag) => { + const match = + project.versioningStrategy === 'semver' + ? tag.tagName.match(semverRegexp) + : tag.tagName.match(calverRegexp); + + if (!match) { + acc.unmatched.push(tag.tagName); + return acc; + } + + const prefix = match[1]; + const baseVersion = + project.versioningStrategy === 'semver' + ? `${match[2]}.${match[3]}` + : match[2]; + + if (!mappedReleases.releases[baseVersion]) { + acc.unmappable.push(tag.tagName); + return acc; + } + + if ( + prefix === 'rc' && + !mappedReleases.releases[baseVersion].candidates.includes(tag.tagName) + ) { + mappedReleases.releases[baseVersion].candidates.push(tag.tagName); + return acc; + } + + if ( + prefix === 'version' && + !mappedReleases.releases[baseVersion].versions.includes(tag.tagName) + ) { + mappedReleases.releases[baseVersion].versions.push(tag.tagName); + return acc; + } + + return acc; + }, + { unmatched: [], unmappable: [] }, + ); +} diff --git a/plugins/github-release-manager/src/features/Stats/hooks/useGetStats.ts b/plugins/github-release-manager/src/features/Stats/hooks/useGetStats.ts new file mode 100644 index 0000000000..f918be0c97 --- /dev/null +++ b/plugins/github-release-manager/src/features/Stats/hooks/useGetStats.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 { useAsync } from 'react-use'; + +import { usePluginApiClientContext } from '../../../contexts/PluginApiClientContext'; +import { useProjectContext } from '../../../contexts/ProjectContext'; + +export const useGetStats = () => { + const { pluginApiClient } = usePluginApiClientContext(); + const { project } = useProjectContext(); + + const stats = useAsync(async () => { + const [allReleases, allTags] = await Promise.all([ + pluginApiClient.stats.getAllReleases({ + owner: project.owner, + repo: project.repo, + }), + pluginApiClient.stats.getAllTags({ + owner: project.owner, + repo: project.repo, + }), + ]); + + return { + allReleases, + allTags, + }; + }, [project]); + + return { + stats, + }; +}; diff --git a/plugins/github-release-manager/src/test-helpers/test-helpers.test.ts b/plugins/github-release-manager/src/test-helpers/test-helpers.test.ts index 8dfa3e6fd5..fbd3b690b4 100644 --- a/plugins/github-release-manager/src/test-helpers/test-helpers.test.ts +++ b/plugins/github-release-manager/src/test-helpers/test-helpers.test.ts @@ -49,6 +49,10 @@ describe('testHelpers', () => { "promoteRc": Object { "promoteRelease": [MockFunction], }, + "stats": Object { + "getAllReleases": [MockFunction], + "getAllTags": [MockFunction], + }, }, "mockBumpedTag": "rc-2020.01.01_1337", "mockCalverProject": Object { diff --git a/plugins/github-release-manager/src/test-helpers/test-helpers.ts b/plugins/github-release-manager/src/test-helpers/test-helpers.ts index 735bcd7d48..fc7a5f5c5b 100644 --- a/plugins/github-release-manager/src/test-helpers/test-helpers.ts +++ b/plugins/github-release-manager/src/test-helpers/test-helpers.ts @@ -278,4 +278,14 @@ export const mockApiClient: IPluginApiClient = { htmlUrl: 'mock_release_html_url', })), }, + + stats: { + getAllTags: jest.fn(async () => { + throw new Error('Not implemented'); + }), + + getAllReleases: jest.fn(async () => { + throw new Error('Not implemented'); + }), + }, };