feat: 增加通知Provider和Jenkins自动同步

This commit is contained in:
湛兮
2026-06-11 21:07:54 +08:00
parent caec5a618d
commit 163f411485
14 changed files with 568 additions and 31 deletions
+2 -2
View File
@@ -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 -1
View File
@@ -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 {}
+2 -2
View File
@@ -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;
}
}
}