diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..d21f26d --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,15 @@ +# See https://docs.github.com/en/repositories/managing-your-repositorys-settings-and-security/customizing-your-repository/about-code-owners +# Order matters — the last matching rule wins. + +# Default reviewers. +* @netresearch/netresearch @netresearch/typo3 + +# CI / release / security automation. +/.github/ @netresearch/netresearch + +# Python package + audit. +/cli_audit/ @netresearch/netresearch +/audit.py @netresearch/netresearch + +# Installer / upgrade shell scripts. +/scripts/ @netresearch/netresearch diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..7ae0d3a --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,37 @@ + + +## Summary + + + +## Linked issues / PRs + + + +## Type of change + +- [ ] Bug fix (`fix:`) +- [ ] New feature (`feat:`) +- [ ] Refactor / internal change (`refactor:`) +- [ ] Docs only (`docs:`) +- [ ] Chore / CI / deps (`chore:`) +- [ ] Breaking change (describe migration below) + +## Test plan + + + +- [ ] `uv run pytest` +- [ ] `uv run python -m flake8 cli_audit tests` +- [ ] `./scripts/test_smoke.sh` +- [ ] Manual scenario: … + +## Checklist + +- [ ] Conventional Commits (`type(scope): description`) +- [ ] Commits signed (`git commit -S --signoff`) +- [ ] `CHANGELOG.md` updated for user-visible changes +- [ ] Relevant `AGENTS.md` files updated if structure / commands changed +- [ ] No secrets, credentials, or PII committed diff --git a/.github/workflows/auto-merge-deps.yml b/.github/workflows/auto-merge-deps.yml index 9fd8954..b14fb2e 100644 --- a/.github/workflows/auto-merge-deps.yml +++ b/.github/workflows/auto-merge-deps.yml @@ -1,35 +1,13 @@ name: Auto-merge dependency PRs on: - pull_request_target: - types: [opened, synchronize, reopened] + pull_request: -permissions: - contents: write - pull-requests: write +permissions: {} jobs: auto-merge: - runs-on: ubuntu-latest - if: >- - github.event.pull_request.user.login == 'dependabot[bot]' || - github.event.pull_request.user.login == 'renovate[bot]' - steps: - - name: Approve PR - env: - PR_URL: ${{ github.event.pull_request.html_url }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: gh pr review --approve "$PR_URL" - - - name: Enable auto-merge - env: - PR_URL: ${{ github.event.pull_request.html_url }} - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - REPO: ${{ github.repository }} - run: | - STRATEGY=$(gh api "repos/$REPO" --jq ' - if .allow_squash_merge then "--squash" - elif .allow_merge_commit then "--merge" - elif .allow_rebase_merge then "--rebase" - else "--merge" end') - gh pr merge --auto "$STRATEGY" "$PR_URL" + uses: netresearch/.github/.github/workflows/auto-merge-deps.yml@main + permissions: + contents: write + pull-requests: write diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a475f35..b6cfa98 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -75,7 +75,8 @@ jobs: - name: Upload coverage to Codecov uses: codecov/codecov-action@57e3a136b779b570ffcdbf80b3bdc90e7fab3de2 # v6.0.0 with: - file: ./coverage.xml + token: ${{ secrets.CODECOV_TOKEN }} + files: ./coverage.xml flags: unittests name: codecov-${{ matrix.os }}-py${{ matrix.python-version }} fail_ci_if_error: false diff --git a/.github/workflows/dependency-review.yml b/.github/workflows/dependency-review.yml new file mode 100644 index 0000000..18834ab --- /dev/null +++ b/.github/workflows/dependency-review.yml @@ -0,0 +1,16 @@ +name: Dependency Review + +on: + pull_request: + branches: [main] + +permissions: {} + +jobs: + dependency-review: + uses: netresearch/.github/.github/workflows/dependency-review.yml@main + permissions: + contents: read + pull-requests: write + with: + fail-on-severity: moderate diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml new file mode 100644 index 0000000..82f6bc2 --- /dev/null +++ b/.github/workflows/security.yml @@ -0,0 +1,23 @@ +name: Security + +on: + push: + branches: [main] + pull_request: + branches: [main] + schedule: + - cron: '0 6 * * 1' + workflow_dispatch: + +permissions: {} + +jobs: + audit: + # Pinned to feat/python-audit-workflow until netresearch/.github#19 merges. + # Switch to @main after merge. + uses: netresearch/.github/.github/workflows/python-audit.yml@feat/python-audit-workflow + permissions: + contents: read + with: + python-version-file: pyproject.toml + package-manager: uv diff --git a/.gitignore b/.gitignore index 583f861..3f353a3 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,9 @@ tools_snapshot.json # Node.js node_modules/ +# Vendored dependencies (this project uses uv; vendor/ is reserved for ad-hoc checkouts) +vendor/ + # AI agent session context (not committed) claudedocs/ .serena/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..74ab571 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,47 @@ +# See https://pre-commit.com for usage. +# +# Install: uv run pre-commit install +# Run all: uv run pre-commit run --all-files + +repos: + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v6.0.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-json + - id: check-added-large-files + - id: check-merge-conflict + - id: mixed-line-ending + args: ['--fix=lf'] + + - repo: https://github.com/PyCQA/flake8 + rev: 7.3.0 + hooks: + - id: flake8 + files: ^(cli_audit/|tests/|audit\.py$) + + - repo: https://github.com/PyCQA/isort + rev: 8.0.1 + hooks: + - id: isort + files: ^(cli_audit/|tests/|audit\.py$) + args: ['--profile=black'] + + - repo: https://github.com/psf/black + rev: 26.3.1 + hooks: + - id: black + files: ^(cli_audit/|tests/|audit\.py$) + + - repo: https://github.com/koalaman/shellcheck-precommit + rev: v0.11.0 + hooks: + - id: shellcheck + files: ^scripts/.*\.sh$ + args: ['--severity=warning'] + +ci: + autofix_commit_msg: 'chore: pre-commit autofixes' + autoupdate_commit_msg: 'chore(deps): pre-commit autoupdate' diff --git a/AGENTS.md b/AGENTS.md index 6d1296c..1ff6c89 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -13,7 +13,7 @@ - Ask before: heavy deps, full rewrites, breaking changes - Never commit secrets, PII, or credentials -## Package management (uv) +## Setup **This project uses [uv](https://docs.astral.sh/uv/) for package management.** Always use `uv run` to execute Python commands. @@ -29,7 +29,11 @@ uv run python -m flake8 # Run linter **Why uv?** Fast dependency resolution, proper lockfile support, and isolated environments without manual venv activation. -## Minimal pre-commit checks +**Prerequisites:** Python ≥ 3.14, `uv`, and a POSIX shell (most installer scripts are Bash). + +## Development + +Minimal pre-commit checks (also enforced by `.pre-commit-config.yaml`): ```bash uv run python -m pytest # All tests (required) @@ -39,6 +43,21 @@ uv run python -m mypy cli_audit # mypy (optional) uv run python audit.py --help # Verify CLI works ``` +Install the git hooks once per checkout: `uv run pre-commit install`. +New features and bug fixes go on a feature branch (`fix/…`, `feat/…`, `chore/…`) → PR against `main` → signed commits (`git commit -S --signoff`). +See [`SECURITY.md`](./SECURITY.md) for vulnerability reporting and [`CHANGELOG.md`](./CHANGELOG.md) for release history. + +## Testing + +```bash +uv run pytest # Full suite (546 tests) +uv run pytest -x -q # Fail-fast, quiet +uv run pytest tests/test_upgrade.py -k multi_version # Focused run +uv run pytest --cov=cli_audit --cov-report=term # With coverage +``` + +Test layout and conventions: see [`tests/AGENTS.md`](./tests/AGENTS.md). Integration suite under `tests/integration/` is collected separately — CI runs it as its own step. + ## Project maintenance vs. tool feature This repo **is** a CLI tool manager, so the word "upgrade" is overloaded: @@ -57,7 +76,7 @@ This repo **is** a CLI tool manager, so the word "upgrade" is overloaded: - [scripts/AGENTS.md](./scripts/AGENTS.md) — Installation scripts (Bash, 33 scripts) - [tests/AGENTS.md](./tests/AGENTS.md) — Test suite (pytest, 14 test files) -## Quick reference +## Commands (verified 2026-04-16) | Command | Purpose | |---------|---------| @@ -99,15 +118,28 @@ This repo **is** a CLI tool manager, so the word "upgrade" is overloaded: **Caches:** - `~/.cache/cli-audit/endoflife.json` — endoflife.date response cache, used as fallback on transient HTTP failures (override path via `CLI_AUDIT_ENDOFLIFE_CACHE`). -## Project overview +## Architecture **AI CLI Preparation v2.0** — Tool version auditing and installation management for AI coding agents. -- **Architecture:** 21 Python modules, 89 JSON tool catalogs +- **Modules:** 21 Python modules under `cli_audit/` (see [`cli_audit/AGENTS.md`](./cli_audit/AGENTS.md)) +- **Catalog:** 89 JSON tool definitions under `catalog/` +- **Installers:** 33 Bash scripts under `scripts/` (see [`scripts/AGENTS.md`](./scripts/AGENTS.md)) - **Phase 1:** Detection & auditing (complete) - **Phase 2:** Installation & upgrade management (complete) - **Entry point:** `audit.py` → `cli_audit` package +### Project structure + +``` +audit.py # CLI dispatcher (cmd_audit, cmd_update, cmd_install, …) +cli_audit/ # Core library (detection, collectors, snapshot, installer, …) +catalog/ # Per-tool JSON definitions (binary_name, version_command, multi_version) +scripts/ # Installer / upgrade / reconcile shell scripts + scripts/lib helpers +tests/ # pytest suite (unit + integration) +Makefile / Makefile.d/ # Task runner: `make audit`, `make upgrade`, `make upgrade-all` +``` + ## Multi-version runtimes Runtimes supporting multiple concurrent versions (PHP, Python, Node.js, Ruby, Go) use dynamic detection from [endoflife.date](https://endoflife.date/) API. @@ -125,3 +157,7 @@ CLI_AUDIT_JSON=1 uv run python audit.py --versions ## When instructions conflict Nearest AGENTS.md wins. User prompts override all files. + +--- + +*Last verified: 2026-04-16 (against `main` at HEAD).* diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..52804ad --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,28 @@ +# Changelog + +All notable changes to this project are documented in this file. + +The format is loosely based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), +and the project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + +## [Unreleased] + +### Added +- Governance files: PR template, `CHANGELOG.md`. Security reporting is covered by the [org-level SECURITY.md](https://github.com/netresearch/.github/blob/main/SECURITY.md). +- `.github/workflows/dependency-review.yml` and a dedicated `security.yml` (pip-audit + bandit + CycloneDX SBOM). CodeQL continues to run via GitHub's default code-scanning setup. +- `.pre-commit-config.yaml` for local hook enforcement (flake8, black, isort, shellcheck). +- README badges: CI, License. +- AGENTS.md structural sections (Commands, Setup, Testing, Architecture, Development). +- Per-cycle `auto_update` storage for multi-version tools (`python@3.13` vs `python@3.14`). +- Persistent endoflife.date cache at `~/.cache/cli-audit/endoflife.json` with fallback on HTTP failure. +- Binary-probe fallback in `guide.sh` when the post-install snapshot refresh is stale. + +### Fixed +- `cmd_update_local` in MERGE mode now refreshes multi-version cycle entries (`python@3.14`, …) instead of only the base-tool entry. Resolved false-negative "Upgrade did not succeed" messages after successful uv installs. + +### Changed +- Upgraded 23 locked Python dev-dependencies to latest compatible versions (bandit 1.9.4, mypy 1.20.1, isort 8.0.1, rich 15.0, coverage 7.13.5, …). + +## Prior history + +See [git log](https://github.com/netresearch/coding_agent_cli_toolset/commits/main) for commits prior to this changelog. Tagged releases: [Releases page](https://github.com/netresearch/coding_agent_cli_toolset/releases). diff --git a/README.md b/README.md index 8d29d29..816067d 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,12 @@ # AI CLI Preparation +[![CI](https://github.com/netresearch/coding_agent_cli_toolset/actions/workflows/ci.yml/badge.svg)](https://github.com/netresearch/coding_agent_cli_toolset/actions/workflows/ci.yml) +[![codecov](https://codecov.io/gh/netresearch/coding_agent_cli_toolset/branch/main/graph/badge.svg)](https://codecov.io/gh/netresearch/coding_agent_cli_toolset) +[![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE) + + + + A minimal utility to verify that tools used by AI coding agents are installed and up to date on your system. It audits versions of common agent toolchain CLIs against the latest upstream releases and prints a pipe-delimited report suitable for quick human scan or downstream tooling. ## Quick Start diff --git a/cli_audit/bulk.py b/cli_audit/bulk.py index 3b73aeb..25488b2 100644 --- a/cli_audit/bulk.py +++ b/cli_audit/bulk.py @@ -7,8 +7,10 @@ from __future__ import annotations +import os import shutil import subprocess +import tempfile import threading import time from concurrent.futures import ThreadPoolExecutor, as_completed @@ -350,7 +352,7 @@ def generate_rollback_script(results: Sequence[InstallResult], verbose: bool = F Path to generated rollback script """ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S") - script_path = f"/tmp/rollback_{timestamp}.sh" + script_path = os.path.join(tempfile.gettempdir(), f"rollback_{timestamp}.sh") with open(script_path, "w") as f: f.write("#!/bin/bash\n") diff --git a/cli_audit/detection.py b/cli_audit/detection.py index 1bc51cf..3520d2d 100644 --- a/cli_audit/detection.py +++ b/cli_audit/detection.py @@ -158,12 +158,15 @@ def get_version_line(path: str, tool_name: str, version_flag: str | None = None, Returns: Version line string or empty string """ - # Priority 1: If catalog specifies custom shell command, run it directly + # Priority 1: If catalog specifies custom shell command, run it directly. + # version_command comes from trusted catalog JSON (committed in-repo), never + # from user input — e.g. `uv python list --only-installed | grep … | sed …`. + # shell=True is required for the pipelines used in the catalog. if version_command: try: - proc = subprocess.run( + proc = subprocess.run( # nosec B602 version_command, - shell=True, + shell=True, # nosec B602 stdout=subprocess.PIPE, stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL, diff --git a/cli_audit/upgrade.py b/cli_audit/upgrade.py index cdd5d95..2dd465d 100644 --- a/cli_audit/upgrade.py +++ b/cli_audit/upgrade.py @@ -562,7 +562,8 @@ def cleanup_old_backups(retention_days: int = 7, verbose: bool = False): """ import glob - backup_dirs = glob.glob("/tmp/upgrade_backup_*") + # Mirror the location used by create_upgrade_backup() (tempfile.mkdtemp). + backup_dirs = glob.glob(os.path.join(tempfile.gettempdir(), "upgrade_backup_*")) cutoff = time.time() - (retention_days * 86400) for backup_dir in backup_dirs: diff --git a/pyproject.toml b/pyproject.toml index 1bc7a1c..caef3f3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -134,3 +134,14 @@ strict_equality = true [[tool.mypy.overrides]] module = ["yaml.*", "pytest.*", "setuptools.*"] ignore_missing_imports = true + +[tool.bandit] +# This project wraps CLI tools — every `subprocess` call is intentional. +# B101 (assert_used): tests use assert; enforced elsewhere by pytest. +# B404/B603: we import subprocess and run it with list-form args on purpose. +# B607 (start_process_with_partial_path): we rely on PATH resolution for +# user-installed CLIs by design. +# B310 (urllib_urlopen): URLs are hardcoded https:// or come from the +# committed catalog; user input never reaches urlopen(). +skips = ["B101", "B310", "B404", "B603", "B607"] +exclude_dirs = ["tests", ".venv", "build", "dist"] diff --git a/tests/test_bulk.py b/tests/test_bulk.py index b813e44..31dca63 100644 --- a/tests/test_bulk.py +++ b/tests/test_bulk.py @@ -495,7 +495,9 @@ def test_generate_rollback_script_cargo(self): script_path = generate_rollback_script([result]) assert os.path.exists(script_path) - assert script_path.startswith("/tmp/rollback_") + # Use tempfile.gettempdir() — on macOS this is /var/folders/..., not /tmp. + expected_prefix = os.path.join(tempfile.gettempdir(), "rollback_") + assert script_path.startswith(expected_prefix) assert script_path.endswith(".sh") # Check script content