chore: converting the rest of the actions
Signed-off-by: benjdlambert <ben@blam.sh>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'@backstage/plugin-scaffolder-backend': patch
|
||||
---
|
||||
|
||||
Migrating to latest action format
|
||||
@@ -76,15 +76,26 @@ export function createCatalogRegisterAction(options: {
|
||||
}): TemplateAction<
|
||||
| {
|
||||
catalogInfoUrl: string;
|
||||
optional?: boolean;
|
||||
optional?: boolean | undefined;
|
||||
}
|
||||
| {
|
||||
catalogInfoUrl: string;
|
||||
optional?: boolean | undefined;
|
||||
catalogInfoPath?: string | undefined;
|
||||
}
|
||||
| {
|
||||
repoContentsUrl: string;
|
||||
catalogInfoPath?: string;
|
||||
optional?: boolean;
|
||||
optional?: boolean | undefined;
|
||||
}
|
||||
| {
|
||||
repoContentsUrl: string;
|
||||
optional?: boolean | undefined;
|
||||
catalogInfoPath?: string | undefined;
|
||||
},
|
||||
JsonObject,
|
||||
'v1'
|
||||
{
|
||||
[x: string]: any;
|
||||
},
|
||||
'v2'
|
||||
>;
|
||||
|
||||
// @public
|
||||
@@ -93,12 +104,23 @@ export function createCatalogWriteAction(): TemplateAction<
|
||||
entity: Record<string, any>;
|
||||
filePath?: string | undefined;
|
||||
},
|
||||
any,
|
||||
'v1'
|
||||
{
|
||||
[x: string]: any;
|
||||
},
|
||||
'v2'
|
||||
>;
|
||||
|
||||
// @public
|
||||
export function createDebugLogAction(): TemplateAction<any, any, 'v1'>;
|
||||
export function createDebugLogAction(): TemplateAction<
|
||||
{
|
||||
message?: string | undefined;
|
||||
listWorkspace?: boolean | 'with-contents' | 'with-filenames' | undefined;
|
||||
},
|
||||
{
|
||||
[x: string]: any;
|
||||
},
|
||||
'v2'
|
||||
>;
|
||||
|
||||
// @public
|
||||
export function createFetchCatalogEntityAction(options: {
|
||||
@@ -126,11 +148,13 @@ export function createFetchPlainAction(options: {
|
||||
}): TemplateAction<
|
||||
{
|
||||
url: string;
|
||||
targetPath?: string;
|
||||
token?: string;
|
||||
targetPath?: string | undefined;
|
||||
token?: string | undefined;
|
||||
},
|
||||
JsonObject,
|
||||
'v1'
|
||||
{
|
||||
[x: string]: any;
|
||||
},
|
||||
'v2'
|
||||
>;
|
||||
|
||||
// @public
|
||||
@@ -141,10 +165,12 @@ export function createFetchPlainFileAction(options: {
|
||||
{
|
||||
url: string;
|
||||
targetPath: string;
|
||||
token?: string;
|
||||
token?: string | undefined;
|
||||
},
|
||||
JsonObject,
|
||||
'v1'
|
||||
{
|
||||
[x: string]: any;
|
||||
},
|
||||
'v2'
|
||||
>;
|
||||
|
||||
// @public
|
||||
@@ -156,18 +182,21 @@ export function createFetchTemplateAction(options: {
|
||||
}): TemplateAction<
|
||||
{
|
||||
url: string;
|
||||
targetPath?: string;
|
||||
values: any;
|
||||
templateFileExtension?: string | boolean;
|
||||
copyWithoutTemplating?: string[];
|
||||
cookiecutterCompat?: boolean;
|
||||
replace?: boolean;
|
||||
trimBlocks?: boolean;
|
||||
lstripBlocks?: boolean;
|
||||
token?: string;
|
||||
targetPath?: string | undefined;
|
||||
values?: Record<string, any> | undefined;
|
||||
copyWithoutRender?: string[] | undefined;
|
||||
copyWithoutTemplating?: string[] | undefined;
|
||||
cookiecutterCompat?: boolean | undefined;
|
||||
templateFileExtension?: string | boolean | undefined;
|
||||
replace?: boolean | undefined;
|
||||
trimBlocks?: boolean | undefined;
|
||||
lstripBlocks?: boolean | undefined;
|
||||
token?: string | undefined;
|
||||
},
|
||||
JsonObject,
|
||||
'v1'
|
||||
{
|
||||
[x: string]: any;
|
||||
},
|
||||
'v2'
|
||||
>;
|
||||
|
||||
// @public
|
||||
@@ -180,15 +209,17 @@ export function createFetchTemplateFileAction(options: {
|
||||
{
|
||||
url: string;
|
||||
targetPath: string;
|
||||
values: any;
|
||||
cookiecutterCompat?: boolean;
|
||||
replace?: boolean;
|
||||
trimBlocks?: boolean;
|
||||
lstripBlocks?: boolean;
|
||||
token?: string;
|
||||
values?: Record<string, any> | undefined;
|
||||
cookiecutterCompat?: boolean | undefined;
|
||||
replace?: boolean | undefined;
|
||||
trimBlocks?: boolean | undefined;
|
||||
lstripBlocks?: boolean | undefined;
|
||||
token?: string | undefined;
|
||||
},
|
||||
JsonObject,
|
||||
'v1'
|
||||
{
|
||||
[x: string]: any;
|
||||
},
|
||||
'v2'
|
||||
>;
|
||||
|
||||
// @public
|
||||
@@ -196,15 +227,17 @@ export const createFilesystemDeleteAction: () => TemplateAction<
|
||||
{
|
||||
files: string[];
|
||||
},
|
||||
JsonObject,
|
||||
'v1'
|
||||
{
|
||||
[x: string]: any;
|
||||
},
|
||||
'v2'
|
||||
>;
|
||||
|
||||
// @public
|
||||
export const createFilesystemReadDirAction: () => TemplateAction<
|
||||
{
|
||||
recursive: boolean;
|
||||
paths: string[];
|
||||
recursive: boolean;
|
||||
},
|
||||
{
|
||||
files: {
|
||||
@@ -218,26 +251,38 @@ export const createFilesystemReadDirAction: () => TemplateAction<
|
||||
fullPath: string;
|
||||
}[];
|
||||
},
|
||||
'v1'
|
||||
'v2'
|
||||
>;
|
||||
|
||||
// @public
|
||||
export const createFilesystemRenameAction: () => TemplateAction<
|
||||
{
|
||||
files: Array<{
|
||||
files: {
|
||||
from: string;
|
||||
to: string;
|
||||
overwrite?: boolean;
|
||||
}>;
|
||||
overwrite?: boolean | undefined;
|
||||
}[];
|
||||
},
|
||||
JsonObject,
|
||||
'v1'
|
||||
{
|
||||
[x: string]: any;
|
||||
},
|
||||
'v2'
|
||||
>;
|
||||
|
||||
// @public
|
||||
export function createWaitAction(options?: {
|
||||
maxWaitTime?: Duration | HumanDuration;
|
||||
}): TemplateAction<HumanDuration, JsonObject, 'v1'>;
|
||||
}): TemplateAction<
|
||||
{
|
||||
minutes?: number | undefined;
|
||||
seconds?: number | undefined;
|
||||
milliseconds?: number | undefined;
|
||||
},
|
||||
{
|
||||
[x: string]: any;
|
||||
},
|
||||
'v2'
|
||||
>;
|
||||
|
||||
// @public @deprecated
|
||||
export type CreateWorkerOptions = {
|
||||
|
||||
@@ -35,73 +35,45 @@ export function createCatalogRegisterAction(options: {
|
||||
}) {
|
||||
const { catalogClient, integrations, auth } = options;
|
||||
|
||||
return createTemplateAction<
|
||||
| { catalogInfoUrl: string; optional?: boolean }
|
||||
| { repoContentsUrl: string; catalogInfoPath?: string; optional?: boolean }
|
||||
>({
|
||||
return createTemplateAction({
|
||||
id,
|
||||
description:
|
||||
'Registers entities from a catalog descriptor file in the workspace into the software catalog.',
|
||||
examples,
|
||||
schema: {
|
||||
input: {
|
||||
oneOf: [
|
||||
{
|
||||
type: 'object',
|
||||
required: ['catalogInfoUrl'],
|
||||
properties: {
|
||||
catalogInfoUrl: {
|
||||
title: 'Catalog Info URL',
|
||||
description:
|
||||
'An absolute URL pointing to the catalog info file location',
|
||||
type: 'string',
|
||||
},
|
||||
optional: {
|
||||
title: 'Optional',
|
||||
input: z =>
|
||||
z.union([
|
||||
z.object({
|
||||
catalogInfoUrl: z.string({
|
||||
description:
|
||||
'An absolute URL pointing to the catalog info file location',
|
||||
}),
|
||||
optional: z
|
||||
.boolean({
|
||||
description:
|
||||
'Permit the registered location to optionally exist. Default: false',
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
type: 'object',
|
||||
required: ['repoContentsUrl'],
|
||||
properties: {
|
||||
repoContentsUrl: {
|
||||
title: 'Repository Contents URL',
|
||||
description:
|
||||
'An absolute URL pointing to the root of a repository directory tree',
|
||||
type: 'string',
|
||||
},
|
||||
catalogInfoPath: {
|
||||
title: 'Fetch URL',
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
z.object({
|
||||
repoContentsUrl: z.string({
|
||||
description:
|
||||
'An absolute URL pointing to the root of a repository directory tree',
|
||||
}),
|
||||
catalogInfoPath: z
|
||||
.string({
|
||||
description:
|
||||
'A relative path from the repo root pointing to the catalog info file, defaults to /catalog-info.yaml',
|
||||
type: 'string',
|
||||
},
|
||||
optional: {
|
||||
title: 'Optional',
|
||||
})
|
||||
.optional(),
|
||||
optional: z
|
||||
.boolean({
|
||||
description:
|
||||
'Permit the registered location to optionally exist. Default: false',
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
output: {
|
||||
type: 'object',
|
||||
required: ['catalogInfoUrl'],
|
||||
properties: {
|
||||
entityRef: {
|
||||
type: 'string',
|
||||
},
|
||||
catalogInfoUrl: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
]),
|
||||
},
|
||||
async handler(ctx) {
|
||||
const { input } = ctx;
|
||||
|
||||
@@ -18,7 +18,6 @@ import fs from 'fs-extra';
|
||||
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
|
||||
import * as yaml from 'yaml';
|
||||
import { resolveSafeChildPath } from '@backstage/backend-plugin-api';
|
||||
import { z } from 'zod';
|
||||
import { examples } from './write.examples';
|
||||
|
||||
const id = 'catalog:write';
|
||||
@@ -33,18 +32,17 @@ export function createCatalogWriteAction() {
|
||||
id,
|
||||
description: 'Writes the catalog-info.yaml for your template',
|
||||
schema: {
|
||||
input: z.object({
|
||||
filePath: z
|
||||
.string()
|
||||
.optional()
|
||||
.describe('Defaults to catalog-info.yaml'),
|
||||
input: {
|
||||
filePath: z =>
|
||||
z.string().optional().describe('Defaults to catalog-info.yaml'),
|
||||
// TODO: this should reference an zod entity validator if it existed.
|
||||
entity: z
|
||||
.record(z.any())
|
||||
.describe(
|
||||
'You can provide the same values used in the Entity schema.',
|
||||
),
|
||||
}),
|
||||
entity: z =>
|
||||
z
|
||||
.record(z.any())
|
||||
.describe(
|
||||
'You can provide the same values used in the Entity schema.',
|
||||
),
|
||||
},
|
||||
},
|
||||
examples,
|
||||
supportsDryRun: true,
|
||||
|
||||
@@ -19,7 +19,6 @@ import { join, relative } from 'path';
|
||||
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
|
||||
import { examples } from './log.examples';
|
||||
import fs from 'fs';
|
||||
import { z } from 'zod';
|
||||
|
||||
const id = 'debug:log';
|
||||
|
||||
@@ -34,24 +33,23 @@ const id = 'debug:log';
|
||||
* @public
|
||||
*/
|
||||
export function createDebugLogAction() {
|
||||
return createTemplateAction<{
|
||||
message?: string;
|
||||
listWorkspace?: boolean | 'with-filenames' | 'with-contents';
|
||||
}>({
|
||||
return createTemplateAction({
|
||||
id,
|
||||
description:
|
||||
'Writes a message into the log and/or lists all files in the workspace.',
|
||||
examples,
|
||||
schema: {
|
||||
input: z.object({
|
||||
message: z.string({ description: 'Message to output.' }).optional(),
|
||||
listWorkspace: z
|
||||
.union([z.boolean(), z.enum(['with-filenames', 'with-contents'])], {
|
||||
description:
|
||||
'List all files in the workspace. If used with "with-contents", also the file contents are listed.',
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
input: {
|
||||
message: z =>
|
||||
z.string({ description: 'Message to output.' }).optional(),
|
||||
listWorkspace: z =>
|
||||
z
|
||||
.union([z.boolean(), z.enum(['with-filenames', 'with-contents'])], {
|
||||
description:
|
||||
'List all files in the workspace. If used with "with-contents", also the file contents are listed.',
|
||||
})
|
||||
.optional(),
|
||||
},
|
||||
},
|
||||
supportsDryRun: true,
|
||||
async handler(ctx) {
|
||||
|
||||
+1
-8
@@ -15,7 +15,6 @@
|
||||
*/
|
||||
|
||||
import { createWaitAction } from './wait';
|
||||
import { Writable } from 'stream';
|
||||
import { examples } from './wait.examples';
|
||||
import yaml from 'yaml';
|
||||
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
|
||||
@@ -23,13 +22,7 @@ import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-
|
||||
describe('debug:wait examples', () => {
|
||||
const action = createWaitAction();
|
||||
|
||||
const logStream = {
|
||||
write: jest.fn(),
|
||||
} as jest.Mocked<Partial<Writable>> as jest.Mocked<Writable>;
|
||||
|
||||
const mockContext = createMockActionContext({
|
||||
logStream,
|
||||
});
|
||||
const mockContext = createMockActionContext();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
@@ -15,19 +15,12 @@
|
||||
*/
|
||||
|
||||
import { createWaitAction } from './wait';
|
||||
import { Writable } from 'stream';
|
||||
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
|
||||
|
||||
describe('debug:wait', () => {
|
||||
const action = createWaitAction();
|
||||
|
||||
const logStream = {
|
||||
write: jest.fn(),
|
||||
} as jest.Mocked<Partial<Writable>> as jest.Mocked<Writable>;
|
||||
|
||||
const mockContext = createMockActionContext({
|
||||
logStream,
|
||||
});
|
||||
const mockContext = createMockActionContext();
|
||||
|
||||
beforeEach(() => {
|
||||
jest.resetAllMocks();
|
||||
|
||||
@@ -48,27 +48,30 @@ export function createWaitAction(options?: {
|
||||
return Duration.fromISOTime(MAX_WAIT_TIME_IN_ISO);
|
||||
};
|
||||
|
||||
return createTemplateAction<HumanDuration>({
|
||||
return createTemplateAction({
|
||||
id,
|
||||
description: 'Waits for a certain period of time.',
|
||||
examples,
|
||||
schema: {
|
||||
input: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
minutes: {
|
||||
title: 'Waiting period in minutes.',
|
||||
type: 'number',
|
||||
},
|
||||
seconds: {
|
||||
title: 'Waiting period in seconds.',
|
||||
type: 'number',
|
||||
},
|
||||
milliseconds: {
|
||||
title: 'Waiting period in milliseconds.',
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
minutes: z =>
|
||||
z
|
||||
.number({
|
||||
description: 'Waiting period in minutes.',
|
||||
})
|
||||
.optional(),
|
||||
seconds: z =>
|
||||
z
|
||||
.number({
|
||||
description: 'Waiting period in seconds.',
|
||||
})
|
||||
.optional(),
|
||||
milliseconds: z =>
|
||||
z
|
||||
.number({
|
||||
description: 'Waiting period in milliseconds.',
|
||||
})
|
||||
.optional(),
|
||||
},
|
||||
},
|
||||
async handler(ctx) {
|
||||
|
||||
@@ -39,39 +39,32 @@ export function createFetchPlainAction(options: {
|
||||
}) {
|
||||
const { reader, integrations } = options;
|
||||
|
||||
return createTemplateAction<{
|
||||
url: string;
|
||||
targetPath?: string;
|
||||
token?: string;
|
||||
}>({
|
||||
return createTemplateAction({
|
||||
id: ACTION_ID,
|
||||
examples,
|
||||
description:
|
||||
'Downloads content and places it in the workspace, or optionally in a subdirectory specified by the `targetPath` input option.',
|
||||
schema: {
|
||||
input: {
|
||||
type: 'object',
|
||||
required: ['url'],
|
||||
properties: {
|
||||
url: {
|
||||
title: 'Fetch URL',
|
||||
url: z =>
|
||||
z.string({
|
||||
description:
|
||||
'Relative path or absolute URL pointing to the directory tree to fetch',
|
||||
type: 'string',
|
||||
},
|
||||
targetPath: {
|
||||
title: 'Target Path',
|
||||
description:
|
||||
'Target path within the working directory to download the contents to.',
|
||||
type: 'string',
|
||||
},
|
||||
token: {
|
||||
title: 'Token',
|
||||
description:
|
||||
'An optional token to use for authentication when reading the resources.',
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
}),
|
||||
targetPath: z =>
|
||||
z
|
||||
.string({
|
||||
description:
|
||||
'Target path within the working directory to download the contents to.',
|
||||
})
|
||||
.optional(),
|
||||
token: z =>
|
||||
z
|
||||
.string({
|
||||
description:
|
||||
'An optional token to use for authentication when reading the resources.',
|
||||
})
|
||||
.optional(),
|
||||
},
|
||||
},
|
||||
supportsDryRun: true,
|
||||
|
||||
@@ -14,18 +14,20 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { UrlReaderService } from '@backstage/backend-plugin-api';
|
||||
import { resolveSafeChildPath } from '@backstage/backend-plugin-api';
|
||||
import {
|
||||
resolveSafeChildPath,
|
||||
UrlReaderService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { ScmIntegrations } from '@backstage/integration';
|
||||
import { examples } from './plainFile.examples';
|
||||
|
||||
import {
|
||||
createTemplateAction,
|
||||
fetchFile,
|
||||
} from '@backstage/plugin-scaffolder-node';
|
||||
|
||||
/**
|
||||
* Downloads content and places it in the workspace, or optionally
|
||||
* in a subdirectory specified by the 'targetPath' input option.
|
||||
* Downloads a single file and places it in the workspace.
|
||||
* @public
|
||||
*/
|
||||
export function createFetchPlainFileAction(options: {
|
||||
@@ -34,45 +36,35 @@ export function createFetchPlainFileAction(options: {
|
||||
}) {
|
||||
const { reader, integrations } = options;
|
||||
|
||||
return createTemplateAction<{
|
||||
url: string;
|
||||
targetPath: string;
|
||||
token?: string;
|
||||
}>({
|
||||
return createTemplateAction({
|
||||
id: 'fetch:plain:file',
|
||||
description: 'Downloads single file and places it in the workspace.',
|
||||
examples,
|
||||
schema: {
|
||||
input: {
|
||||
type: 'object',
|
||||
required: ['url', 'targetPath'],
|
||||
properties: {
|
||||
url: {
|
||||
title: 'Fetch URL',
|
||||
url: z =>
|
||||
z.string({
|
||||
description:
|
||||
'Relative path or absolute URL pointing to the single file to fetch.',
|
||||
type: 'string',
|
||||
},
|
||||
targetPath: {
|
||||
title: 'Target Path',
|
||||
}),
|
||||
targetPath: z =>
|
||||
z.string({
|
||||
description:
|
||||
'Target path within the working directory to download the file as.',
|
||||
type: 'string',
|
||||
},
|
||||
token: {
|
||||
title: 'Token',
|
||||
description:
|
||||
'An optional token to use for authentication when reading the resources.',
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
}),
|
||||
token: z =>
|
||||
z
|
||||
.string({
|
||||
description:
|
||||
'An optional token to use for authentication when reading the resources.',
|
||||
})
|
||||
.optional(),
|
||||
},
|
||||
},
|
||||
supportsDryRun: true,
|
||||
async handler(ctx) {
|
||||
ctx.logger.info('Fetching plain content from remote URL');
|
||||
ctx.logger.info('Fetching plain file content from remote URL');
|
||||
|
||||
// Finally move the template result into the task workspace
|
||||
const outputPath = resolveSafeChildPath(
|
||||
ctx.workspacePath,
|
||||
ctx.input.targetPath,
|
||||
|
||||
@@ -41,89 +41,87 @@ export function createFetchTemplateAction(options: {
|
||||
additionalTemplateFilters?: Record<string, TemplateFilter>;
|
||||
additionalTemplateGlobals?: Record<string, TemplateGlobal>;
|
||||
}) {
|
||||
return createTemplateAction<{
|
||||
url: string;
|
||||
targetPath?: string;
|
||||
values: any;
|
||||
templateFileExtension?: string | boolean;
|
||||
|
||||
// Cookiecutter compat options
|
||||
copyWithoutTemplating?: string[];
|
||||
cookiecutterCompat?: boolean;
|
||||
replace?: boolean;
|
||||
trimBlocks?: boolean;
|
||||
lstripBlocks?: boolean;
|
||||
token?: string;
|
||||
}>({
|
||||
return createTemplateAction({
|
||||
id: 'fetch:template',
|
||||
description:
|
||||
'Downloads a skeleton, templates variables into file and directory names and content, and places the result in the workspace, or optionally in a subdirectory specified by the `targetPath` input option.',
|
||||
examples,
|
||||
schema: {
|
||||
input: {
|
||||
type: 'object',
|
||||
required: ['url'],
|
||||
properties: {
|
||||
url: {
|
||||
title: 'Fetch URL',
|
||||
url: z =>
|
||||
z.string({
|
||||
description:
|
||||
'Relative path or absolute URL pointing to the directory tree to fetch',
|
||||
type: 'string',
|
||||
},
|
||||
targetPath: {
|
||||
title: 'Target Path',
|
||||
description:
|
||||
'Target path within the working directory to download the contents to. Defaults to the working directory root.',
|
||||
type: 'string',
|
||||
},
|
||||
values: {
|
||||
title: 'Template Values',
|
||||
description: 'Values to pass on to the templating engine',
|
||||
type: 'object',
|
||||
},
|
||||
copyWithoutRender: {
|
||||
title: '[Deprecated] Copy Without Render',
|
||||
description:
|
||||
'An array of glob patterns. Any files or directories which match are copied without being processed as templates.',
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
copyWithoutTemplating: {
|
||||
title: 'Copy Without Templating',
|
||||
description:
|
||||
'An array of glob patterns. Contents of matched files or directories are copied without being processed, but paths are subject to rendering.',
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
cookiecutterCompat: {
|
||||
title: 'Cookiecutter compatibility mode',
|
||||
description:
|
||||
'Enable features to maximise compatibility with templates built for fetch:cookiecutter',
|
||||
type: 'boolean',
|
||||
},
|
||||
templateFileExtension: {
|
||||
title: 'Template File Extension',
|
||||
description:
|
||||
'If set, only files with the given extension will be templated. If set to `true`, the default extension `.njk` is used.',
|
||||
type: ['string', 'boolean'],
|
||||
},
|
||||
replace: {
|
||||
title: 'Replace files',
|
||||
description:
|
||||
'If set, replace files in targetPath instead of skipping existing ones.',
|
||||
type: 'boolean',
|
||||
},
|
||||
token: {
|
||||
title: 'Token',
|
||||
description:
|
||||
'An optional token to use for authentication when reading the resources.',
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
}),
|
||||
targetPath: z =>
|
||||
z
|
||||
.string({
|
||||
description:
|
||||
'Target path within the working directory to download the contents to. Defaults to the working directory root.',
|
||||
})
|
||||
.optional(),
|
||||
values: z =>
|
||||
z
|
||||
.record(z.any(), {
|
||||
description: 'Values to pass on to the templating engine',
|
||||
})
|
||||
.optional(),
|
||||
copyWithoutRender: z =>
|
||||
z
|
||||
.array(z.string(), {
|
||||
description:
|
||||
'An array of glob patterns. Any files or directories which match are copied without being processed as templates.',
|
||||
})
|
||||
.optional(),
|
||||
copyWithoutTemplating: z =>
|
||||
z
|
||||
.array(z.string(), {
|
||||
description:
|
||||
'An array of glob patterns. Contents of matched files or directories are copied without being processed, but paths are subject to rendering.',
|
||||
})
|
||||
.optional(),
|
||||
cookiecutterCompat: z =>
|
||||
z
|
||||
.boolean({
|
||||
description:
|
||||
'Enable features to maximise compatibility with templates built for fetch:cookiecutter',
|
||||
})
|
||||
.optional(),
|
||||
templateFileExtension: z =>
|
||||
z
|
||||
.union([z.string(), z.boolean()], {
|
||||
description:
|
||||
'If set, only files with the given extension will be templated. If set to `true`, the default extension `.njk` is used.',
|
||||
})
|
||||
.optional(),
|
||||
replace: z =>
|
||||
z
|
||||
.boolean({
|
||||
description:
|
||||
'If set, replace files in targetPath instead of skipping existing ones.',
|
||||
})
|
||||
.optional(),
|
||||
trimBlocks: z =>
|
||||
z
|
||||
.boolean({
|
||||
description:
|
||||
'If set, the first newline after a block is removed (block, not variable tag).',
|
||||
})
|
||||
.optional(),
|
||||
lstripBlocks: z =>
|
||||
z
|
||||
.boolean({
|
||||
description:
|
||||
'If set, leading spaces and tabs are stripped from the start of a line to a block.',
|
||||
})
|
||||
.optional(),
|
||||
token: z =>
|
||||
z
|
||||
.string({
|
||||
description:
|
||||
'An optional token to use for authentication when reading the resources.',
|
||||
})
|
||||
.optional(),
|
||||
},
|
||||
},
|
||||
supportsDryRun: true,
|
||||
|
||||
@@ -38,60 +38,63 @@ export function createFetchTemplateFileAction(options: {
|
||||
additionalTemplateFilters?: Record<string, TemplateFilter>;
|
||||
additionalTemplateGlobals?: Record<string, TemplateGlobal>;
|
||||
}) {
|
||||
return createTemplateAction<{
|
||||
url: string;
|
||||
targetPath: string;
|
||||
values: any;
|
||||
cookiecutterCompat?: boolean;
|
||||
replace?: boolean;
|
||||
trimBlocks?: boolean;
|
||||
lstripBlocks?: boolean;
|
||||
token?: string;
|
||||
}>({
|
||||
return createTemplateAction({
|
||||
id: 'fetch:template:file',
|
||||
description: 'Downloads single file and places it in the workspace.',
|
||||
examples,
|
||||
schema: {
|
||||
input: {
|
||||
type: 'object',
|
||||
required: ['url', 'targetPath'],
|
||||
properties: {
|
||||
url: {
|
||||
title: 'Fetch URL',
|
||||
url: z =>
|
||||
z.string({
|
||||
description:
|
||||
'Relative path or absolute URL pointing to the single file to fetch.',
|
||||
type: 'string',
|
||||
},
|
||||
targetPath: {
|
||||
title: 'Target Path',
|
||||
}),
|
||||
targetPath: z =>
|
||||
z.string({
|
||||
description:
|
||||
'Target path within the working directory to download the file as.',
|
||||
type: 'string',
|
||||
},
|
||||
values: {
|
||||
title: 'Template Values',
|
||||
description: 'Values to pass on to the templating engine',
|
||||
type: 'object',
|
||||
},
|
||||
cookiecutterCompat: {
|
||||
title: 'Cookiecutter compatibility mode',
|
||||
description:
|
||||
'Enable features to maximise compatibility with templates built for fetch:cookiecutter',
|
||||
type: 'boolean',
|
||||
},
|
||||
replace: {
|
||||
title: 'Replace file',
|
||||
description:
|
||||
'If set, replace file in targetPath instead of overwriting existing one.',
|
||||
type: 'boolean',
|
||||
},
|
||||
token: {
|
||||
title: 'Token',
|
||||
description:
|
||||
'An optional token to use for authentication when reading the resources.',
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
}),
|
||||
values: z =>
|
||||
z
|
||||
.record(z.any(), {
|
||||
description: 'Values to pass on to the templating engine',
|
||||
})
|
||||
.optional(),
|
||||
cookiecutterCompat: z =>
|
||||
z
|
||||
.boolean({
|
||||
description:
|
||||
'Enable features to maximise compatibility with templates built for fetch:cookiecutter',
|
||||
})
|
||||
.optional(),
|
||||
replace: z =>
|
||||
z
|
||||
.boolean({
|
||||
description:
|
||||
'If set, replace file in targetPath instead of overwriting existing one.',
|
||||
})
|
||||
.optional(),
|
||||
trimBlocks: z =>
|
||||
z
|
||||
.boolean({
|
||||
description:
|
||||
'If set, the first newline after a block is removed (block, not variable tag).',
|
||||
})
|
||||
.optional(),
|
||||
lstripBlocks: z =>
|
||||
z
|
||||
.boolean({
|
||||
description:
|
||||
'If set, leading spaces and tabs are stripped from the start of a line to a block.',
|
||||
})
|
||||
.optional(),
|
||||
token: z =>
|
||||
z
|
||||
.string({
|
||||
description:
|
||||
'An optional token to use for authentication when reading the resources.',
|
||||
})
|
||||
.optional(),
|
||||
},
|
||||
},
|
||||
supportsDryRun: true,
|
||||
|
||||
@@ -26,24 +26,16 @@ import { examples } from './delete.examples';
|
||||
* @public
|
||||
*/
|
||||
export const createFilesystemDeleteAction = () => {
|
||||
return createTemplateAction<{ files: string[] }>({
|
||||
return createTemplateAction({
|
||||
id: 'fs:delete',
|
||||
description: 'Deletes files and directories from the workspace',
|
||||
examples,
|
||||
schema: {
|
||||
input: {
|
||||
required: ['files'],
|
||||
type: 'object',
|
||||
properties: {
|
||||
files: {
|
||||
title: 'Files',
|
||||
files: z =>
|
||||
z.array(z.string(), {
|
||||
description: 'A list of files and directories that will be deleted',
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'string',
|
||||
},
|
||||
},
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
supportsDryRun: true,
|
||||
|
||||
@@ -14,19 +14,21 @@
|
||||
* limitations under the License.
|
||||
*/
|
||||
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
|
||||
import z from 'zod';
|
||||
import { resolveSafeChildPath } from '@backstage/backend-plugin-api';
|
||||
import fs from 'fs/promises';
|
||||
import path from 'path';
|
||||
import { z as zod } from 'zod';
|
||||
|
||||
const contentSchema = z.object({
|
||||
name: z.string().describe('Name of the file or directory'),
|
||||
path: z
|
||||
.string()
|
||||
.describe('path to the file or directory relative to the workspace'),
|
||||
fullPath: z.string().describe('full path to the file or directory'),
|
||||
});
|
||||
type Content = z.infer<typeof contentSchema>;
|
||||
const contentSchema = (z: typeof zod) =>
|
||||
z.object({
|
||||
name: z.string().describe('Name of the file or directory'),
|
||||
path: z
|
||||
.string()
|
||||
.describe('path to the file or directory relative to the workspace'),
|
||||
fullPath: z.string().describe('full path to the file or directory'),
|
||||
});
|
||||
|
||||
type Content = zod.infer<ReturnType<typeof contentSchema>>;
|
||||
|
||||
/**
|
||||
* Creates new action that enables reading directories in the workspace.
|
||||
@@ -38,14 +40,14 @@ export const createFilesystemReadDirAction = () => {
|
||||
description: 'Reads files and directories from the workspace',
|
||||
supportsDryRun: true,
|
||||
schema: {
|
||||
input: z.object({
|
||||
paths: z.array(z.string().min(1)),
|
||||
recursive: z.boolean().default(false),
|
||||
}),
|
||||
output: z.object({
|
||||
files: z.array(contentSchema),
|
||||
folders: z.array(contentSchema),
|
||||
}),
|
||||
input: {
|
||||
paths: z => z.array(z.string().min(1)),
|
||||
recursive: z => z.boolean().default(false),
|
||||
},
|
||||
output: {
|
||||
files: z => z.array(contentSchema(z)),
|
||||
folders: z => z.array(contentSchema(z)),
|
||||
},
|
||||
},
|
||||
async handler(ctx) {
|
||||
const files: Content[] = [];
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
|
||||
import { createTemplateAction } from '@backstage/plugin-scaffolder-node';
|
||||
import { resolveSafeChildPath } from '@backstage/backend-plugin-api';
|
||||
import { InputError } from '@backstage/errors';
|
||||
import fs from 'fs-extra';
|
||||
import { examples } from './rename.examples';
|
||||
|
||||
@@ -25,80 +24,60 @@ import { examples } from './rename.examples';
|
||||
* @public
|
||||
*/
|
||||
export const createFilesystemRenameAction = () => {
|
||||
return createTemplateAction<{
|
||||
files: Array<{
|
||||
from: string;
|
||||
to: string;
|
||||
overwrite?: boolean;
|
||||
}>;
|
||||
}>({
|
||||
return createTemplateAction({
|
||||
id: 'fs:rename',
|
||||
description: 'Renames files and directories within the workspace',
|
||||
examples,
|
||||
schema: {
|
||||
input: {
|
||||
required: ['files'],
|
||||
type: 'object',
|
||||
properties: {
|
||||
files: {
|
||||
title: 'Files',
|
||||
description:
|
||||
'A list of file and directory names that will be renamed',
|
||||
type: 'array',
|
||||
items: {
|
||||
type: 'object',
|
||||
required: ['from', 'to'],
|
||||
properties: {
|
||||
from: {
|
||||
type: 'string',
|
||||
title: 'The source location of the file to be renamed',
|
||||
},
|
||||
to: {
|
||||
type: 'string',
|
||||
title: 'The destination of the new file',
|
||||
},
|
||||
overwrite: {
|
||||
type: 'boolean',
|
||||
title:
|
||||
files: z =>
|
||||
z.array(
|
||||
z.object({
|
||||
from: z.string({
|
||||
description: 'The source location of the file to be renamed',
|
||||
}),
|
||||
to: z.string({
|
||||
description: 'The destination of the new file',
|
||||
}),
|
||||
overwrite: z
|
||||
.boolean({
|
||||
description:
|
||||
'Overwrite existing file or directory, default is false',
|
||||
},
|
||||
},
|
||||
})
|
||||
.optional(),
|
||||
}),
|
||||
{
|
||||
description:
|
||||
'A list of file and directory names that will be renamed',
|
||||
},
|
||||
},
|
||||
},
|
||||
),
|
||||
},
|
||||
},
|
||||
supportsDryRun: true,
|
||||
async handler(ctx) {
|
||||
if (!Array.isArray(ctx.input?.files)) {
|
||||
throw new InputError('files must be an Array');
|
||||
}
|
||||
|
||||
for (const file of ctx.input.files) {
|
||||
if (!file.from || !file.to) {
|
||||
throw new InputError('each file must have a from and to property');
|
||||
}
|
||||
|
||||
const sourceFilepath = resolveSafeChildPath(
|
||||
ctx.workspacePath,
|
||||
file.from,
|
||||
);
|
||||
const destFilepath = resolveSafeChildPath(ctx.workspacePath, file.to);
|
||||
|
||||
try {
|
||||
await fs.move(sourceFilepath, destFilepath, {
|
||||
overwrite: file.overwrite ?? false,
|
||||
});
|
||||
ctx.logger.info(
|
||||
`File ${sourceFilepath} renamed to ${destFilepath} successfully`,
|
||||
if (!fs.existsSync(sourceFilepath)) {
|
||||
throw new Error(
|
||||
`File ${sourceFilepath} does not exist, so rename cannot succeed.`,
|
||||
);
|
||||
} catch (err) {
|
||||
ctx.logger.error(
|
||||
`Failed to rename file ${sourceFilepath} to ${destFilepath}:`,
|
||||
err,
|
||||
);
|
||||
throw err;
|
||||
}
|
||||
|
||||
const destExists = fs.existsSync(destFilepath);
|
||||
|
||||
if (destExists && !file.overwrite) {
|
||||
throw new Error(
|
||||
`File ${destFilepath} already exists, refusing to overwrite.`,
|
||||
);
|
||||
}
|
||||
|
||||
await fs.move(sourceFilepath, destFilepath, { overwrite: true });
|
||||
ctx.logger.info(`Moved file from ${sourceFilepath} to ${destFilepath}`);
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
@@ -23,11 +23,9 @@ import { TaskSpec } from '@backstage/plugin-scaffolder-common';
|
||||
import {
|
||||
createTemplateAction,
|
||||
TaskSecrets,
|
||||
TemplateAction,
|
||||
TaskContext,
|
||||
} from '@backstage/plugin-scaffolder-node';
|
||||
import { UserEntity } from '@backstage/catalog-model';
|
||||
import { z } from 'zod';
|
||||
import {
|
||||
AuthorizeResult,
|
||||
PermissionEvaluator,
|
||||
@@ -38,7 +36,6 @@ import {
|
||||
mockCredentials,
|
||||
mockServices,
|
||||
} from '@backstage/backend-test-utils';
|
||||
import { loggerToWinstonLogger } from '../../util/loggerToWinstonLogger';
|
||||
|
||||
describe('NunjucksWorkflowRunner', () => {
|
||||
let actionRegistry: TemplateActionRegistry;
|
||||
@@ -127,32 +124,12 @@ describe('NunjucksWorkflowRunner', () => {
|
||||
handler: fakeActionHandler,
|
||||
schema: {
|
||||
input: {
|
||||
type: 'object',
|
||||
required: ['foo'],
|
||||
properties: {
|
||||
foo: {
|
||||
type: 'number',
|
||||
},
|
||||
},
|
||||
foo: z => z.number(),
|
||||
},
|
||||
},
|
||||
}),
|
||||
);
|
||||
|
||||
actionRegistry.register(
|
||||
createTemplateAction({
|
||||
id: 'jest-legacy-zod-validated-action',
|
||||
description: 'Mock action for testing',
|
||||
handler: fakeActionHandler,
|
||||
supportsDryRun: true,
|
||||
schema: {
|
||||
input: z.object({
|
||||
foo: z.number(),
|
||||
}),
|
||||
},
|
||||
}) as TemplateAction,
|
||||
);
|
||||
|
||||
actionRegistry.register(
|
||||
createTemplateAction({
|
||||
id: 'jest-zod-validated-action',
|
||||
@@ -186,13 +163,14 @@ describe('NunjucksWorkflowRunner', () => {
|
||||
id: 'checkpoints-action',
|
||||
description: 'Mock action with checkpoints',
|
||||
schema: {
|
||||
output: z.object({
|
||||
key1: z.string(),
|
||||
key2: z.string(),
|
||||
key3: z.string(),
|
||||
key4: z.string(),
|
||||
key5: z.string(),
|
||||
}),
|
||||
output: z =>
|
||||
z.object({
|
||||
key1: z.string(),
|
||||
key2: z.string(),
|
||||
key3: z.string(),
|
||||
key4: z.string(),
|
||||
key5: z.string(),
|
||||
}),
|
||||
},
|
||||
handler: async ctx => {
|
||||
const key1 = await ctx.checkpoint({
|
||||
@@ -222,7 +200,9 @@ describe('NunjucksWorkflowRunner', () => {
|
||||
ctx.output('key2', key2);
|
||||
ctx.output('key3', key3);
|
||||
|
||||
// @ts-expect-error - not valid output type
|
||||
ctx.output('key4', key4);
|
||||
// @ts-expect-error - not valid output type
|
||||
ctx.output('key5', key5);
|
||||
},
|
||||
}),
|
||||
@@ -236,7 +216,7 @@ describe('NunjucksWorkflowRunner', () => {
|
||||
actionRegistry,
|
||||
integrations,
|
||||
workingDirectory: mockDir.path,
|
||||
logger: loggerToWinstonLogger(logger),
|
||||
logger,
|
||||
permissions: mockedPermissionApi,
|
||||
});
|
||||
});
|
||||
@@ -280,22 +260,6 @@ describe('NunjucksWorkflowRunner', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should throw an error if the action has legacy zod schema and the input does not match', async () => {
|
||||
const task = createMockTaskWithSpec({
|
||||
steps: [
|
||||
{
|
||||
id: 'test',
|
||||
name: 'name',
|
||||
action: 'jest-legacy-zod-validated-action',
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await expect(runner.execute(task)).rejects.toThrow(
|
||||
/Invalid input passed to action jest-legacy-zod-validated-action, instance requires property \"foo\"/,
|
||||
);
|
||||
});
|
||||
|
||||
it('should run the action when the zod validation passes', async () => {
|
||||
const task = createMockTaskWithSpec({
|
||||
steps: [
|
||||
@@ -313,23 +277,6 @@ describe('NunjucksWorkflowRunner', () => {
|
||||
expect(fakeActionHandler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should run the action when the zod validation passes with legacy zod', async () => {
|
||||
const task = createMockTaskWithSpec({
|
||||
steps: [
|
||||
{
|
||||
id: 'test',
|
||||
name: 'name',
|
||||
action: 'jest-legacy-zod-validated-action',
|
||||
input: { foo: 1 },
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
await runner.execute(task);
|
||||
|
||||
expect(fakeActionHandler).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should run the action when the validation passes', async () => {
|
||||
const task = createMockTaskWithSpec({
|
||||
steps: [
|
||||
|
||||
@@ -28,7 +28,6 @@ import fs from 'fs-extra';
|
||||
import { validate as validateJsonSchema } from 'jsonschema';
|
||||
import nunjucks from 'nunjucks';
|
||||
import path from 'path';
|
||||
import { PassThrough } from 'stream';
|
||||
import * as winston from 'winston';
|
||||
import {
|
||||
SecureTemplater,
|
||||
@@ -40,6 +39,7 @@ import { TaskTrackType, WorkflowResponse, WorkflowRunner } from './types';
|
||||
|
||||
import type {
|
||||
AuditorService,
|
||||
LoggerService,
|
||||
PermissionsService,
|
||||
} from '@backstage/backend-plugin-api';
|
||||
import { UserEntity } from '@backstage/catalog-model';
|
||||
@@ -59,14 +59,13 @@ import { createDefaultFilters } from '../../lib/templating/filters/createDefault
|
||||
import { scaffolderActionRules } from '../../service/rules';
|
||||
import { createCounterMetric, createHistogramMetric } from '../../util/metrics';
|
||||
import { BackstageLoggerTransport, WinstonLogger } from './logger';
|
||||
import { loggerToWinstonLogger } from '../../util/loggerToWinstonLogger';
|
||||
import { convertFiltersToRecord } from '../../util/templating';
|
||||
|
||||
type NunjucksWorkflowRunnerOptions = {
|
||||
workingDirectory: string;
|
||||
actionRegistry: TemplateActionRegistry;
|
||||
integrations: ScmIntegrations;
|
||||
logger: winston.Logger;
|
||||
logger: LoggerService;
|
||||
auditor?: AuditorService;
|
||||
additionalTemplateFilters?: Record<string, TemplateFilter>;
|
||||
additionalTemplateGlobals?: Record<string, TemplateGlobal>;
|
||||
@@ -113,7 +112,7 @@ const createStepLogger = ({
|
||||
}: {
|
||||
task: TaskContext;
|
||||
step: TaskStep;
|
||||
rootLogger: winston.Logger;
|
||||
rootLogger: LoggerService;
|
||||
}) => {
|
||||
const taskLogger = WinstonLogger.create({
|
||||
level: process.env.LOG_LEVEL || 'info',
|
||||
@@ -126,22 +125,7 @@ const createStepLogger = ({
|
||||
|
||||
taskLogger.addRedactions(Object.values(task.secrets ?? {}));
|
||||
|
||||
// This stream logger should be deprecated. We're going to replace it with
|
||||
// just using the logger directly, as all those logs get written to step logs
|
||||
// using the stepLogStream above.
|
||||
// Initially this stream used to be the only way to write to the client logs, but that
|
||||
// has changed over time, there's not really a need for this anymore.
|
||||
// You can just create a simple wrapper like the below in your action to write to the main logger.
|
||||
// This way we also get redactions for free.
|
||||
const streamLogger = new PassThrough();
|
||||
streamLogger.on('data', async data => {
|
||||
const message = data.toString().trim();
|
||||
if (message?.length > 1) {
|
||||
taskLogger.info(message);
|
||||
}
|
||||
});
|
||||
|
||||
return { taskLogger, streamLogger };
|
||||
return { taskLogger };
|
||||
};
|
||||
|
||||
const isActionAuthorized = createConditionAuthorizer(
|
||||
@@ -268,7 +252,7 @@ export class NunjucksWorkflowRunner implements WorkflowRunner {
|
||||
}
|
||||
const action: TemplateAction<JsonObject> =
|
||||
this.options.actionRegistry.get(step.action);
|
||||
const { taskLogger, streamLogger } = createStepLogger({
|
||||
const { taskLogger } = createStepLogger({
|
||||
task,
|
||||
step,
|
||||
rootLogger: this.options.logger,
|
||||
@@ -393,9 +377,7 @@ export class NunjucksWorkflowRunner implements WorkflowRunner {
|
||||
id: await task.getWorkspaceName(),
|
||||
},
|
||||
secrets: task.secrets ?? {},
|
||||
// TODO(blam): move to LoggerService and away from Winston
|
||||
logger: loggerToWinstonLogger(taskLogger),
|
||||
logStream: streamLogger,
|
||||
logger: taskLogger,
|
||||
workspacePath,
|
||||
async checkpoint<T extends JsonValue | void>(opts: {
|
||||
key?: string;
|
||||
|
||||
Reference in New Issue
Block a user