feat: 增加通知Provider和Jenkins自动同步
This commit is contained in:
@@ -329,7 +329,7 @@ export class DeployExecutionService {
|
||||
);
|
||||
|
||||
this.auditService.record({
|
||||
action: `WECOM_NOTIFICATION_${notification.status.toUpperCase()}`,
|
||||
action: `DEPLOY_NOTIFICATION_${notification.status.toUpperCase()}`,
|
||||
resourceType: 'deploy_run',
|
||||
resourceId: run.id,
|
||||
actorName: run.operator,
|
||||
@@ -501,7 +501,7 @@ export class DeployExecutionService {
|
||||
await this.deployNotificationService.sendDeployRunEvent(run, event);
|
||||
|
||||
this.auditService.record({
|
||||
action: `WECOM_NOTIFICATION_${notification.status.toUpperCase()}`,
|
||||
action: `DEPLOY_NOTIFICATION_${notification.status.toUpperCase()}`,
|
||||
resourceType: 'deploy_run',
|
||||
resourceId: run.id,
|
||||
actorName: run.operator,
|
||||
|
||||
@@ -7,11 +7,17 @@ import { DeployExecutionService } from './deploy-execution.service';
|
||||
import { DeployRunRepository } from './deploy-run.repository';
|
||||
import { DeployRunsController } from './deploy-runs.controller';
|
||||
import { DeployRunsService } from './deploy-runs.service';
|
||||
import { JenkinsSyncSchedulerService } from './jenkins-sync-scheduler.service';
|
||||
|
||||
@Module({
|
||||
imports: [AuditModule, JenkinsModule, ProjectsModule, WeComModule],
|
||||
controllers: [DeployRunsController],
|
||||
providers: [DeployExecutionService, DeployRunRepository, DeployRunsService],
|
||||
providers: [
|
||||
DeployExecutionService,
|
||||
DeployRunRepository,
|
||||
DeployRunsService,
|
||||
JenkinsSyncSchedulerService,
|
||||
],
|
||||
exports: [DeployExecutionService, DeployRunRepository, DeployRunsService],
|
||||
})
|
||||
export class DeployRunsModule {}
|
||||
|
||||
@@ -171,7 +171,7 @@ export class DeployRunsService {
|
||||
await this.deployNotificationService.sendDeployRunEvent(run, event);
|
||||
|
||||
this.auditService.record({
|
||||
action: `WECOM_NOTIFICATION_${notification.status.toUpperCase()}`,
|
||||
action: `DEPLOY_NOTIFICATION_${notification.status.toUpperCase()}`,
|
||||
resourceType: 'deploy_run',
|
||||
resourceId: run.id,
|
||||
after: {
|
||||
@@ -193,6 +193,6 @@ export class DeployRunsService {
|
||||
const message =
|
||||
error instanceof Error ? error.message : 'Jenkins sync failed';
|
||||
|
||||
return redactSensitive(message);
|
||||
return redactSensitive({ message }).message;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { AuditService } from '../audit/audit.service';
|
||||
import { EnvConfig } from '../config/env.schema';
|
||||
import { DeployRunsService } from './deploy-runs.service';
|
||||
import { JenkinsSyncSchedulerService } from './jenkins-sync-scheduler.service';
|
||||
|
||||
describe('JenkinsSyncSchedulerService', () => {
|
||||
afterEach(() => {
|
||||
jest.useRealTimers();
|
||||
});
|
||||
|
||||
it('does not start interval when auto sync is disabled', () => {
|
||||
jest.useFakeTimers();
|
||||
const sync = {
|
||||
syncJenkinsRuns: jest.fn(),
|
||||
} satisfies Pick<DeployRunsService, 'syncJenkinsRuns'>;
|
||||
const service = new JenkinsSyncSchedulerService(
|
||||
createConfig({
|
||||
JENKINS_AUTO_SYNC_ENABLED: false,
|
||||
JENKINS_AUTO_SYNC_INTERVAL_MS: 1000,
|
||||
}),
|
||||
sync as unknown as DeployRunsService,
|
||||
createAudit(),
|
||||
);
|
||||
|
||||
service.onModuleInit();
|
||||
jest.advanceTimersByTime(3000);
|
||||
|
||||
expect(sync.syncJenkinsRuns).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('runs one sweep and records aggregate audit data', async () => {
|
||||
const sync = {
|
||||
syncJenkinsRuns: jest.fn(() =>
|
||||
Promise.resolve({
|
||||
checkedAt: '2026-06-11T00:00:00.000Z',
|
||||
totalCandidates: 2,
|
||||
syncedRuns: [{} as never],
|
||||
failedRuns: [{} as never],
|
||||
}),
|
||||
),
|
||||
} satisfies Pick<DeployRunsService, 'syncJenkinsRuns'>;
|
||||
const audit = createAudit();
|
||||
const service = new JenkinsSyncSchedulerService(
|
||||
createConfig({
|
||||
JENKINS_AUTO_SYNC_ENABLED: true,
|
||||
JENKINS_AUTO_SYNC_INTERVAL_MS: 1000,
|
||||
}),
|
||||
sync as unknown as DeployRunsService,
|
||||
audit,
|
||||
);
|
||||
|
||||
await service.runOnce();
|
||||
|
||||
expect(sync.syncJenkinsRuns).toHaveBeenCalledTimes(1);
|
||||
expect(audit.recordMock).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
action: 'JENKINS_AUTO_SYNC_TICK_COMPLETED',
|
||||
after: {
|
||||
totalCandidates: 2,
|
||||
syncedCount: 1,
|
||||
failedCount: 1,
|
||||
},
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('records redacted failure audit when sweep fails', async () => {
|
||||
const sync = {
|
||||
syncJenkinsRuns: jest.fn(() =>
|
||||
Promise.reject(
|
||||
new Error('token=secret-value https://jenkins.internal failed'),
|
||||
),
|
||||
),
|
||||
} satisfies Pick<DeployRunsService, 'syncJenkinsRuns'>;
|
||||
const audit = createAudit();
|
||||
const service = new JenkinsSyncSchedulerService(
|
||||
createConfig({}),
|
||||
sync as unknown as DeployRunsService,
|
||||
audit,
|
||||
);
|
||||
|
||||
await service.runOnce();
|
||||
|
||||
const payload = audit.recordMock.mock.calls[0][0];
|
||||
expect(payload.action).toBe('JENKINS_AUTO_SYNC_TICK_FAILED');
|
||||
expect(JSON.stringify(payload)).not.toContain('secret-value');
|
||||
});
|
||||
});
|
||||
|
||||
function createConfig(
|
||||
values: Partial<Record<keyof EnvConfig, string | number | boolean | undefined>>,
|
||||
): ConfigService<EnvConfig, true> {
|
||||
return {
|
||||
get: jest.fn((key: keyof EnvConfig) => values[key]),
|
||||
} as unknown as ConfigService<EnvConfig, true>;
|
||||
}
|
||||
|
||||
function createAudit(): AuditService & { recordMock: jest.Mock } {
|
||||
const recordMock = jest.fn();
|
||||
|
||||
return {
|
||||
record: recordMock,
|
||||
recordMock,
|
||||
} as unknown as AuditService & { recordMock: jest.Mock };
|
||||
}
|
||||
@@ -0,0 +1,81 @@
|
||||
import { Injectable, OnModuleDestroy, OnModuleInit } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { AuditService } from '../audit/audit.service';
|
||||
import { redactSensitive } from '../common/security/redact-sensitive';
|
||||
import { EnvConfig } from '../config/env.schema';
|
||||
import { DeployRunsService } from './deploy-runs.service';
|
||||
|
||||
/**
|
||||
* 轻量自动同步器,先复用内存发布单和现有 Jenkins 同步逻辑。
|
||||
* 后续接 Redis/BullMQ 后,这里会迁移成队列 worker。
|
||||
*/
|
||||
@Injectable()
|
||||
export class JenkinsSyncSchedulerService
|
||||
implements OnModuleInit, OnModuleDestroy
|
||||
{
|
||||
private timer: ReturnType<typeof setInterval> | undefined;
|
||||
private running = false;
|
||||
|
||||
constructor(
|
||||
private readonly config: ConfigService<EnvConfig, true>,
|
||||
private readonly deployRunsService: DeployRunsService,
|
||||
private readonly auditService: AuditService,
|
||||
) {}
|
||||
|
||||
onModuleInit(): void {
|
||||
if (!this.config.get('JENKINS_AUTO_SYNC_ENABLED', { infer: true })) {
|
||||
return;
|
||||
}
|
||||
|
||||
const intervalMs = this.config.get('JENKINS_AUTO_SYNC_INTERVAL_MS', {
|
||||
infer: true,
|
||||
});
|
||||
|
||||
this.timer = setInterval(() => {
|
||||
void this.runOnce();
|
||||
}, intervalMs);
|
||||
void this.runOnce();
|
||||
}
|
||||
|
||||
onModuleDestroy(): void {
|
||||
if (this.timer) {
|
||||
clearInterval(this.timer);
|
||||
this.timer = undefined;
|
||||
}
|
||||
}
|
||||
|
||||
async runOnce(): Promise<void> {
|
||||
if (this.running) {
|
||||
return;
|
||||
}
|
||||
|
||||
this.running = true;
|
||||
|
||||
try {
|
||||
const summary = await this.deployRunsService.syncJenkinsRuns();
|
||||
|
||||
this.auditService.record({
|
||||
action: 'JENKINS_AUTO_SYNC_TICK_COMPLETED',
|
||||
resourceType: 'deploy_run',
|
||||
after: {
|
||||
totalCandidates: summary.totalCandidates,
|
||||
syncedCount: summary.syncedRuns.length,
|
||||
failedCount: summary.failedRuns.length,
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
const message =
|
||||
error instanceof Error ? error.message : 'Jenkins auto sync failed';
|
||||
|
||||
this.auditService.record({
|
||||
action: 'JENKINS_AUTO_SYNC_TICK_FAILED',
|
||||
resourceType: 'deploy_run',
|
||||
after: {
|
||||
message: redactSensitive({ message }).message,
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
this.running = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user