diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/types.ts b/plugins/scaffolder-backend/src/scaffolder/actions/types.ts
index 9a4ae8b2bf..ff15300b4a 100644
--- a/plugins/scaffolder-backend/src/scaffolder/actions/types.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/actions/types.ts
@@ -39,12 +39,19 @@ export type ActionContext = {
createTemporaryDirectory(): Promise;
templateInfo?: TemplateInfo;
+
+ /**
+ * Whether this action invocation is a dry-run or not.
+ * This will only ever be true if the actions as marked as supporting dry-runs.
+ */
+ isDryRun?: boolean;
};
/** @public */
export type TemplateAction = {
id: string;
description?: string;
+ supportsDryRun?: boolean;
schema?: {
input?: Schema;
output?: Schema;
diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts
index c9d0714392..3a066494b1 100644
--- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts
@@ -23,7 +23,7 @@ import nunjucks from 'nunjucks';
import { JsonObject, JsonValue } from '@backstage/types';
import { InputError } from '@backstage/errors';
import { PassThrough } from 'stream';
-import { isTruthy } from './helper';
+import { generateExampleOutput, isTruthy } from './helper';
import { validate as validateJsonSchema } from 'jsonschema';
import { parseRepoUrl } from '../actions/builtin/publish/util';
import { TemplateActionRegistry } from '../actions';
@@ -236,6 +236,23 @@ export class NunjucksWorkflowRunner implements WorkflowRunner {
const action = this.options.actionRegistry.get(step.action);
const { taskLogger, streamLogger } = createStepLogger({ task, step });
+ if (task.isDryRun && !action.supportsDryRun) {
+ task.emitLog(
+ `Skipping because ${action.id} does not support dry-run`,
+ );
+ const outputSchema = action.schema?.output;
+ if (outputSchema) {
+ context.steps[step.id] = {
+ output: generateExampleOutput(outputSchema) as {
+ [name in string]: JsonValue;
+ },
+ };
+ } else {
+ context.steps[step.id] = { output: {} };
+ }
+ continue;
+ }
+
// Secrets are only passed when templating the input to actions for security reasons
const input =
(step.input &&
diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/helper.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/helper.ts
index 2058ce917f..76951afde4 100644
--- a/plugins/scaffolder-backend/src/scaffolder/tasks/helper.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/tasks/helper.ts
@@ -15,6 +15,7 @@
*/
import { isArray } from 'lodash';
+import { Schema } from 'jsonschema';
/**
* Returns true if the input is not `false`, `undefined`, `null`, `""`, `0`, or
@@ -24,3 +25,31 @@ import { isArray } from 'lodash';
export function isTruthy(value: any): boolean {
return isArray(value) ? value.length > 0 : !!value;
}
+
+export function generateExampleOutput(schema: Schema): unknown {
+ const { examples } = schema as { examples?: unknown };
+ if (examples && Array.isArray(examples)) {
+ return examples[0];
+ }
+ if (schema.type === 'object') {
+ return Object.fromEntries(
+ Object.entries(schema.properties ?? {}).map(([key, value]) => [
+ key,
+ generateExampleOutput(value),
+ ]),
+ );
+ } else if (schema.type === 'array') {
+ const [firstSchema] = [schema.items]?.flat();
+ if (firstSchema) {
+ return [generateExampleOutput(firstSchema)];
+ }
+ return [];
+ } else if (schema.type === 'string') {
+ return '';
+ } else if (schema.type === 'number') {
+ return 0;
+ } else if (schema.type === 'boolean') {
+ return false;
+ }
+ return '';
+}
diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts
index a3a9ef84d1..4a9d43aea6 100644
--- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts
@@ -111,6 +111,7 @@ export interface TaskContext {
secrets?: TaskSecrets;
createdBy?: string;
done: boolean;
+ isDryRun?: boolean;
emitLog(message: string, logMetadata?: JsonObject): Promise;
complete(result: TaskCompletionState, metadata?: JsonObject): Promise;
getWorkspaceName(): Promise;