diff --git a/.changeset/metal-buttons-search.md b/.changeset/metal-buttons-search.md new file mode 100644 index 0000000000..9578d8d2d4 --- /dev/null +++ b/.changeset/metal-buttons-search.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Adds examples to a few scaffolder actions. diff --git a/.changeset/ten-lies-cheat.md b/.changeset/ten-lies-cheat.md new file mode 100644 index 0000000000..1c965a574c --- /dev/null +++ b/.changeset/ten-lies-cheat.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-node': patch +--- + +Export `TemplateExample` from the `createTemplateAction` type. diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.examples.test.ts new file mode 100644 index 0000000000..161ff9d1de --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.examples.test.ts @@ -0,0 +1,78 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PassThrough } from 'stream'; +import os from 'os'; +import { getVoidLogger } from '@backstage/backend-common'; +import { CatalogApi } from '@backstage/catalog-client'; +import { Entity } from '@backstage/catalog-model'; +import { createFetchCatalogEntityAction } from './fetch'; +import { examples } from './fetch.examples'; +import yaml from 'yaml'; + +describe('catalog:fetch examples', () => { + const getEntityByRef = jest.fn(); + const getEntitiesByRefs = jest.fn(); + + const catalogClient = { + getEntityByRef: getEntityByRef, + getEntitiesByRefs: getEntitiesByRefs, + }; + + const action = createFetchCatalogEntityAction({ + catalogClient: catalogClient as unknown as CatalogApi, + }); + + const mockContext = { + workspacePath: os.tmpdir(), + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + secrets: { backstageToken: 'secret' }, + }; + beforeEach(() => { + jest.resetAllMocks(); + }); + + describe('fetch single entity', () => { + it('should return entity from catalog', async () => { + getEntityByRef.mockReturnValueOnce({ + metadata: { + namespace: 'default', + name: 'name', + }, + kind: 'Component', + } as Entity); + + await action.handler({ + ...mockContext, + input: yaml.parse(examples[0].example).steps[0].input, + }); + + expect(getEntityByRef).toHaveBeenCalledWith('component:default/name', { + token: 'secret', + }); + expect(mockContext.output).toHaveBeenCalledWith('entity', { + metadata: { + namespace: 'default', + name: 'name', + }, + kind: 'Component', + }); + }); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.examples.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.examples.ts new file mode 100644 index 0000000000..5bd76dd539 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.examples.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TemplateExample } from '@backstage/plugin-scaffolder-node'; +import yaml from 'yaml'; + +export const examples: TemplateExample[] = [ + { + description: 'Fetch entity by reference', + example: yaml.stringify({ + steps: [ + { + action: 'catalog:fetch', + id: 'fetch', + name: 'Fetch catalog entity', + input: { + entityRef: 'component:default/name', + }, + }, + ], + }), + }, + { + description: 'Fetch multiple entities by referencse', + example: yaml.stringify({ + steps: [ + { + action: 'catalog:fetch', + id: 'fetchMultiple', + name: 'Fetch catalog entities', + input: { + entityRefs: ['component:default/name'], + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.ts index 437da8ae73..75c4bc94ce 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/fetch.ts @@ -16,45 +16,12 @@ import { CatalogApi } from '@backstage/catalog-client'; import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; -import yaml from 'yaml'; import { z } from 'zod'; import { parseEntityRef, stringifyEntityRef } from '@backstage/catalog-model'; +import { examples } from './fetch.examples'; const id = 'catalog:fetch'; -const examples = [ - { - description: 'Fetch entity by reference', - example: yaml.stringify({ - steps: [ - { - action: id, - id: 'fetch', - name: 'Fetch catalog entity', - input: { - entityRef: 'component:default/name', - }, - }, - ], - }), - }, - { - description: 'Fetch multiple entities by referencse', - example: yaml.stringify({ - steps: [ - { - action: id, - id: 'fetchMultiple', - name: 'Fetch catalog entities', - input: { - entityRefs: ['component:default/name'], - }, - }, - ], - }), - }, -]; - /** * Returns entity or entities from the catalog by entity reference(s). * diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.examples.test.ts new file mode 100644 index 0000000000..eb5aa88c0f --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.examples.test.ts @@ -0,0 +1,108 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { PassThrough } from 'stream'; +import os from 'os'; +import { getVoidLogger } from '@backstage/backend-common'; +import { CatalogApi } from '@backstage/catalog-client'; +import { ConfigReader } from '@backstage/config'; +import { ScmIntegrations } from '@backstage/integration'; +import { createCatalogRegisterAction } from './register'; +import { Entity } from '@backstage/catalog-model'; +import { examples } from './register.examples'; +import yaml from 'yaml'; + +describe('catalog:register', () => { + const integrations = ScmIntegrations.fromConfig( + new ConfigReader({ + integrations: { + github: [{ host: 'github.com', token: 'token' }], + }, + }), + ); + + const addLocation = jest.fn(); + const catalogClient = { + addLocation: addLocation, + }; + + const action = createCatalogRegisterAction({ + integrations, + catalogClient: catalogClient as unknown as CatalogApi, + }); + + const mockContext = { + workspacePath: os.tmpdir(), + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should register location in catalog', async () => { + addLocation + .mockResolvedValueOnce({ + entities: [], + }) + .mockResolvedValueOnce({ + entities: [ + { + metadata: { + namespace: 'default', + name: 'test', + }, + kind: 'Component', + } as Entity, + ], + }); + await action.handler({ + ...mockContext, + input: yaml.parse(examples[0].example).steps[0].input, + }); + + expect(addLocation).toHaveBeenNthCalledWith( + 1, + { + type: 'url', + target: + 'http://github.com/backstage/backstage/blob/master/catalog-info.yaml', + }, + {}, + ); + expect(addLocation).toHaveBeenNthCalledWith( + 2, + { + dryRun: true, + type: 'url', + target: + 'http://github.com/backstage/backstage/blob/master/catalog-info.yaml', + }, + {}, + ); + + expect(mockContext.output).toHaveBeenCalledWith( + 'entityRef', + 'component:default/test', + ); + expect(mockContext.output).toHaveBeenCalledWith( + 'catalogInfoUrl', + 'http://github.com/backstage/backstage/blob/master/catalog-info.yaml', + ); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.examples.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.examples.ts new file mode 100644 index 0000000000..198ff52085 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.examples.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TemplateExample } from '@backstage/plugin-scaffolder-node'; +import yaml from 'yaml'; + +export const examples: TemplateExample[] = [ + { + description: 'Register with the catalog', + example: yaml.stringify({ + steps: [ + { + action: 'catalog:register', + id: 'register-with-catalog', + name: 'Register with the catalog', + input: { + catalogInfoUrl: + 'http://github.com/backstage/backstage/blob/master/catalog-info.yaml', + }, + }, + ], + }), + }, +]; 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 3e35e16508..39372f484f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/register.ts @@ -19,29 +19,10 @@ import { ScmIntegrations } from '@backstage/integration'; import { CatalogApi } from '@backstage/catalog-client'; import { stringifyEntityRef, Entity } from '@backstage/catalog-model'; import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; -import yaml from 'yaml'; +import { examples } from './register.examples'; 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. * @public diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.examples.test.ts new file mode 100644 index 0000000000..daa8f4f6b1 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.examples.test.ts @@ -0,0 +1,72 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import fs from 'fs-extra'; + +jest.mock('fs-extra'); + +const fsMock = fs as jest.Mocked; + +import { PassThrough } from 'stream'; +import os from 'os'; +import { getVoidLogger } from '@backstage/backend-common'; +import { createCatalogWriteAction } from './write'; +import { resolve as resolvePath } from 'path'; +import * as yaml from 'yaml'; +import { examples } from './write.examples'; + +describe('catalog:write', () => { + const action = createCatalogWriteAction(); + + const mockContext = { + workspacePath: os.tmpdir(), + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + it('should write the catalog-info.yml in the workspace', async () => { + const entity = { + apiVersion: 'backstage.io/v1alpha1', + kind: 'Component', + metadata: { + name: 'test', + annotations: {}, + }, + spec: { + type: 'service', + lifecycle: 'production', + owner: 'default/owner', + }, + }; + + await action.handler({ + ...mockContext, + input: yaml.parse(examples[0].example).steps[0].input, + }); + + expect(fsMock.writeFile).toHaveBeenCalledTimes(1); + expect(fsMock.writeFile).toHaveBeenCalledWith( + resolvePath(mockContext.workspacePath, 'catalog-info.yaml'), + yaml.stringify(entity), + ); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.examples.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.examples.ts new file mode 100644 index 0000000000..b66cff23d7 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.examples.ts @@ -0,0 +1,48 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TemplateExample } from '@backstage/plugin-scaffolder-node'; +import * as yaml from 'yaml'; + +export const examples: TemplateExample[] = [ + { + description: 'Write a catalog yaml file', + example: yaml.stringify({ + steps: [ + { + action: 'catalog:write', + 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', + }, + }, + }, + }, + ], + }), + }, +]; 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 81fdc73e71..7663ad15c8 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/catalog/write.ts @@ -19,39 +19,10 @@ import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import * as yaml from 'yaml'; import { resolveSafeChildPath } from '@backstage/backend-common'; import { z } from 'zod'; +import { examples } from './write.examples'; 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 diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.examples.test.ts new file mode 100644 index 0000000000..a7f5a1804a --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.examples.test.ts @@ -0,0 +1,86 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getVoidLogger } from '@backstage/backend-common'; +import mockFs from 'mock-fs'; +import os from 'os'; +import { Writable } from 'stream'; +import { createDebugLogAction } from './log'; +import { join } from 'path'; +import yaml from 'yaml'; +import { examples } from './log.examples'; + +describe('debug:log examples', () => { + const logStream = { + write: jest.fn(), + } as jest.Mocked> as jest.Mocked; + + const mockTmpDir = os.tmpdir(); + const mockContext = { + input: {}, + baseUrl: 'somebase', + workspacePath: mockTmpDir, + logger: getVoidLogger(), + logStream, + output: jest.fn(), + createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir), + }; + + const action = createDebugLogAction(); + + beforeEach(() => { + mockFs({ + [`${mockContext.workspacePath}/README.md`]: '', + [`${mockContext.workspacePath}/a-directory/index.md`]: '', + }); + jest.resetAllMocks(); + }); + + afterEach(() => { + mockFs.restore(); + }); + + it('should log message', async () => { + const context = { + ...mockContext, + input: yaml.parse(examples[0].example).steps[0].input, + }; + + await action.handler(context); + + expect(logStream.write).toHaveBeenCalledTimes(1); + expect(logStream.write).toHaveBeenCalledWith( + expect.stringContaining('Hello Backstage!'), + ); + }); + + it('should log the workspace content, if active', async () => { + const context = { + ...mockContext, + input: yaml.parse(examples[1].example).steps[0].input, + }; + + 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')), + ); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.examples.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.examples.ts new file mode 100644 index 0000000000..0e9173bfa6 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.examples.ts @@ -0,0 +1,51 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TemplateExample } from '@backstage/plugin-scaffolder-node'; +import yaml from 'yaml'; + +export const examples: TemplateExample[] = [ + { + description: 'Write a debug message', + example: yaml.stringify({ + steps: [ + { + action: 'debug:log', + id: 'write-debug-line', + name: 'Write "Hello Backstage!" log line', + input: { + message: 'Hello Backstage!', + }, + }, + ], + }), + }, + { + description: 'List the workspace directory', + example: yaml.stringify({ + steps: [ + { + action: 'debug:log', + id: 'write-workspace-directory', + name: 'List the workspace directory', + input: { + listWorkspace: true, + }, + }, + ], + }), + }, +]; 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 481eb9dfdd..524e12b5c9 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts @@ -17,43 +17,10 @@ import { readdir, stat } from 'fs-extra'; import { relative, join } from 'path'; import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; -import yaml from 'yaml'; +import { examples } from './log.examples'; 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 * diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.examples.test.ts new file mode 100644 index 0000000000..595ae7c3b6 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.examples.test.ts @@ -0,0 +1,61 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { getVoidLogger } from '@backstage/backend-common'; +import mockFs from 'mock-fs'; +import { createWaitAction } from './wait'; +import { Writable } from 'stream'; +import os from 'os'; +import { examples } from './wait.examples'; +import yaml from 'yaml'; + +describe('debug:wait examples', () => { + const action = createWaitAction(); + + const logStream = { + write: jest.fn(), + } as jest.Mocked> as jest.Mocked; + + const mockTmpDir = os.tmpdir(); + const mockContext = { + input: {}, + baseUrl: 'somebase', + workspacePath: mockTmpDir, + logger: getVoidLogger(), + logStream, + output: jest.fn(), + createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir), + }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + afterEach(() => { + mockFs.restore(); + }); + + it('should wait for specified period of seconds', async () => { + const context = { + ...mockContext, + input: yaml.parse(examples[0].example).steps[0].input, + }; + const start = new Date().getTime(); + await action.handler(context); + const end = new Date().getTime(); + expect(end - start).toBeGreaterThanOrEqual(50); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.examples.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.examples.ts new file mode 100644 index 0000000000..1cc69b1aed --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.examples.ts @@ -0,0 +1,66 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TemplateExample } from '@backstage/plugin-scaffolder-node'; +import yaml from 'yaml'; + +export const examples: TemplateExample[] = [ + { + description: 'Waiting for 50 milliseconds', + example: yaml.stringify({ + steps: [ + { + action: 'debug:wait', + id: 'wait-milliseconds', + name: 'Waiting for 50 milliseconds', + input: { + milliseconds: 50, + }, + }, + ], + }), + }, + { + description: 'Waiting for 5 seconds', + example: yaml.stringify({ + steps: [ + { + action: 'debug:wait', + id: 'wait-5sec', + name: 'Waiting for 5 seconds', + input: { + seconds: 5, + }, + }, + ], + }), + }, + { + description: 'Waiting for 1 minutes', + example: yaml.stringify({ + steps: [ + { + action: 'debug:wait', + id: 'wait-1min', + name: 'Waiting for 1 minutes', + input: { + minutes: 1, + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.ts index 02cab239cb..ae0af4db5a 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/wait.ts @@ -16,46 +16,13 @@ import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; import { HumanDuration } from '@backstage/types'; -import yaml from 'yaml'; import { Duration } from 'luxon'; +import { examples } from './wait.examples'; const id = 'debug:wait'; const MAX_WAIT_TIME_IN_ISO = 'T00:00:30'; -const examples = [ - { - description: 'Waiting for 5 seconds', - example: yaml.stringify({ - steps: [ - { - action: id, - id: 'wait-5sec', - name: 'Waiting for 5 seconds', - input: { - seconds: 5, - }, - }, - ], - }), - }, - { - description: 'Waiting for 5 minutes', - example: yaml.stringify({ - steps: [ - { - action: id, - id: 'wait-5min', - name: 'Waiting for 5 minutes', - input: { - minutes: 5, - }, - }, - ], - }), - }, -]; - /** * Waits for a certain period of time. * diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.examples.test.ts new file mode 100644 index 0000000000..e3c7d3e15c --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.examples.test.ts @@ -0,0 +1,85 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import yaml from 'yaml'; + +jest.mock('./helpers'); + +import os from 'os'; +import { resolve as resolvePath } from 'path'; +import { getVoidLogger, UrlReader } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { ScmIntegrations } from '@backstage/integration'; +import { createFetchPlainAction } from './plain'; +import { PassThrough } from 'stream'; +import { fetchContents } from './helpers'; +import { examples } from './plain.examples'; + +describe('fetch:plain examples', () => { + const integrations = ScmIntegrations.fromConfig( + new ConfigReader({ + integrations: { + github: [{ host: 'github.com', token: 'token' }], + }, + }), + ); + const reader: UrlReader = { + readUrl: jest.fn(), + readTree: jest.fn(), + search: jest.fn(), + }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + const action = createFetchPlainAction({ integrations, reader }); + const mockContext = { + workspacePath: os.tmpdir(), + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + it('should fetch plain', async () => { + await action.handler({ + ...mockContext, + input: yaml.parse(examples[0].example).steps[0].input, + }); + expect(fetchContents).toHaveBeenCalledWith( + expect.objectContaining({ + outputPath: resolvePath(mockContext.workspacePath), + fetchUrl: + 'https://github.com/backstage/community/tree/main/backstage-community-sessions/assets', + }), + ); + }); + + it('should fetch plain to a specified directory', async () => { + await action.handler({ + ...mockContext, + input: yaml.parse(examples[1].example).steps[0].input, + }); + expect(fetchContents).toHaveBeenCalledWith( + expect.objectContaining({ + outputPath: resolvePath(mockContext.workspacePath, 'fetched-data'), + fetchUrl: + 'https://github.com/backstage/community/tree/main/backstage-community-sessions/assets', + }), + ); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.examples.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.examples.ts new file mode 100644 index 0000000000..4937b72991 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.examples.ts @@ -0,0 +1,53 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TemplateExample } from '@backstage/plugin-scaffolder-node'; +import yaml from 'yaml'; + +export const examples: TemplateExample[] = [ + { + description: 'Downloads content and places it in the workspace.', + example: yaml.stringify({ + steps: [ + { + action: 'fetch:plain', + id: 'fetch-plain', + name: 'Fetch plain', + input: { + url: 'https://github.com/backstage/community/tree/main/backstage-community-sessions/assets', + }, + }, + ], + }), + }, + { + description: + 'Optionally, if you would prefer the data to be downloaded to a subdirectory in the workspace you may specify the ‘targetPath’ input option.', + example: yaml.stringify({ + steps: [ + { + action: 'fetch:plain', + id: 'fetch-plain', + name: 'Fetch plain', + input: { + url: 'https://github.com/backstage/community/tree/main/backstage-community-sessions/assets', + targetPath: 'fetched-data', + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.ts index c10f696bb9..9796744715 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plain.ts @@ -18,6 +18,9 @@ import { UrlReader, resolveSafeChildPath } from '@backstage/backend-common'; import { ScmIntegrations } from '@backstage/integration'; import { fetchContents } from './helpers'; import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; +import { examples } from './plain.examples'; + +export const ACTION_ID = 'fetch:plain'; /** * Downloads content and places it in the workspace, or optionally @@ -31,7 +34,8 @@ export function createFetchPlainAction(options: { const { reader, integrations } = options; return createTemplateAction<{ url: string; targetPath?: string }>({ - id: 'fetch:plain', + id: ACTION_ID, + examples, description: 'Downloads content and places it in the workspace, or optionally in a subdirectory specified by the `targetPath` input option.', schema: { diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.examples.test.ts new file mode 100644 index 0000000000..7ae34cc140 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.examples.test.ts @@ -0,0 +1,71 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import yaml from 'yaml'; + +jest.mock('./helpers'); + +import os from 'os'; +import { resolve as resolvePath } from 'path'; +import { getVoidLogger, UrlReader } from '@backstage/backend-common'; +import { ConfigReader } from '@backstage/config'; +import { ScmIntegrations } from '@backstage/integration'; +import { createFetchPlainFileAction } from './plainFile'; +import { PassThrough } from 'stream'; +import { fetchFile } from './helpers'; +import { examples } from './plainFile.examples'; + +describe('fetch:plain:file examples', () => { + const integrations = ScmIntegrations.fromConfig( + new ConfigReader({ + integrations: { + github: [{ host: 'github.com', token: 'token' }], + }, + }), + ); + const reader: UrlReader = { + readUrl: jest.fn(), + readTree: jest.fn(), + search: jest.fn(), + }; + + beforeEach(() => { + jest.resetAllMocks(); + }); + + const action = createFetchPlainFileAction({ integrations, reader }); + const mockContext = { + workspacePath: os.tmpdir(), + logger: getVoidLogger(), + logStream: new PassThrough(), + output: jest.fn(), + createTemporaryDirectory: jest.fn(), + }; + + it('should fetch plain', async () => { + await action.handler({ + ...mockContext, + input: yaml.parse(examples[0].example).steps[0].input, + }); + expect(fetchFile).toHaveBeenCalledWith( + expect.objectContaining({ + outputPath: resolvePath(mockContext.workspacePath, 'target-path'), + fetchUrl: + 'https://github.com/backstage/community/tree/main/backstage-community-sessions/assets/Backstage%20Community%20Sessions.png', + }), + ); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.examples.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.examples.ts new file mode 100644 index 0000000000..bf71068bf1 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.examples.ts @@ -0,0 +1,37 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TemplateExample } from '@backstage/plugin-scaffolder-node'; +import yaml from 'yaml'; + +export const examples: TemplateExample[] = [ + { + description: 'Downloads a file and places it in the workspace.', + example: yaml.stringify({ + steps: [ + { + action: 'fetch:plain:file', + id: 'fetch-plain-file', + name: 'Fetch plain file', + input: { + url: 'https://github.com/backstage/community/tree/main/backstage-community-sessions/assets/Backstage%20Community%20Sessions.png', + targetPath: 'target-path', + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.ts index 7b33ad93b4..7617398caf 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/plainFile.ts @@ -18,6 +18,7 @@ import { UrlReader, resolveSafeChildPath } from '@backstage/backend-common'; import { ScmIntegrations } from '@backstage/integration'; import { fetchFile } from './helpers'; import { createTemplateAction } from '@backstage/plugin-scaffolder-node'; +import { examples } from './plainFile.examples'; /** * Downloads content and places it in the workspace, or optionally @@ -33,6 +34,7 @@ export function createFetchPlainFileAction(options: { return createTemplateAction<{ url: string; targetPath: string }>({ id: 'fetch:plain:file', description: 'Downloads single file and places it in the workspace.', + examples, schema: { input: { type: 'object', diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.examples.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.examples.test.ts new file mode 100644 index 0000000000..f744460690 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.examples.test.ts @@ -0,0 +1,230 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import os from 'os'; +import { join as joinPath, sep as pathSep } from 'path'; +import fs from 'fs-extra'; +import mockFs from 'mock-fs'; +import { + getVoidLogger, + resolvePackagePath, + UrlReader, +} from '@backstage/backend-common'; +import { ScmIntegrations } from '@backstage/integration'; +import { PassThrough } from 'stream'; +import { fetchContents } from './helpers'; +import { createFetchTemplateAction } from './template'; +import { + ActionContext, + TemplateAction, +} from '@backstage/plugin-scaffolder-node'; +import { examples } from './template.examples'; +import yaml from 'yaml'; + +jest.mock('./helpers', () => ({ + fetchContents: jest.fn(), +})); + +type FetchTemplateInput = ReturnType< + typeof createFetchTemplateAction +> extends TemplateAction + ? U + : never; + +const realFiles = Object.fromEntries( + [ + resolvePackagePath( + '@backstage/plugin-scaffolder-backend', + 'assets', + 'nunjucks.js.txt', + ), + ].map(k => [k, mockFs.load(k)]), +); + +const aBinaryFile = fs.readFileSync( + resolvePackagePath( + '@backstage/plugin-scaffolder-backend', + 'fixtures/test-nested-template/public/react-logo192.png', + ), +); + +const mockFetchContents = fetchContents as jest.MockedFunction< + typeof fetchContents +>; + +describe('fetch:template examples', () => { + let action: TemplateAction; + + const workspacePath = os.tmpdir(); + const createTemporaryDirectory: jest.MockedFunction< + ActionContext['createTemporaryDirectory'] + > = jest.fn(() => + Promise.resolve( + joinPath(workspacePath, `${createTemporaryDirectory.mock.calls.length}`), + ), + ); + + const logger = getVoidLogger(); + + const mockContext = (input: any) => ({ + templateInfo: { + baseUrl: 'base-url', + entityRef: 'template:default/test-template', + }, + input: input, + output: jest.fn(), + logStream: new PassThrough(), + logger, + workspacePath, + createTemporaryDirectory, + }); + + beforeEach(() => { + mockFs({ + ...realFiles, + }); + + action = createFetchTemplateAction({ + reader: Symbol('UrlReader') as unknown as UrlReader, + integrations: Symbol('Integrations') as unknown as ScmIntegrations, + }); + }); + + afterEach(() => { + mockFs.restore(); + }); + + describe('handler', () => { + describe('with valid input', () => { + let context: ActionContext; + + beforeEach(async () => { + context = mockContext(yaml.parse(examples[0].example).steps[0].input); + + mockFetchContents.mockImplementation(({ outputPath }) => { + mockFs({ + ...realFiles, + [outputPath]: { + 'an-executable.sh': mockFs.file({ + content: '#!/usr/bin/env bash', + mode: parseInt('100755', 8), + }), + 'empty-dir-${{ values.count }}': {}, + 'static.txt': 'static content', + '${{ values.name }}.txt': 'static content', + subdir: { + 'templated-content.txt': + '${{ values.name }}: ${{ values.count }}', + }, + '.${{ values.name }}': '${{ values.itemList | dump }}', + 'a-binary-file.png': aBinaryFile, + symlink: mockFs.symlink({ + path: 'a-binary-file.png', + }), + brokenSymlink: mockFs.symlink({ + path: './not-a-real-file.txt', + }), + }, + }); + + return Promise.resolve(); + }); + + await action.handler(context); + }); + + it('uses fetchContents to retrieve the template content', () => { + expect(mockFetchContents).toHaveBeenCalledWith( + expect.objectContaining({ + baseUrl: context.templateInfo?.baseUrl, + fetchUrl: context.input.url, + }), + ); + }); + + it('copies files with no templating in names or content successfully', async () => { + await expect( + fs.readFile(`${workspacePath}/target/static.txt`, 'utf-8'), + ).resolves.toEqual('static content'); + }); + + it('copies files with templated names successfully', async () => { + await expect( + fs.readFile(`${workspacePath}/target/test-project.txt`, 'utf-8'), + ).resolves.toEqual('static content'); + }); + + it('copies files with templated content successfully', async () => { + await expect( + fs.readFile( + `${workspacePath}/target/subdir/templated-content.txt`, + 'utf-8', + ), + ).resolves.toEqual('test-project: 1234'); + }); + + it('processes dotfiles', async () => { + await expect( + fs.readFile(`${workspacePath}/target/.test-project`, 'utf-8'), + ).resolves.toEqual('["first","second","third"]'); + }); + + it('copies empty directories', async () => { + await expect( + fs.readdir(`${workspacePath}/target/empty-dir-1234`, 'utf-8'), + ).resolves.toEqual([]); + }); + + it('copies binary files as-is without processing them', async () => { + await expect( + fs.readFile(`${workspacePath}/target/a-binary-file.png`), + ).resolves.toEqual(aBinaryFile); + }); + + it('copies files and maintains the original file permissions', async () => { + await expect( + fs + .stat(`${workspacePath}/target/an-executable.sh`) + .then(fObj => fObj.mode), + ).resolves.toEqual(parseInt('100755', 8)); + }); + + it('copies file symlinks as-is without processing them', async () => { + await expect( + fs + .lstat(`${workspacePath}/target/symlink`) + .then(i => i.isSymbolicLink()), + ).resolves.toBe(true); + + await expect( + fs.realpath(`${workspacePath}/target/symlink`), + ).resolves.toBe(joinPath(workspacePath, 'target', 'a-binary-file.png')); + }); + + it('copies broken symlinks as-is without processing them', async () => { + await expect( + fs + .lstat(`${workspacePath}/target/brokenSymlink`) + .then(i => i.isSymbolicLink()), + ).resolves.toBe(true); + + await expect( + fs.readlink(`${workspacePath}/target/brokenSymlink`), + ).resolves.toEqual(`.${pathSep}not-a-real-file.txt`); + }); + }); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.examples.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.examples.ts new file mode 100644 index 0000000000..4c4628e715 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.examples.ts @@ -0,0 +1,44 @@ +/* + * Copyright 2021 The Backstage Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +import { TemplateExample } from '@backstage/plugin-scaffolder-node'; +import yaml from 'yaml'; + +export const examples: TemplateExample[] = [ + { + description: + 'Downloads a skelaton directory that lives alongside the template file and fill it out with values.', + example: yaml.stringify({ + steps: [ + { + action: 'fetch:template', + id: 'fetch-template', + name: 'Fetch template', + input: { + url: './skeleton', + targetPath: './target', + values: { + name: 'test-project', + count: 1234, + itemList: ['first', 'second', 'third'], + showDummyFile: false, + }, + }, + }, + ], + }), + }, +]; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts index be7b47521c..791ac7f3cf 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts @@ -29,6 +29,7 @@ import { TemplateGlobal, } from '../../../../lib/templating/SecureTemplater'; import { createDefaultFilters } from '../../../../lib/templating/filters'; +import { examples } from './template.examples'; /** * Downloads a skeleton, templates variables into file and directory names and content. @@ -70,6 +71,7 @@ export function createFetchTemplateAction(options: { id: 'fetch:template', description: 'Downloads a skeleton, templates variables into file and directory names and content, and places the result in the workspace, or optionally in a subdirectory specified by the `targetPath` input option.', + examples, schema: { input: { type: 'object', diff --git a/plugins/scaffolder-node/src/actions/createTemplateAction.ts b/plugins/scaffolder-node/src/actions/createTemplateAction.ts index a667f91744..38aba32778 100644 --- a/plugins/scaffolder-node/src/actions/createTemplateAction.ts +++ b/plugins/scaffolder-node/src/actions/createTemplateAction.ts @@ -20,6 +20,12 @@ import { Schema } from 'jsonschema'; import zodToJsonSchema from 'zod-to-json-schema'; import { JsonObject } from '@backstage/types'; +/** @public */ +export type TemplateExample = { + description: string; + example: string; +}; + /** @public */ export type TemplateActionOptions< TActionInput extends JsonObject = {}, @@ -29,7 +35,7 @@ export type TemplateActionOptions< > = { id: string; description?: string; - examples?: { description: string; example: string }[]; + examples?: TemplateExample[]; supportsDryRun?: boolean; schema?: { input?: TInputSchema; diff --git a/plugins/scaffolder-node/src/actions/index.ts b/plugins/scaffolder-node/src/actions/index.ts index e4af098356..770c2516e7 100644 --- a/plugins/scaffolder-node/src/actions/index.ts +++ b/plugins/scaffolder-node/src/actions/index.ts @@ -17,5 +17,6 @@ export { createTemplateAction, type TemplateActionOptions, + type TemplateExample, } from './createTemplateAction'; export { type ActionContext, type TemplateAction } from './types';