Merge pull request #26607 from acierto/scaffolder-retry

Make it possible to manually retry the scaffolder template from the failed step (based on checkpoints)
This commit is contained in:
Ben Lambert
2024-09-24 10:34:48 +02:00
committed by GitHub
19 changed files with 316 additions and 87 deletions
+8
View File
@@ -0,0 +1,8 @@
---
'@backstage/plugin-scaffolder-backend': patch
'@backstage/plugin-scaffolder-react': patch
'@backstage/plugin-scaffolder-node': patch
'@backstage/plugin-scaffolder': patch
---
Make it possible to manually retry the scaffolder template from the step it failed
+26 -21
View File
@@ -398,7 +398,6 @@ export interface CurrentClaimedTask {
spec: TaskSpec;
state?: JsonObject;
taskId: string;
// (undocumented)
workspace?: Promise<Buffer>;
}
@@ -488,6 +487,8 @@ export class DatabaseTaskStore implements TaskStore {
targetPath: string;
}): Promise<void>;
// (undocumented)
retryTask?(options: { taskId: string }): Promise<void>;
// (undocumented)
saveTaskState(options: { taskId: string; state?: JsonObject }): Promise<void>;
// (undocumented)
serializeWorkspace(options: { path: string; taskId: string }): Promise<void>;
@@ -740,6 +741,8 @@ export interface TaskStore {
targetPath: string;
}): Promise<void>;
// (undocumented)
retryTask?(options: { taskId: string }): Promise<void>;
// (undocumented)
saveTaskState?(options: {
taskId: string;
state?: JsonObject;
@@ -776,6 +779,7 @@ export type TaskStoreEmitOptions<TBody = JsonObject> = {
// @public
export type TaskStoreListEventsOptions = {
isTaskRecoverable?: boolean;
taskId: string;
after?: number | undefined;
};
@@ -884,7 +888,8 @@ export type TemplatePermissionRuleInput<
// src/scaffolder/tasks/DatabaseTaskStore.d.ts:104:5 - (ae-undocumented) Missing documentation for "cleanWorkspace".
// src/scaffolder/tasks/DatabaseTaskStore.d.ts:107:5 - (ae-undocumented) Missing documentation for "serializeWorkspace".
// src/scaffolder/tasks/DatabaseTaskStore.d.ts:111:5 - (ae-undocumented) Missing documentation for "cancelTask".
// src/scaffolder/tasks/DatabaseTaskStore.d.ts:114:5 - (ae-undocumented) Missing documentation for "recoverTasks".
// src/scaffolder/tasks/DatabaseTaskStore.d.ts:114:5 - (ae-undocumented) Missing documentation for "retryTask".
// src/scaffolder/tasks/DatabaseTaskStore.d.ts:117:5 - (ae-undocumented) Missing documentation for "recoverTasks".
// src/scaffolder/tasks/StorageTaskBroker.d.ts:24:5 - (ae-undocumented) Missing documentation for "create".
// src/scaffolder/tasks/StorageTaskBroker.d.ts:26:5 - (ae-undocumented) Missing documentation for "spec".
// src/scaffolder/tasks/StorageTaskBroker.d.ts:27:5 - (ae-undocumented) Missing documentation for "cancelSignal".
@@ -900,31 +905,31 @@ export type TemplatePermissionRuleInput<
// src/scaffolder/tasks/StorageTaskBroker.d.ts:52:5 - (ae-undocumented) Missing documentation for "cleanWorkspace".
// src/scaffolder/tasks/StorageTaskBroker.d.ts:53:5 - (ae-undocumented) Missing documentation for "complete".
// src/scaffolder/tasks/StorageTaskBroker.d.ts:55:5 - (ae-undocumented) Missing documentation for "getInitiatorCredentials".
// src/scaffolder/tasks/StorageTaskBroker.d.ts:83:5 - (ae-undocumented) Missing documentation for "workspace".
// src/scaffolder/tasks/TaskWorker.d.ts:60:5 - (ae-undocumented) Missing documentation for "create".
// src/scaffolder/tasks/TaskWorker.d.ts:61:5 - (ae-undocumented) Missing documentation for "recoverTasks".
// src/scaffolder/tasks/TaskWorker.d.ts:62:5 - (ae-undocumented) Missing documentation for "start".
// src/scaffolder/tasks/TaskWorker.d.ts:63:5 - (ae-undocumented) Missing documentation for "stop".
// src/scaffolder/tasks/TaskWorker.d.ts:64:5 - (ae-undocumented) Missing documentation for "onReadyToClaimTask".
// src/scaffolder/tasks/TaskWorker.d.ts:65:5 - (ae-undocumented) Missing documentation for "runOneTask".
// src/scaffolder/tasks/types.d.ts:123:5 - (ae-undocumented) Missing documentation for "cancelTask".
// src/scaffolder/tasks/types.d.ts:124:5 - (ae-undocumented) Missing documentation for "createTask".
// src/scaffolder/tasks/types.d.ts:125:5 - (ae-undocumented) Missing documentation for "recoverTasks".
// src/scaffolder/tasks/types.d.ts:128:5 - (ae-undocumented) Missing documentation for "getTask".
// src/scaffolder/tasks/types.d.ts:129:5 - (ae-undocumented) Missing documentation for "claimTask".
// src/scaffolder/tasks/types.d.ts:130:5 - (ae-undocumented) Missing documentation for "completeTask".
// src/scaffolder/tasks/types.d.ts:135:5 - (ae-undocumented) Missing documentation for "heartbeatTask".
// src/scaffolder/tasks/types.d.ts:136:5 - (ae-undocumented) Missing documentation for "listStaleTasks".
// src/scaffolder/tasks/types.d.ts:143:5 - (ae-undocumented) Missing documentation for "list".
// src/scaffolder/tasks/types.d.ts:163:5 - (ae-undocumented) Missing documentation for "list".
// src/scaffolder/tasks/types.d.ts:182:5 - (ae-undocumented) Missing documentation for "emitLogEvent".
// src/scaffolder/tasks/types.d.ts:183:5 - (ae-undocumented) Missing documentation for "getTaskState".
// src/scaffolder/tasks/types.d.ts:188:5 - (ae-undocumented) Missing documentation for "saveTaskState".
// src/scaffolder/tasks/types.d.ts:192:5 - (ae-undocumented) Missing documentation for "listEvents".
// src/scaffolder/tasks/types.d.ts:195:5 - (ae-undocumented) Missing documentation for "shutdownTask".
// src/scaffolder/tasks/types.d.ts:196:5 - (ae-undocumented) Missing documentation for "rehydrateWorkspace".
// src/scaffolder/tasks/types.d.ts:200:5 - (ae-undocumented) Missing documentation for "cleanWorkspace".
// src/scaffolder/tasks/types.d.ts:203:5 - (ae-undocumented) Missing documentation for "serializeWorkspace".
// src/scaffolder/tasks/types.d.ts:124:5 - (ae-undocumented) Missing documentation for "cancelTask".
// src/scaffolder/tasks/types.d.ts:125:5 - (ae-undocumented) Missing documentation for "createTask".
// src/scaffolder/tasks/types.d.ts:126:5 - (ae-undocumented) Missing documentation for "retryTask".
// src/scaffolder/tasks/types.d.ts:129:5 - (ae-undocumented) Missing documentation for "recoverTasks".
// src/scaffolder/tasks/types.d.ts:132:5 - (ae-undocumented) Missing documentation for "getTask".
// src/scaffolder/tasks/types.d.ts:133:5 - (ae-undocumented) Missing documentation for "claimTask".
// src/scaffolder/tasks/types.d.ts:134:5 - (ae-undocumented) Missing documentation for "completeTask".
// src/scaffolder/tasks/types.d.ts:139:5 - (ae-undocumented) Missing documentation for "heartbeatTask".
// src/scaffolder/tasks/types.d.ts:140:5 - (ae-undocumented) Missing documentation for "listStaleTasks".
// src/scaffolder/tasks/types.d.ts:147:5 - (ae-undocumented) Missing documentation for "list".
// src/scaffolder/tasks/types.d.ts:167:5 - (ae-undocumented) Missing documentation for "list".
// src/scaffolder/tasks/types.d.ts:186:5 - (ae-undocumented) Missing documentation for "emitLogEvent".
// src/scaffolder/tasks/types.d.ts:187:5 - (ae-undocumented) Missing documentation for "getTaskState".
// src/scaffolder/tasks/types.d.ts:192:5 - (ae-undocumented) Missing documentation for "saveTaskState".
// src/scaffolder/tasks/types.d.ts:196:5 - (ae-undocumented) Missing documentation for "listEvents".
// src/scaffolder/tasks/types.d.ts:199:5 - (ae-undocumented) Missing documentation for "shutdownTask".
// src/scaffolder/tasks/types.d.ts:200:5 - (ae-undocumented) Missing documentation for "rehydrateWorkspace".
// src/scaffolder/tasks/types.d.ts:204:5 - (ae-undocumented) Missing documentation for "cleanWorkspace".
// src/scaffolder/tasks/types.d.ts:207:5 - (ae-undocumented) Missing documentation for "serializeWorkspace".
// src/service/router.d.ts:17:1 - (ae-undocumented) Missing documentation for "TemplatePermissionRuleInput".
// src/service/router.d.ts:22:1 - (ae-undocumented) Missing documentation for "ActionPermissionRuleInput".
// src/service/router.d.ts:30:5 - (ae-undocumented) Missing documentation for "logger".
@@ -16,7 +16,7 @@
import { DatabaseManager } from '@backstage/backend-common';
import { ConfigReader } from '@backstage/config';
import { DatabaseTaskStore } from './DatabaseTaskStore';
import { DatabaseTaskStore, RawDbTaskEventRow } from './DatabaseTaskStore';
import { TaskSpec } from '@backstage/plugin-scaffolder-common';
import { ConflictError } from '@backstage/errors';
import { createMockDirectory } from '@backstage/backend-test-utils';
@@ -267,6 +267,48 @@ describe('DatabaseTaskStore', () => {
expect(event.type).toBe('log');
});
it('should be able to retied cancelled recoverable task', async () => {
const { store, manager } = await createStore();
const client = await manager.getClient();
const { taskId } = await store.createTask({
spec: {
EXPERIMENTAL_recovery: { EXPERIMENTAL_strategy: 'startOver' },
} as TaskSpec,
createdBy: 'me#too',
});
await store.completeTask({ taskId, status: 'cancelled', eventBody: {} });
await store.retryTask?.({ taskId });
const taskAfterRetry = await store.getTask(taskId);
expect(taskAfterRetry.status).toBe('open');
expect(
await client<RawDbTaskEventRow>('task_events')
.where({
task_id: taskId,
event_type: 'recovered',
})
.select(['body', 'event_type', 'task_id']),
).toEqual([
{
body: JSON.stringify({ recoverStrategy: 'startOver' }),
event_type: 'recovered',
task_id: taskId,
},
]);
expect(
await client<RawDbTaskEventRow>('task_events')
.where({
task_id: taskId,
})
.andWhere(q => q.whereIn('event_type', ['cancelled', 'completion']))
.select(['body', 'event_type', 'task_id']),
).toEqual([]);
});
it('should complete the task', async () => {
const { store } = await createStore();
const { taskId } = await store.createTask({
@@ -484,7 +484,7 @@ export class DatabaseTaskStore implements TaskStore {
async listEvents(
options: TaskStoreListEventsOptions,
): Promise<{ events: SerializedTaskEvent[] }> {
const { taskId, after } = options;
const { isTaskRecoverable, taskId, after } = options;
const rawEvents = await this.db<RawDbTaskEventRow>('task_events')
.where({
task_id: taskId,
@@ -502,6 +502,7 @@ export class DatabaseTaskStore implements TaskStore {
const body = JSON.parse(event.body) as JsonObject;
return {
id: Number(event.id),
isTaskRecoverable,
taskId,
body,
type: event.event_type,
@@ -602,6 +603,44 @@ export class DatabaseTaskStore implements TaskStore {
});
}
async retryTask?(options: { taskId: string }): Promise<void> {
await this.db.transaction(async tx => {
const result = await tx<RawDbTaskRow>('tasks')
.where('id', options.taskId)
.update(
{
status: 'open',
last_heartbeat_at: this.db.fn.now(),
},
['id', 'spec'],
);
for (const { id, spec } of result) {
const taskSpec = JSON.parse(spec as string) as TaskSpec;
/**
* Once task is picked up, all event types are replayed.
* We have to remove cancelled or completion event_type as these are as actions for frontend to perform.
* In contrary, we send 'recovered' event_type to reset the state on the frontend side.
*
*/
await tx<RawDbTaskEventRow>('task_events')
.where('task_id', id)
.andWhere(q => q.whereIn('event_type', ['cancelled', 'completion']))
.del();
await tx<RawDbTaskEventRow>('task_events').insert({
task_id: id,
event_type: 'recovered',
body: JSON.stringify({
recoverStrategy:
taskSpec.EXPERIMENTAL_recovery?.EXPERIMENTAL_strategy ?? 'none',
}),
});
}
});
}
async recoverTasks(
options: TaskStoreRecoverTaskOptions,
): Promise<{ ids: string[] }> {
@@ -254,7 +254,9 @@ export interface CurrentClaimedTask {
* The creator of the task.
*/
createdBy?: string;
/**
* The workspace of the task.
*/
workspace?: Promise<Buffer>;
}
@@ -317,7 +319,7 @@ export class StorageTaskBroker implements TaskBroker {
shouldUnsubscribe = true;
}
if (event.type === 'completion') {
if (event.type === 'completion' && !event.isTaskRecoverable) {
shouldUnsubscribe = true;
}
}
@@ -416,8 +418,17 @@ export class StorageTaskBroker implements TaskBroker {
let cancelled = false;
(async () => {
const task = await this.storage.getTask(taskId);
const isTaskRecoverable =
task.spec.EXPERIMENTAL_recovery?.EXPERIMENTAL_strategy ===
'startOver';
while (!cancelled) {
const result = await this.storage.listEvents({ taskId, after });
const result = await this.storage.listEvents({
isTaskRecoverable,
taskId,
after,
});
const { events } = result;
if (events.length) {
after = events[events.length - 1].id;
@@ -485,4 +496,9 @@ export class StorageTaskBroker implements TaskBroker {
},
});
}
async retry?(taskId: string): Promise<void> {
await this.storage.retryTask?.({ taskId });
this.signalDispatch();
}
}
@@ -19,6 +19,7 @@ import { CurrentClaimedTask } from './StorageTaskBroker';
import { WorkspaceProvider } from '@backstage/plugin-scaffolder-node/alpha';
import { DatabaseWorkspaceProvider } from './DatabaseWorkspaceProvider';
import { TaskStore } from './types';
import fs from 'fs-extra';
export interface WorkspaceService {
serializeWorkspace(options: { path: string }): Promise<void>;
@@ -74,6 +75,7 @@ export class DefaultWorkspaceService implements WorkspaceService {
targetPath: string;
}): Promise<void> {
if (this.isWorkspaceSerializationEnabled()) {
await fs.mkdirp(options.targetPath);
await this.workspaceProvider.rehydrateWorkspace(options);
}
}
@@ -119,6 +119,7 @@ export type TaskStoreEmitOptions<TBody = JsonObject> = {
* @public
*/
export type TaskStoreListEventsOptions = {
isTaskRecoverable?: boolean;
taskId: string;
after?: number | undefined;
};
@@ -170,6 +171,8 @@ export interface TaskStore {
options: TaskStoreCreateTaskOptions,
): Promise<TaskStoreCreateTaskResult>;
retryTask?(options: { taskId: string }): Promise<void>;
recoverTasks?(
options: TaskStoreRecoverTaskOptions,
): Promise<{ ids: string[] }>;
@@ -652,6 +652,19 @@ export async function createRouter(
await taskBroker.cancel?.(taskId);
res.status(200).json({ status: 'cancelled' });
})
.post('/v2/tasks/:taskId/retry', async (req, res) => {
const credentials = await httpAuth.credentials(req);
// Requires both read and cancel permissions
await checkPermission({
credentials,
permissions: [taskCreatePermission, taskReadPermission],
permissionService: permissions,
});
const { taskId } = req.params;
await taskBroker.retry?.(taskId);
res.status(201).json({ id: taskId });
})
.get('/v2/tasks/:taskId/eventstream', async (req, res) => {
const credentials = await httpAuth.credentials(req);
await checkPermission({
@@ -687,7 +700,7 @@ export async function createRouter(
res.write(
`event: ${event.type}\ndata: ${JSON.stringify(event)}\n\n`,
);
if (event.type === 'completion') {
if (event.type === 'completion' && !event.isTaskRecoverable) {
shouldUnsubscribe = true;
}
}
+28 -24
View File
@@ -292,6 +292,7 @@ export type SerializedTask = {
// @public
export type SerializedTaskEvent = {
id: number;
isTaskRecoverable?: boolean;
taskId: string;
body: JsonObject;
type: TaskEventType;
@@ -340,6 +341,8 @@ export interface TaskBroker {
// (undocumented)
recoverTasks?(): Promise<void>;
// (undocumented)
retry?(taskId: string): Promise<void>;
// (undocumented)
vacuumTasks(options: { timeoutS: number }): Promise<void>;
}
@@ -498,30 +501,31 @@ export type TemplateGlobal =
// src/files/types.d.ts:7:5 - (ae-undocumented) Missing documentation for "content".
// src/files/types.d.ts:8:5 - (ae-undocumented) Missing documentation for "executable".
// src/files/types.d.ts:9:5 - (ae-undocumented) Missing documentation for "symlink".
// src/tasks/types.d.ts:82:5 - (ae-undocumented) Missing documentation for "cancelSignal".
// src/tasks/types.d.ts:83:5 - (ae-undocumented) Missing documentation for "spec".
// src/tasks/types.d.ts:84:5 - (ae-undocumented) Missing documentation for "secrets".
// src/tasks/types.d.ts:85:5 - (ae-undocumented) Missing documentation for "createdBy".
// src/tasks/types.d.ts:86:5 - (ae-undocumented) Missing documentation for "done".
// src/tasks/types.d.ts:87:5 - (ae-undocumented) Missing documentation for "isDryRun".
// src/tasks/types.d.ts:88:5 - (ae-undocumented) Missing documentation for "complete".
// src/tasks/types.d.ts:89:5 - (ae-undocumented) Missing documentation for "emitLog".
// src/tasks/types.d.ts:90:5 - (ae-undocumented) Missing documentation for "getTaskState".
// src/tasks/types.d.ts:93:5 - (ae-undocumented) Missing documentation for "updateCheckpoint".
// src/tasks/types.d.ts:102:5 - (ae-undocumented) Missing documentation for "serializeWorkspace".
// src/tasks/types.d.ts:105:5 - (ae-undocumented) Missing documentation for "cleanWorkspace".
// src/tasks/types.d.ts:106:5 - (ae-undocumented) Missing documentation for "rehydrateWorkspace".
// src/tasks/types.d.ts:110:5 - (ae-undocumented) Missing documentation for "getWorkspaceName".
// src/tasks/types.d.ts:111:5 - (ae-undocumented) Missing documentation for "getInitiatorCredentials".
// src/tasks/types.d.ts:119:5 - (ae-undocumented) Missing documentation for "cancel".
// src/tasks/types.d.ts:120:5 - (ae-undocumented) Missing documentation for "claim".
// src/tasks/types.d.ts:121:5 - (ae-undocumented) Missing documentation for "recoverTasks".
// src/tasks/types.d.ts:122:5 - (ae-undocumented) Missing documentation for "dispatch".
// src/tasks/types.d.ts:123:5 - (ae-undocumented) Missing documentation for "vacuumTasks".
// src/tasks/types.d.ts:126:5 - (ae-undocumented) Missing documentation for "event$".
// src/tasks/types.d.ts:132:5 - (ae-undocumented) Missing documentation for "get".
// src/tasks/types.d.ts:133:5 - (ae-undocumented) Missing documentation for "list".
// src/tasks/types.d.ts:153:5 - (ae-undocumented) Missing documentation for "list".
// src/tasks/types.d.ts:83:5 - (ae-undocumented) Missing documentation for "cancelSignal".
// src/tasks/types.d.ts:84:5 - (ae-undocumented) Missing documentation for "spec".
// src/tasks/types.d.ts:85:5 - (ae-undocumented) Missing documentation for "secrets".
// src/tasks/types.d.ts:86:5 - (ae-undocumented) Missing documentation for "createdBy".
// src/tasks/types.d.ts:87:5 - (ae-undocumented) Missing documentation for "done".
// src/tasks/types.d.ts:88:5 - (ae-undocumented) Missing documentation for "isDryRun".
// src/tasks/types.d.ts:89:5 - (ae-undocumented) Missing documentation for "complete".
// src/tasks/types.d.ts:90:5 - (ae-undocumented) Missing documentation for "emitLog".
// src/tasks/types.d.ts:91:5 - (ae-undocumented) Missing documentation for "getTaskState".
// src/tasks/types.d.ts:94:5 - (ae-undocumented) Missing documentation for "updateCheckpoint".
// src/tasks/types.d.ts:103:5 - (ae-undocumented) Missing documentation for "serializeWorkspace".
// src/tasks/types.d.ts:106:5 - (ae-undocumented) Missing documentation for "cleanWorkspace".
// src/tasks/types.d.ts:107:5 - (ae-undocumented) Missing documentation for "rehydrateWorkspace".
// src/tasks/types.d.ts:111:5 - (ae-undocumented) Missing documentation for "getWorkspaceName".
// src/tasks/types.d.ts:112:5 - (ae-undocumented) Missing documentation for "getInitiatorCredentials".
// src/tasks/types.d.ts:120:5 - (ae-undocumented) Missing documentation for "cancel".
// src/tasks/types.d.ts:121:5 - (ae-undocumented) Missing documentation for "retry".
// src/tasks/types.d.ts:122:5 - (ae-undocumented) Missing documentation for "claim".
// src/tasks/types.d.ts:123:5 - (ae-undocumented) Missing documentation for "recoverTasks".
// src/tasks/types.d.ts:124:5 - (ae-undocumented) Missing documentation for "dispatch".
// src/tasks/types.d.ts:125:5 - (ae-undocumented) Missing documentation for "vacuumTasks".
// src/tasks/types.d.ts:128:5 - (ae-undocumented) Missing documentation for "event$".
// src/tasks/types.d.ts:134:5 - (ae-undocumented) Missing documentation for "get".
// src/tasks/types.d.ts:135:5 - (ae-undocumented) Missing documentation for "list".
// src/tasks/types.d.ts:155:5 - (ae-undocumented) Missing documentation for "list".
// src/types.d.ts:3:1 - (ae-undocumented) Missing documentation for "TemplateFilter".
// src/types.d.ts:5:1 - (ae-undocumented) Missing documentation for "TemplateGlobal".
```
@@ -76,6 +76,7 @@ export type TaskEventType = 'completion' | 'log' | 'cancelled' | 'recovered';
*/
export type SerializedTaskEvent = {
id: number;
isTaskRecoverable?: boolean;
taskId: string;
body: JsonObject;
type: TaskEventType;
@@ -163,6 +164,8 @@ export interface TaskContext {
export interface TaskBroker {
cancel?(taskId: string): Promise<void>;
retry?(taskId: string): Promise<void>;
claim(): Promise<TaskContext>;
recoverTasks?(): Promise<void>;
+23 -19
View File
@@ -235,6 +235,7 @@ export interface ScaffolderApi {
listTasks?(options: { filterByOwnership: 'owned' | 'all' }): Promise<{
tasks: ScaffolderTask[];
}>;
retry?(taskId: string): Promise<void>;
scaffold(
options: ScaffolderScaffoldOptions,
): Promise<ScaffolderScaffoldResponse>;
@@ -454,6 +455,8 @@ export interface ScaffolderStreamLogsOptions {
// (undocumented)
after?: number;
// (undocumented)
isTaskRecoverable?: boolean;
// (undocumented)
taskId: string;
}
@@ -562,25 +565,26 @@ export const useTemplateSecrets: () => ScaffolderUseTemplateSecrets;
// src/api/types.d.ts:105:5 - (ae-undocumented) Missing documentation for "taskId".
// src/api/types.d.ts:113:5 - (ae-undocumented) Missing documentation for "allowedHosts".
// src/api/types.d.ts:121:5 - (ae-undocumented) Missing documentation for "integrations".
// src/api/types.d.ts:133:5 - (ae-undocumented) Missing documentation for "taskId".
// src/api/types.d.ts:134:5 - (ae-undocumented) Missing documentation for "after".
// src/api/types.d.ts:137:1 - (ae-undocumented) Missing documentation for "ScaffolderDryRunOptions".
// src/api/types.d.ts:138:5 - (ae-undocumented) Missing documentation for "template".
// src/api/types.d.ts:139:5 - (ae-undocumented) Missing documentation for "values".
// src/api/types.d.ts:140:5 - (ae-undocumented) Missing documentation for "secrets".
// src/api/types.d.ts:141:5 - (ae-undocumented) Missing documentation for "directoryContents".
// src/api/types.d.ts:147:1 - (ae-undocumented) Missing documentation for "ScaffolderDryRunResponse".
// src/api/types.d.ts:148:5 - (ae-undocumented) Missing documentation for "directoryContents".
// src/api/types.d.ts:153:5 - (ae-undocumented) Missing documentation for "log".
// src/api/types.d.ts:154:5 - (ae-undocumented) Missing documentation for "steps".
// src/api/types.d.ts:155:5 - (ae-undocumented) Missing documentation for "output".
// src/api/types.d.ts:163:5 - (ae-undocumented) Missing documentation for "getTemplateParameterSchema".
// src/api/types.d.ts:171:5 - (ae-undocumented) Missing documentation for "getTask".
// src/api/types.d.ts:178:5 - (ae-undocumented) Missing documentation for "listTasks".
// src/api/types.d.ts:183:5 - (ae-undocumented) Missing documentation for "getIntegrationsList".
// src/api/types.d.ts:188:5 - (ae-undocumented) Missing documentation for "streamLogs".
// src/api/types.d.ts:189:5 - (ae-undocumented) Missing documentation for "dryRun".
// src/api/types.d.ts:190:5 - (ae-undocumented) Missing documentation for "autocomplete".
// src/api/types.d.ts:133:5 - (ae-undocumented) Missing documentation for "isTaskRecoverable".
// src/api/types.d.ts:134:5 - (ae-undocumented) Missing documentation for "taskId".
// src/api/types.d.ts:135:5 - (ae-undocumented) Missing documentation for "after".
// src/api/types.d.ts:138:1 - (ae-undocumented) Missing documentation for "ScaffolderDryRunOptions".
// src/api/types.d.ts:139:5 - (ae-undocumented) Missing documentation for "template".
// src/api/types.d.ts:140:5 - (ae-undocumented) Missing documentation for "values".
// src/api/types.d.ts:141:5 - (ae-undocumented) Missing documentation for "secrets".
// src/api/types.d.ts:142:5 - (ae-undocumented) Missing documentation for "directoryContents".
// src/api/types.d.ts:148:1 - (ae-undocumented) Missing documentation for "ScaffolderDryRunResponse".
// src/api/types.d.ts:149:5 - (ae-undocumented) Missing documentation for "directoryContents".
// src/api/types.d.ts:154:5 - (ae-undocumented) Missing documentation for "log".
// src/api/types.d.ts:155:5 - (ae-undocumented) Missing documentation for "steps".
// src/api/types.d.ts:156:5 - (ae-undocumented) Missing documentation for "output".
// src/api/types.d.ts:164:5 - (ae-undocumented) Missing documentation for "getTemplateParameterSchema".
// src/api/types.d.ts:172:5 - (ae-undocumented) Missing documentation for "getTask".
// src/api/types.d.ts:185:5 - (ae-undocumented) Missing documentation for "listTasks".
// src/api/types.d.ts:190:5 - (ae-undocumented) Missing documentation for "getIntegrationsList".
// src/api/types.d.ts:195:5 - (ae-undocumented) Missing documentation for "streamLogs".
// src/api/types.d.ts:196:5 - (ae-undocumented) Missing documentation for "dryRun".
// src/api/types.d.ts:197:5 - (ae-undocumented) Missing documentation for "autocomplete".
// src/components/types.d.ts:7:1 - (ae-undocumented) Missing documentation for "TemplateGroupFilter".
// src/extensions/types.d.ts:13:5 - (ae-undocumented) Missing documentation for "uiSchema".
// src/extensions/types.d.ts:30:5 - (ae-undocumented) Missing documentation for ""ui:options"".
@@ -161,6 +161,7 @@ export interface ScaffolderGetIntegrationsListResponse {
* @public
*/
export interface ScaffolderStreamLogsOptions {
isTaskRecoverable?: boolean;
taskId: string;
after?: number;
}
@@ -213,6 +214,13 @@ export interface ScaffolderApi {
*/
cancelTask(taskId: string): Promise<void>;
/**
* Starts the task again from the point where it failed.
*
* @param taskId - the id of the task
*/
retry?(taskId: string): Promise<void>;
listTasks?(options: {
filterByOwnership: 'owned' | 'all';
}): Promise<{ tasks: ScaffolderTask[] }>;
@@ -143,6 +143,11 @@ function reducer(draft: TaskStream, action: ReducerAction) {
}
case 'RECOVERED': {
draft.cancelled = false;
draft.completed = false;
draft.output = undefined;
draft.error = undefined;
for (const stepId in draft.steps) {
if (draft.steps.hasOwnProperty(stepId)) {
draft.steps[stepId].startedAt = undefined;
@@ -185,12 +190,16 @@ export const useTaskEventStream = (taskId: string): TaskStream => {
let subscription: Subscription | undefined;
let logPusher: NodeJS.Timeout | undefined;
let retryCount = 1;
let isTaskRecoverable = false;
const startStreamLogProcess = () =>
scaffolderApi.getTask(taskId).then(
task => {
if (didCancel) {
return;
}
isTaskRecoverable =
task.spec.EXPERIMENTAL_recovery?.EXPERIMENTAL_strategy ===
'startOver';
dispatch({ type: 'INIT', data: task });
// TODO(blam): Use a normal fetch to fetch the current log for the event stream
@@ -199,7 +208,10 @@ export const useTaskEventStream = (taskId: string): TaskStream => {
// stream logs. Without this, if you have a lot of logs, it can look like the
// task is being rebuilt on load as it progresses through the steps at a slower
// rate whilst it builds the status from the event logs
const observable = scaffolderApi.streamLogs({ taskId });
const observable = scaffolderApi.streamLogs({
isTaskRecoverable,
taskId,
});
const collectedLogEvents = new Array<LogEvent>();
@@ -270,12 +282,14 @@ export const useTaskEventStream = (taskId: string): TaskStream => {
);
void startStreamLogProcess();
return () => {
didCancel = true;
if (subscription) {
subscription.unsubscribe();
}
if (logPusher) {
clearInterval(logPusher);
if (!isTaskRecoverable) {
didCancel = true;
if (subscription) {
subscription.unsubscribe();
}
if (logPusher) {
clearInterval(logPusher);
}
}
};
}, [scaffolderApi, dispatch, taskId]);
+2
View File
@@ -259,6 +259,7 @@ export const scaffolderTranslationRef: TranslationRef<
readonly 'ongoingTask.title': 'Run of';
readonly 'ongoingTask.contextMenu.cancel': 'Cancel';
readonly 'ongoingTask.contextMenu.startOver': 'Start Over';
readonly 'ongoingTask.contextMenu.retry': 'Retry';
readonly 'ongoingTask.contextMenu.hideLogs': 'Hide Logs';
readonly 'ongoingTask.contextMenu.showLogs': 'Show Logs';
readonly 'ongoingTask.contextMenu.hideButtonBar': 'Hide Button Bar';
@@ -267,6 +268,7 @@ export const scaffolderTranslationRef: TranslationRef<
readonly 'ongoingTask.pageTitle.hasTemplateName': 'Run of {{templateName}}';
readonly 'ongoingTask.pageTitle.noTemplateName': 'Scaffolder Run';
readonly 'ongoingTask.cancelButtonTitle': 'Cancel';
readonly 'ongoingTask.retryButtonTitle': 'Retry';
readonly 'ongoingTask.startOverButtonTitle': 'Start Over';
readonly 'ongoingTask.hideLogsButtonTitle': 'Hide Logs';
readonly 'ongoingTask.showLogsButtonTitle': 'Show Logs';
+4 -1
View File
@@ -556,6 +556,8 @@ export class ScaffolderClient implements ScaffolderApi_2 {
tasks: ScaffolderTask_2[];
}>;
// (undocumented)
retry?(taskId: string): Promise<void>;
// (undocumented)
scaffold(
options: ScaffolderScaffoldOptions_2,
): Promise<ScaffolderScaffoldResponse_2>;
@@ -676,7 +678,8 @@ export const useTemplateSecrets: () => ScaffolderUseTemplateSecrets_2;
// src/api.d.ts:33:5 - (ae-undocumented) Missing documentation for "dryRun".
// src/api.d.ts:36:5 - (ae-undocumented) Missing documentation for "listActions".
// src/api.d.ts:37:5 - (ae-undocumented) Missing documentation for "cancelTask".
// src/api.d.ts:38:5 - (ae-undocumented) Missing documentation for "autocomplete".
// src/api.d.ts:38:5 - (ae-undocumented) Missing documentation for "retry".
// src/api.d.ts:39:5 - (ae-undocumented) Missing documentation for "autocomplete".
// src/components/OngoingTask/OngoingTask.d.ts:6:22 - (ae-undocumented) Missing documentation for "OngoingTask".
// src/components/fields/EntityPicker/schema.d.ts:15:22 - (ae-undocumented) Missing documentation for "EntityPickerFieldSchema".
// src/components/fields/EntityTagsPicker/schema.d.ts:4:22 - (ae-undocumented) Missing documentation for "EntityTagsPickerFieldSchema".
+19 -6
View File
@@ -217,12 +217,10 @@ export class ScaffolderClient implements ScaffolderApi {
}
private streamLogsEventStream({
isTaskRecoverable,
taskId,
after,
}: {
taskId: string;
after?: number;
}): Observable<LogEvent> {
}: ScaffolderStreamLogsOptions): Observable<LogEvent> {
return new ObservableImpl(subscriber => {
const params = new URLSearchParams();
if (after !== undefined) {
@@ -246,14 +244,14 @@ export class ScaffolderClient implements ScaffolderApi {
};
const ctrl = new AbortController();
fetchEventSource(url, {
void fetchEventSource(url, {
fetch: this.fetchApi.fetch,
signal: ctrl.signal,
onmessage(e: EventSourceMessage) {
if (e.event === 'log') {
processEvent(e);
return;
} else if (e.event === 'completion') {
} else if (e.event === 'completion' && !isTaskRecoverable) {
processEvent(e);
subscriber.complete();
ctrl.abort();
@@ -338,6 +336,21 @@ export class ScaffolderClient implements ScaffolderApi {
return await response.json();
}
async retry?(taskId: string): Promise<void> {
const baseUrl = await this.discoveryApi.getBaseUrl('scaffolder');
const url = `${baseUrl}/v2/tasks/${encodeURIComponent(taskId)}/retry`;
const response = await this.fetchApi.fetch(url, {
method: 'POST',
});
if (!response.ok) {
throw await ResponseError.fromResponse(response);
}
return await response.json();
}
async autocomplete({
token,
resource,
@@ -23,7 +23,8 @@ import Popover from '@material-ui/core/Popover';
import { makeStyles, Theme, useTheme } from '@material-ui/core/styles';
import { useAsync } from '@react-hookz/web';
import Cancel from '@material-ui/icons/Cancel';
import Retry from '@material-ui/icons/Repeat';
import Repeat from '@material-ui/icons/Repeat';
import Replay from '@material-ui/icons/Replay';
import Toc from '@material-ui/icons/Toc';
import ControlPointIcon from '@material-ui/icons/ControlPoint';
import MoreVert from '@material-ui/icons/MoreVert';
@@ -41,8 +42,11 @@ import { scaffolderTranslationRef } from '../../translation';
type ContextMenuProps = {
cancelEnabled?: boolean;
canRetry: boolean;
isRetryableTask: boolean;
logsVisible?: boolean;
buttonBarVisible?: boolean;
onRetry?: () => void;
onStartOver?: () => void;
onToggleLogs?: (state: boolean) => void;
onToggleButtonBar?: (state: boolean) => void;
@@ -58,8 +62,11 @@ const useStyles = makeStyles<Theme, { fontColor: string }>(() => ({
export const ContextMenu = (props: ContextMenuProps) => {
const {
cancelEnabled,
canRetry,
isRetryableTask,
logsVisible,
buttonBarVisible,
onRetry,
onStartOver,
onToggleLogs,
onToggleButtonBar,
@@ -147,10 +154,22 @@ export const ContextMenu = (props: ContextMenuProps) => {
data-testid="start-over-task"
>
<ListItemIcon>
<Retry fontSize="small" />
<Repeat fontSize="small" />
</ListItemIcon>
<ListItemText primary={t('ongoingTask.contextMenu.startOver')} />
</MenuItem>
{isRetryableTask && (
<MenuItem
onClick={onRetry}
disabled={cancelEnabled || !canRetry}
data-testid="retry-task"
>
<ListItemIcon>
<Replay fontSize="small" />
</ListItemIcon>
<ListItemText primary={t('ongoingTask.contextMenu.retry')} />
</MenuItem>
)}
<MenuItem
onClick={cancel}
disabled={
@@ -58,6 +58,9 @@ const useStyles = makeStyles(theme => ({
cancelButton: {
marginRight: theme.spacing(1),
},
retryButton: {
marginRight: theme.spacing(1),
},
logsVisibilityButton: {
marginRight: theme.spacing(1),
},
@@ -130,6 +133,12 @@ export const OngoingTask = (props: {
return 0;
}, [steps]);
const isRetryableTask =
taskStream.task?.spec.EXPERIMENTAL_recovery?.EXPERIMENTAL_strategy ===
'startOver';
const canRetry = canReadTask && canCreateTask && isRetryableTask;
const startOver = useCallback(() => {
const { namespace, name } =
taskStream.task?.spec.templateInfo?.entity?.metadata ?? {};
@@ -157,6 +166,13 @@ export const OngoingTask = (props: {
templateRouteRef,
]);
const [{ status: _ }, { execute: triggerRetry }] = useAsync(async () => {
if (taskId) {
analytics.captureEvent('retried', 'Template has been retried');
await scaffolderApi.retry?.(taskId);
}
});
const [{ status: cancelStatus }, { execute: triggerCancel }] = useAsync(
async () => {
if (taskId) {
@@ -190,9 +206,12 @@ export const OngoingTask = (props: {
>
<ContextMenu
cancelEnabled={cancelEnabled}
canRetry={canRetry}
isRetryableTask={isRetryableTask}
logsVisible={logsVisible}
buttonBarVisible={buttonBarVisible}
onStartOver={startOver}
onRetry={triggerRetry}
onToggleLogs={setLogVisibleState}
onToggleButtonBar={setButtonBarVisibleState}
taskId={taskId}
@@ -229,7 +248,7 @@ export const OngoingTask = (props: {
className={classes.cancelButton}
disabled={
!cancelEnabled ||
cancelStatus !== 'not-executed' ||
(cancelStatus !== 'not-executed' && !isRetryableTask) ||
!canCancelTask
}
onClick={triggerCancel}
@@ -237,6 +256,16 @@ export const OngoingTask = (props: {
>
{t('ongoingTask.cancelButtonTitle')}
</Button>
{isRetryableTask && (
<Button
className={classes.retryButton}
disabled={cancelEnabled || !canRetry}
onClick={triggerRetry}
data-testid="retry-button"
>
{t('ongoingTask.retryButtonTitle')}
</Button>
)}
<Button
className={classes.logsVisibilityButton}
color="primary"
+2
View File
@@ -172,6 +172,7 @@ export const scaffolderTranslationRef = createTranslationRef({
},
subtitle: 'Task {{taskId}}',
cancelButtonTitle: 'Cancel',
retryButtonTitle: 'Retry',
startOverButtonTitle: 'Start Over',
hideLogsButtonTitle: 'Hide Logs',
showLogsButtonTitle: 'Show Logs',
@@ -179,6 +180,7 @@ export const scaffolderTranslationRef = createTranslationRef({
hideLogs: 'Hide Logs',
showLogs: 'Show Logs',
hideButtonBar: 'Hide Button Bar',
retry: 'Retry',
showButtonBar: 'Show Button Bar',
startOver: 'Start Over',
cancel: 'Cancel',