From ce1749f2d64f7a4899d992820410fa6b1a33ab3e Mon Sep 17 00:00:00 2001 From: Erik Engervall Date: Mon, 12 Apr 2021 18:31:35 +0200 Subject: [PATCH] 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"