From c88307114fc32b72a8376821a517b35899da1cb0 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Wed, 8 Oct 2025 14:55:10 +0300 Subject: [PATCH 1/3] PM-2213 - fetch workflowRun attachments --- src/api/ai-workflow/ai-workflow.controller.ts | 76 +++++++++++++++++++ src/api/ai-workflow/ai-workflow.service.ts | 62 ++++++++++++++- src/shared/modules/global/gitea.service.ts | 64 +++++++++++++++- 3 files changed, 200 insertions(+), 2 deletions(-) diff --git a/src/api/ai-workflow/ai-workflow.controller.ts b/src/api/ai-workflow/ai-workflow.controller.ts index 2e7d7c1..0eaf197 100644 --- a/src/api/ai-workflow/ai-workflow.controller.ts +++ b/src/api/ai-workflow/ai-workflow.controller.ts @@ -7,6 +7,7 @@ import { ValidationPipe, Query, Param, + StreamableFile, } from '@nestjs/common'; import { ApiBearerAuth, @@ -207,6 +208,81 @@ export class AiWorkflowController { return this.aiWorkflowService.updateWorkflowRun(workflowId, runId, body); } + @Get('/:workflowId/runs/:runId/attachments') + @Roles( + UserRole.Admin, + UserRole.Copilot, + UserRole.ProjectManager, + UserRole.Reviewer, + UserRole.Submitter, + UserRole.User, + ) + @Scopes(Scope.ReadWorkflowRun) + @ApiOperation({ + summary: 'List all attachments linked to the specific run', + }) + @ApiParam({ + name: 'runId', + description: 'The ID of the run to fetch the attachments for', + required: true, + }) + @ApiResponse({ + status: 200, + description: 'The AI workflow run attachments.', + }) + @ApiResponse({ status: 403, description: 'Forbidden.' }) + async getRunAttachments( + @Param('workflowId') workflowId: string, + @Param('runId') runId: string, + @User() user: JwtUser, + ) { + return await this.aiWorkflowService.getWorkflowRunAttachments( + workflowId, + runId, + user, + ); + } + + @Get('/:workflowId/runs/:runId/attachments/:attachmentId/zip') + @Roles( + UserRole.Admin, + UserRole.Copilot, + UserRole.ProjectManager, + UserRole.Reviewer, + UserRole.Submitter, + UserRole.User, + ) + @Scopes(Scope.ReadWorkflowRun) + @ApiOperation({ + summary: 'Downloard an attachment archive linked to the specific run', + }) + @ApiParam({ + name: 'attachmentId', + description: 'The ID of the workflow run attachment to download', + required: true, + }) + @ApiResponse({ + status: 302, + description: 'Redirect to the blob to download', + }) + @ApiResponse({ status: 403, description: 'Forbidden.' }) + async downloadRunAttachment( + @Param('workflowId') workflowId: string, + @Param('runId') runId: string, + @Param('attachmentId') attachmentId: string, + @User() user: JwtUser, + ) { + const zipResponse = + await this.aiWorkflowService.downloadWorkflowRunAttachment( + workflowId, + runId, + attachmentId, + user, + ); + + return new StreamableFile(zipResponse.data); + } + @Post('/:workflowId/runs/:runId/items') @Scopes(Scope.CreateWorkflowRun) @ApiOperation({ summary: 'Create AIWorkflowRunItems in batch' }) diff --git a/src/api/ai-workflow/ai-workflow.service.ts b/src/api/ai-workflow/ai-workflow.service.ts index 0ef8239..c475227 100644 --- a/src/api/ai-workflow/ai-workflow.service.ts +++ b/src/api/ai-workflow/ai-workflow.service.ts @@ -26,6 +26,7 @@ import { ResourceApiService } from 'src/shared/modules/global/resource.service'; import { UserRole } from 'src/shared/enums/userRole.enum'; import { ChallengeStatus } from 'src/shared/enums/challengeStatus.enum'; import { LoggerService } from 'src/shared/modules/global/logger.service'; +import { GiteaService } from 'src/shared/modules/global/gitea.service'; @Injectable() export class AiWorkflowService { @@ -35,6 +36,7 @@ export class AiWorkflowService { private readonly prisma: PrismaService, private readonly challengeApiService: ChallengeApiService, private readonly resourceApiService: ResourceApiService, + private readonly giteaService: GiteaService, ) { this.logger = LoggerService.forRoot('AiWorkflowService'); } @@ -617,7 +619,19 @@ export class AiWorkflowService { } } - async getRunItems(workflowId: string, runId: string, user: JwtUser) { + /** + * Fetches the workflow & run data for the specified workflowId and runId + * It also makes sure the specified user has the right permissions to access the run + * @param user + * @param workflowId + * @param runId + * @returns + */ + private async getWorkflowRunWithGuards( + user: JwtUser, + workflowId: string, + runId: string, + ) { const workflow = await this.prisma.aiWorkflow.findUnique({ where: { id: workflowId }, }); @@ -703,6 +717,52 @@ export class AiWorkflowService { } } + return { workflow, run }; + } + + async getWorkflowRunAttachments( + workflowId: string, + runId: string, + user: JwtUser, + ) { + const { workflow, run } = await this.getWorkflowRunWithGuards( + user, + workflowId, + runId, + ); + + const [owner, repo] = workflow.gitOwnerRepo.split('/'); + const artifacts = await this.giteaService.getWorkflowRunArtifacts( + owner, + repo, + +run.gitRunId, + ); + return artifacts; + } + + async downloadWorkflowRunAttachment( + workflowId: string, + runId: string, + attachmentId: string, + user: JwtUser, + ) { + const { workflow } = await this.getWorkflowRunWithGuards( + user, + workflowId, + runId, + ); + + const [owner, repo] = workflow.gitOwnerRepo.split('/'); + return this.giteaService.downloadWorkflowRunArtifact( + owner, + repo, + attachmentId, + ); + } + + async getRunItems(workflowId: string, runId: string, user: JwtUser) { + await this.getWorkflowRunWithGuards(user, workflowId, runId); + const items = await this.prisma.aiWorkflowRunItem.findMany({ where: { workflowRunId: runId }, include: { diff --git a/src/shared/modules/global/gitea.service.ts b/src/shared/modules/global/gitea.service.ts index 8f7a051..6f11056 100644 --- a/src/shared/modules/global/gitea.service.ts +++ b/src/shared/modules/global/gitea.service.ts @@ -1,4 +1,9 @@ -import { Injectable, Logger } from '@nestjs/common'; +import { + HttpExceptionOptions, + Injectable, + InternalServerErrorException, + Logger, +} from '@nestjs/common'; import { Api, Repository } from 'src/shared/clients/gitea/gitea.client'; import { aiWorkflow, aiWorkflowRun } from '@prisma/client'; @@ -175,4 +180,61 @@ export class GiteaService { return this.getAiWorkflowDataFromLogs(owner, repo, jobId, retry + 1); } } + + async getWorkflowRunArtifacts(owner: string, repo: string, gitJobId: number) { + try { + const response = await this.giteaClient.repos.getArtifactsOfRun( + owner, + repo, + gitJobId, + ); + + if (response.status > 299) { + throw new InternalServerErrorException(`${response.statusText}`, { + description: (response.data as any)?.message, + } as HttpExceptionOptions); + } + + return response.data; + } catch (e) { + this.logger.error( + 'Failed to fetch Artifacts for git action run', + e?.message ?? e, + { + owner, + repo, + gitJobId, + }, + ); + throw e; + } + } + + async downloadWorkflowRunArtifact( + owner: string, + repo: string, + artifactId: string, + ) { + try { + return await this.giteaClient.repos.downloadArtifact( + owner, + repo, + artifactId, + { + format: 'stream', + }, + ); + } catch (e) { + this.logger.error( + 'Failed to download Artifact for git action run', + e?.message ?? e, + { + owner, + repo, + artifactId, + }, + ); + throw e; + } + } } From 4443dba90269a12504a696b433c7edab2a80bb58 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Wed, 8 Oct 2025 15:08:50 +0300 Subject: [PATCH 2/3] typo --- src/api/ai-workflow/ai-workflow.controller.ts | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/api/ai-workflow/ai-workflow.controller.ts b/src/api/ai-workflow/ai-workflow.controller.ts index 0eaf197..845c721 100644 --- a/src/api/ai-workflow/ai-workflow.controller.ts +++ b/src/api/ai-workflow/ai-workflow.controller.ts @@ -35,12 +35,16 @@ import { Scope } from 'src/shared/enums/scopes.enum'; import { Roles } from 'src/shared/guards/tokenRoles.guard'; import { JwtUser } from 'src/shared/modules/global/jwt.service'; import { User } from 'src/shared/decorators/user.decorator'; +import { SubmissionScanCompleteOrchestrator } from 'src/shared/modules/global/submission-scan-complete.orchestrator'; @ApiTags('ai_workflow') @ApiBearerAuth() @Controller('/workflows') export class AiWorkflowController { - constructor(private readonly aiWorkflowService: AiWorkflowService) {} + constructor( + private readonly aiWorkflowService: AiWorkflowService, + private readonly orchestrator: SubmissionScanCompleteOrchestrator, + ) {} @Post() @Roles(UserRole.Admin) @@ -254,7 +258,7 @@ export class AiWorkflowController { ) @Scopes(Scope.ReadWorkflowRun) @ApiOperation({ - summary: 'Downloard an attachment archive linked to the specific run', + summary: 'Download an attachment archive linked to the specific run', }) @ApiParam({ name: 'attachmentId', @@ -470,4 +474,9 @@ export class AiWorkflowController { body, ); } + + @Get('/submission/:submissionId') + triggerOrchestration(@Param('submissionId') submissionId: string) { + return this.orchestrator.orchestrateScanComplete(submissionId); + } } From 43eec1de711a1aba3f613f25939d6cb2db701ba4 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Wed, 8 Oct 2025 15:10:50 +0300 Subject: [PATCH 3/3] undo dev work --- src/api/ai-workflow/ai-workflow.controller.ts | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/api/ai-workflow/ai-workflow.controller.ts b/src/api/ai-workflow/ai-workflow.controller.ts index 845c721..afa4303 100644 --- a/src/api/ai-workflow/ai-workflow.controller.ts +++ b/src/api/ai-workflow/ai-workflow.controller.ts @@ -35,16 +35,12 @@ import { Scope } from 'src/shared/enums/scopes.enum'; import { Roles } from 'src/shared/guards/tokenRoles.guard'; import { JwtUser } from 'src/shared/modules/global/jwt.service'; import { User } from 'src/shared/decorators/user.decorator'; -import { SubmissionScanCompleteOrchestrator } from 'src/shared/modules/global/submission-scan-complete.orchestrator'; @ApiTags('ai_workflow') @ApiBearerAuth() @Controller('/workflows') export class AiWorkflowController { - constructor( - private readonly aiWorkflowService: AiWorkflowService, - private readonly orchestrator: SubmissionScanCompleteOrchestrator, - ) {} + constructor(private readonly aiWorkflowService: AiWorkflowService) {} @Post() @Roles(UserRole.Admin) @@ -474,9 +470,4 @@ export class AiWorkflowController { body, ); } - - @Get('/submission/:submissionId') - triggerOrchestration(@Param('submissionId') submissionId: string) { - return this.orchestrator.orchestrateScanComplete(submissionId); - } }