Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
---
name: Pull Request
about: Contribute a change to XMem
---

## Summary
<!-- What does this PR do? (1-3 sentences) -->


## Motivation / Problem
<!-- Why is this change needed? Link to the relevant issue. -->

Closes #<!-- issue number -->

## Changes
<!-- Bullet-point list of what you changed -->
-

## Testing
<!-- How did you verify this works? -->
- [ ] Unit tests added / updated (`pytest tests/unit`)
- [ ] Integration tests pass (`pytest tests/integration`)
- [ ] Tested manually — steps below:

```
# command to reproduce
```

## Screenshots / recordings (if UI change)
<!-- Drag & drop a screenshot or screen recording -->

## Checklist
- [ ] My PR title follows [Conventional Commits](https://www.conventionalcommits.org/) (`feat(scope): description`)
- [ ] I ran `ruff check .` and `black --check .` locally with no errors
- [ ] I updated `CHANGELOG.md` if this is a user-visible change
- [ ] I ran `uv lock` if I modified `pyproject.toml`
- [ ] Security-sensitive files modified? Pinged `@ishaanxgupta` or `@ved015`
162 changes: 162 additions & 0 deletions .github/workflows/api-schema-diff.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
# API diff check — detect breaking changes in OpenAPI schema on PRs.
# Compares the OpenAPI spec from the PR branch against `develop` and
# posts a diff comment so reviewers can see exactly what API surface changed.

name: API Schema Diff

on:
pull_request:
branches: [develop, main]
paths:
- "src/api/**"
- "src/schemas/**"

permissions:
contents: read
pull-requests: write

concurrency:
group: api-diff-${{ github.ref }}
cancel-in-progress: true

jobs:
diff:
name: Detect API breaking changes
runs-on: ubuntu-latest
timeout-minutes: 10

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: actions/setup-python@v5
with:
python-version: "3.11"
cache: pip

- name: Install dependencies
run: |
pip install -e ".[dev]"

- name: Generate OpenAPI spec (PR branch)
run: |
python -c "
import json, os
os.environ.setdefault('API_KEYS', '[\"test\"]')
os.environ.setdefault('JWT_SECRET_KEY', 'test')
os.environ.setdefault('PINECONE_API_KEY', 'test')
os.environ.setdefault('PINECONE_INDEX_NAME', 'test')
os.environ.setdefault('NEO4J_PASSWORD', 'test')
os.environ.setdefault('GEMINI_API_KEY', 'test')
os.environ.setdefault('MONGODB_URI', 'mongodb://127.0.0.1:1')
os.environ.setdefault('ENABLE_ANALYTICS', 'false')
os.environ.setdefault('ENABLE_PROMETHEUS', 'false')
from src.api.app import create_app
app = create_app()
spec = app.openapi()
with open('openapi-pr.json', 'w') as f:
json.dump(spec, f, indent=2)
" || echo '{}' > openapi-pr.json

- name: Generate OpenAPI spec (base branch)
run: |
git stash || true
git checkout ${{ github.event.pull_request.base.ref }}
python -c "
import json, os
os.environ.setdefault('API_KEYS', '[\"test\"]')
os.environ.setdefault('JWT_SECRET_KEY', 'test')
os.environ.setdefault('PINECONE_API_KEY', 'test')
os.environ.setdefault('PINECONE_INDEX_NAME', 'test')
os.environ.setdefault('NEO4J_PASSWORD', 'test')
os.environ.setdefault('GEMINI_API_KEY', 'test')
os.environ.setdefault('MONGODB_URI', 'mongodb://127.0.0.1:1')
os.environ.setdefault('ENABLE_ANALYTICS', 'false')
os.environ.setdefault('ENABLE_PROMETHEUS', 'false')
from src.api.app import create_app
app = create_app()
spec = app.openapi()
with open('openapi-base.json', 'w') as f:
json.dump(spec, f, indent=2)
" || echo '{}' > openapi-base.json
git checkout -

- name: Diff OpenAPI specs
id: diff
run: |
pip install deepdiff
python -c "
import json, sys
from deepdiff import DeepDiff

with open('openapi-base.json') as f:
base = json.load(f)
with open('openapi-pr.json') as f:
pr = json.load(f)

diff = DeepDiff(base, pr, ignore_order=True)

if not diff:
print('NO_CHANGES')
sys.exit(0)

# Detect breaking changes
breaking = []
added = []
changed = []

removed = diff.get('dictionary_item_removed', [])
for item in removed:
path = str(item)
if '/paths/' in path:
breaking.append(f'🔴 REMOVED: {path}')

new_items = diff.get('dictionary_item_added', [])
for item in new_items:
path = str(item)
if '/paths/' in path:
added.append(f'🟢 ADDED: {path}')

values_changed = diff.get('values_changed', {})
for path, change in values_changed.items():
changed.append(f'🟡 CHANGED: {path}')

print('---REPORT---')
if breaking:
print('### ⚠️ Breaking Changes')
for b in breaking:
print(f'- {b}')
if added:
print('### ✅ New Endpoints')
for a in added:
print(f'- {a}')
if changed:
print('### 🔄 Modified')
for c in changed[:20]:
print(f'- {c}')
" > api-diff-report.txt 2>&1 || true

cat api-diff-report.txt

- name: Post diff to PR
if: always()
uses: actions/github-script@v7
with:
script: |
const fs = require('fs');
let report = '';
try {
report = fs.readFileSync('api-diff-report.txt', 'utf8');
} catch { report = 'Could not generate API diff.'; }

if (report.includes('NO_CHANGES')) {
return; // No API changes, skip comment
}

await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: context.issue.number,
body: `## 🔍 API Schema Diff\n\n${report}\n\n---\n_Auto-generated by API Schema Diff workflow_`,
});
39 changes: 39 additions & 0 deletions .github/workflows/danger.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
name: Danger PR Review Bot

# Danger runs on the PR and posts a review comment with potential issues.
# It does NOT block the PR — it's purely advisory for the reviewer.

on:
pull_request:
branches: [main, master, develop]

permissions:
pull-requests: write
statuses: write

concurrency:
group: danger-${{ github.ref }}
cancel-in-progress: true

jobs:
danger:
name: Danger Review
runs-on: ubuntu-latest
timeout-minutes: 10

steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0

- uses: actions/setup-node@v4
with:
node-version: "20"

- name: Install Danger
run: npm install --save-dev danger

- name: Run Danger
run: npx danger ci
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
78 changes: 56 additions & 22 deletions .github/workflows/deploy-aws.yml
Original file line number Diff line number Diff line change
@@ -1,28 +1,18 @@
# Deploy XMem to an existing AWS EC2 instance when changes land on main.
# Deploy XMem to the PRODUCTION AWS EC2 instance when changes land on main.
#
# A merged PR produces a push to main — that is what triggers this workflow.
# The domain / DNS record does not move: it still points at the same instance;
# this job only updates the app on that machine (git pull + Docker restart, etc.).
# ⚠️ IMPORTANT: This should only fire from the promote-to-production workflow
# merging develop → main. Direct pushes to main should be blocked by branch
# protection rules (Settings → Branches → main → Require PR).
#
# Required GitHub repository secrets:
# EC2_HOST Public DNS, Elastic IP, or hostname (same host you use with
# `ssh -i your-key.pem ...` today).
# EC2_USER SSH user (e.g. ubuntu, ec2-user, admin).
# EC2_SSH_KEY The SAME private key as your AWS `.pem` file: open the PEM in a
# text editor and paste the entire contents into this secret —
# including the `-----BEGIN ... PRIVATE KEY-----` and `-----END...`
# lines. This is equivalent to `ssh -i key.pem` from your laptop.
# If the key has a passphrase, also add optional secret
# EC2_SSH_KEY_PASSPHRASE (see `passphrase` below).
# EC2_HOST Public DNS, Elastic IP, or hostname
# EC2_USER SSH user (e.g. ubuntu, ec2-user, admin)
# EC2_SSH_KEY Private key (same format as your .pem)
#
# Required repository Variable (Settings → Secrets and variables → Actions → Variables):
# EC2_DEPLOY_PATH Absolute path on the server where this repo is cloned
# (e.g. /home/ubuntu/xmem).
#
# The instance must be able to `git pull` from GitHub (deploy key or cached credentials)
# if the repo is private.
# Required repository Variable:
# EC2_DEPLOY_PATH Absolute path on the server (e.g. /home/ubuntu/xmem)

name: Deploy to AWS EC2
name: Deploy to Production

on:
push:
Expand All @@ -36,11 +26,21 @@ concurrency:
jobs:
deploy:
runs-on: ubuntu-latest
environment: EC2_HOST
environment:
name: production
url: ${{ vars.PRODUCTION_URL }}
permissions:
contents: read
deployments: write

steps:
- name: Create deployment
uses: chrnorm/deployment-action@v2
id: deployment
with:
token: ${{ secrets.GITHUB_TOKEN }}
environment: production

- name: Deploy over SSH
uses: appleboy/ssh-action@v1.2.2
with:
Expand All @@ -52,7 +52,41 @@ jobs:
script: |
set -euo pipefail
cd "${{ secrets.EC2_DEPLOY_PATH }}"

echo "── Pulling latest main ──"
git fetch origin main
git checkout main
git pull origin main
sudo systemctl restart xmem

echo "── Restarting XMem service ──"
sudo systemctl restart xmem

echo "── Waiting for health endpoint ──"
for i in $(seq 1 30); do
HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/health || true)
if [ "$HTTP_CODE" = "200" ]; then
echo "Health check passed (attempt $i)"
exit 0
fi
echo "Health check attempt $i: HTTP $HTTP_CODE — retrying in 10s"
sleep 10
done
echo "FATAL: Health check never returned 200 after 300s"
exit 1

- name: Deployment succeeded
if: success()
uses: chrnorm/deployment-status@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
state: success
environment-url: ${{ vars.PRODUCTION_URL }}

- name: Deployment failed
if: failure()
uses: chrnorm/deployment-status@v2
with:
token: ${{ secrets.GITHUB_TOKEN }}
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
state: failure
Loading
Loading