diff --git a/.changeset/petite-paths-remain.md b/.changeset/petite-paths-remain.md new file mode 100644 index 0000000000..a2bef50c80 --- /dev/null +++ b/.changeset/petite-paths-remain.md @@ -0,0 +1,7 @@ +--- +'@backstage/plugin-scaffolder-node-test-utils': patch +'@backstage/plugin-scaffolder-backend': patch +'@backstage/plugin-scaffolder-node': patch +--- + +An internal refactor which adds additional types to experimental checkpoints diff --git a/plugins/scaffolder-backend/report.api.md b/plugins/scaffolder-backend/report.api.md index 99be4a27f1..3823db15d6 100644 --- a/plugins/scaffolder-backend/report.api.md +++ b/plugins/scaffolder-backend/report.api.md @@ -37,7 +37,7 @@ import { TemplateEntityStepV1beta3 } from '@backstage/plugin-scaffolder-common'; import { TemplateFilter } from '@backstage/plugin-scaffolder-node'; import { TemplateGlobal } from '@backstage/plugin-scaffolder-node'; import { TemplateParametersV1beta3 } from '@backstage/plugin-scaffolder-common'; -import { UpdateCheckpointOptions } from '@backstage/plugin-scaffolder-node'; +import { UpdateTaskCheckpointOptions } from '@backstage/plugin-scaffolder-node'; import { UrlReaderService } from '@backstage/backend-plugin-api'; import { WorkspaceProvider } from '@backstage/plugin-scaffolder-node/alpha'; @@ -442,7 +442,7 @@ export class TaskManager implements TaskContext { // (undocumented) get spec(): TaskSpecV1beta3; // (undocumented) - updateCheckpoint?(options: UpdateCheckpointOptions): Promise; + updateCheckpoint?(options: UpdateTaskCheckpointOptions): Promise; } // @public @deprecated diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts index d6a420e54a..adc5152e4f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -50,7 +50,7 @@ import { import { createConditionAuthorizer } from '@backstage/plugin-permission-node'; import { actionExecutePermission } from '@backstage/plugin-scaffolder-common/alpha'; import { - CheckpointOptions, + CheckpointContext, TaskContext, TemplateAction, TemplateFilter, @@ -377,7 +377,7 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { logger: taskLogger, workspacePath, async checkpoint( - opts: CheckpointOptions, + opts: CheckpointContext, ) { const { key: checkpointKey, fn } = opts; const key = `v1.task.checkpoint.${step.id}.${checkpointKey}`; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index 9a872e7b3f..5e90a11d83 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -23,6 +23,7 @@ import { import { Config } from '@backstage/config'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; import { + CheckpointState, SerializedTask, SerializedTaskEvent, TaskBroker, @@ -31,6 +32,7 @@ import { TaskContext, TaskSecrets, TaskStatus, + UpdateTaskCheckpointOptions, } from '@backstage/plugin-scaffolder-node'; import { WorkspaceProvider } from '@backstage/plugin-scaffolder-node/alpha'; import { JsonObject, Observable, createDeferred } from '@backstage/types'; @@ -38,17 +40,10 @@ import ObservableImpl from 'zen-observable'; import { DefaultWorkspaceService, WorkspaceService } from './WorkspaceService'; import { readDuration } from './helper'; import { InternalTaskSecrets, TaskStore } from './types'; -import { - CheckpointState, - CheckpointSuccessState, - CheckpointFailedState, - UpdateCheckpointOptions, -} from '@backstage/plugin-scaffolder-node'; 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. @@ -144,57 +139,14 @@ export class TaskManager implements TaskContext { return this.storage.getTaskState?.({ taskId: this.task.taskId }); } - /** - * 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 { - 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}`); - } - } + async updateCheckpoint?(options: UpdateTaskCheckpointOptions): Promise { + const { key, ...value } = options; 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; + (this.task.state as TaskState).checkpoints[key] = value; } else { - this.task.state = { checkpoints: { [key]: checkpointValue } }; + this.task.state = { checkpoints: { [key]: value } }; } - await this.storage.saveTaskState?.({ taskId: this.task.taskId, state: this.task.state, @@ -278,9 +230,6 @@ 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; /** diff --git a/plugins/scaffolder-node-test-utils/src/actions/mockActionContext.ts b/plugins/scaffolder-node-test-utils/src/actions/mockActionContext.ts index 5f8844d13d..d364d4dc1e 100644 --- a/plugins/scaffolder-node-test-utils/src/actions/mockActionContext.ts +++ b/plugins/scaffolder-node-test-utils/src/actions/mockActionContext.ts @@ -23,7 +23,7 @@ import { import { JsonObject, JsonValue } from '@backstage/types'; import { ActionContext, - CheckpointOptions, + CheckpointContext, } from '@backstage/plugin-scaffolder-node'; import { loggerToWinstonLogger } from './loggerToWinstonLogger'; @@ -48,7 +48,7 @@ export function createMockActionContext< createTemporaryDirectory: jest.fn(), input: {} as TActionInput, async checkpoint( - opts: CheckpointOptions, + opts: CheckpointContext, ): Promise { return opts.fn(); }, diff --git a/plugins/scaffolder-node/src/actions/types.ts b/plugins/scaffolder-node/src/actions/types.ts index e7e678450d..8104470dd8 100644 --- a/plugins/scaffolder-node/src/actions/types.ts +++ b/plugins/scaffolder-node/src/actions/types.ts @@ -23,7 +23,7 @@ import { BackstageCredentials, LoggerService, } from '@backstage/backend-plugin-api'; -import { CheckpointOptions } from '../checkpoints'; +import { CheckpointContext } from '../checkpoints'; /** * ActionContext is passed into scaffolder actions. @@ -39,7 +39,7 @@ export type ActionContext< workspacePath: string; input: TActionInput; checkpoint( - opts: CheckpointOptions, + opts: CheckpointContext, ): Promise; output( name: keyof TActionOutput, diff --git a/plugins/scaffolder-node/src/checkpoints/types.ts b/plugins/scaffolder-node/src/checkpoints/types.ts index eac3213075..eecf769f5a 100644 --- a/plugins/scaffolder-node/src/checkpoints/types.ts +++ b/plugins/scaffolder-node/src/checkpoints/types.ts @@ -1,5 +1,5 @@ /* - * Copyright 2024 The Backstage Authors + * Copyright 2025 The Backstage Authors * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -13,7 +13,6 @@ * See the License for the specific language governing permissions and * limitations under the License. */ - import { JsonValue } from '@backstage/types'; /** @@ -28,9 +27,9 @@ export type CheckpointStatus = 'failed' | 'success'; * * @public */ -export type CheckpointSuccessState = { - status: Extract; - value: JsonValue; +export type CheckpointSuccessState = { + status: 'success'; + value: T; }; /** @@ -39,34 +38,34 @@ export type CheckpointSuccessState = { * @public */ export type CheckpointFailedState = { - status: Extract; + status: 'failed'; reason: string; }; +/** + * Represents the union of all possible checkpoint state values. + * + * @public + */ +export type CheckpointStateValue = + | CheckpointSuccessState + | CheckpointFailedState; + /** * A map of checkpoint keys to their states. * * @public */ export type CheckpointState = { - [key: string]: CheckpointSuccessState | CheckpointFailedState; + [key: string]: CheckpointStateValue; }; /** - * Options for updating a checkpoint in a task. + * Context for checkpoint function invocation. * * @public */ -export type UpdateCheckpointOptions = { - key: string; -} & CheckpointState[keyof CheckpointState]; - -/** - * Options for checkpoint function invocation. - * - * @public - */ -export type CheckpointOptions = { +export type CheckpointContext = { /** * Unique key for the checkpoint */ diff --git a/plugins/scaffolder-node/src/tasks/index.ts b/plugins/scaffolder-node/src/tasks/index.ts index 930de95237..e047131d2b 100644 --- a/plugins/scaffolder-node/src/tasks/index.ts +++ b/plugins/scaffolder-node/src/tasks/index.ts @@ -25,4 +25,5 @@ export type { TaskContext, TaskEventType, TaskStatus, + UpdateTaskCheckpointOptions, } from './types'; diff --git a/plugins/scaffolder-node/src/tasks/types.ts b/plugins/scaffolder-node/src/tasks/types.ts index cc9023db8b..e3b64fea15 100644 --- a/plugins/scaffolder-node/src/tasks/types.ts +++ b/plugins/scaffolder-node/src/tasks/types.ts @@ -17,7 +17,7 @@ import { BackstageCredentials } from '@backstage/backend-plugin-api'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; import { JsonObject, Observable } from '@backstage/types'; -import { UpdateCheckpointOptions } from '../checkpoints'; +import { CheckpointStateValue } from '../checkpoints'; /** * TaskSecrets @@ -105,6 +105,15 @@ export type TaskBrokerDispatchOptions = { createdBy?: string; }; +/** + * Options for updating a checkpoint in a task. + * + * @public + */ +export type UpdateTaskCheckpointOptions = { + key: string; +} & CheckpointStateValue; + /** * Task * @@ -130,7 +139,7 @@ export interface TaskContext { | undefined >; - updateCheckpoint?(options: UpdateCheckpointOptions): Promise; + updateCheckpoint?(options: UpdateTaskCheckpointOptions): Promise; serializeWorkspace?(options: { path: string }): Promise;