Skip to content

Harden build error handling and bugfixes#58

Merged
samdark merged 5 commits into
masterfrom
harden-build-error-handling
Jun 11, 2026
Merged

Harden build error handling and bugfixes#58
samdark merged 5 commits into
masterfrom
harden-build-error-handling

Conversation

@samdark

@samdark samdark commented Jun 11, 2026

Copy link
Copy Markdown
Member

Summary

  • add safe file writes and marker-based output directory replacement for no-cache builds
  • report invalid content configuration with friendlier build errors and source context
  • validate permalinks to prevent duplicates and output traversal
  • document the hardened build/import/preview behavior

Tests

  • make test

Summary by CodeRabbit

  • New Features

    • Safer builds with atomic output-directory replacement and marker-based swaps
    • Live-reload now serializes rebuilds and emits build-error events to the browser console
  • Bug Fixes

    • Invalid entry dates and malformed front matter now report the originating file path
    • Duplicate, traversal, repeated-slash, and missing-trailing-slash permalinks are rejected
  • Documentation

    • Clarified --no-cache behavior, permalink rules, and importer path option handling

Copilot AI review requested due to automatic review settings June 11, 2026 11:14
@coderabbitai

coderabbitai Bot commented Jun 11, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

Adds a FileWriter utility and migrates writers/commands to it, implements atomic output-directory staging and permalink validation in BuildCommand, hardens front-matter/date parsing with file-aware exceptions, serializes live-reload builds with build-error SSE reporting, tightens parallel-worker exit checks, and updates docs/tests.

Changes

Build and Preview Reliability Hardening

Layer / File(s) Summary
FileWriter utility for safe file persistence
src/Build/FileWriter.php
Adds FileWriter::write() (validated write) and FileWriter::writeAtomic() (temp-file + rename, cleanup on failure).
Consolidate file writing across writers and commands
src/Build/*, src/Console/InitCommand.php, src/Console/NewCommand.php, src/Import/Telegram/TelegramContentImporter.php
Replaces direct file_put_contents() usages with FileWriter::write() / writeAtomic() across many generators, importers, and init/new commands to standardize persistence and error handling.
Atomic output directory replacement and scoped cleanup
src/Console/BuildCommand.php
Adds OUTPUT_MARKER_FILE, prepares optional staging dir, enforces marker-based replaceability, scopes stale-output removal to the active output tree, writes marker before swap, and performs atomic replace with rollback and finally cleanup.
Front-matter and date parsing with file-aware errors
src/Content/Parser/FrontMatterParser.php, src/Content/Parser/EntryParser.php
FrontMatterParser throws on invalid YAML and rejects non-mapping front matter; EntryParser maps malformed date errors to InvalidContentConfigException including the source file path.
ImporterOption path flag and ImportCommand resolution
src/Import/ImporterOption.php, src/Console/ImportCommand.php, src/Import/Telegram/TelegramContentImporter.php
Adds bool $path to ImporterOption; ImportCommand resolves only path-marked options with resolvePath(); Telegram importer marks directory as a path option; docs/tests updated.
Live-reload serialization and build-error SSE reporting
src/Console/ServeCommand.php, src/Web/LiveReload/SiteBuildRunner.php, src/Web/DevServer/assets/live-reload.js
SiteBuildRunner acquires per-output lock and captures last build output; ServeCommand broadcasts reload only on success and emits formatted build-error SSE on failure; client logs build-error to console.
Parallel worker exit-state validation
src/Build/ParallelEntryWriter.php, src/Build/ParallelTaskRunner.php
Adds pcntl_wifexited() checks and requires both normal exit and zero exit status for worker success; worker result files now written with FileWriter; tests assert signaled workers fail.
Docs, tests, and CI runner pinning
docs/*, tests/Unit/*, .github/workflows/*
Docs updated for --no-cache semantics, permalinks, importer path option, and live-reload behavior; tests added/extended for manifest recovery, permalink validation, parser errors, import option handling, live-reload build output capture, and workflows pin Windows/macOS runners and action SHAs.

Sequence Diagram(s)

sequenceDiagram
  participant ServeCommand
  participant SiteBuildRunner
  participant SSEStream
  participant BrowserClient
  ServeCommand->>SiteBuildRunner: buildLiveReloadSite()
  SiteBuildRunner-->>ServeCommand: (bool success) + lastOutput()
  alt success
    ServeCommand->>SSEStream: formatServerSentEvent("reload", payload)
    SSEStream->>BrowserClient: reload
  else failure
    ServeCommand->>SSEStream: formatServerSentEvent("build-error", lastOutput)
    SSEStream->>BrowserClient: build-error
    BrowserClient->>BrowserClient: console.error(event.data)
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • yiipress/engine#47: Updates BuildCommand error handling around FriendlyExceptionInterface and content-config exceptions.

"I'm a rabbit with a tiny pen,
Writing files again and again 🐇,
Atomic swaps that skip the trash,
Permalinks checked — no path can slash,
Build-errors whisper to the browser's console then."

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 18.18% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title 'Harden build error handling' accurately summarizes the main focus of the changeset, which includes improved error handling, validation, safe file writes, and output directory protection across multiple build-related files.
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.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ 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 harden-build-error-handling

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

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

Copilot AI 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.

Pull request overview

This PR hardens the build/import/preview pipeline by making filesystem writes safer, improving build-time error reporting with source context, and adding stricter validation around generated output paths (permalinks) and output directory replacement behavior.

Changes:

  • Introduces FileWriter and migrates many file_put_contents call sites to checked/atomic writes.
  • Improves build-time diagnostics with friendly exceptions for invalid front matter/YAML and malformed dates, including file path context.
  • Hardens build output behavior: validates permalinks for duplicates/traversal and adds marker-based atomic replacement for --no-cache rebuilds; live-reload now reports build failures to the browser console.

Reviewed changes

Copilot reviewed 35 out of 35 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
tests/Unit/Web/LiveReload/SiteBuildRunnerTest.php Asserts build failure output is captured and exposed.
tests/Unit/Content/Parser/FrontMatterParserTest.php Adds coverage for friendly exceptions on malformed YAML front matter.
tests/Unit/Content/Parser/EntryParserTest.php Adds coverage for friendly exceptions on malformed date front matter.
tests/Unit/Console/ImportCommandTest.php Ensures non-path options aren’t mistakenly path-resolved.
tests/Unit/Console/BuildCommandTest.php Adds coverage for friendly content config errors, permalink validation, and marker-based output replacement.
tests/Unit/Build/ParallelTaskRunnerTest.php Ensures signaled workers are treated as failures.
tests/Unit/Build/BuildManifestTest.php Ensures corrupted manifests load safely as empty.
src/Web/LiveReload/SiteBuildRunner.php Serializes preview builds with a lock and captures last build output.
src/Web/DevServer/assets/live-reload.js Logs build-error SSE events to the browser console.
src/Import/Telegram/TelegramContentImporter.php Uses FileWriter and marks directory option as a path.
src/Import/ImporterOption.php Adds path flag to distinguish path vs non-path options.
src/Content/Parser/FrontMatterParser.php Throws InvalidContentConfigException with file context for invalid YAML front matter.
src/Content/Parser/EntryParser.php Wraps malformed date parsing in a friendly content config exception.
src/Console/ServeCommand.php Sends build-error SSE events and properly formats SSE data lines.
src/Console/NewCommand.php Uses FileWriter for generated content file writes.
src/Console/InitCommand.php Uses FileWriter for initial project scaffolding writes.
src/Console/ImportCommand.php Resolves paths only for options explicitly marked as paths.
src/Console/BuildCommand.php Adds permalink validation, safer output cleanup, marker-based atomic replacement for --no-cache, and friendly exception reporting.
src/Build/TaxonomyPageWriter.php Uses FileWriter for taxonomy page writes.
src/Build/SearchIndexGenerator.php Uses FileWriter for search index writes.
src/Build/RedirectPageWriter.php Uses FileWriter for redirect page writes.
src/Build/ParallelTaskRunner.php Uses FileWriter and treats signaled workers as failures.
src/Build/ParallelEntryWriter.php Uses FileWriter and treats signaled workers as failures.
src/Build/NotFoundPageWriter.php Uses FileWriter for 404 page writes.
src/Build/FileWriter.php Adds checked and atomic write helpers.
src/Build/FeedGenerator.php Writes feeds via FileWriter using in-memory generation.
src/Build/DateArchiveWriter.php Uses FileWriter for archive page writes.
src/Build/CollectionListingWriter.php Uses FileWriter for listing page writes.
src/Build/BuildManifest.php Handles corrupted JSON gracefully and saves atomically.
src/Build/BuildCache.php Uses FileWriter for cache writes.
src/Build/AuthorPageWriter.php Uses FileWriter for author page writes.
docs/preview.md Documents serialized rebuilds and build-error console reporting.
docs/importing-content.md Documents ImporterOption::$path.
docs/content.md Documents stricter front matter and permalink behavior.
docs/commands.md Documents marker-based output replacement for --no-cache.
Comments suppressed due to low confidence (1)

src/Build/BuildManifest.php:51

  • In BuildManifest::load(), when the decoded JSON is not an array, only $entries is reset. If the same BuildManifest instance is reused after a successful load (or after in-memory mutations), $configFiles and $trackedDirectories can remain stale even though the manifest is effectively empty, which can lead to incorrect incremental decisions.
        if (!is_array($data)) {
            $this->entries = [];
            return;
        }

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread src/Console/BuildCommand.php
Comment thread src/Console/BuildCommand.php

@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: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (1)
src/Build/BuildManifest.php (1)

48-51: ⚠️ Potential issue | 🟡 Minor | ⚡ Quick win

Inconsistent state reset when manifest data is invalid.

When json_decode throws an exception (lines 40-47), all three properties (entries, configFiles, trackedDirectories) are cleared. However, when the decoded data is not an array (lines 48-51), only entries is reset. Both cases indicate corrupted manifest data and should reset all properties consistently to avoid stale metadata.

🛡️ Proposed fix to reset all properties consistently
 if (!is_array($data)) {
     $this->entries = [];
+    $this->configFiles = [];
+    $this->trackedDirectories = [];
     return;
 }
🤖 Prompt for 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.

In `@src/Build/BuildManifest.php` around lines 48 - 51, When decoded manifest data
is not an array, reset the same three properties that are cleared on JSON decode
failure to avoid stale metadata: set $this->entries, $this->configFiles, and
$this->trackedDirectories to empty arrays and return; update the non-array
handling in BuildManifest (the method that processes the decoded $data) so it
clears all three properties consistently with the exception handler.
🤖 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/Web/LiveReload/SiteBuildRunner.php`:
- Around line 21-24: The current lock logic opens a temp lock file and calls
flock but ignores failures; update the SiteBuildRunner locking code to detect
and handle both fopen() and flock() failures: check the return of fopen(...)
into $lock and if it is false log a warning (or throw an exception) and abort
the build to avoid concurrent runs; if fopen succeeds, check the boolean return
of flock($lock, LOCK_EX) and if that call returns false log a warning (including
the lock filename/hash produced by hash('xxh128', $this->outputDir)) and abort
or fallback to a safe behavior rather than proceeding; ensure you close/unlink
the handle on error and use the same $lock handle for unlock after the build
completes.

In `@tests/Unit/Build/ParallelTaskRunnerTest.php`:
- Around line 72-89: The test uses posix_kill and the SIGKILL constant but they
are not referenced/imported, causing a fatal error; update the test file to
import or reference the POSIX functions/constants so posix_kill(getmypid(),
SIGKILL) resolves (e.g., add the POSIX extension function and signal constant
usage for posix_kill and SIGKILL at the top of ParallelTaskRunnerTest or
fully-qualify their usage) ensuring the call in
testRunTreatsSignaledWorkerAsFailure continues to signal the current process
correctly.

---

Outside diff comments:
In `@src/Build/BuildManifest.php`:
- Around line 48-51: When decoded manifest data is not an array, reset the same
three properties that are cleared on JSON decode failure to avoid stale
metadata: set $this->entries, $this->configFiles, and $this->trackedDirectories
to empty arrays and return; update the non-array handling in BuildManifest (the
method that processes the decoded $data) so it clears all three properties
consistently with the exception handler.
🪄 Autofix (Beta)

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: defaults

Review profile: CHILL

Plan: Pro

Run ID: ee71d3c0-e09e-495b-b42d-86c2ca4ca4a1

📥 Commits

Reviewing files that changed from the base of the PR and between 5bceedc and c2a4bf8.

📒 Files selected for processing (35)
  • docs/commands.md
  • docs/content.md
  • docs/importing-content.md
  • docs/preview.md
  • src/Build/AuthorPageWriter.php
  • src/Build/BuildCache.php
  • src/Build/BuildManifest.php
  • src/Build/CollectionListingWriter.php
  • src/Build/DateArchiveWriter.php
  • src/Build/FeedGenerator.php
  • src/Build/FileWriter.php
  • src/Build/NotFoundPageWriter.php
  • src/Build/ParallelEntryWriter.php
  • src/Build/ParallelTaskRunner.php
  • src/Build/RedirectPageWriter.php
  • src/Build/SearchIndexGenerator.php
  • src/Build/TaxonomyPageWriter.php
  • src/Console/BuildCommand.php
  • src/Console/ImportCommand.php
  • src/Console/InitCommand.php
  • src/Console/NewCommand.php
  • src/Console/ServeCommand.php
  • src/Content/Parser/EntryParser.php
  • src/Content/Parser/FrontMatterParser.php
  • src/Import/ImporterOption.php
  • src/Import/Telegram/TelegramContentImporter.php
  • src/Web/DevServer/assets/live-reload.js
  • src/Web/LiveReload/SiteBuildRunner.php
  • tests/Unit/Build/BuildManifestTest.php
  • tests/Unit/Build/ParallelTaskRunnerTest.php
  • tests/Unit/Console/BuildCommandTest.php
  • tests/Unit/Console/ImportCommandTest.php
  • tests/Unit/Content/Parser/EntryParserTest.php
  • tests/Unit/Content/Parser/FrontMatterParserTest.php
  • tests/Unit/Web/LiveReload/SiteBuildRunnerTest.php

Comment thread src/Web/LiveReload/SiteBuildRunner.php Outdated
Comment thread tests/Unit/Build/ParallelTaskRunnerTest.php

Copilot AI 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.

Pull request overview

Copilot reviewed 38 out of 38 changed files in this pull request and generated 2 comments.

Comment thread src/Build/BuildManifest.php
Comment thread src/Console/BuildCommand.php

@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.

🧹 Nitpick comments (1)
tests/Unit/Packaging/ConfigurationPackagingTest.php (1)

259-310: ⚡ Quick win

Consider validating release.yml runner and action pinning for consistency.

The test packageWorkflowPublishesNightlyBuilds() validates runner labels and SHA pinning for package-static.yml (line 261), but there's no equivalent validation for release.yml. While releaseWorkflowBuildsNotesFromPreviousStableReleaseTag() (line 378) does load release.yml, it only validates release note generation logic.

Both workflows produce release artifacts and should use consistent runner versions and action SHAs. Without explicit validation, they could drift in future changes.

🧪 Suggested test enhancement

Consider adding assertions in an existing test or creating a new test to validate release.yml:

// In releaseWorkflowBuildsNotesFromPreviousStableReleaseTag() or a new test method
self::assertStringContainsString('runs-on: windows-2022', $workflow);
self::assertStringContainsString('runs-on: macos-14', $workflow);
self::assertDoesNotMatchRegularExpression('/uses:\s+[^@\s]+@v\d+/', $workflow);

This would ensure both workflows remain consistent and prevent drift.

🤖 Prompt for 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.

In `@tests/Unit/Packaging/ConfigurationPackagingTest.php` around lines 259 - 310,
Add equivalent runner and action-pinning assertions for release.yml to prevent
drift: in releaseWorkflowBuildsNotesFromPreviousStableReleaseTag() (or a new
test) load release.yml the same way packageWorkflowPublishesNightlyBuilds()
loads package-static.yml, then assert presence of runner labels like 'runs-on:
windows-2022' and 'runs-on: macos-14',
assertDoesNotMatchRegularExpression('/uses:\s+[^@\s]+@v\d+/', $workflow) to
ensure SHAs are pinned, and optionally mirror any other checks you rely on from
packageWorkflowPublishesNightlyBuilds() (e.g., specific artifact names or
persist-credentials count) for consistency.
🤖 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.

Nitpick comments:
In `@tests/Unit/Packaging/ConfigurationPackagingTest.php`:
- Around line 259-310: Add equivalent runner and action-pinning assertions for
release.yml to prevent drift: in
releaseWorkflowBuildsNotesFromPreviousStableReleaseTag() (or a new test) load
release.yml the same way packageWorkflowPublishesNightlyBuilds() loads
package-static.yml, then assert presence of runner labels like 'runs-on:
windows-2022' and 'runs-on: macos-14',
assertDoesNotMatchRegularExpression('/uses:\s+[^@\s]+@v\d+/', $workflow) to
ensure SHAs are pinned, and optionally mirror any other checks you rely on from
packageWorkflowPublishesNightlyBuilds() (e.g., specific artifact names or
persist-credentials count) for consistency.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 47eb361a-4464-4518-be0d-cdf1b84915c8

📥 Commits

Reviewing files that changed from the base of the PR and between c2a4bf8 and 39307ca.

📒 Files selected for processing (10)
  • .github/workflows/package-static.yml
  • .github/workflows/release.yml
  • docs/content.md
  • src/Build/BuildManifest.php
  • src/Console/BuildCommand.php
  • src/Web/LiveReload/SiteBuildRunner.php
  • tests/Unit/Build/BuildManifestTest.php
  • tests/Unit/Build/ParallelTaskRunnerTest.php
  • tests/Unit/Console/BuildCommandTest.php
  • tests/Unit/Packaging/ConfigurationPackagingTest.php
✅ Files skipped from review due to trivial changes (1)
  • docs/content.md
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/Web/LiveReload/SiteBuildRunner.php

@samdark samdark changed the title Harden build error handling Harden build error handling and bugfixes Jun 11, 2026
@samdark samdark merged commit 315ad8d into master Jun 11, 2026
7 checks passed
@samdark samdark deleted the harden-build-error-handling branch June 11, 2026 12:31
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.

2 participants