improve type safety with new type structure

Signed-off-by: Kurt King <kurtaking@gmail.com>
This commit is contained in:
Kurt King
2025-03-21 22:11:08 -06:00
committed by benjdlambert
parent d9f34436be
commit fbd7c921b9
2 changed files with 78 additions and 43 deletions
@@ -60,6 +60,7 @@ import { scaffolderActionRules } from '../../service/rules';
import { createCounterMetric, createHistogramMetric } from '../../util/metrics';
import { BackstageLoggerTransport, WinstonLogger } from './logger';
import { convertFiltersToRecord } from '../../util/templating';
import { CheckpointState } from './StorageTaskBroker';
type NunjucksWorkflowRunnerOptions = {
workingDirectory: string;
@@ -91,16 +92,6 @@ type TemplateContext = {
};
};
type CheckpointState =
| {
status: 'failed';
reason: string;
}
| {
status: 'success';
value: JsonValue;
};
const isValidTaskSpec = (taskSpec: TaskSpec): taskSpec is TaskSpecV1beta3 => {
return taskSpec.apiVersion === 'scaffolder.backstage.io/v1beta3';
};
@@ -396,9 +387,7 @@ export class NunjucksWorkflowRunner implements WorkflowRunner {
if (prevTaskState) {
const prevState = (
prevTaskState.state?.checkpoints as {
[key: string]: CheckpointState;
}
prevTaskState.state?.checkpoints as CheckpointState
)?.[key];
if (prevState && prevState.status === 'success') {
@@ -44,19 +44,30 @@ import { DefaultWorkspaceService, WorkspaceService } from './WorkspaceService';
import { readDuration } from './helper';
import { InternalTaskSecrets, TaskStore } from './types';
type TaskState = {
checkpoints: {
[key: string]:
| {
status: 'failed';
reason: string;
}
| {
status: 'success';
value: JsonValue;
};
};
type CheckpointStatus = 'failed' | 'success';
export type CheckpointSuccessState = {
status: Extract<CheckpointStatus, 'success'>;
value: JsonValue;
};
export type CheckpointFailedState = {
status: Extract<CheckpointStatus, 'failed'>;
reason: string;
};
export type CheckpointState = {
[key: string]: CheckpointSuccessState | CheckpointFailedState;
};
export type UpdateCheckpointOptions = {
key: string;
} & CheckpointState[keyof CheckpointState];
type TaskState = {
checkpoints: CheckpointState;
};
/**
* TaskManager
* @deprecated this type is deprecated, and there will be a new way to create Workers in the next major version.
@@ -152,25 +163,57 @@ export class TaskManager implements TaskContext {
return this.storage.getTaskState?.({ taskId: this.task.taskId });
}
async updateCheckpoint?(
options:
| {
key: string;
status: 'success';
value: JsonValue;
}
| {
key: string;
status: 'failed';
reason: string;
},
): Promise<void> {
const { key, ...value } = options;
if (this.task.state) {
(this.task.state as TaskState).checkpoints[key] = value;
} else {
this.task.state = { checkpoints: { [key]: value } };
/**
* Helper to safely access the checkpoints field from task state
* Ensures type safety when working with task state that might not match our structure
*/
private getCheckpointsFromState(state?: JsonObject): CheckpointState {
if (
state &&
'checkpoints' in state &&
typeof state.checkpoints === 'object'
) {
return state.checkpoints as CheckpointState;
}
return {};
}
async updateCheckpoint?(options: UpdateCheckpointOptions): Promise<void> {
const { key, status } = options;
// Extract appropriate state value based on status
let checkpointValue: CheckpointSuccessState | CheckpointFailedState;
switch (status) {
case 'success': {
const { value } = options;
checkpointValue = { status, value };
break;
}
case 'failed': {
const { reason } = options;
checkpointValue = { status, reason };
break;
}
default: {
// Using status as 'never' gives compile-time guarantee we've handled all cases
const exhaustiveCheck: never = status;
throw new Error(`Unexpected status: ${exhaustiveCheck}`);
}
}
if (this.task.state) {
const taskState: TaskState = {
checkpoints: this.getCheckpointsFromState(this.task.state),
};
// Update with the new checkpoint
taskState.checkpoints[key] = checkpointValue;
this.task.state = taskState;
} else {
this.task.state = { checkpoints: { [key]: checkpointValue } };
}
await this.storage.saveTaskState?.({
taskId: this.task.taskId,
state: this.task.state,
@@ -254,6 +297,9 @@ export interface CurrentClaimedTask {
secrets?: TaskSecrets;
/**
* The state of checkpoints of the task.
* This will be a JsonObject that may contain a `checkpoints` field
* with a structure matching the CheckpointState interface.
* @see CheckpointState
*/
state?: JsonObject;
/**