From af06c6a782db67e4bf3bc32095e8dea805452c83 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Fri, 28 May 2021 10:32:31 +0200 Subject: [PATCH 01/18] Some small fixes Signed-off-by: Oliver Sand --- docs/features/software-catalog/descriptor-format.md | 2 +- .../src/schema/kinds/Template.v1beta2.schema.json | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/features/software-catalog/descriptor-format.md b/docs/features/software-catalog/descriptor-format.md index c8449dd351..988af89e47 100644 --- a/docs/features/software-catalog/descriptor-format.md +++ b/docs/features/software-catalog/descriptor-format.md @@ -722,7 +722,7 @@ You can find out more about the `steps` key ### `spec.owner` [optional] -An [entity reference](#string-references) to the owner of the component, e.g. +An [entity reference](#string-references) to the owner of the template, e.g. `artist-relations-team`. This field is required. In Backstage, the owner of a Template is the singular entity (commonly a team) diff --git a/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json b/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json index a3d8e6ba86..af253549f0 100644 --- a/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json +++ b/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json @@ -1,10 +1,10 @@ { "$schema": "http://json-schema.org/draft-07/schema", - "$id": "TemplateV1beta1", + "$id": "TemplateV1beta2", "description": "A Template describes a scaffolding task for use with the Scaffolder. It describes the required parameters as well as a series of steps that will be taken to execute the scaffolding task.", "examples": [ { - "apiVersion": "backstage.io/v1beta1", + "apiVersion": "backstage.io/v1beta2", "kind": "Template", "metadata": { "name": "react-ssr-template", From bdcd99416092306b3c97bd661f5f7aeb6780f0cf Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Fri, 28 May 2021 10:33:45 +0200 Subject: [PATCH 02/18] Display skipped steps Signed-off-by: Oliver Sand --- plugins/scaffolder/src/components/TaskPage/TaskPage.tsx | 8 +++++++- plugins/scaffolder/src/types.ts | 2 +- 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx index cc1149ee61..c19f9ae37f 100644 --- a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx +++ b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx @@ -172,6 +172,8 @@ export const TaskStatusStepper = memo( const isCompleted = step.status === 'completed'; const isFailed = step.status === 'failed'; const isActive = step.status === 'processing'; + const isSkipped = step.status === 'skipped'; + return ( onUserStepChange(step.id)}> @@ -186,7 +188,11 @@ export const TaskStatusStepper = memo( >
{step.name} - + {isSkipped ? ( + Skipped + ) : ( + + )}
diff --git a/plugins/scaffolder/src/types.ts b/plugins/scaffolder/src/types.ts index 9ba480a441..e821d8fa9c 100644 --- a/plugins/scaffolder/src/types.ts +++ b/plugins/scaffolder/src/types.ts @@ -16,7 +16,7 @@ import { JSONSchema } from '@backstage/catalog-model'; import { JsonValue } from '@backstage/config'; -export type Status = 'open' | 'processing' | 'failed' | 'completed'; +export type Status = 'open' | 'processing' | 'failed' | 'completed' | 'skipped'; export type JobStatus = 'PENDING' | 'STARTED' | 'COMPLETED' | 'FAILED'; export type Job = { id: string; From f26e6008f7044cc0b413203ff8d2d92f783eb51b Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Fri, 28 May 2021 10:44:44 +0200 Subject: [PATCH 03/18] Add `publish:log` action for debugging Signed-off-by: Oliver Sand --- .changeset/slimy-games-brake.md | 5 ++ .../actions/builtin/createBuiltinActions.ts | 2 + .../actions/builtin/publish/index.ts | 1 + .../scaffolder/actions/builtin/publish/log.ts | 72 +++++++++++++++++++ 4 files changed, 80 insertions(+) create mode 100644 .changeset/slimy-games-brake.md create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/log.ts diff --git a/.changeset/slimy-games-brake.md b/.changeset/slimy-games-brake.md new file mode 100644 index 0000000000..210d9f4e3a --- /dev/null +++ b/.changeset/slimy-games-brake.md @@ -0,0 +1,5 @@ +--- +'@backstage/plugin-scaffolder-backend': patch +--- + +Add `publish:log` action for debugging. diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index 0209fc50dd..850b289d3b 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -26,6 +26,7 @@ import { createPublishGithubAction, createPublishGithubPullRequestAction, createPublishGitlabAction, + createPublishLogAction, } from './publish'; export const createBuiltinActions = (options: { @@ -61,6 +62,7 @@ export const createBuiltinActions = (options: { createPublishAzureAction({ integrations, }), + createPublishLogAction(), createCatalogRegisterAction({ catalogClient, integrations }), ]; }; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts index 537a2b882d..d17a0f8750 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts @@ -20,3 +20,4 @@ export { createPublishAzureAction } from './azure'; export { createPublishGitlabAction } from './gitlab'; export { createPublishBitbucketAction } from './bitbucket'; export { createPublishFileAction } from './file'; +export { createPublishLogAction } from './log'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/log.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/log.ts new file mode 100644 index 0000000000..7bff980fad --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/log.ts @@ -0,0 +1,72 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { readdir, stat } from 'fs-extra'; +import { relative, resolve } from 'path'; +import { createTemplateAction } from '../../createTemplateAction'; + +/** + * This task is useful for local development and testing of both the scaffolder + * and scaffolder templates. + */ +export function createPublishLogAction() { + return createTemplateAction<{ message?: string; listWorkspace?: boolean }>({ + id: 'publish:log', + description: + 'Writes a message into the log or list all files in the workspace.', + schema: { + input: { + type: 'object', + properties: { + message: { + title: 'Message to output.', + type: 'string', + }, + listWorkspace: { + title: 'List all files in the workspace, if true.', + type: 'boolean', + }, + }, + }, + }, + async handler(ctx) { + const files = await recursiveReadDir(ctx.workspacePath); + + if (ctx.input?.message) { + ctx.logStream.write(ctx.input.message); + } + + if (ctx.input?.listWorkspace) { + ctx.logStream.write( + `Workspace:\n${files + .map(f => ` - ./${relative(ctx.workspacePath, f)}`) + .join('\n')}`, + ); + } + }, + }); +} + +export async function recursiveReadDir(dir: string): Promise { + const subdirs = await readdir(dir); + const files = await Promise.all( + subdirs.map(async subdir => { + const res = resolve(dir, subdir); + return (await stat(res)).isDirectory() ? recursiveReadDir(res) : [res]; + }), + ); + return files.reduce((a, f) => a.concat(f), []); +} From 498d6488bbb1cfebd143227377ab531cadd75a7c Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Fri, 28 May 2021 12:38:06 +0200 Subject: [PATCH 04/18] Implement conditional steps in backend Signed-off-by: Oliver Sand --- .../src/scaffolder/tasks/TaskWorker.test.ts | 77 ++++++++++++++++++- .../src/scaffolder/tasks/TaskWorker.ts | 64 ++++++++++++--- .../src/scaffolder/tasks/helper.test.ts | 36 +++++++++ .../src/scaffolder/tasks/helper.ts | 26 +++++++ .../src/scaffolder/tasks/types.ts | 1 + 5 files changed, 190 insertions(+), 14 deletions(-) create mode 100644 plugins/scaffolder-backend/src/scaffolder/tasks/helper.test.ts create mode 100644 plugins/scaffolder-backend/src/scaffolder/tasks/helper.ts diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts index edef02064f..64a5ff260f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts @@ -18,13 +18,13 @@ import { getVoidLogger, SingleConnectionDatabaseManager, } from '@backstage/backend-common'; -import { TaskWorker } from './TaskWorker'; -import os from 'os'; import { ConfigReader, JsonObject } from '@backstage/config'; -import { StorageTaskBroker } from './StorageTaskBroker'; -import { DatabaseTaskStore } from './DatabaseTaskStore'; +import os from 'os'; import { createTemplateAction, TemplateActionRegistry } from '../actions'; import { RepoSpec } from '../actions/builtin/publish/util'; +import { DatabaseTaskStore } from './DatabaseTaskStore'; +import { StorageTaskBroker } from './StorageTaskBroker'; +import { TaskWorker } from './TaskWorker'; async function createStore(): Promise { const manager = SingleConnectionDatabaseManager.fromConfig( @@ -54,6 +54,7 @@ describe('TaskWorker', () => { id: 'test-action', handler: async ctx => { ctx.output('testOutput', 'winning'); + ctx.output('badOutput', false); }, }); }); @@ -172,6 +173,74 @@ describe('TaskWorker', () => { expect((event?.body?.output as JsonObject).result).toBe('winning'); }); + it('should execute steps conditionally', async () => { + const broker = new StorageTaskBroker(storage, logger); + const taskWorker = new TaskWorker({ + logger, + workingDirectory: os.tmpdir(), + actionRegistry, + taskBroker: broker, + }); + + const { taskId } = await broker.dispatch({ + steps: [ + { id: 'test', name: 'test', action: 'test-action' }, + { + id: 'conditional', + name: 'conditional', + action: 'test-action', + if: '"{{ steps.test.output.testOutput }}"', + }, + ], + output: { + result: '{{ steps.conditional.output.testOutput }}', + }, + values: {}, + }); + + const task = await broker.claim(); + await taskWorker.runOneTask(task); + + const { events } = await storage.listEvents({ taskId }); + const event = events.find(e => e.type === 'completion'); + console.warn(events); + expect((event?.body?.output as JsonObject).result).toBe('winning'); + }); + + it('should skip steps conditionally', async () => { + const broker = new StorageTaskBroker(storage, logger); + const taskWorker = new TaskWorker({ + logger, + workingDirectory: os.tmpdir(), + actionRegistry, + taskBroker: broker, + }); + + const { taskId } = await broker.dispatch({ + steps: [ + { id: 'test', name: 'test', action: 'test-action' }, + { + id: 'conditional', + name: 'conditional', + action: 'test-action', + if: '{{ steps.test.output.badOutput }}', + }, + ], + output: { + result: + '{{#if steps.conditional}}{{ steps.conditional.output.testOutput }}{{/if}}', + }, + values: {}, + }); + + const task = await broker.claim(); + await taskWorker.runOneTask(task); + + const { events } = await storage.listEvents({ taskId }); + const event = events.find(e => e.type === 'completion'); + expect((event?.body?.output as JsonObject).result).toEqual(''); + }); + it('should parse strings as objects if possible', async () => { const inputAction = createTemplateAction<{ address: { line1: string }; diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts index 1d0d5ad3a3..49e55881b9 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts @@ -14,18 +14,19 @@ * limitations under the License. */ -import { PassThrough } from 'stream'; -import { Logger } from 'winston'; -import * as winston from 'winston'; -import { JsonValue, JsonObject } from '@backstage/config'; -import { validate as validateJsonSchema } from 'jsonschema'; -import { TaskBroker, Task } from './types'; -import fs from 'fs-extra'; -import path from 'path'; -import { TemplateActionRegistry } from '../actions/TemplateActionRegistry'; -import * as Handlebars from 'handlebars'; +import { JsonObject, JsonValue } from '@backstage/config'; import { InputError } from '@backstage/errors'; +import fs from 'fs-extra'; +import * as Handlebars from 'handlebars'; +import { validate as validateJsonSchema } from 'jsonschema'; +import path from 'path'; +import { PassThrough } from 'stream'; +import * as winston from 'winston'; +import { Logger } from 'winston'; import { parseRepoUrl } from '../actions/builtin/publish/util'; +import { TemplateActionRegistry } from '../actions/TemplateActionRegistry'; +import { isTruthy } from './helper'; +import { Task, TaskBroker } from './types'; type Options = { logger: Logger; @@ -48,6 +49,8 @@ export class TaskWorker { }); this.handlebars.registerHelper('json', obj => JSON.stringify(obj)); + + this.handlebars.registerHelper('not', value => !isTruthy(value)); } start() { @@ -102,6 +105,47 @@ export class TaskWorker { }); taskLogger.add(new winston.transports.Stream({ stream })); + + if (step.if !== undefined) { + // Support passing values like false to disable steps + let skip = !step.if; + + // Evaluate strings as handlebar templates + if (typeof step.if === 'string') { + const condition = JSON.parse( + JSON.stringify(step.if), + (_key, value) => { + if (typeof value === 'string') { + const templated = this.handlebars.compile(value, { + noEscape: true, + strict: true, + data: false, + preventIndent: true, + })(templateCtx); + + try { + return JSON.parse(templated); + } catch { + return templated; + } + } + + return value; + }, + ); + + skip = !isTruthy(condition); + } + + if (skip) { + await task.emitLog(`Skipped step ${step.name}`, { + ...metadata, + status: 'skipped', + }); + continue; + } + } + await task.emitLog(`Beginning step ${step.name}`, { ...metadata, status: 'processing', diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/helper.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/helper.test.ts new file mode 100644 index 0000000000..a4e347b827 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/helper.test.ts @@ -0,0 +1,36 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { isTruthy } from './helper'; + +describe('isTruthy', () => { + it.each` + value | result + ${'string'} | ${true} + ${true} | ${true} + ${1} | ${true} + ${['1']} | ${true} + ${{}} | ${true} + ${false} | ${false} + ${''} | ${false} + ${undefined} | ${false} + ${null} | ${false} + ${0} | ${false} + ${[]} | ${false} + `('should be $result for $value', async ({ value, result }) => { + expect(isTruthy(value)).toEqual(result); + }); +}); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/helper.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/helper.ts new file mode 100644 index 0000000000..36f91afdff --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/helper.ts @@ -0,0 +1,26 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 { isArray } from 'lodash'; + +/** + * Returns true if the input is not `false`, `undefined`, `null`, `""`, `0`, or + * `[]`. This behavior is based on the behavior of handlebars, see + * https://handlebarsjs.com/guide/builtin-helpers.html#if + */ +export function isTruthy(value: any): boolean { + return isArray(value) ? value.length > 0 : !!value; +} diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index d7d57a55ff..d4008a64c2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -51,6 +51,7 @@ export type TaskSpec = { name: string; action: string; input?: JsonObject; + if?: string; }>; output: { [name: string]: string }; }; From 6770dddb5698e674d7d1845971176ec970b31fc6 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Fri, 28 May 2021 12:39:08 +0200 Subject: [PATCH 05/18] Add a conditional example to the example template Signed-off-by: Oliver Sand --- .../sample-templates/v1beta2-demo/template.yaml | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/plugins/scaffolder-backend/sample-templates/v1beta2-demo/template.yaml b/plugins/scaffolder-backend/sample-templates/v1beta2-demo/template.yaml index b0693b31aa..8a355f880b 100644 --- a/plugins/scaffolder-backend/sample-templates/v1beta2-demo/template.yaml +++ b/plugins/scaffolder-backend/sample-templates/v1beta2-demo/template.yaml @@ -42,6 +42,7 @@ spec: - title: Choose a location required: - repoUrl + - dryRun properties: repoUrl: title: Repository Location @@ -50,6 +51,10 @@ spec: ui:options: allowedHosts: - github.com + dryRun: + title: Only perform a dry run, don't publish anything + type: boolean + default: false steps: - id: fetch-base @@ -70,6 +75,7 @@ spec: url: https://github.com/backstage/community/tree/main/backstage-community-sessions - id: publish + if: '{{ not parameters.dryRun }}' name: Publish action: publish:github input: @@ -78,12 +84,19 @@ spec: repoUrl: '{{ parameters.repoUrl }}' - id: register + if: '{{ not parameters.dryRun }}' name: Register action: catalog:register input: repoContentsUrl: '{{ steps.publish.output.repoContentsUrl }}' catalogInfoPath: '/catalog-info.yaml' + - name: Results + if: '{{ parameters.dryRun }}' + action: publish:log + input: + listWorkspace: true + output: - remoteUrl: '{{ steps.publish.output.remoteUrl }}' - entityRef: '{{ steps.register.output.entityRef }}' + remoteUrl: '{{#unless parameters.dryRun}}{{ steps.publish.output.remoteUrl }}{{/unless}}' + entityRef: '{{#unless parameters.dryRun}}{{ steps.register.output.entityRef }}{{/unless}}' From 052f42400d156fa13b2ce913cbef30f00837313f Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Fri, 28 May 2021 12:39:34 +0200 Subject: [PATCH 06/18] Extend template entity schema with if in step Signed-off-by: Oliver Sand --- .../src/kinds/TemplateEntityV1beta2.test.ts | 14 ++++++++++++++ .../src/kinds/TemplateEntityV1beta2.ts | 1 + .../src/schema/kinds/Template.v1beta2.schema.json | 7 ++++++- 3 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/catalog-model/src/kinds/TemplateEntityV1beta2.test.ts b/packages/catalog-model/src/kinds/TemplateEntityV1beta2.test.ts index 458bdcd0aa..2571cc947e 100644 --- a/packages/catalog-model/src/kinds/TemplateEntityV1beta2.test.ts +++ b/packages/catalog-model/src/kinds/TemplateEntityV1beta2.test.ts @@ -54,6 +54,7 @@ describe('templateEntityV1beta2Validator', () => { input: { url: './template', }, + if: '{{ parameters.owner }}', }, ], output: { @@ -122,16 +123,29 @@ describe('templateEntityV1beta2Validator', () => { delete (entity as any).spec.steps[0].action; await expect(validator.check(entity)).rejects.toThrow(/action/); }); + it('accepts missing owner', async () => { delete (entity as any).spec.owner; await expect(validator.check(entity)).resolves.toBe(true); }); + it('rejects empty owner', async () => { (entity as any).spec.owner = ''; await expect(validator.check(entity)).rejects.toThrow(/owner/); }); + it('rejects wrong type owner', async () => { (entity as any).spec.owner = 5; await expect(validator.check(entity)).rejects.toThrow(/owner/); }); + + it('accepts missing if', async () => { + delete (entity as any).spec.if; + await expect(validator.check(entity)).resolves.toBe(true); + }); + + it('accepts boolean in if', async () => { + (entity as any).spec.if = true; + await expect(validator.check(entity)).resolves.toBe(true); + }); }); diff --git a/packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts b/packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts index 98b953c9e0..6ee6fcb1f1 100644 --- a/packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts +++ b/packages/catalog-model/src/kinds/TemplateEntityV1beta2.ts @@ -33,6 +33,7 @@ export interface TemplateEntityV1beta2 extends Entity { name?: string; action: string; input?: JsonObject; + if?: string | boolean; }>; output?: { [name: string]: string }; owner?: string; diff --git a/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json b/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json index af253549f0..0bf8522fa4 100644 --- a/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json +++ b/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json @@ -45,7 +45,8 @@ "action": "publish:github", "parameters": { "repoUrl": "{{ parameters.repoUrl }}" - } + }, + "if": "{{ parameters.repoUrl }}" } ], "output": { @@ -128,6 +129,10 @@ "input": { "type": "object", "description": "A templated object describing the inputs to the action." + }, + "if": { + "type": ["string", "boolean"], + "description": "A templated condition that skips the step when evaluated to false. If the condition is true or not defined, the step is executed." } } } From 27a9b503a26a59ba5058b14ca8e1b46f0384a455 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Fri, 28 May 2021 12:43:30 +0200 Subject: [PATCH 07/18] Add changeset Signed-off-by: Oliver Sand --- .changeset/khaki-dancers-admire.md | 23 +++++++++++++++++++++++ .github/styles/vocab.txt | 1 + 2 files changed, 24 insertions(+) create mode 100644 .changeset/khaki-dancers-admire.md diff --git a/.changeset/khaki-dancers-admire.md b/.changeset/khaki-dancers-admire.md new file mode 100644 index 0000000000..7c5c61aa90 --- /dev/null +++ b/.changeset/khaki-dancers-admire.md @@ -0,0 +1,23 @@ +--- +'@backstage/catalog-model': patch +'@backstage/plugin-scaffolder': patch +'@backstage/plugin-scaffolder-backend': patch +--- + +Introduce conditional steps in scaffolder templates. + +A step can now include an `if` property that only executes a step if the +condition is truthy. The condition can include handlebar templates. + +```yaml +- id: register + if: '{{ not parameters.dryRun }}' + name: Register + action: catalog:register + input: + repoContentsUrl: '{{ steps.publish.output.repoContentsUrl }}' + catalogInfoPath: '/catalog-info.yaml' +``` + +Also introduces a `not` helper in handlebar templates that allows to negate +boolean expressions. diff --git a/.github/styles/vocab.txt b/.github/styles/vocab.txt index d91b840292..5b8624c97f 100644 --- a/.github/styles/vocab.txt +++ b/.github/styles/vocab.txt @@ -262,6 +262,7 @@ tooltips touchpoints transpilation transpiled +truthy ui unmanaged unregister From 09fdc24802dcc649bac5abf895475479738df0f3 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Fri, 28 May 2021 12:47:18 +0200 Subject: [PATCH 08/18] Improve documentation Signed-off-by: Oliver Sand --- docs/features/software-templates/writing-templates.md | 1 + .../catalog-model/src/schema/kinds/Template.v1beta2.schema.json | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/features/software-templates/writing-templates.md b/docs/features/software-templates/writing-templates.md index f58db3ae91..bb921bd54b 100644 --- a/docs/features/software-templates/writing-templates.md +++ b/docs/features/software-templates/writing-templates.md @@ -271,6 +271,7 @@ template. These follow the same standard format: ```yaml - id: fetch-base # A unique id for the step name: Fetch Base # A title displayed in the frontend + if: '{{ parameters.name }}' # Optional condition, skip the step if not truthy action: fetch:cookiecutter # an action to call input: # input that is passed as arguments to the action handler url: ./template diff --git a/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json b/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json index 0bf8522fa4..01a012e5aa 100644 --- a/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json +++ b/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json @@ -132,7 +132,7 @@ }, "if": { "type": ["string", "boolean"], - "description": "A templated condition that skips the step when evaluated to false. If the condition is true or not defined, the step is executed." + "description": "A templated condition that skips the step when evaluated to false. If the condition is true or not defined, the step is executed. The condition is true, if the input is not `false`, `undefined`, `null`, `\"\"`, `0`, or `[]`." } } } From 7ecb8a41efb6c455fc086f646218c91d884750ff Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Fri, 28 May 2021 13:01:22 +0200 Subject: [PATCH 09/18] Fix type Signed-off-by: Oliver Sand --- .../scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts | 1 - plugins/scaffolder-backend/src/scaffolder/tasks/types.ts | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts index 64a5ff260f..b381d8af65 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts @@ -203,7 +203,6 @@ describe('TaskWorker', () => { const { events } = await storage.listEvents({ taskId }); const event = events.find(e => e.type === 'completion'); - console.warn(events); expect((event?.body?.output as JsonObject).result).toBe('winning'); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts index d4008a64c2..31c0f77463 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/types.ts @@ -51,7 +51,7 @@ export type TaskSpec = { name: string; action: string; input?: JsonObject; - if?: string; + if?: string | boolean; }>; output: { [name: string]: string }; }; From b0f54086aca42ad8a78247a282bca0863d905d40 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Fri, 28 May 2021 13:13:49 +0200 Subject: [PATCH 10/18] Update api report Signed-off-by: Oliver Sand --- packages/catalog-model/api-report.md | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/catalog-model/api-report.md b/packages/catalog-model/api-report.md index 80da310ef1..0dab9670de 100644 --- a/packages/catalog-model/api-report.md +++ b/packages/catalog-model/api-report.md @@ -534,6 +534,7 @@ export interface TemplateEntityV1beta2 extends Entity { name?: string; action: string; input?: JsonObject; + if?: string | boolean; }>; output?: { [name: string]: string; From 029088419154c5d562c189dcaef494defd60d6c6 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Wed, 2 Jun 2021 15:30:55 +0200 Subject: [PATCH 11/18] Ad test for createPublishLogAction Signed-off-by: Oliver Sand --- .../actions/builtin/publish/log.test.ts | 93 +++++++++++++++++++ 1 file changed, 93 insertions(+) create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/log.test.ts diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/log.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/log.test.ts new file mode 100644 index 0000000000..f554f480e5 --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/log.test.ts @@ -0,0 +1,93 @@ +/* + * Copyright 2021 Spotify AB + * + * 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 mock from 'mock-fs'; +import os from 'os'; +import { Writable } from 'stream'; +import { createPublishLogAction } from './log'; + +describe('publish:log', () => { + 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 = createPublishLogAction(); + + beforeEach(() => { + mock({ + [`${mockContext.workspacePath}/README.md`]: '', + [`${mockContext.workspacePath}/a-directory/index.md`]: '', + }); + jest.resetAllMocks(); + }); + + afterEach(() => { + mock.restore(); + }); + + it('should do nothing', async () => { + await action.handler(mockContext); + + expect(logStream.write).toBeCalledTimes(0); + }); + + it('should log the workspace content, if active', async () => { + const context = { + ...mockContext, + input: { + listWorkspace: 'true', + }, + }; + + await action.handler(context); + + expect(logStream.write).toBeCalledTimes(1); + expect(logStream.write).toBeCalledWith( + expect.stringContaining('./README.md'), + ); + expect(logStream.write).toBeCalledWith( + expect.stringContaining('a-directory/index.md'), + ); + }); + + it('should log message', async () => { + const context = { + ...mockContext, + input: { + message: 'Hello Backstage!', + }, + }; + + await action.handler(context); + + expect(logStream.write).toBeCalledTimes(1); + expect(logStream.write).toBeCalledWith( + expect.stringContaining('Hello Backstage!'), + ); + }); +}); From 805f6ecc157331ba0df378550d698406226d0e14 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Fri, 4 Jun 2021 15:45:19 +0200 Subject: [PATCH 12/18] Disable strict mode Signed-off-by: Oliver Sand --- .../sample-templates/v1beta2-demo/template.yaml | 9 +++++---- .../v1beta2-demo/template/catalog-info.yaml | 2 +- .../src/scaffolder/tasks/TaskWorker.ts | 3 --- 3 files changed, 6 insertions(+), 8 deletions(-) diff --git a/plugins/scaffolder-backend/sample-templates/v1beta2-demo/template.yaml b/plugins/scaffolder-backend/sample-templates/v1beta2-demo/template.yaml index 8a355f880b..f15bef6f75 100644 --- a/plugins/scaffolder-backend/sample-templates/v1beta2-demo/template.yaml +++ b/plugins/scaffolder-backend/sample-templates/v1beta2-demo/template.yaml @@ -35,7 +35,7 @@ spec: description: System of the component ui:field: EntityPicker ui:options: - allowedKinds: + allowedKinds: - System defaultKind: System @@ -64,7 +64,8 @@ spec: url: ./template values: name: '{{ parameters.name }}' - owner: '{{ parameters.owner }}' + owner: '{{ parameters.owner }}' + system: '{{ parameters.system }}' destination: '{{ parseRepoUrl parameters.repoUrl }}' - id: fetch-docs @@ -98,5 +99,5 @@ spec: listWorkspace: true output: - remoteUrl: '{{#unless parameters.dryRun}}{{ steps.publish.output.remoteUrl }}{{/unless}}' - entityRef: '{{#unless parameters.dryRun}}{{ steps.register.output.entityRef }}{{/unless}}' + remoteUrl: '{{ steps.publish.output.remoteUrl }}' + entityRef: '{{ steps.register.output.entityRef }}' diff --git a/plugins/scaffolder-backend/sample-templates/v1beta2-demo/template/catalog-info.yaml b/plugins/scaffolder-backend/sample-templates/v1beta2-demo/template/catalog-info.yaml index 16e651eaf7..b87ea174f0 100644 --- a/plugins/scaffolder-backend/sample-templates/v1beta2-demo/template/catalog-info.yaml +++ b/plugins/scaffolder-backend/sample-templates/v1beta2-demo/template/catalog-info.yaml @@ -7,6 +7,6 @@ spec: type: website lifecycle: experimental owner: {{cookiecutter.owner | jsonify}} -{%- if cookiecutter.backstage_system != "" %} +{%- if cookiecutter.system != "" %} system: {{ cookiecutter.system | jsonify }} {%- endif %} diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts index 49e55881b9..f7ba181c8f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts @@ -118,7 +118,6 @@ export class TaskWorker { if (typeof value === 'string') { const templated = this.handlebars.compile(value, { noEscape: true, - strict: true, data: false, preventIndent: true, })(templateCtx); @@ -162,7 +161,6 @@ export class TaskWorker { if (typeof value === 'string') { const templated = this.handlebars.compile(value, { noEscape: true, - strict: true, data: false, preventIndent: true, })(templateCtx); @@ -249,7 +247,6 @@ export class TaskWorker { if (typeof value === 'string') { return this.handlebars.compile(value, { noEscape: true, - strict: true, data: false, preventIndent: true, })(templateCtx); From c5a6f33c37cac42691f8f338b9545f84e65ce81b Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Fri, 4 Jun 2021 16:48:38 +0200 Subject: [PATCH 13/18] Treat empty string as undefined Signed-off-by: Oliver Sand --- .../v1beta2-demo/template/catalog-info.yaml | 2 +- .../src/scaffolder/tasks/TaskWorker.test.ts | 2 +- .../src/scaffolder/tasks/TaskWorker.ts | 34 ++++++++++++++++++- 3 files changed, 35 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder-backend/sample-templates/v1beta2-demo/template/catalog-info.yaml b/plugins/scaffolder-backend/sample-templates/v1beta2-demo/template/catalog-info.yaml index b87ea174f0..02da577b39 100644 --- a/plugins/scaffolder-backend/sample-templates/v1beta2-demo/template/catalog-info.yaml +++ b/plugins/scaffolder-backend/sample-templates/v1beta2-demo/template/catalog-info.yaml @@ -7,6 +7,6 @@ spec: type: website lifecycle: experimental owner: {{cookiecutter.owner | jsonify}} -{%- if cookiecutter.system != "" %} +{%- if 'system' in cookiecutter %} system: {{ cookiecutter.system | jsonify }} {%- endif %} diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts index b381d8af65..7136998aa5 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts @@ -237,7 +237,7 @@ describe('TaskWorker', () => { const { events } = await storage.listEvents({ taskId }); const event = events.find(e => e.type === 'completion'); - expect((event?.body?.output as JsonObject).result).toEqual(''); + expect((event?.body?.output as JsonObject).result).toBeUndefined(); }); it('should parse strings as objects if possible', async () => { diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts index f7ba181c8f..94eaa62c85 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts @@ -122,6 +122,11 @@ export class TaskWorker { preventIndent: true, })(templateCtx); + // If it's just an empty string, treat it as undefined + if (templated === '') { + return undefined; + } + try { return JSON.parse(templated); } catch { @@ -165,8 +170,14 @@ export class TaskWorker { preventIndent: true, })(templateCtx); + // If it's just an empty string, treat it as undefined + if (templated === '') { + return undefined; + } + // If it smells like a JSON object then give it a parse as an object and if it fails return the string if ( + (templated.startsWith('"') && templated.endsWith('"')) || (templated.startsWith('{') && templated.endsWith('}')) || (templated.startsWith('[') && templated.endsWith(']')) ) { @@ -245,11 +256,32 @@ export class TaskWorker { JSON.stringify(task.spec.output), (_key, value) => { if (typeof value === 'string') { - return this.handlebars.compile(value, { + const templated = this.handlebars.compile(value, { noEscape: true, data: false, preventIndent: true, })(templateCtx); + + // If it's just an empty string, treat it as undefined + if (templated === '') { + return undefined; + } + + // If it smells like a JSON object then give it a parse as an object and if it fails return the string + if ( + (templated.startsWith('"') && templated.endsWith('"')) || + (templated.startsWith('{') && templated.endsWith('}')) || + (templated.startsWith('[') && templated.endsWith(']')) + ) { + try { + // Don't recursively JSON parse the values of this string. + // Shouldn't need to, don't want to encourage the use of returning handlebars from somewhere else + return JSON.parse(templated); + } catch { + return templated; + } + } + return templated; } return value; }, From 4dba3594ae1a4819a09c13892e103bc8e6de1bc9 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Fri, 4 Jun 2021 17:12:51 +0200 Subject: [PATCH 14/18] Deprecate entityRef in output and move it into links, filter empty links Signed-off-by: Oliver Sand --- .../schema/kinds/Template.v1beta2.schema.json | 8 +++- .../v1beta2-demo/template.yaml | 8 +++- .../TaskPage/TaskPageLinks.test.tsx | 18 +++++++-- .../src/components/TaskPage/TaskPageLinks.tsx | 40 +++++++++++-------- plugins/scaffolder/src/types.ts | 4 +- 5 files changed, 55 insertions(+), 23 deletions(-) diff --git a/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json b/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json index 01a012e5aa..b14cdb8ee0 100644 --- a/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json +++ b/packages/catalog-model/src/schema/kinds/Template.v1beta2.schema.json @@ -146,7 +146,7 @@ "description": "A list of external hyperlinks, typically pointing to resources created or updated by the template", "items": { "type": "object", - "required": ["url"], + "required": [], "properties": { "url": { "type": "string", @@ -154,6 +154,12 @@ "examples": ["https://github.com/my-org/my-new-repo"], "minLength": 1 }, + "entityRef": { + "type": "string", + "description": "An entity reference to an entity in the catalog.", + "examples": ["Component:default/my-app"], + "minLength": 1 + }, "title": { "type": "string", "description": "A user friendly display name for the link.", diff --git a/plugins/scaffolder-backend/sample-templates/v1beta2-demo/template.yaml b/plugins/scaffolder-backend/sample-templates/v1beta2-demo/template.yaml index f15bef6f75..86f41b43d2 100644 --- a/plugins/scaffolder-backend/sample-templates/v1beta2-demo/template.yaml +++ b/plugins/scaffolder-backend/sample-templates/v1beta2-demo/template.yaml @@ -99,5 +99,9 @@ spec: listWorkspace: true output: - remoteUrl: '{{ steps.publish.output.remoteUrl }}' - entityRef: '{{ steps.register.output.entityRef }}' + links: + - title: Repository + url: '{{ steps.publish.output.remoteUrl }}' + - title: Open in catalog + icon: 'catalog' + entityRef: '{{ steps.register.output.entityRef }}' diff --git a/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.test.tsx b/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.test.tsx index cd51488242..230df903da 100644 --- a/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.test.tsx +++ b/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.test.tsx @@ -14,10 +14,10 @@ * limitations under the License. */ +import { entityRouteRef } from '@backstage/plugin-catalog-react'; +import { renderInTestApp } from '@backstage/test-utils'; import React from 'react'; import { TaskPageLinks } from './TaskPageLinks'; -import { renderInTestApp } from '@backstage/test-utils'; -import { entityRouteRef } from '@backstage/plugin-catalog-react'; describe('TaskPageLinks', () => { beforeEach(() => {}); @@ -68,9 +68,11 @@ describe('TaskPageLinks', () => { links: [ { url: 'https://first.url', title: 'Cool link 1' }, { url: 'https://second.url', title: 'Cool link 2' }, + { entityRef: 'Component:default/my-app', title: 'Open in catalog' }, + { title: 'Skipped' }, ], }; - const { findByText } = await renderInTestApp( + const { findByText, queryByText } = await renderInTestApp( , { mountedRoutes: { @@ -88,5 +90,15 @@ describe('TaskPageLinks', () => { expect(element).toBeInTheDocument(); expect(element).toHaveAttribute('href', 'https://second.url'); + + element = await findByText('Open in catalog'); + + expect(element).toBeInTheDocument(); + expect(element).toHaveAttribute( + 'href', + '/catalog/default/Component/my-app', + ); + + expect(queryByText('Skipped')).not.toBeInTheDocument(); }); }); diff --git a/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.tsx b/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.tsx index c0b55193c2..394d755424 100644 --- a/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.tsx +++ b/plugins/scaffolder/src/components/TaskPage/TaskPageLinks.tsx @@ -14,21 +14,21 @@ * limitations under the License. */ -import React from 'react'; import { parseEntityName } from '@backstage/catalog-model'; import { IconComponent, IconKey, useApp, useRouteRef } from '@backstage/core'; -import { IconLink } from './IconLink'; import { entityRouteRef } from '@backstage/plugin-catalog-react'; import { Box } from '@material-ui/core'; import LanguageIcon from '@material-ui/icons/Language'; +import React from 'react'; import { TaskOutput } from '../../types'; +import { IconLink } from './IconLink'; type TaskPageLinksProps = { output: TaskOutput; }; export const TaskPageLinks = ({ output }: TaskPageLinksProps) => { - const { entityRef, remoteUrl } = output; + const { entityRef: entityRefOutput, remoteUrl } = output; let { links = [] } = output; const app = useApp(); const entityRoute = useRouteRef(entityRouteRef); @@ -40,12 +40,10 @@ export const TaskPageLinks = ({ output }: TaskPageLinksProps) => { links = [{ url: remoteUrl, title: 'Repo' }, ...links]; } - if (entityRef) { - const entityName = parseEntityName(entityRef); - const target = entityRoute(entityName); + if (entityRefOutput) { links = [ { - url: target, + entityRef: entityRefOutput, title: 'Open in catalog', icon: 'catalog', }, @@ -55,15 +53,25 @@ export const TaskPageLinks = ({ output }: TaskPageLinksProps) => { return ( - {links.map(({ url, title, icon }, i) => ( - - ))} + {links + .filter(({ url, entityRef }) => url || entityRef) + .map(({ url, entityRef, title, icon }) => { + if (entityRef) { + const entityName = parseEntityName(entityRef); + const target = entityRoute(entityName); + return { title, icon, url: target }; + } + return { title, icon, url: url! }; + }) + .map(({ url, title, icon }, i) => ( + + ))} ); }; diff --git a/plugins/scaffolder/src/types.ts b/plugins/scaffolder/src/types.ts index e821d8fa9c..0a34450678 100644 --- a/plugins/scaffolder/src/types.ts +++ b/plugins/scaffolder/src/types.ts @@ -66,12 +66,14 @@ export type ListActionsResponse = Array<{ }>; type OutputLink = { - url: string; title?: string; icon?: string; + url?: string; + entityRef?: string; }; export type TaskOutput = { + /** @deprecated use the `links` property to link out to relevant resources */ entityRef?: string; /** @deprecated use the `links` property to link out to relevant resources */ remoteUrl?: string; From 160daa72bbf1a50864b807a5ac325f8a81004869 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Mon, 7 Jun 2021 10:13:09 +0200 Subject: [PATCH 15/18] Fix tests for if condition in schema Signed-off-by: Oliver Sand --- .../src/kinds/TemplateEntityV1beta2.test.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/packages/catalog-model/src/kinds/TemplateEntityV1beta2.test.ts b/packages/catalog-model/src/kinds/TemplateEntityV1beta2.test.ts index 2571cc947e..0be0289d84 100644 --- a/packages/catalog-model/src/kinds/TemplateEntityV1beta2.test.ts +++ b/packages/catalog-model/src/kinds/TemplateEntityV1beta2.test.ts @@ -145,7 +145,17 @@ describe('templateEntityV1beta2Validator', () => { }); it('accepts boolean in if', async () => { - (entity as any).spec.if = true; + (entity as any).spec.steps[0].if = true; await expect(validator.check(entity)).resolves.toBe(true); }); + + it('accepts emty if', async () => { + (entity as any).spec.steps[0].if = ''; + await expect(validator.check(entity)).resolves.toBe(true); + }); + + it('rejects wrong type if', async () => { + (entity as any).spec.steps[0].if = 5; + await expect(validator.check(entity)).rejects.toThrow(/if/); + }); }); From 69f9a35c94866e9e0662830411d34456aa90913d Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Tue, 8 Jun 2021 09:56:30 +0200 Subject: [PATCH 16/18] Fix template kind tests Signed-off-by: Oliver Sand --- .../catalog-model/src/kinds/TemplateEntityV1beta2.test.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/catalog-model/src/kinds/TemplateEntityV1beta2.test.ts b/packages/catalog-model/src/kinds/TemplateEntityV1beta2.test.ts index 0be0289d84..b06a71c8da 100644 --- a/packages/catalog-model/src/kinds/TemplateEntityV1beta2.test.ts +++ b/packages/catalog-model/src/kinds/TemplateEntityV1beta2.test.ts @@ -140,7 +140,7 @@ describe('templateEntityV1beta2Validator', () => { }); it('accepts missing if', async () => { - delete (entity as any).spec.if; + delete (entity as any).spec.steps[0].if; await expect(validator.check(entity)).resolves.toBe(true); }); @@ -149,7 +149,7 @@ describe('templateEntityV1beta2Validator', () => { await expect(validator.check(entity)).resolves.toBe(true); }); - it('accepts emty if', async () => { + it('accepts empty if', async () => { (entity as any).spec.steps[0].if = ''; await expect(validator.check(entity)).resolves.toBe(true); }); From fff062301c7dab5a53a2905a885bbd518c94da72 Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Tue, 8 Jun 2021 09:57:20 +0200 Subject: [PATCH 17/18] Rename publish:log to debug:log Signed-off-by: Oliver Sand --- .changeset/slimy-games-brake.md | 2 +- .../sample-templates/v1beta2-demo/template.yaml | 2 +- .../actions/builtin/createBuiltinActions.ts | 4 ++-- .../scaffolder/actions/builtin/debug/index.ts | 17 +++++++++++++++++ .../builtin/{publish => debug}/log.test.ts | 11 ++++++----- .../actions/builtin/{publish => debug}/log.ts | 9 ++++----- .../src/scaffolder/actions/builtin/index.ts | 3 ++- .../scaffolder/actions/builtin/publish/index.ts | 7 +++---- 8 files changed, 36 insertions(+), 19 deletions(-) create mode 100644 plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/index.ts rename plugins/scaffolder-backend/src/scaffolder/actions/builtin/{publish => debug}/log.test.ts (89%) rename plugins/scaffolder-backend/src/scaffolder/actions/builtin/{publish => debug}/log.ts (91%) diff --git a/.changeset/slimy-games-brake.md b/.changeset/slimy-games-brake.md index 210d9f4e3a..a8fafb09f3 100644 --- a/.changeset/slimy-games-brake.md +++ b/.changeset/slimy-games-brake.md @@ -2,4 +2,4 @@ '@backstage/plugin-scaffolder-backend': patch --- -Add `publish:log` action for debugging. +Add `debug:log` action for debugging. diff --git a/plugins/scaffolder-backend/sample-templates/v1beta2-demo/template.yaml b/plugins/scaffolder-backend/sample-templates/v1beta2-demo/template.yaml index 86f41b43d2..b3228a20d0 100644 --- a/plugins/scaffolder-backend/sample-templates/v1beta2-demo/template.yaml +++ b/plugins/scaffolder-backend/sample-templates/v1beta2-demo/template.yaml @@ -94,7 +94,7 @@ spec: - name: Results if: '{{ parameters.dryRun }}' - action: publish:log + action: debug:log input: listWorkspace: true diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts index 850b289d3b..b4fb37de8f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/createBuiltinActions.ts @@ -19,6 +19,7 @@ import { CatalogApi } from '@backstage/catalog-client'; import { ScmIntegrations } from '@backstage/integration'; import { TemplaterBuilder } from '../../stages'; import { createCatalogRegisterAction } from './catalog'; +import { createDebugLogAction } from './debug'; import { createFetchCookiecutterAction, createFetchPlainAction } from './fetch'; import { createPublishAzureAction, @@ -26,7 +27,6 @@ import { createPublishGithubAction, createPublishGithubPullRequestAction, createPublishGitlabAction, - createPublishLogAction, } from './publish'; export const createBuiltinActions = (options: { @@ -62,7 +62,7 @@ export const createBuiltinActions = (options: { createPublishAzureAction({ integrations, }), - createPublishLogAction(), + createDebugLogAction(), createCatalogRegisterAction({ catalogClient, integrations }), ]; }; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/index.ts new file mode 100644 index 0000000000..ebdf13d11c --- /dev/null +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/index.ts @@ -0,0 +1,17 @@ +/* + * Copyright 2021 Spotify AB + * + * 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. + */ + +export { createDebugLogAction } from './log'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/log.test.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts similarity index 89% rename from plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/log.test.ts rename to plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts index f554f480e5..720cd75300 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/log.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.test.ts @@ -18,9 +18,10 @@ import { getVoidLogger } from '@backstage/backend-common'; import mock from 'mock-fs'; import os from 'os'; import { Writable } from 'stream'; -import { createPublishLogAction } from './log'; +import { createDebugLogAction } from './log'; +import { join } from 'path'; -describe('publish:log', () => { +describe('debug:log', () => { const logStream = ({ write: jest.fn(), } as jest.Mocked>) as jest.Mocked; @@ -36,7 +37,7 @@ describe('publish:log', () => { createTemporaryDirectory: jest.fn().mockResolvedValue(mockTmpDir), }; - const action = createPublishLogAction(); + const action = createDebugLogAction(); beforeEach(() => { mock({ @@ -68,10 +69,10 @@ describe('publish:log', () => { expect(logStream.write).toBeCalledTimes(1); expect(logStream.write).toBeCalledWith( - expect.stringContaining('./README.md'), + expect.stringContaining('README.md'), ); expect(logStream.write).toBeCalledWith( - expect.stringContaining('a-directory/index.md'), + expect.stringContaining(join('a-directory', 'index.md')), ); }); diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/log.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts similarity index 91% rename from plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/log.ts rename to plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts index 7bff980fad..d3c5a2ea1d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/log.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/debug/log.ts @@ -22,9 +22,9 @@ import { createTemplateAction } from '../../createTemplateAction'; * This task is useful for local development and testing of both the scaffolder * and scaffolder templates. */ -export function createPublishLogAction() { +export function createDebugLogAction() { return createTemplateAction<{ message?: string; listWorkspace?: boolean }>({ - id: 'publish:log', + id: 'debug:log', description: 'Writes a message into the log or list all files in the workspace.', schema: { @@ -43,16 +43,15 @@ export function createPublishLogAction() { }, }, async handler(ctx) { - const files = await recursiveReadDir(ctx.workspacePath); - if (ctx.input?.message) { ctx.logStream.write(ctx.input.message); } if (ctx.input?.listWorkspace) { + const files = await recursiveReadDir(ctx.workspacePath); ctx.logStream.write( `Workspace:\n${files - .map(f => ` - ./${relative(ctx.workspacePath, f)}`) + .map(f => ` - ${relative(ctx.workspacePath, f)}`) .join('\n')}`, ); } diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts index e4281c172b..6218b7f213 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/index.ts @@ -15,6 +15,7 @@ */ export * from './catalog'; +export { createBuiltinActions } from './createBuiltinActions'; +export * from './debug'; export * from './fetch'; export * from './publish'; -export { createBuiltinActions } from './createBuiltinActions'; diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts index d17a0f8750..b1b0c39bd3 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/publish/index.ts @@ -14,10 +14,9 @@ * limitations under the License. */ -export { createPublishGithubAction } from './github'; -export { createPublishGithubPullRequestAction } from './githubPullRequest'; export { createPublishAzureAction } from './azure'; -export { createPublishGitlabAction } from './gitlab'; export { createPublishBitbucketAction } from './bitbucket'; export { createPublishFileAction } from './file'; -export { createPublishLogAction } from './log'; +export { createPublishGithubAction } from './github'; +export { createPublishGithubPullRequestAction } from './githubPullRequest'; +export { createPublishGitlabAction } from './gitlab'; From eca511cb37393a55e220d7220de1092c58c9030d Mon Sep 17 00:00:00 2001 From: Oliver Sand Date: Tue, 8 Jun 2021 09:57:35 +0200 Subject: [PATCH 18/18] Fix TaskWorker tests Signed-off-by: Oliver Sand --- .../src/scaffolder/tasks/TaskWorker.test.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts index 7136998aa5..c6ad1a8105 100644 --- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts +++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts @@ -189,7 +189,7 @@ describe('TaskWorker', () => { id: 'conditional', name: 'conditional', action: 'test-action', - if: '"{{ steps.test.output.testOutput }}"', + if: '{{ steps.test.output.testOutput }}', }, ], output: { @@ -226,8 +226,7 @@ describe('TaskWorker', () => { }, ], output: { - result: - '{{#if steps.conditional}}{{ steps.conditional.output.testOutput }}{{/if}}', + result: '{{ steps.conditional.output.testOutput }}', }, values: {}, });