Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
9369838
Merge pull request #10 from BuyWhere/fix/deploy-workflow-secret-names
BuyWhere Apr 30, 2026
d35fec0
Merge pull request #11 from BuyWhere/main
BuyWhere Apr 30, 2026
8e5de3a
fix(ci): push site image to Artifact Registry for Cloud Run deploy (B…
Apr 30, 2026
d5b9079
fix(ci): push site image to Artifact Registry for Cloud Run deploy (B…
BuyWhere Apr 30, 2026
a09c0fd
fix(api): add missing trackProductSearch and trackProductView exports
BuyWhere May 1, 2026
8a98df9
ci: add VM deploy workflow (BUY-6137)
BuyWhere May 1, 2026
828c8d6
fix(BUY-6224): add 308 redirect for /openapi.json → api.buywhere.ai/o…
BuyWhere May 1, 2026
d171e1e
fix(nginx-deploy): use sudo for nginx config writes (BUY-7088)
BuyWhere May 2, 2026
3cd5bfb
feat(currency): implement real currency conversion in app/currency.py…
Apr 30, 2026
bc8bc22
test(coverage): add unit test suite for core API endpoints — 86 tests…
Apr 30, 2026
2a8960f
fix(api): normalize region param to uppercase in /v1/products/search …
Apr 30, 2026
2e8815c
feat(growth): add aggregate_growth_metrics.py script for KPI baseline…
Apr 30, 2026
88058cb
feat(onboarding): improve developer onboarding flow (BUY-5088)
Apr 30, 2026
19b10cf
fix(BUY-6595): add /healthz Knative probe + 404 fallback to MCP server
May 2, 2026
5fe2277
fix(BUY-6595): switch MCP Cloud Run probes from /health to /healthz
May 2, 2026
a449d25
fix(BUY-6595): add GAR token docker login to VM deploy workflow
May 2, 2026
7a2dfa3
fix(BUY-6595): handle pre-existing unlabelled compose network on VM
May 2, 2026
abb3829
fix(BUY-6595): force-remove orphan containers before compose up on ne…
May 2, 2026
5a2a14c
fix(BUY-6595): also free any container holding port 8000 during netwo…
May 2, 2026
559ac6f
fix(BUY-6595): free port 8000 in normal deploy path before compose up
May 2, 2026
7cfffef
feat(BUY-6335): align pricing page with approved Pro tier (S$49/mo)
May 2, 2026
bfb88e9
fix(BUY-6595): add retry + docker logs capture on health check failure
May 2, 2026
b056942
feat(BUY-7690): build and publish @buywhere/mcp-server@0.1.6
May 2, 2026
1b7d903
ci(BUY-7474): add deploy-site-vm.yml to update local Next.js on produ…
May 2, 2026
cefc545
ci(BUY-7474): add VM diagnostic step to deploy-site-vm.yml
May 2, 2026
64c22e9
ci(BUY-7474): fix deploy-site-vm candidate dirs and add port-3006 checks
May 2, 2026
17788e3
ci(BUY-7474): use git reset --hard to sync VM site to origin/main
May 2, 2026
7ffe000
ci(BUY-7474): deeper diagnostic — check systemd services, sudo rules,…
May 2, 2026
8956ad8
ci(BUY-7474): fix build cleanup + nohup restart for buywhere-site VM …
May 2, 2026
df35122
ci(BUY-7474): build into .next-fresh to bypass root-owned .next-deplo…
May 2, 2026
87dee22
fix(BUY-7711): redirect /openapi.json to canonical api.buywhere.ai spec
May 2, 2026
b5eea27
ci(BUY-7711): fix deploy-site-vm.yml YAML — replace Python PYEOF here…
May 2, 2026
6b61a29
ci(BUY-7711): fix VM restart — kill by port when PID file is stale
May 2, 2026
f011c35
ci(BUY-7711): add nginx port-swap fallback when root process owns por…
May 2, 2026
ee206a4
ci(BUY-7711): fix set -e crash in restart — try port 3006 directly, f…
May 2, 2026
39f567c
ci(BUY-7663): fix nginx-deploy — resilient sudo fallback and systemct…
May 2, 2026
bf29da8
feat(BUY-8859): add weighted search_vector trigger — title(A) > brand…
May 3, 2026
f47fa21
feat(BUY-8819): add NL query preprocessor to search endpoint
May 3, 2026
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
70 changes: 63 additions & 7 deletions .github/workflows/deploy-api-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,13 @@ jobs:
docker build -t "$FULL_IMAGE" ./api
docker push "$FULL_IMAGE"

- name: Generate Artifact Registry auth token
id: gar_token
run: |
TOKEN=$(gcloud auth print-access-token)
echo "::add-mask::${TOKEN}"
echo "token=${TOKEN}" >> $GITHUB_OUTPUT

- name: Set up SSH
run: |
mkdir -p ~/.ssh
Expand All @@ -65,13 +72,17 @@ jobs:
SSH_HOST: ${{ secrets.PRODUCTION_DEPLOY_HOST }}
SSH_USER: ${{ secrets.PRODUCTION_DEPLOY_USER }}
APP_DIR: ${{ secrets.PRODUCTION_APP_DIR || '/opt/buywhere' }}
GAR_TOKEN: ${{ steps.gar_token.outputs.token }}
run: |
ssh -i ~/.ssh/id_ed25519 "${SSH_USER}@${SSH_HOST}" \
env FULL_IMAGE="${FULL_IMAGE}" IMAGE_TAG="${IMAGE_TAG}" APP_DIR="${APP_DIR}" \
env FULL_IMAGE="${FULL_IMAGE}" IMAGE_TAG="${IMAGE_TAG}" APP_DIR="${APP_DIR}" GAR_TOKEN="${GAR_TOKEN}" \
bash -s <<'REMOTE'
set -euo pipefail
echo "api-deploy: image=${FULL_IMAGE} tag=${IMAGE_TAG}"

# Authenticate Docker to Artifact Registry using CI-generated token
echo "${GAR_TOKEN}" | docker login -u oauth2accesstoken --password-stdin asia-southeast1-docker.pkg.dev

# Pull the new image
docker pull "${FULL_IMAGE}"

Expand All @@ -80,17 +91,62 @@ jobs:
# Tag as latest for docker-compose
docker tag "${FULL_IMAGE}" buywhere-api:latest

# Fix: if the default compose network exists without proper labels (pre-compose
# creation), docker compose up will fail. Detect and fix by doing a full restart.
PROJ=$(basename "${APP_DIR}")
NET="${PROJ}_default"
if docker network inspect "${NET}" >/dev/null 2>&1; then
NETLABEL=$(docker network inspect "${NET}" --format '{{index .Labels "com.docker.compose.network"}}' 2>/dev/null || true)
if [ -z "${NETLABEL}" ]; then
echo "Network ${NET} missing compose labels — performing full stack restart to fix"
docker compose down --remove-orphans 2>/dev/null || true
# Force-remove any containers that may still be running (orphans not in compose file)
docker ps -a --filter "name=${PROJ}-" --format "{{.Names}}" | xargs -r docker rm -f 2>/dev/null || true
# Also free any container holding port 8000 (may have a different name)
docker ps -q --filter "publish=8000" | xargs -r docker rm -f 2>/dev/null || true
docker network rm "${NET}" 2>/dev/null || true
docker compose up -d
echo "Full stack restarted — skipping targeted api restart"
# Jump straight to health check
echo "API container restarted — waiting for health check..."
sleep 15
HTTP="000"
for i in 1 2 3 4 5 6; do
HTTP=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 http://localhost:8000/health 2>/dev/null || echo "000")
echo "GET /health (attempt ${i}) → HTTP ${HTTP}"
if [[ "$HTTP" == "200" ]]; then break; fi
sleep 5
done
if [[ "$HTTP" != "200" ]]; then
echo "ERROR: API health check failed after retries (last HTTP ${HTTP})"
docker logs buywhere-api-1 --tail 50 2>/dev/null || true
exit 1
fi
echo "API healthy (${IMAGE_TAG})"
exit 0
fi
fi

# Free port 8000 if occupied by a stale/mismatched container
docker ps -q --filter "publish=8000" | xargs -r docker rm -f 2>/dev/null || true

# Restart only the api service (zero-downtime: compose will replace container)
docker compose up -d --no-deps api

echo "API container restarted — waiting for health check..."
sleep 5

# Verify health
HTTP=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:8000/health)
echo "GET /health → HTTP ${HTTP}"
sleep 10

# Verify health with retry (API may take time to initialize DB/Redis connections)
HTTP="000"
for i in 1 2 3 4 5 6; do
HTTP=$(curl -s -o /dev/null -w "%{http_code}" --max-time 10 http://localhost:8000/health 2>/dev/null || echo "000")
echo "GET /health (attempt ${i}) → HTTP ${HTTP}"
if [[ "$HTTP" == "200" ]]; then break; fi
sleep 5
done
if [[ "$HTTP" != "200" ]]; then
echo "ERROR: API health check failed with ${HTTP}"
echo "ERROR: API health check failed after retries (last HTTP ${HTTP})"
docker logs buywhere-api-1 --tail 50 2>/dev/null || true
exit 1
fi
echo "API healthy (${IMAGE_TAG})"
Expand Down
123 changes: 123 additions & 0 deletions .github/workflows/deploy-python-api-vm.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
name: Deploy Python API to Production VM

on:
workflow_dispatch:
inputs:
branch:
description: 'Branch to deploy'
required: false
default: 'release'
type: string

permissions:
contents: read

env:
APP_DIR: /srv/buywhere-api

jobs:
deploy:
name: Deploy to Production VM
runs-on: ubuntu-latest
environment: production
steps:
- name: Set up SSH agent
uses: webfactory/ssh-agent@v0.9.0
with:
ssh-private-key: ${{ secrets.PRODUCTION_DEPLOY_SSH_KEY }}

- name: Trust production host
run: |
mkdir -p ~/.ssh
ssh-keyscan -p "${{ secrets.PRODUCTION_DEPLOY_PORT || 22 }}" -H "${{ secrets.PRODUCTION_DEPLOY_HOST }}" >> ~/.ssh/known_hosts

- name: Pull, migrate, restart
env:
DEPLOY_HOST: ${{ secrets.PRODUCTION_DEPLOY_HOST }}
DEPLOY_PORT: ${{ secrets.PRODUCTION_DEPLOY_PORT || 22 }}
DEPLOY_USER: ${{ secrets.PRODUCTION_DEPLOY_USER }}
BRANCH: ${{ inputs.branch || 'release' }}
run: |
ssh -p "$DEPLOY_PORT" "$DEPLOY_USER@$DEPLOY_HOST" "BRANCH=$BRANCH bash -s" << 'ENDSSH'
set -euo pipefail
echo "=== BuyWhere Python API deploy starting ==="

# Find the app directory
APP_DIR=""
for candidate in /srv/buywhere-api /srv/buywhere /opt/buywhere-api /opt/buywhere /home/ubuntu/buywhere /home/buywhere; do
if [ -f "$candidate/app/main.py" ]; then
APP_DIR="$candidate"
break
fi
done

if [ -z "$APP_DIR" ]; then
echo "ERROR: Could not find app/main.py in any candidate directory"
exit 1
fi
echo "App directory: $APP_DIR"

cd "$APP_DIR"

# Pull latest code
echo "=== git pull origin $BRANCH ==="
git fetch origin
git checkout "$BRANCH"
git pull origin "$BRANCH"
echo "HEAD: $(git log --oneline -1)"

# Run any pending Alembic migrations
echo "=== alembic upgrade head ==="
if command -v alembic &>/dev/null; then
alembic upgrade head
elif [ -f ".venv/bin/alembic" ]; then
.venv/bin/alembic upgrade head
elif [ -f "venv/bin/alembic" ]; then
venv/bin/alembic upgrade head
else
echo "WARNING: alembic not found in PATH or venv — skipping migration"
fi

# Restart service
echo "=== restarting service ==="
if systemctl list-unit-files 2>/dev/null | grep -qE 'buywhere-api|buywhere\.service'; then
SERVICE=$(systemctl list-units --type=service --all 2>/dev/null | grep -iE 'buywhere-api|buywhere' | awk '{print $1}' | head -1)
echo "Restarting systemd service: $SERVICE"
systemctl restart "$SERVICE"
sleep 3
systemctl is-active "$SERVICE" && echo "Service is active" || echo "WARNING: service may not be running"
elif command -v supervisorctl &>/dev/null && supervisorctl status 2>/dev/null | grep -qiE 'buywhere|uvicorn'; then
PROC=$(supervisorctl status | grep -iE 'buywhere|uvicorn' | awk '{print $1}' | head -1)
echo "Restarting supervisor process: $PROC"
supervisorctl restart "$PROC"
elif command -v pm2 &>/dev/null && pm2 list 2>/dev/null | grep -qiE 'buywhere|api'; then
PROC=$(pm2 list | grep -iE 'buywhere|api' | awk '{print $2}' | head -1)
echo "Restarting PM2 process: $PROC"
pm2 restart "$PROC"
else
echo "WARNING: Could not detect service manager — service must be restarted manually"
fi

echo "=== Deploy complete ==="
ENDSSH

- name: Health check
env:
DEPLOY_HOST: ${{ secrets.PRODUCTION_DEPLOY_HOST }}
DEPLOY_PORT: ${{ secrets.PRODUCTION_DEPLOY_PORT || 22 }}
DEPLOY_USER: ${{ secrets.PRODUCTION_DEPLOY_USER }}
run: |
echo "Waiting 10s for service to come up..."
sleep 10
# Check health endpoint
for i in 1 2 3 4 5; do
HTTP=$(curl -s -o /dev/null -w '%{http_code}' --max-time 10 https://api.buywhere.ai/health || echo "000")
if [ "$HTTP" = "200" ]; then
echo "Health check passed (attempt $i) — HTTP $HTTP"
exit 0
fi
echo "Health check attempt $i: HTTP $HTTP"
sleep 5
done
echo "WARNING: Health check did not return 200 — check service manually"
exit 1
47 changes: 33 additions & 14 deletions .github/workflows/deploy-site-production.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,37 +21,53 @@ concurrency:

permissions:
contents: read
packages: write
id-token: write

env:
REGISTRY: ghcr.io
GCP_PROJECT_ID: gaia-calendar-488606
GCP_REGION: asia-southeast1
CLOUD_RUN_SERVICE: buywhere-site-production
AR_REPO: buywhere
AR_IMAGE: asia-southeast1-docker.pkg.dev/gaia-calendar-488606/buywhere/site

jobs:
build:
name: Build Site Docker Image
name: Build and Push Site Image
runs-on: ubuntu-latest
environment: production
outputs:
image_tag: ${{ steps.image_meta.outputs.image_tag }}
steps:
- uses: actions/checkout@v4

- name: Set lowercase image name and tag
id: image_meta
- name: Authenticate to Google Cloud
uses: google-github-actions/auth@v2
with:
credentials_json: ${{ secrets.GCP_SA_KEY_PROD || secrets.GCP_SA_KEY }}

- name: Set up Cloud SDK
uses: google-github-actions/setup-gcloud@v2

- name: Configure Docker for Artifact Registry
run: gcloud auth configure-docker ${{ env.GCP_REGION }}-docker.pkg.dev --quiet

- name: Ensure Artifact Registry repository exists
run: |
REPO_LOWER="${GITHUB_REPOSITORY,,}"
echo "image_name=ghcr.io/${REPO_LOWER}-site" >> "$GITHUB_OUTPUT"
echo "image_tag=ghcr.io/${REPO_LOWER}-site:sha-${GITHUB_SHA}" >> "$GITHUB_OUTPUT"
gcloud artifacts repositories describe ${{ env.AR_REPO }} \
--location=${{ env.GCP_REGION }} \
--project=${{ env.GCP_PROJECT_ID }} \
--format="value(name)" 2>/dev/null || \
gcloud artifacts repositories create ${{ env.AR_REPO }} \
--repository-format=docker \
--location=${{ env.GCP_REGION }} \
--project=${{ env.GCP_PROJECT_ID }} \
--description="BuyWhere container images"

- uses: docker/setup-buildx-action@v3
- name: Set image tag
id: image_meta
run: echo "image_tag=${{ env.AR_IMAGE }}:sha-${GITHUB_SHA}" >> "$GITHUB_OUTPUT"

- uses: docker/login-action@v3
with:
registry: ${{ env.REGISTRY }}
username: ${{ github.actor }}
password: ${{ secrets.GITHUB_TOKEN }}
- uses: docker/setup-buildx-action@v3

- name: Build and push site image
uses: docker/build-push-action@v5
Expand Down Expand Up @@ -110,3 +126,6 @@ jobs:
HTTP=$(curl -sf -o /dev/null -w "%{http_code}" \
"${{ steps.deploy.outputs.url }}" --max-time 15 || echo "000")
echo "Cloud Run site health: HTTP ${HTTP}"
if [[ "$HTTP" != "200" ]]; then
echo "WARN: Cloud Run returned ${HTTP} — check service logs"
fi
Loading