Merge pull request #27283 from secustor/feat/add-new-readdir-action

feat(plugins/scaffolder-backend): add new fs:readdir action
This commit is contained in:
Ben Lambert
2024-11-08 16:36:27 +01:00
committed by GitHub
7 changed files with 284 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
---
'@backstage/plugin-scaffolder-backend': patch
---
Add new `fs:readdir` action to list current content of the workspace
+20
View File
@@ -231,6 +231,26 @@ export const createFilesystemDeleteAction: () => TemplateAction_2<
JsonObject
>;
// @public
export const createFilesystemReadDirAction: () => TemplateAction_2<
{
paths: string[];
recursive?: boolean | undefined;
},
{
files: {
name: string;
path: string;
fullPath: string;
}[];
folders: {
name: string;
path: string;
fullPath: string;
}[];
}
>;
// @public
export const createFilesystemRenameAction: () => TemplateAction_2<
{
@@ -41,6 +41,7 @@ import {
} from './fetch';
import {
createFilesystemDeleteAction,
createFilesystemReadDirAction,
createFilesystemRenameAction,
} from './filesystem';
import {
@@ -224,6 +225,7 @@ export const createBuiltinActions = (
createFetchCatalogEntityAction({ catalogClient, auth }),
createCatalogWriteAction(),
createFilesystemDeleteAction(),
createFilesystemReadDirAction(),
createFilesystemRenameAction(),
createGithubActionsDispatchAction({
integrations,
@@ -16,3 +16,4 @@
export { createFilesystemDeleteAction } from './delete';
export { createFilesystemRenameAction } from './rename';
export { createFilesystemReadDirAction } from './read';
@@ -0,0 +1,107 @@
/*
* Copyright 2024 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 { createFilesystemReadDirAction } from './read';
import { createMockActionContext } from '@backstage/plugin-scaffolder-node-test-utils';
import { resolve as resolvePath } from 'path';
import { createMockDirectory } from '@backstage/backend-test-utils';
import { examples } from './read.examples';
import yaml from 'yaml';
describe('fs:readdir examples', () => {
const action = createFilesystemReadDirAction();
const mockDir = createMockDirectory();
mockDir.setContent({
workspace: {
'file1.txt': 'hello',
'file2.txt': 'world',
docs: {
'doc1.md': 'hello',
'doc2.md': 'world',
assets: {
'asset1.jpg': 'sddfsdfsdfsdf',
},
},
},
});
const workspacePath = resolvePath(mockDir.path, 'workspace');
beforeEach(() => {
jest.restoreAllMocks();
});
it('should be able to read workspace root', async () => {
const example = examples[0].example;
const mockContext = createMockActionContext<any, any>({
workspacePath,
input: yaml.parse(example).steps[0].input,
});
await action.handler(mockContext);
expect(mockContext.output).toHaveBeenNthCalledWith(1, 'files', [
{
name: 'file1.txt',
path: 'file1.txt',
fullPath: resolvePath(workspacePath, 'file1.txt'),
},
{
name: 'file2.txt',
path: 'file2.txt',
fullPath: resolvePath(workspacePath, 'file2.txt'),
},
]);
expect(mockContext.output).toHaveBeenNthCalledWith(2, 'folders', [
{
name: 'docs',
path: 'docs',
fullPath: resolvePath(workspacePath, 'docs'),
},
]);
});
it('should be able to read directory recursively', async () => {
const example = examples[1].example;
const mockContext = createMockActionContext<any, any>({
workspacePath,
input: yaml.parse(example).steps[0].input,
});
await action.handler(mockContext);
expect(mockContext.output).toHaveBeenNthCalledWith(1, 'files', [
{
name: 'doc1.md',
path: 'docs/doc1.md',
fullPath: resolvePath(workspacePath, 'docs/doc1.md'),
},
{
name: 'doc2.md',
path: 'docs/doc2.md',
fullPath: resolvePath(workspacePath, 'docs/doc2.md'),
},
{
name: 'asset1.jpg',
path: 'docs/assets/asset1.jpg',
fullPath: resolvePath(workspacePath, 'docs/assets/asset1.jpg'),
},
]);
expect(mockContext.output).toHaveBeenNthCalledWith(2, 'folders', [
{
name: 'assets',
path: 'docs/assets',
fullPath: resolvePath(workspacePath, 'docs/assets'),
},
]);
});
});
@@ -0,0 +1,67 @@
/*
* Copyright 2024 The Backstage Authors
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import { TemplateExample } from '@backstage/plugin-scaffolder-node';
import * as yaml from 'yaml';
export const examples: TemplateExample[] = [
{
description: 'Read complete content of workspace',
example: yaml.stringify({
steps: [
{
action: 'fs:readdir',
id: 'read-workdir',
name: 'Read workspace directory',
input: {
paths: ['.'],
},
},
],
}),
},
{
description: 'Get content of the docs folder',
example: yaml.stringify({
steps: [
{
action: 'fs:readdir',
id: 'read-workdir',
name: 'Read workspace directory',
input: {
paths: ['docs'],
recursive: true,
},
},
],
}),
},
{
description: 'Get content of multiple folders',
example: yaml.stringify({
steps: [
{
action: 'fs:readdir',
id: 'read-workdir',
name: 'Read workspace directory',
input: {
paths: ['foo', 'bar'],
recursive: true,
},
},
],
}),
},
];
@@ -0,0 +1,82 @@
/*
* Copyright 2024 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 { 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';
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>;
/**
* Creates new action that enables reading directories in the workspace.
* @public
*/
export const createFilesystemReadDirAction = () => {
return createTemplateAction({
id: 'fs:readdir',
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),
}),
},
async handler(ctx) {
const files: Content[] = [];
const folders: Content[] = [];
for (const localPath of ctx.input.paths) {
const fullWorkspacePath = resolveSafeChildPath(
ctx.workspacePath,
localPath,
);
const content = await fs.readdir(fullWorkspacePath, {
recursive: ctx.input.recursive,
withFileTypes: true,
});
for (const dirent of content) {
const fullPath = path.join(dirent.parentPath, dirent.name);
const element = {
name: dirent.name,
path: path.relative(ctx.workspacePath, fullPath),
fullPath,
};
if (dirent.isDirectory()) {
folders.push(element);
} else {
files.push(element);
}
}
}
ctx.output('files', files);
ctx.output('folders', folders);
},
});
};