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 0000000000..947f9c0f35 Binary files /dev/null and b/plugins/release-manager-as-a-service/src/cards/info/rmaas-flow.png differ diff --git a/plugins/release-manager-as-a-service/src/cards/patchRc/Patch.test.tsx b/plugins/release-manager-as-a-service/src/cards/patchRc/Patch.test.tsx new file mode 100644 index 0000000000..13e1045b6e --- /dev/null +++ b/plugins/release-manager-as-a-service/src/cards/patchRc/Patch.test.tsx @@ -0,0 +1,41 @@ +/* + * Copyright 2021 Spotify AB + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import React from 'react'; +import { render } from '@testing-library/react'; + +import { + mockReleaseBranch, + mockCalverProject, +} from '../../test-helpers/test-helpers'; +import { TEST_IDS } from '../../test-helpers/test-ids'; +import { Patch } from './Patch'; + +describe('Patch', () => { + 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"