Abstract build steps to externalize the build configuration#6866
Abstract build steps to externalize the build configuration#6866alfonso-noriega wants to merge 1 commit intoproj-48450/asset-pipeline-for-hosted-appsfrom
Conversation
|
Warning This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
This stack of pull requests is managed by Graphite. Learn more about stacking. |
Coverage report
Test suite run success3808 tests passing in 1466 suites. Report generated by 🧪jest coverage report action from ef0c29e |
c4c3353 to
d2a2b21
Compare
510502e to
f9f6e10
Compare
27b8cfc to
34005f5
Compare
|
We detected some changes at Caution DO NOT create changesets for features which you do not wish to be included in the public changelog of the next CLI release. |
34005f5 to
c2808f7
Compare
f9f6e10 to
ff3f4e0
Compare
445a4c4 to
f14e0e0
Compare
ff3f4e0 to
635fedb
Compare
635fedb to
66deccf
Compare
f14e0e0 to
ef0c29e
Compare
| const destPath = joinPath(outputDir, destination) | ||
| await mkdir(dirname(destPath)) | ||
| await copyFile(sourcePath, destPath) | ||
| options.stdout.write(`Copied ${source} to ${destination}\n`) |
There was a problem hiding this comment.
copy_files destination path can escape outputDir (arbitrary write)
copySourceEntry builds destPath via joinPath(outputDir, destination). If destination is absolute or contains .., the resolved path can escape outputDir, enabling a build config (notably described as serializable/externalizable) to overwrite arbitrary files on the machine.
Evidence:
const destPath = joinPath(outputDir, destination)
await mkdir(dirname(destPath))
await copyFile(sourcePath, destPath)Impact includes overwriting repo/CI files, compromising developer/CI machines, supply-chain risk, and corrupted artifacts.
| resolvedPatterns, | ||
| resolvedIgnore, | ||
| definition.preserveStructure, | ||
| options, |
There was a problem hiding this comment.
copy_files pattern destination directory can escape outputDir (arbitrary write)
In pattern mode, definition.destination is joined into outputDir without sanitization:
const destinationDir = definition.destination ? joinPath(outputDir, definition.destination) : outputDirAbsolute paths or .. segments can escape outputDir, after which copyByPattern will create directories and write files there—especially dangerous because it can write many files outside the build output.
| const outputFile = joinPath(extension.outputPath, relativePathName) | ||
| if (filepath === outputFile) return | ||
| await copyFile(filepath, outputFile) | ||
| }), |
There was a problem hiding this comment.
Theme bundling writes under extension.outputPath and doesn’t ensure parent dirs exist
executeBundleThemeStep computes:
const outputFile = joinPath(extension.outputPath, relativePathName)
await copyFile(filepath, outputFile)If extension.outputPath is a file path (e.g., .../extension.js), this produces .../extension.js/<relative> and fails. Also, it does not mkdir(dirname(outputFile)), so nested paths fail unless directories already exist.
| } | ||
| return copySourceEntry(entry.source, entry.destination, baseDir, outputDir, options, preserveStructure) | ||
| }), | ||
| ) |
There was a problem hiding this comment.
Unbounded Promise.all copies can exhaust file descriptors on large file sets
Multiple places use Promise.all over potentially large arrays (copyFilesList, copyTomlKeyEntry, copyByPattern). For thousands of files, this can trigger EMFILE: too many open files, high memory usage, and flaky builds.
|
🤖 Code Review · #projects-dev-ai for questions ✅ Complete - 4 findings 📋 History✅ 4 findings |

WHY are these changes introduced?
Fixes #0000
Build steps pipeline infrastructure
Introduces a declarative, data-driven pipeline for executing extension build steps. This is the foundational layer that the next two PRs in the stack build on.
Problem
Previously, each extension's build logic was hardcoded imperatively inside
extension-instance.ts:build()via a largeswitchon the extension type. Adding a new extension type meant touchingcore infrastructure. Build behaviour was also untestable in isolation — you couldn't verify what an extension "would do" at build time without running the full build.
Solution
A typed pipeline where build logic is expressed as configuration, not code. Each extension declares an ordered list of
BuildStepobjects. The pipeline engine reads that config anddispatches to the right executor. Because steps are plain data, they are:
Architecture
The
BuildContextis threaded through every step — it carries the extension instance, build options (stdout/stderr), an abort signal, and astepResultsmap so later steps can read outputsfrom earlier ones.
Step configuration reference
copy_files— the most flexible stepSupports two strategies, selected via the
strategyfield.strategy: 'files'— explicit file listEach entry is one of:
{source: 'path'}{source: 'path', destination: 'out/file'}{tomlKey: 'static_root'}Dot-notation is supported for nested paths (
'nested.field'), and array-of-tables are automatically plucked across all entries.strategy: 'pattern'— glob-based selection from a source directorysourcestringpatternsstring[]**/*)ignorestring[]destinationstringpreserveStructurebooleantrue)Other step types
These wrap the existing build functions and require no additional config beyond
{}:build_themebuildThemeExtensionbundle_themebundleThemeExtensionbundle_uibuildUIExtensioncopy_static_assetsextension.copyStaticAssets()build_functionbuildFunctionExtensioncreate_tax_stub(()=>{})();output stubStep execution behaviour
BuildStepfieldcontinueOnErrorfalsetrue, a failed step is recorded but the pipeline continuesstopOnError(onBuildConfig)truefalse, all steps run regardless of failuresMeasuring impact
How do we know this change was effective? Please choose one:
Checklist