-
Notifications
You must be signed in to change notification settings - Fork 1
feat: implement AI-based deployment job detection in workflows #970
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: staging
Are you sure you want to change the base?
Changes from all commits
b2d089f
9e873ea
4ed7839
853b6e7
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,42 @@ | ||
| import { HttpClient } from '@angular/common/http'; | ||
| import { Injectable, inject } from '@angular/core'; | ||
| import { Observable } from 'rxjs'; | ||
| import { environment } from 'environments/environment'; | ||
| import { KeycloakService } from './keycloak/keycloak.service'; | ||
| import { RepositoryService } from './repository.service'; | ||
|
|
||
| export type WorkflowDeploymentJobDetectionStatus = 'FOUND' | 'NOT_FOUND' | 'UNCLEAR' | 'ERROR'; | ||
|
|
||
| export interface WorkflowDeploymentJobDetectionResponse { | ||
| workflowId: number; | ||
| workflowPath: string; | ||
| ref: string; | ||
| deploymentJobName: string | null; | ||
| status: WorkflowDeploymentJobDetectionStatus; | ||
| message: string; | ||
| } | ||
|
|
||
| @Injectable({ | ||
| providedIn: 'root', | ||
| }) | ||
| export class WorkflowAiService { | ||
| private readonly http = inject(HttpClient); | ||
| private readonly keycloakService = inject(KeycloakService); | ||
| private readonly repositoryService = inject(RepositoryService); | ||
| private readonly baseUrl = environment.serverUrl.replace(/\/$/, ''); | ||
|
|
||
| detectDeploymentJob(repositoryId: number, workflowId: number): Observable<WorkflowDeploymentJobDetectionResponse> { | ||
| const token = this.keycloakService.keycloak.token; | ||
| const currentRepositoryId = this.repositoryService.currentRepositoryId(); | ||
| return this.http.post<WorkflowDeploymentJobDetectionResponse>( | ||
| `${this.baseUrl}/api/settings/${repositoryId}/workflows/${workflowId}/detect-deployment-job`, | ||
| {}, | ||
| { | ||
| headers: { | ||
| ...(token ? { Authorization: `Bearer ${token}` } : {}), | ||
| ...(currentRepositoryId ? { 'X-Repository-Id': String(currentRepositoryId) } : {}), | ||
| }, | ||
| } | ||
| ); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,3 +1,4 @@ | ||
| import { HttpErrorResponse } from '@angular/common/http'; | ||
| import { Component, computed, effect, inject, input, numberAttribute, signal } from '@angular/core'; | ||
| import { FormsModule } from '@angular/forms'; | ||
| import { injectMutation, injectQuery, QueryClient } from '@tanstack/angular-query-experimental'; | ||
|
|
@@ -45,6 +46,8 @@ import { provideTablerIcons, TablerIconComponent } from 'angular-tabler-icons'; | |
| import { IconCircleCheck, IconPencil, IconPlus, IconTrash } from 'angular-tabler-icons/icons'; | ||
| import { SecretGenerateConfirmationComponent } from '@app/components/dialogs/secret-generate-confirmation/secret-generate-confirmation.component'; | ||
| import { getStatusColors } from '@app/core/utils/status-colors'; | ||
| import { firstValueFrom } from 'rxjs'; | ||
| import { WorkflowAiService, WorkflowDeploymentJobDetectionResponse } from '@app/core/services/workflow-ai.service'; | ||
|
|
||
| @Component({ | ||
| selector: 'app-project-settings', | ||
|
|
@@ -81,6 +84,7 @@ import { getStatusColors } from '@app/core/utils/status-colors'; | |
| export class ProjectSettingsComponent { | ||
| private messageService = inject(MessageService); | ||
| private confirmationService = inject(ConfirmationService); | ||
| private workflowAiService = inject(WorkflowAiService); | ||
| queryClient = inject(QueryClient); | ||
|
|
||
| // Signals for repository ID, workflows, and workflow groups | ||
|
|
@@ -112,6 +116,8 @@ export class ProjectSettingsComponent { | |
| // This is recalculated from dropdown selection/assignment in onChangeGroup() | ||
| // This is recalculated from drag&drop logic in updateGroups() | ||
| workflowGroupsMap = signal<Record<number, string>>({}); | ||
| workflowDetectionResults = signal<Record<number, WorkflowDeploymentJobDetectionResponse>>({}); | ||
| detectingWorkflowId = signal<number | null>(null); | ||
| readonly successIconClasses = getStatusColors('success').icon; | ||
|
|
||
| constructor() { | ||
|
|
@@ -287,6 +293,44 @@ export class ProjectSettingsComponent { | |
| }, | ||
| })); | ||
|
|
||
| detectDeploymentJobMutation = injectMutation(() => ({ | ||
| mutationFn: async ({ workflowId }: { workflowId: number }) => { | ||
| return await firstValueFrom(this.workflowAiService.detectDeploymentJob(this.repositoryId(), workflowId)); | ||
| }, | ||
| onMutate: ({ workflowId }) => { | ||
| this.detectingWorkflowId.set(workflowId); | ||
| }, | ||
| onSuccess: result => { | ||
| this.workflowDetectionResults.update(previous => ({ ...previous, [result.workflowId]: result })); | ||
| }, | ||
| onError: error => { | ||
| const workflowId = this.detectingWorkflowId(); | ||
| if (workflowId == null) { | ||
| return; | ||
|
Comment on lines
+307
to
+309
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
The mutation callbacks read and clear a single shared Useful? React with 👍 / 👎. |
||
| } | ||
|
|
||
| const detail = | ||
| error instanceof HttpErrorResponse | ||
| ? error.error?.message || error.message || 'Helios could not analyze the workflow right now.' | ||
| : 'Helios could not analyze the workflow right now.'; | ||
|
|
||
| this.workflowDetectionResults.update(previous => ({ | ||
| ...previous, | ||
| [workflowId]: { | ||
| workflowId, | ||
| workflowPath: '', | ||
| ref: '', | ||
| deploymentJobName: null, | ||
| status: 'ERROR', | ||
| message: detail, | ||
| }, | ||
| })); | ||
| }, | ||
| onSettled: () => { | ||
| this.detectingWorkflowId.set(null); | ||
| }, | ||
| })); | ||
|
|
||
| workflowLabelOptions = Object.values(WorkflowDtoSchema.properties.label.enum); | ||
|
|
||
| // Deployment workflow config: per-workflow job name configuration | ||
|
|
@@ -445,6 +489,22 @@ export class ProjectSettingsComponent { | |
| this.syncWorkflowsMutation.mutate({ path: { repositoryId } }); | ||
| } | ||
|
|
||
| detectDeploymentJob(workflowId: number) { | ||
| this.detectDeploymentJobMutation.mutate({ workflowId }); | ||
| } | ||
|
|
||
| getDetectionResult(workflowId: number) { | ||
| return this.workflowDetectionResults()[workflowId]; | ||
| } | ||
|
|
||
| detectionResultClass(result: WorkflowDeploymentJobDetectionResponse | undefined) { | ||
| if (!result) { | ||
| return 'text-muted-color'; | ||
| } | ||
|
|
||
| return result.status === 'FOUND' ? 'text-green-600 dark:text-green-400' : 'text-muted-color'; | ||
| } | ||
|
|
||
| // Update the groups on the server | ||
| updateGroups() { | ||
| if (!this.repositoryId()) return; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This request builds the URL from the
repositoryIdargument but setsX-Repository-IdfromrepositoryService.currentRepositoryId(), so the path and authorization context can diverge. When they differ (for example during route transitions or if this service is reused with an explicit repository id), the backend computes roles from the header and can authorize against the wrong repository or return 403 for a valid maintainer request. Build the header from the samerepositoryIdargument used in the URL to keep permission checks consistent.Useful? React with 👍 / 👎.