Skip to content

fix(build): refuse to clear an output directory no build produced - #3601

Merged
kojiwakayama merged 4 commits into
mainfrom
fix/dx-20260811-r2-5
Aug 11, 2026
Merged

kojiwakayama merged 4 commits into
mainfrom
fix/dx-20260811-r2-5

Conversation

@kojiwakayama

@kojiwakayama kojiwakayama commented Aug 11, 2026 •

Copy link
Copy Markdown
Contributor

What broke

veryfront build empties its output directory before writing. It never checked
whose directory it was, so a project that already kept anything in dist/ lost
it — silently. Reproduced against published 0.1.1229 (Deno-installed CLI,
sandbox outside the platform checkout):

$ veryfront init host --template minimal --runtime node --yes
$ printf 'console.log(1)\n' > host/dist/index.js
$ printf 'do not delete\n'  > host/dist/IMPORTANT.txt
$ printf 'deep\n'           > host/dist/nested/deep.txt
$ cd host && veryfront build
  ● Building...
  ✓ Built in 2.24s
    2 pages, 0 chunks, 1 asset
    6.16 KB in dist
EXIT=0
$ find dist -type f    # index.js, IMPORTANT.txt, nested/deep.txt: GONE

Not a word about clearing, emptying or removing anything, and exit 0. The same
happened to a configured build.outDir, so the fix that made build.outDir be
honoured only moved the target.

What changed

setupBuildDirectories now decides whether the output directory is the build's
to delete, and deletes only what it can show is its own:

  • holds a _veryfront/ directory → a previous build's output, cleared as
    before (the setup step creates that entry before anything else is written, so
    every directory the build has ever owned carries it, including from older
    releases and from builds that failed halfway — no new marker file, no upgrade
    cliff). A file or symlink of that name is not that directory and does not
    count;
  • empty → cleared as before;
  • absent → nothing to clear (this also drops the
    ! Operation failed, using fallback err=NotFound ... remove '.../dist' line
    that every first build printed);
  • cannot be listed — no permission, a transient filesystem error, a path that
    is a regular file → refused, because a failed inspection proves nothing about
    what is there;
  • anything else → the build stops with [build-failed] naming the directory,
    the entries it found, and the two ways out.
  ✗ Refusing to clear dist: it holds files that no Veryfront build wrote
    (IMPORTANT.txt, index.js, nested). The build empties its output directory
    before writing, which would delete them. Move or delete that directory
    yourself, or build somewhere else with -o/--output or `build: { outDir }`
    in veryfront.config.ts.

The message names the path the developer configured. The CLI resolves the
output to an absolute path before the build sees it, and printing that would
put the machine's filesystem layout into human and --json output alike; an
output outside the project falls back to its last segment rather than a chain
of ../.

The entry names go through the same filter. They come off the filesystem, so a
file can be called anything a filesystem allows: printed straight, a name like
\x1b[2Kfake.txt would not appear in the message but rewrite the line, erasing
the sentence that says what to do about the refusal. Control characters render
as ?, and long names are cut.

Warning and deleting anyway was rejected: the files are still gone, and a
warning after an irreversible deletion is a report, not a choice.

Verification

Regression tests in src/build/production-build/build/build-setup.test.ts
(written first; the refusal cases failed with "Expected function to reject"
and the seeded files deleted before the fix):

  • refuses a foreign output directory and leaves every file, nested included,
    without naming the machine's paths in the message;
  • refuses an output whose listing fails, and asserts remove was never called;
  • refuses a directory whose _veryfront is a file rather than a directory;
  • renders a filename carrying an ANSI erase sequence with no control character
    left in the message;
  • does not remove an absent directory;
  • clears an empty pre-existing directory;
  • still clears stale artifacts from a directory carrying _veryfront/.

Against a build of this branch, driving the finding's own repro in the same
published-0.1.1229 scaffold:

case result
seeded dist/{index.js,IMPORTANT.txt,nested/deep.txt} EXIT=1, all three intact
build.outDir: "custom-out" holding KEEP.txt EXIT=1, KEEP.txt intact
dist/_veryfront as a file, beside a sentinel EXIT=1, sentinel intact
dist as a regular file EXIT=1, file intact
dist/ removed, first build EXIT=0, builds, no NotFound warning
rebuild over our own dist/ with a stale file EXIT=0, stale file cleared
--dry-run over a foreign dist/ EXIT=0, untouched

Suites: src/build/production-build, cli/commands/build, and the full
pre-push gate — all green.

Notes

No documentation change is needed for this one; the behaviour is now
self-describing at the point of failure. Hosted builds stage into a fresh
per-build directory and never reach the refusal.

`veryfront build` emptied its output directory before writing, so a project
that already kept something in `dist/` — its own bundle, a checked-in artifact,
anything — lost it. The CLI printed nothing about the deletion: the developer
saw `✓ Built in 1.35s` and discovered the loss later, from the absence. The
same happened to whatever `build.outDir` pointed at, so moving the framework
out of the project's `dist/` only moved the target.

A directory that holds `_veryfront/` is a previous build's output: the setup
step creates that entry before anything else is written, so every directory the
build has ever owned carries it, including one left by an older release or by a
build that failed halfway. Deleting anything else is not a step a developer can
be assumed to have consented to, and it cannot be undone, so the build now
stops and says what it found and where to send its output instead. Empty and
absent directories are unaffected, so first builds and rebuilds over our own
output behave exactly as before — and an absent directory is no longer
"removed", which also drops the `! Operation failed, using fallback
err=NotFound ... remove '.../dist'` line every first build printed.

Constraint: Existing projects must keep rebuilding without an opt-in flag, so
ownership has to be inferred from output an older release already wrote.

Rejected: Warn and delete anyway | The files are still gone; a warning after an
irreversible deletion is a report, not a choice.

Rejected: Delete only entries a build wrote | Requires a manifest no previous
release left behind, and still guesses at the rest.

Confidence: high

Scope-risk: narrow

Reversibility: clean

Tested: build-setup unit tests (foreign dir refused and intact, empty dir
cleared, absent dir not removed, our own dir still cleared of stale files);
src/build + cli/commands/build suites; tests/integration/server/build; the
finding's own repro against a published-0.1.1229 scaffold — seeded
dist/index.js, dist/IMPORTANT.txt, dist/nested/deep.txt survive with EXIT=1,
and the same for a configured build.outDir

Not-tested: hosted/cloud builds, which stage into a fresh per-build directory
and so never reach the refusal
@coderabbitai

coderabbitai Bot commented Aug 11, 2026 •

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@kojiwakayama, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 23 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 3d16fbc4-3820-45ad-b125-0af5f5b95f07

📥 Commits

Reviewing files that changed from the base of the PR and between 64d6850 and e9b0c49.

📒 Files selected for processing (2)
  • src/build/production-build/build/build-setup.test.ts
  • src/build/production-build/build/build-setup.ts
📝 Walkthrough

Walkthrough

Build setup now checks output-directory ownership before deletion. It rejects unreadable or non-empty foreign directories, while allowing absent, empty, and previously marked Veryfront directories. Tests cover these cases and message path redaction.

Changes

Output Directory Safety

Layer / File(s) Summary
Output ownership validation
src/build/production-build/build/build-setup.ts
The build inspects output directories before clearing them. It allows absent, empty, and _veryfront-marked directories. It rejects unreadable or unowned non-empty directories and formats safe error messages.
Ownership safeguard test coverage
src/build/production-build/build/build-setup.test.ts
Tests verify rejection without deletion for foreign, unreadable, and marker-file outputs. Tests also cover absent, empty, dry-run, and prior-build outputs.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Possibly related PRs

Suggested reviewers: kwakayama

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely summarizes the main change: preventing builds from clearing output directories not produced by Veryfront.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/dx-20260811-r2-5

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 805720ae44

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread src/build/production-build/build/build-setup.ts Outdated
Comment thread src/build/production-build/build/build-setup.ts Outdated
Comment thread src/build/production-build/build/build-setup.ts Outdated
Review of the refusal found three ways it still authorized the deletion it
exists to stop.

An output directory that could not be listed — no permission, a transient
filesystem error, a path that is a regular file rather than a directory — was
treated as ours and removed recursively. A failed inspection proves nothing
about what is there, so it now refuses and says so, the same way a foreign
directory does.

Ownership was claimed by any entry named `_veryfront`. The marker is the
directory this step creates, so a project that keeps a file or a symlink of
that name in its own output had everything beside it deleted. Only a directory
of that name counts now.

The refusal interpolated the output path, which the CLI has already resolved to
an absolute path, putting the machine's filesystem layout into human and
`--json` output alike (AGENTS.md, "Secret and internal-detail safety"). It now
names the path the developer configured — `dist`, `custom-out` — falling back
to the last segment for an output outside the project rather than printing a
chain of `../`.

Confidence: high

Scope-risk: narrow

Reversibility: clean

Tested: build-setup unit tests (uninspectable output refused and intact,
`_veryfront` file refused, refusal free of machine paths) plus the existing
cases; src/build/production-build + cli/commands/build suites; against a
published-0.1.1229 scaffold with a local build — foreign dist/ and a foreign
`build: { outDir }` refused with their seeded files intact, dist/ as a regular
file refused, first build into an absent dist/ and a rebuild over our own
output still green with stale artifacts cleared

Not-tested: a directory readable by stat but not by readdir, which needs a
permission setup the suite cannot make portably; covered by injecting the
listing failure instead

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 3

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@src/build/production-build/build/build-setup.test.ts`:
- Around line 38-42: Replace all direct Deno.* filesystem calls in the test
file, including the fs.remove and fs.readDir implementations and the additional
affected test paths, with the existing runtime-neutral filesystem helpers. Keep
the ownership-safety tests’ behavior unchanged while ensuring no \bDeno\.
references remain anywhere in the file.

In `@src/build/production-build/build/build-setup.ts`:
- Around line 113-123: Sanitize each arbitrary filesystem name before
constructing the refusal message in the build-setup flow: escape control and
terminal characters, redact or otherwise safely handle sensitive identifiers,
and enforce a maximum displayed length per name. Update the names mapping used
by preview and add coverage for an entry name containing control characters,
while preserving the existing preview limit and refusal behavior.
- Around line 35-40: Update the output-directory cleanup flow around
outputDirectoryNeedsClearing and handleErrorWithFallback so ownership validation
and recursive removal use the same directory identity atomically. Replace the
separate validation-then-remove sequence with an adapter operation that binds
the check to deletion, or detect and fail if outputDir is replaced between those
steps; preserve the existing error fallback behavior.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: fec81f3e-3343-46ee-8242-2c84b8716ae6

📥 Commits

Reviewing files that changed from the base of the PR and between 6f30f87 and 626dc06.

📒 Files selected for processing (2)
  • src/build/production-build/build/build-setup.test.ts
  • src/build/production-build/build/build-setup.ts

Comment thread src/build/production-build/build/build-setup.test.ts
Comment thread src/build/production-build/build/build-setup.ts
Comment thread src/build/production-build/build/build-setup.ts Outdated
The refusal lists what it found in the output directory, and those names come
off the filesystem: a file can be called anything a filesystem allows, control
characters and ANSI escapes included. Printed straight, a name like
`\x1b[2Kfake.txt` does not appear in the message — it rewrites the line, and the
sentence it erases is the one that tells the developer what to do about the
refusal. Control characters now render as `?`, the way `ls` shows them, and a
long name is cut so one entry cannot push the rest off screen. The output path
goes through the same treatment, since a project directory can be named the
same way.

Confidence: high

Scope-risk: narrow

Reversibility: clean

Tested: build-setup unit tests, including a new case that writes a file whose
name carries an ANSI erase sequence and asserts the message carries no control
character at all; the refusal, first-build and rebuild paths re-driven against
a published-0.1.1229 scaffold
@kojiwakayama
kojiwakayama added this pull request to the merge queue Aug 11, 2026
Merged via the queue into main with commit e656a0c Aug 11, 2026
33 checks passed
@kojiwakayama
kojiwakayama deleted the fix/dx-20260811-r2-5 branch August 11, 2026 22:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant