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:
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>;
|
||||
|
||||
@@ -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]);
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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".
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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',
|
||||
|
||||
Reference in New Issue
Block a user