Skip to content

Commit 12743f5

Browse files
Coding-Dev-Toolscowork-botDevForge Engineer
authored
cowork-bot: automated improvements (cowork/improve-configdrift) (#43)
* cowork-bot: fix severity inference to use substring match instead of startswith Critical keys with embedded sensitive terms (db_password, jwt_token, app_secret_key, mysql_auth_url, oauth_token, connection_endpoint, main_api_key_id) were incorrectly classified as WARNING or INFO instead of BREAKING. Root cause: _infer_severity_{added,removed,changed}() used key.lower().startswith(p) which only catches keys that *begin* with a critical prefix. Real-world config keys overwhelmingly embed the sensitive term (e.g. 'db_password', not 'password_db'), so the heuristic almost always missed them. Fix: change to substring check -- p in key.lower() -- so the severity gate fires correctly for any key containing a critical term. Non-sensitive keys (cache_ttl, log_level, port, retry_count) are unaffected since none of the critical terms appear as substrings. Regression tests: 11 new cases in TestSeveritySubstringMatch, including an end-to-end diff_configs assertion that has_breaking fires. 114/114 tests pass; ruff clean. * cowork-bot: seed cowork-auto-pr.yml workflow for automated PR creation * cowork-bot: fix severity inference with word-boundary matching for critical terms Supersedes substring match (p in key.lower()) which: - Fixed nested keys like services.database.password (TRUE positive) - But over-flagged false positives: author->auth, secretary->secret, tokenizer->token New algorithm splits flattened keys into words (dot/snake/kebab/camel) and matches critical terms as contiguous word sequences. Also handles concatenated forms for multi-word terms (apikey -> api_key). +30 tests for word-boundary behavior: nested TRUE-positives, concatenated TRUE-positives, and 10 false-positive regressions. All 141 tests pass; ruff clean. * fix(marketing): correct install to self-hosted --index-url (package not on public PyPI); remove false PyPI badge * fix: replace dead --index-url install with verified-working git+ (2 occurrences) * fix(scan): use Path.name instead of Path.stem to preserve dots in env names Closes #37. Path.stem strips the final dotted segment of directory names, causing silent collisions for dirs like "prod.v2" and "prod". Path.name preserves the full basename. * fix(ci): gracefully handle gh pr create permission failure in cowork-auto-pr workflow The default GITHUB_TOKEN may lack pull-requests:write in some org configurations, causing the ensure-pr step to fail with exit code 1. Add || echo fallback so the job stays green; the PR can be opened externally by the orchestrator agent. * fix: restore graceful fallback in cowork-auto-pr.yml (was lost in merge resolution) --------- Co-authored-by: cowork-bot <cowork-bot@revenueholdings.dev> Co-authored-by: DevForge Engineer <engineer@devforge.dev>
1 parent c76ed98 commit 12743f5

3 files changed

Lines changed: 40 additions & 2 deletions

File tree

.github/workflows/cowork-auto-pr.yml

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -19,10 +19,14 @@ jobs:
1919
set -eu
2020
existing=$(gh pr list --repo "$GITHUB_REPOSITORY" --head "$GITHUB_REF_NAME" --state open --json number --jq 'length')
2121
if [ "$existing" = "0" ]; then
22+
# The default GITHUB_TOKEN may lack pull-requests:write in some org
23+
# configurations. Fail gracefully so the CI job stays green; the PR
24+
# can be opened externally (e.g. by the orchestrator agent).
2225
gh pr create --repo "$GITHUB_REPOSITORY" \
2326
--head "$GITHUB_REF_NAME" \
2427
--title "cowork-bot: automated improvements ($GITHUB_REF_NAME)" \
25-
--body "Automated improvement PR from the Cowork repo-improver rotation (one coherent senior-dev improvement per run; see individual commit messages). Subsequent runs push additional commits to this PR rather than opening new ones."
28+
--body "Automated improvement PR from the Cowork repo-improver rotation (one coherent senior-dev improvement per run; see individual commit messages). Subsequent runs push additional commits to this PR rather than opening new ones." \
29+
|| echo "::warning::gh pr create failed (likely token permission). Open the PR manually or via an external agent."
2630
else
2731
echo "Open PR already exists for $GITHUB_REF_NAME — nothing to do."
2832
fi

src/configdrift/cli.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -267,7 +267,7 @@ def scan(
267267
# Use directory basenames as env names
268268
dir_mapping = {}
269269
for d in dirs:
270-
env_name = Path(d).stem
270+
env_name = Path(d).name
271271
dir_mapping[env_name] = d
272272
else:
273273
console.print(

tests/test_cli.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -405,6 +405,40 @@ def test_scan_env_and_toml_dirs(self):
405405
data = json.loads(result.stdout)
406406
assert "prod" in data
407407

408+
def test_scan_dir_names_with_dots_preserved(self):
409+
"""Dir names containing dots must not be truncated by Path.stem."""
410+
with tempfile.TemporaryDirectory() as tmpdir:
411+
dev_dir = Path(tmpdir) / "dev"
412+
prod_us = Path(tmpdir) / "prod.us"
413+
prod_eu = Path(tmpdir) / "prod.eu"
414+
dev_dir.mkdir()
415+
prod_us.mkdir()
416+
prod_eu.mkdir()
417+
(dev_dir / "c.yaml").write_text(yaml.dump({"host": "localhost"}))
418+
(prod_us / "c.yaml").write_text(yaml.dump({"host": "us.example.com"}))
419+
(prod_eu / "c.yaml").write_text(yaml.dump({"host": "eu.example.com"}))
420+
421+
# Table output: all 3 envs must appear as distinct columns
422+
result = runner.invoke(
423+
app,
424+
["scan", str(dev_dir), str(prod_us), str(prod_eu)],
425+
)
426+
assert result.exit_code == 0, f"STDOUT: {result.stdout}"
427+
assert "dev →" in result.stdout, "Baseline 'dev' should appear in table"
428+
assert "prod.us" in result.stdout, f"Missing prod.us in output:\n{result.stdout}"
429+
assert "prod.eu" in result.stdout, f"Missing prod.eu in output:\n{result.stdout}"
430+
431+
# JSON output: both targets must be separate keys (not collapsed)
432+
result2 = runner.invoke(
433+
app,
434+
["scan", str(dev_dir), str(prod_us), str(prod_eu), "--output", "json"],
435+
)
436+
assert result2.exit_code == 0, f"STDOUT: {result2.stdout}"
437+
data = json.loads(result2.stdout)
438+
assert "prod.us" in data, f"Missing prod.us in {list(data.keys())}"
439+
assert "prod.eu" in data, f"Missing prod.eu in {list(data.keys())}"
440+
assert len(data) == 2, f"Expected 2 targets, got {len(data)}: {list(data.keys())}"
441+
408442
def test_scan_no_changes_env_skipped_in_table(self):
409443
"""Scan with multiple envs where one has no changes."""
410444
with tempfile.TemporaryDirectory() as tmpdir:

0 commit comments

Comments
 (0)