[3.x] Empty the entire output directory before every build - #2586
Merged
Conversation
emmadesilva
temporarily deployed
to
pr-documentation-2586
August 10, 2026 19:05 — with
GitHub Actions
Inactive
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## master #2586 +/- ##
===========================================
Coverage 100.00% 100.00%
+ Complexity 1815 1811 -4
===========================================
Files 184 184
Lines 4506 4494 -12
===========================================
- Hits 4506 4494 -12 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
emmadesilva
force-pushed
the
v3/deterministic-output-builds
branch
from
August 10, 2026 19:20
ee7ed2c to
463f970
Compare
emmadesilva
temporarily deployed
to
pr-documentation-2586
August 10, 2026 19:20 — with
GitHub Actions
Inactive
emmadesilva
force-pushed
the
v3/deterministic-output-builds
branch
from
August 10, 2026 19:23
463f970 to
e7eee16
Compare
emmadesilva
temporarily deployed
to
pr-documentation-2586
August 10, 2026 19:23 — with
GitHub Actions
Inactive
emmadesilva
force-pushed
the
v3/deterministic-output-builds
branch
from
August 10, 2026 19:33
e7eee16 to
de42011
Compare
emmadesilva
temporarily deployed
to
pr-documentation-2586
August 10, 2026 19:34 — with
GitHub Actions
Inactive
emmadesilva
force-pushed
the
v3/deterministic-output-builds
branch
from
August 10, 2026 19:51
de42011 to
11a8e5b
Compare
emmadesilva
temporarily deployed
to
pr-documentation-2586
August 10, 2026 19:51 — with
GitHub Actions
Inactive
The output directory is now wholly owned by Hyde and recreated from source on every build, replacing the selective HTML/JSON cleaning that existed because there was no source-controlled home for arbitrary root files. Removes the `empty_output_directory` and `safe_output_directories` options along with the interactive confirmation, and guards against a misconfigured output directory resolving outside the project. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Covers dotfiles and nested directories being removed, static file output disappearing when its source is deleted, and the build failing when the output directory resolves outside the project. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
emmadesilva
force-pushed
the
v3/deterministic-output-builds
branch
from
August 10, 2026 20:21
11a8e5b to
5f76de4
Compare
emmadesilva
temporarily deployed
to
pr-documentation-2586
August 10, 2026 20:21 — with
GitHub Actions
Inactive
emmadesilva
temporarily deployed
to
pr-documentation-2586
August 11, 2026 01:29 — with
GitHub Actions
Inactive
emmadesilva
force-pushed
the
v3/deterministic-output-builds
branch
from
August 11, 2026 01:55
7df55b2 to
ffc20ca
Compare
emmadesilva
temporarily deployed
to
pr-documentation-2586
August 11, 2026 01:55 — with
GitHub Actions
Inactive
emmadesilva
force-pushed
the
v3/deterministic-output-builds
branch
from
August 11, 2026 01:56
ffc20ca to
b702110
Compare
emmadesilva
temporarily deployed
to
pr-documentation-2586
August 11, 2026 01:56 — with
GitHub Actions
Inactive
The output directory guard rejected symbolic links, upwards traversal, and any overlap with a list of project directories. Building outside the project is a real use case, such as a Hyde project in a home directory compiling into the server's public directory, and the symlink check broke builds pointing the output directory at a deploy target. Choosing an output directory that holds other files is a deliberate setting, so the build no longer second-guesses it. The project root is different: it is what an unfilled value resolves to rather than a directory anyone means to hand over, and emptying it destroys the project instead of the site. That leaves it as the only case worth checking, so it is checked where the build can still abort on it. The guard also ran in CleanSiteDirectory, where BuildTask::run() catches the exception, so it printed a failure and let the build continue with exit code 0. Only the command can abort, so the check lives there alone. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
An absolute output directory is resolved relative to the project root rather than rejected, so describing the path as one that must be a subdirectory of the project was misleading for the case users would hit. Name CNAME alongside the existing _static examples instead of arguing against keeping it in the compiled site, which is a practice only existing users have, and one the upgrade guide already covers. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The upgrade step prescribed deleting `hyde.empty_output_directory` from a published config file, but that option was never shipped in `config/hyde.php` and never documented, so no user has an entry to delete. The remaining instructions promised a configuration error for any output directory outside the project or overlapping one the project needs, which overstates what the build checks. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
emmadesilva
force-pushed
the
v3/deterministic-output-builds
branch
from
August 11, 2026 02:33
b702110 to
ad4dc93
Compare
emmadesilva
temporarily deployed
to
pr-documentation-2586
August 11, 2026 02:33 — with
GitHub Actions
Inactive
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Makes the site output directory disposable build output that is recreated from source on every build.
Follows on from #2585, which added
_staticas the source-controlled home for arbitrary root-level files. That was the only reason the cleaner had to preserve unknown files, so it no longer needs to.User-facing changes
_sitesurvived indefinitely — including output whose_staticsource had since been deleted, which is the bug that motivated this.hyde.empty_output_directoryandhyde.safe_output_directories, along with the confirmation prompt.InvalidConfigurationExceptionwhen the output directory is the project root, escapes the project, or is reached through a symbolic link.Local builds now match what a clean CI or GitHub Pages build has always produced: if a file should exist after a build, it comes from source.
The safelist and its prompt were a safeguard on a destructive operation, and that safeguard is replaced rather than dropped.
_staticremoves the reason to selectively preserve unknown files in the output; the deterministic-build contract removes the reason forempty_output_directory; but emptying a directory recursively still needs a guard. The replacement is structural and non-interactive, so it also works in CI, and it runs before the build writes anything, so it covers creating a site in the wrong place as well as emptying one.Validation rules
OutputDirectoryValidatorrejects a configuredhyde.output_directorythat:.segments);..segment;The overlap rule is what keeps
app,.git,vendor, and the source directories out of reach. The Hyde-owned entries are derived from runtime state — every registered page class'ssourceDirectory()(which already includessource_root), the configured media directory, and_static— so a project that renames its source directories is protected at its own names. Only the structural project directories are a fixed list:.git,.github,app,config,node_modules,resources,tests,vendor. Overlap is checked in both directions, so neitherappnorapp/storageis accepted, and an output directory that would contain a source directory is rejected too. The comparison is case-insensitive on every platform, because case-insensitive filesystems resolveAPPtoapp/— verified here, whereis_dir('APP')is true.realpath()would not have closed this: on macOS it echoes back the requested casing rather than the canonical one. Both sides also go through the same lexical segment normalization, so a media directory or source root configured as./assetsis matched by an output directory ofassets; upwards traversal is left intact there and rejected separately for the output path.This is not the safelist inverted. The safelist trusted a closed set of output names and interrogated everything else; this allows any name and refuses only what Hyde knows it must not destroy.
_site,docs,build,dist,public, andfoo/barare all accepted without a prompt.The symlink rule is what makes the traversal rules sound. Laravel's
deleteDirectory()only skips symlinked children — it iterates the root path it is given, so_site -> appwould have emptiedapp/. That is verified by test: with the symlink check removed, the target directory really is emptied. Rejecting links anywhere in the chain also covers an intermediate link whose final directory does not exist yet, whererealpath()returnsfalseand no destination-based check can see where the path leads.Comparing paths as strings was the earlier approach and is deliberately gone: Hyde's
path_join()always joins with/while the kernel base path keeps native separators, so arealpath()prefix comparison would have rejected valid fresh builds on Windows. Working from the configured relative path's segments avoids the comparison entirely.Internal changes
BuildSiteCommand::runPreBuildActions()before any work, because a misconfigured output directory is fatal to the whole build rather than to one task. Build tasks report failures without aborting, so a guard only insideCleanSiteDirectorywould have skipped the delete and then letcompileStaticPages()write the site to the bad path anyway.CleanSiteDirectory::handle()calls the same validator, so the destructive primitive keeps its own safety boundary when the task is invoked directly throughBuildTaskService. There the exception is caught as a task failure, but nothing is deleted.CleanSiteDirectoryis now registered unconditionally, soBuildTaskServiceUnitTestexpectations include it.StaticFilePassthroughTestleaving an empty_staticbehind, which made a repeat run fail.Testing
UnitFramework,FeatureFramework, andFeatureHydepass, apart from the known pre-existingFeaturedImageUnitTestfailure on PHP 8.5 (MediaFile::findHash()). HydeStan reports no errors.Covered: dotfiles and nested directories removed;
_staticoutput disappearing when its source is deleted; project root,..escape, output-directory symlink, and intermediate symlink all rejected; nothing created outside the project; a nested output directory that does not exist yet still builds; and directCleanSiteDirectoryinvocation refusing to empty an invalid destination.🤖 Generated with Claude Code