Skip to content
Open
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
113 changes: 77 additions & 36 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
@@ -1,11 +1,39 @@
name: Deploy API to Render

# Manual, deterministic deploys only.
#
# Automatic push-triggered deploys are intentionally NOT enabled: the staging/
# production Render services, database, and secrets must exist first. Deploying on
# every push/merge before that infrastructure is provisioned would fail. Once
# maintainers confirm the Render staging/prod setup, a `push:` trigger can be
# re-added if desired.
on:
workflow_dispatch: # allows manual trigger from GitHub UI
workflow_dispatch:
inputs:
environment:
description: Target Render environment
required: true
type: choice
options:
- staging
- production
run_smoke_tests:
description: Run smoke tests after deploy
required: true
default: true
type: boolean

# Never let two deploys of the same environment race each other.
concurrency:
group: deploy-${{ inputs.environment }}
cancel-in-progress: false

permissions:
contents: read

jobs:
deploy:
name: Deploy to Render
name: Deploy to Render (${{ inputs.environment }})
runs-on: ubuntu-latest

steps:
Expand All @@ -31,31 +59,46 @@ jobs:
python -m pip install --upgrade pip
pip install -r requirements.txt

# ── Secret check (Determines if smoke tests should run) ───────────
- name: Check for JWT_SECRET
id: check_config
# ── Deterministic Render deploy ────────────────────────────────────
# Triggers a deploy of the chosen environment's service pinned to the
# dispatched commit SHA and blocks until Render reports that specific
# deploy live (or fails). No fixed sleeps: the step's exit code is Render's
# real deploy status.
- name: Deploy to Render and wait for it to go live
env:
RENDER_API_KEY: ${{ secrets.RENDER_API_KEY }}
# Both ids are exposed; the correct one is chosen strictly by the
# selected environment below (no cross-environment fallback).
RENDER_STAGING_SERVICE_ID: ${{ secrets.RENDER_STAGING_SERVICE_ID }}
RENDER_PRODUCTION_SERVICE_ID: ${{ secrets.RENDER_PRODUCTION_SERVICE_ID }}
GITHUB_SHA: ${{ github.sha }}
run: |
if [ -n "${{ secrets.JWT_SECRET }}" ]; then
echo "is_configured=true" >> $GITHUB_OUTPUT
if [ "${{ inputs.environment }}" = "production" ]; then
export RENDER_SERVICE_ID="$RENDER_PRODUCTION_SERVICE_ID"
else
echo "is_configured=false" >> $GITHUB_OUTPUT
export RENDER_SERVICE_ID="$RENDER_STAGING_SERVICE_ID"
fi
if [ -z "$RENDER_API_KEY" ] || [ -z "$RENDER_SERVICE_ID" ]; then
echo "ERROR: RENDER_API_KEY and the ${{ inputs.environment }} service id must be configured as GitHub secrets."
exit 1
fi
python scripts/render_deploy.py

# ── Wait for Render auto-deployment ────────────────────────────────
# Render handles the actual physical deployment when you push.
# We just pause the Action to let Render's servers finish building.
- name: Wait for app to initialise
run: |
echo "Waiting 120 seconds for Render to build and start the app..."
sleep 120

# ── Health gate ────────────────────────────────────────────────────
# ── Health gate (against the selected environment's URL) ───────────
- name: Health gate check
id: health_gate
env:
# Use secret URL if provided, otherwise fallback to default
API_URL: ${{ secrets.API_URL || 'https://openshield-api.onrender.com' }}
STAGING_API_URL: ${{ secrets.STAGING_API_URL }}
PRODUCTION_API_URL: ${{ secrets.PRODUCTION_API_URL }}
run: |
if [ "${{ inputs.environment }}" = "production" ]; then
API_URL="$PRODUCTION_API_URL"
else
API_URL="$STAGING_API_URL"
fi
if [ -z "$API_URL" ]; then
echo "ERROR: the ${{ inputs.environment }} API URL secret is not configured."
exit 1
fi
MAX_RETRIES=5
RETRY_DELAY=15
URL="${API_URL}/health"
Expand All @@ -74,32 +117,30 @@ jobs:
sleep $RETRY_DELAY
done

echo "HEALTH GATE FAILED after $MAX_RETRIES attempts"
echo "Note: If you haven't set up Render for this fork, this is expected."
# Only allow failure on feature branches; fail on main/dev
if [[ "${{ github.ref }}" == "refs/heads/main" || "${{ github.ref }}" == "refs/heads/dev" ]]; then
echo "ERROR: Health check failed on protected branch. Deployment verification required."
exit 1
else
echo "Allowing health check failure on feature branch (infra may not be set up)"
exit 0
fi
echo "ERROR: Health gate failed after $MAX_RETRIES attempts on the ${{ inputs.environment }} environment."
exit 1

# ── Smoke tests ────────────────────────────────────────────────────
# ── Smoke tests (optional; against the same environment URL) ───────
- name: Run smoke tests against live deployment
if: steps.check_config.outputs.is_configured == 'true' || github.event_name == 'workflow_dispatch'
if: inputs.run_smoke_tests
env:
API_URL: ${{ secrets.API_URL || 'https://openshield-api.onrender.com' }}
STAGING_API_URL: ${{ secrets.STAGING_API_URL }}
PRODUCTION_API_URL: ${{ secrets.PRODUCTION_API_URL }}
JWT_SECRET: ${{ secrets.JWT_SECRET }}
AZURE_SUBSCRIPTION_ID: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
AZURE_CLIENT_ID: ${{ secrets.AZURE_CLIENT_ID }}
AZURE_CLIENT_SECRET: ${{ secrets.AZURE_CLIENT_SECRET }}
AZURE_TENANT_ID: ${{ secrets.AZURE_TENANT_ID }}
RUN_REAL_SCAN: "true"
run: |
if [[ "${{ github.ref }}" == "refs/heads/main" && -z "${{ secrets.JWT_SECRET }}" ]]; then
echo "ERROR: Cannot run smoke tests on main branch without JWT_SECRET configured"
if [ "${{ inputs.environment }}" = "production" ]; then
export API_URL="$PRODUCTION_API_URL"
else
export API_URL="$STAGING_API_URL"
fi
if [ -z "$JWT_SECRET" ]; then
echo "ERROR: JWT_SECRET is required to run smoke tests against a deployed environment."
exit 1
fi
echo "Running smoke tests against: $API_URL"
python tests/smoke_test.py
python tests/smoke_test.py
Loading
Loading