diff --git a/.github/workflows/agent-memory-client.yml b/.github/workflows/agent-memory-client.yml index bef1f87..5f3fb24 100644 --- a/.github/workflows/agent-memory-client.yml +++ b/.github/workflows/agent-memory-client.yml @@ -31,17 +31,9 @@ jobs: working-directory: agent-memory-client run: uv sync --extra dev - - name: Lint with Ruff - working-directory: agent-memory-client - run: uv run ruff check agent_memory_client - - - name: Check formatting with Ruff formatter - working-directory: agent-memory-client - run: uv run ruff format --check agent_memory_client - - - name: Type check with mypy - working-directory: agent-memory-client - run: uv run mypy agent_memory_client + - name: Run pre-commit + run: | + uv run pre-commit run --all-files - name: Run tests working-directory: agent-memory-client diff --git a/.github/workflows/agent-memory-server.yml b/.github/workflows/agent-memory-server.yml new file mode 100644 index 0000000..45af6ed --- /dev/null +++ b/.github/workflows/agent-memory-server.yml @@ -0,0 +1,88 @@ +name: Agent Memory Server CI + +on: + push: + branches: [main] + tags: + - 'server/v*.*.*' + pull_request: + branches: [main] + +jobs: + build: + name: Build package + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Set up Python + uses: actions/setup-python@v4 + with: + python-version: '3.12' + + - name: Install build tools + run: | + python -m pip install --upgrade pip + pip install build + + - name: Build package + run: python -m build + + - name: Upload dist artifact + uses: actions/upload-artifact@v4 + with: + name: dist + path: dist/* + + publish-testpypi: + name: Publish to TestPyPI + needs: build + if: startsWith(github.ref, 'refs/tags/server/') && contains(github.ref, '-test') + runs-on: ubuntu-latest + environment: testpypi + permissions: + id-token: write + contents: read + steps: + - name: Download dist artifact + uses: actions/download-artifact@v4 + with: + name: dist + path: dist + + - name: Publish package to TestPyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + repository-url: https://test.pypi.org/legacy/ + packages-dir: dist/ + + publish-pypi: + name: Publish to PyPI + needs: build + if: startsWith(github.ref, 'refs/tags/server/') && !contains(github.ref, '-test') + runs-on: ubuntu-latest + environment: pypi + permissions: + id-token: write + contents: read + steps: + - name: Download dist artifact + uses: actions/download-artifact@v4 + with: + name: dist + path: dist + + - name: Publish package to PyPI + uses: pypa/gh-action-pypi-publish@release/v1 + with: + packages-dir: dist/ + +# Tag Format Guide: +# - For TestPyPI (testing): server/v1.0.0-test +# - For PyPI (production): server/v1.0.0 +# +# Use the script: python scripts/tag_and_push_server.py --test (for TestPyPI) +# python scripts/tag_and_push_server.py (for PyPI) +# +# This workflow uses PyPI Trusted Publishing (OIDC). Ensure the project is configured +# on PyPI to trust this GitHub repository before releasing. diff --git a/.github/workflows/python-tests.yml b/.github/workflows/python-tests.yml index a525521..b428ccc 100644 --- a/.github/workflows/python-tests.yml +++ b/.github/workflows/python-tests.yml @@ -3,6 +3,9 @@ name: Python Tests on: push: branches: [ main ] + tags: + - 'server/v*.*.*' + - 'client/v*.*.*' pull_request: branches: [ main ] diff --git a/agent_memory_server/__init__.py b/agent_memory_server/__init__.py index 7bb66b9..2d39331 100644 --- a/agent_memory_server/__init__.py +++ b/agent_memory_server/__init__.py @@ -1,3 +1,3 @@ """Redis Agent Memory Server - A memory system for conversational AI.""" -__version__ = "0.12.3" +__version__ = "0.12.4" diff --git a/scripts/tag_and_push_server.py b/scripts/tag_and_push_server.py new file mode 100644 index 0000000..4975c0e --- /dev/null +++ b/scripts/tag_and_push_server.py @@ -0,0 +1,174 @@ +#!/usr/bin/env python3 +""" +Script to tag and push agent-memory-server releases. + +This script: +1. Reads the current version from agent_memory_server/__init__.py +2. Creates a git tag in the format: server/v{version} +3. Pushes the tag to origin + +Usage: + python scripts/tag_and_push_server.py [--dry-run] [--force] [--test] +""" + +import argparse +import re +import subprocess +import sys +from pathlib import Path + + +def get_server_version() -> str: + """Read the version from agent_memory_server/__init__.py""" + init_file = Path("agent_memory_server/__init__.py") + + if not init_file.exists(): + raise FileNotFoundError(f"Could not find {init_file}") + + content = init_file.read_text() + + # Look for __version__ = "x.y.z" + version_match = re.search(r'__version__\s*=\s*["\']([^"\']+)["\']', content) + + if not version_match: + raise ValueError(f"Could not find __version__ in {init_file}") + + return version_match.group(1) + + +def run_command(cmd: list[str], dry_run: bool = False) -> subprocess.CompletedProcess: + """Run a command, optionally in dry-run mode.""" + print(f"Running: {' '.join(cmd)}") + + if dry_run: + print(" (dry-run mode - command not executed)") + return subprocess.CompletedProcess(cmd, 0, "", "") + + try: + result = subprocess.run(cmd, capture_output=True, text=True, check=True) + if result.stdout: + print(f" Output: {result.stdout.strip()}") + return result + except subprocess.CalledProcessError as e: + print(f" Error: {e.stderr.strip()}") + raise + + +def check_git_status(): + """Check if git working directory is clean.""" + try: + result = subprocess.run( + ["git", "status", "--porcelain"], capture_output=True, text=True, check=True + ) + if result.stdout.strip(): + print("Warning: Git working directory is not clean:") + print(result.stdout) + response = input("Continue anyway? (y/N): ") + if response.lower() != "y": + sys.exit(1) + except subprocess.CalledProcessError: + print("Error: Could not check git status") + sys.exit(1) + + +def tag_exists(tag_name: str) -> bool: + """Check if a tag already exists.""" + try: + subprocess.run( + ["git", "rev-parse", f"refs/tags/{tag_name}"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + check=True, + ) + return True + except subprocess.CalledProcessError: + return False + + +def main(): + parser = argparse.ArgumentParser( + description="Tag and push agent-memory-server release" + ) + parser.add_argument( + "--dry-run", + action="store_true", + help="Show what would be done without actually doing it", + ) + parser.add_argument( + "--force", + action="store_true", + help="Force tag creation even if tag already exists", + ) + parser.add_argument( + "--test", + action="store_true", + help="Add '-test' suffix to tag for TestPyPI deployment", + ) + + args = parser.parse_args() + + # Change to project root directory + script_dir = Path(__file__).parent + project_root = script_dir.parent + + try: + original_cwd = Path.cwd() + if project_root.resolve() != original_cwd.resolve(): + print(f"Changing to project root: {project_root}") + import os + + os.chdir(project_root) + except Exception as e: + print(f"Warning: Could not change to project root: {e}") + + try: + # Get the current version + version = get_server_version() + tag_suffix = "-test" if args.test else "" + tag_name = f"server/v{version}{tag_suffix}" + + print(f"Current server version: {version}") + print(f"Tag to create: {tag_name}") + print(f"Deployment target: {'TestPyPI' if args.test else 'PyPI (Production)'}") + + if not args.dry_run: + # Check git status + check_git_status() + + # Check if tag already exists + if tag_exists(tag_name): + if args.force: + print(f"Tag {tag_name} already exists, but --force specified") + run_command(["git", "tag", "-d", tag_name], args.dry_run) + else: + print( + f"Error: Tag {tag_name} already exists. Use --force to overwrite." + ) + sys.exit(1) + + # Create the tag + run_command(["git", "tag", tag_name], args.dry_run) + + # Push the tag + push_cmd = ["git", "push", "origin", tag_name] + if args.force: + push_cmd.insert(2, "--force") + + run_command(push_cmd, args.dry_run) + + print(f"\n✅ Successfully tagged and pushed {tag_name}") + + if not args.dry_run: + print("\nThis should trigger the GitHub Actions workflow for:") + if args.test: + print(" - TestPyPI publication (testing)") + else: + print(" - PyPI publication (production)") + + except Exception as e: + print(f"Error: {e}") + sys.exit(1) + + +if __name__ == "__main__": + main()