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 @@
+
+
\ 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:
+
+
+
+> **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 (
+
+
+ )}
+
+
+
+
+
+ );
+};
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:
+
+
+
+
+ );
+};
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 (
+
+ );
+};
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"