Skip to content

Commit acc89c7

Browse files
author
Mike Bumpus
committed
chore: add test suite, CI/CD, LICENSE, CHANGELOG for PyPI readiness
- MIT LICENSE file - CHANGELOG.md (v0.1.0) - 48 pytest tests: all 5 pattern detectors, CLI integration, data models - GitHub Actions: test.yml (matrix py3.9-3.12) + publish.yml (trusted publishing) - Fixed pytest config to exclude fixture files from collection - Verified build produces clean sdist + wheel
1 parent 41dd244 commit acc89c7

16 files changed

Lines changed: 657 additions & 0 deletions

.github/workflows/publish.yml

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
name: Publish to PyPI
2+
3+
on:
4+
release:
5+
types: [published]
6+
7+
permissions:
8+
id-token: write # Required for trusted publishing
9+
10+
jobs:
11+
build:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- uses: actions/checkout@v4
15+
16+
- name: Set up Python
17+
uses: actions/setup-python@v5
18+
with:
19+
python-version: "3.12"
20+
21+
- name: Install build tools
22+
run: python -m pip install --upgrade pip build
23+
24+
- name: Build package
25+
run: python -m build
26+
27+
- name: Upload build artifacts
28+
uses: actions/upload-artifact@v4
29+
with:
30+
name: dist
31+
path: dist/
32+
33+
test:
34+
runs-on: ubuntu-latest
35+
needs: build
36+
steps:
37+
- uses: actions/checkout@v4
38+
39+
- name: Set up Python
40+
uses: actions/setup-python@v5
41+
with:
42+
python-version: "3.12"
43+
44+
- name: Install dependencies
45+
run: |
46+
python -m pip install --upgrade pip
47+
pip install -e ".[dev]"
48+
49+
- name: Run tests
50+
run: pytest tests/ -v --tb=short
51+
52+
publish:
53+
runs-on: ubuntu-latest
54+
needs: [build, test]
55+
environment: pypi
56+
steps:
57+
- name: Download build artifacts
58+
uses: actions/download-artifact@v4
59+
with:
60+
name: dist
61+
path: dist/
62+
63+
- name: Publish to PyPI
64+
uses: pypa/gh-action-pypi-publish@release/v1
65+
# Uses trusted publishing — no API token needed
66+
# Configure at: https://pypi.org/manage/account/publishing/

.github/workflows/test.yml

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
name: Tests
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
test:
11+
runs-on: ubuntu-latest
12+
strategy:
13+
matrix:
14+
python-version: ["3.9", "3.10", "3.11", "3.12"]
15+
16+
steps:
17+
- uses: actions/checkout@v4
18+
19+
- name: Set up Python ${{ matrix.python-version }}
20+
uses: actions/setup-python@v5
21+
with:
22+
python-version: ${{ matrix.python-version }}
23+
24+
- name: Install dependencies
25+
run: |
26+
python -m pip install --upgrade pip
27+
pip install -e ".[dev]"
28+
29+
- name: Lint with ruff
30+
run: ruff check codesentry/
31+
32+
- name: Type check with mypy
33+
run: mypy codesentry/ --ignore-missing-imports
34+
continue-on-error: true
35+
36+
- name: Run tests
37+
run: pytest tests/ -v --tb=short --cov=codesentry --cov-report=term-missing
38+
39+
- name: Verify CLI works
40+
run: |
41+
codesentry --version
42+
codesentry patterns
43+
codesentry scan tests/fixtures/clean_file.py

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,13 @@ venv/
1414
*.swo
1515
.idea/
1616
.vscode/
17+
18+
# Crewly Codes internal artifacts
19+
history.md
20+
bugs.yaml
21+
specs/
22+
testing/
23+
dist/
24+
.pytest_cache/
25+
__pycache__/
26+
*.egg-info/

CHANGELOG.md

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
# Changelog
2+
3+
All notable changes to CodeSentry will be documented in this file.
4+
5+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7+
8+
## [0.1.0] - 2026-01-31
9+
10+
### Added
11+
- Initial release — Phase 0 MVP
12+
- 5 AST-based pattern detectors:
13+
- **CS001**: Generic exception swallowing
14+
- **CS002**: Placeholder code in production (TODO/FIXME with stubs)
15+
- **CS003**: Blocking calls in async functions
16+
- **CS004**: Hardcoded secrets
17+
- **CS005**: Mutable default arguments
18+
- CLI with `scan`, `patterns`, and `version` commands
19+
- Output modes: default, `--teach`, `--quick`, `--format json`
20+
- Exit codes: 0 (clean), 1 (warnings), 2 (errors), 3 (critical)
21+
- Decorator and inheritance-aware detection (reduces false positives)

LICENSE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2026 Mike Bumpus
4+
5+
Permission is hereby granted, free of charge, to any person obtaining a copy
6+
of this software and associated documentation files (the "Software"), to deal
7+
in the Software without restriction, including without limitation the rights
8+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9+
copies of the Software, and to permit persons to whom the Software is
10+
furnished to do so, subject to the following conditions:
11+
12+
The above copyright notice and this permission notice shall be included in all
13+
copies or substantial portions of the Software.
14+
15+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21+
SOFTWARE.

demo/app.py

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
"""
2+
Demo app.py for CodeSentry screenshot
3+
Triggers CS001 at line 47 and CS003 at line 82
4+
"""
5+
6+
import asyncio
7+
import time
8+
import json
9+
10+
11+
# ============================================
12+
# Normal code padding to get line numbers right
13+
# ============================================
14+
15+
def process_data(data):
16+
"""Process incoming data."""
17+
result = []
18+
for item in data:
19+
if item.get("active"):
20+
result.append(item["value"])
21+
return result
22+
23+
24+
def validate_input(user_input):
25+
"""Validate user input."""
26+
if not user_input:
27+
return False
28+
if len(user_input) > 1000:
29+
return False
30+
return True
31+
32+
33+
def format_response(data):
34+
"""Format data for API response."""
35+
return {
36+
"status": "success",
37+
"data": data,
38+
"timestamp": "2026-02-03T12:00:00Z"
39+
}
40+
41+
42+
class DataProcessor:
43+
# Line 47: CS001 - generic-exception-swallow (indented for col 5)
44+
def fetch_config(self):
45+
try:
46+
with open("config.json") as f:
47+
return json.load(f)
48+
except Exception:
49+
pass
50+
51+
def calculate_metrics(self, values):
52+
"""Calculate metrics from values."""
53+
if not values:
54+
return {}
55+
return {
56+
"total": sum(values),
57+
"average": sum(values) / len(values),
58+
"count": len(values)
59+
}
60+
61+
def get_user_preferences(self, user_id):
62+
"""Get user preferences from cache."""
63+
cache = {}
64+
return cache.get(user_id, {})
65+
66+
def normalize_text(self, text):
67+
"""Normalize text input."""
68+
if text is None:
69+
return ""
70+
return text.strip().lower()
71+
72+
73+
class AsyncService:
74+
"""Async service for remote operations."""
75+
76+
async def connect(self):
77+
"""Establish connection."""
78+
pass
79+
80+
# Line 82: CS003 - blocking-call-in-async (indented for col 9)
81+
async def sync_with_server(self):
82+
"""Sync data with remote server."""
83+
print("Starting sync...")
84+
time.sleep(5) # Blocking call in async!
85+
print("Sync complete")
86+
87+
88+
async def main():
89+
"""Main async entry point."""
90+
service = AsyncService()
91+
await service.sync_with_server()
92+
93+
94+
if __name__ == "__main__":
95+
asyncio.run(main())

pyproject.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -80,3 +80,4 @@ testpaths = ["tests"]
8080
python_files = ["test_*.py"]
8181
python_functions = ["test_*"]
8282
addopts = "-v --tb=short"
83+
norecursedirs = ["tests/fixtures"]

tests/conftest.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
"""Shared test fixtures for CodeSentry"""
2+
3+
import ast
4+
from pathlib import Path
5+
6+
import pytest
7+
8+
from codesentry.analyzer import AnalysisEngine
9+
from codesentry.patterns import ALL_PATTERNS
10+
11+
12+
FIXTURES_DIR = Path(__file__).parent / "fixtures"
13+
14+
15+
@pytest.fixture
16+
def engine():
17+
"""Analysis engine with all patterns loaded."""
18+
return AnalysisEngine(ALL_PATTERNS)
19+
20+
21+
@pytest.fixture
22+
def analyze(engine):
23+
"""Helper that analyzes a fixture file by name."""
24+
def _analyze(fixture_name: str):
25+
path = FIXTURES_DIR / fixture_name
26+
assert path.exists(), f"Fixture not found: {path}"
27+
return engine.analyze_file(path)
28+
return _analyze
29+
30+
31+
@pytest.fixture
32+
def analyze_source(engine):
33+
"""Helper that analyzes a source string."""
34+
def _analyze(source: str):
35+
import tempfile
36+
with tempfile.NamedTemporaryFile(mode="w", suffix=".py", delete=False) as f:
37+
f.write(source)
38+
f.flush()
39+
return engine.analyze_file(Path(f.name))
40+
return _analyze

tests/test_clean.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
"""Test that clean code produces no issues."""
2+
3+
4+
def test_clean_file_has_no_issues(analyze):
5+
result = analyze("clean_file.py")
6+
assert result.total_issues == 0
7+
assert result.parse_error is None
8+
assert result.max_severity_code == 0

0 commit comments

Comments
 (0)