name: CI/CD on: push: branches: [main] pull_request: branches: [main] jobs: pre-commit-check: name: Pre-commit checks (PR only) runs-on: ubuntu-latest if: github.event_name == 'pull_request' steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Cache pip (dev) uses: actions/cache@v4 with: path: ~/.cache/pip key: ${{ runner.os }}-pip-dev-${{ hashFiles('dev-requirements.txt') }} restore-keys: | ${{ runner.os }}-pip-dev- - name: Set up Python uses: actions/setup-python@v5 with: # ensure CI enforces modern Python versions python-version: "3.10" - name: Ensure setuptools is installed run: python -m pip install --upgrade pip setuptools wheel - name: Install dev dependencies run: | if [ -f dev-requirements.txt ]; then pip install -r dev-requirements.txt fi - name: Fetch origin/main for pre-commit diff run: git fetch origin main - name: Run pre-commit (PR changed files) run: | TO_REF="${{ github.event.pull_request.head.sha }}" pre-commit run --from-ref origin/main --to-ref $TO_REF --show-diff-on-failure build-and-test: name: Build and test runs-on: ubuntu-latest strategy: matrix: # Quote versions so YAML treats them as strings. Unquoted 3.10 can be parsed as # a float (3.1) which causes actions/setup-python to attempt to install the wrong # runtime. Use '3.10', '3.11', etc. # Note: Python 3.12 temporarily removed due to pkgutil.ImpImporter compatibility issues # with pinned dependency versions (numpy==1.24.3, chromadb==0.4.15) python-version: ["3.10", "3.11"] env: PYTHONPATH: ${{ github.workspace }} steps: - name: Checkout code uses: actions/checkout@v4 with: fetch-depth: 0 - name: Set up Python uses: actions/setup-python@v5 with: python-version: ${{ matrix.python-version }} - name: Ensure setuptools is installed run: python -m pip install --upgrade pip setuptools wheel - name: Install dependencies run: | pip install -r requirements.txt pip install pytest - name: Install linters and formatters run: | pip install black isort flake8 - name: Run linters and formatters (check-only) run: | # Check formatting black --check . # Check import sorting isort --check-only . # Run flake8 (fail the job on lint errors) and exclude virtualenv and dev tools flake8 --max-line-length=88 --exclude venv,dev-tools - name: Run tests run: | pytest deploy-to-render: name: Deploy to Render + Smoke Test runs-on: ubuntu-latest needs: build-and-test if: github.event_name == 'push' && github.ref == 'refs/heads/main' && !contains(github.event.head_commit.message, '[skip-deploy]') env: RENDER_API_KEY: ${{ secrets.RENDER_API_KEY }} RENDER_SERVICE_ID: ${{ secrets.RENDER_SERVICE_ID }} RENDER_SERVICE_URL: ${{ secrets.RENDER_SERVICE_URL }} steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Install jq (for JSON parsing) run: sudo apt-get update && sudo apt-get install -y jq - name: Trigger Render deploy id: trigger run: | set -e echo "Triggering deploy for Render service $RENDER_SERVICE_ID" response=$(curl -s -X POST \ "https://api.render.com/v1/services/${RENDER_SERVICE_ID}/deploys" \ -H "Authorization: Bearer ${RENDER_API_KEY}" \ -H "Content-Type: application/json" \ -d "{}") echo "response: $response" deploy_id=$(echo "$response" | jq -r '.id') if [ -z "$deploy_id" ] || [ "$deploy_id" = "null" ]; then echo "Failed to trigger deploy. Response:" echo "$response" exit 1 fi echo "deploy_id=$deploy_id" >> "$GITHUB_OUTPUT" - name: Wait for Render deploy to finish id: wait run: | set -e # Configurable constants MAX_RETRIES=120 INITIAL_DELAY=5 BACKOFF_STEP=10 BACKOFF_MAX=60 deploy_id="${{ steps.trigger.outputs.deploy_id }}" echo "Polling deploy status for $deploy_id..." retries=0 max_retries=$MAX_RETRIES delay=$INITIAL_DELAY while [ $retries -lt $max_retries ]; do resp=$(curl -s \ -H "Authorization: Bearer ${RENDER_API_KEY}" \ "https://api.render.com/v1/services/${RENDER_SERVICE_ID}/deploys/${deploy_id}") status=$(echo "$resp" | jq -r '.status') echo "Deploy status: $status" # Treat common Render success-like statuses as success so we proceed. # Examples observed: "success", "succeeded", "live", "ready". if echo "$status" | grep -E -i '^(success|succeeded|live|ready)$' >/dev/null 2>&1; then echo "Deploy succeeded (status=$status)" exit 0 fi # Treat common failure-like statuses as failure. if echo "$status" | grep -E -i '^(failed|error|failed_with_errors)$' >/dev/null 2>&1; then echo "Deploy failed; response:" echo "$resp" exit 1 fi sleep $delay retries=$((retries+1)) # exponential backoff: every $BACKOFF_STEP retries double delay up to ${BACKOFF_MAX}s if [ $((retries % BACKOFF_STEP)) -eq 0 ]; then delay=$((delay * 2)) if [ $delay -gt $BACKOFF_MAX ]; then delay=$BACKOFF_MAX fi fi done echo "Timed out waiting for deploy to finish" exit 1 - name: Post-deploy smoke test (check /health) run: | set -e # Configurable smoke test parameters SMOKE_TEST_MAX_RETRIES=12 SMOKE_TEST_DELAY=5 url="${{ env.RENDER_SERVICE_URL }}/health" echo "Checking $url" retries=0 max_retries=$SMOKE_TEST_MAX_RETRIES delay=$SMOKE_TEST_DELAY while [ $retries -lt $max_retries ]; do status_code=$(curl -s -o /dev/null -w "%{http_code}" "$url" || echo "000") echo "HTTP $status_code" if [ "$status_code" -eq 200 ]; then echo "Smoke test passed" exit 0 fi sleep $delay retries=$((retries+1)) done echo "Smoke test failed: $url did not return 200" exit 1 - name: Create deployed.md and open PR if: success() env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} RENDER_SERVICE_URL: ${{ env.RENDER_SERVICE_URL }} run: | set -e BRANCH_NAME="deploy-update-$(date +%s)" git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" git checkout -b $BRANCH_NAME echo "Live URL: ${RENDER_SERVICE_URL}" > deployed.md echo "Deployed at: $(date -u +%Y-%m-%dT%H:%M:%SZ)" >> deployed.md echo "Commit: $GITHUB_SHA" >> deployed.md git add deployed.md git commit -m "docs: update deployed.md after render deploy [skip-deploy]" git push --set-upstream origin $BRANCH_NAME # create PR using GitHub API PR_TITLE="chore: update deployed.md after deploy" PR_BODY="Automated update of deployed.md after successful deploy." PR_PAYLOAD=$(printf '{"title":"%s","head":"%s","base":"main","body":"%s"}' "$PR_TITLE" "$BRANCH_NAME" "$PR_BODY") curl -s -X POST \ -H "Authorization: token $GITHUB_TOKEN" \ -H "Accept: application/vnd.github.v3+json" \ "https://api.github.com/repos/${{ github.repository }}/pulls" \ -d "$PR_PAYLOAD"