feat: 持久化运维审计日志
This commit is contained in:
@@ -42,11 +42,18 @@ export class AgentService {
|
||||
);
|
||||
}
|
||||
|
||||
async createInvocation(input: CreateAgentInvocationInput): Promise<AgentInvocation> {
|
||||
async createInvocation(
|
||||
input: CreateAgentInvocationInput,
|
||||
): Promise<AgentInvocation> {
|
||||
if (!allowedTypes.includes(input.type)) {
|
||||
throw new AppError('VALIDATION_FAILED', 'Unsupported agent invocation type', 400, {
|
||||
throw new AppError(
|
||||
'VALIDATION_FAILED',
|
||||
'Unsupported agent invocation type',
|
||||
400,
|
||||
{
|
||||
type: input.type,
|
||||
});
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
const llmHealth = this.llmClient.healthSummary();
|
||||
@@ -74,7 +81,7 @@ export class AgentService {
|
||||
status: 'success',
|
||||
resultMarkdown: completion.text || mockResults[input.type],
|
||||
};
|
||||
this.recordInvocationAudit(invocation, 'llm', completion.model);
|
||||
await this.recordInvocationAudit(invocation, 'llm', completion.model);
|
||||
} catch (error) {
|
||||
invocation = {
|
||||
...baseInvocation,
|
||||
@@ -84,7 +91,7 @@ export class AgentService {
|
||||
? redactSensitive({ message: error.message }).message
|
||||
: 'LLM invocation failed',
|
||||
};
|
||||
this.recordInvocationAudit(invocation, 'llm_failed');
|
||||
await this.recordInvocationAudit(invocation, 'llm_failed');
|
||||
}
|
||||
} else {
|
||||
invocation = {
|
||||
@@ -92,25 +99,30 @@ export class AgentService {
|
||||
status: 'success',
|
||||
resultMarkdown: mockResults[input.type],
|
||||
};
|
||||
this.recordInvocationAudit(invocation, 'mock', undefined, llmHealth.message);
|
||||
await this.recordInvocationAudit(
|
||||
invocation,
|
||||
'mock',
|
||||
undefined,
|
||||
llmHealth.message,
|
||||
);
|
||||
}
|
||||
|
||||
this.invocations.unshift(invocation);
|
||||
return invocation;
|
||||
}
|
||||
|
||||
private recordInvocationAudit(
|
||||
private async recordInvocationAudit(
|
||||
invocation: AgentInvocation,
|
||||
mode: 'mock' | 'llm' | 'llm_failed',
|
||||
model?: string,
|
||||
reason?: string,
|
||||
): void {
|
||||
): Promise<void> {
|
||||
const action =
|
||||
invocation.status === 'failed'
|
||||
? 'AGENT_INVOCATION_FAILED'
|
||||
: 'AGENT_INVOCATION_CREATED';
|
||||
|
||||
this.auditService.record({
|
||||
await this.auditService.record({
|
||||
action,
|
||||
resourceType: 'agent_invocation',
|
||||
resourceId: invocation.id,
|
||||
|
||||
@@ -1,3 +1,6 @@
|
||||
/**
|
||||
* 审计日志查询入口只返回脱敏后的最近记录。
|
||||
*/
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
|
||||
import { AuditRecordSummary, AuditService } from './audit.service';
|
||||
@@ -9,7 +12,7 @@ export class AuditController {
|
||||
|
||||
@Get()
|
||||
@ApiOkResponse({ description: 'Recent sanitized audit records.' })
|
||||
listRecentRecords(): AuditRecordSummary[] {
|
||||
async listRecentRecords(): Promise<AuditRecordSummary[]> {
|
||||
return this.auditService.getRecentRecords();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,10 +4,10 @@ import { AuditService } from './audit.service';
|
||||
* 审计记录必须先脱敏再暴露给前端或 Agent。
|
||||
*/
|
||||
describe('AuditService', () => {
|
||||
it('stores recent audit records with sensitive values redacted', () => {
|
||||
it('stores recent audit records with sensitive values redacted', async () => {
|
||||
const service = new AuditService();
|
||||
|
||||
service.record({
|
||||
await service.record({
|
||||
action: 'SECRET_CHANGED',
|
||||
resourceType: 'secret',
|
||||
parameterDigest: {
|
||||
@@ -16,7 +16,7 @@ describe('AuditService', () => {
|
||||
},
|
||||
});
|
||||
|
||||
const [record] = service.getRecentRecords();
|
||||
const [record] = await service.getRecentRecords();
|
||||
|
||||
expect(record.action).toBe('SECRET_CHANGED');
|
||||
expect(record.parameterDigest).toEqual({
|
||||
|
||||
+141
-9
@@ -1,5 +1,13 @@
|
||||
/**
|
||||
* 审计服务统一完成敏感信息脱敏、MySQL 持久化和本地降级存储。
|
||||
* 业务服务只提交动作摘要,禁止把 token/webhook/连接串原文写入审计。
|
||||
*/
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { ConfigService } from '@nestjs/config';
|
||||
import { AuditLog, Prisma } from '@prisma/client';
|
||||
import { redactSensitive } from '../common/security/redact-sensitive';
|
||||
import { EnvConfig } from '../config/env.schema';
|
||||
import { PrismaService } from '../prisma/prisma.service';
|
||||
|
||||
export type AuditRecordInput = {
|
||||
action: string;
|
||||
@@ -7,30 +15,154 @@ export type AuditRecordInput = {
|
||||
resourceId?: string;
|
||||
actorId?: string;
|
||||
actorName?: string;
|
||||
requestId?: string;
|
||||
sourceIp?: string;
|
||||
userAgent?: string;
|
||||
before?: Record<string, unknown>;
|
||||
after?: Record<string, unknown>;
|
||||
parameterDigest?: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type AuditRecordSummary = AuditRecordInput & { createdAt: string };
|
||||
export type AuditRecordSummary = AuditRecordInput & {
|
||||
id: string;
|
||||
createdAt: string;
|
||||
};
|
||||
|
||||
type SanitizedAuditRecordInput = Omit<
|
||||
AuditRecordInput,
|
||||
'before' | 'after' | 'parameterDigest'
|
||||
> & {
|
||||
before?: Prisma.InputJsonValue;
|
||||
after?: Prisma.InputJsonValue;
|
||||
parameterDigest?: Prisma.InputJsonValue;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
export class AuditService {
|
||||
private readonly memoryRecords: AuditRecordSummary[] = [];
|
||||
|
||||
record(input: AuditRecordInput): void {
|
||||
constructor(
|
||||
private readonly config?: ConfigService<EnvConfig, true>,
|
||||
private readonly prisma?: PrismaService,
|
||||
) {}
|
||||
|
||||
async record(input: AuditRecordInput): Promise<void> {
|
||||
const sanitized = this.sanitize(input);
|
||||
const prisma = this.databaseClient();
|
||||
|
||||
if (!prisma) {
|
||||
this.pushMemoryRecord(sanitized);
|
||||
return;
|
||||
}
|
||||
|
||||
await prisma.auditLog.create({
|
||||
data: {
|
||||
deployRunId:
|
||||
sanitized.resourceType === 'deploy_run'
|
||||
? sanitized.resourceId
|
||||
: undefined,
|
||||
actorId: sanitized.actorId,
|
||||
actorName: sanitized.actorName,
|
||||
action: sanitized.action,
|
||||
resourceType: sanitized.resourceType,
|
||||
resourceId: sanitized.resourceId,
|
||||
requestId: sanitized.requestId,
|
||||
sourceIp: sanitized.sourceIp,
|
||||
userAgent: sanitized.userAgent,
|
||||
before: sanitized.before,
|
||||
after: sanitized.after,
|
||||
parameterDigest: sanitized.parameterDigest,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getRecentRecords(): Promise<AuditRecordSummary[]> {
|
||||
const prisma = this.databaseClient();
|
||||
|
||||
if (prisma) {
|
||||
const records = await prisma.auditLog.findMany({
|
||||
orderBy: {
|
||||
createdAt: 'desc',
|
||||
},
|
||||
take: 50,
|
||||
});
|
||||
|
||||
return records.map((record) => this.toSummary(record));
|
||||
}
|
||||
|
||||
return [...this.memoryRecords].slice(-50).reverse();
|
||||
}
|
||||
|
||||
private sanitize(input: AuditRecordInput): SanitizedAuditRecordInput {
|
||||
return {
|
||||
...input,
|
||||
before: this.toJsonValue(input.before),
|
||||
after: this.toJsonValue(input.after),
|
||||
parameterDigest: this.toJsonValue(input.parameterDigest),
|
||||
};
|
||||
}
|
||||
|
||||
private toJsonValue(
|
||||
value: Record<string, unknown> | undefined,
|
||||
): Prisma.InputJsonValue | undefined {
|
||||
if (!value) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return JSON.parse(
|
||||
JSON.stringify(redactSensitive(value)),
|
||||
) as Prisma.InputJsonValue;
|
||||
}
|
||||
|
||||
private pushMemoryRecord(input: SanitizedAuditRecordInput): void {
|
||||
this.memoryRecords.push({
|
||||
...input,
|
||||
before: input.before ? redactSensitive(input.before) : undefined,
|
||||
after: input.after ? redactSensitive(input.after) : undefined,
|
||||
parameterDigest: input.parameterDigest
|
||||
? redactSensitive(input.parameterDigest)
|
||||
: undefined,
|
||||
id: `audit_${Date.now()}_${this.memoryRecords.length + 1}`,
|
||||
before: input.before as Record<string, unknown> | undefined,
|
||||
after: input.after as Record<string, unknown> | undefined,
|
||||
parameterDigest: input.parameterDigest as
|
||||
| Record<string, unknown>
|
||||
| undefined,
|
||||
createdAt: new Date().toISOString(),
|
||||
});
|
||||
}
|
||||
|
||||
getRecentRecords(): AuditRecordSummary[] {
|
||||
return [...this.memoryRecords].slice(-50).reverse();
|
||||
private toSummary(record: AuditLog): AuditRecordSummary {
|
||||
return {
|
||||
id: record.id,
|
||||
action: record.action,
|
||||
resourceType: record.resourceType,
|
||||
resourceId: record.resourceId ?? undefined,
|
||||
actorId: record.actorId ?? undefined,
|
||||
actorName: record.actorName ?? undefined,
|
||||
requestId: record.requestId ?? undefined,
|
||||
sourceIp: record.sourceIp ?? undefined,
|
||||
userAgent: record.userAgent ?? undefined,
|
||||
before: this.jsonToRecord(record.before),
|
||||
after: this.jsonToRecord(record.after),
|
||||
parameterDigest: this.jsonToRecord(record.parameterDigest),
|
||||
createdAt: record.createdAt.toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
private jsonToRecord(
|
||||
value: Prisma.JsonValue,
|
||||
): Record<string, unknown> | undefined {
|
||||
if (!value || typeof value !== 'object' || Array.isArray(value)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private databaseClient(): PrismaService | undefined {
|
||||
if (
|
||||
!this.config?.get('USE_DATABASE_READS', { infer: true }) ||
|
||||
!this.config.get('DATABASE_URL', { infer: true })
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return this.prisma;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -42,7 +42,7 @@ export class DeployExecutionService {
|
||||
);
|
||||
let currentStep: string | undefined;
|
||||
|
||||
this.auditService.record({
|
||||
await this.auditService.record({
|
||||
action: 'DEPLOY_EXECUTION_STARTED',
|
||||
resourceType: 'deploy_run',
|
||||
resourceId: currentRun.id,
|
||||
@@ -95,7 +95,7 @@ export class DeployExecutionService {
|
||||
}),
|
||||
);
|
||||
|
||||
this.auditService.record({
|
||||
await this.auditService.record({
|
||||
action: 'DEPLOY_SUCCEEDED',
|
||||
resourceType: 'deploy_run',
|
||||
resourceId: currentRun.id,
|
||||
@@ -132,7 +132,7 @@ export class DeployExecutionService {
|
||||
|
||||
currentRun = await this.advanceNotification(currentRun, 'failed');
|
||||
|
||||
this.auditService.record({
|
||||
await this.auditService.record({
|
||||
action: 'DEPLOY_FAILED',
|
||||
resourceType: 'deploy_run',
|
||||
resourceId: currentRun.id,
|
||||
@@ -197,7 +197,7 @@ export class DeployExecutionService {
|
||||
`Jenkins build #${queueItem.buildNumber} is running.`,
|
||||
);
|
||||
|
||||
this.auditService.record({
|
||||
await this.auditService.record({
|
||||
action: 'JENKINS_BUILD_STARTED',
|
||||
resourceType: 'deploy_run',
|
||||
resourceId: currentRun.id,
|
||||
@@ -268,7 +268,7 @@ export class DeployExecutionService {
|
||||
'success',
|
||||
);
|
||||
|
||||
this.auditService.record({
|
||||
await this.auditService.record({
|
||||
action: 'JENKINS_DRY_RUN_COMPLETED',
|
||||
resourceType: 'deploy_run',
|
||||
resourceId: run.id,
|
||||
@@ -316,7 +316,7 @@ export class DeployExecutionService {
|
||||
: `Jenkins queue ${queueItem.queueId} accepted.`,
|
||||
);
|
||||
|
||||
this.auditService.record({
|
||||
await this.auditService.record({
|
||||
action: 'JENKINS_BUILD_TRIGGERED',
|
||||
resourceType: 'deploy_run',
|
||||
resourceId: run.id,
|
||||
@@ -352,7 +352,7 @@ export class DeployExecutionService {
|
||||
notification.status === 'sent' ? undefined : notification.reason,
|
||||
);
|
||||
|
||||
this.auditService.record({
|
||||
await this.auditService.record({
|
||||
action: `DEPLOY_NOTIFICATION_${notification.status.toUpperCase()}`,
|
||||
resourceType: 'deploy_run',
|
||||
resourceId: run.id,
|
||||
@@ -393,7 +393,7 @@ export class DeployExecutionService {
|
||||
}),
|
||||
);
|
||||
|
||||
this.auditService.record({
|
||||
await this.auditService.record({
|
||||
action: 'DEPLOY_SUCCEEDED',
|
||||
resourceType: 'deploy_run',
|
||||
resourceId: currentRun.id,
|
||||
@@ -429,7 +429,7 @@ export class DeployExecutionService {
|
||||
})) ?? currentRun;
|
||||
currentRun = await this.advanceNotification(currentRun, 'failed');
|
||||
|
||||
this.auditService.record({
|
||||
await this.auditService.record({
|
||||
action: 'DEPLOY_FAILED',
|
||||
resourceType: 'deploy_run',
|
||||
resourceId: currentRun.id,
|
||||
@@ -538,7 +538,7 @@ export class DeployExecutionService {
|
||||
const notification =
|
||||
await this.deployNotificationService.sendDeployRunEvent(run, event);
|
||||
|
||||
this.auditService.record({
|
||||
await this.auditService.record({
|
||||
action: `DEPLOY_NOTIFICATION_${notification.status.toUpperCase()}`,
|
||||
resourceType: 'deploy_run',
|
||||
resourceId: run.id,
|
||||
|
||||
@@ -61,10 +61,10 @@ export class DeployRunsService {
|
||||
}
|
||||
|
||||
const project = await this.projectsService.getProject(input.projectKey);
|
||||
this.validateReleaseRequest(input, project);
|
||||
await this.validateReleaseRequest(input, project);
|
||||
const run = await this.deployRunRepository.create(input);
|
||||
|
||||
this.auditService.record({
|
||||
await this.auditService.record({
|
||||
action: 'DEPLOY_REQUESTED',
|
||||
resourceType: 'deploy_run',
|
||||
resourceId: run.id,
|
||||
@@ -94,7 +94,7 @@ export class DeployRunsService {
|
||||
);
|
||||
}
|
||||
|
||||
this.auditService.record({
|
||||
await this.auditService.record({
|
||||
action: 'DEPLOY_CANCELED',
|
||||
resourceType: 'deploy_run',
|
||||
resourceId: run.id,
|
||||
@@ -150,7 +150,7 @@ export class DeployRunsService {
|
||||
}
|
||||
}
|
||||
|
||||
this.auditService.record({
|
||||
await this.auditService.record({
|
||||
action: 'JENKINS_SYNC_SWEEP_COMPLETED',
|
||||
resourceType: 'deploy_run',
|
||||
after: {
|
||||
@@ -175,7 +175,7 @@ export class DeployRunsService {
|
||||
const notification =
|
||||
await this.deployNotificationService.sendDeployRunEvent(run, event);
|
||||
|
||||
this.auditService.record({
|
||||
await this.auditService.record({
|
||||
action: `DEPLOY_NOTIFICATION_${notification.status.toUpperCase()}`,
|
||||
resourceType: 'deploy_run',
|
||||
resourceId: run.id,
|
||||
@@ -194,12 +194,12 @@ export class DeployRunsService {
|
||||
);
|
||||
}
|
||||
|
||||
private validateReleaseRequest(
|
||||
private async validateReleaseRequest(
|
||||
input: CreateDeployRunInput,
|
||||
project: ProjectSummary,
|
||||
): ProjectEnvironmentSummary {
|
||||
): Promise<ProjectEnvironmentSummary> {
|
||||
if (project.status !== 'active') {
|
||||
this.recordDeployRejected(input, 'Project is archived');
|
||||
await this.recordDeployRejected(input, 'Project is archived');
|
||||
throw new AppError(
|
||||
'VALIDATION_FAILED',
|
||||
`Project ${project.key} is archived and cannot be deployed`,
|
||||
@@ -213,7 +213,7 @@ export class DeployRunsService {
|
||||
);
|
||||
|
||||
if (!environment) {
|
||||
this.recordDeployRejected(input, 'Environment is not configured');
|
||||
await this.recordDeployRejected(input, 'Environment is not configured');
|
||||
throw new AppError(
|
||||
'VALIDATION_FAILED',
|
||||
`Project ${project.key} does not expose ${input.environment} environment`,
|
||||
@@ -223,7 +223,10 @@ export class DeployRunsService {
|
||||
}
|
||||
|
||||
if (!new RegExp(environment.refPattern).test(input.ref)) {
|
||||
this.recordDeployRejected(input, 'Ref does not match release policy');
|
||||
await this.recordDeployRejected(
|
||||
input,
|
||||
'Ref does not match release policy',
|
||||
);
|
||||
throw new AppError(
|
||||
'VALIDATION_FAILED',
|
||||
`Ref ${input.ref} does not match ${environment.releasePolicy} release policy`,
|
||||
@@ -241,11 +244,11 @@ export class DeployRunsService {
|
||||
return environment;
|
||||
}
|
||||
|
||||
private recordDeployRejected(
|
||||
private async recordDeployRejected(
|
||||
input: CreateDeployRunInput,
|
||||
reason: string,
|
||||
): void {
|
||||
this.auditService.record({
|
||||
): Promise<void> {
|
||||
await this.auditService.record({
|
||||
action: 'DEPLOY_REJECTED',
|
||||
resourceType: 'deploy_run',
|
||||
actorName: input.operator,
|
||||
|
||||
@@ -54,7 +54,7 @@ export class JenkinsSyncSchedulerService
|
||||
try {
|
||||
const summary = await this.deployRunsService.syncJenkinsRuns();
|
||||
|
||||
this.auditService.record({
|
||||
await this.auditService.record({
|
||||
action: 'JENKINS_AUTO_SYNC_TICK_COMPLETED',
|
||||
resourceType: 'deploy_run',
|
||||
after: {
|
||||
@@ -67,7 +67,7 @@ export class JenkinsSyncSchedulerService
|
||||
const message =
|
||||
error instanceof Error ? error.message : 'Jenkins auto sync failed';
|
||||
|
||||
this.auditService.record({
|
||||
await this.auditService.record({
|
||||
action: 'JENKINS_AUTO_SYNC_TICK_FAILED',
|
||||
resourceType: 'deploy_run',
|
||||
after: {
|
||||
|
||||
@@ -32,20 +32,33 @@ export class GiteaWebhookService {
|
||||
this.assertSignature(input);
|
||||
|
||||
if (input.headers.event !== 'push') {
|
||||
return this.ignore(input.headers, 'Only push events can trigger test deploys');
|
||||
return this.ignore(
|
||||
input.headers,
|
||||
'Only push events can trigger test deploys',
|
||||
);
|
||||
}
|
||||
|
||||
const payload = this.asPushPayload(input.payload);
|
||||
|
||||
if (!payload.ref.startsWith('refs/heads/')) {
|
||||
return this.ignore(input.headers, 'Only branch push events trigger test deploys');
|
||||
return this.ignore(
|
||||
input.headers,
|
||||
'Only branch push events trigger test deploys',
|
||||
);
|
||||
}
|
||||
|
||||
const branch = payload.ref.replace('refs/heads/', '');
|
||||
const projects = await this.projectsService.listProjects();
|
||||
const project = projects.find((item) => {
|
||||
const repositoryName = item.repositoryUrl.replace(/\.git$/, '').split('/').pop();
|
||||
const fullName = item.repositoryUrl.replace(/\.git$/, '').split('/').slice(-2).join('/');
|
||||
const repositoryName = item.repositoryUrl
|
||||
.replace(/\.git$/, '')
|
||||
.split('/')
|
||||
.pop();
|
||||
const fullName = item.repositoryUrl
|
||||
.replace(/\.git$/, '')
|
||||
.split('/')
|
||||
.slice(-2)
|
||||
.join('/');
|
||||
|
||||
return (
|
||||
item.key === payload.repository.name ||
|
||||
@@ -55,10 +68,17 @@ export class GiteaWebhookService {
|
||||
});
|
||||
|
||||
if (!project) {
|
||||
return this.ignore(input.headers, 'Repository is not managed by this platform');
|
||||
return this.ignore(
|
||||
input.headers,
|
||||
'Repository is not managed by this platform',
|
||||
);
|
||||
}
|
||||
|
||||
const allowedBranches = new Set([project.defaultBranch, 'develop', 'master']);
|
||||
const allowedBranches = new Set([
|
||||
project.defaultBranch,
|
||||
'develop',
|
||||
'master',
|
||||
]);
|
||||
|
||||
if (!allowedBranches.has(branch)) {
|
||||
return this.ignore(
|
||||
@@ -87,7 +107,7 @@ export class GiteaWebhookService {
|
||||
trigger: 'gitea_webhook',
|
||||
});
|
||||
|
||||
this.auditService.record({
|
||||
await this.auditService.record({
|
||||
action: 'GITEA_WEBHOOK_ACCEPTED',
|
||||
resourceType: 'deploy_run',
|
||||
resourceId: run.id,
|
||||
@@ -124,32 +144,44 @@ export class GiteaWebhookService {
|
||||
hubSignature256: input.headers.hubSignature256,
|
||||
})
|
||||
) {
|
||||
throw new AppError('VALIDATION_FAILED', 'Invalid Gitea webhook signature', 401);
|
||||
throw new AppError(
|
||||
'VALIDATION_FAILED',
|
||||
'Invalid Gitea webhook signature',
|
||||
401,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private asPushPayload(payload: unknown): GiteaPushPayload {
|
||||
if (!isRecord(payload) || !isRecord(payload.repository)) {
|
||||
throw new AppError('VALIDATION_FAILED', 'Invalid Gitea push payload', 400);
|
||||
throw new AppError(
|
||||
'VALIDATION_FAILED',
|
||||
'Invalid Gitea push payload',
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
const ref = typeof payload.ref === 'string' ? payload.ref : '';
|
||||
const after = typeof payload.after === 'string' ? payload.after : '';
|
||||
|
||||
if (!ref || !after) {
|
||||
throw new AppError('VALIDATION_FAILED', 'Gitea push payload missing ref or after', 400);
|
||||
throw new AppError(
|
||||
'VALIDATION_FAILED',
|
||||
'Gitea push payload missing ref or after',
|
||||
400,
|
||||
);
|
||||
}
|
||||
|
||||
return payload as GiteaPushPayload;
|
||||
}
|
||||
|
||||
private ignore(
|
||||
private async ignore(
|
||||
headers: GiteaWebhookHeaders,
|
||||
reason: string,
|
||||
projectKey?: string,
|
||||
branch?: string,
|
||||
): GiteaWebhookResult {
|
||||
this.auditService.record({
|
||||
): Promise<GiteaWebhookResult> {
|
||||
await this.auditService.record({
|
||||
action: 'GITEA_WEBHOOK_IGNORED',
|
||||
resourceType: 'gitea_webhook',
|
||||
resourceId: headers.delivery,
|
||||
|
||||
Reference in New Issue
Block a user