Skip to content

Commit 7a93ddd

Browse files
fix: remove duplicated TestChangeStr class in test_diff.py (#16)
# Conflicts: # README.md
2 parents 5d252fe + b005b33 commit 7a93ddd

9 files changed

Lines changed: 207 additions & 78 deletions

File tree

.github/workflows/ci.yml

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,18 +6,23 @@ on:
66
pull_request:
77
branches: [main]
88

9+
permissions:
10+
contents: read
11+
912
jobs:
1013
test:
1114
runs-on: ubuntu-latest
1215
strategy:
1316
matrix:
14-
python-version: ["3.10", "3.11", "3.12"]
17+
python-version: ["3.10", "3.11", "3.12", "3.13"]
1518

1619
steps:
17-
- uses: actions/checkout@v6
20+
- uses: actions/checkout@v4
21+
with:
22+
persist-credentials: false
1823

1924
- name: Set up Python ${{ matrix.python-version }}
20-
uses: actions/setup-python@v6
25+
uses: actions/setup-python@v5
2126
with:
2227
python-version: ${{ matrix.python-version }}
2328

@@ -26,8 +31,7 @@ jobs:
2631
pip install -e ".[dev]"
2732
2833
- name: Lint with ruff
29-
run: pip install ruff && ruff check src/ --target-version py310
34+
run: ruff check src/
3035
- name: Run tests
3136
run: |
3237
python -m pytest tests/ -v --cov=src --cov-report=term-missing
33-

.github/workflows/publish.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,10 +21,10 @@ jobs:
2121
id-token: write
2222

2323
steps:
24-
- uses: actions/checkout@v6
24+
- uses: actions/checkout@v4
2525

2626
- name: Set up Python 3.11
27-
uses: actions/setup-python@v6
27+
uses: actions/setup-python@v5
2828
with:
2929
python-version: "3.11"
3030

CHANGELOG.md

Lines changed: 39 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,44 @@
22

33
All notable changes to ConfigDrift CLI will be documented in this file.
44

5+
## [Unreleased]
6+
7+
### Added
8+
9+
- CLI test suite with comprehensive coverage for Change.\_\_str\_\_() and loader fallback paths
10+
- npm wrapper (`package.json` + `cli.js`) for npm publishing
11+
- GitHub Actions: npm publish workflow (release or manual dispatch)
12+
- GitHub Actions: GitHub Pages deployment workflow
13+
- `CONTRIBUTING.md` with development setup and PR guidelines
14+
- `SECURITY.md` with security policy
15+
- Homebrew and Scoop install methods
16+
- Directory listing badges: Open Source Alternative, LibHunt, Awesome Python
17+
- npm keywords optimized for discoverability (15 terms)
18+
- `revenueholdings-license` gating on all CLI commands
19+
- Beta badge and star CTA in README header
20+
21+
### Changed
22+
23+
- CI test matrix expanded to include Python 3.13
24+
- CI security hardened: `persist-credentials: false`, restricted permissions
25+
- Documentation branding updated from DevForge to Revenue Holdings
26+
- README expanded with CI/CD examples and alternatives comparison
27+
- README tool count updated (8 → 11)
28+
- npm install placement and formatting fixed in README
29+
- `project.urls` metadata added to `pyproject.toml`
30+
31+
### Fixed
32+
33+
- CI lint workflow: removed redundant ruff install and deprecated `--target-version` flag
34+
- GitHub Actions version mismatches in CI and publish workflows
35+
- UTF-8 encoding (mojibake) in file output
36+
- Ruff lint issues: `datetime.UTC`, `X | None` syntax, `E501`, `B904`, `F821`
37+
- Missing `ruff` dev dependency in `pyproject.toml`
38+
- Broken PyPI badges replaced with GitHub release badge (not yet on PyPI)
39+
- `revenueholdings-license` import made optional (fixes CI failures on open-source PRs)
40+
- Dependencies bumped via Dependabot (checkout@v6, setup-node@v6, setup-python@v6)
41+
- README install section formatting (broken Homebrew/Scoop code blocks)
42+
543
## [0.1.0] — 2026-05-14
644

745
### Added
@@ -13,6 +51,6 @@ All notable changes to ConfigDrift CLI will be documented in this file.
1351
- `configdrift init` — generate `.configdrift.yaml` scaffolding
1452
- Three output formats: rich `table`, machine-readable `json`, `silent` (exit code only)
1553
- Severity-level drift classification: Info, Warning, Breaking
16-
- Breaking-change heuristics for critical keys (`database*`, `auth*`, `api_key*`, `secret*`, `password*`, `token*`, `endpoint*`)
54+
- Breaking-change heuristics for critical keys
1755
- CI/CD integration with non-zero exit on breaking drift
1856
- Python 3.10+ support

README.md

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,17 @@
11
# ConfigDrift
22

33
[![GitHub stars](https://img.shields.io/github/stars/Coding-Dev-Tools/configdrift?style=social)](https://github.com/Coding-Dev-Tools/configdrift/stargazers)
4-
[![Awesome DevOps](https://img.shields.io/badge/Awesome_DevOps-Submitted-grey?logo=github)](https://github.com/wmariuss/awesome-devops)<!-- PR #433 -->
54

65
Keep configurations consistent across all environments, automatically. ConfigDrift compares configs, flags drift, and reports compliance violations before they cause incidents.
76

7+
> **Star this repo** if you manage multi-environment configs — it helps other devs find ConfigDrift!
8+
89
[![GitHub release](https://img.shields.io/github/v/release/Coding-Dev-Tools/configdrift?label=latest)](https://github.com/Coding-Dev-Tools/configdrift/releases)
910
![Python](https://img.shields.io/badge/python-3.10%2B-blue)
1011
[![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://github.com/Coding-Dev-Tools/configdrift/blob/main/LICENSE)
1112
[![Open Source Alternative](https://img.shields.io/badge/Open_Source_Alternative-%E2%87%92-blue?logo=opensourceinitiative)](https://www.opensourcealternative.to/project/configdrift)
1213
[![LibHunt](https://img.shields.io/badge/LibHunt-%E2%87%92-blue?logo=codeigniter)](https://www.libhunt.com/r/Coding-Dev-Tools/configdrift)
13-
[![Awesome Python](https://img.shields.io/badge/Awesome_Python-%E2%87%92-blue?logo=python)](https://github.com/uhub/awesome-python)
1414

15-
**Why ConfigDrift?** Environments should behave consistently. When dev, staging, and prod configs diverge, deployments break silently. ConfigDrift compares configurations across environments, highlights drifting keys, deprecated values, and missing settings — before they cause incidents. Supports YAML, JSON, TOML, and .env — and it runs in CI so drift never ships.
1615

1716
## Installation
1817

@@ -162,7 +161,7 @@ ConfigDrift is one of 11 tools in the DevForge suite. One license covers all CLI
162161
---
163162

164163
<p align="center">
165-
<sub>Part of <a href="https://coding-dev-tools.github.io/devforge.dev/">DevForge</a> — CLI tools built by autonomous AI.</sub>
164+
<sub>Part of <a href="https://coding-dev-tools.github.io/revenueholdings.dev/">Revenue Holdings</a> — CLI tools built by autonomous AI.</sub>
166165
</p>
167166

168167
## License

pyproject.toml

Lines changed: 67 additions & 64 deletions
Original file line numberDiff line numberDiff line change
@@ -1,65 +1,68 @@
1-
[build-system]
2-
requires = ["setuptools>=68.0", "wheel"]
3-
build-backend = "setuptools.build_meta"
4-
5-
[project]
6-
name = "configdrift"
7-
version = "0.1.0"
8-
description = "CLI tool that detects and fixes configuration file drift across environments (dev/staging/prod). Supports YAML, JSON, TOML, and .env formats."
9-
readme = "README.md"
10-
requires-python = ">=3.10"
11-
license = "MIT"
12-
authors = [{name = "Revenue Holdings"}]
13-
keywords = ["config", "drift", "diff", "env", "devops", "cli"]
14-
classifiers = [
15-
"Development Status :: 4 - Beta",
16-
"Intended Audience :: Developers",
17-
"Topic :: Software Development :: Quality Assurance",
18-
"Topic :: System :: Systems Administration",
19-
"Programming Language :: Python :: 3",
20-
"Programming Language :: Python :: 3.10",
21-
"Programming Language :: Python :: 3.11",
22-
"Programming Language :: Python :: 3.12",
23-
]
24-
dependencies = [
25-
"typer>=0.9.0",
26-
"rich>=13.0.0",
27-
"pyyaml>=6.0",
28-
"tomli>=2.0.0; python_version < '3.11'",
29-
"tomli-w>=1.0.0",
30-
]
31-
32-
[project.optional-dependencies]
33-
license = ["revenueholdings-license>=0.1.0"]
34-
dev = [
35-
"pytest>=7.0.0",
36-
"pytest-cov>=4.0.0",
37-
"ruff>=0.4.0",
38-
]
39-
toml = ["tomli>=2.0.0", "tomli-w>=1.0.0"]
40-
41-
[project.urls]
42-
Homepage = "https://github.com/Coding-Dev-Tools/configdrift"
43-
Repository = "https://github.com/Coding-Dev-Tools/configdrift"
44-
"Issue Tracker" = "https://github.com/Coding-Dev-Tools/configdrift/issues"
45-
46-
[project.scripts]
47-
configdrift = "configdrift.cli:app"
48-
49-
[tool.setuptools.packages.find]
50-
where = ["src"]
51-
52-
[tool.pytest.ini_options]
53-
testpaths = ["tests"]
54-
addopts = "-v --tb=short"
55-
56-
[tool.ruff]
57-
target-version = "py310"
58-
line-length = 120
59-
60-
[tool.ruff.lint]
61-
select = ["E", "F", "W", "I", "UP", "B", "SIM"]
62-
ignore = ["E501"]
63-
64-
[tool.ruff.lint.isort]
1+
[build-system]
2+
requires = ["setuptools>=68.0", "wheel"]
3+
build-backend = "setuptools.build_meta"
4+
5+
[project]
6+
name = "configdrift"
7+
version = "0.1.0"
8+
description = "CLI tool that detects and fixes configuration file drift across environments (dev/staging/prod). Supports YAML, JSON, TOML, and .env formats."
9+
readme = "README.md"
10+
requires-python = ">=3.10"
11+
license = "MIT"
12+
authors = [{name = "Revenue Holdings"}]
13+
keywords = ["config", "drift", "diff", "env", "devops", "cli"]
14+
classifiers = [
15+
"Development Status :: 4 - Beta",
16+
"Intended Audience :: Developers",
17+
"Topic :: Software Development :: Quality Assurance",
18+
"Topic :: System :: Systems Administration",
19+
"Programming Language :: Python :: 3",
20+
"Programming Language :: Python :: 3.10",
21+
"Programming Language :: Python :: 3.11",
22+
"Programming Language :: Python :: 3.12",
23+
"Programming Language :: Python :: 3.13",
24+
]
25+
dependencies = [
26+
"typer>=0.9.0",
27+
"rich>=13.0.0",
28+
"pyyaml>=6.0",
29+
"tomli>=2.0.0; python_version < '3.11'",
30+
"tomli-w>=1.0.0",
31+
]
32+
33+
[project.optional-dependencies]
34+
license = ["revenueholdings-license>=0.1.0"]
35+
dev = [
36+
"pytest>=7.0.0",
37+
"pytest-cov>=4.0.0",
38+
"ruff>=0.4.0",
39+
]
40+
toml = ["tomli>=2.0.0", "tomli-w>=1.0.0"]
41+
42+
[project.urls]
43+
Homepage = "https://github.com/Coding-Dev-Tools/configdrift"
44+
Documentation = "https://coding-dev-tools.github.io/configdrift"
45+
Repository = "https://github.com/Coding-Dev-Tools/configdrift"
46+
Issues = "https://github.com/Coding-Dev-Tools/configdrift/issues"
47+
Changelog = "https://github.com/Coding-Dev-Tools/configdrift/releases"
48+
49+
[project.scripts]
50+
configdrift = "configdrift.cli:app"
51+
52+
[tool.setuptools.packages.find]
53+
where = ["src"]
54+
55+
[tool.pytest.ini_options]
56+
testpaths = ["tests"]
57+
addopts = "-v --tb=short"
58+
59+
[tool.ruff]
60+
target-version = "py310"
61+
line-length = 120
62+
63+
[tool.ruff.lint]
64+
select = ["E", "F", "W", "I", "UP", "B", "SIM"]
65+
ignore = ["E501"]
66+
67+
[tool.ruff.lint.isort]
6568
known-first-party = ["*"]

src/configdrift/cli.py

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -70,8 +70,6 @@ def check(
7070
raise typer.Exit(code=1)
7171

7272
env_configs: dict[str, dict[str, Any]] = {}
73-
env_labels = []
74-
7573
env_labels = [baseline, target] if len(files) == 2 else [f"file_{i + 1}" for i in range(len(files))]
7674

7775
for label, filepath in zip(env_labels, files, strict=False):

tests/test_cli.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,21 @@ def test_check_error_file_not_found(self):
6666
assert result.exit_code == 1
6767
assert "Error loading" in result.stdout
6868

69+
def test_check_three_files(self):
70+
"""Compare three config files with auto-labeled environments."""
71+
with tempfile.TemporaryDirectory() as tmpdir:
72+
a = Path(tmpdir) / "a.yaml"
73+
b = Path(tmpdir) / "b.yaml"
74+
c = Path(tmpdir) / "c.yaml"
75+
a.write_text(yaml.dump({"host": "localhost", "port": 8080}))
76+
b.write_text(yaml.dump({"host": "staging.example.com", "port": 8080}))
77+
c.write_text(yaml.dump({"host": "prod.example.com", "port": 443}))
78+
79+
result = runner.invoke(app, ["check", str(a), str(b), str(c)])
80+
assert result.exit_code == 0
81+
assert "file_1" in result.stdout or "Config Drift" in result.stdout
82+
assert "prod.example.com" in result.stdout
83+
6984
def test_check_less_than_two_files(self):
7085
"""Only one file should error."""
7186
with tempfile.TemporaryDirectory() as tmpdir:

tests/test_diff.py

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,24 @@ def test_three_envs(self):
105105
assert results["prod"].count == 1
106106

107107

108+
class TestChangeStr:
109+
"""Tests for Change.__str__()."""
110+
111+
def test_str_added(self):
112+
c = Change(key="port", change_type=ChangeType.ADDED, new_value=8080, env="prod")
113+
expected = "[+] port = 8080 (env: prod)"
114+
assert str(c) == expected
115+
116+
def test_str_removed(self):
117+
c = Change(key="port", change_type=ChangeType.REMOVED, old_value=8080, env="dev")
118+
expected = "[-] port (was 8080) (env: dev)"
119+
assert str(c) == expected
120+
121+
def test_str_changed(self):
122+
c = Change(key="host", change_type=ChangeType.CHANGED, old_value="localhost", new_value="prod.example.com", env="prod")
123+
assert str(c) == "[~] host: 'localhost' \u2192 'prod.example.com' (env: prod)"
124+
125+
108126
class TestDiffResultHelpers:
109127
def test_by_type(self):
110128
changes = [

tests/test_loader.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -85,6 +85,60 @@ def test_ignores_comments(self):
8585
assert "This is a comment" not in result
8686

8787

88+
class TestLoadUnsupported:
89+
"""Tests for fallback behaviour with unknown file extensions."""
90+
91+
def test_fallback_yaml_content_in_cfg(self):
92+
"""Files with .cfg containing valid YAML should be parsed via fallback."""
93+
content = yaml.dump({"key": "value", "nested": {"inner": 42}})
94+
with tempfile.NamedTemporaryFile(suffix=".cfg", mode="w", delete=False) as f:
95+
f.write(content)
96+
f.flush()
97+
result = load_file(f.name)
98+
os.unlink(f.name)
99+
assert result["key"] == "value"
100+
assert result["nested.inner"] == 42
101+
102+
def test_fallback_yaml_as_json(self):
103+
"""Files with unknown ext containing valid JSON should be parsed via fallback."""
104+
content = json.dumps({"host": "localhost", "port": 8080})
105+
with tempfile.NamedTemporaryFile(suffix=".cnf", mode="w", delete=False) as f:
106+
f.write(content)
107+
f.flush()
108+
result = load_file(f.name)
109+
os.unlink(f.name)
110+
assert result["host"] == "localhost"
111+
assert result["port"] == 8080
112+
113+
def test_unsupported_format_fallback_to_dotenv(self):
114+
"""Files with unknown extension and no structured content fall through to .env parser and returns a dict."""
115+
with tempfile.NamedTemporaryFile(suffix=".xyz", mode="w", delete=False) as f:
116+
f.write("not a config")
117+
f.flush()
118+
result = load_file(f.name)
119+
os.unlink(f.name)
120+
assert isinstance(result, dict)
121+
122+
def test_unsupported_format_empty_result(self):
123+
"""Garbage content with unknown extension returns empty dict via .env fallback."""
124+
with tempfile.NamedTemporaryFile(suffix=".xyz", mode="w", delete=False) as f:
125+
f.write("!!!garbage!!!")
126+
f.flush()
127+
result = load_file(f.name)
128+
os.unlink(f.name)
129+
assert result == {}
130+
131+
def test_yaml_non_dict_raises(self):
132+
"""YAML containing a list (not a mapping) should raise ValueError."""
133+
content = yaml.dump(["item1", "item2"])
134+
with tempfile.NamedTemporaryFile(suffix=".yaml", mode="w", delete=False) as f:
135+
f.write(content)
136+
f.flush()
137+
with pytest.raises(ValueError, match="YAML file must contain a mapping"):
138+
load_file(f.name)
139+
os.unlink(f.name)
140+
141+
88142
class TestFlattenNested:
89143
def test_flatten(self):
90144
data = {"a": {"b": {"c": 1}, "d": 2}, "e": 3}

0 commit comments

Comments
 (0)