Skip to content

fix(no-ticket): stop credential helpers from overwriting config files that do not parse - #540

Open
cloudsmith-iduffy wants to merge 2 commits into
masterfrom
iduffy/fix-config-merge-data-loss
Open

cloudsmith-iduffy wants to merge 2 commits into
masterfrom
iduffy/fix-config-merge-data-loss

Conversation

@cloudsmith-iduffy

Copy link
Copy Markdown
Collaborator

Description

credential-helper install and uninstall edit the Docker config.json and the Cargo config.toml through merge_config_file(). That function had two problems.

A file with a syntax error got overwritten. If the file did not parse, the function used {} and wrote the result over the file. The .bak file was replaced on the next write, so the original content was lost for good. One trailing comma in config.json was enough:

Before:

$ cat ~/.docker/config.json
{
	"auths": { "ghcr.io": { "auth": "..." } },
	"credsStore": "desktop",
}
$ cloudsmith credential-helper install docker --no-discover   # exit 0
$ cat ~/.docker/config.json
{
  "credHelpers": {
    "docker.cloudsmith.io": "cloudsmith"
  }
}

auths and credsStore are gone. For Cargo, uninstall on a config.toml with an unclosed array emptied the file to 0 bytes. It also reported that it removed entries, and it exited with code 0.

After:

$ cloudsmith credential-helper install docker --no-discover
Error: Cannot parse /home/me/.docker/config.json as JSON: Illegal trailing comma before end of object: line 7 column 25 (char 87). Correct or remove the file. Then run the command again.
$ echo $?
1

The file stays as it was, and the command makes no .bak. A file that is not UTF-8, or whose top-level value is not an object (for example a JSON list), gives the same kind of error. A missing file, an empty file, or a file that has only whitespace still counts as {}.

A valid file was rewritten when nothing changed. The function compared the new serialised text with the text on disk. Docker writes config.json with tabs, so the text never matched. A no-op uninstall docker rewrote the file, made a .bak, and printed "removed credHelpers entries". Now the function makes a deep copy of the data before mutate and compares the data, not the text. If the data does not change, it does not touch the file and returns False. This also applies to a missing file: a no-op merge no longer creates it.

Changes:

  • core/cache_utils.py: new ConfigFileParseError(ValueError) and a _parse_config_text() helper. The function now detects changes on the data, has stricter type hints, and strips a UTF-8 BOM before it parses TOML (Cargo accepts a BOM, tomlkit does not).
  • cli/commands/credential_helper/manage.py: install and uninstall turn ConfigFileParseError into a ClickException, so the user sees one clear error line and exit code 1.

Type of Change

  • Bug fix
  • New feature
  • Breaking change
  • Documentation update
  • Refactoring
  • Other (please describe)

Additional Notes

What changes for users and scripts: if config.json or config.toml does not parse, credential-helper install and uninstall now exit with code 1 instead of 0. They print an error that names the file, and they do not write to it. Previously, the command silently replaced the file. A script that ran install against a broken config now fails, so the user must correct or remove the file. Also, a no-op run no longer rewrites a correctly formatted file, so the file keeps its formatting and the command no longer writes a .bak.

Known gap, left for a follow-up: both installers write or remove the launcher before they merge the config. When the parse fails, install has already written the launcher, and uninstall has already removed it. The config file stays safe, and a second run after the fix finishes the job. Moving the merge before the launcher step means changes to credential_helpers/docker/installer.py, which #522 also changes, so I left it out of this PR.

This PR overlaps with #522: with data-based change detection, uninstall docker no longer creates config.json when the file is missing. The config_exists guards in #522 and in the Cargo installer are still correct, so they can stay.

How to check it:

CLOUDSMITH_NO_KEYRING=1 CLOUDSMITH_PROFILE= CLOUDSMITH_NO_TELEMETRY=1 pytest cloudsmith_cli -m "not integration" -q

New tests: an unparsable JSON file, a top-level JSON list, an unparsable TOML file, a file that is not UTF-8, a TOML file with a BOM, an empty or whitespace-only file, a tab-indented no-op, an empty-file no-op, a missing-file no-op, and a CLI test for install and uninstall. The CLI test checks exit code 1, the file path in the message, an unchanged file, and no .bak.

🤖 Generated with Claude Code

… that do not parse

merge_config_file read a config.json or config.toml with a syntax error
as an empty object and wrote the result over the file. The next write
replaced the .bak, so the original content was lost. Now it raises
ConfigFileParseError, which names the file, and writes nothing. The
install and uninstall commands show this error and exit with code 1.

Change detection now compares the data before and after mutate, not the
serialised text. A file that is valid but has a different formatting is
no longer rewritten when mutate makes no change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Copilot AI balanced review requested due to automatic review settings October 24, 2026 17:25
@cloudsmith-iduffy cloudsmith-iduffy added the breaking change Changes behaviour that scripts or users can depend on label Oct 24, 2026

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🟢 Approval recommended

Static review identified no blocking issues in the focused parsing safeguards, change detection, CLI handling, or regression coverage; automated tests were not run.

No open issues 🎉

What changed in this PR

Protects Docker and Cargo configuration files from being overwritten when parsing fails, and avoids unnecessary rewrites when credential-helper operations make no data changes.

Changes:

  • Rejects malformed, non-UTF-8, and non-object configurations while supporting TOML files with a UTF-8 BOM.
  • Compares configuration data before and after mutation to preserve unchanged files and avoid unnecessary backups.
  • Adds clear CLI errors, regression tests, and changelog entries.
File Description
cloudsmith_cli/​core/​tests/​test_cache_utils.py Tests parsing safeguards, BOM handling, and no-op preservation.
cloudsmith_cli/​core/​cache_utils.py Adds strict parsing and data-based change detection.
cloudsmith_cli/​cli/​tests/​commands/​test_credential_helper_install.py Tests install/uninstall errors and malformed-file preservation.
cloudsmith_cli/​cli/​commands/​credential_helper/​manage.py Converts configuration parsing failures into CLI errors.
CHANGELOG.md Documents file-preservation and no-op behavior fixes.

Review effort: Balanced

@cloudsmith-iduffy
cloudsmith-iduffy marked this pull request as ready for review October 24, 2026 17:42
@cloudsmith-iduffy
cloudsmith-iduffy requested a review from a team as a code owner October 24, 2026 17:42
@cloudsmith-iduffy
cloudsmith-iduffy enabled auto-merge (squash) October 24, 2026 17:42

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

breaking change Changes behaviour that scripts or users can depend on

Development

Successfully merging this pull request may close these issues.

2 participants