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 1f2a17a384..7ece0f84ea 100644 --- a/plugins/scaffolder-backend/report.api.md +++ b/plugins/scaffolder-backend/report.api.md @@ -14,7 +14,6 @@ import { Duration } from 'luxon'; import { EventsService } from '@backstage/plugin-events-node'; import { HumanDuration } from '@backstage/types'; import { JsonObject } from '@backstage/types'; -import { JsonValue } from '@backstage/types'; import { Knex } from 'knex'; import { LoggerService } from '@backstage/backend-plugin-api'; import { PermissionEvaluator } from '@backstage/plugin-permission-common'; @@ -38,6 +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 { UpdateTaskCheckpointOptions } from '@backstage/plugin-scaffolder-node/alpha'; import { UrlReaderService } from '@backstage/backend-plugin-api'; import { WorkspaceProvider } from '@backstage/plugin-scaffolder-node/alpha'; @@ -442,19 +442,7 @@ export class TaskManager implements TaskContext { // (undocumented) get spec(): TaskSpecV1beta3; // (undocumented) - updateCheckpoint?( - options: - | { - key: string; - status: 'success'; - value: JsonValue; - } - | { - key: string; - status: 'failed'; - reason: string; - }, - ): 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 7802a7ab8d..1542db8438 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts @@ -60,6 +60,10 @@ import { scaffolderActionRules } from '../../service/rules'; import { createCounterMetric, createHistogramMetric } from '../../util/metrics'; import { BackstageLoggerTransport, WinstonLogger } from './logger'; import { convertFiltersToRecord } from '../../util/templating'; +import { + CheckpointState, + CheckpointContext, +} from '@backstage/plugin-scaffolder-node/alpha'; type NunjucksWorkflowRunnerOptions = { workingDirectory: string; @@ -91,16 +95,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'; }; @@ -384,10 +378,9 @@ export class NunjucksWorkflowRunner implements WorkflowRunner { secrets: task.secrets ?? {}, logger: taskLogger, workspacePath, - async checkpoint(opts: { - key?: string; - fn: () => Promise | T; - }) { + async checkpoint( + opts: CheckpointContext, + ) { const { key: checkpointKey, fn } = opts; const key = `v1.task.checkpoint.${step.id}.${checkpointKey}`; @@ -396,9 +389,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') { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts index 86f9f7fcf7..c88a1f687c 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/StorageTaskBroker.ts @@ -32,30 +32,19 @@ import { TaskSecrets, TaskStatus, } from '@backstage/plugin-scaffolder-node'; -import { WorkspaceProvider } from '@backstage/plugin-scaffolder-node/alpha'; import { - JsonObject, - JsonValue, - Observable, - createDeferred, -} from '@backstage/types'; + CheckpointState, + WorkspaceProvider, + UpdateTaskCheckpointOptions, +} from '@backstage/plugin-scaffolder-node/alpha'; +import { JsonObject, Observable, createDeferred } from '@backstage/types'; import ObservableImpl from 'zen-observable'; 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; - }; - }; + checkpoints: CheckpointState; }; /** * TaskManager @@ -152,20 +141,9 @@ 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 { + async updateCheckpoint?(options: UpdateTaskCheckpointOptions): Promise { const { key, ...value } = options; + if (this.task.state) { (this.task.state as TaskState).checkpoints[key] = value; } else { diff --git a/plugins/scaffolder-node-test-utils/src/actions/mockActionContext.ts b/plugins/scaffolder-node-test-utils/src/actions/mockActionContext.ts index a6703a12d1..e2f07f2320 100644 --- a/plugins/scaffolder-node-test-utils/src/actions/mockActionContext.ts +++ b/plugins/scaffolder-node-test-utils/src/actions/mockActionContext.ts @@ -22,6 +22,7 @@ import { } from '@backstage/backend-test-utils'; import { JsonObject, JsonValue } from '@backstage/types'; import { ActionContext } from '@backstage/plugin-scaffolder-node'; +import { CheckpointContext } from '@backstage/plugin-scaffolder-node/alpha'; import { loggerToWinstonLogger } from './loggerToWinstonLogger'; /** @@ -44,10 +45,9 @@ export function createMockActionContext< output: jest.fn(), createTemporaryDirectory: jest.fn(), input: {} as TActionInput, - async checkpoint(opts: { - key: string; - fn: () => Promise | T; - }): Promise { + async checkpoint( + opts: CheckpointContext, + ): Promise { return opts.fn(); }, getInitiatorCredentials: () => Promise.resolve(credentials), diff --git a/plugins/scaffolder-node/report-alpha.api.md b/plugins/scaffolder-node/report-alpha.api.md index a96f312d2d..09541fbc56 100644 --- a/plugins/scaffolder-node/report-alpha.api.md +++ b/plugins/scaffolder-node/report-alpha.api.md @@ -27,6 +27,31 @@ export type AutocompleteHandler = ({ }[]; }>; +// @alpha +export type CheckpointContext = { + key: string; + fn: () => Promise | T; +}; + +// @alpha +export type CheckpointState = { + [key: string]: CheckpointStateValue; +}; + +// @alpha +export type CheckpointStateValue = + | { + status: 'failed'; + reason: string; + } + | { + status: 'success'; + value: T; + }; + +// @alpha +export type CheckpointStatus = 'failed' | 'success'; + // @alpha (undocumented) export type CreatedTemplateFilter< TFunctionArgs extends [z.ZodTypeAny, ...z.ZodTypeAny[]], @@ -187,6 +212,11 @@ export type TemplateGlobalFunctionExample = { notes?: string; }; +// @alpha +export type UpdateTaskCheckpointOptions = { + key: string; +} & CheckpointStateValue; + // @alpha export interface WorkspaceProvider { // (undocumented) diff --git a/plugins/scaffolder-node/report.api.md b/plugins/scaffolder-node/report.api.md index a23a9137b1..bceb9980b6 100644 --- a/plugins/scaffolder-node/report.api.md +++ b/plugins/scaffolder-node/report.api.md @@ -4,6 +4,7 @@ ```ts import { BackstageCredentials } from '@backstage/backend-plugin-api'; +import { CheckpointContext } from '@backstage/plugin-scaffolder-node/alpha'; import { Expand } from '@backstage/types'; import { JsonObject } from '@backstage/types'; import { JsonValue } from '@backstage/types'; @@ -15,6 +16,7 @@ import { ScmIntegrations } from '@backstage/integration'; import { SpawnOptionsWithoutStdio } from 'child_process'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; import { TemplateInfo } from '@backstage/plugin-scaffolder-common'; +import { UpdateTaskCheckpointOptions } from '@backstage/plugin-scaffolder-node/alpha'; import { UrlReaderService } from '@backstage/backend-plugin-api'; import { UserEntity } from '@backstage/catalog-model'; import { Writable } from 'stream'; @@ -30,10 +32,9 @@ export type ActionContext< secrets?: TaskSecrets; workspacePath: string; input: TActionInput; - checkpoint(opts: { - key: string; - fn: () => Promise | T; - }): Promise; + checkpoint( + opts: CheckpointContext, + ): Promise; output( name: keyof TActionOutput, value: TActionOutput[keyof TActionOutput], @@ -446,19 +447,7 @@ export interface TaskContext { // (undocumented) taskId?: string; // (undocumented) - updateCheckpoint?( - options: - | { - key: string; - status: 'success'; - value: JsonValue; - } - | { - key: string; - status: 'failed'; - reason: string; - }, - ): Promise; + updateCheckpoint?(options: UpdateTaskCheckpointOptions): Promise; } // @public diff --git a/plugins/scaffolder-node/src/actions/types.ts b/plugins/scaffolder-node/src/actions/types.ts index f13d256332..0162d96d95 100644 --- a/plugins/scaffolder-node/src/actions/types.ts +++ b/plugins/scaffolder-node/src/actions/types.ts @@ -23,6 +23,8 @@ import { BackstageCredentials, LoggerService, } from '@backstage/backend-plugin-api'; +import { CheckpointContext } from '@backstage/plugin-scaffolder-node/alpha'; + /** * ActionContext is passed into scaffolder actions. * @public @@ -36,10 +38,9 @@ export type ActionContext< secrets?: TaskSecrets; workspacePath: string; input: TActionInput; - checkpoint(opts: { - key: string; - fn: () => Promise | T; - }): Promise; + checkpoint( + opts: CheckpointContext, + ): Promise; output( name: keyof TActionOutput, value: TActionOutput[keyof TActionOutput], diff --git a/plugins/scaffolder-node/src/alpha/checkpoints/index.ts b/plugins/scaffolder-node/src/alpha/checkpoints/index.ts new file mode 100644 index 0000000000..16c936b235 --- /dev/null +++ b/plugins/scaffolder-node/src/alpha/checkpoints/index.ts @@ -0,0 +1,16 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +export * from './types'; diff --git a/plugins/scaffolder-node/src/alpha/checkpoints/types.ts b/plugins/scaffolder-node/src/alpha/checkpoints/types.ts new file mode 100644 index 0000000000..5bd1eb5cd3 --- /dev/null +++ b/plugins/scaffolder-node/src/alpha/checkpoints/types.ts @@ -0,0 +1,57 @@ +/* + * 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. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +import { JsonValue } from '@backstage/types'; + +/** + * The status of a checkpoint, indicating whether it succeeded or failed. + * + * @alpha + */ +export type CheckpointStatus = 'failed' | 'success'; + +/** + * Represents the union of all possible checkpoint state values. + * + * @alpha + */ +export type CheckpointStateValue = + | { status: 'failed'; reason: string } + | { status: 'success'; value: T }; + +/** + * A map of checkpoint keys to their states. + * + * @alpha + */ +export type CheckpointState = { + [key: string]: CheckpointStateValue; +}; + +/** + * Context for checkpoint function invocation. + * + * @alpha + */ +export type CheckpointContext = { + /** + * Unique key for the checkpoint + */ + key: string; + /** + * Function to execute for the checkpoint + */ + fn: () => Promise | T; +}; diff --git a/plugins/scaffolder-node/src/alpha/index.ts b/plugins/scaffolder-node/src/alpha/index.ts index 9df968a52d..30f95f116a 100644 --- a/plugins/scaffolder-node/src/alpha/index.ts +++ b/plugins/scaffolder-node/src/alpha/index.ts @@ -28,6 +28,7 @@ export * from '../tasks/alpha'; export * from './filters'; export * from './globals'; export * from './types'; +export * from './checkpoints'; /** * Extension point for managing scaffolder actions. diff --git a/plugins/scaffolder-node/src/tasks/alpha.ts b/plugins/scaffolder-node/src/tasks/alpha.ts index fb6b09026c..4354a798cf 100644 --- a/plugins/scaffolder-node/src/tasks/alpha.ts +++ b/plugins/scaffolder-node/src/tasks/alpha.ts @@ -1,3 +1,5 @@ +import { CheckpointStateValue } from '../alpha'; + /* * Copyright 2024 The Backstage Authors * @@ -14,3 +16,12 @@ * limitations under the License. */ export * from './serializer'; + +/** + * Options for updating a checkpoint in a task. + * + * @alpha + */ +export type UpdateTaskCheckpointOptions = { + key: string; +} & CheckpointStateValue; diff --git a/plugins/scaffolder-node/src/tasks/types.ts b/plugins/scaffolder-node/src/tasks/types.ts index d8cfd24530..d2403f205a 100644 --- a/plugins/scaffolder-node/src/tasks/types.ts +++ b/plugins/scaffolder-node/src/tasks/types.ts @@ -16,7 +16,8 @@ import { BackstageCredentials } from '@backstage/backend-plugin-api'; import { TaskSpec } from '@backstage/plugin-scaffolder-common'; -import { JsonObject, JsonValue, Observable } from '@backstage/types'; +import { JsonObject, Observable } from '@backstage/types'; +import { UpdateTaskCheckpointOptions } from '@backstage/plugin-scaffolder-node/alpha'; /** * TaskSecrets @@ -129,19 +130,7 @@ export interface TaskContext { | undefined >; - updateCheckpoint?( - options: - | { - key: string; - status: 'success'; - value: JsonValue; - } - | { - key: string; - status: 'failed'; - reason: string; - }, - ): Promise; + updateCheckpoint?(options: UpdateTaskCheckpointOptions): Promise; serializeWorkspace?(options: { path: string }): Promise;