Conversation
Split the monolithic directory recreation logic into focused, testable components. Parsing of PVE/PBS config files was moved to directory_recreation_config.go (load/parse helpers and normalization for datastore.cfg). PBS-specific preflight/initialization logic, subdir creation and lock handling were extracted into directory_recreation_pbs*.go files, and PVE-specific scaffolding moved to a dedicated pve file. Ownership and permission handling was isolated in directory_recreation_ownership.go, while path/device utilities and ZFS/root-fs safety checks were factored into directory_recreation_paths.go. The main RecreateStorageDirectories/RecreateDatastoreDirectories functions were simplified to use the new loaders/creators; tests updated accordingly.
Pin CI actions to specific commit SHAs for reproducible workflows and security: codecov action pinned to a v6 commit, anchore/sbom-action (syft) and goreleaser action pinned to specific commits. Also normalize list formatting/whitespace in Codacy MCP instructions for clarity; no functional changes to behavior.
Move and refactor PBS mount guard logic into a dedicated apply implementation, splitting responsibilities and adding helper functions for validation, directory setup and bind-readonly guard mounting. Simplify and harden proc path unescaping with small utility helpers. Extract and reorganize network apply UI flow into request/flow types and separate prompt/rollback files, update tests to use test tool helpers and a fake command runner, and add multiple restore/workflow UI stubs. Also add package comments and small housekeeping changes (embed.go, imports). This improves separation of concerns, testability and readability of mount guard and network apply codepaths.
Validate nil backup candidates before starting raw artifact staging trace logging. This prevents copyRawArtifactsToWorkdirWithLogger from dereferencing a nil candidate before returning its intended error, and adds a regression test covering the nil candidate case.
Remove stale internal helper functions left behind by earlier refactors. This cleans up unused environment detection, container detail formatting, symlink overlay, and restore abort helper code. Also removes the now-unused input import and verifies the cleanup with unused lint and the full Go test suite.
Remove ineffectual assignments reported by golangci-lint. This simplifies config upgrade insertion logic, cleans up mount guard request normalization, and keeps executable permission checks while discarding unused refreshed file info. Verified with ineffassign lint and the full Go test suite.
Apply the Proxmox Backup zombie-process filter before suspicious process matching. Previously the filter ran at the end of the process scan loop, so its continue had no practical effect. This moves the check ahead of suspicious signature detection and adds regression coverage for skipped zombie processes and warned non-zombie matches.
Clean up staticcheck findings across runtime and tests, including deprecated tar usage, error string formatting, fmt.Fprintf conversions, De Morgan simplifications, and embedded selector cleanup. Strengthen fragile tests around nil contexts, NIC repair results, identity detection, and security checks. Make PVE cluster detection tests deterministic by avoiding dependence on the host pvecm command.
Dependency Review✅ No vulnerabilities or license issues or OpenSSF Scorecard issues found.OpenSSF Scorecard
Scanned Files
|
Not up to standards ⛔🔴 Issues
|
| Category | Results |
|---|---|
| Security | 3 critical 6 high |
| Complexity | 4 medium |
🟢 Metrics 1018 complexity · 87 duplication
Metric Results Complexity 1018 Duplication 87
AI Reviewer: first review requested successfully. AI can make mistakes. Always validate suggestions.
TIP This summary will be updated as you push new changes.
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughCentralizes CloseIntoErr and propagates close errors; adds extensive orchestrator restore UI and PBS/PVE directory-recreation/mount-guard code; tightens collectors, notify/HTTP, TUI wrappers, many tests, and pins CI actions. ChangesMain Cohort
Estimated code review effort 🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
✨ Finishing Touches🧪 Generate unit tests (beta)
|
There was a problem hiding this comment.
Pull Request Overview
This PR performs an extensive refactor of core ProxSave workflows and modularizes directory recreation logic; however, it is currently not up to standards. Critical security issues were identified regarding overly permissive file permissions in /tmp and potential path traversal in PBS datastore lock file creation. Additionally, the removal of tar.TypeRegA checks in archive parsers may break compatibility with legacy backups.
Architecturally, the PR introduces extreme complexity in several files (e.g., network_apply_workflow_ui_rollback.go and decrypt_test.go), and the Export function in Prometheus metrics has become a maintenance risk. Due to the broad scope of this PR and the lack of a coverage report, there is a high risk of regression in the refactored TUI and mount guard flows.
About this PR
- The absence of a coverage report for this PR prevents verification of whether new logic paths, particularly in the refactored TUI and mount guard flows, are adequately tested.
- The PR scope is exceptionally broad. Combining major architectural refactoring with bug fixes and linting increases the risk of regression. Future PRs should be decomposed into smaller, atomic units.
Test suggestions
- Verify directory recreation logic for PVE storage structures
- Verify directory recreation logic for PBS datastores with ZFS safety checks
- Verify protection against nil candidate panic in raw artifact staging
- Verify that Proxmox Backup zombie processes are correctly skipped in security scans
- Ensure closeIntoErr correctly captures and wraps errors from io.Closer implementations
- Missing automated XSS validation for dynamic variables in notification templates
- Missing unit tests for tar.TypeRegA compatibility in archive parsers
Prompt proposal for missing tests
Consider implementing these tests if applicable:
1. Missing automated XSS validation for dynamic variables in notification templates
2. Missing unit tests for tar.TypeRegA compatibility in archive parsers
TIP Improve review quality by adding custom instructions
TIP How was this review? Give us feedback
| } | ||
|
|
||
| func createPBSDatastoreLockFile(lockPath string) error { | ||
| file, err := os.OpenFile(lockPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0o640) |
There was a problem hiding this comment.
🔴 HIGH RISK
The lock file is created with 0o640 permissions; however, 0o600 is safer for files containing internal system metadata like PIDs. Additionally, the construction of lockPath from datastorePath (sourced from configuration) should be validated to ensure it remains within the intended datastore root, preventing potential path traversal attacks.
| return nil | ||
| } | ||
|
|
||
| if err := os.MkdirAll("/tmp/proxsave", 0o755); err != nil { |
There was a problem hiding this comment.
🔴 HIGH RISK
Setting /tmp/proxsave permissions to 0o755 is overly permissive. This allows any local user to list the directory contents, which might include sensitive state or diagnostic data. Using 0o700 is recommended to restrict access only to the owner.
| if err := os.MkdirAll("/tmp/proxsave", 0o755); err != nil { | |
| if err := os.MkdirAll("/tmp/proxsave", 0o700); err != nil { |
| return nil, fmt.Errorf("restore metadata entry is missing a tar header") | ||
| } | ||
| if header.Typeflag != tar.TypeReg && header.Typeflag != tar.TypeRegA { | ||
| if header.Typeflag != tar.TypeReg { |
There was a problem hiding this comment.
🟡 MEDIUM RISK
The removal of the tar.TypeRegA check reduces robustness. Many older or non-standard tar implementations use TypeRegA for regular files. Consider restoring the check for both types.
| continue | ||
| } | ||
| if header.Typeflag != tar.TypeReg && header.Typeflag != tar.TypeRegA { | ||
| if header.Typeflag != tar.TypeReg { |
There was a problem hiding this comment.
🟡 MEDIUM RISK
The removal of the tar.TypeRegA check reduces robustness. Many older or non-standard tar implementations use TypeRegA for regular files. Consider restoring the check for both types.
| html.WriteString(fmt.Sprintf(" <p>%s - %s</p>\n", data.Hostname, data.BackupDate.Format("2006-01-02 15:04:05"))) | ||
| fmt.Fprintf(&html, " <div class=\"header\" style=\"background-color: %s;\">\n", statusColor) | ||
| fmt.Fprintf(&html, " <h1>%s Backup Report - %s</h1>\n", proxmoxType, statusText) | ||
| fmt.Fprintf(&html, " <p>%s - %s</p>\n", data.Hostname, data.BackupDate.Format("2006-01-02 15:04:05")) |
There was a problem hiding this comment.
🟡 MEDIUM RISK
Dynamic variables are written directly to the HTML body without escaping, which poses a formatting and security risk. All variable injections in HTML should be wrapped in escapeHTML().
|
|
||
| // Export writes the given metrics snapshot to proxmox_backup.prom in textfileDir. | ||
| func (pe *PrometheusExporter) Export(m *BackupMetrics) error { | ||
| func (pe *PrometheusExporter) Export(m *BackupMetrics) (err error) { |
There was a problem hiding this comment.
🟡 MEDIUM RISK
Suggestion: The Export function is currently too complex (CCN 33) and long (163 lines). Refactoring this into smaller helper methods for each logical metric group (e.g., writeCoreMetrics, writeStorageMetrics, writeStaticInfo) would improve readability and testability.
| return target, nil | ||
| } | ||
|
|
||
| func ensureGuardTargetUnmounted(target string) error { |
There was a problem hiding this comment.
🟡 MEDIUM RISK
Suggestion: The naming of 'ensureGuardTargetUnmounted' is misleading as it returns nil when the target is mounted. It should either return an error if mounted or be renamed to reflect its actual behavior as a mount status check.
| "os" | ||
| ) | ||
|
|
||
| func closeIntoErr(errp *error, closer io.Closer, operation string) { |
There was a problem hiding this comment.
⚪ LOW RISK
Suggestion: The closeIntoErr helper is duplicated across multiple packages. Consider centralizing this logic in a shared internal package (e.g., internal/utils) to ensure consistent error handling behavior and reduce maintenance overhead.
| return err | ||
| } | ||
| if len(blocks) == 0 { | ||
| if err := ensureGuardTargetUnmounted(target); err != nil { |
There was a problem hiding this comment.
⚪ LOW RISK
Nitpick: Redundant mount checks: ensureGuardTargetUnmounted and isAlreadyMounted both invoke isMounted. This logic should be consolidated to avoid multiple calls to the same underlying status check.
There was a problem hiding this comment.
Actionable comments posted: 12
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
cmd/proxsave/runtime_helpers.go (1)
87-101:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winRoot directory is skipped during base-dir discovery.
At Line 87, the loop exits before evaluating
dir == "/"(or platform separator root), so"/env"and"/script"are never checked. That’s a behavior change from common break-at-root traversal and can miss valid root-based installs.Suggested fix
- for dir != "" && dir != "." && dir != string(filepath.Separator) { + for dir != "" && dir != "." { if info, err := os.Stat(filepath.Join(dir, "env")); err == nil && info.IsDir() { baseDir = dir break } if info, err := os.Stat(filepath.Join(dir, "script")); err == nil && info.IsDir() { baseDir = dir break } parent := filepath.Dir(dir) if parent == dir { break } dir = parent }🤖 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 `@cmd/proxsave/runtime_helpers.go` around lines 87 - 101, The loop that searches for a base directory (using variable dir, baseDir and checks via os.Stat(filepath.Join(dir, "env")) and os.Stat(filepath.Join(dir, "script"))) stops before checking the filesystem root; change the traversal to perform the env/script checks for the current dir first and only break after computing parent := filepath.Dir(dir) and comparing parent == dir, so the root directory is evaluated too (i.e., use an unconditional loop over dir with the stat checks before the parent == dir break). Ensure you still set baseDir = dir and break when a match is found.internal/tui/components/panel.go (1)
48-70: 🛠️ Refactor suggestion | 🟠 Major | ⚡ Quick winComplete the receiver method refactor for consistency.
InfoPanel(line 44) was updated to usepanel.SetBackgroundColor(...)instead ofpanel.Box.SetBackgroundColor(...), aligning with the PR's goal of using receiver methods. However,SuccessPanel,ErrorPanel, andWarningPanelstill directly access the embeddedBoxfield viapanel.Box.SetBorderColor(...)andpanel.Box.SetTitleColor(...).Since
Panelembeds*tview.Box, these methods are available directly on thePanelreceiver. Update these functions for consistency.♻️ Proposed refactor for consistency
// SuccessPanel creates a success-styled panel func SuccessPanel(title, message string) *Panel { panel := NewPanel().SetTitle(title) - panel.Box.SetBorderColor(tui.SuccessGreen). - SetTitleColor(tui.SuccessGreen) + panel.SetBorderColor(tui.SuccessGreen). + SetTitleColor(tui.SuccessGreen) return panel } // ErrorPanel creates an error-styled panel func ErrorPanel(title, message string) *Panel { panel := NewPanel().SetTitle(title) - panel.Box.SetBorderColor(tui.ErrorRed). - SetTitleColor(tui.ErrorRed) + panel.SetBorderColor(tui.ErrorRed). + SetTitleColor(tui.ErrorRed) return panel } // WarningPanel creates a warning-styled panel func WarningPanel(title, message string) *Panel { panel := NewPanel().SetTitle(title) - panel.Box.SetBorderColor(tui.WarningYellow). - SetTitleColor(tui.WarningYellow) + panel.SetBorderColor(tui.WarningYellow). + SetTitleColor(tui.WarningYellow) return panel }🤖 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 `@internal/tui/components/panel.go` around lines 48 - 70, SuccessPanel, ErrorPanel, and WarningPanel still call methods on the embedded Box via panel.Box.SetBorderColor/... and panel.Box.SetTitleColor/...; update them to call the receiver methods on Panel directly (e.g., panel.SetBorderColor(...), panel.SetTitleColor(...)) to match the refactor done in InfoPanel and avoid directly referencing panel.Box in SuccessPanel, ErrorPanel, and WarningPanel.internal/orchestrator/restore_decision.go (1)
161-167:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winAccept both
tar.TypeRegandtar.TypeRegAfor regular file validation.The current validation rejects
tar.TypeRegA, which is a valid (though deprecated) alternate representation of regular files defined in Go'sarchive/tarpackage. Go's tar implementation explicitly supports this for backwards compatibility with legacy tar archives. RejectingTypeRegAentries will prevent restoration from older tar archives that use this deprecated format.Update the validation to accept both:
Suggested fix
if header.Typeflag != tar.TypeReg && header.Typeflag != tar.TypeRegA { return nil, fmt.Errorf("archive entry %s is not a regular file", header.Name) }🤖 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 `@internal/orchestrator/restore_decision.go` around lines 161 - 167, The file's readRestoreDecisionMetadata function currently rejects tar entries with header.Typeflag == tar.TypeRegA even though TypeRegA is a valid (deprecated) alternate regular-file flag; update the validation in readRestoreDecisionMetadata to accept both tar.TypeReg and tar.TypeRegA by changing the conditional that checks header.Typeflag so it only errors when header.Typeflag is neither tar.TypeReg nor tar.TypeRegA, keeping the same error message and behavior otherwise.internal/backup/optimizations.go (1)
246-304:⚠️ Potential issue | 🟡 Minor | ⚡ Quick winEmpty trailing chunk file when source size is an exact multiple of
chunkSize.In
splitFile, afterwriteChunkreturnsdone=false, the loop unconditionally creates the next chunk file inwriteChunk(viaos.OpenFile(chunkPath, O_CREATE|...)) before checking for EOF. If the source has exactlyN * chunkSizebytes, the previous call returnedfalse, niloncewritten >= limit, and the following call creates…(N+1).chunk, reads 0/io.EOF, then returnstrue, nil, leaving a zero-byte chunk on disk.A simple fix is to peek/avoid creating the next file when no bytes were read:
♻️ Proposed fix
func writeChunk(src *os.File, chunkPath string, buf []byte, limit int64) (done bool, err error) { - out, err := os.OpenFile(chunkPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, defaultChunkFilePerm) - if err != nil { - return false, err - } - defer closeIntoErr(&err, out, "close chunk file") - - var written int64 + var ( + out *os.File + written int64 + ) + openOut := func() error { + if out != nil { + return nil + } + f, oerr := os.OpenFile(chunkPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, defaultChunkFilePerm) + if oerr != nil { + return oerr + } + out = f + return nil + } + defer func() { + if out != nil { + closeIntoErr(&err, out, "close chunk file") + } + }() for written < limit { remaining := limit - written if remaining < int64(len(buf)) { buf = buf[:remaining] } n, err := src.Read(buf) if n > 0 { + if oerr := openOut(); oerr != nil { + return false, oerr + } if _, wErr := out.Write(buf[:n]); wErr != nil { return false, wErr } written += int64(n) } if err != nil { if err == io.EOF { - return true, nil + return true, nil } return false, err } - if written >= limit { - return false, nil - } } return false, nil }🤖 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 `@internal/backup/optimizations.go` around lines 246 - 304, splitFile/writeChunk create an empty trailing chunk when source size is an exact multiple of chunkSize; modify writeChunk (or its call pattern) so it does not create the output file before confirming there are bytes to write: perform an initial non-destructive check/peek from src (e.g., Read into the provided buf or use src.ReadAt/Seek to test for EOF) and only open/create chunkPath (os.OpenFile) once at least one byte has been read; update writeChunk signature/behavior accordingly so splitFile's loop stops without producing a zero-length chunk when done==true after an initial EOF check.
🧹 Nitpick comments (13)
embed.go (1)
37-43: ⚡ Quick winDocument that returned Data slices should not be modified.
The defensive copy creates a new slice of
DocAssetstructs, but the underlying byte arrays in theDatafields are shared with the cachedinstallableDocs. While the embedded data is likely stored in read-only memory (making modifications panic), explicitly documenting this prevents future confusion.📝 Suggested documentation enhancement
// InstallableDocs returns the list of documentation files embedded in the -// binary that should be written to the installation root. +// binary that should be written to the installation root. The returned +// DocAsset structs are copies, but their Data fields share the underlying +// byte arrays with the cached assets and must not be modified. func InstallableDocs() []DocAsset {🤖 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 `@embed.go` around lines 37 - 43, The InstallableDocs function returns a shallow copy of the installableDocs slice but the DocAsset.Data byte slices are shared; update the function comment for InstallableDocs to explicitly state that callers must treat the returned DocAsset.Data as read-only and must not modify the byte slices (or allocate copies themselves if mutation is required), referencing DocAsset and the shared installableDocs backing data so future maintainers understand the shared underlying memory semantics.internal/storage/secondary.go (1)
295-298: ⚡ Quick winPotential double-close on error path.
If
tempFile.Close()fails at line 296, the function returns immediately at line 297, but the deferred cleanup at lines 245-249 will attempt to closetempFileagain (since it's still non-nil). While this may only result in a harmless "already closed" error, it's cleaner to settempFile = nilbefore checking the close error to prevent the retry.♻️ Suggested fix to prevent double-close
- if err := tempFile.Close(); err != nil { - return fmt.Errorf("failed to close temporary file %s: %w", tempName, err) - } - tempFile = nil + closeErr := tempFile.Close() + tempFile = nil + if closeErr != nil { + return fmt.Errorf("failed to close temporary file %s: %w", tempName, closeErr) + }🤖 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 `@internal/storage/secondary.go` around lines 295 - 298, The tempFile close error path can cause a double-close because the deferred cleanup still sees tempFile non-nil; in the block that calls tempFile.Close() (the local tempFile variable in internal/storage/secondary.go), set tempFile = nil immediately after calling Close() (i.e., before checking/returning on the error) so the deferred cleanup won't try to close it again; modify the Close/error handling around tempFile.Close() accordingly (reference tempFile and the deferred cleanup block that closes it).internal/tui/components/form.go (1)
56-59: ⚡ Quick winInconsistent wrapper usage in AddPasswordField.
Line 58 still calls
f.Form.AddPasswordField(...)directly, while the surrounding methods (lines 51, 82, 109, 121-122) were updated to use the wrapper receiver (f.AddInputField(...),f.AddButton(...), etc.). For consistency with the broader refactor pattern, consider usingf.AddPasswordField(...)instead.♻️ Suggested consistency fix
func (f *Form) AddPasswordField(label string, fieldWidth int, validators ...ValidatorFunc) *Form { f.validators[label] = validators - f.Form.AddPasswordField(label, "", fieldWidth, '*', nil) + f.AddPasswordField(label, "", fieldWidth, '*', nil) return f }🤖 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 `@internal/tui/components/form.go` around lines 56 - 59, The AddPasswordField wrapper currently calls the embedded form method directly (f.Form.AddPasswordField) which is inconsistent with other wrappers; change the call to use the wrapper receiver (call f.AddPasswordField(...) instead of f.Form.AddPasswordField(...)) so it follows the same refactor pattern as AddInputField/AddButton; ensure you pass the same arguments (label, "", fieldWidth, '*', nil) when updating the call and verify there is no accidental recursion by ensuring the wrapper delegates to the underlying form implementation if a different internal helper exists.internal/orchestrator/directory_recreation_pbs_lock.go (1)
60-76: ⚡ Quick winDon't report
changed=truewhen the directory removal failed.When
os.Remove(lockPath)returns an error, this returns(true, err). Callers currently discardchangedon error, so this isn't observable today, but it's a leaky contract: a future caller usingchangedto roll back / log progress will be misled. Same is true for the rename branch — only returntrueafter the operation actually succeeded.♻️ Proposed cleanup
if len(entries) == 0 { logger.Warning("PBS datastore lock: %s is a directory (invalid); removing and recreating as file", lockPath) - return true, os.Remove(lockPath) + if err := os.Remove(lockPath); err != nil { + return false, fmt.Errorf("remove invalid empty lock dir %s: %w", lockPath, err) + } + return true, nil }🤖 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 `@internal/orchestrator/directory_recreation_pbs_lock.go` around lines 60 - 76, The function removeOrRenamePBSDatastoreLockDirectory currently returns (true, err) when os.Remove or os.Rename fails; change it so the function only returns true when the filesystem operation actually succeeded: call os.Remove(lockPath), check its error and if non-nil return (false, err) instead of (true, err); similarly, after attempting os.Rename(lockPath, backupPath) only return (true, nil) when err == nil and return (false, err) on failure; keep the existing log messages and preserve the backupPath behavior in the rename branch.internal/metrics/prometheus.go (1)
81-242: ⚡ Quick winReduce the repetitive write/wrap boilerplate.
The 11
writeMetric+ 8fmt.Fprintfblocks all share the same error-wrapping pattern (write metrics file %s: %w,tmpPath). A small closure capturingfandtmpPathwould eliminate ~70 lines of near-duplicate code and make it harder to introduce inconsistency in future metrics.♻️ Sketch of a possible refactor
// Helper closures capturing f, tmpPath, and a sticky error var writeErr error wrap := func(err error) error { if writeErr != nil { return writeErr } if err != nil { writeErr = fmt.Errorf("write metrics file %s: %w", tmpPath, err) } return writeErr } writeMetric := func(name, mtype, help, value string) error { if writeErr != nil { return writeErr } if _, err := fmt.Fprintf(f, "# HELP %s %s\n", name, help); err != nil { return wrap(err) } if _, err := fmt.Fprintf(f, "# TYPE %s %s\n", name, mtype); err != nil { return wrap(err) } _, err := fmt.Fprintln(f, value) return wrap(err) } writef := func(format string, a ...any) error { if writeErr != nil { return writeErr } _, err := fmt.Fprintf(f, format, a...) return wrap(err) } // Usage becomes: if err := writeMetric("proxmox_backup_start_time_seconds", "gauge", "Unix timestamp of backup start", fmt.Sprintf("proxmox_backup_start_time_seconds %.0f", startTs)); err != nil { return err } // ... if err := writef("proxmox_backup_backups_total{location=%q} %d\n", "local", m.LocalBackups); err != nil { return err }🤖 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 `@internal/metrics/prometheus.go` around lines 81 - 242, Replace the repetitive metric-writing error-wrapping with a small helper closure that captures f and tmpPath and maintains a sticky error: add a wrap(err error) error that sets and returns a single writeErr, refactor the existing writeMetric(name,mtype,help,value) to check writeErr and call wrap on any fmt.Fprintf/Fprintln failures, and add a writef(format string, a ...any) helper that does the same for the ad-hoc fmt.Fprintf calls used for per-location and info metrics; then replace all direct fmt.Fprintf/Fprintln blocks and the current writeMetric usages to return writeErr directly instead of rewrapping with fmt.Errorf("write metrics file %s: %w", tmpPath, err). Ensure symbols modified are writeMetric, wrap (or writeErr), writef and use tmpPath and f captured by the closures.internal/backup/archiver.go (1)
25-32: ⚖️ Poor tradeoffConsider centralizing
closeIntoErrin a shared internal package.This helper is defined here and (per the PR-wide refactor) also in
internal/orchestrator/close_error.go, with identical semantics. Two copies will drift. Moving it into a small shared package (e.g.internal/ioutilorinternal/closeerr) would eliminate the duplication and keep the contract uniform acrossbackup,orchestrator, and any future callers.Not blocking — defer if it requires touching too many import sites for this PR.
🤖 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 `@internal/backup/archiver.go` around lines 25 - 32, The helper closeIntoErr is duplicated; extract it into a small shared internal package (e.g. internal/closeerr or internal/ioutil) and replace the copies in both backup and orchestrator with calls to the shared implementation; create the new package with the same semantics (either keep the function unexported and call via package-scoped name if colocated, or export as CloseIntoErr and update callers), update imports in files that used the old duplicate (e.g. functions currently calling closeIntoErr in backup and the file in orchestrator), remove the redundant definitions, and run tests to ensure behavior is unchanged.internal/orchestrator/directory_recreation_pbs_config.go (1)
35-42: ⚡ Quick winPredictable
tmpPathnext to the target — consideros.CreateTempin the same dir.
fmt.Sprintf("%s.proxsave.tmp", path)is a fixed name. Concurrent normalizations or a stale leftover from a crashed run will clash, and onos.Renamefailure (line 39) the retained_ = os.Remove(tmpPath)only handles the new-temp case. Usingos.CreateTemp(filepath.Dir(path), "datastore.cfg.proxsave-*")to mint a unique file in the same directory keepsos.Renameatomic and avoids the clash.🤖 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 `@internal/orchestrator/directory_recreation_pbs_config.go` around lines 35 - 42, Replace the predictable tmpPath logic with a unique temp file created in the same directory using os.CreateTemp: call os.CreateTemp(filepath.Dir(path), "datastore.cfg.proxsave-*") to obtain a unique file handle/name, write the normalized contents to that temp file (ensure file mode is set via os.Chmod if needed), close the temp file, then os.Rename(tempName, path) to atomically replace the target; on any error remove the temp file (os.Remove) and return the wrapped error. Locate and update the block around tmpPath, os.WriteFile and os.Rename in directory_recreation_pbs_config.go to implement this change.internal/orchestrator/decrypt.go (1)
411-425: 💤 Low valueMinor: temp file is leaked if
tmpFile.Close()fails.
os.CreateTemphas already created the file on disk beforeClose()is called. IfClose()returns an error you bail out without removingtmpPath, and nocleanupis returned to the caller, so the temp file lingers. Close on a fresh empty file is unlikely to fail in practice, but a small belt-and-suspenders cleanup keeps/tmp/proxsavetidy.♻️ Suggested cleanup
tmpPath = tmpFile.Name() if err := tmpFile.Close(); err != nil { + _ = os.Remove(tmpPath) return "", nil, fmt.Errorf("close temp file: %w", err) }🤖 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 `@internal/orchestrator/decrypt.go` around lines 411 - 425, The tmp file created by os.CreateTemp (tmpFile/tmpPath) can leak if tmpFile.Close() fails because you return before installing or running cleanup; ensure the temp is removed on any error after creation: define cleanup (closure referencing tmpPath) immediately after tmpPath is set, or on Close() error explicitly call os.Remove(tmpPath) (and log any remove error) before returning, and still return the cleanup function to the caller; update the code around tmpFile, tmpPath, tmpFile.Close(), and cleanup so that any early return after creation always removes the temp or provides a cleanup callback.internal/orchestrator/directory_recreation_pbs.go (1)
168-217: 💤 Low valueMinor: redundant chown of
basePathwhen it's already indirsToFix.If
computeMissingDirs(basePath)returns the freshly-missing base path,basePathappears indirsToFixand is chowned in the loop, then chowned again unconditionally on line 214. It's idempotent so this is not a defect, just a small redundancy you can de-duplicate if you want — skip-worthy otherwise.🤖 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 `@internal/orchestrator/directory_recreation_pbs.go` around lines 168 - 217, The applyPBSDatastoreOwnership function currently always calls setDatastoreOwnership(basePath) after iterating dirsToFix, which can duplicate ownership work when basePath is already present in dirsToFix; update applyPBSDatastoreOwnership to check whether basePath is already in dirsToFix (or deduplicate dirsToFix) and only call setDatastoreOwnership(basePath) when it was not already processed, keeping the existing logger.Warning behavior on errors and the Debug log for count/context.internal/orchestrator/pbs_notifications_api_apply.go (1)
28-330: 💤 Low valueLGTM — desired-state refactor is a real clarity win.
Splitting the apply path into
loadPBSNotificationDesiredState→ strict cleanup →syncPBSNotificationEndpoints/syncPBSNotificationMatchersmakes the strict-mode contract obvious, and centralizing the listing inlistPBSNotificationIDsremoves duplicated--output-format=jsonplumbing. The redaction wiring (redactFlags+redactIndex) is preserved end-to-end intorunPBSManagerRedacted.Two small notes (skip-or-take):
upsertPBSNotificationEndpointandupsertPBSNotificationMatcheruse%vwhen combining the create + update errors. That's a deliberate trade-off (noerrors.Ison either) but readable;errors.Join(err, upErr)would also work if you ever need programmatic inspection.pbsEndpointRedactIndexeshard-codes[]int{6}for gotify. A named constant explaining "token positional index innotification endpoint gotify create <name> <server> <token> ..." would help future readers if positional args ever shift.🤖 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 `@internal/orchestrator/pbs_notifications_api_apply.go` around lines 28 - 330, Replace the opaque combined-error fmt.Errorf in upsertPBSNotificationEndpoint (the error returned in the create/update failure path) and upsertPBSNotificationMatcher with an error that wraps the joined errors using errors.Join and %w so callers can inspect them (e.g. return fmt.Errorf("endpoint %s:%s: %w", typ, name, errors.Join(err, upErr)) and similarly for matcher), and replace the magic number in pbsEndpointRedactIndexes by introducing a named constant (e.g. gotifyTokenRedactIndex) and return that constant instead of []int{6} so the meaning ("token positional index in `notification endpoint gotify create <name> <server> <token> ...`") is documented next to the constant; update references in pbsEndpointRedactIndexes accordingly.internal/orchestrator/encryption_exported_test.go (2)
248-248: 💤 Low valueSame consideration for file close pattern.
Similar to the pipe cleanup, the anonymous closure pattern for file cleanup seems unnecessarily verbose for test code unless addressing a specific linter requirement.
Also applies to: 276-276
🤖 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 `@internal/orchestrator/encryption_exported_test.go` at line 248, The anonymous closure used to close the test file (defer func() { _ = f.Close() }()) is unnecessarily verbose; replace it with a simple defer f.Close() in the test(s) in encryption_exported_test.go (references: variable f and its close calls around the current occurrences) and make the same replacement at the other occurrence noted (around line 276) to simplify the cleanup pattern.
180-187: 💤 Low valueConsider simpler error discard pattern.
The deferred anonymous closures (
defer func() { _ = ...Close() }()) explicitly discard errors, which is appropriate for test cleanup. However, unless required by a linter, the simpler patterndefer func() { _ = f.Close() }()or even directdefer f.Close()should suffice in test code.If this change addresses a specific linter requirement (e.g., ineffectual assignment), consider documenting that or using a nolint directive for clarity.
🤖 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 `@internal/orchestrator/encryption_exported_test.go` around lines 180 - 187, Replace the verbose deferred anonymous closures that discard Close() errors with the simpler pattern; locate the variables inR, inW, outR, outW in encryption_exported_test.go (the test cleanup block) and change the four lines like `defer func() { _ = inR.Close() }()` to simple `defer inR.Close()` (and similarly for inW, outR, outW); if a linter requires explicit error discard, instead use the shorter `defer func() { _ = inR.Close() }()` form or add a nolint with a brief comment.internal/orchestrator/restore_workflow_ui_fstab.go (1)
36-36: 💤 Low valueConsider making the timeout configurable.
The 90-second timeout for fstab merge confirmation is hardcoded. If this timeout needs to vary based on context or configuration, consider extracting it as a parameter or constant.
🤖 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 `@internal/orchestrator/restore_workflow_ui_fstab.go` at line 36, The hardcoded 90-second timeout passed to ui.ConfirmFstabMerge should be made configurable: replace the literal 90*time.Second with a named constant or a configurable parameter (e.g., fstabMergeTimeout or cfg.FstabMergeTimeout) and update the call site in restore workflow where ConfirmFstabMerge is invoked; ensure the new constant/field has a sensible default and is documented so callers can override it when creating the orchestrator or UI.
🤖 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 @.github/instructions/codacy.instructions.md:
- Around line 10-12: The markdown bullets for `rootPath`, `file`, and `tool`
have one extra leading space causing MD005 failures; normalize indentation by
removing the extra leading space so these list items align with their sibling
bullets (apply the same fix for the other affected groups listed: lines
referencing the other bullet ranges), ensuring all top-level bullets use the
same indentation level throughout the document.
In `@internal/backup/collector_system.go`:
- Around line 529-535: These inventory probes (calls to c.safeCmdOutput with
commandSpec and filepath.Join(commandsDir, "...")) must be best-effort: do not
let their errors abort the whole collection. For each nonessential probe (e.g.,
hostname -f, lsusb, systemctl, dmidecode, sensors) wrap the execution in a
lightweight capability check (use exec.LookPath("cmd") for presence, uid check
or /sys/device checks for dmidecode, and container/service checks for systemctl)
and if the capability is missing log a warning/debug and skip running it; if you
still call c.safeCmdOutput, swallow its error (log it) instead of returning it
so collection continues. Update the sites where these probes occur (the blocks
invoking c.safeCmdOutput with commandSpec and commandsDir for those utilities)
to follow this pattern.
In `@internal/checks/checks.go`:
- Around line 378-382: The defer that logs f.Close() errors should instead treat
Close() failure as a real acquisition failure: modify the function that opens
the lock file (the scope with the deferred func referencing f and lockPath) to
capture and propagate Close() errors (e.g., convert the function to use a named
error return or check Close() immediately before returning success) so that if
f.Close() returns an error you return a non-nil error and do not report the lock
as successfully acquired; keep the log but ensure the Close error prevents
success.
In `@internal/cli/args.go`:
- Around line 212-223: The help writer prints most text to the provided writer w
but calls flag.PrintDefaults(), which writes to the global flag output instead;
update printHelp to temporarily set the flag package output to w before calling
flag.PrintDefaults() and restore the original output afterward so all help text
goes to w; locate the printHelp function and use
flag.CommandLine.Output()/SetOutput or flag.CommandLine.SetOutput(w) with
backing save/restore of the previous io.Writer around the flag.PrintDefaults()
call to ensure consistent, testable output.
In `@internal/orchestrator/directory_recreation_ownership.go`:
- Around line 72-76: The function ensureDatastoreDirectoryMode currently
swallows all os.Stat errors; change the control flow so real Stat failures are
returned immediately and only the non-directory case is ignored. Specifically,
in ensureDatastoreDirectoryMode replace the existing "if err != nil ||
!info.IsDir() { return nil }" logic with: call os.Stat(path), if err != nil
return err, then check if !info.IsDir() { return nil } so only the non-directory
case skips chmod while ENOENT/EACCES/EIO (Stat errors) propagate.
In `@internal/orchestrator/directory_recreation_pbs_config.go`:
- Around line 29-32: The backup filename generation using backupPath with
nowRestore().Format("20060102-150405") can collide when runs occur within the
same second; update the code that builds backupPath (the filepath.Join call) and
the write logic (os.WriteFile) to produce a unique filename per run—for example,
use os.CreateTemp with a timestamped prefix derived from nowRestore() (or append
nowRestore().UnixNano()/a short random suffix) to avoid collisions, then write
the raw bytes to that temp file (ensuring 0600 permissions) and return errors as
before; adjust references to backupPath, nowRestore, filepath.Join, and
os.WriteFile accordingly.
- Around line 25-32: Change the creation of the proxsave directory and backup
file to avoid world-enumerable directory and predictable shared-tmp races:
replace the os.MkdirAll("/tmp/proxsave", 0o755) call with creation of a private
per-run directory (either use os.MkdirAll with 0o700 on an already-trusted
proxsave runtime path or use os.MkdirTemp("/tmp", "proxsave-") to create a
unique temp dir), then write the backup file to that directory via the
backupPath variable before calling os.WriteFile; also ensure directory
permissions are 0o700 and the backup file remains 0o600 so only the owner can
list or read the backup (adjust logic around nowRestore(), backupPath,
os.MkdirAll/os.MkdirTemp, and os.WriteFile accordingly).
In `@internal/orchestrator/mount_guard_apply.go`:
- Around line 180-215: The mount-state probe errors are currently treated as
"not mounted" (safe to guard) where mounted, err := isMounted(guardTarget)
returns an error; change that so probe failures are considered inconclusive and
do not allow protectOfflineTarget() to proceed: in the block that currently logs
a.warning("PBS mount guard: unable to check mount status...") and returns true,
return false instead and update the log message to indicate an inconclusive
probe. Keep the existing strict checks in targetIsMounted and
targetMovedOffRootFS (they already require err == nil), and ensure
mountAttemptSucceeded still only signals success when those helper methods
return true.
In `@internal/orchestrator/restore_archive.go`:
- Line 274: The defer call defer closeIntoErr(&err, logFile, "close detailed
restore log") can escalate a non-fatal log-close failure into a restore error;
change it so closing the best-effort log does not mutate the outer err—e.g.,
replace the defer with a closure that calls logFile.Close() and, if it returns a
non-nil error, logs or warns about the failure (but does not assign into err) so
the restore result is not overwritten; keep references to closeIntoErr, logFile
and the "close detailed restore log" context to locate the change.
In `@internal/orchestrator/restore_workflow_ui_extract.go`:
- Around line 255-269: The staging directory is created with overly permissive
0o755; change the permissions to 0o700 when creating the stage root in
extractStagedCategories (use restoreFS.MkdirAll(w.stageRoot, 0o700)) and
likewise tighten the parent /tmp/proxsave creation in the code that implements
stageDestRoot / restore_archive.go (replace its MkdirAll mode 0o755 with 0o700);
also audit the exportRoot creation (the export root creation site around line
177) and set its MkdirAll to 0o700 so staged/exported sensitive data is not
world-traversable while extracting via extractSelectiveArchive/atomicFileChmod.
In `@internal/orchestrator/restore_workflow_ui_full.go`:
- Around line 58-63: The validate method in fullRestoreUIFlow currently checks
f.prepared.Manifest.ArchivePath but ArchivePath is a direct field on the
preparedBundle; update the validation in fullRestoreUIFlow.validate to check
f.prepared.ArchivePath == "" (instead of f.prepared.Manifest.ArchivePath) so the
function uses the correct field on the preparedBundle and mirrors other usages
of f.prepared.ArchivePath in the codebase.
In `@internal/orchestrator/restore_workflow_ui_run.go`:
- Around line 55-65: The prepared bundle can be created by prepareBundle(), so
change run() to call prepareBundle() (the function used inside
prepareBundleAndPlan()) directly and immediately defer w.prepared.Cleanup() as
soon as prepareBundle() returns a non-nil w.prepared; then perform the remaining
planning steps (analyzeArchive, confirmCompatibility, selectRestorePlan,
configurePlanForRuntime) and return errors as before. In short: obtain the
prepared bundle first, register defer w.prepared.Cleanup() right after
successful prepareBundle() return, then continue with the rest of
prepareBundleAndPlan's logic; keep runFullRestoreWithUI(w.ctx, w.ui,
w.candidate, w.prepared, ...) and runSelectiveRestore() behavior unchanged so
cleanup always runs.
---
Outside diff comments:
In `@cmd/proxsave/runtime_helpers.go`:
- Around line 87-101: The loop that searches for a base directory (using
variable dir, baseDir and checks via os.Stat(filepath.Join(dir, "env")) and
os.Stat(filepath.Join(dir, "script"))) stops before checking the filesystem
root; change the traversal to perform the env/script checks for the current dir
first and only break after computing parent := filepath.Dir(dir) and comparing
parent == dir, so the root directory is evaluated too (i.e., use an
unconditional loop over dir with the stat checks before the parent == dir
break). Ensure you still set baseDir = dir and break when a match is found.
In `@internal/backup/optimizations.go`:
- Around line 246-304: splitFile/writeChunk create an empty trailing chunk when
source size is an exact multiple of chunkSize; modify writeChunk (or its call
pattern) so it does not create the output file before confirming there are bytes
to write: perform an initial non-destructive check/peek from src (e.g., Read
into the provided buf or use src.ReadAt/Seek to test for EOF) and only
open/create chunkPath (os.OpenFile) once at least one byte has been read; update
writeChunk signature/behavior accordingly so splitFile's loop stops without
producing a zero-length chunk when done==true after an initial EOF check.
In `@internal/orchestrator/restore_decision.go`:
- Around line 161-167: The file's readRestoreDecisionMetadata function currently
rejects tar entries with header.Typeflag == tar.TypeRegA even though TypeRegA is
a valid (deprecated) alternate regular-file flag; update the validation in
readRestoreDecisionMetadata to accept both tar.TypeReg and tar.TypeRegA by
changing the conditional that checks header.Typeflag so it only errors when
header.Typeflag is neither tar.TypeReg nor tar.TypeRegA, keeping the same error
message and behavior otherwise.
In `@internal/tui/components/panel.go`:
- Around line 48-70: SuccessPanel, ErrorPanel, and WarningPanel still call
methods on the embedded Box via panel.Box.SetBorderColor/... and
panel.Box.SetTitleColor/...; update them to call the receiver methods on Panel
directly (e.g., panel.SetBorderColor(...), panel.SetTitleColor(...)) to match
the refactor done in InfoPanel and avoid directly referencing panel.Box in
SuccessPanel, ErrorPanel, and WarningPanel.
---
Nitpick comments:
In `@embed.go`:
- Around line 37-43: The InstallableDocs function returns a shallow copy of the
installableDocs slice but the DocAsset.Data byte slices are shared; update the
function comment for InstallableDocs to explicitly state that callers must treat
the returned DocAsset.Data as read-only and must not modify the byte slices (or
allocate copies themselves if mutation is required), referencing DocAsset and
the shared installableDocs backing data so future maintainers understand the
shared underlying memory semantics.
In `@internal/backup/archiver.go`:
- Around line 25-32: The helper closeIntoErr is duplicated; extract it into a
small shared internal package (e.g. internal/closeerr or internal/ioutil) and
replace the copies in both backup and orchestrator with calls to the shared
implementation; create the new package with the same semantics (either keep the
function unexported and call via package-scoped name if colocated, or export as
CloseIntoErr and update callers), update imports in files that used the old
duplicate (e.g. functions currently calling closeIntoErr in backup and the file
in orchestrator), remove the redundant definitions, and run tests to ensure
behavior is unchanged.
In `@internal/metrics/prometheus.go`:
- Around line 81-242: Replace the repetitive metric-writing error-wrapping with
a small helper closure that captures f and tmpPath and maintains a sticky error:
add a wrap(err error) error that sets and returns a single writeErr, refactor
the existing writeMetric(name,mtype,help,value) to check writeErr and call wrap
on any fmt.Fprintf/Fprintln failures, and add a writef(format string, a ...any)
helper that does the same for the ad-hoc fmt.Fprintf calls used for per-location
and info metrics; then replace all direct fmt.Fprintf/Fprintln blocks and the
current writeMetric usages to return writeErr directly instead of rewrapping
with fmt.Errorf("write metrics file %s: %w", tmpPath, err). Ensure symbols
modified are writeMetric, wrap (or writeErr), writef and use tmpPath and f
captured by the closures.
In `@internal/orchestrator/decrypt.go`:
- Around line 411-425: The tmp file created by os.CreateTemp (tmpFile/tmpPath)
can leak if tmpFile.Close() fails because you return before installing or
running cleanup; ensure the temp is removed on any error after creation: define
cleanup (closure referencing tmpPath) immediately after tmpPath is set, or on
Close() error explicitly call os.Remove(tmpPath) (and log any remove error)
before returning, and still return the cleanup function to the caller; update
the code around tmpFile, tmpPath, tmpFile.Close(), and cleanup so that any early
return after creation always removes the temp or provides a cleanup callback.
In `@internal/orchestrator/directory_recreation_pbs_config.go`:
- Around line 35-42: Replace the predictable tmpPath logic with a unique temp
file created in the same directory using os.CreateTemp: call
os.CreateTemp(filepath.Dir(path), "datastore.cfg.proxsave-*") to obtain a unique
file handle/name, write the normalized contents to that temp file (ensure file
mode is set via os.Chmod if needed), close the temp file, then
os.Rename(tempName, path) to atomically replace the target; on any error remove
the temp file (os.Remove) and return the wrapped error. Locate and update the
block around tmpPath, os.WriteFile and os.Rename in
directory_recreation_pbs_config.go to implement this change.
In `@internal/orchestrator/directory_recreation_pbs_lock.go`:
- Around line 60-76: The function removeOrRenamePBSDatastoreLockDirectory
currently returns (true, err) when os.Remove or os.Rename fails; change it so
the function only returns true when the filesystem operation actually succeeded:
call os.Remove(lockPath), check its error and if non-nil return (false, err)
instead of (true, err); similarly, after attempting os.Rename(lockPath,
backupPath) only return (true, nil) when err == nil and return (false, err) on
failure; keep the existing log messages and preserve the backupPath behavior in
the rename branch.
In `@internal/orchestrator/directory_recreation_pbs.go`:
- Around line 168-217: The applyPBSDatastoreOwnership function currently always
calls setDatastoreOwnership(basePath) after iterating dirsToFix, which can
duplicate ownership work when basePath is already present in dirsToFix; update
applyPBSDatastoreOwnership to check whether basePath is already in dirsToFix (or
deduplicate dirsToFix) and only call setDatastoreOwnership(basePath) when it was
not already processed, keeping the existing logger.Warning behavior on errors
and the Debug log for count/context.
In `@internal/orchestrator/encryption_exported_test.go`:
- Line 248: The anonymous closure used to close the test file (defer func() { _
= f.Close() }()) is unnecessarily verbose; replace it with a simple defer
f.Close() in the test(s) in encryption_exported_test.go (references: variable f
and its close calls around the current occurrences) and make the same
replacement at the other occurrence noted (around line 276) to simplify the
cleanup pattern.
- Around line 180-187: Replace the verbose deferred anonymous closures that
discard Close() errors with the simpler pattern; locate the variables inR, inW,
outR, outW in encryption_exported_test.go (the test cleanup block) and change
the four lines like `defer func() { _ = inR.Close() }()` to simple `defer
inR.Close()` (and similarly for inW, outR, outW); if a linter requires explicit
error discard, instead use the shorter `defer func() { _ = inR.Close() }()` form
or add a nolint with a brief comment.
In `@internal/orchestrator/pbs_notifications_api_apply.go`:
- Around line 28-330: Replace the opaque combined-error fmt.Errorf in
upsertPBSNotificationEndpoint (the error returned in the create/update failure
path) and upsertPBSNotificationMatcher with an error that wraps the joined
errors using errors.Join and %w so callers can inspect them (e.g. return
fmt.Errorf("endpoint %s:%s: %w", typ, name, errors.Join(err, upErr)) and
similarly for matcher), and replace the magic number in pbsEndpointRedactIndexes
by introducing a named constant (e.g. gotifyTokenRedactIndex) and return that
constant instead of []int{6} so the meaning ("token positional index in
`notification endpoint gotify create <name> <server> <token> ...`") is
documented next to the constant; update references in pbsEndpointRedactIndexes
accordingly.
In `@internal/orchestrator/restore_workflow_ui_fstab.go`:
- Line 36: The hardcoded 90-second timeout passed to ui.ConfirmFstabMerge should
be made configurable: replace the literal 90*time.Second with a named constant
or a configurable parameter (e.g., fstabMergeTimeout or cfg.FstabMergeTimeout)
and update the call site in restore workflow where ConfirmFstabMerge is invoked;
ensure the new constant/field has a sensible default and is documented so
callers can override it when creating the orchestrator or UI.
In `@internal/storage/secondary.go`:
- Around line 295-298: The tempFile close error path can cause a double-close
because the deferred cleanup still sees tempFile non-nil; in the block that
calls tempFile.Close() (the local tempFile variable in
internal/storage/secondary.go), set tempFile = nil immediately after calling
Close() (i.e., before checking/returning on the error) so the deferred cleanup
won't try to close it again; modify the Close/error handling around
tempFile.Close() accordingly (reference tempFile and the deferred cleanup block
that closes it).
In `@internal/tui/components/form.go`:
- Around line 56-59: The AddPasswordField wrapper currently calls the embedded
form method directly (f.Form.AddPasswordField) which is inconsistent with other
wrappers; change the call to use the wrapper receiver (call
f.AddPasswordField(...) instead of f.Form.AddPasswordField(...)) so it follows
the same refactor pattern as AddInputField/AddButton; ensure you pass the same
arguments (label, "", fieldWidth, '*', nil) when updating the call and verify
there is no accidental recursion by ensuring the wrapper delegates to the
underlying form implementation if a different internal helper exists.
🪄 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: e2ad5d35-6095-414d-a6f6-5866f4683645
📒 Files selected for processing (138)
.github/instructions/codacy.instructions.md.github/workflows/codecov.yml.github/workflows/release.ymlcmd/proxsave/main_defers.gocmd/proxsave/runtime_helpers.gocmd/proxsave/upgrade.goembed.gogo.modinternal/backup/archiver.gointernal/backup/archiver_test.gointernal/backup/archiver_verification_test.gointernal/backup/checksum.gointernal/backup/collector.gointernal/backup/collector_bricks_pve.gointernal/backup/collector_pbs.gointernal/backup/collector_pbs_datastore.gointernal/backup/collector_pve.gointernal/backup/collector_pve_additional_test.gointernal/backup/collector_pve_util_test.gointernal/backup/collector_system.gointernal/backup/collector_test.gointernal/backup/optimizations.gointernal/backup/optimizations_bench_test.gointernal/checks/checks.gointernal/checks/checks_test.gointernal/cli/args.gointernal/config/config.gointernal/config/upgrade.gointernal/environment/detect.gointernal/environment/unprivileged.gointernal/identity/identity.gointernal/identity/identity_test.gointernal/input/input_test.gointernal/logging/logger.gointernal/logging/session_test.gointernal/metrics/prometheus.gointernal/notify/email.gointernal/notify/email_delivery_methods_test.gointernal/notify/email_relay.gointernal/notify/email_relay_test.gointernal/notify/gotify.gointernal/notify/gotify_test.gointernal/notify/telegram.gointernal/notify/telegram_registration.gointernal/notify/telegram_registration_test.gointernal/notify/templates.gointernal/notify/webhook.gointernal/notify/webhook_test.gointernal/orchestrator/backup_run_phases.gointernal/orchestrator/backup_safety.gointernal/orchestrator/backup_safety_glob_test.gointernal/orchestrator/backup_safety_test.gointernal/orchestrator/backup_sources.gointernal/orchestrator/bundle_test.gointernal/orchestrator/close_error.gointernal/orchestrator/decompress_reader_test.gointernal/orchestrator/decrypt.gointernal/orchestrator/decrypt_test.gointernal/orchestrator/decrypt_tui_e2e_helpers_test.gointernal/orchestrator/deps.gointernal/orchestrator/deps_additional_test.gointernal/orchestrator/directory_recreation.gointernal/orchestrator/directory_recreation_config.gointernal/orchestrator/directory_recreation_ownership.gointernal/orchestrator/directory_recreation_paths.gointernal/orchestrator/directory_recreation_pbs.gointernal/orchestrator/directory_recreation_pbs_config.gointernal/orchestrator/directory_recreation_pbs_inspect.gointernal/orchestrator/directory_recreation_pbs_lock.gointernal/orchestrator/directory_recreation_pve.gointernal/orchestrator/directory_recreation_test.gointernal/orchestrator/encryption.gointernal/orchestrator/encryption_exported_test.gointernal/orchestrator/fs_atomic.gointernal/orchestrator/log_parser.gointernal/orchestrator/mount_guard.gointernal/orchestrator/mount_guard_apply.gointernal/orchestrator/mount_guard_more_test.gointernal/orchestrator/network_apply_countdown_test.gointernal/orchestrator/network_apply_preflight_rollback_test.gointernal/orchestrator/network_apply_workflow_ui.gointernal/orchestrator/network_apply_workflow_ui_prompt.gointernal/orchestrator/network_apply_workflow_ui_rollback.gointernal/orchestrator/network_diagnostics.gointernal/orchestrator/network_health.gointernal/orchestrator/network_plan.gointernal/orchestrator/network_staged_apply.gointernal/orchestrator/nic_mapping.gointernal/orchestrator/nic_mapping_additional_test.gointernal/orchestrator/orchestrator.gointernal/orchestrator/pbs_notifications_api_apply.gointernal/orchestrator/pbs_staged_apply.gointernal/orchestrator/prompts_cli_test.gointernal/orchestrator/resolv_conf_repair.gointernal/orchestrator/restore_archive.gointernal/orchestrator/restore_archive_entries.gointernal/orchestrator/restore_archive_extract.gointernal/orchestrator/restore_coverage_extra_test.gointernal/orchestrator/restore_decision.gointernal/orchestrator/restore_decision_test.gointernal/orchestrator/restore_decompression.gointernal/orchestrator/restore_errors_test.gointernal/orchestrator/restore_firewall_additional_test.gointernal/orchestrator/restore_test.gointernal/orchestrator/restore_tui.gointernal/orchestrator/restore_tui_simulation_test.gointernal/orchestrator/restore_workflow_test.gointernal/orchestrator/restore_workflow_ui.gointernal/orchestrator/restore_workflow_ui_apply.gointernal/orchestrator/restore_workflow_ui_backups_services.gointernal/orchestrator/restore_workflow_ui_cluster_apply.gointernal/orchestrator/restore_workflow_ui_extract.gointernal/orchestrator/restore_workflow_ui_fstab.gointernal/orchestrator/restore_workflow_ui_full.gointernal/orchestrator/restore_workflow_ui_plan.gointernal/orchestrator/restore_workflow_ui_run.gointernal/orchestrator/restore_workflow_ui_tfa.gointernal/orchestrator/selective_additional_test.gointernal/orchestrator/temp_registry.gointernal/orchestrator/workflow_ui_tui_decrypt.gointernal/orchestrator/workflow_ui_tui_restore.gointernal/orchestrator/workflow_ui_tui_shared.gointernal/pbs/namespaces_test.gointernal/safeexec/safeexec.gointernal/security/security.gointernal/security/security_test.gointernal/storage/cloud.gointernal/storage/filesystem.gointernal/storage/local.gointernal/storage/secondary.gointernal/tui/abort_context_test.gointernal/tui/components/form.gointernal/tui/components/form_test.gointernal/tui/components/list_form_item.gointernal/tui/components/panel.gointernal/tui/wizard/age.gointernal/tui/wizard/install.gointernal/tui/wizard/post_install_audit_tui.go
💤 Files with no reviewable changes (2)
- internal/environment/unprivileged.go
- internal/orchestrator/network_staged_apply.go
|
@coderabbitai help |
ChatThere are 3 ways to chat with CodeRabbit:
CodeRabbit commands
Other keywords and placeholders
CodeRabbit configuration file (
|
- Make nonessential system inventory probes best-effort - Harden restore staging/export permissions and PBS datastore cfg temp handling - Fix lock close propagation, help output routing, mount guard probe behavior, and chunk splitting - Share close-error handling and clean up several small review findings - Add coverage for tar TypeRegA restore metadata entries
There was a problem hiding this comment.
Actionable comments posted: 6
♻️ Duplicate comments (1)
internal/backup/collector_system.go (1)
60-78:⚠️ Potential issue | 🟠 Major | ⚡ Quick winPropagate cancellation out of
collectBestEffortProbe.Line 75 downgrades every
safeCmdOutputfailure to a warning. That also swallowscontext.Canceled/context.DeadlineExceeded, so a canceled collection can keep running until some later explicitctx.Err()check. Please return context errors here and only suppress real probe failures.Suggested shape
-func (c *Collector) collectBestEffortProbe(ctx context.Context, spec CommandSpec, output, description string, available func() (bool, string)) { +func (c *Collector) collectBestEffortProbe(ctx context.Context, spec CommandSpec, output, description string, available func() (bool, string)) error { if _, err := c.depLookPath(spec.Name); err != nil { c.logger.Debug("Skipping %s: command %s not available: %v", description, spec.Name, err) - return + return nil } if available != nil { ok, reason := available() if !ok { if reason == "" { reason = "required capability not detected" } c.logger.Debug("Skipping %s: %s", description, reason) - return + return nil } } if err := c.safeCmdOutput(ctx, spec, output, description, false); err != nil { + if errors.Is(err, context.Canceled) || errors.Is(err, context.DeadlineExceeded) { + return err + } c.logger.Warning("Skipping %s: %v", description, err) } + return nil }Then let the call sites return the error when this helper reports cancellation.
🤖 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 `@internal/backup/collector_system.go` around lines 60 - 78, collectBestEffortProbe currently swallows all errors from safeCmdOutput and only logs a warning, which hides context cancellations; change collectBestEffortProbe (now a void) to return error, call safeCmdOutput and if it returns context.Canceled or context.DeadlineExceeded (or ctx.Err() != nil) immediately return that error instead of logging, otherwise log/suppress non-context probe failures and return nil; update all call sites of collectBestEffortProbe to propagate/return the error so collection stops on cancellation while still ignoring ordinary probe failures.
🧹 Nitpick comments (5)
internal/orchestrator/encryption_exported_test.go (1)
180-181: ⚡ Quick winPrefer deferring cleanup immediately after successful resource acquisition.
The explicit close calls on lines 180-181 followed by
t.Fatalfwill cause the deferred closes on lines 184-185 to execute on already-closed pipes. While harmless in test code (the error is ignored), the idiomatic Go pattern is to defer cleanup immediately after successful resource creation:inR, inW, err := os.Pipe() if err != nil { t.Fatalf("pipe stdin: %v", err) } defer inR.Close() defer inW.Close() outR, outW, err := os.Pipe() if err != nil { t.Fatalf("pipe stdout: %v", err) // defers for inR/inW clean up automatically } defer outR.Close() defer outW.Close()This eliminates the double-close and makes cleanup automatic without explicit error-path handling.
♻️ Refactored cleanup pattern
inR, inW, err := os.Pipe() if err != nil { t.Fatalf("pipe stdin: %v", err) } + defer inR.Close() + defer inW.Close() + outR, outW, err := os.Pipe() if err != nil { - _ = inR.Close() - _ = inW.Close() t.Fatalf("pipe stdout: %v", err) } - defer inR.Close() - defer inW.Close() defer outR.Close() defer outW.Close()🤖 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 `@internal/orchestrator/encryption_exported_test.go` around lines 180 - 181, Change the test to defer closing the pipes immediately after each successful os.Pipe() call instead of doing explicit closes before t.Fatalf; after creating inR,inW and checking err, call defer inR.Close() and defer inW.Close(), and likewise after creating outR,outW call defer outR.Close() and defer outW.Close(), so you remove the explicit _ = inR.Close()/inW.Close() before t.Fatalf and avoid double-closing while keeping t.Fatalf behavior intact.internal/orchestrator/directory_recreation_pbs_config.go (2)
50-52: ⚡ Quick winRemove redundant chmod:
os.MkdirTempalready sets0o700.
os.MkdirTempcreates the directory with0o700permissions by default, so the explicitos.Chmod(backupDir, 0o700)is redundant and triggers a false-positive static analysis warning.♻️ Remove redundant permission setting
if err != nil { return "", err } - if err := os.Chmod(backupDir, 0o700); err != nil { - return "", err - } prefix := fmt.Sprintf("datastore.cfg.pre-normalize.%s-", nowRestore().Format("20060102-150405"))🤖 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 `@internal/orchestrator/directory_recreation_pbs_config.go` around lines 50 - 52, The explicit os.Chmod(backupDir, 0o700) is redundant because os.MkdirTemp already creates the temp directory with 0o700; remove the os.Chmod call that references backupDir to avoid the static-analysis false positive and keep the original os.MkdirTemp usage intact (locate the os.Chmod(backupDir, 0o700) call in the function that creates backupDir and delete that single statement).
115-147: 💤 Low valueNormalization logic assumes datastore-only content.
The
inDatastoreBlockflag is set when encountering anydatastore:line and never reset, meaning all subsequent non-indented, non-ignored lines will be indented. This is correct ifdatastore.cfgexclusively containsdatastore:blocks, but would incorrectly indent other top-level sections if present.Given the function name
normalizePBSDatastoreCfgand the specific PBS config context, this assumption appears intentional. Consider adding a comment clarifying that the normalizer expects datastore-block-only content.📝 Suggested clarifying comment
func normalizePBSDatastoreCfgContent(content string) (string, int) { + // Assumes content contains only datastore: blocks and their properties. + // Once a datastore block is encountered, all non-ignored, non-indented lines + // are treated as properties requiring indentation. lines := strings.Split(content, "\n") inDatastoreBlock := false🤖 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 `@internal/orchestrator/directory_recreation_pbs_config.go` around lines 115 - 147, The normalizer (normalizePBSDatastoreCfgContent) sets inDatastoreBlock on any "datastore:" and never resets, which will wrongly indent subsequent top-level sections unless the file contains only datastore blocks; update the code by adding a clear explanatory comment above normalizePBSDatastoreCfgContent (and/or classifyPBSDatastoreCfgLine) stating the function assumes datastore-only content and that inDatastoreBlock is intentionally never cleared, or if you want to support mixed config files, change the logic in classifyPBSDatastoreCfgLine to detect other top-level section headers and reset inDatastoreBlock accordingly—refer to the functions normalizePBSDatastoreCfgContent and classifyPBSDatastoreCfgLine to locate where to add the comment or adjust the reset logic.internal/backup/optimizations.go (1)
293-293: 💤 Low valueConsider tightening chunk file permissions.
The chunk file is created with
defaultChunkFilePerm(0o640=rw-r-----), which grants group-read access. Unless the deployment requires group access to backup chunks,0o600(rw-------) better follows least-privilege principles.🔒 Suggested permission tightening
Update the constant at line 23:
- defaultChunkFilePerm = 0o640 + defaultChunkFilePerm = 0o600🤖 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 `@internal/backup/optimizations.go` at line 293, The chunk file is opened with defaultChunkFilePerm (used in os.OpenFile in optimizations.go) which is currently 0o640 and grants group read; change the constant defaultChunkFilePerm to 0o600 so files are created with rw------- (least-privilege) and ensure any tests or code paths referencing defaultChunkFilePerm continue to compile with the new value.internal/orchestrator/directory_recreation_pbs.go (1)
168-203: 💤 Low valueInitialization flow is correct; consider deferring
dirsToFixappend untilMkdirAllsucceeds.The current
ensurePBSDatastoreSubdirsadds a path todirsToFix(line 196) regardless of whether the subsequentos.MkdirAll(line 198) succeeds. OnMkdirAllfailure,applyPBSDatastoreOwnershipwill then callsetDatastoreOwnershipagainst a path that doesn't exist, producing an extra "could not set ownership" warning that just echoes the prior failure. Functionally harmless, but slightly noisier on partial-failure runs.♻️ Optional reorder so the path is only tracked when actually created
func ensurePBSDatastoreSubdirs(basePath string, dirsToFix []string, logger *logging.Logger) (bool, []string) { changed := false for _, subdir := range pbsDatastoreSubdirs { path := filepath.Join(basePath, subdir) - if isMissingPath(path) { - changed = true - dirsToFix = append(dirsToFix, path) - } + wasMissing := isMissingPath(path) if err := os.MkdirAll(path, 0750); err != nil { logger.Warning("Failed to create %s: %v", path, err) + continue + } + if wasMissing { + changed = true + dirsToFix = append(dirsToFix, path) } } return changed, dirsToFix }Note:
0o750on lines 174 and 198 is flagged by the linter, but it appears intentional given thebackup:backupownership and group-traversal needs documented forsetDatastoreOwnership— no change suggested there.🤖 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 `@internal/orchestrator/directory_recreation_pbs.go` around lines 168 - 203, The dirsToFix slice is being appended before os.MkdirAll succeeds in ensurePBSDatastoreSubdirs which causes applyPBSDatastoreOwnership (and ultimately setDatastoreOwnership) to try to change ownership of non-existent paths; fix this by only appending path to dirsToFix after os.MkdirAll returns nil, i.e. call os.MkdirAll(path, 0750) first, handle/log any error (logger.Warning) and only when it succeeds set changed = true and append the path to dirsToFix; keep the rest of the flow (initializePBSDatastore calling applyPBSDatastoreOwnership and ensurePBSDatastoreLockFile) unchanged.
🤖 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 `@internal/backup/collector_system.go`:
- Around line 971-975: The lsmod and SMART discovery calls should be best-effort
like lsusb/dmidecode: modify the safeCmdOutput invocation for the lsmod block
(the commandSpec("lsmod") call and its filepath.Join(commandsDir, "lsmod.txt")
output) and the smartctl --scan invocation (the smartctl command path/args and
its output file) so that failures do not cause the overall collection to abort;
pass the same permissive flag/option used by lsusb/dmidecode (or change the
boolean error-fail parameter to false) and ensure errors are logged at
debug/trace level while continuing the collection.
In `@internal/checks/checks.go`:
- Around line 381-397: When creating the lock file in the block that calls
f.WriteString, syncFile and f.Close (the code that sets
result.Error/result.Message and logs via c.logger), ensure any failure during
WriteString or the final Close removes the partially-created lock file so it
doesn't leave a stale .backup.lock; update the error paths after WriteString and
after the failing Close to attempt to close the file if still open and then call
os.Remove(lockPath) (logging any remove/close errors via c.logger.Warning)
before setting result.Error/result.Message and returning; reference the
variables and functions in the diff (f, WriteString, syncFile, f.Close,
lockPath, c.logger, result) when making the change.
In `@internal/orchestrator/restore_decision_test.go`:
- Around line 65-72: The test TestReadRestoreDecisionMetadataAcceptsTypeRegA
uses the deprecated tar.TypeRegA; replace that usage by defining a local
constant (e.g., const nulTypeFlag byte = 0) and use nulTypeFlag in the
tar.Header Typeflag field instead of tar.TypeRegA so the test preserves the NUL
type behavior without referencing the deprecated API; update any occurrences in
this test function to reference the new local constant.
In `@internal/orchestrator/restore_workflow_ui_extract.go`:
- Around line 181-209: The SAFE cluster apply currently only checks w.exportRoot
so it may run against a partially failed extraction; change runClusterSafeApply
to require a successful export extraction (e.g. ensure w.exportLogPath is set or
add a dedicated flag like w.exportExtracted set when extractSelectiveArchive
returns successfully) before calling runSafeClusterApplyWithUI; update
runClusterSafeApply to skip and log a warning if the export extraction did not
complete (use symbols runClusterSafeApply, extractSelectiveArchive,
w.exportLogPath or a new w.exportExtracted, and runSafeClusterApplyWithUI).
- Around line 245-277: stageAndApplySensitiveCategories currently proceeds to
applyStagedCategories even when extractStagedCategories swallowed non-abort
errors; change extractStagedCategories to return (bool, error) where the bool
indicates successful, complete staging (true) vs staging-with-warnings/errors
(false). Update stageAndApplySensitiveCategories to call success,err :=
extractStagedCategories(); if err != nil return err; if !success {
w.logger.Warning("Skipping apply due to staged extraction errors"); return nil }
so applyStagedCategories is only invoked when staging succeeded. Keep
handleStageExtractError semantics (use it to set w.restoreHadWarnings) but
ensure extractStagedCategories returns false when handleStageExtractError
returns nil for non-abort errors and only sets w.stageLogPath and returns true
on full success.
In `@internal/orchestrator/restore_workflow_ui_plan.go`:
- Around line 13-18: The helper method prepareBundleAndPlan on
restoreUIWorkflowRun is currently unused and flagged by linters; either call it
from the main restore path (replace the current separate prepareBundle() then
planPreparedBundle() sequence with a single call to prepareBundleAndPlan inside
the restore orchestration function that handles UI workflows) or remove the
helper entirely. Locate the restore orchestration entry (the function that
currently invokes prepareBundle() and planPreparedBundle() for
restoreUIWorkflowRun) and swap those calls for a single prepareBundleAndPlan()
invocation, or delete prepareBundleAndPlan() if no single-call refactor is
appropriate; ensure any error handling and return values remain consistent with
the existing callers.
---
Duplicate comments:
In `@internal/backup/collector_system.go`:
- Around line 60-78: collectBestEffortProbe currently swallows all errors from
safeCmdOutput and only logs a warning, which hides context cancellations; change
collectBestEffortProbe (now a void) to return error, call safeCmdOutput and if
it returns context.Canceled or context.DeadlineExceeded (or ctx.Err() != nil)
immediately return that error instead of logging, otherwise log/suppress
non-context probe failures and return nil; update all call sites of
collectBestEffortProbe to propagate/return the error so collection stops on
cancellation while still ignoring ordinary probe failures.
---
Nitpick comments:
In `@internal/backup/optimizations.go`:
- Line 293: The chunk file is opened with defaultChunkFilePerm (used in
os.OpenFile in optimizations.go) which is currently 0o640 and grants group read;
change the constant defaultChunkFilePerm to 0o600 so files are created with
rw------- (least-privilege) and ensure any tests or code paths referencing
defaultChunkFilePerm continue to compile with the new value.
In `@internal/orchestrator/directory_recreation_pbs_config.go`:
- Around line 50-52: The explicit os.Chmod(backupDir, 0o700) is redundant
because os.MkdirTemp already creates the temp directory with 0o700; remove the
os.Chmod call that references backupDir to avoid the static-analysis false
positive and keep the original os.MkdirTemp usage intact (locate the
os.Chmod(backupDir, 0o700) call in the function that creates backupDir and
delete that single statement).
- Around line 115-147: The normalizer (normalizePBSDatastoreCfgContent) sets
inDatastoreBlock on any "datastore:" and never resets, which will wrongly indent
subsequent top-level sections unless the file contains only datastore blocks;
update the code by adding a clear explanatory comment above
normalizePBSDatastoreCfgContent (and/or classifyPBSDatastoreCfgLine) stating the
function assumes datastore-only content and that inDatastoreBlock is
intentionally never cleared, or if you want to support mixed config files,
change the logic in classifyPBSDatastoreCfgLine to detect other top-level
section headers and reset inDatastoreBlock accordingly—refer to the functions
normalizePBSDatastoreCfgContent and classifyPBSDatastoreCfgLine to locate where
to add the comment or adjust the reset logic.
In `@internal/orchestrator/directory_recreation_pbs.go`:
- Around line 168-203: The dirsToFix slice is being appended before os.MkdirAll
succeeds in ensurePBSDatastoreSubdirs which causes applyPBSDatastoreOwnership
(and ultimately setDatastoreOwnership) to try to change ownership of
non-existent paths; fix this by only appending path to dirsToFix after
os.MkdirAll returns nil, i.e. call os.MkdirAll(path, 0750) first, handle/log any
error (logger.Warning) and only when it succeeds set changed = true and append
the path to dirsToFix; keep the rest of the flow (initializePBSDatastore calling
applyPBSDatastoreOwnership and ensurePBSDatastoreLockFile) unchanged.
In `@internal/orchestrator/encryption_exported_test.go`:
- Around line 180-181: Change the test to defer closing the pipes immediately
after each successful os.Pipe() call instead of doing explicit closes before
t.Fatalf; after creating inR,inW and checking err, call defer inR.Close() and
defer inW.Close(), and likewise after creating outR,outW call defer outR.Close()
and defer outW.Close(), so you remove the explicit _ = inR.Close()/inW.Close()
before t.Fatalf and avoid double-closing while keeping t.Fatalf behavior intact.
🪄 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: 7c467bec-78ba-4cab-bc91-129b60ef99d3
📒 Files selected for processing (29)
.github/instructions/codacy.instructions.mdcmd/proxsave/runtime_helpers.goembed.gointernal/backup/archiver.gointernal/backup/collector_system.gointernal/backup/optimizations.gointernal/checks/checks.gointernal/cli/args.gointernal/closeerr/closeerr.gointernal/metrics/prometheus.gointernal/orchestrator/close_error.gointernal/orchestrator/decrypt.gointernal/orchestrator/directory_recreation_ownership.gointernal/orchestrator/directory_recreation_pbs.gointernal/orchestrator/directory_recreation_pbs_config.gointernal/orchestrator/directory_recreation_pbs_lock.gointernal/orchestrator/encryption_exported_test.gointernal/orchestrator/mount_guard_apply.gointernal/orchestrator/pbs_notifications_api_apply.gointernal/orchestrator/restore_archive.gointernal/orchestrator/restore_decision.gointernal/orchestrator/restore_decision_test.gointernal/orchestrator/restore_workflow_ui_extract.gointernal/orchestrator/restore_workflow_ui_fstab.gointernal/orchestrator/restore_workflow_ui_full.gointernal/orchestrator/restore_workflow_ui_plan.gointernal/orchestrator/restore_workflow_ui_run.gointernal/storage/secondary.gointernal/tui/components/panel.go
✅ Files skipped from review due to trivial changes (1)
- .github/instructions/codacy.instructions.md
🚧 Files skipped from review as they are similar to previous changes (14)
- internal/cli/args.go
- internal/orchestrator/restore_decision.go
- internal/storage/secondary.go
- cmd/proxsave/runtime_helpers.go
- internal/orchestrator/directory_recreation_ownership.go
- internal/orchestrator/decrypt.go
- internal/orchestrator/restore_workflow_ui_fstab.go
- internal/orchestrator/restore_workflow_ui_run.go
- internal/metrics/prometheus.go
- embed.go
- internal/orchestrator/restore_workflow_ui_full.go
- internal/backup/archiver.go
- internal/orchestrator/pbs_notifications_api_apply.go
- internal/orchestrator/mount_guard_apply.go
Route the UI restore workflow through prepareBundleAndPlan so the helper is no longer dead code. Preserve prepared bundle cleanup semantics when planning fails, and add a regression test for that failure path. Add the missing go.sum checksums for the tcell version required by go.mod.
Make staged extraction report whether staging completed successfully, not just whether it returned a fatal error. Skip staged apply when extraction errors were downgraded to warnings, preserving restoreHadWarnings behavior and only recording stageLogPath after full staging success. Add regression coverage for incomplete staging.
Require a completed export extraction before running SAFE cluster apply. Use exportLogPath as the success marker from extractSelectiveArchive, skip SAFE apply with a warning when export extraction did not complete, and add regression coverage for the partial-export path.
Replace deprecated tar.TypeRegA usage with explicit NUL typeflag constants. Keep restore metadata parsing behavior for tar entries with typeflag 0, update the regression test to exercise that behavior directly, and remove the deprecated symbol from both test and production code.
Remove partially-created backup lock files when lock creation fails during WriteString or the final Close. Log cleanup failures as warnings, preserve the original lock acquisition error, and add regression coverage for write and close failure paths. Route lsmod and smartctl --scan through the best-effort probe path so failures no longer abort system collection. Add debug-level handling for best-effort command failures and regression coverage to ensure lsmod and SMART scan errors continue without warnings or output files.
Make collectBestEffortProbe return cancellation and deadline errors instead of swallowing them as best-effort probe failures. Propagate those errors from all probe call sites and preserve ordinary probe failures as debug-only skips. Extend command classification so canceled command executions are not downgraded to non-critical failures, and add tests for canceled/deadline probe paths.
Register pipe close defers immediately after each successful os.Pipe call in the non-interactive encryption recipient test. Remove manual pipe closes from the second pipe failure path, preserving t.Fatalf behavior while avoiding duplicated cleanup handling.
There was a problem hiding this comment.
Actionable comments posted: 4
🧹 Nitpick comments (3)
internal/orchestrator/restore_workflow_ui_extract_test.go (1)
20-22: ⚡ Quick winAssert skip semantics, not just “no error”.
This currently passes even if safe apply is attempted but happens to return nil. Add at least one assertion on a skip signal (warning flag and/or no apply artifact/log) so the regression guard is explicit.
🤖 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 `@internal/orchestrator/restore_workflow_ui_extract_test.go` around lines 20 - 22, The test currently only asserts no error from w.runClusterSafeApply(); add an explicit assertion that a skip occurred by checking a skip indicator or absence of an apply artifact/log: after calling w.runClusterSafeApply(), assert either a boolean field (e.g. w.safeApplySkipped), a returned skip flag (extend runClusterSafeApply to return (error, skipped) if needed), or that apply artifacts/log entries are absent (e.g. len(w.applyArtifacts)==0 or search the test logger for the "skipped" message); if the skip condition is not met call t.Fatalf to fail the test..github/workflows/race.yml (1)
8-34: ⚡ Quick winConsider adding timeout and Go module caching.
The race detector can be slow on large codebases. Consider adding a job timeout and Go module caching to improve reliability and performance.
⚡ Optional improvements for performance and reliability
jobs: race: runs-on: ubuntu-latest + timeout-minutes: 30 steps: - name: Checkout repository uses: actions/checkout@v6 - name: Setup Go uses: actions/setup-go@v6 with: go-version-file: 'go.mod' + cache: true - name: Resolve race toolchain run: |The
cache: trueoption insetup-goautomatically caches Go modules based ongo.sum, andtimeout-minutesprevents runaway tests from consuming excessive CI resources.🤖 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/workflows/race.yml around lines 8 - 34, Add a job timeout and enable Go module caching for the race job: set timeout-minutes on the "race" job (e.g., 30) to avoid runaway runs, and add cache: true under the "Setup Go" step's with block (actions/setup-go@v6) so modules are cached based on go.sum; keep the existing "Resolve race toolchain" and "Run race detector" steps (env GOTOOLCHAIN and go test -race ./...) unchanged.internal/backup/collector_system_test.go (1)
164-169: 💤 Low valueOptional: avoid
(nil, nil)from the Stat mock.
Statreturning a nilos.FileInfoon success is unusual and only works here because the single caller (collectHardwareInfo'sif _, err := c.depStat(...); err == nil) discards the info value. If a future change starts using the FileInfo (e.g.,info.IsDir(),info.Mode()), this mock will silently panic the test. Returning a small fakeos.FileInfo(or reusing an existing helper, e.g.,os.Stat(tempDir)for a realFileInfo) would future-proof the test.♻️ Suggested tweak
- Stat: func(path string) (os.FileInfo, error) { - if strings.HasSuffix(path, "/usr/sbin/smartctl") { - return nil, nil - } - return nil, os.ErrNotExist - }, + Stat: func(path string) (os.FileInfo, error) { + if strings.HasSuffix(path, "/usr/sbin/smartctl") { + // Reuse a real FileInfo so future callers that inspect mode/IsDir don't NPE. + return os.Stat(tempDir) + } + return nil, os.ErrNotExist + },🤖 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 `@internal/backup/collector_system_test.go` around lines 164 - 169, The Stat mock in the test should not return (nil, nil); update the mocked Stat implementation used by collectHardwareInfo (the c.depStat callback) so it returns a valid os.FileInfo when simulating success instead of nil — e.g., obtain a real FileInfo via os.Stat on a temp file/dir or construct a minimal os.FileInfo stub; ensure the mock still returns os.ErrNotExist for other paths to preserve current behavior so future uses of info.IsDir()/info.Mode() won't panic.
🤖 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 @.github/workflows/race.yml:
- Around line 12-19: The workflow uses mutable action tags actions/checkout@v6
and actions/setup-go@v6; replace those version tags with their corresponding
commit SHAs (pin to immutable refs) by looking up the latest recommended commit
SHAs for actions/checkout and actions/setup-go and updating the uses lines to
use the full SHA refs (e.g., actions/checkout@<commit-sha> and
actions/setup-go@<commit-sha>) so the Checkout and Setup Go steps are pinned to
specific commits.
In `@internal/backup/collector.go`:
- Around line 1048-1053: The early return on isContextCancellationError in the
depRunCommand call causes pvesh timeout cancellations to abort callers even when
those calls are marked non‑critical; change the logic so that if the
cancellation is from the pvesh-specific deadline and the call is non‑critical
(use the call spec/flag that indicates critical=false, e.g., the spec or the way
safeCmdOutput/safeCmdOutput callers mark critical), you do not return the error
but treat it as a skipped/non‑fatal result (clear err or set a non‑fatal status
on result) and continue; only return early when the cancellation reflects the
parent context (or when spec.Critical==true). Use symbols depRunCommand,
isContextCancellationError, safeCmdOutput and the spec/critical flag (or
propagate a sentinel pvesh timeout error) to implement this conditional
behavior.
In `@internal/orchestrator/encryption_exported_test.go`:
- Around line 178-179: The deferred inR.Close() and inW.Close() calls ignore
returned errors; replace those plain defers with testing cleanup functions that
check and handle errors (e.g., use t.Cleanup or a similar checked cleanup
callback) to call inR.Close() and inW.Close() and fail or log the test if
Close() returns an error; locate the two calls to inR.Close() and inW.Close() in
encryption_exported_test.go and wrap each in a checked cleanup closure that
captures and handles the Close() error instead of deferring it unhandled.
In `@internal/orchestrator/restore_workflow_ui_extract_test.go`:
- Line 17: Replace the hardcoded absolute path "/missing.tar" used in the test
fixture with a deterministic missing path built from the test's temporary
directory; specifically, set preparedBundle.ArchivePath to a path created via
t.TempDir() (e.g., filepath.Join(t.TempDir(), "missing.tar")) so the three
occurrences that reference preparedBundle and its ArchivePath (lines around the
three fixtures) use a guaranteed-missing, test-local file instead of a fixed
root path.
---
Nitpick comments:
In @.github/workflows/race.yml:
- Around line 8-34: Add a job timeout and enable Go module caching for the race
job: set timeout-minutes on the "race" job (e.g., 30) to avoid runaway runs, and
add cache: true under the "Setup Go" step's with block (actions/setup-go@v6) so
modules are cached based on go.sum; keep the existing "Resolve race toolchain"
and "Run race detector" steps (env GOTOOLCHAIN and go test -race ./...)
unchanged.
In `@internal/backup/collector_system_test.go`:
- Around line 164-169: The Stat mock in the test should not return (nil, nil);
update the mocked Stat implementation used by collectHardwareInfo (the c.depStat
callback) so it returns a valid os.FileInfo when simulating success instead of
nil — e.g., obtain a real FileInfo via os.Stat on a temp file/dir or construct a
minimal os.FileInfo stub; ensure the mock still returns os.ErrNotExist for other
paths to preserve current behavior so future uses of info.IsDir()/info.Mode()
won't panic.
In `@internal/orchestrator/restore_workflow_ui_extract_test.go`:
- Around line 20-22: The test currently only asserts no error from
w.runClusterSafeApply(); add an explicit assertion that a skip occurred by
checking a skip indicator or absence of an apply artifact/log: after calling
w.runClusterSafeApply(), assert either a boolean field (e.g.
w.safeApplySkipped), a returned skip flag (extend runClusterSafeApply to return
(error, skipped) if needed), or that apply artifacts/log entries are absent
(e.g. len(w.applyArtifacts)==0 or search the test logger for the "skipped"
message); if the skip condition is not met call t.Fatalf to fail the test.
🪄 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: 5ac40cd9-a17f-41e3-ac0f-55580c50dcc7
📒 Files selected for processing (15)
.github/workflows/race.ymlinternal/backup/collector.gointernal/backup/collector_system.gointernal/backup/collector_system_test.gointernal/checks/checks.gointernal/checks/checks_test.gointernal/orchestrator/directory_recreation_pbs_config.gointernal/orchestrator/encryption_exported_test.gointernal/orchestrator/restore_decision.gointernal/orchestrator/restore_decision_test.gointernal/orchestrator/restore_workflow_decision_test.gointernal/orchestrator/restore_workflow_ui_extract.gointernal/orchestrator/restore_workflow_ui_extract_test.gointernal/orchestrator/restore_workflow_ui_plan.gointernal/orchestrator/restore_workflow_ui_run.go
🚧 Files skipped from review as they are similar to previous changes (4)
- internal/orchestrator/directory_recreation_pbs_config.go
- internal/orchestrator/restore_workflow_ui_run.go
- internal/orchestrator/restore_workflow_ui_plan.go
- internal/orchestrator/restore_workflow_ui_extract.go
Limit RunGoBackup integration tests to a minimal synthetic backup config so they do not collect and compress runner home directories under GitHub Actions. Also make the TUI simulation harness create a fresh SimulationScreen per app instance, avoiding hangs when multi-step prompts reopen a TUI after the previous screen was finalized.
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
internal/orchestrator/tui_simulation_test.go (1)
48-52: ⚡ Quick winAdd screen.Fini() cleanup to properly finalize the SimulationScreen.
The screen is initialized via
screen.Init()at line 49, but never finalized. tcell's standard pattern requires callingscreen.Fini()afterscreen.Init()to properly clean up screen state and close internal resources (buffers and channels). While SimulationScreen doesn't hold OS terminal resources, following tcell's cleanup convention is the correct approach.Add
screen.Fini()in the test cleanup:Suggested cleanup
t.Cleanup(func() { stopCurrentApp() + appMu.RLock() + app := currentApp + appMu.RUnlock() + if app != nil { + if screen := app.GetScreen(); screen != nil { + screen.Fini() + } + } close(done) injectWG.Wait() newTUIApp = orig })🤖 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 `@internal/orchestrator/tui_simulation_test.go` around lines 48 - 52, The test initializes a SimulationScreen with screen.Init() but never finalizes it; add a call to screen.Fini() to properly clean up the SimulationScreen (best practice is to defer screen.Fini() immediately after a successful call to screen.Init()). Locate the tcell.NewSimulationScreen(...) / screen.Init() block and insert defer screen.Fini() right after Init succeeds so that resources are always released even if the test fails; ensure this uses the same screen variable referenced by screen.SetSize(...).
🤖 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 `@internal/orchestrator/tui_simulation_test.go`:
- Around line 48-52: The test creates a fresh SimulationScreen per app
(SimulationScreen and screen variable) but the key-injection goroutine is
guarded by injectOnce.Do(), so the closure captures the first screen and
subsequent apps (created by promptExistingPathDecisionTUI ->
promptNewPathInputTUI) never receive injected keys; move injectOnce (and its Do
call) into the replacement newTUIApp function so each new SimulationScreen
instance starts its own injection goroutine tied to its local screen, or
alternatively remove injectOnce and start a per-app injector in newTUIApp to
ensure the injection closure references the correct screen for each app.
---
Nitpick comments:
In `@internal/orchestrator/tui_simulation_test.go`:
- Around line 48-52: The test initializes a SimulationScreen with screen.Init()
but never finalizes it; add a call to screen.Fini() to properly clean up the
SimulationScreen (best practice is to defer screen.Fini() immediately after a
successful call to screen.Init()). Locate the tcell.NewSimulationScreen(...) /
screen.Init() block and insert defer screen.Fini() right after Init succeeds so
that resources are always released even if the test fails; ensure this uses the
same screen variable referenced by screen.SetSize(...).
🪄 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: f1ad8d73-a676-42bf-8882-b7aa42fa251b
📒 Files selected for processing (4)
.github/workflows/race.ymlinternal/orchestrator/additional_helpers_test.gointernal/orchestrator/orchestrator_test.gointernal/orchestrator/tui_simulation_test.go
| screen := tcell.NewSimulationScreen("UTF-8") | ||
| if err := screen.Init(); err != nil { | ||
| t.Fatalf("screen.Init: %v", err) | ||
| } | ||
| screen.SetSize(120, 40) |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Description: Search for patterns that might create multiple TUI apps in tests
# Look for test functions that might create multiple app instances
rg -n 'newTUIApp.*newTUIApp|Run.*Run|Stop.*Start' --type go internal/orchestrator/
# Check if any prompt functions internally create new apps
ast-grep --pattern 'func prompt$_($$$) $_ {
$$$
app := $$$
$$$
}'
# Look for comments mentioning multiple apps or screens
rg -n -i 'multiple.*app|reopen.*tui|second.*app' --type go internal/orchestrator/Repository: tis24dev/proxsave
Length of output: 7704
🏁 Script executed:
# First, locate and examine the test file
find . -name "tui_simulation_test.go" -type fRepository: tis24dev/proxsave
Length of output: 108
🏁 Script executed:
# Once found, read the entire test file to understand the test structure
cat -n internal/orchestrator/tui_simulation_test.goRepository: tis24dev/proxsave
Length of output: 13761
🏁 Script executed:
# Check if there are multiple newTUIApp calls within a single test function
rg -n 'newTUIApp|injectOnce|func Test' --type go internal/orchestrator/tui_simulation_test.goRepository: tis24dev/proxsave
Length of output: 1038
🏁 Script executed:
# Look for the main tui_simulation.go implementation to understand injectOnce setup
cat -n internal/orchestrator/tui_simulation.go | head -150Repository: tis24dev/proxsave
Length of output: 133
🏁 Script executed:
# Find the prompt function implementations
rg -n 'func promptExistingPathDecisionTUI|func promptNewPathInputTUI|func promptDecryptSecretTUI' --type go internal/orchestrator/Repository: tis24dev/proxsave
Length of output: 622
🏁 Script executed:
# Check what these prompt functions do - look for tui.NewApp or app creation
rg -n 'tui\.NewApp\|NewApp\(\)' --type go internal/orchestrator/ | head -20Repository: tis24dev/proxsave
Length of output: 43
🏁 Script executed:
# Search for how the app is created in the test helper
rg -n 'screenEnv\|newTUIApp' --type go internal/orchestrator/ | grep -v test | head -30Repository: tis24dev/proxsave
Length of output: 43
🏁 Script executed:
# Read the prompt function implementations
cat -n internal/orchestrator/workflow_ui_tui_decrypt_prompts.goRepository: tis24dev/proxsave
Length of output: 7050
🏁 Script executed:
# Look for tui.NewApp() calls in the codebase
rg -n 'tui\.NewApp\|NewApp\(\)' --type go internal/orchestrator/Repository: tis24dev/proxsave
Length of output: 43
🏁 Script executed:
# Check the test helper functions to understand how apps are created
rg -n 'newTUIApp|newTUIWorkflowUI|newTUIRestoreWorkflowUI' --type go internal/orchestrator/ -A 5 | head -60Repository: tis24dev/proxsave
Length of output: 5145
🏁 Script executed:
# Let me trace through what happens in TestPromptExistingPathDecisionTUI_NewPathContextCanceledWhileRunning
# It calls promptExistingPathDecisionTUI with keys that trigger the "Use different path" option
# Let's verify this flow
cat -n internal/orchestrator/tui_simulation_test.go | sed -n '237,257p'Repository: tis24dev/proxsave
Length of output: 1084
🏁 Script executed:
# Check if there are any tests that might try to type keys into a second app
# by checking test patterns that inject multiple keys and call the same prompt
rg -B5 -A10 'withSimAppSequence.*tcell.KeyRight' --type go internal/orchestrator/tui_simulation_test.goRepository: tis24dev/proxsave
Length of output: 43
🏁 Script executed:
# Let me look at what happens in the test that has multiple keys and nested prompts
cat -n internal/orchestrator/tui_simulation_test.go | sed -n '275,325p'Repository: tis24dev/proxsave
Length of output: 2277
Multiple TUI apps in sequence will only receive key injection for the first instance.
The refactoring creates a fresh SimulationScreen for each app (lines 48-52), but the key injection goroutine runs only once per test due to injectOnce.Do() (line 68). The screen variable captured in the injection closure (line 103) references the first app's screen. When a prompt function internally creates multiple app instances—as promptExistingPathDecisionTUI() does at line 67 of workflow_ui_tui_decrypt_prompts.go by calling promptNewPathInputTUI()—only the first app receives injected keys. The second app would block waiting for input, causing test failures or hangs.
Consider moving injectOnce inside the replacement newTUIApp function so each app gets its own injection routine, or document that only one app should be created per test.
🤖 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 `@internal/orchestrator/tui_simulation_test.go` around lines 48 - 52, The test
creates a fresh SimulationScreen per app (SimulationScreen and screen variable)
but the key-injection goroutine is guarded by injectOnce.Do(), so the closure
captures the first screen and subsequent apps (created by
promptExistingPathDecisionTUI -> promptNewPathInputTUI) never receive injected
keys; move injectOnce (and its Do call) into the replacement newTUIApp function
so each new SimulationScreen instance starts its own injection goroutine tied to
its local screen, or alternatively remove injectOnce and start a per-app
injector in newTUIApp to ensure the injection closure references the correct
screen for each app.
- Pin GitHub Actions workflow steps to immutable commit SHAs - Treat pvesh-specific timeouts as non-fatal for non-critical collection commands - Preserve parent context cancellation behavior for pvesh commands - Use deterministic missing archive paths in restore workflow tests - Check pipe close errors in encryption tests - Return valid FileInfo from smartctl Stat test mock - Add targeted regression coverage
There was a problem hiding this comment.
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
internal/backup/collector.go (1)
857-883:⚠️ Potential issue | 🟡 Minor | ⚡ Quick win
copyRegularFilecan report failure while counting success stats.Because
closeIntoErrruns at defer time (Line 868), a source close failure can make the function return an error afterFilesProcessed/BytesCollectedare incremented (Line 878-879), and without incrementingFilesFailed. This leaves inconsistent collection stats.💡 Suggested fix
func (c *Collector) copyRegularFile(src, dest, description string, info os.FileInfo) (err error) { + var written int64 if err := c.prepareCopyDestination(src, dest); err != nil { c.incFilesFailed() return err } srcFile, err := osOpen(src) if err != nil { c.incFilesFailed() return fmt.Errorf("failed to open %s: %w", src, err) } - defer closeIntoErr(&err, srcFile, "close source file") + defer func() { + prevErr := err + closeIntoErr(&err, srcFile, "close source file") + if err != nil { + if prevErr == nil { + c.incFilesFailed() + } + return + } + c.incFilesProcessed() + c.addBytesCollected(written) + }() - written, err := copyRegularFileContents(srcFile, src, dest) + written, err = copyRegularFileContents(srcFile, src, dest) if err != nil { c.incFilesFailed() return err } c.applyMetadata(dest, info) - c.incFilesProcessed() - c.addBytesCollected(int64(written)) c.logger.Debug("Successfully collected %s: %s", description, src) return nil }🤖 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 `@internal/backup/collector.go` around lines 857 - 883, copyRegularFile currently defers closeIntoErr so a close error can be set after you already call c.incFilesProcessed and c.addBytesCollected, producing inconsistent stats; fix it by ensuring the source file is closed (and any close error applied to the function error) before incrementing success counters: after copyRegularFileContents returns, call closeIntoErr (or explicitly close srcFile and propagate its error into err) and handle that error before calling c.applyMetadata, c.incFilesProcessed, c.addBytesCollected and the logger; adjust use of closeIntoErr, srcFile, c.applyMetadata, c.incFilesProcessed and c.addBytesCollected so success stats are only updated if no close error occurred.
🧹 Nitpick comments (1)
.github/workflows/release.yml (1)
73-77: ⚡ Quick winPin GoReleaser binary version instead of
latest.The action itself is pinned, but
with.version: lateststill introduces release-time drift. Prefer a fixed GoReleaser version for reproducible releases. Current stable version is v2.15.4, or use pessimistic semver like~> v2to get the latest v2.x patch.♻️ Proposed change
- name: Run GoReleaser uses: goreleaser/goreleaser-action@1a80836c5c9d9e5755a25cb59ec6f45a3b5f41a8 # v7 with: - version: latest + version: ~> v2 workdir: ${{ github.workspace }} args: release --clean --config .github/.goreleaser.yml🤖 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/workflows/release.yml around lines 73 - 77, The workflow currently sets the GoReleaser action input "version" to "latest"; change the "with.version" value to a pinned semantic version (for reproducible builds) such as "v2.15.4" or a pessimistic semver like "~> v2" instead of "latest", updating the block that uses goreleaser/goreleaser-action so the release step uses a fixed GoReleaser binary version.
🤖 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.
Outside diff comments:
In `@internal/backup/collector.go`:
- Around line 857-883: copyRegularFile currently defers closeIntoErr so a close
error can be set after you already call c.incFilesProcessed and
c.addBytesCollected, producing inconsistent stats; fix it by ensuring the source
file is closed (and any close error applied to the function error) before
incrementing success counters: after copyRegularFileContents returns, call
closeIntoErr (or explicitly close srcFile and propagate its error into err) and
handle that error before calling c.applyMetadata, c.incFilesProcessed,
c.addBytesCollected and the logger; adjust use of closeIntoErr, srcFile,
c.applyMetadata, c.incFilesProcessed and c.addBytesCollected so success stats
are only updated if no close error occurred.
---
Nitpick comments:
In @.github/workflows/release.yml:
- Around line 73-77: The workflow currently sets the GoReleaser action input
"version" to "latest"; change the "with.version" value to a pinned semantic
version (for reproducible builds) such as "v2.15.4" or a pessimistic semver like
"~> v2" instead of "latest", updating the block that uses
goreleaser/goreleaser-action so the release step uses a fixed GoReleaser binary
version.
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro
Run ID: 872eb089-6724-44fa-9341-55da4749dd11
📒 Files selected for processing (13)
.github/workflows/autotag.yml.github/workflows/codecov.yml.github/workflows/dependency-review.yml.github/workflows/race.yml.github/workflows/release.yml.github/workflows/security-ultimate.yml.github/workflows/sync-dev.ymlinternal/backup/collector.gointernal/backup/collector_pbs_datastore.gointernal/backup/collector_system_test.gointernal/backup/collector_test.gointernal/orchestrator/encryption_exported_test.gointernal/orchestrator/restore_workflow_ui_extract_test.go
✅ Files skipped from review due to trivial changes (2)
- .github/workflows/autotag.yml
- .github/workflows/sync-dev.yml
🚧 Files skipped from review as they are similar to previous changes (5)
- .github/workflows/codecov.yml
- .github/workflows/race.yml
- internal/orchestrator/encryption_exported_test.go
- internal/orchestrator/restore_workflow_ui_extract_test.go
- internal/backup/collector_system_test.go
Bump the module Go version and runtime minimum to 1.25.10 so builds use the patched standard library for GO-2026-4971 and GO-2026-4918.
Summary by CodeRabbit
New Features
Bug Fixes & Improvements
Chores