├── scheduler
├── src
│ ├── utils
│ │ ├── ftp.ts
│ │ ├── link.ts
│ │ ├── verify.ts
│ │ ├── shell.ts
│ │ ├── file.ts
│ │ ├── git.ts
│ │ ├── sql.ts
│ │ └── tool.ts
│ ├── app.service.ts
│ ├── middleware
│ │ ├── root
│ │ │ ├── root.middleware.spec.ts
│ │ │ └── root.middleware.ts
│ │ ├── user-update
│ │ │ ├── user-update.middleware.spec.ts
│ │ │ └── user-update.middleware.ts
│ │ └── authentication
│ │ │ ├── authentication.middleware.spec.ts
│ │ │ └── authentication.middleware.ts
│ ├── cluster
│ │ ├── cluster.interface.ts
│ │ ├── cluster.module.ts
│ │ ├── cluster.service.spec.ts
│ │ ├── cluster.controller.spec.ts
│ │ ├── cluster.sql.ts
│ │ ├── cluster.controller.ts
│ │ └── cluster.service.ts
│ ├── members
│ │ ├── members.module.ts
│ │ ├── members.interface.ts
│ │ ├── members.service.spec.ts
│ │ ├── members.controller.spec.ts
│ │ ├── members.sql.ts
│ │ ├── members.controller.ts
│ │ └── members.service.ts
│ ├── app.controller.ts
│ ├── main.ts
│ ├── project
│ │ ├── project.gateway.spec.ts
│ │ ├── project.service.spec.ts
│ │ ├── process
│ │ │ ├── process.service.spec.ts
│ │ │ ├── process.controller.spec.ts
│ │ │ └── process.controller.ts
│ │ ├── release
│ │ │ ├── release.service.spec.ts
│ │ │ ├── release.controller.spec.ts
│ │ │ ├── release.controller.ts
│ │ │ └── release.service.ts
│ │ ├── command
│ │ │ └── command
│ │ │ │ ├── command.service.spec.ts
│ │ │ │ ├── command.controller.spec.ts
│ │ │ │ ├── command.controller.ts
│ │ │ │ └── command.service.ts
│ │ ├── project.controller.spec.ts
│ │ ├── project.interface.ts
│ │ ├── project.module.ts
│ │ ├── project.gateway.ts
│ │ └── project.controller.ts
│ ├── app.controller.spec.ts
│ ├── app.module.ts
│ └── config.ts
├── .prettierrc
├── nest-cli.json
├── tsconfig.build.json
├── test
│ ├── jest-e2e.json
│ └── app.e2e-spec.ts
├── lib
│ ├── rsa_public_key_1024.txt
│ └── rsa_private_key_1024.txt
├── .gitignore
├── .eslintrc.js
├── tsconfig.json
└── package.json
├── cluster
├── src
│ ├── release
│ │ ├── release.interface.ts
│ │ ├── release.module.ts
│ │ ├── release.service.spec.ts
│ │ ├── release.controller.spec.ts
│ │ ├── release.controller.ts
│ │ └── release.service.ts
│ ├── app.service.ts
│ ├── middleware
│ │ ├── permission.middleware.spec.ts
│ │ └── permission.middleware.ts
│ ├── plugin
│ │ ├── interface.ts
│ │ ├── file.ts
│ │ ├── tool.ts
│ │ ├── WebServeAdmin.ts
│ │ ├── start.ts
│ │ └── diskInfo.ts
│ ├── app.controller.ts
│ ├── main.ts
│ ├── equipment
│ │ ├── equipment.module.ts
│ │ ├── equipment.gateway.spec.ts
│ │ ├── equipment.service.spec.ts
│ │ ├── equipment.controller.spec.ts
│ │ ├── equipment.gateway.ts
│ │ ├── equipment.interface.ts
│ │ ├── equipment.controller.ts
│ │ └── equipment.service.ts
│ ├── app.controller.spec.ts
│ ├── app.module.ts
│ └── config.ts
├── .prettierrc
├── nest-cli.json
├── tsconfig.build.json
├── environment.d.ts
├── test
│ ├── jest-e2e.json
│ └── app.e2e-spec.ts
├── process
│ └── koa.js
├── .eslintrc.js
├── tsconfig.json
└── package.json
├── view
├── src
│ ├── type.d.ts
│ ├── assets
│ │ ├── More.png
│ │ ├── bg.webp
│ │ ├── blog.png
│ │ ├── logo.png
│ │ ├── github.png
│ │ ├── seblog.png
│ │ ├── permission.png
│ │ ├── devops_select.png
│ │ ├── logo-github-filled.png
│ │ ├── css
│ │ │ ├── mixin.scss
│ │ │ ├── ress.scss
│ │ │ └── public.scss
│ │ ├── qp.svg
│ │ ├── devops.svg
│ │ ├── out.svg
│ │ ├── gy.svg
│ │ └── avatar.svg
│ ├── env.d.ts
│ ├── components
│ │ ├── Content.vue
│ │ ├── versions.vue
│ │ ├── Aside.vue
│ │ ├── modal-view.vue
│ │ └── branch-config.vue
│ ├── main.ts
│ ├── plugin
│ │ ├── el-drag.ts
│ │ └── directive.ts
│ ├── views
│ │ ├── container.vue
│ │ └── login.vue
│ ├── App.vue
│ ├── stores
│ │ └── global.ts
│ ├── http
│ │ ├── index.ts
│ │ └── api.ts
│ ├── interface.ts
│ ├── router
│ │ └── index.ts
│ └── utils
│ │ └── tool.ts
├── .vscode
│ ├── extensions.json
│ └── vue.code-snippets
├── .env.development
├── public
│ ├── favicon.ico
│ ├── devops_select.png
│ └── static
│ │ └── images
│ │ ├── 6.jpeg
│ │ ├── 6.webp
│ │ ├── add.png
│ │ ├── code.png
│ │ ├── disk.png
│ │ ├── host.png
│ │ ├── kill.png
│ │ ├── mac.png
│ │ ├── max.png
│ │ ├── min.png
│ │ ├── prod.png
│ │ ├── ress.png
│ │ ├── test.png
│ │ ├── uat.png
│ │ ├── user.png
│ │ ├── close.png
│ │ ├── config.png
│ │ ├── linux.png
│ │ ├── loading.png
│ │ ├── project.png
│ │ ├── record.png
│ │ ├── windows.png
│ │ ├── powershell.png
│ │ ├── PACKAGE-CI_files
│ │ ├── host.png
│ │ ├── linux.png
│ │ ├── prod.png
│ │ ├── test.png
│ │ ├── uat.png
│ │ ├── user.png
│ │ ├── project.png
│ │ ├── qp.9109b21e.svg
│ │ ├── out.86cda219.svg
│ │ ├── gy.b1188c89.svg
│ │ └── avatar.0dd5877b.svg
│ │ └── gy.svg
├── .env.production
├── auto-imports.d.ts
├── index.html
├── vite.config.ts
├── tsconfig.json
├── package.json
└── components.d.ts
├── .gitignore
├── LICENSE
└── README.md
/scheduler/src/utils/ftp.ts:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/cluster/src/release/release.interface.ts:
--------------------------------------------------------------------------------
1 | export interface Release {}
2 |
--------------------------------------------------------------------------------
/view/src/type.d.ts:
--------------------------------------------------------------------------------
1 | declare interface Window {
2 | LeaderLine: any
3 | }
--------------------------------------------------------------------------------
/cluster/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all"
4 | }
--------------------------------------------------------------------------------
/scheduler/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "trailingComma": "all"
4 | }
--------------------------------------------------------------------------------
/view/.vscode/extensions.json:
--------------------------------------------------------------------------------
1 | {
2 | "recommendations": ["johnsoncodehk.volar"]
3 | }
4 |
--------------------------------------------------------------------------------
/view/.env.development:
--------------------------------------------------------------------------------
1 | VITE_API_URL=http://127.0.0.1:7001/
2 | VITE_SOCKET_URL=ws://127.0.0.1:7001
--------------------------------------------------------------------------------
/cluster/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "collection": "@nestjs/schematics",
3 | "sourceRoot": "src"
4 | }
5 |
--------------------------------------------------------------------------------
/scheduler/nest-cli.json:
--------------------------------------------------------------------------------
1 | {
2 | "collection": "@nestjs/schematics",
3 | "sourceRoot": "src"
4 | }
5 |
--------------------------------------------------------------------------------
/view/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/public/favicon.ico
--------------------------------------------------------------------------------
/view/src/assets/More.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/src/assets/More.png
--------------------------------------------------------------------------------
/view/src/assets/bg.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/src/assets/bg.webp
--------------------------------------------------------------------------------
/view/src/assets/blog.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/src/assets/blog.png
--------------------------------------------------------------------------------
/view/src/assets/logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/src/assets/logo.png
--------------------------------------------------------------------------------
/view/src/assets/github.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/src/assets/github.png
--------------------------------------------------------------------------------
/view/src/assets/seblog.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/src/assets/seblog.png
--------------------------------------------------------------------------------
/view/.env.production:
--------------------------------------------------------------------------------
1 | VITE_API_URL=https://package-api.js-vue.com/
2 | VITE_SOCKET_URL=wss://package-api.js-vue.com
--------------------------------------------------------------------------------
/view/public/devops_select.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/public/devops_select.png
--------------------------------------------------------------------------------
/view/public/static/images/6.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/public/static/images/6.jpeg
--------------------------------------------------------------------------------
/view/public/static/images/6.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/public/static/images/6.webp
--------------------------------------------------------------------------------
/view/src/assets/permission.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/src/assets/permission.png
--------------------------------------------------------------------------------
/view/public/static/images/add.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/public/static/images/add.png
--------------------------------------------------------------------------------
/view/public/static/images/code.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/public/static/images/code.png
--------------------------------------------------------------------------------
/view/public/static/images/disk.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/public/static/images/disk.png
--------------------------------------------------------------------------------
/view/public/static/images/host.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/public/static/images/host.png
--------------------------------------------------------------------------------
/view/public/static/images/kill.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/public/static/images/kill.png
--------------------------------------------------------------------------------
/view/public/static/images/mac.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/public/static/images/mac.png
--------------------------------------------------------------------------------
/view/public/static/images/max.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/public/static/images/max.png
--------------------------------------------------------------------------------
/view/public/static/images/min.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/public/static/images/min.png
--------------------------------------------------------------------------------
/view/public/static/images/prod.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/public/static/images/prod.png
--------------------------------------------------------------------------------
/view/public/static/images/ress.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/public/static/images/ress.png
--------------------------------------------------------------------------------
/view/public/static/images/test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/public/static/images/test.png
--------------------------------------------------------------------------------
/view/public/static/images/uat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/public/static/images/uat.png
--------------------------------------------------------------------------------
/view/public/static/images/user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/public/static/images/user.png
--------------------------------------------------------------------------------
/view/src/assets/devops_select.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/src/assets/devops_select.png
--------------------------------------------------------------------------------
/view/public/static/images/close.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/public/static/images/close.png
--------------------------------------------------------------------------------
/view/public/static/images/config.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/public/static/images/config.png
--------------------------------------------------------------------------------
/view/public/static/images/linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/public/static/images/linux.png
--------------------------------------------------------------------------------
/view/public/static/images/loading.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/public/static/images/loading.png
--------------------------------------------------------------------------------
/view/public/static/images/project.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/public/static/images/project.png
--------------------------------------------------------------------------------
/view/public/static/images/record.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/public/static/images/record.png
--------------------------------------------------------------------------------
/view/public/static/images/windows.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/public/static/images/windows.png
--------------------------------------------------------------------------------
/view/src/assets/logo-github-filled.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/src/assets/logo-github-filled.png
--------------------------------------------------------------------------------
/view/public/static/images/powershell.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/public/static/images/powershell.png
--------------------------------------------------------------------------------
/cluster/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/scheduler/tsconfig.build.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./tsconfig.json",
3 | "exclude": ["node_modules", "test", "dist", "**/*spec.ts"]
4 | }
5 |
--------------------------------------------------------------------------------
/view/public/static/images/PACKAGE-CI_files/host.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/public/static/images/PACKAGE-CI_files/host.png
--------------------------------------------------------------------------------
/view/public/static/images/PACKAGE-CI_files/linux.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/public/static/images/PACKAGE-CI_files/linux.png
--------------------------------------------------------------------------------
/view/public/static/images/PACKAGE-CI_files/prod.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/public/static/images/PACKAGE-CI_files/prod.png
--------------------------------------------------------------------------------
/view/public/static/images/PACKAGE-CI_files/test.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/public/static/images/PACKAGE-CI_files/test.png
--------------------------------------------------------------------------------
/view/public/static/images/PACKAGE-CI_files/uat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/public/static/images/PACKAGE-CI_files/uat.png
--------------------------------------------------------------------------------
/view/public/static/images/PACKAGE-CI_files/user.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/public/static/images/PACKAGE-CI_files/user.png
--------------------------------------------------------------------------------
/view/public/static/images/PACKAGE-CI_files/project.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/yangrds/package-ci/HEAD/view/public/static/images/PACKAGE-CI_files/project.png
--------------------------------------------------------------------------------
/view/auto-imports.d.ts:
--------------------------------------------------------------------------------
1 | // Generated by 'unplugin-auto-import'
2 | // We suggest you to commit this file into source control
3 | declare global {
4 |
5 | }
6 | export {}
7 |
--------------------------------------------------------------------------------
/cluster/environment.d.ts:
--------------------------------------------------------------------------------
1 | declare namespace NodeJS {
2 | export interface ProcessEnv {
3 | port: number;
4 | dist: string;
5 | name: string;
6 | project: string;
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/cluster/src/app.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 |
3 | @Injectable()
4 | export class AppService {
5 | getHello(): string {
6 | return 'Hello World!';
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/scheduler/src/app.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 |
3 | @Injectable()
4 | export class AppService {
5 | getHello(): string {
6 | return 'Hello World!';
7 | }
8 | }
9 |
--------------------------------------------------------------------------------
/cluster/test/jest-e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "moduleFileExtensions": ["js", "json", "ts"],
3 | "rootDir": ".",
4 | "testEnvironment": "node",
5 | "testRegex": ".e2e-spec.ts$",
6 | "transform": {
7 | "^.+\\.(t|j)s$": "ts-jest"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/scheduler/test/jest-e2e.json:
--------------------------------------------------------------------------------
1 | {
2 | "moduleFileExtensions": ["js", "json", "ts"],
3 | "rootDir": ".",
4 | "testEnvironment": "node",
5 | "testRegex": ".e2e-spec.ts$",
6 | "transform": {
7 | "^.+\\.(t|j)s$": "ts-jest"
8 | }
9 | }
10 |
--------------------------------------------------------------------------------
/scheduler/src/middleware/root/root.middleware.spec.ts:
--------------------------------------------------------------------------------
1 | import { RootMiddleware } from './root.middleware';
2 |
3 | describe('RootMiddleware', () => {
4 | it('should be defined', () => {
5 | expect(new RootMiddleware()).toBeDefined();
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/cluster/src/middleware/permission.middleware.spec.ts:
--------------------------------------------------------------------------------
1 | import { PermissionMiddleware } from './permission.middleware';
2 |
3 | describe('PermissionMiddleware', () => {
4 | it('should be defined', () => {
5 | expect(new PermissionMiddleware()).toBeDefined();
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/scheduler/src/middleware/user-update/user-update.middleware.spec.ts:
--------------------------------------------------------------------------------
1 | import { UserUpdateMiddleware } from './user-update.middleware';
2 |
3 | describe('UserUpdateMiddleware', () => {
4 | it('should be defined', () => {
5 | expect(new UserUpdateMiddleware()).toBeDefined();
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/scheduler/src/middleware/authentication/authentication.middleware.spec.ts:
--------------------------------------------------------------------------------
1 | import { AuthenticationMiddleware } from './authentication.middleware';
2 |
3 | describe('AuthenticationMiddleware', () => {
4 | it('should be defined', () => {
5 | expect(new AuthenticationMiddleware()).toBeDefined();
6 | });
7 | });
8 |
--------------------------------------------------------------------------------
/scheduler/lib/rsa_public_key_1024.txt:
--------------------------------------------------------------------------------
1 | -----BEGIN PUBLIC KEY-----
2 | MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCZ75513uYZEloyFctJp1en3AwH
3 | e6oXIAxpUtmCQJJmXTi79EvORhCkOHnNS7CzWNaCErtENONco0Ptfvz7P4VmFa/w
4 | aZqf/j5XuE7VvOOUF5QZJv4hdJJehvbefi2mI3yKlV/Mh61ktfe2IgVilnZS12yJ
5 | rgDwFoKYEUT05+Yb7QIDAQAB
6 | -----END PUBLIC KEY-----
--------------------------------------------------------------------------------
/scheduler/src/cluster/cluster.interface.ts:
--------------------------------------------------------------------------------
1 | export interface Cluster {}
2 |
3 |
4 | export interface Host {
5 | arch?: string;
6 | cpus?: { model: string; speed: number }[];
7 | hostname?: string;
8 | ip?: string;
9 | platform?: string;
10 | port?: number;
11 | totalmem?: number;
12 | }
--------------------------------------------------------------------------------
/cluster/src/release/release.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { ReleaseController } from './release.controller';
3 | import { ReleaseService } from './release.service';
4 |
5 | @Module({
6 | controllers: [ReleaseController],
7 | providers: [ReleaseService]
8 | })
9 | export class ReleaseModule {}
10 |
--------------------------------------------------------------------------------
/scheduler/src/cluster/cluster.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { ClusterController } from './cluster.controller';
3 | import { ClusterService } from './cluster.service';
4 |
5 | @Module({
6 | controllers: [ClusterController],
7 | providers: [ClusterService]
8 | })
9 | export class ClusterModule {}
10 |
--------------------------------------------------------------------------------
/scheduler/src/members/members.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { MembersController } from './members.controller';
3 | import { MembersService } from './members.service';
4 |
5 | @Module({
6 | controllers: [MembersController],
7 | providers: [MembersService]
8 | })
9 | export class MembersModule {}
10 |
--------------------------------------------------------------------------------
/cluster/src/plugin/interface.ts:
--------------------------------------------------------------------------------
1 | export interface ENV {
2 | port?: number;
3 | dist?: string;
4 | name?: string;
5 | project?: string;
6 | id?: string
7 | }
8 |
9 |
10 | export interface ServeParameter {
11 | name: string;
12 | port: number;
13 | dist: string;
14 | project: string;
15 | project_id: string;
16 | id?: string
17 | }
--------------------------------------------------------------------------------
/scheduler/src/app.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Get } from '@nestjs/common';
2 | import { AppService } from './app.service';
3 |
4 | @Controller()
5 | export class AppController {
6 | constructor(private readonly appService: AppService) {}
7 | @Get()
8 | getHello(): string {
9 | return this.appService.getHello();
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/cluster/src/app.controller.ts:
--------------------------------------------------------------------------------
1 | import { Controller, Get } from '@nestjs/common';
2 | import { AppService } from './app.service';
3 |
4 | @Controller()
5 | export class AppController {
6 | constructor(private readonly appService: AppService) {}
7 |
8 | @Get()
9 | getHello(): string {
10 | return this.appService.getHello();
11 | }
12 | }
13 |
--------------------------------------------------------------------------------
/scheduler/src/members/members.interface.ts:
--------------------------------------------------------------------------------
1 | export interface Parameter {
2 | job_id?: string;
3 | account: string;
4 | pwd: string;
5 | name: string;
6 | jobName: string;
7 | remark: string;
8 | date?: number;
9 | _id?: string;
10 | __v?: string
11 | }
12 |
13 | export interface MembersList {
14 | pageIndex: number;
15 | pageSize: number
16 | }
--------------------------------------------------------------------------------
/cluster/src/main.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core';
2 | import { AppModule } from './app.module';
3 | import { init } from './plugin/start';
4 | import PackageConfig from './config'
5 |
6 |
7 | async function bootstrap() {
8 | PackageConfig.init()
9 | const app = await NestFactory.create(AppModule);
10 | await app.listen(9001);
11 | init()
12 | }
13 | bootstrap();
14 |
--------------------------------------------------------------------------------
/cluster/src/equipment/equipment.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { EquipmentController } from './equipment.controller';
3 | import { EquipmentService } from './equipment.service';
4 | import { EquipmentGateway } from './equipment.gateway';
5 |
6 | @Module({
7 | controllers: [EquipmentController],
8 | providers: [EquipmentService, EquipmentGateway]
9 | })
10 | export class EquipmentModule {}
11 |
--------------------------------------------------------------------------------
/view/src/env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 |
3 | declare module '*.vue' {
4 | import { DefineComponent } from 'vue'
5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types
6 | const component: DefineComponent<{}, {}, any>
7 | export default component
8 | }
9 |
10 | declare module 'qs'
11 | declare module 'axios'
12 | declare module 'leader-line'
13 |
14 |
15 |
--------------------------------------------------------------------------------
/scheduler/src/main.ts:
--------------------------------------------------------------------------------
1 | import { NestFactory } from '@nestjs/core';
2 | import { AppModule } from './app.module';
3 | import { link } from './utils/link'
4 | import PackageConfig from './config'
5 |
6 | async function bootstrap() {
7 | PackageConfig.keyInit()
8 | PackageConfig.staticInit()
9 | PackageConfig.adminInit()
10 | const app = await NestFactory.create(AppModule);
11 | app.enableCors();
12 | await app.listen(7001);
13 | link()
14 | }
15 |
16 | bootstrap();
17 |
--------------------------------------------------------------------------------
/view/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | PACKAGE-CI
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/view/src/components/Content.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
11 |
12 |
--------------------------------------------------------------------------------
/scheduler/src/utils/link.ts:
--------------------------------------------------------------------------------
1 | import PackageConfig from '@/config'
2 | const mongoose = require('mongoose')
3 |
4 | const db = mongoose.connection;
5 |
6 | export async function link() {
7 | let { user, password, host, port, database } = PackageConfig.client
8 | mongoose.connect(`mongodb://${user}:${password}@${host}:${port}/${database}`, function () {
9 | console.log('数据库链接成功');
10 | });
11 | db.on('error', console.error.bind(console, 'connection error:'));
12 | }
13 |
14 |
15 |
--------------------------------------------------------------------------------
/cluster/src/release/release.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { ReleaseService } from './release.service';
3 |
4 | describe('ReleaseService', () => {
5 | let service: ReleaseService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [ReleaseService],
10 | }).compile();
11 |
12 | service = module.get(ReleaseService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/scheduler/src/cluster/cluster.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { ClusterService } from './cluster.service';
3 |
4 | describe('ClusterService', () => {
5 | let service: ClusterService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [ClusterService],
10 | }).compile();
11 |
12 | service = module.get(ClusterService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/scheduler/src/members/members.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { MembersService } from './members.service';
3 |
4 | describe('MembersService', () => {
5 | let service: MembersService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [MembersService],
10 | }).compile();
11 |
12 | service = module.get(MembersService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/scheduler/src/project/project.gateway.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { ProjectGateway } from './project.gateway';
3 |
4 | describe('ProjectGateway', () => {
5 | let gateway: ProjectGateway;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [ProjectGateway],
10 | }).compile();
11 |
12 | gateway = module.get(ProjectGateway);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(gateway).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/scheduler/src/project/project.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { ProjectService } from './project.service';
3 |
4 | describe('ProjectService', () => {
5 | let service: ProjectService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [ProjectService],
10 | }).compile();
11 |
12 | service = module.get(ProjectService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/view/src/main.ts:
--------------------------------------------------------------------------------
1 | import { createApp, provide } from 'vue'
2 | import App from './App.vue'
3 | import ElementPlus from 'element-plus'
4 | import ModalView from '@/components/modal-view.vue'
5 | import 'element-plus/dist/index.css'
6 | import router from '@/router/index'
7 | import directive from '@/plugin/directive'
8 | import { createPinia } from 'pinia'
9 |
10 | const instance = createApp(App)
11 | directive(instance)
12 | instance.use(createPinia())
13 | instance.use(router)
14 | instance.use(ElementPlus)
15 | instance.component('ModalView', ModalView)
16 | instance.mount('#app')
17 |
--------------------------------------------------------------------------------
/cluster/process/koa.js:
--------------------------------------------------------------------------------
1 | const Koa = require("koa");
2 | const static = require("koa-static");
3 | const app = new Koa();
4 |
5 | const { port, name, dist, project } = process.env;
6 |
7 | app.use(static(dist));
8 |
9 | app.use(async (ctx) => {
10 | if (ctx.request.url === "/status") {
11 | ctx.body = { port, env: name, status: "ok", project };
12 | }
13 | });
14 |
15 | app.on("close", () => {
16 | process.send({ type: "close", msg: "服务关闭" });
17 | });
18 |
19 |
20 | app.listen(port, "0.0.0.0", (err) => {
21 | process.send({ type: "listen", msg: "启动成功" });
22 | });
23 |
24 |
--------------------------------------------------------------------------------
/cluster/src/middleware/permission.middleware.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, NestMiddleware } from '@nestjs/common';
2 | import { NextFunction, Response, Request } from 'express';
3 | import PackageConfig from '@/config'
4 | @Injectable()
5 | export class PermissionMiddleware implements NestMiddleware {
6 | use(req: Request, res: Response, next: NextFunction) {
7 | let headers: any = req.headers
8 | if (PackageConfig.sign(headers.token)) {
9 | next();
10 | } else {
11 | res.status(200).send({ code: 404, msg: '节点签名信息异常,解密失败。' })
12 | return
13 | }
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/scheduler/src/project/process/process.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { ProcessService } from './process.service';
3 |
4 | describe('ProcessService', () => {
5 | let service: ProcessService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [ProcessService],
10 | }).compile();
11 |
12 | service = module.get(ProcessService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/scheduler/src/project/release/release.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { ReleaseService } from './release.service';
3 |
4 | describe('ReleaseService', () => {
5 | let service: ReleaseService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [ReleaseService],
10 | }).compile();
11 |
12 | service = module.get(ReleaseService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/cluster/src/equipment/equipment.gateway.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { EquipmentGateway } from './equipment.gateway';
3 |
4 | describe('EquipmentGateway', () => {
5 | let gateway: EquipmentGateway;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [EquipmentGateway],
10 | }).compile();
11 |
12 | gateway = module.get(EquipmentGateway);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(gateway).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/cluster/src/equipment/equipment.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { EquipmentService } from './equipment.service';
3 |
4 | describe('EquipmentService', () => {
5 | let service: EquipmentService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [EquipmentService],
10 | }).compile();
11 |
12 | service = module.get(EquipmentService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/scheduler/src/project/command/command/command.service.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { CommandService } from './command.service';
3 |
4 | describe('CommandService', () => {
5 | let service: CommandService;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | providers: [CommandService],
10 | }).compile();
11 |
12 | service = module.get(CommandService);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(service).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/cluster/src/plugin/file.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs-extra';
2 |
3 | export function dir_remove(dirPath: string, exclude: string = 'node_modules') {
4 | const Files = fs.readdirSync(dirPath)
5 | Files.forEach((fileName) => {
6 | let item = `${dirPath}/${fileName}`
7 | if (fs.existsSync(item) && fileName != exclude) fs.removeSync(item)
8 | })
9 | }
10 |
11 | export function emptyDir(src: string) {
12 | return new Promise((resolve) => {
13 | fs.emptyDir(src, (err: Error) => {
14 | resolve({ code: err ? 500 : 200, msg: err && err.message })
15 | })
16 | })
17 | }
--------------------------------------------------------------------------------
/cluster/src/release/release.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { ReleaseController } from './release.controller';
3 |
4 | describe('ReleaseController', () => {
5 | let controller: ReleaseController;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | controllers: [ReleaseController],
10 | }).compile();
11 |
12 | controller = module.get(ReleaseController);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(controller).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/scheduler/src/cluster/cluster.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { ClusterController } from './cluster.controller';
3 |
4 | describe('ClusterController', () => {
5 | let controller: ClusterController;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | controllers: [ClusterController],
10 | }).compile();
11 |
12 | controller = module.get(ClusterController);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(controller).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/scheduler/src/members/members.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { MembersController } from './members.controller';
3 |
4 | describe('MembersController', () => {
5 | let controller: MembersController;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | controllers: [MembersController],
10 | }).compile();
11 |
12 | controller = module.get(MembersController);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(controller).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/scheduler/src/project/project.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { ProjectController } from './project.controller';
3 |
4 | describe('ProjectController', () => {
5 | let controller: ProjectController;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | controllers: [ProjectController],
10 | }).compile();
11 |
12 | controller = module.get(ProjectController);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(controller).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/scheduler/src/project/process/process.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { ProcessController } from './process.controller';
3 |
4 | describe('ProcessController', () => {
5 | let controller: ProcessController;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | controllers: [ProcessController],
10 | }).compile();
11 |
12 | controller = module.get(ProcessController);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(controller).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/scheduler/src/project/release/release.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { ReleaseController } from './release.controller';
3 |
4 | describe('ReleaseController', () => {
5 | let controller: ReleaseController;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | controllers: [ReleaseController],
10 | }).compile();
11 |
12 | controller = module.get(ReleaseController);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(controller).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/cluster/src/equipment/equipment.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { EquipmentController } from './equipment.controller';
3 |
4 | describe('EquipmentController', () => {
5 | let controller: EquipmentController;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | controllers: [EquipmentController],
10 | }).compile();
11 |
12 | controller = module.get(EquipmentController);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(controller).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/scheduler/src/project/command/command/command.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { CommandController } from './command.controller';
3 |
4 | describe('CommandController', () => {
5 | let controller: CommandController;
6 |
7 | beforeEach(async () => {
8 | const module: TestingModule = await Test.createTestingModule({
9 | controllers: [CommandController],
10 | }).compile();
11 |
12 | controller = module.get(CommandController);
13 | });
14 |
15 | it('should be defined', () => {
16 | expect(controller).toBeDefined();
17 | });
18 | });
19 |
--------------------------------------------------------------------------------
/scheduler/src/project/project.interface.ts:
--------------------------------------------------------------------------------
1 | export interface ENV { port: string }
2 | export interface ENVS { DEV?: ENV; TEST?: ENV; UAT?: ENV; PROD?: ENV }
3 | export interface Project {
4 | name?: string;
5 | project_id?: string;
6 | envs?: ENVS;
7 | gitUrl?: string;
8 | sourceCode?: string;
9 | historyVersion?: string;
10 | compile?: string;
11 | user?: string;
12 | pwd?: string;
13 | branch?: string;
14 | qa?: string;
15 | devs?: string[],
16 | pm?: string;
17 | describe?: string;
18 | cors?: boolean,
19 | date?: number;
20 | _id?: string;
21 | __v?: string;
22 | }
23 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | dist
3 | node_modules
4 | .DS_Store
5 | package-ci
6 | dist-ssr
7 | *.local
8 |
9 | # Logs
10 | logs
11 | *.log
12 | npm-debug.log*
13 | pnpm-debug.log*
14 | yarn-debug.log*
15 | yarn-error.log*
16 | lerna-debug.log*
17 |
18 | # OS
19 | .DS_Store
20 | package-ci-scheduler
21 |
22 | # Tests
23 | /coverage
24 | /.nyc_output
25 |
26 | # IDEs and editors
27 | /.idea
28 | .project
29 | .classpath
30 | .c9/
31 | *.launch
32 | .settings/
33 | *.sublime-workspace
34 |
35 | # IDE - VSCode
36 | .vscode/*
37 | !.vscode/settings.json
38 | !.vscode/tasks.json
39 | !.vscode/launch.json
40 | !.vscode/extensions.json
41 |
42 |
43 |
--------------------------------------------------------------------------------
/scheduler/.gitignore:
--------------------------------------------------------------------------------
1 | # compiled output
2 | dist
3 | node_modules
4 | .DS_Store
5 | package-ci
6 | dist-ssr
7 | *.local
8 |
9 | # Logs
10 | logs
11 | *.log
12 | npm-debug.log*
13 | pnpm-debug.log*
14 | yarn-debug.log*
15 | yarn-error.log*
16 | lerna-debug.log*
17 |
18 | # OS
19 | .DS_Store
20 | package-ci-scheduler
21 |
22 | # Tests
23 | /coverage
24 | /.nyc_output
25 |
26 | # IDEs and editors
27 | /.idea
28 | .project
29 | .classpath
30 | .c9/
31 | *.launch
32 | .settings/
33 | *.sublime-workspace
34 |
35 | # IDE - VSCode
36 | .vscode/*
37 | !.vscode/settings.json
38 | !.vscode/tasks.json
39 | !.vscode/launch.json
40 | !.vscode/extensions.json
41 |
42 |
43 |
--------------------------------------------------------------------------------
/view/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite'
2 | import vue from '@vitejs/plugin-vue'
3 | import Components from 'unplugin-vue-components/vite'
4 | import { NaiveUiResolver } from 'unplugin-vue-components/resolvers'
5 | const path = require('path');
6 |
7 |
8 | // https://vitejs.dev/config/
9 | export default defineConfig({
10 | build: {
11 | outDir: 'package-ci',
12 | sourcemap: true
13 | },
14 | plugins: [
15 | vue(),
16 | Components({
17 | resolvers: [NaiveUiResolver()]
18 | })
19 | ],
20 | define: {
21 | 'process.env': {},
22 | },
23 | resolve: {
24 | // 配置路径别名
25 | alias: {
26 | '@': path.resolve(__dirname, './src'),
27 | },
28 | },
29 | })
30 |
--------------------------------------------------------------------------------
/cluster/src/app.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { AppController } from './app.controller';
3 | import { AppService } from './app.service';
4 |
5 | describe('AppController', () => {
6 | let appController: AppController;
7 |
8 | beforeEach(async () => {
9 | const app: TestingModule = await Test.createTestingModule({
10 | controllers: [AppController],
11 | providers: [AppService],
12 | }).compile();
13 |
14 | appController = app.get(AppController);
15 | });
16 |
17 | describe('root', () => {
18 | it('should return "Hello World!"', () => {
19 | expect(appController.getHello()).toBe('Hello World!');
20 | });
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/view/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": "./",
4 | "paths": {
5 | "@/*": [
6 | "src/*"
7 | ]
8 | },
9 | "target": "esnext",
10 | "useDefineForClassFields": true,
11 | "suppressImplicitAnyIndexErrors": true,
12 | "module": "esnext",
13 | "moduleResolution": "node",
14 | "strict": true,
15 | "jsx": "preserve",
16 | "sourceMap": true,
17 | "resolveJsonModule": true,
18 | "esModuleInterop": true,
19 | "lib": [
20 | "esnext",
21 | "dom"
22 | ]
23 | },
24 | "exclude": [
25 | "node_modules"
26 | ],
27 | "include": [
28 | "src/**/*.ts",
29 | "src/**/*.d.ts",
30 | "src/**/*.tsx",
31 | "src/**/*.vue"
32 | ]
33 | }
--------------------------------------------------------------------------------
/scheduler/src/app.controller.spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { AppController } from './app.controller';
3 | import { AppService } from './app.service';
4 |
5 | describe('AppController', () => {
6 | let appController: AppController;
7 |
8 | beforeEach(async () => {
9 | const app: TestingModule = await Test.createTestingModule({
10 | controllers: [AppController],
11 | providers: [AppService],
12 | }).compile();
13 |
14 | appController = app.get(AppController);
15 | });
16 |
17 | describe('root', () => {
18 | it('should return "Hello World!"', () => {
19 | expect(appController.getHello()).toBe('Hello World!');
20 | });
21 | });
22 | });
23 |
--------------------------------------------------------------------------------
/cluster/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | parserOptions: {
4 | project: 'tsconfig.json',
5 | sourceType: 'module',
6 | },
7 | plugins: ['@typescript-eslint/eslint-plugin'],
8 | extends: [
9 | 'plugin:@typescript-eslint/recommended',
10 | 'plugin:prettier/recommended',
11 | ],
12 | root: true,
13 | env: {
14 | node: true,
15 | jest: true,
16 | },
17 | ignorePatterns: ['.eslintrc.js'],
18 | rules: {
19 | '@typescript-eslint/interface-name-prefix': 'off',
20 | '@typescript-eslint/explicit-function-return-type': 'off',
21 | '@typescript-eslint/explicit-module-boundary-types': 'off',
22 | '@typescript-eslint/no-explicit-any': 'off',
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/cluster/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { MiddlewareConsumer, Module, NestModule } from '@nestjs/common';
2 | import { AppController } from './app.controller';
3 | import { AppService } from './app.service';
4 | import { EquipmentModule } from './equipment/equipment.module';
5 | import { ReleaseModule } from './release/release.module';
6 | import { PermissionMiddleware } from './middleware/permission.middleware'
7 |
8 | @Module({
9 | imports: [EquipmentModule, ReleaseModule],
10 | controllers: [AppController],
11 | providers: [AppService],
12 | })
13 | export class AppModule implements NestModule {
14 | configure(consumer: MiddlewareConsumer) {
15 | consumer
16 | .apply(PermissionMiddleware)
17 | .forRoutes('*')
18 | }
19 | }
20 |
--------------------------------------------------------------------------------
/scheduler/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | parser: '@typescript-eslint/parser',
3 | parserOptions: {
4 | project: 'tsconfig.json',
5 | sourceType: 'module',
6 | },
7 | plugins: ['@typescript-eslint/eslint-plugin'],
8 | extends: [
9 | 'plugin:@typescript-eslint/recommended',
10 | 'plugin:prettier/recommended',
11 | ],
12 | root: true,
13 | env: {
14 | node: true,
15 | jest: true,
16 | },
17 | ignorePatterns: ['.eslintrc.js'],
18 | rules: {
19 | '@typescript-eslint/interface-name-prefix': 'off',
20 | '@typescript-eslint/explicit-function-return-type': 'off',
21 | '@typescript-eslint/explicit-module-boundary-types': 'off',
22 | '@typescript-eslint/no-explicit-any': 'off',
23 | },
24 | };
25 |
--------------------------------------------------------------------------------
/cluster/test/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { INestApplication } from '@nestjs/common';
3 | import * as request from 'supertest';
4 | import { AppModule } from './../src/app.module';
5 |
6 | describe('AppController (e2e)', () => {
7 | let app: INestApplication;
8 |
9 | beforeEach(async () => {
10 | const moduleFixture: TestingModule = await Test.createTestingModule({
11 | imports: [AppModule],
12 | }).compile();
13 |
14 | app = moduleFixture.createNestApplication();
15 | await app.init();
16 | });
17 |
18 | it('/ (GET)', () => {
19 | return request(app.getHttpServer())
20 | .get('/')
21 | .expect(200)
22 | .expect('Hello World!');
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/scheduler/test/app.e2e-spec.ts:
--------------------------------------------------------------------------------
1 | import { Test, TestingModule } from '@nestjs/testing';
2 | import { INestApplication } from '@nestjs/common';
3 | import * as request from 'supertest';
4 | import { AppModule } from './../src/app.module';
5 |
6 | describe('AppController (e2e)', () => {
7 | let app: INestApplication;
8 |
9 | beforeEach(async () => {
10 | const moduleFixture: TestingModule = await Test.createTestingModule({
11 | imports: [AppModule],
12 | }).compile();
13 |
14 | app = moduleFixture.createNestApplication();
15 | await app.init();
16 | });
17 |
18 | it('/ (GET)', () => {
19 | return request(app.getHttpServer())
20 | .get('/')
21 | .expect(200)
22 | .expect('Hello World!');
23 | });
24 | });
25 |
--------------------------------------------------------------------------------
/cluster/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "removeComments": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "allowSyntheticDefaultImports": true,
9 | "target": "es2017",
10 | "sourceMap": true,
11 | "outDir": "./dist",
12 | "baseUrl": "./",
13 | "paths": {
14 | "@/*": [
15 | "src/*"
16 | ]
17 | },
18 | "incremental": true,
19 | "skipLibCheck": true,
20 | "strictNullChecks": false,
21 | "noImplicitAny": false,
22 | "strictBindCallApply": false,
23 | "forceConsistentCasingInFileNames": false,
24 | "noFallthroughCasesInSwitch": false
25 | },
26 | "exclude": [
27 | "node_modules"
28 | ],
29 | }
--------------------------------------------------------------------------------
/scheduler/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "module": "commonjs",
4 | "declaration": true,
5 | "removeComments": true,
6 | "emitDecoratorMetadata": true,
7 | "experimentalDecorators": true,
8 | "allowSyntheticDefaultImports": true,
9 | "target": "es2017",
10 | "sourceMap": true,
11 | "outDir": "./dist",
12 | "baseUrl": "./",
13 | "paths": {
14 | "@/*": [
15 | "src/*"
16 | ]
17 | },
18 | "incremental": true,
19 | "skipLibCheck": true,
20 | "strictNullChecks": false,
21 | "noImplicitAny": false,
22 | "strictBindCallApply": false,
23 | "forceConsistentCasingInFileNames": false,
24 | "noFallthroughCasesInSwitch": false
25 | },
26 | "exclude": [
27 | "node_modules"
28 | ],
29 | }
--------------------------------------------------------------------------------
/scheduler/src/project/command/command/command.controller.ts:
--------------------------------------------------------------------------------
1 | import { Body, Controller, Post } from '@nestjs/common';
2 | import { CommandService } from './command.service'
3 |
4 | @Controller('command')
5 | export class CommandController {
6 | constructor(private readonly commandService: CommandService) { }
7 | @Post('add')
8 | CommandAdd(@Body() body: { command: string; args: string; project_id: string }) {
9 | return this.commandService.CommandAdd(body)
10 | }
11 | @Post('list')
12 | CommandList(@Body() body: { project_id: string }) {
13 | return this.commandService.CommandList(body)
14 | }
15 |
16 | @Post('remove')
17 | CommandRemove(@Body() body: { commandId: string }) {
18 | return this.commandService.CommandRemove(body)
19 | }
20 |
21 |
22 | }
23 |
--------------------------------------------------------------------------------
/cluster/src/equipment/equipment.gateway.ts:
--------------------------------------------------------------------------------
1 | import { OnGatewayConnection, OnGatewayDisconnect, OnGatewayInit, SubscribeMessage, WebSocketGateway, WebSocketServer } from '@nestjs/websockets';
2 | import { Server, Socket } from 'socket.io';
3 | @WebSocketGateway(
4 | {
5 | path: '/socket',
6 | allowEIO3: true,
7 | cors: {
8 | origin: /.*/,
9 | credentials: true
10 | }
11 | }
12 | )
13 | export class EquipmentGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
14 | @WebSocketServer() private ws: Server;
15 | afterInit(server: any) { }
16 | handleConnection(client: any, ...args: any[]) { }
17 | handleDisconnect(client: any) { }
18 | @SubscribeMessage('message')
19 | handleMessage(client: any, payload: any): string {
20 | return 'Hello world!';
21 | }
22 | }
23 |
--------------------------------------------------------------------------------
/scheduler/src/utils/verify.ts:
--------------------------------------------------------------------------------
1 | import * as NodeRSA from 'node-rsa'
2 | import * as path from 'path'
3 | import * as fs from 'fs-extra'
4 | import PackageConfig from '@/config'
5 |
6 | // 公钥加密
7 | export function encrypt(text: string) {
8 | PackageConfig.keyInit()
9 | // 读取公钥
10 | const publicKey = fs.readFileSync(PackageConfig.pubkey_path, 'utf-8');
11 | // 公钥加密
12 | const nodersa = new NodeRSA(publicKey);
13 | const encrypted = nodersa.encrypt(text, 'base64');
14 | // 返回加密内容
15 | return encrypted;
16 | }
17 |
18 |
19 | // 私钥解密
20 | export function decrypt(text: string) {
21 | const privateKey = fs.readFileSync(PackageConfig.prikey_path);
22 | const nodersa = new NodeRSA(privateKey);
23 | const decrypted = nodersa.decrypt(text, 'utf8');
24 | return decrypted;
25 | }
26 |
27 |
28 |
--------------------------------------------------------------------------------
/view/src/assets/css/mixin.scss:
--------------------------------------------------------------------------------
1 | @mixin before($max) {
2 | &::before {
3 | width: 100%;
4 | height: 1px;
5 | display: block;
6 | content: "";
7 | background-color: rgba(0, 0, 0, 0.1);
8 | transform: scaleY($max);
9 | position: absolute;
10 | left: 0;
11 | bottom: 0;
12 | z-index: 9;
13 | }
14 | }
15 |
16 | @mixin TopBefore($max) {
17 | &::before {
18 | width: 100%;
19 | height: 1px;
20 | display: block;
21 | content: "";
22 | background-color: rgba(0, 0, 0, 0.1);
23 | transform: scaleY($max);
24 | position: absolute;
25 | left: 0;
26 | top: 0;
27 | z-index: 9;
28 | }
29 | }
30 |
31 | @mixin omit($index) {
32 | display: -webkit-box;
33 | -webkit-box-orient: vertical;
34 | -webkit-line-clamp: $index;
35 | overflow: hidden;
36 | }
--------------------------------------------------------------------------------
/view/.vscode/vue.code-snippets:
--------------------------------------------------------------------------------
1 | {
2 | // Place your web-Construct 工作区 snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and
3 | // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope
4 | // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is
5 | // used to trigger the snippet and the body will be expanded and inserted. Possible variables are:
6 | // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders.
7 | // Placeholders with the same ids are connected.
8 | // Example:
9 | // "Print to console": {
10 | // "scope": "javascript,typescript",
11 | // "prefix": "log",
12 | // "body": [
13 | // "console.log('$1');",
14 | // "$2"
15 | // ],
16 | // "description": "Log output to console"
17 | // }
18 | }
--------------------------------------------------------------------------------
/scheduler/src/project/project.module.ts:
--------------------------------------------------------------------------------
1 | import { Module } from '@nestjs/common';
2 | import { ProjectController } from './project.controller';
3 | import { ProjectService } from './project.service';
4 | import { ProjectGateway } from './project.gateway';
5 | import { ReleaseService } from './release/release.service';
6 | import { ReleaseController } from './release/release.controller';
7 | import { ProcessController } from './process/process.controller';
8 | import { ProcessService } from './process/process.service';
9 | import { CommandController } from './command/command/command.controller';
10 | import { CommandService } from './command/command/command.service';
11 |
12 | @Module({
13 | controllers: [ProjectController, ReleaseController, ProcessController, CommandController],
14 | providers: [ProjectService, ProjectGateway, ReleaseService, ProcessService, CommandService]
15 | })
16 | export class ProjectModule {}
17 |
--------------------------------------------------------------------------------
/scheduler/src/middleware/root/root.middleware.ts:
--------------------------------------------------------------------------------
1 | import { decrypt } from '@/utils/verify';
2 | import { Injectable, NestMiddleware } from '@nestjs/common';
3 | import { NextFunction, Response, Request } from 'express';
4 |
5 | @Injectable()
6 | export class RootMiddleware implements NestMiddleware {
7 | use(req: Request, res: Response, next: NextFunction) {
8 | let headers: any = req.headers
9 | try {
10 | // 令牌(token)私钥解密
11 | let token: {
12 | account: string;
13 | access: string;
14 | password: string;
15 | job_id: string;
16 | valid: number;
17 | date: number
18 | } = JSON.parse(decrypt(headers.token))
19 | if (token.access === '1') {
20 | next();
21 | } else {
22 | res.status(200).send({ code: 500, msg: '权限不足,需要超级用户权限' })
23 | }
24 | } catch (error) {
25 | res.status(200).send({ code: 500, msg: error.message })
26 | }
27 | }
28 | }
29 |
--------------------------------------------------------------------------------
/scheduler/src/members/members.sql.ts:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose');
2 | // import mongoose from 'mongoose'
3 |
4 | // 标签
5 | const memberSql = new mongoose.Schema({
6 | name: {
7 | type: String,
8 | required: true
9 | },
10 | job_id: {
11 | type: String,
12 | required: true
13 | },
14 | account: {
15 | type: String,
16 | required: true
17 | },
18 | access: {
19 | type: String,
20 | default: '0'
21 | },
22 | pwd: {
23 | type: String,
24 | required: true
25 | },
26 | jobName: {
27 | type: String,
28 | required: true
29 | },
30 | remark: {
31 | type: String,
32 | required: true
33 | },
34 | date: {
35 | type: Number,
36 | default: () => {
37 | return Math.round(new Date() as any)
38 | }
39 | },
40 | });
41 |
42 |
43 | export const memberModel = mongoose.model('member', memberSql);
--------------------------------------------------------------------------------
/scheduler/lib/rsa_private_key_1024.txt:
--------------------------------------------------------------------------------
1 | -----BEGIN PRIVATE KEY-----
2 | MIICdwIBADANBgkqhkiG9w0BAQEFAASCAmEwggJdAgEAAoGBAJnvnnXe5hkSWjIV
3 | y0mnV6fcDAd7qhcgDGlS2YJAkmZdOLv0S85GEKQ4ec1LsLNY1oISu0Q041yjQ+1+
4 | /Ps/hWYVr/Bpmp/+Ple4TtW845QXlBkm/iF0kl6G9t5+LaYjfIqVX8yHrWS197Yi
5 | BWKWdlLXbImuAPAWgpgRRPTn5hvtAgMBAAECgYEAkvZZal7rfnQu+DImaqMA5JkI
6 | QvylXn6SWttmHRxYqLHHQGnzFXN+38mrP3xLrm3aHUSU1IFJr1+PsmXSUGxMhbZb
7 | pjT9je7HFsrW/994/oncjesGskqApTe3uS4tiHEz/ZqrLwiuDn6ppJh8Z6vjNJin
8 | RaDhoCjx10y/a8+wSQECQQDNU41yuLM0UV9xBBGzV2+K5PryVlP+iApLyzVBCL99
9 | mWuEeHQyBl3xW2sy61oY8NfZic/kjaTONpyodb7CK1gpAkEAv+09z311EZbLn6VT
10 | P2vgdFnqnwRQtos69dK5OIi9ipH7jo9i2806f6SPiEkA5SxQwEzZ5ZIO6dKswl7W
11 | xNMuJQJAQrRRMKL2fRmugv3KblQKhboMGKyZDwjNJ4B15ZHc/AXamyeywAm3fVCw
12 | y9MP6yhbR23xHxQxJVkynbva5CPGuQJAfr5d9jsEZ3QsziWwfw5vcIa8oPyJjLMP
13 | Ya0h8gqjGPJkrHVWNI8oFYkH3FlRhQYp4YQe1TMKNDGTMtzMebh4HQJBAMla7vQt
14 | Q/xWCCVbd7yGjstJZNvE1GKys1kwnkwbbvfqHN2rJVYUJSmyNah5MxjBEA8cmu23
15 | Qog8F+yPvRjuOfY=
16 | -----END PRIVATE KEY-----
--------------------------------------------------------------------------------
/cluster/src/plugin/tool.ts:
--------------------------------------------------------------------------------
1 | export function uuid(len: number, radix: number) {
2 | const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
3 | const uuid = [];
4 | let i;
5 | radix = radix || chars.length;
6 |
7 | if (len) {
8 | // Compact form
9 | for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random() * radix];
10 | } else {
11 | // rfc4122, version 4 form
12 | let r;
13 |
14 | // rfc4122 requires these characters
15 | uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
16 | uuid[14] = '4';
17 |
18 | // Fill in random data. At i==19 set the high bits of clock sequence as
19 | // per rfc4122, sec. 4.1.5
20 | for (i = 0; i < 36; i++) {
21 | if (!uuid[i]) {
22 | r = 0 | Math.random() * 16;
23 | uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
24 | }
25 | }
26 | }
27 |
28 | return uuid.join('');
29 | }
--------------------------------------------------------------------------------
/cluster/src/equipment/equipment.interface.ts:
--------------------------------------------------------------------------------
1 | import os, { CpuInfo } from 'os';
2 |
3 | export interface Disk {
4 | filesystem: string
5 | blocks: string
6 | used: string
7 | available: string
8 | capacity: string
9 | mounted: string
10 | }
11 |
12 | export interface SystemInfo {
13 | ip?: string;
14 | port?: number;
15 | cpus?: CpuInfo[];
16 | arch?: string;
17 | totalmem?: number;
18 | hostname?: string;
19 | networkInterfaces?: NodeJS.Dict;
20 | platform?: string;
21 | disk?: Disk[]
22 | }
23 |
24 |
25 | export interface _File {
26 | fieldname: string,
27 | originalname: string,
28 | encoding: string,
29 | mimetype: string,
30 | buffer: Buffer
31 | size: number
32 | }
33 |
34 | export interface EnvInfo {
35 | dev: {
36 | port: string;
37 | };
38 | test: {
39 | port: string;
40 | };
41 | uat: {
42 | port: string;
43 | };
44 | prod: {
45 | port: string;
46 | };
47 | }
48 |
--------------------------------------------------------------------------------
/scheduler/src/middleware/user-update/user-update.middleware.ts:
--------------------------------------------------------------------------------
1 | import { decrypt } from '@/utils/verify';
2 | import { Injectable, NestMiddleware } from '@nestjs/common';
3 | import { NextFunction, Response, Request } from 'express';
4 |
5 | @Injectable()
6 | export class UserUpdateMiddleware implements NestMiddleware {
7 | use(req: Request, res: Response, next: NextFunction) {
8 | let headers: any = req.headers
9 | try {
10 | // 令牌(token)私钥解密
11 | let token: {
12 | account: string;
13 | access: string;
14 | password: string;
15 | job_id: string;
16 | valid: number;
17 | date: number
18 | } = JSON.parse(decrypt(headers.token))
19 | if (req.body.account === token.account) {
20 | next();
21 | } else if (token.access === '1') {
22 | next();
23 | } else {
24 | res.status(200).send({ code: 500, msg: '权限不足,需要超级用户权限' })
25 | }
26 | } catch (error) {
27 | res.status(200).send({ code: 500, msg: '权限不足,需要超级用户权限' })
28 | }
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/scheduler/src/cluster/cluster.sql.ts:
--------------------------------------------------------------------------------
1 | const mongoose = require('mongoose')
2 |
3 | const HostSql = new mongoose.Schema(
4 | {
5 | arch: {
6 | type: String,
7 | required: true
8 | },
9 | hostname: {
10 | type: String,
11 | required: true
12 | },
13 | cpus: {
14 | type: Array,
15 | required: true
16 | },
17 | ip: {
18 | type: String,
19 | index: true,
20 | required: true
21 | },
22 | platform: {
23 | type: String,
24 | required: true
25 | },
26 | port: {
27 | type: String,
28 | required: true
29 | },
30 | totalmem: {
31 | type: String,
32 | required: true
33 | },
34 | date: {
35 | type: Number,
36 | default: () => {
37 | return Math.round(new Date() as any)
38 | }
39 | },
40 | }
41 | )
42 |
43 |
44 | export const hostModel = mongoose.model('host', HostSql);
--------------------------------------------------------------------------------
/view/src/plugin/el-drag.ts:
--------------------------------------------------------------------------------
1 | function mousedown(event: MouseEvent, node: string | HTMLElement) {
2 | event.stopPropagation();
3 | const el: HTMLElement = typeof (node) === 'string' ? document.querySelector(`#${node}`) as HTMLElement : node
4 | el.style.zIndex = Math.round(new Date() as any / 1000).toString()
5 | const leftDifference = event.pageX - el.getBoundingClientRect().left;
6 | const topDifference = event.pageY - el.getBoundingClientRect().top;
7 | function mousemove(event: MouseEvent) {
8 | const top = event.pageY - topDifference;
9 | const left = event.pageX - leftDifference;
10 | el.style.transition = "none";
11 | el.style.left = (left <= 0 ? -4 : left) + "px";
12 | el.style.top = (top <= 0 ? -4 : top) + "px";
13 | }
14 | function mouseup() {
15 | document.removeEventListener("mousemove", mousemove);
16 | document.removeEventListener("mouseup", mouseup);
17 | }
18 | document.addEventListener("mousemove", mousemove);
19 | document.addEventListener("mouseup", mouseup);
20 | }
21 |
22 |
23 | export default mousedown
--------------------------------------------------------------------------------
/view/src/views/container.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
21 |
22 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 梦客
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/scheduler/src/project/command/command/command.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { commandModel } from '@/project/project.sql'
3 | import { _find, _findOne, _remove, _save } from '@/utils/sql';
4 | import { uuid } from '@/utils/tool';
5 |
6 | @Injectable()
7 | export class CommandService {
8 | // 添加指令
9 | async CommandAdd(body: { command: string; args: string | string[]; project_id: string; commandId?: string }) {
10 | body.args = (body.args as string).split(',')
11 | body.commandId = uuid(16, 32)
12 | const sql = new commandModel(body)
13 | const data = await _save(sql)
14 | return data
15 | }
16 |
17 | // 指令列表
18 | async CommandList(body: { project_id: string }) {
19 | const res: any = await _find({ sql: commandModel, params: { project_id: body.project_id }, sort: { date: -1 } })
20 | return res
21 | }
22 |
23 | // 删除指令
24 | async CommandRemove(body: { commandId: string }) {
25 | const res = await _remove(commandModel, { commandId: body.commandId })
26 | return res
27 | }
28 |
29 |
30 |
31 |
32 |
33 | }
34 |
--------------------------------------------------------------------------------
/cluster/src/release/release.controller.ts:
--------------------------------------------------------------------------------
1 | import { Body, Controller, Post, UploadedFile, UseInterceptors } from '@nestjs/common';
2 | import { FileInterceptor } from '@nestjs/platform-express';
3 | import { _File, EnvInfo } from 'src/equipment/equipment.interface';
4 | import { ReleaseService } from 'src/release/release.service'
5 |
6 | @Controller('release')
7 | export class ReleaseController {
8 | constructor(private readonly releaseService: ReleaseService) { }
9 | @Post('dev_release')
10 | @UseInterceptors(FileInterceptor("file"))
11 | async dev_release(@UploadedFile() file: _File, @Body() body) {
12 | body.file = file
13 | return this.releaseService.env_release(body)
14 | }
15 | @Post('prod_release')
16 | @UseInterceptors(FileInterceptor("file"))
17 | async prod_release(@UploadedFile() file: _File, @Body() body) {
18 | body.file = file
19 | return this.releaseService.env_release(body)
20 | }
21 | @Post('init-env')
22 | async init_env(@Body() body: { id: string; envInfo: EnvInfo, type: string; name: string }) {
23 | return this.releaseService.init_env(body)
24 | }
25 |
26 | }
27 |
--------------------------------------------------------------------------------
/cluster/src/config.ts:
--------------------------------------------------------------------------------
1 | import * as os from 'os'
2 | import * as fs from 'fs-extra';
3 | import * as path from 'path';
4 | import * as NodeRSA from 'node-rsa'
5 | const package_ci = path.join(os.userInfo().homedir, 'package-ci-static')
6 | const prikey_path = path.join(package_ci, 'rsa_private_key_1024.txt')
7 | const package_server = path.join(package_ci, 'server.js')
8 | const token = 'package-ci'
9 |
10 |
11 | function sign(text: string) {
12 | // 找不到私钥直接中断流程
13 | if (!fs.existsSync(prikey_path) || !text) return false
14 | // 读取私钥
15 | const privateKey = fs.readFileSync(prikey_path);
16 | // 解密
17 | const nodersa = new NodeRSA(privateKey);
18 | return token === nodersa.decrypt(text, 'utf8');
19 | }
20 |
21 | function init() {
22 | // 判断资源目录是否存在,不存在则创建
23 | fs.ensureDirSync(package_ci)
24 | /*
25 | 读取服务文件
26 | 该文件源码为koa实现由ncc编译,主要功能为托管web静态资源。
27 | */
28 | let server = fs.readFileSync(path.join(__dirname, '../process/index.js'), 'utf-8')
29 |
30 | // 将服务文件内容写入资源目录
31 | fs.writeFileSync(package_server, server)
32 | }
33 |
34 | export default {
35 | package_ci,
36 | package_server,
37 | init,
38 | sign
39 | }
40 |
41 |
--------------------------------------------------------------------------------
/view/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "vite-project",
3 | "version": "0.0.0",
4 | "scripts": {
5 | "dev": "vite --mode development",
6 | "build": "vue-tsc --noEmit && vite build",
7 | "preview": "vite preview"
8 | },
9 | "dependencies": {
10 | "axios": "^0.25.0",
11 | "element-plus": "^1.3.0-beta.9",
12 | "leader-line": "^1.0.7",
13 | "pinia": "^2.0.14",
14 | "qs": "^6.10.3",
15 | "socket.io-client": "^4.4.1",
16 | "vue": "^3.2.25",
17 | "vue-router": "4"
18 | },
19 | "devDependencies": {
20 | "@types/node": "^17.0.12",
21 | "@types/qs": "^6.9.7",
22 | "@vicons/antd": "^0.12.0",
23 | "@vicons/carbon": "^0.12.0",
24 | "@vicons/fa": "^0.12.0",
25 | "@vicons/fluent": "^0.12.0",
26 | "@vicons/ionicons4": "^0.12.0",
27 | "@vicons/ionicons5": "^0.12.0",
28 | "@vicons/material": "^0.12.0",
29 | "@vicons/tabler": "^0.12.0",
30 | "@vitejs/plugin-vue": "^2.0.0",
31 | "naive-ui": "^2.29.0",
32 | "node-sass": "^7.0.1",
33 | "sass": "^1.49.0",
34 | "sass-loader": "^12.4.0",
35 | "typescript": "^4.4.4",
36 | "unplugin-auto-import": "^0.5.11",
37 | "unplugin-vue-components": "^0.19.6",
38 | "vfonts": "^0.0.3",
39 | "vite": "^2.7.2",
40 | "vue-tsc": "^0.29.8"
41 | }
42 | }
43 |
--------------------------------------------------------------------------------
/scheduler/src/project/release/release.controller.ts:
--------------------------------------------------------------------------------
1 | import { Body, Controller, Post } from '@nestjs/common';
2 | import { ReleaseService } from './release.service';
3 | @Controller('release')
4 | export class ReleaseController {
5 | constructor(private readonly releaseService: ReleaseService) { }
6 | @Post('release-list')
7 | release_list(@Body() body: { id: string }) {
8 | return this.releaseService.release_list({ id: body.id });
9 | }
10 | @Post('dev-release')
11 | dev_release(@Body() body: { id: string; release_id: string; uid: string; env: string }) {
12 | body.env = 'DEV';
13 | return this.releaseService.env_release(body);
14 | }
15 | @Post('test-release')
16 | test_release(@Body() body: { id: string; release_id: string; uid: string; env: string }) {
17 | body.env = 'TEST';
18 | return this.releaseService.env_release(body);
19 | }
20 | @Post('uat-release')
21 | uat_release(@Body() body: { id: string; release_id: string; uid: string; env: string }) {
22 | body.env = 'UAT';
23 | return this.releaseService.env_release(body);
24 | }
25 | @Post('prod-release')
26 | prod_release(@Body() body: { id: string; release_id: string; uid: string; env: string }) {
27 | body.env = 'PROD';
28 | return this.releaseService.env_release(body);
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/view/components.d.ts:
--------------------------------------------------------------------------------
1 | // generated by unplugin-vue-components
2 | // We suggest you to commit this file into source control
3 | // Read more: https://github.com/vuejs/vue-next/pull/3399
4 | import '@vue/runtime-core'
5 |
6 | declare module '@vue/runtime-core' {
7 | export interface GlobalComponents {
8 | Aside: typeof import('./src/components/Aside.vue')['default']
9 | BranchConfig: typeof import('./src/components/branch-config.vue')['default']
10 | Content: typeof import('./src/components/Content.vue')['default']
11 | CustomizeConfig: typeof import('./src/components/customize-config.vue')['default']
12 | Header: typeof import('./src/components/Header.vue')['default']
13 | Instruction: typeof import('./src/components/instruction.vue')['default']
14 | ModalBox: typeof import('./src/components/ModalBox.vue')['default']
15 | ModalView: typeof import('./src/components/modal-view.vue')['default']
16 | ProjectUpdate: typeof import('./src/components/project-update.vue')['default']
17 | Release_list: typeof import('./src/components/release_list.vue')['default']
18 | RouterLink: typeof import('vue-router')['RouterLink']
19 | RouterView: typeof import('vue-router')['RouterView']
20 | Versions: typeof import('./src/components/versions.vue')['default']
21 | }
22 | }
23 |
24 | export {}
25 |
--------------------------------------------------------------------------------
/scheduler/src/members/members.controller.ts:
--------------------------------------------------------------------------------
1 | import { Body, Controller, Get, Param, Post, Query, Req } from '@nestjs/common';
2 | import { MembersService } from './members.service'
3 | import { Parameter, MembersList } from './members.interface'
4 | import { Request } from 'express';
5 | @Controller('members')
6 | export class MembersController {
7 | constructor(private readonly membersService: MembersService) { }
8 | // 录入成员
9 | @Post('create')
10 | Create(@Body() body: Parameter) {
11 | return this.membersService.Create(body)
12 | }
13 | // 成员列表
14 | @Post('read')
15 | Read(@Body() body: MembersList) {
16 | return this.membersService.Read(body)
17 | }
18 |
19 | // 成员删除
20 | @Post('delete')
21 | Delete(@Body() body: { id: string }) {
22 | return this.membersService.Delete(body)
23 | }
24 |
25 | // 成员修改
26 | @Post('update')
27 | Update(@Body() body: Parameter) {
28 | return this.membersService.Update(body)
29 | }
30 |
31 | // 成员登陆
32 | @Get('login')
33 | Login(@Query() params: { account: string; password: string; valid: number }) {
34 | return this.membersService.Login(params)
35 | }
36 |
37 | // 成员信息
38 | @Post('user-info')
39 | UserInfo(@Req() req: any) {
40 | return this.membersService.UserInfo(req.headers.token)
41 | }
42 |
43 |
44 | }
45 |
--------------------------------------------------------------------------------
/scheduler/src/utils/shell.ts:
--------------------------------------------------------------------------------
1 | import { spawn, SpawnOptionsWithoutStdio } from 'child_process';
2 |
3 | export interface ImplementShell {
4 | id: string
5 | command: string
6 | args: string[]
7 | options?: SpawnOptionsWithoutStdio,
8 | callback?: (type: string, data: string) => void
9 | }
10 |
11 | export interface ShellParameter {
12 | sid: string
13 | id: string
14 | command: string
15 | args: string[]
16 | }
17 |
18 | const PrimaryKey = {}
19 |
20 | export function kill(id: string) {
21 | PrimaryKey[id].kill()
22 | }
23 |
24 |
25 | export function implementShell({ id, command, args, options, callback }: ImplementShell) {
26 |
27 | if (!(command === 'yarn' || command === 'npm')) {
28 | callback('close', '非法指令,程序已退出。')
29 | return
30 | }
31 |
32 | if (process.platform === 'win32') {
33 | command += '.cmd'
34 | }
35 | const Shell = spawn(command, args, options);
36 |
37 | PrimaryKey[id] = Shell
38 |
39 | Shell.stdout.on('data', (data) => {
40 | callback('stdout', data)
41 | });
42 |
43 |
44 | Shell.stderr.on('data', (data) => {
45 | callback('stderr', data)
46 | });
47 |
48 | Shell.on('close', (code) => {
49 | callback('close', '子进程执行完毕,程序已退出。')
50 | });
51 |
52 | Shell.on('error', (data) => {
53 | console.log(data);
54 | })
55 |
56 | }
57 |
--------------------------------------------------------------------------------
/cluster/src/equipment/equipment.controller.ts:
--------------------------------------------------------------------------------
1 | import { Body, Controller, Post, UploadedFile, UseInterceptors, Headers } from '@nestjs/common';
2 | import { FileInterceptor } from '@nestjs/platform-express';
3 | import { EquipmentService } from './equipment.service'
4 | import { _File } from './equipment.interface';
5 |
6 | @Controller('equipment')
7 | export class EquipmentController {
8 | constructor(private readonly equipmentService: EquipmentService) { }
9 | @Post('system')
10 | getSystem() {
11 | return this.equipmentService.getSystem()
12 | }
13 | @Post('process-list')
14 | ProcessList() {
15 | return this.equipmentService.process_list()
16 | }
17 |
18 | @Post('process-kill')
19 | ProcessKill(@Body() body: { ip: string; id: string }) {
20 | return this.equipmentService.process_kill(body)
21 | }
22 |
23 | @Post('process-kill-all')
24 | ProcessKillAll() {
25 | return this.equipmentService.process_killAll()
26 | }
27 |
28 | @Post('process-init')
29 | ProcessInit() {
30 | return this.equipmentService.process_init()
31 | }
32 |
33 | @Post('process-start')
34 | ProcessStart(@Body() body: { ip: string; id: string }) {
35 | return this.equipmentService.process_start(body)
36 | }
37 | @Post('process-delete')
38 | ProcessDelete(@Body() body: {id: string }) {
39 | return this.equipmentService.process_delete(body)
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/view/src/App.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
52 |
53 |
--------------------------------------------------------------------------------
/view/src/assets/qp.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/view/src/assets/devops.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/view/public/static/images/PACKAGE-CI_files/qp.9109b21e.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/view/src/stores/global.ts:
--------------------------------------------------------------------------------
1 | import { Members_details } from '@/http/api'
2 | import { Project } from '@/interface'
3 | import { isArray } from '@vue/shared'
4 | import { defineStore } from 'pinia'
5 |
6 | export const useCounterStore = defineStore('counter', {
7 | state: () => {
8 | return {
9 | loading: false, personal: {
10 | account: '',
11 | access: '0',
12 | beforePwd: '',
13 | pwd: '',
14 | name: '',
15 | jobName: '',
16 | remark: '',
17 | job_id: ''
18 | }
19 | }
20 | },
21 | actions: {
22 | setLoad(load: boolean) {
23 | this.loading = load
24 | },
25 | async user_details() {
26 | const res = await Members_details()
27 | if (res.code === 200) {
28 | for (const key in res.data) {
29 | this.personal[key] = res.data[key]
30 | }
31 | console.log(this.personal);
32 |
33 | }
34 | },
35 | permission(project: Project, identity: string[]): boolean {
36 | let job_id = this.personal.job_id
37 | if (identity.indexOf('qa') != -1 && job_id === project.qa) return true
38 | if (identity.indexOf('pm') != -1 && job_id === project.pm) return true
39 | if (identity.indexOf('dev') != -1 && project.devs.indexOf(job_id) != -1) return true
40 | return false
41 | }
42 | },
43 | })
--------------------------------------------------------------------------------
/scheduler/src/app.module.ts:
--------------------------------------------------------------------------------
1 | import { MiddlewareConsumer, Module, NestModule, RequestMethod } from '@nestjs/common';
2 | import { AppController } from './app.controller';
3 | import { AppService } from './app.service';
4 | import { MembersModule } from './members/members.module';
5 | import { ProjectModule } from './project/project.module';
6 | import { ClusterModule } from './cluster/cluster.module';
7 | import { AuthenticationMiddleware } from './middleware/authentication/authentication.middleware'
8 | import { RootMiddleware } from './middleware/root/root.middleware'
9 | import { UserUpdateMiddleware } from './middleware/user-update/user-update.middleware'
10 |
11 | @Module({
12 | imports: [MembersModule, ProjectModule, ClusterModule],
13 | controllers: [AppController],
14 | providers: [AppService],
15 | })
16 | export class AppModule implements NestModule {
17 | configure(consumer: MiddlewareConsumer) {
18 | consumer
19 | .apply(AuthenticationMiddleware)
20 | .exclude({ path: '/members/login', method: RequestMethod.ALL })
21 | .forRoutes('project', 'release', 'process', 'cluster', 'members')
22 | .apply(UserUpdateMiddleware)
23 | .forRoutes({ path: '/members/update', method: RequestMethod.ALL })
24 | .apply(RootMiddleware)
25 | .forRoutes(
26 | { path: '/cluster/cluster-remove', method: RequestMethod.ALL },
27 | { path: '/members/delete', method: RequestMethod.ALL },
28 | { path: '/project/remove', method: RequestMethod.ALL },
29 | { path: '/project/create', method: RequestMethod.ALL },
30 | )
31 | }
32 | }
--------------------------------------------------------------------------------
/scheduler/src/middleware/authentication/authentication.middleware.ts:
--------------------------------------------------------------------------------
1 | import { Injectable, NestMiddleware } from '@nestjs/common';
2 | import { NextFunction, Response, Request } from 'express';
3 | import { encrypt, decrypt } from '@/utils/verify'
4 | import { _findOne, ModelResult } from '@/utils/sql';
5 | import { memberModel } from '@/members/members.sql'
6 |
7 | @Injectable()
8 | export class AuthenticationMiddleware implements NestMiddleware {
9 | async use(req: Request, res: Response, next: NextFunction) {
10 | let headers: any = req.headers
11 | try {
12 | // 令牌(token)私钥解密
13 | let token: {
14 | account: string;
15 | password: string;
16 | job_id: string;
17 | valid: number;
18 | date: number
19 | } = JSON.parse(decrypt(headers.token))
20 |
21 |
22 | // 登录时间超过登录时指定的有效时长(默认一天),令牌失效
23 | if ((Math.round(new Date() as any) - token.date) > token.valid) {
24 | res.status(200).send({ code: 304, msg: '无效的令牌' })
25 | return
26 | }
27 |
28 | // 用户数据数据查询
29 | const user: ModelResult = await _findOne(memberModel, { account: token.account })
30 |
31 | // 数据查询失败
32 | if (user.code != 200) {
33 | res.status(200).send({ code: 304, msg: '无效的令牌' })
34 | return
35 | }
36 | // 密码错误
37 | if (user.data.pwd != token.password) {
38 | res.status(200).send({ code: 304, msg: '无效的令牌' })
39 | return
40 | }
41 | next()
42 | } catch (error) {
43 | // 解密过程中出现未知错误
44 | res.status(200).send({ code: 304, msg: '无效的令牌' })
45 | }
46 | }
47 | }
48 |
--------------------------------------------------------------------------------
/view/src/assets/out.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/view/public/static/images/PACKAGE-CI_files/out.86cda219.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scheduler/src/cluster/cluster.controller.ts:
--------------------------------------------------------------------------------
1 | import { Body, Controller, Post } from '@nestjs/common';
2 | import { ClusterService } from './cluster.service'
3 | import { Host } from './cluster.interface'
4 |
5 | @Controller('cluster')
6 | export class ClusterController {
7 | constructor(private readonly clusterService: ClusterService) { }
8 | @Post('load-config')
9 | loadConfig(@Body() body: { ip: string; port: number }) {
10 | return this.clusterService.loadConfig(body)
11 | }
12 | @Post('cluster-create')
13 | clusterCreate(@Body() body: Host) {
14 | return this.clusterService.clusterCreate(body)
15 | }
16 | @Post('cluster-list')
17 | clusterList() {
18 | return this.clusterService.clusterList()
19 | }
20 | @Post('cluster-remove')
21 | clusterRemove(@Body() body: { ip: string }) {
22 | return this.clusterService.clusterRemove(body)
23 | }
24 |
25 | @Post('process-list')
26 | processList(@Body() body: { ip: string }) {
27 | return this.clusterService.process_list(body)
28 | }
29 | @Post('process-kill')
30 | ProcessKill(@Body() body: { ip: string; id: string }) {
31 | return this.clusterService.process_kill(body)
32 | }
33 | @Post('process-kill-all')
34 | ProcessKillAll(@Body() body: { ip: string; }) {
35 | return this.clusterService.process_killAll(body)
36 | }
37 |
38 | @Post('process-init')
39 | processInit(@Body() body: { ip: string; }) {
40 | return this.clusterService.process_init(body)
41 | }
42 |
43 | @Post('process-start')
44 | ProcessStart(@Body() body: { ip: string; id: string }) {
45 | return this.clusterService.process_start(body)
46 | }
47 |
48 | @Post('process-delete')
49 | ProcessDelete(@Body() body: { ip: string; id: string }) {
50 | return this.clusterService.process_delete(body)
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/view/src/assets/css/ress.scss:
--------------------------------------------------------------------------------
1 | HTML,
2 | body,
3 | div,
4 | h1,
5 | h2,
6 | h3,
7 | h4,
8 | h5,
9 | h6,
10 | ul,
11 | ol,
12 | dl,
13 | li,
14 | dt,
15 | dd,
16 | p,
17 | blockquote,
18 | pre,
19 | form,
20 | fieldset,
21 | table,
22 | th,
23 | td {
24 | border: none;
25 | font-family: "Helvetica Neue", Helvetica, PingFangSC, "PingFang SC", NotoSansHans, "Hiragino Sans GB",
26 | "Lantinghei SC", "Microsoft Yahei", 微软雅黑, STXihei, "WenQuanYi Micro Hei", Arial, sans-serif;
27 | font-size: 12px;
28 | margin: 0px;
29 | padding: 0px;
30 | text-align: left;
31 | }
32 |
33 | html,
34 | body {
35 | height: 100%;
36 | width: 100%;
37 | }
38 |
39 | address,
40 | caption,
41 | cite,
42 | code,
43 | dfn,
44 | em,
45 | strong,
46 | th,
47 | var {
48 | font-style: normal;
49 | font-weight: normal;
50 | }
51 |
52 | a {
53 | text-decoration: none;
54 | }
55 |
56 | a:link {
57 | color: #fff;
58 | }
59 |
60 | a:visited {
61 | color: #fff;
62 | }
63 |
64 | a:hover {
65 | color: #fff;
66 | }
67 |
68 | a:active {
69 | color: #fff;
70 | }
71 |
72 | input::-ms-clear {
73 | display: none;
74 | }
75 |
76 | input::-ms-reveal {
77 | display: none;
78 | }
79 |
80 | input {
81 | -webkit-appearance: none;
82 | margin: 0;
83 | outline: none;
84 | padding: 0;
85 | }
86 |
87 | input::-webkit-input-placeholder {
88 | color: #ccc;
89 | }
90 |
91 | input::-ms-input-placeholder {
92 | color: #ccc;
93 | }
94 |
95 | input::-moz-placeholder {
96 | color: #ccc;
97 | }
98 |
99 | input[type="submit"],
100 | input[type="button"] {
101 | cursor: pointer;
102 | }
103 |
104 | button[disabled],
105 | input[disabled] {
106 | cursor: default;
107 | }
108 |
109 | img {
110 | border: none;
111 | }
112 |
113 | ul,
114 | ol,
115 | li {
116 | list-style-type: none;
117 | }
--------------------------------------------------------------------------------
/scheduler/src/utils/file.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs-extra'
2 | import * as path from 'path'
3 | import * as archiver from 'archiver';
4 |
5 |
6 | export function dir_copy(src: string, dest: string) {
7 | return new Promise((resolve) => {
8 | fs.copy(src, dest,
9 | { filter: (filePath: string) => !filePath.includes('node_modules') },
10 | (err: Error): void => {
11 | resolve({ code: err ? 500 : 200, msg: err && err.message })
12 | }
13 | )
14 | })
15 | }
16 |
17 | export function dir_remove(dirPath: string) {
18 | const Files = fs.readdirSync(dirPath)
19 | Files.forEach((fileName) => {
20 | let item = `${dirPath}/${fileName}`
21 | if (fs.existsSync(item) && fileName != 'node_modules') fs.removeSync(item)
22 | })
23 | }
24 |
25 |
26 | export function emptyDir(src: string) {
27 | return new Promise((resolve) => {
28 | fs.emptyDir(src, (err: Error) => {
29 | resolve({ code: err ? 500 : 200, msg: err && err.message })
30 | })
31 | })
32 | }
33 |
34 | export function BaleDirectory(src: string, dist: string, fileName: string) {
35 | return new Promise((resolve) => {
36 |
37 | // 创建一个文件以将归档数据流式传输到。
38 | const output = fs.createWriteStream(`${dist}/${fileName}`);
39 | const archive = archiver('zip', {
40 | zlib: { level: 9 }
41 | });
42 |
43 | // 监听所有要写入的归档数据
44 | // 'close' 事件仅在涉及文件描述符时触发
45 | output.on('close', function () {
46 | resolve({ code: 200, msg: '指定目录压缩完毕' })
47 | });
48 |
49 | // 明确捕获此错误的好习惯
50 | archive.on('error', function (err: Error) {
51 | resolve({ code: 500, msg: err && err.message })
52 | });
53 |
54 | // 管道归档数据到文件
55 | archive.pipe(output);
56 |
57 | // 附加子目录中的文件,将其内容放在存档的根目录
58 | archive.directory(src, false);
59 |
60 | archive.finalize();
61 | })
62 | }
63 |
64 |
--------------------------------------------------------------------------------
/view/src/assets/css/public.scss:
--------------------------------------------------------------------------------
1 | .icon {
2 | width: 1em;
3 | height: 1em;
4 | vertical-align: -0.15em;
5 | fill: currentColor;
6 | overflow: hidden;
7 | }
8 | .el-input__inner {
9 | padding: 0 8px !important;
10 | }
11 |
12 | *::-webkit-scrollbar {
13 | width: 4px !important;
14 | height: 4px !important;
15 | }
16 | *::-webkit-scrollbar-thumb {
17 | width: 4px !important;
18 | height: 4px !important;
19 | border-radius: 10px !important;
20 | background-color: #00bebe !important;
21 | }
22 |
23 | .el-dialog__body {
24 | padding: 10px 20px 10px 20px !important;
25 | }
26 |
27 |
28 |
29 | .file-loading {
30 | width: 100px;
31 | height: 40px;
32 | margin: 0 auto;
33 | user-select: none;
34 | span {
35 | display: inline-block;
36 | width: 8px;
37 | height: 100%;
38 | border-radius: 4px;
39 | margin-right: 5px;
40 | background: lightgreen;
41 | animation: load 0.6s ease infinite;
42 | }
43 | span:nth-child(2) {
44 | animation-delay: 0.13s;
45 | }
46 | span:nth-child(3) {
47 | animation-delay: 0.26s;
48 | }
49 | span:nth-child(4) {
50 | animation-delay: 0.39s;
51 | }
52 | span:nth-child(5) {
53 | animation-delay: 0.52s;
54 | }
55 | }
56 | @keyframes load {
57 | 0%,
58 | 100% {
59 | height: 40px;
60 | background: lightgreen;
61 | }
62 | 50% {
63 | height: 60px;
64 | margin-top: -20px;
65 | background: lightblue;
66 | }
67 | }
68 |
69 | .project-permission {
70 | width: 100%;
71 | height: 100%;
72 | background-color: #f5f5f5;
73 | display: flex;
74 | align-items: center;
75 | justify-content: center;
76 | user-select: none;
77 |
78 | >img {
79 | width: 30px;
80 | height: 30px;
81 | }
82 |
83 | >span {
84 | font-size: 16px;
85 | color: #666;
86 | margin-left: 10px;
87 | text-shadow: 1px 1px 3px rgba($color: #888, $alpha: 0.1);
88 | }
89 | }
90 |
91 |
92 |
--------------------------------------------------------------------------------
/view/src/components/versions.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | {{ scope.row.author_name }}
8 |
9 |
10 |
11 |
12 |
13 |
14 | {{ scope.row.author_email }}
15 |
16 |
17 |
18 |
19 |
20 | {{ dateFormat('YY-mm-dd HH:MM:SS', scope.row.date) }}
21 |
22 |
23 |
24 |
25 |
26 | {{ scope.row.message }}
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
55 |
56 |
57 |
58 |
--------------------------------------------------------------------------------
/view/src/assets/gy.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/cluster/src/plugin/WebServeAdmin.ts:
--------------------------------------------------------------------------------
1 | import http from 'http';
2 | import * as path from 'path';
3 | import { ChildProcess, fork } from 'child_process';
4 | import { ENV, ServeParameter } from './interface';
5 | import { uuid } from './tool';
6 | import PackageConfig from '../config'
7 |
8 | class ServeAdmin {
9 | child: ChildProcess;
10 | id: string;
11 | project_id: string;
12 | env: ENV = {};
13 | status: string;
14 | constructor(serveParameter: ServeParameter) {
15 | this.env.port = serveParameter.port;
16 | this.env.dist = serveParameter.dist;
17 | this.env.name = serveParameter.name;
18 | this.env.id = serveParameter.id;
19 | this.project_id = serveParameter.project_id
20 | this.id = serveParameter.id;
21 | this.env.project = serveParameter.project;
22 | this.init();
23 | }
24 | init() {
25 | const _this = this
26 | /*
27 | 进程启动之后,在获取进程列表,会有一个延迟,所以包装一个微任务进行栈内等待
28 | 原本获取进程列表的时候可以用定时器延迟,但是那并不是一个标准解决方式
29 | */
30 | return new Promise((resolove) => {
31 | _this.child = fork(PackageConfig.package_server, {
32 | env: _this.env as any,
33 | });
34 | // 监听子进程关闭
35 | _this.child.on('exit', () => {
36 | _this.exit(_this.env);
37 | });
38 | // 监听子进程消息
39 | _this.child.on('message', (data) => {
40 | _this.message.call(_this, data)
41 | resolove(true)
42 | })
43 | })
44 | }
45 |
46 | kill() {
47 | this.child.kill();
48 | }
49 |
50 | message(data: { type: string; msg: string }) {
51 | switch (data.type) {
52 | case 'close':
53 | this.child.kill();
54 | break;
55 | case 'listen':
56 | this.listen(this.env);
57 | break;
58 | }
59 | }
60 |
61 | public listen(env: ENV): void {
62 | console.log('\x1B[33m%s\x1B[33m', `服务:项目【${this.env.project}】Id:【${this.env.id}】${this.env.name}环境->${this.env.port}端口运行正常`);
63 | this.status = 'open';
64 | }
65 |
66 | public exit(env: ENV): void {
67 | console.log('\x1B[31m%s\x1B[31m', `错误:项目【${this.env.project}】Id:【${this.env.id}】${this.env.name}环境 ${this.env.port}端口已停止;`)
68 | this.status = 'exit';
69 | }
70 | }
71 |
72 | export default ServeAdmin;
73 |
--------------------------------------------------------------------------------
/view/public/static/images/gy.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/view/public/static/images/PACKAGE-CI_files/gy.b1188c89.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/view/src/plugin/directive.ts:
--------------------------------------------------------------------------------
1 | function loding(): HTMLElement {
2 | const loading = document.createElement('div')
3 | loading.innerHTML = `
4 |
5 |
6 |
7 |
8 |
9 |
10 |
`
11 | loading.className = 'my-loading'
12 | loading.style.width = '100%'
13 | loading.style.height = '100%'
14 | loading.style.backgroundColor = 'rgba(255,255,255,0.5)'
15 | loading.style.position = 'absolute'
16 | loading.style.left = '0'
17 | loading.style.top = '0'
18 | loading.style.display = 'flex'
19 | loading.style.alignItems = 'center'
20 | loading.style.justifyContent = 'center'
21 | loading.style.zIndex = '99999'
22 | return loading
23 | }
24 |
25 |
26 | // 添加虚拟节点(如果不存在)
27 | function addChild(el: Element, className: string) {
28 | let isClass = false
29 | let vnodes = Array.from(el.children)
30 |
31 | vnodes.forEach((el: Element) => {
32 | if (el.className === className) {
33 | isClass = true
34 | }
35 | })
36 |
37 | if (!isClass) {
38 | el.appendChild(el['vnode'])
39 | }
40 |
41 | }
42 |
43 | // 删除虚拟节点(如果存在)
44 | function removeChild(el: Element, className: string) {
45 | let isClass = false
46 | let vnodes = Array.from(el.children)
47 | vnodes.forEach((el: Element) => {
48 | if (el.className === className) {
49 | isClass = true
50 | }
51 | })
52 |
53 | if (isClass) {
54 | el.removeChild(el['vnode'])
55 | }
56 |
57 | }
58 |
59 |
60 |
61 | function directive(instance: any) {
62 | instance.directive('load', {
63 | created(el: HTMLElement) {
64 | el['vnode'] = loding()
65 | },
66 | mounted(el: HTMLElement, binding: any) {
67 | if (binding.value) {
68 | addChild(el, 'my-loading')
69 | }
70 | },
71 | updated(el: any, binding: any) {
72 | if (binding.value) {
73 | addChild(el, 'my-loading')
74 | } else {
75 | removeChild(el, 'my-loading')
76 | }
77 | }
78 | })
79 | }
80 |
81 |
82 |
83 |
84 |
85 | export default directive
--------------------------------------------------------------------------------
/scheduler/src/project/project.gateway.ts:
--------------------------------------------------------------------------------
1 | import { SubscribeMessage, OnGatewayInit, MessageBody, WebSocketGateway, WebSocketServer, OnGatewayConnection, OnGatewayDisconnect } from '@nestjs/websockets';
2 | import { Server, Socket } from 'socket.io';
3 | import { implementShell, ImplementShell, kill, ShellParameter } from '../utils/shell'
4 | import { _findOne } from '../utils/sql'
5 | import * as path from 'path'
6 | import { projectModel } from './project.sql'
7 | import PackageConfig from '@/config'
8 | @WebSocketGateway({
9 | path: '/socket',
10 | allowEIO3: true,
11 | cors: {
12 | origin: /.*/,
13 | credentials: true
14 | }
15 | })
16 | export class ProjectGateway implements OnGatewayInit, OnGatewayConnection, OnGatewayDisconnect {
17 |
18 | @WebSocketServer() private ws: Server;
19 | /**
20 | * 初始化
21 | */
22 | afterInit() {
23 | console.log('socket初始化成功');
24 | }
25 |
26 | /**
27 | * 链接成功
28 | */
29 | handleConnection(client: Socket) {
30 | this.ws.emit('init', { msg: '初始化成功' })
31 | this.ws.emit('install', { msg: 'install' })
32 | }
33 |
34 | /**
35 | * 断开链接
36 | */
37 | handleDisconnect(client: Socket) {
38 |
39 | }
40 |
41 | // 结束shell命令
42 | @SubscribeMessage('shell-kill')
43 | handleKill(client: Socket, body: { id: string }) {
44 | kill(body.id)
45 | }
46 |
47 |
48 | // shell 命令执行
49 | @SubscribeMessage('shell-project')
50 | async handleProject(client: Socket, body: ShellParameter): Promise {
51 | const res: any = await _findOne(projectModel, { project_id: body.id })
52 | let cwd: string = ''
53 | if (res.code === 200) {
54 | cwd = path.join(PackageConfig.static_path, res.data.compile)
55 | }
56 | if (!cwd) {
57 | return
58 | }
59 | implementShell(
60 | {
61 | id: body.sid,
62 | command: body.command,
63 | args: body.args,
64 | options: { cwd },
65 | callback: (type: string, data: string) => {
66 | if (type === 'stdout') {
67 | client.emit('project', { type: 'stdout', text: data.toString() })
68 | } else if (type === 'stderr') {
69 | client.emit('project', { type: 'stderr', text: data.toString() })
70 | } else {
71 | client.emit('project', { type: 'close', text: data.toString() })
72 | }
73 | }
74 | })
75 | }
76 | }
77 |
78 |
--------------------------------------------------------------------------------
/view/src/http/index.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description 网络请求框架封装
3 | */
4 | import Axios from 'axios'
5 | import QS from 'qs'
6 |
7 | Axios.defaults.baseURL = '/'
8 |
9 | // TODO 设置超时时间
10 | Axios.defaults.timeout = 100000
11 |
12 | Axios.defaults.headers = {
13 | 'Access-Control-Allow-Origin': '*',
14 | 'Content-Type': 'application/json'
15 | } as any
16 |
17 | // TODO http code 校验
18 | Axios.defaults.validateStatus = function (status: number) {
19 | return !!status
20 | }
21 |
22 | // TODO GET 请求 params 序列化
23 | Axios.defaults.paramsSerializer = function (params: any) {
24 | return QS.stringify(params)
25 | }
26 |
27 |
28 | // TODO 设置POST等请求 body 序列化
29 | Axios.defaults.transformRequest = [function (body: any) {
30 | const data = body || {}
31 | if (body instanceof window.FormData) {
32 | return body
33 | }
34 |
35 | return JSON.stringify(data)
36 | }]
37 |
38 | /**
39 | * @description 统一 GET 请求
40 | * @param url
41 | * @param params --> GET 请求参数(***?name=walid&age=25)
42 | */
43 | export function get({ url, params = {}, option }: any) {
44 | if (option) {
45 | for (const property in option) {
46 | Axios.defaults[property] = option[property]
47 | }
48 | }
49 | return new Promise((resolve, reject) => {
50 | Axios.get(url, { params: params })
51 | .then((response: any) => {
52 | resolve(response.data)
53 | })
54 | .catch((error: any) => {
55 | reject(error)
56 | })
57 | })
58 | }
59 |
60 | interface POST {
61 | url: string; body?: any; params?: any; option?: any; onUploadProgress?: any;
62 | }
63 |
64 | /**
65 | * @description 统一 POST 请求
66 | * @param url
67 | * @param body --> POST 请求 data
68 | */
69 | export function post({ url, body = {}, params = {}, option, onUploadProgress }: POST) {
70 | if (option) {
71 | for (const property in option) {
72 | Axios.defaults[property] = option[property]
73 | }
74 | }
75 | return new Promise((resolve, reject) => {
76 | Axios.post(url, body, {
77 | params,
78 | onUploadProgress: onUploadProgress,
79 | timeout: 35000000
80 | })
81 | .then((response: any) => {
82 | resolve(response.data)
83 | })
84 | .catch((error: any) => {
85 | reject(error)
86 | })
87 | })
88 | }
89 |
90 |
--------------------------------------------------------------------------------
/cluster/src/plugin/start.ts:
--------------------------------------------------------------------------------
1 | import * as fs from 'fs-extra';
2 | import * as path from 'path';
3 | import ServeAdmin from './WebServeAdmin';
4 | import { ENV, ServeParameter } from './interface';
5 | import { uuid } from './tool';
6 | import PackageConfig from '../config'
7 |
8 | export const process_container = [];
9 |
10 |
11 | export function get_process_container() {
12 | return JSON.parse(JSON.stringify(process_container))
13 | }
14 |
15 | export function start(config: ServeParameter) {
16 | const sub_process = new ServeAdmin(config);
17 | process_container.push(sub_process)
18 | }
19 |
20 | export function init() {
21 | process_container.splice(0, process_container.length)
22 | const dev_path = {
23 | path: path.join(PackageConfig.package_ci, 'DEV'),
24 | dir: 'DEV',
25 | };
26 | const test_path = {
27 | path: path.join(PackageConfig.package_ci, 'TEST'),
28 | dir: 'TEST',
29 | };
30 | const uat_path = {
31 | path: path.join(PackageConfig.package_ci, 'UAT'),
32 | dir: 'UAT',
33 | };
34 | const prod_path = {
35 | path: path.join(PackageConfig.package_ci, 'PROD'),
36 | dir: 'PROD',
37 | };
38 |
39 | let ENVS = [dev_path, test_path, uat_path, prod_path];
40 |
41 | ENVS.forEach((env) => {
42 | // 判断当前环境地址是否存在
43 | if (fs.existsSync(env.path)) {
44 | // 在当前环境目录内所有静态资源目录
45 | const dirs = fs.readdirSync(env.path);
46 |
47 | /*
48 | dirs环境目录,DEV/TEST/UAT/PROD
49 | 遍历部署项目静态资源
50 | */
51 | dirs.forEach((id: string) => {
52 | // 静态资源目录
53 | const dist_path = path.join(env.path, id);
54 |
55 | // 配置文件地址
56 | const config_path = path.join(dist_path, 'env_release_config.json');
57 |
58 | // 检测配置文件是否存在
59 | if (!fs.existsSync(dist_path) || !fs.existsSync(config_path)) {
60 | return;
61 | }
62 | // 从配置文件读取配置信息
63 | const config_json = JSON.parse(fs.readFileSync(config_path).toString());
64 |
65 | if (!config_json.port) {
66 | console.log('\x1B[31m%s\x1B[31m', `错误:项目【${config_json.project_name}】Id:【${config_json.id}】${env.dir}环境 端口未指定->启动失败;`)
67 | return;
68 | }
69 | // 启动服务
70 | start({
71 | id: uuid(16, 32),
72 | project_id: config_json.id,
73 | port: config_json.port,
74 | dist: dist_path,
75 | name: env.dir,
76 | project: config_json.project_name,
77 | });
78 | });
79 | }
80 | });
81 | }
82 |
--------------------------------------------------------------------------------
/view/src/interface.ts:
--------------------------------------------------------------------------------
1 | export interface ENV { port: string, ip: string }
2 | export interface ENVS { DEV?: ENV; TEST?: ENV; UAT?: ENV; PROD?: { port: string, ips: string[] } }
3 |
4 | export interface Project {
5 | project_id?: string;
6 | show?:boolean;
7 | name: string;
8 | envs?: ENVS;
9 | prod_ip?: string;
10 | dev_ip?: string;
11 | gitUrl: string;
12 | localPath?: string;
13 | sourceCode?: string;
14 | branch: string;
15 | qa: string;
16 | devs: string[];
17 | pm: string;
18 | describe: string;
19 | buildName?: string;
20 | notice?: string;
21 | date?: number;
22 | testCount?:number;
23 | uatCount?:number;
24 | prodCount?:number;
25 | _id?: string;
26 | __v?: string;
27 | }
28 |
29 |
30 | export interface Member {
31 | account: string;
32 | access: string;
33 | beforePwd?: string;
34 | pwd?: string;
35 | name: string;
36 | jobName: string;
37 | remark: string;
38 | job_id: string;
39 | date?: number;
40 | _id?: string;
41 | }
42 | export interface Version {
43 | author_email?: string;
44 | author_name?: string;
45 | body?: string;
46 | date?: string;
47 | hash?: string;
48 | message?: string;
49 | refs?: string;
50 | }
51 |
52 | export interface Shell {
53 | command?: string;
54 | args?: string[];
55 | project_id?: string;
56 | commandId?: string;
57 | date?: number
58 | }
59 |
60 | export interface Disk {
61 | filesystem: string;
62 | blocks: string;
63 | used: string;
64 | available: string;
65 | capacity: string;
66 | mounted: string;
67 | schedule: number;
68 | color: string;
69 | }
70 |
71 | export interface Host {
72 | _id?: string;
73 | arch?: string;
74 | cpus?: { model: string; speed: number }[];
75 | disk?: Disk[];
76 | hostname?: string;
77 | ip?: string;
78 | platform?: string;
79 | port?: number;
80 | totalmem?: number;
81 | }
82 |
83 |
84 |
85 |
86 | // 版本构建记录列表数据类型
87 | export interface ReleaseENV { status: null | number, date: null | number, user: null | string }
88 | export interface Release {
89 | DEV: ReleaseENV,
90 | PROD: ReleaseENV,
91 | TEST: ReleaseENV,
92 | UAT: ReleaseENV,
93 | commit_id: string,
94 | date: number,
95 | projectId: string,
96 | size: number,
97 | uid: string,
98 | pack_id?: string,
99 | _id: string,
100 | }
101 |
102 |
--------------------------------------------------------------------------------
/scheduler/src/config.ts:
--------------------------------------------------------------------------------
1 | import * as os from 'os'
2 | import * as path from 'path'
3 | import * as fs from 'fs-extra'
4 | import * as NodeRSA from 'node-rsa'
5 | import { uuid } from './utils/tool';
6 | import { memberModel } from './members/members.sql';
7 | import { _findOne, _save } from './utils/sql';
8 | const key = new NodeRSA({ b: 1024 });
9 | const package_ci = path.join(os.userInfo().homedir, 'package-ci-lib')
10 | const pubkey_path = path.join(package_ci, 'rsa_public_key_1024.txt')
11 | const prikey_path = path.join(package_ci, 'rsa_private_key_1024.txt')
12 | const static_path = path.join(package_ci, 'static')
13 | const token = 'package-ci'
14 | const client = {
15 | // host
16 | host: 'IP地址',
17 | // 端口号
18 | port: '27017',
19 | // 用户名
20 | user: '账号',
21 | // 密码
22 | password: '密码!',
23 | // 数据库名
24 | database: 'construct',
25 | }
26 |
27 | async function adminInit() {
28 | let user = {
29 | name: '超级用户',
30 | pwd: 'admin',
31 | access: '1',
32 | account: 'admin',
33 | jobName: "系统初始化管理员",
34 | remark: "这是系统初始化管理员,记得修改密码哦。",
35 | job_id: uuid(16, 32)
36 | }
37 | const isRepeatRes: any = await _findOne(memberModel, { account: user.account })
38 | if (isRepeatRes.code != 200 || !isRepeatRes.data) {
39 | const sql = new memberModel(user)
40 | await _save(sql)
41 | }
42 | }
43 |
44 | function sign() {
45 | // 读取公钥
46 | const publicKey = fs.readFileSync(pubkey_path, 'utf-8');
47 | // 公钥加密
48 | const nodersa = new NodeRSA(publicKey);
49 | return nodersa.encrypt(token, 'base64');
50 | }
51 |
52 | function staticInit() {
53 | // 判断配置目录是否存在,不存在则创建
54 | fs.ensureDirSync(package_ci)
55 | // 判断静态资源目录是否存在,不存在则创建
56 | fs.ensureDirSync(static_path)
57 | }
58 |
59 | function keyInit() {
60 | // 判断配置目录是否存在,不存在则创建
61 | fs.ensureDirSync(package_ci)
62 |
63 | /* 公钥文件是否存在 */
64 | const isPubkey = fs.existsSync(pubkey_path)
65 |
66 | /* 公钥文件是否存在 */
67 | const isPrikey = fs.existsSync(prikey_path)
68 |
69 | // 秘钥缺失,重新创建秘钥
70 | if (!isPubkey || !isPrikey) {
71 | // 创建公钥
72 | var pubkey = key.exportKey('pkcs8-public');
73 | // 创建私钥
74 | var prikey = key.exportKey('pkcs8-private');
75 | // 写入公钥
76 | fs.outputFileSync(pubkey_path, pubkey)
77 | // 写入私钥
78 | fs.outputFileSync(prikey_path, prikey)
79 | }
80 | }
81 |
82 |
83 | export default {
84 | keyInit,
85 | staticInit,
86 | adminInit,
87 | sign,
88 | package_ci,
89 | pubkey_path,
90 | prikey_path,
91 | static_path,
92 | process_port: 9001,
93 | client
94 | }
--------------------------------------------------------------------------------
/view/src/router/index.ts:
--------------------------------------------------------------------------------
1 | import { createRouter, createWebHashHistory } from 'vue-router'
2 | import container from '@/views/container.vue'
3 | import projectList from '@/views/project-list.vue'
4 | import projectMembers from '@/views/project-member.vue'
5 | import projectDetails from '@/views/project-details.vue'
6 | import Host from '@/views/host.vue'
7 | import testTask from '@/views/WorkOrder/test-task.vue'
8 | import uatTask from '@/views/WorkOrder/uat-task.vue'
9 | import prodTask from '@/views/WorkOrder/prod-task.vue'
10 | import testDetails from '@/views/WorkOrder/test-details.vue'
11 | import uatDetails from '@/views/WorkOrder/uat-details.vue'
12 | import prodDetails from '@/views/WorkOrder/prod-details.vue'
13 | import Login from '@/views/login.vue'
14 | import hostDetails from '@/views/host-details.vue'
15 |
16 | const routes = [
17 | {
18 | path: '/main',
19 | component: container,
20 | children: [
21 | {
22 | path: '/main/project',
23 | component: projectList
24 | },
25 | {
26 | path: '/main/project-members',
27 | component: projectMembers
28 | },
29 | {
30 | path: '/main/project/details/:id',
31 | component: projectDetails
32 | },
33 | {
34 | path: '/main/host',
35 | component: Host
36 | },
37 | {
38 | path: '/main/test-task',
39 | component: testTask
40 | },
41 | {
42 | path: '/main/uat-task',
43 | component: uatTask
44 | },
45 | {
46 | path: '/main/prod-task',
47 | component: prodTask
48 | },
49 | {
50 | path: '/main/test/:id',
51 | component: testDetails
52 | },
53 | {
54 | path: '/main/uat/:id',
55 | component: uatDetails
56 | },
57 | {
58 | path: '/main/prod/:id',
59 | component: prodDetails
60 | },
61 | {
62 | path: '/main/host/:ip',
63 | component: hostDetails
64 | },
65 | {
66 | path: '',
67 | redirect: '/main/project'
68 | }
69 | ]
70 |
71 | },
72 | {
73 | path: '/login',
74 | component: Login,
75 | },
76 | {
77 | path: '',
78 | redirect: '/main'
79 | }
80 |
81 | ]
82 |
83 |
84 | const router = createRouter({
85 | history: createWebHashHistory(),
86 | routes,
87 | })
88 |
89 |
90 | export default router
--------------------------------------------------------------------------------
/scheduler/src/project/project.controller.ts:
--------------------------------------------------------------------------------
1 | import { Body, Controller, Post } from '@nestjs/common';
2 | import { ProjectService } from './project.service'
3 | import { ENVS, Project } from './project.interface'
4 | @Controller('project')
5 | export class ProjectController {
6 | constructor(private readonly projectService: ProjectService) { }
7 | @Post('create')
8 | Create(@Body() body: Project) {
9 | return this.projectService.Create(body)
10 | }
11 | @Post('read')
12 | Read() {
13 | return this.projectService.Read()
14 | }
15 | @Post('remove')
16 | Remove(@Body() body: { id: string }) {
17 | return this.projectService.Remove(body)
18 | }
19 | @Post('details')
20 | Details(@Body() body: { id: string }) {
21 | return this.projectService.Details(body)
22 | }
23 |
24 | @Post('build-version')
25 | build_version(@Body() body: { id: string; pack: string }) {
26 | return this.projectService.build_version(body)
27 | }
28 |
29 |
30 | @Post('origin-pull')
31 | origin_pull(@Body() body: { id: string }) {
32 | return this.projectService.origin_pull(body)
33 | }
34 |
35 |
36 | @Post('read-version')
37 | read_version(@Body() body: { id: string }) {
38 | return this.projectService.read_version(body)
39 | }
40 |
41 | @Post('origin-branch')
42 | origin_branch(@Body() body: { id: string }) {
43 | return this.projectService.origin_branch(body)
44 | }
45 |
46 | @Post('local-version')
47 | local_version(@Body() body: { id: string }) {
48 | return this.projectService.local_version(body)
49 | }
50 |
51 | @Post('local-branch')
52 | local_branch(@Body() body: { id: string }) {
53 | return this.projectService.local_branch(body)
54 | }
55 | @Post('local-checkout')
56 | local_checkout(@Body() body: { localPath: string; branch: string; id: string }) {
57 | return this.projectService.local_checkout(body)
58 | }
59 | @Post('checkout-origin')
60 | checkout_origin(@Body() body: { id: string, branch: string }) {
61 | return this.projectService.checkout_origin(body)
62 | }
63 | @Post('delete-branch')
64 | delete_branch(@Body() body: { id: string, branch: string }) {
65 | return this.projectService.delete_branch(body)
66 | }
67 |
68 | @Post('server-update')
69 | ServerUpdate(@Body() body: { dev_ip: string; prod_ip: string; envs: ENVS; _id: string; name: string }) {
70 | return this.projectService.ServerUpdate(body)
71 | }
72 |
73 | @Post('update')
74 | Update(@Body() body: { project_id: string; devs: string; pm: ENVS; qa: string; buildName: string; describe: string; notice: string }) {
75 | return this.projectService.Update(body)
76 | }
77 | }
78 |
--------------------------------------------------------------------------------
/scheduler/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "package-ci-scheduler",
3 | "version": "0.0.1",
4 | "description": "",
5 | "author": "",
6 | "private": true,
7 | "license": "UNLICENSED",
8 | "scripts": {
9 | "prebuild": "rimraf dist",
10 | "build": "nest build",
11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
12 | "start": "nest start",
13 | "dev": "nest start --watch",
14 | "start:dev": "nest start --watch",
15 | "start:debug": "nest start --debug --watch",
16 | "start:prod": "node dist/main",
17 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
18 | "test": "jest",
19 | "test:watch": "jest --watch",
20 | "test:cov": "jest --coverage",
21 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
22 | "test:e2e": "jest --config ./test/jest-e2e.json",
23 | "win": "pkg . -t node14-win-x64",
24 | "lux": "pkg . -t node14-linux-x64",
25 | "mac": "pkg . -t node14-macos-x64"
26 | },
27 | "bin": "./dist/main.js",
28 | "pkg": {
29 | "assets": []
30 | },
31 | "dependencies": {
32 | "@nestjs/common": "^8.0.0",
33 | "@nestjs/core": "^8.0.0",
34 | "@nestjs/platform-express": "^8.0.0",
35 | "@nestjs/platform-socket.io": "^8.2.6",
36 | "@nestjs/websockets": "^8.2.6",
37 | "@types/node-rsa": "^1.1.1",
38 | "archiver": "^5.3.0",
39 | "form-data": "^4.0.0",
40 | "got": "11.8.2",
41 | "mongodb": "^4.3.1",
42 | "mongoose": "^6.1.8",
43 | "node-rsa": "^1.1.1",
44 | "reflect-metadata": "^0.1.13",
45 | "rimraf": "^3.0.2",
46 | "rxjs": "^7.5.2",
47 | "simple-git": "^3.1.1"
48 | },
49 | "devDependencies": {
50 | "@nestjs/cli": "^8.0.0",
51 | "@nestjs/schematics": "^8.0.0",
52 | "@nestjs/testing": "^8.0.0",
53 | "@types/express": "^4.17.13",
54 | "@types/fs-extra": "^9.0.13",
55 | "@types/jest": "27.0.2",
56 | "@types/node": "^16.0.0",
57 | "@types/supertest": "^2.0.11",
58 | "@typescript-eslint/eslint-plugin": "^5.0.0",
59 | "@typescript-eslint/parser": "^5.0.0",
60 | "eslint": "^8.0.1",
61 | "eslint-config-prettier": "^8.3.0",
62 | "eslint-plugin-prettier": "^4.0.0",
63 | "jest": "^27.2.5",
64 | "pkg": "^5.7.0",
65 | "prettier": "^2.3.2",
66 | "source-map-support": "^0.5.20",
67 | "supertest": "^6.1.3",
68 | "ts-jest": "^27.0.3",
69 | "ts-loader": "^9.2.3",
70 | "ts-node": "^10.0.0",
71 | "tsconfig-paths": "^3.10.1",
72 | "typescript": "^4.3.5"
73 | },
74 | "jest": {
75 | "moduleFileExtensions": [
76 | "js",
77 | "json",
78 | "ts"
79 | ],
80 | "rootDir": "src",
81 | "testRegex": ".*\\.spec\\.ts$",
82 | "transform": {
83 | "^.+\\.(t|j)s$": "ts-jest"
84 | },
85 | "collectCoverageFrom": [
86 | "**/*.(t|j)s"
87 | ],
88 | "coverageDirectory": "../coverage",
89 | "testEnvironment": "node"
90 | }
91 | }
--------------------------------------------------------------------------------
/scheduler/src/utils/git.ts:
--------------------------------------------------------------------------------
1 | import git from 'simple-git'
2 |
3 |
4 | export function Clone({ repoPath, branch, localPath }) {
5 | return new Promise((resolve) => {
6 | // 转义字符替换
7 | // if (user.includes('@')) {
8 | // user = user.replace(/@/g, '%40')
9 | // }
10 | // if (repoPath.includes('https://')) {
11 | // repoPath = repoPath.replace('https://', `https://${user}:${pwd}@`)
12 | // } else if (repoPath.includes('http://')) {
13 | // repoPath = repoPath.replace('http://', `http://${user}:${pwd}@`)
14 | // } else {
15 | // resolve({ code: 500, msg: 'git地址不合法', data: null })
16 | // }
17 |
18 | git().clone(repoPath, localPath, ['-b', branch], (err, data) => {
19 | resolve({ code: err ? 500 : 200, msg: err && err.message, data })
20 | })
21 | })
22 | }
23 |
24 | // 查询本地分支
25 | export function localBranch(localPath: string) {
26 | return new Promise((resolve) => {
27 | git(localPath).branchLocal((err, data) => {
28 | resolve({ code: err ? 500 : 200, msg: err && err.message, data })
29 | })
30 | })
31 | }
32 | // 查询远程分支
33 | export function originBranch(localPath: string) {
34 | return new Promise((resolve) => {
35 | git(localPath).branch(['-r'], (err, data) => {
36 | resolve({ code: err ? 500 : 200, msg: err && err.message, data })
37 | })
38 | })
39 | }
40 |
41 |
42 |
43 | // 当前分支版本列表
44 | export function localVersion(localPath) {
45 | return new Promise((resolve) => {
46 | git(localPath).log([], (err, data) => {
47 | resolve({ code: err ? 500 : 200, msg: err && err.message, data })
48 | })
49 | })
50 | }
51 |
52 | // 当前分支版本列表
53 | export async function pull(localPath: string) {
54 | return new Promise((resolve) => {
55 | git(localPath).pull((err, data) => {
56 | resolve({ code: err ? 500 : 200, msg: err && err.message, data })
57 | })
58 | })
59 | }
60 |
61 |
62 |
63 |
64 | export function checkout(localPath, branch) {
65 | return new Promise((resolve) => {
66 | git(localPath).checkout(branch, (err, data) => {
67 | resolve({ code: err ? 500 : 200, msg: err && err.message, data })
68 | })
69 | })
70 | }
71 |
72 | // 检出远程分支
73 | export function checkoutOrigin(localPath: string, branch: string) {
74 | let local = branch.replace('origin/', '')
75 | return new Promise((resolve) => {
76 | git(localPath).checkout(['-b', local, branch], (err, data) => {
77 | resolve({ code: err ? 500 : 200, msg: err && err.message, data })
78 | })
79 | })
80 | }
81 |
82 | // 删除本地分支
83 | export function deleteBranch(localPath: string, branch: string) {
84 | return new Promise((resolve) => {
85 | git(localPath).branch(['-D', branch], (err, data) => {
86 | resolve({ code: err ? 500 : 200, msg: err && err.message, data })
87 | })
88 | })
89 | }
90 |
--------------------------------------------------------------------------------
/cluster/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "package-ci-cluster",
3 | "version": "0.0.1",
4 | "description": "",
5 | "author": "",
6 | "private": true,
7 | "license": "UNLICENSED",
8 | "scripts": {
9 | "prebuild": "rimraf dist",
10 | "build": "nest build",
11 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
12 | "start": "nest start",
13 | "start:dev": "nest start --watch",
14 | "dev": "nest start --watch",
15 | "start:debug": "nest start --debug --watch",
16 | "start:prod": "node dist/main",
17 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
18 | "test": "jest",
19 | "test:watch": "jest --watch",
20 | "test:cov": "jest --coverage",
21 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand",
22 | "test:e2e": "jest --config ./test/jest-e2e.json",
23 | "ncc": "ncc build -m ./src/fork/koa.js -o ./src/fork",
24 | "win": "pkg . -t node14-win-x64",
25 | "lux": "pkg . -t node14-linux-x64",
26 | "mac": "pkg . -t node14-macos-x64"
27 | },
28 | "bin": "./dist/main.js",
29 | "pkg": {
30 | "assets": []
31 | },
32 | "dependencies": {
33 | "@nestjs/common": "^8.0.0",
34 | "@nestjs/core": "^8.0.0",
35 | "@nestjs/mapped-types": "*",
36 | "@nestjs/platform-express": "^8.0.0",
37 | "@nestjs/platform-socket.io": "^8.2.6",
38 | "@nestjs/websockets": "^8.2.6",
39 | "@vercel/ncc": "^0.34.0",
40 | "compressing": "^1.5.1",
41 | "got": "11.8.2",
42 | "koa": "^2.13.4",
43 | "koa-static": "^5.0.0",
44 | "node-rsa": "^1.1.1",
45 | "reflect-metadata": "^0.1.13",
46 | "rimraf": "^3.0.2",
47 | "rxjs": "^7.2.0",
48 | "socket.io": "^4.4.1"
49 | },
50 | "devDependencies": {
51 | "@nestjs/cli": "^8.0.0",
52 | "@nestjs/schematics": "^8.0.0",
53 | "@nestjs/testing": "^8.0.0",
54 | "@types/express": "^4.17.13",
55 | "@types/fs-extra": "^9.0.13",
56 | "@types/jest": "27.0.2",
57 | "@types/node": "^16.0.0",
58 | "@types/supertest": "^2.0.11",
59 | "@typescript-eslint/eslint-plugin": "^5.0.0",
60 | "@typescript-eslint/parser": "^5.0.0",
61 | "eslint": "^8.0.1",
62 | "eslint-config-prettier": "^8.3.0",
63 | "eslint-plugin-prettier": "^4.0.0",
64 | "jest": "^27.2.5",
65 | "pkg": "^5.7.0",
66 | "prettier": "^2.3.2",
67 | "source-map-support": "^0.5.20",
68 | "supertest": "^6.1.3",
69 | "ts-jest": "^27.0.3",
70 | "ts-loader": "^9.2.3",
71 | "ts-node": "^10.0.0",
72 | "tsconfig-paths": "^3.10.1",
73 | "typescript": "^4.3.5"
74 | },
75 | "jest": {
76 | "moduleFileExtensions": [
77 | "js",
78 | "json",
79 | "ts"
80 | ],
81 | "rootDir": "src",
82 | "testRegex": ".*\\.spec\\.ts$",
83 | "transform": {
84 | "^.+\\.(t|j)s$": "ts-jest"
85 | },
86 | "collectCoverageFrom": [
87 | "**/*.(t|j)s"
88 | ],
89 | "coverageDirectory": "../coverage",
90 | "testEnvironment": "node"
91 | }
92 | }
93 |
--------------------------------------------------------------------------------
/view/src/components/Aside.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
10 |
11 |
12 |
13 |
64 |
65 |
--------------------------------------------------------------------------------
/cluster/src/plugin/diskInfo.ts:
--------------------------------------------------------------------------------
1 |
2 |
3 | const exec = require('child_process').exec;
4 | const os = require('os');
5 |
6 |
7 |
8 |
9 | export default function ():any {
10 | return new Promise((resolve) => {
11 | let drives = [];
12 | switch (os.platform().toLowerCase()) {
13 | case 'win32':
14 | exec(
15 | 'wmic logicaldisk get Caption,FreeSpace,Size,VolumeSerialNumber,Description /format:list',
16 | function (err, stdout, stderr) {
17 | if (err) resolve(null)
18 | let aLines = stdout.split('\r\r\n');
19 | let bNew = false;
20 | let sCaption = ''
21 | let sDescription = ''
22 | let sFreeSpace: any = ''
23 | let sSize: any = ''
24 | let sVolume = '';
25 | for (let i = 0; i < aLines.length; i++) {
26 | if (aLines[i] != '') {
27 | let aTokens = aLines[i].split('=');
28 | switch (aTokens[0]) {
29 | case 'Caption':
30 | sCaption = aTokens[1];
31 | bNew = true;
32 | break;
33 | case 'Description':
34 | sDescription = aTokens[1];
35 | break;
36 | case 'FreeSpace':
37 | sFreeSpace = aTokens[1];
38 | break;
39 | case 'Size':
40 | sSize = aTokens[1];
41 | break;
42 | case 'VolumeSerialNumber':
43 | sVolume = aTokens[1];
44 | break;
45 | }
46 |
47 | } else {
48 | if (bNew) {
49 | sSize = parseFloat(sSize);
50 | if (isNaN(sSize)) {
51 | sSize = 0;
52 | }
53 | sFreeSpace = parseFloat(sFreeSpace);
54 | if (isNaN(sFreeSpace)) {
55 | sFreeSpace = 0;
56 | }
57 |
58 | let sUsed: any = (sSize - sFreeSpace);
59 | let sPercent = '0%';
60 | if (sSize != '' && parseFloat(sSize) > 0) {
61 | sPercent = Math.round((parseFloat(sUsed) / parseFloat(sSize)) * 100) + '%';
62 | }
63 | drives[drives.length] = {
64 | filesystem: sDescription,
65 | blocks: sSize,
66 | used: sUsed,
67 | available: sFreeSpace,
68 | capacity: sPercent,
69 | mounted: sCaption
70 | };
71 | bNew = false;
72 | sCaption = ''; sDescription = ''; sFreeSpace = ''; sSize = ''; sVolume = '';
73 | }
74 |
75 | }
76 | }
77 | resolve(drives)
78 | }
79 | );
80 |
81 | break;
82 |
83 | case 'linux':
84 | // Linux
85 | default:
86 | exec(
87 | 'df -P -g | awk \'NR > 1\'',
88 | function (err, stdout, stderr) {
89 | if (err) return resolve(null)
90 | let aLines = stdout.split('\n');
91 | for (let i = 0; i < aLines.length; i++) {
92 | let sLine = aLines[i];
93 | if (sLine != '') {
94 | sLine = sLine.replace(/ +(?= )/g, '');
95 | let aTokens = sLine.split(' ');
96 | drives[drives.length] = {
97 | filesystem: aTokens[0],
98 | blocks: aTokens[1],
99 | used: aTokens[2],
100 | available: aTokens[3],
101 | capacity: aTokens[4],
102 | mounted: aTokens[5]
103 | };
104 |
105 | }
106 | }
107 | resolve(drives)
108 | }
109 | );
110 |
111 | }
112 | })
113 |
114 |
115 |
116 | }
--------------------------------------------------------------------------------
/cluster/src/release/release.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import * as fs from 'fs-extra';
3 | import * as path from 'path';
4 | import * as compressing from 'compressing';
5 | import { dir_remove } from 'src/plugin/file';
6 | import { emptyDir } from '../plugin/file'
7 | import { EnvInfo } from 'src/equipment/equipment.interface';
8 | import PackageConfig from '../config'
9 |
10 | @Injectable()
11 | export class ReleaseService {
12 | async init_env(body: { id: string; envInfo: EnvInfo, type: string; name: string }) {
13 |
14 | /*
15 | 1、根据【项目id】在当前环境目录下遍历
16 | 2、检测'DEV', 'TEST', 'UAT', 'PROD'下是否存在该项目,不存在则创建一个空目录
17 | 3、然后在该目录写入配置文件信息,
18 | */
19 |
20 | async function envEmptyDir() {
21 | // 执行结果
22 | let list = [];
23 | // 环境集合
24 | let envs: { name: string; port: string }[] = []
25 |
26 | if (body.type === 'dev') {
27 | envs = [
28 | { name: 'DEV', port: body.envInfo.dev.port },
29 | { name: 'TEST', port: body.envInfo.test.port },
30 | { name: 'UAT', port: body.envInfo.uat.port },
31 | ]
32 | } else {
33 | envs = [
34 | { name: 'PROD', port: body.envInfo.prod.port },
35 | ]
36 | }
37 |
38 |
39 | // 遍历环境
40 | for (let i = 0; i < envs.length; i++) {
41 | // 当前环境
42 | let env: { name: string; port: string } = envs[i]
43 |
44 | // 地址拼接
45 | const envPath = path.join(
46 | PackageConfig.package_ci,
47 | env.name,
48 | body.id,
49 | );
50 | // 判断当前环境项目目录是否存在,如果不存在讲生成一个空目录
51 | if (!fs.existsSync(envPath)) {
52 | const res: any = await emptyDir(envPath)
53 | list.push({ ...env, code: res.code, msg: res.msg })
54 | } else {
55 | list.push({ ...env, code: 200, msg: null })
56 | }
57 | // 项目配置信息
58 | const config = {
59 | id: body.id,
60 | project_name: body.name,
61 | port: env.port,
62 | };
63 |
64 | // 写入项目配置信息
65 | fs.writeFileSync(
66 | path.join(envPath, 'env_release_config.json'),
67 | JSON.stringify(config),
68 | );
69 |
70 | }
71 | return list
72 | }
73 | const res = await envEmptyDir()
74 | return res
75 | }
76 |
77 |
78 | // DEV、TEST、UAT 环境部署
79 | async env_release(body: any) {
80 | function sync() {
81 | return new Promise((resolve) => {
82 | const devPath = path.join(
83 | PackageConfig.package_ci,
84 | body.env,
85 | body.id,
86 | );
87 | try {
88 | // 清空目录,参数1:指定目录路径,参数2:排除指定目录文件
89 | dir_remove(devPath, 'env_release_config.json');
90 | } catch (error) {
91 | resolve({ code: 500, msg: error.message });
92 | return
93 | }
94 | // 解压静态资源
95 | compressing.zip
96 | .uncompress(body.file.buffer, devPath)
97 | .then(() => {
98 | resolve({ code: 200, msg: '资源更新成功' });
99 | })
100 | .catch((err: Error) => {
101 | resolve({ code: 500, msg: err.message });
102 | });
103 | });
104 | }
105 | const res = await sync();
106 | return res
107 | }
108 | }
109 |
--------------------------------------------------------------------------------
/view/src/assets/avatar.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/view/public/static/images/PACKAGE-CI_files/avatar.0dd5877b.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/scheduler/src/project/release/release.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { _save, _find, _remove, _findOne, _updateOne } from '../../utils/sql';
3 | import { projectModel, releaseModel } from '../project.sql';
4 | import * as fs from 'fs-extra';
5 | import got from 'got';
6 | import * as path from 'path'
7 | import * as FormData from 'form-data';
8 | import PackageConfig from '@/config'
9 | @Injectable()
10 | export class ReleaseService {
11 | async release_list(body: { id: string }) {
12 | const project: any = await _findOne(projectModel, { project_id: body.id });
13 | if (project.code != 200) {
14 | return project;
15 | }
16 | const res: any = await _find({
17 | sql: releaseModel,
18 | params: { project_id: body.id },
19 | sort: { date: -1 },
20 | skip: 0,
21 | limit: 20
22 | });
23 | return res;
24 | }
25 |
26 | async env_release(body: { id: string; uid: string; release_id: string; env: string }) {
27 | // 项目数据
28 | const projectRes: any = await _findOne(projectModel, { project_id: body.id });
29 | if (projectRes.code != 200) return { code: 501, msg: '项目数据查找失败' }
30 |
31 |
32 | // 部署数据
33 | const releaseRes: any = await _findOne(releaseModel, { uid: body.uid });
34 | if (releaseRes.code != 200) return { code: 501, msg: '部署数据查找失败' }
35 |
36 |
37 | const resInfo = {
38 | // 开发环境IP地址
39 | dev_ip: projectRes.data.dev_ip,
40 | // 正式环境IP地址
41 | prod_ip: projectRes.data.prod_ip,
42 | // 当前环境指定主机节点IP
43 | target_ip: projectRes.data[body.env === 'PROD' ? 'prod_ip' : 'dev_ip'],
44 | // 项目列表主键
45 | project_id: projectRes.data.project_id.toString(),
46 | // 当前环境数据
47 | env: releaseRes.data[body.env],
48 | // 当前项目静态资源压缩包路径(历史版本)
49 | historyVersion: path.join(PackageConfig.static_path, projectRes.data.historyVersion),
50 | // 当前部署数据对应的压缩包(静态资源)版本
51 | pack_id: releaseRes.data.pack_id,
52 | // 当前部署数据列表主键
53 | release_id: releaseRes.data._id.toString()
54 | }
55 |
56 | // 收集数据,准备向指定主机节点传输静态资源,执行部署。
57 | const form = new FormData();
58 | form.append(
59 | 'file',
60 | fs.createReadStream(`${resInfo.historyVersion}/${resInfo.pack_id}.zip`),
61 | );
62 | form.append('id', String(resInfo.project_id));
63 | form.append('env', String(body.env));
64 |
65 | let env_res: any
66 | try {
67 | env_res = await got
68 | .post(`http://${resInfo.target_ip}:${PackageConfig.process_port}/release/dev_release`, {
69 | body: form,
70 | headers: {
71 | token: PackageConfig.sign()
72 | },
73 | timeout: {
74 | lookup: 100,
75 | connect: 3000
76 | }
77 | })
78 | .json();
79 | } catch (error) {
80 | return { code: 500, msg: error.message }
81 | }
82 |
83 | if (env_res.code === 200) {
84 | resInfo.env.status = true;
85 | resInfo.env.date = Math.round(new Date() as any);
86 | // 修改指定uid部署数据
87 | let update_res: any
88 | try {
89 | update_res = await _updateOne(
90 | releaseModel,
91 | { [body.env]: resInfo.env },
92 | { _id: resInfo.release_id },
93 | );
94 | if (update_res.code != 200) return { code: 501, msg: '部署完毕,但是依赖数据更新失败。' }
95 | } catch (error) {
96 | return { code: 501, msg: error.message }
97 | }
98 |
99 | }
100 | return env_res;
101 | }
102 | }
103 |
--------------------------------------------------------------------------------
/view/src/views/login.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | PACKAGE-CI
6 | 专注于前端运维工作,全新的Workflow。
7 |
8 |
9 |
10 |
11 |
13 | 登陆
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
60 |
61 |
62 |
63 |
--------------------------------------------------------------------------------
/scheduler/src/members/members.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { Parameter, MembersList } from './members.interface'
3 | import { memberModel } from './members.sql'
4 | import { _save, _find, _remove, _updateOne, _findCount, _findOne, ModelResult } from '../utils/sql'
5 | import { uuid } from '@/utils/tool';
6 | import { encrypt, decrypt } from '@/utils/verify'
7 | @Injectable()
8 | export class MembersService {
9 | async Login(body: { account: string; password: string; valid: number }): Promise {
10 | const isRepeatRes: any = await _findOne(memberModel, { account: body.account })
11 | if (isRepeatRes.code != 200) return isRepeatRes
12 | if (!isRepeatRes.data) return { code: 500, msg: '无效用户名' }
13 | if (isRepeatRes.data.pwd != body.password) return { code: 500, msg: '密码错误' }
14 | let token = {
15 | account: isRepeatRes.data.account,
16 | access: isRepeatRes.data.access,
17 | password: isRepeatRes.data.pwd,
18 | job_id: isRepeatRes.data.job_id,
19 | valid: (body.valid * 86400000),
20 | date: Math.round(new Date() as any)
21 | }
22 | return { code: 200, data: encrypt(JSON.stringify(token)) }
23 | }
24 | async UserInfo(str: string): Promise {
25 | let token: {
26 | account: string;
27 | access: string;
28 | password: string;
29 | job_id: string;
30 | valid: number;
31 | date: number
32 | } = JSON.parse(decrypt(str))
33 |
34 |
35 | // 用户数据数据查询
36 | const user: ModelResult = await _findOne(memberModel, { account: token.account })
37 |
38 |
39 | if (user.code != 200) {
40 | return { code: 500, data: null, msg: '数据查询失败' }
41 | }
42 |
43 | return {
44 | code: 200,
45 | data: {
46 | name: user.data.name,
47 | account: user.data.account,
48 | access: user.data.access,
49 | user: user.data.account,
50 | jobName: user.data.jobName,
51 | remark: user.data.remark,
52 | job_id: user.data.job_id
53 | },
54 | }
55 | }
56 | // 录入团队成员
57 | async Create(body: Parameter): Promise {
58 | body.job_id = uuid(16, 32)
59 | delete body._id
60 | delete body.__v
61 |
62 | const isRepeatRes: any = await _findOne(memberModel, { account: body.account })
63 | if (isRepeatRes.data) return { code: 500, msg: '用户名已存在' }
64 |
65 | const sql = new memberModel(body)
66 | const data = await _save(sql)
67 | return data
68 | }
69 | // 团队成员列表
70 | async Read(body: MembersList) {
71 | const query: any = { sql: memberModel }
72 | if (query.skip) {
73 | query.skip = body.pageIndex
74 | query.limit = body.pageSize
75 | }
76 | try {
77 | const res: any = await _find(query)
78 | if (res.code === 200) {
79 | res.data.forEach((item: any) => item.pwd = '')
80 | }
81 | const total: any = await _findCount(memberModel)
82 | res.total = total.data
83 | return res
84 | } catch (error) {
85 | return { code: 500, msg: error.message }
86 | }
87 |
88 | }
89 | // 删除团队成员
90 | async Delete(body: { id: string }) {
91 | const data = await _remove(memberModel, { _id: body.id })
92 | return data
93 | }
94 |
95 | async Update(body: Parameter) {
96 | let job_id = body.job_id
97 | delete body.job_id
98 | delete body._id
99 | delete body.__v
100 | const data = await _updateOne(memberModel, body, { job_id })
101 | return data
102 | }
103 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## PACKAGE-CI整体功能介绍及安装操作
2 | PACKAGE-CI是一款完全由JavaScript语言编写的自动化运维工具,主要面向各种规模的互联网软件(前端)开发团队,致力于打造前端项目DevOps一体化解决方案,相信开源的力量。
3 | - GitHub开源地址:[https://github.com/yangrds/package-ci](https://github.com/yangrds/package-ci)
4 | - 博客主页[https://js-vue.com](https://js-vue.com/archives/package-ci)(最新文档将在这里更新)
5 | - 电子邮件地址[yangrd@tom.com](yangrd@tom.com)(可以随时给我发电子邮件)
6 | - 如果您在使用过程中有任何疑问,可以加入群聊提问。
7 | 
8 | ### 软件平台演示
9 | - 演示地址:[https://package.js-vue.com](https://package.js-vue.com/)
10 | - 演示账号:xiaohuajia
11 | - 演示密码:123
12 | ### 软件开源信息
13 | |软件名|描述|开源协议|
14 | |-------|-------|-------|
15 | |package-ci-scheduler|集群调度器|MIT|
16 | |package-ci-cluster|集群节点应用|MIT|
17 | |package-ci-view|前端|MIT|
18 | ### 软件架构图
19 | 
20 | ### 功能特点
21 | 1. 低内存占用,完全抛弃docker容器,由node衍生进程(process)替代容器。
22 | 2. 轻量化应用,由node衍生的进程内仅有一个koa2应用,方便二次开发增减功能。
23 | 3. 完整工作流,项目从开发到功能测试再到UAT交付验收,直至最后项目上线正式环境,都有严格的工作阶段。
24 | 4. 大型集群节点,集群调度器[scheduler]可管理无数台节点服务器[cluster]。
25 | 5. 项目迁移,在调度器平台编辑项目所属节点,即可将项目从A服务器迁移至B服务器。
26 | 6. 指令集合,每个项目都有专属的指令集合,可以添加各种基于npm/yarn的指令,对项目进行各种常规操作(调度器源码已经屏蔽npm/yarn之外的任何指令)
27 | 7. GIT管理,可以对项目进行GIT管理(只读性操作)比如【指定分支克隆】【检出指定分支】【删除指定分支(本地)】【切换分支】【Commit记录】等等。
28 | 8. 团队成员,可以在平台内注册团队成员,每个成员可以和多个项目进行不同的身份绑定,使用不同权限对项目进行不同的日常操作。
29 | ### PACKAGE-CI常规目录操作
30 | **package-ci-scheduler**
31 | 1. 调度器首次启动会在系统用户目录下创建package-ci-lib文件夹,会创建如下文件/目录,自动创建
32 | 1. rsa_public_key_1024.txt(项目公钥)自动创建
33 | 1. rsa_private_key_1024.txt(项目私钥)自动创建
34 | 1. static(资源目录)调度器创建项目时从git克隆的文件全部放在这里,自动创建
35 |
36 | **package-ci-cluster**
37 | 1. 节点应用首次启动会在系统用户目录下创建package-ci-static文件夹,会创建如下文件/目录,自动创建
38 | 2. server.js(服务文件)用koajs编写由ncc编译,每次启动都会扫描如果缺失则会自动创建。
39 | 3. rsa_private_key_1024.txt 调度器里的私钥文件,调度器启动后会自动创建,然后复制到节点应用的目录内,这样调度器才能和节点通信
40 | 4. DEV(目录)该节点内所有项目的DEV环境静态资源全部存储在这里,自动创建
41 | 5. TEST(目录)该节点内所有项目的TEST环境静态资源全部存储在这里,自动创建
42 | 6. UAT(目录)该节点内所有项目的UAT环境静态资源全部存储在这里,自动创建
43 | 7. PROD(目录)该节点内所有项目的生产环境静态资源全部存储在这里,自动创建
44 |
45 | ### 如何启动软件
46 | ```javascript
47 | /*
48 | 【dev】 热启动项目
49 | 【build】 将项目打包至dist目录
50 | 【win/lux/mac】 是配置了pkg封包,可以把dist目录内build好的项目,封装为三个平台的可执行应用。
51 | */
52 | "scripts": {
53 | "prebuild": "rimraf dist",
54 | "build": "nest build",
55 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"",
56 | "start": "nest start",
57 | "dev": "nest start --watch",
58 | "start:dev": "nest start --watch",
59 | "start:debug": "nest start --debug --watch",
60 | "start:prod": "node dist/main",
61 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix",
62 | "win": "pkg . -t node14-win-x64",
63 | "lux": "pkg . -t node14-linux-x64",
64 | "mac": "pkg . -t node14-macos-x64"
65 | }
66 | ```
67 | ## 项目部署流程
68 | | 环境 | 权限 | 说明 |
69 | | -------- | ---------------------------- | -------------------------------------------------------------------------- |
70 | | DEV环境 | 开发成员 | 该环境是给开发成员员线上调试专用,项目所绑定的开发成员可随意部署(无限制) |
71 | | TEST环境 | 测试成员(QA) | 该环境是给测试成员进行功能测试专用,只允许项目绑定的测试人员部署和操作 |
72 | | UAT环境 | 项目经理(PM) | 该环境是给开发成员员线上调试专用,项目所绑定的开发成员可随意部署(无限制) |
73 | | PROD环境 | 项目经理(PM)测试成员(QA) | 项目正式上线,必须PM和QA全部确认后,由开发成员部署至生产环境(PROD) |
74 | ## 软件截图
75 | 
76 | 
77 | 
78 | 
79 | 
80 | 
81 | 
--------------------------------------------------------------------------------
/view/src/components/modal-view.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/scheduler/src/utils/sql.ts:
--------------------------------------------------------------------------------
1 |
2 | export interface ModelResult { code: number; data: any; msg: string }
3 |
4 | // 保存数据
5 | export function _save(sql) {
6 | return new Promise((resolve) => {
7 | sql.save((err: any) => {
8 | if (!err) {
9 | resolve({
10 | code: 200
11 | })
12 | } else {
13 | resolve({
14 | code: 500,
15 | err,
16 | msg: err.message
17 | })
18 | }
19 | })
20 | })
21 | }
22 |
23 | // 查询数据
24 | export function _find({ sql, params = {}, sort = {}, limit = 0, skip = 0 }): Promise {
25 | if (skip > 0) {
26 | skip = (skip - 1) * limit
27 | }
28 | return new Promise((resolve) => {
29 | sql.find(params, (err, data) => {
30 | if (!err) {
31 | resolve({ code: 200, data, msg: '' })
32 | } else {
33 | resolve({ code: 500, data: null, msg: err.message })
34 | }
35 | }).sort(sort).limit(limit).skip(skip)
36 | })
37 | }
38 |
39 | // 查询数据(首条)
40 | export function _findOne(sql, params): Promise {
41 | return new Promise((resolve) => {
42 | sql.findOne(params, (err, data) => {
43 | if (!err) {
44 | resolve({ code: 200, data, msg: '' })
45 | } else {
46 | resolve({ code: 500, data: null, msg: err.message })
47 | }
48 | })
49 | })
50 |
51 | }
52 |
53 | // 删除数据
54 | export function _remove(sql, params): Promise {
55 | return new Promise((resolve) => {
56 | sql.deleteOne(params, (err, data) => {
57 | if (!err) {
58 | resolve({ code: 200, data, msg: '' })
59 | } else {
60 | resolve({ code: 500, data: null, msg: err.message })
61 | }
62 | })
63 | })
64 | }
65 |
66 | // 修改数据(批量)
67 | export function _deleteMany(sql, params): Promise {
68 | return new Promise((resolve) => {
69 | sql.deleteMany(params, (err, data) => {
70 | if (!err) {
71 | resolve({ code: 200, data, msg: '' })
72 | } else {
73 | resolve({ code: 500, data: null, msg: err.message })
74 | }
75 | })
76 | })
77 | }
78 |
79 |
80 |
81 | // 修改数据(批量)
82 | export function _updateMany(sql, params, Primarykey): Promise {
83 | return new Promise((resolve) => {
84 | sql.updateMany(Primarykey, params, (err, data) => {
85 | if (!err) {
86 | resolve({
87 | code: 200,
88 | data,
89 | msg: (data.modifiedCount ? '修改成功' : '修改失败') + ` 更新记录${data.modifiedCount}条`,
90 | })
91 | } else {
92 | resolve({ code: 500, data: null, msg: err.message })
93 | }
94 | })
95 | })
96 | }
97 |
98 |
99 | // 更新数据
100 | export function _updateOne(sql, params, Primarykey): Promise {
101 | return new Promise((resolve) => {
102 | sql.updateOne(Primarykey, params, (err, data) => {
103 | if (!err) {
104 | resolve({
105 | code: 200,
106 | data,
107 | msg: (data.modifiedCount ? '修改成功' : '修改失败') + ` 更新记录${data.modifiedCount}条`,
108 | })
109 | } else {
110 | resolve({ code: 500, data: null, msg: err.message })
111 | }
112 | })
113 | })
114 | }
115 |
116 |
117 | /**
118 | * @description: 查询记录数
119 | * @param {*} sql 数据模型
120 | * @param {*} params 查询条件
121 | * @return {*} 数据记录数量
122 | */
123 | export function _findCount(sql, params = {}): Promise {
124 | return new Promise((resolve) => {
125 | sql.countDocuments(params, (err, data) => {
126 | if (!err) {
127 | resolve({ code: 200, data, msg: '' })
128 | } else {
129 | resolve({ code: 500, data: null, msg: err.message })
130 | }
131 | })
132 | })
133 | }
134 |
135 |
--------------------------------------------------------------------------------
/scheduler/src/project/process/process.controller.ts:
--------------------------------------------------------------------------------
1 | import { Body, Controller, Post, Req } from '@nestjs/common';
2 | import { ProcessService } from './process.service'
3 | import { Request } from 'express';
4 | import { encrypt, decrypt } from '@/utils/verify'
5 | @Controller('process')
6 | export class ProcessController {
7 | constructor(private readonly processService: ProcessService) { }
8 |
9 | // TEST环境创建工单
10 | @Post('test-create')
11 | test_create(@Body() body: { id: string; userId: string, uid: string; job_id?: string }, @Req() req: Request) {
12 | let headers: any = req.headers
13 | try {
14 | let token: {
15 | account: string;
16 | password: string;
17 | job_id: string;
18 | valid: number;
19 | date: number
20 | } = JSON.parse(decrypt(headers.token))
21 |
22 | body.job_id = token.job_id
23 | return this.processService.test_create(body)
24 | } catch (error) {
25 | return { code: 500, msg: error.message }
26 | }
27 |
28 | }
29 | // UAT环境创建工单
30 | @Post('uat-create')
31 | uat_create(@Body() body: { id: string; userId: string, uid: string; job_id?: string }, @Req() req: Request) {
32 |
33 | let headers: any = req.headers
34 | try {
35 | let token: {
36 | account: string;
37 | password: string;
38 | job_id: string;
39 | valid: number;
40 | date: number
41 | } = JSON.parse(decrypt(headers.token))
42 |
43 | body.job_id = token.job_id
44 | return this.processService.uat_create(body)
45 | } catch (error) {
46 | return { code: 500, msg: error.message }
47 | }
48 | }
49 | // PROD环境创建工单
50 | @Post('prod-create')
51 | prod_create(@Body() body: { id: string; userId: string, uid: string; job_id?: string }, @Req() req: Request) {
52 | let headers: any = req.headers
53 | try {
54 | let token: {
55 | account: string;
56 | password: string;
57 | job_id: string;
58 | valid: number;
59 | date: number
60 | } = JSON.parse(decrypt(headers.token))
61 |
62 | body.job_id = token.job_id
63 | return this.processService.prod_create(body)
64 | } catch (error) {
65 | return { code: 500, msg: error.message }
66 | }
67 | }
68 |
69 | // TEST环境列表
70 | @Post('test-list')
71 | test_list(@Body() body: { pageIndex: number, pageSize: number, name: string, task_id: string, commit_id: string, releaseStatus: string, status: string, project_id: string }) {
72 | return this.processService.task_list({ ...body, env: 'TEST' })
73 | }
74 | // UAT环境列表
75 | @Post('uat-list')
76 | uat_list(@Body() body: { pageIndex: number, pageSize: number, name: string, task_id: string, commit_id: string, releaseStatus: string, status: string, project_id: string }) {
77 | return this.processService.task_list({ ...body, env: 'UAT' })
78 | }
79 |
80 | // PROD环境列表
81 | @Post('prod-list')
82 | prod_list(@Body() body: { pageIndex: number, pageSize: number, name: string, task_id: string, commit_id: string, releaseStatus: string, status: string, project_id: string }) {
83 | return this.processService.task_list({ ...body, env: 'PROD' })
84 | }
85 |
86 | // TEST环境工单详情
87 | @Post('test-task')
88 | test_task(@Body() body: { task_id: string }) {
89 | return this.processService.task_details({ ...body, env: 'TEST' })
90 | }
91 | // UAT环境工单详情
92 | @Post('uat-task')
93 | uat_task(@Body() body: { task_id: string }) {
94 | return this.processService.task_details({ ...body, env: 'UAT' })
95 | }
96 | // PROD环境工单详情
97 | @Post('prod-task')
98 | prod_task(@Body() body: { task_id: string }) {
99 | return this.processService.task_details({ ...body, env: 'PROD' })
100 | }
101 | // 工单更新
102 | @Post('task-update')
103 | task_update(@Body() body: { task_id: string; params: any, env: string }) {
104 | return this.processService.task_update(body)
105 | }
106 | }
107 |
108 |
109 |
--------------------------------------------------------------------------------
/view/src/utils/tool.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description:
3 | * @param {*} len // 长度
4 | * @param {*} radix // 基数
5 | * @return {*} UUID
6 | */
7 | export function uuid(len: number, radix: number) {
8 | const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
9 | const uuid = [];
10 | let i;
11 | radix = radix || chars.length;
12 |
13 | if (len) {
14 | // Compact form
15 | for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random() * radix];
16 | } else {
17 | // rfc4122, version 4 form
18 | let r;
19 |
20 | // rfc4122 requires these characters
21 | uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
22 | uuid[14] = '4';
23 |
24 | // Fill in random data. At i==19 set the high bits of clock sequence as
25 | // per rfc4122, sec. 4.1.5
26 | for (i = 0; i < 36; i++) {
27 | if (!uuid[i]) {
28 | r = 0 | Math.random() * 16;
29 | uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
30 | }
31 | }
32 | }
33 |
34 | return uuid.join('');
35 | }
36 |
37 |
38 | export function renderSize(value: any) {
39 | if (null == value || value == '') {
40 | return "0 B";
41 | }
42 | const unitArr: string[] = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
43 | let index = 0;
44 | const srcsize: number = parseFloat(value);
45 | index = Math.floor(Math.log(srcsize) / Math.log(1024));
46 | let size: any = srcsize / Math.pow(1024, index);
47 | size = size.toFixed(2); //保留的小数位数
48 | return size + unitArr[index];
49 | }
50 |
51 | /*
52 | 格式化毫秒
53 | */
54 | export const formatDuring = (t: number) => {
55 | const HOUR = 1000 * 60 * 60;
56 | const d = Math.floor(t / (HOUR * 24));
57 | const h = Math.floor((t % (HOUR * 24)) / (HOUR));
58 | const m = Math.floor((t % (HOUR)) / (1000 * 60));
59 | const s = Math.floor((t % (1000 * 60)) / 1000);
60 |
61 | let text = '';
62 | d && (text += `${d}天`);
63 | h && (text += `${h}小时`);
64 | m && (text += `${m}分`);
65 | s && (text += `${s}秒`);
66 |
67 |
68 | return text || '1秒';
69 | };
70 | /**
71 | * 格式化秒
72 | * @param int value 总秒数
73 | * @return string result 格式化后的字符串
74 | */
75 | export function formatSeconds(date: number): string {
76 | let theTime: number = Math.round(date); // 需要转换的时间秒
77 | let theTime1 = 0; // 分
78 | let theTime2 = 0; // 小时
79 | let theTime3 = 0; // 天
80 | if (theTime > 60) {
81 | theTime1 = Math.round(theTime / 60);
82 | theTime = Math.round(theTime % 60);
83 | if (theTime1 > 60) {
84 | theTime2 = Math.round(theTime1 / 60);
85 | theTime1 = Math.round(theTime1 % 60);
86 | if (theTime2 > 24) {
87 | //大于24小时
88 | theTime3 = Math.round(theTime2 / 24);
89 | theTime2 = Math.round(theTime2 % 24);
90 | }
91 | }
92 | }
93 | let result = "";
94 | if (theTime > 0) {
95 | result = "" + Math.round(theTime) + "秒";
96 | }
97 | if (theTime1 > 0) {
98 | result = "" + Math.round(theTime1) + "分" + result;
99 | }
100 | if (theTime2 > 0) {
101 | result = "" + Math.round(theTime2) + "小时" + result;
102 | }
103 | if (theTime3 > 0) {
104 | result = "" + Math.round(theTime3) + "天" + result;
105 | }
106 | return result;
107 | }
108 |
109 |
110 | export function dateFormat(fmt: string, date: Date | number) {
111 | interface dateType {
112 | "Y+": string, "m+": string, "d+": string, "H+": string, "M+": string, "S+": string
113 | }
114 | let ret
115 | date = new Date(date)
116 | const opt: dateType = {
117 | "Y+": date.getFullYear().toString(), // 年
118 | "m+": (date.getMonth() + 1).toString(), // 月
119 | "d+": date.getDate().toString(), // 日
120 | "H+": date.getHours().toString(), // 时
121 | "M+": date.getMinutes().toString(), // 分
122 | "S+": date.getSeconds().toString() // 秒
123 | // 有其他格式化字符需求可以继续添加,必须转化成字符串
124 | }
125 | for (const k in opt) {
126 | ret = new RegExp("(" + k + ")").exec(fmt)
127 | if (ret) {
128 | fmt = fmt.replace(
129 | ret[1],
130 | ret[1].length == 1 ? opt[k as keyof dateType] : opt[k as keyof dateType].padStart(ret[1].length, "0")
131 | )
132 | }
133 | }
134 | return fmt
135 | }
136 |
137 |
138 |
139 | export function Percentage(num: number, total: number): number {
140 | if (num == 0 || total == 0) {
141 | return 0;
142 | }
143 | return (Math.round(num / total * 10000) / 100.00);// 小数点后两位百分比
144 | }
--------------------------------------------------------------------------------
/scheduler/src/utils/tool.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * @description:
3 | * @param {*} len // 长度
4 | * @param {*} radix // 基数
5 | * @return {*} UUID
6 | */
7 | export function uuid(len: number, radix: number) {
8 | const chars = '0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz'.split('');
9 | const uuid = [];
10 | let i;
11 | radix = radix || chars.length;
12 |
13 | if (len) {
14 | // Compact form
15 | for (i = 0; i < len; i++) uuid[i] = chars[0 | Math.random() * radix];
16 | } else {
17 | // rfc4122, version 4 form
18 | let r;
19 |
20 | // rfc4122 requires these characters
21 | uuid[8] = uuid[13] = uuid[18] = uuid[23] = '-';
22 | uuid[14] = '4';
23 |
24 | // Fill in random data. At i==19 set the high bits of clock sequence as
25 | // per rfc4122, sec. 4.1.5
26 | for (i = 0; i < 36; i++) {
27 | if (!uuid[i]) {
28 | r = 0 | Math.random() * 16;
29 | uuid[i] = chars[(i == 19) ? (r & 0x3) | 0x8 : r];
30 | }
31 | }
32 | }
33 |
34 | return uuid.join('');
35 | }
36 |
37 |
38 | export function renderSize(value: any) {
39 | if (null == value || value == '') {
40 | return "0 B";
41 | }
42 | const unitArr: string[] = ["B", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
43 | let index = 0;
44 | const srcsize: number = parseFloat(value);
45 | index = Math.floor(Math.log(srcsize) / Math.log(1024));
46 | let size: any = srcsize / Math.pow(1024, index);
47 | size = size.toFixed(2); //保留的小数位数
48 | return size + unitArr[index];
49 | }
50 |
51 | /*
52 | 格式化毫秒
53 | */
54 | export const formatDuring = (t: number) => {
55 | const HOUR = 1000 * 60 * 60;
56 | const d = Math.floor(t / (HOUR * 24));
57 | const h = Math.floor((t % (HOUR * 24)) / (HOUR));
58 | const m = Math.floor((t % (HOUR)) / (1000 * 60));
59 | const s = Math.floor((t % (1000 * 60)) / 1000);
60 |
61 | let text = '';
62 | d && (text += `${d}天`);
63 | h && (text += `${h}小时`);
64 | m && (text += `${m}分`);
65 | s && (text += `${s}秒`);
66 |
67 |
68 | return text || '1秒';
69 | };
70 | /**
71 | * 格式化秒
72 | * @param int value 总秒数
73 | * @return string result 格式化后的字符串
74 | */
75 | export function formatSeconds(date: number): string {
76 | let theTime: number = Math.round(date); // 需要转换的时间秒
77 | let theTime1 = 0; // 分
78 | let theTime2 = 0; // 小时
79 | let theTime3 = 0; // 天
80 | if (theTime > 60) {
81 | theTime1 = Math.round(theTime / 60);
82 | theTime = Math.round(theTime % 60);
83 | if (theTime1 > 60) {
84 | theTime2 = Math.round(theTime1 / 60);
85 | theTime1 = Math.round(theTime1 % 60);
86 | if (theTime2 > 24) {
87 | //大于24小时
88 | theTime3 = Math.round(theTime2 / 24);
89 | theTime2 = Math.round(theTime2 % 24);
90 | }
91 | }
92 | }
93 | let result = "";
94 | if (theTime > 0) {
95 | result = "" + Math.round(theTime) + "秒";
96 | }
97 | if (theTime1 > 0) {
98 | result = "" + Math.round(theTime1) + "分" + result;
99 | }
100 | if (theTime2 > 0) {
101 | result = "" + Math.round(theTime2) + "小时" + result;
102 | }
103 | if (theTime3 > 0) {
104 | result = "" + Math.round(theTime3) + "天" + result;
105 | }
106 | return result;
107 | }
108 |
109 |
110 | export function dateFormat(fmt: string, date: Date | number) {
111 | interface dateType {
112 | "Y+": string, "m+": string, "d+": string, "H+": string, "M+": string, "S+": string
113 | }
114 | let ret
115 | date = new Date(date)
116 | const opt: dateType = {
117 | "Y+": date.getFullYear().toString(), // 年
118 | "m+": (date.getMonth() + 1).toString(), // 月
119 | "d+": date.getDate().toString(), // 日
120 | "H+": date.getHours().toString(), // 时
121 | "M+": date.getMinutes().toString(), // 分
122 | "S+": date.getSeconds().toString() // 秒
123 | // 有其他格式化字符需求可以继续添加,必须转化成字符串
124 | }
125 | for (const k in opt) {
126 | ret = new RegExp("(" + k + ")").exec(fmt)
127 | if (ret) {
128 | fmt = fmt.replace(
129 | ret[1],
130 | ret[1].length == 1 ? opt[k as keyof dateType] : opt[k as keyof dateType].padStart(ret[1].length, "0")
131 | )
132 | }
133 | }
134 | return fmt
135 | }
136 |
137 |
138 |
139 | export function Percentage(num: number, total: number): number {
140 | if (num == 0 || total == 0) {
141 | return 0;
142 | }
143 | return (Math.round(num / total * 10000) / 100.00);// 小数点后两位百分比
144 | }
--------------------------------------------------------------------------------
/cluster/src/equipment/equipment.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { SystemInfo, Disk, _File } from './equipment.interface'
3 | import * as os from 'os';
4 | import * as fs from 'fs-extra';
5 | import * as path from 'path';
6 | import { get_process_container, init, process_container } from '../plugin/start';
7 | import got from 'got';
8 | import PackageConfig from '../config'
9 |
10 | // 心跳检查
11 | function ServerStatus(port: string): Promise<{ status: boolean, msg: string }> {
12 | return new Promise(async (resolove) => {
13 | try {
14 | await got.post(`http://0.0.0.0:${port}/status`, {
15 | json: {},
16 | timeout: {
17 | lookup: 100,
18 | connect: 3000
19 | }
20 | }).json();
21 | resolove({ status: true, msg: '' })
22 | } catch (error) {
23 | resolove({ status: false, msg: error.message })
24 | }
25 | })
26 |
27 | }
28 |
29 |
30 | @Injectable()
31 | export class EquipmentService {
32 | async getSystem() {
33 | const syatemInfo: SystemInfo = {}
34 | // 处理器
35 | syatemInfo.cpus = os.cpus()
36 | // 操作系统 CPU 架构
37 | syatemInfo.arch = os.arch()
38 | // 以整数形式返回空闲的系统内存量(以字节为单位)
39 | syatemInfo.totalmem = os.totalmem()
40 | // 以字符串形式返回操作系统的主机名。
41 | syatemInfo.hostname = os.hostname()
42 | // 返回包含已分配网络地址的网络接口的对象。
43 | syatemInfo.networkInterfaces = os.networkInterfaces()
44 | /*
45 | 返回标识操作系统平台的字符串。 该值在编译时设置。
46 | 可能的值为 'aix'、'darwin'、'freebsd'、'linux'、'openbsd'、'sunos' 和 'win32'。
47 | */
48 | if (os.platform() === 'darwin') {
49 | syatemInfo.platform = 'MacOS'
50 | } else if (os.platform() === 'win32') {
51 | syatemInfo.platform = 'Windows'
52 | } else if (os.platform() === 'linux') {
53 | syatemInfo.platform = 'Linux'
54 | }
55 | return syatemInfo
56 | }
57 |
58 | async process_list() {
59 | const data = await this.getSystem()
60 | const list = []
61 | let process_list = get_process_container()
62 | for (let i = 0; i < process_list.length; i++) {
63 | let child = process_list[i]
64 | let serve = {
65 | env: child.env,
66 | id: child.id,
67 | project_id: child.project_id,
68 | status: false,
69 | message: ''
70 | }
71 | try {
72 | const before = await ServerStatus(child.env.port)
73 | serve.status = before.status
74 | } catch (error) {
75 | serve.status = false
76 | serve.message = error.message
77 | }
78 | list.push(serve)
79 | }
80 | return { code: 200, data, list }
81 | }
82 |
83 | async process_kill(body: { id: string }) {
84 | // 根据进程ID从进程池找出当前操作的进程
85 | const child = process_container.find((item) => item.id === body.id)
86 | if (!child) return { code: 500, msg: '未知错误,请重试或者联系管理员。' }
87 | // 关闭进程之前先检测心跳状态
88 | const before = await ServerStatus(child.env.port)
89 | // 心跳正常关闭进程,心跳异常将错误抛给前端
90 | if (before.status) {
91 | // 关闭进程
92 | child.kill()
93 | // 关闭进程之后在次检测心跳状态
94 | const after = await ServerStatus(child.env.port)
95 | if (after.status) {
96 | return { code: 500, msg: `端口【${child.env.port}】关闭失败,请检查服务器配置` }
97 | } else {
98 | return { code: 200, msg: `端口【${child.env.port}】关闭成功` }
99 | }
100 | } else {
101 | return { code: 500, msg: before.msg }
102 | }
103 | }
104 |
105 | async process_killAll() {
106 | try {
107 | for (let i = 0; i < process_container.length; i++) {
108 | const child = process_container[i]
109 | // 关闭进程之前先检测心跳状态
110 | const before = await ServerStatus(child.env.port)
111 | if (before.status) {
112 | // 关闭进程
113 | child.kill()
114 | }
115 | }
116 | return { code: 200, msg: `进程池冻结完毕` }
117 | } catch (error) {
118 | return { code: 500, msg: error.message }
119 | }
120 | }
121 |
122 |
123 | async process_init() {
124 | const res = await this.process_killAll()
125 | if (res.code === 200) {
126 | init()
127 | return { code: 200, msg: '进程池刷新完毕' }
128 | } else {
129 | return res
130 | }
131 | }
132 |
133 |
134 | async process_delete(body: { id: string }) {
135 | try {
136 | ['DEV', 'TEST', 'UAT', 'PROD'].forEach((item: string) => {
137 | const envPath = path.join(PackageConfig.package_ci, item, body.id);
138 | fs.removeSync(envPath)
139 | })
140 | return { code: 200, msg: '' }
141 | } catch (error) {
142 | return { code: 500, msg: error.message }
143 | }
144 |
145 |
146 | }
147 |
148 | async process_start(body: { id: string }) {
149 | const child = process_container.find((item) => item.id === body.id)
150 | if (!child) return { code: 500, msg: '未知错误,请重试或者联系管理员。' }
151 | // 启动进程之前先检测心跳状态
152 | const before = await ServerStatus(child.env.port)
153 | /*
154 | 心跳正常不需要启动进程,向前端抛错误警告。
155 | 心跳异常说明进程没启动,将启动进程。
156 | */
157 |
158 | if (before.status) {
159 | return { code: 500, msg: `端口【${child.env.port}】运行正常,请勿重复启动!` }
160 | } else {
161 | await child.init()
162 | // 启动进程之后在次检测心跳状态
163 | const after = await ServerStatus(child.env.port)
164 | if (after.status) {
165 | return { code: 200, msg: `端口【${child.env.port}】启动成功` }
166 | } else {
167 | return { code: 200, msg: after.msg }
168 | }
169 |
170 | }
171 | }
172 | }
173 |
--------------------------------------------------------------------------------
/view/src/http/api.ts:
--------------------------------------------------------------------------------
1 | import axios, { Method } from "axios";
2 | import { post,get } from './index'
3 |
4 | const address = import.meta.env.VITE_API_URL
5 |
6 |
7 |
8 |
9 | /* 团队成员注册 */
10 | export const Members_create = (body?: any): any => post({ url: `${address}members/create`, body })
11 |
12 | /* 团队成员列表 */
13 | export const Members_read = (body?: any): any => post({ url: `${address}members/read`, body })
14 |
15 | /* 团队成员删除 */
16 | export const Members_delete = (body?: any): any => post({ url: `${address}members/delete`, body })
17 |
18 | /* 团队成员修改 */
19 | export const Members_update = (body?: any): any => post({ url: `${address}members/update`, body })
20 |
21 | /* 团队成员信息 */
22 | export const Members_details = (body?: any): any => post({ url: `${address}members/user-info`, body })
23 |
24 | /* 团队成员登录 */
25 | export const Members_login = (params?: any): any => get({ url: `${address}members/login`, params })
26 |
27 | /* 指令添加 */
28 | export const command_add = (body?: any): any => post({ url: `${address}command/add`, body })
29 |
30 | /* 指令添加 */
31 | export const command_list = (body?: any): any => post({ url: `${address}command/list`, body })
32 |
33 | /* 指令删除 */
34 | export const command_remove = (body?: any): any => post({ url: `${address}command/remove`, body })
35 |
36 | /* 加载GIT分支 */
37 | export const Project_branch = (body?: any): any => post({ url: `${address}project/branch`, body })
38 |
39 | /* 创建项目 */
40 | export const Project_create = (body?: any): any => post({ url: `${address}project/create`, body })
41 |
42 | /* 项目列表 */
43 | export const Project_read = (body?: any): any => post({ url: `${address}project/read`, body })
44 |
45 | /* 项目删除 */
46 | export const Project_remove = (body?: any): any => post({ url: `${address}project/remove`, body })
47 |
48 | /* 项目详情 */
49 | export const Project_details = (body?: any): any => post({ url: `${address}project/details`, body })
50 |
51 | /* 节点更新 */
52 | export const Project_ServerUpdate = (body?: any): any => post({ url: `${address}project/server-update`, body })
53 |
54 | /* 项目信息更新 */
55 | export const Project_Update = (body?: any): any => post({ url: `${address}project/update`, body })
56 |
57 | /* 项目切换分支 */
58 | export const local_checkout = (body?: any): any => post({ url: `${address}project/local-checkout`, body })
59 |
60 | /* 项目版本列表 */
61 | export const local_version = (body?: any): any => post({ url: `${address}project/local-version`, body })
62 |
63 | /* 项目远程分支 */
64 | export const origin_branch = (body?: any): any => post({ url: `${address}project/origin-branch`, body })
65 |
66 | /* 拉取远程分支 */
67 | export const origin_pull = (body?: any): any => post({ url: `${address}project/origin-pull`, body })
68 |
69 | /* 项目本地分支 */
70 | export const local_branch = (body?: any): any => post({ url: `${address}project/local-branch`, body })
71 |
72 | /* 项目本地分支 */
73 | export const checkout_origin = (body?: any): any => post({ url: `${address}project/checkout-origin`, body })
74 |
75 | /* 项目本地分支 */
76 | export const delete_branch = (body?: any): any => post({ url: `${address}project/delete-branch`, body })
77 |
78 | /* 构建版本 */
79 | export const build_version = (body?: any): any => post({ url: `${address}project/build-version`, body })
80 |
81 | /* 版本列表 */
82 | export const read_version = (body?: any): any => post({ url: `${address}project/read-version`, body })
83 |
84 | /* dev发布 */
85 | export const dev_release = (body?: any): any => post({ url: `${address}release/dev-release`, body })
86 |
87 | /* test发布 */
88 | export const test_release = (body?: any): any => post({ url: `${address}release/test-release`, body })
89 |
90 | /* uat发布 */
91 | export const uat_release = (body?: any): any => post({ url: `${address}release/uat-release`, body })
92 |
93 | /* prod发布 */
94 | export const prod_release = (body?: any): any => post({ url: `${address}release/prod-release`, body })
95 |
96 | /* prod创建工单 */
97 | export const prod_create = (body?: any): any => post({ url: `${address}process/prod-create`, body })
98 |
99 | /* test创建工单 */
100 | export const test_create = (body?: any): any => post({ url: `${address}process/test-create`, body })
101 |
102 | /* uat创建工单 */
103 | export const uat_create = (body?: any): any => post({ url: `${address}process/uat-create`, body })
104 |
105 | /* test工单列表 */
106 | export const test_list = (body?: any): any => post({ url: `${address}process/test-list`, body })
107 |
108 | /* uat工单列表 */
109 | export const uat_list = (body?: any): any => post({ url: `${address}process/uat-list`, body })
110 |
111 | /* prod工单列表 */
112 | export const prod_list = (body?: any): any => post({ url: `${address}process/prod-list`, body })
113 |
114 | /* test工单详情 */
115 | export const test_task = (body?: any): any => post({ url: `${address}process/test-task`, body })
116 |
117 | /* uat工单详情 */
118 | export const uat_task = (body?: any): any => post({ url: `${address}process/uat-task`, body })
119 |
120 | /* prod工单详情 */
121 | export const prod_task = (body?: any): any => post({ url: `${address}process/prod-task`, body })
122 |
123 | /* test工单数据修改 */
124 | export const task_update = (body?: any): any => post({ url: `${address}process/task-update`, body })
125 |
126 | /* 部署纪录 */
127 | export const release_list = (body?: any): any => post({ url: `${address}release/release-list`, body })
128 |
129 | /* 加载服务器配置 */
130 | export const cluster_loadConfig = (body?: any): any => post({ url: `${address}cluster/load-config`, body })
131 |
132 | /* 添加集群节点 */
133 | export const cluster_create = (body?: any): any => post({ url: `${address}cluster/cluster-create`, body })
134 |
135 | /* 集群节点列表 */
136 | export const cluster_list = (body?: any): any => post({ url: `${address}cluster/cluster-list`, body })
137 |
138 | /* 删除节点 */
139 | export const cluster_remove = (body?: any): any => post({ url: `${address}cluster/cluster-remove`, body })
140 |
141 | /* 进程列表 */
142 | export const process_list = (body?: any): any => post({ url: `${address}cluster/process-list`, body })
143 |
144 | /* 进程关闭 */
145 | export const process_kill = (body?: any): any => post({ url: `${address}cluster/process-kill`, body })
146 |
147 | /* 节点删除 */
148 | export const process_delete = (body?: any): any => post({ url: `${address}cluster/process-delete`, body })
149 |
150 | /* 进程关闭(全部) */
151 | export const process_killAll = (body?: any): any => post({ url: `${address}cluster/process-kill-all`, body })
152 |
153 |
154 | /* 进程启动(全部) */
155 | export const process_init = (body?: any): any => post({ url: `${address}cluster/process-init`, body })
156 |
157 |
158 | /* 进程启动 */
159 | export const process_start = (body?: any): any => post({ url: `${address}cluster/process-start`, body })
160 |
161 |
162 |
--------------------------------------------------------------------------------
/scheduler/src/cluster/cluster.service.ts:
--------------------------------------------------------------------------------
1 | import { Injectable } from '@nestjs/common';
2 | import { hostModel } from './cluster.sql'
3 | import { projectModel } from '../project/project.sql'
4 | import { Host } from './cluster.interface'
5 | import { _save, _find, _findOne, _remove } from '../utils/sql';
6 | import got from 'got';
7 | import PackageConfig from '@/config'
8 |
9 |
10 | @Injectable()
11 | export class ClusterService {
12 | async loadConfig(body: { ip: string; port: number }) {
13 | try {
14 | const res = await got.post(`http://${body.ip}:${body.port}/equipment/system`, {
15 | json: body,
16 | headers: {
17 | token: PackageConfig.sign()
18 | },
19 | timeout: {
20 | lookup: 100,
21 | connect: 3000
22 | }
23 | }).json();
24 | return { code: 200, data: res }
25 | } catch (error) {
26 | return { code: 500, msg: '配置加载失败' }
27 | }
28 | }
29 | async clusterCreate(body: Host) {
30 | const host: any = await _findOne(hostModel, { ip: body.ip })
31 | if (host && host.data) {
32 | return { code: 500, msg: '该主机IP已存在' }
33 | }
34 | const sql = new hostModel(body)
35 | const res = await _save(sql)
36 | return res
37 | }
38 | async clusterRemove(body: { ip: string }) {
39 | const res = await _remove(hostModel, { ip: body.ip })
40 | return res
41 | }
42 | async clusterList() {
43 | const res = await _find({ sql: hostModel })
44 | if (res.code != 200) return res
45 | res.data.forEach((item: any) => item.port = PackageConfig.process_port);
46 | return res
47 | }
48 |
49 | async process_kill(body: { ip: string; id: string }) {
50 | try {
51 | const processRes: any = await got.post(`http://${body.ip}:${PackageConfig.process_port}/equipment/process-kill`, {
52 | json: body,
53 | headers: {
54 | token: PackageConfig.sign()
55 | },
56 | timeout: {
57 | lookup: 100,
58 | connect: 3000
59 | }
60 | }).json();
61 |
62 | return processRes
63 |
64 | } catch (error) {
65 | return { code: 500, msg: error.message }
66 | }
67 | }
68 |
69 | async process_killAll(body: { ip: string; }) {
70 | try {
71 | const processRes: any = await got.post(`http://${body.ip}:${PackageConfig.process_port}/equipment/process-kill-all`, {
72 | json: {},
73 | headers: {
74 | token: PackageConfig.sign()
75 | },
76 | timeout: {
77 | lookup: 100,
78 | connect: 3000
79 | }
80 | }).json();
81 |
82 | return processRes
83 | } catch (error) {
84 | return { code: 500, msg: error.message }
85 | }
86 | }
87 |
88 | async process_init(body: { ip: string; }) {
89 | try {
90 | const processRes: any = await got.post(`http://${body.ip}:${PackageConfig.process_port}/equipment/process-init`, {
91 | json: {},
92 | headers: {
93 | token: PackageConfig.sign()
94 | },
95 | timeout: {
96 | lookup: 100,
97 | connect: 3000
98 | }
99 | }).json();
100 | return processRes
101 | } catch (error) {
102 | return { code: 500, msg: error.message }
103 | }
104 | }
105 |
106 | async process_delete(body: { ip: string; id: string }) {
107 | try {
108 | const processRes: any = await got.post(`http://${body.ip}:${PackageConfig.process_port}/equipment/process-delete`, {
109 | json: { id: body.id },
110 | headers: {
111 | token: PackageConfig.sign()
112 | },
113 | timeout: {
114 | lookup: 100,
115 | connect: 3000
116 | }
117 | }).json();
118 | return processRes
119 | } catch (error) {
120 | return { code: 500, msg: error.message }
121 | }
122 | }
123 |
124 | async process_start(body: { ip: string; id: string }) {
125 | try {
126 | const processRes: any = await got.post(`http://${body.ip}:${PackageConfig.process_port}/equipment/process-start`, {
127 | json: body,
128 | headers: {
129 | token: PackageConfig.sign()
130 | },
131 | timeout: {
132 | lookup: 100,
133 | connect: 3000
134 | }
135 | }).json();
136 |
137 | return processRes
138 |
139 | } catch (error) {
140 | return { code: 500, msg: error.message }
141 | }
142 | }
143 |
144 | async process_list(body: { ip: string }) {
145 | const projectRes = await _find({ sql: projectModel })
146 | if (projectRes.code != 200) return projectRes
147 | let process_list = []
148 | let project_list = projectRes.data
149 | try {
150 | const processRes: any = await got.post(`http://${body.ip}:${PackageConfig.process_port}/equipment/process-list`, {
151 | json: {},
152 | headers: {
153 | token: PackageConfig.sign()
154 | },
155 | timeout: {
156 | lookup: 100,
157 | connect: 3000
158 | }
159 | }).json();
160 | if (processRes.code != 200) return processRes
161 |
162 | try {
163 | process_list = processRes.list.map((item) => {
164 | const project = project_list.find((k) => k.project_id === item.project_id)
165 | let Data: any = {
166 | project: { describe: project ? project.describe : '该项目已删除', name: project ? project.name : '该项目已删除' },
167 | env: item.env,
168 | status: item.status,
169 | project_id: item.project_id,
170 | id: item.id
171 | }
172 | return Data
173 | })
174 | return { code: 200, systemInfo: { ...processRes.data, ip: body.ip, port: PackageConfig.process_port }, process_list }
175 | } catch (error) {
176 | return { code: 500, msg: error.message }
177 | }
178 | } catch (error) {
179 | return { code: 500, msg: error.message }
180 | }
181 | }
182 | }
183 |
--------------------------------------------------------------------------------
/view/src/components/branch-config.vue:
--------------------------------------------------------------------------------
1 |
2 |
3 |

4 |
当前用户角色权限不足
5 |
6 |
7 |
8 |
9 | 远程分支
10 |
11 |
13 | {{ branch }}
14 |
15 |
16 | 检出
17 |
18 |
19 |
20 |
21 |
22 |
23 | 本地分支
24 |
25 |
27 |
28 | {{ branch.name }}
29 | {{ branch.current ? '*' :
30 | ''
31 | }}
32 |
33 |
34 |
35 | 删除
36 |
37 |
38 |
39 |
40 |
41 | 关闭
42 |
43 |
44 |
45 |
46 |
47 |
48 |
162 |
163 |
164 |
165 |
--------------------------------------------------------------------------------