diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..6ecc5ba --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,46 @@ +name: CI + +on: + pull_request: + branches: [main] + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +jobs: + tests: + name: tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Install + run: | + python -m pip install -U pip + python -m pip install -e '.[mongo,postgres,mcp,dev]' + python -m pip install -e plugins/cfg_impact + - name: Run tests + run: python -m pytest -q + + package-build: + name: package-build + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Build and check distributions + run: | + python -m pip install -U build twine + rm -rf dist plugins/cfg_impact/dist + python -m build + python -m twine check dist/* + cd plugins/cfg_impact + python -m build + python -m twine check dist/* diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml new file mode 100644 index 0000000..87907a3 --- /dev/null +++ b/.github/workflows/publish.yml @@ -0,0 +1,56 @@ +name: Publish Python packages + +on: + release: + types: [published] + workflow_dispatch: + +permissions: + contents: read + +jobs: + publish-cfgit: + name: Publish cfgit + runs-on: ubuntu-latest + environment: pypi-cfgit + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Build + run: | + python -m pip install -U build twine + python -m build + python -m twine check dist/* + - name: Publish cfgit to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist/ + + publish-cfgit-impact: + name: Publish cfgit-impact + runs-on: ubuntu-latest + needs: publish-cfgit + environment: pypi-cfgit-impact + permissions: + id-token: write + contents: read + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: "3.11" + - name: Build + working-directory: plugins/cfg_impact + run: | + python -m pip install -U build twine + python -m build + python -m twine check dist/* + - name: Publish cfgit-impact to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: plugins/cfg_impact/dist/ diff --git a/README.md b/README.md index 58eee7b..d323681 100644 --- a/README.md +++ b/README.md @@ -66,7 +66,7 @@ cfgit is pre-1.0 software. The current implementation includes: - localhost web UI - MCP server - portable Codex or Claude Code skill -- optional `cfg-impact` plugin for deterministic impact summaries and opt-in LLM narration +- optional `cfgit-impact` plugin for deterministic impact summaries and opt-in LLM narration The engine is intentionally DB-neutral. Mongo and Postgres are the first two adapters to prove the storage seam. @@ -410,7 +410,7 @@ standardize the config so that database always uses one env name. paths, finds static references to changed values across configured records, and reports a risk level. -Optional LLM narration lives in the separate `cfg-impact` plugin. It reads the +Optional LLM narration lives in the separate `cfgit-impact` plugin. It reads the real before/after of the change plus a map of the surrounding records, then explains in plain language what the change does, what it ripples into, and how to roll it back: diff --git a/docs/PUBLISHING.md b/docs/PUBLISHING.md new file mode 100644 index 0000000..ae38710 --- /dev/null +++ b/docs/PUBLISHING.md @@ -0,0 +1,82 @@ +# Publishing + +cfgit publishes two Python packages: + +- `cfgit`: Git-style history, diff, drift detection, branch/PR review, and + rollback for live database records without migrating or owning the datastore. +- `cfgit-impact`: optional plugin for deterministic system-impact summaries and + opt-in LLM narration of database record diffs. + +Current first release version: `0.1.0`. + +## One-Time PyPI Setup + +Create both projects on PyPI and configure Trusted Publishing for this repository. + +For `cfgit`: + +- Owner: `AusafMo` +- Repository: `cfgit` +- Workflow: `publish.yml` +- Environment: `pypi-cfgit` + +For `cfgit-impact`: + +- Owner: `AusafMo` +- Repository: `cfgit` +- Workflow: `publish.yml` +- Environment: `pypi-cfgit-impact` + +The workflow uses PyPI OpenID Connect, so no PyPI API token is stored in GitHub +Secrets. + +## Local Build Check + +From the repository root: + +```bash +python -m pip install -U build twine +rm -rf dist plugins/cfg_impact/dist +python -m build +python -m twine check dist/* +cd plugins/cfg_impact +python -m build +python -m twine check dist/* +``` + +## Clean Install Smoke + +```bash +python -m venv /tmp/cfgit-publish-smoke +/tmp/cfgit-publish-smoke/bin/python -m pip install \ + 'dist/cfgit-0.1.0-py3-none-any.whl[mcp]' \ + plugins/cfg_impact/dist/cfgit_impact-0.1.0-py3-none-any.whl +/tmp/cfgit-publish-smoke/bin/cfg --help +/tmp/cfgit-publish-smoke/bin/python -c 'import cfg; import cfg.mcp.server; import cfg_impact; print("imports ok")' +``` + +## Publish + +After Trusted Publishing is configured on PyPI, publish by creating a GitHub +release from a tag: + +```bash +git checkout main +git pull origin main +git tag v0.1.0 +git push origin v0.1.0 +gh release create v0.1.0 --title "v0.1.0" --notes "First public cfgit release." +``` + +The release triggers `.github/workflows/publish.yml`, which publishes `cfgit` +first and `cfgit-impact` second. + +## Install + +```bash +pip install cfgit +pip install 'cfgit[mongo]' +pip install 'cfgit[postgres]' +pip install 'cfgit[mongo,postgres,mcp]' +pip install cfgit-impact +``` diff --git a/plugins/cfg_impact/pyproject.toml b/plugins/cfg_impact/pyproject.toml index a2b8b4e..e604643 100644 --- a/plugins/cfg_impact/pyproject.toml +++ b/plugins/cfg_impact/pyproject.toml @@ -1,13 +1,22 @@ [project] -name = "cfg-impact" -version = "0.0.0" -description = "Optional system-impact analysis plugin for cfgit" +name = "cfgit-impact" +version = "0.1.0" +description = "Optional cfgit plugin for deterministic system-impact summaries and opt-in LLM narration of database record diffs" +readme = "README.md" requires-python = ">=3.11" +license = "Apache-2.0" +authors = [{ name = "Mohammad Ausaf" }] dependencies = [ - "cfg-vcs", + "cfgit>=0.1.0,<0.2.0", "httpx>=0.27", ] +[project.urls] +Homepage = "https://github.com/AusafMo/cfgit" +Repository = "https://github.com/AusafMo/cfgit" +Issues = "https://github.com/AusafMo/cfgit/issues" +Documentation = "https://github.com/AusafMo/cfgit/tree/main/plugins/cfg_impact#readme" + [build-system] requires = ["hatchling"] build-backend = "hatchling.build" diff --git a/pyproject.toml b/pyproject.toml index 457173e..1cb5b2e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] -name = "cfg-vcs" -version = "0.0.0" -description = "Non-custodial version control for live datastore records" +name = "cfgit" +version = "0.1.0" +description = "Git-style history, diff, drift detection, and rollback for live database records without migrating or owning your datastore" readme = "README.md" requires-python = ">=3.11" license = { file = "LICENSE" } @@ -15,6 +15,9 @@ keywords = [ "postgres", "mcp", "agents", + "rollback", + "drift-detection", + "database-versioning", ] # Core has NO database driver and NO LLM SDK. That boundary is enforced in CI @@ -29,9 +32,15 @@ mongo = ["pymongo>=4.6"] postgres = ["psycopg[binary]>=3.2"] cli = ["click>=8.1", "rich>=13.0"] mcp = ["mcp>=1.0"] -impact = [] # the cfg-impact plugin declares its own model-client deps; never in core +impact = [] # the cfgit-impact plugin declares its own model-client deps; never in core dev = ["pytest>=8.0", "pytest-asyncio", "ruff", "mypy", "httpx>=0.27"] +[project.urls] +Homepage = "https://github.com/AusafMo/cfgit" +Repository = "https://github.com/AusafMo/cfgit" +Issues = "https://github.com/AusafMo/cfgit/issues" +Documentation = "https://github.com/AusafMo/cfgit#readme" + [project.scripts] cfg = "cfg.cli.main:main" cfg-mcp = "cfg.mcp.server:main" diff --git a/src/cfg/adapters/mongo.py b/src/cfg/adapters/mongo.py index a806a1e..e94b166 100644 --- a/src/cfg/adapters/mongo.py +++ b/src/cfg/adapters/mongo.py @@ -27,7 +27,7 @@ from pymongo.client_session import ClientSession from pymongo.errors import OperationFailure except ModuleNotFoundError as exc: # pragma: no cover - raise ModuleNotFoundError("install cfg-vcs[mongo] to use MongoAdapter") from exc + raise ModuleNotFoundError("install cfgit[mongo] to use MongoAdapter") from exc class MongoAdapter: diff --git a/src/cfg/adapters/postgres.py b/src/cfg/adapters/postgres.py index 1371f85..7ed33a7 100644 --- a/src/cfg/adapters/postgres.py +++ b/src/cfg/adapters/postgres.py @@ -31,7 +31,7 @@ from psycopg.rows import dict_row from psycopg.types.json import Jsonb except ModuleNotFoundError as exc: # pragma: no cover - raise ModuleNotFoundError("install cfg-vcs[postgres] to use PostgresAdapter") from exc + raise ModuleNotFoundError("install cfgit[postgres] to use PostgresAdapter") from exc _IDENT_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_]*$") diff --git a/src/cfg/interfaces/actions.py b/src/cfg/interfaces/actions.py index 28b2c9d..71488fe 100644 --- a/src/cfg/interfaces/actions.py +++ b/src/cfg/interfaces/actions.py @@ -309,7 +309,7 @@ def impact( from cfg_impact.overview import overview except ModuleNotFoundError as exc: raise ValueError( - "cfg-impact plugin is not installed. Install plugins/cfg_impact or cfg-vcs[impact]." + "cfgit-impact plugin is not installed. Install plugins/cfg_impact or cfgit[impact]." ) from exc return ( overview(engine, record, a=a, b=b, use_llm=use_llm, provider=provider, model=model, against=against), diff --git a/src/cfg/mcp/server.py b/src/cfg/mcp/server.py index eb09d64..cb6addd 100644 --- a/src/cfg/mcp/server.py +++ b/src/cfg/mcp/server.py @@ -8,7 +8,7 @@ from cfg.interfaces import actions from cfg.interfaces.actions import ActionContext -try: # pragma: no cover - exercised when cfg-vcs[mcp] is installed +try: # pragma: no cover - exercised when cfgit[mcp] is installed from mcp.server.fastmcp import FastMCP except ModuleNotFoundError: # pragma: no cover FastMCP = None # type: ignore[assignment] @@ -16,7 +16,7 @@ def _mcp() -> Any: if FastMCP is None: - raise ModuleNotFoundError("install cfg-vcs[mcp] to run the cfgit MCP server") + raise ModuleNotFoundError("install cfgit[mcp] to run the cfgit MCP server") return FastMCP("cfgit")