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
168 changes: 71 additions & 97 deletions .github/workflows/deploy-staging.yml
Original file line number Diff line number Diff line change
@@ -1,49 +1,47 @@
# ═══════════════════════════════════════════════════════════════════════════════
# Deploy XMem to the STAGING (UAT / canary) EC2 instance.
# Deploy PR to Staging for Review
#
# Triggered when:
# 1. A PR is merged into `develop` (automatic — the normal workflow)
# 2. A manual run via workflow_dispatch (for ad-hoc deploys)
# Triggered when a PR is opened or updated targeting `develop` or `main`.
# Deploys the PR branch to the staging server so reviewers can test it live.
#
# After a successful deploy the "Smoke Test Staging" workflow runs
# automatically to verify the app is alive and API contracts work.
# Flow:
# 1. PR opened/updated → deploy PR branch to staging
# 2. Smoke tests run against staging
# 3. Results posted as a comment on the PR
# 4. Reviewer tests on staging URL
# 5. If happy → comment `/promote` to merge to main & deploy to production
#
# ┌──────────────┐ merge ┌──────────┐ auto-deploy ┌──────────┐
# │ feature/fix │ ──────────► │ develop │ ────────────────► │ STAGING │
# └──────────────┘ └──────────┘ └──────────┘
#
# smoke tests run
#
# ┌────────▼────────┐
# │ You review on
# │ staging URL
# └────────┬────────┘
#
# promote-to-production │ (manual)
#
# ┌──────────────┐
# │ PRODUCTION │
# │ (main) │
# └──────────────┘
# ┌──────────────┐ PR opened ┌──────────┐ auto-deploy ┌──────────┐
# │ feature/fix │ ──────────► │ GitHub │ ─────────────► │ STAGING │
# └──────────────┘ └──────────┘ └──────────┘
# │
# smoke tests run
# │
# ┌────────▼────────┐
# │ Comment posted
# │ on the PR
# └────────┬────────┘
# │
# /promote comment │
# ▼
# ┌──────────────┐
# │ PRODUCTION │
# │ (main) │
# └──────────────┘
#
# ── Required secrets (Settings → Secrets → Actions) ──────────────────────────
# STAGING_EC2_HOST Public DNS / Elastic IP of the staging instance
# STAGING_EC2_USER SSH user (e.g. ubuntu)
# STAGING_EC2_SSH_KEY Private key (same format as your .pem)
# STAGING_EC2_DEPLOY_PATH Absolute path on the staging server (/home/ubuntu/xmem)
#
# Staging .env should have:
# - A separate Pinecone index (e.g. xmem-staging)
# - A separate MongoDB database (e.g. xmem_staging)
# - ENABLE_ANALYTICS=false
# - XMEM_ENV=staging
# STAGING_EC2_DEPLOY_PATH Absolute path on the staging server
# ═══════════════════════════════════════════════════════════════════════════════

name: Deploy to Staging
name: Deploy PR to Staging

on:
push:
branches: [develop]
pull_request:
types: [opened, synchronize, reopened]
branches: [develop, main]
workflow_dispatch:
inputs:
ref:
Expand All @@ -52,46 +50,25 @@ on:
default: develop

concurrency:
group: deploy-staging
group: deploy-staging-${{ github.event.pull_request.number || github.ref }}
cancel-in-progress: true # latest push wins — old staging deploy is stale

permissions:
contents: read
deployments: write
statuses: write
pull-requests: write
issues: write

jobs:
# ── 1. Build & validate Docker image ──────────────────────────────────────
validate-build:
name: Validate Docker build
runs-on: ubuntu-latest
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
with:
ref: ${{ github.event.inputs.ref || 'develop' }}

- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3

- name: Build Docker image (no push — validation only)
uses: docker/build-push-action@v5
with:
context: .
push: false
tags: xmem:staging-${{ github.sha }}
cache-from: type=gha
cache-to: type=gha,mode=max

# ── 2. Deploy to staging EC2 ──────────────────────────────────────────────
# ── 1. Deploy PR branch to staging EC2 ─────────────────────────────────────
deploy:
name: Deploy to staging EC2
runs-on: ubuntu-latest
needs: validate-build
timeout-minutes: 20
environment:
name: staging
url: ${{ vars.STAGING_URL }} # e.g. https://staging.xmem.bot
url: ${{ vars.STAGING_URL }}

steps:
- name: Create GitHub deployment
Expand All @@ -101,7 +78,7 @@ jobs:
token: ${{ secrets.GITHUB_TOKEN }}
environment: staging

- name: Deploy over SSH
- name: Deploy PR branch over SSH
uses: appleboy/ssh-action@v1.2.2
with:
host: ${{ secrets.STAGING_EC2_HOST }}
Expand All @@ -113,12 +90,14 @@ jobs:
set -euo pipefail
cd "${{ secrets.STAGING_EC2_DEPLOY_PATH }}"

echo "── Pulling latest develop ──"
git fetch origin develop
git checkout develop
git pull origin develop
PR_BRANCH="${{ github.head_ref || github.event.inputs.ref || 'develop' }}"
echo "── Deploying branch: $PR_BRANCH ──"

echo "── Restarting XMem service ──"
git fetch origin "$PR_BRANCH"
git checkout "$PR_BRANCH"
git pull origin "$PR_BRANCH"

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

echo "── Waiting for health endpoint ──"
Expand Down Expand Up @@ -151,7 +130,7 @@ jobs:
deployment-id: ${{ steps.deployment.outputs.deployment_id }}
state: failure

# ── 3. Smoke tests against live staging ───────────────────────────────────
# ── 2. Smoke tests against live staging ───────────────────────────────────
smoke-test:
name: Smoke test staging
runs-on: ubuntu-latest
Expand Down Expand Up @@ -208,45 +187,40 @@ jobs:
exit 1
fi

# ── Post deployment report as PR comment ──────────────────────────────
- name: Post result to PR
if: always()
if: always() && github.event_name == 'pull_request'
uses: actions/github-script@v7
with:
script: |
const status = '${{ job.status }}' === 'success' ? '✅' : '❌';
const url = '${{ vars.STAGING_URL }}';
const sha = context.sha.substring(0, 7);
const prNumber = context.payload.pull_request.number;
const branch = context.payload.pull_request.head.ref;

// Find any open PR targeting develop with this SHA
const { data: prs } = await github.rest.pulls.list({
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
state: 'closed',
base: 'develop',
sort: 'updated',
per_page: 5,
issue_number: prNumber,
body: [
`## ${status} Staging Deployment Report`,
'',
`| Item | Value |`,
`|------|-------|`,
`| **Branch** | \`${branch}\` |`,
`| **Commit** | \`${sha}\` |`,
`| **Environment** | [Staging](${url}) |`,
`| **Health** | ${url}/health |`,
`| **API Docs** | ${url}/docs |`,
`| **Smoke Tests** | ${{ job.status }} |`,
'',
status === '✅'
? [
'🟢 **Staging is live and healthy!** Test your changes at the staging URL above.',
'',
'**Ready to ship?** Comment `/promote` on this PR to merge to `main` and deploy to production.',
].join('\n')
: '🔴 Smoke tests failed. Check the [workflow run](../actions/runs/${{ github.run_id }}) for details.',
].join('\n'),
});

const matchedPr = prs.find(pr => pr.merge_commit_sha?.startsWith(context.sha.substring(0, 7)) || pr.merged_at);

if (matchedPr) {
await github.rest.issues.createComment({
owner: context.repo.owner,
repo: context.repo.repo,
issue_number: matchedPr.number,
body: [
`## ${status} Staging Deployment Report`,
'',
`| Item | Value |`,
`|------|-------|`,
`| **Commit** | \`${sha}\` |`,
`| **Environment** | [Staging](${url}) |`,
`| **Health** | ${url}/health |`,
`| **Smoke tests** | ${{ job.status }} |`,
'',
status === '✅'
? '🟢 Ready for review. Test at the staging URL above, then [promote to production](../actions/workflows/promote-to-production.yml).'
: '🔴 Smoke tests failed. Check the [workflow run](../actions/runs/${{ github.run_id }}) for details.',
].join('\n'),
});
}
Loading
Loading