Conversation
tis24dev
commented
Feb 3, 2026
- Improve env config parsing and upgrade logic
- Report config upgrade warnings
Refactor config parsing and upgrade flow to better handle comments, casing and multiline values. - setEnvValue: skip commented lines, preserve indentation and inline comments when replacing values. - parseEnvFile/parseEnvValues: ignore comment-only lines inside multi-line block values; return comment text and a caseMap (UPPER->original) for case-insensitive matching; add comment storage to envValue. - splitKeyValueRaw: return parsed inline comment, handle legacy "export " prefixes, and more robustly extract quoted/unquoted values and trailing comments. - computeConfigUpgrade: use case-insensitive lookups via caseMap, track processed user keys, preserve user key casing for extras, count preserved values correctly, and detect no-op upgrades by comparing newContent with originalContent. - renderEnvValue: include preserved inline comments when rendering single-line values. - Added import of utils.IsComment where needed. Also updated internal/orchestrator/.backup.lock (pid/time) as part of the changeset.
Add non-fatal warnings support to the config upgrade flow. Introduce a Warnings []string field on UpgradeResult and propagate warning messages from parseEnvValues through computeConfigUpgrade. Detect and warn about duplicate template keys that differ only by case, case-collision between user keys and template keys, and ignored non-KEY=VALUE lines; preserve behavior for multi-line values. Surface these warnings in the CLI (cmd/proxsave/main.go and cmd/proxsave/upgrade.go) so they are printed during plan/upgrade operations. Update parseEnvValues signature to return case conflict info and warnings, and add tests verifying case-collision and ignored-line warnings.
There was a problem hiding this comment.
Pull request overview
This PR syncs changes from the dev branch to main, focusing on improving environment configuration parsing and upgrade logic with enhanced warning reporting.
Changes:
- Added case-insensitive key matching with collision detection and warnings for duplicate keys that differ only in case
- Introduced inline comment preservation during config upgrades and support for legacy "export KEY=VALUE" syntax
- Enhanced multi-line block value parsing to skip comment lines, consistent with runtime parsing
- Added warning system to report non-fatal parsing issues (ignored lines, case collisions, etc.) to users during config upgrades
Reviewed changes
Copilot reviewed 6 out of 7 changed files in this pull request and generated 10 comments.
Show a summary per file
| File | Description |
|---|---|
| internal/orchestrator/.backup.lock | Updated runtime lock file with new PID and timestamp (should not be in version control) |
| internal/config/upgrade_test.go | Added tests for case collision warnings and ignored line warnings |
| internal/config/upgrade.go | Implemented case-insensitive key matching, comment preservation, export statement handling, and warning generation |
| internal/config/config.go | Added comment filtering in multi-line block value parsing |
| cmd/proxsave/upgrade.go | Added warning output display in upgrade footer |
| cmd/proxsave/main.go | Added warning output for both plan and execute upgrade modes |
| cmd/proxsave/config_helpers.go | Enhanced setEnvValue to preserve comments and handle key matching without prefix checking |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
cmd/proxsave/config_helpers.go
Outdated
| if idx := strings.Index(line, "#"); idx >= 0 { | ||
| // Verify # is not part of the key or value? | ||
| // Assuming standard comment | ||
| commentPart := line[idx:] | ||
| // Ensure it's not inside quotes? The original logic didn't check quotes carefully but let's be safe(r). | ||
| // For setEnvValue we are replacing the value, so we just want to keep the comment at the end. | ||
| comment = commentPart | ||
|
|
||
| // Find spacing before comment | ||
| beforeComment := line[:idx] |
There was a problem hiding this comment.
The comment extraction logic at line 76 doesn't handle the case where the '#' character might be inside quoted values. For example, if value is "some#value", this code will incorrectly treat "#value" as a comment. Consider using similar logic to splitKeyValueRaw (lines 405-423 in upgrade.go) which properly handles quotes when extracting comments.
| if idx := strings.Index(line, "#"); idx >= 0 { | |
| // Verify # is not part of the key or value? | |
| // Assuming standard comment | |
| commentPart := line[idx:] | |
| // Ensure it's not inside quotes? The original logic didn't check quotes carefully but let's be safe(r). | |
| // For setEnvValue we are replacing the value, so we just want to keep the comment at the end. | |
| comment = commentPart | |
| // Find spacing before comment | |
| beforeComment := line[:idx] | |
| // Find a comment start '#' that is not inside quotes | |
| commentIdx := -1 | |
| inSingle := false | |
| inDouble := false | |
| escaped := false | |
| for i, r := range line { | |
| if escaped { | |
| escaped = false | |
| continue | |
| } | |
| switch r { | |
| case '\\': | |
| escaped = true | |
| case '\'': | |
| if !inDouble { | |
| inSingle = !inSingle | |
| } | |
| case '"': | |
| if !inSingle { | |
| inDouble = !inDouble | |
| } | |
| case '#': | |
| if !inSingle && !inDouble { | |
| commentIdx = i | |
| // First '#' outside quotes is treated as the start of a comment | |
| break | |
| } | |
| } | |
| if commentIdx != -1 { | |
| break | |
| } | |
| } | |
| if commentIdx >= 0 { | |
| commentPart := line[commentIdx:] | |
| // For setEnvValue we are replacing the value, so we just want to keep the comment at the end. | |
| comment = commentPart | |
| // Find spacing before comment | |
| beforeComment := line[:commentIdx] |
internal/config/upgrade.go
Outdated
| // Handle legacy "export KEY=VALUE" lines | ||
| if strings.HasPrefix(key, "export ") { | ||
| key = strings.TrimSpace(strings.TrimPrefix(key, "export ")) | ||
| } | ||
| // Also handle tab separation just in case "export\tKEY" | ||
| if strings.HasPrefix(key, "export\t") { | ||
| key = strings.TrimSpace(strings.TrimPrefix(key, "export\t")) | ||
| } |
There was a problem hiding this comment.
The export handling logic only checks for "export " (with space) and "export\t" (with tab) prefixes. However, it doesn't handle the case where there might be multiple spaces or tabs after "export", such as "export KEY=VALUE" or "export\t\tKEY=VALUE". Consider using a more robust approach that handles any whitespace after "export", for example: if strings.HasPrefix(key, "export") { key = strings.TrimSpace(strings.TrimPrefix(key, "export")) }
| // Handle legacy "export KEY=VALUE" lines | |
| if strings.HasPrefix(key, "export ") { | |
| key = strings.TrimSpace(strings.TrimPrefix(key, "export ")) | |
| } | |
| // Also handle tab separation just in case "export\tKEY" | |
| if strings.HasPrefix(key, "export\t") { | |
| key = strings.TrimSpace(strings.TrimPrefix(key, "export\t")) | |
| } | |
| // Handle legacy "export KEY=VALUE" lines with arbitrary whitespace after "export" | |
| if strings.HasPrefix(key, "export") { | |
| key = strings.TrimSpace(strings.TrimPrefix(key, "export")) | |
| } |
| line := fmt.Sprintf("%s=%s", key, value.rawValue) | ||
| if value.comment != "" { | ||
| line += " " + value.comment | ||
| } | ||
| return []string{line} |
There was a problem hiding this comment.
The new comment preservation functionality (storing and re-rendering inline comments) is not covered by tests. Consider adding test cases to verify that inline comments are correctly preserved during config upgrades, both for quoted and unquoted values.
| pid=99930 | ||
| host=pve | ||
| time=2026-02-03T00:07:31+01:00 | ||
| time=2026-02-03T19:42:03+01:00 |
There was a problem hiding this comment.
This backup lock file appears to contain PID and timestamp information that is automatically generated. It should not be included in version control as it represents runtime state and will create merge conflicts. Consider adding this file to .gitignore.
internal/config/upgrade.go
Outdated
| // Handle legacy "export KEY=VALUE" lines | ||
| if strings.HasPrefix(key, "export ") { | ||
| key = strings.TrimSpace(strings.TrimPrefix(key, "export ")) | ||
| } | ||
| // Also handle tab separation just in case "export\tKEY" | ||
| if strings.HasPrefix(key, "export\t") { | ||
| key = strings.TrimSpace(strings.TrimPrefix(key, "export\t")) | ||
| } |
There was a problem hiding this comment.
The new export handling functionality (lines 390-397) added to splitKeyValueRaw is not covered by tests. Consider adding test cases to verify that lines with "export KEY=VALUE" are correctly parsed, including edge cases like multiple spaces after export, tabs, and combinations thereof.
| // BUT checking "nothing changed" is harder now because we might have renamed keys. | ||
| // If we renamed a key, the content CHANGED. | ||
| // So we should compare normalized content? | ||
| // Or just assume if we parsed everything and re-rendered, and it matches original string... |
There was a problem hiding this comment.
These commented lines look like leftover notes from development explaining why the early return logic was modified. Since this logic has been replaced (lines 312-317), these comments should be removed to keep the code clean.
| // BUT checking "nothing changed" is harder now because we might have renamed keys. | |
| // If we renamed a key, the content CHANGED. | |
| // So we should compare normalized content? | |
| // Or just assume if we parsed everything and re-rendered, and it matches original string... |
internal/config/upgrade.go
Outdated
|
|
||
| upperKey := strings.ToUpper(key) | ||
| if templateKey, ok := templateKeyByUpper[upperKey]; ok && templateKey != key { | ||
| if caseConflicts == nil || !caseConflicts[upperKey] { |
There was a problem hiding this comment.
The condition caseConflicts == nil will never be true because caseConflicts is always initialized as a non-nil map in parseEnvValues at line 331. This check should just be !caseConflicts[upperKey] to verify that this particular upper-cased key doesn't have a conflict.
| if caseConflicts == nil || !caseConflicts[upperKey] { | |
| if !caseConflicts[upperKey] { |
| return nil, nil, fmt.Errorf("unterminated multi-line value for %s starting at line %d", key, i+1) | ||
| return nil, nil, nil, nil, nil, fmt.Errorf("unterminated multi-line value for %s starting at line %d", key, i+1) | ||
| } | ||
| blockLines = append(blockLines, lines[i+1:blockEnd]...) |
There was a problem hiding this comment.
The block value parsing here collects all lines as-is (line 363), including comment lines within the block. This is inconsistent with the behavior in config.go (lines 1231-1233) which filters out comments from block values. This inconsistency could lead to comments being preserved during upgrades but ignored during actual config parsing, causing confusion. Consider filtering out comments here as well for consistency.
| blockLines = append(blockLines, lines[i+1:blockEnd]...) | |
| for j := i + 1; j < blockEnd; j++ { | |
| blkLine := lines[j] | |
| if utils.IsComment(strings.TrimSpace(blkLine)) { | |
| continue | |
| } | |
| blockLines = append(blockLines, blkLine) | |
| } |
internal/config/upgrade.go
Outdated
|
|
||
| caseMap[upperKey] = key | ||
|
|
||
| if blockValueKeys[key] && trimmed == fmt.Sprintf("%s=\"", key) { |
There was a problem hiding this comment.
The check for blockValueKeys uses the user's key casing directly, but blockValueKeys map contains uppercase keys (CUSTOM_BACKUP_PATHS, BACKUP_BLACKLIST). If a user has a lowercase or mixed-case version of these keys, the block value won't be detected and will be parsed as a regular line instead. This should use a case-insensitive check, similar to how the template parsing handles it at line 212.
| if blockValueKeys[key] && trimmed == fmt.Sprintf("%s=\"", key) { | |
| if blockValueKeys[upperKey] && trimmed == fmt.Sprintf("%s=\"", key) { |
Introduce utils string helpers (FindInlineCommentIndex, FindClosingQuoteIndex, SetEnvValue) to centralize handling of inline comments, escaped hashes and quoted values. Replace local setEnvValue implementations to call utils.SetEnvValue and update SplitKeyValue/splitKeyValueRaw to use the new helpers for robust parsing. Fix case-insensitive handling of block value keys in config upgrade logic (use upperKey). Add unit tests across packages to cover escaped hashes, preserved comments, quoted values, and SetEnvValue behavior. Minor reformatting/import adjustments in the install wizard to integrate the new utils usage.
Replace manual HasPrefix checks with a Fields-based parse to handle legacy "export KEY=VALUE" lines that use arbitrary whitespace (spaces or tabs). This prevents accidentally stripping keys like "exporter" while still recognizing true "export" prefixes. Added tests to ensure export-with-whitespace is accepted and that keys named "exporter" are not treated as exports.
Add TestUpgradeConfigPreservesInlineComments which verifies UpgradeConfigFile preserves inline comments when adding missing keys. The test seeds a temporary config with inline and quoted '#' comments, runs UpgradeConfigFile, asserts result.Changed is true, and checks the upgraded file still contains the original inline comments for KEY1 and KEY2.
Extend TestPlanUpgradeConfigHandlesExportWhitespace in internal/config/upgrade_test.go to include an export line with tab characters (export\t\tKEY1=custom-tabbed). This ensures the upgrade parsing handles export lines with tabs as well as spaces.
Remove the redundant `caseConflicts == nil` check when warning about keys that differ only by case. Reading a nil map in Go is safe, so using `!caseConflicts[upperKey]` preserves behavior while simplifying the condition and reducing clutter in internal/config/upgrade.go.
Adds TestUpgradeConfigPreservesBlockComments to internal/config/upgrade_test.go. The new test seeds a user config containing a multi-line variable with inline comment lines, runs UpgradeConfigFile, asserts that an upgrade occurs (result.Changed == true) and verifies the block-style comment lines and content are preserved in the upgraded file.
parseEnvFile: normalize parsed environment variable names to uppercase and support lines beginning with "export". Lookups against blockValueKeys and multiValueKeys now use the uppercased key, and stored map keys are uppercased so legacy/lowercase env files are handled consistently. Added tests verifying lowercase keys are normalized (including multi-line block values and multi-value accumulation) and that lines prefixed with various spacing for "export" are parsed correctly.
Codecov Report❌ Patch coverage is 📢 Thoughts on this report? Let us know! |