Continue overhaul of the API's interface - normalize and decouple dependency on GitHub
Signed-off-by: Erik Engervall <erik.engervall@gmail.com>
This commit is contained in:
@@ -81,11 +81,7 @@ export function GitHubReleaseManager({
|
||||
}
|
||||
|
||||
return (
|
||||
<PluginApiClientContext.Provider
|
||||
value={
|
||||
pluginApiClient as any // TODO: Fix type errors
|
||||
}
|
||||
>
|
||||
<PluginApiClientContext.Provider value={pluginApiClient}>
|
||||
<div className={classes.root}>
|
||||
<ContentHeader title="GitHub Release Manager" />
|
||||
|
||||
@@ -119,7 +115,7 @@ function Cards({
|
||||
);
|
||||
|
||||
const { versioningStrategyMatches } = useVersioningStrategyMatchesRepoTags({
|
||||
latestReleaseTagName: gitHubBatchInfo.value?.latestRelease?.tag_name,
|
||||
latestReleaseTagName: gitHubBatchInfo.value?.latestRelease?.tagName,
|
||||
project,
|
||||
repositoryName: gitHubBatchInfo.value?.repository.name,
|
||||
});
|
||||
@@ -151,7 +147,7 @@ function Cards({
|
||||
return (
|
||||
<Alert severity="error">
|
||||
Versioning mismatch, expected {project.versioningStrategy} version, got{' '}
|
||||
{gitHubBatchInfo.value?.latestRelease?.tag_name}
|
||||
{gitHubBatchInfo.value.latestRelease?.tagName}
|
||||
</Alert>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -19,10 +19,8 @@ import { Octokit } from '@octokit/rest';
|
||||
import { readGitHubIntegrationConfigs } from '@backstage/integration';
|
||||
|
||||
import {
|
||||
GhCompareCommitsResponse,
|
||||
GhCreateCommitResponse,
|
||||
GhCreateReferenceResponse,
|
||||
GhCreateReleaseResponse,
|
||||
GhCreateTagObjectResponse,
|
||||
GhGetBranchResponse,
|
||||
GhGetCommitResponse,
|
||||
@@ -36,32 +34,72 @@ import { getRcGitHubInfo } from '../cards/createRc/getRcGitHubInfo';
|
||||
import { SemverTagParts } from '../helpers/tagParts/getSemverTagParts';
|
||||
import { Project } from '../contexts/ProjectContext';
|
||||
|
||||
// export type UnboxPromise<T extends Promise<any>> = T extends Promise<infer U>
|
||||
// ? U
|
||||
// : never;
|
||||
type UnboxPromise<T extends Promise<any>> = T extends Promise<infer U>
|
||||
? U
|
||||
: never;
|
||||
|
||||
type Todo = any;
|
||||
export type ApiMethodRetval<
|
||||
T extends (...args: any) => Promise<any>
|
||||
> = UnboxPromise<ReturnType<T>>;
|
||||
|
||||
type Todo = any; // TODO:
|
||||
type PartialProject = Omit<Project, 'versioningStrategy'>;
|
||||
|
||||
export interface IPluginApiClient {
|
||||
getHost: () => string;
|
||||
|
||||
getRepoPath: (args: PartialProject) => string;
|
||||
|
||||
getOrganizations: () => Promise<{ organizations: string[] }>;
|
||||
|
||||
getRepositories: (args: {
|
||||
owner: string;
|
||||
}) => Promise<{ repositories: string[] }>;
|
||||
|
||||
getUsername: () => Promise<{ username: string }>;
|
||||
|
||||
getRecentCommits: (
|
||||
args: { releaseBranchName?: string } & PartialProject,
|
||||
) => Promise<Todo>;
|
||||
getReleases: (args: { releaseId: number } & PartialProject) => Promise<Todo>;
|
||||
getRelease: (args: { releaseId: number } & PartialProject) => Promise<Todo>;
|
||||
) => Promise<{
|
||||
recentCommits: {
|
||||
sha: string;
|
||||
author: {
|
||||
htmlUrl?: string;
|
||||
login?: string;
|
||||
};
|
||||
commit: {
|
||||
message: string;
|
||||
};
|
||||
}[];
|
||||
}>;
|
||||
|
||||
getLatestRelease: (
|
||||
args: PartialProject,
|
||||
) => Promise<{
|
||||
latestRelease: {
|
||||
targetCommitish: string;
|
||||
tagName: string;
|
||||
prerelease: boolean;
|
||||
id: number;
|
||||
htmlUrl: string;
|
||||
body?: string | null;
|
||||
} | null;
|
||||
}>;
|
||||
|
||||
getRepository: (
|
||||
args: PartialProject,
|
||||
) => Promise<{
|
||||
repository: {
|
||||
pushPermissions: boolean | undefined;
|
||||
defaultBranch: string;
|
||||
name: string;
|
||||
};
|
||||
}>;
|
||||
|
||||
getLatestCommit: (
|
||||
args: { defaultBranch: string } & PartialProject,
|
||||
) => Promise<Todo>;
|
||||
|
||||
getBranch: (args: { branchName: string } & PartialProject) => Promise<Todo>;
|
||||
|
||||
createRc: {
|
||||
@@ -70,21 +108,27 @@ export interface IPluginApiClient {
|
||||
mostRecentSha: string;
|
||||
targetBranch: string;
|
||||
} & PartialProject,
|
||||
) => Promise<Todo>;
|
||||
) => Promise<{ ref: string }>;
|
||||
|
||||
getComparison: (
|
||||
args: {
|
||||
previousReleaseBranch: string;
|
||||
nextReleaseBranch: string;
|
||||
} & PartialProject,
|
||||
) => Promise<Todo>;
|
||||
) => Promise<{ htmlUrl: string; aheadBy: number }>;
|
||||
|
||||
createRelease: (
|
||||
args: {
|
||||
nextGitHubInfo: ReturnType<typeof getRcGitHubInfo>;
|
||||
releaseBody: string;
|
||||
} & PartialProject,
|
||||
) => Promise<Todo>;
|
||||
) => Promise<{
|
||||
createReleaseResponse: {
|
||||
name: string | null;
|
||||
htmlUrl: string;
|
||||
tagName: string;
|
||||
};
|
||||
}>;
|
||||
};
|
||||
|
||||
patch: {
|
||||
@@ -142,7 +186,9 @@ export interface IPluginApiClient {
|
||||
updateRelease: (
|
||||
args: {
|
||||
bumpedTag: string;
|
||||
latestRelease: GhGetReleaseResponse;
|
||||
latestRelease: NonNullable<
|
||||
ApiMethodRetval<IPluginApiClient['getLatestRelease']>['latestRelease']
|
||||
>;
|
||||
tagParts: SemverTagParts | CalverTagParts;
|
||||
selectedPatchCommit: GhGetCommitResponse;
|
||||
} & PartialProject,
|
||||
@@ -157,10 +203,6 @@ export interface IPluginApiClient {
|
||||
} & PartialProject,
|
||||
) => Promise<Todo>;
|
||||
};
|
||||
|
||||
getOrganizations: (args: { ownerIsUser: boolean }) => Promise<Todo>;
|
||||
getUsername: () => Promise<{ username: string }>;
|
||||
getRepositories: (args: { owner: string; username: string }) => Promise<Todo>;
|
||||
}
|
||||
|
||||
export class PluginApiClient implements IPluginApiClient {
|
||||
@@ -221,36 +263,46 @@ export class PluginApiClient implements IPluginApiClient {
|
||||
|
||||
async getOrganizations() {
|
||||
const { octokit } = await this.getOctokit();
|
||||
const { data: orgs } = await octokit.orgs.listForAuthenticatedUser();
|
||||
const orgListResponse = await octokit.paginate(
|
||||
octokit.orgs.listForAuthenticatedUser,
|
||||
{ per_page: 100 },
|
||||
);
|
||||
|
||||
return { orgs };
|
||||
return {
|
||||
organizations: orgListResponse.map(organization => organization.login),
|
||||
};
|
||||
}
|
||||
|
||||
async getRepositories({
|
||||
owner,
|
||||
username,
|
||||
}: {
|
||||
owner: string;
|
||||
username: string;
|
||||
}) {
|
||||
async getRepositories({ owner }: { owner: string }) {
|
||||
const { octokit } = await this.getOctokit();
|
||||
|
||||
if (owner === username) {
|
||||
const { data: repos } = await octokit.repos.listForUser({ username });
|
||||
const repositoryResponse = await octokit
|
||||
.paginate(octokit.repos.listForOrg, { org: owner, per_page: 100 })
|
||||
.catch(async error => {
|
||||
// `owner` is not an org, try listing a user's repositories instead
|
||||
if (error.status === 404) {
|
||||
const userRepositoryResponse = await octokit.paginate(
|
||||
octokit.repos.listForUser,
|
||||
{ username: owner, per_page: 100 },
|
||||
);
|
||||
return userRepositoryResponse;
|
||||
}
|
||||
|
||||
return { repos };
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
|
||||
const { data: repos } = await octokit.repos.listForOrg({ org: owner });
|
||||
|
||||
return { repos };
|
||||
return {
|
||||
repositories: repositoryResponse.map(repository => repository.name),
|
||||
};
|
||||
}
|
||||
|
||||
async getUsername() {
|
||||
const { octokit } = await this.getOctokit();
|
||||
const { data: user } = await octokit.users.getAuthenticated();
|
||||
const userResponse = await octokit.users.getAuthenticated();
|
||||
|
||||
return { username: user.login };
|
||||
return {
|
||||
username: userResponse.data.login,
|
||||
};
|
||||
}
|
||||
|
||||
async getRecentCommits({
|
||||
@@ -261,43 +313,52 @@ export class PluginApiClient implements IPluginApiClient {
|
||||
releaseBranchName?: string;
|
||||
} & PartialProject) {
|
||||
const { octokit } = await this.getOctokit();
|
||||
const sha = releaseBranchName ? `?sha=${releaseBranchName}` : '';
|
||||
const recentCommitsResponse = await octokit.repos.listCommits({
|
||||
owner,
|
||||
repo,
|
||||
...(releaseBranchName ? { sha: releaseBranchName } : {}),
|
||||
});
|
||||
|
||||
const recentCommits: GhGetCommitResponse[] = (
|
||||
await octokit.request(
|
||||
`/repos/${this.getRepoPath({ owner, repo })}/commits${sha}`,
|
||||
)
|
||||
).data;
|
||||
|
||||
return { recentCommits };
|
||||
return {
|
||||
recentCommits: recentCommitsResponse.data.map(commit => ({
|
||||
sha: commit.sha,
|
||||
author: {
|
||||
htmlUrl: commit.author?.html_url,
|
||||
login: commit.author?.login,
|
||||
},
|
||||
commit: {
|
||||
message: commit.commit.message,
|
||||
},
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
async getReleases({ owner, repo }: PartialProject) {
|
||||
async getLatestRelease({ owner, repo }: PartialProject) {
|
||||
const { octokit } = await this.getOctokit();
|
||||
const { data: latestReleases } = await octokit.repos.listReleases({
|
||||
owner,
|
||||
repo,
|
||||
per_page: 1,
|
||||
});
|
||||
|
||||
const releases: GhGetReleaseResponse[] = (
|
||||
await octokit.request(
|
||||
`/repos/${this.getRepoPath({ owner, repo })}/releases`,
|
||||
)
|
||||
).data;
|
||||
if (latestReleases.length === 0) {
|
||||
return {
|
||||
latestRelease: null,
|
||||
};
|
||||
}
|
||||
|
||||
return { releases };
|
||||
}
|
||||
const latestRelease = latestReleases[0];
|
||||
|
||||
async getRelease({
|
||||
owner,
|
||||
repo,
|
||||
releaseId,
|
||||
}: { releaseId: number } & PartialProject) {
|
||||
const { octokit } = await this.getOctokit();
|
||||
|
||||
const latestRelease: GhGetReleaseResponse = (
|
||||
await octokit.request(
|
||||
`/repos/${this.getRepoPath({ owner, repo })}/releases/${releaseId}`,
|
||||
)
|
||||
).data;
|
||||
|
||||
return { latestRelease };
|
||||
return {
|
||||
latestRelease: {
|
||||
targetCommitish: latestRelease.target_commitish,
|
||||
tagName: latestRelease.tag_name,
|
||||
prerelease: latestRelease.prerelease,
|
||||
id: latestRelease.id,
|
||||
htmlUrl: latestRelease.html_url,
|
||||
body: latestRelease.body,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
async getRepository({ owner, repo }: PartialProject) {
|
||||
@@ -363,21 +424,16 @@ export class PluginApiClient implements IPluginApiClient {
|
||||
targetBranch: string;
|
||||
} & PartialProject) => {
|
||||
const { octokit } = await this.getOctokit();
|
||||
const createRefResponse = await octokit.git.createRef({
|
||||
owner,
|
||||
repo,
|
||||
ref: `refs/heads/${targetBranch}`,
|
||||
sha: mostRecentSha,
|
||||
});
|
||||
|
||||
const createdRef: GhCreateReferenceResponse = (
|
||||
await octokit.request(
|
||||
`/repos/${this.getRepoPath({ owner, repo })}/git/refs`,
|
||||
{
|
||||
method: 'POST',
|
||||
data: {
|
||||
ref: `refs/heads/${targetBranch}`,
|
||||
sha: mostRecentSha,
|
||||
},
|
||||
},
|
||||
)
|
||||
).data;
|
||||
|
||||
return { createdRef };
|
||||
return {
|
||||
ref: createRefResponse.data.ref,
|
||||
};
|
||||
},
|
||||
|
||||
getComparison: async ({
|
||||
@@ -390,17 +446,17 @@ export class PluginApiClient implements IPluginApiClient {
|
||||
nextReleaseBranch: string;
|
||||
} & PartialProject) => {
|
||||
const { octokit } = await this.getOctokit();
|
||||
const compareCommitsResponse = await octokit.repos.compareCommits({
|
||||
owner,
|
||||
repo,
|
||||
base: previousReleaseBranch,
|
||||
head: nextReleaseBranch,
|
||||
});
|
||||
|
||||
const comparison: GhCompareCommitsResponse = (
|
||||
await octokit.request(
|
||||
`/repos/${this.getRepoPath({
|
||||
owner,
|
||||
repo,
|
||||
})}/compare/${previousReleaseBranch}...${nextReleaseBranch}`,
|
||||
)
|
||||
).data;
|
||||
|
||||
return { comparison };
|
||||
return {
|
||||
htmlUrl: compareCommitsResponse.data.html_url,
|
||||
aheadBy: compareCommitsResponse.data.ahead_by,
|
||||
};
|
||||
},
|
||||
|
||||
createRelease: async ({
|
||||
@@ -413,24 +469,23 @@ export class PluginApiClient implements IPluginApiClient {
|
||||
releaseBody: string;
|
||||
} & PartialProject) => {
|
||||
const { octokit } = await this.getOctokit();
|
||||
const createReleaseResponse = await octokit.repos.createRelease({
|
||||
owner,
|
||||
repo,
|
||||
tag_name: nextGitHubInfo.rcReleaseTag,
|
||||
name: nextGitHubInfo.releaseName,
|
||||
target_commitish: nextGitHubInfo.rcBranch,
|
||||
body: releaseBody,
|
||||
prerelease: true,
|
||||
});
|
||||
|
||||
const createReleaseResponse: GhCreateReleaseResponse = (
|
||||
await octokit.request(
|
||||
`/repos/${this.getRepoPath({ owner, repo })}/releases`,
|
||||
{
|
||||
method: 'POST',
|
||||
data: {
|
||||
tag_name: nextGitHubInfo.rcReleaseTag,
|
||||
name: nextGitHubInfo.releaseName,
|
||||
target_commitish: nextGitHubInfo.rcBranch,
|
||||
body: releaseBody,
|
||||
prerelease: true,
|
||||
},
|
||||
},
|
||||
)
|
||||
).data;
|
||||
|
||||
return { createReleaseResponse };
|
||||
return {
|
||||
createReleaseResponse: {
|
||||
name: createReleaseResponse.data.name,
|
||||
htmlUrl: createReleaseResponse.data.html_url,
|
||||
tagName: createReleaseResponse.data.tag_name,
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
@@ -640,7 +695,9 @@ export class PluginApiClient implements IPluginApiClient {
|
||||
selectedPatchCommit,
|
||||
}: {
|
||||
bumpedTag: string;
|
||||
latestRelease: GhGetReleaseResponse;
|
||||
latestRelease: NonNullable<
|
||||
ApiMethodRetval<IPluginApiClient['getLatestRelease']>['latestRelease']
|
||||
>;
|
||||
tagParts: SemverTagParts | CalverTagParts;
|
||||
selectedPatchCommit: GhGetCommitResponse;
|
||||
} & PartialProject) => {
|
||||
|
||||
@@ -33,7 +33,6 @@ import { InfoCardPlus } from '../../components/InfoCardPlus';
|
||||
import {
|
||||
ComponentConfigCreateRc,
|
||||
GhGetBranchResponse,
|
||||
GhGetReleaseResponse,
|
||||
GhGetRepositoryResponse,
|
||||
SetRefetch,
|
||||
} from '../../types/types';
|
||||
@@ -43,10 +42,13 @@ import { TEST_IDS } from '../../test-helpers/test-ids';
|
||||
import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext';
|
||||
import { useProjectContext } from '../../contexts/ProjectContext';
|
||||
import { useStyles } from '../../styles/styles';
|
||||
import { ApiMethodRetval, IPluginApiClient } from '../../api/PluginApiClient';
|
||||
|
||||
interface CreateRcProps {
|
||||
defaultBranch: GhGetRepositoryResponse['default_branch'];
|
||||
latestRelease: GhGetReleaseResponse | null;
|
||||
latestRelease: ApiMethodRetval<
|
||||
IPluginApiClient['getLatestRelease']
|
||||
>['latestRelease'];
|
||||
releaseBranch: GhGetBranchResponse | null;
|
||||
setRefetch: SetRefetch;
|
||||
successCb?: ComponentConfigCreateRc['successCb'];
|
||||
@@ -97,7 +99,7 @@ export const CreateRc = ({
|
||||
|
||||
const tagAlreadyExists =
|
||||
latestRelease !== null &&
|
||||
latestRelease.tag_name === nextGitHubInfo.rcReleaseTag;
|
||||
latestRelease.tagName === nextGitHubInfo.rcReleaseTag;
|
||||
const conflictingPreRelease =
|
||||
latestRelease !== null && latestRelease.prerelease;
|
||||
|
||||
@@ -132,7 +134,7 @@ export const CreateRc = ({
|
||||
<Typography>
|
||||
<Differ
|
||||
icon="tag"
|
||||
prev={latestRelease?.tag_name}
|
||||
prev={latestRelease?.tagName}
|
||||
next={nextGitHubInfo.rcReleaseTag}
|
||||
/>
|
||||
</Typography>
|
||||
|
||||
@@ -16,7 +16,7 @@
|
||||
|
||||
import { DateTime } from 'luxon';
|
||||
|
||||
import { GhGetReleaseResponse } from '../../types/types';
|
||||
import { ApiMethodRetval, IPluginApiClient } from '../../api/PluginApiClient';
|
||||
import {
|
||||
mockSemverProject,
|
||||
mockCalverProject,
|
||||
@@ -34,8 +34,8 @@ describe('getRcGitHubInfo', () => {
|
||||
|
||||
describe('calver', () => {
|
||||
const latestRelease = {
|
||||
tag_name: 'rc-2020.01.01_0',
|
||||
} as GhGetReleaseResponse;
|
||||
tagName: 'rc-2020.01.01_0',
|
||||
} as ApiMethodRetval<IPluginApiClient['getLatestRelease']>['latestRelease'];
|
||||
|
||||
it('should return correct GitHub info', () => {
|
||||
expect(
|
||||
@@ -57,8 +57,8 @@ describe('getRcGitHubInfo', () => {
|
||||
|
||||
describe('semver', () => {
|
||||
const latestRelease = {
|
||||
tag_name: 'rc-1.1.1',
|
||||
} as GhGetReleaseResponse;
|
||||
tagName: 'rc-1.1.1',
|
||||
} as ApiMethodRetval<IPluginApiClient['getLatestRelease']>['latestRelease'];
|
||||
|
||||
it("should return correct GitHub info when there's previous releases", () => {
|
||||
expect(
|
||||
|
||||
@@ -18,9 +18,9 @@ import { DateTime } from 'luxon';
|
||||
|
||||
import { getBumpedSemverTagParts } from '../../helpers/getBumpedTag';
|
||||
import { getSemverTagParts } from '../../helpers/tagParts/getSemverTagParts';
|
||||
import { GhGetReleaseResponse } from '../../types/types';
|
||||
import { SEMVER_PARTS } from '../../constants/constants';
|
||||
import { Project } from '../../contexts/ProjectContext';
|
||||
import { ApiMethodRetval, IPluginApiClient } from '../../api/PluginApiClient';
|
||||
|
||||
export const getRcGitHubInfo = ({
|
||||
project,
|
||||
@@ -29,7 +29,9 @@ export const getRcGitHubInfo = ({
|
||||
injectedDate = DateTime.now().toFormat('yyyy.MM.dd'),
|
||||
}: {
|
||||
project: Project;
|
||||
latestRelease: GhGetReleaseResponse | null;
|
||||
latestRelease: ApiMethodRetval<
|
||||
IPluginApiClient['getLatestRelease']
|
||||
>['latestRelease'];
|
||||
semverBumpLevel: keyof typeof SEMVER_PARTS;
|
||||
injectedDate?: string;
|
||||
}) => {
|
||||
@@ -49,7 +51,7 @@ export const getRcGitHubInfo = ({
|
||||
};
|
||||
}
|
||||
|
||||
const tagParts = getSemverTagParts(latestRelease.tag_name);
|
||||
const tagParts = getSemverTagParts(latestRelease.tagName);
|
||||
const { bumpedTagParts } = getBumpedSemverTagParts(tagParts, semverBumpLevel);
|
||||
|
||||
const bumpedTag = `${bumpedTagParts.major}.${bumpedTagParts.minor}.${bumpedTagParts.patch}`;
|
||||
|
||||
@@ -28,10 +28,10 @@ describe('createRc', () => {
|
||||
|
||||
it('should work', async () => {
|
||||
const result = await createRc({
|
||||
pluginApiClient: mockApiClient,
|
||||
defaultBranch: mockDefaultBranch,
|
||||
latestRelease: mockReleaseVersion,
|
||||
nextGitHubInfo: mockNextGitHubInfo,
|
||||
pluginApiClient: mockApiClient,
|
||||
project: mockCalverProject,
|
||||
});
|
||||
|
||||
|
||||
@@ -17,20 +17,23 @@
|
||||
import { getRcGitHubInfo } from '../getRcGitHubInfo';
|
||||
import {
|
||||
ComponentConfigCreateRc,
|
||||
GhCreateReferenceResponse,
|
||||
GhGetReleaseResponse,
|
||||
GhGetRepositoryResponse,
|
||||
ResponseStep,
|
||||
} from '../../../types/types';
|
||||
import { PluginApiClient } from '../../../api/PluginApiClient';
|
||||
import {
|
||||
ApiMethodRetval,
|
||||
IPluginApiClient,
|
||||
} from '../../../api/PluginApiClient';
|
||||
import { GitHubReleaseManagerError } from '../../../errors/GitHubReleaseManagerError';
|
||||
import { Project } from '../../../contexts/ProjectContext';
|
||||
|
||||
interface CreateRC {
|
||||
defaultBranch: GhGetRepositoryResponse['default_branch'];
|
||||
latestRelease: GhGetReleaseResponse | null;
|
||||
latestRelease: ApiMethodRetval<
|
||||
IPluginApiClient['getLatestRelease']
|
||||
>['latestRelease'];
|
||||
nextGitHubInfo: ReturnType<typeof getRcGitHubInfo>;
|
||||
pluginApiClient: PluginApiClient;
|
||||
pluginApiClient: IPluginApiClient;
|
||||
project: Project;
|
||||
successCb?: ComponentConfigCreateRc['successCb'];
|
||||
}
|
||||
@@ -62,23 +65,20 @@ export async function createRc({
|
||||
* 2. Create a new ref based on the default branch's most recent sha
|
||||
*/
|
||||
const mostRecentSha = latestCommit.sha;
|
||||
let createdRef: GhCreateReferenceResponse;
|
||||
try {
|
||||
createdRef = (
|
||||
await pluginApiClient.createRc.createRef({
|
||||
...project,
|
||||
mostRecentSha,
|
||||
targetBranch: nextGitHubInfo.rcBranch,
|
||||
})
|
||||
).createdRef;
|
||||
} catch (error) {
|
||||
if (error.body.message === 'Reference already exists') {
|
||||
throw new GitHubReleaseManagerError(
|
||||
`Branch "${nextGitHubInfo.rcBranch}" already exists: .../tree/${nextGitHubInfo.rcBranch}`,
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
const createdRef = await pluginApiClient.createRc
|
||||
.createRef({
|
||||
...project,
|
||||
mostRecentSha,
|
||||
targetBranch: nextGitHubInfo.rcBranch,
|
||||
})
|
||||
.catch(error => {
|
||||
if (error?.body?.message === 'Reference already exists') {
|
||||
throw new GitHubReleaseManagerError(
|
||||
`Branch "${nextGitHubInfo.rcBranch}" already exists: .../tree/${nextGitHubInfo.rcBranch}`,
|
||||
);
|
||||
}
|
||||
throw error;
|
||||
});
|
||||
responseSteps.push({
|
||||
message: 'Cut Release Branch',
|
||||
secondaryMessage: `with ref "${createdRef.ref}"`,
|
||||
@@ -88,17 +88,17 @@ export async function createRc({
|
||||
* 3. Compose a body for the release
|
||||
*/
|
||||
const previousReleaseBranch = latestRelease
|
||||
? latestRelease.target_commitish
|
||||
? latestRelease.targetCommitish
|
||||
: defaultBranch;
|
||||
const nextReleaseBranch = nextGitHubInfo.rcBranch;
|
||||
const { comparison } = await pluginApiClient.createRc.getComparison({
|
||||
const comparison = await pluginApiClient.createRc.getComparison({
|
||||
...project,
|
||||
previousReleaseBranch,
|
||||
nextReleaseBranch,
|
||||
});
|
||||
const releaseBody = `**Compare** ${comparison.html_url}
|
||||
const releaseBody = `**Compare** ${comparison.htmlUrl}
|
||||
|
||||
**Ahead by** ${comparison.ahead_by} commits
|
||||
**Ahead by** ${comparison.aheadBy} commits
|
||||
|
||||
**Release branch** ${createdRef.ref}
|
||||
|
||||
@@ -108,7 +108,7 @@ export async function createRc({
|
||||
responseSteps.push({
|
||||
message: 'Fetched commit comparison',
|
||||
secondaryMessage: `${previousReleaseBranch}...${nextReleaseBranch}`,
|
||||
link: comparison.html_url,
|
||||
link: comparison.htmlUrl,
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -124,15 +124,15 @@ export async function createRc({
|
||||
responseSteps.push({
|
||||
message: `Created Release Candidate "${createReleaseResponse.name}"`,
|
||||
secondaryMessage: `with tag "${nextGitHubInfo.rcReleaseTag}"`,
|
||||
link: createReleaseResponse.html_url,
|
||||
link: createReleaseResponse.htmlUrl,
|
||||
});
|
||||
|
||||
await successCb?.({
|
||||
gitHubReleaseUrl: createReleaseResponse.html_url,
|
||||
gitHubReleaseUrl: createReleaseResponse.htmlUrl,
|
||||
gitHubReleaseName: createReleaseResponse.name,
|
||||
comparisonUrl: comparison.html_url,
|
||||
previousTag: latestRelease?.tag_name,
|
||||
createdTag: createReleaseResponse.tag_name,
|
||||
comparisonUrl: comparison.htmlUrl,
|
||||
previousTag: latestRelease?.tagName,
|
||||
createdTag: createReleaseResponse.tagName,
|
||||
});
|
||||
|
||||
return responseSteps;
|
||||
|
||||
@@ -18,20 +18,24 @@ import React from 'react';
|
||||
import { Link, Typography } from '@material-ui/core';
|
||||
|
||||
import { Differ } from '../../components/Differ';
|
||||
import { GhGetBranchResponse, GhGetReleaseResponse } from '../../types/types';
|
||||
import { GhGetBranchResponse } from '../../types/types';
|
||||
import { InfoCardPlus } from '../../components/InfoCardPlus';
|
||||
import { TEST_IDS } from '../../test-helpers/test-ids';
|
||||
import { useProjectContext } from '../../contexts/ProjectContext';
|
||||
import { useStyles } from '../../styles/styles';
|
||||
import flowImage from './flow.png';
|
||||
import { ApiMethodRetval, IPluginApiClient } from '../../api/PluginApiClient';
|
||||
|
||||
interface InfoCardProps {
|
||||
releaseBranch: GhGetBranchResponse | null;
|
||||
latestRelease: GhGetReleaseResponse | null;
|
||||
latestRelease: ApiMethodRetval<
|
||||
IPluginApiClient['getLatestRelease']
|
||||
>['latestRelease'];
|
||||
}
|
||||
|
||||
export const Info = ({ releaseBranch, latestRelease }: InfoCardProps) => {
|
||||
const project = useProjectContext();
|
||||
|
||||
const classes = useStyles();
|
||||
|
||||
return (
|
||||
@@ -98,7 +102,7 @@ export const Info = ({ releaseBranch, latestRelease }: InfoCardProps) => {
|
||||
</Typography>
|
||||
|
||||
<Typography>
|
||||
Latest release: <Differ icon="tag" next={latestRelease?.tag_name} />
|
||||
Latest release: <Differ icon="tag" next={latestRelease?.tagName} />
|
||||
</Typography>
|
||||
</div>
|
||||
</InfoCardPlus>
|
||||
|
||||
@@ -23,15 +23,17 @@ import { NoLatestRelease } from '../../components/NoLatestRelease';
|
||||
import {
|
||||
ComponentConfigPatch,
|
||||
GhGetBranchResponse,
|
||||
GhGetReleaseResponse,
|
||||
SetRefetch,
|
||||
} from '../../types/types';
|
||||
import { PatchBody } from './PatchBody';
|
||||
import { useProjectContext } from '../../contexts/ProjectContext';
|
||||
import { useStyles } from '../../styles/styles';
|
||||
import { ApiMethodRetval, IPluginApiClient } from '../../api/PluginApiClient';
|
||||
|
||||
interface PatchProps {
|
||||
latestRelease: GhGetReleaseResponse | null;
|
||||
latestRelease: ApiMethodRetval<
|
||||
IPluginApiClient['getLatestRelease']
|
||||
>['latestRelease'];
|
||||
releaseBranch: GhGetBranchResponse | null;
|
||||
setRefetch: SetRefetch;
|
||||
successCb?: ComponentConfigPatch['successCb'];
|
||||
@@ -53,7 +55,7 @@ export const Patch = ({
|
||||
|
||||
const { bumpedTag, tagParts } = getBumpedTag({
|
||||
project,
|
||||
tag: latestRelease.tag_name,
|
||||
tag: latestRelease.tagName,
|
||||
bumpLevel: 'patch',
|
||||
});
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ describe('PatchBody', () => {
|
||||
beforeEach(jest.clearAllMocks);
|
||||
|
||||
it('should render error', async () => {
|
||||
mockApiClient.getBranch.mockImplementationOnce(() => {
|
||||
(mockApiClient.getBranch as jest.Mock).mockImplementationOnce(() => {
|
||||
throw new Error('banana');
|
||||
});
|
||||
|
||||
|
||||
@@ -38,7 +38,6 @@ import {
|
||||
ComponentConfigPatch,
|
||||
GhGetBranchResponse,
|
||||
GhGetCommitResponse,
|
||||
GhGetReleaseResponse,
|
||||
SetRefetch,
|
||||
} from '../../types/types';
|
||||
import { CalverTagParts } from '../../helpers/tagParts/getCalverTagParts';
|
||||
@@ -50,10 +49,13 @@ import { TEST_IDS } from '../../test-helpers/test-ids';
|
||||
import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext';
|
||||
import { useProjectContext } from '../../contexts/ProjectContext';
|
||||
import { useStyles } from '../../styles/styles';
|
||||
import { ApiMethodRetval, IPluginApiClient } from '../../api/PluginApiClient';
|
||||
|
||||
interface PatchBodyProps {
|
||||
bumpedTag: string;
|
||||
latestRelease: GhGetReleaseResponse;
|
||||
latestRelease: NonNullable<
|
||||
ApiMethodRetval<IPluginApiClient['getLatestRelease']>['latestRelease']
|
||||
>;
|
||||
releaseBranch: GhGetBranchResponse | null;
|
||||
setRefetch: SetRefetch;
|
||||
successCb?: ComponentConfigPatch['successCb'];
|
||||
@@ -75,17 +77,17 @@ export const PatchBody = ({
|
||||
const githubDataResponse = useAsync(async () => {
|
||||
const [
|
||||
{ branch: releaseBranchResponse },
|
||||
{ recentCommits },
|
||||
{ recentCommits: recentCommitsOnDefaultBranch },
|
||||
] = await Promise.all([
|
||||
pluginApiClient.getBranch({
|
||||
...project,
|
||||
branchName: latestRelease.target_commitish,
|
||||
branchName: latestRelease.targetCommitish,
|
||||
}),
|
||||
pluginApiClient.getRecentCommits({ ...project }),
|
||||
]);
|
||||
|
||||
const {
|
||||
recentCommits: recentReleaseBranchCommits,
|
||||
recentCommits: recentCommitsOnReleaseBranch,
|
||||
} = await pluginApiClient.getRecentCommits({
|
||||
...project,
|
||||
releaseBranchName: releaseBranchResponse.name,
|
||||
@@ -93,8 +95,8 @@ export const PatchBody = ({
|
||||
|
||||
return {
|
||||
releaseBranch: releaseBranchResponse,
|
||||
recentReleaseBranchCommits,
|
||||
recentCommits,
|
||||
recentCommitsOnReleaseBranch,
|
||||
recentCommitsOnDefaultBranch,
|
||||
};
|
||||
});
|
||||
|
||||
@@ -146,118 +148,118 @@ export const PatchBody = ({
|
||||
)}
|
||||
|
||||
<Typography className={classes.paragraph}>
|
||||
<Differ icon="tag" prev={latestRelease.tag_name} next={bumpedTag} />
|
||||
<Differ icon="tag" prev={latestRelease.tagName} next={bumpedTag} />
|
||||
</Typography>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function CommitList() {
|
||||
if (!githubDataResponse.value?.recentCommits) {
|
||||
if (!githubDataResponse.value?.recentCommitsOnDefaultBranch) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<List>
|
||||
{githubDataResponse.value.recentCommits.map((commit, index) => {
|
||||
const commitExistsOnReleaseBranch = !!githubDataResponse.value?.recentReleaseBranchCommits.find(
|
||||
({ sha }) => {
|
||||
return sha === commit.sha;
|
||||
},
|
||||
);
|
||||
{githubDataResponse.value.recentCommitsOnDefaultBranch.map(
|
||||
(commit, index) => {
|
||||
const commitExistsOnReleaseBranch = !!githubDataResponse.value?.recentCommitsOnReleaseBranch.find(
|
||||
releaseBranchCommit => releaseBranchCommit.sha === commit.sha,
|
||||
);
|
||||
|
||||
return (
|
||||
<div style={{ position: 'relative' }} key={`commit-${index}`}>
|
||||
{commitExistsOnReleaseBranch && (
|
||||
<Paper
|
||||
elevation={3}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate3d(-50%,-50%,0)',
|
||||
zIndex: 10,
|
||||
color: 'green',
|
||||
padding: 6,
|
||||
background: 'rgba(244,244,244,1)',
|
||||
borderRadius: 8,
|
||||
}}
|
||||
>
|
||||
<FileCopyIcon
|
||||
fontSize="small"
|
||||
style={{ verticalAlign: 'middle' }}
|
||||
/>{' '}
|
||||
Already exists on <b>{releaseBranch?.name}</b>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
<ListItem
|
||||
disabled={
|
||||
patchReleaseResponse.loading ||
|
||||
(patchReleaseResponse.value &&
|
||||
patchReleaseResponse.value.length > 0) ||
|
||||
commitExistsOnReleaseBranch
|
||||
}
|
||||
role={undefined}
|
||||
dense
|
||||
button
|
||||
onClick={() => {
|
||||
if (index === checkedCommitIndex) {
|
||||
setCheckedCommitIndex(-1);
|
||||
} else {
|
||||
setCheckedCommitIndex(index);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Checkbox
|
||||
edge="start"
|
||||
checked={checkedCommitIndex === index}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText
|
||||
id={commit.sha}
|
||||
primary={commit.commit.message}
|
||||
secondary={
|
||||
<>
|
||||
{commit.sha}{' '}
|
||||
<Link
|
||||
color="primary"
|
||||
href={commit.author.html_url}
|
||||
target="_blank"
|
||||
>
|
||||
@{commit.author.login}
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton
|
||||
aria-label="commit"
|
||||
disabled={commitExistsOnReleaseBranch || !releaseBranch}
|
||||
onClick={() => {
|
||||
const repoPath = pluginApiClient.getRepoPath({
|
||||
...project,
|
||||
});
|
||||
const host = pluginApiClient.getHost();
|
||||
|
||||
const newTab = window.open(
|
||||
`https://${host}/${repoPath}/compare/${releaseBranch?.name}...${commit.sha}`,
|
||||
'_blank',
|
||||
);
|
||||
newTab?.focus();
|
||||
return (
|
||||
<div style={{ position: 'relative' }} key={`commit-${index}`}>
|
||||
{commitExistsOnReleaseBranch && (
|
||||
<Paper
|
||||
elevation={3}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
top: '50%',
|
||||
left: '50%',
|
||||
transform: 'translate3d(-50%,-50%,0)',
|
||||
zIndex: 10,
|
||||
color: 'green',
|
||||
padding: 6,
|
||||
background: 'rgba(244,244,244,1)',
|
||||
borderRadius: 8,
|
||||
}}
|
||||
>
|
||||
<OpenInNewIcon />
|
||||
</IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<FileCopyIcon
|
||||
fontSize="small"
|
||||
style={{ verticalAlign: 'middle' }}
|
||||
/>{' '}
|
||||
Already exists on <b>{releaseBranch?.name}</b>
|
||||
</Paper>
|
||||
)}
|
||||
|
||||
<ListItem
|
||||
disabled={
|
||||
patchReleaseResponse.loading ||
|
||||
(patchReleaseResponse.value &&
|
||||
patchReleaseResponse.value.length > 0) ||
|
||||
commitExistsOnReleaseBranch
|
||||
}
|
||||
role={undefined}
|
||||
dense
|
||||
button
|
||||
onClick={() => {
|
||||
if (index === checkedCommitIndex) {
|
||||
setCheckedCommitIndex(-1);
|
||||
} else {
|
||||
setCheckedCommitIndex(index);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<ListItemIcon>
|
||||
<Checkbox
|
||||
edge="start"
|
||||
checked={checkedCommitIndex === index}
|
||||
tabIndex={-1}
|
||||
/>
|
||||
</ListItemIcon>
|
||||
|
||||
<ListItemText
|
||||
id={commit.sha}
|
||||
primary={commit.commit.message}
|
||||
secondary={
|
||||
<>
|
||||
{commit.sha}{' '}
|
||||
<Link
|
||||
color="primary"
|
||||
href={commit.author.htmlUrl}
|
||||
target="_blank"
|
||||
>
|
||||
@{commit.author.login}
|
||||
</Link>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
|
||||
<ListItemSecondaryAction>
|
||||
<IconButton
|
||||
aria-label="commit"
|
||||
disabled={commitExistsOnReleaseBranch || !releaseBranch}
|
||||
onClick={() => {
|
||||
const repoPath = pluginApiClient.getRepoPath({
|
||||
...project,
|
||||
});
|
||||
const host = pluginApiClient.getHost();
|
||||
|
||||
const newTab = window.open(
|
||||
`https://${host}/${repoPath}/compare/${releaseBranch?.name}...${commit.sha}`,
|
||||
'_blank',
|
||||
);
|
||||
newTab?.focus();
|
||||
}}
|
||||
>
|
||||
<OpenInNewIcon />
|
||||
</IconButton>
|
||||
</ListItemSecondaryAction>
|
||||
</ListItem>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
)}
|
||||
</List>
|
||||
);
|
||||
}
|
||||
@@ -275,7 +277,11 @@ export const PatchBody = ({
|
||||
);
|
||||
}
|
||||
|
||||
if (!githubDataResponse.value?.recentCommits[checkedCommitIndex]) {
|
||||
if (
|
||||
!githubDataResponse.value?.recentCommitsOnDefaultBranch[
|
||||
checkedCommitIndex
|
||||
]
|
||||
) {
|
||||
return (
|
||||
<Button disabled variant="contained" color="primary">
|
||||
Patch Release Candidate
|
||||
@@ -291,7 +297,9 @@ export const PatchBody = ({
|
||||
onClick={() => {
|
||||
// FIXME: Optional chaining shouldn't be needed here due to the if-statement above
|
||||
patchReleaseFn(
|
||||
githubDataResponse.value?.recentCommits[checkedCommitIndex],
|
||||
githubDataResponse.value?.recentCommitsOnDefaultBranch[
|
||||
checkedCommitIndex
|
||||
],
|
||||
);
|
||||
}}
|
||||
>
|
||||
|
||||
@@ -17,19 +17,23 @@
|
||||
import {
|
||||
ComponentConfigPatch,
|
||||
GhGetCommitResponse,
|
||||
GhGetReleaseResponse,
|
||||
ResponseStep,
|
||||
} from '../../../types/types';
|
||||
import { CalverTagParts } from '../../../helpers/tagParts/getCalverTagParts';
|
||||
import { GitHubReleaseManagerError } from '../../../errors/GitHubReleaseManagerError';
|
||||
import { PluginApiClient } from '../../../api/PluginApiClient';
|
||||
import {
|
||||
ApiMethodRetval,
|
||||
IPluginApiClient,
|
||||
} from '../../../api/PluginApiClient';
|
||||
import { Project } from '../../../contexts/ProjectContext';
|
||||
import { SemverTagParts } from '../../../helpers/tagParts/getSemverTagParts';
|
||||
|
||||
interface Patch {
|
||||
bumpedTag: string;
|
||||
latestRelease: GhGetReleaseResponse;
|
||||
pluginApiClient: PluginApiClient;
|
||||
latestRelease: NonNullable<
|
||||
ApiMethodRetval<IPluginApiClient['getLatestRelease']>['latestRelease']
|
||||
>;
|
||||
pluginApiClient: IPluginApiClient;
|
||||
project: Project;
|
||||
selectedPatchCommit: GhGetCommitResponse;
|
||||
successCb?: ComponentConfigPatch['successCb'];
|
||||
@@ -52,7 +56,7 @@ export async function patch({
|
||||
throw new GitHubReleaseManagerError('Invalid commit');
|
||||
}
|
||||
|
||||
const releaseBranchName = latestRelease.target_commitish;
|
||||
const releaseBranchName = latestRelease.targetCommitish;
|
||||
/**
|
||||
* 1. Here is the branch we want to cherry-pick to:
|
||||
* > branch = GET /repos/$owner/$repo/branches/$branchName
|
||||
@@ -199,7 +203,7 @@ export async function patch({
|
||||
await successCb?.({
|
||||
updatedReleaseUrl: updatedRelease.html_url,
|
||||
updatedReleaseName: updatedRelease.name,
|
||||
previousTag: latestRelease.tag_name,
|
||||
previousTag: latestRelease.tagName,
|
||||
patchedTag: updatedRelease.tag_name,
|
||||
patchCommitUrl: selectedPatchCommit.html_url,
|
||||
patchCommitMessage: selectedPatchCommit.commit.message,
|
||||
|
||||
@@ -48,7 +48,7 @@ export function Owner({
|
||||
return <CenteredCircularProgress />;
|
||||
}
|
||||
|
||||
if (!value?.orgs) {
|
||||
if (!value?.organizations) {
|
||||
return <Alert severity="error">Could not fetch organizations</Alert>;
|
||||
}
|
||||
|
||||
@@ -74,9 +74,9 @@ export function Owner({
|
||||
<MenuItem value={username}>
|
||||
<strong>{username}</strong>
|
||||
</MenuItem>
|
||||
{value.orgs.map((org, index) => (
|
||||
<MenuItem key={`organization-${index}`} value={org.login}>
|
||||
{org.login}
|
||||
{value.organizations.map((orgName, index) => (
|
||||
<MenuItem key={`organization-${index}`} value={orgName}>
|
||||
{orgName}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
@@ -18,7 +18,7 @@ import React from 'react';
|
||||
import { useAsync } from 'react-use';
|
||||
import { FormControl, InputLabel, Select, MenuItem } from '@material-ui/core';
|
||||
import { Alert } from '@material-ui/lab';
|
||||
import { ControllerRenderProps, useForm } from 'react-hook-form';
|
||||
import { ControllerRenderProps } from 'react-hook-form';
|
||||
|
||||
import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext';
|
||||
import { useFormClasses } from './styles';
|
||||
@@ -26,10 +26,8 @@ import { CenteredCircularProgress } from '../../components/CenteredCircularProgr
|
||||
import { Project } from '../../contexts/ProjectContext';
|
||||
|
||||
export function Repo({
|
||||
username,
|
||||
controllerRenderProps,
|
||||
}: {
|
||||
username: string;
|
||||
controllerRenderProps: ControllerRenderProps;
|
||||
}) {
|
||||
const pluginApiClient = usePluginApiClientContext();
|
||||
@@ -37,11 +35,7 @@ export function Repo({
|
||||
const project: Project = controllerRenderProps.value;
|
||||
|
||||
const { loading, error, value } = useAsync(
|
||||
() =>
|
||||
pluginApiClient.getRepositories({
|
||||
owner: project.owner,
|
||||
username,
|
||||
}),
|
||||
async () => pluginApiClient.getRepositories({ owner: project.owner }),
|
||||
[project.owner],
|
||||
);
|
||||
|
||||
@@ -53,7 +47,7 @@ export function Repo({
|
||||
return <CenteredCircularProgress />;
|
||||
}
|
||||
|
||||
if (!value?.repos) {
|
||||
if (!value?.repositories) {
|
||||
return (
|
||||
<Alert severity="error">
|
||||
Could not fetch repositories for "{project.owner}"
|
||||
@@ -79,9 +73,9 @@ export function Repo({
|
||||
<MenuItem value="">
|
||||
<em>None</em>
|
||||
</MenuItem>
|
||||
{value.repos.map((repository, index) => (
|
||||
<MenuItem key={`repository-${index}`} value={repository.name}>
|
||||
{repository.name}
|
||||
{value.repositories.map((repositoryName, index) => (
|
||||
<MenuItem key={`repository-${index}`} value={repositoryName}>
|
||||
{repositoryName}
|
||||
</MenuItem>
|
||||
))}
|
||||
</Select>
|
||||
|
||||
@@ -44,10 +44,7 @@ export function RepoDetailsForm({
|
||||
/>
|
||||
|
||||
{project.owner.length > 0 && (
|
||||
<Repo
|
||||
controllerRenderProps={controllerRenderProps}
|
||||
username={username}
|
||||
/>
|
||||
<Repo controllerRenderProps={controllerRenderProps} />
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -20,17 +20,16 @@ import { Typography } from '@material-ui/core';
|
||||
|
||||
import { InfoCardPlus } from '../../components/InfoCardPlus';
|
||||
import { NoLatestRelease } from '../../components/NoLatestRelease';
|
||||
import {
|
||||
ComponentConfigPromoteRc,
|
||||
GhGetReleaseResponse,
|
||||
SetRefetch,
|
||||
} from '../../types/types';
|
||||
import { ComponentConfigPromoteRc, SetRefetch } from '../../types/types';
|
||||
import { PromoteRcBody } from './PromoteRcBody';
|
||||
import { useStyles } from '../../styles/styles';
|
||||
import { TEST_IDS } from '../../test-helpers/test-ids';
|
||||
import { ApiMethodRetval, IPluginApiClient } from '../../api/PluginApiClient';
|
||||
|
||||
interface PromoteRcProps {
|
||||
latestRelease: GhGetReleaseResponse | null;
|
||||
latestRelease: ApiMethodRetval<
|
||||
IPluginApiClient['getLatestRelease']
|
||||
>['latestRelease'];
|
||||
setRefetch: SetRefetch;
|
||||
successCb?: ComponentConfigPromoteRc['successCb'];
|
||||
}
|
||||
|
||||
@@ -20,20 +20,19 @@ import { Alert } from '@material-ui/lab';
|
||||
import { Button, Typography } from '@material-ui/core';
|
||||
|
||||
import { Differ } from '../../components/Differ';
|
||||
import {
|
||||
ComponentConfigPromoteRc,
|
||||
GhGetReleaseResponse,
|
||||
SetRefetch,
|
||||
} from '../../types/types';
|
||||
import { ComponentConfigPromoteRc, SetRefetch } from '../../types/types';
|
||||
import { promoteRc } from './sideEffects/promoteRc';
|
||||
import { ResponseStepList } from '../../components/ResponseStepList/ResponseStepList';
|
||||
import { TEST_IDS } from '../../test-helpers/test-ids';
|
||||
import { usePluginApiClientContext } from '../../contexts/PluginApiClientContext';
|
||||
import { useProjectContext } from '../../contexts/ProjectContext';
|
||||
import { useStyles } from '../../styles/styles';
|
||||
import { ApiMethodRetval, IPluginApiClient } from '../../api/PluginApiClient';
|
||||
|
||||
interface PromoteRcBodyProps {
|
||||
rcRelease: GhGetReleaseResponse;
|
||||
rcRelease: NonNullable<
|
||||
ApiMethodRetval<IPluginApiClient['getLatestRelease']>['latestRelease']
|
||||
>;
|
||||
setRefetch: SetRefetch;
|
||||
successCb?: ComponentConfigPromoteRc['successCb'];
|
||||
}
|
||||
@@ -46,7 +45,7 @@ export const PromoteRcBody = ({
|
||||
const pluginApiClient = usePluginApiClientContext();
|
||||
const project = useProjectContext();
|
||||
const classes = useStyles();
|
||||
const releaseVersion = rcRelease.tag_name.replace('rc-', 'version-');
|
||||
const releaseVersion = rcRelease.tagName.replace('rc-', 'version-');
|
||||
const [promoteGitHubRcResponse, promoseGitHubRcFn] = useAsyncFn(
|
||||
promoteRc({
|
||||
pluginApiClient,
|
||||
@@ -71,7 +70,7 @@ export const PromoteRcBody = ({
|
||||
</Typography>
|
||||
|
||||
<Typography className={classes.paragraph}>
|
||||
<Differ icon="tag" prev={rcRelease.tag_name} next={releaseVersion} />
|
||||
<Differ icon="tag" prev={rcRelease.tagName} next={releaseVersion} />
|
||||
</Typography>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -15,8 +15,9 @@
|
||||
*/
|
||||
|
||||
import {
|
||||
mockRcRelease,
|
||||
mockApiClient,
|
||||
mockRcRelease,
|
||||
mockSemverProject,
|
||||
} from '../../../test-helpers/test-helpers';
|
||||
import { promoteRc } from './promoteRc';
|
||||
|
||||
@@ -28,6 +29,7 @@ describe('promoteRc', () => {
|
||||
pluginApiClient: mockApiClient,
|
||||
rcRelease: mockRcRelease,
|
||||
releaseVersion: 'version-1.2.3',
|
||||
project: mockSemverProject,
|
||||
})();
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`
|
||||
|
||||
@@ -14,18 +14,19 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { ComponentConfigPromoteRc, ResponseStep } from '../../../types/types';
|
||||
import {
|
||||
ComponentConfigPromoteRc,
|
||||
GhGetReleaseResponse,
|
||||
ResponseStep,
|
||||
} from '../../../types/types';
|
||||
import { PluginApiClient } from '../../../api/PluginApiClient';
|
||||
ApiMethodRetval,
|
||||
IPluginApiClient,
|
||||
} from '../../../api/PluginApiClient';
|
||||
import { Project } from '../../../contexts/ProjectContext';
|
||||
|
||||
interface PromoteRc {
|
||||
pluginApiClient: PluginApiClient;
|
||||
pluginApiClient: IPluginApiClient;
|
||||
project: Project;
|
||||
rcRelease: GhGetReleaseResponse;
|
||||
rcRelease: NonNullable<
|
||||
ApiMethodRetval<IPluginApiClient['getLatestRelease']>['latestRelease']
|
||||
>;
|
||||
releaseVersion: string;
|
||||
successCb?: ComponentConfigPromoteRc['successCb'];
|
||||
}
|
||||
@@ -47,15 +48,15 @@ export function promoteRc({
|
||||
});
|
||||
responseSteps.push({
|
||||
message: `Promoted "${release.name}"`,
|
||||
secondaryMessage: `from "${rcRelease.tag_name}" to "${release.tag_name}"`,
|
||||
secondaryMessage: `from "${rcRelease.tagName}" 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,
|
||||
previousTagUrl: rcRelease.htmlUrl,
|
||||
previousTag: rcRelease.tagName,
|
||||
updatedTagUrl: release.html_url,
|
||||
updatedTag: release.tag_name,
|
||||
});
|
||||
|
||||
@@ -16,11 +16,11 @@
|
||||
|
||||
import { createContext, useContext } from 'react';
|
||||
|
||||
import { PluginApiClient } from '../api/PluginApiClient';
|
||||
import { IPluginApiClient } from '../api/PluginApiClient';
|
||||
import { GitHubReleaseManagerError } from '../errors/GitHubReleaseManagerError';
|
||||
|
||||
export const PluginApiClientContext = createContext<
|
||||
PluginApiClient | undefined
|
||||
IPluginApiClient | undefined
|
||||
>(undefined);
|
||||
|
||||
export const usePluginApiClientContext = () => {
|
||||
|
||||
@@ -14,8 +14,8 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Project } from '../contexts/ProjectContext';
|
||||
import { CalverTagParts } from './tagParts/getCalverTagParts';
|
||||
import { Project } from '../types/types';
|
||||
|
||||
export function isCalverTagParts(
|
||||
project: Project,
|
||||
|
||||
@@ -14,22 +14,21 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { getLatestRelease } from './getLatestRelease';
|
||||
import { PluginApiClient } from '../api/PluginApiClient';
|
||||
import { IPluginApiClient } from '../api/PluginApiClient';
|
||||
import { Project } from '../contexts/ProjectContext';
|
||||
|
||||
interface GetGitHubBatchInfo {
|
||||
project: Project;
|
||||
pluginApiClient: PluginApiClient;
|
||||
pluginApiClient: IPluginApiClient;
|
||||
}
|
||||
|
||||
export const getGitHubBatchInfo = ({
|
||||
project,
|
||||
pluginApiClient,
|
||||
}: GetGitHubBatchInfo) => async () => {
|
||||
const [{ repository }, latestRelease] = await Promise.all([
|
||||
const [{ repository }, { latestRelease }] = await Promise.all([
|
||||
pluginApiClient.getRepository({ ...project }),
|
||||
getLatestRelease({ project, pluginApiClient }),
|
||||
pluginApiClient.getLatestRelease({ ...project }),
|
||||
]);
|
||||
|
||||
if (latestRelease === null) {
|
||||
@@ -42,7 +41,7 @@ export const getGitHubBatchInfo = ({
|
||||
|
||||
const { branch } = await pluginApiClient.getBranch({
|
||||
...project,
|
||||
branchName: latestRelease.target_commitish,
|
||||
branchName: latestRelease.targetCommitish,
|
||||
});
|
||||
|
||||
return {
|
||||
|
||||
@@ -1,43 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { 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({ pluginApiClient: 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({ pluginApiClient: mockApiClient });
|
||||
|
||||
expect(result).toMatchInlineSnapshot(`null`);
|
||||
});
|
||||
});
|
||||
@@ -1,41 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021 Spotify AB
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { PluginApiClient } from '../api/PluginApiClient';
|
||||
import { Project } from '../contexts/ProjectContext';
|
||||
|
||||
interface GetLatestRelease {
|
||||
pluginApiClient: PluginApiClient;
|
||||
project: Project;
|
||||
}
|
||||
|
||||
export async function getLatestRelease({
|
||||
pluginApiClient,
|
||||
project,
|
||||
}: GetLatestRelease) {
|
||||
const { releases } = await pluginApiClient.getReleases({ ...project });
|
||||
|
||||
if (releases.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const { latestRelease } = await pluginApiClient.getRelease({
|
||||
...project,
|
||||
releaseId: releases[0].id,
|
||||
});
|
||||
|
||||
return latestRelease;
|
||||
}
|
||||
@@ -27,17 +27,15 @@ describe('testHelpers', () => {
|
||||
"getComparison": [MockFunction],
|
||||
},
|
||||
"getBranch": [MockFunction],
|
||||
"getHost": [MockFunction],
|
||||
"getLatestCommit": [MockFunction],
|
||||
"getOctokit": [Function],
|
||||
"getProject": [MockFunction],
|
||||
"getLatestRelease": [MockFunction],
|
||||
"getOrganizations": [MockFunction],
|
||||
"getRecentCommits": [MockFunction],
|
||||
"getRelease": [MockFunction],
|
||||
"getReleases": [MockFunction],
|
||||
"getRepoPath": [MockFunction],
|
||||
"getRepositories": [MockFunction],
|
||||
"getRepository": [MockFunction],
|
||||
"githubAuthApi": Object {
|
||||
"getAccessToken": [MockFunction],
|
||||
},
|
||||
"getUsername": [MockFunction],
|
||||
"patch": Object {
|
||||
"createCherryPickCommit": [MockFunction],
|
||||
"createReference": [MockFunction],
|
||||
@@ -48,29 +46,14 @@ describe('testHelpers', () => {
|
||||
"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",
|
||||
"owner": "mock_owner",
|
||||
"repo": "mock_repo",
|
||||
"versioningStrategy": "calver",
|
||||
},
|
||||
"mockDefaultBranch": "mock_defaultBranch",
|
||||
@@ -80,12 +63,11 @@ describe('testHelpers', () => {
|
||||
"releaseName": "Version 1.2.3",
|
||||
},
|
||||
"mockRcRelease": Object {
|
||||
"body": "mock_body",
|
||||
"html_url": "mock_release_html_url",
|
||||
"htmlUrl": "mock_release_html_url",
|
||||
"id": 1,
|
||||
"prerelease": true,
|
||||
"tag_name": "rc-2020.01.01_1",
|
||||
"target_commitish": "rc/1.2.3",
|
||||
"tagName": "rc-2020.01.01_1",
|
||||
"targetCommitish": "rc/1.2.3",
|
||||
},
|
||||
"mockRecentCommits": Array [
|
||||
Object {
|
||||
@@ -128,12 +110,11 @@ describe('testHelpers', () => {
|
||||
"name": "rc/1.2.3",
|
||||
},
|
||||
"mockReleaseVersion": Object {
|
||||
"body": "mock_body",
|
||||
"html_url": "mock_release_html_url",
|
||||
"htmlUrl": "mock_release_html_url",
|
||||
"id": 1,
|
||||
"prerelease": false,
|
||||
"tag_name": "version-2020.01.01_1",
|
||||
"target_commitish": "rc/1.2.3",
|
||||
"tagName": "version-2020.01.01_1",
|
||||
"targetCommitish": "rc/1.2.3",
|
||||
},
|
||||
"mockSelectedPatchCommit": Object {
|
||||
"author": Object {
|
||||
@@ -148,11 +129,8 @@ describe('testHelpers', () => {
|
||||
"sha": "mock_latestCommit_sha",
|
||||
},
|
||||
"mockSemverProject": Object {
|
||||
"github": Object {
|
||||
"org": "mock_org",
|
||||
"repo": "mock_repo",
|
||||
},
|
||||
"name": "mock_name",
|
||||
"owner": "mock_owner",
|
||||
"repo": "mock_repo",
|
||||
"versioningStrategy": "semver",
|
||||
},
|
||||
"mockTagParts": Object {
|
||||
|
||||
@@ -17,10 +17,8 @@
|
||||
import { CalverTagParts } from '../helpers/tagParts/getCalverTagParts';
|
||||
import { getRcGitHubInfo } from '../cards/createRc/getRcGitHubInfo';
|
||||
import {
|
||||
GhCompareCommitsResponse,
|
||||
GhCreateCommitResponse,
|
||||
GhCreateReferenceResponse,
|
||||
GhCreateReleaseResponse,
|
||||
GhCreateTagObjectResponse,
|
||||
GhGetBranchResponse,
|
||||
GhGetCommitResponse,
|
||||
@@ -28,24 +26,19 @@ import {
|
||||
GhMergeResponse,
|
||||
GhUpdateReferenceResponse,
|
||||
GhUpdateReleaseResponse,
|
||||
Project,
|
||||
} from '../types/types';
|
||||
import { Project } from '../contexts/ProjectContext';
|
||||
import { ApiMethodRetval, IPluginApiClient } from '../api/PluginApiClient';
|
||||
|
||||
export const mockSemverProject: Project = {
|
||||
github: {
|
||||
org: 'mock_org',
|
||||
repo: 'mock_repo',
|
||||
},
|
||||
name: 'mock_name',
|
||||
owner: 'mock_owner',
|
||||
repo: 'mock_repo',
|
||||
versioningStrategy: 'semver',
|
||||
};
|
||||
|
||||
export const mockCalverProject: Project = {
|
||||
github: {
|
||||
org: 'mock_org',
|
||||
repo: 'mock_repo',
|
||||
},
|
||||
name: 'mock_name',
|
||||
owner: 'mock_owner',
|
||||
repo: 'mock_repo',
|
||||
versioningStrategy: 'calver',
|
||||
};
|
||||
|
||||
@@ -72,23 +65,28 @@ const createMockRelease = ({
|
||||
id = 1,
|
||||
prerelease = false,
|
||||
...rest
|
||||
}: Partial<GhGetReleaseResponse> = {}) =>
|
||||
}: Partial<
|
||||
NonNullable<
|
||||
ApiMethodRetval<IPluginApiClient['getLatestRelease']>['latestRelease']
|
||||
>
|
||||
> = {}) =>
|
||||
({
|
||||
id: 1,
|
||||
body: 'mock_body',
|
||||
html_url: 'mock_release_html_url',
|
||||
htmlUrl: 'mock_release_html_url',
|
||||
prerelease,
|
||||
...rest,
|
||||
} as GhGetReleaseResponse);
|
||||
} as NonNullable<
|
||||
ApiMethodRetval<IPluginApiClient['getLatestRelease']>['latestRelease']
|
||||
>);
|
||||
export const mockRcRelease = createMockRelease({
|
||||
prerelease: true,
|
||||
tag_name: 'rc-2020.01.01_1',
|
||||
target_commitish: 'rc/1.2.3',
|
||||
tagName: 'rc-2020.01.01_1',
|
||||
targetCommitish: 'rc/1.2.3',
|
||||
});
|
||||
export const mockReleaseVersion = createMockRelease({
|
||||
prerelease: false,
|
||||
tag_name: 'version-2020.01.01_1',
|
||||
target_commitish: 'rc/1.2.3',
|
||||
tagName: 'version-2020.01.01_1',
|
||||
targetCommitish: 'rc/1.2.3',
|
||||
});
|
||||
|
||||
/**
|
||||
@@ -134,63 +132,39 @@ export const mockSelectedPatchCommit = createMockCommit({
|
||||
/**
|
||||
* MOCK API CLIENT
|
||||
*/
|
||||
export const mockApiClient = {
|
||||
pluginApiClient: {
|
||||
getOctokit: jest.fn(),
|
||||
baseUrl: 'http://mock_base_url.hehe',
|
||||
},
|
||||
|
||||
export const mockApiClient: IPluginApiClient = {
|
||||
getHost: jest.fn(() => 'github.com'),
|
||||
getRepoPath: jest.fn(() => 'erikengervall/playground'),
|
||||
|
||||
getOrganizations: jest.fn(),
|
||||
getRepositories: jest.fn(),
|
||||
getUsername: jest.fn(),
|
||||
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,
|
||||
}),
|
||||
getLatestRelease: jest.fn(), // TODO:
|
||||
getRepository: jest.fn(),
|
||||
getLatestCommit: jest.fn().mockResolvedValue({
|
||||
latestCommit: createMockCommit({ node_id: 'mock_latest_commit' }),
|
||||
}),
|
||||
getOctokit: () => ({
|
||||
octokit: {
|
||||
request: jest.fn(),
|
||||
},
|
||||
getBranch: jest.fn().mockResolvedValue({
|
||||
branch: mockReleaseBranch,
|
||||
}),
|
||||
getProject: jest.fn(),
|
||||
|
||||
getRepository: jest.fn(),
|
||||
githubAuthApi: {
|
||||
getAccessToken: jest.fn(),
|
||||
},
|
||||
createRc: {
|
||||
createRef: jest.fn().mockResolvedValue({
|
||||
createdRef: {
|
||||
ref: 'mock_createRef_ref',
|
||||
} as GhCreateReferenceResponse,
|
||||
}),
|
||||
ref: 'mock_createRef_ref',
|
||||
} as NonNullable<ApiMethodRetval<IPluginApiClient['createRc']['createRef']>>),
|
||||
createRelease: jest.fn().mockResolvedValue({
|
||||
createReleaseResponse: {
|
||||
name: 'mock_createRelease_name',
|
||||
html_url: 'mock_createRelease_html_url',
|
||||
tag_name: 'mock_createRelease_tag_name',
|
||||
} as GhCreateReleaseResponse,
|
||||
}),
|
||||
htmlUrl: 'mock_createRelease_html_url',
|
||||
tagName: 'mock_createRelease_tag_name',
|
||||
},
|
||||
} as NonNullable<ApiMethodRetval<IPluginApiClient['createRc']['createRelease']>>),
|
||||
getComparison: jest.fn().mockResolvedValue({
|
||||
comparison: {
|
||||
html_url: 'mock_compareCommits_html_url',
|
||||
ahead_by: 1,
|
||||
} as GhCompareCommitsResponse,
|
||||
}),
|
||||
htmlUrl: 'mock_compareCommits_html_url',
|
||||
aheadBy: 1,
|
||||
} as NonNullable<ApiMethodRetval<IPluginApiClient['createRc']['getComparison']>>),
|
||||
},
|
||||
patch: {
|
||||
createCherryPickCommit: jest.fn().mockResolvedValue({
|
||||
@@ -252,5 +226,5 @@ export const mockApiClient = {
|
||||
} as GhGetReleaseResponse,
|
||||
}),
|
||||
},
|
||||
project: mockSemverProject,
|
||||
} as any;
|
||||
// project: mockSemverProject,
|
||||
};
|
||||
|
||||
@@ -21,7 +21,7 @@ interface ComponentConfig<Args = void> {
|
||||
|
||||
export interface ComponentConfigCreateRcSuccessCbArgs {
|
||||
gitHubReleaseUrl: string;
|
||||
gitHubReleaseName: string;
|
||||
gitHubReleaseName: string | null;
|
||||
comparisonUrl: string;
|
||||
previousTag?: string;
|
||||
createdTag: string;
|
||||
@@ -30,7 +30,7 @@ export type ComponentConfigCreateRc = ComponentConfig<ComponentConfigCreateRcSuc
|
||||
|
||||
export interface ComponentConfigPromoteRcSuccessCbArgs {
|
||||
gitHubReleaseUrl: string;
|
||||
gitHubReleaseName: string;
|
||||
gitHubReleaseName: string | null;
|
||||
previousTagUrl: string;
|
||||
previousTag: string;
|
||||
updatedTagUrl: string;
|
||||
|
||||
Reference in New Issue
Block a user