Compare commits
	
		
			16 Commits
		
	
	
		
			single-dep
			...
			feat-confi
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| f47a20f942 | |||
| 6b9f846154 | |||
| 3ba8fc9759 | |||
| d5f49531e9 | |||
| 38dd05c4be | |||
| 8467a42b3b | |||
| a299958fcb | |||
| f07e18f71d | |||
| eb7adc0ef2 | |||
| 122dca689d | |||
| fcca1508eb | |||
| 
						 | 
					34cfc71a18 | ||
| de2e9fa8c4 | |||
| 0f1466bf91 | |||
| 
						 | 
					ed71d83581 | ||
| 
						 | 
					574e7ecae7 | 
							
								
								
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							@@ -33,4 +33,6 @@ lerna-debug.log*
 | 
			
		||||
!.vscode/launch.json
 | 
			
		||||
!.vscode/extensions.json
 | 
			
		||||
 | 
			
		||||
/config.yml
 | 
			
		||||
/config.yml
 | 
			
		||||
tsconfig.build.tsbuildinfo
 | 
			
		||||
.eslintcache
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								.husky/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								.husky/.gitignore
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
_
 | 
			
		||||
							
								
								
									
										5
									
								
								.husky/pre-commit
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										5
									
								
								.husky/pre-commit
									
									
									
									
									
										Executable file
									
								
							@@ -0,0 +1,5 @@
 | 
			
		||||
#!/bin/sh
 | 
			
		||||
. "$(dirname "$0")/_/husky.sh"
 | 
			
		||||
 | 
			
		||||
npx lint-staged
 | 
			
		||||
npm test
 | 
			
		||||
							
								
								
									
										14481
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
							
						
						
									
										14481
									
								
								package-lock.json
									
									
									
										generated
									
									
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										14
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										14
									
								
								package.json
									
									
									
									
									
								
							@@ -18,7 +18,8 @@
 | 
			
		||||
    "test:watch": "jest --watch",
 | 
			
		||||
    "test:cov": "jest --coverage",
 | 
			
		||||
    "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
 | 
			
		||||
    "test:e2e": "jest --config ./test/jest-e2e.json"
 | 
			
		||||
    "test:e2e": "jest --config ./test/jest-e2e.json",
 | 
			
		||||
    "prepare": "husky install"
 | 
			
		||||
  },
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@golevelup/nestjs-rabbitmq": "^1.16.1",
 | 
			
		||||
@@ -36,9 +37,10 @@
 | 
			
		||||
    "body-parser": "^1.19.0",
 | 
			
		||||
    "class-transformer": "^0.3.2",
 | 
			
		||||
    "class-validator": "^0.13.1",
 | 
			
		||||
    "configuration": "file:../configuration",
 | 
			
		||||
    "debug": "^4.3.1",
 | 
			
		||||
    "graphql": "^15.5.0",
 | 
			
		||||
    "graphql-tools": "^7.0.2",
 | 
			
		||||
    "graphql-tools": "^8.1.0",
 | 
			
		||||
    "ioredis": "^4.25.0",
 | 
			
		||||
    "jose": "^3.14.0",
 | 
			
		||||
    "js-yaml": "^4.0.0",
 | 
			
		||||
@@ -48,6 +50,7 @@
 | 
			
		||||
    "observable-to-async-generator": "^1.0.1-rc",
 | 
			
		||||
    "pg": "^8.5.1",
 | 
			
		||||
    "pino-pretty": "^4.7.1",
 | 
			
		||||
    "pm2": "^5.1.0",
 | 
			
		||||
    "ramda": "^0.27.1",
 | 
			
		||||
    "reflect-metadata": "^0.1.13",
 | 
			
		||||
    "rimraf": "^3.0.2",
 | 
			
		||||
@@ -56,7 +59,7 @@
 | 
			
		||||
    "typeorm": "^0.2.30"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@nestjs/cli": "^7.5.7",
 | 
			
		||||
    "@nestjs/cli": "^7.6.0",
 | 
			
		||||
    "@nestjs/schematics": "^7.1.3",
 | 
			
		||||
    "@nestjs/testing": "^7.5.1",
 | 
			
		||||
    "@types/body-parser": "^1.19.0",
 | 
			
		||||
@@ -73,7 +76,9 @@
 | 
			
		||||
    "eslint": "^7.12.1",
 | 
			
		||||
    "eslint-config-prettier": "7.2.0",
 | 
			
		||||
    "eslint-plugin-prettier": "^3.1.4",
 | 
			
		||||
    "husky": "^7.0.2",
 | 
			
		||||
    "jest": "^26.6.3",
 | 
			
		||||
    "lint-staged": "^11.1.2",
 | 
			
		||||
    "prettier": "^2.1.2",
 | 
			
		||||
    "supertest": "^6.0.0",
 | 
			
		||||
    "ts-jest": "^26.4.3",
 | 
			
		||||
@@ -101,5 +106,8 @@
 | 
			
		||||
    },
 | 
			
		||||
    "coverageDirectory": "../coverage",
 | 
			
		||||
    "testEnvironment": "node"
 | 
			
		||||
  },
 | 
			
		||||
  "lint-staged": {
 | 
			
		||||
    "{src,apps,libs,test}/**/*.ts": "eslint --cache --fix"
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,7 @@ import { LoggerModule } from 'nestjs-pino';
 | 
			
		||||
import { EtcdModule } from 'nestjs-etcd';
 | 
			
		||||
import pinoPretty from 'pino-pretty';
 | 
			
		||||
import { fromPairs, map, pipe, toPairs } from 'ramda';
 | 
			
		||||
import { ConfigurationsModule } from './configurations/configurations.module';
 | 
			
		||||
 | 
			
		||||
@Module({
 | 
			
		||||
  imports: [
 | 
			
		||||
@@ -64,7 +65,7 @@ import { fromPairs, map, pipe, toPairs } from 'ramda';
 | 
			
		||||
        playground: true,
 | 
			
		||||
        autoSchemaFile: true,
 | 
			
		||||
        installSubscriptionHandlers: true,
 | 
			
		||||
        context: ({ req, connection, ...args }) => {
 | 
			
		||||
        context: ({ req, connection }) => {
 | 
			
		||||
          return connection ? { req: connection.context } : { req };
 | 
			
		||||
        },
 | 
			
		||||
        subscriptions: {
 | 
			
		||||
@@ -106,6 +107,7 @@ import { fromPairs, map, pipe, toPairs } from 'ramda';
 | 
			
		||||
    }),
 | 
			
		||||
    WebhooksModule,
 | 
			
		||||
    CommonsModule,
 | 
			
		||||
    ConfigurationsModule,
 | 
			
		||||
  ],
 | 
			
		||||
  controllers: [AppController],
 | 
			
		||||
  providers: [AppService, AppResolver],
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,9 @@
 | 
			
		||||
import { Module } from '@nestjs/common';
 | 
			
		||||
import { Global, Module } from '@nestjs/common';
 | 
			
		||||
import { PasswordConverter } from './services/password-converter';
 | 
			
		||||
import { RedisMutexModule } from './redis-mutex/redis-mutex.module';
 | 
			
		||||
import { AuthModule } from '@nestjs-lib/auth';
 | 
			
		||||
 | 
			
		||||
@Global()
 | 
			
		||||
@Module({
 | 
			
		||||
  imports: [RedisMutexModule, AuthModule],
 | 
			
		||||
  providers: [PasswordConverter],
 | 
			
		||||
 
 | 
			
		||||
@@ -13,9 +13,9 @@ export class ApplicationException extends Error {
 | 
			
		||||
      this.error = message.error;
 | 
			
		||||
      this.message = message.message as any;
 | 
			
		||||
    } else if (typeof message === 'string') {
 | 
			
		||||
      super((message as unknown) as any);
 | 
			
		||||
      super(message as unknown as any);
 | 
			
		||||
    } else {
 | 
			
		||||
      super((message as unknown) as any);
 | 
			
		||||
      super(message as unknown as any);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -19,12 +19,16 @@ export class HttpExceptionFilter implements ExceptionFilter {
 | 
			
		||||
      case 'graphql': {
 | 
			
		||||
        const errorName = exception.message;
 | 
			
		||||
        const extensions: Record<string, any> = {};
 | 
			
		||||
        const err = exception.getResponse();
 | 
			
		||||
        const err = exception.getResponse() as any;
 | 
			
		||||
        if (typeof err === 'string') {
 | 
			
		||||
          extensions.message = err;
 | 
			
		||||
        } else {
 | 
			
		||||
          Object.assign(extensions, (err as any).extension);
 | 
			
		||||
          extensions.message = (err as any).message;
 | 
			
		||||
          Object.assign(extensions, err.extension);
 | 
			
		||||
          if (typeof err.message === 'string') {
 | 
			
		||||
            extensions.message = err.message;
 | 
			
		||||
          } else {
 | 
			
		||||
            extensions.message = err.error;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        extensions.error = errorName;
 | 
			
		||||
        this.logger.error(extensions);
 | 
			
		||||
 
 | 
			
		||||
@@ -126,6 +126,7 @@ export class BaseDbService<Entity extends AppBaseEntity> extends TypeormHelper {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // eslint-disable-next-line @typescript-eslint/no-unused-vars
 | 
			
		||||
  async canYouRemoveWithIds(ids: string[]): Promise<void> {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										11
									
								
								src/configurations/configurations.module.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								src/configurations/configurations.module.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,11 @@
 | 
			
		||||
import { Configuration } from './entities/configuration.entity';
 | 
			
		||||
import { TypeOrmModule } from '@nestjs/typeorm';
 | 
			
		||||
import { Module } from '@nestjs/common';
 | 
			
		||||
import { ConfigurationsService } from './configurations.service';
 | 
			
		||||
import { ConfigurationsResolver } from './configurations.resolver';
 | 
			
		||||
 | 
			
		||||
@Module({
 | 
			
		||||
  imports: [TypeOrmModule.forFeature([Configuration])],
 | 
			
		||||
  providers: [ConfigurationsResolver, ConfigurationsService],
 | 
			
		||||
})
 | 
			
		||||
export class ConfigurationsModule {}
 | 
			
		||||
							
								
								
									
										30
									
								
								src/configurations/configurations.resolver.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								src/configurations/configurations.resolver.spec.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
import { JwtService } from '@nestjs-lib/auth';
 | 
			
		||||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
import { ConfigurationsResolver } from './configurations.resolver';
 | 
			
		||||
import { ConfigurationsService } from './configurations.service';
 | 
			
		||||
 | 
			
		||||
describe('ConfigurationsResolver', () => {
 | 
			
		||||
  let resolver: ConfigurationsResolver;
 | 
			
		||||
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    const module: TestingModule = await Test.createTestingModule({
 | 
			
		||||
      providers: [
 | 
			
		||||
        ConfigurationsResolver,
 | 
			
		||||
        {
 | 
			
		||||
          provide: ConfigurationsService,
 | 
			
		||||
          useValue: {},
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          provide: JwtService,
 | 
			
		||||
          useValue: {},
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    }).compile();
 | 
			
		||||
 | 
			
		||||
    resolver = module.get<ConfigurationsResolver>(ConfigurationsResolver);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
    expect(resolver).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										39
									
								
								src/configurations/configurations.resolver.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								src/configurations/configurations.resolver.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
			
		||||
import { UnprocessableEntityException } from '@nestjs/common';
 | 
			
		||||
import { GetConfigurationArgs } from './dto/get-configuration.args';
 | 
			
		||||
import { SetConfigurationInput } from './dto/set-configuration.input';
 | 
			
		||||
import { Resolver, Mutation, Args, Query } from '@nestjs/graphql';
 | 
			
		||||
import { ConfigurationsService } from './configurations.service';
 | 
			
		||||
import { Configuration } from './entities/configuration.entity';
 | 
			
		||||
import { any, pipe, values } from 'ramda';
 | 
			
		||||
import { AccountRole, Roles } from '@nestjs-lib/auth';
 | 
			
		||||
 | 
			
		||||
@Roles(AccountRole.admin, AccountRole.super)
 | 
			
		||||
@Resolver(() => Configuration)
 | 
			
		||||
export class ConfigurationsResolver {
 | 
			
		||||
  constructor(private readonly configurationsService: ConfigurationsService) {}
 | 
			
		||||
 | 
			
		||||
  @Mutation(() => Configuration)
 | 
			
		||||
  setConfiguration(
 | 
			
		||||
    @Args('setConfigurationInput', { type: () => SetConfigurationInput })
 | 
			
		||||
    setConfigurationInput: SetConfigurationInput,
 | 
			
		||||
  ) {
 | 
			
		||||
    return this.configurationsService.setConfiguration(setConfigurationInput);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Query(() => Configuration, { nullable: true })
 | 
			
		||||
  getConfiguration(
 | 
			
		||||
    @Args()
 | 
			
		||||
    getConfigurationArgs: GetConfigurationArgs,
 | 
			
		||||
  ) {
 | 
			
		||||
    if (
 | 
			
		||||
      pipe(
 | 
			
		||||
        values,
 | 
			
		||||
        any((value) => !value),
 | 
			
		||||
      )(getConfigurationArgs)
 | 
			
		||||
    ) {
 | 
			
		||||
      throw new UnprocessableEntityException('Must pass a parameter');
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return this.configurationsService.findOneByConditions(getConfigurationArgs);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										49
									
								
								src/configurations/configurations.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										49
									
								
								src/configurations/configurations.service.spec.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,49 @@
 | 
			
		||||
import { getRepositoryToken } from '@nestjs/typeorm';
 | 
			
		||||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
import { ConfigurationsService } from './configurations.service';
 | 
			
		||||
import { Configuration } from './entities/configuration.entity';
 | 
			
		||||
import { IsNull } from 'typeorm';
 | 
			
		||||
import { Etcd3 } from 'etcd3';
 | 
			
		||||
 | 
			
		||||
describe('ConfigurationsService', () => {
 | 
			
		||||
  let service: ConfigurationsService;
 | 
			
		||||
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    const module: TestingModule = await Test.createTestingModule({
 | 
			
		||||
      providers: [
 | 
			
		||||
        ConfigurationsService,
 | 
			
		||||
        {
 | 
			
		||||
          provide: getRepositoryToken(Configuration),
 | 
			
		||||
          useValue: {},
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          provide: Etcd3,
 | 
			
		||||
          useValue: {},
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    }).compile();
 | 
			
		||||
 | 
			
		||||
    service = module.get<ConfigurationsService>(ConfigurationsService);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
    expect(service).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('findOneByConditions', () => {
 | 
			
		||||
    it('should select by projectId only', async () => {
 | 
			
		||||
      const entity = new Configuration();
 | 
			
		||||
      const findOne = jest.fn<any, [any]>(() => Promise.resolve(entity));
 | 
			
		||||
      service['repository'].findOne = findOne;
 | 
			
		||||
 | 
			
		||||
      await expect(
 | 
			
		||||
        service.findOneByConditions({ projectId: 'uuid' }),
 | 
			
		||||
      ).resolves.toEqual(entity);
 | 
			
		||||
 | 
			
		||||
      expect(findOne.mock.calls[0][0]).toMatchObject({
 | 
			
		||||
        projectId: 'uuid',
 | 
			
		||||
        pipelineId: IsNull(),
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										55
									
								
								src/configurations/configurations.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										55
									
								
								src/configurations/configurations.service.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,55 @@
 | 
			
		||||
import { GetConfigurationArgs } from './dto/get-configuration.args';
 | 
			
		||||
import { BaseDbService } from './../commons/services/base-db.service';
 | 
			
		||||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { Configuration } from './entities/configuration.entity';
 | 
			
		||||
import { InjectRepository } from '@nestjs/typeorm';
 | 
			
		||||
import { FindConditions, IsNull, Repository } from 'typeorm';
 | 
			
		||||
import { SetConfigurationInput } from './dto/set-configuration.input';
 | 
			
		||||
import { pick } from 'ramda';
 | 
			
		||||
import { Etcd3 } from 'etcd3';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class ConfigurationsService extends BaseDbService<Configuration> {
 | 
			
		||||
  constructor(
 | 
			
		||||
    @InjectRepository(Configuration)
 | 
			
		||||
    configurationRepository: Repository<Configuration>,
 | 
			
		||||
    private readonly etcd: Etcd3,
 | 
			
		||||
  ) {
 | 
			
		||||
    super(configurationRepository);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async setConfiguration(dto: SetConfigurationInput) {
 | 
			
		||||
    let entity = await this.repository.findOne(
 | 
			
		||||
      pick(['pipelineId', 'projectId'], dto),
 | 
			
		||||
    );
 | 
			
		||||
    if (!entity) {
 | 
			
		||||
      entity = this.repository.create(dto);
 | 
			
		||||
    }
 | 
			
		||||
    entity = await this.repository.save(entity);
 | 
			
		||||
    await this.syncToEtcd(entity);
 | 
			
		||||
    return entity;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async findOneByConditions(dto: FindConditions<GetConfigurationArgs>) {
 | 
			
		||||
    if (dto.projectId && !dto.pipelineId) {
 | 
			
		||||
      dto.pipelineId = IsNull();
 | 
			
		||||
    }
 | 
			
		||||
    return await this.repository.findOne(dto);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  async syncToEtcd({ pipelineId, id }: { pipelineId?: string; id?: string }) {
 | 
			
		||||
    const config = await this.repository.findOneOrFail({
 | 
			
		||||
      where: { pipelineId, id },
 | 
			
		||||
      relations: ['pipeline', 'project'],
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    await this.etcd
 | 
			
		||||
      .put(`share/config/${config.id}`)
 | 
			
		||||
      .value(config.content)
 | 
			
		||||
      .exec();
 | 
			
		||||
    await this.etcd
 | 
			
		||||
      .put(`share/config/${config.pipeline.environment}/${config.project.name}`)
 | 
			
		||||
      .value(config.content)
 | 
			
		||||
      .exec();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										17
									
								
								src/configurations/dto/get-configuration.args.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										17
									
								
								src/configurations/dto/get-configuration.args.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,17 @@
 | 
			
		||||
import { ArgsType } from '@nestjs/graphql';
 | 
			
		||||
import { IsUUID, IsOptional } from 'class-validator';
 | 
			
		||||
 | 
			
		||||
@ArgsType()
 | 
			
		||||
export class GetConfigurationArgs {
 | 
			
		||||
  @IsUUID()
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  pipelineId?: string;
 | 
			
		||||
 | 
			
		||||
  @IsUUID()
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  projectId?: string;
 | 
			
		||||
 | 
			
		||||
  @IsUUID()
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  id?: string;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										26
									
								
								src/configurations/dto/set-configuration.input.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										26
									
								
								src/configurations/dto/set-configuration.input.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,26 @@
 | 
			
		||||
import { ConfigurationLanguage } from './../enums/configuration-language.enum';
 | 
			
		||||
import { IsEnum, IsString, IsUUID, Length, IsOptional } from 'class-validator';
 | 
			
		||||
import { InputType } from '@nestjs/graphql';
 | 
			
		||||
 | 
			
		||||
@InputType()
 | 
			
		||||
export class SetConfigurationInput {
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  @IsUUID()
 | 
			
		||||
  id?: string;
 | 
			
		||||
 | 
			
		||||
  @IsUUID()
 | 
			
		||||
  pipelineId: string;
 | 
			
		||||
 | 
			
		||||
  @IsUUID()
 | 
			
		||||
  projectId: string;
 | 
			
		||||
 | 
			
		||||
  @IsString()
 | 
			
		||||
  content: string;
 | 
			
		||||
 | 
			
		||||
  @IsEnum(ConfigurationLanguage)
 | 
			
		||||
  language: ConfigurationLanguage;
 | 
			
		||||
 | 
			
		||||
  @Length(0, 100)
 | 
			
		||||
  @IsOptional()
 | 
			
		||||
  name = 'Default Configuration';
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										35
									
								
								src/configurations/entities/configuration.entity.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								src/configurations/entities/configuration.entity.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,35 @@
 | 
			
		||||
import { Project } from './../../projects/project.entity';
 | 
			
		||||
import { ConfigurationLanguage } from './../enums/configuration-language.enum';
 | 
			
		||||
import { Pipeline } from './../../pipelines/pipeline.entity';
 | 
			
		||||
import { AppBaseEntity } from './../../commons/entities/app-base-entity';
 | 
			
		||||
import { ObjectType } from '@nestjs/graphql';
 | 
			
		||||
import { Column, Entity, ManyToOne } from 'typeorm';
 | 
			
		||||
 | 
			
		||||
@Entity()
 | 
			
		||||
@ObjectType()
 | 
			
		||||
export class Configuration extends AppBaseEntity {
 | 
			
		||||
  @ManyToOne(() => Pipeline)
 | 
			
		||||
  pipeline: Pipeline;
 | 
			
		||||
 | 
			
		||||
  @Column({ unique: true, nullable: true })
 | 
			
		||||
  pipelineId: string;
 | 
			
		||||
 | 
			
		||||
  @ManyToOne(() => Project)
 | 
			
		||||
  project: Project;
 | 
			
		||||
 | 
			
		||||
  @Column()
 | 
			
		||||
  projectId: string;
 | 
			
		||||
 | 
			
		||||
  @Column({ comment: 'language defined in type field.' })
 | 
			
		||||
  content: string;
 | 
			
		||||
 | 
			
		||||
  @Column({ comment: '配置名称' })
 | 
			
		||||
  name: string;
 | 
			
		||||
 | 
			
		||||
  @Column({
 | 
			
		||||
    type: 'enum',
 | 
			
		||||
    enum: ConfigurationLanguage,
 | 
			
		||||
    comment: 'configuration content language',
 | 
			
		||||
  })
 | 
			
		||||
  language: ConfigurationLanguage;
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										10
									
								
								src/configurations/enums/configuration-language.enum.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								src/configurations/enums/configuration-language.enum.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
import { registerEnumType } from '@nestjs/graphql';
 | 
			
		||||
 | 
			
		||||
export enum ConfigurationLanguage {
 | 
			
		||||
  JavaScript = 'JavaScript',
 | 
			
		||||
  YAML = 'YAML',
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
registerEnumType(ConfigurationLanguage, {
 | 
			
		||||
  name: 'ConfigurationLanguage',
 | 
			
		||||
});
 | 
			
		||||
@@ -5,6 +5,7 @@ import { NestFactory } from '@nestjs/core';
 | 
			
		||||
import { AppModule } from './app.module';
 | 
			
		||||
import { HttpExceptionFilter } from './commons/filters/all.exception-filter';
 | 
			
		||||
import { SanitizePipe } from './commons/pipes/sanitize.pipe';
 | 
			
		||||
import { ServiceRegister } from 'configuration';
 | 
			
		||||
 | 
			
		||||
async function bootstrap() {
 | 
			
		||||
  const app = await NestFactory.create(AppModule, { bodyParser: false });
 | 
			
		||||
@@ -17,6 +18,10 @@ async function bootstrap() {
 | 
			
		||||
  );
 | 
			
		||||
  const httpExceptionFilterLogger = await app.resolve(PinoLogger);
 | 
			
		||||
  app.useGlobalFilters(new HttpExceptionFilter(httpExceptionFilterLogger));
 | 
			
		||||
  await app.listen(configService.get<number>('http.port'));
 | 
			
		||||
  const server = await app.listen(configService.get<number>('http.port', 0));
 | 
			
		||||
  const port = server.address().port;
 | 
			
		||||
  const register = new ServiceRegister({ etcd: { hosts: 'http://rpi:2379' } });
 | 
			
		||||
  register.register('fennec/api', `http://localhost:${port}`);
 | 
			
		||||
  register.register('api.fennec', `http://localhost:${port}`);
 | 
			
		||||
}
 | 
			
		||||
bootstrap();
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { Field, InputType } from '@nestjs/graphql';
 | 
			
		||||
import { InputType } from '@nestjs/graphql';
 | 
			
		||||
import { PipelineUnits } from '../enums/pipeline-units.enum';
 | 
			
		||||
 | 
			
		||||
@InputType()
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import { Field, InputType, Int, ObjectType } from '@nestjs/graphql';
 | 
			
		||||
import { Type } from 'class-transformer';
 | 
			
		||||
import { IsInstance, isInstance, ValidateNested } from 'class-validator';
 | 
			
		||||
import { IsInstance, ValidateNested } from 'class-validator';
 | 
			
		||||
import { WorkUnit } from './work-unit.model';
 | 
			
		||||
 | 
			
		||||
@InputType('WorkUnitMetadataInput')
 | 
			
		||||
 
 | 
			
		||||
@@ -42,9 +42,9 @@ describe('PipelineTaskRunner', () => {
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        logger.handleEvent(event);
 | 
			
		||||
      });
 | 
			
		||||
      expect(message$.pipe(take(1), timeout(100)).toPromise()).rejects.toMatch(
 | 
			
		||||
        'timeout',
 | 
			
		||||
      );
 | 
			
		||||
      await expect(
 | 
			
		||||
        message$.pipe(take(1), timeout(100)).toPromise(),
 | 
			
		||||
      ).rejects.toThrow(/timeout/i);
 | 
			
		||||
    });
 | 
			
		||||
    it('multiple subscribers', async () => {
 | 
			
		||||
      const event = new PipelineTaskEvent();
 | 
			
		||||
@@ -53,13 +53,16 @@ describe('PipelineTaskRunner', () => {
 | 
			
		||||
      const message2$ = logger.getMessage$('test');
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
        logger.handleEvent(event);
 | 
			
		||||
        logger.handleEvent(event);
 | 
			
		||||
      });
 | 
			
		||||
      expect(message$.pipe(take(1), timeout(100)).toPromise()).resolves.toEqual(
 | 
			
		||||
        event,
 | 
			
		||||
      );
 | 
			
		||||
      expect(
 | 
			
		||||
        message2$.pipe(take(1), timeout(100)).toPromise(),
 | 
			
		||||
      ).resolves.toEqual(event);
 | 
			
		||||
      await Promise.all([
 | 
			
		||||
        expect(
 | 
			
		||||
          message$.pipe(take(1), timeout(100)).toPromise(),
 | 
			
		||||
        ).resolves.toEqual(event),
 | 
			
		||||
        expect(
 | 
			
		||||
          message2$.pipe(take(1), timeout(100)).toPromise(),
 | 
			
		||||
        ).resolves.toEqual(event),
 | 
			
		||||
      ]);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,8 @@ import {
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class PipelineTaskLogger implements OnModuleDestroy {
 | 
			
		||||
  private readonly messageSubject = new Subject<PipelineTaskEvent>();
 | 
			
		||||
  private readonly message$: Observable<PipelineTaskEvent> = this.messageSubject.pipe();
 | 
			
		||||
  private readonly message$: Observable<PipelineTaskEvent> =
 | 
			
		||||
    this.messageSubject.pipe();
 | 
			
		||||
 | 
			
		||||
  @RabbitSubscribe({
 | 
			
		||||
    exchange: EXCHANGE_PIPELINE_TASK_FANOUT,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
import { DeployByPm2Service } from './runners/deploy-by-pm2/deploy-by-pm2.service';
 | 
			
		||||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
import { ReposService } from '../repos/repos.service';
 | 
			
		||||
import { PipelineUnits } from './enums/pipeline-units.enum';
 | 
			
		||||
@@ -11,7 +12,7 @@ import { WorkUnitMetadata } from './models/work-unit-metadata.model';
 | 
			
		||||
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
 | 
			
		||||
describe('PipelineTaskRunner', () => {
 | 
			
		||||
  let runner: PipelineTaskRunner;
 | 
			
		||||
  let reposService: ReposService;
 | 
			
		||||
  let deployByPM2Service: DeployByPm2Service;
 | 
			
		||||
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    const module: TestingModule = await Test.createTestingModule({
 | 
			
		||||
@@ -36,11 +37,18 @@ describe('PipelineTaskRunner', () => {
 | 
			
		||||
          provide: AmqpConnection,
 | 
			
		||||
          useValue: {},
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          provide: DeployByPm2Service,
 | 
			
		||||
          useValue: {
 | 
			
		||||
            deploy: () => Promise.resolve(),
 | 
			
		||||
          },
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    }).compile();
 | 
			
		||||
 | 
			
		||||
    reposService = module.get(ReposService);
 | 
			
		||||
    module.get(ReposService);
 | 
			
		||||
    runner = module.get(PipelineTaskRunner);
 | 
			
		||||
    deployByPM2Service = module.get(DeployByPm2Service);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
@@ -65,7 +73,7 @@ describe('PipelineTaskRunner', () => {
 | 
			
		||||
    beforeEach(() => {
 | 
			
		||||
      emitEvent = jest
 | 
			
		||||
        .spyOn(runner, 'emitEvent')
 | 
			
		||||
        .mockImplementation((..._) => Promise.resolve());
 | 
			
		||||
        .mockImplementation(() => Promise.resolve());
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    describe('doTask', () => {
 | 
			
		||||
@@ -75,10 +83,10 @@ describe('PipelineTaskRunner', () => {
 | 
			
		||||
      beforeEach(() => {
 | 
			
		||||
        checkout = jest
 | 
			
		||||
          .spyOn(runner, 'checkout')
 | 
			
		||||
          .mockImplementation((..._) => Promise.resolve('/null'));
 | 
			
		||||
          .mockImplementation(() => Promise.resolve('/null'));
 | 
			
		||||
        doTaskUnit = jest
 | 
			
		||||
          .spyOn(runner, 'doTaskUnit')
 | 
			
		||||
          .mockImplementation((..._) => Promise.resolve());
 | 
			
		||||
          .mockImplementation(() => Promise.resolve());
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      it('only checkout', async () => {
 | 
			
		||||
@@ -171,9 +179,7 @@ describe('PipelineTaskRunner', () => {
 | 
			
		||||
 | 
			
		||||
        doTaskUnit = jest
 | 
			
		||||
          .spyOn(runner, 'doTaskUnit')
 | 
			
		||||
          .mockImplementation((..._) =>
 | 
			
		||||
            Promise.reject(new Error('test error')),
 | 
			
		||||
          );
 | 
			
		||||
          .mockImplementation(() => Promise.reject(new Error('test error')));
 | 
			
		||||
        await runner.doTask(task);
 | 
			
		||||
 | 
			
		||||
        expect(checkout).toBeCalledTimes(1);
 | 
			
		||||
@@ -189,7 +195,7 @@ describe('PipelineTaskRunner', () => {
 | 
			
		||||
      it('success', async () => {
 | 
			
		||||
        const runScript = jest
 | 
			
		||||
          .spyOn(runner, 'runScript')
 | 
			
		||||
          .mockImplementation((..._) => Promise.resolve());
 | 
			
		||||
          .mockImplementation(() => Promise.resolve());
 | 
			
		||||
        const task = new PipelineTask();
 | 
			
		||||
 | 
			
		||||
        const unit = PipelineUnits.test;
 | 
			
		||||
@@ -210,9 +216,7 @@ describe('PipelineTaskRunner', () => {
 | 
			
		||||
      it('failed', async () => {
 | 
			
		||||
        const runScript = jest
 | 
			
		||||
          .spyOn(runner, 'runScript')
 | 
			
		||||
          .mockImplementation((..._) =>
 | 
			
		||||
            Promise.reject(new Error('test error')),
 | 
			
		||||
          );
 | 
			
		||||
          .mockImplementation(() => Promise.reject(new Error('test error')));
 | 
			
		||||
        const task = new PipelineTask();
 | 
			
		||||
 | 
			
		||||
        const unit = PipelineUnits.test;
 | 
			
		||||
@@ -230,7 +234,7 @@ describe('PipelineTaskRunner', () => {
 | 
			
		||||
 | 
			
		||||
    describe('runScript', () => {
 | 
			
		||||
      it('normal', async () => {
 | 
			
		||||
        const spawn = jest.fn((..._: any[]) => ({
 | 
			
		||||
        const spawn = jest.fn<any, any>(() => ({
 | 
			
		||||
          stdout: {
 | 
			
		||||
            on: () => undefined,
 | 
			
		||||
          },
 | 
			
		||||
@@ -256,7 +260,7 @@ describe('PipelineTaskRunner', () => {
 | 
			
		||||
        });
 | 
			
		||||
      });
 | 
			
		||||
      it('failed', async () => {
 | 
			
		||||
        const spawn = jest.fn((..._: any[]) => ({
 | 
			
		||||
        const spawn = jest.fn(() => ({
 | 
			
		||||
          stdout: {
 | 
			
		||||
            on: () => undefined,
 | 
			
		||||
          },
 | 
			
		||||
@@ -291,7 +295,7 @@ describe('PipelineTaskRunner', () => {
 | 
			
		||||
            }, 10);
 | 
			
		||||
          }, 10);
 | 
			
		||||
        });
 | 
			
		||||
        const spawn = jest.fn((..._: any[]) => ({
 | 
			
		||||
        const spawn = jest.fn(() => ({
 | 
			
		||||
          stdout: {
 | 
			
		||||
            on,
 | 
			
		||||
          },
 | 
			
		||||
@@ -304,7 +308,7 @@ describe('PipelineTaskRunner', () => {
 | 
			
		||||
        }));
 | 
			
		||||
 | 
			
		||||
        let emitSuccessCount = 0;
 | 
			
		||||
        jest.spyOn(runner, 'emitEvent').mockImplementation((..._: any[]) => {
 | 
			
		||||
        jest.spyOn(runner, 'emitEvent').mockImplementation(() => {
 | 
			
		||||
          return new Promise((resolve) => {
 | 
			
		||||
            setTimeout(() => {
 | 
			
		||||
              emitSuccessCount++;
 | 
			
		||||
@@ -323,4 +327,25 @@ describe('PipelineTaskRunner', () => {
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('tryRunDeployScript', () => {
 | 
			
		||||
    it('should be call deploy with right args', async () => {
 | 
			
		||||
      const deploy = jest.spyOn(deployByPM2Service, 'deploy');
 | 
			
		||||
      await expect(
 | 
			
		||||
        runner['tryRunDeployScript'](
 | 
			
		||||
          '/test/dir',
 | 
			
		||||
          '@@DEPLOY ecosystem.config.js',
 | 
			
		||||
        ),
 | 
			
		||||
      ).resolves.toBe(true);
 | 
			
		||||
      expect(deploy.mock.calls[0][0]).toEqual('/test/dir/ecosystem.config.js');
 | 
			
		||||
    });
 | 
			
		||||
    it('should return false', async () => {
 | 
			
		||||
      await expect(
 | 
			
		||||
        runner['tryRunDeployScript'](
 | 
			
		||||
          '/test/dir',
 | 
			
		||||
          'pm2 start ecosystem.config.js',
 | 
			
		||||
        ),
 | 
			
		||||
      ).resolves.toBe(false);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
import { DeployByPm2Service } from './runners/deploy-by-pm2/deploy-by-pm2.service';
 | 
			
		||||
import { ReposService } from '../repos/repos.service';
 | 
			
		||||
import { spawn, ChildProcessWithoutNullStreams } from 'child_process';
 | 
			
		||||
import { PipelineTask } from './pipeline-task.entity';
 | 
			
		||||
@@ -27,8 +28,9 @@ import {
 | 
			
		||||
  getSelfInstanceQueueKey,
 | 
			
		||||
  getSelfInstanceRouteKey,
 | 
			
		||||
} from '../commons/utils/rabbit-mq';
 | 
			
		||||
import { rm, unlink } from 'fs/promises';
 | 
			
		||||
import { rm } from 'fs/promises';
 | 
			
		||||
import { rename } from 'fs/promises';
 | 
			
		||||
import { join } from 'path';
 | 
			
		||||
 | 
			
		||||
type Spawn = typeof spawn;
 | 
			
		||||
 | 
			
		||||
@@ -43,6 +45,7 @@ export class PipelineTaskRunner {
 | 
			
		||||
    @Inject('spawn')
 | 
			
		||||
    private readonly spawn: Spawn,
 | 
			
		||||
    private readonly amqpConnection: AmqpConnection,
 | 
			
		||||
    private readonly deployByPM2Service: DeployByPm2Service,
 | 
			
		||||
  ) {}
 | 
			
		||||
  @RabbitSubscribe({
 | 
			
		||||
    exchange: 'new-pipeline-task',
 | 
			
		||||
@@ -274,6 +277,9 @@ export class PipelineTaskRunner {
 | 
			
		||||
    unit: PipelineUnits,
 | 
			
		||||
  ): Promise<void> {
 | 
			
		||||
    await this.emitEvent(task, unit, TaskStatuses.working, script, 'stdin');
 | 
			
		||||
    if (await this.tryRunDeployScript(workspaceRoot, script)) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    return new Promise((resolve, reject) => {
 | 
			
		||||
      const sub = this.spawn(script, {
 | 
			
		||||
        shell: true,
 | 
			
		||||
@@ -317,4 +323,17 @@ export class PipelineTaskRunner {
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async tryRunDeployScript(workspaceRoot: string, script: string) {
 | 
			
		||||
    const match = /^@@DEPLOY\s+(\S*)/.exec(script);
 | 
			
		||||
    if (match) {
 | 
			
		||||
      await this.deployByPM2Service.deploy(
 | 
			
		||||
        join(workspaceRoot, match[1]),
 | 
			
		||||
        workspaceRoot,
 | 
			
		||||
      );
 | 
			
		||||
      return true;
 | 
			
		||||
    } else {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -7,3 +7,5 @@ export const ROUTE_PIPELINE_TASK_DONE = 'pipeline-task-done';
 | 
			
		||||
export const QUEUE_PIPELINE_TASK_DONE = 'pipeline-task-done';
 | 
			
		||||
export const ROUTE_PIPELINE_TASK_KILL = 'pipeline-task-kill';
 | 
			
		||||
export const QUEUE_PIPELINE_TASK_KILL = 'pipeline-task-kill';
 | 
			
		||||
 | 
			
		||||
export const PM2_TOKEN = Symbol('pm2-token');
 | 
			
		||||
 
 | 
			
		||||
@@ -16,11 +16,10 @@ import {
 | 
			
		||||
} from './pipeline-tasks.constants';
 | 
			
		||||
import { PipelineTaskLogger } from './pipeline-task.logger';
 | 
			
		||||
import { PipelineTaskFlushService } from './pipeline-task-flush.service';
 | 
			
		||||
import { CommonsModule } from '../commons/commons.module';
 | 
			
		||||
import { DeployByPm2Service } from './runners/deploy-by-pm2/deploy-by-pm2.service';
 | 
			
		||||
 | 
			
		||||
@Module({
 | 
			
		||||
  imports: [
 | 
			
		||||
    CommonsModule,
 | 
			
		||||
    TypeOrmModule.forFeature([PipelineTask, Pipeline]),
 | 
			
		||||
    RedisModule,
 | 
			
		||||
    ReposModule,
 | 
			
		||||
@@ -68,6 +67,7 @@ import { CommonsModule } from '../commons/commons.module';
 | 
			
		||||
      useValue: spawn,
 | 
			
		||||
    },
 | 
			
		||||
    PipelineTaskFlushService,
 | 
			
		||||
    DeployByPm2Service,
 | 
			
		||||
  ],
 | 
			
		||||
  exports: [PipelineTasksService],
 | 
			
		||||
})
 | 
			
		||||
 
 | 
			
		||||
@@ -35,15 +35,6 @@ export class PipelineTasksResolver {
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Subscription(() => PipelineTask, {
 | 
			
		||||
    resolve: (value) => {
 | 
			
		||||
      return value;
 | 
			
		||||
    },
 | 
			
		||||
  })
 | 
			
		||||
  async pipelineTaskChanged(@Args('id') id: string) {
 | 
			
		||||
    // return await this.service.watchTaskUpdated(id);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @Query(() => [PipelineTask])
 | 
			
		||||
  async listPipelineTaskByPipelineId(@Args('pipelineId') pipelineId: string) {
 | 
			
		||||
    return await this.service.listTasksByPipelineId(pipelineId);
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,6 @@ describe('PipelineTasksService', () => {
 | 
			
		||||
  let service: PipelineTasksService;
 | 
			
		||||
  let module: TestingModule;
 | 
			
		||||
  let taskRepository: Repository<PipelineTask>;
 | 
			
		||||
  let pipelineRepository: Repository<Pipeline>;
 | 
			
		||||
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    module = await Test.createTestingModule({
 | 
			
		||||
@@ -43,7 +42,6 @@ describe('PipelineTasksService', () => {
 | 
			
		||||
 | 
			
		||||
    service = module.get<PipelineTasksService>(PipelineTasksService);
 | 
			
		||||
    taskRepository = module.get(getRepositoryToken(PipelineTask));
 | 
			
		||||
    pipelineRepository = module.get(getRepositoryToken(Pipeline));
 | 
			
		||||
    jest
 | 
			
		||||
      .spyOn(taskRepository, 'save')
 | 
			
		||||
      .mockImplementation(async (data: any) => data);
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@ import { PipelineTask } from './pipeline-task.entity';
 | 
			
		||||
import { Repository } from 'typeorm';
 | 
			
		||||
import { CreatePipelineTaskInput } from './dtos/create-pipeline-task.input';
 | 
			
		||||
import { Pipeline } from '../pipelines/pipeline.entity';
 | 
			
		||||
import debug from 'debug';
 | 
			
		||||
import { AmqpConnection, RabbitRPC } from '@golevelup/nestjs-rabbitmq';
 | 
			
		||||
import {
 | 
			
		||||
  EXCHANGE_PIPELINE_TASK_TOPIC,
 | 
			
		||||
@@ -19,8 +18,6 @@ import { InjectPinoLogger, PinoLogger } from 'nestjs-pino';
 | 
			
		||||
import { getAppInstanceRouteKey } from '../commons/utils/rabbit-mq';
 | 
			
		||||
import { ROUTE_PIPELINE_TASK_KILL } from './pipeline-tasks.constants';
 | 
			
		||||
 | 
			
		||||
const log = debug('fennec:pipeline-tasks:service');
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class PipelineTasksService {
 | 
			
		||||
  constructor(
 | 
			
		||||
 
 | 
			
		||||
@@ -0,0 +1,117 @@
 | 
			
		||||
import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
import { getLoggerToken, PinoLogger } from 'nestjs-pino';
 | 
			
		||||
import { join } from 'path';
 | 
			
		||||
import { DeployByPm2Service } from './deploy-by-pm2.service';
 | 
			
		||||
 | 
			
		||||
describe('DeployByPm2Service', () => {
 | 
			
		||||
  let service: DeployByPm2Service;
 | 
			
		||||
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
    const module: TestingModule = await Test.createTestingModule({
 | 
			
		||||
      providers: [
 | 
			
		||||
        DeployByPm2Service,
 | 
			
		||||
        {
 | 
			
		||||
          provide: getLoggerToken(DeployByPm2Service.name),
 | 
			
		||||
          useValue: new PinoLogger({
 | 
			
		||||
            pinoHttp: {
 | 
			
		||||
              level: 'silent',
 | 
			
		||||
            },
 | 
			
		||||
          }),
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    }).compile();
 | 
			
		||||
 | 
			
		||||
    service = module.get<DeployByPm2Service>(DeployByPm2Service);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
    expect(service).toBeDefined();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('getAppsSn', () => {
 | 
			
		||||
    it('should return right value', () => {
 | 
			
		||||
      expect(
 | 
			
		||||
        service['getAppsSn']([
 | 
			
		||||
          { name: 'app' },
 | 
			
		||||
          { name: 'app#4' },
 | 
			
		||||
          { name: 'app#1' },
 | 
			
		||||
        ]),
 | 
			
		||||
      ).toEqual(4 + 1);
 | 
			
		||||
    });
 | 
			
		||||
    it('should return 1 when no match', () => {
 | 
			
		||||
      expect(
 | 
			
		||||
        service['getAppsSn']([
 | 
			
		||||
          { name: 'bar' },
 | 
			
		||||
          { name: 'foo#4' },
 | 
			
		||||
          { name: 'foo#1' },
 | 
			
		||||
        ]),
 | 
			
		||||
      ).toEqual(4 + 1);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('filterOldApps', () => {
 | 
			
		||||
    it('should return right value', () => {
 | 
			
		||||
      expect(
 | 
			
		||||
        service['filterOldApps'](
 | 
			
		||||
          [{ name: 'app' }],
 | 
			
		||||
          [
 | 
			
		||||
            { name: 'app' },
 | 
			
		||||
            { name: 'app#4' },
 | 
			
		||||
            { name: 'foo#2' },
 | 
			
		||||
            { name: 'bar' },
 | 
			
		||||
          ],
 | 
			
		||||
        ),
 | 
			
		||||
      ).toEqual([{ name: 'app' }, { name: 'app#4' }]);
 | 
			
		||||
    });
 | 
			
		||||
    it('should return [] when no match', () => {
 | 
			
		||||
      expect(
 | 
			
		||||
        service['filterOldApps'](
 | 
			
		||||
          [{ name: 'app' }],
 | 
			
		||||
          [{ name: 'foo#2' }, { name: 'bar' }],
 | 
			
		||||
        ),
 | 
			
		||||
      ).toEqual([]);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('replaceAppName', () => {
 | 
			
		||||
    it('should be replaced with right value', () => {
 | 
			
		||||
      const getAppsSn = jest
 | 
			
		||||
        .spyOn(service, 'getAppsSn' as any)
 | 
			
		||||
        .mockImplementation(() => 1);
 | 
			
		||||
      const options = [{ name: 'app' }, { name: 'foo' }];
 | 
			
		||||
      service['replaceAppName'](options, []);
 | 
			
		||||
      expect(options).toEqual([{ name: 'app#1' }, { name: 'foo#1' }]);
 | 
			
		||||
      expect(getAppsSn).toBeCalledTimes(1);
 | 
			
		||||
      expect(getAppsSn.mock.calls[0][0]).toEqual([]);
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  describe('deploy', () => {
 | 
			
		||||
    it('should be success', async () => {
 | 
			
		||||
      const filterOldApps = jest
 | 
			
		||||
        .spyOn(service, 'filterOldApps' as any)
 | 
			
		||||
        .mockImplementation(() => [{ name: 'app#1' }, { name: 'app#2' }]);
 | 
			
		||||
      const replaceAppName = jest
 | 
			
		||||
        .spyOn(service, 'replaceAppName' as any)
 | 
			
		||||
        .mockImplementation((options) => (options[0].name = 'app#2'));
 | 
			
		||||
      const stopApps = jest
 | 
			
		||||
        .spyOn(service, 'stopApps' as any)
 | 
			
		||||
        .mockImplementation(() => Promise.resolve());
 | 
			
		||||
      await expect(
 | 
			
		||||
        service['deploy'](
 | 
			
		||||
          join(
 | 
			
		||||
            __dirname,
 | 
			
		||||
            '../../../../test/__mocks__/deploy-service/ecosystem.config.js',
 | 
			
		||||
          ),
 | 
			
		||||
          join(__dirname, '../../../../test/__mocks__/deploy-service'),
 | 
			
		||||
        ),
 | 
			
		||||
      ).resolves.toBeFalsy();
 | 
			
		||||
      expect(filterOldApps).toBeCalledTimes(1);
 | 
			
		||||
      expect(replaceAppName).toBeCalledTimes(1);
 | 
			
		||||
      expect(stopApps).toBeCalledTimes(1);
 | 
			
		||||
      stopApps.mockReset();
 | 
			
		||||
 | 
			
		||||
      await service['stopApps']([{ name: 'app#2' }]);
 | 
			
		||||
    }, 10_000);
 | 
			
		||||
  });
 | 
			
		||||
});
 | 
			
		||||
@@ -0,0 +1,98 @@
 | 
			
		||||
import { InjectPinoLogger, PinoLogger } from 'nestjs-pino';
 | 
			
		||||
import { Injectable } from '@nestjs/common';
 | 
			
		||||
import { promisify } from 'util';
 | 
			
		||||
import * as pm2 from 'pm2';
 | 
			
		||||
import { Proc, ProcessDescription, StartOptions } from 'pm2';
 | 
			
		||||
import { clone, last } from 'ramda';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class DeployByPm2Service {
 | 
			
		||||
  constructor(
 | 
			
		||||
    @InjectPinoLogger(DeployByPm2Service.name)
 | 
			
		||||
    private readonly logger: PinoLogger,
 | 
			
		||||
  ) {}
 | 
			
		||||
  async deploy(filePath: string, workspace: string) {
 | 
			
		||||
    const baseConfig: { apps: StartOptions[] } = await import(filePath);
 | 
			
		||||
    const appOptionsList: StartOptions[] = clone(baseConfig.apps);
 | 
			
		||||
 | 
			
		||||
    await promisify<void>(pm2.connect.bind(pm2))();
 | 
			
		||||
    const allApps = await promisify(pm2.list.bind(pm2))();
 | 
			
		||||
    try {
 | 
			
		||||
      if (!Array.isArray(baseConfig.apps)) {
 | 
			
		||||
        this.logger.error(
 | 
			
		||||
          'the "apps" in the PM2 ecosystem configuration is not array',
 | 
			
		||||
        );
 | 
			
		||||
        throw new Error('apps is not array');
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      const oldApps = this.filterOldApps(appOptionsList, allApps);
 | 
			
		||||
 | 
			
		||||
      this.replaceAppName(appOptionsList, oldApps);
 | 
			
		||||
      for (const appOptions of appOptionsList) {
 | 
			
		||||
        const proc = await promisify<StartOptions, Proc>(pm2.start.bind(pm2))({
 | 
			
		||||
          ...appOptions,
 | 
			
		||||
          cwd: workspace,
 | 
			
		||||
        });
 | 
			
		||||
        this.logger.info({ proc }, `start ${appOptions.name}`);
 | 
			
		||||
      }
 | 
			
		||||
      await this.stopApps(oldApps);
 | 
			
		||||
    } catch (err) {
 | 
			
		||||
      await this.stopApps(appOptionsList);
 | 
			
		||||
      throw err;
 | 
			
		||||
    } finally {
 | 
			
		||||
      pm2.disconnect();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async stopApps(apps: ProcessDescription[] | StartOptions[]) {
 | 
			
		||||
    await Promise.all(
 | 
			
		||||
      apps.map(async (app: ProcessDescription | StartOptions) => {
 | 
			
		||||
        let procAtStop: ProcessDescription;
 | 
			
		||||
        let procAtDelete: ProcessDescription;
 | 
			
		||||
        try {
 | 
			
		||||
          const idOrName = 'pm_id' in app ? app.pm_id : app.name;
 | 
			
		||||
          procAtStop = await promisify(pm2.stop.bind(pm2))(idOrName);
 | 
			
		||||
          procAtDelete = await promisify(pm2.delete.bind(pm2))(idOrName);
 | 
			
		||||
          this.logger.info('stop & delete %s success', app.name);
 | 
			
		||||
        } catch (error) {
 | 
			
		||||
          this.logger.error(
 | 
			
		||||
            { error, procAtStop, procAtDelete },
 | 
			
		||||
            'stop & delete %s error',
 | 
			
		||||
            app.name,
 | 
			
		||||
          );
 | 
			
		||||
        }
 | 
			
		||||
      }),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private replaceAppName(
 | 
			
		||||
    optionsList: StartOptions[],
 | 
			
		||||
    oldApps: ProcessDescription[],
 | 
			
		||||
  ) {
 | 
			
		||||
    const appSn = this.getAppsSn(oldApps);
 | 
			
		||||
 | 
			
		||||
    optionsList.forEach((options) => {
 | 
			
		||||
      if (!options.name) {
 | 
			
		||||
        this.logger.error('please give a name for application');
 | 
			
		||||
        throw new Error('app name is not given');
 | 
			
		||||
      }
 | 
			
		||||
      options.name = `${options.name}#${appSn}`;
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private filterOldApps(
 | 
			
		||||
    optionsList: StartOptions[],
 | 
			
		||||
    apps: ProcessDescription[],
 | 
			
		||||
  ) {
 | 
			
		||||
    return apps.filter((app) =>
 | 
			
		||||
      optionsList.some((options) => app.name.split('#')[0] === options.name),
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private getAppsSn(oldApps: ProcessDescription[]) {
 | 
			
		||||
    const appsSn: number[] = oldApps.map(
 | 
			
		||||
      (app) => +(app.name.split('#')?.[1] ?? 0),
 | 
			
		||||
    );
 | 
			
		||||
    return (last(appsSn.sort()) ?? 0) + 1;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -28,4 +28,8 @@ export class CreatePipelineInput {
 | 
			
		||||
  @ValidateNested()
 | 
			
		||||
  @IsInstance(WorkUnitMetadata)
 | 
			
		||||
  workUnitMetadata: WorkUnitMetadata;
 | 
			
		||||
 | 
			
		||||
  @IsString()
 | 
			
		||||
  @MaxLength(100)
 | 
			
		||||
  environment: string;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ export class Pipeline extends AppBaseEntity {
 | 
			
		||||
  @Column()
 | 
			
		||||
  projectId: string;
 | 
			
		||||
 | 
			
		||||
  @Column({ comment: 'eg: remotes/origin/master' })
 | 
			
		||||
  @Column({ comment: 'E.g., remotes/origin/master' })
 | 
			
		||||
  branch: string;
 | 
			
		||||
 | 
			
		||||
  @Column()
 | 
			
		||||
@@ -20,4 +20,7 @@ export class Pipeline extends AppBaseEntity {
 | 
			
		||||
 | 
			
		||||
  @Column({ type: 'jsonb' })
 | 
			
		||||
  workUnitMetadata: WorkUnitMetadata;
 | 
			
		||||
 | 
			
		||||
  @Column()
 | 
			
		||||
  environment: string;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -5,14 +5,11 @@ import { TypeOrmModule } from '@nestjs/typeorm';
 | 
			
		||||
import { Pipeline } from './pipeline.entity';
 | 
			
		||||
import { CommitLogsResolver } from './commit-logs.resolver';
 | 
			
		||||
import { PipelineTasksModule } from '../pipeline-tasks/pipeline-tasks.module';
 | 
			
		||||
import { ReposModule } from '../repos/repos.module';
 | 
			
		||||
import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq';
 | 
			
		||||
import { ConfigModule, ConfigService } from '@nestjs/config';
 | 
			
		||||
import { CommonsModule } from '../commons/commons.module';
 | 
			
		||||
 | 
			
		||||
@Module({
 | 
			
		||||
  imports: [
 | 
			
		||||
    CommonsModule,
 | 
			
		||||
    TypeOrmModule.forFeature([Pipeline]),
 | 
			
		||||
    PipelineTasksModule,
 | 
			
		||||
    RabbitMQModule.forRootAsync(RabbitMQModule, {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,13 +2,11 @@ import { Test, TestingModule } from '@nestjs/testing';
 | 
			
		||||
import { PipelinesService } from './pipelines.service';
 | 
			
		||||
import { Pipeline } from './pipeline.entity';
 | 
			
		||||
import { getRepositoryToken } from '@nestjs/typeorm';
 | 
			
		||||
import { Repository } from 'typeorm';
 | 
			
		||||
import { Project } from '../projects/project.entity';
 | 
			
		||||
import { AmqpConnection } from '@golevelup/nestjs-rabbitmq';
 | 
			
		||||
 | 
			
		||||
describe('PipelinesService', () => {
 | 
			
		||||
  let service: PipelinesService;
 | 
			
		||||
  let repository: Repository<Pipeline>;
 | 
			
		||||
  let pipeline: Pipeline;
 | 
			
		||||
 | 
			
		||||
  beforeEach(async () => {
 | 
			
		||||
@@ -40,7 +38,6 @@ describe('PipelinesService', () => {
 | 
			
		||||
    }).compile();
 | 
			
		||||
 | 
			
		||||
    service = module.get<PipelinesService>(PipelinesService);
 | 
			
		||||
    repository = module.get(getRepositoryToken(Pipeline));
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  it('should be defined', () => {
 | 
			
		||||
 
 | 
			
		||||
@@ -19,7 +19,9 @@ import { plainToClass } from 'class-transformer';
 | 
			
		||||
 | 
			
		||||
@Injectable()
 | 
			
		||||
export class PipelinesService extends BaseDbService<Pipeline> {
 | 
			
		||||
  readonly uniqueFields: Array<Array<keyof Pipeline>> = [['projectId', 'name']];
 | 
			
		||||
  readonly uniqueFields: Array<Array<keyof Pipeline>> = [
 | 
			
		||||
    ['projectId', 'name', 'environment'],
 | 
			
		||||
  ];
 | 
			
		||||
  constructor(
 | 
			
		||||
    @InjectRepository(Pipeline)
 | 
			
		||||
    readonly repository: Repository<Pipeline>,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import { ObjectType } from '@nestjs/graphql';
 | 
			
		||||
import { Entity, Column, DeleteDateColumn } from 'typeorm';
 | 
			
		||||
import { Entity, Column } from 'typeorm';
 | 
			
		||||
import { AppBaseEntity } from '../commons/entities/app-base-entity';
 | 
			
		||||
 | 
			
		||||
@ObjectType()
 | 
			
		||||
 
 | 
			
		||||
@@ -6,11 +6,9 @@ import { Project } from './project.entity';
 | 
			
		||||
import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq';
 | 
			
		||||
import { ConfigModule, ConfigService } from '@nestjs/config';
 | 
			
		||||
import { EXCHANGE_PROJECT_FANOUT } from './projects.constants';
 | 
			
		||||
import { CommonsModule } from '../commons/commons.module';
 | 
			
		||||
 | 
			
		||||
@Module({
 | 
			
		||||
  imports: [
 | 
			
		||||
    CommonsModule,
 | 
			
		||||
    TypeOrmModule.forFeature([Project]),
 | 
			
		||||
    RabbitMQModule.forRootAsync(RabbitMQModule, {
 | 
			
		||||
      imports: [ConfigModule],
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,5 @@
 | 
			
		||||
import { ObjectType, Field } from '@nestjs/graphql';
 | 
			
		||||
import {
 | 
			
		||||
  LogResult,
 | 
			
		||||
  DefaultLogFields,
 | 
			
		||||
  BranchSummary,
 | 
			
		||||
  BranchSummaryBranch,
 | 
			
		||||
} from 'simple-git';
 | 
			
		||||
import { BranchSummaryBranch } from 'simple-git';
 | 
			
		||||
 | 
			
		||||
@ObjectType()
 | 
			
		||||
export class Branch implements BranchSummaryBranch {
 | 
			
		||||
 
 | 
			
		||||
@@ -7,14 +7,12 @@ import { ConfigModule, ConfigService } from '@nestjs/config';
 | 
			
		||||
import { ProjectsModule } from '../projects/projects.module';
 | 
			
		||||
import { EXCHANGE_REPO } from './repos.constants';
 | 
			
		||||
import { RabbitMQModule } from '@golevelup/nestjs-rabbitmq';
 | 
			
		||||
import { CommonsModule } from '../commons/commons.module';
 | 
			
		||||
 | 
			
		||||
@Module({
 | 
			
		||||
  imports: [
 | 
			
		||||
    TypeOrmModule.forFeature([Project]),
 | 
			
		||||
    ConfigModule,
 | 
			
		||||
    ProjectsModule,
 | 
			
		||||
    CommonsModule,
 | 
			
		||||
    RabbitMQModule.forRootAsync(RabbitMQModule, {
 | 
			
		||||
      imports: [ConfigModule],
 | 
			
		||||
      useFactory: (configService: ConfigService) => ({
 | 
			
		||||
 
 | 
			
		||||
@@ -161,7 +161,7 @@ describe('ReposService', () => {
 | 
			
		||||
      const project = new Project();
 | 
			
		||||
      const pipeline = new Pipeline();
 | 
			
		||||
      pipeline.branch = 'test';
 | 
			
		||||
      const fetch = jest.fn((_: any) => Promise.resolve());
 | 
			
		||||
      const fetch = jest.fn<any, [any]>(() => Promise.resolve());
 | 
			
		||||
      pipeline.project = project;
 | 
			
		||||
      const getGit = jest.spyOn(service, 'getGit').mockImplementation(() =>
 | 
			
		||||
        Promise.resolve({
 | 
			
		||||
@@ -182,7 +182,7 @@ describe('ReposService', () => {
 | 
			
		||||
      const project = new Project();
 | 
			
		||||
      const pipeline = new Pipeline();
 | 
			
		||||
      pipeline.branch = 'test';
 | 
			
		||||
      const fetch = jest.fn((_: any) => Promise.resolve());
 | 
			
		||||
      const fetch = jest.fn<any, [any]>(() => Promise.resolve());
 | 
			
		||||
      pipeline.project = project;
 | 
			
		||||
      const getGit = jest
 | 
			
		||||
        .spyOn(service, 'getGit')
 | 
			
		||||
@@ -196,7 +196,7 @@ describe('ReposService', () => {
 | 
			
		||||
      const project = new Project();
 | 
			
		||||
      const pipeline = new Pipeline();
 | 
			
		||||
      pipeline.branch = 'test';
 | 
			
		||||
      const fetch = jest.fn((_: any) => Promise.reject('error'));
 | 
			
		||||
      const fetch = jest.fn<any, [any]>(() => Promise.reject('error'));
 | 
			
		||||
      pipeline.project = project;
 | 
			
		||||
      const getGit = jest.spyOn(service, 'getGit').mockImplementation(() =>
 | 
			
		||||
        Promise.resolve({
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
import { MiddlewareConsumer, Module } from '@nestjs/common';
 | 
			
		||||
import { Module } from '@nestjs/common';
 | 
			
		||||
import { TypeOrmModule } from '@nestjs/typeorm';
 | 
			
		||||
import { PipelineTasksModule } from '../pipeline-tasks/pipeline-tasks.module';
 | 
			
		||||
import { GiteaWebhooksController } from './gitea-webhooks.controller';
 | 
			
		||||
@@ -10,5 +10,4 @@ import { WebhooksService } from './webhooks.service';
 | 
			
		||||
  controllers: [GiteaWebhooksController],
 | 
			
		||||
  providers: [WebhooksService],
 | 
			
		||||
})
 | 
			
		||||
export class WebhooksModule {
 | 
			
		||||
}
 | 
			
		||||
export class WebhooksModule {}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										15
									
								
								test/__mocks__/deploy-service/ecosystem.config.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								test/__mocks__/deploy-service/ecosystem.config.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
			
		||||
module.exports = {
 | 
			
		||||
  apps: [
 | 
			
		||||
    {
 | 
			
		||||
      name: 'app',
 | 
			
		||||
      script: __dirname + '/index.js',
 | 
			
		||||
      watch: false,
 | 
			
		||||
      ignore_watch: ['node_modules'],
 | 
			
		||||
      log_date_format: 'MM-DD HH:mm:ss.SSS Z',
 | 
			
		||||
      env: {},
 | 
			
		||||
      max_restarts: 5,
 | 
			
		||||
      kill_timeout: 10_000,
 | 
			
		||||
      wait_ready: true,
 | 
			
		||||
    },
 | 
			
		||||
  ],
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										23
									
								
								test/__mocks__/deploy-service/index.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								test/__mocks__/deploy-service/index.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
import { createServer } from 'http';
 | 
			
		||||
 | 
			
		||||
var app = createServer(function (req, res) {
 | 
			
		||||
  res.writeHead(200);
 | 
			
		||||
  setTimeout(() => {
 | 
			
		||||
    res.end('hey');
 | 
			
		||||
  }, 2000);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
var listener = app.listen(0, function () {
 | 
			
		||||
  console.log('Listening on port ' + listener.address().port);
 | 
			
		||||
  setTimeout(() => {
 | 
			
		||||
    // Here we send the ready signal to PM2
 | 
			
		||||
    process.send('ready');
 | 
			
		||||
  }, 5000);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
process.on('SIGINT', function () {
 | 
			
		||||
  listener.close();
 | 
			
		||||
  setTimeout(() => {
 | 
			
		||||
    process.exit(0);
 | 
			
		||||
  }, 2000);
 | 
			
		||||
});
 | 
			
		||||
							
								
								
									
										10
									
								
								test/__mocks__/deploy-service/package.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										10
									
								
								test/__mocks__/deploy-service/package.json
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,10 @@
 | 
			
		||||
{
 | 
			
		||||
  "name": "deploy-service",
 | 
			
		||||
  "version": "1.0.0",
 | 
			
		||||
  "description": "For Test",
 | 
			
		||||
  "main": "index.js",
 | 
			
		||||
  "type": "module",
 | 
			
		||||
  "scripts": {},
 | 
			
		||||
  "author": "Ivan Li",
 | 
			
		||||
  "license": "ISC"
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										16
									
								
								test/__mocks__/deploy-service/test.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										16
									
								
								test/__mocks__/deploy-service/test.js
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,16 @@
 | 
			
		||||
import { createServer } from 'http';
 | 
			
		||||
 | 
			
		||||
var app = createServer(function (req, res) {
 | 
			
		||||
  res.writeHead(200);
 | 
			
		||||
  setTimeout(() => {
 | 
			
		||||
    res.end('pm2');
 | 
			
		||||
  }, 2000);
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
var listener = app.listen(33333, function () {
 | 
			
		||||
  console.log('Listening on port ' + listener.address().port);
 | 
			
		||||
  setTimeout(() => {
 | 
			
		||||
    // Here we send the ready signal to PM2
 | 
			
		||||
    process.send('ready');
 | 
			
		||||
  }, 5000);
 | 
			
		||||
});
 | 
			
		||||
@@ -11,6 +11,5 @@
 | 
			
		||||
    "sourceMap": true,
 | 
			
		||||
    "outDir": "./dist",
 | 
			
		||||
    "baseUrl": "./",
 | 
			
		||||
    "incremental": true
 | 
			
		||||
  }
 | 
			
		||||
  },
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user