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
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import {
getDeploymentById,
getDeploymentReadiness,
getDeploymentsByEnvironmentId,
getDeploymentWorkflowConfig,
getEnvironmentById,
getEnvironmentReviewers,
getEnvironmentsByRepositoryId,
Expand Down Expand Up @@ -87,6 +88,7 @@ import {
updateUserSettings,
updateWorkflowGroups,
updateWorkflowLabel,
upsertDeploymentWorkflowConfig,
} from '../sdk.gen';
import type {
CancelDeploymentData,
Expand Down Expand Up @@ -139,6 +141,7 @@ import type {
GetDeploymentByIdData,
GetDeploymentReadinessData,
GetDeploymentsByEnvironmentIdData,
GetDeploymentWorkflowConfigData,
GetEnvironmentByIdData,
GetEnvironmentReviewersData,
GetEnvironmentsByRepositoryIdData,
Expand Down Expand Up @@ -237,6 +240,9 @@ import type {
UpdateWorkflowGroupsError,
UpdateWorkflowLabelData,
UpdateWorkflowLabelError,
UpsertDeploymentWorkflowConfigData,
UpsertDeploymentWorkflowConfigError,
UpsertDeploymentWorkflowConfigResponse,
} from '../types.gen';

export const updateWorkflowLabelMutation = (
Expand Down Expand Up @@ -318,6 +324,39 @@ const createQueryKey = <TOptions extends Options>(id: string, options?: TOptions
return [params];
};

export const getDeploymentWorkflowConfigQueryKey = (options: Options<GetDeploymentWorkflowConfigData>) => createQueryKey('getDeploymentWorkflowConfig', options);

export const getDeploymentWorkflowConfigOptions = (options: Options<GetDeploymentWorkflowConfigData>) => {
return queryOptions({
queryFn: async ({ queryKey, signal }) => {
const { data } = await getDeploymentWorkflowConfig({
...options,
...queryKey[0],
signal,
throwOnError: true,
});
return data;
},
queryKey: getDeploymentWorkflowConfigQueryKey(options),
});
};

export const upsertDeploymentWorkflowConfigMutation = (
options?: Partial<Options<UpsertDeploymentWorkflowConfigData>>
): MutationOptions<UpsertDeploymentWorkflowConfigResponse, UpsertDeploymentWorkflowConfigError, Options<UpsertDeploymentWorkflowConfigData>> => {
const mutationOptions: MutationOptions<UpsertDeploymentWorkflowConfigResponse, UpsertDeploymentWorkflowConfigError, Options<UpsertDeploymentWorkflowConfigData>> = {
mutationFn: async fnOptions => {
const { data } = await upsertDeploymentWorkflowConfig({
...options,
...fnOptions,
throwOnError: true,
});
return data;
},
};
return mutationOptions;
};

export const getGitRepoSettingsQueryKey = (options: Options<GetGitRepoSettingsData>) => createQueryKey('getGitRepoSettings', options);

export const getGitRepoSettingsOptions = (options: Options<GetGitRepoSettingsData>) => {
Expand Down
21 changes: 21 additions & 0 deletions client/src/app/core/modules/openapi/schemas.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,19 @@ export const TestTypeDtoSchema = {
required: ['artifactName', 'name', 'workflowId'],
} as const;

export const DeploymentWorkflowConfigDtoSchema = {
type: 'object',
properties: {
workflowId: {
type: 'integer',
format: 'int64',
},
deployJobName: {
type: 'string',
},
},
} as const;

export const GitRepoSettingsDtoSchema = {
type: 'object',
properties: {
Expand Down Expand Up @@ -189,6 +202,14 @@ export const EnvironmentDeploymentSchema = {
type: 'string',
enum: ['GITHUB', 'HELIOS'],
},
estimatedBuildDurationSeconds: {
type: 'integer',
format: 'int32',
},
estimatedDeployDurationSeconds: {
type: 'integer',
format: 'int32',
},
},
required: ['id', 'type'],
} as const;
Expand Down
24 changes: 24 additions & 0 deletions client/src/app/core/modules/openapi/sdk.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,9 @@ import type {
GetDeploymentsByEnvironmentIdData,
GetDeploymentsByEnvironmentIdErrors,
GetDeploymentsByEnvironmentIdResponses,
GetDeploymentWorkflowConfigData,
GetDeploymentWorkflowConfigErrors,
GetDeploymentWorkflowConfigResponses,
GetEnvironmentByIdData,
GetEnvironmentByIdErrors,
GetEnvironmentByIdResponses,
Expand Down Expand Up @@ -249,6 +252,9 @@ import type {
UpdateWorkflowLabelData,
UpdateWorkflowLabelErrors,
UpdateWorkflowLabelResponses,
UpsertDeploymentWorkflowConfigData,
UpsertDeploymentWorkflowConfigErrors,
UpsertDeploymentWorkflowConfigResponses,
} from './types.gen';

export type Options<TData extends TDataShape = TDataShape, ThrowOnError extends boolean = boolean> = Options2<TData, ThrowOnError> & {
Expand Down Expand Up @@ -294,6 +300,24 @@ export const updateTestType = <ThrowOnError extends boolean = false>(options: Op
});
};

export const getDeploymentWorkflowConfig = <ThrowOnError extends boolean = false>(options: Options<GetDeploymentWorkflowConfigData, ThrowOnError>) => {
return (options.client ?? client).get<GetDeploymentWorkflowConfigResponses, GetDeploymentWorkflowConfigErrors, ThrowOnError>({
url: '/api/settings/{repositoryId}/workflows/{workflowId}/deployment-config',
...options,
});
};

export const upsertDeploymentWorkflowConfig = <ThrowOnError extends boolean = false>(options: Options<UpsertDeploymentWorkflowConfigData, ThrowOnError>) => {
return (options.client ?? client).put<UpsertDeploymentWorkflowConfigResponses, UpsertDeploymentWorkflowConfigErrors, ThrowOnError>({
url: '/api/settings/{repositoryId}/workflows/{workflowId}/deployment-config',
...options,
headers: {
'Content-Type': 'application/json',
...options.headers,
},
});
};

export const getGitRepoSettings = <ThrowOnError extends boolean = false>(options: Options<GetGitRepoSettingsData, ThrowOnError>) => {
return (options.client ?? client).get<GetGitRepoSettingsResponses, GetGitRepoSettingsErrors, ThrowOnError>({
url: '/api/settings/{repositoryId}/settings',
Expand Down
63 changes: 63 additions & 0 deletions client/src/app/core/modules/openapi/types.gen.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,11 @@ export type TestTypeDto = {
workflowId: number;
};

export type DeploymentWorkflowConfigDto = {
workflowId?: number;
deployJobName?: string;
};

export type GitRepoSettingsDto = {
id?: number;
lockExpirationThreshold?: number;
Expand Down Expand Up @@ -64,6 +69,8 @@ export type EnvironmentDeployment = {
createdAt?: string;
updatedAt?: string;
type: 'GITHUB' | 'HELIOS';
estimatedBuildDurationSeconds?: number;
estimatedDeployDurationSeconds?: number;
};

export type EnvironmentDto = {
Expand Down Expand Up @@ -831,6 +838,62 @@ export type UpdateTestTypeResponses = {

export type UpdateTestTypeResponse = UpdateTestTypeResponses[keyof UpdateTestTypeResponses];

export type GetDeploymentWorkflowConfigData = {
body?: never;
path: {
repositoryId: number;
workflowId: number;
};
query?: never;
url: '/api/settings/{repositoryId}/workflows/{workflowId}/deployment-config';
};

export type GetDeploymentWorkflowConfigErrors = {
/**
* Conflict
*/
409: ApiError;
};

export type GetDeploymentWorkflowConfigError = GetDeploymentWorkflowConfigErrors[keyof GetDeploymentWorkflowConfigErrors];

export type GetDeploymentWorkflowConfigResponses = {
/**
* OK
*/
200: DeploymentWorkflowConfigDto;
};

export type GetDeploymentWorkflowConfigResponse = GetDeploymentWorkflowConfigResponses[keyof GetDeploymentWorkflowConfigResponses];

export type UpsertDeploymentWorkflowConfigData = {
body: DeploymentWorkflowConfigDto;
path: {
repositoryId: number;
workflowId: number;
};
query?: never;
url: '/api/settings/{repositoryId}/workflows/{workflowId}/deployment-config';
};

export type UpsertDeploymentWorkflowConfigErrors = {
/**
* Conflict
*/
409: ApiError;
};

export type UpsertDeploymentWorkflowConfigError = UpsertDeploymentWorkflowConfigErrors[keyof UpsertDeploymentWorkflowConfigErrors];

export type UpsertDeploymentWorkflowConfigResponses = {
/**
* OK
*/
200: DeploymentWorkflowConfigDto;
};

export type UpsertDeploymentWorkflowConfigResponse = UpsertDeploymentWorkflowConfigResponses[keyof UpsertDeploymentWorkflowConfigResponses];

export type GetGitRepoSettingsData = {
body?: never;
path: {
Expand Down
12 changes: 9 additions & 3 deletions client/src/app/core/services/deployment-timing.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -133,10 +133,16 @@ export class DeploymentTimingService {
// Get the estimated times for each step based on deployment properties
public getEstimatedTimes(deployment: EnvironmentDeployment): EstimatedTimes {
const prExists = deployment?.prName != null;
const defaultPending = prExists ? 2 : 11;

const pendingMin = deployment?.estimatedBuildDurationSeconds != null ? deployment.estimatedBuildDurationSeconds / 60 : defaultPending;

const inProgressMin = deployment?.estimatedDeployDurationSeconds != null ? deployment.estimatedDeployDurationSeconds / 60 : 4;

return {
REQUESTED: prExists ? 2 : 11, // REQUESTED is not shown but still part of logic
PENDING: prExists ? 2 : 11,
IN_PROGRESS: 4,
REQUESTED: pendingMin,
PENDING: pendingMin,
IN_PROGRESS: inProgressMin,
};
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@ <h3 class="text-xl">Workflows</h3>
<th>Status</th>
<th>Assign Group</th>
<th>Label</th>
<th>Deployment Job</th>
</tr>
</ng-template>
<ng-template pTemplate="body" let-workflow>
Expand Down Expand Up @@ -193,9 +194,37 @@ <h3 class="text-xl">Workflows</h3>
</ng-template>
</p-select>
</td>
<td>
<p-button label="Configure" size="small" severity="secondary" (click)="openDeploymentConfig(workflow)" />
</td>
</tr>
</ng-template>
</p-table>

<!-- Deployment Workflow Config Dialog -->
<p-dialog header="Deployment Job" [(visible)]="deploymentConfigDialogVisible" [modal]="true" [draggable]="false" [style]="{ width: '420px' }">
<div class="flex flex-col gap-4 p-2">
<div class="flex flex-col gap-1">
<label class="font-semibold" for="deployJobName">Deploy Job Name</label>
<p class="text-sm text-muted-color">
The GitHub Actions job name that starts the actual server deployment (e.g. <code>deploy</code>). Helios uses this job to split pre-deployment time from deployment time.
</p>
<input
id="deployJobName"
type="text"
pInputText
[ngModel]="deploymentConfigForm().deployJobName"
(ngModelChange)="setDeploymentConfigField('deployJobName', $event)"
placeholder="e.g. deploy"
class="w-full"
/>
</div>
<div class="flex justify-end gap-2 mt-2">
<p-button label="Cancel" severity="secondary" text (click)="deploymentConfigDialogVisible = false" />
<p-button label="Save" (click)="saveDeploymentConfig()" [loading]="upsertDeploymentWorkflowConfigMutation.isPending()" />
</div>
</div>
</p-dialog>
}

<div class="flex justify-between items-center mb-2">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ import { PanelModule } from 'primeng/panel';
import { TableModule } from 'primeng/table';
import { LockingThresholdsComponent } from '@app/components/locking-thresholds/locking-thresholds.component';
import { PageHeadingComponent } from '@app/components/page-heading/page-heading.component';
import { RotateSecretResponses, TestTypeDto, WorkflowDto, WorkflowGroupDto, WorkflowMembershipDto } from '@app/core/modules/openapi';
import { DeploymentWorkflowConfigDto, RotateSecretResponses, TestTypeDto, WorkflowDto, WorkflowGroupDto, WorkflowMembershipDto } from '@app/core/modules/openapi';
import {
createWorkflowGroupMutation,
deleteWorkflowGroupMutation,
getDeploymentWorkflowConfigOptions,
getDeploymentWorkflowConfigQueryKey,
getGitRepoSettingsOptions,
getGroupsWithWorkflowsOptions,
getGroupsWithWorkflowsQueryKey,
Expand All @@ -30,6 +32,7 @@ import {
getAllTestTypesOptions,
updateGitRepoSettingsMutation,
rotateSecretMutation,
upsertDeploymentWorkflowConfigMutation,
} from '@app/core/modules/openapi/@tanstack/angular-query-experimental.gen';
import { WorkflowDtoSchema } from '@app/core/modules/openapi/schemas.gen';
import { MessageService } from 'primeng/api';
Expand Down Expand Up @@ -286,6 +289,57 @@ export class ProjectSettingsComponent {

workflowLabelOptions = Object.values(WorkflowDtoSchema.properties.label.enum);

// Deployment workflow config: per-workflow job name configuration
deploymentConfigWorkflowId = signal<number | null>(null);
deploymentConfigForm = signal<DeploymentWorkflowConfigDto>({ deployJobName: '' });
deploymentConfigDialogVisible = false;

deploymentWorkflowConfigQuery = injectQuery(() => ({
...getDeploymentWorkflowConfigOptions({
path: { repositoryId: this.repositoryId(), workflowId: this.deploymentConfigWorkflowId()! },
}),
enabled: () => !!this.deploymentConfigWorkflowId(),
}));

private deploymentConfigDataEffect = effect(() => {
const data = this.deploymentWorkflowConfigQuery.data();
this.deploymentConfigForm.set({
deployJobName: data?.deployJobName ?? '',
});
});

upsertDeploymentWorkflowConfigMutation = injectMutation(() => ({
...upsertDeploymentWorkflowConfigMutation(),
onSuccess: () => {
this.queryClient.invalidateQueries({
queryKey: getDeploymentWorkflowConfigQueryKey({
path: { repositoryId: this.repositoryId(), workflowId: this.deploymentConfigWorkflowId()! },
}),
});
this.deploymentConfigDialogVisible = false;
this.messageService.add({ severity: 'success', summary: 'Success', detail: 'Deployment workflow config saved' });
},
}));

openDeploymentConfig(workflow: WorkflowDto) {
this.deploymentConfigForm.set({ deployJobName: '' });
this.deploymentConfigWorkflowId.set(workflow.id);
this.deploymentConfigDialogVisible = true;
}

setDeploymentConfigField(field: keyof DeploymentWorkflowConfigDto, value: string) {
this.deploymentConfigForm.update(form => ({ ...form, [field]: value }));
}

saveDeploymentConfig() {
const workflowId = this.deploymentConfigWorkflowId();
if (!workflowId) return;
this.upsertDeploymentWorkflowConfigMutation.mutate({
path: { repositoryId: this.repositoryId(), workflowId },
body: { ...this.deploymentConfigForm(), workflowId },
});
}

storePreviousLabel(workflow: WorkflowDto) {
// Store the current label before change
this.previousLabel = workflow.label;
Expand Down
Loading
Loading