diff --git a/.changeset/metal-nails-punch.md b/.changeset/metal-nails-punch.md new file mode 100644 index 0000000000..a34c340c0a --- /dev/null +++ b/.changeset/metal-nails-punch.md @@ -0,0 +1,69 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +This patch adds changes to provide examples alongside scaffolder task actions. + +The `createTemplateAction` function now takes a list of examples e.g. + +```typescript +const actionExamples = [ + { + description: 'Example 1', + example: yaml.stringify({ + steps: [ + { + action: 'test:action', + id: 'test', + input: { + input1: 'value', + }, + }, + ], + }), + }, +]; + +export function createTestAction() { + return createTemplateAction({ + id: 'test:action', + examples: [ + { + description: 'Example 1', + examples: actionExamples + } + ], + ..., + }); +``` + +These examples can be retrieved later from the api. + +```bash +curl http://localhost:7007/api/scaffolder/v2/actions +``` + +```json +[ + { + "id": "test:action", + "examples": [ + { + "description": "Example 1", + "example": "steps:\n - action: test:action\n id: test\n input:\n input1: value\n" + } + ], + "schema": { + "input": { + "type": "object", + "properties": { + "input1": { + "title": "Input 1", + "type": "string" + } + } + } + } + } +] +``` diff --git a/.changeset/rude-gifts-pay.md b/.changeset/rude-gifts-pay.md new file mode 100644 index 0000000000..4440f2ffc1 --- /dev/null +++ b/.changeset/rude-gifts-pay.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder': patch +--- + +Show action example yaml on the scaffolder actions documentation page. diff --git a/plugins/scaffolder-backend/api-report.md b/plugins/scaffolder-backend/api-report.md index 66757a01cd..c89ead6d2b 100644 --- a/plugins/scaffolder-backend/api-report.md +++ b/plugins/scaffolder-backend/api-report.md @@ -855,6 +855,10 @@ export class TaskWorker { export type TemplateAction = { id: string; description?: string; + examples?: { + description: string; + example: string; + }[]; supportsDryRun?: boolean; schema?: { input?: Schema; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts index 4b9f883706..48fec3db03 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts @@ -19,6 +19,28 @@ import { ScmIntegrations } from '@backstage/integration'; import { CatalogApi } from '@backstage/catalog-client'; import { stringifyEntityRef } from '@backstage/catalog-model'; import { createTemplateAction } from '../../createTemplateAction'; +import yaml from 'yaml'; + +const id = 'catalog:register'; + +const examples = [ + { + description: 'Register with the catalog', + example: yaml.stringify({ + steps: [ + { + action: id, + id: 'register-with-catalog', + name: 'Register with the catalog', + input: { + catalogInfoUrl: + 'http://github.com/backstage/backstage/blob/master/catalog-info.yaml', + }, + }, + ], + }), + }, +]; /** * Registers entities from a catalog descriptor file in the workspace into the software catalog. @@ -34,9 +56,10 @@ export function createCatalogRegisterAction(options: { | { catalogInfoUrl: string; optional?: boolean } | { repoContentsUrl: string; catalogInfoPath?: string; optional?: boolean } >({ - id: 'catalog:register', + id, description: 'Registers entities from a catalog descriptor file in the workspace into the software catalog.', + examples, schema: { input: { oneOf: [ diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts index da758898dc..5257839f2d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts @@ -20,14 +20,47 @@ import * as yaml from 'yaml'; import { Entity } from '@backstage/catalog-model'; import { resolveSafeChildPath } from '@backstage/backend-common'; +const id = 'catalog:write'; + +const examples = [ + { + description: 'Write a catalog yaml file', + example: yaml.stringify({ + steps: [ + { + action: id, + id: 'create-catalog-info-file', + name: 'Create catalog file', + input: { + entity: { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test', + annotations: {}, + }, + spec: { + type: 'service', + lifecycle: 'production', + owner: 'default/owner', + }, + }, + }, + }, + ], + }), + }, +]; + /** * Writes a catalog descriptor file containing the provided entity to a path in the workspace. * @public */ export function createCatalogWriteAction() { return createTemplateAction<{ filePath?: string; entity: Entity }>({ - id: 'catalog:write', + id, description: 'Writes the catalog-info.yaml for your template', + examples, schema: { input: { type: 'object', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts index c53caff018..6b7ad8816f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts @@ -20,6 +20,7 @@ import os from 'os'; import { Writable } from 'stream'; import { createDebugLogAction } from './log'; import { join } from 'path'; +import yaml from 'yaml'; describe('debug:log', () => { const logStream = { @@ -91,4 +92,43 @@ describe('debug:log', () => { expect.stringContaining('Hello Backstage!'), ); }); + + it('should log the workspace content from an example, if active', async () => { + const example = action.examples?.find( + sample => sample.description === 'List the workspace directory', + )?.example as string; + expect(typeof example).toEqual('string'); + const context = { + ...mockContext, + ...yaml.parse(example).steps[0], + }; + + await action.handler(context); + + expect(logStream.write).toHaveBeenCalledTimes(1); + expect(logStream.write).toHaveBeenCalledWith( + expect.stringContaining('README.md'), + ); + expect(logStream.write).toHaveBeenCalledWith( + expect.stringContaining(join('a-directory', 'index.md')), + ); + }); + + it('should log message from an example', async () => { + const example = action.examples?.find( + sample => sample.description === 'Write a debug message', + )?.example as string; + + const context = { + ...mockContext, + ...yaml.parse(example).steps[0], + }; + + await action.handler(context); + + expect(logStream.write).toHaveBeenCalledTimes(1); + expect(logStream.write).toHaveBeenCalledWith( + expect.stringContaining('Hello Backstage!'), + ); + }); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts index 99d030b772..3863623563 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts @@ -17,6 +17,42 @@ import { readdir, stat } from 'fs-extra'; import { relative, join } from 'path'; import { createTemplateAction } from '../../createTemplateAction'; +import yaml from 'yaml'; + +const id = 'debug:log'; + +const examples = [ + { + description: 'Write a debug message', + example: yaml.stringify({ + steps: [ + { + action: id, + id: 'write-debug-line', + name: 'Write "Hello Backstage!" log line', + input: { + message: 'Hello Backstage!', + }, + }, + ], + }), + }, + { + description: 'List the workspace directory', + example: yaml.stringify({ + steps: [ + { + action: id, + id: 'write-workspace-directory', + name: 'List the workspace directory', + input: { + listWorkspace: true, + }, + }, + ], + }), + }, +]; /** * Writes a message into the log or lists all files in the workspace @@ -30,9 +66,10 @@ import { createTemplateAction } from '../../createTemplateAction'; */ export function createDebugLogAction() { return createTemplateAction<{ message?: string; listWorkspace?: boolean }>({ - id: 'debug:log', + id, description: 'Writes a message into the log or lists all files in the workspace.', + examples, schema: { input: { type: 'object', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/types.ts b/plugins/scaffolder-backend/src/scaffolder/actions/types.ts index 666f7a0d50..0912cc038b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/types.ts @@ -66,6 +66,7 @@ export type ActionContext = { export type TemplateAction = { id: string; description?: string; + examples?: { description: string; example: string }[]; supportsDryRun?: boolean; schema?: { input?: Schema; diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts index e741961ddc..9ff8d43625 100644 --- a/plugins/scaffolder-backend/src/service/router.ts +++ b/plugins/scaffolder-backend/src/service/router.ts @@ -292,6 +292,7 @@ export async function createRouter( return { id: action.id, description: action.description, + examples: action.examples, schema: action.schema, }; }); diff --git a/plugins/scaffolder/api-report.md b/plugins/scaffolder/api-report.md index bb45ad3b44..0a881b4519 100644 --- a/plugins/scaffolder/api-report.md +++ b/plugins/scaffolder/api-report.md @@ -38,6 +38,23 @@ import { UIOptionsType } from '@rjsf/utils'; import { UiSchema } from '@rjsf/utils'; import { z } from 'zod'; +// @public +export type Action = { + id: string; + description?: string; + schema?: { + input?: JSONSchema7; + output?: JSONSchema7; + }; + examples?: ActionExample[]; +}; + +// @public +export type ActionExample = { + description: string; + example: string; +}; + // @alpha export function createNextScaffolderFieldExtension< TReturnValue = unknown, @@ -196,14 +213,7 @@ export interface LayoutOptions

{ export type LayoutTemplate = FormProps_2['ObjectFieldTemplate']; // @public -export type ListActionsResponse = Array<{ - id: string; - description?: string; - schema?: { - input?: JSONSchema7; - output?: JSONSchema7; - }; -}>; +export type ListActionsResponse = Array; // @public export type LogEvent = { diff --git a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx index 1fca434a96..0c16d0bd03 100644 --- a/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx +++ b/plugins/scaffolder/src/components/ActionsPage/ActionsPage.tsx @@ -13,7 +13,7 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -import React from 'react'; +import React, { Fragment } from 'react'; import useAsync from 'react-use/lib/useAsync'; import { scaffolderApiRef } from '../../api'; import { @@ -27,10 +27,15 @@ import { TableContainer, TableHead, TableRow, + Grid, makeStyles, + Accordion, + AccordionSummary, + AccordionDetails, } from '@material-ui/core'; import { JSONSchema7, JSONSchema7Definition } from 'json-schema'; import classNames from 'classnames'; +import ExpandMoreIcon from '@material-ui/icons/ExpandMore'; import { useApi } from '@backstage/core-plugin-api'; import { @@ -39,8 +44,10 @@ import { Header, Page, ErrorPage, + CodeSnippet, MarkdownContent, } from '@backstage/core-components'; +import { ActionExample } from '../../types'; const useStyles = makeStyles(theme => ({ code: { @@ -68,6 +75,34 @@ const useStyles = makeStyles(theme => ({ }, })); +const ExamplesTable = (props: { examples: ActionExample[] }) => { + return ( + + {props.examples.map((example, index) => { + return ( + + + + {example.description} + + + + + + + + + ); + })} + + ); +}; + export const ActionsPage = () => { const api = useApi(scaffolderApiRef); const classes = useStyles(); @@ -181,6 +216,18 @@ export const ActionsPage = () => { {renderTable(action.schema.output)} )} + {action.examples && ( + + }> + Examples + + + + + + + + )} ); }); diff --git a/plugins/scaffolder/src/index.ts b/plugins/scaffolder/src/index.ts index 1ed1346176..ae994191ce 100644 --- a/plugins/scaffolder/src/index.ts +++ b/plugins/scaffolder/src/index.ts @@ -22,6 +22,8 @@ export { scaffolderApiRef, ScaffolderClient } from './api'; export type { + Action, + ActionExample, ListActionsResponse, LogEvent, ScaffolderApi, diff --git a/plugins/scaffolder/src/types.ts b/plugins/scaffolder/src/types.ts index acb0f0e114..422661b896 100644 --- a/plugins/scaffolder/src/types.ts +++ b/plugins/scaffolder/src/types.ts @@ -43,18 +43,36 @@ export type ScaffolderTask = { }; /** - * The response shape for the `listActions` call to the `scaffolder-backend` + * A single action example * * @public */ -export type ListActionsResponse = Array<{ +export type ActionExample = { + description: string; + example: string; +}; + +/** + * The response shape for a single action in the `listActions` call to the `scaffolder-backend` + * + * @public + */ +export type Action = { id: string; description?: string; schema?: { input?: JSONSchema7; output?: JSONSchema7; }; -}>; + examples?: ActionExample[]; +}; + +/** + * The response shape for the `listActions` call to the `scaffolder-backend` + * + * @public + */ +export type ListActionsResponse = Array; /** @public */ export type ScaffolderOutputLink = {