unpack built-in blueprints - #265
Conversation
maxwellpeterson
commented
Aug 19, 2026
- store blueprint source files in source control instead of .gadget archive
- this makes it much easier to develop and review changes to built-in blueprints
- store gadget source files in source control instead of .gadget archives - this makes it much easier to develop and review changes to built-in blueprints
Preview:
|
| for (const entry of (await readdir(sourceDir, {withFileTypes: true})) | ||
| .filter(entry => entry.isDirectory()).toSorted((a, b) => a.name < b.name ? -1 : 1)) { |
There was a problem hiding this comment.
🟡 Re-importing a bundled format after an interrupted import silently updates a leftover hidden folder instead of the real blueprint
Leftover hidden staging/backup folders from an interrupted import are treated as real blueprints (readdir(...).filter(entry => entry.isDirectory()) at packages/workshop-backend/scripts/import-format-blueprint.ts:45-46) when looking up which blueprint to update, so a later import can write the new code into the leftover folder and leave the shipped blueprint unchanged.
Impact: The import command reports success while the bundled format keeps its old code, so the update never reaches deployments.
How a hidden leftover directory hijacks the blueprintId lookup
The installer stages into .<name>.import-<pid>/ and moves the previous directory to .<name>.backup-<pid>/ (packages/workshop-backend/scripts/import-format-blueprint.ts:144-157). If the process is interrupted (Ctrl-C, I/O error where the restore rename fails with ENOTEMPTY), one of those hidden directories survives, containing a blueprint.json with the same blueprintId.
On the next run, the manifest scan does not skip dot-prefixed entries, so both the leftover and the real directory are collected. Because . sorts before letters, manifests.find(candidate => candidate.blueprintId === blueprintId) (line 86) resolves to the leftover, making entry.name the hidden directory: the new files are written there, workspace-docs/ is untouched, and the generator (packages/workshop-backend/scripts/build-format-blueprints.ts:126-127, which deliberately filters dot entries — see scripts/build-format-blueprints.test.ts) keeps bundling the stale source. The only hint is the directory name printed in the summary.
Filtering entry.name.startsWith(".") here, matching the generator, avoids it.
| for (const entry of (await readdir(sourceDir, {withFileTypes: true})) | |
| .filter(entry => entry.isDirectory()).toSorted((a, b) => a.name < b.name ? -1 : 1)) { | |
| for (const entry of (await readdir(sourceDir, {withFileTypes: true})) | |
| .filter(entry => entry.isDirectory() && !entry.name.startsWith(".")) | |
| .toSorted((a, b) => a.name < b.name ? -1 : 1)) { |
Was this helpful? React with 👍 or 👎 to provide feedback.