Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
76 changes: 76 additions & 0 deletions src/api/ai-workflow/ai-workflow.controller.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
ValidationPipe,
Query,
Param,
StreamableFile,
} from '@nestjs/common';
import {
ApiBearerAuth,
Expand Down Expand Up @@ -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({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We shall have a @ApiParam for workflowId right?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As bellow, the params are automatically added by swagger when you use @Param('workflowId') workflowId: string, in the method signature. I used @ApiParam to enhance the description in here.
image

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: 'Download an attachment archive linked to the specific run',
})
@ApiParam({
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again we need to add workflowId and runId to docs call signature.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@kkartunov in the new swagger version, these annotations are used only to add description.
When you use @Param('workflowId') workflowId: string, in the method signature, they're automatically added to swagger as inputs.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

image

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' })
Expand Down
62 changes: 61 additions & 1 deletion src/api/ai-workflow/ai-workflow.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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');
}
Expand Down Expand Up @@ -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 },
});
Expand Down Expand Up @@ -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: {
Expand Down
64 changes: 63 additions & 1 deletion src/shared/modules/global/gitea.service.ts
Original file line number Diff line number Diff line change
@@ -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';

Expand Down Expand Up @@ -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;
}
}
}