refactor: Update GitHub token handling in Config.ts

This commit is contained in:
Simon Larsen
2024-06-12 11:59:22 +01:00
parent 64acf372d6
commit 42253e4e50
14 changed files with 267 additions and 119 deletions

View File

@@ -1,6 +1,6 @@
enum CodeRepositoryType {
GitHub = "GitHub",
GitLab = "GitLab",
GitHub = 'GitHub',
GitLab = 'GitLab',
}
export default CodeRepositoryType;
export default CodeRepositoryType;

View File

@@ -1,11 +1,16 @@
import GenericObject from "../Types/GenericObject";
import GenericObject from '../Types/GenericObject';
export default class EnumUtil {
public static isValidEnumValue<T extends GenericObject>(enumType: T, value: any): boolean {
export default class EnumUtil {
public static isValidEnumValue<T extends GenericObject>(
enumType: T,
value: any
): boolean {
return this.getValues(enumType).includes(value);
}
public static getValues<T extends GenericObject>(enumType: T): Array<string> {
public static getValues<T extends GenericObject>(
enumType: T
): Array<string> {
return Object.values(enumType);
}
}
}

View File

@@ -1,4 +1,3 @@
import ServiceRepository from 'Model/Models/ServiceRepository';
import UserMiddleware from '../Middleware/UserAuthorization';
import CodeRepositoryService, {
Service as CodeRepositoryServiceType,
@@ -11,10 +10,11 @@ import {
} from '../Utils/Express';
import Response from '../Utils/Response';
import BaseAPI from './BaseAPI';
import { LIMIT_PER_PROJECT } from 'Common/Types/Database/LimitMax';
import BadDataException from 'Common/Types/Exception/BadDataException';
import ObjectID from 'Common/Types/ObjectID';
import CodeRepository from 'Model/Models/CodeRepository';
import { LIMIT_PER_PROJECT } from 'Common/Types/Database/LimitMax';
import ServiceRepository from 'Model/Models/ServiceRepository';
export default class CodeRepositoryAPI extends BaseAPI<
CodeRepository,
@@ -54,40 +54,41 @@ export default class CodeRepositoryAPI extends BaseAPI<
},
});
if(!codeRepository) {
if (!codeRepository) {
throw new BadDataException('Code repository not found');
}
const servicesRepository: Array<ServiceRepository> = await ServiceRepositoryService.findBy({
query: {
codeRepositoryId: codeRepository.id!,
enablePullRequests: true,
},
select: {
serviceCatalog: {
name: true,
_id: true,
const servicesRepository: Array<ServiceRepository> =
await ServiceRepositoryService.findBy({
query: {
codeRepositoryId: codeRepository.id!,
enablePullRequests: true,
},
servicePathInRepository: true,
limitNumberOfOpenPullRequestsCount: true,
},
limit: LIMIT_PER_PROJECT,
skip: 0,
props: {
isRoot: true,
},
});
select: {
serviceCatalog: {
name: true,
_id: true,
},
servicePathInRepository: true,
limitNumberOfOpenPullRequestsCount: true,
},
limit: LIMIT_PER_PROJECT,
skip: 0,
props: {
isRoot: true,
},
});
return Response.sendJsonObjectResponse(
req,
res,
{
"codeRepository": CodeRepository.toJSON(codeRepository, CodeRepository),
"servicesRepository": ServiceRepository.toJSONArray(servicesRepository, ServiceRepository),
}
);
return Response.sendJsonObjectResponse(req, res, {
codeRepository: CodeRepository.toJSON(
codeRepository,
CodeRepository
),
servicesRepository: ServiceRepository.toJSONArray(
servicesRepository,
ServiceRepository
),
});
} catch (err) {
next(err);
}

View File

@@ -1,17 +0,0 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class MigrationName1718186569787 implements MigrationInterface {
public name = 'MigrationName1718186569787';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "OnCallDutyPolicyScheduleLayer" ALTER COLUMN "restrictionTimes" SET DEFAULT '{"_type":"RestrictionTimes","value":{"restictionType":"None","dayRestrictionTimes":null,"weeklyRestrictionTimes":[]}}'`
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "CodeRepository" DROP COLUMN "mainBranchName"`
);
}
}

View File

@@ -0,0 +1,23 @@
import { MigrationInterface, QueryRunner } from 'typeorm';
export class MigrationName1718188920011 implements MigrationInterface {
public name = 'MigrationName1718188920011';
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "CodeRepository" ADD "mainBranchName" character varying(100) NOT NULL DEFAULT 'master'`
);
await queryRunner.query(
`ALTER TABLE "CodeRepository" ADD "repositoryHostedAt" character varying(100) NOT NULL DEFAULT 'GitHub'`
);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(
`ALTER TABLE "CodeRepository" DROP COLUMN "repositoryHostedAt"`
);
await queryRunner.query(
`ALTER TABLE "CodeRepository" DROP COLUMN "mainBranchName"`
);
}
}

View File

@@ -9,7 +9,7 @@ import { MigrationName1718101665865 } from './1718101665865-MigrationName';
import { MigrationName1718119926223 } from './1718119926223-MigrationName';
import { MigrationName1718124277321 } from './1718124277321-MigrationName';
import { MigrationName1718126316684 } from './1718126316684-MigrationName';
import { MigrationName1718186569787 } from './1718186569787-MigrationName';
import { MigrationName1718188920011 } from './1718188920011-MigrationName';
export default [
InitialMigration,
@@ -23,5 +23,5 @@ export default [
MigrationName1718119926223,
MigrationName1718124277321,
MigrationName1718126316684,
MigrationName1718186569787,
MigrationName1718188920011,
];

View File

@@ -1,7 +1,6 @@
import { DatabaseName } from '../../EnvironmentConfig';
import DatabaseType from 'Common/Types/DatabaseType';
import ProdDataSourceOptions from './DataSourceOptions';
import Faker from 'Common/Utils/Faker';
import Entities from 'Model/Models/Index';
import { DataSourceOptions } from 'typeorm';
type GetTestDataSourceOptions = () => DataSourceOptions;
@@ -10,15 +9,13 @@ const getTestDataSourceOptions: GetTestDataSourceOptions =
(): DataSourceOptions => {
// we use process.env values directly here because it can change during test runs and we need to get the latest values.
return {
type: DatabaseType.Postgres,
...ProdDataSourceOptions,
host: process.env['DATABASE_HOST'] || 'localhost',
port: parseInt(process.env['DATABASE_PORT']?.toString() || '5432'),
username: process.env['DATABASE_USERNAME'] || 'postgres',
password: process.env['DATABASE_PASSWORD'] || 'password',
database: DatabaseName + Faker.randomNumbers(16),
entities: Entities,
synchronize: true,
};
} as DataSourceOptions;
};
export default getTestDataSourceOptions;

View File

@@ -1,7 +1,4 @@
import URL from 'Common/Types/API/URL';
import CodeRepositoryType from 'Common/Types/CodeRepository/CodeRepositoryType';
import BadDataException from 'Common/Types/Exception/BadDataException';
import EnumUtil from 'Common/Utils/Enum';
type GetStringFunction = () => string;
type GetURLFunction = () => URL;
@@ -20,29 +17,7 @@ export const GetLocalRepositoryPath: GetStringFunction = (): string => {
return process.env['ONEUPTIME_LOCAL_REPOSITORY_PATH'] || '/repository';
};
export const GetRepositoryType: GetStringFunction = (): CodeRepositoryType => {
const repoType: string | undefined = process.env['REPOSITORY_TYPE'];
if(!repoType) {
return CodeRepositoryType.GitHub;
}
if(EnumUtil.isValidEnumValue(CodeRepositoryType, repoType)) {
return repoType as CodeRepositoryType;
}
// check if the repository type is valid and is from the values in the enum.
throw new BadDataException(`Invalid Repository Type ${repoType}. It should be one of ${EnumUtil.getValues(CodeRepositoryType).join(', ')}`);
};
export const GetGitHubToken: GetStringFunction = (): string => {
const token: string = process.env['GITHUB_TOKEN'] || '';
if(GetRepositoryType() === CodeRepositoryType.GitHub && !token) {
throw new BadDataException('GitHub Token is required for GitHub Repository');
}
return token;
}
};

View File

@@ -1,23 +1,19 @@
import { GetLocalRepositoryPath } from './Config';
import CodeRepositoryUtil, { CodeRepositoryResult } from './Utils/CodeRepository';
import { CodeRepositoryResult } from './Utils/CodeRepository';
import InitUtil from './Utils/Init';
import Dictionary from 'Common/Types/Dictionary';
import { PromiseVoidFunction } from 'Common/Types/FunctionTypes';
import CodeRepositoryCommonServerUtil from 'CommonServer/Utils/CodeRepository/CodeRepository';
import CodeRepositoryFile from 'CommonServer/Utils/CodeRepository/CodeRepositoryFile';
import logger from 'CommonServer/Utils/Logger';
import dotenv from 'dotenv';
import InitUtil from './Utils/Init';
dotenv.config();
logger.info('OneUptime Copilot is starting...');
const init: PromiseVoidFunction = async (): Promise<void> => {
await InitUtil.validate(); // validate all the configurations
const codeRepositoryResult: CodeRepositoryResult =
await CodeRepositoryUtil.getCodeRepository();
const codeRepositoryResult: CodeRepositoryResult = await InitUtil.init();
const allFiles: Dictionary<CodeRepositoryFile> =
await CodeRepositoryCommonServerUtil.getFilesInDirectoryRecursive({

View File

@@ -15,7 +15,13 @@ export interface CodeRepositoryResult {
}
export default class CodeRepositoryUtil {
public static async getCodeRepository(): Promise<CodeRepositoryResult> {
public static codeRepositoryResult: CodeRepositoryResult | null = null;
public static async getCodeRepositoryResult(): Promise<CodeRepositoryResult> {
if (this.codeRepositoryResult) {
return this.codeRepositoryResult;
}
const repositorySecretKey: string = GetRepositorySecretKey();
if (!repositorySecretKey) {
@@ -44,14 +50,14 @@ export default class CodeRepositoryUtil {
CodeRepositoryModel
) as CodeRepositoryModel;
const servicesRepository: Array<ServiceRepository> =
(codeRepositoryResult.data['servicesRepository'] as JSONArray).map(
(serviceRepository: JSONObject) =>
ServiceRepository.fromJSON(
serviceRepository,
ServiceRepository
) as ServiceRepository
);
const servicesRepository: Array<ServiceRepository> = (
codeRepositoryResult.data['servicesRepository'] as JSONArray
).map((serviceRepository: JSONObject) => {
return ServiceRepository.fromJSON(
serviceRepository,
ServiceRepository
) as ServiceRepository;
});
if (!codeRepository) {
throw new BadDataException(
@@ -68,14 +74,38 @@ export default class CodeRepositoryUtil {
logger.info(`Code Repository found: ${codeRepository.name}`);
logger.info('Services found in the repository:');
servicesRepository.forEach((serviceRepository: ServiceRepository) => {
logger.info(`- ${serviceRepository.serviceCatalog?.name}`);
});
return {
this.codeRepositoryResult = {
codeRepository,
servicesRepository,
};
return this.codeRepositoryResult;
}
public static async getCodeRepository(): Promise<CodeRepositoryModel> {
if (!this.codeRepositoryResult) {
const result: CodeRepositoryResult =
await this.getCodeRepositoryResult();
return result.codeRepository;
}
return this.codeRepositoryResult.codeRepository;
}
public static async getServiceRepositories(): Promise<
Array<ServiceRepository>
> {
if (!this.codeRepositoryResult) {
const result: CodeRepositoryResult =
await this.getCodeRepositoryResult();
return result.servicesRepository;
}
return this.codeRepositoryResult.servicesRepository;
}
}

View File

@@ -1,18 +1,29 @@
import CodeRepositoryType from "Common/Types/CodeRepository/CodeRepositoryType";
import { GetGitHubToken, GetRepositorySecretKey, GetRepositoryType } from "../Config";
import BadDataException from "Common/Types/Exception/BadDataException";
import { GetGitHubToken, GetRepositorySecretKey } from '../Config';
import CodeRepositoryUtil, { CodeRepositoryResult } from './CodeRepository';
import CodeRepositoryType from 'Common/Types/CodeRepository/CodeRepositoryType';
import BadDataException from 'Common/Types/Exception/BadDataException';
export default class InitUtil {
public static async validate(): Promise<void> {
public static async init(): Promise<CodeRepositoryResult> {
if (!GetRepositorySecretKey()) {
throw new BadDataException('Repository Secret Key is required');
}
const codeRepositoryResult: CodeRepositoryResult =
await CodeRepositoryUtil.getCodeRepositoryResult();
// Check if the repository type is GitHub and the GitHub token is provided
if(GetRepositoryType() === CodeRepositoryType.GitHub && !GetGitHubToken()){
throw new BadDataException("GitHub token is required");
if (
codeRepositoryResult.codeRepository.repositoryHostedAt ===
CodeRepositoryType.GitHub &&
!GetGitHubToken()
) {
throw new BadDataException(
'GitHub token is required for this repository. Please provide the GitHub token in the environment variables.'
);
}
if(!GetRepositorySecretKey()){
throw new BadDataException("Repository Secret Key is required");
}
return codeRepositoryResult;
}
}
}

View File

@@ -1,9 +1,11 @@
import LabelsElement from '../../../../Components/Label/Labels';
import PageComponentProps from '../../../PageComponentProps';
import CodeRepositoryType from 'Common/Types/CodeRepository/CodeRepositoryType';
import ObjectID from 'Common/Types/ObjectID';
import FormFieldSchemaType from 'CommonUI/src/Components/Forms/Types/FormFieldSchemaType';
import CardModelDetail from 'CommonUI/src/Components/ModelDetail/CardModelDetail';
import FieldType from 'CommonUI/src/Components/Types/FieldType';
import DropdownUtil from 'CommonUI/src/Utils/Dropdown';
import Navigation from 'CommonUI/src/Utils/Navigation';
import CodeRepository from 'Model/Models/CodeRepository';
import Label from 'Model/Models/Label';
@@ -28,6 +30,10 @@ const StatusPageView: FunctionComponent<PageComponentProps> = (
title: 'Repository Info',
id: 'repository-info',
},
{
title: 'Details',
id: 'details',
},
{
title: 'Labels',
id: 'labels',
@@ -58,6 +64,34 @@ const StatusPageView: FunctionComponent<PageComponentProps> = (
required: true,
placeholder: 'Description',
},
{
field: {
mainBranchName: true,
},
title: 'Main Branch Name',
fieldType: FormFieldSchemaType.Text,
required: true,
placeholder: 'master',
validation: {
minLength: 2,
noSpaces: true,
noSpecialCharacters: true,
},
stepId: 'details',
},
{
field: {
repositoryHostedAt: true,
},
title: 'Repository Hosted At',
fieldType: FormFieldSchemaType.Dropdown,
required: true,
dropdownOptions:
DropdownUtil.getDropdownOptionsFromEnum(
CodeRepositoryType
),
stepId: 'details',
},
{
field: {
labels: true,
@@ -118,6 +152,18 @@ const StatusPageView: FunctionComponent<PageComponentProps> = (
},
title: 'Description',
},
{
field: {
mainBranchName: true,
},
title: 'Main Branch Name',
},
{
field: {
repositoryHostedAt: true,
},
title: 'Repository Hosted At',
},
{
field: {
secretToken: true,

View File

@@ -4,10 +4,12 @@ import PageMap from '../../Utils/PageMap';
import RouteMap, { RouteUtil } from '../../Utils/RouteMap';
import PageComponentProps from '../PageComponentProps';
import Route from 'Common/Types/API/Route';
import CodeRepositoryType from 'Common/Types/CodeRepository/CodeRepositoryType';
import FormFieldSchemaType from 'CommonUI/src/Components/Forms/Types/FormFieldSchemaType';
import ModelTable from 'CommonUI/src/Components/ModelTable/ModelTable';
import Page from 'CommonUI/src/Components/Page/Page';
import FieldType from 'CommonUI/src/Components/Types/FieldType';
import DropdownUtil from 'CommonUI/src/Utils/Dropdown';
import Navigation from 'CommonUI/src/Utils/Navigation';
import CodeRepository from 'Model/Models/CodeRepository';
import Label from 'Model/Models/Label';
@@ -55,6 +57,16 @@ const CodeRepositoryPage: FunctionComponent<PageComponentProps> = (
}
showViewIdButton={true}
noItemsMessage={'No repositories found.'}
formSteps={[
{
title: 'Repository Info',
id: 'repository-info',
},
{
title: 'Details',
id: 'details',
},
]}
formFields={[
{
field: {
@@ -67,6 +79,7 @@ const CodeRepositoryPage: FunctionComponent<PageComponentProps> = (
validation: {
minLength: 2,
},
stepId: 'repository-info',
},
{
field: {
@@ -76,6 +89,35 @@ const CodeRepositoryPage: FunctionComponent<PageComponentProps> = (
fieldType: FormFieldSchemaType.LongText,
required: true,
placeholder: 'Description',
stepId: 'repository-info',
},
{
field: {
mainBranchName: true,
},
title: 'Main Branch Name',
fieldType: FormFieldSchemaType.Text,
required: true,
placeholder: 'master',
validation: {
minLength: 2,
noSpaces: true,
noSpecialCharacters: true,
},
stepId: 'details',
},
{
field: {
repositoryHostedAt: true,
},
title: 'Repository Hosted At',
fieldType: FormFieldSchemaType.Dropdown,
required: true,
dropdownOptions:
DropdownUtil.getDropdownOptionsFromEnum(
CodeRepositoryType
),
stepId: 'details',
},
]}
showRefreshButton={true}

View File

@@ -4,6 +4,7 @@ import User from './User';
import BaseModel from 'Common/Models/BaseModel';
import Route from 'Common/Types/API/Route';
import { PlanSelect } from 'Common/Types/Billing/SubscriptionPlan';
import CodeRepositoryType from 'Common/Types/CodeRepository/CodeRepositoryType';
import ColumnAccessControl from 'Common/Types/Database/AccessControl/ColumnAccessControl';
import TableAccessControl from 'Common/Types/Database/AccessControl/TableAccessControl';
import TableBillingAccessControl from 'Common/Types/Database/AccessControl/TableBillingAccessControl';
@@ -493,6 +494,44 @@ export default class CodeRepository extends BaseModel {
nullable: false,
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
default: 'master',
})
public mainBranchName?: string = undefined;
@ColumnAccessControl({
create: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.CreateCodeRepository,
],
read: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.ProjectMember,
Permission.ReadCodeRepository,
],
update: [
Permission.ProjectOwner,
Permission.ProjectAdmin,
Permission.ProjectMember,
Permission.EditCodeRepository,
],
})
@TableColumn({
required: true,
type: TableColumnType.ShortText,
canReadOnRelationQuery: true,
title: 'Repository Hosted At',
description:
'Where is this repository hosted at? GitHub, GitLab, Bitbucket, etc.',
})
@Column({
nullable: false,
type: ColumnType.ShortText,
length: ColumnLength.ShortText,
default: CodeRepositoryType.GitHub,
})
public repositoryHostedAt?: CodeRepositoryType = undefined;
}