diff --git a/.github/workflows/leaderboard.yml b/.github/workflows/leaderboard.yml index 9223e5cf..9f580d1a 100644 --- a/.github/workflows/leaderboard.yml +++ b/.github/workflows/leaderboard.yml @@ -88,7 +88,7 @@ jobs: REPO_URL: ${{ steps.extract.outputs.repo_url }} run: | # SAFE: REPO_URL comes from workflow output, not direct user input - ORG_REPO=$(echo "$REPO_URL" | sed 's|https://github.com/||' | sed 's|\.git$||') + ORG_REPO=$(echo "$REPO_URL" | sed 's|git@github.com:||' | sed 's|https://github.com/||' | sed 's|\.git$||') IS_PRIVATE=$(gh repo view "$ORG_REPO" --json isPrivate -q '.isPrivate') @@ -106,7 +106,7 @@ jobs: SUBMITTER: ${{ github.event.pull_request.user.login }} run: | # SAFE: All values in environment variables - ORG_REPO=$(echo "$REPO_URL" | sed 's|https://github.com/||' | sed 's|\.git$||') + ORG_REPO=$(echo "$REPO_URL" | sed 's|git@github.com:||' | sed 's|https://github.com/||' | sed 's|\.git$||') if gh api "/repos/$ORG_REPO/collaborators/$SUBMITTER" 2>/dev/null; then echo "✅ $SUBMITTER is a collaborator on $ORG_REPO" diff --git a/CHANGELOG.md b/CHANGELOG.md index b3e2c6d5..82e827e5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,10 @@ +## [2.27.5](https://github.com/ambient-code/agentready/compare/v2.27.4...v2.27.5) (2026-02-17) + + +### Bug Fixes + +* **cli:** Use removesuffix instead of rstrip for .git URL stripping ([#292](https://github.com/ambient-code/agentready/issues/292)) ([6bd08cf](https://github.com/ambient-code/agentready/commit/6bd08cf50d541ed4a73ed347d43da48a4a540f3f)) + ## [2.27.4](https://github.com/ambient-code/agentready/compare/v2.27.3...v2.27.4) (2026-02-17) diff --git a/CLAUDE.md b/CLAUDE.md index 90ac58b1..10fc52f9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -10,7 +10,7 @@ AgentReady is a Python CLI tool that evaluates repositories against a comprehensive set of carefully researched attributes that make codebases more effective for AI-assisted development. It generates interactive HTML reports, version-control friendly Markdown reports, and machine-readable JSON output. -**Current Status**: v2.27.4 - Core assessment engine complete, most essential assessors implemented, LLM-powered learning, research report management +**Current Status**: v2.27.5 - Core assessment engine complete, most essential assessors implemented, LLM-powered learning, research report management **Self-Assessment Score**: 80.0/100 (Gold) - See `examples/self-assessment/` @@ -446,7 +446,7 @@ Use the `github-pages-docs` agent for documentation updates after: --- **Last Updated**: 2026-02-17 by Jeremy Eder -**AgentReady Version**: 2.27.4 +**AgentReady Version**: 2.27.5 **Self-Assessment**: 80.0/100 (Gold) ✨ ### GitHub Actions Guidelines diff --git a/pyproject.toml b/pyproject.toml index ab0034ab..b9c30b2a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "agentready" -version = "2.27.4" +version = "2.27.5" description = "Assess and bootstrap git repositories for AI-assisted development with automated remediation and continuous learning" authors = [{name = "Jeremy Eder", email = "jeder@redhat.com"}] readme = "README.md" diff --git a/src/agentready/cli/submit.py b/src/agentready/cli/submit.py index 5b1438df..0f100220 100644 --- a/src/agentready/cli/submit.py +++ b/src/agentready/cli/submit.py @@ -68,10 +68,10 @@ def extract_repo_info(assessment_data: dict) -> tuple[str, str, float, str]: try: # Handle SSH format: git@github.com:org/repo.git if repo_url.startswith("git@github.com:"): - org_repo = repo_url.split("git@github.com:")[1].rstrip(".git") + org_repo = repo_url.split("git@github.com:")[1].removesuffix(".git") # Handle HTTPS format: https://github.com/org/repo.git else: - org_repo = repo_url.split("github.com/")[1].strip("/").rstrip(".git") + org_repo = repo_url.split("github.com/")[1].strip("/").removesuffix(".git") org, repo = org_repo.split("/") except (IndexError, ValueError): diff --git a/src/agentready/services/repository_manager.py b/src/agentready/services/repository_manager.py index 70462a34..52ca8317 100644 --- a/src/agentready/services/repository_manager.py +++ b/src/agentready/services/repository_manager.py @@ -93,7 +93,7 @@ def get_repository_name_from_url(self, url: str) -> str: # For URLs, extract from the last part of the path # https://github.com/user/repo.git -> repo parsed = urlparse(url) - path = parsed.path.rstrip("/").rstrip(".git") + path = parsed.path.rstrip("/").removesuffix(".git") return Path(path).name or "repository" def clone_repository( diff --git a/submissions/opendatahub-io/ai-helpers/2026-02-17T19-56-27-assessment.json b/submissions/opendatahub-io/ai-helpers/2026-02-17T19-56-27-assessment.json new file mode 100644 index 00000000..99da5abd --- /dev/null +++ b/submissions/opendatahub-io/ai-helpers/2026-02-17T19-56-27-assessment.json @@ -0,0 +1,800 @@ +{ + "schema_version": "1.0.0", + "metadata": { + "agentready_version": "2.27.3", + "research_version": "1.0.1", + "assessment_timestamp": "2026-02-17T14:55:38.837016", + "assessment_timestamp_human": "February 17, 2026 at 2:55 PM", + "executed_by": "mprpic@fedora", + "command": "/home/mprpic/.cache/uv/archive-v0/Eh8oXxFVmhsh5yX--a8mI/bin/agentready assess .", + "working_directory": "/home/mprpic/repos/github.com/opendatahub-io/ai-helpers" + }, + "repository": { + "path": "/home/mprpic/repos/github.com/opendatahub-io/ai-helpers", + "name": "ai-helpers", + "url": "git@github.com:opendatahub-io/ai-helpers.git", + "branch": "main", + "commit_hash": "66a4d162f47a518515d51e90bf37cc069be874e7", + "languages": { + "JSON": 7, + "Python": 12, + "YAML": 14, + "Markdown": 36, + "Shell": 4 + }, + "total_files": 78, + "total_lines": 9913 + }, + "timestamp": "2026-02-17T14:55:38.837016", + "overall_score": 64.7, + "certification_level": "Silver", + "attributes_assessed": 20, + "attributes_skipped": 5, + "attributes_total": 25, + "findings": [ + { + "attribute": { + "id": "claude_md_file", + "name": "CLAUDE.md Configuration Files", + "category": "Context Window Optimization", + "tier": 1, + "description": "Project-specific configuration for Claude Code", + "criteria": "CLAUDE.md file exists in repository root", + "default_weight": 0.1 + }, + "status": "pass", + "score": 100.0, + "measured_value": "present", + "threshold": "present", + "evidence": [ + "CLAUDE.md found at /home/mprpic/repos/github.com/opendatahub-io/ai-helpers/CLAUDE.md", + "Symlink to AGENTS.md (4558 bytes)", + "AGENTS.md also present (cross-tool compatibility)" + ], + "remediation": null, + "error_message": null + }, + { + "attribute": { + "id": "readme_structure", + "name": "README Structure", + "category": "Documentation Standards", + "tier": 1, + "description": "Well-structured README with key sections", + "criteria": "README.md with installation, usage, and development sections", + "default_weight": 0.1 + }, + "status": "pass", + "score": 100.0, + "measured_value": "3/3 sections", + "threshold": "3/3 sections", + "evidence": [ + "Found 3/3 essential sections", + "Installation: \u2713", + "Usage: \u2713", + "Development: \u2713" + ], + "remediation": null, + "error_message": null + }, + { + "attribute": { + "id": "type_annotations", + "name": "Type Annotations", + "category": "Code Quality", + "tier": 1, + "description": "Type hints in function signatures", + "criteria": ">80% of functions have type annotations", + "default_weight": 0.1 + }, + "status": "pass", + "score": 100.0, + "measured_value": "87.5%", + "threshold": "\u226580%", + "evidence": [ + "Typed functions: 98/112", + "Coverage: 87.5%" + ], + "remediation": null, + "error_message": null + }, + { + "attribute": { + "id": "standard_layout", + "name": "Standard Project Layouts", + "category": "Repository Structure", + "tier": 1, + "description": "Follows standard project structure for language", + "criteria": "Standard directories (src/, tests/, docs/) present", + "default_weight": 0.1 + }, + "status": "fail", + "score": 0.0, + "measured_value": "0/2 directories", + "threshold": "2/2 directories", + "evidence": [ + "Found 0/2 standard directories", + "src/: \u2717", + "tests/: \u2717" + ], + "remediation": { + "summary": "Organize code into standard directories (src/, tests/, docs/)", + "steps": [ + "Create src/ directory for source code", + "Create tests/ directory for test files", + "Create docs/ directory for documentation", + "Move source code into src/", + "Move tests into tests/" + ], + "tools": [], + "commands": [ + "mkdir -p src tests docs", + "# Move source files to src/", + "# Move test files to tests/" + ], + "examples": [], + "citations": [ + { + "source": "Python Packaging Authority", + "title": "Python Project Structure", + "url": "https://packaging.python.org/en/latest/tutorials/packaging-projects/", + "relevance": "Standard Python project layout" + } + ] + }, + "error_message": null + }, + { + "attribute": { + "id": "lock_files", + "name": "Dependency Pinning for Reproducibility", + "category": "Dependency Management", + "tier": 1, + "description": "Dependencies pinned to exact versions in lock files", + "criteria": "Lock file with pinned versions, updated within 6 months", + "default_weight": 0.1 + }, + "status": "fail", + "score": 0.0, + "measured_value": "none", + "threshold": "lock file with pinned versions", + "evidence": [ + "No dependency lock files found" + ], + "remediation": { + "summary": "Add lock file for dependency reproducibility", + "steps": [ + "For npm: run 'npm install' (generates package-lock.json)", + "For Python: use 'pip freeze > requirements.txt' or poetry", + "For Ruby: run 'bundle install' (generates Gemfile.lock)" + ], + "tools": [ + "npm", + "pip", + "poetry", + "bundler" + ], + "commands": [ + "npm install # npm", + "pip freeze > requirements.txt # Python", + "poetry lock # Python with Poetry" + ], + "examples": [], + "citations": [] + }, + "error_message": null + }, + { + "attribute": { + "id": "dependency_security", + "name": "Dependency Security & Vulnerability Scanning", + "category": "Security", + "tier": 1, + "description": "Security scanning tools configured for dependencies and code", + "criteria": "Dependabot, CodeQL, or SAST tools configured; secret detection enabled", + "default_weight": 0.04 + }, + "status": "pass", + "score": 35, + "measured_value": "Security tools configured: Dependabot", + "threshold": "\u226560 points (Dependabot + SAST or multiple scanners)", + "evidence": [ + "\u2713 Dependabot configured for dependency alerts", + " 1 package ecosystem(s) monitored" + ], + "remediation": { + "summary": "Add more security scanning tools for comprehensive coverage", + "steps": [ + "Enable Dependabot alerts in GitHub repository settings", + "Add CodeQL scanning workflow for SAST", + "Configure secret detection (detect-secrets, gitleaks)", + "Set up language-specific scanners (pip-audit, npm audit, Snyk)" + ], + "tools": [ + "Dependabot", + "CodeQL", + "detect-secrets", + "pip-audit", + "npm audit" + ], + "commands": [ + "gh repo edit --enable-security", + "pip install detect-secrets # Python secret detection", + "npm audit # JavaScript dependency audit" + ], + "examples": [ + "# .github/dependabot.yml\nversion: 2\nupdates:\n - package-ecosystem: pip\n directory: /\n schedule:\n interval: weekly" + ], + "citations": [ + { + "source": "OWASP", + "title": "Dependency-Check Project", + "url": "https://owasp.org/www-project-dependency-check/", + "relevance": "Open-source tool for detecting known vulnerabilities in dependencies" + }, + { + "source": "GitHub", + "title": "Dependabot Documentation", + "url": "https://docs.github.com/en/code-security/dependabot", + "relevance": "Official guide for configuring automated dependency updates and security alerts" + } + ] + }, + "error_message": null + }, + { + "attribute": { + "id": "test_coverage", + "name": "Test Coverage Requirements", + "category": "Testing & CI/CD", + "tier": 2, + "description": "Test coverage thresholds configured and enforced", + "criteria": ">80% code coverage", + "default_weight": 0.03 + }, + "status": "not_applicable", + "score": null, + "measured_value": null, + "threshold": null, + "evidence": [ + "Not applicable to ['JSON', 'Python', 'YAML', 'Markdown', 'Shell']" + ], + "remediation": null, + "error_message": null + }, + { + "attribute": { + "id": "precommit_hooks", + "name": "Pre-commit Hooks & CI/CD Linting", + "category": "Testing & CI/CD", + "tier": 2, + "description": "Pre-commit hooks configured for linting and formatting", + "criteria": ".pre-commit-config.yaml exists", + "default_weight": 0.03 + }, + "status": "pass", + "score": 100.0, + "measured_value": "configured", + "threshold": "configured", + "evidence": [ + ".pre-commit-config.yaml found" + ], + "remediation": null, + "error_message": null + }, + { + "attribute": { + "id": "conventional_commits", + "name": "Conventional Commit Messages", + "category": "Git & Version Control", + "tier": 2, + "description": "Follows conventional commit format", + "criteria": "\u226580% of recent commits follow convention", + "default_weight": 0.03 + }, + "status": "fail", + "score": 0.0, + "measured_value": "not configured", + "threshold": "configured", + "evidence": [ + "No commitlint or husky configuration" + ], + "remediation": { + "summary": "Configure conventional commits with commitlint", + "steps": [ + "Install commitlint", + "Configure husky for commit-msg hook" + ], + "tools": [ + "commitlint", + "husky" + ], + "commands": [ + "npm install --save-dev @commitlint/cli @commitlint/config-conventional husky" + ], + "examples": [], + "citations": [] + }, + "error_message": null + }, + { + "attribute": { + "id": "gitignore_completeness", + "name": ".gitignore Completeness", + "category": "Git & Version Control", + "tier": 2, + "description": "Comprehensive .gitignore file with language-specific patterns", + "criteria": ".gitignore exists and includes language-specific patterns from GitHub templates", + "default_weight": 0.03 + }, + "status": "pass", + "score": 100.0, + "measured_value": "12/12 patterns", + "threshold": "\u226570% of language-specific patterns", + "evidence": [ + ".gitignore found (662 bytes)", + "Pattern coverage: 12/12 (100%)" + ], + "remediation": null, + "error_message": null + }, + { + "attribute": { + "id": "one_command_setup", + "name": "One-Command Build/Setup", + "category": "Build & Development", + "tier": 2, + "description": "Single command to set up development environment from fresh clone", + "criteria": "Single command (make setup, npm install, etc.) documented prominently", + "default_weight": 0.03 + }, + "status": "pass", + "score": 100, + "measured_value": "For setup", + "threshold": "single command", + "evidence": [ + "Setup command found in README: 'For setup'", + "Setup automation found: Makefile", + "Setup instructions in prominent location" + ], + "remediation": null, + "error_message": null + }, + { + "attribute": { + "id": "file_size_limits", + "name": "File Size Limits", + "category": "Context Window Optimization", + "tier": 2, + "description": "Files are reasonably sized for AI context windows", + "criteria": "<5% of files >500 lines, no files >1000 lines", + "default_weight": 0.03 + }, + "status": "pass", + "score": 75, + "measured_value": "0 huge, 2 large out of 12", + "threshold": "<5% files >500 lines, 0 files >1000 lines", + "evidence": [ + "Found 2 files 500-1000 lines (16.7% of 12 files)" + ], + "remediation": null, + "error_message": null + }, + { + "attribute": { + "id": "separation_of_concerns", + "name": "Separation of Concerns", + "category": "Code Organization", + "tier": 2, + "description": "Code organized with single responsibility per module", + "criteria": "Feature-based organization, cohesive modules, low coupling", + "default_weight": 0.03 + }, + "status": "pass", + "score": 95.0, + "measured_value": "organization:100, cohesion:83, naming:100", + "threshold": "\u226575 overall", + "evidence": [ + "Good directory organization (feature-based or flat)", + "File cohesion: 2/12 files >500 lines", + "No catch-all modules (utils.py, helpers.py) detected" + ], + "remediation": null, + "error_message": null + }, + { + "attribute": { + "id": "concise_documentation", + "name": "Concise Documentation", + "category": "Documentation", + "tier": 2, + "description": "Documentation maximizes information density while minimizing token consumption", + "criteria": "README <500 lines with clear structure, bullet points over prose", + "default_weight": 0.03 + }, + "status": "pass", + "score": 81.0, + "measured_value": "257 lines, 19 headings, 12 bullets", + "threshold": "<500 lines, structured format", + "evidence": [ + "README length: 257 lines (excellent)", + "Heading density: 7.4 per 100 lines (target: 3-5)", + "12 bullet points, 12 code blocks (concise formatting)" + ], + "remediation": null, + "error_message": null + }, + { + "attribute": { + "id": "inline_documentation", + "name": "Inline Documentation", + "category": "Documentation", + "tier": 2, + "description": "Function, class, and module-level documentation using language-specific conventions", + "criteria": "\u226580% of public functions/classes have docstrings", + "default_weight": 0.03 + }, + "status": "pass", + "score": 100.0, + "measured_value": "83.6%", + "threshold": "\u226580%", + "evidence": [ + "Documented items: 97/116", + "Coverage: 83.6%", + "Good docstring coverage" + ], + "remediation": null, + "error_message": null + }, + { + "attribute": { + "id": "cyclomatic_complexity", + "name": "Cyclomatic Complexity Thresholds", + "category": "Code Quality", + "tier": 3, + "description": "Cyclomatic complexity thresholds enforced", + "criteria": "Average complexity <10, no functions >15", + "default_weight": 0.03 + }, + "status": "pass", + "score": 100.0, + "measured_value": "6.3", + "threshold": "<10.0", + "evidence": [ + "Average cyclomatic complexity: 6.3" + ], + "remediation": null, + "error_message": null + }, + { + "attribute": { + "id": "architecture_decisions", + "name": "Architecture Decision Records (ADRs)", + "category": "Documentation Standards", + "tier": 3, + "description": "Lightweight documents capturing architectural decisions", + "criteria": "ADR directory with documented decisions", + "default_weight": 0.015 + }, + "status": "fail", + "score": 0.0, + "measured_value": "no ADR directory", + "threshold": "ADR directory with decisions", + "evidence": [ + "No ADR directory found (checked docs/adr/, .adr/, adr/, docs/decisions/)" + ], + "remediation": { + "summary": "Create Architecture Decision Records (ADRs) directory and document key decisions", + "steps": [ + "Create docs/adr/ directory in repository root", + "Use Michael Nygard ADR template or MADR format", + "Document each significant architectural decision", + "Number ADRs sequentially (0001-*.md, 0002-*.md)", + "Include Status, Context, Decision, and Consequences sections", + "Update ADR status when decisions are revised (Superseded, Deprecated)" + ], + "tools": [ + "adr-tools", + "log4brains" + ], + "commands": [ + "# Create ADR directory", + "mkdir -p docs/adr", + "", + "# Create first ADR using template", + "cat > docs/adr/0001-use-architecture-decision-records.md << 'EOF'", + "# 1. Use Architecture Decision Records", + "", + "Date: 2025-11-22", + "", + "## Status", + "Accepted", + "", + "## Context", + "We need to record architectural decisions made in this project.", + "", + "## Decision", + "We will use Architecture Decision Records (ADRs) as described by Michael Nygard.", + "", + "## Consequences", + "- Decisions are documented with context", + "- Future contributors understand rationale", + "- ADRs are lightweight and version-controlled", + "EOF" + ], + "examples": [ + "# Example ADR Structure\n\n```markdown\n# 2. Use PostgreSQL for Database\n\nDate: 2025-11-22\n\n## Status\nAccepted\n\n## Context\nWe need a relational database for complex queries and ACID transactions.\nTeam has PostgreSQL experience. Need full-text search capabilities.\n\n## Decision\nUse PostgreSQL 15+ as primary database.\n\n## Consequences\n- Positive: Robust ACID, full-text search, team familiarity\n- Negative: Higher resource usage than SQLite\n- Neutral: Need to manage migrations, backups\n```\n" + ], + "citations": [ + { + "source": "Michael Nygard", + "title": "Documenting Architecture Decisions", + "url": "https://cognitect.com/blog/2011/11/15/documenting-architecture-decisions", + "relevance": "Original ADR format and rationale" + }, + { + "source": "GitHub adr/madr", + "title": "Markdown ADR (MADR) Template", + "url": "https://github.com/adr/madr", + "relevance": "Modern ADR template with examples" + } + ] + }, + "error_message": null + }, + { + "attribute": { + "id": "issue_pr_templates", + "name": "Issue & Pull Request Templates", + "category": "Repository Structure", + "tier": 3, + "description": "Standardized templates for issues and PRs", + "criteria": "PR template and issue templates in .github/", + "default_weight": 0.015 + }, + "status": "pass", + "score": 100, + "measured_value": "PR:True, Issues:7", + "threshold": "PR template + \u22652 issue templates", + "evidence": [ + "PR template found", + "Issue templates found: 7 templates" + ], + "remediation": null, + "error_message": null + }, + { + "attribute": { + "id": "cicd_pipeline_visibility", + "name": "CI/CD Pipeline Visibility", + "category": "Testing & CI/CD", + "tier": 3, + "description": "Clear, well-documented CI/CD configuration files", + "criteria": "CI config with descriptive names, caching, parallelization", + "default_weight": 0.015 + }, + "status": "fail", + "score": 60, + "measured_value": "basic config", + "threshold": "CI with best practices", + "evidence": [ + "CI config found: .github/workflows/build.yml, .github/workflows/deploy.yml, .github/workflows/lint.yml, .github/workflows/sync-labels.yml, .github/workflows/stale-pr.yml, .github/workflows/issue-assign.yml", + "Descriptive job/step names found", + "No caching detected", + "No parallelization detected" + ], + "remediation": { + "summary": "Add or improve CI/CD pipeline configuration", + "steps": [ + "Create CI config for your platform (GitHub Actions, GitLab CI, etc.)", + "Define jobs: lint, test, build", + "Use descriptive job and step names", + "Configure dependency caching", + "Enable parallel job execution", + "Upload artifacts: test results, coverage reports", + "Add status badge to README" + ], + "tools": [ + "github-actions", + "gitlab-ci", + "circleci" + ], + "commands": [ + "# Create GitHub Actions workflow", + "mkdir -p .github/workflows", + "touch .github/workflows/ci.yml", + "", + "# Validate workflow", + "gh workflow view ci.yml" + ], + "examples": [ + "# .github/workflows/ci.yml - Good example\n\nname: CI Pipeline\n\non:\n push:\n branches: [main]\n pull_request:\n branches: [main]\n\njobs:\n lint:\n name: Lint Code\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n\n - name: Set up Python\n uses: actions/setup-python@v5\n with:\n python-version: '3.11'\n cache: 'pip' # Caching\n\n - name: Install dependencies\n run: pip install -r requirements.txt\n\n - name: Run linters\n run: |\n black --check .\n isort --check .\n ruff check .\n\n test:\n name: Run Tests\n runs-on: ubuntu-latest\n steps:\n - uses: actions/checkout@v4\n\n - name: Set up Python\n uses: actions/setup-python@v5\n with:\n python-version: '3.11'\n cache: 'pip'\n\n - name: Install dependencies\n run: pip install -r requirements.txt\n\n - name: Run tests with coverage\n run: pytest --cov --cov-report=xml\n\n - name: Upload coverage reports\n uses: codecov/codecov-action@v3\n with:\n files: ./coverage.xml\n\n build:\n name: Build Package\n runs-on: ubuntu-latest\n needs: [lint, test] # Runs after lint/test pass\n steps:\n - uses: actions/checkout@v4\n\n - name: Build package\n run: python -m build\n\n - name: Upload build artifacts\n uses: actions/upload-artifact@v3\n with:\n name: dist\n path: dist/\n" + ], + "citations": [ + { + "source": "GitHub", + "title": "GitHub Actions Documentation", + "url": "https://docs.github.com/en/actions", + "relevance": "Official GitHub Actions guide" + }, + { + "source": "CircleCI", + "title": "CI/CD Best Practices", + "url": "https://circleci.com/blog/ci-cd-best-practices/", + "relevance": "Industry best practices for CI/CD" + } + ] + }, + "error_message": null + }, + { + "attribute": { + "id": "semantic_naming", + "name": "Semantic Naming", + "category": "Code Quality", + "tier": 3, + "description": "Systematic naming patterns following language conventions", + "criteria": "Language conventions followed, avoid generic names", + "default_weight": 0.015 + }, + "status": "pass", + "score": 100.0, + "measured_value": "functions:100%, classes:100%", + "threshold": "\u226575% compliance", + "evidence": [ + "Functions: 94/94 follow snake_case (100.0%)", + "Classes: 10/10 follow PascalCase (100.0%)", + "No generic names (temp, data, obj) detected" + ], + "remediation": null, + "error_message": null + }, + { + "attribute": { + "id": "structured_logging", + "name": "Structured Logging", + "category": "Code Quality", + "tier": 3, + "description": "Logging in structured format (JSON) with consistent fields", + "criteria": "Structured logging library configured (structlog, winston, zap)", + "default_weight": 0.015 + }, + "status": "not_applicable", + "score": null, + "measured_value": null, + "threshold": null, + "evidence": [ + "No Python dependency files found" + ], + "remediation": null, + "error_message": null + }, + { + "attribute": { + "id": "openapi_specs", + "name": "OpenAPI/Swagger Specifications", + "category": "API Documentation", + "tier": 3, + "description": "Machine-readable API documentation in OpenAPI format", + "criteria": "OpenAPI 3.x spec with complete endpoint documentation", + "default_weight": 0.015 + }, + "status": "not_applicable", + "score": null, + "measured_value": null, + "threshold": null, + "evidence": [ + "Not applicable to ['JSON', 'Python', 'YAML', 'Markdown', 'Shell']" + ], + "remediation": null, + "error_message": null + }, + { + "attribute": { + "id": "branch_protection", + "name": "Branch Protection Rules", + "category": "Git & Version Control", + "tier": 4, + "description": "Required status checks and review approvals before merging", + "criteria": "Branch protection enabled with status checks and required reviews", + "default_weight": 0.005 + }, + "status": "not_applicable", + "score": null, + "measured_value": null, + "threshold": null, + "evidence": [ + "Requires GitHub API integration for branch protection checks. Future implementation will verify: required status checks, required reviews, force push prevention, and branch update requirements." + ], + "remediation": null, + "error_message": null + }, + { + "attribute": { + "id": "code_smells", + "name": "Code Smell Elimination", + "category": "Code Quality", + "tier": 4, + "description": "Linter configuration for detecting code smells and anti-patterns", + "criteria": "Language-specific linters configured (pylint, ESLint, RuboCop, etc.)", + "default_weight": 0.01 + }, + "status": "fail", + "score": 0.0, + "measured_value": "none", + "threshold": "\u226560% of applicable linters configured", + "evidence": [ + "No linters configured" + ], + "remediation": { + "summary": "Configure 4 missing linter(s)", + "steps": [ + "Configure pylint for Python code smell detection", + "Configure ruff for fast Python linting", + "Add actionlint for GitHub Actions workflow validation", + "Configure markdownlint for documentation quality" + ], + "tools": [ + "pylint", + "ruff", + "actionlint", + "markdownlint" + ], + "commands": [ + "pip install pylint && pylint --generate-rcfile > .pylintrc", + "pip install ruff && ruff init", + "npm install --save-dev markdownlint-cli && touch .markdownlint.json" + ], + "examples": [ + "# .pylintrc example\n[MASTER]\nmax-line-length=100\n\n[MESSAGES CONTROL]\ndisable=C0111", + "# .eslintrc.json example\n{\n \"extends\": \"eslint:recommended\",\n \"rules\": {\n \"no-console\": \"warn\"\n }\n}" + ], + "citations": [ + { + "source": "Pylint", + "title": "Pylint Documentation", + "url": "https://pylint.readthedocs.io/", + "relevance": "Official documentation for Pylint code analysis tool" + }, + { + "source": "ESLint", + "title": "ESLint Documentation", + "url": "https://eslint.org/docs/latest/", + "relevance": "Official documentation for ESLint JavaScript/TypeScript linter" + } + ] + }, + "error_message": null + }, + { + "attribute": { + "id": "container_setup", + "name": "Container/Virtualization Setup", + "category": "Build & Development", + "tier": 4, + "description": "Container configuration for consistent development environments", + "criteria": "Dockerfile/Containerfile, docker-compose.yml, .dockerignore, multi-stage builds", + "default_weight": 0.01 + }, + "status": "not_applicable", + "score": null, + "measured_value": null, + "threshold": null, + "evidence": [ + "Not applicable to ['JSON', 'Python', 'YAML', 'Markdown', 'Shell']" + ], + "remediation": null, + "error_message": null + } + ], + "config": { + "weights": {}, + "excluded_attributes": [], + "language_overrides": {}, + "output_dir": null, + "report_theme": "default", + "custom_theme": null + }, + "duration_seconds": 0.2, + "discovered_skills": [] +} \ No newline at end of file diff --git a/tests/unit/test_cli_submit.py b/tests/unit/test_cli_submit.py new file mode 100644 index 00000000..64c23fd6 --- /dev/null +++ b/tests/unit/test_cli_submit.py @@ -0,0 +1,91 @@ +"""Unit tests for CLI submit command.""" + +from agentready.cli.submit import extract_repo_info + + +class TestExtractRepoInfo: + """Test extract_repo_info function.""" + + def test_extract_repo_info_https_with_git_suffix(self): + """Test extract_repo_info handles HTTPS .git suffix correctly.""" + assessment_data = { + "repository": {"url": "https://github.com/feast-dev/feast.git"}, + "overall_score": 85.0, + "certification_level": "Gold", + } + + org, repo, score, tier = extract_repo_info(assessment_data) + assert org == "feast-dev" + assert repo == "feast" # Not "feas" + assert score == 85.0 + assert tier == "Gold" + + def test_extract_repo_info_https_without_git_suffix(self): + """Test extract_repo_info handles HTTPS URL without .git suffix.""" + assessment_data = { + "repository": {"url": "https://github.com/org/my-repo"}, + "overall_score": 70.0, + "certification_level": "Silver", + } + + org, repo, score, tier = extract_repo_info(assessment_data) + assert org == "org" + assert repo == "my-repo" + + def test_extract_repo_info_ssh_with_git_suffix(self): + """Test extract_repo_info handles SSH .git suffix correctly.""" + assessment_data = { + "repository": {"url": "git@github.com:feast-dev/feast.git"}, + "overall_score": 60.5, + "certification_level": "Silver", + } + + org, repo, score, tier = extract_repo_info(assessment_data) + assert org == "feast-dev" + assert repo == "feast" # Not "feas" + assert score == 60.5 + assert tier == "Silver" + + def test_extract_repo_info_ssh_without_git_suffix(self): + """Test extract_repo_info handles SSH URL without .git suffix.""" + assessment_data = { + "repository": {"url": "git@github.com:org/my-repo"}, + "overall_score": 90.0, + "certification_level": "Gold", + } + + org, repo, score, tier = extract_repo_info(assessment_data) + assert org == "org" + assert repo == "my-repo" + + def test_extract_repo_info_preserves_names_ending_in_git_chars(self): + """Regression test: repo names ending in .git characters are preserved. + + rstrip('.git') incorrectly strips individual characters (., g, i, t) + from the end of names. removesuffix('.git') is correct. + """ + test_cases = [ + ("feast-dev/feast.git", "feast-dev", "feast"), # Not "feas" + ("user/audit.git", "user", "audit"), # Not "aud" + ("user/digit.git", "user", "digit"), # Not "di" + ("org/widget.git", "org", "widget"), # Not "widge" + ] + + for url_suffix, expected_org, expected_repo in test_cases: + for url_template in [ + f"https://github.com/{url_suffix}", + f"git@github.com:{url_suffix}", + ]: + assessment_data = { + "repository": {"url": url_template}, + "overall_score": 80.0, + "certification_level": "Gold", + } + + org, repo, score, tier = extract_repo_info(assessment_data) + assert ( + org == expected_org + ), f"Expected org '{expected_org}' but got '{org}' for URL: {url_template}" + assert ( + repo == expected_repo + ), f"Expected repo '{expected_repo}' but got '{repo}' for URL: {url_template}" diff --git a/tests/unit/test_repository_manager.py b/tests/unit/test_repository_manager.py index 79b07b4c..59c5e6cb 100644 --- a/tests/unit/test_repository_manager.py +++ b/tests/unit/test_repository_manager.py @@ -119,3 +119,41 @@ def test_cleanup_nonexistent_repository(self): # Should return True even if directory doesn't exist assert success is True + + def test_get_repository_name_preserves_ending_letters(self): + """Test that repo names ending in .git characters are preserved. + + Regression test: rstrip('.git') incorrectly strips individual characters + (., g, i, t) from the end of names. removesuffix('.git') is correct. + """ + manager = RepositoryManager(Path("/tmp/cache")) + + # These were incorrectly truncated before the fix + assert ( + manager.get_repository_name_from_url( + "https://github.com/feast-dev/feast.git" + ) + == "feast" + ) # Not "feas" + + assert ( + manager.get_repository_name_from_url("https://github.com/user/audit.git") + == "audit" + ) # Not "aud" + + assert ( + manager.get_repository_name_from_url("https://github.com/user/digit.git") + == "digit" + ) # Not "di" + + def test_get_repository_name_from_url_with_trailing_slash(self): + """Test extracting repository name from URL with trailing slash.""" + manager = RepositoryManager(Path("/tmp/cache")) + name = manager.get_repository_name_from_url("https://github.com/user/my-repo/") + assert name == "my-repo" + + def test_get_repository_name_from_git_protocol_url(self): + """Test extracting repository name from git:// URL.""" + manager = RepositoryManager(Path("/tmp/cache")) + name = manager.get_repository_name_from_url("git://github.com/user/my-repo.git") + assert name == "my-repo"