@@ -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' },
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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<Input extends JsonObject> = {
|
||||
*/
|
||||
createTemporaryDirectory(): Promise<string>;
|
||||
|
||||
/**
|
||||
* @deprecated please use templateInfo instead
|
||||
*/
|
||||
metadata?: TemplateMetadata;
|
||||
templateInfo?: TemplateInfo;
|
||||
};
|
||||
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -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<WorkflowResponse> {
|
||||
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<string>();
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
});
|
||||
|
||||
|
||||
@@ -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<HandlebarsWorkflowRunner>;
|
||||
MockedHandlebarsWorkflowRunner.mockImplementation();
|
||||
|
||||
jest.mock('./NunjucksWorkflowRunner');
|
||||
const MockedNunjucksWorkflowRunner =
|
||||
NunjucksWorkflowRunner as jest.Mock<NunjucksWorkflowRunner>;
|
||||
@@ -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({
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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<TemplateEntityV1beta3 | TemplateEntityV1beta2> {
|
||||
}): Promise<TemplateEntityV1beta3> {
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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<string, TemplateFilter>;
|
||||
}
|
||||
|
||||
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: {
|
||||
|
||||
@@ -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<{}>;
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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!,
|
||||
|
||||
@@ -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) => (
|
||||
<Card
|
||||
key={stringifyEntityRef(template)}
|
||||
template={template as TemplateEntityV1beta2}
|
||||
template={template as TemplateEntityV1beta3}
|
||||
deprecated={template.apiVersion === 'backstage.io/v1beta2'}
|
||||
/>
|
||||
))}
|
||||
|
||||
Reference in New Issue
Block a user