Skip to content

Harden build manifest persistence#61

Merged
samdark merged 1 commit into
masterfrom
fix-build-manifest-atomic-save
Jun 20, 2026
Merged

Harden build manifest persistence#61
samdark merged 1 commit into
masterfrom
fix-build-manifest-atomic-save

Conversation

@samdark

@samdark samdark commented Jun 13, 2026

Copy link
Copy Markdown
Member

Summary

  • treat corrupt build manifests as cache misses instead of crashing future builds
  • save manifests through a same-directory temporary file and rename into place
  • throw when manifest writes or replacement fail

Tests

  • make test -- --filter BuildManifestTest
  • make psalm
  • make test

Summary by CodeRabbit

  • Bug Fixes

    • Build system now automatically recovers from corrupted, missing, or unreadable manifest files by resetting incremental state and performing a full rebuild instead of failing.
  • Documentation

    • Updated caching documentation to clarify incremental build manifest behavior and atomic write operations for better reliability.
  • Tests

    • Added comprehensive tests for manifest file cleanup and error handling during save operations.

Copilot AI review requested due to automatic review settings June 13, 2026 08:41
@coderabbitai

coderabbitai Bot commented Jun 13, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro

Run ID: 644b5923-8891-4b46-a6fb-62c1de0561d1

📥 Commits

Reviewing files that changed from the base of the PR and between 3e1c60d and 5022ba1.

📒 Files selected for processing (3)
  • docs/engine.md
  • src/Build/FileWriter.php
  • tests/Unit/Build/BuildManifestTest.php
✅ Files skipped from review due to trivial changes (1)
  • docs/engine.md

📝 Walkthrough

Walkthrough

FileWriter::write() adds an optional $flags parameter passed to error-suppressed file_put_contents. FileWriter::writeAtomic() applies LOCK_EX on temp-file write and suppresses rename() errors before throwing. Three BuildManifest::save() tests verify *.tmp cleanup on success, write failure, and rename failure. Docs note atomic manifest writes and disposable-cache behavior.

Changes

FileWriter Atomic Write Hardening and Save Cleanup Tests

Layer / File(s) Summary
FileWriter flags parameter and atomic rename suppression
src/Build/FileWriter.php
write() gains an optional $flags parameter forwarded to error-suppressed file_put_contents. writeAtomic() passes LOCK_EX to write() for the temp file and switches to @rename() before throwing on replacement failure.
BuildManifest::save() temp-file cleanup tests
tests/Unit/Build/BuildManifestTest.php
Imports RuntimeException and adds testSaveDoesNotLeaveTemporaryManifestFile(), testSaveCleansTemporaryManifestFileWhenWriteFails(), and testSaveCleansTemporaryManifestFileWhenRenameFails(). A private manifestTemporaryFiles() helper globs *.tmp files in the test temp directory.
Docs: atomic manifest write and disposable-cache behavior
docs/engine.md
Clarifies that missing/corrupt manifests reset incremental state for a full rebuild, and that saves use a uniquely named temp file before atomically replacing the manifest.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Possibly related PRs

  • yiipress/engine#58: Implements BuildManifest::save() via FileWriter::writeAtomic(), which is the exact code path that the new temp-file cleanup tests exercise.

Poem

🐇 A temp file was born, then swept away clean,
With LOCK_EX held tight and rename unseen,
No orphaned .tmp shall clutter the ground,
Atomic and tidy — no trace to be found.
The manifest rests where it always should be! ✨

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.69% 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 manifest persistence' accurately summarizes the main change: improving the robustness of build manifest saving and handling through atomic writes and corruption resilience.
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 fix-build-manifest-atomic-save

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 build manifest persistence so builds don’t crash when the manifest is unreadable/corrupt, and so manifest writes are performed via a temp file + rename to reduce the risk of partial writes.

Changes:

  • Treat unreadable/corrupt manifest JSON as a cache miss (reset state instead of throwing).
  • Write manifests to a same-directory temporary file and rename into place; throw on write/replace failures.
  • Add unit tests covering corrupt manifest load behavior and ensuring no .tmp file remains after save.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.

File Description
src/Build/BuildManifest.php Makes manifest loading tolerant to corrupt JSON and implements atomic-ish save via temp file + rename with explicit failure exceptions.
tests/Unit/Build/BuildManifestTest.php Adds regression tests for corrupt-manifest handling and temp-file cleanup behavior on save.

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

Comment on lines 35 to +39
$json = file_get_contents($this->manifestPath);
if ($json === false) {
$this->entries = [];
$this->reset();
return;
}

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

30-33: ⚠️ Potential issue | 🟠 Major | ⚡ Quick win

Reset full state when manifest file is absent.

On Line 30-33, only $this->entries is cleared. If the same BuildManifest instance is reused, stale configFiles/trackedDirectories can leak across loads. Use reset() here for a true cache miss state.

Proposed fix
     public function load(): void
     {
         if (!is_file($this->manifestPath)) {
-            $this->entries = [];
+            $this->reset();
             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 30 - 33, In the BuildManifest
class, when the manifest file is absent (the if (!is_file($this->manifestPath))
branch) replace the partial reset that only sets $this->entries = [] with a full
state reset by calling $this->reset() so that configFiles and trackedDirectories
are also cleared and no stale state leaks across loads.
🤖 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/BuildManifest.php`:
- Around line 78-87: The save() logic currently writes to a deterministic ".tmp"
file and treats any non-false return from file_put_contents as success; change
it to create a unique temp file per call (use
tempnam(dirname($this->manifestPath), basename($this->manifestPath) . '.tmp') or
similar) and write the JSON with exclusive lock, verifying the number of bytes
written equals strlen($json) (or perform a looped fwrite to guarantee full
write); on any write shortfall or error unlink the unique temp file and throw,
and only after a successful full-byte write perform the rename($tmp,
$this->manifestPath) and on rename failure unlink the temp and throw. Ensure you
reference the same $this->manifestPath for temp directory creation and cleanup,
and update the temporaryPath variable usage accordingly.

In `@tests/Unit/Build/BuildManifestTest.php`:
- Around line 135-164: Add PHPUnit tests that cover the remaining branches in
BuildManifest::load() and BuildManifest::save(): add a test (e.g.,
testLoadResetsWhenDecodedJsonIsNotArray) that writes a manifest file whose JSON
decodes to a non-array (for example "null" or a JSON string), calls
BuildManifest::load(), and asserts the manifest was reset (sourceFiles(),
configFiles() empty and isChanged() true for a new source); and add two tests
for save failure branches (e.g., testSaveHandlesWriteFailure and
testSaveHandlesRenameFailure) that simulate file write and rename failures (use
vfsStream or create a non-writable directory to force file_put_contents/rename
to fail), call BuildManifest::save() and assert the code follows the expected
failure behavior (the test should expect the specific exception or error
handling your implementation uses and verify no persistent .tmp file remains or
that the temporary file handling matches the branch behavior).

---

Outside diff comments:
In `@src/Build/BuildManifest.php`:
- Around line 30-33: In the BuildManifest class, when the manifest file is
absent (the if (!is_file($this->manifestPath)) branch) replace the partial reset
that only sets $this->entries = [] with a full state reset by calling
$this->reset() so that configFiles and trackedDirectories are also cleared and
no stale state leaks across loads.
🪄 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: b1cabc00-6f1b-432d-a793-07a9a4f76b90

📥 Commits

Reviewing files that changed from the base of the PR and between 315ad8d and 3e1c60d.

📒 Files selected for processing (2)
  • src/Build/BuildManifest.php
  • tests/Unit/Build/BuildManifestTest.php

Comment thread src/Build/BuildManifest.php Outdated
Comment thread tests/Unit/Build/BuildManifestTest.php Outdated
@samdark samdark force-pushed the fix-build-manifest-atomic-save branch from 3e1c60d to 5022ba1 Compare June 20, 2026 08:29
@samdark samdark merged commit 732d5d3 into master Jun 20, 2026
7 checks passed
@samdark samdark deleted the fix-build-manifest-atomic-save branch June 20, 2026 08:39
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