Skip to content

feat(ci): PR test plugins ship a patch so they stack on shared files (OS-469)#2674

Merged
elibosley merged 3 commits into
masterfrom
feature/os-469-pr-plugin-diff
Jun 19, 2026
Merged

feat(ci): PR test plugins ship a patch so they stack on shared files (OS-469)#2674
elibosley merged 3 commits into
masterfrom
feature/os-469-pr-plugin-diff

Conversation

@elibosley

@elibosley elibosley commented Jun 19, 2026

Copy link
Copy Markdown
Member

Summary

Implements OS-469. The per-PR test plugin used to package changed files as whole-file copies, overwrite them on install, and abort if another webgui-pr-* plugin already managed any of the same files — so two PRs touching one file (e.g. #2672 + #2673, both editing CreateDocker.php) couldn't be tested together.

Now it ships a unified diff and patch-applies it, so non-overlapping edits to the same file stack.

How it works

  • Build (pr-plugin-build.yml): for changed text files, stage base + head versions in system layout and emit pr.patch (diff -ruN, paths apply with patch -p1 at /). Changed binary files are copied whole into binary/ with a binary_files.txt list. Both go in the same tarball.
  • Plugin (generate-pr-plugin.sh): on install, patch -p1 --dry-run --forward first — abort with a clear message on a real reject (overlapping change), otherwise apply and save applied.patch. Binaries are whole-file replaced with a per-binary conflict guard. On remove (and before update), reverse the patch (patch -R) and restore binary backups.
  • Upload/R2 plumbing unchanged — still a single tarball with the same URL/SHA wiring.

Validation (local simulation)

  • Two patches editing the same file at different lines → both apply, both changes coexist.
  • Reversing one → only that change is removed, the other stays.
  • Overlapping edits → dry-run fails → install aborts cleanly.
  • Generated .plg is well-formed XML (all placeholders substituted); bash -n + YAML parse clean.

Notes / still to verify

  • Real validation needs CI + an Unraid box installing two stacked PR plugins. Conveniently, this PR touches .github/**, so it will itself build a PR test plugin with the new code — a live smoke test of the generator.
  • Binary deletions aren't handled (rare); text add/modify/delete are.
  • patch is present in the Unraid base; uses --forward/--batch for non-interactive apply with fuzz.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • Chores
    • Improved plugin install/update and uninstall to use a deterministic, patch-based manifest workflow for safer reversibility.
    • Enhanced plugin package generation to build a patch-oriented payload, distinguishing text changes from full binary replacements.
    • Updated the uninstall confirmation flow to reflect the new removal behavior.

The per-PR test plugin packaged changed files as whole-file copies and
overwrote them on install, aborting if another webgui-pr-* plugin already
managed any of the same files. Two PRs touching one file could not be
tested together.

Now the build emits a unified diff (pr.patch, system-relative paths) for
text files plus whole copies of any binary files, and the plugin applies
the patch with `patch -p1` (dry-run first; abort only on a real reject)
and reverses it on removal. Non-overlapping edits to the same file from
different PR plugins now stack; only truly overlapping hunks conflict.
Binaries keep the whole-file replace with a per-binary conflict guard.

Upload/R2 plumbing is unchanged (still one tarball).

Closes OS-469

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Review Change Stack

Walkthrough

The CI workflow's packaging step is rewritten to classify PR-changed files as text or binary, produce a unified pr.patch from old/new snapshots, and bundle binaries separately with a manifest. The generated plugin script is updated to apply/revert this patch and manage binary files via manifests throughout install and removal.

Changes

Patch-based PR plugin build and lifecycle

Layer / File(s) Summary
CI payload builder: patch + binary classification
.github/workflows/pr-plugin-build.yml
Introduces BASE_SHA, classifies files as text vs binary via git diff --numstat, snapshots text into old//new/ trees, generates build/pr.patch from the diff, copies binaries into build/binary/ with build/binary_files.txt, and archives the full build/ directory into the tarball instead of build/usr/local/**.
Plugin install: documentation and manifest reset
.github/scripts/generate-pr-plugin.sh
Header docs and CHANGES section updated to describe the new patch+binary payload structure and stacking strategy. Pre-install reset reverts any previously applied patch via patch -R and restores backed-up binaries from installed_files.txt before clearing manifest/payload directories.
Plugin install: core patch and binary installation
.github/scripts/generate-pr-plugin.sh
Main install logic extracts the payload, validates pr.patch with --dry-run before applying, persists the applied patch and stored originals for deterministic rollback, and installs binaries via whole-file replacement with backup/manifest tracking and cross-plugin conflict detection against other webgui-pr-* plugins.
Plugin removal and removal UI
.github/scripts/generate-pr-plugin.sh
Removal script reverts the stored patch deterministically, removes files added by the PR or restores backed-up binaries based on the manifest, deletes the emhttp banner directory and plugin config directory. Uninstall confirmation text updated; banner JS no longer invokes Unraid's standard plugin remove action.

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

🐇 A patch is a promise, a diff tells the tale,
Old files backed up safe, new hunks on the trail.
Dry-run first, little rabbit, before you apply—
If conflicts arise, we just wave them goodbye!
Binaries tracked, manifests kept neat,
Roll back with a flourish, the cleanup's complete! 🌿

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: implementing a patch-based stacking mechanism for PR test plugins to handle shared files, with OS-469 as the issue reference.
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.

✏️ 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 feature/os-469-pr-plugin-diff

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

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
.github/scripts/generate-pr-plugin.sh (1)

177-177: 💤 Low value

Consider adding --batch flag for consistency with revert operations.

The patch revert (line 96 and line 284) uses --batch to prevent interactive prompts, but the forward-apply here doesn't. While --forward should prevent most prompts, adding --batch would ensure the install never hangs waiting for input if unexpected conditions arise.

-    patch -p1 -d / --forward < "$PAYLOAD/pr.patch"
+    patch -p1 -d / --forward --batch < "$PAYLOAD/pr.patch"
🤖 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 @.github/scripts/generate-pr-plugin.sh at line 177, The patch command used to
apply the forward patch is missing the `--batch` flag that is already used in
the patch revert operations elsewhere in the script. Add the `--batch` flag to
the patch command (the one with `-p1 -d / --forward` flags) to ensure
consistency and prevent the script from hanging on unexpected interactive
prompts, matching the pattern already established in the revert operations.
🤖 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 @.github/scripts/generate-pr-plugin.sh:
- Line 177: The patch command used to apply the forward patch is missing the
`--batch` flag that is already used in the patch revert operations elsewhere in
the script. Add the `--batch` flag to the patch command (the one with `-p1 -d /
--forward` flags) to ensure consistency and prevent the script from hanging on
unexpected interactive prompts, matching the pattern already established in the
revert operations.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: e2be2f1e-f0c9-4009-a6c2-aad5835e35bd

📥 Commits

Reviewing files that changed from the base of the PR and between cc6a800 and 1b46994.

📒 Files selected for processing (2)
  • .github/scripts/generate-pr-plugin.sh
  • .github/workflows/pr-plugin-build.yml

…se-patch

Reverse-applying a plugin's patch on removal can fail when another stacked
PR plugin edited nearby lines (disturbed reverse context), leaving a file
half-reverted. Instead, ship each text file's original (base) version in the
payload and, on removal/update, restore those originals then re-apply the
other still-installed PR plugins' patches. This is deterministic and
independent of reverse-patch context, so removing one of several plugins
that share a file cleanly drops only that plugin's changes.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@elibosley

Copy link
Copy Markdown
Member Author

Update — removal is now deterministic (rebuild from originals) (commit c6b330eb6)

Addressed a real weakness in the first cut: removal used patch -R (reverse-apply), which can fail when another stacked PR plugin edited nearby lines (the reverse context is disturbed), leaving a file half-reverted.

Now the payload also ships each text file's original (base) version (orig/). On removal (and before an update), the plugin:

  1. restores the files it touched to their shipped originals, then
  2. re-applies every other still-installed PR plugin's patch (patch --forward),

so it cleanly drops only its own changes regardless of overlap/order — no dependence on reverse-patch context.

Verified by simulation, including the hard case: two plugins editing adjacent lines of the same file. Old reverse-apply fails; the new rebuild removes one cleanly, keeps the other intact, and returns the file to stock when the last is removed.

(Files live in the RAM rootfs, so a reboot is always the ultimate fallback — but removal no longer needs it.)

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
.github/scripts/generate-pr-plugin.sh (1)

93-116: 💤 Low value

Restore-then-reapply strategy is sound.

The deterministic rollback approach (restore shipped originals, then re-apply other plugins' patches) correctly addresses the reverse-patch context problem described in PR objectives.

One observation: line 110 silently suppresses patch errors during re-application. This is reasonable since all patches must have passed dry-run during installation, but consider logging a warning (not to stderr, which is also suppressed) so operators can diagnose rare edge cases:

     for other in /boot/config/plugins/webgui-pr-*; do
         [ -d "$other" ] || continue
         [ "$other" == "$PLUGIN_DIR" ] && continue
-        [ -f "$other/applied.patch" ] && patch -p1 -d / --forward < "$other/applied.patch" >/dev/null 2>&1 || true
+        if [ -f "$other/applied.patch" ]; then
+            if ! patch -p1 -d / --forward < "$other/applied.patch" >/dev/null 2>&1; then
+                echo "⚠️  Warning: failed to re-apply patch from $(basename "$other")"
+            fi
+        fi
     done
🤖 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 @.github/scripts/generate-pr-plugin.sh around lines 93 - 116, In the loop
that re-applies patches from other installed PR plugins (the `for other in
/boot/config/plugins/webgui-pr-*` loop), the current patch command silently
suppresses all errors using `>/dev/null 2>&1 || true`. To aid diagnostic
capabilities, modify the patch re-application logic to capture when the patch
fails to apply and log a warning message to a diagnostic log file (not stderr,
which is also redirected to /dev/null), so operators can track which patches
failed to re-apply in rare edge cases, while still maintaining the non-blocking
behavior of the `|| true` continuation.
🤖 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 @.github/scripts/generate-pr-plugin.sh:
- Around line 93-116: In the loop that re-applies patches from other installed
PR plugins (the `for other in /boot/config/plugins/webgui-pr-*` loop), the
current patch command silently suppresses all errors using `>/dev/null 2>&1 ||
true`. To aid diagnostic capabilities, modify the patch re-application logic to
capture when the patch fails to apply and log a warning message to a diagnostic
log file (not stderr, which is also redirected to /dev/null), so operators can
track which patches failed to re-apply in rare edge cases, while still
maintaining the non-blocking behavior of the `|| true` continuation.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

Run ID: a06908e0-43dc-447e-beff-64e29cbe4aa4

📥 Commits

Reviewing files that changed from the base of the PR and between 1b46994 and c6b330e.

📒 Files selected for processing (2)
  • .github/scripts/generate-pr-plugin.sh
  • .github/workflows/pr-plugin-build.yml
🚧 Files skipped from review as they are similar to previous changes (1)
  • .github/workflows/pr-plugin-build.yml

- Add --batch to all forward patch applies (dry-run, install, re-apply loops)
  for consistency with the reverse operations and to avoid any interactive
  hang on unexpected input.
- Re-apply loop now surfaces a warning (stdout + logger) when another PR
  plugin's patch fails to re-apply during rollback, instead of silently
  suppressing it; behavior stays non-blocking.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@elibosley

Copy link
Copy Markdown
Member Author

Thanks @coderabbitai — both nitpicks addressed in 056c371b0:

  1. --batch on forward applies — added to the dry-run, the main install apply, and both re-apply loops (the reverse ops already had it), so no patch --forward can hang on input.
  2. Silent re-apply failures — the re-apply loop now emits a warning to stdout and logger -t webgui-pr when another plugin's patch fails to re-apply during rollback, while keeping the non-blocking behavior.

@elibosley elibosley merged commit 644ab82 into master Jun 19, 2026
4 checks passed
@elibosley elibosley deleted the feature/os-469-pr-plugin-diff branch June 19, 2026 14:33
@coderabbitai

coderabbitai Bot commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Tip

For best results, initiate chat on the files or code changes.

@elibosley: Thanks for the follow-up — both fixes look solid. Adding --batch consistently across all patch --forward calls (dry-run, main apply, and the re-apply loops) closes the hang risk cleanly, and surfacing re-apply failures via both stdout and logger -t webgui-pr gives good visibility without blocking the rollback. Nice work on the whole implementation!

🐇✨

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