feat: 增加发布策略校验
- projects: 为环境补充 releasePolicy 和 refPattern,区分 branch、tag、master 发布策略 - deploy-runs: 创建发布单前校验项目状态、环境和 ref 规则 - repository: 数据库读取路径补齐发布策略推断
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
import { AuditService } from '../audit/audit.service';
|
||||
import { AppError } from '../common/errors/app-error';
|
||||
import { DeployNotificationService } from '../notifications/wecom/deploy-notification.service';
|
||||
import { ProjectSummary } from '../projects/project.types';
|
||||
import { ProjectsService } from '../projects/projects.service';
|
||||
import { DeployExecutionService } from './deploy-execution.service';
|
||||
import { DeployRunRepository } from './deploy-run.repository';
|
||||
@@ -27,8 +28,30 @@ describe('DeployRunsService', () => {
|
||||
steps: [],
|
||||
};
|
||||
|
||||
function createService(options?: { runs?: DeployRunSummary[] }) {
|
||||
const project: ProjectSummary = {
|
||||
id: 'project_001',
|
||||
key: 'access-manage',
|
||||
name: 'access-manage',
|
||||
repositoryUrl: 'https://gitea.ops.mrzhan.top/my-project/access-manage.git',
|
||||
defaultBranch: 'develop',
|
||||
status: 'active',
|
||||
environments: [
|
||||
{
|
||||
name: 'test',
|
||||
displayName: '测试环境',
|
||||
jenkinsJobPath: 'my-project-dev/test-access-manage-develop',
|
||||
releasePolicy: 'branch',
|
||||
refPattern: '^(develop|feature/.+|hotfix/.+)$',
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
function createService(options?: {
|
||||
runs?: DeployRunSummary[];
|
||||
project?: ProjectSummary;
|
||||
}) {
|
||||
const runs = options?.runs ?? [baseRun];
|
||||
const resolvedProject = options?.project ?? project;
|
||||
const repository = {
|
||||
findMany: jest.fn(() => Promise.resolve(runs)),
|
||||
findById: jest.fn((id: string) =>
|
||||
@@ -52,22 +75,7 @@ describe('DeployRunsService', () => {
|
||||
>;
|
||||
const projects = {
|
||||
getProject: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
id: 'project_001',
|
||||
key: 'access-manage',
|
||||
name: 'access-manage',
|
||||
repositoryUrl:
|
||||
'https://gitea.ops.mrzhan.top/my-project/access-manage.git',
|
||||
defaultBranch: 'develop',
|
||||
status: 'active' as const,
|
||||
environments: [
|
||||
{
|
||||
name: 'test' as const,
|
||||
displayName: '测试环境',
|
||||
jenkinsJobPath: 'my-project-dev/test-access-manage-develop',
|
||||
},
|
||||
],
|
||||
}),
|
||||
Promise.resolve(resolvedProject),
|
||||
),
|
||||
} satisfies Pick<ProjectsService, 'getProject'>;
|
||||
const audit = {
|
||||
@@ -149,6 +157,111 @@ describe('DeployRunsService', () => {
|
||||
expect(execution.execute).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('rejects project environments that are not configured before creating a run', async () => {
|
||||
const { audit, execution, repository, service } = createService({
|
||||
project: {
|
||||
...project,
|
||||
key: 'devops-platform-web',
|
||||
defaultBranch: 'master',
|
||||
environments: [
|
||||
{
|
||||
name: 'production',
|
||||
displayName: '生产环境',
|
||||
jenkinsJobPath: 'devops-platform/prod-devops-platform-web-master',
|
||||
releasePolicy: 'master',
|
||||
refPattern: '^master$',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.createRun({
|
||||
projectKey: 'devops-platform-web',
|
||||
environment: 'test',
|
||||
ref: 'master',
|
||||
operator: 'ops-admin',
|
||||
}),
|
||||
).rejects.toBeInstanceOf(AppError);
|
||||
expect(repository.create).not.toHaveBeenCalled();
|
||||
expect(execution.execute).not.toHaveBeenCalled();
|
||||
expect(audit.record).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: 'DEPLOY_REJECTED',
|
||||
after: expect.objectContaining({
|
||||
reason: 'Environment is not configured',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('rejects production refs that do not match tag release policy', async () => {
|
||||
const { audit, execution, repository, service } = createService({
|
||||
project: {
|
||||
...project,
|
||||
environments: [
|
||||
{
|
||||
name: 'production',
|
||||
displayName: '生产环境',
|
||||
jenkinsJobPath: 'my-project-prod/prod-access-manage-tag',
|
||||
releasePolicy: 'tag',
|
||||
refPattern: '^(v\\d+\\.\\d+\\.\\d+.*|v\\d{4}\\.\\d{2}\\.\\d{2}.*)$',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
await expect(
|
||||
service.createRun({
|
||||
projectKey: 'access-manage',
|
||||
environment: 'production',
|
||||
ref: 'master',
|
||||
operator: 'ops-admin',
|
||||
}),
|
||||
).rejects.toBeInstanceOf(AppError);
|
||||
expect(repository.create).not.toHaveBeenCalled();
|
||||
expect(execution.execute).not.toHaveBeenCalled();
|
||||
expect(audit.record).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: 'DEPLOY_REJECTED',
|
||||
after: expect.objectContaining({
|
||||
reason: 'Ref does not match release policy',
|
||||
}),
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('allows master-only production releases for self-managed platform projects', async () => {
|
||||
const { execution, repository, service } = createService({
|
||||
project: {
|
||||
...project,
|
||||
key: 'devops-platform-web',
|
||||
defaultBranch: 'master',
|
||||
environments: [
|
||||
{
|
||||
name: 'production',
|
||||
displayName: '生产环境',
|
||||
jenkinsJobPath: 'devops-platform/prod-devops-platform-web-master',
|
||||
releasePolicy: 'master',
|
||||
refPattern: '^master$',
|
||||
},
|
||||
],
|
||||
},
|
||||
});
|
||||
|
||||
const run = await service.createRun({
|
||||
projectKey: 'devops-platform-web',
|
||||
environment: 'production',
|
||||
ref: 'master',
|
||||
operator: 'ops-admin',
|
||||
idempotencyKey: 'self-managed-master',
|
||||
});
|
||||
|
||||
expect(run.id).toBe('self-managed-master');
|
||||
expect(repository.create).toHaveBeenCalled();
|
||||
expect(execution.execute).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('cancels an existing deploy run', async () => {
|
||||
const { audit, service } = createService();
|
||||
|
||||
|
||||
Reference in New Issue
Block a user