diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts index 691583e63c..4a80cd208d 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/template.ts @@ -131,24 +131,26 @@ export function createFetchTemplateAction(options: { }, }, supportsDryRun: true, - handler: createTemplateActionHandler({ - resolveTemplate: async ctx => { - ctx.logger.info('Fetching template content from remote URL'); + handler: ctx => + createTemplateActionHandler({ + ctx, + resolveTemplate: async () => { + ctx.logger.info('Fetching template content from remote URL'); - const workDir = await ctx.createTemporaryDirectory(); - const templateDir = resolveSafeChildPath(workDir, 'template'); + const workDir = await ctx.createTemporaryDirectory(); + const templateDir = resolveSafeChildPath(workDir, 'template'); - await fetchContents({ - baseUrl: ctx.templateInfo?.baseUrl, - fetchUrl: ctx.input.url, - outputPath: templateDir, - token: ctx.input.token, - ...options, - }); + await fetchContents({ + baseUrl: ctx.templateInfo?.baseUrl, + fetchUrl: ctx.input.url, + outputPath: templateDir, + token: ctx.input.token, + ...options, + }); - return templateDir; - }, - ...options, - }), + return templateDir; + }, + ...options, + }), }); } diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/templateActionHandler.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/templateActionHandler.ts index 9cfd85b348..f280841bfa 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/templateActionHandler.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/templateActionHandler.ts @@ -21,7 +21,6 @@ import { InputError } from '@backstage/errors'; import { ScmIntegrations } from '@backstage/integration'; import { ActionContext, - TemplateActionOptions, TemplateFilter, TemplateGlobal, } from '@backstage/plugin-scaffolder-node'; @@ -50,19 +49,21 @@ export type TemplateActionInput = { lstripBlocks?: boolean; }; -export function createTemplateActionHandler< - I extends TemplateActionInput = TemplateActionInput, +export async function createTemplateActionHandler< + I extends TemplateActionInput, >(options: { - resolveTemplate: (ctx: ActionContext) => Promise; + ctx: ActionContext; + resolveTemplate: () => Promise; integrations: ScmIntegrations; additionalTemplateFilters?: Record; additionalTemplateGlobals?: Record; -}): TemplateActionOptions['handler'] { +}) { const { resolveTemplate, integrations, additionalTemplateFilters, additionalTemplateGlobals: templateGlobals, + ctx, } = options; const templateFilters = { @@ -70,134 +71,128 @@ export function createTemplateActionHandler< ...additionalTemplateFilters, }; - return async (ctx: ActionContext) => { - const { outputDir, copyOnlyPatterns, renderFilename, extension } = - resolveTemplateActionSettings(ctx); + const { outputDir, copyOnlyPatterns, renderFilename, extension } = + resolveTemplateActionSettings(ctx); - const templateDir = await resolveTemplate(ctx); + const templateDir = await resolveTemplate(); - if (isChildPath(templateDir, outputDir)) { - throw new InputError('targetPath must not be within template path'); - } + if (isChildPath(templateDir, outputDir)) { + throw new InputError('targetPath must not be within template path'); + } - ctx.logger.info('Listing files and directories in template'); - const allEntriesInTemplate = await globby(`**/*`, { + ctx.logger.info('Listing files and directories in template'); + const allEntriesInTemplate = await globby(`**/*`, { + cwd: templateDir, + dot: true, + onlyFiles: false, + markDirectories: true, + followSymbolicLinks: false, + }); + + const nonTemplatedEntries = new Set( + await globby(copyOnlyPatterns || [], { cwd: templateDir, dot: true, onlyFiles: false, markDirectories: true, followSymbolicLinks: false, - }); + }), + ); - const nonTemplatedEntries = new Set( - await globby(copyOnlyPatterns || [], { - cwd: templateDir, - dot: true, - onlyFiles: false, - markDirectories: true, - followSymbolicLinks: false, - }), - ); + // Cookiecutter prefixes all parameters in templates with + // `cookiecutter.`. To replicate this, we wrap our parameters + // in an object with a `cookiecutter` property when compat + // mode is enabled. + const { cookiecutterCompat, values } = ctx.input; + const context = { + [cookiecutterCompat ? 'cookiecutter' : 'values']: values, + }; - // Cookiecutter prefixes all parameters in templates with - // `cookiecutter.`. To replicate this, we wrap our parameters - // in an object with a `cookiecutter` property when compat - // mode is enabled. - const { cookiecutterCompat, values } = ctx.input; - const context = { - [cookiecutterCompat ? 'cookiecutter' : 'values']: values, - }; + ctx.logger.info( + `Processing ${allEntriesInTemplate.length} template files/directories with input values`, + ctx.input.values, + ); - ctx.logger.info( - `Processing ${allEntriesInTemplate.length} template files/directories with input values`, - ctx.input.values, - ); + const renderTemplate = await SecureTemplater.loadRenderer({ + cookiecutterCompat: ctx.input.cookiecutterCompat, + templateFilters, + templateGlobals, + nunjucksConfigs: { + trimBlocks: ctx.input.trimBlocks, + lstripBlocks: ctx.input.lstripBlocks, + }, + }); - const renderTemplate = await SecureTemplater.loadRenderer({ - cookiecutterCompat: ctx.input.cookiecutterCompat, - templateFilters, - templateGlobals, - nunjucksConfigs: { - trimBlocks: ctx.input.trimBlocks, - lstripBlocks: ctx.input.lstripBlocks, - }, - }); + for (const location of allEntriesInTemplate) { + let renderContents: boolean; - for (const location of allEntriesInTemplate) { - let renderContents: boolean; - - let localOutputPath = location; - if (extension) { - renderContents = extname(localOutputPath) === extension; - if (renderContents) { - localOutputPath = localOutputPath.slice(0, -extension.length); - } - // extension is mutual exclusive with copyWithoutRender/copyWithoutTemplating, - // therefore the output path is always rendered. + let localOutputPath = location; + if (extension) { + renderContents = extname(localOutputPath) === extension; + if (renderContents) { + localOutputPath = localOutputPath.slice(0, -extension.length); + } + // extension is mutual exclusive with copyWithoutRender/copyWithoutTemplating, + // therefore the output path is always rendered. + localOutputPath = renderTemplate(localOutputPath, context); + } else { + renderContents = !nonTemplatedEntries.has(location); + // The logic here is a bit tangled because it depends on two variables. + // If renderFilename is true, which means copyWithoutTemplating is used, + // then the path is always rendered. + // If renderFilename is false, which means copyWithoutRender is used, + // then matched file/directory won't be processed, same as before. + if (renderFilename) { localOutputPath = renderTemplate(localOutputPath, context); } else { - renderContents = !nonTemplatedEntries.has(location); - // The logic here is a bit tangled because it depends on two variables. - // If renderFilename is true, which means copyWithoutTemplating is used, - // then the path is always rendered. - // If renderFilename is false, which means copyWithoutRender is used, - // then matched file/directory won't be processed, same as before. - if (renderFilename) { - localOutputPath = renderTemplate(localOutputPath, context); - } else { - localOutputPath = renderContents - ? renderTemplate(localOutputPath, context) - : localOutputPath; - } - } - - if (containsSkippedContent(localOutputPath)) { - continue; - } - - const outputPath = resolveSafeChildPath(outputDir, localOutputPath); - if (fs.existsSync(outputPath) && !ctx.input.replace) { - continue; - } - - if (!renderContents && !extension) { - ctx.logger.info( - `Copying file/directory ${location} without processing.`, - ); - } - - if (location.endsWith('/')) { - ctx.logger.info( - `Writing directory ${location} to template output path.`, - ); - await fs.ensureDir(outputPath); - } else { - const inputFilePath = resolveSafeChildPath(templateDir, location); - const stats = await fs.promises.lstat(inputFilePath); - - if (stats.isSymbolicLink() || (await isBinaryFile(inputFilePath))) { - ctx.logger.info( - `Copying file binary or symbolic link at ${location}, to template output path.`, - ); - await fs.copy(inputFilePath, outputPath); - } else { - const statsObj = await fs.stat(inputFilePath); - ctx.logger.info( - `Writing file ${location} to template output path with mode ${statsObj.mode}.`, - ); - const inputFileContents = await fs.readFile(inputFilePath, 'utf-8'); - await fs.outputFile( - outputPath, - renderContents - ? renderTemplate(inputFileContents, context) - : inputFileContents, - { mode: statsObj.mode }, - ); - } + localOutputPath = renderContents + ? renderTemplate(localOutputPath, context) + : localOutputPath; } } - ctx.logger.info(`Template result written to ${outputDir}`); - }; + + if (containsSkippedContent(localOutputPath)) { + continue; + } + + const outputPath = resolveSafeChildPath(outputDir, localOutputPath); + if (fs.existsSync(outputPath) && !ctx.input.replace) { + continue; + } + + if (!renderContents && !extension) { + ctx.logger.info(`Copying file/directory ${location} without processing.`); + } + + if (location.endsWith('/')) { + ctx.logger.info(`Writing directory ${location} to template output path.`); + await fs.ensureDir(outputPath); + } else { + const inputFilePath = resolveSafeChildPath(templateDir, location); + const stats = await fs.promises.lstat(inputFilePath); + + if (stats.isSymbolicLink() || (await isBinaryFile(inputFilePath))) { + ctx.logger.info( + `Copying file binary or symbolic link at ${location}, to template output path.`, + ); + await fs.copy(inputFilePath, outputPath); + } else { + const statsObj = await fs.stat(inputFilePath); + ctx.logger.info( + `Writing file ${location} to template output path with mode ${statsObj.mode}.`, + ); + const inputFileContents = await fs.readFile(inputFilePath, 'utf-8'); + await fs.outputFile( + outputPath, + renderContents + ? renderTemplate(inputFileContents, context) + : inputFileContents, + { mode: statsObj.mode }, + ); + } + } + } + ctx.logger.info(`Template result written to ${outputDir}`); } function resolveTemplateActionSettings( diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/templateFile.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/templateFile.ts index 5f4dcb547d..e70e9435c5 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/templateFile.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/templateFile.ts @@ -95,24 +95,26 @@ export function createFetchTemplateFileAction(options: { }, }, supportsDryRun: true, - handler: createTemplateFileActionHandler({ - resolveTemplateFile: async ctx => { - ctx.logger.info('Fetching template file content from remote URL'); + handler: ctx => + createTemplateFileActionHandler({ + ctx, + resolveTemplateFile: async () => { + ctx.logger.info('Fetching template file content from remote URL'); - const workDir = await ctx.createTemporaryDirectory(); - // Write to a tmp file, render the template, then copy to workspace. - const tmpFilePath = path.join(workDir, 'tmp'); + const workDir = await ctx.createTemporaryDirectory(); + // Write to a tmp file, render the template, then copy to workspace. + const tmpFilePath = path.join(workDir, 'tmp'); - await fetchFile({ - baseUrl: ctx.templateInfo?.baseUrl, - fetchUrl: ctx.input.url, - outputPath: tmpFilePath, - token: ctx.input.token, - ...options, - }); - return tmpFilePath; - }, - ...options, - }), + await fetchFile({ + baseUrl: ctx.templateInfo?.baseUrl, + fetchUrl: ctx.input.url, + outputPath: tmpFilePath, + token: ctx.input.token, + ...options, + }); + return tmpFilePath; + }, + ...options, + }), }); } diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/templateFileActionHandler.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/templateFileActionHandler.ts index dce9b896dd..d82a1dd5a2 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/templateFileActionHandler.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/templateFileActionHandler.ts @@ -16,7 +16,6 @@ import { ScmIntegrations } from '@backstage/integration'; import { ActionContext, - TemplateActionOptions, TemplateFilter, TemplateGlobal, } from '@backstage/plugin-scaffolder-node'; @@ -36,19 +35,21 @@ export type TemplateFileActionInput = { lstripBlocks?: boolean; }; -export function createTemplateFileActionHandler< +export async function createTemplateFileActionHandler< I extends TemplateFileActionInput = TemplateFileActionInput, >(options: { - resolveTemplateFile: (ctx: ActionContext) => Promise; + ctx: ActionContext; + resolveTemplateFile: () => Promise; integrations: ScmIntegrations; additionalTemplateFilters?: Record; additionalTemplateGlobals?: Record; -}): TemplateActionOptions['handler'] { +}) { const { resolveTemplateFile, integrations, additionalTemplateFilters, additionalTemplateGlobals: templateGlobals, + ctx, } = options; const templateFilters = { @@ -56,45 +57,43 @@ export function createTemplateFileActionHandler< ...additionalTemplateFilters, }; - return async (ctx: ActionContext) => { - const outputPath = resolveSafeChildPath( - ctx.workspacePath, - ctx.input.targetPath, - ); - - if (fs.existsSync(outputPath) && !ctx.input.replace) { - ctx.logger.info( - `File ${ctx.input.targetPath} already exists in workspace, not replacing.`, - ); - return; - } - const filePath = await resolveTemplateFile(ctx); - - const { cookiecutterCompat, values } = ctx.input; - const context = { - [cookiecutterCompat ? 'cookiecutter' : 'values']: values, - }; + const outputPath = resolveSafeChildPath( + ctx.workspacePath, + ctx.input.targetPath, + ); + if (fs.existsSync(outputPath) && !ctx.input.replace) { ctx.logger.info( - `Processing template file with input values`, - ctx.input.values, + `File ${ctx.input.targetPath} already exists in workspace, not replacing.`, ); + return; + } + const filePath = await resolveTemplateFile(); - const renderTemplate = await SecureTemplater.loadRenderer({ - cookiecutterCompat, - templateFilters, - templateGlobals, - nunjucksConfigs: { - trimBlocks: ctx.input.trimBlocks, - lstripBlocks: ctx.input.lstripBlocks, - }, - }); - - const contents = await fs.readFile(filePath, 'utf-8'); - const result = renderTemplate(contents, context); - await fs.ensureDir(path.dirname(outputPath)); - await fs.outputFile(outputPath, result); - - ctx.logger.info(`Template file has been written to ${outputPath}`); + const { cookiecutterCompat, values } = ctx.input; + const context = { + [cookiecutterCompat ? 'cookiecutter' : 'values']: values, }; + + ctx.logger.info( + `Processing template file with input values`, + ctx.input.values, + ); + + const renderTemplate = await SecureTemplater.loadRenderer({ + cookiecutterCompat, + templateFilters, + templateGlobals, + nunjucksConfigs: { + trimBlocks: ctx.input.trimBlocks, + lstripBlocks: ctx.input.lstripBlocks, + }, + }); + + const contents = await fs.readFile(filePath, 'utf-8'); + const result = renderTemplate(contents, context); + await fs.ensureDir(path.dirname(outputPath)); + await fs.outputFile(outputPath, result); + + ctx.logger.info(`Template file has been written to ${outputPath}`); } diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/workspaceTemplate.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/workspaceTemplate.ts index 6e2be87757..caa76fc79f 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/workspaceTemplate.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/workspaceTemplate.ts @@ -90,13 +90,12 @@ export function createWorkspaceTemplateAction(options: { }, }, supportsDryRun: true, - handler: createTemplateActionHandler({ - resolveTemplate: async ctx => - resolveSafeChildPath( - ctx.workspacePath, - (ctx.input as any as { sourcePath: string }).sourcePath, - ), - ...options, - }), + handler: ctx => + createTemplateActionHandler({ + ctx, + resolveTemplate: async () => + resolveSafeChildPath(ctx.workspacePath, ctx.input.sourcePath), + ...options, + }), }); } diff --git a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/workspaceTemplateFile.ts b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/workspaceTemplateFile.ts index 57a6ad881c..8b42f3d02e 100644 --- a/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/workspaceTemplateFile.ts +++ b/plugins/scaffolder-backend/src/scaffolder/actions/builtin/fetch/workspaceTemplateFile.ts @@ -65,13 +65,12 @@ export function createWorkspaceTemplateFileAction(options: { }, }, supportsDryRun: true, - handler: createTemplateFileActionHandler({ - resolveTemplateFile: async ctx => - resolveSafeChildPath( - ctx.workspacePath, - (ctx.input as any as { sourcePath: string }).sourcePath, - ), - ...options, - }), + handler: ctx => + createTemplateFileActionHandler({ + ctx, + resolveTemplateFile: async () => + resolveSafeChildPath(ctx.workspacePath, ctx.input.sourcePath), + ...options, + }), }); }