feat-pipelines #1
33
docs/ci-cd.md
Normal file
33
docs/ci-cd.md
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
# CI/CD 流程
|
||||||
|
0. 准备
|
||||||
|
- project information
|
||||||
|
- commit hash
|
||||||
|
1. checkout
|
||||||
|
2. install dependencies
|
||||||
|
3. run test script
|
||||||
|
5. run deploy script
|
||||||
|
6. clear workspace
|
||||||
|
|
||||||
|
## 流水线任务单元描述
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"version": 1,
|
||||||
|
"unit": {
|
||||||
|
"install-dependencies": {
|
||||||
|
"script": "npm ci"
|
||||||
|
},
|
||||||
|
"test": {
|
||||||
|
"script": "npm test"
|
||||||
|
},
|
||||||
|
"build": {
|
||||||
|
"script": "npm build"
|
||||||
|
},
|
||||||
|
"deploy": {
|
||||||
|
"script": [
|
||||||
|
"npm build"
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
```
|
@ -8,6 +8,7 @@ import { AppService } from './app.service';
|
|||||||
import { ProjectsModule } from './projects/projects.module';
|
import { ProjectsModule } from './projects/projects.module';
|
||||||
import { ReposModule } from './repos/repos.module';
|
import { ReposModule } from './repos/repos.module';
|
||||||
import { PipelinesModule } from './pipelines/pipelines.module';
|
import { PipelinesModule } from './pipelines/pipelines.module';
|
||||||
|
import { PipelineTasksModule } from './pipeline-tasks/pipeline-tasks.module';
|
||||||
import configuration from './commons/config/configuration';
|
import configuration from './commons/config/configuration';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@ -41,6 +42,7 @@ import configuration from './commons/config/configuration';
|
|||||||
ProjectsModule,
|
ProjectsModule,
|
||||||
ReposModule,
|
ReposModule,
|
||||||
PipelinesModule,
|
PipelinesModule,
|
||||||
|
PipelineTasksModule,
|
||||||
],
|
],
|
||||||
controllers: [AppController],
|
controllers: [AppController],
|
||||||
providers: [AppService, AppResolver],
|
providers: [AppService, AppResolver],
|
||||||
|
11
src/pipeline-tasks/dtos/create-pipeline-task.input.ts
Normal file
11
src/pipeline-tasks/dtos/create-pipeline-task.input.ts
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
import { InputType } from '@nestjs/graphql';
|
||||||
|
import { PipelineUnits } from '../enums/pipeline-units.enum';
|
||||||
|
|
||||||
|
@InputType()
|
||||||
|
export class CreatePipelineTaskInput {
|
||||||
|
pipelineId: string;
|
||||||
|
|
||||||
|
commit: string;
|
||||||
|
|
||||||
|
units: PipelineUnits[];
|
||||||
|
}
|
7
src/pipeline-tasks/enums/pipeline-units.enum.ts
Normal file
7
src/pipeline-tasks/enums/pipeline-units.enum.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
export enum PipelineUnits {
|
||||||
|
checkout = 'checkout',
|
||||||
|
installDependencies = 'installDependencies',
|
||||||
|
test = 'test',
|
||||||
|
deploy = 'deploy',
|
||||||
|
cleanUp = 'cleanUp',
|
||||||
|
}
|
6
src/pipeline-tasks/enums/task-statuses.enum.ts
Normal file
6
src/pipeline-tasks/enums/task-statuses.enum.ts
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
export enum TaskStatuses {
|
||||||
|
success = 'success',
|
||||||
|
failed = 'failed',
|
||||||
|
working = 'working',
|
||||||
|
pending = 'pending',
|
||||||
|
}
|
9
src/pipeline-tasks/models/pipeline-task-logs.model.ts
Normal file
9
src/pipeline-tasks/models/pipeline-task-logs.model.ts
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
import { TaskStatuses } from '../enums/task-statuses.enum';
|
||||||
|
import { PipelineUnits } from '../enums/pipeline-units.enum';
|
||||||
|
export class PipelineTaskLogs {
|
||||||
|
unit: PipelineUnits;
|
||||||
|
status: TaskStatuses;
|
||||||
|
startedAt?: Date;
|
||||||
|
endedAt?: Date;
|
||||||
|
logs: string[];
|
||||||
|
}
|
33
src/pipeline-tasks/pipeline-task.entity.ts
Normal file
33
src/pipeline-tasks/pipeline-task.entity.ts
Normal file
@ -0,0 +1,33 @@
|
|||||||
|
import { ObjectType } from '@nestjs/graphql';
|
||||||
|
import { Column, Entity, ManyToOne } from 'typeorm';
|
||||||
|
import { Pipeline } from '../pipelines/pipeline.entity';
|
||||||
|
import { PipelineTaskLogs } from './models/pipeline-task-logs.model';
|
||||||
|
import { TaskStatuses } from './enums/task-statuses.enum';
|
||||||
|
import { PipelineUnits } from './enums/pipeline-units.enum';
|
||||||
|
|
||||||
|
@ObjectType()
|
||||||
|
@Entity()
|
||||||
|
export class PipelineTask {
|
||||||
|
@ManyToOne(() => Pipeline)
|
||||||
|
pipeline: Pipeline;
|
||||||
|
@Column()
|
||||||
|
pipelineId: string;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
commit: string;
|
||||||
|
|
||||||
|
@Column({ type: 'enum', enum: PipelineUnits, array: true })
|
||||||
|
units: PipelineUnits[];
|
||||||
|
|
||||||
|
@Column({ type: 'jsonb', default: '[]' })
|
||||||
|
logs: PipelineTaskLogs[];
|
||||||
|
|
||||||
|
@Column({ type: 'enum', enum: TaskStatuses, default: TaskStatuses.pending })
|
||||||
|
status: TaskStatuses;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
startedAt: Date;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
endedAt: Date;
|
||||||
|
}
|
1
src/pipeline-tasks/pipeline-tasks.constants.ts
Normal file
1
src/pipeline-tasks/pipeline-tasks.constants.ts
Normal file
@ -0,0 +1 @@
|
|||||||
|
export const PIPELINE_TASK_QUEUE = 'PIPELINE_TASK_QUEUE';
|
13
src/pipeline-tasks/pipeline-tasks.module.ts
Normal file
13
src/pipeline-tasks/pipeline-tasks.module.ts
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
import { Module } from '@nestjs/common';
|
||||||
|
import { PipelineTasksService } from './pipeline-tasks.service';
|
||||||
|
import { PipelineTasksResolver } from './pipeline-tasks.resolver';
|
||||||
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
|
import { PipelineTask } from './pipeline-task.entity';
|
||||||
|
import { Pipeline } from '../pipelines/pipeline.entity';
|
||||||
|
import { BullModule } from '@nestjs/bull';
|
||||||
|
|
||||||
|
@Module({
|
||||||
|
imports: [TypeOrmModule.forFeature([PipelineTask, Pipeline])],
|
||||||
|
providers: [PipelineTasksService, PipelineTasksResolver],
|
||||||
|
})
|
||||||
|
export class PipelineTasksModule {}
|
18
src/pipeline-tasks/pipeline-tasks.resolver.spec.ts
Normal file
18
src/pipeline-tasks/pipeline-tasks.resolver.spec.ts
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { PipelineTasksResolver } from './pipeline-tasks.resolver';
|
||||||
|
|
||||||
|
describe('PipelineTasksResolver', () => {
|
||||||
|
let resolver: PipelineTasksResolver;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [PipelineTasksResolver],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
resolver = module.get<PipelineTasksResolver>(PipelineTasksResolver);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(resolver).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
4
src/pipeline-tasks/pipeline-tasks.resolver.ts
Normal file
4
src/pipeline-tasks/pipeline-tasks.resolver.ts
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
import { Resolver } from '@nestjs/graphql';
|
||||||
|
|
||||||
|
@Resolver()
|
||||||
|
export class PipelineTasksResolver {}
|
26
src/pipeline-tasks/pipeline-tasks.service.spec.ts
Normal file
26
src/pipeline-tasks/pipeline-tasks.service.spec.ts
Normal file
@ -0,0 +1,26 @@
|
|||||||
|
import { Test, TestingModule } from '@nestjs/testing';
|
||||||
|
import { PipelineTasksService } from './pipeline-tasks.service';
|
||||||
|
import { getRepositoryToken } from '@nestjs/typeorm';
|
||||||
|
import { PipelineTask } from './pipeline-task.entity';
|
||||||
|
|
||||||
|
describe('PipelineTasksService', () => {
|
||||||
|
let service: PipelineTasksService;
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
const module: TestingModule = await Test.createTestingModule({
|
||||||
|
providers: [
|
||||||
|
PipelineTasksService,
|
||||||
|
{
|
||||||
|
provide: getRepositoryToken(PipelineTask),
|
||||||
|
useValue: {},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
}).compile();
|
||||||
|
|
||||||
|
service = module.get<PipelineTasksService>(PipelineTasksService);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be defined', () => {
|
||||||
|
expect(service).toBeDefined();
|
||||||
|
});
|
||||||
|
});
|
60
src/pipeline-tasks/pipeline-tasks.service.ts
Normal file
60
src/pipeline-tasks/pipeline-tasks.service.ts
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
import { Injectable } from '@nestjs/common';
|
||||||
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
|
import { PipelineTask } from './pipeline-task.entity';
|
||||||
|
import { Repository } from 'typeorm';
|
||||||
|
import { CreatePipelineTaskInput } from './dtos/create-pipeline-task.input';
|
||||||
|
import { RedisService } from 'nestjs-redis';
|
||||||
|
import { Pipeline } from '../pipelines/pipeline.entity';
|
||||||
|
import { InjectQueue } from '@nestjs/bull';
|
||||||
|
import { PIPELINE_TASK_QUEUE } from './pipeline-tasks.constants';
|
||||||
|
import { Queue } from 'bull';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class PipelineTasksService {
|
||||||
|
constructor(
|
||||||
|
@InjectRepository(PipelineTask)
|
||||||
|
private readonly repository: Repository<PipelineTask>,
|
||||||
|
@InjectRepository(Pipeline)
|
||||||
|
private readonly pipelineRepository: Repository<Pipeline>,
|
||||||
|
private readonly redis: RedisService,
|
||||||
|
@InjectQueue(PIPELINE_TASK_QUEUE)
|
||||||
|
private readonly queue: Queue<PipelineTask>,
|
||||||
|
) {}
|
||||||
|
async addTask(dto: CreatePipelineTaskInput) {
|
||||||
|
const pipeline = await this.pipelineRepository.findOneOrFail({
|
||||||
|
where: { id: dto.pipelineId },
|
||||||
|
relations: ['project'],
|
||||||
|
});
|
||||||
|
const task = await this.repository.save(this.repository.create(dto));
|
||||||
|
task.pipeline = pipeline;
|
||||||
|
|
||||||
|
const [lckKey, tasksKey] = this.getRedisTokens(pipeline);
|
||||||
|
const redis = this.redis.getClient();
|
||||||
|
await redis.set(lckKey, 0, 'EX', 10, 'NX');
|
||||||
|
const lckSemaphore = await redis.incr(lckKey);
|
||||||
|
if (lckSemaphore > 1) {
|
||||||
|
await this.redis
|
||||||
|
.getClient()
|
||||||
|
.lpush(tasksKey, JSON.stringify(task))
|
||||||
|
.finally(() => {
|
||||||
|
return redis.decr(lckKey);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
this.queue.add(task);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async doTask(task: PipelineTask) {
|
||||||
|
const tasksKey = this.getRedisTokens(task.pipeline)[1];
|
||||||
|
|
||||||
|
const redis = this.redis.getClient();
|
||||||
|
const nextTask = await redis.rpop(tasksKey);
|
||||||
|
if (nextTask) {
|
||||||
|
this.doTask(task).then();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getRedisTokens(pipeline: Pipeline): [string, string] {
|
||||||
|
return [`pipeline-${pipeline.id}:lck`, `pipeline-${pipeline.id}:tasks`];
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user