diff --git a/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.test.ts b/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.test.ts
index 2532293acf..7ae4b6b5a0 100644
--- a/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.test.ts
+++ b/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.test.ts
@@ -23,7 +23,6 @@ import {
SystemEntity,
UserEntity,
} from '@backstage/catalog-model';
-import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common';
import { BuiltinKindsEntityProcessor } from './BuiltinKindsEntityProcessor';
describe('BuiltinKindsEntityProcessor', () => {
@@ -539,38 +538,5 @@ describe('BuiltinKindsEntityProcessor', () => {
},
});
});
- it('generates relations for template entities', async () => {
- const entity: TemplateEntityV1beta2 = {
- apiVersion: 'backstage.io/v1beta2',
- kind: 'Template',
- metadata: { name: 'n' },
- spec: {
- parameters: {},
- steps: [],
- type: 'service',
- owner: 'o',
- },
- };
-
- await processor.postProcessEntity(entity, location, emit);
-
- expect(emit).toBeCalledTimes(2);
- expect(emit).toBeCalledWith({
- type: 'relation',
- relation: {
- source: { kind: 'Group', namespace: 'default', name: 'o' },
- type: 'ownerOf',
- target: { kind: 'Template', namespace: 'default', name: 'n' },
- },
- });
- expect(emit).toBeCalledWith({
- type: 'relation',
- relation: {
- source: { kind: 'Template', namespace: 'default', name: 'n' },
- type: 'ownedBy',
- target: { kind: 'Group', namespace: 'default', name: 'o' },
- },
- });
- });
});
});
diff --git a/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.ts b/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.ts
index 9e420b9d65..4e8cf3fef7 100644
--- a/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.ts
+++ b/plugins/catalog-backend/src/modules/core/BuiltinKindsEntityProcessor.ts
@@ -48,10 +48,6 @@ import {
UserEntity,
userEntityV1alpha1Validator,
} from '@backstage/catalog-model';
-import {
- TemplateEntityV1beta2,
- templateEntityV1beta2Validator,
-} from '@backstage/plugin-scaffolder-common';
import {
CatalogProcessor,
CatalogProcessorEmit,
@@ -67,7 +63,6 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor {
resourceEntityV1alpha1Validator,
groupEntityV1alpha1Validator,
locationEntityV1alpha1Validator,
- templateEntityV1beta2Validator,
userEntityV1alpha1Validator,
systemEntityV1alpha1Validator,
domainEntityV1alpha1Validator,
@@ -135,19 +130,6 @@ export class BuiltinKindsEntityProcessor implements CatalogProcessor {
}
}
- /*
- * Emit relations for the Template kind
- */
- if (entity.kind === 'Template') {
- const template = entity as TemplateEntityV1beta2;
- doEmit(
- template.spec.owner,
- { defaultKind: 'Group', defaultNamespace: selfRef.namespace },
- RELATION_OWNED_BY,
- RELATION_OWNER_OF,
- );
- }
-
/*
* Emit relations for the Component kind
*/
diff --git a/plugins/catalog-react/src/filters.test.ts b/plugins/catalog-react/src/filters.test.ts
index cdc2ce2150..ba31e21122 100644
--- a/plugins/catalog-react/src/filters.test.ts
+++ b/plugins/catalog-react/src/filters.test.ts
@@ -15,7 +15,7 @@
*/
import { Entity } from '@backstage/catalog-model';
-import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common';
+import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import { EntityTextFilter } from './filters';
const entities: Entity[] = [
@@ -37,9 +37,9 @@ const entities: Entity[] = [
},
];
-const templates: TemplateEntityV1beta2[] = [
+const templates: TemplateEntityV1beta3[] = [
{
- apiVersion: 'backstage.io/v1beta2',
+ apiVersion: 'scaffolder.backstage.io/v1beta3',
kind: 'Template',
metadata: {
name: 'react-app',
@@ -52,7 +52,7 @@ const templates: TemplateEntityV1beta2[] = [
},
},
{
- apiVersion: 'backstage.io/v1beta2',
+ apiVersion: 'scaffolder.backstage.io/v1beta3',
kind: 'Template',
metadata: {
name: 'gRPC service',
diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/types.ts b/plugins/scaffolder-backend/src/scaffolder/actions/types.ts
index 55ef68423e..f73c4dce3f 100644
--- a/plugins/scaffolder-backend/src/scaffolder/actions/types.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/actions/types.ts
@@ -19,10 +19,7 @@ import { Writable } from 'stream';
import { JsonValue, JsonObject } from '@backstage/types';
import { Schema } from 'jsonschema';
import { TaskSecrets } from '../tasks/types';
-import {
- TemplateInfo,
- TemplateMetadata,
-} from '@backstage/plugin-scaffolder-common';
+import { TemplateInfo } from '@backstage/plugin-scaffolder-common';
/**
* ActionContext is passed into scaffolder actions.
@@ -47,10 +44,6 @@ export type ActionContext = {
*/
createTemporaryDirectory(): Promise;
- /**
- * @deprecated please use templateInfo instead
- */
- metadata?: TemplateMetadata;
templateInfo?: TemplateInfo;
};
diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.test.ts
deleted file mode 100644
index dcad1fc5d0..0000000000
--- a/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.test.ts
+++ /dev/null
@@ -1,441 +0,0 @@
-/*
- * 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 mockFs from 'mock-fs';
-import * as winston from 'winston';
-import { createTemplateAction, TemplateActionRegistry } from '../actions';
-import { ScmIntegrations } from '@backstage/integration';
-import { ConfigReader } from '@backstage/config';
-import { getVoidLogger } from '@backstage/backend-common';
-import { HandlebarsWorkflowRunner } from './HandlebarsWorkflowRunner';
-import { TaskContext } from './types';
-import { RepoSpec } from '../actions/builtin/publish/util';
-import { TaskSpec } from '@backstage/plugin-scaffolder-common';
-
-describe('LegacyWorkflowRunner', () => {
- let runner: HandlebarsWorkflowRunner;
- const logger = getVoidLogger();
- let actionRegistry = new TemplateActionRegistry();
- let fakeActionHandler: jest.Mock;
-
- const integrations = ScmIntegrations.fromConfig(
- new ConfigReader({
- integrations: {
- github: [{ host: 'github.com', token: 'token' }],
- },
- }),
- );
-
- const createMockTaskWithSpec = (spec: TaskSpec): TaskContext => ({
- spec,
- complete: async () => {},
- done: false,
- emitLog: async () => {},
- getWorkspaceName: () => Promise.resolve('test-workspace'),
- });
-
- beforeEach(() => {
- winston.format.simple(); // put logform the require cache before mocking fs
- mockFs({
- '/tmp': mockFs.directory(),
- });
-
- actionRegistry = new TemplateActionRegistry();
- actionRegistry.register({
- id: 'test-action',
- handler: async ctx => {
- ctx.output('testOutput', 'mockOutputData');
- ctx.output('badOutput', false);
- },
- });
- fakeActionHandler = jest.fn();
- actionRegistry.register({
- id: 'test-metadata-action',
- handler: fakeActionHandler,
- });
-
- runner = new HandlebarsWorkflowRunner({
- actionRegistry,
- integrations,
- workingDirectory: '/tmp',
- logger,
- });
- });
-
- afterEach(() => {
- mockFs.restore();
- });
-
- it('should fail when the action does not exist', async () => {
- const task = createMockTaskWithSpec({
- apiVersion: 'backstage.io/v1beta2',
- steps: [{ id: 'test', name: 'test', action: 'not-found-action' }],
- output: {
- result: '{{ steps.test.output.testOutput }}',
- },
- values: {},
- });
-
- await expect(() => runner.execute(task)).rejects.toThrow(
- /Template action with ID 'not-found-action' is not registered/,
- );
- });
-
- it('should pass metadata through', async () => {
- const entityRef = `template:default/templateName`;
- const task = createMockTaskWithSpec({
- apiVersion: 'backstage.io/v1beta2',
- steps: [
- {
- id: 'test',
- name: 'name',
- action: 'test-metadata-action',
- input: { foo: 1 },
- },
- ],
- output: {},
- values: {},
- templateInfo: { entityRef },
- });
-
- await runner.execute(task);
-
- expect(fakeActionHandler.mock.calls[0][0].templateInfo).toEqual({
- entityRef,
- });
- });
-
- describe('templating', () => {
- it('should template the output', async () => {
- const task = createMockTaskWithSpec({
- apiVersion: 'backstage.io/v1beta2',
- steps: [{ id: 'test', name: 'test', action: 'test-action' }],
- output: {
- result: '{{ steps.test.output.testOutput }}',
- },
- values: {},
- });
-
- const { output } = await runner.execute(task);
-
- expect(output.result).toBe('mockOutputData');
- });
-
- it('should template the input', async () => {
- const inputAction = createTemplateAction<{
- name: string;
- }>({
- id: 'test-input',
- schema: {
- input: {
- type: 'object',
- required: ['name'],
- properties: {
- name: {
- title: 'name',
- description: 'Enter name',
- type: 'string',
- },
- },
- },
- },
- async handler(ctx) {
- if (ctx.input.name !== 'mockOutputData') {
- throw new Error(
- `expected name to be "mockOutputData" got ${ctx.input.name}`,
- );
- }
- },
- });
- actionRegistry.register(inputAction);
-
- const task = createMockTaskWithSpec({
- apiVersion: 'backstage.io/v1beta2',
- steps: [
- { id: 'test', name: 'test', action: 'test-action' },
- {
- id: 'test-input',
- name: 'test-input',
- action: 'test-input',
- input: {
- name: '{{ steps.test.output.testOutput }}',
- },
- },
- ],
- output: {
- result: '{{ steps.test.output.testOutput }}',
- },
- values: {},
- });
-
- const { output } = await runner.execute(task);
-
- expect(output.result).toBe('mockOutputData');
- });
- });
-
- describe('conditionals', () => {
- it('should execute steps conditionally', async () => {
- const task = createMockTaskWithSpec({
- apiVersion: 'backstage.io/v1beta2',
- 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 { output } = await runner.execute(task);
-
- expect(output.result).toBe('mockOutputData');
- });
-
- it('should execute steps conditionally with eq helper', async () => {
- const task = createMockTaskWithSpec({
- apiVersion: 'backstage.io/v1beta2',
- steps: [
- { id: 'test', name: 'test', action: 'test-action' },
- {
- id: 'conditional',
- name: 'conditional',
- action: 'test-action',
- if: '{{ eq steps.test.output.testOutput "mockOutputData" }}',
- },
- ],
- output: {
- result: '{{ steps.conditional.output.testOutput }}',
- },
- values: {},
- });
-
- const { output } = await runner.execute(task);
-
- expect(output.result).toBe('mockOutputData');
- });
-
- it('should skip test conditionally', async () => {
- const task = createMockTaskWithSpec({
- apiVersion: 'backstage.io/v1beta2',
- steps: [
- { id: 'test', name: 'test', action: 'test-action' },
- {
- id: 'conditional',
- name: 'conditional',
- action: 'test-action',
- if: '{{ steps.test.output.badOutput }}',
- },
- ],
- output: {
- result: '{{ steps.conditional.output.testOutput }}',
- },
- values: {},
- });
-
- const { output } = await runner.execute(task);
-
- expect(output.result).toBeUndefined();
- });
- });
-
- describe('parsing', () => {
- it('should parse strings as objects if possible', async () => {
- const inputAction = createTemplateAction<{
- address: { line1: string };
- list: string[];
- address2: string;
- }>({
- id: 'test-input',
- schema: {
- input: {
- type: 'object',
- required: ['address'],
- properties: {
- address: {
- title: 'address',
- description: 'Enter name',
- type: 'object',
- properties: {
- line1: {
- type: 'string',
- },
- },
- },
- address2: {
- type: 'string',
- },
- list: {
- type: 'array',
- items: {
- type: 'string',
- },
- },
- },
- },
- },
- async handler(ctx) {
- if (ctx.input.list.length !== 1) {
- throw new Error(
- `expected list to have length "1" got ${ctx.input.list.length}`,
- );
- }
- if (ctx.input.address.line1 !== 'line 1') {
- throw new Error(
- `expected address.line1 to be "line 1" got ${ctx.input.address.line1}`,
- );
- }
-
- if (ctx.input.address2 !== '{"not valid"}') {
- throw new Error(
- `expected address2 to be "{"not valid"}" got ${ctx.input.address2}`,
- );
- }
- ctx.output('address', ctx.input.address.line1);
- },
- });
- actionRegistry.register(inputAction);
-
- const task = createMockTaskWithSpec({
- apiVersion: 'backstage.io/v1beta2',
- steps: [
- {
- id: 'test-input',
- name: 'test-input',
- action: 'test-input',
- input: {
- address: JSON.stringify({ line1: 'line 1' }),
- list: JSON.stringify(['hey!']),
- address2: '{"not valid"}',
- },
- },
- ],
- output: {
- result: '{{ steps.test-input.output.address }}',
- },
- values: {},
- });
-
- const { output } = await runner.execute(task);
-
- expect(output.result).toBe('line 1');
- });
-
- it('should provide a parseRepoUrl helper', async () => {
- const inputAction = createTemplateAction<{
- destination: RepoSpec;
- }>({
- id: 'test-input',
- schema: {
- input: {
- type: 'object',
- required: ['destination'],
- properties: {
- destination: {
- title: 'destination',
- type: 'object',
- properties: {
- repo: {
- type: 'string',
- },
- host: {
- type: 'string',
- },
- owner: {
- type: 'string',
- },
- organization: {
- type: 'string',
- },
- workspace: {
- type: 'string',
- },
- project: {
- type: 'string',
- },
- },
- },
- },
- },
- },
- async handler(ctx) {
- ctx.output('host', ctx.input.destination.host);
- ctx.output('repo', ctx.input.destination.repo);
-
- if (ctx.input.destination.owner) {
- ctx.output('owner', ctx.input.destination.owner);
- }
-
- if (ctx.input.destination.host !== 'github.com') {
- throw new Error(
- `expected host to be "github.com" got ${ctx.input.destination.host}`,
- );
- }
-
- if (ctx.input.destination.repo !== 'repo') {
- throw new Error(
- `expected repo to be "repo" got ${ctx.input.destination.repo}`,
- );
- }
-
- if (
- ctx.input.destination.owner &&
- ctx.input.destination.owner !== 'owner'
- ) {
- throw new Error(
- `expected repo to be "owner" got ${ctx.input.destination.owner}`,
- );
- }
- },
- });
- actionRegistry.register(inputAction);
-
- const task = createMockTaskWithSpec({
- apiVersion: 'backstage.io/v1beta2',
- steps: [
- {
- id: 'test-input',
- name: 'test-input',
- action: 'test-input',
- input: {
- destination: '{{ parseRepoUrl parameters.repoUrl }}',
- },
- },
- ],
- output: {
- host: '{{ steps.test-input.output.host }}',
- repo: '{{ steps.test-input.output.repo }}',
- owner: '{{ steps.test-input.output.owner }}',
- },
- values: {
- repoUrl: 'github.com?repo=repo&owner=owner',
- },
- });
-
- const { output } = await runner.execute(task);
-
- expect(output.host).toBe('github.com');
- expect(output.repo).toBe('repo');
- expect(output.owner).toBe('owner');
- });
- });
-});
diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts
deleted file mode 100644
index c82da51ead..0000000000
--- a/plugins/scaffolder-backend/src/scaffolder/tasks/HandlebarsWorkflowRunner.ts
+++ /dev/null
@@ -1,308 +0,0 @@
-/*
- * 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 { TaskContext, WorkflowRunner, WorkflowResponse } from './types';
-import * as Handlebars from 'handlebars';
-import { TemplateActionRegistry } from '..';
-import { ScmIntegrations } from '@backstage/integration';
-import { parseRepoUrl } from '../actions/builtin/publish/util';
-import { isTruthy } from './helper';
-import { PassThrough } from 'stream';
-import * as winston from 'winston';
-import { Logger } from 'winston';
-import path from 'path';
-import fs from 'fs-extra';
-import { validate as validateJsonSchema } from 'jsonschema';
-import { JsonObject, JsonValue } from '@backstage/types';
-import { InputError } from '@backstage/errors';
-import { TaskSpec, TaskSpecV1beta2 } from '@backstage/plugin-scaffolder-common';
-
-type Options = {
- workingDirectory: string;
- actionRegistry: TemplateActionRegistry;
- integrations: ScmIntegrations;
- logger: Logger;
-};
-
-const isValidTaskSpec = (taskSpec: TaskSpec): taskSpec is TaskSpecV1beta2 =>
- taskSpec.apiVersion === 'backstage.io/v1beta2';
-/**
- * This is the legacy workflow runner, which supports handlebars. This entire implementation will be replaced
- * with the default workflow runner interface in the future so this entire thing can go bye bye.
- */
-export class HandlebarsWorkflowRunner implements WorkflowRunner {
- private readonly handlebars: typeof Handlebars;
-
- constructor(private readonly options: Options) {
- this.handlebars = Handlebars.create();
-
- // TODO(blam): this should be a public facing API but it's a little
- // scary right now, so we're going to lock it off like the component API is
- // in the frontend until we can work out a nice way to do it.
- this.handlebars.registerHelper('parseRepoUrl', repoUrl => {
- return JSON.stringify(parseRepoUrl(repoUrl, this.options.integrations));
- });
-
- this.handlebars.registerHelper('projectSlug', repoUrl => {
- const { owner, repo } = parseRepoUrl(repoUrl, this.options.integrations);
- return `${owner}/${repo}`;
- });
-
- this.handlebars.registerHelper('json', obj => JSON.stringify(obj));
-
- this.handlebars.registerHelper('not', value => !isTruthy(value));
-
- this.handlebars.registerHelper('eq', (a, b) => a === b);
- }
-
- async execute(task: TaskContext): Promise {
- if (!isValidTaskSpec(task.spec)) {
- throw new InputError(`Task spec is not a valid v1beta2 task spec`);
- }
-
- const { actionRegistry } = this.options;
-
- const workspacePath = path.join(
- this.options.workingDirectory,
- await task.getWorkspaceName(),
- );
- try {
- await fs.ensureDir(workspacePath);
- await task.emitLog(
- `Starting up task with ${task.spec.steps.length} steps`,
- );
-
- const templateCtx: {
- parameters: JsonObject;
- steps: {
- [stepName: string]: { output: { [outputName: string]: JsonValue } };
- };
- } = { parameters: task.spec.values, steps: {} };
-
- for (const step of task.spec.steps) {
- const metadata = { stepId: step.id };
- try {
- const taskLogger = winston.createLogger({
- level: process.env.LOG_LEVEL || 'info',
- format: winston.format.combine(
- winston.format.colorize(),
- winston.format.timestamp(),
- winston.format.simple(),
- ),
- defaultMeta: {},
- });
-
- const stream = new PassThrough();
- stream.on('data', async data => {
- const message = data.toString().trim();
- if (message?.length > 1) {
- await task.emitLog(message, metadata);
- }
- });
-
- 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,
- data: false,
- preventIndent: true,
- })(templateCtx);
-
- // If it's just an empty string, treat it as undefined
- if (templated === '') {
- return undefined;
- }
-
- 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',
- });
-
- const action = actionRegistry.get(step.action);
- if (!action) {
- throw new Error(`Action '${step.action}' does not exist`);
- }
-
- const input =
- step.input &&
- JSON.parse(JSON.stringify(step.input), (_key, value) => {
- if (typeof value === 'string') {
- const templated = this.handlebars.compile(value, {
- noEscape: true,
- data: false,
- preventIndent: true,
- })(templateCtx);
-
- // 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;
- });
-
- if (action.schema?.input) {
- const validateResult = validateJsonSchema(
- input,
- action.schema.input,
- );
- if (!validateResult.valid) {
- const errors = validateResult.errors.join(', ');
- throw new InputError(
- `Invalid input passed to action ${action.id}, ${errors}`,
- );
- }
- }
-
- const stepOutputs: { [name: string]: JsonValue } = {};
-
- // Keep track of all tmp dirs that are created by the action so we can remove them after
- const tmpDirs = new Array();
-
- this.options.logger.debug(`Running ${action.id} with input`, {
- input: JSON.stringify(input, null, 2),
- });
-
- await action.handler({
- // deprecated in favor of templateInfo.baseUrl
- baseUrl: task.spec.baseUrl,
- logger: taskLogger,
- logStream: stream,
- input,
- secrets: task.secrets ?? {},
- workspacePath,
- async createTemporaryDirectory() {
- const tmpDir = await fs.mkdtemp(
- `${workspacePath}_step-${step.id}-`,
- );
- tmpDirs.push(tmpDir);
- return tmpDir;
- },
- output(name: string, value: JsonValue) {
- stepOutputs[name] = value;
- },
- // deprecated in favor of templateInfo
- metadata: task.spec.metadata,
- templateInfo: task.spec.templateInfo,
- });
-
- // Remove all temporary directories that were created when executing the action
- for (const tmpDir of tmpDirs) {
- await fs.remove(tmpDir);
- }
-
- templateCtx.steps[step.id] = { output: stepOutputs };
-
- await task.emitLog(`Finished step ${step.name}`, {
- ...metadata,
- status: 'completed',
- });
- } catch (error) {
- await task.emitLog(String(error.stack), {
- ...metadata,
- status: 'failed',
- });
- throw error;
- }
- }
-
- const output = JSON.parse(
- JSON.stringify(task.spec.output),
- (_key, value) => {
- if (typeof value === 'string') {
- 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;
- },
- );
-
- return { output };
- } finally {
- if (workspacePath) {
- await fs.remove(workspacePath);
- }
- }
- }
-}
diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts
index fb9da56c22..0bd5a3c840 100644
--- a/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/tasks/NunjucksWorkflowRunner.ts
@@ -257,8 +257,6 @@ export class NunjucksWorkflowRunner implements WorkflowRunner {
const stepOutput: { [outputName: string]: JsonValue } = {};
await action.handler({
- // deprecated in favourof templateInfo.baseUrl
- baseUrl: task.spec.baseUrl,
input,
secrets: task.secrets ?? {},
logger: taskLogger,
@@ -274,8 +272,6 @@ export class NunjucksWorkflowRunner implements WorkflowRunner {
output(name: string, value: JsonValue) {
stepOutput[name] = value;
},
- // deprecated in favour of templateInfo
- metadata: task.spec.metadata,
templateInfo: task.spec.templateInfo,
});
diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts
index 9ff6b9c8a1..1a099e4c53 100644
--- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.test.ts
@@ -19,16 +19,10 @@ import { ConfigReader } from '@backstage/config';
import { DatabaseTaskStore } from './DatabaseTaskStore';
import { StorageTaskBroker } from './StorageTaskBroker';
import { TaskWorker } from './TaskWorker';
-import { HandlebarsWorkflowRunner } from './HandlebarsWorkflowRunner';
import { ScmIntegrations } from '@backstage/integration';
import { TemplateActionRegistry } from '../actions';
import { NunjucksWorkflowRunner } from './NunjucksWorkflowRunner';
-jest.mock('./HandlebarsWorkflowRunner');
-const MockedHandlebarsWorkflowRunner =
- HandlebarsWorkflowRunner as jest.Mock;
-MockedHandlebarsWorkflowRunner.mockImplementation();
-
jest.mock('./NunjucksWorkflowRunner');
const MockedNunjucksWorkflowRunner =
NunjucksWorkflowRunner as jest.Mock;
@@ -58,10 +52,6 @@ describe('TaskWorker', () => {
const actionRegistry: TemplateActionRegistry = {} as TemplateActionRegistry;
const workingDirectory = '/tmp/scaffolder';
- const handlebarsWorkflowRunner: HandlebarsWorkflowRunner = {
- execute: jest.fn(),
- } as unknown as HandlebarsWorkflowRunner;
-
const workflowRunner: NunjucksWorkflowRunner = {
execute: jest.fn(),
} as unknown as NunjucksWorkflowRunner;
@@ -72,47 +62,11 @@ describe('TaskWorker', () => {
beforeEach(() => {
jest.resetAllMocks();
- MockedHandlebarsWorkflowRunner.mockImplementation(
- () => handlebarsWorkflowRunner,
- );
MockedNunjucksWorkflowRunner.mockImplementation(() => workflowRunner);
});
const logger = getVoidLogger();
- it('should call the legacy workflow runner when the apiVersion is not beta3', async () => {
- const broker = new StorageTaskBroker(storage, logger);
- const taskWorker = await TaskWorker.create({
- logger,
- workingDirectory,
- integrations,
- taskBroker: broker,
- actionRegistry,
- });
-
- await broker.dispatch({
- spec: {
- apiVersion: 'backstage.io/v1beta2',
- steps: [{ id: 'test', name: 'test', action: 'not-found-action' }],
- output: {
- result: '{{ steps.test.output.testOutput }}',
- },
- values: {},
- },
- });
-
- const task = await broker.claim();
- await taskWorker.runOneTask(task);
-
- expect(MockedHandlebarsWorkflowRunner).toBeCalledWith({
- actionRegistry,
- integrations,
- logger,
- workingDirectory,
- });
- expect(handlebarsWorkflowRunner.execute).toHaveBeenCalled();
- });
-
it('should call the default workflow runner when the apiVersion is beta3', async () => {
const broker = new StorageTaskBroker(storage, logger);
const taskWorker = await TaskWorker.create({
diff --git a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts
index 8de9359806..9defd71c49 100644
--- a/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts
+++ b/plugins/scaffolder-backend/src/scaffolder/tasks/TaskWorker.ts
@@ -15,7 +15,6 @@
*/
import { TaskContext, TaskBroker, WorkflowRunner } from './types';
-import { HandlebarsWorkflowRunner } from './HandlebarsWorkflowRunner';
import { NunjucksWorkflowRunner } from './NunjucksWorkflowRunner';
import { Logger } from 'winston';
import { TemplateActionRegistry } from '../actions';
@@ -31,7 +30,6 @@ import { TemplateFilter } from '../../lib/templating/SecureTemplater';
export type TaskWorkerOptions = {
taskBroker: TaskBroker;
runners: {
- legacyWorkflowRunner: HandlebarsWorkflowRunner;
workflowRunner: WorkflowRunner;
};
};
@@ -68,13 +66,6 @@ export class TaskWorker {
additionalTemplateFilters,
} = options;
- const legacyWorkflowRunner = new HandlebarsWorkflowRunner({
- logger,
- actionRegistry,
- integrations,
- workingDirectory,
- });
-
const workflowRunner = new NunjucksWorkflowRunner({
actionRegistry,
integrations,
@@ -85,7 +76,7 @@ export class TaskWorker {
return new TaskWorker({
taskBroker: taskBroker,
- runners: { legacyWorkflowRunner, workflowRunner },
+ runners: { workflowRunner },
});
}
@@ -100,10 +91,15 @@ export class TaskWorker {
async runOneTask(task: TaskContext) {
try {
- const { output } =
- task.spec.apiVersion === 'scaffolder.backstage.io/v1beta3'
- ? await this.options.runners.workflowRunner.execute(task)
- : await this.options.runners.legacyWorkflowRunner.execute(task);
+ if (task.spec.apiVersion !== 'scaffolder.backstage.io/v1beta3') {
+ throw new Error(
+ `Unsupported Template apiVersion ${task.spec.apiVersion}`,
+ );
+ }
+
+ const { output } = await this.options.runners.workflowRunner.execute(
+ task,
+ );
await task.complete('completed', { output });
} catch (error) {
diff --git a/plugins/scaffolder-backend/src/service/helpers.ts b/plugins/scaffolder-backend/src/service/helpers.ts
index a7148dac40..23200d6aac 100644
--- a/plugins/scaffolder-backend/src/service/helpers.ts
+++ b/plugins/scaffolder-backend/src/service/helpers.ts
@@ -26,10 +26,7 @@ import {
} from '@backstage/catalog-model';
import { Config } from '@backstage/config';
import { assertError, InputError, NotFoundError } from '@backstage/errors';
-import {
- TemplateEntityV1beta2,
- TemplateEntityV1beta3,
-} from '@backstage/plugin-scaffolder-common';
+import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import fs from 'fs-extra';
import os from 'os';
import { Logger } from 'winston';
@@ -95,7 +92,7 @@ export async function findTemplate(options: {
entityRef: CompoundEntityRef;
token?: string;
catalogApi: CatalogApi;
-}): Promise {
+}): Promise {
const { entityRef, token, catalogApi } = options;
if (entityRef.namespace.toLocaleLowerCase('en-US') !== DEFAULT_NAMESPACE) {
@@ -114,5 +111,5 @@ export async function findTemplate(options: {
);
}
- return template as TemplateEntityV1beta3 | TemplateEntityV1beta2;
+ return template as TemplateEntityV1beta3;
}
diff --git a/plugins/scaffolder-backend/src/service/router.test.ts b/plugins/scaffolder-backend/src/service/router.test.ts
index 1c57153fb0..41bc73c7ae 100644
--- a/plugins/scaffolder-backend/src/service/router.test.ts
+++ b/plugins/scaffolder-backend/src/service/router.test.ts
@@ -35,7 +35,7 @@ import {
UrlReaders,
} from '@backstage/backend-common';
import { CatalogApi } from '@backstage/catalog-client';
-import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common';
+import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import { ConfigReader } from '@backstage/config';
import ObservableImpl from 'zen-observable';
import express from 'express';
@@ -76,8 +76,8 @@ const mockUrlReader = UrlReaders.default({
describe('createRouter', () => {
let app: express.Express;
let taskBroker: TaskBroker;
- const template: TemplateEntityV1beta2 = {
- apiVersion: 'backstage.io/v1beta2',
+ const template: TemplateEntityV1beta3 = {
+ apiVersion: 'scaffolder.backstage.io/v1beta3',
kind: 'Template',
metadata: {
description: 'Create a new CRA website project',
diff --git a/plugins/scaffolder-backend/src/service/router.ts b/plugins/scaffolder-backend/src/service/router.ts
index f7e375b0b2..cf073b551b 100644
--- a/plugins/scaffolder-backend/src/service/router.ts
+++ b/plugins/scaffolder-backend/src/service/router.ts
@@ -22,11 +22,8 @@ import { Config } from '@backstage/config';
import { InputError, NotFoundError } from '@backstage/errors';
import { ScmIntegrations } from '@backstage/integration';
import {
- TemplateEntityV1beta2,
TemplateEntityV1beta3,
- TaskSpecV1beta3,
TaskSpec,
- TaskSpecV1beta2,
} from '@backstage/plugin-scaffolder-common';
import express from 'express';
import Router from 'express-promise-router';
@@ -61,13 +58,8 @@ export interface RouterOptions {
additionalTemplateFilters?: Record;
}
-function isSupportedTemplate(
- entity: TemplateEntityV1beta2 | TemplateEntityV1beta3,
-) {
- return (
- entity.apiVersion === 'backstage.io/v1beta2' ||
- entity.apiVersion === 'scaffolder.backstage.io/v1beta3'
- );
+function isSupportedTemplate(entity: TemplateEntityV1beta3) {
+ return entity.apiVersion === 'scaffolder.backstage.io/v1beta3';
}
/** @public */
@@ -181,64 +173,7 @@ export async function createRouter(
token: getBearerToken(req.headers.authorization),
});
- let taskSpec: TaskSpec;
-
- if (isSupportedTemplate(template)) {
- if (template.apiVersion === 'backstage.io/v1beta2') {
- logger.warn(
- `Scaffolding ${stringifyEntityRef(
- template,
- )} with deprecated apiVersion ${
- template.apiVersion
- }. Please migrate the template to backstage.io/v1beta3. https://backstage.io/docs/features/software-templates/migrating-from-v1beta2-to-v1beta3`,
- );
- }
-
- for (const parameters of [template.spec.parameters ?? []].flat()) {
- const result = validate(values, parameters);
- if (!result.valid) {
- res.status(400).json({ errors: result.errors });
- return;
- }
- }
-
- const baseUrl = getEntityBaseUrl(template);
-
- const baseTaskSpec = {
- baseUrl,
- steps: template.spec.steps.map((step, index) => ({
- ...step,
- id: step.id ?? `step-${index + 1}`,
- name: step.name ?? step.action,
- })),
- output: template.spec.output ?? {},
-
- // deprecated in favour of templateInfo
- metadata: { name: template.metadata?.name },
-
- templateInfo: {
- entityRef: stringifyEntityRef({
- kind,
- namespace,
- name: template.metadata?.name,
- }),
- baseUrl,
- },
- };
-
- taskSpec =
- template.apiVersion === 'backstage.io/v1beta2'
- ? ({
- ...baseTaskSpec,
- apiVersion: template.apiVersion,
- values,
- } as TaskSpecV1beta2)
- : ({
- ...baseTaskSpec,
- apiVersion: template.apiVersion,
- parameters: values,
- } as TaskSpecV1beta3);
- } else {
+ if (!isSupportedTemplate(template)) {
throw new InputError(
`Unsupported apiVersion field in schema entity, ${
(template as Entity).apiVersion
@@ -246,6 +181,35 @@ export async function createRouter(
);
}
+ for (const parameters of [template.spec.parameters ?? []].flat()) {
+ const result = validate(values, parameters);
+ if (!result.valid) {
+ res.status(400).json({ errors: result.errors });
+ return;
+ }
+ }
+
+ const baseUrl = getEntityBaseUrl(template);
+
+ const taskSpec: TaskSpec = {
+ apiVersion: template.apiVersion,
+ steps: template.spec.steps.map((step, index) => ({
+ ...step,
+ id: step.id ?? `step-${index + 1}`,
+ name: step.name ?? step.action,
+ })),
+ output: template.spec.output ?? {},
+ parameters: values,
+ templateInfo: {
+ entityRef: stringifyEntityRef({
+ kind,
+ namespace,
+ name: template.metadata?.name,
+ }),
+ baseUrl,
+ },
+ };
+
const result = await taskBroker.dispatch({
spec: taskSpec,
secrets: {
diff --git a/plugins/scaffolder/src/components/Router.tsx b/plugins/scaffolder/src/components/Router.tsx
index a456f2bdd3..b675fdb357 100644
--- a/plugins/scaffolder/src/components/Router.tsx
+++ b/plugins/scaffolder/src/components/Router.tsx
@@ -17,7 +17,7 @@
import React, { ComponentType } from 'react';
import { Routes, Route, useOutlet } from 'react-router';
import { Entity } from '@backstage/catalog-model';
-import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common';
+import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import { ScaffolderPage } from './ScaffolderPage';
import { TemplatePage } from './TemplatePage';
import { TaskPage } from './TaskPage';
@@ -35,14 +35,14 @@ import { useElementFilter } from '@backstage/core-plugin-api';
export type RouterProps = {
/** @deprecated use components.TemplateCardComponent instead */
TemplateCardComponent?:
- | ComponentType<{ template: TemplateEntityV1beta2 }>
+ | ComponentType<{ template: TemplateEntityV1beta3 }>
| undefined;
/** @deprecated use component.TaskPageComponent instead */
TaskPageComponent?: ComponentType<{}>;
components?: {
TemplateCardComponent?:
- | ComponentType<{ template: TemplateEntityV1beta2 }>
+ | ComponentType<{ template: TemplateEntityV1beta3 }>
| undefined;
TaskPageComponent?: ComponentType<{}>;
};
diff --git a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx
index da9f1f74b8..0b3fccc07c 100644
--- a/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx
+++ b/plugins/scaffolder/src/components/ScaffolderPage/ScaffolderPage.tsx
@@ -24,7 +24,7 @@ import {
SupportButton,
} from '@backstage/core-components';
import { Entity } from '@backstage/catalog-model';
-import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common';
+import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import { useRouteRef } from '@backstage/core-plugin-api';
import {
EntityKindPicker,
@@ -52,7 +52,7 @@ const useStyles = makeStyles(theme => ({
export type ScaffolderPageProps = {
TemplateCardComponent?:
- | ComponentType<{ template: TemplateEntityV1beta2 }>
+ | ComponentType<{ template: TemplateEntityV1beta3 }>
| undefined;
groups?: Array<{
title?: React.ReactNode;
diff --git a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx
index aab5e8348a..f73e32189c 100644
--- a/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx
+++ b/plugins/scaffolder/src/components/TaskPage/TaskPage.tsx
@@ -298,10 +298,7 @@ export const TaskPage = ({ loadingText }: TaskPageProps) => {
return;
}
- const formData =
- taskStream.task!.spec.apiVersion === 'backstage.io/v1beta2'
- ? taskStream.task!.spec.values
- : taskStream.task!.spec.parameters;
+ const formData = taskStream.task!.spec.parameters;
const { name } = parseEntityRef(
taskStream.task!.spec.templateInfo?.entityRef,
diff --git a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx
index 7275aa3a9e..54f8188e3b 100644
--- a/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx
+++ b/plugins/scaffolder/src/components/TemplateCard/TemplateCard.tsx
@@ -14,7 +14,7 @@
* limitations under the License.
*/
import { Entity, RELATION_OWNED_BY } from '@backstage/catalog-model';
-import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common';
+import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import {
ScmIntegrationIcon,
scmIntegrationsApiRef,
@@ -99,7 +99,7 @@ const useDeprecationStyles = makeStyles(theme => ({
}));
export type TemplateCardProps = {
- template: TemplateEntityV1beta2;
+ template: TemplateEntityV1beta3;
deprecated?: boolean;
};
@@ -112,7 +112,7 @@ type TemplateProps = {
};
const getTemplateCardProps = (
- template: TemplateEntityV1beta2,
+ template: TemplateEntityV1beta3,
): TemplateProps & { key: string } => {
return {
key: template.metadata.uid!,
diff --git a/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx b/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx
index 3d9da542de..e3cf6eac05 100644
--- a/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx
+++ b/plugins/scaffolder/src/components/TemplateList/TemplateList.tsx
@@ -16,7 +16,7 @@
import React, { ComponentType } from 'react';
import { Entity, stringifyEntityRef } from '@backstage/catalog-model';
-import { TemplateEntityV1beta2 } from '@backstage/plugin-scaffolder-common';
+import { TemplateEntityV1beta3 } from '@backstage/plugin-scaffolder-common';
import {
Content,
ContentHeader,
@@ -34,7 +34,7 @@ import { TemplateCard } from '../TemplateCard';
*/
export type TemplateListProps = {
TemplateCardComponent?:
- | ComponentType<{ template: TemplateEntityV1beta2 }>
+ | ComponentType<{ template: TemplateEntityV1beta3 }>
| undefined;
group?: {
title?: React.ReactNode;
@@ -106,7 +106,7 @@ export const TemplateList = ({
maybeFilteredEntities.map((template: Entity) => (
))}