From ce1749f2d64f7a4899d992820410fa6b1a33ab3e Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Mon, 12 Apr 2021 18:31:35 +0200 Subject: [PATCH 001/140] Migrate ReleaseManagerAsAService to BOSS Signed-off-by: Erik Engervall --- .../plugins/release-manager-as-a-service.yaml | 9 + .../img/release-manager-as-a-service-logo.svg | 13 + packages/app/package.json | 1 + packages/app/src/plugins.ts | 1 + .../release-manager-as-a-service/.eslintrc.js | 3 + .../release-manager-as-a-service/README.md | 45 + .../dev/README.md | 13 + .../dev/index.tsx | 55 + .../release-manager-as-a-service/package.json | 51 + .../src/ReleaseManagerAsAService.tsx | 179 ++ .../src/api/PluginApiClientConfig.ts | 51 + .../src/api/RMaaSApiClient.ts | 412 ++++ .../src/api/serviceApiRef.ts | 26 + .../src/cards/createRc/CreateRc.test.tsx | 67 + .../src/cards/createRc/CreateRc.tsx | 205 ++ .../src/cards/createRc/getRcGheInfo.test.ts | 88 + .../src/cards/createRc/getRcGheInfo.ts | 60 + .../createRc/sideEffects/createGheRc.test.ts | 59 + .../cards/createRc/sideEffects/createGheRc.ts | 129 ++ .../src/cards/info/Info.test.tsx | 38 + .../src/cards/info/Info.tsx | 133 ++ .../src/cards/info/rmaas-flow.png | Bin 0 -> 77527 bytes .../src/cards/patchRc/Patch.test.tsx | 41 + .../src/cards/patchRc/Patch.tsx | 81 + .../src/cards/patchRc/PatchBody.test.tsx | 77 + .../src/cards/patchRc/PatchBody.tsx | 301 +++ .../cards/patchRc/sideEffects/patch.test.ts | 75 + .../src/cards/patchRc/sideEffects/patch.ts | 194 ++ .../src/cards/promoteRc/PromoteRc.test.tsx | 61 + .../src/cards/promoteRc/PromoteRc.tsx | 80 + .../cards/promoteRc/PromoteRcBody.test.tsx | 35 + .../src/cards/promoteRc/PromoteRcBody.tsx | 100 + .../sideEffects/promoteGheRc.test.ts | 42 + .../promoteRc/sideEffects/promoteGheRc.ts | 60 + .../src/components/Differ.tsx | 79 + .../src/components/Divider.test.tsx | 28 + .../src/components/Divider.tsx | 27 + .../src/components/InfoCardPlus.test.tsx | 28 + .../src/components/InfoCardPlus.tsx | 39 + .../src/components/NoLatestRelease.test.tsx | 30 + .../src/components/NoLatestRelease.tsx | 34 + .../src/components/ProjectContext.ts | 33 + .../src/components/ReloadButton.test.tsx | 28 + .../src/components/ReloadButton.tsx | 35 + .../ResponseStepList.test.tsx | 65 + .../ResponseStepList/ResponseStepList.tsx | 113 + .../ResponseStepListItem.test.tsx | 96 + .../ResponseStepList/ResponseStepListItem.tsx | 135 ++ .../src/constants/constants.test.ts | 30 + .../src/constants/constants.ts | 24 + .../errors/ReleaseManagerAsAServiceError.ts | 22 + .../src/helpers/date.test.ts | 26 + .../src/helpers/date.ts | 16 + .../src/helpers/getBumpedTag.test.ts | 124 ++ .../src/helpers/getBumpedTag.ts | 94 + .../src/helpers/getShortCommitHash.test.ts | 33 + .../src/helpers/getShortCommitHash.ts | 28 + .../src/helpers/isCalverTagParts.test.ts | 32 + .../src/helpers/isCalverTagParts.ts | 24 + .../src/helpers/tagParts/getCalverTagParts.ts | 40 + .../src/helpers/tagParts/getSemverTagParts.ts | 40 + .../src/helpers/tagParts/getTagParts.test.ts | 112 + .../src/helpers/tagParts/getTagParts.ts | 32 + .../release-manager-as-a-service/src/index.ts | 19 + .../src/plugin.test.ts | 22 + .../src/plugin.ts | 54 + .../src/routes.ts | 20 + .../src/setupTests.ts | 17 + .../src/sideEffects/getGitHubBatchInfo.ts | 48 + .../src/sideEffects/getLatestRelease.test.ts | 42 + .../src/sideEffects/getLatestRelease.ts | 34 + .../src/styles/styles.ts | 22 + .../src/test-helpers/test-helpers.test.ts | 165 ++ .../src/test-helpers/test-helpers.ts | 255 +++ .../src/test-helpers/test-ids.ts | 53 + .../src/types/types.ts | 1939 +++++++++++++++++ yarn.lock | 5 + 77 files changed, 6927 insertions(+) create mode 100644 microsite/data/plugins/release-manager-as-a-service.yaml create mode 100644 microsite/static/img/release-manager-as-a-service-logo.svg create mode 100644 plugins/release-manager-as-a-service/.eslintrc.js create mode 100644 plugins/release-manager-as-a-service/README.md create mode 100644 plugins/release-manager-as-a-service/dev/README.md create mode 100644 plugins/release-manager-as-a-service/dev/index.tsx create mode 100644 plugins/release-manager-as-a-service/package.json create mode 100644 plugins/release-manager-as-a-service/src/ReleaseManagerAsAService.tsx create mode 100644 plugins/release-manager-as-a-service/src/api/PluginApiClientConfig.ts create mode 100644 plugins/release-manager-as-a-service/src/api/RMaaSApiClient.ts create mode 100644 plugins/release-manager-as-a-service/src/api/serviceApiRef.ts create mode 100644 plugins/release-manager-as-a-service/src/cards/createRc/CreateRc.test.tsx create mode 100644 plugins/release-manager-as-a-service/src/cards/createRc/CreateRc.tsx create mode 100644 plugins/release-manager-as-a-service/src/cards/createRc/getRcGheInfo.test.ts create mode 100644 plugins/release-manager-as-a-service/src/cards/createRc/getRcGheInfo.ts create mode 100644 plugins/release-manager-as-a-service/src/cards/createRc/sideEffects/createGheRc.test.ts create mode 100644 plugins/release-manager-as-a-service/src/cards/createRc/sideEffects/createGheRc.ts create mode 100644 plugins/release-manager-as-a-service/src/cards/info/Info.test.tsx create mode 100644 plugins/release-manager-as-a-service/src/cards/info/Info.tsx create mode 100644 plugins/release-manager-as-a-service/src/cards/info/rmaas-flow.png create mode 100644 plugins/release-manager-as-a-service/src/cards/patchRc/Patch.test.tsx create mode 100644 plugins/release-manager-as-a-service/src/cards/patchRc/Patch.tsx create mode 100644 plugins/release-manager-as-a-service/src/cards/patchRc/PatchBody.test.tsx create mode 100644 plugins/release-manager-as-a-service/src/cards/patchRc/PatchBody.tsx create mode 100644 plugins/release-manager-as-a-service/src/cards/patchRc/sideEffects/patch.test.ts create mode 100644 plugins/release-manager-as-a-service/src/cards/patchRc/sideEffects/patch.ts create mode 100644 plugins/release-manager-as-a-service/src/cards/promoteRc/PromoteRc.test.tsx create mode 100644 plugins/release-manager-as-a-service/src/cards/promoteRc/PromoteRc.tsx create mode 100644 plugins/release-manager-as-a-service/src/cards/promoteRc/PromoteRcBody.test.tsx create mode 100644 plugins/release-manager-as-a-service/src/cards/promoteRc/PromoteRcBody.tsx create mode 100644 plugins/release-manager-as-a-service/src/cards/promoteRc/sideEffects/promoteGheRc.test.ts create mode 100644 plugins/release-manager-as-a-service/src/cards/promoteRc/sideEffects/promoteGheRc.ts create mode 100644 plugins/release-manager-as-a-service/src/components/Differ.tsx create mode 100644 plugins/release-manager-as-a-service/src/components/Divider.test.tsx create mode 100644 plugins/release-manager-as-a-service/src/components/Divider.tsx create mode 100644 plugins/release-manager-as-a-service/src/components/InfoCardPlus.test.tsx create mode 100644 plugins/release-manager-as-a-service/src/components/InfoCardPlus.tsx create mode 100644 plugins/release-manager-as-a-service/src/components/NoLatestRelease.test.tsx create mode 100644 plugins/release-manager-as-a-service/src/components/NoLatestRelease.tsx create mode 100644 plugins/release-manager-as-a-service/src/components/ProjectContext.ts create mode 100644 plugins/release-manager-as-a-service/src/components/ReloadButton.test.tsx create mode 100644 plugins/release-manager-as-a-service/src/components/ReloadButton.tsx create mode 100644 plugins/release-manager-as-a-service/src/components/ResponseStepList/ResponseStepList.test.tsx create mode 100644 plugins/release-manager-as-a-service/src/components/ResponseStepList/ResponseStepList.tsx create mode 100644 plugins/release-manager-as-a-service/src/components/ResponseStepList/ResponseStepListItem.test.tsx create mode 100644 plugins/release-manager-as-a-service/src/components/ResponseStepList/ResponseStepListItem.tsx create mode 100644 plugins/release-manager-as-a-service/src/constants/constants.test.ts create mode 100644 plugins/release-manager-as-a-service/src/constants/constants.ts create mode 100644 plugins/release-manager-as-a-service/src/errors/ReleaseManagerAsAServiceError.ts create mode 100644 plugins/release-manager-as-a-service/src/helpers/date.test.ts create mode 100644 plugins/release-manager-as-a-service/src/helpers/date.ts create mode 100644 plugins/release-manager-as-a-service/src/helpers/getBumpedTag.test.ts create mode 100644 plugins/release-manager-as-a-service/src/helpers/getBumpedTag.ts create mode 100644 plugins/release-manager-as-a-service/src/helpers/getShortCommitHash.test.ts create mode 100644 plugins/release-manager-as-a-service/src/helpers/getShortCommitHash.ts create mode 100644 plugins/release-manager-as-a-service/src/helpers/isCalverTagParts.test.ts create mode 100644 plugins/release-manager-as-a-service/src/helpers/isCalverTagParts.ts create mode 100644 plugins/release-manager-as-a-service/src/helpers/tagParts/getCalverTagParts.ts create mode 100644 plugins/release-manager-as-a-service/src/helpers/tagParts/getSemverTagParts.ts create mode 100644 plugins/release-manager-as-a-service/src/helpers/tagParts/getTagParts.test.ts create mode 100644 plugins/release-manager-as-a-service/src/helpers/tagParts/getTagParts.ts create mode 100644 plugins/release-manager-as-a-service/src/index.ts create mode 100644 plugins/release-manager-as-a-service/src/plugin.test.ts create mode 100644 plugins/release-manager-as-a-service/src/plugin.ts create mode 100644 plugins/release-manager-as-a-service/src/routes.ts create mode 100644 plugins/release-manager-as-a-service/src/setupTests.ts create mode 100644 plugins/release-manager-as-a-service/src/sideEffects/getGitHubBatchInfo.ts create mode 100644 plugins/release-manager-as-a-service/src/sideEffects/getLatestRelease.test.ts create mode 100644 plugins/release-manager-as-a-service/src/sideEffects/getLatestRelease.ts create mode 100644 plugins/release-manager-as-a-service/src/styles/styles.ts create mode 100644 plugins/release-manager-as-a-service/src/test-helpers/test-helpers.test.ts create mode 100644 plugins/release-manager-as-a-service/src/test-helpers/test-helpers.ts create mode 100644 plugins/release-manager-as-a-service/src/test-helpers/test-ids.ts create mode 100644 plugins/release-manager-as-a-service/src/types/types.ts diff --git a/microsite/data/plugins/release-manager-as-a-service.yaml b/microsite/data/plugins/release-manager-as-a-service.yaml new file mode 100644 index 0000000000..08138f6d3b --- /dev/null +++ b/microsite/data/plugins/release-manager-as-a-service.yaml @@ -0,0 +1,9 @@ +--- +title: Release Manager as a Service +author: '@erikengervall' +authorUrl: https://github.com/erikengervall +category: Release management +description: Manage releases without having to juggle git commands +documentation: https://github.com/backstage/backstage/tree/master/plugins/release-manager-as-a-service +iconUrl: img/release-manager-as-a-service.svg +npmPackageName: '@backstage/plugin-release-manager-as-a-service' diff --git a/microsite/static/img/release-manager-as-a-service-logo.svg b/microsite/static/img/release-manager-as-a-service-logo.svg new file mode 100644 index 0000000000..b505d21560 --- /dev/null +++ b/microsite/static/img/release-manager-as-a-service-logo.svg @@ -0,0 +1,13 @@ + + + Export + + + + + + + + + + \ No newline at end of file diff --git a/packages/app/package.json b/packages/app/package.json index e5dbbaa5ff..26d5c36ab6 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -30,6 +30,7 @@ "@backstage/plugin-org": "^0.3.10", "@backstage/plugin-pagerduty": "0.3.2", "@backstage/plugin-register-component": "^0.2.12", + "@backstage/plugin-release-manager-as-a-service": "^0.1.1", "@backstage/plugin-rollbar": "^0.3.3", "@backstage/plugin-scaffolder": "^0.8.0", "@backstage/plugin-search": "^0.3.4", diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index ccc7e2a45c..bedd533e69 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -46,3 +46,4 @@ export { plugin as Kafka } from '@backstage/plugin-kafka'; export { todoPlugin } from '@backstage/plugin-todo'; export { badgesPlugin } from '@backstage/plugin-badges'; export { githubDeploymentsPlugin } from '@backstage/plugin-github-deployments'; +export { releaseManagerAsAServicePlugin } from '@backstage/plugin-release-manager-as-a-service'; diff --git a/plugins/release-manager-as-a-service/.eslintrc.js b/plugins/release-manager-as-a-service/.eslintrc.js new file mode 100644 index 0000000000..13573efa9c --- /dev/null +++ b/plugins/release-manager-as-a-service/.eslintrc.js @@ -0,0 +1,3 @@ +module.exports = { + extends: [require.resolve('@backstage/cli/config/eslint')], +}; diff --git a/plugins/release-manager-as-a-service/README.md b/plugins/release-manager-as-a-service/README.md new file mode 100644 index 0000000000..cdd50d35c6 --- /dev/null +++ b/plugins/release-manager-as-a-service/README.md @@ -0,0 +1,45 @@ +# Release Manager as a Service (RMaaS) + +## Overview + +`RMaaS` enables developers to manage their releases without having to juggle git commands. + +Does it bundle and ship your code? **No**. + +What `RMaaS` does is manage your **[releases](https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/managing-releases-in-a-repository)** on GitHub, building and shipping is entirely up to you as a developer to handle in your CI. + +`RMaaS` is build with industry standards in mind and the flow is as follows: + +![](./src/cards/info/rmaas-flow.png) + +> **GitHub Enterprise (GHE)**: The source control system where releases reside in a practical sense. Read more about GitHub releases here. Note that this plugin works just as well with a non-enterprise account. +> +> **Release Candidate (RC)**: A GHE pre-release intended primarily for internal testing +> +> **Release Version**: A GHE release intended for end users + +Looking at the flow above, a common release lifecycle could be: + +- User presses **Create Release Candidate** + - `RMaaS` + 1. Creates a release branch `rc/` + 1. Creates Release Candidate tag `rc-` + 1. Creates a GitHub prerelease with Release Candidate tag + - Your CI + 1. Detects the new tag by matching the git reference `refs/tags/rc-.*` + 1. Builds and deploys to staging environment for testing +- User presses **Patch** + - `RMaaS` + 1. The selected commit is cherry-picked to the release branch + 1. The release tag is bumped + 1. Updates GitHub release's tag and description with the patch's details + - Your CI + 1. Detects the new tag by matching the git reference `refs/tags/(rc|version)-.*` (Release Versions are patchable as well) + 1. Builds and deploys to staging (or production if Release Version) for testing +- User presses **Promote Release Candidate to Release Version** + - `RMaaS` + 1. Creates Release Version tag `version-` + 1. Promotes the GitHub release by removing the prerelease flag + - Your CI + 1. Detects the new tag by matching the git reference `refs/tags/version-.*` + 1. Builds and deploys to production for testing diff --git a/plugins/release-manager-as-a-service/dev/README.md b/plugins/release-manager-as-a-service/dev/README.md new file mode 100644 index 0000000000..550c8e1844 --- /dev/null +++ b/plugins/release-manager-as-a-service/dev/README.md @@ -0,0 +1,13 @@ +# release-manager-as-a-service + +Welcome to the release-manager-as-a-service plugin! + +_This plugin was created through the Backstage CLI_ + +## Getting started + +Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/release-manager-as-a-service](http://localhost:3000/release-manager-as-a-service). + +You can also serve the plugin in isolation by running `yarn start` in the plugin directory. +This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. +It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. diff --git a/plugins/release-manager-as-a-service/dev/index.tsx b/plugins/release-manager-as-a-service/dev/index.tsx new file mode 100644 index 0000000000..7bdc0ba12a --- /dev/null +++ b/plugins/release-manager-as-a-service/dev/index.tsx @@ -0,0 +1,55 @@ +/* + * 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 { createDevApp } from '@backstage/dev-utils'; +import { + releaseManagerAsAServicePlugin, + ReleaseManagerAsAServicePage, +} from '../src/plugin'; + +createDevApp() + .registerPlugin(releaseManagerAsAServicePlugin) + .addPage({ + element: ( + + ), + title: 'Root Page', + }) + .addPage({ + element: ( + + ), + title: 'Another page', + }) + .render(); diff --git a/plugins/release-manager-as-a-service/package.json b/plugins/release-manager-as-a-service/package.json new file mode 100644 index 0000000000..9a8f1bb7ef --- /dev/null +++ b/plugins/release-manager-as-a-service/package.json @@ -0,0 +1,51 @@ +{ + "name": "@backstage/plugin-release-manager-as-a-service", + "version": "0.1.1", + "main": "src/index.ts", + "types": "src/index.ts", + "license": "Apache-2.0", + "publishConfig": { + "access": "public", + "main": "dist/index.esm.js", + "types": "dist/index.d.ts" + }, + "scripts": { + "build": "backstage-cli plugin:build", + "start": "backstage-cli plugin:serve", + "lint": "backstage-cli lint", + "test": "backstage-cli test", + "diff": "backstage-cli plugin:diff", + "prepack": "backstage-cli prepack", + "postpack": "backstage-cli postpack", + "clean": "backstage-cli clean" + }, + "dependencies": { + "@backstage/core": "^0.7.3", + "@backstage/integration": "^0.5.1", + "@backstage/theme": "^0.2.5", + "@material-ui/core": "^4.11.0", + "@material-ui/icons": "^4.9.1", + "@material-ui/lab": "4.0.0-alpha.45", + "@octokit/rest": "^18.0.12", + "date-fns": "^2.19.0", + "react-dom": "^16.13.1", + "react-router": "6.0.0-beta.0", + "react-use": "^15.3.3", + "react": "^16.13.1" + }, + "devDependencies": { + "@backstage/cli": "^0.6.6", + "@backstage/dev-utils": "^0.1.13", + "@backstage/test-utils": "^0.1.9", + "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react": "^11.2.5", + "@testing-library/user-event": "^12.0.7", + "@types/jest": "^26.0.7", + "@types/node": "^14.14.32", + "msw": "^0.21.2", + "cross-fetch": "^3.0.6" + }, + "files": [ + "dist" + ] +} diff --git a/plugins/release-manager-as-a-service/src/ReleaseManagerAsAService.tsx b/plugins/release-manager-as-a-service/src/ReleaseManagerAsAService.tsx new file mode 100644 index 0000000000..35fd88b03f --- /dev/null +++ b/plugins/release-manager-as-a-service/src/ReleaseManagerAsAService.tsx @@ -0,0 +1,179 @@ +/* + * 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 { Alert } from '@material-ui/lab'; +import { CircularProgress, makeStyles } from '@material-ui/core'; +import { useAsync } from 'react-use'; +import React, { useState } from 'react'; +import { useApi, ContentHeader, ErrorBoundary } from '@backstage/core'; + +import { CreateRc } from './cards/createRc/CreateRc'; +import { getGitHubBatchInfo } from './sideEffects/getGitHubBatchInfo'; +import { Info } from './cards/info/Info'; +import { Patch } from './cards/patchRc/Patch'; +import { + ComponentConfigCreateRc, + ComponentConfigPatch, + ComponentConfigPromoteRc, + GhGetBranchResponse, + GhGetReleaseResponse, + GhGetRepositoryResponse, + Project, + SetRefetch, +} from './types/types'; +import { PromoteRc } from './cards/promoteRc/PromoteRc'; +import { releaseManagerAsAServiceApiRef } from './api/serviceApiRef'; +import { + ApiClientContext, + useApiClientContext, +} from './components/ProjectContext'; +import { RMaaSApiClient } from './api/RMaaSApiClient'; + +interface ReleaseManagerAsAServiceProps { + project: Project; + components?: { + default?: { + createRc?: ComponentConfigCreateRc; + promoteRc?: ComponentConfigPromoteRc; + patch?: ComponentConfigPatch; + }; + custom?: ({ + project, + setRefetch, + latestRelease, + releaseBranch, + repository, + }: { + project: Project; + setRefetch: SetRefetch; + latestRelease: GhGetReleaseResponse | null; + releaseBranch: GhGetBranchResponse | null; + repository: GhGetRepositoryResponse; + }) => JSX.Element[]; + }; +} + +const useStyles = makeStyles(() => ({ + root: { + maxWidth: '999px', + }, +})); + +export function ReleaseManagerAsAService({ + project, + components, +}: ReleaseManagerAsAServiceProps) { + const pluginApiClient = useApi(releaseManagerAsAServiceApiRef); + const RMaaSApi = new RMaaSApiClient({ + pluginApiClient, + repoPath: `${project.github.org}/${project.github.repo}`, + }); + const classes = useStyles(); + + return ( + +
+ + + +
+
+ ); +} + +function Cards({ project, components }: ReleaseManagerAsAServiceProps) { + const apiClient = useApiClientContext(); + const [refetch, setRefetch] = useState(0); + const gitHubBatchInfo = useAsync(getGitHubBatchInfo({ apiClient }), [ + project, + refetch, + ]); + + if (gitHubBatchInfo.error) { + return {gitHubBatchInfo.error.message}; + } + + if (gitHubBatchInfo.loading) { + return ( +
+ +
+ ); + } + + if (gitHubBatchInfo.value === undefined) { + return Failed to fetch latest GHE release; + } + + if (!gitHubBatchInfo.value.repository.permissions.push) { + return ( + + You lack push permissions for repository "{project.github.org}/ + {project.github.repo}" + + ); + } + + return ( + + + + {components?.default?.createRc?.omit !== true && ( + + )} + + {components?.default?.promoteRc?.omit !== true && ( + + )} + + {components?.default?.patch?.omit !== true && ( + + )} + + {components + ?.custom?.({ + project, + setRefetch, + latestRelease: gitHubBatchInfo.value.latestRelease, + releaseBranch: gitHubBatchInfo.value.releaseBranch, + repository: gitHubBatchInfo.value.repository, + }) + .map((customElement, index) => ( +
{customElement}
+ ))} +
+ ); +} diff --git a/plugins/release-manager-as-a-service/src/api/PluginApiClientConfig.ts b/plugins/release-manager-as-a-service/src/api/PluginApiClientConfig.ts new file mode 100644 index 0000000000..430b005b92 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/api/PluginApiClientConfig.ts @@ -0,0 +1,51 @@ +/* + * 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 { ConfigApi, OAuthApi } from '@backstage/core'; +import { Octokit } from '@octokit/rest'; +import { readGitHubIntegrationConfigs } from '@backstage/integration'; + +export class PluginApiClientConfig { + private readonly githubAuthApi: OAuthApi; + readonly baseUrl: string; + + constructor({ + configApi, + githubAuthApi, + }: { + configApi: ConfigApi; + githubAuthApi: OAuthApi; + }) { + this.githubAuthApi = githubAuthApi; + + const configs = readGitHubIntegrationConfigs( + configApi.getOptionalConfigArray('integrations.github') ?? [], + ); + const githubIntegrationConfig = configs.find(v => v.host === 'github.com'); + this.baseUrl = + githubIntegrationConfig?.apiBaseUrl ?? 'https://api.github.com'; + } + + public async getOctokit() { + const token = await this.githubAuthApi.getAccessToken(['repo']); + + return { + octokit: new Octokit({ + auth: token, + baseUrl: this.baseUrl, + }), + }; + } +} diff --git a/plugins/release-manager-as-a-service/src/api/RMaaSApiClient.ts b/plugins/release-manager-as-a-service/src/api/RMaaSApiClient.ts new file mode 100644 index 0000000000..e67d50e46a --- /dev/null +++ b/plugins/release-manager-as-a-service/src/api/RMaaSApiClient.ts @@ -0,0 +1,412 @@ +/* + * 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 { + GhCompareCommitsResponse, + GhCreateCommitResponse, + GhCreateReferenceResponse, + GhCreateReleaseResponse, + GhCreateTagObjectResponse, + GhGetBranchResponse, + GhGetCommitResponse, + GhGetReleaseResponse, + GhGetRepositoryResponse, + GhMergeResponse, + GhUpdateReferenceResponse, + GhUpdateReleaseResponse, +} from '../types/types'; +import { CalverTagParts } from '../helpers/tagParts/getCalverTagParts'; +import { getRcGheInfo } from '../cards/createRc/getRcGheInfo'; +import { PluginApiClientConfig } from './PluginApiClientConfig'; +import { SemverTagParts } from '../helpers/tagParts/getSemverTagParts'; + +/** + * Docs + * https://github.com/octokit/request.js/#the-data-parameter--set-request-body-directly + */ + +export class RMaaSApiClient { + private readonly pluginApiClient: PluginApiClientConfig; + private readonly repoPath: string; + private readonly githubCommonPath: string; + + constructor({ + pluginApiClient, + repoPath, + }: { + pluginApiClient: PluginApiClientConfig; + repoPath: string; + }) { + this.pluginApiClient = pluginApiClient; + this.repoPath = repoPath; + this.githubCommonPath = `/repos/${this.repoPath}`; + } + + public getRepoPath() { + return this.repoPath; + } + + async getRecentCommits({ + releaseBranchName, + }: { releaseBranchName?: string } = {}) { + const { octokit } = await this.pluginApiClient.getOctokit(); + const sha = releaseBranchName ? `?sha=${releaseBranchName}` : ''; + + const recentCommits: GhGetCommitResponse[] = ( + await octokit.request(`${this.githubCommonPath}/commits${sha}`) + ).data; + + return { recentCommits }; + } + + async getReleases() { + const { octokit } = await this.pluginApiClient.getOctokit(); + + const releases: GhGetReleaseResponse[] = ( + await octokit.request(`${this.githubCommonPath}/releases`) + ).data; + + return { releases }; + } + + async getRelease({ releaseId }: { releaseId: number }) { + const { octokit } = await this.pluginApiClient.getOctokit(); + + const latestRelease: GhGetReleaseResponse = ( + await octokit.request(`${this.githubCommonPath}/releases/${releaseId}`) + ).data; + + return { latestRelease }; + } + + async getRepository() { + const { octokit } = await this.pluginApiClient.getOctokit(); + + const repository: GhGetRepositoryResponse = ( + await octokit.request(this.githubCommonPath) + ).data; + + return { repository }; + } + + async getLatestCommit({ defaultBranch }: { defaultBranch: string }) { + const { octokit } = await this.pluginApiClient.getOctokit(); + + const latestCommit: GhGetCommitResponse = ( + await octokit.request( + `${this.githubCommonPath}/commits/refs/heads/${defaultBranch}`, + ) + ).data; + + return { latestCommit }; + } + + async getBranch({ branchName }: { branchName: string }) { + const { octokit } = await this.pluginApiClient.getOctokit(); + + const branch: GhGetBranchResponse = ( + await octokit.request(`${this.githubCommonPath}/branches/${branchName}`) + ).data; + + return { branch }; + } + + createRc = { + createRef: async ({ + mostRecentSha, + targetBranch, + }: { + mostRecentSha: string; + targetBranch: string; + }) => { + const { octokit } = await this.pluginApiClient.getOctokit(); + + const createdRef: GhCreateReferenceResponse = ( + await octokit.request(`${this.githubCommonPath}/git/refs`, { + method: 'POST', + data: { + ref: `refs/heads/${targetBranch}`, + sha: mostRecentSha, + }, + }) + ).data; + + return { createdRef }; + }, + + getComparison: async ({ + previousReleaseBranch, + nextReleaseBranch, + }: { + previousReleaseBranch: string; + nextReleaseBranch: string; + }) => { + const { octokit } = await this.pluginApiClient.getOctokit(); + + const comparison: GhCompareCommitsResponse = ( + await octokit.request( + `${this.githubCommonPath}/compare/${previousReleaseBranch}...${nextReleaseBranch}`, + ) + ).data; + + return { comparison }; + }, + + createRelease: async ({ + nextGheInfo, + releaseBody, + }: { + nextGheInfo: ReturnType; + releaseBody: string; + }) => { + const { octokit } = await this.pluginApiClient.getOctokit(); + + const createReleaseResponse: GhCreateReleaseResponse = ( + await octokit.request(`${this.githubCommonPath}/releases`, { + method: 'POST', + data: { + tag_name: nextGheInfo.rcReleaseTag, + name: nextGheInfo.releaseName, + target_commitish: nextGheInfo.rcBranch, + body: releaseBody, + prerelease: true, + }, + }) + ).data; + + return { createReleaseResponse }; + }, + }; + + patch = { + createTempCommit: async ({ + tagParts, + releaseBranchTree, + selectedPatchCommit, + }: { + tagParts: SemverTagParts | CalverTagParts; + releaseBranchTree: string; + selectedPatchCommit: GhGetCommitResponse; + }) => { + const { octokit } = await this.pluginApiClient.getOctokit(); + + const tempCommit: GhCreateCommitResponse = ( + await octokit.request(`${this.githubCommonPath}/git/commits`, { + method: 'POST', + data: { + message: `Temporary commit for patch ${tagParts.patch}`, + tree: releaseBranchTree, + parents: [selectedPatchCommit.parents[0].sha], + }, + }) + ).data; + + return { tempCommit }; + }, + + forceBranchHeadToTempCommit: async ({ + releaseBranchName, + tempCommit, + }: { + releaseBranchName: string; + tempCommit: GhCreateCommitResponse; + }) => { + const { octokit } = await this.pluginApiClient.getOctokit(); + + await octokit.request( + `${this.githubCommonPath}/git/refs/heads/${releaseBranchName}`, + { + method: 'PATCH', + data: { + sha: tempCommit.sha, + force: true, + }, + }, + ); + }, + + merge: async ({ base, head }: { base: string; head: string }) => { + const { octokit } = await this.pluginApiClient.getOctokit(); + + const merge: GhMergeResponse = ( + await octokit.request(`${this.githubCommonPath}/merges`, { + method: 'POST', + data: { base, head }, + }) + ).data; + + return { merge }; + }, + + createCherryPickCommit: async ({ + bumpedTag, + selectedPatchCommit, + mergeTree, + releaseBranchSha, + }: { + bumpedTag: string; + selectedPatchCommit: GhGetCommitResponse; + mergeTree: string; + releaseBranchSha: string; + }) => { + const { octokit } = await this.pluginApiClient.getOctokit(); + + const cherryPickCommit: GhCreateCommitResponse = ( + await octokit.request(`${this.githubCommonPath}/git/commits`, { + method: 'POST', + data: { + message: `[patch ${bumpedTag}] ${selectedPatchCommit.commit.message}`, + tree: mergeTree, + parents: [releaseBranchSha], + }, + }) + ).data; + + return { cherryPickCommit }; + }, + + replaceTempCommit: async ({ + releaseBranchName, + cherryPickCommit, + }: { + releaseBranchName: string; + cherryPickCommit: GhCreateCommitResponse; + }) => { + const { octokit } = await this.pluginApiClient.getOctokit(); + + const updatedReference: GhUpdateReferenceResponse = ( + await octokit.request( + `${this.githubCommonPath}/git/refs/heads/${releaseBranchName}`, + { + method: 'PATCH', + data: { + sha: cherryPickCommit.sha, + force: true, + }, + }, + ) + ).data; + + return { updatedReference }; + }, + + createTagObject: async ({ + bumpedTag, + updatedReference, + }: { + bumpedTag: string; + updatedReference: GhUpdateReferenceResponse; + }) => { + const { octokit } = await this.pluginApiClient.getOctokit(); + + const tagObjectResponse: GhCreateTagObjectResponse = ( + await octokit.request(`${this.githubCommonPath}/git/tags`, { + method: 'POST', + data: { + type: 'commit', + message: + 'Tag generated by your friendly neighborhood Release Manager as a Service', + tag: bumpedTag, + object: updatedReference.object.sha, + }, + }) + ).data; + + return { tagObjectResponse }; + }, + + createReference: async ({ + bumpedTag, + tagObjectResponse, + }: { + bumpedTag: string; + tagObjectResponse: GhCreateTagObjectResponse; + }) => { + const { octokit } = await this.pluginApiClient.getOctokit(); + + const reference: GhCreateReferenceResponse = ( + await octokit.request(`${this.githubCommonPath}/git/refs`, { + method: 'POST', + data: { + ref: `refs/tags/${bumpedTag}`, + sha: tagObjectResponse.sha, + }, + }) + ).data; + + return { reference }; + }, + + updateRelease: async ({ + bumpedTag, + latestRelease, + tagParts, + selectedPatchCommit, + }: { + bumpedTag: string; + latestRelease: GhGetReleaseResponse; + tagParts: SemverTagParts | CalverTagParts; + selectedPatchCommit: GhGetCommitResponse; + }) => { + const { octokit } = await this.pluginApiClient.getOctokit(); + + const release: GhUpdateReleaseResponse = ( + await octokit.request( + `${this.githubCommonPath}/releases/${latestRelease.id}`, + { + method: 'PATCH', + data: { + tag_name: bumpedTag, + body: `${latestRelease.body} + + #### [Patch ${tagParts.patch}](${selectedPatchCommit.html_url}) + + ${selectedPatchCommit.commit.message}`, + }, + }, + ) + ).data; + + return { release }; + }, + }; + + promoteRc = { + promoteRelease: async ({ + releaseId, + releaseVersion, + }: { + releaseId: GhGetReleaseResponse['id']; + releaseVersion: string; + }) => { + const { octokit } = await this.pluginApiClient.getOctokit(); + + const release: GhGetReleaseResponse = ( + await octokit.request( + `${this.githubCommonPath}/releases/${releaseId}`, + { + method: 'PATCH', + data: { + tag_name: releaseVersion, + prerelease: false, + }, + }, + ) + ).data; + + return { release }; + }, + }; +} diff --git a/plugins/release-manager-as-a-service/src/api/serviceApiRef.ts b/plugins/release-manager-as-a-service/src/api/serviceApiRef.ts new file mode 100644 index 0000000000..1daea6f4c1 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/api/serviceApiRef.ts @@ -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 { createApiRef } from '@backstage/core'; + +import { PluginApiClientConfig } from './PluginApiClientConfig'; + +export const releaseManagerAsAServiceApiRef = createApiRef( + { + id: 'plugin.release-manager-as-a-service.service', + description: + 'Used by the Release Manager as a Service plugin to make requests', + }, +); diff --git a/plugins/release-manager-as-a-service/src/cards/createRc/CreateRc.test.tsx b/plugins/release-manager-as-a-service/src/cards/createRc/CreateRc.test.tsx new file mode 100644 index 0000000000..d4dba6841e --- /dev/null +++ b/plugins/release-manager-as-a-service/src/cards/createRc/CreateRc.test.tsx @@ -0,0 +1,67 @@ +/* + * 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 { render } from '@testing-library/react'; + +import { + mockCalverProject, + mockNextGheInfo, + mockRcRelease, + mockReleaseBranch, + mockReleaseVersion, + mockSemverProject, + mockApiClient, +} from '../../test-helpers/test-helpers'; +import { TEST_IDS } from '../../test-helpers/test-ids'; + +jest.mock('../../components/ProjectContext', () => ({ + useApiClientContext: () => mockApiClient, +})); +jest.mock('./getRcGheInfo', () => ({ + getRcGheInfo: () => mockNextGheInfo, +})); + +import { CreateRc } from './CreateRc'; + +describe('CreateRc', () => { + it('should display CTA', () => { + const { getByTestId } = render( + , + ); + + expect(getByTestId(TEST_IDS.createRc.cta)).toBeInTheDocument(); + }); + + it('should display select element for semver', () => { + const { getByTestId } = render( + , + ); + + expect(getByTestId(TEST_IDS.createRc.semverSelect)).toBeInTheDocument(); + }); +}); diff --git a/plugins/release-manager-as-a-service/src/cards/createRc/CreateRc.tsx b/plugins/release-manager-as-a-service/src/cards/createRc/CreateRc.tsx new file mode 100644 index 0000000000..22877ec1a3 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/cards/createRc/CreateRc.tsx @@ -0,0 +1,205 @@ +/* + * 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, useEffect } from 'react'; +import { Alert } from '@material-ui/lab'; +import { + Button, + FormControl, + InputLabel, + MenuItem, + Select, + Typography, +} from '@material-ui/core'; +import { useAsyncFn } from 'react-use'; + +import { createGheRc } from './sideEffects/createGheRc'; +import { Differ } from '../../components/Differ'; +import { getRcGheInfo } from './getRcGheInfo'; +import { InfoCardPlus } from '../../components/InfoCardPlus'; +import { + ComponentConfigCreateRc, + GhGetBranchResponse, + GhGetReleaseResponse, + GhGetRepositoryResponse, + Project, + SetRefetch, +} from '../../types/types'; +import { ResponseStepList } from '../../components/ResponseStepList/ResponseStepList'; +import { useStyles } from '../../styles/styles'; +import { useApiClientContext } from '../../components/ProjectContext'; +import { SEMVER_PARTS } from '../../constants/constants'; +import { TEST_IDS } from '../../test-helpers/test-ids'; + +interface CreateRcProps { + defaultBranch: GhGetRepositoryResponse['default_branch']; + latestRelease: GhGetReleaseResponse | null; + project: Project; + releaseBranch: GhGetBranchResponse | null; + setRefetch: SetRefetch; + successCb?: ComponentConfigCreateRc['successCb']; +} + +export const CreateRc = ({ + defaultBranch, + latestRelease, + project, + releaseBranch, + setRefetch, + successCb, +}: CreateRcProps) => { + const apiClient = useApiClientContext(); + const classes = useStyles(); + + const [semverBumpLevel, setSemverBumpLevel] = useState<'major' | 'minor'>( + SEMVER_PARTS.minor, + ); + const [nextGheInfo, setNextGheInfo] = useState( + getRcGheInfo({ latestRelease, project, semverBumpLevel }), + ); + + useEffect(() => { + setNextGheInfo(getRcGheInfo({ latestRelease, project, semverBumpLevel })); + }, [semverBumpLevel, setNextGheInfo, latestRelease, project]); + + const [createReleaseResponse, callCreateGheRc] = useAsyncFn( + async (...args) => { + const createGheRcResponseSteps = await createGheRc({ + apiClient, + defaultBranch, + latestRelease, + nextGheInfo: args[0], + successCb, + }); + + return createGheRcResponseSteps; + }, + ); + + if (createReleaseResponse.error) { + return ( + {createReleaseResponse.error.message} + ); + } + + const tagAlreadyExists = + latestRelease !== null && + latestRelease.tag_name === nextGheInfo.rcReleaseTag; + const conflictingPreRelease = + latestRelease !== null && latestRelease.prerelease; + + function Description() { + if (conflictingPreRelease) { + return ( + + The most recent release is already a Release Candidate + + ); + } + + if (tagAlreadyExists) { + return ( + + There's already a tag named{' '} + {nextGheInfo.rcReleaseTag} + + ); + } + + return ( +
+ + + + + + + +
+ ); + } + + function CTA() { + if (createReleaseResponse.loading || createReleaseResponse.value) { + return ( + + ); + } + + return ( + + ); + } + + return ( + + + Create Release Candidate + + + {project.versioningStrategy === 'semver' && + latestRelease && + !conflictingPreRelease && ( +
+ + Select bump severity + + + +
+ )} + + + + +
+ ); +}; diff --git a/plugins/release-manager-as-a-service/src/cards/createRc/getRcGheInfo.test.ts b/plugins/release-manager-as-a-service/src/cards/createRc/getRcGheInfo.test.ts new file mode 100644 index 0000000000..0feb95734f --- /dev/null +++ b/plugins/release-manager-as-a-service/src/cards/createRc/getRcGheInfo.test.ts @@ -0,0 +1,88 @@ +/* + * 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 { format } from 'date-fns'; + +import { GhGetReleaseResponse } from '../../types/types'; +import { + mockSemverProject, + mockCalverProject, +} from '../../test-helpers/test-helpers'; +import { getRcGheInfo } from './getRcGheInfo'; + +const injectedDate = format(1611869955783, 'yyyy.MM.dd'); + +describe('getRCGheInfo', () => { + describe('calver', () => { + const latestRelease = { + tag_name: 'rc-2020.01.01_0', + } as GhGetReleaseResponse; + + it('should return correct Ghe info', () => { + expect( + getRcGheInfo({ + project: mockCalverProject, + latestRelease, + semverBumpLevel: 'minor', + injectedDate, + }), + ).toMatchInlineSnapshot(` + Object { + "rcBranch": "rc/2021.01.28", + "rcReleaseTag": "rc-2021.01.28_0", + "releaseName": "Version 2021.01.28", + } + `); + }); + }); + + describe('semver', () => { + const latestRelease = { + tag_name: 'rc-1.1.1', + } as GhGetReleaseResponse; + + it("should return correct Ghe info when there's previous releases", () => { + expect( + getRcGheInfo({ + project: mockSemverProject, + latestRelease, + semverBumpLevel: 'minor', + }), + ).toMatchInlineSnapshot(` + Object { + "rcBranch": "rc/1.2.0", + "rcReleaseTag": "rc-1.2.0", + "releaseName": "Version 1.2.0", + } + `); + }); + + it("should return correct Ghe info when there's no previous release", () => { + expect( + getRcGheInfo({ + project: mockSemverProject, + latestRelease: null, + semverBumpLevel: 'minor', + }), + ).toMatchInlineSnapshot(` + Object { + "rcBranch": "rc/0.0.1", + "rcReleaseTag": "rc-0.0.1", + "releaseName": "Version 0.0.1", + } + `); + }); + }); +}); diff --git a/plugins/release-manager-as-a-service/src/cards/createRc/getRcGheInfo.ts b/plugins/release-manager-as-a-service/src/cards/createRc/getRcGheInfo.ts new file mode 100644 index 0000000000..6054117d9c --- /dev/null +++ b/plugins/release-manager-as-a-service/src/cards/createRc/getRcGheInfo.ts @@ -0,0 +1,60 @@ +/* + * 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 { format } from 'date-fns'; + +import { getBumpedSemverTagParts } from '../../helpers/getBumpedTag'; +import { getSemverTagParts } from '../../helpers/tagParts/getSemverTagParts'; +import { Project, GhGetReleaseResponse } from '../../types/types'; +import { SEMVER_PARTS } from '../../constants/constants'; + +export const getRcGheInfo = ({ + project, + latestRelease, + semverBumpLevel, + injectedDate = format(new Date(), 'yyyy.MM.dd'), // '0012-01-01T13:37:00.000Z' +}: { + project: Project; + latestRelease: GhGetReleaseResponse | null; + semverBumpLevel: keyof typeof SEMVER_PARTS; + injectedDate?: string; +}) => { + if (project.versioningStrategy === 'calver') { + return { + rcBranch: `rc/${injectedDate}`, + rcReleaseTag: `rc-${injectedDate}_0`, + releaseName: `Version ${injectedDate}`, + }; + } + + if (!latestRelease) { + return { + rcBranch: 'rc/0.0.1', + rcReleaseTag: 'rc-0.0.1', + releaseName: 'Version 0.0.1', + }; + } + + const tagParts = getSemverTagParts(latestRelease.tag_name); + const { bumpedTagParts } = getBumpedSemverTagParts(tagParts, semverBumpLevel); + + const bumpedTag = `${bumpedTagParts.major}.${bumpedTagParts.minor}.${bumpedTagParts.patch}`; + + return { + rcBranch: `rc/${bumpedTag}`, + rcReleaseTag: `rc-${bumpedTag}`, + releaseName: `Version ${bumpedTag}`, + }; +}; diff --git a/plugins/release-manager-as-a-service/src/cards/createRc/sideEffects/createGheRc.test.ts b/plugins/release-manager-as-a-service/src/cards/createRc/sideEffects/createGheRc.test.ts new file mode 100644 index 0000000000..a94cd682ee --- /dev/null +++ b/plugins/release-manager-as-a-service/src/cards/createRc/sideEffects/createGheRc.test.ts @@ -0,0 +1,59 @@ +/* + * 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 { + mockDefaultBranch, + mockReleaseVersion, + mockNextGheInfo, + mockApiClient, +} from '../../../test-helpers/test-helpers'; +import { createGheRc } from './createGheRc'; + +describe('createGheRc', () => { + beforeEach(jest.clearAllMocks); + + it('should work', async () => { + const result = await createGheRc({ + apiClient: mockApiClient, + defaultBranch: mockDefaultBranch, + latestRelease: mockReleaseVersion, + nextGheInfo: mockNextGheInfo, + }); + + expect(result).toMatchInlineSnapshot(` + Array [ + Object { + "link": "mock_latestCommit_html_url", + "message": "Fetched latest commit from \\"mock_defaultBranch\\"", + "secondaryMessage": "with message \\"mock_latestCommit_message\\"", + }, + Object { + "message": "Cut Release Branch", + "secondaryMessage": "with ref \\"mock_createRef_ref\\"", + }, + Object { + "link": "mock_compareCommits_html_url", + "message": "Fetched commit comparision", + "secondaryMessage": "rc/1.2.3...rc/1.2.3", + }, + Object { + "link": "mock_createRelease_html_url", + "message": "Created Release Candidate \\"mock_createRelease_name\\"", + "secondaryMessage": "with tag \\"rc-1.2.3\\"", + }, + ] + `); + }); +}); diff --git a/plugins/release-manager-as-a-service/src/cards/createRc/sideEffects/createGheRc.ts b/plugins/release-manager-as-a-service/src/cards/createRc/sideEffects/createGheRc.ts new file mode 100644 index 0000000000..29491fb13e --- /dev/null +++ b/plugins/release-manager-as-a-service/src/cards/createRc/sideEffects/createGheRc.ts @@ -0,0 +1,129 @@ +/* + * 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 { getRcGheInfo } from '../getRcGheInfo'; +import { + ComponentConfigCreateRc, + GhCreateReferenceResponse, + GhGetReleaseResponse, + GhGetRepositoryResponse, + ResponseStep, +} from '../../../types/types'; +import { RMaaSApiClient } from '../../../api/RMaaSApiClient'; +import { ReleaseManagerAsAServiceError } from '../../../errors/ReleaseManagerAsAServiceError'; + +interface CreateGheRC { + apiClient: RMaaSApiClient; + defaultBranch: GhGetRepositoryResponse['default_branch']; + latestRelease: GhGetReleaseResponse | null; + nextGheInfo: ReturnType; + successCb?: ComponentConfigCreateRc['successCb']; +} + +export async function createGheRc({ + apiClient, + defaultBranch, + latestRelease, + nextGheInfo, + successCb, +}: CreateGheRC) { + const responseSteps: ResponseStep[] = []; + + /** + * 1. Get the default branch's most recent commit + */ + const { latestCommit } = await apiClient.getLatestCommit({ + defaultBranch, + }); + responseSteps.push({ + message: `Fetched latest commit from "${defaultBranch}"`, + secondaryMessage: `with message "${latestCommit.commit.message}"`, + link: latestCommit.html_url, + }); + + /** + * 2. Create a new ref based on the default branch's most recent sha + */ + const mostRecentSha = latestCommit.sha; + let createdRef: GhCreateReferenceResponse; + try { + createdRef = ( + await apiClient.createRc.createRef({ + mostRecentSha, + targetBranch: nextGheInfo.rcBranch, + }) + ).createdRef; + } catch (error) { + if (error.body.message === 'Reference already exists') { + throw new ReleaseManagerAsAServiceError( + `Branch "${nextGheInfo.rcBranch}" already exists: .../tree/${nextGheInfo.rcBranch}`, + ); + } + throw error; + } + responseSteps.push({ + message: 'Cut Release Branch', + secondaryMessage: `with ref "${createdRef.ref}"`, + }); + + /** + * 3. Compose a body for the release + */ + const previousReleaseBranch = latestRelease + ? latestRelease.target_commitish + : defaultBranch; + const nextReleaseBranch = nextGheInfo.rcBranch; + const { comparison } = await apiClient.createRc.getComparison({ + previousReleaseBranch, + nextReleaseBranch, + }); + const releaseBody = `**Compare** ${comparison.html_url} + +**Ahead by** ${comparison.ahead_by} commits + +**Release branch** ${createdRef.ref} + +--- + +`; + responseSteps.push({ + message: 'Fetched commit comparision', + secondaryMessage: `${previousReleaseBranch}...${nextReleaseBranch}`, + link: comparison.html_url, + }); + + /** + * 4. Creates the release itself in GHE + */ + const { createReleaseResponse } = await apiClient.createRc.createRelease({ + nextGheInfo, + releaseBody, + }); + responseSteps.push({ + message: `Created Release Candidate "${createReleaseResponse.name}"`, + secondaryMessage: `with tag "${nextGheInfo.rcReleaseTag}"`, + link: createReleaseResponse.html_url, + }); + + await successCb?.({ + gitHubReleaseUrl: createReleaseResponse.html_url, + gitHubReleaseName: createReleaseResponse.name, + comparisonUrl: comparison.html_url, + previousTag: latestRelease?.tag_name, + createdTag: createReleaseResponse.tag_name, + }); + + return responseSteps; +} diff --git a/plugins/release-manager-as-a-service/src/cards/info/Info.test.tsx b/plugins/release-manager-as-a-service/src/cards/info/Info.test.tsx new file mode 100644 index 0000000000..2c1176aa62 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/cards/info/Info.test.tsx @@ -0,0 +1,38 @@ +/* + * 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 { render } from '@testing-library/react'; + +import { + mockCalverProject, + mockReleaseBranch, +} from '../../test-helpers/test-helpers'; +import { TEST_IDS } from '../../test-helpers/test-ids'; +import { Info } from './Info'; + +describe('Info', () => { + it('should return early if no latestRelease exists', () => { + const { getByTestId } = render( + , + ); + + expect(getByTestId(TEST_IDS.info.info)).toBeInTheDocument(); + }); +}); diff --git a/plugins/release-manager-as-a-service/src/cards/info/Info.tsx b/plugins/release-manager-as-a-service/src/cards/info/Info.tsx new file mode 100644 index 0000000000..3d27e24297 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/cards/info/Info.tsx @@ -0,0 +1,133 @@ +/* + * 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 { Link, Typography } from '@material-ui/core'; + +import { Differ } from '../../components/Differ'; +import { InfoCardPlus } from '../../components/InfoCardPlus'; +import { + GhGetBranchResponse, + GhGetReleaseResponse, + Project, +} from '../../types/types'; +import { useStyles } from '../../styles/styles'; +import { TEST_IDS } from '../../test-helpers/test-ids'; +import rmaasFlowImage from './rmaas-flow.png'; + +interface InfoCardProps { + releaseBranch: GhGetBranchResponse | null; + latestRelease: GhGetReleaseResponse | null; + project: Project; +} + +export const Info = ({ + releaseBranch, + latestRelease, + project, +}: InfoCardProps) => { + const classes = useStyles(); + + return ( + +
+ Terminology + + + GitHub Enterprise (GHE): The source control system + where releases reside in a practical sense. Read more about GitHub + releases{' '} + + here + + . Note that this plugin works just as well with a non-enterprise + account. + + + + Release Candidate: A GHE pre-release intended + primarily for internal testing + + + + Release Version: A GHE release intended for end users + +
+ +
+ Flow + + + RMaaS is built with a specific flow in mind. For example, it assumes + your project is configured to react to tags prefixed with rc or{' '} + version. + + + + Here's an overview of the flow: + + + rmaas-flow +
+ +
+ Details + + + Repository:{' '} + + + + {project.slack && ( + + Slack channel:{' '} + + #{project.slack.channel} + + ) : ( + <>(#{project.slack.channel}) + ) + } + /> + + )} + + + Versioning strategy:{' '} + + + + + Latest release branch:{' '} + + + + + Latest release: + +
+
+ ); +}; diff --git a/plugins/release-manager-as-a-service/src/cards/info/rmaas-flow.png b/plugins/release-manager-as-a-service/src/cards/info/rmaas-flow.png new file mode 100644 index 0000000000000000000000000000000000000000..947f9c0f35da00b84f1267fee8cd06e9c286d1e8 GIT binary patch literal 77527 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 { + it('should return early if no latestRelease exists', () => { + const { getByTestId } = render( + , + ); + + expect( + getByTestId(TEST_IDS.components.noLatestRelease), + ).toBeInTheDocument(); + }); +}); diff --git a/plugins/release-manager-as-a-service/src/cards/patchRc/Patch.tsx b/plugins/release-manager-as-a-service/src/cards/patchRc/Patch.tsx new file mode 100644 index 0000000000..0b5a9fd9aa --- /dev/null +++ b/plugins/release-manager-as-a-service/src/cards/patchRc/Patch.tsx @@ -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 React from 'react'; +import { Typography } from '@material-ui/core'; + +import { getBumpedTag } from '../../helpers/getBumpedTag'; +import { InfoCardPlus } from '../../components/InfoCardPlus'; +import { NoLatestRelease } from '../../components/NoLatestRelease'; +import { + ComponentConfigPatch, + GhGetBranchResponse, + GhGetReleaseResponse, + Project, + SetRefetch, +} from '../../types/types'; +import { useStyles } from '../../styles/styles'; +import { PatchBody } from './PatchBody'; + +interface PatchProps { + latestRelease: GhGetReleaseResponse | null; + project: Project; + releaseBranch: GhGetBranchResponse | null; + setRefetch: SetRefetch; + successCb?: ComponentConfigPatch['successCb']; +} + +export const Patch = ({ + latestRelease, + project, + releaseBranch, + setRefetch, + successCb, +}: PatchProps) => { + const classes = useStyles(); + + function Body() { + if (latestRelease === null) { + return ; + } + + const { bumpedTag, tagParts } = getBumpedTag({ + project, + tag: latestRelease.tag_name, + bumpLevel: 'patch', + }); + + return ( + + ); + } + + return ( + + + Patch Release {latestRelease?.prerelease ? 'Candidate' : 'Version'} + + + + + ); +}; diff --git a/plugins/release-manager-as-a-service/src/cards/patchRc/PatchBody.test.tsx b/plugins/release-manager-as-a-service/src/cards/patchRc/PatchBody.test.tsx new file mode 100644 index 0000000000..74f701a120 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/cards/patchRc/PatchBody.test.tsx @@ -0,0 +1,77 @@ +/* + * 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 { render, waitFor, screen } from '@testing-library/react'; + +import { + mockBumpedTag, + mockRcRelease, + mockReleaseBranch, + mockReleaseVersion, + mockTagParts, + mockApiClient, +} from '../../test-helpers/test-helpers'; + +jest.mock('../../components/ProjectContext', () => ({ + useApiClientContext: () => mockApiClient, +})); + +import { PatchBody } from './PatchBody'; +import { TEST_IDS } from '../../test-helpers/test-ids'; + +describe('PatchBody', () => { + beforeEach(jest.clearAllMocks); + + it('should render error', async () => { + mockApiClient.getBranch.mockImplementationOnce(() => { + throw new Error('lmao hehe'); + }); + + const { getByTestId } = render( + , + ); + + expect(getByTestId(TEST_IDS.patch.loading)).toBeInTheDocument(); + + await waitFor(() => screen.getByTestId(TEST_IDS.patch.error)); + + expect(getByTestId(TEST_IDS.patch.error)).toBeInTheDocument(); + }); + + it('should render not-prerelease description', async () => { + const { getByTestId } = render( + , + ); + + expect(getByTestId(TEST_IDS.patch.loading)).toBeInTheDocument(); + + await waitFor(() => screen.getByTestId(TEST_IDS.patch.notPrerelease)); + + expect(getByTestId(TEST_IDS.patch.notPrerelease)).toBeInTheDocument(); + }); +}); diff --git a/plugins/release-manager-as-a-service/src/cards/patchRc/PatchBody.tsx b/plugins/release-manager-as-a-service/src/cards/patchRc/PatchBody.tsx new file mode 100644 index 0000000000..a30515c9b6 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/cards/patchRc/PatchBody.tsx @@ -0,0 +1,301 @@ +/* + * 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 { Alert, AlertTitle } from '@material-ui/lab'; +import { useAsync, useAsyncFn } from 'react-use'; +import FileCopyIcon from '@material-ui/icons/FileCopy'; +import Paper from '@material-ui/core/Paper'; +import { + Button, + Checkbox, + CircularProgress, + IconButton, + Link, + List, + ListItem, + ListItemIcon, + ListItemSecondaryAction, + ListItemText, + Typography, +} from '@material-ui/core'; +import OpenInNewIcon from '@material-ui/icons/OpenInNew'; + +import { Differ } from '../../components/Differ'; +import { + ComponentConfigPatch, + GhGetBranchResponse, + GhGetCommitResponse, + GhGetReleaseResponse, + SetRefetch, +} from '../../types/types'; +import { CalverTagParts } from '../../helpers/tagParts/getCalverTagParts'; +import { ResponseStepList } from '../../components/ResponseStepList/ResponseStepList'; +import { SemverTagParts } from '../../helpers/tagParts/getSemverTagParts'; +import { useApiClientContext } from '../../components/ProjectContext'; +import { useStyles } from '../../styles/styles'; +import { TEST_IDS } from '../../test-helpers/test-ids'; +import { patch } from './sideEffects/patch'; + +interface PatchBodyProps { + bumpedTag: string; + latestRelease: GhGetReleaseResponse; + releaseBranch: GhGetBranchResponse | null; + setRefetch: SetRefetch; + successCb?: ComponentConfigPatch['successCb']; + tagParts: NonNullable; +} + +export const PatchBody = ({ + bumpedTag, + latestRelease, + releaseBranch, + setRefetch, + successCb, + tagParts, +}: PatchBodyProps) => { + const apiClient = useApiClientContext(); + const [checkedCommitIndex, setCheckedCommitIndex] = useState(-1); + + const gheDataResponse = useAsync(async () => { + const [ + { branch: releaseBranchResponse }, + { recentCommits }, + ] = await Promise.all([ + apiClient.getBranch({ branchName: latestRelease.target_commitish }), + apiClient.getRecentCommits(), + ]); + + const { + recentCommits: recentReleaseBranchCommits, + } = await apiClient.getRecentCommits({ + releaseBranchName: releaseBranchResponse.name, + }); + + return { + releaseBranch: releaseBranchResponse, + recentReleaseBranchCommits, + recentCommits, + }; + }); + + const [patchGheRcResponse, patchGheRcFn] = useAsyncFn(async (...args) => { + const selectedPatchCommit: GhGetCommitResponse = args[0]; + const patchResponseSteps = await patch({ + apiClient, + bumpedTag, + latestRelease, + selectedPatchCommit, + successCb, + tagParts, + }); + + return patchResponseSteps; + }); + + if (gheDataResponse.error) { + return ( + + {gheDataResponse.error.message} + + ); + } + if (patchGheRcResponse.error) { + return {patchGheRcResponse.error.message}; + } + if (gheDataResponse.loading) { + return ; + } + + function Description() { + const classes = useStyles(); + + return ( + <> + {!latestRelease.prerelease && ( + + + The current GHE release is a Release Version + + It's still possible to patch it, but be extra mindful of changes + + )} + + + + + + ); + } + + function CommitList() { + if (!gheDataResponse.value?.recentCommits) { + return null; + } + + return ( + + {gheDataResponse.value.recentCommits.map((commit, index) => { + const commitExistsOnReleaseBranch = !!gheDataResponse.value?.recentReleaseBranchCommits.find( + ({ sha }) => { + return sha === commit.sha; + }, + ); + + return ( +
+ {commitExistsOnReleaseBranch && ( + + {' '} + Already exists on {releaseBranch?.name} + + )} + + 0) || + commitExistsOnReleaseBranch + } + role={undefined} + dense + button + onClick={() => { + if (index === checkedCommitIndex) { + setCheckedCommitIndex(-1); + } else { + setCheckedCommitIndex(index); + } + }} + > + + + + + + {commit.sha}{' '} + + @{commit.author.login} + + + } + /> + + + { + const repoPath = apiClient.getRepoPath(); + + const newTab = window.open( + `https://github.com/${repoPath}/compare/${releaseBranch?.name}...${commit.sha}`, + '_blank', + ); + newTab?.focus(); + }} + > + + + + +
+ ); + })} +
+ ); + } + + function CTA() { + if (patchGheRcResponse.loading || patchGheRcResponse.value) { + return ( + + ); + } + + if (!gheDataResponse.value?.recentCommits[checkedCommitIndex]) { + return ( + + ); + } + + return ( + + ); + } + + return ( +
+ + + + + +
+ ); +}; diff --git a/plugins/release-manager-as-a-service/src/cards/patchRc/sideEffects/patch.test.ts b/plugins/release-manager-as-a-service/src/cards/patchRc/sideEffects/patch.test.ts new file mode 100644 index 0000000000..7a37c0ce72 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/cards/patchRc/sideEffects/patch.test.ts @@ -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 { + mockBumpedTag, + mockReleaseVersion, + mockSelectedPatchCommit, + mockTagParts, + mockApiClient, +} from '../../../test-helpers/test-helpers'; +import { patch } from './patch'; + +describe('patch', () => { + beforeEach(jest.clearAllMocks); + + it('should work', async () => { + const result = await patch({ + apiClient: mockApiClient, + latestRelease: mockReleaseVersion, + bumpedTag: mockBumpedTag, + selectedPatchCommit: mockSelectedPatchCommit, + tagParts: mockTagParts, + }); + + expect(result).toMatchInlineSnapshot(` + Array [ + Object { + "link": "mock_branch__links_html", + "message": "Fetched release branch \\"rc/1.2.3\\"", + }, + Object { + "message": "Created temporary commit", + "secondaryMessage": "with message \\"mock_commit_message\\"", + }, + Object { + "link": "mock_merge_html_url", + "message": "Merged temporary commit into \\"rc/1.2.3\\"", + "secondaryMessage": "with message \\"mock_merge_commit_message\\"", + }, + Object { + "message": "Cherry-picked patch commit to \\"mock_branch_commit_sha\\"", + "secondaryMessage": "with message \\"undefined\\"", + }, + Object { + "message": "Updated reference \\"mock_reference_ref\\"", + }, + Object { + "message": "Created new tag object", + "secondaryMessage": "with name \\"mock_tag_object_tag\\"", + }, + Object { + "message": "Created new reference \\"mock_reference_ref\\"", + "secondaryMessage": "for tag object \\"mock_tag_object_tag\\"", + }, + Object { + "link": "mock_update_release_html_url", + "message": "Updated release \\"mock_update_release_name\\"", + "secondaryMessage": "with tag mock_update_release_tag_name", + }, + ] + `); + }); +}); diff --git a/plugins/release-manager-as-a-service/src/cards/patchRc/sideEffects/patch.ts b/plugins/release-manager-as-a-service/src/cards/patchRc/sideEffects/patch.ts new file mode 100644 index 0000000000..797ed8229c --- /dev/null +++ b/plugins/release-manager-as-a-service/src/cards/patchRc/sideEffects/patch.ts @@ -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 { + ComponentConfigPatch, + GhGetCommitResponse, + GhGetReleaseResponse, + ResponseStep, +} from '../../../types/types'; +import { CalverTagParts } from '../../../helpers/tagParts/getCalverTagParts'; +import { ReleaseManagerAsAServiceError } from '../../../errors/ReleaseManagerAsAServiceError'; +import { RMaaSApiClient } from '../../../api/RMaaSApiClient'; +import { SemverTagParts } from '../../../helpers/tagParts/getSemverTagParts'; + +interface Patch { + apiClient: RMaaSApiClient; + bumpedTag: string; + latestRelease: GhGetReleaseResponse; + selectedPatchCommit: GhGetCommitResponse; + successCb?: ComponentConfigPatch['successCb']; + tagParts: NonNullable; +} + +/** + * Inspo: https://stackoverflow.com/questions/53859199/how-to-cherry-pick-through-githubs-api + */ +export async function patch({ + apiClient, + bumpedTag, + latestRelease, + selectedPatchCommit, + successCb, + tagParts, +}: Patch) { + const responseSteps: ResponseStep[] = []; + + if (!selectedPatchCommit || !selectedPatchCommit.sha) { + throw new ReleaseManagerAsAServiceError('Invalid commit'); + } + + const releaseBranchName = latestRelease.target_commitish; + /** + * 1. Here is the branch we want to cherry-pick to: + * > branch = GET /repos/$owner/$repo/branches/$branchName + * > branchSha = branch.commit.sha + * > branchTree = branch.commit.commit.tree.sha + */ + const { branch: releaseBranch } = await apiClient.getBranch({ + branchName: releaseBranchName, + }); + const releaseBranchSha = releaseBranch.commit.sha; + const releaseBranchTree = releaseBranch.commit.commit.tree.sha; + responseSteps.push({ + message: `Fetched release branch "${releaseBranch.name}"`, + link: releaseBranch._links.html, + }); + + /** + * 2. Create a temporary commit on the branch, which extends as a sibling of + * the commit we want but contains the current tree of the target branch: + * > parentSha = commit.parents.head // first parent -- there should only be one + * > tempCommit = POST /repos/$owner/$repo/git/commits { "message": "temp", "tree": branchTree, "parents": [parentSha] } + */ + const { tempCommit } = await apiClient.patch.createTempCommit({ + releaseBranchTree, + selectedPatchCommit, + tagParts, + }); + responseSteps.push({ + message: 'Created temporary commit', + secondaryMessage: `with message "${tempCommit.message}"`, + }); + + /** + * 3. Now temporarily force the branch over to that commit: + * > PATCH /repos/$owner/$repo/git/refs/heads/$refName { sha = tempCommit.sha, force = true } + */ + await apiClient.patch.forceBranchHeadToTempCommit({ + tempCommit, + releaseBranchName, + }); + + /** + * 4. Merge the commit we want into this mess: + * > merge = POST /repos/$owner/$repo/merges { "base": branchName, "head": commit.sha } + */ + const { merge } = await apiClient.patch.merge({ + base: releaseBranchName, + head: selectedPatchCommit.sha, + }); + responseSteps.push({ + message: `Merged temporary commit into "${releaseBranchName}"`, + secondaryMessage: `with message "${merge.commit.message}"`, + link: merge.html_url, + }); + + /** + * and get that tree! + * > mergeTree = merge.commit.tree.sha + */ + const mergeTree = merge.commit.tree.sha; + + /** + * 5. Now that we know what the tree should be, create the cherry-pick commit. + * Note that branchSha is the original from up at the top. + * > cherry = POST /repos/$owner/$repo/git/commits { "message": "looks good!", "tree": mergeTree, "parents": [branchSha] } + */ + const { cherryPickCommit } = await apiClient.patch.createCherryPickCommit({ + bumpedTag, + mergeTree, + releaseBranchSha, + selectedPatchCommit, + }); + responseSteps.push({ + message: `Cherry-picked patch commit to "${releaseBranchSha}"`, + secondaryMessage: `with message "${cherryPickCommit.message}"`, + }); + + /** + * 6. Replace the temp commit with the real commit: + * > PATCH /repos/$owner/$repo/git/refs/heads/$refName { sha = cherry.sha, force = true } + */ + const { updatedReference } = await apiClient.patch.replaceTempCommit({ + cherryPickCommit, + releaseBranchName, + }); + responseSteps.push({ + message: `Updated reference "${updatedReference.ref}"`, + }); + + /** + * 7. Create tag object: https://developer.github.com/v3/git/tags/#create-a-tag-object + * > POST /repos/:owner/:repo/git/tags + */ + const { tagObjectResponse } = await apiClient.patch.createTagObject({ + bumpedTag, + updatedReference, + }); + responseSteps.push({ + message: 'Created new tag object', + secondaryMessage: `with name "${tagObjectResponse.tag}"`, + }); + + /** + * 8. Create a reference: https://developer.github.com/v3/git/refs/#create-a-reference + * > POST /repos/:owner/:repo/git/refs + */ + const { reference } = await apiClient.patch.createReference({ + bumpedTag, + tagObjectResponse, + }); + responseSteps.push({ + message: `Created new reference "${reference.ref}"`, + secondaryMessage: `for tag object "${tagObjectResponse.tag}"`, + }); + + /** + * 9. Update release + */ + const { release: updatedRelease } = await apiClient.patch.updateRelease({ + bumpedTag, + latestRelease, + selectedPatchCommit, + tagParts, + }); + responseSteps.push({ + message: `Updated release "${updatedRelease.name}"`, + secondaryMessage: `with tag ${updatedRelease.tag_name}`, + link: updatedRelease.html_url, + }); + + await successCb?.({ + updatedReleaseUrl: updatedRelease.html_url, + updatedReleaseName: updatedRelease.name, + previousTag: latestRelease.tag_name, + patchedTag: updatedRelease.tag_name, + patchCommitUrl: selectedPatchCommit.html_url, + patchCommitMessage: selectedPatchCommit.commit.message, + }); + + return responseSteps; +} diff --git a/plugins/release-manager-as-a-service/src/cards/promoteRc/PromoteRc.test.tsx b/plugins/release-manager-as-a-service/src/cards/promoteRc/PromoteRc.test.tsx new file mode 100644 index 0000000000..d38b2e0991 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/cards/promoteRc/PromoteRc.test.tsx @@ -0,0 +1,61 @@ +/* + * 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 { render } from '@testing-library/react'; + +import { + mockRcRelease, + mockReleaseVersion, +} from '../../test-helpers/test-helpers'; +import { TEST_IDS } from '../../test-helpers/test-ids'; + +jest.mock('./PromoteRcBody', () => ({ + PromoteRcBody: () => ( +
Hello
+ ), +})); + +import { PromoteRc } from './PromoteRc'; + +describe('PromoteRc', () => { + it('return early if no latest release present', () => { + const { getByTestId } = render( + , + ); + + expect( + getByTestId(TEST_IDS.components.noLatestRelease), + ).toBeInTheDocument(); + }); + + it('should display not-rc warning', () => { + const { getByTestId } = render( + , + ); + + expect(getByTestId(TEST_IDS.promoteRc.notRcWarning)).toBeInTheDocument(); + }); + + it('should display PromoteRcBody', () => { + const { getByTestId } = render( + , + ); + + expect( + getByTestId(TEST_IDS.promoteRc.mockedPromoteRcBody), + ).toBeInTheDocument(); + }); +}); diff --git a/plugins/release-manager-as-a-service/src/cards/promoteRc/PromoteRc.tsx b/plugins/release-manager-as-a-service/src/cards/promoteRc/PromoteRc.tsx new file mode 100644 index 0000000000..a591f71cd1 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/cards/promoteRc/PromoteRc.tsx @@ -0,0 +1,80 @@ +/* + * 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, AlertTitle } from '@material-ui/lab'; +import { Typography } from '@material-ui/core'; + +import { InfoCardPlus } from '../../components/InfoCardPlus'; +import { NoLatestRelease } from '../../components/NoLatestRelease'; +import { + ComponentConfigPromoteRc, + GhGetReleaseResponse, + SetRefetch, +} from '../../types/types'; +import { PromoteRcBody } from './PromoteRcBody'; +import { useStyles } from '../../styles/styles'; +import { TEST_IDS } from '../../test-helpers/test-ids'; + +interface PromoteRcProps { + latestRelease: GhGetReleaseResponse | null; + setRefetch: SetRefetch; + successCb?: ComponentConfigPromoteRc['successCb']; +} + +export const PromoteRc = ({ + latestRelease, + setRefetch, + successCb, +}: PromoteRcProps) => { + const classes = useStyles(); + + function Body() { + if (latestRelease === null) { + return ; + } + + if (!latestRelease.prerelease) { + return ( + + Latest GHE release is not a Release Candidate + One can only promote Release Candidates to Release Versions + + ); + } + + return ( + + ); + } + + return ( + + + Promote Release Candidate + + + + + ); +}; diff --git a/plugins/release-manager-as-a-service/src/cards/promoteRc/PromoteRcBody.test.tsx b/plugins/release-manager-as-a-service/src/cards/promoteRc/PromoteRcBody.test.tsx new file mode 100644 index 0000000000..4e78e117b6 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/cards/promoteRc/PromoteRcBody.test.tsx @@ -0,0 +1,35 @@ +/* + * 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 { render } from '@testing-library/react'; + +import { mockRcRelease, mockApiClient } from '../../test-helpers/test-helpers'; +import { TEST_IDS } from '../../test-helpers/test-ids'; + +jest.mock('../../components/ProjectContext', () => ({ + useApiClientContext: () => mockApiClient, +})); +import { PromoteRcBody } from './PromoteRcBody'; + +describe('PromoteRcBody', () => { + it('should display CTA', () => { + const { getByTestId } = render( + , + ); + + expect(getByTestId(TEST_IDS.promoteRc.cta)).toBeInTheDocument(); + }); +}); diff --git a/plugins/release-manager-as-a-service/src/cards/promoteRc/PromoteRcBody.tsx b/plugins/release-manager-as-a-service/src/cards/promoteRc/PromoteRcBody.tsx new file mode 100644 index 0000000000..31eb883dee --- /dev/null +++ b/plugins/release-manager-as-a-service/src/cards/promoteRc/PromoteRcBody.tsx @@ -0,0 +1,100 @@ +/* + * 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 { Button, Typography } from '@material-ui/core'; +import { useAsyncFn } from 'react-use'; + +import { Differ } from '../../components/Differ'; +import { + ComponentConfigPromoteRc, + GhGetReleaseResponse, + SetRefetch, +} from '../../types/types'; +import { promoteGheRc } from './sideEffects/promoteGheRc'; +import { ResponseStepList } from '../../components/ResponseStepList/ResponseStepList'; +import { useApiClientContext } from '../../components/ProjectContext'; +import { useStyles } from '../../styles/styles'; +import { TEST_IDS } from '../../test-helpers/test-ids'; + +interface PromoteRcBodyProps { + rcRelease: GhGetReleaseResponse; + setRefetch: SetRefetch; + successCb?: ComponentConfigPromoteRc['successCb']; +} + +export const PromoteRcBody = ({ + rcRelease, + setRefetch, + successCb, +}: PromoteRcBodyProps) => { + const apiClient = useApiClientContext(); + const classes = useStyles(); + const releaseVersion = rcRelease.tag_name.replace('rc-', 'version-'); + const [promoteGheRcResponse, promoseGheRcFn] = useAsyncFn( + promoteGheRc({ apiClient, rcRelease, releaseVersion, successCb }), + ); + + if (promoteGheRcResponse.error) { + return {promoteGheRcResponse.error.message}; + } + + function Description() { + return ( + <> + + Promotes the current Release Candidate to a Release Version. + + + + + + + ); + } + + function CTA() { + if (promoteGheRcResponse.loading || promoteGheRcResponse.value) { + return ( + + ); + } + + return ( + + ); + } + + return ( +
+ + + +
+ ); +}; diff --git a/plugins/release-manager-as-a-service/src/cards/promoteRc/sideEffects/promoteGheRc.test.ts b/plugins/release-manager-as-a-service/src/cards/promoteRc/sideEffects/promoteGheRc.test.ts new file mode 100644 index 0000000000..d6df61a1f3 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/cards/promoteRc/sideEffects/promoteGheRc.test.ts @@ -0,0 +1,42 @@ +/* + * 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 { + mockRcRelease, + mockApiClient, +} from '../../../test-helpers/test-helpers'; +import { promoteGheRc } from './promoteGheRc'; + +describe('promoteGheRc', () => { + beforeEach(jest.clearAllMocks); + + it('should work', async () => { + const result = await promoteGheRc({ + apiClient: mockApiClient, + rcRelease: mockRcRelease, + releaseVersion: 'version-1.2.3', + })(); + + expect(result).toMatchInlineSnapshot(` + Array [ + Object { + "link": "mock_release_html_url", + "message": "Promoted \\"mock_release_name\\"", + "secondaryMessage": "from \\"rc-2020.01.01_1\\" to \\"mock_release_tag_name\\"", + }, + ] + `); + }); +}); diff --git a/plugins/release-manager-as-a-service/src/cards/promoteRc/sideEffects/promoteGheRc.ts b/plugins/release-manager-as-a-service/src/cards/promoteRc/sideEffects/promoteGheRc.ts new file mode 100644 index 0000000000..91371b72a6 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/cards/promoteRc/sideEffects/promoteGheRc.ts @@ -0,0 +1,60 @@ +/* + * 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 { + ComponentConfigPromoteRc, + GhGetReleaseResponse, + ResponseStep, +} from '../../../types/types'; +import { RMaaSApiClient } from '../../../api/RMaaSApiClient'; + +interface PromoteGheRc { + apiClient: RMaaSApiClient; + rcRelease: GhGetReleaseResponse; + releaseVersion: string; + successCb?: ComponentConfigPromoteRc['successCb']; +} + +export function promoteGheRc({ + apiClient, + rcRelease, + releaseVersion, + successCb, +}: PromoteGheRc) { + return async (): Promise => { + const responseSteps: ResponseStep[] = []; + + const { release } = await apiClient.promoteRc.promoteRelease({ + releaseId: rcRelease.id, + releaseVersion, + }); + responseSteps.push({ + message: `Promoted "${release.name}"`, + secondaryMessage: `from "${rcRelease.tag_name}" to "${release.tag_name}"`, + link: release.html_url, + }); + + await successCb?.({ + gitHubReleaseUrl: release.html_url, + gitHubReleaseName: release.name, + previousTagUrl: rcRelease.html_url, + previousTag: rcRelease.tag_name, + updatedTagUrl: release.html_url, + updatedTag: release.tag_name, + }); + + return responseSteps; + }; +} diff --git a/plugins/release-manager-as-a-service/src/components/Differ.tsx b/plugins/release-manager-as-a-service/src/components/Differ.tsx new file mode 100644 index 0000000000..7af998792b --- /dev/null +++ b/plugins/release-manager-as-a-service/src/components/Differ.tsx @@ -0,0 +1,79 @@ +/* + * 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, { ReactNode } from 'react'; +import { grey } from '@material-ui/core/colors'; +import CallSplitIcon from '@material-ui/icons/CallSplit'; +import ChatIcon from '@material-ui/icons/Chat'; +import DynamicFeedIcon from '@material-ui/icons/DynamicFeed'; +import GitHubIcon from '@material-ui/icons/GitHub'; +import LocalOfferIcon from '@material-ui/icons/LocalOffer'; + +interface DifferProps { + next?: string | ReactNode; + prev?: string; + prefix?: string; + icon?: 'tag' | 'branch' | 'ghe' | 'slack' | 'versioning'; +} + +const Icon = ({ icon }: { icon: DifferProps['icon'] }) => { + switch (icon) { + case 'tag': + return ( + + ); + + case 'branch': + return ( + + ); + + case 'ghe': + return ( + + ); + + case 'slack': + return ; + + case 'versioning': + return ( + + ); + + default: + return null; + } +}; + +export const Differ = ({ prev, next, prefix, icon }: DifferProps) => { + return ( + <> + {icon && ( + + {' '} + + )} + {prefix && {prefix}: } + {prev && ( + <> + {prev} + {' → '} + + )} + {next ?? 'None'} + + ); +}; diff --git a/plugins/release-manager-as-a-service/src/components/Divider.test.tsx b/plugins/release-manager-as-a-service/src/components/Divider.test.tsx new file mode 100644 index 0000000000..7a5ac8ad09 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/components/Divider.test.tsx @@ -0,0 +1,28 @@ +/* + * 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 { render } from '@testing-library/react'; + +import { TEST_IDS } from '../test-helpers/test-ids'; +import { Divider } from './Divider'; + +describe('Divider', () => { + it('render Divider', () => { + const { getByTestId } = render(); + + expect(getByTestId(TEST_IDS.components.divider)).toBeInTheDocument(); + }); +}); diff --git a/plugins/release-manager-as-a-service/src/components/Divider.tsx b/plugins/release-manager-as-a-service/src/components/Divider.tsx new file mode 100644 index 0000000000..879a55b98a --- /dev/null +++ b/plugins/release-manager-as-a-service/src/components/Divider.tsx @@ -0,0 +1,27 @@ +/* + * 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 { Divider as MaterialDivider } from '@material-ui/core'; + +import { TEST_IDS } from '../test-helpers/test-ids'; + +export const Divider = () => { + return ( +
+ +
+ ); +}; diff --git a/plugins/release-manager-as-a-service/src/components/InfoCardPlus.test.tsx b/plugins/release-manager-as-a-service/src/components/InfoCardPlus.test.tsx new file mode 100644 index 0000000000..9486d2ac94 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/components/InfoCardPlus.test.tsx @@ -0,0 +1,28 @@ +/* + * 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 { render } from '@testing-library/react'; + +import { TEST_IDS } from '../test-helpers/test-ids'; +import { InfoCardPlus } from './InfoCardPlus'; + +describe('InfoCardPlus', () => { + it('render InfoCardPlus', () => { + const { getByTestId } = render(); + + expect(getByTestId(TEST_IDS.info.infoCardPlus)).toBeInTheDocument(); + }); +}); diff --git a/plugins/release-manager-as-a-service/src/components/InfoCardPlus.tsx b/plugins/release-manager-as-a-service/src/components/InfoCardPlus.tsx new file mode 100644 index 0000000000..9adf295064 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/components/InfoCardPlus.tsx @@ -0,0 +1,39 @@ +/* + * 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 { InfoCard } from '@backstage/core'; +import { makeStyles } from '@material-ui/core'; + +import { TEST_IDS } from '../test-helpers/test-ids'; + +const useStyles = makeStyles(() => ({ + card: { + marginBottom: '3em', + }, +})); + +export const InfoCardPlus = ({ children }: { children?: React.ReactNode }) => { + const classes = useStyles(); + + return ( +
+ {children} +
+ ); +}; diff --git a/plugins/release-manager-as-a-service/src/components/NoLatestRelease.test.tsx b/plugins/release-manager-as-a-service/src/components/NoLatestRelease.test.tsx new file mode 100644 index 0000000000..83eb4f6802 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/components/NoLatestRelease.test.tsx @@ -0,0 +1,30 @@ +/* + * 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 { render } from '@testing-library/react'; + +import { TEST_IDS } from '../test-helpers/test-ids'; +import { NoLatestRelease } from './NoLatestRelease'; + +describe('NoLatestRelease', () => { + it('render NoLatestRelease', () => { + const { getByTestId } = render(); + + expect( + getByTestId(TEST_IDS.components.noLatestRelease), + ).toBeInTheDocument(); + }); +}); diff --git a/plugins/release-manager-as-a-service/src/components/NoLatestRelease.tsx b/plugins/release-manager-as-a-service/src/components/NoLatestRelease.tsx new file mode 100644 index 0000000000..85909d526d --- /dev/null +++ b/plugins/release-manager-as-a-service/src/components/NoLatestRelease.tsx @@ -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 React from 'react'; +import { Alert } from '@material-ui/lab'; + +import { useStyles } from '../styles/styles'; +import { TEST_IDS } from '../test-helpers/test-ids'; + +export const NoLatestRelease = () => { + const classes = useStyles(); + + return ( + + Unable to find any GHE releases + + ); +}; diff --git a/plugins/release-manager-as-a-service/src/components/ProjectContext.ts b/plugins/release-manager-as-a-service/src/components/ProjectContext.ts new file mode 100644 index 0000000000..26a195b5ed --- /dev/null +++ b/plugins/release-manager-as-a-service/src/components/ProjectContext.ts @@ -0,0 +1,33 @@ +/* + * 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 { RMaaSApiClient } from '../api/RMaaSApiClient'; +import { ReleaseManagerAsAServiceError } from '../errors/ReleaseManagerAsAServiceError'; + +export const ApiClientContext = createContext( + undefined, +); + +export const useApiClientContext = () => { + const apiClient = useContext(ApiClientContext); + + if (!apiClient) { + throw new ReleaseManagerAsAServiceError('apiClient not found'); + } + + return apiClient; +}; diff --git a/plugins/release-manager-as-a-service/src/components/ReloadButton.test.tsx b/plugins/release-manager-as-a-service/src/components/ReloadButton.test.tsx new file mode 100644 index 0000000000..c7a52bcdf9 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/components/ReloadButton.test.tsx @@ -0,0 +1,28 @@ +/* + * 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 { render } from '@testing-library/react'; + +import { TEST_IDS } from '../test-helpers/test-ids'; +import { ReloadButton } from './ReloadButton'; + +describe('ReloadButton', () => { + it('render ReloadButton', () => { + const { getByTestId } = render(); + + expect(getByTestId(TEST_IDS.components.reloadButton)).toBeInTheDocument(); + }); +}); diff --git a/plugins/release-manager-as-a-service/src/components/ReloadButton.tsx b/plugins/release-manager-as-a-service/src/components/ReloadButton.tsx new file mode 100644 index 0000000000..44e7ce89b3 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/components/ReloadButton.tsx @@ -0,0 +1,35 @@ +/* + * 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 } from '@material-ui/core'; + +import { TEST_IDS } from '../test-helpers/test-ids'; + +interface ReloadButtonProps { + style?: React.CSSProperties; +} + +export const ReloadButton = ({ style }: ReloadButtonProps) => ( + +); diff --git a/plugins/release-manager-as-a-service/src/components/ResponseStepList/ResponseStepList.test.tsx b/plugins/release-manager-as-a-service/src/components/ResponseStepList/ResponseStepList.test.tsx new file mode 100644 index 0000000000..3aa0986d33 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/components/ResponseStepList/ResponseStepList.test.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 { render } from '@testing-library/react'; + +import { TEST_IDS } from '../../test-helpers/test-ids'; +import { ResponseStepList } from './ResponseStepList'; + +describe('ResponseStepList', () => { + it('should render loading state when loading', () => { + const { getByTestId } = render( + , + ); + + expect( + getByTestId(TEST_IDS.components.circularProgress), + ).toBeInTheDocument(); + }); + + it('should render loading state when no responseSteps', () => { + const { getByTestId } = render( + , + ); + + expect( + getByTestId(TEST_IDS.components.circularProgress), + ).toBeInTheDocument(); + }); + + it('should render dialog content when loading is completed', () => { + const { getByTestId } = render( + , + ); + + expect( + getByTestId(TEST_IDS.components.responseStepListDialogContent), + ).toBeInTheDocument(); + }); +}); diff --git a/plugins/release-manager-as-a-service/src/components/ResponseStepList/ResponseStepList.tsx b/plugins/release-manager-as-a-service/src/components/ResponseStepList/ResponseStepList.tsx new file mode 100644 index 0000000000..e4a3d78308 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/components/ResponseStepList/ResponseStepList.tsx @@ -0,0 +1,113 @@ +/* + * 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, { PropsWithChildren } from 'react'; +import { List, CircularProgress } from '@material-ui/core'; +import Button from '@material-ui/core/Button'; +import Dialog from '@material-ui/core/Dialog'; +import DialogActions from '@material-ui/core/DialogActions'; +import DialogContent from '@material-ui/core/DialogContent'; +import DialogTitle from '@material-ui/core/DialogTitle'; + +import { ResponseStep, SetRefetch } from '../../types/types'; +import { TEST_IDS } from '../../test-helpers/test-ids'; +import { ResponseStepListItem } from './ResponseStepListItem'; + +interface ResponseStepListProps { + responseSteps?: ResponseStep[]; + setRefetch: SetRefetch; + title: string; + animationDelay?: number; + loading: boolean; + closeable?: boolean; + denseList?: boolean; +} + +export const ResponseStepList = ({ + responseSteps, + animationDelay, + setRefetch, + loading = false, + closeable = false, + denseList = false, + title, + children, +}: PropsWithChildren) => { + const [open, setOpen] = React.useState(true); + + const handleClose = () => setOpen(false); + + return ( + { + if (closeable) { + handleClose(); + } + }} + maxWidth="md" + fullWidth + > + {title} + + {loading || !responseSteps ? ( +
+ +
+ ) : ( + <> + + + {responseSteps.map((responseStep, index) => ( + + ))} + + + {children} + + + {closeable && ( + + )} + + + + )} +
+ ); +}; diff --git a/plugins/release-manager-as-a-service/src/components/ResponseStepList/ResponseStepListItem.test.tsx b/plugins/release-manager-as-a-service/src/components/ResponseStepList/ResponseStepListItem.test.tsx new file mode 100644 index 0000000000..0b4ce11887 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/components/ResponseStepList/ResponseStepListItem.test.tsx @@ -0,0 +1,96 @@ +/* + * 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 { render } from '@testing-library/react'; + +import { TEST_IDS } from '../../test-helpers/test-ids'; +import { ResponseStepListItem } from './ResponseStepListItem'; + +describe('ResponseStepListItem', () => { + it('should render', () => { + const { getByTestId } = render( + , + ); + + expect( + getByTestId(TEST_IDS.components.responseStepListItem), + ).toBeInTheDocument(); + }); + + it('should render success icon', () => { + const { getByTestId } = render( + , + ); + + expect( + getByTestId(TEST_IDS.components.responseStepListItemIconSuccess), + ).toBeInTheDocument(); + }); + + it('should render failure icon', () => { + const { getByTestId } = render( + , + ); + + expect( + getByTestId(TEST_IDS.components.responseStepListItemIconFailure), + ).toBeInTheDocument(); + }); + + it('should render link icon', () => { + const { getByTestId } = render( + , + ); + + expect( + getByTestId(TEST_IDS.components.responseStepListItemIconLink), + ).toBeInTheDocument(); + }); + + it('should render default icon', () => { + const { getByTestId } = render( + , + ); + + expect( + getByTestId(TEST_IDS.components.responseStepListItemIconDefault), + ).toBeInTheDocument(); + }); +}); diff --git a/plugins/release-manager-as-a-service/src/components/ResponseStepList/ResponseStepListItem.tsx b/plugins/release-manager-as-a-service/src/components/ResponseStepList/ResponseStepListItem.tsx new file mode 100644 index 0000000000..fd139dae89 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/components/ResponseStepList/ResponseStepListItem.tsx @@ -0,0 +1,135 @@ +/* + * 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, { useEffect, useState } from 'react'; +import { green, red } from '@material-ui/core/colors'; +import { + IconButton, + ListItem, + ListItemIcon, + ListItemText, + makeStyles, +} from '@material-ui/core'; +import CheckCircleOutline from '@material-ui/icons/CheckCircleOutline'; +import ErrorOutlineIcon from '@material-ui/icons/ErrorOutline'; +import FiberManualRecordIcon from '@material-ui/icons/FiberManualRecord'; +import OpenInNewIcon from '@material-ui/icons/OpenInNew'; + +import { TEST_IDS } from '../../test-helpers/test-ids'; +import { ResponseStep } from '../../types/types'; + +interface ResponseStepListItemProps { + responseStep: ResponseStep; + index: number; + animationDelay?: number; +} + +const useStyles = makeStyles({ + item: { + transition: `opacity ${(props: any) => + props.animationDelay <= 0 + ? 0 + : Math.ceil(props.animationDelay / 2)}ms ease-in`, + overflow: 'hidden', + '&:before': { + flex: 'none', + }, + }, + hidden: { + opacity: 0, + height: 0, + minHeight: 0, + }, + shown: { + opacity: 1, + }, +}); + +export const ResponseStepListItem = ({ + responseStep, + index, + animationDelay = 300, +}: ResponseStepListItemProps) => { + const classes = useStyles({ animationDelay }); + const [renderMe, setRenderMe] = useState(false); + + useEffect(() => { + const timeoutId = setTimeout(() => { + setRenderMe(true); + }, animationDelay * index); + + return () => clearTimeout(timeoutId); + }, [animationDelay, index, setRenderMe]); + + function ItemIcon() { + if (responseStep.icon === 'success') { + return ( + + ); + } + + if (responseStep.icon === 'failure') { + return ( + + ); + } + + if (responseStep.link) { + return ( + { + const newTab = window.open(responseStep.link, '_blank'); + newTab?.focus(); + }} + > + + + ); + } + + return ( + + ); + } + + return ( + + + + + + + + ); +}; diff --git a/plugins/release-manager-as-a-service/src/constants/constants.test.ts b/plugins/release-manager-as-a-service/src/constants/constants.test.ts new file mode 100644 index 0000000000..616cf6f21e --- /dev/null +++ b/plugins/release-manager-as-a-service/src/constants/constants.test.ts @@ -0,0 +1,30 @@ +/* + * 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 constants from './constants'; + +describe('constants', () => { + it('should match snapshot', () => { + expect(constants).toMatchInlineSnapshot(` + Object { + "SEMVER_PARTS": Object { + "major": "major", + "minor": "minor", + "patch": "patch", + }, + } + `); + }); +}); diff --git a/plugins/release-manager-as-a-service/src/constants/constants.ts b/plugins/release-manager-as-a-service/src/constants/constants.ts new file mode 100644 index 0000000000..c36c3b63b3 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/constants/constants.ts @@ -0,0 +1,24 @@ +/* + * 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. + */ +export const SEMVER_PARTS: { + major: 'major'; + minor: 'minor'; + patch: 'patch'; +} = { + major: 'major', + minor: 'minor', + patch: 'patch', +} as const; diff --git a/plugins/release-manager-as-a-service/src/errors/ReleaseManagerAsAServiceError.ts b/plugins/release-manager-as-a-service/src/errors/ReleaseManagerAsAServiceError.ts new file mode 100644 index 0000000000..cd6231d229 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/errors/ReleaseManagerAsAServiceError.ts @@ -0,0 +1,22 @@ +/* + * 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. + */ +export class ReleaseManagerAsAServiceError extends Error { + constructor(message: string) { + super(message); + + this.name = 'ReleaseManagerAsAServiceError'; + } +} diff --git a/plugins/release-manager-as-a-service/src/helpers/date.test.ts b/plugins/release-manager-as-a-service/src/helpers/date.test.ts new file mode 100644 index 0000000000..06bdd31573 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/helpers/date.test.ts @@ -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 { getNewDate } from './date'; + +describe('getNewDate', () => { + it('should get a date', () => { + const newDate = getNewDate(); + + expect(newDate.toISOString()).toMatch( + /[\d]{4}-[\d]{2}-[\d]{2}T[\d]{2}:[\d]{2}:[\d]{2}.[\d]{3}Z/, + ); + }); +}); diff --git a/plugins/release-manager-as-a-service/src/helpers/date.ts b/plugins/release-manager-as-a-service/src/helpers/date.ts new file mode 100644 index 0000000000..6c9d8423b6 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/helpers/date.ts @@ -0,0 +1,16 @@ +/* + * 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. + */ +export const getNewDate = () => new Date(); diff --git a/plugins/release-manager-as-a-service/src/helpers/getBumpedTag.test.ts b/plugins/release-manager-as-a-service/src/helpers/getBumpedTag.test.ts new file mode 100644 index 0000000000..dc81f93e67 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/helpers/getBumpedTag.test.ts @@ -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 { + mockCalverProject, + mockSemverProject, +} from '../test-helpers/test-helpers'; +import { getBumpedTag } from './getBumpedTag'; + +describe('getBumpedTag', () => { + describe('calver', () => { + it('should increment patch by 1', () => { + const result = getBumpedTag({ + project: mockCalverProject, + tag: 'rc-2020.01.01_1', + bumpLevel: 'patch', + }); + + expect(result).toMatchInlineSnapshot(` + Object { + "bumpedTag": "rc-2020.01.01_2", + "tagParts": Object { + "calver": "2020.01.01", + "patch": 2, + "prefix": "rc", + }, + } + `); + }); + + it('should increment patch by 1 regardless of semver-specific arg "bumpLevel"', () => { + const result = getBumpedTag({ + project: mockCalverProject, + tag: 'rc-2020.01.01_1', + bumpLevel: 'major', + }); + + expect(result).toMatchInlineSnapshot(` + Object { + "bumpedTag": "rc-2020.01.01_2", + "tagParts": Object { + "calver": "2020.01.01", + "patch": 2, + "prefix": "rc", + }, + } + `); + }); + }); + + describe('semver', () => { + it('should increment patch by 1', () => { + const result = getBumpedTag({ + project: mockSemverProject, + tag: 'rc-1.2.3', + bumpLevel: 'patch', + }); + + expect(result).toMatchInlineSnapshot(` + Object { + "bumpedTag": "rc-1.2.4", + "tagParts": Object { + "major": 1, + "minor": 2, + "patch": 4, + "prefix": "rc", + }, + } + `); + }); + + it('should increment minor by 1', () => { + const result = getBumpedTag({ + project: mockSemverProject, + tag: 'rc-1.2.3', + bumpLevel: 'minor', + }); + + expect(result).toMatchInlineSnapshot(` + Object { + "bumpedTag": "rc-1.3.0", + "tagParts": Object { + "major": 1, + "minor": 3, + "patch": 0, + "prefix": "rc", + }, + } + `); + }); + + it('should increment major by 1', () => { + const result = getBumpedTag({ + project: mockSemverProject, + tag: 'rc-1.2.3', + bumpLevel: 'major', + }); + + expect(result).toMatchInlineSnapshot(` + Object { + "bumpedTag": "rc-2.0.0", + "tagParts": Object { + "major": 2, + "minor": 0, + "patch": 0, + "prefix": "rc", + }, + } + `); + }); + }); +}); diff --git a/plugins/release-manager-as-a-service/src/helpers/getBumpedTag.ts b/plugins/release-manager-as-a-service/src/helpers/getBumpedTag.ts new file mode 100644 index 0000000000..a6d4e43172 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/helpers/getBumpedTag.ts @@ -0,0 +1,94 @@ +/* + * 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 { CalverTagParts } from './tagParts/getCalverTagParts'; +import { getTagParts } from './tagParts/getTagParts'; +import { isCalverTagParts } from './isCalverTagParts'; +import { Project } from '../types/types'; +import { SEMVER_PARTS } from '../constants/constants'; +import { SemverTagParts } from './tagParts/getSemverTagParts'; + +export function getBumpedTag({ + project, + tag, + bumpLevel, +}: { + project: Project; + tag: string; + bumpLevel: keyof typeof SEMVER_PARTS; +}) { + const tagParts = getTagParts({ project, tag }); + + if (isCalverTagParts(project, tagParts)) { + return getPatchedCalverTag(tagParts); + } + + return getBumpedSemverTag(tagParts, bumpLevel); +} + +function getPatchedCalverTag(tagParts: CalverTagParts) { + const bumpedTagParts: CalverTagParts = { + ...tagParts, + patch: tagParts.patch + 1, + }; + const bumpedTag = `${bumpedTagParts.prefix}-${bumpedTagParts.calver}_${bumpedTagParts.patch}`; + + return { + bumpedTag, + tagParts: bumpedTagParts, + }; +} + +function getBumpedSemverTag( + tagParts: SemverTagParts, + semverBumpLevel: keyof typeof SEMVER_PARTS, +) { + const { bumpedTagParts } = getBumpedSemverTagParts(tagParts, semverBumpLevel); + + const bumpedTag = `${bumpedTagParts.prefix}-${bumpedTagParts.major}.${bumpedTagParts.minor}.${bumpedTagParts.patch}`; + + return { + bumpedTag, + tagParts: bumpedTagParts, + }; +} + +export function getBumpedSemverTagParts( + tagParts: SemverTagParts, + semverBumpLevel: keyof typeof SEMVER_PARTS, +) { + const bumpedTagParts = { + ...tagParts, + }; + + if (semverBumpLevel === 'major') { + bumpedTagParts.major = bumpedTagParts.major + 1; + bumpedTagParts.minor = 0; + bumpedTagParts.patch = 0; + } + + if (semverBumpLevel === 'minor') { + bumpedTagParts.minor = bumpedTagParts.minor + 1; + bumpedTagParts.patch = 0; + } + + if (semverBumpLevel === 'patch') { + bumpedTagParts.patch = bumpedTagParts.patch + 1; + } + + return { + bumpedTagParts, + }; +} diff --git a/plugins/release-manager-as-a-service/src/helpers/getShortCommitHash.test.ts b/plugins/release-manager-as-a-service/src/helpers/getShortCommitHash.test.ts new file mode 100644 index 0000000000..4f9b57f6f1 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/helpers/getShortCommitHash.test.ts @@ -0,0 +1,33 @@ +/* + * 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 { getShortCommitHash } from './getShortCommitHash'; + +describe('getShortCommitHash', () => { + it('should get the short version of the commit hash', () => { + const result = getShortCommitHash( + 'bd3fc6f6351018a748bb7f94c0ecf6c1577d8e06', + ); + + expect(result).toEqual('bd3fc6f'); + expect(result.length).toEqual(7); + }); + + it('should throw for invalid commit hashes (too short)', () => { + expect(() => getShortCommitHash('bd3')).toThrowErrorMatchingInlineSnapshot( + `"Invalid shortCommitHash: less than 7 characters"`, + ); + }); +}); diff --git a/plugins/release-manager-as-a-service/src/helpers/getShortCommitHash.ts b/plugins/release-manager-as-a-service/src/helpers/getShortCommitHash.ts new file mode 100644 index 0000000000..7abc541606 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/helpers/getShortCommitHash.ts @@ -0,0 +1,28 @@ +/* + * 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 { ReleaseManagerAsAServiceError } from '../errors/ReleaseManagerAsAServiceError'; + +export function getShortCommitHash(hash: string) { + const shortCommitHash = hash.substr(0, 7); + + if (shortCommitHash.length < 7) { + throw new ReleaseManagerAsAServiceError( + 'Invalid shortCommitHash: less than 7 characters', + ); + } + + return shortCommitHash; +} diff --git a/plugins/release-manager-as-a-service/src/helpers/isCalverTagParts.test.ts b/plugins/release-manager-as-a-service/src/helpers/isCalverTagParts.test.ts new file mode 100644 index 0000000000..33be3cc2e0 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/helpers/isCalverTagParts.test.ts @@ -0,0 +1,32 @@ +/* + * 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, + mockSemverProject, +} from '../test-helpers/test-helpers'; +import { isCalverTagParts } from './isCalverTagParts'; + +describe('isCalverTagParts', () => { + describe('calver', () => { + it('should return true', () => + expect(isCalverTagParts(mockCalverProject, {})).toEqual(true)); + }); + + describe('semver', () => { + it('should return false', () => + expect(isCalverTagParts(mockSemverProject, {})).toEqual(false)); + }); +}); diff --git a/plugins/release-manager-as-a-service/src/helpers/isCalverTagParts.ts b/plugins/release-manager-as-a-service/src/helpers/isCalverTagParts.ts new file mode 100644 index 0000000000..825486c248 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/helpers/isCalverTagParts.ts @@ -0,0 +1,24 @@ +/* + * 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 { CalverTagParts } from './tagParts/getCalverTagParts'; +import { Project } from '../types/types'; + +export function isCalverTagParts( + project: Project, + _tagParts: unknown, +): _tagParts is CalverTagParts { + return project.versioningStrategy === 'calver'; +} diff --git a/plugins/release-manager-as-a-service/src/helpers/tagParts/getCalverTagParts.ts b/plugins/release-manager-as-a-service/src/helpers/tagParts/getCalverTagParts.ts new file mode 100644 index 0000000000..c6169ac7f5 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/helpers/tagParts/getCalverTagParts.ts @@ -0,0 +1,40 @@ +/* + * 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 { ReleaseManagerAsAServiceError } from '../../errors/ReleaseManagerAsAServiceError'; + +export type CalverTagParts = { + prefix: string; + calver: string; + patch: number; +}; + +export function getCalverTagParts(tag: string) { + const result = tag.match( + /(rc|version)-([0-9]{4}\.[0-9]{2}\.[0-9]{2})_([0-9]+)/, + ); + + if (result === null || result.length < 4) { + throw new ReleaseManagerAsAServiceError('Invalid calver tag'); + } + + const tagParts: CalverTagParts = { + prefix: result[1], + calver: result[2], + patch: parseInt(result[3], 10), + }; + + return tagParts; +} diff --git a/plugins/release-manager-as-a-service/src/helpers/tagParts/getSemverTagParts.ts b/plugins/release-manager-as-a-service/src/helpers/tagParts/getSemverTagParts.ts new file mode 100644 index 0000000000..8b2ae2873b --- /dev/null +++ b/plugins/release-manager-as-a-service/src/helpers/tagParts/getSemverTagParts.ts @@ -0,0 +1,40 @@ +/* + * 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 { ReleaseManagerAsAServiceError } from '../../errors/ReleaseManagerAsAServiceError'; + +export type SemverTagParts = { + prefix: string; + major: number; + minor: number; + patch: number; +}; + +export function getSemverTagParts(tag: string) { + const result = tag.match(/(rc|version)-([0-9]+)\.([0-9]+)\.([0-9]+)/); + + if (result === null || result.length < 4) { + throw new ReleaseManagerAsAServiceError('Invalid semver tag'); + } + + const tagParts: SemverTagParts = { + prefix: result[1], + major: parseInt(result[2], 10), + minor: parseInt(result[3], 10), + patch: parseInt(result[4], 10), + }; + + return tagParts; +} diff --git a/plugins/release-manager-as-a-service/src/helpers/tagParts/getTagParts.test.ts b/plugins/release-manager-as-a-service/src/helpers/tagParts/getTagParts.test.ts new file mode 100644 index 0000000000..0536cd3d7d --- /dev/null +++ b/plugins/release-manager-as-a-service/src/helpers/tagParts/getTagParts.test.ts @@ -0,0 +1,112 @@ +/* + * 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, + mockSemverProject, +} from '../../test-helpers/test-helpers'; +import { getTagParts } from './getTagParts'; + +describe('getTagParts', () => { + describe('calver', () => { + it('should return tagParts for RC tag', () => + expect( + getTagParts({ project: mockCalverProject, tag: 'rc-2020.01.01_1' }), + ).toMatchInlineSnapshot(` + Object { + "calver": "2020.01.01", + "patch": 1, + "prefix": "rc", + } + `)); + + it('should return tagParts for Version tag', () => + expect( + getTagParts({ + project: mockCalverProject, + tag: 'version-2020.01.01_1', + }), + ).toMatchInlineSnapshot(` + Object { + "calver": "2020.01.01", + "patch": 1, + "prefix": "version", + } + `)); + + it('should return null for invalid prefix', () => { + expect(() => + getTagParts({ + project: mockCalverProject, + tag: 'invalid-2020.01.01_1', + }), + ).toThrowErrorMatchingInlineSnapshot(`"Invalid calver tag"`); + }); + + it('should return null for invalid calver (missing padded zero)', () => { + expect(() => + getTagParts({ project: mockCalverProject, tag: 'rc-2020.1.01_1' }), + ).toThrowErrorMatchingInlineSnapshot(`"Invalid calver tag"`); + }); + + it('should return null for invalid calver (missing day)', () => { + expect(() => + getTagParts({ project: mockCalverProject, tag: 'rc-2020.01_1' }), + ).toThrowErrorMatchingInlineSnapshot(`"Invalid calver tag"`); + }); + + it('should return null for invalid patch (letter instead of number)', () => { + expect(() => + getTagParts({ project: mockCalverProject, tag: 'rc-2020.01.01_a' }), + ).toThrowErrorMatchingInlineSnapshot(`"Invalid calver tag"`); + }); + }); + + describe('semver', () => { + it('should return tagParts for RC tag', () => + expect(getTagParts({ project: mockSemverProject, tag: 'rc-1.2.3' })) + .toMatchInlineSnapshot(` + Object { + "major": 1, + "minor": 2, + "patch": 3, + "prefix": "rc", + } + `)); + + it('should return tagParts for Version tag', () => + expect(getTagParts({ project: mockSemverProject, tag: 'version-1.2.3' })) + .toMatchInlineSnapshot(` + Object { + "major": 1, + "minor": 2, + "patch": 3, + "prefix": "version", + } + `)); + + it('should throw for invalid prefix', () => { + expect(() => + getTagParts({ project: mockSemverProject, tag: 'invalid-1.2.3' }), + ).toThrowErrorMatchingInlineSnapshot(`"Invalid semver tag"`); + }); + + it('should throw for invalid semver (missing patch)', () => { + expect(() => + getTagParts({ project: mockSemverProject, tag: 'rc-1.2' }), + ).toThrowErrorMatchingInlineSnapshot(`"Invalid semver tag"`); + }); + }); +}); diff --git a/plugins/release-manager-as-a-service/src/helpers/tagParts/getTagParts.ts b/plugins/release-manager-as-a-service/src/helpers/tagParts/getTagParts.ts new file mode 100644 index 0000000000..3e7f85598b --- /dev/null +++ b/plugins/release-manager-as-a-service/src/helpers/tagParts/getTagParts.ts @@ -0,0 +1,32 @@ +/* + * 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 '../../types/types'; + +export function getTagParts({ + project, + tag, +}: { + project: Project; + tag: string; +}) { + if (project.versioningStrategy === 'calver') { + return getCalverTagParts(tag); + } + + return getSemverTagParts(tag); +} diff --git a/plugins/release-manager-as-a-service/src/index.ts b/plugins/release-manager-as-a-service/src/index.ts new file mode 100644 index 0000000000..3461c92d03 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/index.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ +export { + releaseManagerAsAServicePlugin, + ReleaseManagerAsAServicePage, +} from './plugin'; diff --git a/plugins/release-manager-as-a-service/src/plugin.test.ts b/plugins/release-manager-as-a-service/src/plugin.test.ts new file mode 100644 index 0000000000..2ecbad7bc5 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/plugin.test.ts @@ -0,0 +1,22 @@ +/* + * 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 { releaseManagerAsAServicePlugin } from './plugin'; + +describe('release-manager-as-a-service', () => { + it('should export plugin', () => { + expect(releaseManagerAsAServicePlugin).toBeDefined(); + }); +}); diff --git a/plugins/release-manager-as-a-service/src/plugin.ts b/plugins/release-manager-as-a-service/src/plugin.ts new file mode 100644 index 0000000000..a1ccabd20f --- /dev/null +++ b/plugins/release-manager-as-a-service/src/plugin.ts @@ -0,0 +1,54 @@ +/* + * 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 { + configApiRef, + createPlugin, + createApiFactory, + githubAuthApiRef, + createRoutableExtension, +} from '@backstage/core'; + +import { releaseManagerAsAServiceApiRef } from './api/serviceApiRef'; +import { PluginApiClientConfig } from './api/PluginApiClientConfig'; +import { rootRouteRef } from './routes'; + +export const releaseManagerAsAServicePlugin = createPlugin({ + id: 'release-manager-as-a-service', + routes: { + root: rootRouteRef, + }, + apis: [ + createApiFactory({ + api: releaseManagerAsAServiceApiRef, + deps: { + configApi: configApiRef, + githubAuthApi: githubAuthApiRef, + }, + factory: ({ configApi, githubAuthApi }) => + new PluginApiClientConfig({ configApi, githubAuthApi }), + }), + ], +}); + +export const ReleaseManagerAsAServicePage = releaseManagerAsAServicePlugin.provide( + createRoutableExtension({ + component: () => + import('./ReleaseManagerAsAService').then( + m => m.ReleaseManagerAsAService, + ), + mountPoint: rootRouteRef, + }), +); diff --git a/plugins/release-manager-as-a-service/src/routes.ts b/plugins/release-manager-as-a-service/src/routes.ts new file mode 100644 index 0000000000..6527388235 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/routes.ts @@ -0,0 +1,20 @@ +/* + * 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 { createRouteRef } from '@backstage/core'; + +export const rootRouteRef = createRouteRef({ + title: 'release-manager-as-a-service', +}); diff --git a/plugins/release-manager-as-a-service/src/setupTests.ts b/plugins/release-manager-as-a-service/src/setupTests.ts new file mode 100644 index 0000000000..0cec5b395d --- /dev/null +++ b/plugins/release-manager-as-a-service/src/setupTests.ts @@ -0,0 +1,17 @@ +/* + * 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 '@testing-library/jest-dom'; +import 'cross-fetch/polyfill'; diff --git a/plugins/release-manager-as-a-service/src/sideEffects/getGitHubBatchInfo.ts b/plugins/release-manager-as-a-service/src/sideEffects/getGitHubBatchInfo.ts new file mode 100644 index 0000000000..cb044f6f92 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/sideEffects/getGitHubBatchInfo.ts @@ -0,0 +1,48 @@ +/* + * 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 { RMaaSApiClient } from '../api/RMaaSApiClient'; +import { getLatestRelease } from './getLatestRelease'; + +interface GetGitHubBatchInfo { + apiClient: RMaaSApiClient; +} + +export const getGitHubBatchInfo = ({ + apiClient, +}: GetGitHubBatchInfo) => async () => { + const [{ repository }, latestRelease] = await Promise.all([ + apiClient.getRepository(), + getLatestRelease({ apiClient }), + ]); + + if (latestRelease === null) { + return { + latestRelease, + releaseBranch: null, + repository, + }; + } + + const { branch } = await apiClient.getBranch({ + branchName: latestRelease.target_commitish, + }); + + return { + latestRelease, + releaseBranch: branch, + repository, + }; +}; diff --git a/plugins/release-manager-as-a-service/src/sideEffects/getLatestRelease.test.ts b/plugins/release-manager-as-a-service/src/sideEffects/getLatestRelease.test.ts new file mode 100644 index 0000000000..eb275092da --- /dev/null +++ b/plugins/release-manager-as-a-service/src/sideEffects/getLatestRelease.test.ts @@ -0,0 +1,42 @@ +/* + * 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 { mockApiClient } from '../test-helpers/test-helpers'; +import { getLatestRelease } from './getLatestRelease'; + +describe('getLatestRelease', () => { + beforeEach(jest.clearAllMocks); + + it('should return the latest release with id=1', async () => { + const result = await getLatestRelease({ apiClient: mockApiClient }); + + expect(result).toMatchInlineSnapshot(` + Object { + "body": "mock_latest_release", + "html_url": "mock_release_html_url", + "id": 1, + "prerelease": false, + } + `); + }); + + it('should return early with `null` if no releases found', async () => { + mockApiClient.getReleases.mockImplementationOnce(() => ({ releases: [] })); + + const result = await getLatestRelease({ apiClient: mockApiClient }); + + expect(result).toMatchInlineSnapshot(`null`); + }); +}); diff --git a/plugins/release-manager-as-a-service/src/sideEffects/getLatestRelease.ts b/plugins/release-manager-as-a-service/src/sideEffects/getLatestRelease.ts new file mode 100644 index 0000000000..1044d510c5 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/sideEffects/getLatestRelease.ts @@ -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 { RMaaSApiClient } from '../api/RMaaSApiClient'; + +interface GetLatestRelease { + apiClient: RMaaSApiClient; +} + +export async function getLatestRelease({ apiClient }: GetLatestRelease) { + const { releases } = await apiClient.getReleases(); + + if (releases.length === 0) { + return null; + } + + const { latestRelease } = await apiClient.getRelease({ + releaseId: releases[0].id, + }); + + return latestRelease; +} diff --git a/plugins/release-manager-as-a-service/src/styles/styles.ts b/plugins/release-manager-as-a-service/src/styles/styles.ts new file mode 100644 index 0000000000..1548953069 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/styles/styles.ts @@ -0,0 +1,22 @@ +/* + * 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 { makeStyles, Theme } from '@material-ui/core'; + +export const useStyles = makeStyles((_theme: Theme) => ({ + paragraph: { + marginBottom: '1em', + }, +})); diff --git a/plugins/release-manager-as-a-service/src/test-helpers/test-helpers.test.ts b/plugins/release-manager-as-a-service/src/test-helpers/test-helpers.test.ts new file mode 100644 index 0000000000..e72eba4df4 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/test-helpers/test-helpers.test.ts @@ -0,0 +1,165 @@ +/* + * 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 match snapshot', () => { + expect(testHelpers).toMatchInlineSnapshot(` + Object { + "mockApiClient": Object { + "createRc": Object { + "createRef": [MockFunction], + "createRelease": [MockFunction], + "getComparison": [MockFunction], + }, + "getBranch": [MockFunction], + "getLatestCommit": [MockFunction], + "getOctokit": [Function], + "getProject": [MockFunction], + "getRecentCommits": [MockFunction], + "getRelease": [MockFunction], + "getReleases": [MockFunction], + "getRepoPath": [MockFunction], + "getRepository": [MockFunction], + "githubAuthApi": Object { + "getAccessToken": [MockFunction], + }, + "patch": Object { + "createCherryPickCommit": [MockFunction], + "createReference": [MockFunction], + "createTagObject": [MockFunction], + "createTempCommit": [MockFunction], + "forceBranchHeadToTempCommit": [MockFunction], + "merge": [MockFunction], + "replaceTempCommit": [MockFunction], + "updateRelease": [MockFunction], + }, + "pluginApiClient": Object { + "baseUrl": "http://mock_base_url.hehe", + "getOctokit": [MockFunction], + }, + "project": Object { + "github": Object { + "org": "mock_org", + "repo": "mock_repo", + }, + "name": "mock_name", + "versioningStrategy": "semver", + }, + "promoteRc": Object { + "promoteRelease": [MockFunction], + }, + }, + "mockBumpedTag": "rc-2020.01.01_1337", + "mockCalverProject": Object { + "github": Object { + "org": "mock_org", + "repo": "mock_repo", + }, + "name": "mock_name", + "versioningStrategy": "calver", + }, + "mockDefaultBranch": "mock_defaultBranch", + "mockNextGheInfo": Object { + "rcBranch": "rc/1.2.3", + "rcReleaseTag": "rc-1.2.3", + "releaseName": "Version 1.2.3", + }, + "mockRcRelease": Object { + "body": "mock_body", + "html_url": "mock_release_html_url", + "id": 1, + "prerelease": true, + "tag_name": "rc-2020.01.01_1", + "target_commitish": "rc/1.2.3", + }, + "mockRecentCommits": Array [ + Object { + "author": Object { + "html_url": "mock_recentCommits_author_html_url", + "login": "mock_recentCommit_author_login", + }, + "commit": Object { + "message": "mock_latestCommit_message", + }, + "html_url": "mock_latestCommit_html_url", + "node_id": "1", + "sha": "mock_latestCommit_sha", + }, + Object { + "author": Object { + "html_url": "mock_recentCommits_author_html_url", + "login": "mock_recentCommit_author_login", + }, + "commit": Object { + "message": "mock_latestCommit_message", + }, + "html_url": "mock_latestCommit_html_url", + "node_id": "2", + "sha": "mock_latestCommit_sha", + }, + ], + "mockReleaseBranch": Object { + "_links": Object { + "html": "mock_branch__links_html", + }, + "commit": Object { + "commit": Object { + "tree": Object { + "sha": "mock_branch_commit_commit_tree_sha", + }, + }, + "sha": "mock_branch_commit_sha", + }, + "name": "rc/1.2.3", + }, + "mockReleaseVersion": Object { + "body": "mock_body", + "html_url": "mock_release_html_url", + "id": 1, + "prerelease": false, + "tag_name": "version-2020.01.01_1", + "target_commitish": "rc/1.2.3", + }, + "mockSelectedPatchCommit": Object { + "author": Object { + "html_url": "mock_recentCommits_author_html_url", + "login": "mock_recentCommit_author_login", + }, + "commit": Object { + "message": "mock_latestCommit_message", + }, + "html_url": "mock_latestCommit_html_url", + "node_id": "mock_selected_patch_commit", + "sha": "mock_latestCommit_sha", + }, + "mockSemverProject": Object { + "github": Object { + "org": "mock_org", + "repo": "mock_repo", + }, + "name": "mock_name", + "versioningStrategy": "semver", + }, + "mockTagParts": Object { + "calver": "2020.01.01", + "patch": 1, + "prefix": "rc", + }, + } + `); + }); +}); diff --git a/plugins/release-manager-as-a-service/src/test-helpers/test-helpers.ts b/plugins/release-manager-as-a-service/src/test-helpers/test-helpers.ts new file mode 100644 index 0000000000..b994e591d2 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/test-helpers/test-helpers.ts @@ -0,0 +1,255 @@ +/* + * 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 { CalverTagParts } from '../helpers/tagParts/getCalverTagParts'; +import { getRcGheInfo } from '../cards/createRc/getRcGheInfo'; +import { + GhCompareCommitsResponse, + GhCreateCommitResponse, + GhCreateReferenceResponse, + GhCreateReleaseResponse, + GhCreateTagObjectResponse, + GhGetBranchResponse, + GhGetCommitResponse, + GhGetReleaseResponse, + GhMergeResponse, + GhUpdateReferenceResponse, + GhUpdateReleaseResponse, + Project, +} from '../types/types'; + +export const mockSemverProject: Project = { + github: { + org: 'mock_org', + repo: 'mock_repo', + }, + name: 'mock_name', + versioningStrategy: 'semver', +}; + +export const mockCalverProject: Project = { + github: { + org: 'mock_org', + repo: 'mock_repo', + }, + name: 'mock_name', + versioningStrategy: 'calver', +}; + +export const mockDefaultBranch = 'mock_defaultBranch'; + +export const mockNextGheInfo: ReturnType = { + rcBranch: 'rc/1.2.3', + rcReleaseTag: 'rc-1.2.3', + releaseName: 'Version 1.2.3', +}; + +export const mockTagParts = { + prefix: 'rc', + calver: '2020.01.01', + patch: 1, +} as CalverTagParts; + +export const mockBumpedTag = 'rc-2020.01.01_1337'; + +/** + * MOCK RELEASE + */ +const createMockRelease = ({ + id = 1, + prerelease = false, + ...rest +}: Partial = {}) => + ({ + id: 1, + body: 'mock_body', + html_url: 'mock_release_html_url', + prerelease, + ...rest, + } as GhGetReleaseResponse); +export const mockRcRelease = createMockRelease({ + prerelease: true, + tag_name: 'rc-2020.01.01_1', + target_commitish: 'rc/1.2.3', +}); +export const mockReleaseVersion = createMockRelease({ + prerelease: false, + tag_name: 'version-2020.01.01_1', + target_commitish: 'rc/1.2.3', +}); + +/** + * MOCK BRANCH + */ +const createMockBranch = ({ ...rest }: Partial = {}) => + ({ + name: 'rc/1.2.3', + commit: { + sha: 'mock_branch_commit_sha', + commit: { tree: { sha: 'mock_branch_commit_commit_tree_sha' } }, + }, + _links: { html: 'mock_branch__links_html' }, + ...rest, + } as GhGetBranchResponse); +export const mockReleaseBranch = createMockBranch(); + +/** + * MOCK COMMIT + */ +const createMockCommit = ({ node_id = '1' }: Partial) => + ({ + node_id, + author: { + html_url: 'mock_recentCommits_author_html_url', + login: 'mock_recentCommit_author_login', + }, + commit: { + message: 'mock_latestCommit_message', + }, + html_url: 'mock_latestCommit_html_url', + sha: 'mock_latestCommit_sha', + } as GhGetCommitResponse); +export const mockRecentCommits = [ + createMockCommit({ node_id: '1' }), + createMockCommit({ node_id: '2' }), +] as GhGetCommitResponse[]; + +export const mockSelectedPatchCommit = createMockCommit({ + node_id: 'mock_selected_patch_commit', +}); + +/** + * MOCK API CLIENT + */ +export const mockApiClient = { + pluginApiClient: { + getOctokit: jest.fn(), + baseUrl: 'http://mock_base_url.hehe', + }, + + getRepoPath: jest.fn(() => 'erikengervall/playground'), + + getRecentCommits: jest.fn().mockResolvedValue({ + recentCommits: mockRecentCommits, + }), + getReleases: jest.fn().mockResolvedValue({ + releases: [ + createMockRelease({ id: 1, body: 'mock_releases[0]' }), + createMockRelease({ id: 2, body: 'mock_releases[1]' }), + ], + }), + getRelease: jest.fn().mockResolvedValue({ + latestRelease: createMockRelease({ id: 1, body: 'mock_latest_release' }), + }), + + getBranch: jest.fn().mockResolvedValue({ + branch: mockReleaseBranch, + }), + getLatestCommit: jest.fn().mockResolvedValue({ + latestCommit: createMockCommit({ node_id: 'mock_latest_commit' }), + }), + getOctokit: () => ({ + octokit: { + request: jest.fn(), + }, + }), + getProject: jest.fn(), + + getRepository: jest.fn(), + githubAuthApi: { + getAccessToken: jest.fn(), + }, + createRc: { + createRef: jest.fn().mockResolvedValue({ + createdRef: { + ref: 'mock_createRef_ref', + } as GhCreateReferenceResponse, + }), + createRelease: jest.fn().mockResolvedValue({ + createReleaseResponse: { + name: 'mock_createRelease_name', + html_url: 'mock_createRelease_html_url', + tag_name: 'mock_createRelease_tag_name', + } as GhCreateReleaseResponse, + }), + getComparison: jest.fn().mockResolvedValue({ + comparison: { + html_url: 'mock_compareCommits_html_url', + ahead_by: 1, + } as GhCompareCommitsResponse, + }), + }, + patch: { + createCherryPickCommit: jest.fn().mockResolvedValue({ + cherryPickCommit: { + commit: { + message: 'mock_merge_commit_message', + tree: { sha: 'mock_merge_commit_tree_sha' }, + }, + html_url: 'mock_merge_html_url', + } as GhMergeResponse, + }), + createReference: jest.fn().mockResolvedValue({ + reference: { + ref: 'mock_reference_ref', + } as GhCreateReferenceResponse, + }), + createTagObject: jest.fn().mockResolvedValue({ + tagObjectResponse: { + tag: 'mock_tag_object_tag', + sha: 'mock_tag_object_sha', + } as GhCreateTagObjectResponse, + }), + createTempCommit: jest.fn().mockResolvedValue({ + tempCommit: { + message: 'mock_commit_message', + sha: 'mock_commit_sha', + } as GhCreateCommitResponse, + }), + forceBranchHeadToTempCommit: jest.fn().mockResolvedValue(undefined), + merge: jest.fn().mockResolvedValue({ + merge: { + commit: { + message: 'mock_merge_commit_message', + tree: { sha: 'mock_merge_commit_tree_sha' }, + }, + html_url: 'mock_merge_html_url', + } as GhMergeResponse, + }), + replaceTempCommit: jest.fn().mockResolvedValue({ + updatedReference: { + ref: 'mock_reference_ref', + object: { sha: 'mock_reference_object_sha' }, + } as GhUpdateReferenceResponse, + }), + updateRelease: jest.fn().mockResolvedValue({ + release: { + name: 'mock_update_release_name', + tag_name: 'mock_update_release_tag_name', + html_url: 'mock_update_release_html_url', + } as GhUpdateReleaseResponse, + }), + }, + promoteRc: { + promoteRelease: jest.fn().mockResolvedValue({ + release: { + name: 'mock_release_name', + tag_name: 'mock_release_tag_name', + html_url: 'mock_release_html_url', + } as GhGetReleaseResponse, + }), + }, + project: mockSemverProject, +} as any; diff --git a/plugins/release-manager-as-a-service/src/test-helpers/test-ids.ts b/plugins/release-manager-as-a-service/src/test-helpers/test-ids.ts new file mode 100644 index 0000000000..7eae6a3978 --- /dev/null +++ b/plugins/release-manager-as-a-service/src/test-helpers/test-ids.ts @@ -0,0 +1,53 @@ +/* + * 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. + */ +export const TEST_IDS = { + info: { + info: 'rmaas--info', + infoCardPlus: 'rmaas--info-card-plus', + }, + createRc: { + cta: 'rmaas--create-rc--cta', + semverSelect: 'rmaas--create-rc--semver-select', + }, + promoteRc: { + mockedPromoteRcBody: 'rmaas-mocked-promote-rc-body', + notRcWarning: 'rmaas--promote-rc--not-rc-warning', + promoteRc: 'rmaas--promote-rc', + cta: 'rmaas--promote-rc-body--cta', + }, + patch: { + error: 'rmaas--patch-body--error', + loading: 'rmaas--patch-body--loading', + notPrerelease: 'rmaas--patch-body--not-prerelease--info', + body: 'rmaas--patch-body', + }, + components: { + divider: 'rmaas--divider', + reloadButton: 'rmaas--reload-button', + noLatestRelease: 'rmaas--no-latest-release', + circularProgress: 'rmaas--circular-progress', + responseStepListDialogContent: 'rmaas--response-step-list--dialog-content', + responseStepListItem: 'rmaas--response-step-list-item', + responseStepListItemIconSuccess: + 'rmaas--response-step-list-item--item-icon--success', + responseStepListItemIconFailure: + 'rmaas--response-step-list-item--item-icon--failure', + responseStepListItemIconLink: + 'rmaas--response-step-list-item--item-icon--link', + responseStepListItemIconDefault: + 'rmaas--response-step-list-item--item-icon--default', + }, +}; diff --git a/plugins/release-manager-as-a-service/src/types/types.ts b/plugins/release-manager-as-a-service/src/types/types.ts new file mode 100644 index 0000000000..5af069b39b --- /dev/null +++ b/plugins/release-manager-as-a-service/src/types/types.ts @@ -0,0 +1,1939 @@ +/* + * 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. + */ +export interface Project { + /** A unique (in the context of RMaaS) project name */ + name: string; + + /** GitHub details */ + github: { + /** + * Repository's organization + * + * @example erikengervall + */ + org: string; + /** + * Repository's name + * + * @example dockest + */ + repo: string; + }; + + /** Slack details */ + slack?: { + /** Relevant slack channel for the project */ + channel: string; + /** Link to slack channel */ + link?: string; + }; + + /** + * Declares the versioning strategy of the project + * + * semver: `1.2.3` (major.minor.patch) + * calver: `2020.01.01_0` (YYYY.0M.0D_patch) + * + * Default: false + */ + versioningStrategy: 'calver' | 'semver'; +} + +interface ComponentConfig { + successCb?: (args: Args) => Promise | void; + omit?: boolean; +} + +export interface ComponentConfigCreateRcSuccessCbArgs { + gitHubReleaseUrl: string; + gitHubReleaseName: string; + comparisonUrl: string; + previousTag?: string; + createdTag: string; +} +export type ComponentConfigCreateRc = ComponentConfig; + +export interface ComponentConfigPromoteRcSuccessCbArgs { + gitHubReleaseUrl: string; + gitHubReleaseName: string; + previousTagUrl: string; + previousTag: string; + updatedTagUrl: string; + updatedTag: string; +} +export type ComponentConfigPromoteRc = ComponentConfig; + +export interface ComponentConfigPatchSuccessCbArgs { + updatedReleaseUrl: string; + updatedReleaseName: string; + previousTag: string; + patchedTag: string; + patchCommitUrl: string; + patchCommitMessage: string; +} +export type ComponentConfigPatch = ComponentConfig; + +export type SetRefetch = React.Dispatch>; + +export interface ResponseStep { + message: string | React.ReactNode; + secondaryMessage?: string | React.ReactNode; + link?: string; + icon?: 'success' | 'failure'; +} + +/** ******************************** + ******** GitHub API Types ********* + ***********************************/ +export interface GhCompareCommitsResponse { + /** @example 'https://api.github.com/repos/octocat/Hello-World/compare/master...topic'; */ + url: string; + /** @example 'https://github.com/octocat/Hello-World/compare/master...topic'; */ + html_url: string; + /** @example 'https://github.com/octocat/Hello-World/compare/octocat:bbcd538c8e72b8c175046e27cc8f907076331401...octocat:0328041d1152db8ae77652d1618a02e57f745f17'; */ + permalink_url: string; + /** @example 'https://github.com/octocat/Hello-World/compare/master...topic.diff'; */ + diff_url: string; + /** @example 'https://github.com/octocat/Hello-World/compare/master...topic.patch'; */ + patch_url: string; + base_commit: { + /** @example 'https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e'; */ + url: string; + /** @example '6dcb09b5b57875f334f61aebed695e2e4193db5e'; */ + sha: string; + /** @example 'MDY6Q29tbWl0NmRjYjA5YjViNTc4NzVmMzM0ZjYxYWViZWQ2OTVlMmU0MTkzZGI1ZQ=='; */ + node_id: string; + /** @example 'https://github.com/octocat/Hello-World/commit/6dcb09b5b57875f334f61aebed695e2e4193db5e'; */ + html_url: string; + /** @example 'https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e/comments'; */ + comments_url: string; + commit: { + /** @example 'https://api.github.com/repos/octocat/Hello-World/git/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e'; */ + url: string; + author: { + /** @example 'Monalisa Octocat'; */ + name: string; + /** @example 'support@github.com'; */ + email: string; + /** @example '2011-04-14T16:00:49Z'; */ + date: string; + }; + committer: { + /** @example 'Monalisa Octocat'; */ + name: string; + /** @example 'support@github.com'; */ + email: string; + /** @example '2011-04-14T16:00:49Z'; */ + date: string; + }; + /** @example 'Fix all the bugs'; */ + message: string; + tree: { + /** @example 'https://api.github.com/repos/octocat/Hello-World/tree/6dcb09b5b57875f334f61aebed695e2e4193db5e'; */ + url: string; + /** @example '6dcb09b5b57875f334f61aebed695e2e4193db5e'; */ + sha: string; + }; + comment_count: number; + verification: { + verified: boolean; + /** @example 'unsigned'; */ + reason: string; + signature: null | any; + payload: null | any; + }; + }; + author: { + /** @example 'octocat'; */ + login: string; + id: number; + /** @example 'MDQ6VXNlcjE='; */ + node_id: string; + /** @example 'https://github.com/images/error/octocat_happy.gif'; */ + avatar_url: string; + /** @example ''; */ + gravatar_id: string; + /** @example 'https://api.github.com/users/octocat'; */ + url: string; + /** @example 'https://github.com/octocat'; */ + html_url: string; + /** @example 'https://api.github.com/users/octocat/followers'; */ + followers_url: string; + /** @example 'https://api.github.com/users/octocat/following{/other_user}'; */ + following_url: string; + /** @example 'https://api.github.com/users/octocat/gists{/gist_id}'; */ + gists_url: string; + /** @example 'https://api.github.com/users/octocat/starred{/owner}{/repo}'; */ + starred_url: string; + /** @example 'https://api.github.com/users/octocat/subscriptions'; */ + subscriptions_url: string; + /** @example 'https://api.github.com/users/octocat/orgs'; */ + organizations_url: string; + /** @example 'https://api.github.com/users/octocat/repos'; */ + repos_url: string; + /** @example 'https://api.github.com/users/octocat/events{/privacy}'; */ + events_url: string; + /** @example 'https://api.github.com/users/octocat/received_events'; */ + received_events_url: string; + /** @example 'User'; */ + type: string; + site_admin: boolean; + }; + committer: { + /** @example 'octocat'; */ + login: string; + id: number; + /** @example 'MDQ6VXNlcjE='; */ + node_id: string; + /** @example 'https://github.com/images/error/octocat_happy.gif'; */ + avatar_url: string; + /** @example ''; */ + gravatar_id: string; + /** @example 'https://api.github.com/users/octocat'; */ + url: string; + /** @example 'https://github.com/octocat'; */ + html_url: string; + /** @example 'https://api.github.com/users/octocat/followers'; */ + followers_url: string; + /** @example 'https://api.github.com/users/octocat/following{/other_user}'; */ + following_url: string; + /** @example 'https://api.github.com/users/octocat/gists{/gist_id}'; */ + gists_url: string; + /** @example 'https://api.github.com/users/octocat/starred{/owner}{/repo}'; */ + starred_url: string; + /** @example 'https://api.github.com/users/octocat/subscriptions'; */ + subscriptions_url: string; + /** @example 'https://api.github.com/users/octocat/orgs'; */ + organizations_url: string; + /** @example 'https://api.github.com/users/octocat/repos'; */ + repos_url: string; + /** @example 'https://api.github.com/users/octocat/events{/privacy}'; */ + events_url: string; + /** @example 'https://api.github.com/users/octocat/received_events'; */ + received_events_url: string; + /** @example 'User'; */ + type: string; + site_admin: boolean; + }; + parents: [ + { + /** @example 'https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e'; */ + url: string; + /** @example '6dcb09b5b57875f334f61aebed695e2e4193db5e'; */ + sha: string; + }, + ]; + }; + merge_base_commit: { + /** @example 'https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e'; */ + url: string; + /** @example '6dcb09b5b57875f334f61aebed695e2e4193db5e'; */ + sha: string; + /** @example 'MDY6Q29tbWl0NmRjYjA5YjViNTc4NzVmMzM0ZjYxYWViZWQ2OTVlMmU0MTkzZGI1ZQ=='; */ + node_id: string; + /** @example 'https://github.com/octocat/Hello-World/commit/6dcb09b5b57875f334f61aebed695e2e4193db5e'; */ + html_url: string; + /** @example 'https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e/comments'; */ + comments_url: string; + commit: { + /** @example 'https://api.github.com/repos/octocat/Hello-World/git/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e'; */ + url: string; + author: { + /** @example 'Monalisa Octocat'; */ + name: string; + /** @example 'support@github.com'; */ + email: string; + /** @example '2011-04-14T16:00:49Z'; */ + date: string; + }; + committer: { + /** @example 'Monalisa Octocat'; */ + name: string; + /** @example 'support@github.com'; */ + email: string; + /** @example '2011-04-14T16:00:49Z'; */ + date: string; + }; + /** @example 'Fix all the bugs'; */ + message: string; + tree: { + /** @example 'https://api.github.com/repos/octocat/Hello-World/tree/6dcb09b5b57875f334f61aebed695e2e4193db5e'; */ + url: string; + /** @example '6dcb09b5b57875f334f61aebed695e2e4193db5e'; */ + sha: string; + }; + comment_count: number; + verification: { + verified: boolean; + /** @example 'unsigned'; */ + reason: string; + signature: null | any; + payload: null | any; + }; + }; + author: { + /** @example 'octocat'; */ + login: string; + id: number; + /** @example 'MDQ6VXNlcjE='; */ + node_id: string; + /** @example 'https://github.com/images/error/octocat_happy.gif'; */ + avatar_url: string; + /** @example ''; */ + gravatar_id: string; + /** @example 'https://api.github.com/users/octocat'; */ + url: string; + /** @example 'https://github.com/octocat'; */ + html_url: string; + /** @example 'https://api.github.com/users/octocat/followers'; */ + followers_url: string; + /** @example 'https://api.github.com/users/octocat/following{/other_user}'; */ + following_url: string; + /** @example 'https://api.github.com/users/octocat/gists{/gist_id}'; */ + gists_url: string; + /** @example 'https://api.github.com/users/octocat/starred{/owner}{/repo}'; */ + starred_url: string; + /** @example 'https://api.github.com/users/octocat/subscriptions'; */ + subscriptions_url: string; + /** @example 'https://api.github.com/users/octocat/orgs'; */ + organizations_url: string; + /** @example 'https://api.github.com/users/octocat/repos'; */ + repos_url: string; + /** @example 'https://api.github.com/users/octocat/events{/privacy}'; */ + events_url: string; + /** @example 'https://api.github.com/users/octocat/received_events'; */ + received_events_url: string; + /** @example 'User'; */ + type: string; + site_admin: boolean; + }; + committer: { + /** @example 'octocat'; */ + login: string; + id: number; + /** @example 'MDQ6VXNlcjE='; */ + node_id: string; + /** @example 'https://github.com/images/error/octocat_happy.gif'; */ + avatar_url: string; + /** @example ''; */ + gravatar_id: string; + /** @example 'https://api.github.com/users/octocat'; */ + url: string; + /** @example 'https://github.com/octocat'; */ + html_url: string; + /** @example 'https://api.github.com/users/octocat/followers'; */ + followers_url: string; + /** @example 'https://api.github.com/users/octocat/following{/other_user}'; */ + following_url: string; + /** @example 'https://api.github.com/users/octocat/gists{/gist_id}'; */ + gists_url: string; + /** @example 'https://api.github.com/users/octocat/starred{/owner}{/repo}'; */ + starred_url: string; + /** @example 'https://api.github.com/users/octocat/subscriptions'; */ + subscriptions_url: string; + /** @example 'https://api.github.com/users/octocat/orgs'; */ + organizations_url: string; + /** @example 'https://api.github.com/users/octocat/repos'; */ + repos_url: string; + /** @example 'https://api.github.com/users/octocat/events{/privacy}'; */ + events_url: string; + /** @example 'https://api.github.com/users/octocat/received_events'; */ + received_events_url: string; + /** @example 'User'; */ + type: string; + site_admin: boolean; + }; + parents: [ + { + /** @example 'https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e'; */ + url: string; + /** @example '6dcb09b5b57875f334f61aebed695e2e4193db5e'; */ + sha: string; + }, + ]; + }; + /** @example 'behind'; */ + status: string; + ahead_by: number; + behind_by: number; + total_commits: number; + commits: [ + { + /** @example 'https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e'; */ + url: string; + /** @example '6dcb09b5b57875f334f61aebed695e2e4193db5e'; */ + sha: string; + /** @example 'MDY6Q29tbWl0NmRjYjA5YjViNTc4NzVmMzM0ZjYxYWViZWQ2OTVlMmU0MTkzZGI1ZQ=='; */ + node_id: string; + /** @example 'https://github.com/octocat/Hello-World/commit/6dcb09b5b57875f334f61aebed695e2e4193db5e'; */ + html_url: string; + /** @example 'https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e/comments'; */ + comments_url: string; + commit: { + /** @example 'https://api.github.com/repos/octocat/Hello-World/git/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e'; */ + url: string; + author: { + /** @example 'Monalisa Octocat'; */ + name: string; + /** @example 'support@github.com'; */ + email: string; + /** @example '2011-04-14T16:00:49Z'; */ + date: string; + }; + committer: { + /** @example 'Monalisa Octocat'; */ + name: string; + /** @example 'support@github.com'; */ + email: string; + /** @example '2011-04-14T16:00:49Z'; */ + date: string; + }; + /** @example 'Fix all the bugs'; */ + message: string; + tree: { + /** @example 'https://api.github.com/repos/octocat/Hello-World/tree/6dcb09b5b57875f334f61aebed695e2e4193db5e'; */ + url: string; + /** @example '6dcb09b5b57875f334f61aebed695e2e4193db5e'; */ + sha: string; + }; + comment_count: number; + verification: { + verified: boolean; + /** @example 'unsigned'; */ + reason: string; + signature: null | any; + payload: null | any; + }; + }; + author: { + /** @example 'octocat'; */ + login: string; + id: number; + /** @example 'MDQ6VXNlcjE='; */ + node_id: string; + /** @example 'https://github.com/images/error/octocat_happy.gif'; */ + avatar_url: string; + /** @example ''; */ + gravatar_id: string; + /** @example 'https://api.github.com/users/octocat'; */ + url: string; + /** @example 'https://github.com/octocat'; */ + html_url: string; + /** @example 'https://api.github.com/users/octocat/followers'; */ + followers_url: string; + /** @example 'https://api.github.com/users/octocat/following{/other_user}'; */ + following_url: string; + /** @example 'https://api.github.com/users/octocat/gists{/gist_id}'; */ + gists_url: string; + /** @example 'https://api.github.com/users/octocat/starred{/owner}{/repo}'; */ + starred_url: string; + /** @example 'https://api.github.com/users/octocat/subscriptions'; */ + subscriptions_url: string; + /** @example 'https://api.github.com/users/octocat/orgs'; */ + organizations_url: string; + /** @example 'https://api.github.com/users/octocat/repos'; */ + repos_url: string; + /** @example 'https://api.github.com/users/octocat/events{/privacy}'; */ + events_url: string; + /** @example 'https://api.github.com/users/octocat/received_events'; */ + received_events_url: string; + /** @example 'User'; */ + type: string; + site_admin: boolean; + }; + committer: { + /** @example 'octocat'; */ + login: string; + id: number; + /** @example 'MDQ6VXNlcjE='; */ + node_id: string; + /** @example 'https://github.com/images/error/octocat_happy.gif'; */ + avatar_url: string; + /** @example ''; */ + gravatar_id: string; + /** @example 'https://api.github.com/users/octocat'; */ + url: string; + /** @example 'https://github.com/octocat'; */ + html_url: string; + /** @example 'https://api.github.com/users/octocat/followers'; */ + followers_url: string; + /** @example 'https://api.github.com/users/octocat/following{/other_user}'; */ + following_url: string; + /** @example 'https://api.github.com/users/octocat/gists{/gist_id}'; */ + gists_url: string; + /** @example 'https://api.github.com/users/octocat/starred{/owner}{/repo}'; */ + starred_url: string; + /** @example 'https://api.github.com/users/octocat/subscriptions'; */ + subscriptions_url: string; + /** @example 'https://api.github.com/users/octocat/orgs'; */ + organizations_url: string; + /** @example 'https://api.github.com/users/octocat/repos'; */ + repos_url: string; + /** @example 'https://api.github.com/users/octocat/events{/privacy}'; */ + events_url: string; + /** @example 'https://api.github.com/users/octocat/received_events'; */ + received_events_url: string; + /** @example 'User'; */ + type: string; + site_admin: boolean; + }; + parents: [ + { + /** @example 'https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e'; */ + url: string; + /** @example '6dcb09b5b57875f334f61aebed695e2e4193db5e'; */ + sha: string; + }, + ]; + }, + ]; + files: [ + { + /** @example 'bbcd538c8e72b8c175046e27cc8f907076331401'; */ + sha: string; + /** @example 'file1.txt'; */ + filename: string; + /** @example 'added'; */ + status: string; + additions: number; + deletions: number; + changes: number; + /** @example 'https://github.com/octocat/Hello-World/blob/6dcb09b5b57875f334f61aebed695e2e4193db5e/file1.txt'; */ + blob_url: string; + /** @example 'https://github.com/octocat/Hello-World/raw/6dcb09b5b57875f334f61aebed695e2e4193db5e/file1.txt'; */ + raw_url: string; + /** @example 'https://api.github.com/repos/octocat/Hello-World/contents/file1.txt?ref=6dcb09b5b57875f334f61aebed695e2e4193db5e'; */ + contents_url: string; + /** @example '@@ -132,7 +132,7 @@ module Test @@ -1000,7 +1000,7 @@ module Test'; */ + patch: string; + }, + ]; +} + +export interface GhCreateReleaseResponse { + /** @example 'https://api.github.com/repos/octocat/Hello-World/releases/1'; */ + url: string; + /** @example 'https://github.com/octocat/Hello-World/releases/v1.0.0'; */ + html_url: string; + /** @example 'https://api.github.com/repos/octocat/Hello-World/releases/1/assets'; */ + assets_url: string; + /** @example 'https://uploads.github.com/repos/octocat/Hello-World/releases/1/assets{?name,label}'; */ + upload_url: string; + /** @example 'https://api.github.com/repos/octocat/Hello-World/tarball/v1.0.0'; */ + tarball_url: string; + /** @example 'https://api.github.com/repos/octocat/Hello-World/zipball/v1.0.0'; */ + zipball_url: string; + id: number; + /** @example 'MDc6UmVsZWFzZTE='; */ + node_id: string; + /** @example 'v1.0.0'; */ + tag_name: string; + /** @example 'master'; */ + target_commitish: string; + /** @example 'v1.0.0'; */ + name: string; + /** @example 'Description of the release'; */ + body: string; + draft: boolean; + prerelease: boolean; + /** @example '2013-02-27T19:35:32Z'; */ + created_at: string; + /** @example '2013-02-27T19:35:32Z'; */ + published_at: string; + author: { + /** @example 'octocat'; */ + login: string; + id: number; + /** @example 'MDQ6VXNlcjE='; */ + node_id: string; + /** @example 'https://github.com/images/error/octocat_happy.gif'; */ + avatar_url: string; + /** @example ''; */ + gravatar_id: string; + /** @example 'https://api.github.com/users/octocat'; */ + url: string; + /** @example 'https://github.com/octocat'; */ + html_url: string; + /** @example 'https://api.github.com/users/octocat/followers'; */ + followers_url: string; + /** @example 'https://api.github.com/users/octocat/following{/other_user}'; */ + following_url: string; + /** @example 'https://api.github.com/users/octocat/gists{/gist_id}'; */ + gists_url: string; + /** @example 'https://api.github.com/users/octocat/starred{/owner}{/repo}'; */ + starred_url: string; + /** @example 'https://api.github.com/users/octocat/subscriptions'; */ + subscriptions_url: string; + /** @example 'https://api.github.com/users/octocat/orgs'; */ + organizations_url: string; + /** @example 'https://api.github.com/users/octocat/repos'; */ + repos_url: string; + /** @example 'https://api.github.com/users/octocat/events{/privacy}'; */ + events_url: string; + /** @example 'https://api.github.com/users/octocat/received_events'; */ + received_events_url: string; + /** @example 'User'; */ + type: string; + site_admin: boolean; + }; + assets: any[]; +} + +export interface GhUpdateReleaseResponse { + /** @example 'https://api.github.com/repos/octocat/Hello-World/releases/1'; */ + url: string; + /** @example 'https://github.com/octocat/Hello-World/releases/v1.0.0'; */ + html_url: string; + /** @example 'https://api.github.com/repos/octocat/Hello-World/releases/1/assets'; */ + assets_url: string; + /** @example 'https://uploads.github.com/repos/octocat/Hello-World/releases/1/assets{?name,label}'; */ + upload_url: string; + /** @example 'https://api.github.com/repos/octocat/Hello-World/tarball/v1.0.0'; */ + tarball_url: string; + /** @example 'https://api.github.com/repos/octocat/Hello-World/zipball/v1.0.0'; */ + zipball_url: string; + id: number; + /** @example 'MDc6UmVsZWFzZTE='; */ + node_id: string; + /** @example 'v1.0.0'; */ + tag_name: string; + /** @example 'master'; */ + target_commitish: string; + /** @example 'v1.0.0'; */ + name: string; + /** @example 'Description of the release'; */ + body: string; + draft: boolean; + prerelease: boolean; + /** @example '2013-02-27T19:35:32Z'; */ + created_at: string; + /** @example '2013-02-27T19:35:32Z'; */ + published_at: string; + author: { + /** @example 'octocat'; */ + login: string; + id: number; + /** @example 'MDQ6VXNlcjE='; */ + node_id: string; + /** @example 'https://github.com/images/error/octocat_happy.gif'; */ + avatar_url: string; + /** @example ''; */ + gravatar_id: string; + /** @example 'https://api.github.com/users/octocat'; */ + url: string; + /** @example 'https://github.com/octocat'; */ + html_url: string; + /** @example 'https://api.github.com/users/octocat/followers'; */ + followers_url: string; + /** @example 'https://api.github.com/users/octocat/following{/other_user}'; */ + following_url: string; + /** @example 'https://api.github.com/users/octocat/gists{/gist_id}'; */ + gists_url: string; + /** @example 'https://api.github.com/users/octocat/starred{/owner}{/repo}'; */ + starred_url: string; + /** @example 'https://api.github.com/users/octocat/subscriptions'; */ + subscriptions_url: string; + /** @example 'https://api.github.com/users/octocat/orgs'; */ + organizations_url: string; + /** @example 'https://api.github.com/users/octocat/repos'; */ + repos_url: string; + /** @example 'https://api.github.com/users/octocat/events{/privacy}'; */ + events_url: string; + /** @example 'https://api.github.com/users/octocat/received_events'; */ + received_events_url: string; + /** @example 'User'; */ + type: string; + site_admin: boolean; + }; + assets: [ + { + /** @example 'https://api.github.com/repos/octocat/Hello-World/releases/assets/1'; */ + url: string; + /** @example 'https://github.com/octocat/Hello-World/releases/download/v1.0.0/example.zip'; */ + browser_download_url: string; + id: number; + /** @example 'MDEyOlJlbGVhc2VBc3NldDE='; */ + node_id: string; + /** @example 'example.zip'; */ + name: string; + /** @example 'short description'; */ + label: string; + /** @example 'uploaded'; */ + state: string; + /** @example 'application/zip'; */ + content_type: string; + size: number; + download_count: number; + /** @example '2013-02-27T19:35:32Z'; */ + created_at: string; + /** @example '2013-02-27T19:35:32Z'; */ + updated_at: string; + uploader: { + /** @example 'octocat'; */ + login: string; + id: number; + /** @example 'MDQ6VXNlcjE='; */ + node_id: string; + /** @example 'https://github.com/images/error/octocat_happy.gif'; */ + avatar_url: string; + /** @example ''; */ + gravatar_id: string; + /** @example 'https://api.github.com/users/octocat'; */ + url: string; + /** @example 'https://github.com/octocat'; */ + html_url: string; + /** @example 'https://api.github.com/users/octocat/followers'; */ + followers_url: string; + /** @example 'https://api.github.com/users/octocat/following{/other_user}'; */ + following_url: string; + /** @example 'https://api.github.com/users/octocat/gists{/gist_id}'; */ + gists_url: string; + /** @example 'https://api.github.com/users/octocat/starred{/owner}{/repo}'; */ + starred_url: string; + /** @example 'https://api.github.com/users/octocat/subscriptions'; */ + subscriptions_url: string; + /** @example 'https://api.github.com/users/octocat/orgs'; */ + organizations_url: string; + /** @example 'https://api.github.com/users/octocat/repos'; */ + repos_url: string; + /** @example 'https://api.github.com/users/octocat/events{/privacy}'; */ + events_url: string; + /** @example 'https://api.github.com/users/octocat/received_events'; */ + received_events_url: string; + /** @example 'User'; */ + type: string; + site_admin: boolean; + }; + }, + ]; +} + +export interface GhGetReleaseResponse { + /** @example 'https://api.github.com/repos/octocat/Hello-World/releases/1'; */ + url: string; + /** @example 'https://github.com/octocat/Hello-World/releases/v1.0.0'; */ + html_url: string; + /** @example 'https://api.github.com/repos/octocat/Hello-World/releases/1/assets'; */ + assets_url: string; + /** @example 'https://uploads.github.com/repos/octocat/Hello-World/releases/1/assets{?name,label}'; */ + upload_url: string; + /** @example 'https://api.github.com/repos/octocat/Hello-World/tarball/v1.0.0'; */ + tarball_url: string; + /** @example 'https://api.github.com/repos/octocat/Hello-World/zipball/v1.0.0'; */ + zipball_url: string; + id: number; + /** @example 'MDc6UmVsZWFzZTE='; */ + node_id: string; + /** @example 'v1.0.0'; */ + tag_name: string; + /** @example 'master'; */ + target_commitish: string; + /** @example 'v1.0.0'; */ + name: string; + /** @example 'Description of the release'; */ + body: string; + draft: boolean; + prerelease: boolean; + /** @example '2013-02-27T19:35:32Z'; */ + created_at: string; + /** @example '2013-02-27T19:35:32Z'; */ + published_at: string; + author: { + /** @example 'octocat'; */ + login: string; + id: number; + /** @example 'MDQ6VXNlcjE='; */ + node_id: string; + /** @example 'https://github.com/images/error/octocat_happy.gif'; */ + avatar_url: string; + /** @example ''; */ + gravatar_id: string; + /** @example 'https://api.github.com/users/octocat'; */ + url: string; + /** @example 'https://github.com/octocat'; */ + html_url: string; + /** @example 'https://api.github.com/users/octocat/followers'; */ + followers_url: string; + /** @example 'https://api.github.com/users/octocat/following{/other_user}'; */ + following_url: string; + /** @example 'https://api.github.com/users/octocat/gists{/gist_id}'; */ + gists_url: string; + /** @example 'https://api.github.com/users/octocat/starred{/owner}{/repo}'; */ + starred_url: string; + /** @example 'https://api.github.com/users/octocat/subscriptions'; */ + subscriptions_url: string; + /** @example 'https://api.github.com/users/octocat/orgs'; */ + organizations_url: string; + /** @example 'https://api.github.com/users/octocat/repos'; */ + repos_url: string; + /** @example 'https://api.github.com/users/octocat/events{/privacy}'; */ + events_url: string; + /** @example 'https://api.github.com/users/octocat/received_events'; */ + received_events_url: string; + /** @example 'User'; */ + type: string; + site_admin: boolean; + }; + assets: [ + { + /** @example 'https://api.github.com/repos/octocat/Hello-World/releases/assets/1'; */ + url: string; + /** @example 'https://github.com/octocat/Hello-World/releases/download/v1.0.0/example.zip'; */ + browser_download_url: string; + id: number; + /** @example 'MDEyOlJlbGVhc2VBc3NldDE='; */ + node_id: string; + /** @example 'example.zip'; */ + name: string; + /** @example 'short description'; */ + label: string; + /** @example 'uploaded'; */ + state: string; + /** @example 'application/zip'; */ + content_type: string; + size: number; + download_count: number; + /** @example '2013-02-27T19:35:32Z'; */ + created_at: string; + /** @example '2013-02-27T19:35:32Z'; */ + updated_at: string; + uploader: { + /** @example 'octocat'; */ + login: string; + id: number; + /** @example 'MDQ6VXNlcjE='; */ + node_id: string; + /** @example 'https://github.com/images/error/octocat_happy.gif'; */ + avatar_url: string; + /** @example ''; */ + gravatar_id: string; + /** @example 'https://api.github.com/users/octocat'; */ + url: string; + /** @example 'https://github.com/octocat'; */ + html_url: string; + /** @example 'https://api.github.com/users/octocat/followers'; */ + followers_url: string; + /** @example 'https://api.github.com/users/octocat/following{/other_user}'; */ + following_url: string; + /** @example 'https://api.github.com/users/octocat/gists{/gist_id}'; */ + gists_url: string; + /** @example 'https://api.github.com/users/octocat/starred{/owner}{/repo}'; */ + starred_url: string; + /** @example 'https://api.github.com/users/octocat/subscriptions'; */ + subscriptions_url: string; + /** @example 'https://api.github.com/users/octocat/orgs'; */ + organizations_url: string; + /** @example 'https://api.github.com/users/octocat/repos'; */ + repos_url: string; + /** @example 'https://api.github.com/users/octocat/events{/privacy}'; */ + events_url: string; + /** @example 'https://api.github.com/users/octocat/received_events'; */ + received_events_url: string; + /** @example 'User'; */ + type: string; + site_admin: boolean; + }; + }, + ]; +} + +export interface GhGetCommitResponse { + /** @example 'https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e'; */ + url: string; + /** @example '6dcb09b5b57875f334f61aebed695e2e4193db5e'; */ + sha: string; + /** @example 'MDY6Q29tbWl0NmRjYjA5YjViNTc4NzVmMzM0ZjYxYWViZWQ2OTVlMmU0MTkzZGI1ZQ=='; */ + node_id: string; + /** @example 'https://github.com/octocat/Hello-World/commit/6dcb09b5b57875f334f61aebed695e2e4193db5e'; */ + html_url: string; + /** @example 'https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e/comments'; */ + comments_url: string; + commit: { + /** @example 'https://api.github.com/repos/octocat/Hello-World/git/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e'; */ + url: string; + author: { + /** @example 'Monalisa Octocat'; */ + name: string; + /** @example 'support@github.com'; */ + email: string; + /** @example '2011-04-14T16:00:49Z'; */ + date: string; + }; + committer: { + /** @example 'Monalisa Octocat'; */ + name: string; + /** @example 'support@github.com'; */ + email: string; + /** @example '2011-04-14T16:00:49Z'; */ + date: string; + }; + /** @example 'Fix all the bugs'; */ + message: string; + tree: { + /** @example 'https://api.github.com/repos/octocat/Hello-World/tree/6dcb09b5b57875f334f61aebed695e2e4193db5e'; */ + url: string; + /** @example '6dcb09b5b57875f334f61aebed695e2e4193db5e'; */ + sha: string; + }; + comment_count: number; + verification: { + verified: boolean; + /** @example 'unsigned'; */ + reason: string; + signature: null | any; + payload: null | any; + }; + }; + author: { + /** @example 'octocat'; */ + login: string; + id: number; + /** @example 'MDQ6VXNlcjE='; */ + node_id: string; + /** @example 'https://github.com/images/error/octocat_happy.gif'; */ + avatar_url: string; + /** @example ''; */ + gravatar_id: string; + /** @example 'https://api.github.com/users/octocat'; */ + url: string; + /** @example 'https://github.com/octocat'; */ + html_url: string; + /** @example 'https://api.github.com/users/octocat/followers'; */ + followers_url: string; + /** @example 'https://api.github.com/users/octocat/following{/other_user}'; */ + following_url: string; + /** @example 'https://api.github.com/users/octocat/gists{/gist_id}'; */ + gists_url: string; + /** @example 'https://api.github.com/users/octocat/starred{/owner}{/repo}'; */ + starred_url: string; + /** @example 'https://api.github.com/users/octocat/subscriptions'; */ + subscriptions_url: string; + /** @example 'https://api.github.com/users/octocat/orgs'; */ + organizations_url: string; + /** @example 'https://api.github.com/users/octocat/repos'; */ + repos_url: string; + /** @example 'https://api.github.com/users/octocat/events{/privacy}'; */ + events_url: string; + /** @example 'https://api.github.com/users/octocat/received_events'; */ + received_events_url: string; + /** @example 'User'; */ + type: string; + site_admin: boolean; + }; + committer: { + /** @example 'octocat'; */ + login: string; + id: number; + /** @example 'MDQ6VXNlcjE='; */ + node_id: string; + /** @example 'https://github.com/images/error/octocat_happy.gif'; */ + avatar_url: string; + /** @example ''; */ + gravatar_id: string; + /** @example 'https://api.github.com/users/octocat'; */ + url: string; + /** @example 'https://github.com/octocat'; */ + html_url: string; + /** @example 'https://api.github.com/users/octocat/followers'; */ + followers_url: string; + /** @example 'https://api.github.com/users/octocat/following{/other_user}'; */ + following_url: string; + /** @example 'https://api.github.com/users/octocat/gists{/gist_id}'; */ + gists_url: string; + /** @example 'https://api.github.com/users/octocat/starred{/owner}{/repo}'; */ + starred_url: string; + /** @example 'https://api.github.com/users/octocat/subscriptions'; */ + subscriptions_url: string; + /** @example 'https://api.github.com/users/octocat/orgs'; */ + organizations_url: string; + /** @example 'https://api.github.com/users/octocat/repos'; */ + repos_url: string; + /** @example 'https://api.github.com/users/octocat/events{/privacy}'; */ + events_url: string; + /** @example 'https://api.github.com/users/octocat/received_events'; */ + received_events_url: string; + /** @example 'User'; */ + type: string; + site_admin: boolean; + }; + parents: [ + { + /** @example 'https://api.github.com/repos/octocat/Hello-World/commits/6dcb09b5b57875f334f61aebed695e2e4193db5e'; */ + url: string; + /** @example '6dcb09b5b57875f334f61aebed695e2e4193db5e'; */ + sha: string; + }, + ]; + stats: { + additions: number; + deletions: number; + total: number; + }; + files: [ + { + /** @example 'file1.txt'; */ + filename: string; + additions: number; + deletions: number; + changes: number; + /** @example 'modified'; */ + status: string; + /** @example 'https://github.com/octocat/Hello-World/raw/7ca483543807a51b6079e54ac4cc392bc29ae284/file1.txt'; */ + raw_url: string; + /** @example 'https://github.com/octocat/Hello-World/blob/7ca483543807a51b6079e54ac4cc392bc29ae284/file1.txt'; */ + blob_url: string; + /** @example '@@ -29,7 +29,7 @@\n.....'; */ + patch: string; + }, + ]; +} + +export interface GhGetBranchResponse { + /** @example 'master'; */ + name: string; + commit: { + /** @example '7fd1a60b01f91b314f59955a4e4d4e80d8edf11d'; */ + sha: string; + /** @example 'MDY6Q29tbWl0N2ZkMWE2MGIwMWY5MWIzMTRmNTk5NTVhNGU0ZDRlODBkOGVkZjExZA=='; */ + node_id: string; + commit: { + author: { + /** @example 'The Octocat'; */ + name: string; + /** @example '2012-03-06T15:06:50-08:00'; */ + date: string; + /** @example 'octocat@nowhere.com'; */ + email: string; + }; + /** @example 'https://api.github.com/repos/octocat/Hello-World/git/commits/7fd1a60b01f91b314f59955a4e4d4e80d8edf11d'; */ + url: string; + /** @example 'Merge pull request #6 from Spaceghost/patch-1\n\nNew line at end of file.'; */ + message: string; + tree: { + /** @example 'b4eecafa9be2f2006ce1b709d6857b07069b4608'; */ + sha: string; + /** @example 'https://api.github.com/repos/octocat/Hello-World/git/trees/b4eecafa9be2f2006ce1b709d6857b07069b4608'; */ + url: string; + }; + committer: { + /** @example 'The Octocat'; */ + name: string; + /** @example '2012-03-06T15:06:50-08:00'; */ + date: string; + /** @example 'octocat@nowhere.com'; */ + email: string; + }; + verification: { + verified: boolean; + /** @example 'unsigned'; */ + reason: string; + signature: null | any; + payload: null | any; + }; + }; + author: { + /** @example ''; */ + gravatar_id: string; + /** @example 'https://secure.gravatar.com/avatar/7ad39074b0584bc555d0417ae3e7d974?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-140.png'; */ + avatar_url: string; + /** @example 'https://api.github.com/users/octocat'; */ + url: string; + id: number; + /** @example 'octocat'; */ + login: string; + }; + parents: [ + { + /** @example '553c2077f0edc3d5dc5d17262f6aa498e69d6f8e'; */ + sha: string; + /** @example 'https://api.github.com/repos/octocat/Hello-World/commits/553c2077f0edc3d5dc5d17262f6aa498e69d6f8e'; */ + url: string; + }, + { + /** @example '762941318ee16e59dabbacb1b4049eec22f0d303'; */ + sha: string; + /** @example 'https://api.github.com/repos/octocat/Hello-World/commits/762941318ee16e59dabbacb1b4049eec22f0d303'; */ + url: string; + }, + ]; + /** @example 'https://api.github.com/repos/octocat/Hello-World/commits/7fd1a60b01f91b314f59955a4e4d4e80d8edf11d'; */ + url: string; + committer: { + /** @example ''; */ + gravatar_id: string; + /** @example 'https://secure.gravatar.com/avatar/7ad39074b0584bc555d0417ae3e7d974?d=https://a248.e.akamai.net/assets.github.com%2Fimages%2Fgravatars%2Fgravatar-140.png'; */ + avatar_url: string; + /** @example 'https://api.github.com/users/octocat'; */ + url: string; + id: number; + /** @example 'octocat'; */ + login: string; + }; + }; + _links: { + /** @example 'https://github.com/octocat/Hello-World/tree/master'; */ + html: string; + /** @example 'https://api.github.com/repos/octocat/Hello-World/branches/master'; */ + self: string; + }; + protected: boolean; + protection: { + enabled: boolean; + required_status_checks: { + /** @example 'non_admins'; */ + enforcement_level: string; + /** @example ['ci-test', 'linter'] */ + contexts: string[]; + }; + }; + /** @example 'https://api.github.com/repos/octocat/hello-world/branches/master/protection'; */ + protection_url: string; +} + +export interface GhCreateCommitResponse { + /** @example '7638417db6d59f3c431d3e1f261cc637155684cd'; */ + sha: string; + /** @example 'MDY6Q29tbWl0NzYzODQxN2RiNmQ1OWYzYzQzMWQzZTFmMjYxY2M2MzcxNTU2ODRjZA=='; */ + node_id: string; + /** @example 'https://api.github.com/repos/octocat/Hello-World/git/commits/7638417db6d59f3c431d3e1f261cc637155684cd'; */ + url: string; + author: { + /** @example '2014-11-07T22:01:45Z'; */ + date: string; + /** @example 'Monalisa Octocat'; */ + name: string; + /** @example 'octocat@github.com'; */ + email: string; + }; + committer: { + /** @example '2014-11-07T22:01:45Z'; */ + date: string; + /** @example 'Monalisa Octocat'; */ + name: string; + /** @example 'octocat@github.com'; */ + email: string; + }; + /** @example 'my commit message'; */ + message: string; + tree: { + /** @example 'https://api.github.com/repos/octocat/Hello-World/git/trees/827efc6d56897b048c772eb4087f854f46256132'; */ + url: string; + /** @example '827efc6d56897b048c772eb4087f854f46256132'; */ + sha: string; + }; + parents: [ + { + /** @example 'https://api.github.com/repos/octocat/Hello-World/git/commits/7d1b31e74ee336d15cbd21741bc88a537ed063a0'; */ + url: string; + /** @example '7d1b31e74ee336d15cbd21741bc88a537ed063a0'; */ + sha: string; + }, + ]; + verification: { + verified: boolean; + /** @example 'unsigned'; */ + reason: string; + signature: null | any; + payload: null | any; + }; +} + +export interface GhCreateReferenceResponse { + /** @example 'refs/heads/featureA'; */ + ref: string; + /** @example 'MDM6UmVmcmVmcy9oZWFkcy9mZWF0dXJlQQ=='; */ + node_id: string; + /** @example 'https://api.github.com/repos/octocat/Hello-World/git/refs/heads/featureA'; */ + url: string; + object: { + /** @example 'commit'; */ + type: string; + /** @example 'aa218f56b14c9653891f9e74264a383fa43fefbd'; */ + sha: string; + /** @example 'https://api.github.com/repos/octocat/Hello-World/git/commits/aa218f56b14c9653891f9e74264a383fa43fefbd'; */ + url: string; + }; +} + +export interface GhUpdateReferenceResponse { + /** @example 'refs/heads/featureA'; */ + ref: string; + /** @example 'MDM6UmVmcmVmcy9oZWFkcy9mZWF0dXJlQQ=='; */ + node_id: string; + /** @example 'https://api.github.com/repos/octocat/Hello-World/git/refs/heads/featureA'; */ + url: string; + object: { + /** @example 'commit'; */ + type: string; + /** @example 'aa218f56b14c9653891f9e74264a383fa43fefbd'; */ + sha: string; + /** @example 'https://api.github.com/repos/octocat/Hello-World/git/commits/aa218f56b14c9653891f9e74264a383fa43fefbd'; */ + url: string; + }; +} + +export interface GhMergeResponse { + /** @example '7fd1a60b01f91b314f59955a4e4d4e80d8edf11d'; */ + sha: string; + /** @example 'MDY6Q29tbWl0N2ZkMWE2MGIwMWY5MWIzMTRmNTk5NTVhNGU0ZDRlODBkOGVkZjExZA=='; */ + node_id: string; + commit: { + author: { + /** @example 'The Octocat'; */ + name: string; + /** @example '2012-03-06T15:06:50-08:00'; */ + date: string; + /** @example 'octocat@nowhere.com'; */ + email: string; + }; + committer: { + /** @example 'The Octocat'; */ + name: string; + /** @example '2012-03-06T15:06:50-08:00'; */ + date: string; + /** @example 'octocat@nowhere.com'; */ + email: string; + }; + /** @example 'Shipped cool_feature!'; */ + message: string; + tree: { + /** @example 'b4eecafa9be2f2006ce1b709d6857b07069b4608'; */ + sha: string; + /** @example 'https://api.github.com/repos/octocat/Hello-World/git/trees/b4eecafa9be2f2006ce1b709d6857b07069b4608'; */ + url: string; + }; + /** @example 'https://api.github.com/repos/octocat/Hello-World/git/commits/7fd1a60b01f91b314f59955a4e4d4e80d8edf11d'; */ + url: string; + comment_count: number; + verification: { + verified: boolean; + /** @example 'unsigned'; */ + reason: string; + signature: null | any; + payload: null | any; + }; + }; + /** @example 'https://api.github.com/repos/octocat/Hello-World/commits/7fd1a60b01f91b314f59955a4e4d4e80d8edf11d'; */ + url: string; + /** @example 'https://github.com/octocat/Hello-World/commit/7fd1a60b01f91b314f59955a4e4d4e80d8edf11d'; */ + html_url: string; + /** @example 'https://api.github.com/repos/octocat/Hello-World/commits/7fd1a60b01f91b314f59955a4e4d4e80d8edf11d/comments'; */ + comments_url: string; + author: { + /** @example 'octocat'; */ + login: string; + id: number; + /** @example 'MDQ6VXNlcjE='; */ + node_id: string; + /** @example 'https://github.com/images/error/octocat_happy.gif'; */ + avatar_url: string; + /** @example ''; */ + gravatar_id: string; + /** @example 'https://api.github.com/users/octocat'; */ + url: string; + /** @example 'https://github.com/octocat'; */ + html_url: string; + /** @example 'https://api.github.com/users/octocat/followers'; */ + followers_url: string; + /** @example 'https://api.github.com/users/octocat/following{/other_user}'; */ + following_url: string; + /** @example 'https://api.github.com/users/octocat/gists{/gist_id}'; */ + gists_url: string; + /** @example 'https://api.github.com/users/octocat/starred{/owner}{/repo}'; */ + starred_url: string; + /** @example 'https://api.github.com/users/octocat/subscriptions'; */ + subscriptions_url: string; + /** @example 'https://api.github.com/users/octocat/orgs'; */ + organizations_url: string; + /** @example 'https://api.github.com/users/octocat/repos'; */ + repos_url: string; + /** @example 'https://api.github.com/users/octocat/events{/privacy}'; */ + events_url: string; + /** @example 'https://api.github.com/users/octocat/received_events'; */ + received_events_url: string; + /** @example 'User'; */ + type: string; + site_admin: boolean; + }; + committer: { + /** @example 'octocat'; */ + login: string; + id: number; + /** @example 'MDQ6VXNlcjE='; */ + node_id: string; + /** @example 'https://github.com/images/error/octocat_happy.gif'; */ + avatar_url: string; + /** @example ''; */ + gravatar_id: string; + /** @example 'https://api.github.com/users/octocat'; */ + url: string; + /** @example 'https://github.com/octocat'; */ + html_url: string; + /** @example 'https://api.github.com/users/octocat/followers'; */ + followers_url: string; + /** @example 'https://api.github.com/users/octocat/following{/other_user}'; */ + following_url: string; + /** @example 'https://api.github.com/users/octocat/gists{/gist_id}'; */ + gists_url: string; + /** @example 'https://api.github.com/users/octocat/starred{/owner}{/repo}'; */ + starred_url: string; + /** @example 'https://api.github.com/users/octocat/subscriptions'; */ + subscriptions_url: string; + /** @example 'https://api.github.com/users/octocat/orgs'; */ + organizations_url: string; + /** @example 'https://api.github.com/users/octocat/repos'; */ + repos_url: string; + /** @example 'https://api.github.com/users/octocat/events{/privacy}'; */ + events_url: string; + /** @example 'https://api.github.com/users/octocat/received_events'; */ + received_events_url: string; + /** @example 'User'; */ + type: string; + site_admin: boolean; + }; + parents: [ + { + /** @example '553c2077f0edc3d5dc5d17262f6aa498e69d6f8e'; */ + sha: string; + /** @example 'https://api.github.com/repos/octocat/Hello-World/commits/553c2077f0edc3d5dc5d17262f6aa498e69d6f8e'; */ + url: string; + }, + { + /** @example '762941318ee16e59dabbacb1b4049eec22f0d303'; */ + sha: string; + /** @example 'https://api.github.com/repos/octocat/Hello-World/commits/762941318ee16e59dabbacb1b4049eec22f0d303'; */ + url: string; + }, + ]; +} + +export interface GhCreateTagObjectResponse { + /** @example 'MDM6VGFnOTQwYmQzMzYyNDhlZmFlMGY5ZWU1YmM3YjJkNWM5ODU4ODdiMTZhYw=='; */ + node_id: string; + /** @example 'v0.0.1'; */ + tag: string; + /** @example '940bd336248efae0f9ee5bc7b2d5c985887b16ac'; */ + sha: string; + /** @example 'https://api.github.com/repos/octocat/Hello-World/git/tags/940bd336248efae0f9ee5bc7b2d5c985887b16ac'; */ + url: string; + /** @example 'initial version'; */ + message: string; + tagger: { + /** @example 'Monalisa Octocat'; */ + name: string; + /** @example 'octocat@github.com'; */ + email: string; + /** @example '2014-11-07T22:01:45Z'; */ + date: string; + }; + object: { + /** @example 'commit'; */ + type: string; + /** @example 'c3d0be41ecbe669545ee3e94d31ed9a4bc91ee3c'; */ + sha: string; + /** @example 'https://api.github.com/repos/octocat/Hello-World/git/commits/c3d0be41ecbe669545ee3e94d31ed9a4bc91ee3c'; */ + url: string; + }; + verification: { + verified: boolean; + /** @example 'unsigned'; */ + reason: string; + signature: null | any; + payload: null | any; + }; +} + +export interface GhGetRepositoryResponse { + id: number; + /** @example 'MDEwOlJlcG9zaXRvcnkxMjk2MjY5'; */ + node_id: string; + /** @example 'Hello-World'; */ + name: string; + /** @example 'octocat/Hello-World'; */ + full_name: string; + owner: { + /** @example 'octocat'; */ + login: string; + id: number; + /** @example 'MDQ6VXNlcjE='; */ + node_id: string; + /** @example 'https://github.com/images/error/octocat_happy.gif'; */ + avatar_url: string; + /** @example ''; */ + gravatar_id: string; + /** @example 'https://api.github.com/users/octocat'; */ + url: string; + /** @example 'https://github.com/octocat'; */ + html_url: string; + /** @example 'https://api.github.com/users/octocat/followers'; */ + followers_url: string; + /** @example 'https://api.github.com/users/octocat/following{/other_user}'; */ + following_url: string; + /** @example 'https://api.github.com/users/octocat/gists{/gist_id}'; */ + gists_url: string; + /** @example 'https://api.github.com/users/octocat/starred{/owner}{/repo}'; */ + starred_url: string; + /** @example 'https://api.github.com/users/octocat/subscriptions'; */ + subscriptions_url: string; + /** @example 'https://api.github.com/users/octocat/orgs'; */ + organizations_url: string; + /** @example 'https://api.github.com/users/octocat/repos'; */ + repos_url: string; + /** @example 'https://api.github.com/users/octocat/events{/privacy}'; */ + events_url: string; + /** @example 'https://api.github.com/users/octocat/received_events'; */ + received_events_url: string; + /** @example 'User'; */ + type: string; + site_admin: boolean; + }; + private: boolean; + /** @example 'https://github.com/octocat/Hello-World'; */ + html_url: string; + /** @example 'This your first repo!'; */ + description: string; + fork: boolean; + /** @example 'https://api.github.com/repos/octocat/Hello-World'; */ + url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}'; */ + archive_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/assignees{/user}'; */ + assignees_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}'; */ + blobs_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/branches{/branch}'; */ + branches_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}'; */ + collaborators_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/comments{/number}'; */ + comments_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/commits{/sha}'; */ + commits_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}'; */ + compare_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/contents/{+path}'; */ + contents_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/contributors'; */ + contributors_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/deployments'; */ + deployments_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/downloads'; */ + downloads_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/events'; */ + events_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/forks'; */ + forks_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/git/commits{/sha}'; */ + git_commits_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/git/refs{/sha}'; */ + git_refs_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/git/tags{/sha}'; */ + git_tags_url: string; + /** @example 'git:github.com/octocat/Hello-World.git'; */ + git_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/issues/comments{/number}'; */ + issue_comment_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/issues/events{/number}'; */ + issue_events_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/issues{/number}'; */ + issues_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/keys{/key_id}'; */ + keys_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/labels{/name}'; */ + labels_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/languages'; */ + languages_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/merges'; */ + merges_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/milestones{/number}'; */ + milestones_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}'; */ + notifications_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/pulls{/number}'; */ + pulls_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/releases{/id}'; */ + releases_url: string; + /** @example 'git@github.com:octocat/Hello-World.git'; */ + ssh_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/stargazers'; */ + stargazers_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/statuses/{sha}'; */ + statuses_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/subscribers'; */ + subscribers_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/subscription'; */ + subscription_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/tags'; */ + tags_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/teams'; */ + teams_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/git/trees{/sha}'; */ + trees_url: string; + /** @example 'https://github.com/octocat/Hello-World.git'; */ + clone_url: string; + /** @example 'git:git.example.com/octocat/Hello-World'; */ + mirror_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/hooks'; */ + hooks_url: string; + /** @example 'https://svn.github.com/octocat/Hello-World'; */ + svn_url: string; + /** @example 'https://github.com'; */ + homepage: string; + language: null | any; + forks_count: number; + stargazers_count: number; + watchers_count: number; + size: number; + /** @example 'master'; */ + default_branch: string; + open_issues_count: number; + is_template: boolean; + /** @example ['octocat', 'atom', 'electron', 'api'] */ + topics: string[]; + has_issues: boolean; + has_projects: boolean; + has_wiki: boolean; + has_pages: boolean; + has_downloads: boolean; + archived: boolean; + disabled: boolean; + /** @example 'public'; */ + visibility: string; + /** @example '2011-01-26T19:06:43Z'; */ + pushed_at: string; + /** @example '2011-01-26T19:01:12Z'; */ + created_at: string; + /** @example '2011-01-26T19:14:43Z'; */ + updated_at: string; + permissions: { + pull: boolean; + triage: boolean; + push: boolean; + maintain: boolean; + admin: boolean; + }; + allow_rebase_merge: boolean; + template_repository: null | any; + /** @example 'ABTLWHOULUVAXGTRYU7OC2876QJ2O'; */ + temp_clone_token: string; + allow_squash_merge: boolean; + delete_branch_on_merge: boolean; + allow_merge_commit: boolean; + subscribers_count: number; + network_count: number; + license: { + /** @example 'mit'; */ + key: string; + /** @example 'MIT License'; */ + name: string; + /** @example 'MIT'; */ + spdx_id: string; + /** @example 'https://api.github.com/licenses/mit'; */ + url: string; + /** @example 'MDc6TGljZW5zZW1pdA=='; */ + node_id: string; + }; + organization: { + /** @example 'octocat'; */ + login: string; + id: number; + /** @example 'MDQ6VXNlcjE='; */ + node_id: string; + /** @example 'https://github.com/images/error/octocat_happy.gif'; */ + avatar_url: string; + /** @example ''; */ + gravatar_id: string; + /** @example 'https://api.github.com/users/octocat'; */ + url: string; + /** @example 'https://github.com/octocat'; */ + html_url: string; + /** @example 'https://api.github.com/users/octocat/followers'; */ + followers_url: string; + /** @example 'https://api.github.com/users/octocat/following{/other_user}'; */ + following_url: string; + /** @example 'https://api.github.com/users/octocat/gists{/gist_id}'; */ + gists_url: string; + /** @example 'https://api.github.com/users/octocat/starred{/owner}{/repo}'; */ + starred_url: string; + /** @example 'https://api.github.com/users/octocat/subscriptions'; */ + subscriptions_url: string; + /** @example 'https://api.github.com/users/octocat/orgs'; */ + organizations_url: string; + /** @example 'https://api.github.com/users/octocat/repos'; */ + repos_url: string; + /** @example 'https://api.github.com/users/octocat/events{/privacy}'; */ + events_url: string; + /** @example 'https://api.github.com/users/octocat/received_events'; */ + received_events_url: string; + /** @example 'Organization'; */ + type: string; + site_admin: boolean; + }; + parent: { + id: number; + /** @example 'MDEwOlJlcG9zaXRvcnkxMjk2MjY5'; */ + node_id: string; + /** @example 'Hello-World'; */ + name: string; + /** @example 'octocat/Hello-World'; */ + full_name: string; + owner: { + /** @example 'octocat'; */ + login: string; + id: number; + /** @example 'MDQ6VXNlcjE='; */ + node_id: string; + /** @example 'https://github.com/images/error/octocat_happy.gif'; */ + avatar_url: string; + /** @example ''; */ + gravatar_id: string; + /** @example 'https://api.github.com/users/octocat'; */ + url: string; + /** @example 'https://github.com/octocat'; */ + html_url: string; + /** @example 'https://api.github.com/users/octocat/followers'; */ + followers_url: string; + /** @example 'https://api.github.com/users/octocat/following{/other_user}'; */ + following_url: string; + /** @example 'https://api.github.com/users/octocat/gists{/gist_id}'; */ + gists_url: string; + /** @example 'https://api.github.com/users/octocat/starred{/owner}{/repo}'; */ + starred_url: string; + /** @example 'https://api.github.com/users/octocat/subscriptions'; */ + subscriptions_url: string; + /** @example 'https://api.github.com/users/octocat/orgs'; */ + organizations_url: string; + /** @example 'https://api.github.com/users/octocat/repos'; */ + repos_url: string; + /** @example 'https://api.github.com/users/octocat/events{/privacy}'; */ + events_url: string; + /** @example 'https://api.github.com/users/octocat/received_events'; */ + received_events_url: string; + /** @example 'User'; */ + type: string; + site_admin: boolean; + }; + private: boolean; + /** @example 'https://github.com/octocat/Hello-World'; */ + html_url: string; + /** @example 'This your first repo!'; */ + description: string; + fork: boolean; + /** @example 'https://api.github.com/repos/octocat/Hello-World'; */ + url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}'; */ + archive_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/assignees{/user}'; */ + assignees_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}'; */ + blobs_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/branches{/branch}'; */ + branches_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}'; */ + collaborators_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/comments{/number}'; */ + comments_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/commits{/sha}'; */ + commits_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}'; */ + compare_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/contents/{+path}'; */ + contents_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/contributors'; */ + contributors_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/deployments'; */ + deployments_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/downloads'; */ + downloads_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/events'; */ + events_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/forks'; */ + forks_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/git/commits{/sha}'; */ + git_commits_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/git/refs{/sha}'; */ + git_refs_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/git/tags{/sha}'; */ + git_tags_url: string; + /** @example 'git:github.com/octocat/Hello-World.git'; */ + git_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/issues/comments{/number}'; */ + issue_comment_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/issues/events{/number}'; */ + issue_events_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/issues{/number}'; */ + issues_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/keys{/key_id}'; */ + keys_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/labels{/name}'; */ + labels_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/languages'; */ + languages_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/merges'; */ + merges_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/milestones{/number}'; */ + milestones_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}'; */ + notifications_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/pulls{/number}'; */ + pulls_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/releases{/id}'; */ + releases_url: string; + /** @example 'git@github.com:octocat/Hello-World.git'; */ + ssh_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/stargazers'; */ + stargazers_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/statuses/{sha}'; */ + statuses_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/subscribers'; */ + subscribers_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/subscription'; */ + subscription_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/tags'; */ + tags_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/teams'; */ + teams_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/git/trees{/sha}'; */ + trees_url: string; + /** @example 'https://github.com/octocat/Hello-World.git'; */ + clone_url: string; + /** @example 'git:git.example.com/octocat/Hello-World'; */ + mirror_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/hooks'; */ + hooks_url: string; + /** @example 'https://svn.github.com/octocat/Hello-World'; */ + svn_url: string; + /** @example 'https://github.com'; */ + homepage: string; + language: null | any; + forks_count: number; + stargazers_count: number; + watchers_count: number; + size: number; + /** @example 'master'; */ + default_branch: string; + open_issues_count: number; + is_template: boolean; + /** @example ['octocat', 'atom', 'electron', 'api'] */ + topics: string[]; + has_issues: boolean; + has_projects: boolean; + has_wiki: boolean; + has_pages: boolean; + has_downloads: boolean; + archived: boolean; + disabled: boolean; + /** @example 'public'; */ + visibility: string; + /** @example '2011-01-26T19:06:43Z'; */ + pushed_at: string; + /** @example '2011-01-26T19:01:12Z'; */ + created_at: string; + /** @example '2011-01-26T19:14:43Z'; */ + updated_at: string; + permissions: { + admin: boolean; + push: boolean; + pull: boolean; + }; + allow_rebase_merge: boolean; + template_repository: null | any; + /** @example 'ABTLWHOULUVAXGTRYU7OC2876QJ2O'; */ + temp_clone_token: string; + allow_squash_merge: boolean; + delete_branch_on_merge: boolean; + allow_merge_commit: boolean; + subscribers_count: number; + network_count: number; + }; + source: { + id: number; + /** @example 'MDEwOlJlcG9zaXRvcnkxMjk2MjY5'; */ + node_id: string; + /** @example 'Hello-World'; */ + name: string; + /** @example 'octocat/Hello-World'; */ + full_name: string; + owner: { + /** @example 'octocat'; */ + login: string; + id: number; + /** @example 'MDQ6VXNlcjE='; */ + node_id: string; + /** @example 'https://github.com/images/error/octocat_happy.gif'; */ + avatar_url: string; + /** @example ''; */ + gravatar_id: string; + /** @example 'https://api.github.com/users/octocat'; */ + url: string; + /** @example 'https://github.com/octocat'; */ + html_url: string; + /** @example 'https://api.github.com/users/octocat/followers'; */ + followers_url: string; + /** @example 'https://api.github.com/users/octocat/following{/other_user}'; */ + following_url: string; + /** @example 'https://api.github.com/users/octocat/gists{/gist_id}'; */ + gists_url: string; + /** @example 'https://api.github.com/users/octocat/starred{/owner}{/repo}'; */ + starred_url: string; + /** @example 'https://api.github.com/users/octocat/subscriptions'; */ + subscriptions_url: string; + /** @example 'https://api.github.com/users/octocat/orgs'; */ + organizations_url: string; + /** @example 'https://api.github.com/users/octocat/repos'; */ + repos_url: string; + /** @example 'https://api.github.com/users/octocat/events{/privacy}'; */ + events_url: string; + /** @example 'https://api.github.com/users/octocat/received_events'; */ + received_events_url: string; + /** @example 'User'; */ + type: string; + site_admin: boolean; + }; + private: boolean; + /** @example 'https://github.com/octocat/Hello-World'; */ + html_url: string; + /** @example 'This your first repo!'; */ + description: string; + fork: boolean; + /** @example 'https://api.github.com/repos/octocat/Hello-World'; */ + url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/{archive_format}{/ref}'; */ + archive_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/assignees{/user}'; */ + assignees_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/git/blobs{/sha}'; */ + blobs_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/branches{/branch}'; */ + branches_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/collaborators{/collaborator}'; */ + collaborators_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/comments{/number}'; */ + comments_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/commits{/sha}'; */ + commits_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/compare/{base}...{head}'; */ + compare_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/contents/{+path}'; */ + contents_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/contributors'; */ + contributors_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/deployments'; */ + deployments_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/downloads'; */ + downloads_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/events'; */ + events_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/forks'; */ + forks_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/git/commits{/sha}'; */ + git_commits_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/git/refs{/sha}'; */ + git_refs_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/git/tags{/sha}'; */ + git_tags_url: string; + /** @example 'git:github.com/octocat/Hello-World.git'; */ + git_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/issues/comments{/number}'; */ + issue_comment_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/issues/events{/number}'; */ + issue_events_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/issues{/number}'; */ + issues_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/keys{/key_id}'; */ + keys_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/labels{/name}'; */ + labels_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/languages'; */ + languages_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/merges'; */ + merges_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/milestones{/number}'; */ + milestones_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/notifications{?since,all,participating}'; */ + notifications_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/pulls{/number}'; */ + pulls_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/releases{/id}'; */ + releases_url: string; + /** @example 'git@github.com:octocat/Hello-World.git'; */ + ssh_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/stargazers'; */ + stargazers_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/statuses/{sha}'; */ + statuses_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/subscribers'; */ + subscribers_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/subscription'; */ + subscription_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/tags'; */ + tags_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/teams'; */ + teams_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/git/trees{/sha}'; */ + trees_url: string; + /** @example 'https://github.com/octocat/Hello-World.git'; */ + clone_url: string; + /** @example 'git:git.example.com/octocat/Hello-World'; */ + mirror_url: string; + /** @example 'http://api.github.com/repos/octocat/Hello-World/hooks'; */ + hooks_url: string; + /** @example 'https://svn.github.com/octocat/Hello-World'; */ + svn_url: string; + /** @example 'https://github.com'; */ + homepage: string; + language: null | any; + forks_count: number; + stargazers_count: number; + watchers_count: number; + size: number; + /** @example 'master'; */ + default_branch: string; + open_issues_count: number; + is_template: boolean; + /** @example ['octocat', 'atom', 'electron', 'api'] */ + topics: string[]; + has_issues: boolean; + has_projects: boolean; + has_wiki: boolean; + has_pages: boolean; + has_downloads: boolean; + archived: boolean; + disabled: boolean; + /** @example 'public'; */ + visibility: string; + /** @example '2011-01-26T19:06:43Z'; */ + pushed_at: string; + /** @example '2011-01-26T19:01:12Z'; */ + created_at: string; + /** @example '2011-01-26T19:14:43Z'; */ + updated_at: string; + permissions: { + admin: boolean; + push: boolean; + pull: boolean; + }; + allow_rebase_merge: boolean; + template_repository: null | any; + /** @example 'ABTLWHOULUVAXGTRYU7OC2876QJ2O'; */ + temp_clone_token: string; + allow_squash_merge: boolean; + delete_branch_on_merge: boolean; + allow_merge_commit: boolean; + subscribers_count: number; + network_count: number; + }; +} diff --git a/yarn.lock b/yarn.lock index d341a67170..cca4a0ed44 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11408,6 +11408,11 @@ date-fns@^2.0.0-alpha.27, date-fns@^2.16.1: resolved "https://registry.npmjs.org/date-fns/-/date-fns-2.18.0.tgz#08e50aee300ad0d2c5e054e3f0d10d8f9cdfe09e" integrity sha512-NYyAg4wRmGVU4miKq5ivRACOODdZRY3q5WLmOJSq8djyzftYphU7dTHLcEtLqEvfqMKQ0jVv91P4BAwIjsXIcw== +date-fns@^2.19.0: + version "2.19.0" + resolved "https://artifactory.spotify.net/artifactory/api/npm/virtual-npm/date-fns/-/date-fns-2.19.0.tgz#65193348635a28d5d916c43ec7ce6fbd145059e1" + integrity sha1-ZRkzSGNaKNXZFsQ+x85vvRRQWeE= + dateformat@^3.0.0: version "3.0.3" resolved "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" From 64ede919009915fc34605d83aa0c7860d1a9c4cd Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Mon, 12 Apr 2021 18:39:25 +0200 Subject: [PATCH 002/140] Add 'Usage' to docs Signed-off-by: Erik Engervall --- plugins/release-manager-as-a-service/README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/plugins/release-manager-as-a-service/README.md b/plugins/release-manager-as-a-service/README.md index cdd50d35c6..ffb39f2c8f 100644 --- a/plugins/release-manager-as-a-service/README.md +++ b/plugins/release-manager-as-a-service/README.md @@ -43,3 +43,7 @@ Looking at the flow above, a common release lifecycle could be: - Your CI 1. Detects the new tag by matching the git reference `refs/tags/version-.*` 1. Builds and deploys to production for testing + +## Usage + +The plugin exports a single full-page extension `ReleaseManagerAsAServicePage`, which one can add to an app like a usual top-level tool on a dedicated route. From 33aa168c2b285a1a098c23df4b5d9ca725d79aea Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Tue, 13 Apr 2021 09:39:39 +0200 Subject: [PATCH 003/140] Replace references to GHE with GitHub Signed-off-by: Erik Engervall --- .../release-manager-as-a-service/README.md | 12 ++-- .../src/ReleaseManagerAsAService.tsx | 2 +- .../src/api/RMaaSApiClient.ts | 12 ++-- .../src/cards/createRc/CreateRc.test.tsx | 6 +- .../src/cards/createRc/CreateRc.tsx | 53 +++++++++--------- ...heInfo.test.ts => getRcGitHubInfo.test.ts} | 16 +++--- .../{getRcGheInfo.ts => getRcGitHubInfo.ts} | 2 +- .../{createGheRc.test.ts => createRc.test.ts} | 12 ++-- .../{createGheRc.ts => createRc.ts} | 24 ++++---- .../src/cards/info/Info.tsx | 22 ++++---- .../cards/info/{rmaas-flow.png => flow.png} | Bin .../src/cards/patchRc/PatchBody.tsx | 40 ++++++------- .../src/cards/promoteRc/PromoteRc.tsx | 4 +- .../src/cards/promoteRc/PromoteRcBody.tsx | 20 ++++--- ...promoteGheRc.test.ts => promoteRc.test.ts} | 6 +- .../{promoteGheRc.ts => promoteRc.ts} | 6 +- .../src/components/Differ.tsx | 4 +- .../src/components/NoLatestRelease.tsx | 2 +- .../src/test-helpers/test-helpers.test.ts | 2 +- .../src/test-helpers/test-helpers.ts | 4 +- 20 files changed, 128 insertions(+), 121 deletions(-) rename plugins/release-manager-as-a-service/src/cards/createRc/{getRcGheInfo.test.ts => getRcGitHubInfo.test.ts} (84%) rename plugins/release-manager-as-a-service/src/cards/createRc/{getRcGheInfo.ts => getRcGitHubInfo.ts} (98%) rename plugins/release-manager-as-a-service/src/cards/createRc/sideEffects/{createGheRc.test.ts => createRc.test.ts} (92%) rename plugins/release-manager-as-a-service/src/cards/createRc/sideEffects/{createGheRc.ts => createRc.ts} (85%) rename plugins/release-manager-as-a-service/src/cards/info/{rmaas-flow.png => flow.png} (100%) rename plugins/release-manager-as-a-service/src/cards/promoteRc/sideEffects/{promoteGheRc.test.ts => promoteRc.test.ts} (90%) rename plugins/release-manager-as-a-service/src/cards/promoteRc/sideEffects/{promoteGheRc.ts => promoteRc.ts} (95%) diff --git a/plugins/release-manager-as-a-service/README.md b/plugins/release-manager-as-a-service/README.md index ffb39f2c8f..46e45f9ca5 100644 --- a/plugins/release-manager-as-a-service/README.md +++ b/plugins/release-manager-as-a-service/README.md @@ -4,19 +4,19 @@ `RMaaS` enables developers to manage their releases without having to juggle git commands. -Does it bundle and ship your code? **No**. +Does it build and ship your code? **No**. What `RMaaS` does is manage your **[releases](https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/managing-releases-in-a-repository)** on GitHub, building and shipping is entirely up to you as a developer to handle in your CI. -`RMaaS` is build with industry standards in mind and the flow is as follows: +`RMaaS` is built with industry standards in mind and the flow is as follows: -![](./src/cards/info/rmaas-flow.png) +![](./src/cards/info/flow.png) -> **GitHub Enterprise (GHE)**: The source control system where releases reside in a practical sense. Read more about GitHub releases here. Note that this plugin works just as well with a non-enterprise account. +> **GitHub**: The source control system where releases reside in a practical sense. Read more about GitHub releases [here](https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/. Note that this plugin works just as well with GitHub Enterprise) > -> **Release Candidate (RC)**: A GHE pre-release intended primarily for internal testing +> **Release Candidate (RC)**: A GitHub pre-release intended primarily for internal testing > -> **Release Version**: A GHE release intended for end users +> **Release Version**: A GitHub release intended for end users Looking at the flow above, a common release lifecycle could be: diff --git a/plugins/release-manager-as-a-service/src/ReleaseManagerAsAService.tsx b/plugins/release-manager-as-a-service/src/ReleaseManagerAsAService.tsx index 35fd88b03f..b79c492bbb 100644 --- a/plugins/release-manager-as-a-service/src/ReleaseManagerAsAService.tsx +++ b/plugins/release-manager-as-a-service/src/ReleaseManagerAsAService.tsx @@ -85,7 +85,7 @@ export function ReleaseManagerAsAService({ return (
- +
diff --git a/plugins/release-manager-as-a-service/src/api/RMaaSApiClient.ts b/plugins/release-manager-as-a-service/src/api/RMaaSApiClient.ts index e67d50e46a..fb4ad0daa7 100644 --- a/plugins/release-manager-as-a-service/src/api/RMaaSApiClient.ts +++ b/plugins/release-manager-as-a-service/src/api/RMaaSApiClient.ts @@ -28,7 +28,7 @@ import { GhUpdateReleaseResponse, } from '../types/types'; import { CalverTagParts } from '../helpers/tagParts/getCalverTagParts'; -import { getRcGheInfo } from '../cards/createRc/getRcGheInfo'; +import { getRcGitHubInfo } from '../cards/createRc/getRcGitHubInfo'; import { PluginApiClientConfig } from './PluginApiClientConfig'; import { SemverTagParts } from '../helpers/tagParts/getSemverTagParts'; @@ -165,10 +165,10 @@ export class RMaaSApiClient { }, createRelease: async ({ - nextGheInfo, + nextGitHubInfo, releaseBody, }: { - nextGheInfo: ReturnType; + nextGitHubInfo: ReturnType; releaseBody: string; }) => { const { octokit } = await this.pluginApiClient.getOctokit(); @@ -177,9 +177,9 @@ export class RMaaSApiClient { await octokit.request(`${this.githubCommonPath}/releases`, { method: 'POST', data: { - tag_name: nextGheInfo.rcReleaseTag, - name: nextGheInfo.releaseName, - target_commitish: nextGheInfo.rcBranch, + tag_name: nextGitHubInfo.rcReleaseTag, + name: nextGitHubInfo.releaseName, + target_commitish: nextGitHubInfo.rcBranch, body: releaseBody, prerelease: true, }, diff --git a/plugins/release-manager-as-a-service/src/cards/createRc/CreateRc.test.tsx b/plugins/release-manager-as-a-service/src/cards/createRc/CreateRc.test.tsx index d4dba6841e..07582351e8 100644 --- a/plugins/release-manager-as-a-service/src/cards/createRc/CreateRc.test.tsx +++ b/plugins/release-manager-as-a-service/src/cards/createRc/CreateRc.test.tsx @@ -18,7 +18,7 @@ import { render } from '@testing-library/react'; import { mockCalverProject, - mockNextGheInfo, + mockNextGitHubInfo, mockRcRelease, mockReleaseBranch, mockReleaseVersion, @@ -30,8 +30,8 @@ import { TEST_IDS } from '../../test-helpers/test-ids'; jest.mock('../../components/ProjectContext', () => ({ useApiClientContext: () => mockApiClient, })); -jest.mock('./getRcGheInfo', () => ({ - getRcGheInfo: () => mockNextGheInfo, +jest.mock('./getRcGitHubInfo', () => ({ + getRcGitHubInfo: () => mockNextGitHubInfo, })); import { CreateRc } from './CreateRc'; diff --git a/plugins/release-manager-as-a-service/src/cards/createRc/CreateRc.tsx b/plugins/release-manager-as-a-service/src/cards/createRc/CreateRc.tsx index 22877ec1a3..8c4a5bff87 100644 --- a/plugins/release-manager-as-a-service/src/cards/createRc/CreateRc.tsx +++ b/plugins/release-manager-as-a-service/src/cards/createRc/CreateRc.tsx @@ -25,9 +25,9 @@ import { } from '@material-ui/core'; import { useAsyncFn } from 'react-use'; -import { createGheRc } from './sideEffects/createGheRc'; +import { createRc } from './sideEffects/createRc'; import { Differ } from '../../components/Differ'; -import { getRcGheInfo } from './getRcGheInfo'; +import { getRcGitHubInfo } from './getRcGitHubInfo'; import { InfoCardPlus } from '../../components/InfoCardPlus'; import { ComponentConfigCreateRc, @@ -66,37 +66,37 @@ export const CreateRc = ({ const [semverBumpLevel, setSemverBumpLevel] = useState<'major' | 'minor'>( SEMVER_PARTS.minor, ); - const [nextGheInfo, setNextGheInfo] = useState( - getRcGheInfo({ latestRelease, project, semverBumpLevel }), + const [nextGitHubInfo, setNextGitHubInfo] = useState( + getRcGitHubInfo({ latestRelease, project, semverBumpLevel }), ); useEffect(() => { - setNextGheInfo(getRcGheInfo({ latestRelease, project, semverBumpLevel })); - }, [semverBumpLevel, setNextGheInfo, latestRelease, project]); + setNextGitHubInfo( + getRcGitHubInfo({ latestRelease, project, semverBumpLevel }), + ); + }, [semverBumpLevel, setNextGitHubInfo, latestRelease, project]); - const [createReleaseResponse, callCreateGheRc] = useAsyncFn( - async (...args) => { - const createGheRcResponseSteps = await createGheRc({ + const [createGitHubReleaseResponse, createGitHubReleaseFn] = useAsyncFn( + (...args) => + createRc({ apiClient, defaultBranch, latestRelease, - nextGheInfo: args[0], + nextGitHubInfo: args[0], successCb, - }); - - return createGheRcResponseSteps; - }, + }), ); - - if (createReleaseResponse.error) { + if (createGitHubReleaseResponse.error) { return ( - {createReleaseResponse.error.message} + + {createGitHubReleaseResponse.error.message} + ); } const tagAlreadyExists = latestRelease !== null && - latestRelease.tag_name === nextGheInfo.rcReleaseTag; + latestRelease.tag_name === nextGitHubInfo.rcReleaseTag; const conflictingPreRelease = latestRelease !== null && latestRelease.prerelease; @@ -113,7 +113,7 @@ export const CreateRc = ({ return ( There's already a tag named{' '} - {nextGheInfo.rcReleaseTag} + {nextGitHubInfo.rcReleaseTag} ); } @@ -124,7 +124,7 @@ export const CreateRc = ({ @@ -132,7 +132,7 @@ export const CreateRc = ({ @@ -140,11 +140,14 @@ export const CreateRc = ({ } function CTA() { - if (createReleaseResponse.loading || createReleaseResponse.value) { + if ( + createGitHubReleaseResponse.loading || + createGitHubReleaseResponse.value + ) { return ( @@ -157,7 +160,7 @@ export const CreateRc = ({ disabled={conflictingPreRelease || tagAlreadyExists} variant="contained" color="primary" - onClick={() => callCreateGheRc(nextGheInfo)} + onClick={() => createGitHubReleaseFn(nextGitHubInfo)} > Create RC diff --git a/plugins/release-manager-as-a-service/src/cards/createRc/getRcGheInfo.test.ts b/plugins/release-manager-as-a-service/src/cards/createRc/getRcGitHubInfo.test.ts similarity index 84% rename from plugins/release-manager-as-a-service/src/cards/createRc/getRcGheInfo.test.ts rename to plugins/release-manager-as-a-service/src/cards/createRc/getRcGitHubInfo.test.ts index 0feb95734f..1ed3027f14 100644 --- a/plugins/release-manager-as-a-service/src/cards/createRc/getRcGheInfo.test.ts +++ b/plugins/release-manager-as-a-service/src/cards/createRc/getRcGitHubInfo.test.ts @@ -20,19 +20,19 @@ import { mockSemverProject, mockCalverProject, } from '../../test-helpers/test-helpers'; -import { getRcGheInfo } from './getRcGheInfo'; +import { getRcGitHubInfo } from './getRcGitHubInfo'; const injectedDate = format(1611869955783, 'yyyy.MM.dd'); -describe('getRCGheInfo', () => { +describe('getRcGitHubInfo', () => { describe('calver', () => { const latestRelease = { tag_name: 'rc-2020.01.01_0', } as GhGetReleaseResponse; - it('should return correct Ghe info', () => { + it('should return correct GitHub info', () => { expect( - getRcGheInfo({ + getRcGitHubInfo({ project: mockCalverProject, latestRelease, semverBumpLevel: 'minor', @@ -53,9 +53,9 @@ describe('getRCGheInfo', () => { tag_name: 'rc-1.1.1', } as GhGetReleaseResponse; - it("should return correct Ghe info when there's previous releases", () => { + it("should return correct GitHub info when there's previous releases", () => { expect( - getRcGheInfo({ + getRcGitHubInfo({ project: mockSemverProject, latestRelease, semverBumpLevel: 'minor', @@ -69,9 +69,9 @@ describe('getRCGheInfo', () => { `); }); - it("should return correct Ghe info when there's no previous release", () => { + it("should return correct GitHub info when there's no previous release", () => { expect( - getRcGheInfo({ + getRcGitHubInfo({ project: mockSemverProject, latestRelease: null, semverBumpLevel: 'minor', diff --git a/plugins/release-manager-as-a-service/src/cards/createRc/getRcGheInfo.ts b/plugins/release-manager-as-a-service/src/cards/createRc/getRcGitHubInfo.ts similarity index 98% rename from plugins/release-manager-as-a-service/src/cards/createRc/getRcGheInfo.ts rename to plugins/release-manager-as-a-service/src/cards/createRc/getRcGitHubInfo.ts index 6054117d9c..a35b46480e 100644 --- a/plugins/release-manager-as-a-service/src/cards/createRc/getRcGheInfo.ts +++ b/plugins/release-manager-as-a-service/src/cards/createRc/getRcGitHubInfo.ts @@ -20,7 +20,7 @@ import { getSemverTagParts } from '../../helpers/tagParts/getSemverTagParts'; import { Project, GhGetReleaseResponse } from '../../types/types'; import { SEMVER_PARTS } from '../../constants/constants'; -export const getRcGheInfo = ({ +export const getRcGitHubInfo = ({ project, latestRelease, semverBumpLevel, diff --git a/plugins/release-manager-as-a-service/src/cards/createRc/sideEffects/createGheRc.test.ts b/plugins/release-manager-as-a-service/src/cards/createRc/sideEffects/createRc.test.ts similarity index 92% rename from plugins/release-manager-as-a-service/src/cards/createRc/sideEffects/createGheRc.test.ts rename to plugins/release-manager-as-a-service/src/cards/createRc/sideEffects/createRc.test.ts index a94cd682ee..26450ac162 100644 --- a/plugins/release-manager-as-a-service/src/cards/createRc/sideEffects/createGheRc.test.ts +++ b/plugins/release-manager-as-a-service/src/cards/createRc/sideEffects/createRc.test.ts @@ -14,22 +14,22 @@ * limitations under the License. */ import { - mockDefaultBranch, - mockReleaseVersion, - mockNextGheInfo, mockApiClient, + mockDefaultBranch, + mockNextGitHubInfo, + mockReleaseVersion, } from '../../../test-helpers/test-helpers'; -import { createGheRc } from './createGheRc'; +import { createRc } from './createRc'; describe('createGheRc', () => { beforeEach(jest.clearAllMocks); it('should work', async () => { - const result = await createGheRc({ + const result = await createRc({ apiClient: mockApiClient, defaultBranch: mockDefaultBranch, latestRelease: mockReleaseVersion, - nextGheInfo: mockNextGheInfo, + nextGitHubInfo: mockNextGitHubInfo, }); expect(result).toMatchInlineSnapshot(` diff --git a/plugins/release-manager-as-a-service/src/cards/createRc/sideEffects/createGheRc.ts b/plugins/release-manager-as-a-service/src/cards/createRc/sideEffects/createRc.ts similarity index 85% rename from plugins/release-manager-as-a-service/src/cards/createRc/sideEffects/createGheRc.ts rename to plugins/release-manager-as-a-service/src/cards/createRc/sideEffects/createRc.ts index 29491fb13e..2b5b71ee24 100644 --- a/plugins/release-manager-as-a-service/src/cards/createRc/sideEffects/createGheRc.ts +++ b/plugins/release-manager-as-a-service/src/cards/createRc/sideEffects/createRc.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { getRcGheInfo } from '../getRcGheInfo'; +import { getRcGitHubInfo } from '../getRcGitHubInfo'; import { ComponentConfigCreateRc, GhCreateReferenceResponse, @@ -24,21 +24,21 @@ import { import { RMaaSApiClient } from '../../../api/RMaaSApiClient'; import { ReleaseManagerAsAServiceError } from '../../../errors/ReleaseManagerAsAServiceError'; -interface CreateGheRC { +interface CreateRC { apiClient: RMaaSApiClient; defaultBranch: GhGetRepositoryResponse['default_branch']; latestRelease: GhGetReleaseResponse | null; - nextGheInfo: ReturnType; + nextGitHubInfo: ReturnType; successCb?: ComponentConfigCreateRc['successCb']; } -export async function createGheRc({ +export async function createRc({ apiClient, defaultBranch, latestRelease, - nextGheInfo, + nextGitHubInfo, successCb, -}: CreateGheRC) { +}: CreateRC) { const responseSteps: ResponseStep[] = []; /** @@ -62,13 +62,13 @@ export async function createGheRc({ createdRef = ( await apiClient.createRc.createRef({ mostRecentSha, - targetBranch: nextGheInfo.rcBranch, + targetBranch: nextGitHubInfo.rcBranch, }) ).createdRef; } catch (error) { if (error.body.message === 'Reference already exists') { throw new ReleaseManagerAsAServiceError( - `Branch "${nextGheInfo.rcBranch}" already exists: .../tree/${nextGheInfo.rcBranch}`, + `Branch "${nextGitHubInfo.rcBranch}" already exists: .../tree/${nextGitHubInfo.rcBranch}`, ); } throw error; @@ -84,7 +84,7 @@ export async function createGheRc({ const previousReleaseBranch = latestRelease ? latestRelease.target_commitish : defaultBranch; - const nextReleaseBranch = nextGheInfo.rcBranch; + const nextReleaseBranch = nextGitHubInfo.rcBranch; const { comparison } = await apiClient.createRc.getComparison({ previousReleaseBranch, nextReleaseBranch, @@ -105,15 +105,15 @@ export async function createGheRc({ }); /** - * 4. Creates the release itself in GHE + * 4. Creates the release itself in GitHub */ const { createReleaseResponse } = await apiClient.createRc.createRelease({ - nextGheInfo, + nextGitHubInfo: nextGitHubInfo, releaseBody, }); responseSteps.push({ message: `Created Release Candidate "${createReleaseResponse.name}"`, - secondaryMessage: `with tag "${nextGheInfo.rcReleaseTag}"`, + secondaryMessage: `with tag "${nextGitHubInfo.rcReleaseTag}"`, link: createReleaseResponse.html_url, }); diff --git a/plugins/release-manager-as-a-service/src/cards/info/Info.tsx b/plugins/release-manager-as-a-service/src/cards/info/Info.tsx index 3d27e24297..76d821b0a5 100644 --- a/plugins/release-manager-as-a-service/src/cards/info/Info.tsx +++ b/plugins/release-manager-as-a-service/src/cards/info/Info.tsx @@ -25,7 +25,7 @@ import { } from '../../types/types'; import { useStyles } from '../../styles/styles'; import { TEST_IDS } from '../../test-helpers/test-ids'; -import rmaasFlowImage from './rmaas-flow.png'; +import flowImage from './flow.png'; interface InfoCardProps { releaseBranch: GhGetBranchResponse | null; @@ -46,26 +46,26 @@ export const Info = ({ Terminology - GitHub Enterprise (GHE): The source control system - where releases reside in a practical sense. Read more about GitHub - releases{' '} + GitHub: The source control system where releases + reside in a practical sense. Read more about GitHub releases{' '} here - . Note that this plugin works just as well with a non-enterprise - account. + . Note that this plugin works just as well with GitHub Enterprise + (GHE) - Release Candidate: A GHE pre-release intended - primarily for internal testing + Release Candidate: A GitHub prerelease{' '} + intended primarily for internal testing - Release Version: A GHE release intended for end users + Release Version: A GitHub release intended for end + users @@ -82,7 +82,7 @@ export const Info = ({ Here's an overview of the flow: - rmaas-flow + flow
@@ -91,7 +91,7 @@ export const Info = ({ Repository:{' '} diff --git a/plugins/release-manager-as-a-service/src/cards/info/rmaas-flow.png b/plugins/release-manager-as-a-service/src/cards/info/flow.png similarity index 100% rename from plugins/release-manager-as-a-service/src/cards/info/rmaas-flow.png rename to plugins/release-manager-as-a-service/src/cards/info/flow.png diff --git a/plugins/release-manager-as-a-service/src/cards/patchRc/PatchBody.tsx b/plugins/release-manager-as-a-service/src/cards/patchRc/PatchBody.tsx index a30515c9b6..6d15bc6a08 100644 --- a/plugins/release-manager-as-a-service/src/cards/patchRc/PatchBody.tsx +++ b/plugins/release-manager-as-a-service/src/cards/patchRc/PatchBody.tsx @@ -69,7 +69,7 @@ export const PatchBody = ({ const apiClient = useApiClientContext(); const [checkedCommitIndex, setCheckedCommitIndex] = useState(-1); - const gheDataResponse = useAsync(async () => { + const githubDataResponse = useAsync(async () => { const [ { branch: releaseBranchResponse }, { recentCommits }, @@ -91,7 +91,7 @@ export const PatchBody = ({ }; }); - const [patchGheRcResponse, patchGheRcFn] = useAsyncFn(async (...args) => { + const [patchReleaseResponse, patchReleaseFn] = useAsyncFn(async (...args) => { const selectedPatchCommit: GhGetCommitResponse = args[0]; const patchResponseSteps = await patch({ apiClient, @@ -105,17 +105,17 @@ export const PatchBody = ({ return patchResponseSteps; }); - if (gheDataResponse.error) { + if (githubDataResponse.error) { return ( - {gheDataResponse.error.message} + {githubDataResponse.error.message} ); } - if (patchGheRcResponse.error) { - return {patchGheRcResponse.error.message}; + if (patchReleaseResponse.error) { + return {patchReleaseResponse.error.message}; } - if (gheDataResponse.loading) { + if (githubDataResponse.loading) { return ; } @@ -131,7 +131,7 @@ export const PatchBody = ({ severity="info" > - The current GHE release is a Release Version + The current GitHub release is a Release Version It's still possible to patch it, but be extra mindful of changes @@ -145,14 +145,14 @@ export const PatchBody = ({ } function CommitList() { - if (!gheDataResponse.value?.recentCommits) { + if (!githubDataResponse.value?.recentCommits) { return null; } return ( - {gheDataResponse.value.recentCommits.map((commit, index) => { - const commitExistsOnReleaseBranch = !!gheDataResponse.value?.recentReleaseBranchCommits.find( + {githubDataResponse.value.recentCommits.map((commit, index) => { + const commitExistsOnReleaseBranch = !!githubDataResponse.value?.recentReleaseBranchCommits.find( ({ sha }) => { return sha === commit.sha; }, @@ -185,9 +185,9 @@ export const PatchBody = ({ 0) || + patchReleaseResponse.loading || + (patchReleaseResponse.value && + patchReleaseResponse.value.length > 0) || commitExistsOnReleaseBranch } role={undefined} @@ -252,11 +252,11 @@ export const PatchBody = ({ } function CTA() { - if (patchGheRcResponse.loading || patchGheRcResponse.value) { + if (patchReleaseResponse.loading || patchReleaseResponse.value) { return ( Patch Release Candidate @@ -279,8 +279,8 @@ export const PatchBody = ({ color="primary" onClick={() => { // FIXME: Optional chaining shouldn't be needed here due to the if-statement above - patchGheRcFn( - gheDataResponse.value?.recentCommits[checkedCommitIndex], + patchReleaseFn( + githubDataResponse.value?.recentCommits[checkedCommitIndex], ); }} > diff --git a/plugins/release-manager-as-a-service/src/cards/promoteRc/PromoteRc.tsx b/plugins/release-manager-as-a-service/src/cards/promoteRc/PromoteRc.tsx index a591f71cd1..e7583e7085 100644 --- a/plugins/release-manager-as-a-service/src/cards/promoteRc/PromoteRc.tsx +++ b/plugins/release-manager-as-a-service/src/cards/promoteRc/PromoteRc.tsx @@ -53,7 +53,9 @@ export const PromoteRc = ({ className={classes.paragraph} severity="warning" > - Latest GHE release is not a Release Candidate + + Latest GitHub release is not a Release Candidate + One can only promote Release Candidates to Release Versions ); diff --git a/plugins/release-manager-as-a-service/src/cards/promoteRc/PromoteRcBody.tsx b/plugins/release-manager-as-a-service/src/cards/promoteRc/PromoteRcBody.tsx index 31eb883dee..99ca136fac 100644 --- a/plugins/release-manager-as-a-service/src/cards/promoteRc/PromoteRcBody.tsx +++ b/plugins/release-manager-as-a-service/src/cards/promoteRc/PromoteRcBody.tsx @@ -24,7 +24,7 @@ import { GhGetReleaseResponse, SetRefetch, } from '../../types/types'; -import { promoteGheRc } from './sideEffects/promoteGheRc'; +import { promoteRc } from './sideEffects/promoteRc'; import { ResponseStepList } from '../../components/ResponseStepList/ResponseStepList'; import { useApiClientContext } from '../../components/ProjectContext'; import { useStyles } from '../../styles/styles'; @@ -44,12 +44,14 @@ export const PromoteRcBody = ({ const apiClient = useApiClientContext(); const classes = useStyles(); const releaseVersion = rcRelease.tag_name.replace('rc-', 'version-'); - const [promoteGheRcResponse, promoseGheRcFn] = useAsyncFn( - promoteGheRc({ apiClient, rcRelease, releaseVersion, successCb }), + const [promoteGitHubRcResponse, promoseGitHubRcFn] = useAsyncFn( + promoteRc({ apiClient, rcRelease, releaseVersion, successCb }), ); - if (promoteGheRcResponse.error) { - return {promoteGheRcResponse.error.message}; + if (promoteGitHubRcResponse.error) { + return ( + {promoteGitHubRcResponse.error.message} + ); } function Description() { @@ -67,13 +69,13 @@ export const PromoteRcBody = ({ } function CTA() { - if (promoteGheRcResponse.loading || promoteGheRcResponse.value) { + if (promoteGitHubRcResponse.loading || promoteGitHubRcResponse.value) { return ( ); } @@ -83,7 +85,7 @@ export const PromoteRcBody = ({ data-testid={TEST_IDS.promoteRc.cta} variant="contained" color="primary" - onClick={() => promoseGheRcFn()} + onClick={() => promoseGitHubRcFn()} > Promote Release Candidate diff --git a/plugins/release-manager-as-a-service/src/cards/promoteRc/sideEffects/promoteGheRc.test.ts b/plugins/release-manager-as-a-service/src/cards/promoteRc/sideEffects/promoteRc.test.ts similarity index 90% rename from plugins/release-manager-as-a-service/src/cards/promoteRc/sideEffects/promoteGheRc.test.ts rename to plugins/release-manager-as-a-service/src/cards/promoteRc/sideEffects/promoteRc.test.ts index d6df61a1f3..a64cf70f81 100644 --- a/plugins/release-manager-as-a-service/src/cards/promoteRc/sideEffects/promoteGheRc.test.ts +++ b/plugins/release-manager-as-a-service/src/cards/promoteRc/sideEffects/promoteRc.test.ts @@ -17,13 +17,13 @@ import { mockRcRelease, mockApiClient, } from '../../../test-helpers/test-helpers'; -import { promoteGheRc } from './promoteGheRc'; +import { promoteRc } from './promoteRc'; -describe('promoteGheRc', () => { +describe('promoteRc', () => { beforeEach(jest.clearAllMocks); it('should work', async () => { - const result = await promoteGheRc({ + const result = await promoteRc({ apiClient: mockApiClient, rcRelease: mockRcRelease, releaseVersion: 'version-1.2.3', diff --git a/plugins/release-manager-as-a-service/src/cards/promoteRc/sideEffects/promoteGheRc.ts b/plugins/release-manager-as-a-service/src/cards/promoteRc/sideEffects/promoteRc.ts similarity index 95% rename from plugins/release-manager-as-a-service/src/cards/promoteRc/sideEffects/promoteGheRc.ts rename to plugins/release-manager-as-a-service/src/cards/promoteRc/sideEffects/promoteRc.ts index 91371b72a6..d473951a91 100644 --- a/plugins/release-manager-as-a-service/src/cards/promoteRc/sideEffects/promoteGheRc.ts +++ b/plugins/release-manager-as-a-service/src/cards/promoteRc/sideEffects/promoteRc.ts @@ -20,19 +20,19 @@ import { } from '../../../types/types'; import { RMaaSApiClient } from '../../../api/RMaaSApiClient'; -interface PromoteGheRc { +interface PromoteRc { apiClient: RMaaSApiClient; rcRelease: GhGetReleaseResponse; releaseVersion: string; successCb?: ComponentConfigPromoteRc['successCb']; } -export function promoteGheRc({ +export function promoteRc({ apiClient, rcRelease, releaseVersion, successCb, -}: PromoteGheRc) { +}: PromoteRc) { return async (): Promise => { const responseSteps: ResponseStep[] = []; diff --git a/plugins/release-manager-as-a-service/src/components/Differ.tsx b/plugins/release-manager-as-a-service/src/components/Differ.tsx index 7af998792b..02bf60ce46 100644 --- a/plugins/release-manager-as-a-service/src/components/Differ.tsx +++ b/plugins/release-manager-as-a-service/src/components/Differ.tsx @@ -25,7 +25,7 @@ interface DifferProps { next?: string | ReactNode; prev?: string; prefix?: string; - icon?: 'tag' | 'branch' | 'ghe' | 'slack' | 'versioning'; + icon?: 'tag' | 'branch' | 'github' | 'slack' | 'versioning'; } const Icon = ({ icon }: { icon: DifferProps['icon'] }) => { @@ -40,7 +40,7 @@ const Icon = ({ icon }: { icon: DifferProps['icon'] }) => { ); - case 'ghe': + case 'github': return ( ); diff --git a/plugins/release-manager-as-a-service/src/components/NoLatestRelease.tsx b/plugins/release-manager-as-a-service/src/components/NoLatestRelease.tsx index 85909d526d..f09049f3cf 100644 --- a/plugins/release-manager-as-a-service/src/components/NoLatestRelease.tsx +++ b/plugins/release-manager-as-a-service/src/components/NoLatestRelease.tsx @@ -28,7 +28,7 @@ export const NoLatestRelease = () => { className={classes.paragraph} severity="warning" > - Unable to find any GHE releases + Unable to find any GitHub release ); }; diff --git a/plugins/release-manager-as-a-service/src/test-helpers/test-helpers.test.ts b/plugins/release-manager-as-a-service/src/test-helpers/test-helpers.test.ts index e72eba4df4..78c48eb0a7 100644 --- a/plugins/release-manager-as-a-service/src/test-helpers/test-helpers.test.ts +++ b/plugins/release-manager-as-a-service/src/test-helpers/test-helpers.test.ts @@ -73,7 +73,7 @@ describe('testHelpers', () => { "versioningStrategy": "calver", }, "mockDefaultBranch": "mock_defaultBranch", - "mockNextGheInfo": Object { + "mockNextGitHubInfo": Object { "rcBranch": "rc/1.2.3", "rcReleaseTag": "rc-1.2.3", "releaseName": "Version 1.2.3", diff --git a/plugins/release-manager-as-a-service/src/test-helpers/test-helpers.ts b/plugins/release-manager-as-a-service/src/test-helpers/test-helpers.ts index b994e591d2..61d8cb3b17 100644 --- a/plugins/release-manager-as-a-service/src/test-helpers/test-helpers.ts +++ b/plugins/release-manager-as-a-service/src/test-helpers/test-helpers.ts @@ -14,7 +14,7 @@ * limitations under the License. */ import { CalverTagParts } from '../helpers/tagParts/getCalverTagParts'; -import { getRcGheInfo } from '../cards/createRc/getRcGheInfo'; +import { getRcGitHubInfo } from '../cards/createRc/getRcGitHubInfo'; import { GhCompareCommitsResponse, GhCreateCommitResponse, @@ -50,7 +50,7 @@ export const mockCalverProject: Project = { export const mockDefaultBranch = 'mock_defaultBranch'; -export const mockNextGheInfo: ReturnType = { +export const mockNextGitHubInfo: ReturnType = { rcBranch: 'rc/1.2.3', rcReleaseTag: 'rc-1.2.3', releaseName: 'Version 1.2.3', From f90918b6c8e6f95464815bb65c17e87425be6b52 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Tue, 13 Apr 2021 10:02:11 +0200 Subject: [PATCH 004/140] Replace date-fns with luxon Signed-off-by: Erik Engervall --- plugins/release-manager-as-a-service/package.json | 2 +- .../src/cards/createRc/getRcGitHubInfo.test.ts | 14 ++++++++++---- .../src/cards/createRc/getRcGitHubInfo.ts | 4 ++-- yarn.lock | 5 ----- 4 files changed, 13 insertions(+), 12 deletions(-) diff --git a/plugins/release-manager-as-a-service/package.json b/plugins/release-manager-as-a-service/package.json index 9a8f1bb7ef..33b0c5279b 100644 --- a/plugins/release-manager-as-a-service/package.json +++ b/plugins/release-manager-as-a-service/package.json @@ -27,7 +27,7 @@ "@material-ui/icons": "^4.9.1", "@material-ui/lab": "4.0.0-alpha.45", "@octokit/rest": "^18.0.12", - "date-fns": "^2.19.0", + "luxon": "^1.26.0", "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", "react-use": "^15.3.3", diff --git a/plugins/release-manager-as-a-service/src/cards/createRc/getRcGitHubInfo.test.ts b/plugins/release-manager-as-a-service/src/cards/createRc/getRcGitHubInfo.test.ts index 1ed3027f14..59676e989b 100644 --- a/plugins/release-manager-as-a-service/src/cards/createRc/getRcGitHubInfo.test.ts +++ b/plugins/release-manager-as-a-service/src/cards/createRc/getRcGitHubInfo.test.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { format } from 'date-fns'; +import { DateTime } from 'luxon'; import { GhGetReleaseResponse } from '../../types/types'; import { @@ -22,9 +22,15 @@ import { } from '../../test-helpers/test-helpers'; import { getRcGitHubInfo } from './getRcGitHubInfo'; -const injectedDate = format(1611869955783, 'yyyy.MM.dd'); - describe('getRcGitHubInfo', () => { + describe('DateTime', () => { + it('should format dates as expected', () => { + const formattedDate = DateTime.now().toFormat('yyyy.MM.dd'); + + expect(formattedDate).toMatch(/^\d{4}.\d{2}.\d{2}$/); + }); + }); + describe('calver', () => { const latestRelease = { tag_name: 'rc-2020.01.01_0', @@ -36,7 +42,7 @@ describe('getRcGitHubInfo', () => { project: mockCalverProject, latestRelease, semverBumpLevel: 'minor', - injectedDate, + injectedDate: '2021.01.28', }), ).toMatchInlineSnapshot(` Object { diff --git a/plugins/release-manager-as-a-service/src/cards/createRc/getRcGitHubInfo.ts b/plugins/release-manager-as-a-service/src/cards/createRc/getRcGitHubInfo.ts index a35b46480e..df58f73f11 100644 --- a/plugins/release-manager-as-a-service/src/cards/createRc/getRcGitHubInfo.ts +++ b/plugins/release-manager-as-a-service/src/cards/createRc/getRcGitHubInfo.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { format } from 'date-fns'; +import { DateTime } from 'luxon'; import { getBumpedSemverTagParts } from '../../helpers/getBumpedTag'; import { getSemverTagParts } from '../../helpers/tagParts/getSemverTagParts'; @@ -24,7 +24,7 @@ export const getRcGitHubInfo = ({ project, latestRelease, semverBumpLevel, - injectedDate = format(new Date(), 'yyyy.MM.dd'), // '0012-01-01T13:37:00.000Z' + injectedDate = DateTime.now().toFormat('yyyy.MM.dd'), }: { project: Project; latestRelease: GhGetReleaseResponse | null; diff --git a/yarn.lock b/yarn.lock index cca4a0ed44..d341a67170 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11408,11 +11408,6 @@ date-fns@^2.0.0-alpha.27, date-fns@^2.16.1: resolved "https://registry.npmjs.org/date-fns/-/date-fns-2.18.0.tgz#08e50aee300ad0d2c5e054e3f0d10d8f9cdfe09e" integrity sha512-NYyAg4wRmGVU4miKq5ivRACOODdZRY3q5WLmOJSq8djyzftYphU7dTHLcEtLqEvfqMKQ0jVv91P4BAwIjsXIcw== -date-fns@^2.19.0: - version "2.19.0" - resolved "https://artifactory.spotify.net/artifactory/api/npm/virtual-npm/date-fns/-/date-fns-2.19.0.tgz#65193348635a28d5d916c43ec7ce6fbd145059e1" - integrity sha1-ZRkzSGNaKNXZFsQ+x85vvRRQWeE= - dateformat@^3.0.0: version "3.0.3" resolved "https://registry.npmjs.org/dateformat/-/dateformat-3.0.3.tgz#a6e37499a4d9a9cf85ef5872044d62901c9889ae" From 379e568d670b2ae85782bd05b05d1662948d384c Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Tue, 13 Apr 2021 10:18:42 +0200 Subject: [PATCH 005/140] Rename plugin to GitHub Release Manager and remove references to RMaaS Signed-off-by: Erik Engervall --- .../plugins/release-manager-as-a-service.yaml | 8 +-- ...go.svg => github-release-manager-logo.svg} | 0 packages/app/package.json | 2 +- packages/app/src/plugins.ts | 2 +- .../.eslintrc.js | 0 .../README.md | 14 ++--- .../dev/README.md | 6 +- .../dev/index.tsx | 4 +- .../package.json | 2 +- .../src/GitHubReleaseManager.tsx} | 14 ++--- .../src/api/ApiClient.ts} | 4 +- .../src/api/PluginApiClientConfig.ts | 0 .../src/api/serviceApiRef.ts | 11 ++-- .../src/cards/createRc/CreateRc.test.tsx | 0 .../src/cards/createRc/CreateRc.tsx | 0 .../cards/createRc/getRcGitHubInfo.test.ts | 0 .../src/cards/createRc/getRcGitHubInfo.ts | 0 .../createRc/sideEffects/createRc.test.ts | 0 .../cards/createRc/sideEffects/createRc.ts | 4 +- .../src/cards/info/Info.test.tsx | 0 .../src/cards/info/Info.tsx | 6 +- .../src/cards/info/flow.png | Bin 0 -> 78736 bytes .../src/cards/patchRc/Patch.test.tsx | 0 .../src/cards/patchRc/Patch.tsx | 0 .../src/cards/patchRc/PatchBody.test.tsx | 0 .../src/cards/patchRc/PatchBody.tsx | 0 .../cards/patchRc/sideEffects/patch.test.ts | 0 .../src/cards/patchRc/sideEffects/patch.ts | 4 +- .../src/cards/promoteRc/PromoteRc.test.tsx | 0 .../src/cards/promoteRc/PromoteRc.tsx | 0 .../cards/promoteRc/PromoteRcBody.test.tsx | 0 .../src/cards/promoteRc/PromoteRcBody.tsx | 0 .../promoteRc/sideEffects/promoteRc.test.ts | 0 .../cards/promoteRc/sideEffects/promoteRc.ts | 4 +- .../src/components/Differ.tsx | 0 .../src/components/Divider.test.tsx | 0 .../src/components/Divider.tsx | 0 .../src/components/InfoCardPlus.test.tsx | 0 .../src/components/InfoCardPlus.tsx | 0 .../src/components/NoLatestRelease.test.tsx | 0 .../src/components/NoLatestRelease.tsx | 0 .../src/components/ProjectContext.ts | 6 +- .../src/components/ReloadButton.test.tsx | 0 .../src/components/ReloadButton.tsx | 0 .../ResponseStepList.test.tsx | 0 .../ResponseStepList/ResponseStepList.tsx | 0 .../ResponseStepListItem.test.tsx | 0 .../ResponseStepList/ResponseStepListItem.tsx | 0 .../src/constants/constants.test.ts | 0 .../src/constants/constants.ts | 0 .../errors/ReleaseManagerAsAServiceError.ts | 0 .../src/helpers/date.test.ts | 0 .../src/helpers/date.ts | 0 .../src/helpers/getBumpedTag.test.ts | 0 .../src/helpers/getBumpedTag.ts | 0 .../src/helpers/getShortCommitHash.test.ts | 0 .../src/helpers/getShortCommitHash.ts | 0 .../src/helpers/isCalverTagParts.test.ts | 0 .../src/helpers/isCalverTagParts.ts | 0 .../src/helpers/tagParts/getCalverTagParts.ts | 0 .../src/helpers/tagParts/getSemverTagParts.ts | 0 .../src/helpers/tagParts/getTagParts.test.ts | 0 .../src/helpers/tagParts/getTagParts.ts | 0 .../src/index.ts | 2 +- .../src/plugin.test.ts | 6 +- .../src/plugin.ts | 14 ++--- .../src/routes.ts | 4 +- .../src/setupTests.ts | 0 .../src/sideEffects/getGitHubBatchInfo.ts | 4 +- .../src/sideEffects/getLatestRelease.test.ts | 0 .../src/sideEffects/getLatestRelease.ts | 4 +- .../src/styles/styles.ts | 0 .../src/test-helpers/test-helpers.test.ts | 0 .../src/test-helpers/test-helpers.ts | 0 .../src/test-helpers/test-ids.ts | 53 ++++++++++++++++++ .../src/types/types.ts | 2 +- .../src/cards/info/flow.png | Bin 77527 -> 0 bytes .../src/test-helpers/test-ids.ts | 53 ------------------ 78 files changed, 112 insertions(+), 121 deletions(-) rename microsite/static/img/{release-manager-as-a-service-logo.svg => github-release-manager-logo.svg} (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/.eslintrc.js (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/README.md (78%) rename plugins/{release-manager-as-a-service => github-release-manager}/dev/README.md (70%) rename plugins/{release-manager-as-a-service => github-release-manager}/dev/index.tsx (94%) rename plugins/{release-manager-as-a-service => github-release-manager}/package.json (95%) rename plugins/{release-manager-as-a-service/src/ReleaseManagerAsAService.tsx => github-release-manager/src/GitHubReleaseManager.tsx} (92%) rename plugins/{release-manager-as-a-service/src/api/RMaaSApiClient.ts => github-release-manager/src/api/ApiClient.ts} (98%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/api/PluginApiClientConfig.ts (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/api/serviceApiRef.ts (74%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/cards/createRc/CreateRc.test.tsx (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/cards/createRc/CreateRc.tsx (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/cards/createRc/getRcGitHubInfo.test.ts (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/cards/createRc/getRcGitHubInfo.ts (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/cards/createRc/sideEffects/createRc.test.ts (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/cards/createRc/sideEffects/createRc.ts (97%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/cards/info/Info.test.tsx (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/cards/info/Info.tsx (95%) create mode 100644 plugins/github-release-manager/src/cards/info/flow.png rename plugins/{release-manager-as-a-service => github-release-manager}/src/cards/patchRc/Patch.test.tsx (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/cards/patchRc/Patch.tsx (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/cards/patchRc/PatchBody.test.tsx (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/cards/patchRc/PatchBody.tsx (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/cards/patchRc/sideEffects/patch.test.ts (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/cards/patchRc/sideEffects/patch.ts (98%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/cards/promoteRc/PromoteRc.test.tsx (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/cards/promoteRc/PromoteRc.tsx (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/cards/promoteRc/PromoteRcBody.test.tsx (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/cards/promoteRc/PromoteRcBody.tsx (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/cards/promoteRc/sideEffects/promoteRc.test.ts (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/cards/promoteRc/sideEffects/promoteRc.ts (94%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/components/Differ.tsx (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/components/Divider.test.tsx (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/components/Divider.tsx (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/components/InfoCardPlus.test.tsx (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/components/InfoCardPlus.tsx (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/components/NoLatestRelease.test.tsx (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/components/NoLatestRelease.tsx (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/components/ProjectContext.ts (86%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/components/ReloadButton.test.tsx (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/components/ReloadButton.tsx (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/components/ResponseStepList/ResponseStepList.test.tsx (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/components/ResponseStepList/ResponseStepList.tsx (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/components/ResponseStepList/ResponseStepListItem.test.tsx (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/components/ResponseStepList/ResponseStepListItem.tsx (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/constants/constants.test.ts (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/constants/constants.ts (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/errors/ReleaseManagerAsAServiceError.ts (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/helpers/date.test.ts (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/helpers/date.ts (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/helpers/getBumpedTag.test.ts (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/helpers/getBumpedTag.ts (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/helpers/getShortCommitHash.test.ts (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/helpers/getShortCommitHash.ts (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/helpers/isCalverTagParts.test.ts (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/helpers/isCalverTagParts.ts (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/helpers/tagParts/getCalverTagParts.ts (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/helpers/tagParts/getSemverTagParts.ts (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/helpers/tagParts/getTagParts.test.ts (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/helpers/tagParts/getTagParts.ts (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/index.ts (91%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/plugin.test.ts (79%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/plugin.ts (75%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/routes.ts (87%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/setupTests.ts (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/sideEffects/getGitHubBatchInfo.ts (93%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/sideEffects/getLatestRelease.test.ts (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/sideEffects/getLatestRelease.ts (91%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/styles/styles.ts (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/test-helpers/test-helpers.test.ts (100%) rename plugins/{release-manager-as-a-service => github-release-manager}/src/test-helpers/test-helpers.ts (100%) create mode 100644 plugins/github-release-manager/src/test-helpers/test-ids.ts rename plugins/{release-manager-as-a-service => github-release-manager}/src/types/types.ts (99%) delete mode 100644 plugins/release-manager-as-a-service/src/cards/info/flow.png delete mode 100644 plugins/release-manager-as-a-service/src/test-helpers/test-ids.ts diff --git a/microsite/data/plugins/release-manager-as-a-service.yaml b/microsite/data/plugins/release-manager-as-a-service.yaml index 08138f6d3b..9c930b2bbb 100644 --- a/microsite/data/plugins/release-manager-as-a-service.yaml +++ b/microsite/data/plugins/release-manager-as-a-service.yaml @@ -1,9 +1,9 @@ --- -title: Release Manager as a Service +title: GitHub Release Manager author: '@erikengervall' authorUrl: https://github.com/erikengervall category: Release management description: Manage releases without having to juggle git commands -documentation: https://github.com/backstage/backstage/tree/master/plugins/release-manager-as-a-service -iconUrl: img/release-manager-as-a-service.svg -npmPackageName: '@backstage/plugin-release-manager-as-a-service' +documentation: https://github.com/backstage/backstage/tree/master/plugins/github-release-manager +iconUrl: img/github-release-manager-logo.svg +npmPackageName: '@backstage/plugin-github-release-manager' diff --git a/microsite/static/img/release-manager-as-a-service-logo.svg b/microsite/static/img/github-release-manager-logo.svg similarity index 100% rename from microsite/static/img/release-manager-as-a-service-logo.svg rename to microsite/static/img/github-release-manager-logo.svg diff --git a/packages/app/package.json b/packages/app/package.json index 26d5c36ab6..4c734b85f3 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -30,7 +30,7 @@ "@backstage/plugin-org": "^0.3.10", "@backstage/plugin-pagerduty": "0.3.2", "@backstage/plugin-register-component": "^0.2.12", - "@backstage/plugin-release-manager-as-a-service": "^0.1.1", + "@backstage/plugin-github-release-manager": "^0.1.1", "@backstage/plugin-rollbar": "^0.3.3", "@backstage/plugin-scaffolder": "^0.8.0", "@backstage/plugin-search": "^0.3.4", diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index bedd533e69..744456bfaa 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -46,4 +46,4 @@ export { plugin as Kafka } from '@backstage/plugin-kafka'; export { todoPlugin } from '@backstage/plugin-todo'; export { badgesPlugin } from '@backstage/plugin-badges'; export { githubDeploymentsPlugin } from '@backstage/plugin-github-deployments'; -export { releaseManagerAsAServicePlugin } from '@backstage/plugin-release-manager-as-a-service'; +export { releaseManagerAsAServicePlugin } from '@backstage/plugin-github-release-manager'; diff --git a/plugins/release-manager-as-a-service/.eslintrc.js b/plugins/github-release-manager/.eslintrc.js similarity index 100% rename from plugins/release-manager-as-a-service/.eslintrc.js rename to plugins/github-release-manager/.eslintrc.js diff --git a/plugins/release-manager-as-a-service/README.md b/plugins/github-release-manager/README.md similarity index 78% rename from plugins/release-manager-as-a-service/README.md rename to plugins/github-release-manager/README.md index 46e45f9ca5..3411c51000 100644 --- a/plugins/release-manager-as-a-service/README.md +++ b/plugins/github-release-manager/README.md @@ -1,14 +1,14 @@ -# Release Manager as a Service (RMaaS) +# GitHub Release Manager (GRM) ## Overview -`RMaaS` enables developers to manage their releases without having to juggle git commands. +`GRM` enables developers to manage their releases without having to juggle git commands. Does it build and ship your code? **No**. -What `RMaaS` does is manage your **[releases](https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/managing-releases-in-a-repository)** on GitHub, building and shipping is entirely up to you as a developer to handle in your CI. +What `GRM` does is manage your **[releases](https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/managing-releases-in-a-repository)** on GitHub, building and shipping is entirely up to you as a developer to handle in your CI. -`RMaaS` is built with industry standards in mind and the flow is as follows: +`GRM` is built with industry standards in mind and the flow is as follows: ![](./src/cards/info/flow.png) @@ -21,7 +21,7 @@ What `RMaaS` does is manage your **[releases](https://docs.github.com/en/free-pr Looking at the flow above, a common release lifecycle could be: - User presses **Create Release Candidate** - - `RMaaS` + - `GRM` 1. Creates a release branch `rc/` 1. Creates Release Candidate tag `rc-` 1. Creates a GitHub prerelease with Release Candidate tag @@ -29,7 +29,7 @@ Looking at the flow above, a common release lifecycle could be: 1. Detects the new tag by matching the git reference `refs/tags/rc-.*` 1. Builds and deploys to staging environment for testing - User presses **Patch** - - `RMaaS` + - `GRM` 1. The selected commit is cherry-picked to the release branch 1. The release tag is bumped 1. Updates GitHub release's tag and description with the patch's details @@ -37,7 +37,7 @@ Looking at the flow above, a common release lifecycle could be: 1. Detects the new tag by matching the git reference `refs/tags/(rc|version)-.*` (Release Versions are patchable as well) 1. Builds and deploys to staging (or production if Release Version) for testing - User presses **Promote Release Candidate to Release Version** - - `RMaaS` + - `GRM` 1. Creates Release Version tag `version-` 1. Promotes the GitHub release by removing the prerelease flag - Your CI diff --git a/plugins/release-manager-as-a-service/dev/README.md b/plugins/github-release-manager/dev/README.md similarity index 70% rename from plugins/release-manager-as-a-service/dev/README.md rename to plugins/github-release-manager/dev/README.md index 550c8e1844..736d20766e 100644 --- a/plugins/release-manager-as-a-service/dev/README.md +++ b/plugins/github-release-manager/dev/README.md @@ -1,12 +1,12 @@ -# release-manager-as-a-service +# github-release-manager -Welcome to the release-manager-as-a-service plugin! +Welcome to the github-release-manager plugin! _This plugin was created through the Backstage CLI_ ## Getting started -Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/release-manager-as-a-service](http://localhost:3000/release-manager-as-a-service). +Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/github-release-manager](http://localhost:3000/github-release-manager). You can also serve the plugin in isolation by running `yarn start` in the plugin directory. This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. diff --git a/plugins/release-manager-as-a-service/dev/index.tsx b/plugins/github-release-manager/dev/index.tsx similarity index 94% rename from plugins/release-manager-as-a-service/dev/index.tsx rename to plugins/github-release-manager/dev/index.tsx index 7bdc0ba12a..ff794f428a 100644 --- a/plugins/release-manager-as-a-service/dev/index.tsx +++ b/plugins/github-release-manager/dev/index.tsx @@ -16,12 +16,12 @@ import React from 'react'; import { createDevApp } from '@backstage/dev-utils'; import { - releaseManagerAsAServicePlugin, + gitHubReleaseManagerPlugin, ReleaseManagerAsAServicePage, } from '../src/plugin'; createDevApp() - .registerPlugin(releaseManagerAsAServicePlugin) + .registerPlugin(gitHubReleaseManagerPlugin) .addPage({ element: ( +
- +
@@ -172,7 +172,7 @@ function Cards({ project, components }: ReleaseManagerAsAServiceProps) { repository: gitHubBatchInfo.value.repository, }) .map((customElement, index) => ( -
{customElement}
+
{customElement}
))} ); diff --git a/plugins/release-manager-as-a-service/src/api/RMaaSApiClient.ts b/plugins/github-release-manager/src/api/ApiClient.ts similarity index 98% rename from plugins/release-manager-as-a-service/src/api/RMaaSApiClient.ts rename to plugins/github-release-manager/src/api/ApiClient.ts index fb4ad0daa7..a3c1ce35e5 100644 --- a/plugins/release-manager-as-a-service/src/api/RMaaSApiClient.ts +++ b/plugins/github-release-manager/src/api/ApiClient.ts @@ -37,7 +37,7 @@ import { SemverTagParts } from '../helpers/tagParts/getSemverTagParts'; * https://github.com/octokit/request.js/#the-data-parameter--set-request-body-directly */ -export class RMaaSApiClient { +export class ApiClient { private readonly pluginApiClient: PluginApiClientConfig; private readonly repoPath: string; private readonly githubCommonPath: string; @@ -317,7 +317,7 @@ export class RMaaSApiClient { data: { type: 'commit', message: - 'Tag generated by your friendly neighborhood Release Manager as a Service', + 'Tag generated by your friendly neighborhood GitHub Release Manager', tag: bumpedTag, object: updatedReference.object.sha, }, diff --git a/plugins/release-manager-as-a-service/src/api/PluginApiClientConfig.ts b/plugins/github-release-manager/src/api/PluginApiClientConfig.ts similarity index 100% rename from plugins/release-manager-as-a-service/src/api/PluginApiClientConfig.ts rename to plugins/github-release-manager/src/api/PluginApiClientConfig.ts diff --git a/plugins/release-manager-as-a-service/src/api/serviceApiRef.ts b/plugins/github-release-manager/src/api/serviceApiRef.ts similarity index 74% rename from plugins/release-manager-as-a-service/src/api/serviceApiRef.ts rename to plugins/github-release-manager/src/api/serviceApiRef.ts index 1daea6f4c1..e55e99b072 100644 --- a/plugins/release-manager-as-a-service/src/api/serviceApiRef.ts +++ b/plugins/github-release-manager/src/api/serviceApiRef.ts @@ -17,10 +17,7 @@ import { createApiRef } from '@backstage/core'; import { PluginApiClientConfig } from './PluginApiClientConfig'; -export const releaseManagerAsAServiceApiRef = createApiRef( - { - id: 'plugin.release-manager-as-a-service.service', - description: - 'Used by the Release Manager as a Service plugin to make requests', - }, -); +export const githubReleaseManagerApiRef = createApiRef({ + id: 'plugin.github-release-manager.service', + description: 'Used by the GitHub Release Manager plugin to make requests', +}); diff --git a/plugins/release-manager-as-a-service/src/cards/createRc/CreateRc.test.tsx b/plugins/github-release-manager/src/cards/createRc/CreateRc.test.tsx similarity index 100% rename from plugins/release-manager-as-a-service/src/cards/createRc/CreateRc.test.tsx rename to plugins/github-release-manager/src/cards/createRc/CreateRc.test.tsx diff --git a/plugins/release-manager-as-a-service/src/cards/createRc/CreateRc.tsx b/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx similarity index 100% rename from plugins/release-manager-as-a-service/src/cards/createRc/CreateRc.tsx rename to plugins/github-release-manager/src/cards/createRc/CreateRc.tsx diff --git a/plugins/release-manager-as-a-service/src/cards/createRc/getRcGitHubInfo.test.ts b/plugins/github-release-manager/src/cards/createRc/getRcGitHubInfo.test.ts similarity index 100% rename from plugins/release-manager-as-a-service/src/cards/createRc/getRcGitHubInfo.test.ts rename to plugins/github-release-manager/src/cards/createRc/getRcGitHubInfo.test.ts diff --git a/plugins/release-manager-as-a-service/src/cards/createRc/getRcGitHubInfo.ts b/plugins/github-release-manager/src/cards/createRc/getRcGitHubInfo.ts similarity index 100% rename from plugins/release-manager-as-a-service/src/cards/createRc/getRcGitHubInfo.ts rename to plugins/github-release-manager/src/cards/createRc/getRcGitHubInfo.ts diff --git a/plugins/release-manager-as-a-service/src/cards/createRc/sideEffects/createRc.test.ts b/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.test.ts similarity index 100% rename from plugins/release-manager-as-a-service/src/cards/createRc/sideEffects/createRc.test.ts rename to plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.test.ts diff --git a/plugins/release-manager-as-a-service/src/cards/createRc/sideEffects/createRc.ts b/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.ts similarity index 97% rename from plugins/release-manager-as-a-service/src/cards/createRc/sideEffects/createRc.ts rename to plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.ts index 2b5b71ee24..6d45abbb0f 100644 --- a/plugins/release-manager-as-a-service/src/cards/createRc/sideEffects/createRc.ts +++ b/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.ts @@ -21,11 +21,11 @@ import { GhGetRepositoryResponse, ResponseStep, } from '../../../types/types'; -import { RMaaSApiClient } from '../../../api/RMaaSApiClient'; +import { ApiClient } from '../../../api/ApiClient'; import { ReleaseManagerAsAServiceError } from '../../../errors/ReleaseManagerAsAServiceError'; interface CreateRC { - apiClient: RMaaSApiClient; + apiClient: ApiClient; defaultBranch: GhGetRepositoryResponse['default_branch']; latestRelease: GhGetReleaseResponse | null; nextGitHubInfo: ReturnType; diff --git a/plugins/release-manager-as-a-service/src/cards/info/Info.test.tsx b/plugins/github-release-manager/src/cards/info/Info.test.tsx similarity index 100% rename from plugins/release-manager-as-a-service/src/cards/info/Info.test.tsx rename to plugins/github-release-manager/src/cards/info/Info.test.tsx diff --git a/plugins/release-manager-as-a-service/src/cards/info/Info.tsx b/plugins/github-release-manager/src/cards/info/Info.tsx similarity index 95% rename from plugins/release-manager-as-a-service/src/cards/info/Info.tsx rename to plugins/github-release-manager/src/cards/info/Info.tsx index 76d821b0a5..6b371ea873 100644 --- a/plugins/release-manager-as-a-service/src/cards/info/Info.tsx +++ b/plugins/github-release-manager/src/cards/info/Info.tsx @@ -73,9 +73,9 @@ export const Info = ({ Flow - RMaaS is built with a specific flow in mind. For example, it assumes - your project is configured to react to tags prefixed with rc or{' '} - version. + GitHub Release Manager is built with a specific flow in mind. For + example, it assumes your project is configured to react to tags + prefixed with rc or version. diff --git a/plugins/github-release-manager/src/cards/info/flow.png b/plugins/github-release-manager/src/cards/info/flow.png new file mode 100644 index 0000000000000000000000000000000000000000..e70df0d7faaa4944671e29177a0377fa07f6506a GIT binary patch literal 78736 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
k=%sHB8QBb|bTbhmW3(v5V3gi1(vcjrqth;&Lf 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 C@nBW} literal 0 HcmV?d00001 diff --git a/plugins/release-manager-as-a-service/src/cards/patchRc/Patch.test.tsx b/plugins/github-release-manager/src/cards/patchRc/Patch.test.tsx similarity index 100% rename from plugins/release-manager-as-a-service/src/cards/patchRc/Patch.test.tsx rename to plugins/github-release-manager/src/cards/patchRc/Patch.test.tsx diff --git a/plugins/release-manager-as-a-service/src/cards/patchRc/Patch.tsx b/plugins/github-release-manager/src/cards/patchRc/Patch.tsx similarity index 100% rename from plugins/release-manager-as-a-service/src/cards/patchRc/Patch.tsx rename to plugins/github-release-manager/src/cards/patchRc/Patch.tsx diff --git a/plugins/release-manager-as-a-service/src/cards/patchRc/PatchBody.test.tsx b/plugins/github-release-manager/src/cards/patchRc/PatchBody.test.tsx similarity index 100% rename from plugins/release-manager-as-a-service/src/cards/patchRc/PatchBody.test.tsx rename to plugins/github-release-manager/src/cards/patchRc/PatchBody.test.tsx diff --git a/plugins/release-manager-as-a-service/src/cards/patchRc/PatchBody.tsx b/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx similarity index 100% rename from plugins/release-manager-as-a-service/src/cards/patchRc/PatchBody.tsx rename to plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx diff --git a/plugins/release-manager-as-a-service/src/cards/patchRc/sideEffects/patch.test.ts b/plugins/github-release-manager/src/cards/patchRc/sideEffects/patch.test.ts similarity index 100% rename from plugins/release-manager-as-a-service/src/cards/patchRc/sideEffects/patch.test.ts rename to plugins/github-release-manager/src/cards/patchRc/sideEffects/patch.test.ts diff --git a/plugins/release-manager-as-a-service/src/cards/patchRc/sideEffects/patch.ts b/plugins/github-release-manager/src/cards/patchRc/sideEffects/patch.ts similarity index 98% rename from plugins/release-manager-as-a-service/src/cards/patchRc/sideEffects/patch.ts rename to plugins/github-release-manager/src/cards/patchRc/sideEffects/patch.ts index 797ed8229c..6b6190e723 100644 --- a/plugins/release-manager-as-a-service/src/cards/patchRc/sideEffects/patch.ts +++ b/plugins/github-release-manager/src/cards/patchRc/sideEffects/patch.ts @@ -21,11 +21,11 @@ import { } from '../../../types/types'; import { CalverTagParts } from '../../../helpers/tagParts/getCalverTagParts'; import { ReleaseManagerAsAServiceError } from '../../../errors/ReleaseManagerAsAServiceError'; -import { RMaaSApiClient } from '../../../api/RMaaSApiClient'; +import { ApiClient } from '../../../api/ApiClient'; import { SemverTagParts } from '../../../helpers/tagParts/getSemverTagParts'; interface Patch { - apiClient: RMaaSApiClient; + apiClient: ApiClient; bumpedTag: string; latestRelease: GhGetReleaseResponse; selectedPatchCommit: GhGetCommitResponse; diff --git a/plugins/release-manager-as-a-service/src/cards/promoteRc/PromoteRc.test.tsx b/plugins/github-release-manager/src/cards/promoteRc/PromoteRc.test.tsx similarity index 100% rename from plugins/release-manager-as-a-service/src/cards/promoteRc/PromoteRc.test.tsx rename to plugins/github-release-manager/src/cards/promoteRc/PromoteRc.test.tsx diff --git a/plugins/release-manager-as-a-service/src/cards/promoteRc/PromoteRc.tsx b/plugins/github-release-manager/src/cards/promoteRc/PromoteRc.tsx similarity index 100% rename from plugins/release-manager-as-a-service/src/cards/promoteRc/PromoteRc.tsx rename to plugins/github-release-manager/src/cards/promoteRc/PromoteRc.tsx diff --git a/plugins/release-manager-as-a-service/src/cards/promoteRc/PromoteRcBody.test.tsx b/plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.test.tsx similarity index 100% rename from plugins/release-manager-as-a-service/src/cards/promoteRc/PromoteRcBody.test.tsx rename to plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.test.tsx diff --git a/plugins/release-manager-as-a-service/src/cards/promoteRc/PromoteRcBody.tsx b/plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.tsx similarity index 100% rename from plugins/release-manager-as-a-service/src/cards/promoteRc/PromoteRcBody.tsx rename to plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.tsx diff --git a/plugins/release-manager-as-a-service/src/cards/promoteRc/sideEffects/promoteRc.test.ts b/plugins/github-release-manager/src/cards/promoteRc/sideEffects/promoteRc.test.ts similarity index 100% rename from plugins/release-manager-as-a-service/src/cards/promoteRc/sideEffects/promoteRc.test.ts rename to plugins/github-release-manager/src/cards/promoteRc/sideEffects/promoteRc.test.ts diff --git a/plugins/release-manager-as-a-service/src/cards/promoteRc/sideEffects/promoteRc.ts b/plugins/github-release-manager/src/cards/promoteRc/sideEffects/promoteRc.ts similarity index 94% rename from plugins/release-manager-as-a-service/src/cards/promoteRc/sideEffects/promoteRc.ts rename to plugins/github-release-manager/src/cards/promoteRc/sideEffects/promoteRc.ts index d473951a91..34085a3d12 100644 --- a/plugins/release-manager-as-a-service/src/cards/promoteRc/sideEffects/promoteRc.ts +++ b/plugins/github-release-manager/src/cards/promoteRc/sideEffects/promoteRc.ts @@ -18,10 +18,10 @@ import { GhGetReleaseResponse, ResponseStep, } from '../../../types/types'; -import { RMaaSApiClient } from '../../../api/RMaaSApiClient'; +import { ApiClient } from '../../../api/ApiClient'; interface PromoteRc { - apiClient: RMaaSApiClient; + apiClient: ApiClient; rcRelease: GhGetReleaseResponse; releaseVersion: string; successCb?: ComponentConfigPromoteRc['successCb']; diff --git a/plugins/release-manager-as-a-service/src/components/Differ.tsx b/plugins/github-release-manager/src/components/Differ.tsx similarity index 100% rename from plugins/release-manager-as-a-service/src/components/Differ.tsx rename to plugins/github-release-manager/src/components/Differ.tsx diff --git a/plugins/release-manager-as-a-service/src/components/Divider.test.tsx b/plugins/github-release-manager/src/components/Divider.test.tsx similarity index 100% rename from plugins/release-manager-as-a-service/src/components/Divider.test.tsx rename to plugins/github-release-manager/src/components/Divider.test.tsx diff --git a/plugins/release-manager-as-a-service/src/components/Divider.tsx b/plugins/github-release-manager/src/components/Divider.tsx similarity index 100% rename from plugins/release-manager-as-a-service/src/components/Divider.tsx rename to plugins/github-release-manager/src/components/Divider.tsx diff --git a/plugins/release-manager-as-a-service/src/components/InfoCardPlus.test.tsx b/plugins/github-release-manager/src/components/InfoCardPlus.test.tsx similarity index 100% rename from plugins/release-manager-as-a-service/src/components/InfoCardPlus.test.tsx rename to plugins/github-release-manager/src/components/InfoCardPlus.test.tsx diff --git a/plugins/release-manager-as-a-service/src/components/InfoCardPlus.tsx b/plugins/github-release-manager/src/components/InfoCardPlus.tsx similarity index 100% rename from plugins/release-manager-as-a-service/src/components/InfoCardPlus.tsx rename to plugins/github-release-manager/src/components/InfoCardPlus.tsx diff --git a/plugins/release-manager-as-a-service/src/components/NoLatestRelease.test.tsx b/plugins/github-release-manager/src/components/NoLatestRelease.test.tsx similarity index 100% rename from plugins/release-manager-as-a-service/src/components/NoLatestRelease.test.tsx rename to plugins/github-release-manager/src/components/NoLatestRelease.test.tsx diff --git a/plugins/release-manager-as-a-service/src/components/NoLatestRelease.tsx b/plugins/github-release-manager/src/components/NoLatestRelease.tsx similarity index 100% rename from plugins/release-manager-as-a-service/src/components/NoLatestRelease.tsx rename to plugins/github-release-manager/src/components/NoLatestRelease.tsx diff --git a/plugins/release-manager-as-a-service/src/components/ProjectContext.ts b/plugins/github-release-manager/src/components/ProjectContext.ts similarity index 86% rename from plugins/release-manager-as-a-service/src/components/ProjectContext.ts rename to plugins/github-release-manager/src/components/ProjectContext.ts index 26a195b5ed..e9cf87a859 100644 --- a/plugins/release-manager-as-a-service/src/components/ProjectContext.ts +++ b/plugins/github-release-manager/src/components/ProjectContext.ts @@ -15,12 +15,10 @@ */ import { createContext, useContext } from 'react'; -import { RMaaSApiClient } from '../api/RMaaSApiClient'; +import { ApiClient } from '../api/ApiClient'; import { ReleaseManagerAsAServiceError } from '../errors/ReleaseManagerAsAServiceError'; -export const ApiClientContext = createContext( - undefined, -); +export const ApiClientContext = createContext(undefined); export const useApiClientContext = () => { const apiClient = useContext(ApiClientContext); diff --git a/plugins/release-manager-as-a-service/src/components/ReloadButton.test.tsx b/plugins/github-release-manager/src/components/ReloadButton.test.tsx similarity index 100% rename from plugins/release-manager-as-a-service/src/components/ReloadButton.test.tsx rename to plugins/github-release-manager/src/components/ReloadButton.test.tsx diff --git a/plugins/release-manager-as-a-service/src/components/ReloadButton.tsx b/plugins/github-release-manager/src/components/ReloadButton.tsx similarity index 100% rename from plugins/release-manager-as-a-service/src/components/ReloadButton.tsx rename to plugins/github-release-manager/src/components/ReloadButton.tsx diff --git a/plugins/release-manager-as-a-service/src/components/ResponseStepList/ResponseStepList.test.tsx b/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.test.tsx similarity index 100% rename from plugins/release-manager-as-a-service/src/components/ResponseStepList/ResponseStepList.test.tsx rename to plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.test.tsx diff --git a/plugins/release-manager-as-a-service/src/components/ResponseStepList/ResponseStepList.tsx b/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.tsx similarity index 100% rename from plugins/release-manager-as-a-service/src/components/ResponseStepList/ResponseStepList.tsx rename to plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.tsx diff --git a/plugins/release-manager-as-a-service/src/components/ResponseStepList/ResponseStepListItem.test.tsx b/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepListItem.test.tsx similarity index 100% rename from plugins/release-manager-as-a-service/src/components/ResponseStepList/ResponseStepListItem.test.tsx rename to plugins/github-release-manager/src/components/ResponseStepList/ResponseStepListItem.test.tsx diff --git a/plugins/release-manager-as-a-service/src/components/ResponseStepList/ResponseStepListItem.tsx b/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepListItem.tsx similarity index 100% rename from plugins/release-manager-as-a-service/src/components/ResponseStepList/ResponseStepListItem.tsx rename to plugins/github-release-manager/src/components/ResponseStepList/ResponseStepListItem.tsx diff --git a/plugins/release-manager-as-a-service/src/constants/constants.test.ts b/plugins/github-release-manager/src/constants/constants.test.ts similarity index 100% rename from plugins/release-manager-as-a-service/src/constants/constants.test.ts rename to plugins/github-release-manager/src/constants/constants.test.ts diff --git a/plugins/release-manager-as-a-service/src/constants/constants.ts b/plugins/github-release-manager/src/constants/constants.ts similarity index 100% rename from plugins/release-manager-as-a-service/src/constants/constants.ts rename to plugins/github-release-manager/src/constants/constants.ts diff --git a/plugins/release-manager-as-a-service/src/errors/ReleaseManagerAsAServiceError.ts b/plugins/github-release-manager/src/errors/ReleaseManagerAsAServiceError.ts similarity index 100% rename from plugins/release-manager-as-a-service/src/errors/ReleaseManagerAsAServiceError.ts rename to plugins/github-release-manager/src/errors/ReleaseManagerAsAServiceError.ts diff --git a/plugins/release-manager-as-a-service/src/helpers/date.test.ts b/plugins/github-release-manager/src/helpers/date.test.ts similarity index 100% rename from plugins/release-manager-as-a-service/src/helpers/date.test.ts rename to plugins/github-release-manager/src/helpers/date.test.ts diff --git a/plugins/release-manager-as-a-service/src/helpers/date.ts b/plugins/github-release-manager/src/helpers/date.ts similarity index 100% rename from plugins/release-manager-as-a-service/src/helpers/date.ts rename to plugins/github-release-manager/src/helpers/date.ts diff --git a/plugins/release-manager-as-a-service/src/helpers/getBumpedTag.test.ts b/plugins/github-release-manager/src/helpers/getBumpedTag.test.ts similarity index 100% rename from plugins/release-manager-as-a-service/src/helpers/getBumpedTag.test.ts rename to plugins/github-release-manager/src/helpers/getBumpedTag.test.ts diff --git a/plugins/release-manager-as-a-service/src/helpers/getBumpedTag.ts b/plugins/github-release-manager/src/helpers/getBumpedTag.ts similarity index 100% rename from plugins/release-manager-as-a-service/src/helpers/getBumpedTag.ts rename to plugins/github-release-manager/src/helpers/getBumpedTag.ts diff --git a/plugins/release-manager-as-a-service/src/helpers/getShortCommitHash.test.ts b/plugins/github-release-manager/src/helpers/getShortCommitHash.test.ts similarity index 100% rename from plugins/release-manager-as-a-service/src/helpers/getShortCommitHash.test.ts rename to plugins/github-release-manager/src/helpers/getShortCommitHash.test.ts diff --git a/plugins/release-manager-as-a-service/src/helpers/getShortCommitHash.ts b/plugins/github-release-manager/src/helpers/getShortCommitHash.ts similarity index 100% rename from plugins/release-manager-as-a-service/src/helpers/getShortCommitHash.ts rename to plugins/github-release-manager/src/helpers/getShortCommitHash.ts diff --git a/plugins/release-manager-as-a-service/src/helpers/isCalverTagParts.test.ts b/plugins/github-release-manager/src/helpers/isCalverTagParts.test.ts similarity index 100% rename from plugins/release-manager-as-a-service/src/helpers/isCalverTagParts.test.ts rename to plugins/github-release-manager/src/helpers/isCalverTagParts.test.ts diff --git a/plugins/release-manager-as-a-service/src/helpers/isCalverTagParts.ts b/plugins/github-release-manager/src/helpers/isCalverTagParts.ts similarity index 100% rename from plugins/release-manager-as-a-service/src/helpers/isCalverTagParts.ts rename to plugins/github-release-manager/src/helpers/isCalverTagParts.ts diff --git a/plugins/release-manager-as-a-service/src/helpers/tagParts/getCalverTagParts.ts b/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.ts similarity index 100% rename from plugins/release-manager-as-a-service/src/helpers/tagParts/getCalverTagParts.ts rename to plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.ts diff --git a/plugins/release-manager-as-a-service/src/helpers/tagParts/getSemverTagParts.ts b/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.ts similarity index 100% rename from plugins/release-manager-as-a-service/src/helpers/tagParts/getSemverTagParts.ts rename to plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.ts diff --git a/plugins/release-manager-as-a-service/src/helpers/tagParts/getTagParts.test.ts b/plugins/github-release-manager/src/helpers/tagParts/getTagParts.test.ts similarity index 100% rename from plugins/release-manager-as-a-service/src/helpers/tagParts/getTagParts.test.ts rename to plugins/github-release-manager/src/helpers/tagParts/getTagParts.test.ts diff --git a/plugins/release-manager-as-a-service/src/helpers/tagParts/getTagParts.ts b/plugins/github-release-manager/src/helpers/tagParts/getTagParts.ts similarity index 100% rename from plugins/release-manager-as-a-service/src/helpers/tagParts/getTagParts.ts rename to plugins/github-release-manager/src/helpers/tagParts/getTagParts.ts diff --git a/plugins/release-manager-as-a-service/src/index.ts b/plugins/github-release-manager/src/index.ts similarity index 91% rename from plugins/release-manager-as-a-service/src/index.ts rename to plugins/github-release-manager/src/index.ts index 3461c92d03..6768091ee9 100644 --- a/plugins/release-manager-as-a-service/src/index.ts +++ b/plugins/github-release-manager/src/index.ts @@ -14,6 +14,6 @@ * limitations under the License. */ export { - releaseManagerAsAServicePlugin, + gitHubReleaseManagerPlugin as releaseManagerAsAServicePlugin, ReleaseManagerAsAServicePage, } from './plugin'; diff --git a/plugins/release-manager-as-a-service/src/plugin.test.ts b/plugins/github-release-manager/src/plugin.test.ts similarity index 79% rename from plugins/release-manager-as-a-service/src/plugin.test.ts rename to plugins/github-release-manager/src/plugin.test.ts index 2ecbad7bc5..c952ce30fa 100644 --- a/plugins/release-manager-as-a-service/src/plugin.test.ts +++ b/plugins/github-release-manager/src/plugin.test.ts @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { releaseManagerAsAServicePlugin } from './plugin'; +import { gitHubReleaseManagerPlugin } from './plugin'; -describe('release-manager-as-a-service', () => { +describe('github-release-manager', () => { it('should export plugin', () => { - expect(releaseManagerAsAServicePlugin).toBeDefined(); + expect(gitHubReleaseManagerPlugin).toBeDefined(); }); }); diff --git a/plugins/release-manager-as-a-service/src/plugin.ts b/plugins/github-release-manager/src/plugin.ts similarity index 75% rename from plugins/release-manager-as-a-service/src/plugin.ts rename to plugins/github-release-manager/src/plugin.ts index a1ccabd20f..276691762a 100644 --- a/plugins/release-manager-as-a-service/src/plugin.ts +++ b/plugins/github-release-manager/src/plugin.ts @@ -21,18 +21,18 @@ import { createRoutableExtension, } from '@backstage/core'; -import { releaseManagerAsAServiceApiRef } from './api/serviceApiRef'; +import { githubReleaseManagerApiRef } from './api/serviceApiRef'; import { PluginApiClientConfig } from './api/PluginApiClientConfig'; import { rootRouteRef } from './routes'; -export const releaseManagerAsAServicePlugin = createPlugin({ - id: 'release-manager-as-a-service', +export const gitHubReleaseManagerPlugin = createPlugin({ + id: 'github-release-manager', routes: { root: rootRouteRef, }, apis: [ createApiFactory({ - api: releaseManagerAsAServiceApiRef, + api: githubReleaseManagerApiRef, deps: { configApi: configApiRef, githubAuthApi: githubAuthApiRef, @@ -43,12 +43,10 @@ export const releaseManagerAsAServicePlugin = createPlugin({ ], }); -export const ReleaseManagerAsAServicePage = releaseManagerAsAServicePlugin.provide( +export const ReleaseManagerAsAServicePage = gitHubReleaseManagerPlugin.provide( createRoutableExtension({ component: () => - import('./ReleaseManagerAsAService').then( - m => m.ReleaseManagerAsAService, - ), + import('./GitHubReleaseManager').then(m => m.ReleaseManagerAsAService), mountPoint: rootRouteRef, }), ); diff --git a/plugins/release-manager-as-a-service/src/routes.ts b/plugins/github-release-manager/src/routes.ts similarity index 87% rename from plugins/release-manager-as-a-service/src/routes.ts rename to plugins/github-release-manager/src/routes.ts index 6527388235..d9dd0774b9 100644 --- a/plugins/release-manager-as-a-service/src/routes.ts +++ b/plugins/github-release-manager/src/routes.ts @@ -15,6 +15,4 @@ */ import { createRouteRef } from '@backstage/core'; -export const rootRouteRef = createRouteRef({ - title: 'release-manager-as-a-service', -}); +export const rootRouteRef = createRouteRef({ title: 'github-release-manager' }); diff --git a/plugins/release-manager-as-a-service/src/setupTests.ts b/plugins/github-release-manager/src/setupTests.ts similarity index 100% rename from plugins/release-manager-as-a-service/src/setupTests.ts rename to plugins/github-release-manager/src/setupTests.ts diff --git a/plugins/release-manager-as-a-service/src/sideEffects/getGitHubBatchInfo.ts b/plugins/github-release-manager/src/sideEffects/getGitHubBatchInfo.ts similarity index 93% rename from plugins/release-manager-as-a-service/src/sideEffects/getGitHubBatchInfo.ts rename to plugins/github-release-manager/src/sideEffects/getGitHubBatchInfo.ts index cb044f6f92..8051569772 100644 --- a/plugins/release-manager-as-a-service/src/sideEffects/getGitHubBatchInfo.ts +++ b/plugins/github-release-manager/src/sideEffects/getGitHubBatchInfo.ts @@ -13,11 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { RMaaSApiClient } from '../api/RMaaSApiClient'; +import { ApiClient } from '../api/ApiClient'; import { getLatestRelease } from './getLatestRelease'; interface GetGitHubBatchInfo { - apiClient: RMaaSApiClient; + apiClient: ApiClient; } export const getGitHubBatchInfo = ({ diff --git a/plugins/release-manager-as-a-service/src/sideEffects/getLatestRelease.test.ts b/plugins/github-release-manager/src/sideEffects/getLatestRelease.test.ts similarity index 100% rename from plugins/release-manager-as-a-service/src/sideEffects/getLatestRelease.test.ts rename to plugins/github-release-manager/src/sideEffects/getLatestRelease.test.ts diff --git a/plugins/release-manager-as-a-service/src/sideEffects/getLatestRelease.ts b/plugins/github-release-manager/src/sideEffects/getLatestRelease.ts similarity index 91% rename from plugins/release-manager-as-a-service/src/sideEffects/getLatestRelease.ts rename to plugins/github-release-manager/src/sideEffects/getLatestRelease.ts index 1044d510c5..5690206553 100644 --- a/plugins/release-manager-as-a-service/src/sideEffects/getLatestRelease.ts +++ b/plugins/github-release-manager/src/sideEffects/getLatestRelease.ts @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { RMaaSApiClient } from '../api/RMaaSApiClient'; +import { ApiClient } from '../api/ApiClient'; interface GetLatestRelease { - apiClient: RMaaSApiClient; + apiClient: ApiClient; } export async function getLatestRelease({ apiClient }: GetLatestRelease) { diff --git a/plugins/release-manager-as-a-service/src/styles/styles.ts b/plugins/github-release-manager/src/styles/styles.ts similarity index 100% rename from plugins/release-manager-as-a-service/src/styles/styles.ts rename to plugins/github-release-manager/src/styles/styles.ts diff --git a/plugins/release-manager-as-a-service/src/test-helpers/test-helpers.test.ts b/plugins/github-release-manager/src/test-helpers/test-helpers.test.ts similarity index 100% rename from plugins/release-manager-as-a-service/src/test-helpers/test-helpers.test.ts rename to plugins/github-release-manager/src/test-helpers/test-helpers.test.ts diff --git a/plugins/release-manager-as-a-service/src/test-helpers/test-helpers.ts b/plugins/github-release-manager/src/test-helpers/test-helpers.ts similarity index 100% rename from plugins/release-manager-as-a-service/src/test-helpers/test-helpers.ts rename to plugins/github-release-manager/src/test-helpers/test-helpers.ts diff --git a/plugins/github-release-manager/src/test-helpers/test-ids.ts b/plugins/github-release-manager/src/test-helpers/test-ids.ts new file mode 100644 index 0000000000..27aef5567f --- /dev/null +++ b/plugins/github-release-manager/src/test-helpers/test-ids.ts @@ -0,0 +1,53 @@ +/* + * 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. + */ +export const TEST_IDS = { + info: { + info: 'grm--info', + infoCardPlus: 'grm--info-card-plus', + }, + createRc: { + cta: 'grm--create-rc--cta', + semverSelect: 'grm--create-rc--semver-select', + }, + promoteRc: { + mockedPromoteRcBody: 'grm-mocked-promote-rc-body', + notRcWarning: 'grm--promote-rc--not-rc-warning', + promoteRc: 'grm--promote-rc', + cta: 'grm--promote-rc-body--cta', + }, + patch: { + error: 'grm--patch-body--error', + loading: 'grm--patch-body--loading', + notPrerelease: 'grm--patch-body--not-prerelease--info', + body: 'grm--patch-body', + }, + components: { + divider: 'grm--divider', + reloadButton: 'grm--reload-button', + noLatestRelease: 'grm--no-latest-release', + circularProgress: 'grm--circular-progress', + responseStepListDialogContent: 'grm--response-step-list--dialog-content', + responseStepListItem: 'grm--response-step-list-item', + responseStepListItemIconSuccess: + 'grm--response-step-list-item--item-icon--success', + responseStepListItemIconFailure: + 'grm--response-step-list-item--item-icon--failure', + responseStepListItemIconLink: + 'grm--response-step-list-item--item-icon--link', + responseStepListItemIconDefault: + 'grm--response-step-list-item--item-icon--default', + }, +}; diff --git a/plugins/release-manager-as-a-service/src/types/types.ts b/plugins/github-release-manager/src/types/types.ts similarity index 99% rename from plugins/release-manager-as-a-service/src/types/types.ts rename to plugins/github-release-manager/src/types/types.ts index 5af069b39b..f4fadefce3 100644 --- a/plugins/release-manager-as-a-service/src/types/types.ts +++ b/plugins/github-release-manager/src/types/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ export interface Project { - /** A unique (in the context of RMaaS) project name */ + /** A unique (in the context of GitHub Release Manager) project name */ name: string; /** GitHub details */ diff --git a/plugins/release-manager-as-a-service/src/cards/info/flow.png b/plugins/release-manager-as-a-service/src/cards/info/flow.png deleted file mode 100644 index 947f9c0f35da00b84f1267fee8cd06e9c286d1e8..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 77527 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 Date: Tue, 13 Apr 2021 10:28:39 +0200 Subject: [PATCH 006/140] Additional renamings Signed-off-by: Erik Engervall --- ...rvice.yaml => github-release-manager.yaml} | 0 plugins/github-release-manager/dev/index.tsx | 15 ++++++----- .../src/GitHubReleaseManager.tsx | 4 ++- .../createRc/sideEffects/createRc.test.ts | 2 +- .../cards/createRc/sideEffects/createRc.ts | 4 +-- .../src/cards/patchRc/PatchBody.test.tsx | 2 +- .../src/cards/patchRc/sideEffects/patch.ts | 8 +++--- .../src/components/ProjectContext.ts | 4 +-- ...eError.ts => GitHubReleaseManagerError.ts} | 4 +-- .../src/helpers/date.test.ts | 26 ------------------- .../src/helpers/date.ts | 16 ------------ .../src/helpers/getShortCommitHash.ts | 4 +-- .../src/helpers/tagParts/getCalverTagParts.ts | 4 +-- .../src/helpers/tagParts/getSemverTagParts.ts | 4 +-- plugins/github-release-manager/src/index.ts | 5 +--- plugins/github-release-manager/src/plugin.ts | 2 +- 16 files changed, 30 insertions(+), 74 deletions(-) rename microsite/data/plugins/{release-manager-as-a-service.yaml => github-release-manager.yaml} (100%) rename plugins/github-release-manager/src/errors/{ReleaseManagerAsAServiceError.ts => GitHubReleaseManagerError.ts} (85%) delete mode 100644 plugins/github-release-manager/src/helpers/date.test.ts delete mode 100644 plugins/github-release-manager/src/helpers/date.ts diff --git a/microsite/data/plugins/release-manager-as-a-service.yaml b/microsite/data/plugins/github-release-manager.yaml similarity index 100% rename from microsite/data/plugins/release-manager-as-a-service.yaml rename to microsite/data/plugins/github-release-manager.yaml diff --git a/plugins/github-release-manager/dev/index.tsx b/plugins/github-release-manager/dev/index.tsx index ff794f428a..d525b3f55b 100644 --- a/plugins/github-release-manager/dev/index.tsx +++ b/plugins/github-release-manager/dev/index.tsx @@ -15,41 +15,42 @@ */ import React from 'react'; import { createDevApp } from '@backstage/dev-utils'; + import { gitHubReleaseManagerPlugin, - ReleaseManagerAsAServicePage, + GitHubReleaseManagerPage, } from '../src/plugin'; createDevApp() .registerPlugin(gitHubReleaseManagerPlugin) .addPage({ + title: 'Page 1', element: ( - ), - title: 'Root Page', }) .addPage({ + title: 'Page 2', element: ( - ), - title: 'Another page', }) .render(); diff --git a/plugins/github-release-manager/src/GitHubReleaseManager.tsx b/plugins/github-release-manager/src/GitHubReleaseManager.tsx index 6b89eb4439..2011fb40b0 100644 --- a/plugins/github-release-manager/src/GitHubReleaseManager.tsx +++ b/plugins/github-release-manager/src/GitHubReleaseManager.tsx @@ -114,7 +114,9 @@ function Cards({ project, components }: ReleaseManagerAsAServiceProps) { } if (gitHubBatchInfo.value === undefined) { - return Failed to fetch latest GHE release; + return ( + Failed to fetch latest GitHub release + ); } if (!gitHubBatchInfo.value.repository.permissions.push) { diff --git a/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.test.ts b/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.test.ts index 26450ac162..407790f252 100644 --- a/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.test.ts +++ b/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.test.ts @@ -21,7 +21,7 @@ import { } from '../../../test-helpers/test-helpers'; import { createRc } from './createRc'; -describe('createGheRc', () => { +describe('createRc', () => { beforeEach(jest.clearAllMocks); it('should work', async () => { diff --git a/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.ts b/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.ts index 6d45abbb0f..0a532dcb0d 100644 --- a/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.ts +++ b/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.ts @@ -22,7 +22,7 @@ import { ResponseStep, } from '../../../types/types'; import { ApiClient } from '../../../api/ApiClient'; -import { ReleaseManagerAsAServiceError } from '../../../errors/ReleaseManagerAsAServiceError'; +import { GitHubReleaseManagerError } from '../../../errors/GitHubReleaseManagerError'; interface CreateRC { apiClient: ApiClient; @@ -67,7 +67,7 @@ export async function createRc({ ).createdRef; } catch (error) { if (error.body.message === 'Reference already exists') { - throw new ReleaseManagerAsAServiceError( + throw new GitHubReleaseManagerError( `Branch "${nextGitHubInfo.rcBranch}" already exists: .../tree/${nextGitHubInfo.rcBranch}`, ); } diff --git a/plugins/github-release-manager/src/cards/patchRc/PatchBody.test.tsx b/plugins/github-release-manager/src/cards/patchRc/PatchBody.test.tsx index 74f701a120..7ddc6308cf 100644 --- a/plugins/github-release-manager/src/cards/patchRc/PatchBody.test.tsx +++ b/plugins/github-release-manager/src/cards/patchRc/PatchBody.test.tsx @@ -37,7 +37,7 @@ describe('PatchBody', () => { it('should render error', async () => { mockApiClient.getBranch.mockImplementationOnce(() => { - throw new Error('lmao hehe'); + throw new Error('banana'); }); const { getByTestId } = render( diff --git a/plugins/github-release-manager/src/cards/patchRc/sideEffects/patch.ts b/plugins/github-release-manager/src/cards/patchRc/sideEffects/patch.ts index 6b6190e723..1d592b330b 100644 --- a/plugins/github-release-manager/src/cards/patchRc/sideEffects/patch.ts +++ b/plugins/github-release-manager/src/cards/patchRc/sideEffects/patch.ts @@ -20,7 +20,7 @@ import { ResponseStep, } from '../../../types/types'; import { CalverTagParts } from '../../../helpers/tagParts/getCalverTagParts'; -import { ReleaseManagerAsAServiceError } from '../../../errors/ReleaseManagerAsAServiceError'; +import { GitHubReleaseManagerError } from '../../../errors/GitHubReleaseManagerError'; import { ApiClient } from '../../../api/ApiClient'; import { SemverTagParts } from '../../../helpers/tagParts/getSemverTagParts'; @@ -33,9 +33,7 @@ interface Patch { tagParts: NonNullable; } -/** - * Inspo: https://stackoverflow.com/questions/53859199/how-to-cherry-pick-through-githubs-api - */ +// Inspo: https://stackoverflow.com/questions/53859199/how-to-cherry-pick-through-githubs-api export async function patch({ apiClient, bumpedTag, @@ -47,7 +45,7 @@ export async function patch({ const responseSteps: ResponseStep[] = []; if (!selectedPatchCommit || !selectedPatchCommit.sha) { - throw new ReleaseManagerAsAServiceError('Invalid commit'); + throw new GitHubReleaseManagerError('Invalid commit'); } const releaseBranchName = latestRelease.target_commitish; diff --git a/plugins/github-release-manager/src/components/ProjectContext.ts b/plugins/github-release-manager/src/components/ProjectContext.ts index e9cf87a859..61d41f8dc9 100644 --- a/plugins/github-release-manager/src/components/ProjectContext.ts +++ b/plugins/github-release-manager/src/components/ProjectContext.ts @@ -16,7 +16,7 @@ import { createContext, useContext } from 'react'; import { ApiClient } from '../api/ApiClient'; -import { ReleaseManagerAsAServiceError } from '../errors/ReleaseManagerAsAServiceError'; +import { GitHubReleaseManagerError } from '../errors/GitHubReleaseManagerError'; export const ApiClientContext = createContext(undefined); @@ -24,7 +24,7 @@ export const useApiClientContext = () => { const apiClient = useContext(ApiClientContext); if (!apiClient) { - throw new ReleaseManagerAsAServiceError('apiClient not found'); + throw new GitHubReleaseManagerError('apiClient not found'); } return apiClient; diff --git a/plugins/github-release-manager/src/errors/ReleaseManagerAsAServiceError.ts b/plugins/github-release-manager/src/errors/GitHubReleaseManagerError.ts similarity index 85% rename from plugins/github-release-manager/src/errors/ReleaseManagerAsAServiceError.ts rename to plugins/github-release-manager/src/errors/GitHubReleaseManagerError.ts index cd6231d229..144ad7af90 100644 --- a/plugins/github-release-manager/src/errors/ReleaseManagerAsAServiceError.ts +++ b/plugins/github-release-manager/src/errors/GitHubReleaseManagerError.ts @@ -13,10 +13,10 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export class ReleaseManagerAsAServiceError extends Error { +export class GitHubReleaseManagerError extends Error { constructor(message: string) { super(message); - this.name = 'ReleaseManagerAsAServiceError'; + this.name = 'GitHubReleaseManagerError'; } } diff --git a/plugins/github-release-manager/src/helpers/date.test.ts b/plugins/github-release-manager/src/helpers/date.test.ts deleted file mode 100644 index 06bdd31573..0000000000 --- a/plugins/github-release-manager/src/helpers/date.test.ts +++ /dev/null @@ -1,26 +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 { getNewDate } from './date'; - -describe('getNewDate', () => { - it('should get a date', () => { - const newDate = getNewDate(); - - expect(newDate.toISOString()).toMatch( - /[\d]{4}-[\d]{2}-[\d]{2}T[\d]{2}:[\d]{2}:[\d]{2}.[\d]{3}Z/, - ); - }); -}); diff --git a/plugins/github-release-manager/src/helpers/date.ts b/plugins/github-release-manager/src/helpers/date.ts deleted file mode 100644 index 6c9d8423b6..0000000000 --- a/plugins/github-release-manager/src/helpers/date.ts +++ /dev/null @@ -1,16 +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. - */ -export const getNewDate = () => new Date(); diff --git a/plugins/github-release-manager/src/helpers/getShortCommitHash.ts b/plugins/github-release-manager/src/helpers/getShortCommitHash.ts index 7abc541606..bb85703477 100644 --- a/plugins/github-release-manager/src/helpers/getShortCommitHash.ts +++ b/plugins/github-release-manager/src/helpers/getShortCommitHash.ts @@ -13,13 +13,13 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ReleaseManagerAsAServiceError } from '../errors/ReleaseManagerAsAServiceError'; +import { GitHubReleaseManagerError } from '../errors/GitHubReleaseManagerError'; export function getShortCommitHash(hash: string) { const shortCommitHash = hash.substr(0, 7); if (shortCommitHash.length < 7) { - throw new ReleaseManagerAsAServiceError( + throw new GitHubReleaseManagerError( 'Invalid shortCommitHash: less than 7 characters', ); } diff --git a/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.ts b/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.ts index c6169ac7f5..c76ef92a85 100644 --- a/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.ts +++ b/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ReleaseManagerAsAServiceError } from '../../errors/ReleaseManagerAsAServiceError'; +import { GitHubReleaseManagerError } from '../../errors/GitHubReleaseManagerError'; export type CalverTagParts = { prefix: string; @@ -27,7 +27,7 @@ export function getCalverTagParts(tag: string) { ); if (result === null || result.length < 4) { - throw new ReleaseManagerAsAServiceError('Invalid calver tag'); + throw new GitHubReleaseManagerError('Invalid calver tag'); } const tagParts: CalverTagParts = { diff --git a/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.ts b/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.ts index 8b2ae2873b..f6a97908e2 100644 --- a/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.ts +++ b/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.ts @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ReleaseManagerAsAServiceError } from '../../errors/ReleaseManagerAsAServiceError'; +import { GitHubReleaseManagerError } from '../../errors/GitHubReleaseManagerError'; export type SemverTagParts = { prefix: string; @@ -26,7 +26,7 @@ export function getSemverTagParts(tag: string) { const result = tag.match(/(rc|version)-([0-9]+)\.([0-9]+)\.([0-9]+)/); if (result === null || result.length < 4) { - throw new ReleaseManagerAsAServiceError('Invalid semver tag'); + throw new GitHubReleaseManagerError('Invalid semver tag'); } const tagParts: SemverTagParts = { diff --git a/plugins/github-release-manager/src/index.ts b/plugins/github-release-manager/src/index.ts index 6768091ee9..c4177089eb 100644 --- a/plugins/github-release-manager/src/index.ts +++ b/plugins/github-release-manager/src/index.ts @@ -13,7 +13,4 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { - gitHubReleaseManagerPlugin as releaseManagerAsAServicePlugin, - ReleaseManagerAsAServicePage, -} from './plugin'; +export { gitHubReleaseManagerPlugin, GitHubReleaseManagerPage } from './plugin'; diff --git a/plugins/github-release-manager/src/plugin.ts b/plugins/github-release-manager/src/plugin.ts index 276691762a..252b0c6674 100644 --- a/plugins/github-release-manager/src/plugin.ts +++ b/plugins/github-release-manager/src/plugin.ts @@ -43,7 +43,7 @@ export const gitHubReleaseManagerPlugin = createPlugin({ ], }); -export const ReleaseManagerAsAServicePage = gitHubReleaseManagerPlugin.provide( +export const GitHubReleaseManagerPage = gitHubReleaseManagerPlugin.provide( createRoutableExtension({ component: () => import('./GitHubReleaseManager').then(m => m.ReleaseManagerAsAService), From c98ffab90107de8322302af96d9d68a915af07be Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Tue, 13 Apr 2021 10:32:27 +0200 Subject: [PATCH 007/140] Fix broken link in dev README Signed-off-by: Erik Engervall --- plugins/github-release-manager/dev/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/github-release-manager/dev/README.md b/plugins/github-release-manager/dev/README.md index 736d20766e..52a8c8656b 100644 --- a/plugins/github-release-manager/dev/README.md +++ b/plugins/github-release-manager/dev/README.md @@ -10,4 +10,4 @@ Your plugin has been added to the example app in this repository, meaning you'll You can also serve the plugin in isolation by running `yarn start` in the plugin directory. This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. -It is only meant for local development, and the setup for it can be found inside the [/dev](./dev) directory. +It is only meant for local development, and the setup for it can be found inside the `/dev` directory. From 5300b2a3fa41670f98be0ddf483669460d59c991 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Tue, 13 Apr 2021 10:56:58 +0200 Subject: [PATCH 008/140] Renaming related fixes Signed-off-by: Erik Engervall --- packages/app/src/plugins.ts | 2 +- plugins/github-release-manager/README.md | 2 +- .../github-release-manager/src/GitHubReleaseManager.tsx | 8 ++++---- plugins/github-release-manager/src/plugin.ts | 2 +- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/packages/app/src/plugins.ts b/packages/app/src/plugins.ts index 744456bfaa..4099061428 100644 --- a/packages/app/src/plugins.ts +++ b/packages/app/src/plugins.ts @@ -46,4 +46,4 @@ export { plugin as Kafka } from '@backstage/plugin-kafka'; export { todoPlugin } from '@backstage/plugin-todo'; export { badgesPlugin } from '@backstage/plugin-badges'; export { githubDeploymentsPlugin } from '@backstage/plugin-github-deployments'; -export { releaseManagerAsAServicePlugin } from '@backstage/plugin-github-release-manager'; +export { gitHubReleaseManagerPlugin } from '@backstage/plugin-github-release-manager'; diff --git a/plugins/github-release-manager/README.md b/plugins/github-release-manager/README.md index 3411c51000..273e81b252 100644 --- a/plugins/github-release-manager/README.md +++ b/plugins/github-release-manager/README.md @@ -46,4 +46,4 @@ Looking at the flow above, a common release lifecycle could be: ## Usage -The plugin exports a single full-page extension `ReleaseManagerAsAServicePage`, which one can add to an app like a usual top-level tool on a dedicated route. +The plugin exports a single full-page extension `GitHubReleaseManagerPage`, which one can add to an app like a usual top-level tool on a dedicated route. diff --git a/plugins/github-release-manager/src/GitHubReleaseManager.tsx b/plugins/github-release-manager/src/GitHubReleaseManager.tsx index 2011fb40b0..78f7b14b69 100644 --- a/plugins/github-release-manager/src/GitHubReleaseManager.tsx +++ b/plugins/github-release-manager/src/GitHubReleaseManager.tsx @@ -41,7 +41,7 @@ import { } from './components/ProjectContext'; import { ApiClient } from './api/ApiClient'; -interface ReleaseManagerAsAServiceProps { +interface GitHubReleaseManagerProps { project: Project; components?: { default?: { @@ -71,10 +71,10 @@ const useStyles = makeStyles(() => ({ }, })); -export function ReleaseManagerAsAService({ +export function GitHubReleaseManager({ project, components, -}: ReleaseManagerAsAServiceProps) { +}: GitHubReleaseManagerProps) { const pluginApiClient = useApi(githubReleaseManagerApiRef); const apiClient = new ApiClient({ pluginApiClient, @@ -93,7 +93,7 @@ export function ReleaseManagerAsAService({ ); } -function Cards({ project, components }: ReleaseManagerAsAServiceProps) { +function Cards({ project, components }: GitHubReleaseManagerProps) { const apiClient = useApiClientContext(); const [refetch, setRefetch] = useState(0); const gitHubBatchInfo = useAsync(getGitHubBatchInfo({ apiClient }), [ diff --git a/plugins/github-release-manager/src/plugin.ts b/plugins/github-release-manager/src/plugin.ts index 252b0c6674..5f40d4581e 100644 --- a/plugins/github-release-manager/src/plugin.ts +++ b/plugins/github-release-manager/src/plugin.ts @@ -46,7 +46,7 @@ export const gitHubReleaseManagerPlugin = createPlugin({ export const GitHubReleaseManagerPage = gitHubReleaseManagerPlugin.provide( createRoutableExtension({ component: () => - import('./GitHubReleaseManager').then(m => m.ReleaseManagerAsAService), + import('./GitHubReleaseManager').then(m => m.GitHubReleaseManager), mountPoint: rootRouteRef, }), ); From e48f59a90b8ebf13c24a683dd174e6aa02ec1e63 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Tue, 13 Apr 2021 11:46:17 +0200 Subject: [PATCH 009/140] Upgrade required packages in plugin Signed-off-by: Erik Engervall --- plugins/github-release-manager/package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/github-release-manager/package.json b/plugins/github-release-manager/package.json index 230a1c4bb4..5eebc11105 100644 --- a/plugins/github-release-manager/package.json +++ b/plugins/github-release-manager/package.json @@ -20,7 +20,7 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.7.3", + "@backstage/core": "^0.7.4", "@backstage/integration": "^0.5.1", "@backstage/theme": "^0.2.5", "@material-ui/core": "^4.11.0", @@ -34,9 +34,9 @@ "react": "^16.13.1" }, "devDependencies": { - "@backstage/cli": "^0.6.6", + "@backstage/cli": "^0.6.7", "@backstage/dev-utils": "^0.1.13", - "@backstage/test-utils": "^0.1.9", + "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.0.7", From c003e06cc004cc085153112af6240a30e7d73200 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Wed, 14 Apr 2021 11:30:21 +0200 Subject: [PATCH 010/140] Address latest batch of comments Signed-off-by: Erik Engervall --- plugins/github-release-manager/README.md | 4 ++-- .../github-release-manager/src/api/ApiClient.ts | 4 ++++ .../src/api/PluginApiClientConfig.ts | 10 ++++++++-- .../src/cards/createRc/sideEffects/createRc.ts | 2 +- .../src/cards/info/Info.tsx | 6 +++--- .../src/cards/patchRc/PatchBody.tsx | 9 +++++---- .../ResponseStepList/ResponseStepList.tsx | 15 +++++++++------ .../ResponseStepList/ResponseStepListItem.tsx | 6 +++--- 8 files changed, 35 insertions(+), 21 deletions(-) diff --git a/plugins/github-release-manager/README.md b/plugins/github-release-manager/README.md index 273e81b252..331d04bafa 100644 --- a/plugins/github-release-manager/README.md +++ b/plugins/github-release-manager/README.md @@ -6,13 +6,13 @@ Does it build and ship your code? **No**. -What `GRM` does is manage your **[releases](https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/managing-releases-in-a-repository)** on GitHub, building and shipping is entirely up to you as a developer to handle in your CI. +What `GRM` does is manage your **[releases](https://docs.github.com/en/github/administering-a-repository/managing-releases-in-a-repository)** on GitHub, building and shipping is entirely up to you as a developer to handle in your CI. `GRM` is built with industry standards in mind and the flow is as follows: ![](./src/cards/info/flow.png) -> **GitHub**: The source control system where releases reside in a practical sense. Read more about GitHub releases [here](https://docs.github.com/en/free-pro-team@latest/github/administering-a-repository/. Note that this plugin works just as well with GitHub Enterprise) +> **GitHub**: The source control system where releases reside in a practical sense. Read more about [GitHub releases](https://docs.github.com/en/github/administering-a-repository/managing-releases-in-a-repository). Note that this plugin works just as well with GitHub Enterprise) > > **Release Candidate (RC)**: A GitHub pre-release intended primarily for internal testing > diff --git a/plugins/github-release-manager/src/api/ApiClient.ts b/plugins/github-release-manager/src/api/ApiClient.ts index a3c1ce35e5..dcb9ba2288 100644 --- a/plugins/github-release-manager/src/api/ApiClient.ts +++ b/plugins/github-release-manager/src/api/ApiClient.ts @@ -54,6 +54,10 @@ export class ApiClient { this.githubCommonPath = `/repos/${this.repoPath}`; } + public getHost() { + return this.pluginApiClient.host; + } + public getRepoPath() { return this.repoPath; } diff --git a/plugins/github-release-manager/src/api/PluginApiClientConfig.ts b/plugins/github-release-manager/src/api/PluginApiClientConfig.ts index 430b005b92..f06f25998c 100644 --- a/plugins/github-release-manager/src/api/PluginApiClientConfig.ts +++ b/plugins/github-release-manager/src/api/PluginApiClientConfig.ts @@ -19,7 +19,8 @@ import { readGitHubIntegrationConfigs } from '@backstage/integration'; export class PluginApiClientConfig { private readonly githubAuthApi: OAuthApi; - readonly baseUrl: string; + private readonly baseUrl: string; + readonly host: string; constructor({ configApi, @@ -33,7 +34,12 @@ export class PluginApiClientConfig { const configs = readGitHubIntegrationConfigs( configApi.getOptionalConfigArray('integrations.github') ?? [], ); - const githubIntegrationConfig = configs.find(v => v.host === 'github.com'); + + const githubIntegrationConfig = configs.find( + v => v.host === 'github.com' || v.host.startsWith('ghe.'), + ); + + this.host = githubIntegrationConfig?.host ?? 'github.com'; this.baseUrl = githubIntegrationConfig?.apiBaseUrl ?? 'https://api.github.com'; } diff --git a/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.ts b/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.ts index 0a532dcb0d..11f93aedf1 100644 --- a/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.ts +++ b/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.ts @@ -99,7 +99,7 @@ export async function createRc({ `; responseSteps.push({ - message: 'Fetched commit comparision', + message: 'Fetched commit comparison', secondaryMessage: `${previousReleaseBranch}...${nextReleaseBranch}`, link: comparison.html_url, }); diff --git a/plugins/github-release-manager/src/cards/info/Info.tsx b/plugins/github-release-manager/src/cards/info/Info.tsx index 6b371ea873..d05d16bf85 100644 --- a/plugins/github-release-manager/src/cards/info/Info.tsx +++ b/plugins/github-release-manager/src/cards/info/Info.tsx @@ -47,12 +47,12 @@ export const Info = ({ GitHub: The source control system where releases - reside in a practical sense. Read more about GitHub releases{' '} + reside in a practical sense. Read more about{' '} - here + GitHub releases . Note that this plugin works just as well with GitHub Enterprise (GHE) diff --git a/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx b/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx index 6d15bc6a08..80b53bb208 100644 --- a/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx +++ b/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx @@ -16,8 +16,6 @@ import React, { useState } from 'react'; import { Alert, AlertTitle } from '@material-ui/lab'; import { useAsync, useAsyncFn } from 'react-use'; -import FileCopyIcon from '@material-ui/icons/FileCopy'; -import Paper from '@material-ui/core/Paper'; import { Button, Checkbox, @@ -29,8 +27,10 @@ import { ListItemIcon, ListItemSecondaryAction, ListItemText, + Paper, Typography, } from '@material-ui/core'; +import FileCopyIcon from '@material-ui/icons/FileCopy'; import OpenInNewIcon from '@material-ui/icons/OpenInNew'; import { Differ } from '../../components/Differ'; @@ -229,12 +229,13 @@ export const PatchBody = ({ { const repoPath = apiClient.getRepoPath(); + const host = apiClient.getHost(); const newTab = window.open( - `https://github.com/${repoPath}/compare/${releaseBranch?.name}...${commit.sha}`, + `https://${host}/${repoPath}/compare/${releaseBranch?.name}...${commit.sha}`, '_blank', ); newTab?.focus(); diff --git a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.tsx b/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.tsx index e4a3d78308..239e642884 100644 --- a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.tsx +++ b/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.tsx @@ -14,12 +14,15 @@ * limitations under the License. */ import React, { PropsWithChildren } from 'react'; -import { List, CircularProgress } from '@material-ui/core'; -import Button from '@material-ui/core/Button'; -import Dialog from '@material-ui/core/Dialog'; -import DialogActions from '@material-ui/core/DialogActions'; -import DialogContent from '@material-ui/core/DialogContent'; -import DialogTitle from '@material-ui/core/DialogTitle'; +import { + List, + CircularProgress, + Button, + Dialog, + DialogActions, + DialogContent, + DialogTitle, +} from '@material-ui/core'; import { ResponseStep, SetRefetch } from '../../types/types'; import { TEST_IDS } from '../../test-helpers/test-ids'; diff --git a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepListItem.tsx b/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepListItem.tsx index fd139dae89..21486ed87f 100644 --- a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepListItem.tsx +++ b/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepListItem.tsx @@ -14,8 +14,8 @@ * limitations under the License. */ import React, { useEffect, useState } from 'react'; -import { green, red } from '@material-ui/core/colors'; import { + colors, IconButton, ListItem, ListItemIcon, @@ -78,7 +78,7 @@ export const ResponseStepListItem = ({ return ( ); } @@ -87,7 +87,7 @@ export const ResponseStepListItem = ({ return ( ); } From 9f2def6e0dc9ae1a89f49de2eb7e5aac8d71a070 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Wed, 14 Apr 2021 13:45:33 +0200 Subject: [PATCH 011/140] Fix snapshots for copy update Signed-off-by: Erik Engervall --- .../src/cards/createRc/sideEffects/createRc.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.test.ts b/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.test.ts index 407790f252..c50ab0a830 100644 --- a/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.test.ts +++ b/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.test.ts @@ -45,7 +45,7 @@ describe('createRc', () => { }, Object { "link": "mock_compareCommits_html_url", - "message": "Fetched commit comparision", + "message": "Fetched commit comparison", "secondaryMessage": "rc/1.2.3...rc/1.2.3", }, Object { From ef60f6be08cfb2d46791a0e7e0c0e2428800bc48 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Wed, 14 Apr 2021 16:07:48 +0200 Subject: [PATCH 012/140] Merge PluginApiClientConfig and ApiClient into PluginApiClient Signed-off-by: Erik Engervall --- packages/app/src/apis.ts | 11 ++ plugins/github-release-manager/dev/index.tsx | 1 + .../src/GitHubReleaseManager.tsx | 23 ++- .../api/{ApiClient.ts => PluginApiClient.ts} | 150 +++++++++++------- .../src/api/PluginApiClientConfig.ts | 57 ------- .../src/api/serviceApiRef.ts | 5 +- .../src/cards/createRc/CreateRc.test.tsx | 3 +- .../src/cards/createRc/CreateRc.tsx | 7 +- .../cards/createRc/getRcGitHubInfo.test.ts | 1 + .../src/cards/createRc/getRcGitHubInfo.ts | 1 + .../createRc/sideEffects/createRc.test.ts | 3 +- .../cards/createRc/sideEffects/createRc.ts | 17 +- .../src/cards/info/Info.test.tsx | 1 + .../src/cards/info/Info.tsx | 1 + .../src/cards/patchRc/Patch.test.tsx | 1 + .../src/cards/patchRc/Patch.tsx | 1 + .../src/cards/patchRc/PatchBody.test.tsx | 3 +- .../src/cards/patchRc/PatchBody.tsx | 17 +- .../cards/patchRc/sideEffects/patch.test.ts | 3 +- .../src/cards/patchRc/sideEffects/patch.ts | 39 +++-- .../src/cards/promoteRc/PromoteRc.test.tsx | 1 + .../src/cards/promoteRc/PromoteRc.tsx | 1 + .../cards/promoteRc/PromoteRcBody.test.tsx | 3 +- .../src/cards/promoteRc/PromoteRcBody.tsx | 12 +- .../promoteRc/sideEffects/promoteRc.test.ts | 3 +- .../cards/promoteRc/sideEffects/promoteRc.ts | 9 +- .../src/components/Differ.tsx | 1 + .../src/components/Divider.test.tsx | 1 + .../src/components/Divider.tsx | 1 + .../src/components/InfoCardPlus.test.tsx | 1 + .../src/components/InfoCardPlus.tsx | 1 + .../src/components/NoLatestRelease.test.tsx | 1 + .../src/components/NoLatestRelease.tsx | 1 + .../src/components/ProjectContext.ts | 17 +- .../src/components/ReloadButton.test.tsx | 1 + .../src/components/ReloadButton.tsx | 1 + .../ResponseStepList.test.tsx | 1 + .../ResponseStepList/ResponseStepList.tsx | 1 + .../ResponseStepListItem.test.tsx | 1 + .../ResponseStepList/ResponseStepListItem.tsx | 1 + .../src/constants/constants.test.ts | 1 + .../src/constants/constants.ts | 1 + .../src/errors/GitHubReleaseManagerError.ts | 1 + .../src/helpers/getBumpedTag.test.ts | 1 + .../src/helpers/getBumpedTag.ts | 1 + .../src/helpers/getShortCommitHash.test.ts | 1 + .../src/helpers/getShortCommitHash.ts | 1 + .../src/helpers/isCalverTagParts.test.ts | 1 + .../src/helpers/isCalverTagParts.ts | 1 + .../src/helpers/tagParts/getCalverTagParts.ts | 1 + .../src/helpers/tagParts/getSemverTagParts.ts | 1 + .../src/helpers/tagParts/getTagParts.test.ts | 1 + .../src/helpers/tagParts/getTagParts.ts | 1 + plugins/github-release-manager/src/index.ts | 7 +- .../github-release-manager/src/plugin.test.ts | 1 + plugins/github-release-manager/src/plugin.ts | 10 +- plugins/github-release-manager/src/routes.ts | 5 +- .../github-release-manager/src/setupTests.ts | 1 + .../src/sideEffects/getGitHubBatchInfo.ts | 13 +- .../src/sideEffects/getLatestRelease.test.ts | 5 +- .../src/sideEffects/getLatestRelease.ts | 11 +- .../src/styles/styles.ts | 1 + .../src/test-helpers/test-helpers.test.ts | 1 + .../src/test-helpers/test-helpers.ts | 1 + .../src/test-helpers/test-ids.ts | 1 + .../github-release-manager/src/types/types.ts | 1 + 66 files changed, 277 insertions(+), 198 deletions(-) rename plugins/github-release-manager/src/api/{ApiClient.ts => PluginApiClient.ts} (67%) delete mode 100644 plugins/github-release-manager/src/api/PluginApiClientConfig.ts diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index ccc576e727..beea444a6c 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -33,6 +33,7 @@ import { graphQlBrowseApiRef, GraphQLEndpoints, } from '@backstage/plugin-graphiql'; +// import { githubReleaseManagerApiRef } from '@backstage/plugin-github-release-manager'; export const apis: AnyApiFactory[] = [ createApiFactory({ @@ -41,6 +42,16 @@ export const apis: AnyApiFactory[] = [ factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi), }), + // createApiFactory({ + // api: githubReleaseManagerApiRef, + // deps: { githubAuthApi: githubAuthApiRef }, + // factory: ({ githubAuthApi }) => { + // return { + + // } + // }, + // }), + createApiFactory({ api: graphQlBrowseApiRef, deps: { errorApi: errorApiRef, githubAuthApi: githubAuthApiRef }, diff --git a/plugins/github-release-manager/dev/index.tsx b/plugins/github-release-manager/dev/index.tsx index d525b3f55b..b3d343382f 100644 --- a/plugins/github-release-manager/dev/index.tsx +++ b/plugins/github-release-manager/dev/index.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { createDevApp } from '@backstage/dev-utils'; diff --git a/plugins/github-release-manager/src/GitHubReleaseManager.tsx b/plugins/github-release-manager/src/GitHubReleaseManager.tsx index 78f7b14b69..82396438f0 100644 --- a/plugins/github-release-manager/src/GitHubReleaseManager.tsx +++ b/plugins/github-release-manager/src/GitHubReleaseManager.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { Alert } from '@material-ui/lab'; import { CircularProgress, makeStyles } from '@material-ui/core'; import { useAsync } from 'react-use'; @@ -36,10 +37,9 @@ import { import { PromoteRc } from './cards/promoteRc/PromoteRc'; import { githubReleaseManagerApiRef } from './api/serviceApiRef'; import { - ApiClientContext, - useApiClientContext, + PluginApiClientContext, + usePluginApiClientContext, } from './components/ProjectContext'; -import { ApiClient } from './api/ApiClient'; interface GitHubReleaseManagerProps { project: Project; @@ -76,30 +76,29 @@ export function GitHubReleaseManager({ components, }: GitHubReleaseManagerProps) { const pluginApiClient = useApi(githubReleaseManagerApiRef); - const apiClient = new ApiClient({ - pluginApiClient, + pluginApiClient.setRepoPath({ repoPath: `${project.github.org}/${project.github.repo}`, }); const classes = useStyles(); return ( - +
-
+ ); } function Cards({ project, components }: GitHubReleaseManagerProps) { - const apiClient = useApiClientContext(); + const pluginApiClient = usePluginApiClientContext(); const [refetch, setRefetch] = useState(0); - const gitHubBatchInfo = useAsync(getGitHubBatchInfo({ apiClient }), [ - project, - refetch, - ]); + const gitHubBatchInfo = useAsync( + getGitHubBatchInfo({ pluginApiClient: pluginApiClient }), + [project, refetch], + ); if (gitHubBatchInfo.error) { return {gitHubBatchInfo.error.message}; diff --git a/plugins/github-release-manager/src/api/ApiClient.ts b/plugins/github-release-manager/src/api/PluginApiClient.ts similarity index 67% rename from plugins/github-release-manager/src/api/ApiClient.ts rename to plugins/github-release-manager/src/api/PluginApiClient.ts index dcb9ba2288..c2bf8547d4 100644 --- a/plugins/github-release-manager/src/api/ApiClient.ts +++ b/plugins/github-release-manager/src/api/PluginApiClient.ts @@ -13,6 +13,11 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + +import { ConfigApi, OAuthApi } from '@backstage/core'; +import { Octokit } from '@octokit/rest'; +import { readGitHubIntegrationConfigs } from '@backstage/integration'; + import { GhCompareCommitsResponse, GhCreateCommitResponse, @@ -29,88 +34,123 @@ import { } from '../types/types'; import { CalverTagParts } from '../helpers/tagParts/getCalverTagParts'; import { getRcGitHubInfo } from '../cards/createRc/getRcGitHubInfo'; -import { PluginApiClientConfig } from './PluginApiClientConfig'; import { SemverTagParts } from '../helpers/tagParts/getSemverTagParts'; +import { GitHubReleaseManagerError } from '../errors/GitHubReleaseManagerError'; -/** - * Docs - * https://github.com/octokit/request.js/#the-data-parameter--set-request-body-directly - */ - -export class ApiClient { - private readonly pluginApiClient: PluginApiClientConfig; - private readonly repoPath: string; - private readonly githubCommonPath: string; +export class PluginApiClient { + private readonly githubAuthApi: OAuthApi; + private readonly baseUrl: string; + private repoPath?: string; + readonly host: string; constructor({ - pluginApiClient, - repoPath, + configApi, + githubAuthApi, }: { - pluginApiClient: PluginApiClientConfig; - repoPath: string; + configApi: ConfigApi; + githubAuthApi: OAuthApi; }) { - this.pluginApiClient = pluginApiClient; - this.repoPath = repoPath; - this.githubCommonPath = `/repos/${this.repoPath}`; + this.githubAuthApi = githubAuthApi; + + const githubIntegrationConfig = this.getGithubIntegrationConfig({ + configApi, + }); + + this.host = githubIntegrationConfig?.host ?? 'github.com'; + this.baseUrl = + githubIntegrationConfig?.apiBaseUrl ?? 'https://api.github.com'; + } + + private getGithubIntegrationConfig({ configApi }: { configApi: ConfigApi }) { + const configs = readGitHubIntegrationConfigs( + configApi.getOptionalConfigArray('integrations.github') ?? [], + ); + + const githubIntegrationConfig = configs.find( + v => v.host === 'github.com' || v.host.startsWith('ghe.'), + ); + + return githubIntegrationConfig; + } + + private async getOctokit() { + const token = await this.githubAuthApi.getAccessToken(['repo']); + + return { + octokit: new Octokit({ + auth: token, + baseUrl: this.baseUrl, + }), + }; } public getHost() { - return this.pluginApiClient.host; + return this.host; + } + + public setRepoPath({ repoPath }: { repoPath: string }) { + this.repoPath = repoPath; } public getRepoPath() { + if (!this.repoPath) { + throw new GitHubReleaseManagerError('Could not find repoPath'); + } + return this.repoPath; } async getRecentCommits({ releaseBranchName, }: { releaseBranchName?: string } = {}) { - const { octokit } = await this.pluginApiClient.getOctokit(); + const { octokit } = await this.getOctokit(); const sha = releaseBranchName ? `?sha=${releaseBranchName}` : ''; const recentCommits: GhGetCommitResponse[] = ( - await octokit.request(`${this.githubCommonPath}/commits${sha}`) + await octokit.request(`/repos/${this.getRepoPath()}/commits${sha}`) ).data; return { recentCommits }; } async getReleases() { - const { octokit } = await this.pluginApiClient.getOctokit(); + const { octokit } = await this.getOctokit(); const releases: GhGetReleaseResponse[] = ( - await octokit.request(`${this.githubCommonPath}/releases`) + await octokit.request(`/repos/${this.getRepoPath()}/releases`) ).data; return { releases }; } async getRelease({ releaseId }: { releaseId: number }) { - const { octokit } = await this.pluginApiClient.getOctokit(); + const { octokit } = await this.getOctokit(); const latestRelease: GhGetReleaseResponse = ( - await octokit.request(`${this.githubCommonPath}/releases/${releaseId}`) + await octokit.request( + `/repos/${this.getRepoPath()}/releases/${releaseId}`, + ) ).data; return { latestRelease }; } async getRepository() { - const { octokit } = await this.pluginApiClient.getOctokit(); + const { octokit } = await this.getOctokit(); const repository: GhGetRepositoryResponse = ( - await octokit.request(this.githubCommonPath) + await octokit.request(`/repos/${this.getRepoPath()}`) ).data; return { repository }; } async getLatestCommit({ defaultBranch }: { defaultBranch: string }) { - const { octokit } = await this.pluginApiClient.getOctokit(); + const { octokit } = await this.getOctokit(); const latestCommit: GhGetCommitResponse = ( await octokit.request( - `${this.githubCommonPath}/commits/refs/heads/${defaultBranch}`, + `/repos/${this.getRepoPath()}/commits/refs/heads/${defaultBranch}`, ) ).data; @@ -118,10 +158,12 @@ export class ApiClient { } async getBranch({ branchName }: { branchName: string }) { - const { octokit } = await this.pluginApiClient.getOctokit(); + const { octokit } = await this.getOctokit(); const branch: GhGetBranchResponse = ( - await octokit.request(`${this.githubCommonPath}/branches/${branchName}`) + await octokit.request( + `/repos/${this.getRepoPath()}/branches/${branchName}`, + ) ).data; return { branch }; @@ -135,10 +177,10 @@ export class ApiClient { mostRecentSha: string; targetBranch: string; }) => { - const { octokit } = await this.pluginApiClient.getOctokit(); + const { octokit } = await this.getOctokit(); const createdRef: GhCreateReferenceResponse = ( - await octokit.request(`${this.githubCommonPath}/git/refs`, { + await octokit.request(`/repos/${this.getRepoPath()}/git/refs`, { method: 'POST', data: { ref: `refs/heads/${targetBranch}`, @@ -157,11 +199,11 @@ export class ApiClient { previousReleaseBranch: string; nextReleaseBranch: string; }) => { - const { octokit } = await this.pluginApiClient.getOctokit(); + const { octokit } = await this.getOctokit(); const comparison: GhCompareCommitsResponse = ( await octokit.request( - `${this.githubCommonPath}/compare/${previousReleaseBranch}...${nextReleaseBranch}`, + `/repos/${this.getRepoPath()}/compare/${previousReleaseBranch}...${nextReleaseBranch}`, ) ).data; @@ -175,10 +217,10 @@ export class ApiClient { nextGitHubInfo: ReturnType; releaseBody: string; }) => { - const { octokit } = await this.pluginApiClient.getOctokit(); + const { octokit } = await this.getOctokit(); const createReleaseResponse: GhCreateReleaseResponse = ( - await octokit.request(`${this.githubCommonPath}/releases`, { + await octokit.request(`/repos/${this.getRepoPath()}/releases`, { method: 'POST', data: { tag_name: nextGitHubInfo.rcReleaseTag, @@ -204,10 +246,10 @@ export class ApiClient { releaseBranchTree: string; selectedPatchCommit: GhGetCommitResponse; }) => { - const { octokit } = await this.pluginApiClient.getOctokit(); + const { octokit } = await this.getOctokit(); const tempCommit: GhCreateCommitResponse = ( - await octokit.request(`${this.githubCommonPath}/git/commits`, { + await octokit.request(`/repos/${this.getRepoPath()}/git/commits`, { method: 'POST', data: { message: `Temporary commit for patch ${tagParts.patch}`, @@ -227,10 +269,10 @@ export class ApiClient { releaseBranchName: string; tempCommit: GhCreateCommitResponse; }) => { - const { octokit } = await this.pluginApiClient.getOctokit(); + const { octokit } = await this.getOctokit(); await octokit.request( - `${this.githubCommonPath}/git/refs/heads/${releaseBranchName}`, + `/repos/${this.getRepoPath()}/git/refs/heads/${releaseBranchName}`, { method: 'PATCH', data: { @@ -242,10 +284,10 @@ export class ApiClient { }, merge: async ({ base, head }: { base: string; head: string }) => { - const { octokit } = await this.pluginApiClient.getOctokit(); + const { octokit } = await this.getOctokit(); const merge: GhMergeResponse = ( - await octokit.request(`${this.githubCommonPath}/merges`, { + await octokit.request(`/repos/${this.getRepoPath()}/merges`, { method: 'POST', data: { base, head }, }) @@ -265,10 +307,10 @@ export class ApiClient { mergeTree: string; releaseBranchSha: string; }) => { - const { octokit } = await this.pluginApiClient.getOctokit(); + const { octokit } = await this.getOctokit(); const cherryPickCommit: GhCreateCommitResponse = ( - await octokit.request(`${this.githubCommonPath}/git/commits`, { + await octokit.request(`/repos/${this.getRepoPath()}/git/commits`, { method: 'POST', data: { message: `[patch ${bumpedTag}] ${selectedPatchCommit.commit.message}`, @@ -288,11 +330,11 @@ export class ApiClient { releaseBranchName: string; cherryPickCommit: GhCreateCommitResponse; }) => { - const { octokit } = await this.pluginApiClient.getOctokit(); + const { octokit } = await this.getOctokit(); const updatedReference: GhUpdateReferenceResponse = ( await octokit.request( - `${this.githubCommonPath}/git/refs/heads/${releaseBranchName}`, + `/repos/${this.getRepoPath()}/git/refs/heads/${releaseBranchName}`, { method: 'PATCH', data: { @@ -313,10 +355,10 @@ export class ApiClient { bumpedTag: string; updatedReference: GhUpdateReferenceResponse; }) => { - const { octokit } = await this.pluginApiClient.getOctokit(); + const { octokit } = await this.getOctokit(); const tagObjectResponse: GhCreateTagObjectResponse = ( - await octokit.request(`${this.githubCommonPath}/git/tags`, { + await octokit.request(`/repos/${this.getRepoPath()}/git/tags`, { method: 'POST', data: { type: 'commit', @@ -338,10 +380,10 @@ export class ApiClient { bumpedTag: string; tagObjectResponse: GhCreateTagObjectResponse; }) => { - const { octokit } = await this.pluginApiClient.getOctokit(); + const { octokit } = await this.getOctokit(); const reference: GhCreateReferenceResponse = ( - await octokit.request(`${this.githubCommonPath}/git/refs`, { + await octokit.request(`/repos/${this.getRepoPath()}/git/refs`, { method: 'POST', data: { ref: `refs/tags/${bumpedTag}`, @@ -364,11 +406,11 @@ export class ApiClient { tagParts: SemverTagParts | CalverTagParts; selectedPatchCommit: GhGetCommitResponse; }) => { - const { octokit } = await this.pluginApiClient.getOctokit(); + const { octokit } = await this.getOctokit(); const release: GhUpdateReleaseResponse = ( await octokit.request( - `${this.githubCommonPath}/releases/${latestRelease.id}`, + `/repos/${this.getRepoPath()}/releases/${latestRelease.id}`, { method: 'PATCH', data: { @@ -395,11 +437,11 @@ export class ApiClient { releaseId: GhGetReleaseResponse['id']; releaseVersion: string; }) => { - const { octokit } = await this.pluginApiClient.getOctokit(); + const { octokit } = await this.getOctokit(); const release: GhGetReleaseResponse = ( await octokit.request( - `${this.githubCommonPath}/releases/${releaseId}`, + `/repos/${this.getRepoPath()}/releases/${releaseId}`, { method: 'PATCH', data: { diff --git a/plugins/github-release-manager/src/api/PluginApiClientConfig.ts b/plugins/github-release-manager/src/api/PluginApiClientConfig.ts deleted file mode 100644 index f06f25998c..0000000000 --- a/plugins/github-release-manager/src/api/PluginApiClientConfig.ts +++ /dev/null @@ -1,57 +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 { ConfigApi, OAuthApi } from '@backstage/core'; -import { Octokit } from '@octokit/rest'; -import { readGitHubIntegrationConfigs } from '@backstage/integration'; - -export class PluginApiClientConfig { - private readonly githubAuthApi: OAuthApi; - private readonly baseUrl: string; - readonly host: string; - - constructor({ - configApi, - githubAuthApi, - }: { - configApi: ConfigApi; - githubAuthApi: OAuthApi; - }) { - this.githubAuthApi = githubAuthApi; - - const configs = readGitHubIntegrationConfigs( - configApi.getOptionalConfigArray('integrations.github') ?? [], - ); - - const githubIntegrationConfig = configs.find( - v => v.host === 'github.com' || v.host.startsWith('ghe.'), - ); - - this.host = githubIntegrationConfig?.host ?? 'github.com'; - this.baseUrl = - githubIntegrationConfig?.apiBaseUrl ?? 'https://api.github.com'; - } - - public async getOctokit() { - const token = await this.githubAuthApi.getAccessToken(['repo']); - - return { - octokit: new Octokit({ - auth: token, - baseUrl: this.baseUrl, - }), - }; - } -} diff --git a/plugins/github-release-manager/src/api/serviceApiRef.ts b/plugins/github-release-manager/src/api/serviceApiRef.ts index e55e99b072..ea5eb7fee3 100644 --- a/plugins/github-release-manager/src/api/serviceApiRef.ts +++ b/plugins/github-release-manager/src/api/serviceApiRef.ts @@ -13,11 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { createApiRef } from '@backstage/core'; -import { PluginApiClientConfig } from './PluginApiClientConfig'; +import { PluginApiClient } from './PluginApiClient'; -export const githubReleaseManagerApiRef = createApiRef({ +export const githubReleaseManagerApiRef = createApiRef({ id: 'plugin.github-release-manager.service', description: 'Used by the GitHub Release Manager plugin to make requests', }); diff --git a/plugins/github-release-manager/src/cards/createRc/CreateRc.test.tsx b/plugins/github-release-manager/src/cards/createRc/CreateRc.test.tsx index 07582351e8..6a918c68eb 100644 --- a/plugins/github-release-manager/src/cards/createRc/CreateRc.test.tsx +++ b/plugins/github-release-manager/src/cards/createRc/CreateRc.test.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { render } from '@testing-library/react'; @@ -28,7 +29,7 @@ import { import { TEST_IDS } from '../../test-helpers/test-ids'; jest.mock('../../components/ProjectContext', () => ({ - useApiClientContext: () => mockApiClient, + usePluginApiClientContext: () => mockApiClient, })); jest.mock('./getRcGitHubInfo', () => ({ getRcGitHubInfo: () => mockNextGitHubInfo, diff --git a/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx b/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx index 8c4a5bff87..c02fba01d5 100644 --- a/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx +++ b/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React, { useState, useEffect } from 'react'; import { Alert } from '@material-ui/lab'; import { @@ -39,7 +40,7 @@ import { } from '../../types/types'; import { ResponseStepList } from '../../components/ResponseStepList/ResponseStepList'; import { useStyles } from '../../styles/styles'; -import { useApiClientContext } from '../../components/ProjectContext'; +import { usePluginApiClientContext } from '../../components/ProjectContext'; import { SEMVER_PARTS } from '../../constants/constants'; import { TEST_IDS } from '../../test-helpers/test-ids'; @@ -60,7 +61,7 @@ export const CreateRc = ({ setRefetch, successCb, }: CreateRcProps) => { - const apiClient = useApiClientContext(); + const pluginApiClient = usePluginApiClientContext(); const classes = useStyles(); const [semverBumpLevel, setSemverBumpLevel] = useState<'major' | 'minor'>( @@ -79,7 +80,7 @@ export const CreateRc = ({ const [createGitHubReleaseResponse, createGitHubReleaseFn] = useAsyncFn( (...args) => createRc({ - apiClient, + pluginApiClient, defaultBranch, latestRelease, nextGitHubInfo: args[0], diff --git a/plugins/github-release-manager/src/cards/createRc/getRcGitHubInfo.test.ts b/plugins/github-release-manager/src/cards/createRc/getRcGitHubInfo.test.ts index 59676e989b..08eeacdcc9 100644 --- a/plugins/github-release-manager/src/cards/createRc/getRcGitHubInfo.test.ts +++ b/plugins/github-release-manager/src/cards/createRc/getRcGitHubInfo.test.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { DateTime } from 'luxon'; import { GhGetReleaseResponse } from '../../types/types'; diff --git a/plugins/github-release-manager/src/cards/createRc/getRcGitHubInfo.ts b/plugins/github-release-manager/src/cards/createRc/getRcGitHubInfo.ts index df58f73f11..b507f84b20 100644 --- a/plugins/github-release-manager/src/cards/createRc/getRcGitHubInfo.ts +++ b/plugins/github-release-manager/src/cards/createRc/getRcGitHubInfo.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { DateTime } from 'luxon'; import { getBumpedSemverTagParts } from '../../helpers/getBumpedTag'; diff --git a/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.test.ts b/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.test.ts index c50ab0a830..713f0d12f2 100644 --- a/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.test.ts +++ b/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.test.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { mockApiClient, mockDefaultBranch, @@ -26,7 +27,7 @@ describe('createRc', () => { it('should work', async () => { const result = await createRc({ - apiClient: mockApiClient, + pluginApiClient: mockApiClient, defaultBranch: mockDefaultBranch, latestRelease: mockReleaseVersion, nextGitHubInfo: mockNextGitHubInfo, diff --git a/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.ts b/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.ts index 11f93aedf1..433ba8e40d 100644 --- a/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.ts +++ b/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { getRcGitHubInfo } from '../getRcGitHubInfo'; import { ComponentConfigCreateRc, @@ -21,11 +22,11 @@ import { GhGetRepositoryResponse, ResponseStep, } from '../../../types/types'; -import { ApiClient } from '../../../api/ApiClient'; +import { PluginApiClient } from '../../../api/PluginApiClient'; import { GitHubReleaseManagerError } from '../../../errors/GitHubReleaseManagerError'; interface CreateRC { - apiClient: ApiClient; + pluginApiClient: PluginApiClient; defaultBranch: GhGetRepositoryResponse['default_branch']; latestRelease: GhGetReleaseResponse | null; nextGitHubInfo: ReturnType; @@ -33,7 +34,7 @@ interface CreateRC { } export async function createRc({ - apiClient, + pluginApiClient, defaultBranch, latestRelease, nextGitHubInfo, @@ -44,7 +45,7 @@ export async function createRc({ /** * 1. Get the default branch's most recent commit */ - const { latestCommit } = await apiClient.getLatestCommit({ + const { latestCommit } = await pluginApiClient.getLatestCommit({ defaultBranch, }); responseSteps.push({ @@ -60,7 +61,7 @@ export async function createRc({ let createdRef: GhCreateReferenceResponse; try { createdRef = ( - await apiClient.createRc.createRef({ + await pluginApiClient.createRc.createRef({ mostRecentSha, targetBranch: nextGitHubInfo.rcBranch, }) @@ -85,7 +86,7 @@ export async function createRc({ ? latestRelease.target_commitish : defaultBranch; const nextReleaseBranch = nextGitHubInfo.rcBranch; - const { comparison } = await apiClient.createRc.getComparison({ + const { comparison } = await pluginApiClient.createRc.getComparison({ previousReleaseBranch, nextReleaseBranch, }); @@ -107,7 +108,9 @@ export async function createRc({ /** * 4. Creates the release itself in GitHub */ - const { createReleaseResponse } = await apiClient.createRc.createRelease({ + const { + createReleaseResponse, + } = await pluginApiClient.createRc.createRelease({ nextGitHubInfo: nextGitHubInfo, releaseBody, }); diff --git a/plugins/github-release-manager/src/cards/info/Info.test.tsx b/plugins/github-release-manager/src/cards/info/Info.test.tsx index 2c1176aa62..ac278b04c9 100644 --- a/plugins/github-release-manager/src/cards/info/Info.test.tsx +++ b/plugins/github-release-manager/src/cards/info/Info.test.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { render } from '@testing-library/react'; diff --git a/plugins/github-release-manager/src/cards/info/Info.tsx b/plugins/github-release-manager/src/cards/info/Info.tsx index d05d16bf85..f74065d78a 100644 --- a/plugins/github-release-manager/src/cards/info/Info.tsx +++ b/plugins/github-release-manager/src/cards/info/Info.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { Link, Typography } from '@material-ui/core'; diff --git a/plugins/github-release-manager/src/cards/patchRc/Patch.test.tsx b/plugins/github-release-manager/src/cards/patchRc/Patch.test.tsx index 13e1045b6e..fd81b2cd34 100644 --- a/plugins/github-release-manager/src/cards/patchRc/Patch.test.tsx +++ b/plugins/github-release-manager/src/cards/patchRc/Patch.test.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { render } from '@testing-library/react'; diff --git a/plugins/github-release-manager/src/cards/patchRc/Patch.tsx b/plugins/github-release-manager/src/cards/patchRc/Patch.tsx index 0b5a9fd9aa..2bc4ebf652 100644 --- a/plugins/github-release-manager/src/cards/patchRc/Patch.tsx +++ b/plugins/github-release-manager/src/cards/patchRc/Patch.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { Typography } from '@material-ui/core'; diff --git a/plugins/github-release-manager/src/cards/patchRc/PatchBody.test.tsx b/plugins/github-release-manager/src/cards/patchRc/PatchBody.test.tsx index 7ddc6308cf..c6020ed3c6 100644 --- a/plugins/github-release-manager/src/cards/patchRc/PatchBody.test.tsx +++ b/plugins/github-release-manager/src/cards/patchRc/PatchBody.test.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { render, waitFor, screen } from '@testing-library/react'; @@ -26,7 +27,7 @@ import { } from '../../test-helpers/test-helpers'; jest.mock('../../components/ProjectContext', () => ({ - useApiClientContext: () => mockApiClient, + usePluginApiClientContext: () => mockApiClient, })); import { PatchBody } from './PatchBody'; diff --git a/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx b/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx index 80b53bb208..5f14286ed3 100644 --- a/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx +++ b/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React, { useState } from 'react'; import { Alert, AlertTitle } from '@material-ui/lab'; import { useAsync, useAsyncFn } from 'react-use'; @@ -44,7 +45,7 @@ import { import { CalverTagParts } from '../../helpers/tagParts/getCalverTagParts'; import { ResponseStepList } from '../../components/ResponseStepList/ResponseStepList'; import { SemverTagParts } from '../../helpers/tagParts/getSemverTagParts'; -import { useApiClientContext } from '../../components/ProjectContext'; +import { usePluginApiClientContext } from '../../components/ProjectContext'; import { useStyles } from '../../styles/styles'; import { TEST_IDS } from '../../test-helpers/test-ids'; import { patch } from './sideEffects/patch'; @@ -66,7 +67,7 @@ export const PatchBody = ({ successCb, tagParts, }: PatchBodyProps) => { - const apiClient = useApiClientContext(); + const pluginApiClient = usePluginApiClientContext(); const [checkedCommitIndex, setCheckedCommitIndex] = useState(-1); const githubDataResponse = useAsync(async () => { @@ -74,13 +75,13 @@ export const PatchBody = ({ { branch: releaseBranchResponse }, { recentCommits }, ] = await Promise.all([ - apiClient.getBranch({ branchName: latestRelease.target_commitish }), - apiClient.getRecentCommits(), + pluginApiClient.getBranch({ branchName: latestRelease.target_commitish }), + pluginApiClient.getRecentCommits(), ]); const { recentCommits: recentReleaseBranchCommits, - } = await apiClient.getRecentCommits({ + } = await pluginApiClient.getRecentCommits({ releaseBranchName: releaseBranchResponse.name, }); @@ -94,7 +95,7 @@ export const PatchBody = ({ const [patchReleaseResponse, patchReleaseFn] = useAsyncFn(async (...args) => { const selectedPatchCommit: GhGetCommitResponse = args[0]; const patchResponseSteps = await patch({ - apiClient, + pluginApiClient, bumpedTag, latestRelease, selectedPatchCommit, @@ -231,8 +232,8 @@ export const PatchBody = ({ aria-label="commit" disabled={commitExistsOnReleaseBranch || !releaseBranch} onClick={() => { - const repoPath = apiClient.getRepoPath(); - const host = apiClient.getHost(); + const repoPath = pluginApiClient.getRepoPath(); + const host = pluginApiClient.getHost(); const newTab = window.open( `https://${host}/${repoPath}/compare/${releaseBranch?.name}...${commit.sha}`, diff --git a/plugins/github-release-manager/src/cards/patchRc/sideEffects/patch.test.ts b/plugins/github-release-manager/src/cards/patchRc/sideEffects/patch.test.ts index 7a37c0ce72..2b3b9bf3b2 100644 --- a/plugins/github-release-manager/src/cards/patchRc/sideEffects/patch.test.ts +++ b/plugins/github-release-manager/src/cards/patchRc/sideEffects/patch.test.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { mockBumpedTag, mockReleaseVersion, @@ -27,7 +28,7 @@ describe('patch', () => { it('should work', async () => { const result = await patch({ - apiClient: mockApiClient, + pluginApiClient: mockApiClient, latestRelease: mockReleaseVersion, bumpedTag: mockBumpedTag, selectedPatchCommit: mockSelectedPatchCommit, diff --git a/plugins/github-release-manager/src/cards/patchRc/sideEffects/patch.ts b/plugins/github-release-manager/src/cards/patchRc/sideEffects/patch.ts index 1d592b330b..1c97a8f0f6 100644 --- a/plugins/github-release-manager/src/cards/patchRc/sideEffects/patch.ts +++ b/plugins/github-release-manager/src/cards/patchRc/sideEffects/patch.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { ComponentConfigPatch, GhGetCommitResponse, @@ -21,11 +22,11 @@ import { } from '../../../types/types'; import { CalverTagParts } from '../../../helpers/tagParts/getCalverTagParts'; import { GitHubReleaseManagerError } from '../../../errors/GitHubReleaseManagerError'; -import { ApiClient } from '../../../api/ApiClient'; +import { PluginApiClient } from '../../../api/PluginApiClient'; import { SemverTagParts } from '../../../helpers/tagParts/getSemverTagParts'; interface Patch { - apiClient: ApiClient; + pluginApiClient: PluginApiClient; bumpedTag: string; latestRelease: GhGetReleaseResponse; selectedPatchCommit: GhGetCommitResponse; @@ -35,7 +36,7 @@ interface Patch { // Inspo: https://stackoverflow.com/questions/53859199/how-to-cherry-pick-through-githubs-api export async function patch({ - apiClient, + pluginApiClient, bumpedTag, latestRelease, selectedPatchCommit, @@ -55,7 +56,7 @@ export async function patch({ * > branchSha = branch.commit.sha * > branchTree = branch.commit.commit.tree.sha */ - const { branch: releaseBranch } = await apiClient.getBranch({ + const { branch: releaseBranch } = await pluginApiClient.getBranch({ branchName: releaseBranchName, }); const releaseBranchSha = releaseBranch.commit.sha; @@ -71,7 +72,7 @@ export async function patch({ * > parentSha = commit.parents.head // first parent -- there should only be one * > tempCommit = POST /repos/$owner/$repo/git/commits { "message": "temp", "tree": branchTree, "parents": [parentSha] } */ - const { tempCommit } = await apiClient.patch.createTempCommit({ + const { tempCommit } = await pluginApiClient.patch.createTempCommit({ releaseBranchTree, selectedPatchCommit, tagParts, @@ -85,7 +86,7 @@ export async function patch({ * 3. Now temporarily force the branch over to that commit: * > PATCH /repos/$owner/$repo/git/refs/heads/$refName { sha = tempCommit.sha, force = true } */ - await apiClient.patch.forceBranchHeadToTempCommit({ + await pluginApiClient.patch.forceBranchHeadToTempCommit({ tempCommit, releaseBranchName, }); @@ -94,7 +95,7 @@ export async function patch({ * 4. Merge the commit we want into this mess: * > merge = POST /repos/$owner/$repo/merges { "base": branchName, "head": commit.sha } */ - const { merge } = await apiClient.patch.merge({ + const { merge } = await pluginApiClient.patch.merge({ base: releaseBranchName, head: selectedPatchCommit.sha, }); @@ -115,7 +116,9 @@ export async function patch({ * Note that branchSha is the original from up at the top. * > cherry = POST /repos/$owner/$repo/git/commits { "message": "looks good!", "tree": mergeTree, "parents": [branchSha] } */ - const { cherryPickCommit } = await apiClient.patch.createCherryPickCommit({ + const { + cherryPickCommit, + } = await pluginApiClient.patch.createCherryPickCommit({ bumpedTag, mergeTree, releaseBranchSha, @@ -130,7 +133,7 @@ export async function patch({ * 6. Replace the temp commit with the real commit: * > PATCH /repos/$owner/$repo/git/refs/heads/$refName { sha = cherry.sha, force = true } */ - const { updatedReference } = await apiClient.patch.replaceTempCommit({ + const { updatedReference } = await pluginApiClient.patch.replaceTempCommit({ cherryPickCommit, releaseBranchName, }); @@ -142,7 +145,7 @@ export async function patch({ * 7. Create tag object: https://developer.github.com/v3/git/tags/#create-a-tag-object * > POST /repos/:owner/:repo/git/tags */ - const { tagObjectResponse } = await apiClient.patch.createTagObject({ + const { tagObjectResponse } = await pluginApiClient.patch.createTagObject({ bumpedTag, updatedReference, }); @@ -155,7 +158,7 @@ export async function patch({ * 8. Create a reference: https://developer.github.com/v3/git/refs/#create-a-reference * > POST /repos/:owner/:repo/git/refs */ - const { reference } = await apiClient.patch.createReference({ + const { reference } = await pluginApiClient.patch.createReference({ bumpedTag, tagObjectResponse, }); @@ -167,12 +170,14 @@ export async function patch({ /** * 9. Update release */ - const { release: updatedRelease } = await apiClient.patch.updateRelease({ - bumpedTag, - latestRelease, - selectedPatchCommit, - tagParts, - }); + const { release: updatedRelease } = await pluginApiClient.patch.updateRelease( + { + bumpedTag, + latestRelease, + selectedPatchCommit, + tagParts, + }, + ); responseSteps.push({ message: `Updated release "${updatedRelease.name}"`, secondaryMessage: `with tag ${updatedRelease.tag_name}`, diff --git a/plugins/github-release-manager/src/cards/promoteRc/PromoteRc.test.tsx b/plugins/github-release-manager/src/cards/promoteRc/PromoteRc.test.tsx index d38b2e0991..d2602218f9 100644 --- a/plugins/github-release-manager/src/cards/promoteRc/PromoteRc.test.tsx +++ b/plugins/github-release-manager/src/cards/promoteRc/PromoteRc.test.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { render } from '@testing-library/react'; diff --git a/plugins/github-release-manager/src/cards/promoteRc/PromoteRc.tsx b/plugins/github-release-manager/src/cards/promoteRc/PromoteRc.tsx index e7583e7085..d30271be48 100644 --- a/plugins/github-release-manager/src/cards/promoteRc/PromoteRc.tsx +++ b/plugins/github-release-manager/src/cards/promoteRc/PromoteRc.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { Alert, AlertTitle } from '@material-ui/lab'; import { Typography } from '@material-ui/core'; diff --git a/plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.test.tsx b/plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.test.tsx index 4e78e117b6..0dac702791 100644 --- a/plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.test.tsx +++ b/plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.test.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { render } from '@testing-library/react'; @@ -20,7 +21,7 @@ import { mockRcRelease, mockApiClient } from '../../test-helpers/test-helpers'; import { TEST_IDS } from '../../test-helpers/test-ids'; jest.mock('../../components/ProjectContext', () => ({ - useApiClientContext: () => mockApiClient, + usePluginApiClientContext: () => mockApiClient, })); import { PromoteRcBody } from './PromoteRcBody'; diff --git a/plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.tsx b/plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.tsx index 99ca136fac..ef9a67d89f 100644 --- a/plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.tsx +++ b/plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.tsx @@ -13,6 +13,7 @@ * 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 { Button, Typography } from '@material-ui/core'; @@ -26,7 +27,7 @@ import { } from '../../types/types'; import { promoteRc } from './sideEffects/promoteRc'; import { ResponseStepList } from '../../components/ResponseStepList/ResponseStepList'; -import { useApiClientContext } from '../../components/ProjectContext'; +import { usePluginApiClientContext } from '../../components/ProjectContext'; import { useStyles } from '../../styles/styles'; import { TEST_IDS } from '../../test-helpers/test-ids'; @@ -41,11 +42,16 @@ export const PromoteRcBody = ({ setRefetch, successCb, }: PromoteRcBodyProps) => { - const apiClient = useApiClientContext(); + const pluginApiClient = usePluginApiClientContext(); const classes = useStyles(); const releaseVersion = rcRelease.tag_name.replace('rc-', 'version-'); const [promoteGitHubRcResponse, promoseGitHubRcFn] = useAsyncFn( - promoteRc({ apiClient, rcRelease, releaseVersion, successCb }), + promoteRc({ + pluginApiClient, + rcRelease, + releaseVersion, + successCb, + }), ); if (promoteGitHubRcResponse.error) { diff --git a/plugins/github-release-manager/src/cards/promoteRc/sideEffects/promoteRc.test.ts b/plugins/github-release-manager/src/cards/promoteRc/sideEffects/promoteRc.test.ts index a64cf70f81..e57ccd5898 100644 --- a/plugins/github-release-manager/src/cards/promoteRc/sideEffects/promoteRc.test.ts +++ b/plugins/github-release-manager/src/cards/promoteRc/sideEffects/promoteRc.test.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { mockRcRelease, mockApiClient, @@ -24,7 +25,7 @@ describe('promoteRc', () => { it('should work', async () => { const result = await promoteRc({ - apiClient: mockApiClient, + pluginApiClient: mockApiClient, rcRelease: mockRcRelease, releaseVersion: 'version-1.2.3', })(); diff --git a/plugins/github-release-manager/src/cards/promoteRc/sideEffects/promoteRc.ts b/plugins/github-release-manager/src/cards/promoteRc/sideEffects/promoteRc.ts index 34085a3d12..e27f26289a 100644 --- a/plugins/github-release-manager/src/cards/promoteRc/sideEffects/promoteRc.ts +++ b/plugins/github-release-manager/src/cards/promoteRc/sideEffects/promoteRc.ts @@ -13,22 +13,23 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { ComponentConfigPromoteRc, GhGetReleaseResponse, ResponseStep, } from '../../../types/types'; -import { ApiClient } from '../../../api/ApiClient'; +import { PluginApiClient } from '../../../api/PluginApiClient'; interface PromoteRc { - apiClient: ApiClient; + pluginApiClient: PluginApiClient; rcRelease: GhGetReleaseResponse; releaseVersion: string; successCb?: ComponentConfigPromoteRc['successCb']; } export function promoteRc({ - apiClient, + pluginApiClient, rcRelease, releaseVersion, successCb, @@ -36,7 +37,7 @@ export function promoteRc({ return async (): Promise => { const responseSteps: ResponseStep[] = []; - const { release } = await apiClient.promoteRc.promoteRelease({ + const { release } = await pluginApiClient.promoteRc.promoteRelease({ releaseId: rcRelease.id, releaseVersion, }); diff --git a/plugins/github-release-manager/src/components/Differ.tsx b/plugins/github-release-manager/src/components/Differ.tsx index 02bf60ce46..05241e43df 100644 --- a/plugins/github-release-manager/src/components/Differ.tsx +++ b/plugins/github-release-manager/src/components/Differ.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React, { ReactNode } from 'react'; import { grey } from '@material-ui/core/colors'; import CallSplitIcon from '@material-ui/icons/CallSplit'; diff --git a/plugins/github-release-manager/src/components/Divider.test.tsx b/plugins/github-release-manager/src/components/Divider.test.tsx index 7a5ac8ad09..35e725d2ee 100644 --- a/plugins/github-release-manager/src/components/Divider.test.tsx +++ b/plugins/github-release-manager/src/components/Divider.test.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { render } from '@testing-library/react'; diff --git a/plugins/github-release-manager/src/components/Divider.tsx b/plugins/github-release-manager/src/components/Divider.tsx index 879a55b98a..9ddf21e0ff 100644 --- a/plugins/github-release-manager/src/components/Divider.tsx +++ b/plugins/github-release-manager/src/components/Divider.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { Divider as MaterialDivider } from '@material-ui/core'; diff --git a/plugins/github-release-manager/src/components/InfoCardPlus.test.tsx b/plugins/github-release-manager/src/components/InfoCardPlus.test.tsx index 9486d2ac94..efd30b67da 100644 --- a/plugins/github-release-manager/src/components/InfoCardPlus.test.tsx +++ b/plugins/github-release-manager/src/components/InfoCardPlus.test.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { render } from '@testing-library/react'; diff --git a/plugins/github-release-manager/src/components/InfoCardPlus.tsx b/plugins/github-release-manager/src/components/InfoCardPlus.tsx index 9adf295064..38e3c5f388 100644 --- a/plugins/github-release-manager/src/components/InfoCardPlus.tsx +++ b/plugins/github-release-manager/src/components/InfoCardPlus.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { InfoCard } from '@backstage/core'; import { makeStyles } from '@material-ui/core'; diff --git a/plugins/github-release-manager/src/components/NoLatestRelease.test.tsx b/plugins/github-release-manager/src/components/NoLatestRelease.test.tsx index 83eb4f6802..779412ec25 100644 --- a/plugins/github-release-manager/src/components/NoLatestRelease.test.tsx +++ b/plugins/github-release-manager/src/components/NoLatestRelease.test.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { render } from '@testing-library/react'; diff --git a/plugins/github-release-manager/src/components/NoLatestRelease.tsx b/plugins/github-release-manager/src/components/NoLatestRelease.tsx index f09049f3cf..232eb1c6b1 100644 --- a/plugins/github-release-manager/src/components/NoLatestRelease.tsx +++ b/plugins/github-release-manager/src/components/NoLatestRelease.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { Alert } from '@material-ui/lab'; diff --git a/plugins/github-release-manager/src/components/ProjectContext.ts b/plugins/github-release-manager/src/components/ProjectContext.ts index 61d41f8dc9..6417d7bdf5 100644 --- a/plugins/github-release-manager/src/components/ProjectContext.ts +++ b/plugins/github-release-manager/src/components/ProjectContext.ts @@ -13,19 +13,22 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { createContext, useContext } from 'react'; -import { ApiClient } from '../api/ApiClient'; +import { PluginApiClient } from '../api/PluginApiClient'; import { GitHubReleaseManagerError } from '../errors/GitHubReleaseManagerError'; -export const ApiClientContext = createContext(undefined); +export const PluginApiClientContext = createContext< + PluginApiClient | undefined +>(undefined); -export const useApiClientContext = () => { - const apiClient = useContext(ApiClientContext); +export const usePluginApiClientContext = () => { + const pluginApiClient = useContext(PluginApiClientContext); - if (!apiClient) { - throw new GitHubReleaseManagerError('apiClient not found'); + if (!pluginApiClient) { + throw new GitHubReleaseManagerError('pluginApiClient not found'); } - return apiClient; + return pluginApiClient; }; diff --git a/plugins/github-release-manager/src/components/ReloadButton.test.tsx b/plugins/github-release-manager/src/components/ReloadButton.test.tsx index c7a52bcdf9..b33cc0b931 100644 --- a/plugins/github-release-manager/src/components/ReloadButton.test.tsx +++ b/plugins/github-release-manager/src/components/ReloadButton.test.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { render } from '@testing-library/react'; diff --git a/plugins/github-release-manager/src/components/ReloadButton.tsx b/plugins/github-release-manager/src/components/ReloadButton.tsx index 44e7ce89b3..d11f39f954 100644 --- a/plugins/github-release-manager/src/components/ReloadButton.tsx +++ b/plugins/github-release-manager/src/components/ReloadButton.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { Button } from '@material-ui/core'; diff --git a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.test.tsx b/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.test.tsx index 3aa0986d33..18974b079f 100644 --- a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.test.tsx +++ b/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.test.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { render } from '@testing-library/react'; diff --git a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.tsx b/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.tsx index 239e642884..ab545153f7 100644 --- a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.tsx +++ b/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React, { PropsWithChildren } from 'react'; import { List, diff --git a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepListItem.test.tsx b/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepListItem.test.tsx index 0b4ce11887..c549ef4f11 100644 --- a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepListItem.test.tsx +++ b/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepListItem.test.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React from 'react'; import { render } from '@testing-library/react'; diff --git a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepListItem.tsx b/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepListItem.tsx index 21486ed87f..dd54a7a11d 100644 --- a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepListItem.tsx +++ b/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepListItem.tsx @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import React, { useEffect, useState } from 'react'; import { colors, diff --git a/plugins/github-release-manager/src/constants/constants.test.ts b/plugins/github-release-manager/src/constants/constants.test.ts index 616cf6f21e..c731731392 100644 --- a/plugins/github-release-manager/src/constants/constants.test.ts +++ b/plugins/github-release-manager/src/constants/constants.test.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import * as constants from './constants'; describe('constants', () => { diff --git a/plugins/github-release-manager/src/constants/constants.ts b/plugins/github-release-manager/src/constants/constants.ts index c36c3b63b3..4cfd06b7ca 100644 --- a/plugins/github-release-manager/src/constants/constants.ts +++ b/plugins/github-release-manager/src/constants/constants.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export const SEMVER_PARTS: { major: 'major'; minor: 'minor'; diff --git a/plugins/github-release-manager/src/errors/GitHubReleaseManagerError.ts b/plugins/github-release-manager/src/errors/GitHubReleaseManagerError.ts index 144ad7af90..7806d4baad 100644 --- a/plugins/github-release-manager/src/errors/GitHubReleaseManagerError.ts +++ b/plugins/github-release-manager/src/errors/GitHubReleaseManagerError.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export class GitHubReleaseManagerError extends Error { constructor(message: string) { super(message); diff --git a/plugins/github-release-manager/src/helpers/getBumpedTag.test.ts b/plugins/github-release-manager/src/helpers/getBumpedTag.test.ts index dc81f93e67..1fd7cd2ecf 100644 --- a/plugins/github-release-manager/src/helpers/getBumpedTag.test.ts +++ b/plugins/github-release-manager/src/helpers/getBumpedTag.test.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { mockCalverProject, mockSemverProject, diff --git a/plugins/github-release-manager/src/helpers/getBumpedTag.ts b/plugins/github-release-manager/src/helpers/getBumpedTag.ts index a6d4e43172..7e2ebadd35 100644 --- a/plugins/github-release-manager/src/helpers/getBumpedTag.ts +++ b/plugins/github-release-manager/src/helpers/getBumpedTag.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { CalverTagParts } from './tagParts/getCalverTagParts'; import { getTagParts } from './tagParts/getTagParts'; import { isCalverTagParts } from './isCalverTagParts'; diff --git a/plugins/github-release-manager/src/helpers/getShortCommitHash.test.ts b/plugins/github-release-manager/src/helpers/getShortCommitHash.test.ts index 4f9b57f6f1..82cfd031cf 100644 --- a/plugins/github-release-manager/src/helpers/getShortCommitHash.test.ts +++ b/plugins/github-release-manager/src/helpers/getShortCommitHash.test.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { getShortCommitHash } from './getShortCommitHash'; describe('getShortCommitHash', () => { diff --git a/plugins/github-release-manager/src/helpers/getShortCommitHash.ts b/plugins/github-release-manager/src/helpers/getShortCommitHash.ts index bb85703477..e2088caced 100644 --- a/plugins/github-release-manager/src/helpers/getShortCommitHash.ts +++ b/plugins/github-release-manager/src/helpers/getShortCommitHash.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { GitHubReleaseManagerError } from '../errors/GitHubReleaseManagerError'; export function getShortCommitHash(hash: string) { diff --git a/plugins/github-release-manager/src/helpers/isCalverTagParts.test.ts b/plugins/github-release-manager/src/helpers/isCalverTagParts.test.ts index 33be3cc2e0..1f74f2fe40 100644 --- a/plugins/github-release-manager/src/helpers/isCalverTagParts.test.ts +++ b/plugins/github-release-manager/src/helpers/isCalverTagParts.test.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { mockCalverProject, mockSemverProject, diff --git a/plugins/github-release-manager/src/helpers/isCalverTagParts.ts b/plugins/github-release-manager/src/helpers/isCalverTagParts.ts index 825486c248..dbc373a5aa 100644 --- a/plugins/github-release-manager/src/helpers/isCalverTagParts.ts +++ b/plugins/github-release-manager/src/helpers/isCalverTagParts.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { CalverTagParts } from './tagParts/getCalverTagParts'; import { Project } from '../types/types'; diff --git a/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.ts b/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.ts index c76ef92a85..27fe59844a 100644 --- a/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.ts +++ b/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { GitHubReleaseManagerError } from '../../errors/GitHubReleaseManagerError'; export type CalverTagParts = { diff --git a/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.ts b/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.ts index f6a97908e2..eabf1c0a7e 100644 --- a/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.ts +++ b/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { GitHubReleaseManagerError } from '../../errors/GitHubReleaseManagerError'; export type SemverTagParts = { diff --git a/plugins/github-release-manager/src/helpers/tagParts/getTagParts.test.ts b/plugins/github-release-manager/src/helpers/tagParts/getTagParts.test.ts index 0536cd3d7d..7493887d15 100644 --- a/plugins/github-release-manager/src/helpers/tagParts/getTagParts.test.ts +++ b/plugins/github-release-manager/src/helpers/tagParts/getTagParts.test.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { mockCalverProject, mockSemverProject, diff --git a/plugins/github-release-manager/src/helpers/tagParts/getTagParts.ts b/plugins/github-release-manager/src/helpers/tagParts/getTagParts.ts index 3e7f85598b..ff42779d99 100644 --- a/plugins/github-release-manager/src/helpers/tagParts/getTagParts.ts +++ b/plugins/github-release-manager/src/helpers/tagParts/getTagParts.ts @@ -13,6 +13,7 @@ * 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 '../../types/types'; diff --git a/plugins/github-release-manager/src/index.ts b/plugins/github-release-manager/src/index.ts index c4177089eb..bb9a29c419 100644 --- a/plugins/github-release-manager/src/index.ts +++ b/plugins/github-release-manager/src/index.ts @@ -13,4 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -export { gitHubReleaseManagerPlugin, GitHubReleaseManagerPage } from './plugin'; + +export { + gitHubReleaseManagerPlugin, + GitHubReleaseManagerPage, + githubReleaseManagerApiRef, +} from './plugin'; diff --git a/plugins/github-release-manager/src/plugin.test.ts b/plugins/github-release-manager/src/plugin.test.ts index c952ce30fa..a13dda5688 100644 --- a/plugins/github-release-manager/src/plugin.test.ts +++ b/plugins/github-release-manager/src/plugin.test.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { gitHubReleaseManagerPlugin } from './plugin'; describe('github-release-manager', () => { diff --git a/plugins/github-release-manager/src/plugin.ts b/plugins/github-release-manager/src/plugin.ts index 5f40d4581e..c0c6f4fda9 100644 --- a/plugins/github-release-manager/src/plugin.ts +++ b/plugins/github-release-manager/src/plugin.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { configApiRef, createPlugin, @@ -22,9 +23,11 @@ import { } from '@backstage/core'; import { githubReleaseManagerApiRef } from './api/serviceApiRef'; -import { PluginApiClientConfig } from './api/PluginApiClientConfig'; +import { PluginApiClient } from './api/PluginApiClient'; import { rootRouteRef } from './routes'; +export { githubReleaseManagerApiRef }; + export const gitHubReleaseManagerPlugin = createPlugin({ id: 'github-release-manager', routes: { @@ -37,8 +40,9 @@ export const gitHubReleaseManagerPlugin = createPlugin({ configApi: configApiRef, githubAuthApi: githubAuthApiRef, }, - factory: ({ configApi, githubAuthApi }) => - new PluginApiClientConfig({ configApi, githubAuthApi }), + factory: ({ configApi, githubAuthApi }) => { + return new PluginApiClient({ configApi, githubAuthApi }); + }, }), ], }); diff --git a/plugins/github-release-manager/src/routes.ts b/plugins/github-release-manager/src/routes.ts index d9dd0774b9..9406a4c6a7 100644 --- a/plugins/github-release-manager/src/routes.ts +++ b/plugins/github-release-manager/src/routes.ts @@ -13,6 +13,9 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { createRouteRef } from '@backstage/core'; -export const rootRouteRef = createRouteRef({ title: 'github-release-manager' }); +export const rootRouteRef = createRouteRef({ + title: 'github-release-manager', +}); diff --git a/plugins/github-release-manager/src/setupTests.ts b/plugins/github-release-manager/src/setupTests.ts index 0cec5b395d..3ffe1424cc 100644 --- a/plugins/github-release-manager/src/setupTests.ts +++ b/plugins/github-release-manager/src/setupTests.ts @@ -13,5 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import '@testing-library/jest-dom'; import 'cross-fetch/polyfill'; diff --git a/plugins/github-release-manager/src/sideEffects/getGitHubBatchInfo.ts b/plugins/github-release-manager/src/sideEffects/getGitHubBatchInfo.ts index 8051569772..9c7a220ea1 100644 --- a/plugins/github-release-manager/src/sideEffects/getGitHubBatchInfo.ts +++ b/plugins/github-release-manager/src/sideEffects/getGitHubBatchInfo.ts @@ -13,19 +13,20 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ApiClient } from '../api/ApiClient'; + +import { PluginApiClient } from '../api/PluginApiClient'; import { getLatestRelease } from './getLatestRelease'; interface GetGitHubBatchInfo { - apiClient: ApiClient; + pluginApiClient: PluginApiClient; } export const getGitHubBatchInfo = ({ - apiClient, + pluginApiClient, }: GetGitHubBatchInfo) => async () => { const [{ repository }, latestRelease] = await Promise.all([ - apiClient.getRepository(), - getLatestRelease({ apiClient }), + pluginApiClient.getRepository(), + getLatestRelease({ pluginApiClient }), ]); if (latestRelease === null) { @@ -36,7 +37,7 @@ export const getGitHubBatchInfo = ({ }; } - const { branch } = await apiClient.getBranch({ + const { branch } = await pluginApiClient.getBranch({ branchName: latestRelease.target_commitish, }); diff --git a/plugins/github-release-manager/src/sideEffects/getLatestRelease.test.ts b/plugins/github-release-manager/src/sideEffects/getLatestRelease.test.ts index eb275092da..15ef97d453 100644 --- a/plugins/github-release-manager/src/sideEffects/getLatestRelease.test.ts +++ b/plugins/github-release-manager/src/sideEffects/getLatestRelease.test.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { mockApiClient } from '../test-helpers/test-helpers'; import { getLatestRelease } from './getLatestRelease'; @@ -20,7 +21,7 @@ describe('getLatestRelease', () => { beforeEach(jest.clearAllMocks); it('should return the latest release with id=1', async () => { - const result = await getLatestRelease({ apiClient: mockApiClient }); + const result = await getLatestRelease({ pluginApiClient: mockApiClient }); expect(result).toMatchInlineSnapshot(` Object { @@ -35,7 +36,7 @@ describe('getLatestRelease', () => { it('should return early with `null` if no releases found', async () => { mockApiClient.getReleases.mockImplementationOnce(() => ({ releases: [] })); - const result = await getLatestRelease({ apiClient: mockApiClient }); + const result = await getLatestRelease({ pluginApiClient: mockApiClient }); expect(result).toMatchInlineSnapshot(`null`); }); diff --git a/plugins/github-release-manager/src/sideEffects/getLatestRelease.ts b/plugins/github-release-manager/src/sideEffects/getLatestRelease.ts index 5690206553..9d3ebf647d 100644 --- a/plugins/github-release-manager/src/sideEffects/getLatestRelease.ts +++ b/plugins/github-release-manager/src/sideEffects/getLatestRelease.ts @@ -13,20 +13,21 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import { ApiClient } from '../api/ApiClient'; + +import { PluginApiClient } from '../api/PluginApiClient'; interface GetLatestRelease { - apiClient: ApiClient; + pluginApiClient: PluginApiClient; } -export async function getLatestRelease({ apiClient }: GetLatestRelease) { - const { releases } = await apiClient.getReleases(); +export async function getLatestRelease({ pluginApiClient }: GetLatestRelease) { + const { releases } = await pluginApiClient.getReleases(); if (releases.length === 0) { return null; } - const { latestRelease } = await apiClient.getRelease({ + const { latestRelease } = await pluginApiClient.getRelease({ releaseId: releases[0].id, }); diff --git a/plugins/github-release-manager/src/styles/styles.ts b/plugins/github-release-manager/src/styles/styles.ts index 1548953069..19a9434c69 100644 --- a/plugins/github-release-manager/src/styles/styles.ts +++ b/plugins/github-release-manager/src/styles/styles.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { makeStyles, Theme } from '@material-ui/core'; export const useStyles = makeStyles((_theme: Theme) => ({ 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 78c48eb0a7..eb359d06dd 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 @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import * as testHelpers from './test-helpers'; describe('testHelpers', () => { 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 61d8cb3b17..fc246ab4bc 100644 --- a/plugins/github-release-manager/src/test-helpers/test-helpers.ts +++ b/plugins/github-release-manager/src/test-helpers/test-helpers.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + import { CalverTagParts } from '../helpers/tagParts/getCalverTagParts'; import { getRcGitHubInfo } from '../cards/createRc/getRcGitHubInfo'; import { diff --git a/plugins/github-release-manager/src/test-helpers/test-ids.ts b/plugins/github-release-manager/src/test-helpers/test-ids.ts index 27aef5567f..3620b03248 100644 --- a/plugins/github-release-manager/src/test-helpers/test-ids.ts +++ b/plugins/github-release-manager/src/test-helpers/test-ids.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export const TEST_IDS = { info: { info: 'grm--info', diff --git a/plugins/github-release-manager/src/types/types.ts b/plugins/github-release-manager/src/types/types.ts index f4fadefce3..6eafd8f797 100644 --- a/plugins/github-release-manager/src/types/types.ts +++ b/plugins/github-release-manager/src/types/types.ts @@ -13,6 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ + export interface Project { /** A unique (in the context of GitHub Release Manager) project name */ name: string; From 191cb7f893b140bddf4a55f88a710b09c4ef3179 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Wed, 14 Apr 2021 18:11:21 +0200 Subject: [PATCH 013/140] Turn Project into a context and bind it to root component Signed-off-by: Erik Engervall --- plugins/github-release-manager/dev/index.tsx | 26 +- .../src/GitHubReleaseManager.tsx | 81 ++-- .../src/api/PluginApiClient.ts | 392 +++++++++++++----- .../src/api/serviceApiRef.ts | 4 +- .../src/cards/createRc/CreateRc.tsx | 12 +- .../src/cards/createRc/getRcGitHubInfo.ts | 3 +- .../cards/createRc/sideEffects/createRc.ts | 11 +- .../src/cards/info/Info.tsx | 40 +- .../src/cards/patchRc/Patch.tsx | 7 +- .../src/cards/patchRc/PatchBody.tsx | 17 +- .../src/cards/patchRc/sideEffects/patch.ts | 32 +- .../src/cards/promoteRc/PromoteRcBody.tsx | 9 +- .../cards/promoteRc/sideEffects/promoteRc.ts | 4 + .../PluginApiClientContext.ts} | 0 .../src/contexts/ProjectContext.ts | 55 +++ .../src/helpers/getBumpedTag.ts | 2 +- .../src/sideEffects/getGitHubBatchInfo.ts | 10 +- .../src/sideEffects/getLatestRelease.ts | 10 +- .../github-release-manager/src/types/types.ts | 59 +-- 19 files changed, 496 insertions(+), 278 deletions(-) rename plugins/github-release-manager/src/{components/ProjectContext.ts => contexts/PluginApiClientContext.ts} (100%) create mode 100644 plugins/github-release-manager/src/contexts/ProjectContext.ts diff --git a/plugins/github-release-manager/dev/index.tsx b/plugins/github-release-manager/dev/index.tsx index b3d343382f..5518d633e2 100644 --- a/plugins/github-release-manager/dev/index.tsx +++ b/plugins/github-release-manager/dev/index.tsx @@ -26,32 +26,10 @@ createDevApp() .registerPlugin(gitHubReleaseManagerPlugin) .addPage({ title: 'Page 1', - element: ( - - ), + element: , }) .addPage({ title: 'Page 2', - element: ( - - ), + element: , }) .render(); diff --git a/plugins/github-release-manager/src/GitHubReleaseManager.tsx b/plugins/github-release-manager/src/GitHubReleaseManager.tsx index 82396438f0..83ccab5dd1 100644 --- a/plugins/github-release-manager/src/GitHubReleaseManager.tsx +++ b/plugins/github-release-manager/src/GitHubReleaseManager.tsx @@ -28,40 +28,26 @@ import { ComponentConfigCreateRc, ComponentConfigPatch, ComponentConfigPromoteRc, - GhGetBranchResponse, - GhGetReleaseResponse, - GhGetRepositoryResponse, - Project, - SetRefetch, } from './types/types'; import { PromoteRc } from './cards/promoteRc/PromoteRc'; import { githubReleaseManagerApiRef } from './api/serviceApiRef'; import { PluginApiClientContext, usePluginApiClientContext, -} from './components/ProjectContext'; +} from './contexts/PluginApiClientContext'; +import { + ProjectContext, + useProjectContext, + Project, +} from './contexts/ProjectContext'; interface GitHubReleaseManagerProps { - project: Project; components?: { default?: { createRc?: ComponentConfigCreateRc; promoteRc?: ComponentConfigPromoteRc; patch?: ComponentConfigPatch; }; - custom?: ({ - project, - setRefetch, - latestRelease, - releaseBranch, - repository, - }: { - project: Project; - setRefetch: SetRefetch; - latestRelease: GhGetReleaseResponse | null; - releaseBranch: GhGetBranchResponse | null; - repository: GhGetRepositoryResponse; - }) => JSX.Element[]; }; } @@ -72,31 +58,37 @@ const useStyles = makeStyles(() => ({ })); export function GitHubReleaseManager({ - project, components, }: GitHubReleaseManagerProps) { const pluginApiClient = useApi(githubReleaseManagerApiRef); - pluginApiClient.setRepoPath({ - repoPath: `${project.github.org}/${project.github.repo}`, - }); const classes = useStyles(); - return ( - -
- + const project: Project = { + owner: 'erikengervall', + repo: 'playground', + versioningStrategy: 'semver', + }; - -
-
+ return ( + + {/* @ts-ignore-error TODO: Update interface for PluginApiClient */} + +
+ + + +
+
+
); } -function Cards({ project, components }: GitHubReleaseManagerProps) { +function Cards({ components }: GitHubReleaseManagerProps) { const pluginApiClient = usePluginApiClientContext(); + const project = useProjectContext(); const [refetch, setRefetch] = useState(0); const gitHubBatchInfo = useAsync( - getGitHubBatchInfo({ pluginApiClient: pluginApiClient }), + getGitHubBatchInfo({ project, pluginApiClient }), [project, refetch], ); @@ -118,11 +110,11 @@ function Cards({ project, components }: GitHubReleaseManagerProps) { ); } - if (!gitHubBatchInfo.value.repository.permissions.push) { + if (!gitHubBatchInfo.value.repository.pushPermissions) { return ( - You lack push permissions for repository "{project.github.org}/ - {project.github.repo}" + You lack push permissions for repository "{project.owner}/{project.repo} + " ); } @@ -132,15 +124,13 @@ function Cards({ project, components }: GitHubReleaseManagerProps) { {components?.default?.createRc?.omit !== true && ( @@ -158,23 +148,10 @@ function Cards({ project, components }: GitHubReleaseManagerProps) { )} - - {components - ?.custom?.({ - project, - setRefetch, - latestRelease: gitHubBatchInfo.value.latestRelease, - releaseBranch: gitHubBatchInfo.value.releaseBranch, - repository: gitHubBatchInfo.value.repository, - }) - .map((customElement, index) => ( -
{customElement}
- ))} ); } diff --git a/plugins/github-release-manager/src/api/PluginApiClient.ts b/plugins/github-release-manager/src/api/PluginApiClient.ts index c2bf8547d4..0615221c77 100644 --- a/plugins/github-release-manager/src/api/PluginApiClient.ts +++ b/plugins/github-release-manager/src/api/PluginApiClient.ts @@ -27,7 +27,6 @@ import { GhGetBranchResponse, GhGetCommitResponse, GhGetReleaseResponse, - GhGetRepositoryResponse, GhMergeResponse, GhUpdateReferenceResponse, GhUpdateReleaseResponse, @@ -35,12 +34,135 @@ import { import { CalverTagParts } from '../helpers/tagParts/getCalverTagParts'; import { getRcGitHubInfo } from '../cards/createRc/getRcGitHubInfo'; import { SemverTagParts } from '../helpers/tagParts/getSemverTagParts'; -import { GitHubReleaseManagerError } from '../errors/GitHubReleaseManagerError'; +import { Project } from '../contexts/ProjectContext'; -export class PluginApiClient { +// export type UnboxPromise> = T extends Promise +// ? U +// : never; + +type Todo = any; +type PartialProject = Omit; + +export interface IPluginApiClient { + getHost: () => string; + + getRecentCommits: ( + args: { releaseBranchName?: string } & PartialProject, + ) => Promise; + getReleases: (args: { releaseId: number } & PartialProject) => Promise; + getRelease: (args: { releaseId: number } & PartialProject) => Promise; + getRepository: ( + args: PartialProject, + ) => Promise<{ + repository: { + pushPermissions: boolean | undefined; + defaultBranch: string; + }; + }>; + getLatestCommit: ( + args: { defaultBranch: string } & PartialProject, + ) => Promise; + getBranch: (args: { branchName: string } & PartialProject) => Promise; + + createRc: { + createRef: ( + args: { + mostRecentSha: string; + targetBranch: string; + } & PartialProject, + ) => Promise; + + getComparison: ( + args: { + previousReleaseBranch: string; + nextReleaseBranch: string; + } & PartialProject, + ) => Promise; + + createRelease: ( + args: { + nextGitHubInfo: ReturnType; + releaseBody: string; + } & PartialProject, + ) => Promise; + }; + + patch: { + createTempCommit: ( + args: { + tagParts: SemverTagParts | CalverTagParts; + releaseBranchTree: string; + selectedPatchCommit: GhGetCommitResponse; + } & PartialProject, + ) => Promise; + + forceBranchHeadToTempCommit: ( + args: { + releaseBranchName: string; + tempCommit: GhCreateCommitResponse; + } & PartialProject, + ) => Promise; + + merge: ({ + base, + head, + }: { base: string; head: string } & PartialProject) => Promise; + + createCherryPickCommit: ( + args: { + bumpedTag: string; + selectedPatchCommit: GhGetCommitResponse; + mergeTree: string; + releaseBranchSha: string; + } & PartialProject, + ) => Promise; + + replaceTempCommit: ( + args: { + releaseBranchName: string; + cherryPickCommit: GhCreateCommitResponse; + } & PartialProject, + ) => Promise; + + createTagObject: ({ + bumpedTag, + updatedReference, + }: { + bumpedTag: string; + updatedReference: GhUpdateReferenceResponse; + } & PartialProject) => Promise; + + createReference: ( + args: { + bumpedTag: string; + tagObjectResponse: GhCreateTagObjectResponse; + } & PartialProject, + ) => Promise; + + updateRelease: ( + args: { + bumpedTag: string; + latestRelease: GhGetReleaseResponse; + tagParts: SemverTagParts | CalverTagParts; + selectedPatchCommit: GhGetCommitResponse; + } & PartialProject, + ) => Promise; + }; + + promoteRc: { + promoteRelease: ( + args: { + releaseId: GhGetReleaseResponse['id']; + releaseVersion: string; + } & PartialProject, + ) => Promise; + }; +} + +export class PluginApiClient implements IPluginApiClient { + // private readonly getAccessToken: any; private readonly githubAuthApi: OAuthApi; private readonly baseUrl: string; - private repoPath?: string; readonly host: string; constructor({ @@ -52,6 +174,8 @@ export class PluginApiClient { }) { this.githubAuthApi = githubAuthApi; + // this.getAccessToken = () => this.githubAuthApi.getAccessToken(); + const githubIntegrationConfig = this.getGithubIntegrationConfig({ configApi, }); @@ -88,81 +212,102 @@ export class PluginApiClient { return this.host; } - public setRepoPath({ repoPath }: { repoPath: string }) { - this.repoPath = repoPath; - } - - public getRepoPath() { - if (!this.repoPath) { - throw new GitHubReleaseManagerError('Could not find repoPath'); - } - - return this.repoPath; + public getRepoPath({ owner, repo }: PartialProject) { + return `${owner}/${repo}`; } async getRecentCommits({ + owner, + repo, releaseBranchName, - }: { releaseBranchName?: string } = {}) { + }: { + releaseBranchName?: string; + } & PartialProject) { const { octokit } = await this.getOctokit(); const sha = releaseBranchName ? `?sha=${releaseBranchName}` : ''; const recentCommits: GhGetCommitResponse[] = ( - await octokit.request(`/repos/${this.getRepoPath()}/commits${sha}`) + await octokit.request( + `/repos/${this.getRepoPath({ owner, repo })}/commits${sha}`, + ) ).data; return { recentCommits }; } - async getReleases() { + async getReleases({ owner, repo }: PartialProject) { const { octokit } = await this.getOctokit(); const releases: GhGetReleaseResponse[] = ( - await octokit.request(`/repos/${this.getRepoPath()}/releases`) + await octokit.request( + `/repos/${this.getRepoPath({ owner, repo })}/releases`, + ) ).data; return { releases }; } - async getRelease({ releaseId }: { releaseId: number }) { + async getRelease({ + owner, + repo, + releaseId, + }: { releaseId: number } & PartialProject) { const { octokit } = await this.getOctokit(); const latestRelease: GhGetReleaseResponse = ( await octokit.request( - `/repos/${this.getRepoPath()}/releases/${releaseId}`, + `/repos/${this.getRepoPath({ owner, repo })}/releases/${releaseId}`, ) ).data; return { latestRelease }; } - async getRepository() { + async getRepository({ owner, repo }: PartialProject) { const { octokit } = await this.getOctokit(); - const repository: GhGetRepositoryResponse = ( - await octokit.request(`/repos/${this.getRepoPath()}`) - ).data; + const { data: repository } = await octokit.repos.get({ + owner: owner, + repo, + }); - return { repository }; + return { + repository: { + pushPermissions: repository.permissions?.push, + defaultBranch: repository.default_branch, + }, + }; } - async getLatestCommit({ defaultBranch }: { defaultBranch: string }) { + async getLatestCommit({ + owner, + repo, + defaultBranch, + }: { defaultBranch: string } & PartialProject) { const { octokit } = await this.getOctokit(); const latestCommit: GhGetCommitResponse = ( await octokit.request( - `/repos/${this.getRepoPath()}/commits/refs/heads/${defaultBranch}`, + `/repos/${this.getRepoPath({ + owner, + repo, + })}/commits/refs/heads/${defaultBranch}`, ) ).data; return { latestCommit }; } - async getBranch({ branchName }: { branchName: string }) { + async getBranch({ + owner, + repo, + branchName, + }: { branchName: string } & PartialProject) { const { octokit } = await this.getOctokit(); const branch: GhGetBranchResponse = ( await octokit.request( - `/repos/${this.getRepoPath()}/branches/${branchName}`, + `/repos/${this.getRepoPath({ owner, repo })}/branches/${branchName}`, ) ).data; @@ -171,39 +316,49 @@ export class PluginApiClient { createRc = { createRef: async ({ + owner, + repo, mostRecentSha, targetBranch, }: { mostRecentSha: string; targetBranch: string; - }) => { + } & PartialProject) => { const { octokit } = await this.getOctokit(); const createdRef: GhCreateReferenceResponse = ( - await octokit.request(`/repos/${this.getRepoPath()}/git/refs`, { - method: 'POST', - data: { - ref: `refs/heads/${targetBranch}`, - sha: mostRecentSha, + await octokit.request( + `/repos/${this.getRepoPath({ owner, repo })}/git/refs`, + { + method: 'POST', + data: { + ref: `refs/heads/${targetBranch}`, + sha: mostRecentSha, + }, }, - }) + ) ).data; return { createdRef }; }, getComparison: async ({ + owner, + repo, previousReleaseBranch, nextReleaseBranch, }: { previousReleaseBranch: string; nextReleaseBranch: string; - }) => { + } & PartialProject) => { const { octokit } = await this.getOctokit(); const comparison: GhCompareCommitsResponse = ( await octokit.request( - `/repos/${this.getRepoPath()}/compare/${previousReleaseBranch}...${nextReleaseBranch}`, + `/repos/${this.getRepoPath({ + owner, + repo, + })}/compare/${previousReleaseBranch}...${nextReleaseBranch}`, ) ).data; @@ -211,25 +366,30 @@ export class PluginApiClient { }, createRelease: async ({ + owner, + repo, nextGitHubInfo, releaseBody, }: { nextGitHubInfo: ReturnType; releaseBody: string; - }) => { + } & PartialProject) => { const { octokit } = await this.getOctokit(); const createReleaseResponse: GhCreateReleaseResponse = ( - await octokit.request(`/repos/${this.getRepoPath()}/releases`, { - method: 'POST', - data: { - tag_name: nextGitHubInfo.rcReleaseTag, - name: nextGitHubInfo.releaseName, - target_commitish: nextGitHubInfo.rcBranch, - body: releaseBody, - prerelease: true, + await octokit.request( + `/repos/${this.getRepoPath({ owner, repo })}/releases`, + { + method: 'POST', + data: { + tag_name: nextGitHubInfo.rcReleaseTag, + name: nextGitHubInfo.releaseName, + target_commitish: nextGitHubInfo.rcBranch, + body: releaseBody, + prerelease: true, + }, }, - }) + ) ).data; return { createReleaseResponse }; @@ -238,6 +398,8 @@ export class PluginApiClient { patch = { createTempCommit: async ({ + owner, + repo, tagParts, releaseBranchTree, selectedPatchCommit, @@ -245,34 +407,42 @@ export class PluginApiClient { tagParts: SemverTagParts | CalverTagParts; releaseBranchTree: string; selectedPatchCommit: GhGetCommitResponse; - }) => { + } & PartialProject) => { const { octokit } = await this.getOctokit(); const tempCommit: GhCreateCommitResponse = ( - await octokit.request(`/repos/${this.getRepoPath()}/git/commits`, { - method: 'POST', - data: { - message: `Temporary commit for patch ${tagParts.patch}`, - tree: releaseBranchTree, - parents: [selectedPatchCommit.parents[0].sha], + await octokit.request( + `/repos/${this.getRepoPath({ owner, repo })}/git/commits`, + { + method: 'POST', + data: { + message: `Temporary commit for patch ${tagParts.patch}`, + tree: releaseBranchTree, + parents: [selectedPatchCommit.parents[0].sha], + }, }, - }) + ) ).data; return { tempCommit }; }, forceBranchHeadToTempCommit: async ({ + owner, + repo, releaseBranchName, tempCommit, }: { releaseBranchName: string; tempCommit: GhCreateCommitResponse; - }) => { + } & PartialProject) => { const { octokit } = await this.getOctokit(); await octokit.request( - `/repos/${this.getRepoPath()}/git/refs/heads/${releaseBranchName}`, + `/repos/${this.getRepoPath({ + owner, + repo, + })}/git/refs/heads/${releaseBranchName}`, { method: 'PATCH', data: { @@ -283,20 +453,30 @@ export class PluginApiClient { ); }, - merge: async ({ base, head }: { base: string; head: string }) => { + merge: async ({ + owner, + repo, + base, + head, + }: { base: string; head: string } & PartialProject) => { const { octokit } = await this.getOctokit(); const merge: GhMergeResponse = ( - await octokit.request(`/repos/${this.getRepoPath()}/merges`, { - method: 'POST', - data: { base, head }, - }) + await octokit.request( + `/repos/${this.getRepoPath({ owner, repo })}/merges`, + { + method: 'POST', + data: { base, head }, + }, + ) ).data; return { merge }; }, createCherryPickCommit: async ({ + owner, + repo, bumpedTag, selectedPatchCommit, mergeTree, @@ -306,35 +486,43 @@ export class PluginApiClient { selectedPatchCommit: GhGetCommitResponse; mergeTree: string; releaseBranchSha: string; - }) => { + } & PartialProject) => { const { octokit } = await this.getOctokit(); const cherryPickCommit: GhCreateCommitResponse = ( - await octokit.request(`/repos/${this.getRepoPath()}/git/commits`, { - method: 'POST', - data: { - message: `[patch ${bumpedTag}] ${selectedPatchCommit.commit.message}`, - tree: mergeTree, - parents: [releaseBranchSha], + await octokit.request( + `/repos/${this.getRepoPath({ owner, repo })}/git/commits`, + { + method: 'POST', + data: { + message: `[patch ${bumpedTag}] ${selectedPatchCommit.commit.message}`, + tree: mergeTree, + parents: [releaseBranchSha], + }, }, - }) + ) ).data; return { cherryPickCommit }; }, replaceTempCommit: async ({ + owner, + repo, releaseBranchName, cherryPickCommit, }: { releaseBranchName: string; cherryPickCommit: GhCreateCommitResponse; - }) => { + } & PartialProject) => { const { octokit } = await this.getOctokit(); const updatedReference: GhUpdateReferenceResponse = ( await octokit.request( - `/repos/${this.getRepoPath()}/git/refs/heads/${releaseBranchName}`, + `/repos/${this.getRepoPath({ + owner, + repo, + })}/git/refs/heads/${releaseBranchName}`, { method: 'PATCH', data: { @@ -349,53 +537,65 @@ export class PluginApiClient { }, createTagObject: async ({ + owner, + repo, bumpedTag, updatedReference, }: { bumpedTag: string; updatedReference: GhUpdateReferenceResponse; - }) => { + } & PartialProject) => { const { octokit } = await this.getOctokit(); const tagObjectResponse: GhCreateTagObjectResponse = ( - await octokit.request(`/repos/${this.getRepoPath()}/git/tags`, { - method: 'POST', - data: { - type: 'commit', - message: - 'Tag generated by your friendly neighborhood GitHub Release Manager', - tag: bumpedTag, - object: updatedReference.object.sha, + await octokit.request( + `/repos/${this.getRepoPath({ owner, repo })}/git/tags`, + { + method: 'POST', + data: { + type: 'commit', + message: + 'Tag generated by your friendly neighborhood GitHub Release Manager', + tag: bumpedTag, + object: updatedReference.object.sha, + }, }, - }) + ) ).data; return { tagObjectResponse }; }, createReference: async ({ + owner, + repo, bumpedTag, tagObjectResponse, }: { bumpedTag: string; tagObjectResponse: GhCreateTagObjectResponse; - }) => { + } & PartialProject) => { const { octokit } = await this.getOctokit(); const reference: GhCreateReferenceResponse = ( - await octokit.request(`/repos/${this.getRepoPath()}/git/refs`, { - method: 'POST', - data: { - ref: `refs/tags/${bumpedTag}`, - sha: tagObjectResponse.sha, + await octokit.request( + `/repos/${this.getRepoPath({ owner, repo })}/git/refs`, + { + method: 'POST', + data: { + ref: `refs/tags/${bumpedTag}`, + sha: tagObjectResponse.sha, + }, }, - }) + ) ).data; return { reference }; }, updateRelease: async ({ + owner, + repo, bumpedTag, latestRelease, tagParts, @@ -405,12 +605,14 @@ export class PluginApiClient { latestRelease: GhGetReleaseResponse; tagParts: SemverTagParts | CalverTagParts; selectedPatchCommit: GhGetCommitResponse; - }) => { + } & PartialProject) => { const { octokit } = await this.getOctokit(); const release: GhUpdateReleaseResponse = ( await octokit.request( - `/repos/${this.getRepoPath()}/releases/${latestRelease.id}`, + `/repos/${this.getRepoPath({ owner, repo })}/releases/${ + latestRelease.id + }`, { method: 'PATCH', data: { @@ -431,17 +633,19 @@ export class PluginApiClient { promoteRc = { promoteRelease: async ({ + owner, + repo, releaseId, releaseVersion, }: { releaseId: GhGetReleaseResponse['id']; releaseVersion: string; - }) => { + } & PartialProject) => { const { octokit } = await this.getOctokit(); const release: GhGetReleaseResponse = ( await octokit.request( - `/repos/${this.getRepoPath()}/releases/${releaseId}`, + `/repos/${this.getRepoPath({ owner, repo })}/releases/${releaseId}`, { method: 'PATCH', data: { diff --git a/plugins/github-release-manager/src/api/serviceApiRef.ts b/plugins/github-release-manager/src/api/serviceApiRef.ts index ea5eb7fee3..4451dd8dc8 100644 --- a/plugins/github-release-manager/src/api/serviceApiRef.ts +++ b/plugins/github-release-manager/src/api/serviceApiRef.ts @@ -16,9 +16,9 @@ import { createApiRef } from '@backstage/core'; -import { PluginApiClient } from './PluginApiClient'; +import { IPluginApiClient } from './PluginApiClient'; -export const githubReleaseManagerApiRef = createApiRef({ +export const githubReleaseManagerApiRef = createApiRef({ id: 'plugin.github-release-manager.service', description: 'Used by the GitHub Release Manager plugin to make requests', }); diff --git a/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx b/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx index c02fba01d5..2f2fb6963b 100644 --- a/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx +++ b/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx @@ -35,19 +35,18 @@ import { GhGetBranchResponse, GhGetReleaseResponse, GhGetRepositoryResponse, - Project, SetRefetch, } from '../../types/types'; import { ResponseStepList } from '../../components/ResponseStepList/ResponseStepList'; -import { useStyles } from '../../styles/styles'; -import { usePluginApiClientContext } from '../../components/ProjectContext'; import { SEMVER_PARTS } from '../../constants/constants'; import { TEST_IDS } from '../../test-helpers/test-ids'; +import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext'; +import { useProjectContext } from '../../contexts/ProjectContext'; +import { useStyles } from '../../styles/styles'; interface CreateRcProps { defaultBranch: GhGetRepositoryResponse['default_branch']; latestRelease: GhGetReleaseResponse | null; - project: Project; releaseBranch: GhGetBranchResponse | null; setRefetch: SetRefetch; successCb?: ComponentConfigCreateRc['successCb']; @@ -56,12 +55,12 @@ interface CreateRcProps { export const CreateRc = ({ defaultBranch, latestRelease, - project, releaseBranch, setRefetch, successCb, }: CreateRcProps) => { const pluginApiClient = usePluginApiClientContext(); + const project = useProjectContext(); const classes = useStyles(); const [semverBumpLevel, setSemverBumpLevel] = useState<'major' | 'minor'>( @@ -80,10 +79,11 @@ export const CreateRc = ({ const [createGitHubReleaseResponse, createGitHubReleaseFn] = useAsyncFn( (...args) => createRc({ - pluginApiClient, defaultBranch, latestRelease, nextGitHubInfo: args[0], + pluginApiClient, + project, successCb, }), ); diff --git a/plugins/github-release-manager/src/cards/createRc/getRcGitHubInfo.ts b/plugins/github-release-manager/src/cards/createRc/getRcGitHubInfo.ts index b507f84b20..cd2140296f 100644 --- a/plugins/github-release-manager/src/cards/createRc/getRcGitHubInfo.ts +++ b/plugins/github-release-manager/src/cards/createRc/getRcGitHubInfo.ts @@ -18,8 +18,9 @@ import { DateTime } from 'luxon'; import { getBumpedSemverTagParts } from '../../helpers/getBumpedTag'; import { getSemverTagParts } from '../../helpers/tagParts/getSemverTagParts'; -import { Project, GhGetReleaseResponse } from '../../types/types'; +import { GhGetReleaseResponse } from '../../types/types'; import { SEMVER_PARTS } from '../../constants/constants'; +import { Project } from '../../contexts/ProjectContext'; export const getRcGitHubInfo = ({ project, diff --git a/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.ts b/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.ts index 433ba8e40d..0f659fc994 100644 --- a/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.ts +++ b/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.ts @@ -24,20 +24,23 @@ import { } from '../../../types/types'; import { PluginApiClient } from '../../../api/PluginApiClient'; import { GitHubReleaseManagerError } from '../../../errors/GitHubReleaseManagerError'; +import { Project } from '../../../contexts/ProjectContext'; interface CreateRC { - pluginApiClient: PluginApiClient; defaultBranch: GhGetRepositoryResponse['default_branch']; latestRelease: GhGetReleaseResponse | null; nextGitHubInfo: ReturnType; + pluginApiClient: PluginApiClient; + project: Project; successCb?: ComponentConfigCreateRc['successCb']; } export async function createRc({ - pluginApiClient, defaultBranch, latestRelease, nextGitHubInfo, + pluginApiClient, + project, successCb, }: CreateRC) { const responseSteps: ResponseStep[] = []; @@ -46,6 +49,7 @@ export async function createRc({ * 1. Get the default branch's most recent commit */ const { latestCommit } = await pluginApiClient.getLatestCommit({ + ...project, defaultBranch, }); responseSteps.push({ @@ -62,6 +66,7 @@ export async function createRc({ try { createdRef = ( await pluginApiClient.createRc.createRef({ + ...project, mostRecentSha, targetBranch: nextGitHubInfo.rcBranch, }) @@ -87,6 +92,7 @@ export async function createRc({ : defaultBranch; const nextReleaseBranch = nextGitHubInfo.rcBranch; const { comparison } = await pluginApiClient.createRc.getComparison({ + ...project, previousReleaseBranch, nextReleaseBranch, }); @@ -111,6 +117,7 @@ export async function createRc({ const { createReleaseResponse, } = await pluginApiClient.createRc.createRelease({ + ...project, nextGitHubInfo: nextGitHubInfo, releaseBody, }); diff --git a/plugins/github-release-manager/src/cards/info/Info.tsx b/plugins/github-release-manager/src/cards/info/Info.tsx index f74065d78a..1f42c5b603 100644 --- a/plugins/github-release-manager/src/cards/info/Info.tsx +++ b/plugins/github-release-manager/src/cards/info/Info.tsx @@ -18,27 +18,20 @@ import React from 'react'; import { Link, Typography } from '@material-ui/core'; import { Differ } from '../../components/Differ'; +import { GhGetBranchResponse, GhGetReleaseResponse } from '../../types/types'; import { InfoCardPlus } from '../../components/InfoCardPlus'; -import { - GhGetBranchResponse, - GhGetReleaseResponse, - Project, -} from '../../types/types'; -import { useStyles } from '../../styles/styles'; import { TEST_IDS } from '../../test-helpers/test-ids'; +import { useProjectContext } from '../../contexts/ProjectContext'; +import { useStyles } from '../../styles/styles'; import flowImage from './flow.png'; interface InfoCardProps { releaseBranch: GhGetBranchResponse | null; latestRelease: GhGetReleaseResponse | null; - project: Project; } -export const Info = ({ - releaseBranch, - latestRelease, - project, -}: InfoCardProps) => { +export const Info = ({ releaseBranch, latestRelease }: InfoCardProps) => { + const project = useProjectContext(); const classes = useStyles(); return ( @@ -91,30 +84,9 @@ export const Info = ({ Repository:{' '} - + - {project.slack && ( - - Slack channel:{' '} - - #{project.slack.channel} - - ) : ( - <>(#{project.slack.channel}) - ) - } - /> - - )} - Versioning strategy:{' '} diff --git a/plugins/github-release-manager/src/cards/patchRc/Patch.tsx b/plugins/github-release-manager/src/cards/patchRc/Patch.tsx index 2bc4ebf652..e0c1be7a24 100644 --- a/plugins/github-release-manager/src/cards/patchRc/Patch.tsx +++ b/plugins/github-release-manager/src/cards/patchRc/Patch.tsx @@ -24,15 +24,14 @@ import { ComponentConfigPatch, GhGetBranchResponse, GhGetReleaseResponse, - Project, SetRefetch, } from '../../types/types'; -import { useStyles } from '../../styles/styles'; import { PatchBody } from './PatchBody'; +import { useProjectContext } from '../../contexts/ProjectContext'; +import { useStyles } from '../../styles/styles'; interface PatchProps { latestRelease: GhGetReleaseResponse | null; - project: Project; releaseBranch: GhGetBranchResponse | null; setRefetch: SetRefetch; successCb?: ComponentConfigPatch['successCb']; @@ -40,11 +39,11 @@ interface PatchProps { export const Patch = ({ latestRelease, - project, releaseBranch, setRefetch, successCb, }: PatchProps) => { + const project = useProjectContext(); const classes = useStyles(); function Body() { diff --git a/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx b/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx index 5f14286ed3..22edde9ad9 100644 --- a/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx +++ b/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx @@ -45,10 +45,11 @@ import { import { CalverTagParts } from '../../helpers/tagParts/getCalverTagParts'; import { ResponseStepList } from '../../components/ResponseStepList/ResponseStepList'; import { SemverTagParts } from '../../helpers/tagParts/getSemverTagParts'; -import { usePluginApiClientContext } from '../../components/ProjectContext'; +import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext'; import { useStyles } from '../../styles/styles'; import { TEST_IDS } from '../../test-helpers/test-ids'; import { patch } from './sideEffects/patch'; +import { useProjectContext } from '../../contexts/ProjectContext'; interface PatchBodyProps { bumpedTag: string; @@ -68,6 +69,7 @@ export const PatchBody = ({ tagParts, }: PatchBodyProps) => { const pluginApiClient = usePluginApiClientContext(); + const project = useProjectContext(); const [checkedCommitIndex, setCheckedCommitIndex] = useState(-1); const githubDataResponse = useAsync(async () => { @@ -75,13 +77,17 @@ export const PatchBody = ({ { branch: releaseBranchResponse }, { recentCommits }, ] = await Promise.all([ - pluginApiClient.getBranch({ branchName: latestRelease.target_commitish }), - pluginApiClient.getRecentCommits(), + pluginApiClient.getBranch({ + ...project, + branchName: latestRelease.target_commitish, + }), + pluginApiClient.getRecentCommits({ ...project }), ]); const { recentCommits: recentReleaseBranchCommits, } = await pluginApiClient.getRecentCommits({ + ...project, releaseBranchName: releaseBranchResponse.name, }); @@ -95,6 +101,7 @@ export const PatchBody = ({ const [patchReleaseResponse, patchReleaseFn] = useAsyncFn(async (...args) => { const selectedPatchCommit: GhGetCommitResponse = args[0]; const patchResponseSteps = await patch({ + project, pluginApiClient, bumpedTag, latestRelease, @@ -232,7 +239,9 @@ export const PatchBody = ({ aria-label="commit" disabled={commitExistsOnReleaseBranch || !releaseBranch} onClick={() => { - const repoPath = pluginApiClient.getRepoPath(); + const repoPath = pluginApiClient.getRepoPath({ + ...project, + }); const host = pluginApiClient.getHost(); const newTab = window.open( diff --git a/plugins/github-release-manager/src/cards/patchRc/sideEffects/patch.ts b/plugins/github-release-manager/src/cards/patchRc/sideEffects/patch.ts index 1c97a8f0f6..de24b5b142 100644 --- a/plugins/github-release-manager/src/cards/patchRc/sideEffects/patch.ts +++ b/plugins/github-release-manager/src/cards/patchRc/sideEffects/patch.ts @@ -24,11 +24,13 @@ import { CalverTagParts } from '../../../helpers/tagParts/getCalverTagParts'; import { GitHubReleaseManagerError } from '../../../errors/GitHubReleaseManagerError'; import { PluginApiClient } from '../../../api/PluginApiClient'; import { SemverTagParts } from '../../../helpers/tagParts/getSemverTagParts'; +import { Project } from '../../../contexts/ProjectContext'; interface Patch { - pluginApiClient: PluginApiClient; bumpedTag: string; latestRelease: GhGetReleaseResponse; + pluginApiClient: PluginApiClient; + project: Project; selectedPatchCommit: GhGetCommitResponse; successCb?: ComponentConfigPatch['successCb']; tagParts: NonNullable; @@ -36,9 +38,10 @@ interface Patch { // Inspo: https://stackoverflow.com/questions/53859199/how-to-cherry-pick-through-githubs-api export async function patch({ - pluginApiClient, bumpedTag, latestRelease, + pluginApiClient, + project, selectedPatchCommit, successCb, tagParts, @@ -57,6 +60,7 @@ export async function patch({ * > branchTree = branch.commit.commit.tree.sha */ const { branch: releaseBranch } = await pluginApiClient.getBranch({ + ...project, branchName: releaseBranchName, }); const releaseBranchSha = releaseBranch.commit.sha; @@ -73,6 +77,7 @@ export async function patch({ * > tempCommit = POST /repos/$owner/$repo/git/commits { "message": "temp", "tree": branchTree, "parents": [parentSha] } */ const { tempCommit } = await pluginApiClient.patch.createTempCommit({ + ...project, releaseBranchTree, selectedPatchCommit, tagParts, @@ -87,6 +92,7 @@ export async function patch({ * > PATCH /repos/$owner/$repo/git/refs/heads/$refName { sha = tempCommit.sha, force = true } */ await pluginApiClient.patch.forceBranchHeadToTempCommit({ + ...project, tempCommit, releaseBranchName, }); @@ -96,6 +102,7 @@ export async function patch({ * > merge = POST /repos/$owner/$repo/merges { "base": branchName, "head": commit.sha } */ const { merge } = await pluginApiClient.patch.merge({ + ...project, base: releaseBranchName, head: selectedPatchCommit.sha, }); @@ -119,6 +126,7 @@ export async function patch({ const { cherryPickCommit, } = await pluginApiClient.patch.createCherryPickCommit({ + ...project, bumpedTag, mergeTree, releaseBranchSha, @@ -134,6 +142,7 @@ export async function patch({ * > PATCH /repos/$owner/$repo/git/refs/heads/$refName { sha = cherry.sha, force = true } */ const { updatedReference } = await pluginApiClient.patch.replaceTempCommit({ + ...project, cherryPickCommit, releaseBranchName, }); @@ -146,6 +155,7 @@ export async function patch({ * > POST /repos/:owner/:repo/git/tags */ const { tagObjectResponse } = await pluginApiClient.patch.createTagObject({ + ...project, bumpedTag, updatedReference, }); @@ -159,6 +169,7 @@ export async function patch({ * > POST /repos/:owner/:repo/git/refs */ const { reference } = await pluginApiClient.patch.createReference({ + ...project, bumpedTag, tagObjectResponse, }); @@ -170,14 +181,15 @@ export async function patch({ /** * 9. Update release */ - const { release: updatedRelease } = await pluginApiClient.patch.updateRelease( - { - bumpedTag, - latestRelease, - selectedPatchCommit, - tagParts, - }, - ); + const { + release: updatedRelease, + } = await pluginApiClient.patch.updateRelease({ + ...project, + bumpedTag, + latestRelease, + selectedPatchCommit, + tagParts, + }); responseSteps.push({ message: `Updated release "${updatedRelease.name}"`, secondaryMessage: `with tag ${updatedRelease.tag_name}`, diff --git a/plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.tsx b/plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.tsx index ef9a67d89f..7b20845c92 100644 --- a/plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.tsx +++ b/plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.tsx @@ -15,9 +15,9 @@ */ import React from 'react'; +import { useAsyncFn } from 'react-use'; import { Alert } from '@material-ui/lab'; import { Button, Typography } from '@material-ui/core'; -import { useAsyncFn } from 'react-use'; import { Differ } from '../../components/Differ'; import { @@ -27,9 +27,10 @@ import { } from '../../types/types'; import { promoteRc } from './sideEffects/promoteRc'; import { ResponseStepList } from '../../components/ResponseStepList/ResponseStepList'; -import { usePluginApiClientContext } from '../../components/ProjectContext'; -import { useStyles } from '../../styles/styles'; import { TEST_IDS } from '../../test-helpers/test-ids'; +import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext'; +import { useProjectContext } from '../../contexts/ProjectContext'; +import { useStyles } from '../../styles/styles'; interface PromoteRcBodyProps { rcRelease: GhGetReleaseResponse; @@ -43,11 +44,13 @@ export const PromoteRcBody = ({ successCb, }: PromoteRcBodyProps) => { const pluginApiClient = usePluginApiClientContext(); + const project = useProjectContext(); const classes = useStyles(); const releaseVersion = rcRelease.tag_name.replace('rc-', 'version-'); const [promoteGitHubRcResponse, promoseGitHubRcFn] = useAsyncFn( promoteRc({ pluginApiClient, + project, rcRelease, releaseVersion, successCb, diff --git a/plugins/github-release-manager/src/cards/promoteRc/sideEffects/promoteRc.ts b/plugins/github-release-manager/src/cards/promoteRc/sideEffects/promoteRc.ts index e27f26289a..0d38b53fba 100644 --- a/plugins/github-release-manager/src/cards/promoteRc/sideEffects/promoteRc.ts +++ b/plugins/github-release-manager/src/cards/promoteRc/sideEffects/promoteRc.ts @@ -20,9 +20,11 @@ import { ResponseStep, } from '../../../types/types'; import { PluginApiClient } from '../../../api/PluginApiClient'; +import { Project } from '../../../contexts/ProjectContext'; interface PromoteRc { pluginApiClient: PluginApiClient; + project: Project; rcRelease: GhGetReleaseResponse; releaseVersion: string; successCb?: ComponentConfigPromoteRc['successCb']; @@ -30,6 +32,7 @@ interface PromoteRc { export function promoteRc({ pluginApiClient, + project, rcRelease, releaseVersion, successCb, @@ -38,6 +41,7 @@ export function promoteRc({ const responseSteps: ResponseStep[] = []; const { release } = await pluginApiClient.promoteRc.promoteRelease({ + ...project, releaseId: rcRelease.id, releaseVersion, }); diff --git a/plugins/github-release-manager/src/components/ProjectContext.ts b/plugins/github-release-manager/src/contexts/PluginApiClientContext.ts similarity index 100% rename from plugins/github-release-manager/src/components/ProjectContext.ts rename to plugins/github-release-manager/src/contexts/PluginApiClientContext.ts diff --git a/plugins/github-release-manager/src/contexts/ProjectContext.ts b/plugins/github-release-manager/src/contexts/ProjectContext.ts new file mode 100644 index 0000000000..8e4e8df0ae --- /dev/null +++ b/plugins/github-release-manager/src/contexts/ProjectContext.ts @@ -0,0 +1,55 @@ +/* + * 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 Project { + /** + * Repository's owner (user or organisation) + * + * @example erikengervall + */ + owner: string; + /** + * Repository's name + * + * @example dockest + */ + repo: string; + /** + * Declares the versioning strategy of the project + * + * semver: `1.2.3` (major.minor.patch) + * calver: `2020.01.01_0` (YYYY.0M.0D_patch) + * + * Default: false + */ + versioningStrategy: 'calver' | 'semver'; +} + +export const ProjectContext = createContext(undefined); + +export const useProjectContext = () => { + const project = useContext(ProjectContext); + + if (!project) { + throw new GitHubReleaseManagerError('project not found'); + } + + return project; +}; diff --git a/plugins/github-release-manager/src/helpers/getBumpedTag.ts b/plugins/github-release-manager/src/helpers/getBumpedTag.ts index 7e2ebadd35..5409f7d2b3 100644 --- a/plugins/github-release-manager/src/helpers/getBumpedTag.ts +++ b/plugins/github-release-manager/src/helpers/getBumpedTag.ts @@ -17,7 +17,7 @@ import { CalverTagParts } from './tagParts/getCalverTagParts'; import { getTagParts } from './tagParts/getTagParts'; import { isCalverTagParts } from './isCalverTagParts'; -import { Project } from '../types/types'; +import { Project } from '../contexts/ProjectContext'; import { SEMVER_PARTS } from '../constants/constants'; import { SemverTagParts } from './tagParts/getSemverTagParts'; diff --git a/plugins/github-release-manager/src/sideEffects/getGitHubBatchInfo.ts b/plugins/github-release-manager/src/sideEffects/getGitHubBatchInfo.ts index 9c7a220ea1..8a1bec9165 100644 --- a/plugins/github-release-manager/src/sideEffects/getGitHubBatchInfo.ts +++ b/plugins/github-release-manager/src/sideEffects/getGitHubBatchInfo.ts @@ -14,19 +14,22 @@ * limitations under the License. */ -import { PluginApiClient } from '../api/PluginApiClient'; import { getLatestRelease } from './getLatestRelease'; +import { PluginApiClient } from '../api/PluginApiClient'; +import { Project } from '../contexts/ProjectContext'; interface GetGitHubBatchInfo { + project: Project; pluginApiClient: PluginApiClient; } export const getGitHubBatchInfo = ({ + project, pluginApiClient, }: GetGitHubBatchInfo) => async () => { const [{ repository }, latestRelease] = await Promise.all([ - pluginApiClient.getRepository(), - getLatestRelease({ pluginApiClient }), + pluginApiClient.getRepository({ ...project }), + getLatestRelease({ project, pluginApiClient }), ]); if (latestRelease === null) { @@ -38,6 +41,7 @@ export const getGitHubBatchInfo = ({ } const { branch } = await pluginApiClient.getBranch({ + ...project, branchName: latestRelease.target_commitish, }); diff --git a/plugins/github-release-manager/src/sideEffects/getLatestRelease.ts b/plugins/github-release-manager/src/sideEffects/getLatestRelease.ts index 9d3ebf647d..ef609cfd5e 100644 --- a/plugins/github-release-manager/src/sideEffects/getLatestRelease.ts +++ b/plugins/github-release-manager/src/sideEffects/getLatestRelease.ts @@ -15,19 +15,25 @@ */ import { PluginApiClient } from '../api/PluginApiClient'; +import { Project } from '../contexts/ProjectContext'; interface GetLatestRelease { pluginApiClient: PluginApiClient; + project: Project; } -export async function getLatestRelease({ pluginApiClient }: GetLatestRelease) { - const { releases } = await pluginApiClient.getReleases(); +export async function getLatestRelease({ + pluginApiClient, + project, +}: GetLatestRelease) { + const { releases } = await pluginApiClient.getReleases({ ...project }); if (releases.length === 0) { return null; } const { latestRelease } = await pluginApiClient.getRelease({ + ...project, releaseId: releases[0].id, }); diff --git a/plugins/github-release-manager/src/types/types.ts b/plugins/github-release-manager/src/types/types.ts index 6eafd8f797..be0fa6169b 100644 --- a/plugins/github-release-manager/src/types/types.ts +++ b/plugins/github-release-manager/src/types/types.ts @@ -14,44 +14,31 @@ * limitations under the License. */ -export interface Project { - /** A unique (in the context of GitHub Release Manager) project name */ - name: string; +// export interface Project { +// /** +// * Repository's owner (user or organisation) +// * +// * @example erikengervall +// */ +// owner: string; - /** GitHub details */ - github: { - /** - * Repository's organization - * - * @example erikengervall - */ - org: string; - /** - * Repository's name - * - * @example dockest - */ - repo: string; - }; +// /** +// * Repository's name +// * +// * @example dockest +// */ +// repo: string; - /** Slack details */ - slack?: { - /** Relevant slack channel for the project */ - channel: string; - /** Link to slack channel */ - link?: string; - }; - - /** - * Declares the versioning strategy of the project - * - * semver: `1.2.3` (major.minor.patch) - * calver: `2020.01.01_0` (YYYY.0M.0D_patch) - * - * Default: false - */ - versioningStrategy: 'calver' | 'semver'; -} +// /** +// * Declares the versioning strategy of the project +// * +// * semver: `1.2.3` (major.minor.patch) +// * calver: `2020.01.01_0` (YYYY.0M.0D_patch) +// * +// * Default: false +// */ +// versioningStrategy: 'calver' | 'semver'; +// } interface ComponentConfig { successCb?: (args: Args) => Promise | void; From 6691570ec105091326e7696590dc903ef04b9439 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Wed, 14 Apr 2021 23:20:42 +0200 Subject: [PATCH 014/140] Replace root component props with input fields Signed-off-by: Erik Engervall --- plugins/github-release-manager/package.json | 1 + .../src/GitHubReleaseManager.tsx | 135 ++++++++++-------- .../src/api/PluginApiClient.ts | 49 ++++++- .../src/cards/patchRc/PatchBody.tsx | 4 +- .../src/cards/projectForm/Owner.tsx | 85 +++++++++++ .../src/cards/projectForm/Repo.tsx | 90 ++++++++++++ .../src/cards/projectForm/RepoDetailsForm.tsx | 66 +++++++++ .../cards/projectForm/VersioningStrategy.tsx | 62 ++++++++ .../src/cards/projectForm/isProjectValid.tsx | 25 ++++ .../src/cards/projectForm/styles.ts | 29 ++++ .../components/CenteredCircularProgress.tsx | 26 ++++ .../ResponseStepList/ResponseStepList.tsx | 4 +- .../src/helpers/tagParts/getCalverTagParts.ts | 6 +- .../src/helpers/tagParts/getSemverTagParts.ts | 5 + .../src/helpers/tagParts/getTagParts.test.ts | 8 ++ 15 files changed, 526 insertions(+), 69 deletions(-) create mode 100644 plugins/github-release-manager/src/cards/projectForm/Owner.tsx create mode 100644 plugins/github-release-manager/src/cards/projectForm/Repo.tsx create mode 100644 plugins/github-release-manager/src/cards/projectForm/RepoDetailsForm.tsx create mode 100644 plugins/github-release-manager/src/cards/projectForm/VersioningStrategy.tsx create mode 100644 plugins/github-release-manager/src/cards/projectForm/isProjectValid.tsx create mode 100644 plugins/github-release-manager/src/cards/projectForm/styles.ts create mode 100644 plugins/github-release-manager/src/components/CenteredCircularProgress.tsx diff --git a/plugins/github-release-manager/package.json b/plugins/github-release-manager/package.json index 5eebc11105..ec067920e4 100644 --- a/plugins/github-release-manager/package.json +++ b/plugins/github-release-manager/package.json @@ -29,6 +29,7 @@ "@octokit/rest": "^18.0.12", "luxon": "^1.26.0", "react-dom": "^16.13.1", + "react-hook-form": "^6.6.0", "react-router": "6.0.0-beta.0", "react-use": "^15.3.3", "react": "^16.13.1" diff --git a/plugins/github-release-manager/src/GitHubReleaseManager.tsx b/plugins/github-release-manager/src/GitHubReleaseManager.tsx index 83ccab5dd1..3f7887ad53 100644 --- a/plugins/github-release-manager/src/GitHubReleaseManager.tsx +++ b/plugins/github-release-manager/src/GitHubReleaseManager.tsx @@ -15,10 +15,11 @@ */ import { Alert } from '@material-ui/lab'; -import { CircularProgress, makeStyles } from '@material-ui/core'; +import { makeStyles } from '@material-ui/core'; import { useAsync } from 'react-use'; -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import { useApi, ContentHeader, ErrorBoundary } from '@backstage/core'; +import { useForm } from 'react-hook-form'; import { CreateRc } from './cards/createRc/CreateRc'; import { getGitHubBatchInfo } from './sideEffects/getGitHubBatchInfo'; @@ -35,11 +36,11 @@ import { PluginApiClientContext, usePluginApiClientContext, } from './contexts/PluginApiClientContext'; -import { - ProjectContext, - useProjectContext, - Project, -} from './contexts/ProjectContext'; +import { ProjectContext, Project } from './contexts/ProjectContext'; +import { isProjectValid } from './cards/projectForm/isProjectValid'; +import { InfoCardPlus } from './components/InfoCardPlus'; +import { RepoDetailsForm } from './cards/projectForm/RepoDetailsForm'; +import { CenteredCircularProgress } from './components/CenteredCircularProgress'; interface GitHubReleaseManagerProps { components?: { @@ -62,30 +63,54 @@ export function GitHubReleaseManager({ }: GitHubReleaseManagerProps) { const pluginApiClient = useApi(githubReleaseManagerApiRef); const classes = useStyles(); + const usernameResponse = useAsync(() => pluginApiClient.getUsername()); + const { control, watch } = useForm(); + const project: Project = watch('repo-details-form'); - const project: Project = { - owner: 'erikengervall', - repo: 'playground', - versioningStrategy: 'semver', - }; + if (usernameResponse.error) { + return {usernameResponse.error.message}; + } + + if (usernameResponse.loading) { + return ; + } + + if (!usernameResponse.value?.username) { + return Unable to retrieve username; + } return ( - - {/* @ts-ignore-error TODO: Update interface for PluginApiClient */} - -
- + +
+ - -
-
- + + + + + {isProjectValid(project) && ( + + )} +
+
); } -function Cards({ components }: GitHubReleaseManagerProps) { +function Cards({ + components, + project, +}: { + components: GitHubReleaseManagerProps['components']; + project: Project; +}) { const pluginApiClient = usePluginApiClientContext(); - const project = useProjectContext(); const [refetch, setRefetch] = useState(0); const gitHubBatchInfo = useAsync( getGitHubBatchInfo({ project, pluginApiClient }), @@ -97,11 +122,7 @@ function Cards({ components }: GitHubReleaseManagerProps) { } if (gitHubBatchInfo.loading) { - return ( -
- -
- ); + return ; } if (gitHubBatchInfo.value === undefined) { @@ -120,38 +141,40 @@ function Cards({ components }: GitHubReleaseManagerProps) { } return ( - - - - {components?.default?.createRc?.omit !== true && ( - + + - )} - {components?.default?.promoteRc?.omit !== true && ( - - )} + {components?.default?.createRc?.omit !== true && ( + + )} - {components?.default?.patch?.omit !== true && ( - - )} - + {components?.default?.promoteRc?.omit !== true && ( + + )} + + {components?.default?.patch?.omit !== true && ( + + )} + +
); } diff --git a/plugins/github-release-manager/src/api/PluginApiClient.ts b/plugins/github-release-manager/src/api/PluginApiClient.ts index 0615221c77..c0a6476b2b 100644 --- a/plugins/github-release-manager/src/api/PluginApiClient.ts +++ b/plugins/github-release-manager/src/api/PluginApiClient.ts @@ -157,10 +157,13 @@ export interface IPluginApiClient { } & PartialProject, ) => Promise; }; + + getOrganizations: (args: { ownerIsUser: boolean }) => Promise; + getUsername: () => Promise<{ username: string }>; + getRepositories: (args: { owner: string; username: string }) => Promise; } export class PluginApiClient implements IPluginApiClient { - // private readonly getAccessToken: any; private readonly githubAuthApi: OAuthApi; private readonly baseUrl: string; readonly host: string; @@ -174,8 +177,6 @@ export class PluginApiClient implements IPluginApiClient { }) { this.githubAuthApi = githubAuthApi; - // this.getAccessToken = () => this.githubAuthApi.getAccessToken(); - const githubIntegrationConfig = this.getGithubIntegrationConfig({ configApi, }); @@ -190,11 +191,13 @@ export class PluginApiClient implements IPluginApiClient { configApi.getOptionalConfigArray('integrations.github') ?? [], ); - const githubIntegrationConfig = configs.find( - v => v.host === 'github.com' || v.host.startsWith('ghe.'), + const githubIntegrationEnterpriseConfig = configs.find(v => + v.host.startsWith('ghe.'), ); + const githubIntegrationConfig = configs.find(v => v.host === 'github.com'); - return githubIntegrationConfig; + // Prioritize enterprise configs if available + return githubIntegrationEnterpriseConfig ?? githubIntegrationConfig; } private async getOctokit() { @@ -216,6 +219,40 @@ export class PluginApiClient implements IPluginApiClient { return `${owner}/${repo}`; } + async getOrganizations() { + const { octokit } = await this.getOctokit(); + const { data: orgs } = await octokit.orgs.listForAuthenticatedUser(); + + return { orgs }; + } + + async getRepositories({ + owner, + username, + }: { + owner: string; + username: string; + }) { + const { octokit } = await this.getOctokit(); + + if (owner === username) { + const { data: repos } = await octokit.repos.listForUser({ username }); + + return { repos }; + } + + const { data: repos } = await octokit.repos.listForOrg({ org: owner }); + + return { repos }; + } + + async getUsername() { + const { octokit } = await this.getOctokit(); + const { data: user } = await octokit.users.getAuthenticated(); + + return { username: user.login }; + } + async getRecentCommits({ owner, repo, diff --git a/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx b/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx index 22edde9ad9..7eea133827 100644 --- a/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx +++ b/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx @@ -20,7 +20,6 @@ import { useAsync, useAsyncFn } from 'react-use'; import { Button, Checkbox, - CircularProgress, IconButton, Link, List, @@ -50,6 +49,7 @@ import { useStyles } from '../../styles/styles'; import { TEST_IDS } from '../../test-helpers/test-ids'; import { patch } from './sideEffects/patch'; import { useProjectContext } from '../../contexts/ProjectContext'; +import { CenteredCircularProgress } from '../../components/CenteredCircularProgress'; interface PatchBodyProps { bumpedTag: string; @@ -124,7 +124,7 @@ export const PatchBody = ({ return {patchReleaseResponse.error.message}; } if (githubDataResponse.loading) { - return ; + return ; } function Description() { diff --git a/plugins/github-release-manager/src/cards/projectForm/Owner.tsx b/plugins/github-release-manager/src/cards/projectForm/Owner.tsx new file mode 100644 index 0000000000..f95d66159f --- /dev/null +++ b/plugins/github-release-manager/src/cards/projectForm/Owner.tsx @@ -0,0 +1,85 @@ +/* + * 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 { useAsync } from 'react-use'; +import { ControllerRenderProps } from 'react-hook-form'; +import { Alert } from '@material-ui/lab'; +import { FormControl, InputLabel, MenuItem, Select } from '@material-ui/core'; + +import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext'; +import { useFormClasses } from './styles'; +import { CenteredCircularProgress } from '../../components/CenteredCircularProgress'; +import { Project } from '../../contexts/ProjectContext'; + +export function Owner({ + controllerRenderProps, + username, +}: { + controllerRenderProps: ControllerRenderProps; + username: string; +}) { + const pluginApiClient = usePluginApiClientContext(); + const formClasses = useFormClasses(); + const project: Project = controllerRenderProps.value; + + const { loading, error, value } = useAsync(() => + pluginApiClient.getOrganizations(), + ); + + if (error) { + return {error.message}; + } + + if (loading) { + return ; + } + + if (!value?.orgs) { + return Could not fetch organizations; + } + + return ( + + Organizations + + + ); +} diff --git a/plugins/github-release-manager/src/cards/projectForm/Repo.tsx b/plugins/github-release-manager/src/cards/projectForm/Repo.tsx new file mode 100644 index 0000000000..9c83c30565 --- /dev/null +++ b/plugins/github-release-manager/src/cards/projectForm/Repo.tsx @@ -0,0 +1,90 @@ +/* + * 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 { useAsync } from 'react-use'; +import { FormControl, InputLabel, Select, MenuItem } from '@material-ui/core'; +import { Alert } from '@material-ui/lab'; +import { ControllerRenderProps, useForm } from 'react-hook-form'; + +import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext'; +import { useFormClasses } from './styles'; +import { CenteredCircularProgress } from '../../components/CenteredCircularProgress'; +import { Project } from '../../contexts/ProjectContext'; + +export function Repo({ + username, + controllerRenderProps, +}: { + username: string; + controllerRenderProps: ControllerRenderProps; +}) { + const pluginApiClient = usePluginApiClientContext(); + const formClasses = useFormClasses(); + const project: Project = controllerRenderProps.value; + + const { loading, error, value } = useAsync( + () => + pluginApiClient.getRepositories({ + owner: project.owner, + username, + }), + [project.owner], + ); + + if (error) { + return {error.message}; + } + + if (loading) { + return ; + } + + if (!value?.repos) { + return ( + + Could not fetch repositories for "{project.owner}" + + ); + } + + return ( + + Repositories + + + ); +} diff --git a/plugins/github-release-manager/src/cards/projectForm/RepoDetailsForm.tsx b/plugins/github-release-manager/src/cards/projectForm/RepoDetailsForm.tsx new file mode 100644 index 0000000000..03996b484d --- /dev/null +++ b/plugins/github-release-manager/src/cards/projectForm/RepoDetailsForm.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 { Controller, useForm } from 'react-hook-form'; +import { Project } from '../../contexts/ProjectContext'; + +import { VersioningStrategy } from './VersioningStrategy'; +import { Owner } from './Owner'; +import { Repo } from './Repo'; + +export function RepoDetailsForm({ + control, + username, +}: { + control: ReturnType['control']; + username: string; +}) { + return ( + { + const project: Project = controllerRenderProps.value; + + return ( + <> + + + + + {project.owner.length > 0 && ( + + )} + + ); + }} + control={control} + name="repo-details-form" + defaultValue={ + { + owner: '', + repo: '', + versioningStrategy: 'semver', + } as Project + } + /> + ); +} diff --git a/plugins/github-release-manager/src/cards/projectForm/VersioningStrategy.tsx b/plugins/github-release-manager/src/cards/projectForm/VersioningStrategy.tsx new file mode 100644 index 0000000000..b6fa367b00 --- /dev/null +++ b/plugins/github-release-manager/src/cards/projectForm/VersioningStrategy.tsx @@ -0,0 +1,62 @@ +/* + * 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 { + FormControl, + FormControlLabel, + FormLabel, + Radio, + RadioGroup, +} from '@material-ui/core'; +import React from 'react'; +import { ControllerRenderProps } from 'react-hook-form'; +import { Project } from '../../contexts/ProjectContext'; + +export function VersioningStrategy({ + controllerRenderProps, +}: { + controllerRenderProps: ControllerRenderProps; +}) { + const project: Project = controllerRenderProps.value; + + return ( + + Calendar strategy + { + controllerRenderProps.onChange({ + ...project, + versioningStrategy: event.target.value, + } as Project); + }} + > + } + label="Semantic versioning" + /> + } + label="Calendar versioning" + /> + + + ); +} diff --git a/plugins/github-release-manager/src/cards/projectForm/isProjectValid.tsx b/plugins/github-release-manager/src/cards/projectForm/isProjectValid.tsx new file mode 100644 index 0000000000..dd6e9871bb --- /dev/null +++ b/plugins/github-release-manager/src/cards/projectForm/isProjectValid.tsx @@ -0,0 +1,25 @@ +/* + * 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 { Project } from '../../contexts/ProjectContext'; + +export function isProjectValid(project: any): project is Project { + return ( + project?.owner?.length > 0 && + project?.repo?.length > 0 && + project?.versioningStrategy?.length > 0 + ); +} diff --git a/plugins/github-release-manager/src/cards/projectForm/styles.ts b/plugins/github-release-manager/src/cards/projectForm/styles.ts new file mode 100644 index 0000000000..274d0523ac --- /dev/null +++ b/plugins/github-release-manager/src/cards/projectForm/styles.ts @@ -0,0 +1,29 @@ +/* + * 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 { createStyles, makeStyles, Theme } from '@material-ui/core'; + +export const useFormClasses = makeStyles((theme: Theme) => + createStyles({ + formControl: { + margin: theme.spacing(1), + minWidth: 120, + }, + selectEmpty: { + marginTop: theme.spacing(2), + }, + }), +); diff --git a/plugins/github-release-manager/src/components/CenteredCircularProgress.tsx b/plugins/github-release-manager/src/components/CenteredCircularProgress.tsx new file mode 100644 index 0000000000..83b6314021 --- /dev/null +++ b/plugins/github-release-manager/src/components/CenteredCircularProgress.tsx @@ -0,0 +1,26 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React from 'react'; +import { CircularProgress } from '@material-ui/core'; + +export const CenteredCircularProgress = () => { + return ( +
+ +
+ ); +}; diff --git a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.tsx b/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.tsx index ab545153f7..29b8b13dac 100644 --- a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.tsx +++ b/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.tsx @@ -17,7 +17,6 @@ import React, { PropsWithChildren } from 'react'; import { List, - CircularProgress, Button, Dialog, DialogActions, @@ -28,6 +27,7 @@ import { import { ResponseStep, SetRefetch } from '../../types/types'; import { TEST_IDS } from '../../test-helpers/test-ids'; import { ResponseStepListItem } from './ResponseStepListItem'; +import { CenteredCircularProgress } from '../CenteredCircularProgress'; interface ResponseStepListProps { responseSteps?: ResponseStep[]; @@ -68,7 +68,7 @@ export const ResponseStepList = ({ {loading || !responseSteps ? (
-
diff --git a/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.ts b/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.ts index 27fe59844a..0fdd31444e 100644 --- a/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.ts +++ b/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.ts @@ -22,10 +22,10 @@ export type CalverTagParts = { patch: number; }; +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( - /(rc|version)-([0-9]{4}\.[0-9]{2}\.[0-9]{2})_([0-9]+)/, - ); + const result = tag.match(calverRegexp); if (result === null || result.length < 4) { throw new GitHubReleaseManagerError('Invalid calver tag'); diff --git a/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.ts b/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.ts index eabf1c0a7e..018dedcee0 100644 --- a/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.ts +++ b/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.ts @@ -15,6 +15,7 @@ */ import { GitHubReleaseManagerError } from '../../errors/GitHubReleaseManagerError'; +import { calverRegexp } from './getCalverTagParts'; export type SemverTagParts = { prefix: string; @@ -30,6 +31,10 @@ export function getSemverTagParts(tag: string) { throw new GitHubReleaseManagerError('Invalid semver tag'); } + if (tag.match(calverRegexp)) { + throw new GitHubReleaseManagerError('Invalid semver tag, found calver'); + } + const tagParts: SemverTagParts = { prefix: result[1], major: parseInt(result[2], 10), diff --git a/plugins/github-release-manager/src/helpers/tagParts/getTagParts.test.ts b/plugins/github-release-manager/src/helpers/tagParts/getTagParts.test.ts index 7493887d15..91a753dbe3 100644 --- a/plugins/github-release-manager/src/helpers/tagParts/getTagParts.test.ts +++ b/plugins/github-release-manager/src/helpers/tagParts/getTagParts.test.ts @@ -109,5 +109,13 @@ describe('getTagParts', () => { getTagParts({ project: mockSemverProject, tag: 'rc-1.2' }), ).toThrowErrorMatchingInlineSnapshot(`"Invalid semver tag"`); }); + + it('should throw for invalid semver (founds calver)', () => { + expect(() => + getTagParts({ project: mockSemverProject, tag: 'rc-1337.01.01_1' }), + ).toThrowErrorMatchingInlineSnapshot( + `"Invalid semver tag, found calver"`, + ); + }); }); }); From 39914e88ee6df2a01a624bf2aebf4e09c03203f8 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Thu, 15 Apr 2021 16:44:09 +0200 Subject: [PATCH 015/140] Introduce useVersioningStrategyMatchesRepoTags to validate versioningStratefy match at an earlier stage Signed-off-by: Erik Engervall --- .../src/GitHubReleaseManager.tsx | 22 ++++++-- .../src/api/PluginApiClient.ts | 3 +- .../src/cards/patchRc/sideEffects/patch.ts | 20 +++---- .../src/helpers/tagParts/getTagParts.ts | 2 +- .../useVersioningStrategyMatchesRepoTags.ts | 52 +++++++++++++++++++ .../github-release-manager/src/types/types.ts | 26 ---------- 6 files changed, 84 insertions(+), 41 deletions(-) create mode 100644 plugins/github-release-manager/src/helpers/useVersioningStrategyMatchesRepoTags.ts diff --git a/plugins/github-release-manager/src/GitHubReleaseManager.tsx b/plugins/github-release-manager/src/GitHubReleaseManager.tsx index 3f7887ad53..3876d2b3fc 100644 --- a/plugins/github-release-manager/src/GitHubReleaseManager.tsx +++ b/plugins/github-release-manager/src/GitHubReleaseManager.tsx @@ -14,12 +14,12 @@ * limitations under the License. */ +import React, { useState } from 'react'; +import { useAsync } from 'react-use'; +import { useForm } from 'react-hook-form'; import { Alert } from '@material-ui/lab'; import { makeStyles } from '@material-ui/core'; -import { useAsync } from 'react-use'; -import React, { useEffect, useState } from 'react'; import { useApi, ContentHeader, ErrorBoundary } from '@backstage/core'; -import { useForm } from 'react-hook-form'; import { CreateRc } from './cards/createRc/CreateRc'; import { getGitHubBatchInfo } from './sideEffects/getGitHubBatchInfo'; @@ -41,6 +41,7 @@ import { isProjectValid } from './cards/projectForm/isProjectValid'; import { InfoCardPlus } from './components/InfoCardPlus'; import { RepoDetailsForm } from './cards/projectForm/RepoDetailsForm'; import { CenteredCircularProgress } from './components/CenteredCircularProgress'; +import { useVersioningStrategyMatchesRepoTags } from './helpers/useVersioningStrategyMatchesRepoTags'; interface GitHubReleaseManagerProps { components?: { @@ -117,6 +118,12 @@ function Cards({ [project, refetch], ); + const { versioningStrategyMatches } = useVersioningStrategyMatchesRepoTags({ + latestReleaseTagName: gitHubBatchInfo.value?.latestRelease?.tag_name, + project, + repositoryName: gitHubBatchInfo.value?.repository.name, + }); + if (gitHubBatchInfo.error) { return {gitHubBatchInfo.error.message}; } @@ -140,6 +147,15 @@ function Cards({ ); } + if (!versioningStrategyMatches) { + return ( + + Versioning mismatch, expected {project.versioningStrategy} version, got{' '} + {gitHubBatchInfo.value?.latestRelease?.tag_name} + + ); + } + return ( diff --git a/plugins/github-release-manager/src/api/PluginApiClient.ts b/plugins/github-release-manager/src/api/PluginApiClient.ts index c0a6476b2b..24310ef688 100644 --- a/plugins/github-release-manager/src/api/PluginApiClient.ts +++ b/plugins/github-release-manager/src/api/PluginApiClient.ts @@ -304,7 +304,7 @@ export class PluginApiClient implements IPluginApiClient { const { octokit } = await this.getOctokit(); const { data: repository } = await octokit.repos.get({ - owner: owner, + owner, repo, }); @@ -312,6 +312,7 @@ export class PluginApiClient implements IPluginApiClient { repository: { pushPermissions: repository.permissions?.push, defaultBranch: repository.default_branch, + name: repository.name, }, }; } diff --git a/plugins/github-release-manager/src/cards/patchRc/sideEffects/patch.ts b/plugins/github-release-manager/src/cards/patchRc/sideEffects/patch.ts index de24b5b142..7bb0d5e6f0 100644 --- a/plugins/github-release-manager/src/cards/patchRc/sideEffects/patch.ts +++ b/plugins/github-release-manager/src/cards/patchRc/sideEffects/patch.ts @@ -23,8 +23,8 @@ import { import { CalverTagParts } from '../../../helpers/tagParts/getCalverTagParts'; import { GitHubReleaseManagerError } from '../../../errors/GitHubReleaseManagerError'; import { PluginApiClient } from '../../../api/PluginApiClient'; -import { SemverTagParts } from '../../../helpers/tagParts/getSemverTagParts'; import { Project } from '../../../contexts/ProjectContext'; +import { SemverTagParts } from '../../../helpers/tagParts/getSemverTagParts'; interface Patch { bumpedTag: string; @@ -181,15 +181,15 @@ export async function patch({ /** * 9. Update release */ - const { - release: updatedRelease, - } = await pluginApiClient.patch.updateRelease({ - ...project, - bumpedTag, - latestRelease, - selectedPatchCommit, - tagParts, - }); + const { release: updatedRelease } = await pluginApiClient.patch.updateRelease( + { + ...project, + bumpedTag, + latestRelease, + selectedPatchCommit, + tagParts, + }, + ); responseSteps.push({ message: `Updated release "${updatedRelease.name}"`, secondaryMessage: `with tag ${updatedRelease.tag_name}`, diff --git a/plugins/github-release-manager/src/helpers/tagParts/getTagParts.ts b/plugins/github-release-manager/src/helpers/tagParts/getTagParts.ts index ff42779d99..a3f8665554 100644 --- a/plugins/github-release-manager/src/helpers/tagParts/getTagParts.ts +++ b/plugins/github-release-manager/src/helpers/tagParts/getTagParts.ts @@ -16,7 +16,7 @@ import { getCalverTagParts } from './getCalverTagParts'; import { getSemverTagParts } from './getSemverTagParts'; -import { Project } from '../../types/types'; +import { Project } from '../../contexts/ProjectContext'; export function getTagParts({ project, diff --git a/plugins/github-release-manager/src/helpers/useVersioningStrategyMatchesRepoTags.ts b/plugins/github-release-manager/src/helpers/useVersioningStrategyMatchesRepoTags.ts new file mode 100644 index 0000000000..91f4501585 --- /dev/null +++ b/plugins/github-release-manager/src/helpers/useVersioningStrategyMatchesRepoTags.ts @@ -0,0 +1,52 @@ +/* + * 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 { Project } from '../contexts/ProjectContext'; +import { getTagParts } from './tagParts/getTagParts'; + +export const useVersioningStrategyMatchesRepoTags = ({ + latestReleaseTagName, + project, + repositoryName, +}: { + latestReleaseTagName?: string; + project: Project; + repositoryName?: string; +}) => { + const [versioningStrategyMatches, setVersioningStrategyMatches] = useState( + false, + ); + useEffect(() => { + setVersioningStrategyMatches(false); + + if (latestReleaseTagName) { + try { + if (project.repo === repositoryName) { + getTagParts({ project, tag: latestReleaseTagName }); + setVersioningStrategyMatches(true); + } + } catch (error) { + setVersioningStrategyMatches(false); + } + } + }, [latestReleaseTagName, project, repositoryName]); + + return { + versioningStrategyMatches, + }; +}; diff --git a/plugins/github-release-manager/src/types/types.ts b/plugins/github-release-manager/src/types/types.ts index be0fa6169b..04f53fd80a 100644 --- a/plugins/github-release-manager/src/types/types.ts +++ b/plugins/github-release-manager/src/types/types.ts @@ -14,32 +14,6 @@ * limitations under the License. */ -// export interface Project { -// /** -// * Repository's owner (user or organisation) -// * -// * @example erikengervall -// */ -// owner: string; - -// /** -// * Repository's name -// * -// * @example dockest -// */ -// repo: string; - -// /** -// * Declares the versioning strategy of the project -// * -// * semver: `1.2.3` (major.minor.patch) -// * calver: `2020.01.01_0` (YYYY.0M.0D_patch) -// * -// * Default: false -// */ -// versioningStrategy: 'calver' | 'semver'; -// } - interface ComponentConfig { successCb?: (args: Args) => Promise | void; omit?: boolean; From a1163bb8cbb86f278cdb90eeb5cba449021193f5 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Thu, 15 Apr 2021 17:17:52 +0200 Subject: [PATCH 016/140] Fix tests (data-testid props weren't forwarded to CircularProgress) Signed-off-by: Erik Engervall --- .../src/cards/createRc/CreateRc.test.tsx | 12 ++++++++---- .../cards/createRc/sideEffects/createRc.test.ts | 2 ++ .../src/cards/info/Info.test.tsx | 11 ++++++----- .../src/cards/patchRc/Patch.test.tsx | 6 +++++- .../src/cards/patchRc/PatchBody.test.tsx | 10 +++++++--- .../src/cards/patchRc/PatchBody.tsx | 10 +++++----- .../src/cards/patchRc/sideEffects/patch.test.ts | 4 +++- .../src/cards/promoteRc/PromoteRcBody.test.tsx | 14 +++++++++++--- .../src/components/CenteredCircularProgress.tsx | 6 +++--- 9 files changed, 50 insertions(+), 25 deletions(-) diff --git a/plugins/github-release-manager/src/cards/createRc/CreateRc.test.tsx b/plugins/github-release-manager/src/cards/createRc/CreateRc.test.tsx index 6a918c68eb..4bc22398b2 100644 --- a/plugins/github-release-manager/src/cards/createRc/CreateRc.test.tsx +++ b/plugins/github-release-manager/src/cards/createRc/CreateRc.test.tsx @@ -28,13 +28,17 @@ import { } from '../../test-helpers/test-helpers'; import { TEST_IDS } from '../../test-helpers/test-ids'; -jest.mock('../../components/ProjectContext', () => ({ - usePluginApiClientContext: () => mockApiClient, +jest.mock('../../contexts/PluginApiClientContext', () => ({ + usePluginApiClientContext: jest.fn(() => mockApiClient), +})); +jest.mock('../../contexts/ProjectContext', () => ({ + useProjectContext: jest.fn(() => mockCalverProject), })); jest.mock('./getRcGitHubInfo', () => ({ getRcGitHubInfo: () => mockNextGitHubInfo, })); +import { useProjectContext } from '../../contexts/ProjectContext'; import { CreateRc } from './CreateRc'; describe('CreateRc', () => { @@ -43,7 +47,6 @@ describe('CreateRc', () => { , @@ -53,11 +56,12 @@ describe('CreateRc', () => { }); it('should display select element for semver', () => { + (useProjectContext as jest.Mock).mockReturnValue(mockSemverProject); + const { getByTestId } = render( , diff --git a/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.test.ts b/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.test.ts index 713f0d12f2..9f75a6c927 100644 --- a/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.test.ts +++ b/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.test.ts @@ -16,6 +16,7 @@ import { mockApiClient, + mockCalverProject, mockDefaultBranch, mockNextGitHubInfo, mockReleaseVersion, @@ -31,6 +32,7 @@ describe('createRc', () => { defaultBranch: mockDefaultBranch, latestRelease: mockReleaseVersion, nextGitHubInfo: mockNextGitHubInfo, + project: mockCalverProject, }); expect(result).toMatchInlineSnapshot(` diff --git a/plugins/github-release-manager/src/cards/info/Info.test.tsx b/plugins/github-release-manager/src/cards/info/Info.test.tsx index ac278b04c9..ee73f05274 100644 --- a/plugins/github-release-manager/src/cards/info/Info.test.tsx +++ b/plugins/github-release-manager/src/cards/info/Info.test.tsx @@ -22,16 +22,17 @@ import { mockReleaseBranch, } from '../../test-helpers/test-helpers'; import { TEST_IDS } from '../../test-helpers/test-ids'; + +jest.mock('../../contexts/ProjectContext', () => ({ + useProjectContext: jest.fn(() => mockCalverProject), +})); + import { Info } from './Info'; describe('Info', () => { it('should return early if no latestRelease exists', () => { const { getByTestId } = render( - , + , ); expect(getByTestId(TEST_IDS.info.info)).toBeInTheDocument(); diff --git a/plugins/github-release-manager/src/cards/patchRc/Patch.test.tsx b/plugins/github-release-manager/src/cards/patchRc/Patch.test.tsx index fd81b2cd34..3f83a0d9ea 100644 --- a/plugins/github-release-manager/src/cards/patchRc/Patch.test.tsx +++ b/plugins/github-release-manager/src/cards/patchRc/Patch.test.tsx @@ -22,6 +22,11 @@ import { mockCalverProject, } from '../../test-helpers/test-helpers'; import { TEST_IDS } from '../../test-helpers/test-ids'; + +jest.mock('../../contexts/ProjectContext', () => ({ + useProjectContext: jest.fn(() => mockCalverProject), +})); + import { Patch } from './Patch'; describe('Patch', () => { @@ -29,7 +34,6 @@ describe('Patch', () => { const { getByTestId } = render( , diff --git a/plugins/github-release-manager/src/cards/patchRc/PatchBody.test.tsx b/plugins/github-release-manager/src/cards/patchRc/PatchBody.test.tsx index c6020ed3c6..5c208e9955 100644 --- a/plugins/github-release-manager/src/cards/patchRc/PatchBody.test.tsx +++ b/plugins/github-release-manager/src/cards/patchRc/PatchBody.test.tsx @@ -18,16 +18,20 @@ import React from 'react'; import { render, waitFor, screen } from '@testing-library/react'; import { + mockApiClient, mockBumpedTag, + mockCalverProject, mockRcRelease, mockReleaseBranch, mockReleaseVersion, mockTagParts, - mockApiClient, } from '../../test-helpers/test-helpers'; -jest.mock('../../components/ProjectContext', () => ({ - usePluginApiClientContext: () => mockApiClient, +jest.mock('../../contexts/PluginApiClientContext', () => ({ + usePluginApiClientContext: jest.fn(() => mockApiClient), +})); +jest.mock('../../contexts/ProjectContext', () => ({ + useProjectContext: jest.fn(() => mockCalverProject), })); import { PatchBody } from './PatchBody'; diff --git a/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx b/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx index 7eea133827..40582dbc0f 100644 --- a/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx +++ b/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx @@ -15,8 +15,8 @@ */ import React, { useState } from 'react'; -import { Alert, AlertTitle } from '@material-ui/lab'; import { useAsync, useAsyncFn } from 'react-use'; +import { Alert, AlertTitle } from '@material-ui/lab'; import { Button, Checkbox, @@ -42,14 +42,14 @@ import { SetRefetch, } from '../../types/types'; import { CalverTagParts } from '../../helpers/tagParts/getCalverTagParts'; +import { CenteredCircularProgress } from '../../components/CenteredCircularProgress'; +import { patch } from './sideEffects/patch'; import { ResponseStepList } from '../../components/ResponseStepList/ResponseStepList'; import { SemverTagParts } from '../../helpers/tagParts/getSemverTagParts'; -import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext'; -import { useStyles } from '../../styles/styles'; import { TEST_IDS } from '../../test-helpers/test-ids'; -import { patch } from './sideEffects/patch'; +import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext'; import { useProjectContext } from '../../contexts/ProjectContext'; -import { CenteredCircularProgress } from '../../components/CenteredCircularProgress'; +import { useStyles } from '../../styles/styles'; interface PatchBodyProps { bumpedTag: string; diff --git a/plugins/github-release-manager/src/cards/patchRc/sideEffects/patch.test.ts b/plugins/github-release-manager/src/cards/patchRc/sideEffects/patch.test.ts index 2b3b9bf3b2..322fe54255 100644 --- a/plugins/github-release-manager/src/cards/patchRc/sideEffects/patch.test.ts +++ b/plugins/github-release-manager/src/cards/patchRc/sideEffects/patch.test.ts @@ -15,11 +15,12 @@ */ import { + mockApiClient, mockBumpedTag, + mockCalverProject, mockReleaseVersion, mockSelectedPatchCommit, mockTagParts, - mockApiClient, } from '../../../test-helpers/test-helpers'; import { patch } from './patch'; @@ -33,6 +34,7 @@ describe('patch', () => { bumpedTag: mockBumpedTag, selectedPatchCommit: mockSelectedPatchCommit, tagParts: mockTagParts, + project: mockCalverProject, }); expect(result).toMatchInlineSnapshot(` diff --git a/plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.test.tsx b/plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.test.tsx index 0dac702791..a749567043 100644 --- a/plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.test.tsx +++ b/plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.test.tsx @@ -17,12 +17,20 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { mockRcRelease, mockApiClient } from '../../test-helpers/test-helpers'; +import { + mockApiClient, + mockCalverProject, + mockRcRelease, +} from '../../test-helpers/test-helpers'; import { TEST_IDS } from '../../test-helpers/test-ids'; -jest.mock('../../components/ProjectContext', () => ({ - usePluginApiClientContext: () => mockApiClient, +jest.mock('../../contexts/PluginApiClientContext', () => ({ + usePluginApiClientContext: jest.fn(() => mockApiClient), })); +jest.mock('../../contexts/ProjectContext', () => ({ + useProjectContext: jest.fn(() => mockCalverProject), +})); + import { PromoteRcBody } from './PromoteRcBody'; describe('PromoteRcBody', () => { diff --git a/plugins/github-release-manager/src/components/CenteredCircularProgress.tsx b/plugins/github-release-manager/src/components/CenteredCircularProgress.tsx index 83b6314021..262ce13816 100644 --- a/plugins/github-release-manager/src/components/CenteredCircularProgress.tsx +++ b/plugins/github-release-manager/src/components/CenteredCircularProgress.tsx @@ -15,12 +15,12 @@ */ import React from 'react'; -import { CircularProgress } from '@material-ui/core'; +import { CircularProgress, CircularProgressProps } from '@material-ui/core'; -export const CenteredCircularProgress = () => { +export const CenteredCircularProgress = (props: CircularProgressProps) => { return (
- +
); }; From 6c68b63d3243c0e90f51ff27c6806a3b62f5f3c1 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Thu, 15 Apr 2021 20:59:41 +0200 Subject: [PATCH 017/140] Continue overhaul of the API's interface - normalize and decouple dependency on GitHub Signed-off-by: Erik Engervall --- .../src/GitHubReleaseManager.tsx | 10 +- .../src/api/PluginApiClient.ts | 271 +++++++++++------- .../src/cards/createRc/CreateRc.tsx | 10 +- .../cards/createRc/getRcGitHubInfo.test.ts | 10 +- .../src/cards/createRc/getRcGitHubInfo.ts | 8 +- .../createRc/sideEffects/createRc.test.ts | 2 +- .../cards/createRc/sideEffects/createRc.ts | 64 ++--- .../src/cards/info/Info.tsx | 10 +- .../src/cards/patchRc/Patch.tsx | 8 +- .../src/cards/patchRc/PatchBody.test.tsx | 2 +- .../src/cards/patchRc/PatchBody.tsx | 222 +++++++------- .../src/cards/patchRc/sideEffects/patch.ts | 16 +- .../src/cards/projectForm/Owner.tsx | 8 +- .../src/cards/projectForm/Repo.tsx | 18 +- .../src/cards/projectForm/RepoDetailsForm.tsx | 5 +- .../src/cards/promoteRc/PromoteRc.tsx | 11 +- .../src/cards/promoteRc/PromoteRcBody.tsx | 15 +- .../promoteRc/sideEffects/promoteRc.test.ts | 4 +- .../cards/promoteRc/sideEffects/promoteRc.ts | 21 +- .../src/contexts/PluginApiClientContext.ts | 4 +- .../src/helpers/isCalverTagParts.ts | 2 +- .../src/sideEffects/getGitHubBatchInfo.ts | 11 +- .../src/sideEffects/getLatestRelease.test.ts | 43 --- .../src/sideEffects/getLatestRelease.ts | 41 --- .../src/test-helpers/test-helpers.test.ts | 52 +--- .../src/test-helpers/test-helpers.ts | 104 +++---- .../github-release-manager/src/types/types.ts | 4 +- 27 files changed, 455 insertions(+), 521 deletions(-) delete mode 100644 plugins/github-release-manager/src/sideEffects/getLatestRelease.test.ts delete mode 100644 plugins/github-release-manager/src/sideEffects/getLatestRelease.ts diff --git a/plugins/github-release-manager/src/GitHubReleaseManager.tsx b/plugins/github-release-manager/src/GitHubReleaseManager.tsx index 3876d2b3fc..a52db523d4 100644 --- a/plugins/github-release-manager/src/GitHubReleaseManager.tsx +++ b/plugins/github-release-manager/src/GitHubReleaseManager.tsx @@ -81,11 +81,7 @@ export function GitHubReleaseManager({ } return ( - +
@@ -119,7 +115,7 @@ function Cards({ ); const { versioningStrategyMatches } = useVersioningStrategyMatchesRepoTags({ - latestReleaseTagName: gitHubBatchInfo.value?.latestRelease?.tag_name, + latestReleaseTagName: gitHubBatchInfo.value?.latestRelease?.tagName, project, repositoryName: gitHubBatchInfo.value?.repository.name, }); @@ -151,7 +147,7 @@ function Cards({ return ( Versioning mismatch, expected {project.versioningStrategy} version, got{' '} - {gitHubBatchInfo.value?.latestRelease?.tag_name} + {gitHubBatchInfo.value.latestRelease?.tagName} ); } diff --git a/plugins/github-release-manager/src/api/PluginApiClient.ts b/plugins/github-release-manager/src/api/PluginApiClient.ts index 24310ef688..d348680cd1 100644 --- a/plugins/github-release-manager/src/api/PluginApiClient.ts +++ b/plugins/github-release-manager/src/api/PluginApiClient.ts @@ -19,10 +19,8 @@ import { Octokit } from '@octokit/rest'; import { readGitHubIntegrationConfigs } from '@backstage/integration'; import { - GhCompareCommitsResponse, GhCreateCommitResponse, GhCreateReferenceResponse, - GhCreateReleaseResponse, GhCreateTagObjectResponse, GhGetBranchResponse, GhGetCommitResponse, @@ -36,32 +34,72 @@ import { getRcGitHubInfo } from '../cards/createRc/getRcGitHubInfo'; import { SemverTagParts } from '../helpers/tagParts/getSemverTagParts'; import { Project } from '../contexts/ProjectContext'; -// export type UnboxPromise> = T extends Promise -// ? U -// : never; +type UnboxPromise> = T extends Promise + ? U + : never; -type Todo = any; +export type ApiMethodRetval< + T extends (...args: any) => Promise +> = UnboxPromise>; + +type Todo = any; // TODO: type PartialProject = Omit; export interface IPluginApiClient { getHost: () => string; + getRepoPath: (args: PartialProject) => string; + + getOrganizations: () => Promise<{ organizations: string[] }>; + + getRepositories: (args: { + owner: string; + }) => Promise<{ repositories: string[] }>; + + getUsername: () => Promise<{ username: string }>; + getRecentCommits: ( args: { releaseBranchName?: string } & PartialProject, - ) => Promise; - getReleases: (args: { releaseId: number } & PartialProject) => Promise; - getRelease: (args: { releaseId: number } & PartialProject) => Promise; + ) => Promise<{ + recentCommits: { + sha: string; + author: { + htmlUrl?: string; + login?: string; + }; + commit: { + message: string; + }; + }[]; + }>; + + getLatestRelease: ( + args: PartialProject, + ) => Promise<{ + latestRelease: { + targetCommitish: string; + tagName: string; + prerelease: boolean; + id: number; + htmlUrl: string; + body?: string | null; + } | null; + }>; + getRepository: ( args: PartialProject, ) => Promise<{ repository: { pushPermissions: boolean | undefined; defaultBranch: string; + name: string; }; }>; + getLatestCommit: ( args: { defaultBranch: string } & PartialProject, ) => Promise; + getBranch: (args: { branchName: string } & PartialProject) => Promise; createRc: { @@ -70,21 +108,27 @@ export interface IPluginApiClient { mostRecentSha: string; targetBranch: string; } & PartialProject, - ) => Promise; + ) => Promise<{ ref: string }>; getComparison: ( args: { previousReleaseBranch: string; nextReleaseBranch: string; } & PartialProject, - ) => Promise; + ) => Promise<{ htmlUrl: string; aheadBy: number }>; createRelease: ( args: { nextGitHubInfo: ReturnType; releaseBody: string; } & PartialProject, - ) => Promise; + ) => Promise<{ + createReleaseResponse: { + name: string | null; + htmlUrl: string; + tagName: string; + }; + }>; }; patch: { @@ -142,7 +186,9 @@ export interface IPluginApiClient { updateRelease: ( args: { bumpedTag: string; - latestRelease: GhGetReleaseResponse; + latestRelease: NonNullable< + ApiMethodRetval['latestRelease'] + >; tagParts: SemverTagParts | CalverTagParts; selectedPatchCommit: GhGetCommitResponse; } & PartialProject, @@ -157,10 +203,6 @@ export interface IPluginApiClient { } & PartialProject, ) => Promise; }; - - getOrganizations: (args: { ownerIsUser: boolean }) => Promise; - getUsername: () => Promise<{ username: string }>; - getRepositories: (args: { owner: string; username: string }) => Promise; } export class PluginApiClient implements IPluginApiClient { @@ -221,36 +263,46 @@ export class PluginApiClient implements IPluginApiClient { async getOrganizations() { const { octokit } = await this.getOctokit(); - const { data: orgs } = await octokit.orgs.listForAuthenticatedUser(); + const orgListResponse = await octokit.paginate( + octokit.orgs.listForAuthenticatedUser, + { per_page: 100 }, + ); - return { orgs }; + return { + organizations: orgListResponse.map(organization => organization.login), + }; } - async getRepositories({ - owner, - username, - }: { - owner: string; - username: string; - }) { + async getRepositories({ owner }: { owner: string }) { const { octokit } = await this.getOctokit(); - if (owner === username) { - const { data: repos } = await octokit.repos.listForUser({ username }); + const repositoryResponse = await octokit + .paginate(octokit.repos.listForOrg, { org: owner, per_page: 100 }) + .catch(async error => { + // `owner` is not an org, try listing a user's repositories instead + if (error.status === 404) { + const userRepositoryResponse = await octokit.paginate( + octokit.repos.listForUser, + { username: owner, per_page: 100 }, + ); + return userRepositoryResponse; + } - return { repos }; - } + throw error; + }); - const { data: repos } = await octokit.repos.listForOrg({ org: owner }); - - return { repos }; + return { + repositories: repositoryResponse.map(repository => repository.name), + }; } async getUsername() { const { octokit } = await this.getOctokit(); - const { data: user } = await octokit.users.getAuthenticated(); + const userResponse = await octokit.users.getAuthenticated(); - return { username: user.login }; + return { + username: userResponse.data.login, + }; } async getRecentCommits({ @@ -261,43 +313,52 @@ export class PluginApiClient implements IPluginApiClient { releaseBranchName?: string; } & PartialProject) { const { octokit } = await this.getOctokit(); - const sha = releaseBranchName ? `?sha=${releaseBranchName}` : ''; + const recentCommitsResponse = await octokit.repos.listCommits({ + owner, + repo, + ...(releaseBranchName ? { sha: releaseBranchName } : {}), + }); - const recentCommits: GhGetCommitResponse[] = ( - await octokit.request( - `/repos/${this.getRepoPath({ owner, repo })}/commits${sha}`, - ) - ).data; - - return { recentCommits }; + return { + recentCommits: recentCommitsResponse.data.map(commit => ({ + sha: commit.sha, + author: { + htmlUrl: commit.author?.html_url, + login: commit.author?.login, + }, + commit: { + message: commit.commit.message, + }, + })), + }; } - async getReleases({ owner, repo }: PartialProject) { + async getLatestRelease({ owner, repo }: PartialProject) { const { octokit } = await this.getOctokit(); + const { data: latestReleases } = await octokit.repos.listReleases({ + owner, + repo, + per_page: 1, + }); - const releases: GhGetReleaseResponse[] = ( - await octokit.request( - `/repos/${this.getRepoPath({ owner, repo })}/releases`, - ) - ).data; + if (latestReleases.length === 0) { + return { + latestRelease: null, + }; + } - return { releases }; - } + const latestRelease = latestReleases[0]; - async getRelease({ - owner, - repo, - releaseId, - }: { releaseId: number } & PartialProject) { - const { octokit } = await this.getOctokit(); - - const latestRelease: GhGetReleaseResponse = ( - await octokit.request( - `/repos/${this.getRepoPath({ owner, repo })}/releases/${releaseId}`, - ) - ).data; - - return { latestRelease }; + return { + latestRelease: { + targetCommitish: latestRelease.target_commitish, + tagName: latestRelease.tag_name, + prerelease: latestRelease.prerelease, + id: latestRelease.id, + htmlUrl: latestRelease.html_url, + body: latestRelease.body, + }, + }; } async getRepository({ owner, repo }: PartialProject) { @@ -363,21 +424,16 @@ export class PluginApiClient implements IPluginApiClient { targetBranch: string; } & PartialProject) => { const { octokit } = await this.getOctokit(); + const createRefResponse = await octokit.git.createRef({ + owner, + repo, + ref: `refs/heads/${targetBranch}`, + sha: mostRecentSha, + }); - const createdRef: GhCreateReferenceResponse = ( - await octokit.request( - `/repos/${this.getRepoPath({ owner, repo })}/git/refs`, - { - method: 'POST', - data: { - ref: `refs/heads/${targetBranch}`, - sha: mostRecentSha, - }, - }, - ) - ).data; - - return { createdRef }; + return { + ref: createRefResponse.data.ref, + }; }, getComparison: async ({ @@ -390,17 +446,17 @@ export class PluginApiClient implements IPluginApiClient { nextReleaseBranch: string; } & PartialProject) => { const { octokit } = await this.getOctokit(); + const compareCommitsResponse = await octokit.repos.compareCommits({ + owner, + repo, + base: previousReleaseBranch, + head: nextReleaseBranch, + }); - const comparison: GhCompareCommitsResponse = ( - await octokit.request( - `/repos/${this.getRepoPath({ - owner, - repo, - })}/compare/${previousReleaseBranch}...${nextReleaseBranch}`, - ) - ).data; - - return { comparison }; + return { + htmlUrl: compareCommitsResponse.data.html_url, + aheadBy: compareCommitsResponse.data.ahead_by, + }; }, createRelease: async ({ @@ -413,24 +469,23 @@ export class PluginApiClient implements IPluginApiClient { releaseBody: string; } & PartialProject) => { const { octokit } = await this.getOctokit(); + const createReleaseResponse = await octokit.repos.createRelease({ + owner, + repo, + tag_name: nextGitHubInfo.rcReleaseTag, + name: nextGitHubInfo.releaseName, + target_commitish: nextGitHubInfo.rcBranch, + body: releaseBody, + prerelease: true, + }); - const createReleaseResponse: GhCreateReleaseResponse = ( - await octokit.request( - `/repos/${this.getRepoPath({ owner, repo })}/releases`, - { - method: 'POST', - data: { - tag_name: nextGitHubInfo.rcReleaseTag, - name: nextGitHubInfo.releaseName, - target_commitish: nextGitHubInfo.rcBranch, - body: releaseBody, - prerelease: true, - }, - }, - ) - ).data; - - return { createReleaseResponse }; + return { + createReleaseResponse: { + name: createReleaseResponse.data.name, + htmlUrl: createReleaseResponse.data.html_url, + tagName: createReleaseResponse.data.tag_name, + }, + }; }, }; @@ -640,7 +695,9 @@ export class PluginApiClient implements IPluginApiClient { selectedPatchCommit, }: { bumpedTag: string; - latestRelease: GhGetReleaseResponse; + latestRelease: NonNullable< + ApiMethodRetval['latestRelease'] + >; tagParts: SemverTagParts | CalverTagParts; selectedPatchCommit: GhGetCommitResponse; } & PartialProject) => { diff --git a/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx b/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx index 2f2fb6963b..b36478f082 100644 --- a/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx +++ b/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx @@ -33,7 +33,6 @@ import { InfoCardPlus } from '../../components/InfoCardPlus'; import { ComponentConfigCreateRc, GhGetBranchResponse, - GhGetReleaseResponse, GhGetRepositoryResponse, SetRefetch, } from '../../types/types'; @@ -43,10 +42,13 @@ import { TEST_IDS } from '../../test-helpers/test-ids'; import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext'; import { useProjectContext } from '../../contexts/ProjectContext'; import { useStyles } from '../../styles/styles'; +import { ApiMethodRetval, IPluginApiClient } from '../../api/PluginApiClient'; interface CreateRcProps { defaultBranch: GhGetRepositoryResponse['default_branch']; - latestRelease: GhGetReleaseResponse | null; + latestRelease: ApiMethodRetval< + IPluginApiClient['getLatestRelease'] + >['latestRelease']; releaseBranch: GhGetBranchResponse | null; setRefetch: SetRefetch; successCb?: ComponentConfigCreateRc['successCb']; @@ -97,7 +99,7 @@ export const CreateRc = ({ const tagAlreadyExists = latestRelease !== null && - latestRelease.tag_name === nextGitHubInfo.rcReleaseTag; + latestRelease.tagName === nextGitHubInfo.rcReleaseTag; const conflictingPreRelease = latestRelease !== null && latestRelease.prerelease; @@ -132,7 +134,7 @@ export const CreateRc = ({ diff --git a/plugins/github-release-manager/src/cards/createRc/getRcGitHubInfo.test.ts b/plugins/github-release-manager/src/cards/createRc/getRcGitHubInfo.test.ts index 08eeacdcc9..31f9b6a4f4 100644 --- a/plugins/github-release-manager/src/cards/createRc/getRcGitHubInfo.test.ts +++ b/plugins/github-release-manager/src/cards/createRc/getRcGitHubInfo.test.ts @@ -16,7 +16,7 @@ import { DateTime } from 'luxon'; -import { GhGetReleaseResponse } from '../../types/types'; +import { ApiMethodRetval, IPluginApiClient } from '../../api/PluginApiClient'; import { mockSemverProject, mockCalverProject, @@ -34,8 +34,8 @@ describe('getRcGitHubInfo', () => { describe('calver', () => { const latestRelease = { - tag_name: 'rc-2020.01.01_0', - } as GhGetReleaseResponse; + tagName: 'rc-2020.01.01_0', + } as ApiMethodRetval['latestRelease']; it('should return correct GitHub info', () => { expect( @@ -57,8 +57,8 @@ describe('getRcGitHubInfo', () => { describe('semver', () => { const latestRelease = { - tag_name: 'rc-1.1.1', - } as GhGetReleaseResponse; + tagName: 'rc-1.1.1', + } as ApiMethodRetval['latestRelease']; it("should return correct GitHub info when there's previous releases", () => { expect( diff --git a/plugins/github-release-manager/src/cards/createRc/getRcGitHubInfo.ts b/plugins/github-release-manager/src/cards/createRc/getRcGitHubInfo.ts index cd2140296f..de70da6f9c 100644 --- a/plugins/github-release-manager/src/cards/createRc/getRcGitHubInfo.ts +++ b/plugins/github-release-manager/src/cards/createRc/getRcGitHubInfo.ts @@ -18,9 +18,9 @@ import { DateTime } from 'luxon'; import { getBumpedSemverTagParts } from '../../helpers/getBumpedTag'; import { getSemverTagParts } from '../../helpers/tagParts/getSemverTagParts'; -import { GhGetReleaseResponse } from '../../types/types'; import { SEMVER_PARTS } from '../../constants/constants'; import { Project } from '../../contexts/ProjectContext'; +import { ApiMethodRetval, IPluginApiClient } from '../../api/PluginApiClient'; export const getRcGitHubInfo = ({ project, @@ -29,7 +29,9 @@ export const getRcGitHubInfo = ({ injectedDate = DateTime.now().toFormat('yyyy.MM.dd'), }: { project: Project; - latestRelease: GhGetReleaseResponse | null; + latestRelease: ApiMethodRetval< + IPluginApiClient['getLatestRelease'] + >['latestRelease']; semverBumpLevel: keyof typeof SEMVER_PARTS; injectedDate?: string; }) => { @@ -49,7 +51,7 @@ export const getRcGitHubInfo = ({ }; } - const tagParts = getSemverTagParts(latestRelease.tag_name); + const tagParts = getSemverTagParts(latestRelease.tagName); const { bumpedTagParts } = getBumpedSemverTagParts(tagParts, semverBumpLevel); const bumpedTag = `${bumpedTagParts.major}.${bumpedTagParts.minor}.${bumpedTagParts.patch}`; diff --git a/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.test.ts b/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.test.ts index 9f75a6c927..e9ad6aca5a 100644 --- a/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.test.ts +++ b/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.test.ts @@ -28,10 +28,10 @@ describe('createRc', () => { it('should work', async () => { const result = await createRc({ - pluginApiClient: mockApiClient, defaultBranch: mockDefaultBranch, latestRelease: mockReleaseVersion, nextGitHubInfo: mockNextGitHubInfo, + pluginApiClient: mockApiClient, project: mockCalverProject, }); diff --git a/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.ts b/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.ts index 0f659fc994..c136f33cc2 100644 --- a/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.ts +++ b/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.ts @@ -17,20 +17,23 @@ import { getRcGitHubInfo } from '../getRcGitHubInfo'; import { ComponentConfigCreateRc, - GhCreateReferenceResponse, - GhGetReleaseResponse, GhGetRepositoryResponse, ResponseStep, } from '../../../types/types'; -import { PluginApiClient } from '../../../api/PluginApiClient'; +import { + ApiMethodRetval, + IPluginApiClient, +} from '../../../api/PluginApiClient'; import { GitHubReleaseManagerError } from '../../../errors/GitHubReleaseManagerError'; import { Project } from '../../../contexts/ProjectContext'; interface CreateRC { defaultBranch: GhGetRepositoryResponse['default_branch']; - latestRelease: GhGetReleaseResponse | null; + latestRelease: ApiMethodRetval< + IPluginApiClient['getLatestRelease'] + >['latestRelease']; nextGitHubInfo: ReturnType; - pluginApiClient: PluginApiClient; + pluginApiClient: IPluginApiClient; project: Project; successCb?: ComponentConfigCreateRc['successCb']; } @@ -62,23 +65,20 @@ export async function createRc({ * 2. Create a new ref based on the default branch's most recent sha */ const mostRecentSha = latestCommit.sha; - let createdRef: GhCreateReferenceResponse; - try { - createdRef = ( - await pluginApiClient.createRc.createRef({ - ...project, - mostRecentSha, - targetBranch: nextGitHubInfo.rcBranch, - }) - ).createdRef; - } catch (error) { - if (error.body.message === 'Reference already exists') { - throw new GitHubReleaseManagerError( - `Branch "${nextGitHubInfo.rcBranch}" already exists: .../tree/${nextGitHubInfo.rcBranch}`, - ); - } - throw error; - } + const createdRef = await pluginApiClient.createRc + .createRef({ + ...project, + mostRecentSha, + targetBranch: nextGitHubInfo.rcBranch, + }) + .catch(error => { + if (error?.body?.message === 'Reference already exists') { + throw new GitHubReleaseManagerError( + `Branch "${nextGitHubInfo.rcBranch}" already exists: .../tree/${nextGitHubInfo.rcBranch}`, + ); + } + throw error; + }); responseSteps.push({ message: 'Cut Release Branch', secondaryMessage: `with ref "${createdRef.ref}"`, @@ -88,17 +88,17 @@ export async function createRc({ * 3. Compose a body for the release */ const previousReleaseBranch = latestRelease - ? latestRelease.target_commitish + ? latestRelease.targetCommitish : defaultBranch; const nextReleaseBranch = nextGitHubInfo.rcBranch; - const { comparison } = await pluginApiClient.createRc.getComparison({ + const comparison = await pluginApiClient.createRc.getComparison({ ...project, previousReleaseBranch, nextReleaseBranch, }); - const releaseBody = `**Compare** ${comparison.html_url} + const releaseBody = `**Compare** ${comparison.htmlUrl} -**Ahead by** ${comparison.ahead_by} commits +**Ahead by** ${comparison.aheadBy} commits **Release branch** ${createdRef.ref} @@ -108,7 +108,7 @@ export async function createRc({ responseSteps.push({ message: 'Fetched commit comparison', secondaryMessage: `${previousReleaseBranch}...${nextReleaseBranch}`, - link: comparison.html_url, + link: comparison.htmlUrl, }); /** @@ -124,15 +124,15 @@ export async function createRc({ responseSteps.push({ message: `Created Release Candidate "${createReleaseResponse.name}"`, secondaryMessage: `with tag "${nextGitHubInfo.rcReleaseTag}"`, - link: createReleaseResponse.html_url, + link: createReleaseResponse.htmlUrl, }); await successCb?.({ - gitHubReleaseUrl: createReleaseResponse.html_url, + gitHubReleaseUrl: createReleaseResponse.htmlUrl, gitHubReleaseName: createReleaseResponse.name, - comparisonUrl: comparison.html_url, - previousTag: latestRelease?.tag_name, - createdTag: createReleaseResponse.tag_name, + comparisonUrl: comparison.htmlUrl, + previousTag: latestRelease?.tagName, + createdTag: createReleaseResponse.tagName, }); return responseSteps; diff --git a/plugins/github-release-manager/src/cards/info/Info.tsx b/plugins/github-release-manager/src/cards/info/Info.tsx index 1f42c5b603..782ed30460 100644 --- a/plugins/github-release-manager/src/cards/info/Info.tsx +++ b/plugins/github-release-manager/src/cards/info/Info.tsx @@ -18,20 +18,24 @@ import React from 'react'; import { Link, Typography } from '@material-ui/core'; import { Differ } from '../../components/Differ'; -import { GhGetBranchResponse, GhGetReleaseResponse } from '../../types/types'; +import { GhGetBranchResponse } from '../../types/types'; import { InfoCardPlus } from '../../components/InfoCardPlus'; import { TEST_IDS } from '../../test-helpers/test-ids'; import { useProjectContext } from '../../contexts/ProjectContext'; import { useStyles } from '../../styles/styles'; import flowImage from './flow.png'; +import { ApiMethodRetval, IPluginApiClient } from '../../api/PluginApiClient'; interface InfoCardProps { releaseBranch: GhGetBranchResponse | null; - latestRelease: GhGetReleaseResponse | null; + latestRelease: ApiMethodRetval< + IPluginApiClient['getLatestRelease'] + >['latestRelease']; } export const Info = ({ releaseBranch, latestRelease }: InfoCardProps) => { const project = useProjectContext(); + const classes = useStyles(); return ( @@ -98,7 +102,7 @@ export const Info = ({ releaseBranch, latestRelease }: InfoCardProps) => { - Latest release: + Latest release:
diff --git a/plugins/github-release-manager/src/cards/patchRc/Patch.tsx b/plugins/github-release-manager/src/cards/patchRc/Patch.tsx index e0c1be7a24..e19287024c 100644 --- a/plugins/github-release-manager/src/cards/patchRc/Patch.tsx +++ b/plugins/github-release-manager/src/cards/patchRc/Patch.tsx @@ -23,15 +23,17 @@ import { NoLatestRelease } from '../../components/NoLatestRelease'; import { ComponentConfigPatch, GhGetBranchResponse, - GhGetReleaseResponse, SetRefetch, } from '../../types/types'; import { PatchBody } from './PatchBody'; import { useProjectContext } from '../../contexts/ProjectContext'; import { useStyles } from '../../styles/styles'; +import { ApiMethodRetval, IPluginApiClient } from '../../api/PluginApiClient'; interface PatchProps { - latestRelease: GhGetReleaseResponse | null; + latestRelease: ApiMethodRetval< + IPluginApiClient['getLatestRelease'] + >['latestRelease']; releaseBranch: GhGetBranchResponse | null; setRefetch: SetRefetch; successCb?: ComponentConfigPatch['successCb']; @@ -53,7 +55,7 @@ export const Patch = ({ const { bumpedTag, tagParts } = getBumpedTag({ project, - tag: latestRelease.tag_name, + tag: latestRelease.tagName, bumpLevel: 'patch', }); diff --git a/plugins/github-release-manager/src/cards/patchRc/PatchBody.test.tsx b/plugins/github-release-manager/src/cards/patchRc/PatchBody.test.tsx index 5c208e9955..54daf2894f 100644 --- a/plugins/github-release-manager/src/cards/patchRc/PatchBody.test.tsx +++ b/plugins/github-release-manager/src/cards/patchRc/PatchBody.test.tsx @@ -41,7 +41,7 @@ describe('PatchBody', () => { beforeEach(jest.clearAllMocks); it('should render error', async () => { - mockApiClient.getBranch.mockImplementationOnce(() => { + (mockApiClient.getBranch as jest.Mock).mockImplementationOnce(() => { throw new Error('banana'); }); diff --git a/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx b/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx index 40582dbc0f..44907d30f1 100644 --- a/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx +++ b/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx @@ -38,7 +38,6 @@ import { ComponentConfigPatch, GhGetBranchResponse, GhGetCommitResponse, - GhGetReleaseResponse, SetRefetch, } from '../../types/types'; import { CalverTagParts } from '../../helpers/tagParts/getCalverTagParts'; @@ -50,10 +49,13 @@ import { TEST_IDS } from '../../test-helpers/test-ids'; import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext'; import { useProjectContext } from '../../contexts/ProjectContext'; import { useStyles } from '../../styles/styles'; +import { ApiMethodRetval, IPluginApiClient } from '../../api/PluginApiClient'; interface PatchBodyProps { bumpedTag: string; - latestRelease: GhGetReleaseResponse; + latestRelease: NonNullable< + ApiMethodRetval['latestRelease'] + >; releaseBranch: GhGetBranchResponse | null; setRefetch: SetRefetch; successCb?: ComponentConfigPatch['successCb']; @@ -75,17 +77,17 @@ export const PatchBody = ({ const githubDataResponse = useAsync(async () => { const [ { branch: releaseBranchResponse }, - { recentCommits }, + { recentCommits: recentCommitsOnDefaultBranch }, ] = await Promise.all([ pluginApiClient.getBranch({ ...project, - branchName: latestRelease.target_commitish, + branchName: latestRelease.targetCommitish, }), pluginApiClient.getRecentCommits({ ...project }), ]); const { - recentCommits: recentReleaseBranchCommits, + recentCommits: recentCommitsOnReleaseBranch, } = await pluginApiClient.getRecentCommits({ ...project, releaseBranchName: releaseBranchResponse.name, @@ -93,8 +95,8 @@ export const PatchBody = ({ return { releaseBranch: releaseBranchResponse, - recentReleaseBranchCommits, - recentCommits, + recentCommitsOnReleaseBranch, + recentCommitsOnDefaultBranch, }; }); @@ -146,118 +148,118 @@ export const PatchBody = ({ )} - + ); } function CommitList() { - if (!githubDataResponse.value?.recentCommits) { + if (!githubDataResponse.value?.recentCommitsOnDefaultBranch) { return null; } return ( - {githubDataResponse.value.recentCommits.map((commit, index) => { - const commitExistsOnReleaseBranch = !!githubDataResponse.value?.recentReleaseBranchCommits.find( - ({ sha }) => { - return sha === commit.sha; - }, - ); + {githubDataResponse.value.recentCommitsOnDefaultBranch.map( + (commit, index) => { + const commitExistsOnReleaseBranch = !!githubDataResponse.value?.recentCommitsOnReleaseBranch.find( + releaseBranchCommit => releaseBranchCommit.sha === commit.sha, + ); - return ( -
- {commitExistsOnReleaseBranch && ( - - {' '} - Already exists on {releaseBranch?.name} - - )} - - 0) || - commitExistsOnReleaseBranch - } - role={undefined} - dense - button - onClick={() => { - if (index === checkedCommitIndex) { - setCheckedCommitIndex(-1); - } else { - setCheckedCommitIndex(index); - } - }} - > - - - - - - {commit.sha}{' '} - - @{commit.author.login} - - - } - /> - - - { - const repoPath = pluginApiClient.getRepoPath({ - ...project, - }); - const host = pluginApiClient.getHost(); - - const newTab = window.open( - `https://${host}/${repoPath}/compare/${releaseBranch?.name}...${commit.sha}`, - '_blank', - ); - newTab?.focus(); + return ( +
+ {commitExistsOnReleaseBranch && ( + - - - - -
- ); - })} + {' '} + Already exists on {releaseBranch?.name} + + )} + + 0) || + commitExistsOnReleaseBranch + } + role={undefined} + dense + button + onClick={() => { + if (index === checkedCommitIndex) { + setCheckedCommitIndex(-1); + } else { + setCheckedCommitIndex(index); + } + }} + > + + + + + + {commit.sha}{' '} + + @{commit.author.login} + + + } + /> + + + { + const repoPath = pluginApiClient.getRepoPath({ + ...project, + }); + const host = pluginApiClient.getHost(); + + const newTab = window.open( + `https://${host}/${repoPath}/compare/${releaseBranch?.name}...${commit.sha}`, + '_blank', + ); + newTab?.focus(); + }} + > + + + + +
+ ); + }, + )}
); } @@ -275,7 +277,11 @@ export const PatchBody = ({ ); } - if (!githubDataResponse.value?.recentCommits[checkedCommitIndex]) { + if ( + !githubDataResponse.value?.recentCommitsOnDefaultBranch[ + checkedCommitIndex + ] + ) { return ( - ); - } - + ]; return ( )}
diff --git a/plugins/github-release-manager/src/cards/Cards.tsx b/plugins/github-release-manager/src/cards/Cards.tsx index 52a44ecd8b..f39697b664 100644 --- a/plugins/github-release-manager/src/cards/Cards.tsx +++ b/plugins/github-release-manager/src/cards/Cards.tsx @@ -94,32 +94,34 @@ export function Cards({ )} - + {components?.info?.omit !== true && ( + + )} - {components?.default?.createRc?.omit !== true && ( + {components?.createRc?.omit !== true && ( )} - {components?.default?.promoteRc?.omit !== true && ( + {components?.promoteRc?.omit !== true && ( )} - {components?.default?.patch?.omit !== true && ( + {components?.patch?.omit !== true && ( )} diff --git a/plugins/github-release-manager/src/cards/projectForm/Owner.tsx b/plugins/github-release-manager/src/cards/projectForm/Owner.tsx index 5e7d5b2f30..8f67038697 100644 --- a/plugins/github-release-manager/src/cards/projectForm/Owner.tsx +++ b/plugins/github-release-manager/src/cards/projectForm/Owner.tsx @@ -46,7 +46,12 @@ export function Owner({ username }: { username: string }) { .includes(project.owner); return ( - + {loading ? ( ) : ( diff --git a/plugins/github-release-manager/src/cards/projectForm/Repo.tsx b/plugins/github-release-manager/src/cards/projectForm/Repo.tsx index fb05783e3e..4c7b995c44 100644 --- a/plugins/github-release-manager/src/cards/projectForm/Repo.tsx +++ b/plugins/github-release-manager/src/cards/projectForm/Repo.tsx @@ -52,7 +52,12 @@ export function Repo() { const customRepoFromUrl = !repositories.concat(['']).includes(project.repo); return ( - + {loading ? ( ) : ( diff --git a/plugins/github-release-manager/src/cards/projectForm/VersioningStrategy.tsx b/plugins/github-release-manager/src/cards/projectForm/VersioningStrategy.tsx index 23d42009df..ebbee5b419 100644 --- a/plugins/github-release-manager/src/cards/projectForm/VersioningStrategy.tsx +++ b/plugins/github-release-manager/src/cards/projectForm/VersioningStrategy.tsx @@ -36,7 +36,7 @@ export function VersioningStrategy() { useEffect(() => { const { parsedQuery } = getParsedQuery(); - if (!parsedQuery.versioningStrategy) { + if (!parsedQuery.versioningStrategy && !project.isProvidedViaProps) { const { queryParams } = getQueryParamsWithUpdates({ updates: [ { key: 'versioningStrategy', value: project.versioningStrategy }, @@ -48,7 +48,11 @@ export function VersioningStrategy() { }, []); // eslint-disable-line react-hooks/exhaustive-deps return ( - + Calendar strategy (undefined); 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 293bf01891..fb2735182e 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 @@ -52,6 +52,7 @@ describe('testHelpers', () => { }, "mockBumpedTag": "rc-2020.01.01_1337", "mockCalverProject": Object { + "isProvidedViaProps": false, "owner": "mock_owner", "repo": "mock_repo", "versioningStrategy": "calver", @@ -112,6 +113,7 @@ describe('testHelpers', () => { "sha": "mock_sha_selected_patch_commit", }, "mockSemverProject": Object { + "isProvidedViaProps": false, "owner": "mock_owner", "repo": "mock_repo", "versioningStrategy": "semver", 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 80200e2ff0..19e3c57184 100644 --- a/plugins/github-release-manager/src/test-helpers/test-helpers.ts +++ b/plugins/github-release-manager/src/test-helpers/test-helpers.ts @@ -31,12 +31,14 @@ export const mockSemverProject: Project = { owner: mockOwner, repo: mockRepo, versioningStrategy: 'semver', + isProvidedViaProps: false, }; export const mockCalverProject: Project = { owner: mockOwner, repo: mockRepo, versioningStrategy: 'calver', + isProvidedViaProps: false, }; export const mockSearchCalver = `?versioningStrategy=${mockCalverProject.versioningStrategy}&owner=${mockCalverProject.owner}&repo=${mockCalverProject.repo}`; diff --git a/plugins/github-release-manager/src/types/types.ts b/plugins/github-release-manager/src/types/types.ts index a92200614d..84a27add68 100644 --- a/plugins/github-release-manager/src/types/types.ts +++ b/plugins/github-release-manager/src/types/types.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -interface ComponentConfig { +export interface ComponentConfig { successCb?: (args: Args) => Promise | void; omit?: boolean; } From 68fe248d6326aee62bce7f042f2903186a2daa87 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Sun, 18 Apr 2021 18:22:09 +0200 Subject: [PATCH 037/140] Slightly improve types Signed-off-by: Erik Engervall --- plugins/github-release-manager/README.md | 10 +++++ plugins/github-release-manager/dev/index.tsx | 41 ++++++++++++++----- .../src/cards/Cards.tsx | 8 ++-- .../github-release-manager/src/types/types.ts | 25 ++++++----- 4 files changed, 59 insertions(+), 25 deletions(-) diff --git a/plugins/github-release-manager/README.md b/plugins/github-release-manager/README.md index 075068c51a..43c842bc26 100644 --- a/plugins/github-release-manager/README.md +++ b/plugins/github-release-manager/README.md @@ -46,4 +46,14 @@ Looking at the flow above, a common release lifecycle could be: ## Usage +### Importing + The plugin exports a single full-page extension `GitHubReleaseManagerPage`, which one can add to an app like a usual top-level tool on a dedicated route. + +### Configuration + +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. + +See the plugin's dev folder (`dev/index.tsx`) to see some examples. diff --git a/plugins/github-release-manager/dev/index.tsx b/plugins/github-release-manager/dev/index.tsx index b6897e2a9b..bca24d56a4 100644 --- a/plugins/github-release-manager/dev/index.tsx +++ b/plugins/github-release-manager/dev/index.tsx @@ -16,12 +16,13 @@ import React from 'react'; import { createDevApp } from '@backstage/dev-utils'; -import { Alert } from '@material-ui/lab'; +import { Typography } from '@material-ui/core'; import { gitHubReleaseManagerPlugin, GitHubReleaseManagerPage, } from '../src/plugin'; +import { InfoCardPlus } from '../src/components/InfoCardPlus'; function DevWrapper({ children }: { children: React.ReactNode }) { return
{children}
; @@ -31,21 +32,30 @@ createDevApp() .registerPlugin(gitHubReleaseManagerPlugin) .addPage({ title: 'Dynamic', + path: '/dynamic', element: ( - <> - Configure via select inputs + + + Dev notes + Configure plugin via select inputs + - - - - + + ), }) .addPage({ title: 'Static', + path: '/static', element: ( - Statically configured via props + + Dev notes + + Configure plugin statically by passing props to the + `GitHubReleaseManagerPage` component + + - Optionally omit components + + Dev notes + Each components can be omitted + Success callbacks can also be added + diff --git a/plugins/github-release-manager/src/cards/Cards.tsx b/plugins/github-release-manager/src/cards/Cards.tsx index f39697b664..5773717cd8 100644 --- a/plugins/github-release-manager/src/cards/Cards.tsx +++ b/plugins/github-release-manager/src/cards/Cards.tsx @@ -94,14 +94,14 @@ export function Cards({ )} - {components?.info?.omit !== true && ( + {!components?.info?.omit && ( )} - {components?.createRc?.omit !== true && ( + {!components?.createRc?.omit && ( )} - {components?.promoteRc?.omit !== true && ( + {!components?.promoteRc?.omit && ( )} - {components?.patch?.omit !== true && ( + {!components?.patch?.omit && ( { - successCb?: (args: Args) => Promise | void; - omit?: boolean; -} +export type ComponentConfig = + | { + omit?: void; + successCb?: (args: Args) => Promise | void; + } + | { + omit: boolean; + successCb?: void; + }; -interface ComponentConfigCreateRcSuccessCbArgs { +interface CreateRcSuccessCbArgs { gitHubReleaseUrl: string; gitHubReleaseName: string | null; comparisonUrl: string; previousTag?: string; createdTag: string; } -export type ComponentConfigCreateRc = ComponentConfig; +export type ComponentConfigCreateRc = ComponentConfig; -interface ComponentConfigPromoteRcSuccessCbArgs { +interface PromoteRcSuccessCbArgs { gitHubReleaseUrl: string; gitHubReleaseName: string | null; previousTagUrl: string; @@ -36,9 +41,9 @@ interface ComponentConfigPromoteRcSuccessCbArgs { updatedTagUrl: string; updatedTag: string; } -export type ComponentConfigPromoteRc = ComponentConfig; +export type ComponentConfigPromoteRc = ComponentConfig; -interface ComponentConfigPatchSuccessCbArgs { +interface PatchSuccessCbArgs { updatedReleaseUrl: string; updatedReleaseName: string | null; previousTag: string; @@ -46,7 +51,7 @@ interface ComponentConfigPatchSuccessCbArgs { patchCommitUrl: string; patchCommitMessage: string; } -export type ComponentConfigPatch = ComponentConfig; +export type ComponentConfigPatch = ComponentConfig; export interface ResponseStep { message: string | React.ReactNode; From a0145d69c89a474d4cdd11cb7de48463ed050d26 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Sun, 18 Apr 2021 18:24:47 +0200 Subject: [PATCH 038/140] Revert type 'improvement' Signed-off-by: Erik Engervall --- app-config.yaml | 14 +++++++------- plugins/github-release-manager/src/types/types.ts | 13 ++++--------- 2 files changed, 11 insertions(+), 16 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index afbda40852..dd1a3f17eb 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -119,12 +119,12 @@ integrations: - host: github.com token: ${GITHUB_TOKEN} ### Example for how to add your GitHub Enterprise instance using the API: - # - host: ghe.example.net - # apiBaseUrl: https://ghe.example.net/api/v3 - # token: ${GHE_TOKEN} + - host: ghe.spotify.net + apiBaseUrl: https://ghe.spotify.net/api/v3 + token: ${GHE_TOKEN} ### Example for how to add your GitHub Enterprise instance using raw HTTP fetches (token is optional): - # - host: ghe.example.net - # rawBaseUrl: https://ghe.example.net/raw + # - host: ghe.spotify.net + # rawBaseUrl: https://ghe.spotify.net/raw # token: ${GHE_TOKEN} gitlab: - host: gitlab.com @@ -159,8 +159,8 @@ catalog: - target: https://github.com token: ${GITHUB_TOKEN} #### Example for how to add your GitHub Enterprise instance using the API: - # - target: https://ghe.example.net - # apiBaseUrl: https://ghe.example.net/api + # - target: https://ghe.spotify.net + # apiBaseUrl: https://ghe.spotify.net/api # token: ${GHE_TOKEN} ldapOrg: ### Example for how to add your enterprise LDAP server diff --git a/plugins/github-release-manager/src/types/types.ts b/plugins/github-release-manager/src/types/types.ts index 6d7099b9eb..8687150d7e 100644 --- a/plugins/github-release-manager/src/types/types.ts +++ b/plugins/github-release-manager/src/types/types.ts @@ -14,15 +14,10 @@ * limitations under the License. */ -export type ComponentConfig = - | { - omit?: void; - successCb?: (args: Args) => Promise | void; - } - | { - omit: boolean; - successCb?: void; - }; +export type ComponentConfig = { + omit?: boolean; + successCb?: (args: Args) => Promise | void; +}; interface CreateRcSuccessCbArgs { gitHubReleaseUrl: string; From 6436802d298374da72eb8e4a6270e85e46333214 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Tue, 20 Apr 2021 08:29:12 +0200 Subject: [PATCH 039/140] Add useResponseSteps helper and refactor sideeffect accordingly to get iterative progress updates Signed-off-by: Erik Engervall --- .../src/cards/createRc/CreateRc.test.tsx | 7 + .../src/cards/createRc/CreateRc.tsx | 56 ++--- .../createRc/sideEffects/createRc.test.ts | 11 +- .../cards/createRc/sideEffects/createRc.ts | 136 ---------- .../cards/createRc/sideEffects/useCreateRc.ts | 234 ++++++++++++++++++ .../src/cards/patchRc/PatchBody.tsx | 1 - .../src/cards/promoteRc/PromoteRcBody.tsx | 59 ++--- .../components/LinearProgressWithLabel.tsx | 40 +++ .../ResponseStepList/ResponseStepList.tsx | 49 ++-- .../ResponseStepList/ResponseStepList2.tsx | 91 +++++++ .../ResponseStepList/ResponseStepListItem.tsx | 14 +- .../src/helpers/createResponseStepError.ts | 25 ++ .../src/hooks/useResponseSteps.ts | 55 ++++ 13 files changed, 518 insertions(+), 260 deletions(-) delete mode 100644 plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.ts create mode 100644 plugins/github-release-manager/src/cards/createRc/sideEffects/useCreateRc.ts create mode 100644 plugins/github-release-manager/src/components/LinearProgressWithLabel.tsx create mode 100644 plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList2.tsx create mode 100644 plugins/github-release-manager/src/helpers/createResponseStepError.ts create mode 100644 plugins/github-release-manager/src/hooks/useResponseSteps.ts diff --git a/plugins/github-release-manager/src/cards/createRc/CreateRc.test.tsx b/plugins/github-release-manager/src/cards/createRc/CreateRc.test.tsx index 42942d8a38..fa83f39592 100644 --- a/plugins/github-release-manager/src/cards/createRc/CreateRc.test.tsx +++ b/plugins/github-release-manager/src/cards/createRc/CreateRc.test.tsx @@ -37,6 +37,13 @@ jest.mock('../../contexts/ProjectContext', () => ({ jest.mock('./getRcGitHubInfo', () => ({ getRcGitHubInfo: () => mockNextGitHubInfo, })); +jest.mock('./sideEffects/useCreateRc', () => ({ + useCreateRc: () => ({ + run: jest.fn(), + responseSteps: [], + progress: 0, + }), +})); import { useProjectContext } from '../../contexts/ProjectContext'; import { CreateRc } from './CreateRc'; diff --git a/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx b/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx index 3787e1b253..3b08198457 100644 --- a/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx +++ b/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx @@ -18,20 +18,20 @@ import React, { useState, useEffect } from 'react'; import { Alert } from '@material-ui/lab'; import { Button, + Dialog, + DialogTitle, FormControl, InputLabel, MenuItem, Select, Typography, } from '@material-ui/core'; -import { useAsyncFn } from 'react-use'; import { ComponentConfigCreateRc } from '../../types/types'; -import { createRc } from './sideEffects/createRc'; +import { useCreateRc } from './sideEffects/useCreateRc'; import { Differ } from '../../components/Differ'; import { getRcGitHubInfo } from './getRcGitHubInfo'; import { InfoCardPlus } from '../../components/InfoCardPlus'; -import { ResponseStepList } from '../../components/ResponseStepList/ResponseStepList'; import { SEMVER_PARTS } from '../../constants/constants'; import { TEST_IDS } from '../../test-helpers/test-ids'; import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext'; @@ -42,6 +42,8 @@ import { GetLatestReleaseResult, GetRepositoryResult, } from '../../api/PluginApiClient'; +import { ResponseStepList2 } from '../../components/ResponseStepList/ResponseStepList2'; +import { LinearProgressWithLabel } from '../../components/LinearProgressWithLabel'; interface CreateRcProps { defaultBranch: GetRepositoryResult['defaultBranch']; @@ -73,22 +75,23 @@ export const CreateRc = ({ ); }, [semverBumpLevel, setNextGitHubInfo, latestRelease, project]); - const [createGitHubReleaseResponse, createGitHubReleaseFn] = useAsyncFn( - (...args) => - createRc({ - defaultBranch, - latestRelease, - nextGitHubInfo: args[0], - pluginApiClient, - project, - successCb, - }), - ); - if (createGitHubReleaseResponse.error) { + const { run, responseSteps, progress } = useCreateRc({ + defaultBranch, + latestRelease, + nextGitHubInfo, + pluginApiClient, + project, + successCb, + }); + if (responseSteps.length > 0) { return ( - - {createGitHubReleaseResponse.error.message} - + + Create Release Candidate (step {progress}) + + + + + ); } @@ -138,26 +141,15 @@ export const CreateRc = ({ } function CTA() { - if ( - createGitHubReleaseResponse.loading || - createGitHubReleaseResponse.value - ) { - return ( - - ); - } - return ( diff --git a/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.test.ts b/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.test.ts index 824290bbb7..8cecaf6fd5 100644 --- a/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.test.ts +++ b/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.test.ts @@ -21,13 +21,16 @@ import { mockNextGitHubInfo, mockReleaseVersionCalver, } from '../../../test-helpers/test-helpers'; -import { createRc } from './createRc'; +import { useCreateRc } from './useCreateRc'; -describe('createRc', () => { +// TODO: Fix tests +/* eslint-disable jest/no-disabled-tests */ + +describe.skip('useCreateRc', () => { beforeEach(jest.clearAllMocks); - it('should work', async () => { - const result = await createRc({ + it('should work', () => { + const result = useCreateRc({ defaultBranch: mockDefaultBranch, latestRelease: mockReleaseVersionCalver, nextGitHubInfo: mockNextGitHubInfo, diff --git a/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.ts b/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.ts deleted file mode 100644 index 06c408f022..0000000000 --- a/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.ts +++ /dev/null @@ -1,136 +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 { getRcGitHubInfo } from '../getRcGitHubInfo'; -import { ComponentConfigCreateRc, ResponseStep } from '../../../types/types'; -import { - GetLatestReleaseResult, - GetRepositoryResult, - IPluginApiClient, -} from '../../../api/PluginApiClient'; -import { GitHubReleaseManagerError } from '../../../errors/GitHubReleaseManagerError'; -import { Project } from '../../../contexts/ProjectContext'; - -interface CreateRC { - defaultBranch: GetRepositoryResult['defaultBranch']; - latestRelease: GetLatestReleaseResult; - nextGitHubInfo: ReturnType; - pluginApiClient: IPluginApiClient; - project: Project; - successCb?: ComponentConfigCreateRc['successCb']; -} - -export async function createRc({ - defaultBranch, - latestRelease, - nextGitHubInfo, - pluginApiClient, - project, - successCb, -}: CreateRC) { - const responseSteps: ResponseStep[] = []; - - /** - * 1. Get the default branch's most recent commit - */ - const latestCommit = await pluginApiClient.getLatestCommit({ - owner: project.owner, - repo: project.repo, - defaultBranch, - }); - responseSteps.push({ - message: `Fetched latest commit from "${defaultBranch}"`, - secondaryMessage: `with message "${latestCommit.commit.message}"`, - link: latestCommit.htmlUrl, - }); - - /** - * 2. Create a new ref based on the default branch's most recent sha - */ - const mostRecentSha = latestCommit.sha; - const createdRef = await pluginApiClient.createRc - .createRef({ - owner: project.owner, - repo: project.repo, - mostRecentSha, - targetBranch: nextGitHubInfo.rcBranch, - }) - .catch(error => { - if (error?.body?.message === 'Reference already exists') { - throw new GitHubReleaseManagerError( - `Branch "${nextGitHubInfo.rcBranch}" already exists: .../tree/${nextGitHubInfo.rcBranch}`, - ); - } - throw error; - }); - responseSteps.push({ - message: 'Cut Release Branch', - secondaryMessage: `with ref "${createdRef.ref}"`, - }); - - /** - * 3. Compose a body for the release - */ - const previousReleaseBranch = latestRelease - ? latestRelease.targetCommitish - : defaultBranch; - const nextReleaseBranch = nextGitHubInfo.rcBranch; - const comparison = await pluginApiClient.createRc.getComparison({ - owner: project.owner, - repo: project.repo, - previousReleaseBranch, - nextReleaseBranch, - }); - const releaseBody = `**Compare** ${comparison.htmlUrl} - -**Ahead by** ${comparison.aheadBy} commits - -**Release branch** ${createdRef.ref} - ---- - -`; - responseSteps.push({ - message: 'Fetched commit comparison', - secondaryMessage: `${previousReleaseBranch}...${nextReleaseBranch}`, - link: comparison.htmlUrl, - }); - - /** - * 4. Creates the release itself in GitHub - */ - const createReleaseResult = await pluginApiClient.createRc.createRelease({ - owner: project.owner, - repo: project.repo, - nextGitHubInfo: nextGitHubInfo, - releaseBody, - }); - responseSteps.push({ - message: `Created Release Candidate "${createReleaseResult.name}"`, - secondaryMessage: `with tag "${nextGitHubInfo.rcReleaseTag}"`, - link: createReleaseResult.htmlUrl, - }); - - await successCb?.({ - gitHubReleaseUrl: createReleaseResult.htmlUrl, - gitHubReleaseName: createReleaseResult.name, - comparisonUrl: comparison.htmlUrl, - previousTag: latestRelease?.tagName, - createdTag: createReleaseResult.tagName, - }); - - return responseSteps; -} diff --git a/plugins/github-release-manager/src/cards/createRc/sideEffects/useCreateRc.ts b/plugins/github-release-manager/src/cards/createRc/sideEffects/useCreateRc.ts new file mode 100644 index 0000000000..6c3596669e --- /dev/null +++ b/plugins/github-release-manager/src/cards/createRc/sideEffects/useCreateRc.ts @@ -0,0 +1,234 @@ +/* + * 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, useAsyncFn } from 'react-use'; + +import { getRcGitHubInfo } from '../getRcGitHubInfo'; +import { ComponentConfigCreateRc, ResponseStep } from '../../../types/types'; +import { + GetLatestReleaseResult, + GetRepositoryResult, + IPluginApiClient, +} from '../../../api/PluginApiClient'; +import { Project } from '../../../contexts/ProjectContext'; +import { GitHubReleaseManagerError } from '../../../errors/GitHubReleaseManagerError'; +import { useResponseSteps } from '../../../hooks/useResponseSteps'; +import { useEffect, useState } from 'react'; + +interface CreateRC { + defaultBranch: GetRepositoryResult['defaultBranch']; + latestRelease: GetLatestReleaseResult; + nextGitHubInfo: ReturnType; + pluginApiClient: IPluginApiClient; + project: Project; + successCb?: ComponentConfigCreateRc['successCb']; +} + +export function useCreateRc({ + defaultBranch, + latestRelease, + nextGitHubInfo, + pluginApiClient, + project, + successCb, +}: CreateRC) { + const { + responseSteps, + setResponseSteps, + asyncCatcher, + abortIfError: skipIfError, + } = useResponseSteps(); + + /** + * (1) Get the default branch's most recent commit + */ + const getLatestCommit = async () => { + const latestCommit = await pluginApiClient.getLatestCommit({ + owner: project.owner, + repo: project.repo, + defaultBranch, + }); + + const responseStep: ResponseStep = { + message: `Fetched latest commit from "${defaultBranch}"`, + secondaryMessage: `with message "${latestCommit.commit.message}"`, + link: latestCommit.htmlUrl, + }; + setResponseSteps([...responseSteps, responseStep]); + + return { latestCommit }; + }; + + const [latestCommitRes, run] = useAsyncFn(async () => + getLatestCommit().catch(asyncCatcher), + ); + + /** + * (2) Create a new ref based on the default branch's most recent sha + */ + const createRcFromDefaultBranch = async (latestCommitSha: string) => { + const createdRef = await pluginApiClient.createRc + .createRef({ + owner: project.owner, + repo: project.repo, + mostRecentSha: latestCommitSha, + targetBranch: nextGitHubInfo.rcBranch, + }) + .catch(error => { + if (error?.body?.message === 'Reference already exists') { + throw new GitHubReleaseManagerError( + `Branch "${nextGitHubInfo.rcBranch}" already exists: .../tree/${nextGitHubInfo.rcBranch}`, + ); + } + throw error; + }); + + const responseStep: ResponseStep = { + message: 'Cut Release Branch', + secondaryMessage: `with ref "${createdRef.ref}"`, + }; + setResponseSteps([...responseSteps, responseStep]); + + return { ...createdRef }; + }; + + const createRcRes = useAsync(async () => { + skipIfError(latestCommitRes.error); + + if (latestCommitRes.value) { + return createRcFromDefaultBranch( + latestCommitRes.value.latestCommit.sha, + ).catch(asyncCatcher); + } + + return undefined; + }, [latestCommitRes.value, latestCommitRes.error]); + + /** + * (3) Compose a body for the release + */ + const getComparison = async (createdRefRef: string) => { + const previousReleaseBranch = latestRelease + ? latestRelease.targetCommitish + : defaultBranch; + const nextReleaseBranch = nextGitHubInfo.rcBranch; + const comparison = await pluginApiClient.createRc.getComparison({ + owner: project.owner, + repo: project.repo, + previousReleaseBranch, + nextReleaseBranch, + }); + const releaseBody = `**Compare** ${comparison.htmlUrl} + +**Ahead by** ${comparison.aheadBy} commits + +**Release branch** ${createdRefRef} + +--- + +`; + + const responseStep: ResponseStep = { + message: 'Fetched commit comparison', + secondaryMessage: `${previousReleaseBranch}...${nextReleaseBranch}`, + link: comparison.htmlUrl, + }; + setResponseSteps([...responseSteps, responseStep]); + + return { ...comparison, releaseBody }; + }; + + const getComparisonRes = useAsync(async () => { + skipIfError(createRcRes.error); + + if (createRcRes.value) { + return getComparison(createRcRes.value.ref).catch(asyncCatcher); + } + + return undefined; + }, [createRcRes.value, createRcRes.error]); + + /** + * (4) Creates the release itself in GitHub + */ + const createRelease = async (releaseBody: string) => { + const createReleaseResult = await pluginApiClient.createRc.createRelease({ + owner: project.owner, + repo: project.repo, + nextGitHubInfo: nextGitHubInfo, + releaseBody, + }); + + const responseStep: ResponseStep = { + message: `Created Release Candidate "${createReleaseResult.name}"`, + secondaryMessage: `with tag "${nextGitHubInfo.rcReleaseTag}"`, + link: createReleaseResult.htmlUrl, + }; + setResponseSteps([...responseSteps, responseStep]); + + return { ...createReleaseResult }; + }; + + const createReleaseRes = useAsync(async () => { + skipIfError(getComparisonRes.error); + + if (getComparisonRes.value) { + return createRelease(getComparisonRes.value.releaseBody).catch( + asyncCatcher, + ); + } + + return undefined; + }, [getComparisonRes.value, getComparisonRes.error]); + + /** + * (5) Run successCb if defined + */ + useAsync(async () => { + if (successCb && !!createReleaseRes.value && !!getComparisonRes.value) { + skipIfError(createReleaseRes.error); + + try { + await successCb({ + comparisonUrl: getComparisonRes.value.htmlUrl, + createdTag: createReleaseRes.value.tagName, + gitHubReleaseName: createReleaseRes.value.name, + gitHubReleaseUrl: createReleaseRes.value.htmlUrl, + previousTag: latestRelease?.tagName, + }); + } catch (error) { + asyncCatcher(error); + } + + const responseStep: ResponseStep = { + message: 'Success callback successfully called 🚀', + icon: 'success', + }; + setResponseSteps([...responseSteps, responseStep]); + } + }, [createReleaseRes.value]); + + const [progress, setProgress] = useState(0); + useEffect(() => { + setProgress((responseSteps.length / (4 + (!!successCb ? 1 : 0))) * 100); + }, [responseSteps.length, successCb]); + + return { + run, + responseSteps, + progress, + }; +} diff --git a/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx b/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx index f6f16f0199..c32b692c14 100644 --- a/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx +++ b/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx @@ -278,7 +278,6 @@ export const PatchBody = ({ responseSteps={patchReleaseResponse.value} loading={patchReleaseResponse.loading} title="Patch result" - closeable /> ); } diff --git a/plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.tsx b/plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.tsx index ffea5659a1..14a860031b 100644 --- a/plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.tsx +++ b/plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.tsx @@ -16,18 +16,17 @@ import React from 'react'; import { useAsyncFn } from 'react-use'; -import { Alert } from '@material-ui/lab'; -import { Button, Typography } from '@material-ui/core'; +import { Button, Dialog, DialogTitle, Typography } from '@material-ui/core'; import { Differ } from '../../components/Differ'; import { ComponentConfigPromoteRc } from '../../types/types'; import { promoteRc } from './sideEffects/promoteRc'; -import { ResponseStepList } from '../../components/ResponseStepList/ResponseStepList'; import { TEST_IDS } from '../../test-helpers/test-ids'; import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext'; import { useProjectContext } from '../../contexts/ProjectContext'; import { useStyles } from '../../styles/styles'; import { GetLatestReleaseResult } from '../../api/PluginApiClient'; +import { ResponseStepList2 } from '../../components/ResponseStepList/ResponseStepList2'; interface PromoteRcBodyProps { rcRelease: NonNullable; @@ -49,42 +48,26 @@ export const PromoteRcBody = ({ rcRelease, successCb }: PromoteRcBodyProps) => { }), ); - if (promoteGitHubRcResponse.error) { + if (promoteGitHubRcResponse.loading || promoteGitHubRcResponse.value) { return ( - {promoteGitHubRcResponse.error.message} + + Hello + + + ); } - function Description() { - return ( - <> - - Promotes the current Release Candidate to a Release Version. - + return ( + <> + + Promotes the current Release Candidate to a Release Version. + - - - - - ); - } + + + - function CTA() { - if (promoteGitHubRcResponse.loading || promoteGitHubRcResponse.value) { - return ( - - ); - } - - return ( - ); - } - - return ( -
- - - -
+ ); }; diff --git a/plugins/github-release-manager/src/components/LinearProgressWithLabel.tsx b/plugins/github-release-manager/src/components/LinearProgressWithLabel.tsx new file mode 100644 index 0000000000..9b0a0ec1f9 --- /dev/null +++ b/plugins/github-release-manager/src/components/LinearProgressWithLabel.tsx @@ -0,0 +1,40 @@ +/* + * 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, + LinearProgress, + LinearProgressProps, + Typography, +} from '@material-ui/core'; + +export function LinearProgressWithLabel( + props: LinearProgressProps & { value: number }, +) { + return ( + + + + + + {`${Math.round( + props.value, + )}%`} + + + ); +} diff --git a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.tsx b/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.tsx index 5c309cf394..c3b84487f3 100644 --- a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.tsx +++ b/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.tsx @@ -31,7 +31,7 @@ import { TEST_IDS } from '../../test-helpers/test-ids'; import { useRefetchContext } from '../../contexts/RefetchContext'; interface ResponseStepListProps { - responseSteps?: ResponseStep[]; + responseSteps?: (ResponseStep | undefined)[]; title: string; animationDelay?: number; loading: boolean; @@ -43,27 +43,14 @@ export const ResponseStepList = ({ responseSteps, animationDelay, loading = false, - closeable = false, denseList = false, title, children, }: PropsWithChildren) => { - const [open, setOpen] = React.useState(true); const { setRefetchTrigger } = useRefetchContext(); - const handleClose = () => setOpen(false); - return ( - { - if (closeable) { - handleClose(); - } - }} - maxWidth="md" - fullWidth - > + {title} {loading || !responseSteps ? ( @@ -78,29 +65,25 @@ export const ResponseStepList = ({ data-testid={TEST_IDS.components.responseStepListDialogContent} > - {responseSteps.map((responseStep, index) => ( - - ))} + {responseSteps.map((responseStep, index) => { + if (!responseStep) { + return null; + } + + return ( + + ); + })} {children} - {closeable && ( - - )} + + + )} + + ); +}; diff --git a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepListItem.tsx b/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepListItem.tsx index dd54a7a11d..08ac005c8d 100644 --- a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepListItem.tsx +++ b/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepListItem.tsx @@ -14,7 +14,7 @@ * limitations under the License. */ -import React, { useEffect, useState } from 'react'; +import React from 'react'; import { colors, IconButton, @@ -60,19 +60,9 @@ const useStyles = makeStyles({ export const ResponseStepListItem = ({ responseStep, - index, animationDelay = 300, }: ResponseStepListItemProps) => { const classes = useStyles({ animationDelay }); - const [renderMe, setRenderMe] = useState(false); - - useEffect(() => { - const timeoutId = setTimeout(() => { - setRenderMe(true); - }, animationDelay * index); - - return () => clearTimeout(timeoutId); - }, [animationDelay, index, setRenderMe]); function ItemIcon() { if (responseStep.icon === 'success') { @@ -120,7 +110,7 @@ export const ResponseStepListItem = ({ return ( diff --git a/plugins/github-release-manager/src/helpers/createResponseStepError.ts b/plugins/github-release-manager/src/helpers/createResponseStepError.ts new file mode 100644 index 0000000000..7de042a7e7 --- /dev/null +++ b/plugins/github-release-manager/src/helpers/createResponseStepError.ts @@ -0,0 +1,25 @@ +/* + * 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 { ResponseStep } from '../types/types'; + +export function createResponseStepError(error: Error): ResponseStep { + return { + message: 'Something went wrong ❌', + secondaryMessage: `Error message: ${error.message}`, + icon: 'failure', + }; +} diff --git a/plugins/github-release-manager/src/hooks/useResponseSteps.ts b/plugins/github-release-manager/src/hooks/useResponseSteps.ts new file mode 100644 index 0000000000..7efa4188f6 --- /dev/null +++ b/plugins/github-release-manager/src/hooks/useResponseSteps.ts @@ -0,0 +1,55 @@ +/* + * 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 { useState } from 'react'; + +import { ResponseStep } from '../types/types'; + +export function useResponseSteps() { + const [responseSteps, setResponseSteps] = useState([]); + + function abortIfError(error?: Error) { + const RESPONSE_STEP_SKIP = { + responseStep: { + message: 'Skipped due to error in previous step', + icon: 'failure', + } as ResponseStep, + }; + + if (error) { + setResponseSteps([...responseSteps, RESPONSE_STEP_SKIP.responseStep]); + throw error; + } + } + + function asyncCatcher(error: Error) { + const responseStepError: ResponseStep = { + message: 'Something went wrong ❌', + secondaryMessage: `Error message: ${error.message}`, + icon: 'failure', + }; + + setResponseSteps([...responseSteps, responseStepError]); + throw error; + } + + return { + responseSteps, + setResponseSteps, + asyncCatcher, + abortIfError, + }; +} From fef2edfc22eff87a930cc52e7b8238c40259b4c2 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Tue, 20 Apr 2021 10:22:26 +0200 Subject: [PATCH 040/140] Add useResponseSteps to promoteRc & patch as well Signed-off-by: Erik Engervall --- .../src/api/PluginApiClient.ts | 3 +- .../src/cards/createRc/CreateRc.tsx | 2 +- .../{createRc.test.ts => useCreateRc.test.ts} | 0 .../cards/createRc/sideEffects/useCreateRc.ts | 162 ++++---- .../src/cards/patchRc/PatchBody.test.tsx | 12 +- .../src/cards/patchRc/PatchBody.tsx | 65 ++-- .../src/cards/patchRc/sideEffects/patch.ts | 208 ----------- .../{patch.test.ts => usePatch.test.ts} | 11 +- .../src/cards/patchRc/sideEffects/usePatch.ts | 352 ++++++++++++++++++ .../cards/promoteRc/PromoteRcBody.test.tsx | 7 + .../src/cards/promoteRc/PromoteRcBody.tsx | 31 +- .../cards/promoteRc/sideEffects/promoteRc.ts | 64 ---- ...promoteRc.test.ts => usePromoteRc.test.ts} | 16 +- .../promoteRc/sideEffects/usePromoteRc.ts | 112 ++++++ .../src/hooks/useResponseSteps.ts | 8 +- 15 files changed, 622 insertions(+), 431 deletions(-) rename plugins/github-release-manager/src/cards/createRc/sideEffects/{createRc.test.ts => useCreateRc.test.ts} (100%) delete mode 100644 plugins/github-release-manager/src/cards/patchRc/sideEffects/patch.ts rename plugins/github-release-manager/src/cards/patchRc/sideEffects/{patch.test.ts => usePatch.test.ts} (93%) create mode 100644 plugins/github-release-manager/src/cards/patchRc/sideEffects/usePatch.ts delete mode 100644 plugins/github-release-manager/src/cards/promoteRc/sideEffects/promoteRc.ts rename plugins/github-release-manager/src/cards/promoteRc/sideEffects/{promoteRc.test.ts => usePromoteRc.test.ts} (83%) create mode 100644 plugins/github-release-manager/src/cards/promoteRc/sideEffects/usePromoteRc.ts diff --git a/plugins/github-release-manager/src/api/PluginApiClient.ts b/plugins/github-release-manager/src/api/PluginApiClient.ts index 04f39539eb..9c6b840989 100644 --- a/plugins/github-release-manager/src/api/PluginApiClient.ts +++ b/plugins/github-release-manager/src/api/PluginApiClient.ts @@ -414,8 +414,7 @@ export class PluginApiClient implements IPluginApiClient { repo, message: `[patch ${bumpedTag}] ${selectedPatchCommit.commit.message} -${selectedPatchCommit.sha} -${selectedPatchCommit.htmlUrl}`, +${selectedPatchCommit.sha}`, tree: mergeTree, parents: [releaseBranchSha], }); diff --git a/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx b/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx index 3b08198457..172375f115 100644 --- a/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx +++ b/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx @@ -86,7 +86,7 @@ export const CreateRc = ({ if (responseSteps.length > 0) { return ( - Create Release Candidate (step {progress}) + Create Release Candidate diff --git a/plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.test.ts b/plugins/github-release-manager/src/cards/createRc/sideEffects/useCreateRc.test.ts similarity index 100% rename from plugins/github-release-manager/src/cards/createRc/sideEffects/createRc.test.ts rename to plugins/github-release-manager/src/cards/createRc/sideEffects/useCreateRc.test.ts diff --git a/plugins/github-release-manager/src/cards/createRc/sideEffects/useCreateRc.ts b/plugins/github-release-manager/src/cards/createRc/sideEffects/useCreateRc.ts index 6c3596669e..eb11e25702 100644 --- a/plugins/github-release-manager/src/cards/createRc/sideEffects/useCreateRc.ts +++ b/plugins/github-release-manager/src/cards/createRc/sideEffects/useCreateRc.ts @@ -14,10 +14,11 @@ * limitations under the License. */ +import { useEffect, useState } from 'react'; import { useAsync, useAsyncFn } from 'react-use'; import { getRcGitHubInfo } from '../getRcGitHubInfo'; -import { ComponentConfigCreateRc, ResponseStep } from '../../../types/types'; +import { ComponentConfigCreateRc } from '../../../types/types'; import { GetLatestReleaseResult, GetRepositoryResult, @@ -26,7 +27,6 @@ import { import { Project } from '../../../contexts/ProjectContext'; import { GitHubReleaseManagerError } from '../../../errors/GitHubReleaseManagerError'; import { useResponseSteps } from '../../../hooks/useResponseSteps'; -import { useEffect, useState } from 'react'; interface CreateRC { defaultBranch: GetRepositoryResult['defaultBranch']; @@ -47,44 +47,46 @@ export function useCreateRc({ }: CreateRC) { const { responseSteps, - setResponseSteps, + addStepToResponseSteps, asyncCatcher, - abortIfError: skipIfError, + abortIfError, } = useResponseSteps(); /** * (1) Get the default branch's most recent commit */ - const getLatestCommit = async () => { - const latestCommit = await pluginApiClient.getLatestCommit({ - owner: project.owner, - repo: project.repo, - defaultBranch, - }); + const [latestCommitRes, run] = useAsyncFn(async () => { + const latestCommit = await pluginApiClient + .getLatestCommit({ + owner: project.owner, + repo: project.repo, + defaultBranch, + }) + .catch(asyncCatcher); - const responseStep: ResponseStep = { + addStepToResponseSteps({ message: `Fetched latest commit from "${defaultBranch}"`, secondaryMessage: `with message "${latestCommit.commit.message}"`, link: latestCommit.htmlUrl, + }); + + return { + latestCommit, }; - setResponseSteps([...responseSteps, responseStep]); - - return { latestCommit }; - }; - - const [latestCommitRes, run] = useAsyncFn(async () => - getLatestCommit().catch(asyncCatcher), - ); + }); /** * (2) Create a new ref based on the default branch's most recent sha */ - const createRcFromDefaultBranch = async (latestCommitSha: string) => { + const createRcRes = useAsync(async () => { + abortIfError(latestCommitRes.error); + if (!latestCommitRes.value) return undefined; + const createdRef = await pluginApiClient.createRc .createRef({ owner: project.owner, repo: project.repo, - mostRecentSha: latestCommitSha, + mostRecentSha: latestCommitRes.value.latestCommit.sha, targetBranch: nextGitHubInfo.rcBranch, }) .catch(error => { @@ -94,104 +96,86 @@ export function useCreateRc({ ); } throw error; - }); + }) + .catch(asyncCatcher); - const responseStep: ResponseStep = { + addStepToResponseSteps({ message: 'Cut Release Branch', secondaryMessage: `with ref "${createdRef.ref}"`, + }); + + return { + ...createdRef, }; - setResponseSteps([...responseSteps, responseStep]); - - return { ...createdRef }; - }; - - const createRcRes = useAsync(async () => { - skipIfError(latestCommitRes.error); - - if (latestCommitRes.value) { - return createRcFromDefaultBranch( - latestCommitRes.value.latestCommit.sha, - ).catch(asyncCatcher); - } - - return undefined; }, [latestCommitRes.value, latestCommitRes.error]); /** * (3) Compose a body for the release */ - const getComparison = async (createdRefRef: string) => { + const getComparisonRes = useAsync(async () => { + abortIfError(createRcRes.error); + if (!createRcRes.value) return undefined; + const previousReleaseBranch = latestRelease ? latestRelease.targetCommitish : defaultBranch; const nextReleaseBranch = nextGitHubInfo.rcBranch; - const comparison = await pluginApiClient.createRc.getComparison({ - owner: project.owner, - repo: project.repo, - previousReleaseBranch, - nextReleaseBranch, - }); + const comparison = await pluginApiClient.createRc + .getComparison({ + owner: project.owner, + repo: project.repo, + previousReleaseBranch, + nextReleaseBranch, + }) + .catch(asyncCatcher); + const releaseBody = `**Compare** ${comparison.htmlUrl} **Ahead by** ${comparison.aheadBy} commits -**Release branch** ${createdRefRef} +**Release branch** ${createRcRes.value.ref} --- `; - const responseStep: ResponseStep = { + addStepToResponseSteps({ message: 'Fetched commit comparison', secondaryMessage: `${previousReleaseBranch}...${nextReleaseBranch}`, link: comparison.htmlUrl, + }); + + return { + ...comparison, + releaseBody, }; - setResponseSteps([...responseSteps, responseStep]); - - return { ...comparison, releaseBody }; - }; - - const getComparisonRes = useAsync(async () => { - skipIfError(createRcRes.error); - - if (createRcRes.value) { - return getComparison(createRcRes.value.ref).catch(asyncCatcher); - } - - return undefined; }, [createRcRes.value, createRcRes.error]); /** * (4) Creates the release itself in GitHub */ - const createRelease = async (releaseBody: string) => { - const createReleaseResult = await pluginApiClient.createRc.createRelease({ - owner: project.owner, - repo: project.repo, - nextGitHubInfo: nextGitHubInfo, - releaseBody, - }); + const createReleaseRes = useAsync(async () => { + abortIfError(getComparisonRes.error); + if (!getComparisonRes.value) return undefined; - const responseStep: ResponseStep = { + const createReleaseResult = await pluginApiClient.createRc + .createRelease({ + owner: project.owner, + repo: project.repo, + nextGitHubInfo: nextGitHubInfo, + releaseBody: getComparisonRes.value.releaseBody, + }) + .catch(asyncCatcher); + + addStepToResponseSteps({ message: `Created Release Candidate "${createReleaseResult.name}"`, secondaryMessage: `with tag "${nextGitHubInfo.rcReleaseTag}"`, link: createReleaseResult.htmlUrl, + }); + + return { + ...createReleaseResult, }; - setResponseSteps([...responseSteps, responseStep]); - - return { ...createReleaseResult }; - }; - - const createReleaseRes = useAsync(async () => { - skipIfError(getComparisonRes.error); - - if (getComparisonRes.value) { - return createRelease(getComparisonRes.value.releaseBody).catch( - asyncCatcher, - ); - } - - return undefined; }, [getComparisonRes.value, getComparisonRes.error]); /** @@ -199,7 +183,7 @@ export function useCreateRc({ */ useAsync(async () => { if (successCb && !!createReleaseRes.value && !!getComparisonRes.value) { - skipIfError(createReleaseRes.error); + abortIfError(createReleaseRes.error); try { await successCb({ @@ -213,18 +197,18 @@ export function useCreateRc({ asyncCatcher(error); } - const responseStep: ResponseStep = { + addStepToResponseSteps({ message: 'Success callback successfully called 🚀', icon: 'success', - }; - setResponseSteps([...responseSteps, responseStep]); + }); } }, [createReleaseRes.value]); + const TOTAL_STEPS = 4 + (!!successCb ? 1 : 0); const [progress, setProgress] = useState(0); useEffect(() => { - setProgress((responseSteps.length / (4 + (!!successCb ? 1 : 0))) * 100); - }, [responseSteps.length, successCb]); + setProgress((responseSteps.length / TOTAL_STEPS) * 100); + }, [TOTAL_STEPS, responseSteps.length]); return { run, diff --git a/plugins/github-release-manager/src/cards/patchRc/PatchBody.test.tsx b/plugins/github-release-manager/src/cards/patchRc/PatchBody.test.tsx index 7cae67d126..3f62c1a262 100644 --- a/plugins/github-release-manager/src/cards/patchRc/PatchBody.test.tsx +++ b/plugins/github-release-manager/src/cards/patchRc/PatchBody.test.tsx @@ -33,11 +33,21 @@ jest.mock('../../contexts/PluginApiClientContext', () => ({ jest.mock('../../contexts/ProjectContext', () => ({ useProjectContext: jest.fn(() => mockCalverProject), })); +jest.mock('./sideEffects/usePatch', () => ({ + useCreateRc: () => ({ + run: jest.fn(), + responseSteps: [], + progress: 0, + }), +})); import { PatchBody } from './PatchBody'; import { TEST_IDS } from '../../test-helpers/test-ids'; -describe('PatchBody', () => { +// TODO: Fix tests +/* eslint-disable jest/no-disabled-tests */ + +describe.skip('PatchBody', () => { beforeEach(jest.clearAllMocks); it('should render error', async () => { diff --git a/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx b/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx index c32b692c14..0123363768 100644 --- a/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx +++ b/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx @@ -15,11 +15,13 @@ */ import React, { useState } from 'react'; -import { useAsync, useAsyncFn } from 'react-use'; +import { useAsync } from 'react-use'; import { Alert, AlertTitle } from '@material-ui/lab'; import { Button, Checkbox, + Dialog, + DialogTitle, IconButton, Link, List, @@ -37,8 +39,7 @@ import { CalverTagParts } from '../../helpers/tagParts/getCalverTagParts'; import { CenteredCircularProgress } from '../../components/CenteredCircularProgress'; import { ComponentConfigPatch } from '../../types/types'; import { Differ } from '../../components/Differ'; -import { patch } from './sideEffects/patch'; -import { ResponseStepList } from '../../components/ResponseStepList/ResponseStepList'; +import { usePatch } from './sideEffects/usePatch'; import { SemverTagParts } from '../../helpers/tagParts/getSemverTagParts'; import { TEST_IDS } from '../../test-helpers/test-ids'; import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext'; @@ -47,9 +48,10 @@ import { useStyles } from '../../styles/styles'; import { GetBranchResult, GetLatestReleaseResult, - GetRecentCommitsResultSingle, } from '../../api/PluginApiClient'; import { GitHubReleaseManagerError } from '../../errors/GitHubReleaseManagerError'; +import { LinearProgressWithLabel } from '../../components/LinearProgressWithLabel'; +import { ResponseStepList2 } from '../../components/ResponseStepList/ResponseStepList2'; interface PatchBodyProps { bumpedTag: string; @@ -92,20 +94,25 @@ export const PatchBody = ({ }; }); - const [patchReleaseResponse, patchReleaseFn] = useAsyncFn(async (...args) => { - const selectedPatchCommit: GetRecentCommitsResultSingle = args[0]; - const patchResponseSteps = await patch({ - project, - pluginApiClient, - bumpedTag, - latestRelease, - selectedPatchCommit, - successCb, - tagParts, - }); - - return patchResponseSteps; + const { run, responseSteps, progress } = usePatch({ + bumpedTag, + latestRelease, + pluginApiClient, + project, + tagParts, + successCb, }); + if (responseSteps.length > 0) { + return ( + + Patch Release Candidate + + + + + + ); + } if (githubDataResponse.error) { return ( @@ -115,10 +122,6 @@ export const PatchBody = ({ ); } - if (patchReleaseResponse.error) { - return {patchReleaseResponse.error.message}; - } - if (githubDataResponse.loading) { return ; } @@ -192,11 +195,7 @@ export const PatchBody = ({ 0) || - commitExistsOnReleaseBranch || - hasNoParent + progress > 0 || commitExistsOnReleaseBranch || hasNoParent } role={undefined} dense @@ -272,19 +271,9 @@ export const PatchBody = ({ } function CTA() { - if (patchReleaseResponse.loading || patchReleaseResponse.value) { - return ( - - ); - } - return ( diff --git a/plugins/github-release-manager/src/cards/promoteRc/sideEffects/promoteRc.ts b/plugins/github-release-manager/src/cards/promoteRc/sideEffects/promoteRc.ts deleted file mode 100644 index b7a275f3b5..0000000000 --- a/plugins/github-release-manager/src/cards/promoteRc/sideEffects/promoteRc.ts +++ /dev/null @@ -1,64 +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 { ComponentConfigPromoteRc, ResponseStep } from '../../../types/types'; -import { - GetLatestReleaseResult, - IPluginApiClient, -} from '../../../api/PluginApiClient'; -import { Project } from '../../../contexts/ProjectContext'; - -interface PromoteRc { - pluginApiClient: IPluginApiClient; - project: Project; - rcRelease: NonNullable; - releaseVersion: string; - successCb?: ComponentConfigPromoteRc['successCb']; -} - -export function promoteRc({ - pluginApiClient, - project, - rcRelease, - releaseVersion, - successCb, -}: PromoteRc) { - return async (): Promise => { - const responseSteps: ResponseStep[] = []; - - const promotedRelease = await pluginApiClient.promoteRc.promoteRelease({ - ...project, - releaseId: rcRelease.id, - releaseVersion, - }); - responseSteps.push({ - message: `Promoted "${promotedRelease.name}"`, - secondaryMessage: `from "${rcRelease.tagName}" to "${promotedRelease.tagName}"`, - link: promotedRelease.htmlUrl, - }); - - await successCb?.({ - gitHubReleaseUrl: promotedRelease.htmlUrl, - gitHubReleaseName: promotedRelease.name, - previousTagUrl: rcRelease.htmlUrl, - previousTag: rcRelease.tagName, - updatedTagUrl: promotedRelease.htmlUrl, - updatedTag: promotedRelease.tagName, - }); - - return responseSteps; - }; -} diff --git a/plugins/github-release-manager/src/cards/promoteRc/sideEffects/promoteRc.test.ts b/plugins/github-release-manager/src/cards/promoteRc/sideEffects/usePromoteRc.test.ts similarity index 83% rename from plugins/github-release-manager/src/cards/promoteRc/sideEffects/promoteRc.test.ts rename to plugins/github-release-manager/src/cards/promoteRc/sideEffects/usePromoteRc.test.ts index 109f886e5b..d1ef45d9d6 100644 --- a/plugins/github-release-manager/src/cards/promoteRc/sideEffects/promoteRc.test.ts +++ b/plugins/github-release-manager/src/cards/promoteRc/sideEffects/usePromoteRc.test.ts @@ -19,18 +19,22 @@ import { mockReleaseCandidateCalver, mockSemverProject, } from '../../../test-helpers/test-helpers'; -import { promoteRc } from './promoteRc'; +import { usePromoteRc } from './usePromoteRc'; -describe('promoteRc', () => { +// TODO: Fix tests +/* eslint-disable jest/no-disabled-tests */ + +describe.skip('usePromoteRc', () => { beforeEach(jest.clearAllMocks); - it('should work', async () => { - const result = await promoteRc({ + it('should work', () => { + const result = usePromoteRc({ pluginApiClient: mockApiClient, + project: mockSemverProject, rcRelease: mockReleaseCandidateCalver, releaseVersion: 'version-1.2.3', - project: mockSemverProject, - })(); + successCb: jest.fn(), + }); expect(result).toMatchInlineSnapshot(` Array [ diff --git a/plugins/github-release-manager/src/cards/promoteRc/sideEffects/usePromoteRc.ts b/plugins/github-release-manager/src/cards/promoteRc/sideEffects/usePromoteRc.ts new file mode 100644 index 0000000000..b882b0bdcf --- /dev/null +++ b/plugins/github-release-manager/src/cards/promoteRc/sideEffects/usePromoteRc.ts @@ -0,0 +1,112 @@ +/* + * 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 { useState, useEffect } from 'react'; +import { useAsync, useAsyncFn } from 'react-use'; + +import { ComponentConfigPromoteRc } from '../../../types/types'; +import { + GetLatestReleaseResult, + IPluginApiClient, +} from '../../../api/PluginApiClient'; +import { Project } from '../../../contexts/ProjectContext'; +import { useResponseSteps } from '../../../hooks/useResponseSteps'; + +interface PromoteRc { + pluginApiClient: IPluginApiClient; + project: Project; + rcRelease: NonNullable; + releaseVersion: string; + successCb?: ComponentConfigPromoteRc['successCb']; +} + +export function usePromoteRc({ + pluginApiClient, + project, + rcRelease, + releaseVersion, + successCb, +}: PromoteRc) { + const { + responseSteps, + addStepToResponseSteps, + asyncCatcher, + abortIfError, + } = useResponseSteps(); + + /** + * (1) Promote Release Candidate to Release Version + */ + const [promotedReleaseRes, run] = useAsyncFn(async () => { + const promotedRelease = await pluginApiClient.promoteRc + .promoteRelease({ + owner: project.owner, + repo: project.repo, + releaseId: rcRelease.id, + releaseVersion, + }) + .catch(asyncCatcher); + + addStepToResponseSteps({ + message: `Promoted "${promotedRelease.name}"`, + secondaryMessage: `from "${rcRelease.tagName}" to "${promotedRelease.tagName}"`, + link: promotedRelease.htmlUrl, + }); + + return { + ...promotedRelease, + }; + }); + + /** + * (2) Run successCb if defined + */ + useAsync(async () => { + if (successCb && !!promotedReleaseRes.value) { + abortIfError(promotedReleaseRes.error); + + try { + await successCb?.({ + gitHubReleaseUrl: promotedReleaseRes.value.htmlUrl, + gitHubReleaseName: promotedReleaseRes.value.name, + previousTagUrl: rcRelease.htmlUrl, + previousTag: rcRelease.tagName, + updatedTagUrl: promotedReleaseRes.value.htmlUrl, + updatedTag: promotedReleaseRes.value.tagName, + }); + } catch (error) { + asyncCatcher(error); + } + + addStepToResponseSteps({ + message: 'Success callback successfully called 🚀', + icon: 'success', + }); + } + }, [promotedReleaseRes.value]); + + const TOTAL_STEPS = 1 + (!!successCb ? 1 : 0); + const [progress, setProgress] = useState(0); + useEffect(() => { + setProgress((responseSteps.length / TOTAL_STEPS) * 100); + }, [TOTAL_STEPS, responseSteps.length]); + + return { + run, + responseSteps, + progress, + }; +} diff --git a/plugins/github-release-manager/src/hooks/useResponseSteps.ts b/plugins/github-release-manager/src/hooks/useResponseSteps.ts index 7efa4188f6..dae2612df1 100644 --- a/plugins/github-release-manager/src/hooks/useResponseSteps.ts +++ b/plugins/github-release-manager/src/hooks/useResponseSteps.ts @@ -35,7 +35,7 @@ export function useResponseSteps() { } } - function asyncCatcher(error: Error) { + function asyncCatcher(error: Error): never { const responseStepError: ResponseStep = { message: 'Something went wrong ❌', secondaryMessage: `Error message: ${error.message}`, @@ -46,9 +46,13 @@ export function useResponseSteps() { throw error; } + function addStepToResponseSteps(responseStep: ResponseStep) { + setResponseSteps([...responseSteps, responseStep]); + } + return { responseSteps, - setResponseSteps, + addStepToResponseSteps, asyncCatcher, abortIfError, }; From 546005f732c75b457f9632cf29fd802e544e1a54 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Tue, 20 Apr 2021 13:23:36 +0200 Subject: [PATCH 041/140] Add @testing-library/react-hooks devDep Fix tests for Components that use hooks Remove ReloadButton component Signed-off-by: Erik Engervall --- plugins/github-release-manager/package.json | 5 +- .../src/cards/createRc/CreateRc.tsx | 4 +- .../createRc/sideEffects/useCreateRc.test.ts | 65 --------- .../createRc/sideEffects/useCreateRc.test.tsx | 100 +++++++++++++ .../src/cards/patchRc/PatchBody.test.tsx | 7 +- .../src/cards/patchRc/PatchBody.tsx | 4 +- .../patchRc/sideEffects/usePatch.test.ts | 134 ++++++++++++------ .../src/cards/promoteRc/PromoteRcBody.tsx | 4 +- .../sideEffects/usePromoteRc.test.ts | 71 +++++++--- .../src/components/ReloadButton.test.tsx | 29 ---- .../src/components/ReloadButton.tsx | 36 ----- .../ResponseStepList.test.tsx | 7 +- .../ResponseStepList/ResponseStepList.tsx | 23 +-- .../ResponseStepList/ResponseStepList2.tsx | 91 ------------ .../src/hooks/useQueryHandler.test.tsx | 4 +- .../src/test-helpers/test-ids.ts | 1 - yarn.lock | 52 +++++++ 17 files changed, 313 insertions(+), 324 deletions(-) delete mode 100644 plugins/github-release-manager/src/cards/createRc/sideEffects/useCreateRc.test.ts create mode 100644 plugins/github-release-manager/src/cards/createRc/sideEffects/useCreateRc.test.tsx delete mode 100644 plugins/github-release-manager/src/components/ReloadButton.test.tsx delete mode 100644 plugins/github-release-manager/src/components/ReloadButton.tsx delete mode 100644 plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList2.tsx diff --git a/plugins/github-release-manager/package.json b/plugins/github-release-manager/package.json index bd6939a128..cd32f0c105 100644 --- a/plugins/github-release-manager/package.json +++ b/plugins/github-release-manager/package.json @@ -39,12 +39,13 @@ "@backstage/dev-utils": "^0.1.13", "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", + "@testing-library/react-hooks": "^5.1.1", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", "@types/node": "^14.14.32", - "msw": "^0.21.2", - "cross-fetch": "^3.0.6" + "cross-fetch": "^3.0.6", + "msw": "^0.21.2" }, "files": [ "dist" diff --git a/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx b/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx index 172375f115..f3c55693dc 100644 --- a/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx +++ b/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx @@ -42,7 +42,7 @@ import { GetLatestReleaseResult, GetRepositoryResult, } from '../../api/PluginApiClient'; -import { ResponseStepList2 } from '../../components/ResponseStepList/ResponseStepList2'; +import { ResponseStepList } from '../../components/ResponseStepList/ResponseStepList'; import { LinearProgressWithLabel } from '../../components/LinearProgressWithLabel'; interface CreateRcProps { @@ -90,7 +90,7 @@ export const CreateRc = ({ - + ); } diff --git a/plugins/github-release-manager/src/cards/createRc/sideEffects/useCreateRc.test.ts b/plugins/github-release-manager/src/cards/createRc/sideEffects/useCreateRc.test.ts deleted file mode 100644 index 8cecaf6fd5..0000000000 --- a/plugins/github-release-manager/src/cards/createRc/sideEffects/useCreateRc.test.ts +++ /dev/null @@ -1,65 +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 { - mockApiClient, - mockCalverProject, - mockDefaultBranch, - mockNextGitHubInfo, - mockReleaseVersionCalver, -} from '../../../test-helpers/test-helpers'; -import { useCreateRc } from './useCreateRc'; - -// TODO: Fix tests -/* eslint-disable jest/no-disabled-tests */ - -describe.skip('useCreateRc', () => { - beforeEach(jest.clearAllMocks); - - it('should work', () => { - const result = useCreateRc({ - defaultBranch: mockDefaultBranch, - latestRelease: mockReleaseVersionCalver, - nextGitHubInfo: mockNextGitHubInfo, - pluginApiClient: mockApiClient, - project: mockCalverProject, - }); - - expect(result).toMatchInlineSnapshot(` - Array [ - Object { - "link": "latestCommit.html_url", - "message": "Fetched latest commit from \\"mock_defaultBranch\\"", - "secondaryMessage": "with message \\"latestCommit.commit.message\\"", - }, - Object { - "message": "Cut Release Branch", - "secondaryMessage": "with ref \\"mock_createRef_ref\\"", - }, - Object { - "link": "mock_compareCommits_html_url", - "message": "Fetched commit comparison", - "secondaryMessage": "rc/1.2.3...rc/1.2.3", - }, - Object { - "link": "mock_createRelease_html_url", - "message": "Created Release Candidate \\"mock_createRelease_name\\"", - "secondaryMessage": "with tag \\"rc-1.2.3\\"", - }, - ] - `); - }); -}); diff --git a/plugins/github-release-manager/src/cards/createRc/sideEffects/useCreateRc.test.tsx b/plugins/github-release-manager/src/cards/createRc/sideEffects/useCreateRc.test.tsx new file mode 100644 index 0000000000..b0ce35a3ec --- /dev/null +++ b/plugins/github-release-manager/src/cards/createRc/sideEffects/useCreateRc.test.tsx @@ -0,0 +1,100 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; +import { waitFor } from '@testing-library/react'; + +import { + mockApiClient, + mockCalverProject, + mockDefaultBranch, + mockNextGitHubInfo, + mockReleaseVersionCalver, +} from '../../../test-helpers/test-helpers'; +import { useCreateRc } from './useCreateRc'; + +describe('useCreateRc', () => { + beforeEach(jest.clearAllMocks); + + it('should return the expected responseSteps and progress', async () => { + const { result } = renderHook(() => + useCreateRc({ + defaultBranch: mockDefaultBranch, + latestRelease: mockReleaseVersionCalver, + nextGitHubInfo: mockNextGitHubInfo, + pluginApiClient: mockApiClient, + project: mockCalverProject, + }), + ); + + await act(async () => { + await waitFor(() => result.current.run()); + }); + + expect(result.error).toEqual(undefined); + expect(result.current.responseSteps).toHaveLength(4); + }); + + it('should return the expected responseSteps and progress (with successCb)', async () => { + const { result } = renderHook(() => + useCreateRc({ + defaultBranch: mockDefaultBranch, + latestRelease: mockReleaseVersionCalver, + nextGitHubInfo: mockNextGitHubInfo, + pluginApiClient: mockApiClient, + project: mockCalverProject, + successCb: jest.fn(), + }), + ); + + await act(async () => { + await waitFor(() => result.current.run()); + }); + + expect(result.current.responseSteps).toHaveLength(5); + expect(result.current).toMatchInlineSnapshot(` + Object { + "progress": 100, + "responseSteps": Array [ + Object { + "link": "latestCommit.html_url", + "message": "Fetched latest commit from \\"mock_defaultBranch\\"", + "secondaryMessage": "with message \\"latestCommit.commit.message\\"", + }, + Object { + "message": "Cut Release Branch", + "secondaryMessage": "with ref \\"mock_createRef_ref\\"", + }, + Object { + "link": "mock_compareCommits_html_url", + "message": "Fetched commit comparison", + "secondaryMessage": "rc/1.2.3...rc/1.2.3", + }, + Object { + "link": "mock_createRelease_html_url", + "message": "Created Release Candidate \\"mock_createRelease_name\\"", + "secondaryMessage": "with tag \\"rc-1.2.3\\"", + }, + Object { + "icon": "success", + "message": "Success callback successfully called 🚀", + }, + ], + "run": [Function], + } + `); + }); +}); diff --git a/plugins/github-release-manager/src/cards/patchRc/PatchBody.test.tsx b/plugins/github-release-manager/src/cards/patchRc/PatchBody.test.tsx index 3f62c1a262..f9a8babb5d 100644 --- a/plugins/github-release-manager/src/cards/patchRc/PatchBody.test.tsx +++ b/plugins/github-release-manager/src/cards/patchRc/PatchBody.test.tsx @@ -34,7 +34,7 @@ jest.mock('../../contexts/ProjectContext', () => ({ useProjectContext: jest.fn(() => mockCalverProject), })); jest.mock('./sideEffects/usePatch', () => ({ - useCreateRc: () => ({ + usePatch: () => ({ run: jest.fn(), responseSteps: [], progress: 0, @@ -44,10 +44,7 @@ jest.mock('./sideEffects/usePatch', () => ({ import { PatchBody } from './PatchBody'; import { TEST_IDS } from '../../test-helpers/test-ids'; -// TODO: Fix tests -/* eslint-disable jest/no-disabled-tests */ - -describe.skip('PatchBody', () => { +describe('PatchBody', () => { beforeEach(jest.clearAllMocks); it('should render error', async () => { diff --git a/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx b/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx index 0123363768..cd001c5110 100644 --- a/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx +++ b/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx @@ -51,7 +51,7 @@ import { } from '../../api/PluginApiClient'; import { GitHubReleaseManagerError } from '../../errors/GitHubReleaseManagerError'; import { LinearProgressWithLabel } from '../../components/LinearProgressWithLabel'; -import { ResponseStepList2 } from '../../components/ResponseStepList/ResponseStepList2'; +import { ResponseStepList } from '../../components/ResponseStepList/ResponseStepList'; interface PatchBodyProps { bumpedTag: string; @@ -109,7 +109,7 @@ export const PatchBody = ({ - + ); } diff --git a/plugins/github-release-manager/src/cards/patchRc/sideEffects/usePatch.test.ts b/plugins/github-release-manager/src/cards/patchRc/sideEffects/usePatch.test.ts index 17892c3c9d..ff280ed181 100644 --- a/plugins/github-release-manager/src/cards/patchRc/sideEffects/usePatch.test.ts +++ b/plugins/github-release-manager/src/cards/patchRc/sideEffects/usePatch.test.ts @@ -14,66 +14,106 @@ * limitations under the License. */ +import { renderHook, act } from '@testing-library/react-hooks'; +import { waitFor } from '@testing-library/react'; + import { mockApiClient, mockBumpedTag, mockCalverProject, mockReleaseVersionCalver, + mockSelectedPatchCommit, mockTagParts, } from '../../../test-helpers/test-helpers'; import { usePatch } from './usePatch'; -// TODO: Fix tests -/* eslint-disable jest/no-disabled-tests */ - -describe.skip('patch', () => { +describe('patch', () => { beforeEach(jest.clearAllMocks); - it('should work', async () => { - const result = await usePatch({ - bumpedTag: mockBumpedTag, - latestRelease: mockReleaseVersionCalver, - pluginApiClient: mockApiClient, - project: mockCalverProject, - tagParts: mockTagParts, + it('should return the expected responseSteps and progress', async () => { + const { result } = renderHook(() => + usePatch({ + bumpedTag: mockBumpedTag, + latestRelease: mockReleaseVersionCalver, + pluginApiClient: mockApiClient, + project: mockCalverProject, + tagParts: mockTagParts, + }), + ); + + await act(async () => { + await waitFor(() => result.current.run(mockSelectedPatchCommit)); }); - expect(result).toMatchInlineSnapshot(` - Array [ - Object { - "link": "mock_branch_links_html", - "message": "Fetched release branch \\"rc/1.2.3\\"", - }, - Object { - "message": "Created temporary commit", - "secondaryMessage": "with message \\"mock_commit_message\\"", - }, - Object { - "link": "mock_merge_html_url", - "message": "Merged temporary commit into \\"rc/1.2.3\\"", - "secondaryMessage": "with message \\"mock_merge_commit_message\\"", - }, - Object { - "message": "Cherry-picked patch commit to \\"mock_branch_commit_sha\\"", - "secondaryMessage": "with message \\"mock_cherrypick_message\\"", - }, - Object { - "message": "Updated reference \\"mock_reference_ref\\"", - }, - Object { - "message": "Created new tag object", - "secondaryMessage": "with name \\"mock_tag_object_tag\\"", - }, - Object { - "message": "Created new reference \\"mock_reference_ref\\"", - "secondaryMessage": "for tag object \\"mock_tag_object_tag\\"", - }, - Object { - "link": "mock_update_release_html_url", - "message": "Updated release \\"mock_update_release_name\\"", - "secondaryMessage": "with tag mock_update_release_tag_name", - }, - ] + expect(result.error).toEqual(undefined); + expect(result.current.responseSteps).toHaveLength(9); + }); + + it('should return the expected responseSteps and progress (with successCb)', async () => { + const { result } = renderHook(() => + usePatch({ + bumpedTag: mockBumpedTag, + latestRelease: mockReleaseVersionCalver, + pluginApiClient: mockApiClient, + project: mockCalverProject, + tagParts: mockTagParts, + successCb: jest.fn(), + }), + ); + + await act(async () => { + await waitFor(() => result.current.run(mockSelectedPatchCommit)); + }); + + expect(result.error).toEqual(undefined); + expect(result.current.responseSteps).toHaveLength(10); + expect(result.current).toMatchInlineSnapshot(` + Object { + "progress": 100, + "responseSteps": Array [ + Object { + "link": "mock_branch_links_html", + "message": "Fetched release branch \\"rc/1.2.3\\"", + }, + Object { + "message": "Created temporary commit", + "secondaryMessage": "with message \\"mock_commit_message\\"", + }, + Object { + "message": "Forced branch \\"rc/1.2.3\\" to temporary commit \\"mock_commit_sha\\"", + }, + Object { + "link": "mock_merge_html_url", + "message": "Merged temporary commit into \\"rc/1.2.3\\"", + "secondaryMessage": "with message \\"mock_merge_commit_message\\"", + }, + Object { + "message": "Cherry-picked patch commit to \\"mock_branch_commit_sha\\"", + "secondaryMessage": "with message \\"mock_cherrypick_message\\"", + }, + Object { + "message": "Updated reference \\"mock_reference_ref\\"", + }, + Object { + "message": "Created new tag object", + "secondaryMessage": "with name \\"mock_tag_object_tag\\"", + }, + Object { + "message": "Created new reference \\"mock_reference_ref\\"", + "secondaryMessage": "for tag object \\"mock_tag_object_tag\\"", + }, + Object { + "link": "mock_update_release_html_url", + "message": "Updated release \\"mock_update_release_name\\"", + "secondaryMessage": "with tag mock_update_release_tag_name", + }, + Object { + "icon": "success", + "message": "Success callback successfully called 🚀", + }, + ], + "run": [Function], + } `); }); }); diff --git a/plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.tsx b/plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.tsx index fa902a35f0..274d40e6dd 100644 --- a/plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.tsx +++ b/plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.tsx @@ -25,7 +25,7 @@ import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext import { useProjectContext } from '../../contexts/ProjectContext'; import { useStyles } from '../../styles/styles'; import { GetLatestReleaseResult } from '../../api/PluginApiClient'; -import { ResponseStepList2 } from '../../components/ResponseStepList/ResponseStepList2'; +import { ResponseStepList } from '../../components/ResponseStepList/ResponseStepList'; import { LinearProgressWithLabel } from '../../components/LinearProgressWithLabel'; interface PromoteRcBodyProps { @@ -54,7 +54,7 @@ export const PromoteRcBody = ({ rcRelease, successCb }: PromoteRcBodyProps) => { - + ); } diff --git a/plugins/github-release-manager/src/cards/promoteRc/sideEffects/usePromoteRc.test.ts b/plugins/github-release-manager/src/cards/promoteRc/sideEffects/usePromoteRc.test.ts index d1ef45d9d6..a055e4acd8 100644 --- a/plugins/github-release-manager/src/cards/promoteRc/sideEffects/usePromoteRc.test.ts +++ b/plugins/github-release-manager/src/cards/promoteRc/sideEffects/usePromoteRc.test.ts @@ -14,6 +14,9 @@ * limitations under the License. */ +import { renderHook, act } from '@testing-library/react-hooks'; +import { waitFor } from '@testing-library/react'; + import { mockApiClient, mockReleaseCandidateCalver, @@ -21,29 +24,59 @@ import { } from '../../../test-helpers/test-helpers'; import { usePromoteRc } from './usePromoteRc'; -// TODO: Fix tests -/* eslint-disable jest/no-disabled-tests */ - -describe.skip('usePromoteRc', () => { +describe('usePromoteRc', () => { beforeEach(jest.clearAllMocks); - it('should work', () => { - const result = usePromoteRc({ - pluginApiClient: mockApiClient, - project: mockSemverProject, - rcRelease: mockReleaseCandidateCalver, - releaseVersion: 'version-1.2.3', - successCb: jest.fn(), + it('should return the expected responseSteps and progress', async () => { + const { result } = renderHook(() => + usePromoteRc({ + pluginApiClient: mockApiClient, + project: mockSemverProject, + rcRelease: mockReleaseCandidateCalver, + releaseVersion: 'version-1.2.3', + }), + ); + + await act(async () => { + await waitFor(() => result.current.run()); }); - expect(result).toMatchInlineSnapshot(` - Array [ - Object { - "link": "mock_release_html_url", - "message": "Promoted \\"mock_release_name\\"", - "secondaryMessage": "from \\"rc-2020.01.01_1\\" to \\"mock_release_tag_name\\"", - }, - ] + expect(result.error).toEqual(undefined); + expect(result.current.responseSteps).toHaveLength(1); + }); + + it('should return the expected responseSteps and progress (with successCb)', async () => { + const { result } = renderHook(() => + usePromoteRc({ + pluginApiClient: mockApiClient, + project: mockSemverProject, + rcRelease: mockReleaseCandidateCalver, + releaseVersion: 'version-1.2.3', + successCb: jest.fn(), + }), + ); + + await act(async () => { + await waitFor(() => result.current.run()); + }); + + expect(result.current.responseSteps).toHaveLength(2); + expect(result.current).toMatchInlineSnapshot(` + Object { + "progress": 100, + "responseSteps": Array [ + Object { + "link": "mock_release_html_url", + "message": "Promoted \\"mock_release_name\\"", + "secondaryMessage": "from \\"rc-2020.01.01_1\\" to \\"mock_release_tag_name\\"", + }, + Object { + "icon": "success", + "message": "Success callback successfully called 🚀", + }, + ], + "run": [Function], + } `); }); }); diff --git a/plugins/github-release-manager/src/components/ReloadButton.test.tsx b/plugins/github-release-manager/src/components/ReloadButton.test.tsx deleted file mode 100644 index b33cc0b931..0000000000 --- a/plugins/github-release-manager/src/components/ReloadButton.test.tsx +++ /dev/null @@ -1,29 +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 { render } from '@testing-library/react'; - -import { TEST_IDS } from '../test-helpers/test-ids'; -import { ReloadButton } from './ReloadButton'; - -describe('ReloadButton', () => { - it('render ReloadButton', () => { - const { getByTestId } = render(); - - expect(getByTestId(TEST_IDS.components.reloadButton)).toBeInTheDocument(); - }); -}); diff --git a/plugins/github-release-manager/src/components/ReloadButton.tsx b/plugins/github-release-manager/src/components/ReloadButton.tsx deleted file mode 100644 index d11f39f954..0000000000 --- a/plugins/github-release-manager/src/components/ReloadButton.tsx +++ /dev/null @@ -1,36 +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 { Button } from '@material-ui/core'; - -import { TEST_IDS } from '../test-helpers/test-ids'; - -interface ReloadButtonProps { - style?: React.CSSProperties; -} - -export const ReloadButton = ({ style }: ReloadButtonProps) => ( - -); diff --git a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.test.tsx b/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.test.tsx index cfa68b0c32..21f6c6925e 100644 --- a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.test.tsx +++ b/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.test.tsx @@ -27,7 +27,7 @@ jest.mock('../../contexts/RefetchContext', () => ({ describe('ResponseStepList', () => { it('should render loading state when loading', () => { const { getByTestId } = render( - , + , ); expect( @@ -37,7 +37,7 @@ describe('ResponseStepList', () => { it('should render loading state when no responseSteps', () => { const { getByTestId } = render( - , + , ); expect( @@ -49,8 +49,7 @@ describe('ResponseStepList', () => { const { getByTestId } = render( , ); diff --git a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.tsx b/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.tsx index c3b84487f3..e703f08d34 100644 --- a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.tsx +++ b/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.tsx @@ -15,14 +15,7 @@ */ import React, { PropsWithChildren } from 'react'; -import { - Button, - Dialog, - DialogActions, - DialogContent, - DialogTitle, - List, -} from '@material-ui/core'; +import { Button, DialogActions, DialogContent, List } from '@material-ui/core'; import { CenteredCircularProgress } from '../CenteredCircularProgress'; import { ResponseStep } from '../../types/types'; @@ -31,10 +24,9 @@ import { TEST_IDS } from '../../test-helpers/test-ids'; import { useRefetchContext } from '../../contexts/RefetchContext'; interface ResponseStepListProps { - responseSteps?: (ResponseStep | undefined)[]; - title: string; + responseSteps: (ResponseStep | undefined)[]; animationDelay?: number; - loading: boolean; + loading?: boolean; closeable?: boolean; denseList?: boolean; } @@ -44,16 +36,13 @@ export const ResponseStepList = ({ animationDelay, loading = false, denseList = false, - title, children, }: PropsWithChildren) => { const { setRefetchTrigger } = useRefetchContext(); return ( - - {title} - - {loading || !responseSteps ? ( + <> + {loading || responseSteps.length === 0 ? (
)} -
+ ); }; diff --git a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList2.tsx b/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList2.tsx deleted file mode 100644 index 7239d223bb..0000000000 --- a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList2.tsx +++ /dev/null @@ -1,91 +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, { PropsWithChildren } from 'react'; -import { Button, DialogActions, DialogContent, List } from '@material-ui/core'; - -import { CenteredCircularProgress } from '../CenteredCircularProgress'; -import { ResponseStep } from '../../types/types'; -import { ResponseStepListItem } from './ResponseStepListItem'; -import { TEST_IDS } from '../../test-helpers/test-ids'; -import { useRefetchContext } from '../../contexts/RefetchContext'; - -interface ResponseStepListProps { - responseSteps?: (ResponseStep | undefined)[]; - animationDelay?: number; - loading?: boolean; - closeable?: boolean; - denseList?: boolean; -} - -// TODO: Replace `ResponseStepList` with this component - -export const ResponseStepList2 = ({ - responseSteps, - animationDelay, - loading = false, - denseList = false, - children, -}: PropsWithChildren) => { - const { setRefetchTrigger } = useRefetchContext(); - - return ( - <> - {loading || !responseSteps ? ( -
- -
- ) : ( - <> - - - {responseSteps.map((responseStep, index) => { - if (!responseStep) { - return null; - } - - return ( - - ); - })} - - - {children} - - - - - - )} - - ); -}; diff --git a/plugins/github-release-manager/src/hooks/useQueryHandler.test.tsx b/plugins/github-release-manager/src/hooks/useQueryHandler.test.tsx index 757bca9859..a80fa3df04 100644 --- a/plugins/github-release-manager/src/hooks/useQueryHandler.test.tsx +++ b/plugins/github-release-manager/src/hooks/useQueryHandler.test.tsx @@ -48,9 +48,9 @@ describe('useQueryHandler', () => { it('should get parsedQuery and queryParams', () => { const { getByTestId } = render(); - const smt = getByTestId(TEST_ID).innerHTML; + const result = getByTestId(TEST_ID).innerHTML; - expect(smt).toMatchInlineSnapshot(` + expect(result).toMatchInlineSnapshot(` "{ \\"parsedQuery\\": { \\"versioningStrategy\\": \\"semver\\", diff --git a/plugins/github-release-manager/src/test-helpers/test-ids.ts b/plugins/github-release-manager/src/test-helpers/test-ids.ts index 5f537803ca..52a426c9b8 100644 --- a/plugins/github-release-manager/src/test-helpers/test-ids.ts +++ b/plugins/github-release-manager/src/test-helpers/test-ids.ts @@ -54,7 +54,6 @@ export const TEST_IDS = { }, components: { divider: 'grm--divider', - reloadButton: 'grm--reload-button', noLatestRelease: 'grm--no-latest-release', noReleaseBranch: 'grm--no-release-branch', circularProgress: 'grm--circular-progress', diff --git a/yarn.lock b/yarn.lock index 90072e3f79..43cc98ce87 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5550,6 +5550,18 @@ "@babel/runtime" "^7.5.4" "@types/testing-library__react-hooks" "^3.4.0" +"@testing-library/react-hooks@^5.1.1": + version "5.1.1" + resolved "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-5.1.1.tgz#1fbaae8a4e8a4a7f97b176c23e1e890c41bbbfa5" + integrity sha512-52D2XnpelFDefnWpy/V6z2qGNj8JLIvW5DjYtelMvFXdEyWiykSaI7IXHwFy4ICoqXJDmmwHAiFRiFboub/U5g== + dependencies: + "@babel/runtime" "^7.12.5" + "@types/react" ">=16.9.0" + "@types/react-dom" ">=16.9.0" + "@types/react-test-renderer" ">=16.9.0" + filter-console "^0.1.1" + react-error-boundary "^3.1.0" + "@testing-library/react@^11.2.5": version "11.2.6" resolved "https://registry.npmjs.org/@testing-library/react/-/react-11.2.6.tgz#586a23adc63615985d85be0c903f374dab19200b" @@ -6483,6 +6495,13 @@ "@types/webpack" "*" "@types/webpack-dev-server" "*" +"@types/react-dom@>=16.9.0": + version "17.0.3" + resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.3.tgz#7fdf37b8af9d6d40127137865bb3fff8871d7ee1" + integrity sha512-4NnJbCeWE+8YBzupn/YrJxZ8VnjcJq5iR1laqQ1vkpQgBiA7bwk0Rp24fxsdNinzJY2U+HHS4dJJDPdoMjdJ7w== + dependencies: + "@types/react" "*" + "@types/react-dom@^16.9.8": version "16.9.8" resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.8.tgz#fe4c1e11dfc67155733dfa6aa65108b4971cb423" @@ -6526,6 +6545,13 @@ dependencies: "@types/react" "*" +"@types/react-test-renderer@>=16.9.0": + version "17.0.1" + resolved "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-17.0.1.tgz#3120f7d1c157fba9df0118dae20cb0297ee0e06b" + integrity sha512-3Fi2O6Zzq/f3QR9dRnlnHso9bMl7weKCviFmfF6B4LS1Uat6Hkm15k0ZAQuDz+UBq6B3+g+NM6IT2nr5QgPzCw== + dependencies: + "@types/react" "*" + "@types/react-text-truncate@^0.14.0": version "0.14.0" resolved "https://registry.npmjs.org/@types/react-text-truncate/-/react-text-truncate-0.14.0.tgz#588bbabbc7f2a13815e805f3a48942db73fe65fe" @@ -6562,6 +6588,15 @@ dependencies: csstype "^2.2.0" +"@types/react@>=16.9.0": + version "17.0.3" + resolved "https://registry.npmjs.org/@types/react/-/react-17.0.3.tgz#ba6e215368501ac3826951eef2904574c262cc79" + integrity sha512-wYOUxIgs2HZZ0ACNiIayItyluADNbONl7kt8lkLjVK8IitMH5QMyAh75Fwhmo37r1m7L2JaFj03sIfxBVDvRAg== + dependencies: + "@types/prop-types" "*" + "@types/scheduler" "*" + csstype "^3.0.2" + "@types/reactcss@*": version "1.2.3" resolved "https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.3.tgz#af28ae11bbb277978b99d04d1eedfd068ca71834" @@ -6643,6 +6678,11 @@ "@types/node" "*" rollup "^0.63.4" +"@types/scheduler@*": + version "0.16.1" + resolved "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275" + integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA== + "@types/semver@^6.0.0": version "6.2.1" resolved "https://registry.npmjs.org/@types/semver/-/semver-6.2.1.tgz#a236185670a7860f1597cf73bea2e16d001461ba" @@ -13254,6 +13294,11 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" +filter-console@^0.1.1: + version "0.1.1" + resolved "https://registry.npmjs.org/filter-console/-/filter-console-0.1.1.tgz#6242be28982bba7415bcc6db74a79f4a294fa67c" + integrity sha512-zrXoV1Uaz52DqPs+qEwNJWJFAWZpYJ47UNmpN9q4j+/EYsz85uV0DC9k8tRND5kYmoVzL0W+Y75q4Rg8sRJCdg== + filter-obj@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b" @@ -21909,6 +21954,13 @@ react-draggable@^4.0.3: classnames "^2.2.5" prop-types "^15.6.0" +react-error-boundary@^3.1.0: + version "3.1.1" + resolved "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.1.tgz#932c5ca5cbab8ec4fe37fd7b415aa5c3a47597e7" + integrity sha512-W3xCd9zXnanqrTUeViceufD3mIW8Ut29BUD+S2f0eO2XCOU8b6UrJfY46RDGe5lxCJzfe4j0yvIfh0RbTZhKJw== + dependencies: + "@babel/runtime" "^7.12.5" + react-error-overlay@^6.0.7, react-error-overlay@^6.0.9: version "6.0.9" resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a" From acb4c01baf9927c3eb7a9ad005ac64559b1e9d39 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Tue, 20 Apr 2021 14:15:07 +0200 Subject: [PATCH 042/140] Create Dialog component with tests Signed-off-by: Erik Engervall --- .../src/cards/createRc/CreateRc.tsx | 37 +++++++--------- .../src/cards/patchRc/PatchBody.tsx | 37 +++++++--------- .../src/cards/promoteRc/PromoteRcBody.tsx | 23 +++++----- .../src/components/Dialog.test.tsx | 42 +++++++++++++++++++ .../src/components/Dialog.tsx | 38 +++++++++++++++++ .../components/LinearProgressWithLabel.tsx | 2 +- 6 files changed, 123 insertions(+), 56 deletions(-) create mode 100644 plugins/github-release-manager/src/components/Dialog.test.tsx create mode 100644 plugins/github-release-manager/src/components/Dialog.tsx diff --git a/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx b/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx index f3c55693dc..8ba80a9aee 100644 --- a/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx +++ b/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx @@ -18,8 +18,6 @@ import React, { useState, useEffect } from 'react'; import { Alert } from '@material-ui/lab'; import { Button, - Dialog, - DialogTitle, FormControl, InputLabel, MenuItem, @@ -27,23 +25,22 @@ import { Typography, } from '@material-ui/core'; -import { ComponentConfigCreateRc } from '../../types/types'; -import { useCreateRc } from './sideEffects/useCreateRc'; -import { Differ } from '../../components/Differ'; -import { getRcGitHubInfo } from './getRcGitHubInfo'; -import { InfoCardPlus } from '../../components/InfoCardPlus'; -import { SEMVER_PARTS } from '../../constants/constants'; -import { TEST_IDS } from '../../test-helpers/test-ids'; -import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext'; -import { useProjectContext } from '../../contexts/ProjectContext'; -import { useStyles } from '../../styles/styles'; import { GetBranchResult, GetLatestReleaseResult, GetRepositoryResult, } from '../../api/PluginApiClient'; -import { ResponseStepList } from '../../components/ResponseStepList/ResponseStepList'; -import { LinearProgressWithLabel } from '../../components/LinearProgressWithLabel'; +import { ComponentConfigCreateRc } from '../../types/types'; +import { Dialog } from '../../components/Dialog'; +import { Differ } from '../../components/Differ'; +import { getRcGitHubInfo } from './getRcGitHubInfo'; +import { InfoCardPlus } from '../../components/InfoCardPlus'; +import { SEMVER_PARTS } from '../../constants/constants'; +import { TEST_IDS } from '../../test-helpers/test-ids'; +import { useCreateRc } from './sideEffects/useCreateRc'; +import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext'; +import { useProjectContext } from '../../contexts/ProjectContext'; +import { useStyles } from '../../styles/styles'; interface CreateRcProps { defaultBranch: GetRepositoryResult['defaultBranch']; @@ -85,13 +82,11 @@ export const CreateRc = ({ }); if (responseSteps.length > 0) { return ( - - Create Release Candidate - - - - - + ); } diff --git a/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx b/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx index cd001c5110..ce0b3acd1c 100644 --- a/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx +++ b/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx @@ -20,8 +20,6 @@ import { Alert, AlertTitle } from '@material-ui/lab'; import { Button, Checkbox, - Dialog, - DialogTitle, IconButton, Link, List, @@ -35,23 +33,22 @@ import { import FileCopyIcon from '@material-ui/icons/FileCopy'; import OpenInNewIcon from '@material-ui/icons/OpenInNew'; -import { CalverTagParts } from '../../helpers/tagParts/getCalverTagParts'; -import { CenteredCircularProgress } from '../../components/CenteredCircularProgress'; -import { ComponentConfigPatch } from '../../types/types'; -import { Differ } from '../../components/Differ'; -import { usePatch } from './sideEffects/usePatch'; -import { SemverTagParts } from '../../helpers/tagParts/getSemverTagParts'; -import { TEST_IDS } from '../../test-helpers/test-ids'; -import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext'; -import { useProjectContext } from '../../contexts/ProjectContext'; -import { useStyles } from '../../styles/styles'; import { GetBranchResult, GetLatestReleaseResult, } from '../../api/PluginApiClient'; +import { CalverTagParts } from '../../helpers/tagParts/getCalverTagParts'; +import { CenteredCircularProgress } from '../../components/CenteredCircularProgress'; +import { ComponentConfigPatch } from '../../types/types'; +import { Dialog } from '../../components/Dialog'; +import { Differ } from '../../components/Differ'; import { GitHubReleaseManagerError } from '../../errors/GitHubReleaseManagerError'; -import { LinearProgressWithLabel } from '../../components/LinearProgressWithLabel'; -import { ResponseStepList } from '../../components/ResponseStepList/ResponseStepList'; +import { SemverTagParts } from '../../helpers/tagParts/getSemverTagParts'; +import { TEST_IDS } from '../../test-helpers/test-ids'; +import { usePatch } from './sideEffects/usePatch'; +import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext'; +import { useProjectContext } from '../../contexts/ProjectContext'; +import { useStyles } from '../../styles/styles'; interface PatchBodyProps { bumpedTag: string; @@ -104,13 +101,11 @@ export const PatchBody = ({ }); if (responseSteps.length > 0) { return ( - - Patch Release Candidate - - - - - + ); } diff --git a/plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.tsx b/plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.tsx index 274d40e6dd..a3b2ef0f68 100644 --- a/plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.tsx +++ b/plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.tsx @@ -15,18 +15,17 @@ */ import React from 'react'; -import { Button, Dialog, DialogTitle, Typography } from '@material-ui/core'; +import { Button, Typography } from '@material-ui/core'; -import { Differ } from '../../components/Differ'; import { ComponentConfigPromoteRc } from '../../types/types'; -import { usePromoteRc } from './sideEffects/usePromoteRc'; +import { Dialog } from '../../components/Dialog'; +import { Differ } from '../../components/Differ'; +import { GetLatestReleaseResult } from '../../api/PluginApiClient'; import { TEST_IDS } from '../../test-helpers/test-ids'; import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext'; import { useProjectContext } from '../../contexts/ProjectContext'; +import { usePromoteRc } from './sideEffects/usePromoteRc'; import { useStyles } from '../../styles/styles'; -import { GetLatestReleaseResult } from '../../api/PluginApiClient'; -import { ResponseStepList } from '../../components/ResponseStepList/ResponseStepList'; -import { LinearProgressWithLabel } from '../../components/LinearProgressWithLabel'; interface PromoteRcBodyProps { rcRelease: NonNullable; @@ -49,13 +48,11 @@ export const PromoteRcBody = ({ rcRelease, successCb }: PromoteRcBodyProps) => { if (responseSteps.length > 0) { return ( - - Promote Release Candidate - - - - - + ); } diff --git a/plugins/github-release-manager/src/components/Dialog.test.tsx b/plugins/github-release-manager/src/components/Dialog.test.tsx new file mode 100644 index 0000000000..cffd24a0bd --- /dev/null +++ b/plugins/github-release-manager/src/components/Dialog.test.tsx @@ -0,0 +1,42 @@ +/* + * 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 { render } from '@testing-library/react'; + +import { Dialog } from './Dialog'; + +jest.mock('../contexts/RefetchContext', () => ({ + useRefetchContext: () => jest.fn(), +})); + +describe('Dialog', () => { + it('should render Dialog', () => { + const mockTitle = 'mock_dialog_title'; + const mockResponseStepMessage = 'banana'; + + const { baseElement } = render( + , + ); + + expect(baseElement.innerHTML).toMatch(mockTitle); + expect(baseElement.innerHTML).toMatch(mockResponseStepMessage); + }); +}); diff --git a/plugins/github-release-manager/src/components/Dialog.tsx b/plugins/github-release-manager/src/components/Dialog.tsx new file mode 100644 index 0000000000..2a393edcf3 --- /dev/null +++ b/plugins/github-release-manager/src/components/Dialog.tsx @@ -0,0 +1,38 @@ +/* + * 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 { DialogTitle, Dialog as MaterialDialog } from '@material-ui/core'; + +import { LinearProgressWithLabel } from './LinearProgressWithLabel'; +import { ResponseStep } from '../types/types'; +import { ResponseStepList } from './ResponseStepList/ResponseStepList'; + +interface DialogProps { + progress: number; + responseSteps: ResponseStep[]; + title: string; +} + +export const Dialog = ({ progress, responseSteps, title }: DialogProps) => ( + + {title} + + + + + +); diff --git a/plugins/github-release-manager/src/components/LinearProgressWithLabel.tsx b/plugins/github-release-manager/src/components/LinearProgressWithLabel.tsx index 9b0a0ec1f9..0ed8216a3d 100644 --- a/plugins/github-release-manager/src/components/LinearProgressWithLabel.tsx +++ b/plugins/github-release-manager/src/components/LinearProgressWithLabel.tsx @@ -26,7 +26,7 @@ export function LinearProgressWithLabel( props: LinearProgressProps & { value: number }, ) { return ( - + From c6ba5560f8bea18821ea9fa94abb9eb7505c7be1 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Tue, 20 Apr 2021 15:27:04 +0200 Subject: [PATCH 043/140] Move useRefetchContext to Dialog Signed-off-by: Erik Engervall --- .../src/components/Dialog.tsx | 37 +++++++++++++++---- .../ResponseStepList.test.tsx | 4 -- .../ResponseStepList/ResponseStepList.tsx | 15 +------- 3 files changed, 30 insertions(+), 26 deletions(-) diff --git a/plugins/github-release-manager/src/components/Dialog.tsx b/plugins/github-release-manager/src/components/Dialog.tsx index 2a393edcf3..5460bc041b 100644 --- a/plugins/github-release-manager/src/components/Dialog.tsx +++ b/plugins/github-release-manager/src/components/Dialog.tsx @@ -15,11 +15,17 @@ */ import React from 'react'; -import { DialogTitle, Dialog as MaterialDialog } from '@material-ui/core'; +import { + Button, + Dialog as MaterialDialog, + DialogActions, + DialogTitle, +} from '@material-ui/core'; import { LinearProgressWithLabel } from './LinearProgressWithLabel'; import { ResponseStep } from '../types/types'; import { ResponseStepList } from './ResponseStepList/ResponseStepList'; +import { useRefetchContext } from '../contexts/RefetchContext'; interface DialogProps { progress: number; @@ -27,12 +33,27 @@ interface DialogProps { title: string; } -export const Dialog = ({ progress, responseSteps, title }: DialogProps) => ( - - {title} +export const Dialog = ({ progress, responseSteps, title }: DialogProps) => { + const { setRefetchTrigger } = useRefetchContext(); - + return ( + + {title} - - -); + + + + + + + + + ); +}; diff --git a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.test.tsx b/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.test.tsx index 21f6c6925e..67fcf634a3 100644 --- a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.test.tsx +++ b/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.test.tsx @@ -20,10 +20,6 @@ import { render } from '@testing-library/react'; import { ResponseStepList } from './ResponseStepList'; import { TEST_IDS } from '../../test-helpers/test-ids'; -jest.mock('../../contexts/RefetchContext', () => ({ - useRefetchContext: () => jest.fn(), -})); - describe('ResponseStepList', () => { it('should render loading state when loading', () => { const { getByTestId } = render( diff --git a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.tsx b/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.tsx index e703f08d34..082fb99517 100644 --- a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.tsx +++ b/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.tsx @@ -15,13 +15,12 @@ */ import React, { PropsWithChildren } from 'react'; -import { Button, DialogActions, DialogContent, List } from '@material-ui/core'; +import { DialogContent, List } from '@material-ui/core'; import { CenteredCircularProgress } from '../CenteredCircularProgress'; import { ResponseStep } from '../../types/types'; import { ResponseStepListItem } from './ResponseStepListItem'; import { TEST_IDS } from '../../test-helpers/test-ids'; -import { useRefetchContext } from '../../contexts/RefetchContext'; interface ResponseStepListProps { responseSteps: (ResponseStep | undefined)[]; @@ -38,8 +37,6 @@ export const ResponseStepList = ({ denseList = false, children, }: PropsWithChildren) => { - const { setRefetchTrigger } = useRefetchContext(); - return ( <> {loading || responseSteps.length === 0 ? ( @@ -72,16 +69,6 @@ export const ResponseStepList = ({ {children} - - - )} From 725d09af25d43c5ebcaac6ceff92da1f5b5da40d Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Tue, 20 Apr 2021 15:39:59 +0200 Subject: [PATCH 044/140] Align @testing-library/react-hooks version with others' Signed-off-by: Erik Engervall --- plugins/github-release-manager/package.json | 2 +- yarn.lock | 52 --------------------- 2 files changed, 1 insertion(+), 53 deletions(-) diff --git a/plugins/github-release-manager/package.json b/plugins/github-release-manager/package.json index cd32f0c105..c73941ec54 100644 --- a/plugins/github-release-manager/package.json +++ b/plugins/github-release-manager/package.json @@ -39,7 +39,7 @@ "@backstage/dev-utils": "^0.1.13", "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1", - "@testing-library/react-hooks": "^5.1.1", + "@testing-library/react-hooks": "^3.4.2", "@testing-library/react": "^11.2.5", "@testing-library/user-event": "^12.0.7", "@types/jest": "^26.0.7", diff --git a/yarn.lock b/yarn.lock index 45a75c49f6..ef3a66bb24 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5550,18 +5550,6 @@ "@babel/runtime" "^7.5.4" "@types/testing-library__react-hooks" "^3.4.0" -"@testing-library/react-hooks@^5.1.1": - version "5.1.1" - resolved "https://registry.npmjs.org/@testing-library/react-hooks/-/react-hooks-5.1.1.tgz#1fbaae8a4e8a4a7f97b176c23e1e890c41bbbfa5" - integrity sha512-52D2XnpelFDefnWpy/V6z2qGNj8JLIvW5DjYtelMvFXdEyWiykSaI7IXHwFy4ICoqXJDmmwHAiFRiFboub/U5g== - dependencies: - "@babel/runtime" "^7.12.5" - "@types/react" ">=16.9.0" - "@types/react-dom" ">=16.9.0" - "@types/react-test-renderer" ">=16.9.0" - filter-console "^0.1.1" - react-error-boundary "^3.1.0" - "@testing-library/react@^11.2.5": version "11.2.6" resolved "https://registry.npmjs.org/@testing-library/react/-/react-11.2.6.tgz#586a23adc63615985d85be0c903f374dab19200b" @@ -6495,13 +6483,6 @@ "@types/webpack" "*" "@types/webpack-dev-server" "*" -"@types/react-dom@>=16.9.0": - version "17.0.3" - resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-17.0.3.tgz#7fdf37b8af9d6d40127137865bb3fff8871d7ee1" - integrity sha512-4NnJbCeWE+8YBzupn/YrJxZ8VnjcJq5iR1laqQ1vkpQgBiA7bwk0Rp24fxsdNinzJY2U+HHS4dJJDPdoMjdJ7w== - dependencies: - "@types/react" "*" - "@types/react-dom@^16.9.8": version "16.9.8" resolved "https://registry.npmjs.org/@types/react-dom/-/react-dom-16.9.8.tgz#fe4c1e11dfc67155733dfa6aa65108b4971cb423" @@ -6545,13 +6526,6 @@ dependencies: "@types/react" "*" -"@types/react-test-renderer@>=16.9.0": - version "17.0.1" - resolved "https://registry.npmjs.org/@types/react-test-renderer/-/react-test-renderer-17.0.1.tgz#3120f7d1c157fba9df0118dae20cb0297ee0e06b" - integrity sha512-3Fi2O6Zzq/f3QR9dRnlnHso9bMl7weKCviFmfF6B4LS1Uat6Hkm15k0ZAQuDz+UBq6B3+g+NM6IT2nr5QgPzCw== - dependencies: - "@types/react" "*" - "@types/react-text-truncate@^0.14.0": version "0.14.0" resolved "https://registry.npmjs.org/@types/react-text-truncate/-/react-text-truncate-0.14.0.tgz#588bbabbc7f2a13815e805f3a48942db73fe65fe" @@ -6588,15 +6562,6 @@ dependencies: csstype "^2.2.0" -"@types/react@>=16.9.0": - version "17.0.3" - resolved "https://registry.npmjs.org/@types/react/-/react-17.0.3.tgz#ba6e215368501ac3826951eef2904574c262cc79" - integrity sha512-wYOUxIgs2HZZ0ACNiIayItyluADNbONl7kt8lkLjVK8IitMH5QMyAh75Fwhmo37r1m7L2JaFj03sIfxBVDvRAg== - dependencies: - "@types/prop-types" "*" - "@types/scheduler" "*" - csstype "^3.0.2" - "@types/reactcss@*": version "1.2.3" resolved "https://registry.npmjs.org/@types/reactcss/-/reactcss-1.2.3.tgz#af28ae11bbb277978b99d04d1eedfd068ca71834" @@ -6678,11 +6643,6 @@ "@types/node" "*" rollup "^0.63.4" -"@types/scheduler@*": - version "0.16.1" - resolved "https://registry.npmjs.org/@types/scheduler/-/scheduler-0.16.1.tgz#18845205e86ff0038517aab7a18a62a6b9f71275" - integrity sha512-EaCxbanVeyxDRTQBkdLb3Bvl/HK7PBK6UJjsSixB0iHKoWxE5uu2Q/DgtpOhPIojN0Zl1whvOd7PoHs2P0s5eA== - "@types/semver@^6.0.0": version "6.2.1" resolved "https://registry.npmjs.org/@types/semver/-/semver-6.2.1.tgz#a236185670a7860f1597cf73bea2e16d001461ba" @@ -13294,11 +13254,6 @@ fill-range@^7.0.1: dependencies: to-regex-range "^5.0.1" -filter-console@^0.1.1: - version "0.1.1" - resolved "https://registry.npmjs.org/filter-console/-/filter-console-0.1.1.tgz#6242be28982bba7415bcc6db74a79f4a294fa67c" - integrity sha512-zrXoV1Uaz52DqPs+qEwNJWJFAWZpYJ47UNmpN9q4j+/EYsz85uV0DC9k8tRND5kYmoVzL0W+Y75q4Rg8sRJCdg== - filter-obj@^1.1.0: version "1.1.0" resolved "https://registry.npmjs.org/filter-obj/-/filter-obj-1.1.0.tgz#9b311112bc6c6127a16e016c6c5d7f19e0805c5b" @@ -21954,13 +21909,6 @@ react-draggable@^4.0.3: classnames "^2.2.5" prop-types "^15.6.0" -react-error-boundary@^3.1.0: - version "3.1.1" - resolved "https://registry.npmjs.org/react-error-boundary/-/react-error-boundary-3.1.1.tgz#932c5ca5cbab8ec4fe37fd7b415aa5c3a47597e7" - integrity sha512-W3xCd9zXnanqrTUeViceufD3mIW8Ut29BUD+S2f0eO2XCOU8b6UrJfY46RDGe5lxCJzfe4j0yvIfh0RbTZhKJw== - dependencies: - "@babel/runtime" "^7.12.5" - react-error-overlay@^6.0.7, react-error-overlay@^6.0.9: version "6.0.9" resolved "https://registry.npmjs.org/react-error-overlay/-/react-error-overlay-6.0.9.tgz#3c743010c9359608c375ecd6bc76f35d93995b0a" From cba751d5045184514342cfa21aef8f00d3e32c63 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Tue, 20 Apr 2021 19:04:22 +0200 Subject: [PATCH 045/140] Prettifying Signed-off-by: Erik Engervall --- .../src/GitHubReleaseManager.tsx | 2 +- .../src/api/PluginApiClient.ts | 2 +- .../src/cards/Cards.tsx | 22 +++--- .../cards/{patchRc => Patch}/Patch.test.tsx | 0 .../src/cards/{patchRc => Patch}/Patch.tsx | 0 .../{patchRc => Patch}/PatchBody.test.tsx | 4 +- .../cards/{patchRc => Patch}/PatchBody.tsx | 16 ++-- .../hooks}/usePatch.test.ts | 0 .../sideEffects => Patch/hooks}/usePatch.ts | 15 ++-- .../PromoteRc.test.tsx | 10 ++- .../PromoteRc.tsx | 13 +-- .../PromoteRcBody.test.tsx | 2 +- .../PromoteRcBody.tsx | 9 ++- .../hooks}/usePromoteRc.test.ts | 0 .../hooks}/usePromoteRc.ts | 13 ++- .../Owner.test.tsx | 0 .../Owner.tsx | 2 +- .../Repo.test.tsx | 0 .../{projectForm => RepoDetailsForm}/Repo.tsx | 12 +-- .../RepoDetailsForm.tsx | 0 .../VersioningStrategy.test.tsx | 0 .../VersioningStrategy.tsx | 2 +- .../styles.ts | 0 .../src/cards/createRc/CreateRc.test.tsx | 21 ++--- .../src/cards/createRc/CreateRc.tsx | 18 ++--- .../{ => helpers}/getRcGitHubInfo.test.ts | 4 +- .../createRc/{ => helpers}/getRcGitHubInfo.ts | 10 +-- .../useCreateRc.test.tsx | 1 + .../{sideEffects => hooks}/useCreateRc.ts | 15 ++-- .../components/LinearProgressWithLabel.tsx | 40 ---------- .../LinearProgressWithLabel.tsx | 79 +++++++++++++++++++ .../ResponseStepDialog.test.tsx} | 10 +-- .../ResponseStepDialog.tsx} | 41 +++++++--- .../ResponseStepList.test.tsx | 0 .../ResponseStepList.tsx | 0 .../ResponseStepListItem.test.tsx | 2 +- .../ResponseStepListItem.tsx | 2 +- .../useGetGitHubBatchInfo.ts} | 44 +++++++---- .../src/hooks/useResponseSteps.ts | 36 ++++----- .../src/test-helpers/test-helpers.ts | 6 +- .../github-release-manager/src/types/types.ts | 7 ++ 41 files changed, 282 insertions(+), 178 deletions(-) rename plugins/github-release-manager/src/cards/{patchRc => Patch}/Patch.test.tsx (100%) rename plugins/github-release-manager/src/cards/{patchRc => Patch}/Patch.tsx (100%) rename plugins/github-release-manager/src/cards/{patchRc => Patch}/PatchBody.test.tsx (98%) rename plugins/github-release-manager/src/cards/{patchRc => Patch}/PatchBody.tsx (94%) rename plugins/github-release-manager/src/cards/{patchRc/sideEffects => Patch/hooks}/usePatch.test.ts (100%) rename plugins/github-release-manager/src/cards/{patchRc/sideEffects => Patch/hooks}/usePatch.ts (97%) rename plugins/github-release-manager/src/cards/{promoteRc => PromoteReleaseCandidate}/PromoteRc.test.tsx (83%) rename plugins/github-release-manager/src/cards/{promoteRc => PromoteReleaseCandidate}/PromoteRc.tsx (93%) rename plugins/github-release-manager/src/cards/{promoteRc => PromoteReleaseCandidate}/PromoteRcBody.test.tsx (96%) rename plugins/github-release-manager/src/cards/{promoteRc => PromoteReleaseCandidate}/PromoteRcBody.tsx (89%) rename plugins/github-release-manager/src/cards/{promoteRc/sideEffects => PromoteReleaseCandidate/hooks}/usePromoteRc.test.ts (100%) rename plugins/github-release-manager/src/cards/{promoteRc/sideEffects => PromoteReleaseCandidate/hooks}/usePromoteRc.ts (92%) rename plugins/github-release-manager/src/cards/{projectForm => RepoDetailsForm}/Owner.test.tsx (100%) rename plugins/github-release-manager/src/cards/{projectForm => RepoDetailsForm}/Owner.tsx (100%) rename plugins/github-release-manager/src/cards/{projectForm => RepoDetailsForm}/Repo.test.tsx (100%) rename plugins/github-release-manager/src/cards/{projectForm => RepoDetailsForm}/Repo.tsx (100%) rename plugins/github-release-manager/src/cards/{projectForm => RepoDetailsForm}/RepoDetailsForm.tsx (100%) rename plugins/github-release-manager/src/cards/{projectForm => RepoDetailsForm}/VersioningStrategy.test.tsx (100%) rename plugins/github-release-manager/src/cards/{projectForm => RepoDetailsForm}/VersioningStrategy.tsx (100%) rename plugins/github-release-manager/src/cards/{projectForm => RepoDetailsForm}/styles.ts (100%) rename plugins/github-release-manager/src/cards/createRc/{ => helpers}/getRcGitHubInfo.test.ts (98%) rename plugins/github-release-manager/src/cards/createRc/{ => helpers}/getRcGitHubInfo.ts (82%) rename plugins/github-release-manager/src/cards/createRc/{sideEffects => hooks}/useCreateRc.test.tsx (99%) rename plugins/github-release-manager/src/cards/createRc/{sideEffects => hooks}/useCreateRc.ts (95%) delete mode 100644 plugins/github-release-manager/src/components/LinearProgressWithLabel.tsx create mode 100644 plugins/github-release-manager/src/components/ResponseStepDialog/LinearProgressWithLabel.tsx rename plugins/github-release-manager/src/components/{Dialog.test.tsx => ResponseStepDialog/ResponseStepDialog.test.tsx} (83%) rename plugins/github-release-manager/src/components/{Dialog.tsx => ResponseStepDialog/ResponseStepDialog.tsx} (57%) rename plugins/github-release-manager/src/components/{ResponseStepList => ResponseStepDialog}/ResponseStepList.test.tsx (100%) rename plugins/github-release-manager/src/components/{ResponseStepList => ResponseStepDialog}/ResponseStepList.tsx (100%) rename plugins/github-release-manager/src/components/{ResponseStepList => ResponseStepDialog}/ResponseStepListItem.test.tsx (100%) rename plugins/github-release-manager/src/components/{ResponseStepList => ResponseStepDialog}/ResponseStepListItem.tsx (100%) rename plugins/github-release-manager/src/{sideEffects/getGitHubBatchInfo.ts => hooks/useGetGitHubBatchInfo.ts} (55%) diff --git a/plugins/github-release-manager/src/GitHubReleaseManager.tsx b/plugins/github-release-manager/src/GitHubReleaseManager.tsx index 1160016a66..64b7ebb028 100644 --- a/plugins/github-release-manager/src/GitHubReleaseManager.tsx +++ b/plugins/github-release-manager/src/GitHubReleaseManager.tsx @@ -32,7 +32,7 @@ import { InfoCardPlus } from './components/InfoCardPlus'; import { isProjectValid } from './helpers/isProjectValid'; import { PluginApiClientContext } from './contexts/PluginApiClientContext'; import { ProjectContext, Project } from './contexts/ProjectContext'; -import { RepoDetailsForm } from './cards/projectForm/RepoDetailsForm'; +import { RepoDetailsForm } from './cards/RepoDetailsForm/RepoDetailsForm'; import { useQueryHandler } from './hooks/useQueryHandler'; import { useStyles } from './styles/styles'; diff --git a/plugins/github-release-manager/src/api/PluginApiClient.ts b/plugins/github-release-manager/src/api/PluginApiClient.ts index 9c6b840989..276951b598 100644 --- a/plugins/github-release-manager/src/api/PluginApiClient.ts +++ b/plugins/github-release-manager/src/api/PluginApiClient.ts @@ -20,7 +20,7 @@ import { readGitHubIntegrationConfigs } from '@backstage/integration'; import { CalverTagParts } from '../helpers/tagParts/getCalverTagParts'; import { DISABLE_CACHE } from '../constants/constants'; -import { getRcGitHubInfo } from '../cards/createRc/getRcGitHubInfo'; +import { getRcGitHubInfo } from '../cards/CreateRc/helpers/getRcGitHubInfo'; import { Project } from '../contexts/ProjectContext'; import { SemverTagParts } from '../helpers/tagParts/getSemverTagParts'; diff --git a/plugins/github-release-manager/src/cards/Cards.tsx b/plugins/github-release-manager/src/cards/Cards.tsx index 5773717cd8..5fbb7fb1f7 100644 --- a/plugins/github-release-manager/src/cards/Cards.tsx +++ b/plugins/github-release-manager/src/cards/Cards.tsx @@ -15,18 +15,17 @@ */ import React, { useState } from 'react'; -import { useAsync } from 'react-use'; import { ErrorBoundary } from '@backstage/core'; import { Alert } from '@material-ui/lab'; import { CenteredCircularProgress } from '../components/CenteredCircularProgress'; -import { CreateRc } from './createRc/CreateRc'; -import { getGitHubBatchInfo } from '../sideEffects/getGitHubBatchInfo'; +import { CreateRc } from './CreateRc/CreateRc'; import { GitHubReleaseManagerProps } from '../GitHubReleaseManager'; -import { Info } from './info/Info'; -import { Patch } from './patchRc/Patch'; -import { PromoteRc } from './promoteRc/PromoteRc'; +import { Info } from './Info/Info'; +import { Patch } from './Patch/Patch'; +import { PromoteReleaseCandidate } from './PromoteReleaseCandidate/PromoteRc'; import { RefetchContext } from '../contexts/RefetchContext'; +import { useGetGitHubBatchInfo } from '../hooks/useGetGitHubBatchInfo'; import { usePluginApiClientContext } from '../contexts/PluginApiClientContext'; import { useProjectContext } from '../contexts/ProjectContext'; import { useVersioningStrategyMatchesRepoTags } from '../hooks/useVersioningStrategyMatchesRepoTags'; @@ -39,10 +38,11 @@ export function Cards({ const pluginApiClient = usePluginApiClientContext(); const project = useProjectContext(); const [refetchTrigger, setRefetchTrigger] = useState(0); - const gitHubBatchInfo = useAsync( - getGitHubBatchInfo({ project, pluginApiClient }), - [project, refetchTrigger], - ); + const { gitHubBatchInfo } = useGetGitHubBatchInfo({ + pluginApiClient, + project, + refetchTrigger, + }); const { versioningStrategyMatches } = useVersioningStrategyMatchesRepoTags({ latestReleaseTagName: gitHubBatchInfo.value?.latestRelease?.tagName, @@ -111,7 +111,7 @@ export function Cards({ )} {!components?.promoteRc?.omit && ( - diff --git a/plugins/github-release-manager/src/cards/patchRc/Patch.test.tsx b/plugins/github-release-manager/src/cards/Patch/Patch.test.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/patchRc/Patch.test.tsx rename to plugins/github-release-manager/src/cards/Patch/Patch.test.tsx diff --git a/plugins/github-release-manager/src/cards/patchRc/Patch.tsx b/plugins/github-release-manager/src/cards/Patch/Patch.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/patchRc/Patch.tsx rename to plugins/github-release-manager/src/cards/Patch/Patch.tsx diff --git a/plugins/github-release-manager/src/cards/patchRc/PatchBody.test.tsx b/plugins/github-release-manager/src/cards/Patch/PatchBody.test.tsx similarity index 98% rename from plugins/github-release-manager/src/cards/patchRc/PatchBody.test.tsx rename to plugins/github-release-manager/src/cards/Patch/PatchBody.test.tsx index f9a8babb5d..2d8025bd3f 100644 --- a/plugins/github-release-manager/src/cards/patchRc/PatchBody.test.tsx +++ b/plugins/github-release-manager/src/cards/Patch/PatchBody.test.tsx @@ -21,8 +21,8 @@ import { mockApiClient, mockBumpedTag, mockCalverProject, - mockReleaseCandidateCalver, mockReleaseBranch, + mockReleaseCandidateCalver, mockReleaseVersionCalver, mockTagParts, } from '../../test-helpers/test-helpers'; @@ -33,7 +33,7 @@ jest.mock('../../contexts/PluginApiClientContext', () => ({ jest.mock('../../contexts/ProjectContext', () => ({ useProjectContext: jest.fn(() => mockCalverProject), })); -jest.mock('./sideEffects/usePatch', () => ({ +jest.mock('./hooks/usePatch', () => ({ usePatch: () => ({ run: jest.fn(), responseSteps: [], diff --git a/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx b/plugins/github-release-manager/src/cards/Patch/PatchBody.tsx similarity index 94% rename from plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx rename to plugins/github-release-manager/src/cards/Patch/PatchBody.tsx index ce0b3acd1c..6f112cf013 100644 --- a/plugins/github-release-manager/src/cards/patchRc/PatchBody.tsx +++ b/plugins/github-release-manager/src/cards/Patch/PatchBody.tsx @@ -40,12 +40,12 @@ import { import { CalverTagParts } from '../../helpers/tagParts/getCalverTagParts'; import { CenteredCircularProgress } from '../../components/CenteredCircularProgress'; import { ComponentConfigPatch } from '../../types/types'; -import { Dialog } from '../../components/Dialog'; import { Differ } from '../../components/Differ'; import { GitHubReleaseManagerError } from '../../errors/GitHubReleaseManagerError'; +import { ResponseStepDialog } from '../../components/ResponseStepDialog/ResponseStepDialog'; import { SemverTagParts } from '../../helpers/tagParts/getSemverTagParts'; import { TEST_IDS } from '../../test-helpers/test-ids'; -import { usePatch } from './sideEffects/usePatch'; +import { usePatch } from './hooks/usePatch'; import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext'; import { useProjectContext } from '../../contexts/ProjectContext'; import { useStyles } from '../../styles/styles'; @@ -91,7 +91,7 @@ export const PatchBody = ({ }; }); - const { run, responseSteps, progress } = usePatch({ + const { progress, responseSteps, run, runInvoked } = usePatch({ bumpedTag, latestRelease, pluginApiClient, @@ -101,7 +101,7 @@ export const PatchBody = ({ }); if (responseSteps.length > 0) { return ( - 0 || commitExistsOnReleaseBranch || hasNoParent + runInvoked || commitExistsOnReleaseBranch || hasNoParent } role={undefined} dense @@ -238,7 +238,11 @@ export const PatchBody = ({ { const repoPath = pluginApiClient.getRepoPath({ owner: project.owner, diff --git a/plugins/github-release-manager/src/cards/patchRc/sideEffects/usePatch.test.ts b/plugins/github-release-manager/src/cards/Patch/hooks/usePatch.test.ts similarity index 100% rename from plugins/github-release-manager/src/cards/patchRc/sideEffects/usePatch.test.ts rename to plugins/github-release-manager/src/cards/Patch/hooks/usePatch.test.ts diff --git a/plugins/github-release-manager/src/cards/patchRc/sideEffects/usePatch.ts b/plugins/github-release-manager/src/cards/Patch/hooks/usePatch.ts similarity index 97% rename from plugins/github-release-manager/src/cards/patchRc/sideEffects/usePatch.ts rename to plugins/github-release-manager/src/cards/Patch/hooks/usePatch.ts index 92eae6defc..af7e890ca9 100644 --- a/plugins/github-release-manager/src/cards/patchRc/sideEffects/usePatch.ts +++ b/plugins/github-release-manager/src/cards/Patch/hooks/usePatch.ts @@ -17,13 +17,13 @@ import { useEffect, useState } from 'react'; import { useAsync, useAsyncFn } from 'react-use'; -import { ComponentConfigPatch } from '../../../types/types'; -import { CalverTagParts } from '../../../helpers/tagParts/getCalverTagParts'; import { GetLatestReleaseResult, GetRecentCommitsResultSingle, IPluginApiClient, } from '../../../api/PluginApiClient'; +import { CalverTagParts } from '../../../helpers/tagParts/getCalverTagParts'; +import { ComponentConfigPatch, CardHook } from '../../../types/types'; import { Project } from '../../../contexts/ProjectContext'; import { SemverTagParts } from '../../../helpers/tagParts/getSemverTagParts'; import { useResponseSteps } from '../../../hooks/useResponseSteps'; @@ -45,7 +45,7 @@ export function usePatch({ project, tagParts, successCb, -}: Patch) { +}: Patch): CardHook { const { responseSteps, addStepToResponseSteps, @@ -345,8 +345,13 @@ export function usePatch({ }, [TOTAL_STEPS, responseSteps.length]); return { - run, - responseSteps, progress, + responseSteps, + run, + runInvoked: Boolean( + releaseBranchRes.loading || + releaseBranchRes.value || + releaseBranchRes.error, + ), }; } diff --git a/plugins/github-release-manager/src/cards/promoteRc/PromoteRc.test.tsx b/plugins/github-release-manager/src/cards/PromoteReleaseCandidate/PromoteRc.test.tsx similarity index 83% rename from plugins/github-release-manager/src/cards/promoteRc/PromoteRc.test.tsx rename to plugins/github-release-manager/src/cards/PromoteReleaseCandidate/PromoteRc.test.tsx index f4e7c1fb41..e917c295d6 100644 --- a/plugins/github-release-manager/src/cards/promoteRc/PromoteRc.test.tsx +++ b/plugins/github-release-manager/src/cards/PromoteReleaseCandidate/PromoteRc.test.tsx @@ -29,11 +29,13 @@ jest.mock('./PromoteRcBody', () => ({ ), })); -import { PromoteRc } from './PromoteRc'; +import { PromoteReleaseCandidate } from './PromoteRc'; describe('PromoteRc', () => { it('return early if no latest release present', () => { - const { getByTestId } = render(); + const { getByTestId } = render( + , + ); expect( getByTestId(TEST_IDS.components.noLatestRelease), @@ -42,7 +44,7 @@ describe('PromoteRc', () => { it('should display not-rc warning', () => { const { getByTestId } = render( - , + , ); expect(getByTestId(TEST_IDS.promoteRc.notRcWarning)).toBeInTheDocument(); @@ -50,7 +52,7 @@ describe('PromoteRc', () => { it('should display PromoteRcBody', () => { const { getByTestId } = render( - , + , ); expect( diff --git a/plugins/github-release-manager/src/cards/promoteRc/PromoteRc.tsx b/plugins/github-release-manager/src/cards/PromoteReleaseCandidate/PromoteRc.tsx similarity index 93% rename from plugins/github-release-manager/src/cards/promoteRc/PromoteRc.tsx rename to plugins/github-release-manager/src/cards/PromoteReleaseCandidate/PromoteRc.tsx index 42ceb1ff47..56b506bcab 100644 --- a/plugins/github-release-manager/src/cards/promoteRc/PromoteRc.tsx +++ b/plugins/github-release-manager/src/cards/PromoteReleaseCandidate/PromoteRc.tsx @@ -18,20 +18,23 @@ import React from 'react'; import { Alert, AlertTitle } from '@material-ui/lab'; import { Typography } from '@material-ui/core'; +import { ComponentConfigPromoteRc } from '../../types/types'; +import { GetLatestReleaseResult } from '../../api/PluginApiClient'; import { InfoCardPlus } from '../../components/InfoCardPlus'; import { NoLatestRelease } from '../../components/NoLatestRelease'; -import { ComponentConfigPromoteRc } from '../../types/types'; import { PromoteRcBody } from './PromoteRcBody'; -import { useStyles } from '../../styles/styles'; import { TEST_IDS } from '../../test-helpers/test-ids'; -import { GetLatestReleaseResult } from '../../api/PluginApiClient'; +import { useStyles } from '../../styles/styles'; -interface PromoteRcProps { +interface PromoteReleaseCandidateProps { latestRelease: GetLatestReleaseResult; successCb?: ComponentConfigPromoteRc['successCb']; } -export const PromoteRc = ({ latestRelease, successCb }: PromoteRcProps) => { +export const PromoteReleaseCandidate = ({ + latestRelease, + successCb, +}: PromoteReleaseCandidateProps) => { const classes = useStyles(); function Body() { diff --git a/plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.test.tsx b/plugins/github-release-manager/src/cards/PromoteReleaseCandidate/PromoteRcBody.test.tsx similarity index 96% rename from plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.test.tsx rename to plugins/github-release-manager/src/cards/PromoteReleaseCandidate/PromoteRcBody.test.tsx index ca6cce1811..573e85bf10 100644 --- a/plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.test.tsx +++ b/plugins/github-release-manager/src/cards/PromoteReleaseCandidate/PromoteRcBody.test.tsx @@ -30,7 +30,7 @@ jest.mock('../../contexts/PluginApiClientContext', () => ({ jest.mock('../../contexts/ProjectContext', () => ({ useProjectContext: jest.fn(() => mockCalverProject), })); -jest.mock('./sideEffects/usePromoteRc', () => ({ +jest.mock('./hooks/usePromoteRc', () => ({ usePromoteRc: () => ({ run: jest.fn(), responseSteps: [], diff --git a/plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.tsx b/plugins/github-release-manager/src/cards/PromoteReleaseCandidate/PromoteRcBody.tsx similarity index 89% rename from plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.tsx rename to plugins/github-release-manager/src/cards/PromoteReleaseCandidate/PromoteRcBody.tsx index a3b2ef0f68..88d1ccbf79 100644 --- a/plugins/github-release-manager/src/cards/promoteRc/PromoteRcBody.tsx +++ b/plugins/github-release-manager/src/cards/PromoteReleaseCandidate/PromoteRcBody.tsx @@ -18,13 +18,13 @@ import React from 'react'; import { Button, Typography } from '@material-ui/core'; import { ComponentConfigPromoteRc } from '../../types/types'; -import { Dialog } from '../../components/Dialog'; import { Differ } from '../../components/Differ'; import { GetLatestReleaseResult } from '../../api/PluginApiClient'; +import { ResponseStepDialog } from '../../components/ResponseStepDialog/ResponseStepDialog'; import { TEST_IDS } from '../../test-helpers/test-ids'; import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext'; import { useProjectContext } from '../../contexts/ProjectContext'; -import { usePromoteRc } from './sideEffects/usePromoteRc'; +import { usePromoteRc } from './hooks/usePromoteRc'; import { useStyles } from '../../styles/styles'; interface PromoteRcBodyProps { @@ -38,7 +38,7 @@ export const PromoteRcBody = ({ rcRelease, successCb }: PromoteRcBodyProps) => { const classes = useStyles(); const releaseVersion = rcRelease.tagName.replace('rc-', 'version-'); - const { run, responseSteps, progress } = usePromoteRc({ + const { progress, responseSteps, run, runInvoked } = usePromoteRc({ pluginApiClient, project, rcRelease, @@ -48,7 +48,7 @@ export const PromoteRcBody = ({ rcRelease, successCb }: PromoteRcBodyProps) => { if (responseSteps.length > 0) { return ( - { data-testid={TEST_IDS.promoteRc.cta} variant="contained" color="primary" + disabled={runInvoked} onClick={() => run()} > Promote Release Candidate diff --git a/plugins/github-release-manager/src/cards/promoteRc/sideEffects/usePromoteRc.test.ts b/plugins/github-release-manager/src/cards/PromoteReleaseCandidate/hooks/usePromoteRc.test.ts similarity index 100% rename from plugins/github-release-manager/src/cards/promoteRc/sideEffects/usePromoteRc.test.ts rename to plugins/github-release-manager/src/cards/PromoteReleaseCandidate/hooks/usePromoteRc.test.ts diff --git a/plugins/github-release-manager/src/cards/promoteRc/sideEffects/usePromoteRc.ts b/plugins/github-release-manager/src/cards/PromoteReleaseCandidate/hooks/usePromoteRc.ts similarity index 92% rename from plugins/github-release-manager/src/cards/promoteRc/sideEffects/usePromoteRc.ts rename to plugins/github-release-manager/src/cards/PromoteReleaseCandidate/hooks/usePromoteRc.ts index b882b0bdcf..abcab2f480 100644 --- a/plugins/github-release-manager/src/cards/promoteRc/sideEffects/usePromoteRc.ts +++ b/plugins/github-release-manager/src/cards/PromoteReleaseCandidate/hooks/usePromoteRc.ts @@ -17,11 +17,11 @@ import { useState, useEffect } from 'react'; import { useAsync, useAsyncFn } from 'react-use'; -import { ComponentConfigPromoteRc } from '../../../types/types'; import { GetLatestReleaseResult, IPluginApiClient, } from '../../../api/PluginApiClient'; +import { CardHook, ComponentConfigPromoteRc } from '../../../types/types'; import { Project } from '../../../contexts/ProjectContext'; import { useResponseSteps } from '../../../hooks/useResponseSteps'; @@ -39,7 +39,7 @@ export function usePromoteRc({ rcRelease, releaseVersion, successCb, -}: PromoteRc) { +}: PromoteRc): CardHook { const { responseSteps, addStepToResponseSteps, @@ -105,8 +105,13 @@ export function usePromoteRc({ }, [TOTAL_STEPS, responseSteps.length]); return { - run, - responseSteps, progress, + responseSteps, + run, + runInvoked: Boolean( + promotedReleaseRes.loading || + promotedReleaseRes.value || + promotedReleaseRes.error, + ), }; } diff --git a/plugins/github-release-manager/src/cards/projectForm/Owner.test.tsx b/plugins/github-release-manager/src/cards/RepoDetailsForm/Owner.test.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/projectForm/Owner.test.tsx rename to plugins/github-release-manager/src/cards/RepoDetailsForm/Owner.test.tsx diff --git a/plugins/github-release-manager/src/cards/projectForm/Owner.tsx b/plugins/github-release-manager/src/cards/RepoDetailsForm/Owner.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/projectForm/Owner.tsx rename to plugins/github-release-manager/src/cards/RepoDetailsForm/Owner.tsx index 8f67038697..4c040cac4f 100644 --- a/plugins/github-release-manager/src/cards/projectForm/Owner.tsx +++ b/plugins/github-release-manager/src/cards/RepoDetailsForm/Owner.tsx @@ -26,11 +26,11 @@ import { } from '@material-ui/core'; import { CenteredCircularProgress } from '../../components/CenteredCircularProgress'; +import { TEST_IDS } from '../../test-helpers/test-ids'; import { useFormClasses } from './styles'; import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext'; import { useProjectContext } from '../../contexts/ProjectContext'; import { useQueryHandler } from '../../hooks/useQueryHandler'; -import { TEST_IDS } from '../../test-helpers/test-ids'; export function Owner({ username }: { username: string }) { const project = useProjectContext(); diff --git a/plugins/github-release-manager/src/cards/projectForm/Repo.test.tsx b/plugins/github-release-manager/src/cards/RepoDetailsForm/Repo.test.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/projectForm/Repo.test.tsx rename to plugins/github-release-manager/src/cards/RepoDetailsForm/Repo.test.tsx diff --git a/plugins/github-release-manager/src/cards/projectForm/Repo.tsx b/plugins/github-release-manager/src/cards/RepoDetailsForm/Repo.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/projectForm/Repo.tsx rename to plugins/github-release-manager/src/cards/RepoDetailsForm/Repo.tsx index 4c7b995c44..57a339ab81 100644 --- a/plugins/github-release-manager/src/cards/projectForm/Repo.tsx +++ b/plugins/github-release-manager/src/cards/RepoDetailsForm/Repo.tsx @@ -19,18 +19,18 @@ import { useAsync } from 'react-use'; import { useNavigate } from 'react-router'; import { FormControl, - InputLabel, - Select, - MenuItem, FormHelperText, + InputLabel, + MenuItem, + Select, } from '@material-ui/core'; -import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext'; -import { useFormClasses } from './styles'; import { CenteredCircularProgress } from '../../components/CenteredCircularProgress'; +import { TEST_IDS } from '../../test-helpers/test-ids'; +import { useFormClasses } from './styles'; +import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext'; import { useProjectContext } from '../../contexts/ProjectContext'; import { useQueryHandler } from '../../hooks/useQueryHandler'; -import { TEST_IDS } from '../../test-helpers/test-ids'; export function Repo() { const pluginApiClient = usePluginApiClientContext(); diff --git a/plugins/github-release-manager/src/cards/projectForm/RepoDetailsForm.tsx b/plugins/github-release-manager/src/cards/RepoDetailsForm/RepoDetailsForm.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/projectForm/RepoDetailsForm.tsx rename to plugins/github-release-manager/src/cards/RepoDetailsForm/RepoDetailsForm.tsx diff --git a/plugins/github-release-manager/src/cards/projectForm/VersioningStrategy.test.tsx b/plugins/github-release-manager/src/cards/RepoDetailsForm/VersioningStrategy.test.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/projectForm/VersioningStrategy.test.tsx rename to plugins/github-release-manager/src/cards/RepoDetailsForm/VersioningStrategy.test.tsx diff --git a/plugins/github-release-manager/src/cards/projectForm/VersioningStrategy.tsx b/plugins/github-release-manager/src/cards/RepoDetailsForm/VersioningStrategy.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/projectForm/VersioningStrategy.tsx rename to plugins/github-release-manager/src/cards/RepoDetailsForm/VersioningStrategy.tsx index ebbee5b419..b1d4c594cf 100644 --- a/plugins/github-release-manager/src/cards/projectForm/VersioningStrategy.tsx +++ b/plugins/github-release-manager/src/cards/RepoDetailsForm/VersioningStrategy.tsx @@ -24,9 +24,9 @@ import { RadioGroup, } from '@material-ui/core'; +import { TEST_IDS } from '../../test-helpers/test-ids'; import { useProjectContext } from '../../contexts/ProjectContext'; import { useQueryHandler } from '../../hooks/useQueryHandler'; -import { TEST_IDS } from '../../test-helpers/test-ids'; export function VersioningStrategy() { const navigate = useNavigate(); diff --git a/plugins/github-release-manager/src/cards/projectForm/styles.ts b/plugins/github-release-manager/src/cards/RepoDetailsForm/styles.ts similarity index 100% rename from plugins/github-release-manager/src/cards/projectForm/styles.ts rename to plugins/github-release-manager/src/cards/RepoDetailsForm/styles.ts diff --git a/plugins/github-release-manager/src/cards/createRc/CreateRc.test.tsx b/plugins/github-release-manager/src/cards/createRc/CreateRc.test.tsx index fa83f39592..4363d12c98 100644 --- a/plugins/github-release-manager/src/cards/createRc/CreateRc.test.tsx +++ b/plugins/github-release-manager/src/cards/createRc/CreateRc.test.tsx @@ -21,28 +21,31 @@ import { mockApiClient, mockCalverProject, mockNextGitHubInfo, - mockReleaseCandidateCalver, mockReleaseBranch, + mockReleaseCandidateCalver, mockReleaseVersionCalver, mockSemverProject, } from '../../test-helpers/test-helpers'; import { TEST_IDS } from '../../test-helpers/test-ids'; +import { useCreateRc } from './hooks/useCreateRc'; jest.mock('../../contexts/PluginApiClientContext', () => ({ - usePluginApiClientContext: jest.fn(() => mockApiClient), + usePluginApiClientContext: () => mockApiClient, })); jest.mock('../../contexts/ProjectContext', () => ({ useProjectContext: jest.fn(() => mockCalverProject), })); -jest.mock('./getRcGitHubInfo', () => ({ +jest.mock('./helpers/getRcGitHubInfo', () => ({ getRcGitHubInfo: () => mockNextGitHubInfo, })); -jest.mock('./sideEffects/useCreateRc', () => ({ - useCreateRc: () => ({ - run: jest.fn(), - responseSteps: [], - progress: 0, - }), +jest.mock('./hooks/useCreateRc', () => ({ + useCreateRc: () => + ({ + run: jest.fn(), + responseSteps: [], + progress: 0, + runLoading: false, + } as ReturnType), })); import { useProjectContext } from '../../contexts/ProjectContext'; diff --git a/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx b/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx index 8ba80a9aee..f2ca2d7e5f 100644 --- a/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx +++ b/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx @@ -31,13 +31,13 @@ import { GetRepositoryResult, } from '../../api/PluginApiClient'; import { ComponentConfigCreateRc } from '../../types/types'; -import { Dialog } from '../../components/Dialog'; import { Differ } from '../../components/Differ'; -import { getRcGitHubInfo } from './getRcGitHubInfo'; +import { getRcGitHubInfo } from './helpers/getRcGitHubInfo'; import { InfoCardPlus } from '../../components/InfoCardPlus'; +import { ResponseStepDialog } from '../../components/ResponseStepDialog/ResponseStepDialog'; import { SEMVER_PARTS } from '../../constants/constants'; import { TEST_IDS } from '../../test-helpers/test-ids'; -import { useCreateRc } from './sideEffects/useCreateRc'; +import { useCreateRc } from './hooks/useCreateRc'; import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext'; import { useProjectContext } from '../../contexts/ProjectContext'; import { useStyles } from '../../styles/styles'; @@ -72,7 +72,7 @@ export const CreateRc = ({ ); }, [semverBumpLevel, setNextGitHubInfo, latestRelease, project]); - const { run, responseSteps, progress } = useCreateRc({ + const { progress, responseSteps, run, runInvoked } = useCreateRc({ defaultBranch, latestRelease, nextGitHubInfo, @@ -82,7 +82,7 @@ export const CreateRc = ({ }); if (responseSteps.length > 0) { return ( - { - await run(); - }} + onClick={() => run()} > - Create RC + Create Release Candidate ); } diff --git a/plugins/github-release-manager/src/cards/createRc/getRcGitHubInfo.test.ts b/plugins/github-release-manager/src/cards/createRc/helpers/getRcGitHubInfo.test.ts similarity index 98% rename from plugins/github-release-manager/src/cards/createRc/getRcGitHubInfo.test.ts rename to plugins/github-release-manager/src/cards/createRc/helpers/getRcGitHubInfo.test.ts index 6b1feca055..1e41ea0d38 100644 --- a/plugins/github-release-manager/src/cards/createRc/getRcGitHubInfo.test.ts +++ b/plugins/github-release-manager/src/cards/createRc/helpers/getRcGitHubInfo.test.ts @@ -17,11 +17,11 @@ import { DateTime } from 'luxon'; import { - mockSemverProject, mockCalverProject, mockReleaseVersionCalver, mockReleaseVersionSemver, -} from '../../test-helpers/test-helpers'; + mockSemverProject, +} from '../../../test-helpers/test-helpers'; import { getRcGitHubInfo } from './getRcGitHubInfo'; describe('getRcGitHubInfo', () => { diff --git a/plugins/github-release-manager/src/cards/createRc/getRcGitHubInfo.ts b/plugins/github-release-manager/src/cards/createRc/helpers/getRcGitHubInfo.ts similarity index 82% rename from plugins/github-release-manager/src/cards/createRc/getRcGitHubInfo.ts rename to plugins/github-release-manager/src/cards/createRc/helpers/getRcGitHubInfo.ts index d7e63740c2..74407fb749 100644 --- a/plugins/github-release-manager/src/cards/createRc/getRcGitHubInfo.ts +++ b/plugins/github-release-manager/src/cards/createRc/helpers/getRcGitHubInfo.ts @@ -16,11 +16,11 @@ import { DateTime } from 'luxon'; -import { getBumpedSemverTagParts } from '../../helpers/getBumpedTag'; -import { GetLatestReleaseResult } from '../../api/PluginApiClient'; -import { getSemverTagParts } from '../../helpers/tagParts/getSemverTagParts'; -import { Project } from '../../contexts/ProjectContext'; -import { SEMVER_PARTS } from '../../constants/constants'; +import { getBumpedSemverTagParts } from '../../../helpers/getBumpedTag'; +import { GetLatestReleaseResult } from '../../../api/PluginApiClient'; +import { getSemverTagParts } from '../../../helpers/tagParts/getSemverTagParts'; +import { Project } from '../../../contexts/ProjectContext'; +import { SEMVER_PARTS } from '../../../constants/constants'; export const getRcGitHubInfo = ({ project, diff --git a/plugins/github-release-manager/src/cards/createRc/sideEffects/useCreateRc.test.tsx b/plugins/github-release-manager/src/cards/createRc/hooks/useCreateRc.test.tsx similarity index 99% rename from plugins/github-release-manager/src/cards/createRc/sideEffects/useCreateRc.test.tsx rename to plugins/github-release-manager/src/cards/createRc/hooks/useCreateRc.test.tsx index b0ce35a3ec..dd47ec086a 100644 --- a/plugins/github-release-manager/src/cards/createRc/sideEffects/useCreateRc.test.tsx +++ b/plugins/github-release-manager/src/cards/createRc/hooks/useCreateRc.test.tsx @@ -94,6 +94,7 @@ describe('useCreateRc', () => { }, ], "run": [Function], + "runLoading": false, } `); }); diff --git a/plugins/github-release-manager/src/cards/createRc/sideEffects/useCreateRc.ts b/plugins/github-release-manager/src/cards/createRc/hooks/useCreateRc.ts similarity index 95% rename from plugins/github-release-manager/src/cards/createRc/sideEffects/useCreateRc.ts rename to plugins/github-release-manager/src/cards/createRc/hooks/useCreateRc.ts index eb11e25702..2cd18cb4ab 100644 --- a/plugins/github-release-manager/src/cards/createRc/sideEffects/useCreateRc.ts +++ b/plugins/github-release-manager/src/cards/createRc/hooks/useCreateRc.ts @@ -17,15 +17,15 @@ import { useEffect, useState } from 'react'; import { useAsync, useAsyncFn } from 'react-use'; -import { getRcGitHubInfo } from '../getRcGitHubInfo'; -import { ComponentConfigCreateRc } from '../../../types/types'; import { GetLatestReleaseResult, GetRepositoryResult, IPluginApiClient, } from '../../../api/PluginApiClient'; -import { Project } from '../../../contexts/ProjectContext'; +import { CardHook, ComponentConfigCreateRc } from '../../../types/types'; +import { getRcGitHubInfo } from '../helpers/getRcGitHubInfo'; import { GitHubReleaseManagerError } from '../../../errors/GitHubReleaseManagerError'; +import { Project } from '../../../contexts/ProjectContext'; import { useResponseSteps } from '../../../hooks/useResponseSteps'; interface CreateRC { @@ -44,7 +44,7 @@ export function useCreateRc({ pluginApiClient, project, successCb, -}: CreateRC) { +}: CreateRC): CardHook { const { responseSteps, addStepToResponseSteps, @@ -211,8 +211,11 @@ export function useCreateRc({ }, [TOTAL_STEPS, responseSteps.length]); return { - run, - responseSteps, progress, + responseSteps, + run, + runInvoked: Boolean( + latestCommitRes.loading || latestCommitRes.value || latestCommitRes.error, + ), }; } diff --git a/plugins/github-release-manager/src/components/LinearProgressWithLabel.tsx b/plugins/github-release-manager/src/components/LinearProgressWithLabel.tsx deleted file mode 100644 index 0ed8216a3d..0000000000 --- a/plugins/github-release-manager/src/components/LinearProgressWithLabel.tsx +++ /dev/null @@ -1,40 +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, - LinearProgress, - LinearProgressProps, - Typography, -} from '@material-ui/core'; - -export function LinearProgressWithLabel( - props: LinearProgressProps & { value: number }, -) { - return ( - - - - - - {`${Math.round( - props.value, - )}%`} - - - ); -} diff --git a/plugins/github-release-manager/src/components/ResponseStepDialog/LinearProgressWithLabel.tsx b/plugins/github-release-manager/src/components/ResponseStepDialog/LinearProgressWithLabel.tsx new file mode 100644 index 0000000000..a5091021e7 --- /dev/null +++ b/plugins/github-release-manager/src/components/ResponseStepDialog/LinearProgressWithLabel.tsx @@ -0,0 +1,79 @@ +/* + * 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, LinearProgress, Typography } from '@material-ui/core'; + +import { ResponseStep } from '../../types/types'; + +const STATUSES = { + FAILURE: 'FAILURE', + ONGOING: 'ONGOING', + SUCCESS: 'SUCCESS', +} as const; + +export function LinearProgressWithLabel(props: { + progress: number; + responseSteps: ResponseStep[]; +}) { + const roundedValue = Math.ceil(props.progress); + const progress = roundedValue < 100 ? roundedValue : 100; + + const failure = props.responseSteps.some( + responseStep => responseStep.icon === 'failure', + ); + + let status: keyof typeof STATUSES = STATUSES.ONGOING; + if (!failure && progress === 100) status = STATUSES.SUCCESS; + if (failure) status = STATUSES.FAILURE; + + const CompletionEmoji = () => { + if (status === STATUSES.ONGOING) return null; + if (status === STATUSES.FAILURE) return {' 🔥 '}; + return {' 🚀 '}; + }; + + return ( + + + + + + + + + {`${progress}%`} + + + + + ); +} diff --git a/plugins/github-release-manager/src/components/Dialog.test.tsx b/plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepDialog.test.tsx similarity index 83% rename from plugins/github-release-manager/src/components/Dialog.test.tsx rename to plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepDialog.test.tsx index cffd24a0bd..5f1c90c8cf 100644 --- a/plugins/github-release-manager/src/components/Dialog.test.tsx +++ b/plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepDialog.test.tsx @@ -17,19 +17,19 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { Dialog } from './Dialog'; +import { ResponseStepDialog } from './ResponseStepDialog'; -jest.mock('../contexts/RefetchContext', () => ({ +jest.mock('../../contexts/RefetchContext', () => ({ useRefetchContext: () => jest.fn(), })); -describe('Dialog', () => { - it('should render Dialog', () => { +describe('ResponseStepDialog', () => { + it('should render ResponseStepDialog', () => { const mockTitle = 'mock_dialog_title'; const mockResponseStepMessage = 'banana'; const { baseElement } = render( - { +const Transition = forwardRef(function Transition( + props: { children?: React.ReactElement } & TransitionProps, + ref: Ref, +) { + return ; +}); + +export const ResponseStepDialog = ({ + progress, + responseSteps, + title, +}: DialogProps) => { const { setRefetchTrigger } = useRefetchContext(); return ( - + {title} - + - + diff --git a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.test.tsx b/plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepList.test.tsx similarity index 100% rename from plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.test.tsx rename to plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepList.test.tsx diff --git a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.tsx b/plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepList.tsx similarity index 100% rename from plugins/github-release-manager/src/components/ResponseStepList/ResponseStepList.tsx rename to plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepList.tsx diff --git a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepListItem.test.tsx b/plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepListItem.test.tsx similarity index 100% rename from plugins/github-release-manager/src/components/ResponseStepList/ResponseStepListItem.test.tsx rename to plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepListItem.test.tsx index c549ef4f11..f0f185073a 100644 --- a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepListItem.test.tsx +++ b/plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepListItem.test.tsx @@ -17,8 +17,8 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { TEST_IDS } from '../../test-helpers/test-ids'; import { ResponseStepListItem } from './ResponseStepListItem'; +import { TEST_IDS } from '../../test-helpers/test-ids'; describe('ResponseStepListItem', () => { it('should render', () => { diff --git a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepListItem.tsx b/plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepListItem.tsx similarity index 100% rename from plugins/github-release-manager/src/components/ResponseStepList/ResponseStepListItem.tsx rename to plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepListItem.tsx index 08ac005c8d..e1c80691ed 100644 --- a/plugins/github-release-manager/src/components/ResponseStepList/ResponseStepListItem.tsx +++ b/plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepListItem.tsx @@ -28,8 +28,8 @@ import ErrorOutlineIcon from '@material-ui/icons/ErrorOutline'; import FiberManualRecordIcon from '@material-ui/icons/FiberManualRecord'; import OpenInNewIcon from '@material-ui/icons/OpenInNew'; -import { TEST_IDS } from '../../test-helpers/test-ids'; import { ResponseStep } from '../../types/types'; +import { TEST_IDS } from '../../test-helpers/test-ids'; interface ResponseStepListItemProps { responseStep: ResponseStep; diff --git a/plugins/github-release-manager/src/sideEffects/getGitHubBatchInfo.ts b/plugins/github-release-manager/src/hooks/useGetGitHubBatchInfo.ts similarity index 55% rename from plugins/github-release-manager/src/sideEffects/getGitHubBatchInfo.ts rename to plugins/github-release-manager/src/hooks/useGetGitHubBatchInfo.ts index 0a95a7cb5c..5cf5115116 100644 --- a/plugins/github-release-manager/src/sideEffects/getGitHubBatchInfo.ts +++ b/plugins/github-release-manager/src/hooks/useGetGitHubBatchInfo.ts @@ -14,39 +14,49 @@ * limitations under the License. */ +import { useAsync } from 'react-use'; + import { IPluginApiClient } from '../api/PluginApiClient'; import { Project } from '../contexts/ProjectContext'; interface GetGitHubBatchInfo { project: Project; pluginApiClient: IPluginApiClient; + refetchTrigger: number; } -export const getGitHubBatchInfo = ({ +export const useGetGitHubBatchInfo = ({ project, pluginApiClient, -}: GetGitHubBatchInfo) => async () => { - const [repository, latestRelease] = await Promise.all([ - pluginApiClient.getRepository({ ...project }), - pluginApiClient.getLatestRelease({ ...project }), - ]); + refetchTrigger, +}: GetGitHubBatchInfo) => { + const gitHubBatchInfo = useAsync(async () => { + const [repository, latestRelease] = await Promise.all([ + pluginApiClient.getRepository({ ...project }), + pluginApiClient.getLatestRelease({ ...project }), + ]); + + if (latestRelease === null) { + return { + latestRelease, + releaseBranch: null, + repository, + }; + } + + const releaseBranch = await pluginApiClient.getBranch({ + ...project, + branchName: latestRelease.targetCommitish, + }); - if (latestRelease === null) { return { latestRelease, - releaseBranch: null, + releaseBranch, repository, }; - } - - const releaseBranch = await pluginApiClient.getBranch({ - ...project, - branchName: latestRelease.targetCommitish, - }); + }, [project, refetchTrigger]); return { - latestRelease, - releaseBranch, - repository, + gitHubBatchInfo, }; }; diff --git a/plugins/github-release-manager/src/hooks/useResponseSteps.ts b/plugins/github-release-manager/src/hooks/useResponseSteps.ts index dae2612df1..298d8dc11f 100644 --- a/plugins/github-release-manager/src/hooks/useResponseSteps.ts +++ b/plugins/github-release-manager/src/hooks/useResponseSteps.ts @@ -18,37 +18,37 @@ import { useState } from 'react'; import { ResponseStep } from '../types/types'; +const RESPONSE_STEP_FAILURE_ABORT: ResponseStep = { + message: 'Skipped due to error in previous step', + icon: 'failure', +}; + export function useResponseSteps() { const [responseSteps, setResponseSteps] = useState([]); - function abortIfError(error?: Error) { - const RESPONSE_STEP_SKIP = { - responseStep: { - message: 'Skipped due to error in previous step', - icon: 'failure', - } as ResponseStep, - }; + const addStepToResponseSteps = (responseStep: ResponseStep) => { + setResponseSteps([...responseSteps, responseStep]); + }; + const abortIfError = (error?: Error) => { if (error) { - setResponseSteps([...responseSteps, RESPONSE_STEP_SKIP.responseStep]); + addStepToResponseSteps(RESPONSE_STEP_FAILURE_ABORT); throw error; } - } + }; - function asyncCatcher(error: Error): never { + const asyncCatcher = (error?: Error): never => { const responseStepError: ResponseStep = { - message: 'Something went wrong ❌', - secondaryMessage: `Error message: ${error.message}`, + message: 'Something went wrong 🔥', + secondaryMessage: `Error message: ${ + error?.message ? error.message : 'unknown' + }`, icon: 'failure', }; - setResponseSteps([...responseSteps, responseStepError]); + addStepToResponseSteps(responseStepError); throw error; - } - - function addStepToResponseSteps(responseStep: ResponseStep) { - setResponseSteps([...responseSteps, responseStep]); - } + }; return { responseSteps, 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 19e3c57184..f13e5c1c04 100644 --- a/plugins/github-release-manager/src/test-helpers/test-helpers.ts +++ b/plugins/github-release-manager/src/test-helpers/test-helpers.ts @@ -14,15 +14,15 @@ * limitations under the License. */ -import { CalverTagParts } from '../helpers/tagParts/getCalverTagParts'; -import { getRcGitHubInfo } from '../cards/createRc/getRcGitHubInfo'; -import { Project } from '../contexts/ProjectContext'; import { GetBranchResult, GetLatestReleaseResult, GetRecentCommitsResultSingle, IPluginApiClient, } from '../api/PluginApiClient'; +import { CalverTagParts } from '../helpers/tagParts/getCalverTagParts'; +import { getRcGitHubInfo } from '../cards/CreateRc/helpers/getRcGitHubInfo'; +import { Project } from '../contexts/ProjectContext'; const mockOwner = 'mock_owner'; const mockRepo = 'mock_repo'; diff --git a/plugins/github-release-manager/src/types/types.ts b/plugins/github-release-manager/src/types/types.ts index 8687150d7e..c888c7e539 100644 --- a/plugins/github-release-manager/src/types/types.ts +++ b/plugins/github-release-manager/src/types/types.ts @@ -54,3 +54,10 @@ export interface ResponseStep { link?: string; icon?: 'success' | 'failure'; } + +export interface CardHook { + progress: number; + responseSteps: ResponseStep[]; + run: (args: RunArgs) => Promise; + runInvoked: boolean; +} From 6d3fe015fe33b2a906048694021b19c2087ef5dd Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Tue, 20 Apr 2021 19:24:29 +0200 Subject: [PATCH 046/140] Fix tests & add tests for Cards Signed-off-by: Erik Engervall --- .../src/cards/Cards.test.tsx | 53 +++++++++++++++++++ .../src/cards/Patch/hooks/usePatch.test.ts | 1 + .../hooks/usePromoteRc.test.ts | 1 + .../src/cards/createRc/CreateRc.test.tsx | 2 +- .../cards/createRc/hooks/useCreateRc.test.tsx | 2 +- .../src/hooks/useGetGitHubBatchInfo.ts | 2 +- .../src/test-helpers/test-helpers.ts | 8 ++- 7 files changed, 64 insertions(+), 5 deletions(-) create mode 100644 plugins/github-release-manager/src/cards/Cards.test.tsx diff --git a/plugins/github-release-manager/src/cards/Cards.test.tsx b/plugins/github-release-manager/src/cards/Cards.test.tsx new file mode 100644 index 0000000000..2a67a9f62c --- /dev/null +++ b/plugins/github-release-manager/src/cards/Cards.test.tsx @@ -0,0 +1,53 @@ +/* + * 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 { render, act, waitFor } from '@testing-library/react'; + +import { mockApiClient, mockCalverProject } from '../test-helpers/test-helpers'; +import { TEST_IDS } from '../test-helpers/test-ids'; + +jest.mock('../contexts/PluginApiClientContext', () => ({ + usePluginApiClientContext: () => mockApiClient, +})); +jest.mock('../contexts/ProjectContext', () => ({ + useProjectContext: jest.fn(() => mockCalverProject), +})); + +import { Cards } from './Cards'; + +describe('Cards', () => { + it('should render info', async () => { + const { getByTestId } = render( + , + ); + + await act(async () => { + await waitFor(() => getByTestId(TEST_IDS.info.info)); + }); + + expect(getByTestId(TEST_IDS.info.info).innerHTML).toMatchInlineSnapshot( + `"
Terminology

GitHub: The source control system where releases reside in a practical sense. Read more about GitHub releases. (Note that this plugin works just as well with GitHub Enterprise.)

Release Candidate: A GitHub prerelease intended primarily for internal testing

Release Version: A GitHub release intended for end users

"`, + ); + }); +}); diff --git a/plugins/github-release-manager/src/cards/Patch/hooks/usePatch.test.ts b/plugins/github-release-manager/src/cards/Patch/hooks/usePatch.test.ts index ff280ed181..a7b6b2b58d 100644 --- a/plugins/github-release-manager/src/cards/Patch/hooks/usePatch.test.ts +++ b/plugins/github-release-manager/src/cards/Patch/hooks/usePatch.test.ts @@ -113,6 +113,7 @@ describe('patch', () => { }, ], "run": [Function], + "runInvoked": true, } `); }); diff --git a/plugins/github-release-manager/src/cards/PromoteReleaseCandidate/hooks/usePromoteRc.test.ts b/plugins/github-release-manager/src/cards/PromoteReleaseCandidate/hooks/usePromoteRc.test.ts index a055e4acd8..4803f2a551 100644 --- a/plugins/github-release-manager/src/cards/PromoteReleaseCandidate/hooks/usePromoteRc.test.ts +++ b/plugins/github-release-manager/src/cards/PromoteReleaseCandidate/hooks/usePromoteRc.test.ts @@ -76,6 +76,7 @@ describe('usePromoteRc', () => { }, ], "run": [Function], + "runInvoked": true, } `); }); diff --git a/plugins/github-release-manager/src/cards/createRc/CreateRc.test.tsx b/plugins/github-release-manager/src/cards/createRc/CreateRc.test.tsx index 4363d12c98..f4aaaa29ee 100644 --- a/plugins/github-release-manager/src/cards/createRc/CreateRc.test.tsx +++ b/plugins/github-release-manager/src/cards/createRc/CreateRc.test.tsx @@ -44,7 +44,7 @@ jest.mock('./hooks/useCreateRc', () => ({ run: jest.fn(), responseSteps: [], progress: 0, - runLoading: false, + runInvoked: false, } as ReturnType), })); diff --git a/plugins/github-release-manager/src/cards/createRc/hooks/useCreateRc.test.tsx b/plugins/github-release-manager/src/cards/createRc/hooks/useCreateRc.test.tsx index dd47ec086a..e4549b5cb8 100644 --- a/plugins/github-release-manager/src/cards/createRc/hooks/useCreateRc.test.tsx +++ b/plugins/github-release-manager/src/cards/createRc/hooks/useCreateRc.test.tsx @@ -94,7 +94,7 @@ describe('useCreateRc', () => { }, ], "run": [Function], - "runLoading": false, + "runInvoked": true, } `); }); diff --git a/plugins/github-release-manager/src/hooks/useGetGitHubBatchInfo.ts b/plugins/github-release-manager/src/hooks/useGetGitHubBatchInfo.ts index 5cf5115116..e09d165871 100644 --- a/plugins/github-release-manager/src/hooks/useGetGitHubBatchInfo.ts +++ b/plugins/github-release-manager/src/hooks/useGetGitHubBatchInfo.ts @@ -36,7 +36,7 @@ export const useGetGitHubBatchInfo = ({ pluginApiClient.getLatestRelease({ ...project }), ]); - if (latestRelease === null) { + if (!latestRelease) { return { latestRelease, releaseBranch: null, 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 f13e5c1c04..e138512971 100644 --- a/plugins/github-release-manager/src/test-helpers/test-helpers.ts +++ b/plugins/github-release-manager/src/test-helpers/test-helpers.ts @@ -158,9 +158,13 @@ export const mockApiClient: IPluginApiClient = { createMockRecentCommit({ sha: 'mock_sha_recent_commits_2' }), ]), - getLatestRelease: jest.fn(), + getLatestRelease: jest.fn(async () => createMockRelease()), - getRepository: jest.fn(), + getRepository: jest.fn(async () => ({ + pushPermissions: true, + defaultBranch: mockDefaultBranch, + name: mockRepo, + })), getLatestCommit: jest.fn(async () => ({ sha: 'latestCommit.sha', From 8cbf2a2b5d09296c10ab710c7f63676ea2369fa2 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Tue, 20 Apr 2021 21:56:01 +0200 Subject: [PATCH 047/140] Refactor helper Signed-off-by: Erik Engervall --- .../github-release-manager/src/api/PluginApiClient.ts | 2 +- .../src/cards/createRc/CreateRc.test.tsx | 2 +- .../src/cards/createRc/CreateRc.tsx | 2 +- .../src/cards/createRc/hooks/useCreateRc.ts | 2 +- .../createRc => }/helpers/getRcGitHubInfo.test.ts | 2 +- .../{cards/createRc => }/helpers/getRcGitHubInfo.ts | 10 +++++----- .../src/test-helpers/test-helpers.ts | 2 +- 7 files changed, 11 insertions(+), 11 deletions(-) rename plugins/github-release-manager/src/{cards/createRc => }/helpers/getRcGitHubInfo.test.ts (98%) rename plugins/github-release-manager/src/{cards/createRc => }/helpers/getRcGitHubInfo.ts (82%) diff --git a/plugins/github-release-manager/src/api/PluginApiClient.ts b/plugins/github-release-manager/src/api/PluginApiClient.ts index 276951b598..8efaacb4b8 100644 --- a/plugins/github-release-manager/src/api/PluginApiClient.ts +++ b/plugins/github-release-manager/src/api/PluginApiClient.ts @@ -20,7 +20,7 @@ import { readGitHubIntegrationConfigs } from '@backstage/integration'; import { CalverTagParts } from '../helpers/tagParts/getCalverTagParts'; import { DISABLE_CACHE } from '../constants/constants'; -import { getRcGitHubInfo } from '../cards/CreateRc/helpers/getRcGitHubInfo'; +import { getRcGitHubInfo } from '../helpers/getRcGitHubInfo'; import { Project } from '../contexts/ProjectContext'; import { SemverTagParts } from '../helpers/tagParts/getSemverTagParts'; diff --git a/plugins/github-release-manager/src/cards/createRc/CreateRc.test.tsx b/plugins/github-release-manager/src/cards/createRc/CreateRc.test.tsx index f4aaaa29ee..461f04a65d 100644 --- a/plugins/github-release-manager/src/cards/createRc/CreateRc.test.tsx +++ b/plugins/github-release-manager/src/cards/createRc/CreateRc.test.tsx @@ -35,7 +35,7 @@ jest.mock('../../contexts/PluginApiClientContext', () => ({ jest.mock('../../contexts/ProjectContext', () => ({ useProjectContext: jest.fn(() => mockCalverProject), })); -jest.mock('./helpers/getRcGitHubInfo', () => ({ +jest.mock('../../helpers/getRcGitHubInfo', () => ({ getRcGitHubInfo: () => mockNextGitHubInfo, })); jest.mock('./hooks/useCreateRc', () => ({ diff --git a/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx b/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx index f2ca2d7e5f..63715d236d 100644 --- a/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx +++ b/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx @@ -32,7 +32,7 @@ import { } from '../../api/PluginApiClient'; import { ComponentConfigCreateRc } from '../../types/types'; import { Differ } from '../../components/Differ'; -import { getRcGitHubInfo } from './helpers/getRcGitHubInfo'; +import { getRcGitHubInfo } from '../../helpers/getRcGitHubInfo'; import { InfoCardPlus } from '../../components/InfoCardPlus'; import { ResponseStepDialog } from '../../components/ResponseStepDialog/ResponseStepDialog'; import { SEMVER_PARTS } from '../../constants/constants'; diff --git a/plugins/github-release-manager/src/cards/createRc/hooks/useCreateRc.ts b/plugins/github-release-manager/src/cards/createRc/hooks/useCreateRc.ts index 2cd18cb4ab..b0b0c98dca 100644 --- a/plugins/github-release-manager/src/cards/createRc/hooks/useCreateRc.ts +++ b/plugins/github-release-manager/src/cards/createRc/hooks/useCreateRc.ts @@ -23,7 +23,7 @@ import { IPluginApiClient, } from '../../../api/PluginApiClient'; import { CardHook, ComponentConfigCreateRc } from '../../../types/types'; -import { getRcGitHubInfo } from '../helpers/getRcGitHubInfo'; +import { getRcGitHubInfo } from '../../../helpers/getRcGitHubInfo'; import { GitHubReleaseManagerError } from '../../../errors/GitHubReleaseManagerError'; import { Project } from '../../../contexts/ProjectContext'; import { useResponseSteps } from '../../../hooks/useResponseSteps'; diff --git a/plugins/github-release-manager/src/cards/createRc/helpers/getRcGitHubInfo.test.ts b/plugins/github-release-manager/src/helpers/getRcGitHubInfo.test.ts similarity index 98% rename from plugins/github-release-manager/src/cards/createRc/helpers/getRcGitHubInfo.test.ts rename to plugins/github-release-manager/src/helpers/getRcGitHubInfo.test.ts index 1e41ea0d38..51ad5fc122 100644 --- a/plugins/github-release-manager/src/cards/createRc/helpers/getRcGitHubInfo.test.ts +++ b/plugins/github-release-manager/src/helpers/getRcGitHubInfo.test.ts @@ -21,7 +21,7 @@ import { mockReleaseVersionCalver, mockReleaseVersionSemver, mockSemverProject, -} from '../../../test-helpers/test-helpers'; +} from '../test-helpers/test-helpers'; import { getRcGitHubInfo } from './getRcGitHubInfo'; describe('getRcGitHubInfo', () => { diff --git a/plugins/github-release-manager/src/cards/createRc/helpers/getRcGitHubInfo.ts b/plugins/github-release-manager/src/helpers/getRcGitHubInfo.ts similarity index 82% rename from plugins/github-release-manager/src/cards/createRc/helpers/getRcGitHubInfo.ts rename to plugins/github-release-manager/src/helpers/getRcGitHubInfo.ts index 74407fb749..18043033c9 100644 --- a/plugins/github-release-manager/src/cards/createRc/helpers/getRcGitHubInfo.ts +++ b/plugins/github-release-manager/src/helpers/getRcGitHubInfo.ts @@ -16,11 +16,11 @@ import { DateTime } from 'luxon'; -import { getBumpedSemverTagParts } from '../../../helpers/getBumpedTag'; -import { GetLatestReleaseResult } from '../../../api/PluginApiClient'; -import { getSemverTagParts } from '../../../helpers/tagParts/getSemverTagParts'; -import { Project } from '../../../contexts/ProjectContext'; -import { SEMVER_PARTS } from '../../../constants/constants'; +import { getBumpedSemverTagParts } from './getBumpedTag'; +import { GetLatestReleaseResult } from '../api/PluginApiClient'; +import { getSemverTagParts } from './tagParts/getSemverTagParts'; +import { Project } from '../contexts/ProjectContext'; +import { SEMVER_PARTS } from '../constants/constants'; export const getRcGitHubInfo = ({ project, 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 e138512971..3daa449376 100644 --- a/plugins/github-release-manager/src/test-helpers/test-helpers.ts +++ b/plugins/github-release-manager/src/test-helpers/test-helpers.ts @@ -21,8 +21,8 @@ import { IPluginApiClient, } from '../api/PluginApiClient'; import { CalverTagParts } from '../helpers/tagParts/getCalverTagParts'; -import { getRcGitHubInfo } from '../cards/CreateRc/helpers/getRcGitHubInfo'; import { Project } from '../contexts/ProjectContext'; +import { getRcGitHubInfo } from '../helpers/getRcGitHubInfo'; const mockOwner = 'mock_owner'; const mockRepo = 'mock_repo'; From 4c1fdb36ca1bc7638205797c93c8e141b8d6a783 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Tue, 20 Apr 2021 22:01:14 +0200 Subject: [PATCH 048/140] Intermediary commit Signed-off-by: Erik Engervall --- plugins/github-release-manager/src/cards/Cards.tsx | 4 ++-- .../cards/{createRc => _CreateRc}/CreateRc.test.tsx | 0 .../src/cards/{createRc => _CreateRc}/CreateRc.tsx | 0 .../hooks/useCreateRc.test.tsx | 0 .../{createRc => _CreateRc}/hooks/useCreateRc.ts | 0 .../src/cards/{info => _Info}/Info.test.tsx | 0 .../src/cards/{info => _Info}/Info.tsx | 0 .../src/cards/{info => _Info}/flow.png | Bin 8 files changed, 2 insertions(+), 2 deletions(-) rename plugins/github-release-manager/src/cards/{createRc => _CreateRc}/CreateRc.test.tsx (100%) rename plugins/github-release-manager/src/cards/{createRc => _CreateRc}/CreateRc.tsx (100%) rename plugins/github-release-manager/src/cards/{createRc => _CreateRc}/hooks/useCreateRc.test.tsx (100%) rename plugins/github-release-manager/src/cards/{createRc => _CreateRc}/hooks/useCreateRc.ts (100%) rename plugins/github-release-manager/src/cards/{info => _Info}/Info.test.tsx (100%) rename plugins/github-release-manager/src/cards/{info => _Info}/Info.tsx (100%) rename plugins/github-release-manager/src/cards/{info => _Info}/flow.png (100%) diff --git a/plugins/github-release-manager/src/cards/Cards.tsx b/plugins/github-release-manager/src/cards/Cards.tsx index 5fbb7fb1f7..ffe86e1b5b 100644 --- a/plugins/github-release-manager/src/cards/Cards.tsx +++ b/plugins/github-release-manager/src/cards/Cards.tsx @@ -19,9 +19,9 @@ import { ErrorBoundary } from '@backstage/core'; import { Alert } from '@material-ui/lab'; import { CenteredCircularProgress } from '../components/CenteredCircularProgress'; -import { CreateRc } from './CreateRc/CreateRc'; +import { CreateRc } from './_CreateRc/CreateRc'; import { GitHubReleaseManagerProps } from '../GitHubReleaseManager'; -import { Info } from './Info/Info'; +import { Info } from './_Info/Info'; import { Patch } from './Patch/Patch'; import { PromoteReleaseCandidate } from './PromoteReleaseCandidate/PromoteRc'; import { RefetchContext } from '../contexts/RefetchContext'; diff --git a/plugins/github-release-manager/src/cards/createRc/CreateRc.test.tsx b/plugins/github-release-manager/src/cards/_CreateRc/CreateRc.test.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/createRc/CreateRc.test.tsx rename to plugins/github-release-manager/src/cards/_CreateRc/CreateRc.test.tsx diff --git a/plugins/github-release-manager/src/cards/createRc/CreateRc.tsx b/plugins/github-release-manager/src/cards/_CreateRc/CreateRc.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/createRc/CreateRc.tsx rename to plugins/github-release-manager/src/cards/_CreateRc/CreateRc.tsx diff --git a/plugins/github-release-manager/src/cards/createRc/hooks/useCreateRc.test.tsx b/plugins/github-release-manager/src/cards/_CreateRc/hooks/useCreateRc.test.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/createRc/hooks/useCreateRc.test.tsx rename to plugins/github-release-manager/src/cards/_CreateRc/hooks/useCreateRc.test.tsx diff --git a/plugins/github-release-manager/src/cards/createRc/hooks/useCreateRc.ts b/plugins/github-release-manager/src/cards/_CreateRc/hooks/useCreateRc.ts similarity index 100% rename from plugins/github-release-manager/src/cards/createRc/hooks/useCreateRc.ts rename to plugins/github-release-manager/src/cards/_CreateRc/hooks/useCreateRc.ts diff --git a/plugins/github-release-manager/src/cards/info/Info.test.tsx b/plugins/github-release-manager/src/cards/_Info/Info.test.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/info/Info.test.tsx rename to plugins/github-release-manager/src/cards/_Info/Info.test.tsx diff --git a/plugins/github-release-manager/src/cards/info/Info.tsx b/plugins/github-release-manager/src/cards/_Info/Info.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/info/Info.tsx rename to plugins/github-release-manager/src/cards/_Info/Info.tsx diff --git a/plugins/github-release-manager/src/cards/info/flow.png b/plugins/github-release-manager/src/cards/_Info/flow.png similarity index 100% rename from plugins/github-release-manager/src/cards/info/flow.png rename to plugins/github-release-manager/src/cards/_Info/flow.png From c6efd515aec86fb989211966eb1fc439898fddaa Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Tue, 20 Apr 2021 22:01:48 +0200 Subject: [PATCH 049/140] Folder rename fix Signed-off-by: Erik Engervall --- plugins/github-release-manager/src/cards/Cards.tsx | 4 ++-- .../cards/{_CreateRc => CreateRc}/CreateRc.test.tsx | 0 .../src/cards/{_CreateRc => CreateRc}/CreateRc.tsx | 0 .../hooks/useCreateRc.test.tsx | 0 .../{_CreateRc => CreateRc}/hooks/useCreateRc.ts | 0 .../src/cards/{_Info => Info}/Info.test.tsx | 0 .../src/cards/{_Info => Info}/Info.tsx | 0 .../src/cards/{_Info => Info}/flow.png | Bin 8 files changed, 2 insertions(+), 2 deletions(-) rename plugins/github-release-manager/src/cards/{_CreateRc => CreateRc}/CreateRc.test.tsx (100%) rename plugins/github-release-manager/src/cards/{_CreateRc => CreateRc}/CreateRc.tsx (100%) rename plugins/github-release-manager/src/cards/{_CreateRc => CreateRc}/hooks/useCreateRc.test.tsx (100%) rename plugins/github-release-manager/src/cards/{_CreateRc => CreateRc}/hooks/useCreateRc.ts (100%) rename plugins/github-release-manager/src/cards/{_Info => Info}/Info.test.tsx (100%) rename plugins/github-release-manager/src/cards/{_Info => Info}/Info.tsx (100%) rename plugins/github-release-manager/src/cards/{_Info => Info}/flow.png (100%) diff --git a/plugins/github-release-manager/src/cards/Cards.tsx b/plugins/github-release-manager/src/cards/Cards.tsx index ffe86e1b5b..5fbb7fb1f7 100644 --- a/plugins/github-release-manager/src/cards/Cards.tsx +++ b/plugins/github-release-manager/src/cards/Cards.tsx @@ -19,9 +19,9 @@ import { ErrorBoundary } from '@backstage/core'; import { Alert } from '@material-ui/lab'; import { CenteredCircularProgress } from '../components/CenteredCircularProgress'; -import { CreateRc } from './_CreateRc/CreateRc'; +import { CreateRc } from './CreateRc/CreateRc'; import { GitHubReleaseManagerProps } from '../GitHubReleaseManager'; -import { Info } from './_Info/Info'; +import { Info } from './Info/Info'; import { Patch } from './Patch/Patch'; import { PromoteReleaseCandidate } from './PromoteReleaseCandidate/PromoteRc'; import { RefetchContext } from '../contexts/RefetchContext'; diff --git a/plugins/github-release-manager/src/cards/_CreateRc/CreateRc.test.tsx b/plugins/github-release-manager/src/cards/CreateRc/CreateRc.test.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/_CreateRc/CreateRc.test.tsx rename to plugins/github-release-manager/src/cards/CreateRc/CreateRc.test.tsx diff --git a/plugins/github-release-manager/src/cards/_CreateRc/CreateRc.tsx b/plugins/github-release-manager/src/cards/CreateRc/CreateRc.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/_CreateRc/CreateRc.tsx rename to plugins/github-release-manager/src/cards/CreateRc/CreateRc.tsx diff --git a/plugins/github-release-manager/src/cards/_CreateRc/hooks/useCreateRc.test.tsx b/plugins/github-release-manager/src/cards/CreateRc/hooks/useCreateRc.test.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/_CreateRc/hooks/useCreateRc.test.tsx rename to plugins/github-release-manager/src/cards/CreateRc/hooks/useCreateRc.test.tsx diff --git a/plugins/github-release-manager/src/cards/_CreateRc/hooks/useCreateRc.ts b/plugins/github-release-manager/src/cards/CreateRc/hooks/useCreateRc.ts similarity index 100% rename from plugins/github-release-manager/src/cards/_CreateRc/hooks/useCreateRc.ts rename to plugins/github-release-manager/src/cards/CreateRc/hooks/useCreateRc.ts diff --git a/plugins/github-release-manager/src/cards/_Info/Info.test.tsx b/plugins/github-release-manager/src/cards/Info/Info.test.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/_Info/Info.test.tsx rename to plugins/github-release-manager/src/cards/Info/Info.test.tsx diff --git a/plugins/github-release-manager/src/cards/_Info/Info.tsx b/plugins/github-release-manager/src/cards/Info/Info.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/_Info/Info.tsx rename to plugins/github-release-manager/src/cards/Info/Info.tsx diff --git a/plugins/github-release-manager/src/cards/_Info/flow.png b/plugins/github-release-manager/src/cards/Info/flow.png similarity index 100% rename from plugins/github-release-manager/src/cards/_Info/flow.png rename to plugins/github-release-manager/src/cards/Info/flow.png From 88c9eac54dcc0447e969ed1d412d7088aee923b6 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Tue, 20 Apr 2021 22:04:02 +0200 Subject: [PATCH 050/140] PromoteRc component rename Signed-off-by: Erik Engervall --- .../src/cards/Cards.test.tsx | 53 +++++++++++++++++-- .../src/cards/Cards.tsx | 4 +- .../PromoteRc.test.tsx | 10 ++-- .../PromoteRc.tsx | 7 +-- .../PromoteRcBody.test.tsx | 0 .../PromoteRcBody.tsx | 0 .../hooks/usePromoteRc.test.ts | 0 .../hooks/usePromoteRc.ts | 0 8 files changed, 58 insertions(+), 16 deletions(-) rename plugins/github-release-manager/src/cards/{PromoteReleaseCandidate => PromoteRc}/PromoteRc.test.tsx (83%) rename plugins/github-release-manager/src/cards/{PromoteReleaseCandidate => PromoteRc}/PromoteRc.tsx (93%) rename plugins/github-release-manager/src/cards/{PromoteReleaseCandidate => PromoteRc}/PromoteRcBody.test.tsx (100%) rename plugins/github-release-manager/src/cards/{PromoteReleaseCandidate => PromoteRc}/PromoteRcBody.tsx (100%) rename plugins/github-release-manager/src/cards/{PromoteReleaseCandidate => PromoteRc}/hooks/usePromoteRc.test.ts (100%) rename plugins/github-release-manager/src/cards/{PromoteReleaseCandidate => PromoteRc}/hooks/usePromoteRc.ts (100%) diff --git a/plugins/github-release-manager/src/cards/Cards.test.tsx b/plugins/github-release-manager/src/cards/Cards.test.tsx index 2a67a9f62c..dfb5491842 100644 --- a/plugins/github-release-manager/src/cards/Cards.test.tsx +++ b/plugins/github-release-manager/src/cards/Cards.test.tsx @@ -46,8 +46,55 @@ describe('Cards', () => { await waitFor(() => getByTestId(TEST_IDS.info.info)); }); - expect(getByTestId(TEST_IDS.info.info).innerHTML).toMatchInlineSnapshot( - `"
Terminology

GitHub: The source control system where releases reside in a practical sense. Read more about GitHub releases. (Note that this plugin works just as well with GitHub Enterprise.)

Release Candidate: A GitHub prerelease intended primarily for internal testing

Release Version: A GitHub release intended for end users

"`, - ); + expect(getByTestId(TEST_IDS.info.info)).toMatchInlineSnapshot(` +
+
+ Terminology +
+

+ + GitHub + + : The source control system where releases reside in a practical sense. Read more about + + + GitHub releases + + . (Note that this plugin works just as well with GitHub Enterprise.) +

+

+ + Release Candidate + + : A GitHub + + prerelease + + + intended primarily for internal testing +

+

+ + Release Version + + : A GitHub release intended for end users +

+
+ `); }); }); diff --git a/plugins/github-release-manager/src/cards/Cards.tsx b/plugins/github-release-manager/src/cards/Cards.tsx index 5fbb7fb1f7..3463ffa2e3 100644 --- a/plugins/github-release-manager/src/cards/Cards.tsx +++ b/plugins/github-release-manager/src/cards/Cards.tsx @@ -23,7 +23,7 @@ import { CreateRc } from './CreateRc/CreateRc'; import { GitHubReleaseManagerProps } from '../GitHubReleaseManager'; import { Info } from './Info/Info'; import { Patch } from './Patch/Patch'; -import { PromoteReleaseCandidate } from './PromoteReleaseCandidate/PromoteRc'; +import { PromoteRc } from './PromoteRc/PromoteRc'; import { RefetchContext } from '../contexts/RefetchContext'; import { useGetGitHubBatchInfo } from '../hooks/useGetGitHubBatchInfo'; import { usePluginApiClientContext } from '../contexts/PluginApiClientContext'; @@ -111,7 +111,7 @@ export function Cards({ )} {!components?.promoteRc?.omit && ( - diff --git a/plugins/github-release-manager/src/cards/PromoteReleaseCandidate/PromoteRc.test.tsx b/plugins/github-release-manager/src/cards/PromoteRc/PromoteRc.test.tsx similarity index 83% rename from plugins/github-release-manager/src/cards/PromoteReleaseCandidate/PromoteRc.test.tsx rename to plugins/github-release-manager/src/cards/PromoteRc/PromoteRc.test.tsx index e917c295d6..f4e7c1fb41 100644 --- a/plugins/github-release-manager/src/cards/PromoteReleaseCandidate/PromoteRc.test.tsx +++ b/plugins/github-release-manager/src/cards/PromoteRc/PromoteRc.test.tsx @@ -29,13 +29,11 @@ jest.mock('./PromoteRcBody', () => ({ ), })); -import { PromoteReleaseCandidate } from './PromoteRc'; +import { PromoteRc } from './PromoteRc'; describe('PromoteRc', () => { it('return early if no latest release present', () => { - const { getByTestId } = render( - , - ); + const { getByTestId } = render(); expect( getByTestId(TEST_IDS.components.noLatestRelease), @@ -44,7 +42,7 @@ describe('PromoteRc', () => { it('should display not-rc warning', () => { const { getByTestId } = render( - , + , ); expect(getByTestId(TEST_IDS.promoteRc.notRcWarning)).toBeInTheDocument(); @@ -52,7 +50,7 @@ describe('PromoteRc', () => { it('should display PromoteRcBody', () => { const { getByTestId } = render( - , + , ); expect( diff --git a/plugins/github-release-manager/src/cards/PromoteReleaseCandidate/PromoteRc.tsx b/plugins/github-release-manager/src/cards/PromoteRc/PromoteRc.tsx similarity index 93% rename from plugins/github-release-manager/src/cards/PromoteReleaseCandidate/PromoteRc.tsx rename to plugins/github-release-manager/src/cards/PromoteRc/PromoteRc.tsx index 56b506bcab..5d2783bcfd 100644 --- a/plugins/github-release-manager/src/cards/PromoteReleaseCandidate/PromoteRc.tsx +++ b/plugins/github-release-manager/src/cards/PromoteRc/PromoteRc.tsx @@ -26,15 +26,12 @@ import { PromoteRcBody } from './PromoteRcBody'; import { TEST_IDS } from '../../test-helpers/test-ids'; import { useStyles } from '../../styles/styles'; -interface PromoteReleaseCandidateProps { +interface PromoteRcProps { latestRelease: GetLatestReleaseResult; successCb?: ComponentConfigPromoteRc['successCb']; } -export const PromoteReleaseCandidate = ({ - latestRelease, - successCb, -}: PromoteReleaseCandidateProps) => { +export const PromoteRc = ({ latestRelease, successCb }: PromoteRcProps) => { const classes = useStyles(); function Body() { diff --git a/plugins/github-release-manager/src/cards/PromoteReleaseCandidate/PromoteRcBody.test.tsx b/plugins/github-release-manager/src/cards/PromoteRc/PromoteRcBody.test.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/PromoteReleaseCandidate/PromoteRcBody.test.tsx rename to plugins/github-release-manager/src/cards/PromoteRc/PromoteRcBody.test.tsx diff --git a/plugins/github-release-manager/src/cards/PromoteReleaseCandidate/PromoteRcBody.tsx b/plugins/github-release-manager/src/cards/PromoteRc/PromoteRcBody.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/PromoteReleaseCandidate/PromoteRcBody.tsx rename to plugins/github-release-manager/src/cards/PromoteRc/PromoteRcBody.tsx diff --git a/plugins/github-release-manager/src/cards/PromoteReleaseCandidate/hooks/usePromoteRc.test.ts b/plugins/github-release-manager/src/cards/PromoteRc/hooks/usePromoteRc.test.ts similarity index 100% rename from plugins/github-release-manager/src/cards/PromoteReleaseCandidate/hooks/usePromoteRc.test.ts rename to plugins/github-release-manager/src/cards/PromoteRc/hooks/usePromoteRc.test.ts diff --git a/plugins/github-release-manager/src/cards/PromoteReleaseCandidate/hooks/usePromoteRc.ts b/plugins/github-release-manager/src/cards/PromoteRc/hooks/usePromoteRc.ts similarity index 100% rename from plugins/github-release-manager/src/cards/PromoteReleaseCandidate/hooks/usePromoteRc.ts rename to plugins/github-release-manager/src/cards/PromoteRc/hooks/usePromoteRc.ts From fefc545508579d81cbe52b09aad1705d2a9ab218 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Tue, 20 Apr 2021 22:16:47 +0200 Subject: [PATCH 051/140] Create tests for useGetGitHubBatchInfo Signed-off-by: Erik Engervall --- .../src/hooks/useGetGitHubBatchInfo.test.ts | 102 ++++++++++++++++++ .../src/hooks/useGetGitHubBatchInfo.ts | 2 +- plugins/github-release-manager/src/plugin.ts | 5 +- 3 files changed, 107 insertions(+), 2 deletions(-) create mode 100644 plugins/github-release-manager/src/hooks/useGetGitHubBatchInfo.test.ts diff --git a/plugins/github-release-manager/src/hooks/useGetGitHubBatchInfo.test.ts b/plugins/github-release-manager/src/hooks/useGetGitHubBatchInfo.test.ts new file mode 100644 index 0000000000..54ecfa866a --- /dev/null +++ b/plugins/github-release-manager/src/hooks/useGetGitHubBatchInfo.test.ts @@ -0,0 +1,102 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; +import { waitFor } from '@testing-library/react'; + +import { mockApiClient, mockSemverProject } from '../test-helpers/test-helpers'; +import { useGetGitHubBatchInfo } from './useGetGitHubBatchInfo'; + +describe('useGetGitHubBatchInfo', () => { + it('should handle repositories with releases', async () => { + const { result } = renderHook(() => + useGetGitHubBatchInfo({ + pluginApiClient: mockApiClient, + project: mockSemverProject, + refetchTrigger: 1337, + }), + ); + + await act(async () => { + await waitFor(() => result.current.gitHubBatchInfo !== undefined); + }); + + expect(result.current.gitHubBatchInfo).toMatchInlineSnapshot(` + Object { + "loading": false, + "value": Object { + "latestRelease": Object { + "htmlUrl": "mock_release_html_url", + "id": 1, + "prerelease": false, + "tagName": "rc-2020.01.01_1", + "targetCommitish": "rc/1.2.3", + }, + "releaseBranch": 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", + }, + "repository": Object { + "defaultBranch": "mock_defaultBranch", + "name": "mock_repo", + "pushPermissions": true, + }, + }, + } + `); + }); + + it('should handle repositories without any releases', async () => { + (mockApiClient.getLatestRelease as jest.Mock).mockResolvedValueOnce(null); + + const { result } = renderHook(() => + useGetGitHubBatchInfo({ + pluginApiClient: mockApiClient, + project: mockSemverProject, + refetchTrigger: 1337, + }), + ); + + await act(async () => { + await waitFor(() => result.current.gitHubBatchInfo !== undefined); + }); + + expect(result.current.gitHubBatchInfo).toMatchInlineSnapshot(` + Object { + "loading": false, + "value": Object { + "latestRelease": null, + "releaseBranch": null, + "repository": Object { + "defaultBranch": "mock_defaultBranch", + "name": "mock_repo", + "pushPermissions": true, + }, + }, + } + `); + }); +}); diff --git a/plugins/github-release-manager/src/hooks/useGetGitHubBatchInfo.ts b/plugins/github-release-manager/src/hooks/useGetGitHubBatchInfo.ts index e09d165871..5cf5115116 100644 --- a/plugins/github-release-manager/src/hooks/useGetGitHubBatchInfo.ts +++ b/plugins/github-release-manager/src/hooks/useGetGitHubBatchInfo.ts @@ -36,7 +36,7 @@ export const useGetGitHubBatchInfo = ({ pluginApiClient.getLatestRelease({ ...project }), ]); - if (!latestRelease) { + if (latestRelease === null) { return { latestRelease, releaseBranch: null, diff --git a/plugins/github-release-manager/src/plugin.ts b/plugins/github-release-manager/src/plugin.ts index c0c6f4fda9..9c01c1e959 100644 --- a/plugins/github-release-manager/src/plugin.ts +++ b/plugins/github-release-manager/src/plugin.ts @@ -41,7 +41,10 @@ export const gitHubReleaseManagerPlugin = createPlugin({ githubAuthApi: githubAuthApiRef, }, factory: ({ configApi, githubAuthApi }) => { - return new PluginApiClient({ configApi, githubAuthApi }); + return new PluginApiClient({ + configApi, + githubAuthApi, + }); }, }), ], From 8e0d7cd52ea28ff93e231a49e7efd921dc9ad832 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Tue, 20 Apr 2021 22:36:13 +0200 Subject: [PATCH 052/140] Add tests for useResponseSteps Signed-off-by: Erik Engervall --- .../src/hooks/useResponseSteps.test.ts | 143 ++++++++++++++++++ .../src/hooks/useResponseSteps.ts | 16 +- 2 files changed, 151 insertions(+), 8 deletions(-) create mode 100644 plugins/github-release-manager/src/hooks/useResponseSteps.test.ts diff --git a/plugins/github-release-manager/src/hooks/useResponseSteps.test.ts b/plugins/github-release-manager/src/hooks/useResponseSteps.test.ts new file mode 100644 index 0000000000..4460487f06 --- /dev/null +++ b/plugins/github-release-manager/src/hooks/useResponseSteps.test.ts @@ -0,0 +1,143 @@ +/* + * 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 { renderHook, act } from '@testing-library/react-hooks'; + +import { useResponseSteps } from './useResponseSteps'; + +describe('useResponseSteps', () => { + it('should export expected variables', () => { + const { result } = renderHook(() => useResponseSteps()); + + expect(result.current).toMatchInlineSnapshot(` + Object { + "abortIfError": [Function], + "addStepToResponseSteps": [Function], + "asyncCatcher": [Function], + "responseSteps": Array [], + } + `); + }); + + describe('addStepToResponseSteps', () => { + it('should add responseSteps to state', async () => { + const { result } = renderHook(() => useResponseSteps()); + + expect(result.current.responseSteps).toMatchInlineSnapshot(`Array []`); + + act(() => { + result.current.addStepToResponseSteps({ + message: 'totally added a messaage ✌🏼', + }); + }); + + expect(result.current.responseSteps).toMatchInlineSnapshot(` + Array [ + Object { + "message": "totally added a messaage ✌🏼", + }, + ] + `); + }); + }); + + describe('asyncCatcher', () => { + it('should catch Errors and add as failure step, then throw', async () => { + const { result } = renderHook(() => useResponseSteps()); + + expect(result.current.responseSteps).toMatchInlineSnapshot(`Array []`); + + await act(async () => { + await new Promise((_, reject) => reject(new Error(':('))) + .catch(result.current.asyncCatcher) + .catch( + () => void 0, // swallow + ); + }); + + expect(result.current.responseSteps).toMatchInlineSnapshot(` + Array [ + Object { + "icon": "failure", + "message": "Something went wrong 🔥", + "secondaryMessage": "Error message: :(", + }, + ] + `); + }); + + it('should catch unknown Errors and add as failure step, then throw', async () => { + const { result } = renderHook(() => useResponseSteps()); + + expect(result.current.responseSteps).toMatchInlineSnapshot(`Array []`); + + await act(async () => { + await new Promise((_, reject) => reject()) + .catch(result.current.asyncCatcher) + .catch( + () => void 0, // swallow + ); + }); + + expect(result.current.responseSteps).toMatchInlineSnapshot(` + Array [ + Object { + "icon": "failure", + "message": "Something went wrong 🔥", + "secondaryMessage": "Error message: unknown", + }, + ] + `); + }); + }); + + describe('abortIfError', () => { + it('should throw if Error and add a failure step', async () => { + const { result } = renderHook(() => useResponseSteps()); + + expect(result.current.responseSteps).toMatchInlineSnapshot(`Array []`); + + act(() => { + try { + result.current.abortIfError(new Error('Das kaboom')); + } catch (error) { + // + } + }); + + expect(result.current.responseSteps).toMatchInlineSnapshot(` + Array [ + Object { + "icon": "failure", + "message": "Skipped due to error in previous step", + }, + ] + `); + }); + + it('should do nothing if not Error', async () => { + const { result } = renderHook(() => useResponseSteps()); + + expect(result.current.responseSteps).toMatchInlineSnapshot(`Array []`); + + act(() => { + result.current.abortIfError(undefined); + }); + + expect(result.current.responseSteps).toMatchInlineSnapshot(`Array []`); + }); + }); +}); diff --git a/plugins/github-release-manager/src/hooks/useResponseSteps.ts b/plugins/github-release-manager/src/hooks/useResponseSteps.ts index 298d8dc11f..e20587d078 100644 --- a/plugins/github-release-manager/src/hooks/useResponseSteps.ts +++ b/plugins/github-release-manager/src/hooks/useResponseSteps.ts @@ -30,14 +30,7 @@ export function useResponseSteps() { setResponseSteps([...responseSteps, responseStep]); }; - const abortIfError = (error?: Error) => { - if (error) { - addStepToResponseSteps(RESPONSE_STEP_FAILURE_ABORT); - throw error; - } - }; - - const asyncCatcher = (error?: Error): never => { + const asyncCatcher = (error: Error): never => { const responseStepError: ResponseStep = { message: 'Something went wrong 🔥', secondaryMessage: `Error message: ${ @@ -50,6 +43,13 @@ export function useResponseSteps() { throw error; }; + const abortIfError = (error?: Error) => { + if (error) { + addStepToResponseSteps(RESPONSE_STEP_FAILURE_ABORT); + throw error; + } + }; + return { responseSteps, addStepToResponseSteps, From 59b42ece0f37e6ff412d8ff5118111758879895a Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Wed, 21 Apr 2021 11:08:43 +0200 Subject: [PATCH 053/140] Revert accidental app-config.yaml push Signed-off-by: Erik Engervall --- app-config.yaml | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 2af191c56a..ac59874878 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -125,12 +125,12 @@ integrations: - host: github.com token: ${GITHUB_TOKEN} ### Example for how to add your GitHub Enterprise instance using the API: - - host: ghe.spotify.net - apiBaseUrl: https://ghe.spotify.net/api/v3 - token: ${GHE_TOKEN} + # - host: ghe.example.net + # apiBaseUrl: https://ghe.example.net/api/v3 + # token: ${GHE_TOKEN} ### Example for how to add your GitHub Enterprise instance using raw HTTP fetches (token is optional): - # - host: ghe.spotify.net - # rawBaseUrl: https://ghe.spotify.net/raw + # - host: ghe.example.net + # rawBaseUrl: https://ghe.example.net/raw # token: ${GHE_TOKEN} gitlab: - host: gitlab.com @@ -165,8 +165,8 @@ catalog: - target: https://github.com token: ${GITHUB_TOKEN} #### Example for how to add your GitHub Enterprise instance using the API: - # - target: https://ghe.spotify.net - # apiBaseUrl: https://ghe.spotify.net/api + # - target: https://ghe.example.net + # apiBaseUrl: https://ghe.example.net/api # token: ${GHE_TOKEN} ldapOrg: ### Example for how to add your enterprise LDAP server From 881e22c2cd2a73f02eb8611c1c2596e7cf82f4f3 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Wed, 21 Apr 2021 17:33:49 +0200 Subject: [PATCH 054/140] Add tests for test-ids & createResponseStepError Signed-off-by: Erik Engervall --- .../helpers/createResponseStepError.test.ts | 31 +++++++ .../src/test-helpers/test-helpers.test.ts | 2 +- .../src/test-helpers/test-ids.test.ts | 88 +++++++++++++++++++ 3 files changed, 120 insertions(+), 1 deletion(-) create mode 100644 plugins/github-release-manager/src/helpers/createResponseStepError.test.ts create mode 100644 plugins/github-release-manager/src/test-helpers/test-ids.test.ts diff --git a/plugins/github-release-manager/src/helpers/createResponseStepError.test.ts b/plugins/github-release-manager/src/helpers/createResponseStepError.test.ts new file mode 100644 index 0000000000..b6069f2fd6 --- /dev/null +++ b/plugins/github-release-manager/src/helpers/createResponseStepError.test.ts @@ -0,0 +1,31 @@ +/* + * 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 { createResponseStepError } from './createResponseStepError'; + +describe('createResponseStepError', () => { + it('should work', () => { + const result = createResponseStepError(new Error('banana')); + + expect(result).toMatchInlineSnapshot(` + Object { + "icon": "failure", + "message": "Something went wrong ❌", + "secondaryMessage": "Error message: banana", + } + `); + }); +}); 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 fb2735182e..e6765a05ed 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 @@ -17,7 +17,7 @@ import * as testHelpers from './test-helpers'; describe('testHelpers', () => { - it('should match snapshot', () => { + it('should export the correct things', () => { expect(testHelpers).toMatchInlineSnapshot(` Object { "mockApiClient": Object { diff --git a/plugins/github-release-manager/src/test-helpers/test-ids.test.ts b/plugins/github-release-manager/src/test-helpers/test-ids.test.ts new file mode 100644 index 0000000000..2c1195b52e --- /dev/null +++ b/plugins/github-release-manager/src/test-helpers/test-ids.test.ts @@ -0,0 +1,88 @@ +/* + * 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", + "noLatestRelease": "grm--no-latest-release", + "noReleaseBranch": "grm--no-release-branch", + "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", + "infoCardPlus": "grm--info-card-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", + }, + }, + } + `); + }); +}); From 99cabc5865e7396322b2c8507c2d58590a09ac4a Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Wed, 21 Apr 2021 17:54:13 +0200 Subject: [PATCH 055/140] Improve test coverage Signed-off-by: Erik Engervall --- .../src/cards/CreateRc/CreateRc.test.tsx | 4 +- .../cards/CreateRc/hooks/useCreateRc.test.tsx | 10 +-- .../src/cards/Patch/hooks/usePatch.test.ts | 4 +- .../tagParts/getCalverTagParts.test.ts | 71 +++++++++++++++++++ .../tagParts/getSemverTagParts.test.ts | 63 ++++++++++++++++ .../src/hooks/useGetGitHubBatchInfo.test.ts | 2 +- .../src/test-helpers/test-helpers.test.ts | 16 ++++- .../src/test-helpers/test-helpers.ts | 62 ++++++++++++---- 8 files changed, 205 insertions(+), 27 deletions(-) create mode 100644 plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.test.ts create mode 100644 plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.test.ts diff --git a/plugins/github-release-manager/src/cards/CreateRc/CreateRc.test.tsx b/plugins/github-release-manager/src/cards/CreateRc/CreateRc.test.tsx index 461f04a65d..084754ce06 100644 --- a/plugins/github-release-manager/src/cards/CreateRc/CreateRc.test.tsx +++ b/plugins/github-release-manager/src/cards/CreateRc/CreateRc.test.tsx @@ -20,7 +20,7 @@ import { render } from '@testing-library/react'; import { mockApiClient, mockCalverProject, - mockNextGitHubInfo, + mockNextGitHubInfoSemver, mockReleaseBranch, mockReleaseCandidateCalver, mockReleaseVersionCalver, @@ -36,7 +36,7 @@ jest.mock('../../contexts/ProjectContext', () => ({ useProjectContext: jest.fn(() => mockCalverProject), })); jest.mock('../../helpers/getRcGitHubInfo', () => ({ - getRcGitHubInfo: () => mockNextGitHubInfo, + getRcGitHubInfo: () => mockNextGitHubInfoSemver, })); jest.mock('./hooks/useCreateRc', () => ({ useCreateRc: () => diff --git a/plugins/github-release-manager/src/cards/CreateRc/hooks/useCreateRc.test.tsx b/plugins/github-release-manager/src/cards/CreateRc/hooks/useCreateRc.test.tsx index e4549b5cb8..8da92a4f0e 100644 --- a/plugins/github-release-manager/src/cards/CreateRc/hooks/useCreateRc.test.tsx +++ b/plugins/github-release-manager/src/cards/CreateRc/hooks/useCreateRc.test.tsx @@ -21,7 +21,7 @@ import { mockApiClient, mockCalverProject, mockDefaultBranch, - mockNextGitHubInfo, + mockNextGitHubInfoCalver, mockReleaseVersionCalver, } from '../../../test-helpers/test-helpers'; import { useCreateRc } from './useCreateRc'; @@ -34,7 +34,7 @@ describe('useCreateRc', () => { useCreateRc({ defaultBranch: mockDefaultBranch, latestRelease: mockReleaseVersionCalver, - nextGitHubInfo: mockNextGitHubInfo, + nextGitHubInfo: mockNextGitHubInfoCalver, pluginApiClient: mockApiClient, project: mockCalverProject, }), @@ -53,7 +53,7 @@ describe('useCreateRc', () => { useCreateRc({ defaultBranch: mockDefaultBranch, latestRelease: mockReleaseVersionCalver, - nextGitHubInfo: mockNextGitHubInfo, + nextGitHubInfo: mockNextGitHubInfoCalver, pluginApiClient: mockApiClient, project: mockCalverProject, successCb: jest.fn(), @@ -81,12 +81,12 @@ describe('useCreateRc', () => { Object { "link": "mock_compareCommits_html_url", "message": "Fetched commit comparison", - "secondaryMessage": "rc/1.2.3...rc/1.2.3", + "secondaryMessage": "rc/2020.01.01_1...rc/2020.01.01_1", }, Object { "link": "mock_createRelease_html_url", "message": "Created Release Candidate \\"mock_createRelease_name\\"", - "secondaryMessage": "with tag \\"rc-1.2.3\\"", + "secondaryMessage": "with tag \\"rc-2020.01.01_1\\"", }, Object { "icon": "success", diff --git a/plugins/github-release-manager/src/cards/Patch/hooks/usePatch.test.ts b/plugins/github-release-manager/src/cards/Patch/hooks/usePatch.test.ts index a7b6b2b58d..240d39292c 100644 --- a/plugins/github-release-manager/src/cards/Patch/hooks/usePatch.test.ts +++ b/plugins/github-release-manager/src/cards/Patch/hooks/usePatch.test.ts @@ -80,11 +80,11 @@ describe('patch', () => { "secondaryMessage": "with message \\"mock_commit_message\\"", }, Object { - "message": "Forced branch \\"rc/1.2.3\\" to temporary commit \\"mock_commit_sha\\"", + "message": "Forced branch \\"rc/2020.01.01_1\\" to temporary commit \\"mock_commit_sha\\"", }, Object { "link": "mock_merge_html_url", - "message": "Merged temporary commit into \\"rc/1.2.3\\"", + "message": "Merged temporary commit into \\"rc/2020.01.01_1\\"", "secondaryMessage": "with message \\"mock_merge_commit_message\\"", }, Object { diff --git a/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.test.ts b/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.test.ts new file mode 100644 index 0000000000..238abe720a --- /dev/null +++ b/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.test.ts @@ -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 { + mockReleaseVersionCalver, + mockReleaseCandidateCalver, +} from '../../test-helpers/test-helpers'; +import { getCalverTagParts } from './getCalverTagParts'; + +describe('getCalverTagParts', () => { + it('should return tagParts for RC tag', () => { + const result = getCalverTagParts(mockReleaseCandidateCalver.tagName); + + expect(result).toMatchInlineSnapshot(` + 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 { + "calver": "2020.01.01", + "patch": 1, + "prefix": "version", + } + `); + }); + + it('should return null for invalid prefix', () => { + expect(() => + getCalverTagParts('invalid-2020.01.01_1'), + ).toThrowErrorMatchingInlineSnapshot(`"Invalid calver tag"`); + }); + + it('should return null for invalid calver (missing padded zero)', () => { + expect(() => + getCalverTagParts('rc-2020.1.01_1'), + ).toThrowErrorMatchingInlineSnapshot(`"Invalid calver tag"`); + }); + + it('should return null for invalid calver (missing day)', () => { + expect(() => + getCalverTagParts('rc-2020.01_1'), + ).toThrowErrorMatchingInlineSnapshot(`"Invalid calver tag"`); + }); + + it('should return null for invalid patch (letter instead of number)', () => { + expect(() => + getCalverTagParts('rc-2020.01.01_a'), + ).toThrowErrorMatchingInlineSnapshot(`"Invalid calver tag"`); + }); +}); diff --git a/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.test.ts b/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.test.ts new file mode 100644 index 0000000000..9977ab7add --- /dev/null +++ b/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.test.ts @@ -0,0 +1,63 @@ +/* + * 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 { + mockReleaseCandidateSemver, + mockReleaseVersionSemver, +} from '../../test-helpers/test-helpers'; +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", + } + `)); + + it('should return tagParts for Version tag', () => + expect(getSemverTagParts(mockReleaseVersionSemver.tagName)) + .toMatchInlineSnapshot(` + Object { + "major": 1, + "minor": 2, + "patch": 3, + "prefix": "version", + } + `)); + + it('should throw for invalid prefix', () => { + expect(() => + getSemverTagParts('invalid-1.2.3'), + ).toThrowErrorMatchingInlineSnapshot(`"Invalid semver tag"`); + }); + + it('should throw for invalid semver (missing patch)', () => { + expect(() => + getSemverTagParts('rc-1.2'), + ).toThrowErrorMatchingInlineSnapshot(`"Invalid semver tag"`); + }); + + it('should throw for invalid semver (founds calver)', () => { + expect(() => + getSemverTagParts('rc-1337.01.01_1'), + ).toThrowErrorMatchingInlineSnapshot(`"Invalid semver tag, found calver"`); + }); +}); diff --git a/plugins/github-release-manager/src/hooks/useGetGitHubBatchInfo.test.ts b/plugins/github-release-manager/src/hooks/useGetGitHubBatchInfo.test.ts index 54ecfa866a..7d45fd111a 100644 --- a/plugins/github-release-manager/src/hooks/useGetGitHubBatchInfo.test.ts +++ b/plugins/github-release-manager/src/hooks/useGetGitHubBatchInfo.test.ts @@ -43,7 +43,7 @@ describe('useGetGitHubBatchInfo', () => { "id": 1, "prerelease": false, "tagName": "rc-2020.01.01_1", - "targetCommitish": "rc/1.2.3", + "targetCommitish": "rc/2020.01.01_1", }, "releaseBranch": Object { "commit": Object { 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 e6765a05ed..8dfa3e6fd5 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 @@ -58,7 +58,12 @@ describe('testHelpers', () => { "versioningStrategy": "calver", }, "mockDefaultBranch": "mock_defaultBranch", - "mockNextGitHubInfo": Object { + "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", @@ -82,6 +87,13 @@ describe('testHelpers', () => { "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 { @@ -89,7 +101,7 @@ describe('testHelpers', () => { "id": 1, "prerelease": false, "tagName": "version-2020.01.01_1", - "targetCommitish": "rc/1.2.3", + "targetCommitish": "rc/2020.01.01_1", }, "mockReleaseVersionSemver": Object { "htmlUrl": "mock_release_html_url", 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 3daa449376..735bcd7d48 100644 --- a/plugins/github-release-manager/src/test-helpers/test-helpers.ts +++ b/plugins/github-release-manager/src/test-helpers/test-helpers.ts @@ -27,6 +27,18 @@ import { getRcGitHubInfo } from '../helpers/getRcGitHubInfo'; const mockOwner = 'mock_owner'; const mockRepo = 'mock_repo'; +const A_CALVER_VERSION = '2020.01.01_1'; +const MOCK_RELEASE_NAME_CALVER = `Version ${A_CALVER_VERSION}`; +const MOCK_RELEASE_BRANCH_NAME_CALVER = `rc/${A_CALVER_VERSION}`; +const MOCK_RELEASE_CANDIDATE_TAG_NAME_CALVER = `rc-${A_CALVER_VERSION}`; +const MOCK_RELEASE_VERSION_TAG_NAME_CALVER = `version-${A_CALVER_VERSION}`; + +const A_SEMVER_VERSION = '1.2.3'; +const MOCK_RELEASE_NAME_SEMVER = `Version ${A_SEMVER_VERSION}`; +const MOCK_RELEASE_BRANCH_NAME_SEMVER = `rc/${A_SEMVER_VERSION}`; +const MOCK_RELEASE_CANDIDATE_TAG_NAME_SEMVER = `rc-${A_SEMVER_VERSION}`; +const MOCK_RELEASE_VERSION_TAG_NAME_SEMVER = `version-${A_SEMVER_VERSION}`; + export const mockSemverProject: Project = { owner: mockOwner, repo: mockRepo, @@ -47,10 +59,16 @@ export const mockSearchSemver = `?versioningStrategy=${mockSemverProject.version export const mockDefaultBranch = 'mock_defaultBranch'; -export const mockNextGitHubInfo: ReturnType = { - rcBranch: 'rc/1.2.3', - rcReleaseTag: 'rc-1.2.3', - releaseName: 'Version 1.2.3', +export const mockNextGitHubInfoSemver: ReturnType = { + rcBranch: MOCK_RELEASE_BRANCH_NAME_SEMVER, + rcReleaseTag: MOCK_RELEASE_CANDIDATE_TAG_NAME_SEMVER, + releaseName: MOCK_RELEASE_NAME_SEMVER, +}; + +export const mockNextGitHubInfoCalver: ReturnType = { + rcBranch: MOCK_RELEASE_BRANCH_NAME_CALVER, + rcReleaseTag: MOCK_RELEASE_CANDIDATE_TAG_NAME_CALVER, + releaseName: MOCK_RELEASE_NAME_CALVER, }; export const mockTagParts = { @@ -74,24 +92,32 @@ const createMockRelease = ({ id: 1, htmlUrl: 'mock_release_html_url', prerelease, - tagName: 'rc-2020.01.01_1', - targetCommitish: 'rc/1.2.3', + tagName: MOCK_RELEASE_CANDIDATE_TAG_NAME_CALVER, + targetCommitish: MOCK_RELEASE_BRANCH_NAME_CALVER, ...rest, }); + export const mockReleaseCandidateCalver = createMockRelease({ prerelease: true, - tagName: 'rc-2020.01.01_1', - targetCommitish: 'rc/1.2.3', + tagName: MOCK_RELEASE_CANDIDATE_TAG_NAME_CALVER, + targetCommitish: MOCK_RELEASE_BRANCH_NAME_CALVER, }); + export const mockReleaseVersionCalver = createMockRelease({ prerelease: false, - tagName: 'version-2020.01.01_1', - targetCommitish: 'rc/1.2.3', + tagName: MOCK_RELEASE_VERSION_TAG_NAME_CALVER, + targetCommitish: MOCK_RELEASE_BRANCH_NAME_CALVER, +}); + +export const mockReleaseCandidateSemver = createMockRelease({ + prerelease: true, + tagName: MOCK_RELEASE_CANDIDATE_TAG_NAME_SEMVER, + targetCommitish: MOCK_RELEASE_BRANCH_NAME_SEMVER, }); export const mockReleaseVersionSemver = createMockRelease({ prerelease: false, - tagName: 'version-1.2.3', - targetCommitish: 'rc/1.2.3', + tagName: MOCK_RELEASE_VERSION_TAG_NAME_SEMVER, + targetCommitish: MOCK_RELEASE_BRANCH_NAME_SEMVER, }); /** @@ -100,12 +126,18 @@ export const mockReleaseVersionSemver = createMockRelease({ const createMockBranch = ({ ...rest }: Partial = {}): GetBranchResult => ({ - name: 'rc/1.2.3', + name: MOCK_RELEASE_BRANCH_NAME_SEMVER, commit: { sha: 'mock_branch_commit_sha', - commit: { tree: { sha: 'mock_branch_commit_commit_tree_sha' } }, + commit: { + tree: { + sha: 'mock_branch_commit_commit_tree_sha', + }, + }, + }, + links: { + html: 'mock_branch_links_html', }, - links: { html: 'mock_branch_links_html' }, ...rest, }); export const mockReleaseBranch = createMockBranch(); From 278dcdd92bf8c3c022e45d8ed589563fb21d5597 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Wed, 21 Apr 2021 18:00:15 +0200 Subject: [PATCH 056/140] Simplify getTagParts.test.ts now that there's module specific tests in place Signed-off-by: Erik Engervall --- .../src/helpers/tagParts/getTagParts.test.ts | 106 +++--------------- 1 file changed, 18 insertions(+), 88 deletions(-) diff --git a/plugins/github-release-manager/src/helpers/tagParts/getTagParts.test.ts b/plugins/github-release-manager/src/helpers/tagParts/getTagParts.test.ts index 91a753dbe3..fc1ab1479b 100644 --- a/plugins/github-release-manager/src/helpers/tagParts/getTagParts.test.ts +++ b/plugins/github-release-manager/src/helpers/tagParts/getTagParts.test.ts @@ -18,104 +18,34 @@ import { mockCalverProject, mockSemverProject, } from '../../test-helpers/test-helpers'; + +jest.mock('./getCalverTagParts', () => ({ + getCalverTagParts: jest.fn(), +})); +jest.mock('./getSemverTagParts', () => ({ + getSemverTagParts: jest.fn(), +})); + +import { getCalverTagParts } from './getCalverTagParts'; +import { getSemverTagParts } from './getSemverTagParts'; import { getTagParts } from './getTagParts'; describe('getTagParts', () => { + beforeEach(jest.resetAllMocks); + describe('calver', () => { - it('should return tagParts for RC tag', () => - expect( - getTagParts({ project: mockCalverProject, tag: 'rc-2020.01.01_1' }), - ).toMatchInlineSnapshot(` - Object { - "calver": "2020.01.01", - "patch": 1, - "prefix": "rc", - } - `)); + it('should call getCalverTagParts for calver projects', () => { + getTagParts({ project: mockCalverProject, tag: 'banana' }); - it('should return tagParts for Version tag', () => - expect( - getTagParts({ - project: mockCalverProject, - tag: 'version-2020.01.01_1', - }), - ).toMatchInlineSnapshot(` - Object { - "calver": "2020.01.01", - "patch": 1, - "prefix": "version", - } - `)); - - it('should return null for invalid prefix', () => { - expect(() => - getTagParts({ - project: mockCalverProject, - tag: 'invalid-2020.01.01_1', - }), - ).toThrowErrorMatchingInlineSnapshot(`"Invalid calver tag"`); - }); - - it('should return null for invalid calver (missing padded zero)', () => { - expect(() => - getTagParts({ project: mockCalverProject, tag: 'rc-2020.1.01_1' }), - ).toThrowErrorMatchingInlineSnapshot(`"Invalid calver tag"`); - }); - - it('should return null for invalid calver (missing day)', () => { - expect(() => - getTagParts({ project: mockCalverProject, tag: 'rc-2020.01_1' }), - ).toThrowErrorMatchingInlineSnapshot(`"Invalid calver tag"`); - }); - - it('should return null for invalid patch (letter instead of number)', () => { - expect(() => - getTagParts({ project: mockCalverProject, tag: 'rc-2020.01.01_a' }), - ).toThrowErrorMatchingInlineSnapshot(`"Invalid calver tag"`); + expect(getCalverTagParts).toHaveBeenCalledTimes(1); }); }); describe('semver', () => { - it('should return tagParts for RC tag', () => - expect(getTagParts({ project: mockSemverProject, tag: 'rc-1.2.3' })) - .toMatchInlineSnapshot(` - Object { - "major": 1, - "minor": 2, - "patch": 3, - "prefix": "rc", - } - `)); + it('should call getSemverTagParts for calver projects', () => { + getTagParts({ project: mockSemverProject, tag: 'banana' }); - it('should return tagParts for Version tag', () => - expect(getTagParts({ project: mockSemverProject, tag: 'version-1.2.3' })) - .toMatchInlineSnapshot(` - Object { - "major": 1, - "minor": 2, - "patch": 3, - "prefix": "version", - } - `)); - - it('should throw for invalid prefix', () => { - expect(() => - getTagParts({ project: mockSemverProject, tag: 'invalid-1.2.3' }), - ).toThrowErrorMatchingInlineSnapshot(`"Invalid semver tag"`); - }); - - it('should throw for invalid semver (missing patch)', () => { - expect(() => - getTagParts({ project: mockSemverProject, tag: 'rc-1.2' }), - ).toThrowErrorMatchingInlineSnapshot(`"Invalid semver tag"`); - }); - - it('should throw for invalid semver (founds calver)', () => { - expect(() => - getTagParts({ project: mockSemverProject, tag: 'rc-1337.01.01_1' }), - ).toThrowErrorMatchingInlineSnapshot( - `"Invalid semver tag, found calver"`, - ); + expect(getSemverTagParts).toHaveBeenCalledTimes(1); }); }); }); From 06c408cffd10dbccfe0b18e721f12d3442abf64e Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Wed, 21 Apr 2021 18:11:11 +0200 Subject: [PATCH 057/140] Remove unused component NoReleaseBranch & improve tests for NoLatestRelease Signed-off-by: Erik Engervall --- .../src/cards/Cards.tsx | 8 ++++- .../src/components/NoLatestRelease.test.tsx | 35 ++++++++++++++++--- .../src/components/NoReleaseBranch.test.tsx | 31 ---------------- .../src/components/NoReleaseBranch.tsx | 35 ------------------- .../src/test-helpers/test-ids.test.ts | 1 - .../src/test-helpers/test-ids.ts | 1 - 6 files changed, 37 insertions(+), 74 deletions(-) delete mode 100644 plugins/github-release-manager/src/components/NoReleaseBranch.test.tsx delete mode 100644 plugins/github-release-manager/src/components/NoReleaseBranch.tsx diff --git a/plugins/github-release-manager/src/cards/Cards.tsx b/plugins/github-release-manager/src/cards/Cards.tsx index 3463ffa2e3..22d7a8e884 100644 --- a/plugins/github-release-manager/src/cards/Cards.tsx +++ b/plugins/github-release-manager/src/cards/Cards.tsx @@ -90,7 +90,13 @@ export function Cards({ {!gitHubBatchInfo.value.latestRelease && ( - This repository has not releases yet + This repository doesn't have any releases yet + + )} + + {!gitHubBatchInfo.value.releaseBranch && ( + + This repository doesn't have any release branches )} diff --git a/plugins/github-release-manager/src/components/NoLatestRelease.test.tsx b/plugins/github-release-manager/src/components/NoLatestRelease.test.tsx index 779412ec25..1c2ea3ffa3 100644 --- a/plugins/github-release-manager/src/components/NoLatestRelease.test.tsx +++ b/plugins/github-release-manager/src/components/NoLatestRelease.test.tsx @@ -17,15 +17,40 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { TEST_IDS } from '../test-helpers/test-ids'; import { NoLatestRelease } from './NoLatestRelease'; describe('NoLatestRelease', () => { it('render NoLatestRelease', () => { - const { getByTestId } = render(); + const { container } = render(); - expect( - getByTestId(TEST_IDS.components.noLatestRelease), - ).toBeInTheDocument(); + expect(container).toMatchInlineSnapshot(` +
+ +
+ `); }); }); diff --git a/plugins/github-release-manager/src/components/NoReleaseBranch.test.tsx b/plugins/github-release-manager/src/components/NoReleaseBranch.test.tsx deleted file mode 100644 index 6e31be3caa..0000000000 --- a/plugins/github-release-manager/src/components/NoReleaseBranch.test.tsx +++ /dev/null @@ -1,31 +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 { render } from '@testing-library/react'; - -import { TEST_IDS } from '../test-helpers/test-ids'; -import { NoReleaseBranch } from './NoReleaseBranch'; - -describe('NoReleaseBranch', () => { - it('render NoReleaseBranch', () => { - const { getByTestId } = render(); - - expect( - getByTestId(TEST_IDS.components.noReleaseBranch), - ).toBeInTheDocument(); - }); -}); diff --git a/plugins/github-release-manager/src/components/NoReleaseBranch.tsx b/plugins/github-release-manager/src/components/NoReleaseBranch.tsx deleted file mode 100644 index 66c7f61ec0..0000000000 --- a/plugins/github-release-manager/src/components/NoReleaseBranch.tsx +++ /dev/null @@ -1,35 +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 { Alert } from '@material-ui/lab'; - -import { useStyles } from '../styles/styles'; -import { TEST_IDS } from '../test-helpers/test-ids'; - -export const NoReleaseBranch = () => { - const classes = useStyles(); - - return ( - - Unable to find any Release Branch - - ); -}; diff --git a/plugins/github-release-manager/src/test-helpers/test-ids.test.ts b/plugins/github-release-manager/src/test-helpers/test-ids.test.ts index 2c1195b52e..091f2ccb90 100644 --- a/plugins/github-release-manager/src/test-helpers/test-ids.test.ts +++ b/plugins/github-release-manager/src/test-helpers/test-ids.test.ts @@ -36,7 +36,6 @@ describe('test-ids', () => { }, "divider": "grm--divider", "noLatestRelease": "grm--no-latest-release", - "noReleaseBranch": "grm--no-release-branch", "responseStepListDialogContent": "grm--response-step-list--dialog-content", "responseStepListItem": "grm--response-step-list-item", "responseStepListItemIconDefault": "grm--response-step-list-item--item-icon--default", diff --git a/plugins/github-release-manager/src/test-helpers/test-ids.ts b/plugins/github-release-manager/src/test-helpers/test-ids.ts index 52a426c9b8..e7d3240a8b 100644 --- a/plugins/github-release-manager/src/test-helpers/test-ids.ts +++ b/plugins/github-release-manager/src/test-helpers/test-ids.ts @@ -55,7 +55,6 @@ export const TEST_IDS = { components: { divider: 'grm--divider', noLatestRelease: 'grm--no-latest-release', - noReleaseBranch: 'grm--no-release-branch', circularProgress: 'grm--circular-progress', responseStepListDialogContent: 'grm--response-step-list--dialog-content', responseStepListItem: 'grm--response-step-list-item', From aa81eaf7e1d7718de1ca6bd57c14dfef65c63df9 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Wed, 21 Apr 2021 18:19:30 +0200 Subject: [PATCH 058/140] Fix VersioningStrategy bug Signed-off-by: Erik Engervall --- .../src/cards/RepoDetailsForm/VersioningStrategy.tsx | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/plugins/github-release-manager/src/cards/RepoDetailsForm/VersioningStrategy.tsx b/plugins/github-release-manager/src/cards/RepoDetailsForm/VersioningStrategy.tsx index b1d4c594cf..b03b22fd3f 100644 --- a/plugins/github-release-manager/src/cards/RepoDetailsForm/VersioningStrategy.tsx +++ b/plugins/github-release-manager/src/cards/RepoDetailsForm/VersioningStrategy.tsx @@ -27,6 +27,7 @@ import { import { TEST_IDS } from '../../test-helpers/test-ids'; import { useProjectContext } from '../../contexts/ProjectContext'; import { useQueryHandler } from '../../hooks/useQueryHandler'; +import { VERSIONING_STRATEGIES } from '../../constants/constants'; export function VersioningStrategy() { const navigate = useNavigate(); @@ -59,9 +60,9 @@ export function VersioningStrategy() { aria-label="calendar-strategy" name="calendar-strategy" value={project.versioningStrategy} - defaultValue="semver" + defaultValue={VERSIONING_STRATEGIES.semver} onChange={event => { - const queryParams = getQueryParamsWithUpdates({ + const { queryParams } = getQueryParamsWithUpdates({ updates: [{ key: 'versioningStrategy', value: event.target.value }], }); @@ -69,12 +70,12 @@ export function VersioningStrategy() { }} > } label="Semantic versioning" /> } label="Calendar versioning" /> From fe0faf6b7a0bb325994b9c2be8a76323595544b7 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Wed, 21 Apr 2021 18:20:01 +0200 Subject: [PATCH 059/140] Create VERSIONING_STRATEGIES constant Signed-off-by: Erik Engervall --- plugins/github-release-manager/src/constants/constants.ts | 8 ++++++++ .../github-release-manager/src/contexts/ProjectContext.ts | 3 ++- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/plugins/github-release-manager/src/constants/constants.ts b/plugins/github-release-manager/src/constants/constants.ts index c60cc117cf..195a4b80af 100644 --- a/plugins/github-release-manager/src/constants/constants.ts +++ b/plugins/github-release-manager/src/constants/constants.ts @@ -29,3 +29,11 @@ export const DISABLE_CACHE = { 'If-None-Match': '', }, } as const; + +export const VERSIONING_STRATEGIES: { + semver: 'semver'; + calver: 'calver'; +} = { + semver: 'semver', + calver: 'calver', +} as const; diff --git a/plugins/github-release-manager/src/contexts/ProjectContext.ts b/plugins/github-release-manager/src/contexts/ProjectContext.ts index 75bf6db28d..d115c469fa 100644 --- a/plugins/github-release-manager/src/contexts/ProjectContext.ts +++ b/plugins/github-release-manager/src/contexts/ProjectContext.ts @@ -16,6 +16,7 @@ import { createContext, useContext } from 'react'; +import { VERSIONING_STRATEGIES } from '../constants/constants'; import { GitHubReleaseManagerError } from '../errors/GitHubReleaseManagerError'; export interface Project { @@ -39,7 +40,7 @@ export interface Project { * * Default: false */ - versioningStrategy: 'calver' | 'semver'; + versioningStrategy: keyof typeof VERSIONING_STRATEGIES; /** * Project props was provided via props * From cd996e7a0d2a55e2cef0701aed7e88c7aec7e838 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Wed, 21 Apr 2021 20:52:47 +0200 Subject: [PATCH 060/140] Correct FormLabel label for VersioningStrategy Signed-off-by: Erik Engervall --- .../src/cards/RepoDetailsForm/VersioningStrategy.tsx | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugins/github-release-manager/src/cards/RepoDetailsForm/VersioningStrategy.tsx b/plugins/github-release-manager/src/cards/RepoDetailsForm/VersioningStrategy.tsx index b03b22fd3f..29db87b0c4 100644 --- a/plugins/github-release-manager/src/cards/RepoDetailsForm/VersioningStrategy.tsx +++ b/plugins/github-release-manager/src/cards/RepoDetailsForm/VersioningStrategy.tsx @@ -54,13 +54,13 @@ export function VersioningStrategy() { required disabled={project.isProvidedViaProps} > - Calendar strategy + Versioning strategy + { const { queryParams } = getQueryParamsWithUpdates({ updates: [{ key: 'versioningStrategy', value: event.target.value }], @@ -74,6 +74,7 @@ export function VersioningStrategy() { control={} label="Semantic versioning" /> + } From 13906def32e628f2e640cc337e38c6af082b4daa Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Wed, 21 Apr 2021 20:53:41 +0200 Subject: [PATCH 061/140] Improve test coverage for VersioningStrategy & add VERSIONING_STRATEGIES to constants.test.ts Signed-off-by: Erik Engervall --- .../VersioningStrategy.test.tsx | 35 +++++++++++++------ .../src/constants/constants.test.ts | 4 +++ 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/plugins/github-release-manager/src/cards/RepoDetailsForm/VersioningStrategy.test.tsx b/plugins/github-release-manager/src/cards/RepoDetailsForm/VersioningStrategy.test.tsx index 9790fba23a..bc27ea4ce1 100644 --- a/plugins/github-release-manager/src/cards/RepoDetailsForm/VersioningStrategy.test.tsx +++ b/plugins/github-release-manager/src/cards/RepoDetailsForm/VersioningStrategy.test.tsx @@ -15,22 +15,23 @@ */ import React from 'react'; -import { render } from '@testing-library/react'; +import { render, fireEvent } from '@testing-library/react'; import { - mockCalverProject, + mockSemverProject, mockSearchCalver, } from '../../test-helpers/test-helpers'; -import { TEST_IDS } from '../../test-helpers/test-ids'; + +const mockNavigate = jest.fn(); jest.mock('react-router', () => ({ - useNavigate: jest.fn(), + useNavigate: () => mockNavigate, useLocation: jest.fn(() => ({ search: mockSearchCalver, })), })); jest.mock('../../contexts/ProjectContext', () => ({ - useProjectContext: jest.fn(() => mockCalverProject), + useProjectContext: jest.fn(() => mockSemverProject), })); import { VersioningStrategy } from './VersioningStrategy'; @@ -38,11 +39,25 @@ import { VersioningStrategy } from './VersioningStrategy'; describe('Repo', () => { beforeEach(jest.clearAllMocks); - it('should render radio group', async () => { - const { getByTestId } = render(); + it('should render radio group with default values and handle changes', async () => { + const { getByLabelText } = render(); - expect( - getByTestId(TEST_IDS.form.versioningStrategy.radioGroup), - ).toBeInTheDocument(); + const radio1 = getByLabelText('Semantic versioning'); + const radio2 = getByLabelText('Calendar versioning'); + + expect(radio1).toBeChecked(); + expect(radio2).not.toBeChecked(); + + fireEvent.click(radio2); + expect(mockNavigate.mock.calls).toMatchInlineSnapshot(` + Array [ + Array [ + "?versioningStrategy=calver&owner=mock_owner&repo=mock_repo", + Object { + "replace": true, + }, + ], + ] + `); }); }); diff --git a/plugins/github-release-manager/src/constants/constants.test.ts b/plugins/github-release-manager/src/constants/constants.test.ts index 3441221821..cf2b7d7548 100644 --- a/plugins/github-release-manager/src/constants/constants.test.ts +++ b/plugins/github-release-manager/src/constants/constants.test.ts @@ -30,6 +30,10 @@ describe('constants', () => { "minor": "minor", "patch": "patch", }, + "VERSIONING_STRATEGIES": Object { + "calver": "calver", + "semver": "semver", + }, } `); }); From f402ee00e1570dd0d255c51a3a2008a1e7aed874 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Wed, 21 Apr 2021 21:13:22 +0200 Subject: [PATCH 062/140] Revert accidental app-config.yaml push Signed-off-by: Erik Engervall --- app-config.yaml | 3 --- 1 file changed, 3 deletions(-) diff --git a/app-config.yaml b/app-config.yaml index 3b94faa3f9..ac59874878 100644 --- a/app-config.yaml +++ b/app-config.yaml @@ -124,9 +124,6 @@ integrations: github: - host: github.com token: ${GITHUB_TOKEN} - - host: ghe.spotify.net - apiBaseUrl: https://ghe.spotify.net/api/v3 - token: ${GHE_TOKEN} ### Example for how to add your GitHub Enterprise instance using the API: # - host: ghe.example.net # apiBaseUrl: https://ghe.example.net/api/v3 From 3e1761460b8df444e636440b17d2db18cab6ada4 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Wed, 21 Apr 2021 21:30:53 +0200 Subject: [PATCH 063/140] Add tests for PluginApiClient & githubReleaseManagerApiRef Signed-off-by: Erik Engervall --- .../src/api/PluginApiClient.test.ts | 63 +++++++++++++++++++ .../src/api/serviceApiRef.test.ts | 32 ++++++++++ 2 files changed, 95 insertions(+) create mode 100644 plugins/github-release-manager/src/api/PluginApiClient.test.ts create mode 100644 plugins/github-release-manager/src/api/serviceApiRef.test.ts diff --git a/plugins/github-release-manager/src/api/PluginApiClient.test.ts b/plugins/github-release-manager/src/api/PluginApiClient.test.ts new file mode 100644 index 0000000000..e9db190eef --- /dev/null +++ b/plugins/github-release-manager/src/api/PluginApiClient.test.ts @@ -0,0 +1,63 @@ +/* + * 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 { OAuthApi } from '@backstage/core'; + +import { PluginApiClient } from './PluginApiClient'; + +jest.mock('@backstage/integration', () => ({ + readGitHubIntegrationConfigs: jest.fn(() => []), +})); + +describe('PluginApiClient', () => { + it('should return the default plugin api client', () => { + const configApi = { + getOptionalConfigArray: jest.fn(), + } as any; + const githubAuthApi: OAuthApi = { + getAccessToken: jest.fn(), + }; + const pluginApiClient = new PluginApiClient({ configApi, githubAuthApi }); + + expect(pluginApiClient).toMatchInlineSnapshot(` + PluginApiClient { + "baseUrl": "https://api.github.com", + "createRc": Object { + "createRef": [Function], + "createRelease": [Function], + "getComparison": [Function], + }, + "githubAuthApi": Object { + "getAccessToken": [MockFunction], + }, + "host": "github.com", + "patch": Object { + "createCherryPickCommit": [Function], + "createReference": [Function], + "createTagObject": [Function], + "createTempCommit": [Function], + "forceBranchHeadToTempCommit": [Function], + "merge": [Function], + "replaceTempCommit": [Function], + "updateRelease": [Function], + }, + "promoteRc": Object { + "promoteRelease": [Function], + }, + } + `); + }); +}); diff --git a/plugins/github-release-manager/src/api/serviceApiRef.test.ts b/plugins/github-release-manager/src/api/serviceApiRef.test.ts new file mode 100644 index 0000000000..7dc22deb29 --- /dev/null +++ b/plugins/github-release-manager/src/api/serviceApiRef.test.ts @@ -0,0 +1,32 @@ +/* + * 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 { githubReleaseManagerApiRef } from './serviceApiRef'; + +describe('githubReleaseManagerApiRef', () => { + it('should work', () => { + const result = githubReleaseManagerApiRef; + + expect(result).toMatchInlineSnapshot(` + ApiRefImpl { + "config": Object { + "description": "Used by the GitHub Release Manager plugin to make requests", + "id": "plugin.github-release-manager.service", + }, + } + `); + }); +}); From 1925246f848448a2c52c50fbfb0365a3919fef2f Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Wed, 21 Apr 2021 21:51:52 +0200 Subject: [PATCH 064/140] Improve tests for Cards and Info Signed-off-by: Erik Engervall --- .../src/cards/Cards.test.tsx | 2 +- .../src/cards/Info/Info.test.tsx | 51 +++++++++++++++++-- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/plugins/github-release-manager/src/cards/Cards.test.tsx b/plugins/github-release-manager/src/cards/Cards.test.tsx index dfb5491842..8be47b4d59 100644 --- a/plugins/github-release-manager/src/cards/Cards.test.tsx +++ b/plugins/github-release-manager/src/cards/Cards.test.tsx @@ -30,7 +30,7 @@ jest.mock('../contexts/ProjectContext', () => ({ import { Cards } from './Cards'; describe('Cards', () => { - it('should render info', async () => { + it('should omit cards omitted via configuration', async () => { const { getByTestId } = render( ({ useProjectContext: jest.fn(() => mockCalverProject), @@ -30,11 +30,52 @@ jest.mock('../../contexts/ProjectContext', () => ({ import { Info } from './Info'; describe('Info', () => { - it('should return early if no latestRelease exists', () => { - const { getByTestId } = render( - , + it('should return early if no latestRelease exists', async () => { + const { findByText } = render( + , ); - expect(getByTestId(TEST_IDS.info.info)).toBeInTheDocument(); + expect(await findByText(mockReleaseBranch.name)).toMatchInlineSnapshot(` + + rc/1.2.3 + + `); + + expect( + await findByText(`${mockCalverProject.owner}/${mockCalverProject.repo}`), + ).toMatchInlineSnapshot(` + + mock_owner/mock_repo + + `); + + expect(await findByText(mockCalverProject.versioningStrategy)) + .toMatchInlineSnapshot(` + + calver + + `); + + expect(await findByText(mockReleaseCandidateCalver.tagName)) + .toMatchInlineSnapshot(` + + rc-2020.01.01_1 + + `); }); }); From a32d221ab85197e23cfda1edc78c6d961e3a3801 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Wed, 21 Apr 2021 22:19:06 +0200 Subject: [PATCH 065/140] Add tests for LinearProgressWithLabel & a test id for it Signed-off-by: Erik Engervall --- .../LinearProgressWithLabel.test.tsx | 104 ++++++++++++++++++ .../LinearProgressWithLabel.tsx | 20 +++- .../src/test-helpers/test-ids.test.ts | 1 + .../src/test-helpers/test-ids.ts | 1 + 4 files changed, 123 insertions(+), 3 deletions(-) create mode 100644 plugins/github-release-manager/src/components/ResponseStepDialog/LinearProgressWithLabel.test.tsx diff --git a/plugins/github-release-manager/src/components/ResponseStepDialog/LinearProgressWithLabel.test.tsx b/plugins/github-release-manager/src/components/ResponseStepDialog/LinearProgressWithLabel.test.tsx new file mode 100644 index 0000000000..a043758209 --- /dev/null +++ b/plugins/github-release-manager/src/components/ResponseStepDialog/LinearProgressWithLabel.test.tsx @@ -0,0 +1,104 @@ +/* + * 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 { render } from '@testing-library/react'; + +import { LinearProgressWithLabel, testables } from './LinearProgressWithLabel'; +import { TEST_IDS } from '../../test-helpers/test-ids'; + +const { ICONS } = testables; + +describe('LinearProgressWithLabel', () => { + it('should render 50% progress without CompletionEmoji', () => { + const progress = 50; + + const { container, getByTestId } = render( + , + ); + + expect( + getByTestId(TEST_IDS.components.linearProgressWithLabel).getAttribute( + 'style', + ), + ).toContain(`font-size: 141%`); + expect(container.innerHTML).toContain(`${progress}%`); + expect(container.innerHTML).not.toContain(ICONS.SUCCESS); + expect(container.innerHTML).not.toContain(ICONS.FAILURE); + }); + + it('should render 100% progress with CompletionEmoji for success', () => { + const progress = 100; + + const { container, getByTestId } = render( + , + ); + + expect( + getByTestId(TEST_IDS.components.linearProgressWithLabel).getAttribute( + 'style', + ), + ).toContain(`font-size: 157%`); + expect(container.innerHTML).toContain(`${progress}%`); + expect(container.innerHTML).toContain(ICONS.SUCCESS); + expect(container.innerHTML).not.toContain(ICONS.FAILURE); + }); + + it('should render 100% progress with CompletionEmoji for failure if at least one failed response step is present', () => { + const progress = 100; + + const { container } = render( + , + ); + + expect(container.innerHTML).toContain(`${progress}%`); + expect(container.innerHTML).toContain(ICONS.FAILURE); + expect(container.innerHTML).not.toContain(ICONS.SUCCESS); + }); + + it('should round > 100 progress to 100', () => { + const progress = 101; + + const { container } = render( + , + ); + + expect(container.innerHTML).toContain('100%'); + expect(container.innerHTML).toContain(ICONS.SUCCESS); + expect(container.innerHTML).not.toContain(ICONS.FAILURE); + expect(container.innerHTML).not.toContain(`${progress}%`); + }); +}); diff --git a/plugins/github-release-manager/src/components/ResponseStepDialog/LinearProgressWithLabel.tsx b/plugins/github-release-manager/src/components/ResponseStepDialog/LinearProgressWithLabel.tsx index a5091021e7..62d14e771e 100644 --- a/plugins/github-release-manager/src/components/ResponseStepDialog/LinearProgressWithLabel.tsx +++ b/plugins/github-release-manager/src/components/ResponseStepDialog/LinearProgressWithLabel.tsx @@ -18,6 +18,7 @@ import React from 'react'; import { Box, LinearProgress, Typography } from '@material-ui/core'; import { ResponseStep } from '../../types/types'; +import { TEST_IDS } from '../../test-helpers/test-ids'; const STATUSES = { FAILURE: 'FAILURE', @@ -25,6 +26,13 @@ const STATUSES = { SUCCESS: 'SUCCESS', } as const; +const ICONS = { + SUCCESS: '🚀', + FAILURE: '🔥', +}; + +const getFontSize = (progress: number) => 125 + Math.ceil(progress / Math.PI); + export function LinearProgressWithLabel(props: { progress: number; responseSteps: ResponseStep[]; @@ -42,8 +50,8 @@ export function LinearProgressWithLabel(props: { const CompletionEmoji = () => { if (status === STATUSES.ONGOING) return null; - if (status === STATUSES.FAILURE) return {' 🔥 '}; - return {' 🚀 '}; + if (status === STATUSES.FAILURE) return {` ${ICONS.FAILURE} `}; + return {` ${ICONS.SUCCESS} `}; }; return ( @@ -61,12 +69,14 @@ export function LinearProgressWithLabel(props: { @@ -77,3 +87,7 @@ export function LinearProgressWithLabel(props: { ); } + +export const testables = { + ICONS, +}; diff --git a/plugins/github-release-manager/src/test-helpers/test-ids.test.ts b/plugins/github-release-manager/src/test-helpers/test-ids.test.ts index 091f2ccb90..5adcebc3d9 100644 --- a/plugins/github-release-manager/src/test-helpers/test-ids.test.ts +++ b/plugins/github-release-manager/src/test-helpers/test-ids.test.ts @@ -35,6 +35,7 @@ describe('test-ids', () => { "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", diff --git a/plugins/github-release-manager/src/test-helpers/test-ids.ts b/plugins/github-release-manager/src/test-helpers/test-ids.ts index e7d3240a8b..1beb6aec10 100644 --- a/plugins/github-release-manager/src/test-helpers/test-ids.ts +++ b/plugins/github-release-manager/src/test-helpers/test-ids.ts @@ -77,5 +77,6 @@ export const TEST_IDS = { versioning: 'grm--differ--icons--versioning', }, }, + linearProgressWithLabel: 'grm--linear-progress-with-label', }, }; From 491af9430a865a24a931d13a083f6b160f1d31a0 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Thu, 22 Apr 2021 13:33:43 +0200 Subject: [PATCH 066/140] Return objects from contexts to simplify naming Signed-off-by: Erik Engervall --- .../src/GitHubReleaseManager.tsx | 4 ++-- .../src/cards/Cards.test.tsx | 8 ++++++-- .../github-release-manager/src/cards/Cards.tsx | 4 ++-- .../src/cards/CreateRc/CreateRc.test.tsx | 12 +++++++++--- .../src/cards/CreateRc/CreateRc.tsx | 4 ++-- .../src/cards/Info/Info.test.tsx | 4 +++- .../src/cards/Info/Info.tsx | 3 +-- .../src/cards/Patch/Patch.test.tsx | 4 +++- .../src/cards/Patch/Patch.tsx | 2 +- .../src/cards/Patch/PatchBody.test.tsx | 8 ++++++-- .../src/cards/Patch/PatchBody.tsx | 4 ++-- .../src/cards/PromoteRc/PromoteRcBody.test.tsx | 8 ++++++-- .../src/cards/PromoteRc/PromoteRcBody.tsx | 4 ++-- .../src/cards/RepoDetailsForm/Owner.test.tsx | 15 +++++++++------ .../src/cards/RepoDetailsForm/Owner.tsx | 4 ++-- .../src/cards/RepoDetailsForm/Repo.test.tsx | 15 +++++++++------ .../src/cards/RepoDetailsForm/Repo.tsx | 4 ++-- .../RepoDetailsForm/VersioningStrategy.test.tsx | 4 +++- .../cards/RepoDetailsForm/VersioningStrategy.tsx | 2 +- .../src/contexts/PluginApiClientContext.ts | 8 +++++--- .../src/contexts/ProjectContext.ts | 10 +++++++--- .../src/contexts/RefetchContext.ts | 13 +++++++------ 22 files changed, 90 insertions(+), 54 deletions(-) diff --git a/plugins/github-release-manager/src/GitHubReleaseManager.tsx b/plugins/github-release-manager/src/GitHubReleaseManager.tsx index 64b7ebb028..ff77e8bbe4 100644 --- a/plugins/github-release-manager/src/GitHubReleaseManager.tsx +++ b/plugins/github-release-manager/src/GitHubReleaseManager.tsx @@ -81,8 +81,8 @@ export function GitHubReleaseManager(props: GitHubReleaseManagerProps) { } return ( - - + +
diff --git a/plugins/github-release-manager/src/cards/Cards.test.tsx b/plugins/github-release-manager/src/cards/Cards.test.tsx index 8be47b4d59..8546c09cef 100644 --- a/plugins/github-release-manager/src/cards/Cards.test.tsx +++ b/plugins/github-release-manager/src/cards/Cards.test.tsx @@ -21,10 +21,14 @@ import { mockApiClient, mockCalverProject } from '../test-helpers/test-helpers'; import { TEST_IDS } from '../test-helpers/test-ids'; jest.mock('../contexts/PluginApiClientContext', () => ({ - usePluginApiClientContext: () => mockApiClient, + usePluginApiClientContext: () => ({ + pluginApiClient: mockApiClient, + }), })); jest.mock('../contexts/ProjectContext', () => ({ - useProjectContext: jest.fn(() => mockCalverProject), + useProjectContext: () => ({ + project: mockCalverProject, + }), })); import { Cards } from './Cards'; diff --git a/plugins/github-release-manager/src/cards/Cards.tsx b/plugins/github-release-manager/src/cards/Cards.tsx index 22d7a8e884..0b4149212e 100644 --- a/plugins/github-release-manager/src/cards/Cards.tsx +++ b/plugins/github-release-manager/src/cards/Cards.tsx @@ -35,8 +35,8 @@ export function Cards({ }: { components: GitHubReleaseManagerProps['components']; }) { - const pluginApiClient = usePluginApiClientContext(); - const project = useProjectContext(); + const { pluginApiClient } = usePluginApiClientContext(); + const { project } = useProjectContext(); const [refetchTrigger, setRefetchTrigger] = useState(0); const { gitHubBatchInfo } = useGetGitHubBatchInfo({ pluginApiClient, diff --git a/plugins/github-release-manager/src/cards/CreateRc/CreateRc.test.tsx b/plugins/github-release-manager/src/cards/CreateRc/CreateRc.test.tsx index 084754ce06..98bf4903ac 100644 --- a/plugins/github-release-manager/src/cards/CreateRc/CreateRc.test.tsx +++ b/plugins/github-release-manager/src/cards/CreateRc/CreateRc.test.tsx @@ -30,10 +30,14 @@ import { TEST_IDS } from '../../test-helpers/test-ids'; import { useCreateRc } from './hooks/useCreateRc'; jest.mock('../../contexts/PluginApiClientContext', () => ({ - usePluginApiClientContext: () => mockApiClient, + usePluginApiClientContext: () => ({ + pluginApiClient: mockApiClient, + }), })); jest.mock('../../contexts/ProjectContext', () => ({ - useProjectContext: jest.fn(() => mockCalverProject), + useProjectContext: jest.fn(() => ({ + project: mockCalverProject, + })), })); jest.mock('../../helpers/getRcGitHubInfo', () => ({ getRcGitHubInfo: () => mockNextGitHubInfoSemver, @@ -65,7 +69,9 @@ describe('CreateRc', () => { }); it('should display select element for semver', () => { - (useProjectContext as jest.Mock).mockReturnValue(mockSemverProject); + (useProjectContext as jest.Mock).mockReturnValue({ + project: mockSemverProject, + }); const { getByTestId } = render( { - const pluginApiClient = usePluginApiClientContext(); - const project = useProjectContext(); + const { pluginApiClient } = usePluginApiClientContext(); + const { project } = useProjectContext(); const classes = useStyles(); const [semverBumpLevel, setSemverBumpLevel] = useState<'major' | 'minor'>( diff --git a/plugins/github-release-manager/src/cards/Info/Info.test.tsx b/plugins/github-release-manager/src/cards/Info/Info.test.tsx index 4656c4a4e0..2ca2b45576 100644 --- a/plugins/github-release-manager/src/cards/Info/Info.test.tsx +++ b/plugins/github-release-manager/src/cards/Info/Info.test.tsx @@ -24,7 +24,9 @@ import { } from '../../test-helpers/test-helpers'; jest.mock('../../contexts/ProjectContext', () => ({ - useProjectContext: jest.fn(() => mockCalverProject), + useProjectContext: () => ({ + project: mockCalverProject, + }), })); import { Info } from './Info'; diff --git a/plugins/github-release-manager/src/cards/Info/Info.tsx b/plugins/github-release-manager/src/cards/Info/Info.tsx index 5830046bad..72a61a38e6 100644 --- a/plugins/github-release-manager/src/cards/Info/Info.tsx +++ b/plugins/github-release-manager/src/cards/Info/Info.tsx @@ -34,8 +34,7 @@ interface InfoCardProps { } export const Info = ({ releaseBranch, latestRelease }: InfoCardProps) => { - const project = useProjectContext(); - + const { project } = useProjectContext(); const classes = useStyles(); return ( diff --git a/plugins/github-release-manager/src/cards/Patch/Patch.test.tsx b/plugins/github-release-manager/src/cards/Patch/Patch.test.tsx index a9ac2d31a5..2f7f170d9f 100644 --- a/plugins/github-release-manager/src/cards/Patch/Patch.test.tsx +++ b/plugins/github-release-manager/src/cards/Patch/Patch.test.tsx @@ -24,7 +24,9 @@ import { import { TEST_IDS } from '../../test-helpers/test-ids'; jest.mock('../../contexts/ProjectContext', () => ({ - useProjectContext: jest.fn(() => mockCalverProject), + useProjectContext: () => ({ + project: mockCalverProject, + }), })); import { Patch } from './Patch'; diff --git a/plugins/github-release-manager/src/cards/Patch/Patch.tsx b/plugins/github-release-manager/src/cards/Patch/Patch.tsx index 2f184277a1..dc3ca1c5b8 100644 --- a/plugins/github-release-manager/src/cards/Patch/Patch.tsx +++ b/plugins/github-release-manager/src/cards/Patch/Patch.tsx @@ -40,7 +40,7 @@ export const Patch = ({ releaseBranch, successCb, }: PatchProps) => { - const project = useProjectContext(); + const { project } = useProjectContext(); const classes = useStyles(); function Body() { diff --git a/plugins/github-release-manager/src/cards/Patch/PatchBody.test.tsx b/plugins/github-release-manager/src/cards/Patch/PatchBody.test.tsx index 2d8025bd3f..be9ea1ce45 100644 --- a/plugins/github-release-manager/src/cards/Patch/PatchBody.test.tsx +++ b/plugins/github-release-manager/src/cards/Patch/PatchBody.test.tsx @@ -28,10 +28,14 @@ import { } from '../../test-helpers/test-helpers'; jest.mock('../../contexts/PluginApiClientContext', () => ({ - usePluginApiClientContext: jest.fn(() => mockApiClient), + usePluginApiClientContext: () => ({ + pluginApiClient: mockApiClient, + }), })); jest.mock('../../contexts/ProjectContext', () => ({ - useProjectContext: jest.fn(() => mockCalverProject), + useProjectContext: () => ({ + project: mockCalverProject, + }), })); jest.mock('./hooks/usePatch', () => ({ usePatch: () => ({ diff --git a/plugins/github-release-manager/src/cards/Patch/PatchBody.tsx b/plugins/github-release-manager/src/cards/Patch/PatchBody.tsx index 6f112cf013..ec7b6ab1f3 100644 --- a/plugins/github-release-manager/src/cards/Patch/PatchBody.tsx +++ b/plugins/github-release-manager/src/cards/Patch/PatchBody.tsx @@ -65,8 +65,8 @@ export const PatchBody = ({ successCb, tagParts, }: PatchBodyProps) => { - const pluginApiClient = usePluginApiClientContext(); - const project = useProjectContext(); + const { pluginApiClient } = usePluginApiClientContext(); + const { project } = useProjectContext(); const [checkedCommitIndex, setCheckedCommitIndex] = useState(-1); const githubDataResponse = useAsync(async () => { diff --git a/plugins/github-release-manager/src/cards/PromoteRc/PromoteRcBody.test.tsx b/plugins/github-release-manager/src/cards/PromoteRc/PromoteRcBody.test.tsx index 573e85bf10..51724a1e22 100644 --- a/plugins/github-release-manager/src/cards/PromoteRc/PromoteRcBody.test.tsx +++ b/plugins/github-release-manager/src/cards/PromoteRc/PromoteRcBody.test.tsx @@ -25,10 +25,14 @@ import { import { TEST_IDS } from '../../test-helpers/test-ids'; jest.mock('../../contexts/PluginApiClientContext', () => ({ - usePluginApiClientContext: jest.fn(() => mockApiClient), + usePluginApiClientContext: () => ({ + pluginApiClient: mockApiClient, + }), })); jest.mock('../../contexts/ProjectContext', () => ({ - useProjectContext: jest.fn(() => mockCalverProject), + useProjectContext: () => ({ + project: mockCalverProject, + }), })); jest.mock('./hooks/usePromoteRc', () => ({ usePromoteRc: () => ({ diff --git a/plugins/github-release-manager/src/cards/PromoteRc/PromoteRcBody.tsx b/plugins/github-release-manager/src/cards/PromoteRc/PromoteRcBody.tsx index 88d1ccbf79..bfdfd1a976 100644 --- a/plugins/github-release-manager/src/cards/PromoteRc/PromoteRcBody.tsx +++ b/plugins/github-release-manager/src/cards/PromoteRc/PromoteRcBody.tsx @@ -33,8 +33,8 @@ interface PromoteRcBodyProps { } export const PromoteRcBody = ({ rcRelease, successCb }: PromoteRcBodyProps) => { - const pluginApiClient = usePluginApiClientContext(); - const project = useProjectContext(); + const { pluginApiClient } = usePluginApiClientContext(); + const { project } = useProjectContext(); const classes = useStyles(); const releaseVersion = rcRelease.tagName.replace('rc-', 'version-'); diff --git a/plugins/github-release-manager/src/cards/RepoDetailsForm/Owner.test.tsx b/plugins/github-release-manager/src/cards/RepoDetailsForm/Owner.test.tsx index e673c39c59..da95f5fa50 100644 --- a/plugins/github-release-manager/src/cards/RepoDetailsForm/Owner.test.tsx +++ b/plugins/github-release-manager/src/cards/RepoDetailsForm/Owner.test.tsx @@ -31,10 +31,14 @@ jest.mock('react-router', () => ({ })), })); jest.mock('../../contexts/PluginApiClientContext', () => ({ - usePluginApiClientContext: jest.fn(() => mockApiClient), + usePluginApiClientContext: () => ({ + pluginApiClient: mockApiClient, + }), })); jest.mock('../../contexts/ProjectContext', () => ({ - useProjectContext: jest.fn(() => mockCalverProject), + useProjectContext: jest.fn(() => ({ + project: mockCalverProject, + })), })); import { useProjectContext } from '../../contexts/ProjectContext'; @@ -55,10 +59,9 @@ describe('Owner', () => { }); it('should render select for empty owners', async () => { - (useProjectContext as jest.Mock).mockImplementation(() => ({ - ...mockCalverProject, - owner: '', - })); + (useProjectContext as jest.Mock).mockReturnValue({ + project: { ...mockCalverProject, owner: '' }, + }); const { getAllByTestId, getByTestId } = render( , diff --git a/plugins/github-release-manager/src/cards/RepoDetailsForm/Owner.tsx b/plugins/github-release-manager/src/cards/RepoDetailsForm/Owner.tsx index 4c040cac4f..3cd2be6f7e 100644 --- a/plugins/github-release-manager/src/cards/RepoDetailsForm/Owner.tsx +++ b/plugins/github-release-manager/src/cards/RepoDetailsForm/Owner.tsx @@ -33,10 +33,10 @@ import { useProjectContext } from '../../contexts/ProjectContext'; import { useQueryHandler } from '../../hooks/useQueryHandler'; export function Owner({ username }: { username: string }) { - const project = useProjectContext(); + const { pluginApiClient } = usePluginApiClientContext(); + const { project } = useProjectContext(); const formClasses = useFormClasses(); const navigate = useNavigate(); - const pluginApiClient = usePluginApiClientContext(); const { getQueryParamsWithUpdates } = useQueryHandler(); const { loading, error, value } = useAsync(() => pluginApiClient.getOwners()); diff --git a/plugins/github-release-manager/src/cards/RepoDetailsForm/Repo.test.tsx b/plugins/github-release-manager/src/cards/RepoDetailsForm/Repo.test.tsx index a5b7fca25a..ab6c98ea5f 100644 --- a/plugins/github-release-manager/src/cards/RepoDetailsForm/Repo.test.tsx +++ b/plugins/github-release-manager/src/cards/RepoDetailsForm/Repo.test.tsx @@ -31,10 +31,14 @@ jest.mock('react-router', () => ({ })), })); jest.mock('../../contexts/PluginApiClientContext', () => ({ - usePluginApiClientContext: jest.fn(() => mockApiClient), + usePluginApiClientContext: () => ({ + pluginApiClient: mockApiClient, + }), })); jest.mock('../../contexts/ProjectContext', () => ({ - useProjectContext: jest.fn(() => mockCalverProject), + useProjectContext: jest.fn(() => ({ + project: mockCalverProject, + })), })); import { useProjectContext } from '../../contexts/ProjectContext'; @@ -53,10 +57,9 @@ describe('Repo', () => { }); it('should render select for empty repo', async () => { - (useProjectContext as jest.Mock).mockImplementation(() => ({ - ...mockCalverProject, - repo: '', - })); + (useProjectContext as jest.Mock).mockReturnValue({ + project: { ...mockCalverProject, repo: '' }, + }); const { getAllByTestId, getByTestId } = render(); diff --git a/plugins/github-release-manager/src/cards/RepoDetailsForm/Repo.tsx b/plugins/github-release-manager/src/cards/RepoDetailsForm/Repo.tsx index 57a339ab81..cb08a651cb 100644 --- a/plugins/github-release-manager/src/cards/RepoDetailsForm/Repo.tsx +++ b/plugins/github-release-manager/src/cards/RepoDetailsForm/Repo.tsx @@ -33,8 +33,8 @@ import { useProjectContext } from '../../contexts/ProjectContext'; import { useQueryHandler } from '../../hooks/useQueryHandler'; export function Repo() { - const pluginApiClient = usePluginApiClientContext(); - const project = useProjectContext(); + const { pluginApiClient } = usePluginApiClientContext(); + const { project } = useProjectContext(); const navigate = useNavigate(); const formClasses = useFormClasses(); const { getQueryParamsWithUpdates } = useQueryHandler(); diff --git a/plugins/github-release-manager/src/cards/RepoDetailsForm/VersioningStrategy.test.tsx b/plugins/github-release-manager/src/cards/RepoDetailsForm/VersioningStrategy.test.tsx index bc27ea4ce1..4d36b26ed6 100644 --- a/plugins/github-release-manager/src/cards/RepoDetailsForm/VersioningStrategy.test.tsx +++ b/plugins/github-release-manager/src/cards/RepoDetailsForm/VersioningStrategy.test.tsx @@ -31,7 +31,9 @@ jest.mock('react-router', () => ({ })), })); jest.mock('../../contexts/ProjectContext', () => ({ - useProjectContext: jest.fn(() => mockSemverProject), + useProjectContext: () => ({ + project: mockSemverProject, + }), })); import { VersioningStrategy } from './VersioningStrategy'; diff --git a/plugins/github-release-manager/src/cards/RepoDetailsForm/VersioningStrategy.tsx b/plugins/github-release-manager/src/cards/RepoDetailsForm/VersioningStrategy.tsx index 29db87b0c4..67ca47f566 100644 --- a/plugins/github-release-manager/src/cards/RepoDetailsForm/VersioningStrategy.tsx +++ b/plugins/github-release-manager/src/cards/RepoDetailsForm/VersioningStrategy.tsx @@ -31,7 +31,7 @@ import { VERSIONING_STRATEGIES } from '../../constants/constants'; export function VersioningStrategy() { const navigate = useNavigate(); - const project = useProjectContext(); + const { project } = useProjectContext(); const { getParsedQuery, getQueryParamsWithUpdates } = useQueryHandler(); useEffect(() => { diff --git a/plugins/github-release-manager/src/contexts/PluginApiClientContext.ts b/plugins/github-release-manager/src/contexts/PluginApiClientContext.ts index 1954321644..2d4c9f27ac 100644 --- a/plugins/github-release-manager/src/contexts/PluginApiClientContext.ts +++ b/plugins/github-release-manager/src/contexts/PluginApiClientContext.ts @@ -20,15 +20,17 @@ import { IPluginApiClient } from '../api/PluginApiClient'; import { GitHubReleaseManagerError } from '../errors/GitHubReleaseManagerError'; export const PluginApiClientContext = createContext< - IPluginApiClient | undefined + { pluginApiClient: IPluginApiClient } | undefined >(undefined); export const usePluginApiClientContext = () => { - const pluginApiClient = useContext(PluginApiClientContext); + const { pluginApiClient } = useContext(PluginApiClientContext) ?? {}; if (!pluginApiClient) { throw new GitHubReleaseManagerError('pluginApiClient not found'); } - return pluginApiClient; + return { + pluginApiClient, + }; }; diff --git a/plugins/github-release-manager/src/contexts/ProjectContext.ts b/plugins/github-release-manager/src/contexts/ProjectContext.ts index d115c469fa..bd8e78c53b 100644 --- a/plugins/github-release-manager/src/contexts/ProjectContext.ts +++ b/plugins/github-release-manager/src/contexts/ProjectContext.ts @@ -49,14 +49,18 @@ export interface Project { isProvidedViaProps: boolean; } -export const ProjectContext = createContext(undefined); +export const ProjectContext = createContext<{ project: Project } | undefined>( + undefined, +); export const useProjectContext = () => { - const project = useContext(ProjectContext); + const { project } = useContext(ProjectContext) ?? {}; if (!project) { throw new GitHubReleaseManagerError('project not found'); } - return project; + return { + project, + }; }; diff --git a/plugins/github-release-manager/src/contexts/RefetchContext.ts b/plugins/github-release-manager/src/contexts/RefetchContext.ts index 4b9d82a17e..344df3d9a5 100644 --- a/plugins/github-release-manager/src/contexts/RefetchContext.ts +++ b/plugins/github-release-manager/src/contexts/RefetchContext.ts @@ -18,12 +18,13 @@ import { createContext, useContext } from 'react'; import { GitHubReleaseManagerError } from '../errors/GitHubReleaseManagerError'; -export interface Refetch { - refetchTrigger: number; - setRefetchTrigger: React.Dispatch>; -} - -export const RefetchContext = createContext(undefined); +export const RefetchContext = createContext< + | { + refetchTrigger: number; + setRefetchTrigger: React.Dispatch>; + } + | undefined +>(undefined); export const useRefetchContext = () => { const refetch = useContext(RefetchContext); From 57d8ea02374f256715bc071df6385a6786db3432 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Thu, 22 Apr 2021 14:32:58 +0200 Subject: [PATCH 067/140] Add condition for displaying current & separator Signed-off-by: Erik Engervall --- .../src/components/Differ.test.tsx | 4 ++-- .../src/components/Differ.tsx | 17 ++++++++++------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/plugins/github-release-manager/src/components/Differ.test.tsx b/plugins/github-release-manager/src/components/Differ.test.tsx index cb625c4118..b35773f1f5 100644 --- a/plugins/github-release-manager/src/components/Differ.test.tsx +++ b/plugins/github-release-manager/src/components/Differ.test.tsx @@ -30,11 +30,11 @@ describe('Differ', () => { const { getByTestId, queryByTestId } = render(); const icon = getByTestId(TEST_IDS.components.differ.icons.branch); - const current = getByTestId(TEST_IDS.components.differ.current); + const current = queryByTestId(TEST_IDS.components.differ.current); const next = queryByTestId(TEST_IDS.components.differ.next); expect(icon).toBeInTheDocument(); - expect(current.innerHTML).toMatchInlineSnapshot(`"None"`); + expect(current).toMatchInlineSnapshot(`null`); expect(next).not.toBeInTheDocument(); }); diff --git a/plugins/github-release-manager/src/components/Differ.tsx b/plugins/github-release-manager/src/components/Differ.tsx index 047b1fbd7e..d4eda688a6 100644 --- a/plugins/github-release-manager/src/components/Differ.tsx +++ b/plugins/github-release-manager/src/components/Differ.tsx @@ -40,14 +40,17 @@ export const Differ = ({ current, next, icon }: DifferProps) => { )} - - {current ?? 'None'} - + {!!current && ( + + {current ?? 'None'} + + )} + + {current && next && {' → '}} - {next && {' → '}} {next && ( Date: Thu, 22 Apr 2021 15:16:05 +0200 Subject: [PATCH 068/140] Disable refresh button in ResponseStepDialog while requests are ongoing Signed-off-by: Erik Engervall --- .../src/components/ResponseStepDialog/ResponseStepDialog.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepDialog.tsx b/plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepDialog.tsx index ba3de2d77a..89714b7bf0 100644 --- a/plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepDialog.tsx +++ b/plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepDialog.tsx @@ -69,6 +69,7 @@ export const ResponseStepDialog = ({ - ); - } - return ( - - - Create Release Candidate - - + {project.versioningStrategy === 'semver' && latestRelease && !conflictingPreRelease && ( @@ -182,9 +151,50 @@ export const CreateRc = ({
)} - + {conflictingPreRelease || tagAlreadyExists ? ( + <> + {conflictingPreRelease && ( + + The most recent release is already a Release Candidate + + )} - - + {tagAlreadyExists && ( + + There's already a tag named{' '} + {nextGitHubInfo.rcReleaseTag} + + )} + + ) : ( +
+ + + + + + + +
+ )} + + + ); }; diff --git a/plugins/github-release-manager/src/cards/CreateRc/hooks/useCreateRc.ts b/plugins/github-release-manager/src/cards/CreateRc/hooks/useCreateRc.ts index b0b0c98dca..2b1b0b688c 100644 --- a/plugins/github-release-manager/src/cards/CreateRc/hooks/useCreateRc.ts +++ b/plugins/github-release-manager/src/cards/CreateRc/hooks/useCreateRc.ts @@ -45,6 +45,16 @@ export function useCreateRc({ project, successCb, }: CreateRC): CardHook { + if (nextGitHubInfo.error) { + throw new GitHubReleaseManagerError( + `Unexpected error: ${ + nextGitHubInfo.error.title + ? `${nextGitHubInfo.error.title} (${nextGitHubInfo.error.subtitle})` + : nextGitHubInfo.error.subtitle + }`, + ); + } + const { responseSteps, addStepToResponseSteps, @@ -162,7 +172,9 @@ export function useCreateRc({ .createRelease({ owner: project.owner, repo: project.repo, - nextGitHubInfo: nextGitHubInfo, + rcReleaseTag: nextGitHubInfo.rcReleaseTag, + releaseName: nextGitHubInfo.releaseName, + rcBranch: nextGitHubInfo.rcBranch, releaseBody: getComparisonRes.value.releaseBody, }) .catch(asyncCatcher); diff --git a/plugins/github-release-manager/src/cards/Patch/Patch.tsx b/plugins/github-release-manager/src/cards/Patch/Patch.tsx index dc3ca1c5b8..d01a366ffb 100644 --- a/plugins/github-release-manager/src/cards/Patch/Patch.tsx +++ b/plugins/github-release-manager/src/cards/Patch/Patch.tsx @@ -16,6 +16,7 @@ import React from 'react'; import { Typography } from '@material-ui/core'; +import { Alert, AlertTitle } from '@material-ui/lab'; import { GetBranchResult, @@ -40,42 +41,59 @@ export const Patch = ({ releaseBranch, successCb, }: PatchProps) => { - const { project } = useProjectContext(); const classes = useStyles(); - function Body() { - if (latestRelease === null) { - return ; - } - - if (releaseBranch === null) { - return ; - } - - const { bumpedTag, tagParts } = getBumpedTag({ - project, - tag: latestRelease.tagName, - bumpLevel: 'patch', - }); - - return ( - - ); - } - return ( Patch Release {latestRelease?.prerelease ? 'Candidate' : 'Version'} - + ); }; + +function BodyWrapper({ latestRelease, releaseBranch, successCb }: PatchProps) { + const { project } = useProjectContext(); + + if (latestRelease === null) { + return ; + } + + if (releaseBranch === null) { + return ; + } + + const bumpedTag = getBumpedTag({ + project, + tag: latestRelease.tagName, + bumpLevel: 'patch', + }); + + if (bumpedTag.error !== undefined) { + return ( + + {bumpedTag.error.title && ( + {bumpedTag.error.title} + )} + + {bumpedTag.error.subtitle} + + ); + } + + return ( + + ); +} diff --git a/plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepDialog.tsx b/plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepDialog.tsx index 89714b7bf0..d504ffec70 100644 --- a/plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepDialog.tsx +++ b/plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepDialog.tsx @@ -14,20 +14,19 @@ * limitations under the License. */ -import React, { forwardRef, Ref } from 'react'; +import React from 'react'; import { Button, Dialog as MaterialDialog, DialogActions, DialogTitle, - Slide, } from '@material-ui/core'; -import { TransitionProps } from '@material-ui/core/transitions'; import RefreshIcon from '@material-ui/icons/Refresh'; import { LinearProgressWithLabel } from './LinearProgressWithLabel'; import { ResponseStep } from '../../types/types'; import { ResponseStepList } from './ResponseStepList'; +import { Transition } from '../Transition'; import { useRefetchContext } from '../../contexts/RefetchContext'; interface DialogProps { @@ -36,13 +35,6 @@ interface DialogProps { title: string; } -const Transition = forwardRef(function Transition( - props: { children?: React.ReactElement } & TransitionProps, - ref: Ref, -) { - return ; -}); - export const ResponseStepDialog = ({ progress, responseSteps, diff --git a/plugins/github-release-manager/src/components/Transition.tsx b/plugins/github-release-manager/src/components/Transition.tsx new file mode 100644 index 0000000000..0c54e0f851 --- /dev/null +++ b/plugins/github-release-manager/src/components/Transition.tsx @@ -0,0 +1,26 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import React, { forwardRef, Ref } from 'react'; +import { Slide } from '@material-ui/core'; +import { TransitionProps } from '@material-ui/core/transitions'; + +export const Transition = forwardRef(function Transition( + props: { children?: React.ReactElement } & TransitionProps, + ref: Ref, +) { + return ; +}); diff --git a/plugins/github-release-manager/src/helpers/getBumpedTag.test.ts b/plugins/github-release-manager/src/helpers/getBumpedTag.test.ts index 1fd7cd2ecf..27023f653f 100644 --- a/plugins/github-release-manager/src/helpers/getBumpedTag.test.ts +++ b/plugins/github-release-manager/src/helpers/getBumpedTag.test.ts @@ -32,6 +32,7 @@ describe('getBumpedTag', () => { expect(result).toMatchInlineSnapshot(` Object { "bumpedTag": "rc-2020.01.01_2", + "error": undefined, "tagParts": Object { "calver": "2020.01.01", "patch": 2, @@ -51,6 +52,7 @@ describe('getBumpedTag', () => { expect(result).toMatchInlineSnapshot(` Object { "bumpedTag": "rc-2020.01.01_2", + "error": undefined, "tagParts": Object { "calver": "2020.01.01", "patch": 2, @@ -72,6 +74,7 @@ describe('getBumpedTag', () => { expect(result).toMatchInlineSnapshot(` Object { "bumpedTag": "rc-1.2.4", + "error": undefined, "tagParts": Object { "major": 1, "minor": 2, @@ -92,6 +95,7 @@ describe('getBumpedTag', () => { expect(result).toMatchInlineSnapshot(` Object { "bumpedTag": "rc-1.3.0", + "error": undefined, "tagParts": Object { "major": 1, "minor": 3, @@ -112,6 +116,7 @@ describe('getBumpedTag', () => { expect(result).toMatchInlineSnapshot(` Object { "bumpedTag": "rc-2.0.0", + "error": undefined, "tagParts": Object { "major": 2, "minor": 0, @@ -122,4 +127,23 @@ describe('getBumpedTag', () => { `); }); }); + + describe('errors', () => { + it('should propagate errors for invalid tags', () => { + const result = getBumpedTag({ + project: mockCalverProject, + tag: '😬', + bumpLevel: 'patch', + }); + + expect(result).toMatchInlineSnapshot(` + Object { + "error": Object { + "subtitle": "Expected calver matching \\"/(rc|version)-([0-9]{4}\\\\.[0-9]{2}\\\\.[0-9]{2})_([0-9]+)/\\", found \\"😬\\"", + "title": "Invalid tag", + }, + } + `); + }); + }); }); diff --git a/plugins/github-release-manager/src/helpers/getBumpedTag.ts b/plugins/github-release-manager/src/helpers/getBumpedTag.ts index 5409f7d2b3..f8b976b01f 100644 --- a/plugins/github-release-manager/src/helpers/getBumpedTag.ts +++ b/plugins/github-release-manager/src/helpers/getBumpedTag.ts @@ -32,11 +32,17 @@ export function getBumpedTag({ }) { const tagParts = getTagParts({ project, tag }); - if (isCalverTagParts(project, tagParts)) { - return getPatchedCalverTag(tagParts); + if (tagParts.error !== undefined) { + return { + error: tagParts.error, + }; } - return getBumpedSemverTag(tagParts, bumpLevel); + if (isCalverTagParts(project, tagParts.tagParts)) { + return getPatchedCalverTag(tagParts.tagParts); + } + + return getBumpedSemverTag(tagParts.tagParts, bumpLevel); } function getPatchedCalverTag(tagParts: CalverTagParts) { @@ -49,6 +55,7 @@ function getPatchedCalverTag(tagParts: CalverTagParts) { return { bumpedTag, tagParts: bumpedTagParts, + error: undefined, }; } @@ -63,6 +70,7 @@ function getBumpedSemverTag( return { bumpedTag, tagParts: bumpedTagParts, + error: undefined, }; } diff --git a/plugins/github-release-manager/src/helpers/getRcGitHubInfo.ts b/plugins/github-release-manager/src/helpers/getRcGitHubInfo.ts index 18043033c9..70935a0dd3 100644 --- a/plugins/github-release-manager/src/helpers/getRcGitHubInfo.ts +++ b/plugins/github-release-manager/src/helpers/getRcGitHubInfo.ts @@ -49,8 +49,17 @@ export const getRcGitHubInfo = ({ }; } - const tagParts = getSemverTagParts(latestRelease.tagName); - const { bumpedTagParts } = getBumpedSemverTagParts(tagParts, semverBumpLevel); + const semverTagParts = getSemverTagParts(latestRelease.tagName); + if (semverTagParts.error !== undefined) { + return { + error: semverTagParts.error, + }; + } + + const { bumpedTagParts } = getBumpedSemverTagParts( + semverTagParts.tagParts, + semverBumpLevel, + ); const bumpedTag = `${bumpedTagParts.major}.${bumpedTagParts.minor}.${bumpedTagParts.patch}`; diff --git a/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.test.ts b/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.test.ts index 238abe720a..c74438b63a 100644 --- a/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.test.ts +++ b/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.test.ts @@ -21,51 +21,87 @@ import { import { getCalverTagParts } from './getCalverTagParts'; describe('getCalverTagParts', () => { - it('should return tagParts for RC tag', () => { - const result = getCalverTagParts(mockReleaseCandidateCalver.tagName); + describe('happy path', () => { + it('should return tagParts for RC tag', () => { + const result = getCalverTagParts(mockReleaseCandidateCalver.tagName); - expect(result).toMatchInlineSnapshot(` + expect(result).toMatchInlineSnapshot(` + Object { + "tagParts": Object { + "calver": "2020.01.01", + "patch": 1, + "prefix": "rc", + }, + } + `); + }); + + it('should return tagParts for Version tag', () => { + const result = getCalverTagParts(mockReleaseVersionCalver.tagName); + + expect(result).toMatchInlineSnapshot(` + Object { + "tagParts": Object { + "calver": "2020.01.01", + "patch": 1, + "prefix": "version", + }, + } + `); + }); + }); + + describe('invalid calver tags', () => { + it('should return error for invalid prefix', () => { + const result = getCalverTagParts('invalid-2020.01.01_1'); + + expect(result).toMatchInlineSnapshot(` Object { - "calver": "2020.01.01", - "patch": 1, - "prefix": "rc", + "error": Object { + "subtitle": "Expected calver matching \\"/(rc|version)-([0-9]{4}\\\\.[0-9]{2}\\\\.[0-9]{2})_([0-9]+)/\\", found \\"invalid-2020.01.01_1\\"", + "title": "Invalid tag", + }, } `); - }); + }); - it('should return tagParts for Version tag', () => { - const result = getCalverTagParts(mockReleaseVersionCalver.tagName); + it('should return error for invalid calver (missing padded zero)', () => { + const result = getCalverTagParts('rc-2020.1.01_1'); - expect(result).toMatchInlineSnapshot(` + expect(result).toMatchInlineSnapshot(` Object { - "calver": "2020.01.01", - "patch": 1, - "prefix": "version", + "error": Object { + "subtitle": "Expected calver matching \\"/(rc|version)-([0-9]{4}\\\\.[0-9]{2}\\\\.[0-9]{2})_([0-9]+)/\\", found \\"rc-2020.1.01_1\\"", + "title": "Invalid tag", + }, } `); - }); + }); - it('should return null for invalid prefix', () => { - expect(() => - getCalverTagParts('invalid-2020.01.01_1'), - ).toThrowErrorMatchingInlineSnapshot(`"Invalid calver tag"`); - }); + it('should return error for invalid calver (missing day)', () => { + const result = getCalverTagParts('rc-2020.01_1'); - it('should return null for invalid calver (missing padded zero)', () => { - expect(() => - getCalverTagParts('rc-2020.1.01_1'), - ).toThrowErrorMatchingInlineSnapshot(`"Invalid calver tag"`); - }); + expect(result).toMatchInlineSnapshot(` + Object { + "error": Object { + "subtitle": "Expected calver matching \\"/(rc|version)-([0-9]{4}\\\\.[0-9]{2}\\\\.[0-9]{2})_([0-9]+)/\\", found \\"rc-2020.01_1\\"", + "title": "Invalid tag", + }, + } + `); + }); - it('should return null for invalid calver (missing day)', () => { - expect(() => - getCalverTagParts('rc-2020.01_1'), - ).toThrowErrorMatchingInlineSnapshot(`"Invalid calver tag"`); - }); + it('should return error for invalid patch (letter instead of number)', () => { + const result = getCalverTagParts('rc-2020.01.01_a'); - it('should return null for invalid patch (letter instead of number)', () => { - expect(() => - getCalverTagParts('rc-2020.01.01_a'), - ).toThrowErrorMatchingInlineSnapshot(`"Invalid calver tag"`); + expect(result).toMatchInlineSnapshot(` + Object { + "error": Object { + "subtitle": "Expected calver matching \\"/(rc|version)-([0-9]{4}\\\\.[0-9]{2}\\\\.[0-9]{2})_([0-9]+)/\\", found \\"rc-2020.01.01_a\\"", + "title": "Invalid tag", + }, + } + `); + }); }); }); diff --git a/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.ts b/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.ts index 0fdd31444e..09f3b3e3d5 100644 --- a/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.ts +++ b/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { GitHubReleaseManagerError } from '../../errors/GitHubReleaseManagerError'; +import { AlertError } from '../../types/types'; export type CalverTagParts = { prefix: string; @@ -25,17 +25,26 @@ export type CalverTagParts = { export const calverRegexp = /(rc|version)-([0-9]{4}\.[0-9]{2}\.[0-9]{2})_([0-9]+)/; export function getCalverTagParts(tag: string) { - const result = tag.match(calverRegexp); + const match = tag.match(calverRegexp); - if (result === null || result.length < 4) { - throw new GitHubReleaseManagerError('Invalid calver tag'); + if (match === null || match.length < 4) { + const error: AlertError = { + title: 'Invalid tag', + subtitle: `Expected calver matching "${calverRegexp}", found "${tag}"`, + }; + + return { + error, + }; } const tagParts: CalverTagParts = { - prefix: result[1], - calver: result[2], - patch: parseInt(result[3], 10), + prefix: match[1], + calver: match[2], + patch: parseInt(match[3], 10), }; - return tagParts; + return { + tagParts, + }; } diff --git a/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.test.ts b/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.test.ts index 9977ab7add..4a5e218eaf 100644 --- a/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.test.ts +++ b/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.test.ts @@ -21,43 +21,80 @@ import { import { getSemverTagParts } from './getSemverTagParts'; describe('getSemverTagParts', () => { - it('should return tagParts for RC tag', () => - expect(getSemverTagParts(mockReleaseCandidateSemver.tagName)) - .toMatchInlineSnapshot(` - Object { - "major": 1, - "minor": 2, - "patch": 3, - "prefix": "rc", - } - `)); + describe('happy path', () => { + it('should return tagParts for RC tag', () => { + const semverTagParts = getSemverTagParts( + mockReleaseCandidateSemver.tagName, + ); - it('should return tagParts for Version tag', () => - expect(getSemverTagParts(mockReleaseVersionSemver.tagName)) - .toMatchInlineSnapshot(` - Object { - "major": 1, - "minor": 2, - "patch": 3, - "prefix": "version", - } - `)); + expect(semverTagParts).toMatchInlineSnapshot(` + Object { + "tagParts": Object { + "major": 1, + "minor": 2, + "patch": 3, + "prefix": "rc", + }, + } + `); + }); - it('should throw for invalid prefix', () => { - expect(() => - getSemverTagParts('invalid-1.2.3'), - ).toThrowErrorMatchingInlineSnapshot(`"Invalid semver tag"`); + it('should return tagParts for Version tag', () => { + const semverTagParts = getSemverTagParts( + mockReleaseVersionSemver.tagName, + ); + + expect(semverTagParts).toMatchInlineSnapshot(` + Object { + "tagParts": Object { + "major": 1, + "minor": 2, + "patch": 3, + "prefix": "version", + }, + } + `); + }); }); - it('should throw for invalid semver (missing patch)', () => { - expect(() => - getSemverTagParts('rc-1.2'), - ).toThrowErrorMatchingInlineSnapshot(`"Invalid semver tag"`); - }); + describe('invalid semver tags', () => { + it('should return error for invalid prefix', () => { + const semverTagParts = getSemverTagParts('invalid-1.2.3'); - it('should throw for invalid semver (founds calver)', () => { - expect(() => - getSemverTagParts('rc-1337.01.01_1'), - ).toThrowErrorMatchingInlineSnapshot(`"Invalid semver tag, found calver"`); + expect(semverTagParts).toMatchInlineSnapshot(` + Object { + "error": Object { + "subtitle": "Expected semver matching \\"/(rc|version)-([0-9]+)\\\\.([0-9]+)\\\\.([0-9]+)/\\", found \\"invalid-1.2.3\\"", + "title": "Invalid tag", + }, + } + `); + }); + + it('should return error for invalid semver (missing patch)', () => { + const semverTagParts = getSemverTagParts('rc-1.2'); + + expect(semverTagParts).toMatchInlineSnapshot(` + Object { + "error": Object { + "subtitle": "Expected semver matching \\"/(rc|version)-([0-9]+)\\\\.([0-9]+)\\\\.([0-9]+)/\\", found \\"rc-1.2\\"", + "title": "Invalid tag", + }, + } + `); + }); + + it('should return error for invalid semver (founds calver)', () => { + const semverTagParts = getSemverTagParts('rc-1337.01.01_1'); + + expect(semverTagParts).toMatchInlineSnapshot(` + Object { + "error": Object { + "subtitle": "Expected semver matching \\"/(rc|version)-([0-9]+)\\\\.([0-9]+)\\\\.([0-9]+)/\\", found calver \\"rc-1337.01.01_1\\"", + "title": "Invalid tag", + }, + } + `); + }); }); }); diff --git a/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.ts b/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.ts index 018dedcee0..8f5d53f488 100644 --- a/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.ts +++ b/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.ts @@ -14,7 +14,7 @@ * limitations under the License. */ -import { GitHubReleaseManagerError } from '../../errors/GitHubReleaseManagerError'; +import { AlertError } from '../../types/types'; import { calverRegexp } from './getCalverTagParts'; export type SemverTagParts = { @@ -24,23 +24,41 @@ export type SemverTagParts = { patch: number; }; -export function getSemverTagParts(tag: string) { - const result = tag.match(/(rc|version)-([0-9]+)\.([0-9]+)\.([0-9]+)/); +export const semverRegexp = /(rc|version)-([0-9]+)\.([0-9]+)\.([0-9]+)/; - if (result === null || result.length < 4) { - throw new GitHubReleaseManagerError('Invalid semver tag'); +export function getSemverTagParts(tag: string) { + const match = tag.match(semverRegexp); + + if (match === null || match.length < 4) { + const error: AlertError = { + title: 'Invalid tag', + subtitle: `Expected semver matching "${semverRegexp}", found "${tag}"`, + }; + + return { + error, + }; } if (tag.match(calverRegexp)) { - throw new GitHubReleaseManagerError('Invalid semver tag, found calver'); + const error: AlertError = { + title: 'Invalid tag', + subtitle: `Expected semver matching "${semverRegexp}", found calver "${tag}"`, + }; + + return { + error, + }; } const tagParts: SemverTagParts = { - prefix: result[1], - major: parseInt(result[2], 10), - minor: parseInt(result[3], 10), - patch: parseInt(result[4], 10), + prefix: match[1], + major: parseInt(match[2], 10), + minor: parseInt(match[3], 10), + patch: parseInt(match[4], 10), }; - return tagParts; + return { + tagParts, + }; } diff --git a/plugins/github-release-manager/src/helpers/tagParts/validateTagName.ts b/plugins/github-release-manager/src/helpers/tagParts/validateTagName.ts new file mode 100644 index 0000000000..e7d6124c81 --- /dev/null +++ b/plugins/github-release-manager/src/helpers/tagParts/validateTagName.ts @@ -0,0 +1,47 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getCalverTagParts } from './getCalverTagParts'; +import { getSemverTagParts } from './getSemverTagParts'; +import { Project } from '../../contexts/ProjectContext'; + +export const validateTagName = ({ + project, + tagName, +}: { + project: Project; + tagName?: string; +}) => { + if (!tagName) { + return { + tagNameError: null, + }; + } + + if (project.versioningStrategy === 'calver') { + const { error } = getCalverTagParts(tagName); + + return { + tagNameError: error, + }; + } + + const { error } = getSemverTagParts(tagName); + + return { + tagNameError: error, + }; +}; diff --git a/plugins/github-release-manager/src/helpers/tagParts/validateTagParts.test.ts b/plugins/github-release-manager/src/helpers/tagParts/validateTagParts.test.ts new file mode 100644 index 0000000000..cf0147db40 --- /dev/null +++ b/plugins/github-release-manager/src/helpers/tagParts/validateTagParts.test.ts @@ -0,0 +1,120 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { + mockCalverProject, + mockReleaseCandidateCalver, + mockReleaseCandidateSemver, + mockSemverProject, +} from '../../test-helpers/test-helpers'; +import { validateTagName } from './validateTagName'; + +describe('validateTagName', () => { + describe('valid tags', () => { + it('should not return any error for valid semver project', () => { + const result = validateTagName({ + project: mockSemverProject, + tagName: mockReleaseCandidateSemver.tagName, + }); + + expect(result).toMatchInlineSnapshot(` + Object { + "tagNameError": undefined, + } + `); + }); + + it('should not return any error for semver project without any releases (i.e. no tagName)', () => { + const result = validateTagName({ + project: mockSemverProject, + }); + + expect(result).toMatchInlineSnapshot(` + Object { + "tagNameError": null, + } + `); + }); + }); + + describe('mismatching tags', () => { + it('should return error for semver project and calver tag', () => { + const result = validateTagName({ + project: mockSemverProject, + tagName: mockReleaseCandidateCalver.tagName, + }); + + expect(result).toMatchInlineSnapshot(` + Object { + "tagNameError": Object { + "subtitle": "Expected semver matching \\"/(rc|version)-([0-9]+)\\\\.([0-9]+)\\\\.([0-9]+)/\\", found calver \\"rc-2020.01.01_1\\"", + "title": "Invalid tag", + }, + } + `); + }); + + it('should return error for calver project and semver tag', () => { + const result = validateTagName({ + project: mockCalverProject, + tagName: mockReleaseCandidateSemver.tagName, + }); + + expect(result).toMatchInlineSnapshot(` + Object { + "tagNameError": Object { + "subtitle": "Expected calver matching \\"/(rc|version)-([0-9]{4}\\\\.[0-9]{2}\\\\.[0-9]{2})_([0-9]+)/\\", found \\"rc-1.2.3\\"", + "title": "Invalid tag", + }, + } + `); + }); + }); + + describe('invalid tags', () => { + it('should return error for semver project and totally invalid tag', () => { + const result = validateTagName({ + project: mockSemverProject, + tagName: 'this-is-so-invalid', + }); + + expect(result).toMatchInlineSnapshot(` + Object { + "tagNameError": Object { + "subtitle": "Expected semver matching \\"/(rc|version)-([0-9]+)\\\\.([0-9]+)\\\\.([0-9]+)/\\", found \\"this-is-so-invalid\\"", + "title": "Invalid tag", + }, + } + `); + }); + + it('should return error for calver project and totally invalid tag', () => { + const result = validateTagName({ + project: mockCalverProject, + tagName: 'this-is-so-invalid', + }); + + expect(result).toMatchInlineSnapshot(` + Object { + "tagNameError": Object { + "subtitle": "Expected calver matching \\"/(rc|version)-([0-9]{4}\\\\.[0-9]{2}\\\\.[0-9]{2})_([0-9]+)/\\", found \\"this-is-so-invalid\\"", + "title": "Invalid tag", + }, + } + `); + }); + }); +}); diff --git a/plugins/github-release-manager/src/hooks/useVersioningStrategyMatchesRepoTags.ts b/plugins/github-release-manager/src/hooks/useVersioningStrategyMatchesRepoTags.ts index 321b5d4b83..032a5280a1 100644 --- a/plugins/github-release-manager/src/hooks/useVersioningStrategyMatchesRepoTags.ts +++ b/plugins/github-release-manager/src/hooks/useVersioningStrategyMatchesRepoTags.ts @@ -35,13 +35,9 @@ export const useVersioningStrategyMatchesRepoTags = ({ setVersioningStrategyMatches(false); if (latestReleaseTagName) { - try { - if (project.repo === repositoryName) { - getTagParts({ project, tag: latestReleaseTagName }); - setVersioningStrategyMatches(true); - } - } catch (error) { - setVersioningStrategyMatches(false); + if (project.repo === repositoryName) { + const { error } = getTagParts({ project, tag: latestReleaseTagName }); + setVersioningStrategyMatches(error === undefined); } } }, [latestReleaseTagName, project, repositoryName]); diff --git a/plugins/github-release-manager/src/types/types.ts b/plugins/github-release-manager/src/types/types.ts index c888c7e539..ece840fa5f 100644 --- a/plugins/github-release-manager/src/types/types.ts +++ b/plugins/github-release-manager/src/types/types.ts @@ -61,3 +61,8 @@ export interface CardHook { run: (args: RunArgs) => Promise; runInvoked: boolean; } + +export interface AlertError { + title?: string; + subtitle: string; +} From 5f8bae8779ff340b3fea54015e54f40a72d63677 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Fri, 23 Apr 2021 00:12:28 +0200 Subject: [PATCH 070/140] Rename Cards to Features Signed-off-by: Erik Engervall --- plugins/github-release-manager/README.md | 2 +- .../src/GitHubReleaseManager.tsx | 8 ++++-- .../src/components/InfoCardPlus.test.tsx | 2 +- .../src/components/InfoCardPlus.tsx | 6 ++-- .../CreateRc/CreateRc.test.tsx | 0 .../{cards => features}/CreateRc/CreateRc.tsx | 0 .../CreateRc/hooks/useCreateRc.test.tsx | 0 .../CreateRc/hooks/useCreateRc.ts | 0 .../Features.test.tsx} | 22 +++++++++++--- .../Cards.tsx => features/Features.tsx} | 2 +- .../{cards => features}/Info/Info.test.tsx | 0 .../src/{cards => features}/Info/Info.tsx | 27 ++++++++++++------ .../src/{cards => features}/Info/flow.png | Bin .../{cards => features}/Patch/Patch.test.tsx | 0 .../src/{cards => features}/Patch/Patch.tsx | 0 .../Patch/PatchBody.test.tsx | 0 .../{cards => features}/Patch/PatchBody.tsx | 0 .../Patch/hooks/usePatch.test.ts | 0 .../Patch/hooks/usePatch.ts | 0 .../PromoteRc/PromoteRc.test.tsx | 0 .../PromoteRc/PromoteRc.tsx | 0 .../PromoteRc/PromoteRcBody.test.tsx | 0 .../PromoteRc/PromoteRcBody.tsx | 0 .../PromoteRc/hooks/usePromoteRc.test.ts | 0 .../PromoteRc/hooks/usePromoteRc.ts | 0 .../RepoDetailsForm/Owner.test.tsx | 0 .../RepoDetailsForm/Owner.tsx | 0 .../RepoDetailsForm/Repo.test.tsx | 0 .../RepoDetailsForm/Repo.tsx | 0 .../RepoDetailsForm/RepoDetailsForm.tsx | 0 .../VersioningStrategy.test.tsx | 0 .../RepoDetailsForm/VersioningStrategy.tsx | 0 .../RepoDetailsForm/styles.ts | 0 .../src/test-helpers/test-ids.test.ts | 2 +- .../src/test-helpers/test-ids.ts | 2 +- 35 files changed, 50 insertions(+), 23 deletions(-) rename plugins/github-release-manager/src/{cards => features}/CreateRc/CreateRc.test.tsx (100%) rename plugins/github-release-manager/src/{cards => features}/CreateRc/CreateRc.tsx (100%) rename plugins/github-release-manager/src/{cards => features}/CreateRc/hooks/useCreateRc.test.tsx (100%) rename plugins/github-release-manager/src/{cards => features}/CreateRc/hooks/useCreateRc.ts (100%) rename plugins/github-release-manager/src/{cards/Cards.test.tsx => features/Features.test.tsx} (84%) rename plugins/github-release-manager/src/{cards/Cards.tsx => features/Features.tsx} (99%) rename plugins/github-release-manager/src/{cards => features}/Info/Info.test.tsx (100%) rename plugins/github-release-manager/src/{cards => features}/Info/Info.tsx (88%) rename plugins/github-release-manager/src/{cards => features}/Info/flow.png (100%) rename plugins/github-release-manager/src/{cards => features}/Patch/Patch.test.tsx (100%) rename plugins/github-release-manager/src/{cards => features}/Patch/Patch.tsx (100%) rename plugins/github-release-manager/src/{cards => features}/Patch/PatchBody.test.tsx (100%) rename plugins/github-release-manager/src/{cards => features}/Patch/PatchBody.tsx (100%) rename plugins/github-release-manager/src/{cards => features}/Patch/hooks/usePatch.test.ts (100%) rename plugins/github-release-manager/src/{cards => features}/Patch/hooks/usePatch.ts (100%) rename plugins/github-release-manager/src/{cards => features}/PromoteRc/PromoteRc.test.tsx (100%) rename plugins/github-release-manager/src/{cards => features}/PromoteRc/PromoteRc.tsx (100%) rename plugins/github-release-manager/src/{cards => features}/PromoteRc/PromoteRcBody.test.tsx (100%) rename plugins/github-release-manager/src/{cards => features}/PromoteRc/PromoteRcBody.tsx (100%) rename plugins/github-release-manager/src/{cards => features}/PromoteRc/hooks/usePromoteRc.test.ts (100%) rename plugins/github-release-manager/src/{cards => features}/PromoteRc/hooks/usePromoteRc.ts (100%) rename plugins/github-release-manager/src/{cards => features}/RepoDetailsForm/Owner.test.tsx (100%) rename plugins/github-release-manager/src/{cards => features}/RepoDetailsForm/Owner.tsx (100%) rename plugins/github-release-manager/src/{cards => features}/RepoDetailsForm/Repo.test.tsx (100%) rename plugins/github-release-manager/src/{cards => features}/RepoDetailsForm/Repo.tsx (100%) rename plugins/github-release-manager/src/{cards => features}/RepoDetailsForm/RepoDetailsForm.tsx (100%) rename plugins/github-release-manager/src/{cards => features}/RepoDetailsForm/VersioningStrategy.test.tsx (100%) rename plugins/github-release-manager/src/{cards => features}/RepoDetailsForm/VersioningStrategy.tsx (100%) rename plugins/github-release-manager/src/{cards => features}/RepoDetailsForm/styles.ts (100%) diff --git a/plugins/github-release-manager/README.md b/plugins/github-release-manager/README.md index 43c842bc26..2a99912576 100644 --- a/plugins/github-release-manager/README.md +++ b/plugins/github-release-manager/README.md @@ -10,7 +10,7 @@ What `GRM` does is manage your **[releases](https://docs.github.com/en/github/ad `GRM` is built with industry standards in mind and the flow is as follows: -![](./src/cards/info/flow.png) +![](./src/features/Info/flow.png) > **GitHub**: The source control system where releases reside in a practical sense. Read more about [GitHub releases](https://docs.github.com/en/github/administering-a-repository/managing-releases-in-a-repository). (Note that this plugin works just as well with GitHub Enterprise.) > diff --git a/plugins/github-release-manager/src/GitHubReleaseManager.tsx b/plugins/github-release-manager/src/GitHubReleaseManager.tsx index ff77e8bbe4..348d4a0578 100644 --- a/plugins/github-release-manager/src/GitHubReleaseManager.tsx +++ b/plugins/github-release-manager/src/GitHubReleaseManager.tsx @@ -25,14 +25,14 @@ import { ComponentConfigPatch, ComponentConfigPromoteRc, } from './types/types'; -import { Cards } from './cards/Cards'; +import { Features } from './features/Features'; import { CenteredCircularProgress } from './components/CenteredCircularProgress'; import { githubReleaseManagerApiRef } from './api/serviceApiRef'; import { InfoCardPlus } from './components/InfoCardPlus'; import { isProjectValid } from './helpers/isProjectValid'; import { PluginApiClientContext } from './contexts/PluginApiClientContext'; import { ProjectContext, Project } from './contexts/ProjectContext'; -import { RepoDetailsForm } from './cards/RepoDetailsForm/RepoDetailsForm'; +import { RepoDetailsForm } from './features/RepoDetailsForm/RepoDetailsForm'; import { useQueryHandler } from './hooks/useQueryHandler'; import { useStyles } from './styles/styles'; @@ -90,7 +90,9 @@ export function GitHubReleaseManager(props: GitHubReleaseManagerProps) { - {isProjectValid(project) && } + {isProjectValid(project) && ( + + )}
diff --git a/plugins/github-release-manager/src/components/InfoCardPlus.test.tsx b/plugins/github-release-manager/src/components/InfoCardPlus.test.tsx index efd30b67da..01ed7aa4e8 100644 --- a/plugins/github-release-manager/src/components/InfoCardPlus.test.tsx +++ b/plugins/github-release-manager/src/components/InfoCardPlus.test.tsx @@ -24,6 +24,6 @@ describe('InfoCardPlus', () => { it('render InfoCardPlus', () => { const { getByTestId } = render(); - expect(getByTestId(TEST_IDS.info.infoCardPlus)).toBeInTheDocument(); + expect(getByTestId(TEST_IDS.info.infoFeaturePlus)).toBeInTheDocument(); }); }); diff --git a/plugins/github-release-manager/src/components/InfoCardPlus.tsx b/plugins/github-release-manager/src/components/InfoCardPlus.tsx index 38e3c5f388..fe2da57402 100644 --- a/plugins/github-release-manager/src/components/InfoCardPlus.tsx +++ b/plugins/github-release-manager/src/components/InfoCardPlus.tsx @@ -21,7 +21,7 @@ import { makeStyles } from '@material-ui/core'; import { TEST_IDS } from '../test-helpers/test-ids'; const useStyles = makeStyles(() => ({ - card: { + feature: { marginBottom: '3em', }, })); @@ -32,9 +32,9 @@ export const InfoCardPlus = ({ children }: { children?: React.ReactNode }) => { return (
- {children} + {children}
); }; diff --git a/plugins/github-release-manager/src/cards/CreateRc/CreateRc.test.tsx b/plugins/github-release-manager/src/features/CreateRc/CreateRc.test.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/CreateRc/CreateRc.test.tsx rename to plugins/github-release-manager/src/features/CreateRc/CreateRc.test.tsx diff --git a/plugins/github-release-manager/src/cards/CreateRc/CreateRc.tsx b/plugins/github-release-manager/src/features/CreateRc/CreateRc.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/CreateRc/CreateRc.tsx rename to plugins/github-release-manager/src/features/CreateRc/CreateRc.tsx diff --git a/plugins/github-release-manager/src/cards/CreateRc/hooks/useCreateRc.test.tsx b/plugins/github-release-manager/src/features/CreateRc/hooks/useCreateRc.test.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/CreateRc/hooks/useCreateRc.test.tsx rename to plugins/github-release-manager/src/features/CreateRc/hooks/useCreateRc.test.tsx diff --git a/plugins/github-release-manager/src/cards/CreateRc/hooks/useCreateRc.ts b/plugins/github-release-manager/src/features/CreateRc/hooks/useCreateRc.ts similarity index 100% rename from plugins/github-release-manager/src/cards/CreateRc/hooks/useCreateRc.ts rename to plugins/github-release-manager/src/features/CreateRc/hooks/useCreateRc.ts diff --git a/plugins/github-release-manager/src/cards/Cards.test.tsx b/plugins/github-release-manager/src/features/Features.test.tsx similarity index 84% rename from plugins/github-release-manager/src/cards/Cards.test.tsx rename to plugins/github-release-manager/src/features/Features.test.tsx index 8546c09cef..fd81b276cf 100644 --- a/plugins/github-release-manager/src/cards/Cards.test.tsx +++ b/plugins/github-release-manager/src/features/Features.test.tsx @@ -31,12 +31,12 @@ jest.mock('../contexts/ProjectContext', () => ({ }), })); -import { Cards } from './Cards'; +import { Features } from './Features'; -describe('Cards', () => { - it('should omit cards omitted via configuration', async () => { +describe('Features', () => { + it('should omit features omitted via configuration', async () => { const { getByTestId } = render( - { : A GitHub release intended for end users

+ `); }); diff --git a/plugins/github-release-manager/src/cards/Cards.tsx b/plugins/github-release-manager/src/features/Features.tsx similarity index 99% rename from plugins/github-release-manager/src/cards/Cards.tsx rename to plugins/github-release-manager/src/features/Features.tsx index e47181ba4a..673c9f5696 100644 --- a/plugins/github-release-manager/src/cards/Cards.tsx +++ b/plugins/github-release-manager/src/features/Features.tsx @@ -31,7 +31,7 @@ import { useProjectContext } from '../contexts/ProjectContext'; import { useVersioningStrategyMatchesRepoTags } from '../hooks/useVersioningStrategyMatchesRepoTags'; import { validateTagName } from '../helpers/tagParts/validateTagName'; -export function Cards({ +export function Features({ components, }: { components: GitHubReleaseManagerProps['components']; diff --git a/plugins/github-release-manager/src/cards/Info/Info.test.tsx b/plugins/github-release-manager/src/features/Info/Info.test.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/Info/Info.test.tsx rename to plugins/github-release-manager/src/features/Info/Info.test.tsx diff --git a/plugins/github-release-manager/src/cards/Info/Info.tsx b/plugins/github-release-manager/src/features/Info/Info.tsx similarity index 88% rename from plugins/github-release-manager/src/cards/Info/Info.tsx rename to plugins/github-release-manager/src/features/Info/Info.tsx index 72a61a38e6..ff431930d8 100644 --- a/plugins/github-release-manager/src/cards/Info/Info.tsx +++ b/plugins/github-release-manager/src/features/Info/Info.tsx @@ -14,19 +14,20 @@ * limitations under the License. */ -import React from 'react'; -import { Link, Typography } from '@material-ui/core'; +import React, { useState } from 'react'; +import { Link, Typography, Button } from '@material-ui/core'; -import { Differ } from '../../components/Differ'; -import { InfoCardPlus } from '../../components/InfoCardPlus'; -import { TEST_IDS } from '../../test-helpers/test-ids'; -import { useProjectContext } from '../../contexts/ProjectContext'; -import { useStyles } from '../../styles/styles'; -import flowImage from './flow.png'; import { GetBranchResult, GetLatestReleaseResult, } from '../../api/PluginApiClient'; +import { Differ } from '../../components/Differ'; +import { InfoCardPlus } from '../../components/InfoCardPlus'; +import { Stats } from '../../components/Stats/Stats'; +import { TEST_IDS } from '../../test-helpers/test-ids'; +import { useProjectContext } from '../../contexts/ProjectContext'; +import { useStyles } from '../../styles/styles'; +import flowImage from './flow.png'; interface InfoCardProps { releaseBranch: GetBranchResult | null; @@ -36,6 +37,7 @@ interface InfoCardProps { export const Info = ({ releaseBranch, latestRelease }: InfoCardProps) => { const { project } = useProjectContext(); const classes = useStyles(); + const [showStats, setShowStats] = useState(false); return ( @@ -63,6 +65,15 @@ export const Info = ({ releaseBranch, latestRelease }: InfoCardProps) => { Release Version: A GitHub release intended for end users + + + {showStats && }
diff --git a/plugins/github-release-manager/src/cards/Info/flow.png b/plugins/github-release-manager/src/features/Info/flow.png similarity index 100% rename from plugins/github-release-manager/src/cards/Info/flow.png rename to plugins/github-release-manager/src/features/Info/flow.png diff --git a/plugins/github-release-manager/src/cards/Patch/Patch.test.tsx b/plugins/github-release-manager/src/features/Patch/Patch.test.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/Patch/Patch.test.tsx rename to plugins/github-release-manager/src/features/Patch/Patch.test.tsx diff --git a/plugins/github-release-manager/src/cards/Patch/Patch.tsx b/plugins/github-release-manager/src/features/Patch/Patch.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/Patch/Patch.tsx rename to plugins/github-release-manager/src/features/Patch/Patch.tsx diff --git a/plugins/github-release-manager/src/cards/Patch/PatchBody.test.tsx b/plugins/github-release-manager/src/features/Patch/PatchBody.test.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/Patch/PatchBody.test.tsx rename to plugins/github-release-manager/src/features/Patch/PatchBody.test.tsx diff --git a/plugins/github-release-manager/src/cards/Patch/PatchBody.tsx b/plugins/github-release-manager/src/features/Patch/PatchBody.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/Patch/PatchBody.tsx rename to plugins/github-release-manager/src/features/Patch/PatchBody.tsx diff --git a/plugins/github-release-manager/src/cards/Patch/hooks/usePatch.test.ts b/plugins/github-release-manager/src/features/Patch/hooks/usePatch.test.ts similarity index 100% rename from plugins/github-release-manager/src/cards/Patch/hooks/usePatch.test.ts rename to plugins/github-release-manager/src/features/Patch/hooks/usePatch.test.ts diff --git a/plugins/github-release-manager/src/cards/Patch/hooks/usePatch.ts b/plugins/github-release-manager/src/features/Patch/hooks/usePatch.ts similarity index 100% rename from plugins/github-release-manager/src/cards/Patch/hooks/usePatch.ts rename to plugins/github-release-manager/src/features/Patch/hooks/usePatch.ts diff --git a/plugins/github-release-manager/src/cards/PromoteRc/PromoteRc.test.tsx b/plugins/github-release-manager/src/features/PromoteRc/PromoteRc.test.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/PromoteRc/PromoteRc.test.tsx rename to plugins/github-release-manager/src/features/PromoteRc/PromoteRc.test.tsx diff --git a/plugins/github-release-manager/src/cards/PromoteRc/PromoteRc.tsx b/plugins/github-release-manager/src/features/PromoteRc/PromoteRc.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/PromoteRc/PromoteRc.tsx rename to plugins/github-release-manager/src/features/PromoteRc/PromoteRc.tsx diff --git a/plugins/github-release-manager/src/cards/PromoteRc/PromoteRcBody.test.tsx b/plugins/github-release-manager/src/features/PromoteRc/PromoteRcBody.test.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/PromoteRc/PromoteRcBody.test.tsx rename to plugins/github-release-manager/src/features/PromoteRc/PromoteRcBody.test.tsx diff --git a/plugins/github-release-manager/src/cards/PromoteRc/PromoteRcBody.tsx b/plugins/github-release-manager/src/features/PromoteRc/PromoteRcBody.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/PromoteRc/PromoteRcBody.tsx rename to plugins/github-release-manager/src/features/PromoteRc/PromoteRcBody.tsx diff --git a/plugins/github-release-manager/src/cards/PromoteRc/hooks/usePromoteRc.test.ts b/plugins/github-release-manager/src/features/PromoteRc/hooks/usePromoteRc.test.ts similarity index 100% rename from plugins/github-release-manager/src/cards/PromoteRc/hooks/usePromoteRc.test.ts rename to plugins/github-release-manager/src/features/PromoteRc/hooks/usePromoteRc.test.ts diff --git a/plugins/github-release-manager/src/cards/PromoteRc/hooks/usePromoteRc.ts b/plugins/github-release-manager/src/features/PromoteRc/hooks/usePromoteRc.ts similarity index 100% rename from plugins/github-release-manager/src/cards/PromoteRc/hooks/usePromoteRc.ts rename to plugins/github-release-manager/src/features/PromoteRc/hooks/usePromoteRc.ts diff --git a/plugins/github-release-manager/src/cards/RepoDetailsForm/Owner.test.tsx b/plugins/github-release-manager/src/features/RepoDetailsForm/Owner.test.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/RepoDetailsForm/Owner.test.tsx rename to plugins/github-release-manager/src/features/RepoDetailsForm/Owner.test.tsx diff --git a/plugins/github-release-manager/src/cards/RepoDetailsForm/Owner.tsx b/plugins/github-release-manager/src/features/RepoDetailsForm/Owner.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/RepoDetailsForm/Owner.tsx rename to plugins/github-release-manager/src/features/RepoDetailsForm/Owner.tsx diff --git a/plugins/github-release-manager/src/cards/RepoDetailsForm/Repo.test.tsx b/plugins/github-release-manager/src/features/RepoDetailsForm/Repo.test.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/RepoDetailsForm/Repo.test.tsx rename to plugins/github-release-manager/src/features/RepoDetailsForm/Repo.test.tsx diff --git a/plugins/github-release-manager/src/cards/RepoDetailsForm/Repo.tsx b/plugins/github-release-manager/src/features/RepoDetailsForm/Repo.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/RepoDetailsForm/Repo.tsx rename to plugins/github-release-manager/src/features/RepoDetailsForm/Repo.tsx diff --git a/plugins/github-release-manager/src/cards/RepoDetailsForm/RepoDetailsForm.tsx b/plugins/github-release-manager/src/features/RepoDetailsForm/RepoDetailsForm.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/RepoDetailsForm/RepoDetailsForm.tsx rename to plugins/github-release-manager/src/features/RepoDetailsForm/RepoDetailsForm.tsx diff --git a/plugins/github-release-manager/src/cards/RepoDetailsForm/VersioningStrategy.test.tsx b/plugins/github-release-manager/src/features/RepoDetailsForm/VersioningStrategy.test.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/RepoDetailsForm/VersioningStrategy.test.tsx rename to plugins/github-release-manager/src/features/RepoDetailsForm/VersioningStrategy.test.tsx diff --git a/plugins/github-release-manager/src/cards/RepoDetailsForm/VersioningStrategy.tsx b/plugins/github-release-manager/src/features/RepoDetailsForm/VersioningStrategy.tsx similarity index 100% rename from plugins/github-release-manager/src/cards/RepoDetailsForm/VersioningStrategy.tsx rename to plugins/github-release-manager/src/features/RepoDetailsForm/VersioningStrategy.tsx diff --git a/plugins/github-release-manager/src/cards/RepoDetailsForm/styles.ts b/plugins/github-release-manager/src/features/RepoDetailsForm/styles.ts similarity index 100% rename from plugins/github-release-manager/src/cards/RepoDetailsForm/styles.ts rename to plugins/github-release-manager/src/features/RepoDetailsForm/styles.ts diff --git a/plugins/github-release-manager/src/test-helpers/test-ids.test.ts b/plugins/github-release-manager/src/test-helpers/test-ids.test.ts index 5adcebc3d9..199a7a5745 100644 --- a/plugins/github-release-manager/src/test-helpers/test-ids.test.ts +++ b/plugins/github-release-manager/src/test-helpers/test-ids.test.ts @@ -67,7 +67,7 @@ describe('test-ids', () => { }, "info": Object { "info": "grm--info", - "infoCardPlus": "grm--info-card-plus", + "infoFeaturePlus": "grm--info-feature-plus", }, "patch": Object { "body": "grm--patch-body", diff --git a/plugins/github-release-manager/src/test-helpers/test-ids.ts b/plugins/github-release-manager/src/test-helpers/test-ids.ts index 1beb6aec10..a5b38e93a1 100644 --- a/plugins/github-release-manager/src/test-helpers/test-ids.ts +++ b/plugins/github-release-manager/src/test-helpers/test-ids.ts @@ -17,7 +17,7 @@ export const TEST_IDS = { info: { info: 'grm--info', - infoCardPlus: 'grm--info-card-plus', + infoFeaturePlus: 'grm--info-feature-plus', }, createRc: { cta: 'grm--create-rc--cta', From c33462b440f1ae80443886244cc596f156c7d1b0 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Fri, 23 Apr 2021 00:28:53 +0200 Subject: [PATCH 071/140] Introduce Stats Feature Create two new API calls, getAllTags & getAllReleases Rename "components" prop to more suitable "features" Signed-off-by: Erik Engervall --- plugins/github-release-manager/README.md | 2 +- .../src/GitHubReleaseManager.tsx | 7 +- .../src/api/PluginApiClient.test.ts | 4 + .../src/api/PluginApiClient.ts | 63 ++++++ .../src/features/Features.test.tsx | 16 +- .../src/features/Features.tsx | 19 +- .../src/features/Info/Info.tsx | 49 +++-- .../src/features/Stats/DialogBody.tsx | 194 ++++++++++++++++++ .../src/features/Stats/DialogTitle.tsx | 65 ++++++ .../src/features/Stats/Row.tsx | 131 ++++++++++++ .../src/features/Stats/Stats.tsx | 66 ++++++ .../src/features/Stats/Warn.tsx | 56 +++++ .../src/features/Stats/getMappedReleases.tsx | 74 +++++++ .../src/features/Stats/getSummary.tsx | 45 ++++ .../src/features/Stats/getTags.tsx | 75 +++++++ .../src/features/Stats/hooks/useGetStats.ts | 47 +++++ .../src/test-helpers/test-helpers.test.ts | 4 + .../src/test-helpers/test-helpers.ts | 10 + 18 files changed, 880 insertions(+), 47 deletions(-) create mode 100644 plugins/github-release-manager/src/features/Stats/DialogBody.tsx create mode 100644 plugins/github-release-manager/src/features/Stats/DialogTitle.tsx create mode 100644 plugins/github-release-manager/src/features/Stats/Row.tsx create mode 100644 plugins/github-release-manager/src/features/Stats/Stats.tsx create mode 100644 plugins/github-release-manager/src/features/Stats/Warn.tsx create mode 100644 plugins/github-release-manager/src/features/Stats/getMappedReleases.tsx create mode 100644 plugins/github-release-manager/src/features/Stats/getSummary.tsx create mode 100644 plugins/github-release-manager/src/features/Stats/getTags.tsx create mode 100644 plugins/github-release-manager/src/features/Stats/hooks/useGetStats.ts 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'); + }), + }, }; From 4101cecb42ba7581e822089390b49fe5ab1a441e Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Fri, 23 Apr 2021 00:30:47 +0200 Subject: [PATCH 072/140] Minor adjustments for "components" -> "features" renaming Signed-off-by: Erik Engervall --- plugins/github-release-manager/dev/index.tsx | 4 ++-- plugins/github-release-manager/src/features/Features.test.tsx | 2 +- .../github-release-manager/src/features/Info/Info.test.tsx | 1 + 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/plugins/github-release-manager/dev/index.tsx b/plugins/github-release-manager/dev/index.tsx index bca24d56a4..31a6a9a563 100644 --- a/plugins/github-release-manager/dev/index.tsx +++ b/plugins/github-release-manager/dev/index.tsx @@ -74,7 +74,7 @@ createDevApp() Dev notes - Each components can be omitted + Each feature can be omitted Success callbacks can also be added @@ -84,7 +84,7 @@ createDevApp() repo: 'playground-semver', versioningStrategy: 'semver', }} - components={{ + features={{ createRc: { successCb: ({ comparisonUrl, diff --git a/plugins/github-release-manager/src/features/Features.test.tsx b/plugins/github-release-manager/src/features/Features.test.tsx index bccca0e283..d53f542cef 100644 --- a/plugins/github-release-manager/src/features/Features.test.tsx +++ b/plugins/github-release-manager/src/features/Features.test.tsx @@ -37,7 +37,7 @@ describe('Features', () => { it('should omit features omitted via configuration', async () => { const { getByTestId } = render( { , ); From 5e65ce14ff0a649f292e0d061bf2dd8ccfae08d7 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Fri, 23 Apr 2021 00:44:28 +0200 Subject: [PATCH 073/140] Improve coverage of plugin.test.ts Signed-off-by: Erik Engervall --- plugins/github-release-manager/src/plugin.test.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/plugins/github-release-manager/src/plugin.test.ts b/plugins/github-release-manager/src/plugin.test.ts index a13dda5688..60ab101d11 100644 --- a/plugins/github-release-manager/src/plugin.test.ts +++ b/plugins/github-release-manager/src/plugin.test.ts @@ -14,10 +14,16 @@ * limitations under the License. */ -import { gitHubReleaseManagerPlugin } from './plugin'; +import * as plugin from './plugin'; describe('github-release-manager', () => { - it('should export plugin', () => { - expect(gitHubReleaseManagerPlugin).toBeDefined(); + it('should export plugin & friends', () => { + expect(Object.keys(plugin)).toMatchInlineSnapshot(` + Array [ + "githubReleaseManagerApiRef", + "gitHubReleaseManagerPlugin", + "GitHubReleaseManagerPage", + ] + `); }); }); From c865a349e4b7e5a74cd34847890fa9b4535a4898 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Fri, 23 Apr 2021 00:51:23 +0200 Subject: [PATCH 074/140] Move usePluginApiClientContext & useProjectContext into hooks where applicable Signed-off-by: Erik Engervall --- .../src/features/CreateRc/CreateRc.test.tsx | 6 ------ .../src/features/CreateRc/CreateRc.tsx | 3 --- .../CreateRc/hooks/useCreateRc.test.tsx | 8 ++++++-- .../src/features/CreateRc/hooks/useCreateRc.ts | 6 +++--- .../src/features/Patch/PatchBody.tsx | 1 - .../src/features/Patch/hooks/usePatch.test.ts | 8 ++++++-- .../src/features/Patch/hooks/usePatch.ts | 5 ++--- .../features/PromoteRc/PromoteRcBody.test.tsx | 16 +--------------- .../src/features/PromoteRc/PromoteRcBody.tsx | 6 ------ .../PromoteRc/hooks/usePromoteRc.test.ts | 17 ++++++++++++----- .../features/PromoteRc/hooks/usePromoteRc.ts | 14 +++++--------- 11 files changed, 35 insertions(+), 55 deletions(-) diff --git a/plugins/github-release-manager/src/features/CreateRc/CreateRc.test.tsx b/plugins/github-release-manager/src/features/CreateRc/CreateRc.test.tsx index 98bf4903ac..3c4a324b25 100644 --- a/plugins/github-release-manager/src/features/CreateRc/CreateRc.test.tsx +++ b/plugins/github-release-manager/src/features/CreateRc/CreateRc.test.tsx @@ -18,7 +18,6 @@ import React from 'react'; import { render } from '@testing-library/react'; import { - mockApiClient, mockCalverProject, mockNextGitHubInfoSemver, mockReleaseBranch, @@ -29,11 +28,6 @@ import { import { TEST_IDS } from '../../test-helpers/test-ids'; import { useCreateRc } from './hooks/useCreateRc'; -jest.mock('../../contexts/PluginApiClientContext', () => ({ - usePluginApiClientContext: () => ({ - pluginApiClient: mockApiClient, - }), -})); jest.mock('../../contexts/ProjectContext', () => ({ useProjectContext: jest.fn(() => ({ project: mockCalverProject, diff --git a/plugins/github-release-manager/src/features/CreateRc/CreateRc.tsx b/plugins/github-release-manager/src/features/CreateRc/CreateRc.tsx index a3c3d33a26..ec3faeea83 100644 --- a/plugins/github-release-manager/src/features/CreateRc/CreateRc.tsx +++ b/plugins/github-release-manager/src/features/CreateRc/CreateRc.tsx @@ -38,7 +38,6 @@ import { ResponseStepDialog } from '../../components/ResponseStepDialog/Response import { SEMVER_PARTS } from '../../constants/constants'; import { TEST_IDS } from '../../test-helpers/test-ids'; import { useCreateRc } from './hooks/useCreateRc'; -import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext'; import { useProjectContext } from '../../contexts/ProjectContext'; import { useStyles } from '../../styles/styles'; @@ -67,7 +66,6 @@ export const CreateRc = ({ releaseBranch, successCb, }: CreateRcProps) => { - const { pluginApiClient } = usePluginApiClientContext(); const { project } = useProjectContext(); const classes = useStyles(); @@ -88,7 +86,6 @@ export const CreateRc = ({ defaultBranch, latestRelease, nextGitHubInfo, - pluginApiClient, project, successCb, }); diff --git a/plugins/github-release-manager/src/features/CreateRc/hooks/useCreateRc.test.tsx b/plugins/github-release-manager/src/features/CreateRc/hooks/useCreateRc.test.tsx index 8da92a4f0e..cf4fa0f5ed 100644 --- a/plugins/github-release-manager/src/features/CreateRc/hooks/useCreateRc.test.tsx +++ b/plugins/github-release-manager/src/features/CreateRc/hooks/useCreateRc.test.tsx @@ -26,6 +26,12 @@ import { } from '../../../test-helpers/test-helpers'; import { useCreateRc } from './useCreateRc'; +jest.mock('../../../contexts/PluginApiClientContext', () => ({ + usePluginApiClientContext: () => ({ + pluginApiClient: mockApiClient, + }), +})); + describe('useCreateRc', () => { beforeEach(jest.clearAllMocks); @@ -35,7 +41,6 @@ describe('useCreateRc', () => { defaultBranch: mockDefaultBranch, latestRelease: mockReleaseVersionCalver, nextGitHubInfo: mockNextGitHubInfoCalver, - pluginApiClient: mockApiClient, project: mockCalverProject, }), ); @@ -54,7 +59,6 @@ describe('useCreateRc', () => { defaultBranch: mockDefaultBranch, latestRelease: mockReleaseVersionCalver, nextGitHubInfo: mockNextGitHubInfoCalver, - pluginApiClient: mockApiClient, project: mockCalverProject, successCb: jest.fn(), }), diff --git a/plugins/github-release-manager/src/features/CreateRc/hooks/useCreateRc.ts b/plugins/github-release-manager/src/features/CreateRc/hooks/useCreateRc.ts index 2b1b0b688c..30d5a8894f 100644 --- a/plugins/github-release-manager/src/features/CreateRc/hooks/useCreateRc.ts +++ b/plugins/github-release-manager/src/features/CreateRc/hooks/useCreateRc.ts @@ -20,19 +20,18 @@ import { useAsync, useAsyncFn } from 'react-use'; import { GetLatestReleaseResult, GetRepositoryResult, - IPluginApiClient, } from '../../../api/PluginApiClient'; import { CardHook, ComponentConfigCreateRc } from '../../../types/types'; import { getRcGitHubInfo } from '../../../helpers/getRcGitHubInfo'; import { GitHubReleaseManagerError } from '../../../errors/GitHubReleaseManagerError'; import { Project } from '../../../contexts/ProjectContext'; import { useResponseSteps } from '../../../hooks/useResponseSteps'; +import { usePluginApiClientContext } from '../../../contexts/PluginApiClientContext'; interface CreateRC { defaultBranch: GetRepositoryResult['defaultBranch']; latestRelease: GetLatestReleaseResult; nextGitHubInfo: ReturnType; - pluginApiClient: IPluginApiClient; project: Project; successCb?: ComponentConfigCreateRc['successCb']; } @@ -41,10 +40,11 @@ export function useCreateRc({ defaultBranch, latestRelease, nextGitHubInfo, - pluginApiClient, project, successCb, }: CreateRC): CardHook { + const { pluginApiClient } = usePluginApiClientContext(); + if (nextGitHubInfo.error) { throw new GitHubReleaseManagerError( `Unexpected error: ${ diff --git a/plugins/github-release-manager/src/features/Patch/PatchBody.tsx b/plugins/github-release-manager/src/features/Patch/PatchBody.tsx index ec7b6ab1f3..7c99e66f9e 100644 --- a/plugins/github-release-manager/src/features/Patch/PatchBody.tsx +++ b/plugins/github-release-manager/src/features/Patch/PatchBody.tsx @@ -94,7 +94,6 @@ export const PatchBody = ({ const { progress, responseSteps, run, runInvoked } = usePatch({ bumpedTag, latestRelease, - pluginApiClient, project, tagParts, successCb, diff --git a/plugins/github-release-manager/src/features/Patch/hooks/usePatch.test.ts b/plugins/github-release-manager/src/features/Patch/hooks/usePatch.test.ts index 240d39292c..be25b84809 100644 --- a/plugins/github-release-manager/src/features/Patch/hooks/usePatch.test.ts +++ b/plugins/github-release-manager/src/features/Patch/hooks/usePatch.test.ts @@ -27,6 +27,12 @@ import { } from '../../../test-helpers/test-helpers'; import { usePatch } from './usePatch'; +jest.mock('../../../contexts/PluginApiClientContext', () => ({ + usePluginApiClientContext: () => ({ + pluginApiClient: mockApiClient, + }), +})); + describe('patch', () => { beforeEach(jest.clearAllMocks); @@ -35,7 +41,6 @@ describe('patch', () => { usePatch({ bumpedTag: mockBumpedTag, latestRelease: mockReleaseVersionCalver, - pluginApiClient: mockApiClient, project: mockCalverProject, tagParts: mockTagParts, }), @@ -54,7 +59,6 @@ describe('patch', () => { usePatch({ bumpedTag: mockBumpedTag, latestRelease: mockReleaseVersionCalver, - pluginApiClient: mockApiClient, project: mockCalverProject, tagParts: mockTagParts, successCb: jest.fn(), diff --git a/plugins/github-release-manager/src/features/Patch/hooks/usePatch.ts b/plugins/github-release-manager/src/features/Patch/hooks/usePatch.ts index af7e890ca9..06001f1771 100644 --- a/plugins/github-release-manager/src/features/Patch/hooks/usePatch.ts +++ b/plugins/github-release-manager/src/features/Patch/hooks/usePatch.ts @@ -20,18 +20,17 @@ import { useAsync, useAsyncFn } from 'react-use'; import { GetLatestReleaseResult, GetRecentCommitsResultSingle, - IPluginApiClient, } from '../../../api/PluginApiClient'; import { CalverTagParts } from '../../../helpers/tagParts/getCalverTagParts'; import { ComponentConfigPatch, CardHook } from '../../../types/types'; import { Project } from '../../../contexts/ProjectContext'; import { SemverTagParts } from '../../../helpers/tagParts/getSemverTagParts'; +import { usePluginApiClientContext } from '../../../contexts/PluginApiClientContext'; import { useResponseSteps } from '../../../hooks/useResponseSteps'; interface Patch { bumpedTag: string; latestRelease: NonNullable; - pluginApiClient: IPluginApiClient; project: Project; tagParts: NonNullable; successCb?: ComponentConfigPatch['successCb']; @@ -41,11 +40,11 @@ interface Patch { export function usePatch({ bumpedTag, latestRelease, - pluginApiClient, project, tagParts, successCb, }: Patch): CardHook { + const { pluginApiClient } = usePluginApiClientContext(); const { responseSteps, addStepToResponseSteps, diff --git a/plugins/github-release-manager/src/features/PromoteRc/PromoteRcBody.test.tsx b/plugins/github-release-manager/src/features/PromoteRc/PromoteRcBody.test.tsx index 51724a1e22..2d67832587 100644 --- a/plugins/github-release-manager/src/features/PromoteRc/PromoteRcBody.test.tsx +++ b/plugins/github-release-manager/src/features/PromoteRc/PromoteRcBody.test.tsx @@ -17,23 +17,9 @@ import React from 'react'; import { render } from '@testing-library/react'; -import { - mockApiClient, - mockCalverProject, - mockReleaseCandidateCalver, -} from '../../test-helpers/test-helpers'; +import { mockReleaseCandidateCalver } from '../../test-helpers/test-helpers'; import { TEST_IDS } from '../../test-helpers/test-ids'; -jest.mock('../../contexts/PluginApiClientContext', () => ({ - usePluginApiClientContext: () => ({ - pluginApiClient: mockApiClient, - }), -})); -jest.mock('../../contexts/ProjectContext', () => ({ - useProjectContext: () => ({ - project: mockCalverProject, - }), -})); jest.mock('./hooks/usePromoteRc', () => ({ usePromoteRc: () => ({ run: jest.fn(), diff --git a/plugins/github-release-manager/src/features/PromoteRc/PromoteRcBody.tsx b/plugins/github-release-manager/src/features/PromoteRc/PromoteRcBody.tsx index bfdfd1a976..5dfe4b13aa 100644 --- a/plugins/github-release-manager/src/features/PromoteRc/PromoteRcBody.tsx +++ b/plugins/github-release-manager/src/features/PromoteRc/PromoteRcBody.tsx @@ -22,8 +22,6 @@ import { Differ } from '../../components/Differ'; import { GetLatestReleaseResult } from '../../api/PluginApiClient'; import { ResponseStepDialog } from '../../components/ResponseStepDialog/ResponseStepDialog'; import { TEST_IDS } from '../../test-helpers/test-ids'; -import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext'; -import { useProjectContext } from '../../contexts/ProjectContext'; import { usePromoteRc } from './hooks/usePromoteRc'; import { useStyles } from '../../styles/styles'; @@ -33,14 +31,10 @@ interface PromoteRcBodyProps { } export const PromoteRcBody = ({ rcRelease, successCb }: PromoteRcBodyProps) => { - const { pluginApiClient } = usePluginApiClientContext(); - const { project } = useProjectContext(); const classes = useStyles(); const releaseVersion = rcRelease.tagName.replace('rc-', 'version-'); const { progress, responseSteps, run, runInvoked } = usePromoteRc({ - pluginApiClient, - project, rcRelease, releaseVersion, successCb, diff --git a/plugins/github-release-manager/src/features/PromoteRc/hooks/usePromoteRc.test.ts b/plugins/github-release-manager/src/features/PromoteRc/hooks/usePromoteRc.test.ts index 4803f2a551..4b7149ba7b 100644 --- a/plugins/github-release-manager/src/features/PromoteRc/hooks/usePromoteRc.test.ts +++ b/plugins/github-release-manager/src/features/PromoteRc/hooks/usePromoteRc.test.ts @@ -19,19 +19,28 @@ import { waitFor } from '@testing-library/react'; import { mockApiClient, + mockCalverProject, mockReleaseCandidateCalver, - mockSemverProject, } from '../../../test-helpers/test-helpers'; import { usePromoteRc } from './usePromoteRc'; +jest.mock('../../../contexts/PluginApiClientContext', () => ({ + usePluginApiClientContext: () => ({ + pluginApiClient: mockApiClient, + }), +})); +jest.mock('../../../contexts/ProjectContext', () => ({ + useProjectContext: () => ({ + project: mockCalverProject, + }), +})); + describe('usePromoteRc', () => { beforeEach(jest.clearAllMocks); it('should return the expected responseSteps and progress', async () => { const { result } = renderHook(() => usePromoteRc({ - pluginApiClient: mockApiClient, - project: mockSemverProject, rcRelease: mockReleaseCandidateCalver, releaseVersion: 'version-1.2.3', }), @@ -48,8 +57,6 @@ describe('usePromoteRc', () => { it('should return the expected responseSteps and progress (with successCb)', async () => { const { result } = renderHook(() => usePromoteRc({ - pluginApiClient: mockApiClient, - project: mockSemverProject, rcRelease: mockReleaseCandidateCalver, releaseVersion: 'version-1.2.3', successCb: jest.fn(), diff --git a/plugins/github-release-manager/src/features/PromoteRc/hooks/usePromoteRc.ts b/plugins/github-release-manager/src/features/PromoteRc/hooks/usePromoteRc.ts index abcab2f480..4d1a9e4037 100644 --- a/plugins/github-release-manager/src/features/PromoteRc/hooks/usePromoteRc.ts +++ b/plugins/github-release-manager/src/features/PromoteRc/hooks/usePromoteRc.ts @@ -17,29 +17,25 @@ import { useState, useEffect } from 'react'; import { useAsync, useAsyncFn } from 'react-use'; -import { - GetLatestReleaseResult, - IPluginApiClient, -} from '../../../api/PluginApiClient'; +import { GetLatestReleaseResult } from '../../../api/PluginApiClient'; import { CardHook, ComponentConfigPromoteRc } from '../../../types/types'; -import { Project } from '../../../contexts/ProjectContext'; +import { useProjectContext } from '../../../contexts/ProjectContext'; +import { usePluginApiClientContext } from '../../../contexts/PluginApiClientContext'; import { useResponseSteps } from '../../../hooks/useResponseSteps'; interface PromoteRc { - pluginApiClient: IPluginApiClient; - project: Project; rcRelease: NonNullable; releaseVersion: string; successCb?: ComponentConfigPromoteRc['successCb']; } export function usePromoteRc({ - pluginApiClient, - project, rcRelease, releaseVersion, successCb, }: PromoteRc): CardHook { + const { pluginApiClient } = usePluginApiClientContext(); + const { project } = useProjectContext(); const { responseSteps, addStepToResponseSteps, From 10c95472459038a23b6c031acfd8b8cbcd26cb82 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Fri, 23 Apr 2021 02:55:10 +0200 Subject: [PATCH 075/140] Create getCommit hook When opening row, calculate diff in days from creating RC to being done with the Release Version Signed-off-by: Erik Engervall --- .../src/api/PluginApiClient.test.ts | 1 + .../src/api/PluginApiClient.ts | 82 +++++--- .../src/features/Stats/DialogBody.tsx | 81 +------- .../src/features/Stats/Row.tsx | 186 +++++++++++++----- .../src/features/Stats/Summary.tsx | 89 +++++++++ .../src/features/Stats/Warn.tsx | 4 +- .../Stats/helpers/getDecimalNumber.tsx | 27 +++ .../Stats/{ => helpers}/getMappedReleases.tsx | 24 ++- .../Stats/{ => helpers}/getSummary.tsx | 0 .../features/Stats/{ => helpers}/getTags.tsx | 46 +++-- .../src/features/Stats/hooks/useGetCommit.ts | 37 ++++ .../src/test-helpers/test-helpers.test.ts | 1 + .../src/test-helpers/test-helpers.ts | 4 + 13 files changed, 414 insertions(+), 168 deletions(-) create mode 100644 plugins/github-release-manager/src/features/Stats/Summary.tsx create mode 100644 plugins/github-release-manager/src/features/Stats/helpers/getDecimalNumber.tsx rename plugins/github-release-manager/src/features/Stats/{ => helpers}/getMappedReleases.tsx (71%) rename plugins/github-release-manager/src/features/Stats/{ => helpers}/getSummary.tsx (100%) rename plugins/github-release-manager/src/features/Stats/{ => helpers}/getTags.tsx (58%) create mode 100644 plugins/github-release-manager/src/features/Stats/hooks/useGetCommit.ts diff --git a/plugins/github-release-manager/src/api/PluginApiClient.test.ts b/plugins/github-release-manager/src/api/PluginApiClient.test.ts index 998e8bc4e8..867974b80b 100644 --- a/plugins/github-release-manager/src/api/PluginApiClient.test.ts +++ b/plugins/github-release-manager/src/api/PluginApiClient.test.ts @@ -60,6 +60,7 @@ describe('PluginApiClient', () => { "stats": Object { "getAllReleases": [Function], "getAllTags": [Function], + "getCommit": [Function], }, } `); diff --git a/plugins/github-release-manager/src/api/PluginApiClient.ts b/plugins/github-release-manager/src/api/PluginApiClient.ts index 31996f8882..033e074bcf 100644 --- a/plugins/github-release-manager/src/api/PluginApiClient.ts +++ b/plugins/github-release-manager/src/api/PluginApiClient.ts @@ -576,6 +576,7 @@ ${selectedPatchCommit.commit.message}`, return tags.map(tag => ({ tagName: tag.name, + sha: tag.commit.sha, })); }, @@ -598,6 +599,20 @@ ${selectedPatchCommit.commit.message}`, htmlUrl: release.html_url, })); }, + + getCommit: async ({ owner, repo, ref }: { ref: string } & OwnerRepo) => { + const { octokit } = await this.getOctokit(); + + const { data: commit } = await octokit.repos.getCommit({ + owner, + repo, + ref, + }); + + return { + createdAt: commit.commit.committer?.date, + }; + }, }; } @@ -660,28 +675,6 @@ 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<{ @@ -736,6 +729,9 @@ type GetBranch = ( }>; export type GetBranchResult = UnboxReturnedPromise; +/** + * CreateRc + */ type CreateRef = ( args: { mostRecentSha: string; @@ -771,6 +767,9 @@ type CreateRelease = ( }>; export type CreateReleaseResult = UnboxReturnedPromise; +/** + * Patch + */ type CreateTempCommit = ( args: { tagParts: SemverTagParts | CalverTagParts; @@ -876,6 +875,9 @@ type UpdateRelease = ( }>; export type UpdateReleaseResult = UnboxReturnedPromise; +/** + * PromoteRc + */ type PromoteRelease = ( args: { releaseId: NonNullable['id']; @@ -888,6 +890,41 @@ type PromoteRelease = ( }>; export type PromoteReleaseResult = UnboxReturnedPromise; +/** + * Stats + */ +type GetAllTags = ( + args: OwnerRepo, +) => Promise< + Array<{ + tagName: string; + sha: 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 GetCommit = ( + args: { + ref: string; + } & OwnerRepo, +) => Promise<{ + createdAt: string | undefined; +}>; +export type GetCommitResult = UnboxReturnedPromise; + export interface IPluginApiClient { getHost: GetHost; getRepoPath: GetRepoPath; @@ -920,5 +957,6 @@ export interface IPluginApiClient { stats: { getAllTags: GetAllTags; getAllReleases: GetAllReleases; + getCommit: GetCommit; }; } diff --git a/plugins/github-release-manager/src/features/Stats/DialogBody.tsx b/plugins/github-release-manager/src/features/Stats/DialogBody.tsx index 3c2f884417..d8b406e04b 100644 --- a/plugins/github-release-manager/src/features/Stats/DialogBody.tsx +++ b/plugins/github-release-manager/src/features/Stats/DialogBody.tsx @@ -26,17 +26,17 @@ import { 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 { getMappedReleases } from './helpers/getMappedReleases'; +import { getSummary } from './helpers/getSummary'; +import { getTags } from './helpers/getTags'; import { Row } from './Row'; import { useGetStats } from './hooks/useGetStats'; import { useProjectContext } from '../../contexts/ProjectContext'; import { Warn } from './Warn'; +import { Summary } from './Summary'; const useStyles = makeStyles({ table: { @@ -81,80 +81,9 @@ export function DialogBody() { }); } - 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, - )} - - - + diff --git a/plugins/github-release-manager/src/features/Stats/Row.tsx b/plugins/github-release-manager/src/features/Stats/Row.tsx index ea95f8e5c2..53ad5751da 100644 --- a/plugins/github-release-manager/src/features/Stats/Row.tsx +++ b/plugins/github-release-manager/src/features/Stats/Row.tsx @@ -15,7 +15,7 @@ */ import React, { useState } from 'react'; -import { DateTime } from 'luxon'; +import { DateTime, DurationObject } from 'luxon'; import { Box, Collapse, @@ -29,7 +29,9 @@ import { import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown'; import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp'; -import { getMappedReleases } from './getMappedReleases'; +import { getMappedReleases } from './helpers/getMappedReleases'; +import { useGetCommit } from './hooks/useGetCommit'; +import { CenteredCircularProgress } from '../../components/CenteredCircularProgress'; const useRowStyles = makeStyles({ root: { @@ -47,9 +49,6 @@ interface RowProps { 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 ( @@ -67,7 +66,7 @@ export function Row({ baseVersion, mappedRelease }: RowProps) { {baseVersion} - {isPrerelease ? ' (prerelease)' : ''} + {mappedRelease.versions.length === 0 ? ' (prerelease)' : ''} @@ -79,53 +78,152 @@ export function Row({ baseVersion, mappedRelease }: RowProps) { : '-'} - {candidates.length} + {mappedRelease.candidates.length} - {Math.max(0, versions.length - 1)} + {Math.max(0, mappedRelease.versions.length - 1)} - -
- {!isPrerelease && ( - - {versions.map(version => ( - - {version} - - ))} - - )} - - {!isPrerelease && ( - - {' 🚀 '} - - )} - - - {candidates.map(candidate => ( - - {candidate} - - ))} - -
-
+
); } + +function CollapsedEl({ + mappedRelease, +}: { + mappedRelease: ReturnType['releases']['0']; +}) { + const reversedCandidates = [...mappedRelease.candidates].reverse(); + + const { commit: releaseCut } = useGetCommit({ + ref: reversedCandidates[0]?.sha, + }); + const { commit: releaseComplete } = useGetCommit({ + ref: mappedRelease.versions[0]?.sha, + }); + + console.log('*** releaseCut > ', releaseCut.value); // eslint-disable-line no-console + console.log('*** releaseComplete > ', releaseComplete.value); // eslint-disable-line no-console + + let diff = { days: -1 } as DurationObject; + if (releaseCut.value?.createdAt && releaseComplete.value?.createdAt) { + diff = DateTime.fromISO(releaseComplete.value.createdAt) + .diff(DateTime.fromISO(releaseCut.value.createdAt), ['days', 'hours']) + .toObject(); + } + + return ( + + + {mappedRelease.versions.length > 0 && ( + + {mappedRelease.versions.map(version => ( + + {version.tagName} + + ))} + + )} + + {mappedRelease.versions.length > 0 && ( + + {' 🚀 '} + + )} + + + {mappedRelease.candidates.map(candidate => ( + + {candidate.tagName} + + ))} + + + + + {releaseComplete.loading ? ( + + ) : ( + <> + + Release completed{' '} + {releaseComplete.value?.createdAt && + DateTime.fromISO(releaseComplete.value.createdAt) + .setLocale('sv-SE') + .toFormat('yyyy-MM-dd')} + + + + + Release time: {diff.days} days + + + + + RC created{' '} + {releaseCut.value?.createdAt && + DateTime.fromISO(releaseCut.value.createdAt) + .setLocale('sv-SE') + .toFormat('yyyy-MM-dd')} + + + )} + + + ); +} diff --git a/plugins/github-release-manager/src/features/Stats/Summary.tsx b/plugins/github-release-manager/src/features/Stats/Summary.tsx new file mode 100644 index 0000000000..d5cedaeaae --- /dev/null +++ b/plugins/github-release-manager/src/features/Stats/Summary.tsx @@ -0,0 +1,89 @@ +/* + * 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; +} + +export function Summary({ summary }: SummaryProps) { + 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, + )} + + + + ); +} diff --git a/plugins/github-release-manager/src/features/Stats/Warn.tsx b/plugins/github-release-manager/src/features/Stats/Warn.tsx index ecae90426d..b3cb813dfb 100644 --- a/plugins/github-release-manager/src/features/Stats/Warn.tsx +++ b/plugins/github-release-manager/src/features/Stats/Warn.tsx @@ -17,8 +17,8 @@ import React from 'react'; import { Alert } from '@material-ui/lab'; -import { getMappedReleases } from './getMappedReleases'; -import { getTags } from './getTags'; +import { getMappedReleases } from './helpers/getMappedReleases'; +import { getTags } from './helpers/getTags'; import { Project } from '../../contexts/ProjectContext'; interface WarnProps { diff --git a/plugins/github-release-manager/src/features/Stats/helpers/getDecimalNumber.tsx b/plugins/github-release-manager/src/features/Stats/helpers/getDecimalNumber.tsx new file mode 100644 index 0000000000..590d95a757 --- /dev/null +++ b/plugins/github-release-manager/src/features/Stats/helpers/getDecimalNumber.tsx @@ -0,0 +1,27 @@ +/* + * 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. + */ + +export function getDecimalNumber(n: number) { + if (isNaN(n)) { + return 0; + } + + if (n.toString().includes('.')) { + return n.toFixed(2); + } + + return n; +} diff --git a/plugins/github-release-manager/src/features/Stats/getMappedReleases.tsx b/plugins/github-release-manager/src/features/Stats/helpers/getMappedReleases.tsx similarity index 71% rename from plugins/github-release-manager/src/features/Stats/getMappedReleases.tsx rename to plugins/github-release-manager/src/features/Stats/helpers/getMappedReleases.tsx index 52a6204589..af573343e5 100644 --- a/plugins/github-release-manager/src/features/Stats/getMappedReleases.tsx +++ b/plugins/github-release-manager/src/features/Stats/helpers/getMappedReleases.tsx @@ -14,10 +14,10 @@ * 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'; +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, @@ -33,8 +33,14 @@ export function getMappedReleases({ releases: { [baseVersion: string]: { createdAt: string | null; - candidates: string[]; - versions: string[]; + candidates: { + tagName: string; + sha: string; + }[]; + versions: { + tagName: string; + sha: string; + }[]; htmlUrl: string; }; }; @@ -60,8 +66,10 @@ export function getMappedReleases({ if (!acc.releases[baseVersion]) { acc.releases[baseVersion] = { createdAt: release.createdAt, - candidates: prefix === 'rc' ? [release.tagName] : [], - versions: prefix === 'version' ? [release.tagName] : [], + candidates: + prefix === 'rc' ? [{ tagName: release.tagName, sha: '' }] : [], + versions: + prefix === 'version' ? [{ tagName: release.tagName, sha: '' }] : [], htmlUrl: release.htmlUrl, }; return acc; diff --git a/plugins/github-release-manager/src/features/Stats/getSummary.tsx b/plugins/github-release-manager/src/features/Stats/helpers/getSummary.tsx similarity index 100% rename from plugins/github-release-manager/src/features/Stats/getSummary.tsx rename to plugins/github-release-manager/src/features/Stats/helpers/getSummary.tsx diff --git a/plugins/github-release-manager/src/features/Stats/getTags.tsx b/plugins/github-release-manager/src/features/Stats/helpers/getTags.tsx similarity index 58% rename from plugins/github-release-manager/src/features/Stats/getTags.tsx rename to plugins/github-release-manager/src/features/Stats/helpers/getTags.tsx index 4ad38cec17..82d0a6b587 100644 --- a/plugins/github-release-manager/src/features/Stats/getTags.tsx +++ b/plugins/github-release-manager/src/features/Stats/helpers/getTags.tsx @@ -14,11 +14,11 @@ * limitations under the License. */ -import { calverRegexp } from '../../helpers/tagParts/getCalverTagParts'; -import { GetAllTagsResult } from '../../api/PluginApiClient'; +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'; +import { Project } from '../../../contexts/ProjectContext'; +import { semverRegexp } from '../../../helpers/tagParts/getSemverTagParts'; export function getTags({ allTags, @@ -52,20 +52,34 @@ export function getTags({ return acc; } - if ( - prefix === 'rc' && - !mappedReleases.releases[baseVersion].candidates.includes(tag.tagName) - ) { - mappedReleases.releases[baseVersion].candidates.push(tag.tagName); - return acc; + if (prefix === 'rc') { + const existingEntry = mappedReleases.releases[ + baseVersion + ].candidates.find(({ tagName }) => tagName === tag.tagName); + + if (existingEntry) { + existingEntry.sha = tag.sha; + } else { + mappedReleases.releases[baseVersion].candidates.push({ + tagName: tag.tagName, + sha: tag.sha, + }); + } } - if ( - prefix === 'version' && - !mappedReleases.releases[baseVersion].versions.includes(tag.tagName) - ) { - mappedReleases.releases[baseVersion].versions.push(tag.tagName); - return acc; + if (prefix === 'version') { + const existingEntry = mappedReleases.releases[ + baseVersion + ].versions.find(({ tagName }) => tagName === tag.tagName); + + if (existingEntry) { + existingEntry.sha = tag.sha; + } else { + mappedReleases.releases[baseVersion].versions.push({ + tagName: tag.tagName, + sha: tag.sha, + }); + } } return acc; diff --git a/plugins/github-release-manager/src/features/Stats/hooks/useGetCommit.ts b/plugins/github-release-manager/src/features/Stats/hooks/useGetCommit.ts new file mode 100644 index 0000000000..d19aa30331 --- /dev/null +++ b/plugins/github-release-manager/src/features/Stats/hooks/useGetCommit.ts @@ -0,0 +1,37 @@ +/* + * 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 useGetCommit = ({ ref }: { ref: string }) => { + const { pluginApiClient } = usePluginApiClientContext(); + const { project } = useProjectContext(); + + const commit = useAsync(() => + pluginApiClient.stats.getCommit({ + owner: project.owner, + repo: project.repo, + ref, + }), + ); + + return { + commit, + }; +}; 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 fbd3b690b4..89553b3e24 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 @@ -52,6 +52,7 @@ describe('testHelpers', () => { "stats": Object { "getAllReleases": [MockFunction], "getAllTags": [MockFunction], + "getCommit": [MockFunction], }, }, "mockBumpedTag": "rc-2020.01.01_1337", 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 fc7a5f5c5b..629bdb4031 100644 --- a/plugins/github-release-manager/src/test-helpers/test-helpers.ts +++ b/plugins/github-release-manager/src/test-helpers/test-helpers.ts @@ -287,5 +287,9 @@ export const mockApiClient: IPluginApiClient = { getAllReleases: jest.fn(async () => { throw new Error('Not implemented'); }), + + getCommit: jest.fn(async () => { + throw new Error('Not implemented'); + }), }, }; From e3c318ded1c1e54fbed019f09534f103806fec94 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Fri, 23 Apr 2021 14:39:22 +0200 Subject: [PATCH 076/140] Stats: Create individual component for Row children Stats: Improve names for helpers Signed-off-by: Erik Engervall --- .../src/features/Stats/DialogBody.tsx | 38 +-- .../src/features/Stats/Row.tsx | 229 ------------------ .../src/features/Stats/Row/Row.tsx | 96 ++++++++ .../Stats/Row/RowCollapsed/ReleaseTagList.tsx | 71 ++++++ .../Stats/Row/RowCollapsed/ReleaseTime.tsx | 139 +++++++++++ .../Stats/Row/RowCollapsed/RowCollapsed.tsx | 45 ++++ .../src/features/Stats/Warn.tsx | 22 +- .../Stats/helpers/getMappedReleases.tsx | 82 ------- .../Stats/helpers/getReleasesWithTags.tsx | 101 ++++++++ .../src/features/Stats/helpers/getSummary.tsx | 8 +- .../src/features/Stats/helpers/getTags.tsx | 89 ------- .../features/Stats/helpers/mapReleases.tsx | 89 +++++++ 12 files changed, 577 insertions(+), 432 deletions(-) delete mode 100644 plugins/github-release-manager/src/features/Stats/Row.tsx create mode 100644 plugins/github-release-manager/src/features/Stats/Row/Row.tsx create mode 100644 plugins/github-release-manager/src/features/Stats/Row/RowCollapsed/ReleaseTagList.tsx create mode 100644 plugins/github-release-manager/src/features/Stats/Row/RowCollapsed/ReleaseTime.tsx create mode 100644 plugins/github-release-manager/src/features/Stats/Row/RowCollapsed/RowCollapsed.tsx delete mode 100644 plugins/github-release-manager/src/features/Stats/helpers/getMappedReleases.tsx create mode 100644 plugins/github-release-manager/src/features/Stats/helpers/getReleasesWithTags.tsx delete mode 100644 plugins/github-release-manager/src/features/Stats/helpers/getTags.tsx create mode 100644 plugins/github-release-manager/src/features/Stats/helpers/mapReleases.tsx diff --git a/plugins/github-release-manager/src/features/Stats/DialogBody.tsx b/plugins/github-release-manager/src/features/Stats/DialogBody.tsx index d8b406e04b..4248cdc1e8 100644 --- a/plugins/github-release-manager/src/features/Stats/DialogBody.tsx +++ b/plugins/github-release-manager/src/features/Stats/DialogBody.tsx @@ -29,14 +29,14 @@ import { } from '@material-ui/core'; import { CenteredCircularProgress } from '../../components/CenteredCircularProgress'; -import { getMappedReleases } from './helpers/getMappedReleases'; +import { getMappedReleases } from './helpers/mapReleases'; +import { getReleasesWithTags } from './helpers/getReleasesWithTags'; import { getSummary } from './helpers/getSummary'; -import { getTags } from './helpers/getTags'; -import { Row } from './Row'; +import { Row } from './Row/Row'; +import { Summary } from './Summary'; import { useGetStats } from './hooks/useGetStats'; import { useProjectContext } from '../../contexts/ProjectContext'; import { Warn } from './Warn'; -import { Summary } from './Summary'; const useStyles = makeStyles({ table: { @@ -64,20 +64,24 @@ export function DialogBody() { } const { allReleases, allTags } = stats.value; - const mappedReleases = getMappedReleases({ allReleases, project }); - const tags = getTags({ allTags, project, mappedReleases }); - const summary = getSummary({ mappedReleases }); + const { mappedReleases } = getMappedReleases({ allReleases, project }); + const { releasesWithTags } = getReleasesWithTags({ + mappedReleases, + allTags, + project, + }); + const summary = getSummary({ releasesWithTags }); const shouldWarn = - tags.unmappable.length > 0 || - tags.unmatched.length > 0 || - mappedReleases.unmatched.length > 0; + releasesWithTags.unmappableTags.length > 0 || + releasesWithTags.unmatchedTags.length > 0 || + releasesWithTags.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, + unmappableTags: releasesWithTags.unmappableTags, + unmatchedTags: releasesWithTags.unmatchedTags, + unmatchedReleases: releasesWithTags.unmatched, }); } @@ -98,13 +102,13 @@ export function DialogBody() { - {Object.entries(mappedReleases.releases).map( - ([baseVersion, mappedRelease], index) => { + {Object.entries(releasesWithTags.releases).map( + ([baseVersion, releaseWithTags], index) => { return ( ); }, @@ -115,7 +119,7 @@ export function DialogBody() { {shouldWarn && ( - + )} diff --git a/plugins/github-release-manager/src/features/Stats/Row.tsx b/plugins/github-release-manager/src/features/Stats/Row.tsx deleted file mode 100644 index 53ad5751da..0000000000 --- a/plugins/github-release-manager/src/features/Stats/Row.tsx +++ /dev/null @@ -1,229 +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, { useState } from 'react'; -import { DateTime, DurationObject } 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 './helpers/getMappedReleases'; -import { useGetCommit } from './hooks/useGetCommit'; -import { CenteredCircularProgress } from '../../components/CenteredCircularProgress'; - -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(); - - return ( - - - - setOpen(!open)} - > - {open ? : } - - - - - - {baseVersion} - {mappedRelease.versions.length === 0 ? ' (prerelease)' : ''} - - - - - {mappedRelease.createdAt - ? DateTime.fromISO(mappedRelease.createdAt) - .setLocale('sv-SE') - .toFormat('yyyy-MM-dd') - : '-'} - - - {mappedRelease.candidates.length} - - {Math.max(0, mappedRelease.versions.length - 1)} - - - - - - - - - - - ); -} - -function CollapsedEl({ - mappedRelease, -}: { - mappedRelease: ReturnType['releases']['0']; -}) { - const reversedCandidates = [...mappedRelease.candidates].reverse(); - - const { commit: releaseCut } = useGetCommit({ - ref: reversedCandidates[0]?.sha, - }); - const { commit: releaseComplete } = useGetCommit({ - ref: mappedRelease.versions[0]?.sha, - }); - - console.log('*** releaseCut > ', releaseCut.value); // eslint-disable-line no-console - console.log('*** releaseComplete > ', releaseComplete.value); // eslint-disable-line no-console - - let diff = { days: -1 } as DurationObject; - if (releaseCut.value?.createdAt && releaseComplete.value?.createdAt) { - diff = DateTime.fromISO(releaseComplete.value.createdAt) - .diff(DateTime.fromISO(releaseCut.value.createdAt), ['days', 'hours']) - .toObject(); - } - - return ( - - - {mappedRelease.versions.length > 0 && ( - - {mappedRelease.versions.map(version => ( - - {version.tagName} - - ))} - - )} - - {mappedRelease.versions.length > 0 && ( - - {' 🚀 '} - - )} - - - {mappedRelease.candidates.map(candidate => ( - - {candidate.tagName} - - ))} - - - - - {releaseComplete.loading ? ( - - ) : ( - <> - - Release completed{' '} - {releaseComplete.value?.createdAt && - DateTime.fromISO(releaseComplete.value.createdAt) - .setLocale('sv-SE') - .toFormat('yyyy-MM-dd')} - - - - - Release time: {diff.days} days - - - - - RC created{' '} - {releaseCut.value?.createdAt && - DateTime.fromISO(releaseCut.value.createdAt) - .setLocale('sv-SE') - .toFormat('yyyy-MM-dd')} - - - )} - - - ); -} diff --git a/plugins/github-release-manager/src/features/Stats/Row/Row.tsx b/plugins/github-release-manager/src/features/Stats/Row/Row.tsx new file mode 100644 index 0000000000..199aa74fdc --- /dev/null +++ b/plugins/github-release-manager/src/features/Stats/Row/Row.tsx @@ -0,0 +1,96 @@ +/* + * 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 { + Collapse, + IconButton, + Link, + makeStyles, + TableCell, + TableRow, +} from '@material-ui/core'; +import KeyboardArrowDownIcon from '@material-ui/icons/KeyboardArrowDown'; +import KeyboardArrowUpIcon from '@material-ui/icons/KeyboardArrowUp'; + +import { getReleasesWithTags } from '../helpers/getReleasesWithTags'; +import { RowCollapsed } from './RowCollapsed/RowCollapsed'; + +const useRowStyles = makeStyles({ + root: { + '& > *': { + borderBottom: 'unset', + }, + }, +}); + +interface RowProps { + baseVersion: string; + releaseWithTags: ReturnType< + typeof getReleasesWithTags + >['releasesWithTags']['releases']['0']; +} + +export function Row({ baseVersion, releaseWithTags }: RowProps) { + const [open, setOpen] = useState(false); + const classes = useRowStyles(); + + return ( + + + + setOpen(!open)} + > + {open ? : } + + + + + + {baseVersion} + {releaseWithTags.versions.length === 0 ? ' (prerelease)' : ''} + + + + + {releaseWithTags.createdAt + ? DateTime.fromISO(releaseWithTags.createdAt) + .setLocale('sv-SE') + .toFormat('yyyy-MM-dd') + : '-'} + + + {releaseWithTags.candidates.length} + + + {Math.max(0, releaseWithTags.versions.length - 1)} + + + + + + + + + + + + ); +} diff --git a/plugins/github-release-manager/src/features/Stats/Row/RowCollapsed/ReleaseTagList.tsx b/plugins/github-release-manager/src/features/Stats/Row/RowCollapsed/ReleaseTagList.tsx new file mode 100644 index 0000000000..3047c310f1 --- /dev/null +++ b/plugins/github-release-manager/src/features/Stats/Row/RowCollapsed/ReleaseTagList.tsx @@ -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 React from 'react'; +import { Box, Typography } from '@material-ui/core'; + +import { getReleasesWithTags } from '../../helpers/getReleasesWithTags'; + +export function ReleaseTagList({ + releaseWithTags, +}: { + releaseWithTags: ReturnType< + typeof getReleasesWithTags + >['releasesWithTags']['releases']['0']; +}) { + return ( + + {releaseWithTags.versions.length > 0 && ( + + {releaseWithTags.versions.map(version => ( + + {version.tagName} + + ))} + + )} + + {releaseWithTags.versions.length > 0 && ( + + {' 🚀 '} + + )} + + + {releaseWithTags.candidates.map(candidate => ( + + {candidate.tagName} + + ))} + + + ); +} diff --git a/plugins/github-release-manager/src/features/Stats/Row/RowCollapsed/ReleaseTime.tsx b/plugins/github-release-manager/src/features/Stats/Row/RowCollapsed/ReleaseTime.tsx new file mode 100644 index 0000000000..b8cc25fd12 --- /dev/null +++ b/plugins/github-release-manager/src/features/Stats/Row/RowCollapsed/ReleaseTime.tsx @@ -0,0 +1,139 @@ +/* + * 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, 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 { Alert } from '@material-ui/lab'; + +interface ReleaseTimeProps { + releaseWithTags: ReturnType< + typeof getReleasesWithTags + >['releasesWithTags']['releases']['0']; +} + +export function ReleaseTime({ releaseWithTags }: ReleaseTimeProps) { + const reversedCandidates = [...releaseWithTags.candidates].reverse(); + + const firstCandidateSha = reversedCandidates[0]?.sha; + const { commit: releaseCut } = useGetCommit({ ref: firstCandidateSha }); + + const mostRecentVersionSha = releaseWithTags.versions[0]?.sha; + const { commit: releaseComplete } = useGetCommit({ + ref: mostRecentVersionSha, + }); + + if (releaseCut.loading || releaseComplete.loading) { + return ( + + + + ); + } + + if (releaseCut.error) { + return ( + + Failed to fetch the first Release Candidate commit ( + {releaseCut.error.message}) + + ); + } + + if (releaseComplete.error) { + return ( + + Failed to fetch the final Release Version Commit ( + {releaseComplete.error.message}) + + ); + } + + const diff = + releaseCut.value?.createdAt && releaseComplete.value?.createdAt + ? DateTime.fromISO(releaseComplete.value.createdAt) + .diff(DateTime.fromISO(releaseCut.value.createdAt), ['days', 'hours']) + .toObject() + : { days: -1 }; + + return ( + + + + Release completed{' '} + {releaseComplete.value?.createdAt && + DateTime.fromISO(releaseComplete.value.createdAt) + .setLocale('sv-SE') + .toFormat('yyyy-MM-dd')} + + + + + + Release time: {diff.days} days + + + + + + Release Candidate created{' '} + {releaseCut.value?.createdAt && + DateTime.fromISO(releaseCut.value.createdAt) + .setLocale('sv-SE') + .toFormat('yyyy-MM-dd')} + + + + ); +} + +function Wrapper({ children }: { children: React.ReactNode }) { + return ( + + {children} + + ); +} diff --git a/plugins/github-release-manager/src/features/Stats/Row/RowCollapsed/RowCollapsed.tsx b/plugins/github-release-manager/src/features/Stats/Row/RowCollapsed/RowCollapsed.tsx new file mode 100644 index 0000000000..a17707a62c --- /dev/null +++ b/plugins/github-release-manager/src/features/Stats/Row/RowCollapsed/RowCollapsed.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 React from 'react'; +import { Box } from '@material-ui/core'; + +import { getReleasesWithTags } from '../../helpers/getReleasesWithTags'; +import { ReleaseTime } from './ReleaseTime'; +import { ReleaseTagList } from './ReleaseTagList'; + +interface RowCollapsedProps { + releaseWithTags: ReturnType< + typeof getReleasesWithTags + >['releasesWithTags']['releases']['0']; +} + +export function RowCollapsed({ releaseWithTags }: RowCollapsedProps) { + return ( + + + + + + ); +} diff --git a/plugins/github-release-manager/src/features/Stats/Warn.tsx b/plugins/github-release-manager/src/features/Stats/Warn.tsx index b3cb813dfb..a86ac2cf1b 100644 --- a/plugins/github-release-manager/src/features/Stats/Warn.tsx +++ b/plugins/github-release-manager/src/features/Stats/Warn.tsx @@ -17,36 +17,36 @@ import React from 'react'; import { Alert } from '@material-ui/lab'; -import { getMappedReleases } from './helpers/getMappedReleases'; -import { getTags } from './helpers/getTags'; +import { getReleasesWithTags } from './helpers/getReleasesWithTags'; import { Project } from '../../contexts/ProjectContext'; interface WarnProps { - tags: ReturnType; - mappedReleases: ReturnType; + releasesWithTags: ReturnType['releasesWithTags']; project: Project; } -export const Warn = ({ tags, mappedReleases, project }: WarnProps) => { +export const Warn = ({ releasesWithTags, project }: WarnProps) => { return ( - {tags.unmappable.length > 0 && ( + {releasesWithTags.unmappableTags.length > 0 && (
- Failed to map {tags.unmappable.length} tags to + Failed to map{' '} + {releasesWithTags.unmappableTags.length} tags to releases
)} - {tags.unmatched.length > 0 && ( + {releasesWithTags.unmatchedTags.length > 0 && (
- Failed to match {tags.unmatched.length} tags to{' '} + Failed to match{' '} + {releasesWithTags.unmatchedTags.length} tags to{' '} {project.versioningStrategy}
)} - {mappedReleases.unmatched.length > 0 && ( + {releasesWithTags.unmatched.length > 0 && (
- Failed to match {mappedReleases.unmatched.length}{' '} + Failed to match {releasesWithTags.unmatched.length}{' '} releases to {project.versioningStrategy}
)} diff --git a/plugins/github-release-manager/src/features/Stats/helpers/getMappedReleases.tsx b/plugins/github-release-manager/src/features/Stats/helpers/getMappedReleases.tsx deleted file mode 100644 index af573343e5..0000000000 --- a/plugins/github-release-manager/src/features/Stats/helpers/getMappedReleases.tsx +++ /dev/null @@ -1,82 +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 { 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: { - tagName: string; - sha: string; - }[]; - versions: { - tagName: string; - sha: 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' ? [{ tagName: release.tagName, sha: '' }] : [], - versions: - prefix === 'version' ? [{ tagName: release.tagName, sha: '' }] : [], - htmlUrl: release.htmlUrl, - }; - return acc; - } - - return acc; - }, - { unmatched: [], releases: {} }, - ); -} diff --git a/plugins/github-release-manager/src/features/Stats/helpers/getReleasesWithTags.tsx b/plugins/github-release-manager/src/features/Stats/helpers/getReleasesWithTags.tsx new file mode 100644 index 0000000000..1c41f78288 --- /dev/null +++ b/plugins/github-release-manager/src/features/Stats/helpers/getReleasesWithTags.tsx @@ -0,0 +1,101 @@ +/* + * 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['mappedReleases']; +}) { + return { + releasesWithTags: allTags.reduce( + ( + acc: ReturnType['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: [], + }, + ), + }; +} diff --git a/plugins/github-release-manager/src/features/Stats/helpers/getSummary.tsx b/plugins/github-release-manager/src/features/Stats/helpers/getSummary.tsx index f90758987b..b898469c8a 100644 --- a/plugins/github-release-manager/src/features/Stats/helpers/getSummary.tsx +++ b/plugins/github-release-manager/src/features/Stats/helpers/getSummary.tsx @@ -14,14 +14,14 @@ * limitations under the License. */ -import { getMappedReleases } from './getMappedReleases'; +import { getReleasesWithTags } from './getReleasesWithTags'; export function getSummary({ - mappedReleases, + releasesWithTags, }: { - mappedReleases: ReturnType; + releasesWithTags: ReturnType['releasesWithTags']; }) { - return Object.entries(mappedReleases.releases).reduce( + return Object.entries(releasesWithTags.releases).reduce( ( acc: { totalReleases: number; diff --git a/plugins/github-release-manager/src/features/Stats/helpers/getTags.tsx b/plugins/github-release-manager/src/features/Stats/helpers/getTags.tsx deleted file mode 100644 index 82d0a6b587..0000000000 --- a/plugins/github-release-manager/src/features/Stats/helpers/getTags.tsx +++ /dev/null @@ -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 { 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') { - const existingEntry = mappedReleases.releases[ - baseVersion - ].candidates.find(({ tagName }) => tagName === tag.tagName); - - if (existingEntry) { - existingEntry.sha = tag.sha; - } else { - mappedReleases.releases[baseVersion].candidates.push({ - tagName: tag.tagName, - sha: tag.sha, - }); - } - } - - if (prefix === 'version') { - const existingEntry = mappedReleases.releases[ - baseVersion - ].versions.find(({ tagName }) => tagName === tag.tagName); - - if (existingEntry) { - existingEntry.sha = tag.sha; - } else { - mappedReleases.releases[baseVersion].versions.push({ - tagName: tag.tagName, - sha: tag.sha, - }); - } - } - - return acc; - }, - { unmatched: [], unmappable: [] }, - ); -} diff --git a/plugins/github-release-manager/src/features/Stats/helpers/mapReleases.tsx b/plugins/github-release-manager/src/features/Stats/helpers/mapReleases.tsx new file mode 100644 index 0000000000..d1103d5e71 --- /dev/null +++ b/plugins/github-release-manager/src/features/Stats/helpers/mapReleases.tsx @@ -0,0 +1,89 @@ +/* + * 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 { + 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, + ) => { + 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' ? [{ tagName: release.tagName, sha: '' }] : [], + versions: + prefix === 'version' + ? [{ tagName: release.tagName, sha: '' }] + : [], + htmlUrl: release.htmlUrl, + }; + return acc; + } + + return acc; + }, + { + unmatched: [], + releases: {}, + }, + ), + }; +} From c5bd6be684268917377b235373b8db80c2223c0d Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Sun, 25 Apr 2021 17:59:01 +0200 Subject: [PATCH 077/140] Create additional stats for Stats component Add button for fetching and calculating average release times Signed-off-by: Erik Engervall --- plugins/github-release-manager/package.json | 2 + .../src/api/PluginApiClient.ts | 1 - .../src/features/Stats/DialogBody.tsx | 47 +++--- .../Stats/Info/InDepth/AverageReleaseTime.tsx | 49 ++++++ .../features/Stats/Info/InDepth/InDepth.tsx | 132 ++++++++++++++++ .../Stats/Info/InDepth/LongestReleaseTime.tsx | 43 ++++++ .../src/features/Stats/Info/Info.tsx | 41 +++++ .../src/features/Stats/Info/Summary.tsx | 108 +++++++++++++ .../helpers/getReleaseCommitPairs.test.tsx | 109 +++++++++++++ .../Info/helpers/getReleaseCommitPairs.tsx | 65 ++++++++ .../Stats/Info/hooks/useGetReleaseTimes.tsx | 127 +++++++++++++++ .../src/features/Stats/Row/Row.tsx | 28 ++-- .../Stats/Row/RowCollapsed/ReleaseTagList.tsx | 16 +- .../Stats/Row/RowCollapsed/ReleaseTime.tsx | 39 ++--- .../Stats/Row/RowCollapsed/RowCollapsed.tsx | 14 +- .../src/features/Stats/Summary.tsx | 89 ----------- .../src/features/Stats/Warn.tsx | 36 ++--- .../Stats/contexts/ReleaseStatsContext.tsx | 64 ++++++++ .../Stats/helpers/getDecimalNumber.test.tsx | 43 ++++++ .../Stats/helpers/getDecimalNumber.tsx | 4 +- .../Stats/helpers/getMappedReleases.test.tsx | 71 +++++++++ ...{mapReleases.tsx => getMappedReleases.tsx} | 51 +++--- .../Stats/helpers/getReleaseStats.test.tsx | 124 +++++++++++++++ .../Stats/helpers/getReleaseStats.tsx | 81 ++++++++++ .../Stats/helpers/getReleasesWithTags.tsx | 101 ------------ .../Stats/helpers/getSummary.test.tsx | 34 ++++ .../src/features/Stats/helpers/getSummary.tsx | 55 +++---- .../src/features/Stats/hooks/useGetCommit.ts | 15 +- .../src/test-helpers/stats.ts | 70 +++++++++ .../src/test-helpers/test-helpers.test.ts | 146 ------------------ .../src/test-helpers/test-helpers.ts | 27 ++-- .../src/test-helpers/test-ids.test.ts | 88 ----------- 32 files changed, 1319 insertions(+), 601 deletions(-) create mode 100644 plugins/github-release-manager/src/features/Stats/Info/InDepth/AverageReleaseTime.tsx create mode 100644 plugins/github-release-manager/src/features/Stats/Info/InDepth/InDepth.tsx create mode 100644 plugins/github-release-manager/src/features/Stats/Info/InDepth/LongestReleaseTime.tsx create mode 100644 plugins/github-release-manager/src/features/Stats/Info/Info.tsx create mode 100644 plugins/github-release-manager/src/features/Stats/Info/Summary.tsx create mode 100644 plugins/github-release-manager/src/features/Stats/Info/helpers/getReleaseCommitPairs.test.tsx create mode 100644 plugins/github-release-manager/src/features/Stats/Info/helpers/getReleaseCommitPairs.tsx create mode 100644 plugins/github-release-manager/src/features/Stats/Info/hooks/useGetReleaseTimes.tsx delete mode 100644 plugins/github-release-manager/src/features/Stats/Summary.tsx create mode 100644 plugins/github-release-manager/src/features/Stats/contexts/ReleaseStatsContext.tsx create mode 100644 plugins/github-release-manager/src/features/Stats/helpers/getDecimalNumber.test.tsx create mode 100644 plugins/github-release-manager/src/features/Stats/helpers/getMappedReleases.test.tsx rename plugins/github-release-manager/src/features/Stats/helpers/{mapReleases.tsx => getMappedReleases.tsx} (68%) create mode 100644 plugins/github-release-manager/src/features/Stats/helpers/getReleaseStats.test.tsx create mode 100644 plugins/github-release-manager/src/features/Stats/helpers/getReleaseStats.tsx delete mode 100644 plugins/github-release-manager/src/features/Stats/helpers/getReleasesWithTags.tsx create mode 100644 plugins/github-release-manager/src/features/Stats/helpers/getSummary.test.tsx create mode 100644 plugins/github-release-manager/src/test-helpers/stats.ts delete mode 100644 plugins/github-release-manager/src/test-helpers/test-helpers.test.ts delete mode 100644 plugins/github-release-manager/src/test-helpers/test-ids.test.ts diff --git a/plugins/github-release-manager/package.json b/plugins/github-release-manager/package.json index c73941ec54..be544bc1fb 100644 --- a/plugins/github-release-manager/package.json +++ b/plugins/github-release-manager/package.json @@ -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", diff --git a/plugins/github-release-manager/src/api/PluginApiClient.ts b/plugins/github-release-manager/src/api/PluginApiClient.ts index 033e074bcf..52764c6613 100644 --- a/plugins/github-release-manager/src/api/PluginApiClient.ts +++ b/plugins/github-release-manager/src/api/PluginApiClient.ts @@ -591,7 +591,6 @@ ${selectedPatchCommit.commit.message}`, }); return releases.map(release => ({ - release, id: release.id, name: release.name, tagName: release.tag_name, diff --git a/plugins/github-release-manager/src/features/Stats/DialogBody.tsx b/plugins/github-release-manager/src/features/Stats/DialogBody.tsx index 4248cdc1e8..8de40d6ac9 100644 --- a/plugins/github-release-manager/src/features/Stats/DialogBody.tsx +++ b/plugins/github-release-manager/src/features/Stats/DialogBody.tsx @@ -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 ( - <> - + + - +
@@ -102,26 +101,22 @@ export function DialogBody() { - {Object.entries(releasesWithTags.releases).map( - ([baseVersion, releaseWithTags], index) => { + {Object.entries(releaseStats.releases).map( + ([baseVersion, releaseStat], index) => { return ( ); }, )}
-
- - {shouldWarn && ( - - )} - - + {shouldWarn && } + + ); } diff --git a/plugins/github-release-manager/src/features/Stats/Info/InDepth/AverageReleaseTime.tsx b/plugins/github-release-manager/src/features/Stats/Info/InDepth/AverageReleaseTime.tsx new file mode 100644 index 0000000000..659956ddb1 --- /dev/null +++ b/plugins/github-release-manager/src/features/Stats/Info/InDepth/AverageReleaseTime.tsx @@ -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 + + ); +} diff --git a/plugins/github-release-manager/src/features/Stats/Info/InDepth/InDepth.tsx b/plugins/github-release-manager/src/features/Stats/Info/InDepth/InDepth.tsx new file mode 100644 index 0000000000..9fbd2bc925 --- /dev/null +++ b/plugins/github-release-manager/src/features/Stats/Info/InDepth/InDepth.tsx @@ -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 ( + + + In-depth + + + + + + Release time + + + Release time is derived by comparing{' '} + createdAt 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. + + + + + + + In numbers + + + Average release time:{' '} + + + + + Longest release:{' '} + + + + + + {progress === 0 && ( + + + + )} + + + + + + 0 + ? averageReleaseTime + : [{ version: 'x.y.z', days: 0 }] + } + margin={{ top: 5, right: 30, left: 20, bottom: 5 }} + layout="vertical" + > + + + + + + + + {progress > 0 && progress < 100 && ( + + + + )} + + + ); +} diff --git a/plugins/github-release-manager/src/features/Stats/Info/InDepth/LongestReleaseTime.tsx b/plugins/github-release-manager/src/features/Stats/Info/InDepth/LongestReleaseTime.tsx new file mode 100644 index 0000000000..7fd5dbd572 --- /dev/null +++ b/plugins/github-release-manager/src/features/Stats/Info/InDepth/LongestReleaseTime.tsx @@ -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 ) + + ); +} diff --git a/plugins/github-release-manager/src/features/Stats/Info/Info.tsx b/plugins/github-release-manager/src/features/Stats/Info/Info.tsx new file mode 100644 index 0000000000..1c21f66e2c --- /dev/null +++ b/plugins/github-release-manager/src/features/Stats/Info/Info.tsx @@ -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 ( + + + + + + ); +} diff --git a/plugins/github-release-manager/src/features/Stats/Info/Summary.tsx b/plugins/github-release-manager/src/features/Stats/Info/Summary.tsx new file mode 100644 index 0000000000..ff872dfffc --- /dev/null +++ b/plugins/github-release-manager/src/features/Stats/Info/Summary.tsx @@ -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 ( + + Summary + + + Total releases: {summary.totalReleases} + + + + + + + + Patches + Patches per release + + + + + + + Release Candidate + + {summary.totalCandidatePatches} + + {getDecimalNumber( + summary.totalCandidatePatches / summary.totalReleases, + )} + + + + + + Release Version + + {summary.totalVersionPatches} + + {getDecimalNumber( + summary.totalVersionPatches / summary.totalReleases, + )} + + + + + + Total + + + {summary.totalCandidatePatches + summary.totalVersionPatches} + + + {getDecimalNumber( + (summary.totalCandidatePatches + + summary.totalVersionPatches) / + summary.totalReleases, + )} + + + +
+
+
+ ); +} diff --git a/plugins/github-release-manager/src/features/Stats/Info/helpers/getReleaseCommitPairs.test.tsx b/plugins/github-release-manager/src/features/Stats/Info/helpers/getReleaseCommitPairs.test.tsx new file mode 100644 index 0000000000..3d9fb9449c --- /dev/null +++ b/plugins/github-release-manager/src/features/Stats/Info/helpers/getReleaseCommitPairs.test.tsx @@ -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", + }, + }, + ], + } + `); + }); +}); diff --git a/plugins/github-release-manager/src/features/Stats/Info/helpers/getReleaseCommitPairs.tsx b/plugins/github-release-manager/src/features/Stats/Info/helpers/getReleaseCommitPairs.tsx new file mode 100644 index 0000000000..e55d97078e --- /dev/null +++ b/plugins/github-release-manager/src/features/Stats/Info/helpers/getReleaseCommitPairs.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 { 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, + }; +} diff --git a/plugins/github-release-manager/src/features/Stats/Info/hooks/useGetReleaseTimes.tsx b/plugins/github-release-manager/src/features/Stats/Info/hooks/useGetReleaseTimes.tsx new file mode 100644 index 0000000000..2909763a8f --- /dev/null +++ b/plugins/github-release-manager/src/features/Stats/Info/hooks/useGetReleaseTimes.tsx @@ -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( + [], + ); + 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, + }; +} diff --git a/plugins/github-release-manager/src/features/Stats/Row/Row.tsx b/plugins/github-release-manager/src/features/Stats/Row/Row.tsx index 199aa74fdc..95ec244e78 100644 --- a/plugins/github-release-manager/src/features/Stats/Row/Row.tsx +++ b/plugins/github-release-manager/src/features/Stats/Row/Row.tsx @@ -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 ( - + <> - + {baseVersion} - {releaseWithTags.versions.length === 0 ? ' (prerelease)' : ''} + {releaseStat.versions.length === 0 ? ' (prerelease)' : ''} - {releaseWithTags.createdAt - ? DateTime.fromISO(releaseWithTags.createdAt) + {releaseStat.createdAt + ? DateTime.fromISO(releaseStat.createdAt) .setLocale('sv-SE') .toFormat('yyyy-MM-dd') : '-'} - {releaseWithTags.candidates.length} + {releaseStat.candidates.length} - - {Math.max(0, releaseWithTags.versions.length - 1)} - + {Math.max(0, releaseStat.versions.length - 1)} - + - + ); } diff --git a/plugins/github-release-manager/src/features/Stats/Row/RowCollapsed/ReleaseTagList.tsx b/plugins/github-release-manager/src/features/Stats/Row/RowCollapsed/ReleaseTagList.tsx index 3047c310f1..e07b74a171 100644 --- a/plugins/github-release-manager/src/features/Stats/Row/RowCollapsed/ReleaseTagList.tsx +++ b/plugins/github-release-manager/src/features/Stats/Row/RowCollapsed/ReleaseTagList.tsx @@ -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 ( - {releaseWithTags.versions.length > 0 && ( + {releaseStat.versions.length > 0 && ( - {releaseWithTags.versions.map(version => ( + {releaseStat.versions.map(version => ( {version.tagName} @@ -46,7 +44,7 @@ export function ReleaseTagList({ )} - {releaseWithTags.versions.length > 0 && ( + {releaseStat.versions.length > 0 && ( - {releaseWithTags.candidates.map(candidate => ( + {releaseStat.candidates.map(candidate => ( {candidate.tagName} diff --git a/plugins/github-release-manager/src/features/Stats/Row/RowCollapsed/ReleaseTime.tsx b/plugins/github-release-manager/src/features/Stats/Row/RowCollapsed/ReleaseTime.tsx index b8cc25fd12..7f19ec1df9 100644 --- a/plugins/github-release-manager/src/features/Stats/Row/RowCollapsed/ReleaseTime.tsx +++ b/plugins/github-release-manager/src/features/Stats/Row/RowCollapsed/ReleaseTime.tsx @@ -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 ( - - Failed to fetch the final Release Version Commit ( - {releaseComplete.error.message}) - - ); - } - const diff = releaseCut.value?.createdAt && releaseComplete.value?.createdAt ? DateTime.fromISO(releaseComplete.value.createdAt) @@ -83,7 +70,7 @@ export function ReleaseTime({ releaseWithTags }: ReleaseTimeProps) { }} > - 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) { }} > - Release time: {diff.days} days + {diff.days === -1 ? ( + <>Ongoing: {diff.days} days + ) : ( + <>Completed in: {diff.days} days + )} diff --git a/plugins/github-release-manager/src/features/Stats/Row/RowCollapsed/RowCollapsed.tsx b/plugins/github-release-manager/src/features/Stats/Row/RowCollapsed/RowCollapsed.tsx index a17707a62c..101954e49d 100644 --- a/plugins/github-release-manager/src/features/Stats/Row/RowCollapsed/RowCollapsed.tsx +++ b/plugins/github-release-manager/src/features/Stats/Row/RowCollapsed/RowCollapsed.tsx @@ -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 ( - + - + ); } diff --git a/plugins/github-release-manager/src/features/Stats/Summary.tsx b/plugins/github-release-manager/src/features/Stats/Summary.tsx deleted file mode 100644 index d5cedaeaae..0000000000 --- a/plugins/github-release-manager/src/features/Stats/Summary.tsx +++ /dev/null @@ -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; -} - -export function Summary({ summary }: SummaryProps) { - 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, - )} - - - - ); -} diff --git a/plugins/github-release-manager/src/features/Stats/Warn.tsx b/plugins/github-release-manager/src/features/Stats/Warn.tsx index a86ac2cf1b..93c2cc4154 100644 --- a/plugins/github-release-manager/src/features/Stats/Warn.tsx +++ b/plugins/github-release-manager/src/features/Stats/Warn.tsx @@ -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['releasesWithTags']; - project: Project; -} +export const Warn = () => { + const { releaseStats } = useReleaseStatsContext(); + const { project } = useProjectContext(); -export const Warn = ({ releasesWithTags, project }: WarnProps) => { return ( - {releasesWithTags.unmappableTags.length > 0 && ( + {releaseStats.unmappableTags.length > 0 && (
- Failed to map{' '} - {releasesWithTags.unmappableTags.length} tags to - releases + Failed to map {releaseStats.unmappableTags.length}{' '} + tags to releases
)} - {releasesWithTags.unmatchedTags.length > 0 && ( + {releaseStats.unmatchedTags.length > 0 && ( +
+ Failed to match {releaseStats.unmatchedTags.length}{' '} + tags to {project.versioningStrategy} +
+ )} + + {releaseStats.unmatchedReleases.length > 0 && (
Failed to match{' '} - {releasesWithTags.unmatchedTags.length} tags to{' '} + {releaseStats.unmatchedReleases.length} releases to{' '} {project.versioningStrategy}
)} - {releasesWithTags.unmatched.length > 0 && ( -
- Failed to match {releasesWithTags.unmatched.length}{' '} - releases to {project.versioningStrategy} -
- )}
See full output in the console
); diff --git a/plugins/github-release-manager/src/features/Stats/contexts/ReleaseStatsContext.tsx b/plugins/github-release-manager/src/features/Stats/contexts/ReleaseStatsContext.tsx new file mode 100644 index 0000000000..be0d68e2e5 --- /dev/null +++ b/plugins/github-release-manager/src/features/Stats/contexts/ReleaseStatsContext.tsx @@ -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, + }; +}; diff --git a/plugins/github-release-manager/src/features/Stats/helpers/getDecimalNumber.test.tsx b/plugins/github-release-manager/src/features/Stats/helpers/getDecimalNumber.test.tsx new file mode 100644 index 0000000000..b4b1a1df4c --- /dev/null +++ b/plugins/github-release-manager/src/features/Stats/helpers/getDecimalNumber.test.tsx @@ -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`); + }); +}); diff --git a/plugins/github-release-manager/src/features/Stats/helpers/getDecimalNumber.tsx b/plugins/github-release-manager/src/features/Stats/helpers/getDecimalNumber.tsx index 590d95a757..ab4a83fa79 100644 --- a/plugins/github-release-manager/src/features/Stats/helpers/getDecimalNumber.tsx +++ b/plugins/github-release-manager/src/features/Stats/helpers/getDecimalNumber.tsx @@ -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; diff --git a/plugins/github-release-manager/src/features/Stats/helpers/getMappedReleases.test.tsx b/plugins/github-release-manager/src/features/Stats/helpers/getMappedReleases.test.tsx new file mode 100644 index 0000000000..5bea396d5c --- /dev/null +++ b/plugins/github-release-manager/src/features/Stats/helpers/getMappedReleases.test.tsx @@ -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 [], + }, + } + `); + }); +}); diff --git a/plugins/github-release-manager/src/features/Stats/helpers/mapReleases.tsx b/plugins/github-release-manager/src/features/Stats/helpers/getMappedReleases.tsx similarity index 68% rename from plugins/github-release-manager/src/features/Stats/helpers/mapReleases.tsx rename to plugins/github-release-manager/src/features/Stats/helpers/getMappedReleases.tsx index d1103d5e71..55382100ac 100644 --- a/plugins/github-release-manager/src/features/Stats/helpers/mapReleases.tsx +++ b/plugins/github-release-manager/src/features/Stats/helpers/getMappedReleases.tsx @@ -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: [], }, ), }; diff --git a/plugins/github-release-manager/src/features/Stats/helpers/getReleaseStats.test.tsx b/plugins/github-release-manager/src/features/Stats/helpers/getReleaseStats.test.tsx new file mode 100644 index 0000000000..3b59cc4c46 --- /dev/null +++ b/plugins/github-release-manager/src/features/Stats/helpers/getReleaseStats.test.tsx @@ -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", + ], + }, + } + `); + }); +}); diff --git a/plugins/github-release-manager/src/features/Stats/helpers/getReleaseStats.tsx b/plugins/github-release-manager/src/features/Stats/helpers/getReleaseStats.tsx new file mode 100644 index 0000000000..9b77cc1223 --- /dev/null +++ b/plugins/github-release-manager/src/features/Stats/helpers/getReleaseStats.tsx @@ -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, + }; +} diff --git a/plugins/github-release-manager/src/features/Stats/helpers/getReleasesWithTags.tsx b/plugins/github-release-manager/src/features/Stats/helpers/getReleasesWithTags.tsx deleted file mode 100644 index 1c41f78288..0000000000 --- a/plugins/github-release-manager/src/features/Stats/helpers/getReleasesWithTags.tsx +++ /dev/null @@ -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['mappedReleases']; -}) { - return { - releasesWithTags: allTags.reduce( - ( - acc: ReturnType['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: [], - }, - ), - }; -} diff --git a/plugins/github-release-manager/src/features/Stats/helpers/getSummary.test.tsx b/plugins/github-release-manager/src/features/Stats/helpers/getSummary.test.tsx new file mode 100644 index 0000000000..be6052c2a0 --- /dev/null +++ b/plugins/github-release-manager/src/features/Stats/helpers/getSummary.test.tsx @@ -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, + }, + } + `); + }); +}); diff --git a/plugins/github-release-manager/src/features/Stats/helpers/getSummary.tsx b/plugins/github-release-manager/src/features/Stats/helpers/getSummary.tsx index b898469c8a..e42c5c7379 100644 --- a/plugins/github-release-manager/src/features/Stats/helpers/getSummary.tsx +++ b/plugins/github-release-manager/src/features/Stats/helpers/getSummary.tsx @@ -14,32 +14,35 @@ * limitations under the License. */ -import { getReleasesWithTags } from './getReleasesWithTags'; +import { ReleaseStats } from '../contexts/ReleaseStatsContext'; -export function getSummary({ - releasesWithTags, -}: { - releasesWithTags: ReturnType['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, + }, + ), + }; } diff --git a/plugins/github-release-manager/src/features/Stats/hooks/useGetCommit.ts b/plugins/github-release-manager/src/features/Stats/hooks/useGetCommit.ts index d19aa30331..95db493774 100644 --- a/plugins/github-release-manager/src/features/Stats/hooks/useGetCommit.ts +++ b/plugins/github-release-manager/src/features/Stats/hooks/useGetCommit.ts @@ -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, diff --git a/plugins/github-release-manager/src/test-helpers/stats.ts b/plugins/github-release-manager/src/test-helpers/stats.ts new file mode 100644 index 0000000000..22b3b83d1e --- /dev/null +++ b/plugins/github-release-manager/src/test-helpers/stats.ts @@ -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: [], +}; 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 deleted file mode 100644 index 89553b3e24..0000000000 --- a/plugins/github-release-manager/src/test-helpers/test-helpers.test.ts +++ /dev/null @@ -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", - }, - } - `); - }); -}); 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 629bdb4031..68bd9c7e3b 100644 --- a/plugins/github-release-manager/src/test-helpers/test-helpers.ts +++ b/plugins/github-release-manager/src/test-helpers/test-helpers.ts @@ -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', + })), }, }; diff --git a/plugins/github-release-manager/src/test-helpers/test-ids.test.ts b/plugins/github-release-manager/src/test-helpers/test-ids.test.ts deleted file mode 100644 index 199a7a5745..0000000000 --- a/plugins/github-release-manager/src/test-helpers/test-ids.test.ts +++ /dev/null @@ -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", - }, - }, - } - `); - }); -}); From 8853366eb1718b67c0ef21e06220ba3d3157f349 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Thu, 29 Apr 2021 09:29:40 +0200 Subject: [PATCH 078/140] Update plugin yaml author Signed-off-by: Erik Engervall --- microsite/data/plugins/github-release-manager.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/microsite/data/plugins/github-release-manager.yaml b/microsite/data/plugins/github-release-manager.yaml index 9c930b2bbb..febc03f714 100644 --- a/microsite/data/plugins/github-release-manager.yaml +++ b/microsite/data/plugins/github-release-manager.yaml @@ -1,7 +1,7 @@ --- title: GitHub Release Manager -author: '@erikengervall' -authorUrl: https://github.com/erikengervall +author: '@Spotify' +authorUrl: https://github.com/spotify category: Release management description: Manage releases without having to juggle git commands documentation: https://github.com/backstage/backstage/tree/master/plugins/github-release-manager From 90e93079ecd518855fd6eb6b3fb7a5538e0e6843 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Thu, 29 Apr 2021 09:41:40 +0200 Subject: [PATCH 079/140] Address PR comments Signed-off-by: Erik Engervall --- packages/app/src/apis.ts | 11 ------ plugins/github-release-manager/dev/index.tsx | 18 ++++------ .../src/GitHubReleaseManager.tsx | 21 +++++------ .../src/components/Divider.tsx | 10 ++++-- .../src/contexts/PluginApiClientContext.ts | 36 ------------------- .../features/CreateRc/hooks/useCreateRc.ts | 5 +-- .../src/features/Features.tsx | 6 ++-- .../src/features/Patch/PatchBody.tsx | 5 +-- .../src/features/Patch/hooks/usePatch.ts | 5 +-- .../features/PromoteRc/hooks/usePromoteRc.ts | 7 ++-- .../src/features/RepoDetailsForm/Owner.tsx | 5 +-- .../src/features/RepoDetailsForm/Repo.tsx | 5 +-- .../Stats/Info/hooks/useGetReleaseTimes.tsx | 5 +-- .../src/features/Stats/hooks/useGetCommit.ts | 5 +-- .../src/features/Stats/hooks/useGetStats.ts | 5 +-- 15 files changed, 54 insertions(+), 95 deletions(-) delete mode 100644 plugins/github-release-manager/src/contexts/PluginApiClientContext.ts diff --git a/packages/app/src/apis.ts b/packages/app/src/apis.ts index beea444a6c..ccc576e727 100644 --- a/packages/app/src/apis.ts +++ b/packages/app/src/apis.ts @@ -33,7 +33,6 @@ import { graphQlBrowseApiRef, GraphQLEndpoints, } from '@backstage/plugin-graphiql'; -// import { githubReleaseManagerApiRef } from '@backstage/plugin-github-release-manager'; export const apis: AnyApiFactory[] = [ createApiFactory({ @@ -42,16 +41,6 @@ export const apis: AnyApiFactory[] = [ factory: ({ configApi }) => ScmIntegrationsApi.fromConfig(configApi), }), - // createApiFactory({ - // api: githubReleaseManagerApiRef, - // deps: { githubAuthApi: githubAuthApiRef }, - // factory: ({ githubAuthApi }) => { - // return { - - // } - // }, - // }), - createApiFactory({ api: graphQlBrowseApiRef, deps: { errorApi: errorApiRef, githubAuthApi: githubAuthApiRef }, diff --git a/plugins/github-release-manager/dev/index.tsx b/plugins/github-release-manager/dev/index.tsx index 31a6a9a563..62eb2a8246 100644 --- a/plugins/github-release-manager/dev/index.tsx +++ b/plugins/github-release-manager/dev/index.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { createDevApp } from '@backstage/dev-utils'; -import { Typography } from '@material-ui/core'; +import { Box, Typography } from '@material-ui/core'; import { gitHubReleaseManagerPlugin, @@ -24,31 +24,27 @@ import { } from '../src/plugin'; import { InfoCardPlus } from '../src/components/InfoCardPlus'; -function DevWrapper({ children }: { children: React.ReactNode }) { - return
{children}
; -} - createDevApp() .registerPlugin(gitHubReleaseManagerPlugin) .addPage({ title: 'Dynamic', path: '/dynamic', element: ( - + Dev notes Configure plugin via select inputs - +
), }) .addPage({ title: 'Static', path: '/static', element: ( - + Dev notes @@ -64,14 +60,14 @@ createDevApp() versioningStrategy: 'semver', }} /> - + ), }) .addPage({ title: 'Omit', path: '/omit', element: ( - + Dev notes Each feature can be omitted @@ -112,7 +108,7 @@ createDevApp() }, }} /> - + ), }) .render(); diff --git a/plugins/github-release-manager/src/GitHubReleaseManager.tsx b/plugins/github-release-manager/src/GitHubReleaseManager.tsx index f0156af06c..df17b8c002 100644 --- a/plugins/github-release-manager/src/GitHubReleaseManager.tsx +++ b/plugins/github-release-manager/src/GitHubReleaseManager.tsx @@ -30,7 +30,6 @@ import { CenteredCircularProgress } from './components/CenteredCircularProgress' import { githubReleaseManagerApiRef } from './api/serviceApiRef'; import { InfoCardPlus } from './components/InfoCardPlus'; import { isProjectValid } from './helpers/isProjectValid'; -import { PluginApiClientContext } from './contexts/PluginApiClientContext'; import { ProjectContext, Project } from './contexts/ProjectContext'; import { RepoDetailsForm } from './features/RepoDetailsForm/RepoDetailsForm'; import { useQueryHandler } from './hooks/useQueryHandler'; @@ -82,18 +81,16 @@ export function GitHubReleaseManager(props: GitHubReleaseManagerProps) { } return ( - - -
- + +
+ - - - + + + - {isProjectValid(project) && } -
-
- + {isProjectValid(project) && } +
+
); } diff --git a/plugins/github-release-manager/src/components/Divider.tsx b/plugins/github-release-manager/src/components/Divider.tsx index 9ddf21e0ff..6be0f4eb3c 100644 --- a/plugins/github-release-manager/src/components/Divider.tsx +++ b/plugins/github-release-manager/src/components/Divider.tsx @@ -15,14 +15,18 @@ */ import React from 'react'; -import { Divider as MaterialDivider } from '@material-ui/core'; +import { Box, Divider as MaterialDivider } from '@material-ui/core'; import { TEST_IDS } from '../test-helpers/test-ids'; export const Divider = () => { return ( -
+ -
+ ); }; diff --git a/plugins/github-release-manager/src/contexts/PluginApiClientContext.ts b/plugins/github-release-manager/src/contexts/PluginApiClientContext.ts deleted file mode 100644 index 2d4c9f27ac..0000000000 --- a/plugins/github-release-manager/src/contexts/PluginApiClientContext.ts +++ /dev/null @@ -1,36 +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 { createContext, useContext } from 'react'; - -import { IPluginApiClient } from '../api/PluginApiClient'; -import { GitHubReleaseManagerError } from '../errors/GitHubReleaseManagerError'; - -export const PluginApiClientContext = createContext< - { pluginApiClient: IPluginApiClient } | undefined ->(undefined); - -export const usePluginApiClientContext = () => { - const { pluginApiClient } = useContext(PluginApiClientContext) ?? {}; - - if (!pluginApiClient) { - throw new GitHubReleaseManagerError('pluginApiClient not found'); - } - - return { - pluginApiClient, - }; -}; diff --git a/plugins/github-release-manager/src/features/CreateRc/hooks/useCreateRc.ts b/plugins/github-release-manager/src/features/CreateRc/hooks/useCreateRc.ts index 30d5a8894f..b205cfad6a 100644 --- a/plugins/github-release-manager/src/features/CreateRc/hooks/useCreateRc.ts +++ b/plugins/github-release-manager/src/features/CreateRc/hooks/useCreateRc.ts @@ -16,6 +16,7 @@ import { useEffect, useState } from 'react'; import { useAsync, useAsyncFn } from 'react-use'; +import { useApi } from '@backstage/core'; import { GetLatestReleaseResult, @@ -23,10 +24,10 @@ import { } from '../../../api/PluginApiClient'; import { CardHook, ComponentConfigCreateRc } from '../../../types/types'; import { getRcGitHubInfo } from '../../../helpers/getRcGitHubInfo'; +import { githubReleaseManagerApiRef } from '../../../api/serviceApiRef'; import { GitHubReleaseManagerError } from '../../../errors/GitHubReleaseManagerError'; import { Project } from '../../../contexts/ProjectContext'; import { useResponseSteps } from '../../../hooks/useResponseSteps'; -import { usePluginApiClientContext } from '../../../contexts/PluginApiClientContext'; interface CreateRC { defaultBranch: GetRepositoryResult['defaultBranch']; @@ -43,7 +44,7 @@ export function useCreateRc({ project, successCb, }: CreateRC): CardHook { - const { pluginApiClient } = usePluginApiClientContext(); + const pluginApiClient = useApi(githubReleaseManagerApiRef); if (nextGitHubInfo.error) { throw new GitHubReleaseManagerError( diff --git a/plugins/github-release-manager/src/features/Features.tsx b/plugins/github-release-manager/src/features/Features.tsx index 5d3e2a7151..f8c48b0732 100644 --- a/plugins/github-release-manager/src/features/Features.tsx +++ b/plugins/github-release-manager/src/features/Features.tsx @@ -16,17 +16,17 @@ import React, { useState } from 'react'; import { Alert, AlertTitle } from '@material-ui/lab'; -import { ErrorBoundary } from '@backstage/core'; +import { ErrorBoundary, useApi } from '@backstage/core'; import { CenteredCircularProgress } from '../components/CenteredCircularProgress'; import { CreateRc } from './CreateRc/CreateRc'; +import { githubReleaseManagerApiRef } from '../api/serviceApiRef'; import { GitHubReleaseManagerProps } from '../GitHubReleaseManager'; import { Info } from './Info/Info'; import { Patch } from './Patch/Patch'; import { PromoteRc } from './PromoteRc/PromoteRc'; import { RefetchContext } from '../contexts/RefetchContext'; 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'; @@ -36,7 +36,7 @@ export function Features({ }: { features: GitHubReleaseManagerProps['features']; }) { - const { pluginApiClient } = usePluginApiClientContext(); + const pluginApiClient = useApi(githubReleaseManagerApiRef); const { project } = useProjectContext(); const [refetchTrigger, setRefetchTrigger] = useState(0); const { gitHubBatchInfo } = useGetGitHubBatchInfo({ diff --git a/plugins/github-release-manager/src/features/Patch/PatchBody.tsx b/plugins/github-release-manager/src/features/Patch/PatchBody.tsx index 7c99e66f9e..47b1db1578 100644 --- a/plugins/github-release-manager/src/features/Patch/PatchBody.tsx +++ b/plugins/github-release-manager/src/features/Patch/PatchBody.tsx @@ -32,6 +32,7 @@ import { } from '@material-ui/core'; import FileCopyIcon from '@material-ui/icons/FileCopy'; import OpenInNewIcon from '@material-ui/icons/OpenInNew'; +import { useApi } from '@backstage/core'; import { GetBranchResult, @@ -41,12 +42,12 @@ import { CalverTagParts } from '../../helpers/tagParts/getCalverTagParts'; import { CenteredCircularProgress } from '../../components/CenteredCircularProgress'; import { ComponentConfigPatch } from '../../types/types'; import { Differ } from '../../components/Differ'; +import { githubReleaseManagerApiRef } from '../../api/serviceApiRef'; import { GitHubReleaseManagerError } from '../../errors/GitHubReleaseManagerError'; import { ResponseStepDialog } from '../../components/ResponseStepDialog/ResponseStepDialog'; import { SemverTagParts } from '../../helpers/tagParts/getSemverTagParts'; import { TEST_IDS } from '../../test-helpers/test-ids'; import { usePatch } from './hooks/usePatch'; -import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext'; import { useProjectContext } from '../../contexts/ProjectContext'; import { useStyles } from '../../styles/styles'; @@ -65,7 +66,7 @@ export const PatchBody = ({ successCb, tagParts, }: PatchBodyProps) => { - const { pluginApiClient } = usePluginApiClientContext(); + const pluginApiClient = useApi(githubReleaseManagerApiRef); const { project } = useProjectContext(); const [checkedCommitIndex, setCheckedCommitIndex] = useState(-1); diff --git a/plugins/github-release-manager/src/features/Patch/hooks/usePatch.ts b/plugins/github-release-manager/src/features/Patch/hooks/usePatch.ts index 06001f1771..5da8d020b9 100644 --- a/plugins/github-release-manager/src/features/Patch/hooks/usePatch.ts +++ b/plugins/github-release-manager/src/features/Patch/hooks/usePatch.ts @@ -16,6 +16,7 @@ import { useEffect, useState } from 'react'; import { useAsync, useAsyncFn } from 'react-use'; +import { useApi } from '@backstage/core'; import { GetLatestReleaseResult, @@ -23,9 +24,9 @@ import { } from '../../../api/PluginApiClient'; import { CalverTagParts } from '../../../helpers/tagParts/getCalverTagParts'; import { ComponentConfigPatch, CardHook } from '../../../types/types'; +import { githubReleaseManagerApiRef } from '../../../api/serviceApiRef'; import { Project } from '../../../contexts/ProjectContext'; import { SemverTagParts } from '../../../helpers/tagParts/getSemverTagParts'; -import { usePluginApiClientContext } from '../../../contexts/PluginApiClientContext'; import { useResponseSteps } from '../../../hooks/useResponseSteps'; interface Patch { @@ -44,7 +45,7 @@ export function usePatch({ tagParts, successCb, }: Patch): CardHook { - const { pluginApiClient } = usePluginApiClientContext(); + const pluginApiClient = useApi(githubReleaseManagerApiRef); const { responseSteps, addStepToResponseSteps, diff --git a/plugins/github-release-manager/src/features/PromoteRc/hooks/usePromoteRc.ts b/plugins/github-release-manager/src/features/PromoteRc/hooks/usePromoteRc.ts index 4d1a9e4037..6ff6c5ef9f 100644 --- a/plugins/github-release-manager/src/features/PromoteRc/hooks/usePromoteRc.ts +++ b/plugins/github-release-manager/src/features/PromoteRc/hooks/usePromoteRc.ts @@ -16,11 +16,12 @@ import { useState, useEffect } from 'react'; import { useAsync, useAsyncFn } from 'react-use'; +import { useApi } from '@backstage/core'; -import { GetLatestReleaseResult } from '../../../api/PluginApiClient'; import { CardHook, ComponentConfigPromoteRc } from '../../../types/types'; +import { GetLatestReleaseResult } from '../../../api/PluginApiClient'; +import { githubReleaseManagerApiRef } from '../../../api/serviceApiRef'; import { useProjectContext } from '../../../contexts/ProjectContext'; -import { usePluginApiClientContext } from '../../../contexts/PluginApiClientContext'; import { useResponseSteps } from '../../../hooks/useResponseSteps'; interface PromoteRc { @@ -34,7 +35,7 @@ export function usePromoteRc({ releaseVersion, successCb, }: PromoteRc): CardHook { - const { pluginApiClient } = usePluginApiClientContext(); + const pluginApiClient = useApi(githubReleaseManagerApiRef); const { project } = useProjectContext(); const { responseSteps, diff --git a/plugins/github-release-manager/src/features/RepoDetailsForm/Owner.tsx b/plugins/github-release-manager/src/features/RepoDetailsForm/Owner.tsx index 3cd2be6f7e..a392671c0f 100644 --- a/plugins/github-release-manager/src/features/RepoDetailsForm/Owner.tsx +++ b/plugins/github-release-manager/src/features/RepoDetailsForm/Owner.tsx @@ -24,16 +24,17 @@ import { MenuItem, Select, } from '@material-ui/core'; +import { useApi } from '@backstage/core'; import { CenteredCircularProgress } from '../../components/CenteredCircularProgress'; +import { githubReleaseManagerApiRef } from '../../api/serviceApiRef'; import { TEST_IDS } from '../../test-helpers/test-ids'; import { useFormClasses } from './styles'; -import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext'; import { useProjectContext } from '../../contexts/ProjectContext'; import { useQueryHandler } from '../../hooks/useQueryHandler'; export function Owner({ username }: { username: string }) { - const { pluginApiClient } = usePluginApiClientContext(); + const pluginApiClient = useApi(githubReleaseManagerApiRef); const { project } = useProjectContext(); const formClasses = useFormClasses(); const navigate = useNavigate(); diff --git a/plugins/github-release-manager/src/features/RepoDetailsForm/Repo.tsx b/plugins/github-release-manager/src/features/RepoDetailsForm/Repo.tsx index cb08a651cb..b1f8298495 100644 --- a/plugins/github-release-manager/src/features/RepoDetailsForm/Repo.tsx +++ b/plugins/github-release-manager/src/features/RepoDetailsForm/Repo.tsx @@ -24,16 +24,17 @@ import { MenuItem, Select, } from '@material-ui/core'; +import { useApi } from '@backstage/core'; import { CenteredCircularProgress } from '../../components/CenteredCircularProgress'; +import { githubReleaseManagerApiRef } from '../../api/serviceApiRef'; import { TEST_IDS } from '../../test-helpers/test-ids'; import { useFormClasses } from './styles'; -import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext'; import { useProjectContext } from '../../contexts/ProjectContext'; import { useQueryHandler } from '../../hooks/useQueryHandler'; export function Repo() { - const { pluginApiClient } = usePluginApiClientContext(); + const pluginApiClient = useApi(githubReleaseManagerApiRef); const { project } = useProjectContext(); const navigate = useNavigate(); const formClasses = useFormClasses(); diff --git a/plugins/github-release-manager/src/features/Stats/Info/hooks/useGetReleaseTimes.tsx b/plugins/github-release-manager/src/features/Stats/Info/hooks/useGetReleaseTimes.tsx index 2909763a8f..afe9a2f8c5 100644 --- a/plugins/github-release-manager/src/features/Stats/Info/hooks/useGetReleaseTimes.tsx +++ b/plugins/github-release-manager/src/features/Stats/Info/hooks/useGetReleaseTimes.tsx @@ -17,9 +17,10 @@ import { useEffect, useState } from 'react'; import { useAsync, useAsyncFn } from 'react-use'; import { DateTime } from 'luxon'; +import { useApi } from '@backstage/core'; import { getReleaseCommitPairs } from '../helpers/getReleaseCommitPairs'; -import { usePluginApiClientContext } from '../../../../contexts/PluginApiClientContext'; +import { githubReleaseManagerApiRef } from '../../../../api/serviceApiRef'; import { useProjectContext } from '../../../../contexts/ProjectContext'; import { useReleaseStatsContext } from '../../contexts/ReleaseStatsContext'; @@ -45,7 +46,7 @@ type ReleaseTime = { }; export function useGetReleaseTimes() { - const { pluginApiClient } = usePluginApiClientContext(); + const pluginApiClient = useApi(githubReleaseManagerApiRef); const { project } = useProjectContext(); const { releaseStats } = useReleaseStatsContext(); const [averageReleaseTime, setAverageReleaseTime] = useState( diff --git a/plugins/github-release-manager/src/features/Stats/hooks/useGetCommit.ts b/plugins/github-release-manager/src/features/Stats/hooks/useGetCommit.ts index 95db493774..f9681beda6 100644 --- a/plugins/github-release-manager/src/features/Stats/hooks/useGetCommit.ts +++ b/plugins/github-release-manager/src/features/Stats/hooks/useGetCommit.ts @@ -15,13 +15,14 @@ */ import { useAsync } from 'react-use'; +import { useApi } from '@backstage/core'; +import { githubReleaseManagerApiRef } from '../../../api/serviceApiRef'; import { GitHubReleaseManagerError } from '../../../errors/GitHubReleaseManagerError'; -import { usePluginApiClientContext } from '../../../contexts/PluginApiClientContext'; import { useProjectContext } from '../../../contexts/ProjectContext'; export const useGetCommit = ({ ref }: { ref?: string }) => { - const { pluginApiClient } = usePluginApiClientContext(); + const pluginApiClient = useApi(githubReleaseManagerApiRef); const { project } = useProjectContext(); const commit = useAsync(async () => { diff --git a/plugins/github-release-manager/src/features/Stats/hooks/useGetStats.ts b/plugins/github-release-manager/src/features/Stats/hooks/useGetStats.ts index f918be0c97..3e093f68aa 100644 --- a/plugins/github-release-manager/src/features/Stats/hooks/useGetStats.ts +++ b/plugins/github-release-manager/src/features/Stats/hooks/useGetStats.ts @@ -14,13 +14,14 @@ * limitations under the License. */ +import { useApi } from '@backstage/core'; import { useAsync } from 'react-use'; -import { usePluginApiClientContext } from '../../../contexts/PluginApiClientContext'; +import { githubReleaseManagerApiRef } from '../../../api/serviceApiRef'; import { useProjectContext } from '../../../contexts/ProjectContext'; export const useGetStats = () => { - const { pluginApiClient } = usePluginApiClientContext(); + const pluginApiClient = useApi(githubReleaseManagerApiRef); const { project } = useProjectContext(); const stats = useAsync(async () => { From ac64ec2228c49a976bc994d320f94da9a051f6c0 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Thu, 29 Apr 2021 10:55:31 +0200 Subject: [PATCH 080/140] Fix tests Signed-off-by: Erik Engervall --- .../src/features/CreateRc/CreateRc.test.tsx | 5 ++--- .../features/CreateRc/hooks/useCreateRc.test.tsx | 7 +++---- .../src/features/Features.test.tsx | 16 +++++++++------- .../src/features/Info/Info.test.tsx | 3 +-- .../src/features/Patch/Patch.test.tsx | 3 +-- .../src/features/Patch/PatchBody.test.tsx | 12 +++++------- .../src/features/Patch/hooks/usePatch.test.ts | 7 +++---- .../src/features/PromoteRc/PromoteRc.test.tsx | 3 +-- .../features/PromoteRc/PromoteRcBody.test.tsx | 3 +-- .../PromoteRc/hooks/usePromoteRc.test.ts | 7 +++---- .../src/features/RepoDetailsForm/Owner.test.tsx | 12 +++++------- .../src/features/RepoDetailsForm/Repo.test.tsx | 12 +++++------- .../RepoDetailsForm/VersioningStrategy.test.tsx | 3 +-- .../src/helpers/tagParts/getTagParts.test.ts | 7 +++---- .../src/hooks/useQueryHandler.test.tsx | 3 +-- 15 files changed, 44 insertions(+), 59 deletions(-) diff --git a/plugins/github-release-manager/src/features/CreateRc/CreateRc.test.tsx b/plugins/github-release-manager/src/features/CreateRc/CreateRc.test.tsx index 3c4a324b25..5f0ccaa20c 100644 --- a/plugins/github-release-manager/src/features/CreateRc/CreateRc.test.tsx +++ b/plugins/github-release-manager/src/features/CreateRc/CreateRc.test.tsx @@ -25,8 +25,10 @@ import { mockReleaseVersionCalver, mockSemverProject, } from '../../test-helpers/test-helpers'; +import { CreateRc } from './CreateRc'; import { TEST_IDS } from '../../test-helpers/test-ids'; import { useCreateRc } from './hooks/useCreateRc'; +import { useProjectContext } from '../../contexts/ProjectContext'; jest.mock('../../contexts/ProjectContext', () => ({ useProjectContext: jest.fn(() => ({ @@ -46,9 +48,6 @@ jest.mock('./hooks/useCreateRc', () => ({ } as ReturnType), })); -import { useProjectContext } from '../../contexts/ProjectContext'; -import { CreateRc } from './CreateRc'; - describe('CreateRc', () => { it('should display CTA', () => { const { getByTestId } = render( diff --git a/plugins/github-release-manager/src/features/CreateRc/hooks/useCreateRc.test.tsx b/plugins/github-release-manager/src/features/CreateRc/hooks/useCreateRc.test.tsx index cf4fa0f5ed..adbbe3b9f6 100644 --- a/plugins/github-release-manager/src/features/CreateRc/hooks/useCreateRc.test.tsx +++ b/plugins/github-release-manager/src/features/CreateRc/hooks/useCreateRc.test.tsx @@ -26,10 +26,9 @@ import { } from '../../../test-helpers/test-helpers'; import { useCreateRc } from './useCreateRc'; -jest.mock('../../../contexts/PluginApiClientContext', () => ({ - usePluginApiClientContext: () => ({ - pluginApiClient: mockApiClient, - }), +jest.mock('@backstage/core', () => ({ + useApi: () => mockApiClient, + createApiRef: jest.fn(), })); describe('useCreateRc', () => { diff --git a/plugins/github-release-manager/src/features/Features.test.tsx b/plugins/github-release-manager/src/features/Features.test.tsx index d53f542cef..e2f5428331 100644 --- a/plugins/github-release-manager/src/features/Features.test.tsx +++ b/plugins/github-release-manager/src/features/Features.test.tsx @@ -17,13 +17,17 @@ import React from 'react'; import { render, act, waitFor } from '@testing-library/react'; +import { Features } from './Features'; import { mockApiClient, mockCalverProject } from '../test-helpers/test-helpers'; import { TEST_IDS } from '../test-helpers/test-ids'; -jest.mock('../contexts/PluginApiClientContext', () => ({ - usePluginApiClientContext: () => ({ - pluginApiClient: mockApiClient, - }), +jest.mock('@backstage/core', () => ({ + useApi: () => mockApiClient, + createApiRef: jest.fn(), + ErrorBoundary: ({ children }: { children: React.ReactNode }) => ( + <>{children} + ), + InfoCard: ({ children }: { children: React.ReactNode }) => <>{children}, })); jest.mock('../contexts/ProjectContext', () => ({ useProjectContext: () => ({ @@ -31,8 +35,6 @@ jest.mock('../contexts/ProjectContext', () => ({ }), })); -import { Features } from './Features'; - describe('Features', () => { it('should omit features omitted via configuration', async () => { const { getByTestId } = render( @@ -52,7 +54,7 @@ describe('Features', () => { expect(getByTestId(TEST_IDS.info.info)).toMatchInlineSnapshot(`
({ useProjectContext: () => ({ @@ -29,8 +30,6 @@ jest.mock('../../contexts/ProjectContext', () => ({ }), })); -import { Info } from './Info'; - describe('Info', () => { it('should return early if no latestRelease exists', async () => { const { findByText } = render( diff --git a/plugins/github-release-manager/src/features/Patch/Patch.test.tsx b/plugins/github-release-manager/src/features/Patch/Patch.test.tsx index 2f7f170d9f..458861c802 100644 --- a/plugins/github-release-manager/src/features/Patch/Patch.test.tsx +++ b/plugins/github-release-manager/src/features/Patch/Patch.test.tsx @@ -22,6 +22,7 @@ import { mockCalverProject, } from '../../test-helpers/test-helpers'; import { TEST_IDS } from '../../test-helpers/test-ids'; +import { Patch } from './Patch'; jest.mock('../../contexts/ProjectContext', () => ({ useProjectContext: () => ({ @@ -29,8 +30,6 @@ jest.mock('../../contexts/ProjectContext', () => ({ }), })); -import { Patch } from './Patch'; - describe('Patch', () => { it('should return early if no latestRelease exists', () => { const { getByTestId } = render( diff --git a/plugins/github-release-manager/src/features/Patch/PatchBody.test.tsx b/plugins/github-release-manager/src/features/Patch/PatchBody.test.tsx index be9ea1ce45..38c09ab943 100644 --- a/plugins/github-release-manager/src/features/Patch/PatchBody.test.tsx +++ b/plugins/github-release-manager/src/features/Patch/PatchBody.test.tsx @@ -26,11 +26,12 @@ import { mockReleaseVersionCalver, mockTagParts, } from '../../test-helpers/test-helpers'; +import { PatchBody } from './PatchBody'; +import { TEST_IDS } from '../../test-helpers/test-ids'; -jest.mock('../../contexts/PluginApiClientContext', () => ({ - usePluginApiClientContext: () => ({ - pluginApiClient: mockApiClient, - }), +jest.mock('@backstage/core', () => ({ + useApi: () => mockApiClient, + createApiRef: jest.fn(), })); jest.mock('../../contexts/ProjectContext', () => ({ useProjectContext: () => ({ @@ -45,9 +46,6 @@ jest.mock('./hooks/usePatch', () => ({ }), })); -import { PatchBody } from './PatchBody'; -import { TEST_IDS } from '../../test-helpers/test-ids'; - describe('PatchBody', () => { beforeEach(jest.clearAllMocks); diff --git a/plugins/github-release-manager/src/features/Patch/hooks/usePatch.test.ts b/plugins/github-release-manager/src/features/Patch/hooks/usePatch.test.ts index be25b84809..5fe1614e42 100644 --- a/plugins/github-release-manager/src/features/Patch/hooks/usePatch.test.ts +++ b/plugins/github-release-manager/src/features/Patch/hooks/usePatch.test.ts @@ -27,10 +27,9 @@ import { } from '../../../test-helpers/test-helpers'; import { usePatch } from './usePatch'; -jest.mock('../../../contexts/PluginApiClientContext', () => ({ - usePluginApiClientContext: () => ({ - pluginApiClient: mockApiClient, - }), +jest.mock('@backstage/core', () => ({ + useApi: () => mockApiClient, + createApiRef: jest.fn(), })); describe('patch', () => { diff --git a/plugins/github-release-manager/src/features/PromoteRc/PromoteRc.test.tsx b/plugins/github-release-manager/src/features/PromoteRc/PromoteRc.test.tsx index f4e7c1fb41..089b626da3 100644 --- a/plugins/github-release-manager/src/features/PromoteRc/PromoteRc.test.tsx +++ b/plugins/github-release-manager/src/features/PromoteRc/PromoteRc.test.tsx @@ -22,6 +22,7 @@ import { mockReleaseVersionCalver, } from '../../test-helpers/test-helpers'; import { TEST_IDS } from '../../test-helpers/test-ids'; +import { PromoteRc } from './PromoteRc'; jest.mock('./PromoteRcBody', () => ({ PromoteRcBody: () => ( @@ -29,8 +30,6 @@ jest.mock('./PromoteRcBody', () => ({ ), })); -import { PromoteRc } from './PromoteRc'; - describe('PromoteRc', () => { it('return early if no latest release present', () => { const { getByTestId } = render(); diff --git a/plugins/github-release-manager/src/features/PromoteRc/PromoteRcBody.test.tsx b/plugins/github-release-manager/src/features/PromoteRc/PromoteRcBody.test.tsx index 2d67832587..14b294e581 100644 --- a/plugins/github-release-manager/src/features/PromoteRc/PromoteRcBody.test.tsx +++ b/plugins/github-release-manager/src/features/PromoteRc/PromoteRcBody.test.tsx @@ -19,6 +19,7 @@ import { render } from '@testing-library/react'; import { mockReleaseCandidateCalver } from '../../test-helpers/test-helpers'; import { TEST_IDS } from '../../test-helpers/test-ids'; +import { PromoteRcBody } from './PromoteRcBody'; jest.mock('./hooks/usePromoteRc', () => ({ usePromoteRc: () => ({ @@ -28,8 +29,6 @@ jest.mock('./hooks/usePromoteRc', () => ({ }), })); -import { PromoteRcBody } from './PromoteRcBody'; - describe('PromoteRcBody', () => { it('should display CTA', () => { const { getByTestId } = render( diff --git a/plugins/github-release-manager/src/features/PromoteRc/hooks/usePromoteRc.test.ts b/plugins/github-release-manager/src/features/PromoteRc/hooks/usePromoteRc.test.ts index 4b7149ba7b..d682476d33 100644 --- a/plugins/github-release-manager/src/features/PromoteRc/hooks/usePromoteRc.test.ts +++ b/plugins/github-release-manager/src/features/PromoteRc/hooks/usePromoteRc.test.ts @@ -24,10 +24,9 @@ import { } from '../../../test-helpers/test-helpers'; import { usePromoteRc } from './usePromoteRc'; -jest.mock('../../../contexts/PluginApiClientContext', () => ({ - usePluginApiClientContext: () => ({ - pluginApiClient: mockApiClient, - }), +jest.mock('@backstage/core', () => ({ + useApi: () => mockApiClient, + createApiRef: jest.fn(), })); jest.mock('../../../contexts/ProjectContext', () => ({ useProjectContext: () => ({ diff --git a/plugins/github-release-manager/src/features/RepoDetailsForm/Owner.test.tsx b/plugins/github-release-manager/src/features/RepoDetailsForm/Owner.test.tsx index da95f5fa50..aed6fe3496 100644 --- a/plugins/github-release-manager/src/features/RepoDetailsForm/Owner.test.tsx +++ b/plugins/github-release-manager/src/features/RepoDetailsForm/Owner.test.tsx @@ -23,6 +23,8 @@ import { mockSearchCalver, } from '../../test-helpers/test-helpers'; import { TEST_IDS } from '../../test-helpers/test-ids'; +import { useProjectContext } from '../../contexts/ProjectContext'; +import { Owner } from './Owner'; jest.mock('react-router', () => ({ useNavigate: jest.fn(), @@ -30,10 +32,9 @@ jest.mock('react-router', () => ({ search: mockSearchCalver, })), })); -jest.mock('../../contexts/PluginApiClientContext', () => ({ - usePluginApiClientContext: () => ({ - pluginApiClient: mockApiClient, - }), +jest.mock('@backstage/core', () => ({ + useApi: () => mockApiClient, + createApiRef: jest.fn(), })); jest.mock('../../contexts/ProjectContext', () => ({ useProjectContext: jest.fn(() => ({ @@ -41,9 +42,6 @@ jest.mock('../../contexts/ProjectContext', () => ({ })), })); -import { useProjectContext } from '../../contexts/ProjectContext'; -import { Owner } from './Owner'; - describe('Owner', () => { beforeEach(jest.clearAllMocks); diff --git a/plugins/github-release-manager/src/features/RepoDetailsForm/Repo.test.tsx b/plugins/github-release-manager/src/features/RepoDetailsForm/Repo.test.tsx index ab6c98ea5f..9b087d47e2 100644 --- a/plugins/github-release-manager/src/features/RepoDetailsForm/Repo.test.tsx +++ b/plugins/github-release-manager/src/features/RepoDetailsForm/Repo.test.tsx @@ -23,6 +23,8 @@ import { mockSearchCalver, } from '../../test-helpers/test-helpers'; import { TEST_IDS } from '../../test-helpers/test-ids'; +import { useProjectContext } from '../../contexts/ProjectContext'; +import { Repo } from './Repo'; jest.mock('react-router', () => ({ useNavigate: jest.fn(), @@ -30,10 +32,9 @@ jest.mock('react-router', () => ({ search: mockSearchCalver, })), })); -jest.mock('../../contexts/PluginApiClientContext', () => ({ - usePluginApiClientContext: () => ({ - pluginApiClient: mockApiClient, - }), +jest.mock('@backstage/core', () => ({ + useApi: () => mockApiClient, + createApiRef: jest.fn(), })); jest.mock('../../contexts/ProjectContext', () => ({ useProjectContext: jest.fn(() => ({ @@ -41,9 +42,6 @@ jest.mock('../../contexts/ProjectContext', () => ({ })), })); -import { useProjectContext } from '../../contexts/ProjectContext'; -import { Repo } from './Repo'; - describe('Repo', () => { beforeEach(jest.clearAllMocks); diff --git a/plugins/github-release-manager/src/features/RepoDetailsForm/VersioningStrategy.test.tsx b/plugins/github-release-manager/src/features/RepoDetailsForm/VersioningStrategy.test.tsx index 4d36b26ed6..42a17f9b43 100644 --- a/plugins/github-release-manager/src/features/RepoDetailsForm/VersioningStrategy.test.tsx +++ b/plugins/github-release-manager/src/features/RepoDetailsForm/VersioningStrategy.test.tsx @@ -21,6 +21,7 @@ import { mockSemverProject, mockSearchCalver, } from '../../test-helpers/test-helpers'; +import { VersioningStrategy } from './VersioningStrategy'; const mockNavigate = jest.fn(); @@ -36,8 +37,6 @@ jest.mock('../../contexts/ProjectContext', () => ({ }), })); -import { VersioningStrategy } from './VersioningStrategy'; - describe('Repo', () => { beforeEach(jest.clearAllMocks); diff --git a/plugins/github-release-manager/src/helpers/tagParts/getTagParts.test.ts b/plugins/github-release-manager/src/helpers/tagParts/getTagParts.test.ts index fc1ab1479b..825d23c001 100644 --- a/plugins/github-release-manager/src/helpers/tagParts/getTagParts.test.ts +++ b/plugins/github-release-manager/src/helpers/tagParts/getTagParts.test.ts @@ -18,6 +18,9 @@ import { mockCalverProject, mockSemverProject, } from '../../test-helpers/test-helpers'; +import { getCalverTagParts } from './getCalverTagParts'; +import { getSemverTagParts } from './getSemverTagParts'; +import { getTagParts } from './getTagParts'; jest.mock('./getCalverTagParts', () => ({ getCalverTagParts: jest.fn(), @@ -26,10 +29,6 @@ jest.mock('./getSemverTagParts', () => ({ getSemverTagParts: jest.fn(), })); -import { getCalverTagParts } from './getCalverTagParts'; -import { getSemverTagParts } from './getSemverTagParts'; -import { getTagParts } from './getTagParts'; - describe('getTagParts', () => { beforeEach(jest.resetAllMocks); diff --git a/plugins/github-release-manager/src/hooks/useQueryHandler.test.tsx b/plugins/github-release-manager/src/hooks/useQueryHandler.test.tsx index a80fa3df04..4024e37054 100644 --- a/plugins/github-release-manager/src/hooks/useQueryHandler.test.tsx +++ b/plugins/github-release-manager/src/hooks/useQueryHandler.test.tsx @@ -18,6 +18,7 @@ import React from 'react'; import { render } from '@testing-library/react'; import { mockSearchSemver } from '../test-helpers/test-helpers'; +import { useQueryHandler } from './useQueryHandler'; jest.mock('react-router', () => ({ useLocation: jest.fn(() => ({ @@ -25,8 +26,6 @@ jest.mock('react-router', () => ({ })), })); -import { useQueryHandler } from './useQueryHandler'; - const TEST_ID = 'grm--use-query-handler'; const MockComponent = () => { From 09fc040eec6f374f970db948f4534181edfe0e08 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Thu, 29 Apr 2021 10:57:39 +0200 Subject: [PATCH 081/140] Address PR comment regarding ConfigReader mocking Signed-off-by: Erik Engervall --- .../src/api/PluginApiClient.test.ts | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/plugins/github-release-manager/src/api/PluginApiClient.test.ts b/plugins/github-release-manager/src/api/PluginApiClient.test.ts index 867974b80b..d4f86ef9e7 100644 --- a/plugins/github-release-manager/src/api/PluginApiClient.test.ts +++ b/plugins/github-release-manager/src/api/PluginApiClient.test.ts @@ -14,19 +14,13 @@ * limitations under the License. */ -import { OAuthApi } from '@backstage/core'; +import { ConfigReader, OAuthApi } from '@backstage/core'; import { PluginApiClient } from './PluginApiClient'; -jest.mock('@backstage/integration', () => ({ - readGitHubIntegrationConfigs: jest.fn(() => []), -})); - describe('PluginApiClient', () => { it('should return the default plugin api client', () => { - const configApi = { - getOptionalConfigArray: jest.fn(), - } as any; + const configApi = new ConfigReader({}); const githubAuthApi: OAuthApi = { getAccessToken: jest.fn(), }; From 4a83f6493b293868a7c3c52ea34d620adc357b6a Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Thu, 29 Apr 2021 11:04:56 +0200 Subject: [PATCH 082/140] Rename github-release-manager to git-release-manager Signed-off-by: Erik Engervall --- ...elease-manager.yaml => git-release-manager.yaml} | 6 +++--- ...anager-logo.svg => git-release-manager-logo.svg} | 0 packages/app/package.json | 2 +- .../.eslintrc.js | 0 .../README.md | 0 .../dev/README.md | 6 +++--- .../dev/index.tsx | 0 .../package.json | 2 +- .../src/GitHubReleaseManager.tsx | 0 .../src/api/PluginApiClient.test.ts | 0 .../src/api/PluginApiClient.ts | 0 .../src/api/serviceApiRef.test.ts | 2 +- .../src/api/serviceApiRef.ts | 4 ++-- .../src/components/CenteredCircularProgress.tsx | 0 .../src/components/Differ.test.tsx | 0 .../src/components/Differ.tsx | 0 .../src/components/Divider.test.tsx | 0 .../src/components/Divider.tsx | 0 .../src/components/InfoCardPlus.test.tsx | 0 .../src/components/InfoCardPlus.tsx | 0 .../src/components/NoLatestRelease.test.tsx | 0 .../src/components/NoLatestRelease.tsx | 0 .../LinearProgressWithLabel.test.tsx | 0 .../ResponseStepDialog/LinearProgressWithLabel.tsx | 0 .../ResponseStepDialog/ResponseStepDialog.test.tsx | 0 .../ResponseStepDialog/ResponseStepDialog.tsx | 0 .../ResponseStepDialog/ResponseStepList.test.tsx | 0 .../ResponseStepDialog/ResponseStepList.tsx | 0 .../ResponseStepListItem.test.tsx | 0 .../ResponseStepDialog/ResponseStepListItem.tsx | 0 .../src/components/Transition.tsx | 0 .../src/constants/constants.test.ts | 0 .../src/constants/constants.ts | 0 .../src/contexts/ProjectContext.ts | 0 .../src/contexts/RefetchContext.ts | 0 .../src/errors/GitHubReleaseManagerError.ts | 0 .../src/features/CreateRc/CreateRc.test.tsx | 0 .../src/features/CreateRc/CreateRc.tsx | 0 .../features/CreateRc/hooks/useCreateRc.test.tsx | 0 .../src/features/CreateRc/hooks/useCreateRc.ts | 0 .../src/features/Features.test.tsx | 0 .../src/features/Features.tsx | 0 .../src/features/Info/Info.test.tsx | 0 .../src/features/Info/Info.tsx | 0 .../src/features/Info/flow.png | Bin .../src/features/Patch/Patch.test.tsx | 0 .../src/features/Patch/Patch.tsx | 0 .../src/features/Patch/PatchBody.test.tsx | 0 .../src/features/Patch/PatchBody.tsx | 0 .../src/features/Patch/hooks/usePatch.test.ts | 0 .../src/features/Patch/hooks/usePatch.ts | 0 .../src/features/PromoteRc/PromoteRc.test.tsx | 0 .../src/features/PromoteRc/PromoteRc.tsx | 0 .../src/features/PromoteRc/PromoteRcBody.test.tsx | 0 .../src/features/PromoteRc/PromoteRcBody.tsx | 0 .../features/PromoteRc/hooks/usePromoteRc.test.ts | 0 .../src/features/PromoteRc/hooks/usePromoteRc.ts | 0 .../src/features/RepoDetailsForm/Owner.test.tsx | 0 .../src/features/RepoDetailsForm/Owner.tsx | 0 .../src/features/RepoDetailsForm/Repo.test.tsx | 0 .../src/features/RepoDetailsForm/Repo.tsx | 0 .../features/RepoDetailsForm/RepoDetailsForm.tsx | 0 .../RepoDetailsForm/VersioningStrategy.test.tsx | 0 .../features/RepoDetailsForm/VersioningStrategy.tsx | 0 .../src/features/RepoDetailsForm/styles.ts | 0 .../src/features/Stats/DialogBody.tsx | 0 .../src/features/Stats/DialogTitle.tsx | 0 .../Stats/Info/InDepth/AverageReleaseTime.tsx | 0 .../src/features/Stats/Info/InDepth/InDepth.tsx | 0 .../Stats/Info/InDepth/LongestReleaseTime.tsx | 0 .../src/features/Stats/Info/Info.tsx | 0 .../src/features/Stats/Info/Summary.tsx | 0 .../Info/helpers/getReleaseCommitPairs.test.tsx | 0 .../Stats/Info/helpers/getReleaseCommitPairs.tsx | 0 .../Stats/Info/hooks/useGetReleaseTimes.tsx | 0 .../src/features/Stats/Row/Row.tsx | 0 .../Stats/Row/RowCollapsed/ReleaseTagList.tsx | 0 .../features/Stats/Row/RowCollapsed/ReleaseTime.tsx | 0 .../Stats/Row/RowCollapsed/RowCollapsed.tsx | 0 .../src/features/Stats/Stats.tsx | 0 .../src/features/Stats/Warn.tsx | 0 .../features/Stats/contexts/ReleaseStatsContext.tsx | 0 .../Stats/helpers/getDecimalNumber.test.tsx | 0 .../src/features/Stats/helpers/getDecimalNumber.tsx | 0 .../Stats/helpers/getMappedReleases.test.tsx | 0 .../features/Stats/helpers/getMappedReleases.tsx | 0 .../features/Stats/helpers/getReleaseStats.test.tsx | 0 .../src/features/Stats/helpers/getReleaseStats.tsx | 0 .../src/features/Stats/helpers/getSummary.test.tsx | 0 .../src/features/Stats/helpers/getSummary.tsx | 0 .../src/features/Stats/hooks/useGetCommit.ts | 0 .../src/features/Stats/hooks/useGetStats.ts | 0 .../src/helpers/createResponseStepError.test.ts | 0 .../src/helpers/createResponseStepError.ts | 0 .../src/helpers/getBumpedTag.test.ts | 0 .../src/helpers/getBumpedTag.ts | 0 .../src/helpers/getRcGitHubInfo.test.ts | 0 .../src/helpers/getRcGitHubInfo.ts | 0 .../src/helpers/getShortCommitHash.test.ts | 0 .../src/helpers/getShortCommitHash.ts | 0 .../src/helpers/isCalverTagParts.test.ts | 0 .../src/helpers/isCalverTagParts.ts | 0 .../src/helpers/isProjectValid.test.ts | 0 .../src/helpers/isProjectValid.ts | 0 .../src/helpers/tagParts/getCalverTagParts.test.ts | 0 .../src/helpers/tagParts/getCalverTagParts.ts | 0 .../src/helpers/tagParts/getSemverTagParts.test.ts | 0 .../src/helpers/tagParts/getSemverTagParts.ts | 0 .../src/helpers/tagParts/getTagParts.test.ts | 0 .../src/helpers/tagParts/getTagParts.ts | 0 .../src/helpers/tagParts/validateTagName.ts | 0 .../src/helpers/tagParts/validateTagParts.test.ts | 0 .../src/hooks/useGetGitHubBatchInfo.test.ts | 0 .../src/hooks/useGetGitHubBatchInfo.ts | 0 .../src/hooks/useQueryHandler.test.tsx | 0 .../src/hooks/useQueryHandler.ts | 0 .../src/hooks/useResponseSteps.test.ts | 0 .../src/hooks/useResponseSteps.ts | 0 .../useVersioningStrategyMatchesRepoTags.test.tsx | 0 .../hooks/useVersioningStrategyMatchesRepoTags.ts | 0 .../src/index.ts | 0 .../src/plugin.test.ts | 2 +- .../src/plugin.ts | 2 +- .../src/routes.ts | 2 +- .../src/setupTests.ts | 0 .../src/styles/styles.ts | 0 .../src/test-helpers/stats.ts | 0 .../src/test-helpers/test-helpers.ts | 0 .../src/test-helpers/test-ids.ts | 0 .../src/types/types.ts | 0 130 files changed, 14 insertions(+), 14 deletions(-) rename microsite/data/plugins/{github-release-manager.yaml => git-release-manager.yaml} (64%) rename microsite/static/img/{github-release-manager-logo.svg => git-release-manager-logo.svg} (100%) rename plugins/{github-release-manager => git-release-manager}/.eslintrc.js (100%) rename plugins/{github-release-manager => git-release-manager}/README.md (100%) rename plugins/{github-release-manager => git-release-manager}/dev/README.md (72%) rename plugins/{github-release-manager => git-release-manager}/dev/index.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/package.json (96%) rename plugins/{github-release-manager => git-release-manager}/src/GitHubReleaseManager.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/api/PluginApiClient.test.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/api/PluginApiClient.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/api/serviceApiRef.test.ts (94%) rename plugins/{github-release-manager => git-release-manager}/src/api/serviceApiRef.ts (86%) rename plugins/{github-release-manager => git-release-manager}/src/components/CenteredCircularProgress.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/components/Differ.test.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/components/Differ.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/components/Divider.test.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/components/Divider.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/components/InfoCardPlus.test.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/components/InfoCardPlus.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/components/NoLatestRelease.test.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/components/NoLatestRelease.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/components/ResponseStepDialog/LinearProgressWithLabel.test.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/components/ResponseStepDialog/LinearProgressWithLabel.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/components/ResponseStepDialog/ResponseStepDialog.test.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/components/ResponseStepDialog/ResponseStepDialog.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/components/ResponseStepDialog/ResponseStepList.test.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/components/ResponseStepDialog/ResponseStepList.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/components/ResponseStepDialog/ResponseStepListItem.test.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/components/ResponseStepDialog/ResponseStepListItem.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/components/Transition.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/constants/constants.test.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/constants/constants.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/contexts/ProjectContext.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/contexts/RefetchContext.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/errors/GitHubReleaseManagerError.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/CreateRc/CreateRc.test.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/CreateRc/CreateRc.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/CreateRc/hooks/useCreateRc.test.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/CreateRc/hooks/useCreateRc.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Features.test.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Features.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Info/Info.test.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Info/Info.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Info/flow.png (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Patch/Patch.test.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Patch/Patch.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Patch/PatchBody.test.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Patch/PatchBody.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Patch/hooks/usePatch.test.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Patch/hooks/usePatch.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/PromoteRc/PromoteRc.test.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/PromoteRc/PromoteRc.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/PromoteRc/PromoteRcBody.test.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/PromoteRc/PromoteRcBody.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/PromoteRc/hooks/usePromoteRc.test.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/PromoteRc/hooks/usePromoteRc.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/RepoDetailsForm/Owner.test.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/RepoDetailsForm/Owner.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/RepoDetailsForm/Repo.test.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/RepoDetailsForm/Repo.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/RepoDetailsForm/RepoDetailsForm.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/RepoDetailsForm/VersioningStrategy.test.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/RepoDetailsForm/VersioningStrategy.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/RepoDetailsForm/styles.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Stats/DialogBody.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Stats/DialogTitle.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Stats/Info/InDepth/AverageReleaseTime.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Stats/Info/InDepth/InDepth.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Stats/Info/InDepth/LongestReleaseTime.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Stats/Info/Info.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Stats/Info/Summary.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Stats/Info/helpers/getReleaseCommitPairs.test.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Stats/Info/helpers/getReleaseCommitPairs.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Stats/Info/hooks/useGetReleaseTimes.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Stats/Row/Row.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Stats/Row/RowCollapsed/ReleaseTagList.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Stats/Row/RowCollapsed/ReleaseTime.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Stats/Row/RowCollapsed/RowCollapsed.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Stats/Stats.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Stats/Warn.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Stats/contexts/ReleaseStatsContext.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Stats/helpers/getDecimalNumber.test.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Stats/helpers/getDecimalNumber.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Stats/helpers/getMappedReleases.test.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Stats/helpers/getMappedReleases.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Stats/helpers/getReleaseStats.test.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Stats/helpers/getReleaseStats.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Stats/helpers/getSummary.test.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Stats/helpers/getSummary.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Stats/hooks/useGetCommit.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/features/Stats/hooks/useGetStats.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/helpers/createResponseStepError.test.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/helpers/createResponseStepError.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/helpers/getBumpedTag.test.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/helpers/getBumpedTag.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/helpers/getRcGitHubInfo.test.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/helpers/getRcGitHubInfo.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/helpers/getShortCommitHash.test.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/helpers/getShortCommitHash.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/helpers/isCalverTagParts.test.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/helpers/isCalverTagParts.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/helpers/isProjectValid.test.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/helpers/isProjectValid.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/helpers/tagParts/getCalverTagParts.test.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/helpers/tagParts/getCalverTagParts.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/helpers/tagParts/getSemverTagParts.test.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/helpers/tagParts/getSemverTagParts.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/helpers/tagParts/getTagParts.test.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/helpers/tagParts/getTagParts.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/helpers/tagParts/validateTagName.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/helpers/tagParts/validateTagParts.test.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/hooks/useGetGitHubBatchInfo.test.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/hooks/useGetGitHubBatchInfo.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/hooks/useQueryHandler.test.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/hooks/useQueryHandler.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/hooks/useResponseSteps.test.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/hooks/useResponseSteps.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/hooks/useVersioningStrategyMatchesRepoTags.test.tsx (100%) rename plugins/{github-release-manager => git-release-manager}/src/hooks/useVersioningStrategyMatchesRepoTags.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/index.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/plugin.test.ts (95%) rename plugins/{github-release-manager => git-release-manager}/src/plugin.ts (98%) rename plugins/{github-release-manager => git-release-manager}/src/routes.ts (95%) rename plugins/{github-release-manager => git-release-manager}/src/setupTests.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/styles/styles.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/test-helpers/stats.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/test-helpers/test-helpers.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/test-helpers/test-ids.ts (100%) rename plugins/{github-release-manager => git-release-manager}/src/types/types.ts (100%) diff --git a/microsite/data/plugins/github-release-manager.yaml b/microsite/data/plugins/git-release-manager.yaml similarity index 64% rename from microsite/data/plugins/github-release-manager.yaml rename to microsite/data/plugins/git-release-manager.yaml index febc03f714..7eec6040ac 100644 --- a/microsite/data/plugins/github-release-manager.yaml +++ b/microsite/data/plugins/git-release-manager.yaml @@ -4,6 +4,6 @@ author: '@Spotify' authorUrl: https://github.com/spotify category: Release management description: Manage releases without having to juggle git commands -documentation: https://github.com/backstage/backstage/tree/master/plugins/github-release-manager -iconUrl: img/github-release-manager-logo.svg -npmPackageName: '@backstage/plugin-github-release-manager' +documentation: https://github.com/backstage/backstage/tree/master/plugins/git-release-manager +iconUrl: img/git-release-manager-logo.svg +npmPackageName: '@backstage/plugin-git-release-manager' diff --git a/microsite/static/img/github-release-manager-logo.svg b/microsite/static/img/git-release-manager-logo.svg similarity index 100% rename from microsite/static/img/github-release-manager-logo.svg rename to microsite/static/img/git-release-manager-logo.svg diff --git a/packages/app/package.json b/packages/app/package.json index a3ef3698cb..70209f876c 100644 --- a/packages/app/package.json +++ b/packages/app/package.json @@ -28,7 +28,7 @@ "@backstage/plugin-newrelic": "^0.2.6", "@backstage/plugin-org": "^0.3.12", "@backstage/plugin-pagerduty": "0.3.2", - "@backstage/plugin-github-release-manager": "^0.1.1", + "@backstage/plugin-git-release-manager": "^0.1.1", "@backstage/plugin-rollbar": "^0.3.3", "@backstage/plugin-scaffolder": "^0.9.0", "@backstage/plugin-search": "^0.3.4", diff --git a/plugins/github-release-manager/.eslintrc.js b/plugins/git-release-manager/.eslintrc.js similarity index 100% rename from plugins/github-release-manager/.eslintrc.js rename to plugins/git-release-manager/.eslintrc.js diff --git a/plugins/github-release-manager/README.md b/plugins/git-release-manager/README.md similarity index 100% rename from plugins/github-release-manager/README.md rename to plugins/git-release-manager/README.md diff --git a/plugins/github-release-manager/dev/README.md b/plugins/git-release-manager/dev/README.md similarity index 72% rename from plugins/github-release-manager/dev/README.md rename to plugins/git-release-manager/dev/README.md index 52a8c8656b..c78083aa4f 100644 --- a/plugins/github-release-manager/dev/README.md +++ b/plugins/git-release-manager/dev/README.md @@ -1,12 +1,12 @@ -# github-release-manager +# git-release-manager -Welcome to the github-release-manager plugin! +Welcome to the git-release-manager plugin! _This plugin was created through the Backstage CLI_ ## Getting started -Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/github-release-manager](http://localhost:3000/github-release-manager). +Your plugin has been added to the example app in this repository, meaning you'll be able to access it by running `yarn start` in the root directory, and then navigating to [/git-release-manager](http://localhost:3000/git-release-manager). You can also serve the plugin in isolation by running `yarn start` in the plugin directory. This method of serving the plugin provides quicker iteration speed and a faster startup and hot reloads. diff --git a/plugins/github-release-manager/dev/index.tsx b/plugins/git-release-manager/dev/index.tsx similarity index 100% rename from plugins/github-release-manager/dev/index.tsx rename to plugins/git-release-manager/dev/index.tsx diff --git a/plugins/github-release-manager/package.json b/plugins/git-release-manager/package.json similarity index 96% rename from plugins/github-release-manager/package.json rename to plugins/git-release-manager/package.json index be544bc1fb..248250324a 100644 --- a/plugins/github-release-manager/package.json +++ b/plugins/git-release-manager/package.json @@ -1,5 +1,5 @@ { - "name": "@backstage/plugin-github-release-manager", + "name": "@backstage/plugin-git-release-manager", "version": "0.1.1", "main": "src/index.ts", "types": "src/index.ts", diff --git a/plugins/github-release-manager/src/GitHubReleaseManager.tsx b/plugins/git-release-manager/src/GitHubReleaseManager.tsx similarity index 100% rename from plugins/github-release-manager/src/GitHubReleaseManager.tsx rename to plugins/git-release-manager/src/GitHubReleaseManager.tsx diff --git a/plugins/github-release-manager/src/api/PluginApiClient.test.ts b/plugins/git-release-manager/src/api/PluginApiClient.test.ts similarity index 100% rename from plugins/github-release-manager/src/api/PluginApiClient.test.ts rename to plugins/git-release-manager/src/api/PluginApiClient.test.ts diff --git a/plugins/github-release-manager/src/api/PluginApiClient.ts b/plugins/git-release-manager/src/api/PluginApiClient.ts similarity index 100% rename from plugins/github-release-manager/src/api/PluginApiClient.ts rename to plugins/git-release-manager/src/api/PluginApiClient.ts diff --git a/plugins/github-release-manager/src/api/serviceApiRef.test.ts b/plugins/git-release-manager/src/api/serviceApiRef.test.ts similarity index 94% rename from plugins/github-release-manager/src/api/serviceApiRef.test.ts rename to plugins/git-release-manager/src/api/serviceApiRef.test.ts index 7dc22deb29..4e8c204c6b 100644 --- a/plugins/github-release-manager/src/api/serviceApiRef.test.ts +++ b/plugins/git-release-manager/src/api/serviceApiRef.test.ts @@ -24,7 +24,7 @@ describe('githubReleaseManagerApiRef', () => { ApiRefImpl { "config": Object { "description": "Used by the GitHub Release Manager plugin to make requests", - "id": "plugin.github-release-manager.service", + "id": "plugin.git-release-manager.service", }, } `); diff --git a/plugins/github-release-manager/src/api/serviceApiRef.ts b/plugins/git-release-manager/src/api/serviceApiRef.ts similarity index 86% rename from plugins/github-release-manager/src/api/serviceApiRef.ts rename to plugins/git-release-manager/src/api/serviceApiRef.ts index 4451dd8dc8..f44168c476 100644 --- a/plugins/github-release-manager/src/api/serviceApiRef.ts +++ b/plugins/git-release-manager/src/api/serviceApiRef.ts @@ -19,6 +19,6 @@ import { createApiRef } from '@backstage/core'; import { IPluginApiClient } from './PluginApiClient'; export const githubReleaseManagerApiRef = createApiRef({ - id: 'plugin.github-release-manager.service', - description: 'Used by the GitHub Release Manager plugin to make requests', + id: 'plugin.git-release-manager.service', + description: 'Used by the Git Release Manager plugin to make requests', }); diff --git a/plugins/github-release-manager/src/components/CenteredCircularProgress.tsx b/plugins/git-release-manager/src/components/CenteredCircularProgress.tsx similarity index 100% rename from plugins/github-release-manager/src/components/CenteredCircularProgress.tsx rename to plugins/git-release-manager/src/components/CenteredCircularProgress.tsx diff --git a/plugins/github-release-manager/src/components/Differ.test.tsx b/plugins/git-release-manager/src/components/Differ.test.tsx similarity index 100% rename from plugins/github-release-manager/src/components/Differ.test.tsx rename to plugins/git-release-manager/src/components/Differ.test.tsx diff --git a/plugins/github-release-manager/src/components/Differ.tsx b/plugins/git-release-manager/src/components/Differ.tsx similarity index 100% rename from plugins/github-release-manager/src/components/Differ.tsx rename to plugins/git-release-manager/src/components/Differ.tsx diff --git a/plugins/github-release-manager/src/components/Divider.test.tsx b/plugins/git-release-manager/src/components/Divider.test.tsx similarity index 100% rename from plugins/github-release-manager/src/components/Divider.test.tsx rename to plugins/git-release-manager/src/components/Divider.test.tsx diff --git a/plugins/github-release-manager/src/components/Divider.tsx b/plugins/git-release-manager/src/components/Divider.tsx similarity index 100% rename from plugins/github-release-manager/src/components/Divider.tsx rename to plugins/git-release-manager/src/components/Divider.tsx diff --git a/plugins/github-release-manager/src/components/InfoCardPlus.test.tsx b/plugins/git-release-manager/src/components/InfoCardPlus.test.tsx similarity index 100% rename from plugins/github-release-manager/src/components/InfoCardPlus.test.tsx rename to plugins/git-release-manager/src/components/InfoCardPlus.test.tsx diff --git a/plugins/github-release-manager/src/components/InfoCardPlus.tsx b/plugins/git-release-manager/src/components/InfoCardPlus.tsx similarity index 100% rename from plugins/github-release-manager/src/components/InfoCardPlus.tsx rename to plugins/git-release-manager/src/components/InfoCardPlus.tsx diff --git a/plugins/github-release-manager/src/components/NoLatestRelease.test.tsx b/plugins/git-release-manager/src/components/NoLatestRelease.test.tsx similarity index 100% rename from plugins/github-release-manager/src/components/NoLatestRelease.test.tsx rename to plugins/git-release-manager/src/components/NoLatestRelease.test.tsx diff --git a/plugins/github-release-manager/src/components/NoLatestRelease.tsx b/plugins/git-release-manager/src/components/NoLatestRelease.tsx similarity index 100% rename from plugins/github-release-manager/src/components/NoLatestRelease.tsx rename to plugins/git-release-manager/src/components/NoLatestRelease.tsx diff --git a/plugins/github-release-manager/src/components/ResponseStepDialog/LinearProgressWithLabel.test.tsx b/plugins/git-release-manager/src/components/ResponseStepDialog/LinearProgressWithLabel.test.tsx similarity index 100% rename from plugins/github-release-manager/src/components/ResponseStepDialog/LinearProgressWithLabel.test.tsx rename to plugins/git-release-manager/src/components/ResponseStepDialog/LinearProgressWithLabel.test.tsx diff --git a/plugins/github-release-manager/src/components/ResponseStepDialog/LinearProgressWithLabel.tsx b/plugins/git-release-manager/src/components/ResponseStepDialog/LinearProgressWithLabel.tsx similarity index 100% rename from plugins/github-release-manager/src/components/ResponseStepDialog/LinearProgressWithLabel.tsx rename to plugins/git-release-manager/src/components/ResponseStepDialog/LinearProgressWithLabel.tsx diff --git a/plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepDialog.test.tsx b/plugins/git-release-manager/src/components/ResponseStepDialog/ResponseStepDialog.test.tsx similarity index 100% rename from plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepDialog.test.tsx rename to plugins/git-release-manager/src/components/ResponseStepDialog/ResponseStepDialog.test.tsx diff --git a/plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepDialog.tsx b/plugins/git-release-manager/src/components/ResponseStepDialog/ResponseStepDialog.tsx similarity index 100% rename from plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepDialog.tsx rename to plugins/git-release-manager/src/components/ResponseStepDialog/ResponseStepDialog.tsx diff --git a/plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepList.test.tsx b/plugins/git-release-manager/src/components/ResponseStepDialog/ResponseStepList.test.tsx similarity index 100% rename from plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepList.test.tsx rename to plugins/git-release-manager/src/components/ResponseStepDialog/ResponseStepList.test.tsx diff --git a/plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepList.tsx b/plugins/git-release-manager/src/components/ResponseStepDialog/ResponseStepList.tsx similarity index 100% rename from plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepList.tsx rename to plugins/git-release-manager/src/components/ResponseStepDialog/ResponseStepList.tsx diff --git a/plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepListItem.test.tsx b/plugins/git-release-manager/src/components/ResponseStepDialog/ResponseStepListItem.test.tsx similarity index 100% rename from plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepListItem.test.tsx rename to plugins/git-release-manager/src/components/ResponseStepDialog/ResponseStepListItem.test.tsx diff --git a/plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepListItem.tsx b/plugins/git-release-manager/src/components/ResponseStepDialog/ResponseStepListItem.tsx similarity index 100% rename from plugins/github-release-manager/src/components/ResponseStepDialog/ResponseStepListItem.tsx rename to plugins/git-release-manager/src/components/ResponseStepDialog/ResponseStepListItem.tsx diff --git a/plugins/github-release-manager/src/components/Transition.tsx b/plugins/git-release-manager/src/components/Transition.tsx similarity index 100% rename from plugins/github-release-manager/src/components/Transition.tsx rename to plugins/git-release-manager/src/components/Transition.tsx diff --git a/plugins/github-release-manager/src/constants/constants.test.ts b/plugins/git-release-manager/src/constants/constants.test.ts similarity index 100% rename from plugins/github-release-manager/src/constants/constants.test.ts rename to plugins/git-release-manager/src/constants/constants.test.ts diff --git a/plugins/github-release-manager/src/constants/constants.ts b/plugins/git-release-manager/src/constants/constants.ts similarity index 100% rename from plugins/github-release-manager/src/constants/constants.ts rename to plugins/git-release-manager/src/constants/constants.ts diff --git a/plugins/github-release-manager/src/contexts/ProjectContext.ts b/plugins/git-release-manager/src/contexts/ProjectContext.ts similarity index 100% rename from plugins/github-release-manager/src/contexts/ProjectContext.ts rename to plugins/git-release-manager/src/contexts/ProjectContext.ts diff --git a/plugins/github-release-manager/src/contexts/RefetchContext.ts b/plugins/git-release-manager/src/contexts/RefetchContext.ts similarity index 100% rename from plugins/github-release-manager/src/contexts/RefetchContext.ts rename to plugins/git-release-manager/src/contexts/RefetchContext.ts diff --git a/plugins/github-release-manager/src/errors/GitHubReleaseManagerError.ts b/plugins/git-release-manager/src/errors/GitHubReleaseManagerError.ts similarity index 100% rename from plugins/github-release-manager/src/errors/GitHubReleaseManagerError.ts rename to plugins/git-release-manager/src/errors/GitHubReleaseManagerError.ts diff --git a/plugins/github-release-manager/src/features/CreateRc/CreateRc.test.tsx b/plugins/git-release-manager/src/features/CreateRc/CreateRc.test.tsx similarity index 100% rename from plugins/github-release-manager/src/features/CreateRc/CreateRc.test.tsx rename to plugins/git-release-manager/src/features/CreateRc/CreateRc.test.tsx diff --git a/plugins/github-release-manager/src/features/CreateRc/CreateRc.tsx b/plugins/git-release-manager/src/features/CreateRc/CreateRc.tsx similarity index 100% rename from plugins/github-release-manager/src/features/CreateRc/CreateRc.tsx rename to plugins/git-release-manager/src/features/CreateRc/CreateRc.tsx diff --git a/plugins/github-release-manager/src/features/CreateRc/hooks/useCreateRc.test.tsx b/plugins/git-release-manager/src/features/CreateRc/hooks/useCreateRc.test.tsx similarity index 100% rename from plugins/github-release-manager/src/features/CreateRc/hooks/useCreateRc.test.tsx rename to plugins/git-release-manager/src/features/CreateRc/hooks/useCreateRc.test.tsx diff --git a/plugins/github-release-manager/src/features/CreateRc/hooks/useCreateRc.ts b/plugins/git-release-manager/src/features/CreateRc/hooks/useCreateRc.ts similarity index 100% rename from plugins/github-release-manager/src/features/CreateRc/hooks/useCreateRc.ts rename to plugins/git-release-manager/src/features/CreateRc/hooks/useCreateRc.ts diff --git a/plugins/github-release-manager/src/features/Features.test.tsx b/plugins/git-release-manager/src/features/Features.test.tsx similarity index 100% rename from plugins/github-release-manager/src/features/Features.test.tsx rename to plugins/git-release-manager/src/features/Features.test.tsx diff --git a/plugins/github-release-manager/src/features/Features.tsx b/plugins/git-release-manager/src/features/Features.tsx similarity index 100% rename from plugins/github-release-manager/src/features/Features.tsx rename to plugins/git-release-manager/src/features/Features.tsx diff --git a/plugins/github-release-manager/src/features/Info/Info.test.tsx b/plugins/git-release-manager/src/features/Info/Info.test.tsx similarity index 100% rename from plugins/github-release-manager/src/features/Info/Info.test.tsx rename to plugins/git-release-manager/src/features/Info/Info.test.tsx diff --git a/plugins/github-release-manager/src/features/Info/Info.tsx b/plugins/git-release-manager/src/features/Info/Info.tsx similarity index 100% rename from plugins/github-release-manager/src/features/Info/Info.tsx rename to plugins/git-release-manager/src/features/Info/Info.tsx diff --git a/plugins/github-release-manager/src/features/Info/flow.png b/plugins/git-release-manager/src/features/Info/flow.png similarity index 100% rename from plugins/github-release-manager/src/features/Info/flow.png rename to plugins/git-release-manager/src/features/Info/flow.png diff --git a/plugins/github-release-manager/src/features/Patch/Patch.test.tsx b/plugins/git-release-manager/src/features/Patch/Patch.test.tsx similarity index 100% rename from plugins/github-release-manager/src/features/Patch/Patch.test.tsx rename to plugins/git-release-manager/src/features/Patch/Patch.test.tsx diff --git a/plugins/github-release-manager/src/features/Patch/Patch.tsx b/plugins/git-release-manager/src/features/Patch/Patch.tsx similarity index 100% rename from plugins/github-release-manager/src/features/Patch/Patch.tsx rename to plugins/git-release-manager/src/features/Patch/Patch.tsx diff --git a/plugins/github-release-manager/src/features/Patch/PatchBody.test.tsx b/plugins/git-release-manager/src/features/Patch/PatchBody.test.tsx similarity index 100% rename from plugins/github-release-manager/src/features/Patch/PatchBody.test.tsx rename to plugins/git-release-manager/src/features/Patch/PatchBody.test.tsx diff --git a/plugins/github-release-manager/src/features/Patch/PatchBody.tsx b/plugins/git-release-manager/src/features/Patch/PatchBody.tsx similarity index 100% rename from plugins/github-release-manager/src/features/Patch/PatchBody.tsx rename to plugins/git-release-manager/src/features/Patch/PatchBody.tsx diff --git a/plugins/github-release-manager/src/features/Patch/hooks/usePatch.test.ts b/plugins/git-release-manager/src/features/Patch/hooks/usePatch.test.ts similarity index 100% rename from plugins/github-release-manager/src/features/Patch/hooks/usePatch.test.ts rename to plugins/git-release-manager/src/features/Patch/hooks/usePatch.test.ts diff --git a/plugins/github-release-manager/src/features/Patch/hooks/usePatch.ts b/plugins/git-release-manager/src/features/Patch/hooks/usePatch.ts similarity index 100% rename from plugins/github-release-manager/src/features/Patch/hooks/usePatch.ts rename to plugins/git-release-manager/src/features/Patch/hooks/usePatch.ts diff --git a/plugins/github-release-manager/src/features/PromoteRc/PromoteRc.test.tsx b/plugins/git-release-manager/src/features/PromoteRc/PromoteRc.test.tsx similarity index 100% rename from plugins/github-release-manager/src/features/PromoteRc/PromoteRc.test.tsx rename to plugins/git-release-manager/src/features/PromoteRc/PromoteRc.test.tsx diff --git a/plugins/github-release-manager/src/features/PromoteRc/PromoteRc.tsx b/plugins/git-release-manager/src/features/PromoteRc/PromoteRc.tsx similarity index 100% rename from plugins/github-release-manager/src/features/PromoteRc/PromoteRc.tsx rename to plugins/git-release-manager/src/features/PromoteRc/PromoteRc.tsx diff --git a/plugins/github-release-manager/src/features/PromoteRc/PromoteRcBody.test.tsx b/plugins/git-release-manager/src/features/PromoteRc/PromoteRcBody.test.tsx similarity index 100% rename from plugins/github-release-manager/src/features/PromoteRc/PromoteRcBody.test.tsx rename to plugins/git-release-manager/src/features/PromoteRc/PromoteRcBody.test.tsx diff --git a/plugins/github-release-manager/src/features/PromoteRc/PromoteRcBody.tsx b/plugins/git-release-manager/src/features/PromoteRc/PromoteRcBody.tsx similarity index 100% rename from plugins/github-release-manager/src/features/PromoteRc/PromoteRcBody.tsx rename to plugins/git-release-manager/src/features/PromoteRc/PromoteRcBody.tsx diff --git a/plugins/github-release-manager/src/features/PromoteRc/hooks/usePromoteRc.test.ts b/plugins/git-release-manager/src/features/PromoteRc/hooks/usePromoteRc.test.ts similarity index 100% rename from plugins/github-release-manager/src/features/PromoteRc/hooks/usePromoteRc.test.ts rename to plugins/git-release-manager/src/features/PromoteRc/hooks/usePromoteRc.test.ts diff --git a/plugins/github-release-manager/src/features/PromoteRc/hooks/usePromoteRc.ts b/plugins/git-release-manager/src/features/PromoteRc/hooks/usePromoteRc.ts similarity index 100% rename from plugins/github-release-manager/src/features/PromoteRc/hooks/usePromoteRc.ts rename to plugins/git-release-manager/src/features/PromoteRc/hooks/usePromoteRc.ts diff --git a/plugins/github-release-manager/src/features/RepoDetailsForm/Owner.test.tsx b/plugins/git-release-manager/src/features/RepoDetailsForm/Owner.test.tsx similarity index 100% rename from plugins/github-release-manager/src/features/RepoDetailsForm/Owner.test.tsx rename to plugins/git-release-manager/src/features/RepoDetailsForm/Owner.test.tsx diff --git a/plugins/github-release-manager/src/features/RepoDetailsForm/Owner.tsx b/plugins/git-release-manager/src/features/RepoDetailsForm/Owner.tsx similarity index 100% rename from plugins/github-release-manager/src/features/RepoDetailsForm/Owner.tsx rename to plugins/git-release-manager/src/features/RepoDetailsForm/Owner.tsx diff --git a/plugins/github-release-manager/src/features/RepoDetailsForm/Repo.test.tsx b/plugins/git-release-manager/src/features/RepoDetailsForm/Repo.test.tsx similarity index 100% rename from plugins/github-release-manager/src/features/RepoDetailsForm/Repo.test.tsx rename to plugins/git-release-manager/src/features/RepoDetailsForm/Repo.test.tsx diff --git a/plugins/github-release-manager/src/features/RepoDetailsForm/Repo.tsx b/plugins/git-release-manager/src/features/RepoDetailsForm/Repo.tsx similarity index 100% rename from plugins/github-release-manager/src/features/RepoDetailsForm/Repo.tsx rename to plugins/git-release-manager/src/features/RepoDetailsForm/Repo.tsx diff --git a/plugins/github-release-manager/src/features/RepoDetailsForm/RepoDetailsForm.tsx b/plugins/git-release-manager/src/features/RepoDetailsForm/RepoDetailsForm.tsx similarity index 100% rename from plugins/github-release-manager/src/features/RepoDetailsForm/RepoDetailsForm.tsx rename to plugins/git-release-manager/src/features/RepoDetailsForm/RepoDetailsForm.tsx diff --git a/plugins/github-release-manager/src/features/RepoDetailsForm/VersioningStrategy.test.tsx b/plugins/git-release-manager/src/features/RepoDetailsForm/VersioningStrategy.test.tsx similarity index 100% rename from plugins/github-release-manager/src/features/RepoDetailsForm/VersioningStrategy.test.tsx rename to plugins/git-release-manager/src/features/RepoDetailsForm/VersioningStrategy.test.tsx diff --git a/plugins/github-release-manager/src/features/RepoDetailsForm/VersioningStrategy.tsx b/plugins/git-release-manager/src/features/RepoDetailsForm/VersioningStrategy.tsx similarity index 100% rename from plugins/github-release-manager/src/features/RepoDetailsForm/VersioningStrategy.tsx rename to plugins/git-release-manager/src/features/RepoDetailsForm/VersioningStrategy.tsx diff --git a/plugins/github-release-manager/src/features/RepoDetailsForm/styles.ts b/plugins/git-release-manager/src/features/RepoDetailsForm/styles.ts similarity index 100% rename from plugins/github-release-manager/src/features/RepoDetailsForm/styles.ts rename to plugins/git-release-manager/src/features/RepoDetailsForm/styles.ts diff --git a/plugins/github-release-manager/src/features/Stats/DialogBody.tsx b/plugins/git-release-manager/src/features/Stats/DialogBody.tsx similarity index 100% rename from plugins/github-release-manager/src/features/Stats/DialogBody.tsx rename to plugins/git-release-manager/src/features/Stats/DialogBody.tsx diff --git a/plugins/github-release-manager/src/features/Stats/DialogTitle.tsx b/plugins/git-release-manager/src/features/Stats/DialogTitle.tsx similarity index 100% rename from plugins/github-release-manager/src/features/Stats/DialogTitle.tsx rename to plugins/git-release-manager/src/features/Stats/DialogTitle.tsx diff --git a/plugins/github-release-manager/src/features/Stats/Info/InDepth/AverageReleaseTime.tsx b/plugins/git-release-manager/src/features/Stats/Info/InDepth/AverageReleaseTime.tsx similarity index 100% rename from plugins/github-release-manager/src/features/Stats/Info/InDepth/AverageReleaseTime.tsx rename to plugins/git-release-manager/src/features/Stats/Info/InDepth/AverageReleaseTime.tsx diff --git a/plugins/github-release-manager/src/features/Stats/Info/InDepth/InDepth.tsx b/plugins/git-release-manager/src/features/Stats/Info/InDepth/InDepth.tsx similarity index 100% rename from plugins/github-release-manager/src/features/Stats/Info/InDepth/InDepth.tsx rename to plugins/git-release-manager/src/features/Stats/Info/InDepth/InDepth.tsx diff --git a/plugins/github-release-manager/src/features/Stats/Info/InDepth/LongestReleaseTime.tsx b/plugins/git-release-manager/src/features/Stats/Info/InDepth/LongestReleaseTime.tsx similarity index 100% rename from plugins/github-release-manager/src/features/Stats/Info/InDepth/LongestReleaseTime.tsx rename to plugins/git-release-manager/src/features/Stats/Info/InDepth/LongestReleaseTime.tsx diff --git a/plugins/github-release-manager/src/features/Stats/Info/Info.tsx b/plugins/git-release-manager/src/features/Stats/Info/Info.tsx similarity index 100% rename from plugins/github-release-manager/src/features/Stats/Info/Info.tsx rename to plugins/git-release-manager/src/features/Stats/Info/Info.tsx diff --git a/plugins/github-release-manager/src/features/Stats/Info/Summary.tsx b/plugins/git-release-manager/src/features/Stats/Info/Summary.tsx similarity index 100% rename from plugins/github-release-manager/src/features/Stats/Info/Summary.tsx rename to plugins/git-release-manager/src/features/Stats/Info/Summary.tsx diff --git a/plugins/github-release-manager/src/features/Stats/Info/helpers/getReleaseCommitPairs.test.tsx b/plugins/git-release-manager/src/features/Stats/Info/helpers/getReleaseCommitPairs.test.tsx similarity index 100% rename from plugins/github-release-manager/src/features/Stats/Info/helpers/getReleaseCommitPairs.test.tsx rename to plugins/git-release-manager/src/features/Stats/Info/helpers/getReleaseCommitPairs.test.tsx diff --git a/plugins/github-release-manager/src/features/Stats/Info/helpers/getReleaseCommitPairs.tsx b/plugins/git-release-manager/src/features/Stats/Info/helpers/getReleaseCommitPairs.tsx similarity index 100% rename from plugins/github-release-manager/src/features/Stats/Info/helpers/getReleaseCommitPairs.tsx rename to plugins/git-release-manager/src/features/Stats/Info/helpers/getReleaseCommitPairs.tsx diff --git a/plugins/github-release-manager/src/features/Stats/Info/hooks/useGetReleaseTimes.tsx b/plugins/git-release-manager/src/features/Stats/Info/hooks/useGetReleaseTimes.tsx similarity index 100% rename from plugins/github-release-manager/src/features/Stats/Info/hooks/useGetReleaseTimes.tsx rename to plugins/git-release-manager/src/features/Stats/Info/hooks/useGetReleaseTimes.tsx diff --git a/plugins/github-release-manager/src/features/Stats/Row/Row.tsx b/plugins/git-release-manager/src/features/Stats/Row/Row.tsx similarity index 100% rename from plugins/github-release-manager/src/features/Stats/Row/Row.tsx rename to plugins/git-release-manager/src/features/Stats/Row/Row.tsx diff --git a/plugins/github-release-manager/src/features/Stats/Row/RowCollapsed/ReleaseTagList.tsx b/plugins/git-release-manager/src/features/Stats/Row/RowCollapsed/ReleaseTagList.tsx similarity index 100% rename from plugins/github-release-manager/src/features/Stats/Row/RowCollapsed/ReleaseTagList.tsx rename to plugins/git-release-manager/src/features/Stats/Row/RowCollapsed/ReleaseTagList.tsx diff --git a/plugins/github-release-manager/src/features/Stats/Row/RowCollapsed/ReleaseTime.tsx b/plugins/git-release-manager/src/features/Stats/Row/RowCollapsed/ReleaseTime.tsx similarity index 100% rename from plugins/github-release-manager/src/features/Stats/Row/RowCollapsed/ReleaseTime.tsx rename to plugins/git-release-manager/src/features/Stats/Row/RowCollapsed/ReleaseTime.tsx diff --git a/plugins/github-release-manager/src/features/Stats/Row/RowCollapsed/RowCollapsed.tsx b/plugins/git-release-manager/src/features/Stats/Row/RowCollapsed/RowCollapsed.tsx similarity index 100% rename from plugins/github-release-manager/src/features/Stats/Row/RowCollapsed/RowCollapsed.tsx rename to plugins/git-release-manager/src/features/Stats/Row/RowCollapsed/RowCollapsed.tsx diff --git a/plugins/github-release-manager/src/features/Stats/Stats.tsx b/plugins/git-release-manager/src/features/Stats/Stats.tsx similarity index 100% rename from plugins/github-release-manager/src/features/Stats/Stats.tsx rename to plugins/git-release-manager/src/features/Stats/Stats.tsx diff --git a/plugins/github-release-manager/src/features/Stats/Warn.tsx b/plugins/git-release-manager/src/features/Stats/Warn.tsx similarity index 100% rename from plugins/github-release-manager/src/features/Stats/Warn.tsx rename to plugins/git-release-manager/src/features/Stats/Warn.tsx diff --git a/plugins/github-release-manager/src/features/Stats/contexts/ReleaseStatsContext.tsx b/plugins/git-release-manager/src/features/Stats/contexts/ReleaseStatsContext.tsx similarity index 100% rename from plugins/github-release-manager/src/features/Stats/contexts/ReleaseStatsContext.tsx rename to plugins/git-release-manager/src/features/Stats/contexts/ReleaseStatsContext.tsx diff --git a/plugins/github-release-manager/src/features/Stats/helpers/getDecimalNumber.test.tsx b/plugins/git-release-manager/src/features/Stats/helpers/getDecimalNumber.test.tsx similarity index 100% rename from plugins/github-release-manager/src/features/Stats/helpers/getDecimalNumber.test.tsx rename to plugins/git-release-manager/src/features/Stats/helpers/getDecimalNumber.test.tsx diff --git a/plugins/github-release-manager/src/features/Stats/helpers/getDecimalNumber.tsx b/plugins/git-release-manager/src/features/Stats/helpers/getDecimalNumber.tsx similarity index 100% rename from plugins/github-release-manager/src/features/Stats/helpers/getDecimalNumber.tsx rename to plugins/git-release-manager/src/features/Stats/helpers/getDecimalNumber.tsx diff --git a/plugins/github-release-manager/src/features/Stats/helpers/getMappedReleases.test.tsx b/plugins/git-release-manager/src/features/Stats/helpers/getMappedReleases.test.tsx similarity index 100% rename from plugins/github-release-manager/src/features/Stats/helpers/getMappedReleases.test.tsx rename to plugins/git-release-manager/src/features/Stats/helpers/getMappedReleases.test.tsx diff --git a/plugins/github-release-manager/src/features/Stats/helpers/getMappedReleases.tsx b/plugins/git-release-manager/src/features/Stats/helpers/getMappedReleases.tsx similarity index 100% rename from plugins/github-release-manager/src/features/Stats/helpers/getMappedReleases.tsx rename to plugins/git-release-manager/src/features/Stats/helpers/getMappedReleases.tsx diff --git a/plugins/github-release-manager/src/features/Stats/helpers/getReleaseStats.test.tsx b/plugins/git-release-manager/src/features/Stats/helpers/getReleaseStats.test.tsx similarity index 100% rename from plugins/github-release-manager/src/features/Stats/helpers/getReleaseStats.test.tsx rename to plugins/git-release-manager/src/features/Stats/helpers/getReleaseStats.test.tsx diff --git a/plugins/github-release-manager/src/features/Stats/helpers/getReleaseStats.tsx b/plugins/git-release-manager/src/features/Stats/helpers/getReleaseStats.tsx similarity index 100% rename from plugins/github-release-manager/src/features/Stats/helpers/getReleaseStats.tsx rename to plugins/git-release-manager/src/features/Stats/helpers/getReleaseStats.tsx diff --git a/plugins/github-release-manager/src/features/Stats/helpers/getSummary.test.tsx b/plugins/git-release-manager/src/features/Stats/helpers/getSummary.test.tsx similarity index 100% rename from plugins/github-release-manager/src/features/Stats/helpers/getSummary.test.tsx rename to plugins/git-release-manager/src/features/Stats/helpers/getSummary.test.tsx diff --git a/plugins/github-release-manager/src/features/Stats/helpers/getSummary.tsx b/plugins/git-release-manager/src/features/Stats/helpers/getSummary.tsx similarity index 100% rename from plugins/github-release-manager/src/features/Stats/helpers/getSummary.tsx rename to plugins/git-release-manager/src/features/Stats/helpers/getSummary.tsx diff --git a/plugins/github-release-manager/src/features/Stats/hooks/useGetCommit.ts b/plugins/git-release-manager/src/features/Stats/hooks/useGetCommit.ts similarity index 100% rename from plugins/github-release-manager/src/features/Stats/hooks/useGetCommit.ts rename to plugins/git-release-manager/src/features/Stats/hooks/useGetCommit.ts diff --git a/plugins/github-release-manager/src/features/Stats/hooks/useGetStats.ts b/plugins/git-release-manager/src/features/Stats/hooks/useGetStats.ts similarity index 100% rename from plugins/github-release-manager/src/features/Stats/hooks/useGetStats.ts rename to plugins/git-release-manager/src/features/Stats/hooks/useGetStats.ts diff --git a/plugins/github-release-manager/src/helpers/createResponseStepError.test.ts b/plugins/git-release-manager/src/helpers/createResponseStepError.test.ts similarity index 100% rename from plugins/github-release-manager/src/helpers/createResponseStepError.test.ts rename to plugins/git-release-manager/src/helpers/createResponseStepError.test.ts diff --git a/plugins/github-release-manager/src/helpers/createResponseStepError.ts b/plugins/git-release-manager/src/helpers/createResponseStepError.ts similarity index 100% rename from plugins/github-release-manager/src/helpers/createResponseStepError.ts rename to plugins/git-release-manager/src/helpers/createResponseStepError.ts diff --git a/plugins/github-release-manager/src/helpers/getBumpedTag.test.ts b/plugins/git-release-manager/src/helpers/getBumpedTag.test.ts similarity index 100% rename from plugins/github-release-manager/src/helpers/getBumpedTag.test.ts rename to plugins/git-release-manager/src/helpers/getBumpedTag.test.ts diff --git a/plugins/github-release-manager/src/helpers/getBumpedTag.ts b/plugins/git-release-manager/src/helpers/getBumpedTag.ts similarity index 100% rename from plugins/github-release-manager/src/helpers/getBumpedTag.ts rename to plugins/git-release-manager/src/helpers/getBumpedTag.ts diff --git a/plugins/github-release-manager/src/helpers/getRcGitHubInfo.test.ts b/plugins/git-release-manager/src/helpers/getRcGitHubInfo.test.ts similarity index 100% rename from plugins/github-release-manager/src/helpers/getRcGitHubInfo.test.ts rename to plugins/git-release-manager/src/helpers/getRcGitHubInfo.test.ts diff --git a/plugins/github-release-manager/src/helpers/getRcGitHubInfo.ts b/plugins/git-release-manager/src/helpers/getRcGitHubInfo.ts similarity index 100% rename from plugins/github-release-manager/src/helpers/getRcGitHubInfo.ts rename to plugins/git-release-manager/src/helpers/getRcGitHubInfo.ts diff --git a/plugins/github-release-manager/src/helpers/getShortCommitHash.test.ts b/plugins/git-release-manager/src/helpers/getShortCommitHash.test.ts similarity index 100% rename from plugins/github-release-manager/src/helpers/getShortCommitHash.test.ts rename to plugins/git-release-manager/src/helpers/getShortCommitHash.test.ts diff --git a/plugins/github-release-manager/src/helpers/getShortCommitHash.ts b/plugins/git-release-manager/src/helpers/getShortCommitHash.ts similarity index 100% rename from plugins/github-release-manager/src/helpers/getShortCommitHash.ts rename to plugins/git-release-manager/src/helpers/getShortCommitHash.ts diff --git a/plugins/github-release-manager/src/helpers/isCalverTagParts.test.ts b/plugins/git-release-manager/src/helpers/isCalverTagParts.test.ts similarity index 100% rename from plugins/github-release-manager/src/helpers/isCalverTagParts.test.ts rename to plugins/git-release-manager/src/helpers/isCalverTagParts.test.ts diff --git a/plugins/github-release-manager/src/helpers/isCalverTagParts.ts b/plugins/git-release-manager/src/helpers/isCalverTagParts.ts similarity index 100% rename from plugins/github-release-manager/src/helpers/isCalverTagParts.ts rename to plugins/git-release-manager/src/helpers/isCalverTagParts.ts diff --git a/plugins/github-release-manager/src/helpers/isProjectValid.test.ts b/plugins/git-release-manager/src/helpers/isProjectValid.test.ts similarity index 100% rename from plugins/github-release-manager/src/helpers/isProjectValid.test.ts rename to plugins/git-release-manager/src/helpers/isProjectValid.test.ts diff --git a/plugins/github-release-manager/src/helpers/isProjectValid.ts b/plugins/git-release-manager/src/helpers/isProjectValid.ts similarity index 100% rename from plugins/github-release-manager/src/helpers/isProjectValid.ts rename to plugins/git-release-manager/src/helpers/isProjectValid.ts diff --git a/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.test.ts b/plugins/git-release-manager/src/helpers/tagParts/getCalverTagParts.test.ts similarity index 100% rename from plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.test.ts rename to plugins/git-release-manager/src/helpers/tagParts/getCalverTagParts.test.ts diff --git a/plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.ts b/plugins/git-release-manager/src/helpers/tagParts/getCalverTagParts.ts similarity index 100% rename from plugins/github-release-manager/src/helpers/tagParts/getCalverTagParts.ts rename to plugins/git-release-manager/src/helpers/tagParts/getCalverTagParts.ts diff --git a/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.test.ts b/plugins/git-release-manager/src/helpers/tagParts/getSemverTagParts.test.ts similarity index 100% rename from plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.test.ts rename to plugins/git-release-manager/src/helpers/tagParts/getSemverTagParts.test.ts diff --git a/plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.ts b/plugins/git-release-manager/src/helpers/tagParts/getSemverTagParts.ts similarity index 100% rename from plugins/github-release-manager/src/helpers/tagParts/getSemverTagParts.ts rename to plugins/git-release-manager/src/helpers/tagParts/getSemverTagParts.ts diff --git a/plugins/github-release-manager/src/helpers/tagParts/getTagParts.test.ts b/plugins/git-release-manager/src/helpers/tagParts/getTagParts.test.ts similarity index 100% rename from plugins/github-release-manager/src/helpers/tagParts/getTagParts.test.ts rename to plugins/git-release-manager/src/helpers/tagParts/getTagParts.test.ts diff --git a/plugins/github-release-manager/src/helpers/tagParts/getTagParts.ts b/plugins/git-release-manager/src/helpers/tagParts/getTagParts.ts similarity index 100% rename from plugins/github-release-manager/src/helpers/tagParts/getTagParts.ts rename to plugins/git-release-manager/src/helpers/tagParts/getTagParts.ts diff --git a/plugins/github-release-manager/src/helpers/tagParts/validateTagName.ts b/plugins/git-release-manager/src/helpers/tagParts/validateTagName.ts similarity index 100% rename from plugins/github-release-manager/src/helpers/tagParts/validateTagName.ts rename to plugins/git-release-manager/src/helpers/tagParts/validateTagName.ts diff --git a/plugins/github-release-manager/src/helpers/tagParts/validateTagParts.test.ts b/plugins/git-release-manager/src/helpers/tagParts/validateTagParts.test.ts similarity index 100% rename from plugins/github-release-manager/src/helpers/tagParts/validateTagParts.test.ts rename to plugins/git-release-manager/src/helpers/tagParts/validateTagParts.test.ts diff --git a/plugins/github-release-manager/src/hooks/useGetGitHubBatchInfo.test.ts b/plugins/git-release-manager/src/hooks/useGetGitHubBatchInfo.test.ts similarity index 100% rename from plugins/github-release-manager/src/hooks/useGetGitHubBatchInfo.test.ts rename to plugins/git-release-manager/src/hooks/useGetGitHubBatchInfo.test.ts diff --git a/plugins/github-release-manager/src/hooks/useGetGitHubBatchInfo.ts b/plugins/git-release-manager/src/hooks/useGetGitHubBatchInfo.ts similarity index 100% rename from plugins/github-release-manager/src/hooks/useGetGitHubBatchInfo.ts rename to plugins/git-release-manager/src/hooks/useGetGitHubBatchInfo.ts diff --git a/plugins/github-release-manager/src/hooks/useQueryHandler.test.tsx b/plugins/git-release-manager/src/hooks/useQueryHandler.test.tsx similarity index 100% rename from plugins/github-release-manager/src/hooks/useQueryHandler.test.tsx rename to plugins/git-release-manager/src/hooks/useQueryHandler.test.tsx diff --git a/plugins/github-release-manager/src/hooks/useQueryHandler.ts b/plugins/git-release-manager/src/hooks/useQueryHandler.ts similarity index 100% rename from plugins/github-release-manager/src/hooks/useQueryHandler.ts rename to plugins/git-release-manager/src/hooks/useQueryHandler.ts diff --git a/plugins/github-release-manager/src/hooks/useResponseSteps.test.ts b/plugins/git-release-manager/src/hooks/useResponseSteps.test.ts similarity index 100% rename from plugins/github-release-manager/src/hooks/useResponseSteps.test.ts rename to plugins/git-release-manager/src/hooks/useResponseSteps.test.ts diff --git a/plugins/github-release-manager/src/hooks/useResponseSteps.ts b/plugins/git-release-manager/src/hooks/useResponseSteps.ts similarity index 100% rename from plugins/github-release-manager/src/hooks/useResponseSteps.ts rename to plugins/git-release-manager/src/hooks/useResponseSteps.ts diff --git a/plugins/github-release-manager/src/hooks/useVersioningStrategyMatchesRepoTags.test.tsx b/plugins/git-release-manager/src/hooks/useVersioningStrategyMatchesRepoTags.test.tsx similarity index 100% rename from plugins/github-release-manager/src/hooks/useVersioningStrategyMatchesRepoTags.test.tsx rename to plugins/git-release-manager/src/hooks/useVersioningStrategyMatchesRepoTags.test.tsx diff --git a/plugins/github-release-manager/src/hooks/useVersioningStrategyMatchesRepoTags.ts b/plugins/git-release-manager/src/hooks/useVersioningStrategyMatchesRepoTags.ts similarity index 100% rename from plugins/github-release-manager/src/hooks/useVersioningStrategyMatchesRepoTags.ts rename to plugins/git-release-manager/src/hooks/useVersioningStrategyMatchesRepoTags.ts diff --git a/plugins/github-release-manager/src/index.ts b/plugins/git-release-manager/src/index.ts similarity index 100% rename from plugins/github-release-manager/src/index.ts rename to plugins/git-release-manager/src/index.ts diff --git a/plugins/github-release-manager/src/plugin.test.ts b/plugins/git-release-manager/src/plugin.test.ts similarity index 95% rename from plugins/github-release-manager/src/plugin.test.ts rename to plugins/git-release-manager/src/plugin.test.ts index 60ab101d11..5f30848771 100644 --- a/plugins/github-release-manager/src/plugin.test.ts +++ b/plugins/git-release-manager/src/plugin.test.ts @@ -16,7 +16,7 @@ import * as plugin from './plugin'; -describe('github-release-manager', () => { +describe('git-release-manager', () => { it('should export plugin & friends', () => { expect(Object.keys(plugin)).toMatchInlineSnapshot(` Array [ diff --git a/plugins/github-release-manager/src/plugin.ts b/plugins/git-release-manager/src/plugin.ts similarity index 98% rename from plugins/github-release-manager/src/plugin.ts rename to plugins/git-release-manager/src/plugin.ts index 9c01c1e959..507d1044aa 100644 --- a/plugins/github-release-manager/src/plugin.ts +++ b/plugins/git-release-manager/src/plugin.ts @@ -29,7 +29,7 @@ import { rootRouteRef } from './routes'; export { githubReleaseManagerApiRef }; export const gitHubReleaseManagerPlugin = createPlugin({ - id: 'github-release-manager', + id: 'git-release-manager', routes: { root: rootRouteRef, }, diff --git a/plugins/github-release-manager/src/routes.ts b/plugins/git-release-manager/src/routes.ts similarity index 95% rename from plugins/github-release-manager/src/routes.ts rename to plugins/git-release-manager/src/routes.ts index 9406a4c6a7..3b3ea80cc2 100644 --- a/plugins/github-release-manager/src/routes.ts +++ b/plugins/git-release-manager/src/routes.ts @@ -17,5 +17,5 @@ import { createRouteRef } from '@backstage/core'; export const rootRouteRef = createRouteRef({ - title: 'github-release-manager', + title: 'git-release-manager', }); diff --git a/plugins/github-release-manager/src/setupTests.ts b/plugins/git-release-manager/src/setupTests.ts similarity index 100% rename from plugins/github-release-manager/src/setupTests.ts rename to plugins/git-release-manager/src/setupTests.ts diff --git a/plugins/github-release-manager/src/styles/styles.ts b/plugins/git-release-manager/src/styles/styles.ts similarity index 100% rename from plugins/github-release-manager/src/styles/styles.ts rename to plugins/git-release-manager/src/styles/styles.ts diff --git a/plugins/github-release-manager/src/test-helpers/stats.ts b/plugins/git-release-manager/src/test-helpers/stats.ts similarity index 100% rename from plugins/github-release-manager/src/test-helpers/stats.ts rename to plugins/git-release-manager/src/test-helpers/stats.ts diff --git a/plugins/github-release-manager/src/test-helpers/test-helpers.ts b/plugins/git-release-manager/src/test-helpers/test-helpers.ts similarity index 100% rename from plugins/github-release-manager/src/test-helpers/test-helpers.ts rename to plugins/git-release-manager/src/test-helpers/test-helpers.ts diff --git a/plugins/github-release-manager/src/test-helpers/test-ids.ts b/plugins/git-release-manager/src/test-helpers/test-ids.ts similarity index 100% rename from plugins/github-release-manager/src/test-helpers/test-ids.ts rename to plugins/git-release-manager/src/test-helpers/test-ids.ts diff --git a/plugins/github-release-manager/src/types/types.ts b/plugins/git-release-manager/src/types/types.ts similarity index 100% rename from plugins/github-release-manager/src/types/types.ts rename to plugins/git-release-manager/src/types/types.ts From 66918b71fb3a92f85cd39b51d8856602164058a5 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Thu, 29 Apr 2021 11:41:14 +0200 Subject: [PATCH 083/140] Update snapshot Signed-off-by: Erik Engervall --- plugins/git-release-manager/src/api/serviceApiRef.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/git-release-manager/src/api/serviceApiRef.test.ts b/plugins/git-release-manager/src/api/serviceApiRef.test.ts index 4e8c204c6b..28a876be49 100644 --- a/plugins/git-release-manager/src/api/serviceApiRef.test.ts +++ b/plugins/git-release-manager/src/api/serviceApiRef.test.ts @@ -23,7 +23,7 @@ describe('githubReleaseManagerApiRef', () => { expect(result).toMatchInlineSnapshot(` ApiRefImpl { "config": Object { - "description": "Used by the GitHub Release Manager plugin to make requests", + "description": "Used by the Git Release Manager plugin to make requests", "id": "plugin.git-release-manager.service", }, } From 19e6ebc1f20c5580fcaee760712114c56deb8fa6 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Thu, 29 Apr 2021 12:02:05 +0200 Subject: [PATCH 084/140] Continue renaming code from GitHub to Git Signed-off-by: Erik Engervall --- plugins/git-release-manager/README.md | 18 +++--- plugins/git-release-manager/dev/index.tsx | 13 ++--- ...leaseManager.tsx => GitReleaseManager.tsx} | 10 ++-- .../src/api/PluginApiClient.test.ts | 9 ++- .../src/api/PluginApiClient.ts | 10 ++-- .../src/api/serviceApiRef.test.ts | 6 +- .../src/api/serviceApiRef.ts | 4 +- .../src/components/Differ.tsx | 4 +- .../src/contexts/ProjectContext.ts | 4 +- .../src/contexts/RefetchContext.ts | 4 +- ...agerError.ts => GitReleaseManagerError.ts} | 4 +- .../CreateReleaseCandidate.test.tsx} | 22 +++---- .../CreateReleaseCandidate.tsx} | 45 +++++++------- .../hooks/useCreateReleaseCandidate.test.tsx} | 14 ++--- .../hooks/useCreateReleaseCandidate.ts} | 44 +++++++------- .../src/features/Features.test.tsx | 13 ++--- .../src/features/Features.tsx | 58 +++++++++---------- .../src/features/Info/Info.tsx | 17 +++--- .../src/features/Patch/PatchBody.tsx | 26 ++++----- .../src/features/Patch/hooks/usePatch.ts | 4 +- .../src/features/PromoteRc/PromoteRc.tsx | 4 +- .../features/PromoteRc/hooks/usePromoteRc.ts | 4 +- .../src/features/RepoDetailsForm/Owner.tsx | 4 +- .../src/features/RepoDetailsForm/Repo.tsx | 4 +- .../Stats/Info/hooks/useGetReleaseTimes.tsx | 4 +- .../Stats/contexts/ReleaseStatsContext.tsx | 4 +- .../src/features/Stats/hooks/useGetCommit.ts | 8 +-- .../src/features/Stats/hooks/useGetStats.ts | 4 +- ....ts => getReleaseCandidateGitInfo.test.ts} | 10 ++-- ...bInfo.ts => getReleaseCandidateGitInfo.ts} | 2 +- .../src/helpers/getShortCommitHash.ts | 4 +- ...nfo.test.ts => useGetGitBatchInfo.test.ts} | 16 ++--- ...tHubBatchInfo.ts => useGetGitBatchInfo.ts} | 14 ++--- plugins/git-release-manager/src/index.ts | 6 +- .../git-release-manager/src/plugin.test.ts | 6 +- plugins/git-release-manager/src/plugin.ts | 16 ++--- .../src/test-helpers/test-helpers.ts | 14 +++-- .../src/test-helpers/test-ids.ts | 2 +- 38 files changed, 230 insertions(+), 225 deletions(-) rename plugins/git-release-manager/src/{GitHubReleaseManager.tsx => GitReleaseManager.tsx} (90%) rename plugins/git-release-manager/src/errors/{GitHubReleaseManagerError.ts => GitReleaseManagerError.ts} (86%) rename plugins/git-release-manager/src/features/{CreateRc/CreateRc.test.tsx => CreateReleaseCandidate/CreateReleaseCandidate.test.tsx} (78%) rename plugins/git-release-manager/src/features/{CreateRc/CreateRc.tsx => CreateReleaseCandidate/CreateReleaseCandidate.tsx} (79%) rename plugins/git-release-manager/src/features/{CreateRc/hooks/useCreateRc.test.tsx => CreateReleaseCandidate/hooks/useCreateReleaseCandidate.test.tsx} (90%) rename plugins/git-release-manager/src/features/{CreateRc/hooks/useCreateRc.ts => CreateReleaseCandidate/hooks/useCreateReleaseCandidate.ts} (80%) rename plugins/git-release-manager/src/helpers/{getRcGitHubInfo.test.ts => getReleaseCandidateGitInfo.test.ts} (91%) rename plugins/git-release-manager/src/helpers/{getRcGitHubInfo.ts => getReleaseCandidateGitInfo.ts} (97%) rename plugins/git-release-manager/src/hooks/{useGetGitHubBatchInfo.test.ts => useGetGitBatchInfo.test.ts} (85%) rename plugins/git-release-manager/src/hooks/{useGetGitHubBatchInfo.ts => useGetGitBatchInfo.ts} (83%) diff --git a/plugins/git-release-manager/README.md b/plugins/git-release-manager/README.md index 86efc0c069..d9e1703781 100644 --- a/plugins/git-release-manager/README.md +++ b/plugins/git-release-manager/README.md @@ -1,4 +1,4 @@ -# GitHub Release Manager (GRM) +# Git Release Manager (GRM) ## Overview @@ -6,17 +6,17 @@ Does it build and ship your code? **No**. -What `GRM` does is manage your **[releases](https://docs.github.com/en/github/administering-a-repository/managing-releases-in-a-repository)** on GitHub, building and shipping is entirely up to you as a developer to handle in your CI. +What `GRM` does is manage your Git **[releases](https://docs.github.com/en/github/administering-a-repository/managing-releases-in-a-repository)**, building and shipping is entirely up to you as a developer to handle in your CI. `GRM` is built with industry standards in mind and the flow is as follows: ![](./src/features/Info/flow.png) -> **GitHub**: The source control system where releases reside in a practical sense. Read more about [GitHub releases](https://docs.github.com/en/github/administering-a-repository/managing-releases-in-a-repository). (Note that this plugin works just as well with GitHub Enterprise.) +> **Git**: The source control system where releases reside in a practical sense. Read more about [Git releases](https://docs.github.com/en/github/administering-a-repository/managing-releases-in-a-repository). (Note that this plugin works just as well with any system implementing `Git`.) > -> **Release Candidate (RC)**: A GitHub pre-release intended primarily for internal testing +> **Release Candidate (RC)**: A Git pre-release intended primarily for internal testing > -> **Release Version**: A GitHub release intended for end users +> **Release Version**: A Git release intended for end users Looking at the flow above, a common release lifecycle could be: @@ -24,7 +24,7 @@ Looking at the flow above, a common release lifecycle could be: - `GRM` 1. Creates a release branch `rc/` 1. Creates Release Candidate tag `rc-` - 1. Creates a GitHub prerelease with Release Candidate tag + 1. Creates a Git prerelease with Release Candidate tag - Your CI 1. Detects the new tag by matching the git reference `refs/tags/rc-.*` 1. Builds and deploys to staging environment for testing @@ -32,14 +32,14 @@ Looking at the flow above, a common release lifecycle could be: - `GRM` 1. The selected commit is cherry-picked to the release branch 1. The release tag is bumped - 1. Updates GitHub release's tag and description with the patch's details + 1. Updates Git release's tag and description with the patch's details - Your CI 1. Detects the new tag by matching the git reference `refs/tags/(rc|version)-.*` (Release Versions are patchable as well) 1. Builds and deploys to staging (or production if Release Version) for testing - User presses **Promote Release Candidate to Release Version** - `GRM` 1. Creates Release Version tag `version-` - 1. Promotes the GitHub release by removing the prerelease flag + 1. Promotes the Git release by removing the prerelease flag - Your CI 1. Detects the new tag by matching the git reference `refs/tags/version-.*` 1. Builds and deploys to production for testing @@ -48,7 +48,7 @@ Looking at the flow above, a common release lifecycle could be: ### Importing -The plugin exports a single full-page extension `GitHubReleaseManagerPage`, which one can add to an app like a usual top-level tool on a dedicated route. +The plugin exports a single full-page extension `GitReleaseManagerPage`, which one can add to an app like a usual top-level tool on a dedicated route. ### Configuration diff --git a/plugins/git-release-manager/dev/index.tsx b/plugins/git-release-manager/dev/index.tsx index 62eb2a8246..d1d2d6e7d9 100644 --- a/plugins/git-release-manager/dev/index.tsx +++ b/plugins/git-release-manager/dev/index.tsx @@ -18,14 +18,11 @@ import React from 'react'; import { createDevApp } from '@backstage/dev-utils'; import { Box, Typography } from '@material-ui/core'; -import { - gitHubReleaseManagerPlugin, - GitHubReleaseManagerPage, -} from '../src/plugin'; +import { gitReleaseManagerPlugin, GitReleaseManagerPage } from '../src/plugin'; import { InfoCardPlus } from '../src/components/InfoCardPlus'; createDevApp() - .registerPlugin(gitHubReleaseManagerPlugin) + .registerPlugin(gitReleaseManagerPlugin) .addPage({ title: 'Dynamic', path: '/dynamic', @@ -36,7 +33,7 @@ createDevApp() Configure plugin via select inputs - + ), }) @@ -53,7 +50,7 @@ createDevApp() - Success callbacks can also be added - ; features?: { info?: Pick, 'omit'>; @@ -46,8 +46,8 @@ export interface GitHubReleaseManagerProps { }; } -export function GitHubReleaseManager(props: GitHubReleaseManagerProps) { - const pluginApiClient = useApi(githubReleaseManagerApiRef); +export function GitReleaseManager(props: GitReleaseManagerProps) { + const pluginApiClient = useApi(gitReleaseManagerApiRef); const classes = useStyles(); const { getParsedQuery } = useQueryHandler(); @@ -83,7 +83,7 @@ export function GitHubReleaseManager(props: GitHubReleaseManagerProps) { return (
- + diff --git a/plugins/git-release-manager/src/api/PluginApiClient.test.ts b/plugins/git-release-manager/src/api/PluginApiClient.test.ts index d4f86ef9e7..efb0a32ee7 100644 --- a/plugins/git-release-manager/src/api/PluginApiClient.test.ts +++ b/plugins/git-release-manager/src/api/PluginApiClient.test.ts @@ -16,7 +16,7 @@ import { ConfigReader, OAuthApi } from '@backstage/core'; -import { PluginApiClient } from './PluginApiClient'; +import { GitReleaseApiClient } from './PluginApiClient'; describe('PluginApiClient', () => { it('should return the default plugin api client', () => { @@ -24,10 +24,13 @@ describe('PluginApiClient', () => { const githubAuthApi: OAuthApi = { getAccessToken: jest.fn(), }; - const pluginApiClient = new PluginApiClient({ configApi, githubAuthApi }); + const pluginApiClient = new GitReleaseApiClient({ + configApi, + githubAuthApi, + }); expect(pluginApiClient).toMatchInlineSnapshot(` - PluginApiClient { + GitReleaseApiClient { "baseUrl": "https://api.github.com", "createRc": Object { "createRef": [Function], diff --git a/plugins/git-release-manager/src/api/PluginApiClient.ts b/plugins/git-release-manager/src/api/PluginApiClient.ts index 52764c6613..2a0eadcadd 100644 --- a/plugins/git-release-manager/src/api/PluginApiClient.ts +++ b/plugins/git-release-manager/src/api/PluginApiClient.ts @@ -23,7 +23,7 @@ import { DISABLE_CACHE } from '../constants/constants'; import { Project } from '../contexts/ProjectContext'; import { SemverTagParts } from '../helpers/tagParts/getSemverTagParts'; -export class PluginApiClient implements IPluginApiClient { +export class GitReleaseApiClient implements GitReleaseApi { private readonly githubAuthApi: OAuthApi; private readonly baseUrl: string; readonly host: string; @@ -774,7 +774,7 @@ type CreateTempCommit = ( tagParts: SemverTagParts | CalverTagParts; releaseBranchTree: string; selectedPatchCommit: UnboxArray< - UnboxReturnedPromise + UnboxReturnedPromise >; } & OwnerRepo, ) => Promise<{ @@ -812,7 +812,7 @@ type CreateCherryPickCommit = ( args: { bumpedTag: string; selectedPatchCommit: UnboxArray< - UnboxReturnedPromise + UnboxReturnedPromise >; mergeTree: string; releaseBranchSha: string; @@ -827,7 +827,7 @@ type ReplaceTempCommit = ( args: { releaseBranchName: string; cherryPickCommit: UnboxReturnedPromise< - IPluginApiClient['patch']['createCherryPickCommit'] + GitReleaseApi['patch']['createCherryPickCommit'] >; } & OwnerRepo, ) => Promise<{ @@ -924,7 +924,7 @@ type GetCommit = ( }>; export type GetCommitResult = UnboxReturnedPromise; -export interface IPluginApiClient { +export interface GitReleaseApi { getHost: GetHost; getRepoPath: GetRepoPath; getOwners: GetOwners; diff --git a/plugins/git-release-manager/src/api/serviceApiRef.test.ts b/plugins/git-release-manager/src/api/serviceApiRef.test.ts index 28a876be49..8313294683 100644 --- a/plugins/git-release-manager/src/api/serviceApiRef.test.ts +++ b/plugins/git-release-manager/src/api/serviceApiRef.test.ts @@ -14,11 +14,11 @@ * limitations under the License. */ -import { githubReleaseManagerApiRef } from './serviceApiRef'; +import { gitReleaseManagerApiRef } from './serviceApiRef'; -describe('githubReleaseManagerApiRef', () => { +describe('gitReleaseManagerApiRef', () => { it('should work', () => { - const result = githubReleaseManagerApiRef; + const result = gitReleaseManagerApiRef; expect(result).toMatchInlineSnapshot(` ApiRefImpl { diff --git a/plugins/git-release-manager/src/api/serviceApiRef.ts b/plugins/git-release-manager/src/api/serviceApiRef.ts index f44168c476..b2652aac65 100644 --- a/plugins/git-release-manager/src/api/serviceApiRef.ts +++ b/plugins/git-release-manager/src/api/serviceApiRef.ts @@ -16,9 +16,9 @@ import { createApiRef } from '@backstage/core'; -import { IPluginApiClient } from './PluginApiClient'; +import { GitReleaseApi } from './PluginApiClient'; -export const githubReleaseManagerApiRef = createApiRef({ +export const gitReleaseManagerApiRef = createApiRef({ id: 'plugin.git-release-manager.service', description: 'Used by the Git Release Manager plugin to make requests', }); diff --git a/plugins/git-release-manager/src/components/Differ.tsx b/plugins/git-release-manager/src/components/Differ.tsx index d4eda688a6..e5d64cc914 100644 --- a/plugins/git-release-manager/src/components/Differ.tsx +++ b/plugins/git-release-manager/src/components/Differ.tsx @@ -22,7 +22,7 @@ import DynamicFeedIcon from '@material-ui/icons/DynamicFeed'; import GitHubIcon from '@material-ui/icons/GitHub'; import LocalOfferIcon from '@material-ui/icons/LocalOffer'; -import { GitHubReleaseManagerError } from '../errors/GitHubReleaseManagerError'; +import { GitReleaseManagerError } from '../errors/GitReleaseManagerError'; import { TEST_IDS } from '../test-helpers/test-ids'; interface DifferProps { @@ -115,6 +115,6 @@ function Icon({ icon }: IconProps) { ); default: - throw new GitHubReleaseManagerError('Invalid Differ icon'); + throw new GitReleaseManagerError('Invalid Differ icon'); } } diff --git a/plugins/git-release-manager/src/contexts/ProjectContext.ts b/plugins/git-release-manager/src/contexts/ProjectContext.ts index bd8e78c53b..cc13c5ac77 100644 --- a/plugins/git-release-manager/src/contexts/ProjectContext.ts +++ b/plugins/git-release-manager/src/contexts/ProjectContext.ts @@ -17,7 +17,7 @@ import { createContext, useContext } from 'react'; import { VERSIONING_STRATEGIES } from '../constants/constants'; -import { GitHubReleaseManagerError } from '../errors/GitHubReleaseManagerError'; +import { GitReleaseManagerError } from '../errors/GitReleaseManagerError'; export interface Project { /** @@ -57,7 +57,7 @@ export const useProjectContext = () => { const { project } = useContext(ProjectContext) ?? {}; if (!project) { - throw new GitHubReleaseManagerError('project not found'); + throw new GitReleaseManagerError('project not found'); } return { diff --git a/plugins/git-release-manager/src/contexts/RefetchContext.ts b/plugins/git-release-manager/src/contexts/RefetchContext.ts index 344df3d9a5..24867ca7c9 100644 --- a/plugins/git-release-manager/src/contexts/RefetchContext.ts +++ b/plugins/git-release-manager/src/contexts/RefetchContext.ts @@ -16,7 +16,7 @@ import { createContext, useContext } from 'react'; -import { GitHubReleaseManagerError } from '../errors/GitHubReleaseManagerError'; +import { GitReleaseManagerError } from '../errors/GitReleaseManagerError'; export const RefetchContext = createContext< | { @@ -30,7 +30,7 @@ export const useRefetchContext = () => { const refetch = useContext(RefetchContext); if (!refetch) { - throw new GitHubReleaseManagerError('refetch not found'); + throw new GitReleaseManagerError('refetch not found'); } return { diff --git a/plugins/git-release-manager/src/errors/GitHubReleaseManagerError.ts b/plugins/git-release-manager/src/errors/GitReleaseManagerError.ts similarity index 86% rename from plugins/git-release-manager/src/errors/GitHubReleaseManagerError.ts rename to plugins/git-release-manager/src/errors/GitReleaseManagerError.ts index 7806d4baad..3c2b516202 100644 --- a/plugins/git-release-manager/src/errors/GitHubReleaseManagerError.ts +++ b/plugins/git-release-manager/src/errors/GitReleaseManagerError.ts @@ -14,10 +14,10 @@ * limitations under the License. */ -export class GitHubReleaseManagerError extends Error { +export class GitReleaseManagerError extends Error { constructor(message: string) { super(message); - this.name = 'GitHubReleaseManagerError'; + this.name = 'GitReleaseManagerError'; } } diff --git a/plugins/git-release-manager/src/features/CreateRc/CreateRc.test.tsx b/plugins/git-release-manager/src/features/CreateReleaseCandidate/CreateReleaseCandidate.test.tsx similarity index 78% rename from plugins/git-release-manager/src/features/CreateRc/CreateRc.test.tsx rename to plugins/git-release-manager/src/features/CreateReleaseCandidate/CreateReleaseCandidate.test.tsx index 5f0ccaa20c..b7509cd27b 100644 --- a/plugins/git-release-manager/src/features/CreateRc/CreateRc.test.tsx +++ b/plugins/git-release-manager/src/features/CreateReleaseCandidate/CreateReleaseCandidate.test.tsx @@ -19,15 +19,15 @@ import { render } from '@testing-library/react'; import { mockCalverProject, - mockNextGitHubInfoSemver, + mockNextGitInfoSemver, mockReleaseBranch, mockReleaseCandidateCalver, mockReleaseVersionCalver, mockSemverProject, } from '../../test-helpers/test-helpers'; -import { CreateRc } from './CreateRc'; +import { CreateReleaseCandidate } from './CreateReleaseCandidate'; import { TEST_IDS } from '../../test-helpers/test-ids'; -import { useCreateRc } from './hooks/useCreateRc'; +import { useCreateReleaseCandidate } from './hooks/useCreateReleaseCandidate'; import { useProjectContext } from '../../contexts/ProjectContext'; jest.mock('../../contexts/ProjectContext', () => ({ @@ -35,23 +35,23 @@ jest.mock('../../contexts/ProjectContext', () => ({ project: mockCalverProject, })), })); -jest.mock('../../helpers/getRcGitHubInfo', () => ({ - getRcGitHubInfo: () => mockNextGitHubInfoSemver, +jest.mock('../../helpers/getReleaseCandidateGitInfo', () => ({ + getReleaseCandidateGitInfo: () => mockNextGitInfoSemver, })); -jest.mock('./hooks/useCreateRc', () => ({ - useCreateRc: () => +jest.mock('./hooks/useCreateReleaseCandidate', () => ({ + useCreateReleaseCandidate: () => ({ run: jest.fn(), responseSteps: [], progress: 0, runInvoked: false, - } as ReturnType), + } as ReturnType), })); -describe('CreateRc', () => { +describe('CreateReleaseCandidate', () => { it('should display CTA', () => { const { getByTestId } = render( - { }); const { getByTestId } = render( - { ); }; -export const CreateRc = ({ +export const CreateReleaseCandidate = ({ defaultBranch, latestRelease, releaseBranch, successCb, -}: CreateRcProps) => { +}: CreateReleaseCandidateProps) => { const { project } = useProjectContext(); const classes = useStyles(); const [semverBumpLevel, setSemverBumpLevel] = useState<'major' | 'minor'>( SEMVER_PARTS.minor, ); - const [nextGitHubInfo, setNextGitHubInfo] = useState( - getRcGitHubInfo({ latestRelease, project, semverBumpLevel }), + const [releaseCandidateGitInfo, setReleaseCandidateGitInfo] = useState( + getReleaseCandidateGitInfo({ latestRelease, project, semverBumpLevel }), ); useEffect(() => { - setNextGitHubInfo( - getRcGitHubInfo({ latestRelease, project, semverBumpLevel }), + setReleaseCandidateGitInfo( + getReleaseCandidateGitInfo({ latestRelease, project, semverBumpLevel }), ); - }, [semverBumpLevel, setNextGitHubInfo, latestRelease, project]); + }, [semverBumpLevel, setReleaseCandidateGitInfo, latestRelease, project]); - const { progress, responseSteps, run, runInvoked } = useCreateRc({ + const { + progress, + responseSteps, + run, + runInvoked, + } = useCreateReleaseCandidate({ defaultBranch, latestRelease, - nextGitHubInfo, + releaseCandidateGitInfo, project, successCb, }); @@ -99,15 +104,15 @@ export const CreateRc = ({ ); } - if (nextGitHubInfo.error !== undefined) { + if (releaseCandidateGitInfo.error !== undefined) { return ( - {nextGitHubInfo.error.title && ( - {nextGitHubInfo.error.title} + {releaseCandidateGitInfo.error.title && ( + {releaseCandidateGitInfo.error.title} )} - {nextGitHubInfo.error.subtitle} + {releaseCandidateGitInfo.error.subtitle} ); @@ -115,7 +120,7 @@ export const CreateRc = ({ const tagAlreadyExists = latestRelease !== null && - latestRelease.tagName === nextGitHubInfo.rcReleaseTag; + latestRelease.tagName === releaseCandidateGitInfo.rcReleaseTag; const conflictingPreRelease = latestRelease !== null && latestRelease.prerelease; @@ -159,7 +164,7 @@ export const CreateRc = ({ {tagAlreadyExists && ( There's already a tag named{' '} - {nextGitHubInfo.rcReleaseTag} + {releaseCandidateGitInfo.rcReleaseTag} )} @@ -169,7 +174,7 @@ export const CreateRc = ({ @@ -177,7 +182,7 @@ export const CreateRc = ({
diff --git a/plugins/git-release-manager/src/features/CreateRc/hooks/useCreateRc.test.tsx b/plugins/git-release-manager/src/features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate.test.tsx similarity index 90% rename from plugins/git-release-manager/src/features/CreateRc/hooks/useCreateRc.test.tsx rename to plugins/git-release-manager/src/features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate.test.tsx index adbbe3b9f6..2acffbf52c 100644 --- a/plugins/git-release-manager/src/features/CreateRc/hooks/useCreateRc.test.tsx +++ b/plugins/git-release-manager/src/features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate.test.tsx @@ -21,25 +21,25 @@ import { mockApiClient, mockCalverProject, mockDefaultBranch, - mockNextGitHubInfoCalver, + mockNextGitInfoCalver, mockReleaseVersionCalver, } from '../../../test-helpers/test-helpers'; -import { useCreateRc } from './useCreateRc'; +import { useCreateReleaseCandidate } from './useCreateReleaseCandidate'; jest.mock('@backstage/core', () => ({ useApi: () => mockApiClient, createApiRef: jest.fn(), })); -describe('useCreateRc', () => { +describe('useCreateReleaseCandidate', () => { beforeEach(jest.clearAllMocks); it('should return the expected responseSteps and progress', async () => { const { result } = renderHook(() => - useCreateRc({ + useCreateReleaseCandidate({ defaultBranch: mockDefaultBranch, latestRelease: mockReleaseVersionCalver, - nextGitHubInfo: mockNextGitHubInfoCalver, + releaseCandidateGitInfo: mockNextGitInfoCalver, project: mockCalverProject, }), ); @@ -54,10 +54,10 @@ describe('useCreateRc', () => { it('should return the expected responseSteps and progress (with successCb)', async () => { const { result } = renderHook(() => - useCreateRc({ + useCreateReleaseCandidate({ defaultBranch: mockDefaultBranch, latestRelease: mockReleaseVersionCalver, - nextGitHubInfo: mockNextGitHubInfoCalver, + releaseCandidateGitInfo: mockNextGitInfoCalver, project: mockCalverProject, successCb: jest.fn(), }), diff --git a/plugins/git-release-manager/src/features/CreateRc/hooks/useCreateRc.ts b/plugins/git-release-manager/src/features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate.ts similarity index 80% rename from plugins/git-release-manager/src/features/CreateRc/hooks/useCreateRc.ts rename to plugins/git-release-manager/src/features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate.ts index b205cfad6a..a5a174ebd2 100644 --- a/plugins/git-release-manager/src/features/CreateRc/hooks/useCreateRc.ts +++ b/plugins/git-release-manager/src/features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate.ts @@ -23,35 +23,35 @@ import { GetRepositoryResult, } from '../../../api/PluginApiClient'; import { CardHook, ComponentConfigCreateRc } from '../../../types/types'; -import { getRcGitHubInfo } from '../../../helpers/getRcGitHubInfo'; -import { githubReleaseManagerApiRef } from '../../../api/serviceApiRef'; -import { GitHubReleaseManagerError } from '../../../errors/GitHubReleaseManagerError'; +import { getReleaseCandidateGitInfo } from '../../../helpers/getReleaseCandidateGitInfo'; +import { gitReleaseManagerApiRef } from '../../../api/serviceApiRef'; +import { GitReleaseManagerError } from '../../../errors/GitReleaseManagerError'; import { Project } from '../../../contexts/ProjectContext'; import { useResponseSteps } from '../../../hooks/useResponseSteps'; -interface CreateRC { +interface UseCreateReleaseCandidate { defaultBranch: GetRepositoryResult['defaultBranch']; latestRelease: GetLatestReleaseResult; - nextGitHubInfo: ReturnType; + releaseCandidateGitInfo: ReturnType; project: Project; successCb?: ComponentConfigCreateRc['successCb']; } -export function useCreateRc({ +export function useCreateReleaseCandidate({ defaultBranch, latestRelease, - nextGitHubInfo, + releaseCandidateGitInfo, project, successCb, -}: CreateRC): CardHook { - const pluginApiClient = useApi(githubReleaseManagerApiRef); +}: UseCreateReleaseCandidate): CardHook { + const pluginApiClient = useApi(gitReleaseManagerApiRef); - if (nextGitHubInfo.error) { - throw new GitHubReleaseManagerError( + if (releaseCandidateGitInfo.error) { + throw new GitReleaseManagerError( `Unexpected error: ${ - nextGitHubInfo.error.title - ? `${nextGitHubInfo.error.title} (${nextGitHubInfo.error.subtitle})` - : nextGitHubInfo.error.subtitle + releaseCandidateGitInfo.error.title + ? `${releaseCandidateGitInfo.error.title} (${releaseCandidateGitInfo.error.subtitle})` + : releaseCandidateGitInfo.error.subtitle }`, ); } @@ -98,12 +98,12 @@ export function useCreateRc({ owner: project.owner, repo: project.repo, mostRecentSha: latestCommitRes.value.latestCommit.sha, - targetBranch: nextGitHubInfo.rcBranch, + targetBranch: releaseCandidateGitInfo.rcBranch, }) .catch(error => { if (error?.body?.message === 'Reference already exists') { - throw new GitHubReleaseManagerError( - `Branch "${nextGitHubInfo.rcBranch}" already exists: .../tree/${nextGitHubInfo.rcBranch}`, + throw new GitReleaseManagerError( + `Branch "${releaseCandidateGitInfo.rcBranch}" already exists: .../tree/${releaseCandidateGitInfo.rcBranch}`, ); } throw error; @@ -130,7 +130,7 @@ export function useCreateRc({ const previousReleaseBranch = latestRelease ? latestRelease.targetCommitish : defaultBranch; - const nextReleaseBranch = nextGitHubInfo.rcBranch; + const nextReleaseBranch = releaseCandidateGitInfo.rcBranch; const comparison = await pluginApiClient.createRc .getComparison({ owner: project.owner, @@ -173,16 +173,16 @@ export function useCreateRc({ .createRelease({ owner: project.owner, repo: project.repo, - rcReleaseTag: nextGitHubInfo.rcReleaseTag, - releaseName: nextGitHubInfo.releaseName, - rcBranch: nextGitHubInfo.rcBranch, + rcReleaseTag: releaseCandidateGitInfo.rcReleaseTag, + releaseName: releaseCandidateGitInfo.releaseName, + rcBranch: releaseCandidateGitInfo.rcBranch, releaseBody: getComparisonRes.value.releaseBody, }) .catch(asyncCatcher); addStepToResponseSteps({ message: `Created Release Candidate "${createReleaseResult.name}"`, - secondaryMessage: `with tag "${nextGitHubInfo.rcReleaseTag}"`, + secondaryMessage: `with tag "${releaseCandidateGitInfo.rcReleaseTag}"`, link: createReleaseResult.htmlUrl, }); diff --git a/plugins/git-release-manager/src/features/Features.test.tsx b/plugins/git-release-manager/src/features/Features.test.tsx index e2f5428331..15fa360f4a 100644 --- a/plugins/git-release-manager/src/features/Features.test.tsx +++ b/plugins/git-release-manager/src/features/Features.test.tsx @@ -66,7 +66,7 @@ describe('Features', () => { class="MuiTypography-root MuiTypography-body1" > - GitHub + Git : The source control system where releases reside in a practical sense. Read more about @@ -75,9 +75,9 @@ describe('Features', () => { href="https://docs.github.com/en/github/administering-a-repository/managing-releases-in-a-repository" target="_blank" > - GitHub releases + Git releases - . (Note that this plugin works just as well with GitHub Enterprise.) + .

{ Release Candidate - : A GitHub + : A Git prerelease - - intended primarily for internal testing + intended primarily for internal testing

{ Release Version - : A GitHub release intended for end users + : A Git release intended for end users

`); diff --git a/plugins/git-release-manager/src/features/Features.tsx b/plugins/git-release-manager/src/features/Features.tsx index f8c48b0732..ce715654ae 100644 --- a/plugins/git-release-manager/src/features/Features.tsx +++ b/plugins/git-release-manager/src/features/Features.tsx @@ -14,19 +14,19 @@ * limitations under the License. */ -import React, { useState } from 'react'; +import React, { useState, ComponentProps } from 'react'; import { Alert, AlertTitle } from '@material-ui/lab'; import { ErrorBoundary, useApi } from '@backstage/core'; import { CenteredCircularProgress } from '../components/CenteredCircularProgress'; -import { CreateRc } from './CreateRc/CreateRc'; -import { githubReleaseManagerApiRef } from '../api/serviceApiRef'; -import { GitHubReleaseManagerProps } from '../GitHubReleaseManager'; +import { CreateReleaseCandidate } from './CreateReleaseCandidate/CreateReleaseCandidate'; +import { GitReleaseManager } from '../GitReleaseManager'; +import { gitReleaseManagerApiRef } from '../api/serviceApiRef'; import { Info } from './Info/Info'; import { Patch } from './Patch/Patch'; import { PromoteRc } from './PromoteRc/PromoteRc'; import { RefetchContext } from '../contexts/RefetchContext'; -import { useGetGitHubBatchInfo } from '../hooks/useGetGitHubBatchInfo'; +import { useGetGitBatchInfo } from '../hooks/useGetGitBatchInfo'; import { useProjectContext } from '../contexts/ProjectContext'; import { useVersioningStrategyMatchesRepoTags } from '../hooks/useVersioningStrategyMatchesRepoTags'; import { validateTagName } from '../helpers/tagParts/validateTagName'; @@ -34,43 +34,43 @@ import { validateTagName } from '../helpers/tagParts/validateTagName'; export function Features({ features, }: { - features: GitHubReleaseManagerProps['features']; + features: ComponentProps['features']; }) { - const pluginApiClient = useApi(githubReleaseManagerApiRef); + const pluginApiClient = useApi(gitReleaseManagerApiRef); const { project } = useProjectContext(); const [refetchTrigger, setRefetchTrigger] = useState(0); - const { gitHubBatchInfo } = useGetGitHubBatchInfo({ + const { gitBatchInfo } = useGetGitBatchInfo({ pluginApiClient, project, refetchTrigger, }); const { versioningStrategyMatches } = useVersioningStrategyMatchesRepoTags({ - latestReleaseTagName: gitHubBatchInfo.value?.latestRelease?.tagName, + latestReleaseTagName: gitBatchInfo.value?.latestRelease?.tagName, project, - repositoryName: gitHubBatchInfo.value?.repository.name, + repositoryName: gitBatchInfo.value?.repository.name, }); - if (gitHubBatchInfo.error) { + if (gitBatchInfo.error) { return ( Error occured while fetching information for "{project.owner}/ - {project.repo}" ({gitHubBatchInfo.error.message}) + {project.repo}" ({gitBatchInfo.error.message}) ); } - if (gitHubBatchInfo.loading) { + if (gitBatchInfo.loading) { return ; } - if (gitHubBatchInfo.value === undefined) { + if (gitBatchInfo.value === undefined) { return ( Failed to fetch latest GitHub release ); } - if (!gitHubBatchInfo.value.repository.pushPermissions) { + if (!gitBatchInfo.value.repository.pushPermissions) { return ( You lack push permissions for repository "{project.owner}/{project.repo} @@ -81,7 +81,7 @@ export function Features({ const { tagNameError } = validateTagName({ project, - tagName: gitHubBatchInfo.value.latestRelease?.tagName, + tagName: gitBatchInfo.value.latestRelease?.tagName, }); if (tagNameError) { return ( @@ -95,20 +95,20 @@ export function Features({ return ( - {gitHubBatchInfo.value.latestRelease && !versioningStrategyMatches && ( + {gitBatchInfo.value.latestRelease && !versioningStrategyMatches && ( Versioning mismatch, expected {project.versioningStrategy} version, - got "{gitHubBatchInfo.value.latestRelease.tagName}" + got "{gitBatchInfo.value.latestRelease.tagName}" )} - {!gitHubBatchInfo.value.latestRelease && ( + {!gitBatchInfo.value.latestRelease && ( This repository doesn't have any releases yet )} - {!gitHubBatchInfo.value.releaseBranch && ( + {!gitBatchInfo.value.releaseBranch && ( This repository doesn't have any release branches @@ -116,32 +116,32 @@ export function Features({ {!features?.info?.omit && ( )} {!features?.createRc?.omit && ( - )} {!features?.promoteRc?.omit && ( )} {!features?.patch?.omit && ( )} diff --git a/plugins/git-release-manager/src/features/Info/Info.tsx b/plugins/git-release-manager/src/features/Info/Info.tsx index 063c66adee..e6be3b45de 100644 --- a/plugins/git-release-manager/src/features/Info/Info.tsx +++ b/plugins/git-release-manager/src/features/Info/Info.tsx @@ -51,25 +51,24 @@ export const Info = ({ Terminology - GitHub: The source control system where releases - reside in a practical sense. Read more about{' '} + Git: The source control system where releases reside + in a practical sense. Read more about{' '} - GitHub releases + Git releases - . (Note that this plugin works just as well with GitHub Enterprise.) + . - Release Candidate: A GitHub prerelease{' '} - intended primarily for internal testing + Release Candidate: A Git prerelease intended + primarily for internal testing - Release Version: A GitHub release intended for end - users + Release Version: A Git release intended for end users @@ -77,7 +76,7 @@ export const Info = ({ Flow - GitHub Release Manager is built with a specific flow in mind. For + Git Release Manager is built with a specific flow in mind. For example, it assumes your project is configured to react to tags prefixed with rc or version. diff --git a/plugins/git-release-manager/src/features/Patch/PatchBody.tsx b/plugins/git-release-manager/src/features/Patch/PatchBody.tsx index 47b1db1578..8ab21e1112 100644 --- a/plugins/git-release-manager/src/features/Patch/PatchBody.tsx +++ b/plugins/git-release-manager/src/features/Patch/PatchBody.tsx @@ -42,8 +42,8 @@ import { CalverTagParts } from '../../helpers/tagParts/getCalverTagParts'; import { CenteredCircularProgress } from '../../components/CenteredCircularProgress'; import { ComponentConfigPatch } from '../../types/types'; import { Differ } from '../../components/Differ'; -import { githubReleaseManagerApiRef } from '../../api/serviceApiRef'; -import { GitHubReleaseManagerError } from '../../errors/GitHubReleaseManagerError'; +import { gitReleaseManagerApiRef } from '../../api/serviceApiRef'; +import { GitReleaseManagerError } from '../../errors/GitReleaseManagerError'; import { ResponseStepDialog } from '../../components/ResponseStepDialog/ResponseStepDialog'; import { SemverTagParts } from '../../helpers/tagParts/getSemverTagParts'; import { TEST_IDS } from '../../test-helpers/test-ids'; @@ -66,11 +66,11 @@ export const PatchBody = ({ successCb, tagParts, }: PatchBodyProps) => { - const pluginApiClient = useApi(githubReleaseManagerApiRef); + const pluginApiClient = useApi(gitReleaseManagerApiRef); const { project } = useProjectContext(); const [checkedCommitIndex, setCheckedCommitIndex] = useState(-1); - const githubDataResponse = useAsync(async () => { + const gitDataResponse = useAsync(async () => { const [ recentCommitsOnDefaultBranch, recentCommitsOnReleaseBranch, @@ -109,15 +109,15 @@ export const PatchBody = ({ ); } - if (githubDataResponse.error) { + if (gitDataResponse.error) { return ( - Unexpected error: {githubDataResponse.error.message} + Unexpected error: {gitDataResponse.error.message} ); } - if (githubDataResponse.loading) { + if (gitDataResponse.loading) { return ; } @@ -133,7 +133,7 @@ export const PatchBody = ({ severity="info" > - The current GitHub release is a Release Version + The current Git release is a Release Version It's still possible to patch it, but be extra mindful of changes @@ -147,16 +147,16 @@ export const PatchBody = ({ } function CommitList() { - if (!githubDataResponse.value?.recentCommitsOnDefaultBranch) { + if (!gitDataResponse.value?.recentCommitsOnDefaultBranch) { return null; } return ( - {githubDataResponse.value.recentCommitsOnDefaultBranch.map( + {gitDataResponse.value.recentCommitsOnDefaultBranch.map( (commit, index) => { // FIXME: Performance improvement opportunity: Convert to object lookup - const commitExistsOnReleaseBranch = !!githubDataResponse.value?.recentCommitsOnReleaseBranch.find( + const commitExistsOnReleaseBranch = !!gitDataResponse.value?.recentCommitsOnReleaseBranch.find( releaseBranchCommit => releaseBranchCommit.sha === commit.sha || releaseBranchCommit.commit.message.includes(commit.sha), // The selected patch commit's sha is included in the commit message @@ -277,11 +277,11 @@ export const PatchBody = ({ color="primary" onClick={() => { const selectedPatchCommit = - githubDataResponse.value?.recentCommitsOnDefaultBranch[ + gitDataResponse.value?.recentCommitsOnDefaultBranch[ checkedCommitIndex ]; if (!selectedPatchCommit) { - throw new GitHubReleaseManagerError( + throw new GitReleaseManagerError( 'Could not find selected patch commit', ); } diff --git a/plugins/git-release-manager/src/features/Patch/hooks/usePatch.ts b/plugins/git-release-manager/src/features/Patch/hooks/usePatch.ts index 5da8d020b9..dc13e65ea7 100644 --- a/plugins/git-release-manager/src/features/Patch/hooks/usePatch.ts +++ b/plugins/git-release-manager/src/features/Patch/hooks/usePatch.ts @@ -24,7 +24,7 @@ import { } from '../../../api/PluginApiClient'; import { CalverTagParts } from '../../../helpers/tagParts/getCalverTagParts'; import { ComponentConfigPatch, CardHook } from '../../../types/types'; -import { githubReleaseManagerApiRef } from '../../../api/serviceApiRef'; +import { gitReleaseManagerApiRef } from '../../../api/serviceApiRef'; import { Project } from '../../../contexts/ProjectContext'; import { SemverTagParts } from '../../../helpers/tagParts/getSemverTagParts'; import { useResponseSteps } from '../../../hooks/useResponseSteps'; @@ -45,7 +45,7 @@ export function usePatch({ tagParts, successCb, }: Patch): CardHook { - const pluginApiClient = useApi(githubReleaseManagerApiRef); + const pluginApiClient = useApi(gitReleaseManagerApiRef); const { responseSteps, addStepToResponseSteps, diff --git a/plugins/git-release-manager/src/features/PromoteRc/PromoteRc.tsx b/plugins/git-release-manager/src/features/PromoteRc/PromoteRc.tsx index 5d2783bcfd..16dd8de38c 100644 --- a/plugins/git-release-manager/src/features/PromoteRc/PromoteRc.tsx +++ b/plugins/git-release-manager/src/features/PromoteRc/PromoteRc.tsx @@ -46,9 +46,7 @@ export const PromoteRc = ({ latestRelease, successCb }: PromoteRcProps) => { className={classes.paragraph} severity="warning" > - - Latest GitHub release is not a Release Candidate - + Latest Git release is not a Release Candidate One can only promote Release Candidates to Release Versions ); diff --git a/plugins/git-release-manager/src/features/PromoteRc/hooks/usePromoteRc.ts b/plugins/git-release-manager/src/features/PromoteRc/hooks/usePromoteRc.ts index 6ff6c5ef9f..075700f2c1 100644 --- a/plugins/git-release-manager/src/features/PromoteRc/hooks/usePromoteRc.ts +++ b/plugins/git-release-manager/src/features/PromoteRc/hooks/usePromoteRc.ts @@ -20,7 +20,7 @@ import { useApi } from '@backstage/core'; import { CardHook, ComponentConfigPromoteRc } from '../../../types/types'; import { GetLatestReleaseResult } from '../../../api/PluginApiClient'; -import { githubReleaseManagerApiRef } from '../../../api/serviceApiRef'; +import { gitReleaseManagerApiRef } from '../../../api/serviceApiRef'; import { useProjectContext } from '../../../contexts/ProjectContext'; import { useResponseSteps } from '../../../hooks/useResponseSteps'; @@ -35,7 +35,7 @@ export function usePromoteRc({ releaseVersion, successCb, }: PromoteRc): CardHook { - const pluginApiClient = useApi(githubReleaseManagerApiRef); + const pluginApiClient = useApi(gitReleaseManagerApiRef); const { project } = useProjectContext(); const { responseSteps, diff --git a/plugins/git-release-manager/src/features/RepoDetailsForm/Owner.tsx b/plugins/git-release-manager/src/features/RepoDetailsForm/Owner.tsx index a392671c0f..36d63d2b47 100644 --- a/plugins/git-release-manager/src/features/RepoDetailsForm/Owner.tsx +++ b/plugins/git-release-manager/src/features/RepoDetailsForm/Owner.tsx @@ -27,14 +27,14 @@ import { import { useApi } from '@backstage/core'; import { CenteredCircularProgress } from '../../components/CenteredCircularProgress'; -import { githubReleaseManagerApiRef } from '../../api/serviceApiRef'; +import { gitReleaseManagerApiRef } from '../../api/serviceApiRef'; import { TEST_IDS } from '../../test-helpers/test-ids'; import { useFormClasses } from './styles'; import { useProjectContext } from '../../contexts/ProjectContext'; import { useQueryHandler } from '../../hooks/useQueryHandler'; export function Owner({ username }: { username: string }) { - const pluginApiClient = useApi(githubReleaseManagerApiRef); + const pluginApiClient = useApi(gitReleaseManagerApiRef); const { project } = useProjectContext(); const formClasses = useFormClasses(); const navigate = useNavigate(); diff --git a/plugins/git-release-manager/src/features/RepoDetailsForm/Repo.tsx b/plugins/git-release-manager/src/features/RepoDetailsForm/Repo.tsx index b1f8298495..3db956116a 100644 --- a/plugins/git-release-manager/src/features/RepoDetailsForm/Repo.tsx +++ b/plugins/git-release-manager/src/features/RepoDetailsForm/Repo.tsx @@ -27,14 +27,14 @@ import { import { useApi } from '@backstage/core'; import { CenteredCircularProgress } from '../../components/CenteredCircularProgress'; -import { githubReleaseManagerApiRef } from '../../api/serviceApiRef'; +import { gitReleaseManagerApiRef } from '../../api/serviceApiRef'; import { TEST_IDS } from '../../test-helpers/test-ids'; import { useFormClasses } from './styles'; import { useProjectContext } from '../../contexts/ProjectContext'; import { useQueryHandler } from '../../hooks/useQueryHandler'; export function Repo() { - const pluginApiClient = useApi(githubReleaseManagerApiRef); + const pluginApiClient = useApi(gitReleaseManagerApiRef); const { project } = useProjectContext(); const navigate = useNavigate(); const formClasses = useFormClasses(); diff --git a/plugins/git-release-manager/src/features/Stats/Info/hooks/useGetReleaseTimes.tsx b/plugins/git-release-manager/src/features/Stats/Info/hooks/useGetReleaseTimes.tsx index afe9a2f8c5..37f1d88a88 100644 --- a/plugins/git-release-manager/src/features/Stats/Info/hooks/useGetReleaseTimes.tsx +++ b/plugins/git-release-manager/src/features/Stats/Info/hooks/useGetReleaseTimes.tsx @@ -20,7 +20,7 @@ import { DateTime } from 'luxon'; import { useApi } from '@backstage/core'; import { getReleaseCommitPairs } from '../helpers/getReleaseCommitPairs'; -import { githubReleaseManagerApiRef } from '../../../../api/serviceApiRef'; +import { gitReleaseManagerApiRef } from '../../../../api/serviceApiRef'; import { useProjectContext } from '../../../../contexts/ProjectContext'; import { useReleaseStatsContext } from '../../contexts/ReleaseStatsContext'; @@ -46,7 +46,7 @@ type ReleaseTime = { }; export function useGetReleaseTimes() { - const pluginApiClient = useApi(githubReleaseManagerApiRef); + const pluginApiClient = useApi(gitReleaseManagerApiRef); const { project } = useProjectContext(); const { releaseStats } = useReleaseStatsContext(); const [averageReleaseTime, setAverageReleaseTime] = useState( diff --git a/plugins/git-release-manager/src/features/Stats/contexts/ReleaseStatsContext.tsx b/plugins/git-release-manager/src/features/Stats/contexts/ReleaseStatsContext.tsx index be0d68e2e5..452adbb87b 100644 --- a/plugins/git-release-manager/src/features/Stats/contexts/ReleaseStatsContext.tsx +++ b/plugins/git-release-manager/src/features/Stats/contexts/ReleaseStatsContext.tsx @@ -16,7 +16,7 @@ import { createContext, useContext } from 'react'; -import { GitHubReleaseManagerError } from '../../../errors/GitHubReleaseManagerError'; +import { GitReleaseManagerError } from '../../../errors/GitReleaseManagerError'; export interface ReleaseStats { unmappableTags: string[]; @@ -55,7 +55,7 @@ export const useReleaseStatsContext = () => { const { releaseStats } = useContext(ReleaseStatsContext) ?? {}; if (!releaseStats) { - throw new GitHubReleaseManagerError('releaseStats not found'); + throw new GitReleaseManagerError('releaseStats not found'); } return { diff --git a/plugins/git-release-manager/src/features/Stats/hooks/useGetCommit.ts b/plugins/git-release-manager/src/features/Stats/hooks/useGetCommit.ts index f9681beda6..01ff486a74 100644 --- a/plugins/git-release-manager/src/features/Stats/hooks/useGetCommit.ts +++ b/plugins/git-release-manager/src/features/Stats/hooks/useGetCommit.ts @@ -17,17 +17,17 @@ import { useAsync } from 'react-use'; import { useApi } from '@backstage/core'; -import { githubReleaseManagerApiRef } from '../../../api/serviceApiRef'; -import { GitHubReleaseManagerError } from '../../../errors/GitHubReleaseManagerError'; +import { gitReleaseManagerApiRef } from '../../../api/serviceApiRef'; +import { GitReleaseManagerError } from '../../../errors/GitReleaseManagerError'; import { useProjectContext } from '../../../contexts/ProjectContext'; export const useGetCommit = ({ ref }: { ref?: string }) => { - const pluginApiClient = useApi(githubReleaseManagerApiRef); + const pluginApiClient = useApi(gitReleaseManagerApiRef); const { project } = useProjectContext(); const commit = useAsync(async () => { if (!ref) { - throw new GitHubReleaseManagerError('Missing ref to get commit'); + throw new GitReleaseManagerError('Missing ref to get commit'); } return pluginApiClient.stats.getCommit({ diff --git a/plugins/git-release-manager/src/features/Stats/hooks/useGetStats.ts b/plugins/git-release-manager/src/features/Stats/hooks/useGetStats.ts index 3e093f68aa..ed29c09e75 100644 --- a/plugins/git-release-manager/src/features/Stats/hooks/useGetStats.ts +++ b/plugins/git-release-manager/src/features/Stats/hooks/useGetStats.ts @@ -17,11 +17,11 @@ import { useApi } from '@backstage/core'; import { useAsync } from 'react-use'; -import { githubReleaseManagerApiRef } from '../../../api/serviceApiRef'; +import { gitReleaseManagerApiRef } from '../../../api/serviceApiRef'; import { useProjectContext } from '../../../contexts/ProjectContext'; export const useGetStats = () => { - const pluginApiClient = useApi(githubReleaseManagerApiRef); + const pluginApiClient = useApi(gitReleaseManagerApiRef); const { project } = useProjectContext(); const stats = useAsync(async () => { diff --git a/plugins/git-release-manager/src/helpers/getRcGitHubInfo.test.ts b/plugins/git-release-manager/src/helpers/getReleaseCandidateGitInfo.test.ts similarity index 91% rename from plugins/git-release-manager/src/helpers/getRcGitHubInfo.test.ts rename to plugins/git-release-manager/src/helpers/getReleaseCandidateGitInfo.test.ts index 51ad5fc122..23aa7366b7 100644 --- a/plugins/git-release-manager/src/helpers/getRcGitHubInfo.test.ts +++ b/plugins/git-release-manager/src/helpers/getReleaseCandidateGitInfo.test.ts @@ -22,9 +22,9 @@ import { mockReleaseVersionSemver, mockSemverProject, } from '../test-helpers/test-helpers'; -import { getRcGitHubInfo } from './getRcGitHubInfo'; +import { getReleaseCandidateGitInfo } from './getReleaseCandidateGitInfo'; -describe('getRcGitHubInfo', () => { +describe('getReleaseCandidateGitInfo', () => { describe('DateTime', () => { it('should format dates as expected', () => { const formattedDate = DateTime.now().toFormat('yyyy.MM.dd'); @@ -36,7 +36,7 @@ describe('getRcGitHubInfo', () => { describe('calver', () => { it('should return correct GitHub info', () => { expect( - getRcGitHubInfo({ + getReleaseCandidateGitInfo({ project: mockCalverProject, latestRelease: mockReleaseVersionCalver, semverBumpLevel: 'minor', @@ -55,7 +55,7 @@ describe('getRcGitHubInfo', () => { describe('semver', () => { it("should return correct GitHub info when there's previous releases", () => { expect( - getRcGitHubInfo({ + getReleaseCandidateGitInfo({ project: mockSemverProject, latestRelease: mockReleaseVersionSemver, semverBumpLevel: 'minor', @@ -71,7 +71,7 @@ describe('getRcGitHubInfo', () => { it("should return correct GitHub info when there's no previous release", () => { expect( - getRcGitHubInfo({ + getReleaseCandidateGitInfo({ project: mockSemverProject, latestRelease: null, semverBumpLevel: 'minor', diff --git a/plugins/git-release-manager/src/helpers/getRcGitHubInfo.ts b/plugins/git-release-manager/src/helpers/getReleaseCandidateGitInfo.ts similarity index 97% rename from plugins/git-release-manager/src/helpers/getRcGitHubInfo.ts rename to plugins/git-release-manager/src/helpers/getReleaseCandidateGitInfo.ts index 70935a0dd3..bef2e63d44 100644 --- a/plugins/git-release-manager/src/helpers/getRcGitHubInfo.ts +++ b/plugins/git-release-manager/src/helpers/getReleaseCandidateGitInfo.ts @@ -22,7 +22,7 @@ import { getSemverTagParts } from './tagParts/getSemverTagParts'; import { Project } from '../contexts/ProjectContext'; import { SEMVER_PARTS } from '../constants/constants'; -export const getRcGitHubInfo = ({ +export const getReleaseCandidateGitInfo = ({ project, latestRelease, semverBumpLevel, diff --git a/plugins/git-release-manager/src/helpers/getShortCommitHash.ts b/plugins/git-release-manager/src/helpers/getShortCommitHash.ts index e2088caced..55241b390a 100644 --- a/plugins/git-release-manager/src/helpers/getShortCommitHash.ts +++ b/plugins/git-release-manager/src/helpers/getShortCommitHash.ts @@ -14,13 +14,13 @@ * limitations under the License. */ -import { GitHubReleaseManagerError } from '../errors/GitHubReleaseManagerError'; +import { GitReleaseManagerError } from '../errors/GitReleaseManagerError'; export function getShortCommitHash(hash: string) { const shortCommitHash = hash.substr(0, 7); if (shortCommitHash.length < 7) { - throw new GitHubReleaseManagerError( + throw new GitReleaseManagerError( 'Invalid shortCommitHash: less than 7 characters', ); } diff --git a/plugins/git-release-manager/src/hooks/useGetGitHubBatchInfo.test.ts b/plugins/git-release-manager/src/hooks/useGetGitBatchInfo.test.ts similarity index 85% rename from plugins/git-release-manager/src/hooks/useGetGitHubBatchInfo.test.ts rename to plugins/git-release-manager/src/hooks/useGetGitBatchInfo.test.ts index 7d45fd111a..e2a355a2e6 100644 --- a/plugins/git-release-manager/src/hooks/useGetGitHubBatchInfo.test.ts +++ b/plugins/git-release-manager/src/hooks/useGetGitBatchInfo.test.ts @@ -18,12 +18,12 @@ import { renderHook, act } from '@testing-library/react-hooks'; import { waitFor } from '@testing-library/react'; import { mockApiClient, mockSemverProject } from '../test-helpers/test-helpers'; -import { useGetGitHubBatchInfo } from './useGetGitHubBatchInfo'; +import { useGetGitBatchInfo } from './useGetGitBatchInfo'; -describe('useGetGitHubBatchInfo', () => { +describe('useGetHubBatchInfo', () => { it('should handle repositories with releases', async () => { const { result } = renderHook(() => - useGetGitHubBatchInfo({ + useGetGitBatchInfo({ pluginApiClient: mockApiClient, project: mockSemverProject, refetchTrigger: 1337, @@ -31,10 +31,10 @@ describe('useGetGitHubBatchInfo', () => { ); await act(async () => { - await waitFor(() => result.current.gitHubBatchInfo !== undefined); + await waitFor(() => result.current.gitBatchInfo !== undefined); }); - expect(result.current.gitHubBatchInfo).toMatchInlineSnapshot(` + expect(result.current.gitBatchInfo).toMatchInlineSnapshot(` Object { "loading": false, "value": Object { @@ -73,7 +73,7 @@ describe('useGetGitHubBatchInfo', () => { (mockApiClient.getLatestRelease as jest.Mock).mockResolvedValueOnce(null); const { result } = renderHook(() => - useGetGitHubBatchInfo({ + useGetGitBatchInfo({ pluginApiClient: mockApiClient, project: mockSemverProject, refetchTrigger: 1337, @@ -81,10 +81,10 @@ describe('useGetGitHubBatchInfo', () => { ); await act(async () => { - await waitFor(() => result.current.gitHubBatchInfo !== undefined); + await waitFor(() => result.current.gitBatchInfo !== undefined); }); - expect(result.current.gitHubBatchInfo).toMatchInlineSnapshot(` + expect(result.current.gitBatchInfo).toMatchInlineSnapshot(` Object { "loading": false, "value": Object { diff --git a/plugins/git-release-manager/src/hooks/useGetGitHubBatchInfo.ts b/plugins/git-release-manager/src/hooks/useGetGitBatchInfo.ts similarity index 83% rename from plugins/git-release-manager/src/hooks/useGetGitHubBatchInfo.ts rename to plugins/git-release-manager/src/hooks/useGetGitBatchInfo.ts index 5cf5115116..6469b275b5 100644 --- a/plugins/git-release-manager/src/hooks/useGetGitHubBatchInfo.ts +++ b/plugins/git-release-manager/src/hooks/useGetGitBatchInfo.ts @@ -16,21 +16,21 @@ import { useAsync } from 'react-use'; -import { IPluginApiClient } from '../api/PluginApiClient'; +import { GitReleaseApi } from '../api/PluginApiClient'; import { Project } from '../contexts/ProjectContext'; -interface GetGitHubBatchInfo { +interface GetGitBatchInfo { project: Project; - pluginApiClient: IPluginApiClient; + pluginApiClient: GitReleaseApi; refetchTrigger: number; } -export const useGetGitHubBatchInfo = ({ +export const useGetGitBatchInfo = ({ project, pluginApiClient, refetchTrigger, -}: GetGitHubBatchInfo) => { - const gitHubBatchInfo = useAsync(async () => { +}: GetGitBatchInfo) => { + const gitBatchInfo = useAsync(async () => { const [repository, latestRelease] = await Promise.all([ pluginApiClient.getRepository({ ...project }), pluginApiClient.getLatestRelease({ ...project }), @@ -57,6 +57,6 @@ export const useGetGitHubBatchInfo = ({ }, [project, refetchTrigger]); return { - gitHubBatchInfo, + gitBatchInfo, }; }; diff --git a/plugins/git-release-manager/src/index.ts b/plugins/git-release-manager/src/index.ts index bb9a29c419..38b6a4ebb3 100644 --- a/plugins/git-release-manager/src/index.ts +++ b/plugins/git-release-manager/src/index.ts @@ -15,7 +15,7 @@ */ export { - gitHubReleaseManagerPlugin, - GitHubReleaseManagerPage, - githubReleaseManagerApiRef, + gitReleaseManagerPlugin, + GitReleaseManagerPage, + gitReleaseManagerApiRef, } from './plugin'; diff --git a/plugins/git-release-manager/src/plugin.test.ts b/plugins/git-release-manager/src/plugin.test.ts index 5f30848771..a4882ceb9b 100644 --- a/plugins/git-release-manager/src/plugin.test.ts +++ b/plugins/git-release-manager/src/plugin.test.ts @@ -20,9 +20,9 @@ describe('git-release-manager', () => { it('should export plugin & friends', () => { expect(Object.keys(plugin)).toMatchInlineSnapshot(` Array [ - "githubReleaseManagerApiRef", - "gitHubReleaseManagerPlugin", - "GitHubReleaseManagerPage", + "gitReleaseManagerApiRef", + "gitReleaseManagerPlugin", + "GitReleaseManagerPage", ] `); }); diff --git a/plugins/git-release-manager/src/plugin.ts b/plugins/git-release-manager/src/plugin.ts index 507d1044aa..0760f6a393 100644 --- a/plugins/git-release-manager/src/plugin.ts +++ b/plugins/git-release-manager/src/plugin.ts @@ -22,26 +22,26 @@ import { createRoutableExtension, } from '@backstage/core'; -import { githubReleaseManagerApiRef } from './api/serviceApiRef'; -import { PluginApiClient } from './api/PluginApiClient'; +import { gitReleaseManagerApiRef } from './api/serviceApiRef'; +import { GitReleaseApiClient } from './api/PluginApiClient'; import { rootRouteRef } from './routes'; -export { githubReleaseManagerApiRef }; +export { gitReleaseManagerApiRef }; -export const gitHubReleaseManagerPlugin = createPlugin({ +export const gitReleaseManagerPlugin = createPlugin({ id: 'git-release-manager', routes: { root: rootRouteRef, }, apis: [ createApiFactory({ - api: githubReleaseManagerApiRef, + api: gitReleaseManagerApiRef, deps: { configApi: configApiRef, githubAuthApi: githubAuthApiRef, }, factory: ({ configApi, githubAuthApi }) => { - return new PluginApiClient({ + return new GitReleaseApiClient({ configApi, githubAuthApi, }); @@ -50,10 +50,10 @@ export const gitHubReleaseManagerPlugin = createPlugin({ ], }); -export const GitHubReleaseManagerPage = gitHubReleaseManagerPlugin.provide( +export const GitReleaseManagerPage = gitReleaseManagerPlugin.provide( createRoutableExtension({ component: () => - import('./GitHubReleaseManager').then(m => m.GitHubReleaseManager), + import('./GitReleaseManager').then(m => m.GitReleaseManager), mountPoint: rootRouteRef, }), ); diff --git a/plugins/git-release-manager/src/test-helpers/test-helpers.ts b/plugins/git-release-manager/src/test-helpers/test-helpers.ts index 68bd9c7e3b..cbf287a5cf 100644 --- a/plugins/git-release-manager/src/test-helpers/test-helpers.ts +++ b/plugins/git-release-manager/src/test-helpers/test-helpers.ts @@ -18,11 +18,11 @@ import { GetBranchResult, GetLatestReleaseResult, GetRecentCommitsResultSingle, - IPluginApiClient, + GitReleaseApi, } from '../api/PluginApiClient'; import { CalverTagParts } from '../helpers/tagParts/getCalverTagParts'; import { Project } from '../contexts/ProjectContext'; -import { getRcGitHubInfo } from '../helpers/getRcGitHubInfo'; +import { getReleaseCandidateGitInfo } from '../helpers/getReleaseCandidateGitInfo'; const mockOwner = 'mock_owner'; const mockRepo = 'mock_repo'; @@ -59,13 +59,17 @@ export const mockSearchSemver = `?versioningStrategy=${mockSemverProject.version export const mockDefaultBranch = 'mock_defaultBranch'; -export const mockNextGitHubInfoSemver: ReturnType = { +export const mockNextGitInfoSemver: ReturnType< + typeof getReleaseCandidateGitInfo +> = { rcBranch: MOCK_RELEASE_BRANCH_NAME_SEMVER, rcReleaseTag: MOCK_RELEASE_CANDIDATE_TAG_NAME_SEMVER, releaseName: MOCK_RELEASE_NAME_SEMVER, }; -export const mockNextGitHubInfoCalver: ReturnType = { +export const mockNextGitInfoCalver: ReturnType< + typeof getReleaseCandidateGitInfo +> = { rcBranch: MOCK_RELEASE_BRANCH_NAME_CALVER, rcReleaseTag: MOCK_RELEASE_CANDIDATE_TAG_NAME_CALVER, releaseName: MOCK_RELEASE_NAME_CALVER, @@ -168,7 +172,7 @@ export const mockSelectedPatchCommit = createMockRecentCommit({ /** * MOCK API CLIENT */ -export const mockApiClient: IPluginApiClient = { +export const mockApiClient: GitReleaseApi = { getHost: jest.fn(() => 'github.com'), getRepoPath: jest.fn(() => `${mockOwner}/${mockRepo}`), diff --git a/plugins/git-release-manager/src/test-helpers/test-ids.ts b/plugins/git-release-manager/src/test-helpers/test-ids.ts index a5b38e93a1..5e4175ba4d 100644 --- a/plugins/git-release-manager/src/test-helpers/test-ids.ts +++ b/plugins/git-release-manager/src/test-helpers/test-ids.ts @@ -72,7 +72,7 @@ export const TEST_IDS = { icons: { tag: 'grm--differ--icons--tag', branch: 'grm--differ--icons--branch', - github: 'grm--differ--icons--github', + github: 'grm--differ--icons--git', slack: 'grm--differ--icons--slack', versioning: 'grm--differ--icons--versioning', }, From 7a61ec487cc6ce494705a709268f3b4544626bf8 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Thu, 29 Apr 2021 12:03:56 +0200 Subject: [PATCH 085/140] Continue renaming code from GitHub to Git Signed-off-by: Erik Engervall --- plugins/git-release-manager/dev/index.tsx | 8 ++++---- .../hooks/useCreateReleaseCandidate.ts | 6 +++--- plugins/git-release-manager/src/features/Features.tsx | 4 +--- .../src/features/PromoteRc/hooks/usePromoteRc.ts | 4 ++-- .../src/helpers/getReleaseCandidateGitInfo.test.ts | 6 +++--- plugins/git-release-manager/src/types/types.ts | 8 ++++---- 6 files changed, 17 insertions(+), 19 deletions(-) diff --git a/plugins/git-release-manager/dev/index.tsx b/plugins/git-release-manager/dev/index.tsx index d1d2d6e7d9..ba794e7b21 100644 --- a/plugins/git-release-manager/dev/index.tsx +++ b/plugins/git-release-manager/dev/index.tsx @@ -82,8 +82,8 @@ createDevApp() successCb: ({ comparisonUrl, createdTag, - gitHubReleaseName, - gitHubReleaseUrl, + gitReleaseName, + gitReleaseUrl, previousTag, }) => { // eslint-disable-next-line no-console @@ -91,8 +91,8 @@ createDevApp() 'Custom success callback for Create RC', comparisonUrl, createdTag, - gitHubReleaseName, - gitHubReleaseUrl, + gitReleaseName, + gitReleaseUrl, previousTag, ); }, diff --git a/plugins/git-release-manager/src/features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate.ts b/plugins/git-release-manager/src/features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate.ts index a5a174ebd2..d922658d2c 100644 --- a/plugins/git-release-manager/src/features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate.ts +++ b/plugins/git-release-manager/src/features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate.ts @@ -163,7 +163,7 @@ export function useCreateReleaseCandidate({ }, [createRcRes.value, createRcRes.error]); /** - * (4) Creates the release itself in GitHub + * (4) Creates the Git Release itself */ const createReleaseRes = useAsync(async () => { abortIfError(getComparisonRes.error); @@ -202,8 +202,8 @@ export function useCreateReleaseCandidate({ await successCb({ comparisonUrl: getComparisonRes.value.htmlUrl, createdTag: createReleaseRes.value.tagName, - gitHubReleaseName: createReleaseRes.value.name, - gitHubReleaseUrl: createReleaseRes.value.htmlUrl, + gitReleaseName: createReleaseRes.value.name, + gitReleaseUrl: createReleaseRes.value.htmlUrl, previousTag: latestRelease?.tagName, }); } catch (error) { diff --git a/plugins/git-release-manager/src/features/Features.tsx b/plugins/git-release-manager/src/features/Features.tsx index ce715654ae..3649b0c3f7 100644 --- a/plugins/git-release-manager/src/features/Features.tsx +++ b/plugins/git-release-manager/src/features/Features.tsx @@ -65,9 +65,7 @@ export function Features({ } if (gitBatchInfo.value === undefined) { - return ( - Failed to fetch latest GitHub release - ); + return Failed to fetch latest Git release; } if (!gitBatchInfo.value.repository.pushPermissions) { diff --git a/plugins/git-release-manager/src/features/PromoteRc/hooks/usePromoteRc.ts b/plugins/git-release-manager/src/features/PromoteRc/hooks/usePromoteRc.ts index 075700f2c1..ba236b29ce 100644 --- a/plugins/git-release-manager/src/features/PromoteRc/hooks/usePromoteRc.ts +++ b/plugins/git-release-manager/src/features/PromoteRc/hooks/usePromoteRc.ts @@ -77,8 +77,8 @@ export function usePromoteRc({ try { await successCb?.({ - gitHubReleaseUrl: promotedReleaseRes.value.htmlUrl, - gitHubReleaseName: promotedReleaseRes.value.name, + gitReleaseUrl: promotedReleaseRes.value.htmlUrl, + gitReleaseName: promotedReleaseRes.value.name, previousTagUrl: rcRelease.htmlUrl, previousTag: rcRelease.tagName, updatedTagUrl: promotedReleaseRes.value.htmlUrl, diff --git a/plugins/git-release-manager/src/helpers/getReleaseCandidateGitInfo.test.ts b/plugins/git-release-manager/src/helpers/getReleaseCandidateGitInfo.test.ts index 23aa7366b7..c27e37ce86 100644 --- a/plugins/git-release-manager/src/helpers/getReleaseCandidateGitInfo.test.ts +++ b/plugins/git-release-manager/src/helpers/getReleaseCandidateGitInfo.test.ts @@ -34,7 +34,7 @@ describe('getReleaseCandidateGitInfo', () => { }); describe('calver', () => { - it('should return correct GitHub info', () => { + it('should return correct Git info', () => { expect( getReleaseCandidateGitInfo({ project: mockCalverProject, @@ -53,7 +53,7 @@ describe('getReleaseCandidateGitInfo', () => { }); describe('semver', () => { - it("should return correct GitHub info when there's previous releases", () => { + it("should return correct Git info when there's previous releases", () => { expect( getReleaseCandidateGitInfo({ project: mockSemverProject, @@ -69,7 +69,7 @@ describe('getReleaseCandidateGitInfo', () => { `); }); - it("should return correct GitHub info when there's no previous release", () => { + it("should return correct Git info when there's no previous release", () => { expect( getReleaseCandidateGitInfo({ project: mockSemverProject, diff --git a/plugins/git-release-manager/src/types/types.ts b/plugins/git-release-manager/src/types/types.ts index ece840fa5f..8e2a034d3a 100644 --- a/plugins/git-release-manager/src/types/types.ts +++ b/plugins/git-release-manager/src/types/types.ts @@ -20,8 +20,8 @@ export type ComponentConfig = { }; interface CreateRcSuccessCbArgs { - gitHubReleaseUrl: string; - gitHubReleaseName: string | null; + gitReleaseUrl: string; + gitReleaseName: string | null; comparisonUrl: string; previousTag?: string; createdTag: string; @@ -29,8 +29,8 @@ interface CreateRcSuccessCbArgs { export type ComponentConfigCreateRc = ComponentConfig; interface PromoteRcSuccessCbArgs { - gitHubReleaseUrl: string; - gitHubReleaseName: string | null; + gitReleaseUrl: string; + gitReleaseName: string | null; previousTagUrl: string; previousTag: string; updatedTagUrl: string; From 0cd313a7a9f0b14a126fe432e6e56a4ecfacdc31 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Thu, 29 Apr 2021 14:11:47 +0200 Subject: [PATCH 086/140] Rename GitReleaseApiClient file Signed-off-by: Erik Engervall --- ...luginApiClient.test.ts => GitReleaseApiClient.test.ts} | 8 ++++---- .../api/{PluginApiClient.ts => GitReleaseApiClient.ts} | 0 plugins/git-release-manager/src/api/serviceApiRef.ts | 2 +- .../CreateReleaseCandidate/CreateReleaseCandidate.tsx | 2 +- .../hooks/useCreateReleaseCandidate.ts | 2 +- plugins/git-release-manager/src/features/Info/Info.tsx | 2 +- plugins/git-release-manager/src/features/Patch/Patch.tsx | 2 +- .../git-release-manager/src/features/Patch/PatchBody.tsx | 2 +- .../src/features/Patch/hooks/usePatch.ts | 2 +- .../src/features/PromoteRc/PromoteRc.tsx | 2 +- .../src/features/PromoteRc/PromoteRcBody.tsx | 2 +- .../src/features/PromoteRc/hooks/usePromoteRc.ts | 2 +- .../src/features/Stats/helpers/getMappedReleases.tsx | 2 +- .../src/features/Stats/helpers/getReleaseStats.tsx | 2 +- .../src/helpers/getReleaseCandidateGitInfo.ts | 2 +- .../git-release-manager/src/hooks/useGetGitBatchInfo.ts | 2 +- plugins/git-release-manager/src/plugin.ts | 2 +- .../git-release-manager/src/test-helpers/test-helpers.ts | 2 +- 18 files changed, 20 insertions(+), 20 deletions(-) rename plugins/git-release-manager/src/api/{PluginApiClient.test.ts => GitReleaseApiClient.test.ts} (89%) rename plugins/git-release-manager/src/api/{PluginApiClient.ts => GitReleaseApiClient.ts} (100%) diff --git a/plugins/git-release-manager/src/api/PluginApiClient.test.ts b/plugins/git-release-manager/src/api/GitReleaseApiClient.test.ts similarity index 89% rename from plugins/git-release-manager/src/api/PluginApiClient.test.ts rename to plugins/git-release-manager/src/api/GitReleaseApiClient.test.ts index efb0a32ee7..4d89e5b2e6 100644 --- a/plugins/git-release-manager/src/api/PluginApiClient.test.ts +++ b/plugins/git-release-manager/src/api/GitReleaseApiClient.test.ts @@ -16,20 +16,20 @@ import { ConfigReader, OAuthApi } from '@backstage/core'; -import { GitReleaseApiClient } from './PluginApiClient'; +import { GitReleaseApiClient } from './GitReleaseApiClient'; -describe('PluginApiClient', () => { +describe('GitReleaseApiClient', () => { it('should return the default plugin api client', () => { const configApi = new ConfigReader({}); const githubAuthApi: OAuthApi = { getAccessToken: jest.fn(), }; - const pluginApiClient = new GitReleaseApiClient({ + const gitReleaseApiClient = new GitReleaseApiClient({ configApi, githubAuthApi, }); - expect(pluginApiClient).toMatchInlineSnapshot(` + expect(gitReleaseApiClient).toMatchInlineSnapshot(` GitReleaseApiClient { "baseUrl": "https://api.github.com", "createRc": Object { diff --git a/plugins/git-release-manager/src/api/PluginApiClient.ts b/plugins/git-release-manager/src/api/GitReleaseApiClient.ts similarity index 100% rename from plugins/git-release-manager/src/api/PluginApiClient.ts rename to plugins/git-release-manager/src/api/GitReleaseApiClient.ts diff --git a/plugins/git-release-manager/src/api/serviceApiRef.ts b/plugins/git-release-manager/src/api/serviceApiRef.ts index b2652aac65..d8c9887fcb 100644 --- a/plugins/git-release-manager/src/api/serviceApiRef.ts +++ b/plugins/git-release-manager/src/api/serviceApiRef.ts @@ -16,7 +16,7 @@ import { createApiRef } from '@backstage/core'; -import { GitReleaseApi } from './PluginApiClient'; +import { GitReleaseApi } from './GitReleaseApiClient'; export const gitReleaseManagerApiRef = createApiRef({ id: 'plugin.git-release-manager.service', diff --git a/plugins/git-release-manager/src/features/CreateReleaseCandidate/CreateReleaseCandidate.tsx b/plugins/git-release-manager/src/features/CreateReleaseCandidate/CreateReleaseCandidate.tsx index 0f19322a17..cdbd79382e 100644 --- a/plugins/git-release-manager/src/features/CreateReleaseCandidate/CreateReleaseCandidate.tsx +++ b/plugins/git-release-manager/src/features/CreateReleaseCandidate/CreateReleaseCandidate.tsx @@ -29,7 +29,7 @@ import { GetBranchResult, GetLatestReleaseResult, GetRepositoryResult, -} from '../../api/PluginApiClient'; +} from '../../api/GitReleaseApiClient'; import { ComponentConfigCreateRc } from '../../types/types'; import { Differ } from '../../components/Differ'; import { getReleaseCandidateGitInfo } from '../../helpers/getReleaseCandidateGitInfo'; diff --git a/plugins/git-release-manager/src/features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate.ts b/plugins/git-release-manager/src/features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate.ts index d922658d2c..236ba7edbf 100644 --- a/plugins/git-release-manager/src/features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate.ts +++ b/plugins/git-release-manager/src/features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate.ts @@ -21,7 +21,7 @@ import { useApi } from '@backstage/core'; import { GetLatestReleaseResult, GetRepositoryResult, -} from '../../../api/PluginApiClient'; +} from '../../../api/GitReleaseApiClient'; import { CardHook, ComponentConfigCreateRc } from '../../../types/types'; import { getReleaseCandidateGitInfo } from '../../../helpers/getReleaseCandidateGitInfo'; import { gitReleaseManagerApiRef } from '../../../api/serviceApiRef'; diff --git a/plugins/git-release-manager/src/features/Info/Info.tsx b/plugins/git-release-manager/src/features/Info/Info.tsx index e6be3b45de..a0cdc0bdd4 100644 --- a/plugins/git-release-manager/src/features/Info/Info.tsx +++ b/plugins/git-release-manager/src/features/Info/Info.tsx @@ -21,7 +21,7 @@ import BarChartIcon from '@material-ui/icons/BarChart'; import { GetBranchResult, GetLatestReleaseResult, -} from '../../api/PluginApiClient'; +} from '../../api/GitReleaseApiClient'; import { Differ } from '../../components/Differ'; import { InfoCardPlus } from '../../components/InfoCardPlus'; import { Stats } from '../Stats/Stats'; diff --git a/plugins/git-release-manager/src/features/Patch/Patch.tsx b/plugins/git-release-manager/src/features/Patch/Patch.tsx index d01a366ffb..7ceb570c58 100644 --- a/plugins/git-release-manager/src/features/Patch/Patch.tsx +++ b/plugins/git-release-manager/src/features/Patch/Patch.tsx @@ -21,7 +21,7 @@ import { Alert, AlertTitle } from '@material-ui/lab'; import { GetBranchResult, GetLatestReleaseResult, -} from '../../api/PluginApiClient'; +} from '../../api/GitReleaseApiClient'; import { ComponentConfigPatch } from '../../types/types'; import { getBumpedTag } from '../../helpers/getBumpedTag'; import { InfoCardPlus } from '../../components/InfoCardPlus'; diff --git a/plugins/git-release-manager/src/features/Patch/PatchBody.tsx b/plugins/git-release-manager/src/features/Patch/PatchBody.tsx index 8ab21e1112..ab2769e2a1 100644 --- a/plugins/git-release-manager/src/features/Patch/PatchBody.tsx +++ b/plugins/git-release-manager/src/features/Patch/PatchBody.tsx @@ -37,7 +37,7 @@ import { useApi } from '@backstage/core'; import { GetBranchResult, GetLatestReleaseResult, -} from '../../api/PluginApiClient'; +} from '../../api/GitReleaseApiClient'; import { CalverTagParts } from '../../helpers/tagParts/getCalverTagParts'; import { CenteredCircularProgress } from '../../components/CenteredCircularProgress'; import { ComponentConfigPatch } from '../../types/types'; diff --git a/plugins/git-release-manager/src/features/Patch/hooks/usePatch.ts b/plugins/git-release-manager/src/features/Patch/hooks/usePatch.ts index dc13e65ea7..4ad29d1699 100644 --- a/plugins/git-release-manager/src/features/Patch/hooks/usePatch.ts +++ b/plugins/git-release-manager/src/features/Patch/hooks/usePatch.ts @@ -21,7 +21,7 @@ import { useApi } from '@backstage/core'; import { GetLatestReleaseResult, GetRecentCommitsResultSingle, -} from '../../../api/PluginApiClient'; +} from '../../../api/GitReleaseApiClient'; import { CalverTagParts } from '../../../helpers/tagParts/getCalverTagParts'; import { ComponentConfigPatch, CardHook } from '../../../types/types'; import { gitReleaseManagerApiRef } from '../../../api/serviceApiRef'; diff --git a/plugins/git-release-manager/src/features/PromoteRc/PromoteRc.tsx b/plugins/git-release-manager/src/features/PromoteRc/PromoteRc.tsx index 16dd8de38c..915330aaf4 100644 --- a/plugins/git-release-manager/src/features/PromoteRc/PromoteRc.tsx +++ b/plugins/git-release-manager/src/features/PromoteRc/PromoteRc.tsx @@ -19,7 +19,7 @@ import { Alert, AlertTitle } from '@material-ui/lab'; import { Typography } from '@material-ui/core'; import { ComponentConfigPromoteRc } from '../../types/types'; -import { GetLatestReleaseResult } from '../../api/PluginApiClient'; +import { GetLatestReleaseResult } from '../../api/GitReleaseApiClient'; import { InfoCardPlus } from '../../components/InfoCardPlus'; import { NoLatestRelease } from '../../components/NoLatestRelease'; import { PromoteRcBody } from './PromoteRcBody'; diff --git a/plugins/git-release-manager/src/features/PromoteRc/PromoteRcBody.tsx b/plugins/git-release-manager/src/features/PromoteRc/PromoteRcBody.tsx index 5dfe4b13aa..6841c21d86 100644 --- a/plugins/git-release-manager/src/features/PromoteRc/PromoteRcBody.tsx +++ b/plugins/git-release-manager/src/features/PromoteRc/PromoteRcBody.tsx @@ -19,7 +19,7 @@ import { Button, Typography } from '@material-ui/core'; import { ComponentConfigPromoteRc } from '../../types/types'; import { Differ } from '../../components/Differ'; -import { GetLatestReleaseResult } from '../../api/PluginApiClient'; +import { GetLatestReleaseResult } from '../../api/GitReleaseApiClient'; import { ResponseStepDialog } from '../../components/ResponseStepDialog/ResponseStepDialog'; import { TEST_IDS } from '../../test-helpers/test-ids'; import { usePromoteRc } from './hooks/usePromoteRc'; diff --git a/plugins/git-release-manager/src/features/PromoteRc/hooks/usePromoteRc.ts b/plugins/git-release-manager/src/features/PromoteRc/hooks/usePromoteRc.ts index ba236b29ce..7a106c3e51 100644 --- a/plugins/git-release-manager/src/features/PromoteRc/hooks/usePromoteRc.ts +++ b/plugins/git-release-manager/src/features/PromoteRc/hooks/usePromoteRc.ts @@ -19,7 +19,7 @@ import { useAsync, useAsyncFn } from 'react-use'; import { useApi } from '@backstage/core'; import { CardHook, ComponentConfigPromoteRc } from '../../../types/types'; -import { GetLatestReleaseResult } from '../../../api/PluginApiClient'; +import { GetLatestReleaseResult } from '../../../api/GitReleaseApiClient'; import { gitReleaseManagerApiRef } from '../../../api/serviceApiRef'; import { useProjectContext } from '../../../contexts/ProjectContext'; import { useResponseSteps } from '../../../hooks/useResponseSteps'; diff --git a/plugins/git-release-manager/src/features/Stats/helpers/getMappedReleases.tsx b/plugins/git-release-manager/src/features/Stats/helpers/getMappedReleases.tsx index 55382100ac..2cbea22664 100644 --- a/plugins/git-release-manager/src/features/Stats/helpers/getMappedReleases.tsx +++ b/plugins/git-release-manager/src/features/Stats/helpers/getMappedReleases.tsx @@ -15,7 +15,7 @@ */ import { calverRegexp } from '../../../helpers/tagParts/getCalverTagParts'; -import { GetAllReleasesResult } from '../../../api/PluginApiClient'; +import { GetAllReleasesResult } from '../../../api/GitReleaseApiClient'; import { Project } from '../../../contexts/ProjectContext'; import { ReleaseStats } from '../contexts/ReleaseStatsContext'; import { semverRegexp } from '../../../helpers/tagParts/getSemverTagParts'; diff --git a/plugins/git-release-manager/src/features/Stats/helpers/getReleaseStats.tsx b/plugins/git-release-manager/src/features/Stats/helpers/getReleaseStats.tsx index 9b77cc1223..41ce41b1ac 100644 --- a/plugins/git-release-manager/src/features/Stats/helpers/getReleaseStats.tsx +++ b/plugins/git-release-manager/src/features/Stats/helpers/getReleaseStats.tsx @@ -15,7 +15,7 @@ */ import { calverRegexp } from '../../../helpers/tagParts/getCalverTagParts'; -import { GetAllTagsResult } from '../../../api/PluginApiClient'; +import { GetAllTagsResult } from '../../../api/GitReleaseApiClient'; import { Project } from '../../../contexts/ProjectContext'; import { ReleaseStats } from '../contexts/ReleaseStatsContext'; import { semverRegexp } from '../../../helpers/tagParts/getSemverTagParts'; diff --git a/plugins/git-release-manager/src/helpers/getReleaseCandidateGitInfo.ts b/plugins/git-release-manager/src/helpers/getReleaseCandidateGitInfo.ts index bef2e63d44..ce4a9c0c80 100644 --- a/plugins/git-release-manager/src/helpers/getReleaseCandidateGitInfo.ts +++ b/plugins/git-release-manager/src/helpers/getReleaseCandidateGitInfo.ts @@ -17,7 +17,7 @@ import { DateTime } from 'luxon'; import { getBumpedSemverTagParts } from './getBumpedTag'; -import { GetLatestReleaseResult } from '../api/PluginApiClient'; +import { GetLatestReleaseResult } from '../api/GitReleaseApiClient'; import { getSemverTagParts } from './tagParts/getSemverTagParts'; import { Project } from '../contexts/ProjectContext'; import { SEMVER_PARTS } from '../constants/constants'; diff --git a/plugins/git-release-manager/src/hooks/useGetGitBatchInfo.ts b/plugins/git-release-manager/src/hooks/useGetGitBatchInfo.ts index 6469b275b5..653f91e3a5 100644 --- a/plugins/git-release-manager/src/hooks/useGetGitBatchInfo.ts +++ b/plugins/git-release-manager/src/hooks/useGetGitBatchInfo.ts @@ -16,7 +16,7 @@ import { useAsync } from 'react-use'; -import { GitReleaseApi } from '../api/PluginApiClient'; +import { GitReleaseApi } from '../api/GitReleaseApiClient'; import { Project } from '../contexts/ProjectContext'; interface GetGitBatchInfo { diff --git a/plugins/git-release-manager/src/plugin.ts b/plugins/git-release-manager/src/plugin.ts index 0760f6a393..de5a945de3 100644 --- a/plugins/git-release-manager/src/plugin.ts +++ b/plugins/git-release-manager/src/plugin.ts @@ -23,7 +23,7 @@ import { } from '@backstage/core'; import { gitReleaseManagerApiRef } from './api/serviceApiRef'; -import { GitReleaseApiClient } from './api/PluginApiClient'; +import { GitReleaseApiClient } from './api/GitReleaseApiClient'; import { rootRouteRef } from './routes'; export { gitReleaseManagerApiRef }; diff --git a/plugins/git-release-manager/src/test-helpers/test-helpers.ts b/plugins/git-release-manager/src/test-helpers/test-helpers.ts index cbf287a5cf..4dbcb9ce10 100644 --- a/plugins/git-release-manager/src/test-helpers/test-helpers.ts +++ b/plugins/git-release-manager/src/test-helpers/test-helpers.ts @@ -19,7 +19,7 @@ import { GetLatestReleaseResult, GetRecentCommitsResultSingle, GitReleaseApi, -} from '../api/PluginApiClient'; +} from '../api/GitReleaseApiClient'; import { CalverTagParts } from '../helpers/tagParts/getCalverTagParts'; import { Project } from '../contexts/ProjectContext'; import { getReleaseCandidateGitInfo } from '../helpers/getReleaseCandidateGitInfo'; From 03c485f8e2856139886427669afc97e871a9190f Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Thu, 29 Apr 2021 14:30:54 +0200 Subject: [PATCH 087/140] Improve GitReleaseApiClient typings Signed-off-by: Erik Engervall --- .../src/GitReleaseManager.tsx | 26 +- .../src/api/GitReleaseApiClient.ts | 776 ++++++++---------- .../src/contexts/UserContext.ts | 38 + .../src/test-helpers/test-helpers.ts | 4 +- .../git-release-manager/src/types/helpers.ts | 25 + 5 files changed, 430 insertions(+), 439 deletions(-) create mode 100644 plugins/git-release-manager/src/contexts/UserContext.ts create mode 100644 plugins/git-release-manager/src/types/helpers.ts diff --git a/plugins/git-release-manager/src/GitReleaseManager.tsx b/plugins/git-release-manager/src/GitReleaseManager.tsx index fa33faac50..9b10d5d16d 100644 --- a/plugins/git-release-manager/src/GitReleaseManager.tsx +++ b/plugins/git-release-manager/src/GitReleaseManager.tsx @@ -25,14 +25,15 @@ import { ComponentConfigPatch, ComponentConfigPromoteRc, } from './types/types'; -import { Features } from './features/Features'; import { CenteredCircularProgress } from './components/CenteredCircularProgress'; +import { Features } from './features/Features'; import { gitReleaseManagerApiRef } from './api/serviceApiRef'; import { InfoCardPlus } from './components/InfoCardPlus'; import { isProjectValid } from './helpers/isProjectValid'; import { ProjectContext, Project } from './contexts/ProjectContext'; import { RepoDetailsForm } from './features/RepoDetailsForm/RepoDetailsForm'; import { useQueryHandler } from './hooks/useQueryHandler'; +import { UserContext } from './contexts/UserContext'; import { useStyles } from './styles/styles'; interface GitReleaseManagerProps { @@ -65,7 +66,7 @@ export function GitReleaseManager(props: GitReleaseManagerProps) { }; const usernameResponse = useAsync(() => - pluginApiClient.getUsername({ owner: project.owner, repo: project.repo }), + pluginApiClient.getUser({ owner: project.owner, repo: project.repo }), ); if (usernameResponse.error) { @@ -80,17 +81,24 @@ export function GitReleaseManager(props: GitReleaseManagerProps) { return Unable to retrieve username; } + const user = { + username: usernameResponse.value.username, + email: usernameResponse.value.email, + }; + return ( -
- + +
+ - - - + + + - {isProjectValid(project) && } -
+ {isProjectValid(project) && } +
+
); } diff --git a/plugins/git-release-manager/src/api/GitReleaseApiClient.ts b/plugins/git-release-manager/src/api/GitReleaseApiClient.ts index 2a0eadcadd..8e15f5c912 100644 --- a/plugins/git-release-manager/src/api/GitReleaseApiClient.ts +++ b/plugins/git-release-manager/src/api/GitReleaseApiClient.ts @@ -22,6 +22,7 @@ import { CalverTagParts } from '../helpers/tagParts/getCalverTagParts'; import { DISABLE_CACHE } from '../constants/constants'; import { Project } from '../contexts/ProjectContext'; import { SemverTagParts } from '../helpers/tagParts/getSemverTagParts'; +import { UnboxArray, UnboxReturnedPromise } from '../types/helpers'; export class GitReleaseApiClient implements GitReleaseApi { private readonly githubAuthApi: OAuthApi; @@ -71,15 +72,15 @@ export class GitReleaseApiClient implements GitReleaseApi { }; } - public getHost() { + public getHost: GitReleaseApi['getHost'] = () => { return this.host; - } + }; - public getRepoPath({ owner, repo }: OwnerRepo) { + public getRepoPath: GitReleaseApi['getRepoPath'] = ({ owner, repo }) => { return `${owner}/${repo}`; - } + }; - async getOwners() { + getOwners: GitReleaseApi['getOwners'] = async () => { const { octokit } = await this.getOctokit(); const orgListResponse = await octokit.paginate( octokit.orgs.listForAuthenticatedUser, @@ -89,9 +90,9 @@ export class GitReleaseApiClient implements GitReleaseApi { return { owners: orgListResponse.map(organization => organization.login), }; - } + }; - async getRepositories({ owner }: { owner: string }) { + getRepositories: GitReleaseApi['getRepositories'] = async ({ owner }) => { const { octokit } = await this.getOctokit(); const repositoryResponse = await octokit @@ -112,24 +113,23 @@ export class GitReleaseApiClient implements GitReleaseApi { return { repositories: repositoryResponse.map(repository => repository.name), }; - } + }; - async getUsername() { + getUser: GitReleaseApi['getUser'] = async () => { const { octokit } = await this.getOctokit(); const userResponse = await octokit.users.getAuthenticated(); return { username: userResponse.data.login, + email: userResponse.data.email ?? undefined, }; - } + }; - async getRecentCommits({ + getRecentCommits: GitReleaseApi['getRecentCommits'] = async ({ owner, repo, releaseBranchName, - }: { - releaseBranchName?: string; - } & OwnerRepo) { + }) => { const { octokit } = await this.getOctokit(); const recentCommitsResponse = await octokit.repos.listCommits({ owner, @@ -150,9 +150,12 @@ export class GitReleaseApiClient implements GitReleaseApi { }, firstParentSha: commit.parents?.[0]?.sha, })); - } + }; - async getLatestRelease({ owner, repo }: OwnerRepo) { + getLatestRelease: GitReleaseApi['getLatestRelease'] = async ({ + owner, + repo, + }) => { const { octokit } = await this.getOctokit(); const { data: latestReleases } = await octokit.repos.listReleases({ owner, @@ -175,9 +178,9 @@ export class GitReleaseApiClient implements GitReleaseApi { htmlUrl: latestRelease.html_url, body: latestRelease.body, }; - } + }; - async getRepository({ owner, repo }: OwnerRepo) { + getRepository: GitReleaseApi['getRepository'] = async ({ owner, repo }) => { const { octokit } = await this.getOctokit(); const { data: repository } = await octokit.repos.get({ owner, @@ -190,15 +193,13 @@ export class GitReleaseApiClient implements GitReleaseApi { defaultBranch: repository.default_branch, name: repository.name, }; - } + }; - async getLatestCommit({ + getLatestCommit: GitReleaseApi['getLatestCommit'] = async ({ owner, repo, defaultBranch, - }: { - defaultBranch: GetRepositoryResult['defaultBranch']; - } & OwnerRepo) { + }) => { const { octokit } = await this.getOctokit(); const { data: latestCommit } = await octokit.repos.getCommit({ owner, @@ -214,15 +215,13 @@ export class GitReleaseApiClient implements GitReleaseApi { message: latestCommit.commit.message, }, }; - } + }; - async getBranch({ + getBranch: GitReleaseApi['getBranch'] = async ({ owner, repo, branchName, - }: { - branchName: string; - } & OwnerRepo) { + }) => { const { octokit } = await this.getOctokit(); const { data: branch } = await octokit.repos.getBranch({ @@ -246,18 +245,10 @@ export class GitReleaseApiClient implements GitReleaseApi { }, }, }; - } + }; - createRc = { - createRef: async ({ - owner, - repo, - mostRecentSha, - targetBranch, - }: { - mostRecentSha: string; - targetBranch: string; - } & OwnerRepo) => { + createRc: GitReleaseApi['createRc'] = { + createRef: async ({ owner, repo, mostRecentSha, targetBranch }) => { const { octokit } = await this.getOctokit(); const createRefResponse = await octokit.git.createRef({ owner, @@ -276,10 +267,7 @@ export class GitReleaseApiClient implements GitReleaseApi { repo, previousReleaseBranch, nextReleaseBranch, - }: { - previousReleaseBranch: string; - nextReleaseBranch: string; - } & OwnerRepo) => { + }) => { const { octokit } = await this.getOctokit(); const compareCommitsResponse = await octokit.repos.compareCommits({ owner, @@ -301,12 +289,7 @@ export class GitReleaseApiClient implements GitReleaseApi { releaseName, rcBranch, releaseBody, - }: { - rcReleaseTag: string; - releaseName: string; - rcBranch: string; - releaseBody: string; - } & OwnerRepo) => { + }) => { const { octokit } = await this.getOctokit(); const createReleaseResponse = await octokit.repos.createRelease({ owner, @@ -326,18 +309,14 @@ export class GitReleaseApiClient implements GitReleaseApi { }, }; - patch = { + patch: GitReleaseApi['patch'] = { createTempCommit: async ({ owner, repo, tagParts, releaseBranchTree, selectedPatchCommit, - }: { - tagParts: SemverTagParts | CalverTagParts; - releaseBranchTree: string; - selectedPatchCommit: GetRecentCommitsResultSingle; - } & OwnerRepo) => { + }) => { const { octokit } = await this.getOctokit(); const { data: tempCommit } = await octokit.git.createCommit({ owner, @@ -358,12 +337,8 @@ export class GitReleaseApiClient implements GitReleaseApi { repo, releaseBranchName, tempCommit, - }: { - releaseBranchName: string; - tempCommit: CreateTempCommitResult; - } & OwnerRepo) => { + }) => { const { octokit } = await this.getOctokit(); - // await octokit.request("PATCH reposrefs") await octokit.git.updateRef({ owner, repo, @@ -373,12 +348,7 @@ export class GitReleaseApiClient implements GitReleaseApi { }); }, - merge: async ({ - owner, - repo, - base, - head, - }: { base: string; head: string } & OwnerRepo) => { + merge: async ({ owner, repo, base, head }) => { const { octokit } = await this.getOctokit(); const { data: merge } = await octokit.repos.merge({ owner, @@ -405,12 +375,7 @@ export class GitReleaseApiClient implements GitReleaseApi { selectedPatchCommit, mergeTree, releaseBranchSha, - }: { - bumpedTag: string; - selectedPatchCommit: GetRecentCommitsResultSingle; - mergeTree: string; - releaseBranchSha: string; - } & OwnerRepo) => { + }) => { const { octokit } = await this.getOctokit(); const { data: cherryPickCommit } = await octokit.git.createCommit({ owner, @@ -433,10 +398,7 @@ ${selectedPatchCommit.sha}`, repo, releaseBranchName, cherryPickCommit, - }: { - releaseBranchName: string; - cherryPickCommit: CreateCherryPickCommitResult; - } & OwnerRepo) => { + }) => { const { octokit } = await this.getOctokit(); const { data: updatedReference } = await octokit.git.updateRef({ owner, @@ -454,15 +416,7 @@ ${selectedPatchCommit.sha}`, }; }, - createTagObject: async ({ - owner, - repo, - bumpedTag, - updatedReference, - }: { - bumpedTag: string; - updatedReference: ReplaceTempCommitResult; - } & OwnerRepo) => { + createTagObject: async ({ owner, repo, bumpedTag, updatedReference }) => { const { octokit } = await this.getOctokit(); const { data: createdTagObject } = await octokit.git.createTag({ owner, @@ -480,15 +434,7 @@ ${selectedPatchCommit.sha}`, }; }, - createReference: async ({ - owner, - repo, - bumpedTag, - createdTagObject, - }: { - bumpedTag: string; - createdTagObject: CreateTagObjectResult; - } & OwnerRepo) => { + createReference: async ({ owner, repo, bumpedTag, createdTagObject }) => { const { octokit } = await this.getOctokit(); const { data: reference } = await octokit.git.createRef({ owner, @@ -509,12 +455,7 @@ ${selectedPatchCommit.sha}`, latestRelease, tagParts, selectedPatchCommit, - }: { - bumpedTag: string; - latestRelease: NonNullable; - tagParts: SemverTagParts | CalverTagParts; - selectedPatchCommit: GetRecentCommitsResultSingle; - } & OwnerRepo) => { + }) => { const { octokit } = await this.getOctokit(); const { data: updatedRelease } = await octokit.repos.updateRelease({ owner, @@ -536,16 +477,8 @@ ${selectedPatchCommit.commit.message}`, }, }; - promoteRc = { - promoteRelease: async ({ - owner, - repo, - releaseId, - releaseVersion, - }: { - releaseId: NonNullable['id']; - releaseVersion: string; - } & OwnerRepo) => { + promoteRc: GitReleaseApi['promoteRc'] = { + promoteRelease: async ({ owner, repo, releaseId, releaseVersion }) => { const { octokit } = await this.getOctokit(); const { data: promotedRelease } = await octokit.repos.updateRelease({ owner, @@ -563,8 +496,8 @@ ${selectedPatchCommit.commit.message}`, }, }; - stats = { - getAllTags: async ({ owner, repo }: OwnerRepo) => { + stats: GitReleaseApi['stats'] = { + getAllTags: async ({ owner, repo }) => { const { octokit } = await this.getOctokit(); const tags = await octokit.paginate(octokit.repos.listTags, { @@ -580,7 +513,7 @@ ${selectedPatchCommit.commit.message}`, })); }, - getAllReleases: async ({ owner, repo }: OwnerRepo) => { + getAllReleases: async ({ owner, repo }) => { const { octokit } = await this.getOctokit(); const releases = await octokit.paginate(octokit.repos.listReleases, { @@ -599,7 +532,7 @@ ${selectedPatchCommit.commit.message}`, })); }, - getCommit: async ({ owner, repo, ref }: { ref: string } & OwnerRepo) => { + getCommit: async ({ owner, repo, ref }) => { const { octokit } = await this.getOctokit(); const { data: commit } = await octokit.repos.getCommit({ @@ -615,347 +548,332 @@ ${selectedPatchCommit.commit.message}`, }; } -type UnboxPromise> = T extends Promise - ? U - : never; - -type UnboxReturnedPromise< - T extends (...args: any) => Promise -> = UnboxPromise>; - -type UnboxArray = T extends (infer U)[] ? U : T; - type OwnerRepo = { owner: Project['owner']; repo: Project['repo']; }; -type GetHost = () => string; +export interface GitReleaseApi { + getHost: () => string; -type GetRepoPath = (args: OwnerRepo) => string; + getRepoPath: (args: OwnerRepo) => string; -type GetOwners = () => Promise<{ - owners: string[]; -}>; -export type GetOwnersResult = UnboxReturnedPromise; + getOwners: () => Promise<{ + owners: string[]; + }>; -type GetRepositories = (args: { - owner: OwnerRepo['owner']; -}) => Promise<{ - repositories: string[]; -}>; -export type GetRepositoriesResult = UnboxReturnedPromise; + getRepositories: (args: { + owner: OwnerRepo['owner']; + }) => Promise<{ + repositories: string[]; + }>; -type GetUsername = ( - args: OwnerRepo, -) => Promise<{ - username: string; -}>; -export type GetUsernameResult = UnboxReturnedPromise; + getUser: ( + args: OwnerRepo, + ) => Promise<{ + username: string; + email?: string; + }>; -type GetRecentCommits = ( - args: { - releaseBranchName?: string; - } & OwnerRepo, -) => Promise< - { + getRecentCommits: ( + args: { + releaseBranchName?: string; + } & OwnerRepo, + ) => Promise< + { + htmlUrl: string; + sha: string; + author: { + htmlUrl?: string; + login?: string; + }; + commit: { + message: string; + }; + firstParentSha?: string; + }[] + >; + getLatestRelease: ( + args: OwnerRepo, + ) => Promise<{ + targetCommitish: string; + tagName: string; + prerelease: boolean; + id: number; htmlUrl: string; + body?: string | null; + } | null>; + getRepository: ( + args: OwnerRepo, + ) => Promise<{ + pushPermissions: boolean | undefined; + defaultBranch: string; + name: string; + }>; + + getLatestCommit: ( + args: { + defaultBranch: string; + } & OwnerRepo, + ) => Promise<{ sha: string; - author: { - htmlUrl?: string; - login?: string; - }; + htmlUrl: string; commit: { message: string; }; - firstParentSha?: string; - }[] ->; -export type GetRecentCommitsResult = UnboxReturnedPromise; -export type GetRecentCommitsResultSingle = UnboxArray; + }>; -type GetLatestRelease = ( - args: OwnerRepo, -) => Promise<{ - targetCommitish: string; - tagName: string; - prerelease: boolean; - id: number; - htmlUrl: string; - body?: string | null; -} | null>; -export type GetLatestReleaseResult = UnboxReturnedPromise; - -type GetRepository = ( - args: OwnerRepo, -) => Promise<{ - pushPermissions: boolean | undefined; - defaultBranch: string; - name: string; -}>; -export type GetRepositoryResult = UnboxReturnedPromise; - -type GetLatestCommit = ( - args: { - defaultBranch: string; - } & OwnerRepo, -) => Promise<{ - sha: string; - htmlUrl: string; - commit: { - message: string; - }; -}>; -export type GetLatestCommitResult = UnboxReturnedPromise; - -type GetBranch = ( - args: { - branchName: string; - } & OwnerRepo, -) => Promise<{ - name: string; - links: { - html: string; - }; - commit: { - sha: string; + getBranch: ( + args: { + branchName: string; + } & OwnerRepo, + ) => Promise<{ + name: string; + links: { + html: string; + }; commit: { - tree: { - sha: string; + sha: string; + commit: { + tree: { + sha: string; + }; }; }; - }; -}>; -export type GetBranchResult = UnboxReturnedPromise; + }>; -/** - * CreateRc - */ -type CreateRef = ( - args: { - mostRecentSha: string; - targetBranch: string; - } & OwnerRepo, -) => Promise<{ - ref: string; -}>; -export type CreateRefResult = UnboxReturnedPromise; - -type GetComparison = ( - args: { - previousReleaseBranch: string; - nextReleaseBranch: string; - } & OwnerRepo, -) => Promise<{ - htmlUrl: string; - aheadBy: number; -}>; -export type GetComparisonResult = UnboxReturnedPromise; - -type CreateRelease = ( - args: { - rcReleaseTag: string; - releaseName: string; - rcBranch: string; - releaseBody: string; - } & OwnerRepo, -) => Promise<{ - name: string | null; - htmlUrl: string; - tagName: string; -}>; -export type CreateReleaseResult = UnboxReturnedPromise; - -/** - * Patch - */ -type CreateTempCommit = ( - args: { - tagParts: SemverTagParts | CalverTagParts; - releaseBranchTree: string; - selectedPatchCommit: UnboxArray< - UnboxReturnedPromise - >; - } & OwnerRepo, -) => Promise<{ - message: string; - sha: string; -}>; -export type CreateTempCommitResult = UnboxReturnedPromise; - -type ForceBranchHeadToTempCommit = ( - args: { - releaseBranchName: string; - tempCommit: CreateTempCommitResult; - } & OwnerRepo, -) => Promise; -export type ForceBranchHeadToTempCommitResult = UnboxReturnedPromise; - -type Merge = ({ - base, - head, -}: { - base: string; - head: string; -} & OwnerRepo) => Promise<{ - htmlUrl: string; - commit: { - message: string; - tree: { - sha: string; - }; - }; -}>; -export type MergeResult = UnboxReturnedPromise; - -type CreateCherryPickCommit = ( - args: { - bumpedTag: string; - selectedPatchCommit: UnboxArray< - UnboxReturnedPromise - >; - mergeTree: string; - releaseBranchSha: string; - } & OwnerRepo, -) => Promise<{ - message: string; - sha: string; -}>; -export type CreateCherryPickCommitResult = UnboxReturnedPromise; - -type ReplaceTempCommit = ( - args: { - releaseBranchName: string; - cherryPickCommit: UnboxReturnedPromise< - GitReleaseApi['patch']['createCherryPickCommit'] - >; - } & OwnerRepo, -) => Promise<{ - ref: string; - object: { - sha: string; - }; -}>; -export type ReplaceTempCommitResult = UnboxReturnedPromise; - -type CreateTagObject = ({ - bumpedTag, - updatedReference, -}: { - bumpedTag: string; - updatedReference: ReplaceTempCommitResult; -} & OwnerRepo) => Promise<{ - tag: string; - sha: string; -}>; -export type CreateTagObjectResult = UnboxReturnedPromise; - -type CreateReference = ( - args: { - bumpedTag: string; - createdTagObject: CreateTagObjectResult; - } & OwnerRepo, -) => Promise<{ - ref: string; -}>; -export type CreateReferenceResult = UnboxReturnedPromise; - -type UpdateRelease = ( - args: { - bumpedTag: string; - latestRelease: NonNullable; - tagParts: SemverTagParts | CalverTagParts; - selectedPatchCommit: GetRecentCommitsResultSingle; - } & OwnerRepo, -) => Promise<{ - name: string | null; - tagName: string; - htmlUrl: string; -}>; -export type UpdateReleaseResult = UnboxReturnedPromise; - -/** - * PromoteRc - */ -type PromoteRelease = ( - args: { - releaseId: NonNullable['id']; - releaseVersion: string; - } & OwnerRepo, -) => Promise<{ - name: string | null; - tagName: string; - htmlUrl: string; -}>; -export type PromoteReleaseResult = UnboxReturnedPromise; - -/** - * Stats - */ -type GetAllTags = ( - args: OwnerRepo, -) => Promise< - Array<{ - tagName: string; - sha: 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 GetCommit = ( - args: { - ref: string; - } & OwnerRepo, -) => Promise<{ - createdAt: string | undefined; -}>; -export type GetCommitResult = UnboxReturnedPromise; - -export interface GitReleaseApi { - getHost: GetHost; - getRepoPath: GetRepoPath; - getOwners: GetOwners; - getRepositories: GetRepositories; - getUsername: GetUsername; - getRecentCommits: GetRecentCommits; - getLatestRelease: GetLatestRelease; - getRepository: GetRepository; - getLatestCommit: GetLatestCommit; - getBranch: GetBranch; createRc: { - createRef: CreateRef; - getComparison: GetComparison; - createRelease: CreateRelease; + createRef: ( + args: { + mostRecentSha: string; + targetBranch: string; + } & OwnerRepo, + ) => Promise<{ + ref: string; + }>; + + getComparison: ( + args: { + previousReleaseBranch: string; + nextReleaseBranch: string; + } & OwnerRepo, + ) => Promise<{ + htmlUrl: string; + aheadBy: number; + }>; + + createRelease: ( + args: { + rcReleaseTag: string; + releaseName: string; + rcBranch: string; + releaseBody: string; + } & OwnerRepo, + ) => Promise<{ + name: string | null; + htmlUrl: string; + tagName: string; + }>; }; patch: { - createTempCommit: CreateTempCommit; - forceBranchHeadToTempCommit: ForceBranchHeadToTempCommit; - merge: Merge; - createCherryPickCommit: CreateCherryPickCommit; - replaceTempCommit: ReplaceTempCommit; - createTagObject: CreateTagObject; - createReference: CreateReference; - updateRelease: UpdateRelease; + createTempCommit: ( + args: { + tagParts: SemverTagParts | CalverTagParts; + releaseBranchTree: string; + selectedPatchCommit: UnboxArray< + UnboxReturnedPromise + >; + } & OwnerRepo, + ) => Promise<{ + message: string; + sha: string; + }>; + + forceBranchHeadToTempCommit: ( + args: { + releaseBranchName: string; + tempCommit: CreateTempCommitResult; + } & OwnerRepo, + ) => Promise; + merge: ({ + base, + head, + }: { + base: string; + head: string; + } & OwnerRepo) => Promise<{ + htmlUrl: string; + commit: { + message: string; + tree: { + sha: string; + }; + }; + }>; + + createCherryPickCommit: ( + args: { + bumpedTag: string; + selectedPatchCommit: UnboxArray< + UnboxReturnedPromise + >; + mergeTree: string; + releaseBranchSha: string; + } & OwnerRepo, + ) => Promise<{ + message: string; + sha: string; + }>; + + replaceTempCommit: ( + args: { + releaseBranchName: string; + cherryPickCommit: UnboxReturnedPromise< + GitReleaseApi['patch']['createCherryPickCommit'] + >; + } & OwnerRepo, + ) => Promise<{ + ref: string; + object: { + sha: string; + }; + }>; + + createTagObject: ({ + bumpedTag, + updatedReference, + }: { + bumpedTag: string; + updatedReference: ReplaceTempCommitResult; + } & OwnerRepo) => Promise<{ + tag: string; + sha: string; + }>; + + createReference: ( + args: { + bumpedTag: string; + createdTagObject: CreateTagObjectResult; + } & OwnerRepo, + ) => Promise<{ + ref: string; + }>; + + updateRelease: ( + args: { + bumpedTag: string; + latestRelease: NonNullable; + tagParts: SemverTagParts | CalverTagParts; + selectedPatchCommit: GetRecentCommitsResultSingle; + } & OwnerRepo, + ) => Promise<{ + name: string | null; + tagName: string; + htmlUrl: string; + }>; }; promoteRc: { - promoteRelease: PromoteRelease; + promoteRelease: ( + args: { + releaseId: NonNullable['id']; + releaseVersion: string; + } & OwnerRepo, + ) => Promise<{ + name: string | null; + tagName: string; + htmlUrl: string; + }>; }; stats: { - getAllTags: GetAllTags; - getAllReleases: GetAllReleases; - getCommit: GetCommit; + getAllTags: ( + args: OwnerRepo, + ) => Promise< + Array<{ + tagName: string; + sha: string; + }> + >; + getAllReleases: ( + args: OwnerRepo, + ) => Promise< + Array<{ + id: number; + name: string | null; + tagName: string; + createdAt: string | null; + htmlUrl: string; + }> + >; + + getCommit: ( + args: { + ref: string; + } & OwnerRepo, + ) => Promise<{ + createdAt: string | undefined; + }>; }; } + +export type GetOwnersResult = UnboxReturnedPromise; +export type GetRepositoriesResult = UnboxReturnedPromise< + GitReleaseApi['getRepositories'] +>; +export type GetUserResult = UnboxReturnedPromise; +export type GetRecentCommitsResult = UnboxReturnedPromise< + GitReleaseApi['getRecentCommits'] +>; +export type GetRecentCommitsResultSingle = UnboxArray; +export type GetLatestReleaseResult = UnboxReturnedPromise< + GitReleaseApi['getLatestRelease'] +>; +export type GetRepositoryResult = UnboxReturnedPromise< + GitReleaseApi['getRepository'] +>; +export type GetLatestCommitResult = UnboxReturnedPromise< + GitReleaseApi['getLatestCommit'] +>; +export type GetBranchResult = UnboxReturnedPromise; +export type CreateRefResult = UnboxReturnedPromise< + GitReleaseApi['createRc']['createRef'] +>; +export type GetComparisonResult = UnboxReturnedPromise< + GitReleaseApi['createRc']['getComparison'] +>; +export type CreateReleaseResult = UnboxReturnedPromise< + GitReleaseApi['createRc']['createRelease'] +>; +export type CreateTempCommitResult = UnboxReturnedPromise< + GitReleaseApi['patch']['createTempCommit'] +>; +export type ForceBranchHeadToTempCommitResult = UnboxReturnedPromise< + GitReleaseApi['patch']['forceBranchHeadToTempCommit'] +>; +export type MergeResult = UnboxReturnedPromise; +export type CreateCherryPickCommitResult = UnboxReturnedPromise< + GitReleaseApi['patch']['createCherryPickCommit'] +>; +export type ReplaceTempCommitResult = UnboxReturnedPromise< + GitReleaseApi['patch']['replaceTempCommit'] +>; +export type CreateTagObjectResult = UnboxReturnedPromise< + GitReleaseApi['patch']['createTagObject'] +>; +export type CreateReferenceResult = UnboxReturnedPromise< + GitReleaseApi['patch']['createReference'] +>; +export type UpdateReleaseResult = UnboxReturnedPromise< + GitReleaseApi['patch']['updateRelease'] +>; +export type PromoteReleaseResult = UnboxReturnedPromise< + GitReleaseApi['promoteRc']['promoteRelease'] +>; +export type GetAllTagsResult = UnboxReturnedPromise< + GitReleaseApi['stats']['getAllTags'] +>; +export type GetCommitResult = UnboxReturnedPromise< + GitReleaseApi['stats']['getCommit'] +>; +export type GetAllReleasesResult = UnboxReturnedPromise< + GitReleaseApi['stats']['getAllReleases'] +>; diff --git a/plugins/git-release-manager/src/contexts/UserContext.ts b/plugins/git-release-manager/src/contexts/UserContext.ts new file mode 100644 index 0000000000..664c621d3c --- /dev/null +++ b/plugins/git-release-manager/src/contexts/UserContext.ts @@ -0,0 +1,38 @@ +/* + * 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 { GitReleaseManagerError } from '../errors/GitReleaseManagerError'; + +interface User { + username: string; + email?: string; +} + +export const UserContext = createContext<{ user: User } | undefined>(undefined); + +export const useUserContext = () => { + const { user } = useContext(UserContext) ?? {}; + + if (!user) { + throw new GitReleaseManagerError('user not found'); + } + + return { + user, + }; +}; diff --git a/plugins/git-release-manager/src/test-helpers/test-helpers.ts b/plugins/git-release-manager/src/test-helpers/test-helpers.ts index 4dbcb9ce10..886fad9de0 100644 --- a/plugins/git-release-manager/src/test-helpers/test-helpers.ts +++ b/plugins/git-release-manager/src/test-helpers/test-helpers.ts @@ -24,6 +24,7 @@ import { CalverTagParts } from '../helpers/tagParts/getCalverTagParts'; import { Project } from '../contexts/ProjectContext'; import { getReleaseCandidateGitInfo } from '../helpers/getReleaseCandidateGitInfo'; +const mockEmail = 'mock_email'; const mockOwner = 'mock_owner'; const mockRepo = 'mock_repo'; @@ -185,8 +186,9 @@ export const mockApiClient: GitReleaseApi = { repositories: [mockRepo, `${mockRepo}2`], })), - getUsername: jest.fn(async () => ({ + getUser: jest.fn(async () => ({ username: mockOwner, + email: mockEmail, })), getRecentCommits: jest.fn(async () => [ diff --git a/plugins/git-release-manager/src/types/helpers.ts b/plugins/git-release-manager/src/types/helpers.ts new file mode 100644 index 0000000000..55bc6fdcac --- /dev/null +++ b/plugins/git-release-manager/src/types/helpers.ts @@ -0,0 +1,25 @@ +/* + * 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. + */ + +export type UnboxPromise> = T extends Promise + ? U + : never; + +export type UnboxReturnedPromise< + T extends (...args: any) => Promise +> = UnboxPromise>; + +export type UnboxArray = T extends (infer U)[] ? U : T; From cfe5a53da41f03c2509ee37c2435fee733b80c45 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Thu, 29 Apr 2021 15:01:28 +0200 Subject: [PATCH 088/140] Add getSingleTag API method, start simplifying the API client Signed-off-by: Erik Engervall --- .../src/api/GitReleaseApiClient.test.ts | 11 +++ .../src/api/GitReleaseApiClient.ts | 74 ++++++++++++------- .../hooks/useCreateReleaseCandidate.ts | 4 +- .../src/features/Patch/hooks/usePatch.ts | 4 +- .../Stats/Info/hooks/useGetReleaseTimes.tsx | 17 ++--- .../Stats/helpers/getMappedReleases.test.tsx | 14 +--- .../Stats/helpers/getMappedReleases.tsx | 10 +-- .../Stats/helpers/getReleaseStats.test.tsx | 19 +---- .../Stats/helpers/getReleaseStats.tsx | 13 +--- .../src/features/Stats/helpers/getTagDate.ts | 40 ++++++++++ .../src/features/Stats/hooks/useGetTagDate.ts | 46 ++++++++++++ .../src/test-helpers/test-helpers.ts | 6 ++ 12 files changed, 167 insertions(+), 91 deletions(-) create mode 100644 plugins/git-release-manager/src/features/Stats/helpers/getTagDate.ts create mode 100644 plugins/git-release-manager/src/features/Stats/hooks/useGetTagDate.ts diff --git a/plugins/git-release-manager/src/api/GitReleaseApiClient.test.ts b/plugins/git-release-manager/src/api/GitReleaseApiClient.test.ts index 4d89e5b2e6..b37500451f 100644 --- a/plugins/git-release-manager/src/api/GitReleaseApiClient.test.ts +++ b/plugins/git-release-manager/src/api/GitReleaseApiClient.test.ts @@ -37,6 +37,16 @@ describe('GitReleaseApiClient', () => { "createRelease": [Function], "getComparison": [Function], }, + "getBranch": [Function], + "getHost": [Function], + "getLatestCommit": [Function], + "getLatestRelease": [Function], + "getOwners": [Function], + "getRecentCommits": [Function], + "getRepoPath": [Function], + "getRepositories": [Function], + "getRepository": [Function], + "getUser": [Function], "githubAuthApi": Object { "getAccessToken": [MockFunction], }, @@ -58,6 +68,7 @@ describe('GitReleaseApiClient', () => { "getAllReleases": [Function], "getAllTags": [Function], "getCommit": [Function], + "getSingleTag": [Function], }, } `); diff --git a/plugins/git-release-manager/src/api/GitReleaseApiClient.ts b/plugins/git-release-manager/src/api/GitReleaseApiClient.ts index 8e15f5c912..22762ac812 100644 --- a/plugins/git-release-manager/src/api/GitReleaseApiClient.ts +++ b/plugins/git-release-manager/src/api/GitReleaseApiClient.ts @@ -262,18 +262,13 @@ export class GitReleaseApiClient implements GitReleaseApi { }; }, - getComparison: async ({ - owner, - repo, - previousReleaseBranch, - nextReleaseBranch, - }) => { + getComparison: async ({ owner, repo, base, head }) => { const { octokit } = await this.getOctokit(); const compareCommitsResponse = await octokit.repos.compareCommits({ owner, repo, - base: previousReleaseBranch, - head: nextReleaseBranch, + base, + head, }); return { @@ -416,15 +411,15 @@ ${selectedPatchCommit.sha}`, }; }, - createTagObject: async ({ owner, repo, bumpedTag, updatedReference }) => { + createTagObject: async ({ owner, repo, tag, objectSha }) => { const { octokit } = await this.getOctokit(); const { data: createdTagObject } = await octokit.git.createTag({ owner, repo, message: 'Tag generated by your friendly neighborhood Backstage Release Manager', - tag: bumpedTag, - object: updatedReference.object.sha, + tag, + object: objectSha, type: 'commit', }); @@ -545,6 +540,21 @@ ${selectedPatchCommit.commit.message}`, createdAt: commit.commit.committer?.date, }; }, + + getSingleTag: async ({ owner, repo, tagSha }) => { + const { octokit } = await this.getOctokit(); + const singleTag = await octokit.git.getTag({ + owner, + repo, + tag_sha: tagSha, + }); + + return { + date: singleTag.data.tagger.date, + username: singleTag.data.tagger.name, + userEmail: singleTag.data.tagger.email, + }; + }, }; } @@ -654,8 +664,8 @@ export interface GitReleaseApi { getComparison: ( args: { - previousReleaseBranch: string; - nextReleaseBranch: string; + base: string; + head: string; } & OwnerRepo, ) => Promise<{ htmlUrl: string; @@ -695,13 +705,13 @@ export interface GitReleaseApi { tempCommit: CreateTempCommitResult; } & OwnerRepo, ) => Promise; - merge: ({ - base, - head, - }: { - base: string; - head: string; - } & OwnerRepo) => Promise<{ + + merge: ( + args: { + base: string; + head: string; + } & OwnerRepo, + ) => Promise<{ htmlUrl: string; commit: { message: string; @@ -739,13 +749,12 @@ export interface GitReleaseApi { }; }>; - createTagObject: ({ - bumpedTag, - updatedReference, - }: { - bumpedTag: string; - updatedReference: ReplaceTempCommitResult; - } & OwnerRepo) => Promise<{ + createTagObject: ( + args: { + tag: string; + objectSha: string; + } & OwnerRepo, + ) => Promise<{ tag: string; sha: string; }>; @@ -793,6 +802,7 @@ export interface GitReleaseApi { sha: string; }> >; + getAllReleases: ( args: OwnerRepo, ) => Promise< @@ -812,6 +822,16 @@ export interface GitReleaseApi { ) => Promise<{ createdAt: string | undefined; }>; + + getSingleTag: ( + args: { + tagSha: string; + } & OwnerRepo, + ) => Promise<{ + date: string; + username: string; + userEmail: string; + }>; }; } diff --git a/plugins/git-release-manager/src/features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate.ts b/plugins/git-release-manager/src/features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate.ts index 236ba7edbf..ca203a1d10 100644 --- a/plugins/git-release-manager/src/features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate.ts +++ b/plugins/git-release-manager/src/features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate.ts @@ -135,8 +135,8 @@ export function useCreateReleaseCandidate({ .getComparison({ owner: project.owner, repo: project.repo, - previousReleaseBranch, - nextReleaseBranch, + base: previousReleaseBranch, + head: nextReleaseBranch, }) .catch(asyncCatcher); diff --git a/plugins/git-release-manager/src/features/Patch/hooks/usePatch.ts b/plugins/git-release-manager/src/features/Patch/hooks/usePatch.ts index 4ad29d1699..d188e2bf51 100644 --- a/plugins/git-release-manager/src/features/Patch/hooks/usePatch.ts +++ b/plugins/git-release-manager/src/features/Patch/hooks/usePatch.ts @@ -238,8 +238,8 @@ export function usePatch({ .createTagObject({ owner: project.owner, repo: project.repo, - bumpedTag, - updatedReference: updatedRefRes.value, + tag: bumpedTag, + objectSha: updatedRefRes.value.object.sha, }) .catch(asyncCatcher); diff --git a/plugins/git-release-manager/src/features/Stats/Info/hooks/useGetReleaseTimes.tsx b/plugins/git-release-manager/src/features/Stats/Info/hooks/useGetReleaseTimes.tsx index 37f1d88a88..59ae036405 100644 --- a/plugins/git-release-manager/src/features/Stats/Info/hooks/useGetReleaseTimes.tsx +++ b/plugins/git-release-manager/src/features/Stats/Info/hooks/useGetReleaseTimes.tsx @@ -20,6 +20,7 @@ import { DateTime } from 'luxon'; import { useApi } from '@backstage/core'; import { getReleaseCommitPairs } from '../helpers/getReleaseCommitPairs'; +import { getTagDate } from '../../helpers/getTagDate'; import { gitReleaseManagerApiRef } from '../../../../api/serviceApiRef'; import { useProjectContext } from '../../../../contexts/ProjectContext'; import { useReleaseStatsContext } from '../../contexts/ReleaseStatsContext'; @@ -80,19 +81,11 @@ export function useGetReleaseTimes() { const { baseVersion, startCommit, endCommit } = releaseCommitPairs[index]; const [ - { createdAt: startCommitCreatedAt }, - { createdAt: endCommitCreatedAt }, + { tagDate: startCommitCreatedAt }, + { tagDate: 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, - }), + getTagDate({ pluginApiClient, project, tagSha: startCommit.sha }), + getTagDate({ pluginApiClient, project, tagSha: endCommit.sha }), ]); const releaseTime: ReleaseTime = { diff --git a/plugins/git-release-manager/src/features/Stats/helpers/getMappedReleases.test.tsx b/plugins/git-release-manager/src/features/Stats/helpers/getMappedReleases.test.tsx index 5bea396d5c..fd5b23beaf 100644 --- a/plugins/git-release-manager/src/features/Stats/helpers/getMappedReleases.test.tsx +++ b/plugins/git-release-manager/src/features/Stats/helpers/getMappedReleases.test.tsx @@ -38,24 +38,14 @@ describe('getMappedReleases', () => { "releases": Object { "1.0": Object { "baseVersion": "1.0", - "candidates": Array [ - Object { - "sha": "", - "tagName": "rc-1.0.0", - }, - ], + "candidates": Array [], "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", - }, - ], + "candidates": Array [], "createdAt": "2021-01-01T10:11:12Z", "htmlUrl": "html_url", "versions": Array [], diff --git a/plugins/git-release-manager/src/features/Stats/helpers/getMappedReleases.tsx b/plugins/git-release-manager/src/features/Stats/helpers/getMappedReleases.tsx index 2cbea22664..cee64436a1 100644 --- a/plugins/git-release-manager/src/features/Stats/helpers/getMappedReleases.tsx +++ b/plugins/git-release-manager/src/features/Stats/helpers/getMappedReleases.tsx @@ -40,24 +40,18 @@ export function getMappedReleases({ return acc; } - const prefix = match[1] as 'rc' | 'version'; const baseVersion = project.versioningStrategy === 'semver' ? `${match[2]}.${match[3]}` : match[2]; if (!acc.releases[baseVersion]) { - const releaseEntry = { - tagName: release.tagName, - sha: '', - }; - acc.releases[baseVersion] = { baseVersion, createdAt: release.createdAt, htmlUrl: release.htmlUrl, - candidates: prefix === 'rc' ? [releaseEntry] : [], - versions: prefix === 'version' ? [releaseEntry] : [], + candidates: [], + versions: [], }; return acc; diff --git a/plugins/git-release-manager/src/features/Stats/helpers/getReleaseStats.test.tsx b/plugins/git-release-manager/src/features/Stats/helpers/getReleaseStats.test.tsx index 3b59cc4c46..6d0cb1ca3d 100644 --- a/plugins/git-release-manager/src/features/Stats/helpers/getReleaseStats.test.tsx +++ b/plugins/git-release-manager/src/features/Stats/helpers/getReleaseStats.test.tsx @@ -27,24 +27,15 @@ describe('getReleaseStats', () => { baseVersion: '1.0', createdAt: '2021-01-01T10:11:12Z', htmlUrl: 'html_url', - candidates: [ - { - tagName: 'rc-1.0.0', - sha: '', - }, - ], + candidates: [], versions: [], }, '1.1': { baseVersion: '1.1', createdAt: '2021-01-01T10:11:12Z', htmlUrl: 'html_url', - candidates: [ - { - tagName: 'rc-1.1.0', - sha: '', - }, - ], + candidates: [], + versions: [], }, }, @@ -96,10 +87,6 @@ describe('getReleaseStats', () => { "1.1": Object { "baseVersion": "1.1", "candidates": Array [ - Object { - "sha": "", - "tagName": "rc-1.1.0", - }, Object { "sha": "sha", "tagName": "rc-1.1.1", diff --git a/plugins/git-release-manager/src/features/Stats/helpers/getReleaseStats.tsx b/plugins/git-release-manager/src/features/Stats/helpers/getReleaseStats.tsx index 41ce41b1ac..fb3bad8d0c 100644 --- a/plugins/git-release-manager/src/features/Stats/helpers/getReleaseStats.tsx +++ b/plugins/git-release-manager/src/features/Stats/helpers/getReleaseStats.tsx @@ -55,18 +55,7 @@ export function getReleaseStats({ } 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, - }); - } + dest.push(tag); return acc; }, diff --git a/plugins/git-release-manager/src/features/Stats/helpers/getTagDate.ts b/plugins/git-release-manager/src/features/Stats/helpers/getTagDate.ts new file mode 100644 index 0000000000..f0c3bcd445 --- /dev/null +++ b/plugins/git-release-manager/src/features/Stats/helpers/getTagDate.ts @@ -0,0 +1,40 @@ +/* + * 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 { GitReleaseApi } from '../../../api/GitReleaseApiClient'; +import { Project } from '../../../contexts/ProjectContext'; + +interface GetTagDate { + pluginApiClient: GitReleaseApi; + project: Project; + tagSha: string; +} + +export const getTagDate = async ({ + pluginApiClient, + project, + tagSha, +}: GetTagDate) => { + const commitRes = await pluginApiClient.stats.getCommit({ + owner: project.owner, + repo: project.repo, + ref: tagSha, + }); + + return { + tagDate: commitRes.createdAt, + }; +}; diff --git a/plugins/git-release-manager/src/features/Stats/hooks/useGetTagDate.ts b/plugins/git-release-manager/src/features/Stats/hooks/useGetTagDate.ts new file mode 100644 index 0000000000..028b3a1929 --- /dev/null +++ b/plugins/git-release-manager/src/features/Stats/hooks/useGetTagDate.ts @@ -0,0 +1,46 @@ +/* + * 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 { useApi } from '@backstage/core'; + +import { getTagDate } from '../helpers/getTagDate'; +import { gitReleaseManagerApiRef } from '../../../api/serviceApiRef'; +import { GitReleaseManagerError } from '../../../errors/GitReleaseManagerError'; +import { ReleaseStats } from '../contexts/ReleaseStatsContext'; +import { UnboxArray } from '../../../types/helpers'; +import { useProjectContext } from '../../../contexts/ProjectContext'; + +export const useGetTagDate = ({ + tag, +}: { + tag: UnboxArray; +}) => { + const pluginApiClient = useApi(gitReleaseManagerApiRef); + const { project } = useProjectContext(); + + const tagDate = useAsync(async () => { + if (!tag) { + throw new GitReleaseManagerError('Missing tag details to get tag date'); + } + + return await getTagDate({ pluginApiClient, project, tagSha: tag.sha }); + }); + + return { + tagDate, + }; +}; diff --git a/plugins/git-release-manager/src/test-helpers/test-helpers.ts b/plugins/git-release-manager/src/test-helpers/test-helpers.ts index 886fad9de0..56b1d0214f 100644 --- a/plugins/git-release-manager/src/test-helpers/test-helpers.ts +++ b/plugins/git-release-manager/src/test-helpers/test-helpers.ts @@ -306,5 +306,11 @@ export const mockApiClient: GitReleaseApi = { getCommit: jest.fn(async () => ({ createdAt: '2021-01-01T10:11:12Z', })), + + getSingleTag: jest.fn(async () => ({ + date: '2021-04-29T12:48:30.120Z', + username: 'mock_usersingle_tag_name', + userEmail: 'mock_userEsingle_tag_mail', + })), }, }; From 8d0e7f0adbd8274a15593bf8c48cac0e97233481 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Thu, 29 Apr 2021 15:23:29 +0200 Subject: [PATCH 089/140] Continue flattening API Signed-off-by: Erik Engervall --- .../src/api/GitReleaseApiClient.test.ts | 4 +- .../src/api/GitReleaseApiClient.ts | 129 ++++++++++-------- .../src/constants/constants.test.ts | 1 + .../src/constants/constants.ts | 3 + .../hooks/useCreateReleaseCandidate.test.tsx | 18 ++- .../hooks/useCreateReleaseCandidate.ts | 88 ++++++++++-- .../src/features/Patch/hooks/usePatch.test.ts | 4 + .../src/features/Patch/hooks/usePatch.ts | 12 +- .../src/test-helpers/test-helpers.ts | 24 ++-- 9 files changed, 200 insertions(+), 83 deletions(-) diff --git a/plugins/git-release-manager/src/api/GitReleaseApiClient.test.ts b/plugins/git-release-manager/src/api/GitReleaseApiClient.test.ts index b37500451f..215c615963 100644 --- a/plugins/git-release-manager/src/api/GitReleaseApiClient.test.ts +++ b/plugins/git-release-manager/src/api/GitReleaseApiClient.test.ts @@ -33,10 +33,11 @@ describe('GitReleaseApiClient', () => { GitReleaseApiClient { "baseUrl": "https://api.github.com", "createRc": Object { - "createRef": [Function], "createRelease": [Function], "getComparison": [Function], }, + "createRef": [Function], + "createTagObject": [Function], "getBranch": [Function], "getHost": [Function], "getLatestCommit": [Function], @@ -54,7 +55,6 @@ describe('GitReleaseApiClient', () => { "patch": Object { "createCherryPickCommit": [Function], "createReference": [Function], - "createTagObject": [Function], "createTempCommit": [Function], "forceBranchHeadToTempCommit": [Function], "merge": [Function], diff --git a/plugins/git-release-manager/src/api/GitReleaseApiClient.ts b/plugins/git-release-manager/src/api/GitReleaseApiClient.ts index 22762ac812..4a837a02e1 100644 --- a/plugins/git-release-manager/src/api/GitReleaseApiClient.ts +++ b/plugins/git-release-manager/src/api/GitReleaseApiClient.ts @@ -247,21 +247,22 @@ export class GitReleaseApiClient implements GitReleaseApi { }; }; + createRef: GitReleaseApi['createRef'] = async ({ owner, repo, sha, ref }) => { + const { octokit } = await this.getOctokit(); + const createRefResponse = await octokit.git.createRef({ + owner, + repo, + ref, + sha, + }); + + return { + ref: createRefResponse.data.ref, + objectSha: createRefResponse.data.object.sha, + }; + }; + createRc: GitReleaseApi['createRc'] = { - createRef: async ({ owner, repo, mostRecentSha, targetBranch }) => { - const { octokit } = await this.getOctokit(); - const createRefResponse = await octokit.git.createRef({ - owner, - repo, - ref: `refs/heads/${targetBranch}`, - sha: mostRecentSha, - }); - - return { - ref: createRefResponse.data.ref, - }; - }, - getComparison: async ({ owner, repo, base, head }) => { const { octokit } = await this.getOctokit(); const compareCommitsResponse = await octokit.repos.compareCommits({ @@ -304,6 +305,36 @@ export class GitReleaseApiClient implements GitReleaseApi { }, }; + createTagObject: GitReleaseApi['createTagObject'] = async ({ + owner, + repo, + tag, + objectSha, + taggerName, + taggerEmail, + message, + }) => { + const { octokit } = await this.getOctokit(); + const { data: createdTagObject } = await octokit.git.createTag({ + owner, + repo, + message, + tag, + object: objectSha, + type: 'commit', + tagger: { + date: new Date().toISOString(), + email: taggerEmail, + name: taggerName, + }, + }); + + return { + tagName: createdTagObject.tag, + tagSha: createdTagObject.sha, + }; + }; + patch: GitReleaseApi['patch'] = { createTempCommit: async ({ owner, @@ -411,31 +442,13 @@ ${selectedPatchCommit.sha}`, }; }, - createTagObject: async ({ owner, repo, tag, objectSha }) => { - const { octokit } = await this.getOctokit(); - const { data: createdTagObject } = await octokit.git.createTag({ - owner, - repo, - message: - 'Tag generated by your friendly neighborhood Backstage Release Manager', - tag, - object: objectSha, - type: 'commit', - }); - - return { - tag: createdTagObject.tag, - sha: createdTagObject.sha, - }; - }, - createReference: async ({ owner, repo, bumpedTag, createdTagObject }) => { const { octokit } = await this.getOctokit(); const { data: reference } = await octokit.git.createRef({ owner, repo, ref: `refs/tags/${bumpedTag}`, - sha: createdTagObject.sha, + sha: createdTagObject.tagSha, }); return { @@ -652,16 +665,17 @@ export interface GitReleaseApi { }; }>; - createRc: { - createRef: ( - args: { - mostRecentSha: string; - targetBranch: string; - } & OwnerRepo, - ) => Promise<{ + createRef: ( + args: { ref: string; - }>; + sha: string; + } & OwnerRepo, + ) => Promise<{ + ref: string; + objectSha: string; + }>; + createRc: { getComparison: ( args: { base: string; @@ -685,6 +699,20 @@ export interface GitReleaseApi { tagName: string; }>; }; + + createTagObject: ( + args: { + tag: string; + taggerEmail?: string; + message: string; + objectSha: string; + taggerName: string; + } & OwnerRepo, + ) => Promise<{ + tagName: string; + tagSha: string; + }>; + patch: { createTempCommit: ( args: { @@ -749,16 +777,6 @@ export interface GitReleaseApi { }; }>; - createTagObject: ( - args: { - tag: string; - objectSha: string; - } & OwnerRepo, - ) => Promise<{ - tag: string; - sha: string; - }>; - createReference: ( args: { bumpedTag: string; @@ -854,9 +872,7 @@ export type GetLatestCommitResult = UnboxReturnedPromise< GitReleaseApi['getLatestCommit'] >; export type GetBranchResult = UnboxReturnedPromise; -export type CreateRefResult = UnboxReturnedPromise< - GitReleaseApi['createRc']['createRef'] ->; +export type CreateRefResult = UnboxReturnedPromise; export type GetComparisonResult = UnboxReturnedPromise< GitReleaseApi['createRc']['getComparison'] >; @@ -877,7 +893,7 @@ export type ReplaceTempCommitResult = UnboxReturnedPromise< GitReleaseApi['patch']['replaceTempCommit'] >; export type CreateTagObjectResult = UnboxReturnedPromise< - GitReleaseApi['patch']['createTagObject'] + GitReleaseApi['createTagObject'] >; export type CreateReferenceResult = UnboxReturnedPromise< GitReleaseApi['patch']['createReference'] @@ -897,3 +913,6 @@ export type GetCommitResult = UnboxReturnedPromise< export type GetAllReleasesResult = UnboxReturnedPromise< GitReleaseApi['stats']['getAllReleases'] >; +export type GetSingleTagResult = UnboxReturnedPromise< + GitReleaseApi['stats']['getSingleTag'] +>; diff --git a/plugins/git-release-manager/src/constants/constants.test.ts b/plugins/git-release-manager/src/constants/constants.test.ts index cf2b7d7548..759c2c7ab2 100644 --- a/plugins/git-release-manager/src/constants/constants.test.ts +++ b/plugins/git-release-manager/src/constants/constants.test.ts @@ -30,6 +30,7 @@ describe('constants', () => { "minor": "minor", "patch": "patch", }, + "TAG_OBJECT_MESSAGE": "Tag generated by your friendly neighborhood Backstage Release Manager", "VERSIONING_STRATEGIES": Object { "calver": "calver", "semver": "semver", diff --git a/plugins/git-release-manager/src/constants/constants.ts b/plugins/git-release-manager/src/constants/constants.ts index 195a4b80af..099a5913ca 100644 --- a/plugins/git-release-manager/src/constants/constants.ts +++ b/plugins/git-release-manager/src/constants/constants.ts @@ -37,3 +37,6 @@ export const VERSIONING_STRATEGIES: { semver: 'semver', calver: 'calver', } as const; + +export const TAG_OBJECT_MESSAGE = + 'Tag generated by your friendly neighborhood Backstage Release Manager'; diff --git a/plugins/git-release-manager/src/features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate.test.tsx b/plugins/git-release-manager/src/features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate.test.tsx index 2acffbf52c..b4551083a3 100644 --- a/plugins/git-release-manager/src/features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate.test.tsx +++ b/plugins/git-release-manager/src/features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate.test.tsx @@ -23,6 +23,7 @@ import { mockDefaultBranch, mockNextGitInfoCalver, mockReleaseVersionCalver, + mockUser, } from '../../../test-helpers/test-helpers'; import { useCreateReleaseCandidate } from './useCreateReleaseCandidate'; @@ -30,6 +31,9 @@ jest.mock('@backstage/core', () => ({ useApi: () => mockApiClient, createApiRef: jest.fn(), })); +jest.mock('../../../contexts/UserContext', () => ({ + useUserContext: () => ({ user: mockUser }), +})); describe('useCreateReleaseCandidate', () => { beforeEach(jest.clearAllMocks); @@ -49,7 +53,7 @@ describe('useCreateReleaseCandidate', () => { }); expect(result.error).toEqual(undefined); - expect(result.current.responseSteps).toHaveLength(4); + expect(result.current.responseSteps).toHaveLength(6); }); it('should return the expected responseSteps and progress (with successCb)', async () => { @@ -67,7 +71,7 @@ describe('useCreateReleaseCandidate', () => { await waitFor(() => result.current.run()); }); - expect(result.current.responseSteps).toHaveLength(5); + expect(result.current.responseSteps).toHaveLength(7); expect(result.current).toMatchInlineSnapshot(` Object { "progress": 100, @@ -78,7 +82,15 @@ describe('useCreateReleaseCandidate', () => { "secondaryMessage": "with message \\"latestCommit.commit.message\\"", }, Object { - "message": "Cut Release Branch", + "message": "Created Release Branch", + "secondaryMessage": "with ref \\"mock_createRef_ref\\"", + }, + Object { + "message": "Created Tag Object", + "secondaryMessage": "with sha \\"mock_tag_object_sha\\"", + }, + Object { + "message": "Cut Tag Reference", "secondaryMessage": "with ref \\"mock_createRef_ref\\"", }, Object { diff --git a/plugins/git-release-manager/src/features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate.ts b/plugins/git-release-manager/src/features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate.ts index ca203a1d10..3e6967ff37 100644 --- a/plugins/git-release-manager/src/features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate.ts +++ b/plugins/git-release-manager/src/features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate.ts @@ -27,7 +27,9 @@ import { getReleaseCandidateGitInfo } from '../../../helpers/getReleaseCandidate import { gitReleaseManagerApiRef } from '../../../api/serviceApiRef'; import { GitReleaseManagerError } from '../../../errors/GitReleaseManagerError'; import { Project } from '../../../contexts/ProjectContext'; +import { TAG_OBJECT_MESSAGE } from '../../../constants/constants'; import { useResponseSteps } from '../../../hooks/useResponseSteps'; +import { useUserContext } from '../../../contexts/UserContext'; interface UseCreateReleaseCandidate { defaultBranch: GetRepositoryResult['defaultBranch']; @@ -45,6 +47,7 @@ export function useCreateReleaseCandidate({ successCb, }: UseCreateReleaseCandidate): CardHook { const pluginApiClient = useApi(gitReleaseManagerApiRef); + const { user } = useUserContext(); if (releaseCandidateGitInfo.error) { throw new GitReleaseManagerError( @@ -87,18 +90,18 @@ export function useCreateReleaseCandidate({ }); /** - * (2) Create a new ref based on the default branch's most recent sha + * (2) Create release branch based on default branch's most recent sha */ - const createRcRes = useAsync(async () => { + const releaseBranchRes = useAsync(async () => { abortIfError(latestCommitRes.error); if (!latestCommitRes.value) return undefined; - const createdRef = await pluginApiClient.createRc + const createdReleaseBranch = await pluginApiClient .createRef({ owner: project.owner, repo: project.repo, - mostRecentSha: latestCommitRes.value.latestCommit.sha, - targetBranch: releaseCandidateGitInfo.rcBranch, + sha: latestCommitRes.value.latestCommit.sha, + ref: `refs/heads/${releaseCandidateGitInfo.rcBranch}`, }) .catch(error => { if (error?.body?.message === 'Reference already exists') { @@ -111,17 +114,80 @@ export function useCreateReleaseCandidate({ .catch(asyncCatcher); addStepToResponseSteps({ - message: 'Cut Release Branch', + message: 'Created Release Branch', + secondaryMessage: `with ref "${createdReleaseBranch.ref}"`, + }); + + return { + ...createdReleaseBranch, + }; + }, [latestCommitRes.value, latestCommitRes.error]); + + /** + * (3) Create tag object for our soon-to-be-created annotated tag + */ + const tagObjectRes = useAsync(async () => { + abortIfError(releaseBranchRes.error); + if (!releaseBranchRes.value) return undefined; + + const createdTagObject = await pluginApiClient + .createTagObject({ + owner: project.owner, + repo: project.repo, + tag: releaseCandidateGitInfo.rcReleaseTag, + objectSha: releaseBranchRes.value.objectSha, + taggerName: user.username, + taggerEmail: user.email, + message: TAG_OBJECT_MESSAGE, + }) + .catch(asyncCatcher); + + addStepToResponseSteps({ + message: 'Created Tag Object', + secondaryMessage: `with sha "${createdTagObject.tagSha}"`, + }); + + return { + ...createdTagObject, + }; + }, [releaseBranchRes.value, releaseBranchRes.error]); + + /** + * (4) Create reference for tag object + */ + const createRcRes = useAsync(async () => { + abortIfError(tagObjectRes.error); + if (!tagObjectRes.value) return undefined; + + const createdRef = await pluginApiClient + .createRef({ + owner: project.owner, + repo: project.repo, + ref: `refs/tags/${releaseCandidateGitInfo.rcReleaseTag}`, + sha: tagObjectRes.value.tagSha, + }) + .catch(error => { + if (error?.body?.message === 'Reference already exists') { + throw new GitReleaseManagerError( + `Tag reference "${releaseCandidateGitInfo.rcReleaseTag}" already exists`, + ); + } + throw error; + }) + .catch(asyncCatcher); + + addStepToResponseSteps({ + message: 'Cut Tag Reference', secondaryMessage: `with ref "${createdRef.ref}"`, }); return { ...createdRef, }; - }, [latestCommitRes.value, latestCommitRes.error]); + }, [tagObjectRes.value, tagObjectRes.error]); /** - * (3) Compose a body for the release + * (5) Compose a body for the release */ const getComparisonRes = useAsync(async () => { abortIfError(createRcRes.error); @@ -163,7 +229,7 @@ export function useCreateReleaseCandidate({ }, [createRcRes.value, createRcRes.error]); /** - * (4) Creates the Git Release itself + * (6) Creates the Git Release itself */ const createReleaseRes = useAsync(async () => { abortIfError(getComparisonRes.error); @@ -192,7 +258,7 @@ export function useCreateReleaseCandidate({ }, [getComparisonRes.value, getComparisonRes.error]); /** - * (5) Run successCb if defined + * (7) Run successCb if defined */ useAsync(async () => { if (successCb && !!createReleaseRes.value && !!getComparisonRes.value) { @@ -217,7 +283,7 @@ export function useCreateReleaseCandidate({ } }, [createReleaseRes.value]); - const TOTAL_STEPS = 4 + (!!successCb ? 1 : 0); + const TOTAL_STEPS = 6 + (!!successCb ? 1 : 0); const [progress, setProgress] = useState(0); useEffect(() => { setProgress((responseSteps.length / TOTAL_STEPS) * 100); diff --git a/plugins/git-release-manager/src/features/Patch/hooks/usePatch.test.ts b/plugins/git-release-manager/src/features/Patch/hooks/usePatch.test.ts index 5fe1614e42..9741244c23 100644 --- a/plugins/git-release-manager/src/features/Patch/hooks/usePatch.test.ts +++ b/plugins/git-release-manager/src/features/Patch/hooks/usePatch.test.ts @@ -24,6 +24,7 @@ import { mockReleaseVersionCalver, mockSelectedPatchCommit, mockTagParts, + mockUser, } from '../../../test-helpers/test-helpers'; import { usePatch } from './usePatch'; @@ -31,6 +32,9 @@ jest.mock('@backstage/core', () => ({ useApi: () => mockApiClient, createApiRef: jest.fn(), })); +jest.mock('../../../contexts/UserContext', () => ({ + useUserContext: () => ({ user: mockUser }), +})); describe('patch', () => { beforeEach(jest.clearAllMocks); diff --git a/plugins/git-release-manager/src/features/Patch/hooks/usePatch.ts b/plugins/git-release-manager/src/features/Patch/hooks/usePatch.ts index d188e2bf51..7ae8ed920b 100644 --- a/plugins/git-release-manager/src/features/Patch/hooks/usePatch.ts +++ b/plugins/git-release-manager/src/features/Patch/hooks/usePatch.ts @@ -28,6 +28,8 @@ import { gitReleaseManagerApiRef } from '../../../api/serviceApiRef'; import { Project } from '../../../contexts/ProjectContext'; import { SemverTagParts } from '../../../helpers/tagParts/getSemverTagParts'; import { useResponseSteps } from '../../../hooks/useResponseSteps'; +import { TAG_OBJECT_MESSAGE } from '../../../constants/constants'; +import { useUserContext } from '../../../contexts/UserContext'; interface Patch { bumpedTag: string; @@ -46,6 +48,7 @@ export function usePatch({ successCb, }: Patch): CardHook { const pluginApiClient = useApi(gitReleaseManagerApiRef); + const { user } = useUserContext(); const { responseSteps, addStepToResponseSteps, @@ -234,18 +237,21 @@ export function usePatch({ abortIfError(updatedRefRes.error); if (!updatedRefRes.value) return undefined; - const createdTagObject = await pluginApiClient.patch + const createdTagObject = await pluginApiClient .createTagObject({ owner: project.owner, repo: project.repo, tag: bumpedTag, objectSha: updatedRefRes.value.object.sha, + message: TAG_OBJECT_MESSAGE, + taggerName: user.username, + taggerEmail: user.email, }) .catch(asyncCatcher); addStepToResponseSteps({ message: 'Created new tag object', - secondaryMessage: `with name "${createdTagObject.tag}"`, + secondaryMessage: `with name "${createdTagObject.tagName}"`, }); return { @@ -272,7 +278,7 @@ export function usePatch({ addStepToResponseSteps({ message: `Created new reference "${reference.ref}"`, - secondaryMessage: `for tag object "${createdTagObjRes.value.tag}"`, + secondaryMessage: `for tag object "${createdTagObjRes.value.tagName}"`, }); return { diff --git a/plugins/git-release-manager/src/test-helpers/test-helpers.ts b/plugins/git-release-manager/src/test-helpers/test-helpers.ts index 56b1d0214f..47a04e62ff 100644 --- a/plugins/git-release-manager/src/test-helpers/test-helpers.ts +++ b/plugins/git-release-manager/src/test-helpers/test-helpers.ts @@ -40,6 +40,11 @@ const MOCK_RELEASE_BRANCH_NAME_SEMVER = `rc/${A_SEMVER_VERSION}`; const MOCK_RELEASE_CANDIDATE_TAG_NAME_SEMVER = `rc-${A_SEMVER_VERSION}`; const MOCK_RELEASE_VERSION_TAG_NAME_SEMVER = `version-${A_SEMVER_VERSION}`; +export const mockUser = { + username: mockOwner, + email: mockEmail, +}; + export const mockSemverProject: Project = { owner: mockOwner, repo: mockRepo, @@ -214,11 +219,12 @@ export const mockApiClient: GitReleaseApi = { getBranch: jest.fn(async () => createMockBranch()), - createRc: { - createRef: jest.fn(async () => ({ - ref: 'mock_createRef_ref', - })), + createRef: jest.fn(async () => ({ + ref: 'mock_createRef_ref', + objectSha: 'mock_createRef_objectSha', + })), + createRc: { createRelease: jest.fn(async () => ({ name: 'mock_createRelease_name', htmlUrl: 'mock_createRelease_html_url', @@ -231,6 +237,11 @@ export const mockApiClient: GitReleaseApi = { })), }, + createTagObject: jest.fn(async () => ({ + tagName: 'mock_tag_object_tag', + tagSha: 'mock_tag_object_sha', + })), + patch: { createCherryPickCommit: jest.fn(async () => ({ message: 'mock_cherrypick_message', @@ -241,11 +252,6 @@ export const mockApiClient: GitReleaseApi = { ref: 'mock_reference_ref', })), - createTagObject: jest.fn(async () => ({ - tag: 'mock_tag_object_tag', - sha: 'mock_tag_object_sha', - })), - createTempCommit: jest.fn(async () => ({ message: 'mock_commit_message', sha: 'mock_commit_sha', From 34a5d56d80b69d83a1f42ef4ae639a816c4fa6e2 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Thu, 29 Apr 2021 15:30:35 +0200 Subject: [PATCH 090/140] Create getPatchCommitSuffix helper to help the patch list to filter commits Signed-off-by: Erik Engervall --- .../src/api/GitReleaseApiClient.ts | 4 +++- .../src/features/Patch/PatchBody.tsx | 7 ++++++- .../Patch/helpers/getPatchCommitSuffix.ts | 19 +++++++++++++++++++ .../src/features/Patch/hooks/usePatch.ts | 6 +++++- 4 files changed, 33 insertions(+), 3 deletions(-) create mode 100644 plugins/git-release-manager/src/features/Patch/helpers/getPatchCommitSuffix.ts diff --git a/plugins/git-release-manager/src/api/GitReleaseApiClient.ts b/plugins/git-release-manager/src/api/GitReleaseApiClient.ts index 4a837a02e1..d35b5a043b 100644 --- a/plugins/git-release-manager/src/api/GitReleaseApiClient.ts +++ b/plugins/git-release-manager/src/api/GitReleaseApiClient.ts @@ -401,6 +401,7 @@ export class GitReleaseApiClient implements GitReleaseApi { selectedPatchCommit, mergeTree, releaseBranchSha, + messageSuffix, }) => { const { octokit } = await this.getOctokit(); const { data: cherryPickCommit } = await octokit.git.createCommit({ @@ -408,7 +409,7 @@ export class GitReleaseApiClient implements GitReleaseApi { repo, message: `[patch ${bumpedTag}] ${selectedPatchCommit.commit.message} -${selectedPatchCommit.sha}`, +${messageSuffix}`, tree: mergeTree, parents: [releaseBranchSha], }); @@ -757,6 +758,7 @@ export interface GitReleaseApi { >; mergeTree: string; releaseBranchSha: string; + messageSuffix: string; } & OwnerRepo, ) => Promise<{ message: string; diff --git a/plugins/git-release-manager/src/features/Patch/PatchBody.tsx b/plugins/git-release-manager/src/features/Patch/PatchBody.tsx index ab2769e2a1..8e916afa2e 100644 --- a/plugins/git-release-manager/src/features/Patch/PatchBody.tsx +++ b/plugins/git-release-manager/src/features/Patch/PatchBody.tsx @@ -42,6 +42,7 @@ import { CalverTagParts } from '../../helpers/tagParts/getCalverTagParts'; import { CenteredCircularProgress } from '../../components/CenteredCircularProgress'; import { ComponentConfigPatch } from '../../types/types'; import { Differ } from '../../components/Differ'; +import { getPatchCommitSuffix } from './helpers/getPatchCommitSuffix'; import { gitReleaseManagerApiRef } from '../../api/serviceApiRef'; import { GitReleaseManagerError } from '../../errors/GitReleaseManagerError'; import { ResponseStepDialog } from '../../components/ResponseStepDialog/ResponseStepDialog'; @@ -159,7 +160,11 @@ export const PatchBody = ({ const commitExistsOnReleaseBranch = !!gitDataResponse.value?.recentCommitsOnReleaseBranch.find( releaseBranchCommit => releaseBranchCommit.sha === commit.sha || - releaseBranchCommit.commit.message.includes(commit.sha), // The selected patch commit's sha is included in the commit message + // The selected patch commit's sha is included in the commit message, + // which means it's part of a previous patch + releaseBranchCommit.commit.message.includes( + getPatchCommitSuffix({ commitSha: commit.sha }), + ), ); const hasNoParent = !commit.firstParentSha; diff --git a/plugins/git-release-manager/src/features/Patch/helpers/getPatchCommitSuffix.ts b/plugins/git-release-manager/src/features/Patch/helpers/getPatchCommitSuffix.ts new file mode 100644 index 0000000000..09b867fd25 --- /dev/null +++ b/plugins/git-release-manager/src/features/Patch/helpers/getPatchCommitSuffix.ts @@ -0,0 +1,19 @@ +/* + * 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. + */ + +export const getPatchCommitSuffix = ({ commitSha }: { commitSha: string }) => { + return `[Backstage patch ${commitSha}]`; +}; diff --git a/plugins/git-release-manager/src/features/Patch/hooks/usePatch.ts b/plugins/git-release-manager/src/features/Patch/hooks/usePatch.ts index 7ae8ed920b..bc71267089 100644 --- a/plugins/git-release-manager/src/features/Patch/hooks/usePatch.ts +++ b/plugins/git-release-manager/src/features/Patch/hooks/usePatch.ts @@ -24,11 +24,12 @@ import { } from '../../../api/GitReleaseApiClient'; import { CalverTagParts } from '../../../helpers/tagParts/getCalverTagParts'; import { ComponentConfigPatch, CardHook } from '../../../types/types'; +import { getPatchCommitSuffix } from '../helpers/getPatchCommitSuffix'; import { gitReleaseManagerApiRef } from '../../../api/serviceApiRef'; import { Project } from '../../../contexts/ProjectContext'; import { SemverTagParts } from '../../../helpers/tagParts/getSemverTagParts'; -import { useResponseSteps } from '../../../hooks/useResponseSteps'; import { TAG_OBJECT_MESSAGE } from '../../../constants/constants'; +import { useResponseSteps } from '../../../hooks/useResponseSteps'; import { useUserContext } from '../../../contexts/UserContext'; interface Patch { @@ -190,6 +191,9 @@ export function usePatch({ mergeTree: mergeRes.value.commit.tree.sha, releaseBranchSha, selectedPatchCommit: releaseBranchRes.value.selectedPatchCommit, + messageSuffix: getPatchCommitSuffix({ + commitSha: releaseBranchRes.value.selectedPatchCommit.sha, + }), }) .catch(asyncCatcher); From c7ba03393c6abd2fb37827f5fa2cf4708088b680 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Thu, 29 Apr 2021 15:33:51 +0200 Subject: [PATCH 091/140] Replace api.patch.createReference with api.createRef Signed-off-by: Erik Engervall --- .../src/api/GitReleaseApiClient.test.ts | 1 - .../src/api/GitReleaseApiClient.ts | 26 ------------------- .../src/features/Patch/hooks/usePatch.test.ts | 2 +- .../src/features/Patch/hooks/usePatch.ts | 8 +++--- .../src/test-helpers/test-helpers.ts | 4 --- 5 files changed, 5 insertions(+), 36 deletions(-) diff --git a/plugins/git-release-manager/src/api/GitReleaseApiClient.test.ts b/plugins/git-release-manager/src/api/GitReleaseApiClient.test.ts index 215c615963..ba697ca99b 100644 --- a/plugins/git-release-manager/src/api/GitReleaseApiClient.test.ts +++ b/plugins/git-release-manager/src/api/GitReleaseApiClient.test.ts @@ -54,7 +54,6 @@ describe('GitReleaseApiClient', () => { "host": "github.com", "patch": Object { "createCherryPickCommit": [Function], - "createReference": [Function], "createTempCommit": [Function], "forceBranchHeadToTempCommit": [Function], "merge": [Function], diff --git a/plugins/git-release-manager/src/api/GitReleaseApiClient.ts b/plugins/git-release-manager/src/api/GitReleaseApiClient.ts index d35b5a043b..5d263f54a0 100644 --- a/plugins/git-release-manager/src/api/GitReleaseApiClient.ts +++ b/plugins/git-release-manager/src/api/GitReleaseApiClient.ts @@ -443,20 +443,6 @@ ${messageSuffix}`, }; }, - createReference: async ({ owner, repo, bumpedTag, createdTagObject }) => { - const { octokit } = await this.getOctokit(); - const { data: reference } = await octokit.git.createRef({ - owner, - repo, - ref: `refs/tags/${bumpedTag}`, - sha: createdTagObject.tagSha, - }); - - return { - ref: reference.ref, - }; - }, - updateRelease: async ({ owner, repo, @@ -779,15 +765,6 @@ export interface GitReleaseApi { }; }>; - createReference: ( - args: { - bumpedTag: string; - createdTagObject: CreateTagObjectResult; - } & OwnerRepo, - ) => Promise<{ - ref: string; - }>; - updateRelease: ( args: { bumpedTag: string; @@ -897,9 +874,6 @@ export type ReplaceTempCommitResult = UnboxReturnedPromise< export type CreateTagObjectResult = UnboxReturnedPromise< GitReleaseApi['createTagObject'] >; -export type CreateReferenceResult = UnboxReturnedPromise< - GitReleaseApi['patch']['createReference'] ->; export type UpdateReleaseResult = UnboxReturnedPromise< GitReleaseApi['patch']['updateRelease'] >; diff --git a/plugins/git-release-manager/src/features/Patch/hooks/usePatch.test.ts b/plugins/git-release-manager/src/features/Patch/hooks/usePatch.test.ts index 9741244c23..8d91d1c467 100644 --- a/plugins/git-release-manager/src/features/Patch/hooks/usePatch.test.ts +++ b/plugins/git-release-manager/src/features/Patch/hooks/usePatch.test.ts @@ -106,7 +106,7 @@ describe('patch', () => { "secondaryMessage": "with name \\"mock_tag_object_tag\\"", }, Object { - "message": "Created new reference \\"mock_reference_ref\\"", + "message": "Created new reference \\"mock_createRef_ref\\"", "secondaryMessage": "for tag object \\"mock_tag_object_tag\\"", }, Object { diff --git a/plugins/git-release-manager/src/features/Patch/hooks/usePatch.ts b/plugins/git-release-manager/src/features/Patch/hooks/usePatch.ts index bc71267089..140648e91a 100644 --- a/plugins/git-release-manager/src/features/Patch/hooks/usePatch.ts +++ b/plugins/git-release-manager/src/features/Patch/hooks/usePatch.ts @@ -271,12 +271,12 @@ export function usePatch({ abortIfError(createdTagObjRes.error); if (!createdTagObjRes.value) return undefined; - const reference = await pluginApiClient.patch - .createReference({ + const reference = await pluginApiClient + .createRef({ owner: project.owner, repo: project.repo, - bumpedTag, - createdTagObject: createdTagObjRes.value, + ref: `refs/tags/${bumpedTag}`, + sha: createdTagObjRes.value.tagSha, }) .catch(asyncCatcher); diff --git a/plugins/git-release-manager/src/test-helpers/test-helpers.ts b/plugins/git-release-manager/src/test-helpers/test-helpers.ts index 47a04e62ff..6b2951b9a3 100644 --- a/plugins/git-release-manager/src/test-helpers/test-helpers.ts +++ b/plugins/git-release-manager/src/test-helpers/test-helpers.ts @@ -248,10 +248,6 @@ export const mockApiClient: GitReleaseApi = { sha: 'mock_cherrypick_sha', })), - createReference: jest.fn(async () => ({ - ref: 'mock_reference_ref', - })), - createTempCommit: jest.fn(async () => ({ message: 'mock_commit_message', sha: 'mock_commit_sha', From 46d8949703364daa1f9d2feb19e95b5288bd9299 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Thu, 29 Apr 2021 15:43:41 +0200 Subject: [PATCH 092/140] Create annotated tags for promotions as well. Remove duplicate api.getCommit method Signed-off-by: Erik Engervall --- .../src/api/GitReleaseApiClient.test.ts | 3 +- .../src/api/GitReleaseApiClient.ts | 41 ++----- .../hooks/useCreateReleaseCandidate.ts | 4 +- .../PromoteRc/hooks/usePromoteRc.test.ts | 20 +++- .../features/PromoteRc/hooks/usePromoteRc.ts | 102 +++++++++++++++++- .../src/features/Stats/helpers/getTagDate.ts | 2 +- .../src/features/Stats/hooks/useGetCommit.ts | 2 +- .../src/test-helpers/test-helpers.ts | 7 +- 8 files changed, 129 insertions(+), 52 deletions(-) diff --git a/plugins/git-release-manager/src/api/GitReleaseApiClient.test.ts b/plugins/git-release-manager/src/api/GitReleaseApiClient.test.ts index ba697ca99b..1896693c6f 100644 --- a/plugins/git-release-manager/src/api/GitReleaseApiClient.test.ts +++ b/plugins/git-release-manager/src/api/GitReleaseApiClient.test.ts @@ -39,8 +39,8 @@ describe('GitReleaseApiClient', () => { "createRef": [Function], "createTagObject": [Function], "getBranch": [Function], + "getCommit": [Function], "getHost": [Function], - "getLatestCommit": [Function], "getLatestRelease": [Function], "getOwners": [Function], "getRecentCommits": [Function], @@ -66,7 +66,6 @@ describe('GitReleaseApiClient', () => { "stats": Object { "getAllReleases": [Function], "getAllTags": [Function], - "getCommit": [Function], "getSingleTag": [Function], }, } diff --git a/plugins/git-release-manager/src/api/GitReleaseApiClient.ts b/plugins/git-release-manager/src/api/GitReleaseApiClient.ts index 5d263f54a0..44816e860b 100644 --- a/plugins/git-release-manager/src/api/GitReleaseApiClient.ts +++ b/plugins/git-release-manager/src/api/GitReleaseApiClient.ts @@ -195,16 +195,12 @@ export class GitReleaseApiClient implements GitReleaseApi { }; }; - getLatestCommit: GitReleaseApi['getLatestCommit'] = async ({ - owner, - repo, - defaultBranch, - }) => { + getCommit: GitReleaseApi['getCommit'] = async ({ owner, repo, ref }) => { const { octokit } = await this.getOctokit(); const { data: latestCommit } = await octokit.repos.getCommit({ owner, repo, - ref: defaultBranch, + ref, ...DISABLE_CACHE, }); @@ -214,6 +210,7 @@ export class GitReleaseApiClient implements GitReleaseApi { commit: { message: latestCommit.commit.message, }, + createdAt: latestCommit.commit.committer?.date, }; }; @@ -527,20 +524,6 @@ ${selectedPatchCommit.commit.message}`, })); }, - getCommit: async ({ owner, repo, ref }) => { - const { octokit } = await this.getOctokit(); - - const { data: commit } = await octokit.repos.getCommit({ - owner, - repo, - ref, - }); - - return { - createdAt: commit.commit.committer?.date, - }; - }, - getSingleTag: async ({ owner, repo, tagSha }) => { const { octokit } = await this.getOctokit(); const singleTag = await octokit.git.getTag({ @@ -621,9 +604,9 @@ export interface GitReleaseApi { name: string; }>; - getLatestCommit: ( + getCommit: ( args: { - defaultBranch: string; + ref: string; } & OwnerRepo, ) => Promise<{ sha: string; @@ -631,6 +614,7 @@ export interface GitReleaseApi { commit: { message: string; }; + createdAt?: string; }>; getBranch: ( @@ -812,14 +796,6 @@ export interface GitReleaseApi { }> >; - getCommit: ( - args: { - ref: string; - } & OwnerRepo, - ) => Promise<{ - createdAt: string | undefined; - }>; - getSingleTag: ( args: { tagSha: string; @@ -848,7 +824,7 @@ export type GetRepositoryResult = UnboxReturnedPromise< GitReleaseApi['getRepository'] >; export type GetLatestCommitResult = UnboxReturnedPromise< - GitReleaseApi['getLatestCommit'] + GitReleaseApi['getCommit'] >; export type GetBranchResult = UnboxReturnedPromise; export type CreateRefResult = UnboxReturnedPromise; @@ -883,9 +859,6 @@ export type PromoteReleaseResult = UnboxReturnedPromise< export type GetAllTagsResult = UnboxReturnedPromise< GitReleaseApi['stats']['getAllTags'] >; -export type GetCommitResult = UnboxReturnedPromise< - GitReleaseApi['stats']['getCommit'] ->; export type GetAllReleasesResult = UnboxReturnedPromise< GitReleaseApi['stats']['getAllReleases'] >; diff --git a/plugins/git-release-manager/src/features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate.ts b/plugins/git-release-manager/src/features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate.ts index 3e6967ff37..d7cd38effc 100644 --- a/plugins/git-release-manager/src/features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate.ts +++ b/plugins/git-release-manager/src/features/CreateReleaseCandidate/hooks/useCreateReleaseCandidate.ts @@ -71,10 +71,10 @@ export function useCreateReleaseCandidate({ */ const [latestCommitRes, run] = useAsyncFn(async () => { const latestCommit = await pluginApiClient - .getLatestCommit({ + .getCommit({ owner: project.owner, repo: project.repo, - defaultBranch, + ref: defaultBranch, }) .catch(asyncCatcher); diff --git a/plugins/git-release-manager/src/features/PromoteRc/hooks/usePromoteRc.test.ts b/plugins/git-release-manager/src/features/PromoteRc/hooks/usePromoteRc.test.ts index d682476d33..c1618027bb 100644 --- a/plugins/git-release-manager/src/features/PromoteRc/hooks/usePromoteRc.test.ts +++ b/plugins/git-release-manager/src/features/PromoteRc/hooks/usePromoteRc.test.ts @@ -21,6 +21,7 @@ import { mockApiClient, mockCalverProject, mockReleaseCandidateCalver, + mockUser, } from '../../../test-helpers/test-helpers'; import { usePromoteRc } from './usePromoteRc'; @@ -33,6 +34,9 @@ jest.mock('../../../contexts/ProjectContext', () => ({ project: mockCalverProject, }), })); +jest.mock('../../../contexts/UserContext', () => ({ + useUserContext: () => ({ user: mockUser }), +})); describe('usePromoteRc', () => { beforeEach(jest.clearAllMocks); @@ -50,7 +54,7 @@ describe('usePromoteRc', () => { }); expect(result.error).toEqual(undefined); - expect(result.current.responseSteps).toHaveLength(1); + expect(result.current.responseSteps).toHaveLength(4); }); it('should return the expected responseSteps and progress (with successCb)', async () => { @@ -66,11 +70,23 @@ describe('usePromoteRc', () => { await waitFor(() => result.current.run()); }); - expect(result.current.responseSteps).toHaveLength(2); + expect(result.current.responseSteps).toHaveLength(5); expect(result.current).toMatchInlineSnapshot(` Object { "progress": 100, "responseSteps": Array [ + Object { + "message": "Fetched most recent commit from release branch", + "secondaryMessage": "with sha \\"latestCommit.sha\\"", + }, + Object { + "message": "Created Tag Object", + "secondaryMessage": "with sha \\"mock_tag_object_sha\\"", + }, + Object { + "message": "Create Tag Reference", + "secondaryMessage": "with ref \\"mock_createRef_ref\\"", + }, Object { "link": "mock_release_html_url", "message": "Promoted \\"mock_release_name\\"", diff --git a/plugins/git-release-manager/src/features/PromoteRc/hooks/usePromoteRc.ts b/plugins/git-release-manager/src/features/PromoteRc/hooks/usePromoteRc.ts index 7a106c3e51..bbeacb6b59 100644 --- a/plugins/git-release-manager/src/features/PromoteRc/hooks/usePromoteRc.ts +++ b/plugins/git-release-manager/src/features/PromoteRc/hooks/usePromoteRc.ts @@ -23,6 +23,9 @@ import { GetLatestReleaseResult } from '../../../api/GitReleaseApiClient'; import { gitReleaseManagerApiRef } from '../../../api/serviceApiRef'; import { useProjectContext } from '../../../contexts/ProjectContext'; import { useResponseSteps } from '../../../hooks/useResponseSteps'; +import { useUserContext } from '../../../contexts/UserContext'; +import { TAG_OBJECT_MESSAGE } from '../../../constants/constants'; +import { GitReleaseManagerError } from '../../../errors/GitReleaseManagerError'; interface PromoteRc { rcRelease: NonNullable; @@ -36,6 +39,7 @@ export function usePromoteRc({ successCb, }: PromoteRc): CardHook { const pluginApiClient = useApi(gitReleaseManagerApiRef); + const { user } = useUserContext(); const { project } = useProjectContext(); const { responseSteps, @@ -45,9 +49,97 @@ export function usePromoteRc({ } = useResponseSteps(); /** - * (1) Promote Release Candidate to Release Version + * (1) Fetch most recent release branch commit */ - const [promotedReleaseRes, run] = useAsyncFn(async () => { + const [latestReleaseBranchCommitSha, run] = useAsyncFn(async () => { + const latestCommit = await pluginApiClient + .getCommit({ + owner: project.owner, + repo: project.repo, + ref: rcRelease.targetCommitish, + }) + .catch(asyncCatcher); + + addStepToResponseSteps({ + message: 'Fetched most recent commit from release branch', + secondaryMessage: `with sha "${latestCommit.sha}"`, + }); + + return { + ...latestCommit, + }; + }); + + /** + * (2) Create tag object for our soon-to-be-created annotated tag + */ + const tagObjectRes = useAsync(async () => { + abortIfError(latestReleaseBranchCommitSha.error); + if (!latestReleaseBranchCommitSha.value) return undefined; + + const createdTagObject = await pluginApiClient + .createTagObject({ + owner: project.owner, + repo: project.repo, + tag: releaseVersion, + objectSha: latestReleaseBranchCommitSha.value.sha, + taggerName: user.username, + taggerEmail: user.email, + message: TAG_OBJECT_MESSAGE, + }) + .catch(asyncCatcher); + + addStepToResponseSteps({ + message: 'Created Tag Object', + secondaryMessage: `with sha "${createdTagObject.tagSha}"`, + }); + + return { + ...createdTagObject, + }; + }, [latestReleaseBranchCommitSha.value, latestReleaseBranchCommitSha.error]); + + /** + * (3) Create reference for tag object + */ + const createRcRes = useAsync(async () => { + abortIfError(tagObjectRes.error); + if (!tagObjectRes.value) return undefined; + + const createdRef = await pluginApiClient + .createRef({ + owner: project.owner, + repo: project.repo, + ref: `refs/tags/${releaseVersion}`, + sha: tagObjectRes.value.tagSha, + }) + .catch(error => { + if (error?.body?.message === 'Reference already exists') { + throw new GitReleaseManagerError( + `Tag reference "${releaseVersion}" already exists`, + ); + } + throw error; + }) + .catch(asyncCatcher); + + addStepToResponseSteps({ + message: 'Create Tag Reference', + secondaryMessage: `with ref "${createdRef.ref}"`, + }); + + return { + ...createdRef, + }; + }, [tagObjectRes.value, tagObjectRes.error]); + + /** + * (4) Promote Release Candidate to Release Version + */ + const promotedReleaseRes = useAsync(async () => { + abortIfError(createRcRes.error); + if (!createRcRes.value) return undefined; + const promotedRelease = await pluginApiClient.promoteRc .promoteRelease({ owner: project.owner, @@ -66,10 +158,10 @@ export function usePromoteRc({ return { ...promotedRelease, }; - }); + }, [createRcRes.value, createRcRes.error]); /** - * (2) Run successCb if defined + * (5) Run successCb if defined */ useAsync(async () => { if (successCb && !!promotedReleaseRes.value) { @@ -95,7 +187,7 @@ export function usePromoteRc({ } }, [promotedReleaseRes.value]); - const TOTAL_STEPS = 1 + (!!successCb ? 1 : 0); + const TOTAL_STEPS = 4 + (!!successCb ? 1 : 0); const [progress, setProgress] = useState(0); useEffect(() => { setProgress((responseSteps.length / TOTAL_STEPS) * 100); diff --git a/plugins/git-release-manager/src/features/Stats/helpers/getTagDate.ts b/plugins/git-release-manager/src/features/Stats/helpers/getTagDate.ts index f0c3bcd445..8c389e879c 100644 --- a/plugins/git-release-manager/src/features/Stats/helpers/getTagDate.ts +++ b/plugins/git-release-manager/src/features/Stats/helpers/getTagDate.ts @@ -28,7 +28,7 @@ export const getTagDate = async ({ project, tagSha, }: GetTagDate) => { - const commitRes = await pluginApiClient.stats.getCommit({ + const commitRes = await pluginApiClient.getCommit({ owner: project.owner, repo: project.repo, ref: tagSha, diff --git a/plugins/git-release-manager/src/features/Stats/hooks/useGetCommit.ts b/plugins/git-release-manager/src/features/Stats/hooks/useGetCommit.ts index 01ff486a74..83c5324615 100644 --- a/plugins/git-release-manager/src/features/Stats/hooks/useGetCommit.ts +++ b/plugins/git-release-manager/src/features/Stats/hooks/useGetCommit.ts @@ -30,7 +30,7 @@ export const useGetCommit = ({ ref }: { ref?: string }) => { throw new GitReleaseManagerError('Missing ref to get commit'); } - return pluginApiClient.stats.getCommit({ + return pluginApiClient.getCommit({ owner: project.owner, repo: project.repo, ref, diff --git a/plugins/git-release-manager/src/test-helpers/test-helpers.ts b/plugins/git-release-manager/src/test-helpers/test-helpers.ts index 6b2951b9a3..458ffff9a1 100644 --- a/plugins/git-release-manager/src/test-helpers/test-helpers.ts +++ b/plugins/git-release-manager/src/test-helpers/test-helpers.ts @@ -209,12 +209,13 @@ export const mockApiClient: GitReleaseApi = { name: mockRepo, })), - getLatestCommit: jest.fn(async () => ({ + getCommit: jest.fn(async () => ({ sha: 'latestCommit.sha', htmlUrl: 'latestCommit.html_url', commit: { message: 'latestCommit.commit.message', }, + createdAt: '2021-01-01T10:11:12Z', })), getBranch: jest.fn(async () => createMockBranch()), @@ -305,10 +306,6 @@ export const mockApiClient: GitReleaseApi = { }, ]), - getCommit: jest.fn(async () => ({ - createdAt: '2021-01-01T10:11:12Z', - })), - getSingleTag: jest.fn(async () => ({ date: '2021-04-29T12:48:30.120Z', username: 'mock_usersingle_tag_name', From e892b22c7c89f7d05e82dcb0145e438fc3d4c725 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Thu, 29 Apr 2021 17:55:08 +0200 Subject: [PATCH 093/140] Handle both tag and commit objects in Stats Signed-off-by: Erik Engervall --- .../src/api/GitReleaseApiClient.ts | 19 ++- .../helpers/getReleaseCommitPairs.test.tsx | 44 ++++-- .../Info/helpers/getReleaseCommitPairs.tsx | 21 +-- .../Stats/Info/hooks/useGetReleaseTimes.tsx | 38 +++-- .../Stats/Row/RowCollapsed/ReleaseTime.tsx | 58 ++++--- .../Stats/contexts/ReleaseStatsContext.tsx | 14 +- .../Stats/helpers/getReleaseStats.test.tsx | 33 ++-- .../src/features/Stats/helpers/getTagDate.ts | 40 ----- .../src/features/Stats/helpers/getTagDates.ts | 143 ++++++++++++++++++ .../src/features/Stats/hooks/useGetCommit.ts | 43 ------ .../src/features/Stats/hooks/useGetTagDate.ts | 46 ------ .../src/test-helpers/stats.ts | 21 ++- .../src/test-helpers/test-helpers.ts | 8 +- 13 files changed, 295 insertions(+), 233 deletions(-) delete mode 100644 plugins/git-release-manager/src/features/Stats/helpers/getTagDate.ts create mode 100644 plugins/git-release-manager/src/features/Stats/helpers/getTagDates.ts delete mode 100644 plugins/git-release-manager/src/features/Stats/hooks/useGetCommit.ts delete mode 100644 plugins/git-release-manager/src/features/Stats/hooks/useGetTagDate.ts diff --git a/plugins/git-release-manager/src/api/GitReleaseApiClient.ts b/plugins/git-release-manager/src/api/GitReleaseApiClient.ts index 44816e860b..0268ccf0fe 100644 --- a/plugins/git-release-manager/src/api/GitReleaseApiClient.ts +++ b/plugins/git-release-manager/src/api/GitReleaseApiClient.ts @@ -492,17 +492,21 @@ ${selectedPatchCommit.commit.message}`, getAllTags: async ({ owner, repo }) => { const { octokit } = await this.getOctokit(); - const tags = await octokit.paginate(octokit.repos.listTags, { + const tags = await octokit.paginate(octokit.git.listMatchingRefs, { owner, repo, + ref: 'tags', per_page: 100, ...DISABLE_CACHE, }); - return tags.map(tag => ({ - tagName: tag.name, - sha: tag.commit.sha, - })); + return tags + .map(tag => ({ + tagName: tag.ref.replace('refs/tags/', ''), + tagSha: tag.object.sha, + tagType: tag.object.type as 'tag' | 'commit', + })) + .reverse(); }, getAllReleases: async ({ owner, repo }) => { @@ -536,6 +540,7 @@ ${selectedPatchCommit.commit.message}`, date: singleTag.data.tagger.date, username: singleTag.data.tagger.name, userEmail: singleTag.data.tagger.email, + objectSha: singleTag.data.object.sha, }; }, }; @@ -780,7 +785,8 @@ export interface GitReleaseApi { ) => Promise< Array<{ tagName: string; - sha: string; + tagSha: string; + tagType: 'tag' | 'commit'; }> >; @@ -804,6 +810,7 @@ export interface GitReleaseApi { date: string; username: string; userEmail: string; + objectSha: string; }>; }; } diff --git a/plugins/git-release-manager/src/features/Stats/Info/helpers/getReleaseCommitPairs.test.tsx b/plugins/git-release-manager/src/features/Stats/Info/helpers/getReleaseCommitPairs.test.tsx index 3d9fb9449c..2e7f24c194 100644 --- a/plugins/git-release-manager/src/features/Stats/Info/helpers/getReleaseCommitPairs.test.tsx +++ b/plugins/git-release-manager/src/features/Stats/Info/helpers/getReleaseCommitPairs.test.tsx @@ -25,11 +25,13 @@ describe('getReleaseCommitPairs', () => { candidates: [ { tagName: 'rc-1.0.0', - sha: 'sha-1.0.0', + tagSha: 'sha-1.0.0', + tagType: 'tag' as const, }, { tagName: 'rc-1.0.1', - sha: 'sha-1.0.1', + tagSha: 'sha-1.0.1', + tagType: 'tag' as const, }, ], versions: [], @@ -42,13 +44,15 @@ describe('getReleaseCommitPairs', () => { candidates: [ { tagName: 'rc-2.0.0', - sha: 'sha-2.0.0', + tagSha: 'sha-2.0.0', + tagType: 'tag' as const, }, ], versions: [ { tagName: 'version-2.0.0', - sha: 'sha-2.0.0', + tagSha: 'sha-2.0.0', + tagType: 'tag' as const, }, ], }; @@ -60,17 +64,20 @@ describe('getReleaseCommitPairs', () => { candidates: [ { tagName: 'rc-3.0.1', - sha: 'sha-3.0.1', + tagSha: 'sha-3.0.1', + tagType: 'tag' as const, }, { tagName: 'rc-3.0.0', - sha: 'sha-3.0.0', + tagSha: 'sha-3.0.0', + tagType: 'tag' as const, }, ], versions: [ { tagName: 'version-3.0.1', - sha: 'sha-3.0.1', + tagSha: 'sha-3.0.1', + tagType: 'tag' as const, }, ], }; @@ -92,14 +99,29 @@ describe('getReleaseCommitPairs', () => { Object { "releaseCommitPairs": Array [ Object { - "baseVersion": "3.0", + "baseVersion": "2.0", "endCommit": Object { - "sha": "sha-3.0.1", - "tagName": "version-3.0.1", + "tagName": "version-2.0.0", + "tagSha": "sha-2.0.0", + "tagType": "tag", + }, + "startCommit": Object { + "tagName": "rc-2.0.0", + "tagSha": "sha-2.0.0", + "tagType": "tag", + }, + }, + Object { + "baseVersion": "3.0", + "endCommit": Object { + "tagName": "version-3.0.1", + "tagSha": "sha-3.0.1", + "tagType": "tag", }, "startCommit": Object { - "sha": "sha-3.0.0", "tagName": "rc-3.0.0", + "tagSha": "sha-3.0.0", + "tagType": "tag", }, }, ], diff --git a/plugins/git-release-manager/src/features/Stats/Info/helpers/getReleaseCommitPairs.tsx b/plugins/git-release-manager/src/features/Stats/Info/helpers/getReleaseCommitPairs.tsx index e55d97078e..ae54a678d3 100644 --- a/plugins/git-release-manager/src/features/Stats/Info/helpers/getReleaseCommitPairs.tsx +++ b/plugins/git-release-manager/src/features/Stats/Info/helpers/getReleaseCommitPairs.tsx @@ -28,32 +28,19 @@ export function getReleaseCommitPairs({ const endTag = release.versions[0]; // Missing Release Candidate for unknown reason - if (!startTag?.sha) { + if (!startTag) { 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) { + if (!endTag) { return acc; } return acc.concat({ baseVersion: release.baseVersion, - startCommit: { - tagName: startTag.tagName, - sha: startTag.sha, - }, - endCommit: { - tagName: endTag.tagName, - sha: endTag.sha, - }, + startCommit: { ...startTag }, + endCommit: { ...endTag }, }); }, [], diff --git a/plugins/git-release-manager/src/features/Stats/Info/hooks/useGetReleaseTimes.tsx b/plugins/git-release-manager/src/features/Stats/Info/hooks/useGetReleaseTimes.tsx index 59ae036405..3d1d5c5bb4 100644 --- a/plugins/git-release-manager/src/features/Stats/Info/hooks/useGetReleaseTimes.tsx +++ b/plugins/git-release-manager/src/features/Stats/Info/hooks/useGetReleaseTimes.tsx @@ -20,20 +20,22 @@ import { DateTime } from 'luxon'; import { useApi } from '@backstage/core'; import { getReleaseCommitPairs } from '../helpers/getReleaseCommitPairs'; -import { getTagDate } from '../../helpers/getTagDate'; import { gitReleaseManagerApiRef } from '../../../../api/serviceApiRef'; import { useProjectContext } from '../../../../contexts/ProjectContext'; import { useReleaseStatsContext } from '../../contexts/ReleaseStatsContext'; +import { getTagDates } from '../../helpers/getTagDates'; export type ReleaseCommitPairs = Array<{ baseVersion: string; startCommit: { tagName: string; - sha: string; + tagSha: string; + tagType: 'tag' | 'commit'; }; endCommit: { tagName: string; - sha: string; + tagSha: string; + tagType: 'tag' | 'commit'; }; }>; @@ -56,17 +58,17 @@ export function useGetReleaseTimes() { const [progress, setProgress] = useState(0); const { releaseCommitPairs } = getReleaseCommitPairs({ releaseStats }); - const [fnRes, run] = useAsyncFn(() => { + const [releaseTimeResult, run] = useAsyncFn(() => { setProgress(0); - return getAndSetReleaseTime(0); + return getAndSetReleaseTime({ pairIndex: 0 }); }); useAsync(async () => { if (averageReleaseTime.length === 0) return; if (releaseCommitPairs.length === averageReleaseTime.length) return; - await getAndSetReleaseTime(averageReleaseTime.length); - }, [fnRes.value, averageReleaseTime]); + await getAndSetReleaseTime({ pairIndex: averageReleaseTime.length }); + }, [releaseTimeResult.value, averageReleaseTime]); useEffect(() => { const unboundedProgress = Math.round( @@ -77,16 +79,20 @@ export function useGetReleaseTimes() { setProgress(boundedProgress); }, [averageReleaseTime.length, releaseCommitPairs.length]); - async function getAndSetReleaseTime(index: number) { - const { baseVersion, startCommit, endCommit } = releaseCommitPairs[index]; + async function getAndSetReleaseTime({ pairIndex }: { pairIndex: number }) { + const { baseVersion, startCommit, endCommit } = releaseCommitPairs[ + pairIndex + ]; - const [ - { tagDate: startCommitCreatedAt }, - { tagDate: endCommitCreatedAt }, - ] = await Promise.all([ - getTagDate({ pluginApiClient, project, tagSha: startCommit.sha }), - getTagDate({ pluginApiClient, project, tagSha: endCommit.sha }), - ]); + const { + startDate: startCommitCreatedAt, + endDate: endCommitCreatedAt, + } = await getTagDates({ + pluginApiClient, + project, + startTag: startCommit, + endTag: endCommit, + }); const releaseTime: ReleaseTime = { version: baseVersion, diff --git a/plugins/git-release-manager/src/features/Stats/Row/RowCollapsed/ReleaseTime.tsx b/plugins/git-release-manager/src/features/Stats/Row/RowCollapsed/ReleaseTime.tsx index 7f19ec1df9..9094831b32 100644 --- a/plugins/git-release-manager/src/features/Stats/Row/RowCollapsed/ReleaseTime.tsx +++ b/plugins/git-release-manager/src/features/Stats/Row/RowCollapsed/ReleaseTime.tsx @@ -21,22 +21,31 @@ import { Alert } from '@material-ui/lab'; import { CenteredCircularProgress } from '../../../../components/CenteredCircularProgress'; import { ReleaseStats } from '../../contexts/ReleaseStatsContext'; -import { useGetCommit } from '../../hooks/useGetCommit'; +import { useAsync } from 'react-use'; +import { getTagDates } from '../../helpers/getTagDates'; +import { useProjectContext } from '../../../../contexts/ProjectContext'; +import { useApi } from '@backstage/core'; +import { gitReleaseManagerApiRef } from '../../../../api/serviceApiRef'; +import { getDecimalNumber } from '../../helpers/getDecimalNumber'; interface ReleaseTimeProps { releaseStat: ReleaseStats['releases']['0']; } export function ReleaseTime({ releaseStat }: ReleaseTimeProps) { - const firstCandidateSha = [...releaseStat.candidates].reverse()[0]?.sha; - const { commit: releaseCut } = useGetCommit({ ref: firstCandidateSha }); + const pluginApiClient = useApi(gitReleaseManagerApiRef); + const { project } = useProjectContext(); - const mostRecentVersionSha = releaseStat.versions[0]?.sha; - const { commit: releaseComplete } = useGetCommit({ - ref: mostRecentVersionSha, - }); + const releaseTimes = useAsync(() => + getTagDates({ + pluginApiClient, + project, + startTag: [...releaseStat.candidates].reverse()[0], + endTag: releaseStat.versions[0], + }), + ); - if (releaseCut.loading || releaseComplete.loading) { + if (releaseTimes.loading || releaseTimes.loading) { return ( @@ -44,19 +53,22 @@ export function ReleaseTime({ releaseStat }: ReleaseTimeProps) { ); } - if (releaseCut.error) { + if (releaseTimes.error) { return ( Failed to fetch the first Release Candidate commit ( - {releaseCut.error.message}) + {releaseTimes.error.message}) ); } - const diff = - releaseCut.value?.createdAt && releaseComplete.value?.createdAt - ? DateTime.fromISO(releaseComplete.value.createdAt) - .diff(DateTime.fromISO(releaseCut.value.createdAt), ['days', 'hours']) + const { days = 0, hours = 0 } = + releaseTimes.value?.startDate && releaseTimes.value?.endDate + ? DateTime.fromISO(releaseTimes.value.endDate) + .diff(DateTime.fromISO(releaseTimes.value.startDate), [ + 'days', + 'hours', + ]) .toObject() : { days: -1 }; @@ -71,8 +83,8 @@ export function ReleaseTime({ releaseStat }: ReleaseTimeProps) { > {releaseStat.versions.length === 0 ? '-' : 'Release completed '} - {releaseComplete.value?.createdAt && - DateTime.fromISO(releaseComplete.value.createdAt) + {releaseTimes.value?.endDate && + DateTime.fromISO(releaseTimes.value.endDate) .setLocale('sv-SE') .toFormat('yyyy-MM-dd')} @@ -85,11 +97,13 @@ export function ReleaseTime({ releaseStat }: ReleaseTimeProps) { alignItems: 'center', }} > - - {diff.days === -1 ? ( - <>Ongoing: {diff.days} days + + {days === -1 ? ( + <>Ongoing ) : ( - <>Completed in: {diff.days} days + <> + Completed in: {days} days {getDecimalNumber(hours, 1)} hours + )} @@ -103,8 +117,8 @@ export function ReleaseTime({ releaseStat }: ReleaseTimeProps) { > Release Candidate created{' '} - {releaseCut.value?.createdAt && - DateTime.fromISO(releaseCut.value.createdAt) + {releaseTimes.value?.startDate && + DateTime.fromISO(releaseTimes.value.startDate) .setLocale('sv-SE') .toFormat('yyyy-MM-dd')} diff --git a/plugins/git-release-manager/src/features/Stats/contexts/ReleaseStatsContext.tsx b/plugins/git-release-manager/src/features/Stats/contexts/ReleaseStatsContext.tsx index 452adbb87b..85f526b72e 100644 --- a/plugins/git-release-manager/src/features/Stats/contexts/ReleaseStatsContext.tsx +++ b/plugins/git-release-manager/src/features/Stats/contexts/ReleaseStatsContext.tsx @@ -27,21 +27,15 @@ export interface ReleaseStats { baseVersion: string; createdAt: string | null; htmlUrl: string; - - /** - * Ordered from new to old - */ candidates: { tagName: string; - sha: string; + tagSha: string; + tagType: 'tag' | 'commit'; }[]; - - /** - * Ordered from new to old - */ versions: { tagName: string; - sha: string; + tagSha: string; + tagType: 'tag' | 'commit'; }[]; }; }; diff --git a/plugins/git-release-manager/src/features/Stats/helpers/getReleaseStats.test.tsx b/plugins/git-release-manager/src/features/Stats/helpers/getReleaseStats.test.tsx index 6d0cb1ca3d..77e4fe8dc0 100644 --- a/plugins/git-release-manager/src/features/Stats/helpers/getReleaseStats.test.tsx +++ b/plugins/git-release-manager/src/features/Stats/helpers/getReleaseStats.test.tsx @@ -44,14 +44,18 @@ describe('getReleaseStats', () => { 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' }, + { tagType: 'tag' as const, tagSha: 'sha', tagName: 'rc-1.0.0' }, + { tagType: 'tag' as const, tagSha: 'sha', tagName: 'rc-1.0.1' }, + { tagType: 'tag' as const, tagSha: 'sha', tagName: 'rc-1.0.2' }, + { tagType: 'tag' as const, tagSha: 'sha', tagName: 'version-1.0.2' }, + { tagType: 'tag' as const, tagSha: 'sha', tagName: 'rc-1.1.1' }, - { sha: 'unmatchable', tagName: 'rc-1/2/3' }, - { sha: 'unmappable', tagName: 'rc-123.123.123' }, + { tagType: 'tag' as const, tagSha: 'unmatchable', tagName: 'rc-1/2/3' }, + { + tagType: 'tag' as const, + tagSha: 'unmappable', + tagName: 'rc-123.123.123', + }, ], }); @@ -63,24 +67,28 @@ describe('getReleaseStats', () => { "baseVersion": "1.0", "candidates": Array [ Object { - "sha": "sha", "tagName": "rc-1.0.0", + "tagSha": "sha", + "tagType": "tag", }, Object { - "sha": "sha", "tagName": "rc-1.0.1", + "tagSha": "sha", + "tagType": "tag", }, Object { - "sha": "sha", "tagName": "rc-1.0.2", + "tagSha": "sha", + "tagType": "tag", }, ], "createdAt": "2021-01-01T10:11:12Z", "htmlUrl": "html_url", "versions": Array [ Object { - "sha": "sha", "tagName": "version-1.0.2", + "tagSha": "sha", + "tagType": "tag", }, ], }, @@ -88,8 +96,9 @@ describe('getReleaseStats', () => { "baseVersion": "1.1", "candidates": Array [ Object { - "sha": "sha", "tagName": "rc-1.1.1", + "tagSha": "sha", + "tagType": "tag", }, ], "createdAt": "2021-01-01T10:11:12Z", diff --git a/plugins/git-release-manager/src/features/Stats/helpers/getTagDate.ts b/plugins/git-release-manager/src/features/Stats/helpers/getTagDate.ts deleted file mode 100644 index 8c389e879c..0000000000 --- a/plugins/git-release-manager/src/features/Stats/helpers/getTagDate.ts +++ /dev/null @@ -1,40 +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 { GitReleaseApi } from '../../../api/GitReleaseApiClient'; -import { Project } from '../../../contexts/ProjectContext'; - -interface GetTagDate { - pluginApiClient: GitReleaseApi; - project: Project; - tagSha: string; -} - -export const getTagDate = async ({ - pluginApiClient, - project, - tagSha, -}: GetTagDate) => { - const commitRes = await pluginApiClient.getCommit({ - owner: project.owner, - repo: project.repo, - ref: tagSha, - }); - - return { - tagDate: commitRes.createdAt, - }; -}; diff --git a/plugins/git-release-manager/src/features/Stats/helpers/getTagDates.ts b/plugins/git-release-manager/src/features/Stats/helpers/getTagDates.ts new file mode 100644 index 0000000000..08e10efafe --- /dev/null +++ b/plugins/git-release-manager/src/features/Stats/helpers/getTagDates.ts @@ -0,0 +1,143 @@ +/* + * 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 { GitReleaseApi } from '../../../api/GitReleaseApiClient'; +import { GitReleaseManagerError } from '../../../errors/GitReleaseManagerError'; +import { Project } from '../../../contexts/ProjectContext'; + +interface GetTagDates { + pluginApiClient: GitReleaseApi; + project: Project; + startTag: { + tagSha: string; + tagType: 'tag' | 'commit'; + }; + endTag: { + tagSha: string; + tagType: 'tag' | 'commit'; + }; +} + +export const getTagDates = async ({ + pluginApiClient, + project, + startTag, + endTag, +}: GetTagDates) => { + if (startTag.tagType === 'tag' && endTag.tagType === 'tag') { + const [{ date: startDate }, { date: endDate }] = await Promise.all([ + pluginApiClient.stats.getSingleTag({ + owner: project.owner, + repo: project.repo, + tagSha: startTag.tagSha, + }), + pluginApiClient.stats.getSingleTag({ + owner: project.owner, + repo: project.repo, + tagSha: endTag.tagSha, + }), + ]); + + return { + startDate, + endDate, + }; + } + + if (startTag.tagType === 'commit' && endTag.tagType === 'commit') { + const [ + { createdAt: startDate }, + { createdAt: endDate }, + ] = await Promise.all([ + pluginApiClient.getCommit({ + owner: project.owner, + repo: project.repo, + ref: startTag.tagSha, + }), + pluginApiClient.getCommit({ + owner: project.owner, + repo: project.repo, + ref: endTag.tagSha, + }), + ]); + + return { + startDate, + endDate, + }; + } + + if (startTag.tagType === 'tag' && endTag.tagType === 'commit') { + const [{ date: startDate }, { createdAt: endDate }] = await Promise.all([ + getCommitFromTag({ pluginApiClient, project, tag: startTag }), + pluginApiClient.getCommit({ + owner: project.owner, + repo: project.repo, + ref: endTag.tagSha, + }), + ]); + + return { + startDate, + endDate, + }; + } + + if (startTag.tagType === 'commit' && endTag.tagType === 'tag') { + const [{ createdAt: startDate }, { date: endDate }] = await Promise.all([ + pluginApiClient.getCommit({ + owner: project.owner, + repo: project.repo, + ref: startTag.tagSha, + }), + getCommitFromTag({ pluginApiClient, project, tag: endTag }), + ]); + + return { + startDate, + endDate, + }; + } + + throw new GitReleaseManagerError( + `Failed to get tag dates for tags with type "${startTag.tagType}" and "${endTag.tagType}"`, + ); +}; + +async function getCommitFromTag({ + pluginApiClient, + project, + tag, +}: { + pluginApiClient: GetTagDates['pluginApiClient']; + project: GetTagDates['project']; + tag: GetTagDates['startTag'] | GetTagDates['endTag']; +}) { + const singleTag = await pluginApiClient.stats.getSingleTag({ + owner: project.owner, + repo: project.repo, + tagSha: tag.tagSha, + }); + const startCommit = await pluginApiClient.getCommit({ + owner: project.owner, + repo: project.repo, + ref: singleTag.objectSha, + }); + + return { + date: startCommit.createdAt, + }; +} diff --git a/plugins/git-release-manager/src/features/Stats/hooks/useGetCommit.ts b/plugins/git-release-manager/src/features/Stats/hooks/useGetCommit.ts deleted file mode 100644 index 83c5324615..0000000000 --- a/plugins/git-release-manager/src/features/Stats/hooks/useGetCommit.ts +++ /dev/null @@ -1,43 +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 { useAsync } from 'react-use'; -import { useApi } from '@backstage/core'; - -import { gitReleaseManagerApiRef } from '../../../api/serviceApiRef'; -import { GitReleaseManagerError } from '../../../errors/GitReleaseManagerError'; -import { useProjectContext } from '../../../contexts/ProjectContext'; - -export const useGetCommit = ({ ref }: { ref?: string }) => { - const pluginApiClient = useApi(gitReleaseManagerApiRef); - const { project } = useProjectContext(); - - const commit = useAsync(async () => { - if (!ref) { - throw new GitReleaseManagerError('Missing ref to get commit'); - } - - return pluginApiClient.getCommit({ - owner: project.owner, - repo: project.repo, - ref, - }); - }); - - return { - commit, - }; -}; diff --git a/plugins/git-release-manager/src/features/Stats/hooks/useGetTagDate.ts b/plugins/git-release-manager/src/features/Stats/hooks/useGetTagDate.ts deleted file mode 100644 index 028b3a1929..0000000000 --- a/plugins/git-release-manager/src/features/Stats/hooks/useGetTagDate.ts +++ /dev/null @@ -1,46 +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 { useAsync } from 'react-use'; -import { useApi } from '@backstage/core'; - -import { getTagDate } from '../helpers/getTagDate'; -import { gitReleaseManagerApiRef } from '../../../api/serviceApiRef'; -import { GitReleaseManagerError } from '../../../errors/GitReleaseManagerError'; -import { ReleaseStats } from '../contexts/ReleaseStatsContext'; -import { UnboxArray } from '../../../types/helpers'; -import { useProjectContext } from '../../../contexts/ProjectContext'; - -export const useGetTagDate = ({ - tag, -}: { - tag: UnboxArray; -}) => { - const pluginApiClient = useApi(gitReleaseManagerApiRef); - const { project } = useProjectContext(); - - const tagDate = useAsync(async () => { - if (!tag) { - throw new GitReleaseManagerError('Missing tag details to get tag date'); - } - - return await getTagDate({ pluginApiClient, project, tagSha: tag.sha }); - }); - - return { - tagDate, - }; -}; diff --git a/plugins/git-release-manager/src/test-helpers/stats.ts b/plugins/git-release-manager/src/test-helpers/stats.ts index 22b3b83d1e..bd1a33f95a 100644 --- a/plugins/git-release-manager/src/test-helpers/stats.ts +++ b/plugins/git-release-manager/src/test-helpers/stats.ts @@ -25,11 +25,13 @@ export const mockReleaseStats: ReleaseStats = { candidates: [ { tagName: 'rc-1.0.1', - sha: 'sha-1.0.1', + tagSha: 'sha-1.0.1', + tagType: 'tag', }, { tagName: 'rc-1.0.0', - sha: 'sha-1.0.0', + tagSha: 'sha-1.0.0', + tagType: 'tag', }, ], versions: [], @@ -41,25 +43,30 @@ export const mockReleaseStats: ReleaseStats = { candidates: [ { tagName: 'rc-1.1.2', - sha: 'sha-1.1.2', + tagSha: 'sha-1.1.2', + tagType: 'tag', }, { tagName: 'rc-1.1.1', - sha: 'sha-1.1.1', + tagSha: 'sha-1.1.1', + tagType: 'tag', }, { tagName: 'rc-1.1.0', - sha: 'sha-1.1.0', + tagSha: 'sha-1.1.0', + tagType: 'tag', }, ], versions: [ { tagName: 'version-1.1.3', - sha: 'sha-1.1.3', + tagSha: 'sha-1.1.3', + tagType: 'tag', }, { tagName: 'version-1.1.2', - sha: 'sha-1.1.2', + tagSha: 'sha-1.1.2', + tagType: 'tag', }, ], }, diff --git a/plugins/git-release-manager/src/test-helpers/test-helpers.ts b/plugins/git-release-manager/src/test-helpers/test-helpers.ts index 458ffff9a1..2f921c2013 100644 --- a/plugins/git-release-manager/src/test-helpers/test-helpers.ts +++ b/plugins/git-release-manager/src/test-helpers/test-helpers.ts @@ -292,7 +292,8 @@ export const mockApiClient: GitReleaseApi = { getAllTags: jest.fn(async () => [ { tagName: MOCK_RELEASE_CANDIDATE_TAG_NAME_CALVER, - sha: 'mock_sha', + tagSha: 'mock_sha', + tagType: 'tag' as const, }, ]), @@ -308,8 +309,9 @@ export const mockApiClient: GitReleaseApi = { getSingleTag: jest.fn(async () => ({ date: '2021-04-29T12:48:30.120Z', - username: 'mock_usersingle_tag_name', - userEmail: 'mock_userEsingle_tag_mail', + username: 'mock_user_single_tag_name', + userEmail: 'mock_user_single_tag_email', + objectSha: 'mock_single_tag_object_sha', })), }, }; From 802fec55c114ae628e4ee5f1d29d9ea81124d87e Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Thu, 29 Apr 2021 21:57:40 +0200 Subject: [PATCH 094/140] Replace styles/styles with Box stylings Signed-off-by: Erik Engervall --- .../src/GitReleaseManager.tsx | 7 +-- .../src/components/NoLatestRelease.test.tsx | 40 +++++++------ .../src/components/NoLatestRelease.tsx | 19 +++---- .../CreateReleaseCandidate.tsx | 39 +++++++------ .../src/features/Features.test.tsx | 2 +- .../src/features/Info/Info.tsx | 20 +++---- .../src/features/Patch/Patch.tsx | 13 ++--- .../src/features/Patch/PatchBody.tsx | 57 +++++++++---------- .../src/features/PromoteRc/PromoteRc.tsx | 30 +++++----- .../src/features/PromoteRc/PromoteRcBody.tsx | 24 +++++--- .../git-release-manager/src/styles/styles.ts | 26 --------- 11 files changed, 127 insertions(+), 150 deletions(-) delete mode 100644 plugins/git-release-manager/src/styles/styles.ts diff --git a/plugins/git-release-manager/src/GitReleaseManager.tsx b/plugins/git-release-manager/src/GitReleaseManager.tsx index 9b10d5d16d..2a2adf3e12 100644 --- a/plugins/git-release-manager/src/GitReleaseManager.tsx +++ b/plugins/git-release-manager/src/GitReleaseManager.tsx @@ -18,6 +18,7 @@ import React from 'react'; import { useAsync } from 'react-use'; import { Alert } from '@material-ui/lab'; import { useApi, ContentHeader } from '@backstage/core'; +import { Box } from '@material-ui/core'; import { ComponentConfig, @@ -34,7 +35,6 @@ import { ProjectContext, Project } from './contexts/ProjectContext'; import { RepoDetailsForm } from './features/RepoDetailsForm/RepoDetailsForm'; import { useQueryHandler } from './hooks/useQueryHandler'; import { UserContext } from './contexts/UserContext'; -import { useStyles } from './styles/styles'; interface GitReleaseManagerProps { project?: Omit; @@ -49,7 +49,6 @@ interface GitReleaseManagerProps { export function GitReleaseManager(props: GitReleaseManagerProps) { const pluginApiClient = useApi(gitReleaseManagerApiRef); - const classes = useStyles(); const { getParsedQuery } = useQueryHandler(); const { parsedQuery } = getParsedQuery(); @@ -89,7 +88,7 @@ export function GitReleaseManager(props: GitReleaseManagerProps) { return ( -
+ @@ -97,7 +96,7 @@ export function GitReleaseManager(props: GitReleaseManagerProps) { {isProjectValid(project) && } -
+
); diff --git a/plugins/git-release-manager/src/components/NoLatestRelease.test.tsx b/plugins/git-release-manager/src/components/NoLatestRelease.test.tsx index 1c2ea3ffa3..6863109448 100644 --- a/plugins/git-release-manager/src/components/NoLatestRelease.test.tsx +++ b/plugins/git-release-manager/src/components/NoLatestRelease.test.tsx @@ -26,28 +26,32 @@ describe('NoLatestRelease', () => { expect(container).toMatchInlineSnapshot(`
diff --git a/plugins/git-release-manager/src/components/NoLatestRelease.tsx b/plugins/git-release-manager/src/components/NoLatestRelease.tsx index b9cf2f6df9..e4121b1f4c 100644 --- a/plugins/git-release-manager/src/components/NoLatestRelease.tsx +++ b/plugins/git-release-manager/src/components/NoLatestRelease.tsx @@ -16,20 +16,19 @@ import React from 'react'; import { Alert } from '@material-ui/lab'; +import { Box } from '@material-ui/core'; -import { useStyles } from '../styles/styles'; import { TEST_IDS } from '../test-helpers/test-ids'; export const NoLatestRelease = () => { - const classes = useStyles(); - return ( - - Unable to find any Release - + + + Unable to find any Release + + ); }; diff --git a/plugins/git-release-manager/src/features/CreateReleaseCandidate/CreateReleaseCandidate.tsx b/plugins/git-release-manager/src/features/CreateReleaseCandidate/CreateReleaseCandidate.tsx index cdbd79382e..177e9f1195 100644 --- a/plugins/git-release-manager/src/features/CreateReleaseCandidate/CreateReleaseCandidate.tsx +++ b/plugins/git-release-manager/src/features/CreateReleaseCandidate/CreateReleaseCandidate.tsx @@ -17,6 +17,7 @@ import React, { useState, useEffect } from 'react'; import { Alert, AlertTitle } from '@material-ui/lab'; import { + Box, Button, FormControl, InputLabel, @@ -39,7 +40,6 @@ import { SEMVER_PARTS } from '../../constants/constants'; import { TEST_IDS } from '../../test-helpers/test-ids'; import { useCreateReleaseCandidate } from './hooks/useCreateReleaseCandidate'; import { useProjectContext } from '../../contexts/ProjectContext'; -import { useStyles } from '../../styles/styles'; interface CreateReleaseCandidateProps { defaultBranch: GetRepositoryResult['defaultBranch']; @@ -49,12 +49,11 @@ interface CreateReleaseCandidateProps { } const InfoCardPlusWrapper = ({ children }: { children: React.ReactNode }) => { - const classes = useStyles(); return ( - - Create Release Candidate - + + Create Release Candidate + {children} ); @@ -67,7 +66,6 @@ export const CreateReleaseCandidate = ({ successCb, }: CreateReleaseCandidateProps) => { const { project } = useProjectContext(); - const classes = useStyles(); const [semverBumpLevel, setSemverBumpLevel] = useState<'major' | 'minor'>( SEMVER_PARTS.minor, @@ -129,10 +127,7 @@ export const CreateReleaseCandidate = ({ {project.versioningStrategy === 'semver' && latestRelease && !conflictingPreRelease && ( -
+ Select bump severity @@ -150,26 +145,30 @@ export const CreateReleaseCandidate = ({ -
+ )} {conflictingPreRelease || tagAlreadyExists ? ( <> {conflictingPreRelease && ( - - The most recent release is already a Release Candidate - + + + The most recent release is already a Release Candidate + + )} {tagAlreadyExists && ( - - There's already a tag named{' '} - {releaseCandidateGitInfo.rcReleaseTag} - + + + There's already a tag named{' '} + {releaseCandidateGitInfo.rcReleaseTag} + + )} ) : ( -
+ -
+ )} - ); - } - - return ( -
- - - - - -
+ ); }; diff --git a/plugins/git-release-manager/src/features/PromoteRc/PromoteRc.tsx b/plugins/git-release-manager/src/features/PromoteRc/PromoteRc.tsx index 915330aaf4..efee800969 100644 --- a/plugins/git-release-manager/src/features/PromoteRc/PromoteRc.tsx +++ b/plugins/git-release-manager/src/features/PromoteRc/PromoteRc.tsx @@ -16,7 +16,7 @@ import React from 'react'; import { Alert, AlertTitle } from '@material-ui/lab'; -import { Typography } from '@material-ui/core'; +import { Box, Typography } from '@material-ui/core'; import { ComponentConfigPromoteRc } from '../../types/types'; import { GetLatestReleaseResult } from '../../api/GitReleaseApiClient'; @@ -24,7 +24,6 @@ import { InfoCardPlus } from '../../components/InfoCardPlus'; import { NoLatestRelease } from '../../components/NoLatestRelease'; import { PromoteRcBody } from './PromoteRcBody'; import { TEST_IDS } from '../../test-helpers/test-ids'; -import { useStyles } from '../../styles/styles'; interface PromoteRcProps { latestRelease: GetLatestReleaseResult; @@ -32,8 +31,6 @@ interface PromoteRcProps { } export const PromoteRc = ({ latestRelease, successCb }: PromoteRcProps) => { - const classes = useStyles(); - function Body() { if (latestRelease === null) { return ; @@ -41,14 +38,17 @@ export const PromoteRc = ({ latestRelease, successCb }: PromoteRcProps) => { if (!latestRelease.prerelease) { return ( - - Latest Git release is not a Release Candidate - One can only promote Release Candidates to Release Versions - + + + + Latest Git release is not a Release Candidate + + One can only promote Release Candidates to Release Versions + + ); } @@ -57,9 +57,9 @@ export const PromoteRc = ({ latestRelease, successCb }: PromoteRcProps) => { return ( - - Promote Release Candidate - + + Promote Release Candidate + diff --git a/plugins/git-release-manager/src/features/PromoteRc/PromoteRcBody.tsx b/plugins/git-release-manager/src/features/PromoteRc/PromoteRcBody.tsx index 6841c21d86..5cc8def9e2 100644 --- a/plugins/git-release-manager/src/features/PromoteRc/PromoteRcBody.tsx +++ b/plugins/git-release-manager/src/features/PromoteRc/PromoteRcBody.tsx @@ -15,7 +15,7 @@ */ import React from 'react'; -import { Button, Typography } from '@material-ui/core'; +import { Button, Typography, Box } from '@material-ui/core'; import { ComponentConfigPromoteRc } from '../../types/types'; import { Differ } from '../../components/Differ'; @@ -23,7 +23,6 @@ import { GetLatestReleaseResult } from '../../api/GitReleaseApiClient'; import { ResponseStepDialog } from '../../components/ResponseStepDialog/ResponseStepDialog'; import { TEST_IDS } from '../../test-helpers/test-ids'; import { usePromoteRc } from './hooks/usePromoteRc'; -import { useStyles } from '../../styles/styles'; interface PromoteRcBodyProps { rcRelease: NonNullable; @@ -31,7 +30,6 @@ interface PromoteRcBodyProps { } export const PromoteRcBody = ({ rcRelease, successCb }: PromoteRcBodyProps) => { - const classes = useStyles(); const releaseVersion = rcRelease.tagName.replace('rc-', 'version-'); const { progress, responseSteps, run, runInvoked } = usePromoteRc({ @@ -52,13 +50,21 @@ export const PromoteRcBody = ({ rcRelease, successCb }: PromoteRcBodyProps) => { return ( <> - - Promotes the current Release Candidate to a Release Version. - + + + Promotes the current Release Candidate to a Release Version. + + - - - + + + + + + + + ); }; From fb30cfad11b35ef6af50a81204aa204f46f9f26c Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Fri, 7 May 2021 13:09:24 +0200 Subject: [PATCH 138/140] Replace readGitHubIntegrationConfigs with ScmIntegrations.fromConfig Signed-off-by: Erik Engervall --- .../src/api/GitReleaseClient.test.ts | 2 +- .../src/api/GitReleaseClient.ts | 53 +++++++++++++------ 2 files changed, 37 insertions(+), 18 deletions(-) diff --git a/plugins/git-release-manager/src/api/GitReleaseClient.test.ts b/plugins/git-release-manager/src/api/GitReleaseClient.test.ts index c5b03b3c13..1b7cc7876a 100644 --- a/plugins/git-release-manager/src/api/GitReleaseClient.test.ts +++ b/plugins/git-release-manager/src/api/GitReleaseClient.test.ts @@ -31,7 +31,7 @@ describe('GitReleaseClient', () => { expect(gitReleaseClient).toMatchInlineSnapshot(` GitReleaseClient { - "baseUrl": "https://api.github.com", + "apiBaseUrl": "https://api.github.com", "createCommit": [Function], "createRef": [Function], "createRelease": [Function], diff --git a/plugins/git-release-manager/src/api/GitReleaseClient.ts b/plugins/git-release-manager/src/api/GitReleaseClient.ts index 602fb99a5c..9de3ce51f7 100644 --- a/plugins/git-release-manager/src/api/GitReleaseClient.ts +++ b/plugins/git-release-manager/src/api/GitReleaseClient.ts @@ -16,15 +16,16 @@ import { ConfigApi, OAuthApi } from '@backstage/core'; import { Octokit } from '@octokit/rest'; -import { readGitHubIntegrationConfigs } from '@backstage/integration'; +import { ScmIntegrations } from '@backstage/integration'; import { DISABLE_CACHE } from '../constants/constants'; import { Project } from '../contexts/ProjectContext'; import { UnboxArray, UnboxReturnedPromise } from '../types/helpers'; +import { GitReleaseManagerError } from '../errors/GitReleaseManagerError'; export class GitReleaseClient implements GitReleaseApi { private readonly githubAuthApi: OAuthApi; - private readonly baseUrl: string; + private readonly apiBaseUrl: string; readonly host: string; constructor({ @@ -36,27 +37,45 @@ export class GitReleaseClient implements GitReleaseApi { }) { this.githubAuthApi = githubAuthApi; - const githubIntegrationConfig = this.getGithubIntegrationConfig({ - configApi, - }); + const { host, apiBaseUrl } = this.getGithubIntegrationConfig({ configApi }); - this.host = githubIntegrationConfig?.host ?? 'github.com'; - this.baseUrl = - githubIntegrationConfig?.apiBaseUrl ?? 'https://api.github.com'; + this.host = host; + this.apiBaseUrl = apiBaseUrl; } private getGithubIntegrationConfig({ configApi }: { configApi: ConfigApi }) { - const configs = readGitHubIntegrationConfigs( - configApi.getOptionalConfigArray('integrations.github') ?? [], + const integrations = ScmIntegrations.fromConfig(configApi); + const githubIntegrations = integrations.github.list(); + + const defaultIntegration = githubIntegrations.find( + ({ config: { host } }) => host === 'github.com', + ); + const enterpriseIntegration = githubIntegrations.find( + ({ config: { host } }) => host !== 'github.com', ); - const githubIntegrationEnterpriseConfig = configs.find(v => - v.host.startsWith('ghe.'), - ); - const githubIntegrationConfig = configs.find(v => v.host === 'github.com'); + const host = + enterpriseIntegration?.config.host ?? defaultIntegration?.config.host; + const apiBaseUrl = + enterpriseIntegration?.config.apiBaseUrl ?? + defaultIntegration?.config.apiBaseUrl; - // Prioritize enterprise configs if available - return githubIntegrationEnterpriseConfig ?? githubIntegrationConfig; + if (!host) { + throw new GitReleaseManagerError( + 'Invalid API configuration: missing host', + ); + } + + if (!apiBaseUrl) { + throw new GitReleaseManagerError( + 'Invalid API configuration: missing apiBaseUrl', + ); + } + + return { + host, + apiBaseUrl, + }; } private async getOctokit() { @@ -65,7 +84,7 @@ export class GitReleaseClient implements GitReleaseApi { return { octokit: new Octokit({ auth: token, - baseUrl: this.baseUrl, + baseUrl: this.apiBaseUrl, }), }; } From 50d1fecad8b2144559c93d535703b2ea66109eb0 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Mon, 10 May 2021 13:49:23 +0200 Subject: [PATCH 139/140] Pass gitHubIntegrations as param to simplify future tests Signed-off-by: Erik Engervall --- .../src/api/GitReleaseClient.ts | 22 ++++++++++++------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/plugins/git-release-manager/src/api/GitReleaseClient.ts b/plugins/git-release-manager/src/api/GitReleaseClient.ts index 9de3ce51f7..cd178f292a 100644 --- a/plugins/git-release-manager/src/api/GitReleaseClient.ts +++ b/plugins/git-release-manager/src/api/GitReleaseClient.ts @@ -16,7 +16,7 @@ import { ConfigApi, OAuthApi } from '@backstage/core'; import { Octokit } from '@octokit/rest'; -import { ScmIntegrations } from '@backstage/integration'; +import { GitHubIntegration, ScmIntegrations } from '@backstage/integration'; import { DISABLE_CACHE } from '../constants/constants'; import { Project } from '../contexts/ProjectContext'; @@ -37,20 +37,26 @@ export class GitReleaseClient implements GitReleaseApi { }) { this.githubAuthApi = githubAuthApi; - const { host, apiBaseUrl } = this.getGithubIntegrationConfig({ configApi }); + const gitHubIntegrations = ScmIntegrations.fromConfig( + configApi, + ).github.list(); + const { host, apiBaseUrl } = this.getGithubIntegrationConfig({ + gitHubIntegrations, + }); this.host = host; this.apiBaseUrl = apiBaseUrl; } - private getGithubIntegrationConfig({ configApi }: { configApi: ConfigApi }) { - const integrations = ScmIntegrations.fromConfig(configApi); - const githubIntegrations = integrations.github.list(); - - const defaultIntegration = githubIntegrations.find( + private getGithubIntegrationConfig({ + gitHubIntegrations, + }: { + gitHubIntegrations: GitHubIntegration[]; + }) { + const defaultIntegration = gitHubIntegrations.find( ({ config: { host } }) => host === 'github.com', ); - const enterpriseIntegration = githubIntegrations.find( + const enterpriseIntegration = gitHubIntegrations.find( ({ config: { host } }) => host !== 'github.com', ); From abbb9d53fc1d62739520c4b9fecc2229e9912dd6 Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Mon, 10 May 2021 13:52:34 +0200 Subject: [PATCH 140/140] Bump dependencies Signed-off-by: Erik Engervall --- plugins/git-release-manager/package.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/git-release-manager/package.json b/plugins/git-release-manager/package.json index 16487f721e..9aca27d1a9 100644 --- a/plugins/git-release-manager/package.json +++ b/plugins/git-release-manager/package.json @@ -20,9 +20,9 @@ "clean": "backstage-cli clean" }, "dependencies": { - "@backstage/core": "^0.7.5", + "@backstage/core": "^0.7.8", "@backstage/integration": "^0.5.1", - "@backstage/theme": "^0.2.5", + "@backstage/theme": "^0.2.7", "recharts": "^1.8.5", "@material-ui/core": "^4.11.0", "@material-ui/icons": "^4.9.1", @@ -32,11 +32,11 @@ "qs": "^6.10.1", "react-dom": "^16.13.1", "react-router": "6.0.0-beta.0", - "react-use": "^15.3.3", + "react-use": "^17.2.4", "react": "^16.13.1" }, "devDependencies": { - "@backstage/cli": "^0.6.8", + "@backstage/cli": "^0.6.10", "@backstage/dev-utils": "^0.1.13", "@backstage/test-utils": "^0.1.10", "@testing-library/jest-dom": "^5.10.1",