Skip to content

Commit 4cd4137

Browse files
authored
Merge pull request #98 from redis/feat/publish-server-to-pypi-uvx
Add GHA workflow to publish server to PyPI
2 parents 63c3f76 + bf5122b commit 4cd4137

File tree

5 files changed

+269
-12
lines changed

5 files changed

+269
-12
lines changed

.github/workflows/agent-memory-client.yml

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -31,17 +31,9 @@ jobs:
3131
working-directory: agent-memory-client
3232
run: uv sync --extra dev
3333

34-
- name: Lint with Ruff
35-
working-directory: agent-memory-client
36-
run: uv run ruff check agent_memory_client
37-
38-
- name: Check formatting with Ruff formatter
39-
working-directory: agent-memory-client
40-
run: uv run ruff format --check agent_memory_client
41-
42-
- name: Type check with mypy
43-
working-directory: agent-memory-client
44-
run: uv run mypy agent_memory_client
34+
- name: Run pre-commit
35+
run: |
36+
uv run pre-commit run --all-files
4537
4638
- name: Run tests
4739
working-directory: agent-memory-client
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
name: Agent Memory Server CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
tags:
7+
- 'server/v*.*.*'
8+
pull_request:
9+
branches: [main]
10+
11+
jobs:
12+
build:
13+
name: Build package
14+
runs-on: ubuntu-latest
15+
steps:
16+
- uses: actions/checkout@v4
17+
18+
- name: Set up Python
19+
uses: actions/setup-python@v4
20+
with:
21+
python-version: '3.12'
22+
23+
- name: Install build tools
24+
run: |
25+
python -m pip install --upgrade pip
26+
pip install build
27+
28+
- name: Build package
29+
run: python -m build
30+
31+
- name: Upload dist artifact
32+
uses: actions/upload-artifact@v4
33+
with:
34+
name: dist
35+
path: dist/*
36+
37+
publish-testpypi:
38+
name: Publish to TestPyPI
39+
needs: build
40+
if: startsWith(github.ref, 'refs/tags/server/') && contains(github.ref, '-test')
41+
runs-on: ubuntu-latest
42+
environment: testpypi
43+
permissions:
44+
id-token: write
45+
contents: read
46+
steps:
47+
- name: Download dist artifact
48+
uses: actions/download-artifact@v4
49+
with:
50+
name: dist
51+
path: dist
52+
53+
- name: Publish package to TestPyPI
54+
uses: pypa/gh-action-pypi-publish@release/v1
55+
with:
56+
repository-url: https://test.pypi.org/legacy/
57+
packages-dir: dist/
58+
59+
publish-pypi:
60+
name: Publish to PyPI
61+
needs: build
62+
if: startsWith(github.ref, 'refs/tags/server/') && !contains(github.ref, '-test')
63+
runs-on: ubuntu-latest
64+
environment: pypi
65+
permissions:
66+
id-token: write
67+
contents: read
68+
steps:
69+
- name: Download dist artifact
70+
uses: actions/download-artifact@v4
71+
with:
72+
name: dist
73+
path: dist
74+
75+
- name: Publish package to PyPI
76+
uses: pypa/gh-action-pypi-publish@release/v1
77+
with:
78+
packages-dir: dist/
79+
80+
# Tag Format Guide:
81+
# - For TestPyPI (testing): server/v1.0.0-test
82+
# - For PyPI (production): server/v1.0.0
83+
#
84+
# Use the script: python scripts/tag_and_push_server.py --test (for TestPyPI)
85+
# python scripts/tag_and_push_server.py (for PyPI)
86+
#
87+
# This workflow uses PyPI Trusted Publishing (OIDC). Ensure the project is configured
88+
# on PyPI to trust this GitHub repository before releasing.

.github/workflows/python-tests.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@ name: Python Tests
33
on:
44
push:
55
branches: [ main ]
6+
tags:
7+
- 'server/v*.*.*'
8+
- 'client/v*.*.*'
69
pull_request:
710
branches: [ main ]
811

agent_memory_server/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,3 @@
11
"""Redis Agent Memory Server - A memory system for conversational AI."""
22

3-
__version__ = "0.12.3"
3+
__version__ = "0.12.4"

scripts/tag_and_push_server.py

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Script to tag and push agent-memory-server releases.
4+
5+
This script:
6+
1. Reads the current version from agent_memory_server/__init__.py
7+
2. Creates a git tag in the format: server/v{version}
8+
3. Pushes the tag to origin
9+
10+
Usage:
11+
python scripts/tag_and_push_server.py [--dry-run] [--force] [--test]
12+
"""
13+
14+
import argparse
15+
import re
16+
import subprocess
17+
import sys
18+
from pathlib import Path
19+
20+
21+
def get_server_version() -> str:
22+
"""Read the version from agent_memory_server/__init__.py"""
23+
init_file = Path("agent_memory_server/__init__.py")
24+
25+
if not init_file.exists():
26+
raise FileNotFoundError(f"Could not find {init_file}")
27+
28+
content = init_file.read_text()
29+
30+
# Look for __version__ = "x.y.z"
31+
version_match = re.search(r'__version__\s*=\s*["\']([^"\']+)["\']', content)
32+
33+
if not version_match:
34+
raise ValueError(f"Could not find __version__ in {init_file}")
35+
36+
return version_match.group(1)
37+
38+
39+
def run_command(cmd: list[str], dry_run: bool = False) -> subprocess.CompletedProcess:
40+
"""Run a command, optionally in dry-run mode."""
41+
print(f"Running: {' '.join(cmd)}")
42+
43+
if dry_run:
44+
print(" (dry-run mode - command not executed)")
45+
return subprocess.CompletedProcess(cmd, 0, "", "")
46+
47+
try:
48+
result = subprocess.run(cmd, capture_output=True, text=True, check=True)
49+
if result.stdout:
50+
print(f" Output: {result.stdout.strip()}")
51+
return result
52+
except subprocess.CalledProcessError as e:
53+
print(f" Error: {e.stderr.strip()}")
54+
raise
55+
56+
57+
def check_git_status():
58+
"""Check if git working directory is clean."""
59+
try:
60+
result = subprocess.run(
61+
["git", "status", "--porcelain"], capture_output=True, text=True, check=True
62+
)
63+
if result.stdout.strip():
64+
print("Warning: Git working directory is not clean:")
65+
print(result.stdout)
66+
response = input("Continue anyway? (y/N): ")
67+
if response.lower() != "y":
68+
sys.exit(1)
69+
except subprocess.CalledProcessError:
70+
print("Error: Could not check git status")
71+
sys.exit(1)
72+
73+
74+
def tag_exists(tag_name: str) -> bool:
75+
"""Check if a tag already exists."""
76+
try:
77+
subprocess.run(
78+
["git", "rev-parse", f"refs/tags/{tag_name}"],
79+
stdout=subprocess.DEVNULL,
80+
stderr=subprocess.DEVNULL,
81+
check=True,
82+
)
83+
return True
84+
except subprocess.CalledProcessError:
85+
return False
86+
87+
88+
def main():
89+
parser = argparse.ArgumentParser(
90+
description="Tag and push agent-memory-server release"
91+
)
92+
parser.add_argument(
93+
"--dry-run",
94+
action="store_true",
95+
help="Show what would be done without actually doing it",
96+
)
97+
parser.add_argument(
98+
"--force",
99+
action="store_true",
100+
help="Force tag creation even if tag already exists",
101+
)
102+
parser.add_argument(
103+
"--test",
104+
action="store_true",
105+
help="Add '-test' suffix to tag for TestPyPI deployment",
106+
)
107+
108+
args = parser.parse_args()
109+
110+
# Change to project root directory
111+
script_dir = Path(__file__).parent
112+
project_root = script_dir.parent
113+
114+
try:
115+
original_cwd = Path.cwd()
116+
if project_root.resolve() != original_cwd.resolve():
117+
print(f"Changing to project root: {project_root}")
118+
import os
119+
120+
os.chdir(project_root)
121+
except Exception as e:
122+
print(f"Warning: Could not change to project root: {e}")
123+
124+
try:
125+
# Get the current version
126+
version = get_server_version()
127+
tag_suffix = "-test" if args.test else ""
128+
tag_name = f"server/v{version}{tag_suffix}"
129+
130+
print(f"Current server version: {version}")
131+
print(f"Tag to create: {tag_name}")
132+
print(f"Deployment target: {'TestPyPI' if args.test else 'PyPI (Production)'}")
133+
134+
if not args.dry_run:
135+
# Check git status
136+
check_git_status()
137+
138+
# Check if tag already exists
139+
if tag_exists(tag_name):
140+
if args.force:
141+
print(f"Tag {tag_name} already exists, but --force specified")
142+
run_command(["git", "tag", "-d", tag_name], args.dry_run)
143+
else:
144+
print(
145+
f"Error: Tag {tag_name} already exists. Use --force to overwrite."
146+
)
147+
sys.exit(1)
148+
149+
# Create the tag
150+
run_command(["git", "tag", tag_name], args.dry_run)
151+
152+
# Push the tag
153+
push_cmd = ["git", "push", "origin", tag_name]
154+
if args.force:
155+
push_cmd.insert(2, "--force")
156+
157+
run_command(push_cmd, args.dry_run)
158+
159+
print(f"\n✅ Successfully tagged and pushed {tag_name}")
160+
161+
if not args.dry_run:
162+
print("\nThis should trigger the GitHub Actions workflow for:")
163+
if args.test:
164+
print(" - TestPyPI publication (testing)")
165+
else:
166+
print(" - PyPI publication (production)")
167+
168+
except Exception as e:
169+
print(f"Error: {e}")
170+
sys.exit(1)
171+
172+
173+
if __name__ == "__main__":
174+
main()

0 commit comments

Comments
 (0)